泛型详解
核心问题:如何让一个类同时支持多种类型?
场景引入
// 需求:设计一个"盒子"类,能存储不同类型的数据
// 问题:如何实现?
// 方案1:为每种类型写一个类(❌ 类爆炸)
class IntegerBox { private Integer value; }
class StringBox { private String value; }
class UserBox { private User value; }
// ...无穷无尽
// 方案2:使用Object(❌ 类型不安全)
class Box {
private Object value;
public void set(Object value) { this.value = value; }
public Object get() { return value; }
}
Box box = new Box();
box.set("hello");
Integer i = (Integer) box.get(); // 💥 ClassCastException
// 方案3:泛型(✅ 类型安全 + 代码复用)
class Box<T> {
private T value;
public void set(T value) { this.value = value; }
public T get() { return value; }
}
Box<String> box = new Box<>();
box.set("hello");
String s = box.get(); // 无需强转,编译期保证类型安全
一、为什么需要泛型?(JDK 5之前的痛点)
1.1 JDK 5之前的写法
// 没有泛型的集合
List list = new ArrayList();
list.add("hello");
list.add(123); // ✅ 编译通过
list.add(new User()); // ✅ 编译通过
// 取出时必须强转
String s = (String) list.get(0); // ✅ 正常
String s2 = (String) list.get(1); // 💥 运行时 ClassCastException
1.2 三大核心问题
| 问题 | 具体表现 | 后果 |
|---|---|---|
| 类型不安全 | 任何类型都能放入集合 | 运行时才发现类型错误 |
| 强制转换繁琐 | 每次取值都需要 (Type) |
代码冗长,容易出错 |
| 可读性差 | 看不出集合存储的类型 | 维护困难 |
1.3 泛型解决方案
List<String> list = new ArrayList<>();
list.add("hello"); // ✅ 正常
list.add(123); // ❌ 编译错误!类型不匹配
list.add(new User()); // ❌ 编译错误!
String s = list.get(0); // ✅ 无需强转,编译器保证类型
泛型的四大好处:
- 编译期类型检查 → 错误提前发现
- 避免强制转换 → 代码更简洁(实际是编译器自动插入了强转)
- 提升可读性 →
List<String>一看就知道存String - 代码复用 → 一个类适配所有类型
二、泛型的基本使用
2.1 泛型类
public class Box<T> {
private T value;
public void set(T value) {
this.value = value;
}
public T get() {
return value;
}
}
// 使用
Box<String> stringBox = new Box<>();
stringBox.set("hello");
String s = stringBox.get();
Box<Integer> intBox = new Box<>();
intBox.set(100);
Integer i = intBox.get();
2.2 泛型接口
public interface Repository<T> {
void save(T entity);
T findById(Long id);
List<T> findAll();
}
// 实现时指定具体类型
public class UserRepository implements Repository<User> {
@Override
public void save(User entity) { }
@Override
public User findById(Long id) { return null; }
@Override
public List<User> findAll() { return null; }
}
2.3 泛型方法
public class Utils {
// 静态泛型方法(必须自己声明类型参数)
public static <T> T getFirst(List<T> list) {
return list.isEmpty() ? null : list.get(0);
}
// 多个类型参数
public static <K, V> Map<K, V> createMap() {
return new HashMap<>();
}
// 有界类型参数
public static <T extends Number> double sum(List<T> list) {
double total = 0;
for (T num : list) {
total += num.doubleValue(); // Number的方法
}
return total;
}
}
三、类型擦除(Type Erasure)⭐⭐⭐
3.1 什么是类型擦除?
核心定义: Java泛型是"伪泛型",编译后所有泛型信息都会被擦除,字节码中只剩下原始类型(Raw Type)。
List<String> stringList = new ArrayList<>();
List<Integer> intList = new ArrayList<>();
// 运行时,两者的Class完全相同!
System.out.println(stringList.getClass() == intList.getClass());
// true!都是 ArrayList.class
反射验证:泛型只在编译期生效
List<String> list = new ArrayList<>();
list.add("hello");
// 通过反射可以绕过泛型检查
Method add = ArrayList.class.getMethod("add", Object.class);
add.invoke(list, 100); // ✅ 成功添加Integer!
System.out.println(list); // [hello, 100]
String s = list.get(1); // 💥 运行时ClassCastException
3.2 为什么要类型擦除?
历史原因:向后兼容
Java 1.4(2002年) → 没有泛型,代码已经大量部署
↓
Java 5(2004年) → 引入泛型,如何兼容旧代码?
↓
解决方案:编译期检查泛型,运行时擦除泛型信息
↓
结果:新代码(有泛型)和旧代码(无泛型)可以无缝共存
设计思路推演:
- ❌ 方案A:JVM层面实现泛型 → 旧字节码无法运行,破坏兼容性
- ✅ 方案B:编译器层面实现泛型 → 编译后擦除信息,运行时与旧代码一致
代价与限制:
// 限制1:不能用基本类型
List<int> list; // ❌ 编译错误
// 原因:擦除后变成List,int无法转为Object
// 限制2:不能创建泛型数组
T[] array = new T[10]; // ❌ 编译错误
// 原因:擦除后不知道具体类型,无法分配内存
// 限制3:不能用instanceof
if (obj instanceof List<String>) { } // ❌ 编译错误
// 原因:运行时List<String>和List<Integer>是同一个类型
3.3 擦除规则
| 泛型声明 | 擦除后类型 | 说明 |
|---|---|---|
List<String> |
List |
原始类型(Raw Type) |
<T> |
Object |
无界类型参数 |
<T extends Number> |
Number |
有界类型参数,擦除为上界 |
<T extends Comparable<T>> |
Comparable |
擦除为第一个上界 |
示例验证:
// 源码
public class Box<T extends Number> {
private T value;
public void set(T value) { this.value = value; }
public T get() { return value; }
}
// 擦除后等价于
public class Box {
private Number value; // T → Number
public void set(Number value) { this.value = value; }
public Number get() { return value; }
}
3.4 字节码分析:编译器的"魔法"
① 为什么get()不需要强转?
List<String> list = new ArrayList<>();
list.add("hello");
String str = list.get(0); // 看起来没有强转
字节码分析(javap -c):
// Java代码
String str = list.get(0);
// 字节码
0: aload_1 // 加载list
1: iconst_0 // 加载参数0
2: invokeinterface #2 // 调用get方法,返回Object
7: checkcast #3 // ✅ 强转为String!
10: astore_2 // 存储到str变量
真相: 编译器自动插入了 checkcast 指令(强制类型转换),所以我们的Java代码中不需要手写。
② 为什么反射能获取泛型信息?
问题: 类型擦除后,为什么反射还能获取泛型?
public class Demo {
private List<String> stringList; // 成员变量
public List<Integer> getIntList() { // 方法返回值
return null;
}
public void process(List<Double> doubleList) { // 方法参数
}
}
// 反射获取泛型信息
Field field = Demo.class.getDeclaredField("stringList");
Type genericType = field.getGenericType();
System.out.println(genericType); // java.util.List<java.lang.String>
字节码分析(javap -v):
// .class文件中的签名信息(Signature)
Signature: Ljava/util/List<Ljava/lang/String;>;
↑
泛型信息被保留在这里!
真相: .class文件中保留了签名(Signature)信息,反射通过读取Signature获取泛型类型。
关键限制:仅3种情况能获取泛型信息
// ✅ 可以获取
1. 成员变量的泛型:Field.getGenericType()
2. 方法参数的泛型:Method.getGenericParameterTypes()
3. 方法返回值的泛型:Method.getGenericReturnType()
// ❌ 无法获取
4. 局部变量的泛型
void method() {
List<String> localList = new ArrayList<>(); // 局部变量无签名信息
}
3.5 桥接方法(Bridge Method)
经典面试题:子类重写泛型方法时发生了什么?
// 父类
public class ArrayList<E> {
public boolean add(E e) { ... }
}
// 子类
public class StringList extends ArrayList<String> {
@Override
public boolean add(String s) { // 我们重写了add(String)
System.out.println("add String: " + s);
return super.add(s);
}
}
问题: 擦除后父类的add方法参数是Object,子类是String,这不算重写!
编译器的解决方案:生成桥接方法
// 编译后实际生成了两个方法
// 用户写的
public boolean add(String s) {
System.out.println("add String: " + s);
return super.add(s);
}
// 编译器自动生成的桥接方法
public boolean add(Object o) { // 参数是Object,真正重写父类方法
return add((String) o); // 强转后调用用户写的方法
}
验证:
StringList list = new StringList();
Method[] methods = list.getClass().getDeclaredMethods();
for (Method m : methods) {
System.out.println(m.getName() + " - " + m.isBridge());
}
// 输出:
// add - false (用户写的)
// add - true (编译器生成的桥接方法)
四、通配符(?)与上下界
4.1 为什么需要通配符?
// 需求:打印任意数字列表
public void printNumbers(List<Number> numbers) {
for (Number n : numbers) {
System.out.println(n);
}
}
List<Integer> intList = Arrays.asList(1, 2, 3);
printNumbers(intList); // ❌ 编译错误!
// 原因:List<Integer> 不是 List<Number> 的子类型
泛型的不可变性(Invariance):
// 即使 Integer 是 Number 的子类
Integer extends Number
// 但 List<Integer> 不是 List<Number> 的子类
List<Integer> ≠ List<Number>
解决方案:通配符
public void printNumbers(List<? extends Number> numbers) {
// ? extends Number 表示"Number或其子类"
}
printNumbers(Arrays.asList(1, 2, 3)); // ✅ List<Integer>
printNumbers(Arrays.asList(1.1, 2.2)); // ✅ List<Double>
printNumbers(Arrays.asList(1L, 2L)); // ✅ List<Long>
4.2 三种通配符对比
| 写法 | 能否读? | 能否写? | 典型场景 |
|---|---|---|---|
List<T> |
✅ 读出T | ✅ 写入T | 具体类型,功能最全 |
List<?> |
✅ 读出Object | ❌ 只能写null | 未知类型,只读 |
List<? extends T> |
✅ 读出T | ❌ 只能写null | 上界,只读(生产者) |
List<? super T> |
⚠️ 读出Object | ✅ 写入T及其子类 | 下界,只写(消费者) |
4.3 上界通配符 ? extends T(生产者)
List<? extends Number> list = new ArrayList<Integer>();
// ✅ 可以读取(返回Number)
Number n = list.get(0);
// ❌ 不能写入(除了null)
list.add(100); // ❌ 编译错误
list.add(1.5); // ❌ 编译错误
list.add(null); // ✅ 可以,null是所有类型的子类
为什么不能写?
编译器的视角:
list可能是 List<Integer>
list也可能是 List<Double>
list还可能是 List<Long>
如果允许 list.add(1.5):
- 若实际是 List<Integer> → 类型污染!
- 若实际是 List<Double> → 正常
为了类型安全,干脆禁止所有写入操作
4.4 下界通配符 ? super T(消费者)
List<? super Integer> list = new ArrayList<Number>();
// ✅ 可以写入(Integer及其子类)
list.add(100); // ✅ Integer
list.add(new Integer(200)); // ✅ Integer
// ⚠️ 读取只能用Object接收
Integer i = list.get(0); // ❌ 编译错误
Object o = list.get(0); // ✅ 可以
为什么能写?
编译器的视角:
list可能是 List<Integer>
list也可能是 List<Number>
list还可能是 List
但无论哪种情况,Integer的所有父类都能接受Integer
所以写入Integer是类型安全的
五、PECS原则(Producer Extends, Consumer Super)
核心口诀:频繁读取用 extends,频繁写入用 super
| 场景 | 推荐写法 | 原因 |
|---|---|---|
| 只从集合取数据(生产者) | List<? extends T> |
Producer → Extends |
| 只往集合存数据(消费者) | List<? super T> |
Consumer → Super |
| 既读又写 | List<T>(具体类型) |
通配符无法满足 |
经典案例:Collections.copy
public static <T> void copy(
List<? super T> dest, // 消费者:接收数据
List<? extends T> src // 生产者:提供数据
) {
for (int i = 0; i < src.size(); i++) {
dest.set(i, src.get(i)); // src生产,dest消费
}
}
// 使用示例
List<Number> dest = new ArrayList<>();
List<Integer> src = Arrays.asList(1, 2, 3);
Collections.copy(dest, src); // ✅ 类型安全
六、协变与逆变
6.1 核心概念
协变(Covariance): 子类型可以替换父类型
class Animal {}
class Dog extends Animal {}
// 上界通配符 = 协变
List<? extends Animal> animals;
animals = new ArrayList<Dog>(); // ✅ 子类型替换父类型
逆变(Contravariance): 父类型可以替换子类型
// 下界通配符 = 逆变
List<? super Dog> dogs;
dogs = new ArrayList<Animal>(); // ✅ 父类型替换子类型
6.2 应用场景
| 类型 | 方向 | 关键词 | 典型场景 |
|---|---|---|---|
| 协变 | 子→父 | 输出 | 方法返回值、数据提供者 |
| 逆变 | 父→子 | 输入 | 方法参数、数据消费者 |
七、泛型的边界限制
7.1 不能创建泛型数组
List<String>[] array = new List<String>[10]; // ❌ 编译错误
原因:数组协变 + 泛型擦除 = 类型不安全
// 假设允许创建泛型数组
List<String>[] array = new List<String>[10];
// 数组是协变的
Object[] objArray = array;
// 可以放入List<Integer>(运行时都是List)
objArray[0] = new ArrayList<Integer>();
// 取出时期望String,实际是Integer
String s = array[0].get(0); // 💥 ClassCastException
解决方案:
// 方案1:使用集合(推荐)
List<List<String>> listOfLists = new ArrayList<>();
// 方案2:强制压制警告(不推荐)
@SuppressWarnings("unchecked")
List<String>[] array = (List<String>[]) new List[10];
7.2 不能实例化类型参数
public class Box<T> {
private T value = new T(); // ❌ 编译错误
}
原因: 擦除后不知道T的具体类型,无法调用构造器
解决方案:
// 方案1:传入Class对象
public class Box<T> {
private T value;
public Box(Class<T> clazz) throws Exception {
value = clazz.getDeclaredConstructor().newInstance();
}
}
// 方案2:使用工厂方法
public interface Factory<T> {
T create();
}
public class Box<T> {
private T value;
public Box(Factory<T> factory) {
value = factory.create();
}
}
八、高频面试题
Q1: 什么是泛型擦除?为什么要擦除?
答:
- 定义: 编译后所有泛型信息被删除,字节码中只剩原始类型
- 原因: 向后兼容Java 5之前的代码
- 影响: 运行时无法获取泛型实际类型,不能创建泛型数组
Q2: List<String> 和 List<Integer> 运行时是同一个类型吗?
答: 是的,都是ArrayList.class(擦除后)
Q3: 为什么get()不需要强转?
答: 编译器自动插入了checkcast指令(字节码层面的强转)
Q4: 既然擦除了,为什么反射能获取泛型?
答: .class文件中保留了Signature签名信息
- ✅ 可获取:成员变量、方法参数、方法返回值的泛型
- ❌ 无法获取:局部变量的泛型
Q5: 为什么泛型不支持基本类型?
答: 擦除后变成Object,基本类型无法转为Object(需要装箱)
Q6: List<? extends Number> 为什么不能add?
答: 编译器不知道具体是Integer还是Double,为防止类型污染,禁止写入
Q7: PECS原则怎么记?
答: Producer Extends(生产者用extends),Consumer Super(消费者用super)
Q8: 为什么不能创建泛型数组?
答: 数组协变+泛型擦除会破坏类型安全
Q9: 什么是桥接方法?
答: 编译器自动生成的方法,用于解决泛型擦除后重写方法参数类型不匹配的问题
Q10: 泛型在继承中的表现?
答: List<Integer> 不是 List<Number> 的子类(泛型不可变)
九、最佳实践
✅ 推荐做法
// 1. 优先使用具体类型
List<String> list = new ArrayList<>();
// 2. 需要灵活性时用通配符
void print(List<? extends Number> numbers) { }
void add(List<? super Integer> list) { }
// 3. 工具类方法用泛型方法
public static <T> T getFirst(List<T> list) { }
// 4. 避免原始类型
List<String> list = new ArrayList<>(); // 而非 List list
❌ 避免做法
// 1. 不要使用原始类型
List list = new ArrayList(); // 失去泛型意义
// 2. 不要忽略编译警告
@SuppressWarnings("unchecked") // 除非真的知道自己在干嘛
// 3. 不要创建泛型数组
new List<String>[10] // 非法
相关链接
- 01-基本类型与包装类 - 为什么泛型不支持基本类型
- 01-Class对象与反射机制 - 反射如何突破泛型限制
- 集合框架 - 泛型在集合中的应用
记忆口诀
泛型擦除记心间,编译检查运行删 上界extends只能读,下界super才能写 PECS原则要记牢,生产extends消费super 桥接方法编译生,类型安全保无恙 数组泛型不能建,反射获取有限制
⬅️ Class对象与反射机制 🏠 00-Java ➡️ 注解原理
💬 评论