Type Erasure in Java
Here's a fact that explains almost every strange rule about generics: the JVM has no idea generics exist. The compiler uses your type arguments to check the code, then erases them, replacing each type parameter with Object (or its bound) and inserting casts where needed. At runtime, a List<String> and a List<Integer> are both just ArrayList. Java did this so that generic code could run on existing JVMs and work with old, non-generic libraries.
Proof That the Types Are Gone
import java.util.ArrayList;
import java.util.List;
public class Erased {
static class Box<T> {
T value;
T get() { return value; }
}
static class NumberBox<T extends Number> {
T value;
}
public static void main(String[] args) throws Exception {
List<String> names = new ArrayList<>();
List<Integer> marks = new ArrayList<>();
System.out.println(names.getClass() == marks.getClass());
System.out.println(names.getClass().getName());
System.out.println(Box.class.getDeclaredField("value").getType());
System.out.println(Box.class.getDeclaredMethod("get").getReturnType());
System.out.println(NumberBox.class.getDeclaredField("value").getType());
}
}true
java.util.ArrayList
class java.lang.Object
class java.lang.Object
class java.lang.NumberHow it works
- A
List<String>and aList<Integer>have the same runtime class,java.util.ArrayList. The type argument isn't stored in the object. - In the compiled
Box<T>, the fieldT valuehas becomeObject value, andget()returnsObject. - In
NumberBox<T extends Number>,Tis erased to its bound,Number.
What the Compiler Writes Instead
javap -c shows the erased calls and the inserted cast for names.add("Navika"); String first = names.get(0);:
11: invokeinterface #12, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
19: invokeinterface #18, 2 // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
24: checkcast #22 // class java/lang/Stringadd takes an Object and get returns an Object, exactly like the old raw List. The checkcast to String is the cast you no longer write yourself. Because the compiler already refused every non-String add, that cast can never fail.
Consequences You'll Meet
| Because of erasure... | Example |
|---|---|
You can't create new T() or new T[10] | The runtime doesn't know what T is |
You can't use instanceof List<String> | The <String> part doesn't exist at runtime |
| Two overloads can't differ only by type argument | print(List<String>) and print(List<Integer>) both erase to print(List) |
Static fields can't use T | There's one class, shared by every Box<...> |
| Bridge methods are sometimes generated | See "Bridge Methods in Java" |
Each of these is covered with its compile error in "Restrictions of Generics in Java".
Common Mistakes
Expecting list.getClass() to say ArrayList<String>. It's just ArrayList.
Trying to check obj instanceof List<String>. Check obj instanceof List<?>, then check elements.
Overloading methods on different type arguments. Give them different names.
Interview Corner
What is type erasure?
Type erasure is how Java implements generics: the compiler uses type parameters for compile-time checking, then removes them, replacing each type parameter with its bound or Object, inserting casts where values are read, and generating bridge methods where needed. The resulting bytecode is like pre-generics code, so List<String> and List<Integer> share one runtime class. This kept generics compatible with older JVMs and libraries.
What are the consequences of type erasure?
You can't create instances or arrays of a type parameter, use instanceof with a parameterized type, overload methods that differ only in type arguments, use a type parameter in a static context, or create generic exception classes. Runtime reflection on an object can't recover its type arguments.
Practice
- Easy: Print
getClass()for aHashMap<String, Integer>and aHashMap<Long, String>, and compare them. - Harder: Write
class Holder<T extends CharSequence>with a fieldT text, and use reflection to print the field's erased type and its generic type.
Key Takeaways
Was this tutorial helpful?