点击上方“java大数据修炼之道”, 选择“设为星标”

技术干货发文后 👉 第一时间奉上

引言

泛型是 Java 5 引入的特性,用<>括号把类型参数化,让代码在编译期就能做类型检查,同时减少强制类型转换的代码。但很多人不知道的是,Java 的泛型其实是"伪泛型"——它在运行时会把类型参数擦掉,所有泛型信息在编译完成之后就消失了。这和 Kotlin、C# 的泛型完全不同,也是 Java 泛型最核心、最容易踩坑的点。今天来把泛型的本质彻底讲清楚。

一、什么是类型擦除

类型擦除的意思是:当你写 List<String>、List<Integer> 这样的泛型代码时,编译器在生成字节码之前,会把所有类型参数替换成它们的边界(默认是 Object),然后在需要的地方插入强制类型转换。运行时的字节码里,你看不到任何泛型的痕迹,List<String> 和 List<Integer> 在 JVM 里都是同一个类型——原始的 List。

用一个具体的例子说明:

当你写 List<String> list = new ArrayList<>(); list.add("hello"); String s = list.get(0); 的时候,编译器在编译后会把它转换成:List list = new ArrayList(); list.add("hello"); String s = (String) list.get(0); 在编译期,编译器知道 get(0) 返回的一定是 String,所以做了类型检查;但运行期,JVM 根本不知道 list 装的是什么类型,它只知道 list.get(0) 返回的是 Object,然后强制转换成 String。

这就是为什么你不能用 instanceof String 来判断一个泛型集合里装的是什么类型,因为泛型在运行期已经不存在了。instanceof List<String> 在 Java 里是语法错误,instanceof List 才是合法的。

二、类型擦除带来的限制

因为类型擦除,Java 泛型在很多场景下受到限制,这是初学者经常困惑的地方。

第一,不能用基本类型作为类型参数。List<int> 是非法的,因为擦除后是 List<Object>,而 Object 不能持有 int。这不是 Java 设计者的偷懒,而是类型擦除机制本身的要求。所以你会看到 Integer、Double 这些包装类型满天飞,而不是 int、double。

第二,不能 new T()。new T() 在代码里看起来很自然,但 T 擦除后就变成了 Object,你真正创建的是一个 Object,而不是泛型指定的那个类型。同理,不能 new T[],不能用 T.class。这些限制背后的原因都一样:类型参数在运行期已经不存在了,JVM 没有办法知道你到底想创建什么类型的对象。

第三,泛型方法不能被重载。两个方法分别是 void method(List<String>) 和 void method(List<Integer>),在编译后都会被擦除成 void method(List),变成了完全相同的签名,所以不能共存。这是泛型擦除导致的最容易出错的场景之一。

第四,静态方法不能引用类的泛型参数。static T doSomething() 是非法的,因为泛型参数是属于具体实例的,而静态成员不属于任何实例。类型擦除后 T 就没有了归属,编译器无法处理。

三、通配符:? 的三种形态

泛型里有一个经常让人混淆的概念:通配符。List<?>、List<? extends Number>、List<? super Integer> 这三种写法有什么区别?什么时候该用哪个?

无边界通配符 List<?> 表示未知类型。可以读取元素,但只能以 Object 类型读取,因为编译器只知道集合里装的是某种类型,但不知道具体是什么。往里面写入元素是不安全的,编译器会拒绝任何写入操作,因为你不知道要写什么类型。

上界通配符 List<? extends Number> 表示这个集合的元素类型是 Number 及其子类。读取元素时,可以安全地以 Number 类型取出。写入时同样受限——只能写入 null,因为你只知道元素类型是 Number 的子类,但具体是哪个子类编译器无法确定,所以干脆禁止所有写入。这种模式叫"生产者 Extends"。

下界通配符 List<? super Integer> 表示这个集合的元素类型是 Integer 及其父类。读取时只能以 Object 类型取出,因为元素可能是 Object,也可能是 Number,甚至可能是 Integer。写入时则相对宽松——可以写入 Integer 及其子类,因为无论是 Integer 还是它的子类,最终都能安全地放进集合里。这种模式叫"消费者 Super"。

四、PECS 原则:正确选择 extends 和 super

