Java的Type接口和Guava的TypeToken类

2017-12-2

Java里的 Class<T> 类用于表示运行时的类型信息,相关的泛型参数会被擦除,如

@Test
public void rawReflectionTest() {
    // (1)
    List<String> stringList = Lists.newArrayList();
    // (2)
    List<Integer> intList = Lists.newArrayList();

    boolean result = stringList.getClass()
            .isAssignableFrom(intList.getClass());

    Truth.assertThat(result).isTrue();
}

这个代码片段里面,(1)处和(2)处声明的类型分别为 List<String>List<Integer> ,但运行时都会擦除,实际运行类型为原始的 List 类。

但Java内的泛型信息并非在所有的情况下面都会被擦差,在类被编译为类文件时,相关信息就会保存在类文件的元数据里面,这个可以参考一下下面的链接:

基本这些信息已经很详细了。

Type接口及相关子类/子接口

Type接口已有的子接口和实现有:

以及常用的实现=Class<T>=。

除开=Class<T>=(这个具体实现内的方法太多,显示在图内起来反而没什么用处),其他几个接口定义的方法有:

目前常用的子接口是=ParameterizedType=,其包含的方法有

  1. getActualTypeArguments返回一个元素类型为Type的数组
  2. getRawType返回定义这个类型的运行时类型,如定义=class A<T>{}=,此处应该返回=A.class=。
  3. getOwnerType,返回外层类所对应的=Type=。

一个简单的例子如:

public class ReflectionTest {

    @Test
    public void testParameterizedType() throws Exception {
        //(3)
        Type superclass = B.class.getGenericSuperclass();
        //(4)
        Truth.assertThat(superclass).isInstanceOf(ParameterizedType.class);
        ParameterizedType parameterizedType = (ParameterizedType) superclass;
        //(5)
        Truth.assertThat(parameterizedType.getRawType()).isEqualTo(A.class);
        //(6)
        Truth.assertThat(parameterizedType.getOwnerType())
                .isEqualTo(ReflectionTest.class);
        //(7)
        Truth.assertThat(parameterizedType.getActualTypeArguments())
                .isEqualTo(new Type[]{String.class});
    }

    // (1)
    class A<T> {}

    // (2)
    class B extends A<String> {}
}

这里(1)处定义了一个泛型基类=A<T>=,(2)处定义了继承自=A<String>=的类=B=。测试例内(3)处通过=Class<T>=类的=getGenericSuperclass=方法获取=B=对应的超类类型。*这里获取到的类型便是完整的编译期信息*,(4)处测试其对应的类型应为=ParameterizedType=,(5)、(6)、(7)三处分别测试了=getRawType=、=getOwnerType=和=getActualTypeArguments=对应的返回值。

Core Java内关于泛型的一章最后也有完整的一个例子:GenericReflectionTest

=Type=及其已有的子接口,除开可以使用类似上面这样的,使用Java库自带的相关API直接获取已定义好的类的对应=Type=之外,也可以直接实现对应的子接口,构造一个类型信息,如:

Type type = new ParameterizedType() {
    @Override
    public Type[] getActualTypeArguments() {
        return new Type[]{String.class};
    }

    @Override
    public Type getRawType() {
        return List.class;
    }

    @Override
    public Type getOwnerType() {
        return null;
    }
};

此处定义的=type=,便和=List<String>=等同,有时候反序列化时,便有直接构造继承自=Type=的匿名实例的场景。

Guava里面TypeToken

Guava里的=TypeToken=,在原生的结构上面稍微做了一些封装,用起来会更简单一点,文档可以参考:ReflectionExplained · google/guava Wiki

用法如:

TypeToken<String> stringTok = TypeToken.of(String.class);
TypeToken<Integer> intTok = TypeToken.of(Integer.class);

带泛型的用法如:

TypeToken<List<String>> stringListTok = new TypeToken<List<String>>() {}; // (1)

注意如(1)处,*必须定义一个匿名类出来:想要在运行期获得编译期类型信息,那么对应的类文件必须存在(或者凭空构造Type的实例)*。

这个用法Gson里面很常见,如:

List<Integer> s = new Gson().fromJson("[1, 2, 3]",
    new TypeToken<List<Integer>>(){}.getType()
)

通配符(wildcard)类型或者动态的解析/构造类型也是可以的(TypeToken这一节都抄的Guava在Github上的文档):

如通配符类型:

TypeToken<Map<?, ?>> wildMapTok = new TypeToken<Map<?, ?>>() {};

