1. Java的泛型通配符
-
泛型通配符可以使用
A-Z
的任意一个字符,不影响泛型的效果,但会影响理解。class GenericClass2<A> implements GenericInterface<A>{ @Override public A next() { return null; } }
-
通常使用的通配符包括
T
、K
、V
、E
和?
,它们有一些约定俗成的含义:?
表示不确定的 Java 类型。T
(type) 表示具体的一个Java类型K
和V
,即key、value,表示Java键值中的健和值。E
(element) 代表
2. 无界通配符 ?
2.1 一个小需求
-
老板说:list通过
toString()
得到的格式太丑了,我们自己定义一个通用的、打印list的工具方法吧 -
小菜鸟的我,认为这很简单啊,噼噼啪啪敲出了如下代码
public static void commonPrinter(List<Object> list) { for (Object obj : list) { System.out.print(obj + " "); } System.out.print("\n"); }
-
等到验证时,傻眼了,竟然不行 😂
public static void main(String[] args) { List<Integer> intList = new ArrayList<>(); intList.add(1); intList.add(2); commonPrinter(intList); // 编译报错,泛型类型无法继承,即使Integer是Object的子类 }
-
报错信息如下:
java: 不兼容的类型: java.util.List<java.lang.Integer>无法转换为java.util.List<java.lang.Object>
-
奇了个怪了,Java中所有类都是Object的子类啊,我传入Integer类型的list,为啥提示类型不兼容??
-
后来,仔细学习了下Java的泛型,知道了原因
commonPrinter
中,已经为泛型类List
传入了泛型类型Object
- 调用工具方法时,传入
List<Integer>
,相当于下面的代码:List<Object> list = intList;
- 泛型中,是不允许这样使用的:泛型擦除后,泛型类型是无法继承的,即使传入的泛型类型存在继承关系
2.2 如何正确实现上述需求?
-
使用
List<Object>
无法接收各种类型的list,那是不是每个类我都需要单独为其创建一个工具方法? -
能不能有一种入参声明,使其能接收各种类型的list ?
-
这时候,你需要使用无限定通配符
?
,将list定义为List<?> list
,使其可以持有任意类型的list -
注意: 这里的任意类型是指
List<Object>
、List<String>
、List<Integer>
等,而非传入的泛型类型,但是泛型类型决定了list类型public static void main(String[] args) { List<Integer> intList = new ArrayList<>(); intList.add(1); intList.add(2); // 传入List<Integer>类型的集合 commonPrinter(intList); List<String> stringList = new ArrayList<>(); stringList.add("hello"); stringList.add("world"); // 传入List<String>类型的集合 commonPrinter(stringList); } public static void commonPrinter(List<?> list) { for (Object obj : list) { System.out.print(obj + " "); } System.out.print("\n"); }
2.3 新的需求
-
老板说:实现一个工具类,将list的最后一个元素复制后添加到list中
-
这次我学聪明了:为了可以接收并处理各种类型的list,我使用无限定通配符
?
定义参数public static void addElement(List<?> list) { int size = list.size(); if (list.size() > 0) { list.add(list.get(size - 1)); // 编译报错 } }
-
好奇怪啊,获取最后一个元素、再添加一次,有啥不对呢
-
原因:
- 传入的list不知道具体类型,也就是说传入的元素类型不确定
- Java是不允许写入或读取不确定的类型
List<?>
匹配到的元素类型是不确定的,往里面添加元素,很可能出现将String存入Integer集合中- 此时,写操作不被允许,除非添加
null
元素 - 读操作是允许的,因为可以将所有的元素都视为Object类型
2.4 List和List<?>的区别
List<Object>
表示只能接收泛型类型为Object的list对象,List<?>
表示可以接收泛型类型为任意类型的list对象List<Object>
中,元素类型为Object是确定的,可以写入或读取Object类型的对象List<?>
中,元素类型是不确定的,写入操作不被允许(null
除外),只允许读取操作(读取出来的一定是Object类型或其子类型,都可以使用Object接收)
3. 上界通配符<? extends T>
3.1 需求难度增大
-
老板说:你这个工具类方法,可以接收任何类型的list,太宽泛了。我只需要你能接收Number类型或Number的子类型,帮我打印数字数组就可以了
-
菜鸟挠挠头,难道我要对接收到的list,判断它里面的元素类型,是Number类型或Number的子类型才打印?
-
仔细分析一下:老板是将泛型类型限定在了Number及其子类,是否有一种语法可以限定泛型类型为Number及其子类?
-
这时候,可以使用上界通配符实现
<? extends T>
public static void commonPrinter(List<? extends Number> list) { for (Number number : list) { System.out.print(number + " "); } System.out.print("\n"); }
3.2 上界通配符
-
<? extends T>
表示传入泛型类型必须为T或T的子类、接口T或接口T的实现类 -
List<? extends Number>
就限定了传入的list,及其泛型类型必须是Number或Number的子类。 -
下面的代码,编译无法通过:
java: 不兼容的类型: java.util.List<java.lang.String>无法转换为java.util.List<? extends java.lang.Number>
List<String> stringList = new ArrayList<>(); stringList.add("hello"); commonPrinter(stringList);
-
通过上界通配符,限定了泛型类型的上界,使得list存储的元素为Number或Number的子类
-
上界通配符,适合频繁读取的场景
- 读取时,元素都是Number或Number的子类型,可以统一向上转型为Number类型
- 写入时,与
?
一样,由于类型不确定,可能会向List<Integer>
中传入Double类型的对象,这是不允许的 - 因此,上界通配符适合频繁读取的场景,不允许写入(null除外)
4. 下界通配符
4.1 自我挑战的一个需求
- 既然可以通过上界通配符限定泛型类型为T或T的子类,是否可以限定泛型类型为T或T的基类呢?
- 可以使用
<? super T>
,限定传入的泛型类型必需是类T或类T的基类。 - 于是,兴致勃勃改写commonPrinter,限定其泛型类型为Number或Number的基类
4.2 下界通配符
-
问题来了: 这时若读取list,元素类型是什么呢?
- 传入的可能是Number,也可能是基类Object
- 除非使用最终的基类Object进行接收(向上转型),不然无法获取list中的元素
- 这样来看,下界通配符不适合读取的场景
public static void commonPrinter(List<? super Number> list) { for (Object number : list) { System.out.print(number + " "); } System.out.print("\n"); }
-
另一个问题: 不能读,那能写吗?如果写也不能,没啥存在的价值。。。
- 由于泛型类型是Number或Number的基类,向里面添加Number或Number的子类,都可以向上转型为Number或Number的基类
- 这时,写入的类型是确定的,写入操作是允许
- 这样来看,下界通配符适合频繁写入的场景
public static void commonInsert(List<? super Number> list) { list.add(12); }
4.3 一些总结
- 上界通配符,规定了泛型类型的上限:往里存入的元素类型不确定,读取出来的元素类型都可以向上转型为T
- 下界通配符,规定类泛型类型的下限:写入的元素类型为
T或T的子类
,可以向上转型为T或T的基类;读取出来的元素类型不确定,除非使用Object进行接收 - 因此,上界通配符适合读场景,下界通配符适合写场景
PECS(Producer Extends Consumer Super)原则
- 将容器看做一个生产者,想从中读取元素,需要使用上界通配符
“extends”
实现 - 将容器看做一个消费者,向往里写入元素,需要使用下界通配符
“super”
实现
参考文档
后续学习:
- 泛型的常见面试问题