☕ Java

Wildcards

Wildcards — written as ? — represent an unknown type in a generic type argument position. Where a type parameter T is a name given to an unknown type that can be referenced throughout a declaration, a wildcard ? is an anonymous unknown type used at the point of consumption. Wildcards appear in method parameter types, field types, return types, and variable declarations — anywhere a parameterized type appears as a type (not where a type is being declared). The three wildcard forms are: the unbounded wildcard (List<?>), the upper-bounded wildcard (List<? extends Number>), and the lower-bounded wildcard (List<? super Integer>). Wildcards are the mechanism that introduces covariance and contravariance into Java's otherwise invariant generic type system. This entry covers all three forms in depth, the producer-extends consumer-super (PECS) principle, wildcard capture and the capture helper pattern, nested wildcards, and when to use wildcards vs type parameters.

Unbounded Wildcard — List<?> and What It Means

The unbounded wildcard List<?> means "a List of some unknown type." The type argument is unknown but real — at runtime, the list always has a specific element type, but the code using List<?> does not know what it is. This is different from List<Object>, which means "a list that can hold any Object." A List<String> is a subtype of List<?> but is not a subtype of List<Object>. This is the covariance that wildcards introduce: List<?> accepts List<String>, List<Integer>, and List<anything>. The tradeoff of the unknown type is that you cannot write typed elements into a List<?>. The compiler rejects list.add("hello") because ? might be List<Integer>, and a String would violate the element type. The only value you can add to a List<?> is null (the null literal has no type, so it is compatible with any reference type). Reading from List<?> is safe but only as Object — list.get(0) returns Object. You can pass a List<?> to any method that accepts List<? extends Object>, which is everything. Unbounded wildcards are appropriate in three scenarios: when the method only uses Object's methods on the elements (size(), contains() with Object parameter, clear(), iterator() for traversal without type-specific operations); when the code works with the list itself rather than its elements (checking if it is empty, printing its size, returning a subList); or when the API deliberately wants to accept any parameterization and the type does not matter (a utility method that counts elements matching a predicate takes Collection<?> if the predicate is Predicate<Object>). The contrast with a raw type List is crucial. List (raw) also accepts any list, but it disables all generic type checking — the compiler emits unchecked warnings and treats the list's element type as Object without enforcement. List<?> retains type safety: the compiler knows the list has a specific element type, just an unknown one, and it enforces the restrictions (no typed adds) accordingly. Always prefer List<?> over the raw List.
Java
// ── Unbounded wildcard — accepts any List ────────────────────────────
public static void printList(List<?> list) {
    for (Object element : list) {        // element is Object — unknown type
        System.out.println(element);
    }
}

printList(List.of("alpha", "beta", "gamma"));  // OK — List<String>
printList(List.of(1, 2, 3, 4, 5));             // OK — List<Integer>
printList(List.of(3.14, 2.71));                // OK — List<Double>

// ── Cannot add typed elements to List<?> ─────────────────────────────
List<?> unknown = new ArrayList<>(List.of("a", "b", "c"));
// unknown.add("hello");   // COMPILE ERROR — ? might be List<Integer>
// unknown.add(42);        // COMPILE ERROR
unknown.add(null);         // OK — null is always compatible

// Reading returns Object only:
Object first = unknown.get(0);   // OK — Object
// String s = unknown.get(0);   // COMPILE ERROR — get() returns Object, not String

// ── List<?> vs List<Object> — the critical difference ─────────────────
List<String>  strings = List.of("a", "b");
List<Object>  objects = List.of("a", 42);   // List<Object> can hold mixed types

// List<String> IS a subtype of List<?>:
List<?> wild = strings;    // OK — covariance via wildcard

// List<String> is NOT a subtype of List<Object>:
// List<Object> objs = strings;  // COMPILE ERROR — generics are invariant

// List<Object> accepts ONLY List<Object> or raw List:
// List<Object> objs2 = List.of("a", "b");  // COMPILE ERROR

// ── Unbounded wildcard is appropriate when you only need List operations:
public static int countNonNull(List<?> list) {
    int count = 0;
    for (Object element : list) {
        if (element != null) count++;
    }
    return count;
}

public static boolean hasMoreThan(List<?> list, int n) {
    return list.size() > n;     // .size() doesn't care about element type
}

public static void swap(List<?> list, int i, int j) {
    // Cannot call list.set(i, list.get(j)) — get() returns Object, set() needs ?
    // Must use capture helper (see wildcard capture section):
    swapHelper(list, i, j);
}

// Capture helper resolves the unknown type to a named parameter:
private static <T> void swapHelper(List<T> list, int i, int j) {
    T temp = list.get(i);
    list.set(i, list.get(j));
    list.set(j, temp);
}

List<String> data = new ArrayList<>(List.of("a", "b", "c", "d"));
swap(data, 0, 3);
System.out.println(data);  // [d, b, c, a]

// ── Raw type vs wildcard — always prefer wildcard ─────────────────────
List raw = new ArrayList();      // raw — unchecked, unsafe
raw.add("hello");
raw.add(42);                     // no compile error — all type checking disabled

