Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Джош Блох

.pdf
Скачиваний:
57
Добавлен:
08.03.2016
Размер:
27 Мб
Скачать
☆

Глава 5 • Средства обобщенного программирования (Generics)

так что ее wildcard тип не подойдет для этого. Приведем пример по­ лучившейся декларации метода:

// Wildcard-тип для параметра, служащего производителем Е static <Е> Е reduce(List<? extends Е> list, Function<E> f, E initVal)

Будет ли данное изменение на практике значить что-либо? Как оказывается, будет. Предположим, у нас есть List<Integer> и вы хо­ тите уменьшить его с помощью Function<Number>. При оригинальной декларации это компилироваться не будет, однако если вы добавите ограниченный wildcard-тип, то все будет иначе.

Давайте рассмотрим метод union из статьи 27. Пример его де­ кларации:

public static <Е> Set<E> union(Set<E> si, Set<E> s2)

Оба параметра s1 и s2 являются производителями E, так что схе­ ма P E C S говорит нам, что декларация должна иметь вид:

public static <Е> Set<E> union(Set<? extends E> s1, Set<? extends E> s2)

Обратите внимание, что возвращаемый тип все еще Set<E>.

Не используйте wildcard-тип в качестве возвращаемого типа.

Вместо того чтобы давать больше гибкости пользователям, он будет заставлять их использовать wildcard-тип в клиентском коде.

Wildcard -типы при правильном использовании почти невидимы для пользователей класса. Они заставляют методы прини­ мать параметры, которые они должны принимать, и отвергать те, которые должны отвергать. Если пользователь класса должен думать о наличии wildcard-типов, то, значит, что-то не так

СA PI класса.

Ксожалению, правила вывода типов довольно сложны. Они занимают 16 страниц в спецификации языка [JLS, 15.12.2.7-8] и не всегда делают то, что вы от них хотите. Глядя на пересмотренную де­ кларацию union, можно подумать, что сделаете так:

190

С татья 28

Set<Integer> integers = ... ;

Set<Double> doubles - ... ;

Set<Number> numbers = union(integers, doubles);

Если вы попробуете, то получите сообщение об ошибке:

Union.java:14: incompatible types

found : Set<Number & Comparable<? extends Number & Comparable<?»> required: Set<Number>

Set<Number> numbers = union(integers, doubles);

К счастью, есть способ справиться с подобными ошибками. Если компилятор не выводит типы, которые вы хотите, тогда скажем ему, какие типы использовать с помощью исключительного параметра. Не так часто придется это делать, что хорошо, так как исключитель­ ные параметры типа не очень хороши сами по себе. С исключитель­ ным параметром программа компилируется без ошибок:

Set<Number> numbers = Union.<Number>union(integers, doubles);

Теперь обратим внимание на метод max из статьи 27. Вот его оригинальная декларация:

public static <Т extends Comparable<T>> Т max(List<T> list) Вот переделанная декларация, использующая wildcard-типы: public static <Т extends Comparable<? super Т » T max( List<? extends T> list)

Д ля получения измененной декларации мы дважды примени­ ли схему P E C S. Напрямую использовали ее для параметра list. Он произвел экземпляры Т, поэтому мы заменили тип с List<T> на List<? Extends Т>. Рассмотрим более сложное использование на параметре типа Т. Рассмотрим пример использования группово­ го символа на параметре типа. Изначально Т должен был расширять Comparable<T>, но Comparable<T> потребляет экземпляры Т (и произ­ водит целые числа, отражающие порядковые отношения). Следова­ тельно, тип с параметрами Comparable<T> заменяется ограниченным wildcard-типом Comparable<? super Т>. Все Comparable всегда являют­ ся потребителями, так что всегда следует использовать Comparable<?

191

Глава 5 • Средства обобщенного программирования (Generics)

super Т> вместо Comparable<T>. То же самое относится и к Comparator,

так что вы всегда должны использовать Com parato r<? super Т> вме­

сто Comparator<T>.

Измененное декларирование шах вероятнее является самым сложным декларированием метода во всей книге. Дает ли нам что-то эта увеличенная сложность? Да, дает. Вот простой пример списка, который будет исключен в оригинальной декларации, но допустим в измененной:

List<ScheduledFuture<?» scheduledFutures = ... ;

Причина, почему вы не можете применить оригинальное декла­ рирование метода к данному списку, в том, что javautil.concurrent.

