Что такое принцип PECS в Java?

Что такое принцип PECS в Java? фото
Author: Tatyana Milkina
  1. Что такое PECS?
  2. Producer
  3. Consumer
  4. Выводы

1. Что такое PECS?

PECS расшифровывается как Producer extends Consumer super

Принцип PECS непосредственно связан с коллекциями и обобщениями. Когда мы создаем коллекцию желательно делать ее типизированной. Допустим, здесь создается коллекция List с обобщенным типом String:

List<String> strings = new ArrayList<>();

Это значит, что в этой коллекции будут содержаться объекты типа String. А в следующей коллекции integers будут содержаться объекты типа Integer:

Set<Integer> integers = new HashSet<>();

Но иногда мы не знаем какой тип нам нужен. Поэтому в принципе, мы можем не указывать обобщённый тип при объявлении коллекции, как допустим вот здесь для переменной queue:

Queue queue = new ArrayDeque();

Ошибки не будет, но будет warning. Так делать нежелательно, потому что с такой коллекцией очень неудобно работать. Когда вы будете получать элементы из этой коллекции, вы не будете знать, какого типа объекты там находятся. И их нужно будет приводить к определенному типу.

Но когда мы создаём коллекцию, мы всё-таки не всегда знаем какого типа объекты там будут находиться. В этом случае лучше использовать wildcard, либо по-другому мета символ. Wildcard - это вопросительный знак в ромбовидном операторе. Есть три варианта использования этого wildcard:

List<?> persons = ...// unbounded wildcard
List<? extends Employee> employees = ... // upper bounded wildcard
List<? super Employee> managers = ...// // lower bounded wildcard

Первый вариант - unbounded wildcard, то есть неограниченный wildcard.

Второй вариант - мы используем ключевое слово extends, как допустим для переменной employees. Это значит, что эта коллекция будет содержать объекты либо Employee, либо классов, которые наследуют Employee. Все примеры в этом уроке используют такую простенькую иерархию:

Иерархия классов PECS фото

У нас есть класс Person, его расширяет класс Employee, и Manager расширяет класс Employee.

В нашем случае коллекция employees будет содержать объекты Employee либо Manager. Ещё такой вариант ограничения называется upper bounded wildcard, то есть мы ограничиваем сверху. Почему сверху? Смотрите - класс Person, который находится сверху, не используется. То есть, ограничение происходит сверху.

И третий вариант использования wildcard - с ключевым словом super. В этом случае мы указываем, что эта коллекция managers будет содержать объекты Employee, либо его супер классы. В нашем случае - это Employee либо Person. Такой вариант называется lower bounded wildcard, то есть ограничение идёт снизу - нижний класс Manager у нас не будет участвовать.

Более подробно о дженериках и wildcard можно прочитать в Дженерики Java

2. Producer

Итак, начнём с Producer. Если мы используем ключевое слово extends, значит мы создаём Producer. Producer что-то производит, он отдаёт какие-то элементы, но в него мы не можем добавлять элементы. Давайте рассмотрим такой простенький пример:

 public static void processUpperBounded(List<? extends Employee> employees) {
        employees.add(new Employee());//compilation error
        employees.add(null);
        Employee employee = employees.getFirst();
}

У нас есть метод processUpperBounded, с параметром метода List, в котором wildcard ограничен с помощью extends. Коллекция employees является Producer, то есть, если мы добавим сюда новый элемент Employee, у нас будет ошибка компиляции. Но в такую коллекцию мы можем добавлять элементы типа null. Так как коллекция является продюсером, мы можем получать из нее элементы. Допустим, мы вызываем метод getFirst(), который возвращает нам элемент типа Employee.

Теперь давайте посмотрим, какие коллекции мы можем передать на вход такого метода. Создадим три коллекции - persons, employees и managers соответственно с типами Person, Employee и Manager:

List<Person> persons = new ArrayList<>();
List<Employee> employees = new ArrayList<>();
List<Manager> managers = new ArrayList<>();

И вызовем наш метод processUpperBounded, передавая все эти три коллекции:

processUnbounded(persons);//compilation error
processUnbounded(employees);
processUnbounded(managers);

В первом случае у нас будет ошибка компиляции - коллекцию типа Person мы передать не можем, потому что коллекция ограничена с помощью extends. На вход нашего метода processUpperBounded мы можем передать только коллекцию, которая содержит Employee либо Manager, но не Person.

Давайте рассмотрим более осмысленный пример. В следующем коде создаются две коллекции, одна с типом Integer, а вторая с типом Double:

List<Integer> iList = new ArrayList<>();
iList.add(1);
iList.add(5);
iList.add(8);
iList.add(9);

System.out.println(getAverage(iList));

List<Double> iDouble = new ArrayList<>();
iDouble.add(1.7);
iDouble.add(5.7);
iDouble.add(8.3);
iDouble.add(9.2);
System.out.println(getAverage(iDouble));

