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

简介: Java泛型在编译时会进行类型擦除,仅保留原始类型(如Object或限定类型),导致运行时无法获取泛型信息。类型擦除带来诸多问题:反射可绕过泛型限制、静态成员不能使用类的泛型参数、instanceof检查泛型类型不合法、基本类型不能作为泛型参数等。此外,编译器通过桥方法解决多态冲突,并在获取泛型对象时自动插入强制类型转换,确保类型安全。

Java泛型类型擦除以及类型擦除带来的问题
1.什么是泛型擦除
我们都知道Java的泛型是伪泛型,即编译期间所有的泛型信息都会被擦除,如我们代码定义了:List和List,但是对于JVM而言,看到的只有List,由泛型附加的类型信息对于JVM而言是看不到的。代码说明如下:
1.1 原始类型擦除后相等
在这个例子中,我们定义了两个ArrayList数组,不过一个是ArrayList泛型类型的,只能存储字符串;一个是ArrayList泛型类型的,只能存储整数,最后,我们通过list1对象和list2对象的getClass()方法获取他们的类的信息,最后发现结果为true。说明泛型类型String和Integer都被擦除掉了,只剩下原始类型。
1.2 反射添加的元素被擦除
如果直接调用add()方法,那么只能存储整数数据,不过当我们利用反射调用add()方法的时候,却可以存储字符串,这说明了Integer泛型实例在编译之后被擦除掉了,只保留了原始类型。
2.什么是泛型擦除后保留的原始类型
原始类型 就是擦除去了泛型信息,最后在字节码中的类型变量的真正类型,无论何时定义一个泛型,相应的原始类型都会被自动提供,类型变量擦除,并使用其限定类型(无限定的变量用Object)替换。举例说明
其对应的原始类型就是
但如果该类的定义有限定,比如继承了,那么就会产生变化:
此时原始类型就是Comparable,而不再是Object
3.泛型擦除引起的问题及解决方法
3.1 先检查,再编译以及编译的对应和引用传递问题
这里我们可能会有一个疑问,既然说类型变量会在编译的时候擦除掉,那为什么上面的ArrayList中添加String类型的时候就报错了呢,因为String编译时候也会变成Object啊?
A:因为JAVA编译器是通过先检查代码中泛型的类型,然后再进行类型擦除,再进行编译的。那么这个检查到底是针对谁的,我们需要再明确下
A2:如我们上面代码是:
现在我们写成:
此时如果我们与之前的代码兼容,各种引用传值之间,必然会出现下面情况:
这样没错,但是会有个编译时警告,不过在第一种情况下,可以实现与完全使用泛型参数一样的效果,但是第二种没有效果。
因为类型检查是编译时完成的,new ArrayList()只是在内存中开辟一个存储空间,可以存储任何类型的对象,而真正涉及类型检查的是“它的引用”,即list1的方法调用,如add方法,所以list1引用能够完成泛型类型检查(前面声明了String),但是list2(后面声明的只是开辟内存空间,不涉及)由于前面的声明没有添加泛型,所以不行。
所以这里我们也大概知道了,所谓的类型(泛型)检查,是针对引用的。谁是一个引用,用这个引用调用泛型方法,就会对这个引用所调用的方法进行类型检查,而无关它真正引用的对象。
3.2 自动类型转换
因为类型擦除的问题,所以所有的泛型类型变量在最后都会被替换成原始类型,既然都被替换了,那么为什么获取的时候,不需要进行强制类型转换呢?可以看下 ArrayList.get() 方法
可以看到,在return之前,会根据泛型变量进行强转。假设泛型类型变量为Date,虽然泛型信息会被擦除掉,但是会将(E) elementData[index],编译为(Date) elementData[index]。所以我们不用自己进行强转。当存取一个泛型域时也会自动插入强制类型转换。假设Pair类的value域是public的,那么表达式:
也会自动地在结果字节码中插入强制类型转换。
3.3 泛型擦除与多态的冲突与解决方法
假设有一个泛型类
然后有一个子类需要继承
在这个子类中,我们设定父类的泛型类型为Pair,在子类中,我们覆盖了父类的两个方法,我们的原意是这样的:将父类的泛型类型限定为Date,那么父类里面的两个方法的参数都为Date类型。
所以,我们在子类中重写这两个方法一点问题也没有,实际上,从他们的@Override标签中也可以看到,一点问题也没有,实际上是这样的吗?
分析:实际上,类型擦除后,父类的的泛型类型全部变为了原始类型Object,所以父类编译之后会变成下面的样子:
而此时,子类中类型依然是Date,这如果还是在继承关系中,那么根本就不是重写,而是重载了。通过反编译会发现子类中的方法Object getValue()和Date getValue()是同 时存在的,可是如果是常规的两个方法,他们的方法签名是一样的,也就是说虚拟机根本不能分别这两个方法。如果是我们自己编写Java代码,这样的代码是无法通过编译器的检查的,但是虚拟机却是允许这样做的,因为虚拟机通过参数类型和返回类型来确定一个方法,所以编译器为了实现泛型的多态允许自己做这个看起来“不合法”的事情,然后交给虚拟器去区别。
3.4 泛型类型变量不能是基本数据类型
不能用类型参数替换基本类型。就比如,没有ArrayList,只有ArrayList。因为当类型擦除后,ArrayList的原始类型变为Object,但是Object类型不能存储double值,只能引用Double的值。
3.5 编译时集合的instanceof(可能面试考察)
Java
运行代码
复制代码
1
ArrayList arrayList = new ArrayList();
因为类型擦除之后,ArrayList只剩下原始类型,泛型信息String不存在了。那么,编译时进行类型查询的时候使用下面的方法是错误的
Java
运行代码
复制代码
1
if( arrayList instanceof ArrayList)
3.6 泛型在静态方法和静态类中的问题(可能面试考察)
泛型类中的静态方法和静态变量不可以使用泛型类所声明的泛型类型参数,举例说明:
Java
运行代码
复制代码
1
2
3
4
5
6
public class Test2 {
public static T one; //编译错误
public static T show(T one){ //编译错误
return null;
}
}
因为泛型类中的泛型参数的实例化是在对象定义时候指定的,而静态变量和静态方法是不需要通过对象来调用的,对象都没有创建,如何确定这个泛型是何类型呢?所以说上面的代码明显是错误的。
但是需要注意下面的一种特殊情况
Java
运行代码
复制代码
1
2
3
4
5
public class Test2 {
public static T show(T one){ //这是正确的
return null;
}
}
因为这是一个泛型方法,在泛型方法中使用过的T是自己在方法中定义的T,而不是泛型中的T

