如第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);

但是实际上运行这段代码会得到下面的错误,因为如前所述,参数化类型是不可变的:

StackTest.java:7:pushAll(Iterable<Number>) in Stack<Number>
cannot be applied to (Iterable<Inteage>)numberStack.pushAll(integers);

幸运的是Java提供了一种解决方法,称为有限制的通配符类型,来处理类似的情况。pushALL的输入参数类型不应该为"E的Iterable接口",而应该为"E的某个子类型的Iterable接口",有一个通配符类型正符合此意:Iterable<? extends E>,修改pushAll来使用这个类型。

// 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及其客户端正无误的进行了编译,你就知道一切都是类型安全的了。

现在假设想要编写一个pushAll方法,使之与popAll方法相呼应。popAll方法从堆栈中弹出每个元素,并将这些元素添加到指定的集合中。初次尝试编写的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);

仍然有一个通配符类型正式符合此意: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)

虽然列表既可以消费也可以是产生值,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>。不要用通配符类型作为返回类型。除了为用户提供额外的灵活性之外,他还会强制用户在客服端代码中使用统配符类型。

如果使用得当,通配符类型对于类的用户来说几乎是无形的。他们使方法能够接受他们应该接受的参数,并拒绝那些应该拒绝的参数。如果类的用户必须考虑通配符类型,类的API或许就会出错。
遗憾的是,类型推导规则相当复杂。而且他们并非总能完成需要他们完成的工作。看看修改过的union声明,你可能会以为可以向这样编写:

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

但这么做会得到下面的错误消息“

Union.java:14: incompatible types
found : Set<Number & Comparable<? extends Number & Comparable<?>>>
required:Set<Number>Set<Number> numbers=union(intVal,deficient);

使用显示的类型参数来告诉他要使用的类型,可以正确无误的编译:

 Set<Number> numbers=Unioc.<Number>union(intVal,deficient);

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

public static <T extends Comparable<T>> T max(List<T> list)

修改过后:

public static <T extends Comparable<? super T>> T max(List<T> list)

下面是一个简单的列表实例,在初始的声明中不允许这样,修改过的版本则可以:

List<ScheduledFuture<?>> scheduledFutures =...;

在初始的声明中不允许这样? 因为java.util.concurrent.ScheduledFuture没有实现Comparable接口。相反,他是扩展Comparable接口的Delayed接口的子接口。换句话说,ScheduleFuture实例并非只能与其他ScheduledFuture实例相比较;他可以与任何Delayed实例想比较,这就足以导致初始声明时会被拒绝。

修改过的max声明有一个小小的问题:它阻止方法进行编译。羡慕的方法包含了修改过的声明:

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;}

以下是他编译时会产生的错误:

Max.java:7: incompatible types
found : Iterator<capture#591 of extends T>
required:Iterator<T>Iterator<T> i=list.iterator();

这条错误消息意味着list不是一个List< T>,因此它的iterator方法没有返回Iterator< T>,它返回T的某个子类型的一个iterator,因此我们用他代替iterator声明,它使用一个有限责任的通配符类型:

Iterator<? extends T> i=list.iterator();

这是必须对方法体所做的唯一修改。迭代器的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);

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

第二种方法存在一个问题,它优先使用通配符而非类型参数,下面的简单实现都实现不了:

public static void swap(List<?> list, int i, int j) {  list.set(i, list.set(j, list.get(i)));
}

编译时产生的错误:

