Java 通配符详解:?、? extends、? super 一篇搞懂

 更新时间:2025年10月19日 09:36:31   作者:梵得儿SHI  
本文深入解析Java泛型中的通配符(Wildcard)机制,重点讲解无界通配符(?)、上界通配符(? extends T)和下界通配符(? super T)的用法与区别,感兴趣的可以了解一下

在 Java 泛型中,通配符(Wildcard)是解决 “泛型类型不确定” 问题的关键。你可能见过List<?>List<? extends Number>这样的写法,它们就是通配符的典型应用。通配符看似简单,却藏着不少细节:什么时候用?? extends? super有啥区别?为什么有的场景能存元素,有的只能读?今天我们就从泛型的 “不变性” 讲起,彻底搞懂无界通配符、上界通配符和下界通配符的用法、场景和底层逻辑,配上直观图解,让你一次掌握!

一、为什么需要通配符?—— 从泛型的 “不变性” 说起

泛型有个重要特性:不变性(Invariance)。简单说就是:如果BA的子类,List<B>不是List<A>的子类。这个特性会导致一些反直觉的问题,而通配符正是为了解决这些问题而生。

泛型不变性的 “坑”

假设我们有类继承关系:Dog extends Animal,现在看下面的代码:

List<Dog> dogs = new ArrayList<>();
// 编译报错:List<Dog>不能赋值给List<Animal>
List<Animal> animals = dogs; 

为什么会报错?因为如果允许这种赋值,会导致类型安全问题:

// 假设上面的赋值合法
animals.add(new Cat());  // 往本应存Dog的列表里加了Cat
Dog dog = dogs.get(0);   // 运行时会抛ClassCastException(Cat不能转Dog)

泛型的不变性正是为了避免这种安全问题 ——编译时就阻断错误,而不是等到运行时。但这也带来了新问题:如何编写一个能处理 “任意 Animal 子类集合” 的通用方法?比如一个打印所有动物的方法,既想接收List<Dog>,也想接收List<Cat>。这时,通配符就派上用场了。

通配符的核心作用

通配符(?)表示 “未知类型”,它的出现打破了泛型的严格不变性,允许在一定范围内 “灵活匹配” 泛型类型,同时又不破坏类型安全。

泛型不变性与通配符作用图解

二、无界通配符:?(表示 “任意类型”)

无界通配符?表示 “未知的任意类型”,可以理解为? extends Object的简写。它的核心场景是:只需要使用 Object 类的通用方法,不需要关注具体类型

1. 无界通配符的用法

当方法参数需要接收 “任意泛型类型”,且方法内部只读取元素(不修改),或只调用Object类的方法(如toString()equals())时,用?

示例:打印任意集合的元素

// 无界通配符:可以接收List<String>、List<Integer>、List<Dog>等任意List
public static void printList(List<?> list) {
    for (Object obj : list) {  // 只能用Object接收元素
        System.out.println(obj);  // 调用Object的toString()
    }
}

// 调用示例
List<String> strList = Arrays.asList("a", "b");
List<Integer> intList = Arrays.asList(1, 2);
printList(strList);  // 合法
printList(intList);  // 合法

2. 无界通配符的限制:不能添加元素(除了 null)

因为?表示 “未知类型”,编译器无法确定集合能接收什么类型的元素,所以不能添加非 null 元素(null 是所有类型的默认值,例外)。

List<?> list = new ArrayList<String>();
list.add(null);  // 合法(null是任意类型的实例)
// list.add("hello");  // 编译报错:无法确定list是否能接收String

3. 无界通配符 vs 泛型方法

可能有人会问:用泛型方法public static <T> void printList(List<T> list)不也能实现同样的功能吗?

两者的区别在于:

  • 泛型方法的T是 “确定的未知类型”(调用时编译器会推断具体类型),适合需要在方法内部使用类型一致的场景(如返回T类型结果);
  • 无界通配符?是 “完全未知的类型”,适合只需要Object方法的场景,代码更简洁。

无界通配符图解

三、上界通配符:? extends T(表示 “T 或 T 的子类”)

上界通配符? extends T表示 “未知类型,但其必须是 T 或 T 的子类”。核心场景是:需要读取元素,且元素类型是 T 的子类型(即 “Producer”—— 生产者,只产出 T 类型的元素)。

