当前位置: 代码迷 >> 综合 >> Effective Java 优先考虑泛型
  详细解决方案

Effective Java 优先考虑泛型

热度:89   发布时间:2023-09-29 02:24:21.0

一般来说参数化声明并使用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 referencereturn result;}public boolean isEmpty() {return size == 0;}private void ensureCapacity() {if (elements.length == size)elements = Arrays.copyOf(elements, 2 * size + 1);}
}

??这个类一开始就应该被参数化,但既然不是这样的,那么我们接下来就可以对它泛型化(generify)。换句话说我们可以参数化它而不会损害原始非参数化版本的客户端。就目前而言,必须转换从堆栈里弹出来的对象,以及可能在运行时失败的那些转换。将类泛型化的第一个步骤是给它的声明添加一个或者多个类型参数。在这个例子中有一个类型参数,它表示堆栈的元素类型,这个参数的名称通常为E(第68项)。

??下一步是用相应的类型参数替换所有的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 referencereturn result;}... // no changes in isEmpty or ensureCapacity
}

??通常,你将至少得到一个错误或者警告,这个类也不例外。幸运的是,这个类只产生一个错误,如下:

    Stack.java:8: generic array creationelements = new E[DEFAULT_INITIAL_CAPACITY];

??如第28项所描述的,你不能创建不可具体化的(non-reifiable)类型的数组,如E。每当编写用数组支持的泛型时,都会出现这个问题。解决这个问题有两种方法。第一种,直接绕过创建泛型数组的禁令:创建一个Object的数组,并将它转换成泛型数组类型。现在错误是消除了,但是编译器会产生一条警告。这种用法是合法的,但(整体上而言)不是类型安全的:

Stack.java:8: warning: [unchecked] unchecked cast 
found: Object[], required: E[]elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];^

??编译器不可能证明你的程序是类型安全的,但是你可以证明。你自己必须确保未受检的转换不会危及到程序的类型安全性。问题中的数组(elements)被保存在一个私有的域中,永远不会被返回到客户端,或者传给任何其他方法。这个数组中保存的唯一元素,是传给push方法的那些元素,它们的类型为E,因此未受检的转换不会有任何危害。

??一旦你证明了未受检的转换是安全的,就要在尽可能小的范围中禁止警告(第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];
}

??消除Stack中泛型数组创建错误的第二种方法时,将elements域的类型从E[]改为Object[]。这么做会得到一条不同的错误:

Stack.java:19: incompatible types 
found: Object, required: EE result = elements[--size];

??通过把数组中获取到的元素由Object转换成E,可以将这条错误变成一条警告:

Stack.java:19: warning: [unchecked] unchecked cast
found: Object, required: EE result = (E) elements[--size];^

??由于E是一个不可具体化的(non-reifiable)类型,编译器无法在运行时检验转换。同样,你可以轻松地向自己证明未受检的转换是安全的,因此可以禁止该警告。根据第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 referencereturn result;
}

??消除通用数组创建的这两种技术都有它们的支持者。第一个更具有可读性:数组声明为E[]类型,清楚地表明它只包含E实例。它也更简洁:在典型的泛型类中,你可以在代码中的很多地方读取数组;第一种技术只需要一次转换(创建数组的位置),而第二种技术每次读取数组元素时都需要单独的转换。因此,优选第一种,并且在实践中更常用第一种。但是,它会导致堆污染(heap pollution)(第32项):数组的运行时类型与其编译时的类型不匹配(除非E恰好是Object)。这使得一些程序猿非常不安,他们选择第二种技术,尽管在这种情况下堆污染是无害的。

??下面的程序示范了泛型Stack类的使用。程序以相反的顺序打印出它的命令行参数,并转换成大写字母。如果要在从堆栈中弹出的元素上调用String的toUpperCase方法,并不需要显式的转换,并且会确保自动生成的转换会成功:

// Little program to exercise our generic Stack
public static void main(String[] args) {Stack<String> stack = new Stack<>();for (String arg : args)stack.push(arg);while (!stack.isEmpty())System.out.println(stack.pop().toUpperCase());
}

??上面的示例可能看起来与第28项相矛盾,第28项鼓励优先使用列表而不是数组。在泛型中使用列表并不总是可行或可取的。Java并不是生来就支持列表,因此有些泛型如ArrayList,则必须在数组上实现。为了提升性能,其他泛型如HashMap也在数组上实现。

??绝大多数泛型与我们的Stack示例类似,因为它们的类型参数没有限制:你可以创建Stack、Stack<\int[]>、Stack<List>,或者任何其他对象引用类型的Stack。注意不能创建基本类型的Stack:企图创建Stack<\int>或者Stack<\double>会产生一个编译时错误。这是Java泛型系统根本的局限性。你可以通过使用基本包装类型(boxed primitive type)来避开这条限制(第61项)。

??有一些泛型限制了可允许的类型参数值。例如,考虑java.util.concurrent.DelayQueue,其声明如下:

class DelayQueue<E extends Delayed> implements BlockingQueue<E>

 

??类型参数列表()要求实际的类型参数E必须是java.util.concurrent.Delayed的一个子类型。它允许DelayQueue实现及其客户端在DelayQueue的元素上利用Delayed方法,无需显示的转换,也没有出现ClassCastException的风险。类型参数E被称作有限制的类型参数( bounded type parameter)。注意,子类型关系确定了,没个类型都是它自身的子类型[JLS, 4.10],因此创建DelayQueue[Delayed]是合法的。

??总而言之,使用泛型比使用需要在客户端代码中进行转换的类型来得更加安全,也更加容易。在设计新类型的时候,要确保它们不需要这种转换就可以使用。这通常意味着要把类做成泛型。如果你现在有任何类型应该是通用的但却不是通用的,就把现有的类型都泛型化。这对于这些类型的新用户来说会变得更加轻松,又不会破坏现有的客户端(第26项)。