利用有限制通配符来提升API的灵活性。
来源:互联网 发布:全友家私 知乎 编辑:程序博客网 时间:2024/06/01 08:21
参数化类型是不可变的(invariant)。换句话说,对于任何两个截然不同的类型Type1和Type2而言,List<Type1>既不是List<Type2>的子类型,也不是它的超类型。虽然List<String>不是List<Object>的子类型,这与直觉相挬,但是实际上很有意义。你可以将任何对象放进一个List<Object>中,却只能将字符串放进List<String>中。
有时候,我们需要的灵活性要比不可变类型所能提供的更多。考虑堆栈下面就是他的公共API:
public class Stack<E> {
public Stack();
public void push(E e);
public E pop();
public boolean isEmpty();
}
假设我们想要增加一个方法,让他按顺序将一系列的元素全部放到堆栈中。这是第一次尝试,如下:
// pushAll method without wildcard type - deficident!
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<Integer>) numberStack.pushAll(integers);
幸运的是,有一种解决方法。Java提供了一种特殊的参数化类型,称作有限制的通配符类型(bounded wildcard type),来处理类似的情况。pushAll的输入参数类型不应该为“E的Iterable接口”,而应该为“E的某个子类型的Iterable接口”,有一个通配符类型正符合此意:Iterable<? Extends E>。(使用关键字extends 有些误导:确定了子类型(subtype后,每个类型便都是自身的子类型,即便他没有将自身扩展。))我们修改一下pushAll来使用这个类型:
// wildcard type for parameter that serves as an E producer
public void pushAll(Iterable<? extend E> src) {
for (E e :src) push(e);
}
这么修改了之后,不仅Stack可以正确无误的编译,没有通过初始的pushAll声明进行编译的客户端代码也一样可以。因为Stack及其客户端正确无误的进行了编译,你就知道一切都是类型安全的了。
现在假设想要编写一个pushAll方法,使之与popAll方法相呼应。popAll方法从堆栈中弹出每个元素,并将这些元素添加到指定的集合中。初次尝试编写的popAll方法可能像下面这样:
// popAll method without wildcard 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);
如果试着用上述的popAll版本编译这段客户端代码,就会得到一个非常类似于第一次用pushAll时所得到的错误:Collection<Object>不是Collection<Number>的子类型。这一次,通配符类型同样提供了一种解决办法。popAll的输入参数类型不应该为“E的集合”,而应该为“E的某种超类的集合”(这里的超类是确定的,因此E是它自身的一个超类型)。仍然有一个通配符类型正是符合此意:Collection<? super E>。让我们修改popAll来使用它:
// Wildcard type for parameter that serves as an E consumer
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<? super E>。PECS这个助记符突出了使用通配符类型的基本原则。Naftalin和Wadler称之为Get and Put Principle。
记住这个助记符,我们下面来看一些之前的条目中提到过的方法声明。例如:
static <E> reduce(List<E> list , Function<E> f , E initVal)
虽然列表既可以消费也可以产生值,reduce方法还是只用他的list参数作为E生产者(producer),因此他的声明就应该使用一个extends E的通配符类型。参数f表示既可以消费又可以产生E实例的函数,因此通配符类型不适合他。得到的方法声明如下:
// Wildcard type for parameter that serves as an E producer
static <E> E reduce(List<? extends E> list , Function<E> f , E initVal)
这一变化实际上有什么区别吗?事实上,的确有区别。假设你有一个List<Integer>,想通过Function<Number>把他简化。他不能通过初始声明进行编译,但是一旦添加了有限制的通配符类型,就可以了。
现在我们看一个例子。下面是声明:
public static <E> Set<E> uniono(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>。不要用通配符类型作为返回类型。除了为用户提供额外的灵活性之外,他还会强制用户在客户端代码中使用通配符类型。
如果使用得当,通配符类型对于类的用户来说几乎是无形的。他们使方法能够接受他们应该接受的参数,并拒绝那些应该拒绝的参数。如果类的用户必须考虑通配符类型,类的API或许就会出错。
遗憾的是,类型推导(type inference)规则相当复杂,在语言规范中占了整整16页,而且他们并非总能完成需要他们完成的工作。看看修改过的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> numbers = union(integers ,doubles);
幸运的是,有一种办法可以处理这种错误。如果编译器不能推断你希望他拥有的类型,可以通过一个显示的类型参数(explicit type parameter)来告诉他要使用哪种类型。这种情况不太经常发生,这是好事,因为显示的类型参数不太优雅。增加了这个显示的类型参数之后,程序可以正确无误的进行编译:
Set<Number> numbers = Union.<Number>union(integers , doubles);
接下来,我们看一下下面的例子。以下是初始的声明:
public static <T extends Comparable<T>> T max(List<T> list)
下面是修改过的使用通配符类型的声明:
public static <T extends Comparable<? super T>> T max(List<? extends T> list)
为了从初始声明中得到修改后的版本,要应用PECS转换两次。最直接的是运用参数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>。
修改过的max声明可能是整本书中最复杂的方法生命了。所增加的复杂代码真的起作用了么?是的,起作用了。下面是一个简单的列表示例,在初始的声明中不允许这样,修改过的版本则可以:
List<ScheduledFuture<?>> scheduledFutures = ... ;
不能将初始方法声明运用给这个列表的原因在于,java.util.concurrent.ScheduledFuture没有实现Comparable<ScheduledFuture>接口。相反,他是扩展Comparable<Delayed>接口的Delayed接口的子接口。换句话说,ScheduleFuture实例并非只能与其他ScheduledFuture实例相比较;他可以与任何Delayed实例相比较,这就足以导致初始声明时就会被拒绝。
修改过的max声明有一个小小的问题:它阻止方法进行编译。下面的方法包含了修改过的声明:
// Won't compile - wildcards can require change in method body!
public static <T extends Comparable<? super T>> T max(List<? extends 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;
}
以下是它编译时会产生的错误消息:
Max.java:7: incompatible types
found : Iterator<capture#591 of ? extends T>
required: 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);
你更喜欢这两种方法中的哪一种呢?为什么?在公共API中,第二种更好一些,因为他更简单。将他传到一个列表中——任何列表——方法就会交换被索引的元素。不用担心类型参数。一般来说,如果类型参数只在方法声明中出现依次,就可以用通配符取代它。如果是无限制的类型参数,就用无限制的通配符取代它;如果是有限制的类型参数,就用有限制的通配符取代它。
将第二种声明用于swap方法会有一个问题,他优先使用通配符而非类型参数:下面这个简单的实现都不能编译:
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<?>中。幸运的是,有一种方式可以实现这个方法,无需求助于不安全的转换或者原生态类型(raw type)。这种想法就是编写一个私有的辅助方法来捕捉通配符类型。为了捕捉类型,辅助方法必须是泛型方法,像下面这样:
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类型的任何值放进列表都是安全的。swap这个有些费解的实现编译起来却是正确无误的。他允许我们导出swap这个比较好的基于通配符的声明,同时在内部利用更加复杂的泛型方法。swap方法的客户端不一定要面对更加复杂的swapHelper声明,但是他们的确从中受益。
总而言之,在API中使用通配符类型虽然比较需要技巧,但是使API变得灵活的多。如果编写的是将被广泛使用的类库,则一定要适当地利用通配符类型。记住基本原则:
producer-extends,consumer-super(PECS)。还要记住所有的comparable和comparator都是消费者。
- 利用有限制通配符来提升API的灵活性
- 利用有限制通配符来提升API的灵活性。
- EffectiveJava(28)怎么利用有限制的通配符类型来提升API的灵活性
- 第28条:利用有限制通配符来提升API的灵活性
- 第28条 泛型——利用有限制通配符来提升API的灵活性
- effective java(28) 之利用有限制通配符来提升API的灵活性
- (28):利用有限制通配符来提高API的灵活性
- 第二十八条:利用有限通配符提升API的灵活性
- 利用有限通配符提供API灵活性
- Effective Java(数组和泛型的实现方式、用无限制的通配符提高API的灵活性)
- 利用stringbuilder来提升字符串的性能
- javaweb-day22-1(基础加强 - 反射泛型、通配符、有限制的通配符)
- 利用javascript来限制文本的一些文本输入限制
- Java带有限制的通配符
- 利用ShellExecuteEx手动提升用户特权,以管理员权限来运行程序。win7会有UAC弹窗
- 利用Google API进行无长度限制的文本翻译
- 利用XSLT 实现更好的灵活性和方便
- 利用SOA技术增强业务和IT部门的灵活性
- 做一个合格的程序猿之浅析Spring IoC源码(四)分析BeanPostProcessor(1)
- Jtable表格失焦,停止可编辑
- SHELL脚本
- 多线程下载
- mt6735 KK root process :Healthd
- 利用有限制通配符来提升API的灵活性。
- 《HTTP权威指南》学习笔记(6)第6章代理(关键词:计算机网络/HTTP/代理)
- C++ override及虚函数的讲解
- spring mvc json数据的格式化和数据校验
- 匠牛社区AM5728 HDMI接口测试
- Web前端慢加密
- android OkHttp工具类
- 敏捷开发 如何设计好看板?:敏捷看板成功实施的关键?如何通过看板实现项目可视化?
- html头部