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