Java 2 Platform Standard Edition 5.0 のリリースによって Java 言語仕様も Java 誕生以来最も大きな変更が加えられました。 Java 誕生後の数年で、プログラミング言語に求められる機能も変化しました。 とくに .NET と C# 言語の存在は Java 開発チームに少なからず影響を与えているでしょう。
このバージョンのリリースによって、従来のクラスファイルや VM の仕様も変更され、これまでの Java に存在しなかった便利な機能が多く追加されました。 C++ 言語のテンプレートに当たる Generics の存在は、特に重要です。
Generics の他にも、従来の Java 言語では面倒な手続きが必要だった処理を簡素に記述できる仕様が追加されています。
Generics は総称性 (Genericity) とも呼ばれ、日本語では一般にジェネリクスと呼ばれています。 Generics はクラスの定義内で用いられる型を変数化することで、インスタンスを作成するときまで具体的な型を抽象的に表現し、インスタンス生成時に具体的な型を指定します。 これによって、コンパイル時に型を保障しながら汎用的なコレクションクラス(動的配列や辞書)を作成することができます。
Generics の概念は、型宣言で指定される型パラメータリスト (Type parameter) と、型パラメータリストで宣言される型変数 (Type variable) で構成されます。 この Generics が導入されているクラスやインタフェースのことを、特にパラメータ化された型と呼びます。 パラメータ化された型は次のように宣言します。
class クラス名 <型パラメータリスト> { ...
型パラメータリストは、クラスやインタフェースの宣言で、型名に続いて < > で囲まれた中に記述します。 型パラメータリストは、クラスがインスタンス化されるまで具体的な型を保留する型変数を宣言します。
< 型変数1 , 型変数2 , ...>
型変数には、Java 言語で定められている識別子の命名規則に従えばどのような名前でもかまいません。 通常の変数や型名などのように開発者が自由に決定することができますが、規約としてはできるだけ 1 文字の大文字を利用することが推奨されています。 例としては、次のように宣言することができます。
class GenericsTest <T> { ... }
ここで宣言された型変数 T は、クラスを定義するブロック内ならば自由に指定することができます。 型変数 T の具体的な型はクラスがインスタンス化されるまで決定されません。 型変数という名前の通り、T は型を変数のように実行時に変化するものと捉えることができます。
パラメータ化された型をインスタンス化するには、通常の new 演算子によるインスタンス生成式ではなく、型パラメータリストで宣言されている型変数に型を与えなければなりません。 これは次のように記述されるでしょう。
new GenericsTest <String>();
このように、インスタンス生成式でインスタンス化する型名に続いて型パラメータリストで要求された型変数を指定します。 ここで指定する型は、必ず参照型でなければなりません。 int や float などの値型を指定した場合はコンパイルエラーとなります。
パラメータ化された型のインスタンスを変数に保存するには、その変数の型も型変数を指定しなければなりません。 つまり、次のように変数に保存されます。
GenericsTest <String> obj = new GenericsTest <String>();
これは、String 型を型変数に指定した GenericsTest クラスのインスタンスを作成しています。 型変数に指定した型がクラスの内部でどのように利用されるかは実装の問題です。 String 型を指定した場合、GenericsTest クラス内の T 型は、インスタンス生成時に String 型と判断されて作られます。
class Value<T> { private T value; public Value(T value) { this.value = value; } public T getValue() { return value; } } class Test { public static void main(String args[]) { Value<String> obj1 = new Value<String>("kitty on your lap"); Value<Integer> obj2 = new Value<Integer>(new Integer(10)); System.out.println("obj1 type = " + obj1.getValue().getClass()); System.out.println("value = " + obj1.getValue()); System.out.println("obj2 type = " + obj2.getValue().getClass()); System.out.println("value = " + obj2.getValue()); } }
>java Test obj1 type = class java.lang.String value = kitty on your lap obj2 type = class java.lang.Integer value = 10
このプログラムは、型変数 T を宣言するパラメータ化されたクラス Value を作成しています。 このクラスは、書き換え不可能な参照型を提供するクラスとして利用することができます。 このクラスが持つフィールド value は T 型なので、インスタンス生成式で String が指定されれば String 型として、Integer を指定されれば Integer 型として機能します。 汎用型は Object 型で実現することができますが、Object 型の変数は予期せぬインスタンスを参照している可能性があります。 これに対して、Generics はコンパイル時に型検査を行うことができるためより安全に汎用的なクラスを作成することができるのです。
パラメータリストには、カンマ , を区切り文字として複数の型変数を指定することができます。 型変数を複数用いる場合は T の他に E や S などの文字が好まれて使われるようです。
class KeyValue <K, V> { private K key; private V value; public KeyValue(K key , V value) { this.key = key; this.value = value; } public K getKey() { return key; } public V getValue() { return value; } } class Test { public static void main(String args[]) { KeyValue<String, Integer> obj = new KeyValue<String, Integer>("keyValue", new Integer(10)); System.out.println("key=" + obj.getKey()); System.out.println("value=" + obj.getValue()); } }
このプログラムの KeyValue クラスは、型変数 K と V を受け取るパラメータ化されたクラスです。 K と V には、それぞれ個別の方を与えることもできますし、必要によっては同じ型を指定してもかまいません。
型変数はクラス単位ではなく、メソッドの宣言に対して指定することもできます。 この場合、型変数はメソッドのブロック内でのみ有効となります。 メソッドに型変数を指定するにはメソッド宣言において修飾子の後、戻り値型の前に指定します。
public static <T> void method(T value) { ... }
この場合、method() メソッドの引数 value は型変数 T 型として認識されます。 型変数 T は、この場合はメソッドのブロック内でのみ有効となります。 インスタンスメソッドだけではなく、クラスメソッドに対しても型変数を指定することができます。
パラメータ化されたクラスの場合は、インスタンス生成式で型変数の型を指定しましたが、メソッドの型変数に対しては必要ありません。 コンパイラは、メソッドの呼び出し時にメソッドの引数に指定された値から、型変数の型を認識することができます。
class Test { static <T> void generics(T value) { System.out.println(value); } public static void main(String args[]) { generics("Kitty on your lap"); generics(new Integer(10)); } }
このプログラムの generics() メソッドは、型変数 T 型の引数を受け取ります。 main() メソッドの generics() メソッドの呼び出しでは、文字列を渡した場合は generics(String) として認識され、Integer オブジェクトを渡した場合は generics(Integer) として認識します。 しかし、この時点では generics(Object) と何が違うのか、Generics を利用する必要性に疑問を感じるでしょう。 これについては、次章で解説します。
クラスと同様に、インタフェース宣言でも型パラメータリストを指定してパラメータ化することができます。 インタフェースはインスタンス化されることがないので、クラスに実装される際に型変数に対して型を指定します。
interface I <T> { ... } class A implements I<String> { ... }
パラメータ化されているインタフェースをクラスに実装するには、上記に用に記述します。 インタフェースがメソッドで型変数 T を利用している場合、インタフェースを実装するクラスは実装時に指定した型に合わせてメソッドを実装しなければなりません。
interface I <T> { void printValue(T value); } class A implements I<String> { public void printValue(String value) { System.out.println(getClass() + ".printValue() = " + value); } } class B implements I<StringBuffer> { public void printValue(StringBuffer value) { value.append(" : "); value.append(getClass()); value.append(".printValue()"); System.out.println(value); } } class Test { public static void main(String args[]) { A objA = new A(); B objB = new B(); objA.printValue("Kitty on your lap"); objB.printValue(new StringBuffer("Kitty on your lap")); } }
このプログラムでは、型変数 T を宣言するパラメータ化された I インタフェースを宣言してます。 I インタフェースでは、型変数 T 型の変数を引数で受け取る printValue() メソッドを宣言しています。 I インタフェースを実装するクラスは、型引数 T 型に指定した型を用いて、具体的な printValue() メソッドを実装しなければなりません。
A クラスは I<String> インタフェースを実装しているため、I インタフェースの型変数に指定された String 型を受ける printValue() メソッドを実装しなければなりません。 printValue(String) を実装しない場合、コンパイルエラーとなります。
同様に B クラスは StringBuffer クラスを型変数に指定した I インタフェースを実装しているため printValue(StringBuffer) を実装しなければなりません。
パラメータ化された I インタフェースをコンパイルすると、実際には printValue(Object) が宣言されているものと解釈されます。 A クラスや B クラスでは printValue(Object) は実装していませんが、これはコンパイラが暗黙的に自動生成するものとされています。 実際に javap コマンドでクラスを解析すると、printValue(Object) が生成されていることを確認できます。
>javap A Compiled from "test.java" class A extends java.lang.Object implements I{ A(); public void printValue(java.lang.String); public void printValue(java.lang.Object); }
この結果を見て確認できるように、コード上で実装しているのは String 型ですが、一方でコンパイラによって自動的に Object 型の printValue() が追加されています。 この printValue(Object) メソッドは、内部で printValue((String)value) というように、キャスト変換によって実装されているメソッドを間接的に呼び出す仕掛けになっています。 クラスで実装されたメソッドを呼び出すための、コンパイラが自動生成したメソッドをブリッジメソッドと呼びます。
もちろん、printValue(Object) は実行時レベルではあらゆる参照型を受け取ることが論理的には可能です。 しかし、Generics を用いることによって、コンパイル時に型検査が行われるため、謝った型の値が渡された場合はエラーとなります。