☕ Java

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

A lower-bounded wildcard ? super T is placed in a type argument position and means: the actual type argument is T or any supertype of T. When you write List<? super Integer>, the compiler accepts List<Integer>, List<Number>, List<Object>, and List<Serializable> (since Integer implements Serializable) — any type in the ancestry chain of Integer. List<Double> is not acceptable because Double is not a supertype of Integer. The implication for writing: if the list holds ? super Integer, then whatever the actual type is, it can hold an Integer. Integer fits into a List<Integer> trivially; Integer fits into a List<Number> because Integer is a Number; Integer fits into a List<Object> because Integer is an Object. Adding an Integer to any List<? super Integer> is always type-safe. You can also add any subtype of Integer (Integer has no subtypes since it's final, but if T were an abstract class with subclasses, subtypes would also be addable). You cannot add arbitrary values: you cannot add a Double to List<? super Integer>, because the actual type might be List<Integer> and Double is not an Integer. The implication for reading: a List<? super Integer> might be a List<Number>, a List<Object>, or anything in between. The only type guaranteed to be able to hold any element that comes out is Object. Therefore get() on a List<? super Integer> returns Object and nothing more specific. This is the tradeoff: lower-bounded wildcards make writing safe at the cost of typed reading. Contravariance is the mathematical term for this relationship: while Integer is a subtype of Number, List<? super Number> is a subtype of List<? super Integer>. The relationship is inverted. This is the opposite of what upper-bounded wildcards provide (covariance: Integer is a subtype of Number, so List<? extends Integer> is a subtype of List<? extends Number>). Together, upper and lower bounds give Java generics both covariance and contravariance, even though the base generic types (without wildcards) are invariant.
Java
// ── 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

The consumer super half of PECS (Producer Extends, Consumer Super) is best understood through the Comparator<? super T> pattern, which appears throughout the Java standard library in sorting methods, stream operations, and collection APIs. Collections.sort(List<T> list, Comparator<? super T> c) uses ? super T for the Comparator because the comparator is a consumer of T values — it takes T values as input to its compare(T o1, T o2) method. If you have a List<Integer> and a Comparator<Number> that compares by absolute value, the Comparator<Number> can compare Integer values (because Integer extends Number), so it should be usable to sort a List<Integer>. Without ? super T, you could only sort List<Integer> with Comparator<Integer>, not with the more general Comparator<Number>. The pattern generalizes: any time a parameter "consumes" T values (calls methods that accept T as an argument), it should be parameterized with ? super T. This includes: Comparator<? super T> for sorting, Consumer<? super T> for forEach and stream operations, Predicate<? super T> for filtering (the predicate tests T values), and Function<? super T, R> for the input side of mappings. The destination of a copy operation is another classic consumer: static <T> void copy(List<? super T> dest, List<? extends T> src). The dest list consumes T values by accepting them through add(), so it is ? super T. The src list produces T values by providing them through get(), so it is ? extends T. These two forms are the two halves of PECS applied to the same T in one signature. A less obvious consumer pattern: output collection parameters. When a method accumulates results into a caller-provided collection, the collection is a consumer: public static <T> void filter(Collection<T> source, Predicate<? super T> predicate, Collection<? super T> output). The output collection uses ? super T because it only needs to accept T values through add() — it doesn't need to read back typed T values.
Java
// ── 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

The subtyping rules for lower-bounded wildcards are the formal foundation of contravariance in Java generics. Given types A and B where B extends A (B is a subtype of A), the following subtyping relationships hold: List<? super A> is a subtype of List<? super B>. This is the inversion: in the hierarchy of types, A is higher (more general) than B, but in the hierarchy of lower-bounded wildcards, ? super A is more specific (a subtype of) ? super B. Additionally, List<A> is a subtype of List<? super A> (a list of exactly A satisfies "a list of some type that is A or a supertype of A"), and List<? super A> is a subtype of List<?> (the unbounded wildcard accepts any parameterization). These relationships allow lower-bounded wildcards to compose with upper-bounded wildcards and unbounded wildcards in larger type expressions. Lower bounds cannot be applied to type parameters — Java has no syntax for <T super SomeType>. This is a deliberate design decision. Lower bounds on type parameters would introduce significant complexity in the type inference algorithm and in the type system's coherence properties. The practical need for lower bounds is almost entirely satisfied by lower-bounded wildcards on use sites. The rare case where you want to say "the returned type must be some supertype of Integer" is handled instead by returning the specific type you need and letting the caller assign to whatever variable they have. Choosing between ? super T and a type parameter: use ? super T when you only need to write T values into the parameter and don't need to refer to the type by name elsewhere. Use a type parameter (class or method level) when you need to express a relationship between multiple parameters or between a parameter and the return type. A good heuristic: if you find yourself writing an explicit cast from Object after reading from a ? super T collection, you probably should have used a type parameter or rethought the API.
Java
// ── 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

Related Topics in Generics

Generic Class
A generic class in Java is a class parameterized by one or more type parameters, written as class Box<T> or class Pair<K, V>. Type parameters allow a single class definition to work correctly with many different types while preserving full compile-time type safety — eliminating the need for casts, preventing ClassCastException at runtime, and making the intended type contract explicit in the API. The compiler erases type parameters at bytecode level (type erasure), replacing them with their bounds or Object, but all type checking occurs at compile time before erasure. This entry covers type parameter declaration and naming conventions, bounded type parameters (extends, super), multiple type parameters, raw types and why they are unsafe, type erasure and its consequences, wildcards (?, ? extends T, ? super T), the PECS principle, recursive type bounds, generic interfaces, inheritance with generic classes, and how to design generic classes that work correctly across all parameterizations.
Generic Method
A generic method is a method that declares its own type parameters, independent of any type parameters on its enclosing class. The type parameters appear in angle brackets before the return type: public static <T> T identity(T value). Generic methods can appear in non-generic classes, in generic classes (adding their own type parameters beyond the class's), and as both static and instance methods. The compiler infers the type arguments for generic method calls from the argument types, eliminating the need for explicit type witness syntax in most cases. This entry covers type parameter declaration on methods, type inference rules and when inference fails, bounded type parameters on methods, returning computed type relationships, generic methods that operate on multiple type parameters, static generic utility methods (the standard library pattern), the interaction between method type parameters and class type parameters, explicit type witness syntax, and the difference between using a class type parameter vs a method type parameter.
Type Parameters
Type parameters are the placeholders — written as single uppercase letters like T, E, K, V — that make Java generics work. A type parameter declares a variable that stands in for a concrete type, to be supplied by the caller or inferred by the compiler. They appear on classes (class Box<T>), interfaces (interface Comparable<T>), and methods (public static <T> T identity(T value)), and they are the mechanism by which the Java type system achieves compile-time type safety without duplicating code for each concrete type. This entry covers where and how type parameters are declared, the naming conventions and why they exist, the scope rules for type parameters (where they are in scope and where they are not), how type parameters interact with inheritance, how the compiler resolves a type parameter to a concrete type at each call site, and the distinction between a type parameter (a declaration) and a type argument (a concrete substitution).
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.