《理解 Java 泛型中的通配符:extends 与 super 的使用场景》

news2025/4/22 22:29:23

大家好呀!👋 今天我们要聊一个让很多Java初学者头疼的话题——泛型通配符。别担心,我会用最通俗易懂的方式,带你彻底搞懂这个看似复杂的概念。准备好了吗?Let’s go! 🚀

一、为什么我们需要泛型通配符?🤔

首先,让我们回忆一下泛型的基本概念。泛型就像是一个"类型参数",它让我们可以写出更通用的代码。比如:

List stringList = new ArrayList<>();
List intList = new ArrayList<>();

但是,当我们想要写一个方法,可以处理不同类型的List时,问题就来了。比如,我想写一个打印所有List元素的方法:

public void printList(List list) {
    for (Object elem : list) {
        System.out.println(elem);
    }
}

这个方法看起来不错,但实际上它不能处理ListList!😱 因为List并不是List的子类型(虽然String是Object的子类)。

这就是通配符要解决的问题!它让我们可以更灵活地处理不同类型的泛型集合。🎯

二、通配符基础:问号(?)的魔力 ✨

通配符就是一个简单的问号?,它表示"未知类型"。我们可以这样改写上面的方法:

public void printList(List list) {
    for (Object elem : list) {
        System.out.println(elem);
);
}

现在这个方法可以接受任何类型的List了!🎉 因为List表示"某种类型的List,但我不知道具体是什么类型"。

但是,通配符真正的威力在于它可以与extendssuper结合使用,这就是我们今天要深入探讨的重点!🔍

三、上界通配符: 📈

3.1 基本概念

``表示"T或者T的某个子类型"。这被称为"上界通配符"(Upper Bounded Wildcard),因为它限定了类型的上界。

举个生活中的例子🌰:想象你有一个动物园,里面有各种动物。List可以表示"一个包含某种动物(可能是狗、猫、鸟等)的列表"。

3.2 代码示例

class Animal {}
class Dog extends Animal {}
class Cat extends Animal {}

public void processAnimals(List animals) {
    for (Animal animal : animals) {
        System.out.println("处理动物: " + animal);
    }
}

List dogs = new ArrayList<>();
dogs.add(new Dog());
processAnimals(dogs);  // 可以正常工作!

List cats = new ArrayList<>();
cats.add(new Cat());
processAnimals(cats);  // 也可以工作!

3.3 能做什么和不能做什么

可以做的事情

  1. 从集合中读取元素(作为Animal类型)
  2. 调用Animal类的方法

不能做的事情

  1. 向集合中添加元素(除了null)

    animals.add(new Dog());  // 编译错误!
    animals.add(null);       // 这是唯一允许的添加
    

    为什么?因为编译器不知道实际的类型参数是什么。可能是List,也可能是List,所以为了类型安全,不允许添加。

3.4 实际应用场景

这种通配符特别适合"生产者"场景——即你主要从集合中读取数据。比如:

  1. 计算集合中所有数字的总和:

    public double sumOfList(List list) {
        double sum = 0.0;
        for (Number num : list) {
            sum += num.doubleValue();
        }
        return sum;
    }
    
  2. 在图形应用中处理各种形状:

    void drawAll(List shapes) {
        for (Shape shape : shapes) {
            shape.draw();
        }
    }
    

四、下界通配符: 📉

4.1 基本概念

``表示"T或者T的某个父类型"。这被称为"下界通配符"(Lower Bounded Wildcard),因为它限定了类型的下界。

继续动物园的例子🦁:List可以表示"一个可以存放Dog及其子类的列表",比如ListList

4.2 代码示例

public void addDogsToList(List list) {
    list.add(new Dog());
    // 也可以添加Dog的子类
    list.add(new Puppy());  // 假设Puppy extends Dog
}

List animals = new ArrayList<>();
addDogsToList(animals);  // 可以工作

