引言
泛型(Generics)是 Java 5 引入的一项里程碑式特性,它允许在定义类、接口和方法时使用类型参数,从而在编译期提供类型安全检查,并消除大量的强制类型转换。泛型极大地提升了代码的复用性和可读性,是现代 Java 编程中不可或缺的基石。
JDK 21 虽未对泛型本身做出重大改动,但随着语言特性的持续演进------如 record、sealed interface 的引入------泛型与这些新特性的结合愈发紧密。本文将系统性地剖析泛型的核心概念、使用方法、类型擦除机制、通配符应用以及常见陷阱,帮助你真正吃透泛型,写出更健壮、更优雅的 Java 代码。
一、为什么需要泛型?
在没有泛型的时代,Java 集合只能存储 Object 类型,取出时需要强制类型转换,且无法在编译期发现类型错误:
java
// 没有泛型(Java 5 之前)
List list = new ArrayList();
list.add("hello");
list.add(123); // 可以添加任意类型
String s = (String) list.get(0); // 需要强制转换
Integer i = (Integer) list.get(1); // 运行时可能抛出 ClassCastException
使用泛型后:
java
List<String> list = new ArrayList<>();
list.add("hello");
// list.add(123); // 编译错误,类型安全
String s = list.get(0); // 无需强制转换
泛型带来三大核心价值:
- 编译期类型检查:将错误从运行时提前到编译时,显著提高程序可靠性。
- 消除强制类型转换:代码更简洁,减少出错可能。
- 代码复用:一个泛型类可适用于多种类型,无需为每种类型编写重复代码。
二、泛型类与泛型接口
1. 泛型类
定义泛型类时,在类名后添加类型参数列表:
java
public class Box<T> {
private T content;
public void set(T content) {
this.content = content;
}
public T get() {
return content;
}
}
使用方式:
java
Box<String> stringBox = new Box<>();
stringBox.set("Hello");
String content = stringBox.get();
Box<Integer> intBox = new Box<>();
intBox.set(42);
int value = intBox.get();
类型参数可以是任意标识符,但遵循常见约定能让代码更具可读性:
| 类型参数 | 含义 |
|---|---|
E |
Element(集合元素) |
K |
Key(键) |
V |
Value(值) |
T |
Type(类型) |
S, U, V 等 |
第二、第三、第四个类型 |
? |
通配符(后文详述) |
2. 泛型接口
接口同样可以是泛型的,实现类既可以指定具体类型,也可以继续保留类型参数:
java
public interface Pair<K, V> {
K getKey();
V getValue();
}
// 实现时指定具体类型
public class StringIntPair implements Pair<String, Integer> {
private String key;
private Integer value;
@Override
public String getKey() { return key; }
@Override
public Integer getValue() { return value; }
}
// 实现时保留类型参数
public class GenericPair<K, V> implements Pair<K, V> {
private K key;
private V value;
@Override
public K getKey() { return key; }
@Override
public V getValue() { return value; }
}
三、泛型方法
泛型方法是在方法声明中引入类型参数的方法,既可以在普通类中定义,也可以在泛型类中定义。类型参数的作用域仅限于该方法本身:
java
public class Util {
// 静态泛型方法
public static <T> T getMiddle(T... array) {
return array[array.length / 2];
}
// 泛型方法,返回类型与参数类型相关
public static <T> List<T> singletonList(T item) {
List<T> list = new ArrayList<>();
list.add(item);
return list;
}
}
调用时,编译器会根据参数自动推断类型参数,也可以显式指定:
java
String middle = Util.<String>getMiddle("a", "b", "c");
List<Integer> list = Util.singletonList(42); // 自动推断
泛型方法在工具类中应用广泛,例如 Collections 中的经典方法:
java
public static <T extends Comparable<? super T>> void sort(List<T> list) { ... }
四、通配符与上下界
通配符(?)用于表示未知类型,常用于泛型集合作为方法参数时,以增加 API 的灵活性。
1. 上界通配符(? extends T)
表示类型必须是 T 或 T 的子类,适合读取数据,但不能安全写入(除 null 外):
java
public static double sum(List<? extends Number> numbers) {
double total = 0;
for (Number n : numbers) {
total += n.doubleValue();
}
return total;
}
List<Integer> ints = List.of(1, 2, 3);
List<Double> doubles = List.of(1.5, 2.5);
double sum1 = sum(ints); // OK
double sum2 = sum(doubles); // OK
// 不能添加元素
List<? extends Number> list = new ArrayList<Integer>();
// list.add(1); // 编译错误,因为无法确定具体类型
list.add(null); // 可以添加 null
2. 下界通配符(? super T)
表示类型必须是 T 或 T 的父类,适合写入数据,读取时只能读取为 Object:
java
public static void addNumbers(List<? super Integer> list) {
for (int i = 1; i <= 5; i++) {
list.add(i); // 可以添加 Integer
}
}
List<Number> numbers = new ArrayList<>();
addNumbers(numbers); // OK,Integer 可以向上转型为 Number
List<Object> objects = new ArrayList<>();
addNumbers(objects); // OK
List<Double> doubles = new ArrayList<>();
// addNumbers(doubles); // 编译错误,Double 不是 Integer 的父类
读取时:
java
List<? super Integer> list = new ArrayList<Number>();
Object obj = list.get(0); // 只能读取为 Object
// Integer i = list.get(0); // 编译错误
3. 无界通配符(?)
表示任意类型,常用于不确定具体类型但只进行与类型无关的操作:
java
public static void printList(List<?> list) {
for (Object obj : list) {
System.out.println(obj);
}
}
4. PECS 原则
PECS(Producer Extends, Consumer Super)是使用通配符的核心指导原则:
- 如果参数化类型是生产者 (只读取),使用
? extends T。 - 如果参数化类型是消费者 (只写入),使用
? super T。 - 如果既读取又写入,则不要使用通配符。
例如 Collections.copy 方法签名:
java
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
// dest 是消费者(写入),src 是生产者(读取)
}
五、类型擦除
Java 泛型通过类型擦除(Type Erasure)实现,即编译器在编译时移除泛型类型信息,生成的字节码中不保留泛型参数。类型擦除的规则如下:
- 如果类型参数有上界,则替换为上界类型;否则替换为
Object。 - 必要时插入类型转换。
- 生成桥接方法以保持多态。
例如:
java
public class Box<T> {
private T content;
public T get() { return content; }
public void set(T content) { this.content = content; }
}
编译后相当于:
java
public class Box {
private Object content;
public Object get() { return content; }
public void set(Object content) { this.content = content; }
}
如果类型参数有上界:
java
public class Box<T extends Number> {
private T content;
public T get() { return content; }
}
擦除后:
java
public class Box {
private Number content;
public Number get() { return content; }
}
类型擦除的影响
- 无法使用基本类型作为类型参数 :
List<int>不合法,必须使用包装类List<Integer>。 - 无法通过
instanceof检查泛型类型 :if (list instanceof List<String>)编译错误,因为类型信息已擦除。 - 无法创建泛型数组 :
new T[10]不合法,new List<String>[10]也不合法(但可以创建通配符数组new List<?>[10])。 - 无法在静态上下文中使用类型参数:静态字段和方法不能引用类的类型参数。
- 重载冲突 :由于擦除,
void print(List<String> list)与void print(List<Integer> list)不能作为重载,因为擦除后签名相同。
六、泛型的限制与注意事项
1. 不能使用基本类型
java
List<int> list = new ArrayList<>(); // 编译错误
List<Integer> list = new ArrayList<>(); // OK
2. 不能实例化类型参数
java
public <T> void create() {
// T t = new T(); // 编译错误
// T[] arr = new T[5]; // 编译错误
}
可以通过反射或 Supplier 解决:
java
public <T> T create(Supplier<T> supplier) {
return supplier.get();
}
3. 不能抛出或捕获泛型异常
java
// 不能定义泛型异常类
// public class MyException<T> extends Exception {} // 编译错误
// 不能捕获泛型异常
// try {} catch (T e) {} // 编译错误
但可以在 throws 子句中使用类型参数:
java
public <T extends Throwable> void doWork() throws T { ... }
4. 泛型与继承
List<String> 不是 List<Object> 的子类型:
java
List<String> strings = new ArrayList<>();
// List<Object> objects = strings; // 编译错误
但数组具有协变性,这是数组与泛型的重要区别:
java
String[] strings = new String[5];
Object[] objects = strings; // 数组协变,合法
objects[0] = 1; // 运行时抛出 ArrayStoreException
如果希望泛型具有类似协变或逆变的能力,使用通配符(前文已介绍)。
5. 可变参数与泛型
泛型可变参数会触发"未检查警告",因为可变参数本质上是一个数组,而泛型数组是不安全的:
java
@SafeVarargs
public static <T> List<T> asList(T... a) {
return Arrays.asList(a);
}
如果确定方法不会对数组进行不安全操作,可以使用 @SafeVarargs 注解消除警告。
七、泛型与现代 Java 特性的结合
1. 泛型与 record
record 是 JDK 16 正式引入的不可变数据载体,可以轻松定义泛型 record:
java
public record Pair<K, V>(K key, V value) {
// 自动生成构造器、访问器、equals、hashCode、toString
}
Pair<String, Integer> pair = new Pair<>("age", 30);
String key = pair.key();
Integer value = pair.value();
2. 泛型与 sealed interface
密封接口(sealed interface)限制实现类,与泛型结合可以构建更安全的类型层次结构:
java
public sealed interface Result<T> permits Success, Failure {
record Success<T>(T data) implements Result<T> {}
record Failure<T>(String error) implements Result<T> {}
}
Result<String> result = new Result.Success<>("OK");
if (result instanceof Result.Success<String> success) {
String data = success.data();
}
3. 泛型与 Stream API
Stream API 大量使用泛型,配合方法引用和 lambda 可以写出优雅的链式操作:
java
List<String> names = List.of("Alice", "Bob", "Charlie");
List<Integer> lengths = names.stream()
.map(String::length) // 方法引用,类型推断
.toList();
4. 泛型与 Optional
Optional<T> 是泛型类,用于安全处理可能为 null 的值:
java
Optional<String> optional = Optional.of("value");
String result = optional.orElse("default");
八、泛型常见误区
1. 原始类型的使用
使用原始类型(如 List 而不是 List<String>)会绕过泛型检查,产生未检查警告,并可能导致运行时错误:
java
List rawList = new ArrayList();
rawList.add("hello");
rawList.add(123); // 可以添加任意类型
List<String> stringList = rawList; // 未检查警告
String s = stringList.get(1); // 运行时 ClassCastException
永远不要使用原始类型,除非与旧代码交互且无法避免。
2. 通配符误用
错误地认为 List<?> 可以添加任意类型:
java
List<?> list = new ArrayList<String>();
// list.add("hello"); // 编译错误
// list.add(123); // 编译错误
list.add(null); // 只能添加 null
List<?> 表示元素类型未知,因此不能安全添加任何非 null 元素。
3. 泛型数组创建
java
// List<String>[] array = new List<String>[10]; // 编译错误
List<?>[] array = new List<?>[10]; // 合法,但使用时需小心
如果需要泛型数组,可以使用集合代替,或者通过 ArrayList 等间接实现。
九、最佳实践
- 尽量使用泛型,避免原始类型,以获得编译期类型安全。
- 合理使用通配符,遵循 PECS 原则,提高 API 灵活性。
- 泛型方法优于泛型类:如果某个操作只需要对单个方法参数化,使用泛型方法而不是将整个类泛型化。
- 注意类型擦除的影响 ,不要试图在运行时获取泛型类型信息(可以通过传入
Class<T>对象解决)。 - 使用
@SuppressWarnings("unchecked")时尽量缩小范围,并在注释中说明理由。 - 优先使用
List.of()、Map.of()等不可变集合工厂,它们返回泛型化实例,类型安全。 - 在公共 API 中使用通配符增加灵活性 ,例如
void process(List<? extends Number> numbers)而不是void process(List<Number> numbers)。
十、总结
泛型是 Java 类型系统的核心扩展,它让代码在保持类型安全的同时具备高度的复用性。虽然类型擦除带来了一些限制,但理解其原理能够帮助我们避开陷阱。
通过合理使用泛型类、泛型方法、通配符以及与现代特性(record、sealed interface、Stream)的结合,我们可以编写出健壮、优雅且易于维护的代码。在 JDK 21 中,泛型依然是 Java 语言不可或缺的基石,掌握它是每一位 Java 开发者的必修课。