Java泛型类型擦除以及类型擦除带来的问题

本文涉及的产品
RDS DuckDB + QuickBI 企业套餐,8核32GB + QuickBI 专业版
简介: Java泛型在编译时会进行类型擦除,所有泛型信息被移除,替换为原始类型(如Object或限定类型)。例如,List<String>和List<Integer>在运行时均为List,导致无法通过instanceof判断泛型类型。类型检查在编译期完成,基于引用而非实际对象。擦除后,编译器自动插入强制转换保证类型安全。但这也引发多态冲突、静态成员限制等问题,需通过桥方法等机制解决。基本类型不能作为泛型参数,静态上下文中也不能使用类级别泛型参数。

1.什么是泛型擦除
我们都知道Java的泛型是伪泛型,即编译期间所有的泛型信息都会被擦除,如我们代码定义了:List和List,但是对于JVM而言,看到的只有List,由泛型附加的类型信息对于JVM而言是看不到的。代码说明如下:
1.1 原始类型擦除后相等
public class Test {
public static void main(String[] args) {
ArrayList list1 = new ArrayList();
list1.add("abc");
    ArrayList<Integer> list2 = new ArrayList<Integer>();
    list2.add(123);

    System.out.println(list1.getClass() == list2.getClass());
}

}
在这个例子中,我们定义了两个ArrayList数组,不过一个是ArrayList泛型类型的,只能存储字符串;一个是ArrayList泛型类型的,只能存储整数,最后,我们通过list1对象和list2对象的getClass()方法获取他们的类的信息,最后发现结果为true。说明泛型类型String和Integer都被擦除掉了,只剩下原始类型。
1.2 反射添加的元素被擦除
public static void main(String[] args)
throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
ArrayList list = new ArrayList();
list.add(1); //这样调用 add 方法只能存储整形,因为泛型类型的实例为 Integer
list.getClass().getMethod("add", Object.class).invoke(list, "asd");

    for (int i = 0; i < list.size(); i++) {
        // 输出1    asd
        System.out.println(list.get(i));
    }
}

如果直接调用add()方法,那么只能存储整数数据,不过当我们利用反射调用add()方法的时候,却可以存储字符串,这说明了Integer泛型实例在编译之后被擦除掉了,只保留了原始类型。
2.什么是泛型擦除后保留的原始类型
原始类型 就是擦除去了泛型信息,最后在字节码中的类型变量的真正类型,无论何时定义一个泛型,相应的原始类型都会被自动提供,类型变量擦除,并使用其限定类型(无限定的变量用Object)替换。举例说明
class Pair {
private T value;
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}
其对应的原始类型就是
class Pair {
private Object value;
public Object getValue() {
return value;
}
public void setValue(Object value) {
this.value = value;
}
}
但如果该类的定义有限定,比如继承了,那么就会产生变化:
public class Pair {}
此时原始类型就是Comparable,而不再是Object
3.泛型擦除引起的问题及解决方法
3.1 先检查,再编译以及编译的对应和引用传递问题
这里我们可能会有一个疑问,既然说类型变量会在编译的时候擦除掉,那为什么上面的ArrayList中添加String类型的时候就报错了呢,因为String编译时候也会变成Object啊?
A:因为JAVA编译器是通过先检查代码中泛型的类型,然后再进行类型擦除,再进行编译的。那么这个检查到底是针对谁的,我们需要再明确下
A2:如我们上面代码是:
ArrayList list = new ArrayList();
现在我们写成:
ArrayList list = new ArrayList();
此时如果我们与之前的代码兼容,各种引用传值之间,必然会出现下面情况:
ArrayList list1 = new ArrayList(); //第一种 情况
ArrayList list2 = new ArrayList(); //第二种 情况
这样没错,但是会有个编译时警告,不过在第一种情况下,可以实现与完全使用泛型参数一样的效果,但是第二种没有效果。
因为类型检查是编译时完成的,new ArrayList()只是在内存中开辟一个存储空间,可以存储任何类型的对象,而真正涉及类型检查的是“它的引用”,即list1的方法调用,如add方法,所以list1引用能够完成泛型类型检查(前面声明了String),但是list2(后面声明的只是开辟内存空间,不涉及)由于前面的声明没有添加泛型,所以不行。

