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