Java数据结构 (泛型第二节) 泛型擦除机制/泛型的限制/上界下界
书接上回:Java数据结构 (泛型第一节) 为什么要有泛型/泛型语法/泛型方法-CSDN博客
访问作者Github:
https://github.com/Joeysoda/Github_java/blob/main/20240908%E6%B3%9B%E5%9E%8B/src/%E6%B3%9B%E5%9E%8B.java
目录
1. 为什么要有擦除机制?
2. 类型擦除的工作原理
2.1 泛型类的类型擦除
2.2 泛型方法的类型擦除
3. 泛型的上界与下界
3.1 指定上界的泛型
4. 类型擦除带来的限制
4.1 无法创建泛型数组
4.2 无法使用 instanceof 检查泛型类型
4.3 无法创建泛型实例
1. 为什么要有擦除机制?
Java 在 1.5 版本引入了泛型,但 Java 语言的设计理念是保证向下兼容。这意味着即使引入了泛型,Java 程序仍然要能够与不使用泛型的老版本代码(比如 Java 1.4 及更早的代码)兼容。为了实现这一点,Java 的泛型设计为在编译时执行类型检查,而在运行时完全去除泛型信息。这种机制被称为类型擦除。
如果没有类型擦除机制,使用泛型的 Java 程序在运行时将需要处理更多的类型信息,这可能会导致与现有 Java 代码的兼容性问题,并且会增加运行时的复杂性。
2. 类型擦除的工作原理
泛型的类型参数 T
在编译后会被替换为其上界(Upper Bound),如果没有指定上界,则默认为 Object
。编译器会在编译时插入必要的类型转换代码,以确保类型安全。
2.1 泛型类的类型擦除
示例代码:
public class Box<T> {
private T value;
public void setValue(T value) {
this.value = value;
}
public T getValue() {
return value;
}
}
public class Main {
public static void main(String[] args) {
Box<String> box = new Box<>();
box.setValue("Hello");
String value = box.getValue();
System.out.println(value); // 输出: Hello
}
}
编译时类型擦除后的代码:
public class Box {
private Object value; // T 被替换为 Object
public void setValue(Object value) {
this.value = value;
}
public Object getValue() {
return value;
}
}
public class Main {
public static void main(String[] args) {
Box box = new Box();
box.setValue("Hello");
String value = (String) box.getValue(); // 编译器自动插入类型转换
System.out.println(value); // 输出: Hello
}
}
解释:
- 在编译后的代码中,泛型类型
T
被擦除为Object
,这就是类型擦除的工作方式。 - 在运行时,
Box
类实际上只保存了Object
类型,而不是String
类型。 - 编译器自动插入了类型转换
(String) box.getValue()
,以保证类型安全。
2.2 泛型方法的类型擦除
泛型方法示例:
public static <T> void printArray(T[] array) {
for (T element : array) {
System.out.println(element);
}
}
编译时类型擦除后的代码:
public static void printArray(Object[] array) { // T 被擦除为 Object
for (Object element : array) {
System.out.println(element);
}
}
解释:
- 在泛型方法的编译后,类型参数
T
被替换为了Object
,这意味着在运行时,方法printArray
接受的参数类型就是Object[]
,而不再区分具体的泛型类型。
3. 泛型的上界与下界
当泛型类或方法定义了类型边界时,类型擦除后泛型类型会被替换为其上界。如果没有指定上界,则替换为 Object
。
3.1 指定上界的泛型
代码示例:
public class NumberBox<T extends Number> { // T 的上界是 Number
private T value;
public void setValue(T value) {
this.value = value;
}
public T getValue() {
return value;
}
}
编译后的代码:
public class NumberBox {
private Number value; // T 被替换为 Number
public void setValue(Number value) {
this.value = value;
}
public Number getValue() {
return value;
}
}
解释:
- 在编译后,泛型类型
T
被替换为了Number
,因为在定义时我们指定了T extends Number
,即T
的上界是Number
。 - 这意味着编译后的
NumberBox
类可以接受任何Number
类型的子类对象(如Integer
,Double
),但所有泛型信息在运行时都被擦除了。
4. 类型擦除带来的限制
由于泛型的类型擦除机制,Java 中的泛型有一些限制。以下是类型擦除带来的一些常见问题:
4.1 无法创建泛型数组
由于类型擦除,泛型类型在运行时被擦除为 Object
,因此 Java 不允许直接创建泛型数组。因为在运行时无法知道数组元素的具体类型。
T[] array = new T[10]; // 错误:不能创建泛型数组
解决方案: 可以通过强制类型转换来创建泛型数组,使用 Object[]
来存储泛型类型。
T[] array = (T[]) new Object[10]; // 通过类型转换创建泛型数组
但这种方式会导致编译器发出未经检查的转换警告,因为无法在运行时确定数组的实际类型。
4.2 无法使用 instanceof
检查泛型类型
由于泛型在运行时被擦除为 Object
,所以不能使用 instanceof
检查泛型类型。
错误示例:
if (obj instanceof T) { // 错误,无法进行泛型类型检查
// ...
}
解决方案: 可以通过检查擦除后的类型来替代 instanceof
,比如使用泛型的上界。
if (obj instanceof Number) { // T extends Number,可以检查 Number 类型
// ...
}
4.3 无法创建泛型实例
由于泛型在运行时类型被擦除为 Object
或其上界,因此不能在类中直接创建泛型类型的实例。
错误示例:
T obj = new T(); // 错误:无法实例化泛型类型
解决方案: 可以通过传递一个类的引用(Class<T>
对象)来间接创建泛型对象。
示例:
class Box<T> {
private T value;
public Box(Class<T> clazz) throws IllegalAccessException, InstantiationException {
// 使用传递的 Class 对象创建泛型实例
this.value = clazz.newInstance();
}
}
面试题目:
1. 为什么 T[] ts = new T[5];
是不对的?为什么不能直接创建泛型数组?
问题:
既然在编译时泛型 T
会被替换为 Object
,为什么 T[] ts = new T[5];
不能写成类似于 Object[] ts = new Object[5];
?
答案解析:
泛型的类型擦除机制并不能直接与数组创建的机制兼容,因为数组在 Java 中具有 "协变性"(covariant)。这意味着,如果 Integer[]
是 Object[]
的子类型,那么可以将 Integer[]
赋值给 Object[]
类型的变量。这会导致潜在的类型安全问题,尤其是在数组的运行时类型检查过程中,泛型类型擦除与数组的运行时行为产生了冲突。
解决方案:
可以通过创建 Object[]
数组并进行类型转换的方式间接创建泛型数组,但这会发出未经检查的类型转换警告。
-
数组的运行时类型检查:数组在 Java 中保留其类型信息,并且能够在运行时进行类型检查。如果尝试在运行时向
Object[]
数组中插入不兼容的元素,Java 会抛出ArrayStoreException
,因为数组知道自己是存储哪种类型的对象的。例如:
Object[] objects = new Integer[5]; objects[0] = "Hello"; // 运行时会抛出 ArrayStoreException
-
泛型的类型擦除:Java 的泛型在编译时擦除类型信息,所以如果我们能够创建
T[] ts = new T[5];
,编译器无法确保数组的类型安全,因为擦除后T
被替换为了Object
。这意味着我们无法在运行时对数组的元素类型进行检查,可能会导致数组元素的类型与数组声明的类型不匹配。
具体原因总结:
- 数组需要在运行时保留类型信息,而泛型在运行时会被擦除为
Object
,因此创建泛型数组无法保证类型安全。 - 如果允许
T[]
创建,会导致运行时无法检查数组的实际类型,可能会导致ArrayStoreException
。
解决方案:
可以通过创建 Object[]
数组并进行类型转换的方式间接创建泛型数组,但这会发出未经检查的类型转换警告。
@SuppressWarnings("unchecked")
T[] array = (T[]) new Object[5]; // 警告:未经检查的类型转换
2. 类型擦除一定是把 T 变成 Object 吗?
问题:
在类型擦除过程中,泛型 T
是否总是被擦除为 Object
?
答案解析:
不一定。泛型 T
在类型擦除过程中,会根据类型参数的上界来进行替换。如果没有指定上界,那么 T
会被替换为 Object
;但如果有上界,T
会被替换为它的上界类型。
-
无上界的类型擦除:当
T
没有上界时,T
会被擦除为Object
。
class Box<T> {
private T value;
public void setValue(T value) {
this.value = value;
}
public T getValue() {
return value;
}
}
经过类型擦除后:
class Box {
private Object value; // T 被擦除为 Object
public void setValue(Object value) {
this.value = value;
}
public Object getValue() {
return value;
}
}
有上界的类型擦除:如果 T
有上界,例如 T extends Number
,那么在类型擦除时,T
会被替换为 Number
。
代码示例:
class Box<T extends Number> {
private T value;
public void setValue(T value) {
this.value = value;
}
public T getValue() {
return value;
}
}
经过类型擦除后:
class Box {
private Number value; // T 被擦除为 Number
public void setValue(Number value) {
this.value = value;
}
public Number getValue() {
return value;
}
}
总结:
- 无上界泛型
T
:擦除为Object
。 - 有上界泛型
T
:擦除为其上界类型。