Java generic extends and super

Generics: super, extends, list

По смыслу, это что-то вроде шаблона «*», который совпадает с чем угодно.

Представь, что у тебя есть класс «Warrior(Воин)» и метод, который вычисляет, какой из двух воинов сильнее. Вот как, например, это могло бы выглядеть:

class WarriorManager < public static boolean fight(Warrior w1, Warrior w2) < return w1.power > w2.power; > >
MagicWarrior mag = new MagicWarrior(); ArcherWarrior archer = new ArcherWarrior(); boolean isMagicCooler = WarriorManager.fight(mag, archer);

MagicWarrior и ArcherWarrior – это классы-наследники от Warrior .

Немного простовато, но для примера сойдет.

— И вот допустим, ты решил сделать аналогичный метод, где уже дерутся воины стенка на стенку.

class WarriorManager < public static boolean fight(Warrior w1, Warrior w2) < return w1.power > w2.power; > public static boolean fight(List w1, List w2) < return  > >
ArrayListMagicWarrior> magi = new ArrayListMagicWarrior>(); for(int i=0;i<10;i++) magi.add(new MagicWarrior()); ArrayListArcherWarrior> archers = new ArrayListArcherWarrior>(); for(int i=0;i <10;i++) archers.add(new ArcherWarrior()); boolean isMagicCooler = WarriorManager.fight(magi, archers); //ошибка компиляции!

И вот тут ты встречаешь ошибку компиляции и недоумеваешь, а что не так-то?

Все дело в том, что MagicWarrior – это наследник Warrior и его объекты можно передавать в метод fight(Warrior, Warrior)

А вот List — это не наследник List . И передавать его туда нельзя!

— А вот так. То List и то – List. Пусть и с параметрами.

— Да, а я как-то сразу не обратил внимание. И что, есть уже решение этой проблемы?

— Ага. Для этого используется более сложная конструкция. Выглядит это так:

class WarriorManager < public static boolean fight(Warrior w1, Warrior w2) < return w1.power > w2.power; > public static boolean fight(ListWarrior> w1, ListWarrior> w2) < return … >>

«? extends Warrior» обозначает «любой тип, унаследованный от Warrior ».

Т.е. туда можно передать список List и список List, и все будет работать.

— Так это же отлично. Чем меньше всяких таких проблем, тем лучше.

— А если мне не нужен наследник Warrior? Что если я хочу, чтобы в метод можно было передать любой List с любым параметром? Так можно?

Читайте также:  Javascript найти регулярные выражения

— Да, для описания этой ситуации существует две записи:

Они эквивалентны по смыслу, поэтому обычно используют вторую.

— Спасибо, Элли, действительно узнал сегодня много нового.

Источник

Использование generic wildcards для повышения удобства Java API

Этот пост для тех, кто работает над очередным API на языке Java, либо пытается усовершенствовать уже существующий. Здесь будет дан простой совет, как с помощью конструкций ? extends T и ? super T можно значительно повысить удобство вашего интерфейса.

Перейти сразу к сути

Исходный API

Предположим, у вас есть интерфейс некого хранилища объектов, параметризованный, допустим, двумя типами: тип ключа ( K ) и тип значения ( V ). Интерфейс определяет набор методов для работы с данными в хранилище:

public interface MyObjectStore  < /** * Кладёт значение в хранилище по заданному ключу. * * @param key Ключ. * @param value Значение. */ void put(K key, V value); /** * Читает значение из хранилища по заданному ключу. * * @param key Ключ. * @return Значение либо null. */ @Nullable V get(K key); /** * Кладёт все пары ключ-значение в хранилище. * * @param entries Набор пар ключ-значение. */ void putAll(Mapentries); /** * Читает все значения из хранилища по заданным * ключам. * * @param keys Набор ключей. * @return Пары ключ-значение. */ Map getAll(Collection keys); /** * Читает из хранилища все значения, удовлетворяющие * заданному условию (предикату). * * @param p Предикат для проверки значений. * @return Значения, удовлетворяющие предикату. */ Collection getAll(Predicate p); . // и так далее > 

Интерфейс выглядит вполне адекватно и логично, пользователь без проблем может написать простой код для работы с хранилищем:

MyObjectStore carsStore = . ; carsStore.put(20334L, new Car("BMW", "X5", 2013)); Car c = carsStore.get(222L); . 

Однако, в чуть менее тривиальных случаях клиент вашего API столкнётся с неприятными ограничениями.

Использование ? super T

Возьмём последний метод, который читает значения, удовлетворяющие предикату. Что с ним может быть не так? Берём, да и пишем:

