首页/新闻资讯/正文详情

Java 泛型是怎么实现的?从类型擦除讲起

发布时间:2026/9/26 4:04:26 来源:云帆数科 栏目:资讯中心
Java 泛型是怎么实现的?从类型擦除讲起
Java 5 之前集合里存的都是Object放进去的时候没人管你放的是什么取出来的时候靠开发者自己保证强转的类型是对的ListlistnewArrayList();list.add(hello);Integeri(Integer)list.get(0);这三行能正常编译跑到第三行抛ClassCastException。编译器在这里帮不上任何忙因为它看到的只有Object。泛型要解决的就是这件事把类型检查从运行期提到编译期顺带把代码里的强转全干掉ListStringlistnewArrayList();list.add(hello);Integerilist.get(0);// 编译期就报错到这里都是表面的东西。真正值得讲的是这些类型信息在编译之后去哪了因为 Java 的处理方式和 C# 完全不一样。类型擦除擦除之后长什么样拿一段最简单的代码publicclassErasure{publicStringfirst(ListStringlist){returnlist.get(0);}publicstaticvoidmain(String[]args){ListStringlistnewArrayList();list.add(hello);Stringslist.get(0);System.out.println(s);}}用javap -c反编译看first方法的字节码javap-c-pErasurepublic java.lang.String first(java.util.Listjava.lang.String); Code: 0: aload_1 1: iconst_0 2: invokeinterface #7, 2 // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object; 7: checkcast #13 // class java/lang/String 10: areturn public static void main(java.lang.String[]); Code: ... 11: invokeinterface #20, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z ... 19: invokeinterface #7, 2 // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object; 24: checkcast #13 // class java/lang/String 27: astore_2这里能直接看出三件事方法签名那一行还写着Listjava.lang.String那是 class 文件里Signature属性还原出来的不是字节码本身真正的调用是List.get:(I)Ljava/lang/Object;返回值是ObjectString已经没了编译器在get后面补了一条checkcast java/lang/String这就是我们代码里没写的那个强转add那条同理参数类型是Ljava/lang/Object;说明擦除之后add(String)变成了add(Object)。整个过程叫类型擦除Type Erasure规则很简单无界的类型参数擦成Object有界的擦成它的上界。classBoxTextendsNumber{privateTvalue;publicTget(){returnvalue;}}擦完之后T变成Number字段是Number value方法是Number get()。真实类型Integer只在编译器脑子里存在用于插入强转。为什么要擦除Java 5 引入泛型的时候List、Map这些集合类已经存在五年了线上跑着大量用原始类型raw type写的代码还有一堆已经编译好的 class 文件。如果泛型做成运行期具现化reifiedC# 的做法ListString和List就是两个不同的类型老代码在新 JVM 上直接跑不了所有库都得重编译整个生态要跟着动。擦除绕开了这个问题ListString编译完还是List新代码和老代码在字节码层面是同一个东西互操作成本为零。代价就是运行期拿不到类型参数下面那一串限制全是从这里来的。C# 没有这个历史包袱它的泛型是运行期具现化的Listint和Liststring在运行时是不同的类型能typeof(Listint)基本类型还能直接当类型参数不用装箱。擦除带来的限制这一串限制看起来零散根因只有一个运行期不知道T是什么。写法为什么不行new T()擦除后变成new Object()不是想要的那个类型new T[10]数组在运行期要记住自己的元素类型ArrayStoreException就靠它擦除后记不住T.class、instanceof T运行期没有T这个类型对象Listint擦除后是ListObjectObject装不下基本类型只能用包装类static T field静态成员属于类而类型参数属于实例catch (MyExceptionT e)JVM 的异常表只认具体的类型用ListString和ListInteger重载擦除后两个方法的签名都是f(List)是同一个方法static那条单独说一下它最能体现擦除的本质。BoxString和BoxInteger在运行期是同一个类静态区只有一份。如果允许static T instance;那这个字段到底该是String还是Integer就说不清了因为两个不同的Box共用它。重载那条的报错很直白实际编译一下看看publicclassOverload{publicvoidf(ListStringlist){}publicvoidf(ListIntegerlist){}}error: name clash: f(ListInteger) and f(ListString) have the same erasurenew T[]有替代方案传一个ClassT进来用Array.newInstance创建或者干脆让调用方传数组进来。Collection.toArray(T[])用的就是后一种签名里那个数组参数不是为了装数据是为了把运行期的类型信息带进来。ListStringlistnewArrayList();String[]arrlist.toArray(newString[0]);// 必须传否则只能拿到 Object[]桥接方法这是擦除最容易被忽略的副作用。publicclassNodeT{privateTdata;publicTget(){returndata;}}publicclassStringNodeextendsNodeString{OverridepublicStringget(){returnx;}}擦除之后父类的方法签名是Object get()子类的是String get()。JVM 判断一个方法有没有被覆盖看的是方法名加描述符返回类型也算在描述符里所以这两个不是同一个方法子类的String get()并没有覆盖父类的Object get()。就这么放着的话通过Node引用调get()会解析到父类那个返回data的实现上去多态就断了。编译器在StringNode里自动生成了一个桥接方法Bridge Method把这条路接回去。javap -v -p StringNode看到的是public java.lang.String get(); descriptor: ()Ljava/lang/String; Code: 0: ldc #7 // String x 2: areturn public java.lang.Object get(); descriptor: ()Ljava/lang/Object; flags: (0x1041) ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC Code: 0: aload_0 1: invokevirtual #9 // Method get:()Ljava/lang/String; 4: areturn两个get。上面那个是我们写的下面那个是编译器生成的flags里的ACC_BRIDGE和ACC_SYNTHETIC就是标记它身份的东西。它的方法体只有一件事就是转调真正的String get()这样父类引用调Object get()的时候才能落到子类的实现上。ACC_SYNTHETIC的意思是编译器生成、源码里没有所以反射遍历方法时能过滤掉它getDeclaredMethods()返回的数组里其实两个都在。泛型不变性与通配符为什么 List Integer 不是 List NumberInteger是Number的子类但ListInteger不是ListNumber的子类。泛型是不变的invariant。假设允许这么写会怎样ListIntegerintsnewArrayList();ListNumbernumsints;// 假设允许nums.add(3.14);// 往 ListNumber 里加 Double完全合法Integeriints.get(0);// 取出来是 Double炸了问题出在nums.add(3.14)这行它借nums这个名字绕过了ints的类型检查回头ints里就混进了Double。所以泛型选择了不变宁可严一点。对比数组数组是协变的Integer[]确实是Number[]的子类型Integer[]intsnewInteger[10];Number[]numsints;// 允许nums[0]3.14;// 运行时抛 ArrayStoreException数组把类型检查放在了运行期靠每条astore指令去核对出问题才抛异常。泛型把检查放在了编译期代码根本编译不过。同样的问题泛型更早发现这也是一般推荐用集合而不是数组的原因之一。通配符和 PECS不变带来的麻烦是一个接收ListNumber的方法没法接收ListInteger哪怕只是读一读。通配符就是来放松这个限制的。写法能读吗能写吗读出来的类型List? extends Number能不能NumberList? super Integer能能写Integer或其子类Object? extends Number的意思是这是元素类型为Number或其子类的 List但具体是哪个我不知道。既然不知道实际是Integer还是Double往里add任何东西都可能违反真实类型所以编译器索性把写操作禁掉。反过来读是安全的不管实际是哪个子类它一定是Number的子类。? super Integer是倒过来的实际类型是Integer或它的父类往里加Integer一定安全因为Integer是实际类型的子类。但读出来的类型不确定只能保证是Object。PECS 是 Producer Extends, Consumer Super 的缩写意思是只往外读的生产者用extends只往里写的消费者用super。JDK 里的Collections.copy是照这个写的publicstaticTvoidcopy(List?superTdest,List?extendsTsrc)src是数据来源只读用extendsdest是落点只写用super。这样Integer的列表可以拷进Number的列表反过来不行正好对应读出来的东西要能放进写进去的地方。泛型信息其实没全丢有个说法是擦除之后泛型信息就没了这话不准确。擦除擦掉的是方法描述符和字段上的类型参数但类的泛型签名还在存在 class 文件的Signature属性里。看Node的 class 文件就很清楚public T get(); descriptor: ()Ljava/lang/Object; Signature: #19 // ()TT; Signature: #23 // T:Ljava/lang/Object;Ljava/lang/Object;descriptor是 JVM 真正用来做方法解析的已经擦成了ObjectSignature是给编译器、反射和 IDE 看的里面TT;就是类型参数T。所以反射有两套 API一套拿擦除后的一套拿带泛型的publicclassReflect{publicListStringgetNames(){returnnull;}publicstaticvoidmain(String[]args)throwsException{MethodmReflect.class.getMethod(getNames);System.out.println(getReturnType() - m.getReturnType());System.out.println(getGenericReturnType() - m.getGenericReturnType());}}getReturnType() - interface java.util.List getGenericReturnType() - java.util.Listjava.lang.String同一个方法两个方法返回的东西不一样差的就是Signature属性。这个特性被拿来绕过擦除。最经典的是匿名子类捕获泛型Gson 的TypeToken就是这么做TypetypenewTypeTokenListString(){}.getType();new TypeTokenListString(){}创建的是TypeToken的一个匿名子类它 extends 的是TypeTokenListString这个父类签名带着泛型信息写进了子类的 class 文件里。所以getGenericSuperclass()能把它完整读出来Gson 就拿到了ListString。Jackson 的TypeReference、Spring 的ParameterizedTypeReference是同一个套路用法上也要求写成匿名子类的形式原因就在这。

