カタカタブログ

SIerで働くITエンジニアがカタカタした記録を残す技術ブログ。Java, Oracle Database, Linuxが中心です。たまに数学やデータ分析なども。

Java ジェネリクスについて勉強したことのまとめ

Javaのジェネリクスについて理解が浅かったため、今回Effective Java第2版を読んで勉強してみたことをまとめる。
ジェネリクスはJava SE 1.5から導入されたものだが、今回はJava SE 8の環境で検証している。

EFFECTIVE JAVA 第2版 (The Java Series)

EFFECTIVE JAVA 第2版 (The Java Series)

ジェネリクスとは

ジェネリクスはあるクラスを作る上で、型の情報を汎用的に持たせるためのJava SE 6からの仕組みである。
特にリストやハッシュ等のコレクションは任意の型のインスタンスをまとめることができるが、異なる型の要素を持ってはならないという制約がある。この場合、ジェネリクスを使って型情報を汎用化することで、コンパイラが型の安全性を保証できるようになっている。

ジェネリクスのクラスを宣言するときは以下の構文を使う。
Tの部分を型パラメータと言う。型パラメータ名は大文字一文字で、要素に使う型であればE、返り値に使う型であればR、それ以外であればT、複数使うのであればU、などを使うのが一般的のよう。

class MyClass<T> {
    // 略
}

ジェネリクスとコレクション

これを自前の簡単なリストMyListクラスを作って確認する。要素は内部で配列を使って管理する。
まずジェネリクスを使わない場合、任意の型の要素を扱うためにはObjectクラスの配列を宣言する必要があるが、これだと異なる型をリストに混在できてしまうのでよくない。

・ジェネリクスを使わないMyListクラス

class MyList {
     private Object[] elements;
     private static final int MAX = 256;
     private int size = 0;
     public MyList() {
          elements = new Object[MAX];
     }
     public void add(Object e) {
          elements[size++] = e;
     }
     public Object get(int index) {
          return elements[index];
     }
     public int size() {
          return this.size;
     }
}

これは以下のようにStringとint(Integer)が混在するリストを許すことになってしまう。この場合、リストから要素を取り出して使うには、Object型から実際の型にキャストしないといけないが、型が混在している可能性があるため、実行時にキャストエラーが発生してしまう恐れがある。このため、このリストは型安全とは言えないリストとなる。

MyList list = new MyList();
list.add("abc");
list.add("def");
list.add(123);
for(int i=0; i<list.size(); i++) {
     System.out.println(list.get(i));
}

そこでジェネリクス(総称型)を使って型情報をパラメータ化する。
リストに格納する要素の型を型パラメータEとして明示して使うことができるようになる。

・ジェネリクスを使ったMyListクラス

class MyList<E> {
     private E[] elements;
     private static final int MAX = 256;
     private int size = 0;
     public MyList() {
          @SuppressWarnings("unchecked")
          E[] es = (E[])new Object[MAX];
          elements = es;
     }
     public void add(E e) {
          elements[size++] = e;
     }
     public E get(int index) {
          return elements[index];
     }
     public int size() {
          return this.size;
     }
}

こうすると、リスト変数宣言時に型パラメータを指定できるので、
それ以外の型の要素をリストに入れようとするとコンパイルエラーとなる。

MyList<String> list = new MyList<>();
list.add("abc");
list.add("def");
list.add(123); //★コンパイルエラー: The method add(String) in the type MyList<String> is not applicable for the arguments (int)
for(int i=0; i<list.size(); i++) {
     System.out.println(list.get(i));
}

ちなみに、MyListのコンストラクタで以下のようにキャストしている箇所がある。

@SuppressWarnings("unchecked")
E[] es = (E[])new Object[MAX];
elements = es;

ジェネリクスはコンパイル時のみに使う情報で、実行時には消えてしまっているので、型変数のクラスや配列をnewすることはできない。elements = new E[MAX];と書きたいところだがコンパイルエラーとなってしまう。
そこで、Object型の配列で宣言したあとでE[]にキャストしている。ただしこれはコンパイラレベルでは型安全であることを保証できないため、以下の警告が出る。

Type safety: Unchecked cast from Object[] to E[]

しかし、今回のケースでは空のObject型配列をnewしてインスタンス変数のE[] elementsに代入するだけなのでキャストエラーが発生することはない。そのため、コンパイラに型安全であることを伝えて警告を非表示にするために、以下のアノテーションを変数宣言時に付与している。