List dogs = new ArrayList<>();
addDogsToList(dogs);     // 也可以工作

List objects = new ArrayList<>();
addDogsToList(objects);  // 同样可以!

4.3 能做什么和不能做什么

可以做的事情

  1. 向集合中添加T或T的子类元素
  2. 作为参数传递(消费场景)

不能做的事情

  1. 安全地从集合中读取元素(除了作为Object)

    Dog dog = list.get(0);  // 编译错误!
    Object obj = list.get(0);  // 这是可以的
    

    为什么?因为列表可能是List,而你不能保证取出的就是Dog。

4.4 实际应用场景

这种通配符特别适合"消费者"场景——即你主要向集合中添加数据。比如:

  1. 将多个元素添加到集合中:

    public void addNumbers(List list) {
        for (int i = 1; i <= 10; i++) {
            list.add(i);
        }
    }
    
  2. 在GUI应用中添加各种组件:

    void addButtons(List components) {
        components.add(new Button("OK"));
        components.add(new Button("Cancel"));
    }
    

五、PECS原则:生产者用extends,消费者用super �

现在你可能会问:“我什么时候该用extends,什么时候该用super呢?” 🤔

答案就是记住这个简单的口诀:PECS(Producer-Extends, Consumer-Super)

  • Producer(生产者):如果你需要一个数据结构提供(生产)元素给你使用,用extends
  • Consumer(消费者):如果你需要一个数据结构接受(消费)你提供的元素,用super

5.1 PECS示例

假设我们有一个拷贝方法,从一个列表(src)拷贝到另一个列表(dest):

public static  void copy(List dest, List src) {
    for (T item : src) {
        dest.add(item);
    }
}

这里:

  • src是生产者(我们从中读取数据),所以用extends
  • dest是消费者(我们向其中写入数据),所以用super

5.2 为什么PECS有效?

这个原则之所以有效,是因为:

  1. 对于生产者(extends):

    • 你只能从中读取,不能写入(除了null)
    • 读取的元素至少是某种特定类型(上界)
  2. 对于消费者(super):

    • 你可以写入特定类型或其子类
    • 只能以Object形式读取元素

六、无界通配符: 🌌

有时候,你只关心泛型类型本身,而不关心它的类型参数。这时可以使用无界通配符``。

6.1 基本用法

public void printListSize(List list) {
    System.out.println("列表大小: " + list.size());
}

这个方法可以接受任何类型的List,但你只能调用不依赖类型参数的方法(如size(), clear()等)。

6.2 与原生类型的区别

注意List和原生类型List是不同的:

  • List:这是一个知道自己是泛型但不知道具体类型的列表,是类型安全的
  • List:这是Java 5之前的原始类型,完全不知道泛型,不安全

6.3 实际应用

无界通配符常用于:

  1. 当方法实现只需要Object类提供的功能时
  2. 当类型参数不重要或不可知时
  3. 作为泛型类中非泛型方法的参数类型

七、通配符在方法签名中的应用 🎯

通配符不仅可以用在变量声明中,还可以用在方法签名中,使API更加灵活。

7.1 方法参数中的通配符

// 更灵活的API设计
public void process(List numbers) { ... }

// 比下面这种限制更少
public  void process(List numbers) { ... }

7.2 返回类型中的通配符

通常不建议在返回类型中使用通配符,因为这会给方法调用者带来不便。例如:

// 不推荐
public List getNumbers() { ... }

// 调用者使用起来不方便
List numbers = getNumbers();
Number num = numbers.get(0);  // 可以
Integer i = numbers.get(0);   // 编译错误

八、通配符捕获与辅助方法 🕵️‍♂️

有时候我们需要"捕获"通配符的具体类型,这时可以使用辅助方法。

8.1 通配符捕获问题

public void swap(List list, int i, int j) {
    Object temp = list.get(i);
    list.set(i, list.get(j));  // 编译错误!
    list.set(j, temp);         // 编译错误!
}

