Javaジェネリクス再入門

ジェネリクスでは、「」を変数にした「型変数」というものを取り扱う。型変数で何が嬉しいかというと、メジャーな例ではコレクションAPIが挙げられる。java.util.Listとかjava.util.Mapとかのデータを格納するタイプのユーティリティクラスのことだ。

2004年にJavaのバージョンが5.0となるまでは、Javaにはジェネリクスの機能はなかった。なので、Listにデータを格納し、取得する場合は

List list = new ArrayList();
list.add("hello!");
String str = (String) list.get(0);

といったソースコードになる。

add()の引数はObject型で宣言されており、どんな参照型でもadd()することができた。

get()の戻り値もObject型で宣言されておりキャストが必要だった。このキャストはプログラマに任されており、もしこのListのオブジェクトにString型以外のオブジェクトがコンタミすると、実行時にClassCastExceptionが発生した

このClassCastExceptionだが、デバッグは容易ではない。キャストに失敗した場所は例外のスタックトレースで明確だが、知りたいのはListに違う型のデータをadd()した部分なのだから!再現性が低いこともありソースコードを追いかけて、原因箇所を特定するのはとても骨の折れる作業だった。

そこで、このList型にこのListが取り扱う「型」を変数として持たせてみよう。

List<String> list = new ArrayList<String>();
list.add("hello!");
String str = list.get(0);

この型変数にがバインドされている。add()の引数はこの型変数にバインドされたString型以外は受け付けないし、get()はString型を返すことが保証できるようになった。もうClassCastExceptionの原因箇所を探す必要はなくなったんだ!

文法上の混乱しやすいところ

型変数にまつわる記述はだいたい<>で囲われている。一見して同じように見えるが<>は場所によって数種類に分類される。これを把握することがJavaジェネリクスの文法をマスターする近道だ。

種類 宣言 バインド ? 境界 &
型変数の宣言 × *1
型変数へのバインド × × × ×
変数の型の宣言 × ×
型変数での変数宣言 × × × × ×

順に見ていこう。「型変数の宣言」はクラスの宣言のときの

public class Hoge<T> {}

このの部分だ。ここでは、新たな型変数の宣言ができ、また型変数の境界も指定できる。

型変数へのバインド」はnewするときの

new Hoge<String>();

このが、先のHoge型の型変数Tにバインドされるというわけ。

これは、メソッドの仮引数と実引数との関係と似ている。

public static void piyo(String str){}

public static void main(String[] args) {
    piyo("Hello!");
}

メソッドpiyoの仮引数strには、mainメソッドから呼ばれたときには"Hello!"が入る。piyoメソッドが動く間、strは"Hello!"だ。

おなじように、HogeのTは、new HogeされるとStringとなり、Hogeクラスの中でTという型変数が使われるときはString型として扱われる。なので、メソッドの時と同様にHogeのTは「仮型引数」、new HogeのStringは「実型引数」と呼ばれる。

変数の型の宣言」は、ジェネリックな型の変数を宣言するとき。

Hoge<String> hoge;

の部分。このときにはワイルドカードなどが使える。これは後述。

型変数での変数宣言」は<>を伴わない。

public class Hoge<T> {
    private T t;
}

ここではインスタンスフィールドを宣言したが、インスタンスメソッド内でローカル変数として宣言することも出来る。ただし、型変数はインスタンスごとに保持されるものだからstaticフィールドやメソッドではこれらの型変数は利用できない。

仮型引数 - 型変数の宣言

仮型引数の宣言時には、新しい型引数の宣言と、その境界の宣言ができる。複数の型引数を持たせたい場合はカンマで区切る。

型引数は変数名の命名規約と同じだが、慣例としてアルファベットの大文字1字で表現することが多い。2文字以上にする場合は全て大文字にしよう。キャメルケース*2にするとクラス名と紛らわしい。

public class Hoge<T> {}
public class Piyo<T1, T2, T3> {}

また、型引数の型が特定の型を継承(extends)しているかを宣言することが出来る。これを「境界」と呼ぶ。境界はsuperを指定することはできない。

public class A {}
public class B extends A {}
public class C extends B {}

public class Foo1<T extends B> {} // BやCやその派生

(2011/07/29追記)Foo2という記述が可能であるように書いていたが、誤りだったので削除。superによる境界設定はワイルドカードで<? super B>といった表現をする場合のみ可能。

複数のinterfaceを実装していることを要求することもできる。

public class Bar<T extends B & java.io.Serializable> {}

このとき、Javaがclassの単一継承、interfaceの複数継承のわだかまりからか、class&interface[&interface[...] ]という順序で書かないといけない。interfaceのみで宣言することも出来る。

実型引数 - 型変数へのバインド

次は実型引数のほうを見てみよう。実型引数となる場所はふたつあって、ひとつはnewするとき。もうひとつは継承するときだ。

new Hoge<String>();
public class HogeEx extends Hoge<String> {}

場所は違うがやることは一緒。new あるいは extends するクラスの仮型引数の数と、境界に併せて型を並べるだけ。ここにはextendsとかsuperとか&とか?とかは出てこない

ひとつだけ特記するなら型変数のバインドには型変数を用いることができる点。

public class Hoge<T> {}
public class HogeEx2<E> extends Hoge<E> {}

この例ではHogeEx2で宣言されたEをHogeの仮型引数にバインドすることになる。

変数の型の宣言

変数などの型の宣言時にはワイルドカードを使うことができる。

