Generics

Bounded Type Parameters

15 min Lesson 4 of 13

Bounded Type Parameters

So far the type parameters we have seen — like <T> — accept any type. That flexibility is useful, but often you need to restrict which types are allowed, so that you can call specific methods on the value inside your generic code. That is exactly what bounded type parameters do.

The Problem Without Bounds

Imagine you want to write a method that finds the largest of two values. Your first attempt might look like this:

// Does NOT compile — T has no max() or compareTo() public static <T> T max(T a, T b) { return a.compareTo(b) >= 0 ? a : b; // ERROR: T has no compareTo method }

The compiler rejects this because T could be anything — a Thread, a custom Car class, anything. None of those are guaranteed to support compareTo. You need to tell the compiler that T must implement Comparable.

The extends Bound

You constrain a type parameter by writing <T extends SomeType>. This means T must be SomeType itself or a subtype of it — whether SomeType is a class or an interface. The keyword is always extends, even for interfaces.

public static <T extends Comparable<T>> T max(T a, T b) { return a.compareTo(b) >= 0 ? a : b; } // Works for Integer, Double, String — all implement Comparable System.out.println(max(3, 7)); // 7 System.out.println(max("apple", "fig")); // fig

Inside the method body, the compiler now knows every T has a compareTo method, so the call is safe.

extends for both classes and interfaces: In a bound, extends covers both class inheritance and interface implementation. You would write <T extends Runnable> even though Runnable is an interface — there is no implements keyword in type parameter bounds.

A Generic Class with a Bound

Bounds work on class-level type parameters too. Here is a NumberBox that only accepts numeric types and can return the value as a double:

public class NumberBox<T extends Number> { private final T value; public NumberBox(T value) { this.value = value; } public T getValue() { return value; } // Safe because T is guaranteed to extend Number public double asDouble() { return value.doubleValue(); } } // Usage NumberBox<Integer> intBox = new NumberBox<>(42); NumberBox<Double> dblBox = new NumberBox<>(3.14); System.out.println(intBox.asDouble()); // 42.0 System.out.println(dblBox.asDouble()); // 3.14 // NumberBox<String> strBox = new NumberBox<>("hi"); // COMPILE ERROR

The compile error on the last line is exactly what we want — the bound acts as a guard at compile time, not at runtime.

Multiple Bounds

A type parameter can have more than one bound. The syntax uses & to separate them:

<T extends ClassBound & InterfaceOne & InterfaceTwo>

Two rules apply to multiple bounds:

  1. At most one class bound is allowed, and it must come first.
  2. All remaining bounds must be interfaces.

Here is a realistic example. Suppose you are writing a utility that needs objects that are both Serializable and Comparable — for example, to sort and then persist a list of items:

import java.io.Serializable; import java.util.List; import java.util.Collections; public static <T extends Comparable<T> & Serializable> T findMax(List<T> list) { if (list.isEmpty()) { throw new IllegalArgumentException("List must not be empty"); } return Collections.max(list); } // Integer implements Comparable<Integer> and Serializable — it satisfies both bounds List<Integer> numbers = List.of(3, 1, 9, 2, 7); System.out.println(findMax(numbers)); // 9
Class bound must come first: Writing <T extends Serializable & AbstractBase> where AbstractBase is a class will not compile if AbstractBase appears after an interface. Always put the class first: <T extends AbstractBase & Serializable>.

Bounds in Recursive Generic Definitions

You may have noticed Comparable<T> used as the bound rather than the raw Comparable. This is called a recursive type bound and it is important. It says: T can only be compared to another T, not to some arbitrary type. This keeps comparisons type-safe:

// Without recursive bound: can compare T to anything — too loose <T extends Comparable> T badMax(T a, T b) { ... } // With recursive bound: T can only be compared to another T — correct <T extends Comparable<T>> T goodMax(T a, T b) { ... }

You will see this pattern throughout the Java standard library — Collections.sort uses <T extends Comparable<? super T>> for the same reason.

When to Use Bounds

  • Use a bound when your generic code needs to call a method that only certain types have (e.g. compareTo, doubleValue).
  • Use multiple bounds when you need to combine capabilities (e.g. must be both sortable and serializable).
  • Keep bounds as narrow as necessary. An overly tight bound restricts callers unnecessarily.
Prefer interface bounds over class bounds. Bounding on a concrete class (e.g. <T extends ArrayList>) makes your code brittle and often signals you should be using composition or a different design. Bounding on an interface (e.g. <T extends List>) is far more flexible.

Summary

Bounded type parameters allow you to restrict the set of types that can be substituted for a type variable. The extends keyword works for both class and interface bounds. Multiple bounds are joined with &, with the class bound (if any) listed first. Bounds unlock method calls that are impossible on unconstrained T, transforming generic code from merely flexible to genuinely useful.