为什么出错?因为编译器不知道?具体是什么类型,无法保证类型安全。

8.2 使用辅助方法解决

private static  void swapHelper(List list, int i, int j) {
    E temp = list.get(i);
    list.set(i, list.get(j));
    list.set(j, temp);
}

public void swap(List list, int i, int j) {
    swapHelper(list, i, j);  // 这里发生了通配符捕获
}

编译器可以推断出辅助方法中的E就是通配符?的具体类型。

九、通配符与类型参数的区别 🤼

有时候看起来很相似,但它们有重要区别:

特性类型参数 ``通配符 ``
可命名是 (T)
多处使用相同类型
灵活性较低较高
适用场景需要引用类型参数只需要一次使用

9.1 何时使用哪种

  • 当需要在方法中多次引用同一类型时,使用类型参数
  • 当只需要一次使用且不需要知道具体类型时,使用通配符

十、高级话题:通配符嵌套与复杂场景 🧩

通配符可以嵌套使用,处理更复杂的场景。

10.1 嵌套通配符示例

// 一个映射,其键是某种类型的列表
Map> complexMap = new HashMap<>();

// 一个列表,包含各种类型的列表
List> listOfLists = new ArrayList<>();

10.2 通配符与泛型方法的结合

public static  void copyWithFilter(
    List dest, 
    List src, 
    Predicate filter) {
    
    for (T elem : src) {
        if (filter.test(elem)) {
            dest.add(elem);
        }
    }
}

十一、常见误区与陷阱 🚧

11.1 误区1:认为ListList相同

错!List明确知道元素是Object类型,可以安全添加Object。而List表示"不知道是什么类型",只能添加null。

11.2 误区2:过度使用通配符

不是所有地方都需要通配符。如果类型信息重要,使用具体类型参数可能更好。

11.3 误区3:忽略编译器警告

当使用通配符时,如果看到编译器警告,一定要理解原因,不要简单地忽略或压制它们。

十二、实战演练:集合工具类 🛠️

让我们实现一个简单的集合工具类,应用所学的通配符知识。

public class CollectionUtils {
    // 合并两个列表到目标列表
    public static  void merge(
        List dest,
        List src1, 
        List src2) {
        
        dest.addAll(src1);
        dest.addAll(src2);
    }
    
    // 找出最大值
    public static > T max(List list) {
        if (list.isEmpty()) throw new NoSuchElementException();
        T max = list.get(0);
        for (T elem : list) {
            if (elem.compareTo(max) > 0) {
                max = elem;
            }
        }
        return max;
    }
    
    // 过滤列表
    public static  List filter(
        List list, 
        Predicate predicate) {
        
        List result = new ArrayList<>();
        for (T elem : list) {
            if (predicate.test(elem)) {
                result.add(elem);
            }
        }
        return result;
    }
}

十三、总结与最佳实践 🏆

13.1 关键点回顾

  1. ``:用于从结构中读取(生产者),不能写入(除了null)
  2. ``:用于向结构中写入(消费者),只能以Object读取
  3. ``:当类型完全无关紧要时使用
  4. 记住PECS原则:Producer-Extends, Consumer-Super

13.2 最佳实践

  1. 优先使用通配符:它们使API更灵活
  2. 返回类型避免通配符:会给调用者带来不便
  3. 通配符嵌套要谨慎:太复杂的嵌套会降低可读性
  4. 合理使用类型参数和通配符:根据是否需要引用类型决定
  5. 测试边界情况:特别是null值和类型边界

十四、练习题与思考 🤔

为了巩固所学,尝试解决以下问题:

  1. 编写一个方法,将一个List和一个List中的所有元素相加,返回总和
  2. 创建一个通用的addAll方法,可以将一个列表的所有元素添加到另一个列表中,考虑PECS原则
  3. 为什么Collections.max()方法的签名是这样的?
    public static > T max(Collection coll)
    

