Home > Tags > Generics

Generics

なぜ Java の配列は共変で、Generics は共変ではないのか

まず、Java の配列がタイプセーフではない話。

public class ExampleArray {

    public static void main(String[] args) {
        String[] strArray = {"test1", "test2"};
        Object[] objArray = strArray; // 配列は共変なので代入可能

        objArray[0] = new Integer(3); // java.lang.ArrayStoreException
    }
}

上記のように、Java の配列は共変という性質を持っているので、Object[] に String[] を代入することができます。つまり、String[] は Object[] のサブクラスである、ということです。
しかし、Generics の場合は、この性質が当てはまりません(Generics は共変ではない)

public class ExampleGenerics {

    public static void main(String[] args) {
        List<String> strList = new ArrayList<String>();
        strList.add("test1");
        strList.add("test2");

        List<Object> objList = new ArrayList<Object>();

        objList = strList; // コンパイルエラー
    }
}

なぜ、配列は共変で、Generics は共変ではないのか?(一緒の方が分かりやすいのに)

その理由の前に、
配列の例で見たように、型の混入現象は、強い型付け言語である Java としては避けたい現象です。
配列の場合は、不正な型が混入した場所で例外(java.lang.ArrayStoreException)を投げてくれます。
(これは、結構大事な所で、使用時(get)ではなく、設定時(set)にきちんと例外を投げてくれると、バグの混入場所が特定しやすいです)
なぜこんなことができるかというと、配列は自分が何の型であるかを自身で知っている(バイトコードに型情報が存在する)ので、違う型を入れたときに、自分と違うということが判定できるのです。

一方、Generics の場合はコンパイル時に型消去という操作が行われます。
型消去については、別エントリーでも書いてます(Generics(Java)の型消去について

Generics のコードは、コンパイル後にはその型情報を一切残していないので、java.lang.ArrayStoreException のような例外を投げることができません。
そのため、共変ではなくして、型の混入を防いでいるのではないかと思います。

Generics で共変っぽいことをしたい場合は、extends とかの境界条件をつければ可能です。

Generics(Java)の型消去について

Java の Generics の実装方式の型消去についてちょっと調べました。

そもそも ジェネリックプログラミングというのは、Java だけにあるものではなく色んな言語に同様の機能があるようです。
ジェネリックプログラミング wikipedia

Java の場合、言語仕様的に、どうやって Generics を実装しているかというと「型消去(type erasure)」によって行われている。
型消去とは、「コンパイル後のバイトコードに型情報を残さない」ということ。

具体的なサンプルコードで見てみる。

public void method15() {
    List<String> list = new ArrayList<String>();
    list.add("string");
    String str = list.get(0);
}

public void method14() {
     List list = new ArrayList();
     list.add("string");
     String str = (String) list.get(0);
}

これを、jad したものが以下。

  public void method15();
     0  new java.util.ArrayList [15]
     3  dup
     4  invokespecial java.util.ArrayList() [17]
     7  astore_1 [list]
     8  aload_1 [list]
     9  ldc <String "string"> [18]
    11  invokeinterface java.util.List.add(java.lang.Object) : boolean [20] [nargs: 2]
    16  pop
    17  aload_1 [list]
    18  iconst_0
    19  invokeinterface java.util.List.get(int) : java.lang.Object [26] [nargs: 2]
    24  checkcast java.lang.String [30]
    27  astore_2 [str]
    28  return

  public void method14();
     0  new java.util.ArrayList [15]
     3  dup
     4  invokespecial java.util.ArrayList() [17]
     7  astore_1 [list]
     8  aload_1 [list]
     9  ldc <String "string"> [18]
    11  invokeinterface java.util.List.add(java.lang.Object) : boolean [20] [nargs: 2]
    16  pop
    17  aload_1 [list]
    18  iconst_0
    19  invokeinterface java.util.List.get(int) : java.lang.Object [26] [nargs: 2]
    24  checkcast java.lang.String [30]
    27  astore_2 [str]
    28  return

確かに、コンパイルされると型情報が消去されて、Java 1.4 時代のものと同じになっている。

なぜ、Java では型消去という実装方式を採用したか?

C++ の場合、同様の機能にテンプレートというものがあるらしく、そちらはコンパイル後も型情報をインライン展開して残すらしいです。
で、Java の場合、なぜ型消去という実装方式を採用したかというと、「後方互換性」のためらしい。

先の例で見たように、コンパイル後のコードが全く同じになるので、1.4 以前で書かれたコードと 1.5 以上で書かれてコードがコードが混在していても実行できる、ということ。

なるほど、よく出来てる。

Home > Tags > Generics

Search
Feeds
Meta

Return to page top