Type Erasure in Java

Understanding how generics work at compile-time and disappear at runtime

Think of type erasure like invisible ink that you write with during coding! While you're writing and compiling your code, the compiler can see all the generic type information (the invisible ink). But once the code is compiled and runs, all that type information disappears - it's like the ink becomes invisible to the runtime. The JVM (Java Virtual Machine) only sees raw types without the generic details. This is type erasure: generics are a compile-time feature that vanish at runtime!

What is Type Erasure?

Type erasure is the process where the Java compiler removes all generic type information during compilation. Generic types like <String>, <Integer>, and <List<String>> exist only at compile time for type safety. At runtime, they are erased and replaced with raw types (typically Object or the upper bound type).

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// Type Erasure: Generics disappear at runtime!
import java.util.*;
public class TypeErasureBasics {
public static void main(String[] args) {
// COMPILE TIME: Type information exists
List<String> stringList = new ArrayList<>();
stringList.add("Hello");
stringList.add("World");
List<Integer> intList = new ArrayList<>();
intList.add(1);
intList.add(2);
// What the COMPILER sees:
// - stringList is List<String>
// - intList is List<Integer>
// The compiler enforces type safety!
// What the JVM ACTUALLY sees at RUNTIME:
// - Both are just List (no type info!)
// - Type erasure removed <String> and <Integer>
System.out.println(stringList.getClass()); // class java.util.ArrayList
System.out.println(intList.getClass()); // class java.util.ArrayList
// Same class! No difference at runtime!
System.out.println(stringList.getClass().equals(intList.getClass())); // true!
// This is type erasure in action:
// Generics are a COMPILE-TIME feature for type safety
// At RUNTIME, all generic information is erased
// They become raw types (Object)
// Why is this useful?
// 1. Backward compatibility with pre-Java 5 code
// 2. Simpler JVM implementation
// 3. No runtime overhead
// 4. Smaller compiled code size
}
}

Before and After Compilation

Let's see how your generic code looks before and after compilation:

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
// BEFORE COMPILATION (What you write)
// =====================================
public class Box<T> {
private T value;
public void set(T value) {
this.value = value;
}
public T get() {
return value;
}
}
// Usage:
Box<String> box = new Box<>();
box.set("Hello");
String str = box.get(); // No cast needed!
// AFTER COMPILATION (What the JVM sees)
// ======================================
public class Box {
private Object value; // T becomes Object
public void set(Object value) {
this.value = value;
}
public Object get() {
return value;
}
}
// Usage becomes:
Box box = new Box(); // Raw type
box.set("Hello");
String str = (String) box.get(); // Cast added by compiler!
// The compiler:
// 1. Checks that you put String (type safety)
// 2. Then erases <T> and replaces with Object
// 3. Adds automatic casts where needed
// 4. Result: Backward compatible with old code!
// COMPILER TRANSFORMATION EXAMPLE
public class TransformationExample {
public static void main(String[] args) {
// Your code:
List<String> list = new ArrayList<>();
list.add("Hello");
String str = list.get(0);
// After type erasure, becomes:
// List list = new ArrayList();
// list.add("Hello");
// String str = (String) list.get(0); // Cast inserted!
// Compiler bridges the gap between generic code
// (which exists at compile-time)
// and raw types (which exist at runtime)
}
}

Bridge Methods and Type Erasure

