非具象化可能仮パラメータを可変長引数メソッドに使用する場合のコンパイラの警告とエラーの改善

このページでは、次のトピックについて説明します。

ヒープ汚染

ArrayList<Number>List<String> など、パラメータ化された型のほとんどは、非具象化可能型です。非具象化可能型は、実行時に完全には使用可能にならない型です。非具象化可能型は、コンパイル時に型消去と呼ばれる処理を受けます。この処理では、型パラメータと型引数に関する情報がコンパイラによって削除されます。これにより、ジェネリクスより前に作成された Java ライブラリおよびアプリケーションとのバイナリ互換性が保証されます。パラメータ化された型は、その情報が型消去によってコンパイル時に削除されるため、非具象化可能です。

ヒープ汚染は、パラメータ化された型の変数が、そのパラメータ化された型ではないオブジェクトを参照するときに発生します。この状態が発生する可能性があるのは、コンパイル時に非チェック警告を発生させるような操作がプログラムで実行された場合だけです。非チェック警告は、コンパイル時 (コンパイル時の型チェック規則の範囲内で) または実行時に、パラメータ化された型を使用する操作 (キャストやメソッド呼び出しなど) が正しいかどうかを確認できない場合に生成されます。

次に例を示します。

    List l = new ArrayList<Number>();
    List<String> ls = l;       // unchecked warning
    l.add(0, new Integer(42)); // another unchecked warning
    String s = ls.get(0);      // ClassCastException is thrown

型の消去中に、型 ArrayList<Number> および List<String> はそれぞれ ArrayList および List になります。

変数 ls はパラメータ化された型 List<String> を持ちます。l によって参照される Listls に代入されると、コンパイラは未検査警告を生成します。コンパイラは、lList<String> 型を参照するかどうか (実際には参照しない) をコンパイル時に判断できません。また、JVM が実行時にそれを判断できないこともわかっています。その結果、ヒープ汚染が発生します。

結果として、コンパイル時に、別の非チェック警告が add 文で生成されます。コンパイラは、変数 lList<String> 型または List<Integer> 型を参照するかどうかを判断できません (そして別のヒープ汚染状態が発生します)。ただし、コンパイラは get 文では警告やエラーを生成しません。これは List<String>.get メソッドを呼び出して String オブジェクトを取得している、有効な文です。その代わり、この get 文は実行時に ClassCastException をスローします。

詳しく説明すると、ヒープ汚染状態が発生するのは、List オブジェクト l (その static 型は List<Number>) が別の List オブジェクト ls (異なる static 型 List<String> を持つ) に代入される場合です。しかし、コンパイラはこの代入をいまだに許可しています。この代入を許可する必要があるのは、ジェネリクスをサポートしない Java SE のバージョンとの下位互換性を確保するためです。型消去のために、List<Number>List<String>List になります。その結果、コンパイラはオブジェクト l (List という raw 型を持つ) をオブジェクト ls に代入することを許可します。

さらに、l.add メソッドが呼び出されたときにヒープ汚染状態が発生します。add メソッドの 2 番目の static 型仮パラメータは String ですが、このメソッドは異なる型 (Integer) の実パラメータで呼び出されています。しかし、コンパイラはこのメソッド呼び出しをまだ許可します。型消去のために、add メソッドの 2 番目の仮パラメータの型 (List<E>.add(int,E) として定義されている) は Object になります。その結果、型消去のあとで l.add メソッドは Object 型の任意のオブジェクトを追加でき、Object のサブ型である Integer 型のオブジェクトもこれに含まれます。そのため、コンパイラはこのメソッド呼び出しを許可します。

可変長引数メソッドと非具象化可能仮パラメータ

次の例のメソッド ArrayBuilder.addToList について考えます。これは、elements という可変長引数仮パラメータに格納された T 型のオブジェクトを List listArg に追加する、可変長引数 (varargs) メソッドです。

import java.util.*;

public class ArrayBuilder {

  public static <T> void addToList (List<T> listArg, T... elements) {
    for (T x : elements) {
      listArg.add(x);
    }
  }

  public static void faultyMethod(List<String>... l) {
    Object[] objectArray = l;  // Valid
    objectArray[0] = Arrays.asList(new Integer(42));
    String s = l[0].get(0);    // ClassCastException thrown here
  }

}
import java.util.*;

public class HeapPollutionExample {

  public static void main(String[] args) {

    List<String> stringListA = new ArrayList<String>();
    List<String> stringListB = new ArrayList<String>();

    ArrayBuilder.addToList(stringListA, "Seven", "Eight", "Nine");
    ArrayBuilder.addToList(stringListA, "Ten", "Eleven", "Twelve");
    List<List<String>> listOfStringLists = new ArrayList<List<String>>();
    ArrayBuilder.addToList(listOfStringLists, stringListA, stringListB);

    ArrayBuilder.faultyMethod(Arrays.asList("Hello!"), Arrays.asList("World!"));
  }
}

Java SE 7 のコンパイラは、メソッド ArrayBuilder.addToList の定義に関する次の警告を生成します。

warning: [varargs] Possible heap pollution from parameterized vararg type T

