Section 6.5. The Principle of Truth in Advertising


6.5. The Principle of Truth in Advertising

We saw in the previous section that a naï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 structure 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 unchecked 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 indicates 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 toArray. 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, replaces 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 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 warnings. 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[] toArray(Collection<T> c, T[] a) {     if (a.length < c.size())       a = (T[])java.lang.reflect.Array.  // unchecked cast              newInstance(a.getClass().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 component 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 exists mainly for reasons of efficiency, in order to minimize allocations by reusing the 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 collections or comparators. If you still find this use of generics confusing, don't worrywe'll cover this subject in greater detail in Chapter 7.




Java Generics and Collections
Java Generics and Collections
ISBN: 0596527756
EAN: 2147483647
Year: 2006
Pages: 136

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net