这是「Java 从基础到实战」系列的第 3 篇。上一篇我们大量使用了 List<String>、Map<String, Integer> 这样的写法,这对尖括号就是泛型。本文不从概念出发,而是从一个真实需求开始,一步步推导出「为什么需要泛型 → 泛型类 → 泛型方法 → 通配符」的完整演进路线。所有代码在 JDK 17 下可直接运行。
前言
泛型是很多初学者「会用但说不清」的知识点:会写 List<String>,但看到 <T extends Comparable<? super T>> 就头晕。本文的目标是让你看完后能自己写泛型类和泛型方法,并彻底搞懂 ? extends 和 ? super 什么时候用。
环境准备
| 软件 | 版本 |
|---|---|
| JDK | 17(Eclipse Temurin) |
| IntelliJ IDEA | Community 社区版 |
在 IDEA 的 java-basics 项目中,右键 src 目录 → New → Package,创建包 generic,本文所有类都放在这个包下。
步骤 1:回到没有泛型的年代,看看问题在哪
JDK 5 之前没有泛型,集合里存的都是 Object。新建 WithoutGenericDemo.java 体验一下:
package generic; |
运行结果:
2 |
问题很清楚:错误发生在运行时,而不是编译时。塞错数据的那一行编译器不报错,等到线上运行才炸。
把第一行改成泛型写法:
List<String> names = new ArrayList<>(); |
这就是泛型的核心价值:把运行时的 ClassCastException 提前到编译期发现,并省掉手动强转。
步骤 2:泛型类 —— 手写一个统一返回结果 Result
真实需求:接口开发中,所有方法都要返回「状态码 + 提示信息 + 数据」,但数据的类型每个接口都不一样——查用户返回 User,查列表返回 List<User>。
用泛型类一次解决。新建 Result.java:
package generic; |
新建 ResultDemo.java 验证:
package generic; |
预期输出:
Result{code=200, message='success', data=User[name=张三, age=20]} |
这就是后面 Spring Boot 实战篇会天天用的「统一返回体」,你现在已经自己写出来了。
步骤 3:泛型方法 —— 类型参数写在方法上
需求:写一个工具方法,返回任意列表的第一个元素,列表为空则返回默认值。
新建 GenericMethodDemo.java:
package generic; |
对比记忆:
- 泛型类:
class Result<T>—— 类型参数跟着对象走,创建对象时确定; - 泛型方法:
static <T> T firstOrDefault(...)—— 类型参数只在本次调用内有效,每次调用可以不同。
步骤 4:通配符 —— 为什么 List<Integer> 不能赋给 List<Number>
先看一个反直觉的现象:
Integer 是 Number 的子类,但—— |
为什么不行?假设允许,就能通过 nums.add(3.14) 往里塞 Double,那原来的 List<Integer> 就被污染了。所以 Java 规定:泛型不支持这种父子关系传递(不型变),需要用通配符表达。
? extends T:只读取,不写入(生产者)
需求:写一个方法计算「任意数字列表」的总和,List<Integer>、List<Double> 都要能传进来。
新建 WildcardDemo.java:
package generic; |
预期输出:
6.0 |
PECS 原则一句话记忆
Producer Extends, Consumer Super —— 只从集合里读(它是生产者)用
? extends;只往集合里写(它是消费者)用? super;又读又写就别用通配符,老老实实写List<T>。
JDK 源码 Collections.copy 就是教科书示范:
public static <T> void copy(List<? super T> dest, List<? extends T> src) |
常见坑
坑 1:继续使用原始类型(raw type)
错误示范 ❌:
List list = new ArrayList(); // IDEA 会黄色警告:Raw use of parameterized class |
正确写法:永远带上类型参数,哪怕暂时只能确定是 Object ✅
List<String> list = new ArrayList<>(); // 右边菱形语法 <> 自动推断 |
坑 2:往 ? extends 的集合里写入元素
错误示范 ❌:
public static double sum(List<? extends Number> list) { |
原因:? extends Number 意味着「Number 的某个未知子类」——可能是 List<Double>,往里塞 Integer 显然不行,编译器索性禁止一切写入(null 除外)。
正确写法:需要写入时改用 ? super ✅(见步骤 4 的 fillNumbers)。
坑 3:用 instanceof 判断泛型的具体类型
错误示范 ❌:
if (list instanceof List<String>) { // 编译报错 |
原因:Java 泛型是编译期擦除的——运行时 List<String> 和 List<Integer> 都只是 List,类型参数信息已不存在,所以运行时无法区分。
正确写法:只判断原始类型,元素类型靠泛型声明在编译期保证 ✅
if (list instanceof List<?>) { |
💡 这也解释了为什么不能
new T()、不能new T[10]——运行时根本不知道 T 是谁。
小结
本文沿着「问题 → 方案」的路线走完了泛型的主干:
- ✅ 泛型的本质:把类型错误从运行时提前到编译期,省掉强转
- ✅ 泛型类
Result<T>:亲手实现了 Spring 项目标配的统一返回体 - ✅ 泛型方法
<T> T firstOrDefault(...):类型随调用自动推断 - ✅ 通配符 PECS 原则:读用
? extends,写用? super - ✅ 三个坑:拒绝 raw type、
? extends禁止写入、擦除导致运行时无泛型
下一篇进入代码健壮性话题:《Java 异常处理最佳实践:try-with-resources 与自定义异常》,用「错误写法 → 正确写法」的对照重构,看看异常到底该怎么捕、怎么抛,敬请期待。
上一篇:《Java 集合框架实战:ArrayList、HashMap 该怎么选怎么用》
本系列完整目录见博客「技术博文」分类,每两周更新一篇。
💬 评论