泛型通配符

在 Java 泛型中,通配符是比较难理解、但又很重要的一个知识点,我们今天深入详解泛型通配符。
本文对泛型通配符的理解、使用、以及技术选型,具有一定的参考价值。看完之后,你将掌握以下知识:
什么是泛型通配符为什么要用泛型通配符泛型通配符的概念、优点、作用泛型通配符的三种形式PECS原则泛型通配符的使用及选型泛型学习资料:《泛型最全知识导图》、《大厂泛型面试真题26道》,到本篇结尾处获得~
1 泛型通配符的基本概述
泛型通配符用 ? 表示,代表不确定的类型,是泛型的一个重要组成。
相关术语及范例:
ArrayList<E>,称为泛型类型,此处的 E ,称为类型参数变量ArrayList<Integer>,称为参数化的类型 ParameterizedType ,此处的Integer,称为实际类型参数2 为什么要用泛型通配符
泛型通配符的引入,是为了解决类型被限制死之后,不能动态根据实例来确定的缺点。
先来看代码示例,我们现在有这样一个函数:
public void test(List<Number> data) { }
根据泛型规则,这个函数只能传进来 List<Number> 这一种类型, List<Object> 和 List<Integer> 是传不进来的。
如果我们既要泛型,又想把这两个类型的子类、或者父类的泛型传进去,就需要使用到通配符泛型。
3 泛型通配符的优点
可以在保证运行时类型安全的基础上,提高参数类型的灵活性。
4 泛型通配符的作用
泛型本身不支持协变和逆变,通配符能解决协变和逆变的问题。
在 Java 中:
数组可以协变。例如:Dog Extends Animal , Animal[] 与 Dog[] 是可以兼容的。集合不可以协变。List<Animal> 不是 List<Dog> 的父类,为了建立两个集合之间的联系,此时需要引用通配符来解决。泛型 T 是确定的类型,而通配符更加灵活(不确定),更多用于扩充参数的范围
泛型 T就像是变量,将传来的一个具体的类型拿来使用,而通配符则是一种规定,规定你能传哪些参数,就像是一个特殊的实际类型参数。
5 泛型通配符的使用场景
用于类型参数中;用于实例变量,或者局部变量中;有时也可作为返回类型,例如Object的getClass方法。注意:泛型通配符只能用于泛型类的使用(声明变量、方法的参数),不能用于泛型定义、New 泛型实例。
6 泛型通配符的三种形式
<?> :无边界通配符(基本使用)<? extends T> :固定上边界通配符(又称有上限的通配符)<? super T> :固定下边界通配符(又称有下限的通配符)6.1 无边界通配符 <?>
无边界通配符的概述
英文全称 Unbounded Wildcards ,代表任意类型,是万能通配符 ;采用 <?> 的语法形式,来声明使用该类通配符。例如: List<?> ,表示元素类型未知的 List ,它的元素可以匹配任何的类型。这种带通配符的 List ,仅表示它是各种泛型 List 的父类,并不能把元素添加到其中。无边界通配符的作用
可以让泛型能够接受未知类型的数据。
6.2 固定上边界通配符 <? extends T>
固定上边界通配符的概述
英文全称 Upper Bounded Wildcards ;采用 <? extends 类型> 的语法形式,来声明使用该类通配符。例如:List<?extends Number> ,表示 Number 或者其子类型。需要注意的是,此处虽然使用了 extends 关键字,但却不仅限于继承了父类 类型 的子类,也可以代指实现了接口 类型 的类;代表类型变量的范围有限,只能传入某种类型、或者它的子类,适合频繁往外读取数据的场景。固定上边界通配符的作用
当我们不希望 List<?> 是任何泛型 List 的父类,只希望它代表某一类泛型 List 的父类时,我们就使用固定上边界通配符的泛型,来接受指定类、以及其子类类型的数据。
固定上边界通配符的代码示例
public class TopLine { public static void print(List<? extends Number> list) { for (int i = 0; i < list.size(); i++) { System.out.println(list.get(i)); } } public static void main(String[] args) { // Integer 是 Number 的子类,可以调用 print 方法 print(new ArrayList<Integer>()); // String 不是 Number 的子类,没法调用 print 方法 print(new ArrayList<String>()); } }
Integer 是 Number 的子类,如果想要调用 print() 方法中,就可以传入 Integer 集合。
String 不是 Number 的子类,因此不能传入 String 集合。
6.3 固定下边界通配符 <? super T>
固定下边界通配符的概述
英文全称 Lower Bounded Wildcards ;采用 <? super 类型> 的语法形式 ,来声明使用该类通配符。例如: List<?super Number> ,它表示的类型是 Number 或者其父类型;代表类型变量的范围有限,只能传入某种类型、或者其父类,适合频繁插入数据的场景。固定下边界通配符的作用
我们可以使用固定下边界通配符的泛型,来接受指定类及其父类类型的数据。
需要重点注意的是,一个泛型可以单独指定类型通配符的上边界、或下边界,但不能同时指定上边界、下边界。
固定下边界通配符的代码示例
public class LowLine { public static void print(List<? super Integer> list) { for (int i = 0; i < list.size(); i++) { System.out.println(list.get(i)); } } public static void main(String[] args) { // Number 是 Integer 的父类,可以调用 print 方法 print(new ArrayList<Number>()); // Long 不是 Integer 的父类,没法调用 print 方法 // print(new ArrayList<String>()); } }
因为 Number 是 Integer 的父类,如果我们想要调用 print() 方法中,就可以传入 Number 集合。
因为 Long 不是 Integer 的父类,故不能传入 Long 集合。
7 泛型通配符的使用
上一篇文篇泛型的3种使用方式:泛型类、泛型接口、泛型方法中,提到了泛型的使用步骤。
在第三步,我们确定类型变量时,如果不能明确类型变量,就可以使用泛型通配符(泛型通配符代表不确定的类型)。
反之,如果我们能明确地知道类型变量,就不需要使用泛型通配符。
代码示例 1 :不需要使用泛型通配符
我们明确地知道 count() 方法用在哪儿,在编码时,可以直接指明这是一个 Integer 集合。
public class Application { public static Integer count(List<Integer> list) { int total = 0; for (Integer number : list) { total += number; } list.add(total); return total; } public static void main(String[] args) {
在调用方法时,如果不传指定的数据进来,编译会报错。
// 不传指定数据,编译报错 List<String> strList = Arrays.asList("0", "1", "2"); int totalNum = count(strList);
即便我们绕过了编译,程序也很可能无法正常运行。
// 绕过了编译,运行报错 List strList1 = Arrays.asList("0", "1", "2"); totalNum = count(strList1); } }
所以,如果你已经明确自己要做什么,清楚地知道类型变量,就没必要使用泛型通配符。
代码示例 2 :使用泛型通配符,实现一些通用算法
但是,在一些通用方法中,什么类型的数据都能传进来,不能确认类型变量,这时候该怎么办呢?
此时,我们就可以使用泛型通配符,在不用确认类型变量的情况下,来实现一些通用算法。
例如:
我们需要写一个通用方法,把传入的 List 集合输出到控制台。
public class Application { public static void print(List<?> list) { for (int i = 0; i < list.size(); i++) { System.out.println(list.get(i)); } } public static void main(String[] args) {
Integer 集合,可以运行
List<Integer> intList = Arrays.asList(0, 1, 2); print(intList);
String 集合,可以运行
List<String> strList = Arrays.asList("0", "1", "2"); print(strList); } }
List<?> list 指不确定 List 集合装的是什么类型,有可能是 Integer、或 String、或其他。但这不用理会,我们只需要传一个 List 集合进来,这个方法就能正常运行了。
这就是泛型通配符的作用所在。
补充:我们还可以使用泛型通配符,来实现一些特殊算法
泛型通配符也适用于一些特殊算法,但可使用的范围不大。
例如:用户分为普通用户、商家用户,但用户有一些特殊功能,其它角色都没有。这时候,又该怎么办呢?我们可以通过给泛型通配符设定边界,来限定类型变量的范围。
这里就不展示代码示例了。
8 PECS原则
泛型通配符的用法有些复杂,为了更好的帮助我们理解、及正确使用 Java 泛型通配符。Joshua Bloch 在《Effective Java 》第 3 版中提出了 PECS 原则。
PECS 的英文全称是 Producer Extends , Consumer Super ,字面意思是“读取时使用 extends ,写入时使用 super ”。
也就是说:
参数化类型表示一个生产者,我们就使用 <? extends T> ;参数化类型表示一个消费者,我们就使用 <? super T> 。关于 PECS 原则此处就不展开了,这里我们只知道大概就行了,后面会写一篇专门介绍。
9 通配符与类型参数的区别
一般而言,通配符能干的事情都可以用类型参数替换。
例如:
public void testWildCards(Collection<?> collection){}
可以用以下来取代。
public <T> void test(Collection<T> collection){}
值得注意的是,如果用泛型方法来取代通配符,上面代码中的 collection 是能进行写操作的,但要进行强制转换。
public <T> void test(Collection<T> collection){ collection.add((T)new Integer(12)); collection.add((T)"123"); }
需要特别注意的是,类型参数适用于参数之间的类别依赖关系,举例说明。
public class Test2 <T,E extends T>{ T value1; E value2; }
如果一个方法的返回类型依赖于参数的类型,那么通配符也无能为力。
public T test1(T t){ return value1; }
10 泛型通配符和泛型方法的使用选型
在实际应用场景中,有很多时候,我们可以使用泛型方法、来替代泛型通配符。
使用通配符:
public static void test(List<?> list) { }
使用泛型方法:
public <T> void test2(List<T> t) { }
既然通配符和泛型方法都可用,具体该怎样选型呢?
通配符和泛型方法的选型参考:
参数之间的类型有依赖关系的,又或者返回值与参数之间有依赖关系的,使用泛型方法;参数之间的类型没有依赖关系的,使用通配符。总结
以上,我们总结了泛型通配符的概念、优点、作用、使用,对泛型通配符进行了全面解析,为正确选型提供了一定的参考,建议收藏备用,将这个系列连起来看看。
知识内化需要实战,建议大家多动手练习,毕竟实践才能出真知。
最后,附上我整理的泛型学习资料:
1 《泛型知识全景导图》
快速构建泛型知识体系,高清版本原图,几乎囊括了所有泛型核心知识点。
泛型知识全景导图
2 《大厂泛型面试真题26道》
精选大厂高频泛型面试题,都是我最新整理的,备面、复习时都可以用。
大厂泛型面试题26道
如果觉得有帮助,请顺手 点赞 支持下,谢谢。
我是大全哥,持续更新成体系的Java核心技术。知识成体系,学习才高效,我们下期见~
--- end ---


还没有评论,来说两句吧...