一、泛型是什么?
1. 定义:
泛型允许你在定义类、接口或方法时使用类型参数(Type Parameter)。在使用时(如声明变量、创建实例时),再用具体的类型实参(Type Argument) 替换这个参数。它就像是方法的形参和实参,但操作的对象是类型本身。
2. 核心目的:
类型安全(Type Safety):在编译期就能检查类型是否正确,将运行时错误(ClassCastException)转变为编译期错误。
// 没有泛型:编译通过,运行时报 ClassCastException List list = new ArrayList(); list.add("Hello"); Integer num = (Integer) list.get(0); // 运行时错误! // 有泛型:编译期直接报错,无法通过编译 List<String> list = new ArrayList<>(); list.add("Hello"); Integer num = list.get(0); // 编译错误:不兼容的类型
消除强制类型转换(Eliminate Casts):代码更简洁、清晰。
// 没有泛型 String str = (String) list.get(0); // 有泛型 String str = list.get(0); // 自动知道是String,无需强转
二、泛型擦除(Type Erasure)—— 泛型的实现原理
这是 Java 泛型的核心机制,也是很多限制的根源。
1. 是什么?
Java 的泛型是在编译器层面实现的,而不是在运行时。在编译后,所有的泛型类型信息都会被移除(擦除)。编译器在生成字节码时:
将泛型类型参数替换为它的边界(Bound)(如
T extends Number
则替换为Number
)。如果类型参数是无边界的(如
<T>
),则替换为Object
。随之插入必要的强制类型转换,以保持类型安全。
2. 例子:
// 源代码(编译前)
public class Box<T> {
private T value;
public void set(T value) { this.value = value; }
public T get() { return value; }
}
Box<String> stringBox = new Box<>();
stringBox.set("Hello");
String value = stringBox.get(); // 无需强转
// 编译后(概念上,字节码级别)
public class Box { // T 被擦除
private Object value; // T 被替换为 Object
public void set(Object value) { this.value = value; }
public Object get() { return value; } // 返回Object
}
Box stringBox = new Box(); // raw type
stringBox.set("Hello");
String value = (String) stringBox.get(); // 编译器插入了强转!
3. 带来的影响与限制:
不能使用基本类型:如
List<int>
是错误的,必须用List<Integer>
。因为擦除后是Object
,而Object
不能持有int
。instanceof 和 getClass():运行时无法检测泛型类型。
List<String> list = new ArrayList<>(); System.out.println(list instanceof List<String>); // 编译错误 System.out.println(list instanceof List); // 正确,但只能检查到是List,不是List<String>
不能创建泛型数组:
new T[]
或new List<String>[]
都是错误的。因为数组需要在运行时知道其确切的元素类型来保证类型安全,而擦除破坏了这个机制。不能实例化类型参数:
new T()
是错误的,因为擦除后是new Object()
,这通常不是你想要的。
三、桥接方法(Bridge Method)—— 保护多态
桥接方法是编译器为了解决类型擦除与多态冲突而自动生成的方法。
场景: 当一个类继承或实现了一个泛型类/接口,并重写了其中的泛型方法时。
例子:
// 泛型接口
public interface Comparable<T> {
int compareTo(T other);
}
// 实现类
public class String implements Comparable<String> {
// 我们重写的方法签名:int compareTo(String other)
@Override
public int compareTo(String other) { ... }
}
由于类型擦除,父接口 Comparable
中的方法在字节码层面变成了 int compareTo(Object other)
。这导致子类 String
实际上有两个方法:
int compareTo(String other)
(我们自己写的)int compareTo(Object other)
(编译器生成的桥接方法)
桥接方法内部做了什么?
// 编译器生成的桥接方法(概念上)
public int compareTo(Object other) {
// 在桥接方法中,进行类型检查和安全地向下转型
return compareTo((String) other); // 调用我们重写的那个具体类型的方法
}
桥接方法确保了即使在类型擦除后,Java的多态机制(父类引用调用子类方法)也能正常工作,同时保证了类型安全。
四、泛型继承和通配符(Wildcards):extends
& super
这是泛型中最难理解但最强大的部分,通常用 PECS(Producer-Extends, Consumer-Super) 原则来概括。
1. 泛型不变性(Invariance)
首先,理解这一点至关重要:Box<String>
和 Box<Object>
没有继承关系,即使 String
是 Object
的子类。
Box<Object> box = new Box<String>(); // 编译错误!不兼容的类型
这种特性称为不变性(Invariance)。它保证了类型安全。如果上面成立,你就可以 box.set(new Integer(100))
,从而把一个 Integer
放进一个声明为 String
的盒子里。
2. 通配符 ?
为了解决需要泛型协变的需求,引入了通配符 ?
。
3. 上界通配符 ? extends T
(Producer)
含义:表示“未知的某种类型,但它是
T
或T
的子类”。用途:当你主要从泛型结构中读取数据(Producer) 时使用。
规则:你可以安全地从中读取(赋值给
T
或父类引用),但不能向其写入(除了null
)。因为编译器不知道具体是哪种子类,写入可能破坏类型安全。List<? extends Number> numbers = new ArrayList<Integer>(); // 协变,允许 Number num = numbers.get(0); // OK, 可以读取为Number numbers.add(new Integer(100)); // 编译错误!不能写入
4. 下界通配符 ? super T
(Consumer)
含义:表示“未知的某种类型,但它是
T
或T
的父类”。用途:当你主要向泛型结构中写入数据(Consumer) 时使用。
规则:你可以安全地向其写入
T
或T
的子类对象,但读取出来只能赋值给Object
引用。因为编译器只知道容器里是T
的父类,无法确定具体类型。List<? super Integer> list = new ArrayList<Number>(); // 逆变,允许 list.add(new Integer(123)); // OK, 可以写入Integer及其子类 Integer num = list.get(0); // 编译错误!无法安全读取 Object obj = list.get(0); // OK, 只能读取为Object
5. PECS 原则总结
Producer-Extends (P-E):如果你需要一个提供(生产)
T
对象的泛型结构(主要调用get()
),使用<? extends T>
。例如:Collection<? extends T>.get()
。Consumer-Super (C-S):如果你需要一个接收(消费)
T
对象的泛型结构(主要调用add()
),使用<? super T>
。例如:Collection<? super T>.add(T)
。既生产又消费:如果你既要读又要写,那就不要用通配符,直接用确切的类型,如
<T>
。
经典应用:Collections.copy()
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
// dest 是消费者 (Consumer),消费T对象,所以用 ? super T
// src 是生产者 (Producer),生产T对象,所以用 ? extends T
for (int i =0; i < src.size(); i++) {
dest.set(i, src.get(i));
}
}
五、常见问题总结
Q:“详细讲讲Java的泛型。”
A:
“Java泛型的核心目的是提供编译时类型安全和消除强制类型转换。它的实现机制是类型擦除,即在编译后泛型信息会被移除,类型参数会被替换为它的边界或Object,并由编译器自动插入强制转换。
类型擦除带来了一些限制,比如不能使用基本类型、不能进行泛型的instanceof检查、不能创建泛型数组等。为了解决擦除与多态的冲突,编译器会生成桥接方法,它在子类重写泛型方法时,负责进行类型检查和安全转型,从而保证多态性。
泛型具有不变性,Box<String>
不是 Box<Object>
的子类。为了更灵活的API设计,引入了通配符 ?
和 PECS原则:
? extends T
用于生产者,表示可以安全读取,但不能写入。? super T
用于消费者,表示可以安全写入,但读取受限。