Wildcards
A wildcard, written ?, stands for "some unknown type" in a generic type. Wildcards let a method accept a family of parameterized types instead of exactly one: a List<Integer>, a List<Double>, and a List<Number> can all be passed where List<? extends Number> is expected. They exist because generic types are deliberately not covariant, and without wildcards many perfectly reasonable methods could not be written in a type-safe way.
The Problem: Generics Are Invariant
Integer is a subtype of Number, but List<Integer> is not a subtype of List<Number>:
static double sum(List<Number> numbers) {
double total = 0;
for (Number n : numbers) total += n.doubleValue();
return total;
}
List<Integer> ints = List.of(1, 2, 3);
sum(ints); // compile error: List<Integer> cannot be converted to List<Number>
This restriction is intentional. If a List<Integer> could be treated as a List<Number>, this would compile:
List<Integer> ints = new ArrayList<>(List.of(1, 2));
List<Number> nums = ints; // imagine this were allowed
nums.add(3.14); // a Double inside a List<Integer>!
int x = ints.get(2); // ClassCastException
Generics forbid the assignment so that this can never happen. Wildcards then allow the safe subset of such uses.
Upper-Bounded Wildcards: ? extends T
List<? extends Number> means "a list of some type that is Number or a subtype of it." Any of List<Number>, List<Integer>, List<Double>, or List<BigDecimal> matches.
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>: 6.0
sum(List.of(1.5, 2.5)); // List<Double>: 4.0
You can read, but not add
Elements read from a List<? extends Number> are guaranteed to be Numbers, so reading is safe. Adding is not:
List<? extends Number> nums = new ArrayList<Integer>();
Number first = nums.get(0); // OK: whatever it is, it is a Number
nums.add(42); // compile error
nums.add(3.14); // compile error
nums.add(null); // allowed, but rarely useful
The compiler knows the list holds some specific subtype of Number, but not which one. It might be a List<Double>, so adding an Integer could corrupt it. Therefore it allows no additions at all (except null).
Rule: use ? extends T for a parameter that the method only reads from, a "producer" of T values.
Lower-Bounded Wildcards: ? super T
List<? super Integer> means "a list of Integer or some supertype of it": List<Integer>, List<Number>, or List<Object>.
static void addDefaults(List<? super Integer> target) {
target.add(0);
target.add(100);
}
List<Integer> ints = new ArrayList<>();
List<Number> nums = new ArrayList<>();
List<Object> objs = new ArrayList<>();
addDefaults(ints); // OK
addDefaults(nums); // OK
addDefaults(objs); // OK
You can add, but reads are only Object
Whatever the actual list type is, it can hold an Integer, so adding Integers is safe. Reading gives only Object, because the list might be a List<Object> containing anything:
List<? super Integer> target = new ArrayList<Number>();
target.add(5); // OK
Object o = target.get(0); // OK, but only as Object
Integer i = target.get(0); // compile error
Rule: use ? super T for a parameter that the method only writes T values into, a "consumer" of T.
PECS: Producer Extends, Consumer Super
The mnemonic PECS summarizes the two rules:
| Parameter role | Wildcard | Can read as T | Can add T |
|---|---|---|---|
| Producer: the method takes values out of it | ? extends T | Yes | No |
| Consumer: the method puts values into it | ? super T | No (only Object) | Yes |
| Both | Exact type T | Yes | Yes |
The JDK applies PECS throughout. Collections.copy copies from a producer into a consumer:
public static <T> void copy(List<? super T> dest, List<? extends T> src)
List<Integer> src = List.of(1, 2, 3);
List<Number> dest = new ArrayList<>(List.of(0, 0, 0));
Collections.copy(dest, src); // T = Integer: read Integers from src, write them into dest
The same idea appears in functional parameters. Predicate<? super T> and Consumer<? super T> consume T values, and Function<? super T, ? extends R> consumes a T and produces an R:
List<Integer> scores = List.of(45, 82, 67);
Predicate<Object> notNull = o -> o != null;
scores.stream().filter(notNull).count(); // filter takes Predicate<? super Integer>
Consumer<Object> logger = o -> System.out.println("log: " + o);
scores.forEach(logger); // forEach takes Consumer<? super Integer>
A general-purpose Predicate<Object> or Consumer<Object> can be reused for any element type, because a function that accepts any Object certainly accepts an Integer.
Unbounded Wildcards: ?
List<?> means "a list of some unknown type." It is useful when the method does not care about the element type at all, only about the list's structure:
static void printAll(List<?> items) {
System.out.println(items.size() + " item(s): " + items);
}
printAll(List.of(1, 2));
printAll(List.of("a", "b"));
Elements can be read only as Object, and nothing except null can be added. Typical uses are methods that rely only on Object methods (size, isEmpty, toString, equals), or on Class<?> when the exact class is unknown.
List<?> versus List
These are not the same:
List<Object> | List<?> | |
|---|---|---|
Accepts a List<String> argument | No | Yes |
| Can add elements | Yes (any object) | No (only null) |
| Meaning | A list that may hold any mix of objects | A list of one specific, unknown type |
A List<Object> parameter accepts only an actual List<Object>. A List<?> parameter accepts any list.
Wildcards Versus Type Parameters
Many signatures can be written either way:
static void printAll(List<?> items) // wildcard
static <T> void printAll(List<T> items) // type parameter
Prefer a wildcard when the type is used in only one place and the method does not need to refer to it. Use a type parameter when the same type must appear in several places, linking them together:
static <T> void swapFirstTwo(List<T> list) { // needs T: reads one element and writes it back
T tmp = list.get(0);
list.set(0, list.get(1));
list.set(1, tmp);
}
With List<?>, the set calls would not compile, because the compiler could not prove the values being written match the list's unknown type.
Where wildcards are allowed:
- In parameter types, field types, local variable types, and return types (though wildcards in return types are usually awkward for callers).
- Not in
newexpressions (new ArrayList<?>()is illegal), in a class'sextends/implementsclause, or as an explicit type argument to a generic method call.
Practical Example: Processing Payments
A payments module has several payment types. Reporting code should work with a list of any one of them, and routing code should move matching payments into any suitable collection.
import java.util.*;
import java.util.function.Consumer;
import java.util.function.Predicate;
public class Wild {
static abstract class Payment {
final double amount;
Payment(double amount) { this.amount = amount; }
abstract String kind();
@Override public String toString() { return kind() + " " + amount; }
}
static class CardPayment extends Payment {
CardPayment(double a) { super(a); }
String kind() { return "card"; }
}
static class UpiPayment extends Payment {
UpiPayment(double a) { super(a); }
String kind() { return "upi"; }
}
static double total(Collection<? extends Payment> payments) {
double sum = 0;
for (Payment p : payments) sum += p.amount;
return sum;
}
static <T> void moveMatching(Collection<? extends T> source, Predicate<? super T> test,
Collection<? super T> target) {
for (T item : source) {
if (test.test(item)) target.add(item);
}
}
static void printAll(List<?> items) {
System.out.println(items.size() + " item(s): " + items);
}
public static void main(String[] args) {
List<CardPayment> cards = List.of(new CardPayment(1200), new CardPayment(300));
List<UpiPayment> upi = List.of(new UpiPayment(450), new UpiPayment(2500));
System.out.println("Card total: " + total(cards));
System.out.println("UPI total: " + total(upi));
List<Payment> review = new ArrayList<>();
Predicate<Payment> large = p -> p.amount >= 1000;
moveMatching(cards, large, review);
moveMatching(upi, large, review);
printAll(review);
List<Object> audit = new ArrayList<>();
moveMatching(upi, p -> true, audit);
printAll(audit);
Consumer<Object> logger = o -> System.out.println("log: " + o);
cards.forEach(logger);
}
}
Output:
Card total: 1500.0
UPI total: 2950.0
2 item(s): [card 1200.0, upi 2500.0]
2 item(s): [upi 450.0, upi 2500.0]
log: card 1200.0
log: card 300.0
Each wildcard matches the parameter's role:
totalreads only. It takesCollection<? extends Payment>, so aList<CardPayment>and aList<UpiPayment>are both accepted. WithCollection<Payment>, neither call would compile.moveMatchingfollows PECS. The source is a producer (? extends T), and the target is a consumer (? super T). The predicate also consumes values (? super T), which is why a singlePredicate<Payment>filters both cards and UPI payments.- The target can be any suitable collection. Matching payments go into a
List<Payment>for review or aList<Object>for an audit log. printAllusesList<?>. It only needssize()andtoString(), so it accepts any list.forEachtakesConsumer<? super CardPayment>. That is why a generalConsumer<Object>logger works for card payments.