Bounded types
On This Lesson 11 topics
Lesson Overview
A plain T can be any type, so inside the method you can do almost nothing with it: you can store it, return it, and call equals. That is too weak for many real tasks. To find the larger of two values, the method must be able to compare them. To average a list, it must be able to turn each element into a number.
A bound fixes this. It says, "T may be any type, as long as it is at least this". In crate terms, the label is no longer blank: it reads "only crates of tools that can be weighed". Now the worker can safely weigh whatever comes out.
Why this matters: you will meet bounds in the signatures of the JDK and of Spring's helper types, and in your own code each time a generic method needs to compare or calculate.
This lesson builds on "Generic methods", and on interfaces and inheritance. One preview: the Comparable interface used here gets its full treatment in a later module on sorting. Here you need only its single idea: an object that can compare itself with another object of the same type.
Learning Objectives
By the end of this lesson you will be able to:
- Write an upper bound with
extends, such as<T extends Number>. - Explain that
extendsin a bound covers both classes and interfaces. - Explain why a bound lets you call the bound's methods on
T. - Read and write
<T extends Comparable<T>>. - Write a generic class with a bound, such as
Range<T extends Comparable<T>>. - Read a compiler error for a type argument that is outside the bound.
- Recognize a multiple bound such as
<T extends Number & Comparable<T>>.
Written Explanation
The problem: T can do too little
Try to write max for any two values:
static <T> T max(T a, T b) {
return a.compareTo(b) >= 0 ? a : b;
}It does not compile:
error: cannot find symbol
return a.compareTo(b) >= 0 ? a : b;
symbol: method compareTo(T)
location: variable a of type TThe compiler is right. T could be a Contact, an Event, or anything. Not every class has a way to compare itself, so compareTo cannot be allowed.
Upper bounds with extends
You restrict T by writing extends and a type after its name:
static <T extends Number> double average(List<T> nums) {
double total = 0;
for (T n : nums) {
total += n.doubleValue(); // allowed: every Number has doubleValue()
}
return total / nums.size();
}T extends Number means: T must be Number or a subclass of it (Integer, Double, Long, ...). Two things follow:
- Callers are limited.
average(List.of(1, 2, 4))andaverage(List.of(1.5, 2.5))work.average(List.of("a", "b"))is rejected, becauseStringis not aNumber. - The body gains abilities. Since
Tis at least aNumber, you may callNumber's methods, such asdoubleValue(), on aT.
This is the Oracle tutorial's description of bounded type parameters: restrict the types allowed as type arguments, and gain access to the bound's methods.
The word extends is used here for both classes and interfaces. In a bound it means "is a subtype of": it covers "extends" for a class and "implements" for an interface. You never write implements inside the angle brackets.
The most common bound: Comparable<T>
Comparable<T> is a JDK interface with one method, int compareTo(T other). A class that implements it says "I know how to compare myself with another T". It returns a negative number if this object is less than the other, zero if they are equal, and a positive number if it is greater. Integer, Double, and String implement it, so they have a natural order.
To use it as a bound:
static <T extends Comparable<T>> T max(T a, T b) {
return a.compareTo(b) >= 0 ? a : b;
}Read the bound aloud: "T is some type that can be compared with T". The T appears twice because the comparison must be between two values of the same type: an Integer compares with an Integer, a String with a String. You cannot compare an Integer with a String, and the signature makes that impossible.
Now it works for many types and rejects the others:
Utils.max(3, 9); // 9
Utils.max("pear", "apple"); // pear (compared alphabetically)
Utils.max(List.of(4.5, 2.0, 8.25)); // with a list version: 8.25A class of your own, such as Contact, is rejected unless it implements Comparable<Contact>. The compiler's message mentions upper bounds: Comparable<T#1> and lower bounds: Contact.
Bounds on classes
A class can bound its parameter in the same way. A Range that tells whether a value lies between a low and a high end needs comparison:
public class Range<T extends Comparable<T>> {
private final T low;
private final T high;
public Range(T low, T high) {
if (low.compareTo(high) > 0) {
throw new IllegalArgumentException("low must not exceed high");
}
this.low = low;
this.high = high;
}
public boolean contains(T value) {
return value.compareTo(low) >= 0 && value.compareTo(high) <= 0;
}
}Range<Integer> and Range<String> are valid. Range<Object> is not, because Object does not implement Comparable.
More than one bound
A parameter can have several bounds, joined with &:
static <T extends Number & Comparable<T>> T largestAbove(List<T> nums, T limit)Here T must be both a Number and comparable to itself, so the body can call both doubleValue() and compareTo. If one of the bounds is a class, it must come first; any further bounds must be interfaces. Multiple bounds are less common. You will mostly see a single bound.
A note on what bounds do not do
A bound does not change what the compiler treats as the same type. List<Integer> is still not a List<Number>, even if T extends Number. A method taking List<T> with T extends Number still ties all its list elements to one concrete T. Lesson 5 shows how wildcards loosen this.
Examples
Example 1: max of two values (simple)
static <T extends Comparable<T>> T max(T a, T b) {
return a.compareTo(b) >= 0 ? a : b;
}
System.out.println(Utils.max(3, 9));
System.out.println(Utils.max("pear", "apple"));9
pear"pear" is greater than "apple" in alphabetical order, so compareTo is positive and max returns a.
Example 2: average of any numbers (practical)
static <T extends Number> double average(List<T> nums) {
double total = 0;
for (T n : nums) {
total += n.doubleValue();
}
return total / nums.size();
}
System.out.println(Utils.average(List.of(1, 2, 4)));
System.out.println(Utils.average(List.of(1.5, 2.5)));2.3333333333333335
2.0A call with strings is rejected before the program runs:
error: method average in class Utils cannot be applied to given types;
required: List<T>
found: List<String>
reason: inference variable E has incompatible bounds
upper bounds: Number,Object
lower bounds: StringThe lower bound String is what you gave, and the upper bound Number is what the method accepts: they cannot meet.
Example 3: a reusable Range (realistic)
Range<Integer> age = new Range<>(18, 65);
System.out.println(age.contains(30) + " " + age.contains(70));
Range<String> letters = new Range<>("a", "m");
System.out.println(letters.contains("k") + " " + letters.contains("z"));true false
true falseThe same class validates ages, prices, dates, and names, as long as the type compares with itself. Creating new Range<>(Object, Object) would be rejected with a message saying the type argument is not within the bounds of the type variable.
Backend Insight
Validation and data handling in backend code repeats the same questions: is this value in range, which is the latest of these, which is the largest amount? A bounded generic method or class answers them once for every comparable type, instead of once per type.
The idea also explains many library signatures. A repository type like Repository<T extends Entity> limits what can be stored, a sorting method like Collections.sort requires its elements to be comparable, and in later modules you will see Comparable again when you sort lists of your own objects. In each case the bound is a contract: "anything you give me must be able to do this".
Common Mistakes
Mistake 1: Writing implements in a bound
- Incorrect:
<T implements Comparable<T>> - Correct:
<T extends Comparable<T>>
extends is the only keyword in a bound, even for interfaces.
Mistake 2: Forgetting the bound and then calling a method
If the compiler says cannot find symbol for a method you know exists on the type you are passing, the T in the method has no bound that guarantees the method. Add the bound.
Mistake 3: Writing Comparable without its argument
<T extends Comparable> uses the raw type, so compareTo takes an Object and the compiler stops checking. Write Comparable<T> so a String can compare only with a String.
Mistake 4: Passing a class that is not comparable
Utils.max(new Contact(), new Contact()) fails because Contact does not implement Comparable<Contact>. Either implement it (covered later in the course) or choose another way to compare.
Exercises
Try each exercise yourself before opening the solutions.
Level A: Foundation
A1. What does <T extends Number> allow as T: (a) Integer (b) String (c) Double (d) Object?
Show solution A1
A1. (a) and (c). Integer and Double are subclasses of Number. String and Object are not.
A2. Why can a method with <T extends Comparable<T>> call a.compareTo(b), while a method with plain <T> cannot?
Show solution A2
A2. The bound tells the compiler that every T implements Comparable<T>, so compareTo exists. With a plain <T> the compiler only knows T is some Object, which has no compareTo.
A3. Is implements ever written inside the angle brackets of a bound?
Show solution A3
A3. No. A bound always uses extends, whether the bound is a class or an interface.
Level B: Application
B1. Write static <T extends Comparable<T>> T min(List<T> list) returning the smallest element.
Show solution B1
B1.
static <T extends Comparable<T>> T min(List<T> list) {
T best = list.get(0);
for (T item : list) {
if (item.compareTo(best) < 0) {
best = item;
}
}
return best;
}B2. Write static <T extends Number> double sumOfSquares(List<T> nums).
Show solution B2
B2.
static <T extends Number> double sumOfSquares(List<T> nums) {
double total = 0;
for (T n : nums) {
double d = n.doubleValue();
total += d * d;
}
return total;
}sumOfSquares(List.of(1, 2, 3)) is 14.0.
B3. Which of these calls compile for min from B1: min(List.of(5, 2, 8)), min(List.of("pear", "fig")), min(List.of(new Object(), new Object()))?
Show solution B3
B3. The first two compile (results 2 and fig). The third is rejected: Object does not implement Comparable, so the bound is not met.
Level C: Challenge
C1. Write static <T extends Comparable<T>> T clamp(T value, T low, T high) that returns low if value is below it, high if above, and value otherwise.
Show solution C1
C1.
static <T extends Comparable<T>> T clamp(T value, T low, T high) {
if (value.compareTo(low) < 0) return low;
if (value.compareTo(high) > 0) return high;
return value;
}clamp(15, 0, 10) is 10, clamp(-3, 0, 10) is 0, and clamp(4, 0, 10) is 4.
C2. A teammate writes class Cache<T extends Comparable> and says it is fine. Explain what the missing type argument costs.
Show solution C2
C2. Comparable without a type argument is a raw type. Its compareTo takes an Object, so the compiler no longer checks that you compare like with like, and a mismatch (for example a String against an Integer) would only show up at runtime as a ClassCastException. Writing Comparable<T> keeps the check at compile time.
Mini Challenge
Build a tiny Stats utility with bounded methods:
static <T extends Comparable<T>> T maxOf(List<T> list)static <T extends Number> double averageOf(List<T> list)- A class
Range<T extends Comparable<T>>withcontains(T).
Test each with at least two element types, then add one deliberately wrong call (for example averageOf on a List<String>, or Range<Object>) and read the compiler message.
Check your work: you can point, in each message, at the bound that was violated.
Summary & Next Lesson
Key takeaways
- A bound limits the types allowed for
Tand lets the body call the bound's methods. - Write bounds with
extends, for classes and interfaces alike:<T extends Number>. <T extends Comparable<T>>meansTcan be compared with anotherT, socompareTois available.- Classes can be bounded as well:
Range<T extends Comparable<T>>. - Several bounds are joined with
&; a class bound comes first.
Next lesson
Next, "Wildcards: reading the JDK's signatures" introduces ?, so you can read signatures such as List<? extends Number> and understand why List<Integer> is not a List<Number>.
Self-check
- What keyword writes an upper bound?
- What does
<T extends Comparable<T>>let you do inside the method? - Why must
Comparablehave its argument in the bound? - How are two bounds written?
- Does a bound make
List<Integer>assignable toList<Number>?
Show answers
extends, for both classes and interfaces.- Call
compareToon values of typeT, comparing them with other values of the sameT. - Without it
Comparableis a raw type and the compiler stops checking what is compared with what. - Joined with
&, for example<T extends Number & Comparable<T>>, with a class bound first. - No. A bound limits the type argument; it does not make different parameterizations of a generic type related.
References
- Oracle, The Java Tutorials: Bounded Type Parameters. Supports upper bounds with
extends, thatextendscovers interfaces, calling the bound's methods, and multiple bounds with a class listed first. - Oracle, Java SE 21 API: Comparable. Supports that
Comparable<T>definescompareTo, the meaning of its negative, zero, and positive results, and thatIntegerandStringimplement it. - Oracle, The Java Tutorials: Raw Types. Supports the warning that raw types defer type errors to runtime.
- Code and compiler messages were compiled and run with JDK 21.
Practice
Apply this lesson with the module's lab, exercises and quiz.
Marking a lesson complete is optional and you can undo it any time. Your progress stays in this browser.