java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > Java 泛型通配符

Java 泛型通配符 <? extends> vs <? super> 实战场景

作者:嗯嗯嗯吧

本文解析Java泛型通配符<? extends T>和<? super T>的核心区别与应用场景,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

一、引言

泛型通配符是 Java 泛型的核心特性,但其<? extends><? super>的用法常让开发者混淆 —— 用错会导致编译报错(如 “无法添加元素”“类型转换异常”),或隐藏逻辑隐患。实际开发中,集合传参、工具类设计、框架 API 调用都离不开这两个通配符。本文基于 Java 17+,通过 “原理拆解 + 实战场景” 讲清两者差异,帮你快速掌握 “什么时候用 extends,什么时候用 super”。

二、核心知识点解析

1. 泛型通配符基础概念

泛型通配符(Wildcard):用?表示未知的泛型类型,用于限制泛型的取值范围,增强代码灵活性(Java 5 引入,Java 17 + 优化了编译时类型检查精度)。

2. 核心原则:PECS 法则(Producer Extends, Consumer Super)

这是区分两者的关键法则,由 Java 官方推荐:

3. 读写权限差异(Java 17 + 编译检查)

通配符类型读取数据(取)写入数据(存)
<? extends T>可读取为 T 类型(安全)禁止写入(除 null 外),编译报错
<? super T>仅可读取为 Object 类型(不安全)可写入 T 或 T 的子类(安全)

三、实战案例

场景 1:基础场景 —— 集合读写操作对比(Java 17+)

示例 1:<? extends>读取数据(生产者场景)

// Java 17+支持
import java.util.ArrayList;
import java.util.List;
 
public class ExtendsDemo {
    // 统计数字集合的总和(仅读取,生产者)
    public static double sum(List<? extends Number> numberList) {
        double total = 0.0;
        for (Number num : numberList) {
            total += num.doubleValue(); // 读取为Number类型,安全
        }
        // numberList.add(10); // 编译报错:<? extends Number> 禁止添加元素
        return total;
    }
 
    public static void main(String[] args) {
        List<Integer> intList = new ArrayList<>();
        intList.add(10);
        intList.add(20);
        
        List<Double> doubleList = new ArrayList<>();
        doubleList.add(3.14);
        doubleList.add(6.28);
        
        // 支持Integer、Double等Number子类集合
        //http://fycj.tm66d.cn 
        System.out.println("整数列表总和:" + sum(intList)); // 输出30.0
        System.out.println("小数列表总和:" + sum(doubleList)); // 输出9.42
    }
 
}

示例 2:<? super>写入数据(消费者场景)

// Java 17+支持
import java.util.ArrayList;
import java.util.List;
 
public class SuperDemo {
    // 向集合中添加整数(仅写入,消费者)
    public static void addIntegers(List<? super Integer> integerList) {
        integerList.add(10); // 可添加Integer类型
        integerList.add(20); // 可添加Integer子类(此处无子类,直接加Integer)
        // integerList.add(3.14); // 编译报错:Double不是Integer的子类
        
        // 读取时仅能转为Object
        for (Object obj : integerList) {
            System.out.println("元素:" + obj);
        }
    }
 
    public static void main(String[] args) {
        List<Integer> intList = new ArrayList<>();
        List<Number> numberList = new ArrayList<>();
        List<Object> objList = new ArrayList<>();
        
        // 支持Integer、Number、Object等父类集合
        addIntegers(intList); // 输出:10、20
        addIntegers(numberList); // 输出:10、20
        addIntegers(objList); // 输出:10、20
    }
}

场景 2:进阶场景 —— 框架工具类设计(模拟 Collections.addAll)

Java 17 + 的Collections.addAll方法底层使用<? super T>,支持向父类集合添加子类元素,我们模拟其核心逻辑:

// Java 17+支持
import java.util.Collection;
import java.util.List;
 
public class CollectionUtils {
    /**
     * 批量添加元素到集合(消费者场景,用<? super T>)
     * @param dest 目标集合(接收元素)
     * @param elements 待添加元素(T或T的子类)
     */
    @SafeVarargs
    public static <T> boolean addAll(Collection<? super T> dest, T... elements) {
        boolean modified = false;
        for (T elem : elements) {
            modified |= dest.add(elem); // 写入安全,编译通过
        }
        return modified;
    }
 
    public static void main(String[] args) {
        List<Number> numberList = new ArrayList<>();
        Integer[] ints = {1, 2, 3};
        Double[] doubles = {4.0, 5.0};
        
        // 向Number集合添加Integer、Double(Number的子类)
        addAll(numberList, ints); 
        addAll(numberList, doubles);
        
        System.out.println(numberList); // 输出:[1, 2, 3, 4.0, 5.0]
    }
}

场景 3:综合场景 —— 泛型方法的读写结合

// Java 17+支持
import java.util.List;
 
