在Java开发中,泛型(Generics)是一项革命性的特性,它于JDK 5引入,让代码在编译时就能捕获类型错误,并消除了大量繁琐的类型转换。本文将带你全面了解Java泛型的原理、使用场景和最佳实践。
一、为什么需要泛型?
在没有泛型的年代,我们只能这样写集合:
java
List list = new ArrayList();
list.add("hello");
list.add(123); // 随意添加不同类型
String s = (String) list.get(0); // 需要强制转换,容易出错
这种方式存在两个问题:
-
类型不安全:可以向集合中添加任意类型,编译时无法检查。
-
代码冗长 :取值时需要进行强制类型转换,而且转换失败会在运行时抛出
ClassCastException。
泛型解决了这两个问题,它提供了编译时类型安全 和消除了强制转换:
java
List<String> list = new ArrayList<>();
list.add("hello");
// list.add(123); // 编译错误,类型不匹配
String s = list.get(0); // 无需转换
二、泛型的基本概念
2.1 类型参数
泛型中的类型参数通常用单个大写字母表示,常见的有:
-
E- Element(元素,常用于集合) -
K- Key(键) -
V- Value(值) -
N- Number(数字) -
T- Type(类型) -
S,U,V- 第二、三、四个类型参数
2.2 泛型类
定义一个泛型类,在类名后加上尖括号<>:
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 str = stringBox.get();
Box<Integer> intBox = new Box<>();
intBox.set(123);
Integer num = intBox.get();
2.3 泛型接口
接口也可以声明类型参数:
java
public interface Pair<K, V> {
K getKey();
V getValue();
}
实现时指定具体类型:
java
public class OrderedPair<K, V> implements Pair<K, V> {
private K key;
private V value;
public OrderedPair(K key, V value) {
this.key = key;
this.value = value;
}
@Override
public K getKey() { return key; }
@Override
public V getValue() { return value; }
}
2.4 泛型方法
方法也可以声明自己的类型参数,独立于类:
java
public class Util {
// 泛型方法,<T>放在返回值之前
public static <T> T getMiddle(T... a) {
return a[a.length / 2];
}
// 更复杂的例子:比较两个Pair
public static <K, V> boolean compare(Pair<K, V> p1, Pair<K, V> p2) {
return p1.getKey().equals(p2.getKey()) &&
p1.getValue().equals(p2.getValue());
}
}
调用时,类型参数会被自动推断:
java
String middle = Util.getMiddle("a", "b", "c");
Integer midNum = Util.getMiddle(1, 2, 3);
三、类型边界
有时我们需要限制类型参数的范围,例如要求类型参数必须是某个类的子类或实现某个接口。
3.1 上界通配符
使用extends关键字设置上界:
java
// 只接受Number及其子类
public class NumberBox<T extends Number> {
private T number;
public double doubleValue() {
return number.doubleValue();
}
}
可以指定多个边界,用&连接:
java
public class MultipleBound<T extends Number & Comparable<T>> {
// T必须同时是Number的子类并实现Comparable接口
}
3.2 泛型方法中的类型边界
java
// 计算数组中的最小值,T必须实现Comparable
public static <T extends Comparable<T>> T min(T[] array) {
if (array == null || array.length == 0) return null;
T smallest = array[0];
for (int i = 1; i < array.length; i++) {
if (smallest.compareTo(array[i]) > 0) {
smallest = array[i];
}
}
return smallest;
}
四、通配符(Wildcard)
通配符?表示未知类型,常用于方法的参数中,使方法更灵活。
4.1 无界通配符
java
public void printList(List<?> list) {
for (Object obj : list) {
System.out.println(obj);
}
}
List<?>表示任何类型的List,但不能向其中添加元素(除了null),因为类型未知。
4.2 上界通配符
java
// 接受任何Number及其子类的List
public double sum(List<? extends Number> list) {
double sum = 0.0;
for (Number num : list) {
sum += num.doubleValue();
}
return sum;
}
? extends T表示T或T的子类。这种形式适用于读取场景,因为你知道元素至少是T类型。
4.3 下界通配符
java
// 向列表中添加Integer或其父类对象
public void addNumbers(List<? super Integer> list) {
for (int i = 1; i <= 10; i++) {
list.add(i); // 可以添加Integer
}
// Object obj = list.get(0); // 可以获取,但类型是Object
}
? super T表示T或T的父类。这种形式适用于写入场景,因为你知道可以安全地添加T类型及其子类型。
4.4 PECS原则
PECS(Producer Extends, Consumer Super)是通配符使用的经典原则:
-
Producer Extends :如果参数化类型代表一个生产者 (提供数据),使用
<? extends T>。 -
Consumer Super :如果代表一个消费者 (消费数据),使用
<? super T>。
java
// 生产者:从集合中读取数据
public void copy(List<? extends Number> src, List<? super Number> dest) {
for (Number num : src) {
dest.add(num);
}
}
五、类型擦除(Type Erasure)
Java泛型是通过类型擦除实现的,这意味着泛型信息只在编译时存在,运行时会被移除。
5.1 什么是类型擦除?
编译器在编译时进行类型检查,然后将泛型类型替换为原始类型(Raw Type),并插入必要的强制转换。
例如:
java
List<String> list = new ArrayList<>();
list.add("hello");
String s = list.get(0);
编译后实际等价于:
java
List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0);
5.2 擦除的规则
-
无限定类型参数(如
<T>)被替换为Object。 -
有上界的类型参数(如
<T extends Number>)被替换为第一个边界类型(这里是Number)。 -
泛型方法也会被擦除,并可能生成桥方法(Bridge Method)以保持多态。
5.3 擦除带来的限制
由于擦除,泛型有一些使用限制:
1. 不能实例化类型参数
java
// 错误
T obj = new T();
// 错误
T[] array = new T[10];
2. 静态上下文中不能引用类型参数
java
public class Box<T> {
// 错误:静态成员不能使用类的类型参数
private static T value;
// 错误:静态方法不能使用类的类型参数
public static T getValue() { ... }
}
3. 不能创建泛型数组
java
// 编译错误
List<String>[] array = new List<String>[10];
// 可以这样绕过,但不安全
List<String>[] array = (List<String>[]) new List[10];
4. 不能使用instanceof判断泛型类型
java
// 错误
if (obj instanceof List<String>) { ... }
// 只能判断原始类型
if (obj instanceof List<?>) { ... }
六、泛型与继承
泛型类型与继承的关系需要特别注意:
-
List<String>不是List<Object>的子类型。 -
List<Integer>不是List<Number>的子类型,尽管Integer是Number的子类型。 -
数组是协变的(covariant),而泛型是不变的(invariant)。
java
// 数组协变:允许
Number[] numbers = new Integer[10];
// 泛型不变:不允许
List<Number> list = new ArrayList<Integer>(); // 编译错误
这正是通配符发挥作用的地方:
java
// 使用上界通配符实现类似协变
List<? extends Number> list = new ArrayList<Integer>();
七、泛型的最佳实践
7.1 优先使用泛型,避免原始类型
永远不要使用原始类型(如List而不是List<String>),这会失去类型安全性。
7.2 使用通配符增加API灵活性
在设计方法参数时,合理使用? extends和? super让API更通用。
7.3 优先考虑使用泛型方法而不是通配符作为返回值
java
// 好的做法:使用泛型方法
public static <T> T getFirst(List<T> list) { ... }
// 不推荐:通配符作为返回值,调用者需要强制转换
public static <?> getFirst(List<?> list) { ... }
7.4 类型参数命名要有意义
对于简单情况,单字母可以接受;但复杂场景下,使用有意义的名称(如KeyType,ValueType)提高可读性。
7.5 避免泛型数组
尽量使用集合代替数组,或者使用List来达到类似目的。
八、Java 7+的改进
-
菱形操作符:Java 7开始,实例化泛型类时可以省略类型参数:
javaList<String> list = new ArrayList<>(); // 菱形操作符 -
局部变量类型推断 :Java 10引入
var,可以简化局部变量声明:javavar list = new ArrayList<String>(); // 类型推断为ArrayList<String>
九、总结
Java泛型是一个强大且复杂的特性,它通过编译时类型检查提供了类型安全,并消除了强制转换的繁琐。理解泛型需要掌握:
-
泛型类、接口、方法的定义与使用
-
类型边界的约束
-
通配符的灵活运用及PECS原则
-
类型擦除的原理及其带来的限制