2020/05/02

Java - Generics のワイルドカードについて

Java 総称型における、型パラメータのワイルドカードを正しく理解しましょう。上限境界、下限境界についても説明します。Capture helper idiom についても触れています。

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

型パラメータによる制約に依存しないロジックを記載する際に、型パラメータを ? とした変数を宣言することができます。この ? をワイルドカードと呼びます。型パラメータによる制約が必要ないところでは、可読性などの観点から、極力ワイルドカードを用いて変数を宣言するべきです。

ワイルドカードと型パラメータ無しはどう違う?

型パラメータに ? を指定しても、型が制約されないので、型パラメータが無いのと同じように思えます。
しかし実際には、何らかの型制約が付いている、という情報が維持されています。
例えば、addHeadElementToTail() というメソッドを定義することを考えます。リストの先頭の要素を末尾に追加するという挙動をするメソッドです。単純に、以下のように実装するとどうなるでしょう?

public void addHeadElementToTail(List<?> list) {
  list.add(list.get(0));
}

この場合、List::get の戻り値の型は list の型パラメータから ? となりますが、List::add の引数の型が決まらないため、コンパイルエラーになってしまいます。
そこで、Capture helper idiom と呼ばれる手法が使われます。次のコードを見てください。

public void addHeadElementToTail(List<?> list) {
  addHeadElementToTailHelper(list);
}
private <T> void addHeadElementToTailHelper(List<T> list) {
  list.add(list.get(0));
}

addHeadElementToTailHelper() は型パラメータ T を指定して定義しています。これであれば、List::get の戻り値の型と List::add の引数の型が T で一致していることが要請されていて、正しくコンパイルされます。
その上で、addHeadElementToTail() をワイルドカードで定義します。T のところはどんな型でも構わないので、ワイルドカードにしてしまいます。「何らかの型制約が付いている」けれども、何でもいい、ということになるのです。

ワイルドカードと型パラメータ Object はどう違う?

では今度は、型パラメータに Object を指定した場合との違いは何でしょうか?
ワイルドカードは、何らかの型制約があるが、その型は何でもいいという意味ですが、Object を型パラメータに指定すると、Object 型に型制約されているという意味になります。つまり、型パラメータが Object 型でなければならないのです。
どういうことかというと、Java の総称型は、型パラメータについて共変性がないことと関係しています。
例えば、A と B という2つの型があって、B は A を継承した型であるとします (B extends A)。B 型の値は A 型の変数に代入可能です。
このとき、X<T> という総称型に対して X<B> と X<A> という型パラメータを適用した2つの型を考えると、X<B> 型の値は X<A> 型に代入することはできません。これを共変性が無い、と言います。
コードで示すと、以下の1行目はコンパイルできますが、2行目はコンパイルできません。前述の X を List に、A を Object に、B を String に置き換えて考えると分かります。

List<?> list1 = new ArrayList<String>();
List<Object> list2 = new ArrayList<String>();

ワイルドカードの上限境界

総称型が共変性を持たないことから、以下のようなこともできません。

public static <T> void copy(List<T> dst, List<T> src) {
  dst.addAll(src);
}
...
List<Number> dst;
List<Integer> src;
...
copy(dst, src);

Integer は Number のサブタイプですが、T は Integer か Number のどちらかに束縛されなければいけないので、コンパイルエラーになってしまいます。
やりたいことから考えて、src の中身の型と dst の中身の型は一致している必要はなく、ただ代入可能であれば良いわけです。このようなときに、上限境界付きのワイルドカードを用います。

public static <T> void copy(List<T> dst, List<? extends T> src) {
  dst.addAll(src);
}

? extends T と書くことで、T と等しいことではなく T または T のサブタイプであることを要請することができます。一番上の型でも T ということで、上限境界と呼んでいます。

ワイルドカードの下限境界

逆に T のスーパータイプであることを要請する、下限境界という記法もあります。先程の copy() メソッドは下限境界を使えば、次のように書くこともできます。

public static <T> void copy(List<? super T> dst, List<T> src) {
  dst.addAll(src);
}

? super T と書くことで、T または T のスーパータイプであること、つまり下限境界を T に定めたことになります。

ワイルドカードに関する型階層

raw 型やワイルドカードを絡めた各種総称型について、List と Object, Number, Integer を題材に、代入可否をまとめてみました。
縦軸が代入先の変数の型で、横軸が代入しようとする値の型です。
○は型安全で代入可能、△は型安全ではないが代入可能、×は代入不可を意味しています。

 1 2 3 4 5 6 7 8 9101112
 1 Object
 2 List×




 3 List<?>×










 4 List<Object>×
×
×
×
×
×
×
×
×
×
 5 List<Number>×
×
×
×
×
×
×
×
×
×
 6 List<Integer>×
×
×
×
×
×
×
×
×
×
 7 List<? super Object>×

×

×
×
×
×
×
×
×
 8 List<? extends Object>×










 9 List<? super Number>×

×


×

×
×
×
×
 10 List<? extends Number>×

×
×


×
×
×
×

 11 List<? super Integer>×

×




×

×
×
 12 List<? extends Integer>×

×
×
×

×
×
×
×
×

階層構造で書くと、以下のようになります。

Object
├List
│└List<?>
└List<?>
  ├List
  └List<? extends Object>
    ├List<? extends Number>
    │├List<? extends Integer>
    ││└List<Integer>
    │└List<Number>
    └List<? super Integer>
      ├List<? super Number>
      │├List<? super Object>
      ││└List<Object>
      │└List<Number>
      └List<Integer>