Java5の型システムを理解するにはリフレクションAPIを使ってみるのが最短の近道になる

Java5における総称型(generics)の導入に伴い、Javaの型システムは以前と比べて高機能になった反面、理解するためのハードルが高くなっています。もちろん、Javaの型についてきちんと理解するためには言語仕様を勉強すればよいのですが、手っ取り早く理解するための方法としてリフレクションAPIを使ってみるというのが有効です。リフレクションAPIの先祖はJava1.xのころから存在しており、フィールド、メソッド、クラスなどの情報を実行時に取得するためのものですが、総称型に合わせてJava5から新しいAPIが追加されています。ここではリフレクションAPIを使い、Java5の新しい型システムについてまとめてみたいと思います。

JDK1.4までの型はすべてClassクラスのインスタンスに一対一対応する

JDK1.4までに存在していた型はパターンに分けると以下の3通りに分類できます。

  • 基本型(int、doubleなど)
  • 参照型(クラス、インターフェース)
  • 配列型(byte[ ]、String[ ]、String[ ][ ]など)

JDK1.4までの型に関するリフレクションAPIは非常に単純で、これらの型情報はすべてjava.lang.Classによって表現されました。Classのインスタンスとそれぞれの型とが一対一に対応していたのです。基本型や配列はクラスでないので、これらの型がクラスで表現されるというところはちょっと違和感があるかもしれませんが、実際、

  Class intClass = int.class; // intの基本型をあらわすクラスを取得
  Class longArrayClass = long[].class; // long[]をあらわすクラスを取得
  Class long2DArrayClass = long[][].class; // long[][]をあらわすクラスを取得

のようにクラスリテラルの書式を用いてあらゆる型に対応するClassのインスタンスを取得できます。

Java5以降では具象化可能型に限りClassで表現できる

型消去(type erasure)と具象化可能型(reifiable type)については、普通の(業務)Javaアプリケーションでは配列をなるべく使用しない方がよい - 達人プログラマーを目指してでも説明しましたが、

  • 基本型(intなど)
  • 総称化されていないクラスやインターフェース(String、Number、Runnableなど)
  • すべての総称型変数に対して境界のないワイルドカードを持つ型(List<?>、Map<?, ?>など)
  • 総称パラメーター未指定のraw型(List、Mapなど)
  • 具象化可能型の要素を持つ配列(int[ ]、String[ ]、List<?>[ ]、int[ ][ ]など)

のようなものがあります。これらは型消去により型の情報が失われない型であり、JDK1.4までに存在した型と互換のある型です。基本的にJavaの思想は過去のバージョンと互換性を保つということを重視しており、これらの型の情報は従来どおりjava.lang.Classによって表現することができます。(ただし、Java5ではClassクラスが型変数Tによって総称化されています。)実際、以下のクラスのフィールドに対して

class TypePatterns<T> {
	// JDK1.4までに存在していた型のパターン

	/** 基本型 */
	public int primitiveField;

	/** 参照型 */
	public String objectField;

	/** 配列型 */
	public long[] arrayField;
...
}

次のテストコードが示すようにJDK1.4と互換性のあるAPIの呼び出しによりフィールドの情報を取得することができます。

/**
 * 基本型フィールドに対するリフレクション情報の取得
 */
@Test
public void primitiveField() throws Exception {
	Field primitiveField = TypePatterns.class.getField("primitiveField");
	Class<?> primitiveFieldType = primitiveField.getType();
	assertEquals(int.class, primitiveFieldType);
	assertTrue(primitiveFieldType.isPrimitive()); //基本型
}

/**
 * 参照型フィールドに対するリフレクション情報の取得
 */
@Test
public void objectField() throws Exception {
	Field objectField = TypePatterns.class.getField("objectField");
	Class<?> objectFieldType = objectField.getType();
	assertEquals(String.class, objectFieldType);
}

/**
 * 配列型フィールドに対するリフレクション情報の取得
 */
@Test
public void arrayField() throws Exception {
	Field arrayField = TypePatterns.class.getField("arrayField");
	Class<?> arrayFieldType = arrayField.getType();
	assertEquals(long[].class, arrayFieldType);
	assertTrue(arrayFieldType.isArray()); //配列型
	assertEquals(long.class, arrayFieldType.getComponentType()); //要素型の取得
}

従来のAPIを使った場合、具象化可能型でない型は型消去された具象化型として得られる

このように具象化可能型についてはJava5でも以前のバージョンとまったく同様にClassクラスと型が対応しています。しかし、Java5以降ではソースコード上で記述できる型には以下のように具象化可能でない型も存在します。

  • 総称型変数そのもの(Tなど)
  • 実型パラメーターを持つパラメーター化型(List、Mapなど)
  • 境界付きワイルドカードを持つパラメーター化型(List<? extends Number>、Comparable <? super String>など)
  • 具象化可能型でない要素を持つ総称配列(T[ ]、List[ ]など)

これらの型はバイトコード中では型消去により別の具象化型に変換されることにより、直接対応するClassのインスタンスも存在しません。

class TypePatterns<T> {

...
	// Java5でサポートされるようになった型のパターン

	/** 型変数 */
	public T typeVariableField; 

	/** パラメーター化型 (型パラメーターが普通のクラスの場合) */
	public List<String> parameterizedField;

	/** パラメーター化型 (型パラメーターが型変数の場合) */
	public List<T> typeVariableParameterizedField;

	/** パラメーター化型 (型パラメーターがワイルドカード型の場合) */
	public List<? extends Number> wildcardParameterizedField;

	/** 総称型配列(要素が型変数の場合) */
	public T[] typeVariableArrayField;

	/** 総称型配列(要素がパラメーター化型の場合) */
	public List<String>[] parameterizedComponentArrayField;
}

実際、以上で宣言された変数に対して前節と同様にJDK1.4互換のAPIを呼び出してみた結果は次のようになります。

// 総称型を使っている場合

/**
 * 型変数Tに対するリフレクション情報の取得
 */
@Test
public void typeVariableField() throws Exception {
	// JDK1.4までに存在していたAPIは型消去後の情報のみを返す。
	Field typeVariableField = TypePatterns.class.getField("typeVariableField");
	Class<?> typeVariableFieldType = typeVariableField.getType();
	assertEquals(Object.class, typeVariableFieldType); // 型消去後のTの具象化型
}

/**
 * パラメーター化型List<String>に対するリフレクション情報の取得
 */
@Test
public void parameterizedField() throws Exception {
	// JDK1.4までに存在していたAPIは型消去後の情報のみを返す。
	Field parameterizedField = TypePatterns.class.getField("parameterizedField");
	Class<?> parameterizedFieldType = parameterizedField.getType();
	assertEquals(List.class, parameterizedFieldType); // 型消去後のList<String>の具象化型
}

/**
 * パラメーター化型List<T>に対するリフレクション情報の取得
 */
@Test
public void typeVariableParameterizedField() throws Exception {
	// JDK1.4までに存在していたAPIは型消去後の情報のみを返す。
	Field typeVariableParameterizedField = TypePatterns.class.getField("typeVariableParameterizedField");
	Class<?> typeVariableParameterizedFieldType = typeVariableParameterizedField.getType();
	assertEquals(List.class, typeVariableParameterizedFieldType); // 型消去後のList<T>の具象化型
}

/**
 * パラメーター化型<? extends Number>に対するリフレクション情報の取得
 */
@Test
public void wildcardParameterizedField() throws Exception {
	// JDK1.4までに存在していたAPIは型消去後の情報のみを返す。
	Field wildcardParameterizedField = TypePatterns.class.getField("wildcardParameterizedField");
	Class<?> wildcardParameterizedFieldType = wildcardParameterizedField.getType();
	assertEquals(List.class, wildcardParameterizedFieldType); // 型消去後のList<? extends Number>の具象化型
}

/**
 * 総称型配列T[]に対するリフレクション情報の取得
 */
@Test
public void typeVariableArrayField() throws Exception {
	// JDK1.4までに存在していたAPIは型消去後の情報のみを返す。
	Field typeVariableArrayField = TypePatterns.class.getField("typeVariableArrayField");
	Class<?> typeVariableArrayFieldType = typeVariableArrayField.getType();
	assertEquals(Object[].class, typeVariableArrayFieldType); // 型消去後のT[]の具象化型
	assertTrue(typeVariableArrayFieldType.isArray());
	assertEquals(Object.class, typeVariableArrayFieldType.getComponentType());
}

/**
 * 総称型配列List<String>[]に対するリフレクション情報の取得
 */
@Test
public void parameterizedComponentArrayField() throws Exception {
	Field parameterizedComponentArrayField = TypePatterns.class.getField("parameterizedComponentArrayField");
	Class<?> parameterizedComponentArrayFieldType = parameterizedComponentArrayField.getType();
	assertEquals(List[].class, parameterizedComponentArrayFieldType); // 型消去後のList<String>[]の具象化型
	assertTrue(parameterizedComponentArrayFieldType.isArray());
	assertEquals(List.class, parameterizedComponentArrayFieldType.getComponentType());
}

