SpringのTypeDescriptorを使うと型パラメーターを簡単に取得できる

以前に、Java5の型システムを理解するにはリフレクションAPIを使ってみるのが最短の近道になる - 達人プログラマーを目指してで、Java5からリフレクションAPIで総称型を扱うために導入されたTypeインターフェースについて説明しました。復習しておくと、

  • Classクラスからは型消去された実行時の型情報のみが取得できる
  • Typeインターフェースを使うと、拡張されたリフレクションAPIを使って、フィールドやメソッドなどで宣言されている総称型の情報を得られる

ということでした。しかし、そこでの例で示したようにリフレクションAPIを使って総称型の情報を得るのはチェック例外の処理が面倒ですし、また、例外を無視しても、結構回りくどい処理が必要となります。
今のところあまり知られていないようですが、もし、Spring3が利用できるのであれば、TypeDescriptorというクラスを利用すると、このあたりの処理をうまいことカプセル化してくれるため大変に便利です。*1
TypeDescriptor (Spring Framework 5.1.7.RELEASE API)
このクラスはSpring3から汎用的な型変換のために新たに導入されたConversionServiceのAPIで利用されています。(もちろん、総称型以外の任意の型に対して利用できます。)
5. Validation, Data Binding, and Type Conversion

フィールドの総称型を取得する

たとえば、以下のようにフィールドが宣言されているとします。

    String[] strArray = { "test1", "test2" };
    List<String> strList = Arrays.asList("test", "test2");

    Map<String, Integer> map = new HashMap<String, Integer>();

    @SuppressWarnings("unchecked")
    List<List<String>> strListList = Arrays.asList(strList);

この場合、以下のテストコードが示すようにして、型の情報を簡単に取得できます。なお、実はJUnit4のassertThat()ってしっくりこないんです!(特に、メタプログラミングするレイヤでは) - 達人プログラマーを目指しての最後に書いたように、Classクラスを比較するためのヘルパーメソッドを定義しておきます。

    @Test
    public void testResolveFieldType() {
        TypeDescriptor typeDesc = new TypeDescriptor(ReflectionUtils.findField(getClass(), "strList"));

        assertThat(typeDesc.getType(), isSameClassAs(List.class));
        assertThat(typeDesc.getElementType(), isSameClassAs(String.class));

        assertThat(typeDesc.asString(), is("java.util.List<java.lang.String>"));
        assertThat(typeDesc.isCollection(), is(true));
    }

    @Test
    public void testResolveFieldType_NestedType() {
        TypeDescriptor typeDesc = new TypeDescriptor(ReflectionUtils.findField(getClass(), "strListList"));

        assertThat(typeDesc.getType(), isSameClassAs(List.class));
        assertThat(typeDesc.getElementType(), isSameClassAs(List.class));

        assertThat(typeDesc.asString(), is("java.util.List<java.util.List<java.lang.String>>"));
        assertThat(typeDesc.isCollection(), is(true));

        TypeDescriptor elementTypeDesc = typeDesc.getElementTypeDescriptor();

        assertThat(elementTypeDesc.getType(), isSameClassAs(List.class));
        assertThat(elementTypeDesc.getElementType(), isSameClassAs(String.class));

        assertThat(elementTypeDesc.asString(), is("java.util.List<java.lang.String>"));
        assertThat(elementTypeDesc.isCollection(), is(true));
    }

    @Test
    public void testResolveFieldType_Map() {
        TypeDescriptor typeDesc = new TypeDescriptor(ReflectionUtils.findField(getClass(), "map"));

        assertThat(typeDesc.getType(), isSameClassAs(Map.class));
        assertThat(typeDesc.getMapKeyType(), isSameClassAs(String.class));
        assertThat(typeDesc.getMapValueType(), isSameClassAs(Integer.class));

        assertThat(typeDesc.asString(), is("java.util.Map<java.lang.String, java.lang.Integer>"));
        assertThat(typeDesc.isMap(), is(true));
    }

なお、Spring3.0.4まではバグがあり、2番目のケースのように型パラメーターがネストされている場合に正しく動作しません。https://issues.springsource.org/browse/SPR-7562
なお、Fieldのインスタンスを取得する際にReflectioUtils.findField()を使っていることにも注意してください。このクラスを利用することで、リフレクションのチェック例外の処理が不要になります。

