Effective Java笔记第四章泛型第六节利用有限制通配符来提升API的灵活性

Effective Java笔记第四章泛型

第六节利用有限制通配符来提升API的灵活性

1.参数化类型是不可变得,对于任何两个截然不同的类型Type1和Type2而言,List< Type1 >既不是List< Type2 >的子类型,也不是他的超类型。

2.有时候,我们需要的灵活性要比不可变类型所能提供的更多,比如说:

public class Stack<E> {

    private E[] element;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 1;

    public Stack(){

    }

    public void push(E e) {
        ensureCapacity();
        element[size++] = e;
    }

    public E pop() {
        if (size == 0) {
            throw new EmptyStackException();
        }
        E result = element[--size];
        element[size] = null;
        return result;
    }

    public boolean isEmpty() {
        return size == 0;
    }

    private void ensureCapacity() {
        if (element.length == size) {
            element = Arrays.copyOf(element, 2 * size + 1);
        }
    }

}

假设我们需要增加一个方法,让它按顺序将一系类元素都放到堆栈中:

  public void pushAll (Iterable<E> src){
        for (E e : src) {
            push(e);
        }
    }
    
    public static void main(String[] args) {
        Stack<Number> stack=new Stack<>();
        Iterable<Integer> integers=new IdentityArrayList<>();
        //编译报错,参数化类型不可变,虽然Integer是Number的子类,但是Iterable<Integer>和Iterable<Number>没有关系
        stack.pushAll(integers);
    }

java提供了一种特殊的参数化类型,称作有限制的通配符类型,来处理类似的情况。pushAll的输入参数不应该为"E的Iterable接口",而应该是"E的某个子类型的Iterable接口",有一个通配符类型正符合此意;Interable< ? extends E >。(确定了子类型后,每个类型便都是自身的子类型,即使它没有将自身扩展)。我们修改一下上面的代码:

 public void pushAllImprove (Iterable<? extends E> src){
        for (E e : src) {
            push(e);
        }
    }
    
   public static void main(String[] args) {
        Stack<Number> stack=new Stack<>();
        Iterable<Integer> integers=new IdentityArrayList<>();
        //我们可以使用Number的子类型
        stack.pushAllImprove(integers);
    }

假设要编写一个popAll方法,把从堆里弹出的每个元素添加到指定的集合中:

 public void popAll(Collection<E> dst){
        while (!isEmpty()){
            dst.add(pop());
        }
    }
    
    public static void main(String[] args) {
        Stack<Number> stack=new Stack<>();
        Iterable<Integer> integers=new IdentityArrayList<>();
        Collection<Object> object=new HashSet<>();
        //编译报错
        stack.popAll(objects);
    }

popAll的输入参数类型不应该为"E的集合",而应该为"E的某种超类的集合"(这里的超类是确定的,因此E是它自身的一个超类型,有一个通配符类型正好符合此意:Collection< ? super E >),我们对上面的代码进行修改:

public void popAllImprove(Collection<? super E> dst){
        while (!isEmpty()){
            dst.add(pop());
        }
    }
    
  public static void main(String[] args) {
        Stack<Number> stack=new Stack<>();
        Iterable<Integer> integers=new IdentityArrayList<>();
        Collection<Object> object=new HashSet<>();
        //正常运行
        stack.popAllImprove(object);
    }

为了获得最大限度的灵活性,要在表示生产者或者消费者的输入参数上使用通配符类型。如果某个输入参数既是生产者,又是消费者,那么通配符类型就没什么好处了:因为你需要的是严格的类型匹配,这是不用任何通配符而得到的。

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

如果参数化类型表示一个T生产者,就使用< ? extends E >;如果表示一个消费者,就使用<? super E> 。比如:pushAll的src参数产生E实例供Stack使用,因此src相应的类型为Iterable< ? extends E >;popAll的dst参数通过Stack消费E实例,因此dst相应的类型为Collection< ? super E >。

不要用通配符类型作为返回类型。除了为用户提供额外的灵活性之外,他还会强制用户在客户端代码中使用通配符类型。

如果使用得当,通配符类型对于类的用户来说几乎是无形的。他们使方法能够接受他们应该接口的参数,并拒绝那些应该拒绝的参数。如果类的用户必须考虑通配符类型,类的API或许就会出错。

3.下面我们先举个例子:

    public static <T extends Comparable<T>> T max(List<T> list) {
        Iterator<T> i = list.iterator();
        System.out.println("i="+i);
        T result = i.next();
        System.out.println("result="+result);
        while (i.hasNext()) {
            T t = i.next();
            System.out.println("t="+t);
            if (t.compareTo(result) > 0) {
                result = t;
            }
        }
        return result;
    }
    
	public static void main(String[] args) {
        List<ScheduledFuture<?>> list=new ArrayList<>();
        //ScheduledFuture没有实现Comparable<ScheduledFuture>接口,
        // public interface ScheduledFuture<V> extends Delayed, Future<V> {} 它扩展了Comparable<Delayed>接口的Delayed子接口
        max(list);
    }

下面我们对上面的代码进行修改:

 public static <T extends Comparable<? super T>> T maxImprove(List<? extends T> list) {
        Iterator<? extends T> i = list.iterator();
        System.out.println("i="+i);
        T result = i.next();
        System.out.println("result="+result);
        while (i.hasNext()) {
            T t = i.next();
            System.out.println("t="+t);
            if (t.compareTo(result) > 0) {
                result = t;
            }
        }
        return result;
    }
    
    public static void main(String[] args) 
        List<ScheduledFuture<?>> list=new ArrayList<>();
        maxImprove(list);
    }

最直接的运用参数list,它产生T实例,因此将类型从头List< T >改成List< ? extends T >。更灵活的运用是类型参数T,最初T被指定用来扩展Comparable< T >,但是T的comparable消费T实例。因此参数化类型Comparable< T >被修改为有限制通配符类型Comparable< ? super T >。comparable始终是消费者,因此使用时始终应该是Comparable< ? super T >优先于Comparable< T >。对于comparator也一样,因此使用时始终应该是Comparator< ? super T >优先于Comparator< T >。

4.类型参数和通配符之间具有双重性,许多方法都可以利用其中一个或者另一个进行声明。比如说下面:
1)使用无限制的类型参数:

public static <E> void swap(List<E> list, int i, int j) {
    }

2)使用无限制的通配符:

public static void swap2(List<?> list, int i, int j) {
    }

在公共API中,第二种更好一些,因为它更简单。将它传到一个列表中——任何列表——方法就会交换被索引的元素。不用担心类型参数。一般来说,如果类型参数只在方法声明中出现一次,就可以用通配符取代他。如果是无限制的类型参数,就用无限制的通配符取代他;如果是有限制的类型参数,就用有限制的通配符取代他。

不过使用无限制的通配符会有一个问题,他会优先使用通配符而非类型参数,遇到下面这种情况就不能编译:

  //你不能把null之外的任何值放到List<?>中
    public static void test(List<?> list,int i,int j){
        list.set(i,list.set(j,list.get(i)));
    }

对此我们有一种方法:编写一个私有的辅助方法来捕捉通配符类型。为了捕捉类型,辅助方法必须是泛型方法:

  public static void test(List<?> list, int i, int j) {
        testHelper(list,i,j);
    }

	private static <E> void testHelper(List<E> list, int i, int j) {
	      list.set(i, list.set(j, list.get(i)));
	  }

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

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

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值