相关推荐

什么是扩散模型(Diffusion Model)?
什么是扩散模型(Diffusion Model)?

扩散模型(Diffusion Model)是一类通过「逐步加噪声再逐步去噪」来学习数据分布的生成模型,核心思想在 2015 年由 Sohl-Dickstein 等人提出,并在 2020 年 DDPM 论文后真正引爆。它如今是 Stable Diffusion、DALLE 2、Midjourney 等… · 2026/9/26 4:04:20

小白程序员必看:如何让大模型真正“能办事”?
小白程序员必看:如何让大模型真正“能办事”?

文章指出,尽管大模型能判断,但它们需要运行机制才能改变世界。文章介绍了Harness(执行环境)在模型运行中的关键作用,包括上下文管理、工具接口、约束、验证和纠正。ReAct方法强调“想、做、看、再想”的循环过程。文章… · 2026/9/26 4:04:20

系统级Agent与端侧30B MoE:智能体手机开发实战指南
系统级Agent与端侧30B MoE:智能体手机开发实战指南

最近圈子里聊得最多的,基本绕不开这么一件事:AI 智能体手机开始量产了。各家头部厂商都把“系统级 Agent 端侧大模型”当成下一代旗舰的核心卖点,而且陆续有搭载端侧 30B 级别 MoE 模型的真机落地。作为端侧 AI 开发者,我观察到的… · 2026/9/26 4:04:20

