Type Erasure in Java
Generic type information exists only at compile time. Knowing what the compiler removes explains every generics restriction.
- What erasure is
- The evidence
- How bounds are erased
- The restrictions it causes
- No runtime type test
- No new T()
- No generic arrays
- No overloads that erase to the same signature
- Generic exceptions are impossible
- Static members cannot use the class parameter
- Heap pollution
- Generic varargs
- What survives erasure
- Getting a type at runtime, when you must
- Why erasure was chosen
- Common mistakes
- Best practices
- Practice
- Conclusion
-
Java Basics
- Introduction to Java
- Setting Up Java and Writing Your First Program
- Variables, Data Types and Literals in Java
- Type Casting and Type Conversion in Java
- Operators and Expressions in Java
- Input and Output in Java
- Comments, Keywords and Naming Conventions in Java
- Control Flow in Java: if, else and switch
- Loops in Java: for, while and do-while
- Methods
- Arrays and Strings
-
OOP
- Classes and Objects in Java
- Constructors in Java
- The this Keyword in Java
- The static Keyword in Java
- Encapsulation in Java
- Access Modifiers in Java
- Inheritance in Java
- Method Overriding and super in Java
- Polymorphism in Java
- Abstraction, Abstract Classes and Interfaces in Java
- Composition, Aggregation and Association in Java
- The Object Lifecycle in Java
- Core Java
- Exception Handling
-
Collections
- The Java Collections Framework
- List in Java: ArrayList, LinkedList, Vector and Stack
- Set in Java: HashSet, LinkedHashSet and TreeSet
- Map in Java: HashMap, LinkedHashMap and TreeMap
- How HashMap Works Internally in Java
- Queue and Deque in Java: ArrayDeque and PriorityQueue
- Iterators in Java
- Comparable and Comparator in Java
- Collections Utilities and Choosing the Right Collection
- Generics
- Java 8+
- Stream API
- Date and Time
- File and I/O
-
Multithreading
- Threads in Java: Processes, Runnable and Thread
- Thread Lifecycle in Java
- Synchronization in Java: synchronized and volatile
- Locks and Atomic Classes in Java
- Race Conditions and Deadlocks in Java
- The Executor Framework and Thread Pools in Java
- Future and CompletableFuture in Java
- Concurrent Collections in Java
- The Java Memory Model
- JVM and Memory
- Advanced Java
- Networking
- JDBC
- Testing
What erasure is
Generics were added in Java 5, and existing bytecode had to keep working. The solution was type erasure: the compiler checks types, then removes the type arguments and inserts casts where needed. At runtime there is no generic information left.
// What you write
List<String> names = new ArrayList<>();
names.add("Meera");
String first = names.get(0);
// Roughly what the bytecode holds
List names = new ArrayList();
names.add("Meera");
String first = (String) names.get(0); // a cast the compiler proved is safeThe evidence
List<String> texts = new ArrayList<>();
List<Integer> numbers = new ArrayList<>();
System.out.println(texts.getClass() == numbers.getClass()); // true
System.out.println(texts.getClass().getSimpleName()); // ArrayListBoth are the same class at runtime. The type argument existed only for the compiler.
How bounds are erased
| Declared | Erased to |
|---|---|
<T> | Object |
<T extends Number> | Number |
<T extends Comparable<T> & Serializable> | Comparable, the first bound |
List<String> | List |
The restrictions it causes
No runtime type test
// if (value instanceof List<String>) // compile error
if (value instanceof List<?> list) { } // the only form availableNo new T()
public class Factory<T> {
// public T create() { return new T(); } // T does not exist at runtime
}
// The usual workaround: pass a supplier
public class Factory<T> {
private final Supplier<T> supplier;
public Factory(Supplier<T> supplier) {
this.supplier = supplier;
}
public T create() {
return supplier.get();
}
}
Factory<ArrayList<String>> factory = new Factory<>(ArrayList::new);No generic arrays
// List<String>[] arrays = new List<String>[10]; // compile error
@SuppressWarnings("unchecked")
List<String>[] arrays = new List[10]; // legal but uncheckedArrays check their element type at runtime, and generics have no runtime type to check. Mixing them is unsound, so the compiler forbids the direct form. Use List<List<String>> instead.
No overloads that erase to the same signature
public class Printer {
// void print(List<String> items) { }
// void print(List<Integer> items) { } // both erase to print(List)
}Generic exceptions are impossible
// class MyException<T> extends Exception { } // not allowedA catch clause matches types at runtime, and the type argument would be gone.
Static members cannot use the class parameter
public class Holder<T> {
// private static T shared; // not allowed
// static void set(T value) { } // not allowed
static <U> void helper(U value) { } // a method level parameter is fine
}Heap pollution
List<String> texts = new ArrayList<>();
List raw = texts; // raw type, checks disabled
raw.add(42); // no complaint at this line
String value = texts.get(0); // ClassCastException here, far from the causeHeap pollution is a variable of a parameterised type referring to an object that is not of that type. Erasure makes it possible, and the failure surfaces at an unrelated line, which is why raw types are worth avoiding entirely.
Generic varargs
@SafeVarargs
public static <T> List<T> listOf(T... items) {
return new ArrayList<>(Arrays.asList(items));
}A varargs parameter becomes an array, and a generic array is unsound, so the compiler warns. @SafeVarargs asserts that the method only reads the array and never stores into it or lets it escape. It is allowed on static, final and private methods and on constructors.
What survives erasure
public class NoteRepository implements Repository<Note, Long> { }
Type type = NoteRepository.class.getGenericInterfaces()[0];
System.out.println(type); // Repository<Note, java.lang.Long>Type arguments used in a class or method declaration are kept in the class file as metadata, so reflection can read them. What is erased is the type of a value at runtime. This distinction is what lets frameworks discover a declared type while list.getClass() still cannot tell you its element type.
Getting a type at runtime, when you must
public class TypedBox<T> {
private final Class<T> type;
public TypedBox(Class<T> type) {
this.type = type;
}
public T cast(Object value) {
return type.cast(value); // the class object carries the type
}
}
TypedBox<String> box = new TypedBox<>(String.class);Passing a Class object is the standard way to recover the type the compiler discarded. The library uses this pattern in methods such as Collections.checkedList.
Why erasure was chosen
- Existing libraries kept working without recompilation.
- Generic and non generic code could be mixed during the transition.
- No new bytecode or JVM changes were required.
The cost is every restriction above. Some other languages keep type arguments at runtime, at the price of a compatibility break Java was unwilling to make.
Common mistakes
- Expecting
instanceof List<String>to work. - Trying
new T()ornew T[n]. - Mixing raw and parameterised types and getting an exception far from the cause.
- Overloading on methods that erase to the same signature.
- Assuming reflection can report the element type of an existing list.
Best practices
- Never use raw types.
- Pass a
Class<T>when the type is genuinely needed at runtime. - Use
List<List<T>>rather than arrays of generic types. - Treat every unchecked warning as something to fix or to justify with a comment.
- Use
@SafeVarargsonly when the method truly cannot pollute the heap.
Practice
- Show that
List<String>andList<Integer>have the same runtime class. - Explain why a generic class cannot extend
Throwable. - Reproduce heap pollution with a raw type and identify the line that actually throws.
- Rewrite a class that needs
new T()so that it takes aSupplier<T>. - Why can two methods differing only in
List<String>andList<Integer>not coexist?
Conclusion
The compiler checks generics and then throws the type arguments away. Every restriction, no new T(), no generic arrays, no runtime type tests, follows from that one decision, and knowing it turns confusing errors into predictable ones.