Java Type Erasure
Java Type Erasure
Type erasure is the mechanism the Java compiler uses to implement generics: it checks every generic operation against the type arguments you wrote, and then throws almost all of that information away before generating bytecode. List<String> and List<Integer> are different types as far as javac is concerned and identical types as far as the JVM is concerned — both compile down to a single List class operating on Object, with the compiler inserting casts wherever a typed value needs to come back out. Erasure is not an implementation detail you can ignore. It is the direct explanation for bridge methods, unchecked warnings, heap pollution, and most of the restrictions covered in the companion article on what generics cannot do.
What Is Type Erasure?
Type erasure is the process by which the compiler replaces every type parameter with its bound (or Object, if the type parameter is unbounded) once compile-time checking is complete, and removes generic type information from method and field signatures in the generated bytecode.
BEFORE ERASURE (source code, what the compiler sees):
class Box<T> {
private T value;
void set(T value) { this.value = value; }
T get() { return value; }
}
AFTER ERASURE (what the compiler generates - conceptually):
class Box {
private Object value;
void set(Object value) { this.value = value; }
Object get() { return value; }
}
AT THE CALL SITE - the compiler inserts a CHECKCAST:
Box<String> box = new Box<>();
box.set("Ananya");
String name = box.get(); // compiles to: String name = (String) box.get();
WITH A BOUND - erasure substitutes the bound, not Object:
class NumberBox<T extends Number> {
T value;
double asDouble() { return value.doubleValue(); } // Number method - no cast needed
}
// erases to:
class NumberBox {
Number value;
double asDouble() { return value.doubleValue(); }
}
Basic Overview - What Erasure Does and Why It Works
1. EVERY TYPE PARAMETER IS REPLACED BY ITS BOUND
Fresher view : an unbounded <T> becomes Object everywhere it appears
in the erased class. A bounded <T extends Number>
becomes Number - so Number's methods stay callable
on T without a cast, even after erasure.
Deeper view : erasure operates per type parameter, using the
LEFTMOST bound when multiple bounds are declared.
<T extends Number & Comparable<T>> erases T to
Number - calls to compareTo() on such a T require
an additional compiler-inserted cast to
Comparable, because Comparable itself was erased
away from T's visible type.
2. TWO PARAMETERIZATIONS SHARE ONE CLASS AT RUNTIME
Fresher view : new ArrayList<String>().getClass() and
new ArrayList<Integer>().getClass() return the
exact same Class object. The JVM has never heard
of "ArrayList of String" as a distinct type -
there is exactly one ArrayList.class.
Deeper view : the distinction between List<String> and
List<Integer> lives ONLY in the compiler's symbol
table during compilation. A class file does store
a Signature attribute recording the original
generic declaration for reflection and tooling -
but the verifier, the interpreter, and the JIT
never consult it. Every check the JVM performs at
runtime - instanceof, casts, array stores - is
erasure-blind.
3. BRIDGE METHODS RESTORE WHAT ERASURE WOULD OTHERWISE BREAK
Fresher view : when a class implements a generic interface with
a specific type argument, the compiler quietly
generates an extra method - a bridge method - so
that polymorphism still works correctly after
erasure collapses the signatures.
Deeper view : class IntBox implements Comparable<IntBox> declares
compareTo(IntBox). But Comparable itself erases to
compareTo(Object) - that is the method signature
the JVM actually dispatches on for the interface.
The compiler synthesizes a hidden
compareTo(Object o) { return compareTo((IntBox) o); }
marked ACC_BRIDGE and ACC_SYNTHETIC in the class
file, so virtual dispatch through Comparable still
reaches the real, specific method.
4. WHAT SURVIVES vs WHAT IS DISCARDED
Fresher view : reflection can still tell you the DECLARED generic
type of a field or method - Field.getGenericType(),
Method.getGenericReturnType() - because javac keeps
that information around for tools and debuggers.
What it can never tell you is which type argument
a specific OBJECT was built with at runtime.
Deeper view : the Signature class-file attribute is metadata
attached to the declaration (the class, method, or
field), not to any instance. There is no hidden
per-object field recording "this List holds
Strings." This is exactly why heap pollution is
possible in the first place - nothing at runtime
is left to detect it before a CHECKCAST fails.
Why Erasure Is Visible at Runtime
Two different parameterizations of the same generic class are, at runtime, the exact same class. This is the most direct, observable consequence of erasure, and it explains why instanceof cannot check a parameterized type and why raw types can silently defeat the entire type system.
1// File: ErasureBasicsDemo.java
2
3import java.util.*;
4
5public class ErasureBasicsDemo {
6
7 public static void main(String[] args) {
8
9 System.out.println("=== Two different parameterizations share ONE Class object ===");
10 List<String> strings = new ArrayList<>();
11 List<Integer> ints = new ArrayList<>();
12 System.out.println("strings.getClass() : " + strings.getClass());
13 System.out.println("ints.getClass() : " + ints.getClass());
14 System.out.println("Same Class object? : " + (strings.getClass() == ints.getClass()));
15
16 System.out.println();
17
18 System.out.println("=== instanceof only works against the erased raw type ===");
19 System.out.println("strings instanceof List : " + (strings instanceof List));
20 // if (strings instanceof List<String>) { } // COMPILE ERROR - illegal generic type for instanceof
21 System.out.println("strings instanceof List<?> : " + (strings instanceof List<?>));
22
23 System.out.println();
24
25 System.out.println("=== A raw-type reference can defeat compile-time checks entirely ===");
26 List<String> safe = new ArrayList<>();
27 List raw = safe; // raw type reference to the SAME erased List object
28 raw.add(42); // compiles - unchecked warning, no compile-time type check
29 try {
30 String s = safe.get(0); // CHECKCAST String inserted here - fails at THIS line
31 System.out.println("Never printed: " + s);
32 } catch (ClassCastException e) {
33 System.out.println("ClassCastException caught - heap pollution surfaced at retrieval");
34 }
35 }
36}Output:
=== Two different parameterizations share ONE Class object ===
strings.getClass() : class java.util.ArrayList
ints.getClass() : class java.util.ArrayList
Same Class object? : true
=== instanceof only works against the erased raw type ===
strings instanceof List : true
strings instanceof List<?> : true
=== A raw-type reference can defeat compile-time checks entirely ===
ClassCastException caught - heap pollution surfaced at retrieval
The failure never happens at raw.add(42) — that line compiles with only an unchecked warning, because the raw reference raw has already opted out of generics checking. It happens at safe.get(0), several lines later and in code that looks completely correct, because that is where the compiler's inserted CHECKCAST String finally runs against an Integer. This gap between where the mistake is made and where it is detected is the practical cost of erasure.
Bridge Methods
A bridge method is a synthetic method the compiler generates automatically when a class implements a generic interface (or overrides a generic superclass method) with a more specific type than the interface's erased signature would allow. Without it, erasure would silently break polymorphic dispatch through the interface.
1// File: BridgeMethodDemo.java
2
3import java.lang.reflect.Method;
4import java.util.*;
5
6public class BridgeMethodDemo {
7
8 // Implements Comparable<IntBox> - erasure requires an actual
9 // compareTo(Object) method on the class file, because Comparable
10 // itself erases to compareTo(Object). The compiler generates that
11 // method for us automatically.
12 static class IntBox implements Comparable<IntBox> {
13 final int value;
14 IntBox(int value) { this.value = value; }
15
16 @Override
17 public int compareTo(IntBox other) { // the method WE wrote
18 return Integer.compare(this.value, other.value);
19 }
20
21 @Override
22 public String toString() { return "IntBox(" + value + ")"; }
23 }
24
25 public static void main(String[] args) {
26
27 System.out.println("=== The class file has TWO compareTo methods, not one ===");
28 List<Method> compareToMethods = new ArrayList<>();
29 for (Method m : IntBox.class.getDeclaredMethods()) {
30 if (m.getName().equals("compareTo")) compareToMethods.add(m);
31 }
32 compareToMethods.sort(Comparator.comparing(Method::isBridge)); // declared (false) before bridge (true)
33 for (Method m : compareToMethods) {
34 System.out.printf(" compareTo(%s) - bridge=%s synthetic=%s%n",
35 m.getParameterTypes()[0].getSimpleName(),
36 m.isBridge(), m.isSynthetic());
37 }
38
39 System.out.println();
40
41 System.out.println("=== Both are callable - polymorphism through Comparable<IntBox> ===");
42 Comparable<IntBox> asComparable = new IntBox(10);
43 IntBox other = new IntBox(20);
44 System.out.println("asComparable.compareTo(other): " + asComparable.compareTo(other));
45
46 System.out.println();
47
48 System.out.println("=== Sorting uses the bridge - Collections.sort needs Comparable<T> ===");
49 List<IntBox> boxes = new ArrayList<>(List.of(new IntBox(30), new IntBox(10), new IntBox(20)));
50 Collections.sort(boxes);
51 System.out.println("Sorted: " + boxes);
52 }
53}Output:
=== The class file has TWO compareTo methods, not one ===
compareTo(IntBox) - bridge=false synthetic=false
compareTo(Object) - bridge=true synthetic=true
=== Both are callable - polymorphism through Comparable<IntBox> ===
asComparable.compareTo(other): -1
=== Sorting uses the bridge - Collections.sort needs Comparable<T> ===
Sorted: [IntBox(10), IntBox(20), IntBox(30)]
Collections.sort(boxes) calls compareTo through a Comparable reference internally — it has no idea IntBox even exists. That call resolves to the bridge method compareTo(Object), which casts its argument to IntBox and forwards to the real method. Without the bridge, IntBox would technically implement Comparable<IntBox> at the source level while failing to provide the compareTo(Object) method the erased interface actually requires — the compiler generates it so that gap never exists in the class file.
Heap Pollution and Generic Varargs
Heap pollution is the state where a variable of a parameterized type refers to an object that is not actually of that type — something the compiler's static checking is supposed to prevent, but cannot, once a raw type or an unchecked cast enters the picture. Generic varargs parameters are the single most common source of accidental heap pollution, because the array backing a T... parameter is always physically an Object[], regardless of what T is inferred to be at any call site.
1// File: HeapPollutionDemo.java
2
3import java.util.*;
4
5public class HeapPollutionDemo {
6
7 // UNSAFE - do NOT mark this @SafeVarargs. It lets the varargs array
8 // escape by returning it, which is exactly what @SafeVarargs promises does not happen.
9 static <T> T[] toArray(T... args) {
10 return args; // physically an Object[] - T[] is only a compile-time fiction here
11 }
12
13 static <T> T[] pickTwo(T a, T b, T c, int choice) {
14 switch (choice) {
15 case 0: return toArray(a, b);
16 case 1: return toArray(a, c);
17 default: return toArray(b, c);
18 }
19 }
20
21 // THE SAFE FIX - never create or return a generic array; use List<T> instead
22 static <T> List<T> pickTwoSafe(T a, T b, T c, int choice) {
23 return switch (choice) {
24 case 0 -> List.of(a, b);
25 case 1 -> List.of(a, c);
26 default -> List.of(b, c);
27 };
28 }
29
30 public static void main(String[] args) {
31
32 System.out.println("=== Generic varargs arrays are always physically Object[] ===");
33 Object[] erased = pickTwo("Good", "Fast", "Cheap", 0);
34 System.out.println("erased.getClass(): " + erased.getClass().getSimpleName());
35
36 System.out.println();
37
38 System.out.println("=== Assigning to String[] inserts a hidden CHECKCAST that fails ===");
39 try {
40 String[] attributes = pickTwo("Good", "Fast", "Cheap", 1);
41 System.out.println("Never printed: " + Arrays.toString(attributes));
42 } catch (ClassCastException e) {
43 System.out.println("ClassCastException at the assignment - heap pollution surfaced");
44 }
45
46 System.out.println();
47
48 System.out.println("=== The safe fix - return List<T>, never a generic varargs array ===");
49 List<String> safeResult = pickTwoSafe("Good", "Fast", "Cheap", 2);
50 System.out.println("safeResult: " + safeResult);
51 }
52}Output:
=== Generic varargs arrays are always physically Object[] ===
erased.getClass(): Object[]
=== Assigning to String[] inserts a hidden CHECKCAST that fails ===
ClassCastException at the assignment - heap pollution surfaced
=== The safe fix - return List<T>, never a generic varargs array ===
safeResult: [Fast, Cheap]
toArray and pickTwo both compile cleanly with only warnings — nothing about heap pollution is a compile error. The failure surfaces two calls away and one type away from its actual cause: attributes = pickTwo(...) looks like an ordinary assignment, but the object flowing through toArray's T... args parameter was Object[] from the moment it was created, and no amount of generic type argument juggling changes that physical fact.
Real-World Example - Practo Appointment Cache
A clinic-booking backend's in-memory Cache<K, V> is shared between a modern, fully-generic booking module and a legacy billing module that predates the codebase's adoption of generics and only knows about the raw Cache type. The example shows erasure making two parameterizations of Cache identical at runtime, and then shows exactly how a raw-typed caller silently pollutes a Cache<String, Appointment> that a typed caller trusts completely.
1// File: Cache.java
2
3import java.util.*;
4
5public class Cache<K, V> {
6
7 private final Map<K, V> store = new LinkedHashMap<>();
8 private final int maxEntries;
9
10 public Cache(int maxEntries) {
11 this.maxEntries = maxEntries;
12 }
13
14 public void put(K key, V value) {
15 if (store.size() >= maxEntries && !store.containsKey(key)) {
16 K oldest = store.keySet().iterator().next();
17 store.remove(oldest);
18 }
19 store.put(key, value);
20 }
21
22 public V get(K key) {
23 return store.get(key);
24 }
25
26 public int size() { return store.size(); }
27}1// File: Appointment.java
2
3public record Appointment(String patientName, String doctor, String slot) {}1// File: CacheErasureDemo.java
2
3import java.util.*;
4
5public class CacheErasureDemo {
6
7 // Simulates a legacy module written before this codebase adopted
8 // generics - it only knows about the raw Cache type.
9 @SuppressWarnings({"rawtypes", "unchecked"})
10 static void legacyBillingSync(Cache rawCache) {
11 // No compile-time check here at all - "INR 499 - unpaid" is a
12 // String, not an Appointment, and the raw type cannot tell.
13 rawCache.put("SLOT-9AM", "INR 499 - unpaid");
14 }
15
16 public static void main(String[] args) {
17
18 System.out.println("=== Erasure: two parameterizations share ONE Class object ===");
19 Cache<String, Appointment> appointments = new Cache<>(10);
20 Cache<String, String> notes = new Cache<>(10);
21 System.out.println("appointments.getClass(): " + appointments.getClass());
22 System.out.println("notes.getClass() : " + notes.getClass());
23 System.out.println("Same runtime class? : " + (appointments.getClass() == notes.getClass()));
24
25 System.out.println();
26
27 System.out.println("=== A well-typed Cache<String, Appointment> works safely ===");
28 appointments.put("SLOT-9AM", new Appointment("Ananya", "Dr. Rao", "9:00 AM"));
29 appointments.put("SLOT-10AM", new Appointment("Rohit", "Dr. Mehta", "10:00 AM"));
30 Appointment booked = appointments.get("SLOT-9AM");
31 System.out.println("booked: " + booked);
32
33 System.out.println();
34
35 System.out.println("=== Passing the SAME cache into legacy raw-typed code pollutes it ===");
36 legacyBillingSync(appointments); // compiles - a raw parameter accepts any Cache
37 try {
38 Appointment corrupted = appointments.get("SLOT-9AM"); // CHECKCAST Appointment inserted here
39 System.out.println("Never printed: " + corrupted);
40 } catch (ClassCastException e) {
41 System.out.println("ClassCastException on retrieval - the String legacy code stored");
42 System.out.println("looked fine to the raw-typed method, but violated Cache<String, Appointment>");
43 }
44
45 System.out.println();
46
47 System.out.println("=== Fix ===");
48 System.out.println("Fix: never expose a generic API as a raw type across a module boundary.");
49 }
50}Output:
=== Erasure: two parameterizations share ONE Class object ===
appointments.getClass(): class Cache
notes.getClass() : class Cache
Same runtime class? : true
=== A well-typed Cache<String, Appointment> works safely ===
booked: Appointment[patientName=Ananya, doctor=Dr. Rao, slot=9:00 AM]
=== Passing the SAME cache into legacy raw-typed code pollutes it ===
ClassCastException on retrieval - the String legacy code stored
looked fine to the raw-typed method, but violated Cache<String, Appointment>
=== Fix ===
Fix: never expose a generic API as a raw type across a module boundary.
legacyBillingSync(appointments) compiles without a single error — passing a Cache<String, Appointment> where a raw Cache is expected is exactly the kind of unchecked conversion erasure permits, because at the bytecode level there is only one Cache class regardless of what any caller believes its type arguments are. The bug is invisible until appointments.get("SLOT-9AM") is used the way its declared type promises, at which point the CHECKCAST the compiler inserted at that specific call site is what finally catches it — one method call and one field lookup away from where the actual mistake happened.
Type Erasure - Quick Reference
| Concept | What Survives Erasure | What Is Discarded |
|---|---|---|
Type parameter <T> | Erased to its bound (Object if unbounded) | The specific type argument used at any call site |
List<String> vs List<Integer> | Both compile to the same List class | Any runtime distinction - getClass() is identical |
Method signatures using T | The erased signature (e.g. Comparable → compareTo(Object)) | The originally-declared typed signature |
| Overriding with a more specific type | A synthetic bridge method matching the erased signature | Nothing - the bridge exists precisely so nothing is lost |
Reflection - Method.getGenericReturnType() | The declared generic signature, from a class-file Signature attribute | Per-instance knowledge of which type argument an object was built with |
instanceof / casts | Checks only against the erased raw type | The ability to check instanceof List<String> - illegal to even write |
Generic array creation new T[] | Nothing - disallowed entirely | Reliable array-store checking against the intended element type |
Best Practices
Treat every unchecked warning as a potential future ClassCastException, not noise to silence. An unchecked warning means the compiler could not verify a cast or conversion at compile time — it is not saying the code is wrong, it is saying it cannot prove the code is right. Fix the warning at its source when possible; only suppress it when you have personally verified the operation is safe.
Scope @SuppressWarnings("unchecked") as narrowly as possible, with a comment explaining why the cast is actually safe. Applying it to an entire method silences warnings for code the annotation was never meant to cover. Applying it to a single local variable or statement keeps the promise honest and keeps future unrelated unchecked operations in the same method visible.
Prefer List<T> over arrays whenever a generic type parameter is involved. Arrays are reified — they check their component type on every store — while generics are erased. Mixing the two, especially through generic varargs, is the most common real-world source of heap pollution; collections avoid the mismatch entirely.
When you need runtime type information erasure discarded, pass a Class<T> token explicitly instead of working around erasure with instanceof or unchecked casts. This is the standard idiom used throughout the JDK and frameworks for factories, deserializers, and dependency injection — it supplies at runtime exactly the information erasure removed at compile time.
Common Mistakes
Mistake 1 - Checking instanceof Against a Parameterized Type
1import java.util.*;
2
3List<String> list = new ArrayList<>();
4
5// WRONG - illegal generic type for instanceof; erasure means there is
6// no runtime record of the type argument to check against
7// if (list instanceof List<String>) { } // COMPILE ERROR
8
9// CORRECT - check against the raw type or an unbounded wildcard
10if (list instanceof List<?>) {
11 System.out.println("It's some kind of List");
12}Mistake 2 - Creating a Generic Array Directly
1// WRONG - generic array creation is a compile error regardless of context
2// T[] elements = new T[10];
3// List<String>[] lists = new List<String>[5];
4
5// CORRECT - accept a Class<T> token and create a properly-typed array via reflection
6@SuppressWarnings("unchecked")
7static <T> T[] newArray(Class<T> type, int size) {
8 return (T[]) java.lang.reflect.Array.newInstance(type, size);
9}Mistake 3 - Overloading Methods That Erase to the Same Signature
1import java.util.List;
2
3// WRONG - both methods erase to the identical signature process(List) -
4// class Processor {
5// void process(List<String> items) { }
6// void process(List<Integer> items) { } // COMPILE ERROR - name clash
7// }
8
9// CORRECT - give the methods different names
10class Processor {
11 void processStrings(List<String> items) { }
12 void processIntegers(List<Integer> items) { }
13}Mistake 4 - Suppressing an Unchecked Warning Without Verifying Safety
1import java.util.*;
2
3// WRONG - suppressing the warning does not make the cast safe;
4// it silences the compiler while the bug ships to production
5@SuppressWarnings("unchecked")
6static <T> List<T> unsafeCastList(List<?> source) {
7 return (List<T>) source; // no runtime check occurs - can hide a wrong-typed source
8}
9
10// CORRECT - verify each element as it is actually used
11static <T> List<T> checkedCopy(List<?> source, Class<T> elementType) {
12 List<T> result = new ArrayList<>();
13 for (Object item : source) {
14 result.add(elementType.cast(item)); // throws ClassCastException immediately if wrong
15 }
16 return result;
17}Interview Questions
Q1. What is type erasure and why does Java implement generics this way?
Type erasure is the process by which the compiler checks all generic type usage at compile time and then replaces every type parameter with its bound (Object for unbounded parameters) in the generated bytecode. Java chose this approach primarily for backward compatibility: generics were added in Java 5, and erasure allowed existing compiled classes and libraries from before Java 5 to keep working unchanged alongside newly-generic code, and allowed a single List.class to be usable both generically and, via raw types, exactly as it was before generics existed. The alternative — reification, keeping full type information at runtime as C# does — would have required recompiling the entire existing Java ecosystem.
Q2. Why do List
Because erasure means only one List class file exists regardless of how many different type arguments are used to declare List variables at the source level. new ArrayList<String>() and new ArrayList<Integer>() both execute the exact same constructor of the exact same ArrayList.class, and getClass() on either returns that one Class object. The type argument distinction is a compile-time-only concept enforced by the compiler's checks at every add and retrieval; the JVM never sees it.
Q3. What is a bridge method and when does the compiler generate one?
A bridge method is a synthetic method the compiler adds to a class file when a method's erased signature would otherwise differ from the signature needed for correct polymorphic dispatch — most commonly when a class implements a generic interface method with a covariant, more specific parameter type. For example, class IntBox implements Comparable<IntBox> needs an actual compareTo(Object) method to satisfy Comparable's erased contract; the compiler generates one automatically that casts its argument and delegates to the real compareTo(IntBox) method the developer wrote. Bridge methods are marked with the ACC_BRIDGE and ACC_SYNTHETIC flags and are invisible in ordinary source code.
Q4. What is heap pollution, and how does it relate to generic varargs?
Heap pollution occurs when a variable of a parameterized type ends up referring to an object that is not actually an instance of that parameterized type — something normal generics checking is supposed to prevent, but cannot once a raw type or unchecked cast is involved. Generic varargs (T... args) are a frequent source of it because the array backing that parameter is always physically created as Object[], regardless of the type argument inferred for any particular call — if that array escapes the method (by being returned, for instance), any later attempt to treat it as its "true" type T[] triggers a ClassCastException, often far from where the array was actually created.
Q5. Why can't you write instanceof List
Both are illegal for the same underlying reason: erasure removes the runtime information both operations would need. instanceof can only check an object's actual runtime class, and since List<String> and List<Integer> share the identical runtime class, there is nothing distinct to check against — only instanceof List<?> or the raw instanceof List are legal. Array creation new T[10] is disallowed because arrays are reified and enforce their component type on every store; creating an array whose component type is a type variable would mean the JVM has no real component type to enforce, defeating the entire purpose of array-store checking and opening the door to silent heap pollution.
Q6. What information about generics survives into the compiled class file, and how does reflection access it despite erasure?
The compiler stores the originally-declared generic type information — class type parameters, method signatures, field types — in a Signature attribute attached to the class, method, or field in the compiled .class file. This attribute exists purely for tools: the JVM's verifier, interpreter, and JIT ignore it completely and operate only on the erased signatures. Reflection APIs like Method.getGenericReturnType() and Field.getGenericType() parse this attribute to hand back the declared generic type at the level of the declaration — they can tell you a field was declared as List<String>, but they can never tell you whether a specific List object at runtime was actually populated with String elements, because that information was never stored anywhere.
FAQs
Does type erasure make generics "just syntactic sugar"?
Not quite — generics provide genuine compile-time type checking that catches real bugs before the program ever runs, which is far more than cosmetic syntax. What erasure does mean is that generics provide no runtime enforcement of their own; all the safety comes from the compiler having verified the code before erasure ran. Once a raw type or unchecked cast is introduced, that compile-time safety net has a hole in it, which is why "syntactic sugar" is a fair description only of the parts of generics that erasure removes, not the type checking itself.
Why did Java choose erasure instead of reifying generics like C#?
Backward compatibility with the enormous body of pre-Java-5 compiled code and libraries. C# added generics to the CLR itself and could recompile the whole ecosystem around a reified design; Java added generics as a purely compiler-level feature so that ArrayList.class compiled under Java 1.4 kept working, unmodified, when called from newly-generic Java 5 code, and so raw types remained legal for gradual migration. The tradeoff is exactly the topics this article covers - no runtime type checks on generic operations beyond what erasure-inserted casts happen to catch.
Can two overloaded methods differ only by generic type argument?
No. process(List<String>) and process(List<Integer>) both erase to process(List), and the compiler rejects the second declaration as a duplicate method - a "name clash" error, not merely a warning. Any two overloads must differ in their erased parameter types, not just their declared generic type arguments.
What is the Signature attribute in a class file?
It is an optional attribute the compiler writes into a class file to preserve the original generic declaration - the full parameterized type of a class, method, or field - for consumption by tools like the reflection API, debuggers, and IDEs. It has no effect on how the JVM executes the code; the bytecode instructions themselves always operate on the erased types, and the verifier checks only against those erased types.
Does erasure affect performance?
Not meaningfully. After erasure, List<String> and a raw List compile to essentially identical bytecode, and the JIT optimizes both the same way. The one real cost associated with generics is autoboxing - List<Integer> stores boxed Integer objects rather than primitive int values - but that cost comes from the absence of primitive type arguments (covered in the companion Restrictions article), not from erasure itself.
Summary
Type erasure is the single mechanism that explains nearly every surprising thing about Java generics: why List<String> and List<Integer> are the same class at runtime, why instanceof and array creation reject parameterized types, why bridge methods exist in every class implementing a generic interface with a specific type argument, and why heap pollution through raw types or generic varargs is possible at all despite the compiler's best efforts.
The pattern worth internalizing is where the safety actually comes from. Generics do not make the JVM type-safe - the JVM was never told about your type arguments in the first place. Generics make the compiler catch type errors before erasure removes the information needed to catch them at runtime. Every unchecked warning is the compiler telling you it can no longer guarantee that; every CHECKCAST failure downstream of one is that guarantee finally running out.
What to Read Next
Learn the things generics in Java won't let you do.