JPCERT コーディネーションセンター

OBJ03-J. ジェネリックな未加工型とジェネリックでない未加工型を新規コードに混在させない

レガシーコードと新たに書かれたコードとの互換性を確保するためであれば、ジェネリック型に型付けされたコードを、未加工型(raw type)とともに自由に使用してよい。ジェネリック型に型付けされたコードで未加工型を用いると、Javaコンパイラは"未チェック"警告を発するが、コードはコンパイルされる。ジェネリック型とジェネリックでない型を正しく同時に使用するならば、これらの警告は無視できるだろう。しかしそれ以外の場合、警告は危険な操作の存在を知らせるものかもしれない。

Java言語仕様[JLS 2005]§4.8「未加工型(Raw Types)」には以下のように規定されている。

未加工型は、レガシーコードとの互換性を確保する手段としてのみ使用してもよい。プログラミング言語Javaにジェネリクスが導入された後に書かれたコードでは、未加工型を使用すべきでない。未来のバージョンのプログラミング言語Javaでは、未加工型の使用が禁止される可能性がある。

パラメータ化された型(parameterized type)が、パラメータ化された型ではないオブジェクトにアクセスしようとした場合、ヒープ汚染が発生する。以下のコードを例に考えてみよう。

List l = new ArrayList();
List<String> ls = l; // "未チェック"警告を出す

"未チェック"警告のみに頼って本ルールの違反を検出するのは不十分である。Java言語仕様 [JLS 2005] §4.12.2.1「ヒープ汚染」には以下のように規定されている。

このことは、"未チェック"警告が実際に発生した場合にしかヒープ汚染は発生しないということを意味するわけではない点に注意。プログラミング言語Javaの旧バージョン用コンパイラや、"未チェック"警告を抑止できるコンパイラを用いて一部のバイナリがコンパイルされているということもあり得る。こういった慣習は有害であると言える。

レガシークラスを拡張しオーバーライドするメソッドをジェネリック化することは、Java言語仕様上認められていない。[JLS 2005]

違反コード

以下の違反コードは、コンパイルは通るが"未チェック" 警告が出る。これは、パラメータ化された型ではなく、未加工型であるList.add() メソッドを使用しているためである。

class ListUtility {
  private static void addToList(List list, Object obj) {
    list.add(obj); // "未チェック" 警告を出す
  }

  public static void main(String[] args) {
    List<String> list = new ArrayList<String> ();
    addToList(list, 1);
    System.out.println(list.get(0));
  }
}

このコードを実行すると例外がスローされる。その理由は、List<String>Integerを受け取ったからではなく、list.get(0)の返り値が適切な型ではないからである(StringではなくIntegerである)。コードが例外をスローするのは、実際に例外を引き起こす処理を行ってしばらくしてからであるため、デバッグは困難になる。

適合コード (パラメータ化したコレクション)

以下の適合コードは、addToList()メソッドで型チェックが行われるようにメソッドのシグネチャを変更することで、型安全性を強化している。

class Parameterized {
  private static void addToList(List<String> list, String str) {
    list.add(str);     // 警告は出ない
  }

  public static void main(String[] args) {
    List<String> list = new ArrayList<String> ();
    addToList(list, "1");
    System.out.println(list.get(0));
  }
}

パラメータ化されたlistへのObjectの追加はコンパイルエラーになる。これは addToList()を型の一致しない引数で呼び出すことができないからである。したがって、IntegerではなくStringをリストに追加するようにコードを修正している。

適合コード (レガシーコード)

前述の適合コードでは未加工型のコレクション(raw collection)の使用を排除したが、レガシーコードとやりとりするプログラムでこれを実装するのは不可能であろう。

addToList()メソッドが変更不可能なレガシーコードであると仮定する。以下の適合コードでは、Collections.checkedList()メソッドを使用し、リストのビュー(checked view)を作成している。このメソッドが返すラッパーコレクションでは、バックエンドのList<String>に処理を委譲する前にadd()メソッドのなかで実行時型チェックを行う。ラッパーコレクションはレガシーのaddToList()メソッドに安全に渡すことができる。

class ListUtility {
  private static void addToList(List list, Object obj) {
    list.add(obj); // "未チェック" 警告を出す
  }

  public static void main(String[] args) {
    List<String> list = new ArrayList<String> ();
    List<String> checkedList = Collections.checkedList(list, String.class);
    addToList(checkedList, 1);
    System.out.println(list.get(0));
  }
}