十五、结语 🌈

恭喜你坚持到了这里!👏 泛型通配符确实是Java中比较复杂的主题,但一旦掌握了它,你就能写出更灵活、更安全的泛型代码。记住,理解extendssuper的关键在于思考数据的流向——是生产还是消费。

刚开始可能会觉得有点绕,多练习几次就会越来越清晰。就像学骑自行车一样,一开始可能会摔倒几次,但一旦掌握,就再也不会忘记了!🚴‍♂️

希望这篇文章能帮你彻底理解Java泛型通配符。如果有任何问题,欢迎随时讨论!💬

Happy coding! 💻🎉

推荐阅读文章

  • 由 Spring 静态注入引发的一个线上T0级别事故(真的以后得避坑)

  • 如何理解 HTTP 是无状态的,以及它与 Cookie 和 Session 之间的联系

  • HTTP、HTTPS、Cookie 和 Session 之间的关系

  • 什么是 Cookie?简单介绍与使用方法

  • 什么是 Session?如何应用?

  • 使用 Spring 框架构建 MVC 应用程序:初学者教程

  • 有缺陷的 Java 代码:Java 开发人员最常犯的 10 大错误

  • 如何理解应用 Java 多线程与并发编程?

  • 把握Java泛型的艺术:协变、逆变与不可变性一网打尽

  • Java Spring 中常用的 @PostConstruct 注解使用总结

  • 如何理解线程安全这个概念?

  • 理解 Java 桥接方法

  • Spring 整合嵌入式 Tomcat 容器

  • Tomcat 如何加载 SpringMVC 组件

  • “在什么情况下类需要实现 Serializable,什么情况下又不需要(一)?”

  • “避免序列化灾难:掌握实现 Serializable 的真相!(二)”

  • 如何自定义一个自己的 Spring Boot Starter 组件(从入门到实践)

  • 解密 Redis:如何通过 IO 多路复用征服高并发挑战!

  • 线程 vs 虚拟线程:深入理解及区别

  • 深度解读 JDK 8、JDK 11、JDK 17 和 JDK 21 的区别

  • 10大程序员提升代码优雅度的必杀技,瞬间让你成为团队宠儿!

  • “打破重复代码的魔咒:使用 Function 接口在 Java 8 中实现优雅重构!”

  • Java 中消除 If-else 技巧总结

  • 线程池的核心参数配置(仅供参考)

  • 【人工智能】聊聊Transformer,深度学习的一股清流(13)

  • Java 枚举的几个常用技巧,你可以试着用用

  • 由 Spring 静态注入引发的一个线上T0级别事故(真的以后得避坑)

  • 如何理解 HTTP 是无状态的,以及它与 Cookie 和 Session 之间的联系

  • HTTP、HTTPS、Cookie 和 Session 之间的关系

  • 使用 Spring 框架构建 MVC 应用程序:初学者教程

  • 有缺陷的 Java 代码:Java 开发人员最常犯的 10 大错误

  • Java Spring 中常用的 @PostConstruct 注解使用总结

  • 线程 vs 虚拟线程:深入理解及区别

  • 深度解读 JDK 8、JDK 11、JDK 17 和 JDK 21 的区别

  • 10大程序员提升代码优雅度的必杀技,瞬间让你成为团队宠儿!

  • 探索 Lombok 的 @Builder 和 @SuperBuilder:避坑指南(一)

  • 为什么用了 @Builder 反而报错?深入理解 Lombok 的“暗坑”与解决方案(二)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2340370.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

FlaskRestfulAPI接口的初步认识

FlaskRestfulAPI 介绍 记录学习 Flask Restful API 开发的过程 项目来源&#xff1a;【Flask Restful API教程-01.Restful API介绍】 我的代码仓库&#xff1a;https://gitee.com/giteechaozhi/flask-restful-api.git 后端API接口实现功能&#xff1a;数据库访问控制&#xf…

