Программируем на Java [Марк Лой] (pdf) читать онлайн

-  Программируем на Java  [5-е международное издание] (и.с. Бестселлеры o’reilly) 9.1 Мб, 544с. скачать: (pdf) - (pdf+fbd)  читать: (полностью) - (постранично) - Марк Лой - Патрик Нимайер - Дэниэл Лук

Книга в формате pdf! Изображения и текст могут не отображаться!


 [Настройки текста]  [Cбросить фильтры]

Beijing

Boston Farnham Sebastopol

Tokyo

Программируем
на Java
5-е международное издание

Марк Лой, Патрик Нимайер, Дэниэл Лук

2023

ББК 32.973.2-018.1
УДК 004.43
Л68

Лой Марк, Нимайер Патрик, Лук Дэниэл
Л68 Программируем на Java. 5-е межд. изд. — СПб.: Питер, 2023. — 544 с.: ил. —
(Серия «Бестселлеры O’Reilly»).
ISBN 978-5-4461-1836-6
Неважно кто вы — разработчик ПО или пользователь — в любом случае слышали о языке Java.
В этой книге вы на конкретных примерах изучите основы Java, API, библиотеки классов, приемы и
идиомы программирования. Особое внимание авторы уделяют построению реальных приложений.
Вы освоите средства управления ресурсами и исключениями, а также познакомитесь с новыми
возможностями языка, появившимися в последних версиях Java.

16+ (В соответствии с Федеральным законом от 29 декабря 2010 г. № 436-ФЗ.)

ББК 32.973.2-018.1
УДК 004.43

Права на издание получены по соглашению с O’Reilly. Все права защищены. Никакая часть данной книги
не может быть воспроизведена в какой бы то ни было форме без письменного разрешения владельцев
авторских прав.
Информация, содержащаяся в данной книге, получена из источников, рассматриваемых издательством как
надежные. Тем не менее, имея в виду возможные человеческие или технические ошибки, издательство не
может гарантировать абсолютную точность и полноту приводимых сведений и не несет ответственности за
возможные ошибки, связанные с использованием книги. Издательство не несет ответственности за доступность материалов, ссылки на которые вы можете найти в этой книге. На момент подготовки книги к изданию
все ссылки на интернет-ресурсы являются действующими.

ISBN 978-1492056270 англ.

ISBN 978-5-4461-1836-6

Authorized Russian translation of the English edition of Learning Java, 5th Edition
ISBN 9781492056270 © 2020 Marc Loy, Patrick Niemeyer, Daniel Leuck
This translation is published and sold by permission of O’Reilly Media, Inc.,
which owns or controls all rights to publish and sell the same.
© Перевод на русский язык ООО «Прогресс книга», 2022
© Издание на русском языке, оформление ООО «Прогресс книга», 2022
© Серия «Бестселлеры O’Reilly», 2022

Краткое содержание

Предисловие........................................................................................................................ 16
Глава 1. Современный язык........................................................................................ 23
Глава 2. Первое приложение...................................................................................... 53
Глава 3. Рабочие инструменты................................................................................... 90
Глава 4. Язык Java...........................................................................................................110
Глава 5. Объекты в Java...............................................................................................154
Глава 6. Обработка ошибок и запись в журнал................................................205
Глава 7. Коллекции и обобщения...........................................................................236
Глава 8. Текст, числа, дата и время.........................................................................264
Глава 9. Потоки................................................................................................................302
Глава 10. Десктопные приложения..........................................................................332
Глава 11. Сетевые коммуникации и ввод-вывод................................................390
Глава 12. Веб-программирование............................................................................451
Глава 13. Перспективы Java.........................................................................................486
Приложение. Примеры кода и IntelliJ IDEA..........................................................497
Глоссарий..............................................................................................................................513
Об авторах...........................................................................................................................539
Иллюстрация на обложке...........................................................................................540

Оглавление

Предисловие............................................................................................................16
Кому пригодится эта книга........................................................................................................16
Последние изменения................................................................................................................17
Что нового в этом издании книги (Java 11, 12, 13, 14)............................................18
Структура книги.............................................................................................................................18
Интернет-ресурсы.........................................................................................................................20
Условные обозначения...............................................................................................................20
Использование исходного кода примеров.......................................................................21
Благодарности................................................................................................................................21
От издательства..............................................................................................................................22
Глава 1. Современный язык...................................................................................23
Появление Java...............................................................................................................................24
Происхождение Java.............................................................................................................24
Развитие......................................................................................................................................26
Виртуальная машина...................................................................................................................27
Сравнение Java с другими языками......................................................................................30
Структурная безопасность.......................................................................................................34
Упрощать, упрощать, упрощать…..................................................................................34
Безопасность типов и связывание методов...............................................................35
Инкрементальная разработка..........................................................................................37
Динамическое управление памятью.............................................................................37
Обработка ошибок.................................................................................................................39
Потоки..........................................................................................................................................39
Масштабируемость................................................................................................................40
Безопасность на уровне исполнительной системы Java............................................41
Верификатор.............................................................................................................................42

Оглавление  7
Загрузчики классов................................................................................................................44
Менеджеры безопасности.................................................................................................44
Безопасность на уровнях приложения и пользователя.............................................46
История Java....................................................................................................................................47
Прошлое: Java 1.0 — Java 13..............................................................................................47
Настоящее: Java 14.................................................................................................................49
Будущее.......................................................................................................................................51
Доступные средства..............................................................................................................52
Глава 2. Первое приложение.................................................................................53
Инструменты и среда Java.........................................................................................................53
Установка JDK...........................................................................................................................54
Установка OpenJDK в Linux................................................................................................55
Установка OpenJDK в macOS.............................................................................................56
Установка OpenJDK в Windows.........................................................................................57
Настройка конфигурации IntelliJ IDEA и создание проекта................................61
Запуск программы.................................................................................................................64
Загрузка примеров кода.....................................................................................................64
HelloJava............................................................................................................................................66
Классы..........................................................................................................................................68
Метод main()..............................................................................................................................69
Классы и объекты...................................................................................................................71
Переменные и типы...............................................................................................................72
HelloComponent......................................................................................................................72
Наследование...........................................................................................................................74
Класс JComponent..................................................................................................................75
Отношения между классами.............................................................................................75
Пакеты и импортирование.................................................................................................76
Метод paintComponent().....................................................................................................78
HelloJava2: продолжение...........................................................................................................79
Переменные экземпляра....................................................................................................81
Конструкторы...........................................................................................................................82
События.......................................................................................................................................84
Метод repaint().........................................................................................................................87
Интерфейсы..............................................................................................................................87
До свидания… и снова здравствуйте!.................................................................................89

8  Оглавление
Глава 3. Рабочие инструменты..............................................................................90
Среда JDK..........................................................................................................................................90
Виртуальная машина Java.........................................................................................................91
Запуск приложений Java............................................................................................................91
Системные параметры.........................................................................................................93
Classpath............................................................................................................................................94
javap..............................................................................................................................................95
Модули........................................................................................................................................96
Компилятор Java............................................................................................................................96
Первые эксперименты с Java...................................................................................................98
JAR-файлы...................................................................................................................................... 104
Сжатие...................................................................................................................................... 104
Утилита jar............................................................................................................................... 105
Утилита pack200................................................................................................................... 108
Следующий шаг........................................................................................................................... 109
Глава 4. Язык Java................................................................................................. 110
Кодирование текста.................................................................................................................. 111
Комментарии................................................................................................................................ 113
Комментарии javadoc........................................................................................................ 114
Переменные и константы....................................................................................................... 116
Типы.................................................................................................................................................. 118
Примитивные типы............................................................................................................. 119
Ссылочные типы.................................................................................................................. 124
Автоматическое определение типов......................................................................... 126
Передача ссылок.................................................................................................................. 126
Несколько слов о строках............................................................................................... 128
Команды и выражения............................................................................................................. 128
Команды................................................................................................................................... 129
Выражения.............................................................................................................................. 138
Массивы.......................................................................................................................................... 144
Типы массивов...................................................................................................................... 145
Создание и инициализация массива.......................................................................... 146
Использование массивов................................................................................................ 148
Анонимные массивы.......................................................................................................... 149

Оглавление  9
Многомерные массивы..................................................................................................... 150
Типы, классы, массивы... и jshell........................................................................................... 152
Глава 5. Объекты в Java....................................................................................... 154
Классы.............................................................................................................................................. 155
Объявление классов и создание экземпляров..................................................... 156
Обращение к полям и методам..................................................................................... 158
Статические поля и методы............................................................................................ 163
Методы............................................................................................................................................ 165
Локальные переменные................................................................................................... 166
Замещение.............................................................................................................................. 167
Статические методы........................................................................................................... 169
Инициализация локальных переменных................................................................. 171
Передача аргументов и ссылки.................................................................................... 172
Обертки для примитивных типов................................................................................ 174
Перегрузка методов........................................................................................................... 176
Создание объектов.................................................................................................................... 177
Конструкторы........................................................................................................................ 178
Работа с перегруженными конструкторами........................................................... 179
Уничтожение объектов............................................................................................................ 181
Уборка мусора....................................................................................................................... 181
Пакеты............................................................................................................................................. 183
Импортирование классов............................................................................................... 183
Пользовательские пакеты............................................................................................... 185
Видимость полей и методов класса............................................................................ 187
Компиляция с пакетами.................................................................................................... 189
Нетривиальное проектирование классов...................................................................... 189
Субклассирование и наследование............................................................................ 190
Интерфейсы........................................................................................................................... 195
Внутренние классы............................................................................................................. 198
Анонимные внутренние классы................................................................................... 200
Систематизация кода и планирование на случай ошибок..................................... 202
Глава 6. Обработка ошибок и запись в журнал............................................... 205
Исключения.................................................................................................................................. 206
Исключения и классы ошибок....................................................................................... 207

10  Оглавление
Обработка исключений.................................................................................................... 209
Всплывающие исключения............................................................................................. 212
Трассировка стека............................................................................................................... 213
Проверяемые и непроверяемые исключения....................................................... 214
Выдача исключений........................................................................................................... 215
«Расползание» блока try................................................................................................... 219
Секция finally......................................................................................................................... 220
try с ресурсами..................................................................................................................... 221
Обработка исключений и быстродействие ............................................................ 223
Проверочные утверждения.................................................................................................. 223
Включение и отключение проверочных утверждений..................................... 225
Использование проверочных утверждений.......................................................... 225
Журнальный API.......................................................................................................................... 227
Общие сведения.................................................................................................................. 227
Уровни вывода...................................................................................................................... 230
Простой пример................................................................................................................... 231
Конфигурирование журнального API........................................................................ 232
Протоколировщик.............................................................................................................. 234
Быстродействие................................................................................................................... 234
Исключения в реальном мире............................................................................................. 235
Глава 7. Коллекции и обобщения....................................................................... 236
Коллекции...................................................................................................................................... 236
Интерфейс Collection......................................................................................................... 237
Разновидности коллекций.............................................................................................. 238
Интерфейс Map..................................................................................................................... 240
Ограничения типов................................................................................................................... 242
Контейнеры............................................................................................................................ 243
Можно ли улучшить контейнеры?............................................................................... 244
Знакомство с обобщениями.................................................................................................. 245
Несколько слов о типах.................................................................................................... 248
«Ложки не существует»............................................................................................................ 249
Стирание типов.................................................................................................................... 250
Необработанные типы...................................................................................................... 252
Отношения между параметризованными типами...................................................... 253
Почему List не является List?........................................................ 255

Оглавление  11
Преобразования типов............................................................................................................ 256
Преобразования между коллекциями и массивами........................................... 258
Итератор.................................................................................................................................. 258
Метод sort().................................................................................................................................... 260
Приложение: деревья на поле............................................................................................. 260
Заключение................................................................................................................................... 263
Глава 8. Текст, числа, дата и время..................................................................... 264
Строки.............................................................................................................................................. 264
Создание строк..................................................................................................................... 265
Создание строк по другим данным............................................................................. 266
Сравнение строк.................................................................................................................. 267
Поиск......................................................................................................................................... 268
Список методов String....................................................................................................... 269
Создание объектов по строкам........................................................................................... 271
Разбор примитивных чисел............................................................................................ 271
Разбиение текста на лексемы........................................................................................ 272
Регулярные выражения........................................................................................................... 274
Общие сведения о регулярных выражениях......................................................... 274
API регулярных выражений java.util.regex............................................................... 281
Математические средства...................................................................................................... 286
Класс java.lang.Math............................................................................................................ 287
Большие числа...................................................................................................................... 291
Дата и время................................................................................................................................. 292
Локальные дата и время.................................................................................................. 293
Создание и обработка значений даты и времени................................................ 294
Часовые пояса....................................................................................................................... 295
Разбор и форматирование даты и времени............................................................ 296
Разбор ошибок...................................................................................................................... 298
Временные метки................................................................................................................ 300
Другие полезные средства.................................................................................................... 300
Глава 9. Потоки...................................................................................................... 302
Знакомство с потоками............................................................................................................ 303
Класс Thread и интерфейс Runnable........................................................................... 304
Управление потоками....................................................................................................... 307
Смерть потоков.................................................................................................................... 313

12   Оглавление
Синхронизация........................................................................................................................... 315
Организация последовательного доступа к методам........................................ 316
Обращение к переменным классов и экземпляров
из нескольких потоков...................................................................................................... 321
Планирование и приоритеты............................................................................................... 323
Состояние потока................................................................................................................ 324
Квантование........................................................................................................................... 325
Приоритеты............................................................................................................................ 326
Уступка управления............................................................................................................ 327
Быстродействие потоков........................................................................................................ 328
Цена синхронизации......................................................................................................... 328
Потребление ресурсов потоками................................................................................ 329
Вспомогательные средства параллелизма.................................................................... 330
Глава 10. Десктопные приложения.................................................................... 332
Кнопки, ползунки и текстовые поля.................................................................................. 333
Иерархии компонентов.................................................................................................... 333
Архитектура «Модель — представление — контроллер»............................... 334
Надписи и кнопки................................................................................................................ 335
Текстовые компоненты..................................................................................................... 341
Другие компоненты............................................................................................................ 348
Контейнеры и макеты............................................................................................................... 353
JFrame и JWindow................................................................................................................ 353
JPanel......................................................................................................................................... 355
Менеджеры макетов.......................................................................................................... 356
События.......................................................................................................................................... 365
События мыши...................................................................................................................... 365
События действий............................................................................................................... 369
События изменений........................................................................................................... 371
Другие события.................................................................................................................... 372
Модальные и всплывающие окна....................................................................................... 373
Диалоговые окна сообщений........................................................................................ 374
Диалоговые окна подтверждения............................................................................... 377
Диалоговые окна ввода.................................................................................................... 378

Оглавление  13
Влияние многопоточности..................................................................................................... 379
SwingUtilities и обновление компонентов............................................................... 380
Таймеры................................................................................................................................... 383
Что дальше?................................................................................................................................... 386
Меню......................................................................................................................................... 386
Хранение конфигураций.................................................................................................. 388
Нестандартные компоненты и Java 2D...................................................................... 388
JavaFX........................................................................................................................................ 389
Думайте о пользователях....................................................................................................... 389
Глава 11. Сетевые коммуникации и ввод-вывод............................................ 390
Потоки данных............................................................................................................................. 390
Базовый ввод-вывод.......................................................................................................... 393
Символьные потоки данных........................................................................................... 395
Обертки для потоков данных......................................................................................... 397
Класс java.io.File.................................................................................................................... 401
Файловые потоки данных................................................................................................ 407
RandomAccessFile................................................................................................................ 410
Файловый API пакета NIO....................................................................................................... 411
FileSystem и Path................................................................................................................... 411
Файловые операции NIO.................................................................................................. 414
Пакет NIO........................................................................................................................................ 417
Асинхронный ввод-вывод............................................................................................... 418
Быстродействие................................................................................................................... 419
Отображаемые и блокируемые файлы..................................................................... 419
Каналы...................................................................................................................................... 419
Буферы...................................................................................................................................... 420
Кодировщики и декодировщики символов............................................................ 424
FileChannel.............................................................................................................................. 426
Сетевое программирование................................................................................................. 430
Сокеты............................................................................................................................................. 432
Клиенты и серверы............................................................................................................. 433
Клиент DateAtHost............................................................................................................... 438
Сетевая игра........................................................................................................................... 440
Дальнейшие исследования................................................................................................... 450

14  Оглавление
Глава 12. Веб-программирование..................................................................... 451
URL..................................................................................................................................................... 451
Класс URL........................................................................................................................................ 452
Потоковые данные.............................................................................................................. 454
Получение контента в виде объекта.......................................................................... 454
Управление соединениями............................................................................................. 456
Проблема обработчиков.................................................................................................. 457
Полезные фреймворки обработчиков...................................................................... 458
Взаимодействие с веб-приложениями............................................................................. 458
Метод GET............................................................................................................................... 459
Метод POST............................................................................................................................. 460
HttpURLConnection............................................................................................................. 464
HTTPS и безопасная передача данных...................................................................... 464
Веб-приложения Java............................................................................................................... 464
Жизненный цикл сервлета.............................................................................................. 466
Сервлеты................................................................................................................................. 467
Сервлет HelloClient............................................................................................................. 469
Ответ сервлета...................................................................................................................... 471
Параметры сервлетов....................................................................................................... 472
Сервлет ShowParameters.................................................................................................. 473
Управление сеансами пользователей....................................................................... 475
Сервлет ShowSession......................................................................................................... 476
Контейнеры сервлетов............................................................................................................ 479
Настройка конфигурации с использованием web.xml и аннотаций........... 480
URL-шаблоны......................................................................................................................... 483
Развертывание сервлета HelloClient........................................................................... 484
Безграничный интернет.......................................................................................................... 485
Глава 13. Перспективы Java................................................................................ 486
Выпуски Java................................................................................................................................. 486
JCP и JSR................................................................................................................................... 487
Лямбда-выражения................................................................................................................... 488
Переработка существующего кода............................................................................. 489
За пределами базовых возможностей Java.................................................................... 495
Заключение и следующие шаги........................................................................................... 495

Оглавление  15
Приложение. Примеры кода и IntelliJ IDEA...................................................... 497
Где загрузить примеры кода................................................................................................. 497
Установка IntelliJ IDEA............................................................................................................... 499
Установка в Linux................................................................................................................. 499
Установка в macOS.............................................................................................................. 499
Установка в Windows.......................................................................................................... 500
Импортирование примеров кода....................................................................................... 502
Запуск примеров кода............................................................................................................. 505
Загрузка кода веб-приложений........................................................................................... 509
Работа с сервлетами.................................................................................................................. 510
Глоссарий............................................................................................................... 513
Об авторах............................................................................................................. 539
Иллюстрация на обложке.................................................................................... 540

Предисловие

Эта книга научит вас программировать на языке Java и использовать среду разработки приложений. Если вы разработчик или опытный интернет-пользователь,
то наверняка слышали об этом языке. Его появление стало одним из ярчайших
событий в истории интернета, а бизнес в интернете вырос до сегодняшнего
уровня во многом благодаря Java-приложениям. Вероятно, Java является самым
популярным в мире языком программирования. Миллионы разработчиков пишут Java-приложения почти для всех видов компьютеров, которые только можно
представить. В отношении спроса на программистов Java превосходит такие
языки, как C++ и Visual Basic. Он стал фактическим стандартом для разработки
некоторых видов программного обеспечения, особенно веб-сервисов. Многие
вузы включают Java в начальные курсы программирования наряду с другими
актуальными современными языками. Возможно, вы прямо сейчас читаете эти
слова на своих учебных занятиях!
Книга даст вам хорошее представление об основах языка Java, в том числе об
интерфейсах программирования приложений (API), библиотеках классов,
приемах программирования и идиомах. Мы подробно рассмотрим многие
интересные области, а некоторые темы затронем лишь в общих чертах. Другие
книги издательства O’Reilly продолжают с того уровня знаний, на котором мы
остановимся, и предоставляют более полную информацию о конкретных областях и сферах применения Java.
В подходящих случаях мы будем показывать наглядные, реалистичные и интересные примеры кода, избегая монотонного перечисления возможностей. Эти
примеры очень просты, но они подскажут вам, что вы сможете делать с помощью
Java самостоятельно. Мы не хотим заниматься на этих страницах разработкой
очередного «приложения-бестселлера», а вместо этого постараемся дать вам
отправную точку для долгих экспериментов и вдохновить на ваш собственный
проект такого масштаба.

Кому пригодится эта книга
Эта книга написана для профессионалов в области информационных технологий, для студентов, технических специалистов и «финских хакеров». Она

Последние изменения  17

будет полезна всем, кому нужен практический опыт работы с Java, особенно
с целью создания реальных приложений. Кроме того, книгу можно использовать
в качестве экспресс-курса по объектно-ориентированному программированию,
сетевым приложениям и пользовательским интерфейсам.
В процессе изучения Java вы освоите эффективный и практичный подход
к разработке программного обеспечения, началом которого станет глубокое
понимание основ языка Java и его API.
На первый взгляд Java имеет много общего с языками C и C++, и если у вас
есть опыт программирования на одном из них, то вам будет проще изучить Java.
Но если у вас нет такого опыта, не огорчайтесь. Не надо уделять излишнее внимание синтаксическому сходству между Java и C или C++. Во многих отношениях
Java ближе к более динамическим языкам, таким как Smalltalk и Lisp. Хорошо,
если вы уже знаете другие объектно-ориентированные языки, но в этом случае
вам придется, скорее всего, пересмотреть некоторые представления и изменить
некоторые привычки. Считается, что язык Java намного проще таких языков,
как C++ и Smalltalk. Если вы хорошо учитесь на коротких примерах и на собственном опыте, то эта книга должна вам понравиться.
В конце книги мы будем рассматривать Java в контексте веб-приложений, вебслужб и обработки запросов, поэтому вы должны хотя бы в общих чертах знать,
как устроены браузеры, серверы и документы.

Последние изменения
Это, пятое, издание книги «Программируем на Java» («Learning Java») можно
также считать седьмым, обновленным и переименованным изданием нашей
предыдущей популярной книги «Exploring Java». В каждом очередном издании
мы старались не только добавлять материал о новых возможностях языка, но
и тщательно пересматривать и обновлять весь существующий материал, чтобы
систематизировать его и отражать на страницах наш многолетний опыт исследований и практического программирования.
Одно из заметных изменений в последних изданиях книги — сокращение (а затем
и удаление) материала о работе с апплетами, которые теперь уже практически
не используются при создании интерактивных веб-страниц. С другой стороны,
значительно расширены темы веб-приложений и веб-служб Java, ставших вполне
зрелыми технологиями.
Мы рассматриваем все важные особенности последней (на момент написания
книги) из тех версий Java, которые сопровождаются долгосрочной поддержкой
от Oracle. Это Java 11, а ее полное название — Java Standard Edition (SE) 11.
(Бесплатный аналог — OpenJDK 11.) Кроме того, мы упоминаем некоторые

18  Предисловие
особенности трех промежуточных версий: Java 12, Java 13 и Java 14. В компании
Sun Microsystems, которая была «хранителем» Java до Oracle, за много лет несколько раз меняли схему нумерации версий. Чтобы подчеркнуть множество
ценных возможностей, появившихся в версии Java 1.2, ее обозначили термином
«Java 2», а также отказались от термина JDK в пользу SDK. Шестая по порядку
версия (следующая после Java 1.4) получила название Java 5.0, и тогда же Sun
вернула термин JDK. Только после этого продолжилась обычная нумерация:
вышли версии Java 6, Java 7 и т. д.
Сейчас перед нами Java 14. Эта версия представляет собой хорошо развитый
язык, в котором появился ряд изменений синтаксиса и обновлений API и библиотек. Мы постарались отразить эти новые возможности в примерах кода, чтобы
показать современные приемы и стиль программирования на Java.

Что нового в этом издании книги (Java 11, 12, 13, 14)
Это издание мы по традиции переработали таким образом, чтобы сделать его как
можно более полным и актуальным. Мы учли изменения, появившиеся в Java 11
(напомним: это версия с долгосрочной поддержкой), а также в промежуточных
версиях: Java 12, 13 и 14. (Подробнее о средствах Java, включенных в последние
версии и исключенных из них, рассказано в главе 13.) Мы добавили в это издание следующие темы:
Новые возможности языка, в том числе автоматическое определение (выведение) типов в обобщениях, усовершенствованная обработка исключений
и синтаксис автоматического управления ресурсами.
Интерактивная среда jshell для экспериментов с фрагментами кода.
Выражения switch.
Лямбда-выражения.
Обновленные примеры и объяснения по всей книге.

Структура книги
Структура книги выглядит примерно так:
Главы 1 и 2 содержат введение в концепцию языка Java, а также простейшее
руководство, которое поможет вам немедленно приступить к программированию.
В главе 3 рассматриваются важнейшие инструменты для разработки программ на Java (компилятор, интерпретатор, jshell и упаковщик JAR).

Структура книги  19

В главах 4 и 5 представлены фундаментальные концепции программирования, после чего описывается сам язык Java. Изложение начинается с базового синтаксиса, а затем переходит к классам и объектам, исключениям,
массивам, перечислениям, аннотациям и другим темам.
В главе 6 рассматриваются исключения, способы обработки ошибок и средства журналирования (логирования).
В главе 7 рассматриваются коллекции, обобщения и отношения между параметризованными типами Java.
Глава 8 посвящена обработке текста, математическим вычислениям и некоторым другим средствам базового API.
В главе 9 рассматриваются средства для создания многопоточных приложений.
В главе 10 представлены основы разработки графических интерфейсов
(GUI) с помощью пакета Swing.
Глава 11 посвящена вводу-выводу Java, потокам данных, файлам, сокетам,
сетям и пакету NIO.
В главе 12 рассматриваются веб-приложения, сервлеты, WAR-файлы и вебслужбы.
Глава 13 рассказывает о процессе развития Java. Она поможет вам отслеживать будущие изменения в языке и модернизировать существующий код,
используя новые возможности (например, лямбда-выражения, впервые
представленные в Java 8).
Если вы похожи на нас, то вы не читаете книги с начала до конца. А если вы
очень похожи на нас, то наверняка не станете читать это предисловие. Но вдруг
вы все-таки с него начнете? На этот случай мы дадим несколько рекомендаций:
Если вы программист и хотите за пять минут понять всю суть Java, то вас,
скорее всего, заинтересуют примеры кода. Для начала просмотрите главу 2.
Если она не вызовет энтузиазма, перейдите к главе 3 — там рассказано, как
использовать компилятор и интерпретатор. Это станет хорошим первым
шагом.
Если вы собираетесь писать приложения для работы в локальной сети или
в интернете, обратитесь к главам 11 и 12. Сетевые функции — это одна из
самых интересных и важных частей Java.
В главе 10 рассматриваются графические средства и компоненты Java.
Это важно, если вы собираетесь писать обычные десктопные приложения
с графическим интерфейсом (то есть приложения для настольных компьютеров).

20  Предисловие
Глава 13 рассказывает о том, как всегда быть в курсе происходящих в языке
Java изменений, независимо от того, что именно вас интересует.

Интернет-ресурсы
В интернете есть множество источников с информацией о языке Java. Прежде
всего, достоверную информацию вы найдете на официальном сайте Oracle (https://
www.oracle.com/java/technologies). В частности, Oracle публикует документацию
с описаниями классов, методов, операторов и других синтаксических конструкций языка, а также дистрибутивы выпусков Java. Именно с сайта Oracle вам
лучше всего загрузить эталонную реализацию JDK, которая включает в себя
компилятор, интерпретатор и другие инструменты. Oracle также поддерживает
официальный сайт проекта OpenJDK (https://openjdk.java.net) — так называется
основная версия Java с открытым исходным кодом, в состав которой также входят компилятор, интерпретатор и другие инструменты. Мы будем использовать
OpenJDK для всех примеров кода в этой книге.

Условные обозначения
В книге используются следующие шрифтовые обозначения:
Исходный код

Этим шрифтом выделены примеры исходного кода, а также приглашения
командной строки и результаты текстового вывода на экран.
Комментарии в исходном коде

Этим шрифтом в исходном коде выделены комментарии.
Команды и заменяемые элементы

Этим шрифтом выделены команды, которые вводятся в командной строке,
а также те элементы в исходном коде, которые должны быть заменены читателем.
Служебные слова и символы

Этим шрифтом в основном тексте выделены слова и символы, которые
обозначают классы, методы, команды, файлы, пути, форматы, параметры,
значения, теги и другие компьютерные сущности.

Благодарности  21

Термины
Этим шрифтом в основном тексте выделены термины, когда они вводятся
впервые, а также важные понятия и названия.
Веб-ссылки
Этим шрифтом в основном тексте выделены адреса веб-сайтов с полезной
информацией.

Использование исходного кода примеров
Если у вас возникнут вопросы технического характера по использованию примеров кода, направляйте их по электронной почте на адрес bookquestions@oreilly.com.
В общем случае все примеры кода из книги вы можете использовать в своих программах и в документации. Вам не нужно обращаться в издательство за разрешением, если вы не собираетесь воспроизводить существенные части программного
кода. Если вы разрабатываете программу и используете в ней несколько фрагментов кода из книги, вам не нужно обращаться за разрешением. Но для продажи или
распространения примеров из книги вам потребуется разрешение от издательства
O’Reilly. Вы можете отвечать на вопросы, цитируя данную книгу или примеры
из нее, но для включения существенных объемов программного кода из книги
в документацию вашего продукта потребуется разрешение.
Мы рекомендуем, но не требуем добавлять ссылку на первоисточник при цитировании. Под ссылкой на первоисточник мы подразумеваем указание авторов,
издательства и ISBN.
За получением разрешения на использование значительных объемов программного кода из книги обращайтесь по адресу permissions@oreilly.com.

Благодарности
Многие люди внесли свой вклад в работу над книгой — как в ее первоначальном
варианте «Exploring Java», так и в этом издании. Прежде всего, мы благодарим
Тима О’Рейли за то, что он предоставил нам возможность написать эту книгу.
Спасибо Майку Лукидесу (Mike Loukides), редактору всей серии; его терпение и опыт постоянно направляют нас на истинный путь. Другие сотрудники
O’Reilly, в том числе Амелия Блевинс (Amelia Blevins), Зен Маккуэйд (Zan
McQuade), Корбин Коллинз (Corbin Collins) и Джессика Хаберман (Jessica
Haberman), непрестанно делились с нами своим опытом и вдохновением. Это

22  Предисловие
предел наших мечтаний — работать со столь квалифицированной и доброжелательной командой.
Исходная версия глоссария позаимствована из книги Дэвида Фленагана (David
Flanagan) «Java in a Nutshell», вышедшей в издательстве O’Reilly. Также из книги
Дэвида взяты некоторые диаграммы об иерархии классов. Эти диаграммы построены на основе похожих диаграмм Чарльза Л. Перкинса (Charles L. Perkins).
Мы искренне благодарны Рону Бекеру (Ron Becker) за дельные советы и интересные идеи с точки зрения дилетанта, далекого от мира программирования.
Благодарим Джеймса Эллиота (James Elliott) и Дэна Лука (Dan Leuck) за их
превосходные и актуальные отзывы о техническом содержании этого издания.
Как это часто бывает в мире программирования, взгляд со стороны бесценен.
Нам очень повезло, что рядом с нами оказалисьтакие внимательные люди.

От издательства
Ваши замечания, предложения, вопросы отправляйте по адресу comp@piter.com
(издательство «Питер», компьютерная редакция).
Мы будем рады узнать ваше мнение!
На веб-сайте издательства www.piter.com вы найдете подробную информацию
о наших книгах.

ГЛАВА 1

Современный язык

Сегодня самые сложные проекты и самые грандиозные возможности для разработчиков связаны с освоением потенциала сетей. Приложения (программы),
создаваемые в наши дни, независимо от их масштаба и предполагаемой аудитории, почти наверняка будут работать на устройствах, подключенных к интернету.
Сети становятся все более необходимыми, и соответственно меняются требования к привычному программному обеспечению и возникает спрос на совершенно
новые виды приложений, список которых стремительно разрастается.
Всем нам нужны такие программные продукты, которые одинаково работают на
всех платформах и хорошо совместимы с другими приложениями. Нам нужны
динамические приложения, использующие всю мощь глобальной сети и взаимодействующие с разнородными распределенными источниками информации.
Нам нужно по-настоящему распределенное ПО, которое легко расширяется
и обновляется. Нам нужны «умные» приложения, способные самостоятельно
анализировать интернет, выискивать нужную информацию и выполнять функции электронных агентов. Потребность в таких приложениях возникла давно,
но они начали появляться только в последние годы.
В прошлом главная проблема была связана с недостаточными возможностями инструментария для создания таких приложений. Требования к скорости
и портируемости были по большей части взаимоисключающими, а о безопасности часто забывали или понимали ее неправильно. Полностью портируемые
языки были громоздкими, интерпретируемыми и медленными. Но они все равно
пользовались популярностью: как из-за своей высокоуровневой функциональности, так и из-за портируемости. В быстрых языках скорость достигалась путем
привязки к конкретным платформам, поэтому о полноценной портируемости
говорить не приходилось. Существовали и безопасные языки, но они в основном
были ответвлениями портируемых языков и имели те же недостатки. В отличие от них, Java — современный язык, в котором сочетаются все три качества:
портируемость, скорость и безопасность. Именно поэтому Java занимает доминирующее положение в мире программирования более чем через 20 лет после
своего появления.

24  Глава 1. Современный язык

Появление Java
Язык Java, разработанный в Sun Microsystems под руководством асов сетевых технологий Джеймса Гослинга (James Gosling) и Билла Джоя (Bill Joy), проектировался как машинно-независимый язык программирования, достаточно безопасный
для работы в сети и при этом достаточно быстрый для замены низкоуровневого
машинного кода. Java решил все названные выше проблемы и сыграл важную роль
в развитии интернета, что привело нас к нынешнему положению вещей.
На первых порах энтузиазм по поводу Java был основан прежде всего на возможности создания встраиваемых в веб-страницы приложений, называемых
апплетами (applets). Но в те времена возможности апплетов и клиентских
приложений с графическим интерфейсом, написанных на Java, были невелики.
Зато теперь в Java есть Swing — полноценный инструментарий для создания
графических интерфейсов. В результате язык Java стал хорошей платформой
для разработки традиционных клиентских приложений, хотя в этой области
у него появилось немало конкурентов.
Но что еще важнее, язык Java стал ведущей платформой для приложений на базе
веб-технологий и веб-служб. Эти приложения используют разные технологии,
включая API сервлетов Java, веб-службы Java и многие популярные фреймворки и серверы приложений Java — как коммерческие, так и с открытым кодом.
Благодаря своей портируемости и скорости, язык Java завоевал репутацию
оптимальной платформы для разработки современных бизнес-приложений.
Серверы Java, работающие на платформах Linux с открытым кодом, заняли
центральное место в деловом и финансовом мире.
Эта книга покажет вам, как использовать Java для решения реальных задач программирования. В последующих главах рассматриваются многие возможности:
от обработки текста до сетевых коммуникаций, создания десктопных приложений на базе Swing и облегченных приложений на базе веб-технологий и служб.

Происхождение Java
Фундамент Java заложил в 1990-е годы Билл Джой (Bill Joy) — патриарх и ведущий исследователь компании Sun Microsystems. В то время она конкурировала
на относительно небольшом рынке рабочих станций, а компания Microsoft
начала доминировать на более массовом рынке персональных компьютеров
с процессорами Intel. Когда стало ясно, что Sun опоздала на поезд революции
персональных компьютеров, Джой переехал в Аспен (штат Колорадо) для проведения перспективных исследований. Ему нравилась идея решения сложных
задач простыми программными средствами, и он основал компанию с подходящим названием Sun Aspen Smallworks.

Появление Java  25

В небольшую команду программистов, собравшихся в Аспене, одним из первых пришел Джеймс Гослинг (James Gosling), которого называют отцом Java.
В начале 1980-х он прославился как автор Gosling Emacs — новой версии
текстового редактора Emacs, которую он написал на языке C для операционной системы Unix. Gosling Emacs стал популярным, но вскоре его затмила
бесплатная версия под названием GNU Emacs, написанная разработчиком
исходной версии Emacs. К тому времени Гослинг переключился на разработку
оконной системы Sun NeWS, которая в 1987 году конкурировала с X Window
System за позицию графической оболочки Unix. И хотя некоторые считают,
что система NeWS превосходила X, она все-таки проиграла, потому что компания Sun сделала ее проприетарной и не стала публиковать исходный код,
тогда как первые разработчики X сформировали «Консорциум X» и пошли по
противоположному пути.
В ходе работы над NeWS Гослинг осознал всю мощь интеграции выразительного
языка с оконным графическим интерфейсом, поддерживающим работу в сети.
И в компании Sun поняли, что сообщество интернет-программистов категорически не хочет принимать проприетарные стандарты, даже самые замечательные.
Неудача NeWS заложила основу схемы лицензирования Java и открытого кода
(хотя до настоящего «open source» дело не дошло). Гослинг принес полученные
знания в новый проект Билла Джоя. В 1992 году Sun основала для этого проекта дочернюю компанию FirstPerson. Она должна была вывести Sun на рынок
бытовой электроники.
Команда FirstPerson создавала программное обеспечение для таких устройств,
как сотовые телефоны и карманные компьютеры (PDA).
Работающие в этих устройствах приложения должны были в реальном времени
передавать данные через дешевые инфракрасные интерфейсы и традиционные
пакетные сети. Из-за недостатка оперативной памяти и малой пропускной способности каналов приходилось писать компактный и эффективный код. И конечно, приложения такого рода надо было делать безопасными и надежными.
Вначале Гослинг с коллегами программировали на C++, но вскоре обнаружили,
что для их задач этот язык слишком сложен, неповоротлив и уязвим. Тогда они
решили начать с нуля, и Гослинг стал работать над тем, что он называл «C++
минус минус».
Провал Apple Newton (первого карманного компьютера Apple) показал, что
время PDA еще не настало. Поэтому в Sun переключили усилия компании
FirstPerson на интерактивное телевидение (ITV). Для программирования
ТВ-приставок разработали язык Oak — один из ближайших предков Java.
Но при всей своей элегантности и возможности безопасного обмена данными
язык Oak не смог спасти бесперспективную идею интерактивного телевидения. Оно не интересовало покупателей, и вскоре в Sun отказались от этой
концепции.

26  Глава 1. Современный язык
Тогда Джой и Гослинг объединили усилия в поиске актуальной стратегии
применения своего новаторского языка. Дело было в 1993 году, и взрывной
рост интереса ко Всемирной паутине открывал новые возможности. Язык
Oak был компактным, безопасным, архитектурно-независимым и объектноориентированным. Так получилось, что все эти свойства входили в набор
требований к универсальному языку программирования для интернета. Компания Sun быстро сменила приоритеты, и после небольшой переработки Oak
превратился в Java.

Развитие
Не будет преувеличением сказать, что язык Java (и его вариант, ориентированный на разработчиков, — Java Development Kit, или JDK) распространялся
стремительно, как степной пожар. Еще до первого официального выпуска, когда
Java не был полноценным программным продуктом, за него ухватились лидеры ИТ-бизнеса. Лицензии на Java получили Microsoft, Intel, IBM и почти все
остальные крупные производители компьютеров и программного обеспечения.
Тем не менее даже при такой поддержке новый язык испытал в первые годы
немало трудностей роста.
Начались нарушения контрактов и антимонопольные судебные процессы между
Sun и Microsoft, вызванные использованием Java в браузере Internet Explorer.
Это затормозило применение нового языка в Windows — самой популярной
в мире десктопной операционной системе. Участие Microsoft в работе над Java
также стало предметом крупного федерального иска о недобросовестной конкуренции; свидетели дали в суде показания, что гигантская корпорация намеренно
стремилась подорвать Java, внедряя несовместимости в свою версию языка. Тем
временем Microsoft представила в рамках инициативы .NET свой собственный
язык C#, производный от Java, и передумала включать Java в Windows. C# оказался очень хорошим языком, и за последние годы в нем появилось даже больше
инноваций, чем в Java.
Но Java и по сей день продолжает распространяться на многих платформах.
С первого взгляда на архитектуру Java становится ясно, что ее самые замечательные возможности основаны на изолированной среде виртуальной машины,
в которой выполняются Java-приложения. Язык Java предусмотрительно проектировали таким образом, чтобы можно было реализовать архитектуру его
поддержки разными способами: как на программном уровне (в операционных
системах), так и на аппаратном уровне (в специальном оборудовании). Аппаратные реализации Java используются в некоторых смарт-картах и в других
встроенных системах. Вы можете найти интерпретаторы Java даже в таких
«носимых устройствах», как электронные кольца и опознавательные жетоны.
А программные реализации Java созданы для всех современных компьютерных

Виртуальная машина  27

платформ, в том числе мобильных. Один из вариантов Java стал компонентом
операционной системы Google Android, на которой работают миллиарды смартфонов и других мобильных устройств.
В 2010 году корпорация Oracle купила Sun Microsystems и стала куратором
языка Java. Начало оказалось неудачным: Oracle подала судебный иск против
Google из-за интеграции Java в Android — и проиграла. В 2011 году Oracle выпустила Java 7 — важную версию, дополненную новым пакетом ввода-вывода.
В 2017 году в состав Java 9 вошли модули для решения давно существующих
проблем со списком путей classpath и с растущим размером JDK. После выпуска
Java 9 начался быстрый процесс обновлений, в результате которого появилась
актуальная на момент написания книги версия с долгосрочной поддержкой:
Java 11. (Об этих и других версиях подробнее рассказано в разделе «История
Java», с. 47.) Корпорация Oracle продолжает руководить разработкой языка,
но она поделила мир Java надвое: перевела основную линейку версий Java на
дорогостоящую коммерческую лицензию, но сохранила и бесплатный вариант
под названием OpenJDK. Он по-прежнему общедоступен, его любят и на него
ориентируются многие разработчики.

Виртуальная машина
Язык Java одновременно является компилируемым и интерпретируемым. Сначала компилятор преобразует написанный программистом исходный код в простые
двоичные команды, очень похожие на машинный код обычных микропроцессоров. Отличие в том, что исходный код C и C++ компилируется в машинный
код для конкретной модели процессора, а исходный код Java компилируется
в универсальный формат — байт-код, представляющий собой команды для
виртуальной машины (VM).
Скомпилированный байт-код выполняется интерпретатором исполнительной
системы Java, который делает все то же самое, что обычный аппаратный процессор, но в безопасной виртуальной среде. Он выполняет набор команд на базе
стека и управляет памятью подобно операционной системе. Он создает примитивные типы данных, управляет ими, загружает и активирует новые блоки
кода по ссылкам. Важнее всего, что все это делается в соответствии со строго
определенной общедоступной спецификацией, по которой любой разработчик
может создать Java-совместимую виртуальную машину. Сочетание виртуальной
машины и определения языка образует полную спецификацию. В базовом языке
Java нет никаких деталей, которые остались бы неопределенными или зависели
бы от конкретной реализации. Например, Java строго регламентирует размеры
и математические свойства всех своих примитивных типов данных, не оставляя
их на усмотрение разработчиков версий для разных платформ.

28  Глава 1. Современный язык
Интерпретатор Java относительно легок и компактен; его можно реализовать
в любой форме, подходящей для конкретной платформы. Интерпретатор может
быть отдельным приложением или встроенным компонентом другого приложения (например, веб-браузера). Таким образом, код Java является портируемым
по своей природе. Один и тот же байт-код приложения будет правильно работать на любой платформе, в которой есть исполнительная среда Java (рис. 1.1).
Вам не придется писать альтернативные версии своего приложения для разных
платформ или распространять исходный код среди конечных пользователей.
Исходный код

Исполнительная среда Java
Байт-код

Рис. 1.1. Исполнительная среда Java
Фундаментальной единицей кода Java является класс. Как и в других объектноориентированных языках, классы представляют собой компоненты приложений,
содержащие исполняемый код и данные. Скомпилированные классы Java распространяются (публикуются) в универсальном двоичном формате, который
содержит байт-код Java и другую информацию классов. Разработчик может заниматься созданием и сопровождением отдельных классов, сохраняя их в файлах
и архивах — локально или на сетевом сервере. Java умеет находить и загружать
нужные классы динамически, когда они требуются работающему приложению.
Кроме платформенно-зависимой исполнительной системы, в Java входит ряд
фундаментальных классов, содержащих архитектурно-зависимые методы. Эти
платформенные методы (native methods) образуют своего рода шлюз между

Виртуальная машина  29

виртуальной машиной Java и реальным миром. Они реализуются на языке, компилируемом в платформенный код на платформе размещения, и предоставляют
низкоуровневый доступ к ресурсам компьютера: к сети, к оконной системе, к файловой системе и т. д. Однако основная часть кода Java написана на самом языке
Java (инициализируемом этими базовыми примитивами), поэтому легко портируется. Сюда входят такие фундаментальные средства Java, как компилятор Java,
поддержка сети и библиотек GUI, которые также написаны на Java, поэтому доступны в абсолютно одинаковом виде на всех платформах Java без портирования.
Исторически считалось, что интерпретаторы работают медленно, но Java не
является традиционным интерпретируемым языком. Кроме компиляции исходного кода в портируемый байт-код, при разработке языка Java специально
предусматривалось, чтобы программные реализации исполнительной системы
могли дополнительно оптимизировать свое быстродействие методом компиляции байт-кода в платформенный машинный код «на ходу». Этот механизм
называется динамической компиляцией, или JIT-компиляцией (Just In Time).
Благодаря JIT-компиляции, код Java может выполняться с такой же скоростью,
как платформенный код, без ущерба для мобильности и безопасности.
Этот момент часто создает недоразумения при сравнении быстродействия
языков. Существует только одна причина внутреннего снижения производительности, присущая скомпилированному коду Java на стадии выполнения
и необходимая для безопасности и эффективности архитектуры виртуальной
машины: проверка границ массивов. Все остальное может оптимизироваться
в платформенный код точно так же, как в языках со статической компиляцией.
Кроме того, язык Java содержит больше структурной информации, чем многие
другие языки; эта информация расширяет возможности оптимизации. Также
надо помнить, что оптимизация может применяться на стадии выполнения
с учетом фактической логики работы и характеристик приложения. Что можно
хорошо сделать на стадии компиляции, но нельзя сделать еще лучше на стадии
выполнения? Тут все определяется временем.
У традиционной JIT-компиляции есть проблема: оптимизация кода требует
времени. Таким образом, JIT-компилятор может выдавать отличные результаты,
но за это приходится расплачиваться заметной задержкой при запуске приложения. Обычно это не создает проблем для приложений, долго работающих на
стороне сервера, но может стать серьезной проблемой для клиентских программ
и приложений, работающих на компактных устройствах со скромными возможностями. Для решения этой проблемы технология компилятора Java, называемая
HotSpot, использует прием адаптивной компиляции. Если проанализировать
фактическое распределение времени выполнения типичной программы, то
станет ясно: почти все время тратится на то, чтобы относительно малые части
кода выполнялись снова и снова. Многократно выполняемые блоки кода могут
составлять малую часть от размера программы, но такое поведение определяет

30  Глава 1. Современный язык
ее общее быстродействие. Адаптивная компиляция также позволяет исполнительной среде Java применять новые виды оптимизации, попросту невозможные
в языках со статической компиляцией; отсюда и утверждение, что код Java в некоторых случаях может выполняться быстрее кода C или C++.
Чтобы воспользоваться этим фактом, HotSpot вначале работает как обычный
интерпретатор байт-кода Java, но с небольшим отличием: он измеряет время
выполнения кода («профилирует» код), чтобы определить, какие его части выполняются многократно. Определив, какие части кода критичны для быстродействия, HotSpot компилирует эти секции в оптимальный платформенный
машинный код. Поскольку в машинный код компилируется лишь небольшая
часть программы, затраты времени на оптимизацию этой части оказываются
приемлемыми. Оставшаяся часть программы может вообще не компилироваться
(а только интерпретироваться) для экономии памяти и времени. В сущности,
виртуальная машина Java может работать в любом из двух режимов: клиентском
или серверном. Режим определяет, чему будет отдано предпочтение — скорости
запуска и экономии памяти или общему быстродействию. В Java 9 также можно
воспользоваться опережающей (Ahead-of-Time, AOT) компиляцией, если минимизация времени запуска вашего приложения действительно важна.
Возникает естественный вопрос: зачем удалять всю полезную информацию профилирования при каждом закрытии приложения? Вообще говоря, компания Sun
частично уделила внимание этой теме в Java 5.0 — там появилась возможность
использования общих классов, доступных только для чтения, которые хранятся
в оптимизированной форме. Тем самым значительно сокращается как время запуска, так и непроизводительные затраты ресурсов на выполнение многих приложений Java на обычной машине. Технология, применяемая для достижения
этой цели, сложна, но ее идея проста: оптимизировать только те части программы,
которые должны выполняться быстро, и не беспокоиться обо всем остальном.

Сравнение Java с другими языками
Проектируя Java, его создатели руководствовались многолетним опытом программирования на других языках. Уделим немного времени сравнению Java
с другими языками на концептуальном уровне — это будет полезно и читателям,
уже умеющим программировать, и новичкам, которые хотят увидеть общую
картину. Обратите внимание, что эта книга не требует от вас знания какого-­либо
конкретного языка. Мы надеемся, что все упоминания других языков будут достаточно понятными.
В основании любого универсального языка программирования стоят три столпа: портируемость, скорость и безопасность. На рис. 1.2 показаны результаты
сравнения Java с некоторыми языками, популярными на момент его создания.

Сравнение Java с другими языками  31

ПОРТИРУЕМОСТЬ

БЕЗОПАСНОСТЬ

ПОРТИРУЕМОСТЬ

БЕЗОПАСНОСТЬ

СКОРОСТЬ

ПОРТИРУЕМОСТЬ

БЕЗОПАСНОСТЬ

СКОРОСТЬ

ПОРТИРУЕМОСТЬ

БЕЗЗОПАСНОСТЬ

СКОРОСТЬ

Вероятно, вы слышали, что у Java много общего с языками C и C++. На самом
деле это не так, разве что на самом поверхностном уровне. При первом взгляде
на код Java вы заметите, что базовый синтаксис напоминает C или C++… но на
этом сходство заканчивается. Java ни в коем случае не является прямым потомком C или относящегося к следующему поколению C++. Сравнивая языки по
их свойствам, вы увидите, что у Java больше общего с динамическими языками,
такими как Smalltalk и Lisp. Практическая реализация Java в компьютере настолько далека от классического C, что трудно даже представить.

Рис. 1.2. Сравнение языков программирования
Читатели, знакомые с современной ситуацией в программировании, заметят, что
в этом сравнении отсутствует такой популярный язык, как C#. Он в значительной
мере стал ответом Microsoft на появление Java, и надо признать, что в C# добавлен
ряд полезных возможностей. Вследствие общих целей и подходов к проектированию приложений (виртуальная машина, байт-код, изолированная среда и т. д.)
эти две платформы не очень существенно различаются по скорости и по характеристикам безопасности. C# обладает примерно такой же портируемостью, как
и Java. C# многое позаимствовал из синтаксиса C, но на самом деле является более
близким родственником динамических языков. Многие Java-разработчики относительно легко осваивают C#, и наоборот. Большая часть времени, затраченного
на переход с одного языка на другой, уходит на изучение стандартной библиотеки.
Тем не менее даже поверхностное сходство между этими языками заслуживает
внимания. Java заимствует многое из синтаксиса C и C++, так что вы увидите
многие лаконичные языковые конструкции с обилием фигурных скобок и точек
с запятой. Java следует философии языка C в том, что хороший язык должен
быть компактным; иначе говоря, достаточно кратким и систематичным, чтобы
программист мог одновременно удерживать в голове все его возможности. Подобно тому как C можно расширять библиотеками, к основным компонентам
Java можно добавлять пакеты классов, расширяющие синтаксис.

32  Глава 1. Современный язык
Язык C стал таким успешным, потому что он предоставляет достаточно функцио­
нальную среду программирования с высокой производительностью и приемлемым
уровнем портируемости. Java также старается поддерживать баланс между функциональностью, скоростью и портируемостью, но делает это совершенно иным
способом. Язык C жертвует функциональностью ради портируемости; Java на
первых порах жертвовал скоростью ради портируемости. Кроме того, в Java решены
проблемы безопасности, которые остались в C (хотя в современных компьютерах
многие из них решаются в операционной системе и на аппаратном уровне).
Много лет назад, до появления JIT и адаптивной компиляции, Java был
медленнее, чем языки со статической компиляцией. Критики постоянно говорили, что он никогда их не догонит. Но как мы рассказали в предыдущем
разделе, теперь Java уже сравним по быстродействию с языками C и C++
в эквивалентных задачах, поэтому критики в основном притихли. Движок
видеоигры Quake 2 от ID Software, распространяемый с открытым кодом, был
портирован на Java. Если Java достаточно быстр даже для шутеров с видом от
первого лица, то он наверняка будет достаточно быстрым и для большинства
бизнес-приложений.
Языки сценариев, такие как Perl, Python и Ruby, не утратили популярности. Нет
причин, по которым язык сценариев не подходил бы для безопасных сетевых
приложений. Но как правило, языки сценариев не подходят для серьезного
крупномасштабного программирования. Главное преимущество языков сценариев заключается в их динамичности; это мощные инструменты для быстрой
разработки. Некоторые языки сценариев (такие, как Perl) также предоставляют
мощные средства обработки текста, которые в языках более общего назначения
становятся неудобными. Языки сценариев также отличаются высоким уровнем
портируемости, хотя бы на уровне исходного кода.
JavaScript (не путайте с Java!) — это объектно-базированный язык сценариев,
который компания Netscape много лет назад разработала для своего браузера. Сегодня он является встроенным компонентом большинства браузеров, его используют для динамичных и интерактивных веб-приложений. Название JavaScript
происходит от интеграции и некоторого сходства с Java, но по своей сути эти
языки совершенно разные. Впрочем, у JavaScript есть важные применения и за
пределами браузеров (например, платформа Node.js1), а его популярность среди
разработчиков в разных областях продолжает расти. За дополнительной информацией о JavaScript обращайтесь к книге Дэвида Фленагана (David Flanagan)
«JavaScript: The Definitive Guide» (издательство O’Reilly).
Основная проблема языков сценариев — их неформальное отношение к структуре программы и к типам данных. Большинство языков сценариев не явля1

Если вас интересует Node.js, рекомендуем книгу: Пауэрс Ш. Изучаем Node.js. — СПб.:
Питер.

Сравнение Java с другими языками  33

ются объектно-ориентированными. В них упрощены системы типов данных,
и обычно в них нет проработанной системы видимости переменных и функций.
Из-за этих особенностей они хуже подходят для создания больших модульных
приложений. Другая проблема языков сценариев — это скорость; из-за своей
высокоуровневой природы такие языки, обычно интерпретируемые на уровне
исходного кода, часто отличаются медлительностью.
Сторонники конкретных языков сценариев не согласятся с некоторыми из этих
обобщений — и в каких-то случаях будут правы. Языки сценариев в последние
годы совершенствовались, особенно JavaScript, который стал быстрее в результате колоссальных исследований. Но нельзя отрицать фундаментальный факт:
языки сценариев рождались как неформальные, менее структурированные
альтернативы языкам системного программирования, и по многим причинам
языки сценариев, как правило, плохо подходят для больших или сложных проектов (по крайней мере на данный момент).
Java обладает некоторыми важнейшими преимуществами языков сценариев,
прежде всего высокой динамичностью, а также дополнительными преимуществами низкоуровневого языка. В Java есть мощный API регулярных выражений, способный конкурировать с Perl в области работы с текстом, а также есть
языковые средства, упрощающие работу с коллекциями, переменными списками
аргументов, статическим импортированием методов и другими удобными синтаксическими конструкциями, которые делают код более лаконичным.
Инкрементальная разработка на Java с объектно-ориентированными компонентами позволяет быстро создавать приложения и легко изменять их, тем
более что этот язык отличается простотой. Исследования показали, что разработка на Java выполняется быстрее, чем на C или C++, в основном благодаря
богатым синтаксическим возможностям1. Java также включает большую базу
стандартных фундаментальных классов для решения таких типичных задач,
как создание графических интерфейсов и сетевых приложений. Вы можете использовать Maven Central — внешний ресурс с огромным набором библиотек
и пакетов, быстро подключая их к вашей среде программирования для решения
всевозможных задач. В дополнение ко всему перечисленному Java обладает
масштабируемостью и технологическими преимуществами более статических
языков. Java предоставляет безопасную структуру, на базе которой создаются
фреймворки более высокого уровня (и даже другие языки).
Мы уже отмечали, что Java по своей архитектуре близок к таким языкам, как
Smalltalk и Lisp. Только они используются в основном как исследовательские
инструменты, а не средства для разработки крупномасштабных систем. Одна
из причин заключается в том, что для этих языков так и не появились стан1

Например, см.: Phipps G. Comparing Observed Bug and Productivity Rates for Java and
C++. Software — Practice & Experience, том 29, 1999 г.

34  Глава 1. Современный язык
дартные портируемые интерфейсы с сервисами операционной системы, такие
как стандартная библиотека C или фундаментальные классы Java. Исходный
код Smalltalk компилируется в формат интерпретируемого байт-кода и может
динамически компилироваться в платформенный код «на ходу» — примерно так
же, как в Java. Но в Java эта архитектура улучшена: правильность скомпилированного байт-кода проверяется верификатором. Верификатор создает для Java
преимущество перед Smalltalk, потому что код Java требует меньшего количества проверок на стадии выполнения. Кроме того, верификатор байт-кода Java
помогает решать те проблемы безопасности, которые не решаются в Smalltalk.
Далее в этой главе приводится общий обзор языка Java. Мы расскажем, что
нового появилось в Java, что осталось неизменным и почему.

Структурная безопасность
Вы уже знаете, что язык Java проектировался как безопасный. Но что имеется
в виду под безопасностью? Безопасность — от чего или от кого? Наибольшее
внимание к Java привлекают те средства безопасности, которые дают возможность создания новых типов программ с динамической портируемостью. Java
предоставляет несколько уровней защиты от опасных ошибок кода, а также от
таких вредоносных объектов, как вирусы и трояны. В следующем разделе будет
показано, как архитектура виртуальной машины Java оценивает безопасность
кода перед его выполнением и как загрузчик классов Java (механизм загрузки
байт-кода в интерпретаторе Java) строит «стены» вокруг ненадежных классов.
Эти средства закладывают основу для высокоуровневых политик безопасности,
которые могут разрешать или запрещать определенные действия на уровне отдельных приложений.
В этом разделе рассматриваются некоторые общие свойства языка Java. По сравнению со специальными средствами безопасности (кстати, часто упускаемыми из
виду) еще важнее та безопасность, которую Java обеспечивает благодаря отсутствию многих типичных проблем архитектуры и программирования. В Java есть
максимально возможная защита от элементарных ошибок, которые допускают
программисты, в том числе при наследовании от старого кода. Создатели Java
всегда старались сделать язык простым, включать в него средства, доказавшие
свою полезность, и дать программистам возможность создавать на базе языка
сложные конструкции, когда возникает такая необходимость.

Упрощать, упрощать, упрощать…
В Java правит простота. Поскольку работа над Java начиналась с нуля, разработчикам удалось избежать тех особенностей, которые оказались проблемными

Структурная безопасность  35

или неоднозначными в других языках. Например, Java не допускает перегрузки
операторов (которая в других языках позволяет программисту переопределять
смысл таких знаков, как + и -). В Java нет препроцессора исходного кода, поэтому
нет и таких средств, как макросы, команды #define или условная компиляция.
Эти конструкции существуют в других языках прежде всего для поддержки
платформенных зависимостей, и в этом смысле они не нужны в Java. Условная
компиляция также часто применяется при отладке, но хорошо проработанные
технологии оптимизации исполнительной системы Java и такие средства, как
проверочные утверждения (assertions), решают проблему более элегантно. (Вам
определенно стоит заняться их изучением после того, как вы освоите основы
программирования на Java.)
Java предоставляет четко определенную структуру пакетов, в которых упорядочены файлы классов. Система пакетов позволяет компилятору реализовать
часть функциональности традиционной утилиты make (программа для получения исполняемых файлов из исходного кода). Компилятор также может
работать непосредственно со скомпилированными классами Java, потому что
в них сохраняется вся информация о типах; лишние «заголовочные» файлы как
в C/C++ не нужны. Все это означает, что для понимания кода Java требуется
меньше контекстной информации. Иногда вам будет проще прочитать исходный
код Java, чем документацию класса.
Java иначе подходит к некоторым принципам структурирования кода, которые вызывают проблемы в других языках. Например, иерархия классов в Java
строится только путем одиночного наследования (у каждого класса может быть
только один «родительский» класс), зато разрешено множественное наследование интерфейсов. Интерфейс (аналог абстрактного класса в C++) задает логику
работы объекта, но не определяет его реализацию. Это чрезвычайно мощный
механизм, позволяющий разработчику описать нужную логику работы объекта
в виде «контракта», который затем будет действовать и использоваться независимо от любых конкретных реализаций объекта. Интерфейсы в Java устраняют
необходимость во множественном наследовании классов и все связанные с ним
проблемы.
Как будет показано в главе 4, Java — удивительно простой и элегантный язык
программирования, и это одна из важных причин его популярности.

Безопасность типов и связывание методов
Одна из главных характеристик любого языка программирования — тот способ,
которым в нем осуществляется проверка типов. В общем случае языки делятся
на статические и динамические. Разница между ними определяется объемом
информации о переменных, известной на стадии компиляции, относительно
информации, известной на стадии выполнения.

36  Глава 1. Современный язык
В языках со строгой статической типизацией (таких, как C и C++) все типы данных жестко фиксируются на момент компиляции исходного кода. Компилятор
извлекает пользу из этого факта, так как у него имеется достаточно информации для выявления многих видов ошибок перед выполнением кода. Например,
компилятор не позволит сохранить вещественное значение в целочисленной
переменной. Код не требует проверки типов во время выполнения, поэтому он
компилируется в компактную и быструю форму. Однако языкам со статической
типизацией не хватает гибкости. Они не поддерживают коллекции таким же естественным образом, как языки с динамической типизацией, и в них приложение
не может безопасно импортировать новые типы данных во время выполнения.
С другой стороны, в динамических языках (таких, как Smalltalk или Lisp) есть
исполнительная система, которая управляет типами объектов и выполняет необходимую проверку типов во время выполнения приложения. Такие языки
позволяют реализовать более сложную логику работы и во многих отношениях
оказываются более мощными. Но они обычно работают медленнее, менее безо­
пасны и создают больше сложностей при отладке.
Различия между языками сравнивались с различиями между моделями автомобилей1. Языки со статической типизацией (такие, как C++) напоминают
спортивный автомобиль: достаточно быстрый и безопасный, но практичный
только в том случае, если вы едете по хорошей асфальтированной дороге. Языки
с ярко выраженной динамической типизацией (такие, как Smalltalk) больше напоминают внедорожники: они предоставляют больше свободы, но иногда ими
трудно управлять. На них веселее (а иногда и быстрее) проехать напрямую по
перелеску, но там можно застрять в канаве или попасть в лапы медведям.
Еще одна характеристика языка — способ связывания вызовов методов с их
определениями. В статических языках (таких, как C и C++) связывание методов обычно происходит во время компиляции, если только программист не
потребует обратного. С другой стороны, такие языки, как Smalltalk, называются языками с поздним связыванием, потому что поиск определений методов
осуществляется динамически во время выполнения. Раннее связывание важно по соображениям быстродействия; приложение может выполняться без
лишних затрат, вызванных поиском методов во время выполнения. А позднее
связывание обеспечивает большую гибкость. Оно также необходимо для таких
объектно-ориентированных языков, в которых новые типы могут загружаться
динамически и только исполнительная система может определить, какой метод
надо вызвать в каждом конкретном случае.
Java обладает некоторыми сильными сторонами как C++, так и Smalltalk; это
язык со статической типизацией и поздним связыванием. Каждый объект в Java

1

Эту аналогию предложил Маршалл П. Клайн (Marshall P. Cline), автор C++ FAQ.

Структурная безопасность  37

имеет четко определенный тип, известный во время компиляции. Это означает,
что компилятор Java может проводить такие же статические проверки типов
и анализ использования, как в C++. В результате вы не сможете присвоить объект переменной неправильного типа или вызвать для объекта несуществующие
методы. Более того, компилятор Java не позволяет использовать неинициализированные переменные и создавать недоступные команды (см. главу 4).
Однако Java также обладает полноценной динамической типизацией. Исполнительная система Java отслеживает все объекты и позволяет определять
их типы и отношения между ними во время выполнения. Таким образом, вы
можете проанализировать объект на стадии выполнения, чтобы определить,
что он собой представляет. В отличие от C и C++, преобразования объекта от
одного типа к другому проверяются исполнительной системой, а новые виды
динамически загружаемых объектов могут использоваться с некоторой степенью
безопасности типов. Поскольку Java является языком с поздним связыванием,
субкласс может переопределять методы своего суперкласса, даже если субкласс
загружается во время выполнения.

Инкрементальная разработка
Java переносит всю информацию о типах данных и сигнатурах методов из исходного кода в форму скомпилированного байт-кода. Благодаря этому становится
возможной инкрементальная разработка классов Java. Ваш исходный код Java
может безопасно компилироваться вместе с классами из других источников,
совершенно неизвестных вашему компилятору. Иначе говоря, вы можете писать
новый код, который работает с двоичными файлами классов без потери безопасности типов, даже если у вас нет исходного кода этих классов.
Java не страдает от проблемы «хрупкости базовых классов». В таких языках,
как C++, разработка базового класса иногда останавливается из-за того, что
он имеет множество производных классов; для внесения изменений в базовый
класс пришлось бы перекомпилировать все производные классы. Эта проблема
особенно сложна для разработчиков библиотек классов. Java обходит эту проблему динамическим поиском полей в классах. Пока класс сохраняет допустимую форму своей исходной структуры, он может развиваться без нарушения
работоспособности других классов, производных от него или использующих его.

Динамическое управление памятью
Некоторые важные различия между Java и низкоуровневыми языками (такими,
как C и C++) связаны с тем, как реализовано управление памятью. В Java запрещены так называемые указатели, способные ссылаться на произвольные области
памяти; зато добавлен механизм уборки мусора и высокоуровневые массивы.

38  Глава 1. Современный язык
Эти особенности устранили многие проблемы с безопасностью, портируемостью
и оптимизацией, не решаемые другими способами.
Уборка мусора спасла бесчисленных разработчиков от самого распространенного
источника ошибок программирования на C и C++: явного выделения и освобождения памяти. Исполнительная система Java не только размещает объекты
в памяти, но и отслеживает все ссылки на них. Когда объект перестает использоваться, Java автоматически удаляет его из памяти. Как правило, на объекты,
ставшие ненужными, можно не обращать внимания: интерпретатор уберет их
в подходящий момент.
В Java есть сложный уборщик мусора, который работает в фоновом режиме;
это означает, что большинство операций уборки мусора происходит в моменты бездействия, в паузах ввода-вывода, между щелчками мышью или нажатиями клавиш. Эффективные технологии исполнительной системы, такие
как HotSpot, обеспечивают очень рациональную уборку мусора, в которой
учитываются закономерности использования объектов (например, объектов
с малым и большим сроком жизни) и оптимизируется их уничтожение. Исполнительная система Java умеет автоматически настраиваться для оптимального
распределения памяти в разных видах приложений, в зависимости от их поведения. Благодаря профилированию на стадии выполнения, автоматическое
управление памятью может работать намного быстрее самого аккуратного
«ручного» управления — это факт, в который программисты старой школы
все еще не могут поверить.
Мы уже отметили, что в Java нет указателей. Строго говоря, это утверждение
истинно, но оно может вызвать недоразумения. В Java поддерживаются ссылки
(references), которые представляют собой безопасную разновидность указателей.
Ссылка — это строго типизованный идентификатор объекта. В Java обращение
ко всем объектам, кроме примитивных числовых типов, осуществляется по
ссылкам. Вы можете использовать ссылки для создания всех обычных структур,
которые программисты на языке C привыкли создавать с помощью указателей:
связанных списков, деревьев и т. д. Но есть одно отличие: со ссылками вы можете
это делать только при соблюдении безопасности типов.
Другое важное отличие указателей от ссылок заключается в том, что программист не может выполнять арифметические операции со ссылками для
изменения их значений; ссылки указывают только на конкретные методы,
объекты или элементы массива. Ссылки атомарны; со значением ссылки
нельзя выполнять другие операции, кроме присваивания объекту. Ссылки
передаются по значению, и к объекту нельзя обращаться более чем через один
уровень косвенности. Защита ссылок — один из самых фундаментальных
аспектов безопасности Java. Отсюда следует, что код Java должен «играть по
правилам»; он не может залезть туда, куда ему залезать не положено, и он не
может обходить правила.

Структурная безопасность  39

Наконец, надо упомянуть, что массивы в Java являются настоящими, полноценными объектами. Их можно динамически создавать и присваивать, как и любые
другие объекты. Массивы знают свои размеры и типы. Хотя вы не можете напрямую определять классы массивов и создавать субклассы, у массивов есть
четко определенныеотношения наследования, основанные на отношениях их
базовых типов. Когда в языке имеются настолько полноценные массивы, у вас
практически исчезает необходимость в арифметических операциях с указателями как в C и C++.

Обработка ошибок
Язык Java уходит корнями к сетевым устройствам и встроенным системам.
В этих областях важно иметь надежный и разумный способ контроля возникающих ошибок. Поэтому в Java есть мощный механизм исключений,
напоминающий аналогичные механизмы в новых реализациях C++. Исключения — это очень естественный и элегантный способ обработки ошибок.
Исключения позволяют отделить код обработки ошибок от основного кода,
что делает приложения более стабильными, а их логику — более простой для
понимания.
Как только в выполняемом приложении возникает исключение (вследствие
какой-то ошибки), управление передается в заранее подготовленный блок
кода catch. Этот блок получает исключение в виде объекта, содержащего информацию о ситуации, которая стала причиной исключения. Компилятор Java
требует, чтобы каждый метод либо объявлял исключения, которые он может
генерировать, либо перехватывал и обрабатывал их самостоятельно. Таким
образом, информация об ошибках является столь же важной, как аргументы
и возвращаемые типы методов. Программируя на Java, вы точно знаете, с какими исключительными ситуациями вам придется иметь дело, а компилятор
помогает вам писать надежные приложения, не оставляющие эти ситуации
необработанными.

Потоки
Современным приложениям необходима высокая степень параллелизма. Даже
самое линейное приложение может иметь сложный пользовательский интерфейс, а это означает необходимость в нескольких параллельных процессах.
В наши дни компьютеры должны работать быстро, и никому не нравится,
когда они начинают «тормозить» на второстепенных задачах, отнимая время.
Потоки (threads), также называемые потоками выполнения (в отличие от
потоков данных), позволяют эффективно распределять и параллельно выполнять несколько задач, ускоряя работу как клиентских, так и серверных

40  Глава 1. Современный язык
приложений. В Java работать с потоками несложно, потому что их поддержка
встроена в язык.
Параллелизм очень полезен, но для программирования потоков недостаточно
выполнять в них разные задачи в одно и то же время. В большинстве случаев
потоки должны быть синхронизированы (скоординированы) между собой, что
было бы трудно без явной поддержки со стороны языка. Java поддерживает
синхронизацию на базе модели мониторов и условий — своего рода системы
«ключей и замков» для обращения к ресурсам. Ключевое слово synchronized
помечает методы и блоки кода для безопасного последовательного доступа
к ним внутри объекта. Также предусмотрены простые, примитивные методы
для явного ожидания своей очереди и для передачи сигналов между потоками,
заинтересованными в одном объекте.
Кроме того, в Java есть высокоуровневый пакет параллелизма, предоставляющий
мощные средства для реализации типовых паттернов многопоточного программирования, таких как пулы потоков, координация задач и сложная блокировка.
Благодаря этому пакету и сопутствующим инструментам, Java считается одним
из лучших языков для работы с потоками.
Некоторым разработчикам никогда не приходится писать многопоточный код,
но все-таки работа с потоками считается важным навыком программирования
на Java, которым должен владеть каждый разработчик. Эта тема рассматривается в главе 9.

Масштабируемость
На самом низком уровне программы Java состоят из классов. Обычно классы
представляют собой небольшие модульные компоненты. Кроме классов, Java
поддерживает пакеты — это тот уровень структуры, на котором классы группируются в функциональные единицы. Пакеты предоставляют схему формирования имен для упорядочения классов, а также второй уровень организационного
управления видимостью переменных и методов в приложениях Java.
Внутри пакета класс либо обладает общедоступной видимостью, либо защищен
от внешнего доступа. Пакеты формируют другой тип видимости, более близкий
к уровню приложения. Этот уровень хорошо подходит для создания пригодных
для повторного использования компонентов, совместно работающих в системе.
Пакеты также упрощают создание масштабируемых приложений, которые
можно развивать, не превращая их в дебри запутанного кода. Для повторного
использования кода и масштабирования приложений наиболее эффективна
модульная система (появившаяся в Java 9), но эта тема выходит за рамки книги.
Модулям полностью посвящена книга Пола Беккера (Paul Bakker) и Сандера
Мака (Sander Mak) «Java 9 Modularity» (издательство O’Reilly).

Безопасность на уровне исполнительной системы Java  41

Безопасность на уровне исполнительной
системы Java
Одно дело — создать язык, который мешает вам «выстрелить себе в ногу», и совсем другое — создать язык, который помешает кому-то другому «выстрелить
вам в ногу».
Инкапсуляция — это концепция сокрытия внутри класса его данных и логики
работы. Инкапсуляция является важной частью объектно-ориентированных архитектур, так как помогает писать чистый, состоящий из модулей код. Впрочем,
в большинстве языков видимость элементов данных является всего лишь частью
отношений между программистом и компилятором. Это вопрос семантики, а не
утверждение о фактической безопасности данных в той среде, где выполняется
программа.
Когда Бьёрн Страуструп выбрал ключевое слово private для обозначения
скрытых компонентов классов в C++, он думал, скорее всего, о том, чтобы разработчика не беспокоили запутанные подробности кода других разработчиков,
а не о том, как защищать классы и объекты от атак вирусов и троянов. Произвольные преобразования типов и арифметические операции с указателями в C
и C++ позволяют легко нарушать разрешения доступа к классам, не нарушая
при этом правила языка. Возьмем следующий код:
// Код C++
class Finances {
private:
char creditCardNumber[16];
...
};
main() {
Finances finances;
// Формирование указателя для получения доступа
// к конфиденциальным данным внутри класса
char *cardno = (char *)&finances;
printf("Card Number = %.16s\n", cardno);
}

В этой маленькой драме на C++ мы написали код, который нарушает инкапсуляцию класса Finances и извлекает секретную информацию. Подобные фокусы — злоупотребления с нетипизованными указателями — в Java невозможны.
Если этот пример кажется вам нереалистичным, подумайте, как важно защищать
фундаментальные (системные) классы исполнительной системы от подобных
атак. Если ненадежный код сможет повредить компоненты, предоставляющие
доступ к реальным ресурсам (к файловой системе, к сети, к оконной системе),
то у него появится возможность перехватить номера ваших кредиток.

42  Глава 1. Современный язык
В тех случаях, когда приложение Java динамически загружает код из ненадежных источников в интернете и выполняет его одновременно с приложениями,
содержащими конфиденциальную информацию, защита должна быть очень
глубокой. Модель безопасности Java создает вокруг импортированных классов
три уровня защиты (рис. 1.3).
Системные ресурсы
Менеджер безопасности
Загрузчик классов
Верификатор
Ненадежный
источник

Двоичный файл Java

Рис. 1.3. Модель безопасности Java
На внешнем контуре этой системы все решения безопасности на уровне приложения принимаются менеджером безопасности, в сочетании с гибкой политикой безопасности. Менеджер безопасности управляет доступом к системным
ресурсам: к файловой системе, к сетевым портам, к оконной среде и т. д. Работа
менеджера безопасности зависит от способности загрузчика классов защищать
базовые системные классы. Загрузчик классов обеспечивает загрузку классов
из локального хранилища или из сети. А на самом внутреннем уровне вся безо­
пасность системы в конечном счете зависит от верификатора, гарантирующего
целостность получаемых классов.
Этот верификатор байт-кода Java является специальным модулем и неотъемлемой частью исполнительной системы Java. Однако загрузчики классов
и менеджеры безопасности (а точнее, политики безопасности) представляют
собой компоненты, которые могут быть по-разному реализованы в разных приложениях (например, в серверах и браузерах). Для безопасности в среде Java
необходимо правильное функционирование всех этих компонентов.

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

Безопасность на уровне исполнительной системы Java  43

условия. Тем не менее злоумышленник может намеренно собрать некорректный
байт-код. Верификатор должен обнаружить эту опасность.
После того как код будет проверен, он считается безопасным в том смысле,
что в нем нет случайных или злонамеренных ошибок определенных типов.
Например, проверенный байт-код не может фальсифицировать ссылки или
нарушать разрешения доступа к объектам (как в нашем примере с кредиткой).
Он не может выполнять недопустимые преобразования типов и использовать
объекты непредвиденным образом. Он даже не может порождать некоторые
виды внутренних ошибок, таких как выход за верхнюю или нижнюю границу
внутреннего стека. Эти фундаментальные гарантии лежат в основе всей модели
безопасности Java.
Вы можете спросить: разве аналогичных мер безопасности нет во многих интерпретируемых языках? Конечно, вам не удастся повредить интерпретатор языка
BASIC некорректной строкой написанного на BASIC кода, но не забывайте, что
в большинстве интерпретируемых языков защита осуществляется на высоком
уровне. В таких языках обычно есть ресурсоемкие интерпретаторы, выполняющие сложную работу на стадии выполнения, и поэтому эти языки неизбежно
более медленные и громоздкие.
Для сравнения, байт-код Java представляет собой относительно простой низкоуровневый набор команд. И после того, как байт-код прошел предварительную
статическую проверку, он затем выполняется интерпретатором на полной скорости и совершено безопасно, без затратных проверок на стадии выполнения.
Это решение стало одним из принципиально важных нововведений Java.
Верификатор является разновидностью математической «системы доказательства теорем». Проходя по байт-коду Java, верификатор анализирует его,
применяя простые индуктивные правила. Такой анализ возможен, потому что
скомпилированный байт-код Java содержит больше информации о типах, чем
объектный код других похожих языков. Байт-код также должен соблюдать
определенные правила, упрощающие его логику работы. Во-первых, многие
команды байт-кода работают только с конкретными типами данных. Например,
при операциях со стеком используются разные команды для обращения к объектам и к каждому из числовых типов Java. Аналогичным образом для каждого
типа данных есть отдельные команды, перемещающие значение в локальную
переменную и из нее.
Во-вторых, тип объекта, полученного в результате любой операции, всегда
известен заранее. Никакие операции байт-кода не уничтожают значения переменных и не генерируют на выходе более одного типа значения. Поэтому всегда
возможно проанализировать следующую команду и ее операнды, чтобы узнать
тип получаемого значения.

44  Глава 1. Современный язык
Так как операция всегда производит заранее известный тип, типы всех элементов
в стеке и локальных переменных в любой момент будущего могут быть определены по исходному состоянию. Совокупность всей информации о типах в любой
конкретный момент называется состоянием типов стека; именно его Java анализирует перед запуском приложения. Java ничего не знает о фактических значениях, хранящихся в стеке и в переменных; известны только их типы. Однако этой
информации достаточно для соблюдения правил безопасности и для уверенности
в том, что с объектами не будут выполняться некорректные операции.
Чтобы иметь возможность анализировать состояние типов стека, Java устанавливает дополнительное ограничение на выполнение команд своего байт-кода:
все пути к любой точке в коде должны завершаться с одним и тем же состоянием
типов.

Загрузчики классов
Второй уровень безопасности Java — это загрузчик классов. Он отвечает за
передачу байт-кода классов Java интерпретатору. Каждое приложение, которое
загружает классы из сети, должно использовать для этого загрузчик классов.
После того как класс будет загружен и пройдет проверку верификатора, он остается связанным со своим загрузчиком классов. В результате классы эффективно
делятся на пространства имен в зависимости от своего происхождения. Когда
загруженный класс ссылается на другое имя класса, местонахождение нового
класса предоставляется исходным загрузчиком классов. Это означает, что всем
классам, полученным из конкретного источника, может быть разрешено взаимодействовать только с другими классами из того же источника. Например, браузер
с поддержкой Java может использовать загрузчик классов, чтобы сформировать
отдельное пространство для всех классов, загруженных по заданному веб-адресу
(URL). Загрузчики классов также позволяют реализовать комплексную защиту
на основе криптографических цифровых подписей классов.
Поиск классов всегда начинается со встроенных системных классов Java. Они
загружаются из тех мест, которые указаны в переменной classpath интерпретатора Java (см. главу 3). Классы из classpath загружаются системой только один
раз и остаются неизменными. Это означает, что приложение не может заменить
фундаментальные системные классы собственными версиями с измененной
функциональностью.

Менеджеры безопасности
Менеджер безопасности отвечает за принятие решений безопасности на уровне
приложения. Он представляет собой объект, который может быть установлен

Безопасность на уровне исполнительной системы Java  45

приложением для ограничения доступа к системным ресурсам. Менеджер безо­
пасности получает возможность вмешиваться в происходящее каждый раз, когда
приложение пытается обратиться к таким ресурсам, как файловая система,
сетевые порты, внешние процессы и оконная среда. Менеджер безопасности
может разрешить или отклонить каждый такой запрос.
Менеджеры безопасности в первую очередь представляют интерес для приложений, в которых выполнение ненадежного кода является частью нормальной работы. Например, браузер с поддержкой Java может запускать апплеты, загружая
их из ненадежных источников в интернете. Одним из первых действий такого
браузера должна стать установка менеджера безопасности. С этого момента
менеджер безопасности будет ограничивать соответствующие виды доступа.
У приложения появится возможность установить оптимальный уровень доверия перед выполнением произвольного блока кода. После того как менеджер
безопасности будет установлен, его уже нельзя заменить.
Менеджер безопасности работает в сочетании с контроллером доступа, который
позволяет реализовать политики безопасности на высоком уровне посредством
редактирования файла декларативной политики безопасности. Политики доступа могут быть настолько простыми или сложными, насколько того требует
конкретное приложение.
Иногда бывает достаточно запретить доступ ко всем ресурсам или к целым категориям сервисов (например, к файловой системе или к сети). Также возможно принятие сложных решений на основании высокоуровневой информации.
Например, браузер с поддержкой Java может использовать такую политику
доступа, которая позволяет пользователям указать уровень доверия для апплета и разрешать или запрещать доступ к определенным ресурсам в каждой
конкретной ситуации. Конечно, предполагается, что браузер может определить,
каким апплетам следует доверять. Вскоре вы узнаете, как эта проблема решается
посредством цифровых подписей кода.
Целостность менеджера безопасности зависит от защиты, предоставляемой
более низкими уровнями модели безопасности Java. Без гарантий, предоставляемых верификатором и загрузчиком классов, высокоуровневые предположения относительно безопасности системных ресурсов не имеют смысла.
Безопасность, предоставляемая верификатором байт-кода Java, означает, что
интерпретатор не может быть поврежден или фальсифицирован, а код Java
будет использовать компоненты именно так, как следует. В свою очередь, это
означает, что загрузчик классов может гарантировать, что приложение использует фундаментальные системные классы Java, а все обращения к базовым
системным ресурсам могут осуществляться только через эти классы. При этих
ограничениях контроль над такими ресурсами может быть централизован на
высоком уровне с помощью менеджера безопасности и политики, определяемой пользователем.

46  Глава 1. Современный язык

Безопасность на уровнях приложения
и пользователя
Есть тонкая граница между силой, достаточной для решения полезных задач,
и силой делать все что угодно. Java закладывает основу для формирования
безопасной среды, в которой ненадежный код может подвергаться карантину
и выполняться безопасно. Тем не менее если вас не устраивает, что ваш код
ограничивается узкими рамками «черного ящика» и выполняется в полной
изоляции, вам придется разрешить ему доступ хотя бы к некоторым системным ресурсам. Каждый вид доступа сопряжен как с определенными рисками,
так и с преимуществами. Например, в среде браузера преимущество предоставления ненадежному (неизвестному) апплету доступа к вашей оконной
системе заключается в том, что он сможет отображать информацию. Значит,
вы сможете выводить на экран что-то полезное. Но есть риск, что вместо
этого апплет будет выводить какие-то бесполезные, надоедливые или оскорбительные слова.
Вот один крайний случай: даже простой запуск приложения предоставляет ему
ресурс — машинное время. Оно может использоваться рационально или растрачиваться понапрасну. Трудно помешать ненадежному приложению расходовать
ваше время или даже попытаться провести атаку «отказа в обслуживании».
Другой крайний случай: мощное приложение, пользующееся доверием, может
обоснованно требовать доступа к самым разным системным ресурсам (к файловой системе, к созданию процессов, к сетевым интерфейсам), а вредоносное
приложение с доступом к этим же ресурсам может устроить хаос. Суть в том,
что всегда необходимо уделять внимание важным проблемам безопасности,
иногда очень сложным.
В некоторых ситуациях допустимо просто предложить пользователю подтвердить запрос кнопкой «ОК». Язык Java предоставляет средства для реализации
любых политик безопасности. Тем не менее содержание этих политик в конечном
счете зависит от вашей уверенности в происхождении и целостности кода. Здесь
в игру вступают цифровые подписи.
Цифровые подписи в сочетании с сертификатами — это средства для проверки
того, что данные действительно поступили из указанного источника и их никто не подменил по пути. Если банк снабжает своей цифровой подписью свое
приложение «Чековая книжка», то вы можете убедиться в том, что приложение
действительно получено от этого банка (а не от какого-то самозванца) и попало
к вам в неизмененном виде. Следовательно, вы можете разрешить своему браузеру доверять апплетам с цифровой подписью этого банка.

История Java  47

История Java
Язык активно развивается, и бывает трудно уследить, что в нем доступно сейчас,
что обещали разработчики и что изменилось за последнее время. В последующих разделах приведен краткий рассказ о прошлом, настоящем и будущем
Java. Не огорчайтесь, если какие-то термины будут непонятны. Некоторые из
них мы рассмотрим в книге, а остальные вы можете самостоятельно изучить по
мере появления практических навыков и уверенного владения основами Java.
Что касается версий Java, в прилагаемых к ним файлах документации (release
notes) от Oracle есть хорошие списки вносимых изменений со ссылками на более
подробные сведения. Если вы используете старые версии, попробуйте почитать
документацию Oracle (https://docs.oracle.com/en/java).

Прошлое: Java 1.0 — Java 13
Версия Java 1.0 предоставила базовую инфраструктуру для разработки Java: сам
язык и пакеты, позволяющие писать апплеты и простые приложения. Версия 1.0
официально считается устаревшей, но все еще существует немало апплетов,
соответствующих ее API.
Версия Java 1.1 заменила 1.0. В ней были реализованы значительные улучшения
пакета AWT (Abstract Window Toolkit) — исходного средства разработки графических интерфейсов в Java, а также новые паттерны событий, новые языковые
средства (такие, как рефлексия и внутренние классы) и другие крайне важные
возможности. Эта версия много лет поддерживалась многими версиями браузеров Netscape и Internet Explorer. По разным причинам мир браузеров надолго
застрял в этом состоянии.
Версия Java 1.2, получившая от Sun дополнительное название «Java 2», была
выпущена в качестве основной версии в декабре 1998 года. Она содержала много
усовершенствований и дополнений, прежде всего в наборе функций API, включенном в стандартные поставки. Самым заметным дополнением стал пакет разработки графических интерфейсов Swing, представленный в качестве фундаментального API, и новый полноценный API для двумерной графики. Swing — это
усовершенствованный GUI-инструментарий для Java, заметно превосходящий
по своим возможностям старый пакет AWT. (Swing, AWT и некоторые другие
пакеты получили общее название JFC, или Java Foundation Classes). В версии
1.2 также появился полноценный API для работы с коллекциями.
Версия Java 1.3, выпущенная в начале 2000 года, включала ряд второстепенных
улучшений, но в первую очередь была направлена на быстродействие. В вер-

48  Глава 1. Современный язык
сии 1.3 код Java стал заметно быстрее работать на многих платформах, а заодно
оказались исправленными многие ошибки пакета Swing. Тогда же происходило становление нескольких API корпоративного уровня, таких как Servlets
и Enterprise JavaBeans.
В версию Java 1.4, выпущенную в 2002 году, был интегрирован новый набор API
и многие давно ожидавшиеся возможности. В их число входили проверочные
утверждения, регулярные выражения, API конфигураций и протоколирования,
новая система ввода-вывода для крупномасштабных приложений, стандартная
поддержка XML, фундаментальные усовершенствования в AWT и в Swing,
а также развитая поддержка API сервлетов Java для веб-приложений.
Версия Java 5, выпущенная в 2004 году, стала основной версией, в которую были
включены многие давно ожидавшиеся улучшения синтаксиса, в том числе: обобщения, безопасные по типам перечисления, расширенный цикл for, переменные
списки аргументов, статическое импортирование, автоматическая упаковка
и распаковка примитивных типов, а также усовершенствованные метаданные
классов. Новый API многопоточности предоставил мощные средства управления
потоками; также были добавлены API для форматирования печати и парсинга
(разбора данных), аналогичные тем, которые есть в языке C. Механизм RMI
(Remote Method Invocation) был переработан, чтобы исключить необходимость
в скомпилированных каркасах и заглушках. Также был существенно дополнен
стандартный XML API.
Версия Java 6, выпущенная в конце 2006 года, была относительно второстепенной. Она не добавила в язык Java никаких новых синтаксических элементов, зато
содержала новые API расширений (например, для XML и веб-служб).
Версия Java 7, выпущенная в 2011 году, стала относительно существенным
обновлением. В язык был внесен ряд дополнений — например, возможность
использования строк в командах switch (об этом мы расскажем далее). Также
за пять лет, прошедших с выхода Java 6, появились такие важные усовершенствования, как новая библиотека ввода-вывода java.nio.
В версии Java 8, выпущенной в 2014 году, была завершена работа над такими
средствами, как лямбда-выражения и методы по умолчанию. (Ранее их исключили из Java 7 из-за того, что не успевали выпустить ее вовремя.) Также в восьмой
версии была доработана поддержка даты и времени, в том числе возможность
создания неизменяемых объектов с датами, удобных для использования в только
что появившихся лямбда-выражениях.
В версии Java 9, выпущенной после нескольких задержек в 2017 году, появилась
система модулей (Project Jigsaw), а также REPL-оболочка (Read-Evaluate-Print
Loop) jshell. Мы будем использовать jshell во многих кратких примерах кода
в последующих главах книги. В Java 9 из JDK также была исключена поддержка
JavaDB.

История Java  49

Версия Java 10, выпущенная вскоре после Java 9 в начале 2018 года, получила
обновленный механизм уборки мусора, а также ряд других возможностей, например корневые сертификаты для сборок OpenJDK. Была добавлена поддержка
неизменяемых коллекций, а поддержка пакетов со старым стилем оформления
(например, Aqua от Apple) прекратилась.
В версии Java 11, выпущенной в конце 2018 года, появился стандартный клиент
HTTP и протокол TLS 1.3. Модули JavaFX и Java EE были удалены. (Модуль
JavaFX был переработан в автономную библиотеку.) Также были исключены
апплеты. Наряду с Java 8 версия Java 11 стала частью системы долгосрочной
поддержки Oracle (LTS, Long Time Support). Некоторые версии — Java 8, Java 11
и, возможно, Java 17 — будут сопровождаться еще долго. Oracle пытается изменить процесс перехода пользователей и разработчиков на новые версии, однако
у многих есть веские причины для того, чтобы продолжать пользоваться хорошо
известными версиями. С планами и мыслями Oracle относительно LTS-версий
и не-LTS-версий можно ознакомиться в Oracle Technology Network, в разделе
«Oracle Java SE Support Roadmap» (https://www.oracle.com/java/technologies/java-sesupport-roadmap.html).
В версии Java 12, выпущенной в начале 2019 года, были добавлены некоторые
улучшения синтаксиса, в том числе предварительный вариант выражений
switch.
Версия Java 13, выпущенная в сентябре 2019 года, включала предварительные
варианты новых возможностей языка (например, текстовых блоков), а также
значительное изменение реализации API сокетов. Согласно официальной документации, эта впечатляющая разработка предоставляет «более простую и более
современную реализацию, которая упрощает сопровождение и отладку».

Настоящее: Java 14
В книгу включены все самые новые и полезные усовершенствования на момент
последней фазы выпуска Java 14 весной 2020 года. В этой версии добавлен ряд
улучшений синтаксиса в предварительных реализациях, обновлен механизм
уборки мусора, удален API Pack200 и связанные с ним инструменты. Выражения switch, впервые представленные в Java 12, перешли из предварительного
состояния в стандартный язык. К тому времени, когда вы будете читать эту
книгу, появятся новые версии JDK, поскольку они выпускаются каждые полгода.
Oracle хочет, чтобы разработчики рассматривали новые версии как обычные
обновления функциональности. Для целей этой книги вам будет достаточно
Java 11 — надежной версии с долгосрочной поддержкой. При изучении языка
вам не надо беспокоиться обо всех его обновлениях, но если вы используете Java
в реальных проектах, сверьтесь с «дорожной картой» Java, чтобы решить, имеет
ли смысл идти ногу со временем. В главе 13 показано, как можно отслеживать

50  Глава 1. Современный язык
эту «дорожную карту» и как перерабатывать существующий код для использования новейших функций.

Сводка функциональности
Краткая сводка важнейших функциональных возможностей текущего базового
API языка Java:
JDBC (Java Database Connectivity) — основное средство для взаимодействия
с базами данных (начиная с Java 1.1).
RMI (Remote Method Invocation) — система распределенных объектов Java.
RMI позволяет вызывать методы объектов, размещенных на сервере в другом месте сети (начиная с Java 1.1).
Java Security — механизм управления доступом к системным ресурсам, объединенный с единым интерфейсом к криптографическим средствам. Java
Security закладывает основу для классов с цифровыми подписями, о чем
говорилось ранее.
Java Desktop — объединяющий термин для множества функций, появившихся в Java 9, среди которых: компоненты пользовательского интерфейса
Swing; «вариативный пользовательский интерфейс» (способность интерфейса адаптироваться к разным платформам); перетаскивание; 2D-графика;
печать на принтерах; работа с изображениями и звуком; средства доступности (интеграция со специальными программами и с оборудованием для
людей с ограниченными возможностями).
Интернационализация — возможность написания программ, которые адаптируются к языку и локальному контексту, выбранному пользователем;
программа автоматически выводит текст на подходящем языке (начиная
с Java 1.1).
JNDI (Java Naming and Directory Interface) — общая служба для просмотра
ресурсов. JNDI объединяет доступ к службам каталогов, включая LDAP,
Novell NDS и ряд других.
Далее перечислены API стандартных расширений языка. Некоторые из них
(например, предназначенные для работы с XML и веб-сервисами) входят в стандартное издание Java; другие надо загружать отдельно и развертывать в вашем
приложении или на сервере.
JavaMail — унифицированный API для приложений, работающих с электронной почтой.
Java Media Framework — еще один обобщающий термин, включающий
Java 2D, Java 3D, Java Media Framework (фреймворк для координации вывода со многими разными типами контента), Java Speech (для распознава-

История Java  51

ния и синтеза речи), Java Sound (для работы со звуком высокого качества),
Java TV (для интерактивного телевидения и аналогичных приложений)
и т. д.
Сервлеты Java — средство для создания веб-приложений, работающих на
стороне сервера.
Криптография Java — актуальные реализации криптографических алгоритмов. (Этот пакет был отделен от Java Security по юридическим причинам.)
XML / XSL — средства для создания документов XML, для их обработки,
проверки, отображения на объекты Java и обратно, преобразования при помощи таблиц стилей.
В этой книге мы стараемся дать вам представление о некоторых из этих возможностей. К сожалению для нас (но к счастью для разработчиков), среда Java теперь
стала настолько богатой, что рассказать обо всем в одной книге уже невозможно.

Будущее
В наши дни язык Java уже не является модной новинкой, но остается одной
из популярнейших платформ для разработки приложений. Это особенно
справедливо в таких областях, как веб-службы, фреймворки веб-приложений
и инструменты для работы с XML. Хотя языку Java не удалось доминировать на
мобильных платформах (как ему, казалось бы, было суждено), тем не менее Java
и его основные API можно использовать в программировании для мобильной
операционной системы Google Android, используемой на миллиардах устройств
по всему миру. В лагере Microsoft язык C#, производный от Java, захватил
большую часть разработки .NET и принес базовый синтаксис и паттерны Java
на соответствующие платформы.
Виртуальная машина Java (JVM) сама по себе является интересной областью
исследования и развития. Появляются новые языки, которые используют набор
возможностей и повсеместную доступность JVM. Clojure — мощный функцио­
нальный язык, у которого растет число поклонников в самых разных кругах.
Kotlin — другой язык, убедительно завоевывающий популярность в сфере разработки для Android (где ранее господствовал Java). Это язык общего назначения, получающий широкое распространение в новых средах и функционально
совместимый с Java.
Пожалуй, самые интересные области изменений в Java в наши дни связаны
с двумя тенденциями: использование облегченных и упрощенных фреймворков
для бизнеса, а также интеграция платформы Java с динамическими языками для
сценарного программирования веб-страниц и расширений. Всех нас ждет еще
более интересная работа.

52  Глава 1. Современный язык

Доступные средства
У вас есть выбор из нескольких вариантов сред разработки и исполнительных
систем Java. Пакет Oracle JDK доступен для macOS, Windows и Linux. За дополнительной информацией о том, где получить новейшую версию JDK, обращайтесь на веб-сайт Java корпорации Oracle (https://www.oracle.com/java/technologies).
С 2017 года Oracle официально поддерживает обновления проекта с открытым
кодом OpenJDK. Эта бесплатная версия может оказаться достаточной для отдельных программистов и для малых (и даже средних) компаний. Выпуски
OpenJDK отстают от коммерческих выпусков и не включают профессиональную
поддержку от Oracle, но Oracle твердо заявляет о сохранении бесплатного и открытого доступа к Java. Все примеры в книге мы писали и тестировали с помощью OpenJDK. Более подробную информацию вы найдете в первоисточнике,
то есть на сайте OpenJDK (https://openjdk.java.net).
Для быстрой установки бесплатной версии Java 11 Amazon предоставляет свой
дистрибутив Corretto с удобными инсталляторами для всех трех основных
платформ. Версия Java 11 достаточна для всех примеров из этой книги, хотя мы
упомянем и несколько возможностей из более поздних версий.
Также существует целый ряд интегрированных сред разработки (Integrated
Development Environment, IDE) для Java. Одна из них рассматривается в этой
книге: IntelliJ IDEA от компании JetBrains (https://www.jetbrains.com/idea) в бесплатном издании Community Edition. В этой многофункциональной среде
разработки, содержащей полный набор необходимых инструментов, вам будет
удобно писать, тестировать и упаковывать программы.

ГЛАВА 2

Первое приложение

Прежде чем браться за детальное рассмотрение языка Java, попробуем себя
в деле: возьмем фрагменты работоспособного кода и немного поэкспериментируем с ними. В этой главе мы напишем маленькое приложение, которое демонстрирует многие концепции, встречающиеся в книге. Оно послужит своего рода
презентацией основных возможностей языка и написанных на нем приложений.
Эта глава также служит кратким введением в объектно-ориентированные и многопоточные аспекты Java. Вероятно, эти концепции вам еще неизвестны, и в таком
случае мы надеемся, что первое знакомство с ними в Java будет простым и приятным. А если вы уже работали в других объектно-ориентированных или многопоточных средах программирования, то наверняка оцените простоту и элегантность
Java. Эта глава дает самый краткий обзор языка и общее представление о том,
как им пользоваться. Не беспокойтесь, если какие-то из описанных концепций
покажутся непонятными: мы подробнее расскажем о них в последующих главах.
Трудно переоценить важность экспериментов при изучении новых концепций — как в этой главе, так и в других. Не ограничивайтесь чтением примеров
кода — выполняйте их. При каждой возможности мы будем показывать, как использовать jshell (подробнее см. раздел «Первые эксперименты с Java», с. 98),
чтобы проверять фрагменты кода в реальном времени. Исходный код этих примеров, а также всех остальных примеров книги вы можете загрузить с портала
GitHub (https://github.com/l0y/learnjava5e). Компилируйте программы и проверяйте
их в работе. А потом превращайте наши примеры в свои: экспериментируйте
с ними, изменяйте их логику работы, ломайте, чините… в общем, получайте
удовольствие.

Инструменты и среда Java
Теоретически для написания, компиляции и запуска простых Java-приложений
достаточно пакета с открытым кодом Java Development Kit от Oracle (OpenJDK)

54  Глава 2. Первое приложение
и обычного текстового редактора (vi, «Блокнот» и т. д.). Но на практике почти
каждый современный программист пишет приложения в интегрированной среде
разработки (IDE). Это дает много преимуществ: удобный просмотр исходного
кода c цветовым выделением синтаксиса, помощь с навигацией, управление
версиями исходного кода, встроенная документация, сборка, рефакторинг (переработка кода) и развертывание приложений — все эти возможности находятся
прямо под рукой. По этой причине мы пропустим академическое описание
работы с командной строкой и начнем с популярной бесплатной IDE, которая
называется IntelliJ IDEA CE (Community Edition). Впрочем, если вам не хочется работать в IDE, используйте командную строку. Примеры команд: javac
HelloJava.java (для компиляции) и java HelloJava (для запуска).
Для работы в среде IntelliJ IDEA надо сначала установить сам язык Java. В книге
рассматривается версия Java 11 (с несколькими упоминаниями нововведений
в версиях 12 и 13). Хотя примеры кода из этой главы будут успешно работать
и в более ранних версиях, лучше установить JDK версии 11 или выше, чтобы
все примеры из книги гарантированно компилировались. JDK содержит некоторые средства разработчика, которые будут рассмотрены в главе 3. Чтобы
узнать, какая версия установлена на вашем компьютере (и установлена ли),
введите команду java -version в командной строке. Если Java отсутствует или
версия более ранняя, чем JDK 11, загрузите нужную версию с сайта OpenJDK
(https://jdk.java.net). Вы найдете там весь ряд версий для Linux, macOS и Windows
(https://jdk.java.net/archive).
Интегрированную среду IntelliJ IDEA вы можете загрузить с сайта компании
JetBrains (https://www.jetbrains.com/idea/download). Для работы с этой книгой, как
и для создания множества приложений, более чем достаточно бесплатного издания Community. Загружаемый файл представляет собой инсталлятор .exe
(или архив .zip) для Windows, инсталлятор .dmg для macOS или архив .tar.
gz для Linux. При необходимости распакуйте архив и запустите инсталлятор.
В конце книги, в приложении (с. 497), приведена более подробная информация
о загрузке и установке IDEA, а также о том, как загрузить примеры кода из книги.

Установка JDK
Следует сразу сказать, что вы можете свободно загружать и использовать официальный коммерческий пакет Oracle JDK для личных целей. На сайте Oracle
(https://www.oracle.com/java) всегда есть новейшая версия, а также ряд предшествующих версий, в том числе с долгосрочной поддержкой. Старые версии иногда
бывают нужны разработчикам в целях совместимости.
Но если вы планируете использовать Java в коммерческих целях или в составе
проектной группы, то для таких случаев Oracle JDK предоставляется на строгих
условиях платного лицензирования. Из-за этого (и по другим, более философ-

Инструменты и среда Java  55

ским причинам) мы обычно используем бесплатный пакет OpenJDK, упоминавшийся ранее. К сожалению, в этой версии с открытым кодом нет удобных
программ установки (инсталляторов) для разных платформ. Если вам нужна
простота установки и версия с долгосрочной поддержкой (например, Java 8
или 11), выберите другой дистрибутив OpenJDK, например Amazon Corretto
(https://aws.amazon.com/ru/corretto).
Для читателей, которые хотят иметь свободу выбора версии Java и не боятся
небольшой работы по настройке, мы расскажем, как устанавливать OpenJDK
на каждой из трех основных платформ. Для примера мы выбрали версию
Java 13.0.1 — последнюю на момент написания книги. Независимо от того,
в какой операционной системе вы работаете, для загрузки OpenJDK перейдите
на соответствующую страницу на сайте Oracle (https://jdk.java.net/archive).

Установка OpenJDK в Linux
Файл, загружаемый для типичных систем Linux, представляет собой TAR-архив
(.tar.gz). Вы можете распаковать его в любой общий каталог по вашему выбору (например, /usr/lib/jvm). Запустите приложение терминала и выполните
следующие команды1, чтобы перейти в каталог загрузки (например, Downloads),
распаковать архив и проверить Java:
~ $ cd Downloads
~/Downloads $ sudo tar xvf openjdk-13.0.1_linux-x64_bin.tar.gz
--directory /usr/lib/jvm
...
jdk-13.0.1/lib/src.zip
jdk-13.0.1/lib/tzdb.dat
jdk-13.0.1/release

\

~/Downloads $ /usr/lib/jvm/jdk-13.0.1/bin/java -version
openjdk version "13.0.1" 2019-10-15
OpenJDK Runtime Environment (build 13.0.1+9)
OpenJDK 64-Bit Server VM (build 13.0.1+9, mixed mode, sharing)

После успешной установки Java настройте терминал для использования этой
среды, присвоив значения переменных JAVA_HOME и PATH . Чтобы убедиться
в правильности этой конфигурации, проверьте версию компилятора Java javac:

1

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

56  Глава 2. Первое приложение
~/Downloads $ cd
~ $ export JAVA_HOME=/usr/lib/jvm/jdk-13.0.1
~ $ export PATH=$PATH:$JAVA_HOME/bin
~ $ javac -version
javac 13.0.1

Изменения в JAVA_HOME и PATH надо закрепить, обновив стартовые сценарии
или сценарии rc для вашей оболочки. Например, обе строки export, только что
использованные в терминале, можно добавить в файл .bashrc.
Также надо заметить, что многие дистрибутивы Linux предоставляют доступ
к некоторым версиям Java через свои менеджеры пакетов. Поищите в интернете информацию по строкам вида «install java ubuntu» или «install java redhat»
и посмотрите, есть ли для вашей системы альтернативные способы установки
Java, которые лучше соответствуют вашему привычному стилю работы в Linux.

Установка OpenJDK в macOS
Для пользователей macOS установка OpenJDK похожа на установку в Linux:
загрузите архив .tar.gz и распакуйте его в нужное место. В отличие от Linux,
«нужное место» определяется вполне конкретно1.
Воспользуйтесь приложением «Терминал» (Terminal) из папки Applications
Utilities, чтобы распаковать и переместить папку с OpenJDK:
~ $ cd Downloads
Downloads $ tar xf openjdk-13.0.1_osx-x64_bin.tar.gz
Downloads $ sudo mv jdk-13.0.1.jdk /Library/Java/JavaVirtualMachines/

Команда sudo позволяет пользователям с административными привилегиями
выполнять специальные действия, обычно зарезервированные для «суперпользователя». Вам будет предложено ввести пароль. После перемещения папки
JDK задайте значение переменной среды JAVA_HOME. Команда java, включенная
в macOS, теперь сможет найти установленную версию.

1

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

Инструменты и среда Java  57
Downloads $ cd ~
~ $ export
\
JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk-13.0.1.jdk/Contents/Home
~ $ java -version
openjdk version "13.0.1" 2019-10-15
OpenJDK Runtime Environment (build 13.0.1+9)
OpenJDK 64-Bit Server VM (build 13.0.1+9, mixed mode, sharing)

Как и в случае с Linux, вам надо добавить строку JAVA_HOME в соответствующий
стартовый файл (например, в файл .bash_proile в вашем домашнем каталоге),
если вы будете работать с Java в командной строке.
У пользователей macOS 10.15 (Catalina) и более поздних версий могут возникнуть некоторые сложности при установке и проверке Java. Вследствие изменений в macOS корпорация Oracle еще не сертифицировала Java для Catalina
(на момент выхода книги). Конечно, Java все равно можно запускать в системах
Catalina, но наиболее сложные приложения могут столкнуться с ошибками. Заинтересованные пользователи могут прочитать технические заметки Oracle по
использованию JDK с Catalina. В первой части этих заметок рассматривается
установка официального JDK, а вторая часть посвящена установке из архива
.tar.gz, описанной выше.

Установка OpenJDK в Windows
Системы Windows разделяют многие концепции с системами *nix, хотя пользовательские интерфейсы для работы с этимиконцепциями сильно различаются.
Загрузите архив OpenJDK для Windows — это должен быть файл .zip (вместо
файла .tar.gz ). Распакуйте файл в нужную папку. Как и в случае с Linux,
«нужную папку» выбираете вы. Мы создали папку Java в C:\Program Files
и поместили в нее содержимое архива, как показано на рис. 2.1.
Когда папка JDK появится на своем месте, настройте переменные среды (по
аналогии с macOS и Linux). Самый быстрый способ получить доступ к переменным среды — провести поиск по строке «environment» («переменные среды»)
и найти в результатах поиска вариант «Edit the system environment variables»
(«Изменение системных переменных среды»), как показано на рис. 2.2.
Сначала надо создать новую запись для переменной JAVA_HOME и дополнить строку Path информацией о Java. Мы решили добавить эти изменения в системную
часть (System variables), но если вы единственный пользователь своего компьютера, их также можно добавить в пользовательскую часть (User variables).
Создайте новую переменную JAVA_HOME и присвойте ей значение: путь к папке,
в которой установлен JDK (рис. 2.3).

Рис. 2.1. Папка Java в Windows

Рис. 2.2. Поиск редактора переменных среды в Windows

Инструменты и среда Java  59

Рис. 2.3. Создание переменной среды JAVA_HOME в Windows
После того как вы присвоите значение переменной JAVA_HOME, добавьте соответствующую запись в переменную Path, чтобы Windows знала, где искать программы java и javac. Эта запись должна указывать на папку bin, находящуюся
в папке с Java. Чтобы указать в Path значение переменной JAVA_HOME, заключите
ее имя между символами % (%JAVA_HOME%), как показано на рис. 2.4.
Приложение «Командная строка» (Command Prompt) выполняет в Windows
те же функции, что и терминалы в macOS и Linux. Запустите это приложение
и введите команду для проверки версии Java. Результат выглядит примерно так,
как показано на рис. 2.5.

60  Глава 2. Первое приложение

Рис. 2.4. Редактирование переменной среды Path в Windows

Рис. 2.5. Проверка версии Java в Windows

Инструменты и среда Java  61

Конечно, вы можете и дальше работать с Java в командной строке, но лучше
указать в настройках IntelliJ IDEA путь к папке с JDK, а затем постоянно использовать эту удобную среду разработки.

Настройка конфигурации IntelliJ IDEA и создание проекта
При первом запуске IDEA выберите создание нового проекта («New Project»).
Затем убедитесь, что в строке «Project SDK» указана версия Java 11 или выше,
как показано на рис. 2.6, и нажмите кнопку Next.

Рис. 2.6. Первое диалоговое окно нового проекта

62  Глава 2. Первое приложение
Теперь выберите шаблон Command Line App. Он содержит минимальный класс
Java с методом main(), который можно выполнить. (В последующих главах будет подробно рассмотрена структура программ Java и команд, из которых они
состоят.) Выбрав шаблон так, как показано на рис. 2.7, нажмите кнопку Next.

Рис. 2.7. Выбор шаблона для проекта
Наконец, введите имя проекта. Для учебного приложения из этой главы мы
выбрали имя HelloJava. IDEA предложит путь к папке для файлов этого проекта; она будет внутри заданной по умолчанию папки для всех проектов IDEA.
В других случаях вы могли бы нажать кнопку с многоточием (…), чтобы выбрать

Инструменты и среда Java  63

любую другую папку на компьютере. Когда эти два поля будут заполнены, нажмите кнопку Finish (рис. 2.8).

Рис. 2.8. Выбор имени проекта и папки для его файлов
Поздравляем! Вы создали программу Java… почти. В нее надо добавить строку
кода, которая будет выводить сообщение на экран.
Добавьте следующую строку между фигурными скобками, после строки public
static void main(String[] args):
System.out.println("Hello, World!");

Готовая программа должна выглядеть примерно так, как в правой части окна
на рис. 2.9.
Сначала мы запустим этот код, а затем слегка дополним его, чтобы он стал чуть
менее тривиальным. В последующих главах начнутся более интересные примеры, в которых мы будем использовать все больше и больше элементов Java.
Тем не менее процедура создания всех примеров будет более или менее похожей.
Запомните эти подготовительные действия, они вам еще не раз пригодятся.

64  Глава 2. Первое приложение

Рис. 2.9. Запуск программы

Запуск программы
Простой шаблон, который вам предоставила IDEA, стал хорошей отправной
точкой для вашей первой программы. Обратите внимание: на верхней панели
инструментов вы видите название класса Main, а рядом находится кнопка запуска
программы, обозначенная зеленой стрелкой (см. рис. 2.9). Эта кнопка означает, что IDEA понимает, как запустить метод main() в этом классе. Попробуйте
нажать эту кнопку. На вкладке Run в нижней части окна появится сообщение
«Hello World!». И снова поздравляем: вы только что запустили свою первую
программу на Java.

Загрузка примеров кода
Все примеры кода, приведенные в книге, мы разместили на сайте GitHub. Он
считается основным облачным репозиторием как для общедоступных проектов
с открытым кодом, так и для коммерческих проектов с закрытым кодом. Кроме
простых средств для хранения исходного кода и контроля версий, GitHub предоставляет много полезных инструментов. Если вы будете писать приложение
или библиотеку, которыми захотите поделиться с другими, зарегистрируйтесь
на GitHub и хорошо изучите его возможности. Но ZIP-файлы общедоступных
проектов можно загружать без регистрации, как показано на рис. 2.10.

Инструменты и среда Java  65

Рис. 2.10. Загрузка ZIP-архива с GitHub
В итоге у вас должен быть загружен файл с именем learnjava5e-master.zip;
это архив «главной» (master) ветви репозитория. Если вы знакомы с GitHub по
другим проектам, то можете клонировать репозиторий, но это не обязательно:
наш ZIP-файл содержит все необходимое для запуска примеров кода, приведенных в книге.
После распаковки загруженного файла будут созданы папки для всех глав,
содержащих примеры, а также папка game с кодом простой игры. Эта игра
демонстрирует в одном завершенном приложении большую часть концепций
программирования, представленных в книге. И примеры, и игра подробно рассматриваются в следующих главах.
Как упоминалось ранее, любой пример кода, взятый из этого ZIP-файла, можно
скомпилировать и запустить прямо из командной строки. Также вы можете
импортировать код в свою любимую IDE. В приложении (с. 497) подробно
рассказано о том, как лучше всего импортировать эти примеры в IntelliJ IDEA.

66  Глава 2. Первое приложение

HelloJava
По давней программистской традиции мы предлагаем начать изучение языка
с простейшего приложения в стиле «Hello World», которое мы назвали HelloJava.
Затем, прежде чем расстаться с этим примером, мы несколько раз его доработаем (HelloJava, HelloJava2, HelloJava3), дополняя новыми возможностями
и демонстрируя новые концепции. Начнем с минимальной версии:
public class HelloJava {
public static void main( String[] args ) {
System.out.println( "Hello, Java!" );
}
}

Эта программа из пяти строк объявляет класс с именем HelloJava и метод с именем main(). В ней используется стандартный (заранее определенный в языке)
метод println() , выводящий строку текста. Это программа для командной
строки, то есть она выполняется в командном интерпретаторе, напоминающем
«окно DOS», и выводит там свои результаты. Ранее вы использовали шаблон
от IDEA, в котором у класса было имя Main. В этом нет ничего неправильного,
но при создании более сложных программ лучше давать классам более содержательные имена, что мы и постараемся делать в примерах кода. Независимо
от имени класса, вывод в командную строку считается немного старомодным,
поэтому прежде чем двигаться дальше, мы создадим для HelloJava графический
интерфейс (GUI). Пока не отвлекайтесь на код; просто продолжайте читать,
а мы вскоре вернемся к объяснениям.
Вместо строки, вызывающей метод println(), мы создадим объект JFrame для
размещения окна на экране. Для начала заменим строку, содержащую println,
следующими тремя строками:
JFrame frame = new JFrame( "Hello, Java!" );
frame.setSize( 300, 300 );
frame.setVisible( true );

Этот фрагмент кода создает объект JFrame с заголовком «Hello, Java!». Объект
JFrame представляет собой графическое окно. Чтобы оно появилось на экране,
мы просто указываем его размер с помощью метода setSize(), а затем делаем
его видимым с помощью метода setVisible().
Если бы мы на этом остановились, то на экране появилось бы пустое окно с текстом «Hello, Java!» в заголовке. Но мы выведем сообщение и в самом окне, не
ограничиваясь его заголовком. Для этого понадобится еще пара строк. В следующем примере добавлен объект JLabel, который выводит надпись, выровненную
по центру окна. Первая строка с оператором import сообщает Java, где искать

HelloJava  67

классы JFrame и JLabel (то есть определения используемых в программе объектов JFrame и JLabel).
import javax.swing.*;
public class HelloJava {
public static void main( String[] args ) {
JFrame frame = new JFrame( "Hello, Java!" );
JLabel label = new JLabel( "Hello, Java!", JLabel.CENTER );
frame.add( label );
frame.setSize( 300, 300 );
frame.setVisible( true );
}
}

Чтобы скомпилировать и запустить этот код, выберите класс ch02/HelloJava.
java на панели проекта в левой части окна IDEA, а затем снова нажмите кнопку
Run на панели инструментов (кнопку с зеленой стрелкой). Результат показан
на рис. 2.11.

Рис. 2.11. Запуск приложения HelloJava

68  Глава 2. Первое приложение
Вы должны увидеть на экране примерно такое окно, как на рис. 2.12. Очередные
поздравления, вы запустили свое второе приложение на Java! Погрейтесь в лучах
славы от экрана своего монитора.

Рис. 2.12. Работающее приложение HelloJava
Учтите, что при щелчке на кнопке закрытия окна оно исчезает с экрана, но программа продолжает работать. (Этот недостаток мы исправим в последующих версиях.) Чтобы завершить программу в IDEA, нажмите кнопку с красным квадратом;
она находится рядом с кнопкой запуска (зеленой стрелкой). А если программа
запущена в командной строке, нажмите для завершения клавиши Ctrl+C. Кстати,
ничто не мешает вам запустить сразу несколько экземпляров (копий) программы.
Программа HelloJava очень мала, но в ней происходит много всего, что не
заметно с первого взгляда. Эти несколько строк кода — всего лишь вершина
айсберга, а под ней скрываются несколько уровней функциональности, предоставляемой языком Java и его библиотеками Swing. В этой главе мы быстро
пройдем немалый путь, чтобы показать вам общую картину. Мы постараемся
привести достаточно информации, чтобы вы хорошо понимали, что происходит
в каждом примере, но отложим подробные объяснения до последующих глав.
Это относится и к синтаксическим элементам языка Java, и к соответствующим
объектно-ориентированным концепциям программирования. Итак, давайте
разберемся, что же происходит в нашем первом примере.

Классы
Прежде всего определяется класс с именем HelloJava:
public class HelloJava {
...

HelloJava  69

Классы — это основные структурные элементы Java и большинства других
­объектно-ориентированных языков. Класс — это группа элементов данных и связанных с ними функций, способных выполнять операции с этими данными. Элементы данных класса называются переменными (или иногда полями), а функции
называются в Java методами. Основные достоинства объектно-­ориентированных
языков — это взаимосвязь между данными и функциональ­ностью в классах,
а также способность классов к инкапсуляции, то есть к сокрытию деталей их внут­
реннего устройства. Инкапсуляция позволяет разработчику не беспокоиться об
этих низкоуровневых деталях.
В приложении класс может представлять нечто конкретное: кнопку на экране,
информацию в электронной таблице и т. д. Класс может представлять и что-то
более абстрактное: скажем, алгоритм сортировки списка или чувство уныния
у персонажа видеоигры. Например, класс, представляющий электронную таблицу, может содержать переменные, хранящие значения отдельных ячеек, и методы
для выполнения операций с этими ячейками («очистить строку», «вычислить
значения» и т. д.).
Класс HelloJava представляет собой целое приложение Java, написанное в виде
одного класса. Он определяет всего один метод main(), содержащий все тело
нашей программы:
public class HelloJava {
public static void main( String[] args ) {
...

При запуске приложения метод main() вызывается первым. Фрагмент String[]
args нужен, чтобы передавать приложению аргументы командной строки. Мы
подробно разберем метод main() в следующем разделе. Наконец, следует заметить, что эта версия нашего приложения не определяет никакие переменные как
части своего класса, но использует две переменные, frame и label, внутри своего
метода main(). Вскоре мы поговорим о переменных более подробно.

Метод main()
Как было показано ранее, при запуске Java-приложения выбирается конкретный
класс, имя которого передается в качестве аргумента виртуальной машине Java.
Когда мы это сделали, команда java проверила класс HelloJava и выяснила, есть
ли в нем специальный метод с именем main(), соответствующий определенной
форме. Такой метод есть, поэтому он был выполнен. Если бы его не было, мы
бы получили сообщение об ошибке. Метод main() является точкой входа для
всех приложений. Каждое автономное приложение Java содержит как минимум
один класс с методом main(), который выполняет необходимые действия для
запуска всего остального, что есть в приложении.

70  Глава 2. Первое приложение
Метод main() создает окно (JFrame), в которое направляется экранный вывод
класса HelloJava. В данном случае этот метод выполняет всю работу приложения. Но обычно в объектно-ориентированных приложениях обязанности
делегируются многим разным классам. Так мы сделаем в следующей версии: добавим второй класс и покажем, что в ходе эволюции нашего приложения метод
main() останется более или менее неизменным — он просто содержит стартовую
процедуру инициализации.
Кратко разберем метод main(), чтобы пояснить, что он делает. Сначала main()
создает объект класса JFrame — обычное окно с заголовком:
JFrame frame = new JFrame( "Hello, Java!" );

Слово new в этой строке кода крайне важно. JFrame — это имя класса, формирующего различные окна на экране. Но сам этот класс представляет собой только
шаблон — что-то вроде «чертежа», по которому строятся все объекты такого типа.
Ключевое слово new приказывает Java выделить память и создать в памяти один
конкретный объект класса JFrame. В данном случае аргумент в круглых скобках
сообщает этому объекту, какой текст надо вывести в заголовке. Мы могли бы
убрать слова “Hello, Java” и оставить пустые круглые скобки, чтобы получить
JFrame без текста в заголовке (но только потому, что такая возможность специально предусмотрена в классе JFrame).
Все окна JFrame в момент создания имеют очень малый размер. Прежде чем
отображать окно на экране, назначим ему ширину и высоту:
frame.setSize( 300, 300 );

Эта строка — пример вызова метода для конкретного объекта. В данном случае
метод setSize(), определенный в классе JFrame, работает с конкретным объектом
класса JFrame. Этот объект присвоен переменной frame. Затем мы аналогичным
образом создаем экземпляр типа JLabel, представляющий собой надпись:
JLabel label = new JLabel( "Hello, Java!", JLabel.CENTER );

Объект класса JLabel можно сравнить с листом бумаги. Он содержит текст,
расположенный в определенном месте, — как и в нашем окне. Это объектно-­
ориентированная концепция: вы используете объект для того, чтобы хранить
в нем текст, вместо того чтобы «написать» текст вызовом соответствующего
метода и двигаться дальше. Скоро вы поймете, почему это делается именно так.
Теперь разместим надпись в только что созданном окне:
frame.add( label );

Здесь для размещения надписи label внутри объекта класса JFrame используется
метод add(). Объект класса JFrame является своего рода контейнером, в котором

HelloJava  71

могут находиться другие объекты. Вскоре мы поясним эту концепцию. Последняя задача для метода main() — отобразить на экране окно и его содержимое,
которое иначе осталось бы невидимым. Согласитесь, что приложение с невидимым окном было бы довольно скучным.
frame.setVisible( true );

Вот и весь метод main(). Рассматривая другие примеры этой главы, вы увидите,
что по мере изменения класса HelloJava этот метод останется в основном неизменным.

Классы и объекты
Класс — это своего рода «чертеж» для какого-либо компонента приложения;
в классе содержатся те методы и переменные, из которых состоит этот компонент.
Когда приложение работает, в нем могут существовать несколько работающих
копий класса. Эти отдельные «воплощения» класса называются его экземплярами или объектами. Два экземпляра конкретного класса могут содержать разные
данные, но методы у них всегда одинаковые.
Для примера возьмем стандартный класс кнопки Button. Существует только
один класс Button , но приложение может создать много разных объектов
класса Button; все они будут экземплярами одного и того же класса. Более
того, два экземпляра Button могут содержать разные данные, то есть кнопки
могут по-разному выглядеть и выполнять разные действия. В этом смысле
класс можно рассматривать как шаблон для изготовления представляемых
им объектов. Класс — это что-то вроде «формочки для печенья», которой мы
«штампуем» готовые к работе экземпляры (объекты) в памяти компьютера.
Как будет показано далее, это еще не все (классы могут определять информацию, которая будет совместно использоваться всеми экземплярами), но пока
достаточно и этого краткого описания. Понятия классов и объектов детально
объясняются в главе 5.
Термин «объект» очень многозначен и иногда, в некоторых контекстах, используется почти как синоним термина «класс». Объекты — это абстрактные сущности,
которые в той или иной форме встречаются во всех объектно-ориентированных
языках. Мы будем использовать термин «объект» для обозначения экземпляра
класса. Таким образом, экземпляр класса Button может называться кнопкой,
объектом Button или просто объектом.
Метод main() в предыдущем примере создает единственный экземпляр класса
JLabel и выводит его внутри экземпляра класса JFrame. При желании попробуйте
изменить метод main() таким образом, чтобы он создал несколько экземпляров
JLabel, отображаемых в одном окне или в нескольких разных окнах.

72  Глава 2. Первое приложение

Переменные и типы
В Java каждый класс определяет собственный тип (тип данных). Вы можете
создать переменную этого типа, а затем сохранить в ней экземпляр этого класса.
Например, переменная может иметь тип Button и хранить экземпляр класса
Button, или иметь тип SpreadSheetCell и хранить объект SpreadSheetCell, или
иметь один из простых типов (таких, как int или float), представляющих числа.
Все переменные обладают типами и не могут хранить произвольные (не соответствующие этим типам) объекты — это важное свойство языка Java, которое
обеспечивает безопасность и корректность кода.
Если ненадолго забыть о переменных, используемых внутри метода main(),
то в нашем примере HelloJava объявляется только одна переменная args. Это
происходит в объявлении метода main():
public static void main( String[] args ) {

Подобно функциям в других языках программирования, метод в Java объявляет список параметров (переменных), которые должны передаваться ему
в аргументах, с указанием типа каждого параметра. В нашем примере метод
main требует, чтобы при вызове ему передавался один параметр: массив объектов String в переменной с именем args. String — фундаментальный класс для
представления текста в Java. С помощью параметра args приложение получает
аргументы командной строки, переданные при его запуске виртуальной машине
Java. (В данном случае мы их не используем.)
До настоящего момента мы упрощенно говорили, что переменные могут хранить в себе объекты. На самом деле в переменных, имеющих типы классов,
хранятся не сами объекты, а ссылки на них. Ссылка (reference) — это указатель
на объект (то есть адрес в памяти, по которому находится объект). Если объявить переменную с типом какого-либо класса, не присвоив ей объект, она не
будет указывать ни на что. По умолчанию ей будет присвоено значение null,
которое означает «значение не определено». Если вы попытаетесь использовать
переменную со значением null таким образом, как если бы она ссылалась на
реальный объект, то в результате возникнет ошибка времени выполнения (runtime
error) NullPointerException.
Конечно, ссылка на объект должна откуда-то появиться. В нашем примере два
объекта были созданы оператором new. Создание объектов мы подробно рассмотрим далее в этой главе.

HelloComponent
До сих пор все приложение HelloJava состояло из одного класса. Более того,
из-за простоты оно уместилось в единственном методе. И хотя мы использова-

HelloJava  73

ли пару объектов для вывода текста в окне, наш код пока не имеет объектноориентированной структуры. Сейчас мы это исправим, добавив в программу
второй класс. Для пользы дела мы откажемся от стандартного класса JLabel
(прости-прощай, JLabel !) и заменим его нашим собственным графическим
классом HelloComponent. Сначала класс HelloComponent будет совсем простым:
он будет выводить сообщение «Hello, Java!» в фиксированной позиции. Затем
мы расширим его функциональность.
Код нового класса очень краток, мы написали всего несколько новых строк:
import java.awt.*;
class HelloComponent extends JComponent {
public void paintComponent( Graphics g ) {
g.drawString( "Hello, Java!", 125, 95 );
}
}

Вы можете добавить этот фрагмент в файл HelloJava.java или поместить в отдельный файл с именем HelloComponent.java. Если вы выбрали первый вариант,
перенесите новую команду import в начало кода, вслед за другой командой
import. Чтобы использовать новый класс вместо JLabel, просто замените две
строки с упоминанием label следующей строкой:
frame.add( new HelloComponent() );

Теперь при компиляции исходного кода HelloJava.java будут созданы два
двоичных файла классов: HelloJava.class и HelloComponent.class (какой бы
вариант организации исходного кода вы ни выбрали). Результат выполнения
кода почти не отличается от версии с JLabel, но при изменении размеров окна
вы заметите, что в нашем классе нет автоматического выравнивания надписи
по центру.
Что же было сделано и почему мы приложили такие усилия, отказавшись
от великолепно работающего компонента JLabel? Мы создали новый класс
HelloComponent, расширив базовый графический класс с именем JComponent.
Расширение класса — это создание нового класса посредством добавления функциональности в существующий класс. (Эта тема рассматривается в следующем
разделе.) Итак, мы создали расширенную разновидность класса JComponent
с методом paintComponent() , который отвечает за вывод сообщения. Наш
метод paintComponent() получает один аргумент — переменную с именем g
(пожалуй, слишком коротким), имеющую тип Graphics. При вызове метода
paintComponent() переменной g присваивается объект Graphics, который используется в теле метода. Метод paintComponent() и класс Graphics будут
рассмотрены далее. Вы поймете, зачем они понадобились, когда мы займемся
добавлением новых функций в свой компонент.

74  Глава 2. Первое приложение

Наследование
Классы Java образуют иерархию «родитель — потомок», в которой родитель
называется суперклассом, а потомок — субклассом, или подклассом. Эти концеп­
ции будут рассмотрены в главе 5. В Java у каждого класса есть ровно один
суперкласс (один родитель), но может быть много субклассов. Единственное
исключение из правила составляет класс Object, находящийся на самой вершине
иерархии: у него нет суперкласса.
В объявлении класса в предыдущем примере ключевое слово extends указывает,
что наш HelloComponent расширяет класс JComponent, то есть является субклассом класса JComponent:
public class HelloComponent extends JComponent { ... }

Субкласс наследует (полностью или частично) переменные и методы своего
суперкласса. Благодаря наследованию, субкласс может использовать эти переменные и методы так, как если бы он сам объявил (определил) их. Субкласс
может добавить к ним собственные переменные и методы. Субкласс также может
переопределить или изменить унаследованные от суперкласса методы, и в этом
случае каждый такой метод скрывается (замещается) своей новой версией,
определенной в субклассе. Таким образом, наследование дает программисту
мощный механизм, в котором каждый субкласс может уточнять и расширять
функциональность своего суперкласса.
Например, на основе гипотетического класса, представляющего электронную
таблицу, можно сделать субкласс научной электронной таблицы, содержащий дополнительные математические функции и встроенные константы. В таком случае
исходный код научной электронной таблицы будет объявлять методы для дополнительных математических функций и переменные для констант, но при этом новый
класс автоматически включит в себя все переменные и методы, образующие функциональность основной (более простой) электронной таблицы; они унаследуются
от родительского класса. Также это означает, что «научная электронная таблица»
не перестает быть «электронной таблицей», а расширенную версию можно использовать везде, где возможно использование основной, более простой. (Как будет
показано далее, этот факт имеет очень глубокие и принципиальные последствия.)
Иными словами, более специализированные объекты могут использоваться вместо
более общих, а их поведение может настраиваться без изменения всего остального
кода приложения. Этот принцип, называемый полиморфизмом, является одним из
столпов объектно-ориентированного программирования.
Наш класс HelloComponent — это субкласс класса JComponent, то есть он наследует
много переменных и методов, которые не объявлены явно в его исходном коде.
Поэтому он может быть полноценным компонентом в составе JFrame, для этого
нужны лишь незначительные настройки.

HelloJava  75

Класс JComponent
Класс JComponent предоставляет основу для создания всевозможных компонентов пользовательского интерфейса (GUI-компонентов). Все конкретные
компоненты — кнопки, надписи, списки и т. д. — реализуются как субклассы
JComponent.
Мы переопределяем методы в субклассе, чтобы реализовать нужную логику
работы конкретного компонента. На первый взгляд кажется, что такой подход устанавливает излишние ограничения, так как мы получаем в свое распоряжение только заранее определенный набор функций. Но это впечатление
обманчиво. Учтите, что методы, о которых идет речь, предназначены только
для взаимодействия с оконной системой. В них не нужно втискивать весь код
приложения. Реальные приложения состоят из сотен и тысяч классов, каждый
из которых содержит множество собственных методов и переменных. Подавляющее большинство объектов в реальном приложении связано с его спецификой;
они называются объектами предметной области. А класс JComponent и другие
стандартные (предопределенные) классы Java служат основой только для тех
небольших фрагментов кода, которые обрабатывают события пользовательского
интерфейса и выводят информацию для пользователя.
Метод paintComponent() играет важную роль в классе JComponent; мы переопределяем этот метод, чтобы реализовать отображение нашего конкретного компонента на экране. По умолчанию paintComponent() не выводит ничего. Если бы мы
не переопределили его в субклассе, то наш компонент остался бы невидимым.
В данном случае мы переопределяем paintComponent(), чтобы он делал что-то
более интересное. Никакие другие унаследованные методы и поля JComponent
не переопределяются, потому что они предоставляют для нашего простого примера базовую функциональность и разумные значения по умолчанию. По мере
роста нашего приложения HelloJava мы лучше изучим унаследованные методы
и будем использовать дополнительные методы. Также мы добавим некоторые
методы и переменные, предназначенные специально для HelloComponent.
JComponent в действительности является лишь вершиной огромного айсберга под

названием Swing. Swing — это GUI-инструментарий Java, который мы включили
в наш пример командой import в начале кода. Мы подробнее рассмотрим Swing
в главе 10.

Отношения между классами
Класс HelloComponent можно рассматривать как JComponent, и это не будет
ошибкой, потому что при субклассировании создается отношение «является
частным случаем». Любой субкласс — это «частный случай» своего суперкласса,
а также любого из вышестоящих суперклассов. Далее мы поближе познакомимся

76  Глава 2. Первое приложение
с иерархией классов Java, а пока взгляните на рис. 2.13 — вы увидите, что класс
JComponent является субклассом класса Container, который, в свою очередь,
унаследован от Component и т. д.
В этом смысле наш HelloComponent — это «частный случай» класса JComponent,
а также класса Container, и каждый из них может считаться «частным случаем»
класса Component. От этих трех вышестоящих классов HelloComponent наследует
свою базовую функциональность в пользовательском интерфейсе и (как будет показано далее) возможность встраивания в него других графических компонентов.

Рис. 2.13. Часть иерархии классов Java
Component является субклассом класса Object верхнего уровня. Любой класс
является «частным случаем» Object. Все остальные классы Java API наследуют
часть своей логики работы от класса Object, который определяет несколько ба-

зовых методов, как будет показано в главе 5. В международной терминологии
словом «object» (с маленькой буквы) называют объект, то есть экземпляр любого
класса, а именем Object (с большой буквы) обозначают конкретный суперкласс,
самый верхний в иерархии Java.

Пакеты и импортирование
Ранее мы упоминали, что первая строка нашего примера сообщает Java, где
следует искать некоторые из используемых классов:
import javax.swing.*;

HelloJava  77

Точнее говоря, она сообщает компилятору, что мы собираемся использовать
классы из GUI-инструментария Swing (в нашем случае их три: JFrame, JLabel
и JComponent). Эти классы объединены в пакет Java с именем javax.swing. Пакет
Java — это группа классов, родственных по функциональности или области применения. Классы одного пакета имеют особые привилегии взаимного доступа
и часто разрабатываются с расчетом на тесное взаимодействие.
Имена пакетов строятся по иерархическому принципу. Их компоненты перечисляются через точку, например java.util или java.util.zip. Классы пакета должны составляться по правилам, позволяющим находить их по путям
в списке classpath. Классы получают имя пакета как часть своего полного,
или полностью уточненного, имени (fully qualified name). Например, полное
имя класса JComponent имеет вид javax.swing.JComponent. На класс можно
ссылаться напрямую по полному имени, и в этом случае оператор import не
требуется:
public class HelloComponent extends javax.swing.JComponent {...}

Строка import javax.swing.* в начале кода позволяет затем ссылаться на все
классы пакета javax.swing по простым именам. Таким образом, нам не придется использовать полные имена для обозначения классов JComponent, JLabel
и JFrame.
Как было показано при добавлении в код второго класса, исходный файл Java
может содержать одну или несколько строк с оператором import. Эти строки
создают «путь поиска», который сообщает компилятору Java, где искать классы,
когда вы обращаетесь к ним по простым именам. (На самом деле это не путь,
но он предотвращает появление неоднозначных имен, которые могут вызвать
ошибки.) Мы использовали специальную запись .* для обозначения того, что
импортируется весь пакет. Но при необходимости можно импортировать не весь
пакет, а только один класс. Например, в нашем примере из всего пакета java.awt
используется только класс Graphics. Следовательно, вместо импортирования
всех классов пакета AWT (Abstract Window Toolkit) символом * можно было
бы ограничиться одним классом: import java.awt.Graphics. Впрочем, далее нам
будут нужны и другие классы из пакета AWT.
Иерархии пакетов java. и javax. имеют особый смысл. Любой пакет, начинающийся с префикса java., является частью базового Java API и доступен на
любой платформе, поддерживающей Java. Имя пакета javax. обычно обозначает
стандартное расширение базовой платформы, которое может быть установлено
(или не установлено) в вашей системе. Однако за последние годы многие стандартные расширения были добавлены в базовый Java API без переименования.
Примером служит пакет javax.swing; несмотря на свое имя, он входит в состав
базового API. На рис. 2.14 показаны некоторые базовые пакеты Java с указанием
одного-двух характерных классов.

78  Глава 2. Первое приложение

Рис. 2.14. Некоторые базовые пакеты Java
Пакет java.lang содержит фундаментальные классы, необходимые самому языку Java. Этот пакет импортируется автоматически, поэтому нам не потребовался
оператор import для работы с такими классами, как String и System. Пакет java.
awt содержит классы традиционного графического инструментария AWT; пакет
java.net содержит классы для сетевых приложений и т. д.
По мере изучения Java вы будете все лучше понимать, как важно хорошо разбираться во всех доступных пакетах: что делает каждый из них, когда и как
им пользоваться. Это абсолютно необходимо для того, чтобы стать успешным
Java-разработчиком.

Метод paintComponent()
Исходный код класса HelloComponent определяет метод paintComponent(), переопределяющий метод paintComponent() класса JComponent:
public void paintComponent( Graphics g ) {
g.drawString( "Hello, Java!", 125, 95 );
}

Метод paintComponent() вызывается тогда, когда компонент должен вывести
себя на экран. Он получает один аргумент (объект Graphics) и не возвращает
вызывающей стороне никакого значения (void).

HelloJava2: продолжение  79

Модификаторы — это ключевые слова, которые ставятся перед именами классов, переменных и методов для изменения их доступности, логики работы или
смысла. Метод paintComponent() объявлен с модификатором public. Это озна­
чает, что он может вызываться методами других классов, а не только класса
HelloComponent. В данном случае метод paintComponent() вызывается оконной
средой Java. А методы и переменные, объявленные с модификатором private,
доступны только из того класса, которому они принадлежат.
Объект Graphics, то есть экземпляр класса Graphics, представляет конкретную
область графического вывода (также называемую графическим контекстом). Он
содержит методы, которые нужны для рисования в этой области, и переменные,
представляющие такие характеристики, как режимы отсечения и рисования.
Конкретный объект Graphics, передаваемый методу paintComponent(), соответствует той области экрана, которую HelloComponent занимает внутри JFrame.
Класс Graphics предоставляет методы для вывода геометрических фигур, изображений и текста. Метод drawString() объекта Graphics вызывается в классе
HelloComponent для вывода сообщения в области с заданными координатами.
Как мы уже показали, чтобы вызвать метод какого-либо объекта, надо после
имени объекта написать точку (.) и затем имя метода. Например, следующей
строкой мы вызываем метод drawString() объекта Graphics, на который ссыла­
ется наша переменная g:
g.drawString( "Hello, Java!", 125, 95 );

Возможно, вы не сразу привыкнете к мысли о том, что графическим выводом
в нашем приложении занимается метод, который вызывается откуда-то извне
(из оконной среды Java), да еще и в произвольные моменты времени. Как можно
сделать что-то полезное в таких условиях? Как контролировать, что и когда происходит? На эти вопросы мы скоро ответим. А пока просто подумайте, какую
структуру вы бы предложили для приложения, которое реагирует на команды
вместо того, чтобы рисовать по собственной инициативе.

HelloJava2: продолжение
Разобравшись с азами, добавим в наше приложение чуть больше интерактивности. Следующее маленькое обновление позволит перетаскивать надпись мышью.
Впрочем, начинающему программисту это обновление покажется не таким уж
маленьким. Не бойтесь! Все темы, представленные в этом примере, будут подробнее изложены в следующих главах. А пока поэкспериментируйте с нашим
очередным примером; он поможет вам увереннее создавать и запускать программы Java, даже если вы еще не до конца освоились с содержащимся в них кодом.

80  Глава 2. Первое приложение
Мы назовем этот пример HelloJava2, чтобы не создавать лишней путаницы в попытках переделать прежнюю версию. Основные изменения — это расширение
функциональности класса HelloComponent и его переименование с целью избежать конфликтов имен (HelloComponent2, HelloComponent3). Вы уже видели,
как работает наследование, и у вас может появиться вопрос: не лучше ли создать
субкласс класса HelloComponent и воспользоваться наследованием, чтобы написать новый пример на базе существующего и расширить его функциональность?
Нет, в данном случае это не принесло бы пользы, а для наглядности мы просто
напишем класс заново.
Приложение HelloJava2:
// Файл: HelloJava2.java
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
public class HelloJava2
{
public static void main( String[] args ) {
JFrame frame = new JFrame( "HelloJava2" );
frame.add( new HelloComponent2( "Hello, Java!" ) );
frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
frame.setSize( 300, 300 );
frame.setVisible( true );
}
}
class HelloComponent2 extends JComponent
implements MouseMotionListener
{
String theMessage;
int messageX = 125, messageY = 95; // Координаты сообщения
public HelloComponent2( String message ) {
theMessage = message;
addMouseMotionListener( this );
}
public void paintComponent( Graphics g ) {
g.drawString( theMessage, messageX, messageY );
}
public void mouseDragged( MouseEvent e ) {
// Сохранить координаты мыши и перерисовать текст сообщения
messageX = e.getX();
messageY = e.getY();
repaint();
}
}

public void mouseMoved( MouseEvent e ) { }

HelloJava2: продолжение  81

Две косые черты (//) означают, что вся последующая часть строки содержит
комментарий. Мы написали в HelloJava2 несколько комментариев, чтобы вам
было проще следить за происходящим.
Сохраните текст этого примера в файле с именем HelloJava2.java и скомпилируйте его так же, как прежде. В результате должны быть созданы два новых
файла классов: HelloJava2.class и HelloComponent2.class.
Запустите программу следующей командой:
C:\> java HelloJava2

Или, если вы вводите примеры в IDEA, нажмите кнопку Run. Замените сообщение «Hello, Java!» любым другим и наслаждайтесь, перетаскивая его по экрану.
Обратите внимание: при нажатии кнопки закрытия окна приложение завершается; этот факт будет объяснен далее, когда мы будем обсуждать события.
А теперь посмотрим, что же изменилось.

Переменные экземпляра
В класс HelloComponent2 были добавлены некоторые переменные:
int messageX = 125, messageY = 95;
String theMessage;

Переменные messageX и messageY — это целые числа, определяющие текущие
координаты перемещаемой надписи. Мы инициализировали их значениями
по умолчанию, с которыми надпись будет выводиться недалеко от центра окна.
Целочисленные переменные в Java (int) представляют 32-разрядные числа
со знаком, поэтому в них удобно хранить значения координат. Переменная
theMessage относится к типу String и может хранить экземпляры класса String,
то есть текст.
Обратите внимание: эти три переменные объявляются в фигурных скобках
в определении класса, но не внутри какого-либо конкретного метода в этом
классе. Такие переменные называются переменными экземпляра и принадлежат всему объекту в целом. Копии таких переменных присутствуют в каждом
отдельном экземпляре класса. Переменные экземпляра всегда видны и могут
использоваться методами внутри своего класса. В зависимости от своих модификаторов они также могут быть доступны за пределами класса.
Если переменные экземпляра не инициализируются явно, то по умолчанию им
присваивается значение 0, false или null в зависимости от типа переменной.
Числовые типы инициализируются значением 0, логические — значением false,
а переменным с типом класса в таких случаях присваивается значение null,

82  Глава 2. Первое приложение
­ значающее «нет никакого значения». Попытка использовать объект со значео
нием null приводит к ошибке времени выполнения (runtime error).
Переменные экземпляра отличаются от аргументов методов и от других переменных, объявляемых в области видимости конкретного метода. Последние называются локальными переменными. Фактически это скрытые переменные, которые
видны только своему методу (или своему блоку кода). Java не инициализирует
локальные переменные, поэтому вам надо их явно инициализировать. При попытке обращения к локальной переменной, которой еще не присвоено значение,
происходит ошибка компиляции (compile-time error). Локальные переменные
продолжают существовать во время выполнения метода, а затем пропадают (но
перед этим их значения можно где-то сохранить). При каждом вызове метода его
локальные переменные создаются заново, и им должны быть присвоены значения.
Мы использовали новые переменные, чтобы сделать слишком банальный метод
paintComponent() более динамичным. Теперь все аргументы вызова drawString()
определяются этими переменными.

Конструкторы
Класс HelloComponent2 включает особый метод, называемый конструктором.
Конструктор вызывается для создания нового экземпляра класса. При создании
нового объекта Java выделяет для него память, присваивает переменным экземпляра значения по умолчанию и вызывает метод — конструктор класса, выполняющий все необходимые подготовительные действия на уровне приложения.
Имя конструктора всегда совпадает с именем класса. Например, конструктор
класса HelloComponent2 называется HelloComponent2(). Конструктор не имеет
возвращаемого значения, и вы можете исходить из того, что он просто создает
объект с типом своего класса. Как и другие методы, конструкторы могут получать аргументы. Единственный смысл их существования — настройка и инициализация «новорожденных» экземпляров класса, иногда с использованием
информации, переданной в параметрах.
Объект создается оператором new с указанием конструктора класса и всех необходимых аргументов. Полученный экземпляр объекта возвращается как значение. В нашем примере новый экземпляр HelloComponent2 создается в методе
main() следующей строкой:
frame.add( new HelloComponent2( "Hello, Java!" ) );

На самом деле в этой строке выполняются две операции. Можно записать их
в виде двух отдельных строк, чтобы они стали более понятными:
HelloComponent2 newObject = new HelloComponent2( "Hello, Java!" );
frame.add( newObject );

HelloJava2: продолжение  83

Особенно важна первая строка, в которой создается новый объект HelloComponent2.
Конструктор HelloComponent2 получает в аргументе объект String и, как мы запрограммировали, использует его для настройки сообщения, выводимого в окне.
Благодаря небольшой помощи со стороны компилятора Java, заключенный в кавычки текст в исходном коде Java преобразуется в объект String. (Класс String
рассматривается в главе 8.) Вторая строка просто добавляет наш новый компонент
в окно, чтобы сделать его видимым, как это делалось в предыдущих примерах.
Если вы хотите, чтобы сообщение можно было задать при запуске программы,
замените строку конструктора следующей:
HelloComponent2 newobj = new HelloComponent2( args[0] );

Теперь текст сообщения можно передать в командной строке при запуске приложения следующей командой:
C:\> java HelloJava2 "Hello, Java!"

args[0] обозначает первый параметр командной строки. Смысл этой конструк-

ции станет более понятным при рассмотрении массивов в главе 4. Если вы работаете в интегрированной среде разработки, то перед запуском приложения ее
надо настроить, указав параметры командной строки, как показано для IntelliJ
IDEA на рис. 2.15.

Рис. 2.15. Диалоговое окно IDEA для передачи параметров командной строки

84  Глава 2. Первое приложение
Затем конструктор HelloComponent2 решает две задачи: он задает текст переменной экземпляра theMessage и вызывает метод addMouseMotionListener().
Этот метод является частью механизма событий, о котором будет расска­
зано ниже. Он сообщаетсистеме: «Меня интересует все, что происходит
с мышью».
public HelloComponent2( String message ) {
theMessage = message;
addMouseMotionListener( this );
}

Специальная, доступная только для чтения переменная this используется
для явных обращений к объекту (к контексту «этого» объекта) при вызове
addMouseMotionListener(). Метод может использовать this для обращения
к экземпляру объекта, которому он принадлежит. Таким образом, следующие
команды эквивалентны — обе присваивают значение переменной экземпляра
theMessage:
theMessage = message;

или:
this.theMessage = message;

Обычно для ссылок на переменные экземпляра используется более короткая,
неявная форма, но ключевое слово this нам еще пригодится, когда потребуется
явно передать ссылку на текущий объект методу из другого класса. Обычно это
делается для того, чтобы методы других классов вызывали наши общедоступные
методы или использовали открытые переменные.

События
В классе HelloComponent2 есть еще два метода: mouseDragged() и mouseMoved().
Они нужны для получения информации от мыши. Каждый раз, когда пользователь выполняет некоторое действие (например, нажимает клавишу на клавиатуре, перемещает мышь или касается сенсорного экрана), Java генерирует
событие. Событие представляет выполненное действие; оно содержит информацию о действии (например, его время и координаты). Большинство событий
связано с конкретными GUI-компонентами в приложении. Например, нажатие
клавиши может соответствовать вводу символа в конкретном текстовом поле,
а щелчок кнопкой мыши может активизировать конкретную кнопку на экране.
Даже простое перемещение указателя мыши в определенную область экрана
может активизировать такие эффекты, как цветовое выделение или изменение
формы указателя.

HelloJava2: продолжение  85

Чтобы работать с этими событиями, мы импортировали новый пакет java.
awt.event. Он предоставляет определенные классы Event, необходимые для
получения информации от пользователя. Обратите внимание: импортирование пакета java.awt.* не приводит к автоматическому импортированию
пакета event. Импортирование не является рекурсивным. Пакеты не содержат
другие пакеты, несмотря на то что иерархическая схема имен создает такое
впечатление.
Существует много разных классов событий, включая MouseEvent , KeyEvent
и ActionEvent. В основном их смысл вполне очевиден. Событие MouseEvent
происходит тогда, когда пользователь что-то делает с мышью; событие KeyEvent
происходит при нажатии клавиши и т. д. Событие ActionEvent немного особенное; вы увидите его в действии в главе 10. А пока сосредоточимся на обработке
событий MouseEvent.
GUI-компоненты в Java генерируют события для конкретных видов действий пользователя. Например, если нажать кнопку мыши внутри компонента, этот компонент сгенерирует событие мыши. Объекты могут запрашивать
получение событий от одного или нескольких компонентов, регистрируя
слушатель для нужного источника событий. Например, чтобы объявить, что
слушатель хочет получать события перемещения мыши, надо вызвать метод
addMouseMotionListener() соответствующего компонента, передав этому методу
объект слушателя в аргументе. Именно это происходит в конструкторе нашего
класса: компонент вызывает собственный метод addMouseMotionListener()
с аргументом this, что означает «Я хочу получать относящиеся ко мне события
перемещения мыши».
Так мы регистрируемся для получения событий. Но как мы их получаем? Для
этого используются два метода нашего класса, относящиеся к мыши. Метод
mouseDragged() автоматически вызывается для слушателя, чтобы получать события, генерируемые при перетаскивании, то есть при перемещении мыши с любой
нажатой кнопкой. Метод mouseMoved() вызывается каждый раз, когда пользователь перемещает мышь внутри заданной области без нажатия кнопки. В нашем
примере мы разместили эти методы в классе HelloComponent2 и заставили его
зарегистрировать себя как слушатель. Это вполне подходит для нашего простого
компонента перетаскивания текста. Но в хорошем стиле программирования
слушатели событий реализуются в виде классов-адаптеров, обеспечивающих
наглядное отделение графического интерфейса от «бизнес-логики». Эту тему
мы рассмотрим в главе 10.
Наш метод mouseMoved() тривиален: он не делает ничего. Мы игнорируем простые перемещения мыши и уделяем все внимание перетаскиванию. Метод
mouseDragged() более содержателен. Он многократно вызывается оконной
системой для передачи обновляющейся информации о положении указателя
мыши. Вот как он выглядит:

86  Глава 2. Первое приложение
public void mouseDragged( MouseEvent e ) {
messageX = e.getX();
messageY = e.getY();
repaint();
}

В первом аргументе метода mouseDragged() ему передается объект MouseEvent
с именем e; он содержит всю необходимую информацию о событии. Мы запрашиваем у объекта MouseEvent координаты x и y для текущей позиции мыши,
вызывая его методы getX() и getY(). Координаты сохраняются в переменных
экземпляра messageX и messageY для последующего использования.
Элегантность модели событий заключается в том, что вы обрабатываете только
те виды событий, которые вас интересуют. Если вам не нужны события клавиатуры, вы не регистрируете для них слушатель. В этом случае пользователь может
вводить что угодно, но вас это нисколько не побеспокоит. Если слушатели для
конкретного вида событий отсутствуют, Java даже не генерирует это событие.
В результате обработка событий выполняется очень эффективно1.
Пока мы обсуждаем события, следует упомянуть другое небольшое дополнение
в HelloJava2:
frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );

Так мы сообщаем объекту JFrame, что при щелчке на кнопке закрытия окна надо
завершить приложение. Это называется операцией по умолчанию, потому что
эта операция, как и почти все остальные взаимодействия с графическим интерфейсом, управляется событиями. Мы могли бы зарегистрировать слушатель
для окна, чтобы получать уведомления о щелчках на кнопке закрытия и затем
выполнять любые действия по своему усмотрению, но этот удобный метод обрабатывает самые типичные случаи.
Мы обошли вниманием пару вопросов: как система узнает, что наш класс содержит необходимые методы mouseDragged() и mouseMoved()? (Откуда взялись
эти имена?) И зачем нужен метод mouseMoved(), который ничего не делает?
Ответы на эти вопросы связаны с интерфейсами. Тему интерфейсов мы скоро
рассмотрим, а пока разберемся с тем, какова роль метода repaint().

1

С обработкой событий в Java 1.0 дело обстояло совершенно иначе. Тогда в Java еще
не было концепции слушателей, а вся обработка событий осуществлялась переопределением методов в базовых классах GUI. Это было неэффективно, а код усложнялся
из-за размножения компонентов крайне узкого назначения.

HelloJava2: продолжение  87

Метод repaint()
После изменения координат надписи (в результате перетаскивания) компонент
HelloComponent2 должен перерисовать себя. Для этого мы вызываем метод
repaint(), который сообщает системе, что через некоторое время она должна
будет перерисовать изображение в окне. Мы не можем напрямую вызвать метод
paintComponent(), даже если бы захотели, потому что у нас нет графического
контекста для передачи этому методу.
Метод repaint() класса JComponent обеспечивает перерисовку компонента.
Он заставляет оконную систему Java запланировать вызов нашего метода
paintComponent() в ближайший возможный момент; при этом Java предоставляет
необходимый объект Graphics (рис. 2.16).
Среда Java

HelloJava2
Запрос
Графический
контекст

Рис. 2.16. Вызов метода repaint()
Не надо считать такой режим выполнения операции неудобством, вызванным
отсутствием подходящего графического контекста. Главное преимущество заключается в том, что процессом перерисовки занимается кто-то другой, пока
мы можем свободно делать что-то другое. Система Java содержит отдельный
специализированный поток, который обрабатывает все запросы repaint().
Она может планировать и консолидировать запросы repaint() по мере необходимости, что позволяет избежать перегрузки оконной системы в ситуациях
с интенсивной перерисовкой (например, при прокрутке). Есть и другое преимущество: вся функциональность перерисовки инкапсулируется в методе
paintComponent(); по крайней мере, у вас не будет соблазна распределить ее
по всему приложению.

Интерфейсы
А теперь пора ответить на вопрос, который был обойден ранее: как система узнает, что при возникновении события мыши надо вызвать метод mouseDragged()?
Может быть, mouseDragged() — какое-то специальное имя, которое должно присваиваться методу обработки события? Нет, это не так. Ответ на вопрос требует
рассмотрения интерфейсов — одной из самых важных концепций языка Java.

88  Глава 2. Первое приложение
Первый признак использования интерфейса встречается в строке кода, представляющей класс HelloComponent2. Там мы указываем, что этот класс реализует
интерфейс MouseMotionListener:
class HelloComponent2 extends JComponent
implements MouseMotionListener
{

По сути интерфейс — это список методов, которые должен содержать класс.
Этот конкретный интерфейс требует, чтобы в классе были методы с именами
mouseDragged() и mouseMoved(). Интерфейс ничего не говорит о том, что должны
делать эти методы (например, mouseMoved() не делает ничего). Он требует, чтобы
методы получали MouseEvent в аргументе и не возвращали никаких значений
(напомним, отсутствие значения — это void).
Иными словами, интерфейс — это своего рода «контракт» между программистом и компилятором. Указывая, что ваш класс реализует интерфейс
MouseMotionListener, вы тем самым говорите, что эти методы будут доступны
для вызова из других частей системы. Если вы не предоставите эти методы,
произойдет ошибка компиляции.
Влияние интерфейса на программу этим не ограничивается. Интерфейс также
действует как класс. Например, метод может вернуть объект MouseMotionListener
или получить MouseMotionListener в аргументе. Когда вы ссылаетесь на объект
по имени интерфейса, это означает, что реальный класс объекта вас не интересует; требуется только одно — чтобы класс реализовал этот интерфейс. Примером
служит метод addMouseMotionListener(): его аргументом должен быть объект,
реализующий интерфейс MouseMotionListener . Передаваемым аргументом
является this, то есть сам объект HelloComponent2. Тот факт, что он является
экземпляром класса JComponent, роли не играет; это может быть Cookie, Aardvark
или любой другой класс, который вы придумаете. Важно лишь то, что он реализует MouseMotionListener, а следовательно, объявляет, что он содержит два
метода с заданными именами. Вот почему метод mouseMoved() необходим: он
ничего не делает, но интерфейс MouseMotionListener требует, чтобы этот метод
присутствовал.
В Java есть много интерфейсов, определяющих, что должны делать классы.
Идея «контракта» между компилятором и классом очень важна. Есть много
ситуаций вроде описанной выше, когда вас не интересует, к какому классу
относится объект; вам важно лишь то, что он обладает некоторой функциональностью, например прослушиванием событий мыши. Интерфейсы позволяют работать с объектами на уровне их функциональности, не зная их
фактических типов. Концепция интерфейсов чрезвычайно важна для Java как
объектно-ориентированного языка. Мы подробно рассмотрим интерфейсы
в главе 5.

До свидания… и снова здравствуйте!  89

Из главы 5 вы также узнаете, что интерфейсы предоставляют лазейку для
обхода правила Java, согласно которому любой новый класс может расширять
только один родительский класс («одиночное наследование»). Да, класс в Java
может расширять только один класс, но зато может реализовать сколько угодно
интерфейсов. Интерфейсы могут использоваться как типы данных, могут реализовывать другие интерфейсы (но не классы) и могут наследоваться классами
(если класс A реализует интерфейс B, то субклассы A также реализуют B).
Принципиальное отличие заключается в том, что классы не наследуют методы
из интерфейсов; интерфейс всего лишь определяет, какие методы должен содержать класс.

До свидания… и снова здравствуйте!
Пришло время попрощаться с приложением HelloJava. Надеемся, вы получили
представление о некоторых возможностях языка и об основных правилах написания и запуска программ. Это краткое введение помогло вам разобраться
в том, как программировать на Java. Даже если вы что-то не поняли — крепитесь.
Все основные темы, упомянутые в этой главе, будут подробно рассмотрены
в последующих главах. А пока вы прошли «боевое крещение» и познакомились
с важнейшими концепциями и терминами, которые сразу узнаете при следующей встрече.
Мы еще вернемся к HelloJava, а прямо сейчас расскажем о наборе инструментов,
с которыми работают программисты. В главе 3 подробно описаны инструменты,
которые вы уже немного знаете (например, javac), а также другие важные утилиты. Приготовьтесь, вас ждет встреча с лучшими друзьями Java-разработчика!

ГЛАВА 3

Рабочие инструменты

Почти наверняка вы будете разрабатывать Java-приложения в среде IDE: это
может быть Eclipse, VS Code или предпочитаемая авторами книги IntelliJ
IDEA. Тем не менее все основные инструменты, необходимые для создания приложений Java, включены в пакет JDK, который вы, скорее всего, уже загрузили
с сайта Oracle (см. раздел «Установка JDK», с. 54)1. В этой главе мы рассмотрим
базовые средства командной строки, которые нужны для компиляции, запуска
и упаковки приложений Java. В составе JDK есть и многие другие средства разработчика, о которых мы расскажем впоследствии.
За дополнительной информацией об IntelliJ IDEA и за инструкциями по загрузке всех примеров книги в виде проекта обращайтесь к приложению, с. 497.

Среда JDK
После установки Java главная команда java может появиться в пути запуска
автоматически. Но многие другие команды, входящие в JDK, могут быть недоступными, если вы не включите каталог Java bin в свой путь запуска. Ниже
показано, как сделать это в Linux, macOS и Windows. Конечно, эти примеры надо
скорректировать, указав точный путь к Java на вашем компьютере.
Для Linux:
export JAVA_HOME=/usr/lib/jvm/java-12-openjdk-amd64
export PATH=$PATH:$JAVA_HOME/bin

Для macOS:
export JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk-12.jdk/Contents/Home
export PATH=$PATH:$JAVA_HOME/bin

1

Кроме Oracle, есть и другие поставщики JDK; при желании вы можете найти их в интернете и сравнить разные дистрибутивы.

Запуск приложений Java  91

Для Windows:
set JAVA_HOME=c:\Program Files\Java\jdk12
set PATH=%PATH%;%JAVA_HOME%\bin

В macOS ситуация может быть более запутанной, потому что в последних версиях macOS есть встроенные «заглушки» для команд Java. При попытке выполнить
любую из этих команд операционная система предложит вам загрузить Java.
Вы можете заранее загрузить OpenJDK с сайта Oracle; инструкции приведены
в разделе «Инструменты и среда Java», с. 53.
Если вы сомневаетесь в том, какая версия Java установлена на компьютере, выполните команды java и javac c флагом -version:
java -version
# openjdk version "12" 2019-03-19
# OpenJDK Runtime Environment (build 12+33)
# OpenJDK 64-Bit Server VM (build 12+33, mixed mode, sharing)
javac -version
# javac 12

Виртуальная машина Java
Виртуальная машина (VM) Java — это программа, которая реализует исполнительную систему (интерпретатор) Java и выполняет Java-приложения. Это
может быть автономная программа (например, команда java из JDK) или код,
встроенный в другое приложение, такое как браузер. Чаще всего интерпретатор
представляет собой платформенное приложение, версии которого поставляются для каждой конкретной платформы; он запускает другие приложения,
написанные на Java. Многие компиляторы Java и IDE написаны именно на Java
для максимальной портируемости и расширяемости. Например, IDE Eclipse
является чистым Java-приложением.
Виртуальная машина (VM) отвечает за все, что происходит при работе Javaприложений. Она загружает файлы классов Java, проверяет классы из ненадежных источников и выполняет скомпилированный байт-код. Она управляет
памятью и системными ресурсами. Кроме того, лучшие реализации VM выполняют динамическую оптимизацию, компилируя байт-код Java в платформенный
машинный код компьютера.

Запуск приложений Java
В каждом отдельном приложении Java должен быть стартовый класс, содержащий метод с именем main(); код этого метода выполняется сразу после запуска.

92  Глава 3. Рабочие инструменты
Запустите виртуальную машину и передайте ей имя этого класса в командной
строке, чтобы запустить приложение. Также в командной строке можно указать системные параметры для интерпретатора и передаваемые приложению
аргументы:
$ java [параметры для интерпретатора] имя_класса [аргументы]

Класс должен быть указан с полным именем, включающим имя пакета (если
оно есть). Расширение файла .class в имя не включается. Несколько примеров:
$ java animals.birds.BigBird
$ java MyTest

Интерпретатор ищет стартовый класс в classpath, то есть в списке каталогов
и архивных файлов, в которых хранятся классы. Список classpath будет подробно рассмотрен в следующем разделе. Его можно задать либо в переменной среды,
либо в параметре командной строки -classpath. Если заданы оба варианта, то
используется параметр командной строки.
Также команда java может запускать приложения из исполняемых архивов Java,
имеющих формат JAR:
$ java -jar spaceblaster.jar

В таких случаях JAR-файл должен содержать метаданные, в которых указано имя
стартового класса с методом main(), а в качестве classpath служит сам JAR-файл.
После загрузки стартового класса и выполнения его метода main() приложение
может ссылаться на другие классы, запускать дополнительные потоки и создавать пользовательский интерфейс и другие структуры, как показано на рис. 3.1.

Рис. 3.1. Запуск приложения Java
Метод main() должен иметь правильную сигнатуру метода. Сигнатура метода — это набор данных, определяющих метод. Она включает имя метода,
аргументы и возвращаемый тип, а также модификаторы типа и видимости.

Запуск приложений Java  93

Метод main() должен быть открытым (public) и статическим (static); он
должен получать в аргументе массив объектов String и не возвращать никакого значения (void):
public static void main ( String[] myArgs )

Тот факт, что метод main() является открытым и статическим, означает лишь
то, что он доступен глобально и может напрямую вызываться по имени. Смысл
модификаторов видимости (таких, как public) и модификатора static мы объясним в главах 4 и 5.
Единственный аргумент метода main() — массив объектов String. Он содержит
аргументы командной строки, переданные приложению. Имя этого параметра
ни на что не влияет; важен только тип. В Java содержимым myArgs является
массив (подробнее о массивах — в главе 4). В Java массивам известно, сколько
элементов они содержат, и они могут предоставить эту информацию:
int numArgs = myArgs.length;

myArgs[0] — первый аргумент командной строки (и т. д.).

Интерпретатор Java продолжает работать, пока метод main() исходного файла
класса не вернет управление и пока не будут завершены все запущенные им потоки. (Подробнее о потоках — в главе 9.) Специальные потоки, определенные как
потоки-демоны, завершаются автоматически — при завершении всех остальных
частей приложения.

Системные параметры
Хотя из кода Java можно читать системные переменные среды, они плохо подходят для настройки конфигурации приложения. Вместо этого Java позволяет
передать приложению значения произвольных системных параметров при
запуске виртуальной машины. Системные параметры — это простые пары
строк «имя — значение», которые приложение может запросить, вызвав статический метод System.getProperty(). Используйте системные параметры как
более структурированную и портируемую альтернативу аргументам командной
строки и переменным среды, когда надо передать приложению при запуске
основную информацию о конфигурации. Каждый системный параметр передается интерпретатору в командной строке с ключом -D, за которым следует
пара имя=значение. Пример:
$ java -Dstreet=sesame -Dscene=alley animals.birds.BigBird

Затем к значению свойства street можно обратиться следующим образом:
String street = System.getProperty("street");

94  Глава 3. Рабочие инструменты
Приложение может получить параметры своей конфигурации множеством
других способов, в том числе прочитать из файлов или получить по сети во
время выполнения.

Classpath
Путь (path) — это понятие, знакомое каждому, кто работал в DOS или Unix.
Так называется переменная среды, предоставляющая приложениям список
мест для поиска некоторого ресурса. Самый распространенный пример — путь
к исполняемому файлу. В Unix переменная среды PATH содержит список каталогов, в которых последовательно производится поиск при вводе пользователем
коман­ды. Похожая переменная среды Java CLASSPATH содержит список каталогов
для поиска пакетов и классов Java. Как интерпретатор, так и компилятор Java
используют CLASSPATH.
В списке classpath могут быть указаны каталоги и JAR-файлы. Java также поддерживает архивы в традиционном формате ZIP, но JAR и ZIP в действительности имеют одинаковый формат. JAR-файлы — это обычные архивы, в которых
есть служебные файлы (метаданные) с описанием содержимого архивов. JARфайлы создаются утилитой jar из JDK. Есть много общедоступных программ
для создания ZIP-архивов, с помощью которых тоже можно создавать и просматривать JAR-файлы. Формат архива позволяет распространять большие группы
классов и их ресурсов в одном файле. Исполнительная система Java автоматически извлекает отдельные файлы классов из архива по мере необходимости.
Формат classpath зависит от конкретной операционной системы. В Unixподобных системах (включая macOS) задается переменная среды CLASSPATH,
которая содержит список каталогов и архивных файлов классов, разделенных
двоеточиями:
$ export CLASSPATH=/home/vicky/Java/classes:/home/josh/lib/foo.jar:.

В этом примере задается список classpath, состоящий из трех путей: к каталогу
в домашнем каталоге пользователя, к JAR-файлу в каталоге другого пользователя
и к текущему каталогу, который всегда обозначается точкой (.). Последний компонент classpath (текущий каталог) пригодится при экспериментах с классами.
В Windows переменная среды CLASSPATH содержит список каталогов и архивных
файлов классов, разделенных символами «точка с запятой» (;):
C:\> set CLASSPATH=C:\home\vicky\Java\classes;C:\home\josh\lib\foo.jar;.

Программа запуска Java и другие утилиты командной строки знают, где искать
фундаментальные классы (классы, включаемые в каждую реализацию Java).

Classpath  95

Например, классы пакетов java.lang, java.io, java.net и javax.swing являются
фундаментальными и включать их в classpath не нужно.
Пути в списке classpath могут содержать групповые символы *, обозначающие
все JAR-файлы в каталоге. Пример:
$ export CLASSPATH=/home/pat/libs/*

Чтобы найти другие классы, кроме фундаментальных, интерпретатор Java проводит поиск по элементам списка classpath в порядке их следования. При поиске
каждый путь объединяется с полным именем класса. Для примера посмотрим,
как выполняется поиск класса animals.birds.BigBird. Поиск в каталоге /usr/
lib/java из classpath означает, что интерпретатор ищет файл /usr/lib/java/
animals/birds/BigBird.class. Поиск в JAR-архиве /home/vicky/myutils.jar из
classpath (или в аналогичном ZIP-архиве) означает, что интерпретатор ищет
в этом архиве файл animals/birds/BigBird.class.
Для исполнительной системы java и компилятора javac список classpath можно
задать параметром -classpath в командной строке:
$ javac -classpath /home/pat/classes:/utils/utils.jar:. Foo.java

Если не задать ни переменную среды CLASSPATH, ни параметр командной строки,
то по умолчанию в качестве classpath используется текущий каталог (.); это
означает, что исполнительной системе Java будут доступны файлы в текущем
каталоге. Но если вы составите classpath и не включите в него текущий каталог,
то эти файлы окажутся недоступными.
Подозреваем, что около 80% всех проблем, с которыми сталкиваются новички при
изучении Java, связаны с classpath. Возможно, вам надо уделить особое внимание
составлению и проверке classpath прямо сейчас. Если вы работаете в IDE, то эта
среда может избавить вас (частично или полностью) от хлопот с classpath. Однако
в перспективе вам надо будет хорошо понимать, какие пути есть в списке classpath
и каково содержимое соответствующих каталогов и архивов во время работы
ваших приложений. Выявлять проблемы с classpath вам поможет команда javap.

javap
Еще один полезный инструмент, о котором вам надо знать, — это команда javap.
Она выводит на экран описание скомпилированного класса. Для этого не нужен
исходный код класса и даже не нужно знать его точное местонахождение — достаточно, чтобы он входил в classpath. Например, следующая команда выводит
информацию о классе java.util.Stack:
$ javap java.util.Stack

96  Глава 3. Рабочие инструменты
Результат вывода будет таким:
Compiled from "Stack.java"
public class java.util.Stack extends java.util.Vector {
public java.util.Stack();
public E push(E);
public synchronized E pop();
public synchronized E peek();
public boolean empty();
public synchronized int search(java.lang.Object);
}

Такая информация окажется очень полезной, если у вас нет другой документации; также она пригодится при отладке проблем с classpath. При помощи
javap всегда можно определить, входит ли класс в classpath, и даже проверить его версию (многие проблемы возникают из-за дублирования классов
в classpath). По-настоящему любознательные читатели могут запустить javap
с параметром -c, чтобы команда также вывела байт-код виртуальной машины
для каждого метода в классе.

Модули
В Java 9 вместо традиционного списка classpath (который по-прежнему
поддерживается) можно воспользоваться новым решением, основанным на
модулях. Модули позволяют выполнять более детализированное и эффективное развертывание приложений, даже очень больших. Этот вариант требует
дополнительной настройки, поэтому в книге он не рассматривается, но важно
знать, что почти все большие коммерческие приложения имеют модульную
структуру. Ценную информацию и помощь в модульном структурировании
больших проектов вы найдете в книге Пола Баккера (Paul Bakker) и Сандера
Мака (Sander Mak) «Java 9 Modularity» (https://www.oreilly.com/library/view/java9-modularity/9781491954157) — она будет полезной, если вы хотите распространять
свои работы, не ограничиваясь простой отправкой исходного кода в общедоступные репозитории.

Компилятор Java
В этом разделе мы скажем несколько слов о программе javac, которая является
компилятором Java из JDK. Эта программа полностью написана на Java, поэтому доступна на любой платформе с поддержкой исполнительной системы
Java. Компилятор преобразует исходный код Java в скомпилированные классы,
состоящие из байт-кода Java. Исходные файлы должны иметь расширение .java,
а скомпилированные файлы классов — расширение .class. Каждый файл с ис-

Компилятор Java  97

ходным кодом считается отдельной единицей компиляции. Как будет показано
в главе 5, все классы заданной единицы компиляции обладают рядом сходных
признаков, например, относящихся к операторам package и import.
Компилятор javac разрешает использовать один открытый (public) класс
в каждом файле и требует, чтобы имя этого файла совпадало с именем класса.
Если имена файла и класса не совпадают, то javac выдает ошибку компиляции.
Один файл может содержать несколько классов при условии, что только один
из них объявлен открытым, а его имя совпадает с именем файла. Старайтесь не
упаковывать слишком много классов в один исходный файл. Упаковка классов
в файле .java только создает иллюзию связи между ними. В главе 5 рассказано о внутренних классах (inner classes) — таких, которые содержатся в других
классах и интерфейсах.
Например, включите следующий фрагмент кода в файл с именем BigBird.java:
package animals.birds;
public class BigBird extends Bird {
...
}

Скомпилируйте его следующей командой:
$ javac BigBird.java

В отличие от интерпретатора Java, который получает в аргументе имя класса,
javac должен получить в аргумента имя файла (с расширением .java), который
надо скомпилировать. Эта команда создает файл BigBird.class в одном каталоге
с исходным файлом. Хотя в данном примере удобно создать файл класса в одном
каталоге с исходным файлом, в реальных приложениях файл класса обычно
должен храниться в более подходящем месте из classpath.
Вы можете запускать javac с параметром -d, чтобы указывать определенный
каталог для хранения файлов тех классов, которые генерирует javac. Указанный
каталог используется в качестве корневого в иерархии классов, поэтому файлы
.class помещаются в этот каталог или в один из его подкаталогов в зависимости
от того, содержится ли класс в пакете. При необходимости компилятор создает
промежуточные каталоги автоматически. Например, следующая команда создает
файл /home/vicky/Java/classes/animals/birds/BigBird.class:
$ javac -d /home/vicky/Java/classes BigBird.java

В одной команде javac можно указать несколько файлов .java; в этом случае
компилятор создает файл класса для каждого исходного файла. Однако вам
не надо перечислять другие классы, на которые ссылается ваш класс, если они
находятся в classpath в форме исходного кода или в скомпилированном виде.

98  Глава 3. Рабочие инструменты
Во время компиляции класса Java обрабатывает все ссылки на другие классы,
основываясь на classpath.
Компилятор Java «умнее» типичных компиляторов: он заменяет часть функцио­
нальности утилиты make. Например, javac всегда сравнивает время изменения
исходного файла и существующего файла класса (если таковой есть), а затем
при необходимости перекомпилирует исходный файл. Скомпилированный
класс Java хранит информацию об исходном файле, из которого он был скомпилирован, и пока исходный файл остается доступным, компилятор может
перекомпилировать его при необходимости. Если класс BigBird из предыдущего
примера ссылается на другой класс (например, animals.furry.Grover), то javac
ищет исходный файл Grover.java в пакете animals.furry и при необходимости
его перекомпилирует, чтобы класс Grover.class оставался актуальным.
Однако по умолчанию javac проверяет только те исходные файлы, на которые
напрямую ссылаются другие исходные файлы. Таким образом, если у вас есть
устаревший файл класса, на который ссылается только актуальный файл класса (но не исходный код), он не будет обнаружен и перекомпилирован. Из-за
этого (и по многим другим причинам) в большинстве крупных проектов для
управления сборкой, упаковкой и другими подобными задачами используются
полноценные утилиты сборки, например Gradle.
Наконец, важно заметить, что javac может скомпилировать приложение даже
в том случае, если некоторые его классы доступны только в скомпилированных
(двоичных) версиях. Наличие исходного кода всех объектов не обязательно.
Файлы классов Java, как и исходные файлы, содержат всю информацию о типах
данных и сигнатуры всех методов, поэтому компиляция с двоичными файлами
классов настолько же безопасна по отношению к типам и исключениям, как
и компиляция с исходным кодом.

Первые эксперименты с Java
В Java 9 появилась утилита jshell, которая позволяет опробовать фрагменты
кода Java и немедленно увидеть результаты. jshell является оболочкой REPL
(Read-Evaluate-Print Loop, то есть «цикл чтение — вычисление — вывод»). Такие
оболочки есть во многих языках, и до выхода Java 9 появилось много сторонних
разработок, но встроенной программы в JDK тогда не было. Мы уже упоминали
о jshell; теперь познакомимся поближе с возможностями этой программы.
Откройте окно терминала или командной строки вашей операционной системы
либо откройте вкладку терминала в IntelliJ IDEA, как показано на рис. 3.2. Введите команду jshell в командной строке. Вы получите информацию о версии
и краткое напоминание о том, как пользоваться справкой из REPL.

Первые эксперименты с Java  99

Рис. 3.2. Запуск jshell из IDEA
Попробуйте выполнить команду /help intro для вывода справки:
| Welcome to JShell -- Version 12
| For an introduction type: /help intro
jshell> /help intro
|
|
intro
|
=====
|
| The jshell tool allows you to execute Java code, getting immediate results.
| You can enter a Java definition (variable, method, class, etc),
| like: int x = 8
| or a Java expression, like: x + x
| or a Java statement or import.
| These little chunks of Java code are called 'snippets'.
|
| There are also the jshell tool commands that allow you to understand and
| control what you are doing, like: /list
|
| For a list of commands: /help

100  Глава 3. Рабочие инструменты
jshell — это мощный инструмент, и для целей этой книги нам не понадобятся

все его возможности. Но в последующих главах он будет нужен, чтобы пробовать
фрагменты кода Java и быстро вносить изменения по ходу дела. Вспомните пример HelloJava2 из раздела «HelloJava2: продолжение», с. 79. Вы можете создать
элементы пользовательского интерфейса (такие, как JFrame) прямо в REPL, а потом работать с ними — и немедленно получать обратную связь! Вам не придется
сохранять, компилировать, запускать, редактировать, сохранять, компилировать,
запускать… Давайте попробуем:
jshell> JFrame frame = new JFrame( "HelloJava2" )
| Error:
| cannot find symbol
|
symbol:
class JFrame
| JFrame frame = new JFrame( "HelloJava2" );
| ^----^
| Error:
| cannot find symbol
|
symbol:
class JFrame
| JFrame frame = new JFrame( "HelloJava2" );
|
^----^

Не получилось! Оболочка jshell хорошо сделана и обладает широкой функциональностью, но она все понимает буквально. Помните: если вы хотите
использовать класс, не включенный в пакет по умолчанию, то вам надо его импортировать. Это относится не только к исходным файлам Java, но и к работе
с jshell. Попробуем еще раз:
jshell> import javax.swing.*
jshell> JFrame frame = new JFrame( "HelloJava2" )
frame ==> javax.swing.JFrame[frame0,0,23,0x0,invalid,hidden ... led=true]

Уже лучше. Возможно, немного странно, но явно лучше. Наш объект JFrame был
успешно создан. После стрелки ==> сообщается дополнительная информация
о JFrame: размер (0x0) и позиция на экране (0,23). Для других типов объектов
выводится другая информация. Теперь укажем ширину и высоту окна, как это
делалось ранее, и отобразим окно на экране:
jshell> frame.setSize(300,200)
jshell> frame.setLocation(400,400)
jshell> frame.setVisible(true)

Вы увидите, что на экране появилось окно! Оно выглядит вполне современно —
примерно так, как показано на рис. 3.3.

Первые эксперименты с Java  101

Рис. 3.3. Создание объекта JFrame из jshell
Не беспокойтесь о том, что вы можете допустить ошибку в REPL. Появится
сообщение об ошибке, но вы сможете исправить то, что сделали неправильно,
а потом продолжить работу. Представьте, например, что вы допустили опечатку
при попытке изменить размер окна:
jshell> frame.setsize(300,300)
| Error:
| cannot find symbol
|
symbol:
method setsize(int,int)
| frame.setsize(300,300)
| ^-----------^

В языке Java учитывается регистр символов, поэтому setSize() — это не то же
самое, что setsize(). jshell выдает ту же информацию об ошибке, что и компилятор Java, но представляет ее в другом виде. Исправьте ошибку, и вы увидите,
что окно немного увеличилось (рис. 3.4).

Рис. 3.4. Изменение размера окна

102  Глава 3. Рабочие инструменты
Потрясающе! По правде говоря, это было скорее эффектно, чем полезно, но все
только начинается. Добавим текст при помощи класса JLabel:
jshell> JLabel label = new JLabel("Hi jshell!")
label ==>
javax.swing.JLabel[,
0,0,0x0, ...
rticalTextPosition=CENTER]
jshell> frame.add(label)
$8 ==>
javax.swing.JLabel[,0,0,0x0, ...
text=Hi, ...]

Неплохо, но почему надпись не появилась в окне? Этот вопрос будет подробнее
рассмотрен в главе, посвященной пользовательским интерфейсам, но суть в том,
что Java может накапливать графические изменения, прежде чем отображать их
на экране. Этот прием бывает невероятно эффективным, но иногда он может
застать вас врасплох. Заставим окно перерисовать себя (рис. 3.5):
jshell> frame.revalidate()
jshell> frame.repaint()

Рис. 3.5. Добавление объекта JLabel в окно
Теперь надпись видна. Некоторые действия автоматически инициируют вызов
revalidate() или repaint(). Например, любой компонент, уже добавленный
в окно до того, как оно появилось на экране, немедленно становится видимым

Первые эксперименты с Java  103

с появлением окна. Надпись можно удалить тем же способом, которым она была
добавлена. Снова посмотрите, что произойдет при изменении размера окна сразу
же после удаления надписи (рис. 3.6):
jshell> frame.remove(label) // Как и в случае с add(), изображение
// не изменяется немедленно
jshell> frame.setSize(400,150)

Рис. 3.6. Удаление надписи и изменение размеров окна
Видите? Окно стало более узким, а надпись исчезла — и все это без принудительной перерисовки. Мы еще поработаем с GUI-элементами в дальнейших
главах, а пока попробуем провести еще один эксперимент, который показывает,
как легко опробовать новые идеи и методы, которые вы обнаружили в документации. Например, текст надписи можно выровнять по центру; примерный
результат показан на рис. 3.7:
jshell> frame.add(label)
$45 ==>
javax.swing.JLabel[,0,0,300x278,...,
text=Hi jshell!,...]
jshell> frame.revalidate()
jshell> frame.repaint()
jshell> label.setHorizontalAlignment(JLabel.CENTER)

Рис. 3.7. Выравнивание текста по центру надписи

104  Глава 3. Рабочие инструменты
Вероятно, для вас это был очередной головокружительный полет с несколькими
фрагментами кода, которые пока могут выглядеть загадочно. Например, почему
слово CENTER написано в верхнем регистре? И почему перед выравниванием по
центру указывается имя класса JLabel? Надеемся, что после ввода всех этих
строк (даже если вы допустили пару мелких ошибок и исправили их) вы захотели узнать больше. Мы стараемся, чтобы у вас было все необходимое для
собственных экспериментов, когда вы будете читать другие главы этой книги.
Как и во многих других областях, в программировании намного полезнее сделать
что-то своим руками, чем прочитать об этом!

JAR-файлы
Архивные JAR-файлы — это своего рода «чемоданы» из мира Java. Они предоставляют стандартные и портируемые средства для упаковки всех частей приложения Java в компактный пакет, который легко распространять и устанавливать.
В JAR-файл можно поместить что угодно: файлы классов Java, упорядоченные
объекты, файлы данных, графику, звук и т. д. JAR-файл может содержать одну
или несколько цифровых подписей, удостоверяющих его целостность и подлинность. Подписью может снабжаться как файл в целом, так и его отдельные
элементы.
Исполнительная система Java может загружать файлы напрямую из архива,
указанного в CLASSPATH, как было описано ранее. Файлы из JAR-файла, не являющиеся файлами классов (данные, изображения и т. д.), загружаются из classpath
в ваше приложение при помощи метода getResource(). При этом вашему коду
не надо знать, является ли ресурс простым файлом или элементом JAR-архива.
Независимо от того, является ли файл класса или файл данных элементом JARфайла или отдельным файлом в classpath, вы всегда можете ссылаться на него
стандартным способом и предоставить загрузчику классов Java самостоятельно
определить его местонахождение.

Сжатие
Данные, хранящиеся в JAR-файлах, сжимаются стандартным алгоритмом ZIP.
Сжатие значительно ускоряет передачу классов по сети. Беглый просмотр
стандартного дистрибутива Java показывает, что типичный класс сжимается
приблизительно на 40%. Текстовые файлы, содержащие английские слова (например, файлы HTML или ASCII), часто сжимаются до одной десятой части
своего исходного размера и даже менее. (Но графические файлы при сжатии
обычно не уменьшаются, потому что самые распространенные графические
форматы сами реализуют сжатие.)

JAR-файлы  105

При самостоятельной работе с Java вы также можете встретить устаревший формат
архивов Pack200, оптимизированный для байт-кода. Во многих случаях он сжимает
классы в четыре раза сильнее, чем ZIP. Мы поговорим о нем далее в этой главе.

Утилита jar
Утилита jar, включенная в JDK, — это простой инструмент для создания и чтения JAR-файлов. Ее пользовательский интерфейс не назовешь удобным. Он
воспроизводит интерфейс команды Unix tar (Tape ARchive). Если вы работали
с tar, то следующие «заклинания» покажутся вам знакомыми:
jar -cvf jarFile путь [ путь ] [ ... ]

Эта команда создает архив с именем jarFile, содержащий файлы по заданному пути (или по нескольким путям).
jar -tvf jarFile [ путь ] [ ... ]

Эта команда показывает содержимое архива jarFile, опционально — только
по заданному пути (или по нескольким путям).
jar -xvf jarFile [ путь ] [ ... ]

Эта команда распаковывает содержимое архива jarFile, опционально —
только по заданному пути (или по нескольким путям).
В этих командах флаги c, t и x сообщают jar, какую операцию надо выполнить: создание архива, просмотр содержимого архива или извлечение файлов
из архива. Флаг f означает, что следующий аргумент содержит имя JAR-файла
для выполнения операции. Необязательный флаг v приказывает jar выводить
расширенную информацию о файлах; в этом режиме выводится информация
о размерах файлов, времени изменения и степени сжатия.
Последующие элементы командной строки (то есть все, кроме символов, которые указывают jar, что и с какими файлами делать) интерпретируются как
имена элементов архива. Если вы создаете архив, то перечисленные файлы
и каталоги включаются в этот архив. При извлечении из архива извлекаются
только перечисленные файлы. Если файлы не указаны, то jar распаковывает
все содержимое архива.
Допустим, вы только что доделали свою новую игру Spaceblaster. Все файлы,
связанные с игрой, хранятся в трех каталогах. Классы Java хранятся в каталоге
spaceblaster/game, каталог spaceblaster/images содержит игровую графику,
а каталог spaceblaster/docs — игровые данные. Все эти составляющие можно
упаковать в архив следующей командой:
$ jar -cvf spaceblaster.jar spaceblaster

106  Глава 3. Рабочие инструменты
Так как был запрошен расширенный вывод, утилита jar сообщает, что она делает
в данный момент:
adding:spaceblaster/ (in=0) (out=0) (stored 0%)
adding:spaceblaster/game/ (in=0) (out=0) (stored 0%)
adding:spaceblaster/game/Game.class (in=8035) (out=3936) (deflated 51%)
adding:spaceblaster/game/Planetoid.class (in=6254) (out=3288) (deflated 47%)
adding:spaceblaster/game/SpaceShip.class (in=2295) (out=1280) (deflated 44%)
adding:spaceblaster/images/ (in=0) (out=0) (stored 0%)
adding:spaceblaster/images/spaceship.gif (in=6174) (out=5936) (deflated 3%)
adding:spaceblaster/images/planetoid.gif (in=23444) (out=23454) (deflated 0%)
adding:spaceblaster/docs/ (in=0) (out=0) (stored 0%)
adding:spaceblaster/docs/help1.html (in=3592) (out=1545) (deflated 56%)
adding:spaceblaster/docs/help2.html (in=3148) (out=1535) (deflated 51%)

Это значит, что jar создает архив spaceblaster.jar с содержимым каталога
spaceblaster, добавляя в этот архив все каталоги и файлы из spaceblaster. В расширенном режиме jar выводит информацию об экономии места, достигнутой
при сжатии файлов в архиве.
Архив распаковывается следующей командой:
$ jar -xvf spaceblaster.jar

Аналогичным образом из архива извлекаются отдельные файлы или каталоги:
$ jar -xvf spaceblaster.jar filename

Впрочем, обычно незачем распаковывать JAR-файл для использования его
содержимого; Java умеет извлекать файлы из таких архивов автоматически.
Содержимое JAR-файла выводится следующей командой:
$ jar -tvf spaceblaster.jar

Ниже приведен результат. Выводится список всех файлов с указанием их размеров и времени создания:
0
1074
0
0
8035
6254
2295
0
6174
23444
0
3592
3148

Thu
Thu
Thu
Thu
Thu
Thu
Thu
Thu
Thu
Thu
Thu
Thu
Thu

May
May
May
May
May
May
May
May
May
May
May
May
May

15
15
15
15
15
15
15
15
15
15
15
15
15

12:18:54
12:18:54
12:09:24
11:59:32
12:14:08
12:15:18
12:15:26
12:17:00
12:16:54
12:16:58
12:10:02
12:10:16
12:10:02

PDT
PDT
PDT
PDT
PDT
PDT
PDT
PDT
PDT
PDT
PDT
PDT
PDT

2003
2003
2003
2003
2003
2003
2003
2003
2003
2003
2003
2003
2003

META-INF/
META-INF/MANIFEST.MF
spaceblaster/
spaceblaster/game/
spaceblaster/game/Game.class
spaceblaster/game/Planetoid.class
spaceblaster/game/SpaceShip.class
spaceblaster/images/
spaceblaster/images/spaceship.gif
spaceblaster/images/planetoid.gif
spaceblaster/docs/
spaceblaster/docs/help1.html
spaceblaster/docs/help2.html

JAR-файлы  107

Манифесты JAR
Команда jar автоматически включает в архив каталог с именем META-INF. Каталог META-INF содержит файлы, описывающие содержимое JAR-файла. Он всегда
содержит как минимум один файл: MANIFEST.MF. Файл MANIFEST.MF может содержать «упаковочный список» с именами файлов в архиве и набором атрибутов,
определяемыхпользователем, для каждой записи.
Манифест — это текстовый файл с набором строк в формате «ключевое слово:
значение». По умолчанию манифест почти пуст и содержит только информацию
о версии JAR-файла:
Manifest-Version: 1.0
Created-By: 1.7.0_07 (Oracle Corporation)

Также JAR-файлы могут снабжаться цифровыми подписями. В этом случае
в манифест включается контрольная сумма (дайджест, хеш-код) каждого архивируемого элемента, а каталог META-INF содержит файлы цифровой подписи
для элементов в архиве:
Name: com/oreilly/Test.class
SHA1-Digest: dF2GZt8G11dXY2p4olzzIc5RjP3=
...

Вы можете добавить в манифест собственную информацию, определив собственный дополнительный файл манифеста при создании архива. Это одно
из возможных мест для хранения других простых сведений о файлах в архиве
(например, номера версии или информации об авторских правах).
Попробуйте создать файл со следующими строками в формате «ключевое слово:
значение»:
Name: spaceblaster/images/planetoid.gif
RevisionNumber: 42.7
Artist-Temperament: moody

Чтобы добавить эту информацию в манифест нашего архива, сохраните ее
в файле с именем myManifest.mf и введите следующую команду jar:
$ jar -cvmf myManifest.mf spaceblaster.jar spaceblaster

Дополнительный флаг m указывает, что jar должен читать дополнительную
информацию манифеста из файла, заданного в командной строке. Но как jar
различит два файла, указанных в командной строке? Так как m предшествует f,
утилита ожидает найти имя файла манифеста перед именем создаваемого JARфайла. Вы думаете, что это сомнительное решение? Вы правы: если перечислить
имена файлов в неправильном порядке, то jar выполнит неправильную операцию.

108  Глава 3. Рабочие инструменты
Приложение может запросить информацию манифеста из JAR-файла при помощи класса java.util.jar.Manifest.

Создание исполняемого JAR-файла
Кроме атрибутов, в файл манифеста можно включить несколько специальных
значений. Одно из них, Main-Class, позволяет указать стартовый класс с методом
main() для приложения, содержащегося в JAR:
Main-Class: com.oreilly.Game

Если вы добавите эту строку в манифест JAR-файла (при помощи флага m,
описанного выше), то приложение можно будет запустить прямо из JAR-файла:
$ java -jar spaceblaster.jar

Некоторые GUI-интерфейсы поддерживают запуск приложения двойным
щелчком на JAR-файле. Интерпретатор ищет в манифесте значение Main-Class,
а затем загружает указанный класс как стартовый класс приложения. Похоже, эта
функция часто меняется и поддерживается не во всех операционных системах,
поэтому плохо подходит для тех случаев, когда вы собираетесь распространять
свое приложение.

Утилита pack200
Pack200 — это формат архивов, оптимизированный для хранения скомпилированных классов Java. Pack200 — это эффективная «упаковка» для набора
классов, исключающая многие виды избыточной информации при хранении
набора взаимосвязанных классов. По сути, этот способ сжатия заключается
в том, что многие классы разбираются на части, которые потом снова эффективно собираются в один каталог. Затем применяется стандартный архиватор
(такой, как ZIP), и в итоге получается выигрыш по сжатию в четыре раза и более. Исполнительная система Java не поддерживает формат pack200, поэтому
на такие архивы нельзя ссылаться в classpath. Скорее это промежуточный
формат, очень удобный для передачи по сети JAR-файлов, содержащих апплеты
и другие веб-приложения.
Pack200 был популярен для доставки апплетов в прежние времена, но апплеты
ушли в небытие, а вместе с ними и сам формат потерял актуальность. Но вам,
возможно, еще будут встречаться файлы .pack.gz, поэтому мы решили упомянуть инструменты, которыми вы будете пользоваться при работе с ними. Тем
не менее сами эти инструменты были исключены в Java 14.
Для преобразования JAR-файлов в формат pack200 и обратно служат команды
pack200 и unpack200, которые есть в JDK и OpenJDK до Java 13 включительно.

Следующий шаг  109

Например, для преобразования файла foo.jar в foo.pack.gz используйте коман­
ду pack200:
$ pack200 foo.pack.gz foo.jar

А так вы можете преобразовать foo.pack.gz в foo.jar:
$ unpack200 foo.pack.gz foo.jar

Обратите внимание: в процессе работы pack200 полностью разбирает и реконструирует ваши классы — на уровне классов. Поэтому полученный файл foo.
jar не будет совпадать с оригиналом с точностью до байта.

Следующий шаг
Как видите, экосистема Java содержит немало инструментов — они получили
известность еще в то время, когда их впервые включили в JDK. Вам не придется
сразу пользоваться всеми инструментами, упомянутыми в этой главе, так что
не беспокойтесь, если их список показался вам слишком обширным. По мере
дальнейшего изучения языка мы будем в основном пользоваться компилятором
javac. Впрочем, даже тогда компилятор и другие инструменты будут удобно
спрятаны за кнопками IDE. Мы просто рассказали о том, какие инструменты
вам доступны, чтобы вы могли изучить их детальнее, если они вам понадобятся.
Надеемся, что теперь, когда вы уже видели часть арсенала, применяемого для
создания и упаковки кода Java, вы хотите написать что-то существенное. В нескольких ближайших главах будут заложены основы для этого. Итак, за дело!

ГЛАВА 4

Язык Java

С этой главы начинается наш вводный курс синтаксиса языка Java. Поскольку
все читатели отличаются по опыту программирования, нам было нелегко подобрать подходящий уровень для любой аудитории. Мы постарались найти
баланс между подробным обзором для начинающих с примерами синтаксиса
и предоставлением дополнительной информации для более опытных читателей, чтобы они могли быстро оценить различия между Java и другими языками. Так как синтаксис Java является производным от C, мы иногда приводим
сравнения с функциональностью этого языка, но опыт программирования на
C от вас не требуется. Далее, в главе 5 рассматривается объектно-ориентированная сторона Java, а также завершается обсуждение базовых возможностей
языка. В главе 7 рассматриваются обобщения — механизм, который расширяет возможности типов и позволяет реализовать некоторые виды классов
более гибко и безопасно. После этого мы займемся различными API языка
Java и покажем, что можно делать с их помощью. Оставшаяся часть книги
заполнена примерами решений полезных задач из нескольких областей. Если
после вводных глав у вас останутся вопросы, то мы надеемся, что ответы на
них вы найдете в примерах кода. И конечно, вы всегда сможете повышать
квалификацию! Мы постараемся давать ссылки на другие ресурсы, которые
помогут читателям, желающим продолжить изучение Java за рамками рассматриваемых тем.
Для тех, кто только начинает знакомство с программированием, постоянным
попутчиком должен стать интернет. Бесчисленные сайты, статьи «Википедии»,
публикации в блогах и сообщество Stack Overflow помогут разобраться в конкретных темах и найти ответы на возникающие вопросы. Например, в этой книге
рассматривается язык Java и объясняется, как начать писать на нем полезные
программы, но книга не рассказывает о таких фундаментальных концепциях
программирования, как алгоритмы. Впрочем, эти концепции естественным
образом вошли в обсуждения и примеры кода, а ссылки на сторонние ресурсы
позволят вам закрепить в памяти некоторые подробности и заполнить вынужденные пробелы.

Кодирование текста  111

Кодирование текста
Java — язык для интернета. Интернет-сообщество говорит и пишет на множестве
разных человеческих языков, поэтому они должны поддерживаться и в программах, написанных на Java. Один из способов интернационализации в Java
основан на наборе символов Unicode («Юникод») — международном стандарте,
поддерживающем символы большинства существующих языков1. В современных версиях Java символьные и строковые данные хранятся в соответствии со
стандартом Unicode 6.0, в котором для представления каждого символа используются как минимум два байта.
Исходный код Java можно писать в формате Unicode и сохранять во множестве
разных кодировок, от простой двоичной формы до символов Unicode, закодированных в ASCII. Это делает язык Java удобным для программистов из разных
стран. Они могут использовать свои родные языки в именах классов, методов
и переменных, а также в тексте, выводимом в приложениях.
Тип Java char и класс String обладают встроенной поддержкой Unicode. Во внутреннем представлении текст хранится в формате char[] или byte[]; но язык Java
и API работают с ними прозрачно для вас, поэтому вам, как правило, не придется
об этом думать. Unicode хорошо сочетается с ASCII (это самая распространенная
кодировка символов для английского языка). Первые 256 символов определены
как идентичные первым 256 символам кодировки ISO 8859-1 (Latin-1), так что
Unicode фактически обладает обратной совместимостью с самыми распространенными англоязычными кодировками. Кроме того, кодировка UTF-8, одна
из самых популярных для Unicode, сохраняет значения ASCII в однобайтовой
форме. Она используется по умолчанию в скомпилированных файлах классов
Java, так что английский текст в памяти хранится компактно.
Многие платформы не способны отображать все символы Unicode, определенные
в настоящее время. Поэтому при написании программ на Java можно использовать специальные служебные последовательности (escape-последовательности)
Unicode. Символ Unicode может представляться служебной последовательностью следующего вида:
\uxxxx

Здесь xxxx — это последовательность из 1–4 шестнадцатеричных цифр. Она
обозначает символ Unicode в кодировке ASCII. Также эта форма используется
в Java для вывода символов Unicode в тех средах, где они не поддерживаются.
1

За дополнительной информацией о Unicode обращайтесь на сайт http://www.unicode.
org. По иронии судьбы одним из алфавитов, обозначенных как «устаревшие и архаичные» и в настоящее время не поддерживаемых в Unicode, является яванский — исторический язык жителей острова Ява (Java).

112  Глава 4. Язык Java
Кроме того, в Java есть классы для чтения и записи символьных потоков Unicode
в конкретных кодировках, включая UTF-8.
Unicode, как и многие долговечные стандарты в мире технологий, изначально
проектировался с запасом: считалось, что ни в какой мыслимой кодировке не
может быть более 64 К (65 536) символов. Но в итоге даже этого оказалось мало.
Сейчас получили широкое распространение кодировки UTF-32. В частности,
символы «эмодзи», часто встречающиеся в приложениях-мессенджерах, кодируются за пределами стандартного диапазона символов Unicode (например, канонический смайлик кодируется в Unicode значением 1F600). Java поддерживает
для таких символов многобайтовые служебные последовательности UTF-16.
Не все платформы, поддерживающие Java, совместимы с эмодзи; попробуйте
запустить jshell, чтобы узнать, поддерживаются ли символы-эмодзи на вашем
компьютере (рис. 4.1).

Рис. 4.1. Вывод эмодзи в приложении «Терминал» из macOS
Впрочем, с такими символами необходима осторожность. Нам пришлось показать скриншот, чтобы вы увидели, что эти милые картинки отображаются
в jshell на Mac. Но если запустить в этой же операционной системе десктопное
приложение Java с классами JFrame и JLabel, о которых мы рассказывали в главе 3, то вы получите результат, показанный на рис. 4.2.
jshell> import javax.swing.*
jshell> JFrame f = new JFrame("Emoji Test")
f ==>
javax.swing.JFrame[frame0
,0,23,0x0,invalid,hidden ...
=true]
jshell> JLabel l = new JLabel("Hi \uD83D\uDE00")
l ==> javax.swing.JLabel[,
0,0,0x0,invalid,alignmentX=0. ...
=CENTER]

Комментарии  113
jshell> f.add(l)
$12 ==> javax.swing.JLabel[,0,0,0x0,invalid,alignmentX= ...
rticalTextPosition=CENTER]
jshell> f.setSize(300,200)
jshell> f.setVisible(true)

Это не означает, что вы не можете использовать или поддерживать эмодзи
в своих приложениях, — просто надо знать о различиях в функциональности
вывода. Следите за тем, чтобы не создать проблем у пользователей, которые
будут запускать ваши приложения.

Рис. 4.2. Эмодзи не отображаются в JFrame

Комментарии
Java поддерживает как блочные комментарии в стиле C, заключенные в маркеры /* и */, так и строчные комментарии в стиле C++, обозначаемые маркером //:
/*

Это

блочный комментарий
(в несколько строк)

*/

// А это строчный комментарий (в одну строку)
// И это // тоже строчный комментарий

Блочные комментарии имеют как начальный, так и конечный маркер; они могут
содержать большие объемы текста. Но блочные комментарии не должны быть
вложенными, то есть попытка разместить блочный комментарий внутри другого
блочного комментария будет ошибкой с точки зрения компилятора. Строчные

114  Глава 4. Язык Java
комментарии имеют только начальный маркер и завершаются в конце строки;
дополнительные маркеры // внутри строки ни на что не влияют. Строчные
комментарии удобны для включения кратких примечаний в методы; они не
конфликтуют с блочными комментариями, так что вы можете закомментировать
большие фрагменты кода, в которые они вложены.

Комментарии javadoc
Блочные комментарии, начинающиеся с символов /**, представляют собой специальные doc-комментарии (документирующие комментарии). Они предназначены для извлечения автоматическими генераторами документации (например,
программой javadoc из JDK или системами контекстных подсказок во многих
IDE). Doc-комментарий завершается конечным маркером */, как и обычный
блочный комментарий. Внутри doc-комментария все строки, начинающиеся
с символа @, интерпретируются как специальные инструкции для генератора документации, передающие ему информацию об исходном коде. По общепринятым
соглашениям в начале строк doc-комментария обычно добавляют символ *, как
показано в следующем примере, но это не обязательно. Все начальные пробелы
и символы * в строках игнорируются:
/**
* Пожалуй, этот класс - самая потрясающая штука, которую вы
* увидите в своей жизни. Позвольте мне пояснить свое
* личное видение и причины для его создания.
*
* Все началось еще тогда, когда я был ребенком и рос на улицах
* Айдахо. Спрос на картофель был заоблачным, и жизнь была прекрасна...
*
* @see PotatoPeeler
* @see PotatoMasher
* @author John 'Spuds' Smith
* @version 1.00, 19 Nov 2019
*/
class Potato {

Программа javadoc создает документацию классов в формате HTML, читая исходный код и извлекая встроенные комментарии и теги, начинающиеся с символа @.
В данном примере теги включают в документацию класса информацию об авторе
и версии. Теги @see генерируют гипертекстовые ссылки в документации класса.
Компилятор тоже проверяет doc-комментарии. В частности, его интересует
тег @deprecated, который означает, что метод объявлен устаревшим и лучше не
вызывать его в новых программах. Тот факт, что метод является устаревшим,
отмечается в скомпилированном файле класса, поэтому при обнаружении
в коде устаревших методов будут генерироваться предупреждения (даже если
исходный код недоступен).

Комментарии  115

Doc-комментарии могут располагаться над определениями классов, методов
и переменных, но некоторые теги могут быть неприменимы к некоторым из
них. Например, тег @exception может применяться только к методам. В табл. 4.1
приведена сводка тегов, поддерживаемых в doc-комментариях.

Таблица 4.1. Теги в doc-комментариях
Тег

Описание

Применение

@see

Имя связанного класса

Класс, метод или переменная

@code

Содержимое исходного кода

Класс, метод или переменная

@link

Связанный URL-адрес

Класс, метод или переменная

@author

Имя автора

Класс

@version

Строка с версией

Класс

@param

Имя и описание параметра

Метод

@return

Описание возвращаемого значения

Метод

@exception

Имя и описание исключения

Метод

@deprecated

Объявление элемента как устаревшего

Класс, метод или переменная

@since

Версия API, в которой элемент был добавлен

Переменная

Теги javadoc как метаданные
Теги javadoc в doc-комментариях представляют собой метаданные, относящиеся
к исходному коду; другими словами, они добавляют описательную информацию о структуре или содержании кода, которая формально не является частью
приложения. Некоторые дополнительные инструменты расширяют концепцию
тегов в стиле javadoc, позволяя включать в программы Java и другие виды метаданных, которые передаются вместе со скомпилированным кодом и могут легко
использоваться приложением, воздействуя на его компиляцию или на логику
работы во время выполнения. Аннотации Java предоставляют более формальный
и расширяемый способ добавления метаданных в классы Java, методы и переменные. Эти метаданные также доступны на стадии выполнения.

Аннотации
Префикс @ реализует в Java и другую функциональность, которая на первый
взгляд напоминает функциональность тегов. Аннотации используются в Java
для пометки контента, который должен обрабатываться специальным образом.
Аннотации применяются к коду за пределами комментариев. Информация,

116  Глава 4. Язык Java
предоставляемая аннотацией, может быть полезна компилятору или IDE. Например, аннотация @SuppressWarnings заставляет компилятор (и часто также
вашу IDE) скрывать предупреждения о таких потенциальных проблемах, как
недоступный код. Когда мы займемся созданием более интересных классов
в разделе «Нетривиальное проектирование классов», с. 189, вы увидите, что
ваша IDE добавляет в код аннотации @Overrides. Они заставляют компилятор
выполнять некоторые дополнительные проверки, которые способствуют написанию корректного кода и выявлению ошибок до того, как программа будет
запущена вами или другими пользователями.
Вы можете создавать собственные аннотации для работы с другими инструментами и фреймворками. Хотя углубленное изучение аннотаций выходит за рамки
этой книги, мы воспользуемся некоторыми очень удобными аннотациями при
изучении веб-программирования в главе 12.

Переменные и константы
Итак, вы научились комментировать свой код. Это необходимо для того, чтобы
он был наглядным и простым в сопровождении. Теперь нам пора сосредоточиться на самом коде, то есть на том, что компилируется. Все программирование
сводится к работе с кодом. Почти во всех языках важная часть кода содержится
в переменных и константах, которые упрощают работу программиста. В Java
есть как переменные, так и константы. В переменных хранится та информация,
которую вы собираетесь изменять и затем повторно использовать (или информация, неизвестная заранее: например, адрес электронной почты пользователя).
В константах хранится информация, которая не должна изменяться. Примеры
переменных и констант встречались вам даже в наших маленьких вводных
программах. Вспомните простую графическую надпись из раздела «HelloJava»
на с. 66:
import javax.swing.*;
public class HelloJava {
public static void main( String[] args ) {
JFrame frame = new JFrame( "Hello, Java!" );
JLabel label = new JLabel( "Hello, Java!", JLabel.CENTER );
frame.add(label);
frame.setSize( 300, 300 );
frame.setVisible( true );
}
}

В этом фрагменте frame — переменная. В строке 5 она заполняется новым
экземпляром класса JFrame. Затем тот же экземпляр повторно используется

Переменные и константы  117

в строке 7 для добавления надписи. В строке 8 эта переменная снова нужна нам,
чтобы установить размер окна, а в строке 9 — чтобы перевести окно в видимое
состояние. Повторное использование данных — это та область, в которой переменные по-настоящему проявляют себя.
В строке 6 есть константа JLabel.CENTER. Константы содержат значения, которые
остаются неизменными во время выполнения программы. Казалось бы, зачем
хранить таким образом те значения, которые не будут изменяться? Почему бы
просто не вписывать их в код каждый раз, когда они понадобятся? Дело в том,
что автор кода может выбирать имена для всех констант, что сразу приносит
пользу: значения дополняются содержательными описаниями. Возможно, смысл
имени JLabel.CENTER все еще не совсем очевиден, но слово CENTER по крайней
мере намекает на то, что здесь происходит.
Кроме того, именованные константы упрощают внесение изменений. Например,
если в вашем коде указано предельное количество единиц какого-то используемого ресурса, то скорректировать этот предел будет намного проще, если для
этого достаточно изменить инициализированное значение константы. Если бы
для этой же цели использовался числовой литерал, например 5, то вам пришлось
бы выискивать во всех файлах Java все вхождения 5 и изменять их каждый раз,
когда конкретный экземпляр 5 действительно относится к ограничению соответствующего ресурса. Такой ручной поиск с заменой крайне ненадежен, не
говоря уже об однообразии работы.
Типы и исходные значения переменных и констант более подробно описываются
далее, в следующем разделе. Как обычно, не стесняйтесь использовать jshell,
чтобы найти и исследовать некоторые из этих подробностей самостоятельно!
Впрочем, помните, что из-за ограничений интерпретатора вы не сможете объявлять собственные константы верхнего уровня в jshell. Но вы можете использовать константы, определенные для классов (такие, как упомянутая выше
JLabel.CENTER), а также определять константы в ваших собственных классах,
вводимых в jshell. Класс Math содержит множество разных математических
функций и константу, представляющую число π. Попробуем вычислить и сохранить площадь круга в переменной, а затем продемонстрировать, что повторное
присваивание констант не работает.
jshell> double radius = 42.0;
radius ==> 42.0
jshell> Math.PI
$2 ==> 3.141592653589793
jshell> Math.PI = 3;
| Error:
| cannot assign a value to final variable PI
| Math.PI = 3;
| ^-----^

118  Глава 4. Язык Java
jshell> double area = Math.PI * radius * radius;
area ==> 5541.769440932396
jshell> radius = 6;
radius ==> 6.0
jshell> area = Math.PI * radius * radius;
area ==> 113.09733552923255
jshell> area
area ==> 113.09733552923255

Обратите внимание на ошибку компилятора при попытке присвоить числу π
значение 3. Также обратите внимание на то, что значения radius и area можно менять после объявления и инициализации. Тем не менее в любой момент
времени переменная может хранить только одно значение. В переменной area
остается только последний вычисленный результат.

Типы
Система типов в языке программирования описывает, как его элементы данных
(только что упоминавшиеся переменные и константы) связываются со своими
блоками памяти и как они связываются друг с другом. В языке со статической
типизацией (например, C или C++) тип элемента данных представляет собой
простой неизменяемый атрибут, который часто соответствует некоторому
аппаратному понятию, например регистру или значению указателя. В более
динамических языках (таких, как Smalltalk или Lisp) переменные могут обозначать произвольные элементы и даже изменять свои типы на протяжении
своего срока жизни. На проверку того, что происходит в этих языках во время выполнения кода, расходуются значительные ресурсы. Языки сценариев,
такие как Perl, достигают простоты использования благодаря радикально
упрощенной системе типов, в которой переменные могут хранить только некоторые элементы данных, а значения объединяются в общее представление
(например, строки).
В Java сочетаются многие лучшие свойства языков как со статической, так
и с динамической типизацией. Как и в языках со статической типизацией, каждая
переменная и каждый программный элемент в Java имеют тип, известный на стадии компиляции, поэтому исполнительной системе обычно не нужно проверять
корректность присваиваний между типами во время выполнения кода. Кроме
того, в отличие от традиционных C и C++, Java контролирует информацию об
объектах во время выполнения и использует ее для реализации полноценного
динамического поведения. Код Java может загружать новые типы во время выполнения и использовать их объектно-ориентированными способами, допуская

Типы  119

преобразования типов и полноценный полиморфизм (расширение типов). Код
Java также может анализировать свои типы во время выполнения, что делает
возможным очень сложное поведение приложений, например интерпретаторов,
способных динамически взаимодействовать со скомпилированными программами.
Типы данных Java делятся на две категории. Примитивные типы (primitive types)
представляют простые значения, обладающие в языке встроенной функциональностью, такие как числа, логические операторы и символы текста. Ссылочные
типы (reference types), или типы классов, включают объекты и массивы; они
называются ссылочными типами, потому что они «ссылаются» на большие по
объему данные, определяя их «по ссылкам» (вскоре мы объясним, что это означает). Обобщенные типы (generic types) и методы определяют объекты разных
типов и работают с ними, обеспечивая безопасность типов при компиляции.
Например, List — это список List, который может содержать только
элементы String. Обобщенные типы тоже являются ссылочными типами; в главе 7 мы рассмотрим много примеров такого рода.

Примитивные типы
Числа, символы и логические значения (boolean values) относятся к фундаментальным элементам Java. В отличие от других объектно-ориентированных
языков (возможно, более чистых), они не являются объектами. Но для ситуаций, в которых необходимо работать с примитивным типом как с объектом,
Java предоставляет классы-обертки (wrapper classes); о них мы расскажем
далее. Главное преимущество обработки примитивных типов как специальных заключается в том, что компилятору и исполнительной системе Java
проще оптимизировать их реализацию. Примитивные значения и вычисления
по-прежнему могут отображаться на аппаратные элементы компьютера, как
всегда было в низкоуровневых языках. Если вы будете работать с платформенными библиотеками, используя JNI (Java Native Interface) для взаимодействия
с другими языками или службами, то эти примитивные типы займут важное
место в вашем коде.
Важная особенность портируемости Java — точное определение примитивных
типов. Например, вам никогда не придется беспокоиться о размере переменных
типа int на конкретной платформе; этот тип всегда будет 32-разрядным числом
со знаком, которое представлено в дополнительном коде. Размер числовой переменной определяет, насколько большое (или насколько точное) значение в ней
можно сохранить. Например, тип byte предназначен для малых чисел в диапазоне
от –128 до 127, а тип int обеспечивает большинство потребностей в работе с числами, позволяя хранить значения в диапазоне ± 2 миллиарда (приблизительно).
В табл. 4.2 перечислены все примитивные типы Java.

120  Глава 4. Язык Java
Таблица 4.2. Примитивные типы данных Java
Тип

Определение

Приблизительный диапазон
или точность

boolean

Логическое значение

true или false

char

16-разрядный символ Unicode

64 К символов

byte

8-разрядное целое со знаком в дополнительном коде

от -128 до 127

short

16-разрядное целое со знаком в дополнительном коде

от -32 768 до 32 767

int

32-разрядное целое со знаком в дополнительном коде

от -2,1e9 до 2,1e9

long

64-разрядное целое со знаком в дополнительном коде

от -9,2e18 до 9,2e18

float

32-разрядное число с плавающей точкой
в формате IEEE 754

6-7 значимых цифр в дробной
части

double

64-разрядное число с плавающей точкой
в формате IEEE 754

15 значимых цифр в дробной
части

Читатели с опытом программирования на C заметят, что примитивные типы
выглядят как идеализация скалярных типов C на 32-разрядной машине,
и это правда. Именно так они и должны выглядеть. 16-разрядные символы
были обусловлены стандартом Unicode, а произвольные указатели были
исключены по другим причинам. Но в целом синтаксис и семантика примитивных типов Java происходят от C.

Но зачем вообще нужны разные размеры? И снова все возвращается к эффективности и оптимизации. Количество голов в футбольном матче редко выходит за
пределы однозначных чисел, то есть его можно хранить в переменной byte. Для
того, чтобы обозначить количество болельщиков, смотрящих матч, потребуется
что-то побольше. А общая сумма денег, потраченных всеми болельщиками во
всех футбольных матчах чемпионата мира, займет еще больше памяти. Выбирая
правильный размер, вы предоставляете компилятору максимальные возможности для оптимизации кода, что позволяет ускорить работу приложения и (или)
потреблять меньше системных ресурсов.
Если вам нужны очень большие числа, которые не поддерживаются примитивными типами, обратите внимание на классы BigInteger и BigDecimal из пакета
java.Math. Эти классы обеспечивают почти бесконечные размеры и точность.
Некоторые научные или криптографические приложения, требующие хранения
и обработки очень больших (или очень малых) чисел, отдают предпочтение точности перед быстродействием. В книге эти классы описываться не будут, но вы
на всякий случай запомните их для исследований в ненастный день.

Типы  121

Точность чисел с плавающей запятой
В Java все операции с плавающей запятой (которая в международной терминологии называется плавающей точкой: floating point) определяются международной спецификацией IEEE 754. Это означает, что результаты вычислений
с плавающей точкой, как правило, всегда одинаковы на всех платформах Java.
Тем не менее Java позволяет выполнять вычисления с повышенной точностью
на тех платформах, которые поддерживают такую возможность. Это может создать крайне малые и трудные для понимания различия в результатах операций
с высокой точностью. В большинстве приложений вы их никогда не заметите,
но если вы хотите гарантировать, что приложение выдает идеально совпадающие результаты на всех платформах, используйте специальное ключевое слово
strictfp в качестве модификатора для класса, содержащего операции с плавающей точкой (классы рассматриваются в следующей главе). Тогда компилятор
запретит те виды оптимизации, которые зависят от платформы.

Объявление и инициализация переменных
Переменные объявляются внутри методов и классов путем указания имени
типа, за которым следует одно или несколько имен переменных, разделенных
запятыми. Пример:
int foo;
double d1, d2;
boolean isFun;

Переменные можно инициализировать выражением соответствующего типа
при объявлении:
int foo = 42;
double d1 = 3.14, d2 = 2 * 3.14;
boolean isFun = true;

Если переменные, объявленные в составе класса, не были инициализированы (см.
главу 5), то им присваиваются значения по умолчанию. В таком случае переменным числовых типов по умолчанию присваивается ноль, символам присваивается
null-символ (\0), а логическим переменным — значение false. (У ссылоч­ных
типов также имеется значение по умолчанию null, но об этом — далее, в разделе
«Ссылочные типы» на с. 124.) Но локальные переменные, объявленные внутри
метода и существующие только во время вызова метода, должны быть явно инициализированы перед использованием. Как вы увидите, компилятор заставляет
вас соблюдать это правило, поэтому нет риска забыть о нем.

Целочисленные литералы
Целочисленные литералы могут записываться в двоичной системе (основание 2),
а также в восьмеричной (основание 8), десятичной (основание 10) или шест-

122  Глава 4. Язык Java
надцатеричной (основание 16). Двоичные, восьмеричные и шестнадцатеричные
данные в основном используются при работе с низкоуровневой информацией
из файлов или сетевых данных. Они представляют собой удобные группы отдельных битов: из 1, 3 и 4 битов соответственно. У десятичных значений такого
соответствия нет, но, как правило, они намного удобнее для человека при работе
с числовой информацией. Десятичное целое число обозначается последовательностью цифр, начинающейся с одного из символов в диапазоне от 1 до 9:
int i = 1230;

Двоичное число обозначается начальными символами 0b или 0B, за которыми
следует последовательность нулей и единиц:
int i = 0b01001011; // i = 75 (в десятичной записи)

Восьмеричные числа можно отличить от десятичных по начальному нулю:
int i = 01230; // i = 664 (в десятичной записи)

Шестнадцатеричное число состоит из начальных символов 0x (или 0X), за которыми следует последовательность цифр и символов a–f (или A–F), означающих
десятичные числа от 10 до 15:
int i = 0xFFFF; // i = 65535 (в десятичной записи)

Целочисленные литералы по умолчанию имеют тип int, если только они не
завершаются суффиксом L, который означает, что они представляют значение
типа long:
long
long
long
long

l
l
l
l

=
=
=
=

13L;
13; // Эквивалент: число 13 преобразуется из типа int
40123456789L;
40123456789; // Ошибка: слишком большое число для int
// без преобразования

Буква l нижнего регистра тоже допустима, но лучше ее не использовать, потому
что она слишком похожа на цифру 1.
Когда числовой тип используется в присваивании или в выражении вместе с типом большего размера (с большим диапазоном), он может быть автоматически
повышен до большего типа. Во второй строке приведенного выше примера число 13 (целочисленный литерал) по умолчанию имеет тип int, но он повышается
до типа long для присваивания переменной long. Другие числовые операции
и операции сравнения тоже приводят к подобным арифметическим повышениям, как и математические выражения, в которых задействовано несколько
типов. Например, при умножении значения byte на значение int компилятор
сначала повышает byte до int:

Типы  123
byte b = 42;
int i = 43;
int result = b * i; // b повышается до int перед умножением

Но обратная ситуация невозможна: числовое значение не может быть присвоено
типу с меньшим диапазоном без явного преобразования:
int i = 13;
byte b = i; // Ошибка компиляции, требуется явное преобразование
byte b = (byte) i; // OK

Преобразования типов с плавающей точкой в целочисленные типы всегда должны быть явными во избежание возможной потери точности.
Начиная с Java 7, к числовым литералам можно применять простейшее форматирование, разделяя цифры символом подчеркивания: _. Таким образом, если
вы работаете с длинными строками цифр, их можно разбивать на легкочитаемые
группы как в следующем примере:
int RICHARD_NIXONS_SSN = 567_68_0515;
int for_no_reason = 1___2___3;
int JAVA_ID = 0xCAFE_BABE;
long grandTotal = 40_123_456_789L;

Символы подчеркивания могут находиться только между цифрами, но не в начале числа, не в конце числа и не рядом с суффиксом L — признаком типа long.
Попробуйте ввести несколько больших чисел в jshell. Обратите внимание:
при попытке сохранить тип long без суффикса возникает ошибка. Как видите,
форматирование нужно только для вашего удобства. Оно не сохраняется во
внутреннем представлении; в переменной или константе хранится только само
значение:
jshell> long m = 41234567890;
| Error:
| integer number too large
| long m = 41234567890;
|
^
jshell> long m = 40123456789L;
m ==> 40123456789
jshell> long grandTotal = 40_123_456_789L;
grandTotal ==> 40123456789

Поэкспериментируйте с другими примерами. Так вы сможете понять, какой
вариант лучше всего подходит лично вам. Также вы получите представление
о том, какие преобразования и повышения возможны или обязательны. Ничто
не закрепит все эти нюансы в вашей памяти так хорошо, как немедленная обратная связь!

124  Глава 4. Язык Java
Литералы с плавающей точкой
Значения с плавающей точкой могут задаваться в обычной записи или в научной
записи. Литералы с плавающей точкой обычно относятся к типу double, если
только они не завершаются суффиксом f или F, который означает, что они представляют значения типа float. Как и с целочисленными литералами, начиная
с Java 7, можно использовать символы _ для форматирования чисел с плавающей
точкой — но только между цифрами, а не в начале числа, не в конце числа и не
рядом с точкой или суффиксом F.
double d = 8.31;
double e = 3.00e+8;
float f = 8.31F;
float g = 3.00e+8F;
float pi = 3.14_159_265_358;

Символьные литералы
Значение символьного литерала задается либо символом, заключенным
в одинарные кавычки, либо экранированной последовательностью (escapeпоследовательностью) ASCII или Unicode:
char a = 'a';
char newline = '\n';
char smiley = '\u263a';

Ссылочные типы
Java — это объектно-ориентированный язык, поэтому вы можете создавать из
простых примитивов новые, более сложные типы данных при помощи классов.
Каждый класс становится новым типом данных в языке. Например, если вы
создаете новый класс с именем Foo, вы тем самым создаете новый тип данных
с таким же именем Foo. Тип элемента определяет, как он используется и где он
может присваиваться. Как и в случае с примитивами, элемент типа Foo в общем
случае может быть присвоен переменной типа Foo или передан в аргументе
методу, получающему значение типа Foo.
Тип — это не просто атрибут. Классы могут быть связаны разными отношениями
с другими классами, как и представляемые ими типы. Все классы в Java существуют в рамках иерархии «родитель — потомок», в которой субкласс, или подкласс (то
есть дочерний класс), является особым частным случаем своего суперкласса (то
есть родительского класса). Соответствующие типы обладают той же связью: тип
субкласса считается подтипом суперкласса. Поскольку субклассы наследуют всю
функциональность своих суперклассов, объект дочернего типа отчасти эквивалентен родительскому типу или является его расширением. Объект субкласса может
использоваться вместо объекта суперкласса. Например, если вы создали новый

Типы  125

класс Cat, являющийся расширением класса Animal, то новый тип Cat будет подтипом Animal. После этого объекты типа Cat можно использовать везде, где может
использоваться объект типа Animal; говорят, что объект типа Cat совместим по присваиванию с переменной типа Animal. Этот принцип, называемый полиморфизмом
подтипов, является одной из главных особенностей объектно-ориентированных
языков. Классы и объекты детально рассматриваются в главе 5.
Все примитивные типы в Java используются и передаются «по значению».
Иначе говоря, когда примитивное значение (например, int) присваивается
переменной или передается в аргументе методу, Java выполняет копирование
этого значения. С другой стороны, все обращения к ссылочным типам (к типам
классов) происходят «по ссылкам». Ссылка (reference) — это именованный идентификатор объекта. В переменной любого ссылочного типа хранится указатель
на адрес в памяти, где находится конкретный объект ее типа (или ее подтипа).
Когда ссылка присваивается переменной или передается в аргументе методу,
Java выполняет копирование этой ссылки, а не самого объекта, на который она
указывает. Ссылки похожи на указатели в C и C++, но с тем важным отличием,
что все типы ссылок находятся под строгим контролем. В Java невозможно
явным образом назначить или изменить значение переменной ссылочного
типа (указатель на адрес в памяти). Такая переменная получает свое значение
автоматически, только при присваивании ей конкретного объекта.
Рассмотрим пример. Объявим переменную типа Foo с именем myFoo и присвоим
ей подходящий по типу объект1:
Foo myFoo = new Foo();
Foo anotherFoo = myFoo;

Здесь myFoo — переменная ссылочного типа, содержащая ссылку на созданный объект Foo. (Пока не будем отвлекаться на подробное описание создания
объектов; эта тема рассматривается в главе 5.) Объявим вторую переменную
anotherFoo типа Foo и присвоим ей тот же объект. Теперь в программе существуют две идентичные ссылки: myFoo и anotherFoo, но только один экземпляр
Foo. Если изменить что-либо в состоянии самого объекта Foo, то эти изменения
будут видны по любой из двух ссылок. Чтобы «заглянуть за кулисы» и понять,
что происходит при работе со ссылками, выполните в jshell следующий код:
jshell> class Foo {}
| created class Foo
jshell> Foo myFoo = new Foo()
myFoo ==> Foo@21213b92
1

Эквивалентный код C++ выглядит так:
Foo& myFoo = *(new Foo());
Foo& anotherFoo = myFoo;

126  Глава 4. Язык Java
jshell> Foo anotherFoo = myFoo
anotherFoo ==> Foo@21213b92
jshell> Foo notMyFoo = new Foo()
notMyFoo ==> Foo@66480dd7

Обратите внимание на результаты создания и присваивания (конечно, на вашем
компьютере значения указателей будут другими). Вы видите, что переменная
ссылочного типа определяется значением указателя (21213b92, справа от @)
и типом (Foo, слева от @). При создании нового объекта типа Foo вы получите
новое значение указателя. Переменные myFoo и anotherFoo указывают на один
и тот же объект. Переменная notMyFoo указывает на второй, отдельный объект
того же типа.

Автоматическое определение типов
В современных версиях Java постоянно совершенствуется способность компилятора автоматически определять типы переменных во многих ситуациях.
Используйте ключевое слово var в сочетании с объявлением и инициализацией
переменной, чтобы поручить компилятору определить правильный тип:
jshell> class Foo2 {}
| created class Foo2
jshell> Foo2 myFoo2 = new Foo2()
myFoo2 ==> Foo2@728938a9
jshell> var myFoo3 = new Foo2()
myFoo3 ==> Foo2@6433a2

Обратите внимание на вывод myFoo3 в jshell. Хотя мы не указали тип явно,
как было сделано для Foo2, компилятор легко определяет, какой тип следует
использовать, и мы получаем объект типа Foo2.

Передача ссылок
Ссылки на объекты передаются методам по одной схеме. В нашем случае аргументы myFoo и anotherFoo будут эквивалентными:
myMethod( myFoo );

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

Типы  127

да — локальная переменная) в действительности является третьей ссылкой
на объект Foo , в дополнение к myFoo и anotherFoo . Метод может изменить
состояние объекта Foo по этой ссылке (вызывая его методы или изменяя его
переменные), но не может изменить смысл ссылки на myFoo с вызывающей
стороны. Иначе говоря, метод не может изменить ссылку myFoo на стороне
вызова и заставить ее указывать на какой-то другой объект Foo. Метод может
изменить только свою ссылку. Это станет более очевидным, когда мы будем
рассказывать о методах. В этом отношении Java отличается от C++. Если вы
захотите изменить ссылку на объект на стороне вызова в Java, для этого потребуется дополнительный уровень косвенного обращения. То есть сторона
вызова должна будет «обернуть» ссылку в другой объект, чтобы обе стороны
могли использовать общую ссылку на него.
Ссылочные типы всегда содержат указатели на объекты (или содержат null),
а объекты всегда определяются классами. По аналогии с платформенными
типами всем переменным экземпляров или классов, которые не инициализируются явно, при объявлении присваивается значение по умолчанию null. Как
и платформенные типы, локальные переменные ссылочных типов не инициализируются по умолчанию, поэтому вы должны явно присвоить им значения
перед использованием. Впрочем, две особые разновидности ссылочных типов —
массивы и интерфейсы — несколько иначе задают типы объектов, на которые
они ссылаются.
Массивы в Java занимают особое место в системе типов. Массив — это особая
разновидность объектов; он автоматически создается для хранения групп объектов другого типа (который называется базовым типом). При объявлении
ссылки с типом массива неявно создается новый тип класса, который служит
контейнером для своего базового типа, как вы увидите далее в этой главе.
С интерфейсами дело обстоит сложнее. Интерфейс определяет набор методов
и назначает ему соответствующий тип. К объекту, реализующему методы интерфейса, можно обращаться как по типу интерфейса, так и поего собственному
типу. Переменные и аргументы методов могут объявляться как относящиеся
к типу интерфейса (как и любые другие типы классов) и им могут быть присвоены любые объекты, реализующие интерфейс. В результате система типов
становится более гибкой: она позволяет Java выходить за границы иерархии
классов и создавать объекты, которые фактически обладают многими типами.
Интерфейсы будут рассмотрены в следующей главе.
Обобщенные типы (или параметризованные типы), как упоминалось ранее,
являются расширением синтаксиса классов Java, которое реализует дополнительный уровень абстракции при работе классов с другими типами Java. Этот
механизм позволяет изменять специализацию класса без изменения кода исходного класса. Обобщения подробно рассматриваются в главе 7.

128  Глава 4. Язык Java

Несколько слов о строках
Строки в Java являются объектами; следовательно, они относятся к ссылочным
типам. Тем не менее объекты String получают от компилятора дополнительную
поддержку, благодаря которой они больше похожи на примитивные типы. Литеральные строковые значения в исходном коде Java преобразуются в объекты
String компилятором. Их можно использовать напрямую, передавать в аргументах методам или присваивать переменным типа String:
System.out.println( "Hello, World..." );
String s = "I am the walrus...";
String t = "John said: \"I am the walrus...\"";

Оператор + в Java перегружается, чтобы выполнять не только обычное числовое сложение, но и конкатенацию. Оператор + и родственный ему += являются
единственными перегружаемыми операторами в Java:
String quote = "Four score and " + "seven years ago,";
String more = quote + " our" + " fathers" + " brought...";

Здесь Java составляет объект String из строк, объединенных конкатенацией,
и предоставляет его как результат выражения. Класс String и работа с текстом
подробно рассматриваются в главе 8.

Команды и выражения
Команды Java размещаются внутри методов и классов; они описывают все выполняемые в программе операции. Объявления переменных и присваивания
(вроде приведенных в предыдущем разделе) являются командами, как и базовые
структуры языка вроде условных конструкций if / else и циклов. (Подробнее
об этих структурах рассказано далее в этой главе.)
int size = 5;
if ( size > 10 )
doSomething();
for ( int x = 0; x < size; x++ ) { ... }

Выражения предоставляют значения; выражение вычисляется для получения
результата, который может использоваться как часть другого выражения или
команды. Примеры выражений — вызовы методов, создание объектов и, конечно,
математические выражения.
new Object()
Math.sin( 3.1415 )
42 * 64

Команды и выражения  129

Один из основополагающих принципов Java — простота и последовательность.
При отсутствии других ограничений вычисление и инициализация в Java всегда
выполняются в порядке их следования в коде: слева направо, сверху вниз. Вы
увидите, как это правило используется для вычисления выражений присваивания, для вызовов методов, для индексирования массивов и т. д. В некоторых
языках порядок вычисления бывает более сложным или даже зависящим от
реализации. Java исключает этот потенциальный риск, просто и точно определяя последовательность обработки кода. Впрочем, это не означает, что вам
надо писать малопонятные и запутанные команды. Сложные зависимости от
порядка вычисления выражений — плохой стиль программирования, даже если
код работает. Такой код хуже читается и в него сложнее вносить изменения.

Команды
В любой программе все самое интересное делают команды. Они помогают
реализовать алгоритмы, упомянутые в начале главы. Собственно, они не
просто помогают, а становятся основными инструментами для решения задач; каждый шаг алгоритма соответствует одной или нескольким командам.
Команды обычно выполняют одну из четырех операций: ввод данных для присваивания переменным; вывод данных (на терминал, в JLabel и т. д.); принятие
решений относительно того, какие команды должны быть выполнены далее;
повторение одной или нескольких других команд. Рассмотрим примеры всех
этих категорий в Java.
Команды и выражения в Java размещаются в блоках кода. С точки зрения синтаксиса блок представляет собой серию команд, заключенных между открывающей ({) и закрывающей (}) фигурными скобками. В блоке могут находиться
команды объявления переменных, а также многие другие команды и выражения,
упоминавшиеся ранее:
{

}

int size = 5;
setName( "Max" );
...

Методы в Java напоминают функции языка C. В каком-то смысле это обычные
блоки, которые могут получать параметры и вызываться по имени. Например,
мы могли бы написать метод с именем setUpDog():
setUpDog( String name ) {
int size = 5;
setName( name );
...
}

130  Глава 4. Язык Java
Видимость объявленной в блоке переменной ограничивается этим блоком, то
есть переменная не видна за пределами ближайшей пары фигурных скобок:
{

int i = 5;
}
i = 6; // Ошибка компиляции, переменная i не существует

Таким образом, блоки удобны для произвольной группировки команд и переменных. Тем не менее самое распространенное применение блоков — определение групп команд для использования в условных или итеративных командах.

Условные команды if / else
Принятие решений — одна из ключевых концепций программирования. «Если
этот файл существует…» или «если у пользователя есть подключение Wi-Fi…» —
подобные решения принимаются компьютерными программами постоянно.
Условие if / else может определяться так:
if ( условие )
команда;
else
команда;

Весь этот пример является командой, и он может быть вложен в другую команду
if / else. Конструкция if может существовать в двух разных формах: однострочной и блочной. Блочная форма выглядит так:
if ( условие )
[ команда;
[ команда;
[ ... ]
} else {
[ команда;
[ команда;
[ ... ]
}

{
]
]

]
]

Условие — это логическое выражение (boolean expression). Оно может иметь
значение true или false либо может быть таким выражением, при вычислении которого будет получено одно из этих значений. Например, i == 0 — это
­логическое выражение, которое проверяет, содержит ли переменная i значение 0.
Во второй форме команды заключены в блоки, а все команды в блоке выполняются только при выборе соответствующей ветви: if или else. Любые переменные, объявленные в каждом блоке, видимы только в командах этого блока.

Команды и выражения  131

Кроме команд if / else, многие другие команды Java тоже управляют последовательностью выполнения. Они работают примерно так же, как и их аналоги
в других языках.

Команды switch
Во многих языках поддерживается условная конструкция «выбор одного из многих», обычно называемая командой switch или case. Команда switch проверяет
заданную переменную или выражение на возможное совпадение с несколькими
перечисленными вариантами. Предпочтение отдается первому найденному
совпадению, поэтому важен порядок перечисления. И мы неспроста говорим,
что совпадение «возможное», так как значение может не совпасть ни с одним
из вариантов, перечисленных в switch. В таком случае ничего не происходит.
Самая распространенная форма команды switch получает целое число (или
аргумент числового типа, который может быть автоматически повышен до целочисленного типа), строку или перечисление (см. далее) — и выбирает между
несколькими ветвями case с альтернативами-константами1:
switch ( выражение )
{
case выражениеКонстанта :
команда;
[ case выражениеКонстанта :
команда; ]
...
[ default :
команда; ]
}

Выражения case для всех ветвей должны давать разные целочисленные константы или строки на стадии компиляции. Строки сравниваются методом equals()
класса String, который будет подробнее рассмотрен в главе 8. Команда в необязательной секции default выполняется, если ни одно из условий не совпало.
При выполнении команда switch находит ветвь, соответствующую условному
выражению (или ветвь default), и выполняет команду. Тем не менее это еще не
все. Как ни странно, команда switch затем продолжает выполнять другие ветви
(уже после ветви с совпадением), пока не дойдет до конца блока switch или до
специальной команды break. Пара примеров:
int value = 2;
switch( value ) {
case 1:
System.out.println( 1 );
case 2:
1

Поддержка строк в командах switch появилась в Java 7.

132  Глава 4. Язык Java

}

System.out.println( 2 );
case 3:
System.out.println( 3 );

// Выводятся 2 и 3

Чаще всего для завершения каждой ветви используется команда break:
int retValue = checkStatus();
switch ( retVal )
{
case MyClass.GOOD :
// Хороший вариант
break;
case MyClass.BAD :
// Плохой вариант
break;
default :
// Ни то ни другое
break;
}

В этом примере выполняется только одна ветвь — GOOD, BAD или default. «Сквозная» передача управления в switch нужна, когда вы хотите обработать несколько
возможных значений одной командой без использования лишних конструкций
if / else:
int value = getSize();
String size = "Unknown";
switch( value ) {
case MINISCULE:
case TEENYWEENIE:
case SMALL:
size = "Small";
break;
case MEDIUM:
size = "Medium";
break;
case LARGE:
case EXTRALARGE:
size = "Large";
break;
}
System.out.println("Your size is: " + size);

Здесь шесть возможных значений группируются на три случая. И теперь возможность группировки может использоваться напрямую в выражениях: в Java 12
появилась предварительная версия выражений switch. Например, приведенный

Команды и выражения  133

выше код можно модифицировать, создав переменную size, представляющую
размер:
int value = getSize();
String size = switch( value ) {
case MINISCULE:
case TEENYWEENIE:
case SMALL:
break "Small";
case MEDIUM:
break "Medium";
case LARGE:
case EXTRALARGE:
break "Large";
}
System.out.println("Your size is: " + size);

Обратите внимание: в этом случае команда break используется со значением.
Новый синтаксис также можно использовать в командах switch, чтобы код стал
компактнее и понятнее:
int value = getSize();
String size = switch( value ) {
case MINISCULE, TEENYWEENIE, SMALL -> "Small";
case MEDIUM -> "Medium";
case LARGE, EXTRALARGE -> "Large";
}
System.out.println("Your size is: " + size);

Эти выражения появились в языке относительно недавно (в Java 12 для работы
с ними приходилось компилировать программу с флагом --enable-preview),
так что они все еще редко встречаются в интернете. Но если команда switch
вас заинтересует, вы наверняка найдете хорошие примеры, объясняющие мощь
выражений switch.

Циклы do / while
Другая важная концепция, относящаяся к выбору команды, которая должна
выполняться следующей, — повторение. Компьютеры отлично справляются
с многократным выполнением одних и тех же действий. Блоки кода повторяются
в циклах. В Java существуют две основные разновидности циклов. Команды do
и while выполняются, пока логическое выражение возвращает значение true:
while ( условие )
команда;
do

команда;
while ( условие );

134  Глава 4. Язык Java
Цикл while идеально подходит для ожидания некоторого внешнего условия,
например получения электронной почты:
while( mailQueue.isEmpty() )
wait();

Конечно, метод ожидания wait() должен иметь ограничение (обычно ограничение по времени, например ожидание в течение секунды), чтобы он завершился
и позволил циклу выполниться снова. Но когда в очереди появится электронная
почта, обработать нужно будет все поступившие сообщения, а не одно. И снова
цикл while идеально подходит для этой задачи:
while( !mailQueue.isEmpty() ) {
EmailMessage message = mailQueue.takeNextMessage();
String from = message.getFromAddress();
System.out.println("Processing message from " + from);
message.doSomethingUseful();
}

В этом маленьком фрагменте логический оператор ! инвертирует результат
предыдущей проверки. То есть работа должна продолжаться в том случае, если
в очереди что-то есть (если не выполняется условие, что очередь пустая). Обратите внимание, что тело цикла состоит из нескольких команд, поэтому мы
заключили его в фигурные скобки. Внутри этих скобок мы извлекаем из очереди
сообщение и сохраняем его в локальной переменной message. Затем мы выполняем с переменной message некоторые действия, после чего цикл возвращается
к проверке условия пустой очереди. Если очередь не пустая, то весь процесс
повторяется, начиная со следующего доступного сообщения.
В отличие от цикла while и цикла for (см. далее), которые начинаются с проверки своих условий, цикл do-while (часто называемый циклом do) всегда выполняет свое тело хотя бы один раз. Классический пример — проверка ввода
от пользователя, например, на веб-сайте. Вы знаете, что вам нужно получить
некоторые данные, поэтому запрашиваете их в теле цикла. В условии цикла
можно проверять вводимые данные на наличие ошибок. При обнаружении
ошибки цикл перезапускается, а данные запрашиваются снова. Этот процесс
повторяется, пока запрос не будет обработан без ошибок, что обеспечивает
корректность полученных данных.

Цикл for
Самая общая форма цикла for является наследием языка C:
for ( инициализация; условие; приращение )
команда;

В секции инициализация могут объявляться или инициализироваться переменные, видимость которых ограничивается областью видимости команды.

Команды и выражения  135

Цикл for затем начинает серию проверок, в которых сначала проверяется условие, и если оно истинно, выполняется тело команды (или блок). После каждого
прохода тела цикла выполняются выражения из секции приращение, которая
дает возможность обновить переменные перед следующей итерацией:
for ( int i = 0; i < 100; i++ ) {
System.out.println( i );
int j = i;
...
}

В этом примере цикл выполняется 100 раз и выводит значения от 0 до 99. Обратите внимание на то, что переменная j локальна для блока (видна только в командах внутри этого блока) и становится недоступной в коде после цикла for.
Если условие цикла for возвращает false при первой проверке, то тело цикла
и секция приращение выполняться не будут.
В секциях инициализация и приращение цикла for можно использовать несколько выражений, разделенных запятыми. Пример:
for ( int i = 0, j = 10; i < j; i++, j-- ) {
System.out.println(i + " < " + j);
...
}

Также в блоке инициализации можно инициализировать существующие переменные, объявленные за пределами области видимости цикла for. Например,
это можно сделать, если вы хотите использовать конечное значение переменной
цикла в другом месте программы. Обычно не рекомендуется так поступать,
поскольку велик риск ошибок и ваш код может стать трудным для понимания.
Тем не менее такая возможность существует, поэтому вы можете столкнуться
с ситуацией, в которой эта логика работы покажется вам разумной.
int x;
for( x = 0; hasMoreValue(); x++ ) {
getNextValue();
}
// Значение x остается доступным
System.out.println( x );

Расширенный цикл for
Расширенный цикл for в Java похож на команду foreach в некоторых других
языках. Эта команда перебирает серию значений из массива или коллекцию
другого типа:
for ( объявлениеПеременной : значения )
команда;

136  Глава 4. Язык Java
Расширенный цикл for может использоваться для перебора массивов других
типов, а также любых объектов Java, реализующих интерфейс java.lang.
Iterable. К этой категории относится большинство классов из API коллекций
Java. Массивы будут рассматриваться в этой и следующей главах; в главе 7 рассматриваются коллекции Java. Пара примеров:
int [] arrayOfInts = new int [] { 1, 2, 3, 4 };
for( int i : arrayOfInts )
System.out.println( i );
List list = new ArrayList();
list.add("foo");
list.add("bar");
for( String s : list )
System.out.println( s );

Подчеркнем: в этом примере не обсуждаются массивы, класс List или специальный синтаксис. Здесь демонстрируется лишь расширенный цикл for с перебором массива целых чисел и списка строковых значений. Во втором случае
List реализует интерфейс Iterable, а следовательно, может использоваться
в расширенном цикле for.

Команды break / continue
Команда break и родственная ей команда continue могут использоваться для
ускоренного выхода из цикла или условной команды. Команда break заставляет
Java прервать текущую команду цикла (или switch) и продолжить выполнение
после нее. В следующем примере цикл while продолжается бесконечно, пока метод condition() не вернет true; в этом случае срабатывает команда break, которая
прерывает цикл и передает управление в точку с комментарием «после while»:
while( true ) {
if ( condition() )
break;
}
// После while

Команда continue заставляет циклы for и while перейти к следующей итерации,
передавая управление в точку проверки условия. Следующий код выводит числа
от 0 до 99, пропуская 33:
for( int i=0; i < 100; i++ ) {
if ( i == 33 )
continue;
System.out.println( i );
}

Команды и выражения  137

Команды break и continue похожи на одноименные команды языка C, но их
формы в Java могут получать в аргументе метку и передавать управление через
несколько уровней к помеченной точке кода. Такое использование нетипично
для современного программирования на Java, но может пригодиться в особых
случаях. Общая схема выглядит так:
labelOne:
while ( условие ) {
...
labelTwo:
while ( условие ) {
...
// Точка для команды break или continue
}
// После labelTwo
}
// После labelOne

Команды-контейнеры (блоки, условные команды, циклы и т. д.) могут помечаться метками, такими как labelOne и labelTwo. Если в данном примере поместить
в указанную точку команду break или continue без аргумента, то мы получим
такой же эффект, как в предыдущем примере: команда break приводит к продолжению выполнения в точке с комментарием «После labelTwo»; команда
continue немедленно заставляет цикл labelTwo вернуться к проверке условия.
Если в указанной точке находится команда break labelTwo, то она делает то же,
что и обычная команда break, но команда break labelOne переходит на два уровня
вверх и возобновляет выполнение в точке с комментарием «После labelOne».
Аналогичным образом команда continue labelTwo работает как обычная команда
continue, но команда continue labelOne возвращается к проверке цикла labelOne.
Многоуровневые команды break и continue устраняют главное оправдание для
столь порицаемой команды goto в языках C и C++1.
В Java есть и другие команды, которые мы пока не будем рассматривать. Команды
try, catch и finally используются для обработки исключений, как будет показано в главе 6. Команда synchronized предназначена для координации доступа
к командам между несколькими программными потоками; тема синхронизации
потоков рассматривается в главе 9.

Недоступные команды
И последнее замечание: компилятор Java помечает недоступные команды как
ошибки компиляции. Если компилятор выясняет, что какая-либо команда не
получит управления (не будет выполнена) ни при каких условиях, он помечает
1

Переход по именованным меткам считается признаком плохого стиля.

138  Глава 4. Язык Java
ее как недоступную. Конечно, некоторые методы могут ни разу не вызываться
в вашем коде, но компилятор обнаруживает только те из них, для которых он
может «доказать» недоступность на стадии компиляции. Например, метод,
в середине которого находится безусловная команда return, породит ошибку
компиляции, как и метод с заведомо невыполнимым условием:
if (1 < 2) {
// Эта ветвь выполняется всегда
System.out.println("1 is, in fact, less than 2");
return;
} else {
// Недоступные команды, эта ветвь никогда не выполняется
System.out.println("Look at that, seems we got \"math\" wrong.");
}

Выражения
При вычислении выражения вы получаете результат, то есть значение. Оно может
относиться к числовому типу (для арифметических выражений), к ссылоч­ному
типу (при создании объектов) или к специальному типу void, который объявляется для методов, не возвращающих значения. В последнем случае выражение
вычисляется только ради побочных эффектов, то есть для работы, выполняемой
не с целью получения значения. Тип выражения известен во время компиляции.
Значение, генерируемое во время выполнения, относится либо к этому типу, либо
(для ссылочных типов) к подтипу, совместимому по присваиванию.
Выражения уже встречались вам в примерах программ и фрагментах кода. Другие примеры выражений также встречаются в разделе «Присваивание» на с. 140.

Операторы
Операторы позволяют объединять или изменять выражения различными способами. В Java поддерживаются почти все стандартные операторы из языка C.
Эти операторы в Java обладают такими же приоритетами, как в C, что показано
в табл. 4.3.

Таблица 4.3. Операторы Java
Приоритет

Оператор

Тип операнда

Описание

1

++, --

Арифметический

Инкремент и декремент

1

+, -

Арифметический

Унарные плюс и минус

1

~

Целочисленный

Поразрядное отрицание

1

!

Логический

Логическое отрицание

Команды и выражения  139

Приоритет

Оператор

Тип операнда

Описание

1

( тип )

Любой

Преобразование типа

2

*, /, %

Арифметический

Умножение, деление, остаток

3

+, -

Арифметический

Сложение и вычитание

3

+

Строковый

Конкатенация строк

4

>

Целочисленный

Сдвиг вправо с расширением знака

4

>>>

Целочисленный

Сдвиг вправо без расширения

5

=

Арифметический

Числовое сравнение

5

instanceof

Объект

Сравнение типов

6

==, !=

Примитивный

Проверка равенства / неравенства значений

6

==, !=

Объект

Проверка равенства / неравенства ссылок

7

&

Целочисленный

Поразрядная операция AND

7

&

Логический

Логическая операция AND

8

^

Целочисленный

Поразрядная операция XOR

8

^

Логический

Логическая операция XOR

9

|

Целочисленный

Поразрядная операция OR

9

|

Логический

Логическая операция OR

10

&&

Логический

Условная операция AND

11

||

Логический

Условная операция OR

12

?:



Условный тернарный оператор

13

=

Любой

Присваивание

Заметим, что оператор вычисления остатка % может возвращать отрицательное
значение. Попробуйте поэкспериментировать с этими операторами в jshell,
чтобы лучше понять, как они работают. Если у вас еще нет опыта программирования, это будет особенно полезно, чтобы освоиться с операторами и их приоритетами. Выражения и операторы постоянно встречаются даже при решении
самых обыденных задач в вашем коде.
jshell> int x = 5
x ==> 5
jshell> int y = 12
y ==> 12

140  Глава 4. Язык Java
jshell> int sumOfSquares = x * x + y * y
sumOfSquares ==> 169
jshell> int explictOrder = (((x * x) + y) * y)
explictOrder ==> 444
jshell> sumOfSquares % 5
$7 ==> 4

В Java также добавлены некоторые новые операторы. Как вы уже видели, оператор + может использоваться со значениями String для выполнения конкатенации. Так как все целочисленные типы в Java имеют знак, оператор >> может
использоваться для выполнения операции арифметического сдвига вправо
с расширением знака. Оператор >>> интерпретирует операнд как число без знака
и выполняет арифметический сдвиг вправо без расширения знака. В Java операции с отдельными битами выполняются намного реже, чем в других языках,
поэтому вряд ли вы будете часто иметь дело с операторами сдвига. Если же они
встретятся вам в коде, который вам попадется в интернете, запустите jshell
и посмотрите, как они работают, или просто проанализируйте, как работает
код из примера. (Это одно из наших любимых применений jshell!) Оператор
new используется для создания объектов; он будет подробно рассмотрен далее.

Присваивание
Хотя инициализация переменной (то есть совмещение объявления с присваиванием) считается командой без возвращаемого значения, присваивание переменной само по себе является выражением:
int i, j; // Команда
i = 5; // Выражение и команда

Обычно присваивание используется ради его побочных эффектов, но оно также
может использоваться как значение в другой части выражения:
j = ( i = 5 );

Напомним, что чрезмерная зависимость от порядка вычислений (в данном случае использование составного присваивания в сложных выражениях) затрудняет
чтение кода и понимание его логики.

Значение null
Выражение null может быть присвоено любому ссылочному типу. Оно означает «ссылка отсутствует». Ссылка null не может использоваться для обращения к чему-либо, а любая попытка такого рода вызывает исключение
NullPointerException во время выполнения. Вспомните, о чем говорилось
в разделе «Ссылочные типы» на с. 124: null — это значение, присваиваемое по

Команды и выражения  141

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

Обращения к переменным
Оператор «точка» (.) используется для выбора составляющих класса или экземпляра. (Они будут более подробно рассмотрены в следующих главах.) Он
может использоваться, чтобы получить значение переменной экземпляра (для
объекта) или статической переменной (для класса). Также он может задавать
метод, который должен вызываться для объекта или класса:
int i = myObject.length;
String s = myObject.name;
myObject.someMethod();

Выражение ссылочного типа может использоваться в сложных вычислениях
для выбора переменных или методов результата:
int len = myObject.name.length();
int initialLen = myObject.name.substring(5, 10).length();

Здесь мы определяем длину переменной name, вызывая метод length() объекта
String. Во втором случае выполняется промежуточный шаг, на котором мы
запрашиваем подстроку строки name. Метод substring класса String также возвращает ссылку на объект String, длину которого мы запрашиваем. Подобные
составные операции также называются сцепленными вызовами методов (см.
далее). Одна из таких операций, которую мы часто использовали, — это вызов
метода println() для переменной out класса System:
System.out.println( "calling println on out" );

Вызов методов
Методы — это функции, существующие внутри класса; они могут вызываться
на уровне класса или на уровне отдельного экземпляра (в зависимости от разновидности метода). Вызов метода означает выполнение команд, составляющих
его тело, с передачей всех необходимых параметров и, возможно, с возвращением
значения. Вызов метода является выражением, которое возвращает значение.
Тип этого значения называется типом возвращаемого значения метода:
System.out.println( "Hello, World..." );
int myLength = myString.length();

Здесь методы println() и length() вызываются для разных объектов. Метод
length() возвращает целое значение; метод println() возвращает void (зна-

142  Глава 4. Язык Java
чение отсутствует). Стоит заметить, что println() генерирует вывод, но не
возвращает никакого значения. Этот метод нельзя присвоить переменной, как
делалось выше с length().
jshell> String myString = "Hi there!"
myString ==> "Hi there!"
jshell> int myLength = myString.length()
myLength ==> 9
jshell> int mistake = System.out.println("This is a mistake.")
| Error:
| incompatible types: void cannot be converted to int
| int mistake = System.out.println("This is a mistake.");
|
^--------------------------------------^

Методы образуют основную часть программ Java. Хотя вы можете писать некоторые простые приложения, полностью укладывающиеся в единственный
в классе метод main(), вам вскоре станет ясно, что лучше разбивать код на
части. Методы не только делают код приложения более наглядным — они
открывают путь к сложным, интересным и полезным приложениям, которые
попросту невозможны без них. Вспомните наши графические приложения
в разделе «HelloJava» на с. 66. В них использовались методы, определенные
для класса JFrame.
Все примеры пока были простыми, но в главе 5 вы увидите, что ситуация усложняется, когда в одном классе есть одноименные методы с разными типами
параметров или когда метод переопределяется в субклассе.

Команды, выражения и алгоритмы
Итак, создадим набор команд и выражений разных типов для решения практической задачи… А проще говоря, напишем код Java для реализации алгоритма.
Классическим примером считается алгоритм Евклида: нахождение наибольшего
общего делителя двух чисел в простом (хотя и однообразном) процессе многократного вычитания. Для этого нам понадобится цикл while, условная команда
if / else и несколько присваиваний:
int a = 2701;
int b = 222;
while (b != 0) {
if (a > b) {
a = a - b;
} else {
b = b - a;
}
}
System.out.println("GCD is " + a);

Команды и выражения  143

Выглядит не слишком впечатляюще, но работает — именно с такими задачами
лучше всего справляются компьютерные программы. И как раз для этого вы
и нужны! Мы не хотим сказать, что вы были созданы для нахождения наибольшего общего делителя чисел 2701 и 222 (кстати, он равен 37), но вся суть вашей
работы — формулировка решений задач в виде алгоритмов и преобразование
этих алгоритмов в исполняемый код Java. Надеемся, что еще несколько частей
головоломки встали на свои места. Но даже если эти идеи вам не до конца понятны, не огорчайтесь. Процесс программирования требует серьезной практики.
Попробуйте заключить приведенный выше блок кода в класс Java внутри метода
main(). Попробуйте изменить значения a и b. В главе 8 мы займемся преобразованием строк в числа, что позволит вам находить наибольший общий делитель
любых чисел, повторно запуская программу без перекомпиляции и передавая
ей оба числа в параметрах метода main().

Создание объектов
Объекты в Java создаются оператором new:
Object o = new Object();

Аргументом оператора new является конструктор класса. Конструктор представляет собой метод, имя которого всегда совпадает с именем класса. Конструктор задает все необходимые параметры для создания объекта. Значением
выражения new является ссылка с типом создаваемого объекта. Объекты всегда
имеют один или несколько конструкторов, хотя эти конструкторы не всегда
доступны для вас.
Создание объектов подробно рассматривается в главе 5. А пока достаточно
заметить, что создание объекта является выражением, результат которого —
ссылка на этот объект. Небольшая странность заключается в том, что в другой
синтаксической форме результатом выражения с оператором new становится
возвращаемое значение метода, вызванного одновременно с созданием объекта. При необходимости вы можете создать новый объект и одновременно
вызвать его метод, но не присваивать этот объект какой-либо переменной
ссылочного типа:
int hours = new Date().getHours();

Вспомогательный класс Date здесь используется для представления текущего
времени. Мы создаем новый экземпляр Date оператором new и вызываем его
метод getHours() для получения текущего часа в виде целочисленного значения.
Ссылка на объект Date существует достаточно долго, чтобы обеспечить вызов
метода, после чего она пропадает и уничтожается уборщиком мусора в какойто момент будущего (за информацией об уборке мусора обращайтесь к разделу
«Уборка мусора» на с. 181).

144  Глава 4. Язык Java
Еще раз подчеркнем, что такой вызов методов по ссылкам на объекты является
делом стиля. Конечно, понятнее был бы другой вариант: создать промежуточную переменную типа Date для хранения нового объекта, а потом вызвать его
метод getHours(). Тем не менее подобные объединения операций встречаются
достаточно часто. Когда вы освоите язык Java и будете уверенно чувствовать
себя с его классами и типами, вероятно, вы возьмете на вооружение некоторые из
этих приемов. А пока не беспокойтесь о том, что ваш код получается «слишком
длинным». После знакомства со всем материалом этой книги вы поймете, как
важно, чтобы код был «чистым» и простым для понимания.

Оператор instanceof
Оператор instanceof может использоваться для определения типа объекта во
время выполнения программы. Он проверяет, относится ли объект к целевому
типу или к одному из его подтипов. (Еще раз: скоро мы расскажем об иерархиях
классов!) Это то же самое, что спросить, можно ли присвоить объект переменной
целевого типа. Целевым типом может быть тип класса, интерфейса или массива,
как будет показано далее. Оператор instanceof возвращает логическое значение,
которое указывает, относится ли объект к этому типу:
boolean b;
String str = "foo";
b = ( str instanceof String ); // true, так как str относится к типу String
b = ( str instanceof Object ); // Тоже true, так как String — это Object
// b = ( str instanceof Date ); // Компилятор видит, что типы разные!

Оператор instanceof также правильно сообщает, относится ли объект к типу
массива или заданного интерфейса (об этом будет рассказано далее):
if ( foo instanceof byte[] )
...

Важно заметить, что значение null не является типом какого-либо класса. Следующая проверка возвращает false независимо от объявленного типа переменных:
String s = null;
if ( s instanceof String )
// false, так как null не является экземпляром чего-либо

Массивы
Массив — это особая разновидность объекта, предназначенная для хранения
упорядоченного набора элементов. Тип элементов массива называется базовым
типом массива; количество хранящихся в нем элементов называется длиной
массива. Java поддерживает массивы всех примитивных и ссылочных типов.

Массивы  145

Если вы занимались программированием на C или C++, то базовый синтаксис
массивов выглядит очень знакомо. Мы создаем массив заданной длины и обращаемся к его элементам при помощи оператора индексирования []. Но в отличие
от других языков, массивы в Java являются полноценными объектами. Массив
является экземпляром специального класса Java array и имеет соответствующий
тип в системе типов. Это означает, что для использования массива, как и любого
другого объекта, необходимо сначала объявить переменную соответствующего
типа, а затем создать его экземпляр оператором new.
Объекты массивов отличаются от других объектов Java в трех отношениях:
Каждый раз, когда мы объявляем новый тип массива, Java неявно создает
специальный тип класса массива. Чтобы пользоваться массивами, не обязательно знать этот процесс во всех тонкостях, но он поможет понять структуру массивов и их отношения с другими объектами в Java.
Для обращения к элементам массивов используется оператор [], чтобы
операции с массивами выглядели привычно. Вы также можете реализовать
собственные классы, которые будут действовать как массивы, но при этом
вместо специальной записи [] будут использоваться методы get() и set().
Java предоставляет специальную форму оператора new, которая позволяет
сконструировать экземпляр массива заданной длины с записью [] или инициализировать его напрямую структурированным списком значений.

Типы массивов
Переменная типа массива обозначается его базовым типом, за которым следуют
пустые квадратные скобки [].Также Java поддерживает объявления массива
в стиле языка C, с квадратными скобками после имени массива.
Следующие объявления массива эквивалентны:
int [] arrayOfInts; // Рекомендуется
int arrayOfInts []; // В стиле C

В обоих случаях arrayOfInts объявляется как массив целых чисел. В этот момент размер массива еще не является проблемой, потому что мы объявляем
только переменную, имеющую тип массива. Реальный экземпляр класса array
еще не создан, память для него еще не выделена. При объявлении переменной
с типом массива даже невозможно задать длину массива. Размер определяется
исключительно самим объектом массива, а не ссылкой на него.
Массивы ссылочных типов создаются аналогичным способом:
String [] someStrings;
Button [] someButtons;

146  Глава 4. Язык Java

Создание и инициализация массива
Для создания экземпляра массива используется оператор new. После оператора new указываются базовый тип массива и его длина, определяемая целочисленным выражением в квадратных скобках:
arrayOfInts = new int [42];
someStrings = new String [ number + 2 ];

Конечно, объявление массива можно совместить с созданием объекта:
double [] someNumbers = new double [20];
Component [] widgets = new Component [12];

Индексы в массиве начинаются с нуля. Таким образом, первому элементу
someNumbers[] соответствует индекс 0, а последнему — индекс 19. После создания
все элементы массива инициализируются значением по умолчанию для своего
типа. Для числовых типов это означает, что все элементы изначально равны нулю:
int [] grades = new int [30];
grades[0] = 99;
grades[1] = 72;
// grades[2] == 0

Элементы массива объектов содержат ссылки на объекты, как и отдельные переменные, на которые они ссылаются, но не содержат фактических экземпляров
объектов. Следовательно, значением по умолчанию для каждого элемента будет
null, пока вы присвоите значения экземплярам соответствующих объектов:
String names [] = new String [4];
names [0] = new String();
names [1] = "Walla Walla";
names [2] = someObject.toString();
// names[3] == null

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

Аналогом в C или C++ является массив указателей на объекты. Указатели в этих языках
являются 2-байтовыми или 4-байтовыми значениями. Создание массива указателей
сводится к выделению памяти для некоторого числа объектов-указателей. Массив
ссылок устроен похожим образом, но ссылки не являются объектами. Мы не можем
манипулировать ссылками или их частями, кроме как путем присваивания, а объем
памяти для ссылок не регламентируется в высокоуровневой спецификации языка Java.

Массивы  147

самих этих ссылок (пустых «слотов» массива). На рис. 4.3 показан массив names
из предыдущего примера.

Рис. 4.3. Массив Java
Здесь names — это переменная типа String[], то есть массив строк. Этот конкретный объект String[] содержит четыре переменные типа String. Мы присвоили
объекты String первым трем элементам массива, а четвертый содержит значение
по умолчанию null.
Java поддерживает конструкцию с фигурными скобками { } (в стиле языка C)
для создания массива и инициализации его элементов:
int [] primes = { 2, 3, 5, 7, 7+4 }; // Например, primes[2] = 5

При этом неявно создается объект массива с соответствующим типом и длиной, а значения списка выражений, разделенные запятыми, присваиваются его
элементам. Обратите внимание: ключевое слово new и тип массива в данном
случае не нужны. Тип массива был автоматически определен по присваиванию.
Синтаксис { } может использоваться с массивами объектов. В этом случае при
вычислении каждого выражения должен быть получен объект, который присваивается переменной базового типа массива, или значение null. Вот несколько
примеров:
String [] verbs = { "run", "jump", someWord.toString() };
Button [] controls = { stopButton, new Button("Forwards"),
new Button("Backwards") };
// Все типы являются подтипами Object
Object [] objects = { stopButton, "A word", null };

Следующие команды эквивалентны:
Button [] threeButtons = new Button [3];
Button [] threeButtons = { null, null, null };

148  Глава 4. Язык Java

Использование массивов
Размер объекта массива хранится в открытой переменной length:
char [] alphabet = new char [26];
int alphaLen = alphabet.length; // alphaLen == 26
String [] musketeers = { "one", "two", "three" };
int num = musketeers.length; // num == 3

Здесь length — единственное доступное поле массива; это переменная, а не метод.
(Не беспокойтесь: компилятор сообщит вам, если вы случайно добавите круглые
скобки как при вызове метода; время от времени такое бывает с каждым.)
Обращения к массивам в Java практически не отличаются от обращений к массивам в других языках. Чтобы обратиться к элементу, укажите выражение
с целым значением в квадратных скобках после имени массива. Следующий
пример создает массив объектов Button с именем keyPad, после чего заполняет
его объектами Button:
Button [] keyPad = new Button [10];
for ( int i=0; i < keyPad.length; i++ )
keyPad[ i ] = new Button( Integer.toString( i ) );

Не забывайте, что для перебора значений в массиве также можно воспользоваться расширенным циклом for. В следующем примере он используется для
вывода всех только что присвоенных значений:
for (Button b : keyPad)
System.out.println(b);

При попытке обратиться к элементу за пределами массива генерируется исключение ArrayIndexOutOfBoundsException. Оно относится к типу RuntimeException,
так что вы можете либо перехватить и обработать его самостоятельно, если
ожидаете его возникновения, либо просто проигнорировать (см. главу 6). Этот
пример дает некоторое представление о синтаксисе try / catch, который нужен
для фрагментов кода с потенциальными проблемами:
String [] states = new String [50];
try {
states[0] = "California";
states[1] = "Oregon";
...
states[50] = "McDonald's Land"; // Ошибка: выход за границу массива
}
catch ( ArrayIndexOutOfBoundsException err ) {
System.out.println( "Handled error: " + err.getMessage() );
}

Массивы  149

На практике часто требуется скопировать диапазон элементов из одного массива
в другой. Один из способов копирования массивов основан на использовании
низкоуровневого метода arraycopy() класса System:
System.arraycopy( source, sourceStart, destination, destStart, length );

В следующем примере массив names из предыдущего примера увеличивается
вдвое:
String [] tmpVar = new String [ 2 * names.length ];
System.arraycopy( names, 0, tmpVar, 0, names.length );
names = tmpVar;

Новый массив, размер которого вдвое больше размера names, создается и присваивается временной переменной tmpVar. Затем метод arraycopy() используется для копирования элементов names в новый массив. Наконец, новый массив
присваивается переменной names. Если после копирования names не остается ни
одной ссылки на старый объект массива, он будет уничтожен в ходе следующей
уборки мусора.
В другом, более простом способе копирования используются методы java.util.
ArrayscopyOf() и copyOfRange():
byte [] bar = new byte[] { 1, 2, 3, 4, 5 };
byte [] barCopy = Arrays.copyOf( bar, bar.length );
// { 1, 2, 3, 4, 5 }
byte [] expanded = Arrays.copyOf( bar, bar.length+2 );
// { 1, 2, 3, 4, 5, 0, 0 }
byte [] firstThree = Arrays.copyOfRange( bar, 0, 3 );
// { 1, 2, 3 }
byte [] lastThree = Arrays.copyOfRange( bar, 2, bar.length );
// { 3, 4, 5 }
byte [] lastThreePlusTwo = Arrays.copyOfRange( bar, 2, bar.length+2 );
// { 3, 4, 5, 0, 0 }

Метод copyOf() получает исходный массив и целевую длину. Если длина целевого массива больше длины исходного, то новый массив дополняется (нолями
или null) до нужной длины. Метод copyOfRange() получает начальный индекс
(включается в копируемый фрагмент) и конечный индекс (не включается),
а также требуемую длину, которая тоже будет дополнена в случае необходимости.

Анонимные массивы
Часто бывает удобно создавать «одноразовые» массивы, то есть массивы, которые
используются в одной точке программы (к которым программа больше нигде не
обращается). Таким массивам не нужны имена, потому что вы никогда не будете

150  Глава 4. Язык Java
снова ссылаться на них в этом контексте. Например, вы можете создать коллекцию
объектов для передачи в аргументе некоторому методу. Создать обычный именованный массив несложно, но поскольку вы никогда не будете реально работать
с этим массивом (он нужен только для хранения коллекции), лучше так не делать.
Java позволяет легко создавать «анонимные» (то есть неименованные) массивы.
Допустим, вы хотите вызвать метод с именем setPets(), которому в аргументах передается массив объектов Animal. Классы Cat и Dog являются субклассами Animal,
поэтому вызов setPets() с помощью анонимного массива будет выглядеть так:
Dog pokey = new Dog ("gray");
Cat boojum = new Cat ("grey");
Cat simon = new Cat ("orange");
setPets ( new Animal [] { pokey, boojum, simon });

Синтаксис похож на инициализацию массива в объявлении переменной. Мы неявно определяем размер массива и заполняем его данными в фигурных скобках.
Тем не менее, поскольку это не является объявлением переменной, для создания
объекта массива надо явно использовать оператор new и тип массива.
Анонимные массивы иногда используются в качестве замены для списковаргументов переменной длины. Списки аргументов переменной длины (вероятно,
знакомые программистам на языке C) предназначены для передачи методу
произвольного объема данных. Представьте метод для вычисления среднего
арифметического группы чисел. Вы можете поместить все числа в один массив
или же сделать так, чтобы метод мог получать в аргументах одно, два, три числа
и более. С появлением в Java списков аргументов переменной длины1 полезность
анонимных массивов заметно сократилась.

Многомерные массивы
Многомерные массивы поддерживаются в Java в форме массивов объектов,
имеющих тип массива. Синтаксис многомерных массивов напоминает синтаксис языка C: в нем используется несколько пар квадратных скобок, по одной
для каждого измерения. Этот синтаксис может использоваться для обращения
к элементам в разных позициях массива. Пример многомерного массива, представляющего шахматную доску:
ChessPiece [][] chessBoard;
chessBoard = new ChessPiece [8][8];
chessBoard[0][0] = new ChessPiece.Rook;
chessBoard[1][0] = new ChessPiece.Pawn;
...
1

Если эта идея кажется вам интересной, обратитесь к технической документации Oracle
по этой теме. Попробуйте провести поиск в интернете по словам «Oracle varargs».

Массивы  151

Здесь chessBoard объявляется как переменная типа ChessPiece[][] (то есть массив массивов ChessPiece). Объявление также неявно создает тип ChessPiece[].
Этот пример демонстрирует специальную форму оператора new, используемую
для создания многомерных массивов. Он создает массив объектов ChessPiece[],
а затем поочередно создает все элементы в массиве объектов ChessPiece. Затем
chessBoard индексируется для определения значений отдельных элементов
ChessPiece. (Цвет фигур нас пока не интересует.)
Конечно, массивы могут создаваться и более чем с двумя измерениями. Приведем несколько нереальный пример:
Color [][][] rgbCube = new Color [256][256][256];
rgbCube[0][0][0] = Color.black;
rgbCube[255][255][0] = Color.yellow;
...

Мы можем указать лишь часть индексов многомерного массива (например,
только индекс первого уровня), чтобы получить подмассив с меньшей размерностью. В нашем примере переменная chessBoard имеет тип ChessPiece[][].
Выражение chessBoard[0] обозначает первый элемент chessBoard, который
в Java имеет тип ChessPiece[]. Например, шахматную доску можно заполнять
по строкам:
ChessPiece [] homeRow = {
new ChessPiece("Rook"), new ChessPiece("Knight"),
new ChessPiece("Bishop"), new ChessPiece("King"),
new ChessPiece("Queen"), new ChessPiece("Bishop"),
new ChessPiece("Knight"), new ChessPiece("Rook")
};
chessBoard[0] = homeRow;

Не обязательно задавать размеры измерений в многомерном массиве одной
операцией new. Синтаксис оператора new позволяет оставить размеры некоторых
измерений неопределенными. Размер по крайней мере первого (самого важного)
измерения массива должен быть задан обязательно, но размеры последующих,
менее значимых измерений можно оставить без определения. Соответствующие
значения с типами массивов можно задать впоследствии.
Таким способом можно создать шашечную доску, поля которой представлены
логическими значениями (для реальной игры в шашки такой доски будет недостаточно):
boolean [][] checkerBoard;
checkerBoard = new boolean [8][];

Здесь объявляется и создается массив checkerBoard, но его элементы — восемь
объектов boolean[] следующего уровня — остаются пустыми. Таким образом,

152  Глава 4. Язык Java
например, checkerBoard[0] содержит null до того момента, когда мы явно создадим массив и присвоим его:
checkerBoard[0] = new boolean [8];
checkerBoard[1] = new boolean [8];
...
checkerBoard[7] = new boolean [8];

Код двух предыдущих примеров эквивалентен следующему:
boolean [][] checkerBoard = new boolean [8][8];

Иногда вы можете захотеть оставить размеры массива неопределенными.
Например, для того, чтобы сохранять массивы, передаваемые вам другим
методом.
Поскольку длина массива не является частью его типа, представляющие шашечную доску массивы не обязаны иметь одинаковую длину; иначе говоря,
многомерные массивы не обязаны быть «прямоугольными». Например, так
можно представить «сломанную» (но совершенно законную с точки зрения
Java) шашечную доску:
checkerBoard[2] = new boolean [3];
checkerBoard[3] = new boolean [10];

А так создается и инициализируется «треугольный» массив:
int [][] triangle = new int [5][];
for (int i = 0; i < triangle.length; i++) {
triangle[i] = new int [i + 1];
for (int j = 0; j < i + 1; j++)
triangle[i][j] = i + j;
}

Типы, классы, массивы... и jshell
Java поддерживает много разных типов данных, и для каждого из них есть собственный способ представления битами или байтами в памяти. Со временем вы
начнете уверенно работать с int, double, char и String. Но не спешите — именно
для исследования этих фундаментальных элементов языка создавалась оболочка jshell. Не жалейте времени и проверяйте, правильно ли вы понимаете,
что может храниться в той или иной переменной. Очень полезны эксперименты
с массивами. Перепробуйте разные способы объявления массивов и убедитесь
в том, что вы хорошо понимаете, как обращаться к отдельным элементам в одномерных и многомерных структурах.

Типы, классы, массивы... и jshell  153

В jshell также можно экспериментировать с простыми управляющими коман­
дами, например с условными командами if и циклами while. Для ввода многострочных фрагментов кода потребуется немного терпения, но такие эксперименты и практические упражнения исключительно полезны, чтобы ваш мозг
запомнил как можно больше элементов языка Java. Конечно, языки программирования проще человеческих языков, но у тех и у других есть много общего.
Грамотное владение Java приобретается точно так же, как и грамотное владение
английским языком (или тем языком, на котором вы читаете эту книгу). Вскоре
вы начнете приблизительно понимать, что делает тот или иной код, даже если
вы не понимаете всех его нюансов.
А у некоторых частей Java, в частности у массивов, таких нюансов очень много.
Ранее мы упоминали, что массивы в Java являются экземплярами специальных классов. Если у массивов есть соответствующие классы, то какое место
они занимают в иерархии классов и как они связаны между собой? Это хорошие вопросы, но прежде чем отвечать на них, необходимо лучше разобраться
в ­объектно-ориентированных аспектах Java. Этой теме посвящена следующая
глава. А пока поверьте на слово, что массивы занимают свое место в иерархии
классов.

ГЛАВА 5

Объекты в Java

Пора добраться до самой сути Java и исследовать объектно-ориентированные
аспекты этого языка. Термином «объектно-ориентированное проектирование»
называется искусство разбиения приложения на объекты — самостоятельные
компоненты приложения, которые работают совместно. Цель этой процедуры — разбиение задачи на множество мелких задач, более понятных и простых
в решении. За много лет объектно-ориентированные архитектуры доказали свою
полезность, а объектно-ориентированные языки (такие, как Java) стали хорошей
основой для написания приложений — от очень маленьких до очень больших.
Язык Java с самого начала проектировался как объектно-ориентированный, и все
API и библиотеки Java создавались на базе проверенных паттернов объектноориентированного проектирования.
Объектно-ориентированная методология — это система или набор правил, упрощающих разбиение приложения на объекты. Часто это означает отображение
концепций и сущностей из реального мира (иногда называемых «предметной областью задачи») на компоненты приложения. Различные методологии упрощают
разложение приложения на объекты, удобные для повторного использования.
Теоретически это звучит хорошо, но проблема в том, что хорошее объектноориентированное проектирование ближе к искусству, чем к науке. Хотя из
существующих методологий проектирования можно узнать немало полезного,
ни одна из них не поможет вам во всех ситуациях. Дело в том, что ничто не заменит практического опыта.
Мы не будем пытаться рекомендовать вам конкретную методологию; на эту
тему и так написано множество книг1. Вместо этого мы ограничимся советами
из области здравого смысла, пока вы будете делать первые шаги.

1

Когда вы начнете разбираться в объектно-ориентированных концепциях, вам пригодится книга Эриха Гаммы (Erich Gamma) и соавторов «Паттерны объектно-ориентированного проектирования». В ней перечислены полезные решения, доведенные до
совершенства за годы практического использования. Многие из них есть в Java API.

Классы  155

Классы
Классы являются структурными элементами приложений Java. Класс может содержать методы (функции), переменные, код инициализации и, как будет показано
далее, другие классы. Отдельные классы, описывающие отдельные части более
сложной концепции, часто объединяются в пакеты, что улучшает структуру больших проектов. (Каждый класс принадлежит некоторому пакету, даже в простых
примерах, которые мы рассматривали.) Интерфейс может описывать некоторые
сходные аспекты классов, непохожих во всем остальном. Классы могут быть связаны друг с другом путем расширения, а также с интерфейсами — путем реализации.
На рис. 5.1 наглядно представлены основные идеи этого очень короткого абзаца.
Класс
расширяет

Интерфейс
Пакет

расширяет

реализует
расширяет

Рис. 5.1. Классы, интерфейсы и пакеты
На схеме в левом верхнем углу изображен класс Object. Это фундаментальный
класс и на нем основаны все остальные классы Java. Класс Object является
частью фундаментального пакета Java java.lang. В Java также входит пакет
GUI-элементов (элементов графического интерфейса), который называется
javax.swing. Внутри этого пакета класс JComponent определяет все низкоуровневые общие свойства таких графических объектов, как окна (фреймы), кнопки
и холсты (canvases). Например, класс JLabel расширяет класс JComponent. Это
означает, что JLabel наследует некоторые детали от JComponent, но добавляет
к ним собственную функциональность, специфичную для надписей. Как видите, класс JComponent является расширением класса Object — возможно, не напрямую, а через промежуточные классы. Для краткости мы не стали указывать
промежуточные классы и пакеты.

156  Глава 5. Объекты в Java
Программист может определять свои собственные классы и пакеты. Пакет ch05
в правом нижнем углу — нестандартный пакет, который был написан нами.
В этом пакете содержатся такие игровые классы, как Apple и Field. Также в нем
содержится интерфейс GamePiece с некоторыми общими обязательными элементами всех игровых фигур; этот интерфейс реализуется классами Apple, Tree
и Physicist. (В нашей игре класс Field представляет собой поле, на котором
будут выводиться игровые фигуры, но само поле фигурой не является и поэтому
не реализует интерфейс GamePiece.)
Все эти концепции подробно рассматриваются и поясняются примерами в этой
главе. Важно, чтобы вы опробовали все примеры в работе и воспользовались
программой jshell (см. раздел «Первые эксперименты с Java», с. 98), чтобы
закрепить понимание новых тем.

Объявление классов и создание экземпляров
Класс служит своего рода «чертежом» для создания экземпляров — работающих
в выполняемой программе объектов (отдельных копий), реализующих структуру класса. Класс объявляется с ключевым словом class и с выбранным вами
именем. Например, в нашей игре физики бросают яблоки в деревья. Каждое
существительное в этой фразе становится хорошим кандидатом для создания
класса, который его будет представлять. Внутри класса мы создаем переменные
со служебными данными и с другой полезной информацией, а также методы,
которые описывают, что можно делать с экземплярами этого класса.
Начнем с класса, представляющего яблоки. Имена классов принято начинать с заглавной буквы — это считается обязательным! Таким образом, для этого класса
хорошо подойдет имя Apple. Пока мы не будем заталкивать в этот класс все, что
нужно знать о яблоках в нашей игре, и ограничимся несколькими элементами,
которые показывают взаимодействие между классами, переменными и методами:
package ch05;
class Apple {
float mass;
float diameter = 1.0f;
int x, y;

}

boolean isTouching(Apple other) {
...
}
...

Класс Apple содержит четыре переменные: mass, diameter, x и y. Он также определяет метод isTouching(), который получает в аргументе ссылку на другой

Классы  157

экземпляр Apple и возвращает логическое значение boolean. Объявления переменных и методов могут следовать в любом порядке, но при инициализации
любой переменной нельзя использовать «опережающие ссылки» на другие
переменные, которые появятся только в последующих строках. (В нашем маленьком фрагменте переменная diameter может использовать переменную mass
для вычисления исходного значения, но mass не может использовать diameter
для той же цели.) После того как класс Apple будет определен, можно будет
создать объект Apple (экземпляр этого класса):
Apple a1;
a1 = new Apple();
// Или в одну строку:
Apple a2 = new Apple();

Вспомните, что объявление переменной a1 не создает объект Apple; оно только
создает переменную, которая ссылается на объект типа Apple. Затем вы должны
создать объект ключевым словом new, как показано во второй строке приведенного выше фрагмента кода. Однако эти два шага можно объединить в одну строку,
как это сделано для переменной a2. Конечно, однострочная запись ничего не
изменит в программе: эти шаги все равно будут выполняться по отдельности.
Просто объединение объявления с инициализацией иногда нагляднее.
После того как объект Apple будет создан, мы можем обращаться к его переменным и методам, как было показано в примерах из главы 4 и даже в графическом
приложении из раздела «HelloJava» на с. 66. Например, хотя это не очень впечатляет, мы теперь можем написать другой класс PrintAppleDetails, который
является завершенным приложением для создания экземпляра Apple и вывода
информации о нем:
package ch05;
public class PrintAppleDetails {
public static void main(String args[]) {
Apple a1 = new Apple();
System.out.println("Apple a1:");
System.out.println(" mass: " + a1.mass);
System.out.println(" diameter: " + a1.diameter);
System.out.println(" position: (" + a1.x + ", " + a1.y +")");
}
}

Если вы скомпилируете и выполните этот пример, то в терминальном приложении или в окне терминала вашей IDE появится следующий результат:
Apple a1:
mass: 0.0
diameter: 1.0
position: (0, 0)

158  Глава 5. Объекты в Java
Но почему выводится нулевое значение mass? Вспомните, как мы объявили переменные для класса Apple: там инициализируется только значение переменной
diameter. Всем остальным переменным по умолчанию присваивается значение 0,
так как они относятся к числовым типам. (На всякий случай: логическим переменным по умолчанию присваивается значение false, а ссылочным — null.)
Конечно, лучше было бы сделать более интересное «яблоко». Давайте посмотрим, как добавить к нему дополнительные данные.

Обращение к полям и методам
После того как у вас появится ссылка на объект, вы можете использовать его
переменные и методы в точечной записи, как было показано в главе 4. Создадим
новый класс PrintAppleDetails2, укажем значения mass и position для экземпляра a1, после чего выведем его данные:
package ch05;
public class PrintAppleDetails2 {
public static void main(String args[]) {
Apple a1 = new Apple();
System.out.println("Apple a1:");
System.out.println(" mass: " + a1.mass);
System.out.println(" diameter: " + a1.diameter);
System.out.println(" position: (" + a1.x + ", " + a1.y +")");
// Заполнение данных a1
a1.mass = 10.0f;
a1.x = 20;
a1.y = 42;
System.out.println("Updated a1:");
System.out.println(" mass: " + a1.mass);
System.out.println(" diameter: " + a1.diameter);
System.out.println(" position: (" + a1.x + ", " + a1.y +")");
}
}

Вот что у нас получилось:
Apple a1:
mass: 0.0
diameter: 1.0
position: (0, 0)
Updated a1:
mass: 10.0
diameter: 1.0
position: (20, 42)

Превосходно! Теперь a1 выглядит немного лучше. Но давайте посмотрим на
код еще раз. Нам пришлось повторить три строки для вывода данных объекта.

Классы  159

Подобные буквальные повторения подсказывают, что здесь будет уместно
определить метод. Методы позволяют выполнять различные операции внутри
класса; они подробно рассматриваются в разделе «Методы», с. 165. Доработаем
класс Apple и включим в него следующие команды вывода:
public class Apple {
float mass;
float diameter = 1.0f;
int x, y;
// ...
public void printDetails() {
System.out.println(" mass: " + mass);
System.out.println(" diameter: " + diameter);
System.out.println(" position: (" + x + ", " + y +")");
}
}

// ...

После перемещения команд вывода в метод класс PrintAppleDetails3 создается
более компактно, чем его предшественник:
package ch05;
public class PrintAppleDetails3 {
public static void main(String args[]) {
Apple a1 = new Apple();
System.out.println("Apple a1:");
a1.printDetails();
// Заполнение данных a1
a1.mass = 10.0f;
a1.x = 20;
a1.y = 42;
System.out.println("Updated a1:");
a1.printDetails();
}
}

Еще раз взгляните на метод printDetails(), добавленный в класс Apple. В коде
класса можно обращаться к переменным этого класса и вызывать его методы
прямо по их именам. В командах вывода на экран используются простые имена,
такие как mass и diameter. Или возьмем метод isTouching(): координаты x и y
можно использовать без специального префикса. Но чтобы обратиться к координатам какого-то другого объекта Apple, придется воспользоваться точечной
записью. Ниже приведена реализация этого метода с математическими вычислениями (подробности — в разделе «Класс java.lang.Math», с. 287) и с командами
if / else, рассмотренными в разделе «Условные команды if / else», с. 130:

160  Глава 5. Объекты в Java
// Файл: ch05/Apple.java
public boolean isTouching(Apple other) {
double xdiff = x - other.x;
double ydiff = y - other.y;
double distance = Math.sqrt(xdiff * xdiff + ydiff * ydiff);
if (distance < diameter / 2 + other.diameter / 2) {
return true;
} else {
return false;
}
}

Продолжим работу над игрой и создадим класс Field, использующий несколько
объектов Apple. Он создает эти объекты в виде переменных экземпляра и работает с этими объектами с помощью методов setupApples() и detectCollision(),
вызывая методы Apple и обращаясь к переменным этих объектов по ссылкам a1
и a2 (рис. 5.2).

Экземпляр
Класс

Рис. 5.2. Экземпляры класса Apple
package ch05;
public class Field {
Apple a1 = new Apple();
Apple a2 = new Apple();
public void setupApples() {
a1.diameter = 3.0f;
a1.mass = 5.0f;

Классы  161

}

}

a1.x = 20;
a1.y = 40;
a2.diameter = 8.0f;
a2.mass = 10.0f;
a2.x = 70;
a2.y = 200;

public void detectCollisions() {
if (a1.isTouching(a2)) {
System.out.println("Collision detected!");
} else {
System.out.println("Apples are not touching.");
}
}

Чтобы показать, что Field имеет доступ к переменным и методам экземпля­
ров класса Apple, давайте создадим очередную версию нашего приложения
PrintAppleDetails4:
package ch05;
public class PrintAppleDetails4 {
public static void main(String args[]) {
Field f = new Field();
f.setupApples();
System.out.println("Apple a1:");
f.a1.printDetails();
System.out.println("Apple a2:");
f.a2.printDetails();
f.detectCollisions();
}
}

Приложение выводит уже знакомые данные о яблоках, а затем сообщает, соприкасаются ли два яблока:
$ java PrintAppleDetails4
Apple a1:
mass: 5.0
diameter: 3.0
position: (20, 40)
Apple a2:
mass: 10.0
diameter: 8.0
position: (70, 200)
Apples are not touching.

Превосходно — как и предполагалось. Прежде чем читать дальше, попробуйте
изменить позиции яблок, чтобы они соприкасались.

162  Глава 5. Объекты в Java
Модификаторы доступа
Несколько факторов влияют на возможность обращения к компонентам класса
из другого класса. Для управления доступом можно использовать модификаторы
видимости public, private и protected; классы также можно разместить в пакете, что влияет на их видимость. Например, модификатор private обозначает
переменную или метод, которые должны использоваться только другими компонентами этого же класса. Допустим, в предыдущем примере при объявлении
переменной diameter был бы указан модификатор private:
class Apple {
...
private float diameter;
...

Теперь обратиться к переменной diameter из класса Field не удастся:
class Field {
Apple a1 = new Apple();
Apple a2 = new Apple();
...
void setupApples() {
a1.diameter = 3.0f; // Ошибка компиляции
...
a2.diameter = 8.0f; // Ошибка компиляции
...
}
...
}

Если все же требуется обращаться извне к переменной diameter, то для этого
в класс Apple надо, как это принято, включить открытые методы. Назовем их
getDiameter() и setDiameter():
public class Apple {
private float diameter = 1.0f;
...
public void setDiameter(float newDiameter) {
diameter = newDiameter;
}
public float getDiameter() {
return diameter;
}
}

...

Создание подобных методов — хорошее правило программирования, потому
что оно дает гибкие возможности для тех случаев, когда в будущем понадобится

Классы  163

изменить тип или смысл переменной. Далее в этой главе будут рассмотрены пакеты, модификаторы доступа и их влияние на видимость переменных и методов.

Статические поля и методы
Как было сказано выше, переменные и методы экземпляров связаны со своими экземплярами класса и доступны через эти экземпляры (то есть через
конкретные объекты, такие как a1 или f в приведенных примерах). С другой
стороны, поля, объявленные с модификатором static, существуют внутри
класса и совместно используются всеми экземплярами этого класса. Переменные, объявленные с модификатором static, называются статическими
переменными, или переменными класса; методы с модификатором static называются статическими методами, или методами класса. Статические поля
удобно использовать в качестве флагов и идентификаторов, к которым можно
обратиться из любой точки. В класс Apple можно добавить статическую переменную, например, для хранения величины ускорения свободного падения,
чтобы рассчитывать траектории брошенного яблока, когда мы начнем создавать
анимацию в своей игре:
class Apple {
...
static float gravAccel = 9.8f;
...

Новая переменная float с именем gravAccel объявлена с модификатором static.
Это означает, что она существует на уровне класса, а не на уровне отдельного
экземпляра, а при изменении ее значения (напрямую или через любой экземпляр
Apple) значение изменится для всех объектов Apple (рис. 5.3).
К статическим полям в классе можно обращаться так же, как и к полям экземп­
ляров. Внутри класса Apple мы можем использовать gravAccel так же, как
и любую другую переменную:
class Apple {
...
float getWeight () {
return mass * gravAccel;
}
...
}

Поскольку статические поля и методы существуют на уровне класса и не связаны
ни с каким конкретным экземпляром, к ним также можно обращаться напрямую
через класс. Скажем, если вам захочется покидаться яблоками на Марсе, то для
чтения или присваивания переменной gravAccel не понадобится никакой объект

164  Глава 5. Объекты в Java
Apple (например, a1 или a2). Вместо этого для обращения к переменной можно

указать имя класса:

Apple.gravAccel = 3.7;

Экземпляр
Класс

Рис. 5.3. Статические переменные совместно используются всеми экземплярами класса
Эта команда изменяет значение gravAccel, видимое всем экземплярам. Нам
не нужно вручную задавать ускорение свободного падения для Марса в каждом экземпляре Apple. Статические переменные хорошо подходят для любых
данных, общих для всех экземпляров класса во время выполнения. Например, можно создать методы для регистрации экземпляров, чтобы они могли
взаимодействовать друг с другом или чтобы вы могли отслеживать их. Также
статические переменные часто используются для определения констант. В этом
случае модификатор static используется вместе с модификатором final. Таким образом, если бы вас интересовали исключительно яблоки, находящиеся
под воздействием гравитационного поля Земли, класс Apple можно было бы
изменить следующим образом:
class Apple {
...
static final float EARTH_ACCEL = 9.8f;
...

Здесь мы следуем общепринятому соглашению и присваиваем константе имя,
состоящее из прописных букв и символов подчеркивания (если имя состоит из
нескольких слов). Значение EARTH_ACCEL является константой; к нему можно

Методы  165

обратиться из класса Apple или из его экземпляров, но его невозможно изменить
во время выполнения.
Сочетание static и final должно использоваться только для тех данных, которые действительно должны быть неизменными. Компилятору разрешается
«подставлять» эти значения в классы, которые к ним обращаются. Это означает,
что при изменении переменной static final вам, скорее всего, придется перекомпилировать весь код, в котором эти классы используются (и это единственный
случай в Java, когда возникает такая необходимость). Статические поля также
хорошо подходят для значений, используемых при конструировании самого
экземпляра. В нашем примере можно объявить несколько статических значений
для представления различных размеров объектов Apple:
class Apple {
...
static int SMALL = 0, MEDIUM = 1, LARGE = 2;
...

Затем эти варианты используются в методе, который задает размер для экземп­
ляра Apple, или в специальном конструкторе, о котором мы расскажем позже:
Apple typicalApple = new Apple();
typicalApple.setSize( Apple.MEDIUM );

И снова внутри класса Apple можно обращаться к статическим полям просто
по имени, а префикс Apple. не нужен:
class Apple {
...
void resetEverything() {
setSize ( MEDIUM );
...
}
...
}

Методы
До настоящего момента классы в наших примерах были довольно простыми.
Мы поместили в них совсем немного информации: у яблок есть масса, на поле
есть несколько яблок, и т. д. Но мы также упомянули, что эти классы могут
делать что-то полезное. Например, во всех классах PrintAppleDetails перечислялись действия, которые должны выполняться при запуске программы.
Как упоминалось ранее, в Java эти действия заключаются в методы. В случае
PrintAppleDetails это метод main().

166  Глава 5. Объекты в Java
Каждый раз, когда вы выполняете некоторое действие или принимаете решение, вам нужен метод. Кроме хранения переменных (таких, как mass и diameter
в классе Apple), мы также добавили несколько фрагментов кода, содержащих
действия и логику. Методы настолько важны для работы классов, что нам пришлось создать несколько методов (вспомните метод printDetails() в Apple или
метод setupApples() в Field) еще до того, как мы добрались до их формального
обсуждения! Надеемся, что методы, упоминавшиеся ранее, были достаточно
простыми, чтобы понять их по контексту. Но возможности методов далеко не
ограничиваются выводом нескольких переменных или вычислением расстояния. Метод может содержать объявления локальных переменных и другие
команды Java, выполняемые при вызове метода. Метод может возвращать
значение вызывающей стороне. В методе всегда определен тип возвращаемого
значения, которым может быть примитивный тип, ссылочный тип или тип void,
обозначающий отсутствие возвращаемого значения. Метод может получать
аргументы — значения, предоставляемые вызывающей стороной при вызове.
Рассмотрим простой пример:
class Bird {
int xPos, yPos;

}

double fly ( int x, int y ) {
double distance = Math.sqrt( x*x + y*y );
flap( distance );
xPos = x;
yPos = y;
return distance;
}
...

В этом примере класс Bird определяет метод fly(), который получает в аргументах два целых числа: x и y. Он возвращает значение типа double, для чего
используется ключевое слово return . Наш метод получает фиксированное
количество аргументов (два). Но возможны и методы со списками аргументов
переменной длины. Такой метод может получить произвольное количество аргументов и разобраться с полученными данными во время выполнения1.

Локальные переменные
Наш метод fly() объявляет локальную переменную с именем distance, которая
используется для вычисления расстояния полета. Локальные переменные явля1

В книге списки аргументов подробно не рассматриваются, но если вас заинтересует
эта тема и вы захотите заняться самостоятельными исследованиями, проведите поиск
в интернете по слову «varargs».

Методы  167

ются временными; они существуют только внутри области видимости (внутри
блока) своего метода. Локальные переменные создаются в памяти при вызове
метода и обычно уничтожаются при возвращении из метода. К ним нельзя обращаться за пределами самого метода. Если метод выполняется параллельно
в разных потоках, то каждый поток использует собственную версию локальных
переменных метода. Аргументы метода также действуют как локальные переменные в области видимости метода; единственное отличие заключается в том,
что они инициализируются при передаче со стороны вызова.
Объект, созданный внутри метода и присвоенный локальной переменной, может
продолжить существовать после возвращения из метода. Как будет подробно
показано в разделе «Уничтожение объектов», с. 181, это зависит от того, остались
ли ссылки на объект. Если объект создается, присваивается локальной переменной и больше нигде не используется, то после выхода локальной переменной из
области видимости никаких ссылок на этот объект не остается, поэтому он будет
уничтожен уборщиком мусора (см. раздел «Уборка мусора», с. 181). Но если мы
присвоим этот объект переменной экземпляра другого объекта, или передадим
его в качестве аргумента другому методу, или передадим обратно в качестве возвращаемого значения, то он может быть спасен благодаря другой переменной,
содержащей ссылку на него.

Замещение
Если локальная переменная (или аргумент метода) и переменная экземпляра
имеют одинаковые имена, то локальная переменная замещает (или скрывает)
имя переменной экземпляра в области видимости метода. На первый взгляд эта
ситуация выглядит странно, но она встречается довольно часто, когда переменная экземпляра имеет распространенное или очевидное имя. Например, в класс
Apple можно добавить метод move, которому понадобятся новые координаты для
размещения яблока. Аргументам координат будет естественно присвоить имена x и y. Однако класс уже содержит переменные экземпляра с тем же именем:
class Apple {
int x, y;
...

}

public void moveTo(int x, int y) {
System.out.println("Moving apple to " + x + ", " + y);
...
}
...

Если яблоко в настоящий момент находится в позиции (20, 40), а мы вызвали
метод moveTo(40, 50), то как вы думаете, что выведет команда println()? Внутри

168  Глава 5. Объекты в Java
moveTo() имена x и y относятся только к аргументам с указанными именами.

Результат будет выглядеть так:
Moving apple to 40, 50

Если мы не можем добраться до переменных экземпляра x и y, то как же переместить яблоко? Оказывается, Java понимает суть замещения и предоставляет
механизм для обхода таких ситуаций.

Ссылка this
Специальная ссылка this может использоваться каждый раз, когда вам потребуется явно обозначить текущий объект или поле текущего объекта. Часто
в использовании this нет необходимости, потому что ссылка на текущий объект
определяется неявно, например, при использовании переменных экземпляра
с однозначно определяемыми именами внутри класса. Тем не менее this может
использоваться для явных ссылок на переменные экземпляра в объекте, даже
если они замещены. Следующий пример показывает, как при помощи this использовать имена аргументов, замещающие имена переменных экземпляра. Это
довольно распространенный прием, который избавляет вас от необходимости
выдумывать альтернативные имена. Реализация метода moveTo() с замещенными
переменными могла бы выглядеть так:
class Apple {
int x, y;
...

}

public void moveTo(int x, int y) {
System.out.println("Moving apple to " + x + ", " + y);
this.x = x;
if (y > diameter / 2) {
this.y = y;
} else {
this.y = (int)(diameter / 2);
}
}
...

В этом примере выражение this.x обозначает переменную экземпляра x и присваивает ей значение локальной переменной x, которая скрывает ее имя. То же
самое делается с this.y, но с небольшой дополнительной проверкой, которая
гарантирует, что яблоко не опустится под землю. Ключевое слово this используется в этом примере по единственной причине: имена аргументов замещают
переменные экземпляра, а мы хотим обратиться именно к переменным экземпляра. Ссылка this также может использоваться в ситуации, когда вам потребуется передать ссылку на «текущий» объект другому методу, как это было

Методы  169

сделано в графической версии приложения HelloJava в разделе «HelloJava2:
продолжение», с. 79.

Статические методы
Статические методы (методы классов), как и статические переменные, принадлежат всему классу, а не отдельным экземплярам этого класса. Что это значит?
Прежде всего, статический метод существует вне каких-либо конкретных экземпляров класса. Его можно вызвать по имени, через имя класса, без участия
каких-либо объектов. Так как статический метод не привязан к конкретному
экземпляру, он может напрямую обращаться только к другим статическим
полям (статическим переменным и статическим методам) своего класса. Он не
может напрямую обращаться к переменным экземпляра или вызывать методы
экземпляра, потому что для этого нужно было бы сначала понять, к какому экземпляру это относится. Статические методы можно вызывать из экземпляров,
используя такой же синтаксис, как и для методов экземпляров, но здесь важно
то, что они также могут использоваться независимо.
Наш метод isTouching() использует статический метод Math.sqrt(), определяемый классом java.lang.Math. Этот класс будет подробно рассмотрен
в главе 8, а пока мы подчеркнем, что Math — это имя класса, а не экземпляр
объекта Math1. Так как статические методы могут вызываться в любой ситуации, в которой доступно имя класса, они больше напоминают классические
функции в стиле языка C. Статические методы особенно полезны для реализации вспомогательных методов, способных выполнять полезную работу
как в привязке к конкретным экземплярам, так и без них. Например, в нашем
классе Apple можно определить описания всех доступных размеров в виде понятных человеку строк на основе констант, созданных в разделе «Обращение
к полям и методам», с. 158:
class Apple {
...

}

1

public static String[] getAppleSizes() {
// Возвращает имена констант
// Порядковый номер названия должен совпадать со значением константы
return new String[] { "SMALL", "MEDIUM", "LARGE" };
}
...

Класс Math написан так, что его экземпляры вообще не могут создаваться. Он содержит только статические методы и не имеет открытого конструктора. Попытка вызова
метода new Math() приведет к ошибке компиляции.

170  Глава 5. Объекты в Java
Здесь определяется статический метод getAppleSizes(), который возвращает
массив строк с названиями размеров яблок. Метод объявляется статическим,
потому что список размеров остается неизменным независимо от размера любого конкретного экземпляра Apple. При желании метод getAppleSizes() можно
использовать из экземпляра Apple, как и любой метод экземпляра. Например,
метод printDetails (нестатический) можно изменить таким образом, чтобы он
выводил название размера вместо точного значения диаметра:
public void printDetails() {
System.out.println(" mass: " + mass);
// Вывод точного значения диаметра:
// System.out.println(" diameter: " + diameter);
// Вывод названия (приблизительного значения диаметра):
String niceNames[] = getAppleSizes();
if (diameter < 5.0f) {
System.out.println(niceNames[SMALL]);
} else if (diameter < 10.0f) {
System.out.println(niceNames[MEDIUM]);
} else {
System.out.println(niceNames[LARGE]);
}

}

System.out.println("

position: (" + x + ", " + y +")");

Однако этот же метод может вызываться из других классов, с указанием имени класса Apple в точечной записи. Например, самый первый класс
PrintAppleDetails может использовать аналогичную логику для вывода данных
с использованием статического метода и статических переменных:
public class PrintAppleDetails {
public static void main(String args[]) {
String niceNames[] = Apple.getAppleSizes();
Apple a1 = new Apple();
System.out.println("Apple a1:");
System.out.println(" mass: " + a1.mass);
System.out.println(" diameter: " + a1.diameter);
System.out.println(" position: (" + a1.x + ", " + a1.y +")");
if (a1.diameter < 5.0f) {
System.out.println("This is a " + niceNames[Apple.SMALL] + " apple.");
} else if (a1.diameter < 10.0f) {
System.out.println("This is a " + niceNames[Apple.MEDIUM] + " apple.");
} else {
System.out.println("This is a " + niceNames[Apple.LARGE] + " apple.");
}
}
}

Методы  171

Здесь мы используем проверенный экземпляр класса Apple с именем a1, но для
получения списка размеров он не нужен. Обратите внимание: список удобных
названий загружается еще до создания a1. Тем не менее все успешно работает,
как видно из результата:
Apple a1:
mass: 0.0
diameter: 1.0
position: (0, 0)
This is a SMALL apple.

Статические методы также играют важную роль в разных паттернах программирования, в которых использование оператора new для класса ограничивается
одним статическим методом, который называется фабричным методом. Создание объектов более подробно рассматривается в разделе «Конструкторы», с. 178.
Для фабричных методов не существует соглашений об именах, но на практике
часто встречаются имена следующего вида:
Apple bigApple = Apple.createApple(Apple.LARGE);

Мы не будем писать фабричные методы, но вы наверняка найдете их в реальном
коде, особенно при поиске ответов на таких сайтах, как Stack Overflow.

Инициализация локальных переменных
В отличие от переменных экземпляра, получающих значения по умолчанию,
если они не были указаны явно, локальные переменные надо инициализировать
перед использованием. Попытка обратиться к локальной переменной, которой
не было присвоено значение, приводит к ошибке компиляции:
int foo;
void myMethod() {
int bar;
foo += 1; // Все правильно, переменная foo по умолчанию равна 0
bar += 1; // Ошибка компиляции, переменная bar не инициализирована

}

bar = 99;
bar += 1; // Теперь правильно

Обратите внимание: это не означает, что локальная переменная должна быть
инициализирована при объявлении. Просто первое обращение к ней должно
быть присваиванием. Причем присваивание внутри условных команд может
вызвать проблемы:

172  Глава 5. Объекты в Java
void myMethod {
int bar;
if ( someCondition ) {
bar = 42;
...
}
bar += 1; // Все еще ошибка компиляции, переменная bar
}
// может быть неинициализированной

В этом примере переменная bar инициализируется только в том случае, если
someCondition имеет значение true. Компилятор исключает всякий риск и помечает использование bar как ошибку. Ситуацию можно исправить несколькими способами. Можно заранее инициализировать переменную значением по
умолчанию или переместить использование переменной в условную команду.
Также можно принять меры к тому, чтобы путь выполнения не достиг неинициализированной переменной никакими способами, если это имеет смысл
в нашем конкретном приложении. Например, можно проследить за тем, чтобы
значение bar было присвоено в обеих ветвях: if и else. А еще можно вернуть
управление из середины метода:
void myMethod {
int bar;
...
if ( someCondition ) {
bar = 42;
...
} else {
return;
}
bar += 1; // OK!
...
}

В этом случае нет возможности обратиться к bar в неинициализированном состоянии, поэтому компилятор позволяет использовать bar после условной команды.
Почему Java так требовательно относится к локальным переменным? Дело
в том, что один из самых распространенных (и коварных) источников ошибок
в других языках (вроде C и C++) — пропущенная инициализация локальных
переменных, поэтому Java старается вам помочь.

Передача аргументов и ссылки
В начале главы 4 были описаны различия между примитивными типами, которые передаются по значению (посредством копирования), и объектами, которые
передаются по ссылкам. Теперь, когда вы лучше умеете работать с методами
в Java, рассмотрим пример:

Методы  173
void myMethod( int j, SomeKindOfObject o ) {
...
}
// Использование метода
int i = 0;
SomeKindOfObject obj = new SomeKindOfObject();
myMethod( i, obj );

Этот фрагмент кода вызывает myMethod() и передает ему два аргумента. Первый
аргумент i передается по значению; при вызове метода значение i копируется
в параметр метода (в локальную переменную с точки зрения метода) с именем j.
Если myMethod() изменит значение j, то изменится только копия локальной
переменной.
Аналогичным образом копия ссылки на obj помещается в ссылочную переменную o метода myMethod(). Обе ссылки относятся к одному объекту, поэтому
любые изменения, вносимые по любой из ссылок, влияют на единственный существующий экземпляр объекта. Если вы измените, допустим, o.size, то изменение
будет видимым как под именем o.size (внутри myMethod()), так и под именем
obj.size (в вызывающем методе). Но если myMethod() изменит саму ссылку o,
чтобы она указывала на другой объект, то изменение затронет только ссылку
локальной переменной. Это не повлияет на переменную obj на стороне вызова,
которая все еще относится к исходному объекту. В этом смысле передача ссылки
напоминает передачу указателя в C и не похожа на передачу по ссылке в C++.
А если методу myMethod() также потребуется изменить смысл ссылки obj в вызывающем методе (то есть заставить obj указывать на другой объект)? Для этого
проще всего упаковать obj в другой объект. Например, можно упаковать объект
в массив как единственный элемент:
SomeKindOfObject [] wrapper = new SomeKindOfObject [] { obj };

Тогда все стороны смогут ссылаться на объект в форме wrapper[0] и смогут изменять ссылку. Это решение не назовешь эстетичным, но оно показывает, что
вам потребуется только дополнительный уровень косвенных обращений.
Также возможен другой вариант: использование this для передачи ссылки вызывающему объекту. В данном случае вызывающий объект служит оберткой
(wrapper) для ссылки. Рассмотрим следующий фрагмент, который может быть
частью реализации связанного списка:
class Element {
public Element nextElement;

}

void addToList( List list ) {
list.insertElement( this );
}

174  Глава 5. Объекты в Java
class List {
void insertElement( Element element ) {
...
element.nextElement = getFirstElement();
setFirstElement(element);
}
}

Каждый элемент связанного списка содержит указатель на следующий элемент
списка. В этом коде класс Element представляет один элемент; он содержит
метод для включения себя в список. Класс List содержит метод для включения в список произвольного объекта Element. Метод addToList() вызывает
insertElement() с аргументом this (который, конечно, содержит Element). Метод
insertElement() может использовать полученную ссылку this для изменения
переменной экземпляра nextElement объекта Element, а затем снова обновить
начало списка. Тот же прием может использоваться в сочетании с интерфейсами
для реализации обратных вызовов для произвольных методов.

Обертки для примитивных типов
Как мы отмечали в главе 4, в мире Java существует граница между типами классов
(то есть объектами) и примитивными типами (числами, символами и логическими
значениями). В Java это разграничение было введено по соображениям эффективности. При обработке чисел вычисления должны быть по возможности простыми;
необходимость использовать объекты для примитивных типов усложнила бы оптимизацию. Но для тех ситуаций, в которых примитивные типы все-таки должны
интерпретироваться как объекты, Java предоставляет стандартные классы-обертки
(wrapper classes) для каждого из примитивных типов (табл. 5.1).

Таблица 5.1. Обертки для примитивных типов
Примитивный тип

Обертка

void

java.lang.Void

boolean

java.lang.Boolean

char

java.lang.Character

byte

java.lang.Byte

short

java.lang.Short

int

java.lang.Integer

long

java.lang.Long

float

java.lang.Float

double

java.lang.Double

Методы  175

Экземпляр класса-обертки инкапсулирует одно значение соответствующего
типа. Это неизменяемый объект, который служит контейнером для хранения
значения и позволяет прочитать его впоследствии. Объект-обертка строится
из примитивного типа или из представления значения в формате String. Следующие команды эквивалентны:
Float pi = new Float( 3.14 );
Float pi = new Float( "3.14" );

Если при разборе строки происходит ошибка, то конструкторы оберток выдают
исключение NumberFormatException.
Каждая обертка для числового типа реализует интерфейс java.lang.Number,
который предоставляет методы для обращения к значению во всех примитивных формах. Для получения скалярных значений можно использовать
методы doubleValue(), floatValue(), longValue(), intValue(), shortValue()
и byteValue():
Double size = new Double ( 32.76 );
double d = size.doubleValue(); // 32.76
float f = size.floatValue(); // 32.76
longl = size.longValue(); // 32
int i = size.intValue(); // 32

Этот фрагмент эквивалентен преобразованию примитивного значения double
к различным типам.
Чаще всего необходимость в обертках возникает при передаче примитивного значения такому методу, который требует передачи объекта. Например,
в главе 7 будет рассмотрен API коллекций Java — набор тщательно спроектированных классов для работы с группами объектов: списками, множествами
и картами. API коллекций работает с объектными типами, поэтому для сохранения в них все примитивы должны быть заключены в обертки. Скоро вы
увидите, что Java может делать это автоматически. А пока сделаем это самостоятельно. Как вы увидите, List является расширяемой коллекцией объектов
Object. Мы будем использовать обертки для хранения чисел в List (наряду
с другими объектами):
// Простой код Java
List myNumbers = new ArrayList();
Integer thirtyThree = new Integer( 33 );
myNumbers.add( thirtyThree );

Здесь мы создаем обертку Integer, чтобы вставить число в List методом add(),
который должен получать объект. Затем при извлечении элементов из List
значение int можно получить следующим образом:

176  Глава 5. Объекты в Java
// Простой код Java
Integer theNumber = (Integer)myNumbers.get(0);
int n = theNumber.intValue(); // 33

Как упоминалось ранее, если вы поручите Java сделать это за вас (выполнить
«автоупаковку»), то код станет более лаконичным и безопасным. Использование
класса-обертки в основном скрывается от вас компилятором, но оно все еще
задействовано во внутренней реализации:
// Код Java с использованием автоупаковки и обобщений
List myNumbers = new ArrayList();
myNumbers.add( 33 );
int n = myNumbers.get( 0 );

Работа с обобщениями будет продемонстрирована далее.

Перегрузка методов
Перегрузкой методов называется возможность определения нескольких одноименных методов в классе; при вызове метода компилятор выбирает правильный метод на основании аргументов, переданных методу. Из этого следует,
что перегруженные методы должны иметь разное количество аргументов или
разные типы аргументов. (В разделе «Переопределение методов», с. 194, будет
рассмотрено переопределение методов, встречающееся при объявлении методов
с совпадающими сигнатурами в субклассах.)
Перегрузка методов (также называемая ситуационным полиморфизмом) — это
мощный и полезный инструмент. Идея заключается в создании методов, одинаково работающих с аргументами разных типов. При этом создается иллюзия
того, что один метод может работать со многими типами аргументов. Метод
print() из стандартного класса PrintStream показывает хороший пример перегрузки методов в действии. Как вы, вероятно, уже поняли, для вывода строкового
представления чего угодно можно воспользоваться следующим выражением:
System.out.print( аргумент )

Переменная out представляет собой ссылку на объект (PrintStream), в котором
определяются девять разных перегруженных версий метода print(). Версии
получают аргументы следующих типов: Object, String, char[], char, int, long,
float, double и boolean.
class PrintStream {
void print( Object arg ) { ... }
void print( String arg ) { ... }
void print( char [] arg ) { ... }
...
}

Создание объектов  177

Метод print() можно вызвать с аргументом любого из этих типов, и значение
будет выведено соответствующим образом. В языке без перегрузки методов это
потребовало бы более громоздкого решения — например, методов с разными
именами для вывода разных типов объектов. В этом случае вы должны были бы
сами разбираться, какой метод подходит для каждого из типов данных.
В предыдущем примере метод print() был перегружен для поддержки двух
ссылоч­ных типов: Object и String. Что будет, если вы попытаетесь вызвать
print() с другим ссылочным типом? Например, с объектом Date ? При отсутствии точного совпадения типа компилятор ищет допустимый вариант,
совместимый по присваиванию. Поскольку Date, как и все классы, является
субклассом Object, объект Date может быть присвоен переменной типа Object.
Следовательно, этот вариант допустим и будет выбран метод Object.
А если возможных совпадений будет несколько? Допустим, вы хотите вывести
литерал "Hi there". Этот литерал совместим по присваиванию либо со String
(так как он относится к типу String), либо с Object. Здесь компилятор решает, какой вариант «лучше», и выбирает этот метод. В данном случае это метод String.
На интуитивном уровне это можно объяснить так: класс String «ближе» к литералу "Hi there" в иерархии наследования. Такое совпадение является более
конкретным. По другому, немного более строгому определению, первый метод
конкретнее второго метода, если все типы аргументов первого метода совместимы по присваиванию с типами аргументов второго метода. В данном случае
метод для String более конкретен, потому что тип String совместим по присваиванию с типом Object (обратное неверно).
Читатели, внимательно следящие за нашими объяснениями, могли заметить: мы
сказали, что компилятор обрабатывает вызовы перегруженных методов. Перегрузка методов не происходит на стадии выполнения; это важное отличие. Оно
означает, что выбранный метод выбирается однократно, во время компиляции.
После того как перегруженный метод будет выбран, этот выбор фиксируется
до возможной перекомпиляции кода, даже если класс, содержащий вызванный
метод, позднее будет переработан и в него будет добавлен еще более конкретный
перегруженный метод. В этом отношении перегруженные методы отличаются от
переопределенных, которые выбираются во время выполнения и могут быть найдены даже в том случае, если они не существовали на момент компиляции вызывающего класса. На практике это отличие для вас обычно несущественно, так как
вы с большой вероятностью будете перекомпилировать все необходимые классы
одновременно. Переопределение методов будет рассмотрено далее в этой главе.

Создание объектов
Память для объектов в Java выделяется в системной области, которая называется кучей (heap). В отличие от других языков, вам не придется управлять

178  Глава 5. Объекты в Java
этой памятью самостоятельно. Java позаботится о выделении и освобождении
памяти за вас. Java явно выделяет память в тот момент, когда объект создается
оператором new. Что еще важнее, объекты уничтожаются уборщиком мусора,
когда не остается ни одной ссылки на них.

Конструкторы
Объекты создаются оператором new с использованием конструктора. Так называется специальный метод, имя которого совпадает с именем класса, не имеющий
возвращаемого типа. Конструктор вызывается при создании нового экземпляра
класса, что позволяет классу подготовить этот объект к использованию. Конструкторы, как и другие методы, могут получать аргументы и перегружаться
(но они не наследуются, как другие методы).
class Date {
long time;
Date() {
time = currentTime();
}

}

Date( String date ) {
time = parseDate( date );
}
...

В этом примере класс Date имеет два конструктора. Первый конструктор не получает аргументов; он называется конструктором по умолчанию. Конструкторы
по умолчанию играют особую роль: если для класса не определены никакие
конструкторы, то компилятор предоставляет пустой конструктор по умолчанию.
Именно конструктор по умолчанию вызывается тогда, когда вы создаете объект
вызовом конструктора без аргументов. Здесь мы реализовали конструктор по
умолчанию так, чтобы он задавал переменную экземпляра time вызовом гипотетического метода currentTime(), который напоминает функциональность
реального класса java.util.Date. Второй конструктор получает аргумент String.
Предполагается, что этот объект String содержит строковое представление времени, которое можно разобрать для присваивания значения переменной time.
С конструкторами из приведенного примера объект Date может быть создан
следующими способами:
Date now = new Date();
Date christmas = new Date("Dec 25, 2020");

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

Создание объектов  179

Если впоследствии все ссылки на созданный объект будут удалены, то объект
будет уничтожен уборщиком мусора (об этом чуть далее):
christmas = null; // Кандидат для уборки мусора

Присваивание этой ссылке значения null означает, что ссылка уже не указывает на объект даты "Dec 25, 2020". Присваивание переменной christmas
любого другого значения приведет к тому же эффекту. Если только на исходный объект даты не ссылается другая переменная, он становится недоступным
и уничтожается уборщиком мусора. Мы не предлагаем присваивать ссылкам
значение null, чтобы инициировать уничтожение объектов уборщиком мусора.
Как правило, это происходит естественно: при выходе локальных переменных
из области видимости. Но если на объект ссылаются переменные экземпляра
в других объектах, то он будет существовать до тех пор, пока существуют эти
объекты, — именно из-за этих ссылок, а статические переменные фактически
существуют бесконечно.
Еще несколько замечаний: конструкторы не могут объявляться с ключевыми
словами abstract, synchronized или final (их смысл будет объяснен далее).
Тем не менее конструкторы, как и любые другие методы, могут объявляться
с модификаторами видимости public, private или protected, управляющими
их видимостью. Модификаторы видимости подробно рассматриваются в следующей главе.

Работа с перегруженными конструкторами
Конструктор может ссылаться на другой конструктор того же класса или ближайшего суперкласса при помощи специальных форм ссылок this и super. Здесь
будет рассмотрен первый случай, а к конструктору суперкласса мы вернемся
после рассмотрения субклассирования и наследования. Конструктор может
вызвать другой перегруженный конструктор в своем классе, используя вызов
автореферентного метода this() с соответствующими аргументами для выбора
нужного конструктора. Если конструктор вызывает другой конструктор, то он
должен сделать это в своей первой команде:
class Car {
String model;
int doors;
Car( String model, int doors ) {
this.model = model;
this.doors = doors;
// И прочая сложная подготовка
...
}

180  Глава 5. Объекты в Java

}

Car( String model ) {
this( model, 4 /* двери */ );
}
...

В этом примере класс Car имеет два конструктора. Первый, более полный, получает аргументы с моделью машины и количеством дверей. Второй конструктор
получает в аргументе только модель, а затем вызывает первый конструктор со
значением по умолчанию — четыре двери. У такого подхода есть важное преимущество: вся сложная подготовка может выполняться в одном конструкторе.
Другие вспомогательные конструкторы просто вызывают этот конструктор,
передавая ему соответствующие аргументы.
Специальный вызов this() должен находиться в первой команде делегирующего конструктора. Такое ограничение синтаксиса объясняется необходимостью
четко определенной последовательности команд при вызове конструкторов.
В конце этой последовательности Java вызовет конструктор суперкласса (если
это не было сделано явно), чтобы гарантировать правильную инициализацию
унаследованных полей перед продолжением работы.
Также в цепочке сразу после вызова конструктора суперкласса имеется точка,
в которой вычисляются инициализаторы переменных экземпляра текущего
класса. До этой точки невозможно даже обращаться к переменным экземпляра
класса. Мы объясним эту ситуацию подробнее после рассмотрения темы наследования.
На данном этапе вам достаточно знать, что второй (делегированный) конструктор может вызываться только в первой команде вашего конструктора. Например,
следующий код недопустим, а при попытке его скомпилировать произойдет
ошибка:
Car( String m ) {
int doors = determineDoors();
this( m, doors ); // Ошибка: вызов конструктора
// должен быть первой командой
}

Простой конструктор, получающий название модели, не выполняет никаких
промежуточных операций перед вызовом более явного конструктора. Он даже
не может обратиться к переменной экземпляра за значением-константой:
class Car {
...
final int defaultDoors = 4;
...
Car( String m ) {

Уничтожение объектов  181

}

}
...

this( m, defaultDoors ); // Ошибка: обращение
// к неинициализированной переменной

Переменная экземпляра defaultDoors остается неинициализированной до
более поздней точки в цепочке вызовов конструкторов, подготавливающих
объект, поэтому компилятор еще не позволяет обратиться к ней. К счастью, эту
конкретную проблему можно решить использованием статической переменной
вместо переменной экземпляра:
class Car {
...
static final int DEFAULT_DOORS = 4;
...
Car( String m ) {
this( m, DEFAULT_DOORS ); // Правильно!
}
...
}

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

Уничтожение объектов
Итак, теперь вы знаете, как создаются объекты. Пора поговорить об их уничтожении. Если у вас есть опыт программирования на C или C++, то, вероятно,
вам пришлось провести немало времени, выявляя утечки памяти в вашем коде.
Java позаботится об уничтожении объектов за вас. Вам не придется беспокоиться о привычных утечках памяти, и вы сможете сконцентрироваться на более
важных задачах1.

Уборка мусора
Для удаления объектов, которые стали ненужными программе, в Java есть
механизм уборки мусора (garbage collection). Уборщик мусора — своего рода
«смерть с косой». Он скрывается на заднем плане, наблюдая за объектами
1

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

182  Глава 5. Объекты в Java
и ожидая, пока они завершат свое существование. Уборщик мусора периодически подсчитывает ссылки на объекты и смотрит, не пришло ли их время.
Когда все ссылки на объект исчезнут, а последняя возможность обратиться
к объекту будет потеряна, механизм уборки мусора объявит этот объект недоступным и возвратит его память в пул ресурсов. Недоступным считается объект, к которому невозможно перейти по любой комбинации «живых» ссылок
в работающем приложении.
При уборке мусора используется множество разных алгоритмов; архитектура
виртуальной машины Java не требует применения какой-то конкретной схемы.
Однако надо сказать, каким образом эта задача решается в разных реализациях Java. В ранних версиях языка использовался метод «пометки и очистки».
В этой схеме Java сначала сканирует содержимое кучи в поисках непомеченных
объектов. Возможность нахождения объектов в куче основана на том, что они
сохраняются по особой схеме, а указатели на них содержат специальную сигнатуру битов, которая вряд ли сможет возникнуть естественным образом. У такого
алгоритма нет проблемы циклических ссылок, когда объекты ссылаются друг
на друга и кажутся «живыми» даже тогда, когда на самом деле они недоступны
(Java решает эту проблему автоматически). Тем не менее эта схема не самая
быстрая, и она создает паузы в выполнении программы. С тех пор алгоритмы
уборки мусора были значительно улучшены.
Современные уборщики мусора Java работают фактически непрерывно, не вызывая никаких продолжительных задержек в выполнении приложений Java.
Поскольку они являются частью исполнительной системы, они также могут
совершать некоторые операции, которые невозможно выполнить статически.
Например, они делят кучу на несколько областей — для объектов с разными
оцениваемыми сроками жизни. Объекты с коротким сроком жизни размещаются
в специальной области кучи, которая значительно сокращает время их переработки. Объекты с более долгим сроком жизни могут быть перемещены в другие,
менее изменчивые части кучи. В последних реализациях уборщик мусора даже
может адаптировать свое поведение, изменяя размеры частей кучи на основании
фактической производительности приложения. Усовершенствование механизма
уборки мусора в Java по сравнению с первыми выпусками стало весьма значительным. Это одна из причин, по которым язык Java сейчас приблизительно
сравнялся по скорости со многими традиционными языками, возлагающими
бремя управления памятью на программиста.
Как правило, вам не приходится беспокоиться о процессе уборки мусора. Тем не
менее один метод уборки мусора может пригодиться при отладке. Чтобы явно
порекомендовать уборщику мусора провести уборку, вызовите метод System.gc().
Он полностью зависит от конкретной реализации Java и в некоторых случаях
может не делать ничего, но его можно вызвать, когда нужна уверенность, что Java
проведет очистку перед тем, как вы выполните какую-либо операцию.

Пакеты  183

Пакеты
Даже в предыдущих простых примерах вы, возможно, заметили, что решение
задач на языке Java требует создания множества классов. Классы из игры, приводившиеся выше, представляли собой яблоки, физиков, игровое поле и т. д.
В более сложных приложениях или библиотеках могут использоваться сотни
и даже тысячи классов. Все эти классы необходимо как-то упорядочить, и для
этого в Java существует концепция пакетов.
Вспомните второй пример HelloJava из главы 2. Первые строки файла содержат
информацию о местонахождении кода:
import javax.swing.*;
public class HelloJava {
public static void main( String[] args ) {
JFrame frame = new JFrame( "Hello, Java!" );
JLabel label = new JLabel( "Hello, Java!", JLabel.CENTER );
...

Здесь файлу Java было присвоено имя HelloJava, совпадающее с именем главного класса в этом файле. Когда речь заходит о структурировании содержимого
таких файлов, возникает естественная мысль: использовать иерархию папок
для организации этих файлов. Именно так и поступает Java. Пакеты соответствуют именам папок, так же как классы соответствуют именам файлов. Например, посмотрите на исходный Java-код компонентов Swing, использованных
в HelloJava, и вы найдете папку с именем javax, в которой находится папка swing,
а в ней — файлы с именами JFrame.java, JLabel.java и т. д.

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

Импортирование отдельных классов
В программировании часто действует принцип «лучше меньше, да лучше».
Меньше кода — проще сопровождение. Меньше непроизводительных затрат —

184  Глава 5. Объекты в Java
выше эффективность, и т. д. (Впрочем, придерживаясь такого подхода к программированию, надо не забывать и о знаменитом изречении мудрого Эйнштейна:
«Все надо делать по возможности простым, но не проще того».) Если вам нужны
только один-два класса из внешнего пакета, можно импортировать только эти
классы. При таком подходе ваш код станет чуть проще для понимания: другие
программисты будут точно знать, какие классы вы будете использовать.
Пересмотрим приведенный выше фрагмент HelloJava. Ранее мы использовали массовое импортирование (подробнее об этом — в следующем разделе), но мы могли
бы уточнить в коде свои намерения, импортировав только необходимые классы:
import javax.swing.JFrame;
import javax.swing.JLabel;
public class HelloJava {
public static void main( String[] args ) {
JFrame frame = new JFrame( "Hello, Java!" );
JLabel label = new JLabel( "Hello, Java!", JLabel.CENTER );
...

Конечно, такой вариант импортирования занимает больше места в коде, но мы
снова подчеркнем: каждый, кто будет читать или компилировать ваш код, будет
точно знать существующие в нем зависимости. Во многих IDE даже поддерживается функция оптимизации импорта, которая автоматически находит такие
зависимости и перечисляет их по отдельности. Если у вас появится привычка
просматривать эти зависимости, вы поймете, какую пользу они приносят, когда
вы разбираетесь в новом (или старом, но давно забытом) классе.

Импортирование целых пакетов
Конечно, не каждый пакет подходит для выборочного импортирования. Отличным примером служит тот же пакет Swing. Если вы пишете графическое
десктопное приложение, то вы почти наверняка будете использовать Swing
и его многочисленные компоненты. Для импортирования всех классов пакета
используется синтаксис, уже встречавшийся вам ранее:
import javax.swing.*;
public class HelloJava {
public static void main( String[] args ) {
JFrame frame = new JFrame( "Hello, Java!" );
JLabel label = new JLabel( "Hello, Java!", JLabel.CENTER );
...

Символ * является универсальным обозначением для импортирования классов.
Эта версия команды import сообщает компилятору, что он должен быть готов
к использованию всех классов пакета. Подобные команды импортирования

Пакеты  185

часто встречаются во многих распространенных пакетах Java, таких как AWT,
Swing, Utils и пакеты ввода-вывода. Подчеркнем, что этот синтаксис подходит
для любых пакетов, но в тех случаях, когда вы решите, что есть смысл определять импортируемые классы более конкретно, вы получите некоторый прирост
быстродействия на стадии компиляции, а код будет легче читаться.

Отказ от импортирования
Существует еще один вариант использования внешних классов из других пакетов: не импортировать их вообще. В коде можно использовать полные имена
классов. Например, наш класс HelloJava использует классы JFrame и JLabel
из пакета javax.swing. При желании можно ограничиться импортированием
только класса JLabel:
import javax.swing.JLabel;
public class HelloJava {
public static void main( String[] args ) {
javax.swing.JFrame frame = new javax.swing.JFrame( "Hello, Java!" );
JLabel label = new JLabel( "Hello, Java!", JLabel.CENTER );
...

На первый взгляд строка, в которой создается окно, кажется слишком длинной,
но в больших классах с длинными списками импортирования одноразовое использование может сделать код более понятным. Полные имена часто указывают
на то, что класс используется только в одной точке файла. (А если класс используется многократно, то его все-таки следует импортировать.) Такое использование никогда не бывает обязательным, но оно иногда встречается на практике.

Пользовательские пакеты
По мере того как вы продолжите изучать Java, писать все больше кода и решать
более серьезные задачи, у вас будет накапливаться все больше классов. Для
упорядочения этой коллекции можно воспользоваться пакетами. Пакеты объявляются ключевым словом package. Как упоминалось в начале этого раздела,
файл с вашим классом можно разместить в структуре папок в соответствии
с именем пакета. Напомним, что имена пакетов должны быть записаны в нижнем
регистре, с разделением компонентов точками. Например, javax.swing — это
имя нашего пакета графического интерфейса.
Другое соглашение, часто применяемое к именам пакетов, — так называемая
система «обратных доменных имен». В отличие от пакетов, непосредственно
связанных с Java, сторонние библиотеки и другой код, разработанный независимыми авторами, часто именуют на основе доменного имени компании или адреса
электронной почты конкретного разработчика. Например, Mozilla Foundation

186  Глава 5. Объекты в Java
предоставляет ряд библиотек Java для сообщества разработчиков с открытым
кодом. Большинство таких библиотек и вспомогательных средств оформлено
в пакеты, имена которых начинаются с домена Mozilla (mozilla.org), записанного
в обратном порядке: org.mozilla. У обратной записи имени есть удобный (и намеренный) побочный эффект: структура папок на верхнем уровне очень мала.
Есть немало больших проектов, в которых используются библиотеки только из
доменов верхнего уровня com и org.
Если вы пишете свои пакеты независимо от какой-либо компании или контрактного проекта, возьмите за основу свой адрес электронной почты и запишите его
в обратном порядке, по аналогии с доменными именами компаний. Другой популярный вариант для кода, распространяемого в интернете, — использование
домена вашего хостинг-провайдера, такого как GitHub. На GitHub публикуется
огромное множество Java-проектов, создаваемых энтузиастами и любителями.
Например, вы можете создать пакет с именем com.github.myawesomeproject
(конечно, myawesomeproject надо заменить реальным именем проекта). Учтите,
что репозитории на таких сайтах, как GitHub, часто разрешают использовать
имена, недопустимые в именах пакетов. Ваш проект там может называться
my-awesome-project, но имя пакета не может содержать дефисы ни в одной из
частей. Часто такие недействительные символы просто удаляются для получения допустимого имени.
Возможно, вы уже заглянули в другие примеры из архива кода этой книги. Тогда
вы наверняка заметили, что мы разместили их в соответствии с пакетами. Хотя
организация классов в пакетах — неоднозначная тема, в которой не существует
общепризнанных решений, мы выбрали вариант, упрощающий поиск примеров во время чтения книги. Для маленьких примеров в главах созданы пакеты
вида ch05 («глава 5»). Для примера нашей игры, проходящего через всю книгу,
используется пакет game. Наши первые примеры можно было бы переписать,
чтобы они легко укладывались в эту схему:
package ch02;
import javax.swing.*;
public class HelloJava {
public static void main( String[] args ) {
JFrame frame = new JFrame("Hello, Java!");
JLabel label = new JLabel("Hello, Java!", JLabel.CENTER );
...

Необходимо создать папку ch02 и поместить в нее файл HelloJava.java. После
этого пример можно скомпилировать и запустить в командной строке. Перей­
дите в начало иерархии папок и укажите полное имя файла и имя класса:
$ javac ch02/HelloJava.java
$ java ch02.HelloJava

Пакеты  187

Если вы работаете в IDE, эта среда будет управлять пакетами за вас. Просто
создайте свои классы и не забудьте указать главный класс, который запускает
вашу программу.

Видимость полей и методов класса
Ранее уже упоминались модификаторы доступа, которые могут использоваться
при объявлении переменных и методов. Объявление чего-либо с модификатором
public означает, что увидеть вашу переменную или вызвать ваш метод может
кто угодно. Модификатор protected означает, что любой субкласс сможет обратиться к переменной, вызвать метод или переопределить метод, чтобы предоставить альтернативную функциональность, более уместную для субкласса.
Модификатор private означает, что переменная или метод доступны только
внутри самого класса.
Пакеты влияют на доступ к защищенным (protected) элементам класса. Такие
элементы видны и доступны не только для любого субкласса, но и для других
классов того же пакета. Пакеты также учитываются, если модификатор отсутствует. Возьмем для примера текстовые компоненты из пакета mytools.text
(рис. 5.4).

Рис. 5.4. Пакеты и видимость классов
Класс TextComponent не имеет модификатора. Он обладает уровнем видимости
по умолчанию, или «пакетно-приватной» видимостью. Это означает, что другие классы из того же пакета могут обращаться к классу, но классы вне пакета
к нему обратиться не смогут. Данная возможность может быть очень полезна
для классов, относящихся ко внутренней реализации, или вспомогательных
классов. Вы будете свободно пользоваться пакетно-приватными элементами, но
другие программисты смогут использовать только элементы с модификаторами public и protected. На рис. 5.5 более подробно представлены возможности
использования переменных и методов как субклассами, так и внешним кодом.
Обратите внимание: расширение класса TextArea открывает доступ как к открытым методам getText() и setText(), так и к защищенному (protected) методу

188  Глава 5. Объекты в Java

видимость
по умолчанию

виден
виден

виден

виден

виден

виден

Рис. 5.5. Пакеты и видимость элементов классов
formatText(). При этом субкласс MyTextDisplay (о субклассах и ключевом слове
extends подробно рассказано в разделе «Субклассирование и наследование»,
с. 190) не имеет доступа к пакетно-приватной переменной linecount. Однако
в пакете mytools.text, в котором создается класс TextEditor, можно получить
доступ к linecount, а также к методам с модификаторами public или protected.
Внутренняя переменная для содержимого text остается приватной (скрытой)
и недоступной для кого-либо, кроме самого класса TextArea.

В табл. 5.2 приведена сводка уровней видимости в Javа в порядке убывания
ограничений. Методы и переменные всегда видны в том классе, в котором они
объявлены, поэтому в таблице этот уровень видимости не упоминается.

Таблица 5.2. Модификаторы видимости
Модификатор

Видимость за пределами класса

private

Нет

Без модификатора (по умолчанию)

Классы того же пакета

protected

Классы того же пакета и субклассы (в том же пакете
или вне его)

public

Все классы

Нетривиальное проектирование классов  189

Компиляция с пакетами
Вы уже видели, как использовать полное имя класса при компиляции простого
примера. Если вы не работаете в IDE, у вас также есть ряд других вариантов. Например, вы можете скомпилировать все классы заданного пакета таким образом:
$ javac ch02/*.java
$ java ch02.HelloJava

Учтите, что в коммерческих приложениях для предотвращения конфликтов имен
часто используются более сложные имена пакетов. Одна из распространенных
схем — обратная запись доменного имени вашей компании. Например, для примеров кода из книги издательства O’Reilly можно использовать полный префикс
com.oreilly.learningjava5e. Код каждой главы преобразуется в подпакет с этим
префиксом. Компиляция и запуск классов таких пакетов выполняются очень
просто, хотя и не совсем компактно:
$ javac com/oreilly/learningjava5e/ch02/*.java
$ java com.oreilly.learningjava5e.ch02.HelloJava

Команда javac также «понимает» основные зависимости между классами. Если
ваш главный класс использует несколько других классов из той же иерархии
исходного кода (даже если не все они входят в один пакет), то при компиляции
главного класса скомпилируются и остальные (зависимые) классы.
Впрочем, если вы не ограничиваетесь простыми программами с несколькими
классами, то почти наверняка воспользуетесь IDE или программой управления
сборкой (такой, как Gradle или Maven). Эти инструменты не рассматриваются
в книге, но в интернете можно найти много справочников по ним. Maven отлично
подходит для управления большими проектами со множеством зависимостей.
За полным описанием возможностей и функциональности этой популярной
программы обращайтесь к книге «Maven: the Definitive Guide» от создателя
Maven Джейсона Ван Зила (Jason Van Zyl) и его команды из Sonatype (издательство O’Reilly)1.

Нетривиальное проектирование классов
В разделе «HelloJava2: продолжение» (с. 79) два класса находились в одном
файле. Это упрощало процесс компиляции, но не открывало ни одному из
1

Программа Maven настолько изменила ситуацию с управлением зависимостями в Java
и даже в других языках на базе JVM, что появились новые инструменты, базирующиеся
на успехе Maven, например Gradle.

190  Глава 5. Объекты в Java
классов особого доступа к другому. Приступая к работе над более сложными
задачами, вы столкнетесь со многими ситуациями, в которых нетривиальное
проектирование классов, предоставляющих особый доступ, не только удобно,
но и абсолютно необходимо для написания простого в сопровождении кода.

Субклассирование и наследование
Классы в языке Java образуют иерархию. Класс в Java объявляется субклассом
(подклассом) другого класса при помощи ключевого слова extends. Субкласс
наследует переменные и методы от своего суперкласса и может использовать
их так, как если бы они были объявлены в самом субклассе:
class Animal {
float weight;
...
void eat() {
...
}
...
}
class Mammal extends Animal {
// Наследует weight
int heartRate;
...

}

// Наследует eat()
void breathe() {
...
}

В этом примере объект типа Mammal содержит переменную экземпляра weight
и метод eat(). Они унаследованы от класса Animal.
Субкласс может быть расширением только одного суперкласса. В более правильной терминологии говорят, что Java поддерживает одиночное наследование реализации классов. Далее в этой главе будут рассматриваться интерфейсы, которые
занимают место множественного наследования, встречающегося в других языках.
К субклассу может применяться дальнейшее субклассирование. Обычно в ходе
субклассирования происходит специализация или уточнение класса путем
добавления переменных и методов (но субклассирование не может привести
к удалению или сокрытию переменных или методов). Пример:
class Cat extends Mammal {
// Наследует weight и heartRate

Нетривиальное проектирование классов  191
boolean longHair;
...

}

// Наследует eat() и breathe()
void purr() {
...
}

Класс Cat относится к типу Mammal, который происходит от типа Animal. Объекты Cat наследуют все характеристики объектов Mammal, а в конечном итоге
и объектов Animal. Cat также включает дополнительное поведение в виде метода purr() и переменной longHair. Этот пример отношений между классами
показан на рис. 5.6.

Рис. 5.6. Иерархия классов
Субкласс наследует все элементы суперкласса, не помеченные модификатором
private. Как вы вскоре увидите, другие уровни видимости влияют на то, какие
унаследованные элементы класса видны за пределами класса и его субклассов,
но как минимум субкласс содержит такой же набор видимых элементов, что
и его родитель. По этой причине тип субкласса может рассматриваться как
подтип его родителя, а экземпляры подтипа могут использоваться везде, где
могут использоваться экземпляры супертипа. Рассмотрим следующий пример:
Cat simon = new Cat();
Animal creature = simon;

Объект simon класса Cat в этом примере может быть присвоен переменной
creature с типом Animal, потому что Cat является подтипом Animal. Аналогичным
образом любой метод, получающий объект Animal, сможет получить экземпляр
Cat или любого типа Mammal. Это важный аспект полиморфизма в объектноориентированных языках, к числу которых относится и Java. Вы увидите, как
использовать его для уточнения поведения класса и добавления к нему новой
функциональности.

192  Глава 5. Объекты в Java
Замещение переменных
Как вы уже знаете, локальная переменная, имя которой совпадает с именем
переменной экземпляра, замещает (скрывает собой) переменную экземпляра.
Аналогичным образом переменная экземпляра в субклассе может заместить
одноименную переменную экземпляра в родительском классе, как показано на
рис. 5.7. Для полноты картины и для подготовки к изложению более сложного
материала мы рассмотрим подробности сокрытия переменных, но на практике
вам почти никогда не придется так поступать. Гораздо лучше структурировать
код таким образом, чтобы переменные четко различались вследствие разных
имен или схем выбора имен.
На рис. 5.7 переменная weight объявляется в трех местах: как локальная переменная в методе foodConsumption() класса Mammal, как переменная экземпляра
класса Mammal и как переменная экземпляра класса Animal. Выбор фактической
переменной при обращении к ней в коде зависит от области видимости, в которой
вы работаете, и от уточнения ссылки на нее.

Унаследованная
область
видимости

Область
видимости
экземпляра

Локальная
область
видимости

Рис. 5.7. Область видимости замещаемых переменных
В предыдущем примере все переменные относились к одному типу. В несколько более правдоподобном примере использования замещенных переменных
будут изменяться их типы. Например, переменная типа int может замещаться переменной типа double в субклассе, которому требуются вещественные
значения вместо целых. Это можно сделать без изменения существующего
кода, потому что при замещении переменной мы ее не заменяем, а только
скрываем. Обе переменные продолжают существовать: методы суперкласса
видят исходную переменную, а методы субкласса — новую версию. Определение того, какую переменную видит тот или иной метод, происходит во
время компиляции.

Нетривиальное проектирование классов  193

Простой пример:
class IntegerCalculator {
int sum;
...
}
class DecimalCalculator extends IntegerCalculator {
double sum;
...
}

В этом примере переменная экземпляра sum замещается таким образом, что ее
тип меняется с int на double1. Методы, определенные в классе IntegerCalculator,
видят целочисленную переменную sum , а методы, определенные в классе
DecimalCalculator, видят вещественную переменную sum. Тем не менее в конкретном экземпляре DecimalCalculator продолжают существовать обе переменные, которые могут принимать независимые значения. При этом любые
методы, наследуемые классом DecimalCalculator от IntegerCalculator, видят
целочисленную переменную sum.
Так как в DecimalCalculator существуют обе переменные, нам нужен способ,
позволяющий ссылаться на переменную, унаследованную от IntegerCalculator.
Для этого ссылка уточняется ключевым словом super:
int s = super.sum;

В классе DecimalCalculator ключевое слово super, используемое таким образом,
выбирает переменную sum, определенную в суперклассе. Вскоре мы подробнее
рассмотрим использование ключевого слова super.
Другое важное обстоятельство, касающееся замещения переменных, связано
с тем, как они работают при обращении к объекту через родительский тип. Например, на объект DecimalCalculator можно сослаться как на IntegerCalculator,
используя для этого переменную типа IntegerCalculator. Если вы так сделаете,
а затем обратитесь к переменной sum, то получите целочисленную переменную
вместо вещественной:
DecimalCalculator dc = new DecimalCalculator();
IntegerCalculator ic = dc;
int s = ic.sum; // Обращение к sum из IntegerCalculator

То же самое произошло бы при обращении к объекту с применением явного
преобразования к типу IntegerCalculator или при передаче экземпляра методу,
получающему родительский тип.
1

Правильнее было бы создать абстрактный класс Calculator с двумя субклассами:
IntegerCalculator и DecimalCalculator.

194  Глава 5. Объекты в Java
Подчеркнем, что польза от замещения переменных невелика. Значительно
лучше абстрагировать использование переменных другими способами вместо
того, чтобы составлять хитроумные правила видимости. Тем не менее важно
понять суть происходящего, прежде чем говорить о том, как то же самое происходит с методами. Сейчас мы рассмотрим, как одни методы замещаются
другими, или, в более правильной терминологии, как происходит переопределение методов.

Переопределение методов
Вы уже знаете, что в классе можно объявлять перегруженные методы (то есть
методы с тем же именем, но с другим количеством или типом аргументов). Выбор перегруженных методов работает так, как мы рассказали, для всех методов,
доступных классу, включая унаследованные. Это означает, что субкласс может
определять дополнительные перегруженные методы вдобавок к перегруженным
методам, предоставленным суперклассом.
Кроме того, субкласс может определить метод с точно такой же сигнатурой
(с таким же именем и с такими же типами аргументов), как у метода в суперклассе. В этом случае метод субкласса переопределяет метод суперкласса
и фактически заменяет его реализацию (рис. 5.8). Переопределение методов
для изменения поведения (логики работы) объектов называется полиморфизмом подтипов. Именно этот механизм обычно имеют в виду, когда говорят
о мощи объектно-ориентированных языков.

Рис. 5.8. Переопределение методов
На рис. 5.8 класс Mammal переопределяет метод reproduce() класса Animal —
очевидно, описывающий поведение млекопитающих (Mammal) при рождении

Нетривиальное проектирование классов  195

новых особей1. Для Cat можно также переопределить поведение сна, которое
отличается от поведения обобщенной особи Animal, например привычку к кратковременному сну. Класс Cat также добавляет более уникальное поведение:
мурлыканье (purr()) и охоту на мышей (huntMice()).
После всего, что вы видели до настоящего момента, может показаться, что
переопределенные методы просто замещают методы суперклассов по аналогии
с переменными. Но в действительности переопределенные методы обладают
более широкими возможностями. Если в иерархии наследования существует
несколько реализаций метода, то версия из «самого последнего» класса (то есть
находящегося ниже всех в иерархии) всегда переопределяет все остальные, даже
если вы ссылаетесь на объект по ссылке на тип одного из суперклассов2.
Например, если имеется экземпляр Cat, присвоенный переменной более общего типа Animal, то при вызове его метода sleep() вы получите метод sleep(),
реализованный в классе Cat, а не версию из Animal:
Cat simon = new Cat();
Animal creature = simon;
...
creature.sleep(); // Вызывается версия sleep() класса Cat

Иначе говоря, в отношении поведения (вызова методов) класс Cat ведет себя
как Cat независимо от того, ссылаетесь ли вы на него через это имя. В остальных
отношениях переменная creature в этом примере может вести себя как ссылка
на Animal. Как объяснялось ранее, при обращении к замещенной переменной по
ссылке Animal вы получите реализацию из класса Animal, а не из класса Cat. Тем не
менее, поскольку поиск методов выполняется динамически, начиная с субклассов,
будет вызван подходящий метод класса Cat, несмотря на то что мы работаем с ним
на более общем уровне как с объектом Animal. Это означает, что поведение объектов
определяется динамически. Вы можете работать со специализированными объектами так, словно они относятся к более общим типам, и при этом пользоваться
преимуществами их специализированных реализаций поведения.

Интерфейсы
Интерфейсы Java расширяют концепцию абстрактных методов. Часто бывает
нужно создать группу абстрактных методов, определяющих некоторое поведе1

2

Для такого необычного яйцекладущего млекопитающего, как утконос (platypus), имело
бы смысл снова переопределить поведение reproduce() в отдельном субклассе класса
Mammal.
Переопределенные методы в Java похожи на виртуальные методы в C++.

196  Глава 5. Объекты в Java
ние объекта без привязки к какой-либо реализации. В Java эта концепция называется интерфейсом. Интерфейс определяет набор методов, которые должны
быть реализованы классом. Класс в Java может объявить, что он реализует
интерфейс, если он реализует этот набор методов. В отличие от расширения
абстрактного класса, класс, реализующий интерфейс, не обязан наследовать от
какой-то конкретной части иерархии наследования или использовать конкретную реализацию.
Интерфейсы можно сравнить со скаутскими нашивками. Скаут, научившийся делать скворечники, может носить нашивку с изображением скворечника.
Тем самым он сообщает окружающему миру: «Я умею строить скворечники».
Аналогичным образом интерфейс представляет собой список методов, определяющих некоторое поведение объекта. Любой класс, реализующий все методы,
указанные в интерфейсе, может заявить на стадии компиляции, что он реализует
интерфейс. Тогда этот класс получит, словно нашивку, дополнительный тип —
тип этого интерфейса.
Типы интерфейсов очень похожи на типы классов. Вы можете объявить переменную с типом интерфейса; вы можете объявить аргументы методов с типами
интерфейсов; вы можете указать, что возвращаемый тип метода является типом
интерфейса. В каждом случае имеется в виду, что любой объект,реализующий
интерфейс (имеющий «нашивку»), подойдет на эту роль. В этом смысле интерфейсы независимы от иерархии классов. Интерфейсы выходят за границы того,
к какой разновидности объектов относится некоторый объект, и взаимодействуют с ним исключительно на уровне того, что он может делать. Класс может
реализовать столько интерфейсов, сколько потребуется. В этом отношении
интерфейсы Java в значительной мере снимают необходимость во множественном наследовании, которое применяется в других языках (и во всех неприятных
последствиях его применения).
По сути, интерфейс выглядит как чисто абстрактный класс (то есть класс, содержащий только абстрактные методы). Интерфейс определяется ключевым
словом interface и перечислением его методов без тела — только прототипов
(сигнатур):
interface Driveable {
boolean startEngine();
void stopEngine();
float accelerate( float acc );
boolean turn( Direction dir );
}

В этом примере определяется интерфейс с именем Driveable, состоящий из
четырех методов. Методы интерфейса могут объявляться с модификатором
abstract, хотя это и не обязательно; в данном случае мы не стали этого делать.
Что еще важнее, методы интерфейса всегда считаются открытыми, и при жела-

Нетривиальное проектирование классов  197

нии их можно объявить с ключевым словом public. Почему открытыми? Без
этого пользователь интерфейса может их попросту не увидеть, а интерфейсы
обычно предназначаются для описания поведения объекта, а не его реализации.
Интерфейсы определяют функциональность, поэтому имена интерфейсов часто
выбираются в соответствии с их назначением. Driveable, Runnable, Updateable —
все эти имена хорошо подходят для интерфейсов. Любой класс, реализующий
все эти методы, может объявить, что он реализует интерфейс; для этого в определение класса включается специальная секция implements. Пример:
class Automobile implements Driveable {
...
public boolean startEngine() {
if ( notTooCold )
engineRunning = true;
...
}
public void stopEngine() {
engineRunning = false;
}
public float accelerate( float acc ) {
...
}

}

public boolean turn( Direction dir ) {
...
}
...

Здесь класс Automobile реализует методы интерфейса Driveable и объявляет
о реализации Driveable при помощи ключевого слова implements.
Как показано на рис. 5.9, другой класс (например, Lawnmower) тоже может реализовать интерфейс Driveable. На диаграмме приведены реализации интерфейса
Driveable из двух разных классов. Вероятно, что Automobile и Lawnmower происходят от некоторого класса, представляющего примитивное транспортное
средство, но в данном сценарии это не обязательно.
После объявления интерфейса у вас появляется новый тип Driveable. Вы можете объявить переменную типа Driveable и присвоить ей любой экземпляр
объекта Driveable:
Automobile auto = new Automobile();
Lawnmower mower = new Lawnmower();
Driveable vehicle;
vehicle = auto;

198  Глава 5. Объекты в Java
vehicle.startEngine();
vehicle.stopEngine();
vehicle = mower;
vehicle.startEngine();
vehicle.stopEngine();

Рис. 5.9. Реализация интерфейса Driveable
Как Automobile, так и Lawnmower реализуют интерфейс Driveable, поэтому их
можно считать взаимозаменяемыми объектами этого типа.

Внутренние классы
Все классы, встречавшиеся до настоящего момента в книге, были высокоуровневыми, «полноценными» классами, объявленными на уровне файлов или пакетов.
Однако классы в Java могут объявляться на любом уровне видимости, в любой
паре фигурных скобок (то есть почти везде, где может размещаться любая другая
команда Java). Такой внутренний класс (inner class) принадлежит другому классу
или методу, подобно переменной, а обращение к нему аналогичным образом
может ограничиваться его областью видимости. Внутренние классы — полезное
и эстетичное средство структурирования кода. Их родственники, анонимные
внутренние классы, дают возможность еще более мощной сокращенной записи,
которая выглядит так, словно вы можете динамически создавать новые виды
объектов в среде Java со статической типизацией. В Java анонимные внутренние классы отчасти занимают место применяемых в других языках замыканий

Нетривиальное проектирование классов  199

(closures), реализуя эффект работы с состоянием и поведением независимо от
классов.
Тем не менее, когда вы начинаете разбираться во внутренних принципах их
работы, становится понятно, что внутренние классы не так динамичны и элегантны, как кажется на первый взгляд. Внутренние классы представляют собой
исключительно синтаксическое удобство; они не поддерживаются виртуальной
машиной и вместо этого отображаются компилятором на обычные классы Java.
Вам как программисту никогда не придется беспокоиться об этом; вы можете
просто положиться на внутренние классы как на любую другую языковую
конструкцию. Тем не менее вам надо немного узнать о том, как работают внутренние классы, чтобы лучше понимать скомпилированный код и некоторые
потенциальные побочные эффекты.
Внутренние классы по своей сути являются вложенными классами. Пример:
class Animal {
class Brain {
...
}
}

Здесь класс Brain является внутренним: он объявляется внутри области видимости класса Animal. И хотя смысл этого утверждения потребует некоторых
пояснений, для начала скажем, что Java старается по возможности приблизить
этот смысл к другим элементам (методам и переменным), находящимся на этом
уровне видимости. Например, добавим метод в класс Animal:
class Animal {
class Brain {
...
}
void performBehavior() { ... }
}

Как внутренний класс Brain, так и метод performBehavior() находится в области
видимости Animal. Следовательно, в любой точке Animal можно обращаться
к Brain и к performBehavior() напрямую, по имени. Внутри класса Animal можно
вызвать конструктор Brain (new Brain()), чтобы получить объект Brain, или вызвать метод performBehavior() для отработки функциональности этого метода.
Но ни Brain, ни performBehavior() не будут доступны за пределами класса Animal
без дополнительного уточнения.
В теле внутреннего класса Brain и в теле метода performBehavior() мы можем
обращаться ко всем остальным методам и переменным класса Animal. Таким
образом, подобно тому как метод performBehavior() может работать с классом
Brain и создавать экземпляры Brain, методы внутри класса Brain могут вызывать

200  Глава 5. Объекты в Java
метод performBehavior() класса Animal, а также работать со всеми остальными
методами и переменными, объявленными в Animal. Класс Brain «видит» все
методы и переменные класса Animal прямо в своей области видимости.
Последний факт имеет важные последствия. Из Brain можно вызвать метод
performBehavior(); другими словами, из экземпляра Brain можно вызвать метод performBehavior() экземпляра Animal… Но из какого именно экземпляра
Animal ? Если существует несколько объектов Animal (допустим, несколько
объектов Cat и Dog), то необходимо знать, для какого из них вызывается метод
performBehavior(). Что это значит, когда определение класса находится «внутри» другого определения класса? Ответ: объект Brain всегда находится внутри
одного экземпляра Animal — того, который был ему известен при создании.
Назовем объект, содержащий какой-либо экземпляр Brain, его замыкающим
экземпляром.
Объект Brain не может существовать вне замыкающего экземпляра объекта
Animal. Везде, где вы видите экземпляр Brain, он будет привязан к экземпляру
Animal. И хотя объект Brain можно сконструировать из любой точки (то есть из
другого класса), Brain всегда требует замыкающего экземпляра Animal, в котором он будет «храниться». Также можно сказать, что если вы будете обращаться
к Brain за пределами Animal, то такое обращение будет выглядеть как Animal.
Brain. И как и в случае с методом performBehavior(), к нему можно применять
модификаторы для ограничения его видимости. При этом действуют все обычные модификаторы видимости, а внутренние классы также можно объявлять
статическими, как будет показано ниже.

Анонимные внутренние классы
А теперь начинается самое интересное. Как правило, чем глубже инкапсулированы и ограничены в видимости наши классы, тем больше у нас свободы для выбора их имен. Вопрос не сводится к чисто эстетическим предпочтениям. Выбор
имен — важный аспект написания понятного, простого в сопровождении кода.
Как правило, надо использовать настолько компактные и содержательные имена,
насколько это возможно. Как следствие, желательно обойтись без изобретения
имен для эфемерных объектов, которые будут использованы всего один раз.
Анонимные внутренние классы являются расширением синтаксиса операции
new. При создании анонимного внутреннего класса его объявление объединяется с выделением памяти для экземпляра класса, что приводит к фактическому
созданию «одноразового» класса и экземпляра этого класса в одной операции.
После ключевого слова new указывается имя класса или интерфейса, за которым
идет тело класса. Тело класса становится внутренним классом, который либо
расширяет заданный класс, либо (в случае интерфейса) реализует интерфейс.
Один экземпляр класса создается и возвращается как значение.

Нетривиальное проектирование классов  201

Например, можно вернуться к графическому приложению из раздела «HelloJava2:
продолжение», с. 79. В нем создается класс HelloComponent2, который расширяет JComponent и реализует интерфейс MouseMotionListener. Присмотревшись
к этому примеру повнимательнее, можно заметить, что HelloComponent2 никогда не будет реагировать на события перемещения мыши, поступающие от
других компонентов. Будет намного логичнее создать анонимный внутренний
класс специально для перемещения надписи «Hello». В самом деле, компонент
HelloComponent2 предназначен для использования исключительно в нашем демонстрационном приложении. Можно провести рефакторинг (широко практикуемый разработчиками процесс доработки или оптимизации уже работающего
кода), в результате которого класс будет выделен во внутренний класс. Теперь,
когда вы больше знаете о конструкторах и наследовании, класс можно преобразовать в расширение JFrame, вместо того чтобы создавать окно в методе main().
Ниже приведен код приложения HelloJava3, полученный после рефакторинга:
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
public class HelloJava3 extends JFrame {
public static void main( String[] args ) {
HelloJava3 demo = new HelloJava3();
demo.setVisible( true );
}
public HelloJava3() {
super( "HelloJava3" );
add( new HelloComponent3("Hello, Inner Java!") );
setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
setSize( 300, 300 );
}
class HelloComponent3 extends JComponent {
String theMessage;
int messageX = 125, messageY = 95; // Координаты надписи
public HelloComponent3( String message ) {
theMessage = message;
addMouseMotionListener(new MouseMotionListener() {
public void mouseDragged(MouseEvent e) {
messageX = e.getX();
messageY = e.getY();
repaint();
}

}

});

public void mouseMoved(MouseEvent e) { }

202  Глава 5. Объекты в Java

}

}

public void paintComponent( Graphics g ) {
g.drawString( theMessage, messageX, messageY );
}

Попробуйте скомпилировать и запустить этот код. Он должен работать точно
так же, как и исходное приложение HelloJava2. Отличается только то, как мы
упорядочили классы и кто может обращаться к ним (а также к находящимся
в них переменным и методам).

Систематизация кода и планирование
на случай ошибок
Безусловно, классы — самая важная концепция языка Java. Они являются
основой каждой исполняемой программы, портируемой библиотеки или вспомогательного модуля. Мы рассмотрели, из чего состоят классы и как они связываются друг с другом в больших проектах. Вы узнали о создании и уничтожении
объектов, а в конце главы было показано, как внутренние классы (и анонимные
внутренние классы) помогают писать более простой в сопровождении код. Примеры внутренних классов будут встречаться при рассмотрении более сложных
тем, таких как потоки (глава 9) и Swing (глава 10).
При создании классов надо учитывать некоторые важные рекомендации:
Старайтесь скрывать как можно большую часть реализации. Никогда не
раскрывайте больше подробностей внутреннего строения объекта, чем абсолютно необходимо. Этот принцип играет ключевую роль при написании
кода, простого в сопровождении и пригодного для повторного использования. Избегайте открытых переменных в своих объектах (возможно, за исключением констант). Вместо этого создавайте методы доступа (accessor
methods) для чтения и записи значений (даже если они относятся к простым
типам). Позднее, когда потребуется, вы сможете изменять и расширять поведение своих объектов без нарушения работоспособности других классов,
которые от них зависят.
Специализируйте объекты только там, где это необходимо, — старайтесь использовать композицию вместо наследования. Под композицией понимается
использование объекта в его существующей форме как составляющей нового
объекта. Когда вы изменяете или уточняете поведение объекта (посредством
субклассирования), вы применяете наследование. Для повторного использования объектов надо по возможности отдавать предпочтение композиции
перед наследованием, потому что при композиции объектов вы в полной
мере пользуетесь возможностями существующих инструментов. Наследова-

Систематизация кода и планирование на случай ошибок  203

ние приводит к нарушению инкапсуляции объектов, и применять его следует
только тогда, когда оно создает реальные преимущества. Спросите себя, действительно ли вы хотите наследовать от всего класса (хотите ли вы создать
новый «частный случай» этого объекта?) или же можно включить экземпляр
этого класса в ваш класс и делегировать часть работы включенному объекту.
Минимизируйте отношения между объектами и постарайтесь организовать
взаимосвязанные объекты в пакеты. Классы, тесно взаимодействующие
друг с другом, можно сгруппировать в пакеты Java (вспомните рис. 5.1); при
этом также можно спрятать классы, не предназначенные для внешнего использования. Открывайте доступ только к классам, предназначенным для
использования другими разработчиками. Чем слабее связаны ваши объекты, тем проще будет повторно использовать их в будущем.
Эти принципы желательно применять даже в небольших проектах. Примеры
из папки ch05 содержат простые версии классов и интерфейсов, которые будут
нужны для создания игры с бросанием яблок. Посмотрите, как классы Apple,
Tree и Physicist реализуют интерфейс GamePiece — например, метод draw(),
присутствующий в каждом классе. Обратите внимание на то, как Field расширяет JComponent и как главный игровой класс AppleToss расширяет JFrame.
На рис. 5.10 (честно говоря, пока еще не впечатляющем) показан пример

Рис. 5.10. Классы из нашей игры в действии

204  Глава 5. Объекты в Java
совместной работы этих компонентов. Чтобы опробовать его самостоятельно,
скомпилируйте и запустите класс ch05.AppleToss так, как было описано в разделе «Пакеты», с. 183.
Просмотрите комментарии в классах. Попробуйте что-нибудь изменить. Добавьте еще одно дерево. В следующих главах мы будем развивать приложение
на основе этих классов, поэтому если вы заранее разберетесь в том, как они
взаимодействуют, вам будет проще понять дальнейший материал.
Как бы вы ни упорядочили элементы своих классов, классы ваших пакетов или
пакеты ваших проектов, вы неизбежно столкнетесь с ошибками. Одни из них
будут простыми синтаксическими ошибками, которые вы исправите в редакторе.
Другие ошибки интереснее — они встречаются только во время выполнения
вашей программы. Следующая глава расскажет об этих ошибках с точки зрения
Java, а также поможет вам организовать их обработку.

ГЛАВА 6

Обработка ошибок
и запись в журнал

Язык Java глубоко укоренился во встроенных системах (embedded systems). Так
называется программное обеспечение мобильных компьютеров, сотовых телефонов, «умных тостеров» и многих других гаджетов, объединенных «интернетом вещей» (IoT, Internet of Things). В таких приложениях особенно важно
предусмотреть надежную обработку ошибок. Многие согласятся с тем, что их
телефон не должен зависать, а тосты — гореть (возможно, вместе с домом)
из-за программных сбоев. Мы знаем, что нельзя полностью исключить возможность возникновения ошибок. Но выявление и методичная обработка
предполагаемых ошибок на уровне приложения будут шагом в правильном
направлении.
В некоторых языках всю ответственность за обработку ошибок несет программист. Сам язык никак не помогает ему распознавать ошибки и не предоставляет
средства для удобной работы с ними. В языке C функция обычно сообщает об
ошибке, возвращая «нереалистичное» значение (например, традиционное –1
или null). А программисту надо выяснить, в чем заключается плохой результат
и что он означает. Во многих случаях бывает неудобно обходить ограничения,
вызванные передачей значений ошибок, сохраняя при этом нормальный путь
выполнения программы1. Есть и более сложная проблема: ошибки некоторых
типов могут вполне законно возникать почти повсюду, но неразумно и невозможно добавлять соответствующие проверки в каждую точку кода.
В этой главе мы покажем, как столь актуальная «проблема всех проблем» решена
в Java. Сначала мы рассмотрим понятие исключений, чтобы показать, как и по1

Функции C, имеющие неочевидные имена setjmp() и longjmp(), позволяют сохранить
нужную точку в процессе выполнения кода, а потом безусловно вернуться к ней, даже
из очень отдаленного места. В каком-то смысле это соответствует функциональности
исключений языка Java.

206  Глава 6. Обработка ошибок и запись в журнал
чему они возникают, как и где их следует обрабатывать. Также мы рассмотрим
ошибки и проверочные утверждения. Ошибки — это настолько серьезные проблемы, что их, как правило, невозможно устранить во время выполнения, но
можно записать в журнал (в лог-файл) для последующей отладки. Проверочные
утверждения — это широко применяемый способ защиты кода от исключений
и ошибок путем предварительной проверки условий, необходимых для безо­
пасного выполнения тех или иных действий.

Исключения
В Java есть элегантный механизм исключений, который помогает программисту
решать типичные проблемы, возникающие при написании и выполнении кода.
(Обработка исключений в Java в какой-то степени похожа на обработку исключений в C++.) Исключение указывает на возникновение ненормального или
ошибочного состояния. В этот момент программа безусловно передает управление («выдает исключение», или «выбрасывает исключение») в специально
выделенную часть кода, где это состояние перехватывается и обрабатывается.
Таким образом обработка ошибок выполняется независимо от нормальной последовательности выполнения программы. Вам не приходится использовать
специальные возвращаемые значения во всех методах; ошибки обрабатываются
отдельным механизмом. Управление может передаваться на большое расстояние
от глубоко вложенных процедур, поэтому вы можете обрабатывать все ошибки
в каком-то одном удобном для этого месте (или каждую ошибку — там, где она
возникла). Некоторые стандартные методы Java API по-прежнему возвращают
–1 в качестве специального значения, но обычно это ограничивается теми случаями, когда вы ожидаете получить специальное значение, а ситуация не выходит
за рамки допустимого1.
В каждом методе Java должны быть объявлены все проверяемые исключения,
которые он может выдавать; а компилятор следит за тем, чтобы они обрабатывались при каждом вызове этого метода. Таким образом, информация об
ошибках, которые могут возникать в методе, считается настолько же важной,
как типы его аргументов и возвращаемого значения. Впрочем, если вы очень
захотите, то Java позволит вам игнорировать очевидные ошибки, но в таких
случаях это придется делать явным образом. (Вскоре мы рассмотрим те ошибки
и исключения времени выполнения, которые не обязательно объявлять или
обрабатывать в методах.)

1

Например, метод getHeight() класса Image возвращает –1, если высота еще неизвестна.
Ошибка при этом не возникает; значение высоты может появиться в будущем. В такой
ситуации незачем выдавать исключение, это снизило бы эффективность кода.

Исключения  207

Исключения и классы ошибок
Исключения представляют собой экземпляры класса java.lang.Exception и его
субклассов. Субклассы Exception могут содержать специализированную информацию (и иногда определять логику работы) для разных видов исключительных
ситуаций. Но чаще всего это простые «логические» субклассы, которые нужны
только для определения новых типов исключений. На рис. 6.1 показаны субклассы Exception из пакета java.lang. Эта диаграмма дает представление о том,
как организованы исключения; структура классов будет подробнее рассмотрена
в следующей главе. В большинстве других пакетов определяются их собственные типы исключений, которые обычно являются субклассами Exception или
его очень важного субкласса RuntimeException, о котором мы вскоре расскажем.
Например, один из самых необходимых классов исключений — это
IOException из пакета java.io. Класс IOException расширяет Exception и многие его субклассы для решения типичных проблем ввода-вывода (например,
FileNotFoundException) и сетевых проблем (например, MalformedURLException).
Сетевые исключения входят в пакет java.net.

Условные
КЛАСС
обозначения:
расширяет

Рис. 6.1. Субклассы java.lang.Exception

Условные
обозначения:

208  Глава 6. Обработка ошибок и запись в журнал
Объект Exception создается в той точке, где возникло ошибочное состояние.
Он может содержать любую информацию, необходимую для описания этого
исключительного состояния, а также включает полную трассировку стека для
отладки. Трассировка стека — это длинный (иногда очень длинный) список всех
вызванных методов с последовательностью их вызова до точки возникновения
исключения. Использование данных трассировки более подробно рассматривается в разделе «Трассировка стека», с. 213. При передаче управления объект
Exception передается обрабатывающему блоку кода в качестве аргумента. Названия команд throw («выбрасывать») и catch («перехватывать») объясняются
тем, что объект Exception «выбрасывается» из одной точки кода и «перехватывается» в другой точке, где выполнение продолжается.
В Java API также есть класс java.lang.Error, предназначенный для тех ошибок, после которых продолжение программы невозможно. Субклассы Error из
пакета java.lang показаны на рис. 6.2. Среди типов Error заслуживает особого
внимания тип AssertionError, используемый командой assert для обозначения
неудачи при проверке (проверочные утверждения Java рассматриваются далее
в этой главе). В некоторых других пакетах есть собственные субклассы Error, но
вообще субклассы Error встречаются намного реже (и приносят меньше пользы),
чем субклассы Exception. Когда вы пишете код, вам не надо беспокоиться об

Условные
обозначения:
КЛАСС
АБСТРАКТНЫЙ КЛАСС

расширяет

Рис. 6.2. Субклассы java.lang.Error

Исключения  209

исключениях Error (то есть вам не придется их перехватывать); они указывают
на фатальные проблемы или ошибки виртуальной машины. При возникновении такой ошибки интерпретатор Java обычно выводит сообщение и завершает
работу. Даже не пробуйте перехватывать эти исключения и восстанавливать
работу программы, потому что они являются признаком неустранимого сбоя,
а не допустимой ситуации.
Exception и Error — это субклассы класса Throwable. Он является базовым классом для всех объектов, которые могут «выбрасываться» командой throw. Как
правило, вам надо будет расширять только классы Exception и Error, а также

их субклассы.

Обработка исключений
«Сторожевые» команды try / catch формируют блок кода и перехватывают
возникающие в нем исключения указанных типов:
try {
readFromFile("foo");
...
}
catch ( Exception e ) {
// Обработка ошибки
System.out.println( "Exception while reading file: " + e );
...
}

В этом примере все исключения, возникающие в теле секции try, передаются
в секцию catch, где могут обрабатываться. Секция catch работает как метод,
который определяет тип обрабатываемого исключения, а при вызове получает
объект Exception в качестве аргумента. Этот объект передается в переменной e
и «выводится на экран» вместе с сообщением.
Вспомните простую программу из главы 4, вычисляющую наибольший общий
делитель по алгоритму Евклида. Сейчас мы ее доработаем, чтобы пользователь
передавал два числа, a и b, в аргументах командной строки, то есть через массив
args[] в методе main(). Но этот массив имеет тип String. Если мы немного схитрим
и подсмотрим на пару глав вперед, то можем использовать метод из раздела «Разбор примитивных чисел», с. 271, чтобы преобразовать эти аргументы в значения
типа int. Тем не менее этот метод может выдать исключение, если передать ему
вместо допустимого числа что-то другое. Взгляните на наш новый класс Euclid2:
public class Euclid2 {
public static void main(String args[]) {
int a = 2701;
int b = 222;

210  Глава 6. Обработка ошибок и запись в журнал

}

}

// Мы будем разбирать аргументы, только если их ровно 2
if (args.length == 2) {
try {
a = Integer.parseInt(args[0]);
b = Integer.parseInt(args[1]);
} catch (NumberFormatException nfe) {
System.err.println("Arguments were not both numbers.
Using defaults.");
}
} else {
System.err.println("Wrong number of arguments (expected 2).
Using defaults.");
}
System.out.print("The GCD of " + a + " and " + b + " is ");
while (b != 0) {
if (a > b) {
a = a - b;
} else {
b = b - a;
}
}
System.out.println(a);

Запустите эту программу в окне терминала (или воспользуйтесь средствами
передачи аргументов командной строки в IDE, как показано на рис. 2.15), и вы
сможете проверять разные пары чисел без перекомпиляции:
$ javac ch06/Euclid2.java
$ java ch06.Euclid2 18 6
The GCD of 18 and 6 is 6
$ java ch06.Euclid2 547832 2798
The GCD of 547832 and 2798 is 2

Но если при вызове будут переданы аргументы, не являющиеся числами, то вы
получите исключение NumberFormatException и увидите сообщение об ошибке.
Обратите внимание, что программа достойно реагирует на ввод некорректных
значений и выводит сообщение. В этом вся суть обработки ошибок. В реальном
мире ошибки неизбежны, и качество вашего кода зависит от того, как вы их
обрабатываете.
$ java ch06.Euclid2 apples oranges
Arguments were not both numbers. Using defaults.
The GCD of 2701 and 222 is 37

Команда try может иметь несколько секций catch, обрабатывающих разные
типы исключений (субклассы Exception):

Исключения  211
try {
readFromFile("foo");
...
}
catch ( FileNotFoundException e ) {
// Обработка ошибки "файл не найден"
...
}
catch ( IOException e ) {
// Обработка ошибки чтения
...
}
catch ( Exception e ) {
// Обработка всех остальных ошибок
...
}

Секции catch рассматриваются по порядку, и выбирается первый подходящий
вариант. При этом будет выполнена максимум одна секция catch; это значит,
что исключения должны перечисляться в порядке от более конкретных к менее
конкретным. В предыдущем примере FileNotFoundException является субклассом
IOException, поэтому если бы первой секции catch не было, то исключение в данном случае перехватила бы вторая секция. Аналогичным образом любой субкласс
Exception совместим с родительским типом Exception по присваиванию, так что
третья секция catch перехватит все то, что пропустят первые две. Она, подобно
секции default в команде switch, обрабатывает все оставшиеся возможности.
Таким образом мы обеспечили полную обработку, но в общем случае старайтесь
указывать типы перехватываемых исключений как можно конкретнее.
Одно из преимуществ схемы try / catch заключается в том, что любая команда
в блоке try может быть уверена, что все предыдущие команды в блоке были
выполнены успешно. Например, не возникнет внезапной проблемы из-за того,
что программист забыл проверить возвращаемое методом значение. Если в более ранней команде произошла ошибка, то выполнение немедленно передается
в секцию catch; последующие команды никогда не будут выполнены.
В Java 7 появилась альтернатива для использования нескольких секций catch —
обработка нескольких типов исключений в одной секции catch с использованием
синтаксиса |:
try {
// Чтение из сети...
// Запись в файл...
catch ( ZipException | SSLException e ) {
logException( e );
}

Синтаксис | позволяет принимать оба типа исключений в общей секции catch.
Какой же тип у переменной e, передаваемой нашему методу logException()?

212  Глава 6. Обработка ошибок и запись в журнал
(Что с ней можно сделать?) В данном случае этот тип — не ZipException и не
SSLException, а IOException — ближайший общий предок этих двух исключений (тип ближайшего родительского класса, с которым они оба совместимы по
присваиванию). Во многих подобных случаях ближайшим общим типом для
нескольких исключений может быть Exception — родительский тип всех исключений. В отличие от простого перехвата исключений родительского типа,
при перехвате нескольких исключений разных типов в общей секции catch мы
ограничиваемся только ими, явно перечисленными, и пропускаем все остальные типы IOException. Вы можете использовать в секциях catch синтаксис |
и одновременно упорядочивать эти секции — от самых конкретных к самым
общим. Это сочетание позволит вам очень гибко их структурировать. Вы можете
консолидировать всю логику обработки ошибок там, где это удобно, причем без
лишних повторов кода. У этой возможности есть и другие нюансы, и мы к ней
еще вернемся после обсуждения выдачи и повторной выдачи исключений.

Всплывающие исключения
А если исключение не будет перехвачено? Куда оно отправится? Если точка
возникновения исключения не заключена в команду try / catch, то исключение
«всплывает» из метода, где оно возникло, то есть передается из него в тот метод, из
которого он был вызван. Если в вызывающем методе эта точка заключена в блок
try, то управление передается соответствующей секции catch. В ином случае исключение продолжает всплывать вверх по стеку вызовов: от вызываемого метода
к вызывающему. Таким образом исключение всплывает до тех пор, пока не будет
перехвачено или пока не поднимется до самого верхнего уровня программы, что
приведет к ее завершению с сообщением об ошибке времени выполнения. На
самом деле ситуация немного сложнее, потому что компилятор может заставить
нас решить проблему где-то в середине этого пути. В разделе «Проверяемые
и непроверяемые исключения», с. 214, эта тема рассматривается более подробно.
Рассмотрим еще один пример. На рис. 6.3 показано, как метод getContent()
вызывает метод openConnection() из команды try / catch. В свою очередь,
openConnection() вызывает метод sendRequest(), который вызывает метод
write() для отправки некоторых данных.
Как видите, во втором вызове write() выдается исключение IOException. Так
как sendRequest() не содержит команды try / catch для обработки исключения,
оно снова выдается в точке вызова — в методе openConnection(). Поскольку
метод openConnection() тоже не перехватывает исключение, оно выдается еще
раз. И в итоге оно перехватывается командой try в методе getContent() и обрабатывается его секцией catch. Обратите внимание: каждый метод, выдающий
исключение, должен объявить его конкретный тип в секции throws. Мы подробнее
расскажем об этом в разделе «Проверяемые и непроверяемые исключения», с. 214.

Исключения  213

Рис. 6.3. Распространение исключений
Если вы включите высокоуровневую команду try в начало вашего кода, это поможет вам обработать ошибки, которые могут всплывать из фоновых потоков.
Потоки рассматриваются в главе 9, а пока заметим, что неперехваченные исключения могут создавать много проблем при отладке больших и сложных программ.

Трассировка стека
Исключение может пройти немалый путь, прежде чем будет перехвачено и обработано, поэтому нам нужен надежный способ находить ту точку, где оно
возникло. И очень важно знать всю предысторию, то есть ту цепочку вызовов
методов, которая ведет к этой точке. Для целей отладки и журналирования (логирования) каждое исключение может выводить трассировку стека, в которой
указывается метод, породивший исключение, и затем все вложенные вызовы
других методов. Обычно пользователь видит трассировку стека, выводимую
методом printStackTrace().
try {
// Сложная задача со многими уровнями вложенности
} catch ( Exception e ) {
// Вывод информации о том, где возникло исключение
e.printStackTrace( System.err );
...
}

Например, трассировка стека для исключения может выглядеть так:
java.io.FileNotFoundException: myfile.xml
at java.io.FileInputStream.(FileInputStream.java)
at java.io.FileInputStream.(FileInputStream.java)
at MyApplication.loadFile(MyApplication.java:137)
at MyApplication.main(MyApplication.java:5)

Из этой трассировки видно, что метод main() класса MyApplication вызвал метод
loadFile(). Затем метод loadFile() попытался создать объект FileInputStream,

214  Глава 6. Обработка ошибок и запись в журнал
который выдал исключение FileNotFoundException. Учтите, что при достижении
уровня системных классов Java (таких, как FileInputStream) нумерация строк
может пропасть. Это может случиться и при оптимизации кода в некоторых виртуальных машинах. Обычно существует возможность временного отключения
оптимизации для получения точных номеров строк. Тем не менее в особенно
коварных ситуациях изменение хронометража приложения может влиять на
проблему, которую вы пытаетесь отслеживать, и тогда вам придется прибегнуть
к другим средствам отладки.
При отладке исключений вы также можете программно получать трассировку
стека с помощью метода getStackTrace() класса Throwable. (Throwable — базовый класс для Exception и Error.) Этот метод возвращает массив объектов
StackTraceElement, соответствующий всему стеку вызванных методов. Подробности о них вы можете получить из StackTraceElement, используя методы
getFileName(), getClassName(), getMethodName() и getLineNumber(). Нулевой
элемент массива соответствует вершине стека, то есть той конкретной строке
кода, которая породила исключение. Каждый последующий элемент массива
отступает на один уровень, пока не будет достигнут исходный метод main().

Проверяемые и непроверяемые исключения
Ранее мы уже упоминали, что Java заставляет нас явно выражать свои намерения
относительно обработки ошибок. Но нет необходимости обрабатывать все возможные типы ошибок во всех возможных ситуациях. Поэтому исключения Java
делятся на две категории: проверяемые и непроверяемые. Большинство исключений уровня приложения относится к категории проверяемых; это означает, что
любой метод, выдающий исключение, — либо генерирующий его самостоятельно
(см. «Выдача исключений», с. 215), либо игнорирующий произошедшее в нем
исключение, — должен объявить, что он может выдавать исключение этого типа,
в специальной секции throws в объявлении метода. Сейчас вам надо запомнить,
что в методе обязательно должны быть объявлены проверяемые исключения,
которые он может выдавать или передавать от других методов.
Еще раз взгляните на рис. 6.3 и обратите внимание: методы openConnection() и
sendRequest() указывают, что они могут выдавать исключения типа IOException.
Если метод может выдавать исключения нескольких типов, то их можно перечислить, разделяя запятыми:
void readFile( String s ) throws IOException, InterruptedException {
...
}

Секция throws сообщает компилятору, что метод является возможным источником проверяемого исключения и к его обработке должны быть готовы все,

Исключения  215

кто вызывает этот метод. Вызывающая сторона должна либо использовать блок
try / catch для обработки, либо в свою очередь объявить, что она сама может
выдать это исключение.
Исключения, являющиеся субклассами классов java.lang.RuntimeException
и java.lang.Error, относятся к категории непроверяемых. На рис. 6.1 показаны
субклассы RuntimeException. (Субклассы Error обычно резервируются для серьез­
ных проблем с загрузкой классов или с исполнительной системой.) Если вы игнорируете возможность этих исключений, это не приведет к ошибке компиляции;
методы также не обязаны объявлять, что они могут выдавать эти исключения.
Во всех остальных отношениях непроверяемые исключения ведут себя так же,
как и другие исключения. Вы можете перехватывать их, если хотите, но обычно
в этом нет необходимости.
Проверяемые исключения предназначены для проблем уровня приложения,
таких как отсутствие файлов и недоступность веб-серверов. Вы как хороший
программист (и добропорядочный гражданин) должны проектировать свой код
так, чтобы он корректно восстанавливался в подобных ситуациях. Непроверяемые
исключения предназначены для проблем системного уровня, таких как нехватка
памяти или выход за границы массива. Хотя эти ситуации могут свидетельствовать
об ошибках программирования на уровне приложения, они могут возникать где
угодно, и обычно восстановление после них невозможно. К счастью, поскольку
эти исключения являются непроверяемыми, вам не придется заключать в конструкцию try / catch каждую операцию с индексом массива (или объявлять все
вызывающие методы как потенциальные источники таких исключений).
Подведем итог. Проверяемые исключения — это проблемы, которые нормальное
приложение должно всегда обрабатывать корректно. Непроверяемые исключения (исключения времени выполнения и ошибки) — это проблемы, на восстановление после которых ваше приложение обычно не может рассчитывать.
Исключения типа Error означают фатальные ошибки, то есть в нормальном
приложении вам уже бессмысленно их обрабатывать, стараясь восстановить
работоспособность программы.

Выдача исключений
Вы можете выдавать собственные исключения — экземпляры Exception, экземпляры одного из существующих субклассов или ваших собственных специализированных классов исключений. Все, что для этого нужно, — создать экземпляр
Exception и выдать его командой throw:
throw new IOException();

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

216  Глава 6. Обработка ошибок и запись в журнал
(Вероятно, хранить ссылку на созданный здесь объект нет смысла.) Альтернативный конструктор позволяет указать строку с сообщением об ошибке:
throw new IOException("Sunspots!");

Для получения этой строки можно воспользоваться методом getMessage() объекта Exception. Впрочем, часто можно просто вывести сам объект исключения (или
результат вызова toString()), чтобы получить сообщение и трассировку стека.
По соглашению все типы Exception имеют конструктор String такого вида.
Приведенное выше сообщение String особой пользы не приносит. Обычно используется более конкретный субкласс Exception, предоставляющий подробную информацию или по крайней мере более конкретное строковое описание.
Другой пример:
public void checkRead( String s ) {
if ( new File(s).isAbsolute() || (s.indexOf("..") != -1) )
throw new SecurityException(
"Access to file : "+ s +" denied.");
}

В этом коде частично реализуется метод для проверки недопустимого пути.
Если такой путь будет найден, то выдается исключение SecurityException
с информацией о нарушении.
Конечно, в специализированные субклассы Exception можно включать любую
другую информацию, которая может оказаться полезной. Но чаще всего вам
будет достаточно просто иметь новый тип исключения, чтобы правильно передать управление. Например, при разработке парсера (анализатора текста) вы
можете создать собственный тип исключения для обозначения конкретной
разновидности сбоя:
class ParseException extends Exception {
private int lineNumber;
ParseException() {
super();
this.lineNumber = -1;
}
ParseException( String desc, int lineNumber ) {
super( desc );
this.lineNumber = lineNumber;
}

}

public int getLineNumber() {
return lineNumber;
}

Исключения  217

О классах и конструкторах мы подробно рассказывали в разделе «Конструкторы» на с. 82. Тело нашего класса (субкласса Exception) в этом примере позволяет
создавать исключение ParseException — так же, как мы создавали исключения
ранее (обычные или с небольшой дополнительной информацией). Теперь, когда
у нас есть новый тип исключения, мы можем защититься от сбоя таким образом:
// Где-то в коде
...
try {
parseStream( input );
} catch ( ParseException pe ) {
// Некорректный ввод...
// Мы даже можем назвать строку, где возникла проблема!
} catch ( IOException ioe ) {
// Низкоуровневая проблема с каналом связи
}

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

Сцепление и повторная выдача исключений
Иногда требуется выполнить некоторые действия в зависимости от исключения,
а затем выдать вместо него новое исключение. Такое решение часто встречается
при проектировании фреймворков, в которых низкоуровневые детализированные исключения обрабатываются и заменяются исключениями более высокого
уровня, с которыми проще работать. Представьте, что вам надо перехватить
исключение IOException в коммуникационном пакете, затем выполнить завершающие действия (например, освободить некоторые ресурсы), а в итоге выдать
собственное высокоуровневое исключение (например, LostServerConnection).
Очевидное решение — просто перехватить исключение, а затем выдать новое.
Но тогда вы потеряете важную информацию, в том числе трассировку стека
исходного исключения. Чтобы этого не произошло, вы можете воспользоваться
механизмом сцепления исключений. Это означает, что вы должны соединить исходное исключение с новым исключением, которое выдаете. В Java есть явная
поддержка сцепления исключений. При создании экземпляра базового класса
Exception в аргументе может передаваться исключение или же сообщение типа
String вместе с исключением:
throw new Exception( "Here's the story...", causalException );

Впоследствии вы можете получить доступ к исходному исключению методом
getCause(). Есть и более важная возможность: если вы выводите на экран сообщение об исключении (или если его видит пользователь), то Java автоматически

218  Глава 6. Обработка ошибок и запись в журнал
предоставляет информацию об обоих исключениях вместе с их трассировками
стека.
Вы можете добавлять конструктор такого рода в свои субклассы исключений
(с делегированием родительскому конструктору) или же применять следующий
паттерн, в котором метод initCause() класса Throwable явно определяет исходное исключение после конструирования вашего исключения, но до его выдачи:
try {
// ...
} catch ( IOException cause ) {
Exception e =
new IOException("What we have here is a failure to communicate...");
e.initCause( cause );
throw e;
}

Иногда бывает достаточно записать информацию в журнал (в лог-файл) или
выполнить некоторое действие, а затем повторно выдать исходное исключение:
try {
// ...
} catch ( IOException cause ) {
log( cause ); // Записать в журнал
throw cause; // Выдать исключение повторно
}

Сужение типа при повторной выдаче исключений
До выхода Java 7, если надо было обработать несколько типов исключений
в одной секции catch, а затем заново выдать исходное исключение, неизбежно
приходилось расширить объявляемый тип исключения до того типа, который
требовался для перехвата всех вариантов, или же основательно потрудиться,
чтобы избежать этого. В Java 7 компилятор стал умнее. Теперь он может выполнять большую часть работы за программиста, позволяя в большинстве случаев
сужать типы выдаваемых исключений до исходных типов. Лучше всего пояснить
сказанное примером:
void myMethod() throws ZipException, SSLException
{
try {
// Возможная причина ZipException или SSLException
} catch ( Exception e ) {
log( e );
throw e;
}
}

В этом примере мы действуем примитивно: перехватываем сразу все исключения типа Exception в секции catch, чтобы сохранить данные в журнале перед

Исключения  219

повторной выдачей исключения. До выхода Java 7 компилятор потребовал бы,
чтобы секция throws нашего метода также объявляла о выдаче широкого типа
Exception.
Но теперькомпилятор Java в большинстве случаев способен распознать фактические типы исключений, которые могут выдаваться, позволяя указать в секции
throws конкретный набор типов. В этом примере мы могли бы использовать
и секцию catch с несколькими типами. Такой подход, несмотря на небольшую
потерю наглядности, очень помогает более конкретно обрабатывать исключения (даже в коде, написанном до выхода Java 7) без необходимости трудоемкой
переработки секций catch.

«Расползание» блока try
Команда try устанавливает условие для тех команд, которые она защищает:
если в какой-то из них происходит исключение, то последующие команды не
выполняются. Это имеет последствия для инициализации локальных переменных. Если компилятор не может определить, произойдет ли присваивание
локальной переменной в блоке try / catch, то он не позволит использовать эту
переменную. Пример:
void myMethod() {
int foo;
try {
foo = getResults();
}
catch ( Exception e ) {
...
}
int bar = foo;

// Ошибка компиляции: возможно, переменная foo
// не была инициализирована

В этом примере переменная foo не может использоваться в данном месте, потому что есть вероятность того, что ей не будет присвоено значение. Очевидное
решение — переместить присваивание внутрь блока try:
try {
foo = getResults();
int bar = foo; // Нормально, потому что эта точка
// будет достигнута только при успешном выполнении
// предыдущего присваивания
}
catch ( Exception e ) {
...
}

220  Глава 6. Обработка ошибок и запись в журнал
Иногда это решение работает хорошо. Тем не менее при попытке дальнейшего
использования переменной bar в методе myMethod() мы получим ту же проблему. Если не соблюдать осторожность, то в итоге нам придется «затолкать»
все команды в блок try. Чтобы исправить ситуацию, можно добавить в секцию
catch возврат из метода:
try {
foo = getResults();
}
catch ( Exception e ) {
...
return;
}
int bar = foo; // Нормально, потому что эта точка
// будет достигнута только при успешном выполнении
// предыдущего присваивания

Компилятор достаточно умен, чтобы понять: если в блоке try произойдет ошибка, то выполнение не дойдет до точки присваивания bar. Поэтому компилятор
позволяет обратиться к foo. В разных случаях ваш код будет требовать разных
решений, и вы должны знать возможные варианты.

Секция finally
А если перед возвратом из метода через одну из секций catch надо сделать что-то
важное? Чтобы избежать дублирования кода в каждой ветви catch и выполнить
завершающие действия более явно, можно воспользоваться секцией finally.
Секция finally добавляется после try и всех последующих секций catch. Все
команды в секции finally гарантированно будут выполнены независимо от того,
как управление вернется из try (с выдачей исключения или без него):
try {
// Здесь что-то происходит
}
catch ( FileNotFoundException e ) {
...
}
catch ( IOException e ) {
...
}
catch ( Exception e ) {
...
}
finally {
// Завершающие действия, которые выполняются всегда
}

Исключения  221

В этом примере все команды в точке «Завершающие действия…» будут выполнены независимо от того, по какой причине управление передается из try.
Если управление передается в одну из секций catch, то команды finally будут
выполнены после завершения catch. Если ни одна из секций catch не обработает исключение, то команды finally будут выполнены до того, как исключение
всплывет на следующий уровень.
Если команды в try будут выполнены без ошибок, а также если будет выполнена команда return, break или continue, то команды в секции finally все равно
будут выполнены. Чтобы гарантировать выполнение определенных действий,
можно даже использовать try без секций catch:
try {
// Здесь что-то происходит
return;
}
finally {
System.out.println("Whoo-hoo!");
}

Исключение, возникшее в секции catch или finally, обрабатывается обычным
образом; поиск замыкающей конструкции try / catch начинается за пределами
проблемной команды try, после выполнения finally.

try с ресурсами
Секцию finally часто применяют для того, чтобы ресурсы, используемые
в секции try, были гарантированно освобождены, независимо от того, по какой
причине происходит выход из блока:
try {
// Socket sock = new Socket(...);
// Операции с sock
} catch( IOException e ) {
...
}
finally {
if ( sock != null ) { sock.close(); }
}

В таких случаях бывает нужно освободить самые ценные ресурсы или закрыть файлы, сокеты, подключения к базам данных. Иногда ресурсы могут
освобождаться автоматически в процессе уборки мусора, но это произойдет
в лучшем случае в неизвестный момент будущего, а в худшем — никогда не
произойдет (или не произойдет до того, как возникнет нехватка ресурсов).
От таких ситуаций всегда надо защищаться. Показанный подход в целом не-

222  Глава 6. Обработка ошибок и запись в журнал
плох, но у него есть два недостатка. Во-первых, реализация этого паттерна во
всем коде потребует дополнительной работы (проверки на null, как в нашем
примере, и т. д.). Во-вторых, если вы «жонглируете» несколькими ресурсами
в одном блоке finally, возникает риск того, что ваш код освобождения ресурсов сам выдаст исключение (например, при выполнении close()), а работа
останется незавершенной.
В Java 7 проблема значительно упростилась благодаря новой форме секции try,
которая называется «try с ресурсами». Вы можете заключить одну или несколько
команд инициализации ресурсов в круглые скобки после ключевого слова try,
и эти ресурсы будут автоматически закрыты (освобождены) после передачи
управления из блока try:
try (
Socket sock = new Socket("128.252.120.1", 80);
FileWriter file = new FileWriter("foo");
)
{
// Операции с sock и file
} catch ( IOException e ) {
...
}

В этом примере мы инициализируем объекты Socket и FileWriter в секции «try
с ресурсами» и используем их в теле команды try. При выходе управления из
команды try — либо после успешного завершения, либо из-за исключения —
оба ресурса будут автоматически закрыты вызовом их метода close(). Ресурсы
закрываются в порядке, обратном порядку их конструирования, что учитывает
возможные зависимости между ними. Такое поведение поддерживается любым
классом, реализующим интерфейс AutoCloseable (который в настоящий момент
поддерживается более чем ста разными встроенными классами). Метод close()
этого интерфейса должен освобождать все ресурсы, связанные с объектом, и вы
можете легко реализовать его в своих собственных классах. При использовании
«try с ресурсами» вам не придется добавлять специальный код для закрытия
файла или сокета; это делается за вас автоматически.
Другая проблема, которую решает «try с ресурсами», — это уже упомянутая
неприятная ситуация с выдачей исключения во время операции закрытия.
Если в предыдущем примере, где для освобождения ресурсов использовалась
секция finally, возникнет исключение в методе close(), то оно будет выдано
из этой точки, а исходное исключение из тела try окажется полностью потерянным. Но при использовании «try с ресурсами» исходное исключение
сохранится. Если вслед за исключением в теле try, во время последующих
операций автоматического закрытия, возникнет одно или несколько исключений, то именно исходное исключение всплывет из тела try к вызывающей
стороне. Рассмотрим пример:

Проверочные утверждения  223
try (
Socket sock = new Socket("128.252.120.1", 80); // Возможное исключение 3
FileWriter file = new FileWriter("foo"); // Возможное исключение 2
)
{
// Операции с sock и file // Возможное исключение 1
}

После начала try, если в точке 1 происходит исключение, Java пытается закрыть
оба ресурса в обратном порядке, что приводит к возможным исключениям
в точках 2 и 3. В этом случае вызывающий код все равно получит исключение 1.
Впрочем, исключения 2 и 3 не пропадают; они просто «подавляются» и могут
быть найдены методом getSuppressed() класса Throwable при обработке исключения, выданного вызывающей стороне. Этот метод возвращает массив всех
подавленных исключений.

Обработка исключений и быстродействие
Виртуальная машина Java устроена таким образом, что защита от выданного
исключения (с помощью try) всегда «бесплатна». Она не создает никакой дополнительной нагрузки, влияющей на выполнение вашего кода. Тем не менее
само по себе исключение не является «бесплатным». Когда в выполняемом коде
возникает исключение, Java ищет подходящий блок try / catch и занимается
другими длительными операциями.
Это означает, что исключения надо выдавать только в действительно исключительных ситуациях. Старайтесь не использовать исключения в нормальных
ситуациях, особенно если для вас важно быстродействие. Например, при проектировании цикла лучшим решением может быть небольшая проверка в каждой итерации, позволяющая избежать многократно возникающих исключений.
Но если исключение выдается только один раз из миллиарда, то вы, вероятно,
предпочтете отказаться от проверочного кода и не беспокоиться о затратах на
выдачу исключения. Помните, что исключения предназначены для ненормальных ситуаций, а не для обычных и ожидаемых условий (таких, как конец файла).

Проверочные утверждения
Проверочное утверждение (assertion) — это простая проверка некоторого условия
во время выполнения вашего приложения. Проверочные утверждения нужны
для контрольных проверок вашего кода в тех местах, где вы уверены, что при правильном поведении программы гарантируется соблюдение некоторых условий.
Проверочные утверждения отличаются от других проверок, потому что они проверяют условия, которые никогда не должны нарушаться на логическом уровне:

224  Глава 6. Обработка ошибок и запись в журнал
если проверка завершается неудачей, значит, приложение работает некорректно;
обычно в таких случаях оно аварийно завершается с выдачей подходящего сообщения об ошибке. Проверочные утверждения напрямую поддерживаются
языком Java, и их можно отключать во время выполнения приложения, чтобы
избавиться от вызванных ими потерь быстродействия.
Применение проверочных утверждений для проверки правильности поведения
приложения — это простое, но мощное средство контроля качества кода. Проверочные утверждения заполняют пробел между теми аспектами программного
продукта, которые могут автоматически проверяться компилятором, и теми,
которые чаще контролируются «модульными тестами» и тестированием с участием человека. Проверочные утверждения (если они включены) проверяют
предположения относительно поведения программы и превращают их в гарантии. Если у вас уже есть опыт программирования, возможно, вы встречали
конструкции следующего вида1:
if ( !condition )
throw new AssertionError("fatal error: 42");

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

false;
( array.length > min );
a > 0 : a // Выводит значение a
foo != null : "foo is null!" // Выводит сообщение "foo is null"

При неудаче первые два проверочных утверждения выводят только общее
сообщение, тогда как третье выводит значение переменной, а последнее — сообщение "foo is null".
Еще раз: важное преимущество проверочных утверждений заключается не в том,
что они компактнее эквивалентных условий if, а в том, что их можно включать
1

Если у вас уже имеется опыт программирования, надеемся, вы не используете такие
невразумительные сообщения об ошибках! Чем понятнее и содержательнее ваши сообщения, тем лучше.

Проверочные утверждения  225

или отключать при запуске приложения. Отключение проверочных утверждений
означает, что не только не вычисляются проверяемые ими условия, но и само
присутствие этих строк в вашем коде не приводит к снижению быстродействия
(если не считать памяти файлов классов при их загрузке).

Включение и отключение проверочных утверждений
Проверочные утверждения включаются и выключаются перед запуском приложения. Отключенные проверочные утверждения остаются в файлах классов,
но не выполняются и не отнимают времени. Проверочные утверждения можно
включать и отключать для всего приложения, или на уровне отдельных пакетов, или даже на уровне классов. По умолчанию все проверочные утверждения
отключены. Чтобы включить их для всего приложения, используйте команду
java с флагом -ea (или -enableassertions):
$ java -ea MyApplication

Чтобы включить проверочные утверждения для конкретного класса, укажите
после флага имя класса:
$ java -ea:com.oreilly.examples.Myclass MyApplication

Чтобы включить проверочные утверждения только для некоторых пакетов,
укажите имя пакета с завершающим многоточием (...):
$ java -ea:com.oreilly.examples... MyApplication

Когда вы включаете проверочные утверждения для пакета, Java также включает их для всех подчиненных имен пакетов (например, com.oreilly.examples.
text). Но вы можете действовать более избирательно, используя флаг -da (или
-disableassertions), отключающий проверочные утверждения для отдельных
пакетов или классов. Все это можно объединять в произвольные группы, например, так:
$ java -ea:com.oreilly.examples...
\
-da:com.oreilly.examples.text
\
-ea:com.oreilly.examples.text.MonkeyTypewriters MyApplication

В этом примере проверочные утверждения включаются для всего пакета
com.oreilly.examples, кроме пакета com.oreilly.examples.text, а также только
для одного класса MonkeyTypewriters из этого пакета.

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

226  Глава 6. Обработка ошибок и запись в журнал
ным. Проверочное утверждение может использоваться для обеспечения безопасности в любых местах, где вы хотите проверить свои предположения относительно
некоторого поведения программы, которое не может проверяться компилятором.
Типичная ситуация, в которой уместно применение проверочного утверждения, — проверка нескольких условий или значений, одно из которых обязательно
должно присутствовать. В таком случае невыполнение проверочного утверждения как поведения по умолчанию указывает на то, что где-то в коде есть ошибки.
Допустим, у вас имеется значение direction, которое всегда должно содержать
константу LEFT или RIGHT:
if ( direction == LEFT )
doLeft();
else if ( direction == RIGHT )
doRight()
else
assert false : "bad direction";

То же относится к секции default команды switch:
switch ( direction ) {
case LEFT:
doLeft();
break;
case RIGHT:
doRight();
break;
default:
assert false;
}

В общем случае проверочные утверждения не следует использовать для проверки
действительности аргументов методов, потому что это поведение должно быть
частью вашего приложения, а не обычной проверкой качества, которую можно
отключить. Действительность входных данных метода называется его предусловием, и при его нарушении обычно должно выдаваться исключение; таким образом
предусловия повышаются до составляющей «контракта» метода с пользователем.
С другой стороны, проверка правильности результатов методов перед их возвращением — хорошая мысль; такие проверки называются постусловиями.
Иногда определение того, что является или не является предусловием, зависит
от точки зрения. Например, когда метод используется во внутренней реализации
класса, предусловия могут уже гарантироваться вызывающими его методами.
Вероятно, открытые методы класса должны выдавать исключения при нарушении их предусловий, но скрытые (приватные) методы могут использовать
проверочные утверждения, потому что их вызывающие методы всегда тесно
связаны с ними, а их код должен соблюдать правильное поведение.

Журнальный API  227

Журнальный API
Пакет java.util.logging предоставляет чрезвычайно гибкую и простую в использовании основу для вывода системной информации, сообщений об ошибках
и детализированной трассировки (отладочных данных). С пакетом logging вы
можете устанавливать фильтры для выбора записываемых в журнал сообщений, направлять их вывод в один или несколько приемников (включая файлы
и сетевые службы) и форматировать сообщения для потребителей.
Но что самое важное, большая часть базовой конфигурации logging может
настраиваться извне во время выполнения приложения — с помощью файла
настроек или внешней программы. Например, задавая нужные настройки во
время выполнения приложения, можно указать, что сообщения в журнале должны сохраняться в указанном файле в формате XML, а также направляться на
системную консоль в удобочитаемом виде. Кроме того, для каждого приемника
можно задать уровень (или приоритет) выводимых сообщений; сообщения, не
достигающие некоторого порога важности, будут теряться. Соблюдая необходимые соглашения в вашем коде, можно даже настраивать уровни ведения журнала
для конкретных частей вашего приложения; это позволяет включать подробный
вывод для отдельных пакетов и классов без перегрузки избыточным выводом.
Журнальным API Java даже можно управлять в удаленном режиме через API
Java Management Extensions MBean.

Общие сведения
Любой хороший журнальный API должен руководствоваться по крайней мере
двумя основными принципами. Во-первых, он не должен существенно замедлять работу приложений, то есть препятствовать свободному использованию
сообщений разработчиком. Как и в случае с проверочными утверждениями Java,
отключенные журнальные сообщения не должны потреблять сколько-нибудь
значительного времени. Это означает, что наличие команд вывода в журнал не
должно приводить к снижению производительности, если эти команды отключены. Во-вторых, хотя некоторым пользователям может потребоваться расширенная функциональность и возможность детальной настройки, журнальный
API должен иметь простой режим использования. Он должен быть достаточно
удобен, чтобы разработчики, которым вечно не хватает времени, могли пользоваться им вместо старого метода System.out.println(). Журнальный API
в языке Java предоставляет и упрощенную модель, и множество дополнительных,
чрезвычайно удобных методов1.
1

Тем, кому возможностей журнального API окажется недостаточно, рекомендуем
Apache Log4j 2 (https://logging.apache.org/log4j/2.x) и SLF4J (Simple Logging Facade for

228  Глава 6. Обработка ошибок и запись в журнал
Протоколировщики
В системе журнального вывода центральное место занимает протоколировщик —
экземпляр класса java.util.logging.Logger. Как правило, это единственный
класс, с которым вам надо иметь дело в вашем коде. Протоколировщик создается
статическим методом Logger.getLogger(), которому в аргументе передается
имя протоколировщика. Имена формируют иерархию, на вершине которой
находится глобальный корневой протоколировщик, а ниже находится дерево
его потомков. Эта иерархия позволяет наследовать конфигурацию в отдельных
частях дерева, чтобы журнальный вывод мог автоматически настраиваться для
разных частей вашего приложения. По действующим соглашениям в каждом
крупном классе или пакете используется отдельный экземпляр протоколировщика, а в качестве его имени используется имя пакета и (или) имя класса
с точками в качестве разделителей. Пример:
package com.oreilly.learnjava;
public class Book {
static Logger log = Logger.getLogger("com.oreilly.learnjava.Book");

Протоколировщик предоставляет широкий диапазон методов для сохранения
сообщений в журнале; одни получают очень подробную информацию, другие —
только одну строку для простоты использования. Пример:
log.warning("Disk 90% full.");
log.info("New user joined chat room.");

Методы класса протоколировщика будут подробно рассмотрены ниже. Имена
warning и info — примеры уровней вывода; всего определено семь уровней, от
SEVERE (верхний) до FINEST (нижний). Возможность различать журнальные сообщения позволяет выбрать уровень информации, которую вы хотите видеть
во время выполнения. Вместо того чтобы сохранять все подряд и сортировать
позднее (это плохо влияет на скорость работы), вы можете указать, какие
именно сообщения должны генерироваться. Уровни вывода рассматриваются
в следующем разделе.
Также стоит упомянуть, что для очень простых приложений или экспериментов
протоколировщик для имени global содержится в статическом поле Logger.
global. Вы можете использовать его как альтернативу классическому выводу
System.out.println():
Logger.global.info("Doing foo...")

Java) (http://www.slf4j.org), позволяющие еще точнее настраивать уровни журнального
вывода.

Журнальный API  229

Обработчики
Протоколировщики представляют клиентский интерфейс к системе журнального вывода, но непосредственная работа по выводу сообщений в приемники
(например, в файлы или на консоль) выполняется объектами-обработчиками.
С каждым протоколировщиком может быть связан один или несколько таких объектов (субклассов класса Handler), включая обработчики, заранее определенные
в журнальном API: ConsoleHandler, FileHandler, StreamHandler и SocketHandler.
Каждый обработчик знает, как доставлять сообщения своему приемнику.
ConsoleHandler используется конфигурацией по умолчанию для вывода сообщений в командной строке или в системной консоли. FileHandler может направлять

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

Фильтры
Прежде чем передавать сообщение своим или родительским обработчикам, протоколировщик сначала проверяет, достаточен ли уровень вывода для продолжения. Если сообщение не соответствует необходимому уровню, оно отбрасывается в источнике. Помимо проверки уровня, можно реализовать произвольную
фильтрацию сообщений; для этого следует создать классы Filter для проверки
сообщения перед обработкой. Класс Filter может быть применен к протоколировщику извне (так же, как и уровень вывода, обработчики и форматировщики,
о которых будет рассказано в следующем разделе). Класс Filter также можно
связать с конкретным экземпляром Handler для фильтрации записей на стадии
вывода, в отличие от фильтрации в источнике.

Форматировщики
Во внутреннем представлении сообщения передаются в нейтральном формате,
с включением всей предоставленной информации об источнике. Только после

230  Глава 6. Обработка ошибок и запись в журнал
применения обработчика сообщения форматируются для вывода экземпляром объекта Formatter. В журнальном API есть два базовых форматировщика:
SimpleFormatter и XMLFormatter. «Простой» SimpleFormatter используется по
умолчанию для консольного вывода. Он производит короткие, понятные для человека сводки журнальных сообщений. XMLFormatter кодирует все подробности
сообщения в формате записи XML. Схемы DTD для этого формата доступны
на сайте Oracle: http://www.oracle.com/webfolder/technetwork/jsc/dtd/index.html.

Уровни вывода
В табл. 6.1 перечислены уровни вывода в порядке убывания значимости.

Таблица 6.1. Уровни вывода в журнальном API
Уровень

Смысл

SEVERE

Фатальный сбой приложения

WARNING

Уведомление о потенциальной проблеме

INFO

Сообщения, представляющие интерес для конечного пользователя

CONFIG

Подробная информация о конфигурации системы для администратора

FINE, FINER, FINEST

Трассировочные данные для разработчика (с последовательным увеличением уровня детализации)

Все уровни делятся на три категории: для конечного пользователя, для администратора и для разработчика. По умолчанию приложения часто ограничиваются
сообщениями уровня INFO и выше (INFO, WARNING и SEVERE). Эти уровни обычно
видны конечным пользователям, а сообщения, выводимые на этих уровнях,
должны быть пригодными для общего применения. Иначе говоря, они должны
быть сформулированы так, чтобы быть понятными (или хотя бы осмысленными)
для рядовых пользователей приложения. Часто такие сообщения выводятся на
системную консоль или во временном диалоговом окне.
Уровень CONFIG должен использоваться для относительно статической, но
подробной системной информации, которая должна быть рассчитана на администратора или специалиста, устанавливающего программу. Такие сообщения
могут включать информацию об установленных программных модулях, характеристиках системы и параметрах конфигурации. Эти подробности важны, но
для конечных пользователей они, скорее всего, не представляют интереса.
Уровни FINE, FINER и FINEST предназначены для разработчиков и других специалистов, разбирающихся во внутренней архитектуре приложения. Эти уровни

Журнальный API  231

должны использоваться для трассировки приложения на последовательно возрастающих уровнях детализации. Вы можете определить для них собственную
интерпретацию. В следующем разделе будет представлена одна из возможных
схем.

Простой пример
В следующем (откровенно говоря, искусственном) примере мы используем все
уровни вывода для экспериментов с конфигурацией журнального вывода. Хотя
последовательность сообщений выглядит бессмысленно, эти тексты типичны
для сообщений такого типа.
import java.util.logging.*;
public class LogTest {
public static void main(String argv[])
{
Logger logger = Logger.getLogger("com.oreilly.LogTest");

}

}

logger.severe("Power lost - running on backup!");
logger.warning("Database connection lost, retrying...");
logger.info("Startup complete.");
logger.config("Server configuration: standalone, JVM version 1.5");
logger.fine("Loading graphing package.");
logger.finer("Doing pie chart");
logger.finest("Starting bubble sort: value ="+42);

Этот пример не особенно содержателен. Сначала мы запрашиваем экземпляр
logger для своего класса статическим методом Logger.getLogger(), передавая
ему имя класса. По действующим соглашениям должно использоваться полное имя класса, поэтому мы будем считать, что наш класс принадлежит пакету
com.oreilly.
Теперь запустите программу LogTest. На системной консоли должен появиться
вывод, который выглядит примерно так:
Jan 6, 2019 3:24:36 PM LogTest main
SEVERE: Power lost - running on backup!
Jan 6, 2019 3:24:37 PM LogTest main
WARNING: Database connection lost, retrying...
Jan 6, 2019 3:24:37 PM LogTest main
INFO: Startup complete.

Мы видим сообщения INFO, WARNING и SEVERE с указанием даты, временной
метки и имени класса и метода (LogTest, main), из которого они поступили.

232  Глава 6. Обработка ошибок и запись в журнал
Обратите внимание: сообщений более низкого уровня нет. Дело в том, что по
умолчанию обычно выбирается уровень вывода INFO; это означает, что выводятся только сообщения с уровнем INFO и выше. Также обратите внимание,
что вывод направляется на системную консоль, а не в журнальный файл;
этот приемник также используется по умолчанию. А теперь мы опишем, где
выбираются эти настройки по умолчанию и как переопределить их во время
выполнения.

Конфигурирование журнального API
Как было сказано во вводной части, очень важной особенностью журнального
API является возможность настройки детализации вывода на стадии выполнения с помощью внешнего файла или специального приложения. Конфигурация
вывода по умолчанию хранится в файле jre/lib/logging.properties, который
находится в рабочем каталоге Java. Это стандартный конфигурационный файл;
мы уже упоминали о таких файлах в этой главе.
У этого файла простой формат. Вы можете вносить в него изменения, но это
не обязательно. Вместо этого вы можете задать собственный файл для настройки журнального вывода в каждом конкретном случае, например, таким
образом:
$ java -Djava.util.logging.config.file=myfile.properties

В этой командной строке myfile — ваш конфигурационный файл с директивой,
которая будет описана ниже. Если вы хотите использовать этот файл на постоянной основе, укажите его имя в соответствующей записи с помощью Java
Preferences API. Можно даже пойти еще дальше и вместо файла с настройками
указать класс, ответственный за подготовку всей конфигурации журнального
вывода, но мы эту возможность рассматривать не будем.
Очень простой конфигурационный файл может выглядеть так:
# Set the default logging level
.level = FINEST
# Direct output to the console
handlers = java.util.logging.ConsoleHandler

Здесь уровень вывода по умолчанию назначается для всего приложения при
помощи параметра с именем .level (обратите внимание на точку). Также
параметр handlers указывает, что при выводе должен использоваться экземпляр ConsoleHandler (как в конфигурации по умолчанию), чтобы сообщения выводились на консоль. Если вы снова запустите приложение, указав
этот файл для определения конфигурации журнала, вы снова увидите все
сообщения.

Журнальный API  233

Впрочем, это только начало. Перейдем к более сложной конфигурации:
# Set the default logging level
.level = INFO
# Ouput to file and console
handlers = java.util.logging.FileHandler, java.util.logging.ConsoleHandler
# Configure the file output
java.util.logging.FileHandler.level = FINEST
java.util.logging.FileHandler.pattern = %h/Test.log
java.util.logging.FileHandler.limit = 25000
java.util.logging.FileHandler.count = 4
java.util.logging.FileHandler.formatter = java.util.logging.XMLFormatter
# Configure the console output
java.util.logging.ConsoleHandler.level = WARNING
# Levels for specific classes
com.oreilly.LogTest.level = FINEST

В этом примере настраиваются два обработчика: экземпляр ConsoleHandler
с уровнем вывода WARNING и экземпляр FileHandler , направляющий вывод
в XML-файл. Файловый обработчик настроен для регистрации сообщений на
уровне FINEST (все сообщения) и для ротации файлов через каждые 25 000 строк,
с хранением максимум 4 файлов.
Имя файла определяется параметром pattern . Символы / в имени файла
при необходимости автоматически локализуются в \. Специальная последовательность %h обозначает домашний каталог пользователя. Для ссылок на
системный временный каталог может использоваться последовательность
%t . При возникновении конфликтов между именами файлов после точки
автоматически присоединяется число (начиная с нуля). Также можно воспользоваться последовательностью %u, чтобы обозначить позицию вставки
уникального числа в имя. Аналогичным образом при ротации файлов после
точки в конце присоединяется номер. Идентификатор %g управляет позицией
вставки номера ротации.
В нашем примере используется класс XMLFormatter. Также можно было воспользоваться классом SimpleFormatter для отправки того же простого вывода
на консоль. ConsoleHandler также позволяет задать любой форматировщик на
ваш выбор при помощи параметра formatter.
Наконец, ранее мы упоминали о том, что можно управлять уровнями вывода
для отдельных частей приложения. Для этого надо задать параметры протоколировщиков приложения с указанием их иерархических имен:
# Levels for specific logger (class) names
com.oreilly.LogTest.level = FINEST

234  Глава 6. Обработка ошибок и запись в журнал
Здесь уровень вывода назначается только для нашего тестового протоколировщика. Параметры соответствуют иерархии, поэтому уровень вывода для всех
классов пакета oreilly можно задать следующей командой:
com.oreilly.level = FINEST

Уровни вывода задаются в порядке их чтения из конфигурационного файла,
поэтому самые общие параметры должны быть указаны в начале. Уровни, заданные для обработчиков, позволяют файловому обработчику фильтровать только
сообщения, поставляемые протоколировщиками. Следовательно, назначение
файловому обработчику уровня FINEST не «оживит» сообщения, подавленные
протоколировщиком, которому назначен уровень SEVERE (от этого протоколировщика до обработчика доберутся только сообщения SEVERE).

Протоколировщик
В нашем примере использовались семь вспомогательных методов, имена которых соответствовали разным уровням вывода. Также есть три группы общих
методов, которые могут использоваться для передачи более подробной информации. Наиболее важны следующие:
log(Level
log(Level
log(Level
log(Level

level,
level,
level,
level,

String
String
String
String

msg)
msg, Object param1)
msg, Object params[])
msg, Throwable thrown)

В первом аргументе этих методов передается статический идентификатор
уровня вывода из класса Level, за которым следует параметр, массив или тип
исключения. Идентификатор уровня имеет вид Level.SEVERE, Level.WARNING,
Level.INFO и т. д.
Кроме этих четырех методов, также существуют вспомогательные методы
с именами entering(), exiting() и throwing(), которые могут использоваться
разработчиками для вывода подробных трассировочных данных.

Быстродействие
Во вводной части мы упоминали, что быстродействие является приоритетной целью журнального API. В частности, мы упоминали, что журнальные сообщения
фильтруются в источнике, при этом уровни вывода используются для раннего
отсечения сообщений. При этом экономится значительная часть затрат на их
обработку. Однако фильтрация не может предотвратить некоторые виды подготовительных операций, которые могут выполняться перед вызовом. А именно, поскольку мы передаем данные журнальным методом, на практике часто

Исключения в реальном мире  235

конструируются подробные сообщения или объекты преобразуются в строки,
передаваемые в аргументах. Такие операции часто сопряжены со значительными
ресурсами. Чтобы избежать конструирования лишних строк, затратные операции надо «обернуть» в проверку условия методом isLoggable() класса Logger
чтобы уточнить, должны ли эти операции выполняться, например:
if ( log.isLoggable( Level.CONFIG ) ) {
log.config("Configuration: "+ loadExpensiveConfigInfo() );
}

Исключения в реальном мире
Применение исключений для обработки ошибок в Java значительно упрощает
написание кода, защищенного от возможных ошибок. Компилятор заставляет вас
заранее продумывать проверяемые исключения. Безусловно, время от времени
будут возникать непроверяемые исключения, но проверочные утверждения
помогут вам заранее предусмотреть эти проблемы, а если повезет, то и предотвратить ошибки.
Синтаксис «try с ресурсами», появившийся в Java 7, помогает разработчикам
поддерживать чистоту кода и «поступать правильно» при взаимодействии
с ограниченными системными ресурсами, такими как файлы и сетевые подключения. Как упоминалось в начале главы, в других языках тоже предусмот­
рены различные средства для решения подобных проблем. Язык Java старается помочь вам тщательно продумать проблемы, которые могут возникнуть
в вашем коде. И чем больше вы трудитесь над разрешением этих проблем, тем
стабильнее работает ваше приложение и тем лучшие впечатления остаются
у пользователей.
Для выявления самых коварных, «незаметных» ошибок, не приводящих к сбою
вашего приложения, Java предоставляет пакет журналирования java.util.
logging, позволяющий отслеживать причины таких ошибок. Вы всегда сможете
регулировать объем выводимых в журнал данных, сохраняя разумную скорость
работы приложения.
До сих пор многие из наших примеров были простыми и не требовали хитроумного контроля ошибок. Не сомневайтесь: мы еще займемся более интересным
кодом и многими ситуациями, в которых необходима обработка исключений.
В следующих главах будут рассмотрены такие темы, как многопоточное программирование и работа в сети. В этих областях бывает множество ситуаций,
которые могут создать проблемы при выполнении кода, — например, выход изпод контроля сложных вычислений или потеря подключения Wi-Fi. Скоро вы
сможете попробовать (с помощью команды try) все эти новые приемы работы
с ошибками и исключениями.

ГЛАВА 7

Коллекции и обобщения

Когда вы начнете применять свои растущие знания об объектах для решения
все более интересных задач, у вас будет возникать один и тот же вопрос. Как
хранить данные, которыми вы оперируете при решении этих задач? Конечно,
для этого есть переменные всех типов, но в дополнение к ним вам понадобятся
более сложные и вместительные хранилища. Массивы, которые мы рассматривали в одноименном разделе, с. 144, могут стать отправной точкой, но у них есть
некоторые ограничения. В этой главе мы объясним, как осуществляется более
эффективный и гибкий доступ к большим объемам данных. В этом вам поможет
API коллекций Java. Вы также узнаете, как сохранять в коллекциях (то есть
в больших хранилищах) данные разных типов и работать с ними почти так же,
как с отдельными значениями в переменных. Для этого в Java есть обобщения
(generics, дженерики), которые рассматриваются в разделе «Ограничения типов», с. 242.

Коллекции
Коллекции — это структуры данных, лежащие в основе всех видов программирования. Любую обозначенную вами группу объектов вы можете считать
своего рода коллекцией. Например, на базовом уровне Java поддерживает коллекции в виде массивов. Но массивы статичны. Из-за фиксированной длины
они неудобны для хранения объектов в таких группах, которые увеличиваются
и уменьшаются во время работы приложения. Кроме того, в массивах трудно
отражать абстрактные взаимосвязи между объектами. В ранних версиях Java
было только два простейших класса для частичного решения этих проблем:
класс java.util.Vector , представляющий динамический список объектов,
и класс java.util.Hashtable, представляющий карту (ассоциативный массив)
из пар «ключ — значение». Но теперь в Java реализован полноценный подход
к коллекциям в виде фреймворка коллекций (Collections Framework). Оба старых класса все еще поддерживаются, но они были модифицированы для этого
фреймворка (немного странным образом) и, как правило, уже не используются.

Коллекции  237

Коллекции очень просты по сути, но они относятся к самым мощным средствам
во всех языках программирования. Они помогают программистам создавать
такие структуры данных, которые критически важны для решения сложных проблем. В компьютерных науках уделяют много внимания тому, как эффективно
выполнять некоторые типы алгоритмов с помощью коллекций. Если у вас есть
эти инструменты и вы умеете работать с ними, то ваш код становится заметно
компактнее и быстрее. И вам не приходится заново «изобретать велосипед».
Первая версия фреймворка коллекций вышла с двумя большими недостатками.
Во-первых, коллекции были нетипизованными, то есть работали только с недифференцированными объектами Object вместо конкретных типов (таких, как Date
или String). Поэтому при каждом извлечении объекта из коллекции приходилось
выполнять преобразование типов, что явно противоречило ключевой идее Java —
безопасности типов на стадии компиляции. На практике это решение не вызывало
больших проблем, но оказалось громоздким и неудобным. Во-вторых, коллекции
могли состоять только из объектов, а примитивные типы не поддерживались.
Поэтому каждый раз, когда требовалось внести в коллекцию число или другой
примитивный тип, его приходилось предварительно сохранять в классе-обертке,
а после извлечения — распаковывать обратно. Из-за этих недостатков код для
работы с коллекциями получался трудным для понимания и ненадежным.
Все стало гораздо лучше, когда во фреймворке коллекций появились обобщенные типы и механизм автоматического преобразования примитивных типов.
Обобщенные типы (подробнее см. «Ограничения типов», с. 242) дают программисту уверенность в совершенно безопасной типизации при работе с коллекциями. А благодаря автоматической упаковке и распаковке примитивных типов
их можно использовать в коллекциях точно так же, как объекты. Эти новые
инструменты делают код намного компактнее и безопаснее, и теперь они есть
во всех классах коллекций Java, как вы вскоре увидите.
Фреймворк коллекций основан на наборе интерфейсов из пакета java.util. Все
они делятся на две ветви с иерархической структурой. Одна из них унаследована
от интерфейса Collection, который (как и его потомки) определяет контейнер,
способный хранить другие объекты. Вторая, отдельная ветвь основана на интерфейсе Map, определяющем группу пар «ключ — значение»; ключи в таких парах
нужны, чтобы легко находить нужные значения.

Интерфейс Collection
Предком всех коллекций первой ветви является интерфейс с подходящим
именем Collection. Он служит контейнером, элементами которого являются
объекты. Контейнер не содержит информации о том, как следует хранить эти
объекты; например, он «не знает» о том, разрешены ли дубликаты и упорядочены
ли объекты каким-то образом. Все подобные детали определяются дочерними

238  Глава 7. Коллекции и обобщения
интерфейсами. Тем не менее интерфейс Collection определяет ряд базовых
методов, общих для всех этих коллекций:
public boolean add( элемент )

Метод добавляет заданный элемент в коллекцию. Если эта операция завершится успешно, метод возвращает true. Если объект уже есть в коллекции,
а дубликаты в ней не разрешены, метод возвращает false. Некоторые коллекции доступны только для чтения; при вызове этого метода они выдают
исключение UnsupportedOperationException.
public boolean remove( элемент )

Метод удаляет заданный элемент из коллекции. Как и метод add(), он возвращает true при удалении объекта из коллекции. Если этого объекта нет в коллекции, он возвращает false. Коллекции, доступные только для чтения, при
вызове этого метода тоже выдают исключение UnsupportedOperationException.
public boolean contains( элемент )

Метод возвращает true, если в коллекции есть заданный элемент.
public int size()

Метод возвращает количество элементов в коллекции.
public boolean isEmpty()

Метод возвращает true, если коллекция не содержит элементов.
public Iterator iterator()

Метод проверяет все элементы в коллекции. Он возвращает итератор
(Iterator) — объект, который можно использовать для перебора всех элементов коллекции. Итераторы подробно рассматриваются в следующем разделе.
Кроме перечисленных, есть методы addAll(), removeAll() и containsAll(),
которые принимают в качестве аргумента другой (сравниваемый) объект
Collection и, соответственно, добавляют, удаляют и проверяют элементы заданной коллекции.

Разновидности коллекций
Интерфейс Collection имеет три дочерних интерфейса. Set («множество»)
определяет коллекцию, в которой дубликаты недопустимы. В коллекции List
(«список») элементы следуют в определенном порядке. Интерфейс Queue («очередь») определяет буфер для объектов, в котором находящийся в самом начале,
то есть головной элемент должен быть обработан в первую очередь.

Коллекции  239

Интерфейс Set
Интерфейс Set не содержит других методов, кроме унаследованных от Collection.
Он просто выполняет правило запрета дубликатов. Если вы попытаетесь добавить элемент, уже существующий в коллекции Set, тометод add() вернет false.
Субинтерфейс SortedSet позволяет хранить элементы в определенном порядке:
как отсортированный список без дубликатов. Для извлечения подмножеств (тоже
отсортированных) есть методы subSet(), headSet() и tailSet(). Они принимают
в качестве аргументов один или два элемента, которые являются границами подмножества. Для доступа к первому и последнему элементу есть методы first()
и last(), а метод comparator() предоставляет доступ к объекту, который используется для сравнения элементов (см. также раздел «Метод sort()», с. 260).
В Java 7 появился интерфейс NavigableSet, который расширяет SortedSet и добавляет методы для поиска ближайшего (по порядку сортировки) большего или
меньшего значения по отношению к заданному. Этот интерфейс может быть
эффективно реализован при использовании таких алгоритмов, как списки с пропусками (skip lists), ускоряющие поиск упорядоченных элементов.

Интерфейс List
Следующий дочерний интерфейс Collection — List. Он представляет упорядоченную коллекцию, сходную с массивом, но содержащую методы для изменения
позиций элементов в списке:
public boolean add( E элемент )

Метод добавляет заданный элемент в конец списка.
public void add( int индекс , E элемент )

Метод вставляет заданный объект в заданную позицию списка. Если позиция меньше нуля или больше длины списка, то выдается исключение
IndexOutOfBoundsException. Элемент, который ранее находился в заданной
позиции, и все последующие элементы сдвигаются на одну позицию вверх.
public void remove( int индекс )

Метод удаляет элемент в заданной позиции. Все последующие элементы
сдвигаются на одну позицию вниз.
public E get( int индекс )

Метод возвращает элемент в заданной позиции.
public Object set( int индекс , E элемент )

Метод заменяет элемент в заданной позиции заданным элементом. В этой
позиции уже должен присутствовать элемент, в противном случае выдается
исключение IndexOutOfBoundsException.

240  Глава 7. Коллекции и обобщения
Под типом E в этих методах понимается параметризованный тип элемента
класса List.

Интерфейс Queue
Интерфейс Queue представляет коллекцию, которая служит очередью (буфером)
для элементов. Она сохраняет порядок вставленных в нее элементов, и в ней
предусмотрено понятие «начального» («головного») элемента. Очереди могут
быть двух видов: «первым пришел, первым вышел» (FIFO) или «последним
пришел, первым вышел» (LIFO) в зависимости от реализации:
public boolean offer( E элемент ), public boolean add( E элемент )

Метод offer() пытается поместить элемент в очередь, возвращая true в случае успеха. Разные типы Queue могут устанавливать разные ограничения для
типов элементов (а также для длины очереди). В отличие от метода add(),
унаследованного от Collection, этот метод вместо исключения возвращает
логическое значение, чтобы сообщить, что элемент не может быть принят.
public E poll(), public E remove()

Метод poll() удаляет элемент в начале очереди и возвращает его. В отличие
от метода remove(), унаследованного от Collection, в случае пустой очереди
он возвращает null вместо исключения.
public E peek()

Метод возвращает начальный (головной) элемент, не удаляя его из очереди.
Если очередь пуста, он возвращает null.

Интерфейс Map
В составе фреймворка коллекций также есть java.util.Map — карта (map),
то есть коллекция пар «ключ — значение» (также используются термины «словарь» и «ассоциативный массив»). В карте элементы сохраняются и читаются
по значениям ключей; карты хорошо подходят для реализации таких структур,
как кэши и простейшие базы данных. При сохранении значения в карте с ним
связывается объект ключа. Когда вам требуется прочитать значение, карта использует для этого ключ.
Обобщенный тип Map параметризуется двумя типами: типом ключа и типом
значения. В следующем фрагменте используется коллекция HashMap — эффективная, но неупорядоченная реализация карты, которая будет рассмотрена далее:
Map dateMap = new HashMap();
dateMap.put( "today", new Date() );
Date today = dateMap.get( "today" );

Коллекции  241

В старом коде карты были несовершенными: они просто ассоциировали объекты
типа Object с объектами типа Object, а чтение значений требовало соответствующих преобразований типов.
Базовые операции Map достаточно просты. В следующих методах тип K обозначает тип параметра-ключа, а тип V — тип параметра-значения:
public V put( K ключ , V значение )

Метод добавляет в карту заданную пару «ключ — значение». Если карта
уже содержит значение с заданным ключом, то старое значение заменяется
и возвращается как результат операции.
public V get( K ключ )

Метод читает из карты значение, соответствующее ключу.
public V remove( K ключ )

Метод удаляет из карты значение, соответствующее ключу. Удаленное значение возвращается как результат операции.
public int size()

Метод возвращает количество пар «ключ — значение» в карте.
Для получения всех ключей или всех значений в карте есть следующие методы:
public Set keySet()

Метод возвращает объект Set, содержащий все ключи из карты.
public Collection values()

Метод используется для получения всех значений из карты. Полученный
объект Collection может содержать дубликаты.
public Set entrySet()

Метод возвращает объект Set со всеми парами «ключ — значение» (в виде
объектов Map.Entry) из этой карты.
Map имеет один субинтерфейс SortedMap. Он позволяет хранить пары «ключ —

значение», отсортированные в конкретном порядке, в соответствии со значениями ключей. SortedMap предоставляет методы subMap(), headMap() и tailMap() для
выборки подмножеств отсортированных карт. Как и SortedSet, он предоставляет
метод comparator(), который возвращает объект, определяющий порядок сор­
тировки ключей в карте. В Java 7 появился интерфейс NavigableMap, по своей
функциональности сходный с NavigableSet: он добавляет методы для поиска
ближайшего (по порядку сортировки) большего или меньшего значения по отношению к заданому.

242  Глава 7. Коллекции и обобщения
Несмотря на очевидную взаимосвязь, Map формально не является типом Collection
(Map не расширяет интерфейс Collection). Почему? Все методы интерфейса
Collection вроде бы имеют смысл для Map, кроме iterator(). Однако Map содержит
два набора объектов: ключи и значения, и для каждого требуется отдельный итератор. Вот почему Map не реализует Collection. Если вам нужно Collection-подобное
представление Map с ключами и значениями, используйте метод entrySet().
И еще одно замечание по поводу карт: некоторые реализации (включая стандартную коллекцию Java HashMap) позволяют использовать null в качестве ключей
или значений, но другие такой возможности не поддерживают.

Ограничения типов
Суть обобщений — абстрагирование. Обобщения позволяют создавать классы и методы, работающие одинаково с объектами разных типов. Сам термин
«обобщение» происходит от идеи написания обобщенных алгоритмов, которые
можно было бы повторно использовать для многих типов объектов, вместо того
чтобы адаптировать код к каждой конкретной ситуации. Эта концепция не нова;
собственно, она лежит в основе всего объектно-ориентированного программирования. Обобщения Java не столько дополняют язык новыми возможностями,
сколько упрощают написание и чтение повторно используемого кода.
Обобщения поднимают повторное использование на новый уровень: тип объектов, с которыми вы хотите работать, становится явным параметром обобщенного кода. По этой причине обобщения также называют параметризованными
типами. Для обобщенного класса разработчик указывает тип как параметр (аргумент) при каждом использовании обобщенного типа. Класс параметризуется
переданным типом, к которому адаптируется код.
В других языках обобщения иногда называются шаблонами (templates), но там
речь скорее идет о реализации. Шаблоны похожи на промежуточные классы,
которые ожидают получения параметров-типов, чтобы они могли использоваться в программе. В Java был выбран другой путь, у которого есть как свои
достоинства, так и недостатки; они будут подробно рассмотрены в этой главе.
Об обобщениях Java можно рассказывать очень долго. Некоторые нюансы на
первый взгляд могут показаться невразумительными, но не отчаивайтесь. Почти
все, что вы будете делать с обобщениями, например использовать существующие
классы вроде List и Set, — это очень просто и интуитивно понятно. Но для того,
чтобы проектировать и писать ваши собственные обобщения, придется значительно лучше изучить тему и терпеливо экспериментировать.
Мы начнем рассматривать обобщения Java с самой очевидной сферы их применения — в классах контейнеров и в коллекциях, которые мы только что рассмотрели.

Ограничения типов  243

Затем мы сделаем шаг назад и рассмотрим сильные и слабые стороны обобщений.
В завершение мы рассмотрим пару реальных обобщенных классов из Java API.

Контейнеры
В таких объектно-ориентированных языках, как Java, под полиморфизмом понимается то, что объекты всегда в какой-то степени заменяемы. Любой объектпотомок может использоваться вместо его родителя, поэтому каждый объект
является потомком java.lang.Object — своего рода объектно-ориентированной
«Евы». А значит, будет естественно, если самые универсальные типы контейнеров в Java будут работать с типом Object, чтобы в них можно было хранить
практически любые объекты. Под контейнерами мы понимаем классы, способные хранить экземпляры других классов. Лучшим примером контейнеров
служит API коллекций Java из предыдущего раздела. Напомним, что списки
List содержат упорядоченные коллекции элементов типа Object. Множества Map
содержат пары «ключ — значение», при этом ключи и значения относятся к самому общему типу Object. С небольшой поддержкой от оберток для примитивных
типов такая схема работала достаточно хорошо. Тем не менее в каком-то смысле
«коллекция объектов любых типов» также становится «коллекцией без типа»,
а работа с Object возлагает слишком большую ответственность на программиста.
Происходящее напоминает маскарад, на котором все объекты носят одинаковые маски и растворяются в толпе элементов коллекции. Когда объект «переодевается» в тип Object, компилятор перестает видеть его реальный тип. Впоследствии программист может нарушить анонимность объектов при помощи
преобразования типов. И вам лучше проследить за тем, чтобы преобразование
было правильным, иначе вы рискуете нарваться на неприятный сюрприз (как
при попытке сдернуть накладную бороду с участника маскарада):
Date date = new Date();
List list = new ArrayList();
list.add( date );
...
Date firstElement = (Date)list.get(0); // Это правильное преобразование?
// Может быть...

Интерфейс List также содержит метод add(), который получает любую разновидность Object. Здесь мы указали экземпляр ArrayList, который является
реализацией интерфейса List, и добавили объект Date. Будет ли преобразование
правильным в данном случае? Все зависит от того, что происходит в период
времени, обозначенный «…». Конечно, компилятор Java знает, что подобные
операции сопряжены с повышенным риском, и в настоящее время выдает преду­
преждения при добавлении элементов в простой массив ArrayList. В этом нетрудно убедиться при помощи jshell. После импортирования из пакетов java.

244  Глава 7. Коллекции и обобщения
util и javax.swing попробуйте создать объект ArrayList и добавить несколько

разнородных элементов:

jshell> import java.util.ArrayList;
jshell> import javax.swing.JLabel;
jshell> ArrayList things = new ArrayList();
things ==> []
jshell> things.add("Hi there");
| Warning:
| unchecked call to add(E) as a member of the raw type java.util.ArrayList
| things.add("Hi there");
| ^--------------------^
$3 ==> true
jshell> things.add(new JLabel("Hi there"));
| Warning:
| unchecked call to add(E) as a member of the raw type java.util.ArrayList
| things.add(new JLabel("Hi there"));
| ^--------------------------------^
$5 ==> true
jshell> things
things ==> [Hi there, javax.swing.JLabel[...,text=Hi there,...]]

Предупреждения выдаются независимо от типа, указанного при вызове add().
На последнем шаге, при выводе содержимого things, в списке соседствуют
объект String и объект JLabel. Компилятор не беспокоится об использовании
разнородных типов; он просто любезно предупреждает вас, что ему неизвестно, будут ли преобразования типов вроде приведенного выше преобразования
(Date) правильно работать на стадии выполнения.

Можно ли улучшить контейнеры?
Естественно спросить: нельзя ли как-то улучшить ситуацию? А если вам заранее известно, что в список должны включаться только объекты Date? Нельзя
ли сделать собственный список, который принимает только объекты Date, избавиться от преобразования и снова пользоваться помощью компилятора? Как
ни странно, нельзя (по крайней мере сколько-нибудь простым способом).
Первое естественное желание — попробовать «переопределить» методы
ArrayList в субклассе. Но конечно, замена метода add() в субклассе ничего не
переопределяет; просто в классе появляется новый перегруженный метод:
public void add( Object o ) { ... } // Все еще здесь
public void add( Date d ) { ... } // Перегруженный метод

Знакомство с обобщениями  245

Полученный объект будет принимать объекты любого типа — просто при этом
будут вызываться разные методы.
Придется поискать другое решение. Например, можно написать собственный
класс DateList, который не расширяет ArrayList, а делегирует реализацию своих
методов реализации ArrayList. Проделав немалый объем однообразной работы, мы получим объект, который делает все то же, что делает List, но работает
с объектами Date так, что компилятор и исполнительная среда знают их тип и не
позволяют его нарушать. Но в результате этой работы мы окажем себе медвежью
услугу, потому что наш контейнер перестанет быть реализацией List и мы не
сможем использовать его со всеми средствами работы с коллекциями (такими,
как Collections.sort()) или добавить его в другую коллекцию методом addAll()
класса Collection.
Подведем итог: проблема заключается в том, что на самом деле мы хотим не
уточнить поведение наших объектов, а изменить их «контракт с пользователем».
Мы хотим адаптировать их API к более конкретному типу, а полиморфизм не
позволяет это сделать. Похоже, обойтись без Object при работе с коллекциями
не удастся? Но на помощь приходят обобщения.

Знакомство с обобщениями
Упомянутые в предыдущем разделе обобщения представляют собой расширение
синтаксиса, позволяющее специализировать классы и методы для работы с конкретными типами. Обобщенный класс «настраивает себя» с помощью одного или
нескольких параметров-типов, которые получает при каждом обращении к нему.
Например, заглянув в исходный код или в Javadoc-документацию класса List,
вы найдете определение следующего вида:
public class List< E > {
...
public void add( E элемент ) { ... }
public E get( int i ) { ... }
}

Идентификатор E между угловыми скобками (< >) — это параметр-тип (type
parameter)1. Вся эта конструкция в угловых скобках показывает, что класс List
является обобщенным и требует завершения (уточнения) путем передачи ему
в аргументе какого-либо конкретного типа Java. Имя E выбрано произвольно,
1

Другой вариант названия — переменная-тип (type variable). Мы предпочитаем термин
«type parameter», который чаще используется в спецификации языка Java, но на практике вам могут встретиться оба варианта.

246  Глава 7. Коллекции и обобщения
хотя есть соглашения, о которых мы скажем далее. В первой строке параметртип E обозначает тип элементов, которые должны храниться в списке. Далее
класс List в своем теле и в своих методах обращается к E точно так же, как
если бы вместо E был указан реальный тип (который будет подставлен на эти
места позднее). Параметры-типы можно использовать в объявлениях переменных экземпляров, аргументов методов и возвращаемых типов методов.
В данном случае E обозначает тип элементов, которые будут добавляться
методом add(), а также возвращаемый тип метода get(). Давайте разберемся,
как все это работает.
Чтобы использовать класс List, мы указываем нужный нам тип в таких же
угловых скобках:
List listOfStrings;

Здесь мы объявили переменную с именем listOfStrings, взяв за основу обобщенный тип List с параметром-типом String. Мы можем создать и другие специализированные версии List — с любыми другими типами классов Java. Примеры:
List dates;
List decimals;
List foos;

Когда мы конкретизируем обобщенный тип таким образом, это называется воплощением типа (instantiating the type). Другие варианты названия — инстанцирование типа, реализация типа, вызов типа. Сравните: при работе с обычным
типом Java мы просто обращаемся к нему по имени, а обобщенный тип надо
воплощать при каждом использовании1. Точнее говоря, тип должен быть воплощен повсюду, где он может появиться как объявляемый тип переменной (это
показано в нашем примере), как тип аргумента метода, как возвращаемый тип
метода, а также в командах создания объектов с ключевым словом new.
Вернемся к списку listOfStrings. Мы получили такой List, в котором тип
String был подставлен компилятором вместо параметра-типа E в теле класса:
public class List< String > {
...
public void add( String element ) { ... }
public String get( int i ) { ... }
}

1

Но кроме тех случаев, когда программист хочет использовать обобщенный тип обычным, «необобщенным» образом. Эти случаи мы скоро рассмотрим в разделе «Необработанные типы».

Знакомство с обобщениями  247

Мы специализировали класс List для работы только с элементами типа String —
и ни с какими другими! Данная сигнатура методов уже не позволяет им принимать общий тип Object.
List — всего лишь интерфейс. Теперь, чтобы использовать полученную переменную, надо создать экземпляр некоторой реализации List. Для этого нам
снова пригодится ArrayList как в рассмотренных ранее примерах. Как и прежде,
ArrayList — это класс, реализующий интерфейс List, но в данном случае List
и ArrayList являются обобщенными классами. Поэтому их надо инстанцировать

при каждом использовании, указывая нужный параметр-тип. Мы, конечно, создадим такой ArrayList, который предназначен только для хранения элементов
String, то есть соответствующий нашей переменной listOfStrings:
List listOfStrings = new ArrayList();
// Или сокращенная запись для Java 7.0 и выше:
List listOfStrings = new ArrayList();

Как обычно, за ключевым словом new следует тип Java и круглые скобки с возможными аргументами для конструктора класса. В данном случае типом является ArrayList — обобщенный тип ArrayList, воплощенный с типом
String.
Объявлять переменные так, как показано в первой строке предыдущего примера,
не совсем удобно, потому что параметр-тип приходится вводить дважды: в левой
части и в инициализирующем выражении. В сложных случаях запись получается очень длинной: со многими параметрами-типами и даже со вложенными
конструкциями. Начиная с Java 7, компилятору хватает сообразительности,
чтобы определить тип инициализирующего выражения по типу той переменной, которой присваивается значение. Это называется выведением типа, или
автоматическим определением обобщенного типа (generic type inference). Как
показано в нашем примере, при сокращенной записи в правой части объявления
переменной достаточно пустых угловых скобок: < >.
Теперь мы можем использовать свою специализированную версию List со
строками. Компилятор пресекает любые попытки поместить в список что-то
иное, кроме объектов типа String (или подтипа String, если бы он существовал),
и дает возможность читать их методом get() без каких-либо преобразований:
jshell> ArrayList listOfStrings = new ArrayList();
listOfStrings ==> []
jshell> listOfStrings.add("Hey!");
$8 ==> true
jshell> listOfStrings.add(new JLabel("Hey there"));
| Error:
| incompatible types: javax.swing.JLabel cannot be converted to java.lang.String

248  Глава 7. Коллекции и обобщения
| listOfStrings.add(new JLabel("Hey there"));
|
^---------------------^
jshell> String s = strings.get(0);
s ==> "Hey!"

Возьмем другой пример из API коллекций. Интерфейс Map реализует соответствия в стиле словарей: объекты-ключи ассоциируются с объектами-значениями.
Ключи и значения не обязаны относиться к одному типу. Обобщенный интерфейс Map требует двух параметров-типов: для типа ключа и для типа значения.
Javadoc-документация выглядит так:
public class Map< K, V > {
...
public V put( K key, V value ) { ... } // Возвращает старое значение
public V get( K key ) { ... }
}

Мы можем, например, создать Map, чтобы хранить объекты Employee, содержащие сведения о работниках предприятия, идентифицируемых целочисленными
табельными номерами:
Map< Integer, Employee > employees = new HashMap< Integer, Employee >();
Integer bobsId = 314; // Спасибо автоупаковке!
Employee bob = new Employee( "Bob", ... );
employees.put( bobsId, bob );
Employee employee = employees.get( bobsId );

Здесь мы использовали HashMap — обобщенный класс, реализующий интерфейс
Map, — и инстанцировали оба параметра-типа: Integer и Employee. Теперь Map
работает только с ключами типа Integer и хранит значения типа Employee.
Причина, по которой для хранения числа использовалась обертка Integer, заключается в том, что параметры-типы обобщенных классов должны быть типами
классов. Обобщенный класс нельзя параметризовать примитивным типом, например int или boolean. К счастью, благодаря автоупаковке примитивов в Java
(см. «Обертки для примитивных типов», с. 174) мы можем использовать примитивные типы так же, как если бы они были типами-обертками.
Помимо API коллекций, десятки других API тоже используют обобщения, чтобы вы могли адаптировать их к конкретным типам. Мы поговорим о них в тех
случаях, когда они будут встречаться в книге.

Несколько слов о типах
Прежде чем переходить к более важным вещам, надо сказать несколько слов
о том, каким образом мы описываем конкретную параметризацию обобщенного

«Ложки не существует»  249

класса. Самый частый и очевидный способ применения обобщений — для работы
с контейнерами, поэтому обобщенные типы часто воспринимаются как «средства для хранения» объектов, заданных параметрами-типами. В нашем примере
мы назвали List «списком строк», потому что по сути он именно этим
и является. Аналогичным образом можно назвать нашу карту с информацией
о работниках «картой, связывающей идентификаторы работников с объектами
Employee». Тем не менее эти определения в большей степени сосредоточены на
том, что делают классы, чем на самих типах. Но можно взглянуть на обобщенные типы иначе: для примера возьмем контейнер с единственным объектом,
имеющий имя Trap< E >, и создадим его экземпляры для объектов типа Mouse
и типа Bear, то есть Trap или Trap. Будет вполне естественно назвать эти новые типы «мышиная ловушка» и «медвежья ловушка». Точно так же
можно воспринимать List как новый тип «строковый список». А нашу
карту сотрудников можно воспринимать как новый тип «целочисленная карта
объектов сотрудников». Выбирайте тот вариант формулировки, который больше
нравится, но обратите внимание, что во втором варианте обобщение выглядит
как тип. Это может помочь вам сохранить ясность терминов, когда мы будем
обсуждать, как обобщенные типы связаны в системе типов. В таком контексте
«контейнерное» восприятие окажется не совсем логичным.
В следующем разделе мы продолжим обсуждение обобщенных типов Java с другой точки зрения. Вы уже видели, что они могут сделать; пора разобраться, как
они это делают.

«Ложки не существует»
В фильме «Матрица»1 Нео оказывается перед выбором. Взять синюю таблетку
и остаться в вымышленном мире? Или взять красную таблетку и увидеть все
в истинном свете? Работая с обобщениями в Java, мы сталкиваемся с похожей
онтологической дилеммой. Рано или поздно при рассмотрении обобщений нам
придется столкнуться с вопросом об их практической реализации в компьютере.
Наш вымышленный мир создается компилятором, чтобы нам было проще писать
код. Реальность (пусть и не мрачная антиутопия, показанная в фильме) — это
1

Для тех, кому непонятно название раздела, приведем цитату:
«М а л ь ч и к. Не пытайся согнуть ложку. Это невозможно. Сначала надо понять главное.
Н е о. Что главное?
М а л ь ч и к. Ложки не существует.
Н е о. Не существует?
М а л ь ч и к. Знаешь, это не ложка гнется. Все — обман. Дело в тебе».
(Братья Вачовски, «Матрица». 136 минут. Warner Brothers, 1999.)

250  Глава 7. Коллекции и обобщения
более суровое место, полное скрытых опасностей и неудобных вопросов. Почему
преобразования типов не могут правильно работать с обобщениями? Почему я не
могу реализовать что-то вроде двух разных обобщенных интерфейсов в одном
классе? Почему я могу объявить массив обобщенных типов, хотя в Java нельзя
создавать такие массивы? В этой главе мы ответим на эти и другие вопросы,
и вам даже не придется ждать продолжения фильма. Скоро вы научитесь «сгибать ложки» (вернее, типы).
Проектируя обобщения Java, разработчики решали сложнейшую задачу: добавить в популярный язык совершенно новый синтаксис, который безопасно
введет параметризованные типы без ущерба для быстродействия… а заодно сохранить обратную совместимость со всем существующим кодом Java и избежать
значительных изменений в уже скомпилированных классах. Удивительно, что
разработчики вообще справились с этой задачей. Разумеется, им понадобилось
немало времени. И при этом, как часто бывает, пришлось идти на компромиссы,
что породило ряд проблем.

Стирание типов
Для решения этой задачи в Java используется прием, называемый стиранием
типов (erasure). Практически все, что мы делаем с обобщениями, происходит
статически на стадии компиляции. Поэтому информацию об обобщениях можно не включать в скомпилированные классы! Обобщенные свойства классов,
предоставленные компилятором, стираются (удаляются) в скомпилированных классах — так достигается совместимость с обычным, «необобщенным»
кодом. Хотя Java сохраняет информацию об обобщенных свойствах классов
в скомпилированной форме, эта информация используется главным образом
компилятором. А исполнительная среда Java ничего не знает об обобщениях.
Рассмотрим скомпилированный обобщенный класс: уже знакомый нам список
List. Для этого можно воспользоваться командой javap:
$ javap java.util.List
public interface java.util.List extends java.util.Collection{
...
public abstract boolean add(java.lang.Object);
public abstract java.lang.Object get(int);

Результат выглядит точно так же, как он выглядел до внедрения в язык обобщений (в этом можно убедиться при помощи любой старой версии JDK). Обратите внимание, что методы add() и get() используют тип элементов Object.
Вы думаете, что это какая-то хитрость и при воплощении типа где-то в глубине
Java появится новая версия класса? Нет, это не так. Существует только один

«Ложки не существует»  251

класс List, и именно он является реальным типом для всех параметризаций
List, например для List и List, в чем нетрудно убедиться:
List dateList = new ArrayList();
System.out.println( dateList instanceof List ); // true!

Но наш обобщенный dateList не реализует только что упомянутые методы
класса List:
dateList.add( new Object() ); // Ошибка компиляции

В этом проявляется слегка шизофреническая природа обобщений Java. Компилятор в них верит, но исполнительная среда знает, что они — всего лишь иллюзия. Попробуем трезво взглянуть на эту ситуацию и проверить, что dateList
относится к типу List:
System.out.println( dateList instanceof List ); // Ошибка компиляции!
// Недопустимо: обобщенный тип для instanceof

На этот раз компилятор просто опускает шлагбаум и говорит: «Нет». Обобщенный тип нельзя проверить операцией instanceof. Во время выполнения
не существует фактически отличающихся классов для разных параметризаций
List, поэтому оператор instanceof не может отличить один List от другого. Вся
проверка безопасности обобщений осталась на стадии компиляции, и теперь мы
просто работаем с одним реальным типом List.
В действительности произошло следующее: компилятор стер все синтаксические
элементы с угловыми скобками и заменил параметры-типы в нашем классе List
таким типом, который может работать во время выполнения с любым допустимым типом, в данном случае это Object. Похоже, мы вернулись к исходной точке,
не считая того, что у компилятора теперь есть информация, позволяющая ему
проверять все наши действия с обобщениями на стадии компиляции. Следовательно, он может выполнять преобразования типов вместо нас. Попробуйте
декомпилировать класс List (команда javap с ключом -c выводит байткод, если вы не из пугливых), и вы увидите, что в скомпилированном коде есть
преобразование в Date, хотя в исходном коде его не было.
Теперь можно ответить на один из вопросов, поставленных в начале раздела:
«Почему я не могу реализовать что-то вроде двух разных обобщенных интерфейсов в одном классе?» Класс не может реализовать два разных воплощения
обобщенного типа List, потому что во время выполнения они становятся одним
типом и различить их невозможно:
public abstract class DualList implements List, List { }
// Ошибка: java.util.List не может наследоваться с разными аргументами:
// и

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

Необработанные типы
Хотя во время компиляции разные параметризации обобщенного типа рассматриваются компилятором как разные типы (с разными API), вы уже знаете, что
во время выполнения существует только один реальный тип. Например, классы
List и List совместно используют старый класс Java List. Его
называют необработанным типом (raw type), или сырым типом, обобщенного
класса. У каждого обобщенного типа есть свой необработанный тип. Это вырожденная, «тривиальная» для Java форма, из которой удалена вся информация об
обобщенных типах, то есть вместо параметров-типов используется общий тип
Java (такой, как Object)1.
Вы можете работать с необработанными типами точно так же, как это было до
включения обобщений в язык. Единственное отличие заключается в том, что
компилятор выдает предупреждение при их «небезопасном» использовании.
За пределами jshell компилятор умеет распознавать такие проблемы:
// Необобщенный код Java, использующий необработанный тип
List list = new ArrayList(); // Присваивание работает
list.add("foo"); // Предупреждение компилятора об использовании
// необработанного типа

В этом фрагменте необработанный тип List используется «в старом стиле» (как
было принято до выхода Java 5). Поэтому при добавлении объекта в список
компилятор предупреждает, что эта операция непроверяемая или небезопасная:
$ javac MyClass.java
Note: MyClass.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.

Компилятор предлагает использовать ключ -Xlint:unchecked для получения
более конкретной информации о местонахождении небезопасных операций:

1

Когда в Java 5.0 появились обобщения, разработчики языка тщательно позаботились
о том, чтобы необработанный тип каждого обобщенного класса ничем не отличался от
более раннего, необобщенного типа. Таким образом, необработанный тип List в Java 5.0
не отличается от старого, необобщенного типа List, который появился еще в JDK 1.2.
Поскольку практически весь код Java, существовавший на тот момент, не содержал
обобщений, эквивалентность и совместимость типов была очень важна.

Отношения между параметризованными типами  253
$ javac -Xlint:unchecked MyClass.java
warning: [unchecked] unchecked call to add(E) as a member of the raw type
java.util.List: list.add("foo");

Учтите, что при создании и присваивании необработанного типа ArrayList
предупреждение не выдается. Оно будет получено только при вызове «небезо­
пасного» метода (содержащего обращение к параметру-типу). Это означает, что
вы можете использовать старые, «необобщенные» Java API, работающие с необработанными типами. Предупреждения будут выдаваться только при попытке
выполнения небезопасных операций в вашем коде.
И еще несколько слов о стирании типов, прежде чем двигаться дальше. В предыдущих примерах параметры-типы заменялись типом Object, который может
представлять любой тип, применимый к параметру-типу E. Сейчас вы увидите,
что это не всегда так. Для параметров-типов можно установить ограничения,
и при этом компилятор отнесется к стиранию типов более избирательно:
class Bounded< E extends Date > {
public void addElement( E element ) { ... }
}

Такое объявление параметра-типа означает, что тип элемента E обязательно должен быть подтипом типа Date. Поэтому при стирании типа в методе addElement()
компилятор не дойдет до уровня Object, а остановится на Date:
public void addElement( Date element ) { ... }

Здесь Date является верхней границей (upper bound) указанного типа, то есть
Date в данном случае находится на вершине иерархии объектов. Значит, воплощение типа возможно только с Date или «нижестоящими» (производными
от него) типами.
Итак, теперь вы примерно представляете, как работают обобщенные типы, и мы
можем рассмотреть их поведение чуть подробнее.

Отношения между
параметризованными типами
Мы знаем, что параметризованные типы совместно используют единый необработанный тип. Именно из-за этого наш параметризованный List во время
выполнения представляет собой обычный List. Таким образом, переменной
необработанного типа можно присвоить любое воплощение List:
List list = new ArrayList();

254  Глава 7. Коллекции и обобщения
А можно даже пойти в другом направлении и присвоить необработанный тип
конкретному воплощению обобщенного типа:
List dates = new ArrayList(); // Предупреждение

Эта команда генерирует предупреждение о непроверяемой операции при попытке присваивания, но в дальнейшем компилятор полагает, что до присваивания
список содержал только объекты Date. Также допустимо, хотя и бессмысленно,
выполнить в этой команде преобразование типа. О преобразовании к обобщенным типам мы вкратце расскажем в разделе «Преобразования типов», с. 256.
Какими бы ни были типы на стадии выполнения, компилятор «командует парадом» и не позволяет присваивать явно несовместимые объекты:
List dates = new ArrayList(); // Ошибка компиляции!

Конечно, ArrayList не реализует методы типа List, сформированного компилятором, поэтому эти типы несовместимы.
Но как насчет более интересных отношений между типами? Например, интерфейс List является подтипом более общего интерфейса Collection. Будет
ли конкретное воплощение обобщения List совместимо по присваиванию
с каким-либо воплощением обобщенного типа Collection? Зависит ли это от
параметров-типов и отношений между ними? Очевидно, List не является
типом Collection. Но является ли List типом Collection?
Может ли List быть типом Collection?
Сначала мы просто ответим, а потом займемся анализом и пояснениями. Правило гласит, что для простых видов воплощений обобщенных типов, которые
рассматривались до сих пор, наследование применяется только к «базовому»
обобщенному типу, но не к параметрам-типам. Более того, совместимость по
присваиванию применима только в том случае, когда два обобщенных типа
воплощаются одним и тем же параметром-типом. Иначе говоря, все еще существует одномерное наследование от базового типа обобщенного класса, но
с дополнительным ограничением, согласно которому параметры-типы должны
быть идентичны.
Например, так как List является типом Collection, мы можем присваивать воплощения List воплощениям Collection при точном совпадении параметра-типа:
Collection cd;
List ld = new ArrayList();
cd = ld; // OK!

Из этого фрагмента следует, что List является типом Collection —
вполне логично. Но попытка применить ту же логику при расхождении параметров-типов приводит к неудаче:

Отношения между параметризованными типами  255
List lo;
List ld = new ArrayList();
lo = ld; // Ошибка компиляции! Несовместимые типы

Хотя интуиция подсказывает, что объекты Date в List могли бы прекрасно существовать как объекты Object в List, попытка присваивания приводит к ошибке.
В следующем разделе мы подробно объясним, почему это происходит, а пока
заметим, что параметры-типы не совпадают и между параметрами-типами
в обобщениях не существует отношений наследования. В данном случае лучше
думать о воплощениях в контексте типов, а не в контексте того, что они делают.
Мы имеем дело не со «списком дат» и «списком объектов», а скорее с DateList
и ObjectList, отношения между которыми не столь очевидны.
Следующий пример поможет понять, что можно, а чего нельзя делать в таких
ситуациях:
Collection cn;
List li = new ArrayList();
cn = li; // Ошибка компиляции! Несовместимые типы

Воплощение List может быть воплощением Collection, но только в том случае,
если их параметры-типы в точности совпадают. Наследование не распространяется на параметры-типы, и в этом примере возникает ошибка.
Мы упоминали, что это правило применяется к уже рассмотренным простым
видам воплощений. Какие еще виды существуют? Те воплощения, которые
рассматривались до настоящего момента, с передачей реального типа Java в качестве параметра, называются конкретными воплощениями типа (concrete type
instantiations). Далее мы упомянем и об универсальных воплощениях (wildcard
instantiations), сходных с математическими операциями над множествами. Вы
увидите, что возможны и более экзотические виды обобщений с двумерными
отношениями между типами, с зависимостями как от базового типа, так и от
параметризации. Не беспокойтесь: они встречаются не очень часто и не так
страшны, как кажутся на первый взгляд.

Почему List не является List?
Вполне логичный вопрос. Даже если думать о DateList и ObjectList как о разных
типах, все равно можно спросить, не совместимы ли они по присваиванию. Почему нельзя иметь возможность присвоить List переменной List
и работать с элементами Date как с типами Object?
Дело в главной причине для применения обобщений, которая упоминалась во
вводном разделе: изменении API. В простейшем случае, если предположить, что
тип ObjectList расширяет тип DateList, последний будет содержать все мето-

256  Глава 7. Коллекции и обобщения
ды ObjectList и в него можно будет вставлять элементы Object. На это можно
возразить, что обобщения позволяют нам изменять API, так что этот довод не
действует. Это верно, но есть и более серьезная проблема. Если бы мы могли присвоить DateList переменной ObjectList, то мы могли бы использовать методы
Object для вставки в него элементов типов, отличных от Date. Тогда можно было
бы определить для DateList синоним (то есть предоставить альтернативный,
более широкий тип) в форме ObjectList и попытаться обмануть его, заставив
принять другой тип:
DateList dateList = new DateList();
ObjectList objectList = dateList; // Так нельзя
objectList.add( new Foo() ); // Должна произойти ошибка времени выполнения!

При попытке использования фактической реализации DateList с неправильным типом объекта должна происходить ошибка времени выполнения. И здесь
скрывается проблема: обобщения Java не имеют своего представления на стадии
выполнения. Даже если бы эта функциональность была полезной, в текущей
схеме реализации Java невозможно определить, что в связи с этим нужно делать
во время выполнения. На проблему можно взглянуть иначе: такая возможность
была бы попросту опасной, потому что она допускает ошибки времени выполнения, которые невозможно перехватить во время компиляции. Но мы, конечно,
хотим, чтобы ошибки типов всегда выявлялись во время компиляции.
Можно подумать, что запретом таких присваиваний Java гарантирует безопасность типов вашего кода, если он компилируется без предупреждений. К сожалению, таких гарантий нет, но проблема связана не с обобщениями, а с массивами. Если это замечание покажется вам знакомым, так это потому, что мы уже
упоминали об этом — применительно к массивам Java. Типы массивов обладают
отношением наследования, которое допускает такую синонимию:
Date [] dates = new Date[10];
Object [] objects = dates;
objects[0] = "not a date"; // Исключение ArrayStoreException!

Однако массивы на стадии выполнения представляются другими классами, и они
проверяют себя во время выполнения, выдавая исключение ArrayStoreException
в подобных ситуациях. Это значит, что при таком использовании массивов компилятор теоретически не может гарантировать безопасность типов в коде Java.

Преобразования типов
Мы обсудили отношения между обобщенными типами — и даже между обобщенными и необработанными типами. Однако мы еще не исследовали концепцию
преобразований типов в мире обобщений. При совмещении обобщений с необ-

Преобразования типов  257

работанными типами никакие преобразования не были нужны. Вместо этого
мы просто пересекали линию, на которой компилятор выдавал предупреждения
о непроверяемых операциях:
List list = new ArrayList();
List dl = list; // Предупреждение

В обычной ситуации преобразование типа в Java используется для работы
с двумя типами, которые могут быть совместимыми по присваиванию. Например, можно попытаться преобразовать Object в Date, потому что Object может
быть значением Date. Тогда операция преобразования выполняет проверку на
правильность во время выполнения. Преобразование между несвязанными
типами вызывает ошибку компиляции. Например, даже не стоит пытаться преобразовать Integer в String. Такие типы не связаны отношениями наследования.
А как насчет преобразований совместимых обобщенных типов?
Collection cd = new ArrayList();
List ld = (List)cd; // OK!

В этом фрагменте продемонстрировано допустимое преобразование более
общего типа Collection в List. Такое преобразование возможно, потому что тип Collection совместим по присваиванию и может
содержать List . Аналогичным образом следующее преобразование
перехватывает нашу ошибку, когда мы определяем для TreeSet синоним
Collection и пытаемся преобразовать его в List:
Collection cd = new TreeSet();
List ld = (List)cd; // Исключение ClassCastException!
ld.add( new Date() );

Тем не менее есть случай, когда преобразования типов неэффективны с обобщениями, а именно при попытке различить типы по их параметрам-типам:
Object o = new ArrayList();
List ld = (List)o; // Предупреждение о непроверяемой операции,
// она бесполезна
Date d = ld.get(0); // Небезопасно во время выполнения,
// неявное преобразование может завершиться неудачей

Здесь мы определяем для ArrayList синоним в виде простого типа
Object, который затем преобразуется в List. К сожалению, Java не может
отличить List от List во время выполнения, поэтому преобразование бесполезно. Компилятор сообщает об этом, выдавая предупреждение
о непроверяемой операции в точке преобразования. Вы должны это учитывать,
когда будете использовать преобразованный объект впоследствии — он может
оказаться неправильным. Преобразования с обобщенными типами неэффек-

258  Глава 7. Коллекции и обобщения
тивны во время выполнения из-за стирания типов, то есть из-за отсутствия
информации о типах.

Преобразования между коллекциями и массивами
Преобразования между коллекциями и массивами выполняются просто. Для
удобства элементы коллекции можно получить в виде массива при помощи
следующих методов:
public Object[] toArray()
public E[] toArray( E[] a )

Первый метод возвращает простой массив Object. Во второй форме можно действовать более конкретно и получить массив с элементами правильного типа.
Если предоставить массив достаточного размера, он будет заполнен значениями.
Но если массив окажется слишком коротким (например, нулевой длины), будет создан и возвращен новый массив того же типа, но с необходимой длиной.
Таким образом, вы можете просто передать пустой массив с правильным типом:
Collection myCollection = ...;
String [] myStrings = myCollection.toArray( new String[0] );

(Этот трюк выглядит немного неуклюже, и было бы лучше, если бы язык Java
позволял явно задать тип ссылкойClass, но по какой-то причине это невозможно.) Также можно пойти в обратную сторону и преобразовать массив объектов
в коллекцию List статическим методом asList() класса java.util.Arrays:
String [] myStrings = ...;
List list = Arrays.asList( myStrings );

Итератор
Итератор — это объект для перебора последовательности значений. Эта операция встречается так часто, что для нее был определен стандартный интерфейс:
java.util.Iterator. Интерфейс Iterator имеет всего два основных метода:
public E next()

Метод возвращает следующий элемент (элемент обобщенного типа E) перебираемой коллекции.
public boolean hasNext()

Метод возвращает true, если вы еще не перебрали все элементы Collection.
Другими словами, он возвращает true, если возможно вызвать next() для
получения следующего элемента.

Преобразования типов  259

Этот пример демонстрирует использование Iterator для вывода каждого элемента коллекции:
public void printElements(Collection c, PrintStream out) {
Iterator iterator = c.iterator();
while ( iterator.hasNext() ) {
out.println( iterator.next() );
}
}

Кроме методов перебора, Iterator предоставляет возможность удаления элементов из коллекции:
public void remove()

Метод удаляет последний объект, возвращенный вызовом next(), из перебираемой коллекции.
Не все итераторы реализуют remove(). Например, возможность удаления элемента из коллекции, доступной только для чтения, не имеет смысла. Если удаление
элемента запрещено, метод выдает исключение UnsupportedOperationException.
Если remove() вызывается до первого вызова next() или если remove() вызывается дважды подряд, выдается исключение IllegalStateException.

Цикл for с коллекциями
Разновидность цикла for, описанная в разделе «Цикл for», с. 134, может работать со всеми типами Iterable; это означает, что она может использоваться
для перебора любых объектов Collection, так как этот интерфейс расширяет
Iterable. Например, можно перебрать все элементы типизованной коллекции
объектов Date:
Collection col = ...
for( Date date : col )
System.out.println( date );

Эта разновидность встроенных циклов for в языке Java называется расширенным циклом for (в отличие от числовых циклов for, которые появились задолго
до обобщений). Расширенный цикл for применим только к коллекциям типа
Collection, но не к Map. Контейнер Map — совсем другое дело. Он содержит два
разных набора объектов (ключи и значения), поэтому неочевидно, что именно
должен делать цикл. Но поскольку идея перебора карты в цикле выглядит ра­
зумно, вы можете воспользоваться двумя методами Map: keySet() или values()
(или даже entrySet() , если вы действительно хотите, чтобы каждая пара
«ключ — значение» представлялась отдельной сущностью), чтобы получить из
вашей карты подходящую коллекцию, которая будет работать с расширенным
циклом for.

260  Глава 7. Коллекции и обобщения

Метод sort()
Изучая класс java.util.Collections, мы находим в нем разнообразные статические методы для работы с коллекциями. Среди них есть очень интересный
представитель — статический обобщенный метод sort():