泛型的类型擦除是什么?

2025年 阅读约 8 分钟 面试指南 · Java面试

深入解析Java泛型类型擦除机制,从基本概念到桥接方法,分初级、中级、高级三个层次全面讲解。

一句话总结

Java 泛型是编译期检查,运行时通过类型擦除替换为原始类型(无界→Object,有界→上界),兼容旧代码。PECS 原则:Producer Extends(读)、Consumer Super(写)。桥接方法保持多态,匿名内部类可绕过擦除获取泛型信息。

初级理解

Java 泛型使用类型擦除(Type Erasure)实现,泛型信息只在编译期存在,编译后会被擦除,替换为原始类型(Raw Type)。这是为了兼容 JDK 5 之前的代码。

擦除规则:

1. 无限定的类型参数 <T> → 擦除为 Object

2. 有上界的类型参数 <T extends Number> → 擦除为上界 Number

3. 有多个上界的 <T extends A & B> → 擦除为第一个上界 A

// 编译前 List<String> list = new ArrayList<>(); list.add("hello"); String s = list.get(0); // 编译后(等价于) List list = new ArrayList(); list.add("hello"); String s = (String) list.get(0); // 编译器自动插入强制转型

由于类型擦除,以下代码无法通过编译(两个方法签名冲突):

// 编译错误:两个方法签名相同 public void print(List<String> list) {} public void print(List<Integer> list) {}

中级深入

通配符和 PECS 原则:

PECS(Producer Extends, Consumer Super):从泛型集合读取数据用 ? extends T(生产者),向泛型集合写入数据用 ? super T(消费者)。

// Producer Extends:只能读,不能写 List<? extends Number> numbers = new ArrayList<Integer>(); Number n = numbers.get(0); // OK,读取 // numbers.add(1); // 编译错误,不能写入 // Consumer Super:只能写,读出来是 Object List<? super Integer> list = new ArrayList<Number>(); list.add(1); // OK,写入 Object obj = list.get(0); // 只能读到 Object

泛型数组为什么不能创建?由于类型擦除,数组在运行时知道自己的元素类型,但泛型信息已被擦除,两者冲突。例如 new ArrayList<String>[10] 是非法的。

高级拓展

桥接方法(Bridge Method):当子类实现泛型接口或继承泛型父类时,编译器会自动生成桥接方法来保持多态性。

// 源码 interface Comparator<T> { int compare(T o1, T o2); } class StringLengthComparator implements Comparator<String> { @Override public int compare(String o1, String o2) { return o1.length() - o2.length(); } } // 编译后,StringLengthComparator 实际有两个方法: // 1. public int compare(String o1, String o2) — 我们写的 // 2. public int compare(Object o1, Object o2) — 桥接方法 // { return compare((String)o1, (String)o2); }

如何绕过类型擦除获取泛型信息?虽然运行时泛型被擦除了,但可以通过反射获取某些场景下的泛型信息:

1. 子类继承泛型父类时,父类的泛型参数会被保留在字节码的 Signature 属性中

2. 通过 ParameterizedType 可以获取:

// 匿名内部类保留泛型信息 Type type = new TypeReference<List<String>>() {}.getType(); // type 可以获取到 List<String> 的完整泛型信息
面试加分项:能说出桥接方法的生成原理和 PECS 原则,说明你对泛型有深入理解。

实战场景

场景:泛型 DAO 基类

// 泛型 DAO 基类,减少重复代码 public class BaseDao<T, ID> { @Autowired private JdbcTemplate jdbcTemplate; public T findById(ID id) { // 通过反射获取泛型实际类型 Class<T> entityClass = getEntityClass(); return jdbcTemplate.queryForObject( "SELECT * FROM " + entityClass.getSimpleName() + " WHERE id = ?", new BeanPropertyRowMapper<>(entityClass), id); } private Class<T> getEntityClass() { // 子类继承时保留泛型信息 return (Class<T>) ((ParameterizedType) getClass() .getGenericSuperclass()).getActualTypeArguments()[0]; } } // 使用:一行代码搞定 public class UserDao extends BaseDao<User, Long> {}

场景:PECS 实战 — 集合复制

// 从 src 读取(Producer),写入 dest(Consumer) public static <T> void copy(List<? extends T> src, List<? super T> dest) { for (T item : src) { dest.add(item); } } List<Integer> ints = Arrays.asList(1, 2, 3); List<Number> nums = new ArrayList<>(); copy(ints, nums); // Integer extends Number → OK

面试模拟

Q:为什么 Java 泛型用类型擦除而不是真泛型?

A:为了向后兼容。JDK 5 引入泛型时,已有大量无泛型代码。类型擦除让泛型代码和旧代码可以互操作,List<String> 和 List 在运行时是同一个类。代价是运行时丢失类型信息,无法用 instanceof 检查泛型类型。

Q:List<Object> 和 List<?> 有什么区别?

A:List<Object> 可以添加任意 Object,List<?> 只能添加 null(因为不知道具体类型)。List<Object> 是 List<?> 的子类型,但 List<Object> 不是 List<String> 的父类型(泛型没有协变性)。