1. 上界通配符的用法

当方法需要从集合中读取元素,且希望元素是某个类型的子类型(如从 “数字集合” 中读取数字,不管是 Integer 还是 Double),用? extends T

示例:获取集合中的最大值(数字集合)

// 上界通配符:接收Number或其子类(Integer、Double等)的集合
public static double getMax(List<? extends Number> numbers) {
    double max = Double.MIN_VALUE;
    for (Number num : numbers) {  // 安全读取为Number类型
        if (num.doubleValue() > max) {
            max = num.doubleValue();
        }
    }
    return max;
}

// 调用示例
List<Integer> ints = Arrays.asList(1, 3, 2);
List<Double> doubles = Arrays.asList(1.5, 3.8, 2.2);
System.out.println(getMax(ints));   // 3.0
System.out.println(getMax(doubles));// 3.8

2. 上界通配符的限制:不能添加元素(除了 null)

和无界通配符类似,? extends T的具体类型未知(可能是 T 的任意子类),编译器无法确定集合能接收什么类型的元素,因此不能添加非 null 元素

List<? extends Number> nums = new ArrayList<Integer>();
nums.add(null);  // 合法
// nums.add(1);    // 编译报错:无法确定nums是否能接收Integer(可能是List<Double>)
// nums.add(3.14); // 编译报错:同理,可能是List<Integer>

3. 为什么上界通配符适合 “读取”?

        因为不管具体类型是 T 的哪个子类,都可以安全地向上转型为 T。例如List<? extends Number>中的元素,无论实际是 Integer 还是 Double,都能被Number类型接收,因此读取操作是安全的。

上界通配符图解

四、下界通配符:? super T(表示 “T 或 T 的父类”)

下界通配符? super T表示 “未知类型,但其必须是 T 或 T 的父类”。核心场景是:需要向集合中添加元素,且元素类型是 T 的子类型(即 “Consumer”—— 消费者,只接收 T 类型的元素)。

1. 下界通配符的用法

当方法需要向集合中添加元素,且希望元素是某个类型的子类型(如向 “动物集合” 中添加狗或猫,因为它们都是动物),用? super T

示例:向集合中添加元素(动物集合)

// 下界通配符:接收Animal或其父类(如Object)的集合
public static void addDogs(List<? super Animal> animals) {
    animals.add(new Dog());  // 安全添加Dog(Animal的子类)
    animals.add(new Cat());  // 安全添加Cat(Animal的子类)
}

// 调用示例
List<Animal> animalList = new ArrayList<>();
List<Object> objectList = new ArrayList<>();
addDogs(animalList);  // 合法
addDogs(objectList);  // 合法

2. 下界通配符的限制:读取元素只能作为 Object

因为? super T的具体类型是 T 的父类(可能是 T、T 的父类、Object),编译器无法确定具体是哪个父类,所以读取元素时只能用 Object 接收

List<? super Animal> animals = new ArrayList<Object>();
animals.add(new Dog());  // 合法
Object obj = animals.get(0);  // 只能用Object接收
// Animal animal = animals.get(0);  // 编译报错:可能是List<Object>,不能确定是Animal

3. 为什么下界通配符适合 “写入”?

因为 T 的子类可以安全地向上转型为 T 的父类。例如List<? super Animal>可以接收 Animal 的子类(Dog、Cat),因为它们都能被转型为 Animal(或其父类),因此写入操作是安全的。

下界通配符图解

五、通配符的经典原则:PECS

面对三种通配符,很多人会混淆什么时候用哪个。记住这个经典原则:PECS(Producer Extends, Consumer Super)—— 生产者用 extends,消费者用 super。

  • Producer(生产者):如果你需要从集合中读取元素(集合产出元素给你),用? extends T。例:从 “数字集合” 中读取数字计算总和,集合是生产者。

  • Consumer(消费者):如果你需要向集合中写入元素(你给集合传入元素),用? super T。例:向 “动物集合” 中添加狗,集合是消费者。

  • 既读又写:不要用通配符,直接用具体的泛型类型(如List<T>)。

PECS 原则图解

