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).
// 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:
// 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 typebox.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 EXAMPLEpublic 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:
// Bridge Methods: How the compiler handles generic method overrides// Non-generic base classclass Node { Object data; public void setData(Object data) { this.data = data; } public Object getData() { return data; }}// Generic subclass trying to override with specific typepublic 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:
// 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 arraypublic 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> parameterpublic 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:
// instanceof with Generics: What works and what doesn'timport 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:
// Heap Pollution: When generics and raw types mix dangerouslyimport 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:
// Real Implications of Type Erasure and Practical Workaroundsimport 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