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-super(PESC),还要记住所有的comparable和comparator都是消费者。