CSS预处理工具有哪些?分享主流产品

目前主流的CSS预处理工具包括&#xff1a;Sass、Less、Stylus、PostCSS等。其中&#xff0c;Sass是全球使用最广泛的CSS预处理工具之一&#xff0c;以强大的功能、灵活的扩展性以及完善的社区生态闻名。Sass通过增加变量、嵌套、混合宏&#xff08;mixin&#xff09;等功能&…

AI 速读 SpecReason:让思考又快又准!

在大模型推理的世界里&#xff0c;速度与精度往往难以兼得。但今天要介绍的这篇论文带来了名为SpecReason的创新系统&#xff0c;它打破常规&#xff0c;能让大模型推理既快速又准确&#xff0c;大幅提升性能。想知道它是如何做到的吗&#xff1f;快来一探究竟&#xff01; 论…

Qt通过ODBC和QPSQL两种方式连接PostgreSQL或PolarDB PostgreSQL版

一、概述 以下主要在Windows下验证连接PolarDB PostgreSQL版&#xff08;阿里云兼容 PostgreSQL的PolarDB版本&#xff09;。Linux下类似&#xff0c;ODBC方式则需要配置odbcinst.ini和odbc.ini。 二、代码 以下为完整代码&#xff0c;包含两种方式连接数据库&#xff0c;并…

MobaXterm连接Ubuntu(SSH)

1.查看Ubuntu ip 打开终端,使用指令 ifconfig 由图可知ip地址 2.MobaXterm进行SSH连接 点击session,然后点击ssh,最后输入ubuntu IP地址以及用户名

蓝桥杯2024省A.成绩统计

蓝桥杯2024省A.成绩统计 题目 题目解析与思路 题目要求返回至少要检查多少个人的成绩&#xff0c;才有可能选出k名同学&#xff0c;他们的方差小于一个给定的值 T 二分枚举答案位置&#xff0c;将答案位置以前的数组单独取出并排序&#xff0c;然后用k长滑窗O(1)计算方差 问…

Mac mini 安装mysql数据库以及出现的一些问题的解决方案

首先先去官网安装一下mysql数据库&#xff0c;基本上都是傻瓜式安装的流程&#xff0c;我也就不详细说了。 接下来就是最新版的mysql安装的时候&#xff0c;他就会直接让你设置一个新的密码。 打开设置&#xff0c;拉到最下面就会看到一个mysql的图标&#xff1a; 我设置的就是…

俄罗斯方块-简单开发版

一、需求分析 实现了一个经典的俄罗斯方块小游戏&#xff0c;主要满足以下需求&#xff1a; 1.图形界面 使用 pygame 库创建一个可视化的游戏窗口&#xff0c;展示游戏的各种元素&#xff0c;如游戏区域、方块、分数等信息。 2.游戏逻辑 实现方块的生成、移动、旋转、下落和锁…

你学会了些什么200601?--Flask搭建造测试数据平台

搭建造数平台的环境&#xff1a; ***python3.7 ***html5 ***css ***JavaScript ***Ajax ***MySQL 前台页面的显示 1.为了页面美化&#xff0c;使用了JavaScript&#xff0c;通过逐级展开/隐藏的的方式显示下一级菜单 2.为了在提交表单数据时页面不发生跳转&#xff0c;需要引用…

【音视频】FLV格式分析

FLV概述 FLV(Flash Video)是Adobe公司推出的⼀种流媒体格式&#xff0c;由于其封装后的⾳视频⽂件体积⼩、封装简单等特点&#xff0c;⾮常适合于互联⽹上使⽤。⽬前主流的视频⽹站基本都⽀持FLV。采⽤FLV格式封装的⽂件后缀为.flv。 FLV封装格式是由⼀个⽂件头(file header)和…

Keil5没有stm32的芯片库

下载完重启就行了&#xff0c;我这里就不演示了&#xff0c;stm已经下载&#xff0c;随便选的一个芯片库演示一下

