Generic types in Java are stricter than they first appear. A List<Product> is not a List<Object>, even though Product is an Object, and that often surprises newcomers. Wildcards (<?>, <? extends X>, and <? super X>) are how Java lets you write flexible APIs without breaking the type safety that generics are built to protect. This lesson covers why generics are invariant, what each wildcard form means, and the PECS rule that picks between them.
List<Product> Is Not a List<Object>Start with the example that motivates the rest. Intuitively, a method taking List<Object> should accept a List<Product>, because every Product is an Object. The compiler disagrees.
The compiler blocks the assignment. To see why, consider what would happen if it didn't.
If List<Product> were assignable to List<Object>, a String could be inserted into the product list through the wider reference, and a later get from the narrower reference would fail at runtime. Generics refuse the assignment up front so the runtime stays safe.
This property has a name: parameterized types in Java are invariant. List<Product> and List<Object> are unrelated types, even though their type parameters are related. The relationship between Product and Object does not flow up into the relationship between List<Product> and List<Object>.
The top half of the diagram shows the normal class hierarchy: Product extends Object. The bottom half shows what happens when you wrap those types in List<...>. The subtype relationship is gone. Neither parameterized type is a subtype of the other. That's invariance.
Arrays in Java are covariant, which is the opposite of generics. Product[] is a subtype of Object[], and the compiler will happily let you assign one to the other.
The compiler lets the assignment through. At runtime, the array itself remembers it was created as a Product[] and refuses the String. That's ArrayStoreException. The check has been pushed to runtime, which is exactly what generics are designed to avoid.
Arrays were added in Java 1.0 before generics existed, and covariance was deemed worth the runtime check at the time. Generics arrived in Java 5 and learned from that mistake. Invariance is stricter, but it makes the compiler the place where type errors get caught instead of the production logs.
<?>Invariance is safe but stiff. A method that prints any list, regardless of element type, can't take List<Object> as a parameter, because nobody can pass List<Product> to it. This is where the unbounded wildcard <?> enters.
List<?> reads as "a list of some unknown type". It's a supertype of every List<...>. A method that takes List<?> accepts List<Product>, List<String>, List<Integer>, and any other parameterization you want to hand it.
The printSize method accepts both lists without complaint. That's the unbounded wildcard's main purpose: write the method once, accept any element type.
The catch is what the method body can do. Since the type is unknown, the compiler refuses to allow inserts (except null, which fits every reference type). It also refuses to read a specific type out, so reads only return Object.
The error message uses CAP#1, which is the compiler's internal placeholder for "whatever specific type this ? actually captured". The compiler can't prove that a Product matches CAP#1, so it rejects the call. Same for String. The only safe add is null, because null is a valid value for any reference type.
You can do this on List<?> | You cannot do this on List<?> |
|---|---|
Call size(), isEmpty() | Call add(T) with anything except null |
Call clear() | Call set(i, T) with a typed value |
Read elements as Object | Read elements as a specific type |
Iterate as Object | Treat elements as their original type |
Call add(null) | Anything that requires knowing the element type |
List<?> is mostly useful when the method doesn't care what's in the list. Counting, clearing, checking if it's empty, printing the elements through Object.toString(). For reading or writing a specific type, one of the bounded wildcards fits better.
<? extends X>Counting and clearing is fine, but most real methods need to read the elements. Reading elements as a specific type requires an upper-bounded wildcard.
List<? extends Product> reads as "a list of Product or some subtype of Product, but we don't know which subtype". A List<Product> matches. A List<Book> matches if Book extends Product. A List<Toy> matches if Toy extends Product. A List<String> does not match.
printAll accepts both List<Book> and List<Toy>, and it can safely read each element as a Product. That's the win: one method, many list types, type-safe reads.
Adding to a List<? extends Product> is not allowed, though.
The reason is the same shape as the unbounded case. The compiler knows the list holds Product or some subtype, but it doesn't know which subtype. If the caller passed a List<Toy>, sticking a Book into it would corrupt the list. The compiler refuses on safety grounds.
The diagram shows the asymmetry. Every concrete list that matches ? extends Product has elements that are Product or a subtype, so reading as Product is always safe. Writing a specific Book is only safe for some of those concrete lists, not all, so the compiler bans it for everyone.
List<? extends Product> is a producer: it produces Product instances to whoever holds the reference. Reads return Product values and elements can be passed around as Product, but no new elements can be added.
Wildcards don't add runtime overhead. They're a compile-time feature only. The bytecode for List<? extends Product> is identical to the bytecode for List<Product> after type erasure.
<? super X>Reading is solved. What about writing? A method that writes Product instances into a list needs a list willing to accept Product and possibly any supertype of Product. That's the lower-bounded wildcard.
List<? super Product> reads as "a list of Product or some supertype of Product". A List<Product> matches. A List<Object> matches, because Object is a supertype of Product. A List<Book> does not match, because Book is a subtype.
The method accepts both lists and writes safely into either. Adding a Product is allowed because the list, whatever it actually is, has been promised to accept Product (and any subtype of Product, which is why adding a Book also works).
The catch this time is on the read side. When reading from a List<? super Product>, the compiler only knows that whatever comes out is at least an Object. It can't promise the element is a Product, because the list might actually be a List<Object> that contains a String.
List<? super Product> is a consumer: it consumes any Product instances passed to it. Writes work, but reads come back as Object and need a cast or an instanceof check before being treated as anything more specific.
| Wildcard form | Reads as | Writes accept | Mental model |
|---|---|---|---|
List<?> | Object only | null only | Anything goes in, but you can't put or trust anything |
List<? extends Product> | Product | Nothing (except null) | Producer of Product |
List<? super Product> | Object only | Product or any subtype | Consumer of Product |
Each row in the table shows the trade-off involved.
Joshua Bloch coined the mnemonic PECS: Producer Extends, Consumer Super. If a parameter produces values for the method to read, use extends. If a parameter consumes values the method wants to write, use super. If it does both (or neither), don't use a wildcard at all.
The arrows in the diagram say it best. The producer sends Product instances into the method. The consumer receives Product instances from the method. Each role has the wildcard form that matches its direction of data flow.
A common example is a copy method that reads from one collection and writes into another. The source is a producer; the destination is a consumer.
Read the method signature carefully. src is List<? extends T>, the producer. dest is List<? super T>, the consumer. The type parameter T ties them together: whatever element type the source produces, the destination has to be able to accept.
The call passes List<Book> as the source and List<Product> as the destination. Java infers T = Book. List<Book> matches List<? extends Book> (the source list produces Book). List<Product> matches List<? super Book> (the destination list consumes Book, since Product is a supertype of Book).
The same method without wildcards would have too rigid a signature.
The rigid version forces T to be the same type on both sides, which means the caller must use lists with the same element type. That's overly restrictive. The wildcard version separates "what the source produces" from "what the destination consumes", letting the caller pick any compatible pair.
java.util.Collections.copy uses this pattern. So does most of the JDK that walks one collection and writes into another.
The question of when to pick a wildcard versus a bounded type parameter (<T extends Product>) comes up naturally. They look similar and overlap, but the use cases are different.
Use a bounded type parameter when the type shows up in multiple places that have to agree.
T appears in the return type and in the parameter type. The caller and the method share the same T, so a List<Product> produces a Product return value, and a List<Book> produces a Book return value. A wildcard in place of T would lose that connection.
Use a wildcard when the type only appears once in the signature and the method doesn't care which specific type it captured.
Here, the method doesn't return the element type, doesn't pass it to another parameter, and doesn't need to write into the list. Using a wildcard says exactly that. A bounded type parameter would work too, but it adds clutter without adding value.
| Situation | Pick |
|---|---|
| Type appears in return type or multiple parameters | <T extends X> |
| Caller needs the exact captured type back | <T extends X> |
| Method only reads, type used once as a parameter | List<? extends X> |
| Method only writes, type used once as a parameter | List<? super X> |
Method neither reads nor writes elements (just size, etc.) | List<?> |
| Type is fixed at the call site | List<X> (no wildcard) |
A way to think about the difference: a type parameter is a name to use again. A wildcard is a one-shot "the specific type doesn't matter here". Wanting ? to appear twice and refer to the same type is a sign that a type parameter fits better than two wildcards.
To put all three wildcards in one place, consider a service that walks a list of products, computes a discounted version of each one, and writes the results into a destination list. The source produces products; the destination consumes the discounted products.
Three signatures, three wildcards, each one with a job.
source is List<? extends Product>, the producer. The method reads each element as a Product and uses its name and price. The caller can pass List<Book>, List<Toy>, List<Product>, or anything else whose element type is Product or a subtype.
destination is List<? super DiscountedProduct>, the consumer. The method writes DiscountedProduct instances into it. The caller can pass List<DiscountedProduct>, List<Product> (if DiscountedProduct extends Product, which it doesn't here, but the same pattern applies), or List<Object>. The example uses both List<DiscountedProduct> and List<Object> to show the flexibility.
items in printSummary is List<?>, the unbounded wildcard. The method doesn't care what's in the list, only how many items there are and what each one's toString() looks like. The unbounded form expresses "any list at all" without committing to an element type.
The same code without wildcards would force every caller to use the exact types picked at design time, leading to three or four overloads to handle the realistic combinations.
The diagram traces the data flow through the example. Product instances flow from the source into the method. DiscountedProduct instances flow from the method into the destination. The destination, viewed through List<?>, can then be handed to printSummary which doesn't care about element types at all.
Most wildcards in real code live in method parameters. The recurring patterns are these.
Read-only consumer of a typed sequence. The method iterates the parameter and uses each element. The parameter is a producer.
Any List<Book>, List<Toy>, or List<Product> works as the argument. The method only reads, so extends is appropriate.
Write-only collector. The method computes results and writes them into the parameter. The parameter is a consumer.
Source is a producer; destination is a consumer. The caller picks expensive as List<Product>, List<Object>, or any supertype of Product.
Generic "I don't care" parameter. The method doesn't read typed elements and doesn't write at all. The unbounded wildcard fits.
The same method works for every list ever, with no element type required.
Wildcards never change runtime performance. A List<? extends Product> is the same ArrayList at runtime as a List<Product>. The compiler uses the wildcard form to type-check at compile time and then erases it.
A common pattern: a method's wildcard parameter can be passed to another wildcard method. As long as the wildcards line up, the compiler lets the chain go through.
Inside process, items is List<? extends Product>. Passing it to totalPrice, which also takes List<? extends Product>, is fine. The wildcards aren't "captured" in a different way for each call; they're the same constraint.
The compiler error messages around wildcards mention CAP#1, CAP#2, and "capture of ?". Decoding them helps.
When the compiler sees List<? extends Product> at a use site, the exact type is unknown. It invents a fresh placeholder type, CAP#1, that satisfies the constraint (CAP#1 extends Product). Any value of type CAP#1 is also a Product (so reading works), but Product, Book, and Toy are not provably equal to CAP#1 (so writing fails).
The fix isn't to fight the message but to ask what the method actually needs. If it only reads, use extends (and don't try to write). If it needs to write Product, use super. If it needs both, a type parameter <T extends Product> is usually appropriate so the caller commits to one concrete type.
T is captured at the call site (it's whatever the caller chose), and inside the method items is List<T> and item is T. The two match, so add works.
A small program that exercises all three wildcards and contrasts them with arrays for emphasis.
Walk through what each section illustrates. printSize takes List<?> and accepts both List<Book> and List<Toy> without caring about the element type. totalPrice takes List<? extends Product> and reads each element as a Product, summing their prices. addSample takes List<? super Product> and writes both a Product and a Book into the destination, which the caller declared as List<Object>. copy shows PECS in action: the source produces T, the destination consumes T, and the type parameter ties the two together.
These patterns appear across the JDK. Collections.copy, Collections.addAll, Comparator.thenComparing, Stream.filter, all use the same shapes. The reasoning is the same in each case: ask whether the parameter produces values or consumes them, and the wildcard form drops out.
10 quizzes