Generics & Type System

Before generics (Java 5), every collection was raw—(String) list.get(0) casts failed at runtime. Generics push type errors to compile time, but erasure means the JVM still sees raw types at runtime. This chapter connects syntax, wildcards, PECS, and the patterns you see in Spring, JPA, and API design.

mid senior

Why generics exist

Generics let you parameterize types: a List<String> is a list that only holds strings at compile time. The compiler inserts checks; you avoid repetitive casts and ClassCastException surprises in distant code.

Problems generics solve

Without genericsWith generics
Everything stored as ObjectElement type known to compiler
Explicit cast on every readCast inserted by compiler (or eliminated)
Runtime ClassCastExceptionMany errors at compile time
Documentation only in commentsTypes are self-documenting API
Java
// Pre-generics style (still compiles with warnings today)
List raw = new ArrayList();
raw.add(42);
String s = (String) raw.get(0);  // ClassCastException at runtime

// Generic style
List<String> names = new ArrayList<>();
names.add("Ada");
// names.add(42);        // compile error
String name = names.get(0);  // no cast

Generics are a compile-time feature. They improve type safety and enable richer APIs; they do not create new runtime types for List<String> vs List<Integer> (see erasure).

📦 Real World

Spring JdbcTemplate.query(..., RowMapper<T>), JPA Repository<User, Long>, and HTTP clients returning Optional<Response> all rely on generics so callers stay type-safe without casting JSON payloads by hand.

Generic classes, methods & interfaces

Type parameters (commonly T, E, K, V) are placeholders bound when you declare or invoke the generic.

Generic class

Java
public 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("payload");
String v = box.get();

Generic interface

Java
public interface Converter<F, T> {
    T convert(F from);
}

Converter<String, Integer> len = String::length;

Generic method

Type parameters on the method are independent of the class (see dedicated section). Syntax: modifiers, then <T>, then return type.

Java
public static <T> List<T> listOf(T... elements) {
    return List.of(elements);
}

Naming conventions

LetterTypical meaning
TType
EElement (collections)
K, VKey, Value (maps)
NNumber
RReturn type (e.g. Function<T,R>)

Bounded type parameters

Unbounded <T> means “any reference type.” <T extends SomeType> restricts T so you can call methods on the bound.

Single upper bound

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

Recursive type bound — <T extends Comparable<T>>

The type parameter must be comparable to itself—the pattern used by Collections.max and sorting APIs.

Java
public static <T extends Comparable<T>> T max(Collection<T> coll) {
    T best = null;
    for (T item : coll) {
        if (best == null || item.compareTo(best) > 0) {
            best = item;
        }
    }
    return best;
}

// String implements Comparable<String> — recursive bound satisfied
max(List.of("b", "a", "c"));  // "c"

Multiple bounds

Syntax: <T extends A & B & C> — first bound may be a class; rest must be interfaces. T is treated as intersection type at compile time.

Java
interface Named { String name(); }
interface Dated { Instant createdAt(); }

public static <T extends Named & Dated> void audit(T entity) {
    log.info("{} at {}", entity.name(), entity.createdAt());
}
⚠️ Pitfall

You cannot write <T extends String & Runnable> with two classes—only one class in the extends clause. Order matters: class first, then interfaces.

Wildcards & PECS

Wildcards (?) represent unknown types in APIs—especially method parameters— when you want flexibility without fixing a single type parameter for the whole class.

WildcardMeaningCan read asCan write
?Unbounded unknownObjectnull only (except special cases)
? extends TUpper bounded (subtype of T)TGenerally no (except null)
? super TLower bounded (supertype of T)ObjectT and subtypes of T

PECS — Producer Extends, Consumer Super

Java
// Producer — copy FROM src (read elements)
public static void copy(
        List<? extends Number> src,
        List<? super Number> dest) {
    for (Number n : src) {
        dest.add(n);
    }
}

List<Integer> ints = List.of(1, 2, 3);
List<Number> nums = new ArrayList<>();
copy(ints, nums);  // OK — ints is producer, nums is consumer

// Collections.addAll is declared:
// <T> boolean addAll(Collection<? super T> c, T... elements)

When not to use wildcards

If the same type parameter appears in multiple parameter or return positions, use a named type parameter—not wildcards—so relationships are preserved:

Java
// Good — T links argument and return
public static <T> T identity(T value) { return value; }

// Broken with wildcards — cannot express "return same type as arg"
// ? identity(?)  // not valid
🎯 Interview Tip

Explain why List<Object> is not a supertype of List<String> (invariance)—would allow adding Object to a String list. Wildcards restore flexibility at API boundaries only.

Generic methods vs class-level generics

A generic class fixes type parameters for all instance methods (unless overridden by method-level params). A generic method declares its own type parameters—often static utilities where the class is not generic.

Class-level <T>Method-level <T>
One T for whole instanceFresh T per method invocation
new Box<String>() binds TCompiler infers T from arguments
Instance methods use class TStatic methods often need own <T>
Java
public class Utils {
    // Method generic — T independent of Utils
    public static <T> List<T> nCopies(T value, int n) {
        return Collections.nCopies(n, value);
    }

    // Explicit type argument when inference fails
    List<String> list = Utils.<String>nCopies("x", 3);
}

public class Pair<A, B> {
    private final A first;
    private final B second;

