Upper Bound
An upper bound in Java generics restricts a type parameter or wildcard to a specific type and all its subtypes. It is declared with the extends keyword in two distinct contexts: on a type parameter (class Box<T extends Number>) and on a wildcard (List<? extends Number>). Both forms mean 'this type must be Number or any subtype of Number,' but they differ fundamentally in usage: bounded type parameters name the variable and allow it to be referenced throughout a declaration, while bounded wildcards appear at the use site and keep the type anonymous. This entry covers both forms in full, how the bound expands the available API within the generic body, multiple upper bounds on type parameters (T extends A & B & C), the precise subtyping rules introduced by upper-bounded wildcards, how upper bounds interact with type erasure (determining what T erases to), the difference between <T extends Number> and <? extends Number> and when each is appropriate, and practical patterns where upper bounds are the enabling mechanism.
Upper Bound on Type Parameters — T extends SomeType
// ── Upper bound on class type parameter ──────────────────────────────
public class NumberRange<T extends Number & Comparable<T>> {
private final T min;
private final T max;
public NumberRange(T min, T max) {
// compareTo() available from Comparable<T> bound:
if (min.compareTo(max) > 0) throw new IllegalArgumentException("min > max");
this.min = min;
this.max = max;
}
public boolean contains(T value) {
return value.compareTo(min) >= 0 && value.compareTo(max) <= 0;
}
public double span() {
// doubleValue() available from Number bound:
return max.doubleValue() - min.doubleValue();
}
public T getMin() { return min; }
public T getMax() { return max; }
}
NumberRange<Integer> intRange = new NumberRange<>(1, 10);
System.out.println(intRange.contains(5)); // true
System.out.println(intRange.contains(15)); // false
System.out.println(intRange.span()); // 9.0
NumberRange<Double> dblRange = new NumberRange<>(0.0, 1.0);
System.out.println(dblRange.contains(0.5)); // true
System.out.println(dblRange.span()); // 1.0
// COMPILE ERROR — String satisfies neither Number nor Comparable<String> in this combination:
// Correction: String IS Comparable<String> but NOT Number:
// NumberRange<String> strRange = new NumberRange<>("a", "z"); // COMPILE ERROR (not Number)
// ── Upper bound makes bound's methods available ────────────────────────
public static <T extends Comparable<T>> int countGreaterThan(List<T> list, T threshold) {
int count = 0;
for (T element : list) {
if (element.compareTo(threshold) > 0) count++; // compareTo available from bound
}
return count;
}
System.out.println(countGreaterThan(List.of(1,5,3,8,2), 4)); // 2 (5 and 8)
System.out.println(countGreaterThan(List.of("a","m","z","b"), "k")); // 2 (m and z)
// ── Upper bound on method type parameter ─────────────────────────────
public static <T extends AutoCloseable> void withResource(T resource, Consumer<T> action)
throws Exception {
try {
action.accept(resource); // Consumer<T> accepts T
} finally {
resource.close(); // close() available from AutoCloseable bound
}
}
withResource(new FileInputStream("file.txt"), stream -> {
// process stream — typed as FileInputStream, not just AutoCloseable
});
// ── Bound restricts valid type arguments at call sites ─────────────────
public class EventHandler<T extends Event> {
private final List<Consumer<T>> listeners = new ArrayList<>();
public void addListener(Consumer<T> listener) { listeners.add(listener); }
public void fire(T event) {
event.timestamp(); // Event method available from bound
listeners.forEach(l -> l.accept(event));
}
}
EventHandler<MouseEvent> mouseHandler = new EventHandler<>();
// EventHandler<String> bad = new EventHandler<>(); // COMPILE ERROR — String not an EventUpper Bound on Wildcards — ? extends T and Covariance
// ── ? extends T introduces covariance ────────────────────────────────
List<Integer> integers = List.of(1, 2, 3);
List<Double> doubles = List.of(1.1, 2.2, 3.3);
List<Number> numbers = List.of(10, 20.0, 30L);
// Without wildcard — invariant, none of these work:
// List<Number> n1 = integers; // COMPILE ERROR
// List<Number> n2 = doubles; // COMPILE ERROR
// With upper-bounded wildcard — covariant, all work:
List<? extends Number> w1 = integers; // OK
List<? extends Number> w2 = doubles; // OK
List<? extends Number> w3 = numbers; // OK
// ── Reading from ? extends is typed ──────────────────────────────────
public static void analyzeNumbers(List<? extends Number> list) {
double sum = 0, min = Double.MAX_VALUE, max = Double.MIN_VALUE;
for (Number n : list) { // n is Number — all Number methods available
double d = n.doubleValue(); // from Number
sum += d;
if (d < min) min = d;
if (d > max) max = d;
}
System.out.printf("sum=%.1f min=%.1f max=%.1f%n", sum, min, max);
}
analyzeNumbers(integers); // sum=6.0 min=1.0 max=3.0
analyzeNumbers(doubles); // sum=6.6 min=1.1 max=3.3
// ── Writing to ? extends is forbidden ────────────────────────────────
List<? extends Number> mixed = new ArrayList<>(integers);
// mixed.add(42); // COMPILE ERROR — might be List<Double>
// mixed.add(3.14); // COMPILE ERROR — might be List<Integer>
// mixed.add(null); // OK — null is always safe
// ── Subtype relationships ──────────────────────────────────────────────
// If B extends A: List<B> is subtype of List<? extends A>
List<Integer> listInt = List.of(1, 2, 3);
List<? extends Number> asNumber = listInt; // Integer extends Number ✓
List<? extends Object> asObject = listInt; // Integer extends Object ✓
List<? extends Integer> asInteger = listInt; // Integer extends Integer ✓
// List<? extends B> is subtype of List<? extends A> when B extends A:
List<? extends Integer> wildcardInt = List.of(1, 2, 3);
List<? extends Number> wildcardNum = wildcardInt; // OK — transitivity ✓
// ── Upper-bounded wildcard in method parameters ───────────────────────
// Single method serves List<Integer>, List<Double>, List<BigDecimal>, etc.:
public static double average(List<? extends Number> list) {
if (list.isEmpty()) return 0;
return list.stream()
.mapToDouble(Number::doubleValue)
.average()
.orElse(0);
}
System.out.println(average(List.of(1, 2, 3, 4, 5))); // 3.0
System.out.println(average(List.of(1.5, 2.5, 3.5))); // 2.5
System.out.println(average(List.of(100L, 200L, 300L))); // 200.0
// ── When ? extends T vs <T extends UpperBound> ───────────────────────
// USE ? extends when the type name isn't needed in the method body or return:
public static double sum(List<? extends Number> list) { // type name not needed
return list.stream().mapToDouble(Number::doubleValue).sum();
}
// USE <T extends> when the type name appears in multiple positions:
public static <T extends Comparable<T>> T max(List<T> list) { // T in return type
return list.stream().max(Comparator.naturalOrder()).orElseThrow();
}
// If this were written with ?, the return type would be ? extends Comparable<?> — unusable.Upper Bounds and Type Erasure — What T Erases To
// ── Erasure of T extends Number → Number in bytecode ────────────────
public class NumberBox<T extends Number> {
private T value;
public NumberBox(T v) { this.value = v; }
public T get() { return value; } // bytecode: Number get()
}
// At call site with T = Integer:
NumberBox<Integer> box = new NumberBox<>(42);
Integer result = box.get();
// Bytecode generated by compiler (conceptually):
// Number _temp = box.get(); // return type is Number (the erasure)
// Integer result = (Integer) _temp; // compiler inserts checkcast Integer
// ── Multiple bounds — leftmost is the erasure ─────────────────────────
public class SortableNumber<T extends Number & Comparable<T>> {
private T value;
public SortableNumber(T v) { this.value = v; }
// Both bounds in play:
public double asDouble() { return value.doubleValue(); } // Number method
public int compareTo(T other) { return value.compareTo(other); } // Comparable method
// After erasure, value is of type Number (leftmost bound)
// Calls to compareTo(other) require a synthetic cast to Comparable in bytecode
// — the compiler generates this automatically
}
// ── Erasure controls what instanceof can check ────────────────────────
NumberBox<Integer> intBox = new NumberBox<>(42);
NumberBox<Double> dblBox = new NumberBox<>(3.14);
// Can check against the erased type (Number):
System.out.println(intBox.get() instanceof Number); // true — Number is the erased type
// Cannot check parameterization at runtime — the JVM doesn't know T:
// if (intBox instanceof NumberBox<Integer>) { } // COMPILE ERROR
// Can check raw type:
System.out.println(intBox instanceof NumberBox); // true — raw type check
// ── Why unchecked casts warn ──────────────────────────────────────────
Object obj = List.of(1, 2, 3);
// The JVM can only verify that obj is a List — not that it's List<Integer>:
@SuppressWarnings("unchecked")
List<Integer> checkedList = (List<Integer>) obj; // unchecked: Integer not verifiable
// At runtime: checks instanceof List only — Integer part is invisible to JVM
// ── Erasure in practice: bridge methods from upper bounds ─────────────
public interface Processor<T extends Serializable> {
T process(T input); // erases to: Serializable process(Serializable input)
}
class StringProcessor implements Processor<String> {
@Override
public String process(String input) { // String process(String input)
return input.toUpperCase();
}
// Compiler generates bridge:
// (synthetic) Serializable process(Serializable input) {
// return this.process((String) input); // delegates with cast
// }
}
// Polymorphism works correctly through the erased interface:
Processor<String> p = new StringProcessor();
Serializable s = p.process("hello"); // calls bridge → calls real method
System.out.println(s); // HELLO
// ── Performance: leftmost bound minimizes synthetic casts ─────────────
// Prefer this: Number is primary use, Comparable is secondary:
class OptimizedBox<T extends Number & Comparable<T>> {
T value;
void primary() { value.doubleValue(); } // direct — Number is erasure
void secondary() { value.compareTo(value); } // synthetic cast to Comparable in bytecode
}
// Over this: Comparable is accessed directly, Number needs a cast:
class LessOptimized<T extends Comparable<T> & Number> {
T value;
void primary() { value.compareTo(value); } // direct — Comparable is erasure
void secondary() { value.doubleValue(); } // synthetic cast to Number in bytecode
}