Generics

Learning goals

Write type-safe collections and APIs with generics, understand type erasure and bounds, use wildcards (? extends, ? super) for flexible method signatures, and avoid raw types.


Why generics?

Without generics, collections hold Object—casts and ClassCastException at runtime:

List list = new ArrayList();
list.add("hi");
Integer n = (Integer) list.get(0);  // blows up at runtime

With generics, errors move to compile time:

List<String> names = new ArrayList<>();
names.add("Ada");
// names.add(42);     // compile error
String s = names.get(0);

Generic classes and methods

public class Box<T> {
    private T value;

    public Box(T value) {
        this.value = value;
    }

    public T get() {
        return value;
    }
}

public static <T> T first(List<T> list) {
    return list.isEmpty() ? null : list.get(0);
}

Type parameter T is placeholder replaced by concrete type at use site.


Diamond operator

Map<String, List<Integer>> map = new HashMap<>();

Compiler infers type args on right side (JDK 7+).


Bounded type parameters

public static <T extends Comparable<T>> T max(T a, T b) {
    return a.compareTo(b) >= 0 ? a : b;
}

public class NumberBox<T extends Number> {
    private T value;
    public double doubleValue() {
        return value.doubleValue();
    }
}

extends upper bound; super lower bound on wildcards (below).

Multiple bounds: <T extends Number & Comparable<T>>.


Wildcards

Producer extends, consumer super (PECS):

public static double sum(List<? extends Number> numbers) {
    double total = 0;
    for (Number n : numbers) {
        total += n.doubleValue();
    }
    return total;
}

public static void addIntegers(List<? super Integer> dest) {
    dest.add(42);
}
Wildcard Reads (producer) Writes (consumer)
? extends T Yes (as T) No (except null)
? super T As Object Yes (add T)
? As Object No

Use List<T> when both read and write same type; wildcards for API flexibility.


Type erasure

Generics exist at compile time; bytecode uses raw types and casts. Implications:

  • No new T(), T[], or instanceof T.
  • Overloads differing only by type parameter erasure collide.
  • Class<String> vs Class<Integer> — same runtime class Class.

Bridge methods preserve polymorphism for generics overrides.


Raw types (avoid)

List raw = new ArrayList();  // legacy, unchecked warnings

Fix with proper type argument or List<?> if truly unknown.


Generic records and sealed types

record Pair<A, B>(A first, B second) {}

public sealed interface Result<T> permits Success, Failure { }
public record Success<T>(T value) implements Result<T> { }
public record Failure<T>(String error) implements Result<T> { }

Modern Java uses generics throughout standard library (Optional<T>, Stream<T>, Map<K,V>).


Common mistakes

  1. Using raw List — loses safety; enable -Xlint:unchecked.
  2. List<Object> vs List<?> — former accepts any add; latter read-focused unknown type.
  3. Arrays and genericsnew List<String>[10] illegal; use List[] only with care or ArrayList.
  4. Over-generic APIspublic <T> T process(T input) without need adds noise.
  5. Ignoring PECS — inflexible methods or unsafe casts.

Practice checkpoint

  1. Declare Map<String, Integer> word counts variable.

    • Answer: Map<String, Integer> counts = new HashMap<>();
  2. Why List<Object> not accept List<String> assignment for adding?

    • Answer: Would allow inserting non-String into String list via Object view; generics invariant.
  3. Write method copying numbers from List<? extends Number> to List<? super Number>.

    • Answer: Loop read from source, dest.add(num) if dest typed super Number.
  4. Can you do if (obj instanceof List<String>)?

    • Answer: No—erasure; use instanceof List<?> then cast with unchecked warning or pattern.

Interview angles

  • “Erasure—why?” — Backward compatibility with pre-generics bytecode; trade-off vs reified generics (C#).
  • “PECS rule?” — Producer extends, consumer super.
  • “Generic array creation?” — Illegal due to erasure and heap pollution risk.

Next: /java/learn/core/io-and-files/