Wildcards in Java Generics

Use ?, ? extends T, and ? super T to master covariance and contravariance in Java method parameters.

published: reading time: 20 min read author: Geek Workbench
Quick Summary

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 T when a method only reads from a collection
  • Write-only parameters — use ? super T when 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

PatternUse WhenCannot Do
? extends TReading elements as TAdd non-null elements (type unknown)
? super TWriting elements of type TRead as anything more specific than Object
? (unbounded)Operations that need any type infoRead as anything more specific than Object / add any element
<T> (parameter)Needing both read and write, or return typeMore 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> and List<Integer> both erase to List at 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 throw ClassCastException. 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

  1. ? vs Object in Lists: List<?> and List<Object> are not the same. List<?> has unknown element type so you cannot add anything; List<Object> accepts anything.

  2. 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.

  3. 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 Tproducer pattern: read elements as T, cannot add elements (except null)
  • ? super Tconsumer pattern: write elements as T, read as Object
  • ? (unbounded) — unknown type, limited to Object reads and no additions
  • PECS: ? extends T for producers (input to your method), ? super T for 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

1. What is the difference between ? extends T and ? super T in Java generics?
? extends T is an upper-bounded wildcard — it represents some unknown subtype of T. You can **read** elements as T but cannot reliably **write** to it (the unknown subtype could reject the value). ? super T is a lower-bounded wildcard — it represents some unknown supertype of T. You can **write** elements of type T into it but can only safely **read** as Object."
2. What does PECS stand for and when should you use each wildcard?
PECS stands for **Producer Extends, Consumer Super**. When a method parameter is a collection that your method only reads from (produces elements), use ? extends T. When your method only writes to it (consumes elements), use ? super T. When your method does both, use the concrete type parameter <T> or a different approach."
3. Why cannot you add elements to a List<? extends Number>?
Because List<? extends Number> could legally be a List<Integer> or a List<Double>. The compiler does not know the concrete type at the call site. Adding a Double to a List<Integer> would break type safety, so the compiler rejects any addition (except null) regardless of the actual subtype."
4. What is the difference between 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.
5. Can wildcards be used in class declarations?
No. Wildcards are only for usage sites — method parameters, local variable types, return types. A class must declare a type parameter (e.g., class Box<T>), not a wildcard. You can use wildcards when instantiating or when a method accepts a wildcard argument, but the class definition itself uses <T>.
6. What is the difference between List<T> and List<? extends T>?
List<T> is invariant — List<String> is not a subtype of List<Object>. List<? extends T> is covariant — you can pass a List<String> where List<? extends Object> is expected. However, the wildcard restricts write operations: you cannot add non-null elements to ? extends T because the compiler does not know the concrete type. List<T> allows both reads and writes with full type safety at the call site."
7. How do wildcards behave with nested generic types like List<List<? extends Number>>?
Syntactically valid but practically problematic. You can read the outer list's elements as List<? extends Number> but you cannot add elements to those inner lists because the wildcard blocks additions. Deeply nested wildcards are usually a sign of over-engineering. Prefer simpler type parameters or dedicated generic classes for deeply nested scenarios."
8. What does capture conversion mean in the context of wildcards?
It means the compiler has inferred a specific type for the wildcard at a call site but cannot express that type in source code. This happens in inference chains where the compiler captures the wildcard's inferred type internally for type checking but the type cannot be written back as an explicit type argument. It is an internal compiler artifact — you usually see it when combining generics with method overloads or complex inference scenarios."
9. Can a class declaration use a wildcard as its type parameter?
No. A class declaration must use a type parameter (class Box<T>), not a wildcard. Wildcards are only for usage sites — method parameters, local variable declarations, and return types. You cannot write class Box<?> or class Box<? extends Number>. You can, however, use a wildcard when instantiating: Box<? extends Number> as a type for a local variable, though this has limited use."
10. How do wildcards interact with Java arrays?
Arrays in Java are covariant — String[] is a subtype of Object[]. Wildcards and arrays interact differently: a List<? extends Number> is not directly representable as an array (you cannot create Number[] from List<Integer> at runtime). When a method parameter is an array type with a wildcard bound like Object[], the same covariance rules apply — you can pass a String[] but the array's static type is Object[]."
11. What is the difference between List<?> and List<Object>?
List<?> means "a list of some unknown type" — the compiler knows nothing about the element type, so it only allows reads as Object and forbids all additions (except null). List<Object> means "a list of exactly Object" — the compiler allows both reads and writes of any type. List<Object> is a concrete parameterized type; List<?> is a wildcard type with unknown element type. The distinction matters: List<?> accepts any specific list; List<Object> only accepts lists of Object."
12. Can a class implement a generic interface with wildcard usage?
Yes. When a class implements a generic interface, the method signature can include wildcards. For example, class StringList implements List<String> has add(String), not add(?). But you could implement an interface method that uses a wildcard: void process(List<? extends Number> numbers) inside a class. The wildcard usage is in the method signature, not the class declaration."
13. How does the producer-consumer pattern use wildcards?
The producer-consumer pattern (PECS) uses wildcards at call sites to express data flow direction. If a method reads from a collection (produces elements), use ? extends T so the caller can pass List<Integer> for List<? extends Number>. If a method writes to a collection (consumes elements), use ? super T so the caller can pass List<Number> for List<? super Integer>. When a method does both, use the concrete type parameter instead of wildcards."
14. Can overloaded methods differ only in wildcard bounds?
Two methods that differ only in wildcard bounds can coexist: void process(List<? super Integer>) and void process(List<? super Double>) are distinct overloads because the wildcard is part of the type signature. However, calling process(List<Number>) would be ambiguous if both overloads are applicable — the compiler would report an ambiguity error. Prefer concrete type parameters when ambiguity is a concern."
15. What are the pitfalls of using wildcards in varargs?
Yes, but with caveats. void printAll(List<?>... lists) collects varargs into an array. Since arrays and generics interact badly (heap pollution), the compiler warns. @SafeVarargs silences the warning. However, List<?> elements cannot have anything added to them except null, so the method can only read from each list or pass them to other methods that accept wildcards. Be cautious with varargs on generic collections."
16. How does Collections.copy work with super wildcards in practice?
This is the classic PECS implementation. src is ? extends T — it can produce T elements (read as T). dest is ? super T — it can consume T elements (accept T for writing). The T from src is compatible with ? super T because any supertype of T can hold T. The method works because the producer's type is compatible with the consumer's bound, regardless of the concrete types at the call site."
17. What happens when you assign a parameterized type to a wildcard reference?
List<?> list = new ArrayList<String>(); compiles but bypasses type safety. You cannot add non-null elements to list. If you retrieve elements, they come out as Object. Casting List<?> to List<String> requires an unchecked cast — it compiles with a warning because List<String> and List<?> are different types at compile time but the same at runtime. Use Collections.checkedList(list, String.class) for a runtime-safe view."
18. Should you use wildcards as return types? Why or why not?
You can, but it is poor API design. Returning List<? extends Number> forces the caller to deal with an unknown subtype. The caller receives the list but cannot add elements (except null) without a compile error. Prefer returning a concrete parameterized type like List<Number> or the generic type parameter <T> when possible. Wildcard return types are acceptable only in very specific scenarios where the caller is expected to only read."
19. How do wildcards relate to Optional types in Java?
Optional<T> does not use wildcards in its core API, but the concepts relate. Optional<Integer> is not a subtype of Optional<Number> — generics are invariant. If you need a method that accepts Optional<Number> and works with Optional<Integer>, you cannot use wildcards directly because Optional is a value-based class not a collection. Wildcards work with collection-like types where covariance is meaningful for read operations."
20. Can wildcards be used in exception handling or lambda expressions?
No. You cannot use wildcards in a catch clause (catch (Exception<? extends Throwable> e) is a compile error). However, generic types can appear in exception declarations: a generic method can throw T extends Exception. In lambda expressions, you cannot declare generic exception parameters directly — you wrap them in a generic exception type or use a callback interface that declares the exception type. The JVM's exception handling does not support wildcard types."

Further Reading


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.

#java-abstract-classes #java #java-fundamentals

Arithmetic Operators in Java

Master Java arithmetic operators: addition, subtraction, multiplication, division, and modulo with integer division gotchas and operator precedence explained.

#java-arithmetic-operators #java #java-fundamentals

Array Basics in Java

Learn Java array fundamentals: declaration, initialization, element access, and the length property explained simply.

#java-array-basics #java #java-fundamentals