シグネチャに、T とか、K とか書いてあると API そっ閉じしちゃうジェネリクス フォビア向けに、Java の総称型を説明してみた
総称型(ジェネリクス)とは
それまでは、Object 型で書かれていた型指定に対して、利用する型を限定できるように Java1.5 より、総称型が導入された。 主に Collection に対して導入されているので、実際に API を見てみる方が理解が早いと思う。 Java1.4 ArrayList.html#get(int)) では、次のように API が定義されている。 注目して欲しいのは、List に対する get の戻り値で Object 型で定義されている。つまり、Java1.4 までは、取り出してみるまで何が入っているかわからないびっくり箱のようなものだった。
get public Object get(int index) リスト内の指定された位置にある要素を返します。 定義: インタフェース List 内の get 定義: クラス AbstractList 内の get パラメータ: index - 返される要素のインデックス 戻り値: リスト内の指定された位置にある要素 例外: IndexOutOfBoundsException - インデックスが範囲外の場合 (index < 0 || index >= size())
次に、Java1.5 ArrayList.html#get(int)) の同じ API を見てみよう。 同様に、戻り値に注目すると、E 型が返却されている。これが、総称型である。E で定義されているため、何か1つの型であることが確定している。戻される値は、E であり、T や、V ではないのである。
get public E get(int index) リスト内の指定された位置にある要素を返します。 定義: インタフェース List<E> 内の get 定義: クラス AbstractList<E> 内の get パラメータ: index - 返される要素のインデックス 戻り値: リスト内の指定された位置にある要素 例外: IndexOutOfBoundsException - インデックスが範囲外の場合 (index < 0 || index >= size())
総称型の書き方
対象毎に総称型の書き方を説明する。
クラス
@Data public static class GenericsSample<T> { private T hoge; } @Test public void class_test() { GenericsSample<String> sample = new GenericsSample<>(); sample.setHoge("hoge!"); assertThat(sample.getHoge()).isEqualTo("hoge!"); }
クラス名の後ろに総称型を書き、インスタンス生成時に型を指定する。
メソッド
public static <T> T getValue(final T val, final T defaultValue) { return val == null ? defaultValue : val; } @Test public void method_test() { assertThat(getValue(null, "hoge")).isEqualTo("hoge"); assertThat(getValue(null, 1)).isEqualTo(1); assertThat(getValue("hoge", 1)).isEqualTo("hoge"); // Object として判断されている }
メソッドに総称型を適用させる場合は、戻り値の直前に総称型を定義する。この場合、型を明示的に与える必要は無く、引数から推測され定義される。このため、同一の総称型に複数の型を与えると、Object として処理される。
また、メソッドに限ったことでは無いが、総称型は複数定義することもできる。
public static <T, V> String getValueB(final T val, final V defaultValue) { return val == null ? defaultValue.toString() : val.toString(); } @Test public void method_test() { assertThat(getValueB(null, 2)).isEqualTo("2"); assertThat(getValueB("hoge1", 2)).isEqualTo("hoge1"); assertThat(getValueB("hoge1", "hoge2")).isEqualTo("hoge1"); // T,V が、同じ型でも OK }
コンストラクタ
コンストラクタに対しても、総称型を宣言できる。
@Data public static class GenericsA { public <T> GenericA(T args) { System.out.println(args); } } @Test public void constructor_test() { GenericsA genericA = new GenericsA("hoge"); }
しかし、クラスに適用したときとは異なり、メンバで利用するなどはできない。
@Data public static class GenericsA { private T hoge; // ERROR!! T cannot be resolved to a type public <T> GenericsA(T args) { System.out.println(args); } }
総称型のネーミング
総称型の変数名は、特に定められていないが、基本的に1字で定義し、下記の慣例がある。
- E ・・・ Element(要素)
- K ・・・ Key(キー)
- V ・・・ Value(値)
- T ・・・ Type(タイプ)
- N ・・・ Number(数値)
- S,U ・・・ 2,3番目
ワイルドカード(非境界ワイルドカード)
ワイルドカードを利用すると、実行されるまで型が不明なものを表すことができる。それなら、今までの仮型パラメータでできるのでは無いかと思った人は実際に手を動かしてコードを書いてみて欲しい。次のメソッドは仮型パラメータに対して型が指定されていないため、何れもエラーとなる。
private <T> T getList(boolean flag) { return flag ? new ArrayList<String>() : new ArrayList<Integer>(); // ERROR!! Type mismatch: cannot convert from ArrayList<Integer> to T } private <T> List<T> getList(boolean flag) { return flag ? new ArrayList<String>() : new ArrayList<Integer>(); // ERROR!! Type mismatch: cannot convert from ArrayList<Integer> to T }
そこでワイルドカードを利用することにより、実行されるまで型が不明なものを表現する。また、この時取得される値は、Object型となる。
private List<?> getList(boolean flag) { return flag ? Arrays.asList("1", "2") : Arrays.asList(1, 2); } @Test public void wildcard_test() { Object hoge = getList(false).get(0); // Integer hoge = getList(false).get(0); // ERROR!! Type mismatch: cannot convert from capture#13-of ? to Intege System.out.println(hoge); }
また、ワイルドカードを利用した List に、add することはできない。これは、取り出すときを考えてみると理解し易いと思う。どんな型のListでも定義できるのであり、何でも入る List というわけでは無い。
@Test public void wildcard_list_add_test() { List<?> wildcardList = new ArrayList<>(); List<Number> numList = new ArrayList<>(); List<Integer> intList = new ArrayList<>(); numList.add(new Double(1)); numList.add(new Integer(1)); intList.add(new Double(1)); // ERROR!! The method add(Integer) in the type List<Integer> is not applicable for the arguments (Double) intList.add(new Integer(1)); wildcardList.add(new Double(1)); // ERROR!! The method add(capture#13-of ?) in the type List<capture#13-of ?> is not applicable for the arguments (Double) wildcardList.add(new Integer(1)); // ERROR!! The method add(capture#14-of ?) in the type List<capture#14-of ?> is not applicable for the arguments (Integer) }
extends と super(境界ワイルドカード)
ワイルドカードに、上下限を付けることができる。
<? extends T>(上限付き境界ワイルドカード)
extends を利用することで、ワイルドカードに上限を設けることができる。例えば、<? extends Number> であれば、Number のサブタイプであることが保証されることになる。下記の例の場合、ワイルドカードの指定では、Object 型でしか受けられないが、extends の場合は、Number であることが保証されるため、Number で受けることが可能になる。
@Test public void wildcard_extends_list_test() { List<?> wildcardList = Arrays.asList(new Integer(1), new Integer(2)); List<? extends Number> numList = Arrays.asList(new Integer(1), new Integer(2)); // Number wildcardNum = wildcardList.get(0); // ERROR!! Type mismatch: cannot convert from capture#13-of ? to Number Object wildcardNum = wildcardList.get(0); Number number = numList.get(0); }
<? super T>(下限付き境界ワイルドカード)
上限同様、下限を設けることもできる。例えば、<? super Integer> とすれば、Integer がスーパータイプであることが保証される。下記の例の場合、スーパータイプが、Integer であることが保証されるため、Double をセットすることはできない。
@Data public static class GenericSample<T> { private T hoge; } @Test public void wildcard_super_list_test() { GenericSample<? super Integer> superSample = new GenericSample<>(); // superSample.setHoge(new Double(1)); // The method setHoge(capture#15-of ? super Integer) in the type GenericTest.GenericSample<capture#15-of ?super Integer> is not applicable for the arguments (Double) superSample.setHoge(new Integer(1)); }
まとめ
ジェネリクスとは、利用する型を限定させるもので、クラスやメソッドに宣言できる。ネーミングには慣例がある。ジェネリクス フォビアが躊躇するのは、たぶん、ワイルドカードじゃないかと思う。使うのは置いといても、知っていれば API そっ閉じは無くなるんじゃないだろうか。
「変性」と、「PECS」については、長くなってしまったし、使う側のお話なのでまたの機会に。