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

如第25条所述,参数化类型是 不可变的(invariant)。换句话说,对于任何两个截然不同的类型tyle1和type2来说,List既不是List的子类型,也不是他的超类型。虽然List不是List的子类型,这与直觉相悖,但是实际上很有意义。你可以将任何对象放进一个List中,却只能将字符串放进中。

有时候,我需要的灵活性要比不可变类型所能提供的更多。考虑第26条中的堆栈下面就是他的公共API:

public class Stack {
    public Stack();
    public void push(E e);
    public E pop();
    public boolean isEmpty();
}

假设我们想要增加一个方法,让她按照顺序将一系列的元素全部放到堆栈中。这是第一次尝试,如下:

// pushAll method without wildcard type - deficient;  
public void pushAll(Iterable src) {  
    for (E e: src)  
        push(e);  
}  

这个方法编译的时候正确无误,但是并非尽如人意,如果Iterable src的元素类型与堆栈的完全匹配,那就没有问题,但是假如有一个Stack,并且调用了push(intVal),这里的intVal就是Integer类型。这是可以的,因为Integer是Number的一个子类型,因此从逻辑上来说,下面这断代码应该是可行的:

Stack numberStack = new Stack();  
Iterable integers = ...;  
numberStack.pushAll(integers);  

但是实际上运行这段代码会得到错误,错误如下:

第28条:利用有限制通配符来提升API灵活性_第1张图片
P1.png

原因在于Iterable并不是Iterable的子类型(参数化类型是不可变的,相应的概念为,数组是协变的),幸运 的是Java提供了一种解决方法,称为有限制的通配符类型来处理这种情况。使用有限制的通配符Iterable即可解决这个问题(注意,确定了子类型后,第一个类型便都是自身的子类型),修改后的程序如下:

// Wildcard type for parameter that serves as an E producer  
public void pushAll(Iterable src) {  
    for (E e: src)  
        push(e);  
} 

这么修改了之后,不仅Stack可以正确无误的编辑,没有通过初试的pushAll声明进行编译的客户端代码也一样可以,因为Stack及其客户端正无误的进行了编译,你就知道一切都是类型安全的了。

对应的,假如我们要编译一个popAll方法,初次尝试如下:

// popAll method without wildcart type - deficient;  
public void popAll(Collection dst) {  
    while(!isEmpty())  
        dst.add(pop());  
}  

如果目标集合的元素类型与堆栈完全匹配,这段代码编译时还是会正确无误的。运行得很好,但是,也并不意味着尽如人意。假设你有一个Stack和类型Object变量,如果从堆栈中弹出一个元素,并将它保存在该变量中,它的编译和运行都不会出错,考虑如下代码 :

Stack numberStack = new Stack();  
Collection objects = ...;  
numberStack.popAll(objects);  
 
 

运行这段代码会得到一个与pushAll第一种情况类似的错误:


第28条:利用有限制通配符来提升API灵活性_第2张图片
P2.png

对于这种情况java同样提供了一种对应的有限制通配符来解决,popAll的输入参数类型不应该为“E的集合”,而应该为“E的某种超类的集合”。通配符:Collection,根据这种方法修改后的代码如下:

public void popAll(Collection dst) {  
    while(!isEmpty())  
        dst.add(pop());  
}  

由上面这两种情况可以看出,有限制的通配符类型放宽了检查的类型,为了获得最大限度的灵活性,要在表示生产者或者消费者的输入参数上使用通配符类型。如果某个输入参数既是生产者,又是消费者,那么通配符类型就没有什么好处了,因为需要的是严格的类型匹配,这是不用任何通配符而得到的。

下面的助记符便于让你记住要使用哪种通配符类型类型:
**PESC表示producter-extends, consumer-super. **

如果参数化类型表示一个T生产者,就使用;如果它表示一个T消费者,就使用
在我们的Stack实例中,pushAll的src参数产生E实例供Stack使用,因此src相应的类型为Iterable;popAll的dst参数通过Stack消费E实例,因此dst的相应类型为Collection。PECS这个助记符突出了使用通配符类型的基本原则。Naftalin和wadler称之为Get and Put Principle
下面是第25条中的reduce方法就有这条声明:

static  E reduce(List list, Function 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 reduce(List list, Function f, E initVal);

假设有一个List,想通过Function把他简化。他不能通过初始声明进行编译,但是一旦添加了有限制的通配符类型就可以了。

27条中的union方法:

public static  Set union(Set s1, Set s2){
        Set result = new HashSet(s1);
        result.addAll(s2);
        return  result;
    }

试想一下如下调用:

Set integers = ...;  
Set doubles = ...;  
Set numbers = union(integers, doubles);  

重写为:

public static  Set union(Set s1, Set s2){
        Set result = new HashSet(s1);
        result.addAll(s2);
        return  result;
    }

但是实际上,这段代码中的union方法,编译器推断不出需要返回什么类型,所以依然会编译报错(1.8不报错),但是可以显示指明这一点。

Set numbers = Union.union(integers, doubles);  

接下来再看27条的max方法:

public static > T max(List list){
        Iterator 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 max(List list){
       Iterator i = list.iterator();
       T result = i.next();
       while (i.hasNext()){
           T t = i.next();
           if (t.compareTo(result) > 0){
               result = t;
           }
       }
       return result;
   }

那么上面这样复杂的修改真的有用吗?实际上的确是有用的。

List> scheduledFutures = new ArrayList<>();

在没有使用有限制通配符时,如果将scheduledFutures 作为参数传给max方法,会得到如下报错:

第28条:利用有限制通配符来提升API灵活性_第3张图片
P4.png

由于ScheduledFuture没有实现Comparable借口,相反,他是扩展Comparable接口的Delayed接口的子接口。
在使用了有限制的通配符之后就可以进行比较了。

public interface ScheduledFuture extends Delayed, Future {}
public interface Delayed extends Comparable {}

但是你以为这样就完了吗?图样图森破!这段代码实际上是会编译报错的。

第28条:利用有限制通配符来提升API灵活性_第4张图片
P5.png

他意味着list不是一个List,因此它的iterator方法没有返回Iterator。他返回的是T的某个子类型的一个iterator,因此要对iterator进行修改:

public static > T max(List list){
       Iterator 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  void swap(List 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)));  
} 

编译错误:


第28条:利用有限制通配符来提升API灵活性_第5张图片
P6.png

书上的错误:


第28条:利用有限制通配符来提升API灵活性_第6张图片
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  void swapHelper(List list, int i, int j) {  
    list.set(i, list.set(j, list.get(i)));  
}  

swapHelper方法知道list是List。因此,他知道从这个列表中取出的任何值均为E类型,并知道将E类型的任何值放进列表都是安全的。

总之,在API中使用通配符类型虽然比较需要技巧,但是使API变得灵活的多,如果在写的是一个将被广泛使用的类库,则一定要适当地利用通配符类型,记住基本的原则:PECS,还要记得所有的comparable和comparator都是消费者。

你可能感兴趣的:(第28条:利用有限制通配符来提升API灵活性)