六、常见误区与避坑指南

  1. 通配符不能用于泛型类 / 接口的定义通配符只能用于方法参数、变量声明,不能在定义泛型类或接口时使用:

    // 错误:泛型类定义不能用通配符
    class MyClass<?> { ... }
    
  2. List<?> 与 List<Object> 不同

    • List<Object> 可以添加任意类型的元素(因为 Object 是所有类的父类);
    • List<?> 不能添加非 null 元素(因为类型完全未知)。
  3. 上界和下界的嵌套使用复杂场景可能需要嵌套通配符,例如Map<? extends K, ? super V>,遵循 PECS 原则即可:键是生产者(用 extends),值是消费者(用 super)。

  4. 通配符与泛型方法的选择

    • 若方法需要返回与输入同类型的元素,用泛型方法(如public <T> T getFirst(List<T> list));
    • 若方法只需要读取或写入,且不关心具体类型,用通配符更简洁。

七、总结

通配符是 Java 泛型中提升灵活性的关键,核心要点:

通配符类型语法含义适用场景操作限制
无界通配符?任意类型只读取,用 Object 方法可添加 null,读取用 Object
上界通配符? extends TT 或 T 的子类读取元素(生产者)可添加 null,读取用 T
下界通配符? super TT 或 T 的父类写入元素(消费者)可添加 T 及其子类,读取用 Object

记住 PECS 原则:“Producer Extends, Consumer Super”,遇到泛型集合操作时,先判断是 “读” 还是 “写”,再选择对应的通配符。

通配符的设计体现了 Java 泛型 “安全与灵活平衡” 的思想 —— 既打破了严格的不变性,又通过编译时检查保证类型安全。熟练掌握通配符,能让你在处理集合、泛型组件时写出更简洁、更通用的代码!

到此这篇关于Java 通配符详解:?、? extends、? super 一篇搞懂的文章就介绍到这了,更多相关Java 通配符 内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 将Java对象序列化成JSON和XML格式的实例

    将Java对象序列化成JSON和XML格式的实例

    下面小编就为大家分享一篇将Java对象序列化成JSON和XML格式的实例,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2017-12-12
  • Java C++ leetcode执行一次字符串交换能否使两个字符串相等

    Java C++ leetcode执行一次字符串交换能否使两个字符串相等

    这篇文章主要为大家介绍了Java C++ leetcode1790执行一次字符串交换能否使两个字符串相等,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-10-10
  • SpringBoot热重启配置详解

    SpringBoot热重启配置详解

    在本篇文章里小编给大家分享的是关于SpringBoot热重启配置知识点内容,需要的朋友们可以学习下。
    2020-02-02
  • spring中的@MapperScan注解属性解析

    spring中的@MapperScan注解属性解析

    @MapperScan是Spring集成MyBatis时自动扫描Mapper接口的注解,简化配置并支持多数据源,通过属性控制扫描路径和过滤条件,利用动态代理生成Bean,需注意路径匹配及注解优先级,优化项目配置与性能,本文给大家介绍spring中的@MapperScan注解,感兴趣的朋友一起看看吧
    2025-07-07
  • mybatis多个plugins的执行顺序解析

    mybatis多个plugins的执行顺序解析

    这篇文章主要介绍了mybatis多个plugins的执行顺序解析,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-09-09
  • java8中forkjoin和optional框架使用

    java8中forkjoin和optional框架使用

    这篇文章主要介绍了java8中forkjoin和optional框架使用心得以及用法讲解,需要的朋友参考下吧。
    2017-12-12
  • 基于spring boot 的配置参考大全(推荐)

    基于spring boot 的配置参考大全(推荐)

    下面小编就为大家带来一篇基于spring boot 的配置参考大全(推荐)。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-09-09
  • Spring Cloud 2020.0.0正式发布再见了Netflix

    Spring Cloud 2020.0.0正式发布再见了Netflix

    这篇文章主要介绍了Spring Cloud 2020.0.0正式发布再见了Netflix,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-12-12
  • mybatis 传入null值的解决方案

    mybatis 传入null值的解决方案

    这篇文章主要介绍了mybatis 传入null值的解决方案,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-03-03
  • JDK动态代理接口和接口实现类深入详解

    JDK动态代理接口和接口实现类深入详解

    这篇文章主要介绍了JDK动态代理接口和接口实现类,JDK动态代理是代理模式的一种实现方式,因为它是基于接口来做代理的,所以也常被称为接口代理,文中通过实例代码介绍的非常详细,需要的朋友可以参考下
    2022-06-06

最新评论