Java Generics


Bhaskar S 11/04/2008


Generic programming is a programming technique in which one designs and develops reusable components and algorithms using parameterized data types. This feature was added to the Java programming language with the JDK 5 release.

Consider the following example:

Listing.1
import java.util.Iterator;
import java.util.List;
import java.util.LinkedList;

public class Sample01 {
public static void main(String[] args) {
List lst = new LinkedList();
lst.add("1");
lst.add("2");
lst.add("3");

for (Iterator it = lst.iterator(); it.hasNext(); ) {
String s = (String) it.next();
System.out.println(s);
}
}
}

The above example creates a List, adds few String objects to the List, and iterates through the List. In line 13, every time we get the next item from the List, we are forced to type-cast to String. This code is ugly.

Next, consider the same code with a slight change in line 11 as shown below:

Listing.2
import java.util.Iterator;
import java.util.List;
import java.util.LinkedList;

public class Sample02 {
public static void main(String[] args) {
List lst = new LinkedList();
lst.add("1");
lst.add("2");
lst.add("3");
lst.add(new Integer(4));

for (Iterator it = lst.iterator(); it.hasNext(); ) {
String s = (String) it.next();
System.out.println(s);
}
}
}

The above code will compile just fine. Note we add an Integer to the List at line 11. When the code is executed, it would throw a runtime Exception at line 13. This code is unsafe.

With Generics, one would specify the actual data type to be used as a parameterized type. This helps in eliminating type-casts and more importantly, allows the compiler to check for type safety.

The following is the same example using Generics:

Listing.3
import java.util.List; 
import java.util.LinkedList;

public class Sample03 {
public static void main(String[] args) {
List<String> lst = new LinkedList<String>();
lst.add("1");
lst.add("2");
lst.add("3");
// lst.add(new Integer(4)); // Error if uncommented

for (String s : lst) {
System.out.println(s);
}
}
}

The above code is much more elegant and safe (uncommenting line 10 results in compiler error).

Now, we are curious and would like to understand how Generics is implemented in Java. To do that, let us compare the bytecode generated by the Java compiler for the code Sample01.java and the code Sample03.java.

To dump the bytecode of a compiled Java class file, use the javap command.

Execute the following command on Sample01:

$ javap -c Sample01

The following is the bytecode output for Sample01:

Output.1

Compiled from "Sample01.java" 
public class Sample01 extends java.lang.Object{
public Sample01();
Code:
0: aload_0
1: invokespecial #8; //Method java/lang/Object."<init>":()V
4: return

public static void main(java.lang.String[]);
Code:
0: new #16; //class java/util/LinkedList
3: dup
4: invokespecial #18; //Method java/util/LinkedList."<init>":()V
7: astore_1
8: aload_1
9: ldc #19; //String 1
11: invokeinterface #21, 2; //InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
16: pop
17: aload_1
18: ldc #27; //String 2
20: invokeinterface #21, 2; //InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
25: pop
26: aload_1
27: ldc #29; //String 3
29: invokeinterface #21, 2; //InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
34: pop
35: aload_1
36: invokeinterface #31, 1; //InterfaceMethod java/util/List.iterator:()Ljava/util/Iterator;
41: astore_2
42: goto 62
45: aload_2
46: invokeinterface #35, 1; //InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
51: checkcast #41; //class java/lang/String
54: astore_3
55: getstatic #43; //Field java/lang/System.out:Ljava/io/PrintStream;
58: aload_3
59: invokevirtual #49; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
62: aload_2
63: invokeinterface #55, 1; //InterfaceMethod java/util/Iterator.hasNext:()Z
68: ifne 45
71: return
}

Now, execute the following command on Sample03:

$ javap -c Sample03

The following is the bytecode output for Sample03:

Output.2

Compiled from "Sample03.java" 
public class Sample03 extends java.lang.Object{
public Sample03();
Code:
0: aload_0
1: invokespecial #8; //Method java/lang/Object."<init>":()V
4: return

public static void main(java.lang.String[]);
Code:
0: new #16; //class java/util/LinkedList
3: dup
4: invokespecial #18; //Method java/util/LinkedList."<init>":()V
7: astore_1
8: aload_1
9: ldc #19; //String 1
11: invokeinterface #21, 2; //InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
16: pop
17: aload_1
18: ldc #27; //String 2
20: invokeinterface #21, 2; //InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
25: pop
26: aload_1
27: ldc #29; //String 3
29: invokeinterface #21, 2; //InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
34: pop
35: aload_1
36: invokeinterface #31, 1; //InterfaceMethod java/util/List.iterator:()Ljava/util/Iterator;
41: astore_3
42: goto 62
45: aload_3
46: invokeinterface #35, 1; //InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
51: checkcast #41; //class java/lang/String
54: astore_2
55: getstatic #43; //Field java/lang/System.out:Ljava/io/PrintStream;
58: aload_2
59: invokevirtual #49; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
62: aload_3
63: invokeinterface #55, 1; //InterfaceMethod java/util/Iterator.hasNext:()Z
68: ifne 45
71: return
}

Comparing the bytecode for the two shows no differences except for the class name. This is very interesting !!!

From the Java documentation, we learn that Java Generics is implemented using Type Erasure. What this means is that the compiler erases all information about data type passed as parameterized type. This is the reason the generated bytecode for Sample03 is no different from Sample01. The reason why Sun chose this approach was to maintain binary compatibility with Java libraries that were created prior to Generics.