一、痛点:泛型容器的类型僵化
Java泛型默认是不变(invariant) 的。这意味着List<Integer>并非List<Number>的子类型:
java
List<Integer> intList = new ArrayList<>();
List<Number> numList = intList; // 编译错误!类型不兼容
即使Integer是Number子类,其容器也无法直接赋值给父类容器引用,限制了代码复用。
二、协变救星:<? extends T>的核心机制
<? extends T>声明了泛型的上界(upper bound) ,允许接受T及其子类型:
java
List<? extends Number> numList = new ArrayList<Integer>(); // 合法!
此时numList成为“某种Number子类的列表”,实现了协变(covariance)。
三、安全边界:生产者(Producer)的读写限制
✅ 允许读取(安全)
java
Number num = numList.get(0); // 安全:元素必为Number子类
Integer i = (Integer) numList.get(0); // 需显式向下转型(风险自控)
❌ 禁止写入(除null)
java
numList.add(new Integer(10)); // 编译错误!
numList.add(new Double(1.0)); // 同样错误!
numList.add(null); // 唯一允许的写入操作
原因:编译器无法确定numList实际类型(可能是List<Double>),写入操作可能导致类型污染。
四、实战示例:灵活处理异构数值集合
java
// 计算任意Number子类列表的总和
public static double sumList(List<? extends Number> list) {
double sum = 0.0;
for (Number num : list) { // 安全读取
sum += num.doubleValue();
}
return sum;
}
// 使用示例
List<Integer> integers = Arrays.asList(1, 2, 3);
List<Double> doubles = Arrays.asList(1.5, 2.5);
System.out.println(sumList(integers)); // 输出6.0
System.out.println(sumList(doubles)); // 输出4.0
此方法可处理Integer、Double等所有Number子类的列表,无需重载。
五、设计哲学:PECS原则中的生产者(Producer)
在PECS(Producer-Extends, Consumer-Super)原则中:
- Producer:数据来源(如上述
sumList参数),使用<? extends T> - Consumer:数据消费(如集合添加操作),需用
<? super T>
六、Java API中的经典应用
java.util.Collections的copy方法巧妙运用两者:
java
public static <T> void copy(
List<? super T> dest, // 消费者:可写入T及其父类
List<? extends T> src // 生产者:可读取T及其子类
) {
for (int i = 0; i < src.size(); i++) {
dest.set(i, src.get(i)); // 安全:src元素必是dest可接受的类型
}
}
结语:解锁灵活性的双刃剑
extends通配符通过协变打破泛型容器继承链的隔阂,赋予API强大灵活性,代价是写入操作的严格限制。掌握其“生产者”定位与PECS原则,方能精准驾驭泛型系统,在安全与抽象之间找到最佳平衡点。
1899

被折叠的 条评论
为什么被折叠?



