Command Palette

Search for a command to run...

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.

Key Takeaways

  • Generics provide compile-time type safety — prefer them over Object + cast.
  • Type erasure: List<String> and List<Integer> are the same class at runtime — cannot use instanceof with generics.
  • PECS: extends for reading (producer), super for writing (consumer).

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