Java Academy · Урок

Принцип PECS

Производитель — Extends, потребитель — Super

Урок 3 из 413 шагов

«Принцип 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 — локальная установка не требуется.

Все уроки этого курса

  1. Подстановочные типы с верхней границей
  2. Подстановочные типы с нижней границей
  3. Принцип PECS
  4. Подстановочные типы в API
← Назад к Java Academy