I was reading a discussion about C++ templates and C# generics, and how they are different from Java's type-erased generics. I read a statement that said that Java still uses casting at runtime, for instance when dealing with collections. If this is true, I wasn't aware of it!
Let's say that I have code such as:
ArrayList<SomeClass> list = new ArrayList<SomeClass>();
...
SomeClass object = list.get(0);
My question is. Is this effectively compiled to
ArrayList list = new ArrayList();
...
SomeClass object = (SomeClass) list.get(0);
If so, why? I thought that the fact that list is of type ArrayList<SomeClass>
guarantees, at compile time and run time, that only SomeClass will be stored inside the ArrayList? Or can you ever do unsafe type-casting to transform an ArrayList<OtherClass>
into an ArrayList<SomeClass>
?
Are there other occasions where at-runtime type casting is done in Java generics?
Finally, if casting at run time is indeed used, are there occasions when the JIT can elide the run time cast check?
(Please refrain from answering/commenting that micro-optimisations are not worth it, preemptive optimisation is root of all evil, etc. I see these on other similar questions. These points are well understood, but they do not take away the point of trying to understand how type-erased generics are implemented under the hood.)
Here is a short program I wrote:
public class Test<T> {
public T contents;
public Test(T item) {
contents = item;
}
public static void main(String[] args) {
Test<String> t = new Test<String>("hello");
System.out.println(t.contents);
}
}
Try compiling it with javac
, and then look at the bytecode with javap -verbose
. I've selected a few interesting lines:
public java.lang.Object contents;
This should turn up just before the definition of the Test constructor. In the example code it was of type T, now it is an Object. That's erasure.
Now, looking at the main method:
public static void main(java.lang.String[]);
Code:
Stack=3, Locals=2, Args_size=1
0: new #3; //class Test
3: dup
4: ldc #4; //String hello
6: invokespecial #5; //Method "<init>":(Ljava/lang/Object;)V
9: astore_1
10: getstatic #6; //Field java/lang/System.out:Ljava/io/PrintStream;
13: aload_1
14: getfield #2; //Field contents:Ljava/lang/Object;
17: checkcast #7; //class java/lang/String
20: invokevirtual #8; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
23: return
Then we can see the checkcast
command at line 17, just before the println - this is where Java casts from Object
to the erased generic type - String
Your assumption is correct.
The type check is always necessary because you can write the following legal code:
ArrayList<X> good = new ArrayList<X>();
ArrayList q = x;
ArrayList<Y> evil = (ArrayList<Y>)q; //Doesn't throw due to type erasure
evil.add(new Y()); //this will actually succeed
X boom = good.get(0);
The cast from ArrayList
to ArrayList<Y>
will (always) give an unchecked cast warning, but will (also always) succeed at runtime.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With