自Java 1.5开始使用的泛型,泛型给人比较直观的印象是..."尖括号里写了类型我就不用检查类型也不用强转了"。
确实,那先从API的使用者的角度上想问题,泛型还有什么意义?
Discover errors as soon as possible after they are made, ideally at compile time.
泛型提供的正是这种能力。
比如有一个只允许加入String的集合,在没有声明类型参数的情况下,这种限制通常通过注视来保证。
接着在集合中add一个Integer实例,从集合获取元素时使用强转,结果导致ClassCastException。
这样的错误发生在运行时,compiler就爱莫能助了,而声明了类型参数的情况下则是compile-time error。
相比raw type,泛型的优势显而易见,即安全性和表述性。
那么,有了泛型就一定比raw type强吗?
如果类型参数是Object又如何?
这种用法和raw type有什么区别?
如果仅仅通过代码描述,可以说raw type不一定支持哪个类型,而Collection<object>支持任何类型。
正确,但没什么意义。
泛型有种规则叫subtyping rules,比如List是List的子类型,但不是List<object>的子类型。
下面的代码描述了这种情况:
// Uses raw type (List) - fails at runtime!
public static void main(String[] args) {
List<String> strings = new ArrayList<String>();
unsafeAdd(strings, new Integer(42));
String s = strings.get(0); // Compiler-generated cast
}
private static void unsafeAdd(List list, Object o) {
list.add(o);
}
上面的情况导致运行时才能发现错误,而下面这种做法则是编译无法通过:
Test.java:5: unsafeAdd(List<Object>,Object) cannot be applied
to (List<String>,Integer)
unsafeAdd(strings, new Integer(42));
^
而为了应对这种情况Java提供了unbounded wildcard type。
即,对于不确定的类型参数使用‘?’代替。
比如Set<?>可以理解为某个类型的集合。
编码时几乎不会用到raw type,但也有两个例外,而且都和泛型擦除有关。
- must use raw types in class literals.
- it is illegal to use the instanceof operator on parameterized type.
既然如此,为什么Java还保留着raw type用法?
Java 1.5发布的时候Java马上要迎来第一个十年,早已存在大量的代码。
老代码和使用的新特性的代码能够互用至关重要,即 migration compatibility 。
好了,接下来说说使用泛型方面的事情。
关于raw type,在某些IDE中使用了raw type就会出现警告,并提示加上@SuppressWarnings。
比如,eclipse中:
@SuppressWarnings到底有什么作用?
作者给我们的提醒是:要尽量消除这些警告。
提示我们加上@SuppressWarnings是在传达一种信息:无法在运行时检查类型转换的安全性。
而程序员消除这些警告是在传达一种信息:运行时不会出现ClassCastException。
消除警告时优先使用声明类型参数的方式,如果因为某些原因而无法消除警告并且需要证明代码是没有问题时才使用@SuppressWarnings。
如上图所示的那样,@SuppressWarnings可以可以用在变量和方法上,对此我们优先针对更小的粒度。
对于@SuppressWarnings,不忽略,且不盲目。
接着说说subtyping,总觉得因为这一特征,泛型有时反而显得麻烦。
书中原话是covariant(协变)和invaritant。
比如,数组是covariant的,比如Sub是Super的子类,则Sub[]是Super[]的子类。
反之,泛型是invariant的,List不是List的子类。
鉴于这种区别,数组和泛型的难以混合使用。
比如下面这几种写法都是非法的:
new List<E>[]
new List<String>[]
new E[]
下面通过一段代码说明泛型数组为什么非法:
// Why generic array creation is illegal - won‘t compile!
List<String>[] stringLists = new List<String>[1]; // (1)
List<Integer> intList = Arrays.asList(42); // (2)
Object[] objects = stringLists; // (3)
objects[0] = intList; // (4)
String s = stringLists[0].get(0); // (5)
首先假设第一行是合法的。
第二行本身合法,鉴于第一行是合法并且数组是协变的,第三行也是合法的。
鉴于泛型用擦除实现的,即List的运行时类型是List,相应地,List[]的运行时类型是List[],第四行是合法的。
到了第五行则变得矛盾,说好的String类型呢?为什么声明了类型参数还是来了个ClassCastException?
既然如此,索性让第一行产生compile-time error吧,泛型又变得美好了。
举一个例子,比如下面这段代码:
interface Function<T> {
T apply(T arg1, T arg2);
}
static Object reduce(List list, Function f, Object initVal) {
Object[] snapshot;
snapshot = list.toArray();
Object result = initVal;
for (Object e : snapshot)
result = f.apply(result, e);
return result;
}
现在我想把reduce改为泛型方法,于是改成了如下形式:
static <E> E reduce(List<E> list, Function<E> f, E initVal) {
E[] snapshot = (E[])list.toArray();
E result = initVal;
for (E e : snapshot)
result = f.apply(result, e);
return result;
}
显然,结果是编译无法通过。
结果是除了在list.toArray上提示加上@SuppressWarnings之外没有任何问题,完全可以正常运行。
不要忽略@SuppressWarnings! 提示我加上@SuppressWarnings是在告诉我:无法在运行时检查类型转换的安全性。
那我应该加上@SuppressWarnings吗?如果加上的话又怎么保证?
其实解决方法很简单,就是不混用数组和泛型,即:
static <E> E reduce(List<E> list, Function<E> f, E initVal) {
List<E> snapshot;
synchronized (list) {
snapshot = new ArrayList<E>(list);
}
E result = initVal;
for (E e : snapshot)
result = f.apply(result, e);
return result;
}
这就好了,能用泛型就用。
但换个立场,作为一个提供者而不是使用者,问题还会这样容易吗?
比如描述一个栈:
// 无法通过编译
public class Stack <E> {
private E[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public Stack() {
elements = new E[DEFAULT_INITIAL_CAPACITY];
}
public void push(E e) {
ensureCapacity();
elements[size++] = e;
}
public E pop() {
if (size==0)
throw new EmptyStackException();
E result = elements[--size];
elements[size] = null; // Eliminate obsolete reference
return result;
}
// 检查方法略
}
很显然,数组是具体化的,new E[DEFAULTINITIALCAPACITY]上提示Cannot create a generic array of E。
于是我机制地改为(E[])new Object[DEFAULTINITIALCAPACITY]。
这样完全可以通过,然后出现提示让我加上@SuppressWarnings...
我无法忽略,但我可以证明不会出现ClassCastException。
即elements是private的,且没有任何方法可以直接访问它,push和pop的类型都是安全的。
或者我也能将elements的类型改为Object[],并且在pop的时候将元素类型转为E,这样也是可以的。
与其让使用者进行强转,倒不如提供者提供一个安全的泛型。
但不是所有的情况都像上面的例子那样的顺利。
比如我在Stack中增加了一个:
public void pushAll(Iterable<E> src) {
for (E e : src)
push(e);
}
然后我将Iterable传入Stack中,由于泛型不是协变的,果断来了个compile-time error。
但抛开这些不想,将一堆Integer放到一堆Number又显得那么里所应当。
于是我们就有了bounded wildcard type,关键就是这个bounded。
一个‘?‘是wildcard,为其加点限制就是bounded wildcard,即:
public void pushAll(Iterable<? extends E> src) {
for (E e : src)
push(e);
}
相应地,我们再提供一个popAll方法,将pop出来的元素添加到指定集合中。
比如,Stack中的元素必然可以添加到Collection<object>中,也就是:
public void popAll(Collection<? super E> dst) {
while (!isEmpty())
dst.add(pop());
}
对于泛型wildcard的使用,作者指出:PECS,stands for producer-extends, consumer-super.
即,类型参数表示一个生产者则使用<? extends T>,消费者则使用<? super T>。
再举个例子,比如我们要合并两个集合的元素,由于这是生产行为,则声明为:
public static <E> Set<E> union(Set<? extends E> s1,Set<? extends E> s2);
那么能不能在返回类型中使用通配符?
作者建议尽量不要在返回类型中使用,因为这样会让调用变得复杂。
再来个稍微复杂一些的例子,比如我要在某个参数类型的List中找到最大的元素。
最初的声明为:
public static <T extends Comparable<T>> T max(List<T> list)
返回结果从list参数获得,于是将参数声明改为List<? extends T> list。
那<T extends Comparable>有该如何处理。
以java.util.concurrent中的ScheduledFuture和Delayed为例。
(ps:interface ScheduledFuture extends Delayed 且 interface Delayed extends Comparable.)
即,类型T本身没有实现Comparable,但是他的父类实现了Comparable,于是声明为:
public static <T extends Comparable<? super T> > T max(List<? extends T> list)
最后还有一个有意思的例子,先看代码:
public static void swap(List<?> list, int i, int j) {
list.set(i, list.set(j, list.get(i)));
}
这段代码是无法通过编译的,因为无法将null以外的元素添加到List<?>中。
当然,如果直接声明一个类型参数就没有问题,但现在假设我们只有使用通配符,并且不能使用raw type。
既然知道通过类型参数可以解决,于是我们可以这样:
public static void swap(List<?> list, int i, int j) {
swapHelper(list, i, j);
}
private static <E> void swapHelper(List<E> list, int i, int j) {
list.set(i, list.set(j, list.get(i)));
}