ITEM 29: FAVOR GENERIC TYPES
通常,参数化声明并使用JDK提供的泛型类型和方法并不太难。编写自己的泛型类型有点困难,但是值得学习如何编写泛型类型。
考虑第7项中的简单(玩具)堆栈实现:
// 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);
}
}
这个类一开始就应该是参数化的,但是由于它不是参数化的,所以我们可以在事后对它进行泛化。换句话说,我们可以在不损害原始非参数化版本客户机的情况下对其进行参数化。按照目前的情况,客户机必须转换从堆栈弹出的对象,这些转换可能在运行时失败。泛型类的第一步是向其声明中添加一个或多个类型参数。在本例中,有一个类型参数,表示堆栈的元素类型,该类型参数的常规名称是E (Item 68)。
下一步是用适当的类型参数替换类型对象的所有用法,然后尝试编译生成的程序:
// Initial attempt to generify Stack - won't compile!
public class Stack {
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];”
正如 item 28 中所解释的,您不能创建一个不可具体化类型的数组,比如E。每次编写由数组支持的泛型类型时都会出现这个问题。有两种合理的方法来解决它。第一个解决方案直接绕过了泛型数组创建的禁令:创建一个对象数组并将其转换为泛型数组类型。现在,编译器将发出警告,而不是错误。这个用法是合法的,但它不是(一般)类型安全:
“Stack.java:8: warning: [unchecked] unchecked cast found: Object[], required: E[] elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];”
编译器可能无法证明您的程序是类型安全的,但您可以。您必须说服自己,未检查的强制转换不会损害程序的类型安全性。所涉及的数组(元素)存储在一个私有字段中,从来没有返回给客户机或传递给任何其他方法。数组中存储的惟一元素是传递给 push 方法的元素,类型为 E,因此未选中的强制转换不会造成任何危害。
一旦您证明了未选中的强制转换是安全的,就将警告抑制在尽可能小的范围内(item 27)。在本例中,构造函数只包含未选中的数组创建,因此在整个构造函数中禁用警告是合适的。通过添加一个注释,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];
}
消除堆栈中通用数组创建错误的第二种方法是将字段元素的类型从E[]更改为Object[]。如果你这样做,你会得到一个不同的错误:
“Stack.java:19: incompatible types found: Object, required: E. E result = elements[--size];”
您可以通过将从数组中检索到的元素强制转换为E,将此错误转换为警告,但是您将得到警告:
“Stack.java:19: warning: [unchecked] unchecked cast found: Object, required: E
E result = (E) elements[--size];”
因为E是不可具体化的类型,所以编译器无法在运行时检查转换。同样,您可以很容易地向自己证明未选中的强制转换是安全的,因此适当地抑制警告。根据 item 27 的建议,我们只对包含未选中强制类型转换的赋值取消警告,而不是对整个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;
}
消除泛型数组创建的两种技术都有其追随者。第一个更容易读:数组被声明为E[]类型,清楚地表明它只包含E实例。它也更简洁:在一个典型的泛型类中,从数组中读取代码中的许多点;第一种技术只需要一次转换(在创建数组的地方),而第二种技术在每次读取数组元素时都需要单独的转换。因此,第一种技术更可取,在实践中也更常用。但是,它确实会造成堆污染(item 32):数组的运行时类型与其编译时类型不匹配(除非E恰好是Object)。这使得一些程序员非常反感,他们选择了第二种技术,尽管在这种情况下堆污染是无害的。
下面的程序演示了通用堆栈类的使用。程序以相反的顺序打印命令行参数并转换为大写。对栈中弹出的元素调用 String 的 toUpperCase 方法不需要显式的强制转换,自动生成的强制转换保证成功:
// Little program to exercise our generic Stack
public static void main(String[] args) {
Stack stack = new Stack<>();
for (String arg : args)
stack.push(arg);
while (!stack.isEmpty())
System.out.println(stack.pop().toUpperCase());
}
上面的例子可能与 item 28 相矛盾, item 28 鼓励使用列表而不是数组。在泛型类型中使用列表并不总是可行或可取的。Java本身不支持列表,因此一些泛型类型(如ArrayList)必须在数组之上实现。其他泛型类型(如 HashMap)是在数组之上实现的,用于提高性能。
绝大多数泛型类型与我们的堆栈示例相似,因为它们的类型参数没有限制:您可以创Stack