1. 概述
本文将深入探讨 Java 泛型中类型参数与通配符的核心区别及正确使用方式。很多开发者容易混淆这两者,尤其在编写泛型方法时,常纠结该用哪种形式。我们将通过实际场景分析,帮你彻底搞懂这个问题。
2. 泛型类
在 Java 5 引入泛型后,我们得以创建类型参数化的类和接口。定义泛型类时必须使用类型参数,例如 java.lang.Comparable
接口:
public interface Comparable<T> {
public int compareTo(T o);
}
这里 T
是类型参数,在整个接口范围内可用。实例化时需提供具体类型(如 String
)。⚠️ 注意:通配符不能用于定义泛型类或接口。
3. 泛型方法
3.1. 方法参数
编写泛型方法时,类型参数是基础。比如打印任意类型的方法:
public static <T> void print(T item) {
System.out.println(item);
}
❌ 不能直接用通配符定义参数类型,通配符只能作为泛型类型参数的一部分使用。再看两种 swap()
方法声明:
// 方式1:无界类型参数
public static <E> void swap(List<E> list, int src, int des);
// 方式2:无界通配符
public static void swap(List<?> list, int src, int des);
✅ 优先使用通配符版本:当类型参数在方法声明中只出现一次时,通配符能让代码更简洁灵活。这个规则同样适用于有界类型参数。
3.2. 返回类型
考虑一个合并列表的方法,使用通配符作为返回类型:
public static <E> List<? extends E> mergeWildcard(
List<? extends E> listOne,
List<? extends E> listTwo
) {
return Stream.concat(listOne.stream(), listTwo.stream())
.collect(Collectors.toList());
}
合并两个 List<Number>
时,问题出现了:
List<Number> numbers1 = new ArrayList<>();
numbers1.add(5);
numbers1.add(10L);
List<Number> numbers2 = new ArrayList<>();
numbers2.add(15f);
numbers2.add(20.0);
// 编译失败!
List<Number> numbersMerged = CollectionUtils.mergeWildcard(numbers1, numbers2);
❌ 通配符返回类型会强制客户端处理类型问题。正确做法是使用类型参数:
public static <E> List<E> mergeTypeParameter(
List<? extends E> listOne,
List<? extends E> listTwo
) {
return Stream.concat(listOne.stream(), listTwo.stream())
.collect(Collectors.toList());
}
这样就能正确接收 List<Number>
类型结果。
4. 类型边界
泛型边界能限制可用的类型,实现多态处理。主要有三种通配符边界:
- 无界通配符:
List<?>
→ 任意类型列表 - 上界通配符:
List<? extends Number>
→Number
或其子类型(如Integer
) - 下界通配符:
List<? super Integer>
→Integer
或其父类型(如Number
)
类型参数边界则有两种形式:
- 无界类型参数:
List<T>
→ 类型T
的列表 - 有界类型参数:
List<T extends Number & Comparable>
→ 实现Comparable
的Number
子类型
⚠️ 关键区别:
- 类型参数不支持下界
- 类型参数可设置多重边界,通配符不能
4.1. 上界类型
泛型类型是不可变的。虽然 Long
是 Number
的子类,但 List<Long>
不是 List<Number>
的子类。看个求和方法的踩坑案例:
// 错误实现:只能接受 List<Number>
public static long sum(List<Number> numbers) {
return numbers.stream().mapToLong(Number::longValue).sum();
}
List<Integer> integers = Arrays.asList(1, 2, 3);
// 编译失败!List<Integer> 不是 List<Number>
sum(integers);
✅ 解决方案:使用上界通配符
public static long sumWildcard(List<? extends Number> numbers) {
return numbers.stream().mapToLong(Number::longValue).sum();
}
// 现在可以接受任何 Number 子类型列表
List<Integer> integers = Arrays.asList(1, 2, 3);
sumWildcard(integers);
等价类型参数实现:
public static <T extends Number> long sumTypeParameter(List<T> numbers) {
return numbers.stream().mapToLong(Number::longValue).sum();
}
4.2. 下界类型
下界只能用于通配符(类型参数不支持)。使用 super
关键字指定最低层级类型。假设要向列表添加整数:
public static void addNumber(List<? super Integer> list, Integer number) {
list.add(number);
}
这样可接受 List<Integer>
、List<Number>
或 List<Object>
。✅ 遵循 PECS 原则:
- 生产者(Producer)用 extends:只读取元素时
- 消费者(Consumer)用 super:只添加元素时
- 既读又写?用无界类型
4.3. 无界类型
修改集合时使用通配符要小心。看 swap()
方法的经典踩坑:
// 编译失败!
public static void swap(List<?> list, int srcIndex, int destIndex) {
list.set(srcIndex, list.set(destIndex, list.get(srcIndex)));
}
❌ 原因:通配符 ?
表示未知类型,编译器无法保证 set()
操作的类型安全。解决方案是使用辅助方法捕获通配符类型:
private static <E> void swapHelper(List<E> list, int src, int des) {
list.set(src, list.set(des, list.get(src)));
}
public static void swap(List<?> list, int src, int des) {
swapHelper(list, src, des);
}
4.4. 多重边界
当需要多重约束时,类型参数是唯一选择。定义动物类层次:
abstract class Animal {
protected final String type;
protected final String name;
protected Animal(String type, String name) {
this.type = type;
this.name = name;
}
abstract String makeSound();
}
class Dog extends Animal {
public Dog(String type, String name) {
super(type, name);
}
@Override
public String makeSound() {
return "Wuf";
}
}
class Cat extends Animal implements Comparable<Cat> {
public Cat(String type, String name) {
super(type, name);
}
@Override
public String makeSound() {
return "Meow";
}
@Override
public int compareTo(Cat cat) {
return this.name.length() - cat.name.length();
}
}
需要排序且可比较的动物列表时:
public static <T extends Animal & Comparable<T>> void order(List<T> list) {
list.sort(Comparable::compareTo);
}
✅ 效果:List<Cat>
可通过编译,但 List<Dog>
会失败(未实现 Comparable
)。
5. 总结
- ✅ 优先通配符:编写通用库时,通配符能提供更大灵活性
- ✅ PECS 原则:生产者用
extends
,消费者用super
- ✅ 返回类型用类型参数:避免将类型问题抛给客户端
- ⚠️ 注意限制:通配符不支持多重边界,类型参数不支持下界
掌握这些差异后,你就能写出既类型安全又灵活优雅的泛型代码。完整示例代码可在 GitHub 获取。