> ## Content Index
> Fetch the complete content index at: https://www.ggorantala.dev/llms.txt
> Use this file to discover other available public pages before exploring further.

# How to Write a Generic Class and Method in Java (Java 21)
- URL: https://www.ggorantala.dev/how-to-write-generic-class-method-java/
- Published: 2026-06-09T05:29:00.000Z
- Updated: 2026-10-31T06:30:41.000Z
- Description: Write a generic class, generic methods, bounded types and wildcards in Java 21 by building a parcel shelf, with 21 passing checks and the compiler errors.
- Author: Gopi Gorantala
- Tags: how-to-tutorials, core-java, java-tutorials

## A shelf that rejects the wrong parcel at compile time

Here's the failure this article exists to remove. A shelf that stores `Object` accepts a `String` next to a `Parcel`, and nothing complains until somebody reads it back:

```log
raw shelf, at runtime: class java.lang.String cannot be cast to class dev.ggorantala.parcels.Parcel (java.lang.String is in module java.base of loader 'bootstrap'; dev.ggorantala.parcels.Parcel is in unnamed module of loader 'app')

```

By the end you'll know how to write a generic class and method in Java so that the same mistake stops at the compiler. You'll build a small parcel-tracker library: a `Shelf<T>`, a two-parameter `Pair<A, B>`, a `Page<T>`, a `Repository<T, ID>`, and a `Generics` class with a bounded `maxOf`, a `copy` that uses wildcards correctly, and a `groupBy`. Then you'll see what erasure leaves behind at runtime, because that's where generics bite people.

This is for you if you can read `List<String>` but freeze when you have to write `<T extends Comparable<? super T>>`. I'll explain that signature piece by piece, and I'll show every compiler error I forced so you recognise them later.

```log
Demo / Checks
   |
   +-- Shelf<T>, Pair<A,B>, Page<T>     generic classes and records
   +-- Repository<T,ID> -> InMemoryRepository<T,ID>   generic interface + implementation
   +-- Generics.maxOf / copy / groupBy / firstOfType  generic static methods

```

## Tools and versions

| Tool              | Version I used                   | Check it with  |
| ----------------- | -------------------------------- | -------------- |
| JDK               | OpenJDK 21.0.12.1 (Ubuntu build) | java -version  |
| javac             | 21.0.12.1                        | javac -version |
| Gradle (optional) | 8.14.3, Kotlin DSL               | gradle -v      |

Any JDK 21 works; Temurin is fine. Generics themselves have been in Java since 5, but the files below use records, so Java 16 or newer is the floor. I ran everything on the command line. I did not run IntelliJ IDEA or Eclipse for this article, so the IDE steps below come from how those IDEs normally work and I've kept them short. I also didn't run Maven; the Gradle build is the one I executed. No JUnit either: the Maven Central host wasn't reachable from my build environment, so the checks are a plain `main` method that prints PASS or FAIL. You can move them into JUnit 5 later without changing a line of the library.

## Create the parcel-shelf project

Use this tree. Package `dev.ggorantala.parcels` in every file.

```log
parcel-shelf/
├── build.gradle.kts            (optional)
├── settings.gradle.kts         (optional)
└── src/
    ├── main/java/dev/ggorantala/parcels/
    │   ├── Status.java
    │   ├── Parcel.java
    │   ├── RawShelf.java
    │   ├── Shelf.java
    │   ├── Pair.java
    │   ├── Page.java
    │   ├── Repository.java
    │   ├── InMemoryRepository.java
    │   ├── Generics.java
    │   ├── Demo.java
    │   └── Traps.java
    └── test/java/dev/ggorantala/parcels/Checks.java

```

**Command line.** Create the folders, drop the files in, then:

```bash
javac -Xlint:all -d out $(find src -name '*.java')
java -cp out dev.ggorantala.parcels.Demo

```

On Windows PowerShell, use `javac -Xlint:all -d out (Get-ChildItem -Recurse -Filter *.java src).FullName` instead of the `$(find ...)` part.

