Level 2 · 20 min
Generics
Java generics enable type-safe collections and algorithms at compile time. Understanding type erasure, bounded wildcards, and the PECS principle is essential for writing reusable library code and avoiding ClassCastException surprises at runtime.
Type Erasure and Reifiable Types
Java generics use type erasure: generic type parameters are removed at compile time and replaced with Object (or the upper bound for bounded types). This is for backward compatibility with pre-generics bytecode. Consequence: List<String> and List<Integer> are the same type at runtime — you cannot do instanceof List<String> or create new T[]. Reifiable types are types whose full type information is available at runtime: non-generic classes, raw types, unbounded wildcards (List<?>), and arrays of reifiable types. Because of erasure, generic types are NOT reifiable.
Bounded Wildcards: ? extends T / ? super T
? extends T (upper bounded) means 'some type that is T or a subtype of T'. ? super T (lower bounded) means 'some type that is T or a supertype of T'. Key insight: you can READ from a ? extends T collection (you get back at least a T), but you CANNOT WRITE to it (the compiler doesn't know the exact type). You can WRITE to a ? super T collection (you can write a T since it's a subtype), but you get back Object when you READ. This asymmetry is PECS. Effective Java formalizes PECS precisely: 'PECS stands for producer-extends, consumer-super. In other words, if a parameterized type represents a T producer, use <? extends T>; if it represents a T consumer, use <? super T>.' Naftalin and Wadler call this the Get and Put Principle. Type erasure means that at runtime, List<String> and List<Integer> are both just List — the generic type information is stripped by the compiler and replaced with casts. This is why you cannot create a generic array (new T[10] is illegal), cannot use instanceof with parameterized types (list instanceof List<String> is illegal), and cannot overload methods that differ only in their type parameters (both void process(List<String>) and void process(List<Integer>) erase to the same void process(List) signature, causing a compile error).
PECS: Producer Extends, Consumer Super
PECS (Producer Extends, Consumer Super) is the rule for choosing wildcards. Use ? extends T when the parameterized type is a producer — you're reading (producing) values from it. Use ? super T when the parameterized type is a consumer — you're writing (consuming) values into it. Example: Collections.copy(List<? super T> dest, List<? extends T> src) — src produces T (extends), dest consumes T (super). Generic methods use type parameters for maximum flexibility: <T extends Comparable<T>> T max(List<T> list) — works with any Comparable type.
Code example
// Type erasure — both are List at runtime
List<String> strings = new ArrayList<>();
List<Integer> ints = new ArrayList<>();
// strings.getClass() == ints.getClass() → true!
// PECS: copy from producer (extends) to consumer (super)
public static <T> void copy(
List<? super T> dest,
List<? extends T> src) {
for (T item : src) dest.add(item);
}
// Generic method: works for any Comparable type
public static <T extends Comparable<T>> T max(List<T> list) {
T result = list.get(0);
for (T item : list)
if (item.compareTo(result) > 0) result = item;
return result;
}
// Cannot create generic array — compile error:
// T[] arr = new T[10]; // ERROR: generic array creation
// Workaround:
@SuppressWarnings("unchecked")
T[] arr = (T[]) new Object[10]; // cast + suppress