Вопрос 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 и почему их следует избегать]]