Skip to content
Ameen Abozaid
PublishedBEG-031-L04

Bounded types

  • Lesson 4 of 6
  • 10 min read
  • Beginner
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 extends in 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:

java
static <T> T max(T a, T b) {
    return a.compareTo(b) >= 0 ? a : b;
}

It does not compile:

text
error: cannot find symbol
        return a.compareTo(b) >= 0 ? a : b;
  symbol:   method compareTo(T)
  location: variable a of type T

The 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:

java
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:

  1. Callers are limited. average(List.of(1, 2, 4)) and average(List.of(1.5, 2.5)) work. average(List.of("a", "b")) is rejected, because String is not a Number.
  2. The body gains abilities. Since T is at least a Number, you may call Number's methods, such as doubleValue(), on a T.

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:

java
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:

java
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.25

A 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:

java
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 &:

java
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)

java
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"));
text
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)

java
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)));
text
2.3333333333333335
2.0

A call with strings is rejected before the program runs:

text
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: String

The 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)

java
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"));
text
true false
true false

The 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.

java
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.

java
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.

java
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:

  1. static <T extends Comparable<T>> T maxOf(List<T> list)
  2. static <T extends Number> double averageOf(List<T> list)
  3. A class Range<T extends Comparable<T>> with contains(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 T and lets the body call the bound's methods.
  • Write bounds with extends, for classes and interfaces alike: <T extends Number>.
  • <T extends Comparable<T>> means T can be compared with another T, so compareTo is 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

  1. What keyword writes an upper bound?
  2. What does <T extends Comparable<T>> let you do inside the method?
  3. Why must Comparable have its argument in the bound?
  4. How are two bounds written?
  5. Does a bound make List<Integer> assignable to List<Number>?
Show answers
  1. extends, for both classes and interfaces.
  2. Call compareTo on values of type T, comparing them with other values of the same T.
  3. Without it Comparable is a raw type and the compiler stops checking what is compared with what.
  4. Joined with &, for example <T extends Number & Comparable<T>>, with a class bound first.
  5. 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, that extends covers interfaces, calling the bound's methods, and multiple bounds with a class listed first.
  • Oracle, Java SE 21 API: Comparable. Supports that Comparable<T> defines compareTo, the meaning of its negative, zero, and positive results, and that Integer and String implement 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.

Lab and exercises Quiz

Marking a lesson complete is optional and you can undo it any time. Your progress stays in this browser.

Continue to next lesson

Type to search levels, phases, modules and lessons.