所以这里我们也大概知道了,所谓的类型(泛型)检查,是针对引用的。谁是一个引用,用这个引用调用泛型方法,就会对这个引用所调用的方法进行类型检查,而无关它真正引用的对象。
3.2 自动类型转换
因为类型擦除的问题,所以所有的泛型类型变量在最后都会被替换成原始类型,既然都被替换了,那么为什么获取的时候,不需要进行强制类型转换呢?可以看下 ArrayList.get() 方法
public E get(int index) {

RangeCheck(index);  

return (E) elementData[index];  

}
可以看到,在return之前,会根据泛型变量进行强转。假设泛型类型变量为Date,虽然泛型信息会被擦除掉,但是会将(E) elementData[index],编译为(Date) elementData[index]。所以我们不用自己进行强转。当存取一个泛型域时也会自动插入强制类型转换。假设Pair类的value域是public的,那么表达式:
Date date = pair.value;
也会自动地在结果字节码中插入强制类型转换。
3.3 泛型擦除与多态的冲突与解决方法
假设有一个泛型类
class Pair {

private T value;  

public T getValue() {  
    return value;  
}  

public void setValue(T value) {  
    this.value = value;  
}  

}
然后有一个子类需要继承
class DateInter extends Pair {

@Override  
public void setValue(Date value) {  
    super.setValue(value);  
}  

@Override  
public Date getValue() {  
    return super.getValue();  
}  

}
在这个子类中,我们设定父类的泛型类型为Pair,在子类中,我们覆盖了父类的两个方法,我们的原意是这样的:将父类的泛型类型限定为Date,那么父类里面的两个方法的参数都为Date类型。
所以,我们在子类中重写这两个方法一点问题也没有,实际上,从他们的@Override标签中也可以看到,一点问题也没有,实际上是这样的吗?
分析:实际上,类型擦除后,父类的的泛型类型全部变为了原始类型Object,所以父类编译之后会变成下面的样子:
class Pair {
private Object value;

public Object getValue() {  
    return value;  
}  

public void setValue(Object  value) {  
    this.value = value;  
}  

}
而此时,子类中类型依然是Date,这如果还是在继承关系中,那么根本就不是重写,而是重载了。通过反编译会发现子类中的方法Object getValue()和Date getValue()是同 时存在的,可是如果是常规的两个方法,他们的方法签名是一样的,也就是说虚拟机根本不能分别这两个方法。如果是我们自己编写Java代码,这样的代码是无法通过编译器的检查的,但是虚拟机却是允许这样做的,因为虚拟机通过参数类型和返回类型来确定一个方法,所以编译器为了实现泛型的多态允许自己做这个看起来“不合法”的事情,然后交给虚拟器去区别。
3.4 泛型类型变量不能是基本数据类型
不能用类型参数替换基本类型。就比如,没有ArrayList,只有ArrayList。因为当类型擦除后,ArrayList的原始类型变为Object,但是Object类型不能存储double值,只能引用Double的值。
3.5 编译时集合的instanceof(可能面试考察)
ArrayList arrayList = new ArrayList();
因为类型擦除之后,ArrayList只剩下原始类型,泛型信息String不存在了。那么,编译时进行类型查询的时候使用下面的方法是错误的
if( arrayList instanceof ArrayList)
3.6 泛型在静态方法和静态类中的问题(可能面试考察)
泛型类中的静态方法和静态变量不可以使用泛型类所声明的泛型类型参数,举例说明:
public class Test2 {
public static T one; //编译错误
public static T show(T one){ //编译错误
return null;
}
}
因为泛型类中的泛型参数的实例化是在对象定义时候指定的,而静态变量和静态方法是不需要通过对象来调用的,对象都没有创建,如何确定这个泛型是何类型呢?所以说上面的代码明显是错误的。

但是需要注意下面的一种特殊情况
public class Test2 {
public static T show(T one){ //这是正确的
return null;
}
}
因为这是一个泛型方法,在泛型方法中使用过的T是自己在方法中定义的T,而不是泛型中的T

