第29条:优先考虑泛型

It is generally not too difficult to parameterize your declarations and make use of the generic types and methods provided by the JDK. Writing your own generic types is a bit more difficult, but it’s worth the effort to learn how.

通常,参数化你的声明并且利用JDK提供的泛型类型和方法不会很难。只是编写你自己的泛型类型会难一些,但学会如何编写是值得的。

Consider the simple (toy) stack implementation from Item 7:

考虑条目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); 
    }
}

This class should have been parameterized to begin with, but since it wasn’t, we can generify it after the fact. In other words, we can parameterize it without harming clients of the original non-parameterized version. As it stands, the client has to cast objects that are popped off the stack, and those casts might fail at runtime. The first step in generifying a class is to add one or more type parameters to its declaration. In this case there is one type parameter, representing the element type of the stack, and the conventional name for this type parameter is E(Item 68).

这个类一开始就应该参数化,但既然现在没有,没关系,我们接下来就泛型化它。换句话说,我们可以参数化它,而且不用破坏那些使用了原本未参数化版本的客户端。就目前来看,客户端必须将从栈里弹出的对象进行强转,而且在运行时这些强转还有可能失败。将类泛型化的第一步是在它的声明里添加一个或多个类型参数。根据这个步骤要求,需要有一个类型参数来表示栈的元素类型,通常用E作为这个类型参数的名字(条目68)。

The next step is to replace all the uses of the type Object with the appropriate type parameter and then try to compile the resulting program:

下一步,用恰当的类型参数把所有使用了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 
}

You’ll generally get at least one error or warning, and this class is no exception. Luckily, this class generates only one error:

一般情况下,你会得到至少一个错误或者警告,上述那个类也不例外。幸运的事,它只产生了一个错误:

Stack.java:8: generic array creation
elements = new E[DEFAULT_INITIAL_CAPACITY];
^

As explained in Item 28, you can’t create an array of a non-reifiable type, such as E. This problem arises every time you write a generic type that is backed by an array. There are two reasonable ways to solve it. The first solution directly circumvents the prohibition on generic array creation: create an array of Object and cast it to the generic array type. Now in place of an error, the compiler will emit a warning. This usage is legal, but it’s not (in general) type safe:

就像条目28里解释的那样,你无法创建一个不可具化类型(如E)的数组。每次你编写一个泛型类型数组时都会出现这个问题。有两种合理的方式来解决这个问题。第一种办法是直接绕过对泛型数组创建的禁用:创建一个Object数组然后将它强转为泛型数组类型。现在编译器出现了一个警告,而不是错误。这种用法是合法的,但(通常)不是类型安全的:

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

The compiler may not be able to prove that your program is type safe, but you can. You must convince yourself that the unchecked cast will not compromise the type safety of the program. The array in question (elements) is stored in a private field and never returned to the client or passed to any other method. The only elements stored in the array are those passed to the push method, which are of type E, so the unchecked cast can do no harm.

编译器无法证明你的程序是类型安全的,但你可以。你必须说服自己这个未检查强转不会损害程序的类型安全性。相关的数组elements存储在一个私有域里,并且永远不会返回给客户端或作为参数传给另一个方法。存储在数组里的都是由push方法传进来的类型为E的元素,所以未检查强转不会带来什么危害。

Once you’ve proved that an unchecked cast is safe, suppress the warning in as narrow a scope as possible (Item 27). In this case, the constructor contains only the unchecked array creation, so it’s appropriate to suppress the warning in the entire constructor. With the addition of an annotation to do this, Stack compiles cleanly, and you can use it without explicit casts or fear of a ClassCastException:

一旦你证明了未检查强转是安全的,就在尽量小的范围内去禁止警告(条目27)。根据这一点,由于Stack类的构造器只包含未检查数组的创建,所以在构造器方法上去禁止这个警告是合适的。加上这个注解后,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];
}

The second way to eliminate the generic array creation error in Stack is to change the type of the field elements from E[] to Object[]. If you do this, you’ll get a different error:

消除Stack类的泛型数组创建错误的第二种方法是,将域elements的元素从E[]转换成Object[]。如果你这么做了,那么你将会得到一个不同的错误:

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

You can change this error into a warning by casting the element retrieved from the array to E, but you will get a warning:

通过将取出的元素强转成E,可以将这个错误变成一条警告:

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

Because E is a non-reifiable type, there’s no way the compiler can check the cast at runtime. Again, you can easily prove to yourself that the unchecked cast is safe, so it’s appropriate to suppress the warning. In line with the advice ofItem 27, we suppress the warning only on the assignment that contains the unchecked cast, not on the entire pop method:

// 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; 
}

Both techniques for eliminating the generic array creation have their adherents. The first is more readable: the array is declared to be of type E[], clearly indicating that it contains only E instances. It is also more concise: in a typical generic class, you read from the array at many points in the code; the first technique requires only a single cast (where the array is created), while the second requires a separate cast each time an array element is read. Thus, the first technique is preferable and more commonly used in practice. It does, however, cause heap pollution(Item 32): the runtime type of the array does not match its compile-time type (unless E happens to beObject). This makes some programmers sufficiently queasy that they opt for the second technique, though the heap pollution is harmless in this situation.

The following program demonstrates the use of our generic Stack class. The program prints its command line arguments in reverse order and converted to uppercase. No explicit cast is necessary to invoke String’s toUpperCase method on the elements popped from the stack, and the automatically generated cast is guaranteed to succeed:

// 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());
}

The foregoing example may appear to contradictItem 28, which encourages the use of lists in preference to arrays. It is not always possible or desirable to use lists inside your generic types. Java doesn’t support lists natively, so some generic types, such as ArrayList, must be implemented atop arrays. Other generic types, such as HashMap, are implemented atop arrays for performance.

The great majority of generic types are like our Stack example in that their type parameters have no restrictions: you can create a Stack<Object>,Stack<int[]>, Stack<List<String>>, or Stack of any other object reference type. Note that you can’t create a Stack of a primitive type: trying to create a Stack<int> or Stack<double> will result in a compile-time error. This is a fundamental limitation of Java’s generic type system. You can work around this restriction by using boxed primitive types (Item 61).

There are some generic types that restrict the permissible values of their type parameters. For example, consider java.util.concurrent. DelayQueue, whose declaration looks like this:

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

The type parameter list (<E extends Delayed>) requires that the actual type parameter E be a subtype of java.util.concurrent.Delayed. This allows the DelayQueue implementation and its clients to take advantage of Delayed methods on the elements of a DelayQueue, without the need for explicit casting or the risk of a ClassCastException. The type parameter E is known as abounded type parameter. Note that the subtype relation is defined so that every type is a subtype of itself [JLS, 4.10], so it is legal to create a DelayQueue<Delayed>.

In summary, generic types are safer and easier to use than types that require casts in client code. When you design new types, make sure that they can be used without such casts. This will often mean making the types generic. If you have any existing types that should be generic but aren’t, generify them. This will make life easier for new users of these types without breaking existing clients (Item 26).

results matching ""

    No results matching ""