public class GenericCombinationDemo {
    /**
     * 从源集合读取数据(extends),写入目标集合(super)
     * @param source 源集合(生产者)
     * @param dest 目标集合(消费者)
     */
    public static <T> void copy(List<? extends T> source, List<? super T> dest) {
        for (T elem : source) {
            dest.add(elem); // 源集合取T类型,目标集合存T类型,双向安全
        }
    }
 
    public static void main(String[] args) {
        List<Integer> intSource = List.of(1, 2, 3); // Java 9+ List.of(),Java 17+兼容
        List<Number> numberDest = new ArrayList<>();
        List<Object> objDest = new ArrayList<>();
        
        // 从Integer集合复制到Number、Object集合
        copy(intSource, numberDest);
        copy(intSource, objDest);
        
        System.out.println(numberDest); // 输出:[1, 2, 3]
        System.out.println(objDest); // 输出:[1, 2, 3]
    }
}

四、易错点与避坑指南

易错点 1:用<? extends T>尝试添加元素

错误代码

List<? extends Number> list = new ArrayList<Integer>();
list.add(10); // 编译报错:The method add(capture#1-of ? extends Number) is undefined for the type List<capture#1-of ? extends Number>

正确代码

List<Number> list = new ArrayList<Integer>(); // 直接用具体类型接收,或用<? super T>
list.add(10);

原因分析<? extends Number> 可能是List<Integer>List<Double>,若允许添加Integer,当集合实际是List<Double>时会导致类型不匹配,Java 17 + 编译时直接禁止该操作(除list.add(null)外,null 是所有类型的实例)。

易错点 2:用<? super T>读取数据并强转

错误代码

List<? super Integer> list = new ArrayList<Number>();
list.add(10);
Integer num = (Integer) list.get(0); // 编译警告,运行时可能报错

正确代码

List<? super Integer> list = new ArrayList<Number>();
list.add(10);
Object obj = list.get(0); // 仅能安全转为Object
if (obj instanceof Integer) {
    Integer num = (Integer) obj; // 加类型判断,避免强转异常
}

原因分析<? super Integer> 可能是List<Number>List<Object>get(0)返回的是Object类型,直接强转Integer可能因集合中存在其他类型元素(如Double)导致ClassCastException

易错点 3:过度使用通配符,降低代码可读性

错误代码

// 无需通配符,直接用具体泛型即可
public static void process(List<? extends Object> list) {
    // 逻辑...
}

正确代码

public static void process(List<Object> list) {
    // 逻辑...
}

原因分析<? extends Object> 等价于?,但语义不清晰,且无法添加任何非 null 元素;若业务允许所有类型,直接用List<Object>更直观,且支持添加任意元素。

易错点 4:泛型数组与通配符混用错误

错误代码

List<? extends Number>[] arr = new List<Integer>[10]; 
// 编译报错:Generic array creation 

正确代码

List<? extends Number>[] arr = new List[10]; // 用原始类型数组(不推荐)
// 或优先用集合替代数组:
List<List<? extends Number>> list = new ArrayList<>();

原因分析:Java 不允许创建泛型数组(如List<Integer>[]),通配符泛型数组也不例外,避免数组协变导致的类型安全问题(Java 17 + 仍严格限制该语法)。

易错点 5:方法返回值使用通配符

错误代码

// 返回值用通配符,调用方需额外处理类型,体验差
public static List<? extends Number> createList() {
    return new ArrayList<Integer>();
}

正确代码

// 直接返回具体泛型类型,或用泛型方法
public static List<Integer> createList() {
    return new ArrayList<Integer>();
}
// 或支持多种类型:
public static <T extends Number> List<T> createList(Class<T> clazz) {
    return new ArrayList<>();
}

原因分析:返回值用通配符会增加调用方的使用成本(需强制类型转换或类型判断),且无法充分利用泛型的类型安全特性,除非确需返回多种不确定类型,否则优先用具体泛型或泛型方法。

五、总结与扩展

本文核心是掌握 PECS 法则:生产者用<? extends T>(读数据),消费者用<? super T>(写数据),读写结合时两者搭配使用。Java 17 + 对泛型通配符的编译检查更严格,能提前规避多数类型错误。扩展方向:1. 学习泛型上限(<T extends Number>)与上界通配符的区别;2. 研究 Java 21 新特性 “泛型模式匹配” 对通配符的优化;3. 分析 Spring、MyBatis 等框架中通配符的应用场景(如ParameterizedType处理)。

面试高频提问

  1. 泛型通配符<? extends><? super>的核心区别是什么?PECS 法则的含义?
  2. 为什么<? extends T>不能添加元素,而<? super T>可以?
  3. 方法参数用List<T>List<? extends T>有什么区别?
  4. 如何安全地从<? super T>集合中读取T类型元素?

到此这篇关于Java 泛型通配符 <? extends> vs <? super> 实战场景的文章就介绍到这了,更多相关Java 泛型通配符 内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文