【DVWA 靶场通关】 File Inclusion(文件包含漏洞)

1. 前言 文件包含漏洞 是 Web 应用中较为常见的漏洞之一&#xff0c;攻击者通过操控文件路径&#xff0c;访问或包含系统上的敏感文件&#xff0c;甚至执行恶意代码。DVWA&#xff08;Damn Vulnerable Web Application&#xff09;提供了一个理想的实验环境&#xff0c;让安全…

游戏引擎学习第229天

仓库:https://gitee.com/mrxiao_com/2d_game_5 回顾上次内容并介绍今天的主题 上次留下的是一个非常简单的任务&#xff0c;至少第一步是非常简单的。我们需要在渲染器中加入排序功能&#xff0c;这样我们的精灵&#xff08;sprites&#xff09;才能以正确的顺序显示。为此我…

【C++编程入门】:从零开始掌握基础语法

C语言是通过对C语言不足的地方进行优化创建的&#xff0c;C在C语言之上&#xff0c;C当然也兼容C语言&#xff0c; 在大部分地方使用C比C更方便&#xff0c;可能使用C需要一两百行代码&#xff0c;而C只需要五六十行。 目录 C关键字 命名空间 缺省参数 缺省参数分类 函数…

网络开发基础(游戏方向)之 概念名词

前言 1、一款网络游戏分为客户端和服务端两个部分&#xff0c;客户端程序运行在用户的电脑或手机上&#xff0c;服务端程序运行在游戏运营商的服务器上。 2、客户端和服务端之间&#xff0c;服务端和服务端之间一般都是使用TCP网络通信。客户端和客户端之间通过服务端的消息转…

【源码】【Java并发】【AQS】从ReentrantLock、Semaphore、CutDownLunch、CyclicBarrier看AQS源码

&#x1f44b;hi&#xff0c;我不是一名外包公司的员工&#xff0c;也不会偷吃茶水间的零食&#xff0c;我的梦想是能写高端CRUD &#x1f525; 2025本人正在沉淀中… 博客更新速度 &#x1f44d; 欢迎点赞、收藏、关注&#xff0c;跟上我的更新节奏 &#x1f4da;欢迎订阅专栏…

k8s介绍与实践

第一节 理论 基础介绍&#xff0c;部署实践&#xff0c;操作实践&#xff0c;点击这里学习 第二节 dashboard操作 查看安装的dashboard服务信息 kubectl get pod,svc -n kubernetes-dashboard 网页登录地址&#xff1a;https://server_ip:30976/#/login 创建token kube…

KRaft面试思路引导

Kafka实在2.8之后就用KRaft进行集群管理了 Conroller负责选举Leader&#xff0c;同时Controller管理集群元数据状态信息&#xff0c;并将元数据信息同步给各个分区的Leader 和Zookeeper管理一样&#xff0c;会选出一个Broker作为Controller去管理整个集群&#xff0c;但是元数…

FreeRTOS菜鸟入门(六)·移植FreeRTOS到STM32

目录 1. 获取裸机工程模版 2. 下载 FreeRTOS V9.0.0 源码 3. FreeRTOS文件夹内容简介 3.1 FreeRTOS文件夹 3.1.1 Demo文件夹 3.1.2 License 文件夹 3.1.3 Source 文件夹 3.2 FreeRTOS-Plus 文件夹 4. 往裸机工程添加 FreeRTOS 源码 5. 拷贝 FreeRTOSConfig…

14.第二阶段x64游戏实战-分析人物的名字

免责声明&#xff1a;内容仅供学习参考&#xff0c;请合法利用知识&#xff0c;禁止进行违法犯罪活动&#xff01; 本次游戏没法给 内容参考于&#xff1a;微尘网络安全 上一个内容&#xff1a;13.第二阶段x64游戏实战-分析人物等级和升级经验 名字&#xff08;中文英文符号…