Short answer:
It's because generics is metadata to help the compiler help you catch type
errors and everything is compiled to use the lowest common denominator
(usually Object
) and type casts. This isn't done with arrays since arrays
are their own classes. I.e. An ArrayList<String>
and an ArrayList<Number>
both have the class ArrayList
, but an array of String
has class String[]
and an array of
Number
has class Number[]
.
Long answer:
When compiled, everything that is using generics will be using the least
common denominator (which is commonly Object
). This is demonstrated by the
following code:
public class Generics {
public static <T> void print(T what) {
System.out.println(what);
}
public static <T extends Number> void printNumber(T what) {
System.out.println(what);
}
public static void main(String[] args) {
Arrays.stream(Generics.class.getDeclaredMethods())
.filter(m -> m.getName().startsWith("print"))
.forEach(Generics::print);
}
}
This prints:
public static void Generics.print(java.lang.Object)
public static void Generics.printNumber(java.lang.Number)
So we can see that when it's compiled it is compiled as methods that works on Object
and Number
respectively.
This is the reason something like this will compile and run:
ArrayList<String> list = new ArrayList<>();
list.add("foo");
ArrayList<Object> list2 = (ArrayList<Object>)(Object)list;
list2.add(Integer.valueOf(10));
System.out.println(list2.get(0));
System.out.println(list2.get(1));
If you try that you'll see that it prints
foo
10
So by the down/up-cast we turned our ArrayList<String>
into an ArrayList<Object>
- which wouldn't be possible if the ArrayList was actually storing it's contents in an array of type String[]
instead of Object[]
.
Note that trying to do
System.out.println(list.get(0));
System.out.println(list.get(1));
Will result in a ClassCastException
. And this hints at exactly what the
compiler does.
Look at the following code:
public static void doThingsWithList() {
ArrayList<String> list = new ArrayList<>();
list.add("");
String s = list.get(0);
print(s);
}
When compiled, it's turned into this bytecode:
public static void doThingsWithList();
Code:
0: new #11 // class java/util/ArrayList
3: dup
4: invokespecial #12 // Method java/util/ArrayList."<init>":()V
7: astore_0
8: aload_0
9: ldc #13 // String
11: invokevirtual #14 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z
14: pop
15: aload_0
16: iconst_0
17: invokevirtual #15 // Method java/util/ArrayList.get:(I)Ljava/lang/Object;
20: checkcast #16 // class java/lang/String
23: astore_1
24: aload_1
25: invokestatic #17 // Method print:(Ljava/lang/Object;)V
28: return
As you can see on line 20
the result from ArrayList.get
is actually cast to String
.
So generics is just syntactic sugar that turns into automatic type casting with the added benefit that the compiler can use this syntactic sugar to detect code that would result in a ClassCastException
during runtime.
Now, why can't the compiler do the same for String[]
and Object[]
? Couldn't it just turn
public <T> T[] addToNewArrayAndPrint(T item) {
T[] array = new T[10];
array[0] = item;
System.out.println(array[0]);
return array;
}
into
public <T> T[] addToNewArrayAndPrint(T item) {
Object[] array = new Object[1];
array[0] = item;
System.out.println((T) array[0]);
return array;
}
?
Nope. Because that would mean that
Arrays.equals(addToNewArray("foo"), new String[]{ "foo" });
would be false because the first array would have class Object[]
and the second would have class String[]
.
Sure, Java could be changed so that all arrays are of type Object[]
and that all accesses would use casts, just as when using generics. But that would break backwards compatibility whereas using Generics doesn't since an ArrayList<String>
has the same class as an ArrayList
.