动态解析类型:

static <K, V> TypeToken<Map<K, V>> mapToken(TypeToken<K> keyToken, TypeToken<V> valueToken) {
  return new TypeToken<Map<K, V>>() {}
    .where(new TypeParameter<K>() {}, keyToken)
    .where(new TypeParameter<V>() {}, valueToken);
}

// somewhere else
TypeToken<Map<String, BigInteger>> mapToken = mapToken(
   TypeToken.of(String.class),
   TypeToken.of(BigInteger.class));
TypeToken<Map<Integer, Queue<String>>> complexToken = mapToken(
   TypeToken.of(Integer.class),
   new TypeToken<Queue<String>>() {});

动态解析类型这种场景基本目前我只在反序列化时遇到,一个简单的例子如:

Gson内反序列化Map时,一般都要写全类型:

Map<String, Integer> m = new Gson().fromJson(
    "{\"a\": 1, \"b\": 2}",
    new TypeToken<Map<String, Integer>>(){}.getType()
)

使用动态的解析类型,可以写成这样:

@Test
public void testDynamicTypeToken() throws Exception {
    Map<String, Integer> m = myFromJson(
            "{\"a\":1,\"b\":2}",
            TypeToken.of(Integer.class).getType()
    );
    Truth.assertThat(m.get("a")).isInstanceOf(Integer.class);
}

public <T> Map<String, T> myFromJson(String s, Type t) {
    return new Gson().fromJson(s,
            // (1)
            mapToken(
                    TypeToken.of(String.class),
                    TypeToken.of(t)
            ).getType()
    );
}

static <K, V> TypeToken<Map<K, V>> mapToken(TypeToken<K> keyToken, TypeToken<V> valueToken) {
    return new TypeToken<Map<K, V>>() {}
            .where(new TypeParameter<K>() {}, keyToken)
            .where(new TypeParameter<V>() {}, valueToken);
}

上面的代码片段中,(1)处便动态的构造TypeToken。

还有一个方法是resolveType,=TypeToken=的这个方法可以构造出所有一个泛型类类使用参数实例化后(这个说的应该不是很准确)对应方法和字段的类型,如定义类:

abstract class ForResolve<T1, T2, T3, T4, T5, T6> {
    public T1 a;
    public List<T2> b;
    public Map<T3, T4> c;

    abstract public Map<T5, T6> f(Set<T1> d, String s);
}

则使用resolveType:

@Test
public void testTypeResolve() throws Exception {
    TypeToken<?> t = new TypeToken<
        ForResolve<
            // (1)
            Double, String, Integer, Boolean, Map<String, Double>, A>
    >(){};
    // (2)
    TypeToken<?> a = t.resolveType(
            ForResolve.class.getField("a").getGenericType());
    // java.lang.Double
    System.out.println(a);
    // (3)
    TypeToken<?> b = t.resolveType(ForResolve.class.getField("b")
            .getGenericType());
    // java.util.List<java.lang.String>
    System.out.println(b);
    // (4)
    TypeToken<?> c = t.resolveType(ForResolve.class.getField("c")
            .getGenericType());
    // java.util.Map<java.lang.Integer, java.lang.Boolean>
    System.out.println(c);
    // (5)
    Method f = ForResolve.class.getMethod("f", Set.class, String.class);
    TypeToken<?> fReturn = t.resolveType(f.getGenericReturnType());
    // java.util.Map<java.util.Map<java.lang.String, java.lang.Double>,
    //   io.onriv.common.base.ReflectionTest$A>
    TypeToken<?> fArg1 = t.resolveType(f.getGenericParameterTypes()[0]);
    // java.util.Set<java.lang.Double>
    System.out.println(fArg1);
    TypeToken<?> fArg2 = t.resolveType(f.getGenericParameterTypes()[1]);
    // java.lang.String
    System.out.println(fArg2);
}

(1)处使用六个类型=Double,String,Integer,Boolean,Map<String, Double>,A=作为类型参数实例了=ForResolve=。(2)、(3)、(4)、(5)处分别得到了实例后所有字段和方法的类型。

用原生的Type和对应那些子类,一样可以把这些信息全部拿到,就是写起来繁杂一些。但使用了TypeToken,还是可以使得代码稍微简化一些。

总结

这个技巧目前来看基本都用在序列化和反序列化上面。Scala里面也有类似的一个运行时反射框架,*因为有隐式参数,写起来会更为简单一些*,具体参考另一个笔记。