Why wildcards exist
Generics give you compile-time type safety, but they come with a surprise that trips up
almost everyone: List<Integer> is not a List<Number>. Generics are invariant,
and that single fact is the reason wildcards exist. A method that wants to work over a
family of related parameterizations — "any list of numbers," "any collection I can pour
integers into" — needs a way to express that without sacrificing safety. Wildcards (?)
and bounded type parameters (<T extends ...>) are the two tools for the job, and knowing
which to reach for is most of what generics interviews are really testing.
The unbounded wildcard
The simplest wildcard is <?>, read as "a list of some unknown type." You use it when
the method body genuinely doesn't care about the element type — it treats everything as
Object. It is the type-safe replacement for a raw type: a raw List disables type
checking entirely, while List<?> keeps the compiler honest.
static void printAll(List<?> list) { // any element type
for (Object o : list) System.out.println(o); // safe: everything is an Object
// list.add("x"); // compile error — unknown element type
}
printAll(List.of(1, 2, 3)); // List<Integer> ok
printAll(List.of("a", "b")); // List<String> ok
Because the element type is hidden, an unbounded wildcard list is effectively
read-only: you can read elements as Object, but the only thing you can add is
null. Don't confuse List<?> with List<Object> — the latter is a single concrete
type that matches only List<Object>, whereas List<?> matches every parameterization.
Upper bounds: producers
An upper-bounded wildcard <? extends T> means "some unknown subtype of T." It
shines when you want to read T values out of a structure. Every element is guaranteed
to be at least a T, so reading as T is always safe — this is the producer side.
static double sum(List<? extends Number> nums) { // Number or any subtype
double total = 0;
for (Number n : nums) total += n.doubleValue(); // safe: all are Numbers
return total;
}
sum(List.of(1, 2, 3)); // List<Integer> ok
sum(List.of(1.5, 2.5)); // List<Double> ok
What you can't do is add to a List<? extends Number>. The compiler knows the list
is some subtype of Number but not which one — it could really be a List<Integer> or
a List<Double> — so no concrete value is provably safe to insert. The list reads, it does
not write. The rule: extends is for getting, not putting.
Lower bounds: consumers
The mirror image is the lower-bounded wildcard <? super T>, "some unknown
supertype of T." This is what you want when you need to write T values into a
structure. Whatever the real type is, it is T or wider, so a T always fits — the
consumer side.
static void addNumbers(List<? super Integer> list) { // Integer or any supertype
list.add(1); // safe — an Integer fits
list.add(2);
Object o = list.get(0); // reads come back as Object only
}
addNumbers(new ArrayList<Integer>()); // ok
addNumbers(new ArrayList<Number>()); // ok — Integer fits a Number list
addNumbers(new ArrayList<Object>()); // ok — Integer fits an Object list
Reading from a super wildcard gives you back only Object, because the widest type
common to every supertype of T is Object. So super wildcards are for putting in;
the type you read out is untyped. That asymmetry — extends reads, super writes — is the
whole game.
PECS: Producer Extends, Consumer Super
The mnemonic that ties it together is PECS: Producer Extends, Consumer Super. When a
parameter produces Ts for you to read, use <? extends T>; when it consumes Ts you
supply, use <? super T>. The canonical demonstration is a copy method, where the source
produces and the destination consumes.
// src PRODUCES elements -> extends; dest CONSUMES elements -> super
static <T> void copy(List<? extends T> src, List<? super T> dest) {
for (T t : src) dest.add(t); // read from src, write to dest
}
List<Integer> ints = List.of(1, 2, 3);
List<Number> dst = new ArrayList<>();
copy(ints, dst); // Integer producer -> Number consumer
This is exactly how Collections.copy is declared, and the payoff is API flexibility:
callers can pass a wider range of list types on each side than a rigid List<T> would
allow. You see the same idea throughout the standard library — Collection.addAll takes
Collection<? extends E> because the argument is a producer it only reads from.
Bounded type parameters vs wildcards
Wildcards aren't the only way to constrain a type. A bounded type parameter<T extends Number> also restricts T to Number or below — so when do you pick which?
The deciding question is whether you need to name the type. A type parameter has a name
you can reuse; a wildcard does not.
// type parameter: the SAME T links the input and the return type
static <T extends Number> T firstOf(List<T> list) { return list.get(0); }
Integer i = firstOf(List.of(1, 2)); // returns Integer, not just Number
// wildcard: the type is used once and never named
static double sumOf(List<? extends Number> list) { /* ... */ return 0; }
Use a type parameter when the type appears more than once — relating two arguments, or
flowing back through the return type. Use a wildcard when the type shows up exactly once
and you never need to refer to it. A wildcard has no name, so the most a method can return
from List<? extends Number> is a plain Number; if the output must match the caller's
exact element type, you need the named parameter.
Multiple and recursive bounds
A type parameter can demand several bounds at once with &, meaning T must satisfy
all of them. At most one bound may be a class, and if present it must come first; the rest
must be interfaces. Wildcards can carry only a single bound, so this is type-parameter-only
territory.
// T must be BOTH Comparable AND Serializable
static <T extends Comparable<T> & Serializable> T maxOf(T a, T b) {
return a.compareTo(b) >= 0 ? a : b; // Comparable methods available on T
}
A special case you'll see constantly is the recursive bound <T extends Comparable<T>>
— "T must be comparable to itself." It captures the notion "this type knows how to
order its own instances," which is precisely what sorting and min/max require; without the
recursion you couldn't safely pass a T to compareTo.
static <T extends Comparable<T>> T max(List<T> list) {
T best = list.get(0);
for (T t : list)
if (t.compareTo(best) > 0) best = t; // t.compareTo(T) is type-safe
return best;
}
The real Collections.max loosens this to <T extends Comparable<? super T>> — PECS again,
so a subtype can reuse an ancestor's compareTo instead of redeclaring its own.
Wildcard capture
Sometimes you have a <?> signature but the method body needs to name that unknown type.
The compiler assigns it a temporary name (you'll see CAP#1 in errors), but it won't let
you, say, take an element out of a List<?> and put it back in — it can't prove the two
ends match. The fix is the capture-helper pattern: a private generic method that
captures the wildcard into a real type variable.
// public method keeps the clean <?> signature
static void swap(List<?> list, int i, int j) {
swapHelper(list, i, j); // delegate to capture the wildcard
}
private static <T> void swapHelper(List<T> list, int i, int j) {
T tmp = list.get(i); // now there is a concrete T
list.set(i, list.get(j));
list.set(j, tmp);
}
The helper's T captures the wildcard, so inside it the get-and-set both type-check while
callers still see the tidy List<?> API.
Array covariance vs generic invariance
Why is any of this necessary? Because Java made opposite choices for arrays and generics.
Arrays are covariant — String[] is an Object[] — which feels convenient but defers
safety to runtime. Generics are invariant — List<String> is not a List<Object> —
catching the mistake at compile time instead.
Object[] arr = new String[3];
arr[0] = 42; // compiles, throws ArrayStoreException at RUNTIME
List<Object> list = new ArrayList<String>(); // compile error — caught early
Generics chose invariance because erasure removes type info at runtime — there is no
ArrayStore check to fall back on, so the unsafe assignment is forbidden up front.
Wildcards then hand the lost flexibility back, safely: List<? extends Number> accepts a
List<Integer> for reading without ever letting you insert the wrong element. They are the
opt-in bridge between strict invariance and the covariance (extends) or contravariance
(super) you actually want — applied exactly where it's provably sound.
Recap
Generics are invariant by design, and wildcards restore the flexibility that
invariance costs. Use <?> when the element type is irrelevant; use <? extends T> for
producers you read from and <? super T> for consumers you write to — the
PECS rule, "Producer Extends, Consumer Super." Reach for a bounded type parameter
instead of a wildcard whenever you must name the type to reuse it or return it, and lean on
multiple bounds (<T extends A & B>) and recursive bounds
(<T extends Comparable<T>>) when one constraint isn't enough. Remember the get-and-put
principle — extends to get, super to put, an exact type to do both — keep the
capture-helper pattern in your back pocket, and you'll read the standard library's
gnarly signatures, and write your own, with confidence.