19

While looking up (testing) information for another question, I came across something and had completely no clue why it was happening. Now, I know that there is no practical reason to do this, and that this is absolutely horrific code, but why is it that this works:

ArrayList<Quod> test=new ArrayList<Quod>();
ArrayList obj=new ArrayList();
test=obj;
obj.add(new Object());

System.out.println(test.get(0));

So, basically, I am adding an Object to an ArrayList of Quods. Now, I see how java would have no way of efficiently checking for this, because it would have to look through all of the references, which probably aren't even stored anywhere. But then why is it that get() works. Isn't get() suppose to return an instance of Quod, like it says when you put your mouse over it in Eclipse? If it can return an object that is only an object when it promised to return an object of type Quod, why can't I return a String when I say I will return an int?

And things get even weirder. This crashes as it is suppose to with a run-time error(java.lang.ClassCastException error)(!?!?):

ArrayList<Quod> test=new ArrayList<Quod>();
ArrayList obj=new ArrayList();
test=obj;
obj.add(new Object());

System.out.println(test.get(0).toString());

Why can't I call the toString on an Object? And why is it fine for the println() method to call its toString, but not for me to directly?


EDIT: I know that I am not doing anything with the first instance of ArrayList that I create, so it is essentially just a waste of processing time.


EDIT: I am using Eclipse on Java 1.6 Others have said that they get the same results in Eclipse running java 1.8. However, on some other compilers, a CCE error is thrown on both cases.