Collection cars = carsStore.getAll(new Predicate() < @Override public boolean apply(Car exp) < . // Здесь наша логика по выбору автомобиля. >>); 

Но дело в том, что у нашего клиента уже есть предикат для выбора автомобилей. Только он параметризован не классом Car , а классом Vehicle , от которого Car унаследован. Он может попытаться запихать Predicate вместо Predicate , но в ответ получит ошибку компиляции:

no suitable method found for getAll(Predicate)

Компилятор говорит нам, что вызов метода невалиден, поскольку Vehicle — это не Car . Но ведь он является родительским типом Car , а значит, всё, что можно сделать с Vehicle , можно сделать и с Car ! Так что мы вполне могли бы использовать предикат по Vehicle для выбора значений типа Car . Просто мы не сказали компилятору об этом, и, тем самым, заставляем пользователя городить конструкции вроде:

final Predicate vp = mgr.getVehiclePredicate(); Collection cars = carsStore.getAll(new Predicate() < @Override public boolean apply(Car exp) < return vp.apply(exp); >>); 

А ведь всё решается так просто! Нам нужно лишь слегка изменить сигнатуру метода:

Collection getAll(Predicate p); 

Запись Predicate означает «предикат от V или любого супертипа V (вплоть до Object)». Данное изменение никак не ломает компиляцию существующего кода, зато устраняет абсолютно бессмысленные ограничения на параметр предиката. Клиент теперь может использовать свой предикат для Vehicle совершенно свободно:

MyObjectStore carsStore = . ; Predicate vp = mgr.getVehiclePredicate(); Collection cars = carsStore.getAll(vp); 

Мы обобщим данный приём чуть ниже, и запомнить его будет совсем просто.

Читайте также:  Javascript document style width

Использование ? extends T

С передаваемыми коллекциями та же история, только в обратную сторону. Здесь, в большинстве случаев, имеет смысл использовать ? extends T для типа элементов коллекции. Посудите сами: имея ссылку на MyObjectStore , пользователь вполне вправе положить в хранилище набор объектов Map (ведь Car — это подтип Vehicle ), но текущая сигнатура метода не позволяет ему это сделать:

MyObjectStore carsStore = . ; Map cars = new HashMap(2); cars.put(1L, new Car("Audi", "A6", 2011)); cars.put(2L, new Car("Honda", "Civic", 2012)); carsStore.putAll(cars); // Ошибка компиляции. 

Чтобы снять это бессмысленное ограничение, мы, как и в предыдущем примере, расширяем сигнатуру нашего интерфейсного метода, используя wildcard ? extends T для типа элемента коллекции:

Запись Map буквально означает «мапка с ключами типа K или любого из подтипов K и со значениями типа V или любого из подтипов V».

Принцип PECS — Producer Extends Consumer Super

Настало время вывести общий принцип, благодаря которому мы всегда будем писать интерфейсы, абсолютно безопасные с точки зрения типов, но при этом не имеющие бессмысленных и создающих неудобства ограничений.

Этот принцип Joshua Bloch называет PECS (Producer Extends Consumer Super), а авторы книги Java Generics and Collections (Maurice Naftalin, Philip Wadler) — Get and Put Principle. Но давайте остановимся на PECS, запомнить проще. Этот принцип гласит:

Если метод имеет аргументы с параметризованным типом (например, Collection или Predicate ), то в случае, если аргумент — производитель (producer), нужно использовать ? extends T , а если аргумент — потребитель (consumer), нужно использовать ? super T .

Производитель и потребитель, кто это такие? Очень просто: если метод читает данные из аргумента, то этот аргумент — производитель, а если метод передаёт данные в аргумент, то аргумент является потребителем. Важно заметить, что определяя производителя или потребителя, мы рассматриваем только данные типа T.

Читайте также:  Css красивая рамка вокруг блока

В нашем примере Predicate — это потребитель (метод getAll(Predicate) передаёт в этот аргумент данные типа T), а Map — производитель (метод putAll(Map) читает данные типа T — в данном случае под T подразумевается K и V — из этого аргумента).

В случае, если аргумент является и потребителем, и производителем одновременно — например, если метод одновременно и читает из коллекции, и пишет в неё (плохой стиль, но всякое бывает) — тогда его нужно оставить как есть.

С возвращаемыми значениями тоже ничего делать не нужно — никакого удобства использование wildcard-ов в этом случае пользователю не принесёт, а лишь вынудит его использовать wildcard-ы в собственном коде.

Вооружившись PECS-принципом, мы можем теперь пройтись по всем методам нашего MyObjectStore интерфейса и сделать улучшения там, где это требуется. Методы put(K, V) и get(K) улучшений не требуют (т.к. они не имеют аргументов с параметризованным типом); методы putAll(Map) и getAll(Predicate) мы уже и так улучшили, дальше некуда; а вот метод getAll(Collection) имеет аргумент-производитель с параметризованным типом, который мы можем расширить. Вместо

Map getAll(Collection keys); 
Map getAll(Collection keys); 

и радуемся новому, более удобному API! (Заметьте, возвращаемое значение мы не трогаем!)

Другие примеры потребителя и производителя

Производителями могут быть не только коллекции. Самый очевидный пример производителя — это фабрика:

Хорошим примером аргумента, являющегося и производителем, и потребителем, будет аргумент вот такого типа:

Коллекция может быть потребителем в случае, если это ouput-коллекция, в которую метод складывает результат своей работы (хотя такой стиль в Java редко используется и считается плохим тоном).

Заключение

В этой статье мы познакомились с принципом PECS (Producer Extends Consumer Super) и научились его применять при разработке API на Java. Как показывает практика, даже в самых продвинутых программистских конторах об этом принципе некоторые разработчики не знают, и в результате проектируют не совсем удобное API. Но, к счастью, исправляются подобные ошибки очень легко, а запомнив мнемонику PECS однажды, вы уже просто не сможете не пользоваться ей в дальнейшем.

Литература

Источник

Оцените статью