第26条:优先考虑泛型
一般来说,将集合声明参数化,以及使用 JDK 所提供的泛型和泛型方法,这些都不太困难。编写自己的泛型会比较困难一些,但是值得花些时间学习如何编写。
考虑第 6 条中这个简单的堆栈实现:
// Object-based collection - a prime candidate for generics
public class Stack {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public Stack() {
elements = new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(Object e) {
ensureCapacity();
elements[size++] = e;
}
public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null; // Eliminate obsolete reference
return result;
}
public boolean isEmpty() {
return size == 0;
}
private void ensureCapacity() {
if (elements.length == size)
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
这个类是泛型化(generification)的主要备选对象,换句话说,可以适当地强化这个类类利用泛型。根据实际情况来看,必须转换从堆栈里弹出的对象,以及可能在运行时失败的那些转换。将类泛型化的第一个步骤是给它的声明添加一个或者多个类型参数。在这个例子中有一个类型参数,它表示堆栈的元素类型,这个参数的名称通常为E
(见第 44 条)。
下一步是用相应的类型参数替换所有的Object
类型,然后试着编译最终的程序:
// Initial attempt to generify Stack = won't compile
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;
}
... // no changes in isEmpty or ensureCapacity
}
通常,你将至少得到一个错误或警告,这个类也不例外。幸运的是,这个类只产生一个错误,如下:
Stack.java:8: generic array creation
elements = new E[DEFAULT_INITIAL_CAPACITY];
^
如第 25 条中所述,你不能创建不可具体化的(non-reifiable)类型的数组,如E
。每当编写用数组支持的泛型时,都会出现这个问题。解决这个问题有两种方法。第一种,直接绕过创建泛型数组的禁令:创建一个Object
的数组,并将它转换为泛型数组类型。现在错误是消除了,但是编译器会产生一条警告。这种用法是合法的,但(整体上而言)不是类型安全的:
Stack.java:8: [unchecked] unchecked cast
found : Object[], required: E[]
elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
^
编译器不可能证明你的程序是类型安全的,但是你可以证明。你自己必须确保未受检的转换不会危及到程序的类型安全性。相关的数组(即elements
变量)保存在一个私有的域中,永远不会被返回到客户端,或者传递给任何其他方法。这个数组中保存的唯一元素,是传给push
方法的那些元素,它们的类型为E
,因此未受检的转换不会有任何危害。
一旦你证明了未受检的转换是安全的,就要在尽可能小的范围中禁止警告(见第 24 条)。在这种情况下,构造器只包含未受检的数组创建,因此可以在整个构造器中禁止这条警告。通过增加一条注解来完成禁止,Stack
能够正确无误地进行编译,你就可以使用它了,无需显式的转化,也无需担心会出现ClassCastException
异常:
// The elements array will contain only E instances from push(E).
// This is sufficient to ensure type safety, but the runtime
// type of the array won't be E[]; it will always be Object[]!
@SuppressWarnings("unchecked")
public Stack() {
elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
}
消除Stack
中泛型数组创建错误的第二种方法是,将elements
域的类型从E[]
改为Object[]
。这么做会得到一条不同的错误:
Stack.java:19: incompatible types
found : Object, required: E
E result = elements[--size];
^
通过把从数组中获取到的元素有Object
转换成E
,可以将这条错误变成一条警告:
Stack.java:19: warning: [unchecked] unchecked cast
found : Object, required: E
E result = elements[--size];
^
由于E
是一个不可具体化的(non-reifiable)类型,编译器无法在运行时检验转换。你还是可以自己证实未受检的转换是安全的,因此可以禁止该警告。根据第 24 条的建议,我们只要在包含未受检转换的任务上禁止警告,而不是在整个pop
方法上就可以了,如下:
// Appropriate suppression of unchecked warning
public E pop() {
if (size == 0)
throw new EmptyStackException();
// push requires elements to be of type E, so cast is correct
@SuppressWarnings("unchecked") E result =
(E) elements[--size];
elements[size] = null; // Eliminate obsolete reference
return result;
}
具体选择这两种方法中的哪一种来处理泛型数组创建错误,这要看个人的偏好了。所有其他的东西都一样,但是禁止数组类型的未受检转换比禁止标量类型(scalar type)的更加危险,所以建议采用第二种方案。但是在比Stack
更实际的泛型类中,或许多代码会有多个地方需要从数组中读取元素,因此选择第二种方案需要多次转换成E
,而不是只转换成E[]
,这也是第一种方案之所以更常用的原因[Naftalin07,6.7]。
下面的程序示范了泛型Stack
类的使用。程序以相反的顺序打印出它的命令行参数,并转换成大写字母。如果要从堆栈中弹出的元素上调用String
的toUpperCase
方法,并不需要显式的转换,并且会确保自动生成的转换会成功:
// Little program to exercise our generic Stack
public static void mai(String[] args) {
Stack<String> stack = new Stack<String>();
for (String arg : args)
stack.push(args);
while (!stack.isEmpty())
System.out.println(stack.pop.toUpperCase());
}
看来上述的示例与第 25 条相矛盾了,第 25 条鼓励优先使用列表而非数组。实际上并不可能总是或者总想在泛型中使用列表。Java 并不是生来就支持列表,因此有些泛型如ArrayList
则必须在数组上实现。为了提升性能,其他泛型如HashMap
也在数组上实现。
绝大多数泛型就像我们的Stack
示例一样,因为它们的类型参数没有限制:你可以创建Stack<Object>
、Stack<int[]>
、Stack<List<String>>
,或者任何其他对象引用类型的Stack
。注意不能创建基本类型的Stack
:企图创建Stack<int>
或者Stack<double>
会产生一个编译时错误。这是 Java 泛型系统根本的局限性。你可以通过使用包装类型(boxed primitive type)来避开这条限制(见第 49 条)。
有一些泛型限制了可允许的类型参数值。例如,考虑java.util.concurrent.DelayQueue
,其声明如下:
class DelayQueue<E extends Delayed> implements BlockingQueue<E>;
类型参数列表(<E extends Delayed>
)要求实际的类型参数E
必须是java.util.concurrent.Delayed
的一个子类型。它允许DelayedQueue
实现及其客户端在DelayedQueue
元素上利用Delayed
方法,无需显式的转换,也没有出现ClassCastException
的风险。类型参数E
被称作有限制的类型参数(bounded type parameter)。注意,子类型关系确定了,每个类型都是它自身的子类型[JLS,4.10],因此创建DelayedQueue<Delayed>
是合法的。
总而言之,使用泛型比使用需要在客户端代码中进行转换的类型来得更加安全,也更加容易。在设计新类型的时候,要确保它们不需要这种转换就可以使用。这通常意味着要把类做成是泛型的。只要时间允许,就把现有的类型都泛型化。这对于这些类型的新用户来说会变得更加qingso9ng,又不会破坏现有的客户端(见第 23 条)。