ScheduledFuture не реализует Comparable<ScheduledFuture>. Вместо

этого есть субинтерфейс от Delayed, который расширяет Comparable<Delayed>. Другими словами, экземпляр SchedukedFuture не про­ сто сопоставим с другими экземплярами ScheduledFuture; он сопо­ ставим с любым экземпляром Delayed, и этого недостаточно, чтобы декларация его отвергла.

Есть одна небольшая проблема с измененной декларацией max: она не дает методу компилироваться. Вот метод с измененным декла­ рированием:

//Не будет компилироваться - групповым символам могут

//потребоваться изменения в теле метода!

public static <Т extends Comparable<? super Т » Tmax( List<? extends T> list) {

Iterator<T> i = list.iterator(); T result = i.next();

while

(i. hasNextO) {

T t - i.next();

if

(t.compareTo(result) > 0)

 

result = t;

}

return result;

}

192

С татья 28

Вот что произойдет при попытке компиляции:

Max.java:7: incompatible types

found : Iterator<capture#591 of ? extends T> required: Iterator<T>

Iterator<T> i = list.iterator();

Что значит это сообщение об ошибке и как решить проблему? Это значит, что list не является List<T>, так что его метод iterator не возвращает Iterator<Т>. Он возвращает iterator некоего подтипа Т, так что мы заменяем декларацию iterator на эту, которая использу­ ет ограниченные wildcard-типы.

Iterator<? extends Т> i = list.iterator();

Это единственное изменение, которое нужно сделать в теле ме­ тода. Элементы, возвращаемые методом iterator next, являются неки­ ми подтипами Т, так что они могут быть безопасно сохранены в пере­ менной типа Т.

Есть еще одна тема, связанная с wildcard-типами, которая сто­ ит обсуждения. Есть двойственность между параметрами для типов и групповыми символами, так как многие методы могут деклариро­ ваться с использованием либо одного, либо другого. Например, вот две возможные декларации статического метода перестановки двух индексированных пунктов в списке. Первый использует неограни­ ченный параметр wildcard-типа (статья 27), а второй использует не­ ограниченный групповой символ:

// Две возможные декларации метода swap

public static <Е> void swap(List<E> list, int i, int j); public static void swap(List<?> list, int i, int j);

Какая из двух деклараций наиболее предпочтительна, и почему? В открытом API вторая лучше, потому что она проще. Вы переда­ ете в список любой список, и метод переставляет индексированные элементы. Не надо волноваться о параметрах. Как правило, если па­ раметр для типа появляется только один раз в декларации мето­

193

Глава 5 • Средства обобщенного программирования (Generics)

да, замените его групповым символом. Если это не ограниченный параметр, замените его неограниченным групповым символом, если же это ограниченный параметр, замените его ограниченным группо­ вым символом.

Есть одна проблема со второй декларацией swap, использующей групповой символ вместо параметра: непосредственная реализация его не будет компилироваться:

public static void swap(List<?> list, int i, int j) { list.set(i, list.set(j, list.get(i)));

}

Попытка компиляции приведет к малоинформативному сообще­ нию об ошибке:

Swap.java:5: set(int,capture#282 of ?) in List<capture#282 of ?> cannot be applied to (int,Object)

list.set(i, list.set(j, list.get(i)));

Кажется, но это неверно, что мы не можем поместить элемент обратно в список, из которого мы его только что взяли. Проблема в том, что типом List является List<?> и вы не можете поместить любое значение, кроме нулевого в List<?>. К счастью, есть способ реализовать метод, не прибегая к небезопасной передаче или необра­ ботанным типам. Вспомогательным метод должен быть обобщенным методом для того, чтобы захватить тип. Вот как он должен выглядеть:

public static void swap(List<?> list, int i, int j) { swapHelper(list, i, j);

}

// Закрытый вспомогательный метод для захвата групповых символов private static <Е> void swapHelper(List<E> list, int i, int j) {

list.set(i, list.set(j, list.get(i)));

}

Метод swapHelper знает list как list<E>. Следовательно, он зна­ ет, что любое значение, которое он получает из этого списка, является

194

С татья 29

