📋 Раздел 20 · Вопрос #17

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

Если бы метод был объявлен без PECS (copy(List dest, List src)), код выше не скомпилировался бы, так как из-за инвариантности List несовместим с List.


🟢 Junior Level

PECS — это мнемоническое правило, сформулированное Джошуа Блохом в книге «Effective Java», для правильного выбора подстановочных знаков (wildcards) в сигнатурах методов:

Producer Extends, Consumer Super

  • Producer Extends (? extends T): если коллекция или параметр является поставщиком (источником) данных для вашего метода (метод только читает данные из него) $\rightarrow$ используйте ? extends T.
  • Consumer Super (? super T): если коллекция или параметр является приемником (потребителем) данных вашего метода (метод записывает данные в него) $\rightarrow$ используйте ? super T.
// Пример: метод копирования элементов из одного списка в другой
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
    for (T item : src) { // src — Producer (поставляет элементы типа T)
        dest.add(item);  // dest — Consumer (потребляет элементы типа T)
    }
}

List<Integer> integers = List.of(1, 2, 3);
List<Number> numbers = new ArrayList<>();

// Благодаря PECS мы можем скопировать целые числа в список чисел:
copy(numbers, integers); // ✅ dest = List<Number>, src = List<Integer>

Если бы метод был объявлен без PECS (copy(List<T> dest, List<T> src)), код выше не скомпилировался бы, так как из-за инвариантности List<Integer> несовместим с List<Number>.


🟡 Middle Level

1. Гибкость публичного API

Главная цель правила PECS — сделать публичные методы максимально гибкими для вызывающего кода, не нарушая типобезопасность.

Рассмотрим метод подсчета суммы:

// ❌ Плохой дизайн: жесткий инвариантный тип
public static double sumBad(List<Number> numbers) {
    double s = 0;
    for (Number n : numbers) s += n.doubleValue();
    return s;
}

List<Integer> ints = List.of(1, 2, 3);
// sumBad(ints); // ❌ Ошибка компиляции! List<Integer> НЕ является List<Number>!

// ✅ Идиоматичный дизайн по правилу PECS (numbers только читается -> Producer):
public static double sumGood(List<? extends Number> numbers) {
    double s = 0;
    for (Number n : numbers) s += n.doubleValue();
    return s;
}

sumGood(ints); // ✅ Успешно: работает для List<Integer>, List<Double>, List<Float>

2. Применение в функциональных интерфейсах JDK

Стандартные функциональные интерфейсы Java 8 полностью построены на PECS:

// Function потребляет аргумент T (Consumer) и поставляет результат R (Producer):
@FunctionalInterface
public interface Function<T, R> {
    // В методах высшего порядка аргументы типизируются по PECS:
    default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) { ... }
}

// Stream.map использует PECS:
<R> Stream<R> map(Function<? super T, ? extends R> mapper);

Это позволяет передать в поток Stream<Integer> функцию, принимающую более общий тип Number (контравариантность входа), и возвращающую более конкретный подтип (ковариантность выхода).


🔴 Senior Level

Золотое правило возвращаемых типов: «Не используйте Wildcards в Return Types»

[!IMPORTANT] Никогда не используйте wildcards в качестве возвращаемых типов методов! Правило Джошуа Блоха: «Be liberal in what you accept, and strict in what you return».

// ❌ Ужасный API: возвращаемый тип с wildcard
public List<? extends User> getActiveUsers() { ... }

// Вызывающий код превращается в кошмар:
List<? extends User> users = service.getActiveUsers();
// users.add(new User()); // ❌ Ошибка! В такой список ничего нельзя добавить!

Если метод возвращает List<? extends User>, клиентский код не получает никакой дополнительной гибкости. Напротив, компилятор принуждает разработчика объявлять переменные с ? extends, блокируя вызовы методов мутации и требуя явных кастов.

Возвращаемый тип метода всегда должен быть максимально конкретным:

// ✅ Правильно: возвращаем точный тип
public List<User> getActiveUsers() { ... }

Сложный пример из JDK: Collections.max

Рассмотрим сигнатуру метода Collections.max, представляющую собой вершину применения PECS в стандартной библиотеке:

public static <T extends Comparable<? super T>> T max(Collection<? extends T> coll)