When a generic class overrides a non-generic method, the compiler creates bridge methods to maintain type safety:

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
// Bridge Methods: How the compiler handles generic method overrides
// Non-generic base class
class Node {
Object data;
public void setData(Object data) {
this.data = data;
}
public Object getData() {
return data;
}
}
// Generic subclass trying to override with specific type
public class StringNode extends Node {
@Override
public void setData(String data) { // Specific type
super.setData(data);
}
@Override
public String getData() { // Specific type
return (String) super.getData();
}
}
// COMPILER TRANSFORMATION
// ========================
// The compiler generates bridge methods for type erasure:
public class StringNode extends Node {
// Your method (specific type)
public void setData(String data) {
super.setData(data);
}
// Bridge method (generic type) - COMPILER GENERATED!
public void setData(Object data) {
this.setData((String) data); // Calls your method
}
// Your method (specific type)
public String getData() {
return (String) super.getData();
}
// Bridge method (generic type) - COMPILER GENERATED!
public Object getData() {
return this.getData(); // Calls your method
}
}
// Why bridge methods?
// When type erasure happens:
// StringNode.setData(String) becomes StringNode.setData(Object)
// But base class already has Node.setData(Object)
// Bridge methods ensure both versions exist!
public class BridgeMethodDemo {
public static void main(String[] args) {
StringNode node = new StringNode();
// Your code (looks type-safe):
node.setData("Hello");
String data = node.getData();
// Also works with Object reference (polymorphic):
Node nodeRef = node;
nodeRef.setData("World"); // Uses bridge method internally
String data2 = (String) nodeRef.getData();
// Bridge methods make generic override work with
// both compile-time type safety AND runtime polymorphism!
}
}

Why Can't We Create Generic Arrays?

Arrays are reified (type information is preserved at runtime), but generics are erased. This creates a type safety conflict:

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
// Why can't we create generic arrays?
// Arrays are REIFIED (type info at runtime)
// Generics are ERASED (no type info at runtime)
// This creates an unsolvable conflict!
public class GenericArrayProblems {
public static void main(String[] args) {
// PROBLEM 1: Can't create generic arrays directly
// List<String>[] stringListArray = new List<String>[10]; // ✗ COMPILE ERROR!
// Why? T[] can't exist because T is erased to Object at runtime
// PROBLEM 2: ArrayStoreException would be runtime nightmare
Object[] objects = new String[10]; // OK: arrays are reified
objects[0] = 123; // No compile error! But ArrayStoreException at runtime!
// If generics were reified in arrays:
// List<String>[] lists = new List<String>[10];
// lists[0] = new ArrayList<Integer>(); // Should be error!
// But how would runtime enforce type safety?
// Generics don't exist at runtime (they're erased)
// PROBLEM 3: Heap pollution with arrays
String[] strings = new String[5];
Object[] objects2 = strings; // Arrays allow this (covariance)
objects2[0] = 123; // ArrayStoreException caught!
// With generics, this would be impossible to check:
// List<String>[] lists2 = new List<String>[5];
// Object[] objs = lists2;
// objs[0] = new ArrayList<Integer>(); // Can't catch this!
}
}
// SOLUTIONS TO GENERIC ARRAY PROBLEM
// Solution 1: Use List instead of array
public class ListSolution<T> {
private List<T> items = new ArrayList<>(); // Use List, not T[]
public void add(T item) {
items.add(item);
}
public T get(int index) {
return items.get(index);
}
public T[] toArray(T[] template) {
return items.toArray(template); // Ask caller for array type
}
}
// Solution 2: Use Object[] and cast (what ArrayList does internally)
public class GenericStack<E> {
private Object[] elements; // Store as Object[], not E[]
private int size = 0;
public GenericStack() {
elements = new Object[10]; // Create Object array
}
public void push(E element) {
if (size == elements.length) {
Object[] newArray = new Object[elements.length * 2];
System.arraycopy(elements, 0, newArray, 0, size);
elements = newArray;
}
elements[size++] = element;
}
public E pop() {
if (size == 0) throw new IllegalStateException("Stack empty");
@SuppressWarnings("unchecked")
E element = (E) elements[--size]; // Cast when retrieving
elements[size] = null;
return element;
}
}
// Solution 3: Use Class<T> parameter
public class GenericArrayFactory<T> {
private T[] array;
@SuppressWarnings("unchecked")
public GenericArrayFactory(Class<T> type, int length) {
// Use reflection to create correct type array
array = (T[]) java.lang.reflect.Array.newInstance(type, length);
}
public void set(int index, T value) {
array[index] = value;
}
public T get(int index) {
return array[index];
}
// Usage:
// GenericArrayFactory<String> factory = new GenericArrayFactory<>(String.class, 10);
// factory.set(0, "Hello"); // Type safe!
}

instanceof and Generic Limitations