Effective Java 里有一条经典法则叫 PECS:Producer Extends, Consumer Super。如果你的数据是要被读取的(生产者),用 extends;如果是要写入的(消费者),用 super。

一个具体例子:假设有一个工具方法,用来把 src 集合里的元素复制到 dest 集合里。如果 src 是生产者(要从中读取),就用 ? extends;如果 dest 是消费者(要写入),就用 ? super。所以方法的签名应该是:void copy(List<? extends T> src, List<? super T> dest)。Roy Cornelius 和 Josh Bloch 设计的这个模式,是 Java 泛型最优雅的应用之一。

还有一个原则:不要用 List<Object> 来代替 List<?>。List<Object> 是原始类型,编译器对它的类型检查反而比 List<?> 更宽松——你可以往 List<Object> 里写入任何对象,但往 List<?> 里什么都不能写。大多数场景下 List<?> 才是正确的选择。

五、泛型与多态的冲突:桥接方法

类型擦除有时候会和多态产生冲突。假设有一个父类 Animal 和子类 Dog,父类里有一个方法 List<Dog> getDogs(),子类重写这个方法时,类型擦除会把父类的方法变成 List getDogs(),而子类如果直接写 List<Dog> getDogs(),在 JVM 看来两个方法的签名就一样了,不构成重写关系。

JVM 的解决方案是自动生成桥接方法。编译器会为子类额外生成一个签名被擦除后的方法 List getDogs(),它会调用 List<Dog> getDogs() 这个泛型版本。这样从 JVM 的角度看是重写,从 Java 代码的角度看是泛型方法。两个方法同时存在,编译器在调用时会自动选择正确的那个,你不需要关心桥接方法的存在。

这个机制也解释了为什么有时候你用反射调用方法时,会看到两个看起来完全相同的方法——其中一个就是编译器自动生成的桥接方法。

六、实战建议与常见错误

泛型最常见的错误有两个。第一个是把泛型当成强制类型转换的替代品。List list = new ArrayList(); list.add(123); Integer i = (Integer) list.get(0); 这种代码在编译时没有报错,但运行时会抛出 ClassCastException。正确的做法是全程使用泛型,编译期的类型检查会帮助你发现这类错误。

第二个常见错误是在集合遍历时进行增删操作。for (String s : list) { list.remove(s); } 在某些情况下会抛出 ConcurrentModificationException,因为在遍历过程中修改了集合结构。正确的方式是用 Iterator 的 remove 方法,或者使用 CopyOnWriteArrayList 等线程安全的集合。

第三个错误是混淆泛型类和泛型方法的区别。泛型类是在类声明时指定类型参数,类的所有方法共享这个类型参数;泛型方法是在方法声明时单独指定类型参数,方法可以有自己独立的类型参数,和类本身的类型参数可以完全不同。

总结

Java 泛型的核心是类型擦除——类型信息在编译后就消失了,这是 Java 和其他语言泛型实现最大的不同。

类型擦除导致了一系列限制:不能用基本类型、不能 new T、不能 new T[]、泛型方法不能被重载、静态方法不能引用泛型参数。理解这些限制的根本原因,才能真正掌握泛型的本质。

通配符的三种形态——无边界、extends 上界、super 下界——是泛型进阶的关键。PECS 原则:生产者用 extends,消费者用 super,是选择通配符方向的实用准则。

end
===往期精彩文章复习回顾===
1.SpringBoot 插件化开发模式,真香啊!
2.一行代码,实现请假审批流程(Java版)
3.血泪教训,8 个线程池最佳实践和坑
4.SpringBoot骚操作:一个注解秒杀所有类型的文件下载!
5.Controller层代码这么写,同事们都模仿起来了

最近整理一份资料《程序员学习手册》,覆盖了 Java技术、面试题精选、操作系统基础知识、计算机基础知识、Linux教程、计算机网络等等。

获取方式:点“ 在看,关注公众号 Java大数据修炼之道 并回复PDF 领取,更多内容陆续奉上。

长按识别下方二维码关注后回复关键字:PDF领取

你想学的java知识这里都有,长按下方图片识别关注我们吧~

图片

如喜欢本文请点击右上角,把文章分享到朋友圈
因公众号更改推送规则,请点“在看”并加“星标”第一时间获取精彩技术分享
点分享点收藏点在看
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