The Principle of Truth in Advertising 82

Một phần của tài liệu java generics and caollections (Trang 100 - 104)

We saw in the previous section that a nạve method to convert a collection to an array will not work. The first fix we might try is to add an unchecked cast, but we will see shortly that this leads to even more perplexing problems. The correct fix will require us to resort to reflection. Since the same issues arise when converting any generic struc- ture to an array, it is worth understanding the problems and their solution. We will study variations of the static toArray method from the previous section; the same ideas apply to the toArray method in the Collection interface of the Collections Framework.

Here is a second attempt to convert a collection to an array, this time using an un- checked cast, and with test code added:

import java.util.*;

class Wrong {

public static <T> T[] toArray(Collection<T> c) { T[] a = (T[])new Object[c.size()]; // unchecked cast int i=0; for (T x : c) a[i++] = x;

return a;

}

public static void main(String[] args) {

List<String> strings = Arrays.asList("one","two");

String[] a = toArray(strings); // class cast error }

}

The code in the previous section used the phrase new T[c.size()] to create the array, causing the compiler to report a generic array creation error. The new code instead allocates an array of objects and casts it to type T[], which causes the compiler to issue an unchecked cast warning:

% javac -Xlint Wrong.java

Wrong.java:4: warning: [unchecked] unchecked cast found : java.lang.Object[]

required: T[]

T[] a = (T[])new Object[c.size()]; // unchecked cast ^

1 warning

As you might guess from the name chosen for this program, this warning should not be ignored. Indeed, running this program gives the following result:

% java Wrong

Exception in thread "main" java.lang.ClassCastException: [Ljava.lang.Object;

at Wrong.main(Wrong.java:11)

The obscure phrase [Ljava.lang.Object is the reified type of the array, where [L indi- cates that it is an array of reference type, and java.lang.Object is the component type of the array. The class cast error message refers to the line containing the call to toAr ray. This error message may be confusing, since that line does not appear to contain a cast!

In order to see what went wrong with this program, let’s look at how the program is translated using erasure. Erasure drops type parameters on Collection and List, re- places occurrences of the type variable T with Object, and inserts an appropriate cast on the call to toArray, yielding the following equivalent code:

import java.util.*;

class Wrong {

public static Object[] toArray(Collection c) {

Object[] a = (Object[])new Object[c.size()]; // unchecked cast int i=0; for (Object x : c) a[i++] = x;

return a;

}

public static void main(String[] args) { List strings = Arrays.asList(args);

String[] a = (String[])toArray(strings); // class cast error }

}

Erasure converts the unchecked cast to T[] into a cast to Object[], and inserts a cast to String[] on the call to toArray. When run, the first of these casts succeeds. But even

6.5 The Principle of Truth in Advertising | 83

though the array contains only strings, its reified type indicates that it is an array of Object, so the second cast fails.

In order to avoid this problem, you must stick to the following principle:

The Principle of Truth in Advertising: the reified type of an array must be a subtype of the erasure of its static type.

The principle is obeyed within the body of toArray itself, where the erasure of T is Object, but not within the main method, where T has been bound to String but the reified type of the array is still Object.

Before we see how to create arrays in accordance with this principle, there is one more point worth stressing. Recall that generics for Java are accompanied by a cast-iron guarantee: no cast inserted by erasure will fail, so long as there are no unchecked warn- ings. The preceding principle illustrates the converse: if there are unchecked warnings, then casts inserted by erasure may fail. Further, the cast that fails may be in a different part of the source code than was responsible for the unchecked warning! This is why code that generates unchecked warnings must be written with extreme care.

Array Begets Array “Tis money that begets money,” said Thomas Fuller in 1732, observing that one way to get money is to already have money. Similarly, one way to get a new array of a generic type is to already have an array of that type. Then the reified type information for the new array can be copied from the old.

We therefore alter the previous method to take two arguments, a collection and an array. If the array is big enough to hold the collection, then the collection is copied into the array. Otherwise, reflection is used to allocate a new array with the same reified type as the old, and then the collection is copied into the new array.

Here is code to implement the alternative:

import java.util.*;

class Right {

public static <T> T[] Array(toCollection<T> c, T[] a) { if (a.length < c.size())

a = (T[])java.lang.reflect.Array. // unchecked cast newInstance(a.get Class().getComponentType(), c.size());

int i=0; for (T x : c) a[i++] = x;

if (i < a.length) a[i] = null;

return a;

}

public static void main(String[] args) {

List<String> strings = Arrays.asList("one", "two");

String[] a = toArray(strings, new String[0]);

assert Arrays.toString(a).equals("[one, two]");

String[] b = new String[] { "x","x","x","x" };

toArray(strings, b);

assert Arrays.toString(b).equals("[one, two, null, x]");

} }

This uses three methods from the reflection library to allocate a new array with the same component type as the old array: the method getClass (in java.lang.Object) returns a Class object representing the array type, T[]; the method getComponentType (from java.lang.Class) returns a second Class object representing the array’s compo- nent type, T; and the method newInstance (in java.lang.reflect.Array) allocates a new array with the given component type and size, again of type T[]. The result type of the call to newInstance is Object, so an unchecked cast is required to cast the result to the correct type T[].

In Java 5, the class Class has been updated to a generic class Class<T>; more on this shortly.

(A subtle point: in the call to newInstance, why is the result type Object rather than Object[]? Because, in general, newInstance may return an array of a primitive type such as int[], which is a subtype of Object but not of Object[]. However, that won’t happen here because the type variable T must stand for a reference type.)

The size of the new array is taken to be the size of the given collection. If the old array is big enough to hold the collection and there is room left over, a null is written just after the collection to mark its end.

The test code creates a list of strings of length two and then performs two demonstration calls. Neither encounters the problem described previously, because the returned array has the reified type String[], in accordance with the Principle of Truth in Advertising.

The first call is passed an array of length zero, so the list is copied into a freshly allocated array of length two. The second call is passed an array of length four, so the list is copied into the existing array, and a null is written past the end; the original array content after the null is not affected. The utility method toString (in java.util.Arrays) is used to convert the array to a string in the assertions.

The Collections Framework contains two methods for converting collections to arrays, similar to the one we just discussed:

interface Collection<E> { ...

public Object[] toArray();

public <T> T[] toArray(T[] a) }

The first method returns an array with the reified component type Object, while the second copies the reified component type from the argument array, just as in the static method above. Like that method, it copies the collection into the array if there is room (and writes a null past the end of the collection if there is room for that), and allocates a fresh array otherwise. A call to the first method, c.toArray(), returns the same result as a call to the second method with an empty array of objects, c.toArray(new Object[0]). These methods are discussed further at the beginning of Chapter 12.

Often on encountering this design, programmers presume that the array argument ex- ists mainly for reasons of efficiency, in order to minimize allocations by reusing the 6.5 The Principle of Truth in Advertising | 85

array. This is indeed a benefit of the design, but its main purpose is to get the reified types correct! Most calls to toArray will be with an argument array of length zero.

A Classy Alternative Some days it may seem that the only way to get money is to have money. Not quite the same is true for arrays. An alternative to using an array to create an array is to use an instance of class Class.

Instances of the class Class represent information about a class at run time; there are also instances of this class that represent primitive types and arrays. In this text, we will refer to instances of the Class class as class tokens.

In Java 5, the class Class has been made generic, and now has the form Class<T>. What does the T stand for? An instance of type Class<T> represents the type T. For example, String.class has type Class<String>.

We can define a variant of our previous method that accepts a class token of type Class<T> rather than an array of type T[]. Applying newInstance to a class token of type Class<T> returns a new array of type T[], with the component type specified by the class token. The newInstance method still has a return type of Object (because of the same problem with primitive arrays), so an unchecked cast is still required.

import java.util.*;

class RightWithClass {

public static <T> T[] toArray(Collection<T> c, Class<T> k) { T[] a = (T[])java.lang.reflect.Array. // unchecked cast newInstance(k, c.size());

int i=0; for (T x : c) a[i++] = x;

return a;

}

public static void main(String[] args) {

List<String> strings = Arrays.asList("one", "two");

String[] a = toArray(strings, String.class);

assert Arrays.toString(a).equals("[one, two]");

} }

The conversion method is now passed the class token String.class rather than an array of strings.

The type Class<T> represents an interesting use of generics, quite different from col- lections or comparators. If you still find this use of generics confusing, don’t worry—

we’ll cover this subject in greater detail in Chapter 7.

Một phần của tài liệu java generics and caollections (Trang 100 - 104)

Tải bản đầy đủ (PDF)

(286 trang)