泛型的使用示范-2:通配符 extends 与 super 详解
1. 引言Java 泛型中的通配符?常常让初学者感到困惑尤其是? extends T上限通配符和? super T下限通配符的区别。本文通过一个经典的“磁盘”类案例并结合通俗易懂的对比示例帮助你彻底理清它们的适用场景与 PECS 原则。2. 原始案例回顾下面这段代码定义了一个泛型容器DiskT并通过show方法演示了下限通配符? super String的用法// ? extends 类型用于限制泛型的上限可以用于声明处也可以用于参数定义处// ? super 类型用于限定泛型的下限只能用于参数定义处classDiskTextendsObject{// extends 限定类型的上限privateTdata;publicDisk(Tdata){this.datadata;}publicvoidsetData(Tdata){this.datadata;}publicTgetData(){returndata;}}publicclassTest{publicstaticvoidshow(Disk?superStringd){// super 限定泛型的下限System.out.println(d.getData());}publicstaticvoidmain(String[]args){DiskStringd1newDiskString(测试);show(d1);// 只能接受 String 类型或者 String 的父类 Object 类型}}运行结果测试代码要点DiskT extends Object表示T必须是Object或其子类这实际上是所有引用类型的默认上限T extends Object可简写为T。show(Disk? super String d)使用了下限通配符参数d的泛型类型可以是String或String的任意父类如Object因此main方法中传入DiskString是合法的。3. 通配符深度解析3.1 上限通配符? extends T含义类型参数必须是T或T的子类。特点通常用于读取数据生产者。你可以安全地从泛型对象中获取数据但不能写入数据除了null因为无法确定具体的子类型。// 读取示例求列表中所有数字的和publicstaticdoublesum(List?extendsNumberlist){doublesum0.0;for(Numbernum:list){sumnum.doubleValue();// 安全Number 及其子类都有 doubleValue()}// list.add(1); // 编译错误不能向 ? extends Number 添加元素returnsum;}3.2 下限通配符? super T含义类型参数必须是T或T的父类。特点通常用于写入数据消费者。你可以安全地向泛型对象中添加T或其子类的实例但读取时只能以Object类型取出。// 写入示例向集合中添加整数publicstaticvoidaddNumbers(List?superIntegerlist){list.add(100);list.add(200);// Integer i list.get(0); // 编译错误只能读为 ObjectObjectobjlist.get(0);// 安全保证是 Object 子类}3.3 PECS 原则记忆技巧PECSProducer Extends如果只想从结构中读取数据用? extends T。Consumer Super如果只想向结构中写入数据用? super T。如果既读又写则不要使用通配符直接使用具体类型如T。下面的流程图帮你快速记住 PECS 决策路径读取生产数据写入消费数据既读又写需要使用通配符参数主要操作是读取还是写入使用 ? extends T使用 ? super T不要使用通配符直接用 T安全取出 T 类型数据不能写入安全写入 T 及其子类取出为 Object4. 完整案例水果盘为了更直观地理解上限与下限我们设计一个“水果盘”案例importjava.util.ArrayList;importjava.util.List;classFruit{}classAppleextendsFruit{}classRedAppleextendsApple{}publicclassFruitPlate{// 上限通配符从盘中拿水果吃只读publicstaticvoideatFromPlate(List?extendsFruitplate){for(Fruitf:plate){System.out.println(吃掉了: f.getClass().getSimpleName());}// plate.add(new Apple()); // 编译错误不能写入}// 下限通配符向盘中放苹果只写publicstaticvoidputAppleIntoPlate(List?superAppleplate){plate.add(newApple());plate.add(newRedApple());// Fruit f plate.get(0); // 编译错误只能得到 Object}publicstaticvoidmain(String[]args){ListFruitfruitPlatenewArrayList();ListAppleapplePlatenewArrayList();ListRedAppleredApplePlatenewArrayList();// 生产者 extends可以传入 Fruit 及其任意子类列表eatFromPlate(fruitPlate);eatFromPlate(applePlate);eatFromPlate(redApplePlate);// 消费者 super可以传入 Apple 及其任意父类列表putAppleIntoPlate(fruitPlate);// 合法Fruit 是 Apple 的父类putAppleIntoPlate(applePlate);// 合法Apple 自身// putAppleIntoPlate(redApplePlate); // 非法RedApple 是 Apple 的子类不是父类}}下图展示了Fruit、Apple、RedApple的继承关系以及eatFromPlate和putAppleIntoPlate各自兼容的参数类型FruitAppleRedAppleeatFromPlate(? extends Fruit)接受ListFruit接受ListApple接受ListRedApple操作只能读取 FruitputAppleIntoPlate(? super Apple)接受ListFruit接受ListApple不接受ListRedApple操作写入 Apple / RedApple解析eatFromPlate(List? extends Fruit plate)定义了一个“上限”参数你可以传入ListFruit、ListApple或ListRedApple方法内只能从中读取Fruit或其子类。putAppleIntoPlate(List? super Apple plate)定义了一个“下限”参数你可以传入ListApple、ListFruit等方法内可以安全地向其中添加Apple及其子类因为容器至少能装下Apple。5. 小结? extends T采用协变covariant适合从泛型结构中安全地取数据。? super T采用逆变contravariant适合向泛型结构中安全地存数据。记忆 PECS 原则可以让你在定义方法参数时快速选择正确的通配符。希望这篇文章能帮你彻底理清泛型通配符的用法如果仍有疑问欢迎在评论区留言交流。