Skip to content

VOL 01 / CH 12 / LESSON 01

12.1 Type Parameters and Generic Methods ​

After learning about classes, interfaces, collections, and exceptions, you can complete this Java 17 example in about 35 minutes. By the end, you should be able to declare generic classes, records, interfaces, and methods, and accurately express "what capabilities a type must have" using upper bounds.

Three containers return String, Part, and Ledger. Their structures are identical; only the element type differs. Copying three implementations is easy. Keeping all three consistent after every change is the expensive part.

Declaring everything as Object isn't ideal: callers would have to perform explicit casting every time they retrieve an item, and type mismatches wouldn't be caught until runtime. Generics take a different approach, making the type part of the declaration, with callers providing the type at usage time.

1. Raw type defers errors to runtime ​

A raw List does not constrain its element type. Import the relevant java.util types for these snippets. Compile the raw example separately: warnings and a runtime failure are expected. Keep different Box definitions separate too:

java
List raw = new ArrayList();
raw.add("Hammer");
raw.add(42);

String tool = (String) raw.get(1); // Runtime ClassCastException

Errors don't only occur on the last line. The real pollution point is integers being written into a list intended to hold only tool names. Parameterized types push diagnostics back to the write location:

java
List<String> tools = new ArrayList<>();
tools.add("Hammer");
// tools.add(42); // Compilation failed

When migrating old code, don't use @SuppressWarnings("unchecked") to cover the entire class. First, identify the unsafe conversions, and keep the checks and suppressions to the smallest possible scope.

2. Declare Box<T> Once ​

One generic structure replaces the three repeated containers. The type parameter T represents the contained value, and each caller supplies the concrete type when using the container.

java
final class Box<T> {
    private final T value;

    Box(T value) {
        this.value = java.util.Objects.requireNonNull(value, "value");
    }

    T value() {
        return value;
    }
}

T is a type parameter; Box<String>'s String is a type argument. The compiler thus treats the return type of value() as String:

java
Box<String> toolBox = new Box<>("Hammer");
String tool = toolBox.value();

The right <> is a diamond. The compiler infers types from the assignment target and constructor parameters, but inference does not mean "automatic identification at runtime." If the context is insufficient or overloaded constructors compete, explicit type arguments are still required.

3. Records and interfaces can also be parameterized ​

A two-part result can retain two independent types:

java
record Pair<K, V>(K key, V value) {
    Pair {
        java.util.Objects.requireNonNull(key, "key");
        java.util.Objects.requireNonNull(value, "value");
    }
}

For Pair<String, Integer>, key() returns String, and value() returns Integer. A record generates constructors, accessors, and equality code without changing generic rules. It does not deep-copy components, so referenced component objects may still be mutable.

Interfaces can defer type selection to the implementation class:

java
interface Repository<ID, E> {
    java.util.Optional<E> find(ID id);
    void save(ID id, E entity);
}

final class ToolRepository implements Repository<String, String> {
    private final java.util.Map<String, String> values = new java.util.HashMap<>();

    @Override
    public java.util.Optional<String> find(String id) {
        java.util.Objects.requireNonNull(id, "id");
        return java.util.Optional.ofNullable(values.get(id));
    }

    @Override
    public void save(String id, String tool) {
        values.put(java.util.Objects.requireNonNull(id, "id"),
                java.util.Objects.requireNonNull(tool, "tool"));
    }
}

This repository rejects null IDs and values so Optional.empty means only “not found.” Saving the same ID replaces its value. The in-memory example provides neither persistence nor concurrent-write guarantees.

4. Static members cannot use the class type parameter T ​

Box<String> and Box<Integer> share the same static state. Therefore, type parameters of such a class cannot appear directly in static field or static method declarations:

java
final class Box<T> {
    // static T lastValue; // Compilation failed

    static <E> Box<E> of(E value) {
        return new Box<>(value);
    }

    private final T value;
    private Box(T value) { this.value = java.util.Objects.requireNonNull(value, "value"); }
    T value() { return value; }
}

The method of declares its own type parameter E, independent of the class parameter T. A method could reuse the same name and shadow T, but different names make their scopes easier to follow.

5. Declare generic method parameters before the return type ​

java
static <T> T first(java.util.List<T> values) {
    if (values.isEmpty()) {
        throw new IllegalArgumentException("empty values");
    }
    return values.get(0);
}

When calling first(List.of("iron", "wood")), the compiler infers T to be String. Inference uses the declaration and call context, not guesses about the method body. First requires a non-null, nonempty list and returns its first element unchanged; that element may itself be null.

6. Upper Bound Explains "What This Type Must Be Able To Do" ​

Taking the maximum requires comparative ability:

