Generics
Before Java 5 (2004), collections stored Object — every get() required a cast, and every wrong cast caused ClassCastException at runtime.
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
Liststored 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>toList<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:
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 compileVariance rules (Java arrays vs generics):Account[] arr = new CheckingAccount[10]; ✓ arrays are covariant (runtime risk!)List<Account> list = new ArrayList<CheckingAccount>(); ✗ compile errorList<? extends Account> readOnly; // covariance — read Account, no addList<? super CheckingAccount> sink; // contravariance — add CheckingAccountPECS applied:void copy(List<? extends Account> src, // Producer — read from srcList<? super Account> dest) { // Consumer — write to destfor (Account a : src) dest.add(a);}Spring generic patterns:JpaRepository<Payment, Long> // entity + ID typeResponseEntity<PaymentDto> // typed HTTP responseOptional<Payment> // typed absenceCompletableFuture<PaymentResult> // typed async resultPage<Payment> // paginated query resultResult<Payment, PaymentError> // typed success/failureTypeReference<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
- 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 rawList— cannot doinstanceof Paymenton generic param; useClass<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
- <?>: 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>whereList<? extends Account>expected. - <? super T>: "Some supertype of T" — use when method writes to collection. Accepts
List<Account>whereList<? 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)
- Assignment:
List<CheckingAccount> checking→List<? 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)
- Assignment:
List<Account> dest→List<? super CheckingAccount> sink = dest— legal; cansink.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
- Repository<T, ID>: One interface, many entities —
JpaRepository<Payment, Long>; Spring generates typed proxy;findByIdreturnsOptional<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.
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 methodsstatic <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 — producerList<T> findAll();}// Pattern 2: Result<T, E> — typed success/failuresealed 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 tokeninterface PaymentFactory<T extends Account> {T create(String id, double balance);}// Pattern 4: Generic builder-style factory methodclass 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 typeList<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 — invarianceList<? extends Account> readOnly = checking; // covariance OKSystem.out.println("Covariant read: " + readOnly.get(0).id());List<Account> dest = new ArrayList<>();List<? super CheckingAccount> sink = dest; // contravariancesink.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 castBox<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 saveSystem.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.
// Spring Data — generic repositorypublic 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 patternpublic sealed interface ServiceResult<T> permits Success, Failure {record Success<T>(T value) implements ServiceResult<T> {}record Failure<T>(String error) implements ServiceResult<T> {}}@Servicepublic class PaymentService {public ServiceResult<Payment> process(PaymentCommand cmd) {// compiler enforces typed handlingreturn new ServiceResult.Success<>(paymentProcessor.process(cmd));}}// PECS in utility — copy DTOs to entitiespublic static <T extends AccountDto> void mapAccounts(List<? extends T> sources, // Producer ExtendsList<? super AccountDto> targets) { // Consumer Supersources.forEach(dto -> targets.add(dto));}// Bounded wildcard in domainpublic <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) objectwith @SuppressWarnings("unchecked") hides type errors — audit all suppressed warnings in security code paths. - Generic signatures in APIs: Public API returning
Objectforces 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>>() {}.
# Enable all generic warnings in CIjavac -Xlint:unchecked -Werror src/main/java/**/*.java# Find raw type usagegrep -rn "List [a-z]\|Map [a-z]\|new ArrayList()" src/ --include="*.java" | grep -v "<"# Find suppressed unchecked warningsgrep -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.Objectreturn 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
1What is type safety in Java generics?
BeginnerModel answer
Compiler verifies that generic types are used consistently — Listrejects 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?
2What are generics and why does Java have them?
BeginnerModel 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. Listrejects non-Payment at compile time. Follow-up probe
Do generics exist at runtime?
3What is type erasure?
BeginnerModel answer
Compiler removes generic type information at runtime for backward compatibility. Listbecomes 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?
4Why can't you add to List<? extends Account>?
BeginnerModel 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?
5Explain PECS (Producer Extends Consumer Super).
BeginnerModel 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?
6Difference between List<Account> and List<? extends Account>?
BeginnerModel answer
List: can add Account and subclasses, read as Account, invariant. List extends Account>: 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
7What is the difference between <T> and <?>?
IntermediateModel 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?
8Explain covariance and contravariance in Java.
IntermediateModel answer
Covariance: List extends Account> accepts List— read up the hierarchy. Contravariance: List super CheckingAccount> 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?
9What is a bounded type parameter?
IntermediateModel 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?
10How does Spring Data JpaRepository use generics?
IntermediateModel 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?
11Deserialize List<Payment> with Jackson — why TypeReference?
IntermediateModel answer
Type erasure removes Listparameter 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
12Design a generic Result<T, E> for enterprise APIs.
AdvancedModel answer
sealed interface Resultpermits 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?
13Implement generic Repository<T, ID> with PECS for batch save.
AdvancedModel answer
interface Repository{ T save(T entity); void saveAll(List extends T> entities); }. saveAll uses extends because entities are produced (read) into repository. findAll returns List — concrete type, not wildcard. Follow-up probe
Paging Page? 14Why is List<CheckingAccount> not a List<Account>?
AdvancedModel answer
Invariance prevents runtime type hole: if allowed, you could add SavingsAccount to Listreference 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?
15Generic method vs wildcard — when prefer each?
AdvancedModel answer
Generic methodvoid process(List list): T same throughout method, can add and read T. Wildcard void process(List extends T> 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?
16Type token pattern for runtime generic type safety.
AdvancedModel answer
Pass Classor 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
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
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.