泛型 (? extends T和? super T)

本文深入解析Java泛型中的上界通配符<? extends T>和下界通配符<? super T>的概念及应用。通过具体实例说明如何解决因泛型带来的类型转换难题,并探讨其对容器功能的影响。

<? extends T><? super T>是泛型中的“通配符(Wildcards)”“边界(Bounds)”的概念。

  • <? extends T>:上界通配符(Upper Bounds Wildcards)
  • <? super T>:    下界通配符(Lower Bounds Wildcards)

为什么要用通配符和边界?

使用泛型的过程中,经常出现一种很别扭的情况。比如按照题主的例子,我们有Fruit类,和它的派生类Apple类。

class Fruit {}
class Apple extends Fruit {}

然后有一个最简单的容器:Plate类。盘子里可以放一个泛型的“东西”。我们可以对这个东西做最简单的“放”和“取”的动作:set( )和get( )方法。

class Plate<T>{
    private T item;
    public Plate(T t){item=t;}
    public void set(T t){item=t;}
    public T get(){return item;}
}

现在我定义一个“水果盘子”,逻辑上水果盘子当然可以装苹果。

Plate<Fruit> p=new Plate<Apple>(new Apple());

但实际上Java编译器不允许这个操作。会报错,“装苹果的盘子”无法转换成“装水果的盘子”。

error: incompatible types: Plate<Apple> cannot be converted to Plate<Fruit>

所以我的尴尬症就犯了。实际上,编译器脑袋里认定的逻辑是这样的:

  • 苹果 IS-A 水果
  • 装苹果的盘子 NOT-IS-A 装水果的盘子

所以,就算容器里装的东西之间有继承关系,但容器之间是没有继承关系的。所以我们不可以把Plate的引用传递给Plate。

为了让泛型用起来更舒服,就想出了<? extends T><? super T>的办法,来让”水果盘子“和”苹果盘子“之间发生关系。

什么是上界?

下面代码就是“上界通配符(Upper Bounds Wildcards)”:

Plate<? extends Fruit>

一个能放水果以及水果子类的盘子。Plate<? extends Fruit>Plate<Apple>最大的区别就是:Plate<? extends Fruit>Plate<Fruit>以及Plate<Apple>的父类。直接的好处就是,我们可以用“苹果盘子”给“水果盘子”赋值了。

Plate<? extends Fruit> p=new Plate<Apple>(new Apple());

如果把Fruit和Apple的例子再扩展一下,食物分成水果和肉类,水果有苹果和香蕉,肉类有猪肉和牛肉,苹果还有两种青苹果和红苹果。

//Lev 1
class Food{}

//Lev 2
class Fruit extends Food{}
class Meat extends Food{}

//Lev 3
class Apple extends Fruit{}
class Banana extends Fruit{}
class Pork extends Meat{}
class Beef extends Meat{}

//Lev 4
class RedApple extends Apple{}
class GreenApple extends Apple{}

在这个体系中,上界通配符 Plate<? extends Fruit> 覆盖下图中蓝色的区域。

 

什么是下界?

相对应的,“下界通配符(Lower Bounds Wildcards)”:

Plate<? super Fruit>

表达的就是相反的概念:一个能放水果以及水果父类的盘子。Plate<? super Fruit>Plate<Fruit>的基类,但不是Plate<Apple>的基类。对应刚才那个例子,Plate<? super Fruit>覆盖下图中红色的区域。

 

上下界通配符的副作用

边界让Java不同泛型之间的转换更容易了。但不要忘记,这样的转换也有一定的副作用。那就是容器的部分功能可能失效。

还是以刚才的Plate为例。我们可以对盘子做两件事,往盘子里set()新东西,以及从盘子里get()东西。

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>不能往里存,只能往外取

<? extends Fruit>会使往盘子里放东西的set( )方法失效。但取东西get( )方法还有效。比如下面例子里两个set()方法,插入Apple和Fruit都报错。

Plate<? extends Fruit> p=new Plate<Apple>(new Apple());
    
//不能存入任何元素
p.set(new Fruit());    //Error
p.set(new Apple());    //Error

//读取出来的东西只能存放在Fruit或它的基类里。
Fruit newFruit1=p.get();
Object newFruit2=p.get();
Apple newFruit3=p.get();    //Error

