Lower Bound
A lower bound in Java generics restricts a wildcard to a specific type and all its supertypes. It is written as ? super T, meaning 'some type that is T or any ancestor of T in the type hierarchy.' Lower bounds can only be applied to wildcards, never to type parameters — there is no <T super SomeType> in Java. A List<? super Integer> accepts List<Integer>, List<Number>, and List<Object>, but not List<Double> or List<String>. Lower bounds introduce contravariance: the type relationship is inverted compared to upper bounds. A method parameter of type List<? super Integer> accepts more supertypes, not subtypes, which makes the wildcard a safe target for writing Integer values into. This entry covers the mechanics of lower-bounded wildcards in full, the reading and writing rules, contravariance and its implications, PECS applied specifically to the consumer (super) half, the Comparator<? super T> pattern used throughout the standard library, combining lower bounds with type parameters, and the precise subtyping rules lower bounds create.
Lower-Bounded Wildcards — Writing and Contravariance
// ── Lower-bounded wildcard — which types are accepted ────────────────
// List<? super Integer> accepts:
List<Integer> li = new ArrayList<>(); // Integer super Integer ✓
List<Number> ln = new ArrayList<>(); // Number super Integer ✓
List<Object> lo = new ArrayList<>(); // Object super Integer ✓
List<Serializable> ls = new ArrayList<>(); // Serializable super Integer ✓
// Assign to List<? super Integer>:
List<? super Integer> w1 = li; // OK
List<? super Integer> w2 = ln; // OK
List<? super Integer> w3 = lo; // OK
List<? super Integer> w4 = ls; // OK
// NOT accepted:
// List<? super Integer> w5 = new ArrayList<Double>(); // Double is not super of Integer
// List<? super Integer> w6 = new ArrayList<String>(); // String is not super of Integer
// ── Writing to ? super Integer — always safe ──────────────────────────
public static void addIntegers(List<? super Integer> list) {
list.add(1); // Integer — always safe
list.add(42); // Integer — always safe
list.add(100); // Integer — always safe
// list.add(3.14); // COMPILE ERROR — Double not an Integer
// list.add("hello"); // COMPILE ERROR — String not an Integer
}
List<Integer> intList = new ArrayList<>();
List<Number> numList = new ArrayList<>();
List<Object> objList = new ArrayList<>();
addIntegers(intList);
addIntegers(numList);
addIntegers(objList);
System.out.println(intList); // [1, 42, 100]
System.out.println(numList); // [1, 42, 100]
System.out.println(objList); // [1, 42, 100]
// ── Reading from ? super Integer — only Object ────────────────────────
List<? super Integer> consumer = numList;
Object element = consumer.get(0); // only Object guaranteed
// Integer i = consumer.get(0); // COMPILE ERROR — might be List<Number>
// Number n = consumer.get(0); // COMPILE ERROR — might be List<Object>
System.out.println(element); // 1
// ── Contravariance — the subtype relationship is INVERTED ─────────────
// Upper bound (covariant): Integer <: Number → List<Integer> <: List<? extends Number>
// Lower bound (contravariant): Integer <: Number → List<? super Number> <: List<? super Integer>
List<? super Number> superNumber = new ArrayList<Object>();
List<? super Integer> superInteger = superNumber; // OK — contravariance ✓
// List<? super Integer> as = new ArrayList<Number>(); // ...
// List<? super Number> bn = as; // OK because ? super Number ⊆ ? super Integer
// Visual: the direction of assignability is REVERSED:
// With covariance: Integer → Number means List<Integer> → List<? extends Number>
// With contravariance: Integer → Number means List<? super Number> → List<? super Integer>PECS Applied — Comparator<? super T> and Consumer Patterns
// ── Comparator<? super T> — the canonical consumer pattern ──────────
// Without wildcard: only Comparator<Integer> sorts List<Integer>
List<Integer> ints = new ArrayList<>(List.of(3, -1, 4, -1, 5, -9));
Comparator<Number> byAbsValue = Comparator.comparingDouble(n -> Math.abs(n.doubleValue()));
Comparator<Object> byHashCode = Comparator.comparingInt(Object::hashCode);
Comparator<Integer> byValue = Comparator.naturalOrder();
// ? super Integer accepts Comparator<Integer>, Comparator<Number>, Comparator<Object>:
ints.sort(byAbsValue); // OK — Comparator<Number> is ? super Integer ✓
System.out.println(ints); // sorted by absolute value: [-1, -1, 3, 4, 5, -9]
ints.sort(byValue); // OK — Comparator<Integer> is ? super Integer ✓
System.out.println(ints); // [-9, -1, -1, 3, 4, 5]
// WITHOUT the ? super T, only Comparator<Integer> would work:
public static <T> void sortStrict(List<T> list, Comparator<T> comp) { list.sort(comp); }
// sortStrict(ints, byAbsValue); // COMPILE ERROR — Comparator<Number> ≠ Comparator<Integer>
// WITH ? super T, all are accepted:
public static <T> void sortFlexible(List<T> list, Comparator<? super T> comp) {
list.sort(comp);
}
sortFlexible(ints, byAbsValue); // OK
sortFlexible(ints, byValue); // OK
// ── Consumer<? super T> — the standard library functional pattern ─────
List<Integer> numbers = List.of(1, 2, 3, 4, 5);
Consumer<Object> printObject = System.out::println;
Consumer<Number> printDouble = n -> System.out.println(n.doubleValue());
Consumer<Integer> printTwice = n -> System.out.println(n + " " + n);
// Stream.forEach accepts Consumer<? super T>:
numbers.forEach(printObject); // OK — Consumer<Object> is ? super Integer
numbers.forEach(printDouble); // OK — Consumer<Number> is ? super Integer
numbers.forEach(printTwice); // OK — Consumer<Integer> is ? super Integer
// ── PECS full example — copy with both bounds ─────────────────────────
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
for (T element : src) { // src produces T (extends)
dest.add(element); // dest consumes T (super)
}
}
List<Integer> source = List.of(1, 2, 3, 4, 5);
List<Number> destination = new ArrayList<>();
copy(destination, source); // T = Integer; ? super Integer = Number ✓, ? extends Integer = Integer ✓
System.out.println(destination); // [1, 2, 3, 4, 5]
// Can also copy Integer list into Object list:
List<Object> objDest = new ArrayList<>();
copy(objDest, source);
System.out.println(objDest); // [1, 2, 3, 4, 5]
// ── Output collection parameter — consumer pattern ────────────────────
public static <T> void filterInto(
Iterable<? extends T> source,
Predicate<? super T> predicate, // consumes T for testing
Collection<? super T> output) { // consumes T for accumulation
for (T element : source) {
if (predicate.test(element)) {
output.add(element);
}
}
}
List<Integer> positives = new ArrayList<>();
List<Number> asNumbers = new ArrayList<>();
filterInto(List.of(-3, -1, 0, 2, 5), n -> n > 0, positives); // into List<Integer>
filterInto(List.of(-3, -1, 0, 2, 5), n -> n > 0, asNumbers); // into List<Number>
System.out.println(positives); // [2, 5]
System.out.println(asNumbers); // [2, 5]Lower Bound Subtyping Rules and When to Use ? super T
// ── Subtyping rules: List<? super A> is subtype of List<? super B> ─────
// Given: Integer extends Number extends Object
List<Object> listObj = new ArrayList<>();
List<Number> listNum = new ArrayList<>();
List<Integer> listInt = new ArrayList<>();
// Contravariant subtyping:
// List<? super Number> ⊆ List<? super Integer> (because Number is super of Integer)
List<? super Integer> w1 = listObj; // OK — Object super Integer ✓
List<? super Integer> w2 = listNum; // OK — Number super Integer ✓
List<? super Integer> w3 = listInt; // OK — Integer super Integer ✓
List<? super Number> w4 = listObj; // OK — Object super Number ✓
List<? super Number> w5 = listNum; // OK — Number super Number ✓
// List<? super Number> w6 = listInt; // COMPILE ERROR — Integer is NOT super of Number
// Subtype relationship between wildcard types:
// List<? super Number> is a subtype of List<? super Integer>:
List<? super Integer> fromWild = w4; // w4 is List<? super Number> — OK (contravariance) ✓
// ── Lower bound and unbounded wildcard ────────────────────────────────
// List<? super T> ⊆ List<?> for any T:
List<? super Integer> bounded = new ArrayList<Number>();
List<?> unbounded = bounded; // OK — any wildcard is subtype of List<?>
// ── Why <T super SomeType> doesn't exist ──────────────────────────────
// This syntax is illegal in Java — lower bounds only on wildcards:
// public class Accumulator<T super Integer> { } // COMPILE ERROR
// The equivalent is achieved with a wildcard parameter:
public class Accumulator<T> {
private final List<T> storage;
public Accumulator(List<T> storage) { this.storage = storage; }
// ? super T on the parameter achieves the lower-bound effect:
public void accumulate(Collection<? extends T> source) {
storage.addAll(source);
}
}
List<Number> nums = new ArrayList<>();
Accumulator<Number> acc = new Accumulator<>(nums);
acc.accumulate(List.of(1, 2, 3)); // List<Integer> is ? extends Number ✓
acc.accumulate(List.of(1.1, 2.2)); // List<Double> is ? extends Number ✓
System.out.println(nums); // [1, 2, 3, 1.1, 2.2]
// ── Decision guide: ? super T vs type parameter ──────────────────────
// USE ? super T when: only writing T values into the parameter, type name not needed again:
public static <T> void addDefault(List<? super T> list, T defaultValue, int count) {
for (int i = 0; i < count; i++) list.add(defaultValue);
}
// USE type parameter when: return type must match, or multiple params must be consistent:
public static <T> T getOrAdd(List<T> list, int index, T defaultValue) {
if (index < list.size()) return list.get(index);
list.add(defaultValue);
return defaultValue; // return type T matches parameter type — needs type param
}
// The anti-pattern: using ? super T and then casting the read back:
public static void badPattern(List<? super Integer> list) {
list.add(42);
Integer i = (Integer) list.get(0); // unchecked, ugly — signals wrong design
// If you need to read Integer back, use List<Integer>, not List<? super Integer>
}
// ── Practical summary: when each wildcard form is appropriate ─────────
// List<?> — only need to call Object methods or List methods (size, clear)
// List<? extends T> — reading T values out (producing T), never writing
// List<? super T> — writing T values in (consuming T), reading only as Object
// List<T> (type param) — both reading and writing, or type appears in return/other params