типом Е и что безопасно поместить любое значение типа Е в список. Эта несколько сложная реализация swap компилируется без ошибок. Она позволяет экспортировать декларацию swap на основе груп­ повых символов, одновременно извлекая пользу от более сложных обобщенных методов изнутри. Клиентам метода swap не придется сталкиваться с более сложной декларацией swapHelper, но они только выигрывают от этого.

Подведем итоги. Использование wildcard-типов в вашем API делает их более гибкими, хотя и более сложными. Если вы напише­ те библиотеку, которая будет широко использоваться, то правиль­ ное использование wildcard-типов должно быть в ней обязательным. Помните основное правило: схема P E C S и что все Comparable и Com­ parator являются потребителями.

Использование неоднородных контейнеров

Наиболее часто обобщенные средства программирования ис­ пользуются для коллекций, таких как Set и Мар, или для одноэлемент­ ных контейнеров, таких как ThreadLocal и AtomicReference. Во всех этих случаях параметры присваиваются именно контейнерам. Это ограничивает вас до определенного количества параметров на один контейнер. Обычно это именно то, что нам нужно. У типа Set есть один параметр, представляющий его элементы; у типа Мар есть два, представляющие его ключ и типы значений, и.т.д.

Иногда, тем не менее, нам требуется большая свобода действий. Например, строка базы данных может содержать большое количество столбцов, и было бы неплохо получить к ним безопасный доступ с точки зрения использования типов. К счастью, есть такой очень легкий спо­ соб. Идея в том, чтобы параметры присвоить ключу, а не контейнеру. Затем представить ключ с параметрами контейнеру, чтобы добавлять или извлекать значение. Система обобщенных типов используется для гарантии, что тип значений согласуется с его ключом.

195

Глава 5 • Средства обобщенного программирования (Generics)

В качестве простого примера данного подхода рассмотрим класс Favorites, который позволяет своим клиентам хранить и извлекать экземпляры «favorite» из произвольно большого количества классов. Объект Class будет играть роль ключа с параметрами. Причина, по­ чему это работает, в том, что класс Class был обобщен в версии 1.5. Тип литерала класса больше не просто Class, a Class<Т>. Например, String, class относится к типу Class<String> и класс Integer.class от­ носится к классу Class<Integer>. Когда литерал класса передается между методами, чтобы обращаться к информации во время компи­ ляции и во время выполнения, то это называется м етка типа ( type token) [Bracha04].

API для класса Favorites прост. Он выглядит как простая карта, кроме того, что ключ имеет параметры, а не карта. Клиент представ­ ляет объект Class при установке и получении избранных. Вот как выглядит API:

// Безопасный шаблон неоднородного контейнера - API public class Favorites {

public <T> void putFavorite(Class<T> type, T instance); public <T> T getFavorite(Class<T> type);

}

Приведем пример программы, которая практикуется на классе Favorites, сохраняя, извлекая и печатая экземпляры String, Integer

иClass:

//Безопасный шаблон неоднородного контейнера — клиент public static void main(String[] args) {

Favorites f = new FavoritesO; f.putFavorite(String.class, "Java”); f.putFavorite(Integer.class, Oxcafebabe); f.putFavorite(Class.class, Favorites.class);

String favoriteString = f.getFavorite(String.class); int favoritelnteger = f.getFavorite(Integer.class); Class<?> favoriteClass = f.getFavorite(Class.class); System.out.printf("%s %x %s%n”, favoriteString,

196

С татья 29

favoritelnteger, favoriteClass.getName());

}

Как вы и ожидали, программа печатает Java cafebabe Favorites.

Экземпляр Favorites является безопасным с точки зрения типов: он никогда не вернет Integer, если вы просите его вывести String. Он также является неоднородным: все ключи являются клю­ чами разных типов, не как в обычной карте. Следовательно, мы мо­ жем назвать Favorites безопасным неоднородным контейнером.

Реализация Favorites на удивление крошечная. Вот она вся:

// Безопасный шаблон неоднородного контейнера - реализация public class Favorites {

private Map<Class<?>, Object> favorites = new HashMap<Class<?>, Object>();

public <T> void putFavorite(Class<T> type, T instance) { if (type == null)

throw new NullPointerException(“Type is null”); favorites.put(type, instance);

}

public <T> T getFavorite(Class<T> type) { return type.cast(favorites.get(type));

}

}