原因是编译器只知道容器内是Fruit或者它的派生类,但具体是什么类型不知道。可能是Fruit?可能是Apple?也可能是Banana,RedApple,GreenApple?编译器在看到后面用Plate赋值以后,盘子里没有被标上有“苹果”。而是标上一个占位符:CAP#1,来表示捕获一个Fruit或Fruit的子类,具体是什么类不知道,代号CAP#1。然后无论是想往里插入Apple或者Meat或者Fruit编译器都不知道能不能和这个CAP#1匹配,所以就都不允许。

所以通配符<?>和类型参数的区别就在于,对编译器来说所有的T都代表同一种类型。比如下面这个泛型方法里,三个T都指代同一个类型,要么都是String,要么都是Integer。

public <T> List<T> fill(T... t);

但通配符<?>没有这种约束,Plate<?>单纯的就表示:盘子里放了一个东西,是什么我不知道。

所以题主问题里的错误就在这里,Plate<? extends Fruit>里什么都放不进去。

下界<? super T>不影响往里存,但往外取只能放在Object对象里

使用下界<? super Fruit>会使从盘子里取东西的get( )方法部分失效,只能存放到Object对象里。set( )方法正常。

Plate<? super Fruit> p=new Plate<Fruit>(new Fruit());

//存入元素正常
p.set(new Fruit());
p.set(new Apple());

//读取出来的东西只能存放在Object类里。
Apple newFruit3=p.get();    //Error
Fruit newFruit1=p.get();    //Error
Object newFruit2=p.get();

因为下界规定了元素的最小粒度的下限,实际上是放松了容器元素的类型控制。既然元素是Fruit的基类,那往里存粒度比Fruit小的都可以。但往外读取元素就费劲了,只有所有类的基类Object对象才能装下。但这样的话,元素的类型信息就全部丢失。

PECS原则

最后看一下什么是PECS(Producer Extends Consumer Super)原则,已经很好理解了:

  • 频繁往外读取内容的,适合用上界Extends。
  • 经常往里插入的,适合用下界Super。
