seraphyの日記

日記というよりは過去を振り返るときのための単なる備忘録

Javaのジェネリック型引数をリフレクションによって取得する方法

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クラスからインスタンスが作成されることになる。

コンパイル時しかジェネリックの型チェックは行われていないので、このような強引なコードを書いてもエラーにはならず、実行すれば動作してしまう。

javapでバイトコードを逆アセンブルしてみると、

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つのいずれかの種類になる

このType情報は、ClassMethod, 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といった、匿名クラス化を利用することで型情報を取得する、といったテクニックに活用されている。


  1. なお、LocalVariableTable, LocalVariableTypeTable, は デバッグ情報なので、デバックOffでコンパイルすると出てこない。デバッグ情報に対してリフレクション等でアクセスする標準の手段はないので、コードからの利用価値は(残念ながら)ない。

  2. ClassクラスはJava1.5以降、Typeインターフェイスを実装するように拡張されている。

  3. ジェネリック配列は、RAW型として作成してからジェネリック型にcastすればつくれないことはない。(もちろん、@SuppressWarningsで警告をつぶすなどの汚い処理は必要になるし、むろん安全ではない。)