@SuppressWarnings("unchecked”)

このようにジェネリクスを使うことで、型安全なコレクションクラスを使うことができる。

ジェネリクスの不変性

まず、共変(covariant)と不変(invariant)の定義を確認しておく。
共変は、より広義の型から狭義の型へ変換できる・互換性があることを意味している。
一方、不変は広義の型と狭義の型への変換はできず、互換性がないことを意味している。
これは配列は共変で、ジェネリクスは不変である、という意味を理解すると分かりやすい。例えば、Superクラスを継承したクラスをSubクラスとすると、
配列Super[]と配列Sub[]は継承関係があり共変であるが、リストList<Super>とリストList<Sub>は継承関係はなく不変である。

そのことを以下のコードで確認する。

public class Work {
     public static void main(String[] args) {
          Super[] supers = new Super[3];
          Super[] subs = new Sub[3];

          List<Super> superList = new ArrayList<Super>();
          List<Super> subList = new ArrayList<Sub>(); //コンパイルエラー: Type mismatch: cannot convert from ArrayList<Sub> to List<Super>
     }
}
class Super {}
class Sub extends Super {}

Super[]で宣言した配列にはnew Sub[]を代入することができる。
一方で、List<Super>new ArrayList<Sub>を代入すると、型不一致によるコンパイルエラーとなる。

ただし注意点として、この不変性はあくまでジェネリクスとしてList<E>クラスを扱うときの問題であり、要素を扱う場合はポリモーフィズムを活用できる。
つまり、以下のようにSuperクラスのリストにSubクラスのインスタンスを要素として持つことは問題なく行える。

・コンパイルも実行も正常にできるコード

public class Work {
     public static void main(String[] args) {
          MyList<Super> superList = new MyList<>();
          superList.add(new Sub());
     }
}

型パラメータのワイルドカード

ジェネリクスは不変であるため、Listの要素の継承関係、つまりポリモーフィズムを有効に使うためには別の仕組みを用いる必要がある。
例えば、さきほどのMyListに別のリストを引数にとって、そのリストの全要素を自身のリストに加えるgetAndAddAllというメソッドを考える。
上で見たように、superListはSuperクラスをのインスタンスを要素として保持するリストなので、Subクラスのインスタンスを格納することは問題なく行える。
しかしgetAndAddAll(MyList<E> list)の引数listには不変性が働くので、MyList<Sub>を引数に与えるとコンパイルエラーとなる。

・コンパイルエラーとなるコード

public class Work {
     public static void main(String[] args) {
          MyList<Super> superList = new MyList<>();
          MyList<Sub> subList = new MyList<>();
          subList.add(new Sub());
          superList.getAndAddAll(subList);//コンパイルエラー: The method getAndAddAll(MyList<Super>) in the type MyList<Super> is not applicable for the arguments (MyList<Sub>)
      }
}
class Super {}
class Sub extends Super {}
class MyList<E> {
        // 以下のメソッドを追加(それ以外はこれまでと同じなので省略)
     public void getAndAddAll(MyList<E> list) {
          for(int i = 0; i < list.size(); i++) {
               add(list.get(i));
          }
     }
}

そこで、このgetAndAddAllメソッドにMyListを与えて実行できるようにするためには、型パラメータにワイルドカードを使ってgetAndAddAll(MyList<? extends E> list)と指定する。
こうすると、引数に型パラメータを継承したジェネリクス・クラスを指定できるようになる。

・コンパイル・実行可能なコード

public class Work {
     public static void main(String[] args) {
          MyList<Super> superList = new MyList<>();
          MyList<Sub> subList = new MyList<>();
          subList.add(new Sub());
          superList.getAndAddAll(subList);
      }
}
class Super {}
class Sub extends Super {}
class MyList<E> {
        // メソッドの型パラメータを変更(それ以外はこれまでと同じなので省略)
     public void getAndAddAll(MyList<? extends E> list) {
          for(int i = 0; i < list.size(); i++) {
               add(list.get(i));
          }
     }
}

同様に、今度は引数のlistに自身の要素をすべて足し込むputAllというメソッドを考える。
これはさきほどとは逆に、引数として与える型は型パラメータEもしくはそのスーパークラスという意味で、putAll(MyList)<? super E> listと書くと意図通りのコードになる。

・コンパイル・実行可能なコード

public class Work {
     public static void main(String[] args) {
          MyList<Super> superList = new MyList<>();
          MyList<Sub> subList = new MyList<>();
          subList.add(new Sub());
          subList.putAll(superList);
     }
}
class Super {}
class Sub extends Super {}
class MyList<E> {
        // 以下のメソッドを追加(それ以外はこれまでと同じなので省略)
     public void putAll(MyList<? super E> list) {
          for(int i = 0; i < list.size(); i++) {
               list.add(this.get(i));
          }
     }
}

Get&Put原則

? extends E? super Eのどちらを使うべきかを迷った場合はGet&Put原則という考え方がある。
値を取得するだけの場合、つまりgetのときは? extends Eを使い、
値を更新するだけの場合、つまりputのときは? super Eを使い、
その両方を行う場合は、?(つまり任意の型を扱える)を使う、というものである。

まとめ

Javaのコレクションを使う上で便利なジェネリクスについて、扱い方をまとめた。
Listを使う程度であれば普段はあまり意識することなく便利に利用できるジェネリクスだが、ライブラリ開発などでジェネリクスを活用するクラスを作りたい場合は、ちゃんと使い方を知っておく必要がある。
コレクションだけでなく、Java SE 8からのラムダ式や関数型インタフェースでもジェネリクスはよく見かけるので、型パラメータの扱い方を理解しておくこと、扱う上でも正しく使えるようになると思う。

参考文献

Effective Java 第2版 第5章「ジェネリクス」

EFFECTIVE JAVA 第2版 (The Java Series)

EFFECTIVE JAVA 第2版 (The Java Series)

以上