Java 泛型
编译期类型安全,消除强转;运行时因类型擦除无 List<String> 与 List<Integer> 之分。详解 泛型。
为什么用泛型
| 无泛型 | 有泛型 |
|---|---|
List list = ...; String s = (String) list.get(0); | List<String> list = ...; String s = list.get(0); |
| 运行时 ClassCastException | 编译期检查类型 |
泛型类 / 方法 / 接口
public class Box<T> { private T value; }
public static <T> List<T> repeat(T item, int n) { ... }
public interface Mapper<I, O> { O map(I in); }方法级 <T> 与类的 <T> 相互独立。
通配符与 PECS
| 写法 | 读 | 写 | 场景 |
|---|---|---|---|
? extends T | ✅ | ❌(除 null) | 生产者 Extends |
? super T | ❌(除 Object) | ✅ | 消费者 Super |
T | ✅ | ✅ | 既读又写 |
// Collections.copy 典型 PECS
void copy(List<? super T> dest, List<? extends T> src)记忆:extends 上界只读;super 下界只写。
类型擦除
编译后泛型信息被擦除:
List<String> a = new ArrayList<>();
List<Integer> b = new ArrayList<>();
a.getClass() == b.getClass(); // true,都是 ArrayList| 擦除规则 | 示例 |
|---|---|
无界 T | → Object |
T extends Number | → Number |
| 编译器插入强转 | (String) list.get(0) |
擦除带来的限制
// ❌ 不能 new T()
// ❌ 不能 new T[]
// ❌ 静态字段不能用类的类型参数 static T x;
// ❌ instanceof List<String>
// ✅ instanceof List<?>泛型与继承
List<Integer> ints = new ArrayList<>();
// List<Number> nums = ints; // ❌ 编译错误
List<? extends Number> nums = ints; // ✅ 只读视角不变性:List<A> 与 List<B> 无父子关系,即使 A 是 B 子类。
运行时如何拿泛型类型
擦除后默认拿不到 List<String> 的实际参数,但声明处信息可保留:
// 匿名子类保留 ParameterizedType
Type type = new TypeReference<List<String>>() {}.getType();
// 父类泛型参数
Type t = getClass().getGenericSuperclass();MyBatis BaseMapper<T>、Jackson TypeReference、Spring ResolvableType 均利用此技巧。见 Java反射。
常见面试题
Q:泛型和 Object 强转区别?
A:泛型在编译期检查,错误提前暴露;Object 需每次手动 cast。
Q:List<?> 和 List<Object>?
A:List<?> 未知元素类型,几乎不能 add(除 null);List<Object> 可 add 任意 Object,二者不等价。
Q:能否 List<String>[] arr = new List<String>[10]?
A:不能;数组协变 + 擦除会导致堆污染,只允许 (List<String>[]) new List<?>[10] 等不安全写法(不推荐)。
Q:Java 泛型和 C# 泛型?
A:Java 擦除,运行时无 reified 类型;C# 保留运行时泛型信息(面试了解即可)。