Java 泛型详解(2025–2026 面试/实战最实用版)
泛型(Generics)是 Java 5 引入的最重要特性之一,到今天仍然是面试、代码审查、框架源码阅读中最常考察的点。
下面按从基础到高阶的顺序,把最容易混淆、最常踩坑、最有价值的内容全部梳理一遍。
1. 为什么要有泛型?(最本质的两个原因)
- 类型安全(编译期发现错误)
List list = new ArrayList();
list.add("hello");
list.add(123); // 编译通过
String s = (String) list.get(1); // 运行时 ClassCastException
- 消除显式类型转换(代码更简洁)
// 没有泛型
String s = (String) list.get(0);
// 有泛型
List<String> list = new ArrayList<>();
String s = list.get(0); // 无需强转
2. 泛型最核心的三种写法(必须记住)
| 写法位置 | 语法示例 | 含义 | 出现频率 |
|---|---|---|---|
| 类/接口 | class Box<T> | 类型参数 T,定义在类/接口级别 | ★★★★★ |
| 方法 | <E> void print(E e) | 方法级类型参数(独立于类泛型) | ★★★★☆ |
| 变量/参数 | List<String> list | 使用时指定具体类型 | ★★★★★ |
3. 泛型擦除(Erasure)—— 面试必考点
核心结论:
Java 泛型是编译期语法糖,在编译后会被擦除(类型参数被替换为 Object 或上界类型)。
擦除前后对比(字节码层面)
// 源代码
List<String> list = new ArrayList<>();
list.add("hello");
// 编译后(大致)
List list = new ArrayList();
list.add("hello"); // add(Object)
String s = (String) list.get(0);
擦除带来的重要影响(常考)
- 运行时无法获得泛型具体类型
List<String> list = new ArrayList<>();
System.out.println(list.getClass() == ArrayList.class); // true
System.out.println(list instanceof List<String>); // 编译错误!
// 运行时只能判断原始类型
System.out.println(list instanceof List); // true
- 静态变量/方法不能使用泛型类型参数
class Box<T> {
static T value; // 编译错误!
static void set(T t) {} // 编译错误!
}
- 不能创建泛型数组(new T[])
T[] arr = new T[10]; // 编译错误
4. 通配符(Wildcard)—— 最容易混淆的部分
| 通配符写法 | 含义 | 能读(get) | 能写(add) | 经典使用场景 |
|---|---|---|---|---|
List<?> | 任意类型 | 只能得到 Object | 几乎不能写(只能加 null) | 作为只读参数 |
List<? extends Number> | Number 或其子类 | 可以 get 为 Number | 不能写 | “生产者”(Producer Extends) |
List<? super Integer> | Integer 的父类(包括 Integer) | 只能得到 Object | 可以写 Integer 及其子类 | “消费者”(Consumer Super) |
经典面试题:PECS 原则
Producer Extends:如果你要从集合中读取数据,用 ? extends
Consumer Super :如果你要向集合中写入数据,用 ? super
记忆口诀:
“读取用 extends,写入用 super”
5. 泛型方法(最常被忽视但非常强大)
// 最常见的三种泛型方法写法
// 1. 普通泛型方法(最常用)
public static <T> void print(T t) {
System.out.println(t);
}
// 2. 带返回值的泛型方法
public static <T> T getFirst(List<T> list) {
return list.isEmpty() ? null : list.get(0);
}
// 3. 泛型方法 + 多个类型参数
public static <K, V> Map<K, V> newMap() {
return new HashMap<>();
}
注意:泛型方法前的 <T> 是方法级别的类型参数,与类泛型无关。
6. 泛型在集合框架中的真实用法(面试常考)
// 生产者(只读)
void printNumbers(List<? extends Number> list) {
for (Number n : list) { // 安全读取
System.out.println(n);
}
// list.add(1); // 编译错误
}
// 消费者(只写)
void addIntegers(List<? super Integer> list) {
list.add(1); // 安全写入
list.add(100); // OK
// Integer x = list.get(0); // 编译错误,只能得到 Object
}
7. 2025–2026 年高频面试/实战问题
- 为什么
List<String>不是List<Object>的子类型?(类型不变量) <?>、<? extends T>、<? super T>分别能干什么、不能干什么?- 泛型擦除后如何实现类型安全?(编译器插入强制类型转换)
- 为什么静态方法/变量不能使用类泛型参数?
- 泛型数组为什么不允许
new T[]?(可以用反射绕过,但不推荐) - 如何创建一个带有泛型类型的数组?(最安全写法:
(T[]) new Object[size])
8. 一句话总结 + 记忆口诀
一句话本质:
Java 泛型是编译期类型检查 + 语法糖,运行时全部擦除为原始类型(或上界类型),靠编译器帮我们做类型转换。
最实用口诀(背下来就过关):
- 类写
<T>,用时指定类型 - 方法写
<T>,独立于类泛型 - 读取用
extends,写入用super - 运行时全擦除,类型安全靠编译器
- 静态成员别用 T,数组创建别 new T
如果你能手写下面三段代码,就基本掌握泛型 80%:
- 带上下界的泛型方法
- PECS 原则的两个典型方法
- 解释
List<String>为什么不能赋值给List<Object>
需要更深入哪一块?
比如:
- 泛型在反射中的真实写法
- 签名擦除与桥接方法(bridge method)
- 泛型在 Lambda / Stream 中的常见坑
- 面试真题:写一个泛型版的 max 函数
随时告诉我,我继续给你展开~