このコードでもコンパイラは"未チェック"警告を発するが、これは無視できる。しかし、Integerをリストに追加しようとするとエラーになるため、プログラムが無効なデータを処理することを防止できる。

違反コード

以下の違反コードは、未加工型のList.add()メソッドが発する"未チェック"警告を抑止しているため、問題なくコンパイルが通り、実行される。printOne メソッドは、変数type の型に応じて、int 型あるいは double 型で値 1 を出力することを意図している。

class ListAdder {
  @SuppressWarnings("unchecked")
  private static void addToList(List list, Object obj) {
    list.add(obj);     // 未チェック警告
  }

  private static <T> void printOne(T type) {
    if (!(type instanceof Integer || type instanceof Double)) {
      System.out.println("Cannot print in the supplied type");
    }
    List<T> list = new ArrayList<T>();
    addToList(list, 1);
    System.out.println(list.get(0));
  }

  public static void main(String[] args) {
    double d = 1;
    int i = 1;
    System.out.println(d);
    ListAdder.printOne(d);
    System.out.println(i);
    ListAdder.printOne(i);
  }
}

listは正しくパラメータ化されているにもかかわらず、このメソッドは 1.0 ではなく、1 を出力する。これは、intの値 1 が型チェックされずに listに追加されるからである。このコードは以下を出力する。

1.0
1
1
1
適合コード

以下の解決法ではaddToList()メソッドをジェネリック化し、型違反を一掃している。

class ListAdder {
  private static <T> void addToList(List<T> list, T t) {
    list.add(t);     // 警告は出ない
  }

  private static <T> void printOne(T type) {
    if (type instanceof Integer) {
      List<Integer> list = new ArrayList<Integer>();
      addToList(list, 1);
      System.out.println(list.get(0));
    }
    else if (type instanceof Double) {
      List<Double> list = new ArrayList<Double>();

      // addToList(list, 1) とするとコンパイルは通らない
      addToList(list, 1.0);

      System.out.println(list.get(0));
    }
    else {
      System.out.println("Cannot print in the supplied type");
    }
  }

  public static void main(String[] args) {
    double d = 1;
    int i = 1;
    System.out.println(d);
    ListAdder.printOne(d);
    System.out.println(i);
    ListAdder.printOne(i);
  }
}

コードは問題なくコンパイルされ、以下を出力する。

1.0
1.0
1
1

addToList()メソッドが外部で定義されており変更できない場合(たとえばライブラリやコールバックメソッドなど)、前述のprintOne()メソッドと同様の手法が使えるが、addToList(1.0)の代わりにaddToList(1)を使用すると警告は出ない。ジェネリックなコードとジェネリックでないコードが混在する場合、型安全性には十分注意する必要がある。

例外

OBJ03-EX0: クラスリテラルでは未加工型を用いなければならない。たとえば、List<Integer>.classは無効であるが、未加工型のList.classであれば問題ない。[Bloch 2008]

OBJ03-EX1: instanceofオペレータをジェネリック型に用いることはできない。その場合は、ジェネリックなコードと未加工型のコードを混在させてもよい[Bloch 2008]。

if(o instanceof Set) { // 未加工型
  Set<?> m = (Set<?>) o; // ワイルドカード型
  // ...
}
リスク評価

ジェネリック型とジェネリックでない型を混在させると、予期せぬ結果や例外につながる恐れがある。

ルール 深刻度 可能性 修正コスト 優先度 レベル
OBJ03-J P4 L3
参考文献
[Bloch 2008] Item 23. Don't use raw types in new code
[Bloch 2007]
[Bloch 2005] Puzzle 88. Raw Deal
[Darwin 2004] 8.3, Avoid Casting by Using Generics
[JavaGenerics 2004]  
[JLS 2005] Chapter 5. Conversions and Promotions
  §4.8. Raw Types
  §5.1.9. Unchecked Conversion
[Langer 2008] Topic 3, Coping with Legacy
[Naftalin 2006] Chapter 8, Effective Generics
[Naftalin 2006b] Principle of Indecent Exposure
[Schildt 2007] Create a checked collection
翻訳元

これは以下のページを翻訳したものです。

OBJ03-J. Do not mix generic with non-generic raw types in new code (revision 86)

Top へ

Topへ
最新情報(RSSメーリングリストTwitter