Java 的泛型擦除和泛型信息获取
Java 的泛型擦除
擦除
Class c1 = new ArrayList<Integer>().getClass();
Class c2 = new ArrayList<String>().getClass();
System.out.println(c1 == c2);
/* Output
true
*/
ArrayList<Integer> 和 ArrayList<String> 在编译的时候是完全不同的类型。你无法在写代码时,把一个 String 类型的实例加到 ArrayList<Integer> 中。但是在程序运行时,的的确确会输出true。
这就是 Java 泛型的类型擦除造成的,因为不管是 ArrayList<Integer> 还是 ArrayList<String>,在编译时都会被编译器擦除成了 ArrayList。Java 之所以要避免在创建泛型实例时而创建新的类,从而避免运行时的过度消耗。
List<Integer> list = new ArrayList<Integer>();
Map<Integer, String> map = new HashMap<Integer, String>();
System.out.println(Arrays.toString(list.getClass().getTypeParameters()));
System.out.println(Arrays.toString(map.getClass().getTypeParameters()));
/* Output
[E]
[K, V]
*/
我们可能期望能够获得真实的泛型参数,但是仅仅获得了声明时泛型参数占位符。getTypeParameters 方法的 Javadoc 也是这么解释的:仅返回声明时的泛型参数。所以,通过 getTypeParamters 方法无法获得运行时的泛型信息。
擦除到上限
class A<T extends Number> {
}
再用 javap -v cc.unmi.A 来看泛型签名
Signature: #18 // <T:Ljava/lang/Number;>Ljava/lang/Object;
转换
本来a是带有泛型信息,但是b没有,所以在赋值过程中泛型信息就丢失了,b中的T的类型会变成其上限Number
//一下代码中,当把一个带泛型信息的a的实例赋值给一个不带泛型信息的的b的时候,
//a中所有的泛型信息都会发生丢失,也就是说T是Integer的这一信息会丢失,b只知道
//T的类型是Number而已
package ErasureAndConversion;
import UseIt.A1;
class Apple<T extends Number>{
T size;
public Apple(){
}
public Apple(T size){
this.size = size;
}
public void setSize(T size){
this.size = size;
}
public T getSize(){
return this.size;
}
}
public class Erasure {
public static void main(String args[]){
Apple<Integer> a = new Apple<>(6);
// a指向的实例是带有泛型信息的
Integer as = a.getSize();
// 实例a中的T是Integer类型的,所以赋值给as没有任何问题
Apple b = a;
// 这一句就是说明问题的关键了,b是不带泛型信息的,所以a中的泛型信息
// 也就会被擦除,所以,a中的T的类型就只是Number而已了
Number size1 = b.getSize();
// b中的T类型是Number,所以赋值给Number类型的值没有任何问题
// Integer size2 = b.getSize();
// 但是,b中的T并不是Integer的了,因为泛型信息已经被擦除了,所以这一句会
// 报错。
Integer size3 = a.getSize();
// 最后这一句并不会报错,b会发生泛型信息丢失但是a并不会受影响
}
}
下面这两个例子说明的是一样的问题,或者说第二个例子是第一个例子的直观表现,说的是当把带有泛型信息的集合赋值给没有泛型信息的集合时泛型信息就丢失了,所以在把list赋值给ls的时候不会发生问题,因为list已经不知道具体的泛型信息是什么了,所以是Object,所以可以赋值给ls,但是一旦要访问集合中的元素的时候,就会发生类型不匹配的问题。
//java允许把一个List赋值给一个List<Type>所以在下面 List<String> ls = list;
//这一句仅仅只会发生警告而已。
package ErasureAndConversion;
import java.util.ArrayList;
import java.util.List;
public class Erasure2 {
public static void main(String args[]){
List<Integer> li = new ArrayList<>();
li.add(6);
li.add(5);
List list = li;
List<String> ls = list;
// 一样的道理,List没有泛型信息,所以li的泛型信息就丢失了,所以赋值给ls
// 是没有问题的
// System.out.println(ls.get(0));
// 但是当访问ls中的元素的时候,就会发生类型不匹配的问题
}
}
//这个例子和上面的例子是一模一样的
package ErasureAndConversion;
import java.util.ArrayList;
import java.util.List;
public class Erasure3 {
public static void main(String args[]){
List list = new ArrayList();
((ArrayList) list).add(5);
((ArrayList) list).add(4);
// System.out.println((String)list.get(0));
}
}
泛型信息的获取
继承一个泛型基类
class A<T, ID> {
}
class B extends A<String, Integer> {
}
public class Generic {
public static void main(String[] args) {
ParameterizedType parameterizedType = (ParameterizedType) B.class.getGenericSuperclass();
Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
for(Type actualTypeArgument: actualTypeArguments) {
System.out.println(actualTypeArgument);
}
Class clazz = (Class) parameterizedType.getActualTypeArguments()[0];
System.out.println(clazz);
}
}
上面的代码输出:
class java.lang.String
class java.lang.Integer
class java.lang.String
实现一个泛型接口
interface A<T, ID> {
}
class B implements A<String, Integer> {
}
public class Generic {
public static void main(String[] args) {
ParameterizedType parameterizedType = (ParameterizedType) B.class.getGenericInterfaces();
Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
for (Type actualTypeArgument : actualTypeArguments) {
System.out.println(actualTypeArgument);
}
Class clazz = (Class) parameterizedType.getActualTypeArguments()[0];
System.out.println(clazz);
}
}
同样能得到上面的一样的结果。
运行时泛型信息的获取 (假象,实质是通过定义类的方式)
引入一点小知识
匿名内部类
概念:即内部类的简化写法
前提:存在一个类(可以是具体类也可以是抽象类)或接口
格式:new 类名或接口名{重写的方法}
本质:创建的是继承了类或实现了接口的子类匿名对象
匿名类的声明:
匿名类的声明是由java编译器自动派生自一个类实例创建表达式;
匿名类永远不能是抽象的;
匿名类总是隐式的final;
匿名类总是一个内部类,并且不能是static的;
由于Java泛型的实现机制,使用了泛型的代码在运行期间相关的泛型参数的类型会被擦除,我们无法在运行期间获知泛型参数的具体类型(所有的泛型类型在运行时都是Object类型),但是在编译java源代码成 class文件中还是保存了泛型相关的信息,,这些信息被保存在class字节码常量池中,使用了泛型的代码处会生成一个signature签名字段,通过签名signature字段指明这个常量池的地址。
Java 引入泛型擦除的原因是避免因为引入泛型而导致运行时创建不必要的类。通过前面的知识我们其实就可以通过定义类的方式,在类信息中保留泛型信息,从而获得这些泛型信息。简而言之,Java 的泛型擦除是有范围的,即类定义中的泛型是不会被擦除的。
有些场景中,我们需要获取泛型信息的。比如,在调用 HTTP 或 RPC 接口时,我们需要进行序列化和反序列的工作。
例如,我们通过一个 HTTP 接口接收如下的 JSON 数据
[{
"name": "Stark",
"nickName": "Iron Man"
},
{
"name": "Rogers",
"nickName": "Captain America"
}]
我们需要将其映射为 List<Avenger>。
但是之前我们提到了泛型擦除,那我们所使用的 HTTP 或 RPC 框架是如何获取 List 中的泛型信息呢?
如下代码
Map<String, Integer> map = new HashMap<String, Integer>() {};
Type type = map.getClass().getGenericSuperclass();
ParameterizedType parameterizedType = ParameterizedType.class.cast(type);
//ParameterizedType parameterizedType = (ParameterizedType)map.getClass().getGenericSuperclass();
for (Type typeArgument : parameterizedType.getActualTypeArguments()) {
System.out.println(typeArgument.getTypeName());
}
/* Output
java.lang.String
java.lang.Integer
*/
上面这段代码展示了如何获取 map 这个实例所对应类的泛型信息。显然,这次我们成功获得了其中泛型参数信息。有了这些泛型参数,上面所提到的序列化和反序列化工作就是可能的了。
那为什么之前不可以,而这次可以了呢?请注意一个细节
前面的变量声明
Map<Integer, String> map = new HashMap<Integer, String>();
本节中的变量声明
Map<String, Integer> map = new HashMap<String, Integer>() {};
其中最关键的差别是本节的变量声明多了一对大括号,其实是创建了一个匿名内部类,这个类是 HashMap 的子类,泛型参数限定为了 String 和 Integer,这样就通过定义类的方式保留了泛型信息。
框架中的应用
其实很多框架就是使用类定义中的泛型不会被擦除这个特性,实现了相应的功能。
例如,SpringWeb模块的RestTemplate 和 alibaba的fastJson,我们可以使用如下写法:
//这里的ParameterizedTypeReference是一个抽象类,因此约束了必须创建ParameterizedTypeReference的子类,由此成功获取到泛型的实际类型
ResponseEntity<ResponseDTO<UserKeyDTO>> result = restTemplate.exchange(url, null, new ParameterizedTypeReference<ResponseDTO<UserKeyDTO>>(){});
//通过创建TypeReference的匿名内部类的方式来保留反省信息,以便json反序列化时能反射获取到泛型实际类型
ResponseDTO<SysCryptDTO> responseDTO = JSONObject.parseObject(jsonString, new TypeReference<ResponseDTO<SysCryptDTO>>() {});
其中的 new ParameterizedTypeReference<YourType>() {} 就是通过定义一个匿名内部类的方式来获得泛型信息,从而进行反序列化的工作。
总结
Java 泛型擦除是 Java 泛型中的一个重要特性,其目的是避免过多的创建类而造成的运行时的过度消耗。所以,想 ArrayList<Integer> 和 ArrayList<String> 这两个实例,其类实例是同一个。
但很多情况下我们又需要在运行时获得泛型信息,那我们可以通过定义类的方式(通常为匿名内部类,因为我们创建这个类只是为了获得泛型信息)在运行时获得泛型参数,从而满足例如序列化、反序列化等工作的需要。
只要理解了 Java 引入泛型擦除的原因,也自然能理解如何在运行时获取泛型信息了。