Javaのジェネリック型引数をリフレクションによって取得する方法
概要
Javaは、Java1.5からジェネリクス(総称型)が利用可能になったが、1.4以前のコードとの互換性のためにイレージャ(Erasure)とよばれる手法をとっている。
簡単にいうと、イレージャとは、コンパイラによってジェネリクスの型チェックが行われたあと、コンパイル結果であるJavaバイトコードからは、そのジェネリクス型の型情報を消してしまう、という手法である。
しかし、消されているのはバイトコードだけであり、クラスファイル中に含まれるクラスやインターフェイスの型情報、フィールドの型情報、メソッドの引数や戻り値の型情報などにはジェネリック型の情報が残されている。
これらのジェネリック型引数の情報はリフレクションによってアクセスすることができる。
※ 本ソースコード一式はgist:c8f088fc081054116266011ae56d4bd7に置いてあります。
イレージャは何を消し、何を残しているのか?
Javaのジェネリクスはコンパイル時のみ型チェックされイレージャによってバイトコードからは型情報が消去される。
したがって、
package jp.seraphyware.example; import java.util.ArrayList; import java.util.List; class Foo<E> { private List<E> values = new ArrayList<>(); public void add(E value) { values.add(value); } @Override public String toString() { return values.toString(); } } public class GenericErasureExample { public static void main(String... args) { Foo<String> foo1 = new Foo<>(); Foo<Integer> foo2 = new Foo<>(); Foo fooRaw = new Foo(); foo2.add(1234); ((Foo<String>)((Foo)foo2)).add("abcd"); // 大丈夫!?!? System.out.println(foo2); // [1234, abcd]と表示する } }
というコードにおいて、foo1, foo2, fooRawの3つのオブジェクトは、いずれも、同じFooクラスからインスタンスが作成されることになる。
コンパイル時しかジェネリックの型チェックは行われていないので、このような強引なコードを書いてもエラーにはならず、実行すれば動作してしまう。
Classfile /C:/work/workspace/Java8Learn/bin/jp/seraphyware/example/GenericErasureExample.class
Last modified 2018/01/31; size 1028 bytes
MD5 checksum 39d5b60dcd560b681380dc774a1af52d
Compiled from "GenericErasureExample.java"
public class jp.seraphyware.example.GenericErasureExample
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Class #2 // jp/seraphyware/example/GenericErasureExample
#2 = Utf8 jp/seraphyware/example/GenericErasureExample
#3 = Class #4 // java/lang/Object
#4 = Utf8 java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Utf8 Code
#8 = Methodref #3.#9 // java/lang/Object."<init>":()V
#9 = NameAndType #5:#6 // "<init>":()V
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Ljp/seraphyware/example/GenericErasureExample;
#14 = Utf8 main
#15 = Utf8 ([Ljava/lang/String;)V
#16 = Class #17 // jp/seraphyware/example/Foo
#17 = Utf8 jp/seraphyware/example/Foo
#18 = Methodref #16.#9 // jp/seraphyware/example/Foo."<init>":()V
#19 = Methodref #20.#22 // java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
#20 = Class #21 // java/lang/Integer
#21 = Utf8 java/lang/Integer
#22 = NameAndType #23:#24 // valueOf:(I)Ljava/lang/Integer;
#23 = Utf8 valueOf
#24 = Utf8 (I)Ljava/lang/Integer;
#25 = Methodref #16.#26 // jp/seraphyware/example/Foo.add:(Ljava/lang/Object;)V
#26 = NameAndType #27:#28 // add:(Ljava/lang/Object;)V
#27 = Utf8 add
#28 = Utf8 (Ljava/lang/Object;)V
#29 = String #30 // abcd
#30 = Utf8 abcd
#31 = Fieldref #32.#34 // java/lang/System.out:Ljava/io/PrintStream;
#32 = Class #33 // java/lang/System
#33 = Utf8 java/lang/System
#34 = NameAndType #35:#36 // out:Ljava/io/PrintStream;
#35 = Utf8 out
#36 = Utf8 Ljava/io/PrintStream;
#37 = Methodref #38.#40 // java/io/PrintStream.println:(Ljava/lang/Object;)V
#38 = Class #39 // java/io/PrintStream
#39 = Utf8 java/io/PrintStream
#40 = NameAndType #41:#28 // println:(Ljava/lang/Object;)V
#41 = Utf8 println
#42 = Utf8 args
#43 = Utf8 [Ljava/lang/String;
#44 = Utf8 list1
#45 = Utf8 Ljp/seraphyware/example/Foo;
#46 = Utf8 list2
#47 = Utf8 listRaw
#48 = Utf8 LocalVariableTypeTable
#49 = Utf8 Ljp/seraphyware/example/Foo<Ljava/lang/String;>;
#50 = Utf8 Ljp/seraphyware/example/Foo<Ljava/lang/Integer;>;
#51 = Utf8 SourceFile
#52 = Utf8 GenericErasureExample.java
{
public jp.seraphyware.example.GenericErasureExample();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #8 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 19: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Ljp/seraphyware/example/GenericErasureExample;
public static void main(java.lang.String...);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC, ACC_VARARGS
Code:
stack=2, locals=4, args_size=1
0: new #16 // class jp/seraphyware/example/Foo
3: dup
4: invokespecial #18 // Method jp/seraphyware/example/Foo."<init>":()V
7: astore_1
8: new #16 // class jp/seraphyware/example/Foo
11: dup
12: invokespecial #18 // Method jp/seraphyware/example/Foo."<init>":()V
15: astore_2
16: new #16 // class jp/seraphyware/example/Foo
19: dup
20: invokespecial #18 // Method jp/seraphyware/example/Foo."<init>":()V
23: astore_3
24: aload_2
25: sipush 1234
28: invokestatic #19 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
31: invokevirtual #25 // Method jp/seraphyware/example/Foo.add:(Ljava/lang/Object;)V
34: aload_2
35: ldc #29 // String abcd
37: invokevirtual #25 // Method jp/seraphyware/example/Foo.add:(Ljava/lang/Object;)V
40: getstatic #31 // Field java/lang/System.out:Ljava/io/PrintStream;
43: aload_2
44: invokevirtual #37 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
47: return
LineNumberTable:
line 22: 0
line 23: 8
line 24: 16
line 26: 24
line 27: 34
line 28: 40
line 29: 47
LocalVariableTable:
Start Length Slot Name Signature
0 48 0 args [Ljava/lang/String;
8 40 1 list1 Ljp/seraphyware/example/Foo;
16 32 2 list2 Ljp/seraphyware/example/Foo;
24 24 3 listRaw Ljp/seraphyware/example/Foo;
LocalVariableTypeTable:
Start Length Slot Name Signature
8 40 1 list1 Ljp/seraphyware/example/Foo<Ljava/lang/String;>;
16 32 2 list2 Ljp/seraphyware/example/Foo<Ljava/lang/Integer;>;
}
SourceFile: "GenericErasureExample.java"
mainメソッドの中身をみると、(Java1.4以前のように)単純にFooクラスのnewを呼び出して、1234というint値をInteger#valueOfで変換したものと、定数プールから取り出した文字列オブジェクト「abcd」を、addで追加している様子がわかる。
ここではジェネリックの型引数はどこにも考慮されていない。
ところが、LocalVariableTypeTableセクション1をみると、ジェネリック引数が記録されているのが分かる。
つまり、クラスファイル中からジェネリック型引数が全て消去されているわけではない。
あくまでもバイトコードから消去されているだけである。
Fooクラスのクラスファイルもjavapで見てみる。
Classfile /C:/work/workspace/Java8Learn/bin/jp/seraphyware/example/Foo.class
Last modified 2018/01/31; size 939 bytes
MD5 checksum 38ecd671af23752df7f1ae96faab521d
Compiled from "GenericErasureExample.java"
class jp.seraphyware.example.Foo<E extends java.lang.Object> extends java.lang.Object
minor version: 0
major version: 52
flags: ACC_SUPER
Constant pool:
#1 = Class #2 // jp/seraphyware/example/Foo
#2 = Utf8 jp/seraphyware/example/Foo
#3 = Class #4 // java/lang/Object
#4 = Utf8 java/lang/Object
#5 = Utf8 values
#6 = Utf8 Ljava/util/List;
#7 = Utf8 Signature
#8 = Utf8 Ljava/util/List<TE;>;
#9 = Utf8 <init>
#10 = Utf8 ()V
#11 = Utf8 Code
#12 = Methodref #3.#13 // java/lang/Object."<init>":()V
#13 = NameAndType #9:#10 // "<init>":()V
#14 = Class #15 // java/util/ArrayList
#15 = Utf8 java/util/ArrayList
#16 = Methodref #14.#13 // java/util/ArrayList."<init>":()V
#17 = Fieldref #1.#18 // jp/seraphyware/example/Foo.values:Ljava/util/List;
#18 = NameAndType #5:#6 // values:Ljava/util/List;
#19 = Utf8 LineNumberTable
#20 = Utf8 LocalVariableTable
#21 = Utf8 this
#22 = Utf8 Ljp/seraphyware/example/Foo;
#23 = Utf8 LocalVariableTypeTable
#24 = Utf8 Ljp/seraphyware/example/Foo<TE;>;
#25 = Utf8 add
#26 = Utf8 (Ljava/lang/Object;)V
#27 = Utf8 (TE;)V
#28 = InterfaceMethodref #29.#31 // java/util/List.add:(Ljava/lang/Object;)Z
#29 = Class #30 // java/util/List
#30 = Utf8 java/util/List
#31 = NameAndType #25:#32 // add:(Ljava/lang/Object;)Z
#32 = Utf8 (Ljava/lang/Object;)Z
#33 = Utf8 value
#34 = Utf8 Ljava/lang/Object;
#35 = Utf8 TE;
#36 = Utf8 toString
#37 = Utf8 ()Ljava/lang/String;
#38 = Methodref #3.#39 // java/lang/Object.toString:()Ljava/lang/String;
#39 = NameAndType #36:#37 // toString:()Ljava/lang/String;
#40 = Utf8 SourceFile
#41 = Utf8 GenericErasureExample.java
#42 = Utf8 <E:Ljava/lang/Object;>Ljava/lang/Object;
{
private java.util.List<E> values;
descriptor: Ljava/util/List;
flags: ACC_PRIVATE
Signature: #8 // Ljava/util/List<TE;>;
jp.seraphyware.example.Foo();
descriptor: ()V
flags:
Code:
stack=3, locals=1, args_size=1
0: aload_0
1: invokespecial #12 // Method java/lang/Object."<init>":()V
4: aload_0
5: new #14 // class java/util/ArrayList
8: dup
9: invokespecial #16 // Method java/util/ArrayList."<init>":()V
12: putfield #17 // Field values:Ljava/util/List;
15: return
LineNumberTable:
line 6: 0
line 7: 4
line 6: 15
LocalVariableTable:
Start Length Slot Name Signature
0 16 0 this Ljp/seraphyware/example/Foo;
LocalVariableTypeTable:
Start Length Slot Name Signature
0 16 0 this Ljp/seraphyware/example/Foo<TE;>;
public void add(E);
descriptor: (Ljava/lang/Object;)V
flags: ACC_PUBLIC
Signature: #27 // (TE;)V
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: getfield #17 // Field values:Ljava/util/List;
4: aload_1
5: invokeinterface #28, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
10: pop
11: return
LineNumberTable:
line 10: 0
line 11: 11
LocalVariableTable:
Start Length Slot Name Signature
0 12 0 this Ljp/seraphyware/example/Foo;
0 12 1 value Ljava/lang/Object;
LocalVariableTypeTable:
Start Length Slot Name Signature
0 12 0 this Ljp/seraphyware/example/Foo<TE;>;
0 12 1 value TE;
public java.lang.String toString();
descriptor: ()Ljava/lang/String;
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: getfield #17 // Field values:Ljava/util/List;
4: invokevirtual #38 // Method java/lang/Object.toString:()Ljava/lang/String;
7: areturn
LineNumberTable:
line 15: 0
LocalVariableTable:
Start Length Slot Name Signature
0 8 0 this Ljp/seraphyware/example/Foo;
LocalVariableTypeTable:
Start Length Slot Name Signature
0 8 0 this Ljp/seraphyware/example/Foo<TE;>;
}
SourceFile: "GenericErasureExample.java"
Signature: #42 // <E:Ljava/lang/Object;>Ljava/lang/Object;
ここで注目すべき点は、
- class jp.seraphyware.example.Foo<E extends java.lang.Object> extends java.lang.Object
- private java.util.List<E> values;
- public void add(E);
のように、クラス定義、フィールド定義、メソッド定義の、それぞれのジェネリック型引数は、ちゃんと残っている。
クラスまたはインターフェイスといったもののように「型情報」としてクラスファイルに記録される場合にはジェネリックの型引数は保持されるのである。
Type型によるジェネリック型引数へのアクセス
型情報にジェネリックの型引数が記録されているが、これはリフレクションによってアクセス可能になっている。
Java1.5から、その目的のための「Type」というインターフェイスが追加されている。
Typeインターフェイスは以下の5つのいずれかの種類になる
- Class (いわゆる普通のクラス2)
- TypeVariable (ジェネリックの型変数名)
- ParameterizedType (普通のジェネリック型。List<E>のようなジェネリック型引数を付与された型)
- GenericArrayType (ジェネリックを要素にもつ配列型)
- WildcardType (ワイルドカード。?を使った総称型)
このType情報は、Class、Method, Field のリフレクション使用時に、ジェネリック対応情報用のメソッドとして呼び出すことで取得できるようになっている。(旧来の呼び方をすると、ジェネリック情報の無い型情報になる。)
たとえば、toStringにしても、toGenericStringというジェネリック対応メソッドが用意されている。
System.out.println(Foo.class.toString()); // 従来のクラスの表示 System.out.println(Foo.class.toGenericString()); // ジェネリック型を含むクラスの表示
結果は、
class jp.seraphyware.example.Foo class jp.seraphyware.example.Foo<E>
のようになる。
準備
以下、ジェネリック型と、それを使うメソッドから、ジェネリックの型引数情報を取得するコードを示す
実験につかうジェネリックの定義
/** * ジェネリック型の定義 * @param <E> */ interface GenericIntf<E extends Date> { void set(E value); E get(); } /** * ジェネリック型を継承した具象クラスの定義 */ class ConcreteCls implements GenericIntf<Timestamp> { private Timestamp value; @Override public void set(Timestamp v) { this.value = v; } @Override public Timestamp get() { return value; } } /** * ジェネリック型を継承したジェネリッククラスの定義 */ class GenericCls<E extends Date> implements GenericIntf<E> { private E value; @Override public void set(E v) { this.value = v; } @Override public E get() { return value; } }
ジェネリック型引数を解析・表示するコード
/** * 型情報を表示する * 型情報がさらにネストした型情報をもっている場合は再帰的に掘り進む. * @param typ */ private static void showType(Type typ) { showType(typ, ""); } /** * 型情報を表示する. * 型情報がさらにネストした型情報をもっている場合は再帰的に掘り進む. * @param typ 型 * @param prefix 印字する文字列のまえに付与する文字列 */ private static void showType(Type typ, String prefix) { System.out.print(prefix + "typ=" + typ); String typeName; if (typ instanceof Class) { typeName = "Class"; // クラス } else if (typ instanceof TypeVariable) { typeName = "TypeVariable"; // 型変数型 } else if (typ instanceof ParameterizedType) { typeName = "ParameterizedType"; // ジェネリック型 } else if (typ instanceof GenericArrayType) { typeName = "GenericArrayType"; // ジェネリック配列型 } else if (typ instanceof WildcardType) { typeName = "WildcardType"; // ワイルドカード型 } else { // 不明? (java9時点では、上記5種のみのはず) typeName = typ.getClass().getName(); } System.out.println(" -- " + typeName); if (typ instanceof ParameterizedType) { // ジェネリック型の場合は、その型引数を取得する Type[] genericArgs = ((ParameterizedType) typ).getActualTypeArguments(); // 実際の型引数の取得 for (int idx = 0; idx < genericArgs.length; idx++) { showType(genericArgs[idx], prefix + "(" + (idx + 1) + ") "); } } else if (typ instanceof GenericArrayType) { // ジェネリック配列の場合は、その要素型を取得する. Type compType = ((GenericArrayType) typ).getGenericComponentType(); // 要素型の取得 showType(compType, prefix + "(elm) "); } else if (typ instanceof TypeVariable) { // 型変数型の場合は型の制約(範囲)を取得する Type[] bounds = ((TypeVariable<?>) typ).getBounds(); // 範囲の取得 for (int idx = 0; idx < bounds.length; idx++) { showType(bounds[idx], prefix + "(bound#" + (idx + 1) + ") "); } } else if (typ instanceof WildcardType) { // ワイルドカード型の場合は型の制約(上限・下限)を取得する. Type[] ups = ((WildcardType) typ).getUpperBounds(); // 範囲(上限)の取得 for (int idx = 0; idx < ups.length; idx++) { showType(ups[idx], prefix + "(upper#" + (idx + 1) + ") "); } Type[] lows = ((WildcardType) typ).getLowerBounds(); // 範囲(下限)の取得 for (int idx = 0; idx < lows.length; idx++) { showType(lows[idx], prefix + "(lower#" + (idx + 1) + ") "); } } }
クラスのインスタンス化の実験
以下、ジェネリックの型生成方法ごとの、その型情報の取得結果を示す。
ジェネリック派生型をメソッド内で利用する場合
@Comment("ジェネリック派生型をメソッド内で利用する例1") private static void testGenericUse1() { System.out.println("☆" + new ThisMethod() {}); ConcreteCls obj = new ConcreteCls(); System.out.println("target=" + obj.getClass().toGenericString()); Type typ = obj.getClass().getGenericInterfaces()[0]; // 実装しているインターフェイスを取得 showType(typ); System.out.println(); }
結果
☆testGenericUse1(ジェネリック派生型をメソッド内で利用する例1) target=class jp.seraphyware.example.ConcreteCls typ=jp.seraphyware.example.GenericIntf<java.sql.Timestamp> -- ParameterizedType (1) typ=class java.sql.Timestamp -- Class
この例では、ジェネリックインターフェイスGenericIntfを実装した具象クラスConcreteClsのクラス情報から、クラスが実装しているインターフェイス情報のジェネリック版(getGenericInterfaces)でインターフェイスの型を取得している。(1つしかインターフェイスを実装していないので添え字は0で決めうちしている)
ConcreteClsでは、ジェネリックの型引数としてjava.sql.Timestampを指定しており、リフレクションによっても、指定されているのがjava.sql.Timestamp型であることが示されている。
ジェネリック派生型をメソッド内で利用する場合2
@Comment("ジェネリック派生型をメソッド内で利用する例2") private static void testGenericUse2() { System.out.println("☆" + new ThisMethod() {}); GenericCls<java.sql.Date> obj = new GenericCls<>(); System.out.println("target=" + obj.getClass().toGenericString()); Type typ = obj.getClass().getGenericInterfaces()[0]; // 実装しているインターフェイスを取得 showType(typ); System.out.println(); }
結果
☆testGenericUse2(ジェネリック派生型をメソッド内で利用する例2) target=class jp.seraphyware.example.GenericCls<E> typ=jp.seraphyware.example.GenericIntf<E> -- ParameterizedType (1) typ=E -- TypeVariable (1) (bound#1) typ=class java.util.Date -- Class
この例ではジェネリックの型引数として、型変数を指定したままとなっており、それをメソッド中でjava.sql.Date型として利用している。
リフレクションによって取得できるのはクラスファイルに記録されている型情報であるので、この場合は、java.sql.Date型ではなく、GenericClsのクラス定義で使われた「E」という型変数名として取れている。
また、型変数Eは「E extends Date」という範囲を指定しているため、bounds情報として「java.util.Date」が明らかにされている。
ジェネリック型を匿名クラスとしてメソッド内で利用する例1
@Comment("ジェネリック型を匿名クラスとしてメソッド内で利用する例1") private static void testGenericAnonymous1() { System.out.println("☆" + new ThisMethod() {}); GenericIntf<Date> obj = new GenericIntf<Date>() { // 匿名型の生成 @Override public void set(Date v) {} @Override public Date get() { return null; } }; System.out.println("target=" + obj.getClass().toGenericString()); Type typ = obj.getClass().getGenericInterfaces()[0]; // 実装しているインターフェイスを取得 showType(typ); System.out.println(); }
結果
☆testGenericUse0(ジェネリック型を匿名クラスとしてメソッド内で利用する例1) target=class jp.seraphyware.example.GenericReflectionExample$2 typ=jp.seraphyware.example.GenericIntf<java.util.Date> -- ParameterizedType (1) typ=class java.util.Date -- Class
GenericIntfインターフェイスをメソッド内で匿名クラスとして実体化している。
この場合、クラス定義が作成されることになるため、匿名クラス宣言時に使用した「Date型」が、ジェネリックの型引数として取得できるようになっている。
ジェネリック型を匿名クラスとしてメソッド内で利用する例2
@Comment("ジェネリック型を匿名クラスとしてメソッド内で利用する例2") private static void testGenericAnonymous2() { System.out.println("☆" + new ThisMethod() {}); GenericCls<Date> obj = new GenericCls<Date>() {}; // 匿名クラス化する System.out.println("target=" + obj.getClass().toGenericString()); Type typ = obj.getClass().getGenericSuperclass(); // 匿名クラスの親クラスを取得 showType(typ); System.out.println(); }
結果
☆testGenericAnonymous2(ジェネリック型を匿名クラスとしてメソッド内で利用する例2) target=class jp.seraphyware.example.GenericReflectionExample$4 typ=jp.seraphyware.example.GenericCls<java.util.Date> -- ParameterizedType (1) typ=class java.util.Date -- Class
GenericClsジェネリッククラスをメソッド内で単にnewすると型情報としてはジェネリック引数は残らないが、この例のように、GenericClsジェネリッククラスをメソッド内で匿名クラス化することで、ジェネリック引数つきのクラスとしての型情報を作成することができる。このため、親クラスの型情報としてDate型の型引数を取得できるようになる。
メソッドの引数または戻り値としてのジェネリック型引数の情報の実験例
以下、メソッドの引数または戻り値としてジェネリックの型引数の情報を取得する実験を示す
ジェネリック型を引数にとるメソッドの例1
対象コード
public static void method1(List<String> args) { } @Comment("ジェネリック型を引数にとるメソッドの例1") private static void testGenericArgument1() throws NoSuchMethodException, SecurityException { System.out.println("☆" + new ThisMethod() {}); Method method = GenericExample.class.getMethod("method1", List.class); System.out.println("target=" + method.toGenericString()); Type typ = method.getGenericParameterTypes()[0]; // 引数の型 showType(typ); System.out.println(); }
結果
☆testGenericArgument1(ジェネリック型を引数にとるメソッドの例1) target=public static void jp.seraphyware.example.GenericExample.method1(java.util.List<java.lang.String>) typ=java.util.List<java.lang.String> -- ParameterizedType (1) typ=class java.lang.String -- Class
メソッドのジェネリック対応の引数情報はgetGenericParameterTypesで取得することができる。 (ここでは引数は1個なので、添え字0で先頭のTypeを決めうちで取得している)
引数がList<String>であることが明らかとなっている。
ジェネリック型を引数にとるメソッドの例2
対象コード
public static void method2(List<List<GenericIntf<java.sql.Date>>> args) { } @Comment("ジェネリック型を引数にとるメソッドの例2") private static void testGenericArgument2() throws NoSuchMethodException, SecurityException { System.out.println("☆" + new ThisMethod() {}); Method method = GenericExample.class.getMethod("method2", List.class); System.out.println("target=" + method.toGenericString()); Type typ = method.getGenericParameterTypes()[0]; // 引数の型 showType(typ); System.out.println(); }
結果
☆testGenericArgument2(ジェネリック型を引数にとるメソッドの例2) target=public static void jp.seraphyware.example.GenericExample.method2(java.util.List<java.util.List<jp.seraphyware.example.GenericIntf<java.sql.Date>>>) typ=java.util.List<java.util.List<jp.seraphyware.example.GenericIntf<java.sql.Date>>> -- ParameterizedType (1) typ=java.util.List<jp.seraphyware.example.GenericIntf<java.sql.Date>> -- ParameterizedType (1) (1) typ=jp.seraphyware.example.GenericIntf<java.sql.Date> -- ParameterizedType (1) (1) (1) typ=class java.sql.Date -- Class
リストのリストのジェネリック型のようにネストしたジェネリック型の場合でも、トラバースして末端に至るまでの型情報「List<List<GenericIntf<Date>>>」を特定することができている。
ジェネリック型を戻り値にとるメソッドの例
public static Map<String, GenericIntf<java.sql.Date>> method3() { return Collections.emptyMap(); } @Comment("ジェネリック型を戻り値にとるメソッドの例") private static void testGenericReturnType1() throws NoSuchMethodException, SecurityException { System.out.println("☆" + new ThisMethod() {}); Method method = GenericExample.class.getMethod("method3"); System.out.println("target=" + method.toGenericString()); Type typ = method.getGenericReturnType(); // 戻り値の型 showType(typ); System.out.println(); }
結果
☆testGenericReturnType1(ジェネリック型を戻り値にとるメソッドの例) target=public static java.util.Map<java.lang.String, jp.seraphyware.example.GenericIntf<java.sql.Date>> jp.seraphyware.example.GenericExample.method3() typ=java.util.Map<java.lang.String, jp.seraphyware.example.GenericIntf<java.sql.Date>> -- ParameterizedType (1) typ=class java.lang.String -- Class (2) typ=jp.seraphyware.example.GenericIntf<java.sql.Date> -- ParameterizedType (2) (1) typ=class java.sql.Date -- Class
ジェネリック対応の戻り値の型情報はgetGenericReturnTypeによって取得することができる。
引数の場合と同様にジェネリック型を特定できている。
この例ではMap型なので「Map<String, GenericIntf<Date>>」という2つの型引数をもっていることが分かる。
ジェネリック型を戻り値にとるメソッドの例2
@Comment("ジェネリック型を戻り値にとるメソッドの例2") private static void testGenericReturnType2() throws NoSuchMethodException, SecurityException { System.out.println("☆" + new ThisMethod() {}); GenericCls<java.sql.Date> obj = new GenericCls<>(); Method method = obj.getClass().getMethod("get"); System.out.println("target=" + method.toGenericString()); Type typ = method.getGenericReturnType(); // 戻り値の型 showType(typ); System.out.println(); }
結果
☆testGenericReturnType2(ジェネリック型を戻り値にとるメソッドの例2) target=public E jp.seraphyware.example.GenericCls.get() typ=E -- TypeVariable (bound#1) typ=class java.util.Date -- Class
GenericClsクラスの定義のgetメソッドの戻り値は「E extends Date」であるため、 メソッドの型情報として型変数名の「E」と、その範囲である「java.util.Date」が取得されている。
ジェネリック配列を戻り値にとるメソッドの例1
@SuppressWarnings("unchecked") public static GenericIntf<Date>[] getGenericArray() { // メソッド定義の戻り値型の情報としてジェネリック配列型は有効である return new GenericIntf[0]; // ただし、ジェネリック配列を直接作成できないのでRAW型配列として作成する. } @Comment("ジェネリック配列を戻り値にとるメソッドの例1") private static void testGenericArray() throws NoSuchMethodException, SecurityException { System.out.println("☆" + new ThisMethod() {}); Method method = GenericExample.class.getMethod("getGenericArray"); System.out.println("target=" + method.toGenericString()); Type typ = method.getGenericReturnType(); // 戻り値の型 showType(typ); System.out.println(); }
結果
☆testGenericArray(ジェネリック配列を戻り値にとるメソッドの例1) target=public static jp.seraphyware.example.GenericIntf<java.util.Date>[] jp.seraphyware.example.GenericExample.getGenericArray() typ=jp.seraphyware.example.GenericIntf<java.util.Date>[] -- GenericArrayType (elm) typ=jp.seraphyware.example.GenericIntf<java.util.Date> -- ParameterizedType (elm) (1) typ=class java.util.Date -- Class
Javaではジェネリック配列は鬼門であり、たとえば new List<String>[0]のようなジェネリックの配列を直接作成することはできない。3
が、しかし、型情報としては有効であり、そのための専用のタイプであるGenericArrayTypeというTypeも用意されている。
このgetGenericComponentTypeメソッドによって、配列のジェネリック要素型を取得することができる。
ジェネリック配列の配列を戻り値にとるメソッドの例2
@SuppressWarnings("unchecked") public static GenericIntf<Date>[][] getGenericArray2() { // メソッド定義の戻り値型の情報としてジェネリック配列型は有効である return new GenericIntf[][]{}; // ただし、ジェネリック配列を直接作成できないのでRAW型配列として作成する. } @Comment("ジェネリック配列の配列を戻り値にとるメソッドの例2") private static void testGenericArray2() throws NoSuchMethodException, SecurityException { System.out.println("☆" + new ThisMethod() {}); Method method = GenericExample.class.getMethod("getGenericArray2"); System.out.println("target=" + method.toGenericString()); Type typ = method.getGenericReturnType(); // 戻り値の型 showType(typ); System.out.println(); }
結果
☆testGenericArray2(ジェネリック配列の配列を戻り値にとるメソッドの例2) target=public static jp.seraphyware.example.GenericIntf<java.util.Date>[][] jp.seraphyware.example.GenericExample.getGenericArray2() typ=jp.seraphyware.example.GenericIntf<java.util.Date>[][] -- GenericArrayType (elm) typ=jp.seraphyware.example.GenericIntf<java.util.Date>[] -- GenericArrayType (elm) (elm) typ=jp.seraphyware.example.GenericIntf<java.util.Date> -- ParameterizedType (elm) (elm) (1) typ=class java.util.Date -- Class
ネストしたジェネリック配列は、普通にGenericArrayTypeの中にGenericArrayTypeが入っているだけである。 扱い方は基本的に変わらない。
ジェネリックメソッドの型引数の取得方法
メソッドの引数や戻り値ではなく、ジェネリックメソッド自身のジェネリック型引数の取得例を示す。
public static <A extends Date & java.io.Serializable, B> B genericMethodExample(A arg) { return null; } @Comment("ジェネリックメソッドの例") private static void testGenericMethod() throws NoSuchMethodException, SecurityException { System.out.println("☆" + new ThisMethod() {}); Method method = GenericExample.class.getMethod("genericMethodExample", Date.class); System.out.println("target=" + method.toGenericString()); { Type typ = method.getGenericParameterTypes()[0]; // 戻り値の型 showType(typ); } { // メソッドに付与されているパラメータ Type[] typs = method.getTypeParameters(); for (Type typ : typs) { System.out.println("*メソッドの型引数: " + typ); showType(typ); } } System.out.println(); }
結果
☆testGenericMethod(ジェネリックメソッドの例) target=public static <A,B> B jp.seraphyware.example.GenericExample.genericMethodExample(A) typ=A -- TypeVariable (bound#1) typ=class java.util.Date -- Class (bound#2) typ=interface java.io.Serializable -- Class *メソッドの型引数: A typ=A -- TypeVariable (bound#1) typ=class java.util.Date -- Class (bound#2) typ=interface java.io.Serializable -- Class *メソッドの型引数: B typ=B -- TypeVariable (bound#1) typ=class java.lang.Object -- Class
ジェネリックメソッドの型パラメータは、MethodクラスのgetTypeParametersによって取得することができる。
ワイルドカード型のフィールドの例
public static final List<? super Number> wildcardFieldSuper = new ArrayList<>(); public static final List<? extends Number> wildcardFieldExtends = new ArrayList<>(); @Comment("ワイルドカード型のフィールドの例") private static void testWildcard() throws SecurityException, NoSuchFieldException { System.out.println("☆" + new ThisMethod() {}); for (String name : new String[]{"wildcardFieldSuper", "wildcardFieldExtends"}) { Field field = GenericExample.class.getField(name); System.out.println("target=" + field.toGenericString()); Type typ = field.getGenericType(); showType(typ); } System.out.println(); }
結果
☆testWildcard(ワイルドカード型のフィールドの例) target=public static final java.util.List<? super java.lang.Number> jp.seraphyware.example.GenericExample.wildcardFieldSuper typ=java.util.List<? super java.lang.Number> -- ParameterizedType (1) typ=? super java.lang.Number -- WildcardType (1) (upper#1) typ=class java.lang.Object -- Class (1) (lower#1) typ=class java.lang.Number -- Class target=public static final java.util.List<? extends java.lang.Number> jp.seraphyware.example.GenericExample.wildcardFieldExtends typ=java.util.List<? extends java.lang.Number> -- ParameterizedType (1) typ=? extends java.lang.Number -- WildcardType (1) (upper#1) typ=class java.lang.Number -- Class
ワイルドカード型の場合は範囲として上限・下限を取得することができる。 上限、または下限はワイルドカードの指定がsuper, extendsのいずれかによって、いずれか一方は暗黙でObjectになる。(なので、superなのかextendsなのかを判定することができる。)
まとめ
Javaのジェネリックはイレージャによってバイトコード上からは型引数は消されているが、クラスまたはインターフェイス、および、そのメソッド、引数、戻り値の型情報としては型引数は保持されているため、クラスまたはインターフェイスとして定義されていればジェネリックの型引数を取得することも可能である。
ジェネリックの型引数にアクセスしたいケースでは、匿名クラス化するなどしてジェネリック型引数を含むクラス型を作成するなどの工夫があれば便利にアクセスできるようになる。
おまけ
実験コード中にある
System.out.println("☆" + new ThisMethod() {});
のようなコードは、具体的には、以下のクラスを定義している。
/** * この抽象クラスをメソッド内で匿名クラス化した場合、 * その匿名クラス化したメソッドへの情報にアクセスできるようにするためのヘルパ. */ abstract class ThisMethod { /** * この抽象クラスを匿名クラス化したメソッドを取得する. * @return メソッド */ public Method getMethod() { return getClass().getEnclosingMethod(); } /** * この抽象クラスを匿名クラス化したメソッドがCommentアノテーションをもっている場合、そのコメントを取得する. * なければnull * @return */ public String getComment() { Comment comment = getMethod().getAnnotation(Comment.class); return comment != null ? comment.value() : null; } /** * この抽象クラスを匿名クラス化したメソッドのメソッド名とコメントをプリントする. */ public String toString() { StringBuilder buf = new StringBuilder(); buf.append(getMethod().getName()); String comment = getComment(); if (comment != null) { buf.append("(" + comment + ")"); } return buf.toString(); } } /** * メソッドにコメントを付与するアノテーション */ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD, ElementType.TYPE}) @interface Comment { String value(); }
Javaの匿名クラスの奇妙な性質に、匿名クラスを宣言したメソッドを取得する、という変なメソッド getEnclosingMethodが用意されている。
なので、上記のような抽象クラスを定義しておくと、new ThisMethod(){}で、そのメソッドの名前と、そのメソッドに付与されているアノテーション情報をプリントする、といったようなツールを作ることができる。
匿名クラス化は、このほかにも、標準のJavaEEでもAnnotationLiteralといった、匿名クラス化を利用することで型情報を取得する、といったテクニックに活用されている。