List<?> wild2 = new ArrayList<>(); // wildcard — type-safe
// wild2.add("hello");            // COMPILE ERROR — type checking enforced

Upper-Bounded Wildcard — ? extends T

The upper-bounded wildcard List<? extends Number> means "a List of some specific type that is Number or a subtype of Number." The exact type is unknown but is guaranteed to be within the Number hierarchy. This makes List<? extends Number> a covariant type: List<Integer>, List<Double>, List<Float>, and List<Number> itself are all subtypes of List<? extends Number>. The implications for reading and writing are the same as for the unbounded wildcard, but with a tighter bound. Reading from List<? extends Number> returns Number — not just Object. You can call any Number method on the retrieved element: doubleValue(), intValue(), longValue(). Writing is still forbidden: you cannot add a Number to List<? extends Number> because the actual runtime type might be List<Integer>, and a Number that is actually a Double would be a type violation. The mental model: a List<? extends Number> is a producer of Number values. It can produce (provide) elements of type Number for you to consume, but you cannot push values into it because you don't know its exact element type. Upper-bounded wildcards are appropriate when a method only reads from a collection: methods that compute sums, averages, maxima, or any aggregate of elements; methods that iterate and process elements without adding; methods that copy from a source collection. The covariance introduced by ? extends T solves a fundamental problem. Without wildcards, a method summing a list of numbers would need separate overloads for List<Integer>, List<Double>, and List<Float>, or would have to accept List<Number> (which only accepts literal List<Number>, not List<Integer>). With List<? extends Number>, a single method accepts all of them.
Java
// ── Upper-bounded wildcard — reading is typed, writing is forbidden ────
public static double sumList(List<? extends Number> list) {
    double sum = 0;
    for (Number element : list) {        // get() returns Number — not just Object
        sum += element.doubleValue();    // Number methods available
    }
    return sum;
}

// All three work — covariance via ? extends:
System.out.println(sumList(List.of(1, 2, 3, 4, 5)));          // 15.0 (List<Integer>)
System.out.println(sumList(List.of(1.1, 2.2, 3.3)));          // 6.6  (List<Double>)
System.out.println(sumList(List.of(1L, 2L, 3L)));             // 6.0  (List<Long>)
System.out.println(sumList(List.<Number>of(1, 2.0, 3L)));     // 6.0  (List<Number>)

// WITHOUT wildcard — only List<Number> accepted:
public static double sumListNoWildcard(List<Number> list) { /* ... */ }
// sumListNoWildcard(List.of(1, 2, 3));  // COMPILE ERROR — List<Integer> ≠ List<Number>

// ── Cannot add to ? extends ───────────────────────────────────────────
List<? extends Number> numbers = new ArrayList<>(List.of(1, 2, 3));
// numbers.add(42);      // COMPILE ERROR — might be List<Double>
// numbers.add(3.14);    // COMPILE ERROR — might be List<Integer>
numbers.add(null);       // OK — null is always safe

// Reading is fine and typed:
Number first = numbers.get(0);     // Number — can call any Number method
double d = first.doubleValue();

// ── Covariance in practice ────────────────────────────────────────────
List<Integer> ints    = List.of(10, 20, 30);
List<Double>  doubles = List.of(1.1, 2.2, 3.3);

// Both assignable to List<? extends Number>:
List<? extends Number> n1 = ints;     // OK
List<? extends Number> n2 = doubles;  // OK

// ── Upper bound in method parameters ─────────────────────────────────
public static Number findMax(List<? extends Number> list) {
    if (list.isEmpty()) throw new NoSuchElementException();
    Number max = list.get(0);
    for (Number n : list) {
        if (n.doubleValue() > max.doubleValue()) max = n;
    }
    return max;
}

System.out.println(findMax(List.of(3, 1, 4, 1, 5)));  // 5 (Integer)
System.out.println(findMax(List.of(1.1, 9.9, 2.2)));  // 9.9 (Double)

// ── Nested upper-bounded wildcard ────────────────────────────────────
// List of lists of numbers — each inner list can be any number subtype:
public static void printNestedSums(List<? extends List<? extends Number>> matrix) {
    for (List<? extends Number> row : matrix) {
        System.out.println(sumList(row));  // reuse sumList defined above
    }
}

List<List<Integer>> intMatrix = List.of(
    List.of(1, 2, 3),
    List.of(4, 5, 6));
printNestedSums(intMatrix);   // 6.0 / 15.0

// ── Upper bound with generic method type parameter ────────────────────
// Sometimes a generic method is cleaner than a wildcard:
// Wildcard version — cannot refer to element type in return:
public static void copyAll(List<? extends Number> src, List<Number> dest) {
    dest.addAll(src);
}

// Type parameter version — can express relationship between src and dest:
public static <T extends Number> void copyTyped(List<T> src, List<? super T> dest) {
    dest.addAll(src);   // PECS: src produces T, dest consumes T
}

Lower-Bounded Wildcard, PECS, and Wildcard Capture

