美文网首页Effective Java
第28条:利用有限制通配符来提升API灵活性

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

作者: NekoJiang | 来源:发表于2017-06-05 23:48 被阅读0次

    如第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都是消费者。

    相关文章

      网友评论

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

        本文链接:https://www.haomeiwen.com/subject/yhyifxtx.html