Type erasure prevents you from using instanceof with generic types:

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
// instanceof with Generics: What works and what doesn't
import java.util.*;
public class InstanceofAndGenerics {
// DOESN'T WORK: Type parameter in instanceof
public static <T> void checkType1(Object obj) {
// if (obj instanceof T) { } // ✗ COMPILE ERROR!
// T doesn't exist at runtime (erased)
// Compiler: "T is a type variable, cannot use in instanceof"
}
// DOESN'T WORK: Parameterized type in instanceof
public static void checkType2(Object obj) {
// if (obj instanceof List<String>) { } // ✗ COMPILE ERROR!
// <String> is erased, so this is meaningless
// Runtime can't check if elements are Strings
}
// WORKS: Check raw type
public static void checkType3(Object obj) {
if (obj instanceof List) { // OK - no type parameter
List<?> list = (List<?>) obj;
System.out.println("It's a List, but we don't know element type");
}
}
// WORKS: Check raw type with cast
public static void checkType4(Object obj) {
if (obj instanceof ArrayList) { // Check specific raw type
ArrayList<?> list = (ArrayList<?>) obj;
System.out.println("It's an ArrayList");
}
}
// BEST: Use Class<T> parameter
public static <T> void checkType5(Object obj, Class<T> type) {
if (type.isInstance(obj)) { // Check with Class object
T element = type.cast(obj); // Safe cast
System.out.println("Object is " + type.getSimpleName());
}
}
public static void main(String[] args) {
// Examples:
List<String> stringList = new ArrayList<>();
stringList.add("Hello");
// This works:
if (stringList instanceof List) { // OK
System.out.println("It's a List");
}
// But we don't know if it's List<String> or List<Integer>
// at runtime! Type erasure removed that info.
List<Integer> intList = new ArrayList<>();
intList.add(42);
// At runtime, both have identical structure:
System.out.println(stringList.getClass().equals(intList.getClass())); // true
// Using Class parameter (best approach):
checkType5(stringList, String.class);
checkType5(intList, Integer.class);
checkType5(new String[5], String[].class);
// Checking if something is parameterized type:
if (stringList instanceof List) {
List<?> list = (List<?>) stringList; // Use wildcard
if (list.isEmpty()) {
System.out.println("List is empty");
}
}
}
// PATTERN: Generic method with type checking
public static <T> boolean isInstance(Object obj, Class<T> type) {
return type.isInstance(obj);
}
public static <T> T cast(Object obj, Class<T> type) {
if (!type.isInstance(obj)) {
throw new ClassCastException(
"Cannot cast " + obj.getClass() + " to " + type
);
}
return type.cast(obj);
}
}

Heap Pollution

