🧪 Generics · Intermediate

PECS in Java

Producer Extends, Consumer Super.

🧩 The mysteryYou write a copy method. Which list gets ? extends, and which gets ? super? Get it wrong and half your callers can't use it. Four letters fix this forever.

PECS

Producer Extends, Consumer Super. If a parameter produces values your method reads, use **? extends T. If it consumes values your method writes, use ? super T. If it does both, use plain T**.

Producer → extends

drawAll only reads shapes from the list, so the list is a producer: List<? extends Shape>. Now callers can pass a List<Circle> and a List<Shape>.

static void drawAll(
        List<? extends Shape> shapes) {
    for (Shape s : shapes) s.draw();
}
drawAll(circles);  // List<Circle> ✓
🤔 Think first

Why not plain List<Shape>?

Circle extends Shape. Why wouldn't drawAll(List<Shape> shapes) accept a List<Circle>?

Think about it, then reveal the answer

Because generics are invariant: a List<Circle> is not a List<Shape>. The wildcard ? extends Shape is what opens the door for subtypes.

Consumer → super

A destination list you write into is a consumer: List<? super T>. The JDK's own Collections.copy follows PECS exactly: you can copy Integers into a List<Number> or List<Object>.

static <T> void copy(List<? super T> dest,
                     List<? extends T> src)
🔮 Predict it

Your turn

What does this print?

static <T> void moveAll(List<? extends T> src,
                        List<? super T> dst) {
    dst.addAll(src);
}
void main() {
    List<Object> sink = new ArrayList<>();
    sink.add(1);
    moveAll(List.of("b"), sink);
    System.out.println(sink);
}
  1. [1, b]
  2. [b]
  3. Compile error
Show the answer

src produces Strings (? extends T), dst consumes them (? super T). With T = String, a List<Object> is a perfectly good consumer: [1, b].

Comparators are consumers

A comparator consumes the elements it compares. That's why List.sort takes a **Comparator<? super E>**: anything that can compare Numbers can certainly compare Integers.

Comparator<Number> byValue =
    Comparator.comparingDouble(
        Number::doubleValue);
List<Integer> ints = new ArrayList<>();
ints.sort(byValue);   // ✓ ? super Integer
⚠️ The trap

Reading AND writing? No wildcard

If a method both reads from and writes to the same list, neither wildcard works: ? extends blocks the writes, ? super makes reads return Object. Use the exact type **T**.

static <T> void swapEnds(List<T> xs) {
    T first = xs.get(0);           // read
    xs.set(0, xs.get(xs.size() - 1));
    xs.set(xs.size() - 1, first);  // write
}
💼 In the real world

Designing APIs

PECS is what makes JDK methods like Collections.copy, addAll, List.sort and Collections.max so flexible. When you design a library method, applying PECS means callers never have to copy their lists just to fit your signature. It's a favourite interview question, too.

Key takeaways

  1. Read from it (producer) → ? extends T
  2. Write into it (consumer) → ? super T
  3. Both read and write → exact type T
  4. Comparators are consumers: Comparator<? super T>

💡 A producer is a tap you drink from; a consumer is a sink you pour into.

🤯 Did you know?

The PECS mnemonic was coined by Joshua Bloch in Effective Java (2nd edition, 2008). Bloch also designed the Collections Framework that uses it everywhere.

Practice questions

drawAll(...) only READS shapes from a list and draws them. Which parameter type lets callers pass a List<Circle> as well as a List<Shape>?

  1. List<Shape>
  2. List<? extends Shape>
  3. List<? super Shape>
  4. List<Object>
Check your answer

List<? extends Shape>. The list is a producer of Shapes, so ? extends Shape. A plain List<Shape> would reject a List<Circle> because generics are invariant.

Complete the JDK's Collections.copy signature.

static <T> void copy(List<___> dest,
                     List<? extends T> src)
  1. ? super T
  2. ? extends T
  3. T
  4. ?
Check your answer

? super T. dest is written to, so it's a consumer: ? super T. That allows copying a List<Integer> into a List<Number> or List<Object>.

Next: here's the twist. All those type arguments you've been checking? At runtime, they're gone.