您的位置:首页 > 其它

第28条:利用有限制通配符来提升API的灵活性

2014-07-21 17:03 573 查看
术语 :

考虑提供如下API的泛型stack的例子:

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);
但是实际上运行这段代码会得到错误,原因在于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);
}
对应的,假如我们要编译一个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第一种情况类似的错误,对于这种情况java提供了一种对应的有限制通配符来解决,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>。如果使用得当,通配符类型对于类的用户来说几乎是无形的。它们使方法能够接受它们应该接受的参数,并拒绝那些应该拒绝的参数。如果类的用户必须考虑通配符类型,类的API也许就会出错。

依据PESC原则,将union方法修改如下:

public static <E> Set<E> union(Set<? extends E> s1,
Set<? extends E> s2)
注意返回类型是Set<E>。不要用通配符类型作为返回值。用上面的union方法,如果写出如下代码就会出错:

Set<Integer> integers = ...;
Set<Double> doubles = ...;
Set<Number> numbers = union(integers, doubles);
这段代码中的union方法,编译器推断不出需要返回什么类型,但是可以显示指明这一点。

Set<Number> numbers = Union.<Number>union(integers, doubles);
第27条中有一个对max方法的声明如下:

public static <E extends Comparable<T>> T max(List<T> list)
根据PESC原则修改后的max方法如下:

public static <E extends Comparable<? extends T>> T max(List<? extends T> list)
修改过的max方法还是有一个小小的问题,考虑如下代码 :

// Won't compile - wildcard can require change in method body!
public static <T extends Comparable<? extends T>> max(
List<? extends T> list) {
Iterator<T> i = list.iterator();
T result = i.next();
while (i.hashNext()) {
T t = i.next();
if (T.compareTo(result) > 0)
result = t;
}
return result;
}
这段段代码会报错,意味着list.iterator没有返回Iterator<T>,因为它返回了T的一个子类型,知道了错误的原因,那就使用T的一个子类型来修改返回类型,代码如下

// Won't compile - wildcard can require change in method body!
public static <T extends Comparable<? extends T>> max(
List<? extends T> list) {
Iterator<? extends T> i = list.iterator();
T result = i.next();
while (i.hashNext()) {
T t = i.next();
if (T.compareTo(result) > 0)
result = t;
}
return result;
}
还有一个与通配符有关的话题值得探讨。类型参数和通配符之间具有双重性,许多方法都可以利用其中一个或者另一个进行声明。例如,下面是可能的两种静态方法声明,来交换列表中的两个被索引的项目。第一个使用无限制的类型参数,第二个使用无限制的通配符。

// 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)));
}
这段代码编译时会出错,我们干了什么?取出元素再放回到表中,为什么这不成功呢?因为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)));
}
通过这样的方法可以允许我们导出swap这个比较好的基于通配符的声明,同时在内部利用更加复杂的泛型方法来实现。

总之,在API中使用通配符类型虽然比较需要技巧,但是使API变得灵活的多,如果在写的是一个将被广泛使用的类库,则一定要适当地利用通配符类型,记住基本的原则:PECS,还要记得所有的comparable和comparator都是消费者。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