java
static <T extends Comparable<? super T>> T max(java.util.List<T> values) {
    java.util.Iterator<T> iterator = values.iterator();
    if (!iterator.hasNext()) {
        throw new IllegalArgumentException("empty values");
    }
    T result = java.util.Objects.requireNonNull(iterator.next(), "element");
    while (iterator.hasNext()) {
        T candidate = java.util.Objects.requireNonNull(iterator.next(), "element");
        if (candidate.compareTo(result) > 0) result = candidate;
    }
    return result;
}

Comparable<? super T> is wider than Comparable<T>: if the parent class already defines rules for comparing with the parent type, the child class list can also use them. Max requires non-null elements. Its Iterator makes one pass and avoids repeated indexed access, which would be quadratic on a LinkedList. This subclass inherits Comparable<Ranked> and can still be max's T:

java
class Ranked implements Comparable<Ranked> {
    final int rank;
    Ranked(int rank) { this.rank = rank; }
    @Override public int compareTo(Ranked other) {
        return Integer.compare(rank, other.rank);
    }
}

final class SpecialRanked extends Ranked {
    SpecialRanked(int rank) { super(rank); }
}

// Inside a method in the same class as max:
SpecialRanked best = max(java.util.List.of(
        new SpecialRanked(2), new SpecialRanked(5)));
System.out.println(best.rank); // 5

Multiple upper bounds are written as <T extends Base & Audited & Comparable<T>>, and if a class upper bound exists, it must be placed at the leftmost position.

7. Run the Basic Contract ​

Put the generic container, pair record, and "take the first item" method into one runnable example. The compiler rejects mismatched types, while assertions check runtime results; these checks address different failures.

First, prepare a temporary directory:

bash
chapter12_basics_root=$(mktemp -d)
readonly chapter12_basics_root
mkdir -p "$chapter12_basics_root/out"
cd "$chapter12_basics_root"

Save as GenericsBasicsDemo.java:

java
import java.util.List;
import java.util.Objects;

public class GenericsBasicsDemo {
    public static void main(String[] args) {
        Box<String> box = Box.of("Hammer");
        checkEquals("Hammer", box.value());

        Pair<String, Integer> stock = new Pair<>("iron", 12);
        checkEquals("iron", stock.key());
        checkEquals(12, stock.value());

        checkEquals("iron", first(List.of("iron", "wood")));
        checkEquals(9, max(List.of(3, 9, 5)));
        expectIllegal(() -> first(List.of()));

        System.out.println("Generic basics check passed");
    }

    static <T> T first(List<T> values) {
        if (values.isEmpty()) throw new IllegalArgumentException("empty values");
        return values.get(0);
    }

    static <T extends Comparable<? super T>> T max(java.util.List<T> values) {
        java.util.Iterator<T> iterator = values.iterator();
        if (!iterator.hasNext()) {
            throw new IllegalArgumentException("empty values");
        }
        T result = java.util.Objects.requireNonNull(iterator.next(), "element");
        while (iterator.hasNext()) {
            T candidate = java.util.Objects.requireNonNull(iterator.next(), "element");
            if (candidate.compareTo(result) > 0) result = candidate;
        }
        return result;
    }

    static void expectIllegal(Runnable action) {
        try {
            action.run();
            throw new AssertionError("expected IllegalArgumentException");
        } catch (IllegalArgumentException expected) {
            // Empty input contract fails as expected.
        }
    }

    static void checkEquals(Object expected, Object actual) {
        if (!Objects.equals(expected, actual)) {
            throw new AssertionError("expected=" + expected + ", actual=" + actual);
        }
    }

    static final class Box<T> {
        private final T value;
        private Box(T value) { this.value = Objects.requireNonNull(value); }
        static <E> Box<E> of(E value) { return new Box<>(value); }
        T value() { return value; }
    }

    record Pair<K, V>(K key, V value) {
        Pair {
            Objects.requireNonNull(key, "key");
            Objects.requireNonNull(value, "value");
        }
    }
}
bash
javac --release 17 -encoding UTF-8 -Xlint:all -d out GenericsBasicsDemo.java
java -cp out GenericsBasicsDemo

Expected output Generic basics check passed. Clean up after saving record:

bash
cd
ls -ld -- "$chapter12_basics_root"
rm -r -- "$chapter12_basics_root"

Check Each Type Parameter's Scope ​

Distinguish type parameters from type arguments, explain the source of diamond inference, and clarify why static methods must declare their own type parameters. Finally, provide an example where a parent class implements Comparable<Parent> and a child class inherits it, verifying the significance of Comparable<? super T>.

Next, let's tackle the most confusing part: why List<Integer> can't be directly passed to List<Number>, and what each of extends and super allows.

→ Next Step: Invariance, Wildcards, and PECS

Standard entry: JLS §4.5: Parameterized Types, JLS §8.4.4: Generic Methods.

Built with VitePress | Software Systems Atlas