Recursive bounds in Java
<T extends Comparable<? super T>> as used by Collections.max.
<T extends Object & Comparable<? super T>> T max(Collection<? extends T>). Scary? Let's decode it one piece at a time.A type that mentions itself
**<T extends Comparable<T>> is a recursive bound: T appears inside its own bound. It means "T can be compared with other Ts**", so generic code can call compareTo with no casts.
static <T extends Comparable<T>> T max(
List<T> xs) {
T best = xs.get(0);
for (T x : xs)
if (x.compareTo(best) > 0) best = x;
return best;
}Your turn
Using that max, what does this print?
static <T extends Comparable<T>> T max(
List<T> xs) {
T best = xs.get(0);
for (T x : xs)
if (x.compareTo(best) > 0) best = x;
return best;
}
void main() {
IO.println(max(List.of("cat", "dog", "ant")));
}catdogantCompile error
Show the answer
String implements Comparable<String>, so it fits the bound. Alphabetically ant < cat < dog.
The inheritance snag
Fruit implements Comparable<Fruit>, and Apple extends Fruit. Apple **inherits compareTo(Fruit), so Apple is a Comparable<Fruit>**, not a Comparable<Apple>. With T = Apple, the bound Comparable<T> isn't satisfied.
class Fruit implements Comparable<Fruit> {
public int compareTo(Fruit o) { return 0; }
}
class Apple extends Fruit {}
// Apple is Comparable<Fruit>, not <Apple>Apples to apples?
Same simple bound, now with Apples. What happens?
class Fruit implements Comparable<Fruit> {
public int compareTo(Fruit o) { return 0; }
}
class Apple extends Fruit {}
static <T extends Comparable<T>> T first(
List<T> xs) { return xs.get(0); }
void main() {
IO.println(first(new ArrayList<Apple>()));
}Prints nullCompile errorThrows IndexOutOfBoundsException
Show the answer
With T = Apple, the bound demands Comparable<Apple>, but Apple only has the inherited Comparable<Fruit>. Inference fails: compile error.
Fixing the bound
<T extends Comparable<T>>Rejects types that inherit compareTo from a superclass, like Apple.
<T extends Comparable<? super T>>"T can be compared with T or one of its supertypes": matches Apple's inherited compareTo(Fruit). This is PECS again: compareTo consumes T.
Decoding Collections.max
Now you can read it: T must be comparable to itself or a supertype (Comparable<? super T>), and the collection may hold any subtype (? extends T). The extra Object & makes T erase to Object, so the method keeps its old pre-generics signature.
static <T extends Object
& Comparable<? super T>>
T max(Collection<? extends T> coll)Where you'll meet it
Sorting and max/min utilities, TreeMap-style containers, and fluent builders that return their own subtype (Builder<B extends Builder<B>>) all use recursive bounds. Being able to read them makes library source code, and senior interviews, much less intimidating.
Key takeaways
- <T extends Comparable<T>>: T compares with itself
- <T extends Comparable<? super T>>: also accepts subclasses of comparable classes
- Enum<E extends Enum<E>> uses the same trick
- Lets generic code call compareTo safely without casts
Java's own Enum class is declared as Enum<E extends Enum<E>>: a recursive bound. It's how every enum gets a compareTo that only accepts constants of the same enum type.
Practice questions
What does this print?
static <T extends Comparable<T>> T max(List<T> xs) {
T best = xs.get(0);
for (T x : xs)
if (x.compareTo(best) > 0) best = x;
return best;
}
void main() {
var words = List.of("pear", "fig", "plum");
System.out.println(max(words));
}- pear
- plum
- fig
- Compile error
Check your answer
plum. String implements Comparable<String>, so it fits the bound. Alphabetically "plum" > "pear" > "fig", so plum is the maximum.
Why does Collections.max use Comparable<? super T> instead of Comparable<T>?
- To allow comparing Strings with Integers
- So types that inherit compareTo from a superclass — e.g. a subclass of a Comparable<Fruit> class — still qualify
- To make max run faster
- Because Comparable can't be parameterized with T
Check your answer
So types that inherit compareTo from a superclass — e.g. a subclass of a Comparable<Fruit> class — still qualify. If Apple extends Fruit and Fruit implements Comparable<Fruit>, then Apple is a Comparable<Fruit>, not a Comparable<Apple>. ? super T accepts that.