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.
<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>whereComparable<? super T>is needed, and finding out when someone passes dates or a subclass. - Bounding a numeric method by
Numberand summing money withdoubleValue(). That is fine for metrics; for amounts useBigDecimaldirectly.
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
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.
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.
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.
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