Java Bounded Type Parameters

3 minute read

By default, a type parameter <T> can be any reference type. Bounded type parameters restrict T to a specific type or its subtypes — letting you call methods on T that you know exist.


Upper Bounded: <T extends Type>

Restrict T to Type or any subclass/implementor:

// T must be a Number or subclass (Integer, Double, Long, ...)
public static <T extends Number> double sum(List<T> numbers) {
    double total = 0;
    for (T n : numbers) {
        total += n.doubleValue();  // ✅ Can call Number methods on T
    }
    return total;
}

sum(List.of(1, 2, 3));           // T = Integer → 6.0
sum(List.of(1.5, 2.5));          // T = Double → 4.0
// sum(List.of("a", "b"));       // ❌ Compile error — String is not a Number

Without the bound, you could only call Object methods on T.


Why Bounds Are Needed

// Without bound — can't call doubleValue()
public static <T> double sum(List<T> numbers) {
    for (T n : numbers) {
        n.doubleValue();  // ❌ Compile error — T could be anything
    }
}

// With bound — compiler knows T is at least a Number
public static <T extends Number> double sum(List<T> numbers) {
    for (T n : numbers) {
        n.doubleValue();  // ✅ Number has doubleValue()
    }
}

Multiple Bounds

T can extend a class and implement interfaces:

// T must extend Number AND implement Comparable
public static <T extends Number & Comparable<T>> T max(T a, T b) {
    return a.compareTo(b) >= 0 ? a : b;
}

max(10, 20);      // ✅ Integer extends Number, implements Comparable
max(1.5, 2.5);    // ✅ Double extends Number, implements Comparable

Rules for Multiple Bounds

<T extends ClassA & InterfaceB & InterfaceC>
  1. At most one class — and it must come first.
  2. Multiple interfaces are allowed.
  3. Separated by &.
// ✅ Valid
<T extends Number & Comparable<T> & Serializable>

// ❌ Invalid — two classes
<T extends Number & String>

// ❌ Invalid — class must come first
<T extends Comparable<T> & Number>

Bounded Types on Classes

public class NumericBox<T extends Number> {
    private T value;

    public NumericBox(T value) {
        this.value = value;
    }

    public double doubleValue() {
        return value.doubleValue();
    }

    public int intValue() {
        return value.intValue();
    }
}

NumericBox<Integer> intBox = new NumericBox<>(42);
NumericBox<Double> dblBox = new NumericBox<>(3.14);
// NumericBox<String> strBox = new NumericBox<>("hi"); // ❌ Compile error

Recursive Type Bounds

A type parameter that refers to itself — commonly used for comparable types:

// T must be comparable to itself
public static <T extends Comparable<T>> T findMax(List<T> list) {
    if (list.isEmpty()) throw new NoSuchElementException();
    T max = list.get(0);
    for (T item : list) {
        if (item.compareTo(max) > 0) {
            max = item;
        }
    }
    return max;
}

findMax(List.of(3, 1, 4, 1, 5));      // 5
findMax(List.of("banana", "apple"));    // "banana"

The “Curiously Recurring Template Pattern”

public abstract class BaseEntity<T extends BaseEntity<T>> {
    private Long id;

    @SuppressWarnings("unchecked")
    public T withId(Long id) {
        this.id = id;
        return (T) this;  // returns the subclass type
    }
}

public class User extends BaseEntity<User> {
    private String name;

    public User withName(String name) {
        this.name = name;
        return this;
    }
}

// Fluent API — returns the correct type at each step
User user = new User().withId(1L).withName("Alice");

Practical Example: Generic Sort with Bounds

public class SortUtil {

    // Only accepts lists of Comparable elements
    public static <T extends Comparable<T>> void bubbleSort(List<T> list) {
        int n = list.size();
        for (int i = 0; i < n - 1; i++) {
            for (int j = 0; j < n - i - 1; j++) {
                if (list.get(j).compareTo(list.get(j + 1)) > 0) {
                    T temp = list.get(j);
                    list.set(j, list.get(j + 1));
                    list.set(j + 1, temp);
                }
            }
        }
    }
}

List<Integer> nums = new ArrayList<>(List.of(5, 3, 8, 1));
SortUtil.bubbleSort(nums);  // [1, 3, 5, 8]

Why No <T super Type>?

You cannot use super with type parameter declarations:

// ❌ Not valid syntax
public class Box<T super Number> { }

// ❌ Not valid syntax
public static <T super Integer> void process(T item) { }

super is only used with wildcards (? super T), which we cover in the next chapter.

Why? A lower bound on a type parameter wouldn’t provide useful compile-time guarantees — you couldn’t call any specific methods on T.


Key Takeaways

  1. <T extends Type> restricts T to Type and its subtypes.
  2. Bounds let you call methods on T that are defined in the bound type.
  3. Multiple bounds: one class (first) + multiple interfaces, joined by &.
  4. Recursive bounds (T extends Comparable<T>) are common for self-referential types.
  5. super is not allowed on type parameter declarations — only on wildcards.

What’s Next?

Next: Wildcards in Generics — ?, ? extends T, ? super T, and the PECS principle.

Updated: