Java的泛型擦除和运行时泛型信息获取

探讨Java泛型擦除原理,展示如何通过定义类保留泛型信息,实现序列化与反序列化。

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 引入泛型擦除的原因,也自然能理解如何在运行时获取泛型信息了。

<think>好的,用户现在想了解Java擦除机制对运行时类检查的影响。之前我已经回答过关于不变性以及如何传递List的问题,现在需要更深入地探讨类擦除运行时的表现。 首先,我需要回忆类擦除的基本概念。根据引用[3],类擦除是在编译期间移除信息,将代码转换为原始类。这意味着在运行时,像List<String>这样的会被当作List处理。接下来,用户的问题集中在运行时类检查的影响,所以需要结合例子说明类擦除如何导致运行时类信息丢失。 然后,用户之前的讨论中提到了强制类转换的风险,比如将List<String>强制转换为List<Object>可能导致ClassCastException。这可能运行时类检查有关,因为信息运行时不可用,只能检查原始类。 接下来,我需要考虑用户可能真正想了解的内容。他们可能遇到运行时类错误,或者想理解为什么某些类检查在编译时有效但运行时却失败。例如,引用[4]中的例子通过反射绕过编译时检查,向ArrayList<String>添加整数,这会导致运行时问题,但编译时不会报错。这说明类擦除导致运行时缺乏信息,无法进行有效的类检查。 然后,我需要组织回答的结构。首先解释类擦除的基本原理,然后分点讨论对运行时检查的影响,比如instanceof失效、强制转换风险、数组创建限制、反射绕过的问题以及桥接方法的作用。每个点都需要结合引用中的例子或概念,比如引用[4]中的反射示例,说明运行时如何绕过检查。 另外,用户可能想知道如何规避这些问题,所以需要提到使用通配符或辅助方法进行类检查,以及桥接方法在维护多态性中的作用。比如,引用[2]提到类擦除运行时将当作普通类处理,这可能影响重写方法时的桥接方法生成。 最后,确保回答符合之前的格式要求,使用中文,正确引用标记,并生成相关问题。需要检查引用中的内容是否支持每个论点,例如引用[1]提到确保类安全,但运行擦除可能削弱这一点,引用[4]中的反射例子直接展示了类擦除的影响。 总结来说,用户需要明确理解类擦除如何导致运行时无法获取信息,进而影响类检查,以及开发者需要注意的问题解决方案。同时,结合之前的回答,保持连贯性,确保信息准确且易于理解。</think>Java擦除机制对运行时类检查的影响主要体现在**信息运行时的缺失**,这使得多数与相关的类检查只能在编译阶段完成[^3]。以下是具体分析: --- ### 一、擦除的核心原理 1. **编译时保留,运行擦除** 信息仅存在于编译阶段,编译器会通过类擦除代码转换为原始类(Raw Type): ```java List<String> list = new ArrayList<>(); // 编译后等价于:List list = new ArrayList(); ``` 所有参数(如`String`)会被替换为上限类(默认是`Object`)[^3]。 2. **类安全的伪保障** 编译器在编译时插入隐式的类转换代码,但运行时无法验证具体: ```java String s = list.get(0); // 编译后等价于:String s = (String) list.get(0); ``` --- ### 二、对运行时类检查的影响 #### 1. **`instanceof`操作失效** 运行时无法检测参数: ```java List<String> list = new ArrayList<>(); System.out.println(list instanceof List<String>); // 编译错误 System.out.println(list instanceof List<?>); // 合法,但无法区分具体类 ``` #### 2. **强制类转换风险** 擦除后,强制转换可能绕过编译检查: ```java List list = new ArrayList<String>(); list.add(100); // 编译通过(原始类允许添加任意对象) List<Integer> intList = (List<Integer>) list; // 编译警告,运行时可能抛出ClassCastException ``` #### 3. **无法创建数组** 由于运行时无法确认,直接创建数组会导致编译错误: ```java // 编译错误:Cannot create a generic array of List<String> List<String>[] array = new List<String>[10]; ``` #### 4. **反射绕过类约束** 通过反射可绕过编译时的检查(如引用[4]中的示例): ```java ArrayList<String> list = new ArrayList<>(); Method addMethod = list.getClass().getMethod("add", Object.class); addMethod.invoke(list, 42); // 运行时成功添加Integer,但后续读取时可能抛出ClassCastException ``` #### 5. **桥接方法(Bridge Method)的生成** 为维护多态性,编译器会生成桥接方法: ```java interface Converter<T> { T convert(Object input); } class StringConverter implements Converter<String> { @Override public String convert(Object input) { return input.toString(); } } ``` 编译后,`StringConverter`类会生成一个桥接方法`Object convert(Object)`,调用时实际委托给`String convert(Object)`[^3]。 --- ### 三、规避影响的实践建议 1. **使用通配符辅助类检查** 通过`Class`对象检查容器元素的类: ```java public <T> void checkType(List<T> list, Class<T> type) { if (!list.isEmpty() && !type.isInstance(list.get(0))) { throw new IllegalArgumentException("类不匹配"); } } ``` 2. **优先使用方法而非原始类** 通过方法保留类信息: ```java <T> void process(List<T> list) { T item = list.get(0); // 类安全 } ``` 3. **避免不安全的强制转换** 如需强制转换,需通过额外逻辑确保类一致性。 --- ### 总结 擦除机制在**编译时保障类安全**,但**运行时缺乏信息**,开发者需警惕以下场景: - 反射操作破坏类约束 - 强制转换引发`ClassCastException` - 无法直接依赖运行时类检查[^3][^4] ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值