コンパイラが可変引数メソッド検出すると、可変引数仮パラメータを配列に変換します。しかし、Java プログラミング言語では、パラメータ化された型の配列の作成は許可されません。メソッド ArrayBuilder.addToList では、コンパイラは varargs の仮パラメータ T... elements を仮パラメータ T[] elements に (配列) 変換します。しかし、型消去のために、コンパイラは varargs の仮パラメータを Object[] elements に変換します。その結果、ヒープ汚染が発生する可能性があります。詳細については、次のセクション「非具象化可能仮パラメータを持つ可変長引数メソッドの潜在的な脆弱性」を参照してください。

:Java SE 5 および 6 のコンパイラは、ArrayBuilder.addToList が呼び出されたときにこの警告を生成します。この例では、警告はクラス HeapPollutionExample に対して生成されます。これらのコンパイラは、宣言箇所ではこの警告を生成しません。ただし、Java SE 7 は、宣言箇所と呼び出し箇所の両方でこの警告を生成します (警告が注釈によって阻止されている場合は除く。詳細は、「非具象化可能仮パラメータを持つ可変長引数メソッドからの警告の抑制」を参照)。非具象化可能可変長引数仮パラメータを持つ可変長引数メソッドが (呼び出し箇所ではなく) 宣言箇所で見つかったときにコンパイラで警告を生成する利点は、呼び出し箇所は多数存在する可能性があるのに対し、宣言箇所は 1 つしかないことです。

非具象化可能仮パラメータを持つ可変長引数メソッドの潜在的な脆弱性

メソッド ArrayBuilder.faultyMethod を見れば、このようなメソッドについてコンパイラが警告する理由がわかります。このメソッドの最初の文は、可変長引数仮パラメータ lObject 配列 objectArgs に代入しています。

    Object[] objectArray = l;

この文はヒープ汚染を招く可能性があります。可変長引数仮パラメータ l のパラメータ化された型に一致する値は、変数 objectArray に代入でき、したがって l に代入できます。ただし、コンパイラはこの文では非チェック警告を生成しません。コンパイラは、可変長引数仮パラメータ List<String>... l を仮パラメータ List[] l に変換したときに、すでに警告を生成しています。変数 l の型は List[] で、これは Object[] のサブ型なので、この文は有効です。

その結果、次の文のように、任意の型の List オブジェクトを objectArray 配列のどの配列要素に代入しても、コンパイラは警告やエラーを生成しません。

    objectArray[0] = Arrays.asList(new Integer(42));

この文は、Integer 型のオブジェクトを 1 つ含む List オブジェクトを、objectArray 配列の最初の配列要素に代入しています。

ArrayBuilder.makeArray メソッドを次の文で呼び出すとします。

    ArrayBuilder.faultyMethod(Arrays.asList("Hello!"), Arrays.asList("World!"));

実行時に、JVM は次の文で ClassCastException をスローします。

    String s = l[0].get(0);    // ClassCastException thrown here

変数 l の最初の配列要素に格納されているオブジェクトの型は List<Integer> ですが、この文は List<String> 型のオブジェクトを想定しています。

非具象化可能仮パラメータを持つ可変長引数メソッドからの警告の抑制

パラメータ化されたパラメータを持つ可変長引数メソッドを宣言する場合、メソッドの本体で可変長引数仮パラメータの処理が適切でない (ArrayBuilder.faultyMethod メソッドを参照) ために ClassCastException や類似の例外がスローされることがないとわかっていれば、次のオプションのいずれかを使用して、このような可変長引数メソッドに対してコンパイラから生成される警告を抑制できます。

たとえば、次に示すバージョンの ArrayBuilder クラスには、addToList2 および addToList3 という 2 つの追加メソッドがあります。

public class ArrayBuilder {

  public static <T> void addToList (List<T> listArg, T... elements) {
    for (T x : elements) {
      listArg.add(x);
    }
  }

  @SuppressWarnings({"unchecked", "varargs"})
  public static <T> void addToList2 (List<T> listArg, T... elements) {
    for (T x : elements) {
      listArg.add(x);
    }
  }

  @SafeVarargs
  public static <T> void addToList3 (List<T> listArg, T... elements) {
    for (T x : elements) {
      listArg.add(x);
    }
  }

  // ...

}
public class HeapPollutionExample {

  // ...

  public static void main(String[] args) {

    // ...

    ArrayBuilder.addToList(listOfStringLists, stringListA, stringListB);
    ArrayBuilder.addToList2(listOfStringLists, stringListA, stringListB);
    ArrayBuilder.addToList3(listOfStringLists, stringListA, stringListB);

    // ...

  }
}

Java コンパイラはこの例に対して次の警告を生成します。

:Java SE 5 および 6 では、非具象化可能可変長引数仮パラメータを持つ可変長引数メソッドの呼び出しによってヒープ汚染が発生するかどうかは、その呼び出しを行うプログラマが判断する必要があります。しかし、このプログラマがそのメソッドの作成者でない場合は、これを簡単に判断することはできません。Java SE 7 では、このような可変長引数メソッドで可変長引数仮パラメータが適切に処理されること、およびヒープ汚染が発生しないことを保証するのは、メソッドを作成するプログラマの責任です。


Copyright © 1993, 2013, Oracle and/or its affiliates. All rights reserved.