**IntelliJ IDEA.** File → New → Project, pick Java, set the JDK to 21, and create the packages under `src/main/java`. Not executed here. **Eclipse.** File → New → Java Project, set the execution environment to JavaSE-21, and add the same source folders. Not executed here either.

**Gradle** (optional, and the one I ran). Put these two files in the project root:

`settings.gradle.kts`

```kotlin
rootProject.name = "parcel-shelf"

```

`build.gradle.kts`

```kotlin
plugins {
    application
}

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(21)
    }
}

sourceSets {
    create("checks") {
        java.srcDir("src/test/java")
        compileClasspath += sourceSets.main.get().output
        runtimeClasspath += sourceSets.main.get().output
    }
}

application {
    mainClass = "dev.ggorantala.parcels.Demo"
}

tasks.register<JavaExec>("checks") {
    classpath = sourceSets["checks"].runtimeClasspath
    mainClass = "dev.ggorantala.parcels.Checks"
}

```

`gradle run` starts the demo and `gradle checks` runs the checks. The `checks` source set exists only so the check class can live under `src/test/java` without a test framework.

## Write the code: class, method, bound, wildcard

The whole job is about one question: *what does the compiler need to know so it can stop a wrong call?* Each step gives it a bit more.

### Step 1: Create the domain types and a shelf that does not know any better

Goal: have a parcel and the broken baseline to compare against.

`src/main/java/dev/ggorantala/parcels/Status.java`

```java
package dev.ggorantala.parcels;

public enum Status { REGISTERED, IN_TRANSIT, DELIVERED }

```

`src/main/java/dev/ggorantala/parcels/Parcel.java`

```java
package dev.ggorantala.parcels;

public record Parcel(String id, String city, int weightGrams, Status status)
        implements Comparable<Parcel> {

    @Override
    public int compareTo(Parcel other) {
        return Integer.compare(weightGrams, other.weightGrams);
    }
}

```

`src/main/java/dev/ggorantala/parcels/RawShelf.java`

```java
package dev.ggorantala.parcels;

import java.util.ArrayList;
import java.util.List;

/** The "before" picture: no type parameter, everything is Object. */
@SuppressWarnings({"rawtypes", "unchecked"})
public class RawShelf {
    private final List items = new ArrayList();

    public void put(Object item) { items.add(item); }

    public Object get(int index) { return items.get(index); }
}

```

`Parcel` implements `Comparable<Parcel>` on purpose. Step 4 needs it, and it's the first generic type you've already been using without noticing. `RawShelf` is the pre-Java-5 style; I suppress the warnings so it compiles cleanly, because the point is what it does at runtime.

Checkpoint: `javac -Xlint:all -d out $(find src -name '*.java')` prints nothing once all files from the steps below are in place. Compile after each step if you like, but `Demo` needs them all.

### Step 2: Turn the shelf into a generic class

Goal: put a type parameter on the class so the caller chooses what it holds.

`src/main/java/dev/ggorantala/parcels/Shelf.java`

```java
package dev.ggorantala.parcels;

import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;

/** A generic class: T is whatever the caller says it is. */
public class Shelf<T> {
    private final List<T> items = new ArrayList<>();

    public void put(T item) { items.add(item); }

    public T get(int index) { return items.get(index); }

    public int size() { return items.size(); }

    public List<T> asList() { return List.copyOf(items); }

    /** A generic method inside a generic class: R is new, T is the class's. */
    public <R> Shelf<R> map(Function<? super T, ? extends R> fn) {
        Shelf<R> out = new Shelf<>();
        for (T item : items) out.put(fn.apply(item));
        return out;
    }
}

```

`<T>` after the class name declares a **type parameter**: a placeholder that the caller fills in with `Shelf<Parcel>`. Everywhere inside the class, `T` is an ordinary type. Two details worth a second look. `asList()` returns `List.copyOf(items)`, so callers can't reach in and mutate the shelf. And `map` declares its *own* parameter `<R>` before the return type. That's a **generic method**: its type parameter belongs to the method, not the class, and the compiler infers `R` from the lambda you pass.