目录
相关文章
|
1天前
|
缓存 算法 Java
线程池
线程池是一种复用线程资源的机制,通过预先创建并管理一组线程,避免频繁创建和销毁线程带来的开销。任务提交到线程池后,由空闲线程执行,提升系统性能与响应速度。Java中通过`ExecutorService`、`ThreadPoolExecutor`等类实现,支持固定、缓存、调度等多种线程池类型,有效控制并发数,优化资源利用。
13 5
|
2天前
|
机器学习/深度学习 人工智能 自然语言处理
SpringAI+DeepSeek大模型应用开发
本教程以SpringAI为核心,讲解Java与大模型(如DeepSeek)融合开发,助力传统项目智能化。介绍AI基础、Transformer原理及SpringAI应用,推动Java在AI时代焕发新生。适合Java程序员入门大模型开发。
58 2
|
1天前
|
监控 算法 Unix
Thread.sleep(0) 到底有什么用(读完就懂)
`Thread.sleep(0)` 并非无用,它会触发操作系统立即重新进行CPU竞争,让出执行权给其他线程。虽然可能马上再次被调度,但为其他线程(如UI线程)执行提供了机会,避免界面假死。在Windows等抢占式系统中,此操作相当于“主动谦让”,提升多线程协作效率。
10 0
|
1天前
|
存储 缓存 算法
零拷贝
零拷贝技术通过减少上下文切换和内存拷贝提升文件传输性能。传统方式需频繁系统调用与数据拷贝,开销大;零拷贝利用内核态直接将磁盘数据送至网卡,结合PageCache实现高效传输,适用于小文件场景,大幅降低CPU消耗,提高并发能力。
5 0
|
2天前
|
Java 测试技术 Linux
生产环境发布管理
本文介绍大型团队中生产环境发布管理的全流程,涵盖从开发到生产的多环境部署策略(dev→test→pre→prod),结合自动化CI/CD平台实现分支管理、一键发布与隔离构建。通过Jenkins+Docker+K8S实现自动化部署,利用Skywalking/ELK完成日志链路追踪与错误排查,提升发布效率与系统稳定性,适用于高协同需求的复杂项目场景。
13 0
|
2天前
|
存储 缓存 安全
One Trick Per Day
每日一技:Map初始化建议用Guava指定预期大小,避免扩容;禁用Executors创建线程池,防止OOM,推荐自定义ThreadPoolExecutor或使用Guava;Arrays.asList返回不可变列表,禁止增删改;遍历Map优先使用entrySet或forEach;SimpleDateFormat非线程安全,建议用ThreadLocal或Java8新时间API;并发修改记录需加锁,推荐乐观锁配合version字段。
16 0
|
2天前
|
SQL 运维 分布式计算
如何做好SQL质量监控
SLS推出SQL质量监控功能,集成于CloudLens for SLS,从健康分、服务指标、运行明细、SQL Pattern及优化建议五大维度,助力用户全面掌握SQL使用情况,提升查询效率与资源管理能力。
20 0
|
2天前
|
运维 Devops 开发工具
生产环境缺陷管理
针对大型团队多分支开发中bug管理难、易遗漏等问题,我们基于go-git打造了通用化工具git-poison,实现分布式、自动化bug追溯与发布卡点。通过“投毒-解毒-银针”机制,精准阻塞带缺陷版本发布,联动发布与运维平台,显著降低协同成本,避免因人为疏漏导致的生产故障,提升研发效能与系统稳定性。
12 0
|
2天前
|
敏捷开发 Dubbo Java
需求开发人日评估
敏捷开发中,需求人日评估至关重要。本文介绍开发、自测、联调、测试及发布各阶段工时估算方法,并提供常见功能如增删改查、导入导出、远程调用等参考人日,助力团队科学排期。
11 0
|
2天前
|
敏捷开发 Java 测试技术
为什么要单元测试
单元测试是保障软件质量的基石。它通过验证代码最小单元的正确性,提升代码可读性、可维护性与稳定性,助力快速定位问题、增强重构信心、提高研发效率,是现代软件工程不可或缺的实践。
12 0