Java 通配符详解:?、? extends、? super 一篇搞懂
目录
- 一、为什么需要通配符?—— 从泛型的 “不变性” 说起
- 泛型不变性的 “坑”
- 通配符的核心作用
- 泛型不变性与通配符作用图解
- 二、无界通配符:?(表示 “任意类型”)
- 1. 无界通配符的用法
- 2. 无界通配符的限制:不能添加元素(除了 null)
- 3. 无界通配符 vs 泛型方法
- 无界通配符图解
- 三、上界通配符:? extends T(表示 “T 或 T 的子类”)
- 1. 上界通配符的用法
- 2. 上界通配符的限制:不能添加元素(除了 null)
- 3. 为什么上界通配符适合 “读取js”?
- 上界通配符图解
- 四、下界通配符:? super T(表示 “T 或 T 的父类”)
- 1. 下界通配符的用法
- 2. 下界通配符的限制:读取元素只能作为 Object
- 3. 为什么下界通配符适合 “写入”?
- 下界通配符图解
- 五、通配符的经典原则:PECS
- PECS 原则图解
- 六、常见误区与避坑指南
- 七、总结
在 Java 泛型中,通配符(Wildcard)是解决 “泛型类型不确定” 问题的关键。你可能见过List<?>
、List<? extends Number>
这样的写法,它们就是通配符的典型应用。通配符看似简单,却藏着不少细节:什么时候用?
?? extends
和? super
有啥区别?为什么有的场景能存元素,有的只能读?今天我们就从泛型的 “不变性” 讲起,彻底搞懂无界通配符、上界通配符和下界通配符的用法、场景和底层逻辑,配上直观图解,让你一次掌握!
一、为什么需要通配符?—— 从泛型的 “不变性” 说起
泛型有个重要特性:不变性(Invariance)。简单说就是:如果B
是A
的子类,List<B>
不是List<A>
的子类。这个特性会导致一些反直觉的问题,而通配符正是为了解决这些问题而生。
泛型不变性的 “坑”
假设我们有类继承关系:Dog extends Animal
,现在看下面的代码:
List<Dog> dogs = new ArrayList<>(); // 编译报错:List<Dog>不能赋值给List<Animal> List<Animal> animals = dogs;
为什么会报错?因为如果允许这种赋值,会导致类型安全问题:
// 假设上面的赋值合法 animals.add(new Cat()); // 往本应存Dog的列表里加了Cat Dog dog = dogs.get(0); // 运行时会抛ClassCastException(Cat不能转Dog)
泛型的不变性正是为了避免这种安全问题 ——编译时就阻断错误,而不是等到运行时。但这也带来了新问题:如何编写一个能处理 “任意 Animal 子类集合” 的通用方法?比如一个打印所有动物的方法,既想接收List<Dog>
,也想接编程客栈收List<Cat>
。这时,通配符就派上用场了。
通配符的核心作用
通配符(?
)表示 “未知类型”,它的出现打破了泛型的严格不变性,允许在一定范围内 “灵活匹配” 泛型类型,同时又不破坏类型安全。
泛型不变性与通配符作用图解
二、无界通配符:?(表示 “任意类型”)
无界通配符?
表示 “未知的任意类型”,可以理解为? extends Object
的简写。它的核心场景是:只需要使用 Object 类的通用方法,不需要关注具体类型。
1. 无界通配符的用法
当方法参数需要接收 “任意泛型类型”,且方法内部只读取元素(不修改),或只调用Object
类的方法(如toString()
、equals()
)时,用?
。
示例:打印任意集合的元素
// 无界通配符:可以接收List<String>、List<Integer>、List<Dog>等任意List public static void printList(List<?> list) { for (Object obj : list) { // 只能用Object接收元素 System.out.println(obj); // 调用Object的toString() } } // 调用示例 List<String> strList = Arrays.asList("a", "b"); List<Integer> intList = Arrays.asList(1, 2); printList(strList); // 合法 printList(intList); // 合法
2. 无界通配符的限制:不能添加元素(除了 null)
因为?
表示 “未知类型”,编译器无法确定集合能接收什么类型的元素,所以不能添加非 null 元素(null 是所有类型的默认值,例外)。
List<?> list = new ArrayList<String>(); list.add(null); // 合法(null是任意类型的实例) // list.add("hello"); // 编译报错:无法确定list是否能接收String
3. 无界通配符 vs 泛型方法
可能有人会问:用泛型方法public static <T> void printListpython(List<T> list)
不也能实现同样的功能吗?
两者的区别在于:
- 泛型方法的
T
是 “确定的未知类型”(调用时编译器会推断具体类型),适合需要在方法内部使用类型一致的场景(如返回T
类型结果); - 无界通配符
?
是 “完全未知的类型”,适合只需要Object
方法的场景,代码更简洁。
无界通配符图解
三、上界通配符:? extends T(表示 “T 或 T 的子类”)
上界通配符? extends T
表示 “未知类型,但其必须是 T 或 T 的子类”。核心场景是:需要读取元素,且元素类型是 T 的子类型(即 “Producer”—— 生产者,只产出 T 类型的元素)。
1. 上界通配符的用法
当方法需要从集合中读取元素,且希望元素是某个类型的子类型(如从 “数字集合” 中读取数字,不管是 Integer 还是 Double),用? extends T
。
示例:获取集合中的最大值(数字集合)
// 上界通配符:接收Number或其子类(Integer、Double等)的集合 public static double getMax(List<? extends Number> numbers) { double max = Double.MIN_VALUE; for (Number num : numbers) { // 安全读取为Number类型 if (num.doubleValue() > max) { max = num.doubleValue(); } } return max; } // 调用示例 List<Integer> ints = Arrays.asList(1, 3, 2); List<Double> doubles = Arrays.asList(1.5, 3.8, 2.2); System.out.println(getMax(ints)); // 3.0 System.out.println(getMax(doubles));// 3.8
2. 上界通配符的限制:不能添加元素(除了 null)
和无界通配符类似,? extends T
的具体类型未知(可能是 T 的任意子类),编译器无法确定集合能接收什么类型的元素,因此不能添加非 null 元素。
List<? extends Number> nums = new ArrayList<Integer>(); nums.add(null); // 合法 // nums.add(1); // 编译报错:无法确定nums是否能接收Integer(可能是List<Double>) // nums.add(3.14); // 编译报错:同理,可能是List<Integer>
3. 为什么上界通配符适合 “读取”?
因为不管具体类型是 T 的哪个子类,都可以安全地向上转型为 T。例如List<? extends Number>
中的元素,无论实际是 Integer 还是 Double,都能被Number
类型接收,因此读取操作是安全的。
上界通配符图解
四、下界通配符:? super T(表示 “T 或 T 的父类”)
下界通配符? super T
表示 “未知类型,但其必须是 T 或 T 的父类”。核心场景是:需要向集合中添加元素,且元素类型是 T 的子类型(即 “Consumer”—— 消费者,只接收 T 类型的元素)。
1. 下界通配符的用法
当方法需要向集合中添加元素,且希望元素是某个类型的子类型(如向 “动物集合” 中添加狗或猫,因为它们都是动物),用? super T
。
示例:向集合中添加元素(动物集合)
// 下界通配符:接收Animal或其父类(如Object)的集合 public static void addDogs(List<? super Animal> animals) { animals.add(new Dog()); // 安全添加Dog(Animal的子类) animals.add(new Cat()); // 安全添加Cat(Animal的子类) } // 调用示例 List<Animal> animalList = new ArrayList<>(); List<Object> objectList = new ArrayList<>(); addDogs(animalList); // 合法 addDogs(oandroidbjectList); // 合法
2. 下界通配符的限制:读取元素只能作为 Object
因为? super T
的具体类型是 T 的父类(可能是 T、T 的父类、Objectandroid),编译器无法确定具体是哪个父类,所以读取元素时只能用 Object 接收。
List<? super Animal> animals = new ArrayList<Object>(); animals.add(new Dog()); // 合法 Object obj = animals.get(0); // 只能用Object接收 // Animal animal = animals.get(0); // 编译报错:可能是List<Object>,不能确定是Animal
3. 为什么下界通配符适合 “写入”?
因为 T 的子类可以安全地向上转型为 T 的父类。例如List<? super Animal>
可以接收 Animal 的子类(Dog、Cat),因为它们都能被转型为 Animal(或其父类),因此写入操作是安全的。
下界通配符图解
五、通配符的经典原则:PECS
面对三种通配符,很多人会混淆什么时候用哪个。记住这个经典原则:PECS(Producer Extends, Consumer Super)—— 生产者用 extends,消费者用 super。
Producer(生产者):如果你需要从集合中读取元素(集合产出元素给你),用
? extends T
。例:从 “数字集合” 中读取数字计算总和,集合是生产者。Consumer(消费者):如果你需要向集合中写入元素(你给集合传入元素),用
? super T
。例:向 “动物集合” 中添加狗,集合是消费者。既读又写:不要用通配符,直接用具体的泛型类型(如
List<T>
)。
PECS 原则图解
六、常见误区与避坑指南
通配符不能用于泛型类 / 接口的定义通配符只能用于方法参数、变量声明,不能在定义泛型类或接口时使用:
// 错误:泛型类定义不能用通配符 class MyClass<?> { ... }
List<?>
与List<Object>
不同List<Object>
可以添加任意类型的元素(因为 Object 是所有类的父类);List<?>
不能添加非 null 元素(因为类型完全未知)。
上界和下界的嵌套使用复杂场景可能需要嵌套通配符,例如
Map<? extends K, ? super V>
,遵循 PECS 原则即可:键是生产者(用 extends),值是消费者(用 super)。通配符与泛型方法的选择
- 若方法需要返回与输入同类型的元素,用泛型方法(如
public <T> T getFirst(List<T> list)
); - 若方法只需要读取或写入,且不关心具体类型,用通配符更简洁。
- 若方法需要返回与输入同类型的元素,用泛型方法(如
七、总结
通配符是 Java 泛型中提升灵活性的关键,核心要点:
通配符类型 | 语法 | 含义 | 适用场景 | 操作限制 |
---|---|---|---|---|
无界通配符 | ? | 任意类型 | 只读取,用 Object 方法 | 可添加 null,读取用 Object |
上界通配符 | ? extends T | T 或 T 的子类 | 读取元素(生产者) | 可添加 null,读取用 T |
下界通配符 | ? super T | T 或 T 的父类 | 写入元素(消费者) | 可添加 T 及其子类,读取用 Object |
记住 PECS 原则:“Producer Extends, Consumer Super”,遇到泛型集合操作时,先判断是 “读” 还是 “写”,再选择对应的通配符。
通配符的设计体现了 Java 泛型 “安全与灵活平衡” 的思想 —— 既打破了严格的不变性,又通过编译时检查保证类型安全。熟练掌握通配符,能让你在处理集合、泛型组件时写出更简洁、更通用的代码!
到此这篇关于Java 通配符详解:?、? extends、? super 一篇搞懂的文章就介绍到这了,更多相关Java 通配符 内容请搜索编程客栈(www.devze.com)以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程客栈(www.devze.com)!
精彩评论