WiErD0
  • 467
  • 4
  • 16
  • 2
    You aren't adding anything to the `ArrayList`, you are discarding the reference to it when you reassign `test`. – azurefrog Dec 22 '14 at 16:22
  • java.lang.ClassCastException error – WiErD0 Dec 22 '14 at 16:23
  • Yeah, I know I am not adding to ArrayList. But why does the output work/not work – WiErD0 Dec 22 '14 at 16:23
  • What do you mean work/not work. Both of them throw CCE. – aioobe Dec 22 '14 at 16:26
  • 1
    For me in eclipse in java 1.6 the top one outputs java.lang.Object@1e63e3d and doesn't thrown any errors – WiErD0 Dec 22 '14 at 16:30
  • 2
    Why did you delete your answer? It was by far the best one and I was going to accept it as soon as the 5 minute timer ended – WiErD0 Dec 22 '14 at 16:49
  • I deleted it because I can't answer your question. I thought I could but I've now seen proof that my explanation doesn't work. Something is going very wrong if Java 1.6 on IntelliJ gives different results to Java 1.6 on eclipse. – Paul Boddington Dec 22 '14 at 16:51
  • @pbabcdefp Really sorry about that. I appreciate you spending the time to answer the question, though. – WiErD0 Dec 22 '14 at 16:56
  • No worries. I just earned a "disciplined badge" for deleting an answer that had 3 upvotes. I shall treasure it. If nobody can explain why the first one can execute without throwing an exception while the second one can't, and why the results are different to different people, I'll put a bounty on this in a couple of days. It's really weird. – Paul Boddington Dec 22 '14 at 17:00
  • Try replacing `Quod` by other types in the first example. You'll find that `String` produces a `ClassCastException` whereas `Integer` doesn't. When I said that I was getting a `ClassCastException` for both it was because I was actually using `String` instead of `Quod` thinking it wouldn't make any difference. It seems that the first example works ok for every class except `String`. – Paul Boddington Dec 22 '14 at 17:39
  • 1
    [It's still not that simple.](http://ideone.com/F2tKST) (with sun-jdk-7) – Sotirios Delimanolis Dec 22 '14 at 17:50
  • 1
    @pbabcdefp, that's because there's a special version of `println` that takes a `String` as a parameter. All other classes use the version that takes an `Object`. – Ian McLaird Dec 22 '14 at 21:05

2 Answers2

19

Java generics are implemented through type erasure, i.e. type arguments are only used for compilation and linking, but erased for execution. That is, there is no 1:1 correspondence between compile time types and runtime types. In particular, all instances of a generic type share the same runtime class:

new ArrayList<Quod>().getClass() == new ArrayList<String>().getClass();

In the compile time type system, type arguments are present, and used for type checking. In the runtime type system, type arguments are absent, and therefore not checked.

This would be no problem but for casts and raw types. A cast is an assertion of type correctness, and defers the type check from compile time to runtime. But as we have seen, there is no 1:1 correspondence between compile time and runtime types; type arguments are erased during compilation. As such, the runtime can not fully check the correctness of casts containing type parameters, and an incorrect cast can succeed, violating the compile time type system. The Java Language Specification calls this heap pollution.

As a consequence, the runtime can not rely on the correctness of type arguments. Nevertheless, it must enforce the integrity of the runtime type system to prevent memory corruption. It accomplishes this by delaying the type check until the generic reference is actually used, at which point the runtime knows the method or field it must support, and can check that it actually is an instance of the class or interface that declares that field or method.

With that, back to your code example, which I have slightly simplified (this doesn't change the behavior):

ArrayList<Quod> test = new ArrayList<Quod>();
ArrayList obj = test; 
obj.add(new Object());
System.out.println(test.get(0));

The declared type of obj is the raw type ArrayList. Raw types disable the checking of type arguments at compile time. As a consequence, we may pass an Object to its add method, even though the ArrayList may only hold Quod instances in the compile time type system. That is, we have successfully lied to the compiler and accomplished heap pollution.

That leaves the runtime type system. In the runtime type system, the ArrayList works with references of type Object, so passing an Object to the add method is perfectly ok. So is invoking get(), which also returns Object. And here is were things diverge: In your first code example, you have:

System.out.println(test.get(0));

The compile time type of test.get(0) is Quod, the only matching println method is println(Object), and therefore it is that method's signature that is embedded in the class file. At runtime, we therefore pass an Object to the println(Object) method. That is perfectly ok, and hence no exception is thrown.

In your second code example, you have:

System.out.println(test.get(0).toString());

Again, the compile time type of test.get(0) is Quod, but now we are invoking its toString() method. The compiler therefore specifies that the toString method declared in (or inherited to) type Quod is to be invoked. Obviously, this method requires this to point to an instance of Quod, which is why the compiler inserts an additional cast to Quod into the byte code prior to invoking the method - and this cast throws a ClassCastException.

That is, the runtime permits the first code example because the reference is not used in a way specific to Quod, but rejects the second because the reference is used to access a method of type Quod.

That said, you should not rely on when exactly the compiler will insert this synthetic cast, but prevent heap pollution from occurring in the first place by writing type correct code. Java compilers are required to assist you in this by emitting unchecked and raw type warnings whenever your code might cause heap pollution. Get rid of the warnings, and you won't have to understand those details ;-).

meriton
  • 68,356
  • 14
  • 108
  • 175
  • Very nice explanation. I found it insightful to try `System.out.println(((Object) test.get(0)).toString());` and see it succeed. – 5gon12eder Dec 23 '14 at 07:12
  • 2
    I don't really understand this. `toString()` is not specific to `Quod`. `toString()` is a method of `Object`. At runtime, the `toString()` method is chosen dynamically, based on the runtime type of the `Quod` instance. For example, if the class `Rod` extends `Quod`, the compiler has no way of knowing which `toString()` method will be invoked (the one in `Quod` or the one in `Rod`), so the compiler cannot specify which `toString()` method to invoke. So what's the point of casting to a `Quod` first? – Paul Boddington Dec 23 '14 at 12:11
  • I think the compiler inserts the cast to `Quod` because the compile time type of `test` says that should be done (it's an `ArrayList`). In this *particular* case, we might hope that it would elide the cast because it's passing it to a method that accepts an `Object` anyway, but it doesn't. When you do a `get` on an `ArrayList` it *always* inserts a cast to `Foo`. – Ian McLaird Dec 23 '14 at 14:42
  • 3
    @pbabcdefp: The Java language supports evolution of interfaces and classes by retaining binary compatibility wherever possible. When translating the source code into class files, it therefore assumes as little as possible about the existence, subtype relationships, and members of other types. Specifically, if the compiler were to encode (as you suggest) the name of the least specific super class declaring that method, linking that method invocation would fail if that type is no longer a super type, or no longer declares that method (for instance because the method has been moved to a subclass) – meriton Dec 23 '14 at 17:05
  • 1
    This would be particularly vexing as the source code would not have to be changed. – meriton Dec 23 '14 at 17:10
  • 2
    Arguably, an exception could be made for the toString() method, because its existence is mandated by the Java Language Specification. This special case is probably not worth the extra complexity, though. – meriton Dec 23 '14 at 17:11
  • Now that's an explanation. Thank you. I think it would be good to put that stuff about making no assumptions about supertypes etc in your answer, because at the moment, the bit about `toString()` being specific to `Quod` comes across as rather odd. I'll definitely upvote the answer in that case. – Paul Boddington Dec 23 '14 at 17:16
  • This answer doesn't explain `why is it fine for the println() method to call its toString, but not for me to directly?` – Kshitiz Sharma Apr 27 '15 at 09:11
  • It does, to the apparent satisfaction of OP and 8 other people who upvoted this answer. It you have a specific question or comment about my answer, feel free to voice it, but if you don't tell me what you didn't understand, how should I know what to improve? – meriton Apr 27 '15 at 20:54
4

The crux of the question is:

And why is it fine for the println() method to call its toString, but not for me to directly?

ClassCastException exception isn't occurring due to calling toString() but due to an explicit cast added by the compiler.

A picture is worth thousand words, so let's look at some decompiled code.

Consider following code:

public static void main(String[] args) {
    List<String> s = new ArrayList<String>();
    s.add("kshitiz");
    List<Integer> i = new ArrayList(s);

    System.out.println(i.get(0)); //This works
    System.out.println(i.get(0).toString()); // This blows up!!!
}

Now look at the decompiled code:

public static void main(String[] args) {
    ArrayList s = new ArrayList();
    s.add("kshitiz");
    ArrayList i = new ArrayList(s);
    System.out.println(i.get(0));
    System.out.println(((Integer)i.get(0)).toString());
}

See the explicit cast to Integer? Now why didn't compiler add a cast in previous line? The signature of the method println() is:

public void println(Object x)

Since println is expecting an Object and result of i.get(0) is Object no cast is added.

It is also okay for you to invoke toString(), granted that you do it like this so that no cast is generated:

public static void main(String[] args) {
    List<String> s = new ArrayList<String>();
    s.add("kshitiz");
    List<Integer> i = new ArrayList(s);

    myprint(i.get(0));
}

public static void myprint(Object arg) {
    System.out.println(arg.toString()); //Invoked toString but no exception
}
Kshitiz Sharma
  • 17,947
  • 26
  • 98
  • 169