Lab 12: Generics
Objective
Write generic classes and methods, use bounded type parameters, understand wildcards (? extends, ? super), and apply the PECS principle (Producer Extends, Consumer Super).
Background
Generics enable type-safe, reusable code — a single Stack<T> works for String, Integer, or any type, with compile-time type checking. Without generics you'd cast everywhere and discover ClassCastException at runtime. Understanding generics deeply unlocks the Collections framework, Streams, Optional, and every modern Java library.
Time
40 minutes
Prerequisites
Lab 06 (OOP)
Lab 08 (Interfaces)
Lab 09 (Collections)
Tools
Java 21 (Eclipse Temurin)
Docker image:
innozverse-java:latest
Lab Instructions
Step 1: Generic Classes
💡 Type parameters (
<T>,<A, B>) are replaced at compile time with actual types. The diamond<>lets the compiler infer the type from context. Generics exist only at compile time — at runtime allBox<String>andBox<Integer>are justBox(type erasure).
📸 Verified Output:
Step 2: Generic Methods
💡
<T extends Comparable<T>>is a bounded type parameter —Tmust implementComparable<T>. This lets you callcompareTo()inside the method. Without the bound, the compiler doesn't knowThas any methods. Bounds enable generic algorithms that work on any compatible type.
📸 Verified Output:
Step 3: Bounded Type Parameters
💡 Multiple bounds use
&:<T extends Number & Comparable<T>>. The class bound (if any) must come first, followed by interface bounds. This lets you call methods from all bounded types within the generic method. It's used in the JDK itself:<T extends Object & Comparable<? super T>>forCollections.max().
📸 Verified Output:
Step 4: Wildcards — ?, ? extends, ? super
?, ? extends, ? super💡 PECS — Producer Extends, Consumer Super: If a parameter provides (produces) values for you to read →
? extends T. If a parameter accepts (consumes) values you write →? super T.Collections.copy(dst, src)usesList<? super T>for dst andList<? extends T>for src — the canonical PECS example.
📸 Verified Output:
Step 5: Generic Data Structures
💡
@SuppressWarnings("unchecked")silences the cast warning fromObject[]toT[]. This is necessary because Java can't createT[]directly (new T[n]is illegal — type erasure). The cast is safe here because we only storeTinstances, but the compiler can't verify it. Use this annotation minimally and only when the cast is provably safe.
📸 Verified Output:
Step 6: Type Inference & var
💡
vardoesn't mean dynamic typing — the type is fixed at compile time, just inferred.var x = new ArrayList<String>()is exactlyArrayList<String> x = new ArrayList<String>(). Usevarwhen the type is obvious from the right side; avoid it when it obscures what type you're working with.
📸 Verified Output:
Step 7: Generics with Reflection
💡 Type tokens (
Class<T>) are the standard Java pattern to preserve generic type information at runtime (working around erasure). Libraries like Jackson, Gson, Spring, and Hibernate use this pattern extensively.TypeReference<List<String>>in Jackson is a more sophisticated version.
📸 Verified Output:
Step 8: Complete Example — Generic Repository
💡 A generic
Repository<T extends Entity>works for Users, Products, Orders — any entity with an ID. This is the Repository pattern used in Spring Data JPA. The<R extends Comparable<R>>onfindAllSortedByensures you can only sort by comparable fields, catching type errors at compile time.
📸 Verified Output:
Verification
Summary
You've written generic classes, generic methods, bounded type parameters, wildcards with PECS, a generic stack, type inference with var, type tokens for reflection, and a generic repository. Generics are Java's most powerful compile-time safety mechanism — master them and you'll rarely see ClassCastException.
Further Reading
Last updated
