摘要 :编译期有泛型,运行期没泛型。泛型擦除是 Java 为了向后兼容做出的妥协,但也带来了
instanceof不能用、反射受限等一系列坑。
一、问题现象
java
public class GenericErasure {
public static void main(String[] args) {
List<String> strings = new ArrayList<>();
List<Integer> integers = new ArrayList<>();
System.out.println(strings.getClass() == integers.getClass()); // true!
System.out.println(strings.getClass()); // class java.util.ArrayList
}
}
运行结果:
true
class java.util.ArrayList
「明明泛型参数不同,为什么 getClass() 结果一样?」
再看一个更迷惑的:
java
List<String> list = new ArrayList<>();
list.add("hello");
// 绕过编译器泛型检查(反射)
Method addMethod = list.getClass().getMethod("add", Object.class);
addMethod.invoke(list, 123);
addMethod.invoke(list, new Date());
System.out.println(list.get(1)); // 123(Integer 类型!)
System.out.println(list.get(2)); // Date 对象!
// 编译器以为 list 里全是 String,但实际上什么都有
String s = list.get(0); // ✅
String s2 = list.get(1); // ❌ ClassCastException(运行时)
二、踩坑现场
场景 1:instanceof 不能用泛型
java
// ❌ 编译错误
if (obj instanceof List<String>) { // 编译报错!
// ...
}
报错信息:Illegal generic type for instanceof
场景 2:反射获取泛型参数失败
java
public class GenericDao<T> {
private Class<T> entityClass;
public GenericDao() {
// ❌ 运行期拿不到 T 的具体类型
Type superClass = getClass().getGenericSuperclass();
ParameterizedType paramType = (ParameterizedType) superClass;
entityClass = (Class<T>) paramType.getActualTypeArguments()[0];
// 运行时 T 已被擦除为 Object,强制转换会出错
}
}
场景 3:泛型数组创建失败
java
// ❌ 编译错误
List<String>[] arrays = new ArrayList<String>[10]; // 编译报错!
// 原因:数组在运行期检查元素类型,但泛型已被擦除,无法检查
三、原理解析
3.1 什么是泛型擦除(Type Erasure)
Java 的泛型是编译期的语法糖,运行期不存在。
编译期:编译器做泛型检查(不允许往 List<String> 里加 Integer)
↓
字节码:泛型信息被"擦除",替换成上限类型(通常是 Object)
↓
运行期:ArrayList<String> 和 ArrayList<Integer> 都是 ArrayList<Object>
擦除规则:
| 泛型声明 | 擦除后 |
|---|---|
<T> |
Object |
<T extends Number> |
Number |
<T extends Comparable & Serializable> |
Comparable(第一个上限) |
3.2 为什么 Java 要用泛型擦除?
根本原因:向后兼容。
Java 5 才引入泛型,但 JDK 1.4 及之前的代码里已经有大量 List、Map 的使用。如果泛型在运行期保留,所有旧代码都需要重新编译,代价不可接受。
于是 Java 选择了「编译期泛型 + 运行期擦除」的折中方案。
3.3 桥方法(Bridge Method)
泛型擦除带来一个问题:重写的方法签名对不上。
java
// 源码
class MyComparator implements Comparator<String> {
@Override
public int compare(String a, String b) { ... }
}
擦除后:
java
// 擦除后的等价代码(编译器自动生成)
class MyComparator implements Comparator {
public int compare(String a, String b) { ... }
// 编译器自动生成的桥方法
public int compare(Object a, Object b) {
return compare((String) a, (String) b); // 强制转换
}
}
桥方法是编译器在字节码层面自动生成的,用来保证多态正确工作。
3.4 编译期泛型检查的本质
java
List<String> list = new ArrayList<>();
list.add("hello"); // ✅ 编译通过
list.add(123); // ❌ 编译错误
// 编译后(擦除泛型)
List list = new ArrayList<>();
list.add("hello");
list.add(123); // 编译后的字节码里,这行是合法的!
泛型安全是编译器给你的承诺,运行期不保证。
四、正确写法
4.1 获取泛型参数的正确方式(超类技巧)
java
// ✅ 通过继承保留泛型信息
public abstract class GenericDao<T> {
private final Class<T> entityClass;
protected GenericDao() {
Type superClass = getClass().getGenericSuperclass();
ParameterizedType paramType = (ParameterizedType) superClass;
this.entityClass = (Class<T>) paramType.getActualTypeArguments()[0];
}
}
// 子类必须显式指定泛型参数,才能保留到运行期
public class UserDao extends GenericDao<User> {
// User 的泛型信息会保留在 UserDao 的 Class 对象里
}
4.2 绕不开反射时的防御性编程
java
// ✅ 从泛型集合取元素时,显式检查类型
public void process(List<?> list) {
for (Object obj : list) {
if (obj instanceof String) {
String s = (String) obj;
// ...
}
}
}
4.3 不能用 instanceof 泛型?用通配符
java
// ❌ 不能这样
if (obj instanceof List<String>) { }
// ✅ 只能判断到原始类型
if (obj instanceof List) {
List<?> list = (List<?>) obj;
// 遍历时逐个判断元素类型
}
4.4 创建泛型数组的替代方案
java
// ❌ 不能直接创建泛型数组
// List<String>[] arr = new List<String>[10];
// ✅ 用 List 包装
List<List<String>> lists = new ArrayList<>();
lists.add(new ArrayList<>());
// ✅ 或者用反射(不推荐,仅供理解)
List<String>[] arr = (List<String>[]) new List[10];
五、最佳实践
✅ 泛型的 5 条使用规范
- 泛型只在编译期有效,运行期拿不到泛型类型
instanceof不能用于泛型,只能判断原始类型- 用「超类技巧」在运行期获取泛型参数
- 反射向泛型集合插入元素时,做好类型守卫
- 优先用
List<List<String>>代替List<String>[]
🔍 PECS 原则(Producer Extends, Consumer Super)
java
// ✅ 读取用 extends(生产者)
public void read(List<? extends Number> list) {
for (Number n : list) { ... } // 可以读
// list.add(1); ❌ 不能写(除了 null)
}
// ✅ 写入用 super(消费者)
public void write(List<? super Integer> list) {
list.add(1); // 可以写 Integer
// Integer i = list.get(0); ❌ 读出来是 Object
}
🛠️ 阿里巴巴 Java 开发手册规约
【强制】 泛型通配符
<? extends T>来接收返回的数据,此写法的泛型集合不能使用add方法,而<? super T>不能使用get方法,作为接口调用赋值时易出错。【强制】 抽象类命名使用
Abstract或Base开头。
六、小结
- Java 泛型是编译期语法糖,运行期类型信息被擦除(Type Erasure)
- 擦除后用上限类型替换(无上限则为
Object),因此List<String>.class == List<Integer>.class - 编译器通过生成桥方法保证重写的正确性
- 运行期获取泛型类型需要通过「超类技巧」(子类显式指定泛型参数)
instanceof不能用于泛型,数组不能用泛型------这是擦除的直接后果
下一篇预告:枚举的 ordinal() 别乱用,真的会炸 ------ 枚举序号变了,用 ordinal() 做的判断全错了,这个坑在生产环境见过好几次。