Skip to content
Generics In Depth
step 1/7

Reading — step 1 of 7

Learn

~3 min readGenerics, Streams, and Functional Java

Effective Java spends Items 26–33 on generics. The basics (List<String>) are easy. The hard part is bounded types and wildcards — and they're inescapable once you start writing reusable APIs.

Type erasure

Java generics are erased at runtime. List<String> and List<Integer> are the same List class — the type parameter exists only at compile time. Implications:

List<String> strings = new ArrayList<>();
List<Integer> ints = new ArrayList<>();
strings.getClass() == ints.getClass();   // true — both ArrayList.class
  • You can't write new T[10] — the type is erased
  • You can't instanceof List<String> — only instanceof List<?>
  • Generic exceptions can't be thrown (catch needs the actual type)

Different from C++ templates (per-instantiation code) and Rust generics (monomorphized). Java's approach trades runtime info for backward compatibility.

Bounded type parameters

Constrain a type parameter to a hierarchy:

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;
}

<T extends Comparable<T>> says "T must implement Comparable<T>." Now we can call compareTo on it.

Multiple bounds:

<T extends Number & Comparable<T>>

Class first if there's one (only one class allowed), interfaces after.

Wildcards: ?, extends, super

The trickiest part of Java generics. Three forms:

List<?>             // unknown — read Object only, can't add (except null)
List<? extends T>   // some subtype of T — read T's, can't add
List<? super T>     // some supertype of T — add T's, read Object

PECS — Producer Extends, Consumer Super (Effective Java Item 31):

  • If a parameter PRODUCES values for you to read, use ? extends T
  • If it CONSUMES values you provide, use ? super T
// Producer: reads from src, writes into dest
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
    for (T t : src) dest.add(t);
}

Now copy(numberList, integerList) works — Number is a super of Integer.

Without wildcards, you'd be stuck with exact-type matches. Wildcards are how Collections.copy works for any compatible pair.

Generic methods

Methods can have their own type parameters, separate from the class:

public class Util {
    public static <T> List<T> singleton(T t) {
        List<T> list = new ArrayList<>();
        list.add(t);
        return list;
    }
}

List<String> s = Util.singleton("hi");    // T inferred as String
List<Integer> i = Util.<Integer>singleton(42);  // explicit

The <T> between modifiers and return type declares a method-level type parameter.

Common mistakes

  • Raw types (List instead of List<String>) — defeats the type system. Compiler warns but allows. Never write raw types in new code.
  • Misusing super/extends — leads to either won't-compile or surprising restrictions. Apply PECS.
  • Trying to create T[] directly — erased; can't do it. Use Object[] cast or (T[]) Array.newInstance(cls, size).
  • List<Object> vs List<?> — they're DIFFERENT. List<Object> accepts Objects; List<?> accepts any list (read-only essentially).
  • Inheritance variance confusionList<Integer> is NOT a List<Number>. Generics are invariant. Wildcards introduce variance.

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…