Wildcards
Use ? extends to read, ? super to write and ? for unknown types, so your methods accept every list they sensibly can.
double 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 ofNumber. You can read elements asNumber. You cannot add (exceptnull), because the compiler does not know which subtype the list holds.List<? super Integer>: a list ofIntegeror a supertype. You can addIntegervalues. Reading gives you onlyObject.List<?>: a list of something. You can ask its size, iterate it asObjectand 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
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.
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.IntegerProducer 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.
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.
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