文章目录
140. Java 泛型 - Java 泛型类型的擦除
在 Java 中,泛型(Generics)是一种强大的编程特性,它提供了编译时类型检查,使代码更安全和可读。然而,Java 的泛型是通过类型擦除(Type Erasure)实现的,这意味着泛型信息在编译后会被移除,在运行时不可见。
本文将深入讲解类型擦除的原理、示例、桥接方法,以及它如何影响泛型代码的执行方式。✅
1. 什么是类型擦除?
类型擦除是 Java 编译器在编译泛型代码时执行的一个过程,其主要目的是:
- 将泛型类型参数替换为其上界(bound)或
Object
(如果没有上界)。 - 插入类型转换,以保证类型安全。
- 生成桥接方法(Bridge Methods),确保泛型代码在继承时保持多态性。
🔹 为什么需要类型擦除?
- 向后兼容性:Java 泛型是在 JDK 5 引入的,而 Java 需要与旧版本的 JVM 保持兼容。类型擦除确保泛型不会改变 Java 运行时的类型系统。
- 减少运行时开销:避免为每个泛型类型创建不同的字节码,实现泛型代码的复用。
2. 类型擦除的基本规则
Java 编译器在擦除泛型类型参数时,遵循以下规则:
- 如果类型参数是无界的(
T
),则替换为Object
。 - 如果类型参数是有界的(
T extends X
),则替换为X
(上界的第一个类)。 - 如果泛型方法的返回值或参数类型中包含泛型参数,编译器会在必要时插入强制类型转换。
- 在继承泛型类时,编译器可能会生成桥接方法(Bridge Methods)以维持多态性。
3. 类型擦除示例
🔹 示例 1:无界泛型的擦除
// 泛型类:表示链表中的一个节点
public class Node<T> {
private T data;
private Node<T> next;
public Node(T data, Node<T> next) {
this.data = data;
this.next = next;
}
public T getData() { return data; }
}
类型擦除后,编译器生成的代码:
// 编译后(类型擦除)
public class Node {
private Object data; // T 被擦除为 Object
private Node next;
public Node(Object data, Node next) {
this.data = data;
this.next = next;
}
public Object getData() { return data; } // 返回 Object
}
🔍 解析
-
T
是无界的(没有extends
约束),所以它在编译后被替换为Object
。 -
getData()
方法的返回值从T
变为Object
,因此调用者可能需要手动转换类型:Node<String> node = new Node<>("Hello", null); String data = (String) node.getData(); // 必须进行类型转换
🔹 示例 2:有界泛型的擦除
// 只允许 T 是 Comparable 的子类
public class Node<T extends Comparable<T>> {
private T data;
private Node<T> next;
public Node(T data, Node<T> next) {
this.data = data;
this.next = next;
}
public T getData() { return data; }
}
类型擦除后,编译器生成的代码:
// 编译后(类型擦除)
public class Node {
private Comparable data; // T 被擦除为 Comparable
private Node next;
public Node(Comparable data, Node next) {
this.data = data;
this.next = next;
}
public Comparable getData() { return data; } // 返回 Comparable
}
🔍 解析
-
T extends Comparable<T>
表示T
至少是Comparable
的子类。 -
编译器在擦除
T
时,用Comparable
替换T
(而不是Object
)。 -
这样
data
仍然可以使用Comparable
的方法,如compareTo()
:Node<Integer> intNode = new Node<>(10, null); int result = intNode.getData().compareTo(5); // ✅ 可以调用 compareTo()
4. 类型擦除的影响
🔹 影响 1:不能在运行时获取泛型类型
由于类型擦除的存在,泛型参数在运行时不可用:
List<String> strList = new ArrayList<>();
List<Integer> intList = new ArrayList<>();
System.out.println(strList.getClass() == intList.getClass()); // ✅ 输出 true
为什么?
- 在运行时,
List<String>
和List<Integer>
变成相同的List
类,它们的类型信息被擦除。
🔹 影响 2:不能创建泛型数组
List<String>[] arr = new ArrayList<String>[10]; // ❌ 编译错误
原因
-
由于类型擦除,Java 无法保证数组在运行时的类型安全。
-
解决方案:使用
List<?>[]
或ArrayList
代替:List<?>[] arr = new ArrayList<?>[10]; // ✅ 允许
🔹 影响 3:桥接方法(Bridge Methods)
当子类覆盖带泛型的方法时,编译器可能会生成桥接方法以保持多态性。例如:
class Parent<T> {
public T getValue() { return null; }
}
class Child extends Parent<String> {
@Override
public String getValue() { return "Hello"; }
}
编译后的 Child
类(带桥接方法):
class Child extends Parent {
@Override
public String getValue() { return "Hello"; }
// 桥接方法(编译器自动生成)
@Override
public Object getValue() { // 使得父类的 getValue() 仍然可用
return getValue();
}
}
🔍 解析
Parent<T>
被擦除后,其方法变为Object getValue()
。Child
的getValue()
返回String
,不匹配Parent
的Object getValue()
。- 编译器自动生成 一个
Object getValue()
方法,它内部调用String getValue()
,保证多态性。
5. 结论
泛型类型 | 类型擦除后 |
---|---|
T (无界) | Object |
T extends X | X (上界的第一个类) |
List<T> | List<Object> |
T[] | 不允许 |
⚠ 需要注意
- 泛型信息在运行时不可用(因为被擦除)。
- 不能创建泛型数组。
- 可能会有桥接方法,以确保子类的多态行为。
通过理解类型擦除的机制,我们可以更安全、高效地使用 Java 泛型,避免潜在的坑。🎯