🎉🎉《Spring Boot实战案例合集》目前已更新185个案例,我们将持续不断的更新。文末有电子书目录。
💪💪永久更新承诺
我们郑重承诺,所有订阅合集的粉丝都将享受永久免费的后续更新服务。
💌💌如何获取
订阅我们的合集《点我订阅》,并通过私信联系我们,我们将第一时间将电子书发送给您。
环境:Java21
1. 简介
在 Java 泛型中,<? super T> 叫下界通配符,表示参数类型得是 T 或其父类。用它能更灵活接收不同层级父类对象,常用于“写”场景,像往集合添加元素。<? extends T> 是上界通配符,意味着参数类型是 T 或其子类。它适合“读”操作,能从集合获取元素并安全转为 T 类型。二者各有适用场景,合理运用能提升代码灵活性与类型安全性,让泛型在处理不同类型数据时发挥强大威力,助力高效编程。
2.1 为什么需要通配符和边界?
在使用泛型时,我们常常会陷入一些棘手的困境。
让我们通过一个实际的例子来说明。假设我们有一个 Animal 类,以及它的两个子类 Dog 和 Cat。
public class Animal {}public class Dog extends Animal {}public class Cat extends Animal {}
接着,我们有一个简单的 Cage(笼子)类。这个笼子可以存放一个泛型的“动物”。
我们可以对这个笼子里的动物执行最基本的“放入”(放动物进笼子)和“取出”(从笼子拿出动物)操作,也就是 put() 和 get() 方法。
public class Cage<T> {private T item;public Cage(T t) {item = t;}public void set(T t) {item = t;}public T get() {return item;}}
接下来,我定义一个“动物笼子”。从逻辑上讲,动物笼子自然应该能够放狗(这里以狗作为动物的一种具体实例来举例)。
Cage<Animal> cage = new Cage<Dog>(new Dog()) ;
首先,编译都无法通过:
意思是:放狗的笼子不能转换为放动物的笼子。
是不是很无语!
狗是一种动物
但狗笼子不是动物笼子的一种
因此,即使包含的类型相互继承,容器本身也不会继承。因此,对 Cage<Dog> 的引用不能分配给 Cage<Animal>。
为了使泛型更符合人体工程学,Sun 的架构师设计了 <? extends T> 和 <? super T> 来关联。
2.2 什么是上界?
以下代码展示了上界通配符的用法:
Cage extends Animal>
一个能放任何种动物的笼子。
Cage<? extends Animal> 和 Cage<Dog> 之间的关键区别在于:Cage<? extends Animal> 是 Cage<Animal> 和 Cage<Dog> 两者的父类。
直接的好处就是:现在我们可以将一个“狗笼子”赋值给一个“动物笼子”变量了。
Cage extends Animal> p = new Cage<Dog>(new Dog());
接下来,我们再通过如下示例来进行扩展:我们定义Food食物类,然后在分为肉类(Meat)和水果类(Fruit)。Fruit 包括 Apple(苹果)和 Banana(香蕉)。Meat 包括 Pork(猪肉)和 Beef(牛肉)。Apple 有两种类型:GreenApple(青苹果)和 RedApple(红苹果)。
public class Food {}public class Fruit extends Food {}public class Meat extends Food {}public class Apple extends Fruit {}public class Banana extends Fruit {}public class Pork extends Meat {}public class Beef extends Meat {}public class RedApple extends Apple {}public class GreenApple extends Apple {}// 定义容器类public class Plate<T> {private T item ;}
如下图所示,上界通配符 Plate<? extends Meat> 覆盖了图中的红色区域。
2.3 什么是下界?
相应地,"下限通配符 "也是如此:
Plate<? super Meat>
它表达的是相反的概念:一个能放 Meat 以及 Meat 任何父类的盘子。
Plate<? super Meat> 是 Plate<Meat> 的父类,但不是 Plate<Beef> 的父类。
对应前面的例子,Plate<? super Meat> 涵盖了图中红色区域所表示的范围。
2.4 上下界通配符的副作用
边界使不同 Java 泛型之间的转换变得更容易。不过,不要忘记,这种转换也会产生某些副作用。具体来说,容器的某些功能可能会丧失。
还是以 Plate 为例,我们可以对 Plate 做两件事:向 Plate 中 set() 新元素和从 Plate 中 get() 元素。
public class Plate<T> {private T item;public Plate(T t) { item = t; }public void set(T t) { item = t; }public T get() { return item; }}
上界通配符 <? extends T> 的副作用
如果使用 Plate<? extends Meat>(能放 Meat 及其子类),set() 方法会受限:
Plate<? extends Meat> plate = new Plate<Beef>(new Beef());// ❌ 编译错误!不能放入任何具体类型(包括 Beef)plate.set(new Beef());// ✅ 可以取出,类型是 MeatMeat meat = plate.get();
原因:
编译器无法确定 plate 具体是 Plate<Beef>、Plate<Pork> 还是其他 Plate<? extends Meat>,所以禁止放入任何非 null 的具体类型(避免类型污染)。
但可以安全地取出,因为取出的类型一定是 Meat 或其子类(可赋值给 Meat)。
下界通配符 <? super T> 的副作用
如果使用 Plate<? super Meat>(能放 Meat 及其父类),get() 方法会受限:
Plate<? super Meat> plate = new Plate<Object>(new Object());// ✅ 可以放入 Beef(因为 Beef 是 Meat 的子类)plate.set(new Beef());// ❌ 编译错误!取出的类型只能是 ObjectMeat meat = plate.get();// ✅ 可以取出,但类型是 ObjectObject obj = plate.get();
原因:
编译器只能确定 plate 至少能盛放 Meat(可能是 Plate<Meat>、Plate<Object> 等),所以可以安全地放入 Meat 及其子类(如 Beef)。
但取出时只能用 Object 接收,因为具体类型可能是 Meat 或 Object(编译器无法保证更具体的类型)。
2.5 PECS原则
PECS 原则(Producer-Extends, Consumer-Super)
为了更好地记住 <? extends T> 和 <? super T> 的使用场景,有一个有用的记忆口诀:PECS。
Producer-Extends:如果你是一个生产者(从容器中获取数据),使用 <? extends T>
Consumer-Super:如果你是一个消费者(向容器中添加数据),使用 <? super T>
最后,做一个总结:
<? extends T> 用于“生产者”场景,允许你从集合中读取 T 或其子类的元素,但不能向集合中添加元素(除了 null)
<? super T> 用于“消费者”场景,允许你向集合中添加 T 或其子类的元素,但从集合中读取元素时只能赋值给 Object 类型
PECS 原则(Producer-Extends, Consumer-Super)可以帮助你快速确定在特定情况下应该使用哪种通配符。
推荐文章
性能优化!3种方案优化Controller接口调用,性能提升N倍
神器!基于 Spring Boot 处理 CSV 超强工具,太方便了
性能优化!7个策略,让Spring Boot 处理每秒百万请求
强大!借助Spring Boot内置动态刷新功能,实时更新组件
性能提升!@Async与CompletableFuture优雅应用


