Bounded Types
Bounded type parameters constrain which concrete types may be substituted for a type parameter. An upper bound (T extends Number) restricts T to Number and its subtypes, making Number's methods available on values of type T within the generic code. A lower bound cannot be applied to type parameters (only to wildcards), but multiple upper bounds can be combined with & (T extends Number & Comparable<T> & Serializable). Bounds serve two purposes simultaneously: they restrict the valid type arguments that callers can supply, and they expand what the generic code can do with values of type T by exposing the bound's API. Without a bound, only Object's methods are available on T values. This entry covers single and multiple upper bounds, recursive bounds, bounds on class type parameters vs method type parameters, how bounds interact with type erasure, bridge methods generated by the compiler, and practical patterns where bounds enable algorithms impossible with unbounded type parameters.
Single Upper Bound — Restricting and Enabling
// ── Single upper bound — class ────────────────────────────────────────
public class NumberBox<T extends Number> {
private T value;
public NumberBox(T value) { this.value = value; }
public T get() { return value; }
// Number's methods are available because of the bound:
public double asDouble() { return value.doubleValue(); }
public int asInt() { return value.intValue(); }
public long asLong() { return value.longValue(); }
public boolean isPositive() { return value.doubleValue() > 0; }
}
NumberBox<Integer> intBox = new NumberBox<>(42);
NumberBox<Double> dblBox = new NumberBox<>(3.14);
NumberBox<Long> longBox = new NumberBox<>(1_000_000L);
System.out.println(intBox.asDouble()); // 42.0
System.out.println(dblBox.isPositive()); // true
// COMPILE ERROR — String is not a subtype of Number:
// NumberBox<String> strBox = new NumberBox<>("hello");
// ── Single upper bound — method ───────────────────────────────────────
public static <T extends Comparable<T>> T clamp(T value, T min, T max) {
// compareTo() available because T extends Comparable<T>:
if (value.compareTo(min) < 0) return min;
if (value.compareTo(max) > 0) return max;
return value;
}
System.out.println(clamp(5, 1, 10)); // 5
System.out.println(clamp(15, 1, 10)); // 10
System.out.println(clamp("dog", "ant", "fox")); // dog
// ── Upper bound on an interface ────────────────────────────────────────
public static <T extends Iterable<String>> void printAll(T source) {
for (String s : source) { // enhanced-for works because T extends Iterable
System.out.println(s);
}
}
printAll(List.of("alpha", "beta", "gamma")); // List implements Iterable
printAll(Set.of("x", "y", "z")); // Set implements Iterable
// printAll(new int[]{1,2,3}); // COMPILE ERROR — int[] does not implement Iterable
// ── Bound enables calling interface methods ───────────────────────────
public static <T extends AutoCloseable> void useAndClose(T resource) throws Exception {
try {
System.out.println("Using: " + resource);
} finally {
resource.close(); // close() available because T extends AutoCloseable
}
}
// ── Without bound — only Object methods available ─────────────────────
public static <T> void printLength(T value) {
// value.length() // COMPILE ERROR — T is unbounded, only Object methods
System.out.println(value.toString()); // toString() from Object — always OK
System.out.println(value.hashCode()); // hashCode() from Object — always OK
}Multiple Bounds and Recursive Bounds
// ── Multiple bounds ───────────────────────────────────────────────────
// T must be both a Number and Comparable — Integer, Long, Double qualify
public static <T extends Number & Comparable<T>> T clampNumeric(
T value, T min, T max) {
if (value.compareTo(min) < 0) return min; // Comparable bound
if (value.compareTo(max) > 0) return max; // Comparable bound
System.out.printf("%.2f in [%.2f, %.2f]%n",
value.doubleValue(), // Number bound
min.doubleValue(), // Number bound
max.doubleValue()); // Number bound
return value;
}
clampNumeric(5, 1, 10); // 5 in [1.00, 10.00]
clampNumeric(3.14, 0.0, 2.0); // returns 2.0
// COMPILE ERROR — String is Comparable but not Number:
// clampNumeric("hello", "aaa", "zzz");
// ── Class bound must come first ────────────────────────────────────────
// OK — class first, then interfaces:
class MultiBox<T extends Number & Comparable<T> & Serializable> {
private T value;
public MultiBox(T v) { this.value = v; }
public T get() { return value; }
// doubleValue() from Number, compareTo() from Comparable, ready for serialization
}
// COMPILE ERROR — two class bounds not allowed:
// class Bad<T extends Number & Integer> {} // error: cannot extend both
// COMPILE ERROR — class not first:
// class Bad<T extends Comparable<T> & Number> {} // error: Number is a class, must be first
// ── Recursive type bound — self-comparable ────────────────────────────
// The classic form: T can compare itself to other T values
public static <T extends Comparable<T>> T max(List<T> list) {
if (list.isEmpty()) throw new NoSuchElementException();
T result = list.get(0);
for (T element : list) {
if (element.compareTo(result) > 0) result = element;
}
return result;
}
System.out.println(max(List.of(3, 1, 4, 1, 5, 9))); // 9
System.out.println(max(List.of("banana", "apple", "cherry"))); // cherry
// ── Enum's recursive bound in practice ────────────────────────────────
// Enum<E extends Enum<E>> — each enum type refers to itself:
enum Planet { MERCURY, VENUS, EARTH, MARS }
// Because Planet extends Enum<Planet>:
Planet[] planets = Planet.values();
Arrays.sort(planets); // works because Enum<E> implements Comparable<E>
System.out.println(Arrays.toString(planets));
// ── Builder pattern with recursive bound ─────────────────────────────
// Allows fluent chaining through inheritance:
abstract class Builder<T, B extends Builder<T, B>> {
protected String name;
@SuppressWarnings("unchecked")
public B withName(String name) {
this.name = name;
return (B) this; // safe: B is always the concrete subclass
}
public abstract T build();
}
class PersonBuilder extends Builder<Person, PersonBuilder> {
private int age;
public PersonBuilder withAge(int age) {
this.age = age;
return this; // returns PersonBuilder, not Builder
}
@Override public Person build() { return new Person(name, age); }
}
Person p = new PersonBuilder()
.withName("Alice") // returns PersonBuilder (not just Builder)
.withAge(30) // PersonBuilder-specific method
.build();
// ── Type erasure with multiple bounds ────────────────────────────────
// <T extends Number & Comparable<T>>:
// T erases to Number (leftmost bound, which is the class)
// Access through Comparable interface gets synthetic cast in bytecode
// <T extends Comparable<T> & Serializable>:
// T erases to Comparable (leftmost bound, which is an interface)
// Access through Serializable gets synthetic cast in bytecode
// This is why putting the class first matters: it controls what T erases to,
// which affects performance (avoids extra casts for the most-used bound):
// Prefer: <T extends Number & Comparable<T>>
// Over: <T extends Comparable<T> & Number> (Number access costs a cast)Bridge Methods and Bounds in Inheritance
// ── Bridge method generation ──────────────────────────────────────────
public class NumberContainer<T extends Number> {
protected T value;
public NumberContainer(T value) { this.value = value; }
// After erasure: Number getValue() — T erased to bound (Number)
public T getValue() { return value; }
}
// Subclass fixes T = Double:
public class DoubleContainer extends NumberContainer<Double> {
public DoubleContainer(Double value) { super(value); }
// Covariant override — returns Double instead of Number:
@Override
public Double getValue() { return value; }
// Compiler also generates: (bridge) Number getValue() { return this.getValue(); }
// The bridge delegates to the real Double getValue()
}
DoubleContainer dc = new DoubleContainer(3.14);
System.out.println(dc.getValue()); // 3.14 (Double)
// Polymorphism works through the erased type:
NumberContainer<Double> nc = dc;
Number n = nc.getValue(); // calls bridge → calls Double getValue()
System.out.println(n.getClass()); // class java.lang.Double
// ── Observing bridge methods via reflection ───────────────────────────
for (Method m : DoubleContainer.class.getDeclaredMethods()) {
System.out.printf("%-20s bridge=%-5b synthetic=%b%n",
m.getName() + "()" + m.getReturnType().getSimpleName(),
m.isBridge(),
m.isSynthetic());
}
// getValue()Double bridge=false synthetic=false ← your override
// getValue()Number bridge=true synthetic=true ← compiler-generated bridge
// ── Inherited bounds — subclass need not repeat them ──────────────────
class Sorter<T extends Comparable<T>> {
public T findMin(List<T> list) {
return list.stream()
.min(Comparator.naturalOrder()) // works: T has compareTo from bound
.orElseThrow();
}
}
// T still has the Comparable<T> bound — inherited:
class ReverseSorter<T extends Comparable<T>> extends Sorter<T> {
public T findMax(List<T> list) {
return list.stream()
.max(Comparator.naturalOrder()) // T still has compareTo
.orElseThrow();
}
}
ReverseSorter<String> rs = new ReverseSorter<>();
System.out.println(rs.findMax(List.of("banana", "apple", "cherry"))); // cherry
System.out.println(rs.findMin(List.of("banana", "apple", "cherry"))); // apple
// ── Bound tightening in subclass ──────────────────────────────────────
class NumericSorter<T extends Number & Comparable<T>> extends Sorter<T> {
// Tightened: T must also be a Number (Sorter only required Comparable)
public double sumAll(List<T> list) {
return list.stream()
.mapToDouble(Number::doubleValue) // Number bound from this class
.sum();
}
}
NumericSorter<Integer> ns = new NumericSorter<>();
System.out.println(ns.sumAll(List.of(1, 2, 3, 4, 5))); // 15.0
System.out.println(ns.findMin(List.of(3, 1, 4))); // 1