Java 泛型核心机制、类型擦除与实战技巧
Java 泛型是语言层面的重要特性,它让我们在编译期就能获得类型安全,同时保持代码的复用性。不过,理解其背后的原理(尤其是类型擦除)对于编写健壮的泛型代码至关重要。下面我们从基础语法开始,逐步深入到边界约束和通配符的使用。
泛型类
语法规范
定义泛型类时,只需在类名后加上尖括号包裹的类型形参即可。
class ClassName<T1, T2, ..., Tn> {
// 这里可以使用类型参数
}
如果需要继承,也可以指定父类的泛型参数:
class ClassName<T1, T2, ..., Tn> extends ParentClass<T1> {
// 可以只使用部分类型参数
}
实际使用
实例化时,必须明确具体的类型实参。
MyArray<String> myArray = new MyArray<>();
下面是一个完整的示例,展示如何定义一个通用的数组容器:
class MyArray<T> {
private Object[] array = new Object[10];
public void set(int pos, T val) {
array[pos] = val;
}
public T get(int pos) {
return (T) array[pos];
}
public Object[] getArray() {
return array;
}
}
public class Test {
public static void main(String[] args) {
MyArray<String> myArray = new MyArray<>();
}
}
泛型上界
有时候我们需要对传入的类型变量做约束,比如要求它必须是某个类的子类。这时可以用 extends 关键字来定义上界。
定义与使用
class MyArray<T extends Number> {
// ...
}
如果尝试传入非 Number 的子类(如 String),编译器会直接报错。这保证了我们在方法内部调用 Number 特有方法时的安全性。
再看一个更实用的例子,利用 Comparable 接口找出数组中的最大值:
class Alg<T extends Comparable<T>> {
public T findMax(T[] array) {
T max = array[0];
for (int i = 1; i < array.length; i++) {
if (array[i].compareTo(max) > 0) {
max = array[i];
}
}
return max;
}
}
类型擦除
这是 Java 泛型最'坑'但也最关键的地方。JVM 在运行时并不保留泛型信息,所有的泛型类型都会被擦除为原始类型(Raw Type)。
这意味着 List<String> 和 List<Integer> 在运行时都是 List。为了弥补这一点,编译器会在编译阶段做几件事:
- 移除类型参数:字节码中不再包含泛型信息。
- 替换为上界:如果没有显式上界,默认替换为
Object;如果有,则替换为指定的上界。 - 插入类型转换:在读取元素时,编译器会自动插入强制类型转换代码。
- 生成桥接方法:为了保证与旧版本代码的兼容性,可能会生成额外的桥接方法。
这种机制虽然牺牲了运行时的多态性,但换来了向后兼容性和性能上的稳定。
通配符
当不确定具体类型,但又想操作泛型集合时,可以使用通配符 ?。
通配符上界 <? extends T>
主要用于读取数据(PECS 原则中的 Producer Extends)。
class Message<T> {
private T message;
public T getMessage() { return message; }
public void setMessage(T message) { this.message = message; }
}
public static void funExtends(Message<? extends Fruit> temp) {
System.out.println(temp.getMessage());
// 注意:不能调用 setMessage,因为编译器不知道具体是 Apple 还是 Banana
}
通配符下界 <? super T>
主要用于写入数据(PECS 原则中的 Consumer Super)。
class Plate<T> {
private T plate;
public T getPlate() { return plate; }
public void setPlate(T plate) { this.plate = plate; }
}
public static void funSuper(Plate<? super Fruit> temp) {
// 可以添加 Fruit 或其子类
temp.setPlate(new Apple());
temp.setPlate(new Banana());
// 读取时只能当作 Object 处理
System.out.println(temp.getPlate());
}
泛型方法
泛型方法独立于类,可以在静态上下文中使用。
class Alg2 {
public static <T extends Comparable<T>> T findMax(T[] array) {
T max = array[0];
for (int i = 1; i < array.length; i++) {
if (array[i].compareTo(max) > 0) {
max = array[i];
}
}
return max;
}
}
// 调用时显式指定类型
Integer ret = Alg2.<Integer>findMax(array);
泛型的限制
在使用泛型时,有几个常见的坑需要避开:
- 基本类型不可用:不能用
int、double等作为类型参数,必须用包装类(如Integer)。 - 泛型数组创建受限:由于类型擦除,
new List<String>[10]是不允许的。 - 无法实例化泛型对象:
new List<String>()错误,应使用new ArrayList<>()。 - 运行时类型查询:
instanceof或getClass()无法获取泛型的具体类型参数。 - 无继承关系:
List<A>不是List<B>的子类,即使A是B的子类。 - 反射限制:反射无法直接获取泛型参数的具体信息。
掌握这些规则,配合类型擦除的理解,就能写出既安全又灵活的 Java 泛型代码了。

