型変数は、ブロック内で汎用的な型として利用することができましたが、Object 型とは異なりインスタンス化されるまでその実体がわからないのでいくつかの制約が存在します。 制約の一つは、前の章でも解説した int などの基本型(値型)を指定することはできないことです。 このほかに、次のような制約があります。
型変数のインスタンス生成式を作れないという制約は、new 演算子で指定する型として型変数を指定することはできないということです。 同様に、配列のインスタンスを作成することもできません。 型変数 T が宣言されているとした場合、次のインスタンス生成式はコンパイルエラーとなります。
new T();
new T[size];
静的なブロックに型変数を指定できないというのは、static フィールドや static 初期化ブロック内では方変数を使えないということです。 なぜならば、static メンバはインスタンスと結びついていないため型変数が与えられないからです。
Generics の情報はクラスファイルに変換されるときどのように利用されるのでしょう。 C++ 言語のテンプレートは、ソースコード内でテンプレートが使われる度にコンパイラがテンプレートの実体コードを作成するという仕組みでした。 しかし、Java 言語の場合は型変数を結局は Object 型として扱います。
Java 言語の Generics の目的は、動的配列や辞書に保存する要素の型をコンパイル時に保障することで、予期せぬ型を配列に追加してしまったり、誤ったキャスト変換をしないようにすることです。 そのため、Java コンパイラの実装はパラメータ化された型の実体を Object 型に自動的に変換するのです。 この性質をイレイジャ (Erasure) と呼びます。
普段開発するにあってはイレイジャを気にする必要はありません。 しかし、メソッドの引数の型に型変数を指定している場合は、その実体が Object 型であることを意識しなければなりません。 異なる型変数であっても、実体は Object となるためシグネチャが衝突する可能性があります。
class Value<T, S> { public void setValue(T value) {} public void setValue(S value) {} } class Test { public static void main(String args[]) { } }
このプログラムの Value クラスは T と S の型変数を宣言しています。 これらには個別の型を与えることができますが、同じ型が指定される可能性があるという観点から考えても、このコードは問題があります。 T と S はイレイジャによって Object 型に変換されるため、結果として setValue() メソッドのシグネチャが衝突してしまいます。 よって、このコードはコンパイルエラーとなるでしょう。
型変数 T 型の値は、インスタンスが作成されるまで何型なのかを判断することができません。 そのため、唯一保障できるのは Object 型を継承しているということだけです。
型変数 T 型のオブジェクトから Object クラス以外のメンバを呼び出すことができません。 しかし、型変数 T が特定のクラスやインタフェースを実装していることを保障することができれば、T 型の変数から制約されたクラスやインタフェースが宣言しているメソッドを安全に呼び出すことができます。
型変数が何らかのクラスやインタフェースを実装していることを保障するには、extends キーワードを用いて次のように記述します。
<型変数 extends 型>
extends キーワードに続いて制約対象とする型を指定します。 型変数は、必ず指定された型を実装していなければなりません。 デフォルトの <T> は、すなわち <T extends Object> と同じであると考えられます。
class Value<T extends Number> { private T value; public Value(T value) { this.value = value; } public void printValue() { System.out.println("type = " + value.getClass()); System.out.println("double = " + value.doubleValue()); System.out.println("int = " + value.intValue()); } } class Test { public static void main(String args[]) { Value<Integer> obj1 = new Value<Integer>(new Integer(10000)); Value<Double> obj2 = new Value<Double>(new Double(0.12345)); obj1.printValue(); obj2.printValue(); } }
>java Test type = class java.lang.Integer double = 10000.0 int = 10000 type = class java.lang.Double double = 0.12345 int = 0
このプログラムの Value クラスで宣言されている型変数 T は、Number クラスのサブクラスで無ければならないという制約が追加されています。 そのため、T 型の変数は Number クラスのメンバを安全に呼び出すことができます。
Value クラスの型変数 T 型には、Number クラスを継承していない型を指定することはできません。 Byte、Integer、Double クラスなどは全て Number クラスから派生していますが、String 型など Number クラスと関係のない型を指定するとコンパイルエラーとなります。
型変数に対する制約は、単一のクラスだけではなくインタフェースを含めることも可能です。 複数の型を強制するには & 記号を用いて次のように指定します。
<型変数 extends 型 & 型 & 型 & ...>
このように & 記号を用いて複数の型を指定する場合、インタフェースは自由に指定できますがクラスは一つしか指定できません。 クラス型を含むインタフェースとの組み合わせの場合は、クラス型を extends の直後 & よりも前に(つまり一番最初に)指定しなければなりません。 インタフェースのみの場合、順番は関係ありません。
interface A {} interface B {} class C implements A {} class D extends C implements B {} class Value<T extends C & A & B> {} class Test { public static void main(String args[]) { //new Value<C>(); //エラー new Value<D>(); } }
このプログラムの Value クラスの型引数 T は C クラスと継承関係が認められ、かつ A インタフェースと B インタフェースを実装している型でなければなりません。 main() メソッドで C クラスを型変数に指定した場合はエラーとなりますが、これは C クラスが B インタフェースを実装していないためです。 D クラスは C クラスを継承し、B インタフェースを実装しているため条件に一致します。