本系列文章经补充和完善,已修订整理成书《Java编程的逻辑》,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接http://item.jd.com/12299018.html


上节我们介绍了泛型的基本概念和原理,本节继续讨论泛型,主要讨论泛型中的通配符概念。通配符有着令人费解和混淆的语法,但通配符大量应用于Java容器类中,它到底是什么?本节,让我们逐步来解析。

更简洁的参数类型限定

在上节最后,我们提到一个例子,为了将Integer对象添加到Number容器中,我们的类型参数使用了其他类型参数作为上界,代码是:

public <T extends E> void addAll(DynamicArray<T> c) {
for(int i=0; i<c.size; i++){
add(c.get(i));
}
}

我们提到,这个写法有点啰嗦,它可以替换为更为简洁的通配符形式:

public void addAll(DynamicArray<? extends E> c) {
for(int i=0; i<c.size; i++){
add(c.get(i));
}
}

这个方法没有定义类型参数,c的类型是DynamicArray<? extends E>,?表示通配符,<? extends E>表示有限定通配符,匹配E或E的某个子类型,具体什么子类型,我们不知道。

使用这个方法的代码不需要做任何改动,还可以是:

DynamicArray<Number> numbers = new DynamicArray<>();
DynamicArray<Integer> ints = new DynamicArray<>();
ints.add(100);
ints.add(34);
numbers.addAll(ints);

这里,E是Number类型,DynamicArray<? extends E>可以匹配DynamicArray<Integer>。

<T extends E>与<? extends E>

那么问题来了,同样是extends关键字,同样应用于泛型,<T extends E>和<? extends E>到底有什么关系?

它们用的地方不一样,我们解释一下:

  • <T extends E>用于定义类型参数,它声明了一个类型参数T,可放在泛型类定义中类名后面、泛型方法返回值前面。
  • <? extends E>用于实例化类型参数,它用于实例化泛型变量中的类型参数,只是这个具体类型是未知的,只知道它是E或E的某个子类型。

虽然它们不一样,但两种写法经常可以达成相同目标,比如,前面例子中,下面两种写法都可以:

public void addAll(DynamicArray<? extends E> c)
public <T extends E> void addAll(DynamicArray<T> c)

那,到底应该用哪种形式呢?我们先进一步理解通配符,然后再解释。

理解通配符

无限定通配符

还有一种通配符,形如DynamicArray<?>,称之为无限定通配符,我们来看个使用的例子,在DynamicArray中查找指定元素,代码如下:

public static int indexOf(DynamicArray<?> arr, Object elm){
for(int i=0; i<arr.size(); i++){
if(arr.get(i).equals(elm)){
return i;
}
}
return -1;
}

其实,这种无限定通配符形式,也可以改为使用类型参数。也就是说,下面写法:

public static int indexOf(DynamicArray<?> arr, Object elm)

可以改为:

public static <T> int indexOf(DynamicArray<T> arr, Object elm)

不过,通配符形式更为简洁。

通配符的只读性

通配符形式更为简洁,但上面两种通配符都有一个重要的限制,只能读,不能写。

怎么理解呢?看下面例子:

DynamicArray<Integer> ints = new DynamicArray<>();
DynamicArray<? extends Number> numbers = ints;
Integer a = 200;
numbers.add(a);
numbers.add((Number)a);
numbers.add((Object)a);

三种add方法都是非法的,无论是Integer,还是Number或Object,编译器都会报错。为什么呢?

?就是表示类型安全无知,? extends Number表示是Number的某个子类型,但不知道具体子类型,如果允许写入,Java就无法确保类型安全性,所以干脆禁止。我们来看个例子,看看如果允许写入会发生什么:

DynamicArray<Integer> ints = new DynamicArray<>();
DynamicArray<? extends Number> numbers = ints;
Number n = new Double(23.0);
Object o = new String("hello world");
numbers.add(n);
numbers.add(o);

如果允许写入Object或Number类型,则最后两行编译就是正确的,也就是说,Java将允许把Double或String对象放入Integer容器,这显然就违背了Java关于类型安全的承诺。