The parameter is `Function<? super T, ? extends R>` rather than `Function<T, R>`. That question mark is Step 5\. For now, read it as "accept a function that is at least as general as asked for".

Try the compile error. With `Shelf<Parcel> s`, calling `s.put("oops")` stops here:

```log
error: incompatible types: String cannot be converted to Parcel

```

That's the line the raw shelf could never produce.

### Step 3: Add a two-parameter record and a generic page

Goal: show that records and static factories can be generic, and that `map` is the same idea again.

`src/main/java/dev/ggorantala/parcels/Pair.java`

```java
package dev.ggorantala.parcels;

/** Two type parameters. A record can be generic too. */
public record Pair<A, B>(A first, B second) {

    public static <A, B> Pair<A, B> of(A first, B second) {
        return new Pair<>(first, second);
    }

    public Pair<B, A> swap() { return new Pair<>(second, first); }
}

```

`src/main/java/dev/ggorantala/parcels/Page.java`

```java
package dev.ggorantala.parcels;

import java.util.List;
import java.util.function.Function;

public record Page<T>(List<T> content, int number, int totalPages) {

    public Page {
        content = List.copyOf(content);
    }

    public <R> Page<R> map(Function<? super T, ? extends R> fn) {
        return new Page<>(content.stream().<R>map(fn).toList(), number, totalPages);
    }
}

```