    // Method introduces third type parameter
    public static <X, Y> Pair<X, Y> of(X first, Y second) {
        return new Pair<>(first, second);
    }
}

Type inference: Compiler infers type arguments from assignment target, method arguments, and chained return types (Java 7+ diamond, Java 8+ improved inference for lambdas).

Type erasure

For compatibility with pre-generics bytecode, the compiler erases type parameters to their bounds (or Object if unbounded). Generic type information is largely gone at runtime.

What gets erased

  • List<String> becomes List at runtime—elements are still Object references in bytecode
  • <T extends Number> → bound Number where needed for casts
  • Generic methods get bridge methods and synthetic casts inserted by compiler
Java
List<String> a = new ArrayList<>();
List<Integer> b = new ArrayList<>();

System.out.println(a.getClass() == b.getClass());  // true — both ArrayList

// Cannot do: if (list instanceof List<String>) { }
// Can do:     if (list instanceof List<?>) { }

Heap pollution

Mixing raw types with generics can insert non-generic elements into a generic collection—compiler warns with @SafeVarargs / unchecked on varargs:

Java
List<String> strings = new ArrayList<>();
List raw = strings;       // unchecked warning
raw.add(42);                // heap pollution — Integer in List<String>
String s = strings.get(0);  // ClassCastException at runtime

Unchecked cast warnings

Casting to parameterized types cannot be fully verified at runtime—the compiler emits unchecked warnings; you may suppress with @SuppressWarnings("unchecked") only when logically safe (e.g. after a type token check).

Java
Object obj = List.of("a");
@SuppressWarnings("unchecked")
List<String> list = (List<String>) obj;  // warning — verify with TypeReference in JSON libs
🔬 Under the Hood

Bridge methods preserve polymorphism after erasure—e.g. Comparable<String>.compareTo(String) bridges to compareTo(Object). Reflection can read generic signatures via Type / ParameterizedType on fields and methods—not on bare class literals.

Reifiable vs non-reifiable types

A type is reifiable if type information is fully available at runtime—so you can use instanceof, create arrays, and assign to Object without hidden casts.

ReifiableNon-reifiable (erased)
PrimitivesList<String>
Non-generic classesT type parameters
Raw typesParameterized types with type args
Arrays of reifiable componentList<?> (wildcard parameterized)
Unbounded wildcard ?[] rareMap<K,V>

You cannot create new T[] or new List<String>[10] (illegal generic array creation). Workarounds: (T[]) new Object[n] with warning, or ArrayList instead.

Java
String[] ok = new String[3];
// List<String>[] bad = new List<String>[3];  // compile error

List<?> wildcardList = List.of(1, "two");
wildcardList instanceof List<?>;  // OK
// wildcardList instanceof List<String>;  // compile error

Capture: A wildcard type like ? extends Number in a local variable is not a valid type argument when creating nested generics—the compiler uses a capture helper type internally.

Real-world generic patterns

Production code repeats a few shapes: fluent builders tied to subclass type, persistence repositories keyed by ID type, and result types that carry success or error without throwing for control flow.

Builder<T> — recursive generic (F-bounded)

Subclass returns itself from fluent methods—classic pattern uses recursive type bound:

Java
public abstract class Builder<T extends Builder<T>> {
    protected String host = "localhost";

    @SuppressWarnings("unchecked")
    protected T self() { return (T) this; }

    public T host(String host) {
        this.host = host;
        return self();
    }
}

public class HttpClientBuilder extends Builder<HttpClientBuilder> {
    public HttpClient build() {
        return new HttpClient(host);
    }
}

new HttpClientBuilder().host("api.example.com").build();

Repository<T, ID>

Java
public interface Repository<T, ID> {
    Optional<T> findById(ID id);
    T save(T entity);
    void deleteById(ID id);
    List<T> findAll();
}

public interface UserRepository extends Repository<User, UUID> {
    Optional<User> findByEmail(String email);
}

// Spring Data: interface UserRepo extends JpaRepository<User, Long>

Result<T, E> — typed success or failure

Java
public sealed interface Result<T, E> permits Ok, Err {
    static <T, E> Result<T, E> ok(T value) { return new Ok<>(value); }
    static <T, E> Result<T, E> err(E error) { return new Err<>(error); }

    <R> R fold(Function<? super T, ? extends R> onOk,
                 Function<? super E, ? extends R> onErr);
}

record Ok<T, E>(T value) implements Result<T, E> {
    public <R> R fold(Function<? super T, ? extends R> onOk,
                      Function<? super E, ? extends R> onErr) {
        return onOk.apply(value);
    }
}

Similar ideas: Optional<T> for absence, Either in libraries, Vavr Try, Rust-inspired Result in modern Java services for explicit error channels.

💡 Pro Tip

API design: use bounded type parameters inside a class; use wildcards on public method parameters (PECS). Avoid exposing generic arrays; prefer collections. For JSON generic types, use TypeReference<T> (Jackson) to preserve parameterized types at runtime via reflection.

📦 Real World

ResponseEntity<T> in Spring MVC, CompletableFuture<T>, Reactor Mono<T> / Flux<T>, and AWS SDK paginated Page<T> are all parameterized types—getting wildcards right at library boundaries prevents subtle compile failures in application code.