The unbounded wildcard <?> means "a List of some unknown type" —
you don't know or care which. It makes a generic method work over collections
of any element type while staying type-safe, unlike a raw type.
static void printAll(List<?> list) { // any element type
for (Object o : list) System.out.println(o); // safe: everything is an Object
}
printAll(List.of(1, 2, 3)); // List<Integer> ok
printAll(List.of("a", "b")); // List<String> ok
Because the element type is unknown, you can read elements as Object but
can't add anything (except null) — the compiler can't prove your value
matches the hidden type. Use <?> when the method body doesn't depend on the
element type at all.
They look similar but behave very differently:
| Declaration | Accepts | Can add? | Type-safe? |
|---|---|---|---|
List<?> |
any List<X> |
only null |
yes |
List<Object> |
only List<Object> |
any object | yes |
List (raw) |
any List |
anything (unchecked) | no — legacy |
List<String> strs = new ArrayList<>();
List<?> a = strs; // ok — unknown element type
List<Object> b = strs; // compile error — not the same type
List c = strs; // ok but unchecked warnings; defeats generics
List<Object> is a concrete type that only matches List<Object>. List<?>
matches every parameterization but is read-only. The raw List exists
only for pre-generics compatibility — it disables type checking, so avoid it.
<? extends T> means "some unknown subtype of T" — it sets an upper
bound. It's used when you want to read T values out of a structure but
don't need to know the exact subtype.
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
Every element is guaranteed to be at least a T, so reading as T is
safe. This is the producer side — the structure produces Ts for you to
consume.
Because the compiler knows the list is List<SomeSubtypeOfNumber> but not
which subtype. It could be List<Integer>, List<Double>, or anything else —
so adding any concrete value might violate the real element type.
List<? extends Number> nums = new ArrayList<Integer>();
nums.add(1); // compile error — what if it's really List<Double>?
nums.add(1.5); // compile error — what if it's really List<Integer>?
nums.add(null); // the only legal add — null fits any type
Number n = nums.get(0); // reading is fine — guaranteed to be a Number
The list becomes effectively read-only for element types. The rule:
extends wildcards are for getting, not putting.
<? super T> means "some unknown supertype of T" — it sets a lower
bound. It's used when you want to write T values into a structure.
static void addNumbers(List<? super Integer> list) { // Integer or any supertype
list.add(1); // safe — an Integer fits a List of Integer-or-wider
list.add(2);
}
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
Whatever the real type is, it's Integer or wider, so an Integer is always a
valid element. This is the consumer side — the structure consumes the Ts
you supply.
Only Object. The compiler knows the list holds some supertype of T,
but the widest guaranteed common type of any supertype is Object, so that's
all a get can be typed as.
List<? super Integer> list = new ArrayList<Number>();
list.add(42); // writing Integers is fine
Object o = list.get(0); // ok — only Object is guaranteed
Integer i = list.get(0); // compile error — could be a Number/Object list
So super wildcards are primarily for putting in; reading gives you back
untyped Object. This asymmetry with extends is exactly what PECS captures.
PECS = Producer Extends, Consumer Super. When a parameter produces
Ts for you (you read from it), use <? extends T>. When it consumes Ts
you give it (you write to it), use <? super T>. The canonical example is a
copy method:
// 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 how Collections.copy is actually declared. PECS maximizes API
flexibility: callers can pass a wider range of list types on each side.
Use a bounded type parameter when you need to name the type — to refer to it more than once, relate two arguments, or use it in the return type. Use a wildcard when the type is used only once and you never need to name it.
// type parameter: the SAME T links input and return
static <T extends Number> T firstOf(List<T> list) { return list.get(0); }
// wildcard: type used once, never named
static double sumOf(List<? extends Number> list) { /* ... */ return 0; }
A good rule: if a wildcard would force you to invent a helper just to name the type, use a type parameter instead. Otherwise prefer wildcards in API parameters — they read more cleanly to callers.
A type parameter can require several bounds joined by &, meaning T must
satisfy all of them. At most one can be a class, and if present it must
come first; the rest must be interfaces.
// 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; // can call Comparable methods on T
}
// class first, then interfaces:
static <T extends Number & Comparable<T>> T pick(T x) { return x; }
Within the method T exposes the members of all bounds. Wildcards, by
contrast, allow only a single bound (<? extends A>), so multiple bounds
require a named type parameter.
This is a recursive (self-referential) type bound — T must be comparable
to itself. It captures the idea "this type knows how to order its own
instances," which is exactly what sorting and min/max need.
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;
}
max(List.of(3, 1, 2)); // T = Integer, Integer implements Comparable<Integer>
Without the recursion you couldn't safely call compareTo with a T argument.
The real Collections.max uses <T extends Comparable<? super T>> — slightly
looser so a subtype can reuse an ancestor's comparison.
Wildcard capture is the compiler giving the unknown type of a <?> a
temporary name (shown in errors as CAP#1). Sometimes you need to name that
type to operate on it — the fix is a private capture helper generic method
that captures the wildcard into a real type variable.
// fails: compiler won't prove list.get(i) matches list.set(j, ...)
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 public method keeps the clean <?> signature; the helper captures the
wildcard as T so the body type-checks.
Arrays are covariant: String[] is a Object[]. Generics are
invariant: List<String> is not a List<Object>. Generics chose
invariance for compile-time safety — array covariance defers the check to
runtime and can blow up.
Object[] arr = new String[3];
arr[0] = 42; // compiles, throws ArrayStoreException at RUNTIME
List<Object> list = new ArrayList<String>(); // compile error — caught early
Because generics are erased at runtime there's no ArrayStore check to fall
back on, so the language forbids the unsafe assignment up front. Wildcards
then restore the lost flexibility safely (List<? extends Object> accepts a
List<String> but won't let you put the wrong thing in).
Invariance means List<Integer> isn't a List<Number>, so a method taking
List<Number> would reject an Integer list. A wildcard widens the
parameter to accept a family of types while staying safe.
static double total(List<Number> list) { /* ... */ return 0; }
total(new ArrayList<Integer>()); // compile error — invariance
static double total2(List<? extends Number> list) { /* ... */ return 0; }
total2(new ArrayList<Integer>()); // ok — wildcard accepts subtypes
total2(new ArrayList<Double>()); // ok
So wildcards are the bridge between strict invariance and the polymorphism you
actually want — you opt into covariance (extends) or contravariance (super)
exactly where it's safe.
Its signature is static <T extends Object & Comparable<? super T>> T max(Collection<? extends T> coll). The two wildcards do different jobs:
Collection<? extends T> lets it accept any subtype collection (the collection
produces Ts), and Comparable<? super T> lets a type be ordered by a
comparator inherited from an ancestor.
class Animal implements Comparable<Animal> { /* ... */ }
class Dog extends Animal { }
List<Dog> dogs = List.of(new Dog(), new Dog());
Dog biggest = Collections.max(dogs); // Dog uses Animal's Comparable<Animal>
The super on Comparable is the key flexibility: Dog doesn't need its own
compareTo — inheriting Comparable<Animal> suffices. This is PECS applied to
the comparison itself.
boolean addAll(Collection<? extends E> c) uses an upper-bounded wildcard
because the source collection is a producer — addAll only reads from
c and writes into this. That lets you add a collection of any subtype of
E.
List<Number> nums = new ArrayList<>();
List<Integer> ints = List.of(1, 2, 3);
nums.addAll(ints); // ok — Integer extends Number (producer)
Without ? extends E you could only pass an exact Collection<E>, forcing
callers to convert. This is the "Producer Extends" half of PECS in the standard
library.
No. You can't write new with a wildcard type — the compiler needs a
concrete type argument to know what to construct. Wildcards are for variable,
parameter, and field types, not instantiation.
List<?> ok = new ArrayList<String>(); // wildcard on the VARIABLE — fine
List<?> bad = new ArrayList<?>(); // compile error — can't instantiate <?>
var list = new ArrayList<>(); // diamond infers <Object>, not a wildcard
The diamond <> is not a wildcard — it tells the compiler to infer a
concrete type from context. A genuine <?> has no concrete type to allocate, so
construction is forbidden.
A wildcard in a return type leaks into the caller's code: they're forced to
deal with <?> (read-only, capture errors) even though it gave them no
flexibility. The advice (Effective Java) is to use wildcards on parameters,
not returns.
// bad: caller now holds a List<?> they can barely use
static List<?> bad() { return new ArrayList<String>(); }
// good: return a concrete or named type
static <T> List<T> good(T seed) { return new ArrayList<>(); }
If a method's return type would need a wildcard, that's usually a sign it should take a type parameter instead, so the caller gets a usable concrete type.
Wildcards apply at each level independently. List<List<?>> is a list whose
elements are each "a list of some unknown type" — and crucially that outer
list is invariant: it is not the same as List<List<String>>.
List<List<?>> outer = new ArrayList<>();
outer.add(List.of(1, 2)); // ok — inner is List<? = Integer>
outer.add(List.of("a")); // ok — inner is List<? = String>
List<List<String>> strs = new ArrayList<>();
List<List<?>> alias = strs; // compile error — outer level is invariant
To accept "a list of lists of anything" you need List<? extends List<?>> on
the outer level too. Mixing levels (List<? extends List<? extends Number>>) is
legal but a readability red flag.
A wildcard has no name, so you can't mention it in a return type — the best
you could return is Object or another wildcard. A type parameter is named,
so it can appear in the return position and flow that exact type back to the
caller.
// wildcard: can't name the element type to return it
static Number first(List<? extends Number> l) { return l.get(0); } // only Number
// type parameter: returns the caller's exact T
static <T extends Number> T firstTyped(List<T> l) { return l.get(0); }
Integer i = firstTyped(List.of(1, 2)); // gets Integer back, not just Number
So when the output type must match the input element type, a named bounded type parameter is required.
A Comparator<? super T> lets you sort a List<T> with a comparator defined
for T or any of its supertypes — the comparator is a consumer of Ts,
so PECS says super. You shouldn't be forced to write a T-specific comparator
when an ancestor's already works.
// List.sort signature: void sort(Comparator<? super E> c)
List<Dog> dogs = new ArrayList<>(/* ... */);
Comparator<Animal> byAge = Comparator.comparingInt(Animal::age);
dogs.sort(byAge); // ok — a Comparator<Animal> can compare Dogs (super)
Without super you'd have to duplicate byAge as a Comparator<Dog>. This
"Consumer Super" usage is one of the most common real-world wildcard payoffs.
The get-and-put principle is the same rule as PECS stated mechanically:
use extends when you only get values out, use super when you only
put values in, and use an exact type (no wildcard) when you need to do
both.
List<? extends Number> producer = List.of(1, 2);
Number n = producer.get(0); // get: ok | put: forbidden
List<? super Integer> consumer = new ArrayList<Number>();
consumer.add(7); // put: ok | get returns only Object
List<Integer> both = new ArrayList<>();
both.add(7); int x = both.get(0); // exact type: get AND put both work
So if a method must read and write the same element type, drop the wildcard and use a concrete type or a named type parameter.
More Generics interview questions
More ways to practice
The self-quiz is live. Get notified when mock interviews and new question packs drop.