И, представьте, что нам нужно и для одной, и для другой коллекции подсчитать среднее арифметическое. Если не использовать wildcard, то нам нужно писать два отдельных метода - для типа Integer и для типа Double. Это не есть хорошо, так как мы дублируем код. Но мы можем использовать wildcard ограниченный с помощью extends, в нашем случае extends Number. И Integer, и Double являются наследниками класса Number. И на вход метода мы можем передавать коллекции которые содержат Number, либо наследников Number - например Integer, Double, Float, Short:

 public static Number getAverage(List<? extends Number> list) {
        double result = 0;
        Number average;
        for (Number d : list) {
            result += d.doubleValue();
        }
        average = result / list.size();
        return average;
 }

3. Consumer

Переходим к Consumer. Если мы используем для ограничения слово super, это значит, что мы создаём Consumer. Давайте посмотрим на метод processLowerBounded:

public static void processLowerBounded(List<? super Employee> employees) {
        employees.add(new Person()); //compilation error
        employees.add(new Employee());
        employees.add(new Manager());
        Object employee = employees.getFirst();
        if (employee instanceof Employee) {
            Employee myEmployee = (Employee) employee;
        }
        Employee employee = employees.getFirst();//compilation error
}

Параметром этого метода является List с wildcard, ограниченный с ключевым словом super. Давайте посмотрим, какие коллекции мы можем передать на вход этого метода. Опять создаем здесь три коллекции - persons, employees и managers:

List<Person> persons = new ArrayList<>();
List<Employee> employees = new ArrayList<>();
List<Manager> managers = new ArrayList<>();

Мы можем на вход  метода передать коллекцию типа Person и коллекцию с типом Employee. Но мы не можем передать коллекцию, которая содержит объекты типа Manager, у нас будет ошибка компиляции:

processLowerBounded(persons);
processLowerBounded(employees);
processLowerBounded(managers);//compilation error

Wildcard ограничен по нижней границе ключевым словом super, поэтому можно передавать коллекции с объектами либо Person, либо Employee, но не Manager.

Если коллекция является Consumer, это значит, что в такую коллекцию мы можем добавлять объекты, но не получать их. В методе processLowerBounded мы пытаемся добавить в нашу коллекцию employees объекты типа Person, Employee и Manager. И посмотрите, что интересного - когда мы добавляем Person, будет ошибка компиляции, но мы можем добавить объекты Employee и Manager. Здесь я хочу обратить внимание на такой тонкий момент - когда мы передаём на вход метода коллекции, мы можем в employees присвоить только коллекции, которые содержат объекты типа Person и Employee, но не Manager. Но когда мы в эту коллекцию добавляем объекты, то мы не можем добавить сюда значение типа Person, только Employee и Manager. Почему так происходит? Принцип PECS применим при присвоении одной коллекции другой:

List<? extends Employee> list1 = employees;
List<? extends Employee> list2 = managers;
List<? super Employee> list3 = persons;

То есть в переменную, которая объявлена как <? extends Employee>, мы можем присвоить другую коллекцию, которая содержит объекты типа Employee, либо объекты типа Manager. В переменную list3 мы можем добавить коллекцию, которая содержит Person. Но принцип PECS не применим при добавлении объектов в коллекции:

public static void processLowerBounded(List<? super Employee> employees) {
   employees.add(new Person()); //compilation error
   employees.add(new Employee());
   employees.add(new Manager());...

В коллекцию, объявленную как <? super Employee>, мы можем добавить только Employee и Manager. Почему мы не можем добавить объект Person в эту коллекцию? На этапе компиляции компилятор не знает какого именно типа объекты будут содержаться в employees. Но он чётко знает, что это либо Employee, либо Person, либо Object. Если это будет коллекция содержащая тип Employee, то объект типа Person мы в такую коллекцию добавить не можем - не любой Person может быть Employee. Но мы можем добавить в такую коллекцию объект типа Employee и объект типа Manager. Даже если коллекция employees будет иметь тип Person, в такую коллекцию мы можем добавить и Employee, и Manager - они являются Person.

Давайте вернёмся к нашему методу processLowerBounded и несмотря на то, что это Consumer, всё-таки попытаемся получить из него какие-то элементы:

public static void processLowerBounded(List<? super Employee> employees) {
        ...
        Object employee = employees.getFirst();
        if (employee instanceof Employee) {
            Employee myEmployee = (Employee) employee;
        }
       Employee employee = employees.getFirst();//compilation error
}

С помощью метода getFirst() можно получить значение из нашей коллекции, но здесь есть одно 'но' - тип этого значения будет всегда Object. Если мы попытаемся получить это значение в переменную типа Employee, как в последней строчке метода, у нас будет ошибка компиляция. Вот почему это коллекция является Consumer. Но в принципе, в таком варианте тоже можно использовать эти переменные - просто проверяем тип этой переменной, и если она является Employee, то мы делаем приведение.

4. Выводы

Итак, давайте подытожим, что мы изучили на этом уроке:

  1. Мы используем Producer, то есть <? extends>, если необходимо получать значение из коллекции.
  2. Мы используем Consumer, то есть <? super>, если необходимо записывать значение в эту коллекцию.
  3. Чаще всего коллекции с wildcard используется в качестве параметров метода.
  4. Принцип PECS применим только при присвоении одной коллекции другой. 
Курс 'Java для начинающих' на Udemy Курс 'Java для начинающих' на Udemy
Читайте также:
Комментарии