news 2026/9/3 6:57:50

List<String> 和 List<Integer>,运行时其实是同一个 Class

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
List<String> 和 List<Integer>,运行时其实是同一个 Class

「Java 进阶之路」系列 Day26

写在前面

模块三集合框架写到这篇收官,回过头讲讲一直陪着这些集合类出现、却很少被单独拎出来讲透的泛型。很多人知道"Java泛型是伪泛型"这个说法,但问一句"伪在哪、具体擦除掉了什么",就说不清楚了。这篇把类型擦除的机制和几个由此产生的经典坑讲明白。


一、是什么:泛型只存在于编译期,运行时会被擦除

源码里写List of String

编译器做类型检查

编译后擦除成普通的List

运行时JVM只看到List 完全不知道泛型参数是什么

Java的泛型是编译期的类型检查机制<T>这类类型参数只在编译阶段起作用,编译器会在这个阶段帮你检查类型是否匹配、该插入自动类型转换的地方插入转换代码;一旦编译完成,字节码里泛型信息就被"擦除"了,List<String>List<Integer>在运行时其实就是同一个List,用一段代码可以直接验证:

List<String>stringList=newArrayList<>();List<Integer>intList=newArrayList<>();System.out.println(stringList.getClass()==intList.getClass());// true!

这就是"类型擦除"这个说法的直接体现——JVM运行时根本不知道也不关心一个List装的是String还是Integer,泛型参数信息在编译产出.class文件的那一刻就已经不存在了。


二、为什么要这样设计:向后兼容是首要考量

Java泛型是JDK5才引入的,在这之前所有集合类都是直接存Object,取出来自己强制转型。如果泛型引入之后彻底改变运行时的类型表示(比如像C++模板那样,为每种类型参数各自生成一份独立的字节码),那JDK5之前编译出来的、大量存在的老代码将没法和新代码互相调用——类型擦除本质上是用"泛型信息只在编译期存在"这种设计,换来了新旧代码在运行时层面的无缝兼容。代价是运行时丢失了泛型的具体类型信息,由此产生了下面这些容易踩的坑。

擦除的具体规则:没有限定的类型参数(比如<T>)会被擦除成Object;有限定的类型参数(比如<T extends Comparable>)会被擦除成这个限定的上界(也就是Comparable)。


三、怎么用:类型擦除带来的几个经典坑

坑一:不能直接创建泛型数组

