如第25条所述,参数化类型是 不可变的(invariant)。换句话说,对于任何两个截然不同的类型tyle1和type2来说,List<Type1>既不是List<Type2>的子类型,也不是他的超类型。虽然List<String>不是List<Object>的子类型,这与直觉相悖,但是实际上很有意义。你可以将任何对象放进一个List<Object>中,却只能将字符串放进<String>中。
有时候,我需要的灵活性要比不可变类型所能提供的更多。考虑第26条中的堆栈下面就是他的公共API:
public class Stack<E> {
public Stack();
public void push(E e);
public E pop();
public boolean isEmpty();
}
假设我们想要增加一个方法,让她按照顺序将一系列的元素全部放到堆栈中。这是第一次尝试,如下:
// pushAll method without wildcard type - deficient;
public void pushAll(Iterable<E> src) {
for (E e: src)
push(e);
}
这个方法编译的时候正确无误,但是并非尽如人意,如果Iterable src的元素类型与堆栈的完全匹配,那就没有问题,但是假如有一个Stack<Number>,并且调用了push(intVal),这里的intVal就是Integer类型。这是可以的,因为Integer是Number的一个子类型,因此从逻辑上来说,下面这断代码应该是可行的:
Stack<Number> numberStack = new Stack<Number>();
Iterable<Integer> integers = ...;
numberStack.pushAll(integers);
但是实际上运行这段代码会得到错误,错误如下:
P1.png原因在于Iterable<Integer>并不是Iterable<Number>的子类型(参数化类型是不可变的,相应的概念为,数组是协变的),幸运 的是Java提供了一种解决方法,称为有限制的通配符类型来处理这种情况。使用有限制的通配符Iterable<? extends E>即可解决这个问题(注意,确定了子类型后,第一个类型便都是自身的子类型),修改后的程序如下:
// Wildcard type for parameter that serves as an E producer
public void pushAll(Iterable<? extends E> src) {
for (E e: src)
push(e);
}
这么修改了之后,不仅Stack可以正确无误的编辑,没有通过初试的pushAll声明进行编译的客户端代码也一样可以,因为Stack及其客户端正无误的进行了编译,你就知道一切都是类型安全的了。
对应的,假如我们要编译一个popAll方法,初次尝试如下:
// popAll method without wildcart type - deficient;
public void popAll(Collection<E> dst) {
while(!isEmpty())
dst.add(pop());
}
如果目标集合的元素类型与堆栈完全匹配,这段代码编译时还是会正确无误的。运行得很好,但是,也并不意味着尽如人意。假设你有一个Stack<Number>和类型Object变量,如果从堆栈中弹出一个元素,并将它保存在该变量中,它的编译和运行都不会出错,考虑如下代码 :
Stack<Number> numberStack = new Stack<Number>();
Collection<Object> objects = ...;
numberStack.popAll(objects);
运行这段代码会得到一个与pushAll第一种情况类似的错误:
P2.png
对于这种情况java同样提供了一种对应的有限制通配符来解决,popAll的输入参数类型不应该为“E的集合”,而应该为“E的某种超类的集合”。通配符:Collection<? super E>,根据这种方法修改后的代码如下:
public void popAll(Collection<? super E> dst) {
while(!isEmpty())
dst.add(pop());
}
由上面这两种情况可以看出,有限制的通配符类型放宽了检查的类型,为了获得最大限度的灵活性,要在表示生产者或者消费者的输入参数上使用通配符类型。如果某个输入参数既是生产者,又是消费者,那么通配符类型就没有什么好处了,因为需要的是严格的类型匹配,这是不用任何通配符而得到的。
下面的助记符便于让你记住要使用哪种通配符类型类型:
**PESC表示producter-extends, consumer-super. **
如果参数化类型表示一个T生产者,就使用<? extends T>;如果它表示一个T消费者,就使用<? super T>。
在我们的Stack实例中,pushAll的src参数产生E实例供Stack使用,因此src相应的类型为Iterable<? extends E>;popAll的dst参数通过Stack消费E实例,因此dst的相应类型为Collection<? super E>。PECS这个助记符突出了使用通配符类型的基本原则。Naftalin和wadler称之为Get and Put Principle。
下面是第25条中的reduce方法就有这条声明:
static <E> E reduce(List<E> list, Function<E> f, E initVal){
E[] snapshot = (E[])list.toArray();
E result = initVal;
for (E e:snapshot) {
result = f.apply(result, e);
}
return result;
}
虽然列表既可以消费也可以是、产生值,reduce方法还是只用他的list啊、参数作为E生产者,因此他得声明就应该使用一个extends E得通配符类型。参数f表示既可以消费又可以产生E实例的函数,因此通配符类型不适合他,得到的声明如下:
static <E> E reduce(List<? extends E> list, Function<E> f, E initVal);
假设有一个List<Integer>,想通过Function<Number>把他简化。他不能通过初始声明进行编译,但是一旦添加了有限制的通配符类型就可以了。
27条中的union方法:
public static <E> Set<E> union(Set<E> s1, Set<E> s2){
Set<E> result = new HashSet<E>(s1);
result.addAll(s2);
return result;
}
试想一下如下调用:
Set<Integer> integers = ...;
Set<Double> doubles = ...;
Set<Number> numbers = union(integers, doubles);
重写为:
public static <E> Set<E> union(Set<? extends E> s1, Set<? extends E> s2){
Set<E> result = new HashSet<E>(s1);
result.addAll(s2);
return result;
}
但是实际上,这段代码中的union方法,编译器推断不出需要返回什么类型,所以依然会编译报错(1.8不报错),但是可以显示指明这一点。
Set<Number> numbers = Union.<Number>union(integers, doubles);
接下来再看27条的max方法:
public static <T extends Comparable<T>> T max(List<T> list){
Iterator<T> i = list.iterator();
T result = i.next();
while (i.hasNext()){
T t = i.next();
if (t.compareTo(result) > 0){
result = t;
}
}
return result;
}
修改过后:
public static <T extends Comparable<? super T>> T max(List<T> list){
Iterator<T> i = list.iterator();
T result = i.next();
while (i.hasNext()){
T t = i.next();
if (t.compareTo(result) > 0){
result = t;
}
}
return result;
}
那么上面这样复杂的修改真的有用吗?实际上的确是有用的。
List<ScheduledFuture<?>> scheduledFutures = new ArrayList<>();
在没有使用有限制通配符时,如果将scheduledFutures 作为参数传给max方法,会得到如下报错:
P4.png由于ScheduledFuture没有实现Comparable<ScheduledFuture>借口,相反,他是扩展Comparable<Delayed>接口的Delayed接口的子接口。
在使用了有限制的通配符之后就可以进行比较了。
public interface ScheduledFuture<V> extends Delayed, Future<V> {}
public interface Delayed extends Comparable<Delayed> {}
但是你以为这样就完了吗?图样图森破!这段代码实际上是会编译报错的。
P5.png他意味着list不是一个List<T>,因此它的iterator方法没有返回Iterator<T>。他返回的是T的某个子类型的一个iterator,因此要对iterator进行修改:
public static <T extends Comparable<? super T>> T max(List<T> list){
Iterator<? extends T> i = list.iterator();
T result = i.next();
while (i.hasNext()){
T t = i.next();
if (t.compareTo(result) > 0){
result = t;
}
}
return result;
}
这样迭代器的next方法返回的元素属于T的某个子类型,因此让门可以被安全的保存在类型T的一个变量中。
还有一个与通配符有关的话题值得探讨。类型参数和通配符之间具有双重性,许多方法都可以利用其中一个或者另一个进行声明。例如,下面是可能的两种静态方法声明,来交换列表中的两个被索引的项目。第一个使用无限制的类型参数,第二个使用无限制的通配符。
// Two possible declarations for the swap method
public static <E> void swap(List<E> list, int i, int j);
public static void swap(List<?> list, int i, int j);
在公共API中第二种方法更好一些,因为它更简单。一般来说,如果类型参数只在方法声明中出现一次,就可以用通配符取代它。如果是无限制的类型参数就用无限制的通配符来取代,如果 是有限制的类型参数,就用有限制的通配符来取代。但是,第二种方法存在一个问题,它优先使用通配符而非类型参数,下面的简单实现都实现不了:
public static void swap(List<?> list, int i, int j) {
list.set(i, list.set(j, list.get(i)));
}
编译错误:
P6.png
书上的错误:
P7.png
这段代码编译时会出错,我们干了什么?取出元素再放回到表中,为什么这不成功呢?因为list是无限通配符类型List<?>,以前说过,除了null以外的任何对象都无法放入其中。幸运的是,有一种方式可以实现第二种方法,无需求助不安全的转换或者原始类型。这种想法 就是编写一个私有辅助方法来捕捉通配符类型。为了捕捉类型,辅助方法必须是泛型方法,因为并不知道通配符代表的具体类型。如下:
public static void swap(List<?> list, int i, int j) {
swapHelper(list, i, j);
}
// Private helper method for wildcard capture
private static <E> void swapHelper(List<E> list, int i, int j) {
list.set(i, list.set(j, list.get(i)));
}
swapHelper方法知道list是List<E>。因此,他知道从这个列表中取出的任何值均为E类型,并知道将E类型的任何值放进列表都是安全的。
总之,在API中使用通配符类型虽然比较需要技巧,但是使API变得灵活的多,如果在写的是一个将被广泛使用的类库,则一定要适当地利用通配符类型,记住基本的原则:PECS,还要记得所有的comparable和comparator都是消费者。
网友评论