Java Wildcards in Generics

4 minute read

Wildcards (?) represent an unknown type in generics. They enable flexible method parameters that accept a wider range of generic types — something type parameters alone can’t always do.


The Problem: Generic Types Are Invariant

List<Integer> ints = List.of(1, 2, 3);
List<Number> nums = ints;  // ❌ Compile error!

Even though Integer extends Number, List<Integer> is NOT a subtype of List<Number>. This is invariance — and it exists to prevent:

// If this were allowed:
List<Number> nums = ints;
nums.add(3.14);  // adding Double to a List<Integer>! Type safety broken.

Wildcards solve this by expressing “I don’t know the exact type, but I know its bounds.”


Unbounded Wildcard: <?>

“I accept a list of anything”:

public static void printAll(List<?> list) {
    for (Object item : list) {
        System.out.println(item);
    }
}

printAll(List.of("A", "B"));     // ✅
printAll(List.of(1, 2, 3));      // ✅
printAll(List.of(true, false));   // ✅

List<?> vs List<Object>

Type Accepts Can Add
List<?> List<String>, List<Integer>, any List Only null
List<Object> Only List<Object> Any Object
List<String> strings = List.of("A");
List<?> wildcard = strings;        // ✅
List<Object> objects = strings;    // ❌ Compile error

Upper Bounded: ? extends T

“I accept T or any subtype of T” — used when you read from the collection:

public static double sum(List<? extends Number> numbers) {
    double total = 0;
    for (Number n : numbers) {
        total += n.doubleValue();
    }
    return total;
}

sum(List.of(1, 2, 3));         // List<Integer> ✅
sum(List.of(1.5, 2.5));        // List<Double> ✅
sum(List.of(1L, 2L));          // List<Long> ✅

Why You Can’t Add to ? extends

List<? extends Number> list = new ArrayList<Integer>();
// list.add(42);      // ❌ Compile error!
// list.add(3.14);    // ❌ Compile error!
list.add(null);       // ✅ null is the only safe addition

Number n = list.get(0);  // ✅ Reading is safe — we know it's at least Number

The compiler doesn’t know the actual type — it could be List<Integer>, List<Double>, or List<BigDecimal>. Adding anything would be unsafe.


Lower Bounded: ? super T

“I accept T or any supertype of T” — used when you write to the collection:

public static void addNumbers(List<? super Integer> list) {
    list.add(1);
    list.add(2);
    list.add(3);
}

List<Integer> ints = new ArrayList<>();
addNumbers(ints);        // ✅

List<Number> nums = new ArrayList<>();
addNumbers(nums);        // ✅

List<Object> objs = new ArrayList<>();
addNumbers(objs);        // ✅

// List<Double> doubles = new ArrayList<>();
// addNumbers(doubles);  // ❌ Double is not a supertype of Integer

Reading from ? super

List<? super Integer> list = new ArrayList<Number>();
list.add(42);            // ✅ Adding Integer is safe

Object item = list.get(0);  // ✅ But reading gives only Object
// Integer i = list.get(0);  // ❌ Can't assume it's Integer

PECS — Producer Extends, Consumer Super

The most important rule for wildcards:

Role Wildcard You Can
Producer (you read from it) ? extends T get() returns T
Consumer (you write to it) ? super T add(T) is safe
Both read and write Use exact type T Both operations

PECS in Action: Collections.copy()

public static <T> void copy(List<? super T> dest, List<? extends T> src) {
    for (int i = 0; i < src.size(); i++) {
        dest.set(i, src.get(i));
    }
}

List<Number> dest = new ArrayList<>(List.of(0, 0, 0));
List<Integer> src = List.of(1, 2, 3);
Collections.copy(dest, src);  // ✅ dest consumes, src produces

PECS Decision Guide

Do you GET items from the parameter?
└── Use <? extends T>  (producer)

Do you PUT items into the parameter?
└── Use <? super T>    (consumer)

Do you do BOTH?
└── Use <T>            (exact type)

Wildcard Capture

Sometimes the compiler creates an internal “capture” type for ?:

public static void swap(List<?> list, int i, int j) {
    // list.set(i, list.get(j));  // ❌ Compile error — can't put ? back
    swapHelper(list, i, j);       // ✅ Helper captures the type
}

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);
}

The helper method captures the wildcard type as a concrete T, enabling both reads and writes.


Practical Examples

Copying Any Subtype

public static <T> List<T> merge(List<? extends T> a, List<? extends T> b) {
    List<T> result = new ArrayList<>(a.size() + b.size());
    result.addAll(a);  // a produces T
    result.addAll(b);  // b produces T
    return result;
}

List<Number> merged = merge(List.of(1, 2), List.of(3.0, 4.0));
// [1, 2, 3.0, 4.0]

Finding Max with Upper Bound

public static <T extends Comparable<? super T>> T max(List<? extends T> list) {
    Iterator<? extends T> it = list.iterator();
    T max = it.next();
    while (it.hasNext()) {
        T item = it.next();
        if (item.compareTo(max) > 0) {
            max = item;
        }
    }
    return max;
}

The signature Comparable<? super T> means T is comparable to itself or any supertype — allowing classes like Integer (which implements Comparable<Integer>) to work even when the actual Comparable is defined on a parent class.


Summary Table

Wildcard Accepts Can Read As Can Write
<?> Any type Object Only null
<? extends Number> Number, Integer, Double… Number Only null
<? super Integer> Integer, Number, Object Object Integer (and subtypes)
<Number> (exact) Only Number Number Number

Common Mistakes

Mistake Problem Fix
List<Object> instead of List<?> Doesn’t accept List<String> Use <?> or <? extends T>
Adding to ? extends Compile error Use ? super for writing
Reading specific type from ? super Returns Object Use ? extends for reading
Overusing wildcards Code becomes unreadable Use exact types when possible

Key Takeaways

  1. ? = unknown type. <?> accepts any generic type.
  2. ? extends T = read-only (producer). Use when you get() from the collection.
  3. ? super T = write-only (consumer). Use when you add() to the collection.
  4. PECS: Producer Extends, Consumer Super — the golden rule.
  5. List<?> ≠ List<Object> — the former accepts any List<T>, the latter only List<Object>.
  6. Use wildcard capture helpers when you need both read and write with ?.

What’s Next?

Next: Collections Best Practices — choosing the right collection, decision flowcharts, and production-grade patterns.

Updated: