The use of Generics allows a type or method to operate on objects of different types while providing compile-time type safety. However, the JVM knows nothing about type parameters or generics as the compiler erases type parameters when creating bytecode.
According to https://docs.oracle.com/javase/tutorial/java/generics/erasure.html, type erasure aim is
“Type erasure ensures that no new classes are created for parameterized types; consequently, generics incur no runtime overhead.”
In the following examples, we will explore the bytecode resulting of compiling a class with generics and how that code enforces type safety at runtime.
Example 1
Given the class
public class MyClass { public void print(List<String> list){ } }
After compiling
javac -cp myclasses/ -g -d myclasses/ src/main/java/fjab/generics/MyClass.java
the resulting bytecode does not keep the parametized type as the descriptor of the method is just a raw type.
javap -v myclasses/fjab/generics/MyClass.class
public void print(java.util.List<java.lang.String>); descriptor: (Ljava/util/List;)V flags: ACC_PUBLIC Code: stack=0, locals=2, args_size=2 0: return LineNumberTable: line 11: 0 LocalVariableTable: Start Length Slot Name Signature 0 1 0 this Lfjab/generics/MyClass; 0 1 1 list Ljava/util/List; LocalVariableTypeTable: Start Length Slot Name Signature 0 1 1 list Ljava/util/List<Ljava/lang/String;>; Signature: #18 // (Ljava/util/List<Ljava/lang/String;>;)V
Example 2
Given the class
public class MyClass { public void print(List<String> list){} public void print(List<Integer> list){} }
When compiling, the compiler complains because, after erasure, there are two methods with the same signature:
javac -cp myclasses/ -g -d myclasses/ src/main/java/fjab/generics/MyClass.java src/main/java/fjab/generics/MyClass.java:11: error: name clash: print(List<Integer>) and print(List<String>) have the same erasure public void print(List<Integer> list){} ^ 1 error
Example 3
Despite type erasure, the class file keeps knowledge of the original method signature and the compiler uses that information to enforce type safety.
Given these classes
public class MyClass { public void print(List<String> list){} }
public class MyApp { public static void main(String[] args){ List<Integer> src = new ArrayList<>(); MyClass.print(src); } }
Firstly MyClass is compiled successfully
javac -cp myclasses/ -g -d myclasses/ src/main/java/fjab/generics/MyClass.java
Then, MyApp fails to compile as ‘src’ is not a List of Strings but Integers.
javac -cp myclasses/ -g -d myclasses/ src/main/java/fjab/generics/MyApp.java src/main/java/fjab/generics/MyApp.java:14: error: incompatible types: List<Integer> cannot be converted to List<String> MyClass.print(src); ^ Note: Some messages have been simplified; recompile with -Xdiags:verbose to get full output 1 error
Since the classes are compiled separately, when compiling MyApp.java the compiler needs to access MyClass.class to know that the types are incompatible.
Example 4
So the compiler ensures type safety. What if MyClass is changed after the initial compilation and the types are not compatible?
Let’s start with these classes:
public class MyClass { public static void print(List<String> list){ list.get(0).getBytes(); } }
public class MyApp { public static void main(String[] args){ List<String> src = new ArrayList<>(); src.add("hello world"); MyClass.print(src); } }
and get them compiled successfully
javac -cp myclasses/ -g -d myclasses/ src/main/java/fjab/generics/*.java
Now, if ‘MyClass’ is changed for the method ‘print’ to accept a List of Integers
public class MyClass { public static void print(List<Integer> list){ list.get(0).intValue(); } }
It gets compiled successfully
javac -cp myclasses/ -g -d myclasses/ src/main/java/fjab/generics/MyClass.java
However, when MyApp is executed, the application throws a ClassCastException
java -cp myclasses/ fjab.generics.MyApp Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer at fjab.generics.MyClass.print(MyClass.java:11) at fjab.generics.MyApp.main(MyApp.java:15)
Examining the bytecode, it is clear why. The application tries to cast the elements of the List to the expected type and when it finds a String, it throws the Exception. This way, despite the type erasure, the bytecode enforces type safety at runtime.
javap -v myclasses/fjab/generics/MyClass.class
public static void print(java.util.List<java.lang.Integer>); descriptor: (Ljava/util/List;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=1, args_size=1 0: aload_0 1: iconst_0 2: invokeinterface #2, 2 // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object; 7: checkcast #3 // class java/lang/Integer 10: invokevirtual #4 // Method java/lang/Integer.intValue:()I 13: pop 14: return LineNumberTable: line 11: 0 line 12: 14 LocalVariableTable: Start Length Slot Name Signature 0 15 0 list Ljava/util/List; LocalVariableTypeTable: Start Length Slot Name Signature 0 15 0 list Ljava/util/List<Ljava/lang/Integer;>; Signature: #21 // (Ljava/util/List<Ljava/lang/Integer;>;)V
Technical details
This examples have been run on OS X 10.11.1 using:
java -version
java version “1.8.0_45”
Java(TM) SE Runtime Environment (build 1.8.0_45-b14)
Java HotSpot(TM) 64-Bit Server VM (build 25.45-b02, mixed mode)
javac -version
javac 1.8.0_45