Bounded Type Parameters

Restrict a type parameter with extends so generic code can call methods like compareTo or id(), and read the classic Comparable bound.

Syntax<T extends Comparable<? super T>> T max(List<T> items) <T extends Number & Comparable<? super T>> Stats<T> stats(List<T> samples) class Repo<T extends Entity<ID>, ID> { ... }

An unbounded T could be anything, so inside the method you can only call what Object has: equals, hashCode, toString. A bound narrows the choice. <T extends Comparable<T>> means "any type that can compare itself", and now compareTo is available on every T.

The keyword is always extends, even when the bound is an interface. It reads as "is a subtype of".

Bound versus plain interface parameter

Why not just take List<Identifiable>? Because the caller would lose the real type. A method <T extends Identifiable> Map<String, T> indexById(List<T> items) returns Map<String, Customer> when given customers. Taking the interface directly returns Map<String, Identifiable>, and the caller has to cast. Use a bound when you need the interface's methods inside and the real type outside.

Reading the Comparable bound

The JDK writes the bound for sorting as <T extends Comparable<? super T>>. The ? super T part matters for types that inherit their compareTo. LocalDate implements Comparable<ChronoLocalDate>, not Comparable<LocalDate>, so a method bounded by Comparable<T> would reject a list of dates. With ? super T it accepts them.

<T extends Comparable<? super T>> T max(List<T> items)
<N extends Number> double total(List<N> values)
<T extends Number & Comparable<? super T>> Stats<T> stats(List<T> samples)

Multiple bounds

Join bounds with &. At most one of them can be a class, and it must come first; the rest are interfaces. <T extends Number & Comparable<T>> lets you call both doubleValue() and compareTo.

Bounds on classes

Classes can be bounded too: class TopN<T extends Comparable<? super T>> refuses, at compile time, to be created for a type that has no ordering. That is better than a ClassCastException the first time the class tries to sort.

Mistakes and the back-end angle

  • Bounding when you do not call any of the bound's methods. The bound then only limits callers for no gain.
  • Using Comparable<T> where Comparable<? super T> is needed, and finding out when someone passes dates or a subclass.
  • Bounding a numeric method by Number and summing money with doubleValue(). That is fine for metrics; for amounts use BigDecimal directly.

In services, bounds show up around your own interfaces: <T extends Entity<ID>, ID> in a repository, <E extends DomainEvent> in an event publisher, <T extends Comparable<? super T>> in ranking and pagination code.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Write one max or top-N helper that ranks scores, timestamps or latencies by their natural ordering.
  • Index any entity that implements your Identifiable interface by id while keeping its concrete type.
  • Summarise latency or payload-size samples into min, max and mean for a metrics endpoint, whether they are Integer milliseconds or Double seconds.

More examples

Bounding by your own interface

The bound lets the method call id(), and T carries the real type back out, so customers.get returns a Customer. A parameter of type List<Identifiable> would hand back Identifiable and force a cast.

Example · java
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

interface Identifiable {
    String id();
}

record Customer(String id, String name) implements Identifiable {}
record Invoice(String id, long amountCents) implements Identifiable {}

<T extends Identifiable> Map<String, T> indexById(List<T> items) {
    Map<String, T> index = new LinkedHashMap<>();
    for (T item : items) {
        index.put(item.id(), item);
    }
    return index;
}

void main() {
    Map<String, Customer> customers = indexById(
            List.of(new Customer("c-1", "Ada"), new Customer("c-2", "Lin")));
    Map<String, Invoice> invoices = indexById(List.of(new Invoice("inv-9", 4999)));

    Customer lin = customers.get("c-2");   // still a Customer, no cast
    IO.println(lin.name());
    IO.println(invoices);
}
// Output:
// Lin
// {inv-9=Invoice[id=inv-9, amountCents=4999]}

Multiple bounds: summarising latency samples

Comparable supplies compareTo for min and max, Number supplies doubleValue() for the mean, and T carries the real type out, so latency.max() is an Integer. Both bounds are used inside the method, which is what justifies declaring them.

Example · java
import java.util.List;

record Stats<T>(T min, T max, double mean) {}

<T extends Number & Comparable<? super T>> Stats<T> stats(List<T> samples) {
    T min = samples.get(0);
    T max = min;
    double sum = 0;
    for (T sample : samples) {
        if (sample.compareTo(min) < 0) min = sample;
        if (sample.compareTo(max) > 0) max = sample;
        sum += sample.doubleValue();
    }
    return new Stats<>(min, max, sum / samples.size());
}

void main() {
    Stats<Integer> latency = stats(List.of(12, 250, 31, 480, 99));
    IO.println(latency);
    Integer worst = latency.max();   // still an Integer, no cast
    IO.println("worst: " + worst + " ms");

    Stats<Double> load = stats(List.of(0.5, 1.5, 0.25, 1.75));
    IO.println(load);
}
// Output:
// Stats[min=12, max=480, mean=174.4]
// worst: 480 ms
// Stats[min=0.25, max=1.75, mean=1.0]

A bounded generic class

TopN can only be created for types with a natural ordering, which the compiler checks. The same class keeps the best scores or the slowest request times in milliseconds.

Example · java
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;

record Score(String player, int points) implements Comparable<Score> {
    public int compareTo(Score other) {
        return Integer.compare(points, other.points);
    }
}

class TopN<T extends Comparable<? super T>> {
    private final int limit;
    private final List<T> items = new ArrayList<>();

    TopN(int limit) { this.limit = limit; }

    void add(T item) {
        items.add(item);
        items.sort(Comparator.reverseOrder());
        if (items.size() > limit) {
            items.removeLast();
        }
    }

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

void main() {
    TopN<Score> top = new TopN<>(2);
    top.add(new Score("ada", 40));
    top.add(new Score("lin", 95));
    top.add(new Score("sam", 70));
    IO.println(top.items());

    TopN<Integer> slowest = new TopN<>(3);
    for (int ms : new int[] {12, 250, 31, 480, 99}) {
        slowest.add(ms);
    }
    IO.println(slowest.items());
}
// Output:
// [Score[player=lin, points=95], Score[player=sam, points=70]]
// [480, 250, 99]

Discussion

  • Be the first to comment on this lesson.