Здесь происходит несколько тонких вещей. Каждый экземпляр Favprites поддерживается закрытым классом Map<Class<?>, Object>, который называется favorites. Вы можете подумать, что здесь невоз­ можно поместить что-либо в Мар из-за неограниченного wildcard-типа, но на самом деле все наоборот. Дело в том, что wildcard-тип является вложенным: он не является типом Мар, который является wildcard-типом, а является типом его ключа. Это значит, что у каждого ключа могут быть разные типы с параметрами: один может быть Class<String>, другой Class<Integer> и т.д. Вот откуда берется неоднородность.

Также необходимо отметить, что типом значения favoritesMap является просто Object. Другими словами, Мар не является гарантом

197

Глава 5 • Средства обобщенного программирования (Generics)

взаимоотношении типов между ключами и значениями, это означает, что каждое значение относится к типу, представленному его ключом. В действительности система типов Java не имеет достаточно возмож­ ностей, чтобы это выразить. Но мы знаем, что это правда, и извле­ чем из этого преимущество, когда придет время извлечь избранное.

Реализация метода put Favorite довольно тривиальна: она просто помещает в favorites схему связей из объекта Class вэкземпляр favor­ ites. Как было отмечено, это устраняет «связывание типов» между ключом и значением; утрачивается идея, что значение есть экзем­ пляр ключа. Но это нормально, потому что метод get Favorites мо­ жет и должен восстанавливать это связывание.

Реализация метода get Favorite сложнее, чем метода putFavorites. Во-первых, она берет из карты favorites значение, соответ­ ствующее данному объекту Class. Это верная ссылка на объект для возврата, но у нее неверный тип во время компиляции. Ее тип — про­ сто Object (тип значения карты favorites), а нам нужно возвратить Т. Таким образом, реализация метода get Favorite динамически пе­ редает ссылку на объект типу, представленному в качестве объекта Class, с использованием метода cast объекта Class.

Метод Cast - динамический аналог оператора Java cast. Он просто проверяет, что его аргумент является экземпляром типа, представленного объектом Class. Если так, он возвращает аргумент; в противном случае он выводит ClassCastException, предполагая, что клиентский код компилируется без ошибок. Нужно сказать, что мы знаем, что значения в карте favorites всегда совпадают с типами ключей.

Итак, что делает для нас метод cast, зная, что он просто возвра­ щает свои аргументы? Подпись метода cast максимально использует преимущества того факта, что класс Class был обобщен. Его возвра­ щаемый тип — это параметр типа объекта Class:

public class Class<T> { T cast(Object obj);

}

198

С татья 29

Это в точности, что нам нужно от метода get Favorites. Это то, что позволит нам сделать безопасным Favorites, не прибегая к не­ проверенным передачам Т.

Имеются два ограничения для класса Favorites, которые стоит упомянуть. Во-первых, злонамеренные клиенты могут легко взломать безопасность типов экземпляра Favorites, просто используя объект Class в его необработанной форме. Но получившийся клиентский код выведет предупреждения о непроверенных передачах при компиля­ ции. Это не отличается от обычной реализации коллекции, такой как

HashSet и HashMap. Вы можете легко поместить St ring в HashSetclnte-

ger>, используя необработанный тип HashSet (статья 23). Как гово­ рилось, у вас может быть безопасность при выполнении, если вы хо­ тите заплатить за нее. Единственный способ убедиться, что Favorites никогда не нарушит инварианты своих типов, — это заставить метод put Favorite проверить, что instance является на самом деле экзем­ пляром типа, представленным объектом type . И мы уже знаем, как это сделать. Просто используя динамическую передачу:

//Безопасность типов обеспечивается динамической передачей // Achieving runtime type safety with a dynamic cast public <T> void putFavorite(Class<T> type, T instance) {

favorites.put(type, type.cast(instance));

}

Имеются упаковщики коллекций в java, util.Collections, ко­ торые делают тожесамое.Они называются checkedSet,checkedList, checkedMap и т.д. Их методы статической генерации берут объект Class (или два объекта) и дополняют ими коллекцию (или карту). Методы статической генерации являются обобщенными методами, гарантирующими, что типы объекта Class во время компиляции и коллекция согласуются. Упаковщики добавляют материализацию к коллекции, которую они упаковывают. Например, упаковщик вы­ водит ClassException при выполнении, если кто-то пытается доба­ вить Coin в вашу коллекцию Collection<Stamp>. Эти упаковщики полезны для отслеживания, кто добавляет некорректные элементы

199