Питання 17 · Розділ 20

Що таке PECS (Producer Extends Consumer Super)

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

Мовні версії: English Russian Ukrainian

🟢 Junior Level

PECS — це мнемонічне правило для роботи з wildcard в Java дженериках:

  • Producer Extends — якщо потрібно читати дані, використовуй ? extends T
  • Consumer Super — якщо потрібно записувати дані, використовуй ? super T
// Producer Extends — тільки читаємо
List<? extends Number> numbers = List.of(1, 2, 3);
Number n = numbers.get(0);  // ✅ можна читати
// numbers.add(4);  // ❌ не можна писати

// Consumer Super — тільки записуємо
List<? super Integer> integers = new ArrayList<>();
integers.add(42);  // ✅ можна писати
// Integer i = integers.get(0);  // ❌ не можна читати (тільки Object)

Проста аналогія:

  • Producer (постачальник) — віддає дані → extends
  • Consumer (споживач) — приймає дані → super

🟡 Middle Level

Як це працює

Producer Extends:

// Читаємо з source — це producer
public static double sum(List<? extends Number> source) {
    return source.stream()
        .mapToDouble(Number::doubleValue)  // ✅ можна читати 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:

// Записуємо в target — це consumer
public static void addNumbers(List<? super Integer> target, int count) {
    for (int i = 0; i < count; i++) {
        target.add(i);  // ✅ можна писати 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

Повне правило PECS

// І producer І consumer — без wildcard
public static <T> void copy(List<T> dest, List<T> src) {
    dest.addAll(src);  // і читаємо, і пишемо
}

// Правильна сигнатура (як в JDK Collections.max):
public static <T extends Comparable<? super T>> T max(Collection<? extends T> coll)
// ? super T дозволяє compareTo бути успадкованим від суперкласу.

// Тільки consumer — super
public static <T> void fill(List<? super T> list, T value, int count) {
    for (int i = 0; i < count; i++) {
        list.add(value);
    }
}

Типові помилки

  1. Плутанина напрямку: ```java // ❌ Неправильно List<? super Number> numbers = List.of(1, 2, 3); // помилка // numbers.get(0); // можна тільки Object

// ✅ Правильно — producer використовує extends List<? extends Number> numbers = List.of(1, 2, 3); Number n = numbers.get(0); // ✅


2. **Обидва напрямки — wildcard не потрібен:**
```java
// ❌ Марно
public void process(List<? extends T> list) {
    list.add(item);  // ❌ не можна
}

// ✅ Якщо потрібно і читати, і писати — без wildcard
public <T> void process(List<T> list) {
    list.add(list.get(0));  // ✅ можна все
}

🔴 Senior Level

Internal Implementation

Compile-time перевірка:

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

// Extends — компілятор знає lower bound
Number n1 = extendsList.get(0);  // ✅ Number — мінімум
// extendsList.add(42);  // ❌ тип невідомий

// Super — компілятор знає upper bound
superList.add(42);  // ✅ Number точно підійде
Object o = superList.get(0);  // ✅ Object — максимум

Capture conversion:

// Reverse для списку — і читаємо, і пишемо
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);
    }
}

// Не можна використовувати wildcard — потрібно і read, і write
// reverse(List<? extends T>) — не можна set
// reverse(List<? super T>) — не можна get конкретного типу

Архітектурні Trade-offs

Ситуація Wildcard Read Write
Тільки читання ? extends T ✅ T
Тільки запис ? super T Object ✅ T
І те, й інше <T> ✅ T ✅ T
Невідомий тип ? Object null only

Edge Cases

1. Nested PECS:

// Comparator — consumer
Comparator<? super T> comparator

// Фабрика — producer
public static <T> List<T> produce(List<? extends T> source) {
    return new ArrayList<>(source);
}

// Комбінація
public static <T> void sort(List<T> list, Comparator<? super T> comp) {
    list.sort(comp);  // list — і read/write, comp — consumer
}

2. Stream API:

// Collector<T, A, R> — A це accumulator (consumer)
public static <T> Collector<T, ?, List<T>> toList() {
    return Collectors.toList();
}

// ? — accumulator type невідомий (implementation detail)

3. Functional interfaces:

// Function — producer (повертає R)
Function<? super T, ? extends R>

// Consumer — consumer (приймає T)
Consumer<? super T>

// Supplier — producer (повертає T)
Supplier<? extends T>

Продуктивність

PECS:
- Runtime: Zero overhead
- Compile time: строга перевірка типів
- Безпека: запобігає ClassCastException

Продуктивність однакова для всіх wildcard типів

Production Experience

JDK приклади:

// Collections.copy — класичний PECS
public static <T> void copy(
    List<? super T> dest,      // consumer — записуємо T
    List<? extends T> src      // producer — читаємо 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();
}

Реальні кейи:

// Repository pattern
public interface Repository<T> {
    // Producer — повертає дані
    Optional<? extends T> findById(Long id);
    List<? extends T> findAll();

    // Consumer — приймає дані
    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 для максимальної гнучкості
public static <T> void copy(List<? super T> dest, List<? extends T> src) { }

// ✅ Producer для читання
public double sum(List<? extends Number> numbers) { }

// ✅ Consumer для запису
public void fill(List<? super Integer> list, int value) { }

// ✅ Обидва — без wildcard
public <T> void swap(List<T> list, int i, int j) { }

// ❌ Extends для запису
// ❌ Super для читання конкретного типу
// ❌ Wildcard коли потрібно і read, і write

🎯 Шпаргалка для співбесіди

Обов’язково знати:

  • PECS: Producer Extends — читаємо дані, Consumer Super — записуємо дані
  • List<? extends Number> — producer: read як Number, write заборонений
  • List<? super Integer> — consumer: write Integer, read тільки як Object
  • Коли потрібно і read, і write — використовувати <T>, без wildcard
  • JDK приклади: Collections.copy(List<? super T>, List<? extends T>), Collections.max(Collection<? extends T>)
  • PECS — use-site variance, відрізняється від Kotlin out T / in T (declaration-site)

Часті уточнюючі запитання:

  • Наведіть приклад PECS з JDK?Collections.copy(dest, src), Stream.collect(Collector)
  • Коли wildcard НЕ потрібен? — Коли метод і читає, і пише (swap, reverse)
  • Що таке nested PECS?Function<? super T, ? extends R> — T consumer, R producer
  • Чи можна додати null в List<? extends T>? — Так, null допустимий для будь-якого типу

Червоні прапорці (НЕ говорити):

  • ❌ “PECS запобігає ClassCastException в runtime” — Це compile-time перевірка
  • ❌ “Extends дозволяє записувати підтипи” — Extends забороняє запис повністю
  • ❌ “Super дозволяє читати як T” — Super дозволяє читати тільки як Object
  • ❌ “PECS — це патерн проектування” — Це мнемонічне правило для wildcard

Пов’язані теми:

  • [[11. Що таке дженерики (Generics) в Java]]
  • [[13. Що таке type erasure (стирання типів)]]
  • [[16. У чому різниця між <? extends T> і <? super T>]]
  • [[19. Що таке raw types і чому їх слід уникати]]