简体   繁体   中英

How exactly do Generics work?

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.

Java generics are implemented through type erasure, ie 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 ;-).

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
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM