Java泛型学习笔记 - (七)浅析泛型中通配符的使用
一、基本概念:
在学习Java泛型的过程中, 通配符是较难理解的一部分. 主要有以下三类:
1. 无边界的通配符(Unbounded Wildcards), 就是<?>, 比如List<?>.
无边界的通配符的主要作用就是让泛型能够接受未知类型的数据.
2. 固定上边界的通配符(Upper Bounded Wildcards):
使用固定上边界的通配符的泛型, 就能够接受指定类及其子类类型的数据. 要声明使用该类通配符, 采用<? extends E>的形式, 这里的E就是该泛型的上边界. 注意: 这里虽然用的是extends关键字, 却不仅限于继承了父类E的子类, 也可以代指显现了接口E的类.
3. 固定下边界的通配符(Lower Bounded Wildcards):
使用固定下边界的通配符的泛型, 就能够接受指定类及其父类类型的数据. 要声明使用该类通配符, 采用<? super E>的形式, 这里的E就是该泛型的下边界. 注意: 你可以为一个泛型指定上边界或下边界, 但是不能同时指定上下边界.
二、基本使用方法:
1. 无边界的通配符的使用, 我们以在集合List中使用<?>为例. 如:
public static void printList(List<?> list) {
for (Object o : list) {
System.out.println(o);
}
}
public static void main(String[] args) {
List<String> l1 = new ArrayList<>();
l1.add("aa");
l1.add("bb");
l1.add("cc");
printList(l1);
List<Integer> l2 = new ArrayList<>();
l2.add(11);
l2.add(22);
l2.add(33);
printList(l2);
}
这种使用List<?>的方式就是父类引用指向子类对象. 注意, 这里的printList方法不能写成public static void printList(List<Object> list)的形式, 原因我在上一篇博文中已经讲过, 虽然Object类是所有类的父类, 但是List<Object>跟其他泛型的List如List<String>, List<Integer>不存在继承关系, 因此会报错.
有一点我们必须明确, 我们不能对List<?>使用add方法, 仅有一个例外, 就是add(null). 为什么呢? 因为我们不确定该List的类型, 不知道add什么类型的数据才对, 只有null是所有引用数据类型都具有的元素. 请看下面代码:
public static void addTest(List<?> list) {
Object o = new Object();
// list.add(o); // 编译报错
// list.add(1); // 编译报错
// list.add("ABC"); // 编译报错
list.add(null);
}
由于我们根本不知道list会接受到具有什么样的泛型List, 所以除了null之外什么也不能add.
还有, List<?>也不能使用get方法, 只有Object类型是个例外. 原因也很简单, 因为我们不知道传入的List是什么泛型的, 所以无法接受得到的get, 但是Object是所有数据类型的父类, 所以只有接受他可以, 请看下面代码:
public static void getTest(List<?> list) {
// String s = list.get(0); // 编译报错
// Integer i = list.get(1); // 编译报错
Object o = list.get(2);
}
那位说了, 不是有强制类型转换么? 是有, 但是我们不知道会传入什么类型, 比如我们将其强转为String, 编译是通过了, 但是如果传入个Integer泛型的List, 一运行还会出错. 那位又说了, 那么保证传入的String类型的数据不就好了么? 那样是没问题了, 但是那还用<?>干嘛呀? 直接List<String>不就行了.
2. 固定上边界的通配符的使用, 我仍旧以List为例来说明:
public static double sumOfList(List<? extends Number> list) {
double s = 0.0;
for (Number n : list) {
// 注意这里得到的n是其上边界类型的, 也就是Number, 需要将其转换为double.
s += n.doubleValue();
}
return s;
}
public static void main(String[] args) {
List<Integer> list1 = Arrays.asList(1, 2, 3, 4);
System.out.println(sumOfList(list1));
List<Double> list2 = Arrays.asList(1.1, 2.2, 3.3, 4.4);
System.out.println(sumOfList(list2));
}
有一点我们需要记住的是, List<? extends E>不能使用add方法, 请看如下代码:
public static void addTest2(List<? extends Number> l) {
// l.add(1); // 编译报错
// l.add(1.1); //编译报错
l.add(null);
}
原因很简单, 泛型<? extends E>指的是E及其子类, 这里传入的可能是Integer, 也可能是Double, 我们在写这个方法时不能确定传入的什么类型的数据, 如果我们调用:
List<Integer> list = new ArrayList<>();
addTest(list);
那么我们之前写的add(1.1)就会出错, 反之亦然, 所以除了null之外什么也不能add. 但是get的时候是可以得到一个Number, 也就是上边界类型的数据的, 因为不管存入什么数据类型都是Number的子类型, 得到这些就是一个父类引用指向子类对象.
3. 固定下边界通配符的使用. 这个较前面的两个有点难理解, 首先仍以List为例:
public static void addNumbers(List<? super Integer> list) {
for (int i = 1; i <= 10; i++) {
list.add(i);
}
}
public static void main(String[] args) {
List<Object> list1 = new ArrayList<>();
addNumbers(list1);
System.out.println(list1);
List<Number> list2 = new ArrayList<>();
addNumbers(list2);
System.out.println(list2);
List<Double> list3 = new ArrayList<>();
// addNumbers(list3); // 编译报错
}
我们看到, List<? super E>是能够调用add方法的, 因为我们在addNumbers所add的元素就是Integer类型的, 而传入的list不管是什么, 都一定是Integer或其父类泛型的List, 这时add一个Integer元素是没有任何疑问的. 但是, 我们不能使用get方法, 请看如下代码:
public static void getTest2(List<? super Integer> list) {
// Integer i = list.get(0); //编译报错
Object o = list.get(1);
}
这个原因也是很简单的, 因为我们所传入的类都是Integer的类或其父类, 所传入的数据类型可能是Integer到Object之间的任何类型, 这是无法预料的, 也就无法接收. 唯一能确定的就是Object, 因为所有类型都是其子类型.
使用? super E还有个常见的场景就是Comparator. TreeSet有这么一个构造方法:
TreeSet(Comparator<? super E> comparator)
就是使用Comparator来创建TreeSet, 大家应该都清楚, 那么请看下面的代码:
public class Person {
private String name;
private int age;
/*
* 构造函数与getter, setter省略
*/
}
public class Student extends Person {
public Student() {}
public Student(String name, int age) {
super(name, age);
}
}
class comparatorTest implements Comparator<Person>{
@Override
public int compare(Student s1, Student s2) {
int num = s1.getAge() - s2.getAge();
return num == 0 ? s1.getName().compareTo(s2.getName()) : num;
}
}
public class GenericTest {
public static void main(String[] args) {
TreeSet<Person> ts1 = new TreeSet<>(new comparatorTest());
ts1.add(new Person("Tom", 20));
ts1.add(new Person("Jack", 25));
ts1.add(new Person("John", 22));
System.out.println(ts1);
TreeSet<Student> ts2 = new TreeSet<>(new comparatorTest());
ts2.add(new Student("Susan", 23));
ts2.add(new Student("Rose", 27));
ts2.add(new Student("Jane", 19));
System.out.println(ts2);
}
}
不知大家有想过没有, 为什么Comparator<Person>这里用的是父类Person, 而不是子类Student. 初学时很容易困惑, ? super E不应该E是子类才对么? 其实, 实现接口时我们所设定的类型参数不是E, 而是?; E是在创建TreeSet时设定的. 如:
TreeSet<Person> ts1 = new TreeSet<>(new comparatorTest());
TreeSet<Student> ts2 = new TreeSet(new comparatorTest());
这里实例化的comparatorTest的泛型就是<Student super Student>和<Person super Student>(我这么写只是为了说明白). 在实现接口时使用:
// 这是错误的
class comparatorTest implements Comparator<Student> {...}
那么上面的结果就成了: <Student super Person>和<Person super Person>, <Student super Person>显然是错误的.
三、总结:
我们要记住这么几个使用原则, 有人将其称为PECS(即"Producer Extends, Consumer Super", 网上翻译为"生产者使用extends, 消费者使用super", 我觉得还是不翻译的好). 也有的地方写作"in out"原则, 总的来说就是:
- in或者producer就是你要读取出数据以供随后使用(想象一下List的get), 这时使用extends关键字, 固定上边界的通配符. 你可以将该对象当做一个只读对象;
- out或者consumer就是你要将已有的数据写入对象(想象一下List的add), 这时使用super关键字, 固定下边界的通配符. 你可以将该对象当做一个只能写入的对象;
- 当你希望in或producer的数据能够使用Object类中的方法访问时, 使用无边界通配符;
- 当你需要一个既能读又能写的对象时, 就不要使用通配符了.
P.S. 泛型的通配符感觉好麻烦, 中英文资料研究了一整天搞了个大概其. 自己能力有限也不知表达清楚了没有.
References:
[1] The Java™ Tutorials - Upper Bounded Wildcards - https://docs.oracle.com/javase/tutorial/java/generics/upperBounded.html
[2] The Java™ Tutorials - Lower Bounded Wildcards - https://docs.oracle.com/javase/tutorial/java/generics/lowerBounded.html
[3] The Java™ Tutorials - Guidelines for Wildcard Use - https://docs.oracle.com/javase/tutorial/java/generics/wildcardGuidelines.html
[4] stackoverflow - Difference between <? super T> and <? extends T> in Java - http://stackoverflow.com/questions/4343202/difference-between-super-t-and-extends-t-in-java
Java泛型学习笔记 - (七)浅析泛型中通配符的使用的更多相关文章
- Typescript 学习笔记七:泛型
中文网:https://www.tslang.cn/ 官网:http://www.typescriptlang.org/ 目录: Typescript 学习笔记一:介绍.安装.编译 Typescrip ...
- Java IO学习笔记七:多路复用从单线程到多线程
作者:Grey 原文地址:Java IO学习笔记七:多路复用从单线程到多线程 在前面提到的多路复用的服务端代码中, 我们在处理读数据的同时,也处理了写事件: public void readHandl ...
- Android(java)学习笔记89:泛型概述和基本使用
package cn.itcast_01; import java.util.ArrayList; import java.util.Iterator; /* * ArrayList存储字符串并遍历 ...
- Android(java)学习笔记28:泛型概述和基本使用
1. 泛型的概述和基本使用: package cn.itcast_01; import java.util.ArrayList; import java.util.Iterator; /* * Arr ...
- java jvm学习笔记七(jar包的代码认证和签名)
欢迎装载请说明出处:http://blog.csdn.net/yfqnihao 前言: 如果你循序渐进的看到这里,那么说明你的毅力提高了,jvm的很多东西都是比较抽像的,如果不找相对应的代码来辅助理解 ...
- [原创]java WEB学习笔记38:EL 中的 11个 隐含对象 详解
本博客为原创:综合 尚硅谷(http://www.atguigu.com)的系统教程(深表感谢)和 网络上的现有资源(博客,文档,图书等),资源的出处我会标明 本博客的目的:①总结自己的学习过程,相当 ...
- [原创]java WEB学习笔记39:EL中的运算符号(算术运算符,关系运算符,逻辑运算符,empty运算符,条件运算符,括号运算符)
本博客为原创:综合 尚硅谷(http://www.atguigu.com)的系统教程(深表感谢)和 网络上的现有资源(博客,文档,图书等),资源的出处我会标明 本博客的目的:①总结自己的学习过程,相当 ...
- [原创]java WEB学习笔记05:Servlet中的ServletConfig对象
本博客为原创:综合 尚硅谷(http://www.atguigu.com)的系统教程(深表感谢)和 网络上的现有资源(博客,文档,图书等),资源的出处我会标明 本博客的目的:①总结自己的学习过程,相当 ...
- Android(java)学习笔记92:泛型高级之通配符
package cn.itcast_07; import java.util.ArrayList; import java.util.Collection; /* * 泛型高级(通配符) * ?:任意 ...
随机推荐
- 工具网站gallery
jQuery各个版本齐全的api 在线编辑器codepen
- 第一次写Web API接口
API是什么?只知道是网络接口,具体怎么写?不会!如何调用?不会!那怎么办? 第一次的经历~~ 需求:为其他项目提供一个接口 功能:为项目提供询盘信息和商家信息,格式为Json字符串 拿过来,就开始做 ...
- 【转】wpa_supplicant与wpa_cli之间通信过程
[转]wpa_supplicant与wpa_cli之间通信过程 转自:http://blog.chinaunix.net/uid-26585427-id-4051479.html wpa_suppli ...
- django数据库的增删改查
正常启动服务安装数据库cmd命令:python manage.py makemigrations 创建用户 在pxm里面会有一个py文件..这就是生成了表结构cmd命令:python manage.p ...
- Python之路 day2 集合的基本操作
#!/usr/bin/env python # -*- coding:utf-8 -*- #Author:ersa ''' #集合是无序的 集合的关系测试, 增加,删除,查找等操作 ''' #列表去重 ...
- spring 注解的优点缺点
注解与XML配置的区别 注解:是一种分散式的元数据,与源代码耦合. xml :是一种集中式的元数据,与源代码解耦. 因此注解和XML的选择上可以从两个角度来看:分散还是集中,源代码耦合/解耦. 注解的 ...
- el: 在jsp页面内使用函数判断子字符串
e.g. <c:forEach items="${datas}" var="data"> <c:if test="${not fn: ...
- Elasticsearch5.0.1 + Kibana5.0.1 + IK 5.0.1安装记录
最近工作需要,开始研究ES,当前ES的最新版本为5.0.1,从之前的2.x的版本号一下升级到5.x,主要是之前Elastic的产品版本号因为收购等原因很乱,ES 2.X版本的和Kibana 4.x版本 ...
- 如何修改file控件
在移动web开发过程中,常常会用到input file这控件,但css不能修改其样式往往让开发者很头疼,直接把他放到页面上又不美观: 下面介绍的方法,可以将该控件的显示样式替换成一个图标: 该方法 ...
- 【原作】关于Dynpro中的红绿灯显示
问题源自李宁的需求,就不说了. 关于ABAP中的红绿灯网上也有很多资料,不过貌似都是ABAP的程序,没怎么注意.后来根据需求要在Dynpro上也做出那样的效果,所以,研究了一下. 这种红绿灯在ABAP ...