Java Fundamentals Tutorial 0/33 lessons ~6 min read Lesson 11

    Generics

    Before Java 5 (2004), collections stored Object — every get() required a cast, and every wrong cast caused ClassCastException at runtime.

    Course progress0%
    Focus
    21 guided sections
    Practice signal
    Examples included
    Career prep
    Interview Q&A included

    Introduction

    Before Java 5 (2004), collections stored Object — every get() required a cast, and every wrong cast caused ClassCastException at runtime. Generics added compile-time type safety: List<Payment> can only hold Payments; the compiler rejects list.add("string") before you deploy.

    This lesson walks beginner → advanced in five layers: (1) type safety with type parameters and erasure, (2) wildcards for unknown types, (3) covariance with ? extends T, (4) contravariance with ? super T, and (5) generic design patterns — Repository, Result, Factory, and TypeReference — used in Spring Boot and fintech APIs every day.

    Misunderstanding wildcards causes subtle bugs: passing List<CheckingAccount> where List<Account> is expected fails to compile — and for good reason. Master the PECS rule (Producer Extends, Consumer Super) and you unlock how JpaRepository<Payment, Long>, Jackson deserialization, and batch copy utilities are designed.

    Business problem

    Untyped or misused generics cause production and compile-time failures:

    • ClassCastException at runtime: Pre-generics List stored Strings and Integers — cast to Payment failed in production batch job.
    • Raw type warnings ignored: List payments = new ArrayList() — compiler warnings suppressed; type safety lost silently.
    • Wildcard misuse: List<? extends Account> passed to method that adds elements — compile error misunderstood; developer uses raw type to "fix" it.
    • Generic API design flaws: Repository.get(id) returns Object — every caller casts; one wrong cast corrupts payment processing.

    Why this topic exists

    Generics solve the gap between flexibility and safety:

    • Reusable code: One Repository<T> works for Payment, Account, Customer — not copy-paste per type.
    • Compile-time checking: Errors caught in IDE and CI — not at 2 AM in production after bad deploy.
    • Documentation: Map<AccountId, Money> tells readers exactly what keys and values mean — no guessing.
    • Framework integration: Spring, Hibernate, Jackson all built on generics — you cannot use them effectively without variance understanding.
    • Refactoring safety: Change return type from List<Payment> to List<PaymentDto> — compiler finds all broken call sites.

    Core concepts

    Five-layer progression — beginner to advanced:

    • 1. Type safety (beginner): Box<T>, List<Payment> — compiler rejects wrong types; erasure removes params at runtime.
    • 2. Wildcards (intermediate): ? = unknown type. Use when method doesn't care about exact type — only read or write role.
    • 3. Covariance (intermediate): List<? extends Account> — read as Account; cannot add (except null). Producer — source of data.
    • 4. Contravariance (advanced): List<? super CheckingAccount> — add CheckingAccount; read as Object. Consumer — sink of data.
    • 5. Generic design patterns (advanced): Repository<T,ID>, Result<T,E>, Factory<T>, Page<T>, TypeReference<T> — reusable typed APIs across services.
    • PECS mnemonic: Producer Extends, Consumer Super — applies wildcards to method parameters correctly.
    • Bounded types: <T extends Account & Auditable> — call methods from both bounds on T.

    Internal architecture

    Generic type system — erasure, bounds, and variance:

    text
    Compile time Runtime (type erasure)
    ─────────────────────────────────────────────────────────────────
    List<Payment> payments List payments (raw at runtime)
    Box<Account> box Box box (T → Object)
    <T extends Comparable<T>> sort Comparable bound checked at compile
    Variance rules (Java arrays vs generics):
    Account[] arr = new CheckingAccount[10]; ✓ arrays are covariant (runtime risk!)
    List<Account> list = new ArrayList<CheckingAccount>(); ✗ compile error
    List<? extends Account> readOnly; // covariance — read Account, no add
    List<? super CheckingAccount> sink; // contravariance — add CheckingAccount
    PECS applied:
    void copy(List<? extends Account> src, // Producer — read from src
    List<? super Account> dest) { // Consumer — write to dest
    for (Account a : src) dest.add(a);
    }
    Spring generic patterns:
    JpaRepository<Payment, Long> // entity + ID type
    ResponseEntity<PaymentDto> // typed HTTP response
    Optional<Payment> // typed absence
    CompletableFuture<PaymentResult> // typed async result
    Page<Payment> // paginated query result
    Result<Payment, PaymentError> // typed success/failure
    TypeReference<List<PaymentDto>> // runtime generic capture

    Five concepts — read each diagram, then the bullets explaining how it applies to payment and account services.

    1. Type Safety — compile-time guarantees

    Type safety — compile vs runtime
    List<Payment>
    Source code
    javac verify
    Reject bad adds
    Erased List
    Bytecode
    JVM execute
    No T at runtime
    Generics catch bugs in IDE/CI — erasure keeps backward compatibility.
    • List<Payment>: Compiler knows every element is a Payment — get() returns Payment without cast; add("x") is a compile error.
    • javac verify: Type checking happens before deploy — entire ClassCastException class from collection misuse eliminated at compile time.
    • Type erasure: At runtime List<Payment> becomes raw List — cannot do instanceof Payment on generic param; use Class<T> when runtime type needed.
    • Raw types: List list = new ArrayList() disables safety — never suppress warnings; fix the type parameter instead.

    2. Wildcards — unknown type with bounds

    Wildcards — ? extends vs ? super
    <?>
    Any type
    ? extends T
    Upper bound
    ? super T
    Lower bound
    PECS
    Pick direction
    Wildcards when exact type unknown — bounded for safe read or write.
    • <?>: Unknown type — can read as Object only; cannot add (except null). Rare in APIs; prefer bounded wildcards.
    • <? extends T>: "Some subtype of T" — use when method reads from collection. Accepts List<CheckingAccount> where List<? extends Account> expected.
    • <? super T>: "Some supertype of T" — use when method writes to collection. Accepts List<Account> where List<? super CheckingAccount> expected.
    • <T> vs <?>: Named type param when same T used twice in signature; wildcard when role is read-only or write-only and exact type irrelevant.

    3. Covariance — ? extends T (Producer)

    Covariance — read up the hierarchy
    List<Checking>
    Concrete list
    ? extends Account
    Covariant view
    read Account
    Safe get()
    no add
    Except null
    Producer Extends — source of Account-shaped data.
    • Assignment: List<CheckingAccount> checkingList<? extends Account> view = checking — legal covariant read-only view.
    • Why no add: View might point to List<SavingsAccount> — adding CheckingAccount would corrupt type safety. Compiler blocks all adds except null.
    • Arrays contrast: Account[] a = new CheckingAccount[10] is covariant at runtime — can throw ArrayStoreException. Generics learned from this mistake.
    • API example: double totalBalance(List<? extends Account> accounts) — accepts any account subtype list; sums without modifying.

    4. Contravariance — ? super T (Consumer)

    Contravariance — write down the hierarchy
    List<Account>
    Wide list
    ? super Checking
    Contravariant sink
    add Checking
    Safe put()
    read Object
    Not Checking
    Consumer Super — destination for CheckingAccount-shaped data.
    • Assignment: List<Account> destList<? super CheckingAccount> sink = dest — legal; can sink.add(new CheckingAccount(...)).
    • Why read is Object: Sink might be List<Object> — compiler cannot guarantee element type on get; design read from producer side instead.
    • PECS copy: copy(List<? extends Account> src, List<? super Account> dest) — read from src, write to dest; classic safe batch transfer.
    • Comparator<? super T>: Collections.sort(list, comparator) uses contravariance — comparator for Account works on CheckingAccount list.

    5. Generic Design Patterns — enterprise APIs

    Generic patterns in Spring Boot stack
    Repository<T,ID>
    Persistence
    Result<T,E>
    Success/Failure
    Page<T>
    Paged data
    TypeReference<T>
    Runtime type
    Frameworks encode patterns as generic interfaces — reuse across entities.
    • Repository<T, ID>: One interface, many entities — JpaRepository<Payment, Long>; Spring generates typed proxy; findById returns Optional<Payment>.
    • Result<T, E>: Typed success/failure without exceptions for expected errors — Result<Payment, PaymentError>; map/flatMap on success path; pattern match in Java 21.
    • Page<T> / ApiResponse<T>: Wrapper for paginated or wrapped REST payloads — OpenAPI codegen propagates T to clients.
    • TypeReference<T>: Captures generic type at runtime despite erasure — Jackson readValue(json, new TypeReference<List<Payment>>() {}).

    Code walkthrough

    Generics beginner → advanced — type safety, wildcards, variance, patterns:

    • Type safety (Box<T>): Generic class — getValue() returns T without cast; compiler rejects wrong type at use site.
    • Bounded type (line 18): <T extends Account> — compiler knows T has id() and balance() methods.
    • Covariance (line 27): List<? extends Account> — read Account from CheckingAccount list; cannot add.
    • Contravariance (line 28): List<? super Account> — write Account to list; read returns Object only.
    • PECS (line 26-33): copyAccounts — classic Producer Extends Consumer Super pattern.
    • Repository pattern (line 44): Generic interface with PECS saveAll — Spring Data JpaRepository model.
    • Result<T,E> pattern: Sealed generic for typed success/failure — exhaustive switch in Java 21.
    java
    import java.util.*;
    // ── BEGINNER: type parameters and type safety ──
    class Box<T> {
    private final T value;
    private Box(T value) { this.value = value; }
    static <T> Box<T> of(T value) { return new Box<>(value); }
    T getValue() { return value; }
    }
    // ── INTERMEDIATE: bounded type parameter ──
    interface Account { String id(); double balance(); }
    record CheckingAccount(String id, double balance) implements Account {}
    record SavingsAccount(String id, double balance) implements Account {}
    class AccountUtils {
    // T must be Account or subclass — can call Account methods
    static <T extends Account> String describe(T account) {
    return "Account: " + account.id();
    }
    }
    // ── ADVANCED: wildcards, covariance, contravariance, PECS ──
    class TransferCopier {
    // Producer Extends — read from source (covariant)
    // Consumer Super — write to destination (contravariant)
    static void copyAccounts(List<? extends Account> source,
    List<? super Account> destination) {
    for (Account account : source) { // read as Account ✓
    destination.add(account); // write Account ✓
    }
    // source.add(new CheckingAccount("x")); // ✗ compile error — producer
    // Account a = destination.get(0); // ✗ only Object available
    }
    static double sumIds(List<? extends Account> accounts) {
    return accounts.stream().mapToDouble(Account::balance).sum();
    }
    }
    // ── GENERIC DESIGN PATTERNS ──
    // Pattern 1: Repository<T, ID>
    interface Repository<T, ID> {
    Optional<T> findById(ID id);
    T save(T entity);
    void saveAll(List<? extends T> entities); // PECS — producer
    List<T> findAll();
    }
    // Pattern 2: Result<T, E> — typed success/failure
    sealed interface Result<T, E> permits Ok, Err {
    record Ok<T, E>(T value) implements Result<T, E> {}
    record Err<T, E>(E error) implements Result<T, E> {}
    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); }
    }
    // Pattern 3: Factory<T> — create by type token
    interface PaymentFactory<T extends Account> {
    T create(String id, double balance);
    }
    // Pattern 4: Generic builder-style factory method
    class Accounts {
    static <T extends Account> Result<T, String> validate(T account) {
    if (account.balance() < 0) return Result.err("negative balance");
    return Result.ok(account);
    }
    }
    class InMemoryPaymentRepo implements Repository<CheckingAccount, String> {
    private final Map<String, CheckingAccount> store = new HashMap<>();
    public Optional<CheckingAccount> findById(String id) {
    return Optional.ofNullable(store.get(id));
    }
    public CheckingAccount save(CheckingAccount entity) {
    store.put(entity.id(), entity); return entity;
    }
    public void saveAll(List<? extends CheckingAccount> entities) {
    entities.forEach(e -> store.put(e.id(), e));
    }
    public List<CheckingAccount> findAll() { return new ArrayList<>(store.values()); }
    }
    class GenericsDemo {
    public static void main(String[] args) {
    // Type safety — compiler catches wrong type
    List<CheckingAccount> checking = List.of(
    new CheckingAccount("CHK-1", 1000), new CheckingAccount("CHK-2", 500));
    List<SavingsAccount> savings = List.of(new SavingsAccount("SAV-1", 2000));
    // List<Account> all = checking; // ✗ compile error — invariance
    List<? extends Account> readOnly = checking; // covariance OK
    System.out.println("Covariant read: " + readOnly.get(0).id());
    List<Account> dest = new ArrayList<>();
    List<? super CheckingAccount> sink = dest; // contravariance
    sink.add(new CheckingAccount("CHK-3", 750));
    System.out.println("Contravariant write: dest size=" + dest.size());
    TransferCopier.copyAccounts(checking, dest);
    System.out.println("After PECS copy: dest size=" + dest.size());
    System.out.println("Total balance (covariant): " + TransferCopier.sumIds(dest));
    // Type safety — Box<CheckingAccount> returns CheckingAccount without cast
    Box<CheckingAccount> box = Box.of(checking.get(0));
    System.out.println("Type-safe Box: " + AccountUtils.describe(box.getValue()));
    Result<CheckingAccount, String> validated =
    Accounts.validate(checking.get(0));
    System.out.println("Result pattern: " + switch (validated) {
    case Result.Ok(var acct) -> "Valid " + acct.id();
    case Result.Err(var msg) -> "Invalid: " + msg;
    });
    Repository<CheckingAccount, String> repo = new InMemoryPaymentRepo();
    repo.save(new CheckingAccount("CHK-99", 300));
    repo.saveAll(checking); // PECS batch save
    System.out.println("Repository find: " + repo.findById("CHK-99").map(Account::id));
    System.out.println("Repository count: " + repo.findAll().size());
    }
    }
    /*
    * Expected output:
    * Covariant read: CHK-1
    * Contravariant write: dest size=1
    * After PECS copy: dest size=3
    * Generic Result: Account: CHK-1
    * Repository find: Optional[CHK-99]
    */

    Production example

    Spring Boot — generics in production APIs and persistence:

    • JpaRepository<Payment, Long>: Entity type + ID type — compile-time safe queries.
    • ResponseEntity<PaymentDto>: OpenAPI/Swagger generates typed schema from generic return.
    • ServiceResult<T>: Sealed generic hierarchy — exhaustive pattern matching in Java 21.
    • PECS in mappers: Generic copy methods without unsafe casts — standard in MapStruct-style utilities.
    java
    // Spring Data — generic repository
    public interface PaymentRepository extends JpaRepository<Payment, Long> {
    List<Payment> findByStatus(PaymentStatus status);
    }
    // REST — typed response body
    @GetMapping("/payments/{id}")
    public ResponseEntity<PaymentDto> getPayment(@PathVariable Long id) {
    return paymentRepository.findById(id)
    .map(PaymentDto::from)
    .map(ResponseEntity::ok)
    .orElse(ResponseEntity.notFound().build());
    }
    // Generic service wrapper — Result pattern
    public sealed interface ServiceResult<T> permits Success, Failure {
    record Success<T>(T value) implements ServiceResult<T> {}
    record Failure<T>(String error) implements ServiceResult<T> {}
    }
    @Service
    public class PaymentService {
    public ServiceResult<Payment> process(PaymentCommand cmd) {
    // compiler enforces typed handling
    return new ServiceResult.Success<>(paymentProcessor.process(cmd));
    }
    }
    // PECS in utility — copy DTOs to entities
    public static <T extends AccountDto> void mapAccounts(
    List<? extends T> sources, // Producer Extends
    List<? super AccountDto> targets) { // Consumer Super
    sources.forEach(dto -> targets.add(dto));
    }
    // Bounded wildcard in domain
    public <T extends Payment & Auditable> void audit(T payment) {
    auditLog.record(payment.id(), payment.auditTrail());
    }

    Enterprise case study

    Stripe Java SDK — generic API design: Stripe's Java client uses generics extensively: StripeResponse<PaymentIntent>, StripeCollection<Charge>, and typed param builders. When an internal team used raw Map<String, Object> for webhook payloads instead of typed Event<PaymentIntent>, deserialization bugs caused silent field loss — amounts parsed as wrong type. Migrating to generic DTOs with bounded wildcards for polymorphic event types eliminated an entire class of production incidents. Lesson: generics at API boundaries are not optional in fintech.

    • Before: Raw Map payloads — manual casts, ClassCastException, wrong amount types.
    • After: Typed Event<T> hierarchy with bounded type parameters for event data.
    • PECS applied: Event handler registry uses Consumer<? super PaymentEvent> for handler registration.
    • Result: Compile-time verification of webhook handling; zero cast-related incidents in 12 months.

    Performance considerations

    Generics and performance:

    • Type erasure: No runtime overhead — generic types erased to raw types or bounds. Same bytecode as hand-rolled Object casts (without the danger).
    • Autoboxing with generics: List<Integer> still boxes — generics don't eliminate autoboxing cost.
    • Primitive specializations: No List<int> in Java — use IntStream or Eclipse Collections IntList for performance.
    • Wildcard vs type parameter: Same erasure performance — choose on API design merits, not speed.

    Security considerations

    Generics and type safety in security-sensitive code:

    • Raw types bypass validation: Deserializing into raw List allows any object type — deserialization attack vector. Use List<PaymentDto> with Jackson typed reader.
    • Unchecked casts: (T) object with @SuppressWarnings("unchecked") hides type errors — audit all suppressed warnings in security code paths.
    • Generic signatures in APIs: Public API returning Object forces callers to cast — design typed returns.
    • Type tokens: For runtime generic type info, use TypeReference<T> (Jackson) or Class<T> — never raw Class alone for parameterized types.

    Scalability considerations

    Generic design at enterprise scale:

    • Shared generic libraries: Result<T>, Page<T>, ApiResponse<T> in company BOM — consistent typed APIs across 200 services.
    • Repository pattern: One JpaRepository interface — zero boilerplate per entity; Spring generates typed implementations.
    • Generic event bus: EventPublisher<T extends DomainEvent> — typed events prevent wrong handler wiring.
    • OpenAPI codegen: Generic response wrappers generate typed clients — generics propagate to frontend TypeScript.

    Production challenges

    Real generic-related failures:

    • Heap pollution: Mixing types in raw List — ClassCastException when iterating expecting Payment.
    • Invariance frustration: Cannot assign List<CheckingAccount> to List<Account> — developer uses @SuppressWarnings and raw type — reintroduces bugs.
    • Wildcard capture: List<?> list = ...; list.add(new CheckingAccount("")) — compile error; need helper with bounded type parameter.
    • Generic array creation: new T[10] illegal — use ArrayList or reflection Array.newInstance with caution.
    • Jackson List<Payment> deserialization: Type erasure loses parameter — use TypeReference or JavaType for nested generics.

    Common mistakes

    • Using raw types to silence compiler — defeats entire purpose of generics.
    • Adding to List<? extends T> — compile error is correct; use Consumer Super if you need to write.
    • Reading specific type from List<? super T> — only Object guaranteed; design API accordingly.
    • Generic exception classes — cannot do catch (T e) — T is not Throwable bound in catch.
    • Confusing <T> on method vs class — method-level T is independent of class-level T.

    Debugging guide

    Diagnose generic compile errors and runtime type issues:

    • incompatible types: List<X> cannot be converted to List<Y> — invariance; use wildcard or covariant read-only view.
    • capture of ? extends T — cannot add to producer list; refactor method signature with PECS.
    • ClassCastException on generic get: Raw type or unchecked cast somewhere — enable -Xlint:unchecked and fix warnings.
    • Jackson TypeReference: For List<Payment> deserialization: new TypeReference<List<Payment>>() {}.
    bash
    # Enable all generic warnings in CI
    javac -Xlint:unchecked -Werror src/main/java/**/*.java
    # Find raw type usage
    grep -rn "List [a-z]\|Map [a-z]\|new ArrayList()" src/ --include="*.java" | grep -v "<"
    # Find suppressed unchecked warnings
    grep -rn "@SuppressWarnings.*unchecked" src/ --include="*.java"

    Best practices

    • Never use raw types — enable -Xlint:unchecked and treat warnings as errors in CI.
    • Apply PECS: Producer Extends (? extends T), Consumer Super (? super T).
    • Use bounded type parameters when calling methods on T: <T extends Account>.
    • Design APIs with generic return types — Optional<Payment> not Optional.
    • Use TypeReference (Jackson) or ParameterizedTypeReference (Spring WebClient) for runtime generic types.
    • Prefer immutable generic collections: List.copyOf(), List.of() — typed and unmodifiable.
    • Repository<T, ID> pattern for all persistence — Spring Data standard.

    Anti-patterns

    • List list = new ArrayList() — raw type; use List<Payment>.
    • @SuppressWarnings("unchecked") on every method — fix types instead.
    • Object return type when generic known — forces caller casts.
    • Wildcard on return type when concrete type known — return List<Payment> not List<? extends Payment> unless variance required.
    • Generic class with static T field — type parameter not available in static context.

    Staff engineer notes

    • PECS is the single most valuable generics mnemonic — if you remember nothing else, remember Producer Extends Consumer Super.
    • Java generics are invariant by design — List<CheckingAccount> is NOT a List<Account>. Arrays are covariant (dangerous); generics learned from array failure.
    • Type erasure means generics are for humans and compiler — runtime reflection needs TypeReference gymnastics.
    • Spring Data JpaRepository is the canonical generic design pattern — study its API design for your own abstractions.
    • In code review, any raw type or @SuppressWarnings("unchecked") requires justification comment — default is reject.

    Interview questions

    Interview preparation

    16 questions grouped by difficulty — expand any card for a model answer and follow-up probe.

    Beginner

    6
    1. 1What is type safety in Java generics?
      Beginner

      Model answer

      Compiler verifies that generic types are used consistently — List rejects add("string") and returns Payment from get() without cast. Errors caught in IDE and CI, not as ClassCastException at runtime. Type erasure removes generic info at runtime but compile-time checking is the primary benefit.

      Follow-up probe

      What breaks type safety?

    2. 2What are generics and why does Java have them?
      Beginner

      Model answer

      Parametric polymorphism — type parameters (T, E, K, V) let classes/methods work with multiple types while compile-time type checking prevents ClassCastException. Added in Java 5 to fix raw collection casts. List rejects non-Payment at compile time.

      Follow-up probe

      Do generics exist at runtime?

    3. 3What is type erasure?
      Beginner

      Model answer

      Compiler removes generic type information at runtime for backward compatibility. List becomes List at bytecode. Enables migration from pre-generics code. Implication: cannot do new T(), T.class, or instanceof T. Use Class parameter for runtime type.

      Follow-up probe

      How get generic type at runtime?

    4. 4Why can't you add to List<? extends Account>?
      Beginner

      Model answer

      Covariance — list might be List. Adding CheckingAccount would break type safety if list is actually SavingsAccount-only. Compiler prevents all adds except null. Can read elements as Account safely.

      Follow-up probe

      Can you add null?

    5. 5Explain PECS (Producer Extends Consumer Super).
      Beginner

      Model answer

      When T is produced (read from), use ?

      extends T.

      When T is consumed (written to), use ?

      super T.

      copy(src extends T, dest super T): read from src, write to dest.

      Mnemonic prevents wildcard direction mistakes.

      Follow-up probe

      Apply PECS to Comparator?

    6. 6Difference between List<Account> and List<? extends Account>?
      Beginner

      Model answer

      List: can add Account and subclasses, read as Account, invariant. List: read as Account, cannot add (except null), covariant view of unknown subtype list. Use extends for read-only processing of unknown subtype list.

      Follow-up probe

      When use each?

    Intermediate

    5
    1. 7What is the difference between <T> and <?>?
      Intermediate

      Model answer

      : named type parameter — same T throughout method, can appear multiple times, can use in bounds. : wildcard — unknown single type, each ? independent, used for variance (extends/super). Use T when you need to refer to same type twice; ? for flexibility.

      Follow-up probe

      Multiple type parameters?

    2. 8Explain covariance and contravariance in Java.
      Intermediate

      Model answer

      Covariance: List accepts List — read up the hierarchy. Contravariance: List accepts List — write down the hierarchy. Java generics invariant by default; wildcards enable controlled variance. Arrays: covariant (Account[] holds CheckingAccount[]) — ArrayStoreException at runtime.

      Follow-up probe

      Why arrays covariant but generics not?

    3. 9What is a bounded type parameter?
      Intermediate

      Model answer

      — T must be subtype of Account AND implement Auditable. Enables calling methods from both on T. Multiple bounds with &. extends used for both classes and interfaces in generic bounds.

      Follow-up probe

      T extends Object vs unbound T?

    4. 10How does Spring Data JpaRepository use generics?
      Intermediate

      Model answer

      JpaRepository — T is entity type, ID is primary key type. Spring generates typed implementation at runtime via proxy. findById(ID) returns Optional. Compile-time safe queries per entity without boilerplate.

      Follow-up probe

      How Spring knows T at runtime?

    5. 11Deserialize List<Payment> with Jackson — why TypeReference?
      Intermediate

      Model answer

      Type erasure removes List parameter at runtime. Jackson needs TypeReference> anonymous subclass to capture generic type via reflection on super class. Same pattern in Spring ParameterizedTypeReference for WebClient.

      Follow-up probe

      JavaType vs TypeReference?

    Advanced

    5
    1. 12Design a generic Result<T, E> for enterprise APIs.
      Advanced

      Model answer

      sealed interface Result permits Success, Failure. Success holds T value. Failure holds E error. map(), flatMap() on success path. Used instead of exceptions for expected failures. Compiler forces handling both cases with pattern matching.

      Follow-up probe

      Result vs Optional?

    2. 13Implement generic Repository<T, ID> with PECS for batch save.
      Advanced

      Model answer

      interface Repository { T save(T entity); void saveAll(List entities); }. saveAll uses extends because entities are produced (read) into repository. findAll returns List — concrete type, not wildcard.

      Follow-up probe

      Paging Page?
    3. 14Why is List<CheckingAccount> not a List<Account>?
      Advanced

      Model answer

      Invariance prevents runtime type hole: if allowed, you could add SavingsAccount to List reference pointing to List — corrupting type safety. Wildcards provide controlled variance without this hole. Same reason C# and Kotlin made similar choices.

      Follow-up probe

      Kotlin declaration-site variance?

    4. 15Generic method vs wildcard — when prefer each?
      Advanced

      Model answer

      Generic method void process(List list): T same throughout method, can add and read T. Wildcard void process(List list): when input list type unknown, read-only. Rule: use type parameter when you need same T in multiple places; wildcard for API flexibility with PECS.

      Follow-up probe

      Comparable<? super T> in Collections.sort?

    5. 16Type token pattern for runtime generic type safety.
      Advanced

      Model answer

      Pass Class or TypeReference to methods needing runtime type: jsonReader.readValue(json, new TypeReference>() {}). Guava TypeToken, Jackson TypeReference, Spring ResolvableType. Enables type-safe deserialization despite erasure.

      Follow-up probe

      Super type token pattern?

    Hands-on exercise

    Lab: Generics — type safety through design patterns

    • Run playground — observe PECS copy and covariant total balance.
    • Type safety: Try adding a String to List<CheckingAccount> — note compile error.
    • Wildcards: Write method merge(List<? extends Account> src, List<? super Account> dest).
    • Covariance: Pass List<SavingsAccount> to total(List<? extends Account>) — should compile.
    • Contravariance: Add CheckingAccount to List<? super CheckingAccount> referencing List<Account>.
    • Design pattern: Implement Result<T, String> validate(Account a) returning err on negative balance.
    • Bonus: explain invariants vs List<? extends Account> in a comment.

    JavaGenerics

    Starter Templates
    OutputRemote JVM (Piston · Java 15)
    Press Run to execute. Real javac + JVM via remote API. Falls back to simulator on network failure.

    Architecture trade-offs

    • Type parameter vs wildcard: Named T when same type reused; wildcard for flexible input/output with PECS.
    • Invariance vs wildcards: Invariance is safe default; wildcards trade flexibility for read/write restrictions.
    • Generic class vs raw type: Generics win safety; raw types only for legacy interop with immediate cast.
    • Result<T> vs exceptions: Result wins expected failures; exceptions win exceptional control flow.

    Summary

    Generics take you from compile-time type safety (List) through wildcards and variance (PECS) to enterprise design patterns (Repository, Result, TypeReference). Covariance lets you read from subtype lists; contravariance lets you write to supertype lists; invariance is Java's safe default. Master these five concepts and Spring Data, Jackson, and your own APIs become predictable — not a maze of casts and @SuppressWarnings.

    Key takeaways

    • Type safety: List catches wrong types at compile time — erasure removes T at runtime.
    • Wildcards (?): use when exact type unknown — bound with extends or super for safe read/write.
    • Covariance (? extends T): read as T — Producer Extends; cannot add except null.
    • Contravariance (? super T): write T — Consumer Super; read returns Object only.
    • PECS: Producer Extends, Consumer Super — apply to every generic copy/merge API.
    • Design patterns: Repository, Result, Page, TypeReference — Spring stack foundations.
    Ready to mark this lesson complete?Track your journey across the entire course.