Q: How do Generics work in Java? What is Type Erasure?
Answer:
Generics
Generics enable type-safe, parameterized classes, interfaces, and methods. They catch type errors at compile time instead of runtime.
// Without generics: runtime ClassCastException risk
List list = new ArrayList();
list.add("hello");
Integer x = (Integer) list.get(0); // 💥 ClassCastException at RUNTIME
// With generics: compile-time safety
List<String> list = new ArrayList<>();
list.add("hello");
// list.add(42); // ❌ Compilation error — caught EARLY
String x = list.get(0); // No cast needed
Type Erasure
Java generics are a compile-time feature only. The compiler uses generic type information for type checking, then erases all generic types and replaces them with their bounds (or Object).
// What you write:
List<String> strings = new ArrayList<>();
List<Integer> ints = new ArrayList<>();
// After type erasure (what the JVM sees):
List strings = new ArrayList(); // Just "List" — type info is GONE
List ints = new ArrayList();
// At runtime:
strings.getClass() == ints.getClass(); // true! Both are just ArrayList
Consequences of Type Erasure
// ❌ Cannot do these at runtime:
if (obj instanceof List<String>) { } // Compilation error
new T(); // Cannot instantiate type parameter
T[] array = new T[10]; // Cannot create generic array
// ❌ Cannot overload with different generic types:
void process(List<String> list) { }
void process(List<Integer> list) { } // Compilation error — same erasure!
Bounded Type Parameters
// Upper bound: T must be Comparable or its subtype
public <T extends Comparable<T>> T findMax(List<T> list) {
return list.stream().max(Comparator.naturalOrder()).orElseThrow();
}
// Multiple bounds
public <T extends Serializable & Comparable<T>> void process(T item) { }
Wildcards
// Upper-bounded: read-only (producer)
void printAll(List<? extends Number> numbers) {
for (Number n : numbers) { System.out.println(n); }
// numbers.add(42); ❌ Cannot add — compiler doesn't know the exact type
}
// Lower-bounded: write-only (consumer)
void addIntegers(List<? super Integer> list) {
list.add(42); // ✅ Can add Integer or subtypes
// Integer x = list.get(0); ❌ Can only read as Object
}
// Unbounded: completely read-only
void countElements(List<?> list) {
System.out.println(list.size());
}
PECS: Producer Extends, Consumer Super
The mnemonic for remembering wildcard usage:
? extends T→ Read from the collection (it produces items).? super T→ Write to the collection (it consumes items).
[!TIP] In interviews, type erasure is the key insight. "Generics provide compile-time safety but are erased at runtime. This means you can't do runtime type checks on generic types or create generic arrays — it's all synthetic compiler enforcement."