大部分情况下,这种限制是好的,但这使得一些理应正确的基本操作都无法完成,比如交换两个元素的位置,看代码:

public static void swap(DynamicArray<?> arr, int i, int j){
Object tmp = arr.get(i);
arr.set(i, arr.get(j));
arr.set(j, tmp);
}

这个代码看上去应该是正确的,但Java会提示编译错误,两行set语句都是非法的。不过,借助带类型参数的泛型方法,这个问题可以这样解决:

private static <T> void swapInternal(DynamicArray<T> arr, int i, int j){
T tmp = arr.get(i);
arr.set(i, arr.get(j));
arr.set(j, tmp);
} public static void swap(DynamicArray<?> arr, int i, int j){
swapInternal(arr, i, j);
}

swap可以调用swapInternal,而带类型参数的swapInternal可以写入。Java容器类中就有类似这样的用法,公共的API是通配符形式,形式更简单,但内部调用带类型参数的方法。

参数类型间的依赖关系

除了这种需要写的场合,如果参数类型之间有依赖关系,也只能用类型参数,比如说,看下面代码,将src容器中的内容拷贝到dest中:

public static <D,S extends D> void copy(DynamicArray<D> dest,
DynamicArray<S> src){
for(int i=0; i<src.size(); i++){
dest.add(src.get(i));
}
}

S和D有依赖关系,要么相同,要么S是D的子类,否则类型不兼容,有编译错误。不过,上面的声明可以使用通配符简化一下,两个参数可以简化为一个,如下所示:

public static <D> void copy(DynamicArray<D> dest,
DynamicArray<? extends D> src){
for(int i=0; i<src.size(); i++){
dest.add(src.get(i));
}
}

通配符与返回值

还有,如果返回值依赖于类型参数,也不能用通配符,比如,计算动态数组中的最大值,如下所示:

public static <T extends Comparable<T>> T max(DynamicArray<T> arr){
T max = arr.get(0);
for(int i=1; i<arr.size(); i++){
if(arr.get(i).compareTo(max)>0){
max = arr.get(i);
}
}
return max;
}

上面的代码就难以用通配符代替。

通配符还是类型参数?

现在我们再来看,泛型方法,到底应该用通配符的形式,还是加类型参数?两者到底有什么关系?我们总结下:

  • 通配符形式都可以用类型参数的形式来替代,通配符能做的,用类型参数都能做。
  • 通配符形式可以减少类型参数,形式上往往更为简单,可读性也更好,所以,能用通配符的就用通配符。
  • 如果类型参数之间有依赖关系,或者返回值依赖类型参数,或者需要写操作,则只能用类型参数。
  • 通配符形式和类型参数往往配合使用,比如,上面的copy方法,定义必要的类型参数,使用通配符表达依赖,并接受更广泛的数据类型。

超类型通配符

灵活写入

还有一种通配符,与形式<? extends E>正好相反,它的形式为<? super E>,称之为超类型通配符,表示E的某个父类型,它有什么用呢?有了它,我们就可以更灵活的写入了。

如果没有这种语法,写入会有一些限制,来看个例子,我们给DynamicArray添加一个方法:

public void copyTo(DynamicArray<E> dest){
for(int i=0; i<size; i++){
dest.add(get(i));
}
}

这个方法也很简单,将当前容器中的元素添加到传入的目标容器中。我们可能希望这么使用:

DynamicArray<Integer> ints = new DynamicArray<Integer>();
ints.add(100);
ints.add(34);
DynamicArray<Number> numbers = new DynamicArray<Number>();
ints.copyTo(numbers);

Integer是Number的子类,将Integer对象拷贝入Number容器,这种用法应该是合情合理的,但Java会提示编译错误,理由我们之前也说过了,期望的参数类型是DynamicArray<Integer>,DynamicArray<Number>并不适用。

如之前所说,一般而言,不能将DynamicArray<Integer>看做DynamicArray<Number>,但我们这里的用法是没有问题的,Java解决这个问题的方法就是超类型通配符,可以将copyTo代码改为:

