在实际的开发过程中,我们会发现,简单的泛型(比如List<T>)在某些场景下有点死板。
比如,一个接受List<Number>的方法,没办法接收List<Integer>类型的参数。
尽管我们都知道Integer是Number的子类。
为了解决这种问题,Java泛型提供了更强大的工具------通配符 。
今天,我们一起来看看这三个东西:<?>、<? extends T>和<? super T>。
一、为什么需要通配符?
一般有需求才会催生出相应的设计,我们直接看个例子:
java
package com.lazy.snail.day24;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* @ClassName Day24Demo
* @Description TODO
* @Author lazysnail
* @Date 2025/6/18 14:00
* @Version 1.0
*/
public class Day24Demo {
public static void printNumbers(List<Number> list) {
for (Number number : list) {
System.out.println(number);
}
}
public static void main(String[] args) {
List<Integer> integerList = new ArrayList<>(Arrays.asList(1, 2, 3));
List<Double> doubleList = new ArrayList<>(Arrays.asList(1.0, 2.0, 3.0));
List<Number> numberList = new ArrayList<>(Arrays.asList(1, 2.0, 3L));
printNumbers(numberList);
printNumbers(integerList);
printNumbers(doubleList);
}
}
我们本想写个方法,用来打印一个装有任意数字的列表。
实际情况是integerList和doubleList出现了编译错误。
其实Java泛型有一个很重要的概念,泛型类型之间没有继承关系。
就算Integer是Number的子类,List<Integer>也不是List<Number>的子类。它们是两种完全不同的类型。
这种类型限制就是为了保证类型安全。
如果List<Integer>可以被当成List<Number>,我们就可以通过List<Number>的引用往这个列表里添加一个Double,然后List<Integer>就不单纯的只能存放Integer了。
但是,我们又确实有"处理某一类泛型"的需求,所以通配符就来了。
二、无界通配符:<?>
<?>表示的是无界通配符,它代表"任何未知的类型"。List<?>的字面意思就是"一个持有某种未知类型的列表"。
还是来看上面的打印列表的例子:
java
package com.lazy.snail.day24;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* @ClassName Day24Demo2
* @Description TODO
* @Author lazysnail
* @Date 2025/6/18 14:10
* @Version 1.0
*/
public class Day24Demo2 {
public static void printList(List<?> list) {
for (Object o : list) {
System.out.println(o);
}
}
public static void main(String[] args) {
List<Integer> integerList = new ArrayList<>(Arrays.asList(1, 2, 3));
List<String> stringList = new ArrayList<>(Arrays.asList("A", "B", "C"));
printList(integerList);
printList(stringList);
}
}
printList方法现在可以接受任何类型的List了。
<?>的核心限制就是只读,不可写。
List<?>最大的特点就是,你不能往这个列表添加任何元素(除了null)。
因为编译器只知道它是一个列表,但不知道里面具体是什么类型。
它没办法保证你添加的元素符合列表的原始类型约束。
你可以尝试往List<?>放元素试试:
java
package com.lazy.snail.day24;
import java.util.ArrayList;
import java.util.List;
/**
* @ClassName Day24Demo3
* @Description TODO
* @Author lazysnail
* @Date 2025/6/18 14:14
* @Version 1.0
*/
public class Day24Demo3 {
public static void main(String[] args) {
List<?> list = new ArrayList<Integer>();
list.add(1);
list.add("Hello");
list.add(new Object());
list.add(null);
Object o = list.get(0);
}
}
你会发现,除了null以外,什么都放不进去,都是编译错误。
所以,只有当你只需要读取元素,不需要修改集合,并且处理的逻辑不依赖于元素的具体类型的时候,才会选择使用<?>。
三、上界通配符:<? extends T>
<? extends T>表示的是"任何T的子类,或者T本身"。
有了这个通配符,我们之前打印数字的方法就没问题了。
List<? extends Number>的意思是"一个持有Number或其任意子类(比如Integer,Double,Long)的列表"。
java
package com.lazy.snail.day24;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* @ClassName Day24Demo4
* @Description TODO
* @Author lazysnail
* @Date 2025/6/18 14:33
* @Version 1.0
*/
public class Day24Demo4 {
public static void printNumbers(List<? extends Number> list) {
for (Number n : list) {
System.out.println(n.doubleValue());
}
}
public static void main(String[] args) {
List<Integer> integerList = new ArrayList<>(Arrays.asList(1, 2, 3));
List<Double> doubleList = new ArrayList<>(Arrays.asList(1.0, 2.0, 3.0));
printNumbers(integerList);
printNumbers(doubleList);
// 写示例
List<? extends Number> list = new ArrayList<>();
list.add(1);
list.add(1.0);
list.add(null);
}
}
在写示例里,我们尝试往List<? extends T>添加元素,但是<?>一样,编译错误了。
因为编译器它只知道列表里的元素是Number的某个子类,但没办法确定具体是哪个子类。
可能是List<Integer>,也可能是List<Double>。
如果让你添加进去一个Integer,万一这个列表实际上是List<Double> 类型呢?
同样会造成类型不安全。
所以上界通配符<? extends T>主要用于安全的读取数据。
四、下界通配符:<? super T>
下界通配符跟上界通配符相反,<? super T>表示"任何T的父类,或者T本身"。
List<? super Integer>的意思是"一个持有Integer或其任意父类(比如Number, Object)的列表"。
java
package com.lazy.snail.day24;
import java.util.ArrayList;
import java.util.List;
/**
* @ClassName Day24Demo5
* @Description TODO
* @Author lazysnail
* @Date 2025/6/18 14:42
* @Version 1.0
*/
public class Day24Demo5 {
public static void addIntegers(List<? super Integer> list) {
list.add(1);
list.add(2);
}
public static void main(String[] args) {
List<Integer> integerList = new ArrayList<>();
List<Number> numberList = new ArrayList<>();
List<Object> objectList = new ArrayList<>();
addIntegers(integerList);
addIntegers(numberList);
addIntegers(objectList);
}
}
从上面的例子里看出,不管列表是List<Integer>、List<Number>还是List<Object>,你都可以添加一个 Integer对象,这是类型安全的,因为它符合所有这些可能类型的约束。
但是,如果你从List<? super Integer>里读取元素,你没办法确定具体会得到什么类型的对象。
可能是Integer,可能是Number,也可能是Object。
所以,为了类型安全,你只能用Object类型的引用来接收取出来的元素。
java
List<Number> numList = new ArrayList<>();
numList.add(1);
numList.add(2.5);
List<? super Integer> list = numList;
Object o1 = list.get(0);
Integer i1 = list.get(0);
"Integer i1 = list.get(0);"会报编译错误。
所以,下界通配符<? super T>一般都用来安全地写入数据。
五、PECS原则
初学的时候,我们可能不知道什么时候该使用extends,什么时候该使用super。
设计Java集合框架的Joshua Bloch提出了一个很著名的原则:PECS。
Producer Extends:
如果你的方法需要一个泛型集合作为参数,而且这个方法主要是从这个集合里读取(生产)数据,那就用 <? extends T>。
Consumer Super:
如果你的方法主要是往这个集合里写入(添加、消费)数据,那就使用<? super T>。
在Java标准库里,有这样一个方法:Collections.copy()。
这个方法其实就是PECS的具体体现。
来看一下这个方法的源码签名:
java
public static <T> void copy(List<? super T> dest, List<? extends T> src)
参数里的src是数据的生产者 (Producer)。
我们要从src里读取元素,所以用<? extends T>。
这样我们可以从src里安全地取出T和其子类型的对象。
参数里的dest是数据的消费者 (Consumer)。
我们要把元素写入到dest里,所以使用<? super T>。
这样我们就可以安全地把T类型的对象(从src中取出的)添加到dest列表里。
java
package com.lazy.snail.day24;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
* @ClassName Day24Demo6
* @Description TODO
* @Author lazysnail
* @Date 2025/6/18 15:23
* @Version 1.0
*/
public class Day24Demo6 {
public static void main(String[] args) {
List<Integer> src = Arrays.asList(1, 2, 3);
List<Number> dest = new ArrayList<>();
dest.add(0.0);
dest.add(0.0);
dest.add(0.0);
Collections.copy(dest, src);
System.out.println(dest);
}
}
上面的代码里,我们把一个List<Integer>拷贝到了一个List<Number>里。
dest在这里是List<Number>,符合<? super Integer>。
然后src在这里是List<Integer>,符合<? extends Integer>。
所以Collections.copy才能执行成功。
结语
在Java的集合框架中,大量的使用了泛型以及泛型通配符。
Java的集合框架是Java比较重要的一个版块。
了解及掌握了泛型和通配符对我们后续的集合学习非常有帮助。
下一篇预告
Day25 | 为什么说Java的泛型是伪泛型
如果你觉得这系列文章对你有帮助,欢迎关注专栏,我们一起坚持下去!
本文首发于知乎专栏************************************************************************************************************************************************************************************************************************************************************《Java 100天成长计划》****************************************************************************************************************************************************************************************************************************************************************