Java Generics Hell アドベントカレンダー 16日目。
- 前回(15日目) ワイルドカード落穂ひろい
読者の推奨スキルとしてはOCJP Silverぐらいを想定している。
メソッドスコープの型変数
メソッドスコープの型変数へのバインドについては以前も書いたが再掲しておこう。
使用する場合は、通常はバインド型が推論されるのでそのまま呼び出せば済む。
Set<String> set = Collections.singleton("hoge");
型変数にバインドする型を明示する場合はメソッドの手前に山括弧で指定する。
Set<String> set = Collections.<String>singleton("hoge");
メソッドスコープの型変数については明示的にバインドしなくても概ね型が推論される点は知っておくと良い。これはジェネリクスが導入されたJava5当初からある機能だ。「型推論」というと変数宣言の際の型推論ばかりが挙げられがちだが(変数宣言時の型推論についてはJava10 で導入)変数宣言時のみならず、型が推論される部分はいくつかある点は注意されたい。
インスタンススコープの型変数
インスタンススコープの型変数の場合、バインドの仕方は主に2種類ある。ひとつはnewによるバインドで、もうひとつは継承によるバインドだ。
newによるバインドはおなじみであろう。
new ArrayList<String>();
といった場合の<String>の部分である。
インスタンススコープの型変数のバインドもJava7以降では型推論を用いることが出来る。
List<String> list = new ArrayList<>();
<>を書くことで型推論を用いますよ、と明示する。これがないとraw型扱いとなってしまう。この<>をダイヤモンドオペレーターと呼ぶ。
ダイヤモンドオペレーターは、Java5からあるメソッドスコープの型推論の機構をそのまま流用して作られている。おそらく型推論器があるから使い回すとちょっと便利じゃね?ぐらいの機能なのだろう。なお、Java8でStreamAPIを便利に扱うためにラムダ式が導入されたが、このラムダ式を便利に使うために型の推論器が強化され左辺側からの推論に強くなった。この影響で、Java7ではダイヤモンドでうまく推論できずエラーとなる場所でもJava8ならば用いることが出来ることがある。
継承によるバインド
もうひとつのバインド方法は継承の際のバインドである。
public class StringList extends ArrayList<String> { }
継承する際に親クラス(上記例ではArrayList)の型変数に対してバインドすることが出来る。GoFのStrategyパターンのような継承を前提としたデザインパターンなどではこうした継承によるバインドを使うことがままある。
本稿では深入りしないが、再帰ジェネリクスの場合はnewではバインドできず、継承でのバインドしか行えない。
また、これも深入りしないが、継承でのバインドの場合、classファイルに型変数に何がバインドされたのか情報が残るためリフレクションによって型変数に何がバインドされたのか動的に確認することもできる。出来るからってそれを使って何かをやるというシーンは稀だと思うが。
バインド出来るもの
具象型でバインド出来るのは当たり前なのだが、パラメタライズドタイプでバインドすることも出来る。これは意識せずやっていることがあるだろう。
new ArrayList<List<String>>();
この例ではList<String>というパラメタライズドタイプをバインドしているわけだ。
また、スコープ的に有効であれば型変数でバインドすることも出来る。
public class Hoge<T> { public void piyo() { List<?> list = new ArrayList<T>(); } }
上記例ではインスタンススコープの型変数Tをインスタンスメソッド内でArrayListの型変数にバインドしている。
逆にバインド出来ないものとしては共変ワイルドカード、反変ワイルドカードのキャプチャをバインドすることは出来ない。
new ArrayList<? extends X>(); // NG new ArrayList<? super X>(); // NG
これはパラメタライズドタイプでの山括弧には? extends, ? superが現れるため、見た目に紛らわしく思わず書けそうに思えてしまう。しかし、パラメタライズドタイプの山括弧とバインドの山括弧は別物であり、書けるものが異なるので意識して山括弧を見ることが重要だ。