Kimi K3免费使用真相:资源调度、本地部署与vLLM实战指南
Kimi K3免费使用真相:资源调度、本地部署与vLLM实战指南

1. 项目概述:Kimi K3不是“免费白嫖”,而是资源调度的艺术最近两周,朋友圈和AI技术群几乎被“Kimi K3”刷屏。有人晒出每小时200次调用的截图,有人发帖问“为什么我领了赠额却提示额度不足”,还有人凌晨三点在GitHub上… · 2026/9/26 6:03:53

spacedesk无线副屏原理与实战:旧平板变生产力工具
spacedesk无线副屏原理与实战:旧平板变生产力工具

1. 项目概述:为什么“网络副屏”不是噱头,而是真实生产力拐点spacedesk 这个词最近在Windows用户圈里反复刷屏,尤其当有人晒出用一台吃灰三年的小米平板5,连根网线都不接,就稳稳当当地当起了笔记本的第二块扩展屏——左… · 2026/9/26 6:03:53

Eclipse Temurin:生产级OpenJDK发行版的技术原理与落地实践
Eclipse Temurin:生产级OpenJDK发行版的技术原理与落地实践

1. 为什么现在必须认真对待 Eclipse Temurin 的 OpenJDK 如果你最近在搭建 Java 开发环境、部署 Spring Boot 服务,或者只是想给公司服务器换一套稳定可靠的 JDK,那“Eclipse Temurin”这六个字大概率已经出现在你的搜索记录里——不是因为它是新名词&a… · 2026/9/26 6:03:53

WorkBuddy生产级部署蓝皮书:从环境校准到事件驱动工作流
WorkBuddy生产级部署蓝皮书:从环境校准到事件驱动工作流

1. 这不是又一个“安装教程”,而是一份能真正跑通AI工作流的实操蓝皮书WorkBuddy这个词最近在技术圈和产品团队里出现频率越来越高,但很多人点开文档第一眼看到“Node.js环境”“Python依赖”“Docker Compose编排”就下意识关掉页面——不是不想用&… · 2026/9/26 6:03:53

金融系统开发实战:强一致性、分布式事务与合规架构的技术选型指南
金融系统开发实战:强一致性、分布式事务与合规架构的技术选型指南

金融行业这几年变化太快了,快到什么程度?我身边做传统金融IT的朋友,前几年还在维护核心银行系统,现在张口闭口都是实时风控、智能投顾、开放银行API。而"financial-services"这个词,在技术圈里早就不是"… · 2026/9/26 6:03:41

WeKnora 实战:RAG、Agent 与自动 Wiki 构建智能文档库
WeKnora 实战:RAG、Agent 与自动 Wiki 构建智能文档库

文档管理这件事,做过的人都知道痛点在哪:资料越攒越多,找的时候全靠记忆和文件名碰运气;PDF、Word、Markdown、网页剪藏混在一起,想跨格式检索基本靠手动翻;更别提让文档"回答问题"了&#xff0c… · 2026/9/26 6:03:35

数据库课后习题答案别硬背:当测试用例集刷,效率翻倍
数据库课后习题答案别硬背:当测试用例集刷,效率翻倍

简介:万常选版《数据库原理与设计》课后习题答案资源,覆盖第2至6章及第9章,适合正在学习关系模型、数据库建模、关系数据理论与模式求精的本科生、自学者作为复习与自测材料。压缩包共7个文件,含3个doc参考答案、2个sql示例脚本、… · 2026/9/26 0:00:21

OpenClaw 替代品?Hermes Agent 踩坑实录:macOS 飞书接入 TaoToken 配置
OpenClaw 替代品?Hermes Agent 踩坑实录:macOS 飞书接入 TaoToken 配置

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

向下兼容与向上兼容:接口设计中的兼容性策略与工程实践
向下兼容与向上兼容:接口设计中的兼容性策略与工程实践

一次版本升级事故,是很多团队绕不过去的坎。线上环境里,服务端明明已经上线了新版接口,老的移动端还在照着旧文档传参数。请求一到网关,校验直接拒绝,用户操作失败,客服群炸了锅,开发群里开始互… · 2026/9/26 0:00:46

了解更多?预约专属演示

我们的顾问将为您一对一讲解产品与方案

企业微信二维码