Джош Блох
.pdfГлава 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
