📋 Розділ 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>, щоб нащадки могли порівнюватися компаратором базового класу.

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