Информационный портал по безопасности » Программирование » Использование generic wildcards для повышения удобства Java API

 

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

Автор: admin от 5-01-2014, 04:55, посмотрело: 1 313

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

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

        ... // и так далее
}


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

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 вместо Predicate, но в ответ получит ошибку компиляции:

no suitable method found for getAll(Predicate)

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

MyObjectStore<Long, Car> carsStore = ...;

Predicate<Vehicle> vp = mgr.getVehiclePredicate();

Collection<Car> cars = carsStore.getAll(vp);

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

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


С передаваемыми коллекциями та же история, только в обратную сторону. Здесь, в большинстве случаев, имеет смысл использовать ? extends T для типа элементов коллекции. Посудите сами: имея ссылку на MyObjectStore, пользователь вполне вправе положить в хранилище набор объектов Map (ведь 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 буквально означает «мапка с ключами типа K или любого из подтипов K и со значениями типа V или любого из подтипов V».

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.

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

Литература


  • Joshua Bloch — Effective Java (2nd Edition)

  • Maurice Naftalin, Philip Wadler — Java Generics and Collections


  • Источник: Хабрахабр

    Категория: Программирование

    Уважаемый посетитель, Вы зашли на сайт как незарегистрированный пользователь.
    Мы рекомендуем Вам зарегистрироваться либо войти на сайт под своим именем.

    Добавление комментария

    Имя:*
    E-Mail:
    Комментарий:
    • bowtiesmilelaughingblushsmileyrelaxedsmirk
      heart_eyeskissing_heartkissing_closed_eyesflushedrelievedsatisfiedgrin
      winkstuck_out_tongue_winking_eyestuck_out_tongue_closed_eyesgrinningkissingstuck_out_tonguesleeping
      worriedfrowninganguishedopen_mouthgrimacingconfusedhushed
      expressionlessunamusedsweat_smilesweatdisappointed_relievedwearypensive
      disappointedconfoundedfearfulcold_sweatperseverecrysob
      joyastonishedscreamtired_faceangryragetriumph
      sleepyyummasksunglassesdizzy_faceimpsmiling_imp
      neutral_faceno_mouthinnocent