classBox<T>{// T[] arr = new T[10]; // 编译报错!Object[]arr=newObject[10];// 只能这样绕过去}

数组在运行时需要明确知道自己的元素类型(用来做ArrayStoreException这类运行时类型检查),但泛型参数T在运行时已经被擦除成了Object,JVM压根不知道T具体是什么类型,没法创建一个"运行时类型明确"的数组,所以这行代码直接被编译器拒绝。

坑二:静态成员不能使用类的泛型参数

classBox<T>{// static T value; // 编译报错!// static T getValue() { ... } // 编译报错!}

类的泛型参数T是和具体实例绑定的(new Box<String>()new Box<Integer>()是两个不同泛型实例化),但静态成员属于类本身,在类加载的时候就已经存在,不依赖任何具体实例——这时候T到底该是String还是Integer根本无从谈起,所以类的泛型参数不能用在静态字段和静态方法上。

坑三:反射能绕开泛型的编译期检查

List<String>list=newArrayList<>();list.add("hello");try{Methodadd=list.getClass().getMethod("add",Object.class);add.invoke(list,100);// 通过反射,硬塞进去一个Integer!}catch(Exceptione){}System.out.println(list);// ["hello", 100],编译期类型检查形同虚设

泛型的类型检查完全是编译器插入的语法糖,运行时add方法的真实签名其实是add(Object)T被擦除成了Object)。用反射直接拿到这个方法调用,跳过了编译器插入的类型检查逻辑,照样能把一个类型不匹配的元素塞进"声明为List<String>"的集合里——这也说明了泛型提供的类型安全,仅仅是编译期的一层保护,不是运行时真正的类型隔离。


四、面试追问

Q1:什么是类型擦除,Java泛型为什么被称为"伪泛型"?

类型擦除是指泛型的类型参数信息只在编译阶段存在,用于编译期的类型检查,编译完成后字节码里这些类型参数信息就被擦除掉了:没有限定的类型参数擦除成Object,有限定的擦除成限定的上界。因为运行时已经不存在真正独立的泛型类型(List<String>List<Integer>运行时是同一个Class),和C++模板那种为每种类型生成独立代码的"真泛型"不同,所以Java的泛型常被称为"伪泛型"。

Q2:为什么 List<String> 和 List<Integer> 的 getClass() 返回结果相等?

因为类型擦除之后,两者在运行时其实是同一个类——ArrayList。泛型参数StringInteger只在编译期用于类型检查,编译完成后这部分信息就不存在了,JVM在运行时看到的只是普通的ArrayList,不会区分它当初是用什么泛型参数声明的。

Q3:为什么不能创建泛型数组,比如 new T[10]?

因为数组在运行时需要明确知道自己的元素类型,用来支持类似ArrayStoreException这样的运行时类型检查;但泛型参数T在运行时已经被擦除掉了,JVM并不知道T具体对应哪个类型,无法创建一个运行时类型信息完整的数组,所以编译器直接禁止这种写法。

Q4:为什么类的静态成员不能使用这个类声明的泛型参数?

因为类的泛型参数是和具体的实例化绑定的,比如Box<String>Box<Integer>是两种不同的泛型实例化;而静态成员属于类本身,在类加载阶段就已经存在,不依赖任何具体的实例,这时候根本没有一个明确的泛型参数可以对应,所以类的泛型参数不能用在静态字段和静态方法上。

Q5:反射为什么能绕过泛型的类型检查,往 List<String> 里塞进一个 Integer?

因为泛型提供的类型检查是编译器在编译阶段插入的语法糖,add方法真正的运行时签名因为类型擦除已经变成了add(Object)。通过反射直接获取这个方法并调用,跳过了编译器在正常调用路径上插入的类型检查逻辑,运行时ArrayList本身并不会校验传进来的对象是不是String,所以能够成功把类型不匹配的元素塞进去,这也说明泛型的类型安全只在编译期有效,不是运行时层面真正的类型隔离。


下一篇预告

模块三集合框架到这篇正式收官。Day27 开始进入模块四"异常与IO",讲清楚 Checked 异常和 Unchecked 异常的本质区别,以及自定义异常该怎么设计才合理。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/3 6:57:00

MiMo-V2.5 多模态内容生成实战指南

在内容创作领域,我们常常面临一个共同的痛点:需求量大、场景复杂,但人力和时间却极其有限。无论是电商大促期间需要海量短视频素材,还是教育行业要为不同学生定制专属习题,传统的手工制作模式往往显得力不从心。很多团队不得不陷入“加班赶工 - 质量下降 - 反复修改”的恶…

作者头像 李华
网站建设 2026/9/3 6:55:54

基于YOLOv8的水质污染监测系统:从算法到完整应用的工程实践

简介&#xff1a;本资源是一套基于YOLOv8实现的水质污染目标检测系统&#xff0c;面向计算机、人工智能、自动化等专业的本科生及初学者&#xff0c;解决水体中典型污染物&#xff08;如油膜、藻类、悬浮物、垃圾、泡沫&#xff09;的自动识别与可视化监测问题&#xff0c;特别…

作者头像 李华
网站建设 2026/9/3 6:53:03

【需求设计】小写数字金额转换成中文大写金额

amount2cn 设计文档 小写数字金额 → 中文大写金额转换. 原文参考:https://blog.csdn.net/liudglink/article/details/106820490本文件为设计说明,代码实现请访问 gitee仓库 [零]用法与整体流水线 多文件程序(同属 package main),无外部依赖,由 go.mod 管理,直接运行: go r…

作者头像 李华
网站建设 2026/9/3 6:52:37

双曲线方程全解析:从定义推导到高考解题技巧

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/3 6:50:17

Linux之socket编程(四)----Tcp

Tcp tcp进行网络通信在很多部分和udp通信时很相似的 大纲 init 初始化和udp很相似 就是socket的第二个参数时sock_stream 除了上面这些 litsen Tcp依然还需要litsen接口 监听就是有点像接电话,有人打进来电话你就要接 start accept accept的返回值是有些不同的,相对于udp…

作者头像 李华
网站建设 2026/9/3 6:49:03

无线耳机固件升级全解析:从文件名解读到升级后优化

简介&#xff1a;本资源是JieLi&#xff08;杰理&#xff09;AC701n系列耳塞主控芯片JL701n的官方固件升级包v162&#xff0c;面向嵌入式音频设备开发者、固件工程师及硬件调试人员&#xff0c;用于完成耳塞产品的固件烧录、功能验证与ANC/LC3/Opus等音频特性适配。压缩包共124…

作者头像 李华