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

—

от автора

Доброго времени суток!

Этот пост для тех, кто работает над очередным 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);          ... // и так далее } 

Определение Predicate

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 однажды, вы уже просто не сможете не пользоваться ей в дальнейшем.

Литература

  1. Joshua Bloch — Effective Java (2nd Edition)
  2. Maurice Naftalin, Philip Wadler — Java Generics and Collections

ссылка на оригинал статьи http://habrahabr.ru/post/207360/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *