泛型擦除与类型系统
# 19.泛型擦除与类型系统
# 目录介绍
- 6.1 开篇疑问
- 6.2 泛型的诞生背景
- 6.3 泛型擦除的本质
- 6.4 泛型的边界与限制
- 6.5 通配符与PECS原则
- 6.6 泛型的高级特性
- 6.7 运行时获取泛型信息
- 6.8 泛型最佳实践与常见陷阱
- 6.9 Java vs 其他语言的泛型对比
- 6.10 总结与核心要点
# 6.1 开篇疑问
疑惑:Java 的泛型为什么是"假泛型"?List<Integer> 和 List<String> 在运行时是同一个类型吗?为什么泛型不能用 int 只能用 Integer?为什么不能 new T()?泛型擦除后,框架是怎么获取泛型类型信息的?
答疑:Java 泛型是 JDK 5 引入的,采用了"类型擦除"的实现方式。这个设计让泛型与旧版本代码完全兼容,但也带来了很多反直觉的限制。理解擦除机制,是理解泛型所有"怪异行为"的钥匙。
本篇将从泛型的诞生背景出发,深入分析擦除机制、桥接方法、通配符 PECS 原则,以及框架如何在运行时绕过擦除获取泛型信息。
# 6.2 泛型的诞生背景
# 6.2.1 没有泛型的黑暗时代
JDK 5 之前,集合只能存 Object,取出时必须强转:
// JDK 1.4 时代——类型不安全
List list = new ArrayList();
list.add("hello");
list.add(123); // 可以放任何类型,编译器不报错
list.add(new Object());
String s = (String) list.get(0); // 需要强转
String s2 = (String) list.get(1); // 运行时 ClassCastException!
// 问题:编译器无法帮你检查类型错误
// 错误延迟到运行时才暴露,排查困难
2
3
4
5
6
7
8
9
10
11
泛型的目标:将类型检查从运行时提前到编译时。
// JDK 5+ 泛型——编译期安全
List<String> list = new ArrayList<>();
list.add("hello");
list.add(123); // 编译报错!类型不匹配
String s = list.get(0); // 无需强转,编译器保证类型正确
2
3
4
5
# 6.2.2 擦除式泛型vs具化式泛型
实现泛型有两种方式:
| 特性 | 擦除式(Java) | 具化式(C#) |
|---|---|---|
| 运行时类型信息 | 不保留泛型参数 | 保留泛型参数 |
List<int> 支持 | 不支持(必须用包装类) | 支持 |
new T() 支持 | 不支持 | 支持 |
instanceof List<String> | 不支持 | 支持 |
| 运行时每种泛型一个类 | 否(所有 List<X> 共用一个类) | 是(List<int> 和 List<string> 是不同的类) |
| 向后兼容 | 完全兼容旧代码 | 不兼容 |
# 6.2.3 Java为什么选择类型擦除
核心原因:向后兼容。
JDK 5 发布时,已有海量基于 JDK 1.4 的代码和库。Java 必须保证:
- 旧代码不修改就能在新 JVM 上运行
- 新代码能调用旧的非泛型库
- 旧代码能调用新的泛型库
// 旧代码(JDK 1.4 编写)
public void process(List list) {
// ...
}
// 新代码(JDK 5 编写)
List<String> strings = new ArrayList<>();
strings.add("hello");
// 旧代码可以直接使用新的泛型 List(向后兼容)
process(strings); // 合法,泛型擦除后 List<String> 就是 List
2
3
4
5
6
7
8
9
10
11
C# 之所以能用具化式泛型,是因为 .NET 2.0 引入泛型时同时修改了 CLR(虚拟机),不需要与旧版本兼容。Java 选择不修改 JVM 字节码格式,只在编译器层面实现泛型。
代价:擦除带来了各种限制(不能 new T()、不能 instanceof 等),这些"怪异行为"都是向后兼容的代价。
# 6.3 泛型擦除的本质
# 6.3.1 什么是类型擦除
编译时泛型信息用于类型检查,编译完成后泛型信息被擦除,字节码中不包含泛型参数。
// 源代码
List<String> stringList = new ArrayList<>();
List<Integer> intList = new ArrayList<>();
// 编译后(字节码层面)——完全相同
List stringList = new ArrayList();
List intList = new ArrayList();
// 验证
System.out.println(stringList.getClass() == intList.getClass()); // true
System.out.println(stringList.getClass().getName()); // java.util.ArrayList
// 运行时根本不知道 stringList 是 List<String>
2
3
4
5
6
7
8
9
10
11
12
# 6.3.2 擦除后的真实面目
泛型参数被替换为其上界(没有上界则替换为 Object):
// 无界泛型:T → Object
public class Box<T> {
private T value;
public T get() { return value; }
public void set(T value) { this.value = value; }
}
// 擦除后
public class Box {
private Object value;
public Object get() { return value; }
public void set(Object value) { this.value = value; }
}
// 有上界泛型:T extends Number → Number
public class NumberBox<T extends Number> {
private T value;
public double doubleValue() { return value.doubleValue(); }
}
// 擦除后
public class NumberBox {
private Number value;
public double doubleValue() { return value.doubleValue(); }
}
// 多重上界:T extends Comparable & Serializable → 第一个边界 Comparable
public class MultiBox<T extends Comparable<T> & Serializable> {
private T value;
}
// 擦除后
public class MultiBox {
private Comparable value; // 使用第一个边界
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
关键点:有多个边界时,擦除为第一个边界。因此建议将"标签接口"(如 Serializable)放在后面,将实际使用方法的接口放在前面,以减少强制转换。
# 6.3.3 编译器插入的强制转换
编译器在调用处自动插入 checkcast 指令:
// 源代码
Box<String> box = new Box<>();
box.set("hello");
String s = box.get();
// 编译后的字节码等价于
Box box = new Box();
box.set("hello");
String s = (String) box.get(); // 编译器自动插入 checkcast
2
3
4
5
6
7
8
9
字节码验证:
invokevirtual Box.get:()Ljava/lang/Object; // 返回 Object
checkcast java/lang/String // 强转为 String
astore_2
2
3
这就是泛型的本质:编译器在编译期做类型检查,在使用处插入强制转换,然后擦除泛型信息。类型安全由编译器保证,运行时的 checkcast 只是"双保险"。
# 6.3.4 桥接方法的秘密
类型擦除可能导致方法签名冲突,编译器通过桥接方法(Bridge Method)解决多态问题:
public interface Comparable<T> {
int compareTo(T o);
}
public class IntValue implements Comparable<Integer> {
private int value;
@Override
public int compareTo(Integer other) { // 参数是 Integer
return Integer.compare(this.value, other);
}
}
2
3
4
5
6
7
8
9
10
11
12
问题:擦除后 Comparable 接口的方法是 compareTo(Object),但 IntValue 实现的是 compareTo(Integer)。签名不匹配,多态会失效!
解决:编译器自动生成桥接方法:
// 编译器在 IntValue 中自动生成
public class IntValue implements Comparable {
// 用户编写的方法
public int compareTo(Integer other) {
return Integer.compare(this.value, other);
}
// 编译器生成的桥接方法(字节码中可见)
public int compareTo(Object other) { // bridge method
return this.compareTo((Integer) other); // 转发给真正的方法
}
}
2
3
4
5
6
7
8
9
10
11
12
验证桥接方法:
for (Method m : IntValue.class.getDeclaredMethods()) {
System.out.println(m.getName()
+ " bridge=" + m.isBridge()
+ " synthetic=" + m.isSynthetic()
+ " params=" + Arrays.toString(m.getParameterTypes()));
}
// compareTo bridge=false synthetic=false params=[class java.lang.Integer]
// compareTo bridge=true synthetic=true params=[class java.lang.Object]
2
3
4
5
6
7
8
# 6.3.5 用javap验证擦除过程
# 编译并查看字节码
javac Box.java
javap -c -s -p Box.class
2
3
public class Box {
private java.lang.Object value;
descriptor: Ljava/lang/Object;
public java.lang.Object get();
descriptor: ()Ljava/lang/Object;
Code:
0: aload_0
1: getfield #2 // Field value:Ljava/lang/Object;
4: areturn
// Signature 属性中保留了泛型信息(不在字节码指令中,而在元数据中)
// Signature: Ljava/lang/Object; → 实际为 TT;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# 6.4 泛型的边界与限制
# 6.4.1 泛型不能用基本类型
List<int> list = new ArrayList<>(); // 编译错误!
List<Integer> list = new ArrayList<>(); // 正确,使用包装类
2
原因:擦除后 T 变成 Object,而 int 不是 Object 的子类。Java 的基本类型不参与面向对象体系。
// 擦除后 List<T> 的 add 方法变成:
public boolean add(Object e) { ... }
// int 不能赋值给 Object(没有自动装箱发生在泛型层面)
// 虽然 Java 5 引入了自动装箱,但那是在使用处发生的
2
3
4
5
性能影响:使用包装类意味着装箱/拆箱的开销。处理大量数值时,考虑使用专门的原始类型集合库(如 Eclipse Collections 的 IntList)。
未来展望:Project Valhalla 正在为 Java 引入值类型和基本类型泛型(Primitive Generics),预计未来版本支持 List<int>。
# 6.4.2 不能创建泛型数组
// 编译错误
T[] arr = new T[10];
List<String>[] arr = new List<String>[10];
// 编译警告(不安全)
List<String>[] arr = new List[10];
2
3
4
5
6
疑惑:为什么数组可以协变但泛型不行?
论证:Java 数组是协变的(String[] 是 Object[] 的子类型),且运行时知道元素类型:
Object[] arr = new String[3];
arr[0] = "hello"; // OK
arr[1] = 123; // 运行时 ArrayStoreException!数组做了类型检查
// 如果允许泛型数组:
List<String>[] arr = new List<String>[3]; // 假设合法
Object[] objArr = arr; // 数组协变
objArr[0] = new ArrayList<Integer>(); // 运行时无法检查!
// 因为擦除后 List<String> 和 List<Integer> 都是 List
// 类型安全被破坏!
2
3
4
5
6
7
8
9
10
替代方案:
// 使用 ArrayList 代替数组
List<List<String>> listOfLists = new ArrayList<>();
// 使用 @SuppressWarnings(确认安全时)
@SuppressWarnings("unchecked")
T[] arr = (T[]) new Object[10];
// 使用 Array.newInstance
@SuppressWarnings("unchecked")
T[] arr = (T[]) Array.newInstance(clazz, size);
2
3
4
5
6
7
8
9
10
# 6.4.3 不能instanceof泛型
if (obj instanceof List<String>) { } // 编译错误!
if (obj instanceof List<?>) { } // 可以(无界通配符不受擦除影响)
if (obj instanceof List) { } // 可以(原始类型)
2
3
原因:instanceof 是运行时检查,而泛型信息在运行时已被擦除。JVM 无法区分 List<String> 和 List<Integer>。
# 6.4.4 不能new T和new T数组
public class Factory<T> {
public T create() {
return new T(); // 编译错误!
// 擦除后变成 new Object(),不是用户期望的类型
}
}
2
3
4
5
6
替代方案一:传入 Class 对象
public class Factory<T> {
private Class<T> clazz;
public Factory(Class<T> clazz) {
this.clazz = clazz;
}
public T create() throws Exception {
return clazz.getDeclaredConstructor().newInstance();
}
}
// 使用
Factory<User> factory = new Factory<>(User.class);
User user = factory.create();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
替代方案二:传入 Supplier
public class Factory<T> {
private Supplier<T> supplier;
public Factory(Supplier<T> supplier) {
this.supplier = supplier;
}
public T create() {
return supplier.get();
}
}
// 使用
Factory<User> factory = new Factory<>(User::new);
User user = factory.create();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 6.4.5 泛型类的静态上下文限制
public class Box<T> {
// 静态字段不能用类的泛型参数
private static T value; // 编译错误!
// 静态方法不能用类的泛型参数
public static T getValue() { } // 编译错误!
// 但静态方法可以定义自己的泛型参数
public static <E> E convert(E input) { return input; } // OK
}
2
3
4
5
6
7
8
9
10
原因:泛型参数属于实例级别。Box<String> 和 Box<Integer> 共享同一个 Class 对象和静态字段。如果静态字段是 T 类型,它到底是 String 还是 Integer?无法确定。
# 6.4.6 泛型异常的限制
// 不能声明泛型异常类
public class GenericException<T> extends Exception { } // 编译错误!
// 不能 catch 泛型异常
try { ... }
catch (T e) { } // 编译错误!
// 但可以在 throws 中使用类型参数
public <T extends Exception> void foo() throws T { } // OK
2
3
4
5
6
7
8
9
# 6.5 通配符与PECS原则
# 6.5.1 为什么需要通配符
核心问题:泛型不是协变的。
// 数组是协变的
Object[] arr = new String[3]; // OK:String[] 是 Object[] 的子类型
// 泛型不是协变的
List<Object> list = new ArrayList<String>(); // 编译错误!
// List<String> 不是 List<Object> 的子类型
2
3
4
5
6
疑惑:String 是 Object 的子类,为什么 List<String> 不是 List<Object> 的子类?
论证:如果 List<String> 是 List<Object> 的子类型:
List<String> strings = new ArrayList<>();
List<Object> objects = strings; // 假设合法
objects.add(123); // List<Object> 可以放 Integer
String s = strings.get(0); // 运行时 ClassCastException!
// 类型安全被破坏
2
3
4
5
为了在保持类型安全的同时允许灵活的参数传递,Java 引入了通配符。
# 6.5.2 上界通配符extends详解
<? extends T> 表示"T 或 T 的某个子类",但具体是哪个子类未知。
// 可以接受 List<Number>、List<Integer>、List<Double> 等
public double sum(List<? extends Number> list) {
double total = 0;
for (Number n : list) {
total += n.doubleValue(); // 可以读取为 Number 类型
}
return total;
}
// 使用
sum(new ArrayList<Integer>()); // OK
sum(new ArrayList<Double>()); // OK
sum(new ArrayList<String>()); // 编译错误!String 不是 Number 的子类
2
3
4
5
6
7
8
9
10
11
12
13
不能写入(除了 null):
List<? extends Number> list = new ArrayList<Integer>();
Number n = list.get(0); // OK:可以读取为 Number
Integer i = list.get(0); // 编译错误:可能是 Double
list.add(1); // 编译错误!
list.add(1.0); // 编译错误!
list.add(null); // OK:null 是任何引用类型的合法值
2
3
4
5
6
7
8
为什么不能写入? 编译器不知道 ? 代表哪个具体子类。如果实际是 List<Double>,放入 Integer 就破坏了类型安全。编译器为了安全,干脆禁止所有非 null 的写入。
# 6.5.3 下界通配符super详解
<? super T> 表示"T 或 T 的某个父类"。
List<? super Integer> list = new ArrayList<Number>();
// 可以写入 Integer 及其子类
list.add(1); // OK:Integer 是 Integer
list.add((short) 1); // 编译错误:Short 不是 Integer 的子类
// 读取只能得到 Object
Object obj = list.get(0); // OK:只知道是 Object
Integer i = list.get(0); // 编译错误:可能是 Number
2
3
4
5
6
7
8
9
为什么可以写入 Integer? 无论 ? 代表 Integer、Number 还是 Object,Integer 都是它们的子类型,写入安全。
为什么读取只能得到 Object? ? 可能是 Integer、Number 或 Object,编译器只能确定它是 Object 的子类。
# 6.5.4 PECS原则详解与实战
Producer Extends, Consumer Super:
| 角色 | 通配符 | 操作 | 助记 |
|---|---|---|---|
| 生产者(从中读取) | ? extends T | 只能读 | PE |
| 消费者(向其写入) | ? super T | 只能写 | CS |
| 既读又写 | 不用通配符 | 精确类型 | — |
Collections.copy 的经典设计:
public static <T> void copy(
List<? super T> dest, // 消费者:写入 T 类型的数据
List<? extends T> src // 生产者:读取 T 类型的数据
) {
for (int i = 0; i < src.size(); i++) {
T item = src.get(i); // 从 extends 中读取
dest.set(i, item); // 向 super 中写入
}
}
// 使用示例
List<Number> numbers = new ArrayList<>(Arrays.asList(0, 0, 0));
List<Integer> integers = Arrays.asList(1, 2, 3);
Collections.copy(numbers, integers);
// dest 是 List<? super Integer>(Number 是 Integer 的父类)
// src 是 List<? extends Integer>(Integer extends Integer)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
实战:设计灵活的 API
// 不灵活的 API
public static double sumList(List<Number> list) { ... }
// 只能传 List<Number>,不能传 List<Integer>
// 灵活的 API(使用 PECS)
public static double sumList(List<? extends Number> list) { ... }
// 可以传 List<Number>、List<Integer>、List<Double>
// Comparable 的正确声明
public static <T extends Comparable<? super T>> T max(List<? extends T> list) {
// T extends Comparable<? super T>:T 可以与自身或父类比较
// List<? extends T>:可以传入 T 或 T 的子类的 List
}
2
3
4
5
6
7
8
9
10
11
12
13
# 6.5.5 无界通配符的场景
<?> 表示"未知类型",等价于 <? extends Object>。
// 只需要调用 Object 的方法时
public static void printAll(List<?> list) {
for (Object item : list) {
System.out.println(item); // Object.toString()
}
}
// 不关心类型参数时
public static boolean isEmpty(List<?> list) {
return list == null || list.isEmpty();
}
// Class<?> 比 Class(原始类型)更安全
Class<?> clazz = String.class; // 表示"某个类",而非"任何类"
2
3
4
5
6
7
8
9
10
11
12
13
14
# 6.6 泛型的高级特性
# 6.6.1 泛型方法与类型推断
// 泛型方法:类型参数声明在方法级别
public static <T> List<T> singletonList(T item) {
return Collections.singletonList(item);
}
// 类型推断(JDK 7+ 钻石操作符)
List<String> list = new ArrayList<>(); // 推断为 ArrayList<String>
// 类型推断(JDK 8+ 改进)
List<String> list = Collections.emptyList(); // 推断返回类型的泛型参数
process(Collections.emptyList()); // 根据方法参数推断
// 显式指定类型参数(通常不需要)
List<String> list = Collections.<String>emptyList();
2
3
4
5
6
7
8
9
10
11
12
13
14
JDK 8 的推断增强:
// JDK 7 中编译失败,JDK 8 中成功
public static <T> void process(List<T> list) { }
process(Collections.emptyList());
// JDK 7: 无法推断 T,需要写 process(Collections.<String>emptyList())
// JDK 8: 根据目标类型推断 T,编译通过
2
3
4
5
6
# 6.6.2 递归类型边界
// Comparable 的经典递归边界
public interface Comparable<T> {
int compareTo(T o);
}
// 实现类声明自己可以与同类型比较
public class Student implements Comparable<Student> {
@Override
public int compareTo(Student other) { ... }
}
// 泛型方法中使用递归边界
public static <T extends Comparable<T>> T max(Collection<T> coll) {
T max = null;
for (T item : coll) {
if (max == null || item.compareTo(max) > 0)
max = item;
}
return max;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
更灵活的写法(处理继承场景):
// 问题:如果 Child extends Parent implements Comparable<Parent>
// max(List<Child>) 会编译失败,因为 Child 不满足 Comparable<Child>
// 解决:使用 ? super T
public static <T extends Comparable<? super T>> T max(Collection<? extends T> coll) {
// Comparable<? super T>:T 可以与 T 的父类比较
// Collection<? extends T>:可以传入 T 的子类集合
}
2
3
4
5
6
7
8
# 6.6.3 多重边界
// T 必须同时满足多个边界
public class DataProcessor<T extends Comparable<T> & Serializable & Cloneable> {
// T 必须实现 Comparable、Serializable 和 Cloneable
}
// 注意:类边界必须放在第一位
public class Box<T extends Number & Comparable<T>> { } // OK
public class Box<T extends Comparable<T> & Number> { } // 编译错误!类在接口前面
2
3
4
5
6
7
8
# 6.6.4 泛型与可变参数
@SafeVarargs // 抑制堆污染警告
public static <T> List<T> asList(T... items) {
List<T> list = new ArrayList<>();
for (T item : items) {
list.add(item);
}
return list;
}
// 堆污染(Heap Pollution)问题
@SafeVarargs
static <T> T[] toArray(T... args) {
return args; // 看似正确
}
String[] arr = toArray("a", "b");
// 运行时可能抛出 ClassCastException
// 因为可变参数实际创建的是 Object[],而非 String[]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@SafeVarargs 的含义:程序员承诺方法内部不会对泛型可变参数数组做不安全的操作(如写入不兼容类型或将其暴露给外部)。
# 6.7 运行时获取泛型信息
# 6.7.1 Signature属性保留泛型
虽然泛型在字节码指令层面被擦除,但 Class 文件的 Signature 属性中保留了泛型签名信息:
// 使用 javap -v 查看
Signature: #30 // Ljava/util/List<Ljava/lang/String;>;
// 这个信息存储在 Class 文件的属性表中,不影响字节码执行
// 但可以通过反射 API 读取
2
3
4
5
保留的位置:
- 类声明中的泛型参数:
class Dao<T>的 T - 字段声明中的泛型:
List<String> names的 String - 方法签名中的泛型:
List<T> getAll()的 T
不保留的位置:
- 方法内部的局部变量类型参数
- 运行时创建的泛型实例
# 6.7.2 反射获取泛型参数
// 场景1:获取父类的泛型参数
public class UserDao extends BaseDao<User> { }
Type type = UserDao.class.getGenericSuperclass();
ParameterizedType pt = (ParameterizedType) type;
Type[] args = pt.getActualTypeArguments();
Class<?> entityClass = (Class<?>) args[0];
System.out.println(entityClass); // class User
// 场景2:获取字段的泛型参数
public class Config {
private List<String> names;
private Map<String, Integer> scores;
}
Field namesField = Config.class.getDeclaredField("names");
ParameterizedType pt = (ParameterizedType) namesField.getGenericType();
System.out.println(pt.getActualTypeArguments()[0]); // class java.lang.String
Field scoresField = Config.class.getDeclaredField("scores");
ParameterizedType pt2 = (ParameterizedType) scoresField.getGenericType();
System.out.println(pt2.getActualTypeArguments()[0]); // class java.lang.String
System.out.println(pt2.getActualTypeArguments()[1]); // class java.lang.Integer
// 场景3:获取方法参数的泛型
public void process(List<String> names) { }
Method method = getClass().getMethod("process", List.class);
Type[] paramTypes = method.getGenericParameterTypes();
ParameterizedType pt = (ParameterizedType) paramTypes[0];
System.out.println(pt.getActualTypeArguments()[0]); // class java.lang.String
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# 6.7.3 TypeToken模式
Gson 等库使用的经典模式,通过匿名子类捕获泛型信息:
// Gson 的 TypeToken
Type type = new TypeToken<List<User>>(){}.getType();
List<User> users = gson.fromJson(json, type);
// 原理:匿名子类在编译时会将泛型参数写入 Signature 属性
// new TypeToken<List<User>>(){} 创建了一个匿名类
// 这个匿名类继承 TypeToken<List<User>>
// 通过反射读取匿名类的父类泛型参数即可获取 List<User>
2
3
4
5
6
7
8
自己实现 TypeToken:
public abstract class TypeToken<T> {
private final Type type;
protected TypeToken() {
Type superclass = getClass().getGenericSuperclass();
this.type = ((ParameterizedType) superclass).getActualTypeArguments()[0];
}
public Type getType() { return type; }
}
// 使用
Type listOfString = new TypeToken<List<String>>(){}.getType();
System.out.println(listOfString);
// java.util.List<java.lang.String>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 6.7.4 框架中的泛型获取
| 框架 | 获取方式 | 用途 |
|---|---|---|
| Spring | ResolvableType | 推断 Bean 的泛型类型 |
| MyBatis | TypeParameterResolver | 推断 Mapper 方法的返回类型 |
| Gson | TypeToken | JSON 反序列化目标类型 |
| Jackson | TypeReference | JSON 反序列化目标类型 |
| Guava | TypeToken | 通用的类型工具 |
// Spring 的 ResolvableType
ResolvableType type = ResolvableType.forClass(UserDao.class)
.as(BaseDao.class);
Class<?> entityClass = type.getGeneric(0).resolve();
// entityClass = User.class
// Jackson 的 TypeReference
List<User> users = objectMapper.readValue(json,
new TypeReference<List<User>>(){});
2
3
4
5
6
7
8
9
# 6.8 泛型最佳实践与常见陷阱
# 6.8.1 泛型方法优于泛型类
// 不推荐:泛型类(限制了整个类的类型)
public class Converter<T> {
public String convert(T input) { return input.toString(); }
}
// 推荐:泛型方法(更灵活,每次调用可以不同类型)
public class Converter {
public <T> String convert(T input) { return input.toString(); }
}
Converter converter = new Converter();
converter.convert("hello"); // T = String
converter.convert(123); // T = Integer
2
3
4
5
6
7
8
9
10
11
12
13
# 6.8.2 常见的泛型陷阱
陷阱一:泛型数组
// 错误示范
public <T> T[] toArray(List<T> list) {
return (T[]) list.toArray(); // 实际返回 Object[]!
}
String[] arr = toArray(Arrays.asList("a", "b"));
// ClassCastException: Object[] cannot be cast to String[]
// 正确做法
public <T> T[] toArray(List<T> list, Class<T> clazz) {
@SuppressWarnings("unchecked")
T[] arr = (T[]) Array.newInstance(clazz, list.size());
return list.toArray(arr);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
陷阱二:泛型方法重载
// 编译错误:擦除后签名相同
public void process(List<String> list) { }
public void process(List<Integer> list) { }
// 擦除后都是 process(List list),编译器报重复方法
2
3
4
陷阱三:原始类型与泛型混用
List rawList = new ArrayList<String>(); // 原始类型
rawList.add(123); // 编译通过,但类型安全被破坏
List<String> typedList = rawList; // 编译警告
String s = typedList.get(0); // 运行时 ClassCastException
2
3
4
5
# 6.8.3 类型安全的异构容器
// Effective Java 推荐的 TypeSafe Heterogeneous Container 模式
public class Favorites {
private Map<Class<?>, Object> map = new HashMap<>();
public <T> void put(Class<T> type, T instance) {
map.put(type, type.cast(instance)); // 确保类型安全
}
public <T> T get(Class<T> type) {
return type.cast(map.get(type)); // 安全强转
}
}
Favorites f = new Favorites();
f.put(String.class, "hello");
f.put(Integer.class, 123);
String s = f.get(String.class); // "hello",无需强转
Integer i = f.get(Integer.class); // 123
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
这个模式被广泛应用于依赖注入框架中。
# 6.9 Java vs 其他语言的泛型对比
| 特性 | Java(擦除式) | C#(具化式) | Kotlin | Go 1.18+ |
|---|---|---|---|---|
| 运行时类型 | 不保留 | 保留 | 不保留(JVM限制) | 部分保留 |
| 基本类型泛型 | 不支持 | 支持 | 不支持(内联类优化) | 支持 |
new T() | 不可以 | 可以 | reified 内联函数支持 | 不直接支持 |
| 协变/逆变 | 使用处(通配符) | 声明处 + 使用处 | 声明处(out/in) | 不支持 |
| 向后兼容 | 完全兼容 | 不兼容旧 .NET 1.x | N/A | 兼容旧 Go |
| 性能 | 有装箱开销 | 值类型无装箱 | 有装箱开销 | 有部分字典开销 |
Kotlin 的 reified 关键字:
// Kotlin 通过内联函数 + reified 突破擦除限制
inline fun <reified T> isType(value: Any): Boolean {
return value is T // 在 Java 中不可能!
}
// 原理:编译时将 T 替换为具体类型,内联到调用处
isType<String>("hello")
// 内联后变成:
"hello" is String
2
3
4
5
6
7
8
9
# 6.10 总结与核心要点
泛型设计哲学:
- 编译期安全,运行期擦除:用编译器的类型检查代替运行时的强制转换
- 向后兼容优先:擦除保证了泛型代码和旧字节码的兼容性,但牺牲了运行时类型信息
- PECS 原则:extends 用于读(生产者)、super 用于写(消费者),是泛型 API 设计的金科玉律
- Signature 属性:泛型信息虽然在指令层面擦除,但在类文件元数据中保留,框架可以通过反射读取
泛型限制速查表:
| 限制 | 原因 | 替代方案 |
|---|---|---|
不能 new T() | 擦除后变成 new Object() | 传入 Class<T> 或 Supplier<T> |
不能 new T[] | 数组需要运行时类型检查 | Array.newInstance(clazz, size) |
不能 instanceof List<String> | 运行时无泛型信息 | instanceof List<?> |
不能 List<int> | int 不是 Object 子类 | List<Integer>(自动装箱) |
| 不能声明泛型异常类 | catch 需要运行时类型匹配 | 使用非泛型异常 |
| 静态字段不能用类泛型参数 | 泛型属于实例级别 | 静态泛型方法 |
核心要点:
| 问题 | 答案 |
|---|---|
| 什么是类型擦除 | 编译后泛型参数替换为上界(默认 Object) |
| 桥接方法的作用 | 保证擦除后多态仍然正确 |
| extends vs super | extends 只读,super 只写 |
| 框架怎么获取泛型信息 | Class 文件的 Signature 属性 + 反射 API |
| TypeToken 原理 | 匿名子类将泛型参数编码到 Signature 中 |
理解泛型擦除,就理解了 Java 类型系统最深层的设计取舍。