Wildcards in Java Generics
Use ?, ? extends T, and ? super T to master covariance and contravariance in Java method parameters.
Use ?, ? extends T, and ? super T to master covariance and contravariance in Java method parameters.
Wildcards in Java Generics
Wildcards (?) represent an unknown type in generic usage. They are a way to express read patterns and write patterns at the call site without committing to a specific type argument. The three forms cover most scenarios: unbounded (?), upper-bounded (? extends T), and lower-bounded (? super T).
When to Use Wildcards
- Read-only parameters — use
? extends Twhen a method only reads from a collection - Write-only parameters — use
? super Twhen a method only writes to a collection - Flexible APIs — when a method works with any subtype or supertype of a known type
- Consumer-Producer pattern — when a parameter both reads and writes, use different bounds for each position (PECS)
When NOT to Use Wildcards
- Return types — wildcards in return types force callers to deal with unknown types; prefer specific type parameters
- Class field types — storing a wildcard in a field loses type information permanently
- Overly complex signatures — chained wildcards like
List<? extends List<? extends Number>>are hard to read and usually a sign of over-engineering
Code Example: Upper-Bounded Wildcard (? extends T)
// Reading: we only need to know elements are at least T
public static double sum(List<? extends Number> numbers) {
double total = 0.0;
for (Number n : numbers) { // safe read — Number is the upper bound
total += n.doubleValue();
}
return total;
}
// Works with any List of a Number subtype
List<Integer> ints = Arrays.asList(1, 2, 3);
List<Double> doubles = Arrays.asList(1.1, 2.2);
sum(ints); // ? extends Number matches Integer
sum(doubles); // ? extends Number matches Double
Code Example: Lower-Bounded Wildcard (? super T)
// Writing: we can accept T or any supertype of T
public static void addNumbers(List<? super Integer> list) {
list.add(1); // safe write — we know list holds at least Integer
list.add(2);
// list.add(1.5); // compile error — list might be List<Number>
}
List<Number> numList = new ArrayList<>();
List<Object> objList = new ArrayList<>();
addNumbers(numList); // ? super Integer matches Number
addNumbers(objList); // ? super Integer matches Object
Code Example: PECS (Producer Extends, Consumer Super)
public static <T> void copy(List<? extends T> source, List<? super T> dest) {
for (T item : source) { // source produces T (we read it)
dest.add(item); // dest consumes T (we write to it)
}
}
List<Number> numbers = new ArrayList<>();
List<Integer> ints = Arrays.asList(1, 2, 3);
copy(ints, numbers); // T = Number, source=? extends Integer, dest=? super Number
Code Example: Unbounded Wildcard (?)
public static boolean isEmpty(List<?> list) {
return list.isEmpty();
// we only call methods common to all List<?> regardless of element type
}
// List<?> means "a List of some unknown type"
// We can read elements as Object (everything is an Object)
public static void printAll(List<?> list) {
for (Object item : list) {
System.out.println(item);
}
// list.add("new"); // compile error — we cannot add anything (we don't know the type)
}
Mermaid Diagram: Wildcard Variance
classDiagram
direction TB
class Number
class Integer
class Double
class Object
Integer --|> Number
Double --|> Number
Number --|> Object
Failure Scenarios
1. Adding Elements to ? extends T
The restriction on adding elements to ? extends T collections stems from a fundamental asymmetry in what the compiler knows about the collection’s actual type. When a method declares a parameter as List<? extends Number>, the caller can pass a List<Integer>, a List<Double>, or any other List whose type argument is a subtype of Number. The compiler accepts all of these because they are all subtypes of List<? extends Number> under the wildcard’s bound. But at compile time, the method body has no way to know which concrete type was passed.
public static void wrongAdd(List<? extends Number> list) {
list.add(Double.valueOf(1.0)); // compile error!
// The compiler only knows list is List<X> where X extends Number.
// It could be List<Integer>, so adding a Double is unsafe.
}
// Fix: if you need to write, use ? super T instead, or do not use wildcards
The problem is concrete: if you could add a Double to a List<? extends Number>, and the actual list was a List<Integer>, you would be inserting a Double into a list that should only hold Integer values. The JVM would not catch this at runtime because after erasure, List<Integer> and List<Double> are both just List. The type safety that generics provide would be entirely defeated at the point of insertion. The compiler therefore rejects any insertion operation (except adding null, which is a value of no type) on ? extends T parameters.
This is not a limitation of the wildcard syntax — it is the type system enforcing a soundness property. The wildcard represents an existential type: there exists some unknown type X that extends T, and the list is of type List<X>. The method knows only that X exists, not what X is. Adding any element of type T to List<X> is unsafe because X might be a more specific subtype that does not accept T. The only element that is safe to add to an existential collection is null — it is a valid value for any reference type, and it carries no type information that could violate the collection’s element type contract.
2. Reading From ? super T
? super Integer represents an existential type where the collection’s element type is some unknown supertype of Integer — it could be Integer itself, Number, Object, or any other ancestor in the type hierarchy. The lower-bounded wildcard gives you flexibility at the call site (you can pass List<Integer>, List<Number>, or List<Object>), but this flexibility comes at the cost of what you can safely read from the collection.
public static void wrongRead(List<? super Integer> list) {
Integer val = list.get(0); // compile error!
// The compiler only knows list is List<X> where X super Integer.
// It could be List<Object>, and Object is not Integer.
Object val = list.get(0); // only safe read is Object
}
The read restriction mirrors the write restriction from ? extends T, but in the opposite direction. When you declare List<? super Integer>, the unknown type X is a supertype of Integer. Any element you retrieve from the list has a runtime type that is some subtype of X. The compiler cannot guarantee that retrieved element is an Integer — it could just as easily be a Number or an Object that happens to be in a List<Object>. The only type the compiler can guarantee is Object, which is the common supertype of all reference types.
The reasoning parallels the ? extends T case but in reverse. The existential type ? super Integer means “some unknown type X such that Integer extends X”. At runtime, the actual list could be List<Object>, and calling list.get(0) returns an Object. Casting that Object to Integer would be unsafe — it would throw ClassCastException if the object were actually a String or any other non-Integer type. The compiler therefore requires you to read as Object and perform an explicit cast if you need Integer. This is the same situation as reading from a List<Object> directly, except the wildcard encodes the additional constraint that the actual type argument must be a supertype of Integer — which restricts what lists you can pass in but does not change what you get out.
3. Mixing Read and Write with the Same Wildcard
The same wildcard bound cannot give you both read and write access at the same time. ? extends T lets you read as T, but the compiler has no way to know whether whatever subtype X satisfies X extends T will accept a write of an arbitrary T. ? super T hits the mirror problem: it lets you write T, but reading from List<X> where X super T gives you something that might not be a T at all.
public static void mixed(List<? extends Number> list) {
list.add(Integer.valueOf(1)); // compile error
Number n = list.get(0); // fine
}
// ? extends T is read-only; ? super T is write-only
// For mixed access, use the type parameter directly: <T> void process(List<T> list)
When you need both operations on the same collection, use a concrete type parameter instead:
public static <T> void process(List<T> list) {
list.add(Integer.valueOf(1)); // fine — T is concrete at the call site
Number n = list.get(0); // fine — T is known to be at least Number
}
Here T is inferred from the actual list you pass in. List<Integer> means T = Integer; List<Double> means T = Double. The compiler knows the exact type for writes and the lower bound for reads, so both operations are safe. The tradeoff is call-site flexibility — you cannot pass a List<Number> and a List<Object> to the same invocation without explicitly specifying T.
Trade-Off Table
| Pattern | Use When | Cannot Do |
|---|---|---|
? extends T | Reading elements as T | Add non-null elements (type unknown) |
? super T | Writing elements of type T | Read as anything more specific than Object |
? (unbounded) | Operations that need any type info | Read as anything more specific than Object / add any element |
<T> (parameter) | Needing both read and write, or return type | More verbose call site |
Observability Checklist
- Confirm wildcard direction matches the data flow (PECS rule)
- Check return types — wildcards in return types force casts; avoid unless necessary
- Verify static analysis flags “Exceeds bounds of wildcard” warnings
- Ensure API documentation describes what wildcards mean for callers (read-only, write-only)
- Consider refactoring to
<T>if a method signature has multiple wildcards that confuse callers
Security Notes
-
Wildcards do not provide runtime type safety:
List<? extends Number>andList<Integer>both erase toListat runtime. Untrusted data in collections cannot be protected by wildcards. -
Unsafe cast via wildcard:
List<?> list = new ArrayList<String>();compiles fine, but adding a non-String and retrieving it will throwClassCastException. Wildcards let you bypass compile-time checks at your own risk. -
Reflection and wildcards:
Method.getParameterTypes()returns the raw generic type — wildcards are stripped. Passing wildcard-parameterized collections to reflective methods requires extra care.
Pitfalls
-
?vsObjectin Lists:List<?>andList<Object>are not the same.List<?>has unknown element type so you cannot add anything;List<Object>accepts anything. -
Capture confusion: When a wildcard is captured by inference, you may get compiler messages like “capture#1 of ?” — this happens when the compiler infers a specific type for the wildcard but cannot express it.
-
No nested wildcards for collections:
List<List<? extends Number>>is valid but hard to use — you can read inner lists but not add elements to them.
Quick Recap
? extends T— producer pattern: read elements asT, cannot add elements (exceptnull)? super T— consumer pattern: write elements asT, read asObject?(unbounded) — unknown type, limited toObjectreads and no additions- PECS:
? extends Tfor producers (input to your method),? super Tfor consumers (output from your method) - Wildcards are for call-site flexibility, not storage; return types should use concrete type parameters
- Type erasure makes wildcards compile-time only — no runtime type information
Key Takeaways
Wildcards solve the asymmetry between invariant generic types and the covariant/contravariant relationships you actually need at call sites. List<String> is not a subtype of List<Object> — generics are invariant by default — but you often want to pass a List<Integer> where a List<? extends Number> is expected. Wildcards make this work without forcing you to abandon type safety.
The PECS rule (Producer Extends, Consumer Super) is the practical heuristic: if your method only reads from a collection, use ? extends T — the compiler lets you treat elements as T. If your method only writes to a collection, use ? super T — the compiler lets you pass elements of type T. Trying to do both with the same wildcard is a compile error for good reason.
The critical insight is that wildcards are for call-site flexibility, not storage. Storing a wildcard in a field permanently discards type information. Return types should never use wildcards — forcing callers to deal with an unknown type is poor API design.
The bounded wildcard ? extends T lets you read as T but not write (except null), because the compiler cannot guarantee the collection’s actual element type. ? super T lets you write as T but only read as Object, because the collection might hold supertypes of T. These are not flaws — they are the type system enforcing that you cannot violate the collection’s element type contract.
For how all this works under the hood, see Type Erasure in Java Generics — wildcards are compile-time constructs that vanish at runtime just like all other generic syntax.
Interview Questions
List<Object> and List<?>?List<Object> accepts any type — you can add String, Integer, anything. List<?> (unbounded wildcard) accepts null but nothing else, because the wildcard represents an unknown type and the compiler refuses to add anything it cannot guarantee is of that unknown type. List<Object> is concrete; List<?> is deliberately restrictive.Further Reading
- Generic Classes — classes with type parameters
- Generic Methods — writing flexible methods with type parameters
- Type Erasure — how generics are erased at compile time
- Type Bounds — upper and lower bounds on type parameters
- Java Collections Utility — utility methods that use wildcards
- Oracle: Wildcard Types — official documentation on wildcard type arguments
Conclusion
Wildcards (?) are a call-site mechanism for expressing read and write flexibility in generic type usage. ? extends T enables safe reads as T but prohibits writes (except null) because the concrete subtype is unknown. ? super T enables safe writes of T but limits reads to Object. The PECS rule — Producer Extends, Consumer Super — guides which wildcard to use based on whether your method reads from or writes to a collection.
Wildcards are purely a compile-time construct; they vanish at runtime like all generic type information. They are not valid in class declarations, cannot be used as return types, and should not be stored in fields because the type information is permanently lost. For a deeper look at how wildcard variance relates to the underlying type system, see Type Erasure in Java Generics which explains how all generic information disappears at compile time and what that means for type safety.
Category
Related Posts
Abstract Classes in Java
Learn about partially implemented classes that define contracts for subclasses using abstract methods and concrete implementations.
Arithmetic Operators in Java
Master Java arithmetic operators: addition, subtraction, multiplication, division, and modulo with integer division gotchas and operator precedence explained.
Array Basics in Java
Learn Java array fundamentals: declaration, initialization, element access, and the length property explained simply.