<think>好的,我现在需要回答用户关于Java中`<? extends T>``<? super T>`区别的问题。用户之前已经问过深拷贝浅拷贝,现在转向,可能是在学习Java的高级特性,需要理解这些通配符的用法区别。 首先,我应该回忆一下的基本概念。Java提供了编译时的类检查,避免类转换错误。通配符`?`用于表示未知类,而`extends``super`用来限定这个未知类的范围。 接下来,用户问的是`<? extends T>``<? super T>`的区别。我记得这涉及到PECS原则(Producer Extends, Consumer Super),但需要详细解释这两个通配符的作用适用场景。 对于`<? extends T>`,它表示类是T或其子类。这种通配符通常用于从集合中读取数据,即作为生产者。例如,一个`List<? extends Number>`可以接受`List<Integer>`或`List<Double>`。但是,不能向这样的列表中添加元素(除了null),因为编译器无法确定具体的类。 然后是`<? super T>`,它表示类是T或其父类。这种通配符适用于向集合中添加元素,即作为消费者。例如,`List<? super Integer>`可以接受`List<Integer>`、`List<Number>`甚至`List<Object>`。这时候,可以向列表中添加Integer类的元素,但读取时只能以Object类获取,因为具体类不确定。 需要举例子说明两者的使用场景,比如在方法参数中的应用。比如,一个复制方法,可能使用`<? extends T>`作为来源,`<? super T>`作为目标,这样来源是生产者,目标是消费者,符合PECS原则。 同时,要强调两者的界限:`extends`限制了上界,`super`限制了下界。这影响了类安全方法的允许操作,比如添加或读取的限制。 可能用户还容易混淆这两个符号的使用场合,需要明确何时使用哪一个。比如,当需要从数据结构获取数据时用extends,当需要存入数据时用super。 另外,需要注意类擦除的问题,虽然Java在运行时类信息会被擦除,但编译器会根据这些通配符进行类检查,确保代码的安全性。 最后,总结两者的主要区别,应用场景,以及可能带来的限制,比如类转换的问题或者添加元素的限制。确保用户能够理解何时选择哪一种通配符来达到他们的设计目的。</think>--- ### Java中`<? extends T>``<? super T>`的区别 --- #### **1. 核心概念** - **`<? extends T>`(上界通配符)** 表示**未知的具体类,但必须是`T`或其子类**。 **特点**: - 适用于从结构中**读取数据**(生产者角色)。 - **不能写入数据**(除了`null`)。 - **`<? super T>`(下界通配符)** 表示**未知的具体类,但必须是`T`或其父类**。 **特点**: - 适用于向结构中**写入数据**(消费者角色)。 - **可以读取数据**,但返回类只能是`Object`。 --- #### **2. 使用场景与示例** ##### **`<? extends T>`的典场景** **示例:读取集合中的元素** ```java // 定义一个方法,接受Number或其子类的集合 public static void printNumbers(List<? extends Number> list) { for (Number num : list) { System.out.println(num); // 安全读取 } } // 调用示例 List<Integer> intList = Arrays.asList(1, 2, 3); printNumbers(intList); // 合法:Integer是Number的子类 ``` **限制:不能写入** ```java list.add(42); // 编译错误!无法确定实际类 ``` --- ##### **`<? super T>`的典场景** **示例:向集合中添加元素** ```java // 定义一个方法,接受Integer或其父类的集合 public static void addIntegers(List<? super Integer> list) { list.add(42); // 安全写入 list.add(100); } // 调用示例 List<Number> numList = new ArrayList<>(); addIntegers(numList); // 合法:Number是Integer的父类 ``` **限制:读取时类不确定** ```java Object obj = list.get(0); // 只能以Object类接收 ``` --- #### **3. 关键区别** | **对比维度** | **`<? extends T>`** | **`<? super T>`** | |--------------------|-----------------------------------------------|--------------------------------------------| | **类范围** | `T`及其子类(上界) | `T`及其父类(下界) | | **数据操作** | 只读(生产者) | 可写(消费者) | | **读取返回值** | 明确的`T`类 | 只能是`Object`类 | | **写入限制** | 除`null`外不可写入 | 可写入`T`及其子类对象 | | **PECS原则** | Producer Extends(生产者用`extends`) | Consumer Super(消费者用`super`) | --- #### **4. PECS原则(Producer-Extends, Consumer-Super)** - **生产者(Producer)** 若结构**提供数据**(如集合的读取),使用`<? extends T>`。 ```java // 生产者:从src集合读取数据 public static <T> void copy(List<? extends T> src, List<? super T> dest) { for (T item : src) { dest.add(item); // src是生产者,dest是消费者 } } ``` - **消费者(Consumer)** 若结构**消费数据**(如集合的写入),使用`<? super T>`。 ```java // 消费者:向dest集合写入数据 List<Object> dest = new ArrayList<>(); copy(Arrays.asList("A", "B", "C"), dest); // String是Object的子类 ``` --- #### **5. 实际应用示例** - **`Collections.copy()`方法的实现** ```java public static <T> void copy(List<? super T> dest, List<? extends T> src) { // src是生产者(读取),dest是消费者(写入) for (int i = 0; i < src.size(); i++) { dest.set(i, src.get(i)); } } ``` - **工具类设计** ```java // 合并多个生产者集合 public static <T> List<T> merge(List<? extends T> list1, List<? extends T> list2) { List<T> result = new ArrayList<>(); result.addAll(list1); result.addAll(list2); return result; } ``` --- #### **6. 注意事项** 1. **类安全** - `<? extends T>`的读取是类安全的,但写入不明确。 - `<? super T>`的写入是类安全的,但读取需要强制类转换。 2. **灵活性与限制的权衡** - `extends`提供更严格的读取控制,`super`提供更灵活的写入能力。 3. **与方法结合** - 若需要同时支持读写,优先使用方法(如`<T> void process(List<T> list)`)。 --- #### **7. 总结** - **`<? extends T>`**:用于安全读取,限制写入。 - **`<? super T>`**:用于安全写入,限制读取类。 - **核心选择依据**: - 需要读取数据时,选择`extends`。 - 需要写入数据时,选择`super`。 - 遵循PECS原则,可显著提高代码的灵活性安全性。
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值