Heap pollution occurs when a variable of a parameterized type refers to an object that is not of that parameterized type:

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
// Heap Pollution: When generics and raw types mix dangerously
import java.util.*;
public class HeapPollution {
// HEAP POLLUTION EXAMPLE 1: Mixing raw and generic types
public static void example1() {
// Create a List as raw type
List rawList = new ArrayList(); // Raw type (no generics)
// Add mixed types to it
rawList.add("String");
rawList.add(123);
rawList.add(true);
// Now assign to generic type variable
List<String> stringList = rawList; // Heap pollution!
// The variable says "I contain Strings"
// But the actual list contains String, Integer, and Boolean
// When you try to use it:
for (String s : stringList) {
System.out.println(s.length()); // What if s is actually an Integer?
}
// ClassCastException at runtime!
}
// HEAP POLLUTION EXAMPLE 2: Varargs with generics
public static void example2() {
// This is heap pollution-prone (generates compiler warning)
List<String>[] lists = createArrayOfLists("a", "b"); // Warning!
// Type erasure causes issues here
}
@SuppressWarnings("unchecked") // Suppressing heap pollution warning
public static <T> List<T>[] createArrayOfLists(T... elements) {
// Can't create List<T>[] directly
List[] lists = new ArrayList[2];
lists[0] = new ArrayList<>(Arrays.asList(elements));
lists[1] = new ArrayList<>(Arrays.asList(elements));
return lists; // Returning List[] as List<T>[] - heap pollution!
}
// HEAP POLLUTION EXAMPLE 3: From legacy code
public static void example3() {
// Old code (pre-Java 5):
List oldList = new ArrayList();
oldList.add("String");
oldList.add(123);
// New generic code tries to use it:
List<String> typedList = oldList; // Heap pollution!
// typedList declares it only has Strings
// But it actually has mixed types
// Accessing it:
String first = typedList.get(0); // OK: "String"
String second = typedList.get(1); // ClassCastException: 123 is not String!
}
// HOW TO AVOID HEAP POLLUTION
// GOOD: Don't mix raw and generic types
public static void good1() {
List<String> typedList = new ArrayList<>(); // Use generics from start
typedList.add("String");
// typedList.add(123); // Compile error! Type safe!
}
// GOOD: Don't use raw types with generic variables
public static void good2() {
List rawList = new ArrayList(); // Raw type
List<String> typedList = new ArrayList<>(); // Separate generic list
typedList.add("String");
// Keep them separate - don't assign raw to generic!
}
// GOOD: Use proper casting when mixing old/new code
public static void good3() {
List rawList = new ArrayList(); // Old code returns this
rawList.add("String");
// Properly check before using as generic
@SuppressWarnings("unchecked")
List<String> typedList = new ArrayList<>(rawList); // Copy with type safety
// Now typedList is safe
for (String s : typedList) {
System.out.println(s);
}
}
// GOOD: Use <?> wildcard for unknown generic types
public static void good4() {
List rawList = new ArrayList();
rawList.add("String");
rawList.add(123);
List<?> unknownList = rawList; // Admit we don't know the type
// Now compiler prevents unsafe access:
// unknownList.add("something"); // ✗ Compile error!
// This prevents accidentally adding wrong types
}
public static void main(String[] args) {
System.out.println("Heap Pollution occurs when:");
System.out.println("1. Raw types are assigned to generic variables");
System.out.println("2. Parameterized arrays are used (List<T>[])");
System.out.println("3. Mixing old (raw) code with new (generic) code");
System.out.println("4. Using varargs with generic types");
System.out.println();
System.out.println("Prevention:");
System.out.println("- Always use generics from the start");
System.out.println("- Don't mix raw and generic types");
System.out.println("- Use <?> wildcard when type is unknown");
System.out.println("- Be careful with legacy code");
}
}

Real Implications and Workarounds

