为什么 Java 泛型这么聪明,结果运行时却像什么都不知道?
全文目录开篇语前言一、泛型到底是干嘛的——编译器的“贴心保姆”二、类型擦除Type Erasure所谓“运行时失忆”是怎么搞出来的1. 擦除的基本思想2. 擦除的具体步骤简化理解版三、泛型擦除带来的各种“你不能”这些限制不是你笨是 JVM 真做不到 1. 不能 new T()因为 T 在运行时没有具体类型2. 不能创建泛型数组new ListString[10] 不行3. 不能在静态上下文使用类型参数4. 不能 instanceof 精确判断泛型参数5. 不能捕获具体泛型异常6. 重载与擦除冲突看起来不一样其实一个样四、通配符 ?、extends、super你以为只是语法糖结果是门哲学课 1. 为什么需要通配符2. ?啥都行但你就别乱写了五、extends 与 superPECS 原则是怎么来的1. ? extends T上界通配符 → “我只生产不消费”2. ? super T下界通配符 → “我只消费不生产”3. PECS 一句记牢六、综合例子用一段 copy 方法串起泛型 通配符 PECS七、? / extends / super 到底差在哪来个对比小表格 ✅八、最后整理一下如何优雅地和 Java 泛型相处文末开篇语哈喽各位小伙伴们你们好呀我是喵手。运营社区C站/掘金/腾讯云/阿里云/华为云/51CTO欢迎大家常来逛逛今天我要给大家分享一些自己日常学习到的一些知识点并以文字的形式跟大家一起交流互相学习一个人虽可以走的更快但一群人可以走的更远。我是一名后端开发爱好者工作日常接触到最多的就是Java语言啦所以我都尽量抽业余时间把自己所学到所会的通过文章的形式进行输出希望以这种方式帮助到更多的初学者或者想入门的小伙伴们同时也能对自己的技术进行沉淀加以复盘查缺补漏。小伙伴们在批阅的过程中如果觉得文章不错欢迎点赞、收藏、关注哦。三连即是对作者我写作道路上最好的鼓励与支持前言写 Java 的同学谁没跟泛型打过交道ListString、MapString, Integer一顿敲IDE 自动补全用得飞起编译器也相当贴心地给你各种类型检查。但一到运行时你再一看——怎么感觉泛型全“失忆”了ListString和ListInteger居然在运行时看起来一模一样instanceof ListString还不给写数组可以写new String[10]结果new ListString[10]直接红了别急这一切背后其实都指向一个关键词泛型擦除Type Erasure。再加上你经常看到的? extends T、? super T、“PECS 原则”这套组合拳要是真正搞明白了你对 Java 泛型的认知会直接从“能用”升到“掌控”。一、泛型到底是干嘛的——编译器的“贴心保姆”先把气氛放轻松一点泛型的本质其实是编译期的类型检查 自动转型工具。一个最常见的例子ListlistnewArrayList();list.add(hello);Strings(String)list.get(0);// 手动强转VSListStringlistnewArrayList();list.add(hello);Stringslist.get(0);// 不用强转IDE 还帮你补类型区别在哪没有泛型时编译器不管你往List里加啥全是Object你自己负责强转错了就运行时ClassCastException。有泛型时编译器会在编译阶段帮你检查类型不让你往ListString里塞Integer生成的字节码里依然是用Object存只不过自动插入强转换句话说泛型帮你把一些本该运行时报错的事情提前到了编译期。重点来了泛型主要是编译器玩的东西JVM 本身并不知道ListString和ListInteger有啥差别。这就是——类型擦除。二、类型擦除Type Erasure所谓“运行时失忆”是怎么搞出来的1. 擦除的基本思想一句话概括编译期间泛型超级聪明类型信息很丰富编译之后泛型类型参数被“抹掉”变回普通类型。比如有这么一段代码publicclassBoxT{privateTvalue;publicvoidset(Tvalue){this.valuevalue;}publicTget(){returnvalue;}}如果我们用它BoxStringboxnewBox();box.set(hello);Stringsbox.get();从源码层面看T的类型是String。但在编译之后JVM 看到的大概长这样伪代码仅用于理解publicclassBox{privateObjectvalue;publicvoidset(Objectvalue){this.valuevalue;}publicObjectget(){returnvalue;}}注意几点T 被擦除成了 Object如果有上界约束就擦成上界类型比如T extends Number会变成Number泛型类型参数在 class 文件里没有保留部分场景会以“签名信息”形式保留给反射用但运行时类型系统不参与。所以运行时你问 JVM“这个 Box 里装的是 String 还是 Integer”JVM 一脸无辜“我只知道是个 Object你自己看着办。”♂️2. 擦除的具体步骤简化理解版对一个带泛型的类 / 方法编译器大致会做几件事用上界替换类型参数如class BoxT extends Number→ 所有T位置都换成Number没写上界默认是Object。插入必要的强制类型转换源码中看起来返回的是T但擦除后返回Object或上界类型调用时编译器会在调用处生成强转。生成桥接bridge方法多态兼容用涉及泛型协变时会生成“桥方法”保证重写关系正常这里先不展开知道有这回事就行。三、泛型擦除带来的各种“你不能”这些限制不是你笨是 JVM 真做不到 既然运行时不保存真实的泛型类型那一堆看起来“理所当然”的操作就做不了了。下面一条条说1. 不能new T()因为 T 在运行时没有具体类型publicclassBoxT{privateTvalue;publicBox(){// 编译错误Cannot instantiate the type T// this.value new T();}}为什么擦除后T只变成了Object或上界类型JVM 不知道你到底想要new String()还是new Integer()new需要一个具体类型而不是“某种未知的 T”。常见替代方案传入ClassTpublicclassBoxT{privateTvalue;publicBox(ClassTclazz)throwsException{this.valueclazz.getDeclaredConstructor().newInstance();}}2. 不能创建泛型数组new ListString[10]不行// 编译错误ListString[]listsnewListString[10];为什么数组是真正的运行时类型安全结构会记录自己的组件类型但泛型在运行时被擦除成原始类型ListString和ListInteger都只是List如果允许ListString[]存在就会出现下面这种诡异情况Object[]arrnewListString[10];// 假设允许arr[0]newArrayListInteger();// 运行时数组检查只看到是 List放行// 结果你以为第 0 个位置是 ListString其实里面塞了 Integer彻底乱套所以 Java 干脆直接禁止创建泛型数组宁可你烦一点也不让你作死。常用替代ListStringlistnewArrayList();// 用集合代替数组ListListStringouternewArrayList();// 多层嵌套用集合组合3. 不能在静态上下文使用类型参数publicclassBoxT{// 编译错误Cannot make a static reference to the non-static type T// private static T value;// 也不允许// public static T create() { ... }}原因也很直观泛型参数T是和实例绑定的而static是和类绑定的跟具体实例无关类加载时还不知道会不会有人用BoxString还是BoxInteger所以静态上下文没有办法知道 T 是啥。4. 不能instanceof精确判断泛型参数ListStringlistnewArrayList();// 编译错误/* if (list instanceof ListString) { } */// 正确写法只能是if(listinstanceofList){// ...}因为擦除后运行时只有List这个信息String早就没了。5. 不能捕获具体泛型异常// 编译错误Generic class may not extend java.lang.Throwable// class MyExceptionT extends Exception {}// 也不能catch MyExceptionString eJVM 的异常系统只认“具体类型的 Throwable 子类”不可能在运行时根据StringInteger区分不同异常类泛型异常直接被禁止。6. 重载与擦除冲突看起来不一样其实一个样publicclassDemo{// 编译错误name clashpublicvoidtest(ListStringlist){}publicvoidtest(ListIntegerlist){}}编译器抗议说“对不起擦除之后你这两个方法长得一模一样。”因为擦除后变成publicvoidtest(Listlist){}publicvoidtest(Listlist){}方法签名重复JVM 无法区分直接报错。四、通配符 ?、extends、super你以为只是语法糖结果是门哲学课 1. 为什么需要通配符先看一个很多同学一开始都会踩的坑ListStringstringsnewArrayList();ListObjectobjectsnewArrayList();// 编译错误incompatible types// objects strings;很多人心里会想“String是Object的子类那ListString不就是ListObject的子类吗”——不是泛型在 Java 里是不变的invariantString是Object的子类 ✅但ListString和ListObject没有继承关系 ❌这会导致你想写一个方法“接收任何类型的 List”结果写成这样publicvoidprintList(ListObjectlist){...}然后你传入ListString会直接编译错误。于是 Java 引入了一个东西通配符?。2.?啥都行但你就别乱写了如果你只想说“这里是一个装着某种什么类型的 List具体是什么我不关心”可以写publicstaticvoidprintList(List?list){for(Objecto:list){System.out.println(o);}}你现在可以开心地传任何类型的 ListprintList(newArrayListString());printList(newArrayListInteger());printList(newArrayListDouble());但请注意——你基本上不能往List?里加东西除了nullpublicstaticvoidaddSomething(List?list){// 编译错误// list.add(abc);// list.add(123);list.add(null);// 只允许这个}原因很简单编译器不知道?真实类型是啥有可能是ListString也可能是ListInteger你往里面加Object也许会破坏原本的类型安全所以索性禁止写入除了null对谁都安全。一句话List?→我什么都能看读但我尽量少动写。五、extends与superPECS 原则是怎么来的终于到了经典口诀时间——PECS 原则Producer Extends, Consumer Super生产者用extends消费者用super这是记住? extends T和? super T的最好方法之一。1.? extends T上界通配符 → “我只生产不消费”List?extendsNumberlistnewArrayListInteger();意思是list 是一个“装着某种Number 子类型的 List”具体是ListInteger、ListDouble编译器不知道也不在乎你可以安全地“读出来当 Number 用”但你不能随便往里写。例子publicstaticdoublesum(List?extendsNumberlist){doubleresult0;for(Numbern:list){resultn.doubleValue();}returnresult;}你可以传ListIntegerintsList.of(1,2,3);ListDoubledoublesList.of(1.1,2.2);System.out.println(sum(ints));System.out.println(sum(doubles));但你不能这样publicstaticvoidaddNumber(List?extendsNumberlist){// 编译错误// list.add(1);// list.add(1.0);list.add(null);// 依旧只允许 null}因为编译器不知道 list 里到底是 Integer 还是 Double 或者别的 Number 子类你贸然塞一个 Integer 进去如果实际是ListDouble类型安全就挂了所以? extends T的集合适合“生产” T从里面拿 T不适合“消费” T往里面放东西。2.? super T下界通配符 → “我只消费不生产”List?superIntegerlistnewArrayListNumber();意思是list 是一个“某种 Integer的父类型的 List”可能是ListInteger也可能是ListNumber、ListObject这时你可以安全地往里面加 Integer 或其子类但从里面读出来只能当Object看。例子publicstaticvoidaddAllIntegers(List?superIntegerlist){list.add(1);list.add(2);list.add(3);}你可以这样用ListNumbernumsnewArrayList();addAllIntegers(nums);ListObjectobjsnewArrayList();addAllIntegers(objs);但你从里面读的时候Objectolist.get(0);// 只能是 Object// Integer i list.get(0); // 编译错误因为编译器只知道这是某个“Integer 的父类型”但不确定是啥干脆按最安全的Object处理。于是我们得到结论? super T的集合适合“消费” T往里放 T不适合“生产”精确类型的 T读出来只能是 Object。3. PECS 一句记牢Producer Extends, Consumer Super如果一个集合是数据的生产者你只是从里面拿数据→ 用? extends T如果一个集合是数据的消费者你只是往里面塞数据→ 用? super T这个原则在很多 JDK 源码里都有体现其中一个经典例子就是Collections.copy。六、综合例子用一段copy方法串起泛型 通配符 PECS看这段方法定义接近 JDK 的实现publicstaticTvoidcopy(List?superTdest,List?extendsTsrc){for(inti0;isrc.size();i){dest.set(i,src.get(i));}}它在说什么src数据来源是一个“生产 T 的集合”只从里面读 →? extends Tdest数据目的地是一个“消费 T 的集合”只往里面写 →? super T。使用方式ListIntegersrcList.of(1,2,3);ListNumberdestnewArrayList(Arrays.asList(0,0,0));copy(dest,src);// OKsrc 生产 Integerdest 消费 IntegerNumber 的子类这段签名如果你看懂了那恭喜你已经跨过了很多人卡住的那道“泛型理解门槛”。七、?/extends/super到底差在哪来个对比小表格 ✅写法能接受的实参类型以List...为例能不能 add 元素读取类型是什么典型场景List?任何ListX不能除nullObject只读遍历List? extends NumberListInteger、ListDouble等基本不能除null至少是Number只读 Number 数据生产者List? super IntegerListInteger、ListNumber、ListObject可以安全添加Integer及其子类读取时只有Object收集 / 插入 Integer消费者ListInteger只能ListInteger可以添加IntegerInteger最正常的使用方式配合 PECS 提醒自己要从集合里“拿 T” →? extends T要往集合里“放 T” →? super T什么都说不清就先写?但记得你几乎不能往里写。八、最后整理一下如何优雅地和 Java 泛型相处到这儿我们大概把你一开始的问题串了一遍泛型擦除是啥编译时照顾你帮你检查类型、插转型运行时类型参数被擦除成上界默认 ObjectJVM 不知道T是谁。擦除带来了哪些限制不能new T()不能创建new ListString[]这样的泛型数组不能在静态上下文里用类型参数不能instanceof ListString不能搞泛型异常、按泛型类型重载方法。通配符?、extends、super到底怎么用?我什么类型都能接受但基本不给你写? extends T生产者帮我“产出” T只读? super T消费者帮我“消费” T只写。PECS 原则怎么落地数据来源只读List? extends T数据目的地只写List? super T像copy这类方法的签名就是经典示范。如果你看到这里还愿意再抬头回去看看自己项目里的各种List?、MapString, ? extends Something你会发现以前那些“看着就头大”的写法其实每一个都有脾气、有性格、有理由。下一次当你写出一个带? super T的方法签名时不妨在心里小小地得瑟一句“这玩意儿不是 IDE 自动生成的是我知道它为什么要这么写。”… …文末好啦以上就是我这期的全部内容如果有任何疑问欢迎下方留言哦咱们下期见。… …学习不分先后知识不分多少事无巨细当以虚心求教三人行必有我师焉wished for you successed ⭐️若喜欢我就请关注我叭。⭐️若对您有用就请点赞叭。⭐️若有疑问就请评论留言告诉我叭。版权声明本文由作者原创转载请注明出处谢谢支持
上一篇/下一篇内容由系统自动关联
返回资讯列表 →