メソッドシグネチャの総称型を取得する

同様にメソッドのシグネチャから型情報を取得することもできます。たとえば、以下のようなメソッドがあったとして、

	List<String> sampleMethod(List<Integer> intList) {
		...
	}

次のように記述できます。

    @Test
    public void testResolveMethodParameterType() {
        Method sampleMethod = ReflectionUtils.findMethod(getClass(), "sampleMethod", List.class);

        TypeDescriptor typeDesc = new TypeDescriptor(new MethodParameter(sampleMethod, 0));

        assertThat(typeDesc.getType(), isSameClassAs(List.class));
        assertThat(typeDesc.getElementType(), isSameClassAs(Integer.class));
        
        assertThat(typeDesc.asString(), is("java.util.List<java.lang.Integer>"));
        assertThat(typeDesc.isCollection(), is(true));
    }

    @Test
    public void testResolveMethodReturnType() {
        Method sampleMethod = ReflectionUtils.findMethod(getClass(), "sampleMethod", List.class);

        TypeDescriptor typeDesc = new TypeDescriptor(new MethodParameter(sampleMethod, -1));

        assertThat(typeDesc.getType(), isSameClassAs(List.class));
        assertThat(typeDesc.getElementType(), isSameClassAs(String.class));

        assertThat(typeDesc.asString(), is("java.util.List<java.lang.String>"));
        assertThat(typeDesc.isCollection(), is(true));
    }

ここでも先ほどの例と同様にReflectionUtils.findMethod()でMethodオブジェクトを取得しています。この場合MethodParameterオブジェクトを作成することで、特定の場所のパラメーターや戻り値を表現します。
なお、メソッドの戻り値の型については共変戻り値型のパラメーターも型消去によらず正しく解決してくれます。

    static abstract class Parent<T> {
        abstract List<T> getList();
    }
    
    static class Child extends Parent<String> {
        @Override
        List<String> getList() {
            return new ArrayList<String>();
        }
    }


    @Test
    public void testResolveCovariantMethodReturnType() {
        Method methodWithCovariantReturnType = ReflectionUtils.findMethod(Child.class, "getList");
        
        TypeDescriptor typeDesc = new TypeDescriptor(new MethodParameter(methodWithCovariantReturnType, -1));
        
        assertThat(typeDesc.getType(), isSameClassAs(List.class));
        assertThat(typeDesc.getElementType(), isSameClassAs(String.class));
    }

要素の型を動的に解決する

TypeDescriptorはリクレクションAPIの便利なラッパーとして使えるだけでなく、配列やコレクションの要素型を実行時に解決するために利用することもできます。

    @Test
    public void testResolveArrayElementType() {
        TypeDescriptor typeDesc = TypeDescriptor.forObject(strArray);
        assertThat(typeDesc.getElementType(), isSameClassAs(String.class));

        assertThat(typeDesc.asString(), is("java.lang.String[]"));
        assertThat(typeDesc.isArray(), is(true));
    }

    @Test
    public void testResolveCollectionElementType() {
        TypeDescriptor typeDesc = TypeDescriptor.forObject(strList);
        assertThat(typeDesc.getElementType(), isSameClassAs(String.class));

        assertThat(typeDesc.asString(), is("java.util.Arrays$ArrayList<java.lang.String>"));
        assertThat(typeDesc.isCollection(), is(true));
    }

もちろん、コレクションの場合、型消去されてしまいますので、要素の型は同一で、かつ、1個以上要素が含まれているインスタンスを渡す必要があります。
なお、現時点では、ネストされた型パラメーターの動的解決はできないようです。
https://issues.springsource.org/browse/SPR-7569
サンプルコードの全体はgistにアップしてあります。
Spring TypeDescriptor test. · GitHub

*1:TypeDescriptorではコレクションやマップなど特別なパターンでのみ総称型を扱えるようになっています。また、ワイルドカードや型変数は扱えません。つまり、C#のクローズ型に相当するコレクションに対応しています。しかし、Springを利用するようなアプリケーションのレイヤで総称型を使うのは通常このどちらかのパターンが8割以上だと思われるので、実質的には大丈夫でしょう。