public void copyTo(DynamicArray<? super E> dest){
for(int i=0; i<size; i++){
dest.add(get(i));
}
}

这样,就没有问题了。

灵活比较

超类型通配符另一个常用的场合是Comparable/Comparator接口。同样,我们先来看下,如果不使用,会有什么限制。以前面计算最大值的方法为例,它的方法声明是:

public static <T extends Comparable<T>> T max(DynamicArray<T> arr)

这个声明有什么限制呢?我们举个简单的例子,有两个类Base和Child,Base的代码是:

class Base implements Comparable<Base>{
private int sortOrder; public Base(int sortOrder) {
this.sortOrder = sortOrder;
} @Override
public int compareTo(Base o) {
if(sortOrder < o.sortOrder){
return -1;
}else if(sortOrder > o.sortOrder){
return 1;
}else{
return 0;
}
}
}

Base代码很简单,实现了Comparable接口,根据实例变量sortOrder进行比较。Child代码是:

class Child extends Base {
public Child(int sortOrder) {
super(sortOrder);
}
}

这里,Child非常简单,只是继承了Base。注意,Child没有重新实现Comparable接口,因为Child的比较规则和Base是一样的。我们可能希望使用前面的max方法操作Child容器,如下所示:

DynamicArray<Child> childs = new DynamicArray<Child>();
childs.add(new Child(20));
childs.add(new Child(80));
Child maxChild = max(childs);

遗憾的是,Java会提示编译错误,类型不匹配。为什么不匹配呢?我们可能会认为,Java会将max方法的类型参数T推断为Child类型,但类型T的要求是extends Comparable<T>,而Child并没有实现Comparable<Child>,它实现的是Comparable<Base>。

但我们的需求是合理的,Base类的代码已经有了关于比较所需要的全部数据,它应该可以用于比较Child对象。解决这个问题的方法,就是修改max的方法声明,使用超类型通配符,如下所示:

public static <T extends Comparable<? super T>> T max(DynamicArray<T> arr)

就这么修改一下,就可以了,这种写法比较抽象,将T替换为Child,就是:

Child extends Comparable<? super Child>

<? super Child>可以匹配Base,所以整体就是匹配的。

没有<T super E>

我们比较一下类型参数限定与超类型通配符,类型参数限定只有extends形式,没有super形式,比如说,前面的copyTo方法,它的通配符形式的声明为:

public void copyTo(DynamicArray<? super E> dest)

如果类型参数限定支持super形式,则应该是:

public <T super E> void copyTo(DynamicArray<T> dest)

事实是,Java并不支持这种语法。

前面我们说过,对于有限定的通配符形式<? extends E>,可以用类型参数限定替代,但是对于类似上面的超类型通配符,则无法用类型参数替代。

通配符比较

两种通配符形式<? super E>和<? extends E>也比较容易混淆,我们再来比较下。

  • 它们的目的都是为了使方法接口更为灵活,可以接受更为广泛的类型。
  • <? super E>用于灵活写入或比较,使得对象可以写入父类型的容器,使得父类型的比较方法可以应用于子类对象。
  • <? extends E>用于灵活读取,使得方法可以读取E或E的任意子类型的容器对象。

Java容器类的实现中,有很多这种用法,比如说,Collections中就有如下一些方法:

public static <T extends Comparable<? super T>> void sort(List<T> list)
public static <T> void sort(List<T> list, Comparator<? super T> c)
public static <T> void copy(List<? super T> dest, List<? extends T> src)
public static <T> T max(Collection<? extends T> coll, Comparator<? super T> comp)

通过上节和本节,我们应该可以理解这些方法声明的含义了。

小结

本节介绍了泛型中的三种通配符形式,<?>、<? extends E>和<? super E>,并分析了与类型参数形式的区别和联系。

简单总结来说:

  • <?>和<? extends E>用于实现更为灵活的读取,它们可以用类型参数的形式替代,但通配符形式更为简洁。
  • <? super E>用于实现更为灵活的写入和比较,不能被类型参数形式替代。

关于泛型,还有一些细节以及限制,让我们下节来继续探讨。