Understanding type erasure helps you write better generic code and avoid runtime surprises:

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
// Real Implications of Type Erasure and Practical Workarounds
import java.lang.reflect.*;
import java.util.*;
public class TypeErasureImplications {
// IMPLICATION 1: Can't access type parameter at runtime
public class Container<T> {
private T value;
public void store(T value) {
this.value = value;
}
// PROBLEM: No way to know what T is!
public void printType() {
// System.out.println(T.class); // ✗ Won't compile - T doesn't exist
// System.out.println(value.getClass().getSimpleName()); // Gets actual runtime type
}
}
// WORKAROUND 1: Pass Class<T> as parameter
public class SmartContainer<T> {
private T value;
private Class<T> type;
public SmartContainer(Class<T> type) {
this.type = type;
}
public void store(T value) {
this.value = value;
}
public void printType() {
System.out.println("Type: " + type.getSimpleName()); // Now we know!
}
}
// IMPLICATION 2: Can't get actual type in nested generics
public void implication2() {
List<List<String>> nestedList = new ArrayList<>();
// Can only check outer type, not inner types
if (nestedList instanceof List) { // OK
// Can't do: if (nestedList instanceof List<List<String>>) - won't work
}
}
// WORKAROUND 2: Use reflection to get type from fields/methods
public class TypeResolver<T> {
public Class<?> getTypeParameter() throws Exception {
// Get the generic type from the class definition
ParameterizedType type =
(ParameterizedType) this.getClass().getGenericSuperclass();
return (Class<?>) type.getActualTypeArguments()[0];
}
}
// IMPLICATION 3: Serialization problems with generics
public class GenericData<T> {
private T data;
// Problem: Serialization doesn't preserve generic type
// When deserializing, you get T as Object, need to cast
}
// WORKAROUND 3: Store type information explicitly
public class SerializableData<T> {
private T data;
private Class<T> type;
public SerializableData(T data, Class<T> type) {
this.data = data;
this.type = type;
}
// Now you can deserialize with correct type
}
// IMPLICATION 4: Overloading is confusing with generics
public class Overload {
// These look different but are identical after type erasure!
public void process(List<String> list) {
// After erasure: void process(List list)
}
// public void process(List<Integer> list) { // ✗ Compile error!
// After erasure: would be the same: void process(List list)
}
// WORKAROUND 4: Use different method names or wrapper classes
public class BetterOverload {
public void processStrings(List<String> list) {}
public void processIntegers(List<Integer> list) {}
// Or:
public void process(StringList list) {}
public void process(IntegerList list) {}
}
// WORKAROUND 5: Use super type tokens (advanced pattern)
public class TypeToken<T> {
private final Type type;
protected TypeToken() {
Type superclass = getClass().getGenericSuperclass();
type = ((ParameterizedType) superclass).getActualTypeArguments()[0];
}
public Type getType() {
return type;
}
}
// Usage:
// TypeToken<List<String>> token = new TypeToken<List<String>>() {};
// Type type = token.getType(); // Actual generic type preserved!
public static void main(String[] args) throws Exception {
System.out.println("=== Type Erasure Implications ===
");
// Example 1: Lost type information
List<String> stringList = new ArrayList<>();
List<Integer> intList = new ArrayList<>();
System.out.println("Type erasure means:");
System.out.println("- stringList.getClass() = " + stringList.getClass());
System.out.println("- intList.getClass() = " + intList.getClass());
System.out.println("- Both are identical at runtime!
");
// Example 2: Workaround - track type explicitly
Map<String, Class<?>> registry = new HashMap<>();
registry.put("strings", String.class);
registry.put("integers", Integer.class);
System.out.println("Workaround - explicit type tracking:");
System.out.println("- Registered types: " + registry.keySet() + "
");
// Example 3: Type Token pattern
TypeToken<List<String>> stringListToken = new TypeToken<List<String>>() {};
System.out.println("Type Token pattern:");
System.out.println("- Type preserved: " + stringListToken.getType());
}
}

Key Concepts

Compile-Time Feature

Generics exist only during compilation for type checking. The compiler verifies type safety, then erases the information.

Reification vs Erasure

Arrays are reified (type info at runtime), but generics are erased (type info only at compile-time). This is why generic arrays don't work.

Raw Types

After erasure, generic types become raw types. List<String> becomes List, Box<Integer> becomes Box, etc.

Bridge Methods

Compiler creates bridge methods to make generic class overrides work correctly with type erasure.

Best Practices

  • Use generics for compile-time type safety, knowing they disappear at runtime
  • Can't create generic arrays - use List<T> instead of T[]
  • Can't use instanceof with type parameters - check raw type instead
  • Be aware of heap pollution when using legacy raw type code
  • Use Class<T> references when you need runtime type information
  • Avoid casting after using generics - let the compiler enforce types

Common Mistakes

Expecting type information at runtime

Why it's wrong: Type erasure means List<String> and List<Integer> are identical at runtime. Don't rely on instanceof with generic types!

Trying to create generic arrays with new T[]

Why it's wrong: T is erased to Object at runtime, and you can't create Object[], then cast it safely. Use ArrayList<T> instead.

Using instanceof with parameterized types

Why it's wrong: Runtime doesn't know about type parameters. Use if (obj instanceof List) not if (obj instanceof List<String>).

Mixing raw types and generics carelessly

Why it's wrong: This causes heap pollution. If you use raw types with legacy code, you lose type safety and get warnings.

Assuming different instantiations have different types

Why it's wrong: List<String>.class and List<Integer>.class don't exist. Both are just List.class at runtime.

Interview Tips

  • 💡Explain type erasure clearly: generics exist at compile-time, not runtime
  • 💡Understand why generic arrays can't be created (reification vs erasure conflict)
  • 💡Know what bridge methods are and why they're created
  • 💡Explain heap pollution and how it happens with raw types
  • 💡Show you understand instanceof limitations with generic types
  • 💡Demonstrate knowledge of Class<T> as a workaround for runtime type info
  • 💡Explain that type erasure allows backward compatibility with pre-Java 5 code