本系列文章经补充和完善,已修订整理成书《Java编程的逻辑》(马俊昌著),由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买:京东自营连接 html
![]()
上节咱们介绍了泛型的基本概念和原理,本节继续讨论泛型,主要讨论泛型中的通配符概念。通配符有着使人费解和混淆的语法,但通配符大量应用于Java容器类中,它究竟是什么?本节,让咱们逐步来解析。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>
。ide
<T extends E>
与<? extends E>
那么问题来了,一样是extends关键字,一样应用于泛型,<T extends E>
和<? extends E>
到底有什么关系?this
它们用的地方不同,咱们解释一下:spa
<T extends E>
用于定义类型参数,它声明了一个类型参数T,可放在泛型类定义中类名后面、泛型方法返回值前面。<? extends E>
用于实例化类型参数,它用于实例化泛型变量中的类型参数,只是这个具体类型是未知的,只知道它是E或E的某个子类型。虽然它们不同,但两种写法常常能够达成相同目标,好比,前面例子中,下面两种写法均可以:3d
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;
}
复制代码
上面的代码就难以用通配符代替。
如今咱们再来看,泛型方法,到底应该用通配符的形式,仍是加类型参数?二者到底有什么关系?咱们总结下:
还有一种通配符,与形式<? 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编程及计算机技术的本质。用心原创,保留全部版权。