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

Джош Блох

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

Глава 4 Классы и интерфейсы

струкцию this [JLS 15.8.4], можно получить ссылку на включающий экземпляр. Если экземпляр вложенного класса может существовать в отрыве от экземпляра внешнего класса, то вложенный класс не мо­ жет быть нестатическим членом-классом: нельзя создать экземпляр нестатического класса-члена, не создав включающего его экземпляра.

Связь между экземпляром нестатического класса-члена и вклю­ чающим его экземпляром устанавливается при создании первого, и после этого поменять ее нельзя. Обычно эта связь задается авто­ матически путем вызова конструктора нестатического класса-чле­ на из экземпляра метода во внешнем классе. Иногда можно уста­ новить связь вручную, используя выражение enclosinglnstance. newMemberClass(args). Как можно предположить, эта связь занимает место в экземпляре нестатического класса-члена и увеличивает время его создания.

Нестатические классы-члены часто используются для опреде­ ления адаптера (Adapter) [Gamma95, с. 139], при содействии кото­ рого экземпляр внешнего класса воспринимается своим внутренним классом как экземпляр некоторого класса, не имеющего к нему от­ ношения. Например, в реализациях интерфейса Мар нестатические классы-члены обычно применяются для создания представлений кол­ лекций (collection view), возвращаемых методами keySet, entrySet и values интерфейса Мар. Аналогично, в реализациях интерфейсов коллекций, таких как Set или List, нестатические классы-члены обычно используются для создания итераторов:

// Типичный вариант использования нестатического класса-члена public class MySet<E> extends AbstractSet<E> {

... // Основная часть класса опущена public Iterator<E> iterator() {

return new MyIterator();

}

private class Mylterator implements Iterator {

}

}

150

С татья 22

Если вы объявили класс-член, которому не нужен доступ к эк­ земпляру содержащего его класса, не забудьте поместить в соответ­ ствующую декларацию модификатор static с тем, чтобы сделать этот класс-член статическим. Если вы не установите модификатор static, каждый экземпляр класса будет содержать ненужную ссыл­ ку на внешний объект. Поддержание этой связи требует и времени, и места, но не приносит никакой пользы. Если же вам когда-нибудь потребуется разместить в памяти экземпляр этого класса без окружа­ ющего его экземпляра, вы не можете это делать, так как нестатические классы-члены обязаны иметь окружающий их экземпляр.

Закрытые статические классы-члены обычно должны представ­ лять составные части объекта, доступ к которым осуществляется через внешний класс. Например, рассмотрим экземпляр класса Мар, который сопоставляет ключи и значения. Внутри экземпляра Мар для каждой пары ключ/значение обычно создается объект Entry. Хотя каждая такая запись ассоциируется со схемой, клиенту не надо обра­ щаться к собственным методам этой записи (geyKey, getValuensetValue). Соответственно, использовать нестатические классы-члены для представления отдельных записей в схеме Мар было бы расточи­ тельностью, самое лучшее решение — закрытый статический классчлен. Если в декларации этой записи вы случайно пропустите мо­ дификатор static, схема станет работать, но каждая запись будет содержать ненужную ссылку на общую схему, напрасно тратя время и место в памяти.

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

Анонимные классы в языке программирования Java не похожи ни на какие другие. Анонимный класс не имеет имени. Он не являет­ ся членом содержащего его класса. Вместо того чтобы быть деклари­

151

intAr-

Глава 4 Классы и интерфейсы

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

Применение анонимных классов имеет несколько ограничений. Поскольку анонимный класс одновременно декларируется и порож­ дает экземпляр, его можно использовать, только когда его экземпляр должен порождаться лишь в одном месте программы. Анонимный класс не имеет имени, поэтому может применяться только в том слу­ чае, если после порождения экземпляра не нужно на него ссылаться. Анонимный класс обычно реализует лишь методы своего интерфейса или суперкласса. Он не объявляет каких-либо новых методов, так как для доступа к ним нет поименованного типа. Поскольку анонимные классы стоят среди выражений, они должны быть очень короткими, возможно, строк двадцать или меньше. Использование более длин­ ных анонимных классов может усложнить программу с точки зрения ее чтения.

Анонимный класс обычно используется для создания объекта функции (function object) (статья 21), такого как экземпляр класса Comparator. Например, при вызове метода sort отсортирует строки массива по их длине. Другой распространенный случай использова­ ния анонимного класса — создание объекта процесса (process object), такого как экземпляры классов Thread, Runnable или TimerTask. Третий вариант: в статическом методе генерации (см. метод rayAsList в статье 18).

