I don’t understand why the correct example given in the book Java Generics and Collections obeys the Principle of Truth in Advertising

Viewed 85

I think that I understand the concept itself: the reified type of an array must be a subtype of the erasure of its static type. But I feel that looking around for help has only confused me more. Here's my understanding of it and why I'm confused.

Here is some code that converts a Collection<T> to an array of type T.

//This code is taken straight from the book Java Generics and Collections
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;
    }
    public static void main(String[] args) {
       List<String> strings = Arrays.asList("one", "two");
       String[] a = toArray(strings); //class cast error
    }
}

A ClassCastException is thrown. At compile time, erasure converts all occurrences of type T into type Object (because they have no bounds; if they did, they would be converted into their leftmost bound), converts parametric types into their raw types, and adds appropriate casts. So effectively, the code is equivalent to:

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

Since the method toArray effectively returns an Object[], casting it to a String[] is illegal. In this case, the reified type of the array is Object while the erasure of its static type is String, disobeying the principle.

If I understand this correctly, it seems to me that this code should also fail, but actually works without error.

import java.util.*;
public class Right {
    public static <T> T[] toArray(Collection<T> c, T[] a) {
        if (a.length < c.size())
            a = (T[])java.lang.reflect.Array.
                    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]");
    }
}

But following the first example, at compile time, through erasure, wouldn't all occurrences of type T be converted to type Object, parametric types be converted to raw types, and appropriate casts inserted just like the first example? This is what I think it'd look like:

import java.util.*;
public class Right {
    public static Object[] toArray(Collection c, Object[] a) {
        if (a.length < c.size())
            a = (Object[])java.lang.reflect.Array.
                    newInstance(a.getClass().getComponentType(), c.size());
        int i=0; for (Object 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 = (String[])toArray(strings, new String[0]); //class cast exception?????
        assert Arrays.toString(a).equals("[one, two]");
        String[] b = new String[] {"x","x","x","x"};
        toArray(strings, b); /*I think this would be fine because the Principle of Truth in 
                               Advertising is followed within the method body. It's just a problem
                               when attempting to assign the return value of this method within 
                               the main method*/
        assert Arrays.toString(b).equals("[one, two, null, x]");
    }
}

But I assume that's not actually what's happening? To be clear, I feel that the cast to T[] on line 5 in both examples is what's causing the issue, as through erasure, T[] becomes Object[]. So, the array a is effectively of type Object[].

I feel like I have a fundamental misunderstanding about reification or erasure, or maybe something else. I've looked around online and no explanation has been able to satisfy my question, so answers would be greatly appreciated!

1 Answers

So, the array a is effectively of type Object[].

Nope.

There is no array a. Objects don't have names; variables have names. a is a variable. It points at an object. That doesn't mean that object's name is a:

Object a = "foo";
Object b = a;
a = null;

So, should we call "foo" a or b? Neither: Objects do not have names.

a is a variable and its type is effectively Object[], sure. It's pointing at an object, and that object's type (not 'effective type', objects don't have an 'effective' type. they have an actual type) is String[] in your second snippet, and Object[] in the first. THAT is the crucial difference: That ClassCastException occurs in the first snippet because the actual type is not compatible.

Why is this so? Because new T[] gets boiled down in java code to making a new object (hence the new), and as T is erased, the newly created object's actual type will be Object[]. However, in the second snippet, the reflection shenanigans means the newly created array is actually of type String[].. and is then assigned to a, which is a variable of type Object[]. Which doesn't make type sense (List<Object> x = new ArrayList<String>() does not work; you can add Integer objects to a list of objects, which is not a legal move here), but the java spec explicitly says that you can assign any array of any reference type to Object[] anyway, because arrays in java are broken.

Related