Skip to content
Java core 4 min read

Generics

Generics let you parameterize types over the types they operate on, moving an entire class of ClassCastExceptions from runtime to compile time. List<String> is not just documentation — the compiler enforces that only Strings go in and guarantees Strings come out, with no casting at the call site. Generics power every collection in the framework and most well-designed APIs.

Generic Classes

A type parameter is declared in angle brackets after the class name and used like any other type inside the class.

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

Box<String> box = new Box<>();
box.set("hello");
String s = box.get();          // no cast needed

By convention, parameters are single uppercase letters: T (type), E (element), K/V (key/value), R (result), N (number).

Generic Methods

A method can declare its own type parameters, independent of the class, by placing them before the return type.

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

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

The compiler infers T from the arguments, so you rarely specify it explicitly.

Bounded Type Parameters

A bound restricts the type argument to a subtype, unlocking the methods of that supertype inside the generic code. T extends Comparable<T> is the classic bound for “anything orderable.”

static <T extends Comparable<T>> T max(List<T> items) {
    T best = items.get(0);
    for (T item : items) {
        if (item.compareTo(best) > 0) best = item;   // compareTo available via bound
    }
    return best;
}

System.out.println(max(List.of(3, 9, 1)));   // 9

Output:

9

Multiple bounds are joined with &: <T extends Number & Comparable<T>>.

Wildcards and PECS

Generics are invariant: List<Integer> is not a List<Number>. Wildcards reintroduce flexibility for method parameters.

  • ? extends T — an upper-bounded wildcard: you can read Ts out, but cannot add (a producer).
  • ? super T — a lower-bounded wildcard: you can add Ts in, but reads come back as Object (a consumer).

The mnemonic is PECS — Producer Extends, Consumer Super.

// Producer: source only yields Numbers -> use extends
static double sum(List<? extends Number> source) {
    double total = 0;
    for (Number n : source) total += n.doubleValue();
    return total;
}

// Consumer: sink only receives Integers -> use super
static void fill(List<? super Integer> sink, int count) {
    for (int i = 0; i < count; i++) sink.add(i);
}

System.out.println(sum(List.of(1, 2.5, 3L)));   // 6.5
List<Number> nums = new ArrayList<>();
fill(nums, 3);                                   // [0, 1, 2]
System.out.println(nums);

Output:

6.5
[0, 1, 2]

PECS in one line: if a parameter produces values you read, use ? extends; if it consumes values you write, use ? super. The unbounded ? is for code that neither reads typed values nor writes.

Type Erasure

Generics are a compile-time construct. The compiler verifies type usage, inserts casts, then erases type parameters — List<String> and List<Integer> are both just List at runtime. Consequences:

List<String> a = new ArrayList<>();
List<Integer> b = new ArrayList<>();
System.out.println(a.getClass() == b.getClass());   // true — same raw class

Output:

true

Erasure means you cannot: use new T[] or new T(), check obj instanceof List<String>, or overload methods that differ only by generic type. Capture a Class<T> token or a factory if you need the runtime type.

Benefits

  • Compile-time safety — type errors surface at build time, not as production ClassCastExceptions.
  • No manual casts — callers get the right type back directly.
  • Self-documenting APIsMap<UserId, Session> states intent that Map alone cannot.
  • Reusable algorithms — one generic method works across every element type.

Best Practices

  • Prefer generic types and methods over raw types (List without <>) and Object casting.
  • Apply PECS to every parameter: extends for producers, super for consumers.
  • Bound type parameters (<T extends Comparable<T>>) instead of casting inside the method.
  • Use bounded wildcards on parameters, but plain type variables on return types for usability.
  • Never suppress unchecked warnings without isolating and commenting the one line that needs it.

Interview Questions

Q: What is type erasure and why does Java use it? A: Erasure removes generic type information after compilation, replacing type parameters with their bounds (or Object) and inserting casts. Java chose it for migration compatibility — generic and pre-generics code interoperate, and no new bytecode/JVM changes were required.

Q: Explain PECS. A: Producer Extends, Consumer Super. Use ? extends T when a structure only produces (you read Ts from it); use ? super T when it only consumes (you write Ts into it). It maximizes the set of types a method accepts while staying type-safe.

Q: Why can’t you create an array of a generic type, e.g. new T[10]? A: Arrays are reified (they know their element type at runtime) but generics are erased. The runtime couldn’t perform the array’s store-check against T, so the language forbids new T[]. The workaround is an Object[] with an unchecked cast, or Array.newInstance with a Class<T> token.

Q: What is the difference between List<Object> and List<?>? A: List<Object> is a concrete list you can add any Object to. List<?> is an unknown but specific type — you can read elements as Object but cannot add anything except null, because the compiler can’t prove your element matches the (unknown) actual type.

Last updated June 1, 2026
Was this helpful?