In `Pair`, the static `of` declares `<A, B>` again. A static method can't see the class's type parameters (there's no instance, so no `A` to refer to), which is why it needs its own. They happen to share the names; they're different variables. In `Page`, the compact constructor copies the list so the page can't be changed from outside. If records are new to you, [How to use Java records](https://ggorantala.dev/how-to-use-java-records/?ref=ggorantala.dev) covers the compact constructor.

The `.<R>map(fn)` with an explicit type witness is there because `fn` is declared `Function<? super T, ? extends R>` and I wanted `toList()` to give back exactly `List<R>`. Without it, javac fails with `inference variable T has incompatible equality constraints R,CAP#1`. Leave it in.

### Step 4: Write a bounded generic method

Goal: a `maxOf` that works for parcels, strings, or anything comparable, and refuses everything else.

`src/main/java/dev/ggorantala/parcels/Generics.java`

```java
package dev.ggorantala.parcels;

import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;

/** Static generic methods: the type parameter sits before the return type. */
public final class Generics {
    private Generics() {}

    /** Bounded: T must be comparable with itself or a supertype of itself. */
    public static <T extends Comparable<? super T>> T maxOf(Collection<? extends T> items) {
        if (items.isEmpty()) throw new IllegalArgumentException("nothing to compare");
        T best = null;
        for (T item : items) {
            if (best == null || item.compareTo(best) > 0) best = item;
        }
        return best;
    }

    /** PECS: read from the producer (extends), write to the consumer (super). */
    public static <T> void copy(List<? super T> dest, List<? extends T> src) {
        for (T item : src) dest.add(item);
    }

    public static <T, K> Map<K, List<T>> groupBy(Collection<? extends T> items,
                                                 Function<? super T, ? extends K> key) {
        Map<K, List<T>> out = new LinkedHashMap<>();
        for (T item : items) {
            out.computeIfAbsent(key.apply(item), k -> new java.util.ArrayList<>()).add(item);
        }
        return out;
    }

    /** Read-only wildcard: works for List<Parcel>, List<Integer>, anything. */
    public static int countAll(Collection<?> items) { return items.size(); }

    /** Class token: the only way to keep a runtime type when T is erased. */
    public static <T> T firstOfType(Collection<?> items, Class<T> type) {
        for (Object o : items) {
            if (type.isInstance(o)) return type.cast(o);
        }
        return null;
    }
}

```

Read the `maxOf` signature left to right:

- `<T extends Comparable<? super T>>`: T must have a natural ordering, either its own or one inherited from a supertype. The `? super T` matters. A `java.sql.Timestamp` is comparable to `Date`, not to `Timestamp`, and a signature of plain `Comparable<T>` would reject it.
- `Collection<? extends T> items`: I'll read from the collection, so any collection of T or a subtype is fine.
- `T` returned: you get back exactly the type you can use, with no cast.

The bound is a compile-time promise. Pass something that isn't comparable and the compiler refuses. For a deeper look at ordering, see [How to implement Comparable and Comparator in Java](https://ggorantala.dev/how-to-implement-comparable-comparator-java/?ref=ggorantala.dev).

### Step 5: Use wildcards the PECS way

Goal: `copy` into a more general list, `groupBy` with any key function.

Same file, `Generics.java`, already contains them. The rule I use: **P**roducer **E**xtends, **C**onsumer **S**uper. If a parameter *gives* you values (`src`), declare `? extends T`. If it *takes* values (`dest`), declare `? super T`. `copy(List<? super T> dest, List<? extends T> src)` therefore accepts a `List<Parcel>` going into a `List<Object>`, or a `List<Integer>` and a `List<Double>` going into one `List<Number>`. `countAll(Collection<?>)` is the "I only need the size" case: unbounded `?`, and you can read objects out but not add anything.

Why not just `List<Parcel>` to `List<Object>`? Because `List<Parcel>` is **not** a `List<Object>`, even though `Parcel` is an `Object`. If it were, you could add a `String` through the `List<Object>` view and corrupt the original. Wildcards are how you say "any list of something compatible" safely.

### Step 6: Add a generic interface and implementation

Goal: the shape every repository, handler, and mapper in your codebase will eventually have.

`src/main/java/dev/ggorantala/parcels/Repository.java`

```java
package dev.ggorantala.parcels;

import java.util.List;
import java.util.Optional;

public interface Repository<T, ID> {
    void save(T entity);

    Optional<T> findById(ID id);

    List<T> findAll();
}

```

`src/main/java/dev/ggorantala/parcels/InMemoryRepository.java`

```java
package dev.ggorantala.parcels;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;

public class InMemoryRepository<T, ID> implements Repository<T, ID> {
    private final Map<ID, T> store = new LinkedHashMap<>();
    private final Function<? super T, ? extends ID> idOf;

    public InMemoryRepository(Function<? super T, ? extends ID> idOf) {
        this.idOf = idOf;
    }

    @Override public void save(T entity) { store.put(idOf.apply(entity), entity); }

    @Override public Optional<T> findById(ID id) { return Optional.ofNullable(store.get(id)); }

    @Override public List<T> findAll() { return new ArrayList<>(store.values()); }
}

```

Two parameters keep the entity and its key separate: `Repository<Parcel, String>`. The constructor takes a `Function` that extracts the id, so the repository doesn't need `Parcel` to implement anything. That's a design call I'd make again: it keeps the generic class from knowing a thing about your entity.

### Step 7: Wire it together in a demo

`src/main/java/dev/ggorantala/parcels/Demo.java`

```java
package dev.ggorantala.parcels;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

public class Demo {
    public static void main(String[] args) {
        RawShelf raw = new RawShelf();
        raw.put(new Parcel("P1", "Leeds", 800, Status.REGISTERED));
        raw.put("oops, a String");
        try {
            Parcel p = (Parcel) raw.get(1);
            System.out.println(p);
        } catch (ClassCastException e) {
            System.out.println("raw shelf, at runtime: " + e.getMessage());
        }

        Shelf<Parcel> shelf = new Shelf<>();
        shelf.put(new Parcel("P1", "Leeds", 800, Status.REGISTERED));
        shelf.put(new Parcel("P2", "Oslo", 2400, Status.IN_TRANSIT));
        shelf.put(new Parcel("P3", "Leeds", 150, Status.DELIVERED));
        // shelf.put("oops");   // does not compile
        System.out.println("shelf size: " + shelf.size());
        System.out.println("cities: " + shelf.map(Parcel::city).asList());

        System.out.println("heaviest: " + Generics.maxOf(shelf.asList()));
        System.out.println("last city alphabetically: " + Generics.maxOf(List.of("Leeds", "Oslo", "Rome")));

        Map<String, List<Parcel>> byCity = Generics.groupBy(shelf.asList(), Parcel::city);
        byCity.forEach((c, ps) -> System.out.println(c + " -> " + ps.stream().map(Parcel::id).toList()));

        List<Object> sink = new ArrayList<>();
        Generics.copy(sink, shelf.asList());
        System.out.println("sink holds " + sink.size() + " objects");

        Page<Parcel> page = new Page<>(shelf.asList(), 0, 1);
        Page<String> labels = page.map(p -> p.id() + "@" + p.city());
        System.out.println("labels: " + labels);

        Repository<Parcel, String> repo = new InMemoryRepository<>(Parcel::id);
        shelf.asList().forEach(repo::save);
        System.out.println("P2 -> " + repo.findById("P2").map(Parcel::city).orElse("?"));

        Pair<String, Integer> pair = Pair.of("P1", 800);
        System.out.println(pair + " swapped " + pair.swap());
    }
}

```

Checkpoint: compile with `javac -Xlint:all -d out $(find src -name '*.java')`. You should get no output at all, not even a warning.

## Try it end to end

```bash
java -cp out dev.ggorantala.parcels.Demo

```

```log
raw shelf, at runtime: class java.lang.String cannot be cast to class dev.ggorantala.parcels.Parcel (java.lang.String is in module java.base of loader 'bootstrap'; dev.ggorantala.parcels.Parcel is in unnamed module of loader 'app')
shelf size: 3
cities: [Leeds, Oslo, Leeds]
heaviest: Parcel[id=P2, city=Oslo, weightGrams=2400, status=IN_TRANSIT]
last city alphabetically: Rome
Leeds -> [P1, P3]
Oslo -> [P2]
sink holds 3 objects
labels: Page[content=[P1@Leeds, P2@Oslo, P3@Leeds], number=0, totalPages=1]
P2 -> Oslo
Pair[first=P1, second=800] swapped Pair[first=800, second=P1]

```

Two lines are worth a look. `heaviest` is a `Parcel` with no cast anywhere in `Demo`. And "last city alphabetically: Rome" is `maxOf` on strings (natural ordering is alphabetical). My first draft labelled that line "longest city", which was wrong. The output made it obvious, which is a good argument for running things.

On Gradle, `gradle --offline run` prints the same lines. The first run took about 32 seconds on my two-core machine, almost all of it the Gradle daemon starting up.

## Run the 21 checks

`src/test/java/dev/ggorantala/parcels/Checks.java`

```java
package dev.ggorantala.parcels;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

public class Checks {
    static int passed, failed;

    static void check(String name, boolean ok) {
        if (ok) { passed++; System.out.println("PASS " + name); }
        else    { failed++; System.out.println("FAIL " + name); }
    }

    public static void main(String[] args) {
        Parcel a = new Parcel("A", "Leeds", 800, Status.REGISTERED);
        Parcel b = new Parcel("B", "Oslo", 2400, Status.IN_TRANSIT);
        Parcel c = new Parcel("C", "Leeds", 150, Status.DELIVERED);
        List<Parcel> all = List.of(a, b, c);

        Shelf<Parcel> shelf = new Shelf<>();
        all.forEach(shelf::put);
        check("shelf size", shelf.size() == 3);
        check("shelf get", shelf.get(1) == b);
        check("shelf asList is a copy", shelf.asList() != shelf.asList());
        check("shelf map changes type", shelf.map(Parcel::weightGrams).get(0) == 800);
        try { shelf.asList().add(a); check("asList immutable", false); }
        catch (UnsupportedOperationException e) { check("asList immutable", true); }

        check("maxOf parcels", Generics.maxOf(all) == b);
        check("maxOf strings", Generics.maxOf(List.of("a", "c", "b")).equals("c"));
        try { Generics.maxOf(List.<Integer>of()); check("maxOf empty throws", false); }
        catch (IllegalArgumentException e) { check("maxOf empty throws", true); }

        List<Object> sink = new ArrayList<>();
        Generics.copy(sink, all);
        check("copy into List<Object>", sink.size() == 3);
        List<Number> nums = new ArrayList<>();
        Generics.copy(nums, List.of(1, 2));
        Generics.copy(nums, List.of(1.5));
        check("copy ints and doubles into List<Number>", nums.size() == 3);

        Map<String, List<Parcel>> g = Generics.groupBy(all, Parcel::city);
        check("groupBy keys in encounter order", List.copyOf(g.keySet()).equals(List.of("Leeds", "Oslo")));
        check("groupBy Leeds has 2", g.get("Leeds").size() == 2);
        check("countAll wildcard", Generics.countAll(all) == 3 && Generics.countAll(List.of("x")) == 1);

        check("firstOfType hit", Generics.firstOfType(List.of("x", a), Parcel.class) == a);
        check("firstOfType miss", Generics.firstOfType(List.of("x"), Parcel.class) == null);

        Page<Parcel> page = new Page<>(all, 0, 1);
        check("page map", page.map(Parcel::id).content().equals(List.of("A", "B", "C")));
        List<Parcel> editable = new ArrayList<>(all);
        Page<Parcel> copied = new Page<>(editable, 0, 1);
        editable.clear();
        check("page copies content", copied.content().size() == 3);

        Repository<Parcel, String> repo = new InMemoryRepository<>(Parcel::id);
        all.forEach(repo::save);
        check("repo find hit", repo.findById("B").orElseThrow() == b);
        check("repo find miss", repo.findById("Z").isEmpty());
        check("repo findAll order", repo.findAll().equals(all));

        Pair<String, Integer> p = Pair.of("x", 1);
        check("pair swap", p.swap().equals(Pair.of(1, "x")));

        System.out.println(passed + " passed, " + failed + " failed");
        if (failed > 0) System.exit(1);
    }
}

```

```bash
java -cp out dev.ggorantala.parcels.Checks

```

```log
PASS shelf size
PASS shelf get
PASS shelf asList is a copy
PASS shelf map changes type
PASS asList immutable
PASS maxOf parcels
PASS maxOf strings
PASS maxOf empty throws
PASS copy into List<Object>
PASS copy ints and doubles into List<Number>
PASS groupBy keys in encounter order
PASS groupBy Leeds has 2
PASS countAll wildcard
PASS firstOfType hit
PASS firstOfType miss
PASS page map
PASS page copies content
PASS repo find hit
PASS repo find miss
PASS repo findAll order
PASS pair swap
21 passed, 0 failed

```

One check failed on my first run, and it was my test that was wrong, not the library. I'd asserted `page.content() != all` to prove `Page` copies its input, but `List.copyOf` on an already-immutable list (like the one `List.of` returns) hands back the *same* instance. The copy is only real when the source is a mutable `ArrayList`, so the check now clears an editable list after building the page and asserts the page still has three items. If you've ever written `List.copyOf` and wondered whether it allocates, that's the answer: not always.

## What the compiler erases, and what it keeps

**Erasure** means the compiler checks your generics, then removes them. At runtime a `Shelf<Parcel>` is just a `Shelf`. You can see it in the bytecode:

```bash
javap -p -s -cp out dev.ggorantala.parcels.Shelf

```

```log
  public void put(T);
    descriptor: (Ljava/lang/Object;)V
  public T get(int);
    descriptor: (I)Ljava/lang/Object;

```

The `T` is still in the signature (that's what the compiler reads), but the descriptor, the part the JVM runs, says `Object`. For `maxOf`, whose `T extends Comparable<? super T>`, erasure uses the bound instead: the compiler inserts a `checkcast` to `Comparable`. Every place you call `shelf.get(0)` as a `Parcel`, the compiler also inserts a hidden cast. That's why the raw shelf fails *at the cast*, not at `put`.

`Traps.java` shows what that costs you. Run it:

`src/main/java/dev/ggorantala/parcels/Traps.java`

```java
package dev.ggorantala.parcels;

import java.util.ArrayList;
import java.util.List;

/** Runtime facts about erasure, printed rather than asserted. */
public class Traps {
    @SuppressWarnings({"unchecked", "rawtypes"})
    public static void main(String[] args) {
        System.out.println("same class: " + (new ArrayList<String>().getClass() == new ArrayList<Integer>().getClass()));

        List<String> names = new ArrayList<>();
        List raw = names;
        raw.add(42);                       // compiles with a warning, runs fine
        System.out.println("names.size() after raw add: " + names.size());
        try {
            String s = names.get(0);       // the compiler's hidden cast fails here
            System.out.println(s);
        } catch (ClassCastException e) {
            System.out.println("heap pollution surfaces later: ClassCastException");
        }

        Shelf<Parcel>[] arr = new Shelf[2]; // legal, but unchecked
        Object[] objs = arr;
        objs[0] = new Shelf<String>();      // no ArrayStoreException: erasure
        System.out.println("stored Shelf<String> in Shelf<Parcel>[]: " + (objs[0] != null));

        Parcel p = new Parcel("P9", "Rome", 10, Status.REGISTERED);
        List<Object> mixed = List.of("x", 7, p);
        System.out.println("firstOfType Parcel: " + Generics.firstOfType(mixed, Parcel.class).id());
        System.out.println("firstOfType Long: " + Generics.firstOfType(mixed, Long.class));
    }
}

```

```log
same class: true
names.size() after raw add: 1
heap pollution surfaces later: ClassCastException
stored Shelf<String> in Shelf<Parcel>[]: true
firstOfType Parcel: P9
firstOfType Long: null

```

Read the five lines in order:

1. A `List<String>` and a `List<Integer>` have the *same* class at runtime.
2. Adding an `Integer` through a raw `List` reference compiled (with a warning I suppressed) and ran. The list now holds a number while claiming to hold strings. This is called **heap pollution**.
3. The failure shows up later, at an unrelated line, as a `ClassCastException`. That's the real danger: the bug and the crash are far apart.
4. A `Shelf<Parcel>[]` array accepts a `Shelf<String>` without an `ArrayStoreException`, because at runtime both are `Shelf`. This is why Java forbids `new T[]`, and why mixing arrays and generics is a bad idea. Use `List<Shelf<Parcel>>`.
5. `firstOfType` is the workaround for when you genuinely need the runtime type: pass `Parcel.class` (a `Class<T>` token) and call `type.isInstance` and `type.cast`. It returned the parcel for `Parcel.class` and `null` for `Long.class`.

## Errors I hit, and the fixes

All of these are from `javac 21.0.12.1`. I forced each one in a one-line class compiled against the library.

| What I wrote                                   | Message                                                               | Cause and fix                                                                                      |
| ---------------------------------------------- | --------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
| Shelf<int> s                                   | unexpected type / required: reference / found: int                    | Type arguments must be reference types. Use Shelf<Integer>.                                        |
| new T() inside class X<T>                      | unexpected type / required: class / found: type parameter T           | The JVM doesn't know what T is at runtime. Pass a Supplier<T> or a Class<T> instead.               |
| new T\[3\]                                     | generic array creation                                                | Same cause. Use a List<T>, or (T\[\]) new Object\[3\] inside a class that never exposes the array. |
| o instanceof List<String>                      | Object cannot be safely cast to List<String>                          | The <String> isn't there at runtime. Test o instanceof List<?> and check the elements.             |
| List<Object> os = parcels;                     | List<Parcel> cannot be converted to List<Object>                      | Generics are invariant. Take List<? extends Object> or write a generic method.                     |
| l.add(1) on List<? extends Number>             | int cannot be converted to CAP#1                                      | You can read from an extends wildcard, never write to it. If you need to write, you want ? super.  |
| Generics.maxOf(List.of(new Object()))          | inference variable T has incompatible bounds                          | Object isn't Comparable. The bound did its job.                                                    |
| static T t; in class X<T>                      | non-static type variable T cannot be referenced from a static context | Static members belong to the class, not an instance. Give the static method its own <T>.           |
| Shelf<Parcel> s = new Shelf<>(); s.put("oops") | String cannot be converted to Parcel                                  | That's the whole point of generics. Fix the argument.                                              |

If you're on a JDK older than 16, `public record Pair<A, B>` stops compiling. I tried `--release 11` and got `records are not supported in -source 11`. Replace the records with ordinary classes, or upgrade.

## Variations worth knowing

- **Bounded to several types.** `<T extends Number & Comparable<T>>` takes one class bound followed by interface bounds, joined with `&`. If one of them is a class, it must come first.
- **Sealed and generic together.** A `sealed interface Result<T>` with two record cases is the usual shape for a "value or error" return. [How to use sealed classes and interfaces](https://ggorantala.dev/how-to-use-sealed-classes-interfaces-java/?ref=ggorantala.dev) goes through sealed types.
- **Don't reinvent what the JDK has.** `Optional<T>` is a generic class, and if you haven't read how it's meant to be used, [How to use Optional correctly](https://ggorantala.dev/how-to-use-optional-java/?ref=ggorantala.dev) is the next stop. Method references like `Parcel::city` are what let `map` and `groupBy` read well; see [What are method references](https://www.ggorantala.dev/what-are-method-references/).
- **Builders.** If a generic class grows a dozen options, [How to implement the builder pattern](https://ggorantala.dev/how-to-implement-builder-pattern-java/?ref=ggorantala.dev) is how I'd handle it.
- **Tests.** The `main`\-method checks are a stand-in. When you have JUnit, [How to write unit tests with JUnit 5](https://ggorantala.dev/how-to-write-unit-tests-junit-5-java/?ref=ggorantala.dev) shows the real thing (that article is planned and may not be live yet).
- **Production habit.** Turn on `-Xlint:all` in your build and treat `unchecked` warnings as bugs. Every one of them is a spot where the compiler has stopped protecting you.

## Recap

- Add `<T>` after a class or interface name to make it generic; add it before the return type to make a single method generic.
- Static methods can't use the class's `T`, so declare their own.
- Use `extends` bounds when you need to call methods on `T`: `<T extends Comparable<? super T>>`.
- Producer `extends`, consumer `super`: read from `? extends`, write to `? super`.
- Generics exist only at compile time. No `new T()`, no `new T[]`, no `instanceof List<String>`; pass a `Class<T>` when you must have the type at runtime.
- Never use raw types in new code. They switch the checking off.

## Generics questions people ask

**What's the difference between a generic class and a generic method?** A generic class declares its type parameter after the name (`Shelf<T>`) and every instance fixes it. A generic method declares one before the return type (`<R> Shelf<R> map(...)`) and each call can pick a different type. A generic class can contain generic methods, as `Shelf` does.

**Why can't I create an instance of T or an array of T?** After erasure, the JVM doesn't know what T was, so `new T()` and `new T[n]` have nothing to build. Pass a `Supplier<T>` or `Class<T>`, or use a `List<T>`.

**When do I use `? extends` and when `? super`?** If the parameter produces values you read, use `? extends T`. If it consumes values you write, use `? super T`. If you only need `Object`, such as a size, use `?`.

**Is `List<Object>` the same as `List<?>`?** No. A `List<Object>` accepts any object but only another `List<Object>` can be assigned to it. A `List<?>` accepts any list but you can't add to it.

## References

- [The Java Tutorials: Generics](https://docs.oracle.com/javase/tutorial/java/generics/index.html?ref=ggorantala.dev), Oracle
- [Java Language Specification, chapter 4.5 and 8.1.2](https://docs.oracle.com/javase/specs/jls/se21/html/jls-4.html?ref=ggorantala.dev#jls-4.5), Oracle, Java SE 21
- [java.util.List.copyOf](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/List.html?ref=ggorantala.dev#copyOf%28java.util.Collection%29), JDK 21 API.