Главная
Блог разработчиков phpBB
 
+ 17 предустановленных модов
+ SEO-оптимизация форума
+ авторизация через соц. сети
+ защита от спама

Применение generic wildcards для возрастания комфорта Java API

Anna | 3.06.2014 | нет комментариев

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

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

Источник: programmingmaster.ru

Оставить комментарий
Форум phpBB, русская поддержка форума phpBB
Рейтинг@Mail.ru 2008 - 2017 © BB3x.ru - русская поддержка форума phpBB