Hoge<?> hoge = new Hoge<String>();

このワイルドカード?にはsuperとextendsで境界を設定することができる。&による複数のinterfaceの継承は表現できない。

このワイルドカードのことを理解するには、ジェネリックな変数の代入互換性について理解しなくてはならない。

public class A {}
public class B extends A {}
public class C extends B {}

があったとして

A a = new B();
B b = new C();

といったことができるのはJavaの基礎だが、ジェネリクスではこの型の代入互換性とルールが異なる

Hoge<A> a = new Hoge<B>(); // コンパイルエラー!

なぜダメなのか。ArrayListで考えてみよう。

ArrayList<B> bList = new ArrayList<B>();
ArrayList<A> aList = bList; // 本来は代入できないができたと仮定する
aList.add(new A()); // ArrayList<A>にはA型を代入できる
B b = bList.get(0); //ArrayList<B>なのでget()の結果はB型のはず

ここで、aList = bListなので、aListにadd()したA型のインスタンスが、bListからget()できてしまう。ArrayList<B>なのでget()の結果はB型のはずだが、B型より上位のA型がとれてしまった。これではClassCastExceptionになってしまう。

そんなわけで、ワイルドカードを使って

Hoge<? extends A> a = new Hoge<B>();

といったようにしないといけない。ジェネリクスの<>の中は一般のJavaの型の代入互換性とは異なる。このことはよく覚えておかなくてはいけない。

なお、<? extends B>のようなワイルドカードの場合、戻り値に型変数が使われている場合、B型の返り値保証されるが、引数に型変数があるメソッドを呼び出すことができなくなる*3

逆に<? super B>のようなワイルドカードの場合、戻り値に型変数が使われている場合、Object型でしか返り値が受け取れなくなるが、引数に型変数があるメソッドの呼出は自由に行える。

このあたりは

を参照されたし。

型変数のスコープ

さて、ここまで黙っていたのだけど、実は、型変数にはそもそも2種類あって、クラスのインスタンスをスコープ(有効範囲)としたものと、メソッドをスコープとしたものとがある。

メソッドローカルな型変数を宣言したい場合は

public class Sample {
    public <T> void hoge() {}
}

といったように、戻り値の宣言(ここではvoid)の手前に<>を書き、仮型引数を宣言することになる。この例では宣言しただけで意味がない。通常は、メソッド引数か、返り値の型に用いるが、真価を発揮するのはT型で引数を受け取り、T型で返すというような場合だろう。

例としてjava.util.Collectionsのlistメソッドを挙げよう。

public static <T> ArrayList<T> list(Enumeration<T> e) {

さて、このようなメソッドのローカル型変数に実型引数を渡す場合はどうするのか。結構ややこしい。

public class Sample {
    public <T> T xxx(T hoge) {
        return null;
    }
    public static void main(String[] args) {
        Sample sample = new Sample();
        String string = sample.<String>xxx("hello");
    }
}

インスタンスメソッドの場合はオブジェクト.メソッド名()で呼出をするが、この"."とメソッド名の間に実型引数を書く
staticメソッドの場合はクラス名.メソッド名()で呼出をするが、この"."とメソッド名の間に実型引数を書く
インスタンスメソッド内で自分自身のインスタンスメソッドを呼び出す場合などはそのままでは実型引数を書けないので、thisを補って書く。

public class Sample {
    public <T> T xxx(T hoge) {
        return null;
    }
    public void yyy() {
        String string = this.<String>xxx("hello");
    }
}

また、メソッドローカルな型変数は型推論が働く。この辺はコンパイラの実装によって胡散くさい挙動を示したりするところなのだけど、場合によっては便利。先の例などは実際のところ以下のような記述で動く。

public class Sample {
    public <T> T xxx(T hoge) {
        return null;
    }
    public void yyy() {
        String string = xxx("hello");
    }
}

しかし、場合によっては明示的に実型引数を指定しないとダメなケースもあるので、正書法を覚えておくに越したことはない。

重箱の隅的な話としてはコンストラクタでもローカル型変数が使える。使い道は思い浮かばないが。

public class Sample {
    public <T> Sample() {}
}

内部クラスと型変数

エンクロージング型内部クラスではアウタークラスの型変数を扱うことができる。型変数がインスタンスと結びついているわけだが、エンクロージング型内部クラスもインスタンスと結びついているわけだから同じく利用出来る、というわけだ。

public class Outer<O> {
    public class Inner<I> {
        O o;
        I i;
    }
    public static void main(String[] args) {
        Outer<String> outer = new Outer<String>();
        Outer<String>.Inner<Integer> inner = outer.new Inner<Integer>();
        inner.o = "hello!";
        inner.i = 42;
    }
}

2つのクラス間で同じ型変数を扱いたい場合に、Outerで型変数を宣言しておいて、2つのインナークラスで利用する…なんて手法もあるんだけど、トップレベルクラスじゃなくなるといろいろ不便があってなかなか難しい。トップレベルクラスで2つのクラスで相互に同じ型変数を利用したいとかになると複雑怪奇な宣言しないといけなくなるんだがなんとかならないものか。

詳細は以下を参照されたし。

*1:型変数の境界の型がジェネリックな型のときに?を扱える

*2:HogePiyoといったように単語の区切りを大文字始まりとする記法。ラクダのコブに例えてキャメル(らくだ)ケース(大文字小文字の別のこと)と呼ばれる

*3:厳密にはnull値を渡して呼び出すことだけできる