Swap.java:5 set(int.capture#282 of ?) in List<capture#282 of ?>
cannot be applied to (int,Object)list.set(i,list.set(j,list.get(i)));

不能将元素放回到刚刚从中取出的列表中?原来问题在于list的类型为List<?>,你不能把null之外的任何值放到List<?>。幸运的是,有一种方式可以实现这种方法,无需求助不安全的转换或者原始类型。这种想法 就是编写一个私有辅助方法来捕捉通配符类型。为了捕捉类型,辅助方法必须是泛型方法,像下面一样:

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类型的任何值放进列表都是安全的。

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

Effective Java 泛型 第28条:利用有限制通配符来提升API的灵活性相关推荐

  1. 第二十八条:利用有限制通配符来提升API的灵活性

    如第二十五条所述,参数化类型是不可变的.类型Type1和Type2而言,不管Type1与Type2的关系,List<Type1>既不是List<Type2>的子类型,也不是也不 ...

  2. Effective Java 泛型 第23条:请不要在新代码中使用原生态类型

    java1.5发行版本中增加了泛型.在没有泛型之前,从集合中读取到的每一个对象都必须进行装换.如果有人不小心插入了类型错误的对象,在运行时的装换处理器就会出错.有了泛型之后,可以告诉编译器每个集合中接 ...

  3. Effective Java之利用有限制通配符提升API的灵活性(二十八)

    下面先举出一个泛型栈的例子: class Stack<E>{private List<E> element;private int index;private int defa ...

  4. java泛型学习三:受限制的通配符以及泛型方法

     一.受限制的通配符    package generic; import generic.windcard.Circle; import generic.windcard.Shape; import ...

  5. 读完《Effective Java》后,总结了 50 条开发技巧

    点击上方蓝色"程序猿DD",选择"设为星标" 回复"资源"获取独家整理的学习资料! 作者 | Dong GuoChao 来源 | https ...

  6. 读完 Effective Java,我整理了 59 条技巧!(含pdf)

    点击⬆️方"逆锋起笔",公众号回复 编程资源领取大佬们推荐的学习资料 上一篇:CTO 写低级 Bug,致公司 70 GB 数据泄露! 作者:Dong GuoChao 链接:http ...

  7. 第 3 次读 Effective Java,这 58 个技巧最值!

    点击上方蓝色"方志朋",选择"设为星标" 回复"666"获取独家整理的学习资料! 来源:Dong GuoChao <Effective ...

  8. 《Effective Java》真的是一本值得一直去钻研的好书

    文章目录 <Effective Java>读书笔记 第一章 引言 第二章 创建和销毁对象 1 考虑使用静态工厂方法替代构造方法** 2 当构造方法参数过多时使用builder(建造者) 模 ...

  9. 《Effective Java》阅读笔记(二)

    最近在看<Effective Java>这本书,顺便就记录一些笔记,记录一下书中的一些知识点以及对知识点的总结.一般情况会记录所有的知识点,但是知识点太过简单或者无归纳点总结的就不做详细记 ...

最新文章

  1. Android 使用MD5对SharedPreferences密码进行加密
  2. 【Matlab】滤波器常用命令
  3. [导入]C#面向对象设计模式纵横谈(24):(行为型模式) Visitor 访问者模式.zip(10.41 MB)...
  4. 降价上标(Github风味)?
  5. 再学Android之屏幕适配
  6. 中年男人的唯一出路就是安分守己
  7. aswing学习笔记2-不规则外框-请教思路
  8. 用slub track调试use after free问题
  9. 吴伯凡-认知方法论-如何增加自己认知的维度
  10. 论文阅读--异常检测中实时大数据处理的研究挑战
  11. 为什么游戏需要热更新
  12. Offset commit failed with a retriable exception. You should retry committing the latest consumed off
  13. 2020 IEEE 冯诺依曼奖得主:Michael Jordan --机器学习领域泰斗级人物
  14. Kingbase数据库
  15. java swing+mysql简单零件销售管理系统、订货决策系统
  16. EWM常规开发知识汇总
  17. 《他乡的童年》观后感——英国教育
  18. Can‘t load IA 32-bit .dll on a AMD 64-bit platform
  19. php json数据中 双引号变为quot;解决
  20. leetCode 46:全排列(Javascript 解答)

热门文章

  1. 【笨木头Lua专栏】基础补充04:迭代器初探
  2. 海南计算机的初中学校有哪些,海南海口十大初中排行榜
  3. 十进制转换为十六进制(转载过来的)
  4. 网易放大招?生存MMO手游明日之后开测,沙盒+大逃杀游戏你喜欢吗?
  5. 【海神】各种工作特色比照和介绍
  6. ExpandableTextView——一个可折叠的Textview
  7. 虚拟主机环境WinWebMail邮件系统的常见问题处理
  8. android 角标最新设置方案
  9. 郑州大学“战役杯”第二次比赛题解
  10. Android 工程师进阶手册(8 年 Android 开发者的成长感悟)