Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 264
WiErD0 Avatar asked Dec 22 '14 16:12

WiErD0


People also ask

How do generics work?

Generics enable the use of stronger type-checking, the elimination of casts, and the ability to develop generic algorithms. Without generics, many of the features that we use in Java today would not be possible.

How do generics work internally?

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. Again, the compile time type of test.

What is the biggest advantage of generics?

One of the big advantages of generics is performance. Using value types with non – generic collection classes results in boxing and unboxing when the value type is converted to a reference type and vice versa. Type Safety : Another feature of generics is type safety.

What is the exact meaning of generic?

A generic type is a single programming element that adapts to perform the same functionality for a variety of data types.


1 Answers

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 ;-).

like image 115
meriton Avatar answered Sep 21 '22 08:09

meriton