以上の結果が示すように、もともとソースコード上で宣言されている通りの総称型を使った型は取得できず、代わりに型消去後の具象化型が得られることがわかります。型消去により、Java5でもJDK1.4までと同じ結果が得られている、つまり、互換性が保たれているということです。

総称型に対応したJava5の新しいリフレクションAPI

型消去により実際に実行されるバイトコードの中では具象化可能な型しか残りませんが、リフレクション用にソースコード中の総称型の情報が保存されるようになっています。こうした総称型の情報はロジックやデータ構造には一切影響がないもののバイトコード中に埋め込まれたコメント情報のようなものと考えられます。Java5では従来のリフレクションAPIに加えてgetGenericType()という名前のメソッドが提供されており、総称型の情報を実行時に取得できます。たとえばFieldクラスのgetType()メソッドはClass<?>を返しますが、このメソッドからは前節で調べたように型消去後の型情報しか得られません。代わりにgetGenericType()を呼び出すことで総称型の情報が得られます。このメソッドの戻り値はClass<?>ではなくてTypeというインターフェースになっています。このTypeインターフェースが非具象化可能なものも含めてあらゆる型を表現するためにJava5から導入されていますが、従来のClass<?>との関係も含めて以下のクラス図のような関係となっています。

従来からあるClassもTypeを実装しているのですが、具象化可能でないさまざまな型を表現するための新しいクラスが提供されています。このクラス図を見ると、Javaが互換性を維持しながら、どのようにして総称型をサポートするように拡張されているのかが明確になります。ここで従来のClassも含めてJavaの型について以下にまとめてみます。

型のカテゴリ 表現するための型(メタ型) 説明
具象化可能型 Class 基本型、総称を使わない参照型、
配列などJDK1.4以前からある型。
int、long[]、String、List
型変数 TypeVariable クラス、メソッド、コンストラク
などで宣言された型変数。
複数の上限型を持つことができる。
SomeClass
T someMethod(List list)
で宣言されたEやT
ワイルドカード WildcardType パラメーター化型で使用する
ワイルドカードを含んだ型
List<? extends Number>
などの「? extends Number」の部分
パラメーター化型 ParameterizedType パラメーター化された型 List、List
List<? extends Number>など
総称型配列 GenericArrayType 要素に型変数やパラメーター化
された型を使った配列型
T、Listなど

Class以外のTypeに関係するクラスはlava.lang.relectパッケージに格納されています。*1
Oracle Technology Network for Java Developers | Oracle Technology Network | Oracle
それでは、以上の拡張されたAPIを利用して、実際に総称型の情報が取得できることを確認してみます。

/**
 * 型変数Tに対するリフレクション情報の取得
 */
@Test
public void typeVariableField() throws Exception {

	// Java5の拡張APIを使いGenerics固有の情報を取得
	assertTrue(typeVariableField.getGenericType() instanceof TypeVariable);

	TypeVariable<?> typeVariable = (TypeVariable<?>)typeVariableField.getGenericType();
	assertEquals("T", typeVariable.getName()); // 型変数名「T」
	assertArrayEquals(new Type[]{Object.class}, typeVariable.getBounds()); // Tの上限(指定されていないためObject)
	assertEquals(TypePatterns.class, typeVariable.getGenericDeclaration()); // Tの宣言はTypePatternsクラス
}

/**
 * パラメーター化型List<String>に対するリフレクション情報の取得
 */
@Test
public void parameterizedField() throws Exception {

	// Java5の拡張APIを使いGenerics固有の情報を取得
	assertTrue(parameterizedField.getGenericType() instanceof ParameterizedType);

	ParameterizedType parameterizedType  = (ParameterizedType)parameterizedField.getGenericType();
	assertArrayEquals(new Type[]{String.class}, parameterizedType.getActualTypeArguments()); // 型変数に対するパラメーター値「String」
	assertEquals(List.class, parameterizedType.getRawType()); 
}

/**
 * パラメーター化型List<T>に対するリフレクション情報の取得
 */
@Test
public void typeVariableParameterizedField() throws Exception {

	// Java5の拡張APIを使いGenerics固有の情報を取得
	assertTrue(typeVariableParameterizedField.getGenericType() instanceof ParameterizedType);

	ParameterizedType parameterizedType  = (ParameterizedType)typeVariableParameterizedField.getGenericType();
	assertEquals(1, parameterizedType.getActualTypeArguments().length);
	assertTrue(parameterizedType.getActualTypeArguments()[0] instanceof TypeVariable);

	TypeVariable<?> typeVariable = (TypeVariable<?>)parameterizedType.getActualTypeArguments()[0];
	assertEquals("T", typeVariable.getName()); // 型変数名「T」
	assertArrayEquals(new Type[]{Object.class}, typeVariable.getBounds()); // Tの上限(指定されていないためObject)
	assertEquals(TypePatterns.class, typeVariable.getGenericDeclaration()); // Tの宣言はTypePatternsクラス

	assertEquals(List.class, parameterizedType.getRawType());
}

/**
 * パラメーター化型<? extends Number>に対するリフレクション情報の取得
 */
@Test
public void wildcardParameterizedField() throws Exception {

	// Java5の拡張APIを使いGenerics固有の情報を取得
	assertTrue(wildcardParameterizedField.getGenericType() instanceof ParameterizedType);

	ParameterizedType parameterizedType  = (ParameterizedType)wildcardParameterizedField.getGenericType();
	assertEquals(1, parameterizedType.getActualTypeArguments().length);
	assertTrue(parameterizedType.getActualTypeArguments()[0] instanceof WildcardType);
	assertEquals(List.class, parameterizedType.getRawType());

	WildcardType wildcardType = (WildcardType)parameterizedType.getActualTypeArguments()[0];
	assertArrayEquals(new Type[] {Number.class}, wildcardType.getUpperBounds());
	assertArrayEquals(new Type[0], wildcardType.getLowerBounds());
}

/**
 * 総称型配列T[]に対するリフレクション情報の取得
 */
@Test
public void typeVariableArrayField() throws Exception {

	// Java5の拡張APIを使いGenerics固有の情報を取得
	assertTrue(typeVariableArrayField.getGenericType() instanceof GenericArrayType);

	GenericArrayType genericArrayType  = (GenericArrayType)typeVariableArrayField.getGenericType();
	assertTrue(genericArrayType.getGenericComponentType() instanceof TypeVariable);

	TypeVariable<?> typeVariable = (TypeVariable<?>)genericArrayType.getGenericComponentType();
	assertEquals("T", typeVariable.getName()); // 型変数名「T」
	assertArrayEquals(new Type[]{Object.class}, typeVariable.getBounds()); // Tの上限(指定されていないためObject)
	assertEquals(TypePatterns.class, typeVariable.getGenericDeclaration()); // Tの宣言はTypePatternsクラス
}

/**
 * 総称型配列List<String>[]に対するリフレクション情報の取得
 */
@Test
public void parameterizedComponentArrayField() throws Exception {

	// Java5の拡張APIを使いGenerics固有の情報を取得
	assertTrue(parameterizedComponentArrayField.getGenericType() instanceof GenericArrayType);

	GenericArrayType genericArrayType  = (GenericArrayType)parameterizedComponentArrayField.getGenericType();
	assertTrue(genericArrayType.getGenericComponentType() instanceof ParameterizedType);

	ParameterizedType parameterizedType  = (ParameterizedType)genericArrayType.getGenericComponentType();
	assertArrayEquals(new Type[]{String.class}, parameterizedType.getActualTypeArguments()); // 型変数に対するパラメーター値「String」
	assertEquals(List.class, parameterizedType.getRawType()); 
}

このように、具象化可能でない型について、実行時に型情報を得ることができることが分かります。実際、HibernateやSpringなどの最新のバージョンではこうした総称型の情報をメタ情報として有効に活用しています。なお、総称化されていないJDK1.4互換の型の場合、getGenericType()メソッドはgetType()メソッドと同様にClass<?>を返すようになっています。ClassはTypeを実装しているため、つじつまが合っています。(サンプルプログラムはgistにアップしてあります。https://gist.github.com/888253

まとめ

以上の要点をまとめます。

  • JDK1.4以前の型はClassクラスと一対一に対応していた
  • Java5以降でも具象化可能型はClassクラスと対応しており、互換性が維持されている。
  • 型消去によって具象化可能でない型の情報は消去されるが、コメント的なメタ情報としてリフレクションAPIを使って取得することができる。
  • Java5以降では総称型の情報を取得するためTypeインターフェースを頂点とするメタ型の階層が提供されている。

リフレクションAPIは名前のとおりJava自身の状態を実行時に映し出すためのものであり、APIのクラス設計にJava言語の仕様が表現されています。普段リフレクションAPIを使わないという人も、使い方を調査してみると、Java言語の仕様に関して理解を深めることができると思います。

*1:Java6ではjavax.lang.model.typeパッケージ配下にも同一名のクラス群が格納されていますが、これは実行時のリフレクションとは無関係でソースコード上の解析に関するものです。