Академический Документы
Профессиональный Документы
Культура Документы
РН
PTR 4-е полное С ^П П ТЕ Р
издание
Cgntmp*
Bruce Eckel
щшшщ ■ | ■ ■ ■
Thinking m Java
4th Edition
PH
PTR
HARCCHHR COmPUTER SCIENCE
БРЮС ЭККЕЛЬ
ФИЛОСОФИЯ
JAVA
4-е полное
издание
Е^П П ТЕР’
Москва •Санкт-Петербург •Нижний Новгород •Воронеж
Ростов-на-Дону •Екатеринбург •Самара •Новосибирск
Киев •Харьков •Минск
2015
ББК 32.973.2-018.1
УДК 004.3
Э38
Эккель Б.
Э38 Философия Java. 4-е полное изд. — СПб.: Питер, 2015. — 1168 c.: ил. — (Серия
«Классика computer science»).
ISBN 978-5-496-01127-3
Впервые читатель может познакомиться с полной версией этого классического труда, который ранее
на русском языке печатался в сокращении. Книга, выдержавшая в оригинале не одно переиздание, за
глубокое и поистине философское изложение тонкостей языка Java считается одним из лучших пособий
для программистов. Чтобы по-настоящему понять язык Java, необходимо рассматривать его не просто
как набор неких команд и операторов,апонять его «философию», подход к решению задач, в сравнении
с таковыми в других языках программирования. На этих страницах автор рассказывает об основных
проблемах написания кода: в чем их природа и какой подход использует Java в их разрешении. Поэтому
обсуждаемые в каждой главе черты языка неразрывно связаны с тем, как они используются для решения
определенных задач.
1 2 + (Для детей старше 12 лет. В соответствии с Федеральным законом от 29 декабря 2010 г. № 436-
ФЗ.)
ББК 32.973.2-018.1
УДК 004.3
Права на издание получены по соглашению с Prentice Hall, Inc. Upper Sadle River, New Jersey 07458. Все права за
щищены. Никакая часть данной книги не может быть воспроизведена в какой бы то ни было форме без письменного
разрешения владельцев авторских прав.
Информация, содержащаяся в данной книге, получена из источников, рассматриваемых издательством как надеж
ные. Тем не менее, имея в виду возможные человеческие или технические ошибки, издательство не может гаран
тировать абсолютную точность и полноту приводимых сведений и не несет ответственности за возможные ошибки,
связанные с использованием книги.
Введение.................................................................................................................... 33
Глава 3. Операторы.......................................................................................................... 95
Приложение Б. Ресурсы..........................................................................................1159
Содержание
Предисловие............. 25
JavaSE5 и SE6................................................................................................................. 26
java SE6........................................................................ 27
Четвертое издание........................................................................................................... 27
Изменения........................................................................................................................ 27
Замечания о дизайне обложки....................................................................................... 29
Благодарности................................................................................................................. 29
Введение......................... 33
Предпосылки.................................................................................................................... 34
H3y4eHneJava.................................................................................................................. 34
Цели...................................................................................................................................35
Обучение по материалам книги.................................................................................... 36
HTML-документация JD K ........................................................................... 36
Упражнения......................................................................................................................37
Сопроводительные материалы...................................................................................... 37
Исходные тексты программ........................................................................................... 38
Стандарты оформления кода...................................................................................... 39
Ошибки.............................................................................................................................39
Однокорневая иерархия....................................................................................................55
Контейнеры........................................................................................................................ 56
Параметризованные типы................................................................................................ 57
Создание и время жизни объектов.................................................................................. 58
Обработка исключений: борьба с ошибками................................................................. 60
Параллельное выполнение............................................................................................... 61
Java и Интернет....................................................................... 62
Что такое W eb?............................................................................................................... 62
Вычисления «клиент—сервер»...................................................................................62
Web как гигантский сервер......................................................................................... 63
Программирование на стороне клиента..................................................................... 63
Модули расширения....................................................................................................64
Языки сценариев.......................................................................................................... 65
Java.................................................................................................................................. 66
Альтернативы................................................................................................................ 66
.NET и C # .......................................................................................................................67
Интернет и интрасети.................................................................................................. 67
Программирование на стороне сервера...................................................................... 68
Резюме..................................................................................................................................69
Глава 3. Операторы...................................................................................... 95
Простые команды печати................................................................................................95
Операторы J ava............................................................................................................... 96
Приоритет................................................................................................. 96
Присваивание............................................................................................................... 97
Совмещение имен во время вызова методов....................................... ,•.................98
Математические операторы........................................................................................99
Унарные операторы плюс и минус.........................................................
Автоувеличение и автоуменьшение......................................................................... 101
Операторы сравнения................................................................................................103
Проверка объектов на равенство............................................................................103
Логические операторы...............................................................................................104
Ускоренное вычисление.......................................................................................... 105
Литералы........................................................................................................................ 106
Экспоненциальная запись.........................................................................................108
Поразрядные операторы........................................................................................... 109
Операторы сдвига.......................................................................................................110
Тернарный оператор.................................................................................................. 113
Операторы + и += для String.................................................................................... 114
Типичные ошибки при использовании операторов................................................115
Операторы приведения.............................................................................................. 116
Округление и усечение.............................................................................................. 117
Повышение..................................................................................
BJava отсутствует sizeof................................................................................................118
Сводка операторов..................................................................................................... 118
Резюме................................................................................................
do-while............................................................................................................................129
for..................................................................................................................................... 129
Оператор-запятая..........................................................................................................131
CnHTaKCHcforeach........................................................................
return...................................................................................................................................133
break и continue.................................................................................................................134
Нехорошая команда goto.................................................................................................136
switch............................................................................................................................... 140
Резюме................................................................................................................................142
List................................................................................................................ 333
Итераторы........................................................................................................................ 336
ListIterator.....................................................................................................................339
LinkedList........................................................................................................................ 340
С тек...................................................................................................................................341
Множество....................................................................................................................... 343
М ар....................................................................................................................................346
Очередь.............................................................................................................................350
PriorityQueue...................................................................................................................351
Collection и Iterato r........................................................................................................ 353
Foreach и итераторы....................................................................................................... 356
Идиома «Метод-Адаптер»..............................................................................................358
Резюме.............................................................................................................................. 361
XML................................................................................................................................... 805
Предпочтения.................................................................................................................. 807
Резюме...............................................................................................................................809
Значки..........................................................................................................................1066
Подсказки..................................................................................................................-. 1067
Текстовые поля...........................................................................................................1068
Рамки........................................................................................................................... 1069
Мини-редактор...........................................................................................................1070
Ф лаж ки....................................................................................................................... 1071
Переключатели........................................................................................................... 1073
Раскрывающиеся списки.......................................................................................... 1074
Списки......................................................................................................................... 1075
Панель вкладок...........................................................................................................1076
Окна сообщений........................................................................................ .';..............1077
Меню...................................................................................... 1079
Всплывающие меню...................................................................................................1084
Рисование....................................................................................................................1085
Диалоговые окна........................................ ........................................... .-.................. 1089
Диалоговые окна выбора файлов................................................ .'.......................... 1092
HTML для компонентов Swing................................................................................ 1094
Регуляторы и индикаторы выполнения................................................................. 1095
Выбор внешнего вида и поведения программы.....................................................1096
Деревья, таблицы и буфер обмена...........................................................................1098
JNLP nJava Web S tart...................................................................................................1098
Параллельное выполнение и Swing............................................................................1103
Продолжительные задачи......................................................................................... 1103
Визуальные потоки....................................................................................................1110
Визуальное программирование и KOMnoHenraJavaBean........................................ 1112
Что такое компонент}ауаВеап?.............................................................................. 1113
Получение информации о компоненте Bean: инструмент Introspector.............1115
Более сложный компонент Bean..............................................................................1120
KoMnoHeHTMjavaBean и синхронизация................................................................ 1123
Упаковка компонента B ean...................................................................................... 1127
Поддержка более сложных компонентов Bean......................................................1128
Больше о компонентах Bean.....................................................................................1129
Альтернативы для Swing...............................................................................................1129
Построение веб-клиентов Flash с использованием Flex.......................................... 1130
Первое приложение F lex .......................................................................................... 1131
Компилирование M X M L......................................................................................... 1132
MXML и ActionScript...................................................... ’.......................................1133
Контейнеры и элементы управления...................................................................... 1133
Эффекты и стили....................................................................................................... 1135
События.................... 1136
Связывание cJava...................................................................................................... 1136
Модели данных и связывание данных....................................................................1139
Построение и развертывание....................................................................................1140
Создание приложений SW T........................................................................................ 1141
Установка S W T ...................................................................................................... 1142
Первое приложение................................................................................................... 1142
Содержание 23
1 Но по моему мнению, язык Python наиболее близок к достижению этой цели (см. wwmPython.
org).
26 Предисловие
Java SE6
Эта книга была грандиозным и трудоемким проектом, и еще до ее завершения вышла
бета-версия J ava SE6 (кодовое название mustang). И хотя Bjava SE6 были внесены не
большие второстепенные изменения, которые позволили улучшить некоторые примеры,
в основном HOBHteCTBaJava SE6 не отразились на содержимом книги; их главной целью
было повышение скорости и возможности библиотек, выходящие за рамки материала.
Код, приведенный в книге, был успешно протестирован в предвыпускной версии
Java SE6; вряд ли в официальном выпуске встретятся какие-то изменения, влияющие
на материал. Если это все же произойдет, эти изменения будут отражены в исходном
тексте программ, доступном на сайте www.MindView.net.
На обложке указано, что эта книга предназначена дляJava SE5/6; это означает «на
писана A?mJava SE5 с учетом очень значительных изменений, появившихся в языке
в этой версии, но материал в равной степени применим Kjava SE6».
Четвертое издание
Работая над новым изданием книги, я стремился реализовать в нем все, что узнал
с момента выхода последнего издания. Часто эти поучительные уроки позволяли мне
исправить какую-нибудь досадную ошибку или просто оживить скучный материал.
Нередко в ходе работы над новым изданием у меня появлялись увлекательные новые
идеи, а досаду от выявления ошибок затмевала радость открытия и возможность вы
ражения своих идей в более совершенной форме.
Изменения
Компакт-диск, который традиционно прилагался к книге, в этом издании отсутствует.
Важная часть этого диска —мультимедийный семинар Thinking in С (созданный для
MindView Чаком Алисоном) —теперь доступна в виде загружаемой Flash-презентации.
Цель этого семинара — подготовка читателей, незнакомых с синтаксисом С, к по
ниманию материала книги. Хотя в двух главах книги приведено неплохое вводное
описание синтаксиса, для неопытных читателей их может оказаться недостаточно.
Семинар Thinkingin С поможет таким читателям подняться на необходимый уровень.
Пришлось, например, полностью переписать главу «Параллельное выполнение» (неког
да «Многопоточность»), так чтобы материал соответствовал основным нововведениям
Java SE5, но при этом в нем были отражены основные концепции многопоточности.
Без такого фундамента понять более сложные вопросы многозадачности очень трудно.
Я провел много месяцев в потустороннем мире «многопоточности», и материал, при
веденный в конце главы, не только закладывает фундамент, но и прокладывает дорогу
в более сложные территории.
Практически для каждой новой возможности Java SE5 в книге появилась отдельная
глава, а другие новшества были отражены в изменениях существующего материала.
28 Предисловие
От издательства
Ваши замечания, предложения, вопросы отправляйте по адресу электронной почты
comp@prter.com (издательство «Питер», компьютерная редакция).
Мы будем рады узнать ваше мнение!
На веб-сайте издательства http://www.piter.com вы найдете подробную информацию о
наших книгах.
Книга «Философия^ча» была написана Брюсом Эккелем достаточно давно, поэтому
приведенные автором ссылки на сайты могли измениться. При подготовке русского
издания мы постарались оставить в тексте только действующие адреса, но не можем
гарантировать, что они не изменятся в дальнейшем.
Даже веб-страничка автора переехала с www.MindView.net на mindviewinc.com, т&к что не
удивляйтесь, если какая-либо ссылка не будет работать. В этом случае попробуйте
воспользоваться удобной для вас поисковой системой.
Благодарности
Во-первых, благодарю тех, кто работал со мной на семинарах, организовывал кон
сультации и развивал преподавательский процесс: Дэйва Бартлетта (который не
мало потрудился над главой 15), Билла Веннерса, Чака Алисона, ДЖереми Мейера
и Джейми Кинга. Я благодарен вам за терпение, которое вы проявили, пока я пытался
сформировать команду из таких независимых людей, как мы.
Недавно, несомненно благодаря Интернету, я оказался связан с удивительно большим
количеством людей, которые помогали мне в моих изысканиях, как правило, работая
из своих домашних офисов. Будь это раньше, мне пришлось бы платить за гигантские
офисные пространства, чтобы вместить всех этих людей, но теперь, благодаря Сети,
факсам и отчасти телефону, я способен воспользоваться их помощью без особых затрат.
В моих попытках научиться «играть в команде» вы мне очень помогли, и я надеюсь
и дальше учиться тому, как улучшить свою собственную работу с помощью усилий дру
гих людей. Пола Стьюар оказала бесценную помощь, взявшись за мой беспорядочный
бизнес и приведя его в порядок (спасибо, что подталкивала меня, Пола, когдая не хотел
ничего делать). Джонатан Вилкокс перетряс всю структуру моей корпорации, перево
рачивая каждый «камешек», под которым мог скрываться «скорпион», и заставил нас
пройти через процесс эффективного и юридически верного обустройства компании.
Спасибо за вашу заботу и постоянство. Шерилин Кобо (которая нашла Полу) стала
экспертом в обработке звука и значительной части мультимедийных курсов, а также
бралась за другие задачи. Спасибо за твое упорство в борьбе с неподатливыми ком
пьютерными делами. Ребята из Amaio (Прага) помогли мне в нескольких проектах.
Дэниел Уилл-Харрис был вдохновителем работы через Интернет, и конечно же, он
сыграл главную роль во всех моих решениях относительно дизайна.
За прошедшие годы Джеральд Вайнберг стал моим неофициальным учителем и на
ставником; я благодарен ему за это.
30 Предисловие
Эрвин Варга оказал огромную помощь с технической правкой 4-го издания — хотя
другие участники помогали в работе над разными главами и примерами, Эрвин был
главным техническим рецензентом книги; кроме того, он взялся за переработку ру
ководства по решениям для 4-го издания. Эрвин нашел ошибки и сделал немало бес
ценных усовершенствований. Его скрупулезность и внимание к подробностям просто
поражают. Безусловно, это лучший технический рецензент, с которым я когда-либо
работал. Спасибо, Эрвин.
Мой блог на сайте Билла Веннерса zmmArtima.com приносил много полезной инфор
мации, когда мне требовалось «обкатать» ту или иную идею. Спасибо читателям, чьи
комментарии помогали прояснять описанные концепции, — в том числе Джеймсу
Уотсону, Говарду Ловатту, Майклу Баркеру и другим (и особенно тем, кто помогал
с обобщенными типами).
Спасибо Марку Уэлшу за неустанную поддержку.
Эван Кофски, как и прежде, способен сходу разъяснить многие заумные подробности
настройки и сопровождения веб-серверов на базе Linux. Кроме того, он помогает в оп
тимизации и настройке безопасности сервера MindView.
Кафетерий Camp4 Coffee в Crested Butte, Колорадо, стал настоящим местом встреч
во время преподавания семинаров. Пожалуй, это самая лучшая закусочная из всех,
что я когда-либо видел. Спасибо моему приятелю Алу Смиту за то, что он создал это
замечательное место, весьма интересно и приятно вспоминать его. И спасибо всем
приветливым бариста Camp4.
Некоторые инструменты с открытыми текстами стали просто незаменимыми дЛя
меня в процессе разработки, и каждый раз, когда я их использую, я благодарю разра
ботчиков. CygwmXhttp://ze)Z0ze).cygzmn.com) решил бесчисленные проблемы, которые
система Windows не могла или не хотела решить, и с каждым днем я все больше при
вязываюсь к нему (был бы у меня этот инструмент лет 15 назад, когда мой мозг был
забит хитростями Gnu Emacs). Eclipse от IBM (http://z 0mm.eclipse.org) — чудесный
подарок сообществу разработчиков, и я ожидаю от него много хорошего по мере его
развития (когда это IBM вошла в моду? Кажется, я что-то упустил в этой жизни).
JetBrains IntelliJ Idea продолжает прокладывать новые творческие пути в области
инструментария разработчика.
В этом издании книги я начал использовать Enterprise Architect от Sparxsystems, и эта
система стала моим любимым инструментом для работы с UML. Код системы форма
тирования KOflaJalopy Марко Хансикера (wtvw.triemax.com) неоднократно пригодился
мне во время работы, а Марко помог приспособить его для мойх специфических по
требностей. Также в некоторых ситуациях пригодился созданный Славой Пстовым
редактор^бй : и его плагины (z0z0wjedit.org) — это неплохой редактор начального
уровня для семинаров.
И конечно же, если я не наскучил напоминанием об этом во всех остальных местах, для
решения своих задач я постоянно использовал Python (z0 zm.Pythor1.org), детище моего
приятеля Гвидо Ван Россума и его эксцентричных гениев. Я провел с ними несколько
великолепных дней, занимаясь экстремальным программированием (Тим Питерс, я по
местил ту мышку, что ты мне одолжил, в рамку, у нее есть официальное имя «Мышка
Благодарности 31
Тима»). Вам, ребята, надо поискать более приличные места для еды. (Спасибо также
всему сообществу Python, совершенно изумительные люди.)
Множество людей посылало мне исправления для книги, и я признателен всем вам,
но в особенности хочу поблагодарить (относительно первого издания): Кевина Pay-
лерсона (нашел тонны замечательных ошибок), Боба Резендеса (просто бесподобно),
Джона Пинто, Джо Данте, Джо Шарпа (все трое были великолепны), Дэвида Комбса
(множество грамматических исправлений), д-ра Роберта Стивенсона, Джона Кука,
Франклина Чена, Зева Гринера, Дэвида Карра, Леандер Строшейн, Стива Кларка,
Чарльза Ли, Остина Магера, Денниса Рота, Рок Оливейра, Дугласа Данна, Дейян
Ристик, Нейл Галарно, Дэвида Мальковски, Стива Вилкинсона и многих других. Про
фессор Марк Мюрренс предпринял большие усилия для того, чтобы опубликовать
и сделать доступной электронную версию первого издания книги в Европе.1
Спасибо вссм, кто помог мне переписать примеры для библиотеки Swing (для второго
издания), и всем остальным: Джону Шварцу, Томасу Киршу, Рахиму Адаше, Раджишу
Джайн, Рави Мантене, Бану Раджамани, Йенсу Брандту, Нитину Шираваму, Маль
кольму Дэвису и всем выразившим мне поддержку.
В 4-м издании Крис Гриндстафф сильно помог с разработкой раздела, посвященного
SWT, а Шон Невилл написал первый проект материалов по Flex.
Крейг Брокшимдт и Ген Кийока —очень умные люди, которые стали моими друзьями
и оказали на меня необычное влияние, поскольку они занимаются йогой и практикуют
некоторые другие способы духовного развития —на мой взгляд, очень поучительные
и воодушевляющие.
Для меня не удивительно, что понимание Delphi помогло мне понять Java, так как
в обоих заложено много общих идей и решений. Мои друзья, специалисты по Delphi,
помогли мне окунуться в мир этого прекрасного средства разработки программ. Это
Марко Канту (еще один итальянец —может быть, это как-то связано со способностями
кязыкам программирования?), Нейл Рубенкинг (который был вегетарианцем и зани
мался дзеном и йогой, но только до тех пор, пока не увлекся компьютерами) и, конечно
же, Зак Урлокер (администратор продуктов Delphi), давний приятель, с которым мы
объездили весь свет. И конечно, все мы в долгу перед выдающимися способностями
Андерса Хейлсберга, который продолжает трудиться над C# (этот язык, как вы узнаете,
стал одним из источников вдохновения для coздaнияJava SE5).
Поддержка и понимание моего друга Ричарда Хейла Шоу были очень нужны. (И Ким
также.) Ричард вместе со мной проводил множество семинаров, и мы старались вы
работать идеальную обучающую программу для наших учеников.
Дизайн книги, обложки и фотографий выполнен моим другом Дэниэлем Уилл-Харри-
сом, известным автором и дизайнером (www.Will-Harns.com), который в школе делал
надписи из переводных картинок, пока еще не изобрели компьютеры и настольные из
дательские системы, а также все время жаловался, что я бормочу трудясь над алгеброй.
Впрочем, оригинал-макеты для печати я подготавливал сам, поэтому все типографские
ошибки на моей совести. Для написания книги был использован Microsoft Word XP;
для типографии текст готовился в Adobe Acrobat, печаталась книга прямо с файлов
PDF. В качестве дани электронному веку финальные версии первого и второго изданий
32 Предисловие
Предпосылки
Предполагается, что читатель уже обладает определенным опытом программирования:
он понимает, что программа представляет собой набор команд; имеет представление
о концепциях подпрограммы/функции/макроопределения; управляющих командах
(например, i f ) и циклических конструкциях типа «w hile» и т. п. Обо всем этом вы
легко могли узнать из многих источников —программируя на макроязыке или работая
с таким инструментом, как PerL Если вы уже имеете достаточно опыта и не испыты
ваете затруднений в понимании основных понятий программирования, то сможете
работать с этой книгой. Конечно, книга будет проще для тех, кто использовал язык С
и особенно С++; если вы незнакомы с этими языками, это не значит, что книга вам
не подходит —однако приготовьтесь основательно поработать (мультимедийный се
минар, который можно загрузить с сайта www.MindView.net, поможет быстро освоить
основные понятия Java). Но вместе с тем, начну я с основных концепций и понятия
объектно-ориентированного программирования (ООП) и базовых управляющих
механизмов J ava.
Несмотря на частые упоминания возможностей языков С и С++, они не являются
неразрывной частью книги —скорее, они предназначены для того, чтобы помочь всем
программистам увидеть связь}ауа с этими языками — от которых, в конце концов,
и произошел H3biKjava. Я попытаюсь сделать эти связки проще и объяснять подробнее
то, что незнакомый с С/С++ программист может не понять.
Изучение Java
Примерно в то самое время, когда вышла моя первая книга Using С++ (Osborne/
McGraw-Hill, 1989), я начал обучать этому языку. Преподавание языков программиро
вания стало моей профессией; с 1987 года я видел немало кивающих голов и озадачен
ных выражений лиц во всех аудиториях мира. Но когда я начал давать корпоративные
уроки для небольших групп, я кое-что понял. Даже те ученики, которые усердно кивали
и улыбались, многое не понимали до конца. Я обнаружил, будучи несколько лет во
главе направления С++ на конференции разработчиков программного обеспечения
(а позднее создав и вoзглaвивJava-нaпpaвлeниe), что я и другие преподаватели имеют
обыкновение слишком быстро выдавать аудитории слишком большое число тем. Со
временем из-за разницы в уровне подготовки аудитории и способа изложения мате
риала я бы потерял немало учеников. Возможно, это чересчур, но так как я являюсь
одним из убежденных противников традиционного чтения лекций (у большинства,
я полагаю, это неприятие возникает из-за скуки), я хотел, чтобы за мной успевали все
обучающиеся.
Цели 35
Цели
Подобно моей предыдущей книге Философия С++, при планировании этой книги
я прежде всего ориентировался на метод изучения языка. Размышляя о каждой гла
ве, я думаю о том, какие темы будут эффективно работать на усвоение материала во
время семинара. Мнение аудитории помогает мне выявить трудные моменты, которые
следует изложить более подробно.
В каждой главе я пытаюсь описать одну возможность (или небольшую группу логиче
ски связанных возможностей), не полагаясь на понятия, которые еще не упоминались
ранее. Это позволяет усвоить каждую тему в контексте текущих знаний читателя,
прежде чем двигаться дальше.
В данной книге я пытаюсь достичь следующего.
1. Последовательно представлять материал, чтобы читатель мог усвоить каждое
понятие перед тем, как продолжить изучение. Я тщательно слежу за порядком
изложения, чтобы каждая возможность была рассмотрена до того, как она будет
использоваться в программе. Конечно, это не всегда удается; в таких ситуациях
приводится краткое вводное описание.
2. Использовать примеры как можно более простые и наглядные. Иногда это меша
ет быть ближе к проблемам «реального» мира, но я обнаружил, что начинающим
лучше, когда они понимают пример во всех подробностях, а не восхищаются ipaH-
диозностью решаемой задач. Также существуют достаточно строгие ограничения
по объему кода, который можно усвоить в классе. Несомненно, меня будут крити
ковать за «игрушечные примеры», но я обращаюсь к ним в надежде на получение
педагогического эффекта.
3. Рассказывать то, что на мой взгляд действительно важно для понимания языка, а ие
все, что я знаю. Я полагаю, что существует различная по важности информация и в
36 Введение
HTML-документация JDK
H3biKjava и его библиотеки от компании Sun Microsystems (бесплатно загружаемые
по адресу http://java.sun.com) предоставляются с документацией в электронном виде,
которая просматривается из любого браузера. Почти все книги, посвященные Java,
повторяют эту документацию. Так как такая документация у вас уже имеется или вы
можете загрузить ее, в книге она повторяться не будет (за исключением необходимых
случаев), поскольку найти описание класса можно гораздо быстрее с помощью браузе
ра, нежели листать в его поиске книгу (и потом, интерактивная документация обычно
5nnpp гиржяя^ Чятттр тем vmrnm-e пппстп ссылку на «локументапию TDK». Книга будет
Сопроводительные материалы 37
предоставлять дополнительные описания для классов только в том случае, если это
необходимо для понимания какого-то примера.
Упражнения
Я обнаружил, что простые упражнения исключительно полезны для полного по
нимания темы обучакццимся, поэтому набор таких упражнений приводится в конце
каждой главы.
Большинство упражнений спланированы так, чтобы их можно было пройти за ра
зумный промежуток времени в классе, в то время как преподаватель будет наблюдать
за их выполнением, проверяя, все ли студенты усваивают материал. Некоторые из них
нетривиальны, хотя ни одно упраж нение не создает особы х трудностей.
Решения к отдельным упражнениям можно найти в электронном документе The
Thinking InJava Annotated Solution Guide, доступном за небольшую плату на сайте
www.MindView.net.
Сопроводительные материалы
Еще одним преимуществом данной книги является бесплатный мультимедийный
семинар, который можно загрузить на сайте www.MindView.net. Это семинар Thinking
ln С, предлагающий введение в синтаксис, операторы и функции языка С, на котором
основан синтаксис Java. В предыдущих изданиях книги этот семинар входил в состав
компакт-диска Foundations ForJava, но теперь этот семинар доступен для бесплатной
загрузки. Дополнительно он включает первые семь лекций из второго издания семи
нара Hands-OnJava.
Поначалу я задумывал поручить Чаку Аллисону создать семинар ThinkingIn С в виде
отдельного продукта, но потом решил включить его во второе издание Thinking In
С++ и второе и третье издания Thinking InJava, так как на семинарах слишком часто
появлялись люди, не обладающие достаточной подготовкой С. Видимо, они думали
так: «Я буду классным программистом, что мне этот С, а лучше возьмусь за С++ или
Java; нечего тратить время на С, лучше начну прямо с C++/Java>>. Посидев на занятиях,
люди постепенно начинают открывать для себя тот факт, что требования к знанию
синтаксиса С включены здесь совсем не напрасно.
Технологии изменились, и теперь Thinking in С стало разумнее оформить в виде пре
зентации (вместо компакт-диска). Размещение семинара в Интернете помогает гаран
тировать, что все учащиеся будут обладать необходимой подготовкой.
Семинар Thinking in С позволит книге охватить большую аудиторию. Несмотря на
то что в главе 3 рассматриваются фундаментальные элeмeнтыJava, заимствованные
нз С, семинар излагает материал не так быстро и выдвигает еще меньше требований
к навыкам студента, чем книга.
38 Введение
Ошибки
Сколько бы усилий ни прилагал писатель для поиска ошибок, все равно какие-то из
них «просачиваются» и потом бросаются в глаза читателю. Если вы обнаружите что-то
похожее на ошибку, используйте ссылку на сайте www.MindView.netRля уведомления
об ошибке и выражения вашего мнения в отношении возможного исправления. Ваша
помощь приветствуется.
Введение в объекты
в тонкостях ООП. Поэтому большинство идей в данной главе служат тому, чтобы дать
вам цельное представление об ООП. Однако многие не воспринимают общей идеи
до тех пор, пока не увидят конкретно, как все работает; такие люди нередко вязнут
в общих словах, не имея перед собой примеров. Если вы принадлежите к последним
и горите желанием приступить к основам языка, можете сразу перейти к следующей
главе —это не помешает вам в написании программ или изучении языка. И все же чуть
позже вам стоит вернуться к этой главе, чтобы расширить свой кругозор и понять, по
чему так важны объекты и какое место они занимают при проектировании программ.
Развитие абстракции
Все языки программирования построены на абстракции. Возможно, трудность ре
шаемых задач напрямую зависит от типа и качества абстракции. Под словом «тип»
я имею в виду ответ на вопрос: «Что конкретно мы абстрагируем?» Язык ассемблера
есть небольшая абстракция от компьютера, на базе которого он работает. Многие так
называемые «командные» языки, созданные вслед за ним (такие, как Fortran, BASIC
и С), представляли собой абстракции следующего уровня. Эти языки обладали зна
чительным преимуществом по сравнению с ассемблером, но их основная абстракция
по-прежнему заставляет мыслить в контексте структуры компьютера, а не решаемой
задачи. Программист должен установить связь между моделью машины (в «про
странстве решения», которое представляет место, где реализуется решение, — на
пример, компьютер) и моделью задачи, которую и нужно решать (в «пространстве
задачи», которое является местом существования задачи, — например, прикладной
областью). Для установления связи требуются усилия, оторванные от собственно
языка программирования; в результате появляются программы, которые трудно пи
сать и тяжело поддерживать. Мало того, это еще создало целую отрасль «методологий
программирования».
Альтернативой моделированию машины является моделирование решаемой задачи.
Ранние языки, подобные LISP и APL, выбирали особый подход к моделированию
окружающего мира («Все задачи решаются списками» или «Алгоритмы решают все»
соответственно). PROLOG трактует все проблемы как цепочки решений. Были созданы
языки для программирования, основанного на системе ограничений, и специальные
языки, в которых программирование осуществлялось посредством манипуляций
с графическими конструкциями (область применения последних оказалась слишком
узкой). Каждый из этих подходов хорош в определенной области решаемых задач, но
стоит выйти из этой сферы, как использовать их становится затруднительно.
Объектный подход делает шаг вперед, предоставляя программисту средства для пред
ставления задачи в ее пространстве. Такой подход имеет достаточно общий характер
и не накладывает ограничений на тип решаемой проблемы. Элементы пространства за
дачи и их представления в пространстве решения называются «объектами». (Вероятно,
вам понадобятся и другие объекты, не имеющие аналогов в пространстве задачи.) Идея
состоит в том, что программа может адаптироваться к специфике задачи посредством
создания новых типов объектов так, что во время чтения кода, решающего задачу, вы
одновременно видите слова, ее описывающие. Это более гибкая и мощная абстракция,
42 Глава 1 • Введение в объекты
Суть сказанного в том, что объект может иметь в своем распоряжении внутренние
данные (которые и есть состояние объекта), методы (которые определяют поведение),
и каждый объект можно уникальным образом отличить от любого другого объекта —
говоря более конкретно, каждый объект обладает уникальным адресом в памяти1.
1 На самом деле это слишком сильное утверждение, поскольку объекты могут существовать на
разных компьютерах и адресных пространствах, а также храниться на диске. В таких случаях
для идентификации объекта приходится использовать не адрес памяти, а что-то другое,
2 Некоторые специалисты различают эти два понятия: они считают, что тип определяет интер
фейс, а класс — конкретную реализацию этого интерфейса.
44 Глава 1 • Введение в объекты
Им я типа Light
on()
Интерфейс off()
brighten()
dim()
Скрытая реализация
Программистов имеет смысл разделить на создателей классов (те, кто создает новые
типы данны х) и программистов-клиентов1(потребители классов, использую щ иетипьт
данных в своих приложениях). Цель вторых —собрать как можно больше классов, чтобы
заниматься быстрой разработкой программ. Цель создателя класса —построить класс,
открывающий только то, что необходимо программисту-клиенту, и скрывающий все
остальное. Почему? Программист-клиент не сможет получить доступ к скрытым частям,
а значит, создатель классов оставляет за собой возможность произвольно их изменять,
не опасаясь, что это кому-то повредит. Скрытая часть обычно и самая «хрупкая» часть
объекта, которую легко может испортить неосторожный или несведущий программист-
клйент, поэтому сокрытие реализации сокращает количество ошибок в программах.
В любых отношениях важно иметь какие-либо границы, не переступаемые никем из
участников. Создавая библиотеку, вы устанавливаете отношения с программистом-
клйентом. Он является таким же программистом, как и вы, но будет использовать
вашу библиотеку для создания приложения (а может быть, библиотеки более высокого
уровня). Если предоставить доступ ко всем членам класса кому угодно, программист-
клйент сможет сделать с классом все, что ему заблагорассудится, и вы никак не смо
жете заставить его «играть по правилам». Даже если вам впоследствии понадобится
ограничить доступ к определенным членам вашего класса, без механизма контроля
доступа это осуществить невозможно. Все внутреннее строение класса открыто для
всех желающих.
Таким образом, первой причиной для ограничения доступа является необходимость
уберечь «хрупкие» детали от программиста-клиента —части внутренней «кухни», не
являющиеся составляющими интерфейса, при помощи которого пользователи решают
свои задачи. На самом деле это полезно и пользователям —они сразу увидят, что для
них важно, а на что можно не обращать внимания.
Вторая причина появления ограничений доступа —стремление позволить разработчику
библиотеки изменить внутренние механизмы класса, не беспокоясь о том, как это от
разится на программисте-клиенте. Например, вы можете реализовать определенный
класс «на скорую руку», чтобы ускорить разработку программы, а затем переписать
его, чтобы повысить скорость работы. Если вы правильно разделили и защитили ин
терфейс и реализацию, сделать это будет совсем несложно.1
! Для большинства диаграмм этого вполне достаточно. Не обязательно уточнять, что именно
используется в данном случае —композиция или агрегирование.
48 Глава 1 • Введение в объекты
Наследование
Сама по себе идея объекта крайне удобна. Объект позволяет совмещать данные и функ
циональность на концептуальном уровне, то есть вы можете представить нужное по
нятие проблемной области прежде, чем начнете его конкретизировать применительно
к диалекту машины. Эти концепции и образуют фундаментальные единицы языка
программирования, описываемые с помощью ключевого слова class.
Но согласитесь, было бы неэффективно создавать какой-то класс, а потом проделывать
всю работу заново для похожего класса. Гораздо рациональнее взять готовый класс,
«клонировать» его, а затем внести добавления и обновления в полученном клоне. Это
именно то, что вы получаете в результате наследования, с одним исключением —если
изначальный класс (называемый также базовым классом, суперклассом илкродитель-
ским классом) изменяется, то все изменения отражаются и на его «клоне» (называемом
производным классом,унаследованным классом, субклас.сом или дочерним классом).
Хотя наследование иногда наводит на мысль, что интерфейс будет дополнен новыми
методами (особенно Bjava, где наследование обозначается ключевым словом extends,
то есть «расширять»), это совсем не обязательно. Второй, более важный способ мо-
дификации классов заключается в изменении поведения уже существующих методов
базового класса. Это называется переопределением (или замещением) метода.
Наследование 51
Для замещения метода нужно просто создать новое определение этого метода в произ
водном классе. Вы как бы говорите: «Я использую тот же метод интерфейса, но хочу,
чтобы он выполнял другие действия для моего нового типа».
К онечно, при виде этой иерархии становится ясно, что базовы й класс «охлаж даю щ ая
система» недостаточно гибок; его следует переименовать в «систему контроля темпера
туры», так, чтобы он включал и нагрев —и после этого заработает принцип замены. Тем
не менее эта диаграмма представляет пример того, что может произойти в реальности.
После знакомства с принципом замены может возникнуть впечатление, что этот под
ход (полная замена) — единственный способ разработки. Вообще говоря, если ваши
иерархии типов так работают, это действительно хорошо. Но в некоторых ситуациях
совершенно необходимо добавлять новые методы к интерфейсу производного класса.
При внимательном анализе оба случая представляются достаточно очевидными.
В этом все дело —когда посылается сообщение, программист и не хочет знать, какой
код выполняется; метод прорисовки с одинаковым успехом может применяться и к
окружности, и к прямоугольнику, и к треугольнику, а объект выполнит верный код,
зависящий от его характерного типа.
Если вам не нужно знать, какой именно фрагмент кода выполняется, то когда вы
добавляете новый подтип, код его реализации может измениться, но без изменений
в том методе, из которого он был вызван. Если компилятор не обладает информацией,
какой именно код следует выполнить, что же он делает? В следующем примере объект
BirdController (управление птицей) может работать только с обобщенными объектами
Bird (птица), не зная типа конкретного объекта. С точки зрения B ird C on troller это
удобно, поскольку для него не придется писать специальный код проверки типа ис
пользуемого объекта Bird для обработки какого-то особого поведения. Как же все-таки
происходит, что при вызове метода move() без указания точного типа Bird исполняет
ся верное действие —объект Goose (гусь) бежит, летит или плывет, а объект Penguin
(пингвин) бежит или плывет?
В некоторых языках необходимо явно указать, что для метода должен использо
ваться гибкий механизм позднего связывания (в С++ для этого предусмотрено
ключевое слово v ir t u a l ) .B этих языках методы по умолчанию компонуются не
динамически. В Java позднее связывание производится по умолчанию, и вам не
нужно помнить о необходимости добавления каких-либо ключевых слов для обе
спечения полиморфизма.
Вспомним о примере с фигурами. Семейство классов (основанных на одинаковом
интерфейсе) было показано на диаграмме чуть раньше в этой главе. Для демонстра
ции полиморфизма мы напишем фрагмент кода, который игнорирует характерные
особенности типов и работает только с базовым классом. Этот код отделен от специ
фики типов, поэтому его проще писать и понимать. И если новый тип (например,
шестиугольник) будет добавлен посредством наследования, то написанный вами код
будет работать для нового типа фигуры так же хорошо, как прежде. Таким образом,
программа становится расширяемой.
Допустим, вы написали HaJava следующий метод (вскоре вы узнаете, как это делать):
void doSomething(Shape shape) {
shape.erase(); // стереть
//...
shape.draw(); // нарисовать
>
Метод работает с обобщенной фигурой (Shape), то есть не зависит от конкретного
типа объекта, который рисуется или стирается. Теперь мы используем вызов метода
doSomething() в другой части программы:
Заметьте, что здесь не сказано: «Если ты объект Circle, делай это, а если ты объект
Square, делай то-то и то-то». Такой код с отдельными действиями для каждого возмож
ного типа Shape будет путаным, и его придется менять каждый раз при добавлении
нового подтипа Shape. А так, вы просто говорите: «Ты фигура, и я знаю, что ты способна
нарисовать и стереть себя, ну так и делай это, а о деталях позаботься сама».
В коде метода doSomething() интересно то, что все само собой получается правильно.
При вызове draw() для объекта Circle исполняется другой код, а не тот, что отрабаты
вает при вызове draw() для объектов Square или Line, а когда draw() применяется для
неизвестной фигуры Shape, правильное поведение обеспечивается использованием
реального типа Shape. Это в высшей степени интересно, потому что, как было замечено
чуть ранее, когда компилятор генерирует код doSomething(), он не знает точно, с какими
типами он работает. Соответственно, можно было бы ожидать вызова версий методов
draw() и erase() из базового класса Shape, а не их вариантов из конкретных классов
Circle, Square или Line. И тем не менее все работает правильно благодаря полиморфизму.
Компилятор и система исполнения берут на себя все подробности; все, что вам нужно
знать —как это происходит... и что еще важнее —как создавать программы, используя
такой подход. Когда вы посылаете сообщение объекту, объект выберет правильный
вариант поведения даже при восходящем преобразовании, '
Однокорневая иерархия
Вскоре после появления С++ стал активно обсуждаться вопрос —должны ли все клас
сы обязательно наследовать от единого базового класса? BJava (как практически во
всех других ООП-языках, кроме С++) на этот вопрос был дан положительный ответ.
В основе всей иерархии типов лежит единый базовый класс Object. Оказалось, что
однокорневая иерархия имеет множество преимуществ.
Все объекты в однокорневой иерархии имеют некий общий интерфейс, так что, по
большому счету, все они могут рассматриваться как один основополагающий тип.
В С++ был выбран другой вариант — общего предка в этом языке не существует.
С точки зрения совместимости со старым кодом эта модель лучше соответствует
традициям С, и можно подумать, что она менее ограничена. Но как только возникнет
56 Глава 1 • Введение в объекты
Контейнеры
Часто бывает заранее неизвестно, сколько объектов потребуется для решения опре
деленной задачи и как долго они будут существовать. Также непонятно, как хранить
такие объекты. Сколько памяти следует выделить для хранения этих объектов? Неиз
вестно, так как эта информация станет доступна только во время работы программы.
Многие проблемы в объектно-ориентированном программировании решаются про
стым действием: вы создаете еще один тип объекта. Новый тип объекта, решающего
эту конкретную задачу, содержит ссылки на другие объекты. Конечно, для решения
этой задачи можно использовать и м асси вы , поддерживаемые в большинстве языков.
Однако новый объект, обычно называемый конт ейнером (или же коллекцией , но B java
этот термин используется в другом смысле), будет расширяться по мере необходимо
сти, чтобы вместить все, что вы в него положите. Поэтому вам не нужно будет знать
загодя, сколько объектов будет храниться в контейнере. Просто создайте контейнер,
а он уже позаботится о подробностях.
К счастью, хороший ООП-язык поставляется с набором готовых контейнеров. В С++
это часть стандартной библиотеки С++, иногда называемая библиотекой ст андарт ны х
шаблонов (Standard Template Library, ST L ). Sm alltalk поставляется с очень широким на
бором KOHTeftHepoB.Java также содержит контейнеры в своей стандартной библиотеке.
Для некоторых библиотек считается, что достаточно иметь один единый контейнер
для всех нужд, но в других (например, B ja v a ) предусмотрены различные контейнеры
на все случаи жизни: несколько различных типов списков List (для хранения после
довательностей элементов), карты Мар (известные также как ассоциат ивны ем ассиеы ,
Параметризованные типы 57
Параметризованные типы
До BbmmaJava SE5 в контейнерах могли храниться только данные Object —единствен
ного универсального ranaJava. Однокорневая иерархия означает, что любой объект
может рассматриваться как Object, поэтому контейнер с элементами Object подойдет
для хранения любых объектов1.
При работе с таким контейнером вы просто помещаете в него ссылки на объекты,
а позднее извлекаете их. Но если контейнер способен хранить только Object, то при
помещении в него ссылки на другой объект происходит его преобразование к Object,
то есть утрата его «индивидуальности». При выборке вы получае1ге ссылку на Object,
а не ссылку на тип, который был помещен в контейнер. Как же преобразовать ее к кон
кретному типу объекта, помещенного в контейнер?
Задача решается тем же преобразованием типов, но на этот раз тип изменяется не по
восходящей линии (от частного к общему), а по нисходящей (от общего к частному).
Данный способ называется нисходящим преобразованием. В случае восходящего
быть, это записи о планах полетов всех малых самолетов, покидающих аэропорт.
Так появляется второй контейнер для малыхсамолетов; каждый раз, когда в системе
создается новый объект самолета, он также включается и во второй контейнер, если
самолет является малым. Далее некий фоновый процесс работает с объектами в этом
контейнере в моменты минимальной занятости.
Теперь задача усложняется: как узнать, когда нужно удалять объекты? Даже если вы
закончили работу с объектом, возможно, с ним продолжает взаимодействовать другая
часть системы. Этот же вопрос возникает и в ряде других ситуаций, и в программных
системах, где необходимо явно удалять объекты после завершения работы с ними
(например, в С++), он становится достаточно сложным.
Где хранятся данные объекта и как определяется время его жизни? В С++ на первое
место ставится эффективность, поэтому программисту предоставляется выбор. Для
достижения максимальной скорости исполнения место хранения и время жизни могут
определяться во время написания программы. В этом случае объекты размещаются
в стеке (такие переменные называются автоматическими) или в области статиче
ского хранилища. Таким образом, основным фактором является скорость создания
и уничтожения объектов, и это может быть неоценимо в некоторых ситуациях. Однако
при этом приходится жертвовать гибкостью, так как количество объектов, время их
жизни и типы должны быть точно известны на стадии разработки программы. При
решении задач более широкого профиля — разработки систем автоматизированного
проектирования (CAD), складского учета или управления воздушным движением —
это требование может оказаться слишком жестким.
Второй путь —динамическое создание объектов в области памяти, называемой куцей
(heap). В таком случае количество объектов, их точные типы и время жизци остаются
неизвестными до момента запуска программы. Все это определяется «на ходу» во время
работы программы. Если вам понадобится новый объект, вы просто создаете его в куче
тогда, когда потребуется. Так как управление кучей осуществляется динамически, во
время исполнения программы на выделение памяти из кучи требуется гораздо больше
времени, чем при выделении памяти в стеке. (Для выделения памяти в стеке достаточно
всего одной машинной инструкции, сдвигающей указатель стека вниз, а освобождение
осуществляется перемещением этого указателя вверх. Время, требуемое на выделение
памяти в куче, зависит от структуры хранилища.)
При использовании динамического подхода подразумевается, что объекты больщие
н сложные, таким образом, дополнительные затраты времени на выделение и осво
бождение памяти не окажут заметного влияния на процесс их создания. Потом, до
полнительная гибкость очень важнадля решения основных задач программирования.
BJava используется исключительно второй подход1. Каждый раз при создании объекта
используется ключевое слово new для построения динамического экземпляра.
Впрочем, есть и другой фактор, а именно время жизни объекта. В языках, поддер
живающих создание объектов в стеке, компилятор определяет продолжительность
существования объекта и может автоматически уничтожить его. Однако при создании
объекта в куче компилятор не имеет представления о сроках жизни объекта. В таких
Параллельное выполнение
Одной из фундаментальных концепций программирования является идея одновре
менного выполнения нескольких операций. Многие задачи требуют, чтобы программа
прервала свою текущую работу, решила какую-то другую задачу, а затем вернулась
в основной процесс. Проблема решалась разными способами. На первых порах програм
мисты, знающие машинную архитектуру, писали процедуры обработки прерываний,
то есть приостановка основного процесса выполнялась на аппаратном уровне. Такое
решение работало неплохо, но оно было сложным и немобильным, что значительно
усложняло перенос подобных программ на новые типы компьютеров.
Иногда прерывания действительно необходимы для выполнения операций задач,
критичных по времени, но существует целый класс задач, где просто нужно разбить
задачу на несколько раздельно выполняемых частей, так, чтобы программа быстрее
реагировала на внешние воздействия. Эти раздельно выполняемые части программы
называются потоками, а весь принцип получил название многозадачности, или парал-
лельнъих вычислений. Часто встречающийся пример многозадачности —пользователь
ский интерфейс. В программе, разбитой на потоки, пользователь может нажать кнопку
и получить быстрый ответ, не ожидая, пока программа завершит текущую операцию.
Обычно задачи всего лишь определяют схему распределения времени на однопроцес
сорном компьютере. Но если операционная система поддерживает многопроцессорную
обработку, каждая задача может быть назначена на отдельный процессор; так достига
ется настоящий параллелизм. Одно из удобных свойств встроенной в язык многозадач
ности состоит в том, что программисту не нужно знать, один процессор в системе или
несколько. Программа логически разделяется на потоки, и если машина имеет больше
одного процессора, она исполняется быстрее, без каких-либо специальных настроек.
Все это создает впечатление, что потоки использовать очень легко. Но тут кроется
подвох: совместно используемые ресурсы. Если несколько потоков пытаются одно
временно получить доступ к одному ресурсу, возникнут проблемы. Например, два про
цесса не могут одновременно посылать информацию на принтер. Для предотвращения
конфликта совместные ресурсы (такие, как принтер) должны блокироваться во время
использования. Поток блокирует ресурс, завершает свою операцию, а затем снимает
блокировку для того, чтобы кто-то еще смог получить доступ к ресурсу.
Поддержка параллельного выполнения встроена в H3biKjava, а с Bbnn^OMjava SE5
к ней добавилась значительная поддержка на уровне библиотек.
62 Глава 1 • Введение в объекты
Java и Интернет
E ^ n J a v a представляет собой очередной язык программирования, возникает вопрос:
чем же он так важен и почему преподносится как революционный шаг в разработке
программ? С точки зрения традиционных задач программирования ответ очевиден не
сразу. Хотя H3biKjava пригодится и при построении автономных приложений, самым
важным его применением было и остается программирование для сети World Wide Web.
Простая идея распространения информации между людьми имеет столько уровней слож
ности в своей реализации, что в целом ее решение кажется недостижимым. И все-таки
онажизненно необходима: примерно половинавсех задач программирования основана
именно на ней. Она задействована в решении разнообразных проблем, от обслуживания
заказов и операций по кредитным карточкам, до распространения всевозможных дан
ных —научных, правительственных, котировок акций... Список можно продолжать до
бесконечности. В прошлом для каждой новой задачи приходилось создавать отдельное
решение. Эти решения непросто создавать, еще труднее ими пользоваться, и пользо
вателю приходилось изучать новый интерфейс с каждой новой программой. Задача
клиент—серверных вычислений нуждается в более широком подходе.
Модули расширения
Одним из важнейших направлений в клиентском программировании стала разработ
ка модулей расширения (plug-ins). Этот подход позволяет программисту добавить
Java и Интернет 65
Языки сценариев
Разработка модулей расширения привела к появлению множества языков для напи
сания сценариев. Используя язык сценария, вы встраиваете клиентскую программу
прямо в HTML-страницу, а модуль, обрабатывающий данный язык, автоматически
активизируется при ее просмотре. Языки сценария обычно довольно просты для
изучения; в сущности, сценарный код представляет собой текст, входящий в состав
HTML-страницы, поэтому он загружается очень быстро, как часть одного запроса
к серверу во время получения страницы. Расплачиваться за это приходится тем, что
любой в силах просмотреть (и украсть) ваш код. Впрочем, вряд ли вы будете писать
что-либо заслуживающее подражания и утонченное на языках сценариев, поэтому
проблема копирования кода не так уж страшна.
Языком сценариев, который поддерживается практически любым браузером без уста
новки дополнительных модулей, является^уа8спр1 (имеющий весьма мало общего
cJava; имя было выбрано для того, чтобы «урвать» кусочек ycnexaJava на рынке).
К сожалению, исходные реализации JavaScript в разных браузерах довольно сильно
отличались друг от друга, и даже между разными версиями одного браузера. Стандар-
TH3aHHnJavaScript в форме ECMAScript была полезна, но потребовалось время, чтобы
ее поддержка появилась во всех браузерах (вдобавок компания Microsoft активно про
двигала собственный язык VBScript, отдаленно HanoMHHaBnmftJavaScript). В общем
случае разработчику приходится ограничиваться минимумом возможностей JavaS
cript, чтобы код гарантированно работал во всех браузерах. Что касается обработки
ошибок и отладки кода JavaScript, то занятие это в лучшем случае непростое. Лишь
недавно разработчикам удалось создать действительно сложную систему, написанную
HaJavaScript (компания Google, служба GMail), и это потребовало высочайшего на
пряжения сил и опыта.
Это показывает, что языки сценариев, используемые в браузерах, были предназначены
хтя решения круга определенных задач, в основном для создания более насыщенного
и интерактивного графического пользовательского интерфейса (GUI). Однако язык
сценариев может быть использован для решения 80 процентов задач клиентского про
граммирования. Ваша задача может как раз входить в эти 80 процентов. Поскольку
языки сценариев позволяют легко и быстро создавать программный код, вам стоит
сначала рассмотреть именно такой язык, перед тем как переходить к более сложным
технологическим решениям BpofleJava.
66 Глава 1 • Введение в объекты
Java
Если языки сценариев берут на себя 80 процентов задач клиентского программирова
ния, кому жетогда «по зубам» остальные 20 процентов? Для них наиболее популярным
решением сегодня я в л я е тс я ^ у а . Это не только мощный язык программирования,
разработанный с учетом вопросов безопасности, платформенной совместимости и ин
тернационализации, но также постоянно совершенствуемый инструмент, дополняемый
новыми возможностями и библиотеками, которые элегантно вписываются в решение
традиционно сложных задач программирования: многозадачности, доступа к базам
данных, сетевого программирования и распределенных вычислений. Клиентское
программирование HaJava сводится к разработке апплетов, а также к использованию
пакета Java Web Start.
Апплет —мини-программа, которая может исполняться только внутри браузера. Ап
плеты автоматически загружаются в составе веб-страницы (так же, как загружается,
например, графика). Когда апплет активизируется, он выполняет программу. Это одно
из преимуществ апплета —он позволяет автоматически распространять программы для
клиентов с сервера именно тогда, когда пользователю понадобятся эти программы, и не
раньше. Пользователь получает самую свежую версию клиентской программы, без вся
ких проблем и трудностей, связанных с переустановкой. В соответствии с идеологией
Java, программист создает только одну программу, которая автоматически работает на
всех компьютерах, где имеются браузеры со встроенным HHrepnpeTaTopoMjava. (Это
верно практически для всех компьютеров.) Так KaKjava является полноценным языком
программирования, как можно большая часть работы должна выполняться на стороне
клиента перед обращением к серверу (или после него). Например, вам не понадобится
пересылать запрос по Интернету, чтобы узнать, что в полученных данных или каких-то
параметрах была ошибка, а компьютер клиента сможет быстро начертить какой-либо
график, не ожидая, пока это сделает сервер и отошлет обратно файл с изображением.
Такая схема не только обеспечивает мгновенный выигрыш в скорости и отзывчивости,
но также снижает загрузку основного сетевого транспорта и серверов, предотвращая
замедление работы с Интернетом в целом.
Альтернативы
Честно говоря, amweTbiJava не оправдали начальных восторгов. При первом появле-
HnnJava все относились к апплетам с большим энтузиазмом, потому что они делали
возможным серьезное программирование на стороне клиента, повышали скорость
отклика и снижали загрузку канала для интернет-приложений. Апплетам предрекали
большое будущее.
И действительно, в Web можно встретить ряд очень интересных апплетов. И все же
массовый переход на апплеты так и не состоялся. Вероятно, главная проблема заклю
чалась в том, что загрузка 10-мегабайтного пакета для установки среды Java Runtime
Environment (JR E ) слишком пугала рядового пользователя. Тот факт, что компания
Microsoft не стала BK/m4aTbJRE в поставку Internet Explorer, окончательно решил судьбу
апплетов. Как бы то ни было, апплеты Java так и не получили широкого применения.
Впрочем, апплеты и приложения Java Web Start в некоторых ситуациях приносят
большую пользу. Если конфигурация компьютеров конечных пользователей нахо
дится под контролем (например, в организациях), применение этих технологий для
Java и Интернет 67
.NET и C#
Некоторое время основным conepHHKOMjava-апплетов считались компоненты ActiveX
от компании Microsoft, хотя они и требовали для своей работы наличия на машине
клиента Wiadows. Теперь Microsoft противопоставила^уа полноценных конкурентов:
это платформа .NET и язык программирования С#. Платформа .NET представляет
собой примерно то же самое, что и виртуальная машина Java (JV M ) и библиотеки
Java,an3biK C# имеет явное сходство с языком Java. Вне всяких сомнений, это лучшее,
что создала компания Microsoft в области языков и сред программирования. Конечно,
разработчики из Microsoft имели некоторое преимущество; они видели, что в Java
удалось, а что нет, и могли отталкиваться от этих фактов, но результат получился
вполне достойным. Впервые с момента своего появления yJava появился реальный
соперник. Разработчикам из Sun пришлось как следует взглянуть на С#, выяснить,
по каким причинам программисты могут захотеть перейти на этот язык, и приложить
максимум усилий для серьезного ynynrneHHaJava eJava SE5.
В данный момент основные сомнения вызывает вопрос о том, разрешит ли Microsoft
полностью переносить .NET надругие платформы. В Microsoft утверждают, что ника
кой проблемы в этом нет и проект Mono (;wzvwgo-mono.com) предоставляет частичную
реализацию .NET для Linux. Впрочем, раз реализация эта неполная, то пока Microsoft
не решит выкинуть из нее какую-либо часть, делать ставку на .NET как на межплат
форменную технологию еще рано.
Интернет и интрасети
Web представляет решение наиболее общего характера для клиент—серверных задач,
так что имеет смысл использовать ту же технологию для решения задач в частных слу
чаях; в особенности это касается классического клиент—серверного взаимодействия
шкутри компании. При традиционном подходе «клиент—сервер» возникают проблемы
с различиями в типах клиентских компьютеров, к ним добавляется трудность установки
зовых программ для клиентов; обе проблемы решаются веб-браузерами й программи
рованием на стороне клиента. Когда технология Web используется для формирования
информационной сети Внутри компании, ее называют интрасетью. Интрасети предо
ставляют гораздо большую безопасность в сравнении с Интернетом, потому что вы
мажете физически контролировать доступ к серверам вашей компании. Что касаётея
обучения, человеку, понимающему концепцию браузера, гораздо легче разобраться
в разных страницах и апплетах, так что время освоения новых систем сокращается.
68 Глава 1 • Введение в объекты
Резюме
Вы знаете, как выглядят процедурные программы: определения данных и вызовы
функций. Чтобы выяснить предназначение такой программы, необходимо приложить
усилие, просматривая функции и создавая в уме общую картину. Именно из-за этого
создание таких программ требует использования промежуточных средств —сами по
себе они больше ориентированы на компьютер, а не на решаемую задачу.
Так как ООП добавляет много новых понятий к тем, что уже имеются в процедурных
языках, естественно будет предположить, что кoдJava будет гораздо сложнее, чем ана
логичный метод на процедурном языке. Но здесь вас ждет приятный сюрприз: хорошо
написанную программу HaJava обычно гораздо легче понять, чем ее процедурный
аналог. Все, что вы видите — это определения объектов, представляющих понятия
пространства решения (а не понятия компьютерной реализации), и сообщения, посы
лаемые этим объектам, которые представляют действия в этом пространстве. Одно из
преимуществ ООП как раз и состоит в том, что хорошо спроектированную программу
можно понять, просто проглядывая исходные тексты. К тому же обычно приходится
писать гораздо меньше кода, поскольку многие задачи с легкостью решаются уже
существующими библиотеками классов.
ООП и a3bHcJava подходят не для всех. Очень важно сначала выяснить свои потреб
ности, чтобы решить, удовлетворит ли вас переход HaJava или лучше остановить свой
выбор на другой системе программирования (в том числе и на той, что вы сейчас исполь
зуете). Если вы знаете, что в обозримом будущем столкнетесь с весьма специфическими
потребностями или в вашей работе будут действовать ограничения, с которыми Java не
справляется, лучше рассмотреть другие возможности. (В особенности я рекомендую
присмотреться к языку Python, www.Python.org.) BыбиpaяJava, необходимо понимать,
какие еще доступны варианты и почему вы выбрали именно этот путь.
Все является объектом
Все эти различия упрощены в Java. Вы обращаетесь с любыми данными как с объ
ектом, и поэтому повсюду используется единый последовательный синтаксис. Хотя
вы обращаетесь со всеми данными как с объектом, идентификатор, которым вы ма
нипулируете, на самом деле представляет собой ссылку на объект1. Представьте себе
телевизор (объект) с пультом дистанционного управления (ссылка). Во время вла
дения этой ссылкой у вас имеется связь с телевизором, но при переключении канала
или уменьшения громкости вы распоряжаетесь ссылкой, которая, в свою очередь,
манипулирует объектом. А если вам захочется перейти в другое место комнаты, все
еще управляя телевизором, вы берете с собой «ссылку», а не сам телевизор.
Также пульт может существовать сам по себе, без телевизора. Таким образом, сам факт
наличия ссылки еще не значит наличия присоединенного к ней объекта. Например,
для хранения слова или предложения создается ссылка String:
String s;
! Этот вопрос очень важен. Некоторые программисты скажут: «понятно, это указатель», но это
предполагает соответствующую реализацию. Также ccbmKnJava по синтаксису более похожи
на ссылки С++, чем наего указатели. В первом издании книги я решил ввести новый термин
«дескриптор» (handle), потому что между ccbuncaMnJava и С++ существуют серьезные отличия.
Я основывался на опыте С++ и не хотел сбивать с толку программистов на этом языке, так
как большей частью именно они будут H3y4aTbJava. Во втором издании я решил прибегнуть
к более традиционному термину «ссылка», предполагая, что это поможет быстрее освоить
новые особенности языка, в котором и без моих новых терминов много необычного. Однако
есть люди, возражающие даже против термина «ссылка». Я прочитал в одной книге, что «со
вершенно неверно говорить, что Java поддерживает передачу объектов по ссылке», потому
что идентификаторы объектов Java на самом деле (согласно автору) являются ссылками на
объекты. И (он продолжает) все фактически передается по значению. Так что передача идет не
по ссылке, а «ссылка на объект передается по значению». Можно поспорить с тем, насколько
точны столь запутанные рассуждения, но я полагаю, что мое объяснение упрощает понимание
концепции и ничему не вредит (блюстители чистоты языка могут сказать, что это неправда,
но я всегда могу возразить, что речь идет всего лишь о подходящей абстракции).
72 Глава 2 • Все является объектом
Это не только значит «предоставьте мне новый объект string», но также указывает,
как создать строку посредством передачи начального набора символов.
Конечно, кроме String, Bjava имеется множество готовых типов. Но еще важнее то, что
вы можете создавать свои собственные типы. Вообще говоря, именно создание новых
типов станет вашим основным занятием при программировании HaJava, и именно его
мы будем рассматривать в книге.
1 В качестве примера можно привести пулы строк. Все строковые литералы и константы со
строковыми значениями автоматически помещаются в специальную статическую область
памяти.
Все объекты должны создаваться явно 73
Все числовые значения являются знаковыми, так что не ищите слова unsigned.
Размер типа boolean явно не определяется; указывается лишь то, что этот тип может
принимать значения true и false.
Классы-«обертки» позволяют создать в куче не-примитивный объект для представ
ления примитивного типа. Например:
char с = 'x';
Character ch = new Character(c);
74 Глава 2 • Все является объектом
и обратно:
char с = ch;
Массивы в Java
Фактически все языки программирования поддерживают массивы. Использование
массивов в С и С++ небезопасно, потому что массивы в этих языках представляют
собой обычные блоки памяти. Если программа попытается получить доступ к мас
сиву за пределами его блока памяти или использовать память без предварительной
инициализации (типичные ошибки при программировании), последствия могут быть
непредсказуемы.
Одной из основных ц е л е й ^ у а является безопасность, поэтому многие проблемы,
досаждавшие программистам на С и С++, не существуют eJava. Массив B ja v a гаран
тированно инициализируется, к нему невозможен доступ за пределами его границ.
Проверка границ массива обходится относительно дорого, как и проверка индекса во
время выполнения, но предполагается, что повышение безопасности и подъем произ
водительности стоят того (к тому ж е ^ у а иногда может оптимизировать эти операции).
Объекты никогда не приходится удалять 75
При объявлении массива объектов на самом деле создается массив ссылок, и каждая
из этих ссылок автоматически инициализируется специальным значением, представ
ленным ключевым словом null. Оно означает, что ссылка на самом деле не указывает
на объект. Вам необходимо присоединять объект к каждой ссылке перед тем, как ее
использовать, или при попытке обращения по ссылке null во время исполнения про
граммы произойдет ошибка. Таким образом, типичные ошибки при работе с массивами
eJava предотвращаются заблаговременно.
Также можно создавать массивы простейших типов. И снова компилятор гарантирует
инициализацию —выделенная для нового массива память заполняется нулями.
Массивы будут подробнее описаны в последующих главах.
{
int x = 12 ;
{
int x = 96; // неверно
}
>
Компилятор объявит, что переменная x уже была определена. Таким образом, возмож
ность языков С и С++ «замещать » переменные из внешней области действия не под
держивается. С оздатели^уа посчитали, что она приводит к излишнему усложнению
программ.
языков использовали ключевое слово class в смысле «Я собираюсь описать новый тип
объектов». За ключевым словом class следует имя нового типа. Например:
class ATypeName { /* Тело класса */ }
Эта конструкция вводит новый тип. Тело класса состоит из комментария (символы
/* и */ и все, что находится между ними, — см. далее в этой главе), и пользы от него
немного, но вы можете теперь создавать объект этого типа ключевым словом new:
ATypeName а = new ATypeName();
Впрочем, объекту нельзя «приказать» что-то сделать (то есть послать ему необходимые
сообщения) до тех пор, пока для него не будут определены методы.
Поля и методы
При определении класса (строго говоря, вся ваша работа на Java сводится к опре
делению классов, созданию объектов этих классов и отправке сообщений этим объ
ектам) в него можно включить две разновидности элементов: поля (fields) (иногда
называемые переменными класса) и методы (methods) (еще называемые функциями
класса). Поле представляет собой объект любого типа, с которым можно работать по
ссылке, или объект примитивного типа. Если используется ссылка, ее необходимо
инициализировать, чтобы связать с реальным объектом (ключевым словом new, как
было показано ранее).
Каждый объект использует собственный блок памяти для своих полей данных; со
вместное использование обычных полей разными объектами класса невозможно.
Пример класса с полями:
class DataOnly {
int i;
double d;
boolean b;
>
Такой класс ничего не делает, кроме хранения данных, но вы можете создать объект
этого класса:
DataOnly data = new DataOnly();
Полям класса можно присваивать значения, но для начала необходимо узнать, как
обращаться к членам объекта. Для этого сначала указывается имя ссылки на объект,
затем следует точка, а далее имя члена, принадлежащего объекту:
ссылка. член
Например:
data.i = 47;
data.d = 1 .1 ;
data.b = false;
Также ваш объект может содержать другие объекты, данные которых вы хотели бы
изменить. Для этого просто продолжите «цепочку из точек». К примеру:
^Plane.leftTank.capacity = 100;
78 Глава 2 * Все является объектом
Класс DataOnly не способен ни на что, кроме хранения данных, так как в н е м отсут
ствуют методы. Чтобы понять, как они работают, необходимо разобраться, что такое
аргументы и возвращаемые значения. Вскоре мы вернемся к этой теме.
Например, представьте, что у вас есть метод f(), вызываемый без аргументов, который
возвращает значение типа int. Если у вас имеется в наличии объект а, для которого
может быть вызван метод f(), в вашей власти использовать следующую конструкцию:
int x= a.f();
Список аргументов
Список аргументов определяет, какая информация передается методу. Как легко до
гадаться, эта информация —как и все eJava —воплощается в форме объектов, поэтому
в списке должны быть указаны как типы передаваемых объектов, так и их имена. Как и
в любой другой ситуации Bjava, где мы вроде бы работаем с объектами, на самом деле
используются ссылки2. Впрочем, тип ссылки должен соответствовать типу передава
емых данных. Если предполагается, что аргумент является строкой (то есть объектом
String), вы должны передать именно строку или ожидайте сообщения об ошибке.
Статические методы, о которых вы узнаете немного позже, вызываются для класса, а не для
объекта.
: За исключением уже упомянутых «специальных» типов данных: boolean, byte, short, char,
int. float, long, double. Впрочем, в основном вы будете передавать объекты, а значит, ссылки
на них.
80 Глава 2 • Все является объектом
int storage(String s) {
return s.length() * 2 ;
>
Метод указывает, сколько байтов потребуется для хранения данных определенной
строки. (Строки состоят из символов char, размер которых — 16 битов, или 2 байта;
это сделано для поддержки набора символов Юникод.) Аргумент имеет тип String
и называется s. Получив объект s, метод может работать с ним точно так же, как и с
любым другим объектом (то есть отправлять ему сообщения). В данном случае вы
зывается метод length(), один из методов класса String; он возвращает количество
символов в строке.
Также обратите внимание на ключевое слово return, выполняющее два действия. Во-
первых, оно означает: «выйти из метода, все сделано». Во-вторых, если метод возвра
щает значение, это значение указывается сразу же за командой return. В нашем случае
возвращаемое значение —это результат вычисления s . length() * 2.
Метод может возвращать любой тип, но если вы не хотите пользоваться этой возможно
стью, следует указать, что метод возвращает void. Ниже приведено несколько примеров:
boolean flag() { return true; >
float naturalLogBase() { return 2.718; }
void nothing() { return; }
void nothing 2 () {}
Когда выходным типом является void, ключевое слово return нужно лишь для заверше
ния метода, поэтому при достижении конца метода его присутствие необязательно. Вы
можете покинуть метод в любой момент, но если при этом указывается возвращаемый
тип, отличный от void, то компилятор заставит вас (сообщениями об ошибках) вернуть
подходящий тип независимо от того, в каком месте метода было прервано выполнение.
К этому моменту может сложиться впечатление, что программа — это просто набор
объектов со своими методами, которые принимают другие объекты в качестве аргумен
тов и отправляют им сообщения. По большому счету так оно и есть, но в следующей
главе вы узнаете, как производить кропотливую низкоуровневую работу с принятием
решений внутри метода. В этой главе достаточно рассмотрения на уровне отправки
сообщений.
Видимость имен
Проблема управления именами существует в любом языке программирования. Если
имя используется в одном из модулей программы и оно случайно совпало с име
нем в другом модуле у другого программиста, то как отличить одно имя от другого
и предотвратить их конфликт? В С это определенно является проблемой, потому что
Создание программы на Java 81
программа с трудом поддается контролю в условиях «моря» имен. Классы С++ (на
которых основаны ^accbiJava) скрывают функции внутри классов, поэтому их имена
не пересекаются с именами функций других классов. Однако в С++ дозволяется ис
пользование глобальных данных и глобальных функций, соответственно, конфликты
полностью не исключены. Для решения означенной проблемы в С++ введены про
странства имен (namespaces), которые используют дополнительные ключевые слова.
В языке Java для решения этой проблемы было использовано свежее решение. Для
создания уникальных имен библиотек разработчики Java предлагают использовать
доменное имя, записанное в обратном порядке, так как эти имена всегда уникальны.
Мое доменное имя —MindViezeJ.net, и утилиты моейшрограммной библиотеки могли
бы называться net.mindviezej.utility.foibles. За перевернутым доменным именем следует
перечень каталогов, разделенных точками.
В eepcHHxJava 1.0 и 1.1 доменные суффиксы corn, edu, org, net по умолчанию запи
сывались заглавными буквами, таким образом, имя библиотеки выглядело так: NET.
mindview.utility.foibles. В процессе разработки Java 2 было обнаружено, что принятый
подход создает проблемы, и с тех пор имя пакета записывается строчными буквами.
Такой механизм означает, что все ваши файлы автоматически располагаются в своих
собственных пространствах имен и каждый класс в файле должен иметь уникальный
идентификатор. Язык сам предотвращает конфликты имен.
Как правило, целый набор классов импортируется именно таким образом (вместо
импортирования каждого класса по отдельности).
В данном примере как s t i . i , так и s t2 .i имеют одинаковые значения, равные 47, по
тому что они ссылаются на один блок памяти.
Существует два способа обратиться к статической переменной. Как было видно выше,
на нее можно ссылаться по имени объекта, например st2. i. Также возможно обратиться
к ней прямо через имя класса; для нестатических членов класса такая возможность
отсутствует.
StaticTest.i++;
Или, поскольку метод increment() является статическим, можно вызвать его с прямым
указанием класса:
Inc rementable.increment();
Если применительно к полям ключевое слово static радикально меняет способ опреде
ления данных (статические данные существуют на уровне класса, в то время как неста
тические данные существуют на уровне объектов), но в отношении методов изменения
не столь принципиальны. Одним из важных применений static является определение
методов, которые могут вызываться без объектов. В частности, это абсолютно необхо
димо для метода main(), который представляет собой точку входа в приложение.
Если вы вернетесь к началу, выберете пакет j a va.lang, а затем класс System, то увидите,
что он имеет несколько полей. При выборе поля out обнаруживается, что оно представ
ляет собой статический объект PrintStream. Так как поле описано с ключевым словом
static, вам не понадобится создавать объекты ключевым словом new. Действия, которые
можно выполнять с объектом out, определяются его типом: PrintStream. Для удобства
в описание этого типа включена гиперссылка, и если щелкнуть на ней, вы обнаружите
список всех доступных методов. Этих методов довольно много, и они будут позже рас
смотрены. Сейчас нас интересует только метод println(), вызов которого фактически
означает: «вывести то, что передано методу, на консоль и перейти на новую строку».
Таким образом, в любую программу HaJava можно включить вызов вида System.out.
println(''KaKofi-To текст"), чтобы вывести сообщение на консоль.
Имя класса совпадает с именем файла. Когда вы создаете отдельную программу, по
добную этой, один из классов, описанных в файле, должен иметь совпадающее с ним
название. (Если это условие нарушено, компилятор сообщит об ошибке.) Одноимен
ный класс должен содержать метод с именем main() со следующей сигнатурой и воз
вращаемым типом:
public static void main<String[] args) {
Ключевое слово public обозначает, что метод доступен для внешнего мира (об этом
подробно рассказывает глава 5). Аргументом метода main() является массив строк.
В данной программе массив args не используется, но компилятор^уа настаивает на
его присутствии, так как массив содержит параметры, переданные программе в ко
мандной строке.
Строка, в которой распечатывается дата, довольно интересна:
System.out.println(new Date());
Аргумент представляет собой объект Date, который создается лишь затем, чтобы
передать свое значение (автоматически преобразуемое в String) методу println().
Как только команда будет выполнена, объект Date становится ненужным, уборщик
мусора заметит это, и в конце концов сам удалит его. Нам не нужно беспокоиться об
его удалении.
Обратившись к доку м ен тац и и ^К с сайта http://java.sun.com, вы увидите, что класс
System содержит много других методов для получения интересных результатов (одной
из сильных CTopoHjava является обширный набор стандартных библиотек). Пример:
//: object/ShowProperties.java
Компиляция и выполнение
Чтобы скомпилировать и выполнить эту программу, а также все остальные программы
в книге, вам понадобится среда разработки Java. Существует множество различных
сред разработок от сторонних производителей, но в этой книге мы предполагаем, что
вы избрали бесплатную cpeдyJDK (Java Developer’s Kit) от фирмы Sun. Если же вы
используете другие системы разработки программ1, вам придется просмотреть их до
кументацию, чтобы узнать, как компилировать и запускать программы.
Часто используется компилятор IBM jikes, так как он работает намного быстрее компилятора
javac от Sun (впрочем, при построении групп файлов с использованием Ant различия не столь
заметны). Также существуют проекты с открытыми исходными текстами, направленные на
создание компиляторов, сред времени исполнения и библиoтeкJava.
86 Глава 2 • Все является объектом
1 Существует один фактор, который следует учитывать при работе с JVM на платформе MS
Windows. Для вывода сообщений на консоль используется кодировка символов DOS (cp866).
Так как для Windows по умолчанию принята кодировка Windows-1251, то очень часто бывает
так, что русскоязычные сообщения не удается прочитать с экрана, они будут казаться иерогли
фами. Для исправления ситуации можно перенаправлять поток вывода следующим способом:
java HelloDate > result.txt, тогда вывод программы окажется в файле resuft.txt (годится любое
другое имя) и его можно будет прочитать. Этот подход применим к любой программе. Или
же просто используйте одну из множества программ-«знакогенераторов» (например, keyrus),
работая с экраном MS-DOS. Тогда вам не потребуются дополнительные действия попере-
направлению. Плюс станет возможной работа под отладчиком JDB. Третий вариант, более
сложный, но обеспечивающий вам независимость от машины, заключается во встраивании
перекодирования в свою программу посредством методов setOut и setErr (обходит байт-
ориентированность потока PrintStream). Российские программисты давно (а отсчет идет
с 1997 года) приспособились к этой ситуации. Одно из решений, позволяющее печатать на
консоль в правильной кодировке, можно найти на сайте www.javaportal.nj (статья «Русские
буквы и не только..>). (Нужно загрузить класс http://www.javaportal.ru/java/artides/msdiars/
CodepagePrintStream.java, скомпилировать его и описать в переменной окружения. Данный
путь лучше отложить до ознакомления с соответствующей темой (глава 12).) —Пргшеч. ред.
Комментарии и встроенная документация 87
Документация в комментариях
Пожалуй, основные проблемы с документированием кода связаны с его сопровожде
нием. Если код и его документация существуют раздельно, корректировать описание
программы при каждом ее изменении становится задачей не из легких. Решение выгля
дит очень просто; совместить код и документацию. Проще всего объединить их в одном
файле. Но для полноты картины понадобится специальный синтаксис комментариев,
чтобы помечать документацию, и инструмент, который извлекал бы эти комментарии
н оформлял их в подходящем виде. Именно это было сделано Bjava.
Инструмент для извлечения комментариев называется javadoc, он является частью пакета
JDK. Некоторые возможности компилятора^уа используются в немдля поиска пометок
в комментариях, включенных в ваши программы. Он не только извлекает помеченную
информацию, но также узнает имя класса или метода, к которому относится данный
фрагмент документации. Таким образом, с минимумом затраченных усилий можно
создать вполне приличную сопроводительную документацию для вашей программы.
Результатом работы программы javadoc является HTML-файл, который можно просмо
треть в браузере. Таким образом, утилита javadoc позволяет создавать и поддерживать
единый файл с исходным текстом и автоматически строить полезную документацию.
В результате получается простой и практичный стандарт по созданию документации,
поэтому мы можем ожидать (и даже требовать) наличия документации для всех би-
6_THOTeKjava.
88 Глава 2 • Все является объектом
Синтаксис
Все команды javadoc находятся только внутри комментариев /**. Комментарии, как
обычно, завершаются последовательностью */• Существует два основных способа ра
боты с javadoc: встраивание HTML-текста или использование разметки документации
(тегов). Автономные теги документации —это команды, которые начинаются симво
лом @и размещаются с новой строки комментария. (Начальный символ * игнорирует
ся.) Встроенные теги документации могут располагаться в любом месте комментария
javadoc, также начинаются со знака @, но должны заключаться в фигурные скобки.
Существуют три вида документации в комментариях для разных элементов кода:
класса, переменной и метода. Комментарий к классу записывается прямо перед его
определением; комментарий к переменной размещается непосредственно перед ее
определением, а комментарии к методу тоже записывается прямо перед его опреде
лением. Простой пример:
//: object/Documentationl.java
/** Комментарий к классу */
public class Documentationl {
/** Комментарий к переменной */
public int ij
/** Комментарий к методу */
public void f() {}
> I I I :~
Заметьте, что javadoc обрабатывает документацию в комментариях только для членов
класса с уровнем доступа public и protected. Комментарии для членов private и чле
нов с доступом в пределах пакета игнорируются, и документация по ним не строится.
(Впрочем, флаг -p riv ate включает обработку и этих членов.) Это вполне логично,
поскольку только public - и protected-члены доступны вне файла, и именно они инте
ресуют программиста-клиента.
Результатом работы программы является HTML-файл в том же формате, что и остальная
документация ^7inJava, так что пользователям будет привычно и удобно просматривать
и вашу документацию. Попробуйте набрать текст предыдущего примера, «пропустите»
его через javadoc и просмотрите полученный HTML-файл, чтобы увидеть результат.
Встроенный HTML
Javadoc вставляет команды HTML в итоговый документ. Это позволяет полностью
использовать все возможности HTML; впрочем, данная возможность прежде всего
ориентирована на форматирование кода:
Комментарии и встроенная документация 89
//: object/Documentation2.java
j**
* <pre>
* System.out.pnintln(new DateQ);
* </pre>
*/
/l/:~
Вы можете использовать HTML точно так же, как в обычных страницах, чтобы при
вести описание к нужному формату:
//: object/Documentation3.java
/**
* Можно <еш>даже</еш> вставить список:
* <ol>
* <li> Пункт первый
* <li> Пункт второй
* <li> Пункт третий
* </ol>
*/
///:~
Javadoc игнорирует звездочки в начале строк, а также начальные пробелы. Текст пере
форматируется таким образом, чтобы он соответствовал виду стандартной докумен
тации. Не используйте заголовки вида <hl> или <h2> во встроенном HTML, потому
что javadoc вставляет свои собственные заголовки, и ваши могут с ними «пересечься».
Встроенный HTML-код поддерживается всеми типами документации в комментари
ях —для классов, переменных или методов.
>i •
Примеры тегов
Далее описаны некоторые из тегов javadoc, используемых при документировании
программы. Прежде чем применять javadoc для создания чего-либо серьезного, про
смотрите руководство по нему в документации naKeTaJDK, чтобы получить полную
информацию о его использовании.
{@docRoot}
Позволяет получить относительный путь к корневой папке, в которой находится до
кументация. Полезен при явном задании ссылок на страницы из дерева документации.
{@inheritDoc}
Наследует документацию базового класса, ближайшего к документируемому классу,
в текущий файл с документацией.
@version
Имеет следующую форму:
@version информация-о-версии
Поле и н ф о р м а ц и я - о - в е р с и и содержит ту информацию, которую вы сочли нужным
включить. Когда в командной строке javadoc указывается опция -version, в созданной
документации специально отводится место, заполняемое информацией о версиях.
@author
Записывается в виде:
0author информация-об-авторе
Предполагается, что поле информация-об-авторе представляет собой имя автора, хотя
в него также можно включить адрес электронной почты и любую другую информацию.
Когда в командной строке javadoc указывается опция -author, в созданной документации
сохраняется информация об авторе.
Для создания списка авторов можно записать сразу несколько таких тегов, но они
должны размещаться последовательно. Вся информация об авторах объединяется
в один раздел в сгенерированном коде HTML.
@since
Тег позволяет задать версию кода, с которой началось использование некоторой воз
можности. В частности, он присутствует в HTM L-документации noJava, где служит
для указания BepcmiJDK.
@param
Полезен при документировании методов. Форма использования:
@param имя-параметра описание
где имя-параметра —это идентификатор в списке параметров метода, а описание —текст
описания, который можно продолжить на несколько строк. Описание считается за
вершенным, когда встретится новый тег. Можно записывать любое количество тегов
@рагат, по одному для каждого параметра метода.
@return
Форма использования:
0 return описание
где описание объясняет, что именно возвращает метод. Описание может состоять из
нескольких строк.
Комментарии и встроенная документация 91
@throws
Исключения будут рассматриваться в главе 9. В двух словах это объекты, которые
можно «инициировать» (throw) в методе, если его выполнение потерпит неудачу.
Хотя при вызове метода создается всегда один объект исключения, определенный
метод может вырабатывать произвольное количество исключений, и все они требуют
описания. Соответственно, форма тега исключения такова:
0 throws полное-имя-класса описание
где полное-имя-класса дает уникальное имя класса исключения, который где-то опре
делен, а описание (которое может продолжаться в последующих строках) объясняет,
почему данный метод способен создавать это исключение при своем вызове.
@deprecated
Тег используется для пометки устаревших возможностей, замещенных новыми
н улучшенными. Он сообщает о том, что определенные средства программы не следует
использовать, так как в будущем они, скорее всего, будут убраны. При использовании
метода с пометкой @deprecated компилятор выдает предупреждение. BJava SE5 тег
gdeprecated был заменен аннотацией 0Deprecated (см. далее).
Пример документации
Вернемся к нашей первой программе HaJava, но на этот раз добавим в нее комментарии
со встроенной документацией:
//: object/HelloDate.java
i4 >0rt java.util.*;
Резюме
В этой главе я постарался привести информацию о программировании H aJava, до
статочную для написания самой простой программы. Также был представлен обзор
языка и некоторых его основных свойств. Однако примеры до сих пор имели форму
«сначала это, потом это, а после что-то еще». В следующих двух главах будут пред
ставлены основные операторы, используемые при программировании HaJava, а также
способы передачи управления в вашей программе.1
Упражнения
В других главах упражнения будут распределяться по тексту главы. Поскольку в этой
главе мы только рассмотрели принципы написания простейших программ, упражнения
собраны в конце.
В круглых скобках после номера указывается сложность упражнения по шкале от 1
до 10.
Решения некоторых упражнений приведены в электронном документе The Thinking in
Java Annotated Solution Guide, который можно приобрести на сайте www.MindView.net.
1. (2) Создайте класс с полями in t и char, которые не инициализируются в программе.
Выведите их значения, чтобы убедиться в том, 4ToJava выполняет инициализацию
по умолчанию.
2. (1) На основании примера HelloDate.java в этой главе напишите программу «Привет,
мир», которая просто выводит это сообщение. Программа будет содержать только
один метод (тот, который исполняется при запуске программы —main()). Не забудьте
объявить его статическим (s ta tic ) и включите список аргументов, даже если вы не
будете его использовать. Скомпилируйте программу с помощью javac и запустите
на исполнение из java. Если вы используете не JDK, а другую среду разработки
программ, выясните, как в ней компилируются и запускаются программы.
3. (1) Найдите фрагмент кода с классом ATypeName и сделайте из него программу, при
годную для компиляции и запуска.
4. (1) Сделайте то же для кода с использованием класса DataOnly.
5. (1) Измените упражнение 4 так, чтобы данным класса DataOnly присваивались
значения, а затем распечатайте их в методе main().
6. (2) Напишите программу, включающую метод storage(), приведенный ранее в этой
главе.
7. ( 1 ) Превратите фрагменты кода с классом lncrementable в работающую программу.
8. (3) Напишите программу, которая демонстрирует, что независимо от количества
созданных объектов класс содержит только один экземпляр поля static.
9. (2) Напишите программу, демонстрирующую автоматическую упаковку прими
тивных типов.
10. (2) Напишите программу, которая выводит три параметра командной строки. Для
получения аргументов вам потребуется обращение к элементам массива строк
(String).
Операторы Java
Оператор получает один или несколько аргументов и создает на их основе новое значе
ние. Форма передачи аргументов несколько иная, чем при вызове метода, но эффект тбт
же самый. Сложение (+), вычитание и унарный минус (-), умножение (*), деление (/)
и присваивание (=) работают одинаково фактически во всех языках программирования.
Все операторы работают с операндами и выдают какой-то результат. Вдобавок некото
рые операторы могут изменить значение операнда. Это называется побочньшэффектом.
Как правило, операторы, изменяющие значение своих операндов, используются именно
ради побочного эффекта, но вы должны помнить, что полученное значение может быть
использовано в программе и обычным образом, независимо от побочных эффектов.
Почти все операторы работают только с примитивами. Исключениями являются =,
= = и !=, которые могут быть применены к объектам (и нередко создают изрядную
путаницу). Вдобавок класс String поддерживает операции + и +=.
Приоритет
Приоритет операций определяет порядок вычисления выражений с несколькими
операторами. В Java существуют конкретные правила для определения очередности
вычислений. Легче всего запомнить, что деление и умножение выполняются раньше
сложения и вычитания. Программисты часто забывают правила предшествования, по
этому для явного задания порядка вычислений следует использовать круглые скобки.
Например, взгляните на команды (1) и (2):
//: operators/Precedence.java
Присваивание
Присваивание выполняется оператором =. Трактуется он так: «взять значение из
правой части выражения (часто называемое просто значением) и скопировать его
в левую часть (часто называемую именующгш выражением)». Значением может быть
любая константа, переменная или выражение, но в качестве именующего выражения
обязательно должна использоваться именованная переменная (то есть для хранения
значения должна выделяться физическая память). Например, вы можете присвоить
постоянное значение переменной:
а = 4
class Tank {
int levelj
продолжение &
98 ГлаваЗ • Операторы
>
public class Assignment {
public static void main(String[] args) {
Tank tl = new Tank();
Tank t2 e new Tank();
tl.level = 9;
t2.1evel = 47;
print("l: tl.level: " + tl.level +
", t 2 .1 evel: " + t 2 .1 evel);
tl * t 2 j
print(" 2 : tl.level: " + tl.level +
", t 2 .1 evel: " + t 2 .1 evel);
tl.level = 27;
print("3: tl.level: " + tl.level +
", t 2 .1 evel: " + t 2 .1 evel);
>
} /* Output:
1: tl.level: 9, t2.1evel: 47
2: tl.level: 47, t2.1evel: 47
3: tl.level: 27, t2.1evel: 27
*///:~
Класс предельно прост, и два его экземпляра (tl и t 2 ) создаются внутри метода
Tank
main(). Переменной level для каждого экземпляра придаются различные значения,
а затем ссылка t 2 присваивается tl, в результате чего tl изменяется. Во многих языках
программирования можно было ожидать, что tl и t 2 будут независимы все время, но
из-за присваивания ссылок изменение объекта tl отражается на объекте t 2 ! Это про
исходит из-за того, что tl и t 2 содержат одинаковые ссылки, указывающие на один
объект. (Исходная ссылка, которая содержалась в tl и указывала на объект со значе
нием 9, была перезаписана во время присваивания и фактически потеряна; ее объект
будет вскоре удален уборщиком мусора.)
Этот феномен совмещения имен часто называют синонимией (aliasing), и именно она
является основным способом работы с объектами eJava. Но что делать, если совме
щение имен нежелательно? Тогда можно пропустить присваивание и записать:
tl.level = t 2 .1 evel;
При этом программа сохранит два разных объекта, а не «выбросит» один из них, «при
вязав» ссылки tl и t2 к единственному объекту. Вскоре вы поймете, что прямая работа
с полями данных внутри объектов противоречит принципам объектно-ориентированной
разработке. Впрочем, это непростой вопрос, так что пока вам достаточно запомнить,
что присваивание объектов может таить в себе немало сюрпризов.
2. (1) Создайте класс с полем типа float. Используйте его для демонстрации совме
щения имен.
Совмещение имен во время вызова методов
Совмещение имен также может происходить при передаче объекта методу:
//: operators/PassObject.java
// Передача объектов методам может работать
// не так, как вы привыкли.
import static net.mindview.util.Print.*;
Операторы Java 99
class Letter {
char с;
}
public class PassObject {
static void f(Letter у) {
y.c = 'z*;
}
public static void main(String[] args) {
Letter x = new Letter();
x.c = 'a';
print("l: x.c: " + x.c);
f(x );
print("2 : x.c: " + x.c);
}
> /* Output
1 : x.c: a
2 : x.c: z
*} ///:~
Математические операторы
Основные математические операторы остаются неизменными почти во всех языках
программирования: сложение (+), вычитание (-), деление (/), умножение (*) и остаток
от деления нацело (%). При делении нацело выполняется усечение, а не округление
результата.
BJava также используется укороченная форма записи для того, чтобы одновременно
произвести операцию и присваивание. Она обозначается оператором с последующим
знаком равенства и работает одинаково для всех операторов языка (когда в этом есть
смысл). Например, чтобы прибавить 4 к переменной x и присвоить результат x, ис
пользуйте команду x += 4.
Следующий пример демонстрирует использование математических операторов:
//: operators/MathOps.java
// Демонстрация математических операций,
i^?ort java.util.*;
i^>ort static net.mindview.util.Print.*;
v - w : 0.47753322
V * w : 0.028358962
v / w : 9.940527
U += V : 10.471473
u -= v : 9.940527
U *= v : 5.2778773
u /= v : 9.940527
*///:~
Для получения случайных чисел создается объект Random. Если он создается без па-
paMeTpoB,Java использует текущее время для раскрутки генератора, чтобы при каж
дом запуске программы выдавались разные числа. Однако в примерах, приведенных
в книге, результаты должны быть по возможности стабильными, чтобы их можно
было проверить внешними средствами. Если при создании объекта Random указывается
начальное число, то при каждом выполнении программы будет генерироваться одна
и та же серия значений1. Чтобы результаты стали более разнообразными, удалите на
чальное число из примеров.
Программа генерирует различные типы случайных чисел, вызывая соответствующие
методы объекта Random: nextInt() и nextFloat() (также можно использовать nextLong()
и nextDouble()). Аргумент nextInt() задает верхнюю границу генерируемых чисел.
Нижняя граница равна 0, но для предотвращения возможного деления на 0 результат
смещается на 1.
4. (2) Напишите программу, которая вычисляет скорость для постоянных значений
расстояния и времени.
но читающий код может запутаться, так что яснее будет написать так:
x = а * (-b);
Автоувеличение и автоуменьшение
В Java, как и С, существует множество различных сокращений. Сокращения могут
упростить написание кода, а также упростить или усложнить его чтение.
1В колледже, в котором я учился, число 47 считалось «волшебным» — тогда это и вошло в при
вычку.
102 ГлаваЗ • Операторы
Операторы сравнения
Операторы сравнения выдают логический (boolean) результат. Они проверяют, в ка
ком отношении находятся значения их операндов. Если условие проверки истинно,
оператор выдает true, а если ложно —false. К операторам сравнения относятся сле
дующие: «меньше чем» (<), «больше чем» (>), «меньше чем или равно» (<=), «больше
чем или равно» (>=), «равно» (==) и «не равно» (! =). «Равно» и «не равно» работают
для всех примитивных типов данных, однако остальные сравнения не применимы
к типу boolean.
> /* Output:
true
*///:~
На этот раз результат окажется «истиной» (true), как и предполагалось. Но все не так
просто, как кажется. Если вы создадите свой собственный класс вроде такого:
104 Глава 3 • Операторы
//: operators/EqualsMethod2.java
// Метод equals() по умолчанию не сравнивает содержимое
class Value {
int ij
>
public class EqualsMethod2 {
public static void main(String[] args) {
Value vl = new Value();
Value v2 = new Value();
vl.i = v 2 .i = 100 ;
System.out.println(vl.equals(v2));
}
} /* Output:
false
*///:~
мы вернемся к тому, с чего начинали: результатом будет false. Дело в том, что метод
equals() по умолчанию сравнивает ссылки. Следовательно, пока вы не переопре
делите этот метод в вашем новом классе, то и не получите желаемого результата.
К сожалению, переопределение будет рассматриваться только в главе 8, а пока
осторожность и общее понимание принципа работы equals() позволит избежать
некоторых неприятностей.
Большинство классов библиотек^уа реализуют метод equals() по-своему, сравнивая
содержимое объектов, а не ссылки на них.
5. (2) Создайте класс Dog, содержащий два поля типа String: name и says. В методе
создайте два объекта Dog с разными именами (spot и scruffy) и сообщениями.
main()
Выведите значения обоих полей для каждого из объектов.
6. (3) В упражнении 5 создайте новую ссылку на Dog и присвойте ее объекту spot.
Сравните ссылки оператором == и методом equals().
Логические операторы
Логические операторы И (&&), ИЛИ (| |) и НЕ (!) производят логические значения
true и false, основанные на логических отношениях своих аргументов. В следующем
примере используются как операторы сравнения, так логические операторы:
//: operators/Bool.java
// Операторы сравнений и логические операторы,
import java.util.*;
import static net.mindview.util.Print.*;
Ускоренное вычисление
При работе с логическими операторами можно столкнуться с феноменом, н а
зываемым «ускоренным вычислением». Это значит, что выражение вычисляется
только до тех пор, пока не станет очевидно, что оно принимает значение «истина»
или «ложь». В результате некоторые части логического выражения могут быть
проигнорированы в процессе сравнения. Следующий пример демонстрирует
ускоренное вычисление:
106 Глава 3 • Операторы
//: operators/ShortCircuit.java
// Демонстрация ускоренного вычисления
// при использовании логических операторов,
import static net.mindview.util.Print.*;
Естественно было бы ожидать, что все три метода должны выполняться, но результат
программы показывает другое. Первый метод возвращает результат true, поэтому
вычисление выражения продолжается. Однако второй метод выдает результат false.
Так как это автоматически означает, что все выражение будет равно false, зачем про
должать вычисления? Только лишняя трата времени. Именно это и стало причиной
введения в язык ускоренного вычисления; отказ от лишних вычислений обеспечивает
потенциальный выигрыш в производительности.
Литералы
Обычно, когда вы записываете в программе какое-либо значение, компилятор точ
но знает, к какому типу оно относится. Однако в некоторых ситуациях однозначно
определить тип не удается. В таких случаях следует помочь компилятору определить
точный тип, добавив дополнительную информацию в виде определенных символьных
Литералы 107
Экспоненциальная запись
Экспоненциальные значения записываются, по-моему, очень неудачно:
//: operators/Exponents.java
// "e" означает "10 в степени".
1 Джон Кирхем пишет: «Я начал программировать в 1960 году на FORTRAN II, используя ком
пьютер IBM 1620. В то время, в 60-е и 70-е годы, FORTRAN использовал только заглавные
буквы. Возможно, это произошло потому, что большинство старых устройств ввода были теле
тайпами, работавшими с 5-битовым кодом Бодо, который не поддерживал строчные буквы.
Буква E в экспоненциальной записи также была заглавной и не смешивалась с основанием
натурального логарифма e, которое всегда записывается маленькой буквой. Символ E просто вы
ражал экспоненциальный характер, то есть обозначал основание системы счисления — обычно
таким было 10. В те годы программисты широко использовали восьмеричную систему. И хотя
я и не замечал такого, но если бы я увидел восьмеричное число в экспоненциальной форме,
я бы предположил, что имеется в виду основание 8. Первый раз я встретился с использовани
ем маленькой e в экспоненциальной записи в конце 1970-х годов, и это было очень неудобно.
Проблемы появились потом, когда строчные буквы по инерции перешли в FORTRAN. У нас
существовали все нужные функции для действий с натуральными логарифмами, но все они
записывались прописными буквами».
Литералы 109
Поразрядные операторы
Поразрядные операторы манипулируют отдельными битами в целочисленных при
митивных типах данных. Результат определяется действиями булевой алгебры с со
ответствующими битами двух операндов.
Эти битовые операторы происходят от низкоуровневой направленности языка С, где
часто приходится напрямую работать с оборудованием и устанавливать биты в аппа
ратных регистрах. Java изначально разрабатывался для управления телевизионными
приставками, поэтому эта низкоуровневая ориентация все еще была нужна. Впрочем,
вам вряд ли придется часто использовать эти операторы.
Поразрядный оператор И (&) заносит 1 в выходной бит, если оба входных бита были
равны 1; в противном случае результат равен 0. Поразрядный оператор ИЛИ ( |) заносит
1 в выходной бит, если хотя бы один из битов операндов был равен 1; результат равен
0 только в том случае, если оба бита операндов были нулевыми. Оператор ИСКЛЮ
ЧАЮЩЕЕ ИЛИ (XOR, ^) имеет результатом единицу тогда, когда один или другой из
входных битов был единицей, но не оба вместе. Поразрядный оператор НЕ (~, также
называемый оператором двоичного дополнения) является унарным оператором, то есть
имеет только один операнд. Поразрядное НЕ производит бит, «противоположный»
исходному —если входящий бит является нулем, то в результирующем бите окажется
единица, если входящий бит —единица, получится ноль.
Поразрядные операторы и логические операторы записываются с помощью одних
и тех же символов, поэтому полезно запомнить мнемоническое правило: так как биты
«маленькие», в поразрядных операторах используется всего один символ.
Поразрядные операторы могут комбинироваться со знаком равенства =, чтобы совме
стить операцию с присваиванием: &=, | = и ^= являются допустимыми сочетаниями. (Так
как ~ является унарным оператором, он не может использоваться вместе со знаком =.)
110 Глава 3 • Операторы
Тип boolean трактуется как однобитовый, поэтому операции с ним выглядят по-другому.
Вы вправе выполнить поразрядные И, ИЛИ и ИСКЛЮ ЧАЮ Щ ЕЕ ИЛИ, но НЕ ис
пользовать запрещено (видимо, чтобы предотвратить путаницу с логическим НЕ). Для
типа boolean поразрядные операторы производят тот же эффект, что и логические, за
одним исключением — они не поддерживают ускоренного вычисления. Кроме того,
в число поразрядных операторов для boolean входит оператор ИСКЛЮ ЧАЮ Щ ЕЕ
ИЛИ, отсутствующий в списке логических операторов. Для булевых типов не раз
решается использование операторов сдвига, описанных в следующем разделе.
10. (3) Напишите программу с двумя константами: обе константы состоят из череду
ющихся нулей и единиц, но у одной нулю равен младший бит, а у другой старший
(подсказка: константы проще всего определить в шестнадцатеричном виде). Объ
едините эти две константы всеми возможными поразрядными операторами. Для
вывода результатов используйте метод Integer.toBinaryString().
Операторы сдвига
Операторы сдвига также манипулируют битами и используются только с примитив
ными целочисленными типами. Оператор сдвига влево (<<) сдвигает влево операнд,
находящийся слева от оператора, на количество битов, указанных после оператора.
Оператор сдвига вправо ( » ) сдвигает вправо операнд, находящийся слева от операто
ра, на количество битов, указанных после оператора. При сдвиге вправо используется
заполнение знаком: при положительном значении новые биты заполняются нулями,
а при отрицательном — единицами. BJavaTaroKe поддерживается беззнаковый сдвиг
вправо >>>, использующий заполнение нулями\ независимо от знака старшие биты за
полняются нулями. Такой оператор не имеет аналогов в С и С++.
Если сдвигаемое значение относится к типу char, byte или short, эти типы приво
дятся к in t перед выполнением сдвига, и результат также получится in t. При этом
используется только пять младших битов с «правой» стороны. Таким образом, нельзя
сдвинуть битов больше, чем вообще существует для целого числа int. Если вы про
водите операции с числами long, то получите результаты типа long. При этом будет
задействовано только шесть младших битов с «правой» стороны, что предотвращает
использование излишнего числа битов.
Сдвиги можно совмещать со знаком равенства (<<=, или >>=, или >>>=). Именующее
выражение заменяется им же, но с проведенными над ним операциями сдвига. Однако
прй этом возникает проблема с оператором беззнакового правого сдвига, совмещен
ного с присваиванием. При использовании его с типом byte или short вы не получите
правильных результатов. Вместо этого они сначала будут преобразованы к типу in t
и сдвинуты вправо, а затем обрезаны при возвращении к исходному типу, и результатом
станет -l. Следующий пример демонстрирует это:
//: operators/URShift.java
// Проверка беззнакового сдвига вправо.
import static net.mindview.util.Print.*;
print(Integer.toBinaryString(i));
i » > = 10 ;
print(Integer.toBinaryString(i));
long 1 = -1 ;
print(Long.toBinaryString(l));
1 >>>= 10;
print(Long.toBinaryString(1));
short s = -1 ;
print(Integer.toBinaryString(s));
s » > = 10 ;
print(Integer.toBinaryString(s));
byte b = -1 ;
print(Integer.toBinaryString(b));
b >>>= 10 ;
print(Integer.toBinaryString(b));
b = -1 ;
print(Integer.toBinaryString(b));
print(Integer.toBinaryString(b>>>10));
}
} /* Output:
11111111111111111111111111111111
1111111111111111111111
1111111111111111111111111111111111111111111111111111111111111111
111111111111111111111111111111111111111111111111111111
11111111111111111111111111111111
11111111111111111111111111111111
11111111111111111111111111111111
llllllllllllllllllllllllllllllll
11111111111111111111111111111111
1111111111111111111111
*///:~
long 1 = rand.nextLong();
long га = rand.nextLong();
printBinaryLong("-lL", -lL);
printBinaryLong("+lL", +lL);
long 11 = 9223372036854775807L;
printBinaryLong("MaKC. положит.", 11);
long lln = -9223372036854775808L;
printBinaryLong("MaKC. отрицат.", lln);
printBinaryLong("l", 1);
printBinaryLong("~l", ~1);
printBinaryLong("-l", -1);
printBinaryLong( "m''л m);
printBinaryLong( "1 & m", 1 & m);
printBinaryLong( "1 I m", 1 | m);
printBinaryLong( "1 ^ m", 1 Л m);
printBinaryLong( "1 « 5", 1 « 5);
printBinaryLong( "1 » 5", 1 » 5);
printBinaryLong( "(~1 ) » 5", (~1) » 5);
printBinaryLong( "1 > » 5" , 1 > » 5);
printBinaryLong("(~l) > » 5", (~1) >>> 5);
}
static void printBinaryInt(String s, int i) {
print(s + ", int: " + i + ", двоичное: \n
Integer.toBinaryString(i));
}
static void printBinaryLong(String s, long 1) {
print(s + ", long: " + 1 + ", двоичное:\п
Long.toBinaryString(l));
}
> /* Output:
-1 , int: -l, двоичное:
llllllllllllllllllllllllllllllll
+ 1 , int: 1 , двоичное:
1
макс, положит., int: 2147483647, двоичное:
1111111111111111111111111111111
макс, отрицат., int: -2147483648, двоичное:
10000000000000000000000000000000
i, int: -1172028779, двоичное:
10111010001001000100001010010101
~i, int: 1172028778, двоичное:
1000101110110111011110101101010
-i, int: 1172028779, двоичное:
1000101110110111011110101101011
j, int: 1717241110, двоичное:
1100110010110110000010100010110
i & j, int: 570425364, двоичное:
100010000000000000000000010100
i | j, int: -25213033, двоичное:
Литералы 113
11111110011111110100011110010111
i ^ j, int: -595638397, двоичное:
11011100011111110100011110000011
i « 5, int: 1149784736, двоичное:
1000100100010000101001010100000
i » 5, int: -36625900, двоичное:
11111101110100010010001000010100
(~i) » 5, int: 36625899, двоичное:
10001011101101110111101011
i >>> 5, int: 97591828, двоичное:
101110100010010001000010100
(~i) » > 5, int: 36625899, двоичное:
10001011101101110111101011
Тернарный оператор
Тернарный оператор необычен тем, что он использует три операнда. И все же это
действительно оператор, так как он производит значение, в отличие от обычной кон
струкции выбора if-e lse , описанной в следующем разделе. Выражение записывается
в такой форме:
логическое-условие ? выражение© : выражение1
Вы видите, что код ternary() более компактен по сравнению с записью без примене
ния тернарного оператора в standardIfElse(). С другой стороны, код standardIfElse()
более понятен, а объем не настолько уж увеличивается. Хорошенько подумайте, стоит
ли использовать тернарный оператор —обычно его применение ограничивается при
сваиванием переменной одного из двух значений.
Обратите внимание: первая команда print выводит ol 2 вместо 3 (как было бы при
простом суммировании целых чисел). Это объясняется тем, что компилятор Java
преобразует x, у и z в их строковые представления и выполняет конкатенацию этих
строк. Вторая команда print преобразует начальную переменную к типу String, так
что преобразование не зависит от того, что стоит на первом месте. Далее мы видим
пример использования оператора += для присоединения строки к s и использование
круглых скобок для управления порядком вычисления выражения, так что значения
int суммируются перед выводом.
Операторы приведения
Слово приведение используется в смысле «приведение к другому типу». В определенных
CHTyaHHnxJava самостоятельно преобразует данные к другим типам. Например, если
вещественной переменной присваивается целое значение, компилятор автоматически
выполняет соответствующее преобразование (int преобразуется во float). Приведение
позволяет сделать замену типа более очевидной или выполнить ее принудительно
в случаях, где это не происходит в обычном порядке.
Чтобы выполнить приведение явно, запишите необходимый тип данных (включая
все модификаторы) в круглых скобках слева от преобразуемого значения. Пример:
//: operators/Casting.java
новый тип данных способен хранить больше информации, чем прежний, и поэтому
потеря данных исключена.
В Java разрешается приводить любой простейший тип данных к любому другому
простейшему типу, но это не относится к типу boolean, который вообще не подлежит
приведению. Классы также не поддерживают произвольное приведение. Чтобы пре
образовать один класс в другой, требуются специальные методы. (Как будет показано
позднее, объекты можно преобразовывать в рамках семейства типов; объект Дуб можно
преобразовать вДереео, и наоборот, но не к постороннему типу вроде Камня.)
Округление и усечение
При выполнении сужающих преобразований необходимо обращать внимание на усечение
и округление данных. Например, как должен действовать кoмпилятopJava при преоб
разовании вещественного числа в целое? Скажем, если значение 29,7 приводится к типу
int, что получится —29 или 30? Ответ на этот вопрос может дать следующий пример:
//: operators/CastingNumbers.java
// Что происходит при приведении типов
// float или double к целочисленным значениям?
import static net.mindview.util.Print.*;
> /* Output:
Math.round(above): 1
M a t h .round(below): 0
Math.round(fabove): 1
Math.round(fbelow): 0
*///:~
Повы ш ение
П ри проведении лю бых математических и поразрядны х операций прим итивны е
типы данных, меньшие in t (то есть char, byte и short), приводятся к типу in t перед
Проведением операций, и получаемый результат имеет тип in t. Поэтому, если вам
снова понадобится присвоить его меньшему типу, придется использовать приведение
(с возможной потерей информации). В основном самый емкий тип данных, присут
ствующий в выражении, и определяет величину результата этого выражения; так, при
перемножении flo a t и double результатом станет double, а при сложении long и in t вы
получите в результате long.
Сводка операторов
Следующий пример показывает, какие примитивные типы данных использую тся
с теми или иными операторами. Вообще-то это один и тот же пример, повторенный
много раз, но для разных типов данных. Ф айл должен компилироваться без ошибок,
поскольку все строки, содержащие неверные операции, предварены символами / / ! .
//: operators/A110ps.java
// Проверяет все операторы со всеми
// примитивными типами данных, чтобы показать,
// какие операции допускаются компилятором Java.
X++J
x - - j
x = (char)+y;
x = (char)-y;
// Операции сравнения и логические операции:
f(x > у);
f(x >= у ) ;
f(x < у);
f(x <= y ) j
f(x == у ) ;
f(x != у ) ;
//! f(!x);
//1 f(x && у ) ;
/ / ! f ( x || у ) ;
// Поразрядные операции:
x= ( c h a r ) ~ y ;
x = ( c h a r ) (x & у ) ;
x = ( c h a r ) (x | у);
x = ( c h a r ) (x Л у);
x = (char)(x << 1 ) ;
x = (char)(x >> 1 ) ;
x = (char)(x >>> 1 ) ;
// Совмещенное п р и с в а и в а н и е :
x += у;
x -= у;
x *= у ;
x /= у;
x %= у ;
x <<= 1 ;
x >>= 1 ;
x >>>= 1 ;
x &= у;
x ^= у ;
x |= у;
// Приведение:
//! boolean b = (boolean)x;
byte В = (byte)x;
short s = (short)x;
in t i = (in t)x ;
long 1 = (long)x;
flo a t f = (flo a t)x ;
double d = (double)x;
>
void byteTest(byte x, byte у) {
// Арифметические операции:
x = ( b y t e ) (x* у) j
x = ( b y t e ) (x / y)j
x = (byte)(x % у);
x = ( b y t e ) (x + y)j
x = ( b y t e ) (x - у);
x++;
x J
x = (byte)+ у;
x = (byte)- у;
// Операции сравнения и логические операции
f(x > y)j
f(x >= у ) ;
f(x < у);
B Java отсутствует sizeof 121
x -= у;
x *= у;
x /= у;
x %= у;
x «= 1;
x >>= 1 ;
x >>>= 1 ;
x &= у;
x ^= у;
x 1= у;
// Приведение:
//! boolean b = (boolean)x;
char с = (char)x;
byte В = (byte)x;
short s = (short)x;
long 1 = (long)xj
float f = (float)x;
double d = (double)x;
>
void longTest(long x, long у) {
// Арифметические операции:
x = x * у;
x = x / у;
x = x % у;
x = x + у;
x = x - у;
x++j
X--J
X = +y;
x = -yj
// Операции сравнения и логические операции:
f(x > у);
f(x >= у);
f(x < у);
f(x <= у);
f(x == у);
f(x != у);
//! f(!x);
//! f(x && y)j
//! f(x M у);
// Поразрядные операции:
x = ~y;
x = x & yj
x = x | у;
x = x ^ у;
x = x << 1 ;
x = x >> 1 ;
x = x >>> 1 ;
// Совмещенное присваивание:
x += у;
x -= у;
x *= у;
x /= у;
x %= у;
x «= 1;
x >>= 1 ;
x >»= 1;
x &= у;
продолжение ё>
124 Глава 3 • Операторы
x ^= у;
x 1= у;
// Приведение:
//! boolean b = (boolean)x;
char с = (char)xj
byte В = (byte)xj
short s = (short)x;
int i = (int)x;
float f = (float)xj
double d = (double)x;
}
void floatTest(float x, float у) {
// Арифметические операции:
x = x * yj
x = x / yj
x = x % yj
x « x + yj
x = x - уj
x++;
x--;
x = +yj
x = -yj
// Операции сравнения и логические операции
f(x > у);
f(x >= у);
f(x < у);
f(x <= у);
f(x == у);
f(x != у);
//i f(!x);
//! f(x && у);
//! f(x M у);
// Поразрядные операции:
//! x = ~yj
//! x = x & у;
//! x = x | у;
//! x = x ^ у;
//! x = x << lj
//! x = x » 1 ;
//! x = x » > 1 ;
// Совмещенное присваивание:
x += у;
x -= у;
x *= у;
x /= у;
x %= у;
//! x « = 1 ;
//! x >>= lj
//! x >>>= lj
//! x &= yj
//! x ^= yj
//! x |= yj
// Приведение:
//! boolean b = (boolean)xj
char с = (char)xj
byte В = (byte)xj
short s = (short)xj
int i = (int)xj
B Java отсутствует sizeof 125
long 1 = (long)x;
double d = (double)x;
>
void doubleTest(double x, double у) {
// Арифметические операции:
x = x * у;
x = x / у;
x = x % у;
x = x + у;
x = x - у;
x+ + j
x--;
x = +y;
x = -у;
// Операции сравнения и логические операции:
f(x > у);
f(x >= у);
f(x < у);
f(x <= у);
f(x == у);
f(x != у);
//! f(!x);
//! f(x && у);
//! f(x || y)j
// Поразрядные операции:
//! x = ~y;
//! x = x & у;
//! x = x | у;
//! x = x Л у j
//! x = x << 1 ;
//! x = x >> 1 ;
//! x = x >>> lj
// Совмещенное присваивание:
x += у;
x -= у;
x *= у;
x /= у;
x % = у;
//! x « = lj
//! x >>= 1 ;
//! x >>>= 1 ;
//! x &= у;
//! x ^= у;
//! x |= у;
// Приведение:
//! boolean b = (boolean)xj
char с = (char)x;
byte В = (byte)xj
short s = (short)xj
int i = (int)x;
long 1 = (long)x;
float f = (float)xj
>
} f/ /:~
Заметьте, что действия с типом boolean довольно ограниченны. Ему можно присвоить
значение true или false, проверить на истинность или ложность, но нельзя добавлять
логические переменные к другим типам или производить с ними любые иные операции.
126 Глава 3 ♦ Операторы
В случае с типами char, byte и short можно заметить эффект повышения при ис
пользовании арифметических операторов. Любая арифметическая операция с этими
типами дает результат типа int, который затем нужно явно приводить к изначальному
тиПу (сужающее приведение, при котором возможна потеря информации). При ис
пользовании значений типа in t приведение осуществлять не придется, потому что все
значения уже имеют этот тип. Однако не заблуждайтесь относительно безопасности
происходящего. При перемножении двух достаточно больших целых чисел in t про
изойдет переполнение. Следующий пример демонстрирует сказанное:
//: operators/Overflow.java
// Сюрприз! В Java возможно переполнение.
Резюме
Читатели с опытом работы на любом языке семейства С могли убедиться, что опера-
TopbiJava почти ничем не отличаются от классических. Если же материал этой главы
показался трудным, обращайтесь к мультимедийной презентации «Thinking in С»
(www.MindView.net).
Управляющие
конструкции
true и false
Все конструкции с условием вычисляют истинность или ложность условного выраже
ния, чтобы определить способ выполнения. Пример условного выражения —А == В.
Оператор сравнения == проверяет, равно ли значение А значению в. Результат про
верки может быть истинным (true) или ложным (false). Любой из описанных в этой
главе операторов сравнения может применяться в условном выражении. Заметьте,
4T o J a v a не разрешает использовать числа в качестве логических значений, хотя это
позволено в С и С++ (где не-ноль считается «истинным», а ноль — «ложным»). Если
вам потребуется использовать числовой тип там, где требуется boolean (скажем, в ус
ловии if(a)), то сначала придется его преобразовать к логическому типу оператором
сравнения в условном выражении —например, if(a != 0 ).
if-else
Команда if-else является, наверное, наиболее распространенным способом передачи
управления в программе. Присутствие ключевого слова else не обязательно, поэтому
конструкция if существует в двух формах:
128 Глава 4 • Управляющие конструкции
!^логическое выражение)
команда
ИЛИ ¥
!^логическое выражение)
команда
else
команда
Условие должно дать результат типа boolean. В секции команда располагается либо
простая команда, завершенная точкой с запятой, или составная конструкция из команд,
заключенная в фигурные скобки.
В качестве примера применения if-e ls e представлен метод te s t(), который выдает
информацию об отношениях между двумя числами —«больше», «меньше» или «равно»:
//: control/IfElse.java
import static net.mindview.util.Print.*;
Внутри метода test() встречается конструкция else if; это не новое ключевое слово, а else,
за которым следует начало другой команды if.
Java, как С и С++, относится к языкам со свободным форматом. Тем не менее в коман
дах управления рекомендуется делать отступы, благодаря чему читателю программы
будет легче понять, где начинается и заканчивается управляющая конструкция.
Циклы
Конструкции while, do-while и for управляют циклами и иногда называются цикли
ческими командами. Команда повторяется до тех пор, пока управляющее логическое
выражение не станет ложным. Форма цикла while следующая:
Управляющие конструкции 129
иППе(логическое выражение)
команда
do-while
Форма конструкции do-while такова:
do
команда
while(norw4ecKoe выражение);
Единственное отличие цикла do-while от while состоит в том, что цикл do-while вы
полняется по крайней мере единожды, даже если условие изначально ложно. В цикле
while, если условие изначально ложно, тело цикла никогда не отрабатывает. На прак
тике конструкция do-while употребляется реже, чем while.
for
Пожалуй, конструкции for составляют наиболее распространенную разновидность
циклов. Цикл fo r проводит инициализацию перед первым шагом цикла. Затем вы
полняется проверка условия цикла, и в конце каждой итерации осуществляется некое
«приращение» (обычно изменение управляющей переменной). Цикл for записывается
следующим образом:
^г(инициализация; логическое выражение; шаг)
команда
130 Глава4 • Управляющиеконструкции
Любое из трех выражений цикла (инициализация, логическое выражение или шаг) можно
пропустить. Перед выполнением каждого шага цикла проверяется условие цикла;
если оно окажется ложным, выполнение продолжается с инструкции, следующей за
конструкцией for. В конце каждой итерации выполняется секция шаг.
Цикл for обычно используется для «счетных» задач:
//: control/ListCharacters.java
// Пример использования цикла "for": перебор
// всех ASCII-символов нижнего регистра
*///:~
Оператор-запятая
Ранее в этой главе уже упоминалось о том, что оператор «запятая» (но не запятая-
разделитель, которая разграничивает определения и аргументы функций) может ис
пользоваться Bjava только в управляющем выражении цикла for. И в секции иници
ализации цикла, и в его управляющем выражении можно записать несколько команд,
разделенных запятыми; они будут обработаны последовательно.
Оператор «запятая» позволяет определить несколько переменных в цикле for, но все
эти переменные должны принадлежать к одному типу:
//: control/CommaOperator.java
Синтаксис foreach
B Java SE5 появилась новая, более компактная форма !Ъгдля перебора элементов
массивов и контейнеров (см. главы 16 и 17). Этаупрощ енная форма; называемая
синтаксисом foreach, не требует ручного изменения переменной int для перебора
последовательности объектов — цикл автоматически представляет очередной
элемент.
Следующая программа создает массив float, после чего перебирает все его элементы:
132 Глава4 • Управляющиеконструкции
//: control/ForEachFloat.java
import java.util.*;
Массив заполняется уже знакомым циклом for, потому что для его заполнения должны
использоваться индексы. Упрощенный синтаксис используется в следующей команде:
for(float x : f)
Эта конструкция определяет переменную x типа flo at, после чего последовательно
присваивает ей элементы f.
Любой метод, возвращающий массив, может использоваться с fo r e a c h 1. Например,
класс String содержит метод toCharArray(), возвращающий массив char; следовательно,
перебор символов строки может осуществляться так:
//: control/ForEachString.java
Как будет показано далее, «синтаксис foreach» также работает для любого объекта,
поддерживающего интерфейс lterable.
Многие команды for основаны на переборе серии целочисленных значений:
for (int i = 0; i < 100; i++)
1Далее этот термин будет использоваться для обозначения данной разновидности циклов, хотя
такого ключевого слова Bjava нет. —Пргшеч. ред.
return 133
Метод range() перегружен, то есть одно имя метода может использоваться с разными
списками аргументов (вскоре перегрузка будет рассмотрена более подробно). Первая
перегруженная форма range() просто начинает с нуля и генерирует значения до верхней
границы диапазона (не включая ее). Вторая форма начинает с первого значения и про
ходит до значения, на единицу меньшую второго. Наконец, третья форма использует
величину приращения. Как вы вскоре увидите, range() является очень простой формой
генераторов, которые будут рассматриваться позже.
Обратите внимание на использование printb() вместо print(). Метод printb() не вы
водит символ новой строки, что позволяет выводить строку по частям.
Синтаксис foreach не только экономит объем вводимого кода. Что еще важнее, он
значительно упрощает чтение программы и указывает, что вы пытаетесь сделать
(перебрать все элементы массива), вместо подробного описания того, как вы собира
етесь это сделать («Я создаю индекс для перебора всех элементов массива»). Я буду
использовать синтаксис foreach везде, где это возможно.
return
Следующая группа ключевых слов обеспечивает безусловные переходы, то есть пере
дачу управления без проверки каких-либо условий. К их числу относятся команды
return, break и continue, а также конструкция перехода по метке, аналогичная goto
в других языках.
У ключевого слова return имеются два предназначения: оно указывает, какое значение
возвращается методом (если только он не возвращает тип void), а также используется
134 Глава 4 • Управляющие конструкции
break и continue
В теле любого из циклов можно управлять потоком программы при помощи специ
альных ключевых слов break и continue. Команда break завершает цикл, при этом
оставшиеся операторы цикла не выполняются. Команда c o n t i n u e останавливает
выполнение текущей итерации цикла и переходит к началу цикла, чтобы начать вы
полнение нового шага.
Следующая программа показывает пример использования команд break и continue
внутри циклов for и while:
//: control/BreakAndContinue.java
// Применение ключевых слов break и continue
import static net.mindyiew.util.Range.*;
break и continue 135
В цикле for переменная i никогда не достигает значения 100 —команда break прерывает
цикл, когда значение переменной становится равным74. Обычно break используется
только тогда, когда вы точно знаете, что условие выхода из цикла действительно до
стигнуто. Команда continue переводит исполнение в начало цикла (и таким образом
увеличивает значение i), когда i не делится без остатка на 9. Если деление произво
дится без остатка, значение выводится на экран.
Второй цикл for демонстрирует использование синтаКсиса foreach с тем же резуль
татом.
Последняя часть программы демонстрирует «бесконечный цикл», который, в теории,
должен исполняться вечно. Однако в теле цикла вызывается команда break, которая
и завершает цикл. Команда continue переводит исполнение к началу цикла, и при этом
остаток цикла не выполняется. (Таким образом, вывод на экран в последнем цикле
происходит только в том случае, если значение i делится на 10 без остатка.) Значение О
выводится, так как 0 % 9 дает в результате 0.
Вторая форма бесконечного цикла — f o r ( ;;) . Компилятор реализует конструкции
while(true) и f o r ( ;;) одинаково, так что выбор является делом вкуса.
21. (1) Измените упражнение 1 так, чтобы выход из программы осуществлялся клю
чевым словом break при значении 99. Попробуйте использовать ключевое слово
return.
136 Глава 4 • Управляющие конструкции
1 Оригинал статьи Go То Statement considered harmful имеет постоянный адрес в Интернете: http://
www.acm.org/classics/oct95. — Примеч.ред.
Нехорошая команда goto 137
//...
break labell; // 4
>
>
В первом случае (1) команда break прерывает выполнение внутреннего цикла, и управ
ление переходит к внешнему циклу Во втором случае (2) оператор continue передает
управление к началу внутреннего цикла. Но в третьем варианте (3) команда continue
labell влечет выход из внутреннего и внешнего циклов и возврат к метке labell. Да
лее выполнение цикла фактически продолжается, но с внешнего цикла. В четвертом
случае (4) команда break labell также вызывает переход к метке labell, но на этот раз
повторный вход в итерацию не происходит. Это действие останавливает выполнение
обоих циклов.
Пример использования цикла for с метками:
//: control/LabeledFor.java
// Цикл for с метками
import static net.mindview.util.Print.*;
_ _
138 Глава 4 • Управляющие конструкции
Заметьте, что оператор break завершает цикл for, вследствие этого выражение с ин
крементом не выполняется до завершения очередного шага. Поэтому из-за пропуска
операции инкремента в цикле переменная непосредственно увеличивается на единицу,
когда i == 3. При выполнении условия i == 7 команда continue outer переводит вы
полнение на начало цикла; инкремент опять пропускается, поэтому и в этом случае
переменная увеличивается явно.
Без команды break outer программе не удалось бы покинуть внешний цикл из внутрен
него цикла, так как команда break сама по себе завершает выполнение только текущего
цикла (это справедливо и для continue).
Конечно, если завершение цикла также приводит к завершению работы метода, можно
просто применить команду return.
Теперь рассмотрим пример, в котором используются команды break и continue с мет
ками в цикле while:
//: control/LabeledWhile.java
// Цикл while с метками
import static net.mindview.util.Print.*;
>
if(i == 3) {
print("continue outer");
continue outer;
>
if(i == 5) {
print("break");
break;
>
if(i == 7) {
print("break outer");
break outer;
}
}
>
>
> /* Output:
Внешний цикл while
i = 1
continue
i = 2
i = 3
continue outer
Внешний цикл while
i = 4
i = 5
break
Внешний цикл while
i = 6
i = 7
break outer
*///:~
switch
Команду switch часто называют командой выбора. С помощью конструкции switch
осуществляется выбор из нескольких альтернатив, в зависимости от значения цело
численного выражения. Форма команды выглядит так:
switch(цeлoчиcлeннoe-выpaжeниe) {
case целое-значение1 : команда; break;
case целое-значение2 : команда; break;
case целое-значениеЗ : команда; break;
case целое-значение4 : команда; break;
case целое-значение5 : команда; break;
// ...
default: оператор;
fo r( in t i = 0; i < 100; i+ + ) {
in t с = ra n d .n e x tIn t(2 6 ) + 'a';
p rin tn b ((c h a r)c + ", " + с + ": ");
s w itc h ( c ) {
case 'a':
case 'e':
case 'i':
case 'o-:
case 'u': print("rnacHaa");
break;
case 'y':
case 'w': print("ycnoBHO гласная");
break;
default: print("cornacHaa");
>
}
}
} /* O u t p u t :
У> 121: Условно гласная
n, 110: согласная
z, 122: согласная
Ь, 98: согласная
r, 114: согласная
n, 110: согласная
y> 121: Условно гласная
z> 103: согласная
с, 99: согласная
f, 102: согласная
о, lll: гласная
W, 119: Условно гласная
z, 122: согласная
метод rand. nextlnt() выдает случайное число in t от 0 до 25, к которому затем прибав
ляется значение ' а ' . Это означает, что символ а автоматически преобразуется к типу
in t для выполнения сложения.
Чтобы вывести с в символьном виде, его необходимо преобразовать к типу char; в про
тивном случае значение будет выведено в числовом виде.
22. (2) Создайте команду switch, которая выводит сообщение в каждой секции case.
Разместите ее в цикле for, проверяющем все допустимые значения case. Каждая
142 Глава 4 * Управляющие конструкции
секция case должна завершаться командой break. Затем удалите команды break
и посмотрите, что произойдет.
23. (4) Числами Фибоначчи называется числовая последовательность 1 ,1 ,2 ,3 ,5 ,8 ,1 3 ,
21, 34 и т. д., в которой каждое число, начиная с третьего, является суммой двух
предыдущих. Напишите метод, который получает целочисленный аргумент и выво
дит указанное количество чисел Фибоначчи. Например, при запуске командой java
Fibonacci 5 (где Fibonacci — имя класса) должна выводиться последовательность
1, 1, 2, 3, 5.
24. Вампирами называются числа, состоящие из четного количества цифр и полученные
перемножением пары чисел, каждое из которых содержит половину цифр резуль
тата. Цифры берутся из исходного числа в произвольном порядке, завершающие
нули недопустимы. Примеры:
1) 1261 =21 *60;
2) 1827 = 21 *87;
3) 2187 = 27*81.
Резюме
В этой главе завершается описание основных конструкций, присутствующих почти
во всех языках программирования: вычислений, приоритета операторов, приведения
типов, условных конструкций и циклов. Теперь можно сделать следующий шаг на пути
к миру объектно-ориентированного программирования. Следующая глава ответит на
важные вопросы об инициализации объектов и завершении их жизненного цикла, по
сле чего мы перейдем к важнейшей концепции сокрытия реализации.
Инициализация
5 и завершение
class Rock {
Rock() { // Это и есть конструктор
System.out.print("Rock ");
}
>
public class SimpleConstructor {
public static void main(String[] args) {
for(int i = 0; i < 10; i++)
new Rock();
»;
>
> /* Output:
Rock Rock Rock Rock Rock Rock Rock Rock Rock Rock
*///:~
class Rock2 {
Rock2(int i) {
System.out.println("Rock " + i + " ");
>
>
public class SimpleConstructor2 {
Перегрузка методов 145
Перегрузка методов
Одним из важнейших аспектов любого языка программирования является исполь
зование имен. Создавая объект, вы фактически присваиваете имя области памяти.
Метод —имя для действия. Использование имен при описании системы упрощает ее
понимание и модификацию. Работа программиста сродни работе писателя; в обоих
случаях задача состоит в том, чтобы донести свою мысль до читателя.
Проблемы возникают при перенесении нюансов человеческого языка в языки про
граммирования. Часто одно и то же слово имеет несколько разных значений — оно
перегружено. Это полезно, особенно в отношении простых различий. Вы говорите
«вымыть посуду», «вымыть машину» и «вымыть собаку». Было бы глупо вместо этого
говорить «посудоМыть посуду», «машиноМыть машину» и «собакоМыть собаку»
только для того, чтобы слушатель не утруждал себя выявлением разницы между
146 Глава 5 * Инициализация и завершение
class Tree {
int height;
Tree() {
print("CaxaeM росток");
height = 0;
>
Tree(int initialHeight) {
height = initialHeight;
print("Co3flaHne нового дерева высотой " +
height + " м.");
}
void info() {
print("Aepeeo высотой " + height + " м.");
}..
void info(String s) {
print(s + ": Дерево высотой " + height + " м.");
>
>
Объект Tree (дерево) может быть создан или в форме ростка (без аргументов), или
в виде «взрослого растения» с некоторой высотой. Для этого в классе определяются
два конструктора; один используется по умолчанию, а другой получает аргумент
с высотой дерева.
Возможно, вы захотите вызывать метод info() несколькими способами. Например,
вызов с аргументом-строкой info(String) используется при необходимости вывода
дополнительной информации, а вызов без аргументов i n f o ( ) — когда дополнений
к сообщению метода не требуется. Было бы странно давать два разных имени методам,
когда их схожесть столь очевидна. К счастью, перегрузка методов позволяет исполь
зовать одно и то же имя для обоих методов.
//: initialization/OverloadingOrder.java
// Перегрузка, основанная на порядке
// следования аргументов,
import static net.mindview.util.Print.*;
Перегрузка с примитивами
Простейший тип может быть автоматически приведен от меньшего типа к большему,
и это в состоянии привнести немалую путаницу в перегрузку. Следующий пример по
казывает, что происходит при передаче примитивного типа перегруженному методу:
//: initialization/PrimitiveOverloading.java
// Повышение примитивных типов и перегрузка,
import static net.mindview.util.Print.*;
void testConstVal() {
printnb("5: ");
fl(5);f2(5);f3(5);f4(5);f5(5);f6(5);f7(5);print();
>
void testChar() {
char x = 'x';
printnb("char: ")j
fl(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x);print();
>
void testByte() {
byte x = 0;
System.out.println("napaMeTp типа byte:");
fl(x);f2(x);f3(x);f4(x);f5<x)jf6(x);f7(x);
}
void testShort() {
short x = 0j
printnb("short: ");
fl(x);f2(x);f3(x)jf4(x);f5(x)jf6(x)jf7(x);print();
>
void testInt() {
int x = 0j
printnb("int: ")j
fl(x);f2(x);f3(x);f4(x)jf5(x);f6(x);f7(x)jprint();
>
void testLong() {
long x = 0;
printnb("long:");
fl(x);f2(x);f3(x);f4(x);f5(x)jf6(x);f7(x);print();
>
void testFloat() {
float x = 0;
System.out.println("float:");
fl(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x);print()j
>
void testDouble() {
double x = 0;
printnb("double:")j
fl(x)jf2(x);f3(x);f4(x);f5(x)jf6(x);f7(x);print();
}
public static void main(String[] args) {
PrimitiveOverloading p =
new PrimitiveOverloading();
p .testConstVal();
p.testChar();
p.testByte();
продолжение ^>
150 Глава 5 • Инициализация и завершение
p.testShort()j
p.testInt();
p.testLong();
p.testFloat();
p.testDouble();
}
} /* 0utput:
5: fl(int) f2(int) f3(int) f4(int) f5(long) f6(float) f7(double)
char: fl(char) f2(int) f3(int) f4(int) f5(long) f6(float) f7(double)
byte: fl(byte) f2(byte) f3(short) f4(int) f5(long) f6<float) f7(double)
short: fl(short) f2(short) f3(short) f4(int) f5(long) f6(float) f7(double)
int: fl(int) f2(int) f3(int) f4(int) f5(long) f6(float) f7(double)
long: fl(long) f2(long) f3(long) f4(long) f5(long) f6(float) f7(double)
float: fl(float) f2(float) f3(float) f4(float) f5(float) f6(float) f7(double)
double: fl(double) f2(double) f3(double) f4(double) f5(double) f6(double) f7(double)
*///:~
void testDouble() {
double x = 0;
print("napaMeTp типа double:");
fl(x);f2((float)x);f3((long)x);f4((int)x);
f5((short)x);f6((byte)x);f7((char)x);
>
public static void main(String[] angs) {
Demotion p = new Demotion();
p.testDouble();
}
) /* Output:
параметр типа double:,
fl(double),
f2(float),
f3(lbng),
f4(int),
f5(short),
f6(byte),
f7(char)
*///:~
Здесь методы требуют сужения типов данных. Если ваш аргумент «шире», необхо
димо явно привести его к нужному типу. В противном случае компилятор выведет
сообщение об ошибке.
Такой подход прекрасно сработает в ситуации, где компилятор может однозначно вы
брать нужную версию метода по контексту, например in t x = f(). Однако возвращае
мое значение при вызове метода может быть проигнорировано; это часто называется
вызовом метода для получения побочного эффекта, так как метод вызывается не для
естественного результата, а для каких-то других целей. Допустим, метод вызывается
следующим способом:
f();
Конструкторы по умолчанию
Как упоминалось ранее, конструктором по умолчанию называется конструктор без
аргументов, применяемый для создания «типового объекта». Если созданный вами
класс не имеет конструктора, компилятор автоматически добавит конструктор по
умолчанию. Например:
//: initialization/DefaultConstructor.java
Строка
new Bird();
class Bird2 {
Bird2(int i) {>
Bird2(double d) {>
>
public class NoSynthesis {
public static void main(String[] args) {
//! Bird2 b = new Bird2()j // Нет конструктора по умолчанию!
Bird2 b2 = new Bird2(l);
Bird2 b3 = new Bird2(1.0);
}
> / / / :~
Теперь при попытке выполнения new Bird2() компилятор заявит, что не можетнайти
конструктор, подходящий по описанию. Получается так: если определения конструк
торов отсутствуют, компилятор скажет: «Хотя бы один конструктор необходим, по
звольте создать его за вас». Если же вы записываете конструктор явно, компилятор
говорит: «Вы написали конструктор, а следовательно, знаете, что вам нужно; и если
вы создали конструктор по умолчанию, значит, он вам и не нужен».
3. (1) Создайте класс с конструктором по умолчанию (без параметров), который вы
водит на экран сообщение. Создайте объект этого класса.
4. Добавьте к классу из упражнения 3 перегруженный конструктор, принимающий
в качестве параметра строку (String) и распечатывающий ее вместе с сообщением.
Перегрузка методов 153
class Person {
public void eat(Apple apple) {
Apple peeled = apple.getPeeled();
System.out.println("Yummy");
>
>
1 Некоторые демонстративно пишут th is перед каждым методом и полем класса, объясняя это
тем, что «так яснее и доходчивее». Не делайте этого. Мы используем языки высокого уровня
по одной причине: они выполняют работу за нас. Если вы станете писать th is там, где это
не обязательно, то запутаете и разозлите любого человека, читающего ваш код, поскольку
в большинстве программ ссылки th is в таком контексте не используются. Последовательный
и понятный стиль программирования экономит и время, и деньги.
Перегрузка методов 155
class Peeler {
static Apple peel(Apple apple) {
// ... Снимаем кожуру
return apple; // Очищенное яблоко
>
>
class Apple {
Apple getPeeled() { return Peeler.peel(this); }
>
public class PassingThis {
public static void main(String[] args) {
new Person().eat(new Apple());
>
} I* Output:
Yummy
*///:~
s = ss;
>
Flower(String Sj int petals) {
this(petals);
//! this(s); // Вызов другого конструктора запрещен!
this.s = s; // Другое использование "this"
print("ApryMeHTbi String и int")j
>
Flower() {
this("hi% 47);
print("KOHCTpyKTop по умолчанию (без аргументов)");
>
void printPetalCount() {
//! this(ll); // Разрешается только в конструкторах!
print("petalCount = " + petalCount + " s = "+ s);
>
p u b lic s ta tic v o id m a in (5 trin g [] args) {
Flower x = new Flower();
x .printPetalCount();
}
} /* Output:
Конструктор с параметром intj petalCount= 47
Аргументы String и int
Конструктор по умолчанию (без аргументов)
petalCount = 47 s = hi
*///:~
Конструктор Flower(String s, int petals) показывает, что при вызове одного конструктора
через this вызывать второй запрещается. Вдобавок вызов другого конструктора должен
быть первой выполняемой операцией, иначе компилятор выдаст сообщение об ошибке.
Пример демонстрирует еще один способ использования this. Так как имена аргумен
та s и поля данных класса s совпадают, возникает неоднозначность. Разрешить это
затруднение можно при помощи конструкции t h i s . s, однозначно определяющей поле
данных класса. Вы еще не раз встретите такой подход в различныхДауа-программах,
да и в этой книге он практикуется довольно часто.
Метод printPetalCount() показывает, что компилятор не разрешает вызывать конструк
тор из обычного метода; это разрешено только в конструкторах.
9. (1) Подготовьте класс с двумя (перегруженными) конструкторами. Используя
ключевое слово this, вызовите второй конструктор из первого.
1 Впрочем, это можно сделать, передав ссылку на объект в статический метод. Тогда по пере
данной ссылке (которая 3aM eH H eT th is) вы сможетевызывать обычные, нестатические, методы
и получать доступ к обычным полям. Но д ля получения такого эффекта проще создать обычный,
нестатический, метод.
Очистка: финализация и уборка мусора 157
ВНИМАНИЕ --------------------------------------------------------------------------------------
1. Ваши объекты могут быть и не переданы уборщику мусора.
2. Уборка мусора не является уничтожением.
Запомните эту формулу, и многих проблем удастся избежать. Она означает, что если
перед тем, как объект станет ненужным, необходимо выполнить некоторое заверша
ющее действие, то это действие вам придется выполнить собственноручно. BJava нет
понятия деструктора или сходного с ним, поэтому придется написать обычный метод
158 Глава 5 • Инициализация и завершение
ВНИМАНИЕ ----------------------------------------------------------------------------------------
3. Процесс уборки мусора относится только к памяти.
После прочтения этого абзаца у вас, скорее всего, сложилось мнение, что метод
fin a liz e () используется нечасто1. И правда, это не то место, где следует прово
дить рутинные операции очистки. Но где же тогда эти обычные операции будут
уместны?
Условие «готовности»
В общем, вы не должны полагаться на вызов метода finalize() —создавайте отдельные
«функции очистки» и вызывайте их явно. Скорее всего, fin alize() пригодится только
в особых ситуациях нестандартного освобождения памяти, с которыми большинство
программистов никогда не сталкивается. Тем не менее существует очень интересное
1Джошуа Блош в своей книге (в разделе «избегайте финализаторов») высказывается еще реши
тельнее: «Финализаторы непредсказуемы, зачастую опасны и чаще всего не нужны». Effective
Java, с. 20 (издательство Addison-Wesley, 2011).
160 Глава 5 • Инициализация и завершение
применение метода f in a liz e (), не зависящее от того, вызывается он каждый раз или
нет. Это проверка условия готовностих объекта.
В той точке, где объект становится ненужным, —там, где он готов к проведению очист
ки, —этот объект должен находиться в состоянии, когда освобождение закрепленной
за ним памяти безопасно. Например, если объект представляет открытый файл, то он
должен быть соответствующим образом закрыт, перед тем как его «приберет» уборщик
мусора. Если какая-то часть объекта не будет готова к уничтожению, результатом станет
ошибка в программе, которую затем очень сложно обнаружить. Ценность fin a liz e ()
в том и состоит, что он позволяет вам обнаружить такие ошибки, даже если и не всегда
вызывается. Единожды проведенная финализация явным образом укажет на ошибку,
а это все, что вам нужно.
Простой пример использования данного подхода:
//: initialization/TerminationCondition.java
// Использование finalize() для выявления объекта,
// не осуществившего необходимой финализации.
class Book {
boolean checkedOut = false;
Book(boolean checkOut) {
checkedOut = checkOut;
>
void checkIn() {
checkedOut = false;
>
public void finalize() {
if(checkedOut)
System.out.println("Ошибка: checkedOut");
// О б ы ч н о э т о делается так:
// Super.finalize(); // Вызов версии базового класса
}
>
public class TerminationCondition {
public static void main(String[] args) {
Book novel = new Book(true);
// Правильная очистка:
novel.checkIn();
// Теряем ссылку, забыли про очистку:
new Book(true);
// Принудительная уборка мусора и финализация:
System.gc();
}
> /* Output:
Ошибка: checkedOut
* ///:~
«Условие готовности» состоит в том, что все объекты Book должны быть «сняты с уче
та» перед предоставлением их в распоряжение уборщика мусора, но в методе main()
программист ошибся и не отметил один из объектов Book. Если бы в методе finalize() 1
Явная инициализация
Что делать, если вам понадобится придать переменной начальное значение? Проще все
сделать это прямым присваиванием этой переменной значения в точке ее объявления
166 Глава 5 • Инициализация и завершение
в классе. (Заметьте, что в С++ такое действие запрещено, хотя его постоянно пытаются
выполнить новички.) В следующем примере полям уже знакомого класса ln itia lV a lu e s
присвоены начальные значения:
//: in itia liz a t io n / I n itia lV a lu e s 2 .java
// Явное определение начальных значений переменных
Инициализация конструктором
Для проведения инициализации можно использовать конструктор. Это придает боль
шую гибкость процессу программирования, так как появляется возможность вызова
методов и выполнения действия по инициализации прямо во время работы программы.
Впрочем, при этом необходимо учитывать еще одно обстоятельство: оно не исключает
автоматической инициализации, происходящей перед выполнением конструктора.
Например, в следующем фрагменте
//: in it ia liz a t io n / C o u n t e r .ja v a
p u b lic c la s s C o u n te r {
in t i;
C o u n te r() { i = 7; >
// . . .
} f//:~
переменной i сначала будет присвоено значение 0, а затем уже 7. Это верно для всех
примитивных типов и ссылок на объекты, включая те, которым задаются явные зна
чения в точке определения. По этим причинам компилятор не пытается заставить вас
инициализировать элементы в конструкторе, или в ином определенном месте, или
перед их использованием —инициализация и так гарантирована.
Порядок инициализации
Внутри класса очередность инициализации определяется порядком следования пере
менных, объявленных в этом классе. Определения переменных могут быть разбросаны
по разным определениям методов, но в любом случае переменные инициализируются
перед вызовом любого метода —даже конструктора. Например:
//: i n i t i a l i z a t i o n / O r d e r O f I n i t i a l i z a t i o n . ja v a
// Д ем о н стр и р ует порядок инициализации,
im p o r t s t a t ic n e t .m in d v ie w .u t il.P r in t .* ;
^
// При вы зо ве к о н с т р у к т о р а д л я с о з д а н и я о б ъ е к т а
// W indow в ы в о д и тся со о бщ ен и е:
c la s s Window {
W in d o w ( in t m a r k e r ) { p r in t ( " W in d o w ( " + m ark e r + " ) ” ) ; >
}
продолжение ■&
168 Глава 5 • Инициализация и завершение
class House {
Window wl = new Window(l); // Перед конструктором
House() {
// Показывает, что выполняется конструктор:
print("HouseQ");
w3 = new Window(33); // Повторная инициализация w3
}
Window w2 = new Window(2)j // После конструктора
void f() { print("f()"); }
Window w3 = new Window(3); // В конце
>
public class OrderOfInitialization {
public static void main(String[] args) {
House h = new House();
h.f(); // Показывает, что объект сконструирован
}
> /* Output:
Window(l)
Window(2)
Window(3)
House()
Window(33)
Ю
*///:~
class Bowl {
Инициализация конструктором 169
Bowl(int marker) {
print("Bowl(" + marker + ")");
>
void fl(int marker) {
print("fl(" + marker + ")");
>
>
class Table {
static Bowl bowll = new Bowl(l);
Table() {
print("Table()");
bowl2.fl(l);
>
void f2(int marker) {
print(''f2(" + marker + ")");
>
static Bowl bowl2 = new Bowl(2);
}
class Cupboard {
Bowl bowl3 = new Bowl(3);
static Bowl bowl4 = new Bowl(4);
Cupboard() {
print("Cupboard()"); bowl4.fl(2);
>
void f3(int marker) {
print("f3(" + marker + ")”);
}
static Bowl bowl5 = new Bowl(5);
}
public class StaticInitialization {
public static void main(String[] args) {
print("Co3flaHMe нового объекта Cupboard в main()")j
new Cupboard();
print("Co3flaHne нового объекта Cupboard в main()")j
new Cupboard()j
table.f2(l);
cupboard.f3(l);
>
static Table table = new Table();
static Cupboard cupboard = new Cupboard()j
} /* Output:
Bowl(l)
Bowl(2)
Table()
fl(l)
Bowl(4)
Bowl(5)
Bowl(3)
Cupboard()
fl(2)
Создание нового объекта Cupboard в main()
Bowl(3)
Cupboard()
fl(2)
Создание нового объекта Cupboard в main()
продолжение &
170 Глава 5 • Инициализация и завершение
Bowl(3)
Cupboard()
fl(2)
f2(l)
f3(l)
*///:~
Класс Bowl позволяет проследить за процессом создания классов, классы Table и Cupboard
содержат определения статических объектов Bowl. Заметьте, что в классе Cupboard не
статическая переменная Bowl bowl3 создается до статических определений.
Из выходных данных программы видно, что статическая инициализация происходит
только в случае необходимости. Если вы не создаете объектов Table и никогда не об
ращаетесь к Ta b l e .bowll или T a b l e .bowl2, то соответственно не будет и объектов static
Bowl bowll и static Bowl bowl2. Они инициализируются только при создании первого
объекта Table (или при первом обращении к статическим данным). После этого ста
тические объекты заново не инициализируются.
Сначала инициализируются статические члены, если они еще не были проинициа-
лизированы, и только затем нестатические объекты. Доказательство справедливости
этого утверждения легко найти в результате работы программы. Для выполнения
main() (а это статический метод!) загружается класс staticlnitialization; затем ини
циализируются статические поля table и cupboard, вследствие чего загружаются эти
классы. И так как все они содержат статические объекты Bowl, загружается класс Bowl.
Таким образом, все классы программы загружаются до начала main(). Впрочем, эта
ситуация нетипична, поскольку в рядовой программе не все поля объявляются как
статические, как в данном примере.
Неплохо теперь обобщить знания о процессе создания объекта. Для примера возьмем
класс с именем Dog:
□ Хотя ключевое слово static и не используется явно, конструктор в действитель
ности является статическим методом. При создании первого объекта типа Dog или
при первом вызове статического метода/обращения к статическому полю класса Dog
HHTepnpeTaTopJava должен найти класс Dog.class. Поиск осуществляется в стан
дартных каталогах, перечисленных в переменной окружения CLASSPATH.
□ После загрузки файла Dog.class (с созданием особого объекта Class, о котором мы
узнаем попозже) производится инициализация статических элементов. Таким об
разом, инициализация статических членов проводится только один раз, при первой
загрузке объекта Class.
□ При создании нового объекта конструкцией new Dog() для начала выделяется блок
памяти, достаточный для хранения объекта Dog в куче.
□ Выделенная память заполняется нулями, при этом все примитивные поля объекта
Dog автоматически инициализируются значениями по умолчанию (ноль для чисел,
его эквиваленты для типов boolean и char, null для ссылок).
□ Выполняются все действия по инициализации, происходящие в точке определения
полей класса.
□ Выполняются конструкторы. Как вы узнаете из главы 7, на этом этапе выполняется
довольно большая часть работы, особенно при использовании наследования.
Инициализация конструктором 171
class Cup {
Cup(int marker) {
print("Cup(" + marker + ")");
>
void f(int marker) {
print("f(" + marker + ")");
>
}
class Cups {
static Cup cupl;
static Cup cup2;
static {
cupl = new Cup(l);
cup2 = new Cup(2);
>
Cups() {
print("Cups()");
}
}
public class ExplicitStatic {
public static void main(String[] args) {
print("Inside main()");
Cups.cupl.f(99); // (1)
>
// static Cups cupsl = new Cups(); // (2)
// static Cups cups2 = new Cups(); // (2)
> /* Output:
Inside main()
Cup(l)
Cup(2)
f(99)
*///:~
172 Глава 5 • Инициализация и завершение
class Mug {
Mug(int marker) {
print(''Mug(" + marker + ")");
>
void f(int marker) {
print("f(" + marker + ")");
>
Mug(2)
mugl & mug2 инициализированы
Mugs()
new Mugs() завершено
Mug(l)
Mug(2)
mugl & mug2 инициализированы
Mugs(int)
new Mugs(l) завершено
*///:~
Инициализация массивов
Массив представляет собой последовательность объектов или примитивов, относящих
ся к одному типу и обозначаемую одним идентификатором. Массивы определяются
и используются с помощью оператора индексирования [ ]. Чтобы определить ссылку на
массив в программе, вы просто указываете вслед за типом пустые квадратные скобки:
int[] al;
мы имеем лишь массив из ссылок — до тех пор, пока каждая ссылка не будет ини
циализирована новым объектом Integer (в данном случае это делается посредством
автоупаковки):
a[i] = rand.nextInt(500);
class А { int i; }
Видно, что метод print() принимает массив объектов типа Object, перебирает его
элементы и выводит их. Классы из стандартной библиотеки Java при печати выво
дят осмысленную информацию, однако объекты классов в данном примере выводят
имя класса, затем символ @и несколько шестнадцатеричных цифр. Таким образом, по
умолчанию класс выводит имя и адрес объекта (если только вы не переопределите
в классе метод toString() —см. далее).
До BbixoAaJava SE5 переменные списки аргументов реализовывались именно так. BJava
SE5 эта долгожданная возможность наконец-то была добавлена в язык —теперь для
определения переменного списка аргументов может использоваться многоточие, как
видно в определении метода printArray:
//: initialization/NewVarArgs.java
// Создание списков аргументов переменной длины
// с использованием синтаксиса массивов.
} /* 0utput:
class [Ljava.lang.Character; длина 1
class [Ljava.lang.Character; длина 0
class [I длина 1
class [I длина 0
int[]: class [I
*///:~
Метод getClass() является частью Object; он будет подробно рассмотрен в главе 14.
Этот метод возвращает описание класса объекта, при выводе которого отображается
закодированная строка, представляющая тип класса. Начальный символ «[» означает,
что это массив с элементами типа, указанного далее. Обозначение «I» соответствует
примитивному типу int; для дополнительной проверки я создал в последней строке
массив int и вывел его тип. Проверка подтверждает, что списки аргументов пере
менной длины не используют автоматическую упаковку, а действительно содержат
примитивные типы.
Впрочем, списки аргументов переменной длины могут нормально сочетаться с авто
матической упаковкой. Пример:
//: initialization/AutoboxingVarargs.java
System.out.print("second")j
for(Integer i : args)
System.out.print(" " + i);
System.out.println();
}
static void f(Long... args) {
System.out.println("third");
>
public static void main(String[] args) {
f('a', 'b\ 'c')j
f(i);
f(2, l)j
f(0);
f(0L);
//! f(); // Не компилируется из-за неоднозначности
}
> /* Output:
first a b c
second 1
second 2 1
second 0
third
*///:~
20. (1) Напишите метод main () , использующий список аргументов переменной длины
вместо обычного синтаксиса. Выведите все элементы полученного массива args.
Протестируйте программу с разным количеством аргументов командной строки.
Перечисления
Одним из незначительных (на первый взгляд!) нововведений^уа SE5 стало ключевое
слово enum, значительно упрощающее работу программиста при группировке и исполь
зовании перечислимъис типов. В прошлом для этого приходилось создавать набор цело
численных констант, но для таких значений не существует естественных ограничений
в рамках указанного набора; таким образом, работа с ними связана с большим риском
и сложностями. Перечислимые типы имеют достаточно стандартное применение, по
этому их поддержка всегда присутствовала в С, С++ и ряде других языков. До выхода
Java SE5 nporpaMMncrJava, желающий смоделировать функциональность перечисли
мых типов, должен был много знать и действовать осторожно. Теперь перечисления
также поддерживаются Bjava, причем они обладают большими возможностями, чем
их аналоги из С/С++. Рассмотрим простой пример:
//: initialization/Spiciness.java
public enum Spiciness {
NOT, MILD, MEDIUM, НОТ, FLAMIN6
} ///:~
//: initialization/SimpleEnumUse.java
public class SimpleEnumUse {
public static void main(String[J args) {
Spiciness howHot = Spiciness.MEDIUM;
System.out.println(howHot);
}
} /* Output:
MEDIUM
*///:~
//: initialization/EnumOrder.java
public class EnumOrder {
public static void main(String[] args) {
for(Spiciness s : Spiciness.values())
System.out.println(s + ", ordinal " + s.ordinal());
>
} /* Output:
NOT, ordinal 0
MILD, ordinal 1
MEDIUM, ordinal 2
HOT, ordinal 3
FLAMING, ordinal 4
*///:~
Может показаться, что перечисления составляют новый тип данных, но на самом деле
перечисления являются классами и обладают своими методами, так что во многих от
ношениях с перечислением можно работать как с любым другим классом.
Особенно удобен механизм работы с перечислениями в командах switch:
//: initialization/Burrito.java
Резюме
Такой сложный механизм инициализации, как конструктор, показывает, насколько
важное внимание в языке уделяется инициализации. Когда Бьерн Страуструп раз
рабатывал С++, в первую очередь он обратил внимание на то, что низкая продуктив
ность С связана с плохо продуманной инициализацией, которой была обусловлена
значительная доля ошибок. Аналогичные проблемы возникают и при некорректной
финализации. Так как конструкторы позволяют гарантироватъ соответствующие
инициализацию и завершающие действия по очистке (компилятор не позволит создать
объект без вызова конструктора), тем самым обеспечивается полная управляемость
и защищенность программы.
В языке С++ уничтожение объектов играет очень важную роль, потому что объекты,
созданные оператором new, должны быть соответствующим образом разрушены. BJava
память автоматически освобождается уборщиком мусора, и аналоги деструкторов
обычно не нужны. В таких случаях уборщик MycopaJava значительно упрощает про
цесс программирования и к тому же добавляет так необходимую безопасность нри
освобождении ресурсов. Некоторые уборщики мусора могут проводить завершающие
Резюме 185
1 См. «Refactoring: Improving the Design of Existing Code», by Martin Fowler, et aI. (Addison-Wesley,
1999). Некоторые специалисты возражают против рефакторинга на том основании, что от
кода не требуется ничего, кроме его работоспособности, и рефакторинг такого кода является
напрасной тратой времени. Проблема в том, что основные затраты времени и средств при
работе над проектом связаны не с исходным написанием кода, а с его сопровождением. Более
понятный код оборачивается значительной экономией средств.
Пакет как библиотечный модуль 187
//: access/FullQualification.java
Структура кода
В результате компиляции для каждого класса, определенного в файле .java, создается
класс с тем ж е именем, но с расш ирением .class. Таким образом, при компиляции не
скольких файлов .java мож ет появиться целый ряд файлов с расш ирением . class. Если
вы программировали на ком пилируем ом языке, то, наверное, привыкли к тому, что
компилятор генерирует промежуточные файлы (обы чно с расш ирением OBJ), которые
затем объединяю тся компоновщ иком для получения исполняем ого ф айла или библи-
отеки-Java работает не так. Рабочая программа представляет собой набор однородны х
файлов .class, которые объединяю тся в пакет и сж имаю тся в ф aй л JA R (yranH ToftJava
jar). PlHTepnpeTaTopJava отвечает за поиск, загрузку и интерпретацию 1 этих файлов.
Библиотека также является набором файлов с классами. В каждом файле имеется один
public-класс с любым количеством классов, не имеющих спецификатора public. Если
вы хотите объявить, что все эти компоненты (хранящиеся в отдельных файлах .java
и .class) связаны друг с другом, воспользуйтесь ключевым словом package.
Директива package должна находиться в первой незакомментированной строке файла.
Так, команда
package access;
//: access/QualifiedMyClass.java
(Обратите внимание: первый комментарий в каждом файле этой книги задает местона
хождение файла в дереве исходного кода —эта информация используется средствами
автоматического извлечения кода.)
Если вы посмотрите на файлы, то увидите имя пакета net.mindview.simple, но что с пер
вой частью пути? О ней позаботится переменная окружения CLASSPATH, которая на
моей машине выглядит следующим образом:
CLASSPATH=.;D:\3AVA\LIB;C:\DOC\DavaT
Как видите, CLASSPATH может содержать несколько альтернативных путей для по
иска.
Однако для файлов^И . используется другой подход. Вы должны записатьимя файла
JAR в переменной CLASSPATH, не ограничиваясь указанием пути к месту его распо
ложения. Таким образом, для ф а й л а ^ Я с именем grape.jar переменная окружения
должна выглядеть так:
CLASSPATH=.;D:\3AVA\LIB;C:\flavoPS\grape.jan
Так как пакет ja v a .u til.* тоже содержит класс с именем Vector, это может приве
сти к потенциальному конфликту. Но пока вы не начнете писать код, вызывающий
конфликты, все будет в порядке — и это хорошо, поскольку иначе вам бы пришлось
тратить лишние усилия на предотвращение конфликтов, которых на самом деле нет.
Конфликт действительно произойдет при попытке создать Vector:
Vector v = new Vector();
Пользовательские библиотеки
Полученные знания позволяют вам создавать собственные библиотеки, сокращающие
или полностью исключающие дублирование кода. Для примера можно взять уже знако
мый псевдоним для метода System.out.println(), сокращающий количество вводимых
символов. Его можно включить в класс Print:
//: net/mindview/util/Print.java
// Методы печати, которые могут использоваться
// без спецификаторов благодаря конструкции
// 3ava SE5 static import.
package net.mindview.util;
import java.io.*;
Вторым компонентом этой библиотеки может быть набор методов range() (см. главу 4)
с возможностью использования синтаксиса foreach для простых целочисленных по
следовательностей :
//: net/mindview/util/Range.java
// Array creation methods that can be used without
// qualifiers, using Java SE5 static imports:
package net.mindview.util;
3 . (2) Создайте два пакета debug и debugoff, содержащие одинаковые классы с методом
debug(). Первая версия выводит на консоль свой аргумент типа String, вторая не
делает ничего. Используйте директиву static import для импортирования класса
в тестовую программу и продемонстрируйте эффект условной компиляции.
public
При использовании ключевого слова public вы фактически объявляете, что следующее
за ним объявление члена класса доступно для всех, и прежде всего для клиентских про
граммистов, использующих библиотеку. Предположим, вы определили пакет dessert,
содержащий следующий компилируемый модуль:
//: access/dessert/Cookie.java
// Создание библиотеки,
package access.dessert;
то сможете создать объект Cookie, поскольку конструктор этого класса объявлен от
крытым (public), и сам класс также объявлен как public. (Понятие открытого класса
мы позднее рассмотрим чуть подробнее.) Тем не менее метод bite() этого класса недо
ступен в файле Dinner .java, поскольку доступ к нему предоставляется только в пакете
dessert. Так компилятор предотвращает неправильное использование методов.
Пакет по умолчанию
С другой стороны, следующий код работает, хотя на первый взгляд он вроде бы на
рушает правила:
//: access/Cake.java
// Обращение к классу из другого компилируемого модуля
class Cake {
public static void main(String[] args) {
Pie x = new Pie();
x.f();
}
} /* Output:
Pie.f()
*///:~
class Pie {
void f() { System.out.println("Pie.f()"); }
> ///:~
Вроде бы эти два файла не имеют ничего общего, и все же в классе Cake можно создать
объект Pie и вызвать его метод f()! (Чтобы файлы компилировались, переменная
CLASSPATH должна содержать символ точки.) Естественно было бы предположить,
что класс Pie и метод f() имеют доступ в пределах пакета и поэтому закрыты для Cake.
Они действительно обладают доступом в пределах пакета — все верно. Однако их
доступность в классе Cake.java объясняется тем, что они ниходятся в одном каталоге
и не имеют явно заданного имени пакета. Java по умолчанию включает такие файлы
в «пакет по умолчанию» для текущего каталога, поэтому они обладают доступом
в пределах пакета к другим файлам в этом каталоге.
private
Ключевое слово private означает, что доступ к члену класса не предоставляется никому,
кроме методов этого класса. Другие классы того же пакета также не могут обращаться
к private-членам. На первый взгляд, вы вроде бы изолируете класс даже от самого себя.
198 Главаб • Управление доступом
С другой стороны, вполне вероятно, что пакет создается целой группой разработчиков;
в этом случае private позволяет изменять члены класса, не опасаясь, что это отразится
на другом классе данного пакета.
Предлагаемый по умолчанию доступ в пределах пакета часто оказывается достаточен
для сокрытия данных; напомню, что такой член класса недоступен пользователю пакета.
Это удобно, так как обычно используется именно такой уровень доступа (даже в том
случае, когда вы простй забудете добавить спецификатор доступа). Таким образом, до
ступ public чаще всего используется тогда, когда вы хотите сделать какие-либо члены
классадоступными для программиста-клиента. Может показаться, что спецификатор
дбступа private применяется редко и можно обойтись и без него. Однако разумное
применение private очень важно, особенно в условиях многопоточного программи
рования (см. главу 21).
Пример использования private:
//: access/IceCream.java
// Демонстрация ключевого слова private.
class Sundae {
private Sundae() {}
static Sundae makeASundae() {
return new Sundae();
>
>
public class IceCream {
public static void main(String[] args) {
//! Sundae x = new Sundae();
Sundae x = Sundae.makeASundae()j
>
> ///:~
Перед вами пример ситуации, в которой private может быть очень полезно: предполо
жим, вы хотите контролировать процесс создания объекта, не разрешая посторонним
вызывать конкретный конструктор (или любые конструкторы). В данном примере
запрещается создавать объекты Sundae с помощью конструктора; вместо этого поль-
зовательдолжен использовать метод makeASundae().
Все «вспомогательные» методы классов стоит объявить как private, чтобы предотвра
тить их случайные вызовы в пакете; тем самым вы фактически запрещаете изменение
поведения метода или его удаление.
То же относится и к private-полям внутри класса. Если только вы не собираетесь предоста
вить доступ пользователям к внутренней реализации (а это происходит гораздо реже, чем
можно себе представить), объявляйте все поля своих классов со спецификатором private.
protected
Чтобы понять смысл спецификатора доступа protected, необходимо немного забежать
вперед. Сразу скажу, что понимание этого раздела не обязательно до знакомства с на
следованием (глава 7). И все же для получения цельного представления здесь приво
дится описание protected и примеры его использования.
Спецификаторы доступа Java 199
Ключевое слово protected тесно связано с понятием наследования, при котором к уже
существующему классу (называемому базовым классом) добавляются новые члены,
причем исходная реализация остается неизменной. Также можно изменять поведение
уже существующих членов класса. Для создания нового класса на базе существующего
используется ключевое слово extends:
c la s s Foo e x te n d s Bar {
p u b lic c la s s C o o k ie {
продолжение ^>
200 Главаб • Управлениедоступом
p u b lic C o o k ie () {
System.out.println("KoHCTpyKTop Cookie")j
>
p ro tected v o id b ite ( ) {
S y s te m .o u t .p r in t ln ( " b ite '') j
>
> ///:~
p u b lic c la s s C h o c o la te C h ip 2 exten ds C o o k ie {
p u b lic C h o c o la te C h ip 2 () {
S y s te m .o u t.p rin tln (" C h o c o la te C h ip 2 co n stru cto r");
>
p u b lic v o id ch om p() { b ite ( ); > / / Защищенный м е т о д
p u b lic s ta tic v o id m a in (S trin g [] a rg s) {
C h o c o la te C h ip 2 x = new C h o c o l a t e C h i p 2 ( ) ;
x .c h o m p ();
>
> / O u tp u t:
Конструктор C o o k ie
Конструктор C h o c o la te C h ip 2
b ite
*///:~
Интерфейс и реализация
Контроль над доступом часто называют сокрытием реализации. Помещение данных
и методов в классы в комбинации с сокрытием реализации часто называют инкапсуляци
ей}. В результате появляется тип данных, обладающий характеристиками и поведением.
Доступ к типам данных ограничивается по двум причинам. Первая причина — про-
граммист-клиент должен знать, что он может использовать, а что не может. Вы вольны1
Доступ к классам
BJava с помощью спецификаторов доступа возможно также указать, какие из классов
внутри библиотеки будут доступны для ее пользователей. Если вы хотите, чтобы класс
был открыт программисту-клиенту, то добавляете ключевое слово public для класса
в целом. При этом вы управляете даже самой возможностью создания объектов данного
класса программистом-клиентом.
Для управления доступом к классу спецификатор доступа записывается перед клю
чевым словом class:
public class Widget {
import access.Widget;
ИЛИ
import access.*;
1 На самом деле доступ private или protected могут иметь внутренние классы , но это особый
случай (см. главу 8).
Доступ к классам 203
Если вы хотите перекрыть доступ к классу для всех, объявите все его конструкторы
со спецификатором private, соответственно запрещая кому бы то ни было создание
объектов этого класса. Только вы сами, в статическом методе своего класса, сможете
создавать такие объекты. Пример:
//: access/Lunch.java
// Спецификаторы доступа для классов.
// Использование конструкторов, объявленных private,
// делает класс недоступным при создании объектов.
class Soupl {
private Soupl() {>
// (1) Разрешаем создание объектов в статическом методе:
public static Soupl makeSoup() {
return new Soupl()j
>
>
class Soup2 {
private Soup2() {>
// (2) Создаем один статический объект
// и по требованию возвращаем ссылку на него,
private static Soup2 psl = new Soup2();
public static Soup2 access() {
return psl;
>
public void f() {}
>
// В файле может быть определен только один риЬИс-класс:
public class Lunch {
void testPrivate() {
// Запрещено, так как конструктор объявлен приватным:
//! Soupl soup = new Soupl();
}
void testStatic() {
Soupl soup = Soupl.makeSoup();
>
void testSingleton() {
Soup2.access().f();
>
> ///:~
До этого момента большинство методов возвращало или void, или один из примитив
ных типов, поэтому определение:
public static Soupl makeSoup() {
return new Soupl();
>
на первый взгляд смотрится немного странно. Слово Soupl перед именем метода
(makeSoup) показывает, что возвращается методом. В предшествующих примерах
обычно использовалось обозначение void, которое подразумевает, что метод не имеет
возвращаемого значения. Однако метод также может возвращать ссылку на объект;
в данном случае возвращается ссылка на объект класса Soupl.
204 Главаб • Управлениедоступом
Классы Soupi и Soup2 наглядно показывают, как предотвратить прямое создание объ
ектов класса, объявляя все его конструкторы со спецификатором private. Помните,
что без явного определения хотя бы одного конструктора компилятор сгенерирует
конструктор по умолчанию (конструктор без аргументов). Определяя конструктор по
умолчанию в программе, вы запрещаете его автоматическое создание. Если конструктор
объявлен со спецификатором private, никто не сможет создавать объекты данного клас
са. Но как же тогда использовать этот класс? Рассмотренный пример демонстрирует
два способа. В классе Soupi определяется статический метод, который создает новый
объект Soupi и возвращает ссылку на него. Это бывает полезно в ситуациях, где вам
необходимо провести некоторые операции над объектом перед возвратом ссылки на
него или при подсчете общего количества созданных объектов Soupl (например, для
ограничения их максимального количества).
В классе Soup2 использован другой подход — D программе вссгда создается не более
одного объекта этого класса. Объект Soup2 создается как статическая приватная пере
менная, пэтому он всегда существует только в одном экземпляре и его невозможно
получить без вызова открытого метода access().
8 . (4) По образцу примера Lunch.java создайте класс с именем ConnectionManager,
который управляет фиксированным массивом объектов Connection. Программист-
клиент не должен напрямую создавать объекты Connection, а может получать их
только с помощью статического метода в классе ConnectionManager. Когда запас
объектов у класса ConnectionManager будет исчерпан, он должен вернуть ссылку
null. Протестируйте классы в методе main().
class PackagedClass {
public PackagedClass() {
System.out.println("Co3AaeM класс в пакете");
>
>
Затем сохраните в каталоге, отличном от access/local, такой файл:
// access/foreign/Foreign.java
package access.foreign;
import access.local.*;
Резюме
В любых отношениях важно установить ограничения, которые соблюдаются всеми
сторонами. При создании библиотеки вы устанавливаете отношения с пользователем
библиотеки (программистом-клиентом), который создает программы или библиотеки
более высокого уровня с использованием ваших библиотек.
Если программисты-клиенты предоставлены сами себе и не ограничены никакими
правилами, они могут делать все, что им заблагорассудится, с любыми членами клас
са —даже теми, доступ к которым вам хотелось бы ограничить. Все детали реализации
класса открыты для окружающего мира.
В этой главе рассматривается процесс построения библиотек из классов; во-первых,
механизм группировки классов внутри библиотеки и, во-вторых, механизм управления
доступом к членам класса.
По оценкам проекты на языке С начинают «рассыпаться» примерно тогда, когда код
достигает объема от 50 до 100 Кбайт, так как С имеет единое «пространство имен»;
в системе возникают конфликты имен, создающие массу неудобств. BJava ключевое
слово package, схема именования пакетов и ключевое слово import обеспечивают полный
контроль над именами, так что конфликта имен можно легко избежать.
Существуют две причины для ограничения доступа к членам класса. Первая —предот
вращение использования клиентами внутренней реализации класса, не входящей во
внешний интерфейс. Объявление полей и методов со спецификатором private только
помогает пользователям класса, так как они сразу видят, какие члены класса для них важ
ны, а какие можно игнорировать. Все это упрощает понимание и использование класса.
Вторая, более важная причина для ограничения доступа — возможность изменения
внутренней реализации класса, не затрагивающего программистов-клиентов. На
пример, сначала вы реализуете класс одним способом, а затем выясняется, что ре
структуризация кода позволит повысить скорость работы. Отделение интерфейса от
реализации позволит сделать это без нарушения работоспособности существующего
пользовательского кода, в котором этот класс используется.
Открытый интерфейс класса — это то, что фактически видит его пользователь, по
этому очень важно «довести до ума» именно эту, самую важную, часть класса в про
цессе анализа и разработки. И даже при этом у вас остается относительная свобода
действий. Даже если идеальный интерфейс не удалось построить с первого раза, вы
можете добавить в него новые методы — без удаления уже существующих методов,
которые могут использоваться программистами-клиентами.
Управление доступом в основном ориентировано на отношения (и передачу инфор
мации) между создателем библиотеки и ее внешними клиентами. Такая ситуация
существует далеко не всегда. Например, если вы пишете весь код самостоятельно или
работаете в составе небольшой группы, все объявления могут быть объединены в один
пакет. В подобных случаях используется иная схема передачи информации, и жесткое
управление доступом порой оказывается неэффективным —доступа в пределах пакета
(по умолчанию) может быть вполне достаточно.
Повторное использование
7 классов
Синтаксис композиции
До этого момента мы уже довольно часто использовали композицию —ссылка на вне
дряемый объект просто включается в новый класс. Допустим, вам понадобился объект,
Синтаксис композиции 207
содержащий несколько объектов String, пару полей примитивного типа и объект еще
одного класса. Для не-примитивных объектов в новый класс включаются ссылки,
а примитивы определяются сразу:
//: reusing/SprinklerSystem.java
// Композиция для повторного использования кода.
class WaterSource {
private String s;
WaterSource() {
System.out.println("WaterSource()");
s = "сконструирован";
}
public String toString() { return s; >
}
public class SprinklerSystem {
private String valvel, valve2, valve3, valve4;
private WaterSource source = new WaterSource();
private int i;
private float f;
public String toString() {
return
"valvel = "+ valvel + " " +
"valve2 = "+ valve2 + " " +
"valve3 = "+ valve3 + " " +
"valve4 = "+ valve4 + "\n" +
"i = - + i + « •• + -f = -- + f + *■ - +
"source * " + source;
>
public static void main(String[] args) {
SprinklerSystem sprinklers = new SprinklerSystem();
System.out.println(sprinklers);
>
> /* Output:
WaterSource()
valvel = null valve2 = null valve3 = null valve4 = null
i = 0 f = 0.0 source = сконструирован
*///:~
В обоих классах определяется особый метод toString(). Позже вы узнаете, что каждый
не-примитивный объект имеет метод t o S t r i n g ( ) , K O T o p b m вызывается в специальных
случаях, когда компилятор располагает объектом, а хочет получить его строковое
представление в формате String. Поэтому в выражении из метода SprinklerSystem.
toString():
компилятор видит, что к строке "source = " «прибавляется» объект класса WaterSource.
Компилятор не может это сделать, поскольку к строке можно «добавить» только такую
же строку, поэтому он преобразует объект source в String, вызывая метод toString().
После этого компилятор уже в состоянии соединить две строки и передать резуЛьтат
вм eт oд Sy st em .ou t. pг in tl n() (иливстатические методы print() и ргШ лЬ(),использу-
емые в книге). Чтобы подобное поведение поддерживалось вашим классом, достаточно
включить в него метод toString().
208 Глава 7 • Повторное использование классов
class Soap {
private String s;
Soap() {
print("Soap()")j
s = "Constructed";
>
public String toString() { return s; >
>
public class Bath {
private String // Инициализация в точке определения:
sl = "Счастливый",
s2 = "Счастливый",
s3, s4;
private Soap castille;
private int i;
private float toy;
public Bath() {
print("B конструкторе Bath()");
s3 = "Радостный";
toy = 3.14f;
castille = new Soap();
}
// Инициализация экземпляра:
{ i = 47; }
public String toString() {
if(s4 == null) // Отложенная инициализация:
s4 = "Радостный";
return
"sl = " + sl + "\n" +
"s2 = " + s2 + "\n" +
Синтаксис наследования 209
Синтаксис наследования
Наследование является неотъемлемой 4acTbK>Java (и любого другого языка ООП).
Фактически оно всегда используется при создании класса, потому что даже если класс
не объявляется производным от другого класса, он автоматически становится произ
водным от корневого классаДауа Object.
Синтаксис композиции очевиден, но для наследования существует совершенно дру
гая форма записи. При использовании наследования вы фактически говорите: «Этот
новый класс похож на тот старый класс». В программе этот факт выражается перед
фигурной скобкой, открывающей тело класса: сначала записывается ключевое слово
extends, а затем имя базового (base) класса. Тем самым вы автоматически получаете
доступ ко всем полям и методам базового класса. Пример:
//: reusing/Detergent.java
// Синтаксис наследования и его свойства,
import static net.mindview.util.Print.*j
class Cleanser {
продолжение &
210 Глава 7 • Повторное использование классов
Конечно, очень важно, чтобы подобъект базового класса был правильно инициализи
рован, и гарантировать это можно только одним способом: выполнить инициализа
цию в конструкторе, вызывая при этом конструктор базового класса, у которого есть
необходимые знания и привилегии для проведения инициализации базового класса.
Java автоматически вставляет вызовы конструктора базового класса в конструктор
производного класса. В следующем примере задействовано три уровня наследования:
//: reusing/Cartoon.java
// Вызовы конструкторов при проведении наследования,
import static net.mindview.util.Print.*j
class Art {
Art() { print("KoHCTpyKTop Art"); }
>
class Drawing extends Art {
Drawing() { print("KoHCTpyKTop Drawing"); >
>
public class Cartoon extends Drawing {
public Cartoon() { print("KoHCTpyKTop Cartoon"); }
public static void main(String[] args) {
Cartoon x = new Cartoon();
}
} /* Output:
Конструктор Art
Конструктор Drawing
Конструктор Cartoon
* I I I :~
Как видите, конструирование начинается с «самого внутреннего» базового класса,
поэтому базовый класс инициализируется еще до того, как он станет доступным для
конструктора производного класса. Даже если конструктор класса Cartoon не опреде
лен, компилятор сгенерирует конструктор по умолчанию, в котором также вызывается
конструктор базового класса.
3 . (2) Докажите предыдущее утверждение.
4 . (2) Докажите, что конструкторы базового класса: (а) всегда вызываются и (б) всегда
вызываются перед исполнением конструкторов производного класса.
5 . ( 1) Создайте два класса Аи в, имеющие конструкторы по умолчанию (с пустым спи
ском аргументов), которые выводят сообщение о вызове. Создайте новый класс с,
производный от А; создайте в с поле типа в. Не создавайте конструктор С. Создайте
объект класса С и проследите за происходящим.
Конструкторы с аргументами
В предыдущем примере использовались конструкторы по умолчанию, то есть кон
структоры без аргументов. У компилятора не возникает проблем с вызовом таких
конструкторов, так как вопросов о передаче аргументов не возникает. Если класс не
имеет конструктора по умолчанию или вам понадобится вызвать конструктор базового
класса с аргументами, этот вызов придется оформить явно, с указанием ключевого
слова super и передачей аргументов:
Синтаксис наследования 213
//: reusing/Chess.java
// Наследование, конструкторы и аргументы,
import static net.mindview.util.Print.*j
class Game {
Game(int i) {
print("KoHCTpyKTop Game");
}
>
class BoardGame extends Game {
BoardGame(int i) {
super(i);
print("KoHCTpyKTop BoardGame");
}
>
public class Chess extends BoardGame {
Chess() {
super(ll)j
print("KoHCTpyKTop Chess");
}
public static void main(String[] args) {
Chess x = new Chess();
>
} /* Output:
Конструктор Game
Конструктор BoardGame
Конструктор Chess
*///:~
Делегирование
Третий вид отношений, не поддерживаемый Bjava напрямую, называется делегирова
нием. Он занимает промежуточное положение между наследованием и композицией:
экземпляр существующего класса включается в создаваемый класс (как при компози
ции), но в то же время все методы встроенного объекта становятся доступными в новом
классе (как при наследовании). Например, класс SpaceShipControls имитирует модуль
управления космическим кораблем:
//: reusing/SpaceShipControls.java
controls.down(velocity);
>
public void forward(int velocity) {
controls.forward(velocity);
>
public void left(int velocity) {
controls.left(velocity);
>
public void right(int velocity) {
controls.right(velocity);
>
public void turboBoost() {
controls.turboBoost();
}
public void up(int velocity) {
controls.up(velocity);
>
public static void main(String[] args) {
SpaceShipDelegation protector =
new SpaceShipDelegation("NSEA Protector");
protector.forward(100);
}
} / / / :~
Как видите, вызовы методов переадресуются встроенному объекту controls, а интер
фейс остается таким же, как и при наследовании. С другой стороны, делегирование
позволяет лучше управлять происходящим, потому что вы можете ограничиться не
большим подмножеством методов встроенного объекта.
Хотя делегирование не поддерживается языком Java, его поддержка присутствует
во многих средах разработки. Например, приведенный пример был автоматически
сгенерирован BjetBrains Idea IDE.
11. (3) Измените пример Detergent. java так, чтобы в нем использовалось делегирование.
class Plate {
Plate(int i) {
print("KoHCTpyKTop Plate");
}
>
class DinnerPlate extends Plate {
DinnerPlate(int i) {
super(i);
продолжение &
216 Глава 7 • Повторное использование классов
pnint("KoHCTpyKTop DinnerPlate");
>
>
class Utensil {
Utensil(int i) {
print("KoHCTpyKTop Utensil");
>
>
class Spoon extends Utensil {
Spoon(int i) {
super(i);
pnint("KoHCTpyKTOp Spoon");
>
Конструктор Fork
Конструктор Utensil
Конструктор Knife
Конструктор Plate
Конструктор DinnerPlate
Конструктор PlaceSetting
*///:~
class Shape {
Shape(int i) { print("KoHCTpyKTop Shape"); >
void dispose() { print("3aeepmeHHe Shape")j }
>
class Circle extends Shape {
Circle(int i) {
super(i);
print("PHcyeM окружность Circle");
}
void dispose() {
print("CTnpaeM окружность Circle")j
super.dispose();
>
}
продолжение ^>
2X8 Глава 7 • Повторное использование классов
Все в этой системе является некоторой разновидностью класса Shape (который, в свою
очередь, неявно наследует от корневого Knaccaobject). Каждый класс переопределяет
метод dispose() класса Shape, вызывая при этом версию метода из базового класса
с помощью ключевого слова super. Все конкретные классы, унаследованные от Shape, —
Circle, Triangle и Line, имеют конструкторы, которые просто выводят сообщение,
хотя во время жизни объекта любой метод может сделать что-то, требующее очистки.
В каждом классе есть свой собственный метод dispose(), который восстанавливает
ресурсы, не связанные с памятью, к исходному состоянию до создания объекта.
В методе main() вы можете заметить два новых ключевых слова, которые будут подробно
рассмотрены в главе 12: try и finally. Ключевое слово try показывает, что следующий
за ним блок (ограниченный фигурными скобками) является защищенной секцией. Код
в секции finally выполняется всегда, независимо от того, как прошло выполнение блока
try. (При обработке исключений можно выйти из блока try некоторыми необычными
способами.) В данном примере секция finally означает: «Что бы ни произошло, в конце
всегда вызывать метод x.dispose()».
Также обратите особое внимание на порядок вызова завершающих методов для ба
зового класса и объектов-членов в том случае, если они зависят друг от друга. В ос
новном нужно следовать тому же принципу, что использует компилятор С++ при
вызове деструкторов: сначала провести завершающие действия для вашего класса
в последовательности, обратной порядку их создания. (Обычно для этого требуется,
чтобы элементы базовых классов продолжали существовать.) Затем вызываются за
вершающие методы из базовых классов, как и показано в программе.
Во многих случаях завершающие действия не создают проблем; достаточно дать
уборщику мусора выполнить свою работу. Но уж если понадобилось провести их
явно, сделайте это со всей возможной тщательностью и вниманием, так как в процессе
уборки мусора трудно в чем-либо быть уверенным. Уборщик мусора вообще может не
220 Глава 7 • Повторное использование классов
Сокрытие имен
Если какой-либо из методов базового KnaccaJava был перегружен несколько раз, пере
определение имени этого метода в производном классе не скроет ни одну из базовых
версий (в отличие от С++). Поэтому перегрузка работает вне зависимости от того, где
был определен метод —на текущем уровне или в базовом классе:
//: reusing/Hide.java
// Перегрузка имени метода из базового класса
// в производном классе не скроет базовую версию метода.
import static net.mindview.util.Print.*;
class Homer {
char doh(char с) {
print("doh(char)");
return 'd';
>
float doh(float f) {
print("doh(float)");
return 1.0f;
>
>
class Milhouse {>
Мы видим, что все перегруженные методы класса Homer доступны классу Bart, хотя
класс Bart и добавляет новый перегруженный метод (в С++ такое действие спрятало
Композиция в сравнении с наследованием 221
бы все методы базового класса). Как вы увидите в следующей главе, на практике при
переопределении методов гораздо чаще используется точно такое же описание и список
аргументов, как и в базовом классе. Иначе легко можно запутаться (и поэтому С++
запрещает это, чтобы предотвратить совершение возможной ошибки).
B Java SE5 появилась аннотация @Override; она не является ключевым словом, но
может использоваться так, как если бы была им. Если вы собираетесь переопределить
метод, используйте @Override, и компилятор выдаст сообщение об ошибке, если вместо
переопределения будет случайно выполнена перегрузка:
//: neusing/Lisa.java
// {CompileTimeError} (Won't compile)
// двигатель
class Engine {
public void start() {} // запустить
public void rev() {} // переключить
public void stop() {} // остановить
}
продолжение •&
222 Глава 7 • Повторное использование классов
// колесо
class Wheel {
public void inflate(int psi) {} // накачать
>
// окно
class Window {
public void rollup() {> // поднять
public void rolldown() {> // опустить
>
// дверь
class Door {
public Window window = new Window(); // окно двери
public void open() {> // открыть
public void close() {} // закрыть
>
// машина
public class Car {
public Engine engine = new Engine();
public Wheel[] wheel = new Wheel[4];
public Door
left = new Door()j
right = new Door(); // двухдверная машина
public Car() {
for(int i = 0; i < 4; i++)
wheel[i] = new Wheel();
>
public static void main(String[] args) {
Car саг = new Car()j
car.left.window.rollup();
can.wheel[0].inflate(72)j
}
> /f/:~
Так как композиция объекта является частью проведенного анализа задачи (а не просто
частью реализации класса), объявление членов класса открытыми (public) помогает
программисту-клиенту понять, как использовать класс, и облегчает создателю класса
найисание кода. Однако нужно все-таки помнить, что описанный случай является
специфическим и в основном поля класса следует объявлять как private.
При использовании наследования вы берете уже существующий класс и создаете его
специализированную версию. В основном это значит, что класс общего назначения адап
тируется для конкретной задачи. Если вы чуть-чуть подумаете, то поймете, что не имело
бы смысла использовать композицию машины и средства передвижения —машина не
содержит средства передвижения, она сама есть это средство. Взаимосвязь «является»
выражается наследованием, а взаимосвязь «имеет» описывается композицией.
14 . (1) В Car.java добавьте в класс Engine метод service() и вызовите его из main().
protected
После знакомства с наследованием ключевое слово protected наконец-то обрело
смысл. В идеале закрытых членов private должно было быть достаточно. В реальности
protected 223
class Villain {
private String name;
protected void set(String nm) { name = nm; }
public Villain(String name) { this.name = name; }
public String toString() {
return "fl объект Villain и мое имя " + name;
}
>
public class Orc extends Villain {
private int orcNumber;
public Orc(String name, int orcNumber) {
super(name);
this.orcNumber = orcNumber;
}
public void change(String name, int orcNumber){
set(name); // Доступно, так как объявлено protected
this.orcNumber = orcNumber;
}
public String toString() {
return "Orc " + orcNumber + ” : " + super.toString();
}
public static void main(String[] args) {
Orc orc = new Огс("Лимбургер", 12);
print(orc);
orc.change("6o6", 19);
print(orc);
>
) /* Output:
Orc 12: Я объект Villain и мое имя Лимбургер
Orc 19: Я объект Villain и мое имя Боб
*///:~
Как видите, метод change() имеет доступ к методу set(), поскольку тот объявлен как
protected. Также обратите внимание, что метод toS tring() класса Orc определяется
с использованием версии этого метода из базового класса.
15. (2) Создайте класс в пакете. Ваш класс должен содержать метод со спецификатором
protected. Попытайтесь вызвать метод protected за пределами пакета, и объясните,
что происходит. Затем создайте класс, производный от вашего, и вызовите метод
protected из другого метода вашего производного класса.
224 Глава 7 • Повторное использование классов
c la s s In stru m e n t {
p u b lic v o id p la y ( ) {>
s t a t ic v o id tu n e (In s tru m e n t i) {
/ / ...
i.p la y ( ) ;
>
>
/ / О б ъ екты W in d та к ж е я в л я ю тс я о б ъ е к т а м и In stru m e n t,
// п о с к о л ь к у они имеют т о т же и н т е р ф е й с :
p u b lic c la s s W in d e x te n d s In stru m e n t {
p u b lic s t a t ic v o id m a in ( S t r in g [ ] a rg s) {
W in d flu t e = new W i n d ( ) j
I n s t r u m e n t .t u n e ( flu t e ) ; // В о сх о д я щ е е преобразование
>
> / / / :~
£ нижнему краю страницы. (Конечно, вы можете рисовать свои диаграммы так, как
лзчтете нужным.) Для файла Wind.java диаграмма наследования выглядит так.
Неизменные данные
Во многих языках программирования существует тот или иной способ сказать компиля
тору, что частица данных является «константой». Константы полезны в двух ситуациях:
□ константпа времени компиляции, которая никогда не меняется;
□ значение, инициализируемое во время работы программы, которое нельзя изменять.
Компилятор подставляет значение константы времени компиляции во все выраже
ния, где оно используется; таким образом предотвращаются некоторые издержки
выполнения. BJava подобные константы должны относиться к примитивным типам,
а для их определения используется ключевое слово final. Значение такой константы
присваивается во время определения.
Поле, одновременно объявленное с ключевыми словами s ta tic и fin al, существует
в памяти в единственном экземпляре и не может быть изменено.
При использовании слова fin al со ссылками на объекты его смысл не столь очевиден.
Для примитивов fin a l делает постоянным значение, но для ссылки на объект по
стоянной становится ссыпка. После того как такая ссылка будет связана с объектом,
она уже не сможет указывать на другой объект. Впрочем, сам объект при этом может
изменяться; Bjava нет механизмов, позволяющих сделать произвольный объект неиз
менным. (Впрочем, вы сами можете написать ваш класс так, чтобы его объекты фак
тически были константными.) Данное ограничение относится и к массивам, которые
тоже являются объектами.
Следующий пример демонстрирует использование fin a l для полей классов. По дей
ствующим правилам поля, объявленные одновременно с ключевыми словами sta tic
и final (то есть константы времени компиляции), записываются прописными буквами,
а входящие в них слова разделяются символами подчеркивания.
//: r e u s in g / F in a lD a t a .ja v a
/ / Действие ключевого слова final для полей.
im p o r t j a v a . u t i l . * ;
im p o r t s t a t ic n e t .m in d v ie w .u t il.P r in t .* j
c la s s V a lu e {
in t i; // д о сту п в пределах пакета
p u b lic V a lu e ( in t i) { t h is .i = i; )
}
Ключевое слово final 227
p u b lic c la s s F in a lD a t a {
p r iv a t e s t a t ic Random r a n d = new R a n d o m (4 7 );
p r iv a t e S t r in g id ;
p u b lic F in a lD a t a ( S t r in g id ) { t h is .id = id ; >
// М о г у т быть к о н с т а н т а м и в р е м е н и ко м п и л я ц и и :
p r iv a t e fin a l in t v a lu e O n e = 9 ;
p r iv a t e s t a t ic fin a l in t VALUE_TWO = 9 9 ;
// Т и п и ч н а я о т к р ы т а я константа:
p u b lic s t a t ic fin a l in t VA LU E_TH R EE = 3 9 ;
/ / Не мож ет б ы ть к о н с т а н т о й в р е м е н и ко м п и л яц и и :
p r iv a t e f in a l in t i4 = r a n d .n e x t I n t ( 2 0 ) ;
s t a t ic fin a l in t IN T _ 5 = r a n d . n e x t I n t ( 2 0 ) ;
p r iv a t e V a lu e v l = new V a l u e ( l l ) ;
p r iv a t e f i n a l V a l u e v 2 = new V a l u e ( 2 2 ) ;
p r iv a t e s t a t ic f i n a l V a l u e V A L_ 3 = new V a l u e ( 3 3 ) ;
/ / М асси вы :
p r iv a t e f in a l ir r t [ ] а = { 1, 2, 3, 4, 5, 6 };
p u b lic S t r in g to S tr in g ( ) {
re tu rn id + ": ” + " i4 = " + i4 + ”, IN T _ 5 = " + IN T 5 ;
}
p u b lic s t a t ic v o id m a in ( S t r in g [ ] a rg s) {
F in a lD a t a fd l = new F i n a l D a t a ( " f d l " ) ;
//! f d l. v a lu e O n e + + ; / / Ош ибка: значение н ел ьзя изм енить
f d l.v 2 .i+ + ; / / О б ъ е к т н е я в л я е т с я н еизм енны м !
fd l.v l = new V a l u e ( 9 ) ; // O K - не я в л я е т с я неизменны м
f o r ( in t i = 0; i < fd l.a .le n g t h ; i+ + )
fd l.a [ i] + + ; / / О б ъ е к т н е я в л я е т с я н еизм енны м !
//! fd I .v 2 = new V a l u e ( 0 ) ; / / Ош ибка: сс ы л к у
//! fd l.V A L _ 3 = new V a l u e ( l ) ; // нельзя изм енить
//! fd l.a = new i n t [ 3 ] ;
p r in t ( fd l) ;
p r in t ( " C o 3 f la e M F in a lD a t a " ) ;
F in a lD a t a fd 2 = new F i n a l D a t a ( " f d 2 ” ) ;
p r in t ( fd l) ;
p r in t ( fd 2 ) ;
}
> /* O u t p u t :
fd l: i4 = 15, IN T _ 5 = 18
С о зд а е м F i n a l D a t a
fd l: i4 = 15, IN T _ 5 = 18
fd 2 : i4 = 13, IN T _ 5 = 18
* / / / :~
18 . (2) Создайте класс, содержащий два поля: sta tic fin a l и fin a l. Продемонстрируйте
различия между ними.
Пустые константы
B Java разрешается создавать пустые константы — поля, объявленные как f i n a l , но
которым не было присвоено начальное значение. Во всех случаях пустую константу
обязательно нужно инициализировать перед использованием, и компилятор следит
за этим. Впрочем, пустые константы расширяют свободудействий при использовании
ключевого слова f i n a l , так как, например, поле f i n a l в классе может быть разным для
каждого объекта и при этом оно сохраняет свою неизменность. Пример:
//: c 0 6 :B la n k F in a l.ja v a
// "П усты е" н е и зм е н н ы е поля.
c la s s Poppet {
p r iv a t e in t i;
P o p p e t ( in t ii) { i = ii; >
}
p u b lic c la s s B la n k F in a l {
p r iv a t e fin a l in t i = 0; // И н ициализированная константа
p r iv a t e f in a l in t j; // П устая константа
p r iv a t e fin a l Poppet p; // П у с та я к о н ста н та -ссы л к а
// П у с ты е к о н с т а н т ы НЕОБХОДИМО и н и ц и а л и з и р о в а т ь
// в кон структоре:
p u b lic B la n k F in a l( ) {
j = lj // И нициализация пустой константы
p = new P o p p e t ( l ) j // И нициализация пустой неизм енной ссы л к и
}
p u b lic B la n k F in a l( in t x) {
j = x; // И нициализация пустой константы
p = new P o p p e t ( x ) ; // И нициализация пустой неизм енной ссы л к и
}
p u b lic s t a t ic v o id m a in ( S t r in g [ ] a rg s) {
Ключевое слово final 229
new BlankFinal();
new BlankFinal(47);
}
> ///:~
class Gizmo {
public void spin() {}
>
Методы f() и g() показывают, что происходит при передаче методу примитивов с по
меткой final: их значение можно прочитать, но изменить его не удастся. Эта возмож
ность чаще всего используется при передаче данных анонимным внутренним классам,
о которых вы узнаете в главе 10.
Неизменные методы
Неизменные методы используются по двум причинам. Первая причина —«блокировка»
метода, чтобы производные классы не могли изменить его содержание. Это делается
230 Глава 7 • Повторное использование классов
по соображениям проектирования, когда вам точно надо знать, что поведение метода
не изменится при наследовании.
Второй причиной в прошлом считалась эффективность. В более ранних реализациях
Java объявление метода с ключевым словом fin a l позволяло компилятору превратить
все вызовы такого метода во встроенные (inline). Когда компилятор видит метод, объ
явленный как final, он может (на свое усмотрение) пропустить стандартный механизм
вставки кода для проведения вызова метода (занести аргументы в стек, перейти к телу
метода, исполнить находящийся там код, вернуть управление, удалить аргументы из
стека и распорядиться возвращенным значением) и вместо этого подставить на место
вызова копию реального кода, находящегося в теле метода. Таким образом устраня
ются издержки обычного вызова метода. Конечно, для больших методов подстановка
приведет к «разбуханию» программы и, скорее всего, никаких преимуществ от ис
пользования прямого встраивания не будет.
В последних BepcnnxJava виртуальная машина выявляет подобные ситуации и устра
няет лишние передачи управления при оптимизации, поэтому использовать fin a l для
методов уже не обязательно —и более того, нежелательно. BJava SE5/6 стоит поручить
проблемы эффективности кoмпилятopyиJVM и объявлять методы fin a l только в том
случае, если вы хотите явно запретить переопределение1.
Специф икаторы final и private
Любой закрытый (p riv a te ) метод в классе косвенно является неизменным (fin a l)
методом. Так как вы не в силах получить доступ к закрытому методу, то не сможете
и переопределить его. Ключевое слово fin a l можно добавить к закрытому методу, но
его присутствие ни на что не повлияет.
Это может вызвать недоразумения, так как при попытке переопределения закрытого
(private) метода, также неявно являющегося final, все вроде бы работает, и компилятор
не выдает сообщений об ошибках:
//: re u sin g /Fin a lO verrid in g Illu sio n . java
// Все выглядет так, будто закрытый (и неизменный) метод
H можно переопределить, но это заблуждение,
import s ta tic n e t.m in d v ie w .u til.P rin t.* ;
class W ithFinals {
// То же, что и просто private:
private f in a l void f ( ) { p rin t("W ith F in a ls .f()" ); }
// Также автоматически является f in a l:
private void g() { p rin t("W ith F in a ls.g ()"); }
}
c la ss O verridingprivate extends W ithFinals {
private f in a l void f ( ) {
p rin t (''O verridingPrivate. f ( ) и);
>
Неизменные классы
О б ъ я в л я я к л асс н е и зм ен н ы м (за п и с ы в а я в его о п р е д ел ен и и кл ю ч ев о е сл о в о f i n a l ) ,
вы п о к азы в аете, ч то не со б и р аетесь и с п о л ь зо в а ть это т к л асс в кач еств е базо во го п р и
232 Глава 7 • Повторное использование классов
c la s s S m a llB r a in {}
fin a l c la s s D in o s a u r {
in t i = 7j
in t j = 1;
S m a llB r a in x = new S m a l l B r a i n ( ) j
v o id f() {>
>
//! c la s s F u rth e r e x te n d s D i n o s a u r {}
/ / Ош ибка: Н ельзя р а с ш и р и ть н еизм енны й к л а с с D in o s a u r
p u b lic c la s s J u r a s s ic {
p u b lic s t a t ic v o id m a in ( S t r in g [ ] a rg s) {
D in o s a u r n = new D i n o s a u r ( ) ;
n .f( ) ;
n .i = 40;
n .j + + ;
}
} / / / :~
Заметьте, что поля класса могут быть, а могут и не быть неизменными, по вашему выбо
ру. Те же правила верны и для неизменных методов, вне зависимости от того, объявлен
ли класс целиком как final. Объявление класса со спецификатором fin a l запрещает
наследование от него —и ничего больше. Впрочем, из-за того, что это предотвращает
наследование, все методы в неизменном классе также являются неизменными, по
скольку нет способа переопределить их. Поэтому компилятор имеет тот же выбор для
обеспечения эффективности выполнения, как и в случае с явным объявлением методов
как fin al. И если вы добавите спецификатор fin a l к методу в классе, объявленном
всецело как fin al, то это ничего не будет значить.
22. Создайте неизменный (final) класс и попытайтесь создать класс, производный от него.
Предостережение
На первый взгляд идея объявления неизменных методов (fin a l) во время разработки
класса выглядит довольно заманчиво —никто не сможет переопределить ваши методы.
Иногда это действительно так.
Но будьте осторожнее в своих допущениях. Трудно предусмотреть все возможности
повторного использования класса, особенно для классов общего назначения. Определяя
метод как final, вы блокируете возможность использования класса в проектах других
программистов только потому, что сами не могли предвидеть такую возможность.
•
зрения логики. Тем не менее мы видим пример ситуации, в которой сами проектиров
щики Java используют наследование от V e c t o r . В тот момент, когда класс S t a c k был
создан, они должны были осознать, что fin a l-методы вводят излишние ограничения.
Во-вторых, многие полезные методы класса V e c t o r , такие как a d d E le m e n t ( ) и e l e m e n t A t ( ) ,
объявлены с ключевым словом s y n c h r o n i z e d . Как вы увидите в главе 12, синхронизация
сопряжена со значительными издержками во время выполнения, которые, вероятно,
сводят к нулю все преимущества от объявления метода как f i n a l . Все это лишь под
тверждает теорию о том, что программисты не умеют правильно находить области
для применения оптимизации. Очень плохо, что такой неуклюжий дизайн проник
в стандартную библиотеку Java. (К счастью, современная библиотека контейнеров
Java заменяет v e c t o r классом A r r a y L i s t , который сделан гораздо более аккуратно и по
общепринятым нормам. К сожалению, существует очень много готового кода, напи
санного с использованием старой библиотеки контейнеров.)
Также интересно, что класс Hashtable, другой важный класс стандартной библиотеки
Java 1 .0 /1 .1 , не содерж ит ни одного f i n a l -метода. О чевидно, что классы стандартной
библиотеки создавались соверш енно разны ми лю дьм и (в частности, им ена м етодов
Hashtable значительно короче им ен м етодов v ecto r — это ещ е одно доказательство),
а ведь именно этот факт не долж ен быть очевиден для пользователей библиотек. Н еп о
следовательное проектирование лишь услож няет работу пользователя — лиш ний довод
в пользу сквозного контроля архитектуры и кода (кстати, в соврем енной библиотеке
KOHTeiiHepoBjava класс Hashtable зам енен классом HashMap).
1 Конструктор также является статическим методом, хотя ключевое слово static и не указыва
ется явно. Таким образом, формально загрузка класса выполняется при первом обращении
к любому из его статических методов.
234 Глава 7 • Повторное использование классов
Инициализация с наследованием
Полезно разобрать процесс инициализации полностью, включая наследование, чтобы
получить общую картину происходящего. Рассмотрим следующий пример:
//: r e u s in g / B e e t le .ja v a
// Полный п р о ц е с с ини ци ализации .
im p o r t s t a t ic n e t .m in d v ie w .u t il.P r in t .* ;
c la s s In sect {
p r iv a t e in t i = 9;
p ro te c te d in t j;
In se ct() {
S y s te m . o u t . p r i n t l n ( " i = " + i + ", j = " + j) j
j = 39;
>
p r iv a t e s t a t ic in t x l =
p r in t in it ( " n o n e s t a t ic I n s e c t .x l и н и ци ал и зи р о ван о ");
s t a t ic in t p r in t I n it ( S t r in g s) {
p r in t ( s ) ;
re tu rn 47;
>
>
няется по тем же правилам и в том же порядке, что и для производного класса. После
завершения работы конструктора базового класса инициализируются переменные,
в порядке их определения. Наконец, выполняется оставшееся тело конструктора.
23 . (2) Продемонстрируйте, что загрузка класса выполняется только один раз. Дока
жите, что загрузка может быть вызвана как созданием первого экземпляра класса,
так и обращением к статическому члену.
24 . (2) В файле Beetle.java создайте еще один тип, производный от B eetle, в таком же
формате, как и у других классов. Проследите за работой программы и объясните
результат.
Резюме
Как наследование, так и композиция позволяют создавать новые типы на основе уже
существующих типов. Композиция обычно применяется для повторного использо
вания реализации в новом типе, а наследование — для повторного использования
интерфейса. Так как производный класс имеет интерфейс базового класса, к нему
можно применить восходящее преобразование к базовому классу; это очень важно
для работы полиморфизма (см. следующую главу).
Несмотря на особое внимание, уделяемое наследованию в ООП, при начальном про
ектировании обычно предпочтение отдается композиции, а к наследованию следует
обращаться только там, где это абсолютно необходимо. Композиция обеспечивает
несколько большую гибкость. Вдобавок, применяя хитрости наследования к встроен
ным типам, можно изменять точный тип и соответственно поведение этих встроенных
объектов во время исполнения. Таким образом, появляется возможность изменения
поведения составного объекта во время исполнения программы.
При проектировании системы вы стремитесь создать иерархию, в которой каждый
класс имеет определенную цель, чтобы он не был ни излишне большим (не содержал
слишком много функциональности, затрудняющей его повторное использование),
236 Глава 7 • Повторное использование классов
ни раздражающе мал (так, что его нельзя использовать сам по себе, не добавив перед
этим дополнительные возможности). Если архитектура становится слишком сложной,
часто стоит внести в нее новые объекты, разбивая существующие объекты на меньшие
составные части.
Важно понимать, что проектирование программы является пошаговым, последо
вательным процессом, как и обучение человека. Оно основано на экспериментах;
сколько бы вы ни анализировали и ни планировали, в начале работы над проектом у
вас еще останутся неясности. Процесс пойдет более успешно —и вы быстрее добьетесь
результатов, если начнете «выращивать» свой проект как живое, эволюционирующее
существо, нежели «воздвигнете» его сразу, как небоскреб из стекла и металла. Насле
дование и композиция — два важнейших инструмента объектно-ориентированного
программирования, которые помогут вам выполнять эксперименты такого рода.
Полиморфизм
p u b lic enum N o t e {
M ID D L E _ C , C_SHARP, B_FLAT; // И т .д .
} / / / :~
c la s s In stru m e n t {
p u b lic v o id p la y ( N o t e n) {
p r in t ( "In stru m e n t. p la y ( ) ” );
>
>
/ / / :~
//: p o ly m o r p h is m / m u s ic / W in d .ja v a
package p o ly m o r p h is m .m u s ic ;
//: p o ly m o r p h is m / m u s ic / M u s ic . j a v a
// Н аследование и восходящ ее преобразование
padkage p o ly m o r p h is m .m u s ic ;
p u b lic c la s s M u s ic {
p u b lic s t a t ic v o id tu n e (In s tru m e n t i) {
// ...
i . p l a y ( N o t e . M ID D LE _ C );
>
Снова о восходящем преобразовании 239
p u b lic s t a t ic v o id m a in ( S t r in g [ ] a rg s) {
W in d f l u t e * new W in d ( ) j
t u n e ( flu t e ) j // В о сх од ящ е е п р е о б р а з о в а н и е
}
} /* O u t p u t :
W in d .p la y ( ) M IDDLE_C
*///:~
c la s s S t r in g e d e x te n d s In stru m e n t {
p u b lic v o id p la y ( N o t e n) {
p r in t ( " S t r in g e d .p la y ( ) " + n )j
}
}
c la s s B ra ss e x te n d s In stru m e n t {
p u b lic v o id p la y ( N o t e n) {
p r in t ( " B r a s s .p la y ( ) " + n )j
>
>
p u b lic c la s s M u s ic 2 {
p u b lic s t a t ic v o id t u n e ( W in d i) {
i . p l a y ( N o t e . MIDDLE C ) ;
}
p u b lic s t a t ic v o id t u n e ( S t r in g e d i) {
i . p l a y ( N o t e . M ID D LE_C ) ;
}
p u b lic s t a t ic v o id tu n e (B ra s s i) {
i . p l a y ( N o t e . M ID D LE _ C ) ;
>
p u b lic s t a t ic v o id m a in ( S t r in g [ ] a rg s) {
W in d f l u t e = new W i n d ( ) ;
S t r in g e d v io lin = new S t r i n g e d ( ) ;
продолжение &
240 Глава 8 • Полиморфизм
B ra ss fre n c h H o rn = new B r a s s ( ) ;
t u n e ( flu t e ) ; // Без восходящ его преобразования
t u n e ( v io lin ) ;
tu n e (fre n c h H o rn );
>
} /* O u t p u t :
W in d .p la y ( ) M ID D LE_C
S t r in g e d .p la y ( ) M ID D LE_C
B r a s s .p la y O M ID D LE_C
* / / / :~
Программа работает, но у нее есть огромный недостаток: для каждого нового Instrument
приходится писать новый, зависящий от конкретного типа метод tune(). Объем про
граммного кода увеличивается, а при добавлении нового метода (такого, как tune())
или нового типа инструмента придется выполнить немало дополнительной работы.
А если учесть, что компилятор не выводит сообщений об ошибках, если вы забудете
перегрузить один из ваших методов, то и весь процесс работы с типами станет совер
шенно неуправляемым.
Разве не лучше было бы написать единственный метод, в аргументе которого передается
базовый класс, а не один из производных классов? Разве не удобнее было бы забыть
о производных классах и написать обобщенный код для базового класса?
Именно это и позволяет делать полиморфизм. Тем не менее многие программисты
с опытом работы на процедурных языках при работе с полиморфизмом испытывают
некоторые затруднения.
1. (2) Создайте класс C y c l e с производными классами U n i c y c l e , B i c y c l e и T r i c y c l e .
Покажите, что экземпляр каждого из производных типов может быть преобразован
в C y c l e , на примере вызова метода r i d e ( ) .
Особенности
Сложности с программой Music.java обнаруживаются после ее запуска. Она выводит
строку wind.play(). Именно это и требуется, но непонятно, откуда берется такой ре
зультат. Взгляните на метод tuneQ:
p u b lic s t a t ic v o id tu n e (In stru m e n t i) {
/ / ...
i . p l a y ( N o t e . M ID D L E _ C );
>
Связывание «метод-вызов»
П р и с о е д и н е н и е в ы зо в а м ето д а к т е л у м ето д а н а зы в а е т с я связыванием. Е с л и с в я з ы в а
н и е п р о в о д и т с я п ер ед зап у с к о м п р о гр ам м ы (к о м п и л я т о р о м и к о м п о н о в щ и к о м , есл и
Особенности 241
он есть), оно называется ранним связыванием (early binding). Возможно, ранее вам не
приходилось слышать этот термин, потому что в процедурных языках никакого выбора
связывания не было. Компиляторы С поддерживают только один тип вызова —раннее
связывание.
Неоднозначность предыдущей программы кроется именно в раннем связывании:
компилятор не может знать, какой метод нужно вызывать, когда у него есть только
ссылка на объект I n s t r u m e n t .
Проблема решается благодаря позднему связыванию (late binding), то есть связыванию,
проводимому во время выполнения программы, в зависимости от типа объекта. Позд
нее связывание также называют д и н а м и ч е с к и (dynamic) или связыванием на стадии
выполнения (runtime binding). В языках, реализующих позднее связывание, должен
существовать механизм определения фактического типа объекта во время работы
программы для вызова подходящего метода. Иначе говоря, компилятор не знает типа
объекта, но механизм вызова методов определяет его и вызывает соответствующее
тело метода. Механизм позднего связывания зависит от конкретного языка, но не
трудно предположить, что для его реализации в объекты должна включаться какая-то
дополнительная информация.
Для всех методов Java используется механизм позднего связывания, если только
метод не был объявлен как f i n a l (приватные методы являются f i n a l по умолчанию).
Следовательно, вам не придется принимать решений относительно использования
позднего связывания —оно осуществляется автоматически.
Зачем объявлять метод как f i n a l ? Как уже было замечено в предыдущей главе, это
запрещает переопределение соответствующего метода. Что еще важнее, это фактиче
ски «отключает» позднее связывание или, скорее, указывает компилятору на то, что
позднее связывание не является необходимым. Поэтомудля методов f i n a l компилятор
генерирует чуть более эффективный код. Впрочем, в большинстве случаев влияние на
производительность вашей программы незначительно, поэтому f i n a l лучше использо
вать в качестве продуманного элемента своего проекта, а не как средство улучшения
производительности.
p u b lic c la s s Shape {
p u b lic v o id d ra w () {>
p u b lic v o id e ra se () {}
> / / / :~
//: p o ly m o r p h is m / s h a p e / C ir c le .j a v a
package p o ly m o r p h is m .s h a p e j
im p o r t s t a t ic n e t .m in d v ie w .u t il.P r in t .* ;
p u b lic c la s s C ir c le e x te n d s Shape {
p u b lic v o id d ra w () { p r in t ( " C ir c le .d r a w ( ) " ) ; }
p u b lic v o id e ra se () { p r in t ( " C ir c le .e r a s e ( ) " ) j >
> / / / :~
Особенности 243
//: p o ly m o r p h is m / s h a p e / S q u a r e . j a v a
package p o ly m o r p h is m . s h a p e ;
im p o r t s t a t ic n e t .m in d v ie w .u t il.P r in t .* ;
p u b lic c la s s S q u a re e x te n d s Shape {
p u b lic v o id d ra w () { p r in t ( " S q u a r e .d r a w Q " ) ; >
p u b lic v o id e ra se () { p r in t ( " S q u a r e .e r a s e Q " ) ; >
> / / / :~
//: p o ly m o r p h is m / s h a p e / T r ia n g le . ja v a
package p o ly m o r p h is m . s h a p e ;
im p o r t s t a t ic n e t .m in d v ie w .u t il.P r in t .* ;
p u b lic c la s s T r ia n g le e x te n d s Shape {
p u b lic v o id d ra w () { p r in t ( " T r ia n g le .d r a w ( ) " ) ; }
p u b lic v o id e ra se () { p r in t ( " T r ia n g le .e r a s e ( ) " ) ; }
> /ff'~
//: p o ly m o r p h is m / s h a p e / R a n d o m S h a p e G e n e r a t o r .ja v a
// "Ф аб р и ка", сл уч ай н ы м о б р а з о м создаю щ ая о б ъ е к т ы :
p a c k a g e p o ly m o r p h is m . s h a p e ;
im p o r t j a v a . u t i l . * ;
p u b lic c la s s R a n d o m S h a p e G e n e r a to r {
p r iv a t e Random r a n d = new R a n d o m (4 7 );
p u b lic Shape n e x t ( ) {
s w it c h ( r a n d .n e x t I n t ( 3 ) ) {
d e fa u lt :
case 0: re tu rn new C i r c l e ( ) ;
case 1: re tu rn new S q u a r e ( ) ;
case 2: re tu rn new T r i a n g l e ( ) ;
>
>
> ///:~
//: p o l y m o r p h is m / S h a p e s .j a v a
// P o ly m o r p h is m in 3ava.
im p o r t p o l y m o r p h is m . s h a p e . * ;
p u b lic c la s s Shapes {
p r iv a t e s t a t ic R a n d o m S h a p e G e n e r a to r g e n =
new R a n d o m S h a p e G e n e r a t o r ( ) ;
p u b lic s t a t ic v o id m a in ( S t r in g [ ] a rg s) {
S h ap e[] s * new S h a p e [ 9 ] ;
/ / З а п о л н я е м м а с с и в ф и гу р а м и :
fo r ( in t i = 0; i < s .le n g t h ; i+ + )
s [ i] = g e n .n e x t ( ) ;
/ / Полиморфные вы зовы м е т о д о в :
fo r(S h a p e sh p : s)
s h p .d r a w ( ) ;
>
} /* O u t p u t :
T r ia n g le .d r a w ( )
T r ia n g le .d r a w ( )
S q u a r e .d r a w ( )
T r ia n g le .d r a w ( )
S q u a r e .d r a w ( )
T r ia n g le .d r a w ( )
продолжение &
244 Глава 8 • Полиморфизм
S q u a r e .d r a w ( )
T r ia n g le .d r a w ( )
C ir c le .d r a w ( )
*///:~
Расширяемость
Теперь вернемся к программе Music.java. Благодаря полиморфизму вы можете добавить
в нее сколько угодно новых типов, не изменяя метод t u n e ( ) . В хорошо спроектирован
ной ООП-программе большая часть ваших методов (или даже все методы) следуют
модели метода t u n e ( ) , оперируя только с интерфейсом базового класса. Такая про
грамма является расширяемой, поскольку в нее можно добавить дополнительную
функциональность, определяя новые типы данных от общего базового класса. Методы,
Особенности 245
c la s s In stru m e n t {
v o id p la y ( N o t e n) { p r in t ( " I n s t r u m e n t .p la y ( ) '' + n ) ; }
S t r in g w h a t( ) { re tu rn "In stru m e n t"; >
v o id a d ju s t() { p r in t ( " A d ju s t in g In stru m e n t"); >
>
c la s s W ind e x t e n d s In stru m e n t {
v o id p la y ( N o t e n) { p r in t ( " W in d .p la y ( ) " + n ); }
S t r in g w h a t() { re tu rn " W in d " ; )
v o id a d ju s t() { p r in t ( " A d ju s t in g W in d " ) ; }
>
c la s s P e r c u s s io n e x te n d s In stru m e n t {
v o id p la y ( N o t e n) { p r in t ( " P e r c u s s io n .p la y ( ) " + n ); }
S t r in g w h a t() { re tu rn " P e r c u s s io n " ; }
v o id a d ju s t() { p r in t ( " A d ju s t in g P e r c u s s io n " ) ; }
}
продолжение ^>
246 Глава 8 • Полиморфизм
c la s s S t r in g e d e x te n d s In stru m e n t {
v o id p la y ( N o t e n) { p r in t ( " S t r in g e d .p la y ( ) " + n ); >
S t r in g w h a t() { re tu rn " S t r in g e d " ; >
v o id a d ju st() { p r in t ( " A d ju s t in g S t r in g e d " ) ; >
>
c la s s B ra ss e x t e n d s W in d {
v o id p la y ( N o t e n) { p r in t ( " 8 r a s s .p la y ( ) " + n ); >
v o id a d ju s t() { p r in t ( " A d ju s t in g B ra ss” ); )
>
c la s s W oodw ind e x t e n d s W in d {
v o id p la y ( N o t e n) { p r in t ( " W o o d w in d .p la y ( ) ” + n ); )
S t r in g w h a t() { re tu rn "W o o d w in d ” ; }
>
p u b lic c la s s M u s ic 3 {
// Р а б о та м етода не з а в и с и т о т ф а к ти ч е ск о го ти п а о б ъ е к т а ,
// п о это м у типы , добавленны е в с и с т е м у , будут работать правильно:
p u b lic s t a t ic v o id tu n e (In stru m e n t i) {
// ...
i.p la y ( N o t e .M I D D L E _ C ) ;
>
p u b lic s t a t ic v o id t u n e A ll( I n s t r u m e n t [ ] e) {
fo r(In s tru m e n t i : e)
t u n e ( i) ;
>
p u b lic s t a t ic v o id m a in ( S t r in g [ ] a rg s) {
/ / В о сх о д я щ е е п р е о б р а з о в а н и е пр и д о б а в л е н и и в м а с с и в :
In stru m e n t[] o rch e stra = {
new W i n d ( ) ,
new P e r c u s s i o n ( ) ,
new S t r i n g e d ( ) ,
new B r a s s ( ) ,
new W o o d w in d ()
>J
t u n e A ll( o r c h e s t r a ) ;
>
> /* O u t p u t :
W in d .p la y ( ) M IDD LE_C
P e r c u s s io n .p la y ( ) M IDD LE_C
S t r in g e d .p la y ( ) M IDD LE_C
B r a s s .p la y ( ) M IDD LE_C
W o o d w in d .p la y ( ) M ID D LE_C
* // /:~
6. ( 1) Измените программу Music3.java так, чтобы метод what() стал методом корневого
KnaccaObject t o S t r i n g ( ). Попробуйте вывести информацию об объектах I n s t r u m e n t
вызовом S y s t e m . o u t . p r i n t l n ( ) (без использования преобразований).
7 . (2) Добавьте новый подтип I n s t r u m e n t в программу Music3.java. Убедитесь в том, что
полиморфизм работает правильно и для этого нойого типа.
8. (2) Измените программу Music3.java, чтобы в ней случайным образом генерировались
объекты i n s t r u m e n t , как в программе Shapes.java.
9. (3) Создайте иерархию наследования, используя в качестве основы различные типы
грызунов. Базовым классом станет R o d e n t (грызун), а производными классами будут
M o u se (мышь), H a m s t e r (хомяк) и т. п. В базовом классе определите несколько общих
методов, которые затем переопределите в производных классах, для того чтобы они
производили действие, свойственное конкретному типу объекта. Создайте массив
из объектов R o d e n t , заполните его различными производными типами и вызовите
методы базового класса, чтобы увидеть результат работы программы.
10. (3) Создайте базовый класс с двумя методами. В первом методе вызовите второй
метод. Определите производный класс и переопределите второй метод. Создайте
объект производного класса, выполните восходящее преобразование к базовому
типу и вызовите первый метод. Объясните результат.
p u b lic c la s s P r iv a t e O v e r r id e {
p r iv a t e v o id f() { p r in t ( " p r iv a t e f()" ); >
p u b lic s t a t ic v o id m a in ( S t r in g [ ] a rg s) {
P r iv a t e O v e r r id e p o = new D e r i v e d ( ) ;
p o .f( ) ;
>
>
c la s s D e r iv e d e x te n d s P r iv a t e O v e r r id e {
p u b lic v o id f() { p r in t ( ”p u b lic f ( ) M) ; >
} /* O u tp u t:
p r iv a t e f()
*///:~
c la s s Super {
p u b lic in t fie ld = 0;
p u b lic in t g e t F ie ld ( ) { re tu rn fie ld ; >
>
c la s s Sub e x te n d s Super {
p u b lic in t fie ld = 1;
p u b lic in t g e t F ie ld ( ) { re tu rn f ie ld ; }
p u b lic in t g e t S u p e r F ie ld ( ) { re tu rn s u p e r .fie ld ; }
>
p u b lic c la s s F ie ld A c c e s s {
p u b lic s t a t ic v o id m a in ( S t r in g [ ] a rg s) {
S u p e r s u p = new S u b ( ) ; // U p c a s t
S y s t e m .o u t .p r in t ln ( " s u p .fie ld = " + s u p .f ie ld +
", s u p .g e t F ie ld ( ) = " + s u p .g e t F ie ld ( ) ) j
S u b su b = new S u b ( ) ;
S y s t e m .o u t .p r in t ln ( " s u b .fie ld = " +
s u b .fie ld + ", s u b .g e t F ie ld ( ) = " +
s u b .g e t F ie ld ( ) +
", s u b .g e t S u p e r F ie ld ( ) = " +
s u b .g e t S u p e r F ie ld ( ) ) ;
}
} /* O u tp u t:
s u p .f ie ld = 0, s u p .g e t F ie ld ( ) = 1
s u b .fie ld = 1, s u b .g e t F ie ld ( ) = 1, s u b .g e t S u p e r F ie ld ( ) = 0
*///:~
а только в виде побочного эффекта от вызова методов. Кроме того, использовать одно
имя для поля базового и производного класса вообще не рекомендуется, потому что
это создает путаницу.
Статические методы не поддерживают полиморфного поведения:
//: p o l y m o r p h is m / S t a t i c P o ly m o r p h is m . j a v a
// С т а т и ч е с к и е м етоды не я вл я ю тся полиморфными.
c la s s S t a t ic S u p e r {
p u b lic s t a t ic S t r in g s t a t ic G e t ( ) {
re tu rn "Б азов ая вер си я s t a t i c G e t ( ) " j
>
p u b lic S t r in g d y n a m ic G e t ( ) {
re tu rn "Б азов ая в е р с и я d y n a m ic G e t ( ) " j
}
>
c la s s S t a t ic S u b e x te n d s S t a t ic S u p e r {
p u b lic s t a t ic S t r in g s t a t ic G e t ( ) {
re tu rn "П р о и зв о д н а я в е р с и я s t a t i c G e t ( ) " ;
>
p u b lic S t r in g d y n a m ic G e t ( ) {
re tu rn "П р о и з в о д н а я в е р с и я d y n a m ic G e t ( ) " ;
}
}
p u b lic c la s s S t a t ic P o ly m o r p h is m {
p u b lic s t a t ic v o id m a in ( S t r in g [ ] a rg s) {
S t a t ic S u p e r su p = new S t a t i c S u b ( ) ; // В осход ящ ее п р е о б р а з о в а н и е
S y s te m . o u t . p r i n t l n ( s u p . s t a t i c G e t ( ) ) ;
S y s t e m . o u t . p r i n t l n ( s u p . d y n a m ic G e t ( ) ) ;
>
} /* O u t p u t :
Б азо вая вер си я s t a t i c G e t ( )
П р о и зв о д н а я в е р с и я d y n a m ic G e t ( )
*///:~
Конструкторы и полиморфизм
Конструкторы отличаются от обычных методов, и эти отличия проявляются и при
использовании полиморфизма. Хотя конструкторы сами по себе не полиморфны (фак
тически они представляют собой статические методы, только ключевое слово s t a t i c
опущено), вы должны хорошо понимать, как работают конструкторы в сложных по
лиморфных иерархиях. Такое понимание в дальнейшем поможет избежать некоторых
затруднительных ситуаций.
c la s s M eal {
M e a l( ) { p r in t ( " M e a l( ) " ) ; }
}
c la s s B re a d {
B re a d () { p r in t ( ''B r e a d ( ) " ) J >
>
c la s s Cheese {
C h e e se () { p r in t ( " C h e e s e ( ) " ) j >
>
c la s s L e ttu ce {
L e ttu ce () { p r in t ( " L e t t u c e ( ) " ) j >
}
c la s s Lunch e x te n d s M eal {
Lu n ch () { p r in t ( " L u n c h ( ) " ) j >
>
c la s s P o r t a b le L u n c h e x te n d s Lunch {
P o r t a b le L u n c h ( ) { p r in t ( " P o r t a b le L u n c h ( ) " ) ;}
>
p u b lic c la s s S a n d w ic h e x t e n d s P o r t a b le L u n c h {
p r iv a t e B re a d b = new B r e a d ( ) ;
p r iv a t e C heese c = new C h e e s e ( ) ;
p r iv a t e L e ttu ce 1 = new L e t t u c e ( ) ;
p u b lic S a n d w ic h ( ) { p r i n t ( " S a n d w i c h ( ) " ) ; >
p u b lic s t a t i c v o id m a in ( S t r in g [ ] a r g s ) {
new S a n d w ic h ( ) ;
>
Конструкторы и полиморфизм 251
> /* O u t p u t :
M e a l( )
L u n ch ()
P o r t a b le L u n c h ( )
B re a d ()
C h e e se ()
L e ttu ce ()
S a n d w ic h ( )
*///:~
c la s s C h a r a c t e r is t ic {
p r iv a t e S t r in g s;
C h a r a c t e r is t ic ( S t r in g s) {
t h is .s = s;
p r in t ( " C o 3 fla e M C h a r a c t e r is t ic " + s);
>
p ro te c te d v o id d is p o s e ( ) {
p r in t ( " 3 a e e p u j a e M C h a r a c t e r is t ic " + s);
>
>
c la s s D e s c r ip t io n {
p r iv a t e S t r in g s;
D e s c r ip t io n ( S t r in g s) {
t h is .s = s;
p r in t ( " C o 3 fla e M D e s c r ip t io n " + s);
>
p ro te c te d v o id d is p o s e ( ) {
p r in t ( " 3 a e e p u j a e M D e s c r ip t io n " + s);
>
}
/ / ж и в о тн о е
c la s s A n it n a l e x te n d s L iv in g C r e a t u r e {
p r iv a t e C h a r a c t e r is t ic p =
new C h a r a c t e r i s t i c ( " H M e e T се р д ц е ");
p r iv a t e D e s c r ip t io n t =
new Description('^nBOTHoe, не растение");
A n im a l( ) { p r in t ( " A n im a l( ) " ) ; }
p ro te c te d v o id d is p o s e ( ) {
Конструкторы и полиморфизм 253
p r in t ( " d is p o s e ( ) в A n im a l ");
t .d is p o s e ( ) ;
p .d is p o s e ( ) ;
s u p e r .d is p o s e ( ) ;
>
// зе м н о в о д н о е
c la s s A m p h ib ia n e x te n d s A n im a l {
p r iv a t e C h a r a c t e r is t ic p =
new Characteristic("MoxeT жить в воде");
private Description t =
new D e s c r i p t i o n ( " n в воде, и на з е м л е " ) ;
A m p h ib ia n ( ) {
p r in t ( ''A m p h ib ia n ( ) " ) ;
>
p ro te c te d v o id d is p o s e ( ) {
p r in t ( " d is p o s e ( ) в A m p h ib ia n ");
t .d is p o s e ( ) ;
p .d is p o s e ( ) ;
s u p e r .d is p o s e ( ) ;
>
>
// л ягуш ка
p u b lic c la s s F ro g e x t e n d s A m p h ib ia n {
private Characteristic p = new Characteristic("KBaKaeT");
private Description t = new Description("ecT жуков");
public Frog() { print(''Frog()"); }
protected void dispose() {
print("3aBepmeHHe Frog");
t.dispose();
p.dispose();
super.dispose();
>
p u b lic s t a t ic v o id m a in ( S t r in g [ ] a rg s) {
F ro g f r o g = new F r o g ( ) ;
print("noKa!");
frog.dispose();
>
} /* O u t p u t :
С о зд а е м C h a r a c t e r i s t i c ж ивое с у щ е с т в о
С о зд а е м D e s c r i p t i o n обы чное ж ивое су щ е с т в о
L iv in g C r e a t u r e ( )
С о зд а е м C h a r a c t e r i s t i c им еет сердце
С о зд а е м D e s c r i p t i o n ж ивотное, не р а с т е н и е
A n im a l( )
С о зд а е м C h a r a c t e r i s t i c мож ет ж ить в во д е
С о зд а е м D e s c r i p t i o n и в воде, и н а зе м л е
A m p h ib ia n ( )
С о зд а е м C h a r a c t e r i s t i c квакает
С о зд а е м D e s c r i p t i o n е с т ж уков
F ro g ()
П о ка!
з а в ер ш ен и е F r o g
Заверш аем D e s c r i p t i o n е с т ж уков
Заверш аем C h a r a c t e r i s t i c квакает
d is p o s e ( ) в A m p h ib ia n
продолжение &
254 Глава 8 • Полиморфизм
c l3 s s S h a re d {
p r iv a t e in t re fco u n t = 0;
p r iv a t e s t a t ic lo n g c o u n te r = 0;
p r iv a t e f in a l lo n g id = c o u n te r + + ;
p u b lic S h a re d () {
p r in t ( " C o 3 A a e M " + t h is ) ;
>
p u b lic v o id a d d R e f() { re fco u n t+ + ; }
p ro te c te d v o id d is p o s e ( ) {
if ( - - r e f c o u n t == 0 )
p r in t ( " D is p o s in g " + t h is ) ;
>
p u b lic S t r in g t o S tr in g ( ) { re tu rn "S h a re d " + id ; )
>
Конструкторы и полиморфизм 255
c la s s C o m p o s in g {
p r iv a t e S h a re d sh a re d ;
p r iv a t e s t a t ic lo n g c o u n te r = 0;
p r iv a t e f in a l lo n g id = c o u n te r + + ;
p u b lic C o m p o s in g ( S h a r e d sh a re d ) {
p r in t ( " C o 3 f la e M " + t h is ) ;
th is .s h a r e d = sh a re d ;
t h i s . s h a re d . a d d R e f() ;
}
protected void dispose() {
print("disposing " + this);
shared.dispose();
>
p u b lic S t r in g to S tr in g ( ) ( re tu rn " C o m p o s in g " + id ; >
>
class Glyph {
void draw() { print("Glyph.draw()"); >
Glyph() {
print("Glyph() перед вызовом draw()");
draw();
print("Glyph() после вызова draw()");
>
>
class RoundGlyph extends Glyph {
private int radius = 1;
RoundGlyph(int r) {
radius = r;
print(''RoundGlyph.RoundGlyph()j radius = " + radius);
}
void draw() {
print("RoundGlyph.draw(), radius = " + radius);
}
>
Конструкторы и полиморфизм 257
//: polymorphism/CovariantReturn.java
class Grain {
public StringtoString() { return "Grain"; }
>
class Wheat extends Grain {
public String toString() { return "Wheat"j >
}
class Mill {
Grain process() { return new Grain(); >
>
class WheatMill extends Mill {
Wheat process() { return new Wheat()j }
>
public class CovariantReturn {
public static void main(String[] args) {
Mill m = new Mill();
Grain g - m.process();
5ystem.out.println(g);
m = new WheatMill();
g = m.process();
System.ou t.println(g)j
>
> /* Output:
Grain
Wheat
*///:~
Главное o^H 4H eJava SE5 от предыдущих BepcHftJava заключается в том, что старые
версии заставляли переопределение process() возвращать Grain вместо wheat, хотя
тип Wheat, производный от Grain, является допустимым возвращаемым типом. Ко
вариантность возвращаемых типов позволяет вернуть более специализированный
тип Wheat.
Наследование при проектировании 259
class Actor {
public void act() {>
>
class HappyActor extends Actor {
public void act() { print("HappyActor"); >
>
class SadActor extends Actor {
public void act() { print("SadActor"); )
>,
class Stage {
private Actor actor = new HappyActor();
public void change() { actor = new SadActor(); }
public void performPlay() { actor.act(); >
>
public class Transmogrify {
public static void main(String[] args) {
Stage stage = new Stage();
stage.performPlay()j
stage.change();
stage.performPlay();
>
} /* Output:
HappyActor
SadActor
*///:~
class Useful {
public void f() {>
public void g() {>
}
class MoreUseful extends Useful {
public void f() {}
public void g() {}
public void u() {}
public void v() {>
public void w() {}
}
public class RTTI {
public static void main(Stning[] args) {
Резюме 261
Useful[] x = {
new Useful(),
new MoreUseful()
>;
x[0].f();
x[l].g();
// Стадия компиляции: метод не найден в классе Useful:
//! x[l].u();
((MoreUseful)x[l]).u()j // Нисходящее преобразование /RTTI
((MoreUseful)x[0]).u(); // Происходит исключение
>
} ///:~
Класс MoreUseful расширяет интерфейс класса Useful. Но благодаря наследованию он
также может быть преобразован к типу Useful. Вы видите, как это происходит, при
инициализации массива x в методе main(). Так как оба объекта в массиве являются
производными от Useful, вы можете послать сообщения (вызвать методы) f() и g()
для обоих объектов, но при попытке вызова метода u() (который существует только
в классе MoreUseful) вы получите сообщение об ошибке компиляции.
Чтобы получить доступ к расширенному интерфейсу объекта MoreUseful, используйте
нисходящее преобразование. Если тип указан правильно, все пройдет успешно; иначе
произойдет исключение ClassCastException. Вам не понадобится писатьдополнитель-
ный код для этого исключения, поскольку оно указывает на общую ошибку, которая
может произойти в любом месте программы. Тег комментария {ThrowsException} со
общает системе построения кода, использованной в книге, что при выполнении про
граммы следует ожидать возникновения исключения.
Впрочем, RTTI не сводится к простой проверке преобразований. Например, можно
узнать, с каким типом вы имеете дело, прежде чем проводить нисходящее преобразо
вание. Глава 11 полностью посвящена изучению различных аспектов динамического
определения THnoeJava.
18. (2) Используя иерархию Cycle из упражнения 1, включите метод balance() в классы
Unicycle и Bicycle, но не в Tricycle. Создайте экземпляры всех трех типов и выпол
ните их восходящее преобразование в массив Cycle. Попробуйте вызвать balance( )
для каждого элемента массива. Теперь выполните нисходящее преобразование,
вызовите balance() и проанализируйте результат.
Резюме
Полиморфизм означает «многообразие форм». В объектно-ориентированном про
граммировании базовый класс предоставляет общий интерфейс, а различные версии
динамически связываемых методов —разные формы использования интерфейса.
Как было показано в этой главе, невозможно понять или создать примеры с ис
пользованием полиморфизма, не прибегая к абстракции данных и наследованию.
Полиморфизм — это возможность языка, которая не может рассматриваться изо
лированно; она работает только согласованно, как часть «общей картины» взаимо
отношений классов.
262 Глава8 • Полиморфизм
a b stra ct In stru m e n t
e x t ln d s e x te n d s ex te n d s
_______ ii_______ _______| _______
W ind Percussion Strln g ed
7Y
1
ext<mds ex te n d s
Интерфейсы
Ключевое слово interface становится следующим шагом на пути к абстракции. Клю
чевое слово abstract позволяет создать в классе один или несколько неопределенных
Интерфейсы 267
in terfa ce In stru m e n t \
void p la y (); ,
S trin g w h a t(); |
void a d ju st();
5 1
im p lq m e n ts im p le m e n ts m p te ^ e n ts
in te r fa c e In strum ent {
// Константа времени компиляции:
in t VALUE = 5; // является и s ta tic , и fin a l
// Определения м ето д ов недопустимы:
v o id p la y (N o te n); // А втом атически объявлен как p u b lic
v o id a d ju st();
}
c la s s W in d im p le m e n ts In strum ent {
p u b lic v o id p la y (N o te n) {
p rin t( th is + " .p la y ( ) " + n);
>
p u b lic S trin g to S trin g () { retu rn "W in d "; >
p u b lic v o id a d ju st() { p rin t( th is + ’’ . a d j u s t ( ) " ) ; >
>
c la s s P e rc u s s io n im p le m e n ts In strum ent {
p u b lic v o id p la y (N o te n) {
p rin t( th is + " .p la y ( ) " + n);
>
p u b lic S trin g to S trin g () { retu rn "P e rc u ssio n "; }
p u b lic v o id ad ju st() { p rin t( th is + " .a d ju s t()" ); }
>
c la s s S trin g e d im p le m e n ts In strum ent {
p u b lic v o id p la y (N o te n) {
p rin t( th is + " .p la y ( ) " + n);
}
p u b lic S trin g to S trin g () { return "S trin g e d "; }
p u b lic v o id ad ju st() { p rin t( th is + ’’ . a d j u s t ( ) ” ) ; }
>
c la s s B rass exten ds W in d {
p u b lic S trin g to S trin g () { retu rn "B rass”; >
>
Интерфейсы 269
В этой версии присутствует еще одно изменение: метод what() был заменен на
toString(). Так как метод toString() входит в корневой класс Object, его присутствие
в интерфейсе не обязательно.
Остальной код работает так же, как прежде. Неважно, проводите ли вы преобразование
к «обычному» классу с именем Instrument, к абстрактному классу с именем Instrument
или к интерфейсу с именем Instrument —действие будет одинаковым. В методе tune()
ничто не указывает на то, является ли класс Instrument «обычным» или абстрактным,
или это вообще не класс, а интерфейс.
5. (2) Создайте интерфейс, содержащий три метода, в отдельном пакете. Реализуйте
этот интерфейс в другом пакете.
6. (2) Докажите, что все методы интерфейса автоматически являются открытыми
(public).
7. (1) Измените упражнение 9 из главы 8 так, чтобы тип Rodent был оформлен как
интерфейс.
8 . (2) В программе Sandwich.java из главы 8 создайте интерфейс с именем FastFood
(с подходящими методами); измените класс Sandwich так, чтобы он реализовал
этот интерфейс.
270 ГлаваЭ • Интерфейсы
class Processor {
public String name() {
return getClass() . getSimpleName();
>
Object process(Object input) { return input; >
>
class Upcase extends Processor {
String process(Object input) { // Ковариантный возвращаемый тип
return ((String)input).toUpperCase();
>
}
class Downcase extends Processor {
String process(Object input) {
return ((String)input).toLowerCase();
>
>
class S p litte r extends Processor {
String process(Object input) {
// Аргумент s p lit ( ) используется для разбиения строки
return A rra y s.to S trin g (((S trin g )in p u t).sp lit(" "));
>
>
public class Apply {
Отделение интерфейса от реализации 271
package intenfaces.filters;
//: interfaces/interfaceprocessor/Processor.java
package interfaces.interfaceprocessor;
//: interfaces/interfaceprocessor/FilterProcessor.java
package interfaces.interfaceprocessor;
import interfaces.filters.*j
При наследовании базовый класс вовсе не обязан быть абстрактным или «конкрет
ным» (без абстрактных методов). Если наследование действительно осуществляется
не от интерфейса, то среди прямых «предков» класс может быть только один — все
остальные должны быть интерфейсами. Имена интерфейсов перечисляются вслед
за ключевым словом implements и разделяются запятыми. Интерфейсов может быть
сколько угодно, причем к ним можно проводить восходящее преобразование. Сле
дующий пример показывает, как создать новый класс на основе конкретного класса
и нескольких интерфейсов:
//: interfaces/Adventure.java
// Использование нескольких интерфейсов.
interface CanFight {
void fight();
}
interface CanSwim {
void swim();
}
interface CanFly {
void fly()j
>
class ActionCharacter {
public void fight() {}
>
class Hero extends ActionCharacter
implements CanFight, CanSwim, CanFly {
public void swim() {}
public void fly() {>
>
public class Adventure {
public static void t(CanFight x) { x.fight(); }
public static void u(CanSwim x) { x.swim(); >
public static void v(CanFly x) { x.fly(); }
public static void w(ActionCharacter x) { x.fight(); }
public static void main(String[] args) {
Hero h = new Hero();
t(h); // Используем объект в качестве типа CanFight
u(h); // Используем объект в качестве типа CanSwim
v(h); // Используем объект в качестве типа CanFly
w(h); // Используем объект в качестве ActionCharacter
>
> ffl:~
276 Глава 9 • Интерфейсы
interface Monster {
void menace();
>
Goeazimom
Raeuuacio
Nuoadesiw
Hageaikux
Ruqicibui
Numasetih
Kuuuuozog
Waqizeyoy
*///:~
Интерфейс Readable требует только присутствия метода read(). Метод read() либо
добавляет данные в аргумент CharBuffer (это можно сделать несколькими способами;
обращайтеськдокументации CharBuffer), либо возвращает-1 при отсутствии входных
данных.
Допустим, у нас имеется класс, не реализующий интерфейс Readable, — как заставить
его работать с Scanner? Перед вами пример класса, генерирующего вещественные числа:
//: interfaces/RandomDoubles.java
import java.util.*;
Так как интерфейсы можно добавлять подобным образом только к существующим клас
сам, это означает, что любой класс может быть адаптирован для метода, получающего
интерфейс. В этом проявляется одно из преимуществ интерфейсов перед классами.
16. (3) Создайте класс, который генерирует серию char. Адаптируйте его так, чтобы он
мог использоваться для передачи данных Scanner.
Поля в интерфейсах
Так как все поля, помещаемые в интерфейсе, автоматически являются статическими
(static) и неизменными (final), объявление interface хорошо подходитдля создания
групп постоянных значений. До вы хода^уа SE5 только так можно было имитировать
перечисляемый тип enum из языков С и С++. Это выглядело примерно так:
//: interfaces/Months.java
// Использование интерфейсов для создания групп констант,
package interfaces;
//: interfaces/RandVals.java
// Инициализация полей интерфейсов
// не-константными выражениями,
import java.util.*;
Вложенные интерфейсы
Интерфейсы могут вкладываться в классы и в другие интерфейсы1. При этом обнару
живается несколько весьма интересных особенностей:
//: interfaces/nesting/NestingInterfaces.java
package interfaces.nesting;
class А {
interface В {
void f();
>
public class BImp implements В {
public void f() {>
>
private class BImp2 implements В {
public void f() {>
>
public interface С {
void f();
}
class CImp implements С {
public void f() {>
>
private class CImp2 implements С {
public void f() {>
>
private interface 0 {
void f()j
>
private class DImp implements D {
public void f() {>
>
public class DImp2 implements D {
public void f() {}
}
public D getD() { return new DImp2(); >
private D dRef;
public void receiveD(D d) {
dRef = d;
dRef.f();
>
interface E {
interface G {
void f();
}
// Избыточное объявление public:
public interface H {
void f();
}
void g();
// Не может быть private внутри интерфейса:
//! private interface I {>
}
public class NestingXnterfaces {
public class BImp implements A.B {
public void f() {>
>
class CImp implements A.C {
public void f() {}
>
// Нельзя реализовать private-интерфейс, кроме как
// внутри класса, где он был определен:
//! class DImp implements A.D {
f / \ public void f() {>
//! >
class Elmp implements E {
public void g() {>
}
class EGImp implements E.G {
public void f() {>
}
class EImp2 implements E {
public void g() {>
продолжение #
284 Глава 9 • Интерфейсы
Интерфейсы и фабрики
На концептуальном уровне интерфейс представляет собой «шлюз», ведущий к разным
реализациям, а типичным механизмом создания объектов, реализующих интерфейс,
является паттерн проектирования «Фабричный метод». Вместо прямого вызова
конструктора вызывается метод объекта-фабрики, который создает реализацию ин
терфейса — таким образом код теоретически полностью изолируется от реализации
интерфейса, что позволяет прозрачно заменять одну реализацию другой. Следующая
программадемонстрирует структуру паттерна «Фабричный метод»:
//: interfaces/Factories.java
import static net.mindview.util.Print.*;
interface Service {
void methodl();
void method2();
>
interface ServiceFactory {
Service getService();
>
class Implementationl implements Service {
Implementationl() {> // Доступ в пределах пакета
public void methodl() {print("Implementationl methodl");}
public void method2() {print("Implementationl method2'');}
}
class ImplementationlFactory implements ServiceFactory {
public Service getService() {
return new Implementationl();
>
>
class Implementation2 implements Service {
Implementation2() {> // Доступ в пределах пакета
public void methodl() {print("Implementation2 methodl");}
public void method2() {print("Implementation2 method2");}
}
class Implementation2Factory implements ServiceFactory {
public Service getService() {
return new lmplementation2();
}
}
public class Factories {
public static void serviceConsumer(ServiceFactory fact) {
Service s = fact.getService();
s.methodl();
s.method2();
}
продолжение ^>
286 Глава9 • Интерфейсы
>
Резюме 287
Если класс Games представляет сложный блок кода, этот механизм позволяет повторно
использовать этот код для разных типов игр. Нетрудно представить более сложные
игры, которые могут извлечь пользу из этого паттерна. В следующей главе будет пред
ставлен более элегантный способ реализации фабрик на базе анонимных внутренних
классов.
18. (2) Создайте интерфейс Cycle с реализациями Unicycle, Bicycle й Tricycle. Создайте
фабрикудля каждой разновидности Cycle и код, использующий эти фабрики.
19. (3) Создайте на базе паттерна «Фабричный метод» программную среду, моделиру
ющую броски монет и броски кубиков.
Резюме
После знакомства с интерфейсами возникает соблазнительная мысль: интерфейсы —это
хорошо, и их всегда стоит использовать вместо конкретных классов. Конечно, прак
тически везде, где создается класс, вместо него можно создать интерфейс и фабрику.
Многие программисты поддались этому искушению и стали создавать интерфейсы
и фабрики там, где это возможно. Не имея полной уверенности в том, когда может
потребоваться другая реализация, они всегда добавляли эту абстракцию. Интерфейсы
стали своего рода скороспелой оптимизацией.
Любая абстракция должна базироваться на реальных потребностях. Интерфейсы от
носятся к числу аспектов системы, которые вводятся по мере необходимости в резуль
тате рефакторинга, а не являются лишним логическим уровнем, добавляемым сплошь
и рядом со всей сопутствующей сложностью. Лишняя сложность весьма существенна,
и если вдруг становится ясно, что она появилась вследствие добавления интерфейсов
«на всякий случай», а не по реальной необходимости —я начну сомневаться во всех
решениях из области проектирования, принятых этим человеком.
В общем случае рекомендуется отдавать предпочтение классам перед интерфейсами.
Начните с классов, и если вдруг станет ясно, что в данной ситуации необходимы ин
терфейсы, — проведите рефакторинг. Интерфейсы — замечательный инструмент, но
и им можно злоупотреблять.
Внутренние классы
Внутренний класс, используемый внутри метода ship(), выглядит так же, как и все
остальные классы. Очевидное отличие одно —имена классов вложены в класс Parcell.
Вскоре вы увидите, что это не единственное отличие.
Как правило, внешний класс содержит метод, возвращающий ссылку на внутренний
класс, как в методах to() и contents() в следующем примере:
//: innerclasses/Parcel2.java
// Возврат ссылки на внутренний класс.
Contents с = contents();
Destination d = to(dest);
System.out.println(d.readLabel());
>
public static void main(String[] args) {
Parcel2 p = new Parcel2();
p .ship("TaH3aHwa")j
Parcel2 q = new Parcel2();
// Определение ссылок на внутренние классы:
Parcel2.Contents с = q.contents();
Parcel2.Destination d = q.to("5opHeo");
>
> /* Output:
Танзания
* ///:-
Если вам понадобится создать объект внутреннего класса где-либо еще, кроме как в не
статическом методе внешнего класса, нужно будет указать тип этого объекта следующим
образом: ИмяВнешнегоКласса.ИмяВнутреннегоКласса, что и делается в методе main () .
1. ( 1) Напишите класс с именем 0uter, содержащий внутренний класс с именем lnner.
Добавьте в 0uter метод, возвращающий объект типа Inner. В методе main() создайте
и инициализируйте ссылку на lnner.
interface Selector {
boolean end();
Object current();
void next();
>
public class Sequence {
private Object[] items;
private int next * 0;
public Sequence(int size) { items = new Object[size]; >
public void add(Object x) {
if(next < items.length)
items[next++] * x;
}
1 Этот подход сильно отличается от присущего С++, где вложенные классы —просто механизм
для скрытия имен. Вложенные классы С++ не имеют связи с объектом-оболочкой и соответ
ствующих прав на доступ к его элементам.
Создание внутренних классов 291
Kiracc Sequence — это просто «обертка» для массива с элементами Object, имеющего
фиксированный размер. Для добавления нового объекта в конец последовательности
используется метод add() (при наличии свободного места в массиве). Для выборки
каждого объекта в последовательности Sequence предусмотрен интерфейс с именем
Selector; это пример использования паттерна «Итератор», который будет описан позже.
Интерфейс Selector позволяет узнать, достигнут ли конец последовательности (метод
end()), обратиться к текущему объекту (метод current()) и передвинуться к следующему
объекту последовательности (метод next()). Так как Selector является интерфейсом,
многие другие классы вправе реализовать его по-своему, и многие методы могут брать
его в качестве параметра, чтобы получить единообразный код.
Здесь SequenceSelector является закрытым (private) классом, предоставляющим функ
циональность интерфейса Selector. В методе main( ) вы можете наблюдать за процессом
создания последовательности, с последующим заполнением ее объектами String. Затем
объект Selector создается вызовом selector() для получения интерфейса Selector, ко
торый используется для перемещения по последовательности и выбора ее элементов.
На первый взгляд создание класса SequenceSelector похоже на создание обычного вну
треннего класса. Но рассмотрите его повнимательнее. Заметьте, что каждый из методов:
и end(), и current(), и next( ) —обращается к ссылке items, которая не является частью
класса SequenceSelector, а относится к закрытому (private) полю внешнего класса.
Впрочем, внутренний класс может обращаться ко всем полям и методам внешнего
класса, как будто они описаны в нем самом. Такая возможность очень удобна, как на
глядно доказывает приведенный пример.
Итак, внутренний класс автоматически имеет доступ к членам внешнего класса. Как
же это происходит? Внутренний класс должен содержать ссылку на определенный
объект окружающего класса, ответственного за его создание. При обращении к члену
окружающего класса эта ссылка используется для вызова нужного члена. К счастью,
все технические подробности берет на себя компилятор, но теперь вы знаете, что
292 Глава 10 • Внутренние классы
.this и .new
Если вам потребуется получить ссылку на объект внешнего класса, укажите имя
внешнего класса с точкой и this. Полученная ссылка автоматически относится к пра
вильному типу, который известен и проверяется во время компиляции, не требуя
лишних затрат ресурсов во время выполнения. Следующий пример показывает, как
использовать конструкцию .this:
//: in n e rc la s s e s / D o tT h is .ja v a
// Д о с т у п к о б ъ е к т у внешнего к л а с с а .
p u b lic c la s s D o tT h is {
v o id f() { S y s te m .o u t.p r in tln ( " D o tT h is .f( ) " ) ; >
p u b lic c la s s Inner {
p u b lic D o tT h is outer() {
return D o tT h is .th is ;
// " th is " без уточнения соответствует объекту ln n e r
>
}
p u b lic Inner in n e r () { return new I n n e r ( ) ; }
p u b lic s ta tic v o id m a in (S trin g [] a rg s) {
D o tT h is dt = new D o t T h i s ( ) ;
D o tT h is .I n n e r d ti = d t.in n e r() j
d ti.o u te r().f();
}
} /* O u t p u t :
D o tT h is .f()
*///:~
Иногда в программе требуется приказать объекту создать объект одного из его вну
тренних классов. Для этого в выражение new включается ссылка на другой объект
внешнего класса с синтаксисом . new, как в следующем примере:
//: in n e rc la s s e s / D o tN e w .ja v a
// Прямое создание объекта внутреннего класса с синтаксисом .n e w .
p u b lic c la s s DotNew {
p u b lic c la s s I n n e r {>
Внутренние классы и восходящее преобразование 293
//: in n e rc la s s e s / P a r c e l3 .ja v a
// И с п о л ь з о в а н и е конструкции .new д л я создания экземпляров внутренних классов.
p u b lic c la s s P a rc e l3 {
c la s s C o nten ts {
p riv a te in t i = 11;
p u b lic in t v a lu e () { return i; }
}
c la s s D e s tin a tio n {
p riv a te S trin g la b e l;
D e s tin a tio n ( S trin g w hereTo) { la b e l = w hereTo; }
S trin g re a d L a b e l() { return la b e l; >
}
p u b lic s ta tic v o id m a in (S trin g [] arg s) {
P a rc e l3 p = new P a r c e l 3 ( ) ;
// M u s t u s e in s ta n c e of outer c la s s
// t o create an in s ta n c e o f th e in n e r c la s s :
P a rc e l3 .C o n te n ts c = p .n e w C o n t e n t s ( ) ;
P a rc e l3 .D e s tin a tio n d = p .ne w D e s t in a t io n ( " T a H 3 a H H f l" ) ;
}
> ///:~
4 . (2) Добавьте в класс S e q u e n c e . S e q u e n c e S e l e c t o r метод для получения ссылки на
внешний класс S e q u e n c e .
5 . (1) Создайте класс с внутренним классом. В отдельном классе создайте экземпляр
внутреннего класса.
class Parcel4 {
private class PContents implements Contents {
private in t i = 11;
public in t value() { return i ; }
}
protected class PDestination implements Destination {
private String lab el;
private PDestination(String whereTo) {
label * whereTo;
>
public String readLabel() { return label; }
>
public Destination destination(String s) {
return new PDestination(s);
>
public Contents contents() {
return new PContents();
>
>
public class TestParcel {
public s ta tic void main(String[] args) {
Parcel4 p = new Parcel4();
Contents c = p.contents();
Destination d = p.destination("TaH3aHMH");
// Обращение к закрытому классу невозможно:
//! Parcel4.PContents pc = p.new PContents();
>
} / / / :~
В класс Parcel4 было добавлено кое-что новое: внутренний класс PContents является
закрьггым (объявлен как private), поэтому нигде, кроме как во внешнем для него классе
Внутренние классы в методах и областях действия 295
Parcel4, нельзя получить к нему доступ. Класс PDestination объявлен как protected,
следовательно, доступ к нему имеют только класс Parcel4, классы из одного пакета
с Parcel4 (так как спецификатор доступа protected также дает доступ в пределах своего
пакета) и наследники класса Parcel4. Отсюда вывод, что программист-клиент обладает
ограниченной информацией и доступом к этим членам класса. Вообще говоря, нельзя
даже провести нисходящее преобразование к закрытому (private) внутреннему классу
(или к защищенному (protected) внутреннему классу, кроме как из его наследника),
так как у вас нет доступа к его имени, что показано в классе TestParcel. Таким образом,
закрытый внутренний класс позволяет разработчику класса полностью запретить ис
пользование определенных типов и скрыть все детали реализации класса. Вдобавок
расширение интерфейса с точки зрения программиста-клиента не будет иметь смысла,
поскольку он не сможет получить доступ к дополнительным методам, не принадле
жащим к открытой части класса. Также у компилятора Java появится возм ож ность
оптимизировать код.
6. (2) Создайте интерфейс, содержащий хотя бы один метод, в отдельном пакете.
Создайте класс в другом пакете. Добавьте защищенный внутренний класс, реали
зующий интерфейс. В третьем пакете создайте производный класс; внутри метода
верните объект защищенного внутреннего класса, преобразованный в интерфейс.
7 . (2) Создайте класс, содержащий закрытое поле и закрытый метод. Создайте вну
тренний класс с методом, который изменяет поле внешнего класса и вызывает метод
внешнего класса. Во втором методе внешнего класса создайте объект внутреннего
класса и вызовите его метод; продемонстрируйте эффект на объекте внешнего класса.
8. (2) Проверьте, доступны ли для внешнего класса закрытые элементы внутреннего
класса.
TrackingSlip(String s) {
id = s;
}
String getSlip() { return id; }
>
TrackingSlip ts = new TrackingSlip("slip");
String s = ts.getSlip();
>
// Здесь использовать нельзя! Вне области действия:
//! TrackingSlip ts = new TrackingSlip("x");
>
public void track() { internalTracking(true); }
public static void main(String[] args) {
Parcel6 p = new Parcel6();
p.track();
>
} ///:~
Класс ТrackingSlip вложен в область действия команды if. Это не означает, что класс
создается условно, —он компилируется вместе с остальным кодом. Тем не менее этот
класс недоступен за пределами области действия, в которой он определяется. В осталь
ном он выглядит как самый обычный класс.
7 . (1) Создайте интерфейс, содержащий минимум один метод. Реализуйте его, опре
деляя внутренний класс в методе, который возвращает ссылку на ваш интерфейс.
8 . (1) Повторите предыдущее упражнение, но определите внутренний класс в области
действия внутри метода.
9 . (2) Создайте закрытый внутренний класс, реализующий открытый интерфейс. На
пишите метод, который возвращает ссылку на экземпляр закрытого внутреннего
класса, преобразованную к интерфейсу восходящим преобразованием. Чтобы по
казать, что внутренний класс полностью скрыт, попробуйте выполнить нисходящее
преобразование к нему.
public Wrapping(int x) { i = x; }
public int value() { return i; >
} ///: ~
Класс Wrapping содержит конструктор с аргументом, чтобы ситуация была чуть более
интересной.
Точка с запятой в конце анонимного внутреннего класса отмечает не конец тела класса,
а конец выражения, содержащего анонимный класс. Таким образом, данное использо
вание точки с запятой ничем не отличается от ее использования в любом другом месте.
Инициализация также может выполняться при определении полей в анонимном классе:
//: innerclasses/Parcel9.java
// Анонимный внутренний класс, выполняющий инициализацию
// (сокращенная версия Parcel5.java).
interface Service {
void methodl();
void method2();
interface ServiceFactory {
Service getService();
}
class Implementationl implements Service {
private Implementationl() {>
public void methodl() {print("Implementationl methodl");}
public void method2() {print("Implementationl method2");}
public static ServiceFactory factory =
new ServiceFactory() {
public Service getService() {
return new Implementationl();
>
>;
}
class Implementation2 implements Service {
private Implementation2() {>
public void methodl() {print("Implementation2 methodl");}
public void method2() {print("Implementation2 method2");}
public static ServiceFactory factory =
new ServiceFactory() {
public Service getService() {
return new Implementation2();
>
\ ^’ продолжение &
302 Глава 10 • Внутренние классы
}J
>
public class Games {
public static void playGame(GameFactory factory) {
Game s = factory.getGame();
while(s.move())
t
}
public static void main(String[] args) {
playGame(Checkers.factory);
playGame(Ches s .factory);
>
) /* Output:
Checkers move 0
Checkers move 1
Checkers move 2
Chess move 0
Chess move 1
Chess move 2
Chess move 3
*///:~
Вложенные классы
Если связь между объектом внутреннего класса и объектом внешнего класса вам
не нужна, внутренний класс можно сделать статическим (объявить его как sta tic ).
Часто такой класс называют вложенным1 (nested). Для того чтобы понять значение
ключевого слова s ta tic в отношении внутренних классов, вы должны вспомнить, что
объект обычного внутреннего класса скрыто хранит ссылку на объект создавшего его
объемлющего внешнего класса. Это условие не выполняется для внутренних классов,
объявленных с ключевым словом sta tic . Вложенный класс обладает следующими
характеристиками.
1. Для создания его объекта не понадобится объект внешнего класса.
2. Вы не можете обращаться к членам не-статического объекта внешнего класса из
объекта вложенного класса.
1Примерно то же самое, что и вложенные классы С++, за тем исключением, что в Java они
способны обращаться к закрытым членам внешнего класса.
304 Глава 10 • Внутренние классы
class MNA {
private void f() {}
class А {
private void g() {>
public class В {
void h() {
g();
f()j
}
>
}
>
public class MultiNestingAccess {
public static void main(String[] args) {
MNA mna = new MNA();
MNA.A mnaa = mna.new A();
MNA.A.B mnaab = mnaa.new B();
mnaab.h();
>
> ///:~
Вы можете видеть, что в классе MNA.A.B методы f() и g( ) вызываются без дополнитель
ных уточнений (несмотря на то, что они объявлены как private). Этот пример также
демонстрирует синтаксис, который следует использовать при создании объектов вну
тренних классов произвольного уровня вложенности из другого класса. Выражение
. new дает верную область действия, и вам не приходится уточнять его полное имя при
вызове конструктора.
Обычно внутренний класс наследует от класса или реализует интерфейс, а код внутрен
него класса манипулирует объектом внешнего класса, в котором он был создан. Значит,
можно сказать, что внутренний класс —это нечто вроде «окна» во внешний класс.
Возникает естественный вопрос: «Если мне нужна ссылка на интерфейс, почему бы
внешнему классу не реализовать этот интерфейс?» Ответ здесь таков: «Если это все,
что вам нужно, —значит, так и следует поступить». Но что же отличает внутренний
класс, реализующий интерфейс, от внешнего класса, реализующего тот же интерфейс?
Далеко не всегда удается использовать удобство интерфейсов — иногда приходится
работать и с реализацией. Поэтому наиболее веская причина для использования вну
тренних классов формулируется так:
Каждый внутренний класс способен независимо наследовать определенную реализа
цию. Таким образом, внутренний класс не ограничен при наследовании в ситуациях, где
внешний класс уже наследует реализацию.
Без способности внутренних классов наследовать (фактически) реализацию более
чем одного конкретного или абстрактного класса некоторые задачи планирования
и программирования имели бы крайне сложное решение. Поэтому внутренний класс
выступает как «довесок» множественного наследования. Интерфейсы берут на себя
часть этой задачи, в то время как внутренние классы фактически обеспечивают «мно
жественное наследование реализации». То есть внутренние классы позволяют вам
наследовать от нескольких «не-интерфейсов».
Чтобы понять сказанное, рассмотрим ситуацию, где два интерфейса тем или иным
способом должны быть реализованы в классе. Вследствие гибкости интерфейсов у вас
есть два варианта: одиночный класс или внутренний класс:
//: innerclasses/MultiInter*faces.java
// Два способа реализации нескольких интерфейсов
// в одном классе.
interface А {}
interface В {}
class Y implements А {
В makeB() {
// Анонимный внутренний класс:
return new B() {};
}
>
public class Multiinterfaces {
static void takesA(A а) {>
static void takesB(B b) {}
public static void main(String[] args) {
X x = new X();
Y у = new Y()j
takesA(x)j
takesA(y);
takesB(x ) ;
takesB(y.makeB());
>
> ///:~
308 Глава 10 • Внутренние классы
Конечно, выбор того или иного способа организации кода зависит от конкретной
ситуации. Впрочем, сама решаемая вами задача должна подсказать, что для нее пред
почтительно: один отдельный класс или внутренний класс. Но при отсутствии иных
ограничений оба подхода, использованные в рассмотренном примере, ничем не от
личаются с точки зрения реализации. Оба они работают.
Однако, если вместо интерфейсов у вас имеются конкретные или абстрактные классы,
придется «звать на помощь» внутренние классы, если новый класс должен как-то за
действовать функциональность двух других классов:
//: innerclasses/MultiImplementation.java
// При использовании конкретных или абстрактных классов
// внутренние классы предоставляют единственный способ
// провести "множественное наследование реализации".
package innerclasses;
class D {}
abstract class E {}
class Z extends D {
E makeE() { return new E() {>; >
>
public class Multiimplementation {
static void takesD(D d) {>
static void takesE(E e) {>
public static void main(String[] args) {
Z z = new Z()j
takesD(z);
takesE(z.makeE());
}
> / / / :~
Если вам не приходится решать задачу «множественного наследования реализации»,
скорее всего, вы сможете написать любую программу без использования особенностей
внутренних классов. С другой стороны, внутренние классы открывают следующие
дополнительные возможности.
1. У внутреннего класса может существовать произвольное количество экземпляров,
каждый из которых содержит собственную информацию, не зависящую от состо
яния объекта внешнего класса.
2. Один внешний класс может содержать несколько внутренних классов, которые
по-разному реализуют один интерфейс или наследуют от единственного базового
класса. Пример такой конструкции вскоре будет рассмотрен.
3. Место создания объекта внутреннего класса не привязано к месту и времени соз
дания объекта внешнего класса.
4. Внутренний класс не создает взаимосвязи классов типа «является тем-то», способ
ной вызвать путаницу; он представляет собой отдельную сущность.
Например, если в программе Sequence.java отсутствовали бы внутренние классы, то
нам пришлось бы заявить, что «класс Sequence есть класс Selector», и при этом огра
ничиться только одним объектом Selector для конкретного объекта Sequence. А вы
Внутренние классы: зачем? 309
interface Incrementable {
void increment();
>
// ПРОСТО реализуем интерфейс:
class Calleel implements Incrementable {
private int i = 0;
public void increment() {
i++; продолжение ■&
310 Глава 10 • Внутренние классы
System,out.println(i)j
>
}
class MyIncrement {
public void increment() { System.out.println("flpyran операция"); }
public static void f(MyIncrement mi) { mi.increment(); )
>
// Если ваш класс должен вызывать метод increment()
// по-другому> необходимо использовать внутренний класс:
class Callee2 extends MyIncrement {
private int i = 0;
private void increment() {
super.increment();
i++;
print(i);
}
private class Closure implements Incrementable {
public void increment() {
// Указывается метод внешнего класса> иначе
// возникнет бесконечная рекурсия:
Callee2.this.increment();
>
>
Incrementable getCallbackReference() {
return new Closure();
>
}
class Caller {
private Incrementable callbackReference;
Caller(Increraentable cbh) { callbackReference = cbh; }
void go() { callbackReference.increment(); }
>
public class Callbacks {
public static void main(String[] args) {
Calleel cl = new Calleel();
Callee2 c2 = new Callee2();
MyIncrement.f(c2);
Caller callerl = new Caller(cl);
Caller caller2 = new Caller(c2.getCallbackReference());
callerl.go();
callerl.go();
caller2.go();
caller2.go();
>
) /* Output:
Другая операция
1
1
2
Другая операция
2
Другая операция
3
*///:~
Внутренние классы: зачем? 311
Этот пример также показывает, какая существует разница при реализации интерфей
са внешним или внутренним классом. Класс Calleel —наиболее очевидное решение
задачи с точки зрения написания программы. Класс Callee2 наследует от класса
MyIncrement, в котором уже есть метод increment(), выполняющий действие, никак не
связанное с тем, что ожидает от него интерфейс lncrementable. Когда класс MyIncrement
расширяется для получения Callee2, метод increment() нельзя переопределить для
использования в качестве метода интерфейса lncrementable, поэтому приходится об
ратиться к отдельной реализации во внутреннем классе. Также следует отметить, что
создание внутреннего класса не затрагивает и не изменяет существующий интерфейс
внешнего класса.
Обратите внимание: все элементы, за исключением метода getCallbackReference(),
в классе Callee2 являются закрытыми. Для того чтобы установить любое соединение
с окружающим миром, необходим интерфейс lncrementable. Здесь можно видеть, как
интерфейсы способствуют полному отделению интерфейса от реализации.
Внутренний класс Closure просто реализует интерфейс lncrementable, предоставляя
при этом связь с объектом Callee2, но связь эта безопасна. Кто бы ни получил ссылку
на lncrementable, он в состоянии вызвать только метод increment(), и других возмож
ностей у него нет (в отличие от указателя, с которым программист может вытворять
все, что вздумается).
Класс Caller берет ссылку на lncrementable в своем конструкторе (хотя передача ссылки
для обратного вызова может происходить в любое время), а после этого, чуть позже,
использует ссылку для «обратного вызова» объекта Callee.
Ценность обратного вызова кроется в его гибкости —вы можете динамически выбирать
функции, выполняемые во время работы программы. Практическая выгода такого
подхода станет более очевидной в главе 14, равномерно насыщенной обратными вы
зовами для реализации графического интерфейса пользователя (GUI).
знать, что метод add() присоединяет объект Event к концу контейнера List, метод size()
возвращает количество хранимых в контейнере элементов, конструкция foreach по
следовательно перебирает объекты Event из List, а метод remove() удаляет элемент из
заданной позиции списка:
//: innerclasses/controller/Controller.java
// Вместе с классом Event составляет систему
// управления общего характера:
import java.util.*;
1По некоторым причинам я всегда решал эту задачу с особым удовольствием; это началось еще
с одной из первых моих книг С++ Inside & Out, noJava-реализация —гораздо более элегантная.
314 Глава 10 • Внутренние классы
Заметьте, что поля light, water и thermostat принадлежат внешнему классу Greenhouse-
Controls, и все же внутренние классы имеют возможность обращаться к ним, не ис
пользуя особой записи и не запрашивая особых разрешений. Также в методы action()
обычно включается код управления оборудованием.
Большая часть событий Event выглядит схоже, однако классы Bell и Restart пред
ставляют собой особые случаи. Bell выдает звуковой сигнал, а затем добавляет новый
объект Bell в список ожидания событий, чтобы звонок сработал снова. Заметьте, что
внутренние классы действуют почти как множественное наследование: классы Bell
и Restart содержат все методы класса Event, а также выглядят так, словно они содержат
все методы внешнего raraccaGreenhouseControls.
Классу Restart передается массив событий Event, которые он добавляет в контроллер.
Так как Restart также является объектом Event, вы можете добавить этот объект в спи
сок событий в методе Restart.action(), чтобы система регулярно перезапускалась.
Следующий класс настраивает систему, создавая объект GreenhouseControls и добавляя
в него разнообразные типы объектов Event. Это пример паттерна проектирования «Ко
манда» —каждый объект в eventList представляет запрос, инкапсулирующий объект:
//: innerclasses/GreenhouseController.java
// Настройка и запуск системы управления.
// {Args: 5000}
import innerclasses.controller.*;
class WithInner {
class Inner {}
>
public class InheritInner extends WithInner.Inner {
//! InheritInner() {} // Не компилируется
InheritInner(WithInner wi) {
wi.super();
>
public static void main(String[] args) {
WithInner wi = new WithInner();
InheritInner ii = new InheritInner(wi);
}
} ///:~
318 Глава 10 • Внутренние классы
cldss Egg {
private Yolk у;
protected class Yolk {
public Yolk() { print("Egg.Yolk(D; }
>
public Egg() {
print("New Egg()");
у = new Yolk();
>
>
public class BigEgg extends Egg {
public class Yolk {
public Yolk() { print("BigEgg.Yolk()"); >
>
public static void main(String[] args) {
new BigEgg();
>
) /* Output:
New Egg()
Egg.Yolk()
*///:~
Этот пример просто показывает, что при наследовании от внешнего класса никаких
дополнительных действий для внутренних классов не производится. Два внутренних
класса — совершенно отдельные составляющие, с независимыми пространствами
имен. Впрочем, сохранилась возможность явно унаследовать от внутреннего класса:
//: innerclasses/BigEgg2.java
// Правильное наследование внутреннего класса.
import static net.mindview.util.Print.*;
class Egg2 {
protected class Yolk {
public Yolk() { print("Egg2.YolkQ"); }
public void f() { print("Egg2.Yolk.f()");}
}
private Yolk у = new Yolk();
public Egg2() { print("New Egg2()")j >
public void insertYolk(Yolk yy) { у = yy; >
public void g() { y.f()j }
>
public class BigEgg2 extends Egg2 {
public class Yolk extends Egg2.Yolk {
public Yolk() { print(''BigEgg2.Yolk()"); }
public void f() { print("BigEgg2.Yolk.f()"); >
}
public BigEgg2() { insertYolk(new Yolk())j >
public static void main(String[] args) {
Egg2 e2 = new BigEgg2();
e2.g()j
>
> /* Output:
Egg2.Yolk()
New Egg2()
Egg2.Yolk()
BigEgg2.Yolk()
BigEgg2.Yolk.f()
*///:~
Теперь класс BigEgg2.Yolk явно расширяет класс Egg2.Yolk и переопределяет его мето
ды. Метод insertYolk( ) позволяет классу BigEgg2 передать один из своих собственных
объектов Yolk в класс Egg2, где тот присоединяется к ссылке у, поэтому, когда в мето
де g() вызывается метод y.f(), используется переопределенная версия f(). Второй
вызов Egg2.Yolk() —это запуск конструктора базового класса из конструктора класса
BigEgg2.Yolk. Вы также можете обнаружить, что при вызове метода g ( ) использовалась
переопределенная версия метода g().
//: innerclasses/LocalInnenClass.java
// Хранит последовательность объектов,
import static net.mindview.util.Print.*;
interface Counter {
int next();
}
public class LocalInnerClass {
private int count = 0;
Counter getCounter(final String name) {
// Локальный внутренний класс:
class LocalCounter implements Counter {
public LocalCounter() {
// У локального внутреннего класса
// может быть собственный конструктор:
pririt(”LocalCounter()");
>
public int next() {
printb(name); // Неизменный аргумент
return count++;
>
}
return new LocalCounter();
}
// То же с анонимным внутренним классом:
Counter getCounter2(final String name) {
return new Counter() {
// Анонимный внутренний класс не может содержать
// именованного конструктора, только инициализатор экземпляра
{
print("Counter()");
>
public int next() {
printnb(name); // Неизменный аргумент
return count++;
>
>;
>
public static void main(String[] args) {
LocalInnerClass lic = new LocalInnerClass();
Counter
cl = lic.getCounter('^OKanbHbift "),
c2 = lic.getCounter2("AHOHHMHb^ ");
for(int i = 0; i < 5; i++)
print(cl.next());
for(int i = 0; i < 5; i++)
print(c2.next());
>
} /* Output:
LocalCounter()
Counter()
Локальный 0
Локальный 1
Локальный 2
Локальный 3
Локальный 4
Можно ли переопределить внутренний класс? 321
Анонимный 5
Анонимный 6
Анонимный 7
Анонимный 8
Анонимный 9
* / / / :~
1 С другой стороны, символ $ является управляющим в системе UNIX, и при просмотре фай
лов .class в этой ОС могут возникнуть проблемы. Это довольно странно, ведь компания Sun
(разработч ик J ava) в основном использует систему UNIX. Я полагаю, что они вообще не об
ратили внимания на этот аспект, предполагая, что в основном внимание программиста будет
привлечено к файлам с исходными текстами программы.
322 Глава 10 • Внутренние классы
Резюме
Интерфейсы и внутренние классы —весьма утонченные понятия, и во многих других
объектно-ориентированных языках вы их не найдете. Например, в С++ нет ничего
похожего. Вместе они решают те задачи, которые С++ пытается осилить, используя
множественное наследование. Однако множественное наследование С++ создает
массу проблем, по сравнению с ним интерфейсы и внутренние классы Java гораздо
более доступны.
Хотя рассмотренные конструкции довольно просты, необходимость вовлечения их
в проектирование программ сначала не совсем очевидна, так же, как и в случае с по
лиморфизмом. По прошествии определенного времени вы научитесь сразу оценивать,
где большую выгоду даст интерфейс, где внутренний класс, а где нужны обе возмож
ности сразу. Но на данном этапе достаточно ознакомиться хотя бы с их синтаксисом
и семантикой. Со временем, когда вы увидите примеры практического применения,
вы привыкнете к этим возможностям языка и только будете удивляться, как раньше
обходились без них.
Коллекции объектов
class Apple {
private static long counter;
private final long id = counter++;
public long id() { return id; )
>
1 В некоторых языках —таких, как Perl, Python и Ruby, —реализована встроенная поддержка
контейнеров.
2 Здесь безусловно пригодилась бы перегрузка операторов. Классы контейнеров С++ и C#
предоставляют более элегантный синтаксис за счет использования перегрузки операторов.
Обобщенные типы и классы, безопасные по отношению к типам 325
class Orange {}
GrannySmith@7d772e
Gala@llbS6e7
Fuji@35ce36
Braeburn0757aef
*/l/:~
Таким образом, в контейнер для объектов Apple можно поместить объект одного из
субтипов Apple.
Выходные данные были получены от реализации метода toString() класса Object,
которая выводит имя класса с последующим шестнадцатеричным числом без знака —
представлением хеш-кода объекта (сгенерированным методом hashCode()). Хеш-коды
более подробно рассматриваются в главе 17.
1. (2) Создайте новый класс с именем Gerbil с полем gerbilNumber типа int, инициали
зируемым в конструкторе. Определите в нем метод hopQ, который выводит значение
gerbilNumber и короткое сообщение. Создайте контейнер ArrayList и добавьте в него
объекты Gerbil. Используйте метод get() для перебора элементов и вызова hop()
для каждого объекта Gerbil.
Основные концепции
Библиотека контейнеров Java решает вопрос «хранения ваших объектов», рассма
тривая его как совокупность двух различных концепций, выраженных основными
интерфейсами библиотеки:
□ Коллекция (Collection): последовательность отдельных элементов, формируемая
по некоторым правилам. Интерфейс List (список) хранит элементы в определенной
последовательности, а в интерфейсе Set (множество) нельзя хранить повторяющиеся
элементы. Интерфейс Queue (очередь) выдает элементы в порядке, определяемом
дисциплиной очереди (обычно совпадающем с порядком вставки).
□ Карта (мар): набор пар объектов «ключ-значение», с возможностью выборки зна
чения по ключу. Контейнер ArrayList позволяет получить объект по числу, так что
он в каком-то смысле связывает числа с объектами. Карта позволяет получить
объект по другому объекту. Также часто встречается термин ассоциативный массив
(потому что объекты ассоциируются с другими объектами) и словарь (потому что
объект-значение ищется по объекту-ключу, по аналогии с поиском определения
по слову). Интерфейсы, производные от Мар, — мощный и полезный инструмент.
В идеале большая часть кода должна взаимодействовать с этими интерфейсами (хотя
это не всегда возможно), а точный тип указывается только в точке создания контейнера.
Таким образом, команда создания контейнера List может выглядеть так:
List<Apple> apples « new ArrayList<Apple>()j
Так как в этом примере используются только методы Collection, подойдет объект
любого класса, производного от Collection, но самым распространенным типом по
следовательности является ArrayList.
Имя add() создает впечатление, что метод добавляет новый элемент в коллекцию.
Однако в документации используется тщательно выбранная формулировка: add()
«...убеждается в том, что в Collection присутствует заданный элемент». Эта формули
ровка подразумевает функциональность множества (Set), для которого элемент добав-
ляется только в том случае, если он еще не присутствует в контейнере. Для ArrayList
или любой разновидности List вызов add() всегда эквивалентен добавлению, потому
что объекты List не проверяют наличие дубликатов.
Для перебора всех контейнеров Collection может применяться синтаксис foreach,
как в приведенном примере. Позднее в этой главе будет представлена более гибкая
концепция итераторов.
2. (1) Измените пример SimpleCollection.java так, чтобы в качестве контейнера с исполь
зовалось множество (Set).
3. (2) Измените пример innerclasses/Sequence.java так, чтобы в контейнер можно было
добавить произвольное количество элементов.
Добавление групп элементов 329
//: holding/AsListInference.java
// Arrays.asList() пытается угадать тип.
import java.util.*;
class Snow {}
class Powder extends Snow {}
class Light extends Powder {}
class Heavy extends Powder {}
class Crusty extends Snow {>
classSlush extends Snow {>
// Не откомпилируется:
// List<Snow> snow2 = Arrays.asList(
// new Light(), new Heavy());
// Компилятор сообщает:
// получено : java.util.List<Powder>
// требуется : java.util.List<Snow>
// У Collections.addAll() проблем нет:
List<Snow> snow3 = new ArrayList<Snow>();
Collections.addAll(snow3, new Light(), new Heavy())j
Вывод контейнеров
Для создания печатного представления массива необходимо использовать A r r a y s .
toString(), но контейнеры запросто выводятся без посторонней помощи. Следующий
пример также демонстрирует использование основных типов контейнеров:
//: holding/PrintingContainers.java
// Контейнеры распечатываются автоматически,
import java.util.*;
import static net.mindview.util.Print.*;
Добавление групп элементов 331
Как уже было упомянуто, в библиотеке контейнеров Java существует две категории.
Разница между ними основана на том, сколько в одной «ячейке» контейнера помещается
элементов. Коллекции (Collection) содержат только один элемент в каждой ячейке
(имя немного не соответствует их реальному предназначению, иногда «коллекциями»
называют сами библиотеки контейнеров). К коллекциям относятся список (List), где
в определенной последовательности хранится группа элементов; множество (Set),
в которое можно добавлять только по одному элементу определенного типа; и очередь
(Queue), позволяющая вставлять объекты с одного «конца» контейнера и извлекать их
с другого «конца» (в контексте данного примера очередь представляет собой лишь
другую разновидность последовательности, поэтому она не представлена). У карты
(Мар) в каждой ячейке хранятся два объекта: ключ и связанное с ним значение.
4. (3) Создайте класс-генератор, который при каждом вызове next() выдает имена
персонажей вашего любимого фильма в виде объектов String. Когда список за
канчивается, программа снова возвращается к началу. Используйте генератор для
заполнения массива и контейнеров ArrayList, LinkedList, HashSet, LinkedHashSet
и TreeSet, после чего выведите содержимое каждого контейнера.
List 333
List
Контейнер List гарантирует хранение списка элементов в определенной последователь
ности. Интерфейс List добавляет в Collection методы вставки и удаления элементов
в середине списка.
Существуют две основные разновидности List:
Строки выходных данных пронумерованы, чтобы вывод можно было связать с исход
ным кодом. В первой строке приведен исходный контейнер объектов Pet. В отличие
от массива контейнер List позволяет добавлять и удалять элементы после создания,
а также изменяет свои размеры. Это очень принципиальный момент: последователь
ность может изменяться. Результат добавления Hamster продемонстрирован в главе 2:
объект добавился в конец списка.
Метод contains() проверяет, присутствует ли объект в списке. Чтобы удалить объект,
передайте ссылку на него методу remove( ). Кроме того, если у вас имеется ссылка на
объект, вы можете определить индекс объекта в List при помощи метода indexOf()
(см. строку 4).
При принятии решения о том, присутствует ли элемент в List, определении индекса
элемента и удалении элемента из List по ссылке используется метод equals() (часть
корневого ^ a cc aO bj ec t) . Объекты Pet определяются как уникальные, и хотя в списке
уже присутствуют два объекта Cymric, если я создам новый объект Cymric и передам его
indexOf(), результат будет равен - 1 (объект не найден), а попытка вызвать remove() для
этого объекта вернет false. Для других классов метод equals() может быть определен
иначе —объекты String, например, считаются равными, если два объекта String име
ют одинаковое содержимое. Таким образом, чтобы избежать неприятных сюрпризов,
важно помнить, что поведение List изменяется в зависимости от поведения equals().
В строках вывода 7 и 8 удаление объекта, точно совпадающего с объектом в контейнере
List, выполняется успешно.
Итераторы
При использовании любого класса контейнера должен быть способ помещать в него
что-либо и получать это что-либо обратно. Помимо прочего, именно хранение объек
тов —задача номер одиндля контейнера. В контейнере List для добавления объектов
может использоваться метод add(), а для их получения —метод get().
Но если взглянуть на происходящее на более высоком уровне, обнаруживается одна
проблема: чтобы начать что-то делать с контейнером, необходимо знать его точный тип.
Сначала может показаться, что это не так уж плохо, но что если вы начали использовать
в программе контейнер List, а затем обнаружили, что в вашем случае гораздо более
эффективным будет множество (Set)? Или предположим, что вы хотели бы написать
код общего вида, который не зависит от типа контейнера, с которым он работает, так,
чтобы он был применим к любому контейнеру?
Итераторы 337
1 Метод remove() принадлежит к числу так называемых «необязательных» методов (есть идру-
гие); это означает, что он не обязан быть реализованным всеми реализациями Iterator. Эта
тема рассматривается в главе 17. Впрочем, контейнеры стандартной библиотеки^уа реализуют
remove(), поэтому пока вам не нужно беспокоиться о нем.
Итераторы 339
11. (2) Напишите метод, который использует Iterator для перебора Collection и вы
водит результат вызова toString() для каждого объекта в контейнере. Заполните
объектами разные типы Collection и примените свой метод к каждому контейнеру.
ListIterator .
Интерфейс ListIterator представляет собой более мощную разновидность Iterator,
которая работает только с классами List. Хотя Iterator может перемещаться только
вперед, итератор ListIterator является двусторонним. Он может выдать индексы
следующего и предыдущего элементов относительно текущей позиции итератора
в списке, а также заменить последний посещенный элемент методом set(). Метод
listlterator() возвращает объект ListIterator, указывающий в начало List, а вызов
listIterator(n) создает объект ListIterator, изначально установленный в позицию
списка с индексом n. Следующий пример демонстрирует эти возможности:
//: holding/ListIteration.java
import typeinfo.pets.*;
import java.util.*;
Метод P e ts.randomPet() используется для замены всех объектов Pet в контейнере List
начиная с позиции 3.
12. (3) Создайте и заполните контейнер List<integer>. Создайте второй контейнер
List<lnteger> того же размера. Используйте итераторы Listlterator для чтения
элементов из первого контейнера List и вставки их во второй контейнер в обратном
порядке. (Проанализируйте несклько разных способов решения этой задачи.)
LinkedList
Класс LinkedList, как и ArrayList, реализует базовый интерфейс List, но при этом
выполняет некоторые операции (вставка и удаление в середине списка) более эффек
тивно, чем ArrayList. И наоборот, с операциями произвольного доступа он работает
менее эффективно.
В LinkedList также добавляются методы, которые позволяют использовать его как
стек, очередь или двустороннюю очередь (дек).
Некоторые из этих методов представляют собой синонимы или небольшие видоиз
менения для создания имен, более знакомых в контексте конкретного применения
(прежде всего Queue). Например, методы getFirst() и element() идентичны — они
возвращают начало (первый элемент) списка без его удаления и выдают исключение
NoSuchElementException, если список пуст. Метод peek() — вариация на тему этихдвух
методов, которая возвращает null для пустого списка.
Метод addFirst() вставляет элемент в начало списка.
Метод offer() делает то же, что add() и addLast(). Все эти методы добавляют элемент
в конец списка.
Метод removeLast() удаляет и возвращает последний элемент списка.
Следующий пример демонстрирует основные сходства и различия между этими опе
рациями. Он не повторяет поведение, представленное в примере ListFeatures.java:
//: holding/LinkedListFeatures.java
import typeinfo.pets.*;
import java.util.*;
import static net.mindview.util.Print.*;
Стек
Стек часто называют контейнером, построенным на принципе «первый вошел, по
следний вышел» (LIFO). То есть что бы вы ни поместили (push) в стек в последнюю
очередь, это будет первым, что вы получите при «выталкивании» (pop) элемента из
стека. Стек нередко сравнивают со стопкой тарелок —тарелка, положенная в стопку
последней, будет первой снята с нее.
В классе LinkedList имеются методы, напрямую реализующие функциональность
стека, поэтому вы просто используете LinkedList, не создавая для стека новый класс.
Впрочем, иногда отдельный класс для контейнера-стека решит задачу лучше:
342 Глава 11 • Коллекции объектов
//: net/mindview/util/Stack.java
// Создание стека на основе LinkedList.
package net.mindview.util;
import java.util.LinkedList;
net.mindview.util.Stack<String> stack =
new net.mindview.util.Stack<Stning>();
for(String s : "My dog has fleas".split(" "))
stack.push(s);
while(!stack.empty())
System.out.print(stack.pop() + ” ");
System.out.println();
java.util.Stack<String> stack2 =
new java.util.Stack<String>Q;
for(String s : "My dog has fleas".split(""))
stack2.push(s);
while(!stack2.empty())
System.out.print(stack2.pop() + '* ");
>
> /* Output:
fleas has dog My
fleas has dog My
*///:~
Два класса Stack обладают одинаковым интерфейсом, но в java.util нет общего интер
фейса Stack —вероятно потому, что исходный, плохо спроектированный класс java.
util.StackH3java 1.0 «захватил» этоимя. И хотя java.util.StackcyuiecTByeT, LinkedList
предоставляет более качественную реализацию стека, поэтому решение net.mindview.
util.Stack является предпочтительным.
Теперь при любой ссылке на Stack будет выбираться версия net.mindview.util, а для
выбора java.util.Stack необходимо использовать полностью уточненное имя.
15. (4) Стеки часто используются для вычисления выражений в языках программи
рования. Используя реализацию net.mindview.util.Stack, вычислите результат
следующего выражения, в котором «+» означает «занести следующую букву в стек»,
а «-» — «извлечь верхний элемент стека и вывести его».
+U+n+c— +e+r+t— +a-+i-+n+t+y— + -+r+u--+l+e+s —
Множество
Контейнер Set сохраняет не более одного экземпляра каждого объекта-значения. При
попытке добавить более одного экземпляра эквивалентного объекта Set предотвращает
появление дубликата. Чаще всего set используется для тестирования принадлежности,
чтобы пользователь мог легко узнать, присутствует ли объект в множестве. По этой при
чине поиск по ключу обычно является самой важной операцией для Set, и разработчик
чаще всего выбирает реализацию HashSet, оптимизированную по скорости поиска по ключу.
Setобладает таким же интерфейсом, как Collection, поэтому в Set нетдополнительной
функциональности, присутствующей в двух разновидностях List. Вместо этого Set
представляет собой разновидность Collection —просто обладает другим поведением.
(Идеальная ситуация для применения наследования и полиморфизма: выражение
344 Глава 11 • Коллекции объектов
//: holding/SetOpenations.java
import java.util.*;
import static net.mindview.util.Print.*;
Мар
Возможность отображения объектов на другие объекты может оказаться чрезвычайно
полезной при решении задач программирования. Для примера рассмотрим программу,
которая проверяет «случайность» чисел, вырабатываемых классом для производства
случайных чисел Random. В идеале этот метод должен равномерно распределять сге
нерированные числа, но для проверки необходимо создать набор случайных чисел
и подсчитать количество чисел в разных диапазонах. Контейнер Мар легко решает эту
задачу; в качестве ключа используется сгенерированное число, а в качестве значения —
количество сгенерированных экземпляров этого числа):
//: holding/Statistics.java
// Простой пример использования HashMap.
import java.util.*;
Map 347
import typeinfo.pets.*;
import java.util.*;
import static net.mindview.util.Print.*;
Контейнер Map может вернуть множество (Set) своих ключей, коллекцию (Collection)
своих значений или множество (Set) их пар. Метод keySet() возвращает контейнер
Set, содержащий все ключи из petPeople, который используется в команде foreach для
перебора элементов мар.
17. (2) Возьмите класс Gerbil из упражнения 1 и поместите его в контейнер Мар. Ис
пользуйте объект String, содержащий имя каждого объекта Getbil, в качестве ключа
для связывания с объектом Gerbil (значение), помещаемым в таблицу. Получите
Iterator для keySet() и используйте его для перемещения по Map, с выборкой объ
екта Gerbil для каждого ключа, выводом ключа и вызовом метода hop().
18. (3) Заполните контейнер HashMap парами «ключ-значение». Выведите результаты,
чтобы продемонстрировать упорядочение по хеш-коду. Извлеките пары, отсорти
руйте по ключу и поместите результат в LinkedHashMap. Покажите, что элементы
хранятся в порядке вставки.
19. (2) Повторите предыдыщее упражнение с HashSet и LinkedHashSet.
20. (3) Измените упражнение 16 так, чтобы в контейнере хранилось количество вхож
дений каждой гласной.
21. (3) Используя контейнер Map<string,Integer>, создайте по образцу UniqueWords.
java программу для подсчета вхождений слов в файле. Отсортируйте результаты
методом Collections.sort() со вторым аргументом String.CASE_lNSENSlTlVE_ORDER
(для получения алфавитной сортировки) и выведите результат.
22. (5) Измените предыдущее упражнение так, чтобы для хранения слов в нем ис
пользовался класс с объектом String и полем счетчика. Для хранения списка слов
должен использоваться контейнер Set, содержащий такие объекты.
23. (4) Взяв за отправную точку программу Statistics.java, создайте программу, которая
циклически повторяет этот тест, проверяя, не появляется ли какое-либо из полу
ченных случайных чисел чаще других.
24. (2) Заполните карту LinkedHashMap строковыми ключами и такими же значениями,
взятыми по вашему усмотрению. После этого извлеките пары, отсортируйте их по
ключам и заново вставьте в карту.
25. (3) Создайте контейнер Map<String,ArrayList<Integer>>. Используя net.mindview.
TextFile, откройте текстовый файл и прочитайте его по словам (передайте "\\w+" во
втором аргументе конструктора TextFile). Подсчитывайте словавпроцессе чтения;
для каждого слова в файле сохраните в ArrayList<lnteger> счетчик слов, связанный
с этим словом (то есть фактически позицию файла, в которой было обнаружено
данное слово).
350 Г л а в а П • Коллекцииобъектов
Очередь
Очередь (queue) —это контейнер, работающий по принципу «первый вошел, первый
вышел» (FIFO). Иначе говоря, объекты помещаются в один «конец» очереди, а извле
каются с другого «конца». Таким образом, порядок занесения объектов в контейнер
будет совпадать с порядком их извлечения оттуда. Соответственно, очереди часто
используются для надежного перемещения объектов из одной области программы
в другую. Очереди также играют важную роль в параллельном программировании,
как будет показано в главе 21, потому что они обеспечивают безопасную передачу
объектов между задачами.
Класс LinkedList содержит несколько методов для поддержки поведения очередей
и реализует интерфейс Queue; соответственно, LinkedList может использоваться как
реализация Queue. Восходящее преобразование LinkedList в Queue позволяет исполь
зовать в этом примере методы, специфические для Queue:
//: holding/QueueDemo.java
// Восходящее преобразование Queue в LinkedList.
import java.util.*;
PriorityQueue
Термин FIFO описывает самую типичную дисциплину очереди — правило, которое
для группы элементов очереди определяет, какой элемент будет извлечен следующим.
Согласно принципу FIFO, следующим должен извлекаться элемент, который дольше
всех ожидает в очереди.
В приоритетной очереди PriorityQueue следующим извлекается элемент, обладающий
наивысшим приоритетом. Например, в аэропорту клиент может быть обслужен вне
очереди, если его самолет готовится к вылету. В системе передачи сообщений некото
рые сообщения могут содержать более важную информацию; они должны быть срочно
обработаны независимо от времени поступления. Контейнеры PriorityQueue были
добавлены B j a v a SE5 для автоматической реализации такого поведения.
При вызове offer() для объекта, помещаемого в PriorityQueue, позиция этого объ
екта в очереди определяется сортировкой1. Сортировка по умолчанию использует
естественный порядок следования объектов в очереди, но вы можете изменить его,
предоставив собственную реализацию Comparator. PriorityQueue гарантирует, что при
вызове peek(), poll() или remove() будет получен элемент с наивысшим приоритетом.
Создать приоритетную очередь для работы со встроенными типами (Integer, String,
Character и т. д.) совсем несложно. В следующем примере первая группа значений
идентична случайным значениям из предыдущего примера; как видно из приведенного
результата, они извлекаются из PriorityQueue в другом порядке:
//: holding/PriorityQueueDemo.java
import java.util.*;
' Вообще говоря, это зависит от реализации. Алгоритмы приоритетных очередей обычно вы
полняют сортировку при вставке, но также возможна выборка наиболее приоритетного эле
мента при извлечении. Выбор алгоритма может быть важен, если приоритеты объектов могут
изменяться во время нахождения в очереди.
352 Глава 11 • Коллекции объектов
Collection и Iterator
—корневой интерфейс, описывающий общие аспекты всех последовательных
C o lle c tio n
контейнеров. Считайте, что это своего рода «случайный интерфейс», появившийся из-за
общности между другими интерфейсами. Кроме того, класс j a v a . u t i l . A b s t r a c t C o l l e c -
t i o n предоставляет реализацию C o l l e c t i o n по умолчанию, так что вы можете создать
новый подтип A b s t r a c t C o l l e c t i o n без избыточного дублирования кода.
Один из доводов в пользу существования такого интерфейса заключается в том, что он
позволяет создавать более универсальный код. Код, написанный для интерфейса, а не
для реализации, может применяться к большему количеству объектов1. Итак, метод,
получающий C o l l e c t i o n , может быть применен к любому типу, реализующему C o l l e c
t i o n , а это позволяет разработчику нового класса реализовать C o l l e c t i o n , чтобы класс
мог использоваться с моим методом. Интересно, что в стандартной библиотеке С++
нет общего базового класса для всех контейнеров; все сходство между контейнерами
обеспечивается итераторами. По аналогичному пути можно было пойти и Bjava —так,
чтобы сходство между контейнерами выражалось итератором, а не C o l l e c t i o n . Однако
эти два подхода тесно связаны, а реализация C o l l e c t i o n также означает поддержку
метода i t e r a t o r ( ) :
//: h o ld in g / I n t e r f a c e V s I t e r a t o r . ja v a
im p o rt t y p e i n f o . p e t s . * ;
im p o rt j a v a . u t i l . * ;
продолжение &
c la s s PetSequence {
p ro tected P et[] p ets = P e ts .c re a te A rra y (8 );
>
p u b lic c l a s s N o n C o lle c tio n S e q u e n c e ex ten ds P etSequence {
p u b lic Iterato r< P et> it e r a t o r ( ) {
r e t u r n new I t e r a t o r < P e t > ( ) {
p r iv a t e i n t in d e x = 0;
p u b l i c b o o le a n h a s N e x t() {
r e t u r n in d e x < p e t s .le n g t h ;
>
p u b lic Pet next() { return p e ts [in d e x + + ]; >
p u b lic v o id rem ove() { // N o t im p le m e n te d
t h r o w new U n s u p p o r t e d O p e r a t i o n E x c e p t i o n ( ) ;
>
>;
>
продолжение &
356 Глава 11 • Коллекции объектов
Foreach и итераторы
До настоящего момента синтаксис foreach использовался в основном с массивами, но
он также работает для любого объекта C o lle ctio n . Мы уже рассмотрели несколько при
меров его использования с A rra y L ist, но здесь приводится более общее доказательство:
//: holding/ForEachCollections.java
// Все коллекции работают с foreach.
import ja v a .u t il.* ;
Так как cs является C o lle ctio n , этот пример показывает, что конструкция foreach может
использоваться для всех объектов C o lle c tio n .
Почему это решение работает? B Java SE появился новый интерфейс it e r a b le , кото
рый содержит метод it e r a t o r ( ) для получения объекта Ite ra to r; именно интерфейс
Iterab le используется foreach для перемещения по последовательности. Таким образом,
если вы создадите любой класс, реализующий Iterab le , его можно будет использовать
в команде foreach:
//: holding/IterableClass.java
// Любая реализация Iterable работает с foreach.
import ja v a .u t il.* ;
1 До BbixoAaJava SE5 этот метод был недоступен; считалось, что он слишком тесно связан
с операционной системой, а следовательно, нарушает принцип «написано один раз, работает
везде». Факт его включения наводит на мысль, что проектировщики Java становятся более
прагматичными.
358 Глава 11 • Коллекции объектов
Идиома «Метод-Адаптер»
А если имеется существующий класс, который реализует I t e r a b l e , и вы хотите до
бавить новые способы использования этого класса в комнаде f o r e a c h ? Предположим,
вы хотите иметь возможность выбора между перебором списка слов в прямом или
обратном направлении. Если просто создать производный класс и переопределить
метод i t e r a t o r < ) , вы замените существующий метод, и выбора не будет.
Одно из решений основано на том, что я называю «идиомой Метод-Адаптер». «Адап
тер» в названии происходит от паттернов проектирования, потому что для команды
f o r e a c h необходимо предоставить конкретный интерфейс. Если у вас имеется один
интерфейс, а нужен другой, проблема решается написанием адаптера. В данном случае
я хочу добавить возможность получения обратного итератора к прямому итератору по
умолчанию, поэтому обычное переопределение не подходит. Вместо этого мы добавим
метод, создающий объект I t e r a b l e , который может использоваться в команде f o r e a c h .
Как видно из следующего примера, это позволяет нам предоставить разные способы
использования f o r e a c h :
//: h o ld in g / A d a p te rM e th o d Id io m .ja v a
/ / Идиома "М етод-Адаптер" позволяет использовать fo re a c h
// с другими разновидностями I te ra b le .
im p o rt j a v a . u t i l . * ;
L is t< ln te g e r> l i s t 2 = A r r a y s . a s L i s t ( i a ) ;
S y s t e m . o u t . p r i n t l n ( " f l o перемешивания: " + l i s t 2 ) ;
C o lle c tio n s . s h u ffle ( lis t 2 , ra n d );
S y s te m .o u t.p rin tln (" n o c A e перемеш ивания: " + lis t2 ) ;
S y s te m .o u t.p rin tln ("M a c c H B : " + A rra y s .to S trin g ( ia ) );
>
> /* O u t p u t :
До п е р е м е ш и в а н и я : [1, 2, 3, 4, 5, 6} 7, 8, 9, 10]
Резюме 361
П о сл е п ер ем еш и в ан и я: [4, 6, 3, 1, 8, 7, 2, 5, 10, 9]
массив: [1, 2, 3, 4, 5, 6, 7, 8, 9, 1 0]
До пер е м е ш и в а н и я : [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
П о сл е п ер ем еш и в ан и я: [9, 1, 6, 3, 7, 2, 5, 10, 4, 8]
массив: [9, 1, 6, 3, 7, 2, 5, 10, 4, 8]
*///:~
Резюме
В Java предусмотрены следующие способы хранения объектов.
1. Массив ассоциирует с объектом числовой индекс. Он содержит объекты известного
типа, так что при поиске объекта нет необходимости проводить преобразование.
Он может иметь несколько измерений и способен хранить примитивы, но после
создания его размер изменить невозможно.
2. Коллекция ( C o l l e c t i o n ) заполняется одиночными элементами, в то время как карта
( М а р ) хранит ассоциированные пары «ключ-значение». При использовании обоб
щенных типов Java разработчик указывает тип объекта, хранимого в контейнере,
так что поместить объект неправильного типа ему не удастся, а с извлекаемыми
объектами не нужно выполнять преобразование типа. И C o l l e c t i o n , и Мар автомати
чески изменяют свои размеры с добавлением новых элементов. Контейнер не может
хранить примитивы, но механизм автоматической упаковки обеспечит необходимые
преобразования к типам-«оберткам», хранимым в контейнере.
3. Подобно массиву, список ( L i s t ) также ассоциирует с объектами числовые индек
сы —массивы и списки можно представить себе как упорядоченные контейнеры.
4. Используйте список A r r a y L i s t , если имеется необходимость частого получения
произвольных элементов, или L i n k e d L i s t , если будет производиться множество
вставок и удалений элементов в середине списка.
5. Возможности очередей и стеков предоставляет класс L in k e d L is t.
p u b lic c la s s C o n ta in e rM e th o d s {
p u b lic s ta tic v o id m a in (S trin g [] args) {
C o n ta in e rM e th o d D iffe re n c e s .m a in ( a rg s );
}
Резюме 363
} /* O u t p u t : (пример)
C o lle c tio n : [add, a d d A ll, c le a r, c o n ta in s , c o n ta in s A ll,
e q u a ls , hashCode, is E m p ty , ite ra to r, rem ove, re m o v e A ll,
re ta in A ll, s iz e , toA rray]
In te rfa ce s in C o lle c tio n : [I te r a b le ]
S et exten ds C o l l e c t i o n , adds: []
In te rfa ce s in Set: [C o lle c tio n ]
HashSet e x te n d s S e t , adds: []
I n te rfa c e s in H ashSet: [ S e t, C lo n e a b le , S e ria liz a b le ]
L in k e d H a s h S e t e x te n d s H a sh S e t, adds: []
In te rfa ce s in L in k e d H a sh S e t: [S et, C lo n e a b le , S e ria liz a b le ]
T re e S e t exten ds S e t , adds: [ p o l l L a s t , n a v ig a b le H e a d S e t,
d e s c e n d in g I t e r a t o r , lo w e r, headSet, c e i l i n g , p o l l F i r s t ,
subSet, n a v ig a b le T a ilS e t, com parator, fir s t, flo o r, la s t,
n a v ig a b le S u b S e t, h ig h e r , t a i l S e t ]
I n t e r fa c e s in T re e S e t: [ N a v ig a b le S e t, C lo n e a b le ,
S e ria liz a b le ]
L i s t exten ds C o l l e c t i o n , adds: [lis tI te ra to r, in d e x O f, g et,
s u b L ist, set, la s tI n d e x O f]
In te rfa ce s in L is t: [C o lle c tio n ]
A rra y L is t exten ds L is t, adds: [e n s u re C a p a c ity , trim T o S iz e ]
In te rfa ce s in A rra y L is t: [L is t, Random Access, C lo n e a b le ,
S e ria liz a b le ]
L in k e d L is t exten ds L is t, adds: [p o llL a s t, o ffe r,
d e s c e n d in g Ite ra to r, a d d F irs t, p eekLast, re m o v e F irst,
p e e k F ir s t, rem o veLast, g etLast, p o llF ir s t, pop, p o ll,
a dd Last, re m o v e F irstO ccu rre n ce , g e tF irs t, e le m e n t, peek,
o ffe rL a st, push, o ffe rF irs t, rem oveLastO ccu rrence]
I n te rfa c e s in L in k e d L is t: [L is t, Deque, C lo n e a b le ,
S e r ia liz a b le ]
Queue e x t e n d s C o l l e c t i o n , adds: [o ffe r, e le m e n t, peek,
p o ll]
In te rfa ce s in Queue: [ C o lle c tio n ]
P rio rity Q u e u e e x te n d s Queue, adds: [co m p arato r]
In te rfa ce s in P rio rity Q u e u e : [ S e ria liz a b le ]
Map: [ c le a r, c o n ta in s K e y , c o n ta in s V a lu e , en tryS et, e q u a ls ,
g e t, hashCode, is E m p ty , keyS et, put, p u tA ll, rem ove, s iz e ,
v a lu e s]
HashMap e x t e n d s Map, adds: []
In te rfa ce s in H ashM ap: [M ap, C lo n e a b le , S e ria liz a b le ]
L in k e d H a s h M a p e x t e n d s H a sh M a p , adds: []
In te rfa ce s in L in k e d H a s h M a p : [Map]
S o r t e d M a p e x t e n d s Map, adds: [subMap, com parator, fir s tK e y ,
la s tK e y , headMap, ta ilM a p ]
In te rfa ce s in SortedM ap: [Map]
T re e M a p e x t e n d s M ap, adds: [d e s c e n d in g E n try S e t, su b M a p ,
p o llL a s tE n try , la s tK e y , flo o rE n try , la s tE n try , lo w e rK e y ,
n a v ig a b le H e a d M a p , n a v ig a b le T a ilM a p , d e sce n d in g K e y S e t,
ta ilM a p , c e i l i n g E n t r y , h ig h e rK e y , p o l l F i r s t E n t r y ,
com parator, f i r s t K e y , f lo o r K e y , h ig h e r E n t r y , f i r s t E n t r y ,
n a v ig a b le S u b M a p , hea d M a p , lo w e rE n try , c e ilin g K e y ]
In te rfa ce s in TreeM ap: [ N a v ig a b le M a p , C lo n e a b le ,
S e ria liz a b le ]
*///:~
Основные концепции
В С и нескольких других ранних языках существовали подобные механизмы, хотя они
обычно определялись как правила, которые должны были соблюдаться программистами,
366 Глава 12 • Обработка ошибок и исключения
Основные исключения
Исключительная ситуация возникает, когда невозможно нормальное продолжение
работы метода или части программы, выполняющихся в данный момент. Важно отли
чать исключительную ситуацию от «нормальной» ошибки, когда текущая обстановка
Джим Грей, обладатель премии Тьюринга за вклад в теорию транзакций, в своем интервью на
сайте www.acmqueue.org.
368 Глава 12 • Обработка ошибок и исключения
Аргументы исключения
Исключения, как и любые другие o 6 beK TbiJava, создаются в куче оператором new, ко
торый выделяет память и вызывает конструктор. Существует два конструктора для
всех стандартных исключений: конструктор по умолчанию и конструктор, который
принимает в качестве аргумента строку, где можно поместить подходящую информа
цию об исключении:
t h r o w new N u l l P o i n t e r E x c e p t i o n ( " t = n u ll" )j
Указанная строка потом может быть извлечена различными методами, о чем будет
рассказано позже.
Ключевое слово t h r o w влечет за собой ряд довольно интересных действий. Как прави
ло, сначала ключевое слово new используется для создания объекта, представляющего
условие происшедшей ошибки. Ссылка на указанный объект передается команде t h r o w .
И этот объект фактически «возвращается» методом, несмотря на то, что для возвра
щаемого объекта обычно предусмотрен совсем другой тип. Таким образом, упрощенно
можно говорить об обработке исключений как об альтернативном механизме возврата
из исполняемого метода, хотя заходить слишком далеко с этой аналогией не стоит. Вы
дача исключений также позволяет выходить из простых областей действия. В любом
случае, объект исключения возвращается, а исполнение текущего метода или области
действия завершается.
Но на этом сходство с обычным возвратом из метода и заканчивается, поскольку при
возврате с выдачей исключения управление передается совсем не в то место, куда оно
было бы возвращено при нормальном вызове. (Управление передается подходящему
обработчику исключения, который может находиться очень «далеко» —на расстоянии
нескольких уровней в стеке вызова — от метода, где, собственно, и возникла исклю
чительная ситуация.)
Вообще говоря, можно возбудить любой тип исключений, происходящих от объекта
T h r o w a b l e (корневой класс иерархии исключений). Обычно в программе возбуждают
ся разные типы исключений для разных типов ошибок. Информация о случившейся
ошибке содержится внутри объекта исключения, а также указывается косвенно в са
мом типе этого объекта, чтобы кто-то на более высоком уровне сумел выяснить, как
поступить с исключением. (Очень часто именно тип объекта исключения является
единственно доступной информацией об ошибке, в то время как внутри объекта не
содержится никакой содержательной информации.)
Перехват исключений 369
Перехват исключений
Чтобы увидеть, как перехватываются ошибки, сначала следует усвоить понятие
охраняемого участка, который представляет собой часть программы, где вероятно
появление исключений, и за которым следует специальный блок, отвечающий за об
работку этих исключений.
Блок try
Если вы «находитесь» внутри метода и возбуждаете исключение (или это сделает
другой вызванный вами метод), этот метод завершит работу при возникновении исклю
чения. Но если вы не хотите, чтобы оператор throw завершил работу метода, создайте
специальный блок внутри метода, в котором будет перехватываться исключение. В этом
блоке, который называется блоком try, программа вызывает различные методы. Этот
блок представляет собой простую область действия, введенную ключевым словом try:
try {
// Часть пр о гра м м ы , способная возбуж дать исклю чения
>
При тщательной проверке ошибок в языке программирования, который не поддержи
вает обработку исключений, вам бы пришлось добавить к вызову каждого метода, даже
одного и того же, дополнительный код для проверки ошибок. С обработкой исключе
ний все помещается в блок try, который и перехватывает все возможные исключения
в одном месте. А это означает, что вашу программу становится значительно легче
писать и читать, поскольку выполняемая задача не смешивается с обработкой ошибок.
Обработчики исключений
Конечно, возбужденное исключение должно где-то найти свое «пристанище». Этим
местом является обработчик исключений, организуемый для любого исключения, ко
торое вы хотите перехватить. Обработчики исключений размещаются прямо за блоком
try и вводятся ключевым словом catch:
try {
,// Ч а с т ь п р о гра м м ы , способная возбуж дать исклю чения
> catch(Typel id l) {
// П е р е х в а т искл ю ч ен ия T y p e l
> catch(Type2 id 2 ) {
// Перехват искл ю ч ен ия T y p e 2
} catch(Type3 id 3 ) {
// П е р е х в а т искл ю ч ен ия Т у р е З
>
// И т. д.
Для создания собственного класса исключения вам придется унаследовать его от уже
существующего типа, желательно того, который наиболее близок вашему по значению
(хоть это и не всегда возможно). Простейший путь — создать класс с конструктором
по умолчанию, что потребует от вас минимум работы:
//: e x c e p tio n s / I n h e r it in g E x c e p t io n s . ja v a
// С о з д а н и е с о б с т в е н н о г о исключения,
im p o rt com . b r u c e e c k e l . s i m p l e t e s t . * ;
Перехвачено L o g g in g E x c e p tio n
Aug 3 0 , 2005 4 : 0 2 : 3 1 PM L o g g i n g E x c e p t i o n < in it>
SE VERE : L o g g in g E x c e p tio n
at
L o g g in g E x c e p t io n s . m a in ( L o g g i n g E x c e p t i o n s . j a v a :2 4 )
Перехвачено L o g g in g E x c e p t io n
* / / / :~
p u b lic c la s s L o g g in g E x c e p tio n s 2 {
p riv a te s ta tic Logger lo g g e r =
L o g g e r.g e tL o g g e r(" L o g g in g E x c e p tio n s 2 " );
s ta tic v o id lo g E x c e p tio n (E x c e p tio n e) {
S trin g W rite r tr a c e = new S t r i n g W r i t e r ( ) ;
e .p rin tS ta c k T ra c e ( n e w P rin tW rite r(tra c e ));
lo g g e r. s e v e re (tra c e .t o S t r in g ( ) ) ;
>
p u b lic s ta tic v o id m a in ( S trin g [ ] args) {
try {
t h r o w new N u l l P o i n t e r E x c e p t i o n ( ) ;
> c a tc h (N u llP o in te rE x c e p tio n e) {
lo g E x c e p tio n ( e ) ;
>
}
> /* O u t p u t : (90% m a t c h )
Aug 3 0 л 2005 4 : 0 7 : 5 4 PM L o g g i n g E x c e p t i o n s 2 lo g E x c e p tio n
SEVERE: j a v a . la n g .N u llP o in t e r E x c e p t io n
at
L o g g i n g E x c e p t i o n s 2 . m a i n ( L o g g i n g E x c e p t i o n s 2 . j a v a : 16)
*///:~
В класс исключения добавлено поле x вместе с методом, который читает это значение,
и дополнительным конструктором, который его присваивает. Кроме того, метод T h r o w -
a b l e . g e t M e s s a g e ( ) был переопределен для получения более содержательного сообщения.
Спецификация исключений
В языке Java рекомендуется сообщать программисту, вызывающему ваш метод, об
исключениях, которые данный метод способен возбуждать. Так как пользователь, вы
зывающий метод, может написать весь необходимый код для перехвата возможных
исключений, это полезно. Конечно, когда доступен исходный код, программист-кли-
ент может пролистать его в поиске предложений throw, но часто случается так, что
библиотека поставляется без исходных текстов. Для решения этой проблемы в Java
добавили синтаксис (обязательный для использования), позволяющий вам любезно
сообщить потребителю метода об исключениях, возбуждаемых этим методом, чтобы
он сумел правильно обработать их. Этот синтаксис называется спецификацией исклю
чений (exception specification), является частью объявления метода и следует сразу за
списком аргументов.
Спецификация исключений предваряет дополнительное ключевое слово throws, за ко
торым перечисляются все возможные типы исключений. Таким образом, определение
метода выглядит примерно так:
v o id f() th ro w s T o o B ig , T o o S m a ll, D iv Z e ro { //...
Если вы пишете
v o id f() { //...
p u b lic c la s s E x c e p tio n M e th o d s {
p u b lic s ta tic v o id m a in ( S trin g [ ] arg s) {
try {
th ro w new E x c e p t i o n ( " M o e исклю чение");
) c a tc h ( E x c e p tio n e) {
p r i n t ( ' T l e p e x B a 4 eH 0 " ) ;
p rin t( " g e tM e s s a g e ( ) :" + e .g e tM e s s a g e ());
p rin t( ''g e tL o c a liz e d M e s s a g e ( ):" +
e .g e tL o c a liz e d M e s s a g e ( ) ) ;
p rin t( " to S tr in g ( ):" + e);
p r i n t ( " p r in t S t a c kTr a c e ( ) : " ) j
e . p rin tS ta c k T ra c e ( S y s te m .o u t ) ;
>
}
} /* O u t p u t :
Перехвачено
g e t M e s s a g e ( ) : Мое ис к л ю ч е н и е
g e tL o c a liz e d M e s s a g e ():M o e ис к л ю ч е н и е
to S trin g ():ja v a .la n g .E x c e p tio n : Мое ис кл ю ч ен и е
p rin tS ta c k T ra c e ( ):
ja v a .la n g .E x c e p tio n : Мое ис кл ю ч ен и е
at E x c e p t io n M e t h o d s . m a in ( E x c e p t io n M e t h o d s . j a v a :8 )
*///:~
Трассировка стека
К информации, предоставляемой методом p r i n t S t a c k T r a c e (), также можно обратиться
напрямую методом g e t S t a c k T r a c e (). Этот метод возвращает массив элементов трасси
ровки стека; каждый элемент представляет один кадр стека. Нулевой элемент нахо
дится на вершине стека и описывает последний вызов метода в последовательности.
Следующая программа демонстрирует пример данных трассировки:
//: e x c e p tio n s / W h o C a lle d .ja v a
// Программный д о с т у п к информации т р а с с и р о в к и стека.
p u b lic c la s s W h o C a lle d {
s ta tic v o id f< ) {
// Г е н е р и р у е м ис к л ю ч е н и е д л я з а п о л н е н и я т р а с с и р о в к и стека
try {
t h r o w new E x c e p t i o n ( ) ;
} catch (E x ce p tio n e) {
fo r(S ta c k T ra c e E le m e n t ste : e .g e tS ta c k T ra c e ())
S y s t e m . o u t . p r i n t l n ( s t e . getM ethodN am e( ) ) ;
>
>
s ta tic v o id g() { f(); >
s ta tic v o id h() { gQ ; >
p u b lic s ta tic v o id m a in ( S trin g [ ] arg s) {
Ю;
S y s t e m . o u t . p r i n t l n ( " -------------------------------------------------------- ” ) ;
g() •
S y s t e m . o u t . p r i n t l n ( " ...........- .............. .............. .................................." ) ;
h();
>
} /* O u t p u t :
f
K in
f
g
Kin
f
g
h
K in
*///:~
p u b lic c la s s R e th ro w in g {
p u b lic s ta tic v o id f() throw s E x c e p tio n {
S y s te m .o u t.p r in tln (" C o 3 fla H n e и скл ю ч ен ия в f()");
throw new E x c e p t i o n ( " B 0 3 6 y x A e H O из f Q " ) ;
В методе g ( ) , e . p r i n t S t a c k T r a c e ( )
ja v a .la n g .E x c e p tio n : возбуж дено из f()
at R e th ro w in g . f ( R e th ro w in g .j a v a :7 )
at R e th ro w in g .g (R e th ro w in g .ja v a :ll)
at R e th ro w in g .m a in (R e th ro w in g .ja v a :2 9 )
m a in : p rin tS ta c k T ra c e ()
ja v a .la n g .E x c e p tio n : возбуждено из f ( )
at R e t h r o w in g . f ( R e t h r o w in g . j a v a :7)
at R e th ro w in g .g (R e th ro w in g .ja v a :ll)
at R e t h r o w in g . m a in ( R e t h r o w in g . j a v a :2 9 )
С о з д а н и е искл ю ч ен ия в f ( )
В методе h ( ) , e . p r i n t S t a c k T r a c e ( )
ja v a .la n g .E x c e p tio n : возбуждено из f ( )
at R e th ro w in g .f(R e th ro w in g .ja v a :7 )
at R e th ro w in g .h (R e th ro w in g .ja v a :2 0 )
at R e t h r o w in g . m a in ( R e t h r o w in g . j a v a : 35)
m a in : p rin tS ta c k T ra c e ()
ja v a .la n g .E x c e p tio n : возбуждено из f ( )
at R e t h r o w in g . h ( R e t h r o w in g . j a v a :24)
at R e th ro w in g .m a in (R e th ro w in g .ja v a :3 5 )
*///:~
) c a tc h (T w o E x c e p tio n e) {
S y s te m .o u t.p rin tln (
"Перехвачено во внешнем б л о к е t r y , e .p rin tS ta c k T ra c e ( ) " ) j
e . p rin tS ta c k T ra c e ( S y s te m .o u t ) ;
>
>
> /* O u tp u t:
Создание и скл ю ч ен ия в f ( )
Перехвачено во в н у т р е н н е м б л о к е t r y , e .p rin tS ta c k T ra c e ( )
O n e E x ce p tio n : из f ( )
at R e t h r o w N e w . f ( R e t h rowNew. j a v a : 1 5 )
at R e t h rowNew. m a i n ( R e t h ro w N e w . j a v a : 2 0 )
Перехвачено во внешнем б л о к е t r y , e .p rin tS ta c k T ra c e ( )
T w o E x c e p tio n : из в н утр е н н е го блока t r y
at R e th ro w N e w .m a in (R e th ro w N e w .ja v a :2 5 )
*///:~
Цепочки исключений
Зачастую бывает нужно перехватить одно исключение и возбудить следующее, не теряя
при этом информации о первом исключении, — это называется цепочкой гижлючений
(exception chaining). До выпуска naKeTaJDK 1.4 программистам приходилось само
стоятельно писать код, сохраняющий информацию о предыдущем исключении, однако
теперь все подклассы T h r o w a b l e могут принимать в качестве аргумента конструктора
объект-причину. Под причиной подразумевается изначальное исключение, и передавая
ее в новый объект, вы поддерживаете трассировку стека вплоть до самого его начала,
даже если при этом создаете и возбуждаете новое исключение.
Интересно отметить, что единственными подклассами класса T h r o w a b l e , принимающими
объект-причину в качестве аргумента конструктора, являются три основополагающих
класса исключений: E r r o r (используется виртуальной машиной (JVM ) длясообщений
о системных ошибках), E x c e p t i o n и R u n t i m e E x c e p t i o n . Если вам понадобится организо
вать цепочку из других типов исключений, придется использовать метод i n i t C a u s e ( ) ,
а не конструктор.
В следующем примере используется динамическое добавление полей в объект
DynamicFields во время работы программы:
//: e x c e p tio n s / D y n a m ic F ie ld s . ja v a
// Д инамическое добавлен и е по л ей в класс.
// Д е м о н с т р и р у е т ц е п о ч к у и с к л ю ч е н и й ,
im p o rt s ta tic n e t.m in d v ie w .u til.P rin t.* ;
p u b lic c la s s D y n a m ic F ie ld s {
p riv a te O b je c t[][] fie ld s ;
Перехват любого типа исключения 383
dfe.initCause(new NullPointerException());
throw dfe;
}
int fieldNumber = hasField(id);
if(fieldNumber == -1)
fieldNumber = makeField(id);
Object result = nullj
try {
result = getField(id); // Получаем старое значение
} catch(NoSuchFieldException e) {
// Используем конструктор с "причиной":
throw new RuntimeException(e);
>
fields[fieldNumber][l] = value;
return result;
>
public static void main(String[] args) {
DynamicFields df = new DynamicFields(3);
print(df);
try {
df.setField("d", "Значение d");
df.setField("number"j 47);
df.setField("number2", 48);
print(df);
df.setField("d", "Новое значение d");
df.setField("number3"j 11);
print("df: " + df);
print("df.getField(\"d\") : " + df.getField("d"));
Object field = df.setField("d", null); // Исключение
} catch(NoSuchFieldException e) {
e.printStackTrace(System.out);
> catch(DynamicFieldsException e) {
e.printStackTrace(System.out);
>
>
} /* Output:
null: null
null: null
null: null
d: Значение d
number: 47
number2: 48
Было бы ужасно вот так проверять каждую ссылку, Передаваемую вашим методам (так
как вы не знаете, была ли передана верная ссылка). К счастью, вам не нужно этого
делать — это входит в стандартную проверку во время исполнения Java-программы,
и при попытке использования ссылки, содержащей null, автоматически возбуждается
NullPointerException. Таким образом, использованная в примере конструкция из
быточна (хотя, возможно, вам стоит выполнить другие проверки, предотвращающие
появление NullPointerException).
Есть целая группа исключений, принадлежащих к этой категории. Они всегда воз
буждаются в Java автоматически, и вам нет нужды включать их в спецификацию
исключений. Они удобно сгруппированы, поскольку все унаследованы от одного
базового класса RuntimeException. Это идеальный пример наследования: образова
ние семейства классов, имеющих общие характеристики и поведение. Вам также не
придется создавать спецификацию исключений, говорящую, что метод возбуждает
RuntimeException (или любое унаследованное от него исключение), так как эти ис
ключения относятся к неконтролируемым, (unchecked). Такие исключения означают
ошибки в программе, и фактически вам никогда не придется перехватывать их —это
делается автоматически. Если бы вам пришлось проверять возможность возбуждения
RuntimeException, программа стала бы слишком беспорядочной. И хотя обычно пере
хватывать RuntimeException He требуется, возможно, в своих собственных пакетах вы
сочтете целесообразным возбуждать некоторые из них.
Что же происходит, когда подобные исключения не перехватываются? Так как компи
лятор не заставляет включать спецификацию таких исключений, можно предположить,
что RuntimeException найдет способ проникнуть прямо в метод ma i n ( ) и не будет пере
хвачен. Чтобы увидеть все в действии, испытайте следующий пример:
//: exceptions/NeverCaught.java
// Игнорирование исключений RuntimeException.
// {ThrowsException>
Результат работы программы показывает, что вне зависимости от того, было ли воз
буждено исключение, блок finally выполняется всегда.
Этот пример также подсказывает возможное решение проблемы с невозможностью
возврата в Java к месту, где было возбуждено исключение, о чем мы говорили чуть
1 Механизм обработки исключений в языке С++ не имеет аналога fin ally, поскольку опирается
на деструкторы в такого рода действиях.
ЗавершениеспомощьюАпаНу 389
раньше. Если расположить блок try в цикле, можно также определить условие, на
основании которого будет решено, должна ли программа продолжаться. Вы можете
также добавить статический счетчик или какое-то другое «устройство», чтобы позво
лить циклу попытаться решить задачу несколькими способами. Это один из способов
повысить отказоустойчивость программ.
//: exceptions/OnOffSwitch.java
// Для чего используется finally?
f () ;
sw.off();
} catch(OnOffExceptionl e) {
System.out.println("OnOffExceptionl");
sw.off()J
> catch(0n0ffException2 e) {
System.out.println("OnOffException2 ") ;
sw.off();
>
>
> /* Output:
8КЛ
выкл
*///:~
p u b lic s t a t ic v o id m a in ( S t r in g [ ] a rg s) {
p r in t ( " B x o f lH M в первы й бл о к t r y " ) ;
try {
p r in t( " B x O A M M во втор о й бл о к t r y " ) ;
try {
th ro w new F o u r E x c e p t i o n ( ) ;
} f in a lly {
p r in t ( " fin a lly во втором блоке t r y " ) j
}
} c a t c h ( F o u r E x c e p t io n e) {
S y s te m . o u t . p r i n t l n (
"П е р е х ва ч е н о F o u r E x c e p tio n в первом блоке t r y " ) ;
> f in a lly {
S y s t e m .o u t .p r in t ln ( " f in a lly в п ер во м б л о к е t r y " ) ;
>
>
> /* O u t p u t :
Входим в первы й б л о к t r y
Входим во в т о р о й б л о к t r y
f in a lly во втором блоке t r y
Перехвачено F o u r E x c e p tio n в первом блоке t r y
fin a lly в п ер во м б л о к е t r y
V //;~
Блок fin a lly также исполняется в ситуациях, где используются операторы break
и continue. Заметьте, что комбинация fin ally и операторов break и continue с метками
устраняет д л я ^ у а необходимость в операторе goto.
13. (2) Измените упражнение 9, добавив туда блок f i n a l l y . Проверьте, что блок вы
полняется даже в случае возбуждения N u l l P o i n t e r E x c e p t i o n .
14. (2) Покажите, что программа OnOffSwitch.java может завершиться сбоем при воз
буждении R u n t i m e E x c e p t i o n внутри блока t r y .
U . (2) Продемонстрируйте, что программа WithFinally.java работает корректно при воз
буждении R u n t i m e E x c e p t i o n внутри блока t r y .
p u b lic c la s s M u lt ip le R e t u r n s {
p u b lic s t a t ic v o id f( in t i) {
р г ^ С 'И н и ц и а л и з а ц и я , требую щ ая з а в е р ш е н и я " ) ;
try {
print("To4Ka 1");
if(i == 1) return;
print("To4Ka 2");
if(i == 2) return;
print("To4Ka 3");
if(i == 3) return;
print("KoHeu");
продолжение #
392 Глава 12 • Обработка ошибок и исключения
return;
} finally {
print("3aeepuiaKH4ne действия");
}
>
public static void main(String[] args) {
for(int i = 1; i <= 4; i++)
f<i);
>
} /* Output:
Инициализация, требующая завершения
Точка 1
Завершающие действия
Инициализация, требующая завершения
Точка 1
Точка 2
Завершающие действия
Инициализация, требующая завершения
Точка 1
Точка 2
Точка 3
Завершающие действия
Инициализация, требующая завершения
Точка 1
Точка 2
Точка 3
Конец
Завершающие действия
*///:~
//: exceptions/LostMessage.java
// Как можно потерять исключение.
Исключение может быть потеряно еще проще: достаточно включить в блок finally
команду return:
//: exceptions/ExceptionSilencer.java
При выполнении эта программа не выдает никаких результатов, хотя в ней возбуж
дается исключение.
18. (3) Добавьте в LostMessage.java второй уровень потери исключений, чтобы исключение
HoHumException само замещалось третьим исключением.
Ограничения исключений
При переопределении метода вы вправе возбуждать только те исключения, которые
были описаны в методе базового класса. Это полезное ограничение означает, что про
грамма, работающая с базовым классом, автоматически сможет работать и с объектом,
произошедшим от базового (конечно, это фундаментальный принцип ООП), включая
и исключения.
Следующий пример демонстрирует виды ограничений (во время компиляции), на
ложенные на исключения:
//: exceptions/StormyInning.java
// Переопределенные методы могут возбуждать только
// исключения, описанные в версии базового класса,
// или исключения, производные от исключений
// базового класса.
interface Storm {
public void event() throws RainedOut;
public void rainHard() throws RainedOut;
}
public class StormyInning extends Inning implements Storm {
// Можно добавлять новые исключения для
// конструкторов, но вы должны обработать
// и исключения базового конструктора:
public StormyInning()
throws RainedOut, BaseballException {}
public StormyInning(String s)
throws Foul, BaseballException {}
Ограничения исключений 395
1 Язык С++ стандарта ISO вводит аналогичные ограничения при возбуждении исключений
унаследованными версиями методов (исключения обязаны быть такими же или унаследован
ными от исключений базовых версий методов). Это единственный способ С++ для контроля
верности описания исключений во время компиляции.
Конструкторы 397
Конструкторы
Во время написания кода с исключениями следует постоянно спрашивать себя: «Если
произойдет исключение, будет ли все корректно завершено?» Большую часть времени
вы в известной степени под защитой, но конструкторы привносят проблему. Конструк
тор приводит объект в определенное начальное состояние, но может начать выполнять
какое-либо действие —такое, как открытие файла, — которое не будет правильно за
вершено, пока пользователь не освободит объект, вызвав специальный завершающий
метод. Если вы возбуждаете исключения из конструктора, эти финальные действия
могут быть исполнены ошибочно. И это означает, что при написании конструкторов
вы должны быть особенно внимательны.
Казалось бы, проблема может быть решена при помощи блока finally. Но это не так-то
просто, ведь finally выполняется всегда, и даже тогда, когда вы не хотите исполнять
завершающий код до вызова какого-то метода. Если сбой в конструкторе происходит
до того, как он будет выполнен полностью, то он может не успеть создать некоторую
часть объекта, бсвобождаемого в finally.
В нижеследующем примере создается класс lnputF ile, который открывает файл
и позволяет читать из него по одной строке (преобразованной в объект String). Он
использует классы FileReader и BufferedReader из стандартной библиотеки ввода-вы-
вода^уа, которая будет изучена в главе 18, но эти классы достаточно просты, и вряд
ли работа с ними вызовет вопросы:
//: exceptions/InputFile.java
// Исключения в конструкторах,
import java.io.*;
in.close();
} catch(IOException e2) {
System.out.println("omn6Ka при выполнении in.close()");
}
throw e; // Rethrow
> finally {
// Здесь файл не закрывается!!!
>
>
public String getLine() {
String s;
try {
s = in.readLine();
> catch(IOException e) {
throw new RuntimeException("oum6Ka при выполнении readLine()");
>
return s;
>
public void dispose() {
try {
in.close();
System.out.println("dispose() успешен'*);
} catch(IOException e2) {
throw new RuntimeException("oum6Ka при выполнении in.closeQ");
>
>
> / / / :~
Конструктор класса lnputFile получает в качестве аргумента строку (String), со
держащую имя открываемого файла. Внутри блока try он создает экземпляр класса
FileReader для этого файла. Класс FileReader не особенно полезен сам по себе, поэтому
Mbt встраиваем его в созданный BufferedReader, с которым и работаем, —заметьте, что
одно из преимуществ lnputFile состоит в том, что он объединяет эти двадействия.
Если вызов конструктора FileReader проходит неудачно, он возбуждает исключение
FileNotFoundException, которое должно быть перехвачено отдельно. В этом случае не
нужно закрывать файл, так как он и не был открыт. Все другие блоки catch обязаны
закрыть файл, так как он уже был открыт ко времени входа в эти предложения. (Ко
нечно, все было бы сложнее в случае, если бы несколько методов могли возбуждать
FileNotFoundException. Здесь бы вам потребовалось несколько блоков try.) Метод
close() также может возбудить исключение, потому попытка его вызова предпри
нимается в новом блоке try, пусть даже и находящемся в предложении catch, — для
K O M nnnaT opaJava это всего лишь еще одна пара фигурных скобок. После выполне
ния всех необходимых действий по месту исключение возбуждается заново, и это
правильно — ведь вы не хотите, чтобы вызывающий метод полагал, что объект был
благополучно создан.
В этом примере блок finally определенно не годится для закрытия файла, поскольку
в таком варианте закрытие происходило бы каждый раз по завершении работы кон
структора. Так как файл должен оставаться открытым на протяжении срока жизни
объекта lnputFile, подобное решение неверно.
Метод getLine() возвращает объект String, содержащий очередную строку из фай
ла. Он вызывает метод readLine(), способный возбуждать исключения, но они
Конструкторы 399
создается новый блок try. Блок finally, выполняющий завершающие действия, свя
зан с внутренним блоком try; в случае неудачи при конструировании блок finally
не выполняется, но он всегда будет выполнен в случае успешного конструирования.
Эта общая идиома завершения должна использоваться и в том случае, если конструктор
не возбуждает исключений. Основное правило выглядит так: сразу же после создания
объекта, требующего зачистки, начинается конструкция try-finally:
//: exceptions/CleanupIdiom.java
// За созданием каждого объекта, нуждающегося в завершении,
// должна следовать конструкция try-finally
nc5.dispose()j
}
} catch(ConstructionException e) { // Конструктор nc5
System.out.println(e);
> finally {
nc4.dispose();
>
} catch(ConstructionException e) { // Конструктор nc4
System.out.println(e)j
>
}
> /* Output:
NeedsCleanup 1 освобожден
NeedsCleanup 3 освобожден
NeedsCleanup 2 освобожден
NeedsCleanup 5 освобожден
NeedsCleanup 4 освобожден
*///:~
23. (4) Добавьте в предыдущее упражнение класс с методом dispose(). Измените класс
FailingConstructor так, чтобы конструктор создавал один из таких объектов в поле
класса; далее конструктор может выдать исключение, после чего создает второй
объект с необходимостью вызова dispose(). Напишите код для защиты от ошибок;
в методе main() убедитесь в том, что защита распространяется на все возможные
ситуации с ошибками.
24. (3) Добавьте в класс FailingConstructor метод dispose(). Напишите код для пра
вильного использования этого класса.
402 Глава 12 • Обработка ошибок и исключения
Отождествление исключений
При возбуждении исключения механизм обработки исключений ищет в списке «бли
жайших» обработчиков подходящий, в том порядке, в каком они были записаны. Когда
соответствие обнаруживается, исключение считается найденным, и дальнейшего по
иска не происходит.
Отождествление исключений не требует точного соответствия между исключением
и обработчиком. Объект порожденного класса подойдет и для обработчика, изначально
написанного для базового класса:
//: exceptions/Human.java
// Перехват иерархий исключений.
Исключение Sneeze будет перехвачено в первом блоке catch, который ему соответству
ет ~ таковым, конечно, является первый блок. Но если удалить первый блок catch,
оставив только catch для Annoyance, код все равно работает правильно, поскольку исклю
чения Sneeze перехватывает базовый класс. Другими словами, блок catch(Annoyance а)
перехватит Annoyance или любой другой класс, унаследованный от него. Это удобно,
ведь если вы решите добавить больше производных исключений к вашему методу,
программа пользователя этого метода не потребует изменений, так как клиент пере
хватывает исключения базового класса.
Если вы попытаетесь «замаскировать» исключения производного класса, поместив
сначала предложение catch базового класса, как ниже в примере:
try {
throw new Sneeze();
) catch(Annoyance а) {
Альтернативные решения 403
/ / ...
> catch(Sneeze s) {
/ / ...
}
то получите сообщение об ошибке от компилятора, который заметит, что предложение
catch для исключения Sneeze никогда не выполнится.
25. (2) Создайте трехуровневую иерархию исключений. Далее сделайте базовый класс
А с методом, который возбуждает исключение, являющееся основой иерархии. Унас
ледуйте класс в от А и переопределите метод так, чтобы он возбуждал исключение
из второго уровня иерархии. Аналогично поступите при наследовании класса С от В.
В методе main( ) создайте класс С, проведите восходящее преобразование к классу А,
а затем вызовите метод.
Альтернативные решения
Система обработки исключений —это «черный ход», позволяющий вашей программе
отказаться от нормального выполнения ее последовательности предложений. Черный
ход «открывается» при возникновении «исключительных ситуаций», когда обычная
работа далее невозможна или нежелательна. Исключения представляют собой усло
вия, с которыми текущий метод справиться не в состоянии. Причина, по которой воз
никли системы обработки исключений, кроется в том, что программисты не желали
иметь дело с обременительным подходом, навязывающим проверку всех возможных
условий возникновения ошибок каждой функции. В результате ошибки ими просто
игнорировались. Стоит отметить, что вопрос удобства программиста при обработке
ошибок был для разработчиков Java первичной мотивацией.
Один из главных советов при использовании исключений таков: не обрабатывайте
исключение до тех пор, пока вы не знаете, что с ним делать. По сути, отделение кода,
ответственного за обработку ошибок, от места, где ошибка возникает, является одной
из главных целей обработки исключений. Это позволяет вам сосредоточиться на
том, что вы хотите сделать, в одном фрагменте кода и реализовать обработку ошибок
в совершенно другом месте программы. В результате основной код не перемежается
с логикой обработки ошибок и его легко поддерживать и понимать. Обработка исклю
чений также сокращает объем кода, потому что один обработчик может обслуживать
разные точки возникновения ошибок.
Контролируемые исключения немного усложняют происходящее, поскольку они
заставляют вас добавлять блоки catch там, где вы не всегда еще готовы справиться
с ошибкой. В итоге возникает проблема «пагубности поспешного поглощения»:
try {
// ... делает что-то полезное
> са^Ь(ОбязывающееИсключение e) {> // Поглощено!
Программисты (и я в том числе, в первом издании книги), не долго думая, делали самое
бросающееся в глаза, и «поглощали» исключение, зачастую непреднамеренно, но как
только дело было сделано, компилятор был удовлетворен, поэтому пока вы не вспомина
ли о необходимости пересмотреть и исправить код,,то и не вспоминали об исключении.
404 Глава12 • Обработка ош ибокиисключения
Предыстория
Обработка исключений зародилась в таких системах, как PL/1 и Mesa, а затем мигри-
ровалавС Ьи, Smalltalk, Modula-3, Ada, Eiffel, С++, Python,JavanB появившиеся после
Java языки Ruby и С#. Конструкции Java сходны с конструкциями С++, за исключе
нием тех мест, где создатели языка чувствовали, что решения С++ создают проблемы.
Обработка исключений была добавлена в С++ в процессе его стандартизации доволь
но поздно и предназначалась для предоставления программистам инфраструктуры,
которую они с большей охотой стали бы использовать для обработки ошибок и вос
становления после сбоя (за это ратовал Бьерн Страуструп, прародитель языка). Мо
дель исключений в С++ в основном была заимствована из CLU. Впрочем, в то время
существовали и другие языки с поддержкой обработки исключений: Ada, Smalltalk
(в обоих были исключения, но отсутствовали их спецификации) и Modula-3 (в котором
существовали и исключения, и их спецификации).
В своих первых заметках, посвященныхданному вопросу1, Лисков и Снайдер замети
ли, что основным недостатком языков, подобных С, рапортующих об ошибках лишь
с переменным успехом, является следующее:
1 Барбара Лисков и Алан Снайдер: Exception HandlingIn CLU, «Труды IEEE по программному
обеспечению», том SE-5, номер 6, ноябрь 1979. Заметки эти недоступны в сети Интернет, так
что для получения экземпляра вам придется обратиться в библиотеку.
Альтернативные решения 405
+...после каждого вызова необходима проверка условия, определяющая, что бьто получено
в результате. Такое требование приводит программам, трудным в чтении и, возможно,
неэффективньш, из-за чего программисты теряют всякое желание сигнализировать
обисключенияхиобрабатывать их».
Заметьте, что одной из основных причин создания систем обработки исключений
была отмена приведенного требования, но с контролируемыми исклю чениямив^уа
мы обычно такой тип кода и видим. Они продолжают:
+...требование присоединена обработчика к въюову, ставшему причиной возникновения
исключения, приведет к „неудобоваримым “программам, в которых выражения будут
перемежаться обработчиками».
Следуя подходу CLU при разработке исключений С++, Страуструп утверждал, что
его целью было уменьшение количества кода, требуемого для восстановления после
ошибки. Я полагаю, что он наблюдал за программистами, которые не писали обраба
тывающий ошибки код на С, поскольку объем этого кода был устрашающим, а раз
мещение создавало путаницу. В результате все происходило в стиле С, когда ошибки
в коде игнорировались, а с проблемами справлялись при помощи отладчиков. Чтобы
исключения использовались в программе, разработчиков необходимо было убедить
писать «дополнительный» код, чего они обычно не делали. Таким образом, чтобы
склонить их на сторону лучшего способа обработки ошибок, объем «лишнего» кода
не должен был становиться им в тягость. Я думаю, важно учитывать эту цель при рас
смотрении эффективности контролируемых исключений Bjava.
С++ добавил к идее CLU дополнительную возможность: специфирование исключений,
позволяющее программно описать, какие исключения могут возникать при вызове
данного метода. В действительности у спецификации исключения два предназначения.
Она может говорить: «Я возбуждаю это исключение в коде, а вы его обрабатываете». Но
она также может утверждать: «Я игнорирую исключение, которое имеет шанс на воз
никновение в моем коде, но обрабатываете его снова вы». При освещении механизмов
исключений мы концентрировались на утверждении «обрабатываете вы», но здесь мне
хотелось бы поближе рассмотреть тот факт, что зачастую мы игнорируем исключение,
и именно это может сформулировать спецификация исключения.
В С++ спецификация исключений не является частью информации о типе функции.
Единственная проверка, осуществляемая во время компиляции, имеет целью удостове
риться в последовательности использования исключений: к примеру, если функция или
метод возбуждает исключения, то перегруженная или переопределенная версия должна
возбуждать те же самые исключения. Однако, в отличие oTjava, во время компиляции
не проводится проверки на то, действительно ли функция или метод возбуждают данное
псключение, или на полноту спецификации (то есть описывает ли она все исключения,
возможные для этого метода). Проверка все же происходит, но уже во время работы про
граммы. Если возбуждается исключение, не являющееся частью спецификации исклю
чений, программа на С++ вызывает функцию unexpected() из стандартной библиотеки.
Интересно отметить, что из-за использования шаблонов (templates) вы вовсе не най
дете спецификации исключений в стандартной библиотеке С++. Таким образом, Bjava
существуют ограничения на возможности использования обобщенных THnoBjava со
спецификациями исключений.
406 Глава 12 • Обработка ошибок и исключения
Перспективы
Во-первых, стоит заметить, что n3HKjava по сути стал первопроходцем в использова
нии контролируемых исключений (несомненно, из-за спецификаций исключений С++
и того факта, что программисты на С++ не уделяли им слишком много внимания). Это
был эксперимент, повторить который с тех пор пока не решился еще ни один язык.
Во-вторых, контролируемые исключения кажутся, несомненно, хорошим средством
при рассмотрении вводных примеров и в небольших программах. Оказывается, что
трудноуловимые проблемы начинают проявляться при разрастании программы. Ко
нечно, программы не становятся большими тут же и сразу, но они имеют тенденцию
расти незаметно. И когда языки, не предназначенные для больших проектов, исполь
зуются для небольших, но растущих проектов, мы в некоторый момент с удивлением
обнаруживаем, что ситуация постепенно выходит из-под контроля. Именно это, как
я полагаю, может произойти, когда проверок типов слишком много, и особенно в от
ношении контролируемых исключений.
Масштаб программы, похоже, является немаловажным вопросом. Это представляет
собой проблему, поскольку, как правило, в дискуссиях демонстрируются небольшие
программки. Один из проектировщиков C# подчеркнул1, что:
«...Изучение неболыиш программ приводит к выводу, что требование спецификации
истслючений позволяет и увеличить продуктивность разработчика, и улучшить ка
чество кода; однако опыт с большими проектами приводит к другому заключению —
уменьшенная продуктивность и небольшое улучшение качества кода wtu отсутствие
такового вовсе».
В отношении необрабатываемых исключений создатели CLU говорят2:
<Мы посчитали, что невозможпо требоватъ от программиста написания обработчика
в ситуациях\ где нет возможности провести осмысленные действия».
Страуструп, объясняя, почему объявление функции без спецификации исключений
означает, что функция может возбудить любое исключение, а не то, что исключений
вообще не возникает, утверждает3:
«В таком случае понадобшись бы спецификации исключения практически для любой
функции, это стало бы серьезной причиной для перекомпиляции и препятствовало бы
взаимодействию с программами, написанными на других языках. Все это заставило
бы программистов низвергнуть механизм обработки исключений и писать фальшивый
код, подавляющий исключения. Это дало бы ложное чувство безопасности людям, не
сумевшим увидеть исключения».
Именно такое поведение — «низвержение» исключений — и происходит с управляе
мыми исключениями eJava.
Мартин Фаулер (автор книг UML Distilled, Refactoring и Analysis Pattems) написал
мне следующее:
1 http://discuss.develop.com/archive5/wa.exe?A2=ind001 lA&L=DOTNET&P=R32820.
2 Exception Handling in CLU, Liskov & Snyder.
3 Bjarne Stroustrup, The C++ Programming Language, 3rd Edition (Addison-Wesley, 1997), c. 376.
Альтернативные решения 407
его мне не хочется, так же как и печатать банальное сообщение». Благодаря цепочкам
исключений у этой проблемы появляется простое решение. Управляемое исключение
просто «заворачивается» в класс RuntimeException, примерно так:
try {
// ... делаем что-нибудь полезное
> саТсИ(НеЗнаюЧтоДелатьСЭтимКонтролируемымИсключением e) {
throw new RuntimeException(e);
}
Кажется, что это идеальное решение, если вы хотите «отключить» контролируемое
исключение, —вы не «поглощаете» его, вам не приходится описывать его в своей специ
фикации исключений, и благодаря цепочке исключений вы не теряете информацию
об оригинальном исключении.
Такой прием дает возможность игнорировать исключение и пустить его «всплывать»
вверх по стеку вызова без необходимости писать конструкции try-catch и/или специ
фикации исключений. Впрочем, вы все еще можете перехватить и обработать некоторое
исключение, используя метод getCause(), как показано здесь:
//: exceptions/TurnOffChecking.java
// "Отключение" контролируемых исключений,
import java.io.*;
import static net.mindview.util.Print.*;
class WrapCheckedException {
void throwRuntimeException(int type) {
try {
switch(type) {
case 0: throw new FileNotFoundException();
case 1: throw new IOException();
case 2: throw new RuntimeException('Tfle я?");
default: return;
>
} catch(Exception e) { // Превращаем в неконтролируемое:
throw new RuntimeException(e);
>
}
29. (1) Измените все типы исключений в StormyInning.java так, чтобы они расширяли
RuntimeException. Покажите, что при этом не обязательны ни спецификации ис
ключений, ни блоки try. Удалите комментарии / / ! и продемонстрируйте, что эти
методы могут компилироваться без спецификаций.
30. (2 ) И зм ените пример Human.java так, чтобы исклю чения наследовали от
RuntimeException. Измените метод main() так, чтобы прием из примера TurnOffChecking.
java использовался для обработки разных типов исключений.
Резюме 411
Резюме
Исключения являются неотъемлемым аспектом программирования на языке Java;
без умения работать с ними вы далеко не уйдете. По этой причине исключения пред
ставлены на этой стадии изложения материала — хотя существует много библиотек
(например, библиотека ввода-вывода, упоминавшаяся ранее), с которыми невозможно
работать без обработки исключений.
Одно из преимуществ механизма обработки исключений заключается в том, что он
позволяет сосредоточиться на решаемой задаче в одном месте, а затем разобраться
с ошибками, которые могут возникнуть в этом коде, в другом месте. И хотя исключения
обычно рассматриваются как инструменты передачи информации и восстановления
работоспособности после ошибок во время выполнения, я не уверен в том, насколько
часто реализуется аспект «восстановления» —да и насколько это возможно. По моим
оценкам, это происходит менее чем в 10 % случаев, и даже в этом случае восстановление
обычно сводится к раскрутке стека в последнее, заведомо стабильное состояние, а не
к выполнению каких-то восстановительных операций. Не знаю, правда это или нет,
но я пришел к убеждению, что подлинная ценность исключений проявляется именно
в области передачи информации. Java фактически требует, чтобы программа сообщала
обо всех ошибках в виде исключений, и в этом проявляется огромное преимущество
Java перед такими языками, как С++, которые позволяют сообщать об ошибках мно
гими разными способами —или не сообщать о них вовсе. Последовательная система
передачи информации об ошибках означает, что вам уже не нужно задавать себе вопрос:
412 Глава 12 • Обработка ошибок и исключения
Постоянство строк
Объекты класса String постоянны (immutable). Просмотрев документацию класса
String B j D K , вы увидите, что каждый метод класса, который на первый взгляд изменяет
String, в действительности создает и возвращает новый объект String с включенными
изменениями. Исходный объект String при этом не модифицируется.
Рассмотрим следующий пример:
//: strings/Immutable.java
import static net.mindview.util.Print.*;
Должен ли метод upcase() изменять свой аргумент? С точки зрения читателя кода аргу
мент обычно представляет информацию, предоставляемую методу, а не ту, которая тре
бует изменения. Это важное обстоятельство, упрощающее написание и понимание кода.
Перегрузка + и StringBuilder
Так как объекты String не модифицируются, для одного объекта String в программе
можно создать сколько угодно «синонимов». Поскольку объект String доступен только
для чтения, одна ссылка ни при каких условиях не изменит данные, используемые по
другим ссылкам.
Оператор + был перегружен для объектов String. Перегрузкой называется изменение
смысла оператора при его использовании с конкретным классом. (Перегрузка опера
торов в Java ограничивается операторами + и += для класса string; Java не позволяет
программисту перегружать другие операторы1.)
Оператор + может использоваться для конкатенации объектов String:
//: strings/Concatenation.java
1 В С++ программист может перегружать любые операторы по своему усмотрению. Так как этот
процесс часто приводит к сложностям, coздaтeлиJava решили, что это «плохая» возможность,
которую не стоит включать в язык. Видимо, она все же была недостаточно плохой, если они сами
не обошлись без перегрузки операторов, и как ни парадоксально, Bjava использовать перегрузку
операторов было бы намного проще, чем в С++. Например, в языках Python (www.Python.org)
и С#, использующих уборку мусора, реализован упрощенный механизм перегрузки операторов.
Перегрузка + и StringBui!der 415
Как можно было быреализовать эту серию операций? Объект String ''abc" может
содержать метод append(), который создает новый объект String из подстроки "abc",
объединенной с содержимым mango. Полученный объект String создает новый объект
String, к которому добавляется подстрока "def” и т. д.
Не только код цикла стал короче и проще, но и метод теперь создает только один объект
StringBuilder. Явное создание StringBuilder также позволяет заранее выделить объ
ект нужного размера (если вы располагаете соответствующей информацией), чтобы
избежать многократных повторных выделений памяти для буфера.
Таким образом, если при создании метода toString() выполняются простые операции,
в которых компилятор может разобраться самостоятельно, обычно можно доверить
построение результата компилятору. Но если в вычислениях задействован цикл, лучше
я в н о использовать StringBuilder в toString():
//: strings/UsingStringBuilder.java
import java.util.*j
Непреднамеренная рекурсия
Так как стандартные контейнерь^ауа (как и все остальные классы) в конечном итоге
наследуют от Object, они содержат метод toString(). Этот метод был переопределен,
чтобы контейнеры могли выдать свое представление в формате String, включающее
данные о хранящихся в них объектах. Например, метод ArrayList.toString( ) перебирает
элементы ArrayList и вызывает для каждого элемента toString():
//: strings/ArrayListDisplay.java
import generics.coffee.*j
import java.util.*;
Допустим, вы хотите, чтобы метод toString() выводил адрес объекта вашего класса.
Казалось бы, для этого достаточно использовать ссылку this:
//: strings/InfiniteRecursion.java
// Accidental recursion.
// {RunByHand}
import java.util.*;
Непреднамеренная рекурсия 419
Операции со строками
В таблице представлены некоторые основные методы объектов String. Перегруженные
методы объединены в одну строку таблицы.
продолжение &
420 Глава 13 • Строки
Форматирование вывода
Одним из долгожданных нововведений, появившихся в Java SE5, стал механизм
форматирования вывода в стиле команды p rin tf () языка С. Он не только упрощает
код вывода, но и предоставляет разработчикам Java мощные средства управления
форматированием и выравниванием1.
printf()
Метод p rin tf() языка С не «собирает» строки так, как это делается Bjava; он получает
одну форматную строку и вставляет в нее значения, форматируя их при подстановке.
Вместо того чтобы использовать для конкатенации текста в кавычках и переменных
перегруженный оператор + (который не перегружается в языке С), p rin tf() использует
специальные служебные комбинации для обозначения позиции данных. Аргументы,
вставляемые в форматную строку, перечисляются в виде списка, разделенного за
пятыми.
Например:
printf("CTpoKa 1: [%d %f]\n", x, y)j
Марк Уэлш помог мне написать этот раздел, а также раздел «Сканирование ввода».
422 Глава 13 • Строки
System.out.format()
BJava SE5 появился новый метод format (), доступный в объектах PrintStream и Print-
Writer (эти объекты более подробно рассматриваются в главе 18), к которым также
относится System.out. Метод forraat() создан по образцу printf() языка С. Также
существует вспомогательный метод printf(), который просто вызывает format(); ис
пользуйте его, если это имя кажется вам более привычным. Простой пример:
//: strings/SimpleFormat.java
Как видите, вызовы format() и p rin tf() эквивалентны. В обоих случаях передается
одна форматная строка, за которой перечисляются аргументы —по одному для каждого
форматного спецификатора.
Класс Formatter
Вся новая функциональность форматирования обеспечивается классом Formatter из
пакета java.util. Класс Formatter можно рассматривать как преобразователь, при
водящий форматную строку и данные к нужному результату. При создании объекта
Formatter вы сообщаете ему, куда следует выдать результат, передавая эту информацию
конструктору:
//: strings/Turtle.java
import java.io.*;
import java.util.*;
Форматные спецификаторы
Для управления интервалами и выравниванием вставляемых данных потребуются
более сложные форматные спецификаторы. Общий синтаксис выглядит так:
%[аргумент_индекс$][флаги] [ширина ][. точность]преобразование
Total 25.06
*///:~
Преобразования Formatter
Чаще всего на практике используются следующие преобразования
Символы преобразования
_d__________ Целое число (десятичное)
c Символ Юникода
b ~ Логическое значение
s Строка
f ~ Вещественное число (в десятичной записи)
e Вещественное число (в экспоненциальной записи)
x Целое число (шестнадцатеричное)
~h Хеш-код (в шестнадцатеричной записи)
%_________ Литерал «%»
char u = 'a';
System.out.println("u = ’a'");
f.format("s: %s\n", u);
// f.format("d: %d\n'\ u);
f.format("c: %c\n", u);
f.format("b: %b\n", u)j
// f.format("f: %f\n", u);
// f.format("e: %e\n", u);
// f.format(''x: %x\n", u);
f.format("h: %h\n"j u)j
int v = 1 2 1 ;
System.out.println("v = 121");
f.format("d: %d\n", v);
f.format("c: %c\n", v);
f.format("b: %b\n"j v);
f.format("s: %s\n", v);
// f.format("f: %f\n", v);
// f.format("e: %e\n"j v);
f.format("x: %x\n", v);
f.format("h: %h\n", v);
double x = 179.543;
System.out.println("x = 179.543");
// f.format("d: %d\n", x);
// f.format("c: %c\n", x);
f.format("b: %b\n", x);
f.format("s: %s\n", x);
f.format("f: %f\n", x);
f.format("e: %e\n", x);
// f.format("x: %x\n", x);
f.format("h: %h\n", x);
boolean z = false;
System.out.println("z = false");
// f.format("d: %d\n", z);
// f.format("c: %c\n", z);
f.format("b: %b\n", z);
f.format("s: %s\n", z);
// f.format("f: %f\n", z);
// f.format("e: %e\n", z);
// f.format("x: %x\n"j z);
f.format("h: %h\n", z);
}
} /* Output: (Sample)
u = 'a'
s: а
с: а
b: true
h: 61
v = 12 1
d: 12 1
с: у
b: true
s: 12 1
x: 79
h; 79
w = new BigInteger("50000000000000")
d: 50000000000000
b: true
s: 50000000000000
x: 2d79883d2000
h: 8842ala7
x = 179.543
Форматирование вывода 427
b: true
s: 179.543
f: 179.543000
e: 1 .795430e+02
h: lef462c
у = new Conversion()
b: true
s: Conversion09cabl6
h: 9cabl6
z - false
b: false
s: false
h: 4d5
V//:~
В закомментированных строках приведены преобразования, недействительные для
конкретного типа переменной; попытка их выполнения приводит к возбуждению
исключения.
Обратите внимание: преобразование b работает для всех переменных. Хотя оно дей
ствительно для всех типов аргументов, оно может работать не так, как вы ожидаете.
Для примитивов boolean или объектов Boolean результат будет равен true или false в за
висимости от значения, но для любого другого аргумента, отличного от nill, результат
всегда равен true. Даже для числового значения 0, которое является синонимом false
во многих языках (включая С), будет получено значение true; будьте осторожны при
использовании этого преобразования с типами, отличными от boolean.
Также существуют другие типы преобразований и другие параметры форматного специ
фикатора. О них можно прочитать в описании класса Formatter из документации JDK.
5 . (5) Для каждого базового типа преобразования в приведенной таблице напишите
самое сложное из возможных выражений форматирования. Другими словами, ис
пользуйте все возможные форматные спецификаторы, доступные для этого типа
преобразования.
String.format()
Проектировщики Java SE5 также создали аналог функции sprintf() языка С, пред
назначенной для создания строк. Статический метод String.format() получает те же
аргументы, что и метод format() класса Formatter, но возвращает String. Он может
пригодиться в ситуации, в которой format () нужно вызвать всего один раз:
//: strings/DatabaseException.java
System.out.println(e);
>
}
} /* Output:
DatabaseException: (t3, q7) Ошибка записи
*///:~
//: net/mindview/util/Hex.java
package net.mindview.utilj
import java.io.*j
*///:
Регулярные выражения 429
Регулярные выражения
Регулярные выражения давно поддерживаются стандартными утилитами Unix (такими,
как sed и awk), а также языками Python и Perl (некоторые разработчики считают, что
именно регулярные выражения стали основной причиной успеха Perl). Ранее основные
средства работы со строками были реализованы Bjaya в классах String, StringBuf-
fer и StringTokenizer, которые обладают относительно простыми возможностями по
сравнению с регулярными выражениями.
Регулярные выражения —мощный и гибкий инструмент обработки текстов. Они по
зволяют определять на программном уровне сложные шаблоны для поиска текста во
входной строке. Обнаружив совпадение для шаблона, вы можете обработать его так,
как считаете нужным. Синтаксис регулярных выражений поначалу выглядит устра
шающе, но это компактный и динамичный язык, который может использоваться для
решения самых разнообразных задач обработки строк, поиска и выделения совпадений,
редактирования и проверки.
Основы
Регулярные выражения предназначены для обобщенного описания строк по прин
ципу: «Если строка содержит такие-то элементы, то в ней находится совпадение для
искомого критерия». Например, чтобы указать, что перед числом может стоять знак
«-» (но его может и не быть), вы включаете в условие поиска знак «-», за которым
следует вопросительный знак:
-?
Символы
_B___________________ Символ В
V<hh Символ с шестнадцатеричным кодом Oxhh
\uhhhh Символ Юникода с шестнадцатеричным представлением Oxhhhh
\t Табуляция
\n Новая строка
_ V __________________ Возврат курсора
_y___________________ Подача страницы
\e__________________ Escape
Символьные классы
• Любой символ
[abc] Любой из символов а, b и с (то же, что a|b|c)
[^abc] Любой символ, кроме а, b и с (отрицание)
[a-zA-Z] Любой символ от а до z и от А до Z (диапазон)
[abc[hij]] Любой из символов а, b, с, h, i, j (то же, что a|b|c|h|i|j) (объедине
ние)
[a-z8rfk[hij]] Символ h, i или j (пересечение)
\s Пропуск (пробел, табуляция, новая строка, подача страницы,
возврат курсора)
^^S Символ, не являющийся пропуском ([^\s])
_№__________________ Цифра [0-9]
J D __________________ Не цифра [^0-9]
\w Символ слова [a-zA-Z_0-9]
\W Символ, не являющийся символом слова [^\w]
Регулярные выражения 433
Здесь приведена лишь небольшая подборка; за полным списком всех шаблонов регуляр
ных выражений обращайтесь к странице ja v a .util.regex.Pattern в докум ентации^К .
Логические операторы
XY X, за которым следует Y
~xjY
_ X или Y
Сохраняющая группировка. Позднее в выражении к i-й сохраненной
группе можно обратиться при помощи записи \i
Привязка к границам
А.
Начало строки
$ Конец строки
J b ________________ Граница слова
J B ________________ Не граница слова
_Vf________________ Конец предыдущего совпадения
Квантификаторы
Квантификатор описывает режим «поглощения» входного текста шаблоном:
□ Максимальные квантификаторы используются по умолчанию. В максимальном ре
жиме для выражения подбирается максимально возможное количество возможных
совпадений. Одна из типичных ошибок — полагать, что шаблон совпадет только
с первой возможной группой символов, тогда как в действительности механизм
регулярных выражений продолжает двигаться вперед, пока не подберет возможное
совпадение максимальной длины.
□ М индальны й квантификатор (задается вопросительным знаком) старается ограни
читься минимальным количеством символов, необходимых для соответствия шаблону.
434 Глава 13 • Строки
Помните, что выражение X часто приходится заключать в круглые скобки, чтобы оно
работало так, как нужно. Для примера возьмем следующее выражение:
abc+
CharSequence
Интерфейс CharSequence устанавливает обобщенное определение последовательности
символов, выделенной в классе CharBuffer, String, StringBuffer или StringBuilder:
interface CharSequence {
charAt(int i);
length();
subSequence(int start, int end)j
toString();
>
Pattern и Matcher
В общем случае следует компилировать объекты регулярных выражений вместо того,
чтобы использовать весьма ограниченные возможности String. Для этого импортируйте
Регулярные выражения 435
к строке
"Arline ate eight apples and one orange while Anita hadn't any''
flnd()
Метод Matcher.find() может использоваться для поиска множественных совпадений
шаблона в объекте CharSequence, к которому он применяется. Пример:
//: strings/Finding.java
import java.util.regex.*;
import static net.mindview.util.Print.*j
while(m.findQ)
printnb(m.group() + " ");
print()j
int i = 0;
while(m.find(i)) {
printnb(m.group() + " ");
i++j
>
}
} /* Output:
Evening is full of the linnet s wings
Evening vening ening ning ing ng g is is s full full ull 11
1 of of f the the he e linnet linnet innet nnet net et t s
s wings wings ings ngs gs s
V //:~
Шаблон \\w+ разбивает входные данные на слова. Метод find() действует как итератор,
перемещаясь по входной строке. Второй версии find() может передаваться целочислен
ный аргумент с позицией символа, с которой должен начинаться поиск, —эта версия
сбрасывает позицию поиска до значения аргумента, как видно из выходных данных.
Группы
Группы представляют собой части регулярного выражения, заключенные в круглые
скобки, к которым позднее можно обращаться по номеру группы. Группа 0 соответ
ствует совпадению всего выражения, группа 1 —совпадению первого подвыражения
в круглых скобках и т. д. Таким образом, в выражении
A(B(C))D
start() и end()
После успешного поиска совпадения метод start() возвращает начальный индекс
предыдущего совпадения, а метод end () возвращает индекс последнего символа совпа
дения, увеличенный на 1. Вызов start() или end() после неуспешной операции поиска
совпадения (или до ее начала) порождает исключение IllegalStateException. Следу
ющая программа также демонстрирует применение методов matches () и lookingAt ()*:1
//: strings/StartEnd.java
import java.util.regex.*;
import static net.mindview.util.Print.*;
Флаги шаблонов
Альтернативный метод compile() получает флаги, управляющие процессом поиска
совпадений:
Pattern Pattern.compile(String regex, int flag)
1 Понятия не имею, как создателиДауа придумали это название и что оно должно означать. По
крайней мере приятно знать, что в этом мире хоть что-то остается неизменным: люди, изо
бретающие невразумительные имена методов, продолжают работать в Sun, а политика отказа
от рецензирования программных архитектур все еще действует. Простите за сарказм, но через
несколько лет такие вещи начинают утомлять.
Регулярные выражения 441
split()
Метод split() разбивает входную строку по совпадениям регулярного выражения
и создает массив объектов String:
String[] split(CharSequence input)
String[] split(CharSequence input, int limit)
Операции замены
Регулярные выражения особенно полезны при замене текста. Для ее выполнения до
ступны следующие методы:
re pl ac eF ir st (St ri ng replacement) заменяет первое совпадение во входной строке
строкой replacement.
read() читаетвесь файл и возвращает его содержимое в виде объекта String. Пере
менная mlnput предназначенадля хранения текста совпадения (обратите внимание на
группирующие круглые скобки) между /*! и !*/. Затем серия из двух и более пробелов
сокращается до одного пробела, а пробелы в начале строк удаляются (чтобы удаление
произошло во всех строках, а не только в начале ввода, должен быть активен много
строчный режим). Эти две замены выполняются эквивалентным (но более удобным
в данном случае) методом replaceAll(), который является частью String. Поскольку
каждая замена используется в программе только один раз, такая реализация не сни
жает эффективность по сравнению с предварительной компиляцией в объект Pattern.
Метод replaceFirst() выполняет только первую найденную замену. Кроме того, за
меняющие строки в r e p l a c e F i r s t ( ) и r e p l a c e A l l ( ) представляют собой обычные
литералы, и если вы хотите выполнить какую-либо дополнительную обработку при
каждой замене, они вам не помогут. В таких случаях необходимо использовать метод
appendReplacement(), который позволяет задать код, выполнямый при замене. В преды
дущем примере при построении итогового буфера sbuf выбираются и обрабатываются
данные group() (в данном случае гласные, найденные при помощи регулярного выра
жения, преобразуются к верхнему регистру). Обычно все замены выполняются одна за
другой, после чего вызывается метод appendTail(), но если вы захотите смоделировать
метод replaceFirst() (или выполнить замену для первых n вхождений), выполните
замену и вызовите appendTail(), чтобы поместить остаток в sbuf.
Метод appendReplacement() также позволяет включить ссылку на сохраненную группу
прямо в строку замены; для этого используется обозначение $g, где g —номер группы.
Впрочем, эта возможность предназначена для более простых задач и не принесет же
лаемого результата в приведенной программе.
reset()
Существующий объект Matcher может быть применен к новой символьной последо
вательности методами reset ():
//: strings/Resetting.java
import java.util.regex.*;
17 . (8) Напишите программу, которая читает файл с исходным кодом ^уа (имя файла
передается в командной строке) и выводит все комментарии, содержащиеся в файле.
18 . (8) Напишите программу, которая читает файл с исходным KOAOMjava (имя файла
передается в командной строке) и выводит все строковые литералы, содержащиеся
в файле.
19 . (8) На основе двух последних упражнений напишите программу, которая анали
зирует исходный KOflJava и выдает список всех имен классов, использованных в
программе.
Сканирование ввода
Когда-то чтение данных из текстового файла или стандартного ввода считалось от
носительно хлопотным делом. Обычно программа читала строку текста, разбивала ее
на лексемы, после чего использовала различные методы классов integer, Double и т. д.
для разбора данных:
//: strings/SimpleRead.java
import java.io.*;
public class SimpleRead {
public static BufferedReader input = new BufferedReader(
new StringReader("Sir Robin of Camelot\n22 1.61803"));
public static void main(String[) args) {
try {
System.out.println("What is your name?");
String name = input.readLine();
System.out.println(name);
System.o u t .println(
"How old are you? What is your favorite double?");
System.out.println("(input: <age> <double>)");
String numbers = input.readLine();
System.out.println(numbers);
Операции замены 447
Ограничители Scanner
По умолчанию Scanner разбивает входные данные по пропускам, но вы также можете
задать собственный ограничитель в форме регулярного выражения:
//: strings/ScannerDelimiter.java
import java.util.*;
while(scanner.hasNextInt())
System.out.println(scanner.nextInt());
}
} /* Output:
12
42
78
99
42
*///:~
В этом примере в качестве разделителей при чтении из String используются запятые
(окруженные произвольным количеством пропусков). Тот же способ может исполь
зоваться для чтения из файлов, разделенных запятыми. Кроме метода useDelimiter(),
назначающего шаблон разделителя, также существует метод delimiter(), который
возвращает текущий объект Pattern, используемый в качестве разделителя.
StringTokenizer
До введения поддержки регулярных выражений (Bj2SE1.4) или класса Scanner (eJava
SE5) для разбиения строк использовался класс StringTokenizer. Теперь те же задачи
решаются намного проще и компактнее, но ниже приведено простое сравнение String
Tokenizer с двумя другими методами:
//: strings/ReplacingStringTokenizer.java
import java.util.*j
Резюме
В прошлом поддерж ка строковых операций Bjava была весьма примитивной, но в по
следних версиях языка появились куда более совершенные средства, позаимствованные
из других языков. Н а данный момент поддерж ка строк Bjava достаточно полна, хотя
иногда приходится обращать внимание на подробности, связанные с эффективностью,
например на правильность использования StringB uilder.
Информация о типах
Circle.draw()
Square.draw()
Triangle.draw()
*///:~
Метод draw() базового класса Shape неявно использует метод toString() для вывода
идентификатора класса, передавая методу System.o u t .println() ссылку this (обратите
знимание: метод toString() объявлен абстрактным, чтобы производные классы были
эоязаны переопределить его и чтобы предотвратить создание экземпляров Shape). Когда
при выводе строки в выражении конкатенации (с оператором + и объектами String)
встречается объект String, автоматически вызывается его метод toString(), возвраща
ющий строковое представление объекта. Каждый производный класс переопределяет
метод toString( ) (из базового класса Object), чтобы метод draw( ) (полиморфно) выво-
Л 1Л в каждом случае различную информацию.
В данном примере восходящее преобразование происходит во время помещения объ-
екта-фигуры в контейнер List<Shape>. При восходящем преобразовании к Shape тот
факт, что объекты являются к о н к р ет н ы м и р а з н о в и д н о с т я м и Shape, теряется. С точки
зрения массива в нем хранятся просто объекты Shape.
При извлечении элемента из массива контейнер (который в действительности хранит
все в формате Object) автоматически преобразует результат обратно в Shape, Это основ
ная форма RTTI, поскольку все подобные преобразования в H3biKeJava проверяются
зо время исполнения на корректность. Именно для этого и служит RTTI: во время
выполнения программы проверяется истинный тип объекта.
В нашем случае определение типа происходит частично: тип Object преобразуется
к базовому типу фигур Shape, а не к конкретным типам Circle, Square или Triangle.
Просто потому, что в данный момент только нам и зв е с т н о то, что массив заполнен
фигурами Shape. Во время компиляции правильность преобразований обеспечивается
контейнером и системой обобщенных ranoeJava, но при выполнении требует явного
преобразования типов.
После в действие вступает полиморфизм —для каждой фигуры Shape выполняемый
код определяется в зависимости от того, окружность ли это (Circle), прямоугольник
tSquare) или треугольник (Triangle). И в основном именно так все и должно работать;
основная часть вашего кода не должна заботиться о ф а к т и ч ес к о м типе объекта, она
оперирует с универсальным представлением целого семейства объектов (в нашем
случае это фигура (shape)). В результате программа легче пишется, а впоследствии
проще читается и сопровождается, и самые сложные проекты проще реализовать, по
нять и изменить. По этой причине полиморфизм считается желательной целью при
написании объектно-ориентированных программ.
Но что, если у вас имеется не совсем обычная задача, для успешного решения кото
рой необходимо знать точный тип объекта, обладая ссылкой только базового типа?
К примеру, пользователи программы с фигурами захотели подсветить определенные
фигуры фиолетовым цветом. Таким образом они легко обнаружат на экране все тре
угольники — они будут отличаться от других фигур цветом. А может, метод должен
выполнить поворот для фигур из списка, но поворачивать круги бессмысленно, и вы
лредпочитате исключить их из обработки. Эту задачу помогает решить RTTI: вы
454 Глава 14 • Информация о типах
можете запросить точный тип объекта, на который указывает ссылка базового типа
shape, и отобрать объекты, требующие особого обращения.
Объект Class
Чтобы понять, как работает RTTI eJava, сначала необходимо узнать, каким образом
хранится информация о типе во время выполнения программы. Делается это с по
мощью специального объект а типа Class, который и содержит информацию о классе.
Более того, объект Class используется при создании всех «обыкновенных» объектов
любой программы. Java выполняет операции RTTI с использованием объекта class
даже при выполнении явного преобразования типа. Класс Class также предоставляет
ряд других возможностей использования RTTI.
Для каждого класса, использующегося вашей программой, существует свой объект
Class. То есть каждый раз при написании и последующей компиляции нового класса
для него создается объект Class (который затем сохраняется в одноименном файле
с расширением .class). Чтобы создать объект этого класса, виртуальная машина Java
(JVM ) использует подсистему, называемую загрузчиком классов.
Подсистема загрузчика классов в действительности может включать цепочку за
грузчиков, но существует всего один первичны й за гр узч и к классов , который является
частью реализации JVM. Первичный загрузчик классов загружает так называемые
доверенны е классы, включая классы Java API (обычно с локального диска). Как пра
вило, включать дополнительные загрузчики в цепочку не обязательно, но в особых
ситуациях (например, при загрузке классов для поддержки приложений веб-серверов
или загрузке классов по сети) в вашем распоряжении имеется механизм подключения
дополнительных загрузчиков классов.
Все классы загружаются в JVM динамически, при первом использовании класса.
Это происходит при первом обращении к статическому члену класса. Оказывается,
конструктор тоже является статическим членом класса, хотя ключевое слово static
для конструктора не используется. Таким образом, создание нового объекта класса
оператором new также считается обращением к статическому члену класса.
Итак, программа Java не загружается полностью в самом начале; ее фрагменты за
гружаются по мере необходимости. В этом отношении Java отличается от многих
традиционных языков. Динамическая загрузка позволяет реализовать поведение,
которое трудно или невозможно воспроизвести в языках со статической загрузкой
(таких, как С++).
Загрузчик классов сначала проверяет, загружен ли объект Class для указанного типа.
При отрицательном результате загрузчик по умолчанию ищет файл .class с соответ
ствующим именем (внешний загрузчик, например, может прочитать байт-код из базы
данньгх). В процессе загрузки исполнительная система п роверяет байт-код класса,
убеждаясь в том, что он не был поврежден и не содержит некорректного кода Java
(одна из линий обеспечения безопасности Bjava).
Как только объект Class для определенного типа окажется в памяти, в дальнейшем
он используется при создании всех объектов этого типа. Следующая программа по
ясняет сказанное:
Необходимость в динамическом определении типов (RTT1) 455
//: typeinfo/SweetShop.java
// Examination of the way the class loader works,
import static net.mindview.util.Print.*;
class Candy {
static { print("3arpy3Ka класса Candy"); >
>
class Gum {
static { print("3arpy3Ka класса Gum"); }
>
class Cookie {
static { print("3 arpy3 Ka класса Cookie'*); >
}
Class.forName("Gum");
Это вызов статического метода класса Class (которому принадлежат все объекты
Class). Объект Class ничем не хуже обычных объектов, поэтому вы можете создавать
его и манипулировать ссылками на него. (Именно так и поступает загрузчик классов.)
456 Глава14 • Информацияотипах
Один из способов получить ссылку на объект Class —вызвать метод forName(), передав
ему в качестве аргумента строку (String) с именем (следите за правильностью напи
сания!) определенного класса. Этот метод возвращает ссылку на объект типа Class,
которая нами здесь игнорировалась; вызов методаС1а55^ог№те() был произведен для
получения побочного эффекта, частью которого является загрузка класса Gum, если он
еще не в памяти. В процессе загрузки выполняется sta tic-инициализатор класса Gum.
В рассмотренном примере, если бы метод Class.forName() завершился неудачей (не
смог бы найти класс, который вы хотели загрузить), он возбудил бы исключение
ClassNotFoundException (в идеале, название исключения скажет вам практически все,
что нужно знать о возникшей ошибке1). Здесь мы просто информируем о проблеме
и двигаемся дальше, однако в более сложной программе можно было бы попытаться
исправить ошибку в обработчике исключения.
Каждый раз, когда вы хотите использовать информацию о типе во время выполнения,
сначала необходимо получить ссылку на соответствующий объект Class. Для этого
удобно воспользоваться методом class.forName(), потому что для получения ссылки
на Class объект не нужен. Но если у вас уже имеется объект интересующего вас типа,
для получения ссылки на Class можно вызвать метод, являющийся частью корневого
класса Object: getClass(). Метод возвращает ссылку на объект Class, представляющий
фактический тип объекта. Класс Class содержит много интересных методов; вот лишь
несколько из них:
//: typeinfo/toys/ToyTest.java
interface HasBatteries {}
interface Waterproof {}
interface Shoots {>
class Toy {
// Закомментируйте следующий конструктор по умолчанию,
// чтобы увидеть ошибку NoSuchMethodError ив (*1*)
Toy() {>
Toy(int i) {>
>
Gum.class;
Лично я предпочитаю использовать версию .class, так как она лучше сочетается
с обычными классами.
Интересно, что создание ссылки на объект Class с применением синтаксиса . class не
приводит к автоматической инициализации объекта class. В действительности под
готовка класса к использованию состоит из трех шагов.
1. выполняемая загрузчиком классов. Загрузчик находит байт-код
З а гр узк а ,
(обычно, но не обязательно, хранящийся на диске в каталогах CLASSPATH)
и создает на его основе объект Class.
class Initable {
static final int staticFinal = 47;
static final int staticFinal2 =
ClassInitialization.rand.nextInt(1000);
static {
System.out.pгintln("Инициaлизaция Initable");
>
>
class Initable2 {
static int staticNonFinal = 147;
static {
System.out.р г 1 п ^ п ( ”Инициализация Initable2");
>
>
class Initable3 {
static int staticNonFinal = 74;
static {
System.out.println("Инициaлизaция Initable3");
. >
>
Как видите, обычную ссылку на Class можно связать с любым другим объектом Class,
тогда как параметризованную ссылку можно связать только с объявленным типом.
Использование синтаксиса обобщенных типов позволяет компилятору реализовать
дополнительную проверку типа.
А если вы захотите немного ослабить ограничение? Казалось бы, можно использовать
решение следующего вида:
Class<Number> genericNumberClass = int.class;
Выглядит разумно, потому что Integer наследует от Number. Однако такое решение не
сработает, потому что объект Class для Integer не является субклассом объекта Class
для Number (эта тема более подробно рассматривается в главе 15).
462 Глава 14 • Информация о типах
BJava SE5 запись class<?> считается предпочтительной по сравнению с Class, хотя эти
две формы эквивалентны, а простое обозначение Class, как вы видели, не порождает
предупреждения компилятора. Преимущество записи Class<?> заключается в том, что
она ясно показывает: ссылка на неконкретный класс не используется случайно или по
незнанию. Разработчик сознательно выбирает неконкретную версию.
Чтобы создать ссылку на Class, ограниченную типом и л и л ю б ы м и е г о п о д т и п а м и ,
метасимвол ? следует объединить с ключевым словом extends для создания п р и в я зк и .
Таким образом, вместо class<Number> используется запись следующего вида:
//: typeinfo/BoundedClassReferences.java
class CountedInteger {
private static long counter;
private final long id = counter++;
public String toString() { return Long.toString(id); >
>
//: typeinfo/ClassCasts.java
нисходящее присваивание без явного приведения типа, которое сообщает, что по име
ющейся у вас дополнительной информации вы точно знаете, что объект относится к
конкретному фактическому типу (компилятор проверяет корректность нисходящего
преобразования и не разрешит выполнить приведение к типу, не являющемуся про
изводным классом).
Также в Java существует и третья форма RTTI. Она реализуется ключевым словом
instanceof, которое сообщает вам, принадлежит ли объект к некоторому определенному
типу. Результат запроса имеет логический (boolean) тип, поэтому вы просто «задаете»
вопрос в следующей форме:
if(x instanceof Dog)
((Dog)x).bark();
//: typeinfo/pets/Pet.java
package typeinfo.pets;
//: typeinfo/pets/Dog.java
package typeinfo.pets;
продолжение &
466 Глава 14 • Информация о типах
//: typeinfo/pets/Mutt.java
package typeinfo.pets;
//: typeinfo/pets/Pug.java
package typeinfo.pets;
//: typeinfo/pets/Cat.java
package typeinfo.pets;
//: typeinfo/pets/EgyptianMau.java
package typeinfo.pets;
//: typeinfo/pets/Manx.java
package typeinfo.pets;
//: typeinfo/pets/Cymric.java
package typeinfo.pets;
//: typeinfo/pets/Rodent.java
package typeinfo.pets;
//: typeinfo/pets/Rat.java
package typeinfo.pets;
II: typeinfo/pets/Mouse.java
package typeinfo.pets;
Также нам понадобится инструмент для случайного создания разных объектов, произ
водных от Pet, а также (для удобства) массивов и контейнеров List c элементами Pet.
Чтобы этот инструмент мог пережить несколько разных реализаций, мы определим
его в виде абстрактного класса:
//: typeinfo/pets/PetCreator.java
// Creates random sequences of Pets,
package typeinfo.pets;
import java.util.*;
//: typeinf0/PetC0unt.3ava
// Using instanceof.
import typeinfo.pets.*;
import java.util.*;
import static net.mindview.util.Print.*;
//: typeinfo/pets/Pets.java
// Фасад для создания PetCreator по умолчанию.
package typeinfo.petsj
import java.util.*;
Рекурсивный подсчет
Контейнер Мар в примере PetCount3.PetCounter был заранее заполнен разными классами
Pet. Вместо предварительного заполнения также можно воспользоваться методом
Class.isAssignableFrom() и создать инструмент общего назначения, который не огра
ничивается подсчетом Pet:
//: net/mindview/util/TypeCounter.java
// Подсчет экземпляров семейства типов,
package net.mindview.util;
import java.util.*;
Как видно из выходных данных, подсчитываются как базовые, так и реальные типы.
11. (2) Добавьте класс G e rb il в библиотеку typeinfo.pets и измените все примеры этой
главы так, чтобы в них использовался новый класс.
12 . (3) Используйте TypeCounter с классом CoffeeGenerator.java из главы 15.
13 . (3) Используйте TypeCounter с примером RegisteredFactories.java этой главы.
Зарегистрированные фабрики 475
Зарегистрированные фабрики
У системы генерирования объектов иерархии Pet есть один недостаток: каждый
раз, когда в иерархию добавляется новый тип Pet, вы должны добавить его данные
в LiteralPetCreator.java. При регулярном добавлении новых классов это требование может
создать проблемы.
Напрашивается мысль о добавлении в каждый субкласс статического инициализато
ра, который будет добавлять свой класс в некий список. К сожалению, статические
инициализаторы вызываются только при первой загрузке класса, поэтому возникает
классический «порочный круг»: класс отсутствует в списке генератора, в результате
генератор не может создать объект этого класса и класс не будет загружен и включен
в список.
По сути, вам придется создавать список самостоятельно, вручную (если только вам не
захочется написать программу, которая просматривает и анализирует кодовую базу,
а затем создает и компилирует список). Вероятно, лучшее, что можно сделать в такой
ситуации, —разместить список в каком-то очевидном месте. Пожалуй, для этой цели
лучше всего подойдет базовый класс иерархии.
Также мы внесем другое изменение — создание объекта делегируется самому классу
с использованием паттерна «Фабричный метод». Фабричный метод может вызываться
полиморфно и создает объект нужного типа. В этой очень простой версии «фабричным»
является метод create() интерфейса Factory:
//: typeinfo/factory/Factory.java
package typeinfo.factory;
public interface Factory<T> { T create(); > ///:~
В этом примере базовый класс Pet содержит контейнер List с «фабричными» объ
ектами. Фабрики типов, которые должны производиться методом createRandom(),
«регистрируются» в базовом классе посредством добавления в список partFactories:
//: typeinfo/RegisteredFactories.java
// Регистрация фабрик класса в базовом классе,
import typeinfo.factory.*;
import java.util.*;
class Part {
public String toString() {
return getClass().getSimpleName();
>
static List<Factory<? extends Part>> partFactories =
new ArrayList<Factory<? extends Part»();
static {
// Для Collections.addAll() выдается предупреждение
// "неконтролируемое создание обобщенного массива”
partFactories.add(new FuelFilter.Factory());
partFactories.add(new AirFilter.Factory());
partFactories.add(new CabinAirFilter.Factory());
partFactories.add(new OilFilter.Factory());
продолжение ^>
476 Глава 14 • Информация о типах
partFactories.add(new FanBelt.Factory());
partFactories.add(new PowerSteeringBelt.Factory());
partFactories.add(new GeneratorBelt.Factory());
>
private static Random rand = new Random(47);
public static Part createRandom() {
int n = rand.nextInt(partFactories.size());
return partFactories.get(n).create();
}
>
>
>
}
В данном примере Filter и Belt существуют только для классификации, поэтому про
грамма не должна создавать экземпляры этих классов, а только их субклассов. Если
класс должен создаваться методом createRandom(), то он содержит внутренний класс
Factory. Как видно из примера, имя Factory может использоваться только с уточнением
typeinfo.factory.Factory.
же это долго1. К счастью, с помощью отражения нам под силу написать простой ин
струмент, который будет показывать полный интерфейс класса. Вот как он работает:
//: typeinfo/ShowMethods.java
// Использование отражения для вывода на экран всех методов
// класса, включая те, что были определены в базовых классах.
// {Args: ShowMethods}
import java.lang.reflect.*;
import java.util.regex.*;
import static net.mindview.util.Print.*;
■Сказанное относится в основном к документации ранних B epcnftJava. Теперь фирма Sun зна
чительно улучшила НТМЬ-документацик^ауа и найти методы базовых классов стало проще.
482 Глава 14 ♦ Информация о типах
> / * Output:
public static void main(String[])
public native int hashCode()
public final native Class getClass()
public final void wait(long,int) throws
InterruptedException
public final void wait() throws InterruptedException
public final native void wait(long) throws
InterruptedException
public boolean equals(Object)
public String toString()
public final native void notify()
public final native void notifyAll()
public ShowMethods()
*///:~
Она придет на помощь, если вам не хочется рыскать по иерархиям классов в интерак
тивной документации или когда у вас возникнет желание узнать,например, о том, есть
ли у некоторого класса методы, которые возвращают объекты Color.
В главе 22 создается еще одна версия этой программы, на этот раз с графическим ин
терфейсом (специально адаптированная для графических компонентов библиотеки
Swing), что позволяет держать ее постоянно в памяти и при необходимости быстро
разыскивать методы какого-либо класса.
17. (2) Измените регулярное выражение в программе ShowMethods.java так, чтобы оно
дополнительно выделяло ключевые слова native и final (подсказка: используйте
оператор ИЛИ — | ).
ДО. (i) Отмените объявление ShowMethods как открытого (public) класса. Убедитесь
в том, что синтезированный конструктор по умолчанию пропадает из результатов.
19. (4) В программе ToyTestjava используйте отражение для создания объекта Toy кон
структором с аргументами.
20. (5) Найдите описание класса java.lang.ciass в докум ентации^К (http://java.sun.
сот). Напишите программу, которая получает имя класса в параметре командной
строки, а затем использует методы класса Class для вывода всей доступной ин
формации о классе. Протестируйте программу на одном из классов стандартной
библиотеки^уа и на своем собственном классе.
Динамические заместители
Заместитель (ргоху) является одним из основных паттернов проектирования. Он
представляет собой объект, который подставляется наместо «настоящего» объекта для
предоставления дополнительных или других операций —обычно подразумевающих
взаимодействие с «настоящим» объектом, поэтомузаместитель чаще всего выполняет
функции посредника. Простейший пример, демонстрирующий структуру заместителя:
//: typeinfo/SimpleProxyDemo.java
import static net.mindview.util.Print.*;
interface Interface {
void do5omething();
void somethingElse(String arg);
}
class RealObject implements Interface {
public void doSomething() { print("doSomething"); >
public void somethingElse(String arg) {
print("somethingElse " + arg);
}
}
class SimpleProxy implements Interface {
private Interface proxiedj
public SimpleProxy(Interface proxied) {
this.proxied = proxied;
}
продолжение ^>
484 Глава 14 • Информация о типах
Так как метод consumer() получает Interface, он не может знать, получает ли он RealOb-
ject или Proxy, поскольку оба класса реализуют Interface. Объект Ргоху, вставленный
между клиентом и RealObject, выполняет операции и вызывает идентичный метод
RealObject.
this.proxied = proxied;
>
public Object
invoke(Object proxy. Method method, Object[] args)
throws Throwable {
System.out.println("**** proxy: " + proxy.getClass() +
”, method: " + method + ", args: " + args);
if(args != null)
for(Object arg : angs)
System.out.println(" " + arg);
return method.invoke(proxied, args);
>
}
class SimpleDynamicProxy {
public static void consumer(Interface iface) {
iface.doSomething();
iface.somethingElse("bonobo");
>
public static void main(String[] args) {
RealObject real = new RealObject();
consumer(real);
// Insert a proxy and call again:
Interface proxy = (Interface)Proxy.newProxyInstance(
Interface.class.getClassLoader(),
new Class[]{ Interface.class >,
new DynamicProxyHandler(real));
consumer(proxy);
>
) /* Output: (95% match)
doSomething
somethingElse bonobo
**** proxy: class $Proxy0, method: public abstract void
Interface.doSomething(), args: null
doSomething
**** proxy: class $Proxy0, method: public abstract void
Interface.somethingElse(java.lang.String), args:
[Ljava.lang.Object;g>42e816
bonobo
somethingElse bonobo
*///:~
В данном случае проверяются имена методов, но также можно было проверять другие
аспекты сигнатуры и даже конкретные значения аргументов.
Динамический заместитель — не тот инструмент, который вы будете использовать
ежедневно, но он очень хорошо подходит для решения некоторых видов задач.
Паттерн «Заместитель» и другие паттерны проектирования представлены в книгах
«Thinking in Patterns» (см. wwwMindView.net) и «Приемы объектно-ориентированного
проектирования» Эрика Гаммы и др. (Питер, 2013).
21. (3) Измените пример SimpleProxyDemo.java, чтобы в нем измерялось время вызова
методов.
22. (3) Измените пример SimpleDynamicProxy.java, чтобы в нем измерялось время вызова
методов.
23. (3) Внутри метода invoke() из npHMepaSimpleDynamlcProxy.Java попробуйте вывести
аргумент-заместитель. Объясните, что при этом происходит.
Проект* 1. Напишите систему, использующую динамические заместители для реали
зации транзакций: заместитель закрепляет транзакцию, если опосредованный вызов
выполнен успешно (т. e. не возбудил исключений) или выполняет отмену в случае
неудачи. Закрепление и отмена должны работать для внешних текстовых файлов, что
выходит за границы исключений^ауа. Уделите особое внимание атомарности операций.
Null-объекты
Если отсутствие объекта представляется встроенным значением null, то ссылку при
каждом использовании приходится проверять на null. Многочисленные проверки
утомительны, а код становится громоздким. Проблема в том, что null не имеет соб
ственного поведения (если не считать возбуждения исключения NullPointerException
при попытке выполнения операции). Иногда в таких ситуациях помогает концепция
null-объекта2, принимающего сообщения для объекта, который он «изображает», но
возвращающего значения, указывающие на отсутствие «настоящего» объекта. Разра
ботчик может считать, что все объекты действительны и ему не нужно тратить время
на проверки null (а также чтение соответствующего кода).
И хотя б ы л о б ы занятно представить я з ы к программирования, к о т о р ы й автоматически
создает null-объекты за вас, н а п р а ктике и х повсеместное исп о л ь з о в а н ие не и м е е т
с м ы с л а — иногда п р о в е р к и на null вполне достаточно, иногда м о ж н о с д о с т а т о ч н ы м
основанием полагать, что с с ы л к а о т л ична от null, а иногда д о п у с т и м ы д а ж е р е ш е н и я
с в ы я в л е н и е м а н о м а л ь н ы х с и т у а ц и й через NullPointerException. П о х о ж е , null-объекты
приносят н а и б о л ь ш у ю пользу « в б л и з и к д а н н ы м » — д л я объектов, п р е д с т а в л я ю щ и х
class Person {
public final String first;
public final String last;
public final String address;
// И т.д.
public Person(String first, String last, String address){
this.first = first;
this.last = last;
this.address = address;
>
public String toString() {
return "Person: " + first + " " + last + " " + address;
>
public static class NullPerson
extends Person implements Null {
private NullPerson() { super("None", "None", "None"); }
public String toString() { return "NullPerson"; >
}
public static final Person NULL = new NullPerson();
> l//'.~
class Position {
private String title;
private Person person;
public Position(String jobTitle, Person employee) {
title = jobTitle;
person = employee;
if(person == null)
person = Person.NULL;
>
public Position(String jobTitle) {
title = jobTitle;
person = Person.NULL;
}
public String getTitle() { return title; }
public void setTitle(String newTitle) {
title = newTitle;
>
public Person getPerson() { return person; }
public void setPerson(Person newPerson) {
person = newPerson;
if(person == null)
person = Person.NULL;
>
public String toString() {
return "Position: ” + title + ” " + person;
>
} ///:~
Использовать null-объектдля Position не нужно, поскольку существование Person.NULL
подразумевает, что вакансия свободна (возможно, позднее выяснится необходимость
в добавлении явной реализации null-объекта для Position, но принцип YAGNI1 (You
Aren’t Going to Need It) требует использовать в прототипе «самое простое решение,
которое может сработать», и добавлять дополнительные возможности лишь при по
явлении реальной необходимости).
Класс s ta ff теперь может проверять null-объекты при заполнении вакансий:
//: typeinfo/Staff.java
import java.util.*;
//: typeinfo/Operation.java
public interface А {
void f();
> / / / :~
Затем интерфейс реализуется, но разработчик при желании может добраться до фак
тического типа реализации:
//: typeinfo/InterfaceViolation.java
// Программирование в обход интерфейса,
import typeinfo.interfacea.*;
class В implements А {
public void f() {>
public void g() {>
>
public class InterfaceViolation {
public static void main(String[] args) {
А а = new B();
a.f()j
// a.g(); // Ошибка компиляции
System.out.println<a.getClass().getName());
if(a instanceof В) {
В b = (B)a;
b.g();
>
}
> / * Output:
В
*///:~
Используя RTTI, мы обнаруживаем, что интерфейс а реализуется классом в.Выполняя
приведение типа к в,мы можем вызвать метод, отсутствующий в А. Все это абсолютно
законно и допустимо, но возможно, вам бы не хотелось предоставлять такую возмож
ность программистам-клиентам; ведь тем самым в программе образуется более высокий
уровень связности, чем вам хотелось бы. Другими словами, ключевое слово interface на
самом деле ни от чего не защищает, а факт использования в для реализации А в данном
случае по сути является обычной декларацией1.
Возможное решение проблемы — просто заявить, что если программист решает ис
пользовать фактический класс вместо интерфейса, то он действует на свой страх и риск.
Вероятно, это разумно во многих случаях, но если «вероятно» вас не устраивает, можно
применить более жесткие меры.
Проще всего использовать для реализации доступ на уровне пакета, чтобы внешние
клиенты не могли видеть ее:
//: typeinfo/packageaccess/HiddenC.java
package typeinfo.packageaccess;
i^>ort typeinfo.interfacea.*;
i430rt static net.mindview.util.Print.*;
class С implements А {
public void f() { print("public C.f()"); >
public void g Q { print("public C.g()"); >
void u() { print("package C.u()'')j >
protected void v() { print("protected C.v()")j >
private void w() { print("private C.w()"); }
}
public class HiddenC {
public static А makeA() { return new C(); }
> I I I :~
Единственная открытая (public) часть этого пакета HiddenC при вызове создает интер
фейс А. Здесь интересно то, что даже если makeA() будет возвращать с, за пределами
пакета не удастся использовать ничего, кроме А, так как имя С вне пакета будет недо
ступно.
Теперь при попытке нисходящего преобразования к С ничего не выйдет, потому что за
пределами пакета тип С недоступен:
//: typeinfo/HiddenImplementation.java
// Sneaking around package access.
uvort typeinfo.interfacea.*;
i*port typeinfo.packageaccess.*;
i*port java.lang.reflect.*;
Флаг -private означает, что программа должна выводить все члены, даже закрытые.
Результат выглядит так:
class typeinfo.packageaccess.C extends
java.lang.Object implements typeinfo.interfacea.A {
typeinfo.packageaccess.C( ) ;
public void f();
public void g();
void u();
protected void v();
private void w();
>
Итак, кто угодно может узнать имена и сигнатуры ваших самых закрытых методов
и вызвать их.
А если реализовать интерфейс как закрытый внутренний класс? Вот как это выглядит:
//: typeinfo/InnerImplementation.java
// Закрытые внутренние классы не скрыты от отражения.
import typeinfo.interfacea.*;
import static net.mindview.util.Print.*;
class InnerA {
private static class С implements А {
public void f() { print("public C.f()"); >
public void g() { print("public C.g()"); >
void u() { print("package C.u()")j >
protected void v() { print("protected C.v()")j }
private void w() { print("private C.w()''); }
}
public static А makeA() { return new C(); }
}
Интерфейсы и информация типов 497
class AnonymousA {
public static А makeA() {
return new A() {
public void f() { print("public C.f()"); >
public void g() { print("public C.g()"); >
void u() { print("package C.u()"); >
protected void v() { print("protected C.v()"); >
private void w() { print("private C.w()"); >
};
>
}
public class AnonymousImplementation {
public static void main(String[] args) throws Exception {
А а = AnonymousA.makeA();
a.f();
System.out.println(а .getClass().getName());
// Отражение позволяет получить доступ к анонимному классу:
HiddenImplementation.callHiddenMethod(a, "g");
HiddenImplementation.callHiddenMethod(a, "u");
HiddenImplementation.callHiddenMethod(a, "v");
HiddenImplementation.callHiddenMethod(a, "w") ;
}
> /* Output:
public C.f()
AnonymousA$l
public C.g()
package C.u()
protected C.v()
private C.w()
*///:~
498 Глава 14 • Информация о типах
class WithPrivateFinalField {
private int i = 1;
private final String s = "I'm totally safe";
private String s2 = "Am I safe?";
public String toString() {
return "i = " + i + ", " + s + ", " + s2;
>
}
public class ModifyingPrivateFields {
public static void main(String[] args) throws Exception {
WithPrivateFinalField pf = new WithPrivateFinalField();
System.out.println(pf);
Field f = pf.getClass().getDeclaredField("i'');
f.setAccessible(true);
System.out.println("f.getInt(pf): " +f.getInt(pf));
f.setInt(pf, 47);
System.out.println(pf);
f = pf.getClass().getDeclaredField("s");
f.setAccessible(true);
System.out.println("f.get(pf): " + f.get(pf));
f.set(pf, "No, you're not!");
System.out.println(pf);
f = pf.getClass().getDeclaredField("s2");
f.setAccessible(true);
System.out.println("f.get(pf): " + f.get(pf));
f.set(pf, "No, you're not!");
System.out.println(pf);
>
> /* Output:
i = 1, I'm totally safe, Am I safe?
f.getInt(pf): 1
i = 47, I'm totally safe, Am I safe?
f.get(pf): I'm totally safe
i = 47, I'm totally safe, Am I safe?
f.get(pf): Am I safe?
i = 47, I'm totally safe, No, you're not!
*///:~
25. (2) Создайте класс, содержащий методы с разными уровнями доступа: закрытым
(p riva te ), защищенным (protected), с доступом в пределах пакета Напишите код
для вызова этих методов из-за пределов пакета класса.
Резюме
Динамическое определение типов (R TTI) позволяет вам получить информацию
о точном типе объекта тогда, когда у вас имеется лишь ссылка на базовый класс. Таким
образом, оно может неправильно использоваться новичком, который еще не понял и не
успел оценить всю мощь полиморфизма. У многах людей, ранее работавших с про
цедурными языками, возникает сильное желание превратить свою программу в одну
(или несколько) большую конструкцию switch. Они могут это сделать с помощью RTTI,
из-за чего не дают в полной мере воспользоваться преимуществами полиморфизма.
В Java рекомендуется использовать именно полиморфные методы везде, где можно,
а к RTTI прибегать только при крайней необходимости.
Впрочем, при использовании полиморфных методов требуется полный контроль
над базовым классом, поскольку в некоторой точке программы, после наследования
очередного класса, вы можете обнаружить, что базовый класс не содержит нужного
вам метода, и тогда на помощь приходит RTTI: при наследовании вы расширяете
интерфейс класса, добавляя в него новые методы. Особенно верно это при использо
вании в качестве базовых классов библиотек, которые вы не можете изменить. Далее
в вашем коде в подходящий момент вы узнаете свой новый тип и вызываете для него
его собственный метод. Такой подход не противоречит основам полиморфизма и рас
ширяемости программы, так как добавление в программу нового типа не требует пре
рывания бесчисленного множества предложений switch. Однако, чтобы извлечь пользу
из дополнительной функциональности нового класса, придется использовать RTTI.
Добавление некоторого специфического метода в базовый класс будет выгодно только
одному производному классу, который действительно реализует его, но все остальные
производные классы будут вынуждены использовать для этого метода какую-либо бес
полезную «заглушку». Это «загрязняет» интерфейс базового класса и раздражает тех,
кому приходится переопределять ненужные абстрактные методы при наследовании
от базового класса. Например, рассмотрим иерархию классов, представляющих музы
кальные инструменты. Предположим, что вы хотите прочистить мундштуки духовых
инструментов своего оркестра. Конечно, можно поместить в базовый класс Instrument
(общее представление музыкального инструмента) еще один метод c le a rS p itV a lv e ()
(прочистка мундштуков), но тогда получится, что и у синтезатора, и у барабана есть
мундштук! С помощью RTTI можно получить гораздо более верное решение данной
задачи, поскольку этот метод уместно поместить в более конкретный класс (например,
в класс Wind, базовый для всех духовых инструментов). Однако еще более разумным
стало бы включение в класс Instrument метода preparelnstrum ent() (подготовить ин
струмент к игре), который подошел бы всем инструментам без исключения, но при
решении задачи вы могли не заметить этого и ошибочно решить, что в данном случае
без RTTI не обойтись.
Наконец, иногда RTTI решает проблемы производительности. Если ваш код использует
полиморфизм по всем правилам, но один из объектов чрезвычайно непродуктивно
500 Глава14 • Информацияотипах
обрабатывается кодом, предназначенным для базового типа, то для этого объекта можно
сделать исключение, определить его точный тип с помощью RTTI и работать с ним
более производительно. Однако ни в коем случае не следует слишком рано беспоко
иться об эффективности программ, как бы соблазнительно это ни выглядело. Сначала
надо получить работающую программу и только после этого решать, достаточно ли
быстро она работает, и разобраться с проблемами быстродействия, вооружившись
инструментами для проверки скорости исполнения (см. приложение http://M indView.
net/Books/BetterJava).
Вы также узнали, что отражение открывает совершенно новый круг возможностей,
основанных на более динамичном стиле программирования. Некоторых разработчиков
динамичность отражения беспокоит. Для того, кто привык к безопасности статической
проверки типов, возможность выполнения операций, которые могут проверяться только
во время выполнения, а обо всех ошибках сообщают посредством исключений, кажется
шагом в неверном направлении. Доходит до того, что, по мнению некоторых разра
ботчиков, сама возможность выдачи исключений времени выполнения говорит о том,
что такого кода следует избегать. На мой взгляд, это иллюзия; во время выполнения
всегда могут возникнуть какие-нибудь проблемы, порождающие исключения, —даже
в программе без блоков try и спецификаций исключений.
Также я полагаю, что существование последовательной модели получения информа
ции об ошибках позволяет писать динамический код средствами отражения. Конечно,
лучше писать код, который может проверяться статически... там, где это возможно. Но
я полагаю, что динамический код — один из важнейших аспектов, отделяющих Java
от таких языков, как С++.
26 . (3) Реализуйте clearSpitValve() так, как описано выше.
Обобщенные типы
Сравнение с С++
Создатели Java утверждают, что источником вдохновения их работы был язык С++.
Несмотря на это, Java можно изучать без знания С++; я решил именно так и посту
пить —кроме тех случаев, когда сравнение поможет глубже понять материал.
Обобщения требуют сравнения с С++ по двум причинам. Во-первых, понимание
некоторых особенностей шаблонов С++ (основного прототипа обобщений, включая
базовый синтаксис) поможет разобраться в основах самой концепции, а также (что
очень важно) в том, на что обобщения Java не способны — и почему. Я постараюсь
четко объяснить, где проходят эти границы, потому что, по моему опыту, понимание
этих границ повышает квалификацию программиста. Зная, что вы сделать не можете,
вы будете более эффективно использовать доступные возможности (отчасти потому,
что вы не тратите время, выбираясь из очередного тупика).
Вторая причина заключается в том, что в сообществе Java недостаточно хорошо по
нимают шаблоны С++, и это недопонимание мешает понять цели обобщений.
Итак, хотя в этой главе приведены примеры шаблонов С++, я постараюсь свести их
количество к минимуму.
Простые обобщения 503
Простые обобщения
Одна из самых убедительных причин для применения обобщений —классы контейне
ров, представленные в главе 11 (а также более подробно рассматриваемые в главе 17).
Контейнер представляет собой хранилище для объектов, с которыми работает про-
фамма. И хотя то же самое можно сказать и о массивах, контейнеры обладают большей
гибкостью и отличаются от простых массивов своими характеристиками. Практически
з любой программе возникает необходимость хранения используемых объектов, по
этому контейнеры относятся к числу наиболее часто используемых библиотек классов.
Рассмотрим класс, предназначенный для хранения одного объекта. Разумеется, в этом
классе должен быть указан точный тип хранимого объекта:
7: generics/Holderl.java
class Automobile {}
Но этот класс нельзя назвать универсальным, потому что никакие другие данные в нем
храниться не могут. Писать новый класс для каждого возможного типа не хотелось
бы. До BbixonaJava SE5 можно было бы просто хранить поле Object:
//: generics/Holder2.java
Иногда бывает нужно, чтобы в контейнере хранились объекты разных типов, но чаще
в контейнере размещаются объекты только одного типа. Одной из главных причин для
введения обобщений была возможность указать, какой тип объектов должен храниться
в контейнере, и чтобы эта информация контролировалась компилятором.
Итак, вместо Object было бы желательно использовать «условный» тип, с которым про
граммист может определиться позднее. Для этого параметр-тип указывается в угловых
504 Глава 15 • Обобщенные типы
2 . ( 1) Создайте класс для хранения трех объектов одного типа, с методами сохранения
и выборки этих объектов и конструктором для инициализации всех трех объектов.
Библиотека кортежей
На практике при вызове метода часто возникает необходимость вернуть несколько
объектов. Команда return позволяет вернуть только один объект, поэтому задачу при
ходится решать созданием объекта, содержащего несколько возвращаемых объектов.
Конечно, можно писать специальный класс каждый раз, когда возникнет такая ситуа
ция, нО обобщения позволяют решить задачу один раз и сэкономить время в будущем.
В то же время такое решение гарантирует безопасность типов во время компиляции.
Группа объектов, «завернутых» в один объект, называется кортежем (tuple). Полу
чатель объекта может читать элементы, но не может добавлять новые (эта концепция
также называется «объектом передачи данных»).
Обычно кортеж может иметь произвольную длину, но все объекты кортежа могут от
носиться к разным типам. Однако мы хотим задать тип каждого объекта и убедиться
в том, что при чтении значения получатель получит правильный тип. Для решения
Простые обобщения 505
class Amphibian {}
class Vehicle {}
System.out.println(k());
>
} /* Output: (80% match)
(hi, 47)
(Amphibianglf6a7b9, hi, 47)
(Vehicle035ce36, Amphibian^757aef, hi, 47)
(Vehicle09cabl6, Amphibian@la46e30, hi, 47, 11.1)
*///:~
Класс стека
Рассмотрим нечто менее тривиальное: традиционный стек. В главе 11 была пред
ставлена реализация стека на базе LinkedList — класс net.mindview.util.Stack. В том
примере класс LinkedList уже содержал необходимые методы для создания стека. Стек
конструировался посредством композиции одного обобщенного класса (Stack<T>)
с другим обобщенным классом (LinkedList<T>). Обобщенный тип вел себя как самый
обычный тип (за несколькими исключениями, которые будут рассмотрены ниже).
Вместо использования LinkedList мы также могли реализовать собственный механизм
внутреннего хранения данных.
//: generics/LinkedStack.java
// Стек, реализованный на базе связанного списка.
if(!top.end())
top = top.next;
return result;
}
public static void main(String[] args) {
LinkedStack<String> lss = new LinkedStack<String>();
for(String s : "Phasers on stun!".split(" "))
lss.push(s);
String s;
while((s = lss.pop()) != null)
System.out.println(s)j
>
} /* Output:
stun!
on
Phasers
*///:~
Внутренний класс Node тоже является обобщенным и имеет собственный параметр-тип.
В этом примере для определения пустого стека используется сторож (end sentinel).
Сторож создается при конструировании LinkedStack, и при каждом вызове push()
новый узел Node<T> создается и связывается с предыдущим узлом Node<T>. При вы
зове pop() всегда возвращается top.item, после чего текущий узел Node<T> удаляется
и происходит переход к следующему узлу; но при достижении сторожа перемещение
не выполняется. Если клиент будет и дальше вызывать pop(), он будет получать null
(признак пустого стека).
5 . (2) Удалите параметр-тип класса Node и измените остальной код LinkedStack.java так,
чтобы показать, что для внутреннего класса доступны обобщенные параметры-типы
внешнего класса.
RandomList
Рассмотрим другой пример: допустим, мы хотим создать особую разновидность списка,
которая случайным образом выбирает один из своих элементов при каждом вызове
select(). Чтобы реализация такого списка работала с любыми объектами, следует
использовать обобщения:
//: generics/RandomList.java
import java.util.*;
Обобщенные интерфейсы
Обобщения также работают с интерфейсами. Например, генератор представляет со
бой класс для создания объектов. В действительности он является разновидностью
паттерна проектирования «Фабричный метод», но запрашивая у генератора новый
объект, вы не передаете ему никаких аргументов, тогда как фабричный метод обычно
вызывается с аргументами. Генератор умеет создавать новые объекты без дополни
тельной информации.
Как правило, генератор определяет всего один метод, создающий новые объекты. Мы
назовем его next () и включим в стандартный инструментарий:
//: net/mindview/util/Generator.java
// Обобщенный интерфейс,
package net.mindview.util;
public interface Generator<T> { T next(); > ///:~
//: generics/coffee/Mocha.java
package generics.coffee;
public class Mocha extends Coffee {> ///:~
//: generics/coffee/Cappuccino.java
package generics.coffee;
public class Cappuccino extends Coffee {} ///:~
продолжение &
510 Глава 15 • Обобщенные типы
//: generics/coffee/Americano.java
package generics.coffee;
public class Americano extends Coffee {} ///:~
//: generics/coffee/Breve.java
package generics.coffee;
public class Breve extends Coffee {> ///:~
Теперь мы можем реализовать интерфейс Generator<Coffee> для создания случайных
типов объектов Coffee:
//: generics/coffee/CoffeeGenerator.java
// Генерирование разных типов Coffee:
package generics.coffee;
import java.util.*;
import net.mindview.util.*;
Mocha В
Mocha 4
Breve 5
Americano 6
Latte 7
Cappuccino 8
Cappuccino 9
*///:~
Обобщенные методы
До настоящего момента мы рассматривали параметризацию целых классов. Также
возможна параметризация методов внутри класса. Сам класс при этом может быть
обобщенным, а может и не быть —это не зависит от наличия обобщенных методов.
Обобщенный метод может изменяться независимо от класса. Как правило, применять
обобщенные методы следует там, где только возможно, —иначе говоря, если существует
возможность сделать обобщенным только метод вместо целого класса, вероятно, такое
решение будет менее громоздким.
Чтобы определить обобщенный метод, следует поместить список параметров-типов
перед возвращаемым значением:
//: generics/GenericMethods.java
(ключевое слово extends и вопросительные знаки будут описаны позже в этой главе).
Создается впечатление, что мы повторяемся, а компилятор мог бы определить один из
списков обобщенных аргументов по другому. К сожалению, он этого сделать не может,
но механизм автоматического определения аргументов-типов в обобщенных методах
5 14 Глава 15 ♦ Обобщенныетипы
Mocha 3
1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144,
*///:~
Вы видите, как обобщенный метод сокращает объем кода, необходимого для создания
объекта Generator. Обобщения Java все равно заставляют передать объект Class, так
что его можно с таким же успехом использовать для автоматического определения
типов в методе create().
14. (1) Измените пример BaslcGeneratorDemo.java, чтобы в нем использовалась явная
форма создания Generator (то есть используйте явный вызов конструктора вместо
обобщенного метода create()).
Операции с множествами
Рассмотрим еще один пример использования обобщенных методов: математические
отношения, выражаемые с использованием множеств (Set). Их удобно определить
в виде обобщенных методов, которые могут использоваться с любыми типами:
//: net/mindview/util/Sets.java
package net.mindview.util;
import java.util.*;
Для удобства (чтобы имена не надо было уточнять) в следующем примере исполь
зуется статическое импортирование. В примере используется EnumSet — инструмент
Java SE5 для простого создания объектов Set на базе перечислений (дополнительная
информация о EnumSet приведена в главе 19). В данном случае статический метод
Enumset.range() получает первый и последний элементы диапазона, создаваемого
в результирующем объекте Set:
//: generics/WatercolorSets.java
import generics.watercolors.*;
import java.util.*;
import static net.mindview.util.Print.*j
import static net.mindview.util.Sets.*;
import static generics.watercolors.Watercolors.*;
17. (4) Изучите документацик^ОК по EnumSet и найдите в ней метод clone(). Однако
для ссылки на интерфейс Set, передаваемой в Sets.java, метод clone() вызываться не
может. Удастся ли вам изменить код Sets.java таким образом, чтобы он обрабатывал
как общий случай интерфейса Set, так и особый случай EnumSet, используя clone()
вместо создания нового объекта HashSet?
class Customer {
private static long counter = l;
private final long id = counter++;
private Customer() {}
public String toString() { return "Customer " + id; }
// Метод для получения объектов Generator:
public static Generator<Customer> generator() {
return new Generator<Customer>() {
public Customer next() { return new Customer(); }
};
}
>
class Teller {
private static long counter = 1;
private final long id = counter++;
private Teller() {}
public String toString() { return "Teller " + id; }
// Объект Generator:
public static Generator<Teller> generator =
new Generator<Teller>() {
public Teller next() { return new Teller(); )
}J
}
public class BankTeller {
public static void serve(Teller t, Customer c) {
System.out.println(t + " serves " + c);
>
public static void main(String[] args) {
Random rand = new Random(47);
Queue<Customer> line = new LinkedList<Customer>();
Generators.fill(linej Customer.generator(), 15);
List<Teller> tellers = new ArrayList<Teller>();
Generators.fill(tellers, Teller.generator, 4);
for(Customer c : line)
serve(tellers.get(rand.nextInt(tellers.size())), c);
}
продолжение &
524 Глава 15 • Обобщенные типы
> /* Output:
Teller B serves Customer 1
Teller 2 serves Customer 2
Teller 3 serves Customer 3
Teller 1 serves Customer 4
Teller 1 serves Customer 5
Teller 3 serves Customer 6
Teller 1 serves Customer 7
Teller 2 serves Customer 8
Teller 3 serves Customer 9
Teller 3 serves Customer 10
Teller 2 serves Customer 11
Teller 4 serves Customer 12
Teller 2 serves Customer 13
Teller 1 serves Customer 14
Teller 1 serves Customer 15
*///:~
class Product {
private final int id;
private String description;
private double price;
public Product(int IDnumber, String descr, double price){
id = IDnumber;
description = descr;
this.price = price;
System.out.println(toString());
>
public String toString() {
return id + ": " + description + ", price: $" + price;
>
public void priceChange(double change) {
price += change;
>
public static Generator<Product> generator =
new Generator<Product>() {
private Random rand = new Random(47);
public Product next() {
return new Product(rand.nextInt(1000), "Test”,
Math.round(rand.nextDouble() * 1000.0) + 0.99);
>
};
}
class Shelf extends ArrayList<Product> {
public Shelf(int nProducts) {
Generators.fill(this, Product.generator, nProducts);
>
}
class Aisle extends ArrayList<Shelf> {
public Aisle(int nShelves, int nProducts) {
for(int i = 0; i < nShelves; i++)
add(new Shelf(nProducts));
}
^ продолжение &
526 Глава15 • Обобщенныетипы
class CheckoutStand {}
class Office {}
Загадка стирания
По мере более основательного изучения обобщений проявляются некоторые аспекты,
которые, на первый взгляд, выглядят бессмысленно. Скажем, хотя вы можете исполь
зовать конструкцию ArrayList.class, запись ArrayList<Integer>.class недопустима.
Также рассмотрим следующий пример:
//: generics/ErasedTypeEquivalence.java
import java.util.*;
Подход С++
Ниже приведен пример использования шаблонов С++. Как видите, синтаксис параме
тризованных типов очень похож, потому что источником вдохновения для создателей
Java был язык С++:
//: generics/Templates.cpp
#include <iostream>
using namespace std;
Класс Manipulator хранит объект типа т. Нас здесь интересует метод manipulate(), ко
торый вызывает метод f() для obj. Как он может определить, что у параметра-типа т
существует метод f()? Компилятор С++ осуществляет проверку при создании экзем
пляра шаблона, так что в точке создания экземпляра Manipulator<HasF> он убеждается
в том, что HasF содержит метод f(). Если это условие не выполняется, компилятор
выдает сообщение об ошибке; таким образом обеспечивается безопасность типов.
Подход С++ 529
На С++ такой код пишется тривиально, потому что при создании шаблона код шаблона
знает тип своих параметров. С обобщениями^уадело обстоит иначе. Ниже приведен
аналог HasF:
//: generics/HasF.java
class Manipulator<T> {
private T obj;
public Manipulator(T x) { obj = x; }
// Ошибка: не удается найти метод f():
public void manipulate() { obj.f()j }
>
public class Manipulation {
public static void main(String[] args) {
HasF hf = new HasF();
Manipulator<HasF> manipulator =
new Manipulator<HasF>(hf);
manipulator.manipulate();
>
> ///:~
Из-за стирания компилятор Java не может связать требование, согласно которому
метод manipulate() должен быть способен вызвать f() для obj, с тем фактом, что
класс HasF содержит метод f(). Чтобы вызвать f(), необходимо помочь обобщенному
классу — передать ему ограничение, которое сообщает компилятору, что он должен
принимать только типы, удовлетворяющие этому ограничению. Для этой цели снова
используется ключевое слово extends. Благодаря введению ограничения следующий
код успешно компилируется:
//: generics/Manipulator2.java
//: generics/Manipulator3.java
class Manipulator3 {
private HasF obj;
public Manipulator3(HasF x) { obj = x; }
public void manipulate() { obj.f(); }
> ///:-
Мы подошли к важному моменту: обобщения приносят пользу только тогда, когда вы
хотите использовать параметры-типы, более «универсальные», чем конкретный тип
(и все его подтипы), —то есть если код должен работать в границах нескольких классов.
В результате параметры-типы и их применение в практическом обобщенному коде
обычно сложнее простой замены класса. Впрочем, это не значит, что любая конструк
ция вида <T extends HasF> бесполезна. Например, если класс содержит метод, который
возвращает т, обобщения приносят пользу, потому что они возвращают точный тип:
//: generics/ReturnGenericType.java
Миграционная совместимость
Чтобы устранить все возможные недоразумения по поводу стирания, необходимо четко
понимать, что оно не является особенностью языка. Это компромисс в реализации
обобщений^уа, неизбежный из-за того, что обобщения не были частью языка с самого
начала. Этот компромисс принесет вам немало неприятностей, поэтому постарайтесь
пораньше привыкнуть к нему и понять, почему он появился.
Если бы обобщения были частью Java 1.0, то они не были бы реализованы на базе
стирания — создатели языка воспользовались бы конкретизацией (reification) для
поддержания параметров-типов на уровне полноправных сущностей,чтобы с ними
можно было выполнять типизованные языковые и рефлексивные операции. Позже
в этой главе будет показано, что стирания понижают уровень «обобщенности». Обоб
щения java все равно полезны — но не настолько, насколько могли бы, и причиной
этому является стирание.
В реализации со стиранием обобщенные типы рассматриваются как «второсортные»
типы, которые не могут использоваться в некоторых важных контекстах. Обобщенные
Подход С++ 531
Проблема стирания
Итак, главной причиной для реализации стирания является процесс перехода от
необобщенного кода к обобщенному и возможность встраивания обобщений в язык
без нарушения работоспособности существующих библиотек. Благодаря стиранию
существующий необобщенный клиентский код используется без изменений, пока
клиенты не будут готовы переписать код для поддержки обобщений. Это стоящая
цель, потому что она предотвращает внезапное нарушение работоспособности всего
существующего кода.
532 Глава 15 • Обобщенные типы
код класса Foo д о л ж е н знать, что теперь о н работает с Cat. С и н т а к с и с создает впечат
ление, что тип т автоматически подставляется везде, где он используется в классе. Но
это не так, и при написании кода класса вы должны постоянно напоминать себе: «Нет,
это всего лишь Object».
Кроме того, стирание и миграционная совместимость означают, что правильность ис
пользования обобщений не обеспечивается там, где это было бы желательно:
//: generics/ErasureAndInheritance.java
class GenericBase<T> {
private T element;
public void set(T arg) { arg = element; }
public T get() { return element; }
>
class Derivedl<T> extends GenericBase<T> {}
Граничные ситуации
На мой взгляд, из-за стирания самый нелогичный аспект обобщений заключается
в возможности представления того, что не имеет смысла. Пример:
//: generics/ArrayMaker.java
import java.lang.reflect.*;
import java.util.*;
//: generics/ListMaker.java
import java.util.*;
Хотя компилятор не может ничего знать о T внутри create(), он все равно может
проследить (во время компиляции) за тем, что значения, помещаемые в result, от
носятся к типу Т, и поэтому соглашается с ArrayList<T>. Итак, хотя стирание удаляет
информацию о фактическом типе внутри метода или класса, компилятор все равно
может обеспечить внутреннюю целостность использования типа в методе или классе.
Так как стирание удаляет информацию о типе в теле метода, во время выполнения
важны гр а н и ч н ы е т о ч к и , в которых объекты входят в метод и покидают его. В этих
точках компилятор выполняет проверку типа во время компиляции и вставляет код
приведения типа. Рассмотрим следующий необобщенный пример:
//: generics/SimpleHolder.java
Методы set() и get() просто сохраняют значение и выдают его, а приведение типа
проверяется в точке вызова get ().
Теперь вставим в этот код поддержку обобщений:
//: generics/GenericHolder.java
Компенсация стирания
Как мы видели, стирание приводит к невозможности выполнения некоторых опера
ций в обобщенном коде. Любые операции, требующие знания точного типа во время
выполнения, работать не будут:
//: generics/Erased.java
// {CompileTimeError} (не компилируется)
class Building {}
class House extends Building {}
int main() {
Foo<Bar> fb;
Foo<int> fi; // ...и работает с примитивами
> /l/:~
class ClassAsFactory<T> {
T x;
public ClassAsFactory(Class<T> kind) {
try {
x = kind.newInstance();
> catch(Exception e) {
throw new RuntimeException(e);
>
>
': generics/FactoryConstraint.java
2*terface FactoryI<T> {
T create();
^ass Foo2<T> {
private T x;
public <F extends FactoryI<T>> Foo2(F factory) {
x = factory.create();
>
/ / ...
Это решение — всего лишь разновидность передачи Class<T>. В обоих решениях ис
пользуются фабрики; просто Class<T> — встроенный объект-фабрика, а в приведенном
решении фабрика создается явно. Но в этом случае вы можете пользоваться преиму
ществами проверки на стадии компиляции.
Другое возможное решение основано на паттерне проектирования «Шаблонный метод».
В следующем примере get() является шаблонным методом, а create() определяется
в субклассе для создания объекта соответствующего типа:
//: generics/CreatorGeneric.java
System.out.println(element.getClass().getSimpleName());
}
>
public class CreatorGeneric {
public static void main(Stning[] args) {
Creator с = new Creator();
c.f();
}
> /* Output:
X
"///:•
22. (6) Используя метку типа в сочетании с отражением, создайте метод, использую
щий версию newInstance() с аргументами для создания объекта класса, конструктор
которого получает аргументы.
23. (1) Измените пример FactoryConstraint.java, чтобы метод create() получал аргумент.
24. (3) Измените упражнение 21 так, чтобы объекты-фабрики хранились в Мар вместо
Class<?>.
Массивы обобщений
Как было показано в Erased.java, создать массив обобщений невозможно. Как правило,
проблема решается использованием ArrayList везде, где возникает потребность в соз
дании массива обобщений:
//: generics/ListOfGenerics.java
import java.util.*;
class Generic<T> {}
public class ArrayOfGenericReference {
static Generic<Integer>[] giaj
> ///:~
Как и прежде, использовать конструкцию T[] array = new T[sz] нельзя, так что м ы
соЗдаем массив объектов и выполняем приведение типа.
Метод rep() возвращает T[], что для gai в main() соответствует Integer[], но при по
пытке вызвать его и сохранить результат по ссылке на Integer[ ] выдается исключение
Cl as sCastException — снова потому, что фактическим типом времени выполнения
является Object [].
Если откомпилировать пример GenericArray.java с закомментированной аннотацией
@SuppressWarnings, компилятор выдает предупреждение:
1 warning
GenericArray2<Integer> gai =
new GenericArray2<Integer>(10);
for(int i = 0; i < 10; i ++)
gai.put(i, i);
for(int i = 0; i < 10; i ++)
System.out.print(gai.get(i) + " ");
System.out.println();
try {
Integer[] ia = gai.rep();
> catch(Exception e) { System.out.println(e); }
}
} /* Output: (Sample)
0 1 2 3 4 5 6 7 8 9
java.lang.ClassCastException: [Ljava.lang.Object; cannot be
cast to [Ljava.lang.Integer;
*///:~
Ограничения
Ограничения были кратко представлены ранее в этой главе. Они сужаютдиапазон па
раметров-типов, которые могут использоваться с обобщениями. И хотя это позволяет
вам задавать свои правила относительно типов, к которым могут применяться обоб
щения, более важным может оказаться другой эффект: возможность вызова методов
из ограниченных типов.
Так как стирание удаляет информацию типа, для неограничиваемых параметров обоб
щений можно вызывать только методы, доступные для Object. Но при ограничении
параметра подмножеством типов вы сможете вызывать методы этого подмножества.
1 http://gafter.blogspot.com/2004/09/puzzling-through-erasure-answer.html
Ограничения 545
// Множественные ограничения:
class ColoredDimension<T extends Dimension & HasColor> {
T item;
ColoredDimension(T item) { this.item = item; }
T getItem() { return item; >
java.awt.Color color() { return item.getColor(); }
int getX() { return item.x; }
int getY() { return item.y; >
int getZ() { return item.z; }
}
interface Weight { int weight(); >
class HoldItem<T> {
T item;
HoldItem(T item) { this.item = item; >
T getItem() { return item; >
>
class Colored2<T extends HasColor> extends HoldItem<T> {
Colored2(T item) { super(item); >
java.awt.Color color() { return item.getColor(); }
>
class ColoredDimension2<T extends Dimension & HasColor>
extends Colored2<T> {
ColoredDimension2(T item) { super(item); )
int getX() { return item.x; )
int getY() { return item.y; )
int getZ() { return item.z; )
>
class Solid2<T extends Dimension & HasColor & Weight>
extends ColoredDimension2<T> {
Solid2(T item) { super(item); )
int weight() { return item.weight(); )
>
public class InheritBounds {
public static void main(String[] args) {
Solid2<Bounded> solid2 =
new Solid2<Bounded>(new Bounded());
solid2.color();
solid2.getY();
solid2.weight();
>
} ///:~
Маски
Мы уже видели простые примеры использования масок (вопросительные знаки в вы
ражениях обобщенных аргументов) в главах 11 и 14. В этом разделе тема рассматри
вается более подробно.
Начнем с примера, демонстрирующего особое поведение массивов: массив производ
ного типа можно присвоить ссылке на массив базового типа:
//: generics/CovariantArrays.java
Первая строка main() создает массив Apple и присваивает его ссылке на массив Fruit.
Это разумно —яблоко (Apple) является разновидностью фрукта (Fruit), так что массив
элементов Apple также может рассматриваться как массив Fruit.
Но если фактическим типом массива является Арр1е[],то в массив могут помещаться
только объекты типа Apple или типов, производных от Apple, что фактически работает
как во время компиляции, так и во время выполнения. Но обратите внимание: ком
пилятор позволяет поместить объект Fruit в массив. Для компилятора это выглядит
Маски 549
p u b lic c la s s N o n C o v a r ia n t G e n e r ic s {
// Ошибка ком п ил яц ии : н е со вм е сти м ы е типы :
L is t < F r u it > flis t = new A r r a y L i s t < A p p l e > ( ) ;
} /l/:~
И хотя на первый взгляд это читается как «Контейнер с элементами Apple нельзя
присвоить контейнеру Fruit», помните, что применение обобщений не сводится
к контейнерам. По сути говорится другое: «Обобщение с Apple не может быть при
своено обобщению с Fruit». Если (как в случае в массивами) компилятор знает о коде
достаточно, чтобы понять, что в нем задействованы контейнеры, возможно, он мог бы
немного смягчить требования. Но компилятор такой информацией не располагает
и поэтому отказывается разрешать «восходящее преобразование». Впрочем, на деле
это не является «восходящим преобразованием» — контейнер List с элементами
Apple не является List с элементами Fruit. В контейнере List с элементами Apple
могут храниться объекты типа Apple и типов, производных от Apple, а в контейнере
List с элементами Fruit могут храниться любые разновидности Fruit. Да, в том числе
и Apple, но от этого контейнер не становится контейнером List с элементами Apple; он
остается контейнером List с элементами Fruit. Контейнер List с элементами Apple не
эквивалентен по типу контейнеру List с элементами Fruit, несмотря на то что Apple
является разновидностью Fruit.
Настоящая проблема заключается в том, что речь идет о типе контейнера, а не о типе
элементов, хранящихся в контейнере. В отличие от массивов обобщения не обладают
встроенной ковариантностью. Это связано с тем, что массивы полностью определяются
в языке, а следовательно, для них реализованы встроенные проверки как на стадии
компиляции, так и на стадии выполнения, тогда как с обобщениями компилятор и ис
полнительная система не знают, что вы хотите делать со своими типами и какие при
этом должны использоваться правила.
5 50 Глава 15 • Обобщенные типы
p u b lic c la s s G e n e r ic s A n d C o v a r ia n c e {
p u b lic s t a t ic v o id m a in ( S t r in g [ ] a rg s) {
/ / М а ск и о б е с п е ч и в а ю т к о в а р и а н т н о с т ь :
L is t < ? e x te n d s F r u it > flis t = new A r r a y L i s t < A p p l e > ( ) j
/ / Ошибка к о м п и л я ц и и ; д о б а в л е н и е о б ъ е к т о в н е в о зм о ж н о :
// f lis t .a d d ( n e w A p p le ( ) ) j
// f li s t . a d d ( n e w F r u it ( ) ) ;
// f li s t . a d d ( n e w O b je c t());
flist.add(null); // Допустимо, но неинтересно
/ / Мы з н а е м , что возвр ащ ается к а к минимум F r u i t :
F r u it f = flis t .g e t ( 0 ) ;
>
> I I I :~
Теперь f l i s t имеет тип L i s t < ? e x t e n d s F r u i t > , что можно прочитать как «список
с элементами любого типа, производного от F r u i t » . Однако это не означает, что в L i s t
можно будет хранить любые виды F r u i t . Маска относится к определенному типу, так
что это означает «некоторый конкретный тип, не указанный для ссылки f l i s t » . Итак,
присваиваемый объект L i s t должен содержать некоторый указанный тип (например,
F r u i t или A p p l e ) , но для восходящего преобразования к f l i s t этот тип несущественен.
что можно сделать с таким контейнером L i s t ? Если вы не знаете, какой тип хранится
в L i s t , как безопасно добавить объект? Как и в случае с «восходящим преобразовани
ем» массива в CovariantArrays.java, это невозможно —только на этот раз запрет исходит
от компилятора, а не от исполнительной системы. Проблема обнаруживается быстрее.
Кто-то скажет, что это уже перебор, потому что теперь мы не можем даже добавить A p p le
в контейнер L i s t , в котором, как только что было сказано, будут храниться объекты
A p p l e . Да, но ведь компилятор этого не знает! L i s t < ? e x t e n d s F r u i t > может на законных
основаниях указывать на L is t < O r a n g e > . После такого «восходящего преобразования»
теряется способность передачи чего-либо, даже O b j e c t .
С другой стороны, при вызове метода, возвращающего F r u i t , такой подход безопасен;
известно, что все объекты в списке должны относиться как минимум к типу F r u i t ,
поэтому компилятор не возражает.
26. (2) Продемонстрируйте ковариантность массивов на примере N u m b e r и I n t e g e r .
27. (2) Покажите, что ковариантность не работает с L is t на примере N u m b e r и In te g e r,
затем добавьте щаблоны.
//: generics/CompilerIntelligence.java
import java.util.*;
У contains() и indexOf() аргументы относятся к типу Object, так что маски не задей
ствованы и компиляторы разрешают вызов. Это означает, что проектировщик обобщен
ного класса сам решает, какие вызовы «безопасны», и использует типы Object для их
аргументов. Чтобы запретить вызов при использовании типа с масками, используйте
параметр-тип в списке аргументов.
Для примера рассмотрим очень простой класс Holder:
//: generics/Holder.java
Класс Holder содержит метод set(), который получает T; метод get(), который полу
чает т, и метод equals(), который получает Object. Как вы уже видели, при создании
объекта Hold er <A pp le > вы не сможете выполнить его восходящее преобразование
в Holder<Fruit>, но можно повыситьдо Holder<? extends Fruit>. Если вызвать get(), ме
тод вернет Fruit —это максимальная информация, которой он располагает при наличии
ограничения «любой класс, расширяющий Fruit». Если у вас больше информации, вы
можете преобразовать его к конкретному подтипу Fruit без выдачи предупреждений,
но возникает исключение ClassCastException. Метод set() не будет работать с Apple
или Fruit, потому что аргумент set() тоже объявлен как «? Extends Fruit»; это озна
чает, что им может быть что угодно, а компилятор не может проверить безопасность
типов для «чего угодно».
Однако метод equals() работает нормально, потому что он получает аргумент Object
вместо т. Таким образом, компилятор только обращает внимание на типы передаваемых
и возвращаемых объектов. Он не анализирует код, чтобы проверить, выполняются ли
в нем фактические операции чтения или записи.
Контравариантность
Также можно пойти другим путем и использовать ограничения супертипа. В этом слу
чае разработчик устанавливает ограничение по любому базовому классу некоторого
классаконструкцией<? super Мус1а55>илидажепараметру-типу:<? super т>(хотя
вы не сможете задать ограничение супертипа для обобщенного параметра, то есть
конструкция <T super MyClass> запрещена). Это позволяет безопасно передать типи
зованный объект обобщенному типу. Таким образом, ограничение супертипа делает
возможной запись в Collection:
//: generics/SuperTypeWildcards.java
import java.util.*;
s ta tic void f l ( ) {
Apple а = readExact(apples);
F ru it f = readExact(fruit)j
f * readExact(apples);
>
// Но если речь идет о классе, тип устанавливается
// при создании экземпляра класса:
sta tic class Reader<T> {
T readExact(List<T> l i s t ) { return lis t.g e t( 0 ) ; }
>
s ta tic void f2() {
Reader<Fruit> fruitReader = new Reader<Fruit>();
Fru it f = fruitR eader.readExact(fruit);
// F ru it а = fruitReader.readExact(apples); // Ошибка:
// readExact(List<Fruit>) не может
// применяться к (List<Apple>).
}
s ta tic class CovariantReader<T> {
T readCovariant(List<? extends T> l i s t ) {
return lis t.g e t( 0 ) ;
>
>
s ta tic void f3() {
CovariantReader<Fruit> fruitReader =
new CovariantReader<Fruit>()j
Fru it f = fruitR eader.readCovariant(fruit)j
Fru it а = fruitReader.readCovariant(apples);
}
public sta tic void main(String[] args) {
f l ( ) ; f2 (); f3 ();
>
} I I I :~
Как и прежде, первый метод readExact() использует точный тип. Итак, если исполь
зуется точный тип без масок, вы сможете читать и записывать этот точный тип в L is t.
Кроме того, для возвращаемого значения статический обобщенный метод readExact()
фактически «адаптируется» к каждому вызову и возвращает Apple из List<Apple> и Fru it
из List< Fruit> , как показано в f l ( ) . Таким образом, при использовании статического
обобщенного метода ковариантность нужна не всегда.
Но если вы работаете с обобщенным классом, параметр задается для класса при созда
нии экземпляра этого класса. Как показано в f2 (), экземпляр fru itR e ad e r может про
читать F r u it из List< Fruit> , поскольку это его точный тип. Но контейнер List<Apple>
также должен производить объекты F ru it, а fru itR e ad e r этого не позволяет.
Для решения проблемы метод CovariantReader.readCovariant() получает List<? ex
tends T>, поэтому читать т из этого списка безопасно (все объекты в списке как минимум
имеют тип т, а могут относиться к типу, производному от т). В методе f3 () мы видим,
что теперь тип F ru it можно прочитать из List<Apple>.
28. (4) Создайте обобщенный класс Genericl<T> с одним методом, получающим
аргумент типа T. Создайте второй обобщенный класс Generic2<T> с одним методом,
возвращающим аргумент типа т. Напишите обобщенный метод с контравариантным
аргументом первого обобщенного класса, который вызывает его метод. Протести
руйте на типах библиотеки typeinfo.pets.
Маски 555
Неограниченные маски
Неограниченная маска <?> означает «что угодно», так что может показаться, что
использование неограниченной маски эквивалентно использованию самого типа.
Действительно, компилятор на первый взгляд соглашается с этим предположением:
//: generics/UnboundedWildcardsl.java
import java.util.*;
//: generics/UnboundedWildcards2.java
import java.util.*;
rawArgs(raw, lng);
rawArgs(qualified, lng);
rawArgs(unbounded, lng);
rawArgs(bounded, lng);
unboundedArg(raw, lng);
unboundedArg(qualified, lng);
558 Глава 15 • Обобщенные типы
unboundedArg(unbounded, lng);
unboundedArg(bounded, lng);
Фиксация
Есть ситуация, в которой использование <?> вместо неспециализированного типа
особенно принципиально. Если передать неспециализированный тип методу, исполь-
зующему<?>, компилятор может автоматически вычислить фактический параметр-тип
и вызвать другой метод, использующий точный тип. Следующий пример демонстрирует
работу этого механизма, который называется фиксацией (capture conversion), потому
что неуказанный тип маски фиксируется и преобразуется к точному типу. В этом
случае комментарии о предупреждениях действуют только при удалении аннотации
@SuppressWarnings:
//: generics/CaptureConversion.java
т из f2(), потому что тип т неизвестен для f2(). Фиксация — интересный механизм,
но его возможности невелики.
29. (5) Создайте обобщенный метод, который получает аргумент Holder<List<?>>.
Определите, какие методы могут или не могут вызываться для Holder и для List.
Повторите для аргумента List<Holder<?>>.
Проблемы
В этом разделе рассматриваются разнообразные проблемы, встречающиеся при ис
пользовании o6o6nieHHftJava.
//: generics/ByteSet,java
import java.util.*;
2654
3909
5202
2209
5458
V //: ~
Так как RandomGenerator.lnteger реализует Generator<Integer>, я надеялся, что автомати
ческая упаковка преобразует значение next() из Integer в int. Однако автоматическая
упаковка не применяется к массивам, поэтому решение не работает.
30. (2) Создайте объект Holder для каждой «обертки» примитивного типа. Продемон
стрируйте, что автоматическая упаковка и распаковка работает для методов set ()
и get() каждого экземпляра.
class FixedSizeStack<T> {
private int index = 0;
private 0bject[] storage;
продолжение #
564 Глава 15 • Обобщенные типы
Вы обязаны выполнить приведение типа, но при этом вам подсказывают, что делать
этого не следует. Для решения следует применить новую форму приведения типа,
появившуюся Bjava SE5, —приведение через обобщенный класс:
//: generics/ClassCasting.java
import java.io.*;
import java.util.*j
Перегрузка
Следующая программа не откомпилируется, хотя на первый взгляд выглядитлогично:
//: generics/UseList.java
// {CompileTimeError} (не компилируется)
import java.util.*;
//: generics/UseList2.java
import java.util.*;
Есть смысл сузить тип, с которым может сравниваться субкласс ComparablePet. Напри
мер, Cat может сравниваться только с другими объектами Cat:
//: generics/HijackedInterface.java
// {CompileTimeError} (не компилируется)
Самоограничиваемые типы
Существует одна довольно заумная идиома, которая периодически встречается в обоб-
щ ениях^уа. Вот как она выглядит:
class SelfBounded<T extends SelfBounded<T>> { // ...
Напоминает два зеркала, обращенных друг к другу; своего рода бесконечное отражение.
Класс SelfBounded получает обобщенный аргумент т; T ограничивается по SelfBounded,
получающего аргумент т.
Когда вы впервые сталкиваетесь с этой идиомой, понять ее довольно трудно. Это
лишний раз указывает, что ключевое слово extends в ограничениях играет совершенно
иную роль, чем при создании субклассов.
class GenericType<T> {}
//: generics/CRGWithBasicHolder.java
Самоограничение
BasicHolder м о ж е т использовать в своем о б о б щ е н н о м параметре л ю б о й тип, как п о
казывает с л е д у ю щ и й пример:
//: generics/Unconstrained.java
class Other {}
class BasicOther extends BasicHolder<Other> {>
//: generics/SelfBounding.java
//: generics/NotSelfBounded.java
Ковариантность аргументов
Полезность самоограничиваемых аргументов заключается в том, что они производят
ковариантные типы аргументов —типы аргументов методов меняются в соответствии
с субклассами.
И хотя самоограничиваемые типы также производят возвращаемые типы, соответ
ствующие типу субкласса, это не так важно, потому что ковариантные возвращаемые
типы были представлены в Jave SE5:
//: generics/CovariantReturnTypes.java
urterface OrdinaryGetter {
Base get()j
class OrdinarySetter {
void set(Base base) {
System.out.println("OrdinarySetter.set(Base)");
>
>
class DerivedSetter extends OrdinarySetter {
void set(Derived derived) {
System.out.println("DerivedSetter.set(Derived)");
}
}
продолжение ^>
572 Глава 15 • Обобщенные типы
import java.util.*;
ЗаПустив программу, вы увидите, что вставка Cat проходит незамеченной для dogsl, а dogs2
немедленно возбуждает исключение при вставке неправильного типа. Также видно, что
объекты производного типа могут помещаться в контейнер с проверкой базового типа.
35. ( 1) Измените пример CheckedListjava так, чтобы в нем использовались классы coffee,
определенные в этой главе.
Исключения
Из-за стирания возможности использования обобщений с исключениями сильно
ограниченны. Блок catch не может перехватывать исключение обобщенного типа, по
тому что тип исключения должен быть известен и во время компиляции, и во время
выполнения. Кроме того, обобщенный класс не может прямо или косвенно наследо
вать от Throwable (это препятствует определению обобщенных исключений, которые
невозможно перехватить).
Однако параметры-типы могут использоваться в секции throws объявления метода. Это
позволяет писать обобщенный код, изменяемый по типу контролируемого исключения:
//: generics/ThrowGenericException.java
import java.util.*;
} catch(Failure2 e) {
System.out.println(e);
>
}
> ///:~
Примеси
Похоже, со временем термин « п р и м е съ ъ (mixin) обрел много разных значений, но ос
новной смысл подразумевает смешение функциональности нескольких классов для
получения итогового класса, представляющего все типы примесей. Часто это делается
в последнюю минуту, что позволяет легко и быстро объединять классы.
Одна из полезных особенностей примесей заключается в том, что они единообразно
применяют характеристики и поведение нескольких классов. Кроме того, если вы за
хотите что-то изменить в классе примеси, эти изменения распространяются по всем
классам, к которым применяется примесь. По этой причине примеси в определенном
смысле связаны с аспектно-ориентированным программированием (АОП), а аспекты
часто рекомендуют для решения проблемы примесей.
Примеси в С++
Именно использование примесей было одним из самых сильных аргументов в пользу
множественного наследования в С++. Однако более интересный и элегантный под
ход к реализации примесей основан на использовании параметризованных типов,
где примесь представляется классом, производным от параметра-типа. В С++ можно
легко создавать примеси, потому что С++ запоминает тип параметров своих шаблонов.
В следующем примере С++ представлены два типа примесей: один «подмешивает»
свойство временной метки, а другой —серийный номер каждого экземпляра:
//: generics/Mixins.cpp
#include <string>
#include <ctime>
#include <iostream>
using namespace stdj
class Basic {
string value;
3ublic:
void set(string val) { value = val; >
string get() { return value; >
int main() {
TimeStamped<SerialNumbered<Basic> > mixinl, mixin2;
mixinl.set("test string 1");
mixin2.set("test string 2");
cout << mixinl.get() << " " << mixinl.getStamp() «
" " « mixinl.getSerialNumber() << endl;
cout << mixin2.get() « " " << mixin2.getStamp() <<
" " « mixin2.getSerialNumber() « endl;
> /* Output: (Sample)
test string 1 1129840250 1
test String 2 1129840250 2
*///:~
B main() итоговые типы mixinl и mixin2 содержат все методы примесных типов. При
месь можно рассматривать как функцию, отображающую существующие классы на
новые субклассы. Обратите внимание, насколько тривиально создаются примеси этим
способом; по сути вы просто говорите: «Вот что мне нужно», —а остальное происходит
автоматически:
TimeStamped<SerialNumbered<Basic> > mixinl, mixin2;
Класс Mixin фактически использует делегирование, так что каждый примесный тип
требует поля в Mixin, и вы должны написать все необходимые методы в Mixin для пере
дачи вызовов соответствующим объектам. В этом примере используются тривиальные
классы, но при более сложных примесях код быстро разрастается1.
37 . (2) Добавьте в Mixins.java новый класс примеси Colored. Подмешайте его в Mixin
и покажите, что программа работает.
1 Некоторые среды разработки —такие, как Eclipse и IntelliJ Idea, —автоматически генерируют
код делегирования.
Исключения 579
class Basic {
private String value;
public void set(String val) { value => val; >
public String get() { return value; >
}
class Decorator extends Basic {
protected Basic basic;
public Decorator(Basic basic) { this.basic = basic; >
public void set(String val) { basic.set(val); >
public String get() { return basic.get(); )
>
class TimeStamped extends Decorator {
private final long timeStamp;
public TimeStamped(Basic basic) {
super(basic);
timeStamp = new Date().getTime();
>
public long getStamp() { return timeStamp; }
} продолжение *
1Тема паттернов рассматривается в книге «Thinking in Patterns (with Java)», которую можно
найти на сайте 7menei.MindView.net. Также см. «Приемы объектно-ориентированного проекти
рования», Э. Гаммы и др. (Питер, 2013).
580 Глава 15 • Обобщенные типы
Так как все типы-примеси включают только динамический, а не статический тип, это
решение все равно хуже подхода С++, потому что вам приходится выполнять нисходя
щее преобразование к соответствующему типу, прежде чем вызывать для него методы.
Несмотря на это, оно намного ближе к «настоящим» примесям.
В направлении поддержки примесей Bjava ведется значительная работа, включающая
создание как минимум одного расширения —n3biKaJam, предназначенного специально
для поддержки примесей.
582 Глава15 • Обобщенные типы
Латентная типизация
В начале этой главы была представлена концепция написания кода, который может
применяться как можно шире. Для этого необходимы механизмы ослабления ограни-
чений на типы, с которыми может работать код, без потери преимуществ статической
проверки типов. Тогда мы сможем писать код, используемый в большем количестве
ситуаций без изменений, то есть более «обобщенный» код.
06o6uxemwJava вроде бы делают шаг в нужном направлении. Когда вы пишете или ис
пользуете обобщение, которое просто хранит объекты, этот код будет работать с любым
типом (кроме примитивов, но как мы видели, автоматическая упаковка сглаживает
эту проблему). Для кода несущественно, с каким типом он работает; такой код может
применяться везде, а следовательно, является достаточно универсальным.
Проблема возникает при выполнении манипуляций с обобщенными типами (помимо
вызова методов Object), потому что из-за стирания для успешного вызова конкретных
методов обобщенных объектов в вашем коде необходимо задать ограничения обобщен
ных типов. Это существенно снижает полезность концепции «обобщений», потому что
вам приходится ограничивать свои обобщенные типы так, чтобы они наследовали от
конкретных классов или реализовали конкретные интерфейсы. В некоторых ситуациях
это может привести к использованию обычного класса или интерфейса, потому что
ограниченное обобщение может не отличаться от него по сути.
В некоторых языках программирования эта проблема решается при помощи латент
ной (latent), или структурной, типизации. Также встречается экзотическое выражение
«утиная типизация», означающее: «Если нечто плавает как утка и крякает как утка,
то с ним можно обходиться как с уткой». Термин «утиная типизация» стал довольно
популярным, потому что он, в отличие от других терминов, не несет лишней истори
ческой нагрузки.
Обобщенный код обычно вызывает небольшое количество методов обобщенного
типа. Язык с латентной типизацией ослабляет это ограничение (и порождает более
универсальный код), требуя лишь реализации подмножества методов, а не конкретного
класса или интерфейса. По этой причине латентная типизация позволяет охватывать
иерархии классов и вызывать методы, которые не являются частью общего интерфейса.
Таким образом, фрагмент кода может по сути означать: «Меня не интересует, к какому
типу ты относишься, — главное, что ты можешь вызвать speak() и s it ( ) » . Отказ от
требования конкретного типа повышает универсальность их кода.
Латентная типизация представляет собой механизм структурирования и повторного
использования кода. Фрагмент кода, написанный с применением латентной типизации,
использовать заново будет проще, чем без нее. Структура и повторное использование
кода —краеугольные камни всего программирования: написать код один раз, исполь
зовать много раз, хранить код в одном месте. Поскольку я не обязан указывать имя
точного интерфейса, с которым работает мой код, с латентной типизацией я смогу
написать меньший объем кода, который будет проще применять в разных местах.
Латентная типизация 583
class Dog:
def speak(self):
print "Arf!"
def sit(self):
print "Sitting"
def reproduce(self):
pass
class Robot:
def speak(self):
print "Clickl''
def sit(self):
print "Clank!”
def oilChange(self):
pass
def perform(anything):
anything.speak()
anything.sit()
a = Dog()
b = Robot()
perform(a)
perform(b)
#:~
class Dog {
public:
void speak() {}
void sit() {}
void reproduce() {>
>;
class Robot {
public:
void speak() {)
void sit() {}
void oilChange() {
>;
template<class T> void perform(T anything) {
anything.speak();
anything.sit();
}
int main() {
Dog d;
Robot r;
perform(d);
perform(r);
} ///:~
B Python и С++ типы Dog и Robot не имеют ничего общего, кроме того, что они содержат
два метода с одинаковыми сигнатурами. С точки зрения типов это совершенно разные
типы. Однако метод perform() не обращает внимания на конкретный тип своего аргу
мента, а латентная типизация позволяет ему получать объекты обоих типов.
С++ убеждается, что он действительно может отправлять эти сообщения. При по
пытке передачи неправильного типа компилятор выдает сообщение об ошибке (эти
сообщения традиционно были очень длинными и страшными —главная причина, по
которой шаблоны С++ пользовались дурной славой). И хотя они делают это в разное
время —С++ во время компиляции, Python во время выполнения, —обаязыка следят
за тем, чтобы типы использовались правильно, а следовательно, считаются сильно
типизованными1. Латентная типизация не нарушает сильную типизацию.
Поскольку обобщения были добавлены Bjava на поздней стадии, ни малейшей возмож
ности реализовать латентную типизацию нё было, поэтому в^у ад ан н ая возможность
не поддерживается. В результате на первый взгляд кажется, что механизм обобщений
Java «менее обобщен», чем в языках с поддержкой латентной типизации12. Например,
при попытке реализовать приведенный пример HaJava придется использовать класс
или интерфейс и указать его в выражении ограничения:
1 Из-за возможности приведения типов, которое фактически подавляет систему типов, некоторые
специалисты считают С++ языком со слабой типизацией, но это преувеличение. Пожалуй,
правильнее сказать, что С++ обладает «сильной типизацией с лазейкой».
2 06o6njeHHaJava на базе стирания иногда называются второклассными обобщениями.
Латентная типизация 585
//: generics/Performs.java
//: generics/SimpleDogsAndRobots.java
// Removing the generic; code still works.
class CommunicateSimply {
static void perform(Performs performer) {
performer.speak();
performer.sit();
}' продолжение +
586 Глава 15 • Обобщенные типы
Отражение
Первый способ основан на использовании отражения. Вот как выглядит метод
perform(), использующий латентную типизацию:
//: generics/LatentReflection.java
// Использование отражения для моделирования латентной типизации.
import java.lang.reflect.*j
import static net.mindview.util.Print.*;
// Не реализует Performs:
class Mime {
public void walkAgainstTheWind() {>
public void sit() { print("Pretending to sit"); >
public void pushInvisibleWalls() {>
public String toString() { return "Mime"; >
>
// Не реализует Performs:
class SmartDog {
public void speak() { print("Woof!'*); }
public void sit() { print("Sitting"); >
public void reproduce() {}
>
class CommunicateReflectively {
public static void perform(Object speaker) {
Class<?> spkr = speaker.getClass();
try {
try {
Method speak = spkr.getMethod("speak");
speak.invoke(speaker);
Компенсация отсутствия латентной типизации 587
> catch(NoSuchMethodException e) {
print(speaker + " cannot speak");
>
try {
Method sit = spkr.getMethod("sit");
sit.invoke(speaker);
} catch(NoSuchMethodException e) {
print(speaker + " cannot sit")j
}
> catch(Exception e) {
throw new RuntimeException(speaker.toString(), e);
>
>
>
public class LatentReflection {
public static void main(String[] args) {
CommunicateReflectively.perform(new SmartDog());
CommunicateReflectively.perform(new Robot());
CommunicateReflectively.perform(new Mime());
>
} /* Output:
Woof!
Sitting
Click!
Clank!
Mime cannot speak
Pretending to sit
*///:~
Здесь классы полностью разъединены и не имеют общих базовых классов (кроме Object)
или интерфейсов. Используя отражение, метод CornmunicateReflectively.perforrp()
может динамически проверить доступность нужных методов и вызвать их. Он даже
может обработать ситуацию, в которой Mime содержит только один из необходимых
методов, и частично решает свою задачу.
import java.util.*;
import static net.mindview.util.Print.*;
class ApplyTest {
public static void main(String[] args) throws Exception {
List<Shape> shapes = new ArrayList<Shape>();
for(int i = 0; i < 10; i++)
shapes.add(new Shape());
Apply.apply(shapes, Shape.class.getMethod(''rotate"));
Apply.apply(shapes,
Shape.class.getMethod("resize", int.class), 5);
List<Square> squares = new ArrayList<Square>();
for(int i = 0; i < 10; i++)
squares.add(new Square());
Apply.apply(squares, Shape.class.getMethod("rotate"));
Apply.apply(squares,
Shape.class.getMethod("resize", int.class), 5);
//: generics/Fill.java
// Обобщение Идеи FilledList
// {main: FillTest}
import java.util.*;
>
}
}
class Contract {
private static long counter = 0;
private final long id = counter++;
public String toString() {
return getClass().getName() + " " + id;
}
>
class TitleTransfer extends Contract {>
class FillTest {
public static void main(String[] args) {
List<Contract> contracts = new ArrayList<Contract>();
Fill.fill(contracts, Contract.class, 3);
Fill.fill(contracts, TitleTransfer.class, 2);
for(Contract c: contracts)
System.out.println(c);
SimpleQueue<Contract> contractQueue =
new SimpleQueue<Contract>();
// Не сработает. Метод fill() недостаточно обобщен:
// Fill.fill(contractQueue, Contract.class, 3);
}
> /* Output:
Contract 0
Contract 1
Contract 2
TitleTransfer 3
TitleTransfer 4
*///:~
В этой ситуации механизм параметризованных типов с латентной типизацией бу
дет полезен, потому что вы не зависите от прошлых решений, принятых создателем
конкретной библиотеки, и вам не придется переписывать свой код каждый раз, когда
встретится новая библиотека, не учитывающая эту ситуацию (то есть код действительно
«обобщен»). В приведенном выше случае, посколькупроектировщики^уа (по понят
ным причинам) не видели необходимости в интерфейсе Addable, мы ограничиваемся
иерархией Collection, а класс SimpleQueue, несмотря на наличие у него метода add(),
не подойдет. Код, ограниченный использованием collection, вряд лй можно считать
полностью обобщенным. С латентной типизацией такой проблемы не будет.
class AddableSimpleQueue<T>
extends SimpleQueue<T> implements Addable<T> {
public void add(T item) { super.add(item); }
}
class Fill2Test {
public static void main(String[] args) {
// Адаптация Collection:
List<Coffee> carrier = new ArrayList<Coffee>();
Fill2.fill(
new AddableCollectionAdapter<Coffee>(carrier),
Coffee.class, 3);
// Вспомогательный метод фиксирует тип:
Fill2.fill(Adapter.collectionAdapter(carrier),
Latte.class, 2);
for(Coffee с: carrier)
print(c);
print("--------------- ------");
// Использование адаптированного класса:
AddableSimpleQueue<Coffee> coffeeQueue =
new AddableSimpleQueue<Coffee>();
Fill2.fill(coffeeQueuej Mocha.class, 4)j
Fill2.fill(coffeeQueue, Latte.class, 1);
for(Coffee с: coffeeQueue)
print(c);
}
) /* Output:
Coffee 0
Coffee 1
Coffee 2
Latte 3
Latte 4
Mocha 5
Mocha 6
Mocha 7
Mocha 8
Latte 9
*///:~
Класс Fill2 не требует Collection, в отличие от Fill. Вместо этого ему требуется любой
объект, реализующий Addable, а интерфейс Addable был написан только для Fill — это
проявление латентного типа, который компилятор должен был создать для меня.
В этой версии также был добавлен перегруженный метод fill(), получающий Genera
tor вместо маркера типа. Тип Generator безопасен на стадии компиляции: компилятор
следит за тем, чтобы передавался правильный объект Generator, так что исключения
возбуждаться не будут.
Первый адаптер AddableCollectionAdapter работает с базовым типом Collection; это
означает, что может использоваться любая реализация Collection. Эта версия просто
сохраняет ссылку на Collection и использует ее для реализации add().
Если вместо базового класса иерархии используется конкретный тип, это позволяет
несколько сократить объем кода при создании адаптера с применением наследования,
как видно на примере AddableSimpleQueue.
594 Глава 15 • Обобщенные типы
' Иногда эти объекты называются функторами. Я буду использовать термин «объект функции»,
потому что в математике термин «функтор» имеет вполне конкретное и совершенно другое
значение.
Использование объектов функций как стратегий 595
тем, что, в отличие от обычных методов, они могут передаваться при вызовах, а также
могут иметь состояние, поддерживаемое между вызовами. Конечно, нечто подобное
можно сделать при помощи любого метода класса, но объекты функций (как и любые
паттерны проектирования), прежде всего, отличаются по своему предназначению.
В данном случае они предназначены для создания чего-то, что выглядит как метод,
но может передаваться при вызовах; следовательно, это «что-то» тесно связано с пат
терном проектирования «Стратегия» (а иногда неотличимо от него).
Как я обнаружил для многих паттернов проектирования, границы между паттернами
здесь размываются: мы создаем объекты функций, которые выполняют адаптацию,
и эти объекты передаются методам для использования как стратегии.
Выбрав этот путь, я добавил разнообразные обобщенные методы, которые изначально
намеревался создать, и несколько других. Результат выглядит так:
//: generics/Functional.java
import java.math.*;
import java.util.concurrent.atomic.*;
import java.util.*;
import static net.mindview.util.Print.*;
private T bound;
public GreaterThan(T bound) { this.bound = bound; }
public boolean test(T x) {
return x.compareTo(bound) > 0j
}
}
static class MultiplyingIntegerCollector
implements Collector<Integer> {
private Integer val = 1;
public Integer function(Integer x) {
val *= x;
return val;
>
public Integer result() { return val; }
}
public static void main(String[] args) {
// Обобщения, списки аргументов переменной длины
// и упаковка работают совместно:
List<Integer> li = Arrays.asList(l, 2, 3, 4, 5, 6, 7);
Integer result = reduce(li, new IntegerAdder());
print(result);
print(forEach(li,
new MultiplyingIntegerCollector()).result());
print(filter(lbd,
new GreaterThan<BigDecimal>(new BigDecimal(3))));
print(transform(lbd,new BigDecimalUlp()));
>
} I * Output: i
28
-26
[5, 6, 7]
5040
210
11.000000
[3.300000, 4.400000]
[11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47]
311
true
265
[0 .000001 , 0 .000001 , 0 .000001 , 0 .000001 ]
*///:~
Я начинаю с определения интерфейсов для разных типов объектов функций. Они
создавались по мере надобности, пока я разрабатывал разные методы и находил при
менение для каждого из них. Класс Combiner был предложен анонимным читателям
одной из статей, опубликованных на моем сайте. Он абстрагируется от подробностей
попытки сложения двух объектов и просто говорит, что они каким-то образом объеди
няются. В результате и IntegerAdder, и IntegerSubtracter могут быть типами Combiner.
UnaryFunction получает один аргумент и производит результат; аргумент и результат не
обязаны относиться к одному типу. Collector используется в качестве «параметра-на
копителя», и вы можете извлечь результат при завершении. UnaryPredicate производит
логический (boolean) результат. Также можно определить другие объекты функций,
но для демонстрации достаточно и этих.
Класс Functional содержит набор обобщенных методов, применяющих объекты функ
ций к последовательностям. Метод reduce() применяет функцию в Combiner к каждому
элементу последовательности для получения единого результата.
Метод forEach() получает объект Collector и применяет его функцию к каждому
элементу, игнорируя результат каждого вызова функции. Такая возможность может
применяться просто ради побочных эффектов (что не соответствует «функциональ
ному» стилю программирования, но все равно может быть полезно), или же С о П е ^ о г
может поддерживать внутреннее состояние и стать параметром-накопителем, как
в приведенном примере.
Метод transform() создает список, вызывая UnaryFunction для каждого объекта в по
следовательности и фиксируя результат.
Наконец, метод filter() применяет UnaryPredicate к каждому объекту в последователь
ности, сохраняет элементы, для которых было получено значение true, в контейнере
List и возвращает его.
При желании вы можете определить дополнительные обобщенные функции. Библи
отека С++ STL содержит множество примеров такого рода. Проблема также была
решена в некоторых библиотеках с открытым кодом —таких, KaKjGA (Generic Algo-
rithmsforJava).
Резюме 599
Этот вызов создает список из всех элементов li, больших 4, после чего применяет
MultiplyingIntegerCollector() к полученному списку и извлекает result() . Я не стану
подробно объяснять остальной код — вероятно, вы сможете разобраться в нем само
стоятельно. n
42. (5) Создайте два класса, никак не связанных друг с другом. Каждый класс должен
содержать значение и по меньшей мере методы для получения и модификации этого
значения. Измените пример Functional.java так, чтобы он выполнял функциональные
операции с коллекциями классов (эти операции не обязаны быть арифметическими,
как в Functional.java).
Резюме
Мне приходилось объяснять, как работают шаблоны С++, с самого момента их по
явления, и вероятно, я выдвигал следующий аргумент дольше, чем кто-либо. Лишь
недавно я начал задумываться над тем, насколько актуален этот аргумент — так ли
часто проблема, которую я собираюсь описать, проникает в программы?
Этот аргумент выглядит так. Одной из самых привлекательных областей для исполь
зования механизма обобщенных типов являются классы контейнеров: List, Set, Мар
и т. д. —те, которые рассматривались в главе 11 и будут более подробно рассматриваться
в главе 17. Д о ^ у а SE5 при помещении объекта в контейнер происходило восходящее
преобразование в Object, приводящее к потере информации. Когда объект извлекался
из контейнера для выполнения каких-либо операций, его приходилось преобразовы
вать обратно к фактическому типу. В качестве примера я приводил контейнер List с
элементами Cat (вариация на эту тему с объектами Apple и Orange приводилась в начале
главы 11)5До появления обобщенных версий этих KOHTeftHepoBjava SE5 в контейнер
заносился тип Object и извлекался тип Object, поэтому в контейнер с элементами Cat
можно было легко поместить объект Dog.
Однако я з ы к ^ у а д о появления обобщений не допускал некорректного использова
ния объектов, помещенных в контейнер. Если вы заносили в контейнер Cat объект
Dog, а потом пытались интерпретировать все элементы контейнера как Cat, при из
влечении ссылки на Dog и попытки ее преобразования к Cat происходило исключение
600 Глава15 • Обобщенныетипы
Дополнительная литература
Вводная информация по o6o6n*emmMjava представлена в учебнике Гилада Брача
«Generics in theJava Programming Language» {http://java.sun.com/j2se/1.5/pdf/generics-
tutorial.pdf).
Еще один чрезвьгчайно полезный ресурс — «Java Generics FAQ» Анжелики Лангер
(www.langer.camelot.de/GenericsFAQrfavaGenericsFAQ.html).
Дополнительную информацию о шаблонах можно найти в статье Торгерсона, Эрнста,
Хансена, фон дер Axe, Брача и Гафтера «Adding Wildcards to theJava Programming
Language» (www.jot.fm/issues/issue_2004_ 12/article5).
В конце главы 5 вы научились определять и инициализировать массивы.
Как мы обычно представляем себе массивы? Их можно создать и заполнить данными,
можно выбрать элементы по целочисленным индексам, и они не изменяют размер.
В большинстве случаев ничего больше знать не нужНо, но иногдас массивами требу-
ется выполнять более сложные операции, и разработчику приходится выбирать между
массивом и более гибким контейнером. В этой главе вы получите болёе глубокое
представление о массивах.
class BerylliumSphere {
private static long counter;
private final long id = counter++;
public String toString() { return "Sphere " + id; >
>
public class ContainerComparison {
public static void main(String[] args) {
BerylliumSphere[] spheres = new BerylliumSphere[10];
for(int i = 0; i < 5; i++)
spheres[i] = new BerylliumSphere();
print(Arrays.toString(spheres));
print(spheres[4]);
List<BerylliumSphere> sphereList =
new ArrayList<BerylliumSphere>();
for(int i = 0; i < 5; i++)
sphereList.add(new BerylliumSphere());
print(sphereList);
print(sphereList.get(4));
int[] integers = { 0, 1 , 2, 3, 4, 5 );
print(Arrays.toString(integers));
print(integers[4]);
продолжение &
604 Глава 16 • Массивы
// Массивы примитивов:
int[] e; // Null-ссыпка
int[] f = new int[5];
// Примитивы в массиве автоматически
// инициализируются нулями:
print("f: " + Arrays.toString(f));
int[] g = new int[4];
for(int i = 0; i < g.length; i++)
g[i] = i*i;
int[] h = { 11, 47, 93 };
// Ошибка компиляции: переменная e не инициализирована:
//!print("e.length = '1 + e.length);
print("f.length = " + f. length);
print("g.length = " + g. length);
pnint("h.length = " + h. length);
e = h;
print("e.length = + e.length);
e = new int[]{ 1, >;
print("e.length = + e.length)j
>
> /* Output:
b: [null, null, null, null, null]
a. length = 2
b. length = 5
c. length = 4
d. length = 3
a.length = 3
f: [0, 0, 0, 0, 0]
f. length = 5
g. length = 4
h. length = 3
e. length = 3
e.length = 2
*///:~
В большинстве ситуаций эта новая форма записи более удобна для написания кода.
Выражение
а * d;
показывает, как можно взять ссылку, присоединенную к одному объекту массива,
и присвоить ее другому объекту массива; так же, как и в случае с любым другим типом
объекта. Теперь как а, так и d указывают на один и тот же массив в общей куче.
Вторая часть программы ArrayOptions.java показывает, что работа с массивами прими
тивов практически ничем не отличается от работы с массивами объектов, за одним
исключением: массивы примитивов хранят реальные значения.
1. (2) Создайте метод, получающий в аргументе массив объектов BerylliumSphere.
Вызовите метод с динамическим созданием аргумента. Продемонстрируйте, что
обычная групповая инициализация массива в этом случае не работает. Найдите
ситуации, в которых обычная групповая инициализация массива работает, а ди
намическая групповая инициализация оказывается излишней.
Возврат массива 607
Возврат массива
Предположим, что вы пишете метод и не хотите возвращать из него что-то одно, а со
бираетесь вернуть целое множество. В языках типа С и С++ осуществить подобное
сложно, так как в них нельзя отдавать назад массив, разрешается возвращать лишь ссыл
ку на него. Это сразу ведет к затруднениям, поскольку становится сложно определить
жизненный цикл такого массива, что легко может стать причиной «утечки памяти».
BJava используется похожий подход, но вы просто «возвращаете массив». BJava вам
не приходится беспокоиться о жизненном цикле массива —он будет доступен столь
ко, сколько вам понадобится, а когда станет не нужен, об его удалении позаботится
уборщик мусора.
В качестве примера рассмотрим возврат массива строк (string):
//: arrays/IceCream.java
// Возвращение массивов из методов,
import java.util.*;
Метод flavorSet () создает массив строк String с именем results. Размер массива уста
навливается аргументом метода n. Далее метод случайным образом выбирает из массива
608 Глава 16 • Массивы
Многомерные массивы
Java позволяет легко создавать многомерные массивы. Для многомерного массива
примитивов каждый вектор заключается в фигурные скобки:
//: a r r a y s / M u lt id im e n s io n a lP r im it iv e A r r a y . ja v a
// С о зд а н и е многомерных м а с с и в о в ,
im p o rt ja v a .u til.* ;
Первая конструкция new создает массив, у которого первый элемент имеет случайную
длину, а остальные не определены. Вторая конструкция new в цикле f o r заполняет
элементы, но оставляет третий индекс неопределенным до третьего new.
Аналогичным образом можно поступать с массивами непримитивных объектов. Сле
дующий пример показывает, как объединить несколько выражений new в фигурных
скобках:
//: a rra y s / M u ltid im e n s io n a lO b je c tA rra y s .ja v a
im p o rt ja v a .u til.* j
new B e r y l l i u m S p h e r e ( ) , new B e r y l l i u m S p h e r e ( ) ,
new B e r y l l i u m S p h e r e ( ) , new B e r y l l i u m S p h e r e ( ) ,
new B e r y l l i u m S p h e r e ( ) , new B e r y l l i u m S p h e r e ( ) },
>;
S y s te m .o u t. p r in t l n ( A r r a y s . d e e p T o S trin g (s p h e re s )) ;
>
} /* O u t p u t :
[[S ph ere 0 , Sphere 1], [Sphere 2, Sphere 3, Sphere 4 ,
Sphere 5], [Sphere 6, Sphere 7, Sphere 8, Sphere 9, Sphere
10, Sphere 11, Sphere 12, S p here 13]]
*///:~
Как видите, spheres — еще один ступенчатый массив с разными длинами списков
объектов.
Автоматическая упаковка также работает с инициализаторами массивов:
//: a rra y s / A u to b o x in g A rr a y s .ja v a
im p o rt ja v a .u til.* ;
p u b lic c la s s A u to b o x in g A rra y s {
p u b lic s ta tic v o id m a in (S trin g [] arg s) {
In teg er[][] а = { // A u t o b o x i n g :
{ i , 2 , 3,. 4 , 5 , iб , 7j. 8 , 9 , 10 b I
{ 21, 22, 23, 2 4 , 2 5 , 26 , 27, 28, 29, 30 >
{ 51, 52, 53, 54, 55, 56, 57, 58, 59, 60 >
{ 71, 72, 73, 74, 75, 76, 77, 78, 79, 80 }
>;
S y s te m .o u t.p rin tln (A rra y s .d e e p T o S trin g (a ));
}
} /* O u t p u t :
[[1, 2, 3, 4, 5, 6, 7, 8, 9, 10], [21, 22, 23, 24, 25, 26,
27, 28, 29, 30], [51, 52, 53, 54, 55, 56, 57, 58, 59, 60],
[71, 72, 73, 74, 75, 76, 77, 78, 79, 80]]
* / / / :~
А вот как происходит последовательное построение непримитивных объектов:
//: a rra y s / A s s e m b lin g M u ltid im e n s io n a lA rra y s . java
/ / С о з д а н и е многомерных м а с с и в о в ,
im p o rt ja v a .u til.* ;
Массивы и обобщения
В общем случае массивы и обобщения плохо сочетаются друг с другом. Например, вы
не сможете создать массивы параметризованных типов:
P ee l< B a n a n a> [] p e e ls = new P e e l < B a n a n a > [ 1 0 ] ; // Недопустимо
c la s s C la s s P a ra m e te r< T > {
public T[] f(T[] arg) { return arg; }
}
c la s s M eth o dP aram eter {
p u b lic s ta tic <T> T [ ] f(T [] arg) { return arg; >
>
p u b lic cla s s P a ra m e te riz e d A rra y T y p e {
p u b lic s ta tic v o id m a in (S trin g [] args) {
In teg er[] in ts = { 1, 2, 3, 4, 5 };
D o u b le [] d o u b le s = { 1 .1 , 2 .2 , 3 .3 , 4 .4 , 5 .5 };
In teg er[] in ts 2 =
new C l a s s P a r a m e t e r < I n t e g e r > ( ) . f ( i n t s ) j
D o u b le [] d o u b le s 2 =
new C l a s s P a r a m e t e r < D o u b l e > ( ) . f ( d o u b l e s ) j
in ts 2 = M e th o d P a ra m e te r.f(in ts );
d o u b le s 2 = M e th o d P a ra m e te r.f(d o u b le s );
}
> ///:~
Обратите внимание на удобство использования параметризованного метода вместо
параметризованного класса: вам не нужно создавать экземпляр класса с параметром
для всех типов, к которым он применяется, и его можно сделать статическим. Конечно,
параметризованный метод не всегда должен выбираться вместо параметризованного
класса, но часто он оказывается предпочтительным.
Оказывается, утверждение о том, что создать массивы обобщенных типов невозможно,
не совсем точно. Правда, компилятор не позволяет создать экземпляр массива обоб
щенного типа, однако он позволяет создать ссылку на такой массив, например:
L is t< S trin g > [] ls ;
Эта строка проходит компиляцию без жалоб. И хотя вы не сможете создать объект
массива, содержащего обобщения, можно создать массив необобщенного типа и вы
полнить преобразование типа:
//: a rr a y s / A rra y O fG e n e ric s .ja v a
// Возмож ность со з д а н и я м а сси в а обо бщ ен ий,
im p o rt ja v a .u til.* ;
Массивы и обобщения 613
p u b lic c la s s A n ra y O fG e n e ric s {
@ S u p p r e s s W a r n i n g s ( " u n c h e c k e d ")
p u b lic s ta tic v o id m a in ( S trin g [ ] a rg s) {
L is t< S trin g > [] ls ;
L is t[] la = new L i s t [ 1 0 ] ;
ls = ( L is t< S trin g > [] ) la ; // "Неконтролируемое" предупреждение
ls [0 ] = new A r r a y L i s t < S t r i n g > ( ) j
// П р о в е р к а в о вр ем я к о м п и л я ц и и п р и в о д и т к о ш ибке:
//! ls [l] = new A r r a y L i s t < I n t e g e r > ( ) ;
для присваивания A r r a y L i s t < I n t e g e r > массиву без ошибок во время компиляции или
выполнения.
Если вы знаете, что не собираетесь выполнять восходящее преобразование, а ваши по
требности относительно просты, существует возможность создания массива обобщений,
обеспечивающего базовую проверку типов во время компиляции. Однако обобщенный
контейнер почти всегда будет предпочтительнее массива обобщений.
В общем случае, обобщения эффективны на границах класса или метода. Во внутренней
реализации обобщения обычно становятся бесполезными из-за стирания. Например,
вы не можете создать массив обобщенного типа:
//: a rra y s / A rra y O fG e n e ric T y p e .ja v a
// М асси вы обобщенных т и п о в не к о м п и л и р у ю т с я .
Arrays.fill()
Класс Arrays из стандартной библиотеки Java содержит довольно незамысловатый
метод f i l l ( ) —одно значение просто копируется во все ячейки массива, или, в случае
с объектами, копируется одна и та же ссылка. Пример:
//: a r r a y s / F i l l i n g A r r a y s . ja v a
// И с п о л ь з о в а н и е м е т о д а A r r a y s . f i l l ( )
im p o rt j a v a . u t i l . * ;
im p o rt s t a t i c n e t.m in d v ie w .u til.P rin t.* ;
Заполнять можно как целый массив, так и диапазон элементов (как в двух последних
командах). Но поскольку A r r a y s . f i l l ( ) может вызываться только для одного значения
данных, такая возможность не особенно полезна.
Генераторы данных
Для создания более интересных массивов данных более гибкими средствами мы вос
пользуемся концепцией генератора, представленной в главе 15. Программа, использу
ющая интерфейс G e n e r a t o r , может создавать любые данные, выбирая соответствующий
генератор (пример паттерна проектирования «Стратегия» —разные генераторы пред
ставляют разные стратегии1).
В этом разделе будут представлены некоторые разновидности генераторов, и как вы
уже видели, вы можете легко определять собственные генераторы.
Начнем с базового набора генераторов-счетчиков для всех «оберток» примитивных
типов и S t r i n g . Классы генераторов вложены в класс C o u n t i n g G e n e r a t o r , чтобы они могли
использовать имя типа, совпадающее с именем генерируемого типа; например, генера
тор, создающий объекты I n t e g e r , будет создаваться выражением new C o u n t i n g G e n e r a t o r .
In te g e r():
продолжение x3>
1Здесь возникает некоторая неоднозначность: также можно утверждать, что генератор пред
ставляет паттерн «Команда». Однако я считаю, что задачей является заполнение массива,
а генератор решает часть этой задачи, поэтому к «Стратегии» он ближе, чем к «Команде».
616 Глава 16 • Массивы
C h aracter: а b с d e f g h i j
B yte: 0 1 2 3 4 5 6 7 8 9
B o o le an : tru e fa ls e tru e fa ls e tru e fa ls e tru e fa ls e tru e
fa ls e
*///:~
p riv a te in t mod = 1 0 0 0 0 ;
p u b lic In te g e r() {}
p u b lic I n te g e r( in t m od ulo) { mod = m o d u l o ; >
p u b lic ja v a .la n g .I n te g e r next() {
retu rn r.n e x tIn t(m o d );
>
>
p u b lic s ta tic cla s s
Long im p le m e n ts G e n e r a t o r < j a v a . la n g . L o n g > {
p riv a te in t mod = 1 0 0 0 0 ;
p u b lic Long() {}
p u b lic L o n g (in t m od u lo) { mod = m o d u l o ; >
p u b lic ja v a .la n g .L o n g next() {
return new j a v a . l a n g . L o n g ( r . n e x t I n t ( m o d ) ) ;
>
>
p u b lic s ta tic c la s s
F lo a t im p le m e n ts G e n e r a t o r < j a v a . l a n g . F l o a t > {
p u b lic ja v a .la n g .F lo a t next() {
// У се ч е н и е дробной части до двух цифр:
in t trim m e d = M a th .ro u n d ( r.n e x tF lo a t( ) * 100);
re tu rn ((flo a t)trim m e d ) / 100;
>
>
p u b lic s ta tic c la s s
D o u b le im p le m e n ts G e n e r a t o r < j a v a .la n g .D o u b le > {
p u b lic ja v a .la n g .D o u b le next() {
lo n g trim m e d = M a t h . r o u n d ( r . n e x t D o u b l e ( ) * 100);
re tu rn ( (d o u b le )trim m e d ) / 100;
>
>
> ///:~
R a n d o m G e n e ra to r.S trin g наследует от C o u n tin g G e n e ra to r.S trin g и просто подключает
новый генератор C h a r a c t e r .
Чтобы генерируемые числа не были слишком большими, R a n d o m G e n e r a t o r . l n t e g e r по
умолчанию использует коэффициент Ю 000, но перегруженный конструктор позволяет
выбрать меньшее значение. Похожий подход используется для R a n d o m G e n e r a t o r . L o n g .
Для генераторов F l o a t и D o u b l e дробная часть усекается.
Мы можем повторно использовать класс G e n e r a t o r s T e s t для тестирования RandomGene-
rator:
заполняя его при помощи генератора. Учтите, что программа создает только массивы
подтипов O b j e c t и не может создавать массивы примитивов:
//: n e t/ m in d v ie w / u til/ G e n e ra te d . ja va
package n e t.m in d v ie w .u til;
im p o rt ja v a .u til.* ;
p u b lic c la s s G en erated {
// Заполнение су щ е ст в у ю щ е г о м а с с и в а :
p u b lic s ta tic <T> T [ ] array(T[] а, G enerator< T> gen) {
return new C o l l e c t i o n D a t a < T > ( g e n , a .le n g th ) .to A rra y ( a );
>
// С о з д а н и е нового м ассива:
@ S u p p re ssW a rn in g s("u n ch e ck e d ")
p u b lic s ta tic <T> T [ ] a rra y (C la s s < T > type,
G en erator< T> gen, in t s iz e ) {
T[] а =
( т [ ] ) 1 а у а - 1 а п б * г е ^1е с * - А г г а У - п ем 1п 5 * а п с е ( * У Р е ^ s i z e ) ;
r e t u r n new C o l l e c t i o n D a t a < T > ( g e n , s i z e ) . t o A r r a y ( a ) j
>
> / / / :~
Класс C o l l e c t i o n D a t a будет определен в главе 17. Он создает объект C o l l e c t i o n , запол
ненный элементами, созданными генератором G e n e r a t o r g e n . Количество элементов
определяется вторым аргументом конструктора. Все подтипы C o l l e c t i o n должны со
держать метод t o A r r a y ( ) , который заполняет аргумент-массив элементами C o l l e c t i o n .
Второй метод использует отражение для динамического создания нового массива
подходящего типа и размера. Затем он заполняется таким же способом, как и в первом
методе.
Применение генераторов для создания массивов 621
Хотя массив а инициализирован, эти значения заменяются при передаче через метод
G e n e r a t e d . a r r a y ( ) , который оставляет исходный массив на месте. Инициализация b
p u b lic c la s s C onvertTo {
p u b lic s ta tic b o o le a n [] p rim itiv e (B o o le a n [ ] in ) {
b o o le a n [] re s u lt = new b o o l e a n [ i n . l e n g t h ] ;
fo r( in t i = 0; i < in .le n g th ; i+ + )
re s u lt[ i] = in [ i] ; // А в т о м а т и ч е с к а я распаковка
retu rn re s u lt;
>
p u b lic s ta tic ch ar[] p rim itiv e (C h a ra c te r[] in ) {
ch ar[] re s u lt = new c h a r [ i n . l e n g t h ] ;
fo r( in t i = 0; i < in .le n g th ; i+ + )
re s u lt[ i] = in [i];
retu rn re s u lt;
>
p u b lic s ta tic byte[] p rim itiv e (B y te [] in ) {
byte[] re s u lt = new b y t e [ i n . l e n g t h ] ;
fo r( in t i = 0; i < in .le n g th ; i+ + )
re s u lt[ i] = in [i];
retu rn re s u lt;
}
p u b lic s ta tic sh o rt[] p rim itiv e (S h o rt[] in ) {
sh o rt[] re s u lt = new s h o r t [ i n . l e n g t h ] ;
продолжение ё>
622 Глава 16 • Массивы
fo r( in t 1 = 0; i < in .le n g th ; i+ + )
re s u lt[ i] = in [i];
retu rn re s u lt;
>
p u b lic s ta tic in t[ ] p rim itiv e (I n te g e r[] in ) {
in t[ ] re s u lt = new i n t [ i n . l e n g t h ] ;
fo r( in t i = 0; i < in .le n g th ; i++)
re s u lt[ i] = in [i];
retu rn re s u lt;
>
p u b lic s ta tic lo n g [] p rim itiv e (L o n g [] in ) {
lo n g [] re s u lt = new l o n g [ i n . l e n g t h ] ;
fo r( in t i = 0; i < in .le n g th ; i++)
re s u lt[ i] = in [i];
retu rn re s u lt;
}
p u b lic s ta tic flo a t[ ] p rim itiv e (F lo a t[ ] in ) {
flo a t[ ] re s u lt = new f l o a t [ i n . l e n g t h ] ;
fo r( in t i = 0; i < in .le n g th ; i++)
re s u lt[ i] = in [ i] ;
retu rn re s u lt;
}
p u b lic s ta tic d o u b le [] p rim itiv e (D o u b le [] in ) {
d o u b le [] re s u lt = new d o u b l e [ i n . l e n g t h ] ;
fo r( in t i = 0; i < in .le n g th ; i+ + )
re s u lt[ i] = in [ i] ;
retu rn re s u lt;
}
} / / / :~
Каждая версия prim itive() создает соответствующий массив примитивов нужной
длйны, а затем копирует элементы из массива типов-«оберток». Обратите внимание
на автоматическую распаковку в выражении:
re s u lt[ i] = in [i];
14. (6) Создайте массив каждого из примитивных типов. Заполните каждый массив с
использованием CountingGenerator. Выведите содержимое каждого массива.
15. (2) Измените пример ContainerComparison.java: создайте G e n e r a t o r для B e ry lliu m S p h e re
и измените метод m a i n ( ) , чтобы он использовал G e n e r a t o r с G e n e r a t e d . a r r a y ( ) .
16. (3) Начав с CountingGenerator.java, создайте класс который создает S k ip G e n e ra to r,
Класс Arrays
В пакете j a v a . u t i l вы обнаружите класс A r r a y s , который содержит набор статических
методов для выполнения нескольких, часто используемых операций над массива
ми. В их числе есть шесть основных методов: e q u a l s ( ) для проверки двух массивов
на равенство (и d e e p E q u a l s ( ) для многомерных массивов); f i l l ( ) , который мы уже
видели ранее; s o r t ( ) для сортировки массива; b i n a r y S e a r c h ( ) для поиска элементов
в отсортированном массиве; t o S t r i n g ( ) для получения строкового представления мас
сива; h a s h C o d e ( ) для вычисления хеш-кода (подробнее см. главу 17). Все эти методы
перегружены для всех простейших типов и класса O b j e c t . Вдобавок существует один
дополнительный метод a s L i s t ( ) , который преобразует любой массив в контейнер
L i s t , —этот метод был описан в главе 11.
Копирование массива
В стандартной библиотеке^уа имеется статический ( s t a t i c ) метод с именем S y s t e m .
a r r a y c o p y ( ) , который способен гораздо быстрее копировать массивы, чем получается
в случае «ручного» копирования элементов в цикле f o r . Метод S y s t e m . a r r a y c o p y ( )
перегружен для всех нужных типов. Следующий пример манипулирует массивами
целыхчисел ( i n t ) :
//: a rra y s / C o p y in g A rra y s .ja v a
// U s i n g S y s t e m . a r r a y c o p y ( )
im p o rt ja v a .u til.* ;
im p o rt s ta tic n e t.m in d v ie w .u til.P rin t.* ;
p u b lic c la s s C o p y in g A rra y s {
p u b lic s ta tic v o id m a in (S trin g [] arg s) {
in t[ ] i = new i n t [ 7 ] ;
in t[ ] j = new i n t [ 1 0 ] ;
A rra y s .fill(i, 47);
A rra y s .fill(j, 99);
p rin t( " i = " + A rra y s .to S trin g ( i) ) ;
p rin t( " j = " + A r ra y s .to S trin g ( j) );
S y s te m .a rra y c o p y ( i, 0, j, 0, i.le n g th );
Применение генераторов для создания массивов 625
Сравнение массивов
В классе Arrays имеется перегруженный метод equals() для сравнения целых масси
вов, для всех примитивных типов и для Object. При сравнении массивов проверяются
следующие условия: в массивах должно быть равное количество элементов и каждый
элемент должен быть эквивалентен соответствующему элементу другого массива, для
проверки этого условия используется метод equals(). (Для примитивов привлекается
метод equals() соответствующегокласса-«обертки»; например, метод Integer.equals()
для целого числа int.) Вот пример:
626 Глава 16 • Массивы
b efo re s o rtin g :
[[i = 58, j = 55], [i = 93, j = 61], [i = 61, j = 29]
, [i = 68, j = 0], [i = 22, j = 7], [i = 88, j = 28]
, [i = 51, j = 89], [i = 9, j = 78], [i = 98, j = 61]
, [i = 20, j = 58], [i = 16, j = 40], [i = 11, j = 22]
3
a fte r s o rtin g :
[[i =9 , j =78], [i = 11, j = 22], [i = 16, j = 40]
, [ i = 20, j = 5 8 ], [ i =2 2 , j = 7 ],[ i = 51, j = 89]
, [1 = 5 8 , j = 5 5 ] , [ i =6 1 , j = 29], [ i = 68, j = 0]
, [1 = 8 8 , j = 2 8 ] , [ i =9 3 , j = 61], [ i = 98, j = 61]
3
*///:~
p u b lic c la s s R everse {
p u b lic s ta tic v o id m a in (S trin g [] a rg s) {
Com pType[] а = G e n e ra te d .a rra y (
new C o m p T y p e [ 1 2 ] , C o m p T y p e .g e n e ra to r());
p rin t( " b e fo re s o r t in g : ” );
p rin t( A r ra y s .to S tr in g ( a )) ;
Применение генераторов для создания массивов 629
Сортировка массива
Со встроенными методами для сортировки вы можете упорядочить любой массив
простейших типов или любой массив объектов, который либо реализует C o m p a r a b l e ,
либо имеет связанный объект C o m p a r a t o r 1. Следующий пример генерирует случайные
строки ( S t r i n g ) и упорядочивает их:
//: a rra y s / S trin g S o rtin g .ja v a
// С ортировка м ассива строк.
im p o rt ja v a .u til.* ;
import net.mindview.util.*;
im p o rt s t a t i c n e t.m in d v ie w .u til.P rin t.* ;
После просмотра результата работы программы вы сразу сможете заметить, что алго
ритм сортировки строк являетеялексикографическим: все слова начинающиеся с про
писных букв, помещаются в начало списка, и только после них следуют слова с первой
строчной буквой. (Обычно так сортируются записи в телефонных книгах.) Чтобы
сгруппировать слова без учета регистра, используйте режим S t r i n g . C A S E _ I N S E N S I T I V E _
ORDER, как показано в последнем вызове s o r t ( ) предыдущего примера.
В цикле while производятся случайные значения для поиска, до тех пор пока в массиве
не будет найдено одно из них.
Метод A r r a y s . b i n a r y S e a r c h ( ) возвращает большее или равное нулю значение, если
при поиске был найден нужный элемент. В противном случае методом возвращается
отрицательное значение, представляющее позицию, где следует вставить элемент,
если бы отсортированный массив поддерживался «вручную». Получаемое значение:
-(пози ц и я для в с та в к и ) - 1
Позиция для вставки —это индекс первого элемента, оказавшегося больше, чем ключ
поиска, или значение а . size(), если все элементы массива меньше ключа поиска.
632 Глава 16 • Массивы
p u b lic c la s s A lp h a b e tic S e a rc h {
p u b lic s ta tic v o id m a in (S trin g [] arg s) {
String[] sa = Generated.array(new String[30],
new RandomGenerator.String(5));
A rra y s .s o rt(s a , S t r i n g . CASE_INSENSITIVE_ORDER);
S ystem . o u t . p r i n t l n ( A r r a y s . t o S t r i n g ( s a ) ) ;
in t in d e x = A rra y s .b in a ry S e a rc h (s a , sa[10],
S trin g .C A S E _ IN S E N S IT IV E _ O R D E R );
S y s te m .o u t.p rin tln (" I n d e x : "+ i n d e x + "\ n ”+ s a [ in d e x ] ) ;
}
> /* O u t p u t :
[bkIna, cQ rG S; cXZJo, dLsmw, eGZMm, Eq U C B , g w sq P , h K cxr,
HLGEa, HqXum, HxxHv, JMRoE, 3mzMs, M esbt, MNvqe, nyGcF,
ogoYW, O n eO E , OWZnT, RF3QA, rU kZ P , s g q ia , s lJ rL , suEcU,
uTpnX, v p fF v , W HkjU, xxEA3, YN zbr, zDyCy]
Index: 10
HxxHv
*// /:~
Резюме
В этой главе было показано, 4ToJava предоставляет основательную поддержку низко
уровневых массивов фиксированного размера. Такие массивы отдают предпочтение
производительности перед гибкостью, как и в модели массивов С и С++. В исходной
BepcHHjava низкоуровневые массивы фиксированного размера были абсолютно не
обходимы — не только потому, что создател и ^уа решили включить примитивные
типы (также по соображениям производительности), но и потому, что поддержка
контейнеров в этой версии была минимальной. Таким образом, в ранних B epcnaxJava
массивы всегда были предпочтительным вариантом.
В последующих BepcnaxJava поддержка контейнеров значительно улучшилась, и теперь
контейнеры превосходят массивы во всех отношениях, кроме производительности,
даже при том, что производительность контейнеров была значительно улучшена. Как
указывается в других местах книги, проблемы производительности все равно обычно
никогда не возникают там, где вы склонны искать их причины.
С добавлением автоматической упаковки и обобщений хранение примитивов в кон
тейнерах существенно упростилось, что становится дополнительным доводом в пользу
замены низкоуровневых массивов контейнерами. Так как обобщения предоставляют
контейнеры, безопасные по отношению к типам, массивы уже не обладают преиму
ществами и в этом отношении.
Как упоминалось в этой главе (и как вы увидите при попытке использовать их), обоб
щения плохо совместимы с массивами. Часто даже когда вам удается как-то совместить
обобщения с массивами (см. следующую главу), во время компиляции все равно будут
выдаваться предупреждения.
При обсуждении конкретных примеров создатели H3MKaJava несколько раз напрямую
говорили мне, что следует использовать контейнеры вместо массивов (я использовал
массивы для демонстрации конкретных приемов, поэтому у меня такого выбора не было).
Все эти аспекты показывают, что при программировании H aJava п осл едн и х версий
следует отдавать предпочтение контейнерам перед массивами. И только если вы б у
дете твердо убеж дены в том, что производительность создает проблемы (а переход на
массивы изм енит ситуацию ), переработайте код для использования массивов.
Это заявление кажется довольно смелым, но в некоторых языках вообще нет низкоуров
невых массивов фиксированного размера. В них есть только контейнеры с изменяемым
размером, существенно превосходящие по своей функциональности традиционные
массивы C /C ++/Java. Например, в Python1 имеется тип l i s t , который использует
базовый синтаксис массива, но обладает существенно большей функциональностью —
он даже может использоваться для наследования:
#: a rra y s / P y th o n L is ts .p y
a L is t = [1, 2, 3, 4, 5]
p rin t ty p e (a L is t) # < type 'lis t'>
p rin t a L is t # [1, 2, 3j 4, 5]
продолжение &
1 См. www.Python.org.
634 Глава 16 • Массивы
Базовый синтаксис Python был представлен в предыдущей главе. Здесь список соз
дается простым заключением последовательности объектов, разделенных запятыми,
в квадратные скобки. Результат представляет собой объект с типом времени выпол
нения l i s t (вывод команд p r i n t приводится в комментариях в той же строке). При
выводе списка выводится такой же результат, как при использовании метода A r r a y s .
to S trin g () Bjava.
Создание '«среза», то есть субпоследовательности списка, осуществляется включением
оператора : в операцию индексирования. Тип l i s t содержит много других встроенных
операций.
—определение класса; базовые классы перечисляются в круглых скобках. Вну
M y L is t
три класса команды def определяют методы, а первый аргумент метода автоматически
эквивалентен t h i s в языке Java, если не считать того, что в Python он задается явно,
а идентификатор se lf определяется по соглашению (то есть не является ключевым
словом). Обратите внимание на автоматическое наследование конструктора.
Хотя в Python все данные представляются объектами (включая целочисленные
и вещественные типы), у программиста все равно остается лазейка; для оптимизации
частей кода, критичных по быстродействию, можно написать расширения на С, С++
или специальном языке Pyrex, предназначенном для простого ускорения кода. Таким
образом достигается «объектная чистота» без ущерба для быстродействия.
Язык P H P 1 идет еще дальше: в нем существует только один тип массива, который
используется как для массивов с целочисленным индексированием, так и для ассо
циативных массивов (Мар).
После многолетней эвoлюцииJava интересно поразмыслить над тем, стоило бы вклю
чать поддержку примитивов и низкоуровневых массивов, если бы работа над языком
началась заново? Если бы их не было, можно было бы получить по-настоящему «чи
стый» объектно-ориентированный язык (что бы там ни говорили, Java не является
«чистым» ОО-языком, притом именно из-за низкоуровневого балласта). Исходный
1 См. wz 0w .p h p .n e t .
Резюме 635
/T ~ T-- ------------ т п г
8
___ Производит [ AbstractMap j
■--------1i---------
j LlstIterator j**-/----- List ] j Set |j Queue A "-""!.
<r^zr' SortedMap
SortedSet
- % - - - _ ________ ~ I I
г----- ------- 1 I H ashM apj
TreeMap
i AbstractList [ i AbstractSet i
. ---------
IdentityHashMap
Z T
LinkedHashMap
| H ashSet| TreeSet Hashtable
WeakHashMap (Legacy)
Ф ___
LinkedHashSet Comparable w ^ r Comparator
Вспомогательные
3 . f~ ^ классы ^~N
J222L) ^ rra y U s^ [ AbstractSequentialList i Collections
3 3
Arrays
]
5tack
LinkedList
(унаследовано)
[
□ Enum Set и EnumMap, специальные реализации S e t и Мар для перечислений (см. главу 19).
□ Некоторые вспомогательные возможности класса C o l l e c t i o n s .
Блоки с контуром из длинных штрихов представляют абстрактные классы; также на
диаграмме присутствуют классы, имена которых начинаются с « A b s t r a c t » . На первый
взгляд это выглядит несколько странно, но это всего лишь вспомогательные част ичные
реализации некоторого интерфейса. Например, создавая собственную версию S e t , вы
не будете начинать с интерфейса S e t с реализацией всех методов; вместо этого вы опре
делите класс, производный от A b s t r a c t S e t , и выполните минимальную необходимую
работу для создания нового класса. Тем не менее библиотека контейнеров содержит
достаточную функциональность практически для любых ситуаций, так что обычно на
классы, имена которых начинаются с A b s t r a c t , можно не обращать внимания.
Заполнение контейнеров
Хотя проблема вывода контейнеров была решена, заполнение контейнеров страдает
от тех же недостатков, что и j a v a . u t i l . A r r a y s . Как и в случае с A r r a y s , существует
вспомогательный класс C o l l e c t i o n s со статическими служебными методами, к числу
которых принадлежит метод с именем f i l l ( ) . Как и версия из A r r a y s , эта реализация
f i l l ( ) просто дублирует одну ссылку на объект в контейнере. Кроме того, она рабо
тает только для объектов, но полученный список можно передать конструктору или
методу a d d A l l ( ) :
//: c o n ta in e rs / F illin g L is ts .ja v a
// М етоды C o l l e c t i o n s . f i l l ( ) и C o lle c tio n s .n C o p ie s ( ).
im p o rt ja v a .u til.* ;
продолжение &
638 Глава 17 • Подробнее о контейнерах
cla s s S trin g A d d re s s {
p riv a te S trin g s;
p u b lic S trin g A d d re s s (S trin g s) { th is .s = s; >
p u b lic S trin g to S trin g () {
return s u p e r.to S trin g () + " " + s;
}
>
p u b lic c la s s F illin g L is ts {
p u b lic s ta tic v o id m a in (S trin g [] arg s) {
L is t< S tr in g A d d r e s s > lis t= new A r r a y L i s t < S t r i n g A d d r e s s > (
C o lle c tio n s .n C o p ie s ( 4 , new 5 t r i n g A d d r e s s ( " H e l l o " ) ) ) ;
S y s te m .o u t.p rin tln ( lis t);
C o lle c tio n s .fill( lis t, new S t r i n g A d d r e s s ( " W o r l d J " ) ) ;
S y s te m .o u t.p r in tln ( lis t);
}
} /* O u t p u t : (S a m p le )
[S trin g A d d re ss@ 8 2 b a 4 1 H e llo , S trin g A d d re s s @ 8 2 b a 4 l H e l l o ,
S trin g A d d re ss @ 8 2 b a 4 1 H e llo , S trin g A d d re s s g ) 8 2 b a 4 1 H e l l o ]
[S trin g A d d re ss@ 9 2 3 e 3 0 W o r ld ! , S trin g A d d re s s @ 9 2 3 e 3 0 W o r ld !,
S trin g A d d re ss @ 9 2 3 e 3 0 W o r ld ! , S trin g A d d re ss @ 9 2 3 e 3 0 W o r ld !]
*///:~
Этот пример демонстрирует два способа заполнения C o lle c tio nссылками на один
объект. Первый, C o l l e c t i o n s . n C o p i e s ( ) , создает объект L is t, который передается кон
структору и используется для заполнения A r r a y L i s t .
Метод t o S t r i n g ( ) в S t r i n g A d d r e s s вызывает метод O b j e c t . t o S t r i n g ( ) , который воз
вращает имя класса с шестнадцатеричным беззнаковым представлением хеш-кода
объекта (сгенерированным методом h a s h C o d e ( ) ) . Из результатов видно, что все ссылки
указывают на один объект, причем ситуация не изменяется и после вызова второго
метода C o l l e c t i o n s . f i l l ( ) . Полезность метода f i l l ( ) снижается еще и из-затого, что
он может только заменять элементы, уже присутствующие в списке, и не может до
бавлять новые элементы.
Решение с Generator
Практически все подтипы C o l l e c t i o n имеют конструктор, который получает другой
объект C o l l e c t i o n , используемый для заполнения нового контейнера. Таким образом,
для простого создания тестовых данных достаточно построить класс с конструктором,
в аргументах которого передается объект G e n e r a t o r (см. главы 15 и 16) и количество
объектов q u a n t i t y :
//: n e t / m in d v ie w / u t il/ C o lle c t io n D a ta .ja v a
|| Заполнение коллекции с использованием объекта генератора.
package n e t.m in d v ie w .u til;
im p o rt ja v a .u til.* ;
//: c o n t a in e r s / C o lle c t io n D a t a T e s t . ja va
im p o rt j a v a . u t i l . * ;
im p o rt n e t.m in d v ie w .u til.* ;
Генераторы Мар
Аналогичный подход можно использовать и для м а р , но для этого потребуется класс
P a i r , поскольку для заполнения М а р при каждом вызове метода next() генератора
должна производиться пара объектов (ключ и значение):
//: n e t/ m in d v ie w / u til/ P a ir.ja v a
package n e t.m in d v ie w .u til;
// Д ва р а з н ы х генератора:
p u b lic M apD ata(Generator< K> genK, G enerator< V > genV,
in t q u a n tity ) {
fo r( in t i = 0j i < q u a n tity ; i+ + ) {
p u t(g e n K .n e x t(), g e n V .n e x t());
}
>
// Генератор ключа и о д н о з н а ч е н и е :
p u b lic M apD ata(Generator< K> genK, V v a lu e , in t q u a n tity ){
fo r( in t i = 0; i < q u a n tity ; i+ + ) {
p u t(g e n K .n e x t(), v a lu e );
}
}
// Ite ra b le и генератор значения:
p u b lic M apD ata (Ite ra b le < K > genK, G en erator< V > genV) {
f o r ( K key : genK) {
p u t(k e y, g e n V .n e x t());
>
>
// I t e r a b l e и одно зн ачен ие:
p u b lic M apD ata (Ite ra b le < K > genK, V v a lu e ) {
fo r(K key : genK) {
put(key, v a lu e );
>
}
// Обобщенные в с п о м о г а т е л ь н ы е мето д ы :
p u b lic s ta tic <K,V> M a pD ata < K ,V >
m ap (G e n e ra to r< P a ir< K ,V > > gen, in t q u a n tity ) {
return new M a p D a t a < K ,V > ( g e n , q u a n tity );
>
p u b lic s ta tic <K,V> M a pD ata < K ,V >
m ap(G enerator< K> genK, G en erator< V > genV, in t q u a n tity ) {
return new M a p D a t a < K ,V > ( g e n K , genV, q u a n tity );
>
p u b lic s ta tic <K,V> M apD ata< K,V >
m ap(G enerator< K> genK, V v a lu e , in t q u a n tity ) {
return new M a p D a t a < K ,V > ( g e n K , v a lu e , q u a n tity );
}
p u b lic s ta tic <K,V> M a pD ata < K ,V >
m a p (Ite ra b le < K > genK, G enerator< V> genV) {
return new M a p D a t a < K ,V > ( g e n K , genV);
>
p u b lic s ta tic <K,V> M a pD ata < K ,V >
m a p (Ite ra b le < K > genK, V v a lu e ) {
return new M a p D a t a < K ,V > ( g e n K , v a lu e );
>
> ///:~
Таким образом, появляется выбор между использованием одного генератора
G e n e r a t o r < P a i r < K , V > > , двух раздельных генераторов, одного генератора и константы,
реализации I t e r a b l e (а значит, любых реализаций C o l l e c t i o n ) и генератора или I t e r a b l e
и значения. Обобщенные вспомогательные методы сокращают объем необходимого
кода для создания объектов M a p D a t a .
Рассмотрим пример использования M a p D a t a . Генератор L e t t e r s также реализует I t e r -
a b l e , предоставляя I t e r a t o r ; это позволяет использовать его для тестирования методов
M a p D a t a . m a p ( ) , которые работают с I t e r a b l e :
642 Глава 17 • Подробнее о контейнерах
//: c o n ta in e rs / M a p D a ta T e s t.ja v a
im p o rt ja v a .u til.* ;
im p o rt n e t.m in d v ie w .u til.* ;
im p o rt s ta tic n e t.m in d v ie w .u til.P rin t.* ;
return new F l y w e i g h t M a p ( ) {
p u b lic S e t< M a p .E n try < S trin g ,S trin g > > en tryS et() {
retu rn new E n t r y S e t ( s i z e ) ;
>
>;
>
s ta tic M a p < S trin g ,S trin g > шар = new F l y w e i g h t M a p ( ) ;
p u b lic s ta tic M a p < S trin g ,S trin g > c a p ita ls ( ) {
return map; // Полная к а р т а
>
p u b lic s ta tic M a p < S trin g ,S trin g > c a p ita ls ( in t s iz e ) {
return s e le c t(s iz e ); // Ч а с т и ч н а я карта
>
s ta tic L is t< S trin g > names =
new A r r a y L i s t < S t r i n g > ( m a p . k e y S e t ( ) ) j
// В с е им ен а :
p u b lic s ta tic L is t< S trin g > nam es() { return nam es; }
// Ч а сти ч н ы й с п и с о к :
p u b lic s ta tic L is t< S trin g > n a m e s (in t s i z e ) {
return new A r r a y L i s t < S t r i n g > ( s e l e c t ( s i z e ) . k e y S e t ( ) ) j
>
p u b lic s ta tic v o id m a in (S trin g [] arg s) {
p rin t(c a p ita ls (1 0 ));
p rin t(n a m e s (1 0 ));
p rin t(n e w H a s h M a p < S trin g ,S trin g > (c a p ita ls (3 )))j
p rin t(n e w L in k e d H a s h M a p < S tr in g ,S trin g > (c a p it a ls ( 3 ) )) ;
p rin t(n e w T r e e M a p < S t r in g ,S t r in g > ( c a p it a ls ( 3 ) ) );
p rin t( n e w H a s h ta b le < S tr in g ,S trin g > (c a p ita ls (3 )));
p rin t(n e w H a sh S e t< S trin g > (n a ra e s(6 )));
p rin t(n e w L in k e d H a s h S e t< S trin g > ( n a m e s ( 6 ) ) ) j
p rin t(n e w T re e S e t< S trin g > (n a m e s(6 )));
p rin t(n e w A rra y L is t< S trin g > (n a m e s (б ) ) ) ;
p rin t(n e w L in k e d L is t< S trin g > (n a m e s (6 )))j
p r in t ( c a p it a ls ( ) . g et("B R A ZIL"));
>
} /* O u tp ut:
{ A L G E R IA = A lg ie rs, A NGOLA=Luanda, BEN IN = Porto-N ovo,
BOTSWANA=Gaberone, B U L G A R I A = S o f ia , BURKINA
FASO = O u ag ad ou go u , BURUNDI=Buju mbur3j CAMEROON=Yaounde, САРЕ
VER D E= Praia, CENTRAL AFRICAN R E PU B LIC= B a ng ui>
[ALGERIA, ANGOLA, B ENIN, BOTSWANA, B U LG ARIA, BURKINA FASO,
BURUNDI, CAMEROON, CAPE V ER D E, CENTRAL AFRICAN R E P U B L I C ]
{BEN IN = Porto-N ovo, ANGOLA=Luanda, A LG E R IA = A lgie rs}
{ A L G E R IA = A lg ie rs, A NGOLA= Luanda, B EN IN = Porto-N ovo}
{ A L G E R IA = A lg ie rs, A NGOLA=Luanda, B EN IN = Porto-N ovo}
{ A L G E R IA = A lg ie rs, A NGOLA= Luanda, BEN IN = Porto-N ovo}
[B U LGA RIA , BURKINA FA SO, BOTSWANA, B EN IN , ANGOLA, ALGERIA]
[ALGERIA, ANGOLA, B EN IN , BOTSWANA, B U L G A R IA , BURKINA FASO]
[ALGERIA, ANGOLA, B E N IN , BOTSWANA, B U LG ARIA, BURKINA FASO]
[ALGERIA, ANGOLA, B E N IN , BOTSWANA, B U L G A R IA , BURKINA FASO]
[ALGER IA, ANGOLA, B EN IN , BOTSWANA, B U L G A R IA , BURKINA FASO]
B ra s ilia
*///:~
p u b lic c la s s C o u n tin g I n te g e rL is t
exten ds A b s tr a c tL is t< I n te g e r> {
p riv a te in t s iz e ;
p u b lic C o u n tin g I n te g e rL is t(in t siz e ) {
th is .s iz e = s iz e < 0 ? 0 : s iz e ;
>
p u b lic In teg er g e t( in t in d e x ) {
return I n te g e r.v a lu e O f(in d e x )j
>
p u b lic in t s iz e () { return s iz e j >
p u b lic s ta tic v o id m a in (S trin g [] args) {
S y s te m .o u t. p rin tln (n e w C o u n tin g I n te g e rL is t( 3 0 ) ) j
}
} /* O u t p u t :
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 2 9]
*///:~
p u b lic c la s s C o u n tin g M a p D a ta
exten ds A b s tra c tM a p < I n te g e r,S trin g > {
p riv a te in t s iz e ;
p riv a te s ta tic S trin g [] chars =
"А В С D E F G H I 3 К L M N 0 P Q R S T U V W X Y Z"
.s p lit( " ");
p u b lic C o u n tin g M a p D a ta (in t siz e ) {
i f ( s i z e < 0) t h i s . s i z e = 0j
t h is .s iz e = siz e ;
}
p riv a te s ta tic cla s s En try
im p le m e n ts M a p .E n try < In te g e r,S trin g > {
in t in d e x ;
E n try (in t in d e x ) { th is .in d e x = in d e x j >
p u b lic b o o le a n e q u a ls (O b je c t о) {
return In te g e r.v a lu e O f(in d e x ).e q u a ls (o );
>
p u b lic In te g e r g etK ey() { return in d e x ; }
p u b lic S trin g g e tV a lu e () {
return
c h a rs [in d e x % c h a rs .le n g th ] +
I n te g e r.to S trin g ( in d e x / c h a rs .le n g th );
}
p u b lic S trin g s e tV a lu e (S trin g v a lu e ) {
th row new U n s u p p o r t e d O p e r a t i o n E x c e p t i o n ( ) ;
}
p u b lic i n t hashCode() {
re tu rn I n te g e r.v a lu e O f(in d e x ).h a s h C o d e ();
>
}
p u b lic S e t< M a p .E n try < In te g e r,S trin g > > en tryS et() {
// L in k e d H a s h S e t с о х р а н я е т порядок инициализации:
S e t< M a p .E n try < In te g e r,S trin g > > e n trie s =
new L i n k e d H a s h S e t < M a p . E n t r y < I n t e g e r , S t r i n g > > ( ) ;
fo r( in t i = 0; i < s iz e ; i++)
e n trie s .a d d (n e w E n try (i));
return e n trie s ;
}
p u b lic s ta tic v o id m a in (S trin g [] arg s) {
System.out.println(new CountingMapData(60));
>
} /* O u t p u t :
{0=A0, 1=B0, 2=C0, 3 =D0, 4= E 0 , 5=F0, 6=G0 ,
7:=H0, 8=10,
9=30, 10=K0, 11= L0, 12=M0, 13=1N0, 14=00, 15=1P0, 16=Q0,
17=R0, 18=S0, 19=T0, 20=:U0, 21==V0, 22==W0, 23:=X0, 24=Y0,
25=Z0, 2 6 = A lj 27=B1, 28=‘C l , 29:=D1, 30=■ El, 31:=F1, 32=G1,
33=H1, 34=11, 35=31, 36=:K1, 37:=L1, 38==M1, 39 =N1, 40=01,
41= P1, 42=Q1, 43=R1, 44=‘S I , 45: =T1, 46== ui, 47: =V1, 48=W1,
49=X1, 50=Y1, 51=Z1, 52==A2, 53:=B2, 54==C2, 55:=D2, 56=E2,
57= F2, 58=G2, 59=H2>
*///:~
650 Глава 17 • Подробнее о контейнерах
на слова при помощи T e x t F i l e , после чего использует слова как источник данных
для C o l l e c t i o n . Покажите, что ваше решение работает.
5. (3) Измените пример CountingMapData.java и обеспечьте полноценную реализацию
паттерна «Легковес»; для этого добавьте класс En tryS et вроде использованного
в Countries.java.
Функциональность Collection
В таблице 17.1 перечислены все возможные операции C o l l e c t i o n (кроме методов,
унаследованных от корневого класса o b j e c t ) , а значит, и операции со c n n c K a M n ( L i s t )
и множествами ( S e t ) . (Объекты L i s t также обладают несколькими дополнительными
возможностями.) Карты м а р не являются производными от C o l l e c t i o n и поэтому рас
сматриваются отдельно.
Метод Предназначение
Iterator<T> fterator() Возвращает объект Iterator<T>, который можно использовать для
перемещения по элементам контейнера
boolean remove(Object) Если аргумент содержится в контейнере, то один его экземпляр
из него удаляется. Возвращает true, если удаление прошло
успешно (необязательный метод)
boolean Удаляет все элементы, содержащиеся в аргументе. Возвращает
removeAII(Collection<?>) true, если было проведено хотя бы одно удаление (необязатель
ный метод)
boolean Оставляет в контейнере только те элементы, которые присут
retainAII(Coltectlon<?>) ствуют в аргументе («пересечение» на языке теории множеств).
Возвращает true, если произошли какие-либо изменения (необя
зательный метод)
int size() Возвращает количество элементов в контейнере
Object[] toArray() Возвращает массив, содержащий все элементы контейнера
<T> T[] toArrayCT[] а) Возвращает массив, содержащий все элементы контейнера, тип
которых совпадает с типом массива-аргумента а (вместо Object)
Заметьте, что здесь нет метода g e t ( ) для произвольной выборки элементов. Причина
в том, что в иерархию C o l l e c t i o n также входит множество ( S e t ) , у которого имеется
своя собственная внутренняя структура расположения элементов (что лишает доступ
к произвольному элементу всякого смысла). Таким образом, если вам понадобится
просмотреть все элементы коллекции, придется применить итератор.
Следующий пример демонстрирует все перечисленные методы. И снова они работают
со всяким контейнером, производным от C o l l e c t i o n , в качестве «наименьшего общего
кратного» здесь используется контейнер A r r a y L i s t :
//: c o n ta in e rs / C o lle c tio n M e th o d s .ja v a
// О п е р а ц и и , которые м о г у т вы полняться
// с о в с е м и р а з н о в и д н о с т я м и C o l l e c t i o n .
im p o rt ja v a .u til.* ;
im p o rt n e t . m i n d v i e w . u t i l . * j
im p o rt s ta tic n e t.m in d v ie w .u til.P rin t.* ;
продолжение £>
652 Глава 17 • Подробнее о контейнерах
Необязательные операции
Методы, выполняющие различные операции добавления и удаления, относятся к не
обязательным операциям в интерфейсе C o l l e c t i o n . Это означает, что реализация класса
не обязана предоставлять работоспособные определения таких методов.
Такое определение интерфейса весьма необычно. Как вы уже видели, в объектно-ори
ентированном проектировании интерфейс определяет контракт. Он говорит: «Как бы
вы ни реализовали этот интерфейс, я гарантирую, что вы сможете отправлять объекту
эти сообщения1». «Необязательная» операция нарушает этот фундаментальный прин
цип; речь идет о том, что некоторые методы не выполняют осмысленных действий.
Вместо этого они возбуждают исключения! Казалось бы, безопасность типов во время
компиляции теряется.
Впрочем, не все так плохо. Если операция является необязательной, компилятор все
равно ограничивает вас вызовом методов этого интерфейса. Ситуация не похожа на
динамические языки, в которых можно вызвать любой метод для любого объекта
и только во время выполнения узнать, сработает ли конкретный вызов12. Кроме того,
многие методы, которые получают объект C o l l e c t i o n в аргументе, ограничиваются
чтением из C o l l e c t i o n , а все методы чтения в C o l l e c t i o n являются обязательными.
Зачем определять методы как «необязательные»? Это предотвращает размножение
интерфейсов в архитектуре. Во многих библиотеках контейнеров появляется не
вразумительная куча интерфейсов, описывающих все вариации на основную тему.
В интерфейсах даже невозможно отразить все особые случаи, потому что кто-нибудь
всегда сможет изобрести новый интерфейс. Благодаря подходу с «неподдерживаемыми
операциями» в библиотеке контейнеров^уа достигается очень важная цель: контей
неры просты в изучении и использовании. Неподдерживаемые операции — особый
случай, который можно отложить до возникновения необходимости. Однако для того,
чтобы этот подход работал, необходимо выполнение некоторых условий.
1Я использую термин «интерфейс» для описания как формального ключевого слова interface,
так и в более общем смысле «методы, поддерживаемые любым классом или субклассом».
2 В таком изложении это кажется странным и не очень практичным, но как было показано ранее
(особенно в главе 14), такое динамическое поведение иногда открывает очень полезные воз
можности.
6 54 Глава 17 • Подробнее о контейнерах
Неподдерживаемые операции
Самая частая причина неподдерживаемых операций — контейнеры, работающие на
базе структуры данных с фиксированным размером. Такие контейнеры создаются
в результате преобразования массива в L i s t методом A r r a y s . a s L i s t ( ) . Также можно
заставить любой контейнер (включая М а р ) выдавать исключения U n s u p p o r t e d O p
e r a t i o n E x c e p t i o n посредством использования «немодифицирующих» методов класса
try { c .c le a r( ) ; } c a tc h (E x c e p tio n e) {
S y s te m .o u t.p rin tln (" c le a r(): " + e);
}
try { c .a d d (" X * '); } c a tc h (E x c e p tio n e) {
S y s te m .o u t.p rin tln (" a d d (): " + e);
>
try { c .a d d A ll(c 2 ); } c a tc h (E x c e p tio n e) {
S y s te m ,o u t.p rin tln ( " a d d A llQ : " + e);
>
try { c .re m o v e (" C " ); } c a tc h (E x c e p tio n e) {
S y s te m .o u t.p rin tln ( " re m o v e Q : " + e);
>
// М етод L i s t . s e t ( ) изменяет значение,
// н о н е и з м е н я е т р а з м е р с т р у к т у р ы д ан н ы х :
try {
lis t.s e t( 0 j "X ");
} c a tc h ( E x c e p tio n e) {
S y s te m .o u t.p rin tln ( " L is t.s e t( ): " + e)j
}
>
p u b lic s ta tic v o id m a in (S trin g [] arg s) {
L is t< S trin g > lis t =
A rra y s .a s L is t(" A В С D E F G H I J К L " .s p lit( " "));
te s t( " M o d ifia b le Copy", new A r r a y L i s t < S t r i n g > ( l i s t ) ) ;
te s t(" A rra y s .a s L is tQ " , lis t) ;
te s t(" u n m o d ifia b le L is t()" ,
C o lle c t io n s . u n m o d ifia b le L is t(
new A r r a y L i s t < S t r i n g > ( l i s t ) ) ) ;
}
> /* O u t p u t :
--- M o d ifia b le Copy —
--- A rra y s .a s L is t( ) —
re ta in A llQ : ja v a .la n g .U n s u p p o rte d O p e ra tio n E x c e p tio n
re m o v e A ll(): ja v a .la n g .U n s u p p o rte d O p e ra tio n E x c e p tio n
c le a r(): j a v a . l a n g . U n s u p p o rte d O p e ra tio n E x c e p tio n
a d d ( ) : ja v a .la n g .U n s u p p o rte d O p e ra tio n E x c e p tio n
a d d A ll( ): j a v a . l a n g . U n s u p p o rte d O p e ra tio n E x c e p tio n
rem o ve(): ja v a .la n g .U n s u p p o rte d O p e ra tio n E x c e p tio n
— u n m o d ifia b le L is t( ) —
re ta in A llQ : ja v a .la n g .U n s u p p o rte d O p e ra tio n E x c e p tio n
re m o v e A ll(): ja v a .la n g .U n s u p p o rte d O p e ra tio n E x c e p tio n
c le a r(): ja v a .la n g .U n s u p p o rte d O p e ra tio n E x c e p tio n
add( ): j a v a . l a n g . U n s u p p o rte d O p e ra tio n E x c e p tio n
a d d A ll( ) : ja v a .la n g .U n s u p p o rte d O p e ra tio n E x c e p tio n
rem ove(): ja v a .la n g .U n s u p p o rte d O p e ra tio n E x c e p tio n
L is t . s e t(): j a v a . l a n g . U n s u p p o rte d O p e ra tio n E x c e p tio n
*H/:~
Так как A r r a y s . a s L i s t ( ) создает L i s t на базе массива фиксированного размера, вполне
разумно, что поддерживаются только операции, не изменяющие размер массива. Любой
метод, приводящий к изменению размера нижележащей структуры данных, выдает ис
ключение U n s u p p o r t e d O p e r a t i o n E x c e p t i o n для обозначения вызова неподдерживаемого
метода (ошибка программирования).
Учтите, что результат A r r a y s . a s L i s t ( ) всегда можно передать в аргументе конструктора
любой реализации C o l l e c t i o n (или воспользоваться методом a d d A l l ( ) , или статическим
методом C o l l e c t i o n s . a d d A l l ( ) ) для создания обычного контейнера, допускающего
656 Глава 17 • Подробнее о контейнерах
Функциональность List
На базовом уровне контейнеры L i s t просты в использовании. Чаще всего используются
методы a d d ( ) для добавления объектов, g e t ( ) для их выборки по одному и i t e r a t o r ( )
для получения итератора последовательности.
Вдобавок, на самом деле есть два типа списков: основной A r r a y L i s t , который особенно
хорош при извлечении произвольных элементов из контейнера, и гораздо более мощный
список L i n k e d L i s t (который не разрабатывался для быстрого доступа к произвольным
элементам, но зато он обладает более полным набором методов).
Методы следующего примера охватывают совокупность различных видов действий:
то, что может каждый список (метод b a s i c T e s t ( ) ) , перемещение по списку посредством
итератора ( i t e r M o t i o n ( ) ) в сравнении с изменением списка с помощью итератора
( i t e r M a n i p u l a t i o n ( ) ) , просмотр результатов операций со списками ( t e s t V i s u a l ( ) )
и операции, доступные только в классе L i n k e d L i s t .
Функциональность List 657
// : c o n ta in e rs / L is ts .ja v a
// Оп ерац ии с о с п и с к а м и L is t.
im p o rt j a v a . u t i l . * ;
im p o rt n e t . m i n d v i e w . u t i l . * ;
im p o r t s t a t i c n e t.m in d v ie w .u til.P rin t.* ;
p u b lic c la s s L is ts {
p riv a te s ta tic b o o le a n b;
p riv a te s ta tic S trin g s;
p riv a te s ta tic in t i;
p riv a te s ta tic I t e ra to r< S trin g > it;
p riv a te s ta tic L is tIte ra to r< S trin g > lit;
p u b lic s ta tic v o id b a s ic T e s t(L is t< S trin g > а) {
a .a d d ( l, "x"); // Д о б а в л е н и е в п о з и ц и и 1
a .a d d ("x '* ); // Добавление в кон ц е
// Добавление коллекции:
a .a d d A ll( C o u n trie s .n a m e s (2 5 ) );
// Д о б а в л е н и е ко л л е к ц и и с п о з и ц и и 3:
а . a d d A ll(3 , C o u n t r ie s . nam es(25));
b = a .c o n ta in s (" l" ) ; // З н а ч е н и е п р и с у т с т в у е т ?
// В ся коллекция?
b = a .c o n ta in s A ll(C o u n trie s .n a m e s (2 5 ));
// Списки о б е с п е ч и в а ю т п р о и з в о л ьн ы й д о с т у п -
// быстрый д ля A r r a y L i s t , медленный д л я L in k e d L is t:
s = a .g e t( l) ; // П о л у ч е н и е (типизованного) объекта в по з и ц и и 1
i = a .in d e x O f( " l" ); // П о л у ч е н и е индекса объекта
b = a .is E m p ty () ; // С п и с о к со д е р ж и т эл ем ен ты ?
it = a .ite ra to r( ) j // Обычный I t e r a t o r
lit = a .lis tlte ra to r() ; // L is tI te ra to r
lit = a .lis tI te ra to r (3 ) ; // Н а ч а т ь с по з и ц и и 3
i = a .la s tI n d e x O f( " l" ); // П о с л е д н е е вхождение
a .re m o v e (l); / / У д а л е н и е в по з и ц и и 1
a .re m o v e (''3 ")j // Удаление з а д а н н о го объ екта
a .s e t(l, "y"); // З а п и с а т ь "у" в позицию 1
// О с та в и ть в се элементы, присутствующие в а ргум ен те
// (пересечен ие д вух множ еств):
а . re ta in A ll(C o u n trie s .n a m e s (2 5 ));
// У д а л е н и е в с е х э л е м е н т о в , п р и с у тс тв у ю щ и х в а р г у м е н т е :
а . r e m o v e A ll( C o u n t r ie s . nam es(25));
i = a .s iz e (); // Определение размера
a .c le a r() ; // Удаление в с е х элем ентов
}
p u b lic s ta tic v o id ite rM o tio n (L is t< S trin g > а) {
L is tIte ra to r< S trin g > it = a .lis tI te ra to r() ;
b = it.h a s N e x t() ;
b = it.h a s P r e v io u s ( ) j
s = it.n e x t();
i = it.n e x tI n d e x () ;
s = it.p re v io u s ();
i = it.p re v io u s I n d e x ( ) ;
>
p u b lic s ta tic v o id ite rM a n ip u la tio n (L is t< S trin g > а) {
L is tI te ra to r< S trin g > it = a .lis tI te ra to r() ;
it.a d d ( " 4 7 " ) ;
// Переход к элем енту после a d d ( ) :
продолжение &
658 Глава 17 • Подробнее о контейнерах
it.n e x t( ) j
// Удаление эл ем ента за в н о в ь со зд ан н ы м :
it.re m o v e ()j
// П е р е х о д к э л е м е н т у п о с л е rem ove():
it.n e x t( ) j
/ / И зм ен ен ие э л е м е н т а п о с л е у д а л е н н о г о :
it.s e t(" 4 7 " );
>
p u b lic s ta tic v o id te s tV is u a l(L is t< S trin g > а) {
p rin t( a );
L is t< S trin g > b = C o u n trie s.n a m e s(2 5 )j
p rin t( " b = " + b);
a .a d d A ll( b );
a .a d d A ll( b ) ;
p rin t( a );
// Вставка, удаление и замена элементов
// с использованием L is tI te ra to r:
L is tI te ra to r< S trin g > x = a .lis tI te ra to r(a .s iz e ()/ 2 );
x ,a d d ("o n e ");
p rin t(a );
p rin t(x .n e x t())j
x .re m o v e ();
p rin t( x .n e x t( ) );
x .s e t(" 4 7 " );
p rin t( a );
// П ер еб о р с п и с к а в обратном направлении:
x = a .lis tI te ra to n (a .s iz e ( )) ;
w h ile ( x .h a s P re v io u s ( ))
p rin tn b (x .p re v io u s () + " ");
p rin t( ) ;
p rin t( " te s tV is u a l fin is h e d " ) j
>
// Н екоторы е о п е р а ц и и , поддерживаемые т о л ь к о д ля L in k e d L is t:
p u b lic s ta tic v o id te s tL in k e d L is t( ) {
L in k e d L is t< S trin g > 11 = new L i n k e d L i s t < S t r i n g > ( ) ;
ll.a d d A ll( C o u n tr ie s .n a m e s ( 2 5 ) ) ;
p rin t( ll) ;
// И н т е р п р е т и р о в а т ь к а к с т е к - занесение:
ll.a d d F irs t(" o n e " );
11. a d d F in s t(" tw o ");
p rin t( ll) ;
// А н а л о г ч т е н и я с вершины с т е к а (без извлечения):
p rin t( ll.g e tF irs t( ) ) ;
// А н а л о г и з в л е ч е н и я из с т е к а :
p rin t( ll.re m o v e F irs t( ) );
p rin t( ll.re m o v e F irs t( ) );
// И н т е р п р е т и р о в а т ь как очередь с извлечением элементов
// с к о н ц а :
p r i n t (1 1 . rem oveLast( ) ) ;
p rin t( ll) ;
>
p u b lic s ta tic v o id m a in (S trin g [] arg s) {
// Каждый р а з с о з д а е т с я и заполняется новый с п и с о к :
b a s ic T e s t(
new L i n k e d L i s t < S t r i n g > ( C o u n t r i e s . n a m e s ( 2 5 ) ) ) ;
Set и порядок хранения 659
b a s ic T e s t(
new A r r a y L i s t < S t r i n g > ( C o u n t r i e s . n a m e s ( 2 5 ) ) ) ;
ite rM o tio n (
new L i n k e d L i s t < S t r i n g > ( C o u n t r i e s . n a m e s ( 2 5 ) ) ) ;
ite rM o tio n (
new A r r a y L i s t < 5 t r i n g > ( C o u n t r i e s . n a m e s ( 2 5 ) ) ) j
ite rM a n ip u la tio n (
new L i n k e d L i s t < S t r i n g > ( C o u n t r i e s . n a m e s ( 2 5 ) ) ) j
ite rM a n ip u la tio n (
new A r r a y L i s t < S t r i n g > ( C o u n t r i e s . n a m e s ( 2 5 ) ) ) j
te s tV is u a l(
new L i n k e d L i s t < S t r i n g > ( C o u n t r i e s . n a m e s ( 2 5 ) ) ) j
te s tL in k e d L is t( ) ;
>
> /* (Execu te t o see o u tp u t) * // /:~
c la s s SetType {
in t i;
p u b lic S e tT y p e (in t n) { i = nj }
p u b lic b o o le a n e q u a ls (O b je c t о) {
return о i n s t a n c e o f S e t T y p e && ( i == ( ( S e t T y p e ) o ) . i ) ;
>
p u b lic S trin g to S trin g () { return I n te g e r.to S tr in g ( i) ; }
>
c la s s HashType e x te n d s SetType {
p u b lic H a sh T y p e (in t n) { super(n); }
p u b lic in t hashCode() { return i; }
}
5et и порядок хранения 661
j a v a . l a n g . C o m p a ra b le
ja v a .la n g .C la s s C a s tE x c e p tio n : HashType c a n n o t be c a s t t o
j a v a . l a n g . C o m p a ra b le
*///:~
SortedSet
Э л е м е н т ы отсортированного м н о ж е с т в а SortedSet гарантированно хранятся в порядке
сортировки, что позволяет задействовать д о п о л н и т е л ь н у ю функ ц и о н а л ь но с т ь , обе
с п е ч и в а е м у ю и н т е р ф е й с о м SortedSet.
Очереди
Если не считать многопоточных версий, B j a v a SE5 используются всего две реализации
Q u e u e : L i n k e d L i s t и P r i o r i t y Q u e u e . Они различаются прежде всего по поведению упо
рядочения, а не по производительности. В следующем примере задействованы многие
реализации Q u e u e (не все из них работают в этом примере), включая многопоточные
реализации. Элементы помещаются с одного конца очереди, а извлекаются с другого:
//: c o n ta in e rs / Q u e u e B e h a v io r. ja v a
// Сравнение поведения некоторых очередей
im p o rt ja v a .u til.c o n c u rre n t.* ;
im p o rt ja v a .u til.* ;
im p o rt n e t.m in d v ie w .u til.* ;
p u b lic cla s s Q u e u e B e h a v io r {
p riv a te s ta tic in t count = 10;
s ta tic <T> v o i d test(Q u eue< T> queue, G en erato r< T> gen) {
fo r( in t i = 0; i < co u nt; i+ + )
queue. o ffe r ( g e n . n e x t( ) );
w h ile (q u e u e .p e e k () != n u l l )
S y s te m .o u t.p rin t(q u e u e .re m o v e () + " ");
S y s te m .o u t. p r i n t l n ( ) ;
}
s ta tic c la s s Gen i m p l e m e n t s G e n e ra to r< S trin g > {
S trin g [] s = ( " o n e tw o t h r e e fo u r fiv e s ix seven " +
"e ig h t n in e te n " ) .s p lit( " ")J
in t i;
p u b lic S trin g next() { retu rn s [ i+ + ] ; }
}
Очереди 665
Приоритетные очереди
Приоритетные очереди были кратко представлены в главе 11. Давайте рассмотрим
более интересную задачу: список дел, в котором каждый объект содержит строку и два
приоритета, первичный и вторичный. Порядок следования элементов списка снова
определяется реализацией C o m p a r a b l e :
//: c o n ta in e rs / T o D o L is t.ja v a
// Н е т р и в и а л ь н о е и с п о л ь з о в а н и е P rio rity Q u e u e .
im p o rt ja v a .u til.* ;
Деки
Дек (двусторонняя очередь) ведет себя как очередь, но с возможностью добавления
и удаления элементов с обоих концов. В L i n k e d L i s t имеются методы поддержки опе
раций дека, но явно определенного интерфейса для деков в стандартных библиотеках
Java не существует. Таким образом, L i n k e d L i s t не может реализовать этот интерфейс,
и вы не можете выполнить восходящее преобразование к интерфейсу D e q u e (в отличие
от интерфейса Q u e u e в предыдущем примере). Впрочем, класс D e q u e можно создать
посредством композиции и просто предоставить доступ к соответствующим методам
из L i n k e d L i s t :
//: n e t/ m in d v ie w / u til/ D e q u e .ja v a
// С о з д а н и е Deque и з L in k e d L is t.
package n e t.m in d v ie w .u til;
im p o rt ja v a .u til.* ;
Карты (Мар)
Как вы узнали в главе 11, отличительная особенность карты (также называемой acco-
циативтлм массивом) состоит в том, что в ней хранятся связи «ключ-значение» (пары),
чтобы к значениям можно было обращаться по ключу. Стандартная библиотека^уа
содержит различные типы карт: классы H a s h M a p , T r e e M a p , L i n k e d H a s h M a p , W e a k H a s h M a p ,
C o n c u r r e n t H a s h M a p и I d e n t i t y H a s h M a p . Они имеют одинаковый интерфейс (поскольку все
реализуют интерфейс М а р ) , но отличаются в производительности, порядке добавления
и представления пар объектов, сроках хранения объектов и способах сравнения ключей.
Количество реализаций интерфейса Мар убедительно свидетельствует о важности этой
разновидности контейнеров.
668 Глава 17 • Подробнее о контейнерах
Для более глубокого понимания Мар полезно посмотреть, как конструируются ассо
циативные массивы. Возьмем очень простую реализацию:
//: c o n t a in e r s / A s s o c ia t i v e A r r a y . ja va
// С в я зы в а н и е ключей с о з н а ч е н и я м и ,
im p o rt s ta tic n e t.m in d v ie w .u til.P rin t.* ;
ocean : d a n cin g
tre e : ta ll
earth : brown
sun : warm
d a n cin g
* / / / :~
Производительность
Для карт очень важен вопрос производительности. Например, использование ли
нейного поиска в g e t ( ) при поиске ключа —достаточно медленный процесс. Карта
H a s h M a p значительно увеличивает скорость такого поиска. Вместо неторопливого
поиска ключа она работает со специальным значением, называемымхеш-тсодсш (hash
code). Хеш-код —это способ преобразования некоторой информации об объекте в «от
носительно уникальное» целое число, связанное с этим объектом. Метод h a s h C o d e ( )
встроен в корневой класс O b j e c t , поэтому хеш-код может генерироваться для любых
o6beKTOBjava. Карта H a s h M a p задействует метод Ь а з Ь С о Ь е О д л я быстрого поиска ключа.
Это приводит к впечатляющему всплеску производительности1.
p u b lic c la s s Maps {
p u b lic s ta tic v o id p rin tK e y s (M a p < In te g e r,S trin g > map) {
p rin tn b (" S iz e = " + m a p .s iz e () + ", ")j
p rin tn b (" K e y s : ");
p rin t(m a p .k e y S e t())j // С о з д а н и е м н о ж е с т в а ключей
}
p u b lic s ta tic v o id te s t( M a p < I n te g e r,S trin g > map) {
p r in t ( m a p . g e t C l a s s ( ) . g e tS im p le N a m e () ) ;
m a p .p u tA ll(n e w C o u n tin g M a p D a ta (2 5 ));
// Map р е а л и з у е т п о в е д е н и е 'S e t' д л я клю чей:
m a p .p u tA ll(n e w C o u n tin g M a p D a ta (2 5 ));
p rin tK e y s (m a p );
// С о з д а н и е коллекции значений:
p rin tn b (" V a lu e s : ");
p rin t(m a p .v a lu e s ());
p rin t(m a p );
p rin t(" m a p .c o n ta in s K e y (ll): " + m a p .c o n ta in s K e y (ll));
p rin t( " m a p .g e t( ll): " + m a p .g e t( ll) ) ;
p rin t( " m a p .c o n ta in s V a lu e (\ " F 0 \ " ) : "
+ m a p .c o n ta in s V a lu e ( " F 0 " ) ) ;
In teg er key = m a p .k e y S e t ( ) .it e r a t o r ( ) .n e x t ( ) ;
p rin t( " F ir s t key in map: " + key);
m a p .re m o v e (k e y );
p rin tK e y s (m a p );
m a p .c le a r();
p rin t( ''m a p .is E m p ty ( ): " + m a p .is E m p ty ( )) ;
m ap . p u t A l l ( new C o u n t i n g M a p D a t a ( 2 5 ) ) ;
// Операции с Set изменяю т Мар:
m ap . k e y S e t ( ) . r e m o v e A l l ( m a p . k e y S e t ( ) ) ;
p rin t( " m a p .is E m p ty ( ) : " + m a p .is E m p ty Q );
)
p u b lic s ta tic v o id m a in ( S trin g [ ] arg s) {
test(new H a sh M a p < In te g e r,S t r i n g > ( ) ) ;
test(new T re e M a p < I n te g e r,S trin g > ())j
test(new L in k e d H a s h M a p < In te g e r,S trin g > ( ) ) ;
test(n ew I d e n tity H a s h M a p < In te g e r,S t r in g > ( ) ) ;
te s t( n e w C o n c u rre n tH a s h M a p < In te g e rjS trin g > ());
te s t( n e w W e a k H a sh M a p < In te g e r,S trin g > ());
>
} /* O u tp u t:
HashMap
S ize = 25, Keys: [15, 8, 23, 16, 7, 22, 9, 21, 6, 1, 14,
24, 4, 19, 11, 18, 3, 12, 17, 2, 13, 20j, 1 0 , 5 , 0]
V a lu e s : [ P 0 , 1 0 , X 0 , Q 0 , H 0 , W0, 30,
Y0, V0, , G 0 , B0, 0 0 ,
E0, T0, L 0 , S 0 , D 0 , M0, R0, C 0 , N0, U0, K 0 , F 0 , A 0]
{15=P0, 8=10 , 23 =X0, 16=Q0, 7=H0, 22 =W0j, 9 = 3 0 , 21=V0, 6=G0,
1=B0, 14= 00, 24= Y 0 , 4=E0, 19= T0, 11= L 0 , 18= S0, 3=D0, 12=M0,
17=R0, 2=C0, 13= N 0 , 20=U0, 10=K0, 5= F 0 , 0=A0)
m a p .c o n ta in s K e y (ll): tru e
m a p .g e t( ll) : L0
m a p .c o n ta in s V a lu e ( " F 0 " ) : tru e
F irs t key i n map: 15
S iz e = 24, Keys: [8, 23, 16, 7, 22, 9, 21, 6, 1, 14, 24, 4,
19, 11, 18, 3, 12, 17, 2, 13, 20, 10, 5, 0]
m a p .is E m p ty (): tru e
m a p .is E m p ty (): tru e
672 Глава 17 • Подробнее о контейнерах
Метод printK eys() показывает, как получить коллекцию (C o lle c tio n ) из карты (Мар).
Метод keySet() производит множество (Set), элементами которого являю тся все клю
чи таблицы. Благодаря усовершенствованной поддержке вывода в Java SE5 можно
просто вывести результат вызова метода v a lu e s(), который производит коллекцию
C o lle c t io n со всеми значениями карты (Мар). (Заметьте, что ключи карты должны
быть уникальны, в то время как значения могут повторяться.) Так как полученные
коллекции значений и ключей неразрывно связаны с картой, любое проведенное в них
изменение немедленно отразится на ней.
Остальная часть программы демонстрирует простые операции с картами (Мар), а затем
проверяет сущ ествующ ие типы карт.
SortedMap
Если вы используете отсортированную карту типа SortedMap (к ней относится один
класс TreeMap), все ключи гарантированно следуют в определенном порядке, что по
зволяет определить в интерфейсе SortedMap несколько дополнительных методов.
//: containers/SortedMapDemo.java
// Операции с TreeMap.
import ja v a .u til.* ;
import net.mindview.util.*;
import s ta tic net.m indview .util.Print.*;
public class SortedMapDemo {
public static void main(String[] args) {
TreeMap<Integer,String> sortedMap =
new TreeMap<Integer,String>(new CountingMapData(10));
print(sortedMap ) ;
Integer low = sortedMap.firstKey()j
Integer high = sortedMap.lastKey();
print(low);
print(high);
Iterator<Integer> i t = sortedMap.keySet().iterator();
fo r(in t i = 0; i <= 6; i++) {
Карты (Map) 673
Здесь пары хранятся в порядке сортировки по ключам. Так как в ТгееМар существует
некоторое подобие порядка, концепция «позиции» становится более логичной, и по
является возможность получения первых и последних элементов, а также подкарт.
LinkedHashMap
К арта LinkedHashMap производит хеш ирование для ускорения работы, но при этом
также поддерживает порядок вставки пар объектов при переборе (H an p n M ep ,S ystem .
o u t. p rin tln ( ) выполняет перебор, и вы видите его результаты). Вдобавок конструктор
позволяет настроить LinkedHashMap в соответствии с принципом выборки «наименее
используемые идут первыми» (LRU, least-recently-used). Алгоритм учитывает часто
ту доступа к элементам, и те из них, что востребовались реже других, помещаются
в начало списка. Это помогает создавать программы, которые в целях экономии пе
риодически высвобождают ресурсы. Вот простой пример, иллюстрирующий обе эти
характеристики:
//: containers/LinkedHashMapDemo.java
// Операции с LinkedHashMap.
import ja v a .u til.* ;
import net.m indview .util.*;
import sta tic net.m indview .util.Print.*;
Хеширование и хеш-коды
В примерах главы 11 в качестве ключей HashMap использовались уже существующие
классы. Эти примеры работали, так как готовые классы содержат все необходимое для
правильной работы класса в качестве ключа хеш-таблицы.
Часто ошибки происходят, когда разработчик сам пишет класс, который должен стать
ключом HashMap, и забывает реализовать весь необходимый служебный код. Например,
рассмотрим систему прогноза погоды, которая ставит в соответствие объекты Groundhog
(сурок) объектам P re d ictio n (прогноз погоды). Все выглядит довольно просто — вы
создаете два класса: Groundhog для ключей таблицы и P re d ictio n для сопоставленного
ему значения:
//: containers/Groundhog.java
// Выглядит весьма правдоподобно, но не работает.
//: containers/Prediction.java
// Прогнозирование погоды no поведению сурка,
import ja v a .u til.* ;
//: containers/SpringDetector.java
// Какая же будет погода?
import ja v a .la n g .re fle c t.* ;
import ja v a .u til.* ;
import sta tic net.m indview .u til.P rint.* ;
Все выглядит просто, но тем не менее не работает. Проблема состоит в том, что класс
Groundhog унаследован от общего корневого iomccaObject, адлягенерирования хеш-кода
объекта используется реализация hashCode() этого класса. По умолчанию этот метод
возвращает адрес объекта. Таким образом, хеш-код первого экземпляра Groundhog(3)
676 Глава 17 • Подробнее о контейнерах
Понимание hashCode()
Рассмотренный пример — только начало. Он показывает, что если не переопределить
методы equals() и hashCode() для ключей, применяемых в хеш ированных структурах
данных (HashSet, HashMap, LinkedHashSet и LinkedHashMap), они просто не смогут нор
мально функционировать. Но чтобы качественно решать такие задачи, необходимо
понимать, что происходит внутри хеш ированных структур данных.
Во-первых, для чего вообще нужно хеш ирование? Д ля поиска объекта по другому
объекту. Но это можно сделать с помощью TreeMap или реализовать собственную карту
(Мар). В отличие от реализации с хеш ированием следующий пример реализует Мар на
базе двух контейнеров A rra y L ist. В отличие от AssociativeArray.java, он включает полную
реализацию интерфейса Мар, чем и объясняется присутствие en try S e tQ :
//: containers/SlowMap.java
// Реализация Мар на базе списков A rra yList.
import ja v a .u til.* ;
import net.m indview .util.*;
Метод put() просто помещает ключи и значения в соответствующий объект A rra yL ist.
В соответствии с правилами интерфейса Мар он должен возвращать старый ключ или
null, если старый ключ отсутствует.
Также в соответствии со спецификацией Мар метод g et() выдает n u ll, если ключ от
сутствует в SlowMap. Если ключ существует, то он используется для получения чис
лового индекса, обозначающего его позицию в списке keys, а число — для получения
ассоциированного значения из списка values индексированием. Обратите внимание:
в get() ключ key относится к типу Object, а не к параметризованному типу К, как можно
было бы ожидать (и как было в AssodativeArray.java). Это следствие того, что обобщения
были включены в H3bncJava на поздней стадии, — если бы обобщения присутствовали
в язы ке с самого начала, метод get () мог бы использовать тип своего параметра.
М етод M ap.entrySet() долж ен производить множество объектов Map.Entry, О днако
Map.Entry — интерфейс, описывающий структуру, зависимую от реализации, поэтому
если вы захотите создать собственный тип Мар, то вам такж е придется определить
реализацию Map. Entry:
Карты (Map) 679
//: containers/MapEntry.java
// Простая реализация Map.Entry
// для пробной реализации Мар.
import ja v a .u til.* ;
Здесь очень простой класс MapEntry хранит и получает ключи и значения. Он исполь
зуется в e n try S e t() для создания множества (S et) пар «ключ-значение». Обратите
внимание: e n try S e t() использует HashSet для хранения пар, а MapEntry использует
упрощенный подход, просто используя вызов hashCode() для ключа. И хотя это ре
шение выглядит очень просто и вроде бы работает в тривиальном тесте из SlowMap.
main(), оно не может считаться правильной реализацией из-за копирования ключей
и значений. П равильная реализация entrySet() должна создавать представление для
Мар (вместо копирования), через которое может производиться модификация исходной
карты (чего копия сделать не позволяет). Одно из возможных реш ений представлено
в упражнении 16.
Обратите внимание: метод equals() в MapEntry должен проверять как ключи, так и зна
чения. Смысл метода hashCode() будет описан ниже.
Представление содержимого SlowMap в формате String автоматически создается методом
to S trin g (), определенным в AbstractMap.
1 Идеальная хеш-функция реализована в KnaccaxJava SE5 EnumMap и EnumSet, потому что пере
числения (enum) определяют фиксированное количество экземпляров (см. главу 19).
Карты (Map) 681
Так как «ячейки» хеш-таблицы часто называют узловыми группами (buckets), массив,
представляющий фактическую таблицу, называют buckets. Чтобы распределение было
более равномерным, количество узлов обычно выбирают из простых чисел1. Заметьте,
что таблица —это массив списков LinkedList, поэтому она автоматически приспособле
на к возникновению коллизий — каждый новый элемент группы просто добавляется
в конец списка соответствующей группы. И xoTnJava не позволяет создавать массивы
обобщений, вы можете создать ссъигку на такой массив. В данном случае удобно вы
полнить восходящее преобразование к такому массиву для предотвращения лишних
приведений типа в коде.
В методе put() для ключа вызывается метод hashCode(), а полученное значение преоб
разуется в положительное число. Чтобы полученныйрезультат ограничивался размером
1 Как оказывается, простые числа —это не самый лучший размер для хеш-таблиц, и последние
реализации хеш-таблиц Bjava используют размер, равный степени числа 2 (после тщательного
тестирования). Операции деления или получения остатка на современных процессорах выпол
няются относительно медленно, а для метода возведения ключа в квадрат вместо деления можно
использовать маскирование крайних разрядов. Метод g e t ( ) вызывается чаще других, поэтому
операция получения остатка (%) отнимает большую часть времени, а метод «степеней двойки»
устраняет эти затраты (правда, это может повлиять на работу некоторых методов hashCode ()).
Карты (Map) 683
массива buckets, для хеш-кода применяется оператор остатка от деления. Если в полу
ченном узле таблицы находится ссылка n u ll, значит, список элементов еще пуст, и для
этого узла создается новый список Lin k ed List для хранения элементов. Впрочем, чаще
процесс протекает по-другому: в списке производится поиск дубликатов, и если они
есгь, старое значение помещается в oldvalue, а на его место помещается новое значение.
Ф лаг found следит за тем, была ли найдена старая пара «ключ-значение», если она не
найдена, то новая пара добавляется к концу списка.
Метод get() вычисляет индекс в массиве buckets так же, как это делается в put() (это
важно, чтобы программа пришла к той же ячейке). Если в ней имеется список элементов
LinkedList, в нем проводится окончательный поиск.
Переопределение hashCode()
Теперь, когда вы понимаете, как работает хеширование, вам будет проще разобраться
в реализации hashCode ().
Прежде всего вы не управляете созданием конкретного значения, используемого для
индексирования массива узловых групп. Оно зависит от емкости конкретного объекта
HashMap, и эта емкость может варьироваться в зависимости от текущего заполнения
контейнера и коэффициента загрузки таблицы (этот термин будет объяснен позд
нее). Значение, возвращаемое вашим методом ЬазЬСс^е(),будет затем обработано для
684 Глава 17 • Подробнее о контейнерах
Есть и еще один аспект: хорошо продуманный метод hashCode() возвращает равномерно
распределенные значения. Если результаты метода имеют склонность к «скучиванию»,
HashMap или HashSet в нескольких местах будут излишне загружены, что немедленно
приведет к сниж ению эф ф ективности поиска по сравнению со скоростью поиска
в равномерно загруженной таблице.
В своей книге EffectiveJava (издательство Addison-Wesley, 2001)Д ж ош уа Блош пред
лагает простой рецепт создания подходящего метода hashCode().
4. Возвратите re su lt.
5. Взгляните на полученный результат, возвращаемый методом hashCode(), и удо
стоверьтесь в том, что одинаковые экземпляры имеют совпадающий хеш-код.
s = str;
created.add(s);
7/ id - общее количество экземпляров данной строки,
// используемых классом CountedString:
for(String s2 : created)
if(s2 .equ als(s))
id++;
Так как у всех питомцев в этом примере есть имена, они сортируются сначала по типу,
затем по имени внутри типа.
Написать правильные реализации hashCode() и equals() для нового классадостаточ-
но непросто. В проекте A pacheJakarta Commons (jakarta.apache.org/commons), в пакете
lang можно найти несколько инструментов, способных облегчить эту задачу (проект
включает в себя несколько других потенциально полезных библиотек; похоже, он
становится ответом сообщ ествапрограмм истов^уа сообществу программистов С++
www.boost.org).
26. (2) Добавьте в CountedString поле char, которое также инициализируется в конструк
торе. Измените методы hashCode() и equals(), чтобы в них включалось значение
этого поля.
27. (3) Измените метод hashCode() из CountedString.java —удалите использование комби
нации с id. Продемонстрируйте, что CountedString по-прежнему работает как ключ.
В чем проблема такого решения?
28. (4) Измените код netymindview/utilyH^uple.java и сделайте его классом общего назначе-
ния —добавьте методы hashCode(), equals() иреализуйте СотрагаЫедля всехтипов
Tuple.
Выбор реализации
Вероятно, вы уже поняли, что на самом деле существуют только четыре фундаменталь
ные разновидности контейнеров: карта (Мар), список (L is t), множество (Set) и очередь
Выбор реализации 689
(Queue), но для
каждой имеется несколько различных реализаций. Если вам необходима
функциональность определенного интерфейса, на основании чего вы примете решение
о подходящей для него реализации?
Чтобы ответить на этот вопрос, надо осознать, что каждый компонент имеет свои
характерные свойства, преимущества и недостатки. Например, на диаграмме классов
в начале этой главы видно, что контейнеры Hashtable, Vector и Stack не вредят старому
коду. С другой стороны, в новом коде эти классы лучше вообще не применять.
Разные типы Queue в б и б л и о т е к е ^ у а различаются только по тому, как они получают
и передают значения (о том, почему это важно, рассказано в главе 21).
В определении различий между другими контейнерами часто исходят из того, на осно
ве каких структур данных, «физически» реализующих необходимый интерфейс, они
строятся. Это значит, что, например, при использовании списков A rra yL ist и LinkedList,
реализующих интерфейс L is t, базовые операции L is t будут работать одинаково неза
висимо от выбранного типа. Однако класс A rra y L ist основан на массиве, в то время как
класс Lin k ed L ist реализован как двунаправленный список: в нем хранятся отдельные
объекты, связанны е ссылками на предыдущ ий и следую щ ий элементы списка. По
этой причине список LinkedList хорошо подходиттогда, когда планируется выполнять
много вставок и удалений из середины списка. (В нем также имеются дополнительные
возможности, описанные в абстрактном классе A b stra ctS e q u e n tia lL ist.) В противном
случае список A rra y L is t обычно работает быстрее.
В качестве другого примера возьмем множества: интерфейс Set реализован классами
TreeSet, HashSet и LinkedHashSet1. Все они ведут себя по-разному: класс HashSet предна
значен для повседневного использования и обеспечивает хорошую скорость поиска.
LinkedHashSet хранит пары в порядке добавления, а множество TreeSet работает на базе
карты TreeMap и спроектировано специально как множество, постоянно находящееся
в упорядоченном состоянии. Вы выбираете реализацию в зависимости от того, что
вам необходимо.
Иногда разные реализации некоторого контейнера имеют общие операции, но отли
чаются по производительности. В этом случае реализация выбирается на основании
частоты использования той или иной операции и того, насколько быстро она должна
выполняться. В таких случаях решение должно приниматься по результатам тести
рования производительности.
Среда тестирования
Чтобы предотвратить дублирование кода и обеспечить логическую целостность
тестирования, я вы делил базовую ф ун кциональность тести рован и я в отдельную
программную среду. Следующий код определяет базовый класс, на основе которого
вы создаете список анонимных внутренних классов — по одному для каждого текста.
Каждый из внутренних классов вызывается как часть процесса тестирования. Такое
решение позволяет легко добавлять и удалять тесты.
Перед нами еще один пример паттерна проектирования «Шаблонный метод». Хотя мы
следуем типичному для «Шаблонного метода» приему переопределения метода Test.
te s t( ) в каждом конкретном тесте, в данном случае базовый код (который остается
неизменным) выделяется в отдельный класс T e ste r1. Тип тестируемого контейнера
задается обобщенным параметром с:
//: containers/Test.java
// Средадля проведения хронометражного тестирования контейнеров.
В каждом объекте Test хранится имя теста. При вызове метода te s t( ) должен пере
даваться тестируемый контейнер с «объектом передачи данных», содержащим раз
личные параметры конкретного теста: s iz e (количество элементов в контейнере),
loops (количество итераций в тесте) и т. д. Эти параметры могут использоваться (или
не использоваться) в каждом конкретном тесте.
Для каждого контейнера выполняется серия вызовов te s t() с разными TestParam, по
этому TestParam также содержит статические методы array(), упрощающие создание
массивов объектов TestParam. Первая версия a rra y() получает переменный список
аргументов с чередующимися значениями size и loops, а вторая получает также список,
внутренними значениями которого являются строки String, что позволяет использовать
ее для разбора аргументов командной строки:
//: containers/TestParam.java
// "Объект передачи данных".
v a ls [i] = Integer.decode(values[i]);
return array(vals);
}
} ///:~
операции. Учтите, что System. nanoTime () обычно выдает значения с точностью больше 1
(которая зависит от конкретной машины или операционной системы), что вносит не
которую погрешность в результаты.
Результаты могут изменяться от компью тера к компьютеру; тесты предназначены
только для сравнения относительной производительности разных контейнеров.
Выбор List
Ниже приводятся тесты производительности для основных операций L is t. Д ля срав
нения такж е приведены важнейш ие операции Queue. Д ля тестирования двух видов
контейнеров создаются два разных списка тестов. В данном случае операции Queue
относятся только к LinkedList.
//: containers/ListPerformance. java
// тестирование производительности операций
// при работе с L is t.
// {Args: 100 500} Небольшие значения, чтобы тесты
// не занимали много времени,
import ja v a .u til.* ;
import net.m indview .util.*;
Integer[] ia = Generated.array(Integer.class,
new CountingGenerator.Integer(), size);
return A rra ys.a sList(ia);
>
>;
arrayTest.setHeadline("Array as L ist");
arrayTest.timedTest();
Tester.defaultParams= TestParam.array(
10, 5000, 100, 5000, 1000, 1000, 10000, 200);
if(args.length > 0)
Tester.defaultParams = TestParam.array(args);
ListTester.run(new ArrayList<Integer>(), te sts);
ListTester.run(new LinkedList<Integer>(), tests);
ListTester.run(new Vector<Integer>(), tests);
Tester.fieldW idth = 12;
Tester<LinkedList<Integer>> qTest =
new Tester<LinkedList<Integer>>(
new LinkedList<Integer>(), qTests);
qTest.setHeadline("Queue te sts");
qTe s t.timedTest();
>
} /* Output: (Sample)
— Array as L ist —
size get set
10 130 183
100 130 164
1000 129 165
10000 129 165
ArrayList ■
size add get set iteradd insert remove
10 121 139 191 435 3952 446
100 72 141 191 247 3934 296
1000 98 141 194 839 2202 923
10000 122 144 190 6880 14042 7333
LinkedList
size add get set iteradd insert remove
10 182 164 198 658 366 262
100 106 202 230 457 108 201
1000 133 1289 1353 430 136 239
10000 172 13648 13187 435 255 239
-- Vector --
size add get set iteradd insert remove
10 129 145 187 290 3635 253
100 72 144 190 263 3691 292
1000 99 145 193 846 2162 927
10000 108 145 186 6871 14730 7135
___ yUcUc
Ппона focf*c
LcbLb
size addFirst addLast rmFirst rmLast
10 199 163 251 253
100 98 92 180 179
1000 99 93 216 212
10000 111 109 262 384
*///:~
для небольших тестов). Хотя результаты выглядят вполне разумно, можно переписать
тестовую среду так, чтобы вызов подготовительного метода (который в данном случае
будет включать вызов c le a r()) осуществлялся вне хронометражного цикла.
Чтобы получить правильные временные данные, для каждого теста необходимо точно
вычислить количество выполняемых операций и вернуть это значение из te s t().
Тесты get и set используют генератор случайных чисел для произвольных обраще
ний к L is t. И з выходных данных видно, что для списков на базе массивов и A rra yL ist
обращения выполняются очень быстро независимо от размера списка, тогда как для
LinkedList время обращения существенно увеличивается с увеличением размера спи
ска. Очевидно, связанные списки не очень хорошо подходят для частых произвольных
обращений.
Тест ite ra d d использует итератор для вставки новых элементов в середине списка.
Для A rra yL ist эти операции занимают больше времени с увеличением списка, но для
LinkedList они обходятся относительно дешево и выполняются за постоянное время
независимо от размера. Это логично: при вставке контейнер A rrayList должен выделять
место и сдвигать все ссылки вперед. Перемещение обходится дороже с ростом ArrayList.
Контейнеру LinkedList достаточно создать ссылку на новый элемент без модификации
остальных элементов списка, поэтому затраты остаются примерно одинаковыми и не
зависят от размера списка.
Тесты in se rt и remove используют ячейку 5 для вставки и удаления (вместо одного из
концов списка). У LinkedList предусмотрена особая обработка концов списка — это
повышает скорость при использовании LinkedList как очереди. Однако при добавле
нии или удалении элементов в середине списка вносятся затраты на произвольный
доступ, которые, как мы уже видели, изменяются в зависимости от реализации L ist.
При выполнении вставки и удаления в позиции 5 затраты на произвольный доступ
должны быть пренебрежимо малыми, и в результатах будет отражено время вставки
и удаления без учета специализированных оптимизаций для концов списка. Из резуль
татов видно, что затраты на вставку и удаление в LinkedList относительно невелики
и не изменяются с размером списка, но для A rra y L ist вставка обходится очень дорого,
а затраты возрастают с размером списка.
Из тестов Queue видно, как быстро LinkedList выполняет вставку и удаление на концах
списка; такое поведение оптимально для поведения очередей.
В общем случае достаточно вызвать T e ste r.ru n () с передачей контейнера и списка
tests. Однако в данном случае необходимо переопределить метод in it ia liz e ( ) ,4 T o 6 b i
список очищался и заполнялся заново перед каждым тестом — в противном случае
контроль за размером списка будет теряться между тестами. Класс L istT e ste r насле
дует от Tester и выполняет эту инициализацию с использованием CountinglntegerList.
Вспомогательный метод run() также переопределяется.
Также полезно сравнить эффективность обращения к массиву и контейнеру (прежде
всего A rra yL ist). В первом тесте main() создается специальный объект Test с исполь
зованием анонимного внутреннего класса. Метод i n i t i a l i z e ( ) переопределяется для
создания нового объекта при каждом вызове (хранимый объект container игнориру
ется, так что при вызове этого конструктора Tester аргумент container равен n u ll).
698 Глава 17 • Подробнее о контейнерах
Новый объект создается методом Generated. array() (который определяется в главе 16)
и A rra y s .a s L ls t() . В этом случае можно выполнить только два теста, потому что при
использовании L is t на базе массива вставка и удаление элементов невозможны, по
этому нужные тесты выбираются из списка te s ts при помощи метода L is t .s u b L is t ( ) .
Д ля операций get() и set() с произвольным доступом реализация L is t на базе массива
работает чуть быстрее A rra y L ist, но для LinkedList те же операции обходятся намного
дороже, потому что этот контейнер не рассчитан на операции произвольного доступа.
О т контейнера Vector следует держаться подальше; он включен в библиотеку только
для поддержки старого кода (и работает в программах только потому, что он был
адаптирован в L is t для обеспечения совместимости).
Вероятно, лучш е всего и сп ользовать A r r a y L is t по ум олчанию и переходить на
L in k e d L is t, если вам потребуется дополнительная ф ункциональность и л и же при
возникновении проблем производительности из-за слишком большого числа вставок
и удалений в середине массива. Если вы работаете с группой элементов ф иксиро
ванного размера, либо используйте реализацию L is t на базе массива (создаваемую
вызовом A rra y s . a s L is t( )), либо при необходимости — фактический массив.
CopyOnWriteArrayList — специализированная реализация L is t, предназначенная для
многопоточного программирования; она рассматривается в главе 21.
29 . (2) Измените код ListPerformance.java так, чтобы в L is t вместо Integer хранились объ
екты String. Для создания тестовых значений используйте генератор из главы 16.
30 . (3) Сравните производительность C o lle c tio n s .s o r t( ) для A rra y L is t и LinkedList.
31 . (5 ) Создайте контейнер, инкапсулирую щ ий массив Контейнер должен
S trin g .
поддерживать только добавление и удаление S trin g , так что при использовании
проблем с преобразованием типа не будет. Если размер внутреннего массива недо
статочен для следующего добавления, контейнер должен автоматически изменять
его размер. В main() сравните производительность контейнера с ArrayList<String>.
32 . (2) Повторите предыдущее упражнение для контейнера in t и сравните производи
тельность с ArrayList<Integer>. Включите в сравнение процесс инкрементирования
каждого объекта в контейнере.
33 . (5) Создайте реализацию которая во внутреннем пред
F a stT ra v e rsa lLin k e d List,
ставлении использует LinkedList для быстрых вставок/удалений и A rra y L ist для
быстрого перебора и операций get(). Протестируйте, внеся соответствующие из
менения в ListPerformance.java.
Опасности микротестов
При написании так называемых микротестов необходимо действовать внимательно,
не делать слишком много предположений и по возможности сузить тесты, чтобы в них
действительно измерялись только интересующие вас операции. Также следует поза
ботиться о том, чтобы тесты выполнялись достаточно долго для получения интересных
данных, и принять во внимание, что некоторые технологии Java H otS pot активизи
руются только при выполнении программы в течение определенного времени (это
обстоятельство также важно учитывать для программ с малым временем выполнения).
Выбор реализации 699
или
java RandomBounds upper
Заметьте, что класс LinkedHashSet вставляет элементы немного медленнее, чем HashSet;
так происходит из-за необходимости поддерживать не только хеш ированный контей
нер, но и связанны й список.
34 . (1) И змените код SetPerformance.java так, чтобы в Set вместо хранились объ
Integer
екты Strin g . Д ля создания тестовых значений используйте генератор из главы 16.
10 281 76 93
100 179 70 73
1000 267 102 72
10000 1305 265 97
- LinkedHashMap
size put get ite ra te
10 354 100 72
100 273 89 50
1000 385 222 56
10000 2787 341 56
IdentityHashMap
size put get ite ra te
10 290 144 101
100 204 287 132
1000 508 336 77
10000 767 266 56
Уаэ^иэсНМзп
WcdNndbrindp —
size put get ite ra te
10 484 146 151
100 292 126 117
1000 411 136 152
10000 2165 138 555
Hashtable
size put get ite ra te
10 264 113 113
100 181 105 76
1000 260 201 80
10000 1245 134 77
*/l/:~
Вставка для всех реализаций Мар (кроме IdentityHashMap) существенно замедляется
с увеличением размера Мар. Однако в общем случае поиск требует значительно мень
ших затрат, чем вставка, — и это хорошо, потому что находить элементы в контейнере
обычно требуется гораздо чаще, чем вставлять их.
Производительность Hashtable примерно равна производительности HashMap. Впрочем,
это и не удивительно — реализация HashMap должна заменить устаревший контейнер
Hashtable, поэтому она и спользуетте же механизмы хранения и поиска данных (о ко
торых будет рассказано позднее).
TreeMap показывает куда более скромные результаты. Как и TreeSet, она предназначена
для создания упорядоченных списков. В древовидной структуре, положенной в основу
TreeMap, элементы всегда хранятся в упорядоченном виде, а значит, их не нужно специ
ально сортировать. После заполнения TreeMap можно использовать методы keySet() для
получения множества (Set) ключей карты и to A rray() для создания массива ключей.
После этого статический метод A rra y s . binarySearch () позволит быстро найти нужные
объекты в отсортированном массиве. Конечно, все это делается только тогда, когда по
каким-либо причинам реализация HashMap вам не подходит, так как именно HashMap
обеспечивает быстрое нахождение объектов. К тому же создать карту HashMap из суще
ствующей TreeMap очень просто: для этого нужно просто определить один объект или
вызвать p u tA ll( ) . Вывод: когда вы используете таблицу, ваш им основным выбором
должен стать класс HashMap, и только при необходимости для часто сортируемой карты
(Мар) следует выбирать TreeMap.
704 Глава 17 • Подробнее о контейнерах
Реализация LinkedHashMap работает медленнее, чем HashMap, так как ей приходится под
держивать не только хешированный контейнер, но и связанный список (для сохранения
порядка вставки). По этой причине перебор выполняется быстрее.
Производительность ldentityHashMap отличается от других карт, поскольку в ней
сравнение выполняется не методом equals (), а оператором ==. Реализация WeakHashMap
будет описана позднее в этой главе.
35. ( 1) Измените код MapPerformance.java и включите в него тесты для реализации slowMap.
36. (5) Измените карту SlowMap, чтобы вместо двух списков ArrayList в ней хранился
один список объектов ArrayList или MapEntry. Покажите, что эта версия класса ра
ботает правильно. С помощью программы MapPerformance.java проверьте быстродей
ствие нового класса. После этого измените метод put() так, чтобы после помещения
очередной парът он проводил сортировку карты, а в методе get() для поиска ключа
примените метод Collections.binarySearch (). Сравните быстродействие этой версии
таблицы с предыдущими реализациями.
37. (2) Модифицируйте класс SimpleHashMap так, чтобы в нем использовались списки
ArrayList вместо LinkedList. Измените программу MapPerformance.java и сравните
быстродействие двух реализаций.
Если вы знаете, что в таблице HashMap будет храниться много элементов, имеет смысл
сделать ее емкость достаточно большой, чтобы предотвратить слишком частое про
ведение перехеширования1.
38. (3) В интерактивной докум ентации^К найдите описание класса HashMap. Создайте
карту HashMap, заполните ее элементами и определите ее коэффициент загрузки.
Оцените скорость поиска, а затем попытайтесь увеличить быстродействие, создав
новую карту HashMap с большей начальной емкостью, скопируйте старую версию
в новую и проведите проверку скорости поиска для новой таблицы.
39. (6)Добавьте в SimpleHashMap закрытый (private) метод rehash(), который проводит
перехеширование, когда коэффициент загрузки превышает 0,75. В процессе пере
хеширования удвойте число ячеек, затем проведите поиск первого простого числа
после полученного значения, чтобы узнать, сколько надо сделать новых ячеек.
! Джошуа Блош замечает в своем неофициальном сообщении: «...Я полагаю, что мы совершили
ошибку, позволив деталям реализации (таким, как размер таблицы и коэффициент ее загрузки)
„просочиться" в программные интерфейсы API. Клиент скорее должен был бы сообщать ожи
даемый максимальный размер таблицы, и эти параметры следовало выводить из него. Выбирая
параметры самостоятельно, клиент легко может навредить работе таблицы. Как крайность
можно вспомнить параметр capacityIncrement класса Vector. Никому не следовало пользо
ваться им, а нам не нужно было давать такую возможность. Если приравнять ему ненулевое
значение, трудоемкость операций возрастет с линейной до квадратичной. Другими словами,
исчезает производительность. Со временем мы поняли эти вещи. Если вы посмотрите на класс
IdentityHashMap, то увидите, что низкоуровневых параметров для настройки в нем нет».
706 Глава 17 • Подробнее о контейнерах
checkedSortedSet(
SortedSet<T>,
Class<T> type)
max(Collection) Находит наибольший или наименьший элемент аргумента
с использованием «естественного» сравнения объектов
min(Collection) Collection___________________________________________
max(Collectlon, Comparator) Находит наибольший или наименьший элемент Collection
с использованием сравнения по критерию Comparator
min(Coltection, Comparator)
indexOfSubList(List source, Находит начальный индекс первого вхождения target
в source - или -1, если вхождения не обнаружены
List target)
lastIndexOfSubList(List Находит начальный индекс последнего вхождения target
в source - или -1, если вхождения не обнаружены
source, List target)___________
replaceAII(List<T>, Заменяет все вхождения oldVal на newVal
T oldVal, T newVal)
reverse(List) Переставляет элементы в обратном порядке
reverseOrder() Возвращает Comparator для обратного порядка перебора
объектов коллекции, реализующей Comparable<T>. Вторая
reverseOrder(
версия изменяет порядок для переданного
Comparator<T>) объекта Comparator
rotate(List, int distance) Перемещает все элементы вперед на расстояние distance;
при этом элементы в конце коллекции перемещаются
в ее начало
shuffle(List) Генерирует случайную перестановку для заданного списка.
Первая форма использует внутренний генератор случайных
shuffle(List, Random)
чисел, во второй форме вы можете передать собственную
версию
sort(List<T>) Сортирует List<T> с использованием «естественного» упо
рядочения. Вторая форма позволяет задать свой объект
sort(List<T>,
Comparator для сортировки
Comparator<? superT> c)
copy(List<? superT> dest, Копирует элементы из src в dest
List<? extends T> src)
swap(List, int i, int j) Меняет местами элементы i и j в Ust. Скорее всего, работает
быстрее, чем реализация, которую вы напишете «вручную»
fill(List<? superT>, T x) Заменяет все элементы списка значением x
nCopies(int n, T x) Возвращает неизменяемый экземпляр List<T> размера n,
все ссылки в котором указывают на x
disjoint(Collection, Collection) Возвращает true, если две коллекции не имеют общих эле
ментов
frequency(Collectlon, Object x) Возвращает количество элементов в Collection, равных x
emptyList() Возвращает неизменяемый пустой экземпляр List, Мар или
Set. Возвращается обобщение, поэтому полученная реализа
emptyMap()
ция Collection параметризуется по нужному типу
emptySet()
Вспомогательные средства работы с коллекциями 707
//: containers/ListSortSearch.java
// Поиск и сортировка списков с использованием
// вспомогательного класса Collections,
import java.util.*;
import static net.mindview.util.Print.*;
40. (5) Создайте класс, содержащий два объекта String, и реализуйте для этого класса
интерфейс Comparable так, чтобы его метод проводил сравнение с учетом только пер
вой строки. Заполните массив и список ArrayList объектами этого класса, применив
710 Глава 17 • Подробнее о контейнерах
List<String> а = Collections.unmodifiableList(
new ArrayList<String>(data));
ListIterator<String> lit = a.listIterator();
print(lit.next()); // Чтение разрешено
//! lit.add("one"); // Но изменение невозможно.
Set<String> s = Collections.unmodifiableSet(
new HashSet<String>(data));
print(s); // Чтение разрешено
//! s.add("one")j// Но изменение невозможно.
// Для SortedSet:
Set<String> ss = Collections.unmodifiableSortedSet(
new TreeSet<String>(data))j
Map<String,String> m = Collections.unmodifiableMap(
new HashMap<StringjString>(Countries.capitals(6 )));
Сортировка и поиск в списках 711
// Для SortedMap:
Map<String,String> sm =
Collections.unmodifiableSortedMap(
new TreeMap<String,String>(Countries.capitals(6)));
>
} /* Output:
[ALGERIA, ANGOLA, BENIN, BOTSWANA, BULGARIA, BURKINA FASO]
ALGERIA
[BULGARIA, BURKINA FASO, BOTSWANA, BENIN, ANGOLA, ALGERIA]
{BULGARIA=5ofia, BURKINA FASO=Ouagadougou,
BOTSWANA=Gaberone, BENIN=Porto-Novo, ANGOLA=Luanda,
ALGERIA=Algiers}
*///:~
Вызов «unmodifiable-метода» для конкретного типа не запрещается во время ком
пиляции, но после выполнения преобразования вызовы таких методов, изменяю
щие содержимое конкретного контейнера, приведут к возникновению исключения
UnsupportedOperationException.
В любом случае, перед тем как сделать контейнер неизменяемым, его следует запол
нить какими-либо значимыми данными. После заполнения контейнера лучше всего
заменить существующую ссылку новой, полученной методом, делающим контейнер
«неизменяемым». Наложив однажды запрет на изменения, вы больше не сможете что-
либо модифицировать в этом контейнере. Вместе с тем, этот инструмент позволяет
хранить изменяемый контейнер в классе как его закрытый (private) член, а по запросу
возвращать ссылку на контейнер, допускающий только чтение. Таким образом, у вас
остается возможность менять содержимое контейнера в классе, но все внешние поль
зователи смогут лишь читать его.
new TreeSet<String>());
Map<String,String> га = Collections.synchronizedMap(
new HashMap<String,String>());
Map<String,String> sm =
Collections.synchronizedSortedMap(
new TreeMap<String,String>());
}
> ///:~
Новый контейнер следует немедленно передать соответствующему «synchronized-
методу», как показано выше. Тем самым вы сводите к минимуму опасность внешнего
доступа к несинхронизированному контейнеру.
Срочный отказ
В контейнерах Java работает механизм, не позволяющий изменять их содержимое сразу
нескольким процессам. Проблема возникает, если вы проводите перебор элементов
контейнера, и в это время некоторый другой процесс пытается вставить, удалить или
изменить один из объектов контейнера. Возможно, этот объект вами уже просмотрен,
или вам еще предстоит провести с ним некоторые действия, или размер контейнера
уменьшился после того, как вы узнали его с помощью метода size(), — опасных си
туаций, подобных описанным, может возникнуть великое множество. Библиотека
KOHTeftHepoBjava поддерживает механизм «срочного отказа», который следит за тем,
не было ли проведено в контейнере изменений, не относящихся к текущему процессу.
Если вдруг окажется, что кто-то еще изменяет контейнер, немедленно возбуждается
исключение ConcurrentModificationException. В этомизаключается принцип «срочно
го отказа» —механизм не пытается обнаружить проблему позднее или использовать
более сложный алгоритм.
Увидеть действие механизма срочного отказа своими глазами довольно просто —
нужно создать итератор, а затем добавить к коллекции новый элемент, поместив его
в позицию, на которую указывает итератор, как в следующем примере:
//: containers/FailFast.java
// Демонстрация механизма "срочного отказа",
iraport java.util.*;
Здесь исключение возникает потому, что объект помещается в контейнер после того,
как для этого контейнера был запрошен итератор. Возможность изменения двумя неза
висимыми частями программы одного и того же контейнера ведет к неопределенности,
Удержание ссылок 713
поэтому исключение предупреждает вас о том, что код нужно поменять — в нашем
случае надо запрашивать итератор после того, как все элементы будут добавлены
в контейнер.
Реализации ConcurrentHashMap, CopyOnWriteArrayList и CopyOnWriteArraySet используют
механизмы, предотвращающие выдачу исключения ConcurrentModificationExceptions.
Удержание ссылок
Библиотека java.lang.ref содержит набор классов, которые позволяют проводить
уборку мусора с большей гибкостью и особенно полезны при наличии в программе
больших объектов, потенциально опасных в смысле нехватки памяти. Существуют три
класса, унаследованных от абстрактного класса Reference: SoftReference, WeakReference
и PhantomReference. Каждый из них предоставляет свой способ контроля над уборщи
ком мусора, если только объект достижим с помощью одного из объектов Reference.
Если объект достижим, это значит, что где-то в вашей программе его можно обнару
жить. В обычном варианте это значит, что у вас есть ссылка на этот объект, хранящийся
в стеке, или ссылка на объект, в котором есть ссылка на рассматриваемый объект;
промежуточных звеньев может быть много. Если объект достижим, уборщик мусора
не имеет права удалить его, так как объект все еще используется программой. Если
объект недостижим, программа уже не может никаким образом обратиться к нему,
потому его удаление уборщиком мусора не вызовет осложнений.
Класс Reference используется тогда, когда необходимо продолжить хранить ссылку
на объект (вы хотите иметь возможность обратиться к этому объекту), но в то же
время вы хотите разрешить уборщику мусора удаление объекта. Таким образом, у вас
остается возможность обращаться к объекту, но если свободной памяти не осталось,
вы разрешаете освобождение этого объекта.
Объект Reference служит посредником между вами и обычной ссылкой, и других
обычных ссылок на объект (тех, что не «упакованы» в объектах Reference) существо
вать не должно. Если уборщик мусора обнаружит, что объект достижим напрямую по
обычной ссылке, он не станет его освобождать.
Классы SoftReference, WeakReference и PhantomReference «располагаются» по порядку:
каждый следующий «слабее», чем предыдущий, и отвечает за свой уровень дости
жимости. Класс SoftReference предназначен для реализации буферов памяти. Класс
WeakReference необходим при реализации «канонического отображения», гдеэкземпля-
ры объектов могут одновременно использоваться в различных местах программы, для
экономии памяти, что не предотвращает освобождения ключей (или значений). Класс
PhantomReference предназначен для планирования «предсмертных» действий объекта
и позволяет осуществить их более гибкими способами в сравнении со стандартным
механизмом финализации^уа.
Для SoftReference и WeakReference у вас есть выбор — помещать или не помещать их
во вспомогательный класс ReferenceQueue (механизм «предсмертной» очистки объ
екта), однако создание PhantomReference возможно только в объекте ReferenceQueue.
Вот простой пример:
714 Глава 17 • Подробнее о контейнерах
//: containers/References.java
// Демонстрация объектов Reference.
import java.lang.ref.*;
import java.util.*;
class VeryBig {
private static final int SIZE = 10000;
private long[] la = new long[SIZE];
private String ident;
public VeryBig(String id) { ident = id; }
public String toString() { return ident; }
protected void finalize() {
System.out.println("Finalizing ” + ident);
>
}
public class References {
private static ReferenceQueue<VeryBig> rq =
new ReferenceQueue<VeryBig>();
public static void checkQueue() {
Reference<? extends VeryBig> inq = rq.poll();
if(inq != null)
System.out.println("In queue: " + inq.get());
>
public static void main(String[] args) {
int size = 10;
// Или использовать размер, заданный в командной строке:
if(args.length > 0)
size = new lnteger(args[0]);
LinkedList<SoftReference<VeryBig>> sa =
hew LinkedList<SoftReferehce<VeryBig>>();
for(int i = 0; i < size; i++) {
sa.add(new SoftReference<VeryBig>(
new VeryBig("Soft " + i), rq));
System.out.println("lust created: " + sa.getLast());
checkQueue();
}
LinkedList<WeakReference<VeryBig>> wa =
new LinkedList<WeakReference<VeryBig>>();
for(int i = 0; i < size; i++) {
wa.add(new WeakReference<VeryBig>(
new VeryBig("Weak " + i), rq));
System.out.println("Dust created: " + wa.getLast());
checkQueue();
>
SoftReference<VeryBig> s =
new SoftReference<VeryBig>(new VeryBig("Soft"));
WeakReference<VeryBig> w =
new WeakReference<VeryBig>(new VeryBig("Weak"));
System.gc();
LinkedList<PhantomReference<VeryBig>> pa =
new LinkedList<PhantomReference<VeryBig>>O;
for(int i = 0; i < size; i++) {
pa.add(new PhantomReference<VeryBig>(
new VeryBig("Phantom " + i), rq));
System.out.println("lust created: " + pa.getLast());
checkQueue();
}
}
} /* (Execute to see output) *///:~
Удержание ссылок 715
WeakHashMap
Библиотека контейнеров содержит специальную карту (мар) для хранения «слабых»
ссылок.-WeakHashMap. Этот класс разработан, чтобы упростить реализацию «каноническо
го отображения»: с целью экономии памяти для конкретного значения создается только
один экземпляр. Когда программе понадобится этот объект, она выбирает его из карты
и использует (вместо того, чтобы создавать «с нуля»). Карту можно заполнить значени
ями во время инициализации, но обычно значения создаются по мере необходимости.
Так как этот механизм предназначен для экономии памяти, очень удобно то, что
WeakHashMap позволяет уборщику мусора автоматически удалять ключи и значения.
Вам не нужно делать ничего особенного, при помещении в WeakHashMap они будут ав
томатически «заворачиваться» в объекты WeakReference. Присоединенная процедура
позволяет проводить очистку, когда ключи становятся больше не нужны, как показано
в следующем примере:
//: containers/CanonicalMapping.java
// Демонстрация WeakHashMap.
import java.util.*;
class Element {
private String ident;
public Element(String id) { ident = id; }
public String toString() { return ident; }
public int hashCode() { return ident.hashCode(); >
public boolean equals(Object r) {
return r instanceof Element &&
ident.equals(((Element)r).ident);
}
protected void finalize() {
System.out.println("Finalizing " +
getClass().getSimpleName() + " " + ident);
>
>
class Key extends Element {
public Key(String id) { super(id); }
>
class Value extends Element {
public Value(String id) { super(id); }
>
public class CanonicalMapping {
public static void main(String[] args) {
int size = 1000;
продолжение &
716 Глава 17 • Подробнее о контейнерах
Vector и Enumeration
Единственной последовательностью, обладающей способностью саморасширения,
eJava версий 1.0/1.1 был контейнер Vector, поэтому он использовался весьма интен
сивно. У него так много пороков, что я не стану даже упоминать о них (любопытным
рекомендую посмотреть первое издание книги, которое можно загрузить с сайта www.
MindView.net). В общих чертах это аналог списка ArrayList, но только с длинными
и неуклюжими именами методов. В библиотеке KOHTeftHepoBjava 2 класс Vector был
переработан, теперь с ним можно обращаться как с коллекцией (Collection) или как
со списком (List). Но не стоит воспринимать эти изменения чересчур оптимистично
и думать, что Vector как контейнер стал лучше —он включен в библиотеку исключи
тельно для поддержки старого кода.
Итератор, вошедший B j a v a версий 1.0/1.1, был назван новым именем «перечисле
ние» (enumeration), вместо устоявшегося термина «итератор», к которому привыкли
все разработчики. Интерфейс Enumeration компактнее, чем интерфейс Iterator, он
содержит только два метода, которые имеют более длинные имена: метод boolean
Hashtable 717
Для получения Enumeration следует вызвать метод elements(), после чего можно про
водить перебор элементов (только в прямом порядке).
Последняя строка программы создает список ArrayList и применяет к нему метод
enumeration(), чтобы адаптировать итератор Iterator к старому Enumeration. Таким
образом, даже если вы имеете старый код, которому необходим Enumeration, он может
использоваться с новыми контейнерами.
Hashtable
Как вы могли видеть из сравнительного анализа быстродействия контейнеров, в ос
новном контейнер hashtable весьма схож с картой HashMap, даже имена методов у них
похожи. А это означает, что в новом коде нет никаких причин использовать класс
Hashtable вместо HashMap.
Stack
Понятие стека мы рассмотрели чуть раньше, когда изучали список LinkedList. Что
действительно странно в реализации стека, имеющегося B j a v a 1.0/1.1, — вместо ис
пользования класса Vector посредством композиции класс Stack был унаследован от
718 Глава 17 • Подробнее о контейнерах
// И с п о л ь з о в а н и е L in k e d L is t как с т е к а :
L in k e d L is t< S trin g > ls ta c k = new L i n k e d L i s t < S t r i n g > ( ) ;
fo r(M o nth m : M o n th .v a lu e s ())
ls ta c k .a d d F irs t( m .to S trin g ( ) );
p rin t( " ls ta c k = ” + ls ta c k );
w h ile ( !ls ta c k .is E m p ty O )
p rin tn b (ls ta c k .re m o v e F irs t() + '' ");
>
) /* O u t p u t :
Stack = [DANUARY, FEBRUARY, MARCH, APRIL, MAY, DUNE, DULY,
AUGUST, SEPTEMBER, OCTOBER, NOVEMBER]
e l e m e n t 5 = DUNE
p o p p in g e le m e n ts :
The la s t lin e NOVEMBER OCTOBER SEPTEMBER AUGUST DULY DUNE
MAY A P R I L MARCH FEBRUARY DANUARY l s t a c k = [NOVEMBER,
OCTOBER, SEPTEM BER, AUGUST, DULY, DUNE, MAY, APRIL, MARCH,
Hashtable 719
FEBRUARY, JANUARY]
NOVEMBER OCTOBER SEPTEMBER AUGUST JULY JUNE MAY APRIL MARCH
FEBRUARY JANUARY stack2 = [NOVEMBER, OCTOBER, SEPTEMBER,
AUGUST, JULY, JUNE, MAY, APRIL, MARCH, FEBRUARY, JANUARY]
NOVEMBER OCTOBER SEPTEMBER AUGUST JULY JUNE MAY APRIL MARCH
FEBRUARY JANUARY
*///:~
BitSet
Класс BitSet служит для эффективного хранения большого количества «переключа
телей» с двумя состояниями (включено-выключено). Он эффективен только с точки
зрения размеров; по эффективности доступа он обычно уступает низкоуровневым
массивам.
К тому же минимальный размер данных, хранимых классом BitSet, эквивалентен типу
длинного целого (long): 64 бита. Поэтому при хранении данных меньшего размера —
например, 8-битовыхзначений —BitSet станет крайне неэффективным; в таком случае
лучше создать свой класс, или просто массив, чтобы хранить в нем флаги. (Впрочем,
это утверждение истинно только при создании большого количества объектов, со
держащих списки данных с двумя состояниями, и принимать решение следует только
на основании профилирования и других метрик. Если вы будете руководствоваться
только тем, что данные, по вашему мнению, занимают слишком много памяти, это
приведет лишь к усложнению и напрасным тратам времени.)
Стандартный контейнер автоматически меняет свой размер по мере добавления в него
новых элементов, и контейнер BitSet не является исключением. Следующий пример
показывает, как работает этот класс:
//: containers/Bits.java
// Демонстрация BitSet.
import java.util.*;
import static net.mindview.util.Print.*;
short st = (short)rand.nextInt();
BitSet bs = new BitSet();
for(int i = 15; i >= 0; i--)
if(((l « i) & st) != 0)
bs.set(i);
else
bs.clear(i);
print(''short value: " + st);
printBitSet(bs);
int it = rand.nextInt();
BitSet bi = new BitSet();
for(int i = 31; i >= 0; i--)
if(((l « i) & it) != 0)
bi.set(i);
else
bi.clear(i);
print(''int value: " + it);
printBitSet(bi);
Для создания случайных чисел byte, short и int используется генератор случайных
чисел, после чего полученные числа превращаются в соответствующую маску битов
(с помощью класса BitSet). Это прекрасно работает, поскольку изначальный размер
данных контейнера BitSet —64 бита и расширять его нет необходимости. Затем соз
даются контейнеры BitSet большего размера.
EnumSet (см. главу 19) обычно предпочтительнее BitSet при работе с фиксирован
ным набором флагов, которому можно присвоить имя (вместо числовых позиций
битов), — это снижает вероятность ошибок, потому что EnumSet позволяет работать с
именами вместо позиций конкретных битов. EnumSet также предотвращает случайное
добавление новых флагов, что приводит к появлению серьезных, трудно обнаружимых
ошибок. Существует совсем немного ситуаций, в которых есть смысл использовать
BitSet вместо EnumSet, —если необходимое количество флагов неизвестно заранее, или
если присваивание имен флагам не имеет смысла, или если вам нужны специальные
операции BitSet (см. документацию BitSet и EnumSet B j D K ) .
Резюме
Пожалуй, библиотека контейнеров является самой важной библиотекой в объектно-
ориентированном языке. В большинстве программ контейнеры используются чаще
любыхдругих библиотечных компонентов. Некоторые языки (например, Python) даже
содержат встроенные компоненты основных контейнеров (списки, карты и множества).
Как было показано в главе 11, контейнеры позволяют выполнить без особых усилий
много интересных операций. Однако на какой-то стадии для правильного исполь
зования контейнеров вам потребуется больше знать о них — в частности, нужно
разбираться в хешировании для написания собственного метода h a s h C o d e ( ) (и знать,
когда это необходимо), а также уметь выбрать из нескольких реализаций ту, которая
лучше всего подходит для ваших целей. В этой главе рассматриваются эти концепции,
а также приводится дополнительная полезная информация о библиотеке контейнеров.
К завершению этой главы вы должны быть хорошо подготовлены к использованию
KOHTeftHepoeJava в повседневной работе.
Класс File
Перед тем как перейти к классам, которые осуществляют реальные запись и чтение
данных, мы рассмотрим вспомогательные инструменты библиотеки, чтобы облегчить
обращение с файлами и каталогами.
Имя класса File весьма обманчиво: легко подумать, что оно всегда относится к файлу,
но это не так. Класс File может представлять имя определенного файла или набор
файлов, находящихся в каталоге. Если класс представляет каталог, можно вызвать
его метод list(), чтобы получить массив строк, содержащих имена всех файлов. Ис
пользовать в данной ситуации массив (а не более гибкий контейнер) очень удобно:
количество файлов в каталоге выражается конечным числом, как и размер массива,
а если понадобится узнать имена файлов в другом каталоге, надо просто создать еще
один объект File. Вообще говоря, для этого класса куда лучше подошло бы имя FilePath
(путь к файлу). Следующий раздел покажет, как использовать этот класс в совокуп
ности с тесно связанным с ним интерфейсом FilenameFilter.
//: io/DirList.java
// Вывод содержимого каталога с использованием регулярных выражений.
// {Args: "D.*\.java"}
import java.util.regex.*;
import java.io.*;
import java.util.*;
import java.util.regex.*;
import java.io.*;
import java.util.*;
Заметьте, что аргумент метода filter() должен быть неизменным (final). Это необ
ходимо для того, чтобы анонимный внутренний класс смог получить к нему доступ
даже за пределами своей области действия.
Несомненно, структура программы улучшилась — хотя бы потому, что объект
FilenameFilter теперь неразрывно связан с внешним классом DirList2. Впрочем,
можно продолжить совершенствование и определить безымянный внутренний класс
как аргумент метода list(); в этом случае код станет еще компактнее;
//: io/DirList3.java
// Построение анонимного внутреннего класса "на месте".
// {Args: "D.*\.java")
import java.util.regex.*;
import java.io.*;
import java.util.*;
На этот раз неизменным (final) оказывается аргумент метода main(), так как безымян
ный внутренний класс использует параметр командной строки (args[0]) напрямую.
Именно так безымянные внутренние классы позволяют быстро создать специализиро
ванный «одноразовый» класс для решения конкретной задачи. Одно из преимуществ
такого решения заключается в том, что составные части кода, решающего определенную
задачу, находятся в одном месте. С другой стороны, полученный код читается сложнее,
поэтому применять его следует осмотрительно.
1. (3) Измените программу DirList.java (или одну из ее разновидностей) так, чтобы
объект FilenameFilter открывал и читал каждый файл (с использованием класса
net.mindview.util.TextFile) и принимал файл на основании того, присутствуют ли
в этом файле аргументы командной строки.
2. (2) Создайте класс с именем SortedDirList, конструктор которого принимает инфор
мацию о пути к файлу и на основе этой информации составляет отсортированный
список файлов в указанном каталоге. Создайте два перегруженных метода list () ,
один должен возвращать весь список файлов, другой —подмножество списка, со
ответствующее аргументу (регулярное выражение).
3. (3) Измените программу DirList.java (или одну из ее разновидностей) так, чтобы она
суммировала размеры выбранных файлов.
result.append("]");
return result.toString();
}
public static void pprint(Collection<?> с) {
System.out.println(pformat(c));
>
public static void pprint(Object[] с) {
System.out.println(pformat(Arrays.asList(c)))j
>
} ///:~
Метод создает отформатированную строку на базе Collection, а метод
pformat()
pprint() использует pformat() для выполнения своей работы. Обратите внимание на
отдельную обработку особых случаев: отсутствие элементов и один элемент. Также
имеется версия pprint() для массивов.
Вспомогательный класс Directory включен в пакет netmlndview.util, чтобы с ним было
удобно работать. Пример использования:
//: io/DirectoryDemo.java
// Пример использования вспомогательного класса Directory.
import java.io.*;
import net.mindview.util.*;
import static net.mindview.util.Print.*;
.\TestEOF.java
.\TransferTo.java
.\xfiles\ThawAlien.java
.\FreezeAlien.class
.\GZIPcompress.class
.\ZipCompress.class
*///:~
Если вам непонятен второй аргумент в вызовах local() и walk(), вернитесь к главе 13
и освежите в памяти синтаксис регулярных выражений.
Класс File 731
Можно пойти еще дальше и создать класс, который будет обходить каталоги и обра
батывать содержащиеся в них файлы в соответствии с объектом Strategy (еще один
пример паттерна проектирования «Стратегия»):
//: net/mindview/util/ProcessFiles.java
package net.mindview.util;
import java.io.*;
Первым делом в методе main() вызывается метод renameTo(), который позволяет пере
именовывать (или перемещать) файлы, используя для этого второй аргумент — еще
один объект File, который указывает на новое местоположение или имя.
Немного поэкспериментировав с этой программой, вы увидите, что создать пути про
извольной сложности очень просто, поскольку всю работу за вас фактически делает
метод mkdirs().
6 . (5) Используйте ProcessFiles для поиска в заданном поддереве каталогов всех
файлов с исходным кодом Java, измененных после конкретной даты.
Ввод и вывод
В библиотеках ввода-вывода часто используется абстракция потока (stream) —про
извольного источника или приемника данных, который способен производить или
получать некоторую информацию. Поток скрывает подробности низкоуровневых
операций, выполняемых с данными непосредственно в устройствах ввода-вывода.
Классы библиотеки ввода-выводаДауа разделены надве части —одни осуществляют
ввод, другие вывод. В этом можно убедиться, просмотрев НТМЬ-документациюДОК
в вашем браузере. Вследствие применения наследования все классы, производные от
базовых классов InputStream или Reader, имеют методы с именами read() для чтения
одиночных байтов или массива байтов. Аналогично, все классы, унаследованные от
базовых классов OutputStream или Writer, имеют методы с именами write( ) для записи
одиночных байтов или массива байтов. Впрочем, вы вряд ли станете использовать эти
методы напрямую —они в основном предназначены для других классов, а другие классы
на их основе предоставляют более удобный интерфейс. Объекты потоков редко созда
ются наоснове одного класса; чаще несколько объектов «наслаиваются» друг на друга
для получения необходимой функциональности (пример паттерна проектирования
«Декоратор», о котором будет рассказано в этом разделе). Необходимость создания
потока на основе нескольких объектов —главная причина затруднений с библиотекой
ввoдa-вывoдaJava у новичков.
Для изучения полезно разделить классы по категориям в зависимости от их функций.
В первой BepcHnJava (1.0) разработчики решили, что все, связанное с вводом данных,
должно быть унаследовано от базового класса InputStream, а все, имеющее отношение
к выводу данных, —от класса OutputStream.
В этой главе используется тот же подход, что и в предыдущих главах: я приведу обзор
классов, но за полной информацией (например, полным списком методов конкретного
класса) следует обращаться к дoкyмeнтaцииJDK.
Типы InputStream
Назначение базового класса InputStream — представлять классы, которые получают
данные из различных источников. Такими источниками могут быть:
□ массивбайтов;
□ строка (объект String);
Ввод и вывод 735
□ файл;
□ «канал» (pipe), который работает как настоящий физический трубопровод: вы по
мещаете данные в один его «конец», а извлекаете их из другого конца;
□ последовательность различных потоков, которые можно объединить в одном потоке;
□ другие источники, подобные соединениям с сетью Интернет (такие возможности
описаны в книге Thinking In EnterpriseJava, доступной на сайте www.MindView.net).
Каждый из указанных источников имеет ассоциированный с ним подкласс базового
класса InputStream (табл. 18.1). Дополнительно, существуетеще класс FilterInputStream,
который также является производным классом inputstream и представляет собой ос
нову для классов-«декораторов», добавляющих к входным потокам полезные свойства
и интерфейсы. Его мы обсудим чуть позже.
Типы OutputStream
В данную категорию (табл. 18.2) попадают классы, определяющие, куда направляются
ваши данные: в массив байтов (здесь нельзя использовать строки (S trin g ) напрямую;
предполагается, что вы сможете воссоздать их из массива байтов), в файл или канал.
Вдобавок, класс F ilterO utputstream предоставляет базовы й интерф ейс для классов-
«декораторов», которы е способны добавлять к сущ ествую щ им потокам полезны е
свойства и интерфейсы. Подробности мы отложим на потом.
1Трудно сказать, насколько удачным было такое решение, особенно если сравнить с простотой
библиотек ввода-вывода в других языках. Тем не менее причина была именно такой.
738 Глава 18 • Система ввода-вывода Java
Следует особо отметить, что без изменений остался класс DataOutputStream, так что
для передачи данных независимым от платформы и машины способом по-прежнему
используются иерархии InputStream и OutputStream.
new FileReader(filename));
String s;
StringBuilder sb = new StringBuilder()j
while((s = in.readLine())!= null)
sb.append(s + "\n")j
in.close()j
return sb.toString();
>
public static void main(String[] args)
throws IOException {
System.out.print(read("BufferedInputFile.java"));
}
} /* (Execute to see output) *///:~
Объект StringBuilder sb служит для того, чтобы собрать воедино весь прочитанный
текст (вклю чая переводы строк, поскольку метод readLine() их отбрасывает). Эта
строка затем еще не раз встретится в программе. В завершение файл закрывается вы
зовом close()1.
Ф орм ально метод c lo s e () должен вызы ваться в завершающем методе f i n a l i z e ( ) ,
а последний должен вызываться при выходе из программы (вне зависимости от того,
сработал ли уборщик мусора). Однако это было реализовано недостаточно согласован
но, и потому единственный способ завершить ввод-вывод безопасно — явно вызвать
метод close().
7. (2) Откройте текстовый файл для построчного чтения. Считывайте из него строки
в объект String, который затем помещается в LinkedList. Выведите все строки в об
ратном порядке средствами класса LinkedList.
8. (1) Измените упражнение 7 так, чтобы имя файла с обрабатываемым текстом пере
давалось в командной строке.
9. (1) Измените упражнение 8 так, чтобы буквы во всех прочитанных строках преоб
разовывались к верхнему регистру Направьте результат в «устройство» стандарт
ного вывода System.out.
10. (2) Измените упражнение 8, чтобы программа получала в дополнительных аргумен
тах командной строки слова, которые нужно найти в файле. Выведите все строки,
где были найдены указанные слова.
11. (2) В примере innerdasses/GreenhouseControlter.java класс GreenhouseController содержит
жестко запрограммированный набор событий. Измените программу так, чтобы она
читала события и их относительное время из текстового файла. (Уровень сложности
8: используйте для построения событий паттерн «Фабричный метод» —см. Thinking
in Pattems (with Java) на сайте remmMindView.net).
1 В исходном варианте архитектуры предполагалось, что метод close() будет вызываться при
выполнении finalize( ), и подобное определение finalize() встречается для классов ввода-
вывода. Однако, как было указано ранее, функциональность finalize() работает не так, как
ее изначально планировали проектировщикиДауа (а если говорить точнее - она была безвоз
вратно «запорота»), так что единственным надежным решением можно считать явный вызов
close() для файлов.
Типичное использование потоков ввода-вывода 745
Чтение из памяти
В этом примере результат String из BufferedInputFile.read() используется для соз
дания класса StringReader. После этого символы читаются методом read(), и каждый
следующий символ посылается на консоль.
//: io/MemoryInput.java
import java.io.*;
Заметьте, что метод read () возвращ ает следующ ий символ в формате in t, поэтому
для правильного вывода на консоль приходится приводить его результат к типу char.
Вывод в файл
Объект FileWriter записывает данные в файл. Буф еризация применяется при вводе/
выводе практически всегда (попробуйте прочитать файл без нее, и вы увидите, насколь
ко ее отсутствие влияет на производительность — скорость чтения уменьшится в не
сколько раз), поэтому мы декорируем его в BufferedWriter. После этого подключается
PrintWriter, чтобы выполнять форматированный вывод. Ф ай лсдан н ы м и , созданный
такой конфигурацией ввода-вывода, можно прочитать как обычный текстовый файл.
//: io/BasicFileOutput.java
import java.io.*j
>
> /* (Execute to see output) *///:~
При записи строк в ф айл к ним добавляю тся их номера. Заметьте, что LineNumber-
lnputstream для этого не применяется, поскольку этот класс никому не нужен. Как и по
казано в рассматриваемом примере, своя собственная нумерация реализуется тривиально.
Когдаданные входного потока исчерпываются, метод readLine() возвращает null. Д ля
потока out явно вы зы вается метод close(); если не вызвать его для всех выходных
файловых потоков, в буферах могут остаться данные, и ф айл получится неполным.
12. (3 ) И змените упражнение 8, чтобы оно также открывало текстовый ф айл для за
писи. Запиш ите в файл строки из Linkedlist вместе с номерами строк (не пытайтесь
использовать классы LineNumber).
13. (3 ) И зм ен и те п ри м ер BasicFileOutput.java, чтобы в нем и с п о л ь зо в а л ся объект
LineNumberReader для подсчета строк. Обратите внимание, насколько проще под
считывать строки самостоятельно.
748 Глава 18 • Система ввода-вывода Java
14. ( 2 ) Н а базе кода BasicFileOutput.java напиш ите программу для сравнения п р ои зводи
тельности записи в файл при использовании буферизованного и небуферизованного
ввода-вывода.
1 Язык разметки XML —еще один способ переноса данных между различными платформами,
к тому же он не зависит от того, есть ли среда выполнения^уа на машине или нет. О работе
с данными XML будет рассказано позднее в этой главе.
Типичное использование потоков ввода-вывода 749
1 В стандарте Unicode 3.0 (раздел 3.8, Transformations) говорится, что UTF-8 сериализует значе
ния в последовательность от одного до четырех байтов. Описание UTF-8 во второй редакции
ISO/IEC 10646 допускает также пятый и шестой байты, но это не является корректным для
стандарта Unicode. Сказанное в двух предыдущих предложениях является особенностью
HMeHHoJava-реализации UTF-8. — Примеч.ред.
750 Глава 18 • Система ввода-вывода Java
М етод display() откры вает ф ай л и вы водит семь элем ентов как зн ачен ия double.
В методе main() ф ай л сначала создается, затем откры вается и и зм еняется. Так как
значение double всегда состоит из 8 байтов, при вы зове seek() д л я п ози ц и он и ро
в ан и я к зн ачен и ю d o u b l e с ном ером 5 нуж ное см ещ ение о п р ед ел яется просты м
ум нож ением 5*8.
Средства чтения и записи файлов 751
Как уже было замечено, класс RandomAccessFile практически ничем не связан с ие
рархией библиотеки ввода-вы вода Java, если не считать реализации интерф ейсов
Datalnput и DataOutput. Таким образом, комбинировать его с подклассами inputStream
и Outputstream не получится. Можно предположить, что класс доступа к буферу памя
ти ByteArrayInputStream стал бы хорошим потоком произвольного доступа, но класс
RandomAccessFile работает только с файлами. Буф еризация уже встроена в него и при
соединить ее, как это делается с потоками, нельзя.
Все возможности настройки сводятся ко второму аргументу конструктора: можно от
крыть RandomAccessFile только для чтения (указав строку «г») или для чтения/записи
(строка «rw»).
Также стоит рассмотреть возможность употребления вместо класса RandomAccessFile
механизма отображаемых в память файлов.
Каналы
В этой главе бы ли коротко упом януты классы каналов Pi pe dI np ut St rea m, Piped-
OutputStream, PipedReader и PipedWriter. Это не значит, что они редко используются
или не слишком полезны, просто их смысл и действие нельзя донести до понимания
до тех пор, пока не объяснена многозадачность: каналы предназначены для связи
между отдельными программными потоками. Поэтому они будут подробно описаны
в главе 21, там же для них имеется несколько примеров.
класса T extF ile, который будет хранить содерж им ое файла в списке ArrayList (чтобы
при работе с содержимы м файла были доступны все возм ож ности ArrayList):
//: net/mindview/util/TextFile.java
// Статические функции для чтения и записи
// текстовых файлов, а также для работы с файлом
// как со списком ArrayList.
package net.mindview.util;
import java.io.*;
import java.util.*;
Н есмотря на то что кода в этом классе немного, его применение позволит сэкономить
вам уйму времени и сделать вашу ж изнь проще, в чем вы еще будете иметь возмож
ность убедиться чуть позже.
Также для чтения текстовых файлов можно воспользоваться классом java.uti!.Scanner,
появивш имся Bjava SE5. О днако этот класс предназначен только для чтения файлов,
но не для записи, и этот инструмент (не включенный в java.to) предназначен в основном
для создания обработчиков программного кода или «мини-языков».
17. (4) И спользуя TextFile и Map<Character, Integer>, создайте программу для подсчета
вхождений символов в файл. (Например, если в файле буква «а» встречается 12 раз,
с этим символом связы вается значение типа Integer, равное 12.)
18. (1) И змените пример TextFile.java так, чтобы он передавал исклю чения lOException
вызывающей стороне.
О дин перегруженный метод получает аргумент File; во втором передается имя файла
(аргумент String). О ба метода возвращают полученный массив байтов.
М етод available() возвращ ает размер массива, а эта перегруженная версия метода
read() заполняет массив.
19. (2) И спользуя классы и Map<Byte, Integer>, создайте программу для под
BinaryFile
счета вхождений разных байтов в файле.
20. (4 ) И спользуя метод Directory.walk() и проверьте, что все файлы .class
BinaryFile,
в дереве каталогов начинаются с шестнадцатеричной последовательности «CAFE-
BABE».
Стандартный ввод-вывод 755
Стандартный ввод-вывод
Термином стандартный ввод-вывод обозначается принятая в UNIX концепция единого
потока информации, используемого программой (в некоторой форме эта концепция
присутствует и в W indows, и во многих других операционны х системах). Вся ин
формация программы приходит из стандартного ввода (standard input), все данные
записываются в стандартный вывод (standard o u tp u t), а все ошибки программы пере
сылаются в стандартный поток для ошибок (standard error). Значение стандартного
ввода-вывода состоит в том, что программы легко соединять в цепочку, где стандартный
вывод одной программы становится стандартным вводом другой программы. Этот
инструмент открывает очень много полезных возможностей.
Спецификация исключения присутствует здесь из-за того, что метод readLine( ) может
возбуждать исключение IOException. Стоит напомнить, что поток System.in обычно
буферизуется (впрочем, как и больш инство потоков).
21. (1) Напишите программу, которая получает данные из стандартного ввода и пре
образует все символы к верхнему регистру, после чего направляет результаты
756 Глава 18 • Система ввода-вывода Java
□ setOut(PrintStream)
□ setErr(PrintStream)
1 В главе 22 показано еще более удобное решение — это программа с графическим интерфейсом,
которая перенаправляет стандартный вывод в текстовое поле с автоматической прокруткой
текста.
Управление процессами 757
Управление процессами
Часто в nporpaMMaxJava возникает необходимость выполнения других программ опе
рационной системы, с управлением вводом и выводом таких программ. Библиотека
Java предоставляет классы для выполнения таких операций.
Одна из стандартных задач — запуск программы и направление полученных резуль
татов на консоль. В этом разделе представлен вспомогательный класс, упрощ ающ ий
выполнение этой задачи.
При его использовании могут возникать два вида ошибок: обычные ош ибки, п ри
водящие к выдаче исклю чений (дл я которых мы просто перезапускаем исключение
RuntimeException), и ошибки, обусловленные выполнением самого процесса. Д ля опо
вещения о таких ош ибках будет использоваться отдельное исключение:
//: net/mindview/util/OSExecuteException.java
package net.mindview.util;
//: net/mindview/util/OSExecute.java
// Выполнение команды операционной системы
// и передача вывода на консоль,
package net.mindview.util;
import java.io.*;
*///:~
В этом прим ере деком пилятор javap (вклю ченны й в nocTaBK yJDK ) используется для
деком пиляции программы.
22. (5) И змените пример OSExecute.java так, чтобы вместо вывода стандартного потока
ошибок результат выполнения возвращался в виде списка List с элементами String.
П родемонстрируйте использование новой версии класса.
1 Описываемые здесь каналы (channels) следует отличать от каналов (pipe), создаваемых клас
сами PipedInputStream и PipedOutputStream. Первые представляют собой еще один источник
данных, а вторые налаживают обмен данными между различными процессами. —Примеч. перев.
760 Глава 18 • Система ввода-вывода Java
сущ ествующий байтовый массив в буфер ByteBuffer методом wrap(). Когда вы так
делаете, байтовый массив не копируется, а используется как хранилищ е для полу
ченного буфера ByteBuffer.
Ф айл data.txt заново открывается с помощью класса RandomAccessFile. Заметьте, что
вы вправе перемещать канал FileChannel по записям, здесь он был передвинут в конец
файла так, чтобы дополнительные записи присоединялись к концу файла.
Доступ «только для чтения» можно осуществить, явно выделяя память для байтового
буфера ByteBuffer статическим методом allocate(). П редназначение nio — быстрое
перемещение большого количества данных, поэтому размер буфера имеет значение:
на самом деле установленный в примере размер в один килобайт меньше, чем обычно
требуется (вам надо будет поэкспериментировать с вашим работающим приложением,
чтобы найти оптимальное решение).
Возможно получить еще большее быстродействие, используя вместо метода allocate()
метод allocateDirect(). Он производит буфер «прямого доступа», который может быть
еще теснее связан с операционной системой. О днако такой буфер требует больше
ресурсов, а реализация его отличается в различных операционных системах. Таким
образом, опять имеет смысл поэкспериментировать с вашим работающим приложени
ем, чтобы выяснить, дадут ли буферы прямого доступа лучшую производительность.
После того как вы вызовете метод read() буфера FileChannel, чтобы сохранить байты
в буфере ByteBuffer, вы также должны вызвать для буфера метод flip() , позволяющий
впоследствии извлечь из буфера его данные (да, все это выглядит немного непродуман
но, но помните, что расчет делался на высокое быстродействие и поэтому все сделано
на низком уровне). И если затем нам снова понадобится буфер для чтения, придется
вызывать перед каждым методом read() метод clear(). Увидеть это можно в простой
программе для копирования файлов:
//: io/ChannelCopy.java
// Копирование файлов с использованием каналов и буферов.
// {Args: ChannelCopy.java test.txt}
import java.nio.*;
import java.nio.channels.*;
import java.io.*j
Как видно из листинга, создаются два канала FileChannel: для чтения и для записи.
Выделяется буфер ByteBuffer, а когда метод FileChannel.read() возвращ ает - 1 , это
значит, что мы достигли конца входных данных (без сомнения, наследие U N IX и С).
П осле каждого вы зова метода read(), помещающего данные в буфер, метод flip()
подготавливает буфер так, чтобы информация из него могла быть извлечена методом
write( ). После Bbi30Bawrite() инф ормация все еще хранится в буфере, поэтому метод
clear() сбрасывает все внутренние указатели, чтобы буфер снова был способен при
нимать данные в методе read().
Впрочем, рассм отренная програм м а не лучш им образом вы п олняет копирование
файлов. Специальны е методы transferTo() и transferFrom() позволяю т напрямую
присоединить один канал к другому:
//: io/TransferTo.java
// Using transferTo() between channels
// {Args: TransferTo.java TransferTo.txt}
import java.nio.channels.*;
import java.io.*;
Преобразование данных
Если вы вспомните программу GetChannel.java, то увидите, что для вывода информации
из ф айла нам приходилось считывать из буфера по одному байту и преобразовывать
его из типа byte к типу char. Такой подход выглядит весьма примитивно — если вы
посмотрите на класс java.nio.CharBuffer, то увидите, что в нем есть метод toString(),
который, как сказано в описании, «возвращ ает строку из символов, находящ ихся
в данном буфере». Байтовый буфер ByteBuffer можно представить в виде символьного
буфера CharBuffer, это делает метод asCharBuffer(), почему бы так и не поступить? Как
вы увидите уже из первой команды вывода, такое решение не сработает:
//: io/BufferToText.java
// Преобразование текста в ByteBuffer и обратно
import java.nio.*;
import java.nio.channels.*;
import java.nio.charset.*;
import java.io.*;
Новый ввод-вывод (nio) 763
import java.util.*;
import static net.mindview.util.Print.*;
Таким образом, возвращ аясь к программе BufferToText.java, если вы вызовете для бу
ф ера метод rewind() (чтобы вернуться к его началу), а затем используете кодировку
по умолчанию в методе decode( ), данные буфера CharBuffer будут правильно выведе
ны на консоль. Д ля определения кодировки по умолчанию вызовите метод System.
getProperty("file.encoding"), которы й возвращ ает строку с названием кодировки.
П ередавая эту строку в метод Charset.forName(), вы получите объект Charset, с помо
щью которого и декодируете строку.
Другой подход — кодировать данные методом encode() так, чтобы при чтении файла
выводилось что-нибудь понятное (см. третью часть BufFerToText.java). Здесь для записи
текста в файл используется кодировка UTF-16BE, и при последующем чтении остается
только преобразовать данные в буфер CharBuffer, и вы получите ожидаемый текст.
Наконец, вы видите, что происходит при записи в буфер ByteBuffer через CharBuffer (мы
узнаем об этом чуть позже). Заметьте, что для байтового буфера выделяется 24 байта.
Н а каждый символ (char) отводится два байта, соответственно, буфер вместит 12 сим
волов, а у нас в строке «Some text» их только девять. Оставш иеся нулевые байты все
Новый ввод-вывод (nio) 765
Извлечение примитивов
Несмотря на то что в буфере ByteBuffer хранятся только байты, он поддерживает м е
тоды, образующие любые значения примитивов из этих байтов. Следующий пример
демонстрирует вставку и чтение из буф ера разнообразных значений примитивных
типов с использованием этих методов:
//: io/GetData.java
// Получение различных представлений данных из ByteBuffer
import java.nio.*;
import static net.mindview.util.Print.*;
>
} /* Output:
i = 1025
H о w d у i
l2390
99471142
99471142
9.9471144E7
9.9471142E7
*///:~
После выделения байтового буфера мы проверяем все его значения, чтобы убедиться
в том, что при выделении содержимое буфера автоматически обнуляется. Сравниваются
все 1024 значения, хранимые в буфере (вплоть до последнего, индекс которого —раз
мер буфера — возвращается методом lim it()), и все они оказываются нулями.
Простейший способ вставить примитив в ByteBuffer — получить подходящее «пред
ставление» этого буф ера методами asCharBuffer(), asShortBuffer() и т. n., а затем
поместить в это представление значение методом put(). В примере мы так поступаем
для каждого из примитивных типов. Единственное отклонение из этого ряда — ис
пользование буф ера ShortBuffer, требующего приведения типов (которое усекает
и изменяет результирующее значение). Все остальные представления не нуждаются
в преобразовании типов.
Представления буферов
«Представления буферов» дают вам возможность интерпретировать байтовый буфер
через «окно» конкретного примитивного типа. Байтовы й буфер все так же хранит
действительные данные и одновременно поддерживает представление, поэтому все
изменения, которые вы сделаете в представлении, отразятся на содержимом байтового
буфера. Как мы увидели из предыдущего примера, это удобно для вставки значений
примитивов в байтовый буфер. П редставления такж е позволяю т читать значения
примитивов из буфера, по одному (раз он «байтовый» буфер) или пакетами (в мас
сивы). Следующий пример манипулирует целыми числами ( in t) в буфере ByteBuffer
с помощью класса lntBuffer:
//: io/IntBufferDemo.java
// Работа с целыми числами в буфере ByteBuffer
// посредством буфера lntBuffer
import java.nio.*;
while(ib.hasRemaining()) {
int i = ib.get()j
System.out.println(i)j
>
}
> /* Output:
99
11
42
47
1811
143
811
1016
*///:~
П ерегруж енны й м етод put () первый раз вызывается для пом ещ ения в буф ер массива
целых чисел int. Последующие вызовы put() и get() обращаются к конкретному числу
int из байтового буф ера ByteBuffer. Заметьте, что такие обращ ения к примитивным
типам по абсолю тной пози ц и и такж е мож но осущ ествить напрям ую через буф ер
ByteBuffer.
Как только байтовый буфер ByteBuffer будет заполнен целыми числами или другими
примитивами через представление, его можно передать для непосредственной записи
в канал. Так же просто можно прочитать данные из канала и использовать представ
ление для преобразования данных к конкретному примитивному типу. Следующий
пример интерпретирует одну и ту же последовательность байтов как числа short,
int, float, long и double, создавая для одного байтового буфера ByteBuffer различны е
представления:
//: io/ViewBuffers.java
import java.nio.*;
import static net.mindview.util.Print.*;
((ByteBuffer)bb.rewind()).asIntBuffer();
printnb("Int Buffer ");
w h ile(ib . hasRemaining())
printnb(ib.position()+ " -> " + ib .g e t() + ", ");
p rin t();
LongBuffer lb =
((ByteBuffer)bb. rewind( ) ) . asLongBuffer();
printnb("Long Buffer ");
while(lb.hasRemaining())
printnb(lb.positionQ + " -> " + lb.get() + ", ");
p rin t();
ShortBuffer sb =
( (ByteBuffer)bb. rewind()) . asShortBuffer();
printnb("Short Buffer ")j
while(sb.hasRemaining())
printnb(sb.position()+ " -> " + sb.get() + ", ");
p rin t();
DoubleBuffer db =
( (ByteBuffer)bb. rewind()) . asDoubleBuffer()j
printnb("Double Buffer ");
while(db.hasRemaining())
printnb(db.position()+ " -> " + db.get() + ", ")j
}
} /* Output:
Byte Buffer 0 -> 0, 1 -> 0, 2 -> 0, 3 -> 0, 4 -> 0, 5 -> 0,
б -> 0, 7 -> 97,
Char Buffer 0 -> , 1 -> , 2 -> , 3 -> а,
Float Buffer 0 -> 0.0, 1 -> 1.36E-43,
Int Buffer 0 -> 0, 1 -> 97,
Long Buffer 0 -> 97,
Short Buffer 0 -> 0 , 1 -> 0, 2 -> 0, 3 -> 97,
Double Buffer 0 -> 4.8E-322,
*///:~
0 0 0 0 0 0 0 97 bytes
а chars
0 0 0 97 shorts
0 97 ints
97 longs
4.8E-322 doubles
24. (1) И змените код IntBufferDemo.java, чтобы в нем использовался тип double.
Новый ввод-вывод (nio) 769
0 0 0 0 0 0 0 0 0 1 1 0 0 0 0 1
Ы b2
Если вы прочитаете эти данные как тип short (B y te B u ffe r.a sS h o rtB u ffe r()), то полу
чите число 97 (00000000 01100001), однако при изменении порядка байтов вы увидите
число 24 832 (01100001 00000000).
Следующий пример показывает, как порядок следования байтов отражается на сим
волах в зависимости от настроек буфера:
//: io/Endians.java
// Порядок следования байтов и хранение данных,
import java.nio.*;
import ja v a .u til.* ;
import s ta tic net.m ind view .u til.P rint.* ;
bb. order(ByteOrder.BIG_ENDIAN);
bb. asCharBuffer(). put("abcdef');
print(A rrays.toStrin g(bb.array{) ))j
bb.rewind();
bb. order(ByteOrder. LITTLE_ENDIAN);
bb. asCharBuffer().put("abcdef");
print(A rrays.toStrin g(bb. a rra y()));
>
> /* Output:
[0, 97, 0, 98, 0, 99, 0, 100, 0, 101, 0 ,1 0 2 ]
[0, 97, 0, 98, 0, 99, 0, 100, 0, 101, 0, 102]
[97, 0, 98, ё, 99, 0, 100, 0, 101, 0, 102, 0]
*///:~
В буфере ByteBuffer достаточно места для хранения всех байтов символьного массива,
поэтому для вывода байтов годится метод array(). Метод a rra y( ) является необязатель
ным и вызывать его следует только для буфера на базе массива; в противном случае
возбуждается исключение UnsupportedOperationException.
Символьный массив помещается в буфер B y te B u ffe r посредством представления
charBuffer. При выводе содержащихся в буфере байтов вы можете видеть, что настройка
по умолчанию совпадает с режимом big_endian, в то время как атрибут little _ e n d ia n
переставляет байты в обратном порядке.
Подробнее обуферах
Буфер состоит из данных и четырех индексов, используемых для доступа к данным
и для эффективного манипулирования ими. К этим индексам относятся метка (mark),
позиция (position), предел (limit) и вместимость (capacity). Класс B u ffe r содержит
методы, предназначенные для установки и сброса значений этих индексов, а также
чтения их текущих значений (табл. 18.7).
Методы, вставляющие данные в буфер и считывающие их оттуда, обновляют эти
индексы для учета изменений.
Новый ввод-вывод (пю) 771
Вспомогательные
Файловая система или сеть средства
getChannel()
wrIte(ByteBuffer)
F ile C h a n n e l 5| B y te B u ffe r
read (Byte BufFer)
Л
m ap(FileChannel.M apM ode,posltlon,size)
i :
M a p p e d B y te B u ffe r
Т а б л и ц а 1 8 .7 . Методы Buffer
Метод Описание
capacity() Возвращает емкость буфера
clear() Очищает буфер, устанавливает позицию в ноль, а предел делает равным
емкости. Этот метод можно вызывать для перезаписи существующего
буфера
~fiip6 Устанавливает предельное значение равным позиции, а позицию приравни
вает к нулю. Метод используется для подготовки буфера к чтению,
после того как в него были записаны данные
limit() Возвращает предел
limit(int lim) Устанавливает предел
mark() Приравнивает метке значение позиции
position() Возвращает значение позиции
position(int pos) Устанавливает значение позиции
remaining() Возвращает разность между предельным значением и позицией
hasRemaining() Возвращает true, если между позицией и предельным значением
еще остались элементы
Хотя получить буфер CharBuffer можно и напрямую, вызвав для символьного массива
метод wrap(), здесь сначала выделяется служащий основой байтовый буфер ByteBuffer,
а символьный буфер CharBuffer создается как представление байтового. Это подчер
кивает, что в конечном счете все манипуляции производятся с байтовым буфером,
поскольку именно он взаимодействует с каналом.
Новый ввод-вывод (nio) 773
__________________________ J
U s i n 9 В u f f e г s
7^ /Ч
\ 1_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ JL
и s i n 9 В u f f e г S
7 ^ 7 'ч
Два вызова «относительных» методов get() сохраняют значение первых двух символов
в переменных cl и c2. После этих вызовов буфер выглядит так:
i ______________________ J
и s i n 9 В u f f e r S
7 ч У ч
J1_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ JL
и s i п 9 В u f f e г s
У ^ 7 4^
774 Глава 18 • Система ввсща-вывода Java
I
s U i n 9 В u f f e г s
I Pos .l Ш2
__________ \ ________________ J
s U i П 9 В u f f 0 Г s
/Ч 7 4^
Этот процесс продолжается до тех пор, пока не будет просмотрен весь буфер. В конце
цикла while позиция находится в конце буфера. При выводе буфера на печать распе
чатываются только символы, находящиеся между позицией и пределом. Поэтому, если
вы хотите вывести буфер целиком, придется установить позицию на начало буфера,
используя для этого метод rewind(). Вот в каком состоянии находится буфер после
вызова метода rewind() (значение метки стало неопределенным):
LsapJ
1
S U n i В 9 f u e f s г
7 Ч 7 s;
Производительность
Хотя быстродействие «старого» ввода-вывода было улучшено за счет переписывания
его с учетом новых библиотек nio, техника отображения файлов качественно эффек
тивнее. Следующая программа выполняет простое сравнение производительности:
//: io/MappedIO.java
import java.nio.*;
import java.nio.channels.*;
import ja v a .io .* ;
System.out.format("%.2f\n", duration/1.0e9);
} catch(IOException e) {
throw new RuntimeException(e);
}
}
public abstract void test() throws IOException;
>
private sta tic Tester[] tests = {
new Tester("Stream Write") {
public void test() throws IOException {
DataOutputStream dos = new DataOutputStream(
new BufferedOutputStream(
new FileOutputStream(new File("temp.tmp"))))
fo r(in t i = 0; i < numOfIntsj i++)
dos.w riteInt(i);
dos.close();
>
Ь
new Tester("Mapped Write") {
public void te st() throws IOException {
FileChannel fc =
new RandomAccessFile("temp.tmp", "rw")
.getChannel();
IntBuffer ib = fc.map(
FileChannel.MapMode. READ_WRITE, 0, fc .s iz e ( ))
.asIntBuffer();
fo r(in t i = 0; i < numOfInts; i++)
ib .p u t(i);
fc .c lo s e ();
>
b
new Tester("Stream Read") {
public void test() throws IOException {
DataInputStream dis = new DataInputStream(
new BufferedInputStream(
new FileInputStream("temp.tmp")));
fo r(in t i = 0; i < numOfInts; i++)
dis.readInt();
d is.c lo se ();
>
Ь
new Tester("Mapped Read") {
public void test() throws IOException {
FileChannel fc = new FileInputStream(
new File("temp.tmp")).getChannel();
IntBuffer ib = fc.map(
FileChannel.MapMode. READ_ONLY, 0Л fc .s iz e ( ))
.asIntBuffer();
w hile(ib . hasRemaining())
ib .g e t();
fc .c lo s e ();
>
},
new Tester("Stream Read/Write") {
public void test() throws IOException {
RandomAccessFile raf = new RandomAccessFile(
new File("temp.tmp"), "rw");
ra f.w rite In t(l);
Новый ввод-вывод (nio) 777
Блокировка файлов
Блокировка файлов, появившаяся в naKeTeJDK 1.4, позволяет синхронизировать до
ступ к файлу как к совместно используемому ресурсу. Впрочем, программные потоки,
претендующие на один и тот же файл, могут принадлежать различным виртуальным
машинам JVM, или один поток может быть Java-потоком, а другой представлять со
бой обычный поток операционной системы. Блокированные файлы видны другим
процессам операционной системы, поскольку механизм блокировки Java напрямую
связан со средствами операционной системы.
Простой пример блокировки файла:
//: io/FileLocking.java
import java.nio.channels.*;
import java.util.concurrent.*;
import java.io .* ;
Для блокировки всего файла целиком используется объект F ile L o c k , для получе
ния которого следует вызвать метод tryLock() класса FileChannel. (Сетевые каналы
SocketChannel, DatagramChannel и ServerSocketChannel не нуждаются в блокировании,
так как они доступны в пределах одного процесса. Вряд ли вы будете совместно ис
пользовать сокет между двумя процессами.) Метод tryLo ck() не приостанавливает
программу. Он пытается овладеть объектом блокировки, но если ему это не удается
(если другой процесс уже владеет этим объектом или файл не является разделяемым),
то он просто возвращает управление. Метод lock() ждет до тех пор, пока не удастся
получить объект блокировки, или поток, в котором этот метод был вызван, не будет
прерван, или же пока не будет закрыт канал, для которого был вызван метод lock().
Блокировка снимается методом FileC hann el.release().
Также возможно заблокировать часть файла, вызвав метод:
tryLock(long position, long size, boolean shared)
или
lock(long position, long size, boolean shared)
Класс потока LockAndModify устанавливает область буфера и получает его для моди
фикации методом s lic e ( ) . В методе run() запрашивается объект блокировки для фай
лового канала (запросить блокировку для буфера нельзя —это возможно только для
канала). Вызов метода lo ck() напоминает механизм синхронизации доступа потоков
к объектам, у вас появляется некая «критическая секция» с монопольным доступом
к данной части файла1.
Блокировки автоматически снимаются при завершении pa6oTbiJVM, закрытии канала,
для которого они были получены, но можно также явно вызвать метод relea se() объ
екта FileLock, как показано в нашем примере.
Сжатие данных
Библиотека ввода-вывода^уа содержит классы, поддерживающие чтение и запись
в потоки со сжатыми данными (табл. 18.8). Они базируются на уже существующих
потоках ввода-вывода и предоставляют функциональность сжатия.
Эти классы не являются производными от классов Reader и W riter, а входят в иерар
хию lnputStream и OutputStream. Это связано с тем, что библиотека сжатия работает не
с символами, а с байтами. Впрочем, иногда приходится смешивать потоки двух типов.
(Не забудьте, что байтовые потоки легко преобразуются в символьные — просто ис
пользуйте классы InputStreamReader и OutputStreamWriter.)
import ja va .io .* ;
import ja v a .u til.* ;
import sta tic net.m indview .util.Print.*;
in t с;
while((c = in.read()) != -1)
out.w rite(c);
in .c lo s e ()j
o u t.flu sh ();
>
o u t.clo se(); '
// Контрольная сумма действительна только после закрытия файла!
print("Checksum: " + csum.getChecksum().getValue())j
// Теперь извлечь файлы:
print("Reading f i l e " ) ;
FileInputStream f i = new FileInputStream ("test.zip")j
CheckedInputStream csumi =
new CheckedInputStream(fi, new Adler32())j
ZipInputStream in2 = new ZipInputStream(csumi);
BufferedInputStream bis = new BufferedInputStream(in2);
ZipEntry ze;
while((ze = in2.getNextEntry()) != n u ll) {
print("Reading f i l e ” + ze);
in t x;
while((x = b is.read ()) != -1)
System.out.write(x);
>
if(a rg s.le n g th == 1)
print("Checksum: " + csumi.getChecksum().getValue());
b is .c lo s e ();
// Альтернативный способ открытия и чтения ZIP-файлов:
Z ip F ile z f = new Z ip F ile (" te s t.z ip " ) ;
Enumeration e = z f.e n trie s () j
while(e.hasMoreElements()) {
ZipEntry ze2 = (ZipEntry)e.nextElement()j
p rin t( " F ile : " + ze2);
// ...после чего данные извлекаются так же, как преи&е
>
/* if(a rg s.le n g th ~ 1) */
>
) /* (Execute to see output) *///:~
Параметры запуска — просто набор букв (дефисы или другие признаки не нужны).
Пользователи систем UNIX/Linux сразу заметят сходство с программой tar. Допустимы
следующие параметры:
1 Возможно, дело в том, что комментарий ZIP-файла может быть многострочным объемом
до 64 Кбайт (в зависимости от версии архиватора, при этом он не сжимается), в отличие от
комментариев для отдельных вхождений. В таком случае текст комментария добавляется
(замещается) из отдельного файла TXT. Автор готов с этим согласиться. — Примеч. ред.
Сжатие данных 785
Команда создает файл JAR с именем myJarFile.jar, в котором содержатся все файлы
классов .class из текущего каталога, вместе с автоматически созданным манифестом.
ja r cmf m yJarFile.jar myManifestFile.mf * .class
После этого среда исполнения Java сможет искать файлы с классами в архивах libl.
jar и lib2.jar.
786 Глава 18 • Система ввода-вывода Java
Утилита jar не настолько универсальна, как архиватор zip. Например, она не позволя
ет добавлять или обновлять файлы в уже существующем архиве JAR. Также нельзя
перемещать файлы и удалять их после перемещения. Но при этом созданный файл
JAR всегда читается инструментом ja r на другой платформе (архиваторы zip о такой
совместимости могут только мечтать)1.
Как будет показано в главе 22, файль^АИ также используются для упаковки визуаль
ных KOMnoHeHToeJavaBean.
Сериализация объектов
Созданный вами объект существует до тех пор, пока он вам нужен, но ни при каких
условиях он не будет существовать после завершения программы. На первый взгляд это
выглядит логично, но в некоторых ситуациях было бы очень полезно, если бы инфор
мация объекта продолжала сохраняться даже в то время, когда программа не работает.
Затем при следующем запуске объект уже находится на своем месте и содержит ту же
информацию, что при последнем запуске. Конечно, нужного эффекта можно добиться,
сохраняя информацию в файл или базу данных, но в духе объектно-ориентированного
программирования было бы удобно иметь возможность объявления «долговременных»
объектов, чтобы система позаботилась обо всех подробностях за вас.
Сериализация (serialization) объектов Java позволяет преобразовать любой объект,
реализующий интерфейс S e r ia liz a b le , в последовательность байтов, по которой за
тем можно полностью восстановить исходный объект. Сказанное справедливо и для
сетевых соединений, а это значит, что механизм сериализации автоматически компен
сирует различия между операционными системами. То есть можно создать объект на
машине с ОС Windows, превратить его в последовательность байтов и послать их по
сети на машину с ОС UNIX, где объект будет корректно воссоздан. Вам не надо думать
о различных форматах данных, порядке следования байтов или других подробностях.
Сама по себе сериализация объектов интересна потому, что с ее помощью можно осу
ществить легковесное долговременное хранение (lightweight persistence). Это означает,
что время жизни объекта определяется не только временем выполнения программы —
объект существует и между запусками программы. Можно взять объект и записать его
на диск, а после, при другом запуске программы, восстановить его в первоначальном
виде и таким образом получить эффект «живучести». Почему такой способ хранения
называется «легковесным»? Дело в том, что программа не может определить объект
как «постоянный» при помощи некоторого ключевого слова, то есть долговременное
хранение напрямую не поддерживается языком (хотя со временем следует этого ожи
дать). Система выполнения не заботится о деталях сериализации — вам приходится
собственноручно сериализовывать и восстанавливать объекты вашей программы.
Если вам необходим более серьезный механизм сериализации, попробуйте библиотеку
Hibernate (http://hibem atesourceforge.net). За подробностями обращайтесь к книге
Thinking In EnterpriseJava, которую можно загрузить с сайта www.MindView.net.
1 Для платформы Windows apxneJAR является не чем иным, как файлом ZIP (сменив расши
рение, вы сможете работать с ним как с обычным ZIP-файлом). — Примеч. ред.
Сериализация объектов 787
Механизм сериализации объектов был добавлен в язык для поддержки двух расши
ренных возможностей. Удаленный вызовметодов]а\а (RMI) позволяет работать с объ
ектами, «обитающими» на других компьютерах, так, словно они существуют на вашей
машине. При отправке сообщений удаленным объектам необходимо транспортировать
аргументы и возвращаемые значения. Для этого используется сериализация объектов.
Удаленный вызов методов обстоятельно обсуждается в книге ThinkingIn EnterpriseJava.
Сериализация объектов также требуется визуальным KOMnoHeHTaMjavaBean, которые
описаны в главе 22. Информация состояния визуальных компонентов обычно задается
во время разработки. Эту информацию о состоянии необходимо сохранить, а затем
при запуске программы восстановить; данную задачу решает сериализация объектов.
Сериализовать объект достаточно просто, если он реализует интерфейс S e r ia liz a b le
(это идентификационный интерфейс, в нем нет ни одного метода). Когда в язык до
бавили механизм сериализации, во многие классы стандартной библиотеки внесли
изменения так, чтобы они были готовы к сериализации. К таким классам относятся
все классы-«обертки» для простейших типов, все классы контейнеров и многие другие.
Даже объекты Class, представляющие классы, можно сериализовать.
Для того чтобы сериализовать объект, требуется создать выходной поток OutputStream,
который нужно упаковать в объект ObjectOutputStream. На этой стадии остается вы
зывать метод w rite O b je ct(), чтобы объект был сериализован и передан в выходной
поток Outputstream (процесс сериализации объекта осуществляется на уровне байтов,
поэтому используются иерархии Inputstream и Outputstream). Для восстановления
объекта необходимо упаковать объект InputStream в ObjectInputStream, азатем вызвать
метод readObject(). Как обычно, такой метод возвращает ссылку на Object, поэтому
после вызова метода следует провести нисходящее преобразование для получения
объекта нужного типа.
В процессе сериализации объектов учитываются ссылки, содержащиеся в объекте.
Сохраняется не только сам образ объекта, но и все связанные с ним объекты, и все
объекты в связанных объектах и т. д. Это часто называют «паутиной объектов», к ко
торой можно присоединить одиночный объект, а также массив ссылок на объекты
и объекты-члены. Если бы вы создавали свой собственный механизм сериализации,
отслеживание всех имеющихся в объектах ссылок стало бы весьма нелегкой задачей.
Однако в сериализации объектов Java, похоже, никаких трудностей со ссылками нет —
судя по всему, в этот язык встроен достаточно эффективный алгоритм создания графов
объектов. Следующий пример проверяет механизм сериализации, создавая цепочку
связанных объектов, каждый из которых связан со следующим сегментом цепочки,
а также имеет массив ссылок на объекты другого класса с именем Data:
//: io/Worm.java
// Сериализация объектов,
import ja v a .io .* ;
import ja v a .u til.* ;
import sta tic net.m indview .util.Print.*;
i f ( - - i > 0)
next = new Worm(i, (char)(x + 1));
>
public worm() {
print("Default constructor");
}
public String toString() {
StringBuilder result = new StringB uilder(":");
result.append(c);
result.append("(");
for(Data dat : d)
result.append(dat);
result.append(")");
if(next != null)
re s u lt. append(next);
return re su lt.toS trin g ();
}
public sta tic void main(String[] args)
throws ClassNotFoundException, lOException {
Worm w = new Worm(6, 'a ') ;
print("w = " + w);
ObjectOutputStream out = new ObjectOutputStream(
new FileOutputStream("worm.out"));
out.writeObject("Worm storage\n");
out.writeObject(w);
out.close(); // Также сбрасывает буфер
ObjectInputStream in = new ObjectInputStream(
new FileInput5tream("worm.out”));
String s = (String)in.readObject();
Worm w2 = (Worm)in.readObject();
print(s + "w2 = " + w2);
ByteArrayOutputStream bout =
new ByteArrayOutputStream();
ObjectOutputStream out2 = new ObjectOutputStream(bout)
out2.writeObject("Worm storage\n");
out2.writeObject(w);
out2.flush();
ObjectInputStream in2 = new ObjectInputStream(
new ByteArrayInputStream(bout.toByteArray()));
Сериализация объектов 789
s = (String)in2.readO bject();
Wonm w3 = (Worm)in2.readObjectQ;
p rin t(s + "w3 = " + w3);
>
} |* Output:
Wonm constructor: б
Worm constructor: 5
Worm constructor: 4
Worm constructor: 3
Worm constructor: 2
Worm constructor: 1
w = :a(853):b(119):c(802):d(788):e(199):f(881)
Worm storage
w2 = :a(853):b(119):c(802):d(788):e(199):f(881)
Worm storage
w3 = :a(853):b(119):c(802):d(788):e(199):f(881)
V//:~
Чтобы сделать все немного интереснее, массив объектов Data в классе Worm инициали
зируется случайными числами. (Таким образом, нельзя заподозрить компилятор в том,
что он использует дополнительную информацию для хранения объектов.) Каждый
сегмент Worm помечается порядковым номером-символом (ch ar), который автома
тически генерируется в процессе рекурсивного формирования связанной цепочки
объектов worm. При создании цепочки необходимо указать ее размер в конструкторе
класса Worm. Для инициализации ссылки next рекурсивно вызывается конструктор
класса worm, однако с каждым разом размер цепочки уменьшается на единицу. В по
следнем сегменте цепочки ссылка next остается со значением n u ll, что показывает
на конец цепочки.
Все это было сделано с единственной целью —создать достаточно сложную структуру
для проверки сериализации «на прочность». Впрочем, сам акт сериализации проходит
проще простого. После того как создается поток ObjectOutputStream (на основе другого
выходного потока), метод w riteO bject() записывает в него объект. Заметьте, что в поток
также записывается строка (strin g ). В этот же поток можно поместить все простейшие
типы, используя те же методы, что и в классе DataOutputStream (оба потока реализуют
одинаковый интерфейс).
В программе есть два похожих фрагмента кода. В первом запись и чтение производятся
в файл, а во втором для разнообразия хранилищем служит массив байтов ByteArray.
Чтение и запись объектов посредством сериализации возможны в любые классы, про
изводные от DataInputStream или DataOutputStream, в том числе и в сетевые подключения
(эта возможность описана в книге ThinkingIn EnterpriseJava).
Из вывода видно, что десериализованный объект действительно содержит все ссылки,
присутствовавшие в исходном объекте.
Заметьте, что в процессе восстановления объекта, реализующего интерфейс S eria liza b le,
никакие конструкторы (даже конструктор по умолчанию) не вызываются. Объект
790 Глава 18 • Система ввода-вывода Java
Поиск класса
А что необходимо для восстановления объекта после проведения сериализации? На
пример, предположим, что вы создали объект, сериализовали его и отправили в виде
файла или через сетевое соединение на другой компьютер. Сумеет ли программа на
д р у г о м к о м п ь ю т е р е реконструировать объект, опираясь только на те данные, что в ы
записали в файл в процессе сериализации?
Лучший способ получить ответ на этот вопрос (как обычно, впрочем) —провести экс
перимент. Следующий файл располагается в подкаталоге данной главы:
//: io/Alien.java
// Сериализуемый класс,
import ja v a ,io .* ;
public class A lien implements S e ria liza b le {> ///:~
1 В этом разделе Эккель рассказывает о сериализации по умолчанию, где для успешного по
следующего восстановления объекта требуется соблюдение трех условий, которые здесь не
оговариваются. Во-первых, классы, реализующие интерфейс Serializable, обязаны иметь
конструктор без аргументов (здесь это конструктор по умолчанию Worm ()), Этот конструктор
будетп вызван, когда объект восстанавливается из файла с расширением SER (в такой файл за
писываются сериализуемые пакетом Beans Development Kit (BeanBox) компоненты JavaBean).
Во-вторых, если Serializable уже реализован в суперклассе, то его реализация не требуется
в подклассе, то есть способность к сериализации наследуется. И последнее, по умолчанию
сериализуются только не статические и не transient-поля (см. раздел ниже «Ключевое слово
transient»). П о л я сериализуемых классов целиком и полностью восстанавливаются из потока,
но предполагается и случай, когда подтипы несериализуемых классов д о л ж н ы сериализоваться.
Это возможно только при наличии расширяющего несериализуемый класс конструктора без
аргументов. В о время десериализации поля несериализуемого класса будут инициализированы
через объявленный открьггым (public) или з а щ и щ е н н ы м (protected) конструктор без аргу
ментов. Этот конструктор должен быть доступен из сериализуемого подкласса. В противном
случае будет получена ошибка времени выполнения. Все это подробно описано в документации
Java Oava.io.Serializable.java) и в руководствеJava tutorial от Sun (файл ^avabeans^erslstence\
index.html). — Примеч. ред.
Сериализация объектов 791
Даже для открытия файла и чтения объекта mystery необходим объект C lass для A lien ;
виртуальная ManiHHaJava (JV M ) не может найти файл AHen.class (если только он не на
ходится через переменную окружения CLASSPATH, но в этом примере такого быть не
должно). Возникает исключение classNotFoundException. Таким образом, виртуальная
машина Java должна быть способна обнаружить ассоциированный с объектом A lie n
файл .class.
\
\
Управление сериализацией
Как вы могли убедиться, встроенный механизм сериализации достаточно прост в при
менении. Но что, если у вас возникли особые требования? Возможно, из соображений
безопасности вы не хотите сохранять некоторые части вашего объекта, или же сериа
лизовать какой-либо объект, содержащийся в главном объекте, не имеет смысла, так
как при восстановлении он все равно должен создаваться заново.
Чтобы управлять процессом сериализации, реализуйте в своем классе интерфейс
E x t e r n a liz a b le вместо интерфейса S e r i a l i z a b l e . Этот интерфейс расширяет ори
гинальный интерфейс S e r ia liz a b le и добавляет в него два метода, w r it e E x t e r n a l( )
и readExternal(), которые автоматически вызываются в процессе сериализации и вос
становления объектов, позволяя вам попутно выполнить специфические действия.
Ниже приведен простой пример реализации Externalizable. Заметьте также, что классы
B li p l и B lip 2 практически одинаковы —разные у них только названия, да и те из серии
«Найдите 10 отличий»:
792 Глава 18 • Система ввода-вывода Java
Saving objects:
B lipl.w riteEx tern al
Blip2.w riteExternal
Recovering Ы :
B lip l Constructor
B lipl.readExternal
*/ll:~
Почему же в программе не восстанавливается объект Blip2? При попытке его восстанов
ления в программе возбуждается исключение. Видите ли вы разницу между классами
B li p l и Blip2 ? Конструктор класса B l i p l является открытым (p u b lic), в то время как
конструктор класса B lip 2 таковым не является, и именно это приводит к исключению
в процессе восстановления. Попробуйте объявить конструктор класса B lip2 открытым
и удалить комментарии / /! и вы увидите, что все работает, как и было запланировано.
При восстановлении b l вызывается конструктор по умолчанию класса B lip l. Ситуа
ция отличается от восстановления объекта, реализующего интерфейс S e r ia liz a b le ,
при котором объект конструируется исключительно на основе сохраненных данных,
без вызова конструкторов. В случае с объектом E x te rn a liza b le происходит нормаль
ный процесс реконструирования (включая инициализацию в точке определения),
и далее вызывается метод readExternal(). Помните об этом при реализации объектов
Extern alizable —в особенности обратите внимание на то, что вызывается конструктор
по умолчанию.
В следующем примере показано, что надо сделать для полноты операций сохранения
и восстановления объекта Externalizable:
//: io/Blip3.java
// Восстановление объекта Externalizable.
import ja v a .io .* ;
import s ta tic net.m indview .u til.P rint.* ;
Поля date и username объявлены без модификатора tra n sie n t, поэтому сериализация
для них проводится автоматически. Однако поле password описано как tra n sie n t и по
этому не сбрасывается на диск; кроме того, механизм сериализации его игнорирует при
восстановлении. Когда объект восстанавливается, поле password равно n u ll. Заметьте,
что хотя метод to S trin g () собирает объект S trin g с использованием перегруженного
оператора +, ссылка n u ll автоматически преобразуется в строку nu ll.
Также видно, что поле date сохраняется надиске и при восстановлении его значение
не меняется.
Так как объекты E x te rn aliza b le по умолчанию не сохраняют полей, ключевое слово
transient для них не имеет смысла. Оно применяется только для объектов S eria liza b le.
В этом примере одна из строк класса объявлена как tran sien t, с целью продемонстриро
вать, что такие поля при вызове метода defaultW riteObject() не сохраняются. Эта строка
сохраняется и восстанавливается программой явно. П оля класса инициализирую тся
в конструкторе, а не в точке определения, чтобы показать, что они не инициализиру
ются каким-либо особы м м еханизм ом в процессе восстановления.
Е сли вы соби раетесь использовать встроенны й м ехани зм сер и али зац и и дл я за п и
си полей объекта (б е з объявления tr a n s ie n t), н уж н о при зап иси объекта в первую
очередь вызвать м етод defau ltW riteO b ject(), а при восстановлении объекта — метод
defaultReadObject(). Э то вообщ езагадочны е методы. Например, если вы вызовете метод
defaultW riteO bject() для потока ObjectOutputStream и не передадите ем у аргументов, он
все ж е как-то узнает, какой объект надо записать, где находится ссылка на него и как
записать все его н е-tr a n sie n t, составляющ ие. М истика.
При сохранении и восстановлении t r a n s ie n t-объектов используется бол ее знакомый
код. И все ж е посмотрите, что происходит. В методе main() создается объект S e r ia lC tl,
который затем сериализуется потоком ObjectOutputStream. (П р и этом для вывода ис
пользуется буф ер, а не ф айл — для потока ObjectOutputStream это одн о и то ж е.) Н е
посредственно сериализация выполняется в следую щ ей строке;
о . writeObj e ct(sc );
Версии
Иногда необходимо изменить версию сохраненного класса (объекты исходного класса
могут храниться, например, в базе данных). Поддержка для этого есть, но делать это
следует только в особых ситуациях, и желательно глубокое понимание вопросов се
риализации, какого данная книга не предоставляет. Тема подробно рассматривается
в докум ентации^К , которую можно загрузить на сайте h ttp ://ja va .su n .co m .
Обратите внимание, что в докум ен тац и и ^К многие комментарии начинаются так:
П р ед у п р еж д ен и е. Сериализованные объекты этого класса не будут совместимы с п о
следующ ими выпусками библиотеки Swing. Текущий механизм сериализации годит ся
только для крат ковременного хранения или для удаленны х въизовов из приложений...
Все это происходит из-за того, что на данный момент механизм контроля версий недо
статочно соверш енен для эф ф ективной поддерж ки больш инства ситуаций, особенно
для хранения KOMnoHeHTOBjavaBean. Разработчики языка продолжают улучшать его
архитектуру, в чем и состоит смысл таких предупреждений.
Долговременное хранение
Идея привлечь технологию сериализации для того, чтобы сохранить состояние вашей
программы и в будущем восстановить его, выглядит соблазнительно. Но прежде чем
делать это, необходимо ответить на несколько вопросов. Что произойдет, если со
хранить два объекта, в которых имеется ссылка на некоторый общий третий объект?
Когда вы восстановите эти объекты, сколько экземпляров третьего объекта появится
в программе? Что, если вы сохраните объекты в отдельных файлах, а затем десериа
лизуете их в разных частях программы?
Следующий пример демонстрирует возможные проблемы:
//: io/MyWorld.java
import ja v a .io .* ;
import ja v a .u til.* ;
import s ta tic net.m indview .util.P rint.*;
В этом примере интересно еще то, что в нем показано, как использовать механизм
сериализации и байтовый массив для «глубокого копирования» любого объекта с ин
терфейсом Serializable. (Глубокое копирование —это создание дубликата всего графа
объектов, а не просто основного объекта и его ссылок.)
Объекты Animal содержат поля типа House. В методе main() создается список List с не
сколькими объектами Animal, его дважды записывают в один поток и еще один раз —
в отдельный поток. Когда эти списки восстанавливают и распечатывают, получается
Сериализация объектов 801
результат, показанный для одного запуска (объекты при каждом запуске программы
будут располагаться в различных областях памяти).
Конечно, нет ничего удивительного в том, что восстановленные объекты и их оригиналы
имеют разные адреса. Но заметьте, что адреса в восстановленных объектах animalsl
и animals2 совпадают, вплоть до повторения ссылок на объект House, разделяемый
обоими списками. С другой стороны, при восстановлении списка animals3 система не
имеет представления о том, что находящиеся в них объекты уже были восстановлены
и имеются в программе, поэтому она создает новую сеть объектов.
Если все данные сериализуются в один выходной поток, у вас есть гарантия того, что
сохраненная вами сеть объектов затем полностью восстановится в первоначальном
виде, без лишних дубликатов объектов. Конечно, состояние объекта может измениться
между первой и последней записью, но это уже на вашей совести — сохраненные объ
екты останутся в гом состоянии, в котором вы их записали (с теми связями, чго у них
были на момент сериализации).
Если уж необходимо зафиксировать состояние системы, безопаснее всего сделать это
в рамках «атомарной» операции. Если вы сохраняете что-то, затем выполняете какие-то
действия, снова сохраняете данные и т. д., вам не удастся безопасно зафиксировать со
стояние системы. Вместо этого следует поместить все объекты, образующие состояние
системы, в контейнер и сохранить этот контейнер единой операцией. После можно
восстановить его вызовом одного метода.
Следующий пример — имитатор системы автоматизированного проектирования
(CAD), в котором используется такой подход. Вдобавок в нем сохраняются статиче
ские (static) поля —если вы взглянете н адокум ентацию ^К , то увидите, что класс
Class реализует интерфейс Serializable, поэтомудля сохранения статических данных
достаточно сохранить объект Class. Это достаточно разумное решение.
//: io/StoreCADState.java
// Сохранение и восстановление состояния вымышленной системы,
import java.io.*;
import java.util.*;
Классы Circle и Square — простые подклассы фигуры Shape; разница между ними только
в способе инициализации поля color: окружность (Circle) задает значение этого поля
в месте определения, а прямоугольник (Square) инициализирует его в конструкторе.
Класс Line мы обсудим чуть позже.
В методе main() один список ArrayList используется для хранения объектов Class,
а другой —для хранения фигур.
Восстановление объектов выполняется очень просто:
//: io/RecoverCADState.java
// Restoring the state of the pretend CAD system.
// {RunFirst: StoreCADState}
import java.io.*;
import java.util.*;
XML
У механизма сериализации объектов есть одно важное ограничение: он работает только
с o6beKTaMnJava, то есть десериализовать такие объекты можно только в программах
Java. Более универсальное решение заключается в преобразовании данных в формат
XML, чтобы их можно было использовать на разных платформах и языках.
Из-за популярности этой технологии существует много вариантов программирования
XML, включая библиотеки javax.xml.*, входящие в nocraBKyJDK. Я решил использовать
свободно распространяемую библиотеку XOM Эллиота Расти Харольда (библиотеку
и документацию можно загрузить на сайте т е т т ж о т . п и ) , потому что это самый простой
и прямолинейный способ создания и модификации XML в языке Java. Кроме того,
XOM уделяет особое внимание корректности данных XML.
Предположим, у вас имеются объекты Person с именами и фамилиями, которые вам
хотелось бы сериализовать в XML. Приведенный ниже класс Person содержит метод
getXML(), который использует XOM для преобразования данных Person в объект XML
Element, и конструктор, который получает Element и извлекает данные Person (обратите
внимание: примеры XML находятся в отдельном подкаталоге):
//: xml/Person.java
// Использование библиотеки XOM для записи и чтения XML
// {Requires: nu.xom.Node; установите библиотеку
// XOM с сайта http://www.xom.nu >
import nu.xom.*;
import java.io.*;
import java.util.*;
Elements elements =
doc,getRootElement().getChildElements();
for(int i = 0j i < elements.size(); i++)
add(new Person(elements.get(i)));
>
public static void main(String[] args) throws Exception {
People p = new People("People.xml");
System.out.println(p)j
>
} /* Output:
[Dr. Bunsen Honeydew, Gonzo The Great, Phillip 3. Fry]
*///:~
Предпочтения
В naKeTeJDK 1.4 появился программный интерфейс Preferences API для работы
с предпочтениями (preferences). Предпочтения гораздо более тесно связаны с долго
временным хранением, чем механизм сериализации объектов, поскольку они позволяют
автоматически сохранять и восстанавливать вашу информацию. Однако их использо
вание сдерживается маленькими и ограниченными наборами данных —хранить в них
можно только примитивы и строки, и длина строки не должна превышать 8 Кбайт (не
то чтобы очень мало, но делать с этим что-то серьезное вряд ли стоит). Как и предпо
лагает название нового API, предпочтения предназначены для хранения и получения
информации о предпочтениях пользователя и конфигурации программы.
Предпочтения —это набор пар «ключ-значение» (как в карте), образующих иерархию
узлов. Хотя иерархия узлов и годится для построения сложных структур, чаще всего
808 Глава 18 • Система ввода-вывода Java
создают один узел, названный так же, как и класс, и хранят информацию в нем. Вот
простой пример:
//: io/PreferencesDemo.java
import java.util.prefs.*;
import static net.mindview.util.Print.*;
В этом случае при первом запуске программы значение usageCount равно нулю, а при
последующих запусках оно должно измениться.
Когда вы запустите программу PreferencesDemo.java, вы увидите, что значение usageCount
действительно увеличивается при каждом запуске программы. Но где же хранятся
данные? После первого запуска программы не появляется никаких локальных фай
лов. Система предпочтений привлекает для хранения данных подходящие системные
возможности, которые в различных операционных системах различны. В Windows
используется реестр (поскольку он и так представляет собой иерархию узлов с набо
ром пар «ключ-значение»). Вся суть заключается в том, что информация сохраняется
автоматически и вам не приходится беспокоиться о том, как это работает в различных
системах.
В программном интерфейсе предпочтений гораздо больше возможностей, чем было
здесь показано. За более подробным описанием обратитесь к дoкyмeнтaцииJDK, эта
тема там изложена достаточно подробно.
33 . (2) Напишите программу, которая выводит текущее значение, связанное с катало
гом, и запрашивает новое значение. Используйте API предпочтений для хранения
значения.
Резюме
Библиотека ввода-вывода Java подходит для выполнения основных операций: она
позволяет проводить чтение данных и запись их на консоль, в файл, в буфер памяти,
даже в сетевое соединение Интернета. Применяя наследование, можно создавать
новые типы объектов для ввода и вывода данных. Вы даже можете расширить виды
объектов, принимаемых потоком, переопределяя метод toString(), который автомати
чески вызывается при передаче объекта методу, ожидающему получить строку (String)
(ограниченное «автоматическое преобразование типов» Java).
Но есть и вопросы, на которые вы не найдете ответов в интерактивной документации
и библиотеке ввoдa-вывoдaJava. Например, было бы замечательно, если бы при по
пытке перезаписи файла возбуждалось исключение —многие программные системы
позволяют указать, что файл должен открываться для вывода только в том случае,
если файла с таким именем еще не существует. В H3biKeJava проверка существования
файла возможна лишь с помощью объекта File, поскольку если вы откроете файл как
FileOutputstream или FileWriter, старый файл всегда будет перезаписан.
Ключевое слово епитп создает новый тип с ограниченньш набором именованных значений,
и работ ат ь с этими значениям и можно как с обычными компонентами программы.
Д анная возможность иногда оказывается чрезвычайно полезной1.
Перечисления (enumerations) были кратко представлены в конце главы 5. Теперь,
когда вы более глубоко noHHMaeTeJava, мы можем поближе познакомиться с перечис
лениями Java SE5. Вы увидите, что перечисления открывают ряд очень интересных
возможностей, но эта глава также поможет лучше понять другие возможности языка,
которые уже рассматривались ранее, —такие, как обобщения и отражение. Также мы
рассмотрим несколько новых паттернов проектирования.
for(Shrubbery s : Shrubbery.values()) {
print(s + " ordinal: " + s.ordinal());
printnb(s.compareTo(Shrubbery.CRAWLING) + " ")j
printnb(s.equals(Shrubbery.CRAWLING) + " ");
print(s == Shrubbery.CRAWLING);
print(s.getDeclaringClass());
print(s.name());
print("................. ..... ");
}
// Получить значение из перечисления по строковому имени:
for(String s : "HANGING CRAWLING GROUND".split(" ")) {
Shrubbery shrub = Enum.valueOf(Shrubbery.class, s)j
print(shrub);
>
>
} /* Output:
GROUND ordinal: 0
-1 false false
class Shrubbery
GROUND
CRAWLING ordinal: 1
0 true true
class Shrubbery
CRAWLING
HANGING ordinal: 2
1 false false
class Shrubbery
HANGING
HANGING
CRAWLING
GROUND
*///:~
System.out.println(s);
>
}
> /* Output:
Scout
Cargo
Transport
Cruiser
Battleship
Mothership
*///:~
Метод toString() получает имя объекта spaceShip, вызывая метод name(), и изменяет
результат так, чтобы прописной была только первая буква.
Хотя обычно экземпляр перечисления должен уточняться его типом, в секциях case
это не обязательно. В следующем примере перечисление используется для создания
простейшего конечного автомата:
//: enumerated/TrafficLight.java
// Перечисления в командах switch.
import static net.mindview.util.Print.*;
Странности values()
Как упоминалось ранее, все классы перечислений создаются за вас компилятором
и расширяют класс Enum. Но если присмотреться к классу Enum, вы увидите, что метода
values() в нем нет, хотя мы его и использовали. Нет ли других «скрытых» методов?
Чтобы получить ответ на этот вопрос, можно написать маленькую программу с ис
пользованием отражения:
//: enumerated/Reflection.java
// Анализ перечислений с использованием отражения.
import java.lang.reflect.*;
import java.util.*;
import net.mindview.util.*j
import static net.mindview.util.Print.*;
methods.add(m.getName());
print(methods);
return methods;
}
public static void main(String[] args) {
Set<String> exploreMethods = analyze(Explore.class);
Set<String> enumMethods = analyze(Enum.class);
print("Explore.containsAll(Enum)? " +
exploreMethods.containsAll(enumMethods));
printnb("Explore.removeAll(Enum): ");
exploreMethods.removeAll(enumMethods);
print(exploreMethods);
// Decompile the code for the enum:
OSExecute.command("javap Explore");
}
> /* Output:
---- Analyzing class Explore ----
Interfaces:
Base: class java.lang.Enum
Methods:
[compareTo, equals, getClass, getDeclaringClass, hashCode,
name, notify, notifyAll, ordinal, toString, valueOf,
values, wait]
---- Analyzing class java.lang.Enum ----
Interfaces:
java.lang.Comparable<E>
interface java.io.Serializable
Base: class java.lang.Object
Methods:
[compareTo, equals, getClass, getDeclaringClass, hashCode,
name, notify, notifyAll, ordinal, toString, valueOf, wait]
Explore.containsAll(Enum)? true
Explore.removeAll(Enum): [values]
Compiled from "Reflection.java"
final class Explore extends java.lang.Enum{
public static final Explore HERE;
public static final Explore THERE;
public static final Explore[] values();
public static Explore valueOf(java.lang.String);
static {};
}
*///:~
Так как getEnumConstants() является методом Class, его можно вызвать для класса, не
являющегося перечислением:
//: enumerated/NonEnum.java
Однако метод возвращает null, так что при попытке использования результата воз
буждается исключение.
Реализация, а не наследование
Мы установили, что все перечисления расширяют java.lang.Enum. Так KaKjava не
поддерживает множественное наследование, это означает, что перечисление не может
быть создано посредством наследования:
enum NotPossible extends Pet { ... // Не работает
Случайный выбор 8 19
Случайный выбор
Во многих примерахэтой главы (как, например, в CartoonCharacter.next()) использу
ется случайный выбор из экземпляров перечисления. Эту задачу можно перевести на
более общий уровень с использованием обобщений и поместить результат в библиотеку:
//: net/mindview/util/Enums.java
package net.mindview.util;
import java.util.*;
//: enumerated/menu/Meal.java
package enumerated.menu;
SOUP
VINDALOO
FRUIT
TEA
SALAD
BURRITO
FRUIT
TEA
SALAD
BURRITO
CREME_CARAME L
LATTE
SOUP
BURRITO
TIRAMISU
ESPRESSO
*///:~
enum SecurityCategory {
STOCK(Security.Stock.class), BOND(Security.Bond.class);
Security[] values;
SecurityCategory(Class<? extends Security> kind) {
values = kind.getEnumConstants<);
>
Случайный выбор 823
interface Security {
enum Stock implements Security { SHORT, LONG, MARGIN }
enum Bond implements Security { MUNICIPAL, DUNK >
>
public Security randomSelection() {
return Enums.random(values);
}
public static void main(String[] args) {
for(int i = 0; i < 10; i++) {
SecurityCategory category =
Enums.random(SecurityCategory.class);
System.out.println(category + ": " +
category *randomSelection());
>
}
} /* Output:
BOND: MUNICIPAL
BOND: MUNICIPAL
STOCK: MARGIN
STOCK: MARGIN
BOND: DUNK
STOCK: SHORT
STOCK: LONG
STOCK: LONG
BOND: MUNICIPAL
BOND: DUNK
*///:~
что список аргументов переменной длины тоже решает проблему, но слегка уступает
по эффективности явной передаче аргументов. Таким образом, при вызове of() с пере
дачей от двух до пяти аргументов вы получите (чуть более быстрые) вызовы с явной
передачей, а при вызове с одним или более чем пятью аргументами будет вызвана
версия of( ) со списком аргументов переменной длины. Учтите, что при вызове с одним
аргументом компилятор не будет конструировать массив аргументов, так что вызов
этой версии обходится без лишних затрат.
Объекты EnumSet строятся на базе 64-разрядных значений long, а состояние каждого
экземпляра перечисления обозначается одним битом. Это означает, что множество
EnumSet может представлять до 64 элементов без выхода за пределы одного long. А что
произойдет, если перечисление содержит более 64 элементов?
//: enumerated/BigEnumSet.java
import java.util.*;
Использование EnumMap
EnumMap—специализированная карта, ключи которой хранятся в одном перечислении.
Из-за ограничений, действующих для перечислений, внутренняя реализация EnumMap
может базироваться на массиве. Такие контейнеры работают исключительно быстро,
поэтому EnumMap можно свободно использовать для поиска значений по ключам из
перечислений.
Вызов put() разрешен только для ключей, входящих в перечисление, но в остальном
контейнер EnumMap ведет себя как самая обычная карта.
Использование EnumSet вместо флагов 827
Методы констант
П еречисления^уа обладают очень интересной возможностью, которая позволяет
назначить каждому экземпляру перечисления свое поведение. Для этого следует
определить один или несколько абстрактных методов в составе перечисления, а затем
определить методы для каждого экземпляра перечисления. Пример:
//: enumerated/ConstantSpecificMethod.java
import java.util.*;
import java.text.*;
enum LikeClasses {
WINKEN { void behavior() { print("Behaviorl"); } },
BLINKEN { void behavior() { print("Behavior2")j } },
Использование EnumSet вместо флагов 829
};
abstract void action();
>
EnumSet<Cycle> cycles =
EnumSet.of(Cycle.BASIC, Cycle.RINSE);
public void add(Cycle cycle) { cycles.add(cycle); >
public void washCar() {
for(Cycle с : cycles)
c.action()j
>
public String toString() { return cycles.toString(); }
public static void main(String[] args) {
CarWash wash = new CarWash();
print(wash);
wash.washCar();
// Порядок добавления неважен:
wash.add(Cycle.BLOWDRY);
wash.add(Cycle.BLOWDRY); // Дубликаты игнорируются
wash.add(Cycle.RINSE)j
wash.add(Cycle.HOTWAX);
print(wash);
wash.washCar();
>
} /* Output:
[BASIC, RINSE]
The basic wash
Rinsing
[BASIC, HOTWAX, RINSE, BLOWDRY]
The basic wash
Applying hot wax
Rinsing
Blowing dry
*///:~
Цепочка обязанностей
В паттерне « Ц е п о ч к а о б я з а н н о с т е й » р а з р а б о т ч и к создает н е с к о л ь к о с п о с о б о в р е ш е
ния некоторой задачи и связывает их в цепочку. Поступающий запрос передается по
цепочке до тех пор, пока одно из решений не сможет обработать его.
Простой вариант «Цепочки обязанностей» легко реализуется методами констант. Рас
смотрим модель почтового отделения, которое пытается обрабатывать входящую почту
по общим правилам, но продолжает применять разные варианты до тех пор, пока не
придет к выводу о невозможности доставки. Каждая попытка может рассматриваться
как «Стратегия» (еще один паттерн проектирования), а весь список в целом — как
«Цепочка обязанностей».
Все начинается с описания почтового отправления. Все разные характеристики, учи
тываемые при обработке, могут быть представлены перечислениями. Так как объекты
Mail будут генерироваться случайным образом, для снижения вероятности получить
(например) YES в категории GeneralDelivery проще всего создать побольше экземпляров,
отличных от YES, так что определения enum на первый взгляд смотрятся немного странно.
В определении Mail присутствует метод randomMail(), создающий случайные объекты
почтовых отправлений. Метод generator() создает объект Iterable, который использует
randomMail() для создания набора объектов — по одному при каждом вызове next()
через итератор. Такая конструкция позволяет легко создать цикл fo r e a c h вызовом
M a i l .generator():
//: enumerated/PostOffice.java
// Modeling а post office.
import java.util.*;
import net.mindview.util.*;
import static net.mindview.util.Print.*;
class Mail {
// Множественные N0 снижают вероятность
// случайного выбора YES:
enum GeneralDelivery {YES^N01,N02,N03,N04,N05}
enum Scannability {UNSCANNABLE,YESl,YES2,YES3,YES4}
enum Readability {ILLEGIBLE,YESl,YES2,YES3,YES4}
enum Address {INC0RRECT,0Kl,0K2,0K3,0K4,0K5,0K6}
enum ReturnAddress {MISSING,0Kl,0K2,0K3,0K4,0K5}
GeneralDelivery generalDeliveryj
Scannability scannability: ^ .
продолжение &
832 Глава19 • Перечислимыетипы
R e a d a b ility re a d a b ility ;
Address address;
R eturnAddress returnA ddress;
s ta tic lo n g c o u n t e r = 0;
lo n g id = counter+ + ;
p u b lic S trin g to S trin g () { return " M a il " + id ; }
p u b lic S trin g d e ta ils ( ) {
return to S trin g () +
", G eneral D e liv e ry : " + g e n e ra lD e liv e ry +
", A ddress S c a n a b ili t y : " + s c a n n a b ility +
", Address R e a d a b ility : " + re a d a b ility +
", A ddress A d d re ss: " + address +
", R eturn a d d r e s s : " + returnA ddress;
}
// G e n e r a t e t e s t M a i l :
p u b lic s ta tic M a il ra n d o m M a il() {
Mail m = new Mail();
m. g e n e r a l D e l i v e r y = Enum s. r a n d o m ( G e n e r a l D e l i v e r y . c l a s s ) ;
m .s c a n n a b ility = E n u m s .ra n d o m (S c a n n a b ility .c la s s );
m .re a d a b ility = E n u m s .ra n d o m (R e a d a b ility .c la s s );
m .ad d re ss = E n u m s.ra n d o m (A d d re ss.c la ss );
m .re tu rn A d d re ss = E n u m s.ra n d o m (R e tu rn A d d re ss.c la ss);
r e t u r n m;
>
p u b lic s ta tic I te ra b le < M a il> g e n e ra to r(fin a l in t count) {
return new I t e r a b l e < M a i l > ( ) {
in t n = cou nt;
p u b lic I te ra to r< M a il> ite ra to r() {
return new I t e r a t o r < M a i l > ( ) {
p u b lic b o o le a n h a s N e x t( ) { return n-- > 0; )
p u b lic M a il next() { return ra n d o m M a il( ); }
p u b lic v o id rem ove() { // Не р е а л и з о в а н о
th ro w new U n s u p p o r t e d O p e r a t i o n E x c e p t i o n ( ) ;
}
};
}
};
>
>
p u b lic c la s s P o s tO ffic e {
enum M a i l H a n d l e r {
GENERAL_DELIVERY {
b o o le a n h a n d l e ( M a i l m) {
s w itc h (m .g e n e ra lD e liv e ry ) {
c a s e YES:
p rin t(" U s in g general d e liv e ry fo r " + m);
return tru e;
d e fa u lt: return fa ls e ;
>
>
>>
MACHINE_SCAN {
b o o le a n h a n d l e ( M a i l m) {
sw itc h (m .s c a n n a b ility ) {
c a s e UNSCANNABLE: return fa ls e ;
d e fa u lt:
sw itc h (m .a d d re ss ) {
Использование EnumSet вместо флагов 833
1 Проекты могут использоваться (например) в качестве тем для курсовых работ. Решения для
них не приводятся в сборнике.
Использование EnumSet вместо флагов 835
Конечные автоматы
Перечислимые типы идеально подходят для реализации конечных автоматов. Конеч
ный автомат обладает конечным числом состояний. В общем случае автомат переходит
между состояниями в зависимости от вводимых данных, но также возможны пере
ходные состояния, из которых автомат выходит сразу же после завершения их задач.
Для каждого состояния имеются допустимые сочетания входных действий, причем
разные входные действия переводят автомат в разные новые состояния. Так как
перечисления ограничивают набор допустимых вариантов, они хорошо подходят для
определения разиых состояний и входных действий.
С каждым состоянием также обычно связываются некоторые выходные данные.
Хорошим примером конечного автомата является торговый автомат. Сначала мы
определяем различные варианты ввода в перечислении:
//: e n u m e ra te d / In p u t.ja v a
p ackag e enu m erated ;
im p o rt ja v a .u til.* ;
p u b lic enum I n p u t {
N ICK EL(5), D IM E(10), QU ARTER(25), D O LLAR(100),
TOOTHPASTE(200), C H IPS(75), SO D A(100), SO AP(50),
ABORT_TRANSACTION {
p u b lic in t am ou nt() { / / З апрещ ен о
th ro w new R u n t i m e E x c e p t i o n ( " A B O R T . a m o u n t ( ) " ) ;
>
},
STOP { / / Э т о должен б ы ть п о с л е д н и й э к з е м п л я р ,
p u b lic in t am ount() { / / З апрещ ен о
th ro w new R u n t i m e E x c e p t i o n ( " S H U T _ D 0 W N . a m o u n t O ,,) ;
}
};
in t v a lu e ; // В центах
I n p u t(in t v a lu e ) { th is .v a lu e = v a lu e ; )
In pu t() {)
in t am ount() { return v a lu e ; }; // В цен тах
s ta tic Random r a n d = new R a n d o m ( 4 7 ) ;
p u b lic s ta tic Input ra n d o m S e le c tio n () {
/ / Не включая STO P:
return v a lu e s ( ) [ra n d .n e x tI n t(v a lu e s () .le n g th - 1)];
}
> ///:~
Обратите внимание: с двумя разновидностями i n p u t связана сумма, поэтому в ин
терфейсе определен метод a m o u n t ( ) . Однако вызов a m o u n t ( ) для двух других видов
l n p u t некорректен, поэтому при вызове a m o u n t ( ) они возбуждают исключения. Такая
и удобным в сопровождении:
//: e n u m e ra te d / V e n d in g M a c h in e . j a v a
// {Args: V e n d in g M a c h in e In p u t.tx t}
package enum erated;
import java.util.*;
im p o rt n e t.m in d v ie w .u til.* ;
im p o rt s ta tic e n u m e ra te d .In p u t.* ;
im p o rt s ta tic n e t.m in d v ie w .u til.P rin t.* ;
enum C a t e g o r y {
M O N E Y (N IC K EL , DIME, QUARTER, DOLLAR),
I T E M _ S E L E C T IO N ( T O O T H P A S T E , CHIPS, SODA, SOAP),
Q U I T _ T RANSACTI O N (A B ORT_TRA NSA C TION ),
SHUT_DOWN(STOP);
p riv a te In pu t[] v a lu e s ;
C a te g o ry (In p u t... types) { v a lu e s = ty p e s; }
p riv a te s ta tic E n u m M a p < I n p u t ,C a t e g o r y > c a te g o rie s =
new E n u m M a p < I n p u t , C a t e g o r y > ( I n p u t . c l a s s ) ;
s ta tic {
fo r(C a te g o ry c : C a te g o ry .c la s s .g e tE n u m C o n s ta n ts ())
fo r(In p u t type : c .v a lu e s )
c a te g o rie s .p u t(ty p e , c);
>
p u b lic s ta tic C ateg ory c a te g o riz e (In p u t in p u t) {
return c a te g o rie s .g e t( in p u t) ;
}
>
p u b lic cla s s V e n d in g M a c h in e {
p riv a te s ta tic S tate state = S ta te .R E S T IN G ;
p riv a te s ta tic in t am ount = 0 ;
p riv a te s ta tic Input s e le c tio n = n u ll;
enum S t a t e D u r a t i o n { T RA NSIENT } / / П е р е ч и с л е н и е д ля пометки
enum S t a t e {
RESTING {
v o id next(Inp ut in p u t) {
s w itc h (C a te g o ry .c a te g o riz e (in p u t)) {
c a s e MONEY:
am ou nt += i n p u t . a m o u n t ( ) ;
State = ADDING_MONEY;
break;
case SHUT_DOWN:
State = TER M INAL;
default:
>
}
b
ADDING_MONEY {
v o id n e x t(In p u t in p u t) {
s w itc h (C a te g o ry .c a te g o riz e (in p u t)) {
c a s e MONEY:
amount += i n p u t . a m o u n t ( ) ;
break;
case IT E M _ S E L E C T IO N :
se le c tio n = in p u t;
Использование EnumSet вместо флагов 837
// Для п р о стейш ей п р о в е р к и р а б о т о с п о с о б н о с т и :
c la s s R a n d o < n I n p u tG e n e ra to r i m p l e m e n t s G e n e r a t o r < I n p u t > {
p u b lic Input n e x t() { return In p u t.ra n d o m S e le ctio n (); >
>
// С о з д а н и е данных I n p u t по файлу с т р о к , р азд ел енн ы х символом ’; ' :
c la s s F ile In p u tG e n e ra to r im p l e m e n t s G e n e r a t o r < I n p u t > {
p riv a te Ite ra to r< S trin g > in p u t;
p u b lic F ile I n p u t G e n e r a t o r ( S t r in g file N a m e ) {
i n p u t = new T e x t F i l e ( f i l e N a m e , ";" ).ite ra to r();
>
p u b lic Input n e x t() {
if( !in p u t.h a s N e x tQ )
return n u ll;
return E n u m .v a lu e O f(In p u t.c la s s , in p u t.n e x t( ) .trim ( ) ) ;
}
} /* O u t p u t :
25
50
75
here i s y o u r CHIPS
0
100
200
here i s y o u r TOOTHPASTE
0
25
35
Your change: 35
0
25
35
I n s u ffic ie n t money f o r SODA
35
60
70
75
I n s u ffic ie n t money f o r SODA
75
Your change: 75
0
H a lte d
*///:~
Так как выбор между экземплярами перечисления чаще всего реализуется командой
s w i t c h (обратите внимание, как язык упрощает выбор вариантов по перечислению),
один из самых частых вопросов, которые следует задавать себе при упорядочении
нескольких перечислений: «По какому условию должен осуществляться выбор?»
В нашем примере для каждого состояния S t a t e переключение должно осуществлять
ся по базовым категориям входных действий: внесение денег, выбор товара, отмена
покупки, выключение автомата. Однако внутри этих категорий возможно внесение
разных валют и выбор разных товаров. Перечисление C a t e g o r y группирует разные
типы I n p u t , чтобы метод c a t e g o r i z e ( ) мог создать соответствующую категорию C a t e g o r y
в команде s w i t c h . Метод использует класс EnumMap для эффективного и безопасного
выполнения поиска.
Использование EnumSet вместо флагов 839
Изучив класс VendingMachine, вы увидите, чем состояния отличаются друг от друга и как
они реагируют на входные действия. Также обратите внимание на два переходных со
стояния; в состоянии run() машина ожидает ввода lnput и продолжает перемещаться
между состояниями, пока ее текущее состояние остается переходным.
Для тестирования VendingMachine можно воспользоваться двумя способами, с двумя
разными объектами Generator. RandomInputGenerator просто выдает случайные вари
анты ввода (кроме SHUT_DOWN). Работа этого генератора в течение продолжительного
времени поможет убедиться в том, что автомат не переходит в недопустимое состояние.
FilelnputG enerator получает файл с описанием ввода в текстовой форме, преобразует
его в экземпляры перечисления и создает объекты lnput.
Вот как выглядит текстовый файл для получения результатов, приведенных выше:
/ / : 1 e n u ro e ra te d / V e n d in g M a c h in e I n p u t.tx t
QUARTER; QUARTER; QUARTER; CHIPS;
DOLLAR; DOLLAR; TOOTHPASTE;
QUARTER; DIME; ABORT_TRANSACTION;
QUARTER; DIME; SODA;
QUARTER; DIME; NICKEL; SODA;
ABORT_TRANSACTIO N;
STOP;
/ / / :~
Одно из ограничений этой архитектуры заключается в том, что поля VendingMachine,
с которыми работают экземпляры перечисления State, должны быть статическими;
это означает, что экземпляр VendingMachine может быть только один. Это не такая
уж большая проблема, если рассмотреть фактическую реализацию, так как на одном
компьютере с большой вероятностью будет запускаться только одно приложение.
10. (7) Измените класс VendingMachine (только) с использованием EnumMap, чтобы в одной
программе можно было создать несколько экземпляров VendingMachine.
11. (7) В программном обеспечении настоящего торгового автомата нужно иметь воз
можность легко добавлять и менять тип продаваемых товаров, так что ограничения,
устанавливаемые перечислением для lnput, не практичны (не забудьте, что пере
числения предназначены для ограниченных наборов типов). Измените код Vend-
ingMachine.java так, чтобы продаваемые товары представлялись классом, а не были
частью lnput, и инициализируйте контейнер A rra y L ist этих объектов из текстового
файла (используйте n e t.m in d v ie w .u til.T e x tF ile ).
Проект1. Разработайте модель торгового автомата с поддержкой интернационализации,
чтобы один автомат мог быть легко приспособлен для всех стран.
Множественная диспетчеризация
При использовании нескольких взаимодействующих типов программа основательно
усложняется. Для примера возьмем систему для разбора и выполнения математиче
ских выражений. Было бы желательно использовать запись вида Number.plus(Number),
1 Проекты могут использоваться (например) в качестве тем для курсовых работ. Решения для
них здесь не приводятся.
840 Глава19 • Перечислимыетипы
//: en u m e ra te d /R o S h a m B o l.ja va
// Д е м о н с т р а ц и я м н о ж ес твен н о й д и с п е т ч е р и з а ц и и .
p ackage enum erated;
im p o rt ja v a .u til.* ;
im p o rt s ta tic e n u m e ra te d .O u tco m e .* ;
in te r fa c e Item {
Ou tco m e c o m p e t e ( I t e m it) ;
Ou tco m e e v a l ( P a p e r p ) ;
Ou tco m e e v a l ( S c i s s o r s s);
Ou tco m e e v a l ( R o c k r);
>
cla s s P a p e r im p le m e n ts Item {
p u b lic O u tco m e c o m p e t e ( I t e m it) { return it.e v a l( th is ); >
p u b lic O u tco m e e v a l ( P a p e r p ) { return DRAW; }
p u b lic Ou tco m e e v a l ( S c i s s o r s s) { r e t u r n WIN; >
p u b lic Outcom e e v a l ( R o c k r) { return LOSE; >
p u b lic S trin g to S trin g () { return "Paper"; >
}
1 Этот пример несколько лет просуществовал в реализациях на С++ и Java (в книге Thinking
in P a tte rn s) на сайте w w w .M in d V iew .n et, после чего появился без ссылки на источник в книге
других авторов.
Использование EnumSet вместо флагов 841
c la s s S c is s o rs im p le m e n ts Item {
p u b lic O u tc o m e com pete(Item it) { retu rn it.e v a l( th is ) ; >
p u b lic O u tc o m e e v a l(P a p e r p) { return LOSE; >
p u b lic O u tc o m e e v a l(S c is s o rs s) { retu rn DRAW; }
p u b lic O u tc o m e e v a l(R o c k r) { retu rn WIN; }
p u b lic S trin g to S trin g () { return " S c is s o rs " ; }
>
c la s s Rock im p le m e n ts Item {
p u b lic O u tc o m e com pete(Item it) { retu rn it.e v a l( th is ); }
p u b lic O u tc o m e e v a l ( P a p e r p) { retu rn WIN; }
p u b lic O u tc o m e e v a l(S c is s o rs s) { return LOSE; >
p u b lic O u tc o m e e v a l(R o c k r) { return DRAW; }
p u b lic S trin g to S trin g () { retu rn "R ock"; >
}
p u b lic cla s s RoSham Bol {
s ta tic fin a l in t SIZE = 20;
p riv a te s ta tic Random r a n d = new R a n d o m ( 4 7 ) ;
p u b lic s ta tic Item new Item () {
s w itc h (ra n d .n e x tI n t(3 )) {
d e fa u lt:
case 0: return new S c i s s o r s ( ) ;
case 1: return new P a p e r ( ) ;
case 2: return new R o c k ( ) ;
>
>
p u b lic s ta tic v o id m atch (Item a, Item b) {
System . o u t . p r i n t l n (
a + " vs. " + b + ": " + a .c o m p e te (b ));
}
p u b lic s ta tic v o id m a in (S trin g [] arg s) {
fo r( in t i = 0; i < SIZE; i+ + )
m atch(new Item (), new Item ());
>
} /* O u t p u t :
Rock v s . Rock: DRAW
Paper v s . Rock: WIN
Paper v s . Rock: WIN
Paper v s . Rock: WIN
S c is s o rs vs. Paper: WIN
S c is s o rs vs. S c is s o rs : DRAW
S c is s o rs vs. P aper: WIN
Rock v s . P aper: LOSE
Paper v s . Paper: DRAW
Rock v s . Paper: LOSE
Paper v s. S c is s o rs : LOSE
Paper v s . S c is s o rs : LOSE
Rock v s . S c is s o r s : WIN
Rock v s . Paper: LOSE
Paper v s . Rock: WIN
S c is s o rs vs. Paper: WIN
Paper v s . S c is s o rs : LOSE
Paper v s . S c is s o rs : LOSE
Paper v s . S c is s o rs : LOSE
Paper v s . S c is s o rs : LOSE
* // /:~
842 Глава 19 • Перечислимые типы
//: e n u m e ra te d /R o S h a m B o .ja v a
// Общий и н с т р у м е н т а р и й д л я пр и м ер о в RoShamBo.
p acka g e enu m erated;
im p o rt n e t.m in d v ie w .u til.* ;
p u b lic c la s s RoShamBo {
p u b lic s ta tic <T e x t e n d s C o m p e t i t o r < T > >
v o id m atch (T а , T b) {
S y s te m .o u t. p r i n t l n (
а + " vs. " + b + ": " + a .c o m p e te (b ));
>
p u b lic s ta tic <T e x t e n d s Enum<T> & C o m p e t i t o r < T > >
v o id p la y (C la s s< T > rs b C la s s , in t s iz e ) {
fo r( in t i = 0; i < s iz e ; i+ + )
m atch(
E n u m s. r a n d o m ( r s b C l a s s ) , E n u m s . r a n d o m ( r s b C l a s s ) ) ;
>
> ///:~
Метод play() не имеет возвращаемого значения, в котором задействован параметр-тиц т,
поэтому может показаться, что в типе ciass<T> можно использовать маски (wildcards)
вместо начального описания параметра. Однако маски не могут расширять более од
ного базового типа, поэтому приходится использовать приведенное выше выражение.
}
>
>>
ROCK {
p u b lic O u tc o m e c o m p e t e ( R o S h a m B o 3 it ) {
s w itc h ( it) {
d e fa u lt:
case PA PER : retu rn L O SE ;
case SCISSORS: return WIN;
case ROCK: retu rn DRAW;
>
>
>;
p u b lic ab stract O u tco m e com pete(RoSham Bo3 it) ;
p u b lic s ta tic v o id m a in ( S trin g [ ] args) {
R o S h a m B o .p la y (R o S h a m B o 3 .c la ss , 20);
>
} /* Same o u t p u t as R o S h am B o 2 .ja v a * // /:~
PAPER {
p u b lic O u tco m e co m p e t e ( R o S h a m B o 4 o p p o n e n t ) {
retu rn com pete(RO CK, opponen t);
>
>J
O u tc o m e co m p e t e ( R o S h a m B o 4 l o s e r , RoShamBo4 o p p o n e n t ) {
retu rn ((oppon ent == t h i s ) ? Outcom e.D RAW
: ((opponent == l o s e r ) ? O u tco m e .W I N
: O u tco m e .LO S E ));
>
p u b lic s ta tic v o id m a in ( S trin g [ ] arg s) {
R o S h a m B o .p la y ( R o S h a m B o 4 .c la s s , 20);
>
} / * Same o u t p u t as R o S h am B o 2 .ja v a * // /:~
Диспетчеризация с EnumMap
«Полноценную» двойную диспетчеризацию можно реализовать с использованием
класса EnumMap, который специально спроектирован для очень эффективной работы
с перечислениями. Так как целью является переключение по двум неизвестным типам,
карта EnumMap с элементами EnumMap может использоваться для достижения двойной
диспетчеризации:
//: e n u m e r a t e d / R o S h a m B o 5 .j a v a
// М нож ественная д и с п е т ч е р и з а ц и я на б а з е
// карты EnumMap с э л ем ен та м и EnumMap.
p ac ka g e enu m erated;
im p o rt ja v a .u til.* ;
im p o rt s ta tic en u m e ra te d .O u tco m e .*;
//: e n u m e ra te d / R o S h a m B o 6 .ja v a
// П еречи сл ен ия использую т "таблицы "
// в м е сто множ ественной д и с п е т ч е р и з а ц и и .
packag e en u m erated ;
im p o rt s ta tic e n u m e ra te d .O u tco m e .* ;
Резюме
Хотя перечислимые типы сами по себе не особенно сложны, я решил разместить эту
главу во второй половине книги из-за интересных возможностей, которые открыва
ются при использовании перечислений в сочетании с такими возможностями, как
полиморфизм, обобщения и отражения.
П еречисления^уа намного функциональнее перечислений С или С++, хотя они все
еще остаются «второстепенной» возможностью, без которой язык обходился (пусть и с
некоторыми неудобствами) в течение многих лет. Тем не менее в этой главе представ
лены некоторые важные применения этой «второстепенной» возможности — иногда
перечисления предоставляют все необходимое для элегантного, наглядного решения
задачи. А как я указывал, элегантность важна, а наглядность может определять различия
848 Глава19 • Перечислимыетипы
между успешным решением и решением, которое завершается неудачей из-за того, что
оно оказалось непонятным для других.
К сожалению, Bjava 1.0 термин «перечисление» (enumeration) использовался вместо
общепринятого термина «итератор» для обозначения объекта, выбирающего каждый
элемент последовательности (см. главу 17). С тех пор ошибка была исправлена, но
конечно, интерфейс E n u m e r a t i o n нельзя было просто убрать, поэтому он до сих пор про
должает встречаться в старом (а иногда и новом!) коде, библиотеках и документации.
Аннотации
1Джереми Мейер приехал в Crested Butte и провел две недели со мной, совместно работая над
этой главой. Его помощь была неоценимой.
2 Несомненно, эта аннотация была создана по образцу аналогичной возможности C# (хотя
в C# это ключевое слово, а не аннотация; таким образом, при переопределении метода в C#
необходимо использовать ключевое слово override, тогда K a K B ja v a аннотация @Override не
является обязательной).
850 Глава 20 • Аннотации
Базовый синтаксис
В следующем примере метод t e s t E x e c u t e ( ) снабжается аннотацией @ T e s t . Сама по себе
эта аннотация ничего не делает, но компилятор проследит за тем, чтобы определение
аннотации @ T e s t присутствовало в пути построения. Как будет показано позднее
в этой главе, вы можете написать программу, которая будет выполнять этот метод
через механизм отражения.
//: a n n o ta tio n s / T e s ta b le .ja v a
package a n n o ta tio n s ;
im p o rt n e t.m in d v ie w .a tu n it.* ;
p u b lic c la s s T e s ta b le {
p u b lic v o id ex ecu te() {
System . o u t . p r i n t l n ( " E x e c u t in g . . " ) ;
}
@ Test v o id testExecute() { ex e cu te(); >
> ///:~
Методы, снабженные аннотациями, не отличаются от других методов. Аннотация @ T e s t
в этом примере может использоваться в сочетании с другими модификаторами —та
кими, как p u b l i c , s t a t i c или v o i d . С точки зрения синтаксиса аннотации используются
почти так же, как модификаторы.
Определение аннотаций
Ниже приведено определение предыдущей аннотации. Как видите, определения анно
таций во многом похожи на определения интерфейсов. Более того, они компилируются
в файлы классов, как и интерфейсыЗауа:
Базовый синтаксис 851
//: n e t/ m in d v ie w / a tu n it/ T e s t. ja v a
// T h e @ T e s t t a g .
package n e t .n in d v ie w .a t u n it ;
im p o rt j a v a . l a n g . a n n o t a t i o n . * ;
0 T a r g e t ( E l e m e n t T y p e . METHOD)
§ R e te n tio n (R e te n tio n P o lic y .R U N T IM E )
p u b lic g in t e r fa c e Test {) ///:~
g T a r g e t ( E l e m e n t T y p e .M E T H O D )
0 R e te n tio n (R e te n tio n P 6 1 ic y .R U N T IM E )
p u b lic ^ in te rfa c e UseCase {
p u b lic in t id ( ) ;
p u b lic S trin g d e s c rip tio n () d e fa u lt "no d e s c r i p t i o n " ;
} ///:~
i
Обратите внимание: id и description напоминают объявления методов. Так как id
проходит- проверку типов, осуществляемую компилятором, это надежный механизм
связывания поисковой базы данных с документом, содержащим описания сценариев
использования, и исходным кодом. Элемент description имеет значение по умолчанию,
которое будет использовано обработчиком аннотаций в том случае, если при снабжении
метода аннотацией значение не указано.
Следующий класс содержит три метода, помеченных аннотацией gUseCase:
//: a n n o ta tio n s / P a s s w o rd U tils .ja v a
im p o rt j a v a . u t i l . * ;
Мета-аннотации
В настоящее время в языкеДауа определены всего три стандартные аннотации (см.
выше) и четыре мета-аннотации, предназначенные для пометки аннотаций.
p u b lic c la s s U seCaseTracker {
p u b lic s ta tic v o id
tra ck U se C a se s(L ist< In te g e r> useCases, C la ss< ? > c l) {
fo r(M e th o d m : c l.g e tD e c la re d M e th o d s ( )) {
UseCase uc = m . g e t A n n o t a t i o n ( U s e C a s e . c l a s s ) ;
if( u c != n u l l ) {
S y s te m .o u t.p rin tln (" F o u n d Use C a s e :'' + u c . i d ( ) +
" " + u c .d e s c r ip tio n () )j
u s e C a s e s . renrave(new I n t e g e r ( u c . i d ( ) ) ) ;
>
}
fo r( in t i : useCases) {
S y s te m .o u t.p rin tln ( " W a rn in g : M is s in g use case-" + i) ;
}
}
p u b lic s ta tic v o id m a in (S trin g [] arg s) {
L ist< In te g e r> useCases = new A r r a y L i s t < I n t e g e r > ( ) ;
C o lle c tio n s .a d d A ll(u s e C a s e s , 47, 48, 49, 50);
tra ckU seC a se s(u se C a ses, P a s s w o rd U tils .c la s s );
}
} /* O u t p u t :
Found U se C a s e : 4 7 Passw ords m ust c o n t a i n at le a s t one
n u m e ric
Found U s e C a s e : 4 8 no d e s c r i p t i o n
Found U s e C a s e : 4 9 New p a s s w o r d s c a n 't equal p re v io u s ly used
ones
W a rn in g : M is s in g use case-5 0
*// /:~
Элементы аннотаций
Метка guseCase, определенная в UseCase.java, содержит элемент id типа in t и элемент
d e s c rip tio n типа S trin g . Ниже перечислены допустимые типы элементов аннотаций.
□ Все примитивы ( in t, flo a t, boolean и т. д.).
□ String.
□ Class.
□ enum.
□ Annotation.
□ Массивы всех перечисленных типов.
При попытке использования любого другого типа компилятор сообщает об ошибке.
Использовать классы-«обертки» запрещено, по благодаря автоматической упаковке
это не является серьезным ограничением. Также элементы сами по себе могут быть
аннотациями. Как вы вскоре убедитесь, вложение аннотаций бывает очень полезным.
@ Таr g e t ( E l e m e n t T y p e . METHOD)
@ R e t e n t i o n ( R e t e n t i o n P o l i c y . RUNTIME)
p u b lic ^ in te rfa c e S im u la tin g N u ll {
p u b lic in t id ( ) d e fa u lt -1;
p u b lic S trin g d e s c rip tio n () d e fa u lt "";
> ///:~
@ T a rg e t(E le m e n tT y p e .T Y P E ) // A p p l i e s to c la s se s o n ly
@ R e te n tio n (R e te n tio n P o lic y .R U N T I M E )
p u b lic ^ in te rfa c e D B T a b le {
p u b lic S trin g n a m e () d e fa u lt "";
> / / / :~
Каждый аргумент E l e m e n t T y p e в аннотации @ T a r g e t устанавливает ограничение, которое
сообщает компилятору, что ваша аннотация может применяться только к этому кон
кретному типу. Вы можете указать одно значение из перечисления E l e m e n t T y p e или же
перечислить любую комбинацию значений, разделяя их запятыми. Чтобы аннотация
могла применяться к любому типу E l e m e n t T y p e , аннотацию @ T a r g e t можно опустить,
хотя
t
это встречается нечасто,
Обратите внимание: аннотация @ D B T a b l e содержит элемент n a m e ( ) , чтобы аннотация
могла предоставить имя таблицы базы данных, которая будет создана обработчиком.
Аннотации для полей JavaBean выглядят так:
//: a n n o t a t io n s / d a t a b a s e / C o n s t r a in t s . ja v a
package a n n o t a tio n s .d a t a b a s e ;
im p o rt ja v a .la n g .a n n o ta tio n .* ;
@ T a rg e t(E le m e n tT y p e .F IE L D )
0 R e t e n t i o n ( R e t e n t i o n P o l i c y . RUNTIME)
p u b lic @ in te rfa c e C o n s tra in ts {
b o o le a n p rim a ry K e y () d e fa u lt fa ls e ;
продолжение #
856 Глава 20 • Аннотации
//: a n n o ta tio n s / d a ta b a s e / S Q L In te g e r. ja v a
package a n n o ta tio n s .d a ta b a s e ;
im p o rt ja v a .la n g .a n n o ta tio n .* ;
@ T a rg e t(E le m e n tT y p e .F IE L D )
@ R e te n tio n (R e te n tio n P o lic y .R U N T I M E )
p u b lic @ in te rfa c e SQ LIn teger {
S trin g n a m e () d e fa u lt "";
C o n s tra in ts c o n s tra in ts ( ) d e f a u lt @ C o n s tra in ts ;
> I I I :~
Аннотация @ C o n s t r a i n t s позволяет обработчику получить метаданные, относящиеся
к таблице базы данных. Она представляет лишь небольшое подмножество ограничений,
обычно предоставляемых базами данных, но по крайней мере общее представление
о происходящем. Элементам p r i m a r y K e y ( ) , a l l o w N u l l ( ) и u n i q u e ( ) присваиваются разу
мные значения по умолчанию, так что в большинстве случаев пользователю аннотации
не придется вводить слишком много текста.
Двадругих объявления @interface определяют типы SQL. Чтобы эта структура реально
работала, необходимо определить аннотации для всех дополнительных типов SQL.
В нашем примере двух будет достаточно.
У каждого типа имеется элемент name () и элемент c o n s t r a i n t s (). Последний использует
механизм вложения аннотаций для включения информации об ограничениях типа
столбца. Обратите внимание: для элемента c o n t r a i n t s ( ) задано значение по умолчанию
@ C o n s t r a i n t s . Так как после типа аннотации значения элементов в круглых скобках не
указаны, значение по умолчанию для c o n s t r a i n t s ( ) в действительности представляет
собой аннотацию @ C o n s t r a i n t s с собственным набором значений по умолчанию. Чтобы
создать вложенную аннотацию @ C o n s t r a i n t s , у которой по умолчанию ограничение
уникальности истинно, можно определить элемент следующим образом:
//: a n n o ta tio n s / d a ta b a s e / U n iq u e n e s s . ja v a
// Пример влож енной а н н о т а ц и и
package a n n o ta tio n s .d a ta b a s e ;
p u b lic ^ in te rfa c e U n iq u e n e s s {
C o n s tra in ts c o n s tra in ts ()
d e fa u lt @ C o n s tra in ts (u n iq u e = tru e );
> ///:~
Написание обработчиков аннотаций 857
@ D B T a b le ( n a m e = "MEMBER")
p u b lic c la s s Member {
@ S Q L S trin g (3 0 ) S trin g fir s t N a m e ;
@ S Q L S trin g (5 0 ) S t r i n g .la stN a m e ;
@ SQ LInteger I n t e g e r age;
@ S Q L S trin g ( v a lu e = 30,
c o n s tra in ts = @ C o n s tra in ts (p rim a ry K e y = tru e))
S trin g h a n d le ;
s ta tic in t m em berCount;
p u b lic S trin g g e tH a n d le () { retu rn h a n d le ; )
p u b lic S trin g g e tF irs tN a m e () { retu rn firs tN a m e ; >
p u b lic S trin g g etLastN am e() { retu rn la s tN a m e ; )
p u b lic S trin g to S trin g () { return h a n d le ; }
p u b lic In teg er g etA ge() { retu rn age; }
} ///:~
Аннотации класса @овтаЬ1е задано значение «MEMBER», которое будет использовано
в качестве имени таблицы. Свойства f i r s t N a m e и l a s t N a m e снабжаются аннотациями
@ S Q L S t r i n g и получают значения 30 и 50 соответственно. Эти аннотации интересны по
Обработчик использует это значение для задания размера создаваемого столбца SQL.
Синтаксис значения по умолчанию удобен, но он быстро усложняется. Взгляните на
аннотацию поля h a n d l e . В нем используется аннотация @ S Q L S t r i n g , но это поле также
должно быть первичным ключом базы данных, поэтому для вложенной аннотации
@ C o n s t r a i n t должен быть установлен тип элемента p r i m a r y K e y . Здесь-то и начинаются
проблемы. Теперь для вложенной аннотации приходится использовать длинную форму
с парой «ключ-значение», с повторным указанием имени элементамимени @ i n t e r f a c e .
Но так как элемент со специальным именем v a l u e уже не является единственным за
данным элементом, воспользоваться сокращенной записью не удастся. Как видите,
результат выглядит некрасиво.
Альтернативные решения
Также существуют другие способы создания аннотаций для этой задачи. Например,
можно создать один класс аннотации с именем @ T a b l e C o l u m n , содержащий элемент-пере
числение, определяющий значения S T R I N G , I N T E G E R , F L O A T и т. д. Тем самым устраняется
858 Глава 20 • Аннотации
необходимость в объявлении ^interface для каждого типа SQL, но зато теряется воз
можность уточнения типов дополнительными элементами (размер, точность и т. д.) —
что, пожалуй, полезнее.
Также можно воспользоваться элементом S t r i n g для описания фактического типа
SQL —например, <<VARCHAR(30)* или «INTEGER». Это позволитуточнять типы, но
отображение r a n o B j a v a на типы SQL привязывается к вашему коду, что нежелательно
с точки зрения архитектуры. Вряд ли вам захочется перекомпилировать классы при
изменении базы данных; было бы элегантнее просто сообщить обработчику аннота
ций, что вы используете другую «разновидность» SQL, чтобы этот факт был учтен
при обработке аннотаций.
В третьем возможном решении поле помечается сразу двумя типами аннотаций, C on
stra in ts и соответствующего типа SQL (например, 0 S Q L l n t e g e r ) . Результат выглядит
немного неряшливо, но компилятор позволяет назначить для цели аннотации сколько
угодно разных аннотаций. Учтите, что при использовании нескольких аннотаций одна
аннотация не может быть использована дважды.
Реализация обработчика
Ниже приведен пример обработчика аннотаций, который читает файл класса, проверяет
его на аннотации базы данных и генерирует команду SQL для создания базы данных:
//: a n n o ta tio n s / d a ta b a s e / T a b le C re a to r.ja v a
// О бработчик аннотаций с испо льзо ван ием отражения.
// {Args: a n n o ta tio n s .d a ta b a s e .M e m b e r}
package a n n o ta tio n s .d a ta b a s e ;
im p o rt ja v a .la n g .a n n o ta tio n .* ;
im p o rt ja v a .la n g .r e fle c t.* ;
im p o rt ja v a .u til.* ;
p u b lic c la s s T a b le C re a to r {
p u b lic s ta tic v o id m a in (S trin g [] arg s) th row s E x c e p tio n {
if( a rg s .le n g th < 1) {
S y s te m .o u t.p rin tln ("a rg u m e n ts : an n otated c la s se s");
S y s te m .e x it( 0 ) ;
}
fo r( S trin g classN a m e : arg s) {
C la ss< ? > cl = C la s s .fo rN a m e (c la s s N a m e );
D B T a b le d b T a b le = c l.g e tA n n o ta tio n (D B T a b le .c la s s );
Написание обработчиков аннотаций 859
if( d b T a b le == n u l l ) {
S ystem . o u t . p r i n t l n (
"No D B T a b l e a n n o ta tio n s in c la s s " + classN a m e );
c o n tin u e ;
>
S trin g ta b leN am e = d b T a b le .n a m e ( ) ;
// Е с л и имя не у к а з а н о , использовать имя C l a s s :
if(ta b le N a m e .le n g th () < 1)
ta b leN a m e = c l.g e t N a m e ( ) .t o U p p e r C a s e ( ) ;
L is t< S trin g > co lu m n D e fs = new A r r a y L i s t < S t r i n g > ( ) ;
fo r( F ie ld fie ld : c l.g e tD e c la re d F ie ld s ()) {
S trin g colum nNam e = n u l l ;
A n n o ta tio n [] anns = fie ld .g e tD e c la r e d A n n o ta tio n s ( );
if( a n n s .le n g th < 1)
c o n tin u e ; // Не я в л я е т с я с т о л б ц о м та б л и ц ы б а з ы д ан н ы х
if( a n n s [ 0 ] in s t a n c e o f S Q L ln te g e r) {
S Q LIn teger sInt = (S Q L ln te g e r) anns[0];
// И с п о л ь з о в а т ь имя п о л я , если имя н е у к а з а н о
if( s I n t.n a m e ( ) .le n g th ( ) < 1)
colum nNam e = f i e l d . g e t N a m e ( ) . t o U p p e r C a s e ( ) ;
e ls e
colum nNam e = s I n t . n a m e ( ) ;
co lu m n D e fs .a d d (c o lu m n N a m e + " IN T" +
g etC o n st r a i n t s ( s I n t . c o n s t r a in t s ( ) ) ) ;
}
if( a n n s [ 0 ] in s t a n c e o f S Q L S trin g ) {
S Q L S trin g s S trin g = (S Q L S trin g ) anns[0];
// И с п о л ь з о в а т ь имя п о л я , если имя не у к а з а н о
if( s S tr in g .n a m e () .le n g th ( ) < 1)
co lu m n N am e = f i e l d . g e t N a m e ( ) . t o U p p e r C a s e ( ) ;
e ls e
co lu m n N a m e = s S t r i n g . n a m e ( ) ;
c o l u m n D e f s . a d d ( c o l u m n N a m e + " V A R C H A R (" +
s S trin g .v a lu e () + ")” +
g e tC o n s tra in ts (s S tr in g . c o n s t r a in t s ( ) ));
>
S trin g B u ild e r c r e a t e C o m m a n d = new S t r i n g B u i l d e r (
"C R E A T E T A B L E " + ta b le N a m e + "(");
fo r( S trin g co lu m n D e f : c o lu m n D e fs )
cre a te C o m m a n d .a p p e n d ("\ n " + co lu m n D e f + " , " ) ;
// У д а л и т ь завершающую з а п я т у ю
S trin g ta b le C re a te = c re a te C o m m a n d .s u b s trin g (
0, c re a te C o m m a n d .le n g th () - 1) + ");";
S y s te m .o u t.p rin tln ( " T a b le C re a tio n SQL f o r " +
cla ssN a m e + " is :\n " + ta b le C re a te );
}
>
>
p riv a te s ta tic S trin g g e tC o n s tra in ts (C o n s tra in ts con) {
S trin g c o n s tra in ts = "";
if( !c o n .a llo w N u ll( ) )
c o n s tra in ts += " NOT N U L L " ;
if(c o n .p rim a ry K e y ())
c o n s tra in ts += " PRIMARY K E Y " ;
if( c o n .u n iq u e ( ) )
c o n s tra in ts += " U N I Q U E " ;
return c o n s tra in ts ;
} продолжение ^>
860 Глава 20 • Аннотации
> /* O u t p u t :
T a b le C re a tio n SQL f o r a n n o ta tio n s .d a ta b a s e .M e m b e r is :
C REA TE T A B L E MEMBER(
FIRSTNAME V A R C H A R ( 3 0 ) ) ;
T a b le C re a tio n SQL f o r a n n o ta tio n s .d a ta b a s e .M e m b e r is :
C REA TE T A B L E MEMBER(
FIRSTNAME V A R C H A R ( 3 0 ) ,
LASTNAME VARCHAR(50));
T a b le C re a tio n SQ L f o r a n n o ta tio n s .d a ta b a s e .M e m b e r is :
C REATE T A B L E MEMBER(
FIRSTNAME V A R C H A R ( 3 0 ) ,
LASTNAME V A R C H A R ( 5 0 ) ,
AGE I N T ) ;
T a b le C re a tio n SQL f o r a n n o ta tio n s .d a ta b a s e .M e m b e r is :
C R EA T E T A B L E MEMBER(
FIRSTNAME V A R C H A R ( 3 0 ) ,
LASTNAME VARCHAR(50)j
AGE INT,
HANDLE V A R CH AR (3 0 ) PRIMARY K E Y ) ;
* / / / :~
Метод m a i n ( ) перебирает имена классов, заданные в командной строке. Каждый класс
загружается методом f o r N a m e ( ) и проверяется на наличие аннотации @ D B T a b l e мето
дом g e t A n n o t a t i o n ( D B T a b l e . c l a s s ) . Если аннотация присутствует, программа находит
и сохраняет имя таблицы. Затем все поля класса загружаются и проверяются методом
g e t D e c l a r e d A n n o t a t i o n s ( ) . Этот метод возвращает массив всех аннотаций, определен
5 Проекты могут использоваться (например) в качестве тем для курсовых работ. Решения для
них здесь не приводятся.
Написание обработчиков аннотаций 861
продолжение A>
1 Впрочем, при использовании нестандартного ключа -XclassesAsDecls можно работать с анно
тациями из откомпилированных классов.
2 npoeKTHpoBniHKHjava намекают, что зеркало - то, в чем можно найти отражение.
862 Глава 20 • Аннотации
@Т а r g e t ( E l e m e n t T y p e . T Y P E )
@ R e t e n t i o n ( R e t e n t i o n P o l i c y . SOURCE)
p u b lic g > in te rfa c e E x tra ctIn te rfa ce {
p u b lic S trin g v a lu e ();
} ///: ~
В аргументе R e t e n t i o n P o l i c y выбрано значение SO URCE, потому что сохранять эту анно
тацию в файле класса после выделения интерфейса бессмысленно. Следующий класс
предоставляет открытый метод, который может стать частью полезного интерфейса:
//: a n n o ta tio n s / M u ltip lie r.ja v a
/ / О б р а б о т к а а н н о т а ц и й на б а з е A P T .
package a n n o ta tio n s ;
F iler вместо обычного PrintWriter заключается в том, что он позволяет apt отслежи
вать все создаваемые новые файлы, чтобы их можно было проверить на аннотации
и откомпилировать при необходимости.
Также можно заметить, что метод createSourceFile() открывает обычный выходной
поток с правильным именем для класса или интерфейса^уа. Поддержка создания
языковых конструкций Java отсутствует, так что исходный KOflJava приходится гене
рировать с использованием примитивных методов print() и println(). А это значит, что
вы должны следить за парными скобками и синтаксической корректностью своего кода.
Метод process() вызывается программой apt, которой необходима фабрика, предо
ставляющая нужный обработчик:
//: annotations/InterfaceExtractorProcessorFactory.java
// Обработка аннотаций на базе APT.
package annotations;
import com.sun.mirror.apt.*;
import com.sun.mirror.declaration.*;
import java.util.*;
package annotations;
public interface IMultiplier {
public int multiply (int x, int у);
>
Этот файл также будет откомпилирован apt, поэтому вы найдете файл IMultiplier.class
в том же каталоге.
2 . (3) Добавьте поддержку деления в программу извлечения интерфейса.
Set<AnnotationTypeDeclaration> atds,
AnnotationProcessorEnvironment env) {
return new TableCreationProcessor(env);
}
public Collection<String> supportedAnnotationTypes() {
return Arrays.asList(
"annotations.database.DBTable",
"annotations.database.Constraints",
"annotations.database.SQLString" ,
"annotations.database.SQLInteger");
}
public Collection<String> supportedOptions() {
return Collections.emptySet();
}
private static class TableCreationProcessor
implements AnnotationProcessor {
private final AnnotationProcessorEnvironment env;
private String sql = "";
public TableCreationProcessor(
AnnotationProcessorEnvironment env) {
this.env - envj
>
public void process() {
for(TypeDeclaration typeDecl :
env.getSpecifiedTypeDeclarations()) {
typeDecl.accept(getDeclarationScanner(
new TableCreationVisitor(), NO_OP))j
sql = sql.substring(0, sql.length() - 1) + ");";
System.out.println("creation SQL is :\n" + sql);
sql = ""j
>
}
private class TableCreationVisitor
extends SimpleDeclarationVisitor {
public void visitClassDeclaration(
ClassDeclaration d) {
DBTable dbTable = d.getAnnotation(DBTable.class)
if(dbTable != null) {
sql += "CREATE TABLE ";
sql += (dbTable.name().length() < 1)
? d.getSimpleName().toUpperCase()
: dbTable.name();
sql += " (";
>
>
public void visitFieldDeclaration(
FieldDeclaration d) {
String columnName = "";
if(d.getAnnotation(SQLInteger.class) != null) {
SQLInteger sInt = d.getAnnotation(
SQLInteger.class);
// Использовать имя поля, если имя не указано
if(sInt.name().length() < 1)
columnName = d.getSimpleName().toUpperCase()
else
columnName = sInt.name()j
sql += "\n " + columnName + " INT" +
getConstraints(sInt.constraints()) + ",";
}
Использование паттерна «Посетитель» с apt 867
if(d.getAnnotation(SQLString.class) != null) {
SQLString sString = d.getAnnotation(
SQLString.class);
// Use field name if name not specified.
if(sString.name().length() < 1)
columnName = d.getSimpleName().toUpperCase();
else
columnName = sStning.name();
sql += "\n w + columnName + " VARCHAR(" +
sString.value() + ”)" +
getConstraints(sString.constraints()) + ",";
>
>
private String getConstraints(Constraints con) {
String constraints = "";
if(!соп.allowNull())
constraints += ” NOT NULL";
if(con.primaryKey())
constraints += " PRIMARY KEY";
if(con.unique())
constraints += " UNIQUE";
return constraints;
>
>
>
> I I I :~
Результат выглядит так же, как в предыдущем примере DBTable.
Обработчик и посетитель в этом примере представлены внутренними классами. Обра
тите внимание: метод process() только добавляет класс посетителя и инициализирует
строку SQL.
Оба параметра getDeclarationScanner() определяют посетителей; первый исдоль-
зуется перед посещением каждого объявления, а второй — после. Этому обработ
чику нужен только первый посетитель, поэтому во втором параметре передается
NO_OP — статическое поле интерфейса DeclarationVisitor для посетителя, который
не делает ничего.
Класс TableCreationVisitor расширяет SimpleDeclarationVisitor, переопределяя два
метода, visitClassDeclaration() и visitFieldDeclaration().SimpleDeclarationVisitor —
адаптер, реализующий все методы интерфейса DeclarationVisitor, так что вы можете
сосредоточиться на тех методах, которые вам нужны. В методе visitClassDeclaration()
объект classDeclaration проверяется на аннотацию DBTable, и если она присутствует,
инициализируется первая часть строки SQL. В методе visitFieldDeclaration() объ
явление поля проверяется на аннотации полей, а информация извлекается таким же
способом, как в примере, приведенном ранее в этой главе.
На первый взгляд такая схема выглядит излишне сложной, но такое решение лучше
масштабируется. Если сложность обработчика аннотаций возрастет, написание авто
номного обработчика (как в предыдущем примере) также станет весьма непростым
делом.
Использование аннотаций
при модульном тестировании
Модульным тестированием (unit testing) называется практика создания тестов для
каждого метода класса для регулярного тестирования компонентов класса на правиль
ность поведения. Самый популярный инструмент модульного тестирования д л я ^ у а
Ha3biBaeTcnJUnit; на момент написания книги cHcreMaJUnit находилась в процессе
обновления до версии 4, в которой должны были появиться аннотации1. Одной из
главных проблем BepcHftJUni^o появления аннотаций был объем подготовительного
кода, необходимого для создания и выполнения TecTOBjUnit. Со временем он сокра
тился, но поддержка аннотаций становится шагом на пути к заданной цели — «самой
простой системе модульного тестирования, которая работает».
В версиях JU nit до появления аннотаций для хранения модульных тестов приходи
лось создавать отдельный класс. С аннотациями модульные тесты можно включить
в тестируемый класс, что позволяет свести к минимуму затраты времени и сил на
модульное тестирование. У такого подхода есть дополнительное преимущество: он
позволяет тестировать закрытые методы так же легко, как и открытые.
Так как эта среда тестирования базируется на аннотациях, она называется @Unit.
Простейшая форма тестирования (с которой вам, вероятно, в основном придется
иметь дело) использует аннотацию @Test для пометки тестируемых методов. Одна из
разновидностей тестовых методов не получает аргументов и возвращает логический
признак успеха или неудачи. Вы можете присваивать тестовым методам любые имена
по своему усмотрению. Кроме того, тестовые методы @Unit могут обладать любым
уровнем доступа, включая private.
Чтобы использовать @Unit, достаточно импортировать библиотеку net.mindview.atunit12,
пометить соответствующие методы и поля маркерами @Unit (о которых вы больше
узнаете в следующих примерах), после чего заставить вашу систему построения вы
полнить @Unit для полученного класса. Простой пример:
//: annotations/AtUnitExamplel.java
package annotations;
import net.mindview.atunit.*;
import net.mindview.util.*;
. m3
. failureTest (failed)
. anotherDisappointment (failed)
(5 tests)
OK (2 tests)
*///:~
OK (2 tests)
*///:~
Для каждого теста создается новый объект testObject, так как для каждого теста соз
дается объект AtUnitComposition.
В отличие O T j U n i t здесь нет специальных методов assert, но вторая форма @Test по
зволяет вернуть void (или boolean, если вы предпочитаете возвращать true или false).
Для проверки успешного выполнения можно использовать командь^ауа assert. Обыч
но они должны включаться ключом ^ a командной строки java, но @ U n i t включает их
автоматически. Для обозначения неудачи даже можно воспользоваться исключением.
Одной из целей проектирования @ U n i t был минимальный объем дополнительного
Использование аннотаций при модульном тестировании 871
(4 tests)
import net.mindview.util.*;
OK (3 tests)
*///:~
. scramble2 'brontosauruses'
tsaeborornussu
. words 'are'
OK (3 tests)
*///:~
Использование аннотаций при модульном тестировании 875
Running cleanup
. test2
Running cleanup
. test3
Running cleanup
OK (3 tests)
*///:~
assert top().equals("B");
}
public static void main(String[] args) throws Exception {
OSExecute.command(
"java net.mindview.atunit.AtUnit StackLStringTest");
>
> /* Output:
annotations.StackLStringTest
. _push
. _pop
• _top
OK (3 tests)
*///:~
«Семейства» не нужны
У @Unit есть одно важное преимущество nepeAjUnit: в @Unit не нужны «семейства»
(suites). B JU n it необходимо каким-то образом сообщить программе, что же следует
протестировать; так появилось понятие «семейства» для группировки тестов, чтобы
nporpaMMaJUnit могла найти их и выполнить тесты.
@Unit просто ищет файлы классов, содержащие соответствующие аннотации, после
чего выполняет методы @Test. Работая над системой тестирования @Unit, я стремился
сделать ее максимально прозрачной, чтобы люди могли начать пользоваться ею, просто
добавляя методы @Test, без дополнительного кода или знаний, нeoбxoдимыxдляJUnit
и многих других сред модульного тестирования. Тесты писать и так непросто, поэтому
@Unit старается по возможности упростить эту работу —чтобы повысить вероятность
того, что программист действительно напишет положенные тесты.
Реализация @Unit
Сначала необходимо определить все типы аннотаций. Они представляют собой про
стые метки без полей. Аннотация @Test была определена в начале главы, а остальные
аннотации выглядят так:
//: net/mindview/atunit/TestObjectCreate.java
// Аннотация @Unit @TestObjectCreate.
package net.mindview.atunit;
import java.lang.annotation.*;
п р о д о л ж ен и е &
878 Глава 20 • Аннотации
@Т а rget(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public ginterface TestObjectCreate {} ///:~
//: net/mindview/atunit/TestObjectCleanup.java
// Аннотация @Unit 0TestObjectCleanup.
package net.mindview.atunit;
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public ^interface TestObjectCleanup {> ///:~
//: net/mindview/atunit/TestProperty.java
// Аннотация @Unit @TestProperty.
package net.mindview.atunit;
im0ort java.lang.annotation.*;
Для всех тестов задан режим RetentionPolicy.RUNTIME, потому что система @Unit должна
распознать тесты в откомпилированном коде.
Чтобы реализовать систему выполнения тестов, мы воспользуемся отражением для
извлечения аннотаций. Наосновании полученной информации программа решает, как
следует строить тестовые объекты и выполнять тесты. С аннотациями код получается
на удивление компактным и прямолинейным:
//: net/mindview/atunit/AtUnit.java
// Среда модульного тестирования на базе аннотаций.
// {RunByHand}
package net.mindview.atunit;
import java.lang.reflect.*;
import java.io.*;
import java.util.*;
import net.mindview.util.*;
import static net.mindview.util.Print.*;
public class AtUnit implements ProcessFiles.Strategy {
static Class<?> testClass;
static List<String> failedTests= new ArrayList<String>();
static long testsRun = 0;
static long failures = 0;
public static void main(String[] args) throws Exception {
ClassLoader.getSystemClassLoader()
.setDefaultAssertionStatus(true); // Включить assert
new ProcessFiles(new AtUnit(), "class").start(args);
if(failures == 0)
print("OK (" + testsRun + " tests)");
else {
print("(" + testsRun + " tests)");
print("\n>>> " + failures + " FAILURE" +
(failures > 1 ? "S" ; "") + " <<<");
for(String failed : failedTests)
print(" " + failed);
>
>
Использование аннотаций при модульном тестировании 879
} catch(Exception e) {
throw new RuntimeException(e);
>
>
}
static class TestMethods extends ArrayList<Method> {
void addIfTestMethod(Method m) {
if(m.getAnnotation(Test.class) == null)
return;
if(!(m.getReturnType().equals(boolean.class) ||
m .getReturnType().equals(void.class)))
throw new RuntimeException("@Test method" +
" must return boolean or void");
m.setAccessible(true); // На случай, если метод закрытый и т.д.
add(m);
>
>
private static Method checkForCreatorMethod(Method m) {
if(m.getAnnotation(TestObjectCreate.class) == null)
return null;
if(!m.getReturnType().equals(testClass))
throw new RuntimeException("@TestObjectCreate " +
"must return instance of Class to be tested");
if((m.getModifiers() &
java.lang.reflect.Modifier.STATIC) < 1)
throw new RuntimeException("@TestObjectCreate " +
"must be static.");
m.setAccessible(true);
return m;
}
private static Method checkForCleanupMethod(Method m) {
if(m.getAnnotation(TestObjectCleanup.class) == null)
return null;
if(!m.getReturnType().equals(void.class))
throw new RuntimeException("@TestObjectCleanup " +
"must return void");
if((m.getModifiers() &
java.lang.reflect.Modifier.STATIC) < 1)
throw new RuntimeException("@TestObjectCleanup ” +
"must be static.'');
if(m.getParameterTypes().length == 0 ||
m.getParameterTypes()[0] != testClass)
throw new RuntimeException(”@TestObjectCleanup " +
"must take an argument of the tested type.");
m.setAccessible(true);
return m;
>
private static Object createTestObject(Method creator) {
if(creator != null) {
try {
return creator.invoke(testClass);
} catch(Exception e) {
throw new RuntimeException("Couldn't run " +
"@TestObject (creator) method.'');
>
> else { // Использовать конструктор по умолчанию:
try {
return testClass.newInstance();
Использование аннотаций при модульном тестировании 881
> catch(Exception e) {
throw new RuntimeException("Couldn't create а " +
"test object. Try using а @TestObject method.")j
>
>
}
} U /:~
>
>
}
// В этой версии поля не удаляются (см. текст),
if(modified)
ctClass.toBytecode(new DataOutputStream(
new FileOutputStream(cFile)));
ctClass.detach();
) catch(Exception e) {
throw new RuntimeException(e)j
>
>
> / / /:~
ClassPool — своего рода «картинка» всех модифицируемых классов в системе. Про
цесс извлечения объектов CtClass из ClassPool напоминает загрузку классов BjVM
с использованием загрузчика классов и Class.forName().
Объект CtClass содержит байт-код объекта класса; он позволяет получить информацию
о классе и манипулировать с кодом этого класса. Здесь мы вызываем getDeclared-
Methods() (по аналогии с механизмом отраж ения^уа) и получаем объект Methodlnfo
для каждого объекта ctMethod. Теперь можно переходить к просмотру аннотаций. Если
метод содержит аннотацию из пакета net.mindview.atunit, то этот метод удаляется.
Если класс был модифицирован, то исходный файл класса заменяется новым классом.
На момент написания книги функциональность «удаления» была только добавлена
в Javassist\ и мы обнаружили, что удаление полей @TestProperty оказывается более
сложной задачей, чем удаление методов. На эти поля могут ссылаться статические
операции инициализации, поэтому их нельзя просто удалить. Приведенная выше
версия кода удаляет только методы @Unit. Впрочем, следите за обновлениями на сайте
Javassist; возможно, удаление полей со временем станет возможным. А пока следует
заметить, что метод внешнего тестирования, продемонстрированный в AtUnitExtern-
afTestjava, позволяет удалить все тесты простым удалением файла класса, созданного
тестовым кодом.
Резюме
Аннотации стали одним из полезных нововведений в Java. Это структурированный,
обеспечивающий проверку типов механизм добавления метаданных, который не за
громождает код и не затрудняет его чтение. Аннотации помогают избавиться от рутины,
связанной с написанием дескрипторов развертывания и других генерируемых файлов.
Замена TeraJavadoc @deprecated аннотацией @Deprecated — лишь один из признаков
того, насколько лучше аннотации подходят для передачи информации о классах, чем
комментарии.
В nocTaBKyJava SE5 включен лишь небольшой набор аннотаций. Это означает, что
если найти нужную библиотеку не удастся, вам придется самостоятельно создавать1
1Доктор Сигэру Чиба очень любезно добавил метод CtClass.removeMethod () по нашей просьбе.
886 Глава20 • Аннотации
аннотации и связанную с ними логику. При помощи программы apt вы можете автома
тически откомпилировать только что сгенерированные файлы; это упрощает процесс
построения, но в настоящее время mirror API фактически содержит лишь базовую
функциональность, которая помогает идентифицировать элементы определений
KnaccoBjava. Как было показано выше, манипуляции с байт-кодом можно запрограм
мировать вручную или же воспользоваться библиотекой Javassist.
Безусловно, со временем ситуация улучшится, и поставщики API и фреймворков
начнут включать аннотации в свои инструментарии. Как нетрудно представить по
системе @Unit, скорее всего, аннотации окажут значительное влияние на практику
программирования HaJava.
Параллельное
выполнение
Многогранная параллельность
Главная причина сложностей с изучением параллельного программирования заклю
чается в том, что оно применяется для решения самых разнообразных задач и реали
зуется разными методами, поэтому между разными случаями не существует четкого
соответствия. В результате вам приходится разбираться во всех нюансах и частных
случаях, чтобы эффективно использовать параллельное программирование.
Задачи, решаемые при помощи параллельного программирования, можно условно
разделить на категории «скорости» и «управляемости структуры».
Ускорение выполнения
Со скоростью на первый взгляд дело обстоит просто: если вы хотите, чтобы программа
выполнялась быстрее, разбейте ее на части и запустите каждую часть на отдельном
процессоре. Параллельность является основным инструментом многопроцессорного
программирования. В наши дни, когда закон Мура постепенно перестает действовать
(по крайней мере для традиционных микросхем), ускорение проявляется в форме мно
гоядерных процессоров, нежели в ускорении отдельных чипов. Чтобы ваша программа
работала быстрее, вы должны научиться эффективно использовать дополнительные
процессоры, и это одна из возможностей, которую параллельное программирование
может вам предоставить.
На многопроцессорном компьютере задачи могут распределяться по разным процес
сорам, что приводит к радикальному возрастанию скорости. Это явление характерно
для мощных многопроцессорных веб-серверов, которые могут распределять большое
количество пользовательских запросов по разным процессорам в программе, назна
чающей отдельный поток для каждого запроса.
Однако параллельность часто повышает производительность программ, выполняю
щихся на одном процессоре.
На первый взгляд это нелогично. Если задуматься, выполнение многопоточной
программы на одном процессоре неизбежно должно сопровождаться повышенными
затратами ресурсов по сравнению с последовательным выполнением всех частей про
граммы, из-за так называемых переключений контекста (перехода от одной задачи
к другой). Казалось бы, эффективнее выполнить все части программы как одну задачу
и избавиться от затрат на переключение контекста.
Однако при этом необходимо учитывать проблему блокирования. Если одна задача
в программе не может продолжать выполнение из-за какого-то условия, неподкон
трольного программе (обычно ввода-вывода), говорят, что эта задача или программный
поток блокируется (blocks). Традиционная программа останавливается до изменения
внешнего условия. Однако в программе, написанной с учетом параллельности, во
время блокировки одной задачи могут продолжать выполняться другие задачи, так
что программа не будет простаивать. Итак, с точки зрения производительности па
раллельность на однопроцессорной машине имеет смысл только в том случае, если
некоторые задачи могут блокироваться.
890 Глава21 • Параллельноевыполнение
1 Например, Эрик Рэймонд настоятельно продвигает эту точку зрения в книге «The Art of UNIX
Programming» (Addison-Wesley, 2004).
2 Иногда высказывается мнение, что попытки «пристегнуть» параллелизм к последовательному
языку обречены на неудачу, но вы должны составить собственное мнение.
3 Вообще-то это требование так и не было реализовано в полной мере, поэтому компания Sun уже
не так громко продвигает этот лозунг. Парадоксально, но одна из причин, по которой принцип
«написано однажды - работает везде» не сработал в полной мере, была связана с проблемами
в системе потокового выполнения, которые, возможно, будут исправлены eJava SE5.
892 Глава 21 • Параллельное выполнение
Определение задач
Поток выполняет некоторую задачу, поэтому вам нужны средства для описания таких
задач. Эти средства предоставляются интерфейсом Runnable. Чтобы определить зада
чу, просто реализуйте Runnable и напишите метод run(), который выполнит нужные
действия.
Например, следующая задача LiftOff выводит обратный отсчет перед запуском:
//: concurrency/LiftOff.java
// Demonstration of the Runnable interface.
this.countDown = countDown;
>
public String status() {
return ?'#" + id + "(" +
(countDown > 0 ? countDown : "Liftoff!") + " ) , ";
>
public void run() {
while(countDown-- > 0) {
System.out.print(status());
Thread.yield();
>
>
> ///:~
Идентификатор id различает экземпляры задачи. Он объявлен с ключевым словом
final, потому что значение не должно изменяться после инициализации.
Метод run() практически всегда являет собой некоторый цикл, который выполняется,
пока поток еще нужен, поэтому вам придется определить условие выхода из такого
цикла (можно просто использовать команду return, как сделано в рассматриваемой
программе). Зачастую метод run() реализуется в форме бесконечного цикла; это значит,
что завершение потока осуществляется с помощью какого-либо внешнего фактора или
он будет выполняться бесконечно (чуть позже в этой главе вы узнаете, как безопасно
сигнализировать потоку, чтобы он «остановился»).
Вызов статического метода Thread.yield() внутри run() является рекомендацией для
планировщика потоков (подсистема механизма потоков Java, которая переключает
процессор с одного потока на другой). По сути она означает: «Важная часть моего
цикла выполнена, и сейчас было бы неплохо переключиться на другую задачу». Этот
вызов полностью необязателен, но он используется здесь, потому что с ним результат
получается более интересным: вы с большей вероятностью увидите доказательства
переключения потоков.
В следующем примере вызов run() не управляется отдельным потоком; метод просто
напрямую вызывается в main() (поток, конечно, при этом используется: тот, который
всегда создается для main()):
//: concurrency/MainThread.java
Класс Thread
Традиционный способ преобразования объекта Runnable в выполняемую задачу основан
на передаче его конструктору Thread. Следующий пример показывает, как организовать
выполнение объекта Liftoff с использованием Thread:
//: concurrency/BasicThreads.java
// Простейшее использование класса Thread.
Конструктору Thread необходим только объект Runnable. Метод start() объекта Thread
выполняет необходимую инициализацию потока, после чего вызывает метод run()
объекта Runnable для запуска задачи в новом потоке.
На первый взгляд может показаться, что вызов start() должен привести к вызову ме
тода с большим временем выполнения, из вывода видно, что start () быстро возвращает
управление —сообщение «Waiting for LiftOff» появляется до завершения отсчета. Вы
вызываете LiftOff.run(), выполнение этого метода еще не завершено, но поскольку
LiftOff.run( ) выполняется другим потоком, в потоке main( ) можно выполнять другие
операции. (Данная возможность не ограничивается потоком main() — любой поток
может запустить другой поток.)
Итак, в программе выполняются сразу два метода — main() и LiftOff.run(). Метод
run() содержит код, выполняемый «одновременно» с другими потоками в программе.
Использование Executor
Объекты Executor, появившиеся Bjava SE5 (java.util.concurrent), значительно упрощают
параллельное программирование и управляю т объектами Thread за разработчиком.
Они образует дополнительную логическую прослойку между клиентом и исполня
емой задачей; вместо того чтобы запускать задачу напрямую, клиент поручает это
Очень часто один объект Executor может использоваться для создания и управления
всеми задачами в вашей системе.
Вызов shutdown() предотвращает отправку новых задач объекту Executor. Текущий по
ток (в нашем случае поток, выполняю щ ий m ain()) продолжает выполнять все задачи,
отправленные до вызова shutdown (). Программа завершается сразу же после завершения
всех задач, переданных Executor.
CachedThreadPool из предыдущего примера легко заменяется другим типом Executor.
Так, объект FixedThreadPool использует ограниченный набор потоков для выполнения
переданных задач:
//: concurrency/FixedThreadPool.java
import java.util.concurrent.*;
exec.execute(new LiftOff())j
exec.shutdown()j
>
> /* Output:
#0(9), #0(8), #0(7), #0(6), #0(5), #0(4), #0(3), #0(2),
#0(1), #0(Llftoff!), #1(9), #1(8), #1(7), #1(6), #1(5),
#1(4), #1(3), #1(2), #1(1), 41(Liftoff!), #2(9), #2(8),
#2(7), #2(6), #2(5), #2(4), #2(3), #2(2), #2(1),
#2(Liftoff!), #3(9), #3(8), #3(7), #3(6), #3(5), #3(4),
#3(3), #3(2), #3(1), #3(Liftoffl), #4(9), #4(8), #4(7),
#4(6), #4(5), #4(4), #4(3), #4(2), #4(1), #4(Liftoff!),
*///:~
>
продолжение ^>
ЮО Глава 21 • Параллельное выполнение
>. (2) Измените упражнение 2, чтобы задача была представлена объектом Callable
для суммирования значений всех чисел Ф ибоначчи. Создайте несколько задач
и выведите результаты.
Эжидание
Гакже для управления потоками можно воспользоваться методом sleep (), который
1ереводит поток в состояние ожидания на заданный промежуток времени. Если в классе
.iftO ff заменить вызов y ie ld () на вызов sleep (), вы получите следующее:
Возвращение значений из задач 901
//: concurrency/SleepingTask.java
// Вызов sleep() для приостановки выполнения.
import java.util.concurrent.*j
Вызов sleep() может выдать исклю чение InterruptedException, которое, как видно
из листинга, перехваты вается в run(). Так как исклю чения не передаю тся меж ду
потоками обратно в main(), вы долж ны локально обрабатывать любые исключения,
возникаю щ ие в задачах.
B J a v a SE5 появилась новая версия sleep(), входящ ая в класс TimeUnit (см. выше).
О на делает код более понятным, поскольку позволяет задать единицы времени для
задержки sleep(). Класс TimeUnit также может использоваться для выполнения пре
образований, как будет показано позднее.
В зависимости от платформы можно заметить, что задачи выполняю тся детермини-
рованно — от нуля до четырех, затем снова до нуля. Такое поведение объясняется тем,
что после каждой команды вывода каждая задача переходит в ожидание (блокируется),
чтобы планировщ ик мог переклю читься на другой поток и активизировать другую
задачу. О днако последовательное поведение опирается на ниж ележ ащ ий механизм
потоков, который отличается между операционными системами, поэтому рассчиты
вать на него нельзя. Если вам необходимо управлять порядком выполнения потоков,
лучше всего воспользоваться элементами синхронизации (см. далее), или в некоторых
случаях — вообще не использовать потоки, а вместо них написать свои собственные
взаимодействую щ ие процедуры, которы е будут передавать управление друг другу
в нужном порядке.
902 Глава21 • Параллельное выполнение
Приоритет
Приоритет ( priority) потока показывает планировщику, насколько важен этот поток.
Хотя порядок обращения процессора к существующему набору потоков и не детер
минирован, если существует несколько приостановленных потоков, одновременно
ожидающих запуска, планировщ ик сначала запустит поток с большим приоритетом.
Впрочем, это не значит, что потоки с младшими приоритетами не выполняются во
все (то есть отказа в обслуживании из-за приоритетов не возникает). Потоки с более
низкими приоритетами просто запускаются чуть реже.
В подавляющем большинстве случаев все потоки должны выполняться с приоритетом
по умолчанию. Попытки манипулирования с приоритетами потоков обычно являются
ошибкой.
Следующий пример демонстрирует уровни приоритетов. Приоритеты задаются ме
тодом setPriority() и читаются методом getPriority():
//: concurrency/SimplePriorities.java
// Shows the use of thread priorities,
import java.util.concurrent.*;
exec.shutdown();
}
> /* Output: (70% match)
Thread[pool-l-thread-6,l0,main]: 5
Thread[pool-l-thread-6jl9,main]: 4
Thread[pool-l-thread-6,10,main]: 3
Thread[pool-l-thread-6,10jmain]: 2
Thread[pool-l-thread-6,10,main]: 1
Thread[pool-l-thread-3,l,main]: 5
Thread[pool-l-thread-2,l,main]: 5
Thread[pool-l-thread-l,l,main]: 5
Thread[pool-l-thread-5,l,main]: 5
Thread[pool-l-thread-4,l,main]: 5
Нетрудно заметить, что поток под номером 1 обладает наивысшим приоритетом, а все
остальные потоки имеют минимальный приоритет. П риоритет задается в начале run();
задавать его в конструкторе бессмысленно, так как объект Executor в этот момент еще
не начал выполнение задачи.
В метод ru n () бы ли добавлены 100 000 достаточно затратны х операций с плаваю
щей запятой, вклю чая суммирование и деление с числом двойной точности double.
Переменная d была отмечена как volatile, чтобы компилятор не пы тался проводить
оптим изацию 1. Без этих вы числений вы не увидите эф ф екта установки различны х
приоритетов (попробуйте: закомментируйте цикл for с вы числениями). В процессе
вычислений вы видите, как планировщ ик уделяет больше внимания потоку с приори
тетом MAX_PRlORlTY (по крайней мере, таково было поведение программы на машине под
управлением Windows XP). И хотя вывод на консольтакж е является «дорогостоящей»
операцией, с ним вы не увидите влияние уровней приоритетов, поскольку вывод на
консоль не прерывается (иначе экран был бы заполнен несуразицей), в то время как
математические вычисления, приведенные выше, прерывать допустимо. Вычисления
вы полняю тся достаточно долго, соответственно, м еханизм плани рования потоков
вмешивается в процесс и чередует потоки, проявляя при этом внимание к более при
оритетным, поэтому вы сокоприоритетны е потоки получаю т предпочтение. Тем не
менее, чтобы обеспечить переключение контекста, программа регулярно выполняет
команды yield().
1 Это ключевое слово упоминается вскользь без всякого разъяснения. Его смысл в наложении
дополнительных ограничений на объявленные переменные в каждом потоке при выполнении
типовых действий: use, assign, read, load1,store, write, lock, unlock. Правила, регламентирующие
использование volatile, вы найдете в Java Language Specification, Second Edition, глава 17,
Threads and Locks. —Пргтеч. ped.
904 Глава 21 • Параллельное выполнение
Уступки
Если вы знаете, что в текущей итерации цикла в методе run() было сделано все, что
требовалось, то вы можете подсказать механизму планирования потоков, что процес
сором теперь может воспользоваться какой-либо другой поток. Эта подсказка (всего
лишь рекомендация — нет никакой гарантии, что ваша реализация «прислушается»
к ней) получает форму вызова метода y ie ld (). При вызове y ie ld () вы сообщаете, что
в системе могут выполняться другие потоки с тем же приоритетом.
В примере LiftOff.java вызов yield() используется для получения более равномерной
обработки разных задач LiftOff. Попробуйте закомментировать вызов Thread.yield()
в LiftOff.run() и посмотрите, что получится. В общем и целом метод yield() часто
используется неправильно, и я не рекомендую полагаться на него как на серьезное
средство настройки вашего приложения.
Потоки-демоны
Поток-демон (daem on) предоставляет некоторые общие услуги в фоновом режиме,
пока выполняется программа, но не является ее неотъемлемой частью. Таким образом,
когда все потоки не-демоны заканчивают свою деятельность, программа завершается.
И обратно, если существуют работающие потоки не-демоны, программа продолжает
выполнение. Существует, например, поток не-демон, выполняющий метод main().
//: concurrency/SimpleDaemons.java
// Потоки-демоны не препятствуют завершению работы программы.
import java.util.concurrent.*;
import static net.mindview.util.Print.*;
V //:
//: concurrency/DaemonFromFactory.java
// Использование ThreadFactory для создания демонов,
import java.util.concurrent.*j
import net.mindview.util.*;
import static net.mindview.util.Print.*;
while(true) {
TimeUnit.MILLISECONDS.sleep(l00);
print(Thread.currentThread() + " " + this);
>
} catch(InterruptedException e) {
print("Interrupted");
>
>
public static void main(String[] args) throws Exception {
ExecutorService exec = Executors.newCachedThreadPool(
new DaemonThreadFactory());
for(int i = 0; i < 10; i++)
exec.execute(new DaemonFromFactory());
print("All daemons started");
TimeUnit.MlLLISECONDS.sleep(500); // Небольшая задержка
>
} /* (Execute to see output) *///:~
Чтобы узнать значения для вызова конструктора базового класса, я просто заглянул
в исходный код Executors.java.
Чтобы узнать, является ли поток демоном, вызовите его метод isDaemon(). Если поток
является демоном, то все потоки, которые он создает, также автоматически становятся
демонами, что и демонстрируется следующим примером:
//: concurrency/Daemons.java
// Потоки-демоны порождают других демонов,
import java.util.concurrent.*;
import static net.mindview.util.Print.*;
while(true)
Thread.yield();
}
}
П оток класса Daemon переводится в режим демона, а затем порождает связку новых
потоков (для которых ф лагдем она явно не устанавливается), чтобы показать, что они
все равно будут демонами. Затем поток переходит в бесконечный цикл, на каждом
шаге которого вызывается метод y ie ld (), передающий управление другому процессу.
Учтите, что потоки-демоны завершают свои методы run() без выполнения блока finally:
//: concurrency/DaemonsDontRunFinally.java
// Потоки-демоны не выполняют блок finally.
import java.util.concurrent.*;
import static net.mindview.util.Print.*;
Разновидности реализации
Во всех предыдущих примерах все классы задач реализую т Runnable. В очень простых
случаях можно пойти по другому пути и использовать наследование непосредственно
от Thread:
//: concurrency/SimpleThread.java
// Прямое наследование от класса Thread.
System.out.print(this);
if(--countDown == 0)
return;
>
>
public static void main(String[] args) {
for(int i = 0; i < 5; i++)
new SimpleThread();
>
} /* Output:
#1(5), #1(4),#1(3), #1(2), #1(1), #2(5), #2(4), #2(3),
#2(2), #2(1),#3(5), #3(4), #3(3), #3(2), #3(1), #4(5),
#4(4), #4(3),#4(2), #4(1), #5(5), #5(4), #5(3), #5(2),
#5(1),
*///:~
И мена объектов Thread задаются вызовом соответствующего конструктора Thread. Д ля
обращ ения к имени в toString() используется имя getName().
Другая идиома, которая тоже встречается на практике, — самоуправляемая реализация
Runnable:
//: concurrency/SelfManaged.java
// Объект Runnable, содержащий свой управляющий поток.
> catch(InterruptedException e) {
print("sleep() interrupted");
>
>
public String toString() {
return getName() + ": " + countDown;
>
>;
t.start();
}
}
продолжение &
912 Глава 21 • Параллельное выполнение
Третий и четвертый классы в этом примере повторяют первые два класса, ноисполь-
зуют интерфейс Runnable вместо класса Thread.
Класс ThreadMethod демонстрирует создание потока внутри метода. Метод вызывает
ся тогда, когда поток готов к запуску, и возвращает управление после начала работы
потока. Если поток выполняет второстепенную операцию и не является ж изненно
важным для работы класса, вероятно, такое решение полезнее и уместнее запуска по
тока в конструкторе класса.
10. (4) И змените упражнение 5 по образцу класса ThreadMethod, чтобы метод runTask()
получал аргумент с количеством суммируемых чисел Ф ибоначчи и при каждом
вызове runTask() возвращал объект Future, полученный при вызове submit().
Терминология
Как показано в предыдущем разделе, у разработчика имеется выбор в отношении реа
лизации параллельных программ на H3biKeJava, и этот выбор может быть непростым.
Часто проблемы возникаю т из-за терминологии, используемой при описании техно
логий параллельного программирования, — особенно там, где задействованы потоки.
Вероятно, вы уже видите различия между выполняемой задачей и потоком, в котором
она выполняется; это различие особенно заметно проявляется в би б ли о теках^уа, по
тому что вы не можете контролировать класс Thread (это разделение становится еще
более очевидным для объектов Executor, которые создают потоки и управляю т ими за
вас). Вы создаете задачи и каким-то образом связываете их с потоками, чтобы поток
управлял выполнением этой задачи.
B J a v a класс Thread сам по себе ничего не делает. Он только управляет выполнением
полученной им задачи. Тем не менее в литературе постоянно используются выраже
ния вида «поток выполняет то или иное действие». Создается впечатление, что по
ток — это и есть задача. К огдая впервые столкнулся с noTOKaMnJava, это впечатление
было настолько сильным, что я решил, что класс задачи очевидным образом должен
наследовать от Thread. Добавьте к этому неудачный выбор имени интерфейса Runnable,
который, как мне кажется, было бы гораздо уместнее назвать Task. Если интерфейс
явно не делает ничего, кроме инкапсуляции своих методов, схема «деЛает-это-а Ы е »
годится, но если интерфейс предназначен для выражения концепции более высокого
уровня (как Task), то правильнее было бы выбрать концептуальное имя.
Проблема заключается в смешении разных уровней абстракции. Н а концептуальном
уровне мы хотим создать задачу, которая выполняется независимо от других задач,
поэтому нужно иметь возможность определить задачу и сказать «Вперед!», не беспо
коясь о подробностях. Но физическое создание потоков может обходиться достаточно
дорого, поэтому для экономии ресурсов нужно организовать управление потоками.
Таким образом, с точки зрения реализации задачи разумно отделять от потоков. Вдо
бавок потоковая м о д е л ь ^ у а основана на низкоуровневых р-потоках (pthreads, PO SIX
threads) из языка С, при работе с которыми необходимо во всех подробностях понимать
суть происходящего. Эта низкоуровневая сущность отчасти проникла в реализацию
Java, поэтому, чтобы оставаться на более высоком уровне абстракции, необходимо
действовать дисциплинированно при написании кода (в этой главе я постараю сь
действовать именно так).
914 Глава 21 • Параллельное выполнение
Присоединение к потоку
Каждый поток может вызвать метод jo in (), чтобы дождаться заверш ения другого
потока перед своим продолжением. Если поток вызывает t.j o i n ( ) для другого пото
ка t, то вызывающий поток приостанавливается до тех пор, пока целевой поток t не
завершится (когда метод t . isAlive() вернет false).
Вызвать метод join() можно также и с аргументом, определяющим тайм-аут (в миллисе
кундах или в миллисекундах с наносекундами); если целевой поток не закончит работу
за означенный период времени, метод join( ) все равно вернет управление инициатору.
Вызов join() может быть прерван вызовом метода interrupt() для потока-инициатора,
соответственно, понадобится конструкция try-catch.
Все эти операции продемонстрированы в следующем примере:
//: concurrency/3oining.java
// Как работает join().
import static net.mindview.util.Print.*;
> catch(InterruptedException e) {
print("Interrupted");
>
print(getName() + " join completed”);
}
>
Класс Sleeper — это тип потока, который приостанавливается на время, указанное в его
конструкторе. В методе run( ) вызов метода sleep( ) может закончиться при истечении
врем ени задерж ки, но может и прерваться. В блоке catch вы водится инф орм ация
о прерывании вместе со значением, возвращ аемым методом islnterrupted(). Когда
interrupt() для данного потока вызы вает другой поток, устанавливается флаг, по
казывающий, что поток был прерван. Однако этот ф лаг сбрасывается при обработке
исключения, поэтому внутри блока catch результатом всегда будет false. Ф лаг исполь
зуется в других ситуациях, где поток может исследовать свое прерванное состояние
в стороне от исключения.
3oiner — поток, который ожидает пробуждения потока Sleeper, вызывая для последнего
метод join(). В методе main() каждому объекту 3oiner сопоставляется Sleeper, и вы
можете видеть в результатах работы программы, что если Sleeper был прерван или
заверш ился нормально, 3oiner прекращает работу вместе с потоком Sleeper.
Библиотеки Java SE5 java.util.concurrent содержат такие инструменты, как CyclicBar-
rier (см. далее в этой главе); они могут оказаться более эффективными, чем метод
jo in () из исходной библиотеки потоков.
// : concurrency/ResponsiveUI.java
// Чуткость пользовательского интерфейса.
// {RunByHand}
class UnresponsiveUI {
private volatile double d = 1;
public UnresponsiveUI() throws Exception {
while(d > 0)
d = d + (Math.PI + Math.E) / d;
System.in.read(); // Never gets here
>
}
Группы потоков
Группа потоков (th r e a d g r o u p ) х р а н и т с о в о к у п н о с т ь п о т о к о в . П о д в е с т и и т о г з н а
ч е н и ю г р у п п п о т о к о в м о ж н о , п р и в е д я ц и т а т у и з Д ж о ш у а Б л о ш а 1, а р х и т е к т о р а
п р огр ам м н ого о б есп еч ен и я и з Sun, и сп р ави в ш его и зн ач и тел ь н о ул уч ш и в ш его
б и б л и о т е к у ^ К 1.2:
Перехват исключений
Из-за особой природы потоков вы не можете перехватывать исключения, возбужденные
из потока. После того как исключение выходит из метода run() задачи, оно передается
на консоль, если только вы не примете особые меры по перехвату таких исключений.
До в ы х о д а ^ у а SE5 для перехвата таких исключений использовались группы потоков,
но начиная с Jav a SE5 проблему можно решить при помощи объектов Executor, так
что теперь вам не нужно ничего знать о группах потоков (разве что для понимания
унаследованного кода; дополнительную информацию о группах потоков можно найти
в книге Thinkingin Java, 2nd Edition на сайте www.MindView.net).
Следующая задача всегда выдает исключение, которое выходит за пределы ее метода
run(), а метод main() демонстрирует, что происходит при ее выполнении:
//: concurrency/ExceptionThread.java
// {ThrowsException}
import java.util.concurrent.*j
1 А также еще в раде мест, возникающих по мере работы cJava. Впрочем, к чему останавливаться
на этом? Я консультировал достаточно проектов, к которым также применима данная фило
софия.
918 Глава 21 • Параллельное выполнение
new MyUncaughtExceptionHandler());
System.out.println(
"eh = " + t.getUncaughtExceptionHandler());
return tj
>
один раз. Раз путник один, вам не приходится принимать во внимание проблему двух
сущностей, пытающихся конкурировать за один и тот же ресурс, подобно двум людям,
которые хотят одновременно поставить машину в одном месте, вдвоем пройти в одну
дверь или даже говорить одновременно.
Когда на «сцену» выходит многозадачность, одиночеству приходит конец. Теперь у вас
есть сразу два или три потока, которые стремятся получить доступ к одному и тому
же ограниченному ресурсу. Если не предотвратить такие коллизии, два потока могут
попытаться получить доступ к одному счету в банке, одновременно распечатать два
документа на одном принтере, изменить одно и то же значение и т. п.
//: concurrency/EvenChecker.java
import java.util.concurrent.*;
//: concurrency/EvenGenerator.java
// Коллизии между потоками.
Одна задача может вызвать next() после того, как другая задача выполнила первое
увеличение currentEvenValue, но не второе (в месте, отмеченном комментарием «Опас
ная точка»). В результате значение оказывается в «некорректном» состоянии. Чтобы
доказать, что это возможно, EvenChecker.test() создает группу объектов EvenChecker
для непрерывного чтения вывода EvenGenerator и проверки их всех на четность. Если
среди значений окажется нечетное, программа сообщает об ошибке и завершается.
Рано или поздно в программе произойдет сбой, потому что задачи EvenChecker могут
обратиться к информации EvenGenerator в тот момент, когда она находится в «некор
ректном» состоянии. Однако проблема может проявиться только после того, как Even
Generator пройдет много циклов — это зависит от особенностей вашей операционной
системы и других подробностей реализации. Чтобы ускорить возникновение сбоя,
попробуйте вставить вызов yield() между первым и вторым приращением. Это одна
из проблем многопоточных программ — на первый взгляд они работают нормально,
тогда как на самом деле в них присутствует ошибка, просто ее вероятность очень низка.
Важно, что сама операция приращения состоит из нескольких шагов, а задачи могут
приостанавливаться механизмом потоков в середине инкремента —другими словами,
инкремент Bjava не является атомарной операцией. Таким образом, выполнение даже
одного инкремента без защиты задачи небезопасно.
1 От Брайана Гетца, соавтора книги «Java Concurrency in Practice» (Брайан Гетц, Тим Пейерс,
Джошуа Блош, Джозеф Боубир, Дэвид Холмс и Дуг Ли, издательство Addison-Wesley, 2006).
Совместное использование ресурсов 925
Синхронизация EvenGenerator
Добавление ключевого слова synchronized в EvenGenerator.java позволяет предотвратить
нежелательный доступ со стороны потоков:
//: concurrency/SynchronizedEvenGenerator.java
// Упрощение мьютексов с использованием
// ключевого слова synchronized.
// {RunByHand}
public class
SynchronizedEvenGenerator extends IntGenerator {
private int currentEvenValue = 0;
public synchronized int next() {
++currentEvenValue;
Thread.yield(); // Cause failure faster
++currentEvenValue;
return currentEvenValue;
}
public static void main(String[] args) {
EvenChecker.test(new SynchronizedEvenGenerator());
>
> ///:~
// {RunByHand>
import java.util.concurrent.locks.*;
В общем случае ключевое слово synchronized уменьшает объем кода и снижает вероят
ность ошибок пользователя, так что объекты Lock обычно применяются только в осо
бых ситуациях. Например, с ключевым словом synchronized невозможно обработать
неудачную попытку получения блокировки или прервать попытки получения блоки
ровки после истечения заданного промежутка времени — в таких случаях необходимо
использовать библиотеку concurrent
//: concurrency/AttemptLocking.java
// Объекты Lock из библиотеки concurrent позволяют
// отказаться от попыток получения блокировки
// по тайм-ауту. ,
import java.util.concurrent.*;
import java.util.concurrent.locks.*;
p u b lic c la s s A to m ic ity {
in t i;
v o id fl( ) { i+ + ; }
v o id f2 () { i += 3 ; }
> /* O u t p u t : ( 5 a m p le )
•••
v o id f l ( ) ;
Code:
0: a lo a d _ 0
1: dup
2: g e tfie ld #2; // П о ле i : I
5: ic o n s t_ l
б: ia d d
7: p u tfie ld #2; //Поле i : I
10: return
v o id f2 ();
Code:
0: a lo a d _ 0
1: dup
2: g e tfie ld #2; //Поле i : I
5: ic o n st_ 3
6: ia d d
7: p u tfie ld #2; //Поле i : I
10: return
*///:~
p u b lic c la s s A to m ic ity T e s t im p l e m e n t s R u n n a b l e {
p riv a te in t i = 0;
p u b lic in t g e tV a lu e () { return i; >
p riv a te syn ch ro n ize d v o id even In crem en t() { i+ +; i+ +j >
p u b lic v o id run() {
w h ile (tru e )
p u b lic c la s s S e ria lN u m b e rG e n e ra to r {
p riv a te s ta tic v o la tile in t se ria lN u m b e r = 0;
p u b lic s ta tic in t n e x tS e ria lN u m b e r() {
return s e ria lN u m b e r+ + ; // Небезопасно для потоков
>
> // /:~
1 Источником вдохновения послужила книга Effective Java Джошуа Блоша, изд-во Addison-
Wesley, 2001, с. 190.
932 Глава 21 • Параллельное выполнение
// М н о го к р а тн о и с п о л ь з у е т п а м я ть во избежание ее и с ч е р п а н и я :
cla s s C irc u la rS e t {
p riv a te in t[ ] array;
p riv a te in t le n ;
p riv a te in t i n d e x = 0;
p u b lic C irc u la rS e t(in t s iz e ) {
a r r a y = new i n t [ s i z e ] ;
le n = s iz e ;
// И нициализируем з н а ч е н и е м , к о т о р о е не п р о и з в о д и т с я
// классом S e r ia lN u m b e r G e n e r a to r:
fo r( in t i = 0; i < siz e ; i++)
a rra y [ i] = -1;
>
p u b lic sy n ch ro n ize d v o id a d d (in t i) {
a rra y [in d e x ] = i;
// "П е р е н о с " и н д е к с а с пе р е з а п и сью ст ар ы х э л е м е н т о в :
i n d e x = + + in d ex % l e n ;
}
p u b lic sy n ch ro n ize d b o o le a n c o n t a i n s ( i n t v a l) {
fo r( in t i = 0; i < le n ; i++)
if( a rra y [i] == v a l ) return true;
return fa ls e ;
>
>
p u b lic c la s s S e ria lN u m b e rC h e ck e r {
p riv a te s ta tic fin a l in t SIZE = 10;
Совместное использование ресурсов 933
Атомарные классы
В Java SE5 появились специальные классы атомарных переменных: Atom iclnteger,
AtomicLong, AtomicReference и т. д., обеспечивающие выполнение атомарных условных
обновлений в форме:
boolean compareAndSet(expectedValue, updateValue);
// {RunByHand}
im p o rt j a v a . u t i l . c o n c u r r e n t . a to m ic .* ;
Критические секции
Иногда бывает нужно предотвратить доступ нескольких потоков только к части кода,
а не к методу в целом. Фрагмент кода, который изолируется таким способом, называется
критической секцией (critical section), для его создания также применяется ключевое
слово s y n c h r o n i z e d . Н а этот раз слово s y n c h r o n i z e d указывает на объект, блокировка
которого должна использоваться для синхронизации последующего фрагмента кода:
sy n ch ro n ize d (sy n cO b je ct) {
// К этом у к о д у в любой м о м ен т времени
/ / может о б р а щ а т ь с я т о л ь к о одн а задача
>
package c o n c u rre n c y ;
im p o r t ja v a .u til.c o n c u rre n t.* ;
im p o r t j a v a . u t i l . c o n c u r r e n t . a t o m ic .* ;
im p o rt ja v a .u til.* ;
c la s s P a ir { // П о т о к о в о - н е б е з о п а с н ы й к л а с с
p riv a te in t x, y;
p u b lic P a ir(in t x, in t y) {
th is .x = x;
th is .y = y;
}
p u b lic P a ir() { th is (0 , 0); >
p u b lic in t g etX () { re tu rn x; >
p u b lic in t g etY () { return y; >
p u b lic v o id in cre m e n tX () { x++; >
p u b lic v o id in c re m e n tY () { y++; >
p u b lic S trin g to S trin g () {
return "x: " + x + ", y: " + y;
>
p u b lic c la s s P a ir V a lu e s N o tE q u a lE x c e p tio n
exten ds R u n tim e E x c e p tio n {
p u b lic P a irV a lu e s N o tE q u a lE x c e p tio n () {
s u p e r(''P a ir v a lu e s not e q u a l: " + P a ir.th is ) ;
>
>
// Произвольный и н ва р и а н т - переменные должны быть равны:
p u b lic v o id ch eckS tate() {
if( x != у )
t h r o w new P a i r V a l u e s N o t E q u a l E x c e p t i o n ( ) ;
>
>
// С и н х р о н и з а ц и я ц е л о г о м е т о д а :
c la s s P a irM a n a g e rl ex ten ds P a irM a n a g e r {
p u b lic sy n ch ro n ize d v o id in c re m e n t ( ) {
p .in c re m e n tX ();
p .in c re m e n tY ( );
Совместное использование ресурсов 937
s to re (g e tP a irQ );
}
>
// И с п о л ь з о в а н и е критической секции:
cla s s P a irM a n a g e r2 e x te n d s P a irM a n a g e r {
p u b lic v o id in c re m e n t() {
P a i r te m p ;
s y n c h ro n iz e d (th is ) {
p .in c re m e n tX ();
p .in c re m e n tY ();
tem p = g e t P a i r ( ) ;
>
store(tem p);
>
c la s s P a irC h e c k e r im p le m e n ts R u nna b le {
p riv a te P a irM a n a g e r pm;
p u b lic P a irC h e c k e r(P a irM a n a g e r pm) {
th is .p m = pm;
>
p u b lic v o id run() {
w h ile (tru e ) {
p m .c h e c k C o u n te r.in c re m e n tA n d G e t();
p m .g e tP a ir().c h e c k S ta te ();
>
>
>
p u b lic cla s s C ritic a lS e c tio n {
// Т е с т и р о в а н и е д в у х разн ы х п о д х о д о в :
s ta tic v o id
te s tA p p ro a c h e s(P a irM a n a g e r p m anl, P a irM a n a g e r pman2) {
E x e c u to rS e rv ic e exec = E x e c u to rs.n e w C a c h e d T h re a d P o o l();
P a irM a n ip u la to r
pm l = new P a i r M a n i p u l a t o r ( p m a n l ) ,
pm2 = new P a i r M a n i p u l a t o r ( p m a n 2 ) ;
P a irC h e c k e r
p c h e c k l = new P a i r C h e c k e r ( p m a n l ) ,
p c h e c k 2 = new P a i r C h e c k e r ( p m a n 2 ) ;
e x e c . e x e c u t e ( p m l);
e x e c.e x e cu te (p m 2 );
e x e c . e x e c u te (p c h e c k l);
продолжение &
938 Глава 21 • Параллельное выполнение
e x e c.e x e cu te (p c h e c k 2 );
try {
T im e U n it.M IL L IS E C O N D S .s le e p (5 0 0 )j
} c a tc h ( I n te rru p te d E x c e p tio n e) {
S y s te m .o u t. p r i n t l n ( "S le e p in t e r r u p t e d " );
>
S y s te m .o u t.p rin tln (" p m l: " + pm l + "\npm2: " + pm2)j
S y s te m .e x it(0 );
>
p u b lic s ta tic v o id m a in (5 trin g [] args) {
P a irM a n a g e r
pm anl = new P a i r M a n a g e r l ( ) ,
pman2 = new P a i r M a n a g e r 2 ( ) j
te s tA p p ro a c h e s(p m a n lj pman2);
}
> / * O u t p u t : ( S a m p le )
p m l: P a i r : x : 15 , у : 15 c h e c k C o u n t e r = 272565
pm2: P a ir: x: 16, у: 16 c h e c k C o u n t e r = 3956974
*///:~ '
П Синхронизация в с е г о м етода:
c la s s E x p lic it P a ir M a n a g e r l exten ds P a irM a n a g e r {
p riv a te Lock lo c k = new R e e n t r a n t L o c k ( ) ;
p u b lic sy n ch ro n ize d v o id in c re m e n t() {
lo c k .lo c k () ;
try {
p .in c re m e n tX ();
p .in c re m e n tY ();
s to re ( g e tP a ir( ) );
> fin a lly {
lo c k .u n lo c k ( ) ;
>
>
pman2 = new E x p l i c i t P a i r M a n a g e r 2 ( ) ;
C ritic a lS e c tio n .te s tA p p ro a c h e s (p m a n l, pman2);
}
} /* O u t p u t : ( S a m p le )
p m l: P a in : x: 15 , у: 15 c h e c k C o u n t e r = 174035
pm2: P a ir: x: 16 , у: 16 c h e c k C o u n t e r = 2608588
* // /:~
Эта программа, использующая большую часть кода CriticalSection.java, создает новые типы
P a i r M a n a g e r , использующие явную блокировку с объектами L o c k . E x p l i c i t P a i r M a n a g e r 2
демонстрирует создание критической секции с использованием объекта L o c k ; вызов
s t o r e ( ) находится вне критической секции.
cla s s D u alSyn ch {
p riv a te O b je ct s y n c O b j e c t = new O b j e c t ( ) ;
p u b lic sy n ch ro n ize d v o id f() {
fo r( in t i = 0; i < 5; i++) {
p rin t( " f( ) " ) ;
T h re a d .y ie ld ();
>
>
p u b lic v o id g() {
sy n ch ro n ize d (sy n cO b je ct) {
fo r( in t i = 0; i < 5; i++) {
p rin t( " g ( )" );
T h re a d .y ie ld ();
>
>
>
>
p u b lic v o id run() {
d s .f();
>
} .s ta rt();
d s .g ()j
>
} /* O u t p u t : ( S a m p le )
g()
f()
g()
f()
g()
f()
g()
Ю
g()
f()
*///:~
продолжение &
942 Глава 21 • Параллельное выполнение
c la s s Accessor im p l e m e n t s R u n n a b le {
p riv a te fin a l in t id ;
p u b lic A c c e s s o r( in t id n ) { id = id n ; >
p u b lic v o id ru n() {
w h ile ( ! Th read . cu rren tTh rea d ( ) . is I n te rr u p te d ()) {
T h re a d L o c a lV a ria b le H o ld e r.in c re m e n t();
S ystem . o u t , p r i n t l n ( t h i s );
T h re a d .y ie ld ();
>
}
p u b lic S trin g to S trin g () {
return "#" + i d + ": " +
T h re a d L o c a lV a ria b le H o ld e r. g e t( ) j
>
>
p u b lic c la s s T h re a d L o c a lV a ria b le H o ld e r {
p r i v a t e s t a t i c T h re a d L o ca l< In te g e r> v a lu e =
new T h r e a d L o c a l < I n t e g e r > ( ) {
p riv a te Random r a n d = new R a n d o m (4 7 );
p ro tected syn ch ro n ize d In te g er in itia lV a lu e ( ) {
return ra n d .n e x tIn t(1 0 0 0 0 );
>
>;
p u b lic s ta tic v o id in cre m e n t() {
v a lu e .s e t(v a lu e .g e t() + 1);
>
p u b lic s ta tic in t get() { return v a lu e .g e t(); >
p u b lic s ta tic v o id m a in (S trin g [) args) th row s E x c e p tio n {
E x e c u to rS e rv ic e exec = E x e c u to rs.n e w C a c h e d T h re a d P o o l();
fo r( in t i = 0j i < 5; i++)
e x e c.e x e cu te (n e w A c c e s s o r ( i ) ) ;
T im e U n it.S E C 0 N D S .s le e p (3 ); / / Небольшая задержка
e x e c.sh u td o w n N o w (); // В се экземпляры A c c e s s o r завершаются
>
} /* O u t p u t : ( S a m p le )
#0 9259
#1 556
#2 6694
#3 1862
#4 962
#0 9260
#1 557
#2 6695
#3 1863
#4 963
* ///:~
При запуске программы вы увидите доказательства того, что для каждого из потоков
выделяется отдельная память, так как каждый поддерживает собственную копию
счетчика — при том, что объект T h r e a d L o c a l V a r i a b I e H o l d e r только один.
Завершение задач
В некоторых из приведенных примеров методы c a n c e l ( ) и i s C a n c e l e d ( ) размещаются
в классе, видимом для всех задач. Задачи используют проверку i s C a n c e l e d ( ) , чтобы
определить, когда следует завершиться; это разумный подход к решению проблемы.
Однако в некоторых ситуациях задачу требуется завершить быстрее. В этом разделе
вы узнаете о некоторых аспектах и проблемах такого завершения.
Начнем с рассмотрения примера, который не только демонстрирует проблему завер
шения, но и дает дополнительный пример совместного доступа к ресурсам,
В этой модели дирекция парка хочет узнать, сколько людей ежедневно заходит в парк
через несколько ворот. У каждых ворот имеется вертушка; после увеличения счетчика
вертушки увеличивается общий счетчик, представляющий общее количество посети
телей парка.
//: co n cu rre n cy/O rn a m e n ta lG a rd e n . ja v a
im p o rt ja v a .u til.c o n c u rre n t.* ;
im p o rt ja v a .u til.* ;
im p o rt s ta tic n e t.m in d v ie w .u til.P rin t.* ;
cla s s Count {
p riv a te in t co u n t = 0;
p riv a te Random r a n d = new R a n d o m ( 4 7 );
/ / Удаляем ключевое с л о в о s y n c h r o n i z e d ,
// что бы у в и д е т ь с б о й в с и с т е м е п о д с ч е т а :
p u b lic sy n ch ro n ize d in t in c re m e n t() {
in t tem p = c o u n t j
if(ra n d .n e x tB o o le a n ()) // У с т у п а е т у п р а в л е н и е
T h re a d .y ie ld (); // в п о л о вин е случаев
return ( c o u n t = ++tem p);
}
p u b lic sy n ch ro n ize d in t v a lu e () { return cou n t; }
c la s s En tran ce im p le m e n ts R u nna b le {
p riv a te s ta tic C o u n t c o u n t = new C o u n t ( ) ;
p riv a te s ta tic L is t< E n tra n c e > entrances =
new A r r a y L i s t < E n t r a n c e > Q ;
p riv a te in t num ber = 0 ;
// Для ч т е н и я с и н х р о н и з а ц и я не нужна:
p riv a te fin a l in t id ;
p riv a te s ta tic v o la tile b o o le a n c a n c e le d = fa ls e ;
// Атомарная операция с v o l a t i l e -полем:
p u b lic s ta tic v o id c a n c e l() { c a n c e le d = tru e ; >
p u b lic E n tra n c e ( in t id ) {
th is .id = id ;
// Задача о с т а е т с я в с п и с к е . Также п р е д о т в р а щ а е т
// у н и ч т о ж е н и е "м е р тв ы х " з а д а ч при у б о р к е м у с о р а : ~ .
продолжение &
944 Глава 21 • Параллельное выполнение
e n tra n c e s. a d d (th is );
}
p u b lic v o id ru n() {
w h ile (!c a n c e le d ) {
s y n c h ro n iz e d (th is ) {
++number;
>
p rin t(th is + " T o ta l: " + co u n t.in c re m e n t());
try {
T im e U n it.M IL L IS E C O N D S . s l e e p ( 1 0 0 ) ;
> c a tc h ( I n te rru p te d E x c e p tio n e) {
p rin t(" s le e p in te rru p te d " );
>
>
p rin t( " S to p p in g " + th is );
>
public synchronized int getValue() { return number; }
p u b lic S trin g to S trin g () {
return "Entrance " + id + ": " + g e tV a lu e () ;
}
p u b lic s ta tic in t g e tT o ta lC o u n t() {
return c o u n t.v a lu e ();
}
p u b lic c la s s O r n a m e n t a lG a r d e n {
p u b lic s ta tic v o id m a in ( S trin g [ ] args) throw s E x c e p tio n {
E x e c u to rS e rv ic e exec = E x e c u to rs .n e w C a c h e d T h re a d P o o l();
fo r( in t i = 0; i < 5; i++)
e x e c.e x e cu te (n e w E n tra n c e ( i) ) ;
// П р о р а б о т а т ь н е к о т о р о е в р ем я , затем
// о с т а н о в и т ь с я и с о б р а т ь данные:
T im e U n it.S E C 0 N D S .s le e p (3 );
E n tra n c e .c a n c e l();
e x e c.sh u td o w n ();
i f ( ! e x e c . a w a itT e rm in a tio n (2 5 0 , T im e U n i t . M I L L I S E C O N D S ) )
p rin t(''S o m e t a s k s were n o t t e r m i n a t e d ! " ) ;
p rin t( " T o ta l: " + E n tra n ce .g e tT o ta lC o u n t());
p rin t("S u m of En tran ces: " + E n tra n ce .s u m E n tra n ce s());
>
> /* O u t p u t : ( S a m p le )
En tran ce 0: 1 T o ta l: 1
En trance 2: 1 T o ta l: 3
En trance 1: 1 T o ta l: 2
En tran ce 4: 1 T o ta l: 5
En trance 3: 1 T o ta l: 4
E n t r a n c e 2: 2 T o ta l: 6
En tran ce 4: 2 T o ta l: 7
En tran ce 0: 2 T o ta l: 8
>
• • •
En tran ce 3: 29 T o t a l : 143
Завершение задач 945
En tran ce 0: 29 T o t a l : 144
En tran ce 4: 29 T o t a l : 145
En tran ce 2: 30 T o t a l : 147
En trance 1: 30 T o t a l : 146
En tran ce 0: 30 T o t a l : 149
En tran ce 3: 30 T o t a l : 148
En trance 4: 30 T o t a l : 150
S to p p in g En tran ce 2: 30
S to p p in g E n tran ce 1: 30
S to p p in g E n tran ce 0: 30
S to p p in g En tran ce 3: 30
S to p p in g En tran ce 4: 30
T o ta l: 150
Sum o f En trances: 150
*///:~
Один объект C o u n t хранит главный счетчик посетителей парка и хранится в статическом
поле класса E n t r a n c e . Методы C o u n t . i n c r e m e n t ( ) и C o u n t . v a l u e ( ) синхронизируются
для управления доступом к полю c o u n t . Метод i n c r e m e n t ( ) использует объект Rand o m
для того, чтобы уступать управление вызовом y i e l d ( ) примерно в половине случаев,
между выборкой c o u n t в t e m p и увеличением/сохранением t e m p обратно в c o u n t . Если
закомментировать ключевое слово s y n c h r o n i z e d для i n c r e m e n t ( ) , программа перестает
работать, потому что сразу несколько задач будут пытаться обращаться и изменять
c o u n t одновременно (с вызовом y i e l d ( ) проблема проявляется быстрее).
i
Состояния потока
Поток может находиться в одном из четырех возможных состояний.
1. П ереходное (new): поток находится в этом состоянии очень недолго, только во
время создания. Он получает все необходимые системные ресурсы и выполняет
инициализацию. Н а этой стадии он получает право на выделение процессорного
времени. Далее поток переводится планировщиком в активное или заблокирован
ное состояние.
2. Активное (runnable): в таком состоянии поток будет выполняться тогда, когда у
механизма разделения времени процессора появятся для него свободные кванты.
Таким образом, поток может как не выполняться, так и выполняться, однако ничто
не препятствует последнему при получении потоком процессорного времени; он
не «мертв» и не блокирован.
3. Блокировки (blocked): поток может выполняться, однако есть что-то, что мешает
ему это сделать. Пока поток имеет данный статус, квантирующий время процессор
пропускает его очередь и не выделяет ему циклов на выполнение. До тех пор пока
поток не перейдет в рабочее состояние, он не в состоянии произвести ни одной
операции.
4. Завершенное (dead): в этом состоянии поток не ставится на выполнение и не полу
чает процессорного времени. Его задача завершена, и он не может стать активным.
Одним из способов перехода в завершенное состояние является возврат из метода
run(), но поток задачи также может быть прерван явно, как мы вскоре увидим.
Завершение при блокировке 947
Прерывание
Как нетрудно предположить, выход из метода R u n n a b l e . r u n ( ) на середине — дело более
хлопотное, чем ожидание до проверки ф лага отмены (и ли другой точки, в которой
програм мист готов к выходу из метода). П ри выходе из заблокированной задачи
может возникнуть необходимость в освобождении ресурсов. По этой причине выход
из середины метода r u n ( ) задачи больше напоминает исключение, чем что-либо еще,
поэтому в потоках Jav a для такой отмены применяются исключения1. (Здесь ситуация
балансирует на грани ненадлежащего применения исключений, потому что они будут
часто использоваться для управления выполнением программы.) Чтобы вернуться
в заведомо допустимое состояние при заверш ении задачи, необходимо тщ ательно
проанализировать пути выполнения кода и написать условие c a t c h для освобождения
всех ресурсов.
e cu to r:
//: c o n c u r r e n c y / I n t e r r u p t in g . java
// Прерывание з а б л о к и р о в а н н о г о п о т о к а ,
im p o rt j a v a . u t i l . c o n c u r r e n t . * ;
im p o r t j a v a . i o . * ;
im p o rt s t a t i c n e t.m in d v ie w .u til.P rin t.* ;
c la s s S le e p B lo ck e d im p le m e n t s R u n n a b le {
p u b lic v o id run() {
try {
T i m e U n i t . SECONDS. s l e e p ( 1 0 0 ) ;
> c a tc h (in te rru p te d E x c e p tio n e) {
p rin t(" I n te rru p te d E x c e p tio n " );
>
p r in t ( " E x itin g S le e p B lo c k e d . r u n ( ) ");
}
>
cla s s IO B locked i m p le m e n t s R u n n a b le {
p riv a te InputStream in ;
p u b lic IO B locked (Inp utStream is ) { in = is ; }
p u b lic v o id run() {
try {
p rin t("W a itin g fo r re a d ():");
in .re a d () ;
} ca tch (IO E x ce p tio n e) {
if(T h re a d .c u rre n tT h re a d ().is In te rru p te d ()) {
Завершение при блокировке 949
cla s s S y n c h ro n iz e d B lo c k e d im p le m e n ts R u nna b le {
p u b lic sy n ch ro n ize d v o id f() {
w h ile ( tru e ) // Блокировка н икогда не с н и м а е т с я
T h re a d .y ie ld ();
>
p u b lic S y n c h ro n iz e d B lo c k e d () {
new T h r e a d ( ) {
p u b lic void ru n () {
f(); // Блокировка у с т а н а в л и в а е т с я этим потоком
}
> .s ta rt( ) j
>
p u b lic v o id run() {
p rin t( " T r y in g to c a ll f()")j
f();
p rin t( " E x itin g S y n c h ro n iz e d B lo c k e d .ru n ()" )j
>
import java.util.concurrent.*;
import java.io.*;
import static net.mindview.util.Print.*;
public class CloseResource {
public static void main(String[] args) throws Exception {
ExecutorService exec = Executors.newCachedThreadPool();
ServerSocket server = new ServerSocket(8080);
InputStream socketInput =
new Socket("localhost"> 8080).getInputStream();
exec.execute(new IOBlocked(socketInput));
exec.execute(new IOBlocked(System.in ));
TimeUnit.MILLISECONDS. sleep(100);
print("Shutting down a ll threads");
exec. shutdownNow();
r
T i m e U n i t . SECONDS. s l e e p ( l ) ;
p rin t( " C lo s in g " + s o c k e tI n p u t.g e tC la s s ( ) .g e tN a m e () ) ;
s o c k e tI n p u t.c lo s e (); // Осво бо ж ден ие з а б л о к и р о в а н н о г о п о т о к а
T im e U n it.S E C O N D S .s le e p ( l) ;
p n in t(" C lo s in g " + S y s te m .in .g e tC la s s ( ) .g e tN a m e ( ) );
S y s te m .in .c lo s e ( ) ; // О сво бо ж ден ие з а б л о к и р о в а н н о г о п о т о к а
>
} /* O u tp ut: (85% m a t c h )
W a itin g f o r read ():
W a itin g f o r read ():
S h u ttin g down a l l th rea d s
C lo s in g j a v a . n e t . SocketInp utStream
In terrup ted from b lo c k e d I/O
E x itin g IO B lo c k e d .ru n ()
C lo s in g j a v a . i o . B u ffered In p u tS trea m
E x itin g IO B lo c k e d .ru n ()
*///:~
c la s s N IO B lo ck e d im p le m e n ts Ru nnable {
p riv a te fin a l SocketC h an nel sc;
p u b lic N I O B lo c k e d ( S o c k e tC h a n n e l sc) { th is .s c = sc; }
p u b lic v o id ru n() {
try {
p rin t(" W a itin g f o r read() in ” + th is );
s c . re a d (B yte B u ffe r.a llo c a t e ( l) );
} c a tc h (C lo s e d B y ln te rru p tE x c e p tio n e) {
p rin t( " C lo s e d B y I n te rru p tE x c e p tio n " ) ;
} c a tc h (A s y n c h ro n o u s C lo s e E x c e p tio n e) {
p r i n t ( "A s y n c h ro n o u s C lo s e E x c e p tio n ");
) ca tch (IO E x ce p tio n e) {
t h r o w new R u n t i m e E x c e p t i o n ( e ) ;
}
p rin t( " E x itin g N I O B lo c k e d .ru n () " + th is );
}
>
ServerSocket s e r v e r = new S e r v e r S o c k e t ( 8 0 8 0 ) ;
In etS ocketA ddress is a =
new I n e t S o c k e t A d d r e s s C l o c a l h o s t ” , 8080);
SocketC h an nel s c l = S o c k e tC h a n n e l.o p e n (is a );
Sock etC h an nel sc2 = S o c k e tC h a n n e l.o p e n ( is a ) ;
F u tu re < ? > f = e x e c .su b m it(n e w N I O B lo c k e d ( s c l) ) ;
e x e c . e x e c u t e ( new N I O B l o c k e d ( s c 2 ) ) ;
e x e c.sh u td o w n ();
T i m e U n i t . S ECONDS. s l e e p ( 1 ) ;
// Прерывание ч е р е з вызов c a n c e l :
f.c a n c e l( tru e ) ;
T im e U n it.S E C O N D S . s l e e p ( l ) ;
/ / Р а з б л о к и р о в а н и е п о с р е д с т в о м за кр ы ти я ка н а л а :
s c 2 .c lo s e ();
>
} /* O u t p u t : ( S a m p le )
W a itin g f o r re a d () in N I 0 B lo c k e d @ 7 a 8 4 e 4
W a itin g f o r read () in N IO B lo cked @ 1 5 c7 8 5 0
C lo s e d B y In te rru p tE x c e p tio n
E x itin g N IO B lo c k e d .ru n () N IO B lo cked @ 1 5 c7 8 5 0
A s y n c h ro n o u s C lo s e E x c e p tio n
E x itin g N IO B lo ck e d .ru n () N I 0 B lo c k e d @ 7 a 8 4 e 4
* / / / :~
Как видно из приведенного примера, для разблокирования также можно закрьггь канал,
хотя такая необходимость возникает очень редко. Учтите, что использование e x e c u t e ( )
для запуска обеих задач с вызовом e . s h u t d o w n N o w ( ) позволяет легко завершить все;
сохранение F u t u r e в приведенном примере было необходимо лишь для того, чтобы
прерывание было передано только одному из двух потоков1.
18. (2) Создайте класс, не являющийся задачей, с методом, который вызывает sleep()
на долгий промежуток времени. Создайте задачу, которая вызывает метод первого
класса. В main() запустите задачу и прервите ее методом in te rru p t(). Убедитесь
в том, что задача завершилась корректно.
19. (4) Измените пример OrnamentalGarden.java так, чтобы в нем использовался метод
in te r r u p t ( ) .
Блокирование по мьютексу
Как было показано в примере Interrupting.java, при попытке вызова синхронизированного
метода для объекта, для которого уже была установлена блокировка, вызывающая за
дача приостанавливается до получения блокировки. Следующий пример показывает,
как один мьютекс может многократно захватываться одной задачей:
//: c o n c u rre n c y / M u ltiL o c k .ja v a
// П оток может м н о г о к р а т н о з а х в а т ы в а т ь
// о дн у б л о к и р о в к у .
im p o rt s ta tic n e t.m in d v ie w .u til.P rin t.* ;
p u b lic c la s s M u ltiL o c k {
p u b lic sy n c h ro n iz e d v o id fl( in t cou nt) {
if( c o u n t - - > 0) {
p rin t( " fl( ) c a llin g f2 () w ith count " + co u nt);
f2 (co u n t);
}
>
p u b lic s y n ch ro n ize d v o id f2 ( in t co u nt) {
if( c o u n t-- > 0) {
p rin t( " f2 ( ) c a llin g fl( ) w ith count " + co u nt);
fl(c o u n t);
>
>
p u b lic s ta tic v o id m a in (S trin g [] args) th row s E x c e p tio n {
fin a l M u ltiL o c k m u l t i L o c k = new M u l t i L o c k ( ) ;
new T h r e a d ( ) {
p u b lic v o id ru n() {
m u ltiL o c k .fl(1 0 ) ;
>
} .s ta rt();
>
) /* O u tput:
fl( ) c a llin g f2 () w ith count 9
f2 () c a llin g fl( ) w ith count 8
fl( ) c a llin g f 2 ( ) w ith count 7
f2 () c a llin g fl( ) w ith count 6
fl( ) c a llin g f2 () w ith count 5
f2 () c a llin g fl( ) w ith count 4
fl( ) c a llin g f2 () w ith count 3
f2 () c a llin g fl( ) w ith count 2
fl( ) c a llin g f2 () w ith count 1
f2 () c a llin g fl( ) w ith count 0
*// /:~
cla s s B lo c k e d M u te x {
p riv a te Lock lo c k = new R e e n t r a n t L o c k ( ) ;
p u b lic B lo c k e d M u te x () {
продолжение &
954 Глава 21 • Параллельное выполнение
// Немедленное п о л у ч е н и е б л о к и р о в к и д л я д е м о н с т р а ц и и
// прерывания з а д а ч л з а б л о к и р о в а н н ы х п о R e e n t r a n t L o c k :
lo c k .lo c k ( ) ;
>
p u b lic v o id f() {
try {
// Н и ко гд а не б у д е т д о с т у п е н д л я в т о р о й з а д а ч и
lo c k .lo c k I n te rr u p tib ly ( ) ; // Специальный вы зов
p rin t(" lo c k a cq u ire d in f()")j
} ca tc h (In te rru p te d E x c e p tio n e) {
p rin t(" I n te rru p te d from lo c k a c q u is itio n in f()")j
>
>
>
c l a s s B lo c k e d 2 im p le m e n ts R u n n a b le {
B l o c k e d M u t e x b l o c k e d = new B l o c k e d M u t e x ( ) ;
p u b lic v o id run() {
p rin t(" W a itin g fo r f() in B lo ck e d M u te x ");
b lo c k e d .f()j
p rin t("B ro k e n o u t o f b lo c k e d c a ll" ) ;
>
>
Проверка прерывания
Учтите, что при вызове in te rru p t() для потока прерывание может случиться только
при входе задачи в блокирующую операцию (или если задача уже находится в ней) —
кроме непрерываемого ввода-вывода или заблокированных синхронизированных
1 Учтите, что вызов t . i n t e r r u p t () может случиться до вызова blo ck ed . f () (хотя это и мало
вероятно).
Завершение при блокировке 955
методов, когда вы ничего не можете сделать. Но что, если вы написали код, который
может выдавать или не выдавать такой блокирующий вызов в зависимости от условий
выполнения? Если выход может осуществляться только с возбуждением исключения
к блокирующему вызову, вы не всегда сможете покинуть цикл r u n ( ) . Таким образом,
если вы вызываете i n t e r r u p t ( ) для остановки задачи, вашей задаче необходим второй
способ выхода на случай, если цикл r u n ( ) не совершает никаких блокирующих вызовов.
Такая возможность обеспечивается состоянием прерывания, которое устанавливает
ся вызовом i n t e r r u p t ( ) . Состояние прерывания проверяется вызовом i n t e r r u p t e d ( ) .
Оно не только сообщает, был ли вызван метод i n t e r r u p t ( ) , но и сбрасывает состояние
прерывания. Сброс состояния прерывания предотвращ ает повторные оповещ ения
о прерывании задачи: оповещение поступает либо через одиночное исключение l n -
t e r r u p t e d E x c e p t i o n , либо через одну успешную проверку T h r e a d . i n t e r r u p t e d ( ) . Если
вы захотите снова провери ть, в о зн и к л о ли прерывание, сохраните результат вызова
T h r e a d . i n t e r r u p t e d ().
c la s s N e e d sC le a n u p {
p riv a te fin a l in t id ;
p u b lic N e e d s C le a n u p (in t id e n t) {
id = id e n t;
p rin t(" N e e d s C le a n u p " + id ) ;
>
p u b lic v o id cle a n u p () {
p rin t( " C le a n in g up " + id ) ;
>
>
cla s s B lo ck e d 3 im p le m e n ts R u n n a b le {
p riv a te v o la tile d o u b le d = 0 .0 ;
p u b lic v o id run() {
try {
w h ile (lT h re a d .in te rru p te d ()) {
// точка1
N e e d sC le a n u p n l = new N e e d s C l e a n u p ( l ) ;
// t r y - f i n a l l y начинается с р а з у же з а определением
// n l , чтобы г а р а н т и р о в а т ь освобож дение n l :
try {
p r i n t ( " S le e p in g ” );
T im eU n i t . SECONDS. s l e e p ( 1 ) ;
// т о ч к а 2
N e e d sC le a n u p n2 = new N e e d s C l e a n u p ( 2 ) ;
// Г а р а н т и р у е т п равильное освобож дение n2:
try {
p rin t( " C a lc u la tin g " ) ;
продолжение &
956 Глава 21 • Параллельное выполнение
/ / Продолжительная неблокирующая о п е р а ц и я :
fo r( in t i = 1; i < 2500000; i++)
d = d + (M ath .P I + M a th .E ) / d;
p rin t(" F in is h e d tim e -c o n su m in g o p e r a t i o n " ) ;
} fin a lly {
n 2 .c le a n u p ();
}
} fin a lly {
n l.c le a n u p ();
>
>
p rin t( " E x itin g v ia w h ile () test");
} c a tc h (I n te rru p te d E x c e p tio n e) {
p rin t( " E x itin g v ia I n te rru p te d E x c e p tio n " );
>
>
}
p u b lic c la s s ln te r ru p tin g I d io m {
p u b lic s ta tic v o id m a in (S trin g [] args) th row s E x c e p tio n {
if(a rg s .le n g th != 1) {
p rin t(" u s a g e : ja v a ln te rru p tin g Id io m d e la y -in -m S " );
S y s te m .e x it(l);
>
Thread t = new T h r e a d ( n e w B l o c k e d 3 ( ) ) ;
t.s ta rt();
T i m e U n i t . M IL LIS E C O N D S . s l e e p ( n e w ln t e g e r ( a r g s [ 0 ] ) );
t.in te rru p t();
>
> /* O u t p u t : ( S a m p le )
N e e d sC le a n u p 1
S le e p in g
N e e d sC le a n u p 2
C a lc u la tin g
F in is h e d tim e-con su m in g o p e r a tio n
C l e a n i n g up 2
C le a n in g up 1
N e e d sC le a n u p 1
S le e p in g
C le a n in g up 1
E x itin g v ia I n te rru p te d E x c e p tio n
*///:~
wait() и notifyAII()
Метод w a i t ( ) позволяет дождаться изменения некоторого условия, неподконтрольного
текущему методу. Часто это условие изменяется другой задачей. Программа не должна
«крутиться» в цикле, проверяя условие внутри задачи; такое поведение называется
активным ожиданием и обычно приводит к неэффективному расходованию ресурсов
процессора. По этой причине w a i t ( ) приостанавливает задачу, ожидая изменения не
которого условия, и только при B b i 3 0 B e n o t i f y ( ) или n o t i f y A l l ( ) (который сообщает,
что произошло нечто интересное) задача активизируется и проверяет изменения. Таким
образом, w a i t ( ) предоставляет механизм синхронизации действий между задачами.
Важно понять, что метод s l e e p ( ) не освобождает объект блокировки — как и метод
y i e l d ( ) . С другой стороны, когда задача входит в w a i t ( ) внутри метода, выполнение
Рассмотрим простой пример. Автомат WaxOMatic.java содержит два процесса: один на
носит воск на кузов машины ( C a r ) , а другой полирует его. Задача полировки не может
быть выполнена, пока не будет завершена задача нанесения, а задача нанесения должна
дождаться завершения полировки, прежде чем наносить другой слой воска. И WaxOn
и W a x O f f используют объект С а г , который использует методы w a i t ( ) и n o t i f y A l l ( ) для
приостановки и активизации задач, ожидающих изменения внешнего условия:
//: co n cu rre n cy / w a x o m a tic/ W a x O M a tic. ja v a
// Простейшее в з а и м о д е й с т в и е между з а д а ч а м и ,
package co n cu rre n cy .w a x o m a tic;
im p o rt ja v a .u til.c o n c u rr e n t.* ;
im p o rt s ta tic n e t.m in d v ie w .u til.P rin t.* ;
c la s s Car {
p riv a te b o o l e a n waxOn = f a l s e ;
p u b lic s y n ch ro n ize d v o id waxed() {
waxOn = t r u e ; // Г о т о в о к по л и р о в к е
n o tify A ll();
>
p u b lic sy n ch ro n ize d v o id b u ffe d () {
waxOn = f a l s e ; // Г о т о в о к нан есен ию с л о я в о с к а
n o tify A ll();
>
p u b lic sy n ch ro n ize d v o id w a itF o rW a x in g ()
throw s In te rru p te d E x c e p tio n {
w h ile ( w a x O n == f a l s e )
w a it( );
}
p u b lic sy n ch ro n ize d v o id w a itF o rB u ffin g ( )
th ro w s In te rru p te d E x c e p tio n {
w h ile ( w a x O n == t r u e )
w a it();
}
>
c la s s WaxOn i m p l e m e n t s R u nnable {
p riv a te Car ca r;
p u b lic W a x O n (C a r с ) { ca r = с; }
p u b lic v o id run() {
try {
w h ile (!T h r e a d .in te r ru p te d ()) {
p r i n t n b ( " W a x Оп! ");
T im e U n it.M IL L IS E C O N D S .sle e p (2 0 0 );
c a r,w a x e d ();
c a r .w a it F o r B u f f in g ( );
>
продолжение &
960 Глава 21 • Параллельное выполнение
} catch(InterruptedException e) {
print("Exiting via interrupt");
}
print("Ending Wax On task");
>
Runnable.Его метод run() должен вызывать notifyAll() для первой задачи после
истечения некоторого количества секунд, чтобы первая задача могла вывести со
общение. Протестируйте свои классы с использованием Executor.
22. (4) Реализуйте пример активного ожидания. Одна задача приостанавливается на
некоторое время, после чего устанавливает флаг. Вторая задача следит за флагом
в цикле while (активное ожидание), и когда флаг принимает значение true, сбрасывает
его в состояние false и выводит информацию об изменении на консоль. Обратите
внимание, сколько времени программа проводит в активном ожидании, и создайте
вторую версию программы, использующую w ait() вместо активного ожидания.
Пропущенные сигналы
При координации двух потоков с использованием notify()/wait{) или notifyAll()/
wait() возможна потеря сигналов. Допустим, поток Tl оповещает T2, а два потокаре-
ализуются по следующей (ошибочной) схеме:
Tl:
synchronized(sharedMonitor) {
кнастройка условия для T2>
sharedMonitor.notify ();
>
T2:
while<someCondition) {
// Точка 1
synchronized(sharedMonitor) {
sharedMonitor.wait();
>
>
<настройкаусло&ия для T2> —действие, которое предотвращает вызов wait() потоком
T2 (если это не было сделано ранее).
Предположим, T2 проверяет условие someCondition и обнаруживает, что оно истинно.
В точке 1 планировщик потоков может переключиться на Tl. Поток Tl выполняет на
стройку, после чего вызывает notify(). Когда T2 продолжает выполнение, он уже не
успевает осознать, что условие изменилось, и слепо входит в wait(). Вызов notify()
будет пропущен, и T2 будет неопределенно долго ожидать сигнала, который уже был
отправлен, что приведет к взаимной блокировке.
Проблема решается предотвращением ситуации гонки по переменной someCondition.
Вот как выглядит правильный подход для T2:
synchronized(sharedMonitor) {
while(someCondition)
sharedMonitor.wait();
}
Если теперь Tl выполняется в первую очередь, то при возвращении управления T2
определит, что условие изменилось, и не будет входить в wait(). И наоборот, если
сначала выполняется T2, произойдет вход в wait() с последующей активизацией со
стороны Tl. Таким образом, возможность пропуска сигнала исключается.
*
Взаимодействие между задачами
V
963
*
notify() и notifyAII()
Так как формально для одного объекта Саг более одной задачи может находиться в ожи
дании wait(), вместо простого n o t i f y ( ) 6 ^ e T безопаснее вызвать notifyAll(). Однако
структура приведенной выше программы такова, что в wait( ) будет находиться только
одна задача, поэтому вы можете использовать notify() вместо notifyAll().
Использование notify() вместо notifyAll() является оптимизацией. Только одна
задача из многих кандидатов, ожидающих по объекту блокировки, будет активизи
рована вызовом notify(), так что при попытке использования notify() вы должны
быть уверены в том, что активизируется именно та задача, которая вам нужна. Кроме
того, для использования notify() все задачи должны ожидать по одному условию.
При использовании notify() изменение условия должно быть актуально только для
одной задачи. Наконец, эти ограничения всегда должны быть истинны для всех воз
можных субклассов. Если какие-либо из этих условий не выполняются, используйте
notifyAll() вместо notify().
class Blocker {
synchronized void waitingCall() {
try {
while(!Thread.interrupted()) {
wait();
Systera.out.print(Thread.currentThread() + " ");
>
} catch(InterruptedException e) {
// Допустимый способ выхода
}
>
synchronized void prod() { notify(); >
synchronized void prodAll() { notifyAll(); }
>
class Task implements Runnable {
static Blocker blocker = new Blocker();
public void run() { blocker.waitingCall(); }
>
class Task2 implements Runnable {
// Отдельный объект Blocker:
static Blocker blocker = new Blocker();
public void run() { blocker.waitingCall(); >
>
продолжение &
964 Глава 21 • Параллельное выполнение
Производители и потребители
В качестве примера заглянем в ресторан, в котором на всех один шеф-повар и один
официант. Официант должен ждать, пока шеф-повар приготовит кушанье. Как только
шеф-повар заканчивает стряпню, он подает официанту сигнал, и последний доставляет
блюдо до места и возвращается к ожиданию. Это великолепный пример кооперации
задач: шеф-повар представляет из себя производителя, а официант является потреби
телем. Задачи должны согласовать свои действия друг с другом при приготовлении
и потреблении еды, а система должна корректно завершить свою работу. Вот как вы
глядит ситуация, смоделированная в коде:
966 Глава 21 • Параллельное выполнение
//: concurrency/Restaurant.java
// Взаимодействие потоков в модели "производитель-потребитель"
import java.util.concurrent.*;
import static net.mindview.util.Print.*;
class Meal {
private final int orderNum;
public Meal(int orderNum) { this.orderNum = orderNum; )
public String toString() { return "Meal " + orderNumj >
>
class WaitPerson implements Runnable {
private Restaurant restaurant;
public WaitPerson(Restaurant r) { restaurant = r; >
public void run() {
try {
while(lThread.interrupted()) {
synchronized(this) {
while(restaurant.meal == null)
wait(); // ... Пока повар приготовит блкщо
}
print("Waitperson got " + restaurant.meal);
synchronized(restaurant.chef) {
restaurant.meal = null;
restaurant.chef.notifyAll(); // Готово для следующего блюда
>
>
} catch(InterruptedException e) {
print("WaitPerson interrupted");
>
>
>
class Chef implements Runnable {
private Restaurant restaurant;
private int count = 9;
public Chef(Restaurant r) { restaurant = r; }
public void run() {
try {
while(lThread.interrupted()) {
synchronized(this) {
while(restaurant.meal != null)
wait(); // ... Когда заберут блюдо
>
if(++count == 10) {
print("Out of food, closing");
restaurant.exec.shutdownNow();
>
printnb("Order up! ");
synchronized(restaurant.waitPerson) {
restaurant.meal = new Meal(count);
restaurant.waitPerson.notifyAll();
>
T imeUnit.MILLISECONDS.sleep(i09);
>
) catch(InterruptedExcteption e) {
print("Chef interrupted");
>
>
>
Взаимсщейсгвие между задачами 967
Она гарантирует, что прежде чем вы выберетесь из цикла ожидания, условие будет
выполнено, а в том случае, если вы были оповещены о чем-то не имеющем отношения
к условию (что может случиться в варианте метода notifyAll()) или условие измени
лось еще до того, как вы полностью покинули цикл ожидания, гарантирован возврат
к ожиданию.
Заметьте, что вызов notifyAll() должен сначала захватить объект блокировки
o6beKTawaitPerson. Вызов метода wait() в методе WaitPerson.run() автоматически
освобождает блокируемый объект, значит, это возможно. Благодаря тому, что для
вызова метода notifyAll() требуется владеть объектом блокировки, гарантируется,
что два потока, пытающиеся вызвать notifyAll () по одному объекту, не «перебегут
друг другу дорогу».
Оба метода run() спроектированы с расчетом на корректное завершение, для чего весь
вызов run() заключается в блокТгу(). Блок catch закрывается сразу же после закрыва
ющей фигурной скобки метода гип(),так что если задача получает InterruptedException,
она немедленно завершается после перехвата исключения.
В коде Chef обратите внимание на то, что после вызова shutdownNow() можно просто
вернуть управление из гип();собственно, обычно именно так и следует поступать. Тем
не менее наше решение немного интереснее. Помните, что shutdownNow() отправляет
interrupt() всем задачам, запущенным ExecutorService. Но в случае Chef задача не за
вершается немедленно после получения interrupt(), потому что прерываниевозбуж-
дает InterruptedException только тогда, когда задача пытается войти в (прерываемую)
блокирующую операцию. Таким образом, сначала выводится сообщение «Order up!»,
а затем возбуждается исключение InterruptedException, когда Chef пытается вызвать
sleep( ). Если убрать вызов sleep(), задача доберется до начала цикла run( ) и завершится
из-за проверки Thread.interrupted() без возбуждения исключения.
В предыдущем примере используется только одна точка, в которой одна задача со
храняет объект, чтобы другая задача могла позднее этим объектом воспользоваться.
Однако в типичной реализации «производитель-потребитель» для хранения произ
водимых и потребляемых объектов используется очередь FIFO. Такие очереди более
подробно рассматриваются позже в этой главе.
24. (1) Решите задачу сотрудничества с одним производителем и одним потребителем,
используя методы wait() и notifyAll(). Производитель не должен переполнить
буфер потребителя-получателя, что может произойти в том случае, если первый
работает быстрее второго. Если же потребитель действует оперативнее, он не дол
жен считывать одни и те же данные по несколько раз. Не стройте никаких предпо
ложений в отношении относительной скорости их работы.
25. (1) В классе Chef из примера Restaurant.java верните управление (return) из run() по
сле вызова shutdownNow(). Обратите внимание на различия в поведении программы.
26. (8) Добавьте в пример Restaurant.java класс BusBoy («уборщик»). После того как блю
до будет доставлено, класс WaitPerson должен оповестить BusBoy о необходимости
убрать.
Взаимодействие между задачами 969
class Car {
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
private boolean waxOn = false;
public void waxed() {
lock.lock();
try {
waxOn = true; // Готово к полировке
condition.signalAll();
> finally {
lock.unlock();
}
}
public void buffed() {
lock.lock();
try {
waxOn = false; // Готово к нанесению слоя воска
condition.signalAll();
> finally {
lock.unlock();
}
>
public void waitForWaxing() throws InterruptedException {
lock.lock();
try {
while(waxOn == false)
condition.await();
> finally {
lock.unlock();
>
>
public void waitForBuffing() throws InterruptedException{
lock.lock();
try {
while(waxOn == true)
продолжение &
970 Глава 21 • Параллельное выполнение
condition.await();
} finally {
lock,unlock();
}
}
>
class WaxOn implements Runnable {
private Car car;
public WaxOn(Car c) { car = c; }
public void run() {
try {
while(!Thread.interrupted()) {
printnb("Wax On! ”);
T imeUn it.MILLISECONDS.sleep(200);
car.waxed();
car.waitForBuffing();
>
} catch(InterruptedException e) {
print("Exiting via interrupt");
>
print("Ending Wax On task");
}
В конструкторе Car один объект Lock производит объект Condition, который исполь
зуется для управления взаимодействиями между задачами. Однако объект Condition
не содержит информации о состоянии процесса, поэтому нам понадобится дополни
тельная логическая переменная waxOn.
Сразу же после каждого вызова lock() должна следовать конструкция try-finaXly,
которая гарантирует, что блокировка будет снята в любом случае. Как и со встроен
ными версиями, задача должна захватить блокировку, прежде чем вызывать await(),
signal() или signalAll().
Следует заметить, что это решение сложнее предыдущего, и сложность в данном слу
чае ничего не дает. Объекты Lock и Condition необходимы только для более сложных
потоковых задач.
27 . (2) Измените пример Restaurantjava так, чтобы в нем явно использовались объекты
Lock и Condition.
Производители-потребители и очереди
Методы wait() и notifyAll() решают проблему взаимодействия задач на довольно
низком уровне, с согласованием каждого взаимодействия. Во многих случаях можно
подняться на более высокий уровень абстракции и решить проблему кооперации за
дач при помощи синхронизированной очереди, которая разрешает выполнять вставку
и удаление только одной задаче. Эта возможность предоставляется интерфейсом java.
util.concurrent.BlockingQueue, который имеет ряд стандартных реализаций. Чаще всего
используется очередь неограниченного размера LinkedBlockingQueue; ArrayBlocking-
Queue имеет фиксированный размер, поэтому количество элементов, которые можно
поместить в очередь до того, как она будет заблокирована, ограниченно.
Эти очереди также приостанавливают задачу-потребителя, если эта задача попытается
получить объект из пустой очереди, и возобновляют выполнение при появлении эле
ментов. Блокирующие очереди способны решить многие задачи куда более простым
и надежным способом, чем wait() и notifyAll().
Ниже приведен простой пример с организацией последовательного выполнения объ
ектов LiftOff. Потребителем является объект LiftOffRunner, который извлекает каж
дый объект LiftOff из очереди BlockingQueue и выполняет его. (То есть он использует
собственный поток, явно вызывая run(), вместо того чтобы порождать новый поток
для каждой задачи.)
//: concurrency/TestBlockingQueues.java
// {RunByHand}
import java.util.concurrent.*;
import java.io.*;
import static net.mindview.util.Print.*;
static void
test(String msg, BlockingQueue<LiftOff> queue) {
print(msg);
LiftOffRunner runner = new LiftOffRunner(queue)j
Thread t = new Thread(runner);
t.start();
for(int i = 0; i < 5j i++)
runner.add(new LiftOff(5));
getkey("Press 'Enter' (" + msg + ")");
t.interrupt();
print("Finished " + msg + " test");
>
public static void main(String[] args) {
test("LinkedBlockingQueue"j // Неограниченный размер
new LinkedBlockingQueue<LiftOff>());
test("ArrayBlockingQueue", // Фиксированный размер
new ArrayBlockingQueue<LiftOff>(3));
test("SynchronousQueue", // Размер 1
new SynchronousQueue<LiftOff>());
Взаимодействие между задачами 973
>
} ///:~
Задачи помещаются в очередь BlockingQueue в main() и извлекаются из BlockingQueue
в LiftOffRunner. Заметьте, что LiftOffRunner может игнорировать проблемы синхро
низации, потому что эти проблемы решаются в BlockingQueue.
28. (3) Измените пример TestBlockingQueues.java и добавьте новую задачу, которая по
мещает LiftOff в BlockingQueue (вместо того, чтобы делать это в main()).
class Toast {
public enum Status { DRY, BUTTERED, JAMMED >
private Status status = Status.DRY;
private final int id;
public Toast(int idn) { id = idn; >
public void butter() { status = Status.BUTTERED; }
public void jam() { status = Status.lAMMED; }
public Status getStatus() { return status; >
public int getId() { return id; }
public String toString() {
return "Toast " + id + ": " + status;
>
}
class ToastQueue extends LinkedBlockingQueue<Toast> {}
print("Toaster off");
>
>
// Нанесение масла:
class Butterer implements Runnable {
private ToastQueue dryQueue, butteredQueue;
public Butterer(ToastQueue dry, ToastQueue buttered) {
dryQueue = dry;
butteredQueue = buttered;
>
public void run() {
try {
while(!Thread.interrupted()) {
// Блокирует до готовности следукхцего тоста:
Toast t = dryQueue.take();
t.butter();
print(t);
butteredQueue.put(t);
}
} catch(InterruptedException e) {
print("Butterer interrupted");
>
print("Butterer off");
>
// Потребление тоста:
class Eater implements Runnable {
private ToastQueue finishedQueue;
private int counter = 0;
public Eater(ToastQueue finished) {
finishedQueue = finished;
}
public void run() {
Взаимодействие между задачами 975
try {
while(!Thread.interrupted()) {
// Блокирует до готовности следующего тоста:
Toast t = finishedQueue.take();
// Проверить, что тосты следуют по порядку,
// г все тосты намазаны джемом:
if(t.getId() != counter++ ||
t.getStatus() != Toast.Status.JAMMED) {
print(”>>>> Error: " + t);
System.exit(l);
} else
print("Chomp! " + t)j
}
} catch(InterruptedException e) {
print("Eater interrupted");
>
print("Eater off");
>
}
public class ToastOMatic {
public static void main(String[] args) throws Exception {
ToastQueue dryQueue = new ToastQueue(),
butteredQueue = new ToastQueue(),
finishedQueue = new ToastQueue();
ExecutorService exec = Executors.newCachedThreadPool();
exec.execute(new Toaster(dryQueue));
exec.execute(new Butterer(dryQueue, butteredQueue));
exec.execute(new 3ammer(butteredQueue, finishedQueue));
exec.execute(new Eater(finishedQueue));
TimeUnit.SECONDS.sleep(5);
exec.shutdownNow();
>
) /* (Execute to see output) *///:~
exec.execute(receiver);
TimeUnit.5EC0NDS.sleep(4);
exec.shutdownNow();
>
} /* Output: (65% match)
Read: A, Read: B, Read: C, Read: D, Read: E, Read: F, Read:
G, Read: H, Read: I, Read: 3, Read: K, Read: L, Read: M,
java.lang.InterruptedException: sleep interrupted Sender
sleep interrupted
java.io.InterruptedIOException Receiver read exception
*/ / / :~
Классы Sender и Receiver представляют задачи, которым требуется взаимодействовать
друг с другом. В классе Sender создается канал PipedWriter, представляющий собой
автономный объект, однако при создании канала PipedReader в классе Receiver необхо
димо указать в конструкторе ссылку на PipedWriter. Sender записывает данные в канал
Writer и бездействует в течение случайно выбранного промежутка времени. Однако
в классе Receiver вы не найдете следов присутствия sleep() или wait (). Когда он про
водит чтение методом read(), то автоматически блокируется при отсутствии данных.
Заметьте, что потоки sender и receiver запускаются из main() после того, как объекты
были полностью сконструированы. Если запускать не полностью сконструированные
объекты, каналы на различных платформах могут демонстрировать несогласованное по
ведение. (Учтите, что решение с BlockingQueue более надежно и просто в использовании.)
Важное различие между PipedReader и обычным вводом-выводом проявляется при
вызове shutdownNow() —объект PipedReader является прерываемым, тогда как, напри
мер, если заменить вызов in.read() на System.in.read(), вызов interrupt() не приведет
к выходу из вызова read().
30. ( 1) Измените пример PipedIO.java, чтобы в нем вместо канала использовалась очередь
BlockingQueue.
Взаимная блокировка
Итак, потоки способны перейти в блокированное состояние, а объекты могут обладать
синхронизированными методами, которые запрещают использование объекта до тех
пор, пока не будет снята блокировка. Вероятна ситуация, в которой один поток ожидает
другой поток, тот, в свою очередь, ждет освобождения еще одного потока и так далее,
пока эта цепочка не замыкается на поток, который ожидает освобождения первого по
тока. Получается замкнутый круг потоков, которые дожидаются освобождения друг
друга и никто не может двинуться первым. Такая ситуация называется тупиком, или
взаимной блокировкой (deadlock).
Если вы запускаете программу и она незамедлительно оказывается в положении вза
имной блокировки, вы сразу понимаете, что у вас проблемы и их следует найти. По-
настоящему неприятна ситуация, когда ваша программа по всем признакам работает
прекрасно, но тем не менее потенциально способна войти во взаимную блокировку.
В таких ситуациях ничто не указывает на возможность возникновения взаимоблокиров
ки, и такая возможность «тихо» присутствует в программе, пока нежданно-негаданно
978 Глава 21 • Параллельное выполнение
//: concurrency/Philosopher.java
// Обедающий философ
import java.util.concurrent.*;
Взаимная блокировка 979
import java.util.*;
import static net.mindview.util.Print.*;
int ponder = 5;
if(args.length > 0)
ponder = lnteger.parselnt(args[0])j
int size = 5;
if(args.length > 1)
size = Integer.parseInt(args[l]);
ExecutorService exec = Executors.newCachedThreadPool();
Chopstick[] sticks = new Chopstick[size];
for(int i = 0; i < size; i++)
sticks[i] = new Chopstick();
for(int i = 0; i < size; i++)
exec.execute(new Philosophen(
sticks[i], sticks[(i+l) % size]j i, ponder));
if(args.length == 3 && args[2].equals("timeout"))
TimeUnit.SEC0NDS.sleep(5);
else {
System.out.println("Press 'Enter' to quit")j
System.in.read();
>
exec.shutdownNow();
>
} /* (Execute to see output) *///:~
CountDownLatch
Класс предназначен для синхронизации одной или нескольких задач, ожидающих
завершения набора операций, выполняемых другими задачами.
Объект CountDownLatch получает исходное значение счетчика, и любая задача, вызыва
ющая await() для этого объекта, блокируется до тех пор, пока счетчик не уменьшится
до нуля. Другие задачи могут вызывать countDown() для объекта, чтобы уменьшить
счетчик, —предположительно до момента, когда задача завершит свою работу. Объект
CountDownLatch спроектирован для «одноразового» использования, то есть его счетчик
невозможно сбросить. Если вам нужна версия с возможностью сброса, используйте
CyclicBarrier.
п родол ж ен и е *3>
984 Глава 21 • Параллельное выполнение
// Ожидание по CountDownLatch:
class WaitingTask implements Runnable {
private static int counter = 0;
private final int id = counter++;
private final CountDownLatch latch;
WaitingTask(CountDownLatch latch) {
this.latch = latch;
>
public void run() {
try {
latch.await();
print("Latch barrier passed for " + this);
) catch(InterruptedException ex) {
print(this + " interrupted");
}
>
public String toString() {
return String.format("WaitingTask %l$-3d ", id);
}
>
public class CountDownLatchDemo {
static final int SIZE = 100;
public static void main(String[] args) throws Exception {
ExecutorService exec = Executors.newCachedThreadPool();
// Все должны совместно использовать один объект CountDownLatch:
CountDownLatch latch = new CountDownLatch(SIZE);
for(int i = 0; i < 10; i++)
exec.execute(new WaitingTask(latch));
for(int i = 0; i < SIZE; i++)
exec,execute(new TaskPortion(latch));
print("Launched all tasks");
exec.shutdown(); // Выход при завершении всех задач
>
> /* (Execute to see output) *//f :~
Объект TaskPortion ожидает в течение случайного времени, имитируя завершение
части задачи, а объект WaitingTask моделирует часть системы, которая должна до
ждаться завершения исходной части задачи. Все задачи работают с одним объектом
CountDownLatch, который определяется в main().
32. (7) Используйте CountDownLatch для решения задачи корреляции результатов отдель
ных входов в OrnamentalGarden.java. Удалите лишний код из новой версии примера.
CyclicBarrier
Класс CyclicBarrier используется в тех ситуациях, когда вы хотите создать группу
параллельно работающих задач, а затем дождаться их завершения, прежде чем пере
ходить к следующему шагу (отдаленное подобие join()). Все задачи останавливаются
у «барьера», чтобы затем дружно двинуться вперед. Класс CyclicBarrier очень похож
на CountDownLatch, но класс CountDownLatch предназначен для «одноразового» исполь
зования, а объекты CyclicBarrier могут использоваться снова и снова.
В начале своего знакомства с компьютерами я увлекался компьютерным моделиро
ванием, и параллельное выполнение было ключевым фактором, который делал их
возможным. Самая первая программа, написанная мной1, моделировала лошадиные
скачки, была написана на BASIC и называлась (из-за ограничений на имена файлов)
HOSRAC.BAS. Ниже приведена объектно-ориентированная, потоковая версия этой про
граммы с использованием CyclicBarrier.
//: concurrency/HorseRace.java
// Использование CyclicBarrier.
import java.util.concurrent.*;
import java.util.*;
import static net.mindview.util.Print.*;
1 Тогда я учился в средней школе; в нашем классе был установлен телетайп ASR-33 с 110-бодо-
вым акустическим модемом для связи с НР-1000.
986 Глава 21 • Параллельное выполнение
DelayQueue
Неограниченная очередь BlockingQueue объектов, реализующих интерфейс Delayed.
Объект может быть извлечен из очереди только по истечении назначенной задержки.
Очередь сортируется таким образом, что в ее начале размещается объект, с момента
истечения задержки которого прошло наибольшее время. Если ни у одного элемента
задержка не прошла, начальный элемент отсутствует, и вызов poll() вернет null (по
этой причине в очередь нельзя помещать элементы null).
В следующем примере объекты Delayed сами являются задачами, а DelayedTaskConsumer
извлекает из очереди самую «неотложную» задачу (с наибольшим сроком истечения)
и запускает ее. Таким образом, DelayQueue представляет собой разновидность приори
тетной очереди.
//: concurrency/DelayQueueDemo.java
import java.util.concurrent.*;
import java.util.*;
import static java.util.concurrent.TimeUnit.*;
import static net.mindview.util.Print.*;
* п р о до л ж ен и е d>
988 Глава 21 • Параллельное выполнение
>
> /* Output:
[128 ] Task 11 [200 ] Task 7 [429 ] Task 5 [520 ] Task 18
[555 ] Task1 [961 ] Task 4 [998 ] Task 16 [1207] Task 9
[1693] Task 2 [1809] Task 14 [1861] Task 3 [2278] Task 15
[3288] Task 10 [3551] Task 12 [4258] Task 0 [4258] Task 19
[4522] Task 8 [4589] Task 13 [4861] Task 17 [4868] Task 6
(0:4258) (1:555) (2:1693) (3:1861) (4:961) (5:429) (6:4868)
(7:200) (8:4522) (9:1207) (10:3288) (11:128) (12:3551)
(13:4589) (14:1809) (15:2278) (16:998) (17:4861) (18:520)
(19:4258) (20:5000)
[5000] Task 20 Calling shutdownNow()
Finished DelayedTaskConsumer
*///:~
PriorityBlockingQueue
По сути это приоритетная очередь с блокирующими операциями выборки. В следу
ющем примере объекты приоритетной очереди представляют собой задачи, которые
990 Глава 21 • Параллельное выполнение
//: concurrency/PriorityBlockingQueueDemo.java
import java.util.concurrent.*;
import java.util.*;
import static net.mindview.util.Print.*;
public void
repeat(Runnable event, long initialDelay, long period) {
scheduler.scheduleAtFixedRate(
event, initialDelay, period, TimeUnit.MILLISECONDS);
>
class LightOn implements Runnable {
public void run() {
// Поместите сюда код управления оборудованием,
// выполняющий непосредственное включение света.
System.out.println("Tunning on lights");
light = true;
>
>
class LightOff implements Runnable {
public void run() {
// Поместите сюда код управления оборудованием,
// выполняющий выключение света.
System.out.println("Turning off lights");
light = false;
>
>
class WaterOn implements Runnable {
public void run() {
// Здесь размещается код управления оборудованием.
System.out.println("Turning greenhouse water on");
water = true;
>
>
class WaterOff implements Runnable {
public void run() {
// Здесь размещается код управления оборудованием.
System.out.println("Turning greenhouse water off");
water = false;
>
>
class ThermostatNight implements Runnable {
public void run() {
// Здесь размещается код управления оборудованием.
System.out.println("Thermostat to night setting");
setThermostat ("Night'');
>
}
class ThermostatDay implements Runnable {
public void run() {
// Здесь размещается код управления оборудованием.
System.out.println("Thermostat to day setting");
setThermostat("Day");
}
>
class Bell implements Runnable {
public void run() { System.out.println("Bing!"); }
>
class Terminate implements Runnable {
public void run() {
System.out.println("Terminating");
scheduler.shutdownNow();
// Для этого задания необходимо запустить отдельную задачу,
// так как планировщик завершен:
п р о до л ж ен и е ^>
994 Глава 21 • Параллельное выполнение
new Thread() {
public void run() {
for(DataPoint d : data)
System.out.println(d);
>
}.start();
>
}
// Новая возможность: сбор данных
static class DataPoint {
final Calendar time;
final float temperature;
final float humidity;
public DataPoint(Calendar d, float temp, float hum) {
time = d;
temperature = temp;
humidity = hum;
}
public String toString() {
return time.getTime() +
String.format(
" temperature: %l$.lf humidity: %2$.2f",
temperature, humidity);
}
>
private Calendar lastTime = Calendar.getInstance();
{ // Регулировка даты до получаса
lastTime.set(Calendar.MINUTE, 39);
lastTime.set(Calendar.SECOND, 00);
>
private float lastTemp = 65.0f;
private int tempDirection = +l;
private float lastHumidity = 50.0f;
private int humidityDirection = +1;
private Random rand = new Random(47);
List<DataPoint> data = Collections.synchronizedList(
new ArrayList<DataPoint>());
class CollectData implements Runnable {
public void run() {
System.out.println("Collecting data");
synchronized(GreenhouseScheduler.this) {
// Имитировать более длинный интервал:
lastTime.set(Calendar.MINUTE,
lastT ime.get(Calendar.MINUTE) + 30);
// Направление меняется в 1 из 5 случаев:
if(rand.nextInt(5) == 4)
tempDirection = -tempDirection;
// Сохранение предыдущего значения:
lastTemp = lastTemp +
tempDirection * (1.0f + rand.nextFloat());
if(rand.nextInt(5) == 4)
humidityDirection = -humidityDirection;
lastHumidity = lastHumidity +
humidityDirection * rand.nextFloat();
// Calendar необходимо клонировать, в противном случае все
// объекты DataPoint будут содержать ссылки на то же
// значение lastTime. Для простейших объектов - таких,
// как Calendar, - достаточно вызова clone().
Новые библиотечные компоненты 995
data.add(new DataPoint((Calendar)lastTime.clone(),
lastTemp, lastHumidity));
}
>
>
public static void main(String[] args) {
GreenhouseScheduler gh = new GreenhouseScheduler();
gh.schedule(gh.new Terminate(), 5000);
// Former "Restart" class not necessary:
gh.repeat(gh.new Bell(), 0, 1000);
gh.repeat(gh.new ThermostatNight(), 0, 2000);
gh.repeat(gh.new LightOn(), 0, 200);
gh.repeat(gh.new LightOff(), 0, 400);
gh.repeat(gh.new WaterOn(), 0, 600);
gh.repeat(gh.new WaterOff(), 0, 800);
gh.repeat(gh.new ThermostatDay(), 0, 1400);
gh.repeat(gh.new CollectData(), 500, 500);
>
) /* (Execute to see output) * / / / : ~
Эта версия изменяет структуру кода и добавляет новую возможность: сбор данных
температуры и влажности. Объект DataPoint содержит и выводитодин элементданных,
а планируемая задача CollectData генерирует моделируемые данные и добавляет их
в List<DataPoint> в Greenhouse при каждом запуске.
Обратите внимание на ключевые слова volatile и synchronized; они предотвращают
нежелательное взаимодействие задач. Все методы List, хранящие DataPoint, синхрони
зируются с использование метода synchronizedList() из библиотеки java.util.CoUections
при создании List.
33. (7) Измените пример GreenhouseScheduter.java так, чтобы в нем использовался класс
DelayQueue вместо ScheduledExecutor.
Semaphore
Обычная блокировка (из concurrent.locks или встроенная блокировка synchronized) раз
решает обращаться к ресурсу в любой момент времени только одной задаче. Семафор
со счетчиком разрешает n задачам обращаться к ресурсу одновременно. Также можно
представить, что семафор выдает «разрешения» на использовацие ресурсов, хотя на
самом деле объекты разрешений не используются.
В качестве примера рассмотрим концепцию пула объектов, который управляет огра
ниченным набором объектов и позволяет захватить объект для использования, а затем
вернуть обратно после завершения работы с ним. Такая функциональность может быть
инкапсулирована в обобщенном классе.
/./: concurrency/Pool.java
// Использование семафора с пулом для ограничения
// количества задач, которые могут использовать ресурс,
import java.util.concurrent.*;
import java.util.*;
В методе main() создается объект Pool для объектов Fat, и группа задач CheckoutTask
начинает работать с Pool. Затем поток main() начинает получать объекты Fat, не воз
вращая их в пул. После того как в пуле кончатся объекты, Semaphore запретит получение
новых объектов. Метод run( ) объекта blocked блокируется, и через две секунды следует
вызов cancel(). Pool игнорирует лишние возвраты объектов.
Этот пример полагается на то, что клиент Pool будет тщательно соблюдать правила
и добровольно возвращать использованные объекты; самое простое решение... когда
оно работает. Тем не менее если у вас нет полной уверенности, в книге «Thinking in
Patterns» {remmMiruiViemnet) исследуются дополнительные возможности управления
объектами, выделенными из пулов.
Exchanger
Exchangei— барьер, который меняет местами объекты двух задач. Когда задача входит
в барьер, она использует один объект, а при выходе его место занимает объект, ранее
удерживавшийся другой задачей. Как правило, объекты Exchanger используются тогда,
когда одна задача выполняет затратные операции создания объектов, а другая задача
эти объекты потребляет; таким образом, создание новых объектов может происходить
параллельно с их потреблением.
Чтобы использовать класс Exchanger, мы создадим задачи производителя и потребителя,
которые посредством обобщений и генераторов будут работать с любыми видами объ
ектов, и применим их к классу Fat. ExchangerProducer и ExchangerConsumer используют
List<T> в качестве заменяемого объекта; каждая задача содержит объект Exchanger
Новые библиотечные компоненты 999
для List<T>. При вызове Exchanger.exchange() задача блокируется до тех пор, пока
задача-партнер не вызовет свой метод exchange(); когда оба метода exchange() будут
завершены, объекты List<T> меняются местами:
//: concurrency/ExchangerDemo.java
import java.util.concurrent.*;
import java.util.*;
import net.mindview.util.*;
Моделирование
Одна из самых интересных и творческих областей параллельного программирования —
компьютерное моделирование. В параллельной модели каждый компонент может быть
представлен отдельной задачей, что существенно упрощает программирование. Многие
видеоигры и компьютерные анимации реализуются на базе моделей, а приведенные ранее
примеры HorseRace.java и GreenhouseScheduler.java также могут рассматриваться как модели.
Модель кассира
Эта классическая модель может использоваться для представления любых ситуаций,
в которых объекты появляются случайным образом и обслуживаются ограниченным
количеством серверов за случайное время. Построенная модель позволяет определить
идеальное количество серверов.
В следующем примере каждый клиент банка обслуживается за некоторое время,
которое определяется случайным образом для каждого клиента. При этом заранее не
Моделирование 1001
известно, сколько клиентов появится в каждый интервал; эта величина также опре
деляется случайным образом.
//: concurrency/BankTellerSimulation.java
// Использование очередей и многопоточной модели.
// {Angs: 5}
import java.util.concurrent.*;
import java.util.*;
workingTellers.add(teller);
>
public void adjustTellerNumber() {
// Система управления - изменяя числа, можно выявить
// проблемы стабильности в управляющем механизме.
// Если очередь слишком длинная, добавить кассира:
if(customers.size() / workingTellers.size() > 2) {
// Если кассиры отдыхают или занимаются другим делом,
// вернуть одного:
if(tellersDoingOtherThings.size() > 0) {
Teller teller = tellersDoingOtherThings.remove();
teller.serveCustomerLine();
workingTellers.offer(teller);
return;
>
// Иначе создать (принять на работу) нового кассира
Teller teller = new Teller(customers);
exec.execute(teller);
workingTellers.add(teller);
return;
>
// Если очередь достаточно короткая, убрать кассира:
if(workingTellers.size() > 1 &&
customers.size() / workingTellers.size() < 2)
reassignOneTeller();
// Если очереди нет, достаточно одного кассира:
if(customers.size() == 0)
while(workingTellers.size() > 1)
reassignOneTeller();
>
// Отправить кассира на другую работу или отдых:
private void reassignOneTeller() {
Teller teller = workingTellers.poll();
teller.doSomethingElse();
tellersDoingOtherThings.offer(teller);
}
public void run() {
try {
while(!Thread.interrupted()) {
TimeUnit.MILLISECONDS.sleep(adjustmentPeriod);
adjustTellerNumber();
System.out.print(customers + " { ");
for(Teller teller : workingTellers)
System.out.print(teller.shortString() + " ");
System.out.println("}");
>
) catch(InterruptedException e) {
System.out.println(this + "interrupted");
>
5ystem.out.println(this + "terminating");
>
public String toString() { return "TellerManager "; >
>
public class BankTellerSimulation {
static final int MAX_LINE_SIZE = 50;
static final int AD3USTMENT_PERI0D = 1000;
public static void main(String[] args) throws Exception { _
продолжение тУ
1004 Глава 21 • Параллельное выполнение
Объекты Customer очень просты —они содержат всего одно поле final int. Так как эти
объекты никогда не изменяются, они доступны только для чтения, а следовательно, не
требуют синхронизации или применения volatile. Кроме того, каждая задача Teller
удаляет из входной очереди по одному объекту Customer и работает с ним до заверше
ния, так что к Customer в любой момент времени будет обращаться только одна задача.
Объект CustomerLine представляет одну очередь, в которой клиенты ожидают обслужи
вания объектом Teller. Он представляет собой контейнер ArrayBlockingQueue с методом
toString(), который выводит результат в нужном формате.
Моделирование ресторана
Эта модель расширяет простой пример Restaurant.java, приведенный ранее в этой гла
ве. Он добавляет новые компоненты модели —такие, как заказы (Order) и тарелки
(Plate), — и повторно использует классы меню из главы 19.
BlockingQueue<Plate> filledOrders =
new LinkedBlockingQueue<Plate>Q;
public WaitPerson(Restaurant rest) { restaurant = rest; )
public void placeOrder(Customer cust, Food food) {
try {
// Вообще говоря, этот вызов не должен блокироваться,
// потому что LinkedBlockingQueue не имеет
// ограничений по размеру:
restaurant.orders.put(new Order(cust, this, food));
) catch(InterruptedException e) {
print(this + " placeOrder interrupted");
>
>
public void run() {
try {
while(!Thread.interrupted()) {
// Блокируется, пока блюдо не будет готово
Plate plate = filledOrders.take();
print(this + "received " + plate +
" delivering to " +
plate.getOrde r().getCustomer());
plate.getOrder().getCustomer().deliver(plate);
>
) catch(InterruptedException e) {
print(this + " interrupted");
>
print(this + " off duty");
>
public String toString() {
return "WaitPerson " + id + " ";
>
* ///:-
В этом примере следует обратить внимание на управление сложностью с применением
очередей для взаимодействия между задачами. Этот прием сильно упрощает парал
лельное программирование за счет инверсии управления: задачи не взаимодействуют
друг с другом напрямую. Вместо этого они отправляют объекты друг другу через
очереди. Задача-получатель обрабатывает объект, рассматривая его как сообщение
(вместо того, чтобы возиться с сообщением, «наложенным» на объект). Если вы будете
следовать этой схеме там, где это возможно, вероятность того, что построенная вами
параллельная система будет надежной, существенно увеличится.
36. ( 10) Измените пример RestaurantWithQueues.java так, чтобы на каждый стол создавался
один объект бланка заказа OrderTicket. Замените order на orderTicket и добавьте
класс Table, представляющий стол с несколькими посетителями.
Распределение работы
Следующая модель связывает воедино многие концепции этой главы. Рассмотрим
гипотетическую роботизированную линию для сборки машин. Каждый объект Car
создается за несколько этапов: сначала строится рама, затем устанавливается двигатель,
приводной механизм и колеса.
//: concurrency/CarBuilder.java
// Сложный пример взаимодействия задан,
import java.util.concurrent.*;
import java.util.*;
import static net.mindview.util.Print.*;
class Car {
private final int id;
private boolean
engine = false, driveTrain = false, wheels = false;
public Car(int idn) { id = idn; )
// Пустой объект Car:
public Car() { id = -1; >
public synchronized int getId() { return id; }
public synchronized void addEngine() { engine = true; }
public synchronized void addDriveTrain() {
driveTrain = true;
>
public synchronized void addWheels() { wheels = true; }
public synchronized String toString() {
return "Car " + id + " [" + " engine: " + engine
+ " driveTrain: " + driveTrain
+ " wheels: " + wheels + " ]";
>
>
п родолж ен и е &
class CarQueue extends LinkedBlockingQueue<Car> {}
1010 Глава 21 • Параллельное выполнение
exec.execute(new ChassisBuilder(chassisQueue));
TimeUnit.SECONDS.sleep(7)j
exec.shutdownNow();
>
} /* (Execute to see output) *///:~
Объекты Саг перемещаются из одного места в другое через очередь CarQueue —разно
видность LinkedBlockingQueue. Объект ChassisBuilder создает «минимальную» версию
Car и помещает ее в CarQueue. ОбъектАэБетЫег получает Car из CarQueue и привлекает
объекты Robot для работы над ним. Использование CyclicBarrier позволяет Assembler
дождаться, пока все объекты Robot завершат свою работу; в этот момент Car помещается
в выходную очередь CarQueue для перемещения к следующей операции. Потребителем
последнего объекта CarQueue является объект Reporter, который просто выводит Саг,
чтобы продемонстрировать, что все задачи были выполнены.
Объекты Robot объединяются в пул, и когда возникает необходимость в выполнении
операции, из пула извлекается соответствующий объект. После завершения операции
Robot возвращается в пул.
Оптимизация
В библиотеку Java SE5 java.util.concurrent включены классы, предназначенные для по
вышения производительности. При просмотре библиотеки concurrent трудно сразу
понять, какие классы предназначены для повседневного использования (как, напри
мер, BlockingQueue), а какие существуют только для улучшения производительности.
В этом разделе рассматриваются некоторые проблемы быстродействия и классы,
предназначенные для оптимизации.
(double)lockTime/(double)synchTime);
}
} |* Output: (75% match)
synchronized: 244919117
Lock: 939098964
Lock/synchronized = 3.834
*///:~
1 Брайан Гетц оказал большую помощь, объясняя мне эти вопросы. Дополнительная информа
ция об измерении производительности приведена по адресу www.ibm.com/developerworks/
library/j-jtp 12214.
.0 1 6 Глава 21 • Параллельное выполнение
value.getAndAdd(рге Loaded[i]);
if(++i >= SIZE)
index.set(0);
>
public long read() { return value.get(); }
}
public class SynchronizationComparisons {
static BaseLine baseLine = new BaseLine();
static SynchronizedTest synch = new SynchronizedTest();
static LockTest lock = new LockTest();
static AtomicTest atomic = new AtoraicTest();
static void test() {
Cycles : 50000
BaseLine : 20966632
synchronized : 24326555
Lock : 53669950
Atomic : 30552487
synchronized/BaseLine : 1.16
Lock/BaseLine : 2.56
Atomic/BaseLine : 1.46
synchronized/Lock : 0.45
synchronized/Atomic : 0.79
Lock/Atomic : 1.76
Cycles : 100000
Оптимизация 1019
BaseLine 41512818
synchronized 43843003
Lock 87430386
Atomic 51892350
synchronized/BaseLine : 1.06
Lock/BaseLine : 2.11
Atomic/BaseLine : 1.25
synchronized/Lock : 0.50
synchronized/Atomic : 0.84
Lock/Atomic : 1.68
Cycles 200000
BaseLine 80176670
synchronized 5455046661
Lock 177686829
Atomic 101789194
synchronized/BaseLine : 68.04
Lock/BaseLine : 2.22
Atomic/BaseLine : 1.27
synchronized/Lock : 30.70
synchronized/Atomic : 53.59
Lock/Atomic : 1.75
Cycles 400000
BaseLine 160383513
synchronized 780052493
Lock 362187652
Atomic 202030984
synchronized/BaseLine : 4.86
Lock/BaseLine : 2.26
Atomic/BaseLine : 1.26
synchronized/Lock : 2.15
synchronized/Atomic : 3.86
Lock/Atomic : 1.79
Cycles 800000
BaseLine 322064955
synchronized 336155014
Lock 704615531
Atomic 393231542
synchronized/BaseLine : 1.04
Lock/BaseLine : 2.19
Atomic/BaseLine : 1.22
synchronized/Lock : 0.47
synchronized/Atomic : 0.85
Lock/Atomic : 1.79
Cycles 1600000
BaseLine 650004120
synchronized 52235762925
Lock 1419602771
Atomic 796950171
synchronized/BaseLine : 80.36
Lock/BaseLine : 2.18
Atomic/BaseLine : 1.23
synchronized/Lock : 36.80
synchronized/Atomic : 65.54
Lock/Atomic : 1.78
п родол ж ен и е &
1020 Глава 21 • Параллельное выполнение
Cycles 3200000
BaseLine 1285664519
synchronized 96336767661
Lock 2846988654
Atomic 1590545726
synchronized/BaseLine 74.93
Lock/BaseLine 2.21
Atomic/BaseLine 1.24
synchronized/Lock 33.84
synchronized/Atomic 60.57
Lock/Atomic 1.79
*///:~
Помните, что эта программа всего лишь дает представление о различиях между раз
ными реализациями мьютексов, а приведенные результаты описывают эти различия
на моем конкретном компьютере в конкретных обстоятельствах. Эксперименты по
казывают, что при разном количестве потоков и изменении времени выполнения могут
происходить значительные изменения в поведении. Некоторые оптимизации времени
выполнения активизируются только после того, как программа проработает несколько
минут —или несколько часов для серверных программ.
С учетом сказанного достаточно очевидно, что решения с Lock обычно сущ ественно
превосходят по эффективности решения с synchronized,aдoпoлнитeльныe затраты при
использовании synchronized изменяются в широких пределах, тогда как в реш ениях
с Lock они относительно постоянны.
Вопросы производительности
Если вы в основном ограничиваетесь чтением из контейнера без блокировок, операции
с ним будут выполняться намного быстрее, чем с его synchronized-аналогом, из-за до
полнительных затрат на установление и снятие блокировок. Этот принцрш справедлив
и при небольшом количестве операций записи в контейнер без блокировок, но здесь
будет интересно выяснить, что же в данном контексте можно считать «небольшим».
В этом разделе я постараюсь дать некоторое представление о различиях в произво
дительности между контейнерами в разных условиях. Начнем с общей тестовой среды
для тестирования любых типов контейнеров, включая Мар. Параметр С представляет
тип контейнера:
//: concurrency/Tester.java
// Программа для тестирования контейнеров
// с параллельным доступом.
import java.util.concurrent.*;
import net.mindview.util.*;
import java.util.*;
import net.mindview.util.*;
* продолжение &
1028 Глава 21 • Параллельное выполнение
Оптимистическая блокировка
Хотя объекты Atomic выполняют атомарные операции (такие, как decrementAndGet()),
некоторые классы Atomic также позволяют реализовать так называемую «оптими
стическую блокировку». Это означает, что мьютекс при вычислениях реально не
используется, но после завершения вычислений и готовности к обновлению объекта
Atomic используется метод с именем compareAndSet(). Методу передается старое и новое
значения, и если старое значение не согласуется со значением в объекте Atomic, опера
ция завершается неудачей —это означает, что другая задача успела изменить объект.
В общем случае мы могли бы воспользоваться мьютексом (synchronized или Lock) для
предотвращения одновременной модификации объекта несколькими задачами, но
в данной ситуации проявляем «оптимизм»: оставляем данные незаблокированными
и надеемся, что ни одна задача не вмешается и не изменит их. И снова все это делается
ради производительности —применение Atomic вместо synchronized или Lock обеспе
чивает существенный выигрыш в производительности.
Что произойдет, если операция compareAndSet() будет выполнена неудачно? Здесь
начинаются сложности, из-за которых оптимистическая блокировка применяется
только для задач, которые можно адаптировать для этих требований. Вы должны
Оптимизация 1029
решить, что происходит при неудаче; это очень важный момент, потому что если вы не
сможете каким-то образом восстановить работу программы, то вместо оптимистичной
блокировки следует применять традиционные мьютексы. Возможно, операцию мож
но повторить, и со второго раза все получится. А может быть, ошибку можно просто
проигнорировать —в некоторых моделях потеря одного элемента данных теряется на
фоне общей картины (конечно, для принятия такого решения необходимо достаточно
хорошо понимать вашу модель).
Возьмем фиктивную модель из 100 ООО «генов» длиной 30 (допустим, начало некоей
разновидности генетическихалгоритмов). При каждой «эволюции» генетического ал
горитма выполняются очень серьезные вычисления, поэтому вы решаете использовать
многопроцессорную машину для распределения задач и повышения производитель
ности. Кроме того, вы используете объекты Atomic вместо Lock, чтобы избежать лишних
затрат, связанных с применением мьютексов. (Естественно, это решение строится
уже после того, как вы сначала записали свой код в простейшем варианте с ключе
вым словом synchronized. Программа заработала, вы убедились, что она выполняется
слишком медленно, и начали применять средства повышения производительности!)
Из-за природы модели, если во время вычисления произойдет коллизия, обнаружив
шая эту коллизию задача может просто проигнорировать ее и не обновлять значение.
Вот как это выглядит:
//: concurrency/FastSimulation.java
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;
import java.util.*;
import static net.mindview.util.Print.*;
ReadWriteLock
Класс ReadWriteLocks оптимизирует ситуацию с относительно редкой записью и частыми
чтениями из структуры данных. Несколько задач могут читать данные одновременно
при условии, что ни одна задача не пытается их записывать. Если установлена блоки
ровка записи, то чтение становится невозможным до ее освобождения.
Совершенно невозможно предсказать, улучшитли ReadWriteLock производительность
вашей программы. Это зависит от таких аспектов, как относительная частота чтения
данных (по сравнению с частотой их записи), время операций чтения и записи (блоки
ровка более сложна, поэтому при коротких операциях преимущества не проявляются),
интенсивность конкуренции потоков, а также от того, выполняется ли программа на
многопроцессорной машине. В конечном итоге узнать, принесет ли пользу ReadWriteLock
на вашей машине или нет, можно только одним способом —попробовать на практике.
Следующий пример демонстрирует простейшее использование ReadWriteLock:
//: concurrency/ReaderWriterList.java
import java.util.concurrent.*;
import java.util.concurrent.locks.*;
import java.util.*;
import static net.mindview.util.Print.*j
class ReaderWriterListTest {
ExecutorService exec = Executors.newCachedThreadPool();
private final static int SIZE = 100;
private static Random rand = new Random(47);
private ReaderWriterList<Integer> list =
new ReaderWriterList<Integer>(SIZE, 0);
private class Writer implements Runnable {
public void run() {
try {
for(int i = 0; i < 20; i++) { // 2-секундный тест
list.set(i, rand.nextInt());
TimeUnit.MILLISECONDS.sleep(100);
}
} catch(InterruptedException e) {
// Допустимый способ выхода
}
print("Writer finished, shutting down");
exec.shutdownNow();
}
>
private class Reader implements Runnable {
public void run() {
try {
while(!Thread.interrupted()) {
for(int i = 0; i < SIZE; i++) {
продолжение #
1032 Глава 21 • Параллельное выполнение
чем одной задачей чтения, и если проверка дает положительный результат —выводит
их количество для демонстрации того, что блокировка чтения может быть получена
сразу несколькими задачами.
Чтобы протестировать R e a d e r W r i t e r L i s t , класс R e a d e r W r i t e r L i s t T e s t создает задачи
чтения и записи для R e a d e r W r i t e r L i s t < I n t e g e r > . Обратите внимание: задач записи на
много меньше, чем задач чтения.
Заглянув в докум ентацию ^К для R e e n t r a n t R e a d W r i t e L o c k , вы увидите, что класс со
держит и другие методы, а при работе с ним приходится учитывать факторы «равно
доступности» и «выбора политики». Это достаточно сложный инструмент, и при
менять его следует только в том случае, когда вы активно ищете пути к повышению
производительности. В первой версии программы следует использовать обычную
синхронизацию и применять R e a d W r i t e L o c k только при необходимости.
40. (6) По образцу ReaderWriterList.java создайте реализацию R e a d e r W r i t e r M a p на базе H a s h -
м а р . Проанализируйте ее производительность при помощи измененной программы
MapComparisons.java. Какие результаты она показывает по сравнению с синхронизи-
рованой реализацией H a s h M a p и C o n c u r r e n t H a s h M a p ?
Активные объекты
Вероятно, к этой странице вы уже поняли, что параллельное программирование Bjava —
очень сложная тема и ее очень трудно правильно использовать. Вдобавок на первый
взгляд происходящее выглядит противоестественно —задачи работают параллельно,
а разработчик должен тратить огромные силы на то, чтобы они не мешали работать
друг другу.
Если вы когда-либо писали на ассемблере, многопоточное программирование вызы
вает похожие ощущения: важна каждая мелочь, вы сами отвечаете за все, нет никакой
«страховки» в виде проверки компилятора.
Оптимизация 1033
Может, что-то не так с самой потоковой моделью? В конце концов, она пришла почти
неизменной из мира процедурного программирования. Возможно, существует более
удобная модель параллельности, которая лучше подходит для объектно-ориентиро
ванного программирования.
Одно из альтернативных решений связано с использованием активнъюс объектов. Эти
объекты называются «активными»1, потому что каждый объект поддерживает соб
ственный рабочий поток и очередь сообщений, а все запросы к этому объекту ставятся
в очередь для последовательного выполнения. Таким образом, при использовании
активных объектов организуется последовательная обработка сообщений, а не методов;
это означает, что нам не нужно защищаться от проблем, которые могут возникнут при
прерывании задачи на середине ее цикла.
Когда вы отправляете сообщение активному объекту, это сообщение преобразуется
в задачу, которая помещается в очередь объекта для выполнения в будущем. Класс
Java SE5 Future хорошо подходит для реализации этой схемы. Рассмотрим простой
пример с двумя методами и организацией очереди вызовов:
//: concurrency/ActiveObjectDemo.java
// В аргументах асинхронных методов могут передаваться
// только константы, неизменные значения,
// "отсоединенные объекты" или другие активные объекты,
import java.util.concurrent.*;
import java.util.*;
import static net.mindview.util.Print.*;
Резюме
В этой главе я постарался изложить основы параллельного программирования с при
менением noTOKOBjava. Из этого материала вы должны были понять следующее.
1. В программе могут выполняться несколько независимых задач.
2. Разработчик должен проанализировать все возможные проблемы при завершении
этих задач.
3. Задачи могут взаимодействовать друг с другом через общие ресурсы. Для предот
вращения конфликтов используются мьютексы (блокировки).
4. В плохо спроектированной программе могут возникнуть взаимные блокировки.
Очень важно понимать, когда рационально использовать параллельное выполнение,
а когда этого делать не стоит. Основные причины его поддержки:
□ возможность управления несколькими подзадачами, одновременное выполнение
которых позволяет эффективнее распоряжаться ресурсами компьютера (включая
возможность незаметного распределения этих задач по нескольким процессорам);
□ улучшение структуры кода;
□ удобство для конечного пользователя.
Классический пример сбалансированного расходования ресурсов — использование
процессора во время ожидания завершения операций ввода-вывода. Улучшение
структуры кода обычно проявляется в компьютерном моделировании. Классический
пример чуткого пользовательского интерфейса —отслеживание кнопки «Остановить»
во время продолжительного процесса загрузки.
Резюме 1037
1 «Голодание» (иначе оттеснение, «подвисание») имеет место, когда один или более потоков
процесса блокируются в получении доступа к ресурсу и не могут вследствие этого двигаться
дальше. Тупик —это крайняя форма голодания, когда два или более потоков блокируются по
условию, которое не может быть удовлетворено. В случае динамического тупика (livelock) два
или более процесса руководствуются изменениями в других, уступая свое право на ресурс,
из-за чего они «гоняются по кругу» и ни один из них не может выполнять никакой полезной
работы. Принципиальное отличие от просто тупика (deadlock) —отсутствие блокировки по
токов. —П рим еч. р е д .
1038 Глава 21 • Параллельное выполнение
Дополнительная литература
К сожалению, в области параллельного программирования много ошибочной инфор
мации — это лишь подчеркивает, насколько сложна эта тема и как просто решить,
будто вы разбираетесь в ней. Я это знаю по собственному опыту; в прошлом я уже
неоднократно полагал, что понимаю тонкости многопоточного программирования,
и обнаруживал, что я ошибаюсь —и наверняка это еще случится в будущем. Когда вы
беретесь за новый документ о параллельном программировании, всегда приходится
заниматься самостоятельными изысканиями и выяснять, в какой степени автор по
нимает (или не понимает) тему. Ниже перечислены некоторые книги, которые я могу
с полной уверенностью назвать надежными источниками.
Java Concurrency in Practice, авторы: Брайан Гетц, Тим Пейерлс, Джошуа Блош, Джозеф
Боубир, Дэвид Холмс и ДугЛи (Addison-Wesley, 2006). По сути, это самая авторитетная
работа в мире параллельного программирования HaJava.
ConcurrentProgramminginJava, второе издание, автор: ДугЛи (Addison-Wesley, 2000).
Кйига была выпущена задолго до появления^уа SE5, но большая часть работы Дуга
была воплощена в новых библиотеках java.util.concurrent, поэтому книга исключитель
но важна для полноценного понимания проблем параллельности. Автор выходит за
рамки параллельности^уа и обсуждает современный менталитет параллельности для
разных языков и технологий. Местами материал оказывается слишком сложным, и все
же книга заслуживает того, чтобы прочитать ее несколько раз (желательно с переры
вом в несколько месяцев для усвоения информации). Дуг —один из немногих людей
в этом мире, действительно разбирающийся в параллельности, так что вы не пожалеете
о потраченном времени.
The Java Language Specification, третье издание (глава 17), авторы: Гослинг, Джой,
Стили и Брача (Addison-Wesley, 2005). Техническая спецификация, доступная в виде
электронного документа по адресу http://java,sun.com/docsfoooks/jh.
Графический
интерфейс
1 Есть еще одна разновидность этого выражения — это так называемый «принцип не изумлять
понапрасну» или, что то ж е самое, «не преподносите пользователю сюрпризов».
1040 Глава22 • Графическийинтерфейс
1 Стоит отметить, что корпорация IBM разработала новую библиотеку пользовательского ин
терфейса с открытыми исходными текстами для своего текстового редактора Eclipse (www.
Eclipse.orq), альтернативного Swing.
Апплет 1041
Апплет
При первом появлении^уа большая часть шумихи по поводу языка была связана
с апплетами — программами, которые могли пересылаться по Интернету для выпол
нения в браузере (внутри так называемой песочницы по соображениям безопасности).
1 М ой любимый пример - стиль оформления от Кена Арнольда, при котором окна выглядят
так, словно они нарисованы на салфетке (см. http://napkinlaf.sourceforge.net).
1042 Глава22 • Графическийинтерфейс
Основы Swing ч
Чтобы приложение стало чуть более интересным, мы добавим в 3Frame объект 3Label:
//: gui/HelloLabel.java
import javax.swing.*;
import java.util.concurrent.*;
Вспомогательный класс
Чтобы избавиться от лишнего кода, мы воспользуемся изложенными выше концеп
циями и создадим вспомогательный класс, который будет использоваться в примерах
Swing этой главы:
1 Эта практика была введена Bjava SE5, поэтому во многих старых программах она не приме
няется. Это вовсе не указывает на невежество авторов - рекомендуемые практики находятся
в постоянном развитии.
Создание кнопки 1045
//: net/mindview/util/SwingConsole.java
// Вспомогательная библиотека для запуска с консоли
// примеров Swing (как апплетов, так и DFrame).
package net.mindview.util;
import javax.swing.*;
Создание кнопки
Создать кнопку очень просто: надо вызвать конструктор класса 3Button с текстом,
который должен выводиться на кнопке. Позже вы увидите, что можно проделывать
и более интересные вещи (например, разместить изображение на кнопке).
Обычно при создании кнопки в классе определяется новое поле типа 3Button, чтобы
позднее к ней можно было обратиться из программы.
Кнопка JButton является графическим компонентом —маленьким окном, —которое
автоматически перерисовывается при обновлении главного окна приложения. Это
значит, что вам не нужно программно прорисовывать кнопку или любой другой эле
мент графического интерфейса; вы просто размещаете их на форме, а об остальном
они позаботятся сами. Обычно кнопка размещается на форме в конструкторе:
//: gui/Buttonl.java
// Putting buttons on а Swing application,
import javax.swing.*;
import java.awt.*;
import static net.mindview.util.SwingConsole.*;
setLayout(new FlowLayout());
add(bl);
add(b2);
>
public static void main(String[] args) {
run(new Buttonl(), 200, 100);
}
> ///:~
Здесь появилось кое-что новое: перед размещением элементов на панели 3Frame ее
размечают с помощью «менеджера расположения» (layout manager) типа FlowLayout.
С помощью менеджера расположения панель самостоятельно определяет, где будут
находиться компоненты на форме. По умолчанию апплеты используют менеджер
BorderLayout, но в данном случае он нам не подходит, так как по умолчанию после до
бавления нового компонента он скрывает уже добавленные компоненты. Менеджер
расположения FlowLayout размещает компоненты на форме равномерно, в направлении
слева направо и сверху вниз.
4 . ( 1) Покажите, что без вызова setLayout() в Buttonl.java в программе появится только
одна кнопка.
Перехват событий
Вы, конечно, заметили, что после компиляции и запуска программы, когда вы нажима
ете на кнопки, ровным счетом ничего не происходит. Чтобы в программе выполнялись
какие-то действия, необходимо написать дополнительный код, который будет опреде
лять, что делать при нажатии кнопки. Основа управляемого событиями программиро
вания, к которому большей частью относится программирование графического интер
фейса (GUI), состоит в привязке события к коду, который это событие обрабатывает.
Библиотека Swing позволяет четко отделить интерфейс (графические компоненты) от
реализации (код, который должен выполняться, когда компонент сообщает о некотором
событии). Каждый компонент библиотеки Swing способен сообщить обо всех собьггиях,
которые могут с ним произойти, и каждое событие можно обработать индивидуально.
Таким образом, если вам не интересно, например, прохождение над вашей кнопкой
курсора мыши, вы просто не регистрируете свою заинтересованность в этом событии.
Это очень понятный и элегантный способ программирования событий. Как только вы
поймете его основы, вы сможете легко использовать компоненты Swing, которых до
этого даже в глаза не видели, —вообще такая модель событий относится к технологии
визуальных KOMnoHeHTOBjavaBean в целом (эту технологию мы обсудим чуть позже).
Для начала мы сконцентрируемся на главном событии, каковое есть у каждого ком
понента. В случае с кнопкой 3Button таким «основным» событием является нажатие
этой кнопки. Чтобы зарегистрировать свою заинтересованность в событии нажатия
кнопки, вызовите метод класса 3Button с именем addActionListener(). Этот метод
ожидает получить аргумент, который должен представлять из себя объект, реализу
ющий интерфейс ActionListener. Интерфейс ActionListener состоит из одного метода
с именем actionPerformed(). Значит, для присоединения кода обработки главного
события кнопки 3Button нужно реализовать интерфейс ActionListener в своем классе
Перехватсобытий 1047
)тот объект — кнопка 3Button. Метод getText() возвращает текст на кнопке, который
томещается в текстовое поле DTextField, чтобы показать, что при нажатии кнопки
щйствительно выполняется код обработки события.
3 конструкторе также вызывается метод addActionListener(), чтобы зарегистрировать
збъект ButtonListener для обеих кнопок программы.
Засто бывает удобнее реагировать на события с помощью анонимного внутреннего
сласса, который реализует интерфейс ActionListener, особенно если учесть, что обыч-
io используется только один экземпляр этого класса для каждого класса слушателя.
Трограмму Button2.java можно изменить так, чтобы она использовала анонимный
знутренний класс:
' / ’ gui/Button2b.java
4 Using anonymous inner classes.
Import javax.swing.*;
Import java.awt.*j
Import java.awt.event.*;
Import static net.mindview.util.SwingConsole.*;
Текстовые области
Текстовая область 3TextArea очень похожа на однострочное поле iTextField, но она
тозволяет редактировать несколько строк текста и имеет расширенные возможности.
Перехват событий 1049
Особенно полезен метод append(); с его помощью стандартный вывод с легкостью пере
водится в текстовое поле, а с программами становится удобнее работать, поскольку
выведенные сообщения не исчезают как раньше — их можно просмотреть и прокру
тить назад. В качестве примера рассмотрим программу, заполняющую текстовое поле
lTextArea данными, которые производит генератор geography из главы 17:
//: gui/TextArea.java
// Использование элемента управления 3TextArea.
import javax.swing.*j
import java.awt.*;
import java.awt.event.*;
import java.util.*;
import net.mindview.util.*;
import static net.mindview.util.SwingConsole.*j
помещаться на рабочий экран. Это все, что нужно сделать для получения полнофункцио
нальной прокрутки. Разбираясь с тем, как организуется такая же прокрутка в других
программных средах, я еще больше оценил простоту и мощность таких компонентов,
как панель DScrollPane.
6. (7) Преобразуйте пример strings^estRegularExpression.java в интерактивную программу
Swing, в которой входная строка вводится в первой области 3TextArea, а регулярное
выражение —в поле JTextField. Результат должен выводиться во второй области
3TextArea.
BorderLayout
По умолчанию DFrame использует менеджер расположения BorderLayout. Без дополни
тельных указаний он берет компонент, добавленный методом add(), и помещает его
в центр панели, растягивая границы компонента к сторонам панели.
Управление расположением компонентов 1051
FlowLayout
Этот менеджер расположения просто последовательно «выкладывает» компоненты на
форму один за другим, слева направо, пока не заполняется все доступное пространство
на одной строке, затем он переходит вниз на следующую строку компонентов и про
должает в том же порядке.
В следующем примере функции конструктора формы вручаются FlowLayout, за
тем на форме размещаются кнопки. Заметьте, что укладчик FlowLayout использует
1052 Глава22 • Графическийинтерфейс
GridLayout
Менеджер расположения GridLayout позволяет вам построить таблицу компонентов,
и каждый новый компонент помещается в таблицу в направлении слева направо
и сверху вниз относительно предыдущего. В конструкторе этого «компонентоуклад-
чика» надо указать необходимое вам количество строк и столбцов, они будут иметь
одинаковые размеры:
//: gui/GridLayoutl.java
// Демонстрация табличного расположения GridLayout.
import javax.swing.*;
import java.awt.*;
import static net.mindview.util.SwingConsole.*;
GridBagLayout
Менеджер расположения GridBagLayout предоставляет выдающиеся возможности для
расположения элементов; он позволяет подробно указать, как должны располагаться
области окна, как они должны менять размер при изменении границ окна. Но за все
надо платить —GridBagLayout является самым сложным менеджером расположения,
и понять его нелегко. Основное его предназначение — автоматическое генерирова
ние кода в построителе GUI (построители обычно используют GridBagLayout, а не
абсолютные позиции в пикселах). Если ваш пользовательский интерфейс настолько
изощрен, что прочие укладчики компонентов вас не устраивают, тогда для создания
этого интерфейса лучше привлечь автоматизированное средство: Если вам все же
хочется погрузиться в хитросплетения макетов GridBagLayout, могу порекомендовать
вам любую книгу, полностью посвященную библиотеке Swing.
Также можно воспользоваться менеджером TableLayout, который не входит в библиоте
ку Swing, но может загружаться с сайта http://java.sun.com. Этот компонент работает на
базе GridBagLayout и скрывает большую часть сложностей, поэтому он может серьезно
упростить этот способ.
Абсолютное позиционирование
Также возможно указать абсолютное расположение графических компонентов, то
есть их позиции в пикселах. Для этого следует:
1. Для используемого вами контейнера (Container) вместо менеджера расположения
передать методу setLayout() ссылку null: setLayout(null).
2. Для каждого компонента вызвать метод setBounds() или reshape( ) (в зависимости от
версии языка), передавая ограничивающий прямоугольник компонента в пикселах.
Это также можно сделать в конструкторе или методе paint(), все зависит от того,
какого результата вы добиваетесь.
Некоторые GUI-построители широко используют описанный подход, но, вообще
говоря, это не лучший способ создания графического интерфейса.
BoxLayout
Так как у людей возникало очень много проблем в понимании и применении
GridBagLayout, в библиотеку Swing включили дополнительный менеджер расположения
BoxLayout, который перенял многие преимущества GridBagLayout без его сложностей.
Благодаря этому вы можете постоянно использовать его при «ручном» создании гра
фического интерфейса (повторюсь еще раз, если ваш интерфейс становится слишком
хитер, пусть автоматизированный построитель GUI позаботится обо всех деталях без
вашего участия). Менеджер BoxLayout дает вам возможность размещать компоненты
и вертикально, и горизонтально, а также позволяет управлять пространством между
компонентами. Примеры использования BoxLayout приведены в приложении к книге
на сайте www.MindView.net.
1054 Глава22 • Графический интерфейс
Лучший вариант?
Библиотека Swing способна на многое; с ее помощью можно создать полнофункцио
нальные программы, написав несколько строк кода. Примеры в этой книге относительно
просты, и для лучшего усвоения материала в них стоит создавать пользовательский
интерфейс «вручную». Просто совмещая несколько простых режимов расположения,
можно получить весьма неплохой интерфейс. Однако в некоторый момент построение
графического интерфейса вручную теряет смысл —оно становится слишком сложным,
и время, потраченное на эту работу, расходуется неэффективно. Создатели Java и би
блиотеки Swing ориентировали язык и библиотеки на поддержку автоматизированных
средств построения GUI специально ради того, чтобы ускорить процесс разработки
программ и сэкономить ваше время. Если вы понимаете, как действуют менеджеры
расположения и что лежит в основе системы обработки событий (описанной в сле
дующем разделе), умение размещать компоненты путем кодирования для вас не так
важно —пусть это сделает подходящий визуальный инструмент (в конце концов, язык
Java разрабатывался для увеличения продуктивности программистов).
Все примеры, разобранные нами в данной главе, уже использовали общие принципы
модели событий библиотеки Swing, а конец текущего раздела прояснит до конца все
частностиэтой модели.
Т а б л и ц а 2 2 .1 (продолж ение )
1 События с именем MouseMotionEvent не существует, хотя кажется, что оно должно быть. И на
жатия на кнопки мыши, и перемещение мыши встроены в событие MouseEvent, так что второе
его появление в таблице не случайно и не является ошибкой.
Модель событий библиотеки Swing 1057
^ продолжение ^>
1058 Глава 22 • Графический интерфейс
Этот список далеко не полон —отчасти из-за того, что модель событий позволяет легко
создавать дополнительные типы событий и ассоциированных слушателей. Поэтому
вы будете регулярно сталкиваться с библиотеками, имеющими собственные события;
впрочем, пугаться не стоит ~ знания, полученные в этом разделе, позволят вам быстро
в них разобраться.
1060 Глава 22 • Графический интерфейс
* продолжение d>
1 В H3biKeJava версий 1.0/1.1 нельзя было даже с пользой создать класс, производный от класса
кнопки. Это был лишь один из многих просчетов в библиотеке AWT.
1062 Глава22 • Графический интерфейс
Компоненты Swing
Теперь, когда вы знаете, что такое менеджеры расположения и модель событий, пришло
время разобраться с конкретными компонентами библиотеки Swing. Этот раздел кратко
описывает основные компоненты библиотеки и их важнейшие свойства, которые вы
будете использовать чаще всего. Все примеры сделаны достаточно небольшими, чтобы
вы могли легко скопировать из них код и воспользоваться им в вашей собственной
программе.
Помните, что:
1. Вы сможете рассмотреть исходный текст этих примеров прямо во время их запуска
в браузере, так как он включен в HTML-страницу вместе с апплетом (эти страницы
можно найти на www.MindView.net).
2. Документация JDK на сайте http://java.sun.com содержит исчерпывающую ин
формацию обо всех классах библиотеки Swing и их методах (здесь же рассказано
только о некоторых из них).
3. Для компонентов применена удобная система именования, поэтому можно просто
угадать, как написать имя и зарегистрировать слушателя определенного события.
В изучении отдельных компонентов вам поможет программа ShowAddListeners.java.
064 Глава 22 • Графический интерфейс
яопки
иблиотека Swing включает в себя несколько различных видов кнопок. Все кноп-
и, флажки, переключатели, даже пункты меню унаследованы от базового класса
jstractButton (который, раз уж от него наследуются даже пункты меню, лучше было бы
азвать AbstractSelector или еще более обобщенно и абстрактно). Но до менюдело дойдет
есколько позже, а пока следующий пример демонстрирует различные виды кнопок:
': gui/Buttons.java
I Различные кнопки Swing.
nport javax.swing.*;
nport javax.swing.border.*;
nport javax.swing.plaf.basic.*;
nport java.awt.*;
nport static net.mindview.util.SwingConsole.*;
руппы кнопок
1сли вы хотите, чтобы переключатели использовались для определения взаимоисклю-
ающих состояний, вам нужно создать соответствующую «группу кнопок» ButtonGroup.
Компоненты Swing 1065
Но, как показывает следующий пример, в такую группу можно добавить любую кнопку,
унаследованную от AbstractButton.
Чтобы избежать повторения одного и того же кода, следующий пример использует
отражение для формирования групп, состоящих из различных типов кнопок. Делается
это в методе makeBPanel(), который создает группу кнопок на панели 3Panel. Второй
аргумент метода makeBPanel() — массив строк (String). Для каждой строки создается
кнопка класса, указанного в первом аргументе, после чего она добавляется на панель:
//: gui/ButtonGroups,java
// Использование отражения для создания групп кнопок,
// производных от базовой кнопки AbstractButton.
import javax.swing.*;
import javax.swing.border.*;
import java.awt.*;
import java.lang.reflect.*;
import static net.mindview.util.SwingConsole.*;
Значки
Вы можете использовать значок lcon внутри 3Label или в компонентах, унаследованных
от базовой кнопки AbstractButton (включая JButton, 3CheckBox, DRadioButton, а также
все виды меню (JMenultem)). Как будет показано в одном из будущих примеров, это
делается достаточно тривиально. Следующий пример исследует всевозможные способы
применения значков lcon в кнопках и их производных.
Для значков годятся любые файлы в формате GIF; картинки в данном примере явля
ются частью комплектации этой книги, их можно найти на сайте wwwMindView.net
Чтобы открыть файл с изображением и загрузить его, нужно просто создать объект
lmagelcon, передав его конструктору имя файла с изображением. После этого полу
ченный значок (lcon) можно использовать в программе.
//: gui/Faces.java
// Поведение значков в кнопках 3Button.
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import static net.mindview.util.SwingConsole.*;
jb.setIcon(faces[3]);
mad = false;
} else {.
jb.setlcon(faces[0]);
mad * true;
}
jb.setVerticalAlignment(3Button.TOP);
jb.setHorizontaWlignment(3Button.LEFT) ;
>
»;
jb.setRolloverEnabled(true);
j b .setRolloverIcon(faces[1]);
jb.setPressedIcon(faces[2]);
jb.setDlsabledIcon(faces[4]);
jb.setToolTipText("Yow!");
add(jb);
jb2.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
if(jb.isEnabled()) {
jb.setEnabled(false);
jb2.setText("Enable");
} else {
jb.setEnabled(true);
jb2.setText("Disable");
>
}
»J
add(jb2);
>
public static void main(String[] args) {
run(new Faces(), 250j 125);
>
> ///:~
Значки (icon) можно использовать во многих конструкторах различных компонентов
библиотеки Swing, однако не запрещается установить его и после создания кнопки —
для этого предназначен метод setlcon(). В этом примере также показано, как на кноп
ку 3Button (как и для любой кнопки AbstractButton) помещаются различные значки,
которые появляются при нажатии кнопки, ее отключении, проведении над кнопкой
курсором мыши. Вы увидите, что подобная анимация весьма оживляет кнопку.
Подсказки
В предыдущем примере к кнопке быладобавлена «подсказка» (tool tip). Практически
все классы, которые послужат основой вашего графического интерфейса, унаследова
ны от базового компонента библиотеки Swing 3Component, в котором имеется метод с
названием setToolTipText(String). Поэтому практическидлявсего, что вы помещаете
на форму, можно вызвать этот метод, чтобы подключить соответствующую подсказку
(jc —ссылка на компонент класса, производного от 3Component):
jc .setToolTipText(”Это подсказка");
Текстовые поля
Этот пример показывает дополнительные возможности текстовых полей JTextField:
//: gui/TextFields.java
// Text fields and 3ava events.
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.text.*;
import java.awt.*;
import java.awt.event.*;
import static net.mindview.util.SwingConsole.*j
tl.setEditable(true);
>
>
class B2 implements ActionListener {
public void actionPerformed(ActionEvent e) {
ucd.setUpperCase(false);
tl.setText("Inserted by Button 2: " + s);
ucd.setUpperCase(true);
tl.setEditable(false);
>
}
public static void main(String[] args) {
run(new TextFields(), 375, 200);
>
>
class UpperCaseDocument extends PlainDocument {
private boolean upperCase = true;
public void setUpperCase(boolean flag) {
upperCase = flag;
}
public void
insertString(int offset, String str, AttributeSet attSet)
throws BadLocationException {
if(upperCase) str = str.toUpperCase();
super.insertString(offset, str, attSet);
>
> / / / :~
Текстовое поле t3 служит для отчета о действиях в текстовом поле tl. Вы увидите, что
«слушатель действий» поля tl срабатывает только при нажатии в этом поле клавиши
Enter.
К текстовому полю tl присоединены несколько слушателей. Слушатель Tl реализует
интерфейс DocumentListener и откликается на любые изменения в «документе» (в на
шем случае это содержимое текстового поля 3TextField). Он автоматически копирует
текст из поля tl в поле t2. Вдобавок поле tl использует в качестве документа класс,
производный от базового классадокумента PlainDocument, с именем UpperCaseDocument,
который при вводе преобразует все символы в верхний регистр. Он автоматически
обнаруживает нажатия клавиши BackSpace и производит удаление символов, изменяет
позицию курсора и вообще делает все так, как и ожидает пользователь.
13. (3) Измените программу TextFields.java так, чтобы символы в текстовом поле t2 оста
вались в том регистре, в котором они были напечатаны, а не преобразовывались
автоматически к верхнему регистру.
Рамки
Класс 3Component содержит метод с именем setBorder(), который позволяет вам разме
стить на компонентах различные, порой весьма любопытные окантовки. Следующий
пример демонстрирует несколько различных рамок, предоставляемых библиотекой
Swing, используя для этого метод showBorders (), который создает панель 3Panel и ото
бражает на ней рамку. Пример также использует RTTI для определения имени класса
1070 Глава22 • Графическийинтерфейс
рамки (из которого исключается информация пути), которое затем размещается в се
редине каждой панели 3Panel:
//: gui/Borders.java
// Разные варианты рамок в Swing.
import javax.swing.*;
import javax.swing.border.*;
import java.awt.*;
import static net.mindview.util.SwingConsole.*;
Мини-редактор
Управляющий элемент 3TextPane предоставляет обширные возможности по редакти-
роПанию текстов, не требуя от программиста больших усилий. Следующий пример
демонстрирует простейший вариант использования этого компонента, в котором не
используется большая часть его возможностей:
//: gui/TextPane.java
// JTextPane - миниатюрный редактор текста,
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
Компоненты Swing 1071
import net.mindview.util.*;
import static net.mindview.util.SwingConsole.*;
Флажки
Флажок позволяет произвести выбор из двух возможных состояний («включено/вы-
ключено»), он состоит из небольшого прямоугольника и сопроводительного текста.
В прямоугольник обычно помещается маленький крестик «х» (или какой-либо другой
индикатор статуса), или он остается незаполненным, все зависит от состояния флажка.
Флажок 3CheckBox, как правило, создается конструктором, которому передается текст
надписи. Вы можете узнать состояние флажка и установить его, а также считать или
изменить текст надписи уже после создания флажка.
Когда пользователь изменяет состояние флажка 3CheckBox, происходит событие, кото
рое можно обработать точно так же, как и в случае с обычной кнопкой 3Button, то есть
1072 Глава 22 • Графический интерфейс
Переключатели
Концепция «радиокнопок» (переключателей) пришла в программирование GUI
из старых, еще не электронных радиоприемников, где при нажатии одной кнопки
остальные «выскакивали». Интерфейсные переключатели тоже позволяют выбрать
один вариант из многих.
Чтобы создать связанную группу кнопок переключателя JRadioButton, надо объединить
их в группу (ButtonGroup) (на форме может существовать произвольное количество
таких групп). Одна из кнопок изначально может быть «включена» (передачей true во
втором аргументе конструктора). Если вы попытаетесь включить несколько кнопок
сразу, то только та кнопка, что была указана последней, сохранит это состояние. Все
остальные автоматически отключатся.
Вот простой пример использования переключателей, в котором для перехвата событий
используются объекты ActionListener:
//: gui/RadioButtons.java
// Использование переключателей 3RadioButton.
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import static net.mindview.util.SwingConsole.*;
Раскрывающиеся списки
Как и переключатели, раскрывающийся список предназначен для того, чтобы поль
зователь смог выбрать один элемент из группы возможных значений. Однако данный
способ более «компактен», к тому же здесь легче осуществить динамическое измене
ние элементов списка, не пугая пользователя. (Переключатели тоже можно изменять
динамически, однако такие метаморфозы раздражают пользователя.)
По умолчанию 3ComboBox отличается от аналогичных списков ОС Windows, которые
позволяют вам выбирать из списка или набирать свое собственное значение. Чтобы полу
чить список такого типа, необходимо вызвать метод setEditable( ). В остальном список
3ComboBox работает так же, и он тоже позволяет выбрать один и только один элемент
списка. В следующем примере созданный раскрывающийся список заполняется не
сколькими пунктами, а затем при нажатии кнопки к ним присоединяются новые пункты.
//: gui/ComboBoxes.java
// Использование раскрывающихся списков,
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import static net.mindview.util.SwingConsole.*;
Списки
Обычные списки довольно существенно отличаются от только что рассмотренных рас
крывающихся списков, и не только внешне. В то время как раскрывающийся список
(3ComboBox) становится виден целиком только при выборе его элемента, просто список
3List постоянно занимает на экране определенное количество строк и не изменяется.
Если вам понадобятся в работе результаты выбора, вызовите метод getSelectedValues(),
который возвращает массив объектов String с текстами выбранных элементов списка.
Список 3List позволяет выбирать несколько элементов одновременно: если вы щел
кнете кнопкой мыши на нужных вам элементах, удерживая клавишу Ctri, то выделены
будут они все. Если выделить один элемент, а затем, удерживая клавишу ShHt, щелкнуть
на другом элементе, выделяются все элементы между этими двумя. Для исключения
элемента из выделенной группы щелкните на нем, удерживая клавишу Ctri.
//: gui/List.java
import javax.swing.*;
import javax.swing.border.*;
import javax.swing.event.*;
import java.awt.*;
import java.awt.event.*;
import static net.mindview.util.SwingConsole.*;
t.append(item + "\n");
>
}J
private int count = 0;
public List() {
t .setEditable(false);
setLayout(new FlowLayoutQ);
// Создание рамки для компонентов:
Border brd = BorderFactory.createMatteBorder(
lj 1, 2, 2, Color.BLACK);
1st.setBorder(brd);
t.setBorder(brd);
// Добавляем в список первые четыре элемента
for(int i = 0; i < 4; i++)
lItems.addElement(flavors[count++]);
add(t);
add(lst)j
add(b);
// Регистрация слушателей событий
1st.addListSelectionListener(ll);
b.addActionListener(bl);
>
public static void main(String[] args) {
run(new List(), 250, 375);
>
> ///:~
Список также был заключен в рамку.
Если вам нужно просто разместить массив строк (String) в списке 3List, все мож
но сделать намного проще: передайте массив конструктору класса JList, и список
элементов будет создан автоматически. Единственная причина присутствия в этом
примере «модели списка» —необходимость изменения содержимого списка во время
выполнения программы.
В списках 3List отсутствует автоматическая прямая поддержка прокрутки. Конечно,
проблема решается очень просто: упакуйте список 3List в панель прокрутки 3ScrollPane,
а все остальное будет сделано автоматически.
16. (5) Упростите пример List.java: передайте массив конструктору и устраните дина
мическое включение элементов в список.
Панель вкладок
Панель вкладок JTabbedPane позволяет создать диалоговое окно с набором вкладок,
у которого к одной из сторон прижат набор «корешков» вкладок. Чтобы переклю
читься к содержимому другого диалогового окна, достаточно щелкнуть на корешке
нужной вкладки.
//: gui/TabbedPanel.java
// Демонстрация панели вкладок,
import javax.swing.*;
import javax.swing.event.*;
import java.awt.*;
import static net.mindview.util.SwingConsole.*;
Компоненты Swing 1077
Окна сообщений
Оконные среды часто содержат стандартный набор окон сообщений, который позволяет
вам быстро информировать пользователя о чем-то или получать от него информацию.
В библиотеке Swing эти окна сообщений создаются классом 30ptionPane. С его по
мощью можно получить большое количество разнообразных информационных окон
(некоторые из них весьма впечатляют), но чаще всего используются окна с сообще
ниями программы и окна с подтверждениями выбора, которые вызываются посред
ством статических (static) методов DOptionPane.showMessageDialog() и DOptionPane.
showConfirmDialog(). Следующий пример демонстрирует некоторые из окон сообщений,
предоставляемых классом JOptionPane:
//: gui/MessageBoxes.java
// Демонстрация возможностей JOptionPane.
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import static net.mindview.util.SwingConsole.*;
Меню
Все компоненты, способные отображать меню, — 3Applet, 3Frame, 3Dialog и их потом
ки —содержат метод set3MenuBar(), который принимает в качестве аргумента линейку
меню 3MenuBar (для компонента допускается только одна линейка меню). К компоненту
3MenuBar можно добавлять меню 3Menu, к которым, в свою очередь, уже позволено до
бавлять пункты меню 3MenuItem1. С любым пунктом меню 3Menultem можно добавлять
компонент ActionListener, который вызывается при выборе этого меню.
BJava и Swing меню формируется прямо в коде программы. Пример создания очень
простого меню:
//: gui/SimpleMenus.java
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import static net.mindview.util,SwingConsole.*;
1Обычные пункты меню также можно присоединять непосредственно к линейке меню тем же
методом add. — Примеч. ред.
1080 Глава22 • Графическийинтерфейс
Всплывающие меню
Всплывающее меню DPopupMenu обычно реализуется следующим образом: создается
внутренний класс, расширяющий адаптер обработчика событий мыши MouseAdapter,
после чего объект этого внутреннего класса добавляется ко всем компонентам, к ко
торым вы хотели бы присоединить всплывающее меню:
//: gui/Popup.java
// Создание всплывающих меню библиотеки Swing.
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
Компоненты Swing 1085
Рисование
В хорошей GUI-среде рисование должно быть достаточно простым — в библиотеке
Swing рисовать действительно несложно. Проблема любого примера, вычерчиваю
щего что-либо на экране, —относительно сложные вычисления, которые определяют,
в каком месте экрана будет происходить графический вывод. Эти вычисления гораздо
1086 Глава 22 • Графический интерфейс
сложнее вызова самих функций рисования, однако они часто помещаются прямо в вы
зов функций, и кажется, что функции рисования сложнее, чем они есть на самом деле.
Рассмотрим задачу отображения некоторых данных на экране — в нашем случае
данные будут предоставлены встроенным методом Math.sin(), который возвращает
значение синуса угла. Чтобы сделать программу интереснее, а также для включения
в Наше поле зрения дополнительных компонентов библиотеки Swing, добавим в нее
регулятор JSlider, который будет располагаться в нижней части формы. Он позволяет
динамически управлять периодом «волны» синусоиды. Вдобавок, если вы увеличите
или уменьшите размеры окна, то увидите, что изображение функции автоматически
приспособится к новым размерам.
Хотя для рисования подходит любой компонент, унаследованный от JComponent, обычно
поверхностью для вывода служит панель DPanel. В ней необходимо переопределить
один метод, с именем paintComponent(), который вызывается при перерисовке компо
нента (об этом можете не беспокоиться, библиотека Swing делает это автоматически).
При вызове метода ему передается объект для рисования Graphics, который вы вправе
использовать для рисования и графического вывода.
В следующем примере процесс рисования и связанные с ним вычисления проводятся
в классе SineDraw; класс SineWave просто настраивает программу и создает регулятор
JSlider. В классе SineDraw определен метод setC ycles(), который позволяет другому
объекту — в данном случае регулятору — управлять отображаемой синусоидой.
//: gui/SineWave.java
// Рисование средствами 5wing, использование JSlider.
import javax.swing.*;
import javax.swing.event.*;
import java.awt.*;
import static net.mindview.util.SwingConsole.*;
>
>
public void setCycles(int newCycles) {
cycles = newCycles;
points = SCALEFACTOR * cycles * 2;
sines = new double[points];
for(int i = 0; i < points; i++) {
double radians = (Math.PI / SCALEFACTOR) * i;
sines[i] = Math.sin(radians);
}
repaint();
>
в программировании таких задач на других языках подсказывал, что это будет тяже
ло, однако оказалось, что Bjava это самая простая часть проекта. Я создал регулятор
DSlider (аргументы конструктора —это наименьшее, наибольшее и начальное значе
ние шкалы регулятора, хотя в классе DSlider есть и другие конструкторы) и поместил
его в DFrame. Затем я просмотрел документацик^ВК и обнаружил, что для ползунка
можно использовать только одного слушателя, с именем addChangeListener, который
вызывается при изменении положения ползунка. Единственному методу интерфейса
слушателя с именем stateChanged() передается аргумент-событие ChangeEvent, с по
мощью которого можно обратиться к источнику события и узнать новое положение
ползунка. После этого остается вызвать метод setCycles() объекта sines, этот метод
обновляет значение и перерисовывает панель 3Panel.
Обычно все компоненты библиотеки Swing используются так же легко, даже если вы
с ними до этого не работали. Надо просмотреть документацию, добавить компонент
на форму, присоединить подходящего слушателя событий —вот и все.
Если ваша задача сложна и требует расширенных средств рисования, существует вели
кое множество альтернатив, включая визуальные KOMnoHembiJavaBean от сторонних
производителей и технологию сложного рисования Java 2D API. К сожалению, эти
средства выходят за рамки темы книги, но если ваш код для рисования не справляется
с поставленными задачами, вам непременно следует обратить на них внимание.
21. (5) Преобразуйте программу SineWave.java так, чтобы класс SineDnaw стал компонен-
TOMjavaBean. Для этого определите в классе подходящие get- и set-методы.
22. (7) Создайте приложение с использованием SwingConsole. В нем должно быть три
регулятора, каждый отвечает соответственно за интенсивность красной, синей
и зеленой составляющей в цвете java.awt.Color\ Остальную часть формы должна
заполнить панель JPanel, закрашиваемая сформированным из значенийтрех регуля
торов цветом. Также добавьте три текстовых поля, недоступных для редактирования,
в которых укажите текущие числовые значения составляющих цвета RGB.
23. (8) Взяв за отправную точку пример SineWaves.java, создайте программу, которая
рисует на экране вращающийся квадрат. Один регулятор должен управлять ско
ростью вращения, а второй —размером квадрата.
24. (7) Помните игрушку «волшебный экран» с двумя ручками — одна управляет
вертикальным движением рисующей точки, другая управляет горизонтальным
движением? Создайте подобие такой игрушки на компьютере, взяв за образец код
SineWave.java. Место ручекдолжны занять ползунки регуляторов. Добавьте кнопку,
которая будет полностью очищать экран.
25. (8) Взяв за отправную точку пример SineWaves.java, создайте программу (приложение
с использованием класса SwingConsole), которая рисует анимированную синусои
ду, пробегающую через все окно, как на осциллографе, используя для управления
анимацией java.util.Timer. Скорость анимации должна задаваться регулятором
j avax.swing.3Slider.
26. (5) Измените упражнение 25 так, чтобы в приложении создавалось сразу несколько
панелей с синусоидами. Количество панелей должно задаваться в командной строке.
Компоненты Swing 1089
27 . (5) Снова измените упражнение 25, чтобы для управления анимацией использовался
таймер javax.swing.Timer, Отметьте отличия между ним и таймером java.util.Timer,
28 . (7) Создайте класс для моделирования кубика (просто класс, без графического ин
терфейса). Создайте пять объектов кубиков и проведите серию бросков. Выведите
кривую с суммой очков при каждом броске; обеспечьте динамическое изменение
кривой при новых бросках.
Диалоговые окна
Диалоговое окно —это окно, которое создается другим окном. Его предназначение —
выполнить некоторые специфические действия, не загромождая родительского окна
лишними подробностями. Диалоговые окна часто используются в оконных средах.
Для создания диалогового окна нужно использовать наследование от класса lDialog,
который представляет собой еще один вид окна Window, как и окно с рамкой JFrame.
Размещение элементов управления диалога lDialog определяется менеджером рас
положения (по умолчанию таковым является BorderLayout), для обработки событий
к нему присоединяются слушатели. Вот очень простой пример:
//: gui/Dialogs.java
// Создание и использование диалоговых окон,
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import static net.mindview.util.SwingConsole.*;
p = new DPanel()j
p.setLayout(new GridLayout(2,1));
p.add(fileName);
p.add(dir)j
add(p, BorderLayout.NORTH)j
>
class OpenL implements ActionListener {
public void actionPerformed(ActionEvent e) {
3FileChooser c = new 3FileChooser();
// Демонстрация диалогового окна открытия файлов:
int rVal = c.showOpenDialog(FileChooserTest.this);
if(rVal == 3FileChooser.APPR0VE_0PTI0N) {
fileName.setText(c.getSelectedFile().getName());
dir.setText(c.getCunrentDirectopy().toString());
>
if(rVal == DFileChooser.CANCEL_OPTION) {
fileName.setText("You pressed cancel'1);
dir.setText("")j
>
>
>
class SaveL implements ActionListener {
public void actionPerformed(ActionEvent e) {
3FileChooser с = new JFileChooser();
// Демонстрация диалогового окна сохранения файлов:
int rVal = c.showSaveDialog(FileChooserTest.this);
if(rVal == 3FileChooser.APPR0VE_0PTI0N) {
fileName.setText(c.getSelectedFile().getName());
dir.setText(с.getCurrentDirectory().toString());
>
if(rVal == 3FileChooser.CANCEL_OPTION) {
fileName.setText("You pressed cancel");
dir.setText("”);
>
>
>
public static void main(String[] args) {
run(new FileChooserTest(), 250, 150);
>
> ///:~
Стоит заметить, что диалог DFileChooser обладает большими возможностями —напри
мер, он позволяет назначать фильтры для сокращения списка отображаемых файлов.
Для вызова окна открытия файла вы вызываете метод showOpenDialog(), адля окна со
хранения файла —метод showSaveDialog(). Эти методы не возвращают управление до тех
пор, пока диалоговое окно не будет закрыто. На выходе из диалога объект 3FileChooser
остается доступен, с его помощью можно получить различную информацию. Напри
мер, методы getSelectedFile() и getCurrentDirectory() представляют собой два способа
запроса информации об имени выбранного файла и его расположения. Если они воз
вращают null, это значит, что пользователь отменил выбор файла и завершил диалог.
29. (3) В aoKyMeHTauHnJDKiumnaKeTajavax.swing найдите описание класса 3ColorChooser,
который отображает диалоговое окно для выбора цвета. Напишите программу
с кнопкой, нажатие на которую открывает это диалоговое окно на экране.
1094 Глава 22 • Графический интерфейс
Текст должен начинаться с тега <html>, за которым уже могут следовать другие теги
языка HTM L Заметьте, что использовать закрывающие теги не обязательно и их про
пуск не считается ошибкой.
При своем вызове слушатель ActionListener добавляет на форму новую надпись 3Label,
текст которой выводится в формате HTM L Но так как этот компонент был наложен
на форму после конструирования, макет формы необходимо построить заново, для
чего вызывается метод validate().
Текст в формате HTML применим также для наборов вкладок (UabbedPane), пунктов
меню (3Menultem), подсказок (lToolTip), переключателей (iRadioButton) и флажков
(3CheckBox).
Включать какой-либо код в секцию catch не обязательно, так как класс ulManager,
отвечающий за внешний вид и поведение программы, автоматически задействует
кросс-платформенный интерфейс, если попытки установки другого интерфейса за
вершатся неудачей. Впрочем, в процессе отладки программы исключение может быть
полезным —например, если вы захотите вывести в секции catch некоторое сообщение.
Следующая программа выбирает оформление программы в зависимости от получен
ного параметра командной строки и показывает, как выглядят некоторые компоненты
в различных пользовательских интерфейсах.
//: gui/LookAndFeel.java
// Выбор оформления программы.
// {Args: motif}
import javax.swing.*;
import java.awt.*;
import static net.mindview.util.SwingConsole.*;
} else if(args[0].equals("motif")) {
try {
UIManagen.setLookAndFeel("com.sun.java."+
“swing.plaf.motif.MotifLookAndFeel");
} catch(Exception e) {
e.printStackTraceQ;
>
> else usageError();
// Заметьте> что тип пользовательского интерфейса
// должен быть выбран перед созданием компонентов,
run(new LookAndFeel(), 300, 300);
>
> ///:~
Пользовательский интерфейс программы можно также явно указать строкой с име
нем, как это сделано в нашей программе для MotifLookAndFeel. Однако такой под
ход применим при выборе только двух видов пользовательского интерфейса; этого
и платформенно-независимого интерфейса Metal, выбираемого по умолчанию. Хотя
для Wlndows и Macintosh также существуют строковые обозначения, использовать
их можно только на соответствующих платформах (эти имена возвращаются методом
getSystemLookAndFeelClassName() в процессе выполнения программы).
Также возможно создать собственный пакет оформления —например, если вам пона
добится придать особый колорит программам какой-то компании. Однако это требует
больших усилий и выходит за рамки темы данной книги (более того, часто эта тема не
рассматривается даже в книгах, полностью посвященных библиотеке Swing!).
import java.awt.event.*;
import java.io.*;
} catch(UnavailableSenviceException use) {
throw new RuntimeException(use);
>
if(fs 1= null) {
try {
fileContents = fs.saveFileDialog(”.",
new 5tring[]{"txt"L
new ByteArrayInputStream(
ep.getText().getBytes()),
fileContents.getName());
if(fileContents == null)
return;
fileName.setText(fileContents.getName());
} catch(Exception exc) {
throw new RuntimeException(exc);
>
>
>
}
public static void main(String[] args) {
3nlpFileChooser fc = new 3nlpFileChooser();
fc.setSize(400, 300);
fc.setVisible(true);
}
} / / / :~
Заметьте, что классы FileOpenService и FileCloseService импортируются из naKeTajavax.
jnlp и что нигде в тексте программы нет прямого обращения к 3FileChooser. Два вида
сервиса, используемых нами, необходимо получать с помощью метода ServiceManager.
lookup(), ресурсы на машине клиента доступны только через объекты, возвращаемые
этим методом. В нашем примере чтение файлов и запись в них осуществлялись посред
ством интерфейса FileContent, предоставляемого^ЬР. Попытки обращения к ресурсам
напрямую (например, создание объектов File и FileReader) приведут к исключению
SecurityException, точно так же, как это случилось бы при попытке использования
этих объектов из неподписанного апплета. Если вы хотите работать с этими классами
и не намерены ограничиваться услугами интерфейсов JNLP, необходимо подписать
свой архив JAR.
Закомментированная команда jar в JnlpFileChooser.java создает необходимый файл JAR.
Для нашего примера файл запуска выглядит так:
//:! gui/jnlp/filechooser.jnlp
<?xml version=''1.0" encoding=''UTF-8"?>
<jnlp spec = "1.0+"
codebase="file:C:/AAA-TI34/code/gui/jnlp"
href="filechooser.jnlp">
<information>
<title>FileChooser demo application</title>
<vendor>Mindview Inc.</vendor>
<description>
3nlp File chooser Application
</description>
<description kind="short">
Demonstrates opening, reading and writing a text file
</description>
<icon href="mindview.gif''/>
п р о д о л ж ен и е &
1102 Глава 22 • Графический интерфейс
<offline-allowed/>
</information>
<resources>
<j2se version="1.3+"
href="http://java.sun.com/pnoducts/autodl/j2se"/>
<jar href="jnlpfilechooser.jar" download="eager"/>
</resources>
<application-desc
main-class="gui.jnlp.3nlpFileChooser'7>
</jnlp>
///:~
Файл запуска включен в архив исходного кода этой книги (с файла vmw.MindView.
net), сохраняется под именем filechooser.jnlp (без первой и последней строки) в том же
каталоге, где находится архив формата^И . Как видно из листинга, это файл формата
XML, с одним тегом <jnlp>. У него есть несколько вложенных элементов, которые
в основном понятны без комментариев.
Атрибут spec в элементе jnlp сообщает клиентской системе, какая BepcnnJNLP требу
ется для запуска приложения. Атрибут codebase указывает на каталог расположения
данного файла запуска и необходимых ресурсов. Обычно в нем содержится URL неко
торого веб-сервера, но в нашем случае это каталог на локальной машине, что позволяет
легко протестировать приложение. Помните, что для успешного запуска программы
необходимо изменить этот путь так, чтобы он обозначал соответствующий каталог
на вашеймашине. Атрибут href должен указывать имя файла запуска.
Terinformation включает в себя многочисленные подэлементы, предоставляющие ин
формацию о приложении. Они используются административной K O H conw oJava Web
Start или ее эквивалентом, которые устанавливают приложение JNLP и позволяют
пользователю запускать его из командной строки, создавать ярлыки и т. п.
Тег resources служит той же цели, что и тег applet в HTML. Подэлемент j2se указывает
версию naKeTaJ2SE, необходимую для запуска приложения, а в элементе ja r задается
имя архива фopмaтaJAR, в котором находятся классы. В элементе jar также имеется
атрибут download, который может принимать значения eager или lazy. Он указывает
peaлизaцииJNLP, нужно ли полностью загружать apxивJARдлязaпycкa приложения.
Атрибут application-desc сообщает peaлизaцииJNLP, какой класс является запуска
емым в фaйлeJAR (то есть определяет точку входа).
Еще один полезный подэлемент тега jnlp — элемент security, в нашем файле не по
казанный. Он мог бы выглядеть следующим образом:
<security>
<all~permissions/>
<security/>
Продолжительные задачи
Одна из основных ошибок, допускаемых при программировании графического интер
фейса, —случайное использование потокадиспетчеризации событий для выполнения
продолжительной задачи. Простой пример:
//: gui/LongRunningTask.java
// Плохо написанная программа,
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.util.concurrent.*;
import static net.mindview.util.SwingConsole.*; _, л
продолжение тУ
1104 Глава22 • Графическийинтерфейс
try {
TimeUnit.SECONDS.sleep(3);
} catch(InterruptedException e) {
System.out.println(this + " interrupted");
return;
>
System.out.println(this + " completed");
}
public String toString() { return "Task " + id; )
public long id() { return id; }
};
public class InterruptableLongRunningTask extends lFrame {
private lButton
bl = new DButton("Start Long Running Task"),
b2 = new DButton("End Long Running Task");
ExecutorService executor =
Executors.newSingleThreadExecutor();
public InterruptableLongRunningTask() {
bl.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
Task task = new Task();
executor.execute(task);
System.out.println(task + " added to the queue");
>
»J
b2.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
executor.shutdownNow(); // Силовое решение
>
»;
setLayout(new FlowLayoutQ);
add(bl);
add(b2);
>
public static void main(String[] args) {
run(new InterruptableLongRunningTask(), 200, 150);
>
> ///:~
Уже лучше, но при нажатии b2 для объекта ExecutorService вызывается метод shut-
downNow(), который его закрывает. При попыткедобавления новых задач вы получите
исключение. Таким образом, при нажатии кнопки b2 программа становится неработо
способной. Нам требуется другое —закрыть текущую задачу (и отменить ожидающие
задачи) без остановки всего остального. M exaH H 3M java SE5 Callable/Future, описанный
в главе 21, — именно то, что нужно. Мы определим новый класс с именем TaskManager,
который содержит кортежи с объектом Callable, представляющим задачу, и объектом
Future, поступающим от Callable. Необходимость использования кортежа объясняется
тем, что он позволяет нам отслеживать исходную задачу, чтобы мы могли получить
дополнительную информацию, недоступную в Future. Вот как это выглядит:
//: net/mindview/util/TaskItem.java
// Объект Future и объект Callable, который его создает,
package net.mindview.util;
import java.util.concurrent.*;
п р о до л ж ен и е ^>
1106 Глава 22 • Графический интерфейс
return results;
>
} ///:~
Объект TaskManager представляет собой контейнер ArrayList с объектами TaskItem. Он
также содержит однопоточный объект Executor, чтобы при вызове add() для Callable он
отправлял Callable на выполнение и сохранял итоговый объект Future вместе с исход>
ной задачей. Если вам потребуется что-нибудь сделать с задачей, у вас имеется ссылка
на нее. В качестве простого примера в purge() используется метод toString() задачи.
Теперь этот класс может использоваться для управления продолжительными задачами
в нашем примере:
//: gui/InterruptableLongRunningCallable.java
// Использование Callable для выполнения
// продолжительных задач,
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.util.concurrent.*;
import net.mindview.util.*;
import static net.mindview.util.SwingConsole.*;
manager)
tt.task.id(); // Преобразование типа не требуется
for(5tring result : manager.getResults())
System.out.println(result);
>
});
setLayout(new FlowLayout());
add(bl);
add(b2);
add(b3);
>
public static void main(String[] args) {
run(new InterruptableLongRunningCallable(), 200, 150);
>
) ///:~
Как видите, CallableTask делает точно то же самое, что Task, но при этом возвращает
результат — в нашем случае объект String, идентифицирующ ий задачу.
Для решения сходной задачи были созданы средства, не входящие в Swing (и не яв
ляющиеся частью стандартной nocTaBK H java): SwingWorker (на сайте Sun) и Foxtrot
{http://foxtrot.sourceforge.net), но на момент написания книги они еще не были дора
ботаны для использования MexaHH3MaJava SE5 C a lla b le /F u tu r e .
Часто бывает важно предоставить конечному пользователю какие-то визуальные при
знаки того, что задача продолжает выполняться, и информацию о ходе ее выполнения.
Обычно для этой цели используется компонент 3ProgressBar или ProgressMonitor.
В следующем примере используется ProgressMonitor:
//: gui/MonitoredLongRunningCallable.java
// Вывод информации о ходе выполнения
// с использованием ProgressMonitor.
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.util.concurrent.*;
import net.mindview.util.*;
import static net.mindview.util.SwingConsole.*;
SwingUtilities.invokeLater(
new Runnable() {
public void run() {
monitor.setProgress(progress);
>
>
);
>
> catch(InterruptedException e) {
monitor.close();
System.out.println(this + " interrupted");
return "Result: " + this + " interrupted";
>
System.out.println(this + " completed");
return "Result: " + this + " completed";
>
public String toString() { return "Task " + id; }
33. (6) Измените код IntermptabIeLongRunningCaltebte.java так, чтобы все задачи выполня
лись параллельно, а не последовательно.
Визуальные потоки
Следующий пример создает класс 3Panel, реализующий Runnable, который окра
шивает себя в разные цвета. Приложение получает из командной строки значения,
определяющие размер сетки цветов и продолжительность задержки sleep() между
изменениями. Экспериментируя с этими значениями, можно обнаружить некоторые
интересные, а иногда и необъяснимые особенности реализации многопоточности на
вашей платформе:
П : gui/ColorBoxes.java
// Визуальная демонстрация многопоточности.
U {Args: 12 50}
import javax.swing.*;
import java.awt.*;
import java.util.concurrent.*;
import java.util.*;
import static net.mindview.util.SwingConsole.*;
> catch(InterruptedException e) {
// Допустимый способ выхода
>
}
>
public class ColorBoxes extends lFrame {
private int grid = 12;
private int pause = 50;
private static ExecutorService exec =
Executors.newCachedTbreadPool();
public ColorBoxes() {
setLayout(new GridLayout(grid, grid));
for(int i = 0; i < grid * grid; i++) {
CBox cb = new CBox(pause);
add(cb);
exec.execute(cb);
>
}
public static void main(String[] args) {
ColorBoxes boxes = new ColorBoxes();
if(args.length > 0)
boxes.grid = new lnteger(args[0]);
if(args.length > 1)
boxes.pause = new Integer(args[l]);
run(boxes, 500, 400);
>
} ///:~
Класс ColorBoxes настраивает GridLayout так, чтобы компонент содержал grid ячеек
в каждом измерении. Затем он добавляет соответствующее количество объектов CBox
для заполнения сетки, передавая каждому значение pause. В методе main( ) полям pause
и grid назначаются значения по умолчанию, которые можно изменить при передаче
аргументов командной строки.
Вся основная работа выполняется в классе CBox, который является производным от
DPanel и реализует интерфейс Runnable, чтобы каждый компонент 3Panel также мог бы
стать независимой задачей. Этими задачами управляет объект ExecutorService.
Текущим цветом ячейки является cColor. Цвета создаются конструктором Color, полу
чающим 24-разрядное число (в нашем примере оно выбирается случайным образом).
Метод paintComponent() весьма прост; он просто назначает цвет cColor и заполняет
этим цветом всю панель lPanel.
Метод run() содержит бесконечный цикл, который присваивает cColor новый слу
чайный цвет, а затем вызывает repaint() для его отображения. Затем поток приоста
навливается вызовом sleep() на промежуток времени, заданный в командной строке.
О вызове repaint() в run() стоит сказать особо. На первый взгляд может показаться,
что мы создаем множество потоков, каждый из которых заставляет выполнить пере
рисовку. Казалось бы, это нарушает принцип, согласно которому задачи должны ста
виться в очередь событий. Однако эти потоки в действительности не изменяют общий
ресурс. Вызов repaint() не форсирует перерисовку, а всего лишь устанавливает флаг,
который означает, что когда поток диспетчеризации событий в следующий раз будет
1112 Глава 22 • Графический интерфейс
готов к перерисовке, эта область будет кандидатом на перерисовку. Таким образом, эта
программа не создает проблем с потоками в Swing.
Когда поток диспетчеризации событий выполняет paint(), он сначала вызывает paint-
Component(), затем paintBorder() и paintChildren(). Если вы должны переопределить
paint() в производном компоненте, обязательно вызовите версию paint() базового
класса, чтобы были выполнены все необходимые действия.
Именно благодаря гибкости этой архитектуры и привязке потоков к каждому эк
земпляру DPanel вы можете экспериментировать и создать столько потоков, сколько
потребуется. (На практике количество потоков, с которыми нормально справляется
JVM, ограниченно.)
Программа также демонстрирует радикальные различия в производительности и по
ведении между разными реализациями JVM на разных платформах.
34. (4) Измените пример ColorBoxes.java так, чтобы он сначала разбрасывал по экрану
«звездочки», а затем случайным образом изменял их цвета.
Визуальное программирование
и компоненты JavaBean
Читая эту книгу, вы имели возможность убедиться, как хорошо подходит n3biKjava
для создания многократно используемых фрагментов кода. «Единицей многократного
использования» является класс, поскольку он представляет собой связанный набор
характеристик (полей) и действий (методов), а его многократное использование реа
лизуется посредством композиции или наследования.
Наследование и полиморфизм —неотъемлемые составляющие объектно-ориентирован
ного программирования, но в большинстве случаев при создании приложения нужны
компоненты, которые просто делают то, что вам нужно. В идеале разработчик просто
включает эти компоненты в программу —подобно инженеру-электронщику, который
собирает микросхемы на печатной плате. Также кажется, что должен быть и способ
ускорить процесс создания программы посредством аналогичной «модульной сборки».
«Визуальное программирование» стало популярным —чрезвычайно популярным —
с появлением среды разработки программ Visual Basic (VB) от фирмы Microsoft, за
которой последовала среда разработки второго поколения Delphi фирмы Borland
(между прочим, это она стимулировала появление технологии JavaBeans). В этих
программных средах компоненты представлены визуально, что вполне логично, так
как обычно компоненты представляют собой некоторый элемент управления (кнопка,
текстовое поле и т. д.). Общее визуальное представление компонента часто полно
стью совпадает с его внешним видом в работающей программе. Таким образом, часть
процесса визуального программирования заключается в том, что вы перетаскиваете
компонент с палитры и помещаете его на форму. Построитель приложений «пишет»
код, выполняющий необходимые действия, и этот код при выполнении выводит на
экран соответствующий компонент.
Визуальное программирование и компоненты JavaBean 1113
class Spots {}
}
public void addKeyListener(KeyListener 1) {
/ / ...
}
public void removeKeyListener(KeyListener 1) {
/ / ...
}
// "Обычный" открытый метод:
public void croak() {
System.out.println("Ribbet!");
>
> H/ : ~
Во-первых, нетрудно убедиться, что это обыкновенный класс. Обычно все поля класса
объявляются закрытыми (private) и изменяются только с помощью методов и свойств.
По соглашению об именах свойства компонента именуются jumps, color, spots и jumper
(заметьте изменение регистра первой буквы в имени свойства). Хотя в первых трех
случаях имя внутреннего идентификатора класса совпадает с именем свойства, на при
мере свойства jumper вы можете видеть, что свойство не заставляет вас использовать
какой-то конкретный идентификатор для внутренних переменных (более того, этих
переменных может вообще не быть).
Данный компонент Bean позволяет обрабатывать события ActionEvent и KeyEvent, для
чего были добавлены соответствующие методы add и remove. Наконец, обычный метод
croak() также является частью этого компонента — просто потому, что он является
открытым (public), а не потому, что его имя следует какой-то схеме.
pnint("Public methods:");
for(MethodDescriptor m : bi.getMethodDescriptors())
print(m .getMethod().toSt ring());
print("======================");
print("Event support:");
for(EventSetDescriptor e: bi.getEventSetDescriptors()){
print("Listener type:\n ” +
e.getListenerType().getName());
for(Method lm : e.getListenerMethods())
print("Listener method:\n " + lm.getName());
for(MethodDescriptor lmd :
e .getListenerMetbodDescriptors() )
print(''Method descriptor:\n " + lmd.getMethod());
Method addListener= e.getAddListenerMethod();
print("Add Listener Method:\n " + addListener);
Method removeListener = e.getRemoveListenerMethod();
print("Remove Listener Method:\n "+ removeListener);
print( ====================");
}
>
class Dumper implements ActionListener {
public void actionPerformed(ActionEvent e) {
String name = query.getText();
Class<?> c = null;
try {
c = Class.forName(name);
} catch(ClassNotFoundException ex) {
results.setText("Couldn't find " + name);
return;
>
dump(c);
>
>
public BeanDumper() {
DPanel p = new DPanel();
p.setLayout(new FlowLayout());
p.add(new JLabel("Qualified bean name:"));
p.add(query);
add(BorderLayout.NORTH, p);
add(new JScrollPane(results));
Dumper dmpr = new Dumper();
query.addActionListener(dmpr);
query.setText("frogbean.Frog");
// Программный запуск события
dmpr.actionPerformed(new ActionEvent(dmpr, 0, ""));
}
public static void main(String[] args) {
run(new BeanDumper()j 600, 500);
>
} I I I :~
Всю работу выполняет метод BeanDumper.dump(). Сначала он пытается создать объект
BeanInfo, и если это ему удается, он вызывает методы класса Beaninfo дляполучения
информации о свойствах, методах и событиях. При вызове метода lntrospector.
getBeanlnfo() передается второй аргумент. Он сообщает lntrospector, в каком месте
иерархии наследования изучаемого объекта надо закончить исследования. В данном
случае исследования прекращаются при достижении корневого класса Object, так как
методы этого класса нас не интересуют.
1118 Глава22 • Графическийинтерфейс
Property type:
boolean
Property name:
jumper
Read method:
public boolean isJumper()
Write method:
public void setlumper(boolean)
Property type:
int
Property name:
jumps
Read method:
public int getlumps()
Write method:
public void setJumps(int)
Property type:
frogbean.Spots
Property name:
spots
Визуальное программирование и компоненты JavaBean 1119
Read method:
public frogbean.Spots getSpots()
Write method:
public void setSpots(frogbean.Spots)
Public methods:
public void setSpots(frogbean.Spots)
public void setColor(Color)
public void set3umps(int)
public boolean isDumper()
public frogbean.Spots getSpots()
public void croak()
public void addActionListener(ActionListener)
public void addKeyListener(KeyListener)
public Color getColor()
public void set3umper(boolean)
public int getJumps()
public void removeActionListener(ActionListener)
public void removeKeyListener(KeyListener)
Event support:
Listener type:
KeyListener
Listener method:
keyPressed
Listener method:
keyReleased
Listener method:
keyTyped
Method descriptor:
public abstract void keyPressed(KeyEvent)
Method descriptor:
public abstract void keyReleased(KeyEvent)
Method descriptor:
public abstract void keyTyped(KeyEvent)
Add Listener Method:
public void addKeyListener(KeyListener)
Remove Listener Method:
public void removeKeyListener(KeyListener)
Listener type:
ActionListener
Listener method:
actionPerformed
Method descriptor:
public abstract void actionPerformed(ActionEvent)
Add Listener Method:
public void addActionListener(ActionListener)
Remove Listener Method:
public void removeActionListener(ActionListener)
вы здесь видите (подобные методам для чтения и записи свойств), на самом деле полу
чены из объектов Method, которые можно использовать для запуска ассоциированных
с ними методов объекта.
Список открытых (public) методов включает в себя как обыкновенные методы, не
связанные со свойствами или событиями (например, метод croak{)), так и методы,
закрепленные за ними. Это все методы, которые можно программно вызвать для
компонента Bean, и среда разработки готова выдать список доступных методов, чтобы
упростить вашу работу.
Наконец, мы видим, что все события досконально «препарируются»: выдается вся
информация о слушателе, о его методах, о методах для добавления и удаления слуша
теля. В принципе, при наличии объекта B e a n l n f o можно получить всю существенную
информацию о компоненте Bean. Вы даже можете вызывать методы этого компонента,
хотя у вас нет ничего, кроме простого объекта (здесь снова помогает отражение).
public class
BangBean extends JPanel implements Serializable {
private int xm, ymj
private int cSize = 20; // Диаметр окружности
private String text = "Bang!";
private int fontSize = 48;
private Color tColor = Color.RED;
private ActionListener actionListener;
public BangBean() {
addMouseListener(new ML());
addMouseMotionListener(new MML());
>
public int getCircleSize() { return cSize; >
public void setCircleSize(int newSize) {
Визуальное программирование и компоненты JavaBean 1121
cSize = newSize;
}
public String getBangText() { return text; >
public void setBangText(String newText) {
text = newText;
>
public int getFontSize() { return fontSize; }
public void setFontSize(int newSize) {
fontSize = newSize;
>
public Color getTextColor() { return tColor; >
public void setTextColor(Color newColor) {
tColor = newColor;
>
public void paintComponent(Graphics g) {
super.paintComponent(g);
g.setColor(Color.BLACK);
g.drawOval(xm - cSize/2, ym - cSize/2, cSize, cSize);
}
// Одноадресный слушатель, простейшая форма
// управления слушателями:
public void addActionListener(ActionListener 1)
throws TooManyListenersException {
if(actionListener != null)
throw new TooManyListenersException();
actionListener = 1;
>
public void removeActionListener(ActionListener 1) {
actionListener = null;
>
class ML extends MouseAdapter {
public void mousePressed(MouseEvent e) {
Graphics g = getGraphics();
g.setColor(tColor);
g.setFont(
new Font("TimesRoman", Font.BOLD, fontSize));
int width = g.getFontMetrics().stringWidth(text);
g.drawString(text, (getSize().width - width) /2,
getSize().height/2);
g.dispose();
// Вызов метода слушателя:
if(actionListener != null)
actionListener.actionPerformed(
new ActionEvent(BangBean.this,
ActionEvent.ACTION_PERFORMED, null));
>
>
class MML extends MouseMotionAdapter {
public void mouseMoved(MouseEvent e) {
xm = e.getX();
ym = e.getY();
repaint();
>
>
public Dimension getPreferredSize() {
return new Dimension(200, 200);
>
> ///:~
1122 Глава 22 • Графический интерфейс
p u b lic s t a t ic v o id m a in ( S t r in g [ ] a rg s) {
ru n (n e w B a n g B e a n T e s t ( ) , 400, 5 0 0 );
>
> ///:~
Внутри среды разработки этот класс для компонента BangBean не используется, но
тем не менее он достаточно полезен и позволяет провести быстрое тестирование
каждого вашего компонента Bean. Класс BangBeanTest помещает компонент BangBean
в DFrame и присоединяет к нему простого слушателя. Слушатель (ActionListener) вы
водит количество обработанных событий ActionEvent в текстовое поле (3TextField).
Обычно большую часть такого кода автоматически пишет среда разработки, в которой
используется этот компонент.
Когда вы будете исследовать компонент BangBean с помощью программы BeanDumper
или подключите этот компонент к любой среде визуальной разработки приложений,
вы увидите, что у него имеется больше свойств и событий, чем можно обнаружить
по приведенному выше коду. Ведь компонент BangBean унаследован от панели 3Panel,
которая также является визуальным компонентом Bean; следовательно, вы увидите
и ее свойства и события.
35 . (6) Найдите и загрузите из Интернета один или несколько бесплатных построителей
графического интерфейса или же приобретите коммерческий продукт. Определите,
что необходимо сделать для добавления компонента BangBean в эту среду програм
мирования, и выполните эти действия.
Первый пункт достаточно очевиден, а вот над вторым утверждением надо немного по
размыслить. В примере BangBean.java мы обошли проблему синхронизации, игнорируя
ключевое слово synchronized и используя одноадресные события. Вот этот пример,
измененный так, чтобы компонент можно было использовать в многозадачной среде
(к тому же теперь в нем используется групповая обработка событий):
//: gui/BangBean2.java
// Вам следует создавать свои компоненты Bean
// именно таким способом, чтобы их можно было
// использовать в многозадачном окружении,
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.util.*;
import static net.mindview.util.SwingConsole.*;
Name: bangbean/BangBean.class
3ava-Bean: True
Предполагается, что конечному файлу в формате JAR должно быть присвоено имя
BangBean.jar, а манифест находится в файле с HMeHeMBangBean.mf.
change="updateOutput()" />
<mx:Label id="output" text="Hello!" />
</mx:Application>
///:~
Компилирование MXML
Работу с Flcx проще всего начать с бесплатной пробной версии, которую можно за
грузить на сайте www.macromedia.com/software/flex/triaP. Продукт распространяется
в нескольких версиях, от бесплатных пробных до корпоративных серверных; кроме
того, Macromedia предоставляет дополнительные инструменты для разработки прило
жений Flex. Состав каждой версии может изменяться, за подробностями обращайтесь
на сайт Macromedia. Возможно, вам также придется внести изменения в файле jvm.
conflg из каталога bin установки Flex.
Чтобы откомпилировать код MXML в байт-код Flash, у вас имеются два варианта.
1. Поместите файл MXML в веб-приложение^уа вместе со страницами HTM LnJSP
в файл WAR и организуйте компиляцию запросов файла .mxml во время выполнения
при каждом запросе URL-адреса документа MXML из браузера.
2. Откомпилируйте файл MXML при помощи компилятора командной строки Flex
mxmlc.
Для первого варианта (компиляции во время выполнения) помимо Flex потребу
ется контейнер сервлетов (такой, как Apache Tomcat). Файл(-ы) WAR контейнера
сервлетов необходимо обновить данными конфигурации Flex, и он должен включать
JAR-файлы Flex —все эти операции выполняются автоматически при установке Flex.
После настройки фaйлaWAR вы можете поместить файлы MXML в веб-приложение
и запросить URL-адрес документа в любом браузере. Flex откомпилирует приложение
по первому запросу (по аналогии с м оделью ^Р ) и в дальнейшем будет поставлять
откомпилированный и кэшированный SWF-контейнер в оболочке HTML.
Для второго варианта сервер не понадобится. При вызове компилятора Flex mxmlc из
командной строки создаются файлы SWF, которые вы можете развертывать по своему
усмотрению. Исполняемый файл mxmlc находится в каталоге bin установки Flex, а при
запуске без аргументов он выводит список допустимых параметров командной строки.
Обычно в командной строке указывается местоположение библиотеки клиентских
компонентов Flex (значение параметра -flexlib), но в очень простых случаях — как
в первых двух примерах — компилятор Flex может попытаться определить местопо
ложение библиотеки компонентов самостоятельно. Итак, первые два примера можно
откомпилировать следующими командами:1
1 Обратите внимание: загружать надо Flex, а не Flex Builder (это среда разработки).
Построение веб-клиентов Flash с использованием Flex 1133
mxmlc.exe helloflexl.mxml
mxmlc.exe helloflex2.mxml
Команды создают файл helloflex2.swf, который можно запустить в Flash или разместить
вместе с HTML на любом сервере HTTP (после того, как поддержка Flash будет за
гружена в браузере, часто бывает достаточно сделать двойной щелчок на файле SWF,
чтобы запустить его в браузере).
Для файла helloflex2.swf пользовательский интерфейс, отображаемый в Flash Player,
будет выглядеть так.
« __ ___ _
|
Hello:’ T h is was n o tto o hard to d o ...
MXML и ActionScript
MXML представляет собой декларативную сокращенную запись для классов Action
Script. Везде, где вы видите тег MXML, существует класс ActionScript с тем же именем.
Разбирая разметку MXML, компилятор Flex сначала преобразует XML в ActionScript
и загружает классы ActionScript, упоминаемые в ссылках, после чего компилирует
и компонует ActionScript в SWF.
Приложение Flex может быть написано полностью на ActionScript, без использования
MXML. Таким образом, MXML —всего лишь вспомогательный инструмент. Компо
ненты пользовательского интерфейса —такие, как контейнеры и элементы управле
ния, — обычно объявляются в MXML, тогда как логика обработки событий и другая
клиентская логика обеспечивается в коде ActionScriptnJava.
Вы можете создать собственные элементы управления MXML и включить ссылки на
них в разметку MXML, написав для этого классы ActionScript. Также можно объединить
существующие элементы управления и контейнеры MXML в новом документе MXML,
ссылка на который может быть включена в виде тега в другой документ MXML. О том,
как это делается, более подробно рассказано на веб-сайте Macromedia.
Эффекты и стили
Flash Player выводит графику с использованием векторных операций, что позволяет
выполнять выразительные преобразования во время выполнения. Эффекты Flex дают
некоторое представление о таких анимациях. Эти эффекты применяются к событиям
мыши, поддерживаемым элементом управления Image, для albumImage.
Flex также предоставляет эффекты для таких распространенных анимаций, как перехо
ды, постепенная смена изображений и модуляция альфа-каналов. Помимо встроенных
1136 Глава 22 • Графическийинтерфейс
События
Пользовательский интерфейс может рассматриваться как конечный автомат; он вы
полняет действия при изменении состояний. В Flex для управления этими измене
ниями используются события. Библиотека классов Flex содержит обширный набор
элементов управления с многочисленными событиями, описывающими все аспекты
перемещения мыши и использования клавиатуры.
Например, атрибут click элемента управления Button представляет одно из событий
элементауправления. Значение, присвоенное click, представляет функцию или встро
енный фрагмент сценария. Например, в файле MXML элемент управления ControlBar
содержит кнопку refreshSongsButton для обновления списка файлов. Из тега видно,
что при возникновении события click вызывается метод songService.getSongs(). В дан
ном примере событие click элемента управления Button содержит ссылку на объект
RemoteObject, соответствующий методу^уа.
Связывание с Java
Тег RemoteObject в конце файла MXML устанавливает связь с внешним KaaccoMjava
gui.flex.SongService. Клиент Flex использует метод getSongs() KnaccaJava для полу
чения данных DataGrid. Для этого он должен выглядеть как служба (service) —конечная
точка, с которой клиент может обмениваться сообщениями. Служба, определяемая
в теге RemoteObject, содержит атрибут source, обозначающий KnaccJava для RemoteObject,
Построение веб-клиентов Flash с использованием Flex 1137
При заданной конфигурации MXML это приведет к вызову метода getSongs() класса
SongService:
//: gui/flex/SongService.java
package gui.flex;
import java.util.*;
this.songMediaUrl = songMediaUrl;
}
public void setAlbum(String album) { this.album = album;}
public String getAlbum() { return album; }
public void setAlbumImageUrl(String albumImageUrl) {
this.albumImageUrl = albumImageUrl;
>
public String getAlbumImageUrl() { return albumImageUrl;}
public void setArtist(String artist) {
this.artist = artist;
>
public String getArtist() { return artist; }
public void setName(String name) { this.name = name; }
public String getName() { return name; }
public void setSongMediaUrl(String songMediaUrl) {
this.songMediaUrl = songMediaUrl;
>
public String getSongMediaUrl() { return songMediaUrl; }
} ///:~
При инициализации приложения или при нажатии кнопки refreshSongsButton вы
зывается метод getSongs(), а при возвращении управления вызывается обработчик
ActionScriptonSongs(event.result) для заполнения songGrid.
Ниже приведен код ActionScript, включаемый при помощи элемента управления
Script в файле MXML:
//: gui/flex/song5cript.as
function getSongs() {
songService.getSongs();
}
function selectSong(event) {
var song = songGrid.getItemAt(event.itemIndex);
showSongInfo(song);
}
function showSongInfo(song) {
songInfo.text = song.name + newline;
songInfo.text += song.artist + newline;
songInfo.text += song.album + newline;
albumImage.source = song.albumImageUrl;
songPlayer.contentPath = song.songMediaUrl;
songPlayer.visible = true;
>
function onSongs(songs) {
songGrid.dataProvider = songs;
} ///:~
Чтобы обрабатывать выделение ячеек DataGrid, мы добавляем атрибут события cell-
Press в объявление DataGrid в файл MXML:
cellPress="selectSong(event)"
Когда пользователь щелкает на песне в сетке DataGrid, это приводит к вызову select-
Song() в приведенном выше коде ActionScript.
Построение веб-клиентов Rash с использованием Flex 1139
Кроме объектов Java, Flex также может обращаться к веб-службам на базе SOAP
и REST-совместимым службам HTTP с использованием элементов управления Web-
Service и HttpService соответственно. Доступ ко всем службам осуществляется с учетом
ограничений системы безопасности.
Построение и развертывание
В предыдущих примерах мы обходились без флага -flexlib в командной строке, но для
компиляции этой программы необходимо задать местонахождение файла flex-conflg.
xml. В моей установке следующая команда работает, но вам придется привести ее в со
ответствие со своей конфигурацией (команда состоит из одной строки, разбитой для
удобства чтения):
//:! gui/flex/build-conmand.txt
mxmlc -flexlib c:/"Program
Files"/Macromedia/Flex/jrun4/servers/default/flex/WEB-
INF/flex songs.mxml
///:~
Чтобы убедиться в том,что сервер был успешно запущен, откройте в браузере стра
ницу http://localhost:8700/samples и просмотрите примеры (а заодно ознакомьтесь
с возможностями Flex).
Вместо того чтобы компилировать приложение в командной строке, вы также можете
откомпилировать его через сервер. Для этого перенесите исходные файлы, таблицу
стилей CSS и т. д. в каталог jrun4/servers/defaultyflex и обратитесь к ним из браузера по
адресу http://localhost:8700/flex/songs.mxml.
Чтобы успешно запустить приложение, необходимо выполнить настройку как на
cTopoHeJava, так и на стороне Flex.
Java: откомпилированные файлы Song.java и SongService.java должны находиться
в каталоге WEB-INF/classes. Именно здесь должны находиться классы WAR согласно
cпeцификaцииJ2EE. Также можно упаковать файлы в фopмaтJAR и перенести их
в WEB-INF/Iib. Если вы используете JRun, они будут находиться в jrun4/servers/default/
flex/WEB-INF/classes/gui/flex/Song.class и jrun4/servers/default,/flex/WEB-INF/classes/gui/flex/Song-
Service.class соответственно. Также необходимо предоставить веб-приложению доступ
к файлам графики и MP3 ( ^ n J R u n корневым каталогом веб-приложения является
jrun4/servers/default/flex).
Flex: по соображениям безопасности Flex не может обращаться к o6beKTaMjava, если
только вы явно не разрешите такие обращения, внеся соответствующие изменения
Создание приложений SWT 1141
Установка SWT
Для работы приложений SWT необходимо загрузить и установить библиотеку SWT
с сайта проекта Eclipse. Откройте страницу www.eclipse.org/downloads/ и выберите
зеркальный сайт. Перейдите по ссылкам к текущей сборке Eclipse и найдите сжатый
файл с именем, которое начинается с «swt» и включает имя платформы (например,
«win32»). В этом файле вы найдете файл swt.jar. В простейшем варианте установки
файл swt.jar просто помещается в каталог)ге/НЬ/ех1 (при этом вам не придется вносить
никаких изменений в CLASSPATH). При распаковке библиотеки SWT могут обнару
житься дополнительные файлы, которые необходимо установить в соответствующих
местах вашей платформы. Например, в поставку Win32 входят файлы DLL, которые
должны находиться где-то в ja v a .lib ra ry .p a th (обычно совпадает с переменной
окружения PATH, но вы можете выполнить программу objectyShowProperties.java для
получения фактического значения java.library.path). После того как это будет сделано,
вы сможете прозрачно компилировать и выполнять приложения SWT так, как любые
другие программы Java.
Документация SWT находится в отдельном загружаемом пакете.
Другой возможный путь — установка редактора Eclipse, включающего SWT и доку
ментацию SWT, для просмотра которой можно воспользоваться справочной системой
Eclipse.
Первое приложение
Начнем с простейшего приложения в стиле канонической программы «Hello World»:
//: swt/HelloSWT.java
// {Requires: org.eclipse.swt.widgets.Display; You must
// install the SWT library from http://www.eclipse.org >
import org.eclipse.swt.widgets.*;
уровня, в котором строятся все остальыне компоненты. При вызове setText() аргумент
становится текстом надписи в заголовке окна.
Чтобы вывести на экран окно (а следовательно, и приложение), необходимо вызвать
open() для Shell.
Метод main() на этой стадии вызывает neadAndDispatch() для объекта Display (это оз
начает, что в приложении может быть только один объект Display). Метод readAndDis-
patch() возвращает true, если в очереди имеются другие события, ожидающие обработ
ки. В этом случае он должен быть немедленно вызван повторно. Но если ожидающих
событий нет, вызывается метод sleep() объекта Display, который делает непродолжи
тельную паузу, прежде чем снова проверять очередь событий.
Когда программа будет завершена, объект Display необходимо явно освободить вы
зовом dispose (). SWT часто заставляет явно освобождать ресурсы, потому что обычно
используются ресурсы операционной системы, которые могут быть исчерпаны.
Чтобы показать, что объект Shell представляет главное окно программы, следующая
программа создает несколько объектов Shell:
//: swt/ShellsAreMainWindows.java
import org.eclipse.swt.widgets.*j
display.dispose();
}
static boolean shellsDisposed() {
for(int i = 0; i < shells.length; i++)
if(shells[i].isDisposed())
return true;
return false;
>
} / / / :~
При запуске открываются десять главных окон. Если вы закроете одно из них, это
приведет к закрытию всех окон. SWT также использует менджеры расположения —
отличные от тех, которые используются в Swing, но работающие по тому же принципу.
Ниже приведен более сложный пример, который получает текст от System.getProper-
ties () и добавляет его в Shell:
//: swt/DisplayProperties.java
import org.eclipse.swt.*;
import org.eclipse.swt.widgets.*;
import org.eclipse.swt.layout.*;
import java.io.*;
import org.eclipse.swt.widgets.*;
import org.eclipse.swt.layout.*;
import java.util.*;
41. (4) Измените пример DisplayEnvironment.java так, чтобы в нем не использовался класс
SWTConsole.
Меню
Следующий пример демонстрирует работу с простейшими меню: он читает свой ис
ходный код, разбивает его на слова, после чего заполняет меню этими словами:
//: swt/Menus.java
// Развлечения с меню.
import swt.util.*;
import org.eclipse.swt.*;
import org.eclipse.swt.widgets.*;
import java.util.*;
import net.mindview.util.*;
int i = 0;
while(it.hasNext()) {
addItem(bar, it, mItem[i]);
i = (i + 1) % mItem.length;
>
>
static Listener listener = new Listener() {
public void handleEvent(Event e) {
System.out.println(e.toString());
>
>;
void
addItera(Menu bar, Iterator<String> it, MenuItem mItem) {
MenuItera item = new MenuItem(mItem.getMenu(),SWT.PUSH);
item.addListener(SWT.Selection, listener);
item.setText(it.next());
>
public static void main(String[] args) {
SWTConsole.run(new Menus(), 600, 200);
>
> ///:*•
Объект Menu добавляется в Shell, а класс Composite позволяет получить связанный
с ним объект Shell методом getShell(). Класс TextFile взят из пакета netmindview.gtil,
описанного ранее в книге; в данном случае класс TreeSet заполняется словами, чтобы
они упорядочивались по алфавиту. Числа при этом пропускаются. Из потока слов
выбираются имена меню верхнего уровня; затем создаются подменю, которые запол
няются оставшимися словами.
При выборе одной из команд меню Listener просто выводит объект события, чтобы
вы видели, какая информация в нем содержится. При запуске программы становится
видно, что эта информация включает текст меню, поэтому вы можете реагировать на
команду в зависимости от этого текста —или же предоставить разных слушателей для
разных меню (более надежное решение в контексте интернационализации).
tab.setControl(composite);
>
gc.dispose();
updatePoint(e);
>
if(browser != null) {
browser.setUrl("http://www.mindview.netH);
tab.setControl(browser);
>
}
Графика
Вот как выглядит программа Swing SineWave.java, переработанная для SWT:
//: swt/SineWave.java
// SWT-версия программ Swing SineWave.java.
import swt.util.*;
import org.eclipse.swt.*;
import org.eclipse.swt.widgets.*;
import org.eclipse.swt.events.*;
import org.eclipse.swt.layout.*;
c la s s S in e D r a w e x te n d s Canvas {
private static final int SCALEFACTOR = 200;
private int cycles;
private int points;
private double[] sines;
private int[] pts;
public SineDraw(Composite parent, int style) {
super(parent, style);
addPaintListener(new PaintListener() {
public void paintControl(PaintEvent e) {
int maxWidth = getSize().x;
double hstep = (double)maxWidth / (double)points;
int maxHeight = getSize().y;
pts = new int[points];
for(int i = 0; i < points; i++)
pts[i] = (int)((sines[i] * maxHeight / 2 * .95)
+ (maxHeight / 2));
e .g c .setForeground(
e .display.getSystemColor(SWT.COLOR_RED));
for(int i = 1; i < points; i++) {
int xl = (int)((i - 1) * hstep);
int x2 = (int)(i * hstep);
int yl = pts[i - 1];
int y2 = pts[i];
e.gc.drawLine(xl, yl, x2, y2);
>
>
»;
setCycles(5);
}
>
>
} catch(InterruptedException e) {
// Допустимый способ выхода
} catch(SWTException e) {
// Допустимый способ выхода: родитель
// был завершен.
}
}
>
43 . (6) Выберите один из примеров Swing, которые не были переработаны в этом раз
деле, и преобразуйте его для SWT.
Резюме
И з всех библиотек язы ка Java именно библиотека графического пользовательского
интерфейса (G U I) претерпела наиболее значительные изменения в ходе эволюции
языка. Библиотека AWT из a3biKaJava версии 1.0 постоянно подвергалась критике как
одна из самых плохих библиотек, когда-либо созданных в программном обеспечении,
и хотя она позволяла создавать переносимые программы, программы эти были «оди
наково посредственными на всех платформах». К тому же она была ограниченной,
запутанной и попросту неэлегантной в сравнении с инструментами создания прило
жений для определенных платформ.
Когда с новым выпуском Java версии 1.1 появилась улучш енная модель событий
и технология JavaBeans, начался новый этап — теперь стало реально создавать визу
альные компоненты, которые можно было легко перетаскивать и помещать на форму
в средствах визуальной разработки приложений. Вдобавок, разработка модели событий
и KOMnoHeHTOBjavaBean подтвердила тенденцию к упрощению процесса программи
рования и поддержки кода (то, что было забыто в AWT версии 1.0). Но только после
появления библиотеки KnaccoBjFC/Swing работа была закончена. Теперь с помощью
компонентов библиотеки Swing независимое от платформы программирование G U I
перестало быть мечтой.
Настоящая революция произошла в области сред разработки. Если вы хотите, чтобы
коммерческая среда разработки закрьггого язы ка улучшилась, вам остается только
надеяться, чтобы его производитель внес нужные изменения. H o Ja v a — открытая
среда, и это не только дает широкие возможности для конкуренции в области средств
визуального создания программ, но и поощряет эту конкуренцию. Чтобы такие средства
воспринимались серьезно, в них просто необходимо встраивать поддержку технологии
JavaBeans. Это значит, что для всех существуют равные условия: с появлением более
совершенной среды разработки никто не привязан к старой системе —можно легко ее
бросить и перейти к более перспективному инструменту, чтобы повысить свою про
изводительность. Такое здоровое соперничество в области производства визуальных
средств разработки программ сулит программистам только хорошее.
1156 Глава 22 • Графический интерфейс
Данная глава всего лишь дает начальные сведения о программировании GUI. Она
показывает, как легко можно создавать большие приложения, затрачивая минимум
усилий. Того, что вы увидели и о чем узнали, должно хватить для решения большей
части задач, связанных с созданием пользовательского интерфейса. Однако Swing,
SW T и F lash/F lex способны на большее. Вероятно, вы найдете в них средства для
решения любых задача, которые только сможете представить.
Ресурсы
Презентации Бена Гэлбрайта на сайте www.galbraiths.org/presentations неплохо описы
вают возможности Swing и SWT.
Приложение
Программные средства
□ Пакет pa3pa6oTKHjDK с сайта http://javasun.com . Даж е если вы используете среду
разработки программ от стороннего производителя, все равно рядом следует д ер
жать naKeTjDK, натот случай, если вы найдете возможную ошибку в компиляторе.
naK eT jD K — это своего рода эталон, и если вы обнаружите ош ибку в нем, скорее
всего, она уж ехор ош о известна.
□ Документация JDK в формате HTML с сайта http://java.sun.com. Мне еще не при
ходилось видеть справочник по стандартным библиотекам Java, данные которого
не устарели и не упустили ни одной детали. Хотя H TM L-документация фирмы Sun
просто переполнена различными мелкими неточностями, и иногда до неприличия
немногословна, по крайней мере, она содержит описания всех классов и методов.
Поначалу может показаться, что использовать интерактивную документацию не
удобно по сравнению с традиционным печатным справочником, но, по крайней
мере, во время просмотра H TM L-документов вы сможете составить для себя общую
картину. Если это вам не поможет, обратитесь к печатным книгам.
□ JEdit, бесплатный редактор Славы Пестова, написан HaJava, так что вы увидите
н астольж ^ауа-п ри лож ен и е в действии. Редактор широко использует подключа
емые модули (плагины), многие из которых были созданы активным сообществом.
Загружается с сайта wz0w.jedit.org.
1160 Приложение Б. Ресурсы
Книги
□ C oreJava 2 (7-e издание), авторы Хорстманн и Корнелл, 2 тома (издательство
Prentice-H all, 2005). Гигантская, всеобъемлющая книга; именно в ней я первым
делом йщ у ответы на возникающие у меня вопросы. Эту книгу я могу пореко
мендовать вам, если вы закончили чтение Thinking in Java и хотели бы выйти на
новый уровень.
□ The Java Class Libraries: An Annotated Reference, авторы Патрик Чан и Розанна
Ли (издательство Addison-Wesley, 1997). Как ни печально, книга немного устарела,
но именно такой должна быть хорошая интерактивная документация: достаточно
подробные объяснения для практической работы. Один из рецензентов Thinkingin
Java сказал буквально следующее: «Если бы мне разрешили пользоваться только
одной книгой noJava, я бы выбрал эту (в дополнение к вашей, конечно)». Я не раз
деляю его восхищения относительно этой книги. Она большая, дорогая, и качество
примеров меня не удовлетворяет. Но в нее стоит заглянуть, если вы ищете ответ на
сложный вопрос; она глубже (и попросту объемнее) других книг. Тем не менее в Core
Java 2 приведено более актуальное описание многих библиотечных компонентов.
□ Java Network Programming, 2-е издание, автор Эллиот Расти Гарольд (издательство
O ’Reilly, 2000). Я не понимал сетевых механизмов Java, пока не обнаружил эту кни
гу. Ко всему прочему, сайт автора этой книги весьма хорош, он содержит описание
текущих и предстоящих событий в o6лacтиJava-paзpaбoтoк и не симпатизирует
какому-либо гиганту индустрии. Этот сайт постоянно обновляется и никогда не
отстает от времени. См. http://cafeaulait.org.
□ Приемы объектно-ориентированного проектирования, авторы Гамма, Гельм, Джон
сон и Влиссидес (Питер, 2013). Родоначальник концепции паттернов и моделей
в программировании; упоминается во многих главах этой книги.
□ Refactoring to P attem s, автор Джошуа Кериевски (издательство Addison-Wesley,
2005). К нига связы вает р еф ак то р и н г с паттернам и п роекти рован и я. С ам ая
Книги 1161
полезная особенность этой книги заклю чается в том, что она показывает, как
организовать эволюцию архитектуры посредством интеграции паттернов по мере
надобности.
□ The Art ofU N IX Programming, автор Эрик Рзйконд (Addison-Wesley, 2004). Хотя
Java является платформенно-независимым языком, из-за его доминирования на сер
верах разработчикам желательно разбираться в Unix/Linux. Книга Эрика содержит
отличный вводный курс истории и философии этой операционной системы. Это
весьма увлекательное чтиво для тех, кто хочет разобраться в базовых концепциях
компьютерных технологий.
Анализ и планирование
□ Extreme Programming Explained, 2-е издание, авторы Кент Бек и Синтия Андрес
(издательство Addison-Wesley, 2005). Я всегдадумал, что должен быть совершенно
другой, гораздо лучший процесс разработки программ, и, честное слово, экстремаль
ное программирование (X P) близко к этому. Существует еще только одна книга,
оказавшая на меня такое же сильное впечатление, —это Peopleware (описанная чуть
ниже), но она в основном посвящена рабочему окружению и корпоративной куль
туре. Extreme Programming Explained говорит о программировании и переводит все
его понятия,даже совсем недавние «открытия», насвой язык. Они идут настолько
далеко, что иные осмеливаются утверждать, что рисовать схемы и диаграммы можно,
но только если не тратить на это слишком много времени, в противном случае их
надо просто выбросить. (Вы заметите, что на данной книге нет штампа «Одобрено.
UM L».) Я видел разработчиков, которые поступали на работу в компанию только
потому, что практиковалось экстремальное программирование. Небольшая книга,
маленькие главы, не требующие больших усилий для прочтения и захватывающие
ваше внимание с первой страницы. Вы начинаете представлять себя в такой рабочей
атмосфере, и это открывает перед вами абсолютно новый мир.
□ UML DistiUed, 2-е издание, автор Мартин Фаулер (издательство Addison-Wesley,
2000). Когда вы в первый раз сталкиваетесь с UML, кажется, что его невозможно
понять —такое там множество диаграмм и мелочей. Автор утверждает, что большая
часть всего этого попросту никому не нужна, поэтому он отбрасывает малозначи
тельные детали и говорит только о самом главном. При разработке большинства
проектов требуются лишь несколько инструментов построения диаграмм, и цель
книги — научить читателя создавать хорошую структуру и план проекта, не бес
покоясь при этом о мелких деталях. Отличная, тоненькая, «читабельная» книга;
если вам понадобится понять UML, начните с нее.
□ Domain-Driven Design, автор Эрик Эванс (издательство Addison-Wesley, 2004).
Центральное место в этой книге занимает модель предметной области как основ
ной артефакт процесса проектирования. Я всегда считал, что это важное смещение
акцента, которое помогает проектировщикам держаться на правильном уровне
абстракции.
□ The Unified Software D evelopm ent Process, авторы И вар Якобсен, Гради Буч
и Джеймс Рамбо (издательство Addison-Wesley, 1999). Передтем как начать читать
1162 Приложение Б. Ресурсы
Python
□ Learning Python, 2-е издание, авторы Марк Лутц и Дэвид Эшер (издательство
O ’Reilly, 2003). Отлично подходящее для программистов введение в мой любимый
язы к программирования Python, прекрасно сочетающийся cJava. В книге коротко
1164 Приложение Б. Ресурсы
ООО «Питер Пресс», 192102, Санкт-Петербург, ул. Андреевская (д. Волкова), д. 3, литер А, пом. 7H.
Налоговая льгота — общероссийский классификатор продукции OK 034-2014,58.11.12.000 —
Книги печатные професси-ональные, технические и научные.
Подписано в печать 22.10.14. Формат 70x100/16. Усл. п. л. 94,170. Тираж 1500. Заказ 5939
Отпечатано в полном соответствии с качеством предоставленны х издательством материалов
в Чеховский Печатный Двор. 142300, Чехов, Московская область, г. Чехов, ул. Полиграфистов, д.1.
ЛЗЛАТЕПЬСКПЙ лом
Ъ ^ П П Т Е Р
* Z ^ WWW.PITER.COM
ПЗПАТЕЛЬСКПП ПОМ
РОССИЯ
Санкт-Петербург: м. «Выборгская», Б. Сампсониевский np., д. 29а
тел./факс: (812) 703-73-73, 703-73-72; e-mail; sales@piter.com
Москва: м. «Электрозаводская», Семеновская наб., д. 2/1, стр. 1
тел./факс: (495) 234-38-15; e-mail: saies@msk.piter.com
Воронеж: тел.: 8 951 861-72-70; e-mail: voronej@piter.com
Екатеринбург ул. Бебеля, д. 11а
тел./факс: (343) 378-98-41,378-98-42; e-mail: oftice@ekat.piter.com
Нижний Новгород: тел.: 8 960 187-85-50; e-mail: nnovgorod@piter.com
Новосибирск: Комбинатский nep., д. 3
тел./факс: (383) 279-73-92; e-mail: sib@nsk.piter.com
Ростов-на-Дону: ул. Ульяновская, д. 26
тел./факс: (863) 269-91-22, 269-91-30; e-mail: piter-ug@rostov.piter.com
Самара: ул. Молодогвардейская, д. 33a, оф ис 223
тел./факс: (846) 277-89-79, 229-68-09; e-mail: samara@piter.com
УКРАИНА
Киев: Московский np., д. 6, корп. 1 ,о ф и с 33
телУфакс: (044) 490-35-69,490-35-68; e-mail: office@kiev.piter.com
Харьков: ул. Суздальские ряды, д. 12, оф ис 10
тел./факс: (057) 7584145, +38 067 545-55-64; e-mail: piter@kharkov.piter.com
БЕЛАРУСЬ
Минск: ул. Розы Люксембург, д. 163
тел./факс: (517) 208-80-01, 208-81-25; e-mail: minsk@piter.com
С& Заказ книг по почте: на сайте www.piter.com; по тел.: (812) 703-73-74, доб. 6225
с п л д
СЯНКТ-ПЕТЕРБУРГСКЯЯ
ПНТИВИРУСННЯ
ПЯБОРЯТОРИЯ
ДПНИЛОВЯ
В п е р в ы е читатель м о ж е т п о зн а к о м и ть с я с п о л н о й в е р си е й этого
к л а сси ч е ск о го труда, к о то р ы й р а н е е на р у с с к о м я зы ке печатался
в с о к р а щ е н и и . Книга, в ы д ер ж а в ш ая в о р и ги н а л е не о д н о п е р е и зд а н и е ,
за гл уб о ко е и п о и с ти н е ф и л о со ф ск о е и зл о ж е н и е то н к о сте й язы ка Java
считается о д н и м из л у ч ш и х п о со б и й для п р о гр а м м и с т о в .
Ч тобы п о -н а сто я щ е м у п о н я ть fl3biKJava, н е о б х о д и м о р а ссм а тр и в а ть его
не п р о с т о как н а б о р н е к и х к о м а н д и о п е р а то р о в , а п о н я ть его «философию »,
п о д х о д к р е ш е н и ю задач, в с р а в н е н и и с та к о в ы м и в д р у ги х я зы ка х
п р о гр а м м и р о в а н и я . На эти х с т р а н и ц а х а в т о р р а ссказы в ает о б о с н о в н ы х
п р о б л е м а х н а п и са н и я кода: в чем их п р и р о д а и како й п о дхо д и сп о л ьзуе т
Java в их р а зр е ш е н и и . П о это м у о б с у ж д а е м ы е в к а ж д о й главе черты язы ка
н е р а з р ы в н о св я за н ы с тем, как о н и и сп о л ьзую тся для р е ш е н и я
о п р е д е л е н н ы х задач.