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内的泛型信息并非在所有的情况下面都会被擦差,在类被编译为类文件时,相关信息就会保存在类文件的元数据里面,这个可以参考一下下面的链接:
- Java为什么要添加运行时获取泛型的方法。知乎上这个问题里面R大的回答。 以及R大在回答里面放出来的链接:
- Reifiable generics与Type erasure generics各有怎样的优点与缺点? - 知乎
- 答复: Java获得泛型类型 - Script Ahead, Code Behind - ITeye博客 - gson/TypeToken.java at 2b15334a4981d4e0bd4f2c2d34b8978a4167f36a · google/gson,Gson里=TypeToken=类的实现 - guice/TypeLiteral.java at abc78c361d9018da211690b673accb580a52abf2 · google/guice,Guice里面的类似实现。 - 其实在guava里面也有一个TypeToken类:guava/TypeToken.java at master · google/guava · GitHub,这个类的接口比gson里面的实现要丰富一些。
- Examining Class Modifiers and Types (The Java™ Tutorials > The Reflection API > Classes)
基本这些信息已经很详细了。
Type接口及相关子类/子接口
Type
接口已有的子接口和实现有:
以及常用的实现=Class<T>=。
除开=Class<T>=(这个具体实现内的方法太多,显示在图内起来反而没什么用处),其他几个接口定义的方法有:
目前常用的子接口是=ParameterizedType=,其包含的方法有
- getActualTypeArguments返回一个元素类型为Type的数组
- getRawType返回定义这个类型的运行时类型,如定义=class A<T>{}=,此处应该返回=A.class=。
- 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里面也有类似的一个运行时反射框架,*因为有隐式参数,写起来会更为简单一些*,具体参考另一个笔记。