Question 17 Β· Section 20

What is PECS (Producer Extends Consumer Super)

Structured Java interview answer with junior, middle, and senior-level explanation.

Language versions: English Russian Ukrainian

🟒 Junior Level

PECS is a mnemonic rule for working with wildcards in Java generics:

  • Producer Extends β€” if you need to read data, use ? extends T
  • Consumer Super β€” if you need to write data, use ? super T
// Producer Extends β€” read only
List<? extends Number> numbers = List.of(1, 2, 3);
Number n = numbers.get(0);  // βœ… can read
// numbers.add(4);  // ❌ cannot write

// Consumer Super β€” write only
List<? super Integer> integers = new ArrayList<>();
integers.add(42);  // βœ… can write
// Integer i = integers.get(0);  // ❌ cannot read (only Object)

Simple analogy:

  • Producer (supplier) β€” gives data β†’ extends
  • Consumer (consumer) β€” accepts data β†’ super

🟑 Middle Level

How it works

Producer Extends:

// Reading from source β€” this is a producer
public static double sum(List<? extends Number> source) {
    return source.stream()
        .mapToDouble(Number::doubleValue)  // βœ… can read Number
        .sum();
}

List<Integer> ints = List.of(1, 2, 3);
List<Double> doubles = List.of(1.5, 2.5);

sum(ints);     // βœ… Integer extends Number
sum(doubles);  // βœ… Double extends Number

Consumer Super:

// Writing to target β€” this is a consumer
public static void addNumbers(List<? super Integer> target, int count) {
    for (int i = 0; i < count; i++) {
        target.add(i);  // βœ… can write Integer
    }
}

List<Integer> ints = new ArrayList<>();
List<Number> numbers = new ArrayList<>();
List<Object> objects = new ArrayList<>();

addNumbers(ints, 3);     // βœ… Integer super Integer
addNumbers(numbers, 3);  // βœ… Number super Integer
addNumbers(objects, 3);  // βœ… Object super Integer

Full PECS rule

// Both producer AND consumer β€” no wildcard
public static <T> void copy(List<T> dest, List<T> src) {
    dest.addAll(src);  // both reading and writing
}

// Correct signature (as in JDK Collections.max):
public static <T extends Comparable<? super T>> T max(Collection<? extends T> coll)
// ? super T allows compareTo to be inherited from a superclass.

// Consumer only β€” super
public static <T> void fill(List<? super T> list, T value, int count) {
    for (int i = 0; i < count; i++) {
        list.add(value);
    }
}

Common mistakes

  1. Confusing direction: ```java // ❌ Wrong List<? super Number> numbers = List.of(1, 2, 3); // error // numbers.get(0); // can only get Object

// βœ… Correct β€” producer uses extends List<? extends Number> numbers = List.of(1, 2, 3); Number n = numbers.get(0); // βœ…


2. **Both directions β€” no wildcard needed:**
```java
// ❌ Useless
public void process(List<? extends T> list) {
    list.add(item);  // ❌ cannot
}

// βœ… If you need both read and write β€” no wildcard
public <T> void process(List<T> list) {
    list.add(list.get(0));  // βœ… everything works
}

πŸ”΄ Senior Level

Internal Implementation

Compile-time check:

List<? extends Number> extendsList = new ArrayList<Integer>();
List<? super Number> superList = new ArrayList<Object>();

// Extends β€” compiler knows lower bound
Number n1 = extendsList.get(0);  // βœ… Number β€” minimum
// extendsList.add(42);  // ❌ type unknown

// Super β€” compiler knows upper bound
superList.add(42);  // βœ… Number will definitely fit
Object o = superList.get(0);  // βœ… Object β€” maximum

Capture conversion:

// Reverse for a list β€” both reading and writing
public static <T> void reverse(List<T> list) {
    int size = list.size();
    for (int i = 0; i < size / 2; i++) {
        T temp = list.get(i);
        list.set(i, list.get(size - 1 - i));
        list.set(size - 1 - i, temp);
    }
}

// Cannot use wildcard β€” need both read and write
// reverse(List<? extends T>) β€” cannot set
// reverse(List<? super T>) β€” cannot get specific type

Architectural Trade-offs

Situation Wildcard Read Write
Read only ? extends T βœ… T ❌
Write only ? super T Object βœ… T
Both <T> βœ… T βœ… T
Unknown type ? Object null only

