Java Generics Wildcards
Table of Contents + β
In the last lesson you learned about Java generic methods. You saw the ? symbol pop up, and this is where we explain it. The ? is called a wildcard, the piece that makes generic methods flexible enough to accept many kinds of lists. Letβs learn wildcards properly.
π€ The problem wildcards solve
Here is a problem that surprises almost everyone. You write a method that takes a list of numbers. You expect it to accept a list of integers, because an integer is a number. But the compiler says no. This is what that broken attempt looks like.
import java.util.List;
public class Main {
static void printAll(List<Number> list) { // wants exactly List<Number> for (Number n : list) { System.out.println(n); } }
public static void main(String[] args) { List<Integer> ints = List.of(1, 2, 3); printAll(ints); // compile error }}Even though Integer is a subtype of Number, a List<Integer> is not a subtype of List<Number>. So printAll rejects it.
Compile error
Main.java: error: incompatible types: List<Integer> cannot be converted to List<Number> printAll(ints); ^That rule is called invariance, and it is the whole reason wildcards exist. Letβs see why Java is so strict.
π§© Why a List of Integer is not a List of Number
This feels wrong at first. Integer is a Number, so surely a list of integers is a list of numbers? In real life, yes. In Javaβs type system, no, and there is a safety reason. Imagine Java did allow it. Then this dangerous code would compile.
List<Integer> ints = new ArrayList<>();List<Number> nums = ints; // pretend this were allowednums.add(3.14); // adding a Double into a list of Integer!Integer x = ints.get(0); // boom: a Double is sitting where an Integer should beHere is what would go wrong:
- Treat a
List<Integer>as aList<Number>and you could add aDoublethrough theNumberview. - Later someone reading through the
Integerview gets aDoubleand crashes. - So Java keeps generic types invariant:
List<Integer>andList<Number>are different, unrelated types. - This keeps you safe, but makes methods rigid.
List<Number>accepts onlyList<Number>, nothing else.
Wildcards are how we loosen that back up without losing the safety.
β The unbounded wildcard: List<?>
The simplest wildcard is a lone question mark. List<?> means βa list of some unknown typeβ. You do not name it and do not care what it is. You only do things that work for any type. Here is a method that prints any list.
import java.util.List;
public class Main {
static void printAll(List<?> list) { // accepts a list of anything for (Object item : list) { System.out.println(item); } }
public static void main(String[] args) { List<Integer> ints = List.of(1, 2, 3); List<String> names = List.of("Alex", "Riya");
printAll(ints); // works printAll(names); // also works }}The parameter List<?>:
- Accepts every list:
List<Integer>,List<String>,List<Double>, all of them. - Reads each element as an
Object, the one thing we know for sure about any element. - Only calls things every type can do, like printing.
Output
123AlexRiyaThere is an important catch:
- The type is unknown, so you cannot add normal elements to a
List<?>. - The compiler has no idea what belongs there, so it blocks additions to keep the list safe.
- The only thing you may add is
null, becausenullfits any type. - So treat
List<?>as read-only-ish: good for reading and inspecting, not for adding.
π€ The upper-bounded wildcard: List<? extends Number>
The plain ? only saw Object, not Number. Often you want more: accept many list types and still treat the elements as numbers so you can do maths. That is the upper bound.
List<? extends Number>means βa list ofNumberor any subtype ofNumberβ.- It accepts
List<Integer>,List<Double>,List<Long>, andList<Number>itself. extendssets an upper limit: the unknown type sits atNumberor below it.
Here is a method that sums any list of numbers.
import java.util.List;
public class Main {
static double sum(List<? extends Number> numbers) { double total = 0; for (Number n : numbers) { // every element is at least a Number total += n.doubleValue(); // so doubleValue() is always safe } return total; }
public static void main(String[] args) { List<Integer> ints = List.of(1, 2, 3); List<Double> doubles = List.of(1.5, 2.5);
System.out.println(sum(ints)); // accepts List<Integer> System.out.println(sum(doubles)); // accepts List<Double> }}This fixes the invariance problem from earlier:
- The same
sumnow takes aList<Integer>and aList<Double>. - Every element is at least a
Number, son.doubleValue()is always safe. - This is called a producer: the list produces values that you read out.
Output
6.04.0You cannot add to a List<? extends Number>
Here is the part people trip over. You can read from an extends-wildcard list, but you cannot add to it. The list holds numbers, so why not add a number? This is the code that fails.
import java.util.List;
public class Main {
static void addOne(List<? extends Number> numbers) { numbers.add(1); // compile error }}The compiler stops you.
Compile error
Main.java: error: incompatible types: int cannot be converted to CAP#1 numbers.add(1); ^ where CAP#1 is a fresh type-variable: CAP#1 extends Number from capture of ? extends NumberHere is the reasoning:
? extends Numbermeans βsome subtype of Number, but the compiler does not know whichβ.- The real list might be a
List<Double>. Adding anIntegerwould break it. - Since the compiler cannot tell the real subtype, it forbids all adds to be safe.
- So upper-bounded means read, not write.
π₯ The lower-bounded wildcard: List<? super Integer>
What if you want the opposite, a method that writes integers into a list? For that you use the lower bound, List<? super Integer>.
- It means βa list of
Integeror any supertype ofIntegerβ. supersets a lower limit: the unknown type sits atIntegeror above it.- So it accepts
List<Integer>,List<Number>, andList<Object>, which can all hold anInteger.
Here is a method that adds a few integers into a list.
import java.util.ArrayList;import java.util.List;
public class Main {
static void addNumbers(List<? super Integer> list) { list.add(1); // safe: the list holds Integer or a supertype list.add(2); list.add(3); }
public static void main(String[] args) { List<Integer> ints = new ArrayList<>(); List<Number> nums = new ArrayList<>(); List<Object> objs = new ArrayList<>();
addNumbers(ints); // List<Integer> works addNumbers(nums); // List<Number> works addNumbers(objs); // List<Object> works
System.out.println(ints); System.out.println(nums); System.out.println(objs); }}Now adding is allowed:
- Whatever the real list is, it accepts an
Integer, becauseIntegerfits a list ofInteger,Number, orObject. - The same
addNumberswrites into all three. - This is called a consumer: the list consumes the values you put in.
Output
[1, 2, 3][1, 2, 3][1, 2, 3]There is a trade. With ? super Integer you can add integers, but reading back gives you only Object. The list might be a List<Object>, so Object is the most the compiler can promise. That is the mirror image of extends: extends is good for reading, super is good for writing.
π§ The PECS rule
A tiny phrase captures the pattern. Once it sticks, you never forget which to use.
PECS = Producer Extends, Consumer Super.
- If a parameter produces values you read out, use
? extends. Likesum, which reads numbers out. - If a parameter consumes values you put in, use
? super. LikeaddNumbers, which writes integers in. - If you only inspect the list, the plain
?is enough.
Here is the classic PECS shape: a copy method that reads from a source and writes into a destination.
import java.util.ArrayList;import java.util.List;
public class Main {
// src produces values (extends), dest consumes values (super) static <T> void copy(List<? extends T> src, List<? super T> dest) { for (T item : src) { dest.add(item); } }
public static void main(String[] args) { List<Integer> src = List.of(10, 20, 30); List<Number> dest = new ArrayList<>();
copy(src, dest); System.out.println(dest); }}- The source is the producer, so it uses
? extends Tand we only read from it. - The destination is the consumer, so it uses
? super Tand we only write to it. - One method copies a
List<Integer>into aList<Number>, the very thing invariance blocked at the start.
Output
[10, 20, 30]π§ Wildcards in your method parameters
The real lesson: wildcards mostly belong in method parameters. They let one method accept many related list types. This program shows all three side by side.
import java.util.ArrayList;import java.util.List;
public class Main {
static void printAll(List<?> list) { // unbounded: just inspect for (Object item : list) System.out.println(item); }
static double sum(List<? extends Number> nums) { // extends: read numbers double total = 0; for (Number n : nums) total += n.doubleValue(); return total; }
static void fill(List<? super Integer> list) { // super: write integers for (int i = 1; i <= 3; i++) list.add(i); }
public static void main(String[] args) { List<Integer> box = new ArrayList<>(); fill(box); // write into it System.out.println(sum(box)); // read out of it printAll(box); // just inspect it }}One list flows through all three methods:
fillwrites into it using? super Integer.sumreads from it using? extends Number.printAllinspects it using the bare?.- Each method asks for exactly the flexibility it needs and no more.
Output
6.0123When to skip the wildcard
If your method needs to both read a specific type and write the same specific type, do not use a wildcard at all. Use a named type parameter like <T> instead. Wildcards shine when you only read, or only write, not both.
β οΈ Common Mistakes
A few wildcard slip-ups that catch almost everyone.
Adding to a List<? extends Number>. These wildcards are for reading. The real subtype is unknown, so the compiler blocks all adds.
// β Wrong: cannot add to an extends-wildcard list, the real type is unknownstatic void addOne(List<? extends Number> list) { list.add(1); // compile error}
// β
Correct: to write, use a lower bound with superstatic void addOne(List<? super Integer> list) { list.add(1); // fine, the list accepts Integer}Confusing extends with super. Remember PECS. extends is for producers you read from. super is for consumers you write to. Mixing them up gives a method that compiles but refuses the operation you wanted.
// β Wrong: you want to WRITE integers, but extends blocks writingstatic void store(List<? extends Integer> list) { list.add(5); // compile error}
// β
Correct: use super when you writestatic void store(List<? super Integer> list) { list.add(5); // works}Expecting List<Number> to accept a List<Integer>. It will not, because of invariance. Use a wildcard to make the method flexible.
// β Wrong: rejects List<Integer> because of invariancestatic double sum(List<Number> nums) { ... }
// β
Correct: accepts List<Integer>, List<Double>, and morestatic double sum(List<? extends Number> nums) { ... }Reading a specific type out of a ? super list. A List<? super Integer> might really be a List<Object>, so reading gives you Object, not Integer. Do not expect the specific type back.
β Best Practices
Habits that make wildcards easy to get right.
- Follow PECS. Producer Extends, Consumer Super. Read, use
? extends. Write, use? super. This one rule answers most wildcard questions. - Use the unbounded
?for inspect-only methods. If you only read the size or print the contents,List<?>is the simplest and clearest choice. - Put wildcards on parameters, not return types. Returning a wildcard type just pushes the awkwardness onto your caller. Return a concrete type instead.
- Skip the wildcard when you read and write the same type. Reach for a named type parameter like
<T>in that case, not a wildcard. - Never try to add to a
? extendslist. Treat it as read-only and your code will stay clean. - Let wildcards widen what your methods accept. They are the cure for invariance, so use them to make one method serve many related list types.
π§© What Youβve Learned
Nice work, that is a tricky corner of Java handled. Letβs recap wildcards.
- β
The wildcard
?means some unknown type, and it fixes the rigidity caused by invariance. - β
Invariance is why a
List<Integer>is not aList<Number>; it keeps the list safe from wrong-type inserts. - β
The unbounded
List<?>accepts any list and is read-only-ish, you cannot add normal elements. - β
The upper bound
List<? extends Number>is a producer: read numbers out, but you cannot add. - β
The lower bound
List<? super Integer>is a consumer: write integers in, but reads only give youObject. - β PECS sums it up: Producer Extends, Consumer Super.
Check Your Knowledge
Test what you learned. Pick an answer for each question, then click Check.
- 1
Why is a List<Integer> not accepted where a List<Number> is expected?
Why: Generics are invariant: even though Integer is a Number, List<Integer> and List<Number> are different, unrelated types.
- 2
What can you do with a parameter of type List<? extends Number>?
Why: An upper-bounded wildcard is a producer: you can read elements as Number, but the compiler blocks adds because the real subtype is unknown.
- 3
Which wildcard lets you safely add Integer values into a list?
Why: A lower-bounded wildcard, List<? super Integer>, is a consumer: the list is guaranteed to accept Integer values.
- 4
What does the PECS rule stand for?
Why: PECS means Producer Extends, Consumer Super: use extends when reading, super when writing.
π Whatβs Next?
You now understand wildcards, the last big idea in generics. Next we step away from generics and start working with files, beginning with the class Java uses to represent a file or folder on disk.