第三十一条:使用有限制的通配符增加API的灵活性

参数化类型是不变的 。 换句话说,对于任何两个截然不同的类型 Typel 和 Type2 而言, List<Type1 >既不是 List<Type 2 > 的子类型,也不是它的超类型 。虽然 L ist<String>不是 List<Object>的子类型,这与直觉相悖,但是实际上很有意义 。你可以将任何对象放进一个List<Object>中,却只能将字符串放进 List<String>中 。由于 List<String>不能像 List<Object> 能做任何事情,它不是一个子类型 。

 有时候,我们需要的灵活性要比不变类型所能提供的更多 。比如第 29 条中的stack类 。下面就是它的公共 API:

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

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

// 未使用通配符类型的pullAll方法----存在缺陷
public void pushAll(Iterable<E> src) {
    for(E e : src)
        push(e);
}

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

Stack <Number> numberStack = new Stack<>() ;
Iterable<Integer> integers = ...;
numberStack. pushAll(integers);

但是,如果尝试这么做,就会得到下面的错误消息,因为参数化类型是不可变的:

幸运的是,有一种解决办法 。Java 提供了一种特殊的参数化类型,称作有限制的通配符类型(bounded wildcard type ),它可以处理类似的情况 。pushAll 的输入参数类型不应该为“ E 的 Iterable 接口”,而应该为“ E 的某个子类型的 Iterable 接口”通配符类型Iterable<?extends E >正是这个意思 。 (使用关键字 ex ten也有些误导 :回忆一下第29 条中的说法,确定了子类型( subtype )后,每个类型便都是自身的子类型,即使它没有将自身扩展 。)我们修改一下 pushAll 来使用这个类型:

public void pushAll(Iterable<? extends E> src) {
    for(E e : src)
        push(e);
}

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

现在假设想要编写一个 pushAll 方法,使之与 popAll 方法相呼应 。popAll 方法从堆校中弹出每个元素,并将这些元素添加到指定的集合中 。 初次尝试编写的 popAll 方法可能像下面这样 :

public void popAll(Col1ection<E> dst) {
    while (!isEmpty())
        dst.add(pop());
}

此外,如果目标集合的元素类型与堆栈的完全匹配,这段代码编译时还是会正确无误,并且运行良好 。 但是,也并不意味着尽如人意 。 假设你有一个 Stack<Number >和 Object 类型的变量 。 如果从堆校中弹出 一个元素,并将它保存在该变量中,它的编译和运行都不会出错,那你为何不能也这么做呢?

Stack<Number> numberStack = new Stack<Number>() ;
Collection<Object> objects = ...;
numberStack.popAll(objects) ;

如果试着用上述 的 popAll 版本编译这段客户端代码,就会得到一个非常类似于第一次用 pushAll 时所得到的错误:Collection<Object >不是 Collection<Number>的子类型 。 这一次通配符类型同样提供了一种解决办法 。popAll 的输入参数类型不应该为“ E 的集合”,而应该为“ E 的某种超类的集合”(这里的超类是确定的,因此 E 是它自身的一个超类型)。 仍有一个通配符类型正符合此意:Collection<? super E > 。 让我们修改 popAll 来使用它:

public void popAll(Collection<? super E> dst) {
    while (!isEmpty())
        dst.add(pop();
}

做了 这个变动之后,Stack 和客户端代码就都可以正确无误地编译了 。

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

下面的助记符便于让你记住要使用哪种通配符类型 :

PECS 表示 producer-extends,consumer-super 。

换句话说,如果参数化类型表示一个生产者 T ,就使用<? extends T >;如果它表示一个消 费者 T ,就使用 <? super T > 。 在我们的 Stack 示例中,pushAll 的 src 参数产生 E 实 例供 Stack 使用 ,因 此 src 相 应的类型为 Iterable<? extends E> ; popAll的 dst 参数通过 Stack 消费 E 实例,因此 dst 相应的类型为 Collection<? s uper E > 。PECS 这个助记符突 出了使用通配符类型的基本原则 。Naftalin 和 Wadler 称之为 Get αnd Put Principle。(这个公式很重要)

我们再来看下之前提到过的方法和构造器声明。第二十八条中的chooder类的构造器声明如下:

public Chooser(Collection<T> choices)

这个构造器只用 choices 集合来生成类型 T 的值(并把它们保存起来供后续使用),因此它的声明应该使用一个 extends T 的通配符类型。得到的构造器声明如下:

// 参数被用作T类型的生产者时,使用的通配符类型

public Chooser(Collection<? extends T> choices)

这一变化实际上有什么区别吗?事实上,的确有区别。假设你有一个List<Integer>,

想通过Function<Number>把它简化。它不能通过初始声明进行编译,但是一旦添加了有限制的通配符类型,就可以进行编译了。现在让我们看看第 30 条中的 union 方法。声明如下:

public static <E> Set<E> union(Set<E> s1, Set<E> s2)

s1 和 s2 这两个参数都是生产者 E,因此根据 PECS 助记符,这个声明应该是:

public static <E> Set<E> union(Set<? extends E> s1,Set<? extends E> s2)

注意返回类型仍然是 Set<E>。不要用通配符类型作为返回类型。除了为用户提供额外的灵活性之外,它还会强制用户在客户端代码中使用通配符类型。修改了声明之后,这段代码就能正确编译了:

Set<Integer>    integers = Set.of(1, 3, 5);

Set<Double>    doubies = Set.of(2.0, 4.0, 6.0);

Set<Number>    numbers = union(integers, doubles);

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

一般来说, 如果类型参数只在方法声明中出现一次,就可以用通配符取代它 。 如果是无限制的类型参数,就用无限制的通配符取代它;如果是有限制的类型参数,就用有限制的通配符取代它。

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

 所有文章无条件开放,顺手点个赞不为过吧!

                                                        

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值