Wildcards

Use ? extends to read, ? super to write and ? for unknown types, so your methods accept every list they sensibly can.

Syntaxdouble sum(List<? extends Number> values) // read from it void fill(List<? super Integer> target) // write into it int count(Collection<?> items) // do not care <T> void copy(List<? super T> dst, List<? extends T> src)

Generic types are invariant: a List<Integer> is not a List<Number>, even though an Integer is a Number. If it were, you could put a Double into the list through the List<Number> reference and break the List<Integer>. Arrays made exactly that mistake: Object[] a = new String[1] compiles, and storing an Integer fails at runtime with ArrayStoreException.

Invariance is safe but rigid. A method double sum(List<Number> values) refuses a List<Integer>. Wildcards loosen it.

The three forms

  • List<? extends Number>: a list of some subtype of Number. You can read elements as Number. You cannot add (except null), because the compiler does not know which subtype the list holds.
  • List<? super Integer>: a list of Integer or a supertype. You can add Integer values. Reading gives you only Object.
  • List<?>: a list of something. You can ask its size, iterate it as Object and remove, but not add.

PECS

The rule of thumb is Producer Extends, Consumer Super. A parameter the method reads from is a producer: use extends. A parameter it writes into is a consumer: use super. Collections.copy shows both in one signature:

static <T> void copy(List<? super T> dest, List<? extends T> src)

A parameter that is both read and written needs an exact type, with no wildcard.

List<?> is not List<Object>

A List<String> can be passed to a List<?> parameter but not to a List<Object> one. Use ? for code that does not care about the element type, such as logging a size or checking emptiness.

Where wildcards go

Put wildcards in method parameters, where they widen what callers can pass. Avoid them in return types: a method returning List<? extends Order> forces every caller to deal with the wildcard. Return the concrete List<Order>.

In a service this comes up when a method takes events, DTOs or numbers from different but related types: an outbox that accepts any List<? extends DomainEvent>, a metrics method that sums any Collection<? extends Number>, a helper that fills a caller-supplied List<? super Row>. The JDK uses the same pattern throughout: addAll(Collection<? extends E>), sort(Comparator<? super E>), Function<? super T, ? extends R>.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Accept any List<? extends DomainEvent> in an outbox publisher so each event type can be passed without copying.
  • Sum metrics from a Collection<? extends Number> whether the samples are Integer counts or Double timings.
  • Let a row mapper write results into a caller-supplied List<? super Row> so the caller chooses the list type.

More examples

Why List<Integer> is not a List<Number>

Generics reject the unsafe assignment at compile time, and ? extends gives a read-only view instead. Arrays allow the assignment and only fail when the wrong element is stored.

Example · java
import java.util.ArrayList;
import java.util.List;

void main() {
    List<Integer> ints = new ArrayList<>(List.of(1, 2));
    // List<Number> nums = ints;          // does not compile
    List<? extends Number> view = ints;    // fine: read as Number
    Number first = view.get(0);
    // view.add(3.5);                     // does not compile
    IO.println(first);

    Object[] array = new String[1];        // arrays are covariant
    try {
        array[0] = 42;
    } catch (ArrayStoreException e) {
        IO.println("ArrayStoreException: " + e.getMessage());
    }
}
// Output:
// 1
// ArrayStoreException: java.lang.Integer

Producer and consumer in one signature

The source only produces values, so it is ? extends T; the sink only consumes them, so it is ? super T. One method moves both kinds of event into a shared List<Event> outbox.

Example · java
import java.util.ArrayList;
import java.util.List;

interface Event {
    String name();
}

record OrderPlaced(String orderId) implements Event {
    public String name() { return "order.placed " + orderId; }
}

record UserSignedUp(String email) implements Event {
    public String name() { return "user.signed-up " + email; }
}

<T> void drainTo(List<? extends T> source, List<? super T> sink) {
    for (T item : source) {
        sink.add(item);
    }
}

void main() {
    List<OrderPlaced> orders = List.of(new OrderPlaced("o-1"), new OrderPlaced("o-2"));
    List<UserSignedUp> signups = List.of(new UserSignedUp("[email protected]"));

    List<Event> outbox = new ArrayList<>();
    drainTo(orders, outbox);
    drainTo(signups, outbox);

    for (Event e : outbox) {
        IO.println(e.name());
    }
}
// Output:
// order.placed o-1
// order.placed o-2
// user.signed-up [email protected]

The unbounded wildcard

List<?> accepts a list of any element type, which List<Object> would not. The price is that nothing except null can be added, because the compiler cannot prove the value fits.

Example · java
import java.util.ArrayList;
import java.util.List;

String describe(List<?> items) {
    String first = items.isEmpty() ? "none" : String.valueOf(items.get(0));
    return items.size() + " item(s), first: " + first;
}

void main() {
    IO.println(describe(List.of("a", "b")));
    IO.println(describe(List.of(42)));
    IO.println(describe(List.of()));

    List<?> unknown = new ArrayList<>(List.of("x"));
    // unknown.add("y");   // does not compile: the element type is unknown
    unknown.add(null);     // null is the only value that fits every type
    IO.println(unknown);
}
// Output:
// 2 item(s), first: a
// 1 item(s), first: 42
// 0 item(s), first: none
// [x, null]

Discussion

  • Be the first to comment on this lesson.