Локальные классы, вероятно, относятся к наиболее редко исполь­ зуемой из этих четырех категорий вложенных классов. Локальный класс можно декларировать везде, где разрешается декларировать локальную переменную, и он подчиняется тем же самым правилам видимости. Локальный класс имеет несколько признаков, объедини-

152

С татья 22

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

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

153

Средства обобщенного

программирования

(Generics)

1В релизе 1.5 в языке Java появились средства обобщенного програм­ мирования (Generics). До их появления необходимо было передавать все объекты из коллекции. Если кто-то случайно вставит объект не­ правильного типа, передача завершится с ошибкой при выполнении. С помощью средств обобщенного программирования вы даете ука­ зания компилятору, какие типы объектов разрешены в каждой кол­ лекции. Компилятор автоматизирует передачу объектов и на этапе компиляции говорит вам о том, что вы пытаетесь вставить объект неверного типа. Это приводит к тому, что программы становятся не только более безопасными, но и более «чистыми». Однако эти преимущества сопряжены с определенными сложностями. В этой главе мы расскажем о том, как свести к минимуму сложности, и мак­ симально извлечь пользу из преимуществ. Для более детального из­ учения материала посмотрите учебник Лангера [Langer08] или книгу Нафталина и Уодлера [Naftalin07].

154

С татья 23

Не используйте необработанные типы в новом коде

Д ля начала несколько терминов. Класс или интерфейс, деклара­ ция которого содержит один или более типовых параметров (type parameters), является обобщенным (generic) классом или интерфей­ сом [JLS, 8.1.2, 9.1.2]. Например, в релизе 1.5 у интерфейса List есть единственный типовой параметр Е, представляющий тип элемента списка. Технически наименование интерфейса теперь будет List<E> (читается «Список Е»), но часто для краткости его называют List. Обобщенные классы и интерфейсы известны под общим наименова­ нием обобщенные типы (generic types).

Каждый обобщенный тип определяет набор типов с параме­ трами (parameterized types), который состоит из наименования класса или интерфейса, после которого следует в угловых скобках список актуальных типовых параметров (actual type parameters),

соответствующий формальным типовым параметрам, относящимся к обобщенному типу [JLS, 4.4, 4.5]. Например, List<String> (чи­ тается «список String») — это тип с параметрами, представляющий список, элементы которого принадлежат типу String. (String — это актуальный параметр типа, соответствующий формальному параме­ тру типа Е.)

Наконец, каждый обобщенный тип определяет необработан­ ный тип (raw type), который является наименованием обобщенно­ го типа без каких-либо актуальных типовых параметров [JLS, 4.8]. Например, необработанным типом, соответствующим List<E>, будет List. Необработанные типы ведут себя так, словно вся информа­ ция об обобщенном типе оказалась стерта при его декларировании. На практике необработанный тип List ведет себя аналогично тому, как вел себя интерфейс List до добавления к платформе обобщенных средств программирования .

До релиза 1.5 это было бы типичной декларацией коллекции:

155

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

// Необработанный тип коллекции - не стоит так делать!

/* *

* Моя коллекция марок (stamp). Содержит только экземпляры Stamp.

* /

private final Collection stamps = ... ;

Е сли вы случайно добавите в вашу коллекцию марок монету, оши­ бочное добавление будет откомпилировано и выполнено без ошибок:

// Ошибочная вставка монеты в коллекцию марок stamps.add(new Coin( ... ));

Ошибки не будет до тех пор, пока вы не станете извлекать моне­ ту из коллекции марок:

// Необработанный тип итератора - не стоит так делать! for (Iterator i = stamps.iterator(); i.hasNext(); ) {

Stamp s = (Stamp) i.nextO; // Throws ClassCastException

... //Do something with the stamp

}

Как уже было упомянуто в этой книге, лучше всего находить ошибки сразу, как только они сделаны, в идеале на этапе компиля­ ции. В нашем же случае вы не найдете ошибку до момента выпол­ нения программы, а найдете ее намного позднее, чем она сделана, и совершенно не в том коде, в котором она была допущена. Как толь­ ко вы увидите сообщение ClassCastException, вам придется искать по всему коду и смотреть, как запускаются методы, которые поме­ щают монету в коллекцию марок. Компилятор тут вам не сможет по­ мочь, потому что он не понимает комментарий, в котором сказано «Contains only Stamp instances».

С использованием средств обобщенного программирования вы заменяете комментарий расширенной декларацией типов для кол­ лекции, которая даст компилятору дополнительную информацию, не­ доступную ранее в комментарии:

// Коллекция типов с параметрами - безопасно private final Collection<Stamp> stamps = ... ;

156

С тать я 23

Благодаря такой декларации компилятор знает, что stamps долж­ на содержать только экземпляры Stamp, и гарантирует, что это ус­ ловие будет соблюдаться, при условии, что весь код компилируется компилятором из релиза 1.5 или более позднего и что весь код ком­ пилируется без каких-либо предупреждений (или сокрытия преду­ преждений, см. статью 24). Когда коллекция stamps декларируется с использованием типов с параметрами, ошибочное добавление при­ водит к сообщению об ошибке уже на этапе компиляции, которое точно сообщает, в чем ошибка:

Test.java:9: add(Stamp) in Collection<Stamp> cannot be applied to (Coin)

stamps.add(new Coin());

Дополнительным преимуществом будет то, что вам больше не по­ требуется передавать объекты вручную при извлечении элементов из коллекции. Компилятор добавляет невидимые механизмы переда­ чи для вас и гарантирует, что они обязательно сработают (опять-таки при условии, что весь ваш код откомпилирован компилятором, кото­ рый знает о существовании обобщенных средств программирования и не скроет никаких предупреждений). Это утверждение верно вне зависимости от того, используете ли вы цикл for-each (статья 46):

// Цикл for-each поверх коллекции с параметрами - безопасно for (Stamp s : stamps) { // No cast

// Do something with the stamp

}

или обычный цикл:

//Цикл for, декларируемый с использованием итератора с параметрами, -

//безопасно

for (Iterator<Stamp> i = stamps.iterator(); i.hasNext(); ) { Stamp s = i.next(); // No cast necessary

... //Do something with the stamp

}

157

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

Хотя перспектива случайной вставки монеты в коллекцию марок может показаться маловероятной, тем не менее проблема вполне ре­ альна. Например, легко представить, что кто-то вставит экземпляр java, utils. Date в коллекцию, которая должна содержать только эк­

земпляры java.sql.Date.

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

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

спараметрами методам, которые были созданы для использования только простых типов, и наоборот. Это требование, известное как

миграционная совместимость (migration compatibility), привело к решению сохранить поддержку необработанных типов.

Когда вам не нужно использовать необработанные типы, такие как List в новом коде, совершенно нормально использовать типы с параметрами, чтобы разрешить добавление произвольного объекта, такого как List<Object>. В чем же разница между необработанным типом List и типом с параметрами List<Object>? Проще говоря, в первом нет опции проверки типа, в то время как последний точно говорит компилятору, что он может содержать объект любого типа. В то время как вы можете передать List<String> параметру с типом List, вы не можете передать его с типом List<Object>. Существуют правила образования подтипов для средств обобщенного программи­ рования. List<String> является подтипом необработанного типа List, но не является таковым для типа с параметрами List<Object> (ста­

158

С татья 23

тья 25). Как следствие, вы теряете в безопасности типов в случае использования необработанного типа, такого как List, но сохра­ няете ее при использовании типа с параметрами List<Object>.

Для конкретного примера рассмотрим следующую программу:

//Использует необработанный тип (List) - происходит ошибка

//при выполнении!

public static void main(St ring[] args) { List<String> strings = new ArrayList<String>(); unsafeAdd(strings, new Integer(42));

String s = strings.get(O); // Compiler-generated cast

}

private static void unsafeAdd(List list, Object o) { list.add(o);

}

Эта программа компилируется, но из-за того, что используется необработанный тип List, выходит предупреждение:

Test.java:10: warning: unchecked call to add(E) in raw type List list.add(o);

И действительно, если вы запускаете программу, то вы получа­ ете сообщение Class Exception, когда программа пытается передать результат запуска strings, get(0) в String. Эта передача уже сгене­ рирована компилятором, поэтому гарантировано, что она успешно завершится. В данном случае мы проигнорировали предупреждение компилятора и заплатили за это.

Если вы замените необработанный тип List типом с параметра­ ми List<Object> в декларации unsafedd и попробуете снова скомпи­ лировать программу, то обнаружите, что она более не компилируется. Появляется сообщение об ошибке:

Test.java:5: unsafeAdd(List<Object>,Object) cannot be applied to (List<String>,Integer)

unsafeAdd(strings, new Integer(42));

159

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]