《理解 Java 泛型中的通配符:extends 与 super 的使用场景》
大家好呀!👋 今天我们要聊一个让很多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);}
}
这个方法看起来不错,但实际上它不能处理List或List!😱 因为List并不是List的子类型(虽然String是Object的子类)。
这就是通配符要解决的问题!它让我们可以更灵活地处理不同类型的泛型集合。🎯
二、通配符基础:问号(?)的魔力 ✨
通配符就是一个简单的问号?,它表示"未知类型"。我们可以这样改写上面的方法:
public void printList(List list) {for (Object elem : list) {System.out.println(elem);
);
}
现在这个方法可以接受任何类型的List了!🎉 因为List表示"某种类型的List,但我不知道具体是什么类型"。
但是,通配符真正的威力在于它可以与extends和super结合使用,这就是我们今天要深入探讨的重点!🔍
三、上界通配符: 📈
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 能做什么和不能做什么
可以做的事情:
- 从集合中读取元素(作为Animal类型)
- 调用Animal类的方法
不能做的事情:
-
向集合中添加元素(除了null)
animals.add(new Dog()); // 编译错误! animals.add(null); // 这是唯一允许的添加为什么?因为编译器不知道实际的类型参数是什么。可能是
List,也可能是List,所以为了类型安全,不允许添加。
3.4 实际应用场景
这种通配符特别适合"生产者"场景——即你主要从集合中读取数据。比如:
-
计算集合中所有数字的总和:
public double sumOfList(List list) {double sum = 0.0;for (Number num : list) {sum += num.doubleValue();}return sum; } -
在图形应用中处理各种形状:
void drawAll(List shapes) {for (Shape shape : shapes) {shape.draw();} }
四、下界通配符: 📉
4.1 基本概念
``表示"T或者T的某个父类型"。这被称为"下界通配符"(Lower Bounded Wildcard),因为它限定了类型的下界。
继续动物园的例子🦁:List可以表示"一个可以存放Dog及其子类的列表",比如List或List。
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 能做什么和不能做什么
可以做的事情:
- 向集合中添加T或T的子类元素
- 作为参数传递(消费场景)
不能做的事情:
-
安全地从集合中读取元素(除了作为Object)
Dog dog = list.get(0); // 编译错误! Object obj = list.get(0); // 这是可以的为什么?因为列表可能是
List,而你不能保证取出的就是Dog。
4.4 实际应用场景
这种通配符特别适合"消费者"场景——即你主要向集合中添加数据。比如:
-
将多个元素添加到集合中:
public void addNumbers(List list) {for (int i = 1; i <= 10; i++) {list.add(i);} } -
在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有效?
这个原则之所以有效,是因为:
-
对于生产者(
extends):- 你只能从中读取,不能写入(除了null)
- 读取的元素至少是某种特定类型(上界)
-
对于消费者(
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 实际应用
无界通配符常用于:
- 当方法实现只需要Object类提供的功能时
- 当类型参数不重要或不可知时
- 作为泛型类中非泛型方法的参数类型
七、通配符在方法签名中的应用 🎯
通配符不仅可以用在变量声明中,还可以用在方法签名中,使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:认为List和List相同
错!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 关键点回顾
- ``:用于从结构中读取(生产者),不能写入(除了null)
- ``:用于向结构中写入(消费者),只能以Object读取
- ``:当类型完全无关紧要时使用
- 记住PECS原则:Producer-Extends, Consumer-Super
13.2 最佳实践
- 优先使用通配符:它们使API更灵活
- 返回类型避免通配符:会给调用者带来不便
- 通配符嵌套要谨慎:太复杂的嵌套会降低可读性
- 合理使用类型参数和通配符:根据是否需要引用类型决定
- 测试边界情况:特别是null值和类型边界
十四、练习题与思考 🤔
为了巩固所学,尝试解决以下问题:
- 编写一个方法,将一个
List和一个List中的所有元素相加,返回总和 - 创建一个通用的
addAll方法,可以将一个列表的所有元素添加到另一个列表中,考虑PECS原则 - 为什么
Collections.max()方法的签名是这样的?public static > T max(Collection coll)
十五、结语 🌈
恭喜你坚持到了这里!👏 泛型通配符确实是Java中比较复杂的主题,但一旦掌握了它,你就能写出更灵活、更安全的泛型代码。记住,理解extends和super的关键在于思考数据的流向——是生产还是消费。
刚开始可能会觉得有点绕,多练习几次就会越来越清晰。就像学骑自行车一样,一开始可能会摔倒几次,但一旦掌握,就再也不会忘记了!🚴♂️
希望这篇文章能帮你彻底理解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 的“暗坑”与解决方案(二)
相关文章:
《理解 Java 泛型中的通配符:extends 与 super 的使用场景》
大家好呀!👋 今天我们要聊一个让很多Java初学者头疼的话题——泛型通配符。别担心,我会用最通俗易懂的方式,带你彻底搞懂这个看似复杂的概念。准备好了吗?Let’s go! 🚀 一、为什么我们需要泛型通配符&…...
C#学习第17天:序列化和反序列化
什么是序列化? 定义:序列化是指把对象转换为一种可以轻松存储或传输的格式,如JSON、XML或二进制格式。这个过程需要捕获对象的类型信息和数据内容。用途:使得对象可以持久化到文件、发送至网络、或存储在数据库中。 什么是反序列…...
FlaskRestfulAPI接口的初步认识
FlaskRestfulAPI 介绍 记录学习 Flask Restful API 开发的过程 项目来源:【Flask Restful API教程-01.Restful API介绍】 我的代码仓库:https://gitee.com/giteechaozhi/flask-restful-api.git 后端API接口实现功能:数据库访问控制…...
CSS预处理工具有哪些?分享主流产品
目前主流的CSS预处理工具包括:Sass、Less、Stylus、PostCSS等。其中,Sass是全球使用最广泛的CSS预处理工具之一,以强大的功能、灵活的扩展性以及完善的社区生态闻名。Sass通过增加变量、嵌套、混合宏(mixin)等功能&…...
微信小程序中,将搜索组件获取的值传递给父页面(如 index 页面)可以通过 自定义事件 或 页面引用 实现
将搜索组件获取的值传递给父页面(如 index 页面)可以通过 自定义事件 或 页面引用 实现 方法 1:自定义事件(推荐) 步骤 1:搜索组件内触发事件 在搜索组件的 JS 中,当获取到搜索值时,…...
深度学习预训练和微调
目录 1. 预训练(Pre-training)是什么? 2. 微调(Fine-tuning)是什么? 3. 预训练和微调的对象 4. 特征提取如何实现? 预训练阶段: 微调阶段: 5. 这样做的作用和意义 …...
AI 速读 SpecReason:让思考又快又准!
在大模型推理的世界里,速度与精度往往难以兼得。但今天要介绍的这篇论文带来了名为SpecReason的创新系统,它打破常规,能让大模型推理既快速又准确,大幅提升性能。想知道它是如何做到的吗?快来一探究竟! 论…...
Qt通过ODBC和QPSQL两种方式连接PostgreSQL或PolarDB PostgreSQL版
一、概述 以下主要在Windows下验证连接PolarDB PostgreSQL版(阿里云兼容 PostgreSQL的PolarDB版本)。Linux下类似,ODBC方式则需要配置odbcinst.ini和odbc.ini。 二、代码 以下为完整代码,包含两种方式连接数据库,并…...
MobaXterm连接Ubuntu(SSH)
1.查看Ubuntu ip 打开终端,使用指令 ifconfig 由图可知ip地址 2.MobaXterm进行SSH连接 点击session,然后点击ssh,最后输入ubuntu IP地址以及用户名...
Lambda 函数与 peek 操作的使用案例
Lambda 函数和 peek 操作是 Java 8 Stream API 中非常有用的特性,下面我将介绍它们的使用案例。 Lambda 函数使用案例 Lambda 表达式是 Java 8 引入的一种简洁的匿名函数表示方式。 集合操作 List<String> names Arrays.asList("Alice", "B…...
C# 的 字符串插值($) 和 逐字字符串(@) 功能
这段代码使用了 C# 的 字符串插值($) 和 逐字字符串() 功能,并在 SQL 语句中动态拼接变量。下面详细解释它们的用法: 1. $(字符串插值) $ 是 C# 的 字符串插值 符号,允许…...
软考 中级软件设计师 考点知识点笔记总结 day13 数据库系统基础知识 数据库模式映像 数据模型
文章目录 数据库系统基础知识6.1 基本概念6.1.1 DBMS的特征与分类 6.2 数据库三级模式两级映像6.3 数据库的分析与设计过程6.4 数据模型6.4.1 ER模型6.4.2 关系模型 数据库系统基础知识 基本概念 数据库三级模式两级映像 数据库的分析与设计过程 数据模型 关系代数 数据库完整…...
蓝桥杯2024省A.成绩统计
蓝桥杯2024省A.成绩统计 题目 题目解析与思路 题目要求返回至少要检查多少个人的成绩,才有可能选出k名同学,他们的方差小于一个给定的值 T 二分枚举答案位置,将答案位置以前的数组单独取出并排序,然后用k长滑窗O(1)计算方差 问…...
Mac mini 安装mysql数据库以及出现的一些问题的解决方案
首先先去官网安装一下mysql数据库,基本上都是傻瓜式安装的流程,我也就不详细说了。 接下来就是最新版的mysql安装的时候,他就会直接让你设置一个新的密码。 打开设置,拉到最下面就会看到一个mysql的图标: 我设置的就是…...
俄罗斯方块-简单开发版
一、需求分析 实现了一个经典的俄罗斯方块小游戏,主要满足以下需求: 1.图形界面 使用 pygame 库创建一个可视化的游戏窗口,展示游戏的各种元素,如游戏区域、方块、分数等信息。 2.游戏逻辑 实现方块的生成、移动、旋转、下落和锁…...
STM32的启动方式
目录 一、从主闪存存储器启动(Main Flash Memory) 二、从系统存储器启动(System Memory) 三、从内置SRAM启动(Embedded SRAM) 四、从外挂存储介质启动的实现方式 1. 存储介质选型 2. 硬件连接 3. 引…...
你学会了些什么200601?--Flask搭建造测试数据平台
搭建造数平台的环境: ***python3.7 ***html5 ***css ***JavaScript ***Ajax ***MySQL 前台页面的显示 1.为了页面美化,使用了JavaScript,通过逐级展开/隐藏的的方式显示下一级菜单 2.为了在提交表单数据时页面不发生跳转,需要引用…...
【音视频】FLV格式分析
FLV概述 FLV(Flash Video)是Adobe公司推出的⼀种流媒体格式,由于其封装后的⾳视频⽂件体积⼩、封装简单等特点,⾮常适合于互联⽹上使⽤。⽬前主流的视频⽹站基本都⽀持FLV。采⽤FLV格式封装的⽂件后缀为.flv。 FLV封装格式是由⼀个⽂件头(file header)和…...
Keil5没有stm32的芯片库
下载完重启就行了,我这里就不演示了,stm已经下载,随便选的一个芯片库演示一下...
【DVWA 靶场通关】 File Inclusion(文件包含漏洞)
1. 前言 文件包含漏洞 是 Web 应用中较为常见的漏洞之一,攻击者通过操控文件路径,访问或包含系统上的敏感文件,甚至执行恶意代码。DVWA(Damn Vulnerable Web Application)提供了一个理想的实验环境,让安全…...
游戏引擎学习第229天
仓库:https://gitee.com/mrxiao_com/2d_game_5 回顾上次内容并介绍今天的主题 上次留下的是一个非常简单的任务,至少第一步是非常简单的。我们需要在渲染器中加入排序功能,这样我们的精灵(sprites)才能以正确的顺序显示。为此我…...
【C++编程入门】:从零开始掌握基础语法
C语言是通过对C语言不足的地方进行优化创建的,C在C语言之上,C当然也兼容C语言, 在大部分地方使用C比C更方便,可能使用C需要一两百行代码,而C只需要五六十行。 目录 C关键字 命名空间 缺省参数 缺省参数分类 函数…...
Python3网络爬虫开发--爬虫基础
网络爬虫基础 1.1 HTTP基本原理 1.1.1 URI和URL URI即统一资源标志符,URL即统一资源定位符。 有这样一个链接,http://test.com/test.txt,在这个链接中,包含了访问协议https,访问目录(即根目录),资源名称(test.txt)。通过这样的链接,可以在互联网上找到这个资源,这…...
网络开发基础(游戏方向)之 概念名词
前言 1、一款网络游戏分为客户端和服务端两个部分,客户端程序运行在用户的电脑或手机上,服务端程序运行在游戏运营商的服务器上。 2、客户端和服务端之间,服务端和服务端之间一般都是使用TCP网络通信。客户端和客户端之间通过服务端的消息转…...
MyBatisPlus-QueryWrapper的exists方法拼接SQL中的EXISTS子句
在 MyBatis-Plus 中,QueryWrapper 的 exists 方法用于拼接 SQL 中的 EXISTS 子句,通常用于构 建子查询条件。以下是具体用法和示例: 1. 基本语法 // 判断是否存在符合条件的记录 queryWrapper.exists(String existsSql); queryWrapper.notExists(String existsSq…...
【源码】【Java并发】【AQS】从ReentrantLock、Semaphore、CutDownLunch、CyclicBarrier看AQS源码
👋hi,我不是一名外包公司的员工,也不会偷吃茶水间的零食,我的梦想是能写高端CRUD 🔥 2025本人正在沉淀中… 博客更新速度 👍 欢迎点赞、收藏、关注,跟上我的更新节奏 📚欢迎订阅专栏…...
k8s介绍与实践
第一节 理论 基础介绍,部署实践,操作实践,点击这里学习 第二节 dashboard操作 查看安装的dashboard服务信息 kubectl get pod,svc -n kubernetes-dashboard 网页登录地址:https://server_ip:30976/#/login 创建token kube…...
SpringBoot 3 与 SpringDoc 打造完美接口文档
文章目录 1. SpringDoc 简介1.1 SpringDoc 优势2. 环境准备2.1 Maven 依赖2.2 基础配置3. 创建基本文档配置类4. 控制器 API 文档注解4.1 基本控制器示例4.2 模型类示例5. 高级功能5.1 API分组5.2 安全配置5.3 隐藏特定端点6. 参数描述6.1 路径参数6.2 查询参数6.3 请求体7. 响…...
【HFP】蓝牙HFP协议音频连接核心技术深度解析
目录 一、音频连接建立的总体要求 1.1 发起主体与时机 1.2 前提条件 1.3 同步连接的建立 1.4 通知机制 二、不同主体发起的音频连接建立流程 2.1 连接建立触发矩阵 2.2 AG 发起的音频连接建立 2.3 HF 发起的音频连接建立 三、编解码器连接建立流程 3.1 发起条件 3.…...
KRaft面试思路引导
Kafka实在2.8之后就用KRaft进行集群管理了 Conroller负责选举Leader,同时Controller管理集群元数据状态信息,并将元数据信息同步给各个分区的Leader 和Zookeeper管理一样,会选出一个Broker作为Controller去管理整个集群,但是元数…...
