Этот пост для тех, кто работает над очередным API на языке Java, либо пытается усовершенствовать уже существующий. Здесь будет дан простой совет, как с помощью конструкций ? extends T
и ? super T
можно значительно повысить удобство вашего интерфейса.
Перейти сразу к сути
Исходный API
Предположим, у вас есть интерфейс некого хранилища объектов, параметризованный, допустим, двумя типами: тип ключа (K
) и тип значения (V
). Интерфейс определяет набор методов для работы с данными в хранилище:
public interface MyObjectStore<K, V> { /** * Кладёт значение в хранилище по заданному ключу. * * @param key Ключ. * @param value Значение. */ void put(K key, V value); /** * Читает значение из хранилища по заданному ключу. * * @param key Ключ. * @return Значение либо null. */ @Nullable V get(K key); /** * Кладёт все пары ключ-значение в хранилище. * * @param entries Набор пар ключ-значение. */ void putAll(Map<K, V> entries); /** * Читает все значения из хранилища по заданным * ключам. * * @param keys Набор ключей. * @return Пары ключ-значение. */ Map<K, V> getAll(Collection<K> keys); /** * Читает из хранилища все значения, удовлетворяющие * заданному условию (предикату). * * @param p Предикат для проверки значений. * @return Значения, удовлетворяющие предикату. */ Collection<V> getAll(Predicate<V> p); ... // и так далее }
interface Predicate<E> { /** * Возвращает true, если значение удовлетворяет * условию, false в противном случае. * * @param exp Выражение для проверки. * @return true, если удовлетворяет; false, если нет. */ boolean apply(E exp); }
Интерфейс выглядит вполне адекватно и логично, пользователь без проблем может написать простой код для работы с хранилищем:
MyObjectStore<Long, Car> carsStore = ...; carsStore.put(20334L, new Car("BMW", "X5", 2013)); Car c = carsStore.get(222L); ...
Однако, в чуть менее тривиальных случаях клиент вашего API столкнётся с неприятными ограничениями.
Использование ? super T
Возьмём последний метод, который читает значения, удовлетворяющие предикату. Что с ним может быть не так? Берём, да и пишем:
Collection<Car> cars = carsStore.getAll(new Predicate<Car>() { @Override public boolean apply(Car exp) { ... // Здесь наша логика по выбору автомобиля. } });
Но дело в том, что у нашего клиента уже есть предикат для выбора автомобилей. Только он параметризован не классом Car
, а классом Vehicle
, от которого Car
унаследован. Он может попытаться запихать Predicate<Vehicle>
вместо Predicate<Car>
, но в ответ получит ошибку компиляции:
no suitable method found for getAll(Predicate<Vehicle>)
Компилятор говорит нам, что вызов метода невалиден, поскольку Vehicle
— это не Car
. Но ведь он является родительским типом Car
, а значит, всё, что можно сделать с Car
, можно сделать и с Vehicle
! Так что мы вполне могли бы использовать предикат по Vehicle
для выбора значений типа Car
. Просто мы не сказали компилятору об этом, и, тем самым, заставляем пользователя городить конструкции вроде:
final Predicate<Vehicle> vp = mgr.getVehiclePredicate(); Collection<Car> cars = carsStore.getAll(new Predicate<Car>() { @Override public boolean apply(Car exp) { return vp.apply(exp); } });
А ведь всё решается так просто! Нам нужно лишь слегка изменить сигнатуру метода:
Collection<V> getAll(Predicate<? super V> p);
Запись Predicate<? super V>
означает «предикат от V или любого супертипа V (вплоть до Object)». Данное изменение никак не ломает компиляцию существующего кода, зато устраняет абсолютно бессмысленные ограничения на параметр предиката. Клиент теперь может использовать свой предикат для Vehicle
совершенно свободно:
MyObjectStore<Long, Car> carsStore = ...; Predicate<Vehicle> vp = mgr.getVehiclePredicate(); Collection<Car> cars = carsStore.getAll(vp);
Мы обобщим данный приём чуть ниже, и запомнить его будет совсем просто.
Использование ? extends T
С передаваемыми коллекциями та же история, только в обратную сторону. Здесь, в большинстве случаев, имеет смысл использовать ? extends T
для типа элементов коллекции. Посудите сами: имея ссылку на MyObjectStore<Long, Vehicle>
, пользователь вполне вправе положить в хранилище набор объектов Map<Long, Car>
(ведь Car
— это подтип Vehicle
), но текущая сигнатура метода не позволяет ему это сделать:
MyObjectStore<Long, Vehicle> carsStore = ...; Map<Long, Car> cars = new HashMap<Long, Car>(2); cars.put(1L, new Car("Audi", "A6", 2011)); cars.put(2L, new Car("Honda", "Civic", 2012)); carsStore.putAll(cars); // Ошибка компиляции.
Чтобы снять это бессмысленное ограничение, мы, как и в предыдущем примере, расширяем сигнатуру нашего интерфейсного метода, используя wildcard ? extends T
для типа элемента коллекции:
void putAll(Map<? extends K, ? extends V> entries);
Запись Map<? extends K, ? extends V>
буквально означает «мапка с ключами типа 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<T>
илиPredicate<T>
), то в случае, если аргумент — производитель (producer), нужно использовать? extends T
, а если аргумент — потребитель (consumer), нужно использовать? super T
.
Производитель и потребитель, кто это такие? Очень просто: если метод читает данные из аргумента, то этот аргумент — производитель, а если метод передаёт данные в аргумент, то аргумент является потребителем. Важно заметить, что определяя производителя или потребителя, мы рассматриваем только данные типа T.
В нашем примере Predicate<T>
— это потребитель (метод getAll(Predicate<T>)
передаёт в этот аргумент данные типа T), а Map<K, V>
— производитель (метод putAll(Map<K, V>) читает данные типа T — в данном случае под T подразумевается K и V — из этого аргумента).
В случае, если аргумент является и потребителем, и производителем одновременно — например, если метод одновременно и читает из коллекции, и пишет в неё (плохой стиль, но всякое бывает) — тогда его нужно оставить как есть.
С возвращаемыми значениями тоже ничего делать не нужно — никакого удобства использование wildcard-ов в этом случае пользователю не принесёт, а лишь вынудит его использовать wildcard-ы в собственном коде.
Вооружившись PECS-принципом, мы можем теперь пройтись по всем методам нашего MyObjectStore
интерфейса и сделать улучшения там, где это требуется. Методы put(K, V)
и get(K)
улучшений не требуют (т.к. они не имеют аргументов с параметризованным типом); методы putAll(Map<? extends K, ? extends V>)
и getAll(Predicate<? super V>)
мы уже и так улучшили, дальше некуда; а вот метод getAll(Collection<K>)
имеет аргумент-производитель с параметризованным типом, который мы можем расширить. Вместо
Map<K, V> getAll(Collection<K> keys);
делаем
Map<K, V> getAll(Collection<? extends K> keys);
и радуемся новому, более удобному API! (Заметьте, возвращаемое значение мы не трогаем!)
Другие примеры потребителя и производителя
Производителями могут быть не только коллекции. Самый очевидный пример производителя — это фабрика:
interface Factory<T> { /** * Создаёт новый экземпляр объекта заданного типа. * * @param args Аргументы. * @return Новый объект. */ T create(Object... args); }
Хорошим примером аргумента, являющегося и производителем, и потребителем, будет аргумент вот такого типа:
interface Cloner<T> { /** * Клонирует объект. * * @param obj Исходный объект. * @return Копия. */ T clone(T obj); }
Коллекция может быть потребителем в случае, если это ouput-коллекция, в которую метод складывает результат своей работы (хотя такой стиль в Java редко используется и считается плохим тоном).
Заключение
В этой статье мы познакомились с принципом PECS (Producer Extends Consumer Super) и научились его применять при разработке API на Java. Как показывает практика, даже в самых продвинутых программистских конторах об этом принципе некоторые разработчики не знают, и в результате проектируют не совсем удобное API. Но, к счастью, исправляются подобные ошибки очень легко, а запомнив мнемонику PECS однажды, вы уже просто не сможете не пользоваться ей в дальнейшем.
Литература
ссылка на оригинал статьи http://habrahabr.ru/post/207360/
Добавить комментарий