Принцип PECS
Производитель — Extends, потребитель — Super
«Принцип PECS» — бесплатный урок Java Academy на CoddyKit. Это урок 3 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения Java Academy, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс Java Academy содержит 4 уроков всего.
Что означает PECS
PECS означает «производитель — верхняя граница, потребитель — нижняя граница». Это мнемоническое правило из книги Джошуа Блоха «Эффективная Java».
- Если параметр производит значения T (Вы читаете из него), используйте
? extends T. - Если параметр потребляет значения T (Вы записываете в него), используйте
? super T.
public class Main {
public static void main(String[] args) {
System.out.println("Producer Extends, Consumer Super");
}
}Классический метод copy
Классический пример PECS — метод copy. Источник производит значения, поэтому используется ? extends T. Целевой объект потребляет значения, поэтому используется ? super T.
import java.util.ArrayList;
import java.util.List;
public class Main {
static <T> void copy(List<? extends T> src, List<? super T> dest) {
for (T t : src) dest.add(t);
}
public static void main(String[] args) {
List<Integer> src = List.of(1, 2, 3);
List<Object> dest = new ArrayList<>();
copy(src, dest);
System.out.println(dest);
}
}Почему оба подстановочных знака повышают гибкость
Благодаря PECS метод copy работает со множеством сочетаний типов: источник Integer можно использовать с целевым объектом типа Number или Object, а источник String — с целевым объектом типа CharSequence.
Без подстановочных знаков типы должны были бы полностью совпадать, что сильно ограничило бы повторное использование.
import java.util.ArrayList;
import java.util.List;
public class Main {
static <T> void copy(List<? extends T> src, List<? super T> dest) {
for (T t : src) dest.add(t);
}
public static void main(String[] args) {
List<Integer> ints = List.of(10, 20);
List<Number> nums = new ArrayList<>();
copy(ints, nums);
System.out.println(nums);
}
}Пример производителя
Методу, который только читает из своей коллекции, следует использовать ограничение сверху. Здесь мы суммируем значения источника.
import java.util.List;
public class Main {
static double sum(List<? extends Number> producer) {
double total = 0;
for (Number n : producer) total += n.doubleValue();
return total;
}
public static void main(String[] args) {
System.out.println(sum(List.of(1, 2, 3)));
System.out.println(sum(List.of(1.1, 2.2)));
}
}Пример потребителя
Методу, который только записывает в свою коллекцию, следует использовать ограничение снизу. Здесь мы помещаем значения в потребитель.
import java.util.ArrayList;
import java.util.List;
public class Main {
static void pushDigits(List<? super Integer> consumer) {
for (int i = 0; i < 5; i++) consumer.add(i);
}
public static void main(String[] args) {
List<Number> nums = new ArrayList<>();
pushDigits(nums);
System.out.println(nums);
}
}Обе роли: используйте обычный тип
Если параметр одновременно является производителем и потребителем, ему не следует использовать подстановочный знак.
Методу, который читает из одного списка и записывает в него, нужен точный тип T.
import java.util.ArrayList;
import java.util.List;
public class Main {
static <T> void rotate(List<T> list) {
if (list.isEmpty()) return;
T last = list.remove(list.size() - 1);
list.add(0, last);
}
public static void main(String[] args) {
List<Integer> list = new ArrayList<>(List.of(1, 2, 3));
rotate(list);
System.out.println(list);
}
}PECS в Comparator
JDK повсеместно применяет PECS. Collections.max принимает Comparator<? super T>, потому что компаратор потребляет значения T, чтобы сравнивать их.
import java.util.Comparator;
import java.util.List;
public class Main {
public static void main(String[] args) {
Comparator<Object> byHash = Comparator.comparingInt(Object::hashCode);
List<String> words = List.of("a", "bb", "ccc");
String max = java.util.Collections.max(words, byHash);
System.out.println(max);
}
}PECS в Stream.collect
Многие функциональные интерфейсы следуют правилу PECS. Consumer<? super T> может принимать обработчик, написанный для более общего типа.
import java.util.List;
import java.util.function.Consumer;
public class Main {
public static void main(String[] args) {
Consumer<Object> printer = o -> System.out.println("got: " + o);
List<String> items = List.of("x", "y");
items.forEach(printer); // forEach takes Consumer<? super T>
}
}Производители и потребители в Function
Function<? super T, ? extends R> — полная форма PECS: входные данные потребляются (ограничение снизу), а выходные производятся (ограничение сверху).
Благодаря этому преобразование может принимать входные данные более общего типа и возвращать результаты более узкого типа.
import java.util.function.Function;
public class Main {
static <T, R> R apply(Function<? super T, ? extends R> fn, T value) {
return fn.apply(value);
}
public static void main(String[] args) {
Function<Object, Integer> len = o -> o.toString().length();
System.out.println(apply(len, "hello"));
}
}Возвращаемые типы: избегайте подстановочных знаков
Не используйте подстановочные знаки в возвращаемых типах. Они вынуждают вызывающий код тоже работать с подстановочными знаками, усложняя его.
Возвращайте конкретный тип, а подстановочные знаки оставляйте в параметрах, где они повышают гибкость.
import java.util.ArrayList;
import java.util.List;
public class Main {
// Good: concrete return type
static List<Number> doubled(List<? extends Number> src) {
List<Number> out = new ArrayList<>();
for (Number n : src) out.add(n.doubleValue() * 2);
return out;
}
public static void main(String[] args) {
System.out.println(doubled(List.of(1, 2, 3)));
}
}Полное объединение по PECS
Объединим всё вместе: метод merge читает данные из двух источников и записывает их в один потребитель.
import java.util.ArrayList;
import java.util.List;
public class Main {
static <T> void merge(List<? extends T> a, List<? extends T> b, List<? super T> out) {
for (T t : a) out.add(t);
for (T t : b) out.add(t);
}
public static void main(String[] args) {
List<Integer> a = List.of(1, 2);
List<Integer> b = List.of(3, 4);
List<Number> out = new ArrayList<>();
merge(a, b, out);
System.out.println(out);
}
}Быстрая проверка
Примените правило PECS.
Итоги
Вы изучили принцип PECS:
- Производитель — верхняя граница: для параметров только для чтения используется
? extends T. - Потребитель — нижняя граница: для параметров только для записи используется
? super T. - Для параметров, выполняющих обе роли, используется обычный параметр типа.
- Избегайте подстановочных знаков в возвращаемых типах.
Далее применим эти идеи к проектированию реальных API.
public class Main {
public static void main(String[] args) {
System.out.println("PECS recap: Producer Extends, Consumer Super");
}
}Изучай Java с ИИ-репетитором — бесплатно
Пиши и запускай код прямо в браузере, получай мгновенную помощь от ИИ-репетитора 24/7 и продолжи учиться на сайте или в приложении.
- Курсы
- 104
- Уроки
- 374
Часто задаваемые вопросы
Урок «Принцип PECS» бесплатный?
Да — полный текст урока «Принцип PECS» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс Java Academy, подпишись на CoddyKit PRO. Курс Java Academy содержит 4 уроков всего.
Чему я научусь в уроке «Принцип PECS»?
Производитель — Extends, потребитель — Super Ты практикуешь Java Academy с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.
Нужен ли мне опыт, чтобы начать Java Academy?
Предыдущий опыт не требуется. Java Academy на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 3 из 4.
Сколько времени занимает урок «Принцип PECS»?
Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.
Можно ли писать и запускать код в этом уроке Java Academy?
Да. Каждый урок Java Academy включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.
Все уроки этого курса
- Подстановочные типы с верхней границей
- Подстановочные типы с нижней границей
- Принцип PECS
- Подстановочные типы в API