十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

List<String>为啥运行时不认账?泛型擦除和PECS一次讲透

List<String>为啥运行时不认账?泛型擦除和PECS一次讲透 你好呀我是陪你刷面试题的老师。先抛一个特别反直觉的问题你先在心里答一下下面这段代码会输出什么ListStringanewArrayList();ListIntegerbnewArrayList();System.out.println(a.getClass()b.getClass());System.out.println(a.getClass().getName());一个装字符串、一个装整数泛型明明不一样结果运行起来第一行打印的居然是true第二行是java.util.ArrayList——泛型信息好像凭空消失了。很多同学面试被追问到这里就卡住了。今天我们就顺着这个现象把泛型、类型擦除、通配符、PECS、桥接方法这一整套东西像讲故事一样给你捋顺。这些全是阿里、字节、美团的高频考点建议收藏慢慢看。第一幕没有泛型的年代到底有多痛要知道泛型解决了什么问题得先回到 JDK 1.5 之前。那时候的集合里面只能装Object——也就是一个什么都能往里扔的大筐。// 没有泛型什么都能装取出来必须强转ListlistnewArrayList();list.add(张三);list.add(18);// 编译器完全不拦你混装一时爽Stringname(String)list.get(0);Stringage(String)list.get(1);// 编译能过运行直接崩运行结果非常扎心Exception in thread main java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String发现问题没类型错误被一路拖到运行时才爆炸。一个真实项目里这种强转成千上万谁也保证不了每个都转对线上随时可能因为一个ClassCastException挂掉。泛型就是来救场的。它的本质叫参数化类型把这个集合到底装什么类型变成一个参数在你写代码的时候就约定好让编译器帮你盯着// 有了泛型约定这个筐只能装 StringListStringlistnewArrayList();list.add(张三);list.add(18);// 编译就报错根本不让你混装Stringnamelist.get(0);// 取出来就是 String不用再强转一句话总结泛型把类型错误提前到编译期同时消灭了到处都是的强制类型转换。第二幕泛型的三种长相泛型不是只能写在集合上它有三种标准用法面试让你手写时别写错位置。① 泛型类类型参数声明在类名后面。publicclassResultT{// T 是类型形参惯例用 T/E/K/Vprivateintcode;privateTdata;// 类内部到处都能用 TpublicTgetData(){returndata;}publicvoidsetData(Tdata){this.datadata;}}ResultUserr1newResult();// T 被替换成 UserResultOrderr2newResult();// 同一个类复用到任意类型② 泛型方法那个T必须写在返回值前面用来声明这是个泛型方法这是最容易漏的地方。// T 写在返回值 T 前面缺一不可publicstaticTTgetFirst(ListTlist){returnlist.isEmpty()?null:list.get(0);}StringsgetFirst(newArrayListString());// T 自动推断为 String③ 泛型接口最典型的就是ComparableT。publicclassUserimplementsComparableUser{publicintcompareTo(Usero){/* 比较逻辑 */}}顺便记一下这几个字母约定面试别闹笑话TType 类型、EElement 元素、K/V键/值、RReturn 返回值、?未知类型的通配符。第三幕类型擦除——泛型最核心的底层机制重点来了。Java 的泛型被称为“伪泛型”因为它只活在编译期一旦编译成字节码泛型信息就会被擦掉这就是类型擦除Type Erasure。擦除规则其实就两条记死它类型参数声明擦除后被替换成T没有上界ObjectT extends Number有上界上界Number所以开头那段代码就解释通了String、Integer编译后全被擦成了Object字节码里只剩一个光秃秃的ArrayList它们的getClass()当然是同一个。那取值的时候为什么不用我们自己强转因为编译器偷偷在取值的地方插入了一条checkcast强转指令。整个过程是这样的源码阶段ArrayListString编译器严格校验类型 ↓ javac 编译类型擦除 字节码ArrayListT 被替换成 Object ↓ get 取值处 自动插入 checkcast转回 String泛型信息是不是全丢光了——Signature 还留了一份这里有个特别容易答错的细节。虽然对象实例层面泛型被擦了但类和方法上声明的泛型会以 Signature 属性额外存进字节码反射是能读到的。我用 JDK 11 实测ArrayList 声明的类型参数: [E] emptyList 泛型方法返回类型: java.util.ListT所以一定要分清楚两件事对象实例层面泛型被擦除运行时 new 出来的都是同一个裸类类/方法声明层面泛型签名被 Signature 保留反射可见。这也解释了为什么 Spring、Jackson、MyBatis 这些框架能靠反射知道你要的是ListUser而不是List。桥接方法Bridge Method擦除之后打的补丁这是大厂特别爱追问的加分点。你写的是implements ComparableUser方法签名是compareTo(User)可擦除之后接口要求的是compareTo(Object)俩签名对不上多态不就失效了吗编译器的办法是偷偷生成一个桥接方法// 你自己只写了这一个方法publicintcompareTo(Usero){...}// 编译器额外生成的桥接方法反编译可见标记为 synthetic bridgepublicintcompareTo(Objecto){returncompareTo((User)o);// 转一手再去调你真正写的方法}所以你用反射getMethods()时偶尔会看到同名方法出现两次那就是桥接方法用method.isBridge()就能判断。它的存在是为了保证类型擦除之后多态依然成立。第四幕通配符 ? extends / ? super 与 PECS要理解通配符先得接受一个反直觉的事实泛型是不协变的。哪怕Integer extends Number下面这行也编译不过ListIntegerintsnewArrayList();ListNumbernumsints;// 编译报错ListInteger 不是 ListNumber 的子类为什么这么设计假设允许那你就能往一个实际装 Integer的ListNumber里add一个Double类型安全当场崩盘。Java 的解法是用通配符来安全地表达某一族类型。上界通配符 ? extends Number生产者只能读、不能写List?extendsNumberlistnewArrayListInteger();Numbernlist.get(0);// 读没问题取出来一定是 Numberlist.add(Integer.valueOf(1));// 编译报错不让 add除了 null为什么不让写因为编译器只知道它是 Number 的某个子类但到底是 Integer 还是 Double它不确定自然不敢让你 add 任何具体类型进去。下界通配符 ? super Integer消费者只能写、读出来是 ObjectList?superIntegerlistnewArrayListNumber();list.add(Integer.valueOf(1));// 写没问题往父类容器装子类绝对安全Objectolist.get(0);// 读只能当 Object类型太宽没意义PECS 原则Producer-ExtendsConsumer-Super记一句全世界通用的口诀PECS生产者往外吐数据、主要 get用 extends消费者往里收数据、主要 add用 super。JDK 里最经典的例子是Collections.copy我用反射把它的真实泛型签名打出来给你看参数0 类型: java.util.List? super T 下界 super T → dest 负责写入是消费者 参数1 类型: java.util.List? extends T 上界 extends T → src 负责读出是生产者对照源码一看就懂从 src 里get读出来生产者用 extends往 dest 里add写进去消费者用 super。publicstaticTvoidcopy(List?superTdest,// 目的地消费 TList?extendsTsrc){// 来源生产 Tfor(inti0;isrc.size();i){dest.add(src.get(i));// 从 src 读往 dest 写}}三种通配符放一起对比面试直接背这张表写法别名能 get 读能 add 写典型场景? extends T上界 / 生产者读出为 T不行遍历、求和、查找? super T下界 / 消费者只能当 Object可以写 T拷贝目的地、批量 add?无界等价 ? extends Object读出为 Object不行只读、不关心元素类型第五幕泛型的 5 个高频坑踩一个凉一个这些是面试和写代码时最常见的坑一条条记住坑 1不能new T()也不能new T[]。擦除后运行时根本不知道 T 是谁没法实例化。要创建对象得把ClassT当参数传进来用反射clazz.getDeclaredConstructor().newInstance()。坑 2基本类型不能当泛型实参。只能写ListInteger不能写Listint因为擦除后是 Object而 int 不是对象——这也是为什么要有自动装箱。坑 3泛型数组不推荐。new T[]、new ListString[10]会报错或警告因为数组是协变的、运行时还要检查元素类型跟类型擦除天生冲突。正确姿势是用集合套集合ListListString。坑 4instanceof 后面不能带泛型。obj instanceof ListString编译报错运行时泛型都没了还判断啥只能写obj instanceof List?。坑 5静态成员不能用类的类型参数 T。静态字段/静态方法属于类、早于对象创建那时候 T 还没被指定。静态场景要写成自己声明泛型的泛型方法classBoxT{staticTx;// 报错静态字段不能用类的 TstaticTget(){returnnull;}// 报错同上staticEEok(Ee){returne;}// 正确静态泛型方法自己声明 E}第六幕回到项目里泛型到底香在哪说个后端项目里几乎人人都写过的东西——统一接口返回结果。如果没有泛型你得给每种 data 都写一个返回类有了泛型一个RT通吃publicclassRT{privateintcode;privateStringmsg;privateTdata;publicstaticTRTok(Tdata){RTrnewR();r.code200;r.datadata;returnr;}}// 调用处类型一目了然前端拿到的 data 类型由这里决定RUseruserR.ok(userInfo);RListOrderordersR.ok(orderList);再比如写一个通用的安全取配置方法一套逻辑适配所有返回类型还保证类型安全publicstaticTTgetConfig(Stringkey,ClassTclazz){ObjectvalconfigMap.get(key);returnclazz.cast(val);// 一处方法适配任意类型}所以面试问你泛型有什么好处标准回答是这四点编译期类型安全、消除强转与代码复用、配合通配符提升 API 灵活性、运行时擦除不产生额外对象开销。一句话总结背就完了知识点一句话记忆泛型本质参数化类型把类型检查提前到编译期类型擦除运行时 T 擦成上界Object取值处自动 checkcastSignature声明处泛型保留在字节码反射可读框架靠它桥接方法编译器生成 Object 入参方法保证擦除后多态PECS生产者 extends 读、消费者 super 写五个坑不 new T、不用基本类型、慎用数组、instanceof 用 ?、静态自带泛型最后送你一段顺口溜泛型本质参数化类型安全编译查运行擦除变上界取值自动做强转extends 往外读、super 往里写PECS 别搞反不 new T、不装基本类型桥接方法补多态。这篇如果帮你把泛型彻底理顺了欢迎点个赞、点个关注Java 集合框架与基础系列会持续更新下去。下一篇我们讲异常体系受检异常和运行时异常到底怎么分、try-catch-finally 的执行顺序、项目里自定义异常的正确姿势又是一个面试高频、初学者头大的点我们继续用讲故事的方式把它掰开揉碎。有任何疑问欢迎在评论区交流我会逐条回复咱们一起稳扎稳打拿下大厂面试加油#Java #java面试 #后端开发 #泛型 #集合框架 #面试题 #程序员
返回列表