The lower-bounded wildcard List<? super Integer> means "a List of some specific type that is Integer or a supertype of Integer." The actual type might be List<Integer>, List<Number>, or List<Object>. This makes the type contravariant: a List<Object> is a subtype of List<? super Integer>, even though Object is a supertype of Integer. The implications for reading and writing are the inverse of upper-bounded wildcards. Writing into List<? super Integer> is safe: whatever the actual type is, it is Integer or a supertype, so adding an Integer (or an Integer subtype) is always valid. Reading is restricted: you can only read Object, because the actual type might be List<Object> — the most you can guarantee is that what comes out is an Object. The mental model: a List<? super Integer> is a consumer of Integer values. It can consume (accept) Integer values you push into it, but reading from it only gives you Object. Lower-bounded wildcards appear in method parameters that write into a collection: sorting comparators, accumulation targets, output parameters. PECS (Producer Extends, Consumer Super) is the mnemonic for choosing between the two: if the parameterized type produces T values that your code reads, use ? extends T. If it consumes T values that your code writes, use ? super T. The canonical example is the signature of Collections.copy: static <T> void copy(List<? super T> dest, List<? extends T> src) — dest consumes T (super), src produces T (extends). Wildcard capture is a pattern used when you need to call a method on a List<?> that requires referring to the list's element type by name. The compiler calls this "capture" — it gives the wildcard type a synthetic name like CAP#1 internally. You can expose this by writing a private generic helper method: the wildcard List<?> is passed to a private <T> method, where the wildcard's type is captured as T, making it possible to read and set the same position's element.
Java
// ── Lower-bounded wildcard — writing is typed, reading is Object ───────
public static void fillWithIntegers(List<? super Integer> list, int count) {
    for (int i = 0; i < count; i++) {
        list.add(i);     // OK — Integer fits in ? super Integer
    }
}

List<Integer> intList  = new ArrayList<>();
List<Number>  numList  = new ArrayList<>();
List<Object>  objList  = new ArrayList<>();

fillWithIntegers(intList, 3);   // List<Integer> — Integer supertype of itself
fillWithIntegers(numList, 3);   // List<Number>  — Number is supertype of Integer
fillWithIntegers(objList, 3);   // List<Object>  — Object is supertype of Integer

System.out.println(intList);   // [0, 1, 2]
System.out.println(numList);   // [0, 1, 2]
System.out.println(objList);   // [0, 1, 2]

// COMPILE ERROR — List<Double> is not ? super Integer:
// fillWithIntegers(new ArrayList<Double>(), 3);

// Reading returns only Object:
List<? super Integer> consumer = numList;
Object o = consumer.get(0);   // only Object guaranteed
// Integer i = consumer.get(0);  // COMPILE ERROR — might be List<Number>

// ── PECS — Producer Extends, Consumer Super ───────────────────────────
// copy: src produces T, dest consumes T
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
    for (T element : src) {     // reading from src — extends (producer)
        dest.add(element);      // writing to dest  — super  (consumer)
    }
}

List<Integer> source      = List.of(1, 2, 3, 4, 5);
List<Number>  destination = new ArrayList<>();
copy(destination, source);       // T = Integer; dest=List<Number>, src=List<Integer>
System.out.println(destination); // [1, 2, 3, 4, 5]

// ── PECS with Comparator — consumer of T ─────────────────────────────
// Comparator<? super T>: the comparator consumes T values to compare them
public static <T> void sort(List<T> list, Comparator<? super T> comparator) {
    list.sort(comparator);
}

List<String> words = new ArrayList<>(List.of("banana", "apple", "cherry"));
// Comparator<Object> can sort a List<String> — Object is supertype of String:
sort(words, Comparator.comparingInt(Object::hashCode));

// Natural order comparator is Comparator<String> — exact match also works:
sort(words, Comparator.naturalOrder());
System.out.println(words);  // [apple, banana, cherry]

// ── Wildcard capture — helper method pattern ──────────────────────────
// Cannot swap elements of List<?> directly — get() returns Object, set() needs ?
public static void swap(List<?> list, int i, int j) {
    swapCapture(list, i, j);    // delegate to helper that captures the type
}

// The wildcard is captured as T — now we can read and write the same type:
private static <T> void swapCapture(List<T> list, int i, int j) {
    T temp = list.get(i);       // T known — safe read
    list.set(i, list.get(j));   // T known — safe write
    list.set(j, temp);          // T known — safe write
}

List<String> letters = new ArrayList<>(List.of("a", "b", "c", "d"));
swap(letters, 0, 3);
System.out.println(letters);  // [d, b, c, a]

// ── When wildcard vs type parameter ──────────────────────────────────
// USE wildcard when the type doesn't need to be referred to elsewhere:
public static void printSize(Collection<?> c) {
    System.out.println(c.size());     // no need to name the element type
}

// USE type parameter when the type must be consistent across multiple uses:
public static <T> T getFirst(List<T> list, T defaultValue) {
    return list.isEmpty() ? defaultValue : list.get(0);
    // T must be the same type for both list's element and defaultValue
}

// USE type parameter when the return type must relate to the parameter:
public static <T> List<T> repeat(T value, int times) {
    // List<T> returned — caller gets back the same type they provided
    List<T> result = new ArrayList<>(times);
    for (int i = 0; i < times; i++) result.add(value);
    return result;
}

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.