Topic 8.4
Bounded Type Parameters
In one line
A bound limits which types a type parameter accepts: <T extends Number> means "any Number or subclass", and <T extends Comparable<T>> means "any type that can compare itself". In return, your generic code may call the bound's methods, like doubleValue() or compareTo(), on values of type T.
Think of it like this
A ride at a fair says "riders must be at least 120 cm tall". It still takes many different people, but only people who meet the rule, and because of the rule the ride can safely assume every rider fits the seat belt. A bound is that sign on a type parameter: <T extends Number> lets in Integer, Double, Long and so on, and because they're all Numbers, the code inside may call doubleValue() on them.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Bound
- A rule on a type parameter that limits which types it accepts, like
extends Number. - Upper bound
- A bound written
T extends X, meaningTmust beXor a subtype ofX. - Multiple bounds
- Several bounds joined with
&, like<T extends Number & Comparable<T>>:Tmust satisfy all of them. - Recursive bound
- A bound that mentions the type parameter itself, like
<T extends Comparable<T>>: "T can be compared with T". - Comparable
- The generic interface
Comparable<T>with one method,int compareTo(T other), returning negative, zero or positive for less, equal or greater. - Erasure of a type variable
- The single real type the compiler uses for
Tin the bytecode: its first bound, orObjectif it has none. - Not within bounds
- The compiler's complaint when a type argument doesn't satisfy a parameter's bound, like
Stats<String>forStats<T extends Number>.
Step by step
01Unbounded T only knows Object
You want a generic max(a, b). Inside it you need a.compareTo(b). But for an unbounded T, the compiler only knows T is some Object, and Object has no compareTo.
The error even tells you why: T extends Object declared in method <T>max(T,T). Every unbounded type variable is implicitly extends Object.
02Add an upper bound
<T extends Comparable<T>> promises the compiler that every T implements Comparable<T>. Now a.compareTo(b) compiles, and the method works for String, Integer, LocalDate and your own comparable classes.
Callers pay for the promise: max(new Object(), new Object()) is rejected, because Object isn't Comparable. The bound moves a check from run time (a ClassCastException inside a sort) to compile time.
static <T extends Comparable<T>> T max(T a, T b) {
return a.compareTo(b) >= 0 ? a : b;
}
max("pear", "apple"); // T = String -> "pear"
max(3, 9); // T = Integer -> 903Bounding a class's type parameter
Classes take bounds the same way: class Stats<T extends Number>. Inside, every T is a Number, so value.doubleValue() works. Users can write Stats<Integer> or Stats<Double>, but not Stats<String>.
extends is used for interfaces too: <T extends Runnable>, never <T implements Runnable>.
04Multiple bounds with &
<T extends Number & Comparable<T>> accepts types that are both: Integer, Long, Double, BigDecimal. Inside, you can call Number methods (doubleValue()) and Comparable methods (compareTo).
The rule mirrors class declarations: one class at most, listed first, then any number of interfaces. Java uses the first bound for erasure, so the order isn't cosmetic.
05What the bound becomes in bytecode
Bounds aren't only a compile-time check: they decide the erased type. javap -s shows the real descriptors. max takes and returns Comparable; clamp, whose first bound is Number, takes and returns Number.
That's why the first bound matters: calls to the other bounds' methods need an extra checkcast to that interface inside the method.
06The subclass trap: Comparable<T> vs Comparable<? super T>
class Fruit implements Comparable<Fruit> and class Apple extends Fruit. An Apple is comparable, but to Fruit, not specifically to Apple: it implements Comparable<Fruit>, never Comparable<Apple>.
So max(List<Apple>) with <T extends Comparable<T>> fails: T = Apple would need Apple implements Comparable<Apple>. The JDK's own Collections.max uses <T extends Comparable<? super T>>: "T is comparable to T or to some supertype of T". That accepts Apple.
Try it yourself
- 1
Break the Number bound
In the first example, add
System.out.println(sum(List.of("1", "2")));. Predict whether it compiles, then read theupper bounds: Number,Objectandlower bounds: Stringlines in the error. - 2
Uncomment the strict call
In the last example, add
System.out.println(strictMax(apples));and compile. Compare the message with the one in the walkthrough, then fix it by changingstrictMax's bound toComparable<? super T>. - 3
Swap the bound order
In the multiple-bounds example, rewrite
clamp's bound as<T extends Comparable<T> & Number>. Predict the error before compiling.
Code & diagrams
Expected output
sum marks: 245.0
sum temps: 112.0
sum views: 3.0E9
first: 70, average: 81.66666666666667Version 1.10 beats 1.9 because compareTo compares numbers, not text. As strings, "1.10" would sort before "1.9".
Expected output
17
pear
1.10
true falseExpected output
100
2.5
0
120 is above 100 (as double: 120.0)
dosa costs 80The wildcard ? super T is explained in Topic 8.5. This is the same bound Collections.max and Collections.sort use.
Expected output
strict, fruits: mango(300g)
flexible, apples: fuji(180g)
flexible, fruits: mango(300g)Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Call a method the bound doesn't promise
Write static <T> T max(T a, T b) { return a.compareTo(b) >= 0 ? a : b; } with no bound.
Break #2
Pass a type outside the bound
With static <T extends Number> double sum(List<T> list), call sum(List.of("1", "2")).
Break #3
Put an interface before the class in multiple bounds
Write <T extends Comparable<T> & Number>.
Myth vs fact
Myth
For interface bounds you write implements: <T implements Comparable<T>>.
Fact
Bounds always use extends, for classes and interfaces alike. implements isn't allowed in a type parameter section.
Myth
<T extends Number> lets you add Integers and Doubles to the same List<T>.
Fact
T is still one specific type per use. A List<T> with T = Integer takes only Integers. To hold mixed numbers, use List<Number>.
Myth
You can write <T super Integer> for a lower bound.
Fact
Type parameters only take upper bounds. Lower bounds exist only for wildcards: List<? super Integer> (Topic 8.5).
Myth
The order of multiple bounds doesn't matter.
Fact
A class bound must come first, and the first bound is what T erases to in the bytecode, which affects binary compatibility and the casts the compiler inserts.
Interview problem
The problem
A reusable top-k method
An interviewer asks: write one method that returns the k largest items from a list, for any element type that has a natural order, including subclasses that inherit their ordering. Explain your signature.
You're given
- Works for
Integer,String, and a classApple extends Fruitwhere onlyFruit implements Comparable<Fruit>. - Doesn't modify the input list.
- Returns the items largest first.
The interviewer follows up
Why not just <T extends Comparable<T>>?
How would you support types without a natural order?
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Changing a method's bounds changes its erased descriptor. Turning
<T> T max(T, T)(descriptor usesObject) into<T extends Comparable<T>>(usesComparable) breaks binary compatibility: old compiled callers fail withNoSuchMethodError. This is whyCollections.maxis declared<T extends Object & Comparable<? super T>>: the redundantObject &keeps its erasure asObject, matching the pre-generics signature. - ▸
Enum<E extends Enum<E>>is the classic recursive bound. It letsEnumdeclarecompareTo(E o)andgetDeclaringClass()returningClass<E>, soDay.MONDAY.compareTo(Month.MAY)is a compile error. The same "self type" trick appears in builder hierarchies (abstract class Builder<B extends Builder<B>>withB self()) so chained setters return the subclass type. - ▸
Bounds let the compiler emit direct
invokeinterface Comparable.compareTocalls on the erased type, with no reflection or casts per element in the loop. The JIT then devirtualises and inlinescompareTowhen the call site sees one or two concrete types, so a bounded genericmaxis as fast as a hand-writtenintversion for boxed values. - ▸
Prefer
Comparable<? super T>in public APIs (as the JDK does);Comparable<T>silently rejects subclasses that inherit their comparison. Inside private code where all types are final, the simpler form is fine.
Remember this
- 1
Without a bound, a type variable's only known supertype is
Object, so on aTyou can call onlyObjectmethods:equals,hashCode,toString,getClass. Writinga.compareTo(b)on an unboundedTfails withcannot find symbol ... method compareTo(T). An upper bound written withextendstells the compiler more:<T extends Comparable<T>>makescompareTo(T)available. - 2
In a bound,
extendsmeans "is a subtype of", for classes and interfaces. You never writeimplementsthere:<T extends Runnable>is correct even thoughRunnableis an interface. The bound checks every use:Stats<String>withclass Stats<T extends Number>fails withtype argument String is not within bounds of type-variable T. - 3
A type parameter can have several bounds joined by
&:<T extends Number & Comparable<T>>means "aNumberthat can also compare itself" (likeInteger). At most one bound may be a class, and it must come first; the rest must be interfaces. Putting an interface first and a class second givesinterface expected here. - 4
<T extends Comparable<T>>is a recursive bound (also called F-bounded):Tappears inside its own bound. It reads "T is comparable to other T's". The JDK uses the same idea inclass Enum<E extends Enum<E>>, which is how every enum'scompareToaccepts only its own constants. The fully flexible version is<T extends Comparable<? super T>>, which also accepts a subclass likeApplewhosecompareTowas inherited fromFruit. You'll understand the? superpart in Topic 8.5. - 5
The bound also decides what
Tbecomes after compilation. An unboundedTerases toObject;<T extends Comparable<T>>erases toComparable;<T extends Number & Comparable<T>>erases to the first bound,Number.javap -sshows these erased types in the method descriptors. Topic 8.6 builds on this. - 6
There is no lower bound for a type parameter:
<T super Integer>isn't legal Java. Lower bounds exist only for wildcards (? super Integer, Topic 8.5). A good rule: use a bound when the generic code needs to call methods of the bound or relate types (Tcompared toT); otherwise leaveTunbounded so the code works for more types.
Explain it without notes
What does an upper bound like <T extends Number> do for the caller and for the code inside the method?
Explain <T extends Comparable<T>>. Why is it called a recursive bound, and what problem does Comparable<? super T> solve?
What are the rules for multiple bounds, and why does the order matter?
How does a bound affect the bytecode of a generic method?
Practice
Write static <T extends Comparable<? super T>> T min(List<T> list) and test it with integers and strings.
Write a class Range<T extends Comparable<? super T>> with contains(T value) and test it with Range<Integer>(1, 10) and Range<String>("b", "m").
Write static <T extends Number> double[] toDoubles(List<T> list) and print the result for List.of(1, 2, 3) with Arrays.toString.
Trade-offs
- ↔
A bound makes the method more powerful inside (you can call the bound's methods) but less widely usable outside (fewer types qualify). Use the weakest bound that lets the code work.
- ↔
Comparable<? super T>is more correct for public APIs but harder to read thanComparable<T>. Many teams use the simple form internally and the JDK form in shared libraries. - ↔
Bounded generics versus taking a
Comparatorparameter: a bound relies on the type's natural order and is concise; a comparator works for any type and any order. Libraries usually offer both.
Done when you can
Done when you can explain why an unbounded
Tonly hasObjectmethods.Done when you can write
<T extends Number>and<T extends Comparable<? super T>>methods.Done when you can write multiple bounds in the legal order.
Done when you can read a "not within bounds" error and fix it.
Done when you can say what a bounded type variable erases to.
Done when you can explain the
Apple extends Fruitproblem withComparable<T>.