Java Generics Wildcards

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 allowed
nums.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 be

Here is what would go wrong:

  • Treat a List<Integer> as a List<Number> and you could add a Double through the Number view.
  • Later someone reading through the Integer view gets a Double and crashes.
  • So Java keeps generic types invariant: List<Integer> and List<Number> are different, unrelated types.
  • This keeps you safe, but makes methods rigid. List<Number> accepts only List<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

1
2
3
Alex
Riya

There 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, because null fits 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 of Number or any subtype of Number”.
  • It accepts List<Integer>, List<Double>, List<Long>, and List<Number> itself.
  • extends sets an upper limit: the unknown type sits at Number or 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 sum now takes a List<Integer> and a List<Double>.
  • Every element is at least a Number, so n.doubleValue() is always safe.
  • This is called a producer: the list produces values that you read out.

Output

6.0
4.0

You 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 Number

Here is the reasoning:

  • ? extends Number means β€œsome subtype of Number, but the compiler does not know which”.
  • The real list might be a List<Double>. Adding an Integer would 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 Integer or any supertype of Integer”.
  • super sets a lower limit: the unknown type sits at Integer or above it.
  • So it accepts List<Integer>, List<Number>, and List<Object>, which can all hold an Integer.

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, because Integer fits a list of Integer, Number, or Object.
  • The same addNumbers writes 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. Like sum, which reads numbers out.
  • If a parameter consumes values you put in, use ? super. Like addNumbers, 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 T and we only read from it.
  • The destination is the consumer, so it uses ? super T and we only write to it.
  • One method copies a List<Integer> into a List<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:

  • fill writes into it using ? super Integer.
  • sum reads from it using ? extends Number.
  • printAll inspects it using the bare ?.
  • Each method asks for exactly the flexibility it needs and no more.

Output

6.0
1
2
3

When 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 unknown
static void addOne(List<? extends Number> list) {
list.add(1); // compile error
}
// βœ… Correct: to write, use a lower bound with super
static 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 writing
static void store(List<? extends Integer> list) {
list.add(5); // compile error
}
// βœ… Correct: use super when you write
static 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 invariance
static double sum(List<Number> nums) { ... }
// βœ… Correct: accepts List<Integer>, List<Double>, and more
static 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 ? extends list. 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 a List<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 you Object.
  • βœ… 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. 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. 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. 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. 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.

Java File Class

Share & Connect