----------------

未完待续,查看最新文章,敬请关注微信公众号“老马说编程”(扫描下方二维码),从入门到高级,深入浅出,老马和你一起探索Java编程及计算机技术的本质。用心写作,原创文章,保留所有版权。

Java编程的逻辑 (36) - 泛型 (中) - 解析通配符的更多相关文章

  1. Java编程的逻辑 (35) - 泛型 (上) - 基本概念和原理

    本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http:/ ...

  2. Java编程的逻辑 (37) - 泛型 (下) - 细节和局限性

    本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http:/ ...

  3. Java编程的逻辑 (89) - 正则表达式 (中)

    本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http:/ ...

  4. 《Java编程的逻辑》 - 文章列表

    <计算机程序的思维逻辑>系列文章已整理成书<Java编程的逻辑>,由机械工业出版社出版,2018年1月上市,各大网店有售,敬请关注! 京东自营链接:https://item.j ...

  5. Java编程的逻辑 (6) - 如何从乱码中恢复 (上)?

    ​本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http: ...

  6. Java编程的逻辑 (7) - 如何从乱码中恢复 (下)?

    本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http:/ ...

  7. Java编程的逻辑 (27) - 剖析包装类 (中)

    ​本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http: ...

  8. Java编程的逻辑 (26) - 剖析包装类 (上)

    本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http:/ ...

  9. Java编程的逻辑 (34) - 随机

    本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http:/ ...

随机推荐

  1. BZOJ 1412 [ZJOI2009]狼和羊的故事 | 网络流

    显然是个最小割嘛! 一开始我是这么建图的: 源点向狼连INF 羊向汇点连INF 每两个相邻格子间连双向边,边权为1 然后T成狗 后来我是这么建图的: 源点向狼连INF 羊向汇点连INF 狼和空地向相邻 ...

  2. 【CH4201】楼兰图腾

    题目大意:给定一个长度为 N 的序列,从序列中任意挑出三个数,求满足中间的数字值最小(最大)有多少种情况. 题解:建立在值域上的树状数组,从左到右扫描一遍序列,统计出每个点左边有多少个数大于(小于)该 ...

  3. c++中的 Stl 算法(很乱别看)

    #include <iostream> #include <vector> #include <functional> #include <algorithm ...

  4. LeetCode 9 合并两个有序列表

    将两个有序链表合并为一个新的有序链表并返回.新链表是通过拼接给定的两个链表的所有节点组成的. 示例: 输入:1->2->4, 1->3->4 输出:1->1->2- ...

  5. 包学会之浅入浅出Vue.js:开学篇(转)

    包学会之浅入浅出Vue.js:开学篇 蔡述雄,现腾讯用户体验设计部QQ空间高级UI工程师.智图图片优化系统首席工程师,曾参与<众妙之门>书籍的翻译工作.目前专注前端图片优化与新技术的探研. ...

  6. JSON.parseObject(String str)与JSONObject.parseObject(String str)的区别

    一.首先来说说fastjson fastjson 是一个性能很好的 Java 语言实现的 JSON 解析器和生成器,来自阿里巴巴的工程师开发.其主要特点是: ① 快速:fastjson采用独创的算法, ...

  7. Python模块调用方式详解

    Python模块调用方式详解 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 模块,用一砣代码实现了某个功能的代码集合. 类似于函数式编程和面向过程编程,函数式编程则完成一个功能,其 ...

  8. SQL语句(十二)分组查询

    (十二)分组查询 将数据表中的数据按某种条件分成组,按组显示统计信息 查询各班学生的最大年龄.最小年龄.平均年龄和人数 分组 SELECT <字段名表1> FROM <表名> ...

  9. Java SSM框架之MyBatis3(四)MyBatis之一对一、一对多、多对多

    项目搭建Springboot 1.5  pom.xml <?xml version="1.0" encoding="UTF-8"?> <pro ...

  10. Passbook

    CHENYILONG Blog Passbook 技术博客http://www.cnblogs.com/ChenYilong/ 新浪微博http://weibo.com/luohanchenyilon ...