相关文章
|
6月前
|
消息中间件 NoSQL Java
SpringCloud
本课程基于黑马程序员2025版SpringCloud实战教学,聚焦高频面试与实际应用,涵盖微服务、RabbitMQ、Redis高级、ElasticSearch等核心内容,结合项目实战与AI模块优化,助力掌握企业级开发技能。资料详尽,尊重原创,侵权必究。
|
6月前
|
Arthas 存储 运维
记Arthas实现一次CPU排查与代码热更新
本文介绍如何使用Arthas排查线上Java应用CPU占用过高问题,结合thread、watch、jad等指令定位阻塞线程与异常代码,实现无需重启服务的热更新修复,并通过profile生成火焰图进行性能分析,提升线上问题排查效率。
|
6月前
|
存储 算法 BI
xxljob本地运行
本文介绍XXL-JOB分布式任务调度的使用教程。包含源码获取、服务端部署(数据库导入、配置修改、启动访问)、客户端运行与执行器注册,以及调度任务配置和路由策略详解。通过实际操作演示任务创建、参数设置、手动执行与日志查看,帮助开发者快速掌握XXL-JOB核心功能,实现定时任务的可视化管理和高效调度。(238字)
|
6月前
|
消息中间件 Java 数据库
异步消息组件MQ高级
本文详解RabbitMQ消息可靠性保障机制,涵盖生产者重试、确认机制(Confirm/Return)、消息持久化及消费可靠性。通过配置重试、回调处理与失败消息表结合定时任务重发,确保消息不丢失,提升系统稳定性。
|
6月前
|
安全 Java 应用服务中间件
实现权限管理的技术
权限管理技术选型需综合考量。常见方案如Apache Shiro配置简单但安全维护弱;Spring Security功能强大但较复杂;自定义ACL灵活但理解成本高。多数框架基于ACL或RBAC二次封装,应根据项目实际选择最合适的方案。
|
6月前
|
JavaScript Dubbo Java
Http
基于HTTP表单的远程调用协议,采用Spring HttpInvoker实现,支持多短连接、同步传输,序列化为表单格式。适用于参数大小混合、提供者多于消费者的服务场景,可被应用程序和浏览器JS共用,支持URL或表单调用,不支持文件传输。要求参数符合Bean规范,推荐使用Servlet Bridge模式部署,需与Servlet容器端口及上下文路径一致。
|
6月前
|
Dubbo Java 应用服务中间件
3.Hessian
Hessian 1协议基于HTTP通信,采用Servlet暴露服务,Dubbo内置Jetty支持。支持与原生Hessian服务互操作,多连接短连接,同步传输,Hessian二进制序列化,适用于参数较大、提供者较多场景,如页面及文件传输。需实现Serializable,限制自定义集合类。配置简单,支持多端口与直连。
|
6月前
|
存储 监控 Java
整合切面,参数拦截+过滤
该类基于Spring AOP实现请求参数日志记录,通过`@Before`、`@Around`和`@After`切面拦截Controller层方法,自动记录请求来源、URL、方式、参数及执行耗时,便于调试与监控,日志通过LogProxy输出,提升系统可观测性。(238字)
|
6月前
|
XML SQL 监控
整合Logback,滚动记录+多文件
`logback-spring.xml` 配置了多模块日志分离输出,按类别将支付、任务、SQL、错误等日志写入不同文件,支持滚动策略与UTF-8编码。通过 `LogProxy.getLogger(&quot;LOG_NAME&quot;)` 获取指定日志器,实现精准日志记录,便于问题追踪与系统监控。(236字符)
|
6月前
|
XML JSON Java
映射关系(1-1 1-n n-n)
MyBatis中通过resultMap实现关联映射:一对一使用resultMap解决字段与属性名不一致;一对多在“一”方配置&lt;collection&gt;,如用户包含多个角色;多对一通过&lt;association&gt;关联,如博客关联作者;多对多借助中间类,双方均用&lt;collection&gt;维护集合关系。

热门文章

最新文章