Разбор каждого элемента:

  1. Collection<? extends T> coll: коллекция является поставщиком (Producer) элементов для поиска максимума. Мы только читаем из нее. Поэтому ? extends T.
  2. <T extends Comparable<? super T>>: интерфейс Comparable является потребителем (Consumer), так как его метод compareTo(T o) принимает объект на вход для сравнения. Если бы было написано Comparable<T>, то класс Dog extends Animal (где Animal implements Comparable<Animal>) не смог бы использовать этот метод, так как Dog реализует Comparable<Animal>, а не Comparable<Dog>. Вариант ? super T гарантирует поддержку полиморфных иерархий!

4 Tricky Questions

1. Почему Джошуа Блох в «Effective Java» категорически предостерегает от использования wildcards в возвращаемых типах методов?

Ответ: Возвращаемый тип с подстановочным знаком (public List<? extends User> getUsers()) не дает вызывающему коду абсолютно никаких преимуществ. Наоборот, он «заражает» клиентский код: вызывающий программист вынужден также объявлять переменные с wildcards, теряет возможность записывать новые элементы в коллекцию (ведь в ? extends User ничего нельзя положить, кроме null) и сталкивается с трудностями при передаче этой коллекции в другие методы.

Контракт метода должен следовать принципу Постела: будьте снисходительны к тому, что принимаете (используйте wildcards в аргументах), и строги к тому, что возвращаете (возвращайте точные типы List<User>).

2. Как правило PECS объясняет объявление метода map() в Java Stream API (map(Function<? super T, ? extends R> mapper))?

Ответ: Интерфейс Function выполняет две противоположные роли:

  1. Он потребляет (Consumer) исходный элемент потока $T$ для трансформации. В соответствии с PECS входной параметр должен быть ? super T. Это позволяет использовать функцию, принимающую более широкий супертип (например, метод Object.toString() или функцию Number -> ... для потока Stream<Integer>).
  2. Он поставляет (Producer) новый элемент типа $R$ в результирующий поток. В соответствии с PECS выходной параметр должен быть ? extends R. Это гарантирует, что если метод ожидает возврата Number, функция может вернуть конкретный подтип Integer или Double.

3. Если метод должен одновременно и читать элементы из переданного списка, и записывать новые элементы в него, какую конструкцию следует использовать?

Ответ: В этом случае подстановочные знаки (Wildcards) использовать нельзя.

Правило PECS гласит: extends — только для чтения, super — только для записи. Если структура данных одновременно выполняет обе роли (и Producer, и Consumer), никакое ослабление типов недопустимо, иначе нарушится инвариант типобезопасности. Метод обязан объявлять точный инвариантный тип без знаков вопроса:

public static <T> void swapAndDouble(List<T> list, int i, int j) {
    T item = list.get(i); // Читаем как T
    list.set(j, item);    // Записываем как T
}

4. Скомпилируется ли вызов addToList(new ArrayList<Cat>()), если метод объявлен как void addToList(List<? super Animal> list)?

Ответ: Нет, не скомпилируется.

Конструкция ? super Animal означает: «список некоторого типа, который является предком (суперклассом) для Animal». Допустимыми аргументами могут быть List<Animal> или List<Object>.

Класс Cat является потомком (подклассом) Animal, а не его предком. Если бы компилятор разрешил передать List<Cat> в метод, ожидающий List<? super Animal>, то метод имел бы полное право положить туда new Dog() (ведь собака — это Animal), что мгновенно разрушило бы память исходного списка котов. Компилятор предотвратит такой вызов ошибкой несоответствия типов.


🎯 Шпаргалка для интервью

Главное о PECS

  • Формулировка: Producer Extends, Consumer Super.
  • Producer (? extends T): Источник данных. Метод только извлекает элементы. Запись запрещена.
  • Consumer (? super T): Приемник данных. Метод только сохраняет элементы. Чтение дает только Object.
  • И то, и другое: Не используйте wildcards! Используйте строгий инвариантный тип <T>.
  • Возвращаемые типы: Табу на wildcards. Возвращайте всегда точный конкретный тип (List<T>, а не List<? extends T>).
  • Сравнение: В компараторах всегда пишите Comparator<? super T>, чтобы потомки могли сравниваться компаратором базового класса.

Связанные темы