Edge Cases

1. Nested PECS:

// Comparator β€” consumer
Comparator<? super T> comparator

// Factory β€” producer
public static <T> List<T> produce(List<? extends T> source) {
    return new ArrayList<>(source);
}

// Combination
public static <T> void sort(List<T> list, Comparator<? super T> comp) {
    list.sort(comp);  // list β€” both read/write, comp β€” consumer
}

2. Stream API:

// Collector<T, A, R> β€” A is accumulator (consumer)
public static <T> Collector<T, ?, List<T>> toList() {
    return Collectors.toList();
}

// ? β€” accumulator type unknown (implementation detail)

3. Functional interfaces:

// Function β€” producer (returns R)
Function<? super T, ? extends R>

// Consumer β€” consumer (accepts T)
Consumer<? super T>

// Supplier β€” producer (returns T)
Supplier<? extends T>

Performance

PECS:
- Runtime: Zero overhead
- Compile time: strict type checking
- Safety: prevents ClassCastException

Performance is the same for all wildcard types

Production Experience

JDK examples:

// Collections.copy β€” classic PECS
public static <T> void copy(
    List<? super T> dest,      // consumer β€” writing T
    List<? extends T> src      // producer β€” reading T
) {
    Iterator<? extends T> si = src.iterator();
    for (int i = 0; i < src.size(); i++) {
        dest.set(i, si.next());
    }
}

// Stream.collect
public final class Stream<T> {
    public <R, A> R collect(
        Collector<? super T, A, R> collector  // T β€” consumer
    ) { }
}

// Optional.orElseGet
public T orElseGet(Supplier<? extends T> supplier) {  // T β€” producer
    return value != null ? value : supplier.get();
}

Real-world cases:

// Repository pattern
public interface Repository<T> {
    // Producer β€” returns data
    Optional<? extends T> findById(Long id);
    List<? extends T> findAll();

    // Consumer β€” accepts data
    void save(T entity);
    void saveAll(List<? extends T> entities);
}

// Event Publisher
public class EventPublisher<T extends Event> {
    private final List<Consumer<? super T>> listeners = new ArrayList<>();

    public void subscribe(Consumer<? super T> listener) {
        listeners.add(listener);  // consumer
    }

    public void publish(T event) {
        listeners.forEach(l -> l.accept(event));
    }
}

Best Practices

// βœ… PECS for maximum flexibility
public static <T> void copy(List<? super T> dest, List<? extends T> src) { }

// βœ… Producer for reading
public double sum(List<? extends Number> numbers) { }

// βœ… Consumer for writing
public void fill(List<? super Integer> list, int value) { }

// βœ… Both β€” no wildcard
public <T> void swap(List<T> list, int i, int j) { }

// ❌ Extends for writing
// ❌ Super for reading a specific type
// ❌ Wildcard when you need both read and write

🎯 Interview Cheat Sheet

Must know:

  • PECS: Producer Extends β€” reading data, Consumer Super β€” writing data
  • List<? extends Number> β€” producer: read as Number, write forbidden
  • List<? super Integer> β€” consumer: write Integer, read only as Object
  • When you need both read and write β€” use <T>, no wildcard
  • JDK examples: Collections.copy(List<? super T>, List<? extends T>), Collections.max(Collection<? extends T>)
  • PECS β€” use-site variance, differs from Kotlin out T / in T (declaration-site)

Frequent follow-up questions:

  • Give a PECS example from JDK? β€” Collections.copy(dest, src), Stream.collect(Collector)
  • When is wildcard NOT needed? β€” When the method both reads and writes (swap, reverse)
  • What is nested PECS? β€” Function<? super T, ? extends R> β€” T consumer, R producer
  • Can you add null to List<? extends T>? β€” Yes, null is valid for any type

Red flags (DO NOT say):

  • ❌ β€œPECS prevents ClassCastException at runtime” β€” It’s a compile-time check
  • ❌ β€œExtends allows writing subtypes” β€” Extends forbids writing entirely
  • ❌ β€œSuper allows reading as T” β€” Super allows reading only as Object
  • ❌ β€œPECS is a design pattern” β€” It’s a mnemonic rule for wildcards

Related topics:

  • [[11. What are Generics in Java]]
  • [[13. What is type erasure]]
  • [[16. What is the difference between <? extends T> and <? super T>]]
  • [[19. What are raw types and why should you avoid them]]