Level 2 · 20 min
Generics in OOP
Generics provide compile-time type safety and eliminate the need for explicit casts. Java generics use type erasure — generic type information is removed at compile time — which has implications for reflection and certain patterns.
Generics and Type Safety
Without generics: List list = new ArrayList(); list.add('hello'); String s = (String) list.get(0) — cast can fail at runtime. With generics: List<String> list = new ArrayList<>() — the compiler guarantees the list only contains Strings. ClassCastException becomes a compile error. Generic methods: <T extends Comparable<T>> T max(T a, T b) — works for any Comparable type.
Type Erasure
Java generics use erasure — at runtime, List<String> and List<Integer> are both just List. The generic type T is erased to Object (or to the bound if bounded). This means: you cannot use T in instanceof checks, you cannot create new T(), and List<String>.class doesn't exist — only List.class. Erasure is the reason Java's generics are sometimes called 'fake generics' compared to C#.
Wildcards and PECS
PECS: Producer Extends, Consumer Super. List<? extends Shape> produces Shapes — you can read Shapes from it but not write (type unknown). List<? super Circle> consumes Circles — you can write Circles into it but reading gives Object. Bounded type parameters: <T extends Comparable<T>> means T must implement Comparable. Useful for algorithms that need to compare elements. Bloch's Item 31 (Effective Java) formalizes PECS with a stack example: pushAll(Iterable<? extends E> src) — the source is a producer of E, so extends applies; popAll(Collection<? super E> dst) — the destination is a consumer of E, so super applies. Bloch's rule of thumb: "For maximum flexibility, use wildcard types on input parameters that represent producers or consumers. Do not use wildcard types as return types" — doing so forces clients to use wildcards in their own code. Collections.sort(List<T>, Comparator<? super T>) is the canonical example from the JDK: a Comparator<Object> can sort a List<String> because Object is a supertype of String.
Code example
// Generic stack with type safety
class Stack<T> {
private final Deque<T> storage = new ArrayDeque<>();
void push(T item) { storage.push(item); }
T pop() { return storage.pop(); }
T peek() { return storage.peek(); }
}
// Generic method: works for any Comparable
<T extends Comparable<T>> T max(List<T> list) {
return list.stream().max(Comparator.naturalOrder()).orElseThrow();
}
// PECS: copy from producer to consumer
<T> void copy(List<? extends T> source, List<? super T> destination) {
for (T item : source) destination.add(item);
}
// copy(List<Integer>, List<Number>) — Number is super of Integer