Notifications

No notifications

/Phase 3

Generics — Type-safe Collections & Methods

Generics let a class or method work with *any* type while keeping compile-time type safety. List means "a list of strings" — the compiler refuses anything else, and you don't need to cast on the way out. Underneath, Java implements generics with type erasure — generic info is stripped at runtime — which leads to a few quirks worth knowing.

On this page

Detailed Theory

# Generics

Why generics?

Without them you'd write:
List names = new ArrayList();
names.add("alice");
names.add(42);                       // oops — no error
String s = (String) names.get(1);    // ClassCastException at runtime
With them the compiler catches it:
List<String> names = new ArrayList<>();
names.add("alice");
names.add(42);                       // compile error
String s = names.get(0);             // no cast needed

Generic class

public class Box<T> {
    private T value;
    public void set(T value) { this.value = value; }
    public T get()           { return value; }
}

Box<Integer> b = new Box<>(); b.set(42); int x = b.get(); // auto-unboxed

Generic method

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

String s = firstOrNull(List.of("a", "b")); // T inferred as String

The *before* the return type declares the type parameter.

Bounded type parameters

> — T must implement Comparable.
public static <T extends Comparable<T>> T max(List<T> list) {
    T best = list.get(0);
    for (T x : list) if (x.compareTo(best) > 0) best = x;
    return best;
}
Note: extends here means *extends or implements* — the same keyword for classes and interfaces.

Wildcards: ? extends vs ? super (PECS)

  • List — producer of Number. You can READ Numbers from it. You CANNOT add (except null), because the actual type might be List or List.
  • List — consumer of Integer. You can WRITE Integers into it. Reading gives back Object.
PECS = *Producer Extends, Consumer Super*.

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

public static void addInts(List<? super Integer> dst) { // consumer dst.add(1); dst.add(2); }

Type erasure (the gotcha)

At runtime, List and List are both just List. Consequences:

  • You cannot do new T[10] or new T().
  • You cannot if (x instanceof List) — only instanceof List.
  • You cannot overload methods that differ only in their generic parameter — they erase to the same signature.
  • Generic types cannot be primitives. Use the wrappers: List, not List.