Вы находитесь на странице: 1из 594

Java

Справочник разработчика
СЕДЬМОЕ ИЗДАНИЕ
Java
in а Nutshell
SEVENTH EDITION

Benjamin J Evans and David Flanagan

Beijing. Boston. Farnham. Sebastopol •Tokyo o·REILLY"


Java
Справочник разработчика

СЕДЬМОЕ ИЗДАНИЕ

Бенджамин Дж. Эванс и Дэвид Флэнаган

Москва · Санкт-Петербург
2019
ББК 32.973.26-018.2.75
314
УДК 681.3.07
ООО "Диалектика"

Зав. редакцией С.Н. Тригуб


1Iеревод с анг11ийско1·0 и редакция И.В. Берштейна

По общим вопросам обращайтесь в издатс11ьство "Диа11ектика" по адресу:


iпfo@dialcktika.com, http://www.dialektika.com

Эванс, Бенджамин Дж" Флэнаган, Дэвид.

Э14 Java. Справочник разработчика, 7-е изд. : Пер. с англ. - СПб. ООО
"Диалектика", 2019. - 592 с.: ил. - Парал. тит. англ.

ISBN 978-5-907144-61-3 (рус.)


ББК 32.973.26-018.2.75
Все названия программных продуктов являются зарегистрирова1111ыми тортвыми марками соответ­

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

()'Reilly Media, Inc.

At1thori1.ed Rшsian translatio11 of the F.nglish edition of fava in а Nutshell, 7th Ь'dition (ISBN 978-1-492-
03725-5) CcJ 2019 Benjamin ). Evans and l)avid Flaпagan. All rights reserved.
This translation is pнЬlishcd and sold Ьу permission of O'Reilly Media, lnc" which owns or controls all rights
to pнЬlish and sell the same.
All rights reserved. No part of this work may Ье rcprodнced or transmittcd in any form or Ьу any mcaпs,
clcctronic or mechanical, inclнding photocopying, recordiпg, or Ьу any information storage or retrieval system.
withottt the prior written permission of the copyright owпer and thc PнЬlishcr.

Научно-популярное издание
Бенджамин Дж. Эванс, Дэвид Флэнаган

Java. Справочник разработчика


7-е издание

!Iонписано в нечап. 31.07.2019. Формат 70х100/\6.


Гарнитура Тimes.
Усл. 11сч. л.47,73. Уч.-изд. л. 26.8.
Тираж 500 эк:~. Заказ № 6447.

Отпечатано в АО "Первая Обращовая типография"


Филиап "Чеховский Пе•1атный Двор"
142300, Московская об11ас1ъ, 1·. Чехов, ул. Ilопиrрафистов, д. 1
Сайт: www.chpd.п1, E-mail: sales@)chpd.rн, теп. 8 (499) 270-73-59

ООО "Диалектика': 195027, Санкт-Петербург, Ма1·нитщ·орская ул., д. 30, лит. А, пом. 848

ISBN 978-5-907144-61-3 (рус.) @ 2019 ООО "Диа11сктика"


ISBN 978-1-492-03725-5 (а111·л.) (с) 2019 Benjamin ). F.vans and [)avid l'lanagan
Оrnавnение

Введение 19

Часть 1. Введение в Java 29


Глава 1. Введение в среду Java 31
Глава 2. Основы синтаксиса Java 51
Глава 3. Объектно-ориентированное программирование на Java 165
Глава 4. Система типов Java 217
Глава S. Введение в объектно-ориентированное
проектирование на Java 281
Глава 6. Управление оперативной памятью и параллелизм в Java 313

Часть 11. Работа с платформой Java 351


Глава 7. Соглашения по программированию и документированию 353
Глава 8. Работа с коллекциями Java 375
Глава 9. Обработка данных в типичных форматах 415
Глава 10. Обработка файлов и ввод-вывод 443
Глава 11.Загрузка классов, рефлексия и дескрипторы методов 473
Глава 12. Модули на платформе Java 499
Глава 13. Инструментальные средства платформы Java 523
Приложение А. Дополнительные средства 547
Предметный указатель 573
Содержание

Содержание 6
Посвящение 17
Предисловие 18

Введение 19
Изменения в седьмом издании 19
Краткое содержание книги 20
Дополнительная литература 23
Условные обозначения, принятые в книге 23
Благодарности 25
Об авторах 26
Об изображении на обложке 27
От издательства 28

Часть 1. Введение в Java


Гnава 1. Введение в среду Java 31
Язык, виртуальная машина и экосистема Java 31
Что собой представляет язык Java 33
Что такое JVM 34
Что такое экосистема Java 36
Краткая история развития Java 38
Жизненный цикл программы на Java 41
Часто задаваемые вопросы 41
Безопасность Java 44
Сравнение Java с другими языками программирования 44
Сравнение Java с языком С 45
Сравнение Java с языком С++ 45
Сравнение Java с языком Python 45
Сравнение Java с языком JavaScript 46
Ответы на критику в адрес Java 46
Чрезмерная многословность 46
Медленность изменений 47
Проблемы производительности 48
Ненадежность 49
Чрезмерная корпоративность 49

Глава 2. Основы синтаксиса Java 51


Анализ программ на Java по нисходящей 52
Лексическая структура 53
Набор символов в Юникоде 53
Учет регистра букв и пробелы 54
Комментарии 54
Зарезервированные слова 56
Идентификаторы 56
Литералы 58
Знаки препинания 58
Примитивные типы данных 58
Тип boolean 59
Тип char 60
Целочисленные типы данных 63
Числовые типы с плавающей точкой 65
Взаимные преобразования примитивных типов данных 68
Выражения и операции 70
Краткая сводка операций 71
Арифметические операции 78
Операция сцепления строк 80
Операции инкремента и декремента 80
Операции сравнения 81
Логические операции 83
Поразрядные логические операции и операции сдвига 85
Операции присваивания 88
Условная операция 90
Операция instanceof 91
Специальные операции 92
Операторы 94

Содержание 7
Операторы-выражения 95
Составные операторы 96
Пустой оператор 96
Операторы с меткой 96
Операторы объявления локальных переменных 97
Условный оператор if/else 99
Оператор swi tch 102
Оператор while 104
Оператор do while 105
Оператор for 106
Цикл типа foreach 108
Оператор break 110
Оператор continue 111
Оператор return 112
Оператор synchronized 112
Оператор throw 114
Блок операторов try/catch/finally 115
Оператор try с ресурсами 119
Оператор assert 120
Методы 122
Определение методов 122
Модификаторы доступа к методам 126
Проверяемые и непроверяемые исключения 128
Списки аргументов переменной длины 130
Введение в классы и объекты 132
Определение класса 132
Создание объекта 133
Применение объектов 134
Объектные литералы 135
Лямбда-выражения 136
Массивы 137
Типы массивов 138
Создание и инициализация массивов 141
Применение массивов 142
Многомерные массивы 146
Ссылочные типы данных 148
Сравнение ссылочных типов с примитивными типами 148

8 Содержание
Манипулирование объектами и копиями ссылок 149
Сравнение объектов 152
Упаковочные и распаковочные преобразования 153
Пакеты и пространство имен в Java 154
Объявление пакета 155
Глобально уникальные имена пакетов 156
Импорт ссылочных типов 156
Импорт статических членов 159
Структура исходного файла Java 161
Определение и выполнение программ на Java 163
Резюме 164

Глава 3. Объектно-ориентированное программирование на Java 165


Краткий обзор классов 165
Основные определения ООП 166
Другие ссылочные типы 167
Синтаксис определения класса 168
Поля и методы 169
Синтаксис объявления полей 170
Поля класса 171
Методы класса 172
Поля экземпляра 174
Методы экземпляра 174
Принцип действия ссылки this 175
Создание и инициализация объектов 177
Определение конструктора 177
Определение нескольких конструкторов 178
Вызов одного конструктора из другого 179
Устанавливаемые по умолчанию значения и инициализаторы полей 180
Подклассы и наследование 182
Расширение класса 183
Суперклассы, класс Object и иерархия классов 185
Конструкторы подклассов 186
Вызов конструкторов по цепочке и конструктор по умолчанию 187
Сокрытие полей суперкласса 189
Переопределение методов из суперкласса 191
Сокрытие данных и инкапсуляция 198

Содержание 9
Управление доступом 199
Методы доступа к данным 205
Абстрактные классы и методы 208
Преобразования ссылочных типов 211
Краткие итоги по модификаторам доступа 214

Глава 4. Система типов Java 217


Интерфейсы 218
Определение интерфейса 219
Расширение интерфейсов 220
Реализация интерфейса 221
Методы с реализацией по умолчанию 224
Маркерные интерфейсы 229
Обобщения в Java 230
Введение в обобщения 230
Обобщенные типы и параметры типа 232
Ромбовидный синтаксис 233
Стирание типов 234
Ограничение параметров типа 235
Введение в ковариантность 237
Подстановки 239
Обобщенные методы 243
Статическая и динамическая типизация 244
Применение и разработка обобщенных типов 245
Перечисления и аннотации 245
Перечисления 246
Аннотации 248
Определение специальных аннотаций 249
Типовые аннотации 251
Лямбда- выражения 251
Преобразование лямбда-выражений 253
Ссылки на методы 255
Функциональное программирование 257
Лексическая область видимости и локальные переменные 259
Вложенные типы данных 262
Статические типы членов 264
Нестатические типы членов 267

1О Содержание
Локальные классы 270
Анонимные классы 274
Необозначаемые типы и выводимый тип var 276
Резюме 278

Глава 5. Введение в объектно-ориентированное


проектирование на Java 281
Значения в Java 281
Основные методы из класса j ava. lang. Obj ect 283
Метод toString () 286
Метод equals () 286
Метод hashCode () 286
Метод ComparaЫe: : compareTo () 288
Метод clone () 289
Особенности объектно-ориентированного проектирования 289
Константы 290
Интерфейсы в сравнении с абстрактными классами 290
Допустимо ли использовать методы с реализацией по умолчанию
как трейты? 293
Методы экземпляра или методы класса? 294
Композиция в сравнении с наследованием 296
Наследование полей и методы доступа 299
Шаблон "Одиночка" 302
Объектно-ориентированное проектирование с помощью
лямбда-выражений 304
Вложенные классы и лямбда-выражения 305
Лямбда-выражения в сравнении со ссылками на методы 306
Исключения и их обработка 307
Безопасное программирование на Java 310

Гnаваб. Управление оперативной памятью и параллелизм в Java 313


Основные принципы управления оперативной памятью в Java 313
Утечки памяти в Java 314
Введение в алгоритм маркировки и очистки 315
Простой алгоритм маркировки и очистки 317
Оптимизация сборки "мусора" в JVM 319
Эвакуация 320

Содержание 11
"Куча" в виртуальной машине HotSpot JVM 323
Другие сборщики "мусора" 326
Полное завершение 328
Подробное описание полного завершения 330
Поддержка параллелизма в Java 332
Жизненный цикл потока исполнения 334
Доступность и изменяемость 336
Исключение и защита состояния 338
Ключевое слово volatile 342
Полезные методы из класса Thread 344
Не рекомендованные для применения методы из класса Thread 346
Работа с потоками исполнения 347
Резюме 349

Часть 11. Работа с пnатформой Java


Глава 7. Соглашения по программированию и документированию 353
Соглашения по именованию и выделению прописными буквами 353
Именование на практике 356
Документирующие комментарии в Java 358
Структура документирующего комментария 359
Дескрипторы документирующих комментариев 361
Дескрипторы, встраиваемые в документирующие комментарии 365
Перекрестные ссылки в документирующих комментариях 367
Документирующие комментарии к пакетам 370
Доклеты 370
Соглашения по переносимым программам 371

Глава 8. Работа с коллекциями Java 375


Введение в прикладной интерфейс API для коллекций 375
Интерфейс Collection 376
Интерфейс Set 379
Интерфейс List 382
Интерфейс Мар 389
Интерфейсы Queue и BlockingQueue 395
Ввод элементов в очередь 396
Удаление элементов из очереди 396
Служебные методы 398

12 Содержание
Массивы и вспомогательные методы 401
Потоки данных и лямбда-выражения в Java 403
Функциональные подходы к программированию 404
Прикладной интерфейс Streams API 409
Резюме 414

Г11ава9. Обработка данных в типичных форматах 415


Текст 415
Специальный синтаксис для символьных строк 416
Неизменяемость символьных строк 418
Регулярные выражения 421
Числа и математические операции 425
Представление целочисленных типов данных в Java 425
Представление чисел с плавающей точкой в Java 427
Стандартная библиотека Java для математических функций 430
Представление даты и времени в версии Java 8 433
Введение в прикладной интерфейс API даты и времени
в версии Java 8 433
Запросы 437
Корректоры дат 439
Устаревшая поддержка даты и времени 440
Резюме 441

Г11ава 10. Обработка файлов и ввод-вывод 443


Классический ввод-вывод в Java 443
Файлы 444
Потоки ввода-вывода 446
Потоки чтения и записи 447
Еще раз об операторе try с ресурсами 449
Недостатки классической организации ввода-вывода 450
Современный ввод-вывод в Java 451
Файлы 451
Путь 453
Каналы и буфера системы ввода-вывода NIO 456
Отображаемые буфера байтов 459
Асинхронный ввод-вывод 460
Стиль на основе будущих действий 460

Содержание 13
Стиль на основе обратных вызовов 461
Службы наблюдения и поиска в каталогах 462
Работа в сети 464
Протокол НТТР 464
Протокол ТСР 467
Протокол IP 470

Гnава 11. Загрузка классов, рефлексия и дескрипторы методов 473


Файлы и объекты классов, метаданные 473
Примеры объектов классов 473
Объекты классов и метаданные 474
Стадии загрузки классов 476
Загрузка 476
Верификация 477
Подготовка и разрешение 478
Инициализация 478
Безопасное программирование и загрузка классов 479
Прикладная загрузка классов 481
Иерархия загрузчиков классов 483
Рефлексия 486
Когда следует пользоваться рефлексией 486
Как пользоваться рефлексией 487
Динамические прокси-классы 491
Дескрипторы методов 493
Тип метода 493
Поиск метода 494
Вызов дескрипторов методов 496

Гnава 12. Модули на платформе Java 499


Зачем нужны модули 500
Модуляризация комплекта JDK 501
Разработка собственных модулей 505
Основной синтаксис модулей 505
Построение простого модульного приложения 506
Путь к модулям 508
Автоматические модули 509
Открытые модули 509

14 Содержание
Службы 511
Многоверсионные архивные JАR-файлы 512
Преобразование в многоверсионный архивный JАR-файл 514
Переход на модульную платформу 515
Специальные образы файлов на стадии выполнения 517
Трудности, связанные с модулями 518
Класс Unsafe и затруднения, связанные с ним 518
Отсутствие контроля версий 520
Медленные темпы внедрения 520
Резюме 522

Глава 13. Инструментальные средства платформы Java 523


Инструментальные средства командной строки 523
Утилита javac 524
Утилитаjаvа 526
Утилита j ar 529
Утилита javadoc 530
Утилита jdeps 531
Утилита jps 533
Утилита j sta t 533
Утилитаjstаtd 534
Утилита j info 535
Утилита j stack 536
Утилита jmap 537
Утилита javap 538
Утилита j aotc 539
Утилита j link 539
Утилитаjmоd 541
Введение в утилиту j shell 542
Резюме 546

Приложение А. Дополнительные средства 547


Введение в Nashorn 547
Поддержка других яз1>1ков в JVM 548
Побудительные причины 549
Исполнение исходного кода JavaScript в Nashorn 550
Выполнение сценариев JavaScript из командной строки 550

Содержание 15
Применение оболочки Nashorn 551
Nashorn и javax.script 553
Применение j avax. script вместе с Nashorn 553
Расширенные функциональные возможности Nashorn 556
Обращение к Java из
Nashorn 556
Расширения языка JavaScript в Nashorn 560
Внутренний механизм действия Nashorn 562
Будущее Nashorn и GraalVM 563
VisualVM 566

Предметный указатель 573

1б Содержание
Посвящение
Эта книга посвящается всем, кто учит миру и противостоит насилию.
Предисnовие
Версия Java 8 была выпущена в марте 2014 года, а 6-е издание этой книги
вышло в свет несколько месяцев спустя. За последующие почти пять лет в
среде Java произошло немало перемен. К самым примечательным новшествам
относится внедрение модулей на платформе Java (в рамках проекта Jigsaw) и
переход на новый шестимесячный цикл выпусков очередных версий. Обе эти
доработки имеют решающее значение для дальнейшего успешного развития
платформы и экосистемы Java в течение последующих двадцати лет.
Вслед за версией Java 9, которая была выпущена с большой задержкой и
в которой были внедрены модули, сразу же последовали версии Java 10 и11,
и в настоящее время долгосрочной поддержкой пользуются все версии Java
с 8-й по 11-ю. Такие перемены в периодичности выпуска очередных версий
привели к выдвижению на передний план комплекта инструментальных

средств с открытым исходным кодом для разработки приложений на Java


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

мо от того, будут ли разработчики приложений полагаться на надежную и


испытанную версию Java 8 или же перейдут на версию Java 11, присоединив­
шись к области микрослужб.
Так или иначе, сейчас самое время перейти (или вернуться) к разработ­
ке приложений на Java. Заглядывая в будущее, следует отметить ряд важных
перемен (например, в типах значений), которые существенно изменят ха­
рактер разработки приложений на Java. Эти перемены начнут проявляться
в ближайшие один-два года и войдут в повседневный опыт разработчиков
приложений на Java.
И я буду вполне удовлетворен, если, работая над ставшим уже классиче­
ским текстом моего соавтора Дэвида Флэнагана, мне удалось сохранить не
только букву, но и дух в этом новом издании, обновив его с целью привлечь
внимание нового поколения разработчиков.

Бен Эванс, Монтерей, 2018 г.


Введение

Эта книга является настольным пособием по Java, которое верно послужит


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

свойства исполняющей системы на платформе Java. В части 11 основные по­


нятия разъясняются на примерах из самых важных прикладных интерфейсов
API. И хотя эта книга охватывает версию Java 11, мы, ее авторы, вполне осоз­
наем, что она еще не принята повсеместно, и поэтому особо подчеркиваем
там, где это уместно, что отдельное средство внедрено в версии Java 8, 9 или
10. Во всех примерах, приведенных в книге, употребляется синтаксис из вер­
сии Java 11, в том числе ключевое слово var и лямбда-выражения.

Изменения в седьмом издании


Если в шестом издании этой книги описывается версия Java 8, то в настоя­
щем издании - версия Java 11. Но с версии Java 9 процесс выпуска очередных
версий претерпел существенные изменения, и поэтому настоящее издание
вышло в свет лишь через год после выпуска версии Java 9. Кроме того, Java 11
является первой версией с долгосро11ной поддержкой (LTS), выпущенной после
версии Java 8, и поэтому можно ожидать, что многие разработчики сразу же
перейдут от Java 8 к версии Java 11.
В настоящем, седьмом издании мы попытались обновить само понятие на­
стольного справочного пособия. Ведь современным разработчикам приложе­
ний на Java требуется знать не только синтаксис и прикладные интерфейсы
API. По мере созревания среды Java особое значение приобрели (даже для
массовой разработки) такие вопросы программирования на Java, как парал­
лелизм, объектно-ориентированное проектирование, управление оператив­
ной памятью и система типов.

В настоящем издании мы решили, что большинство разработчиков, веро­


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

ковых средств после версии Java 8. Модульная система, внедренная в версии


Java 9 как главное ее нововведение, остается все еще новой - по крайней

мере, для некоторых разработчиков.

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


В первых шести главах описываются язык и платформа Java, поэтому их
очень важно прочитать полностью. В настоящем издании основное внимание
уделяется реализации Java в OpenJDK (Open Java Development Кit - комплект
инструментальных средств с открытым исходным кодом для разработки при­
ложений на Java) от компании Oracle, хотя и не в полной мере. Поэтому раз­
работчики, работающие в других средах Java, найдут для себя немало полез­
ного в этих главах. Часть 1 включает в себя следующие главы.

• Глава 1. Введение в среду Java. Дается общий обзор языка и платфор­


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

• Глава 2. Основы синтаксиса Java. Подробно поясняется язык програм­


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

программирующие на Java могут пользоваться ею как справочником по


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

проработать эту главу, чтобы изучить язык Java, хотя ее лучше читать
вместе с другой вводной литературой по Java (например, вторым из­
данием книги Head First Java Берта Бейтса (Bert Bates) и Кэти Сьерра
(Kathy Sierra), издательство O'Reilly, 2009 г.).
• Глава 3. Объектно-ориентированное проrраммирование на Java.
В главе3 поясняется, как пользоваться основным синтаксисом, описан­
ным в главе 2, для написания простых объектно-ориентированных про­
грамм на Java. В этой главе предполагается отсутствие опыта объектно­
ориентированного программирования (ООП), поэтому начинающие

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

• Глава 4. Система типов Java. Материал этой главы основывается на


описании ООП на Java, приведенном в главе 3. В ней представлены дру­
гие особенности системы типов Java, в том числе обобщенные и пере­
числимые типы, а также аннотации. Дополнив общую картину, можно
перейти к обсуждению лямбда-выражений - самого главного нововве­
дения в версии Java 8.
• Глава 5. Введение в объектно-ориентированное проектирование на
Java. Дается общий обзор основных методик надежного проектирова­
ния объектно-ориентированных программ и вкратце затрагиваются во­
просы проектных шаблонов и их применения в разработке программ­
ного обеспечения.

• Глава 6. Управление оперативной памятью и параллелизм в Java. По­


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

Материал перечисленных выше первых шести глав позволяет изучить


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

• Глава 7. Соглашения по программированию и документированию.


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

• Глава 8. Работа с коллекциями Java. Представлены стандартные би­


блиотеки коллекций Java. Они содержат структуры данных, в том числе
List, Мар и Set, играющие очень важную роль в функционировании
буквально каждой программы на Java. Здесь также поясняется новая

Введение 21
абстракция потоков данных и взаимосвязь коллекций с лямбда-выра­
жениями.

• Глава 9. Обработка данных в типичных форматах. Поясняется, как


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

• Глава 10. Обработка файлов и ввод-вывод. Рассматриваются самые


разные способы доступа к файлам: от классического, принятого в преж­
них версиях Java, до более современного и даже асинхронного. В конце
главы кратко описана работа в сети с помощью основных прикладных
интерфейсов API на платформе Java.
• Глава 11. Загрузка классов, рефлексия и дескрипторы методов. Дается
введение в тонкое искусство метапрограммирования на Java. Сначала в

ней представлено понятие метаданных о типах Java, а затем рассматри­


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

сительно нового средства - дескрипторов методов.

• Глава 12. Модули на платформе Java. Описывается модульная система


на платформе Java (JPMS), которая стала главным нововведением в вер­
сии Java 9. В ней также представлены обширные изменения, связанные
с внедрением модулей.

• Глава 13. Инструментальные средства платформы Java. Рассматрива­


ется комплект OpenJDK от компании Oracle, в состав которого входит
целый ряд полезных инструментальных средств для разработки прило­
жений на Java. К наиболее примечательным из них относятся интерпре­
татор и компилятор Java. Помимо инструментальных средств из ком­
плекта OpenJDK, в главе описываются интерактивная среда j shell и
новые средства для работы с модульной системой на платформе Java.
• Приложение А. Дополнительные средства. Описывается инстру­
ментальное средство Nashorn, служащее в качестве реализации языка
сценариев JavaScript, действующей поверх виртуальной машины Java.
Инструментальное средство Nashorn входит в состав версии Java 8
и служит альтернативой другим реализациям JavaScript. Здесь также

22 Введение
рассматривается графическое средство VisualVM, предназначенное для
текущего контроля виртуальных машин Java.

Дополнительная литература
В издательстве O'Reilly вышла серия книг по программированию на Java,
включая ряд книг, дополняющих материал настоящего издания. Ниже при­

ведено их краткое описание.

• Learning Java, 4th Edition. Patrick Niemeyer и Daniel Leuck (http: / / shop.
orei 11у.com/product/О63 692 О 02 34 63. do ). Это исчерпывающий
учебный вводный курс вJava, включая такие темы, как XML и програм­
мирование клиентских приложений на Java.

• Java 8 Lambdas. Richard Warburton (h t t р: / / s hop. о re i 11 у. com/


product/0636920030713 .do) 1• В этой книге подробно описываются
лямбда-выражения, внедренные в версии Java 8, а также вводятся поня­
тия функционального программирования, которые могут быть незна­
комы разработчикам, переходящим из прежних версий Java.
• Head First Java. Bert Bates and Kathy Sierra (http: / / shop. oreilly. com/
product/9780596009205. do) 2• В этой книге принят особый подход к
обучению Java. Те разработчики, которые привыкли мыслить наглядно,
найдут ее отличным дополнением к традиционной литературе по Java.

С полным перечнем литературы по Java от издательства O'Reilly можно оз­


накомиться по адресу http: / /j ava. oreilly. com/.

Условные обозначения, принятые в книге


В этой книге приняты следующие условные обозначения.

• Курсив. Служит для выделения термина при его упоминании в первый раз.

• Моноширинный шрифт. Им выделены адреса электронной почты, веб­


сайтов, имен файлов и каталогов.

• Полужирный моноширинный шрифт. Служит для выделения команд.

1 В русском переводе книга вышпа под н.вванием Лямбда-выражения в Java 8 в издатепьстве


"ДМК': Москва, 2014 г.
2 В русском переводе книга вышпа под названием Изучаем fava в издатепьстве "Эксмо",
Москва, 2018 г.

Введение 23
• Наклонный моноширинный шрифт. Им выделены аргументы методов,
заполнителей, вместо которых подставляются конкретные значения в

программе, а также принципиальные разделы или строки кода.

г----,

1i.. Совет или рекомендация.

LJJ\
1

~i Общее примечание.

~
Предупреждение или предостережение.

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

тичный подход к делу, служивший дополнительным стимулом к написанию


книги на самых важных этапах работы над ней.
Особая благодарность выражается Джиму Гауфу, Ричарду Уэрбертону,
Джону Оливеру, Трише Джи, а также Стивену Коулбурну.
Как всегда, Мартийн Вербург оставался добрым другом, деловым партне­
ром, внимательным слушателем и кладезем полезных советов.

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


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

ся в полной мере их дельными советами, то в этом он может винить только


самого себя.

Введение 25
Об авторах
Бенджамин Дж. Эванс - соучредитель начинающей компании jClarity,
предоставляющей инструментальные средства тестирования и оценивания
производительности в помощь группам разработки и эксплуатации про­
граммного обеспечения. Он является также организатором сообщества раз­
работчиков LJC (London Java Community) и членом исполнительного коми­
тета процесса JCP, помогая определять стандарты для экосистемы Java. Он
обладает почетным званием "чемпиона по Java'' и титулом "рок-звезды" как
авторитетного участника конференций JavaOne, является одним из авторов
книги The Well-Grounded fava Developer и регулярно выступает с публичными
докладами по платформе Java, производительности, параллелизму и смежным
вопросам. Бенджамин окончил Кембриджский университет, получив степень
магистра математических наук.

Дэвид Флэнаrан служит инженером по разработке программного обе­


спечения в компании Mozilla; в настоящее время работает над веб-сайтом
MDN (Mozilla Developer Network - Сеть разработчиков компании Mozilla).
К числу его книг, вышедших в издательстве O'Reilly, относятся семь изданий
данной книги, fava Examples in а Nutshell, fava Foundation Classes in а Nutshell,
favaScript: The Definitive Guide, а также favaScript Pocket Reference. Дэвид окон­
чил Массачусетсский технологический институт, получив степень магистра
вычислительных наук и техники. Он проживает со своей женой и детьми на
северо-западном тихоокеанском побережье Соединенных Штатов между го­
родами Сиэтл, Вашингтон и Ванкувер, что в канадской провинции Британ­
ская Колумбия.
Об изображении на обложке
На обложке этой книги изображен яванский тигр (Panthera tigris
sondaica) - особый подвид для острова Ява. И хотя такая генетическая обо­
собленность яванского тигра однажды представила биологам и другим ис­
следователям неимоверную возможность изучать эволюцию видов, данный
подвид почти исчез с начала вторжения человека в ареал его обитания. Поло­
жение этого вида тигров усугубляется еще и тем, что остров Ява стал самым
густонаселенным на Земле, и осознание зыбкости такого положения яванских
тигров, очевидно, пришло слишком поздно, чтобы обеспечить их выживание
даже в неволе.

В последний раз яванского тигра явно наблюдали в 1976 году, ав 1994 году
этот вид был объявлен Всемирным фондом дикой природы исчезнувшим.
Тем не менее в национальном парке Меру Бетири, который находится в рай­
оне горного кряжа Муриа, что в восточной части острова Ява, наблюдаются
случаи появления этого вида тигров. Что бы убедиться в том, что яванские
тигры по-прежнему существуют, в 2012 году были использованы фотоловушки.
Многие виды животных, изображенных на обложках книг издательства
O'Reilly, находятся под угрозой исчезновения, хотя все они важны для нашего
мира. Подробнее о том, как помочь спасению этих видов животных, можно
узнать по адресу animals. oreilly. com. На лицевой стороне обложки кни­
ги приведено изображение гравюры XIX века из Дуврского художественного
архива.

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

Наши электронные адреса:


E-mail: info@williaтspuЫishing. сот
WWW: http://www. williaтspuЬlishing. сот

28 Введение
1
Введение в Java

Часть 1 служит введением в язык и платформу Java. В перечисленных ниже


главах предоставляется достаточно сведений, чтобы приступить к надлежа­
щему пользованию языком Java.
Глава 1. Введение в среду Java
Глава 2. Основы синтаксиса Java
Глава 3. Объектно-ориентированное программирование на Java
Глава 4. Система типов Java
Глава 5. Введение в объектно-ориентированное проектирование на Java
Глава 6. Управление оперативной памятью и параллелизм в Java
1
Введение в среду Java

Добро пожаловать в версию Java 11. Этот номер версии, вероятно, удивит
вас так же, как и нас. Еще совсем недавно версия Java 5 казалась чем-то но­
вым, а ведь с тех пор прошло уже пятнадцать лет и выпущено 6 основных

версий.

Вполне возможно, что вы вступаете в экосистему Java из среды другого


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

В этой главе подготавливается почва для описания языка Java, на котором


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

ны Java, прежде чем перейти к обсуждению жизненного цикла программы


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

Язык, виртуальная маwина и экосистема Java


Среда программирования на Java существует с конца 1990-х годов. Она со­
стоит из языка Java и поддерживающей его исполняющей системы, иначе на­
зываемой виртуальной машиной Java (JVM).
В то время, когда был разработан язык Java, такое разделение считалось
новшеством, но теперь оно стало в большей степени общей тенденцией в раз­
работке программного обеспечения. Примечательно, что в среде .NET, выпу­
щенной корпорацией Microsoft через несколько лет после Java, был принят
весьма схожий подход к архитектуре платформы.
Важное отличие платформ .NET и Java заключается в том, что Java всегда
воспринималась как относительно открытая экосистема со многими постав­

щиками, хотя ее разработка велась владельцем технологии. На протяжении


всей истории развития Java эти поставщики сотрудничали и соперничали за

отдельные компоненты технологии Java.


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

В настоящее время Java принадлежит корпорации Oracle, которая приоб­


рела компанию Sun Microsystems, где был первоначально разработан язык
Java. В создание реализаций стандартизированных технологий внесли нема­
лый вклад и другие коммерческие организации, в том числе Red Hat, IВМ,
Amazon, AliBaba, SAP, Azul Systems и Fujitsu.

Начиная с версии Java 11, основной эталонной реализацией Java

1~
является комплект OpenJDK с открытым исходным кодом, в раз­
работке которого принимают участие многие из перечисленных
выше коммерческих организаций. Они поставляют свои про­
граммные продукты на основе именно этого комплекта.

По существу, Java состоит из нескольких различных, хотя и связанных


вместе сред и спецификаций, в том числе Java Moblle Edition (Java МЕ) 1 , Java
Standard Edition (Java SE) и Java Enterprise Edition (Java ЕЕ)2. В этой книге рас­
сматривается только среда Java SE версии 11, хотя в ней делаются некоторые

1 Java МЕ - это прежний стандарт для смартфонов и мобильных телефонов. И хотя в


настоящее время на смартфонах и мобильных телефонах более распространены операционные
системы Android и iOS, тем не менее Java МЕ занимает немалую долю рынка для встроенных
устройств.

2 Платформа Java ЕЕ теперь перенесена в Eclipse Foundation, где она продолжает свое
существование в виде проекта Jakarta ЕЕ.

32 Глава 1. Введение в среду Java


исторические заметки относительно времени внедрения отдельных средств

на платформе. Мы еще коснемся в дальнейшем вопросов стандартизации, а


до тех пор перейдем к рассмотрению языка Java и виртуальной машины JVM
как отдельных, хотя и взаимосвязанных понятий.

Что собой представляет язык Java


Программы на Java написаны в виде исходного кода на языке Java. Это
удобочитаемый язык программирования, строго основанный на классах и
ориентированный на объекты. Синтаксис этого языка намеренно смоделиро­
ван на основе синтаксиса С и С++ и явно рассчитан на тех программистов,
которые перешли из этих языков.

Несмотря на то что исходный код Java очень похож на исходный


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

Язык Java считается относительно простым для чтения и написания, хотя


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

положен отраслевой опыт применения таких языков, как С++, и в нем пред­
принята попытка удалить сложные языковые средства, но в то же время со­

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

В общем, язык Java служит устойчивым, прочным основанием для разра­


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

лали в технологию Java.


Язык Java подвергался постепенному пересмотру, хотя и не был полностью
переписан, начиная с его появления в 1996 году. И это означает, что неко­

торые первоначальные проектные решения в Java, которые были уместны в


конце 1990-х годов, оказывают влияние на сам язык и поныне (подробнее об
этом - в главах 2 и 3).
Самые радикальные изменения языка были внесены в версии Java 8,
т.е. почти через десять лет, а некоторые считают, что и с момента его за­

рождения. Такие языковые средства, как лямбда-выражения, и тщательно

Язык, виртуальная машина и экосистема Java 33


пересмотренный базовый код коллекций нашли весьма широкое распростра­
нение, навсегда изменив подход разработчиков к написанию кода на Java.
С тех пор была выпущена версия 9 с главным (и долго откладывавшимся)
дополнением: модульной системой на платформе Java (JPMS).
После выпуска версии Java 9 проект был переведен на новую модель бо­
лее частых выпусков очередных версий Java через каждые шесть месяцев, что
и привело к появлению версии Java 11. Язык Java подчиняется специфика­
ции JLS (Java Language Specification - спецификация на язык Java), в которой
определяется поведение реализации, которая ей соответствует.

Что такое JVM


JVM - это программа, предоставляющая исполняющую среду, которая

требуется для выполнения программ на Java. Программы на Java не могут вы­


полняться, если отсутствует JVM для соответствующих аппаратных средств
и операционной системы на той платформе, где требуется их выполнить.
Правда, виртуальная машина JVM перенесена для выполнения на большом
числе платформ: от игровых приставок и проигрывателей формата Blu-ray до
больших ЭВМ.
Программы на Java, как правило, запускаются из командной строки в сле­

дующей форме:

java <аргументы> <имя_проrраммы>

В итоге JVM порождается как процесс операционной системы, предостав­


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

важных частей программы, заменяя их равнозначным скомпилированным

машинным кодом. Следует также иметь в виду, что JVM и пользовательская

34 Глава 1. Введение в среду Java


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

Архитектура JVM построена на основе многолетнего опыта работы с


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

• Служить контейнером для выполняемого в нем прикладного кода.

• Обеспечить более безопасную и надежную среду выполнения, чем


в С/С++.

• Избавить разработчиков от необходимости управлять памятью.

• Предоставить межплатформенную среду выполнения.

Первая из перечисленных выше целей упоминалась ранее при рассмотре­

нии виртуальной машины JVM и ее интерпретатора байт-кода. В соответ­


ствии с этой целью JVM служит контейнером для прикладного кода. Вторая
и третья цели будут рассмотрены в главе 6, когда речь пойдет об управлении
памятью в среде Java.
Четвертая цель иногда называется "написано однажды, выполняется вез­
де" (WORA). Это свойство файлов классов Java, позволяющее переносить их
с одной платформы на другую, где они будут неизменно выполняться при на­
личии JVM. И это означает, что программа на Java может быть разработана
(и преобразована в файлы классов) на машине, работающей под управлением
macOS, а файлы классов перенесены затем в Linux, Windows или на другие
платформы, где они будут выполняться без всяких доработок.

Среда Java была повсеместно перенесена на самые разные плат­


формы, включая такие их основные разновидности, как Linux,
macOS и Windows. В этой книге употребляется выражение
"большинство реализаций", обозначающее те платформы, с кото­
рыми чаще всего приходится иметь дело разработчикам. Такие
платформы, как macOS, Windows, Linux, BSD Unix и аналогич­
ные им, считаются основными и причисляются к большин­
ству реализаций.

Язык, виртуальная машина и экосистема Java 35


Помимо рассмотренных выше четырех основных целей, у архитектуры
JVM имеется еще одна, не всегда признаваемая или обсуждаемая особен­
ность. Информация о выполнении используется в ней для самоуправления.
В результате исследований программного обеспечения в 1970-е и 1980-е
годы обнаружилось, что поведение программ во время выполнения дает не­
мало интересных и полезных образцов, которые невозможно выявить во
время компиляции. Виртуальная машина JVM стала первой действительно
ведущей платформой, на которой были использованы результаты подобных
исследований.

Она собирает информацию о выполнении, чтобы принимать более взве­


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

Характерным тому примером служит то обстоятельство, что не все части


программы на Java будут равновероятно вызываться в течение срока дей­
ствия программы: одни из них будут вызываться намного чаще, чем другие.
На платформе Java это обстоятельство выгодно используется в технологии,
называемой динамической (JIТ) компиляцией.
В виртуальной машине HotSpot JVM, которая была впервые включена ком­
панией Sun Microsystems в состав версии Java 1.3 и применяется до сих пор,
сначала определяются те части программы, которые вызываются чаще всего.

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

С и С++. Стандарт, в котором описывается надлежащий режим функциони­


рования JVM, называется спецификацией виртуальной машины Java (JVM
Specification).

Что такое экосистема Java


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

36 Глава 1. Введение в среду Java


JVM служит прочным, переносимым, высокопроизводительным основанием
для выполнения программ на Java (и других языках). Совместно эти взаимос­
вязанные технологии служат прочным основанием, на которое коммерческие

организации могут уверенно опереться, когда им приходится выбирать, на


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

На самом деле в современной технологической экосистеме можно редко

найти компонент технологии, не предлагающий соединитель с Java. Всякий


тип корпоративной системы текущего контроля, реляционные базы данных,
нереляционные распределенные базы данных типа NoSQL, системы обмена
сообщениями, Интернет вещей (IoT) - все это интегрируется с Java.
Именно это обстоятельство послужило главной побудительной причиной
для принятия технологий Java на предприятиях и в крупных компаниях. Раз­

работчики смогли раскрыть свой творческий потенциал, пользуясь готовыми


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

Платформу Android от компании Google иногда считают осно­


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

Платформа Java становится привлекательной для выполнения приклад­


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

В действительности, кроме Java, имеется немало других языков программи­


рования, нацеленных на применение JVM и взаимодействие с Java, что дает
возможность выгодно пользоваться в них успехами данной платформы. К их
числу относятся Kotlin, Scala, Groovy и многие другие языки. И хотя все они

Язык, виртуальная машина и экосистема Java 37


невелики по сравнению с Java, тем не менее, занимают отдельные ниши в сре­

де Java, служа источником для нововведений и составляя здоровую конкурен­

цию Java.

Краткая история развития Java


Версия fava 1.0 (1996 г.)
Это была первая общедоступная версия Java и содержала лишь 212
классов, организованных в восемь пакетов. На платформе Java всегда
делался акцент на обратную совместимость, и поэтому код, написанный
в версии Java 1.0, будет выполняться и в версии Java 11 без дополни­
тельной модификации или перекомпиляции.

Версия fava 1.1 (1997 г.)


Эта версия более чем в два раза увеличила размеры платформы Java.
В ней были внедрены так называемые "внутренние классы" и первая
версия прикладного интерфейса Reflection API, предназначенного для
рефлексии.

Версия fava 1.2 (1998 г.)


Это была весьма значительная версия Java, поскольку в ней размеры
платформы Java увеличились втрое. Данная версия примечательна по­
явлением прикладного интерфейса Java Collections API с коллекциями
в виде списков, множеств и отображений. Многие новые средства, поя­
вившиеся в версии 1.2, вынудили компанию Sun Microsystems переиме­
новать данную платформу в "платформу Java 2·: Но обозначение "Java 2"
стало просто торговой маркой, а не номером конкретной выпущенной

версии.

Версия fava 1.3 (2000 г.)


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

Версия fava 1.4 (2002 г.)


Это была еще одна довольно крупная версия, дополненная такими
новыми весьма важными функциональными возможностями, как

38 Глава 1. Введение в среду Java


повышенная производительность, прикладной интерфейс API низ­
коуровневого ввода-вывода, регулярные выражения для обработки
текста, библиотеки XML и XSLT, поддержка сетевого протокола SSL,
прикладной интерфейс API протоколирования, а также поддержка
шифрования.

Версия fava 5 (2004 г.)


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

версий. В состав этой версии вошло 3562 класса и интерфейса в 166 па­
кетах. К примечательным дополнениям данной версии относятся ути­
литы для параллельного программирования, каркас и классы для уда­

ленного управления, а также инструментарий для самой виртуальной


машины Java.
Версия Java 6 (2006 г.)
И эта версия стала в основном обновленной в отношении эксплуата­
ции и производительности. В ней был внедрен прикладной интерфейс
Compiler API, расширено применение и область видимости аннотаций,
а также предоставлены привязки для взаимодействия языков сценариев

с Java. Кроме того, было исправлено большое количество внутренних


программных ошибок и внесено усовершенствований в виртуальную
машину JVM и библиотеку Swing, предназначенную для построения
графического пользовательского интерфейса приложений на Java.
Версия Java 7 (2011 г.)
Эта версия стала первой, выпущенной под эгидой компании Oracle.
В нее был включен целый ряд основных обновлений языка и платфор­
мы. Благодаря внедрению оператора try с ресурсами и прикладного

интерфейса NI0.2 API разработчики получили возможность писать


более надежный и менее подверженный ошибкам код для обращения
с ресурсами и организации ввода-вывода. В прикладном интерфейсе
Method Handles API были предоставлены дескрипторы методов в каче­
стве более простой и надежной альтернативы рефлексии. Кроме того,
в данной версии был открыт доступ к invokedynamic - первой ин­
струкции на уровне байт-кода после выпуска версии Java 1.0.

Краткая история развития Java 39


Версия Java 8 (2014 г.) (LTS)
Это была крупная версия, внесшая самые существенные изменения в
язык после версии Java 5, а возможно, и всех предыдущих версий. Бла­

годаря внедрению лямбда-выражений появилась возможность значи­


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

Версия fava 9 (2017 г.)


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

Версия fava 10 (март 2018 г.)


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

Версия fava 11 (сентябрь 2018 г.) (LTS)


Это текущая версия Java, также разработанная в течение краткосроч­
ного шестимесячного цикла. Она стала первой модульной версией Java
с долгосрочной поддержкой (LTS). В ней было внедрено относительно
мало новых средств, непосредственно доступных разработчикам: диа­
гностическое средство Flight Recorder и новый прикладной интерфейс
НТТР/2 API. В данной версии внесен ряд дополнительных изменений,
хотя она предназначена в основном для повышения устойчивости.

В настоящий момент производственными являются только версии Java 8


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

40 Глава 1. Введение в среду Java


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

Жизненный цикn проrраммы на Java


Чтобы стало понятнее, каким образом компилируется и исполняется код,
написанный на Java, и в чем отличие Java от других типов сред программиро­
вания, рассмотрим конвейерную схему, приведенную на рис. 1.1.
"--------------,
1
1

.class
: __.1 Новый 1
:,- тип

~ javac
010110
001011
Загрузка классов//'
-------
~~ 110100

C)""'"~=~j
100010

: JVM :
~---------~----~
Рис. 1.1. Порядок компиляции и загрузки кода /ava

Данный процесс начинается с исходного кода на языке Java, который пере­

дается утилите j avac для получения файлов классов, содержащих результат


компиляции исходного кода в байт-код Java. Файл класса является наимень­
шей функциональной единицей, которой оперирует платформа Java, и един­
ственным способом преобразовать новый код в выполняемую программу.
Новые файлы классов становятся доступными через механизм загрузки
классов (подробнее о том, как действует загрузка классов, речь пойдет в главе
10). Благодаря этому новый тип становится доступным интерпретатору для
выполнения.

Часто задаваемые вопросы

В этом разделе даются ответы на некоторые из наиболее часто задаваемых


вопросов по поводу Java и жизненного цикла программ, написанных в среде
программирования на Java.

Что такое байт-код?

Когда разработчики впервые знакомятся с виртуальной машиной JVM,


они иногда считают ее "одним компьютером внутри другого". И поэтому

Жизненный цикл программы на Java 41


байт-код им нетрудно представить себе как "машинный код для ЦП внутрен­
него компьютера" или "машинный код для вымышленного компьютера".
В действительности байт-код совсем не похож на машинный код, который
обычно выполняется на реальном аппаратном процессоре. Напротив, специ­
алисты по вычислительной технике обычно называют байт-код разновидно­
стью промежуточного представления, находящегося на полпути между ис­
ходным и машинным кодом. Все назначение байт-кода состоит в том, чтобы
служить форматом для эффективного выполнения интерпретатором JVM.
Является ли утилита javac компилятором?
Как правило, компиляторы производят машинный код, тогда как утилита

javac - байт-код, совсем не похожий на машинный код. Тем не менее фай­


лы классов в какой-то степени похожи на объектные файлы (с расширением
. dll в Windows или . so в Unix), и они, конечно, неудобочитаемы.
С точки зрения теории вычислительной техники утилита j avac больше
всего похожа на переднюю половину компилятора. Ведь она создает проме­
жуточное представление, которое может быть затем использовано для полу­
чения (генерирования) машинного кода.
Но поскольку файлы классов создаются на отдельной стадии процесса по­
строения, напоминающей компиляцию исходного кода на С/С++, многие раз­
работчики считают выполнение утилиты j avac компиляцией. В этой книге
термины "компилятор исходного кода" или "компилятор j avac" будут ис­
пользоваться для обозначения стадии получения файлов классов с помощью
утилиты javac. А термин "компиляция" отдельно зарезервирован для обо­
значения динамической компиляции, поскольку именно она фактически дает
машинный код.

Откуда название "байт-код"?


Код инструкции (или код операции) составляет единственный байт, хотя
у некоторых операций имеются также параметры, указываемые после них

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


практике используется около 200 инструкций, хотя некоторые из них не гене­
рируются в последних версиях утилиты javac.
Является ли байт-код оптимизированным?

На начальном этапе развития платформы Java утилита javac производила


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

42 Глава 1. Введение в среду Java


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

Действительно ли байт-код является машинно-независимым?


А как насчет порядка следования байтов?

Формат байт-кода всегда остается неизменным независимо от типа маши­


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

Является ли Java интерпретирующим языком?

По существу, виртуальная машина JVM является интерпретатором, хотя


динамическая компиляция заметно повышает ее производительность. С од­
ной стороны, в большинстве интерпретирующих языков (например, РНР,
Perl, Ruby и Python) программы интерпретируются непосредственно из ис­
ходного кода (как правило, путем построения абстрактного синтаксического
дерева из исходного файла). А с другой стороны, интерпретатору JVM требу­
ются файлы классов, что, в свою очередь, требует отдельной стадии компиля­
ции ИСХОДНОГО кода утилитой j avac.
Можно ли выполнять на JVM программы,
написанные на других языках?

Да, можно. На виртуальной машине JVM можно выполнить любой досто­


верный файл класса, а это означает, что программы, написанные на других
языках, кроме Java, могут быть выполнены двумя способами. Во-первых, у
них может быть компилятор исходного кода, производящий аналогично ути­
лите javac файлы классов, которые будут выполняться на JVM таким же об­
разом, как и код Java (именно такой способ принят в языках, подобных Scala).
И, во-вторых, в других языках, кроме Java, можно реализовать интерпретатор
и исполняющую среду на Java, чтобы интерпретировать исходный код непо­
средственно из этих языков. Именно такой способ принят в языках, подоб­
ных JRuby, хотя исполняющая среда JRuby настолько сложна, что способна
выполнять вторичную динамическую компиляцию при определенных обсто­
ятельствах.

Жизненный цикл программы на Java 43


БезопасностьJаvа
Платформа Java с самого начала разрабатывалась с учетом безопасности,
и это дает ей заметное преимущество над многими другими существующими

системами и платформами. Архитектура безопасности Java была разработана


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

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


В этом разделе вкратце описываются некоторые отличия платформы Java
от других известных сред программирования.

44 Глава 1. Введение в среду Java


Сравнение Java с языком С
• Язык Java является объектно-ориентированным, а язык С - процедурным.

• Язык Java является переносимым через файлы классов, а язык С требу­


ет перекомпиляции кода.

• Язык Java предоставляет обширный инструментарий как часть испол­


няющей среды.

• В языке Java отсутствуют указатели и равнозначная арифметика ука­


зателей.

• В языке Java обеспечивается автоматическое управление памятью через


сборку "мусора".

• В языке Java отсутствует возможность распределять память на низком


уровне (т.е. отсутствуют структуры).

• В языке Java отсутствует препроцессор.

Сравнение Java с языком С++


• В языке Java применяется более простая объектная модель, чем вязы­
ке С++.

• Диспетчеризация в Java является виртуальной по умолчанию.

• Параметры методов в Java всегда передаются по значениям, хотя имеет­


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

• В языке Java не поддерживается полноценное множественное наследо­


вание.

• Обобщения в Java менее эффективны, но в то же время менее опасны,


чем шаблоны в языке С++.

• В языке Java отсутствует переопределение операций.

Сравнение Java с языком Python


• Язык Java является статически типизированным, тогда как язык Pyt-
hon - динамически типизированным.

• Язык Java является многопоточным, а в языке Python допускается одно­


временное исполнение кода лишь в одном потоке.

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


• В языке Java поддерживается динамическая компиляция, а в основной
реализации Python такая поддержка отсутствует.

• Байт-код Java подвергается обширным статическим проверкам, тогда


как байт-код Python им не подвергается.

Сравнение Java с языком JavaScript


• Язык Java является статически типизированным, тогда как язык
JavaScript - динамически типизированным.

• В языке Java применяются объекты, основанные на классах, а в языке


JavaScript - объекты, основанные на прототипах.
• В языке Java обеспечивается добротная инкапсуляция объектов, тогда
как в языке JavaScript она не обеспечивается.

• В языке Java имеются пространства имен, тогда как в языке JavaScript


они отсутствуют.

• Язык Java является многопоточным, а язык JavaScript таковым не явля­


ется.

Ответы на критику в адрес Java


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

ниями в сочетании с чрезмерно рьяным рекламированием в первых версиях

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

Чрезмерная многословность
Базовый язык Java иногда критиковали за чрезмерную многословность.
Ведь даже такие простые операторы Java, как Obj ect о = new Obj ect () ; ,
кажутся избыточными, поскольку тип Obj ect присутствует в них по обе
стороны знака равенства в операции присваивания. Критики, указывавшие
на такую избыточность, отмечали, что в других языках программирования

46 Гnава 1. Введение в среду Java


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

приходится реагировать на перебои или требуется сопровождать прикладной


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

В последних версиях Java разработчики языка попытались отреагировать


на некоторые из возникших вопросов, найдя те места, где синтаксис может

стать менее многословным, и лучшее применение информации о типах. На­


пример:

//Файлы вспомогательных методов:


byte[] contents =
Files.readAllBytes(Paths.get("/home/ben/myFile.Ьin"));

// Ромбовидный синтаксис для повторяющейся


//информации о типах:
List<String> 1 = new ArrayList<>();

//Выведение типа локальных переменных:


var threadPool = Executors.newScheduledThreadPool(2);

//Лямбда-выражения, упрощающие запускаемые


11 потоки исполнения:
threadPool.submit(() ->
{ System.out.println("On Threadpool"); });

Тем не менее общий принцип Java состоит в том, чтобы вносить измене­
ния в язык очень медленно и обстоятельно. Поэтому темпы этих изменений
могут совершенно не удовлетворять критиков.

Медленность изменений
Язык Java существует в своем первоначальном виде более двадцати лет,
не претерпев за это время полного пересмотра. За тот же самый пери­
од были выпущены обратно совместимые версии многих других языков

Ответы на критику в адрес Java 47


программирования (например, С# от корпорации Microsoft), и поэтому неко­
торые разработчики критикуют создателей Java за то, что они не поступили
таким же образом. Более того, за последние годы язык Java не раз попадал
под огонь острой критики за медленное внедрение тех языковых средств, ко­

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

Консервативный подход к разработке языка программирования был при­


нят компанией Sun Microsystems (а затем и компанией Oracle) с целью избе­
жать накладных расходов и внешних последствий от внедрения ошибочных
средств в весьма крупную пользовательскую базу. Многие компании, прини­
мавшие участие в разработке Java, вложили основные средства в эту тех­
нологию, и поэтому разработчики языка несли особую ответственность за
нарушение существующей пользовательской и установочной базы.
Каждое новое языковое средство должно быть тщательно продумано не
только в отдельности, но и с точки зрения его взаимодействия со всеми суще­
ствующими языковыми средствами. Влияние новых языковых средств может

иногда выходить за пределы их непосредственной области действия. А ведь


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

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

48 Глава 1. Введение в среду Java


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

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

лированный в платформенно-ориентированный код С и С++.


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

Ненадежность
В течение 2013 года в безопасности платформы Java был обнаружен це­
лый ряд уязвимостей, что заставило отсрочить дату выпуска версии Java 8.
Но даже перед этим некоторые подвергли критике рекордное количество уяз­
вимостей в безопасности платформы Java.
Многие из этих уязвимостей относились к настольным компонентам, а
также компонентам GUI в системе Java, не оказывавшим отрицательного
влияния на код веб-сайтов или другой серверный код, написанный на Java.
Следует, однако, подчеркнуть, что на всех платформах программирования
периодически возникают осложнения, и во многих других языках програм­

мирования имеется сравнимая история обнаружения уязвимостей в безопас­


ности, которые намного менее общеизвестны.

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

Ответы на критику в адрес Java 49


программного обеспечения с открытым исходным кодом. Это один из наи­
более употребительных языков программирования в проектах, размещаемых
в информационном хранилище GitHub и других местах хранения проектов
в Интернете.
Наконец, наиболее широко применяемая реализация самого языка Java ос­
новывается на комплекте OpenJDK, который, в свою очередь, является про­
ектом с открытым исходным кодом, с энергичным и постоянно растущим

числом участников.

50 Глава 1. Введение в среду Java


2
Основы синтаксиса Java

В этой главе дается краткое, но всеобъемлющее введение в синтаксис Java.


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

Итак, в главе описывается синтаксис программ на Java, начиная с самого


низкого уровня и постепенно переходя к структуре более высокого порядка.
Здесь освещаются следующие вопросы.

• Символы, употребляемые для написания программ на Java, и кодировка


ЭТИХ СИМВОЛОВ.

• Литеральные значения, идентификаторы и прочие лексемы, составляю­


щие программу на Java.
• Типы данных, которыми можно манипулировать в Java.
• Операции, применяемые в Java для группирования отдельных лексем
в более крупные выражения.

• Операторы, группирующие выражения и прочие операторы в форму


логических блоков кода Java.
• Методы, которые являются именованными совокупностями операторов
Java и могут вызываться из других блоков кода Java.
• Классы, представляющие собой совокупности методов и полей. Классы
являются центральными элементами программы на Java и составляют
основу объектно-ориентированного программирования. Рассмотрению
классов и объектов посвящена глава 3.
• Пакеты, являющиеся совокупностями связанных вместе классов.

• Программы на Java, состоящие из одного или нескольких взаимодей­


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

Синтаксис большинства языков программирования сложен, и в этом от­


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

Анализ проrрамм на Java по нисходящей


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

Мы еще вернемся к рассмотрению операторов package и import в разделе

"Пакеты и пространство имен в Java" далее в этой главе.


После необязательных объявлений в операторах package и import следу­
ет нуль или больше ссылок на определения типов. Подробнее о ссылках на
разнообразные типы данных речь пойдет в главах 3 и 4, а до тех пор доста­
точно сказать, что эти ссылки чаще всего встречаются в определениях, кото­

рые делаются в операторах class или interface.


В определении ссылки на тип данных встречаются такие члены, как поля,

методы и конструкторы. Наибольшее значение из всех этих членов имеют

52 Глава 2. Основы синтаксиса Java


методы, которые являются блоками кода Java, состоящими из операторов.
Итак, определив эти основные термины, приступим к анализу программы на
Java снизу вверх, исследуя основные единицы синтаксиса, которые нередко
называются лексемами.

Лексическая структура
В этом разделе поясняется лексическая структура программы на Java. Он

начинается с рассмотрения набора символов в Юникоде, применяемых для


написания программ на Java. Затем в этом разделе описываются лексемы, со­
ставляющие программу на Java, а также поясняется назначение комментари­
ев, идентификаторов, зарезервированных слов, литералов и прочего.

Набор символов в Юникоде


Программы на Java пишутся с помощью набора символов в Юникоде.
Символы в Юникоде можно употреблять в программе на Java повсюду, в
том числе в комментариях и таких идентификаторах, как имена переменных.
В отличие от набора 7-разрядных символов в коде ASCII, подходящего для
английского языка, и набора 8-разрядных символов Latin-1 по стандарту ISO,
пригодного для большинства западноевропейских языков, набор символов в
Юникоде способен представить алфавит практически любого письменного
языка, употребляемого на планете.

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


Юникода или не желаете принуждать других программистов,
просматривающих или редактирующих написанный вами код,
пользоваться текстовым редактором с поддержкой Юникода,
вставьте символы в Юникоде в свои программы на Java, приме­
няя специальную управляющую последовательность \ uxxxx, где
после обратной косой черты и строчной буквы u указывается
4-разрядный шестнадцатеричный код символа в Юникоде. На­
пример, управляющая последовательность \ u О О 2О обозначает
символ пробела, а управляющая последовательность \uОЗсО -
символ тт.

Разработчики Java вложили немало труда и потратили немало времени,


чтобы сделать поддержку Юникода первоклассной. Если ваше коммерче­
ское приложение рассчитано на пользователей во всем мире, особенно не на

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

Юникода.

Учет реrистра букв и пробелы


В языке Java учитывается регистр букв. Его ключевые слова написаны
прописными буквами и должны использоваться именно в таком виде. Это
означает, что слова While и WHILE не равнозначны ключевому слову while.
А если переменная объявлена в программе под именем i, то на нее нельзя
ссылаться по имени I.

В общем, полагаться на учет регистра букв, чтобы различать

1~
1

1_ -
:
идентификаторы, не стоит. Ни в коем случае не пользуйтесь
этим в своем коде и, в частности, никогда не присваивайте иден­

тификатору такое же имя, как и ключевое слово, но только в


другом регистре букв.

В языке Java игнорируются пробелы, знаки табуляции, новой строки и


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

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

который начинается с символов // и продолжается до конца текущей строки.


Например:

int i = О; 11 инициализировать переменную цикла

Второй разновидностью является многострочный комментарий, кото­


рый начинается с символов / * и продолжается на протяжении любого числа
строк вплоть до символов * /. Любой текст, находящийся между символами

54 Глава 2. Основы синтаксиса Java


/*и*/, игнорируется утилитой компиляции javac. И хотя такая разновид­
ность обычно применяется для составления многострочных комментариев,
она вполне пригодна и для однострочных комментариев. Но такого рода ком­
ментарии не могут быть вложенными (т.е. один комментарий/* */не может
присутствовать в другом комментарии). Составляя многострочные коммен­
тарии, программисты нередко пользуются дополнительными символами *,
чтобы выделить комментарии в исходном тексте программы. Ниже приведен
типичный пример многострочного комментария.

/*
* Сначала установить соединение с сервером.

* Если попытка установить соединения окажется


* неудачной, сразу же выйти из программы.
*/

Третья разновидность комментариев является частным случаем второй.


Так, если комментарий начинается с символов / * *, он считается специаль­
ным документирующим комментарием. Как и обычные многострочные ком­
ментарии, документирующие комментарии оканчиваются символами */ и не
допускают вложение. Если вы пишете класс Java, предполагая, что им будут
пользоваться другие программисты, снабдите его документирующими ком­
ментариями, чтобы вставить документацию на этот класс и все его методы
непосредственно в исходный код. Утилита javadoc извлечет эти коммента­
рии и обработает их, чтобы автоматически составить оперативно доступную
документацию на ваш класс. Документирующий комментарий может содер­

жать дескрипторы НТМL-разметки и дополнительный синтаксис, понятный


утилите javadoc. Например:

/**
* Выгрузить файл на веб-сервер

*
* @param file Выгружаемый файл
* @return <tt>true</tt> при удачном исходе операции,
* <tt>false</tt> при неудачном исходе операции
* @author David Flanagan
*/

Дополнительные сведения о синтаксисе документирующих комментариев


приведены в главе 7, а об утилите javadoc - в главе 13.
Комментарии могут появляться между любыми лексемами в программе
на Java, но только не в самой лексеме. В частности, комментарии нельзя вво­
дить в строковые литералы, заключаемые в двойные кавычки. Ведь любой

Лексическая структура SS
комментарий, находящийся в строковом литерале, становится частью этого

литерала.

Зарезервированные слова
Перечисленные ниже слова являются зарезервированными в языке Java.
Они относятся к синтаксису этого языка, и поэтому их нельзя употреблять
для именования переменных, классов и прочих элементов программы.

abstract const final int puЫic throw


assert continue finally interface return throws
boolean default float lonq short transient
break do for native static true
byte douЫe qoto new strictfp try
case else if null super void
catch enum implements packaqe switch volatile
char extends import private synchronized while
class false instanceof protected this

Ключевые слова
true, false и null формально считаются литералами.
Последовательность символов var не является ключевым словом и вместо

этого указывает на выводимость типа локальной переменной. А последова­


тельность, состоящую из единственного знака подчеркивания (_), нельзя упо­
треблять в качестве идентификатора. Кроме того, имеются 10 ограниченных
ключевых слов, которые считаются ключевыми только в контексте объявле­
ния модуля на платформе Java.
Нам еще не раз встретятся упомянутые выше зарезервированные слова

далее в этой книге. Одни из них обозначают примитивные типы данных, дру­
гие - операторы Java (о тех и других речь пойдет далее в этой главе), а третьи
служат для определения классов и их членов (подробнее о них - в главе 3).
Однако ключевые слова const и goto фактически не применяются в языке
Java, хотя они и зарезервированы. А у ключевого слова interface имеется
дополнительная форма @interface, применяемая при определении типов,
называемых аннотациями. Некоторые из ключевых слов (в частности, final
и defaul t) имеют самое разное смысловое значение в зависимости от кон­
текста.

Идентификаторы
Идентификатор - это имя, присваиваемое некоторой части програм­
мы на Java, например, классу, методу в классе или переменной, объявляемой

Sб Глава 2. Основы синтаксиса Java


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

лар США($), английский фунт стерлингов (f.) или японская иены (У).
Знак подчеркивания (_) в коде ASCII также заслуживает особого упомина­
ния. Первоначально знак подчеркивания можно было свободно употреблять
в качестве идентификатора или отдельной его части. Но в последних верси­
ях Java, включая и Java 11, знак подчеркивания нельзя применять в качестве
идентификатора.
И хотя знак подчеркивания может появляться в качестве идентификато­
ра в унаследованном коде программ на Java, он больше не допускается как
полноценный идентификатор. Это сделано для поддержки ожидаемого в бу­
дущем языкового средства, в котором знак подчеркивания приобретет новое
специальное синтаксическое значение.

Знаки денежных единиц предназначены для применения в авто­


матически генерируемом исходном коде, например, в коде, про­

изводимом утилитой j avac. Избегая употребления знаков де­


нежных единиц в своих идентификаторах, вы можете избавить
себя от конфликтов с автоматически генерируемыми идентифи­
каторами.

Идентификаторы, обозначающие имена переменных, обычно принято


указывать в смешанном написании. Это означает, что первой в имени пере­
менной должна быть строчная буква, а остальные слова в нем должны на­
чинаться с прописной буквы. Формально символы, допустимые в нача­
ле и в средине идентификатора, определяются с помощью методов isJava
Identif ierStart () и isJavaidentif ierPart () из класса j ava. lang.
Character.
Ниже приведены примеры допустимых идентификаторов.

I xl theCurrentTime current 11
Обратите внимание на особый пример идентификатора 8 в кодировке
UTF-8. Этот символ японского алфавита, называемый "выдрой", вполне до­
пустим в качестве идентификатора в Java. Идентификаторы, состоящие из
символов в другом коде, кроме ASCII, редко употребляются в программах на

Лексическая структура 57
Java, написанных в основном западными программистами, хотя иногда они

все же встречаются.

Литералы
Литералы являются исходными символами, непосредственно представля­

ющими постоянные значения, появляющиеся как они есть в исходном коде

Java. К их числу относятся целые и дробные числа с плавающей точкой, от­


дельные символы в одиночных кавычках, символьные строки в двойных ка­

вычках, а также зарезервированные слова true, false и null. Так, все при­
веденные ниже примеры символов и их последовательностей являются лите­

ралами.

1 1.0 '1' lL"one"true false null

Более подробно синтаксис для выражения числовых, символьных и строко­


вых литералов рассматривается далее, в разделе "Примитивные типы данных':

Знаки препинания
В языке Java употребляется также целый ряд знаков препинания в каче­
стве лексем. В спецификации на язык Java эти знаки разделяются (в какой-то
степени произвольно) на две категории разделителей и операций. Ниже пере­
числены 12 разделителей.

( ) [ ]

А для обозначения операций служат следующие знаки:

+ - * / % & л « » >»
+= -= *= /= %= &= 1= л= <<= ))= >>>=
== '= < <= > >=
~ && 1 1 ++ -- ? ->

Разделители будут еще не раз встречаться в тексте этой книги, а каждая


операция описывается далее, в разделе "Выражения и операции':

Примитивные типы данных


В языке Java поддерживаются восемь основных типов данных, называемых

примитивными типами (табл. 2.1). К примитивным типам данных относятся

58 Гnава 2. Основы синтаксиса Java


логический (или булев) тип, символьный тип, четыре целочисленных типа и
два числовых типа с плавающей точкой, отличающиеся количеством битов,
которые их представляют, а следовательно, допустимым диапазоном пред­

ставления чисел.

Таблица 2.1. Примитивные типы данных в Java


Тип Содержимое Значение Раз­ Диапазон
по умоn- ряд-
чанию ность

Ьoolean true или false false 1 бит Отсутствует


char Символ в Юникоде \uOOOO 16 бит \uOOOO-uFFFF
byte Целое число со знаком о 8 бит -128 -128
short Целое число со знаком о 16 бит -32768 - 32767
int Целое число со знаком о 32 бита -2147483648 - 2147483647
lonq Целое число со знаком о 64 бита -9223372036854775808 -
9223372036854775807
float Дробное число с плава- о.о 32 бита 1. 4Е-45 - 3. 4028235Е+38
ющей точкой по стан-
дарту IEEE 754
douЫe Дробное число с плава- о.о 64 бита 4. 9Е-324 -
ющей точкой по стан- 1.7976931348623157Е+З08
дарту IEEE 754

В последующих разделах кратко описаны все примитивные типы данных


в языке Java. Помимо примитивных типов, в Java поддерживаются неприми­
тивные типы данных, иначе называемые ссылочными типами и представлен­

ные в разделе "Ссылочные типы данных':

Тип boolean
Этот тип данных представляет логические значения. Он может принимать
лишь одно из двух возможных значений, обозначающих два логических со­
стояния: "включено" или "выключено'', "да" или "нет'', "истина" или "ложь".
Для представления этих логических значений в Java зарезервированы два
ключевых слова true (истина) и false (ложь).
Программистам, переходящим на Java из других языков (особенно
JavaScript и С), следует, иметь в виду, что в Java дается намного более строгое
определение логических значений, чем в других языках программирования.
В частности, тип boolean не является ни целочисленным, ни объектным и
не допускает применение несовместимых с ним значений. Иными словами,

Примитивные типы данных 59


в Java нельзя употреблять такие сокращения, как в приведенном ниже фраг­
менте кода.

Object о= new Object();


int i = 1;

if (о) (
while(i)
// ...

Вместо этого в Java приходится писать более понятный код, явно указы­
вая, что именно требуется сравнивать:

if (о != null) (
while (i != 0) (
// ...

Тип char
Этот тип данных представляет символы в Юникоде. В языке Java принят
особый подход к представлению символов. В частности, утилита j avac при­
нимает идентификаторы и литералы в кодировке UTF-8 (т.е. в кодировке
переменной длины) как входные данные. Но в Java символы представлены
в кодировке фиксированной длины: 16-разрядной (до версии Java 9) или
8-разрядной (начиная с версии Java 9), применяемой по стандарту IS0-8859-1
в западноевропейских языках и называемой также Latin-1, если такая воз­
можность имеется.

Такое отличие внешнего представления символов от внутреннего обычно


не доставляет никаких хлопот разработчикам. Для этого, как правило, доста­
точно запомнить следующее правило: чтобы включить символьный литерал
в программу на Java, его следует заключить в одиночные кавычки (в виде
апострофов), как показано ниже.

char с= 'А';

Любым символом в Юникоде можно, конечно, воспользоваться в каче­


стве символьного литерала или указать его непосредственно в виде управ­

ляющей последовательности символов \u в Юникоде. Кроме того, в Java под­


держивается целый ряд других управляющих последовательностей символов,

60 Гnава 2. Основы синтаксиса Java


упрощающих как представление наиболее употребительных непечатных сим­
волов в коде ASCII (например, newline для обозначения новой строки), а
также знаков препинания, имеющих специальное значение в Java. Например:

char tab = '\t', nul = '\000', aleph = '\u05DO',


slash = '\ \';

В табл. 2.2 перечислены управляющие символы, которые можно употреб­


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

Таблица 2.2. Управляющие символы в Java


Управляющая Символьное значение
последователь­

ность символов

\Ь Возврат на один символ назад


\t Горизонтальная табуляция
\n Перевод строки
\f Прогон страницы
\r Возврат каретки

\" Двойные кавычки

\' Одиночные кавычки

\\ Обратная косая черта


\ххх Символ из набора Latin-1 с кодировкой ххх, где ххх - восьмерич­
ное (по основанию 8) число в пределах от ООО до 377. Допускаются
также формы х и \.юс, как, например, \О, хотя они и не рекоменду­
ются, поскольку способны вызвать осложнения в строковых констан­
тах, где после управляющей последовательности следует обычная
цифра. Вместо этой формы лучше пользоваться формой \uxxxx
\uxxxx Символ в Юникоде с кодировкой хххх, где хххх - 4-разрядное
шестнадцатеричное число. Управляющие символы в Юникоде могут
присутствовать повсюду в программе на Java, а не только в символь­
ных и строковых литералах

Значения типа char могут быть преобразованы в значения различных


целочисленных типов, и наоборот, поскольку они относятся к 16-разряд­
ному целочисленному типу данных. Но, в отличие от целочисленных типов
byte, short, int и
long, тип char является беззнаковым. Для оперирования
символами в классе Character определен целый ряд полезных статических

(static) методов, в том числе методы isDigi t (), isJavaLetter (), isLower
Case () и toUpperCase ().

Примитивные типы данных 61


Язык Java и его тип данных cha r изначально разрабатывались с учетом
Юникода. Но стандарт на Юникод постепенно развивается, и поэтому в
каждой новой версии Java принимается новая версия этого стандарта. Так,
в версии Java 7 принят стандарт Unicode 6.0, а в версии Java 8 - стандарт
Unicode 6.2.
В последние выпуски стандарта на Юникод включены символы, кодиров­
ки или кодовые точки которые не вписываются в 16 бит. Эти дополнитель­
ные символы, к числу которых зачастую относятся китайские иероглифы,
занимают 21 бит, и поэтому они не могут быть представлены одним значе­
нием типа char. Вместо этого для хранения кодовой точки дополнительного
символа придется воспользоваться значением типа int или закодировать ее в
так называемую "суррогатную пару", состоящую из двух значений типа char.
Вам вряд ли придется иметь дело с дополнительными символами, если

только вы не пишете прикладные программы, локализуемые на азиатских

языках. Если же вы предполагаете обрабатывать символы, не вписывающиеся


в тип char, воспользуйтесь методами из классов Character, String и дру­

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


пользованием кодовых точек типа int.
Строковые литералы

Помимо типа char, в Java имеется также тип данных String для обработ­
ки символьных и текстовых строк, обычно называемых просто строками. Но
тип String относится к ссылочным типам, представленным классами, а не к

примитивным типам языка Java. А поскольку строки широко применяются


в программировании, то в Java предусмотрен синтаксис для включения
строковых значений в исходный текст программы. Строковый литерал типа
String состоит из произвольного текста, заключенного в двойные кавычки,
в отличие от символьного литерала типа char, заключаемого в одиночные

кавычки. Например:

"Hello World"
"'This' is а string!"

Строковые литералы могут состоять из любых управляющих последова­


тельностей символов, которые, в свою очередь, могут быть представлены в
виде символьных литералов типа (см. табл.
2.2). Чтобы включить двой­
char
ные кавычки в состав строкового литерала типа String, достаточно восполь­

зоваться последовательностью символов \ ". В связи с тем что тип String яв­
ляется ссылочным, строковые литералы более подробно описываются далее,

62 Глава 2. Основы синтаксиса Java


в разделе "Объектные литералы': А о том, как оперировать объектами типа
String в Java, речь пойдет в главе 9.

Целочисленные типы данных


К целочисленным в Java относятся типы byte, short, int и long. Как сле­
дует из табл. 2.1, эти четыре типа данных отличаются лишь своей разряд­
ностью (т.е. количеством битов), а следовательно, допустимым диапазоном
представления чисел. Все целочисленные типы представляют целые числа со
знаком, а ключевое слово unsigned, которое обозначало бы целочисленный
тип данных без знака, как в С и С++, в языке Java отсутствует.
Литералы каждого из этих типов данных обозначаются, как и следовало
ожидать, в виде последовательности десятичных цифр, дополнительно пред­
варяемой знаком "минус': 1 Ниже приведены некоторые примеры допустимых
целочисленных литералов.

о
1
123
-42000

Целочисленные литералы являются 32-разрядными значениями и поэтому


интерпретируются в Java как относящиеся к типу int, если только они не
оканчиваются символом L или 1. В последнем случае целочисленные литера­
лы являются 64-разрядными значениями и поэтому интерпретируются в Java
как относящиеся к типу long, что и демонстрируется в приведенных ниже

примерах.

1234 // значение типа iпt


12341 // значение типа loпg
OxffL //еще одно значение типа long

Целочисленные литералы могут быть также выражены в шестнадцатерич­


ной, двоичной или восьмеричной форме записи. Так, литерал, начинающийся
с символов Ох или ОХ, интерпретируется как шестнадцатеричное число (по
основанию 16), где дополнительные цифры, кроме 0-9, обозначаются буква­
ми A-F (или a-f).
Целочисленные двоичные литералы начинаются с символов ОЬ и могут
содержать только единицы и нули. А поскольку двоичные литералы могут

1 Формально знак "минус" обозначает операцию над литералом и нс является частью самого
литерала.

Примитивные типы данных 63


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

В языке Java поддерживаются также целочисленные восьмеричные лите­

ралы (по основанию 8). Такие литералы начинаются с символа О и не могут


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

Oxff 11 десятичное число 255, выраженное

11 в шестнадцатеричной форме

0377 11 то же самое число, выраженное в

11 восьмеричной форме(по основанию 8)


ОЬ0010 1111 11 десятичное число 47, выраженное

11 в двоичной форме
OxCAFEBABE 11 волшебное число (системный код),

11 обозначающее файлы классов Java

Целочисленная арифметика в Java вообще не приводит к переполнению


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

byte Ы = 127, Ь2 = 1; // наибольшее байтовое значение


11 равно 127
byte sum = (byte) (Ьl + Ь2); // сумма сворачивается в
// наименьшее байтовое значение -128

А ниже демонстрируется пример антипереполнения.

byte Ь3 = -128, Ь4 = 5; 11 наименьшее байтовое значение

11 равно -128
byte sum2 = (byte) (Ь3 - Ь4); 11 сумма сворачивается в

11 большое байтовое значение 123

Ни компилятор, ни интерпретатор Java никоим образом не предупреж­


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

64 Гnава 2. Основы синтаксиса Java


целочисленное деление нацело или с остатком на нуль приводит к генериро­

ванию исключения типа ArithmeticException.


Каждому целочисленному типу данных соответствует класс-оболочка
Byte, Short, Integer и Long. В каждом из этих классов определены кон­
станты MIN_ VALUE и МAX _ VALUE, обозначающие допустимый диапазон пред­
ставления чисел для соответствующего типа данных. В этих классах опреде­
лены также статические методы, например Byte. parseByte () и Integer.
parseint (), предназначенные для преобразования строковых значений в
целочисленные.

Числовые типы с плавающей точкой


Вещественные числа в Java представлены типами данных float и douЫe.
Как следует из табл. 2.1, тип float является 32-разрядным и представляет
числовое значение с плавающей точкой одинарной точности, а тип douЫe -
64-разрядным и представляет числовое значение с плавающей точкой двой­

ной точности. Оба эти типа данных соответствуют стандарту IEEE 754-1985,
где определяются оба формата чисел с плавающей точкой и поведение их
арифметики.
Числовые значения с плавающей точкой могут быть включены буквально
в исходный текст программы на Java в виде необязательного ряда цифр, деся­
тичной точки и последующего за ней ряда цифр. Ниже приведены некоторые
примеры числовых значений с плавающей точкой.

123.45
о.о
.01

В числовых литералах с плавающей точкой может использоваться экспо­

ненциальное представление, где после одного числа следует буква е или Е,


обозначающая экспоненту (т.е. показатель степени), и еще одно число. Это
второе число представляет степень числа 10, на которую умножается первое
число. Например:

1.2345Е02 // 1.2345 * 10л2 или 123.45


le-6 // 1 * ~ол-6 или 0.000001
6.02е23 //число Авогадро: 6.02 * 1ол23

Числовые литералы с плавающей точкой являются по умолчанию значени­


ями типа douЫe. Чтобы включить числовое значение типа float в исходный
текст программы на Java, после заданного числа следует указать символ f или

Примитивные типы данных 65


F, как показано ниже. Числовые литералы с плавающей точкой нельзя выра­
зить в шестнадцатеричной, двоичной или восьмеричной форме записи.
douЫe d = 6.02Е23;
float f = 6.02e23f;

Точность представления чисел


в форме с плавающей точкой
Большинство вещественных чисел по своему характеру не могут быть
точно представлены конечным числом битов. Поэтому очень важно
помнить, что числовые значения типа f1oat и double лишь прибли­
зительно представляют указанные в них числа. Так, тип f1oa t служит
приблизительным 32-разрядным представлением вещественных чисел с
точностью, по крайней мере, до шести значащих цифр, а тип double -
приблизительным 64-разрядным представлением с точностью, по край­
ней мере, до пятнадцати значащих цифр. Более подробно о точности
представления вещественных чисел в форме с плавающей точкой речь
пойдет в главе 9.

Помимо простых чисел, с помощью типов данных float и douЫe мо­


гут быть представлены четыре специальных значения: положительная и от­
рицательная бесконечность, нуль и NaN (не число). Бесконечные значения
получаются в результате вычислений с плавающей точкой, приводящих к
переполнению допустимого диапазона представления чисел для типа дан­

ных float или douЫe. Если же вычисление с плавающей точкой приводит


к антипереполнению допустимого диапазона представления чисел для типа

данных float или douЬle, то в результате получается нулевое значение.

Рассмотрим в качестве примера неоднократное деление значения


1. О типа douЫe на значение 2. О того же самого типа в цикле
while. Сколько бы ни делить одно из этих чисел на другое, в ма­
тематическом представлении результат такого деления никогда

не достигнет нуля. А в представлении с плавающей точкой после


достаточного количества повторяющихся шагов деления резуль­

тат окажется настолько малым, что его нельзя будет отличить


от нуля.

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

положительным и отрицательным нулем в зависимости от направления,

66 Глава 2. Основы синтаксиса Java


в котором произошло антипереполнение. Но положительный и отрицатель­
ный нули ведут себя практически одинаково. Наконец, специальное значение
с плавающей точкой NaN обозначает "не число". Значение NaN получается
в результате выполнения недопустимой операции с плавающей точкой (на­
пример, О. 0/0. О). Ниже приведены примеры операторов, выполнение кото­
рых приводит к появлению описанных выше специальных значений.

douЫe inf = 1.0/0.0; 11 положительная бесконечность

douЫe neginf = -1.0/0.0; 11 отрицательная бесконечность

douЫe negzero = -1.0/inf; 11 отрицательный нуль

douЫe NaN = О.О/О.О; 11 не число

Примитивным типам float и douЫe соответствуют классы-оболочки


Float и DouЫe. В каждом из этих классов определены константы MIN _ VALUE,
МAX _ VALUE, NEGATIVE INFINITY, POSITIVE INFINITY и NaN, обозначающие
минимально допустимое значение, максимально допустимое значение, поло­

жительную бесконечность, отрицательную бесконечность и не число соответ­


ственно.

Типы данных с плавающей точкой в Java способны справиться с


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

ние квадратного корня из отрицательного числа.

Бесконечные значения с плавающей точкой ведут себя так, как и предпо­


лагалось. Например, в результате сложения или вычитания любого бесконеч­
ного значения из другого аналогичного значения получается бесконечность.
Отрицательный нуль ведет себя практически так же, как и положительный,
поэтому операция равенства == фактически сообщает, что отрицательный
нуль равен положительному нулю. Чтобы отличить отрицательный нуль от
положительного (т.е. обычного), достатоЧно выполнить деление на него. Так,
в результате операции 1 . О/ О. О получается положительная бесконечность,
а в результате операции 1 . О/ -0 . О - отрицательная бесконечность. Наконец,
операция равенства == сообщает, что значение NaN не является числом и не
равно ни одному из чисел, включая и само это значение! Чтобы проверить,
является ли значение типа float или douЫe равным NaN, следует вызвать
методы Float. isNaN () и DouЫe. isNaN ().

Примитивные типы данных 67


Взаимные преобразования примитивных типов данных
В языке Java допускаются взаимные преобразования целочисленных и чис­
ловых значений с плавающей точкой. А поскольку каждый символ соответ­
ствует числу в кодировке Юникода, то вполне допустимо взаимное преобра­
зование символьных значений типа char и целочисленных и числовых зна­
чений с плавающей точкой. В действительности примитивный тип boolean
является единственным, не допускающим взаимного преобразования в дру­
гие примитивные типы данных в Java.
Имеются два основных типа преобразований примитивных типов данных.
В частности, расширяющее преобразование происходит в том случае, если зна­
чение одного типа преобразуется в значение более широкого типа, имеющего
больший допустимый диапазон представления чисел. Например, расширяю­
щие преобразования выполняются в Java автоматически, если литерал типа
int присваивается переменной типа douЬle, а литерал типа char - пере­

менной типа int.


Но совсем другое дело - сужающие преобразования. В частности, сужа­
ющее преобразование происходит в том случае, если значение одного типа
преобразуется в значение более узкого типа. Сужающие преобразования не
всегда оказываются надежными. Так, преобразование целочисленного значе­
ния 13 в байтовое вполне обосновано, чего нельзя сказать о преобразовании
целочисленного значения 13 О О О в соответствующее байтовое значение, по­
скольку допустимый диапазон представления чисел для типа byte составля­

ет от -128 до 127. В результате сужающего преобразования можно потерять


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

в приведенном ниже примере кода.

int i = 13;
/ / byte Ь = i; //несовместимые типы: возможна потеря
//данных в результате преобразования
//из типа int в тип byte

Единственным исключением из этого правила является возможность

присвоить целочисленный литерал (значение типа int) переменной типа


byte или short, если данный литерал вписывается в допустимый диапазон

68 Глава 2. Основы синтаксиса Java


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

терный тому пример.

byte Ь = 13;

Если вам требуется выполнить сужающее преобразование и вы уверены,


что не потеряете при этом данные или точность их представления, можете

принудительно выполнить такое преобразование в Java с помощью языкового


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

int i = 13;
byte Ь = (byte) i; //принудительно преобразовать
//тип int в тип byte
i = (int) 13.456; // принудительно привести этот литерал
//типа douЫe к значению 13 типа int

Приведение примитивных типов данных чаще всего применяется для пре­


образования числовых значений с плавающей точкой в целочисленные зна­
чения. При этом дробная часть числового значения с плавающей точкой про­
сто отбрасывается (т.е. числовое значение с плавающей точкой округляется
до нуля, а не до ближайшего целого числа). Другие виды округления чисел
выполняются с помощью статических методов Math. round (), Math. floor ()
иMath.ceil().

Тип
cha r зачастую действует как целочисленный, и поэтому значением
типа char можно пользоваться везде, где требуется значение типа int или
long. Не следует, однако, забывать, что тип char является беззнаковым, а сле­
довательно, он ведет себя иначе, чем тип short, несмотря на то, что оба этих
типа данных являются 16-разрядными, как демонстрируется в следующем

фрагменте кода:

short s = (short) Oxffff; 11 эти биты представляют число -1 те же

char с '\uffff'; 11 самые биты, обозначающие символ в Юникоде

int il = s; 11 в результате преобразования

11 типа short в тип int получается

11 значение -1
int i2 с; 11 в результате преобразования типа char
11 в тип int получается значение 65535

В табл. 2.3 показано, какие примитивные типы данных могут быть пре­
образованы в другие примитивные типы и каким образом выполняет­
ся их преобразование. Буква "Н" в табл. 2.3 указывает на невозможность

Примитивные типы данных 69


преобразования; буквой "Р" обозначается расширяющее преобразование, ко­
торое выполняется в Java автоматически и неявно; а буквой "С'' - сужающее
преобразование, требующее явного приведения типов.

Таблица 2.3. Взаимные преобразования примитивных типов в Java


Целевые типы
Исходный тип boolean byte short char int long float douЫe

boolean н н н н н н н
byte н р с р р р р

short н с с р р р р

char н с с р р р р

int н с с с р Р* р

long н с с с с Р* Р*

float н с с с с с р

douЫe с с с с с с с

Наконец, "Р*" в табл. 2.3 означает, что преобразование является автома-


тически расширяемым, но при таком преобразовании некоторые младшие
разряды значения могут быть потеряны. Это может, например, произойти,
когда значение типа int или long преобразуется в значение числового типа
с плавающей точкой (подробнее см. в табл. 2.3). Ведь допустимый диапазон
представления чисел для типов с плавающей точкой больше, чем для типа
int или long, и поэтому значения обоих этих типов могут быть представле­
ны типом float или douЫe. Тем не менее типы данных с плавающей точкой

представляют числа приближенно и не всегда могут иметь столько же знача­


щих цифр, сколько у целочисленных типов данных (дополнительные сведе­
ния о числах с плавающей точкой приведены в главе 9).

Выражения и операции
До сих пор в этой главе рассматривались примитивные типы данных, ко­

торыми можно манипулировать в программах на Java, а также показывалось,

как включать значения примитивных типов в виде литералов в исходный

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

значений. Эти литералы и переменные являются лексемами, из которых со­

ставляются программы на Java.

70 Глава 2. Основы синтаксиса Java


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

1.7 //числовой литерал с плавающей точкой


true //логический литерал
sum //переменная

Когда интерпретатор Java вычисляет литеральное выражение, результи­


рующим оказывается значение самого литерала. А когда интерпретатор Java
вычисляет переменное выражение, результирующим оказывается значение,

хранящееся в переменной.

Первичные выражения не представляют особого интереса. Более сложные


выражения состоят из операций, объединяющих первичные выражения. Так,
в приведенном ниже примере два первичных выражения, содержащих пере­

менную и числовой литерал с плавающей точкой, объединяются в выражение


присваивания с помощью операции присваивания.

sum = 1.7

Операции применяются не только для объединения первичных выраже­


ний, но и для составления выражений любого уровня сложности. Так, все
приведенные ниже примеры выражений являются вполне допустимыми.

sum = 1 + 2 + 3 * 1.2 + (4 + 8)/3.0


sum/Math.sqrt(3.0 * 1.234)
(int) (sum + 33)

Краткая сводка операций


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

Предшествование

Предшествование каждой операции указано в столбце "П" табл. 2.4 и обо­


значает порядок, в котором выполняются операции. Так, операции с более

Выражения и операции 71
высоким предшествованием выполняются прежде операций с более низким
предшествованием. Рассмотрим в качестве примера следующее выражение:

а + Ь * с

Операция умножения обладает более высоким предшествованием, чем


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

правил элементарной математики. Предшествование можно рассматривать


как меру, определяющую, насколько тесно операции привязаны к их операн­

дам. Чем больше число, обозначающее степень предшествования операций,


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

(а + Ь) * с

Устанавливаемое по умолчанию предшествование операций выбрано в


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

11 приведение к типу класса в сочетании


11 с доступом к его членам:
( (Integer) о) .intValue();

11 присваивание в сочетании со сравнением:


while ( ( line = in. readLine () ) 1 = null) { ...

11 поразрядные операции в сочетании со сравнением:


if ( (flags & (PUBLIC 1 PROTECTED)) != 0) { ... }

Ассоциативность

Ассоциативность - это свойство операций, определяющее порядок вы­


числения выражений, который в противном случае окажется неоднозначным.

Это особенно важно в том случае, если выражение состоит из нескольких


операций с одинаковым предшествованием.

72 Глава 2. Основы синтаксиса Java


Большинство операций ассоциативны слева направо, а это означает, что
они выполняются слева направо. Но операции присваивания и унарные опе­
рации обладают ассоциативностью справа налево. Ассоциативность каждой
операции или группы операций указана в столбце ''Х' табл. 2.4, где буквами
"П" - "П" о б означается ассоциативность слева направо, а б уквами "П'' -"Л" -
ассоциативность справа налево.

Все аддитивные операции ассоциативны слева направо, и поэтому выра­


жение а+Ь-с вычисляется слева направо: (а+Ь) -с. А операции присваива­

ния и унарные операции выполняются справа налево. Рассмотрим в качестве


примера следующее сложное выражение:

а = Ь += с = -~d

Это выражение вычисляется в следующем порядке:

а= (Ь += (с= -(~d) ))

Как и предшествование, ассоциативность операций устанавливает поря­

док вычисления конкретного выражения по умолчанию. Такой порядок по


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

Сводная таблица операций в Java


В табл. 2.4 сведены все операции, доступные в Java. В столбцах "П" и ''Х'
данной таблицы указаны предшествование и ассоциативность каждой груп­
пы связанных вместе операций соответственно. Пользуйтесь данной табли­
цей для получения оперативной справки по операциям (и особенно их пред­
шествованию), когда в этом возникнет потребность.

Таблица 2.4. Операции в Java


п А Операция Типы операндов Выполнение

16 Л-П объект, член Доступ к члену объекта


[ ] массив, значение типа int Доступ к элементу массива
( арrунен'~'Ы) метод, список аргументов Вызов метода
++,-- переменная Постфиксный инкремент,
декремент

15 П-Л ++, -- переменная Префиксный инкремент,


декремент

+, - число Унарный плюс, унарный минус

Выражения и операции 73
Продолжение табл. 2.4
п А Операция Типы операндов Выпоnнение

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

логическое значение Логическая операция "НЕ"

14 п-л new класс, список аргументов Создание объекта


(!!'JШ) тип, любое значение Приведение (преобразование)
типов

13 л-п *, /, % число, число Умножение, деление, остаток


от деления

12 л-п +' - число, число Сложение, вычитание

+ строка, любое значение Сцепление строк


11 л-п << целое значение, целое Сдвиг влево
значение

>> целое значение, целое Сдвиг вправо с расширением


значение знака

>>> целое значение, целое Сдвиг вправо с дополнением


значение нулями

10 л-п <,<= ЧИСЛО, ЧИСЛО Меньше, меньше или равно

>,>= число, число Больше, больше или равно


instanceof ссылка, тип Сравнение типов

9 л-п == примитивный тип, примитив- Равно (наличие одинаковых


ный тип значений)

!= примитивный тип, примитив- Не равно (наличие разных


ный тип значений)

-- ссылка, ссылка Равно (ссылка на один и тот


же объект)

!= ссылка, ссылка Не равно (ссылка на разные


объекты)

8 Л-П & целое значение, целое Поразрядное "И"


значение

& логическое значение, Логическая операция "И"


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

7 л-п " целое значение, целое Поразрядное "Исключающее


значение ИЛИ"

" логическое значение, Логическая операция "Исклю-


логическое значение чающее ИЛИ"

6 л-п целое значение, целое Поразрядное "ИЛИ"


значение

логическое значение, Логическая операция "ИЛИ"


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

74 Глава 2. Основы синтаксиса Java


Окончание табл. 2.4
п А Операция Типы операндов Выполнение

5 Л-П && логическое значение, Условная логическая


логическое значение операция "И"

4 Л-П 11 логическое значение, Условная логическая


логическое значение операция "ИЛИ"

3 П-Л ? логическое значение, любое Условная (тернарная)


значение операция

2 П-Л = переменная, любое значение Присваивание

*=, /=, %=, переменная, любое значение Присваивание с соответствую­


щей операцией

+=, -=, <<=,


>>=, >>>=,
&=,л=, 1=

п-л ~ список аргументов, тело Лямбда-выражение


метода

Количество и тип операндов

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


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

-n //операция унарного минуса

Но большинство операций являются бинарными (двухместными) и выпол­


няются над двумя операндами. Хотя операция - на самом деле принимает
обе формы: унарную и бинарную.

а - Ь // вычитание является бинарной операцией

В языке Java определяется также тернарная (трехместная) операция, за­


частую называемая условной. Она действует подобно условному оператору
i f в пределах выражения. Три ее операнда разделяются знаком вопроса и
двоеточием, а второй и третий операнды должны быть приведены к одному
и тому же типу.

х >у? х : у// тернарное выражение для вычисления


//большего значения переменных х и у

Выражения и операции 75
Помимо определенного количества аргументов, в каждой операции пред­

полагаются конкретные типы операндов. В четвертом столбце табл. 2.4 пере­


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

Число
Целочисленное значение, числовое значение с плавающей точкой или

символьное значение (т.е. любое значение примитивного типа, кроме


boolean). Автораспаковка (см. далее раздел "Упаковочные и распако­
вочные преобразования") означает, что в данном контексте вместо этих
примитивных типов могут быть также использованы объекты соответ­
ствующих классов-оболочек (например, Character, Integer и ОоuЫе).

Целое значение
Это значение типа byte, short, int, long или char, хотя значения типа
long не допускаются в операции [ ] , предназначенной для доступа
к элементам массива. Благодаря автораспаковке вместо целочисленных

значений этих примитивных типов допускается также указывать объ­


екты соответствующих классов-оболочек: Byte, Short, Integer, Long,
и Character.
Ссылка
Объект или массив.

Переменная
Переменная или все, что допускает присваивание значения (например,
элемент массива).

Возвращаемый тип

Подобно тому, как в каждой операции предполагаются ее операнды кон­


кретного типа, в результате выполнения каждой операции получается значе­

ние конкретного типа. Так, арифметические, поразрядные логические опера­


ции, а также операции инкремента, декремента и сдвига возвращают значе­

ние типа douЬle, если, по крайней мере, один из операндов относится к типу

douЫe. Если же хотя бы один из операндов этих операций относится к типу


float, то в результате их выполнения возвращается значение типа float.
А если хотя бы один из операндов этих операций относится к типу long, то
в результате их выполнения возвращается значение типа long. В противном
случае возвращается значение типа int, даже если оба операнда относятся
к типам byte, short или char, которые уже типа int.

76 Глава 2. Основы синтаксиса Java


Операции сравнения, равенства и логические операции всегда возвраща­

ют логические значения. Каждая операция присваивания возвращает присво­


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

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

Например, у операции инкремента++ имеется побочный эффект инкре­


ментирования переменной. Так, в выражении ++а инкрементируется значе­
ние переменной а и возвращается вновь инкрементированное значение. Если
же вычислить это выражение снова, то возвращаемое значение окажется

иным. Побочные эффекты имеются и у различных операций присваивания.


Например, выражение а*=2 может быть также написано как а=а*2. Значение
данного выражения равно значению переменной а, умноженному на 2, но это

выражение имеет побочный эффект сохранения своего значения обратно


в переменной а.

Операция ( ) , предназначенная для вызова метода, также имеет побочные


эффекты, если они возникают в вызываемом методе. Некоторые методы, на­
пример Math. sqrt (),просто вычисляют и возвращают значение без всяких
побочных эффектов. Но, как правило, у методов имеются побочные эффек­
ты. Наконец, операция new имеет значительный побочный эффект создания
нового объекта.

Порядок вычисления

Когда интерпретатор Java вычисляет выражение, он выполняет различные


операции в том порядке, на который указывают круглые скобки в выраже­
нии, а также предшествование и ассоциативность операций. Но прежде чем
выполнить любую операцию, интерпретатор Java вычисляет ее операнды.
(Исключение из этого правила составляют операции &&, 1 1 и ? : , где далеко
не всегда вычисляются все операнды.) Интерпретатор Java всегда вычисляет
операнды слева направо, и это имеет значение, если операндами являются

Выражения и операции 77
выражения, содержащие побочные эффекты. Рассмотрим в качестве примера
следующий фрагмент кода:

int а 2;
int v = ++а + ++а * ++а;

Несмотря на то что умножение должно выполняться прежде сложения,


в данном примере сначала вычисляются операнды операции +. Но посколь­
ку в качестве обоих операндов данной операции указано выражение ++а, то
сначала вычисляются их значения 3 и 4 соответственно, а следовательно, все
выражение вычисляется в следующем порядке: 3 + 4 * 5, что в итоге дает зна­
чение 2 3, присваиваемое переменной v.

Арифметические операции
Такие операции могут выполняться над целыми числами, числами с плава­

ющей точкой и даже символами (т.е. над всеми примитивными типами, кро­
ме boolean). Если любой из операндов является числом с плавающей точкой,
то применяется арифметическая операция с плавающей точкой, а иначе -
целочисленная арифметическая операция. И это имеет значение, поскольку
целочисленная арифметика отличается от арифметики с плавающей точкой,
в частности, выполнением операции деления и порядком обработки ситуа­
ций переполнения и антипереполнения. Ниже перечислены арифметические
операции, доступные в Java.
Сложение (+)
Операция + выполняет сложение двух чисел. Как будет показано ниже,
операция + может быть использована для сцепления строк. Если лю­
бым из операндов операции + является строка, то и другой операнд
преобразуется в строку. Если же требуется соединить сложение со сце­
плением, воспользуйтесь круглыми скобками. Например:

System.out.println("Total: "+ З + 4); //выводит строку


// "Total: 34", а не число 7 1

Вычитание(-)
Если операция - применяется как бинарная, она вычитает свой вто­
рой операнд из первого. Например, вычисление выражения 7- 3 дает
в итоге значение 4. С помощью операции - можно также выполнить
унарное отрицание.

78 Глава 2. Основы синтаксиса Java


Умножение (*)
Операция * перемножает оба свои операнда. Например, вычисление
выражения 7 * 3 дает в итоге значение 21.
Деление(/)
Операция / делит свой первый операнд на второй. Если оба ее операнда
являются целочисленными, то в итоге получается целочисленное значе­

ние, а остаток теряется. Но если любой из операндов данной операции


содержит значение с плавающей точкой, то в результате получается зна­
чение с плавающей точкой. Целочисленное деление на нуль приводит к
генерированию исключения типа Ari thmeticException, тогда как де­
ление на нуль с плавающей точкой дает бесконечный результат или не
число (NaN), как демонстрируется в приведенных ниже примерах.

7/3 // вычисляется значение 2


7/3.0f // вычисляется значение 2.ЗЗЗЗЗЗf
7/0 //генерируется исключение типа ArithmeticException
7/0.0 // вычисляется положительная бесконечность
О.О/О.О // вычисляется не число (NaN)

Деление по модулю(%)
Операция % вычисляет остаток от целочисленного деления по модулю
первого операнда на второй. Например, в результате выполнения опе­
рации 7 %3 получается значение 1, обозначающее остаток от целочис­
ленного деления 7 на 3. Знак получаемого результата остается таким
же, как и у первого операнда. Несмотря на то что операция деления по
модулю обычно выполняется над целочисленными операндами, она
пригодна и для операндов с плавающей точкой. Например, вычисление
выражения 4. 3 % 2 .1 дает в итоге значение О. 1. При попытке вычис­
лить остаток от целочисленного деления по модулю на нуль генерирует­

ся исключение типа ArithmeticException. При делении любого числа


с плавающей точкой по модулю на О. О получается не число (NaN), как,
впрочем, и при делении бесконечного значения по модулю на число с
плавающей точкой.

Унарный минус (-)


Если операция - применяется как унарная, т.е. к единственному опе­
ранду, то она выполняет унарное отрицание. Иными словами, эта опе­
рация преобразует положительное значение в равнозначное отрица­
тельное значение, и наоборот.

Выражения и операции 79
Операция сцепления строк
Помимо сложения чисел, операция+ (и связанная с ней операция+=) вы­
полняет сцепление (т.е. соединение) строк. Так, если любой из операндов опе­
рации + оказывается строкой, то другой операнд автоматически преобразует­
ся в строку. Например:

System.out.println("Quotient: "+ 7/3.0f);


// выводит строку "Quotient: 2.3333333"

Таким образом, объединяя любое выражение в круглых скобках вместе со


строкой в операции сложения, следует проявлять особую внимательность.
В противном случае операция сложения будет интерпретирована как опера­
ция сцепления строк.

Преобразования всех примитивных типов в строковые типы встроены в


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

Операции инкремента и декремента


Операция++ инкрементирует (т.е. увеличивает на 1) значение своего един­
ственного операнда, который должен быть переменной, элементом массива
или полем объекта. Поведение этой операции зависит от ее положения отно­
сительно операнда. Так, если операция инкремента указана перед операндом,
то она называется префиксной, инкрементируя свой операнд и вычисляя его
инкрементированное значение. А если операция инкремента указана после
операнда, то она называется постфиксной, инкрементируя свой операнд, но
вычисляя значение операнда прежде, чем инкрементировать его.

Например, в следующем фрагменте кода устанавливается значение 2в обе­


их переменных i и j:
i = 1;
= ++i;

А в приведенных ниже строках кода в переменной i устанавливается зна­


чение 2, а в переменной j - значение 1.
i = 1;
j = i++;

80 Глава 2. Основы синтаксиса Java


Аналогично операция -- декрементирует (т.е. уменьшает на 1) значение
своего единственного операнда, который должен быть переменной, элемен­
том массива или полем объекта. Подобно операции ++, поведение опера­
ции -- зависит от ее положения относительно операнда. Так, если операция
декремента указана перед операндом, она декрементирует свой операнд и

возвращает его декрементированное значение. А если операция декремента

указана после операнда, то она декрементирует свой операнд, но возвращает


недекрементированное значение.
Выражения х++ и х-- равнозначны выражениям х=х+ 1 и х=х-1 соответ­
ственно, за исключением того, что в них применяются операции инкремента

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


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

a[i++J++; // инкрементирует значение элемента массива

a[i++) = a[i++) + 1; //прибавляет 1 к значению элемента


//массива и сохраняет новое значение
//в другом его элементе

Операции инкремента и декремента чаще всего применяются как в пре­


фиксной, так и в постфиксной форме для положительного или отрицательно­
го приращения счетчика, управляющего циклом.

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

няются в условных операторах if, а также в циклах while и for для приня­

тия решений о ветвлении и циклическом выполнении программы. Например:

if (о ! = null) ... ; / / операция проверки на неравенство


while(i < a.length) ... ;//операция проверки на "меньше"

В языке Java предоставляются следующие операции равенства.

Равно(==)
Операция == вычисляет логическое значение true, если оба ее опе­
ранда равны, а иначе - логическое значение false. Если указаны опе­

ранды примитивных типов данных, то в данной операции проверяется

Выражения и операции 81
равенство значений самих операндов. А если указаны операнды ссы­
лочных типов, то в данной операции проверяется, делаются ли в них

ссылки на один и тот же объект или массив. Иными словами, в данной


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

Если операция== применяется для сравнения двух числовых или сим­


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

равенство с обычным положительным нулевым значением. Кроме того,


специальное значение NaN (не число) не равно ни одному из чисел, в
том числе и себе самому. Чтобы проверить, является ли числовое значе­
ние с плавающей точкой специальным значением NaN, следует вызвать
метод Float. isNan () или DouЫe. isNan ().
Не равно(!=)
Операция ! =совершенно противоположна операции==. Она вычисля­
ет логическое значение true, если оба ее операнда имеют разные зна­
чения примитивного типа или же ссылаются на разные объекты или
массивы, а иначе - логическое значение false.
Операции отношения могут выполняться над числами и символами, но
не над логическими значениями, объектами или массивами, поскольку они
относятся к неупорядоченным типам данных. В языке Java предоставляются

следующие операции отношения.

Меньше(<)
Вычисляет логическое значение true, если первый операнд меньше

второго.

Меньше или равно(<=)


Вычисляет логическое значение true, если первый операнд меньше или

равен второму операнду.

82 Глава 2. Основы синтаксиса Java


Больше(>)
Вычисляет логическое значение true, если первый операнд больше вто­
рого.

Больше или равно(>=)


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

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

Как было показано выше, операции сравнения выполняют сравнение сво­


их операндов, получая логический результат типа boolean, который зачастую

испопьзуется в операторах ветвпения или циклов. Чтобы сдепать решения о


ветвпении или циклическом выполнении программы по заданным условиям

более интересными, чем одиночное сравнение, можно воспользоваться ло­


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

ческие значения. Ниже перечислены логические операции, доступные в Java.


Условная логическая операция "И"(&&)
Выполняет логическую операцию "И" над своими операндами. Вы­
числяет логическое значение true, если и только если истинны оба ее
операнда. Еспи же любой из операндов оказывается ложным, то данная
операция вычисляет логическое значение false. Например:

if (х < 10 && у > 3) .•. 11 если оба сравнения истинны

Эта операция (и все остапьные логические операции, кроме унарной


операции ! ) обладает более низкой степенью предшествования, чем
операции сравнения. Следовательно, писать строки кода, аналогичные
приведенной выше, вполне допустимо. Тем не менее некоторые про­
граммисты предпочитают явно указывать порядок вычисления с помо­

щью круглых скобок, как показано ниже. Но вы вольны выбрать тот


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

if ( (х < 10) && (у > 3)) •••

Данная операция называется условной логической операцией "И" по­


тому, что она вычисляет свой второй операнд по заданному условию.

Если первый операнд оказывается ложным (false), то и все выражение

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

if (data != null && i < data.length && data[i] != -1)

Второе и третье сравнения в данном выражении могут вызвать ошибки,


если первое или второе сравнение окажется ложным. Правда, это не так уж
и важно благодаря условному характеру операции & &.

Условная логическая операция "ИЛИ" ( 1 1)


Выполняет логическую операцию "ИЛИ" над своими операндами типа
boolean. Вычисляет логическое значение true, если истинным окажет­

ся любой или оба операнда. Если же оба операнда окажутся ложны­


ми, то вычисляется логическое значение false. Как и в операции &&,
второй операнд вычисляется в операции 1 1 не всегда. Так, если первый
операнд оказывается истинным (true), то и все выражение оказывает­
ся истинным независимо от значения второго операнда. В таком случае
второй операнд данной операции просто пропускается.

Логическая операция "НЕ" (!)


• Эта унарная операция изменяет логическое значение своего операнда.
Так, если она применяется к логическому значению true, то в итоге вы­
числяется логическое значение false. А если она применяется к логи­

ческому значению false, то в итоге вычисляется логическое значение

true. Данная операция находит полезное применение в таких выраже­


ниях, как приведенное ниже.

i f 1 ! found) ... 11 переменная found объявлена ранее


11 как логическая типа boolean
while ( 1 c.isEmpty()) 11 метод isEmpty() возвращает
11 логическое значение

Операция ! является унарной, а следовательно, обладает высокой сте­


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

if ( ! (Х > у && у > Z))

84 Глава 2. Основы синтаксиса Java


Логическая операция "И" ( &)
Если операция & применяется с операндами типа boolean, то она ведет

себя подобно операции &&, за исключением того, что всегда вычисляет


оба операнда независимо от значения первого операнда. Данная опера­
ция практически всегда применяется как поразрядная к целочисленным

операндам, хотя многие программирующие на Java вообще не признают


ее применение к операндам типа boolean.
Логическая операция "ИЛИ" ( 1)
Выполняет логическую операцию "ИЛИ" над своими операндами типа
boolean. Действует подобно операции 1 1, за исключением того, что
всегда вычисляет оба операнда, даже если первый из них оказывает­
ся истинным (true). Операция 1 практически всегда применяется как
поразрядная к целочисленным операндам и крайне редко к операндам

типа boolean.
Логическая операция "исключающее ИЛИ" (")
Если эта операция выполняется над операндами типа boolean, то она

вычисляет результат применения логической операции "исключающее


ИЛИ" к ее операндам. В частности, она вычисляет логическое значе­
ние true, если хотя бы один из двух ее операндов оказывается истин­
ным. Иными словами, данная операция вычисляет логическое значение
false, если оба ее операнда оказываются ложными (false) или истин­
ными (true). В отличие от операций && и 1 I, данная операция должна
всегда вычислять оба операнда. Операция л чаще всего применяется
как поразрядная к целочисленным операндам. А если ее операнды от­
носятся к типу boolean, то она равнозначна операции ! =.

Поразрядные лоrические операции и операции сдвиrа


Такие операции сдвига относятся к категории низкоуровневых операций,
предназначенных для манипулирования отдельными битами, составляющи­
ми целочисленное значение. Поразрядные логические операции нечасто при­
меняются в современных программах на Java, за исключением низкоуров­
невых операций (например, в сетевом программировании). Они служат для
проверки и установки отдельных битов признаков (или флагов) в конкретном
значении. Чтобы понять принцип действия этих операций, необходимо раз­
бираться в двоичных (по основанию 2) числах и форме их дополнительного
кода для представления отрицательных чисел.

Выражения и операции 85
Поразрядные логические операции нельзя применять к числовым значе­
ниям с плавающей точкой, логическим значениям, массивам или объектам
в качестве операндов. Если операции &, 1и л применяются к операндам типа
boolean, они выполняют совсем другую логическую операцию, как поясня­

лось в предыдущем разделе.

Если оба операнда поразрядной логической операции относятся к типу


long, то и результат данной операции относится к типу long, а иначе -
к типу int. Если же левый операнд логической операции сдвига относится
к типу long, то и результат данной операции относится к типу long, а ина­
че - к типу int. Ниже перечислены доступные в Java логические операции:

как поразрядные, так и сдвига.

Поразрядное дополнение (-)


Унарная операция ~ известна под названием поразрядного дополнения
или поразрядного отрицания. Она обращает каждый бит своего един­
ственного операнда, преобразуя единицы в нули, а нули - в единицы.
Например:

byte Ь = -12; // -00001100 = => 11110011 или


11 -13 в десятичной форме
flags = flags & -f; //сбросить признак f в
//наборе признаков

Поразрядное "И" (&)


Эта операция объединяет два целочисленных операнда, выполняя логи­
ческую операцию "И" над отдельными их битами. В итоге результиру­
ющий бит устанавливается лишь в том случае, если соответствующий
бит установлен в обоих операндах. Например:

10 & 7 // 00001010 & 00000111 = => 00000010 или 2


if ( (flags & f) != 0) // проверить, установлен ли признак f

Если операция & применяется к операндам типа boolean, что бывает


нечасто, то она выполняется как описанная ранее логическая опера­

ция "И':

Поразрядное "ИЛИ" ( 1)
Эта операция объединяет два целочисленных операнда, выполняя логи­
ческую операцию "ИЛИ" над отдельными их битами. В итоге результи­
рующий бит устанавливается в том случае, если соответствующий бит
установлен хотя бы в одном или в обоих операндах. Результирующий

86 Глава 2. Основы синтаксиса Java


бит сбрасывается лишь в том случае, если сброшены соответствующие
биты в обоих операндах. Например:
10 7
1 // 00001010 00000111 = => 00001111
1 или 15
flags = flags 1 f; //установить признак f
Если операция 1 применяется к операндам типа boolean, что бывает
нечасто, то она выполняется как описанная ранее логическая операция

"ИЛИ':

Поразрядное "ИЛИ"(")
Эта операция объединяет два целочисленных операнда, выполняя ло­
гическую операцию "исключающее ИЛИ" над отдельными их битами.
В итоге результирующий бит устанавливается в том случае, если соот­
ветствующие биты в обоих операндах оказываются в разном состоянии.
Результирующий бит сбрасывается лишь в том случае, если сброшены
или установлены соответствующие биты в обоих операндах. Например:

10 л 7 // 00001010 л 00000111 = => 00001101 или 13


Если операция л применяется к операндам типа boolean, что бывает
нечасто, то она выполняется как описанная ранее логическая операция

"исключающее или·:

Сдвиг влево (<<)


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

на 2". Например:

10 << 1 // ОЬ00001010 << 1 00010100 = 20 = 10*2


7 << 3 // ObOOOOOlll << 3 00111000 = 56 = 7*8
-1 << 2 // OxFFFFFFFF << 2 OxFFFFFFFC = -4 = -1*4
// OxFFFF FFFC ==
// OЫlll - 1111 1111 1111 - 1111 1111 - 1111 1100
- - - -

Если левый операнд относится к типу long, то значение правого опе­


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

вого операнда должно быть в пределах от О до 31.


Сдвиг вправо со знаком(>>)
Операция >> сдвигает биты левого операнда вправо на количество по­
зиций, указанных в правом операнде. При этом младшие биты левого

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

10 » 1 11 00001010 >> 1 00000101 5 = 10/2


27 » 3 11 00011011 >> 3 00000011 3 = 27/8
-50 » 2 11 11001110 >> 2 11110011 -13 != -50/4

Если значение левого операнда положительное, а значение правого опе­

ранда равно п, то выполнение операции >> равнозначно целочисленно­


му делению на 2".
Сдвиг вправо без знака (>>>)
Эта операция подобна операции >>, за исключением того, что она всег­
да вдвигает нули в старшие разряды результирующего значения незави­

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


нулями и применяется в том случае, если левый операнд интерпрети­
руется как значение без знака, несмотря на то, что все целочисленные
типы определены в Java со знаком. Например:

Oxff »> 4 11 11111111 >>> 4 00001111 = 15 = 255/16


-50 >» 2 // OxFFFFFFCE >>> 2 = Ox3FFFFFF3 = 1073741811

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

Левая сторона операции присваивания иначе называется лево­


сторонним значением lvalue. В языке Java оно должно ссылать­

ся на некоторую присваиваемую область памяти (т.е. ту, в кото­


рую можно записать данные).

С правой стороны операции присваивания, иначе называемой правосто­


ронним значением rvalue, может находиться значение любого типа, присва­
иваемое переменной. Но важнее то, что выражение имеет побочный эффект

88 Глава 2. Основы синтаксиса Java


фактически выполняемого присваивания, т.е. сохранения правостороннего

-l- - -
значения rvalue в левостороннем значении 1 value.

В отличие от других бинарных операций, операции присваива­

ния ассоциативны справа налево. Это означает, что в выражении


а=Ь=с присваивание происходит справа налево следующим об-
,· - разом: а= (Ь=с).

Основной для присваивания является операция =. Ее не следует путать


с операцией равенства ==. Чтобы правильно различать обе эти операции,
знак = рекомендуется трактовать как "присваивается значение':
Помимо упомянутой выше простой операции присваивания, в Java опре­
делено 11 других операций, в которых присваивание сочетается с 5 арифме­
тическими операциями и 6 логическими операциями: как поразрядными, так

и сдвига. Например, операция += извлекает значение из левой переменной,

прибавляет к нему значение правого операнда, сохраняет полученную сумму


обратно в левой переменной как побочный эффект и возвращает эту сумму в
качестве значения всего выражения. Таким образом, выражение х+=2 почти
такое же, как и выражение х=х+2, а отличается оно лишь тем, что при выпол­

нении операции += левый операнд вычисляется только один раз. Это отличие

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

а [i ++] += 2;
a[i++] = a[i++] + 2;

Общая форма составных операций присваивания выглядит следующим


образом:

lvalue ор= rvalue

Она равнозначна приведенной ниже общей форме, при условии, что у ле­
востороннего значения 1vа1 ue отсутствуют побочные эффекты.

lvalue = lvalue ор rvalue

Ниже перечислены доступные в Java составные операции присваивания.

+= *= /= %= 11 арифметически операции

11 плюс присваивание

&= 1= л=
11 поразрядные логические операции

11 плюс присваивание

<<= >>= >>>= 11 операции сдвига плюс присваивание

Выражения и операции 89
Чаще всего применяются операции += и -=, хотя операции &= и 1= могут

также пригодиться для манипулирования признаками (или флагами) типа


boolean. Например:

i += 2; 11 инкрементировать счетчик цикла на 2


с -= 5; 11 декрементировать счетчик цикла на 5
flags 1= f; 11 установить признак f в целочисленном

11 наборе признаков

flags &= -f; 11 сбросить признак f в целочисленном

11 наборе признаков

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

Условная операция ?: является в какой-то степени малоизвестной тер­


нарной (трехместной) операцией, унаследованной из языка С. Она позволяет
встроить условную конструкцию в выражение. Ее можно рассматривать как
операционный вариант условного оператора if/else. Первый и второй опе­
ранды условной операции разделяются знаком вопроса (? ), тогда как второй
и третий операнды - двоеточием (: ). Значение первого операнда должно
быть логическим типа boolean, а значения второго и третьего операндов -
любого типа, хотя они должны приводиться к одному и тому же типу.
Сначала в условной операции вычисляется ее первый операнд. Если он
истинный (true), то вычисляется второй операнд, который используется
в качестве значения всего выражения. А если первый операнд оказывается
ложным (false), то из условной операции возвращается значение ее третьего
операнда. Второй и третий операнды условной операции никогда не вычис­

ляются вместе, поэтому следует проявлять особую внимательность, исполь­


зуя выражения с побочными эффектами в данной операции. Ниже приведен
ряд примеров применения условной операции.

int max = (х > у) ? х : у;


String name = (name != null) ? name : "unknown";

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

ций присваивания. Поэтому заключать ее операнды в круглые скобки обычно


не требуется. Тем не менее многие программисты заключают первый операнд
условной операции в круглые скобки ради повышения удобочитаемости ус­
ловных выражений. И это особенно справедливо в связи с тем, что условное
выражение в условном операторе i f всегда указывается в круглых скобках.

90 Глава 2. Основы синтаксиса Java


Операция instanceof
Эта операция тесно связана с объектами и функционированием системы
типов в Java. Если вы только начинаете изучать язык Java, можете пропустить
этот раздел, чтобы вернуться к нему, как только усвоите в должной мере объ­
екты в Java.
В качестве левого операнда в операции instanceof требуется указывать
объект или элемент массива, а в качестве правого операнда - наименование
ссылочного типа. Эта операция вычисляет логическое значение true, если
объект или массив является экземпляром указанного типа, а иначе - возвра­
щается логическое значение false. Если в качестве левого операнда указано
пустое значение null, операция instanceof всегда вычисляет логическое

значение false. Если же вычисление выражения в операции instanceof


дает в итоге логическое значение true, это означает, что левый операнд мож­
но благополучно привести к типу правого операнда и присвоить его значение
переменной в правом операнде.
Операцию instanceof можно применять только к ссылочным типам и
объектам, но не к примитивным типам и значениям. Ниже приведен ряд при­
меров применения операции instanceof.
11 истинно: все строки являются экземплярами типа String:
"string" instanceof String
11 истинно: строки являются также экземплярами типа Object:
instanceof Object
11 ложно: пустое значение null вообще не является
11 ничьим экземпляром:
null instanceof String

Object о= new int[] {1, 2' 3);


о instanceof int [] 11 истинно: элемент относится к

11 массиву типа int


о instanceof byte [] 11 ложно: элемент не относится к

11 массиву типа byte


о instanceof Object 11 истинно: все массивы являются

11 экземплярами типа Object

11 убедиться с помощью операции instanceof в


11 безопасности приведения объекта к указанному типу:
if (object instanceof Point)
Point р = (Point) object;

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

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


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

Специальные операции

В языке Java имеется шесть особых конструкций, которые иногда считают­


ся операциями, а порой - просто частью основного синтаксиса этого языка.

Эти "операции" приведены в табл. 2.4 для того, чтобы продемонстрировать


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

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

Доступ к членам объекта или класса (.)


Объект представляет собой совокупность данных и методов, опериру­
ющих этими данными. Поля данных и методы объекта называются его
членами. Доступ к этим членам осуществляется с помощью операции­
точки (. ). Так, если о является выражением, в котором определяется
ссылка на объект (или имя класса), а f - имя поля в объекте (или клас­
се), то в операции о. f осуществляется доступ к значению, хранящему­
ся в данном поле. А если m- это имя метода, то в операции о . m про­
исходит обращение к данному методу с целью вызвать его, используя
поясняемую далее операцию ().
Доступ к элементам массива ( [])
Массив представляет собой нумерованный список значений. К каждому
элементу массива можно обратиться по его порядковому номеру, иначе
называемому индексом. Операция [] позволяет обращаться к отдель­
ным элементам массива. Так, если а - это массив, а i - выражение,
в котором вычисляется значение типа int, то в операции а [ i] проис­

ходит обращение к одному элементу массива а. В отличие от других


операций над целочисленными значениями, данная операция ограни­

чивается значениями индекса массива типа int или более узкого цело­
численного типа.

Вызов методов ( ())


Метод представляет собой совокупность кода Java, который может быть
выполнен (или вызван) по указанному имени метода, после которого в

92 Глава 2. Основы синтаксиса Java


круглых скобках задается от нуля и больше разделяемых запятой вы­
ражений. Значения этих выражений называются аргументами метода.
Метод обрабатывает заданные аргументы и дополнительно, хотя и нео­
бязательно возвращает значение, становящееся значением того выраже­
ния, из которого вызывается данный метод. Так, если о. m- это ссылка
на метод без аргументов, данный метод может быть вызван с помощью
операции о. m( ) . А если в методе предполагаются три аргумента, то его
можно вызвать, например, из такого выражения: о. m(х, у, z) , где о -
получатель метода. Так, если о является объектом, он называется объ­
ектом-получателем. Прежде чем вызывать метод, интерпретатор Java
вычисляет каждый аргумент, передаваемый методу. Выражения, из ко­

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

слева направо, что имеет особое значение, если какие-нибудь аргумен­


ты имеют побочные эффекты.

Лямбда-выражение(->)
Лямбда-выражение представляет собой совокупность исполняемого
кода Java, которая, по существу, является телом метода. Оно состоит из
списка аргументов метода (от нуля и больше разделяемых запятой вы -
ражений, заключаемых в круглые скобки), после которого следует опе­
рация "стрелка" лямбда-выражения и блок кода Java. Если блок кода
состоит из единственного оператора, то фигурные скобки, в которые
обычно заключается блок кода, можно опустить. Если же лямбда-вы­
ражение принимает единственный аргумент, то его можно не заключать
в круглые скобки.

Создание объекта (new)


В языке Java объекты создаются с помощью операции new, после кото­
рой указывается тип создаваемого объекта и заключаемый в круглые
скобки список аргументов, передаваемых конструктору объектов. Этот
конструктор представляет собой специальный блок кода, в котором ини­
циализируется вновь создаваемый объект. Таким образом, синтаксис соз­
дания объектов подобен синтаксису вызова методов в Java. Например:

new ArrayList<String>();
new Point(l,2)

Создание массива (new)


Массивы являются частным случаем объектов, поэтому они создают­
ся с помощью операции new, которая, впрочем, имеет несколько иной

Выражения и операции 93
синтаксис. В частности, после ключевого слова new указывается тип

создаваемого массива и его размер, заключаемый в квадратные скобки,


как, например, new int [ 5]. Иногда массивы могут быть также созданы
с помощью синтаксиса литералов массивов.

Преобразование или приведение типов ( ())


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

скобках указывается тип, к которому требуется привести преобразуе­


мый тип данных, а в качестве второго аргумента - значение преобразу­
емого типа данных или выражение в круглых скобках. Например:
(byte) 28 //привести целочисленный литерал к типу byte
(int) (х + 3.14f) // привести числовую сумму с плавающей
//точкой к целочисленному типу
(String)h.get(k) // привести обобщенный объект к строке

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

щих потоком выполнения программы, например, ветвлением по условию или

циклом. Это означает, что они способны изменить исходный линейный поря­
док выполнения программы вполне определенным образом. Все операторы,
определяемые в Java, сведены в табл. 2.5.
Таблица 2.5. Операторы Java
Оператор Назначение Синтаксис

Выражение Побочные эффекты перененвая = выраженяе;

выраженяе++;
не!l'од() ;
new !l'Иn ( ) ;

Составной Группирование опера­ { опера!rоры

торов

Пустой Ничего не делать

94 Глава 2. Основы синтаксиса Java


Окончание табл. 2.5
Оператор Назначение Синтаксис

С меткой Наименование .метжа: onepar:rop


оператора

Переменная Объявление переменной [final] r:rJUr 1UU1 [= .энаvею~rе


[, иня [= .эна veНJre ]]

if Ветвление по условию if ( iwpaжeНJre ) onepar:rop


[ else onepar:rop ]
switch Ветвление по условию switch ( iwpaжeНJre )
{ [ case аа~ражеюrе :
onepar:rop.u ] ".
defaul t: onepar:rop.u ] }
while Организация цикла while ( аа~ражеюrе ) onepar:rop
do Организация цикла do onepar:rop while (.&а.!Ражеюrе) ;
for Упрощенный цикл for ( .IOIJIЦXBJDr.ЭSЦXR ;
про•вржа ; •нжренвнr:r

onepar:rop
foreach Циклический обход for ( пврвненнаж :
коллекции •r:rep11pyeю.zй обrъежт )
onepar:rop
break Выход из блока кода break [ неr:rжа ]
continue Перезапуск цикла continue [ неr:rжа ]
return Завершение метода return [ аа~ражеНJ1е
synchronized Выполнение критическо- synchronized (.&а.!Ражею~rе
го раздела кода { onepar:rop.u }
throw Генерирование throw аа~ражея.е
исключения

try Обработка исключения try { onepar:rop.u


[ са tch ( type 11НS1
{ onepar:rop.u } ] ".
finally { onepar:rop.u } ]
assert Проверка инварианта assert JUUJapxaнr:r [ 0111J1бжа ] ;

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

Операторы 95
операторов-выражений относятся выражения присваивания, инкремента

и декремента, вызова методов и создания объектов. Например:

а = 1; 11 присваивание

х *= 2; 11 присваивание с арифметической операцией


i ++; 11 постфиксный инкремент
--с; 11 префиксный инкремент
System.out.println("statement"}; //вызов метода

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

for(int i = О; i < 10; i++) {


a[i]++; // тело этого цикла образует составной оператор
b[i]--; //он состоит из двух операторов-выражений

Пустой оператор
В языке Java пустой оператор обозначается точкой с запятой. Этот опера­
тор ничего не делает, хотя его синтаксис иногда приносит пользу. Например,
пустой оператор можно употребить в теле цикла for, как показано ниже.

// инкрементировать элементы массива:


for(int i = О; i < 10; a[i++]++)
//тело цикла образует пустой оператор:
/* пустой оператор */;

Операторы с меткой
Оператором с меткой является такой оператор, которому можно присво­
ить имя, предварив его идентификатором и двоеточием. Метки обычно упо­
требляются в операторах break и continue. Например:

11 снабженный меткой цикл:


rowLoop: for(int r = О; r < rows.length; r++)
// еще один пример применения метки:
colLoop: for(int с = О; с < columns.length; с++}

break rowLoop;

96 Глава 2. Основы синтаксиса Java


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

оператора. Все переменные следует непременно объявить, прежде чем поль­


зоваться ими. И это обычно делается в операторе объявления переменных.
В связи с тем что язык Java является статически типизированным, в объявле­
нии переменной указывается ее тип, а это означает, что в ней могут хранить­
ся значения только данного типа.

В простейшей форме объявления переменной указывается ее тип и имя,


как показано ниже.

int counter;
String s;

В объявлении переменной можно также указать ее инициализатор, т.е. вы­


ражение, в котором задается начальное значение объявляемой переменной.
Например:

int i = О;
String s = readLine();
11 обсуждаемые далее инициализаторы массива:
int[] data = {x+l, х+2, х+З};

Компилятор Java не разрешает пользоваться локальными переменными,


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

Если у переменной имеется инициализатор, программист может восполь­


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

var i О; 11 тип переменной i выводится как int


var s = readLine(); 11 тип переменной s выводится как String

Операторы 97
Такой синтаксис может быть удобным в практике программирования. Но,
изучая язык Java, им лучше не пользоваться до тех пор, пока не будет твердо
усвоена система типов данных в Java.
В одном операторе объявления переменных можно объявить и инициа­
лизировать сразу несколько переменных, но все они должны быть одного и
того же явно заданного типа. Имена переменных и дополнительные, хотя
и необязательные их инициализаторы разделяются запятыми:

int i, j, k;
float х = l.Of, у = l.Of;
String question = "Really Quit?", response;

Операторы объявления переменных могут начинаться с ключевого слова


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

final String greeting = getLocalLanguageGreeting();

Операторы объявления переменных могут присутствовать повсюду в коде


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

частью оператора цикла for, как поясняется далее.

Локальные переменные можно употреблять только в теле того метода или


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

11 определение метода:
void method() {
int i = О; //объявить переменную i
//здесь переменная i находится в
//своей области видимости:
while (i < 10) {
int j =О; //объявить переменную j; здесь начинается
//область видимости переменной j
i++; // здесь переменная i находится в своей области
//видимости, поэтому инкрементировать ее
//а здесь оканчивается область видимости переменной
System.out.println(i); //здесь переменная i по-прежнему
11 находится в своей области видимости
//а здесь оканчивается область видимости переменной i

98 Гnава 2. Основы синтаксиса Java


Условный оператор if/else
Условный оператор i f относится к числу основополагающих управляю­
щих операторов в Java, позволяя принимать решение, а точнее, выполнять
операторы по заданному условию. С условным оператором if связано задан­
ное выражение и выполняемый оператор. Так, если вычисляется логическое
значение true заданного выражения, интерпретатор Java выполняет указан­
ный оператор. А если вычисляется логическое значение false заданного вы­
ражения, то интерпретатор Java пропускает указанный оператор.

В языке Java допускается задавать в условном выражении тип


класса-оболочки Boolean вместо примитивного типа boolean.
В таком случае объект-оболочка автоматически распаковывается.

Ниже приведен характерный пример применения условного оператора if.


if (username == null) // если переменная username содержит
username = "John Doe"; // пустое значение null, использовать
//задаваемое по умолчанию значение

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


чается условное выражение, они являются обязательной частью синтаксиса
условного оператора if. Как пояснялось ранее, блок операторов, заключае­
мых в фигурные скобки, сам является оператором, и поэтому условный опе­
ратор i f можно также написать следующим образом:
if ((address==null) 11 (address.equals(""))) {
address = "[undefined]";
System.out.println("WARNING: no address specified.");

В обычную форму условного оператора i f входит также дополнительное,


хотя и необязательное ключевое слово else, после которого следует второй
выполняемый оператор. В такой форме условного оператора i f выполняется
первый заданный оператор, если вычисляется логическое значение true ус­

ловного выражения, а иначе - второй оператор. Например:

if (username 1 = null)
System.out.println("Hello" + username);
else {
username = askQuestion("What is your name?");
System.out.println("Hello" + username + ". Welcome 1 " ) ;

Операторы 99
Применяя вложенные условные операторы if/else, следует принять спе­
циальные меры предосторожности, чтобы вспомогательный оператор else
был тесно связан с соответствующим оператором if. Рассмотрим в качестве
примера следующий фрагмент кода:

if (i == j)
if ( j == k)
System.out.println("i equals k");
else
System.out.println("i doesn't equal j"); //НЕВЕРНО'!

В данном примере внутренний оператор if образует единственный услов­


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

тором if связан вспомогательный оператор else, - разве что из заданного


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

i f ( i == j)
if ( j == k)
System.out.println("i equals k"I;
else
System.out.println("i doesn't equal j"); // НЕВЕРНО 11

И хотя такой код вполне допустим, он явно не соответствует намерениям


программиста. Применяя вложенные условные операторы if/else, следует
также употреблять фигурные скобки, чтобы повысить удобочитаемость ис­
ходного кода. Ниже приведен лучший способ написания кода из рассматри­
ваемого здесь примера.

i f 1i == j) {
if 1j == k)
System.out.println("i equals k");

else {
System.out.println("i doesn't equal j");

Вспомогательный оператор else if


Условный оператор if/else удобен для проверки заданного условия и вы­
бора между двумя выполняемыми операторами или блоками кода. Но что,

100 Глава 2. Основы синтаксиса Java


если требуется сделать выбор среди нескольких блоков кода? Для этой цели
служит вспомогательный оператор е 1 s е i f, который на самом деле являет­
ся не каким-то новым синтаксисом, а обычным идиоматическим вариантом
применения стандартного условного оператора if/else. Ниже приведен ха­
рактерный пример применения вспомогательного оператора else if.
if (n == 1) {
11 выполнить блок кода #1

else i f (n == 2) {
11 выполнить блок кода #2

else i f (n == 3) {
11 выполнить блок кода #3

else
11 выполнить блок кода #4, если не удается
11 выполнить все остальные блоки кода

В приведенном выше примере кода нет ничего особенного. Он просто со­


стоит из последовательного ряда условных операторов if, где каждый услов­

ный оператор if является частью вспомогательного оператора else из пре­


дыдущего условного оператора. Пользоваться конструкцией else if более
предпочтительно и допустимо, чем писать условные операторы в полностью

вложенной форме, как показано ниже.

if (n == 1) {
11 выполнить блок кода #1

else
if ( n == 2) {
11 выполнить блок кода #2

else
if (n == 3) {
11 выполнить блок кода #3

else
11 выполнить блок кода #4, если не удается
11 выполнить все остальные блоки кода

Операторы 101
Оператор swi tch
Условный оператор i f обусловливает ветвление в потоке выполнения про­
граммы. Для выполнения множественного ветвления можно, конечно, вос­

пользоваться несколькими условными операторами if, как демонстрирова­

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


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

ры i f могут существенно затруднить чтение исходного кода, особенно если


этот код был со временем реорганизован или отличается многими уровнями
вложения условных операторов if.
Лучшее решение состоит в применении оператора swi tch, унаследованно­

го в Java из языка программирования С. Однако синтаксис данного оператора


не настолько изящен, как у остальных операторов Java, а отказ от пересмотра

его конструкции повсеместно считается ошибкой.

Вначале оператора swi tch задается условное выражение, кото­


рое может быть одного из примитивных типов int, short, char,
byte (или соответствующих им классов-оболочек), ссылочного
типа String или перечислимого типа (подробнее о перечисли-
мых типах речь пойдет в главе 4).
После заданного условного выражения следует блок кода, заключаемый в
фигурные скобки и содержащий различные точки входа, соответствующие
возможным значениям условного выражения. Например, следующий опера­

тор switch равнозначен повторяющимся условным операторам if и else/if,


упоминавшимся в предыдущем разделе:

switch (n) {
case 1: 11 начать здесь, если n == 1
11 выполнить блок кода #1
break; 11 остановить выполнение здесь

case 2: 11 начать здесь, если n == 2


11 выполнить блок кода #2
break; 11 остановить выполнение здесь

case з: 11 начать здесь, если n == з

11 выполнить блок кода #3


break; 11 остановить выполнение здесь если не удается

default: 11 выполнить все остальные блоки кода ...


11 выполнить блок кода #4
break; 11 остановить выполнение здесь

102 Глава 2. Основы синтаксиса Java


Как следует из приведенного выше примера кода, различные точки входа
в оператор swi tch снабжены метками, состоящими из ключевого слова case,
целочисленного значения и двоеточия или специального ключевого слова

defaul t и двоеточия. При выполнении оператора swi tch интерпретатор


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

тора, следующего после метки специальной ветви defaul t:. А если метка
специальной ветви defaul t: отсутствует, то интерпретатор Java пропустит

все тело оператора swi tch.


Обратите внимание на применение ключевого слова break в конце каж­
дой ветви case из приведенного выше примера кода. Оператор break описы­
вается далее в этой главе, но в данном примере он вынуждает интерпретатор

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

Указывать окончание каждой ветви case оператором break или


связанным с ним оператором следует явным образом. В отсут­
ствие операторов break выполнение кода в операторе swi tch
начнется с первого оператора после совпавшей метки в ветви

case и продолжится до тех пор, пока не достигнет конца блока


операторов. После этого произойдет "провал" потока управле­
ния в следующую ветвь case, где выполнение кода продолжится

вместо выхода из блока.

Писать такой код, "проваливающийся" из одной ветви case в другую,


приходится лишь в редких случаях, но в 99% всех случаев каждую обычную
ветвь case и специальную ветвь defaul t следует аккуратно завершать, что­

бы остановить выполнение кода в операторе swi tch. Как правило, для этой
цели применяется оператор break, хотя вполне подходят операторы return
и throw.
Как следствие такого "провала" сквозь ветви по умолчанию, в опера­
торе swi tch допускается снабжать один и тот же выполняемый оператор

Операторы 103
несколькими метками со вспомогательным оператором case. В качестве ха­

рактерного примера ниже приведено применение оператора swi tch в теле

метода.

boolean parseYesOrNoResponse(char response) {


switch(response) {
case 'у':
case 'У': return true;
case 'п':
case 'N': return false;
default:
throw new IllegalArgumentException(
"Response must Ье У or N");

На оператор swi tch и его ветви case с метками накладывается ряд важ­

ных ограничений. Во-первых, условное выражение, задаваемое вначале опе­


ратора swi tch, должно быть надлежащего типа: одного из примитивных
типов byte, char, short, int (или соответствующих им классов-оболочек),
ссылочного типа String или перечислимого типа. Числовые типы с плаваю­
щей точкой и логический тип boolean не поддерживаются, как, впрочем, и
примитивный тип long, хотя он и является целочисленным. Во-вторых, зна­

чение, связанное с меткой в каждой ветви case, должно быть постоянным


или константным выражением, которое способен вычислить оператор. Метка
ветви case не может содержать динамически вычисляемое выражение, вклю­

чающее, например, переменные или вызовы методов. В-третьих, значения


меток в ветвях case должны находиться в пределах допустимого диапазона

представления чисел для того типа данных, который указан в условном выра­
жении оператора swi tch. И, наконец, не допускаются одинаковые значения

меток в двух или более ветвях case или больше одной метки специальной
ветви default.

Оператор while
Это основной оператор Java, позволяющий выполнять повторяющиеся
действия. Иными словами, это одна из основных циклических конструкций в
языке Java. Ниже приведен синтаксис оператора цикла while.
while (выражение)

оператор

104 Глава 2. Основы синтаксиса Java


Сначала в операторе while вычисляется заданное выражение, значение
которого в итоге должно быть типа boolean или Boolean. Если это логиче­
ское значение false, то интерпретатор Java пропустит оператор, указанный
в теле цикла, и перейдет к следующему оператору в программе. А если это
логическое значение t rue, то будет выполнен опера тор, образующий тело
цикла, после чего заданное выражение вычисляется снова. Если на этот раз

оно примет логическое значение false, то интерпретатор Java перейдет к


следующему оператору в программе, а иначе он снова выполнит опера тор,

указанный в теле цикла. Выполнение цикла while продолжится до тех пор,


пока заданное выражение остается истинным (true), т.е. пока оно не ста­
нет ложным (false), и тогда данный цикл завершится, а интерпретатор Java
перейдет к следующему оператору в программе. С помощью синтаксической
конструкции while (true) можно создать бесконечный цикл.
Ниже приведен пример организации цикла while, в котором выводятся

числа от О до 9.
int count = О;
while (count < 10)
System.out.println(count);
count++;

Как видите, значение переменной count начинается в данном примере с


нуля и постепенно увеличивается на единицу всякий раз, когда выполняется

тело цикла while. Как только данный цикл будет выполнен 10 раз, заданное
в нем условное выражение станет ложным (false), т.е. значение переменной
count перестанет быть меньше 10. На этом выполнение операторов в теле
цикла while завершится, а интерпретатор Java перейдет к следующему опера­

тору в программе. У большинства циклов имеется переменная счетчика, по­


добная переменной count. Как правило, таким переменным присваиваются
имена i, j и k, хотя рекомендуются более описательные имена, упрощающие
понимание исходного кода.

Оператор do while
Цикл do while очень похож на цикл while, за исключением того, что его
условное выражение проверяется в конце, а не в начале цикла. Это означает,
что тело данного цикла всегда выполняется хотя бы один раз. Ниже приведен
синтаксис оператора цикла do while.

Операторы 105
do
оператор

while (выражение);

Отметим некоторые отличия цикла do while от более простого цикла


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

пятой. Дело в том, что цикл do завершается своим условным выражением, а


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

int count = О;
do {
System.out.println(count);
count++;
while(count < 101;

Цикл do while применяется намного реже, чем родственный ему цикл


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

Оператор f or
Этот оператор предоставляет циклическую конструкцию, которая зача­
стую оказывается намного более удобной, чем у циклов do и while. В опе­
раторе for выгодно используется распространенный шаблон организации
циклов. В большинстве циклов имеется счетчик или некоторая переменная
состояния цикла, которая инициализируется перед началом цикла, прове­

ряется на предмет выполнения тела цикла, а затем инкрементируется или

как-то иначе обновляется в конце тела цикла перед повторным вычислением


проверяемого выражения. Таким образом, инициализация, проверка и об­
новление являются тремя важными стадиями манипулирования переменной

цикла, и в приведенном ниже синтаксисе оператора for эти стадии явно ука­
заны как его составные части.

fоr(инициализация; проверка; обновление)

оператор

Такой цикл for, по существу, равнозначен следующему циклу while:

1Об Глава 2. Основы синтаксиса Java


инициализация;

while (проверка)
оператор;

обновление;

Расположение выражений инициализации, проверки и обновления в на­


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

или какого-нибудь другого присваивания.


В следующем примере кода демонстрируется организация цикла for для
вывода чисел от О до 9, как в приведенных выше примерах циклов while
и do:
int count;
for(count = О ; count < 10 ; count++)
System.out.println(count);

Обратите внимание на то, что синтаксис оператора for позволяет распо­


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

объявить переменную цикла в выражении инициализации, чтобы она была


доступна в теле цикла и недоступна за его пределами. Например:

for(int count = О ; count < 10 ; count++)


System.out.println(count);

Более того, синтаксис оператора for не накладывает никаких ограниче­


ний на организацию циклов, в которых применяется лишь одна переменная.

В обеих частях, инициализации и обновления, цикла for можно указывать

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

for (int i = О, j = 10 ; i < 10 i++, j--)


surn += i * j;
Несмотря на то что во всех приведенных до сих пор примерах демонстри­
ровался циклический подсчет чисел, применение цикла for этим отнюдь не

ограничивается. Например, цикл for можно организовать для обхода эле­


ментов связного списка, как показано ниже.

for(Node n = listHead; n 1= null; n = n.nextNode())


process (n);

Все выражения инициализации, проверки и обновления цикла for явля­

ются необязательными. Для организации цикла for требуется лишь указать


точки с запятой, разделяющие эти выражения. Так, если опустить выражение
проверки, оно будет считаться истинным (true). Таким образом, можно ор­
ганизовать бесконечный цикл вроде for (;;).

Цикл типа foreach


Оператор цикла for больше всего подходит для обработки данных при­
митивных типов, но не совсем пригоден для обработки коллекций объектов.
Вместо него в Java предоставляется альтернативный синтаксис для организа­
ции цикла типа :foreach, в котором можно обходить коллекции объектов.
В цикле типа foreach применяется ключевое слово for и заключаемое в

круглые скобки объявление переменной цикла (без инициализации), двое­


точие, проверяемое выражение и, наконец, оператор (или блок операторов),
образующий тело цикла:

fоr(объявление: выражение)

оператор

Несмотря на название "цикл типа foreach'', для этого цикла отсутствует


отдельное ключевое слово foreach. Вместо этого двоеточие интерпретиру­
ется в синтаксисе данного цикла как предлог "in'', например: "foreach name in
studentNames" (для каждого имени в коллекции имен студентов).
При описании циклов while, do и for ранее демонстрировался пример

вывода 10 чисел. То же самое можно сделать и в цикле типа foreach, хотя он


больше подходит для обхода элементов коллекции. Чтобы выполнить такой
цикл 10 раз (и вывести 10 чисел), потребуется массив или другая коллекция,

108 Глава 2. Основы синтаксиса Java


состоящая из 10 элементов. Ниже показано, как это можно сделать непосред­
ственно в коде.

!/ эти числа требуется вывести на экран:


int[] primes =
new int[] { 2, 3, 5, 7, 11, 13, 17, 19, 23, 29 );
11 а это цикл, в котором они выводятся:
for(int n : primes)
System.out.println(n);

Возможности цикnа типа foreach


Цикл типа foreach заметно отличается от циклов while, for и do, по­
скольку в нем скрывается счетчик или итератор цикла. Это весьма плодот­
ворная идея, как станет ясно при рассмотрении лямбда-выражений, хотя
имеются такие алгоритмы, которые нельзя вполне естественно выразить с

помощью цикла типа foreach.


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

Если воспользоваться для этой цели традиционным циклом for, то исходный


код данной задачи может выглядеть следующим образом:

for(int i = О; i < words.length; i++)


if (i > 0) System.out.print(", ");
System.out.print(words[i] );

Это очень простая задача, но ее нельзя решить с помощью цикла типа


foreach, не отслеживая дополнительно его состояние. Дело в том, что в ци­
кле типа foreach не предоставляется возможность подсчитывать его шаги

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

Аналогичное затруднение возникает, когда цикл типа foreach


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

Ниже перечислен ряд других задач, которые нельзя решить с помощью


цикла типа foreach.

Операторы 109
• Обойти элементы массива или коллекции типа List в обратном порядке.

• Использовать один счетчик цикла для доступа к элементам двух масси­


вов с одинаковыми индексами.

• Перебрать элементы коллекции типа List, вызывая метод get () вме­


сто ее итератора.

Оператор break
Этот оператор вынуждает интерпретатор Java сразу же перейти в конец

того оператора, который содержит данный оператор. Применение оператора

break уже демонстрировалось ранее при описании оператора swi tch. Опе­

ратор break чаще всего обозначается ключевым словом break и точкой с за­
пятой, как показано ниже.

break;

Если оператор break применяется в такой форме, он вынуждает интер­


претатор Java немедленно выйти из самого внутреннего оператора while, do,
for или swi tch, который его содержит. Например:

for(int i = О; i < data.length; i++) {


if (data [i) == target) {
11 если искомый элемент массива найден,
index = i; //запомнить его индекс
break;
)
11 интерпретатор Java переходит сюда после
11 выполнения оператора break

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

такой форме, он вынуждает интерпретатор Java немедленно выйти из имено­


ванного блока, который может состоять из любых операторов, а не только из
цикла или оператора swi tch. Например:

TESTFORNULL: if (data 1 = null) {


for(int row = О; row < numrows; row++)
for(int col = О; col < numcols; col++)
if (data[row] [col) == null)
break TESTFORNULL; //трактовать массив
//как неопределеннь~

11 О Гnава 2. Основы синтаксиса Java


/!интерпретатор Java переходит сюда после
// выполнения оператора break TESTFORNULL

Оператор continue
Если оператор break вызывает выход из цикла, то оператор continue об­
условливает выход из текущего шага цикла и начинает следующий шаг. В обе­
их формах без метки и с меткой оператор continue может быть использован
только в цикле while, do или for. Так, если оператор continue применяет­

ся без метки, он вызывает начало нового шага в самом внутреннем цикле.


А если оператор continue применяется с меткой, обозначающей цикл, содер­
жащий данный оператор, то он вызывает начало нового шага в именованном

цикле. Например:

for(int i = О; i < data.length; i++) {


//циклически обойти данные
if (data[i] == -1) // если значение данных отсутствует,
continue; // перейти к следующему шагу цикла
process(data[i] ); //обработать значение данных

Циклы while, do и for несколько отличаются тем, как оператор continue


начинает новый шаг.

• В цикле while интерпретатор Java просто возвращается в начало дан­


ного цикла, снова проверяет его условие и опять выполняет тело цикла,

если вычисляется логическое значение true данного условия.

• В цикле do интерпретатор Java переходит непосредственно в конец дан­


ного цикла, где он проверяет его условие, чтобы решить, следует ли вы­
полнять очередной шаг данного цикла.

• В цикле for интерпретатор Java переходит непосредственно в начало


данного цикла, где он вычисляет сначала выражение обновления, а за­

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


цикл снова. Как следует из приведенных выше примеров, поведение
цикла for с оператором continue отличается от поведения "по суще­
ству, равнозначного" цикла while. В частности, выражение обновле­
ния вычисляется в цикле for, чего не делается в равнозначном цикле

while.

Операторы 111
Оператор return
Этот оператор предписывает интерпретатору Java остановить выполнение
текущего метода. Если в методе объявлен возврат значения, то после опера­
тора return должно быть указано выражение. Значение этого выражения
становится значением, возвращаемым из метода. Например, следующий ме­

тод вычисляет и возвращает квадрат числа:

douЬle square(douЫe х) {
// в этом методе вычисляется квадрат значения переменной х
return х * х; // вычислить и возвратить значение

Некоторые методы объявляются с ключевым словом void, указывающим


на то, что данный метод не возвращает никакого значения. Интерпретатор

Java выполняет такие методы, по очереди исполняя их операторы до тех пор,


пока не достигнет конца метода. Исполнив последний оператор, интерпре­
татор Java осуществляет неявный возврат из метода. Но иногда из методов,
объявленных как void, приходится осуществлять явный возврат, прежде чем
будет достигнут последний оператор. В таком случае в теле метода можно
употребить оператор return без всякого выражения. Например, приведен­
ный ниже метод выводит, а не возвращает квадратный корень своего аргу­

мента. Если в качестве аргумента задано отрицательное число, то происходит


возврат из данного метода без всякого вывода результата.

//В этом методе выводится квадратный корень


//значения переменной х:
void printSquareRoot(douЫe х) {
if (х < 0) return; //если значение переменной х
//окажется отрицательным,
11 произвести возврат из метода
System.out.println(Math.sqrt(x) ); //вывести квадратный
//корень значения переменной х

Оператор synchronized
В языке Java всегда поддерживалось многопоточное программирование.
Более подробно об этом речь пойдет далее в книге (особенно в разделе "Под­
держка параллелизма в Java'' главы 6). Следует, однако, иметь в виду, что до­
биться правильного параллелизма на практике совсем не просто, поскольку
ему присущ целый ряд особенностей.

112 Глава 2. Основы синтаксиса Java


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

шается его состояние. Предотвратить такое нарушение помогает предостав­


ляемый в Java оператор synchronized. Ниже приведен его синтаксис.

synchronized ( выражение ) {
операторы

Здесь выражение обозначает вычисляемое выражение, определяющее в


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

В языке Java защита состояния (т.е. данных) объекта входит в ос­


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

ния из критических разделов кода.

Прежде чем выполнить блок операторов, интерпретатор Java сначала полу­


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

Для синхронизированного метода экземпляра интерпретатор Java полу­


чает исключающую блокировку экземпляра класса. (Методы класса и эк­
земпляра обсуждаются в главе 3.) Его можно рассматривать как блок кода
synchronized (this) { ... }, охватывающий весь метод. Статический син­
хронизированный (static synchronized) метод класса вынуждает интер­
претатор Java получать исключительную блокировку класса (формально объ­
екта класса соответствующего типа), прежде чем выполнять такой метод.

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

В языке Java оператор throw служит для генерирования исключения. Ниже


приведена его общая форма.

throw выражение;

Заданное выражение должно в итоге определять объект исключения, опи­


сывающий возникшую исключительную или ошибочную ситуацию. Подроб­
нее об исключениях речь пойдет далее, а до тех пор достаточно знать, что
исключение:

• представлено объектом;

• относится к типу подкласса, производного от класса Exception;


• играет особую роль в синтаксисе Java;
• может быть двух разных типов: проверяемым и непроверяемым.

Ниже приведен характерный пример кода, в котором генерируется исклю­


чение.

puЫic static douЫe factorial (int х) {


if < О)

throw new IllegalArgumentException("x must Ье >=О");


douЫe fact;
for(fact=l.0; х > 1; fact *= х, х--)
/*пустой оператор*/ ; //обратите внимание на
11 применение пустого оператора
return fact;

Когда интерпретатор Java выполняет оператор throw, он сразу же преры­


вает нормальное выполнение программы и приступает к поиску обработчика
исключений, способного перехватить (или обработать) возникшее исклю­
чение. В обработчиках исключений обычно применяются блоки операторов
try/catch/finally, предназначенных для выявления, перехвата и заверше­
ния обработки исключений, как поясняется в следующем разделе. Интерпре­
татор Java сначала проверяет в объемлющем блоке кода наличие связанного

114 Глава 2. Основы синтаксиса Java


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

Если в объемлющем блоке кода отсутствует подходящий обработчик ис­


ключений, интерпретатор Java проверит следующий объемлющий на более
высоком уровне блок кода в данном методе, и так далее до тех пор, пока не
будет обнаружен обработчик исключений. Если же в методе отсутствует об­
работчик, способный обработать исключение, сгенерированное оператором
throw, интерпретатор Java прекратит выполнение текущего метода и возвра­
тится к вызвавшему его методу. А далее интерпретатор Java приступит к по­
иску обработчика исключений в блоках кода вызывающего метода. Подоб­
ным образом исключения распространяются через лексическую структуру
методов Java вверх по стеку вызовов интерпретатора Java. Если исключение
вообще не перехвачено, оно распространяется вплоть до метода main () дан­
ной программы. И если оно не обрабатывается в данном методе, то интерпре­
татор Java выводит сообщение об ошибке и трассировку стека для обозначе­
ния места, где возникло исключение, а затем совершает выход из программы.

Блок операторов try/catch/finally


В языке Java применяется особый механизм обработки исключений. В клас­
сической форме он представлен блоком операторов try/catch/finally. Так,
в операторе try устанавливается блок кода для выявления исключений. По­
сле этого блока следует от нуля и больше блоков оператора catch, в которых
перехватываются и обрабатываются конкретные исключения. А поскольку в
каждом блоке оператора catch могут быть обработаны разные исключения,
то для обозначения обработки нескольких исключений употребляется знак 1,
разделяющий разные исключения, которые должны перехватываться в бло­
ке оператора catch. И, наконец, после блоков оператора catch следует до­
полнительный, хотя и не обязательный блок оператора finally, содержащий
код очистки, который будет гарантированно выполнен независимо от того,
что именно произойдет в блоке оператора try.

Операторы 115
Синтаксис блока оператора try
Операторы catch и :fi.na11y необязательны, но в каждом блоке опе­
ратора try должны быть объявлены некоторые автоматически управ­
ляемые ресурсы (конструкция оператора try с ресурсами), или же ему
должен сопутствовать один или оба оператора catch и :fi.na11y (клас­
сическая конструкция). Все блоки операторов try, catch и :fi.na11y
начинаются и оканчиваются фигурными скобками. Эти скобки являют­
ся обязательной частью синтаксиса и не могут быть опущены, даже если
соответствующий блок состоит из единственного оператора.

В следующем примере кода демонстрируется синтаксис и назначение бло­


ка операторов try/catch/finally:
try (
// Обычно код в этом блоке выполняется сверху вниз без
11 особых затруднений. Но иногда в нем может быть
/!сгенерировано исключение как непосредственно в
//операторе throw, так и косвенно при вызове метода,
//в котором генерируется исключение.

catch (SomeException el) (


//Этот блок состоит из операторов, обрабатывающих объект
//исключения некоторого типа SomeException или
11 производного от него типа. В операторах из этого блока
11 можно обращаться к объекту исключения по имени el.

catch (AnotherException 1 YetAnotherException е2) (


//Этот блок состоит из операторов, обрабатывающих
11 исключение типа AnotherException или
//типа YetAnotherException или же производных от них
//типов. В операторах из этого блока можно обращаться
//к объекту исключения по имени е2.

finally (
// Этот блок состоит из операторов, которые всегда
// выполняются после выхода из блока оператора try и
//независимо от того, произойдет ли из него вь~од,
11 при следующих условиях:
// 1) как обычно, по достижении конца блока;
11 2) вследствие возврата из оператора break,
// continue или return;
/! 3) с исключением, обработанным выше в блоке
// оператора catch;

116 Глава 2. Основы синтаксиса Java


11 4) с неперехваченным исключением, которое не

11 было обработано.

11 Но если в блоке оператора try вызывается


11 метод System.exit(), то интерпретатор Java
11 совершит выход из программы еще до выполнения

11 блока оператора finally.

Оператор try
В этом операторе устанавливается блок кода, где могут возникнуть исклю­
чения, которые требуется обработать, или же требуется выполнить специаль­
ный код очистки, когда он завершается по какой-нибудь причине. В самом
блоке оператора try не делается ничего интересного, поскольку все операции
по обработке исключений и очистке от них выполняются в блоках операто­
ров catch и finally.
Оператор catch
После блока оператора try может следовать от нуля или больше блоков
оператора catch, в которых указывается код для обработки различных ти­
пов исключений. В каждом операторе catch объявляется единственный ар­
гумент, обозначающий (возможно, с помощью специального знака 1) типы
исключений, которые могут быть обработаны в блоке данного оператора, а
также предоставляется имя, по которому можно обращаться к объекту обра­
батываемого исключения. Любой тип исключения, который может быть об­
работан в блоке оператора catch, должен быть представлен некоторым под­
классом, производным от класса ThrowaЫe.

Когда генерируется исключение, интерпретатор Java приступает к поиску


оператора catch с аргументом того же типа, что и у объекта исключения или
суперкласса данного типа. Затем интерпретатор Java выполняет блок перво­
го же обнаруженного оператора catch, совпадающего с критерием поиска.
В блоке этого оператора са tch должны быть предприняты любые действия,
требующиеся для обработки возникшего исключения. Так, если возникнет
исключение типа j ava. io. FileNotFoundException, его можно обработать,
предложив пользователю проверить правильность введенных им данных, и

повторить попытку.

Указывать оператор са tch для перехвата каждого из возможных исклю­


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

Операторы 117
и быть перехваченным в вызывающем методе. А в других случаях (напри­
мер, при возникновении программной ошибки, о которой сигнализирует ис­
ключение типа NullPointerException) правильнее отреагировать, вообще
не перехватывая исключение, но позволив ему распространиться и вынудить

интерпретатор Java выйти из программы с выводом трассировки стека и со­

общением об ошибке.

Оператор f inally
Этот оператор, как правило, служит для очистки после выполнения кода
в блоке оператора try (например, закрытия файла и завершения сетевого со­
единения). Оператор finally удобен тем, что он гарантированно выполняет­
ся в том случае, если выполняется любая часть блока оператора try, незави­
симо от того, как завершится блок оператора try. В действительности един­
ственный способ выйти из блока оператора try, не позволив выполниться
блоку оператора finally, состоит в вызове метода System. exi t (),который
вынудит интерпретатор Java прекратить выполнение программы.

В обычной ситуации управление программой достигает конца блока опе­


ратора try, а затем продолжается в блоке оператора
finally, где выполняет­
ся необходимая очистка. Если же выход из блока оператора try происходит
вследствие выполнения оператора return, continue или break, то блок опе­
ратора finally выполняется прежде, чем управление перейдет в новое место
назначения.

Если исключение возникает в блоке оператора try, с которым связан блок


оператора catch для обработки этого исключения, то управление передается
сначала блоку оператора catch, а затем блоку оператора finally. Если же
отсутствует локальный блок оператора catch для обработки данного исклю­
чения, управление передается сначала блоку оператора finally, а затем рас­
пространяется вверх к ближайшему объемлющему блоку оператора catch,
способному обработать данное исключение.
Если в самом блоке оператора finally управление передается через опе­
ратор return, continue, break или throw или вызов метода, генерирующего

исключение, то ожидающая своей очереди передача управления отменяется

и обрабатывается эта новая передача управления. Так, если в блоке операто­


ра finally генерируется исключение, оно заменяет собой любое исключе­
ние, которое находится в процессе генерирования. Если же в блоке оператора
finally выполняется оператор return, то возврат из метода происходит, как

обычно, даже если исключение было сгенерировано и еще не обработано.

118 Гnава 2. Основы синтаксиса Java


Операторы try и finally могут использоваться совместно без исключе­
ний и любых блоков оператора catch. В таком случае блок оператора finally
содержит код очистки, который гарантированно выполняется независимо от

наличия операторов break, continue или return в блоке оператора try.

Оператор try с ресурсами


Стандартная форма блока оператора try имеет весьма общий характер,
но имеется целый ряд типичных случаев, требующих от разработчиков про­
являть особую внимательность при написании блоков операторов catch и
finally. В частности, если требуется очистить, закрыть или освободить те
ресурсы, которые больше не нужны после их использования.
В языке Java предоставляется очень удобный механизм для автоматическо­
го освобождения ресурсов, известный под названием оператора try с ре­
сурсами (TWR). Более подробно оператор try с ресурсами рассматривается
в разделе "Классический ввод-вывод в Java" главы 10, но ради полноты из­
ложения представим его синтаксис. В следующем примере демонстрируется

порядок открытия файла средствами класса FileinputStream, что приводит


к созданию объекта, требующего очистки:

try (InputStream is =
new FileinputStream("/Users/ben/details.txt"))

// ... обработать файл

В этой новой форме оператор try принимает параметры, представляющие


объекты, требующие очистки 2 • Область действия этих объектов ограничива­
ется блоком данного оператора try, после чего они автоматически очищают­
ся независимо от того, каким образом завершается этот блок. И разработчи­
ку не нужно писать никаких блоков оператора catch и finally, поскольку
компилятор Java автоматически вставляет требующийся код очистки.
Весь новый код, оперирующий ресурсами, следует теперь писать в стиле
оператора try с ресурсами, поскольку он намного менее подвержен ошибкам,
чем написанные вручную блоки оператора catch, и не страдает недостатка­
ми, присущими таким механизмам, как полное завершение (см. раздел "Пол­
ное завершение" в главе 6).

2 Формально они должны реализовывать интерфейс AutoCloseaЫe.

Операторы 119
Оператор assert
Этот оператор является попыткой предоставить возможность проверять
проектные допущения в коде Java. Проверяемое утверждение состоит из
ключевого слова assert и логического выражения, вычисление которого, по

мнению программиста, должно в итоге дать логическое значение true. По


умолчанию утверждения не активизируются, а оператор assert ничего фак­
тически не делает.

Но утверждения можно активизировать как отладочное средство, и тогда


оператор assert вычисляет заданное выражение. Если оно действительно ис­
тинно (true), то оператор assert ничего больше не делает. А если заданное
выражение вычисляется как ложное (false), то утверждение не проходит и
оператор assert генерирует исключение типа j ava. lang. AssertionError.

Оператор assert применяется крайне редко, помимо базовых


библиотек из комплекта JDК. Он оказывается недостаточно гиб­
ким для тестирования большинства приложений и поэтому ред­
ко применяется рядовыми разработчиками. Вместо этого они
пользуются обычными средствами тестирования вроде JUnit.

В операторе assert можно задать второе дополнительное, хотя и необя­


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

(false), то значение второго выражения интерпретируется как код ошибки


или сообщение об ошибке и передается конструктору AssertionError ().
Ниже приведен полный синтаксис оператора assert.
assert утверждение;

или

assert утверждение : код ошибки;

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


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

что одно из допущений программиста не подтвердилось. Это означает, что

120 Гnава 2. Основы синтаксиса Java


прикладной код применяется за пределами тех параметров, на которые он

рассчитан, а следовательно, нельзя ожидать, что он будет выполняться пра­


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

пытаться перехватывать данное исключение, если только это не делается на

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

1
~--~~.'. Ни в коем случае не генерируйте исключение типа Assertion
; Error в своем коде, поскольку это может иметь неожиданные
1

последствия в будущих версиях платформы Java.

Активизация утверждений

Ради эффективности утверждения не имеет смысла проверять всякий раз,


когда выполняется код. Ведь в операторах assert закодированы допущения,

которые должны быть всегда истинны. Поэтому утверждения по умолчанию


дезактивизированы, а выполнение операторов assert не возымеет никако­

го действия. Тем не менее код утверждений остается скомпилированным в


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

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

бы активизировать утверждения в системных классах, - аргументом -esa.


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

java -ea:com.example.sorters.MergeSort
com.example.sorters.Test

Что бы активизировать утверждения во всех классах из отдельного пакета


и его подпакетов, следует указать аргумент -еа, двоеточие, имя пакета и

многоточие в командной строке:

java -ea:com.example.sorters ... com.example.sorters.Test

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


зуя аргумент командной строки -da. Например, утверждения можно сначала

Операторы 121
активизировать во всем пакете, а затем дезактивизировать их в отдельном

классе или подпакете, как показано ниже.

java -ea:com.example.sorters ...


-da:com.example.sorters.QuickSort
java -ea:com.example.sorters ...
-da:com.example.sorters.plugins ..

Наконец, во время загрузки классов можно проконтролировать, активи -


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

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

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

• Имя метода.

• Количество, порядок следования, тип и наименование параметров, при­


меняемых в методе.

3 В спецификации на язык Java термин "сигнатура" имеет формальное значение, которое


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

122 Глава 2. Основы синтаксиса Java


• Тип значения, возвращаемого из метода.

• Проверяемые исключения, которые могут быть сгенерированы в методе


(в его сигнатуре могут быть также перечислены непроверяемые исклю­
чения, хотя это и необязательно).

• Различные модификаторы доступа, предоставляющие дополнительные


сведения о методе.

В сигнатуре метода определяется все, что требуется знать о нем, прежде


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

из которых начинается со своей сигнатуры.

Сигнатура метода выглядит следующим образом:

модификаторы тип имя (список параметров) [throws исключения]

После сигнатуры (или спецификации) метода следует тело (или реализа­


ция) метода, которое состоит из последовательности операторов Java, заклю­
чаемых в фигурные скобки. Если метод является абстрактным (см. главу 3),
его реализация опускается, а тело заменяется единственной точкой с запятой.
В сигнатуру метода могут также входить объявления переменных типа. Та­
кие методы называются обобщенными. Более подробно обобщенные методы
и переменные типа рассматриваются в главе 4.
Ниже приведен ряд примеров определений методов, которые начинаются

с сигнатуры и продолжаются телом метода.

//Этому методу передается массив строк, и он не


//возвращает никакого значения. Имя и сигнатура этого
//метода служат точкой входа во все программы на Java:
puЫic static void main(String[] args) {
if (args.length > 0)
System.out.println("Hello" + args[O]);
else System.out.println("Hello world");

//Этому методу передаются два аргумента типа douЫe,


11 и он возвращает значение типа douЫe:
static douЫe distanceFromOrigin(douЫe х, douЫe у) (
return Math.sqrt(x*x + у*у);

Методы 123
11 Этот является абстрактным метод, а значит, у него
11 отсутствует тело. Следует, однако, иметь в виду, что
11 при его вызове могут генерироваться исключения:
protected abstract String readText(rile f, String eпcoding)
throws rileNotroundException,
UnsupportedEncodingException;

В качестве модификаторов указывается от нуля и более ключевых слов,


предназначенных для обозначения модификаторов доступа и разделяемых
пробелами. Метод может быть, например, объявлен с модификаторами до­
ступа puЫic и static. Допустимые модификаторы доступа и их назначение
описываются в следующем разделе.

В сигнатуре метода тип обозначает тип значения, возвращаемого из ме­


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

После определения модификаторов доступа и типа возвращаемого значе­


ния в сигнатуре метода указывается его имя. Имена методов, как и перемен­
ных, являются идентификаторами, и, как и все идентификаторы в Java, они
могут содержать буквы любого языка, представленного набором символов в
Юникоде. Вполне допустимо и зачастую очень удобно определить несколь­
ко методов под одним и тем же именем, при условии, что они отличаются

списком своих параметров. Определение нескольких методов с одинаковым


именем называется перегрузкой методов.

В отличие от других языков программирования, в Java отсут­


ствуют анонимные методы. Вместо этого в версии Java 8 внедре­
ны лямбда-выражения, похожие на анонимные методы, но авто­
матически преобразуемые во время выполнения в подходящие
именованные методы. Подробнее об этом см. далее в разделе
"Лямбда- выражения':

Например, упоминавшийся ранее метод System. out. println () является


перегружаемым. Один метод под именем println выводит строку, а другие

124 Глава 2. Основы синтаксиса Java


методы под тем же самым именем - значения различных примитивных ти­

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

Когда определяется метод, после имени метода всегда указывается список


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

Когда метод вызывается, значения передаваемых ему аргументов должны со­


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

в строке кода с сигнатурой метода. Передаваемые значения совсем не обяза­


тельно должны совпадать по типу с параметрами, указанными в сигнатуре

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


не требуя их приведения.

~!
Если в методе Java не предполагается никаких аргументов, спи­
сок его параметров просто обозначается пустыми круглыми

- скобками (),но не (void). В языке Java пустой тип


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

С и С++.

Программирующим на Java разрешается определять и вызывать методы,


void вооб­

принимающие переменное количество аргументов с помощью синтаксиса,

вольно называемого varargs (аргументы переменной длины). Более подробно


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

классов исключений, которые должны быть перечислены во вспомогательных


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

Если метод может сгенерировать одно или более исключение, такая воз­
можность должна быть указана в его сигнатуре с помощью ключевого слова

Методы 125
throws, которое располагается после списка параметров и снабжается после­
дующими именами классов тех исключений, которые способен сгенерировать
данный метод. Если же метод не генерирует никаких исключений, то ключе­

вое слово throws в его сигнатуре не употребляется. А если метод генерирует


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

Модификаторы доступа к методам


Модификаторы доступа к методам состоят из нуля или более ключевых
слов, в том числе puЫic, static или abstract. Ниже перечислены допу­

стимые модификаторы доступа к методам и вкратце описано их назначение.


abstract
Модификатор доступа abstract обозначает абстрактный метод без ре­
ализации. Фигурные скобки и операторы Java, обычно составляющие
тело метода, заменяются единственной точкой с запятой. Класс, содер­

жащий абстрактный метод, сам должен быть объявлен абстрактным


(abstract). Такой класс является неполным, и поэтому получить его
экземпляр нельзя (подробнее об этом - в главе 3).
final
Модификатор доступа f i na 1 обозначает конечный метод, который
нельзя переопределить или скрыть в подклассе, и поэтому он легко под­

дается оптимизации во время компиляции, в отличие от обычных ме­


тодов. Все закрытые (pri vate) методы неявно оказываются конечны­
ми, как, впрочем, и все методы любого класса, объявленные конечными
(final).
native
Модификатор доступа na t i ve обозначает реализацию метода, написан­
ную на каком-нибудь платформенно-ориентированном языке вроде С и
внешне доступную для программы на Java. Как у абстрактных методов,
так и у платформенно-ориентированных (native) отсутствует тело, а
фигурные скобки заменяются единственной точкой с запятой.

Реализация платформенно-ориентированных методов


В первоначальном выпуске Java платформенно-ориентированные ме­
тоды иногда применялись для повышения эффективности, но теперь

126 Глава 2. Основы синтаксиса Java


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

puЫic,protected,private

Эти модификаторы доступа обозначают, можно ли и где именно при­


менить метод за пределами того класса, в котором он определен. Эти
очень важные модификаторы доступа более подробно разъясняются
в главе 3.
static
Модификатор доступа static обозначает статический метод клас­
са, связанный с самим классом, а не с его экземпляром (подробнее об
этом - в главе 3).
strictfp
Сокращение fp в этом нескладном наименовании модификатора досту­
па означает "плавающая точка". Как правило, в Java выгодно использу­
ется любая возможность повысить точность вычислений с плавающей
точкой, опираясь на аппаратные средства исполняющей платформы.
А указание модификатора доступа strictfp в объявлении метода вы­
нуждает Java строго придерживаться стандарта при выполнении такого

метода и производить арифметические операции с плавающей точкой


только в 32- или 64-разрядной форме, даже если это приводит к менее
точным результатам.

synchronized
Модификатор доступа synchronized обозначает потокобезопасный
метод. Прежде чем вызывать синхронизированный (synchronized) ме­
тод из потока исполнения, необходимо получить блокировку его класса
(для статических методов) или соответствующий экземпляр класса (для
нестатических методов). Благодаря этому искпючается одновременное
исполнение синхронизированного метода в двух потоках.

Методы 127
Модификатор доступа synchronized обозначает лишь подробности ре­
ализации, поскольку имеются другие способы сделать методы потокобезо­
пасными. Формально он не является частью спецификации метода или при­
кладного интерфейса API. В качественно составленной документации, как
правило, явно указывается, является ли метод потокобезопасным. Поэтому,
имея дело с многопоточными программами, не стоит особенно полагаться на
наличие или отсутствие ключевого слова synchronized.

1~-~ Особенно интересный случай представляют аннотации (подроб-


1 1
· нее о них речь пойдет в главе 4). Их можно рассматривать как
! нечто среднее между модификатором доступа к методу и допол-
1

l
нительной вспомогательной информацией о типе данных.

Проверяемые и непроверяемые исключения

В механизме обработки исключений в Java различаются два типа исклю­


чений: проверяемые и непроверяемые. Отличие проверяемых исключений от
непроверяемых связано с теми обстоятельствами, при которых могут быть
сгенерированы исключения. В частности, проверяемые исключения возника­
ют при конкретных, вполне определенных обстоятельствах, а зачастую - при
тех исключительных ситуациях, из которых может быть полностью или ча­
стично восстановлена нормальная работа прикладной программы.
Рассмотрим в качестве примера некоторый код, способный найти свой
файл конфигурации в одном из доступных каталогов. Если попытаться от­
крыть файл из того каталога, где он отсутствует, то будет сгенерировано
исключение типа FileNotFoundException. В данном примере требуется
перехватить такого рода исключение и попытаться найти файл в другом до­
ступном месте. Иными словами, исключительную ситуацию, возникающую в

связи с отсутствием файла, можно исправить, поскольку это вполне понят­


ный и предсказуемый неудачный исход поиска файла в каталоге.
С другой стороны, операции в среде Java могут иметь ряд неудачных ис­
ходов, которые нелегко предвидеть или предусмотреть из-за условий вы­

полнения или неправильного обращения с библиотечным кодом. Напри­


мер, никоим образом нельзя предвидеть возникновение исключения типа
OutOfMemoryError, а в любом методе, где применяются объекты или мас­
сивы, может быть сгенерировано исключение типа NullPointerException,
если ему передается неверный пустой (null) аргумент.

128 Гnава 2. Основы синтаксиса Java


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

Такие исключения подпадают под действие закона Мэрфи, который гласит:


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

если вообще возможно, просто потому, что они непредсказуемы.


Чтобы выяснить, является ли исключение проверяемым или непро­
веряемым, следует помнить, что исключения являются объектами типа
ThrowaЫe и делятся на две основные категории, представленные его под­

классами Error и Exception. В частности, любой объект, относящийся к


типу Error, представляет проверяемое исключение. Имеется также подкласс
RuntimeException, производный от класса Exception, а любой подкласс,
производный от класса RuntimeException, представляет непроверяемое ис­
ключение. Все остальные исключения относятся к категории проверяемых.

Обработка проверяемых исключений

В языке Java приняты разные правила для обработки проверяемых и не­


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

Даже если вы вообще не генерируете проверяемое исключение самостоя­


тельно, то иногда вам все же придется пользоваться вспомогательным опера­

тором throws для объявления проверяемого исключения. Если же в одном


методе вызывается другой метод, способный сгенерировать проверяемое ис­
ключение, то вы должны включить в его тело код обработки такого исключе­
ния или объявить с помощью ключевого слова throws, что в данном методе
может быть сгенерировано это исключение.
Например, в приведенном ниже методе предпринимается попытка оце­
нить размер веб-страницы, и для этой цели в нем используются стандарт­
ные библиотеки из пакета j ava. net и класс URL (подробнее об этом расска­
зывается в главе 10). А поскольку в данном методе применяются методы и
конструкторы, способные генерировать различные исключения типа j а vа .

Методы 129
io. IOException, то об этом объявляется во вспомогательном операторе
throws.
puЫic static estimateHomepageSize(String host)
throws IOException {
URL url = new URL("htp://"+ host +"/");
try (InputStream in = url.openStream())
return in.availaЬle();

В действительности приведенный выше код содержит программную


ошибку, поскольку в нем неверно указан спецификатор h t р: / / сетево­
го протокола, который вообще не существует. Поэтому выполнение метода
estimateHomepageSize () будет всегда неудачно завершаться исключением
типа MalformedURLException.

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


исключение? Об этом можно судить по сигнатуре данного метода, а иначе
компилятор уведомит (в сообщении об ошибке), что был вызван метод, где
генерируются исключения, которые требуется обработать или объявить.

Списки аргументов переменной длины


Методы можно объявить таким образом, чтобы принимать и быть вызван­
ными с переменным числом аргументов. Такие методы обычно называются
методами с аргументами переменной длины. Так, в методе вывода данных в
отформатированном виде System. out. printf () и связанном с ним мето­
де format () из класса String используются аргументы переменной длины,
как, впрочем, и в целом ряде важных методов из прикладного интерфейса
Reflection API, входящего в состав пакета j ava. lang. reflect.
Чтобы объявить список аргументов переменной длины, достаточно ука­
зать многоточие ( ... ) вслед за типом последнего аргумента в списке. При
этом многоточие указывает на то, что последний аргумент в списке может

повторяться нуль или более раз. Например:

puЬlic static int max(int first, int ... rest)


/* тело этого метода пока что опущено */

Методы с аргументами переменной длины обрабатываются исключитель­


но компилятором. В таких методах список аргументов переменной длины

130 Глава 2. Основы синтаксиса Java


преобразуется в массив. Для исполняющей системы Java метод max () отлича­
ется от следующего метода:

puЫic static int max(int first, int[] rest)


/* тело этого метода пока что опущено */

Чтобы преобразовать сигнатуру с аргументами переменной длины, доста­


точно заменить многоточие ( ... ) квадратными скобками ( [ ] ). Однако мно­
готочие можно указать в списке параметров метода лишь один раз и только

после типа последнего параметра в списке.

А теперь реализуем тело метода max ( ) из данного примера:

puЫic static int max(int first, int ... rest) {


int max = first;
for(int i : rest) {
//вполне допустимо, т.к. остальное фактически
//является массивом
if (i > max) max = i;

return max;

В приведенной выше реализации метод max () объявлен с двумя аргумен­


тами. В качестве первого аргумента служит обычное значение типа in t, но
второй аргумент может повторяться нуль и более раз. Так, все приведенные
ниже вызовы метода max ( ) являются вполне допустимыми.

max(O)
max (1, 2)
max(lб, 8, 4, 2, 1)

Методы с аргументами переменной длины компилируются в методы, ожи­

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


разом, чтобы включить в них код для создания и инициализации подобного
массива. Например, вызов max (1, 2, 3) компилируется в следующий вызов:

max ( 1, new int [] { 2, З ))

В действительности, если аргументы метода уже находятся в массиве,


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

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

Введение в классы и объекты


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

дов, оперирующих этими значениями. Классы относятся к одному из пяти


ссылочных типов данных, поддерживаемых в языке Java, но это едва ли не са­

мый важный ссылочный тип. Более подробному описанию классов посвяще­


на глава 3, а здесь дается лишь введение в классы, поскольку они находятся

на следующем более высоком уровне синтаксиса после методов. Кроме того,


для усвоения материала остальной части этой главы потребуется элементар­
ное представление о классах и простейшем синтаксисе их определения, полу­

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


Самое главное, что в классах определяются новые типы данных. Напри -
мер, можно определить класс Point, чтобы представить точку данных в дву­
мерной прямоугольной системе координат. С этой целью в данном классе
следует определить поля (каждое типа douЫe) для хранения координат х и
у точки, а также методы для манипулирования и оперирования этой точкой.
Таким образом, класс Point представляет новый тип данных.
При обсуждении типов данных очень важно отличать сам тип данных от
тех значений, которые он представляет. Так, тип данных char представля­

ет символы в Юникоде, но значение типа char - один конкретный символ.


Если класс является типом данных, то значение класса называется объектом.
Для его обозначения употребляется имя класса, потому что в каждом клас­
се определяется тип (разновидность, вид или класс) объектов. Упомянутый
выше класс Point является типом данных, представляющим точки с коор­

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

Определение класса
Ниже приведено одно из возможных определений рассмотренного выше
класса Point.

132 Глава 2. Основы синтаксиса Java


/** Этот класс представляет точку с прямоугольными
координатами (х,у) */
puЫic class Point {
//координаты точки
puЫic douЫe х, у;

puЫic Point(douЫe х, douЫe у) {


//конструктор, в котором инициализируются поля:
this.x = х; this.y =у; //инициализировать поля

puЫic douЫe distanceFromOrigin()


//метод, оперирующий полями х и у:
return Math.sqrt(x*x + у*у);

Приведенное выше определение класса хранится в файле Point. j ava и


компилируется в файл Point. class, где оно становится доступным для при­
менения в программах на Java и других классах. Данное определение класса
предоставляется здесь ради полноты изложения материала и введения в его

контекст, но не стоит ожидать, что оно станет сразу же понятным во всех

подробностях. Теме определения классов посвящена большая часть главы 3.


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

каждом компьютере, где выполняется Java.

Создание объекта
Определив класс Point как новый тип данных, можно объявить перемен­
ную для хранения объекта типа Point в следующей строке кода:

Point р;

Но объявление переменной для хранения объекта типа Point не приводит


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

объекта (т.е. его тип) и дополнительный, хотя и не обязательный список ар­


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

Введение в классы и объекты 133


/!создать объект типа Point, представляющий точку
//с координатами (2,-3.5), а также объявить
/!переменную р и сохранить в ней ссылку на новый
//объект типа Point:
Point р = new Point(2.0, -3.5);

/!создать другой объект, представляющий текущую дату:


LocalDateTime d = new LocalDateTime();

11 создать еще один объект типа HashSet для хранения


//множества символьных строк:
Set<String> words = new HashSet<>();

Для создания объектов в Java чаще применяется ключевое слово new, но


стоит упомянуть и ряд других способов. Во-первых, классы, удовлетворяю­
щие определенным критериям, настолько важны, что для создания их объек­
тов в Java определяется специальный синтаксис, как поясняется далее в этом
разделе. Во-вторых, в Java поддерживается механизм динамической загрузки
классов и получения их экземпляров в прикладных программах (подробнее
об этом - в главе 11). И, наконец, объекты можно создавать путем десериа­
лизации. Так, если состояние объекта сохранено (т.е. сериализировано - как
правило, в файле), такой объект можно воссоздать средствами класса j ava.
io.ObjectinputStream.

Применение объектов
Итак, пояснив, каким образом определяются классы и получаются их эк­
земпляры в процессе создания объектов, перейдем к рассмотрению синтакси­
са Java, позволяющего применять эти объекты. Напомним, что в классе опре­
деляется совокупность полей и методов. У каждого объекта данного класса
имеются свои копии его полей и доступ к его методам. Для доступа к име­
нованным полям и методам объекта здесь и далее будет употребляться знак
точки (. ). Например:

Point р
= new Point(2, 3); 11 создать объект
douЫe х = р.х; // получить значение из поля объекта
р.у = р.х * р.х; // установить значение в поле объекта
douЫe d = p.distanceFromOrigin(); //получить доступ
!/к методу объекта

Этот синтаксис весьма распространен в программировании на объектно­


ориентированных языках, и Java не является здесь исключением. Обратите
особое внимание на выражение р. distanceFromOrigin () в приведенном

134 Глава 2. Основы синтаксиса Java


выше примере кода. В этом выражении компилятору Java предписывается
найти метод distanceFromOrigin (), определенный в классе Point, и вос­
пользоваться им для выполнения вычислений над полями объекта р. Более
подробно эта операция рассматривается в главе 3.

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

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


определяется также литеральный синтаксис для ряда специальных ссылоч -

ных типов, как поясняется в последующих подразделах.

Строковые литералы

Класс String представляет текст в виде символьных строк. А поскольку


прикладные программы обычно взаимодействуют со своими пользователями
через набираемый текст, то способность манипулировать текстовыми стро­
ками очень важна для любого языка программирования. В языке Java стро­
ки являются объектами, а текст представлен типом данных, определяемым в
классе String. В современных программах на Java строковые данные обычно
применяются больше, чем какие-нибудь другие их типы. А поскольку стро­
ки являются настолько важным типом данных в Java, то текст разрешается
включать в программы буквально, заключая его в двойные кавычки (").На­
пример:

String name = "David";


System.out.println("Hello, "+ name);

Не следует путать двойные кавычки, в которые заключаются строковые


литералы, с одиночными кавычками (или апострофами), в которые заключа­
ются символьные литералы. Строковые литералы могут содержать любые по­
следовательности управляющих символов (см. табл. 2.2). Последовательности
управляющих символов особенно удобны для встраивания двойных кавычек
в строковые литералы, заключаемые в двойные кавычки. Например:

String story =
"\t\"How сап you stand it?\" he asked sarcastically.\n";

Строковые литералы не могут содержать комментарии и состоять боль­


ше чем из одной строки. В языке не поддерживается синтаксис продолже­

ния строки, позволяющий интерпретировать две отдельные строки как одну.

Введение в классы и объекты 135


Если же требуется представить длинную текстовую строку, не вмещающуюся
в одной строке кода, ее следует разбить на отдельные строковые литералы,
соединив их знаком+, обозначающим операцию сцепления строк. Например:

11 следующая строка кода недопустима, т.к. строковые


11 литералы, нельзя разбивать, перенося на новую строку:
String х = "This is а test of the
emergency broadcast system";

11 вместо этого лучше сделать следующее:


String s "This is а test of the "
+ "emergency broadcast system";

Отдельные строковые литералы сцепляются при компиляции программы,


но не при ее выполнении. Поэтому беспокоиться о снижении производитель­
ности нет никаких оснований.

Литераnы типов

Следующий тип данных, в котором поддерживается собственный син­


таксис объектных литералов, определен в классе Class. Экземпляры класса
Class представляют тип данных Java и содержат метаданные о том типе, на

который они ссылаются. Чтобы включить объект типа Class буквально в


программу на Java, достаточно указать имя любого типа данных с расшире­
нием . class. Например:

Class<?> typeint = int.class;


Class<?> typeintArray = int[] .class;
Class<?> typePoint = Po1nt.class;

Пустая ссыnка

Ключевым словом null обозначается специальное литеральное значение,


означающее пустую ссылку или отсутствие всякой ссылки. Особенность пу­
стого значения null состоит в том, что оно является членом каждого ссы­

лочного типа. Пустое значение null можно присвоить переменным любого


ссылочного типа. Например:

String s = null;
Point р = null;

Лямбда-выражения
Главным новшеством в версии Java 8 стало внедрение лямбда-выраже­
ний. Такие выражения весьма распространены в языках программирования

136 Глава 2. Основы синтаксиса Java


и особенно в языках функционального программирования (например, Lisp,
Haskell и OCaml). Но благодаря эффективности и гибкости лямбда-выраже­
ний их применение выходит далеко за рамки языков функционального про­
граммирования, и поэтому их можно обнаружить практически во всех совре­
менных языках программирования.

Определение лямбда-выражения
Лямбда-выражение, по существу, является функцией, у которой нет
имени и которую можно рассматривать как значение в языке програм­

мирования. А поскольку в Java не разрешается выполнять код вне клас­


сов, то в этом языке лямбда-выражение является анонимным методом,
определенным в некотором классе, возможно, даже неизвестном разра­

ботчику.

Ниже показано, каким образом выглядит синтаксис лямбда-выражений.

список_параметров ) -> { операторы }

А это простой и довольно традиционный пример лямбда-выражения:

RunnaЬle r = () -> System.out.println("Hello World");

Если лямбда-выражение применяется как значение, оно автоматически


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

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

быть одного и того же типа.

Массивы 137
Элементы массива нумеруются начиная с нуля, а их допустимые индексы
находятся в пределах от нуля до величины, на единицу меньшей количества

элементов в массиве. Например, элемент массива по индексу 1 является вто­


рым элементом в массиве. Количество элементов в массиве составляет его
длину. Длина массива указывается при его создании и вообще не меняется.
Элемент массива может быть любого допустимого в Java типа, включая и
типы массивов. Это означает, что в Java поддерживаются массивы массивов,
допускающие разновидность многомерных массивов. Хотя в Java не поддер­
живаются матричные многомерные массивы, обнаруживаемые в ряде других
языков программирования.

Типы массивов
Как и у классов, типы массивов являются ссылочными. А экземпляры
массивов являются объектами, как и экземпляры классов 4 . Но, в отличие от
классов, типы массивов совсем не обязательно определять, а достаточно ука­
зать квадратные скобки после типа элемента массива. Например, в следую­
щем фрагменте кода объявляются три переменные типа массива:

byte Ь; //тип byte является примитивным


byte[] arrayOfBytes; // byte(] - это массив
// байтовых значений
byte [] [] arrayOfArrayOfBytes; / / byte [] [] - это массив
11 массивов типа byte[]
String[] points; // String(] - это массив строк

Длина массива не имеет никакого отношения к его типу. Например, нельзя


объявить метод, ожидающий получить массив, состоящий ровно из четырех
значений типа int. Если параметр метода относится к типу int [],из вызы­
вающего кода можно передать массив с любым количеством элементов, в том
числе и нулевым.

Несмотря на то что типы массивов не относятся к классам, их экземпля­


ры все же являются объектами. Это означает, что массивы наследуют методы
из классаj ava. lang. Obj ect. В массивах реализуется интерфейс CloneaЫe
и переопределяется метод clone (),тем самым гарантируя, что массив мо­
жет всегда быть клонирован и что в методе clone () не будет сгенериро­
вано исключение CloneNotSupportedException. Кроме того, в массивах

4 При рассмотрении массивов возникают терминологические трудности. В отличие от классов


и их экземпляров, термином массив обозначается как тип массива, так и его экземпляр. Но на
практике из самого контекста становится ясно, идет ли речь о типе или о значении.

138 Глава 2. Основы синтаксиса Java


реализуется интерфейс SerializaЬle, что дает возможность выполнить се­
риализацию массива, если только сам тип его элементов подлежит сериализа­

ции. Наконец, у всех массивов имеется поле length, объявленное как puЫic
final int и определяющее количество элементов в массиве.

Расширяющие преобразования типов массивов

Как упоминалось выше, массивы расширяют класс Obj ect и реализуют ин­

терфейсы CloneaЫe и SerializaЬle, и поэтому тип любого массива можно


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

т [] может быть присвоен массиву типа S [ ] . Следует, однако, иметь в виду,


что для массивов заданного примитивного типа такого рода расширяющие

преобразования типов не допускаются. В качестве примера в приведенных


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

другого массива означает, что тип массива во время компиляции не всегда

соответствует его типу во время выполнения.

String[] arrayOfStrings; //этот массив создан в


11 каком-то другом месте кода
int [] [] arrayOfArraysOfint; / / и этот массив создан в
11 каком-то другом месте кода
11 Строки присваиваются переменным типа Object, поэтому и
11 массив типа String[] присваивается массиву типа Object[]:
Object[] оа = arrayOfStrings;

//В классе String реализуется интерфейс ComparaЫe,


//поэтому массив типа String[] можно считать
//относящимся к типу ComparaЬle[]:
ComparaЫe[] са= arrayOfStrings;

//Массив типа int[] расширяет тип Object, поэтому и


//массив типа int[] [] присваивается массиву типа Object[]:
Object[] оа2 = arrayOfArraysOfint;

//Все массивы клонируются и сериализируются как объекты


11 Object:
типа
Object arrayOfStrings;
о =
CloneaЫe с = arrayOfArraysOfint;
SerializaЫe s = arrayOfArraysOfint[OJ;

Массивы 139
Такое расширяющее преобразование типов называется кова­
риантностью массивов и считается по современным меркам

историческим пережитком и ошибочным средством из-за несо­


впадения типов, проявляющегося во время компиляции и вы-

полнения.

Как правило, компилятор должен вводить проверки на совпадение типов

перед выполнением любой операции по сохранению значения ссылочного


типа в элементе массива, чтобы тип этого значения совпал с типом элемента
массива во время выполнения. Если же проверка на совпадение типов во вре­
мя выполнения завершится неудачно, будет сгенерировано исключение типа
ArrayStoreException.
Синтаксис массивов, совместимый с языками С и С++

Как пояснялось ранее, для обозначения типа массива достаточно указать


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

Это правило распространяется на локальные переменные, поля и параметры

методов. Например:

// В следующей строке кода объявляются локальные


// переменные типа int, int [] и int [] []:
int justOne, arrayOfThem[], arrayOfArrays [] [];

//В трех следующих строках кода объявляются элементы

//массива одного и того же типа:


puЬlic String[] [] aasl; // предпочтительный в Java синтаксис
puЫic String aas2 [] []; // синтаксис, совместимый с С и С++
puЫic String[] ааsЗ[]; //неясный гибридньм синтаксис

// В сигнатуре следующего метода указаны два параметра


// одного и того же типа:
puЫic static douЬle dotProduct(douЬle[] х, douЬle у[])

{ ." }

Такая совместимость синтаксиса - крайне редкое явление, и по­


этому пользоваться им не рекомендуется.

140 Глава 2. Основы синтаксиса Java


Создание и инициализация массивов

Чтобы создать массив в Java, следует воспользоваться ключевым словом


new, как и при создании объекта. У типов массивов отсутствуют конструк­
торы, но при создании массива требуется указывать его длину. Требующийся
размер массива можно задать в виде неотрицательного целого числа в ква­

дратных скобках:

11 создать новый массив для хранения 1024 байтов:


byte[] buffer = new byte[1024];

11 создать массив из 50 ссьток на строки:


String(] lines = new String[50];

Когда массив создается с помощью упомянутого выше синтаксиса, каждый


его элемент автоматически инициализируется тем же значением по умолча­

нию, что и для полей класса: логическим значением false для элементов типа
boolean, символьным значением \uOOOO для элементов типа char, значени­
ем О для целочисленных элементов, значением О •О для числовых элементов с
плавающей точкой и пустым значением null для элементов ссылочного типа.
Выражения для создания массивов могут также использоваться для созда­
ния и инициализации многомерных массивов. Но соответствующий синтак­

сис оказывается несколько более сложным, и поэтому он поясняется далее


в этом разделе.

Инициализация массивов

Чтобы создать массив и инициализировать его элементы в одном выраже­


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

String(] greetings = new String(]


{ "Hello", "Hi", "Howdy" );
int[] smallPrimes = new int(]
{ 2, з, 5, 7, 11, 13, 17, 19, );

Обратите внимание на то, что упомянутый выше синтаксис допускает соз­


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

Массивы 141
можно считать литералами массивов. Ниже приведены характерные тому
примеры.

!/ вызвать метод, передав ему литерал анонимного массива,


!/состоящего из двух символьных строк:
Striпg respoпse = askQuestion("Do you want to quit?",
new String(] {"Yes", "No"});

11 вызвать еще один метод с анонимным массивом, состоящим


//из анонимных объектов:
douЫe d = computeAreaOfTriangle(new Point[]
{ пеw Point(l,2),
new Point(З,4),
пеw Point(З,2) });

Если инициализатор массива оказывается составной частью объявления


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

String(] greetiпgs = { "Hello", "Hi", "Howdy" };


int[] powersOfTwo = {1, 2, 4, 8, 16, 32, 64, 128};

Литералы массивов создаются и инициализируются при выполнении, а не


при компиляции программы. Рассмотрим в качестве примера следующий ли­
терал массива:

iпt[] perfectNumЬers = {6, 28};


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

iпt[] perfectNumЬers = пеw int[2];


perfectNumЬers [О] 6;
perfectNumЬers(l] = 28;
Тот факт, что в Java вся инициализация массивов выполняется во время
выполнения, имеет важное следствие. Это означает, что выражения в ини -
циализаторе массива могут быть вычислены во время выполнения и совсем
не обязательно должны быть константами во время компиляции. Например:

Po1nt [] points = ( circlel. getCenterPoiпt (), circle2. getCenterPoint () };

Применение массивов
Как только массив будет создан, можно приступить к его применению.
В последующих подразделах поясняются основы доступа к элементам массива

142 Глава 2. Основы синтаксиса Java


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

Доступ к элементам массива

Элементы массива, по существу, являются переменными. Когда элемент


массива появляется в выражении, вычисляется значение, хранящееся в дан­

ном элементе. А когда элемент массива оказывается с левой стороны опе­


рации присваивания, в нем сохраняется новое значение. Но, в отличие от
обычной переменной, у элемента массива отсутствует имя, а имеется лишь
порядковый номер его местоположения в массиве, т.е. индекс. Для доступа к
элементам массива служат квадратные скобки, в которых указывается индекс
массива. Так, если а - это выражение, в котором вычисляется ссылка на мас­
сив, то обращение к конкретному его элементу осуществляется по индексу
а [ i] , где i - целочисленный литерал или выражение, вычисляемое значение
которого относится к типу int. Например:

//создать массив из двух строк:


Str1ng[] responses = new String[2];
responses [О] "Yes"; // установить первый элемент массива
responses[l] = "No"; //установить второй элемент массива

//а теперь вывести значения элементов данного массива:


System.out.println(question +" (" + responses[O] + "/"
+ responses [ 1] + " ) : ") ;

//Ссылка на массив и его индекс могут быть более сложными:


douЫe datum = data.getMatrix()
[data.row() * data.numColumns() + data.column()];

Выражение для индекса массива непременно должно быть типа int или
расширяемого до него типа: или даже
char. Очевидно, что мас­
byte, short
сив нельзя индексировать значением типа boolean, float или douЫe. Не
следует также забывать, что в поле length типа int хранится длина масси­
ва и что количество его элементов не может превышать предельное значение

Integer. МАХ_VALUE. Если попытаться проиндексировать массив в выраже­


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

допустимого диапазона преставления чисел для типа int.


Границы массива

Напомним, что первым в массиве а является элемент а [О] , вторым - эле­


мент а [ 1], а последним - элемент а [а. length-1]. Типичной ошибкой при

Массивы 143
обращении с массивами является употребление индекса, который оказыва­
ется слишком малым (отрицательным индексом) или же слишком большим
(большим или равным длине массива length). В таких языках программиро­
вания, как С или С++, доступ к элементам массива до его начала или после
его конца приводит к непредсказуемому поведению, которое может меняться

в зависимости от конкретного обращения к массиву или платформы. Такие


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

Циклический обход массивов


Для обхода всех элементов массива с целью выполнить над ними некото­
рую операцию обычно организуются циклы. И делается это обычно с помо­
щью оператора цикла for. Так, в следующем примере кода вычисляется сум­
ма всех целых чисел, хранящихся в массиве:

int[] prirnes = { 2, 3, 5, 7, 11, 13, 17, 19, 23 };


int surnOfPrirnes = О;
for(int i = О; i < prirnes.length; i++)
surnOfPrirnes += prirnes[i];

Структура приведенного выше цикла for довольно типичная и ча­


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

for(int р : prirnes) surnOfPrirnes += р;

Копирование массивов

Во всех типах массивов реализуется интерфейс CloneaЫe, и поэто­


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

clone () для копирования массивов гарантируется, что исключение типа

CloneNotSupportedException не будет сгенерировано.

144 Глава 2. Основы синтаксиса Java


int[] data { 1, 2, 3 ) ;
int[] сору (int[]) data.clone();

В методе clone ( ) создается неполная копия массива. Если тип элемента

массива является ссылочным, то копируются только ссылки, а не объекты,


доступные по этим ссылкам. А поскольку копия оказывается неполной,
любой массив можно клонировать, даже если сами его элементы не относятся
к типу CloneaЫe.

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


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

сива. В-третьих, передать целевой массив и его индекс в качестве третьего и

четвертого аргументов. Наконец, указать количество копируемых элементов

в качестве пятого аргумента.

Метод arraycopy () действует правильно даже в том случае, если копии


перекрываются в пределах одного и того же массива. Так, если, "удалив" эле­
мент по индексу О из массива а, требуется сместить элементы между индек­
сами 1иn на одну позицию в начало массива, чтобы они заняли позиции по
индексам от О до n-1, это можно сделать следующим образом:

System.arraycopy(a, 1, а, О, n);

Служебные методы для обработки массивов

В классе j ava. util. Arrays содержится целый ряд статических служеб­


ных методов для обработки массивов. Большинство этих методов в значи­
тельной степени перегружаются, предоставляя версии для массивов каждого
примитивного типа и еще одну версию для массивов объектов. В частности,
методы sort () и ЬinarySearch () отчасти удобны для сортировки масси­
вов и поиска в них. Метод equals () позволяет сравнивать содержимое двух
массивов. А метод Arrays. toString () удобен для преобразования содержи­
мого массива в строку (например, тогда, когда требуется вывести отладоч­
ную или протокольную информацию). В состав класса Arrays входят также

Массивы 145
методы deepEquals (), deepHashCode () и deepToString (),предназначен­

ные для обработки многомерных массивов.

Многомерные массивы
Как пояснялось ранее, тип массива обозначается как тип его элемента с
последующей парой квадратных скобок. Так, массив типа char обозначает­
ся как char [], а массив массивов типа char - как char [] []. Если же сами
элементы массива являются массивами, то такой массив называется много­
мерным. Чтобы обрабатывать многомерные массивы, необходимо знать не­
которые дополнительные их особенности.
Допустим, требуется представить таблицу умножения с помощью много­
мерного массива:

int[] [] products; //таблица умножения

Каждая пара квадратных скобок обозначает одно измерение, поэтому при­


веденный выше массив является двумерным. Для доступа к одному элемен­
ту типа int этого двумерного массива необходимо указать два индексных
значения: по одному на каждое измерение. Значение типа int, хранящееся в
любом отдельном элементе двумерного массива, будет равно произведению
двух индексов, при условии, что данный массив был предварительно иници­
ализирован как таблица умножения. Так, products [ 2] [ 4] будет равно 8, а
products [3] [7] - 21.
Создать новый многомерный массив можно с помощью ключевого слова
new, указав его размер в обоих измерениях. Например:
int[] [] products = new int[lOJ [10];

В некоторых языках программирования такой массив создается в виде од­


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

• Объявляется переменная products для хранения массива массивов


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

каждому элементу типа int каждого из этих новых 10 массивов при­

сваивается значение О.

146 Глава 2. Основы синтаксиса Java


Иными словами, приведенная выше единственная строка кода равнознач -

на следующему фрагменту кода:

int [] [] products new int [ 10] []; 11 создать массив для


11 хранения 10 значений типа int[]
for(int i =О; i < 10; i++) //выполнить цикл 10 раз ...
products[i] = new iпt[lO]; // ... и создать 10 массивов

Операция new выполняет всю эту дополнительную инициализацию ав­


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

float [] [] [] globalTemperatureData =
new float [360] [180) [100];

Применяя операцию new для создания многомерных массивов, совсем не

обязательно указывать размер массива во всех его измерениях - достаточно


сделать это лишь в крайнем слева измерении (или измерениях). Например,
следующие строки кода вполне допустимы:

float[] [] [] globalTemperatureData new float[360] [] [];


float[] [] [] globalTemperatureData = new float[360] [180] [];

В первой из приведенных выше строк кода создается одномерный массив,


где каждый элемент может содержать двухмерный массив float [] []. А во
второй строке кода создается двухмерный массив, где каждый элемент явля­
ется массивом float []. Но если требуется указать размер только в некото­
рых измерениях многомерного массива, то эти измерения должны быть край­
ними слева. Так, следующие строки кода недопустимы:

float[] [] [] globalTemperatureData =
new float[360][][100]; //Ошибка!
float [] [] [] globalTemperatureData =
new float[] [180] [100]; // Ошибка 1

Как и одномерный, многомерный массив может быть инициализирован с


помощью инициализатора массивов. Чтобы вложить одни массивы в другие,
достаточно указать ряд вложенных соответственно фигурных скобок. Напри­
мер, объявить, создать и инициализировать таблицу умножения 5х5 можно
следующим образом:

int [] [] products = { {о, о, о, о, о)'


{о, 1, 2, 3, 4)'
{о' 2, 4' 6, 8)'
{о, 3' 6, 9, 12)'

{о, 4' 8' 12, 16) );

Массивы 147
А если требуется инициализировать многомерный массив, не объявляя
соответствующую переменную, то можно употребить синтаксис анонимного
инициализатора массива, как показано ниже.

boolean response =
ЫlingualQuestion (question, new String [] []
{{ "Yes", "No" }, { "Oui", "Non" }});

При создании многомерного массива с помощью операции, обозначаемой


ключевым словом new, в качестве надлежащей нормы практики рекомендует­

ся задавать лишь прямоугольные массивы. Такие массивы имеют одинаковый


размер во всех своих измерениях.

Ссылочные типы данных


Рассмотрев массивы и представив классы и объекты, перейдем к более
общему описанию ссылочных типов. Классы и массивы относятся к двум из
пяти разновидностей ссылочных типов в Java. Классы были вкратце пред­
ставлены ранее в этой главе, а более подробно они будут рассматриваться
вместе с интерфейсами в главе 3. Перечислимые и аннотационные типы бу­
дут представлены в главе 4.
В этом разделе не описывается конкретный синтаксис каждого из ссылоч­
ных типов в отдельности, а, напротив, поясняется их общее поведение и де­
монстрируются их отличия от примитивных типов в Java. Термином объект в
этом разделе обозначается значение или экземпляр любого ссылочного типа,
включая массивы.

Сравнение ссылочных типов с примитивными типами


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

• В языке Java определены восемь примитивных типов данных, и про­


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

148 Глава 2. Основы синтаксиса Java


• Примитивные типы данных представляют одиночные значения, ассы­

лочные типы являются составными (или агрегированными), чтобы


хранить нуль и более примитивных значений или объектов. Например,
объект рассматриваемого здесь гипотетического класса Point может
содержать два значения типа douЫe для представления координат х и

у отдельных точек. Массивы типа char [] и Point [] также относятся


к составным типам данных, поскольку в них хранится последователь­

ность примитивных значений типа char или объекты типа Point.


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

хранения этого значения в соответствующем количестве байтов опера­


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

~ 1 Когда объект присваивается переменной или передается методу,


область оперативной памяти, представляющая объект, не копи-
1

руется. Вместо этого сохраняется в переменной или передается


- методу лишь ссылка на данную область памяти.

Ссылки в Java совершенно скрыты, а представление ссылки полностью за­


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

Манипулирование объектами и копиями ссылок


В следующем примере кода демонстрируется манипулирование примитив­
ным значением типа int:

Ссылочные типы данных 149


int х 42;
int у х;

После выполнения приведенных выше строк кода переменная у содер­


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

ния 42.
А теперь выясним, что произойдет, если выполнить аналогичный элемен­
тарный код, но на этот раз воспользоваться ссылочным типом вместо при­

митивного:

Point р new Point(l.O, 2.0);


Point q р;

После выполнения приведенного выше фрагмента кода в переменной q


хранится ссылка на переменную р. А в виртуальной машине Java находится
лишь одна копия объекта типа Point, но теперь - две копии ссылки на этот
объект. И это обстоятельство имеет два важных следствия. Допустим, после
двух приведенных выше строк кода следует такой фрагмент кода:

System.out.println(p.x); //вывести координату х


точки р: 1.0 11
q.x = 13.О; // а теперь изменить координату х точки q
System.out.println(p.x); //вывести координату х точки р
//снова; на этот раз она равна 13.0

В переменных р и q хранятся ссылки на один и тот же объект, поэтому


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

стью объектов, как демонстрируется в следующем фрагменте кода:

11 в переменной greet хранится ссьтка на массив:


char[] greet = { 'h','e','l','l','o' };
char[] cuss = greet; // в переменной сохраняется
/ / та же самая ссьтка
cuss [ 4] = ' ! '; / / воспользоваться этой ссьткой, чтобы
//внести изменения в элемент массива
System.out.println(greet); // вывести строку "hell 1 "

Аналогичное отличие в поведении примитивных и ссылочных типов на­

блюдается и в том случае, когда аргументы передаются методам. Рассмотрим


в качестве примера следующий метод:

150 Глава 2. Основы синтаксиса Java


void changePrimitive(int х) {
while(x > 0) {
System.out.println(x--);

Когда вызывается метод changePrirni ti ve (), в качестве параметра х ему


передается заданная копия аргумента, используемого для вызова данного

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


цикла, который декрементируется до нуля. А поскольку параметр х относит­

ся к примитивному типу, то в теле данного метода используется собственная


копия его значения, что вполне обоснованно.
А с другой стороны, выясним, что произойдет, если модифицировать ме­
тод таким образом, чтобы он принимал параметр ссылочного типа:

void changeReference ( Point р) {


while (р.х > 0) {
System.out.println(p.x--);

Когда вызывается этот метод, ему передается закрытая копия ссылки на


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

Point q = new Point(З.O, 4.5); //точка с координатой х,


11 равной 3
changeReference(q); /!вывести значения 3,2,1 координаты х
//и модифицировать объект типа Point
System.out.println(q.x); //координатах точки q теперь
//равна 01

Когда вызывается методchangeReference (),ему передается копия ссыл­


ки, хранящейся в переменной q. И теперь ссылки на один и тот же объект
содержатся как в переменной q, так и в параметре р. В теле данного метода
может быть использована своя ссылка, чтобы внести изменения в объект. Од­
нако в теле метода changeReference () нельзя изменить содержимое пере­

менной q. Иными словами, в теле данного метода можно коренным образом


изменить объект типа Point, но нельзя изменить тот факт, что переменная q
ссылается на этот объект.

Ссылочные типы данных 151


Сравнение объектов
Как пояснялось ранее, примитивные и ссылочные типы данных суще­
ственно отличаются тем, как они присваиваются переменным, передаются

методам и копируются. Они также отличаются тем, как они сравниваются на


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

сравниваемых значений (т.е. наличие у них совершенно одинаковых битов).


Но если операция== выполняется над ссылочными типами, то в ней сравни­
ваются ссылки, а не сами объекты. Иными словами, в операции == проверя­
ется, делаются ли сравниваемые ссылки на один и тот же объект. Но в ней не
проверяется, имеют ли оба сравниваемых объекта одинаковое содержимое.
Ниже приведен характерный тому пример.

String letter = "о";


String s "hello"; // оба эти объекта типа String
String t = "hell" + letter; //этот объект содержит тот же
//самый текст, что и предыдущий объект,
// тем не менее, они не равны'
if (s == t) System.out.println("equal");

byte[] а (1,2,3);
//копия с одинаковым содержимым:
byte[] Ь (byte[]) a.clone();
// Тем не менее, массивы а и Ь не равны!
if (а == Ь) System.out.println("equal");

Оперируя ссылочными типами, следует иметь в виду две разновидности


равенства, ссылок и объектов, которые важно различать. Это можно, в част­
ности, сделать, описывая словом "одинаковый" равенство ссылок, а словом
"равный" - разные объекты с одним и тем же содержимым. Чтобы прове­
рить два неодинаковых объекта на равенство, следует передать один из них
методу equals () другого объекта:

String letter = "о";


String s "hello"; // оба эти объекта типа String
String t = "hell" + letter; // этот объект содержит тот
/ / же самый текст, что и предьщущий объект,
// о чем свидетельствует метод equals():
if (s.equals(t))
{ System.out.println("equal"); }

152 Глава 2. Основы синтаксиса Java


Все объекты наследуют метод equals () из класса Obj ect, но в стандарт­
ной реализации применяется операция == для проверки ссылок на одинако­

вость, а не на равенство содержимого объектов. В том классе, где требуется


разрешить сравнение объектов на равенство, можно определить свою версию
метода equals (). В рассматриваемом здесь классе Point этого не делает­
ся, но все же делается в классе String, как демонстрируется в приведенном
выше примере кода. Метод equals () можно вызвать и для массива, хотя это
то же самое, что и выполнить операцию ==, поскольку массивы всегда насле­

дуют стандартный метод equals (),сравнивающий ссылки, а не содержимое

массивов. Массивы можно сравнить на равенство и с помощью удобного ме­


тода j ava. util .Arrays. equals ().

Упаковочные и распаковочные преобразования


Примитивные и ссылочные типы данных ведут себя по-разному. Но при­
митивные типы иногда полезно рассматривать как объекты, и поэтому в со­
став платформы Java входят классы-оболочки для каждого из примитивных
типов. Все классы-облолочки Boolean, Byte, Short, Character, Integer,
Long, Float и DouЫe являются неизменяемыми, конечными классами, эк­

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

ние. Как правило, эти классы-оболочки применяются в тех случаях, когда


примитивные значения требуется сохранить в коллекциях вроде списка типа
j ava. util. List.
// создать коллекцию в виде списка целочисленных значений:
List<Integer> numЬers = new ArrayList<>();
// сохранить заключенное в оболочку примитивное значение:
numЬers.add(new Integer(-1));

/!извлечь примитивное значение:


int i = numЬers.get(O) .intValue();

В языке Java допускаются преобразования, называемые упаковочными и


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

передается методу. Распаковочные преобразования выполняются автомати­


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

Ссылочные типы данных 153


упаковка и распаковка выполняются в Java автоматически, данное языковое

средство нередко называют автоупаковкой.


Ниже приведены некоторые примеры автоматических упаковочных и рас­
паковочных преобразований.

Integer i = О; // целочисленный литерал типа int


// упаковывается в объект типа Integer
NumЬer n = O.Of; // числовой литерал с плавающей точкой
// упаковывается в объект типа Float и
11 расширяется до типа NumЬer
Integer i = 1; // это упаковочное преобразование
int j i; // здесь значение переменной i распаковывается
i++; //распакованное значение переменной i сначала
// инкрементируется, а затем снова упаковывается
Integer k = i+2; // значение переменной i распаковывается,
// а полученная сумма снова упаковывается
i null;
i; // здесь распаковка приводит к генерированию
11 исключения типа NullPointerException

Автоупаковка упрощает также обращение с коллекциями. Рассмотрим в


качестве примера применение обобщений (языкового средства Java, о котором
речь пойдет в разделе "Обобщения в Java'' главы 4), чтобы наложить ограни­
чение на те типы данных, которые допускается вводить в списки и другие

коллекции:

// создать список целочисленных значений:


List<Integer> numЬers = new ArrayList<>();
11 упаковать целочисленное значение типа int
11 в объект типа Integer:
numЬers.add(-1);
11 распаковать объект типа Integer в
11 целочисленное значение типа int:
int i = numЬers.get(O);

Пакеты и пространство имен в Java


Пакет представляет собой именованное собрание классов, интерфейсов и
прочих ссылочных типов. Пакеты служат для группирования связанных вме­
сте классов и определения пространства имен для входящих в них классов.

Базовые классы платформы Java собраны в пакеты, имена которых на­


чинаются на j ava. Например, самые основные классы языка Java находятся
в пакете java.lang, различные служебные классы - в пакете java.util,

154 Глава 2. Основы синтаксиса Java


классы для ввода-вывода - в пакете j ava. io, а классы для работы в сети -
в пакетеj а va. net. Некоторые из пакетов состоят из подпакетов вроде j а va.
lang. reflect и j ava. util. regex. Имена пакетов с расширениями плат­
формы Java, стандартизированными компанией Oracle (или первоначально
компанией Sun Microsystems), как правило, начинаются на javax. Некоторые
из этих расширений (например, из пакета j avax. swing и многих его под­
пакетов) были в дальнейшем внедрены в состав базовой платформы. И, на­
конец, на платформе Java принято также несколько утвержденных норм, в со­
ответствии с которыми пакеты именуются по органу стандартизации, где они

были созданы, например org. wЗс и org. orng.


Каждый класс обозначается как простым именем, задаваемым при его
определении, так и полностью уточненным именем, включающим в себя имя
пакета, в который входит этот класс. Например, класс String входит в па­
кет j ava. lang, поэтому его полностью уточненное имя таково: j ava. lang.
String.
Сначала в этом разделе поясняется, как размещать свои классы и интер­
фейсы в пакете и как выбирать имя пакета, не вступающее в конфликт с име­
нем любого другого пакета. Затем поясняется, как выборочно импортировать
имена типов или статические члены в пространство имен, чтобы не набирать
имя пакета вместе с именем применяемого класса или интерфейса.

Объявление пакета
Чтобы задать пакет, в который входит класс, следует указать в объявле­
нии ключевое слово package, которое должно быть указано в первой строке
кода, не считая комментариев и пробелов, программы на Java, сохраняемой
в исходном файле. После ключевого слова package должно быть указано
имя требуемого пакета и точка с запятой. В качестве примера ниже приве­
дена строка кода с такой директивой из исходного файла Java. Все классы,
определенные в этом исходном файле, входят в состав пакета org. apache.
cornrnons. net.
package org.apache.commons.net;

Если в исходном файле Java нет ни одной директивы package, все классы,
определенные в данном файле, входят по умолчанию в состав безымянного
стандартного пакета. В данном случае полностью и не полностью уточненные
имена класса совпадают.

Пакеты и пространство имен в Java 1SS


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

Гnобаnьно уникаnьные имена пакетов


К числу важных функций пакетов относится разделение пространства
имен Java и предотвращение конфликтов имен между классами. Например,
различить классы j ava. util. List и j ava. awt. List можно только по име­

нам их пакетов. Но для этого должны отличаться имена самих пакетов. Как
разработчик Java компания Oracle контролирует имена всех пакетов, начина­
ющиеся на java, javax и sun.
К числу распространенных схем именования относится применение до­
менных имен, элементы которых располагаются в обратном порядке, в каче­
стве префикса для всех имен пакетов. Например, в рамках общего проекта
Apache создается сетевая библиотека как часть конкретного проекта Apache
Commons, доступного по адресу http://commons.apache.org/, а следова­
тельно, для обращения к этой библиотеке служит имя пакета org. apache.
commons. net.
Однако эти правила именования пакетов распространяются в основном на
разработчиков прикладных интерфейсов API. Так, если другие программисты
будут пользоваться классами, разрабатываемыми вместе с иными неизвест­
ными классами, то имена пакетов должны быть глобально уникальными. А с
другой стороны, если разрабатывается приложение Java, ни один из классов
которого не предполагается выпускать для повторного использования други­

ми разработчиками, тогда весь набор классов заранее известен и можно не


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

Импорт ссыnочных типов


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

156 Глава 2. Основы синтаксиса Java


включая имя пакета. Так, если требуется написать код для манипулирования
файлом, используя класс File из пакета java.io, следует ввести имя java.
io . Fi1e. Из этого правила имеются следующие исключения.

• Ссылочные типы из пакета j ava. lang настолько важны и общеупотре­


бительны, что на них можно всегда ссылаться по их простым именам.

• Из исходного кода в ссылочном типе р . Т можно ссылаться на другие


ссылочные типы, определенные в пакете р, по их простым именам.

• На ссылочные типы, импортированные в пространство имен с помо­

щью директивы import, можно ссылаться по их простым именам.

Два первых исключения называются автоматическим импортом. Ссылоч­


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

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

И это делается в объявлении с помощью ключевого слова import.


Объявления с ключевым словом import должны быть сделаны в самом
начале исходного файла Java сразу же после объявления с ключевым словом
package, если таковое делается, но прежде определений любых ссылочных
типов. В исходном файле Java можно сделать сколько угодно объявлений с
ключевым словом import. Объявление, сделанное с помощью ключевого
слова import, распространяется на определения всех ссылочных типов в ис­
ходном файле Java, но ни на одно из последующих объявлений с ключевым
словом import.
У объявления с ключевым словом import имеются две формы. Чтобы
импортировать единственный ссылочный тип в пространство имен, после

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

import java.io.File; // теперь можно ввести имя File


//вместо имени java.io.File

Другая форма объявления с ключевым словом import называется импор­


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

Пакеты и пространство имен в Java 157


типа из данного пакета без его имени. Так, если требуется воспользоваться
рядом других классов из пакета j ava. io, помимо класса File для этой цели
достаточно импортировать весь пакет следующим образом:

import java.io.*; //теперь можно пользоваться простыми


//именами для обозначения всех классов из пакета java.io

Действие синтаксиса импорта ссылочных типов по требованию не распро­


страняется на подпакеты. Так, если импортируется пакет j ava. util, к клас­
су j ava. util. zip. ZipinputStream все равно придется обращаться по его
полностью уточненному имени.

Объявление в форме импорта ссылочных типов по требованию применя­


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

торый фактически применяется в прикладном коде. Данная форма объяв­


ления импорта называется "по требованию" именно потому, что ссылочные
типы импортируются в ней постольку, поскольку они применяются.

Конфликты имен и сокрытие

Объявления с ключевым словом import просто неоценимы для програм­


мирования на Java. Но они открывают возможность для конфликтов имен.
Рассмотрим в качестве примера пакеты j ava. util и j ava. awt, которые со­

держат ссылочные типы по имени List.


В частности, j а v а . u t i 1 . Li s t является важным и часто употребляемым
интерфейсом. А в состав пакета j ava. awt входит целый ряд важных ссылоч­
ных типов, зачастую применяемых в клиентских приложениях, но тип j а v а.
awt. List не относится к их числу, поскольку считается устаревшим. В одном
и том же исходном файле Java не допускается импортировать оба ссылочных
типа, j ava. util. List и j ava. awt. List, поэтому следующие объявления
импорта единственного ссылочного типа приводят к ошибке во время ком­
пиляции:

import java.util.List;
import java.awt.List;

А вот импортировать оба пакета по требованию вполне допустимо:

import java.util.*; //для коллекций и прочих служебных средств


import java.awt.*; //для шрифтов, цветов и графики

158 Глава 2. Основы синтаксиса Java


Но затруднение возникает при попытке воспользоваться ссылочным ти­
пом List. Этот ссылочный тип импортируется по требованию из любого
из двух указанных выше пакетов, поэтому любая попытка воспользоваться
ссылочным типом List, указав его не полностью уточненное имя, приведет
к ошибке по время компиляции. В качестве выхода из затруднительного по­
ложения в данном случае придется явно указать имя требующегося пакета.
Ссылочный тип j ava. util. List употребляется намного чаще, чем ссы­
лочный тип j ava. awt. List, и поэтому оба объявления импорта ссылочных
типов по требованию целесообразно объединить с объявлением импорта
единственного ссылочного типа, чтобы снять неоднозначность при обраще­
нии по имени List, как показано ниже.

import java.util.*; //для коллекций и прочих служебных средств


import java.awt.*; //для шрифтов, цветов и графики
import java.util.List; //чтобы отличить от ссылочного типа java.awt.List

При таких объявлениях в исходном коде можно пользоваться именем


List, имея в виду интерфейс j ava. util. List. Если же требуется восполь­
зоваться классом j ava. awt. List, это можно по-прежнему сделать, дополнив
имя данного класса именем его пакета. В данном случае конфликты имен
между пакетами java. util и java.awt не возникают, а их ссылочные типы
будут импортированы по требованию при обращении к ним без имени их
пакета.

Импорт статических членов

Как и ссылочные типы, статические члены таких типов могут быть так­
же импортированы с помощью ключевых слов import static. (Более под­
робно статические члены поясняются в главе 3. Но если вы еще незнакомы с
ними, то вам, возможно, придется впоследствии вернуться к этому разделу.)
Подобно объявлениям импорта ссылочных типов по отдельности и по требо­
ванию, импорт статических членов можно объявлять с помощью ключевого
слова import в двух формах: единственного статического члена и по требо­
ванию. Допустим, требуется написать программу, ориентированную на тек­
стовый ввод-вывод, отправляющую результаты в стандартный поток вывода
System. out. В таком случае можно объявить импорт единственного статиче­
ского члена, как показано ниже, чтобы сэкономить на вводе исходного текста
данной программы.

import static java.lang.System.out;

Пакеты и пространство имен в Java 159


И тогда можно сделать вызов метода out. println () вместо вызова
System. out. println ().Допустим также, что требуется написать програм­
му, в которой применяется немало тригонометрических и прочих функций,
реализованных в соответствующих методах из класса Ма th. Набирать много­
кратно имя класса Ма th в такой программе, явно ориентированной на чис­
ловые методы, было бы неудобно и только ухудшило бы удобочитаемость ее
исходного кода. В таком случае может вполне подойти объявление импорта
статических членов по требованию:

import static java.lang.Math.*

Благодаря такому объявлению с ключевым словом import можно свобод­


но составлять краткие выражения вроде sqrt ( abs ( s in ( х) ) ) , не предваряя

имя каждого статического метода именем его класса Ма th.

Еще одним важным применением объявлений с ключевыми словами


import static является импорт имен констант в прикладной код. Это осо­
бенно удобно для перечислимых типов (см. главу 4). Допустим, в исходном
коде разрабатываемой программы требуется воспользоваться значениями
следующего перечислимого типа:

package climate.temperate;
enum Seasons { WINTER, SPRING, SUММER, AUTUМN f;
С этой целью можно было бы импортировать сначала перечислимый тип
climate. temperate. Seasons, а затем предварить константы его именем:
Seasons. SPRING. Но ради краткости и удобочитаемости исходного кода
можно импортировать сами значения данного перечислимого типа, как по­

казано ниже. Пользоваться объявлениями импорта статических членов для


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

import static climate.temperate.Seasons.*;

Импорт статических членов и перегружаемые методы

В объявлении импорта статических членов с помощью ключевого слова


import на самом деле импортируется имя, а не конкретный член с этим име­

нем. А поскольку в Java допускается перегрузка методов и одинаковые имена


полей и методов в ссылочных типах, то в объявлении импорта единственного
статического члена можно импортировать не один такой член. Рассмотрим в
качестве примера следующую строку кода:

import static java.util.Arrays.sort;

160 Глава 2. Основы синтаксиса Java


В приведенном выше объявлении в пространство имен импортируется
имя sort, а не 19 методов sort (),определенных в классе j ava. util. Arrays.
Если воспользоваться в исходном коде импортированным именем sort для
вызова одноименного метода, то компилятор проанализирует типы аргумен­

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

ный тому пример.

import static java.util.Arrays.sort;


import static java.util.Collect1ons.sort;

Можно было бы предположить, что такой код приведет к синтаксической


ошибке, но на самом деле этого не произойдет, поскольку методы sort (),
определенные в классе Collections, имеют иные сигнатуры, чем все мето­
ды sort (),определенные в классе Arrays. Если в исходном коде программы
употребляется имя sort, компилятор проанализирует типы аргументов, что­
бы определить, какой из 21-го импортированного метода sort () на самом
деле имелся в виду.

Структура исходного файла Java


Ранее в этой главе синтаксис Java рассматривался от мелких до крупных
элементов, от отдельных символов и лексем до операций, выражений, опера­
торов, методов и вплоть до классов и пакетов. С практической точки зрения

единицей структуры программы на Java, с которой вам чаще всего придется


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

• Дополнительная, хотя и не обязательная директива package.


• Нуль или более директив import или import static.
• Одно или более определений ссылочных типов.

Эти элементы могут, конечно, перемежаться комментариями, но они долж­


ны следовать в исходном файле Java в перечисленном выше порядке. Вот,
собственно, и вся структура исходного файла Java. Все операторы Java, кроме
директив package и import, которые не являются подлинными операторами,

Структура исходного файла Java 161


должны быть указаны в теле методов, а все методы - в определениях ссы­
лочных типов.

На исходные файлы Java накладываются и другие важные ограничения.


Прежде всего, каждый такой файл должен содержать хотя бы один класс
верхнего уровня, объявленный открытым (puЫic), т.е. предназначенным для
применения в других классах из разных пакетов. Такой класс может содер­
жать любое количество вложенных или внутренних классов, которые также
объявлены открытыми. Более подробно модификатор доступа puЫic и вло­
женные классы рассматриваются в главе 3.
Второе ограничение касается имени исходного файла Java. Если исход­
ный файл Java содержит открытый (puЫic) класс, то в качестве его имени
должно быть выбрано имя данного класса, дополненное расширением . j ava.
Так, если класс Point объявлен как puЫic, его исходный код должен при­
сутствовать в файле Point. j ava. Независимо от того, являются ли классы
открытыми, в качестве надлежащей нормы практики программирования ре­
комендуется определять классы по одному на каждый исходный файл Java,
присваивая ему имя данного класса.

Когда компилируется исходный файл Java, каждый определенный в нем


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

. class. Так, если в файле Point. j ava определен класс Point, компилятор
Java скомпилирует его в файл Point. class. В большинстве систем файлы
классов хранятся в каталогах, соответствующих именам их пакетов. Напри­
мер, класс сот. davidflanagan. exaтples. Point определен в файле класса
coт/davidflanagan/exaтples/Point.class.
Исполняющей среде Java известно, где находятся файлы классов, пред­
назначенные для стандартной системы классов, и поэтому они могут быть
загружены по мере необходимости. Когда, например, интерпретатор Java
выполняет программу, в которой требуется воспользоваться классом сот.
davidflanagan. exaтples. Point, ему известно, что исходный код данного

класса находится в каталоге coт/davidflanagan/exaтples/, и по умолча­


нию он "ищет" в текущем каталоге подкаталог с таким же именем. Чтобы
дать интерпретатору Java команду искать класс не в текущем каталоге, а в
других местах, следует указать параметр -classpath при вызове интерпре­

татора Java из командной строки или установить соответственно переменную

162 Глава 2. Основы синтаксиса Java


окружения CLASS РАТН. Подробнее об интерпретаторе Java (исполняемом
файле j ava. ехе) речь пойдет в главе 13.

Опредеnение и выпоnнение проrрамм на Java


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

puЬlic static void main(String[) args)

Метод main () служит главной точкой входа в программу на Java. Именно


здесь интерпретатор Java начинает свое выполнение. Этому методу передает­
ся массив строк, но он не возвращает никакого значения. После возврата из
метода main () интерпретатор Java завершает свое выполнение, если только в
методе main () не были созданы отдельные потоки исполнения. В последнем
случае интерпретатор Java ожидает завершения всех этих потоков, прежде
чем завершить свое выполнение.

Чтобы выполнить программу на Java, следует запустить на выполнение


утилиту j ava (исполняемый файл j ava. ехе) из командной строки, указав
полностью уточненное имя класса, содержащего метод main (). Но следует
иметь в виду, что в командной строке указывается имя класса, а не имя фай­
ла, содержащего этот класс. Любые дополнительные аргументы, указываемые
в командной строке, передаются методу main () в качестве его параметра
типа String [].А для того чтобы сообщить интерпретатору Java, где следует
искать классы, требующиеся в программе, возможно, придется также указать
параметр -classpath (или -ер) в командной строке, как показано ниже.

java -classpath /opt/Jude com.davidflanagan.jude.Judedatafile.jude

Здесь j ava - это команда запуска интерпретатора Java на выполнение;


-classpath /opt/Jude - параметр, сообщающий интерпретатору Java, где
следует искать файлы с расширением . class; сот. davidflanagan. j ude.
Jude - имя выполняемой программы, т.е. имя класса, в котором определен
метод main (); datafile. j ude - строка, передаваемая методу main () в ка­
честве единственного элемента массива объектов типа String.
Программы на Java можно выполнять и более простым способом. Так,
если программа и все ее вспомогательные классы, кроме тех, которые входят

в состав платформы Java, надлежащим образом укомплектованы в архивный

Опредепение и выпопнение программ на Java 163


файл Java (или JАR-файл), то для запуска такой программы на выполнение
достаточно указать имя соответствующего архивного JАR-файла. В следую­
щем примере демонстрируется запуск на выполнение программы Censum,
анализирующей журнал регистрации сборки "мусора":

java -jar /usr/local/Censum/censum.jar

В некоторых операционных системах архивные JАR-файлы создаются ав­


томатически, поэтому для запуска программы Censum на выполнение в таких
системах достаточно ввести приведенную ниже команду. Подробнее о том,
как выполнять программы на Java, - главе 13.
% /usr/local/Censum/censum.jar

Резюме
В этой главе был представлен основной синтаксис языка Java. Вследствие
взаимосвязанного характера синтаксиса языков программирования вполне

возможно, что в данный момент вы чувствуете, что не полностью уяснили

весь синтаксис языка Java. Овладеть любым языком, будь то естественным


или машинным, можно лишь на практике.

Следует также заметить, что одни части синтаксиса Java употребляются


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

ние, перейдем к следующей главе и рассмотрим классы и объекты, которым


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

164 Гnава 2. Основы синтаксиса Java


3
Объектно-ориентированное
проrраммирование на Java

Итак, изложив основы синтаксиса Java, можно приступить к рассмотре­


нию принципов объектно-ориентированного программирования (ООП) на
Java. Во всех программах на Java применяются объекты, а тип объекта опре­
деляется в классе или интерфейсе. Всякая программа на Java определяется
в виде класса, а нетривиальные программы состоят из определений целого

ряда классов и интерфейсов.


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

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


Термин "объектно-ориентированный" имеет разное смысловое
значение в различных языках программирования. Не следует,

в частности, считать, что Java действует таким же образом, как


и излюбленные вами языки ООП. И это особенно касается про ­
граммирующих на С++ или Python.

Краткий обзор кnассов


Классы являются самым основополагающим структурным элементом всех
программ на Java. Не определив класс, нельзя написать программу на Java.
В классах находятся все операторы Java и реализуются все методы.
Основные определения ООП
Ниже приведены самые важные определения для ООП.

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

определяется новый ссылочный тип. Например, в главе 2 был опреде­


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

Объект
• Является экземпляром класса. Так, объект типа Point представляет зна­
чение данного ссылочного типа, т.е. одну точку на двухмерной коорди­
натной плоскости.

Объекты зачастую создаются путем получения экземпляров класса с по­


мощью операции, которая обозначается ключевым словом new и служит для
вызова конструктора данного класса, как показано ниже. Более подробно
конструкторы рассматриваются в разделе "Создание и инициализация объ­
ектов" далее в этой главе.

Point р = new Point(l.O, 2.0);

Определение класса состоит из сигнатуры и тела. В сигнатуре класса


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

Члены могут быть статическими или нестатическими. Статический член


относится к самому классу, тогда как нестатический член связан с экземпля­

рами класса (см. раздел "Поля и методы" далее в этой главе).

Имеются четыре наиболее распространенных члена: поля и ме­


тоды класса, а также поля и методы экземпляра. Большая часть
программирования на Java приходится на взаимодействие с эти­

ми разновидностями членов.

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


Расширяемый класс называется суперклассом, а его расширение - подклассом.

166 Глава 3. Объектно-ориентированное программирование на Java


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

Члены класса могут быть объявлены с модификаторами доступа puЫic,


protected или pri vate 1 • Эти модификаторы определяют видимость, или
доступность, членов класса для его клиентов и подклассов. Благодаря этому
в классах можно управлять доступом к членам, не входящим в состав их от­

крытого прикладного интерфейса API. Такая возможность скрывать члены


способствует применению принципа ООП, называемого инкапсуляцией дан­
ных, как обсуждается в разделе "Сокрытие данных и инкапсуляция" далее
в этой главе.

Другие ссылочные типы

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


или несколько интерфейсов. Интерфейс представляет собой ссылочный тип,
похожий на класс, где определяются сигнатуры методов, но отсутствуют тела

методов для их реализации.

Начиная с версии Java 8 в интерфейсах можно употреблять ключевое сло­


во default для указания на дополнительный, хотя и необязательный метод,
определяемый в самом интерфейсе. Если метод необязательный, в интерфей­
се должна быть определена его реализация по умолчанию (отсюда и выбор
ключевого слова defaul t). Именно эта реализация и будет использоваться
во всех классах, реализующих данных интерфейс, где не предоставляется
своя реализация необязательного метода.
От класса, реализующего интерфейс, требуется предоставить тела методов
интерфейса без реализации по умолчанию. Экземпляры класса, реализующе­
го интерфейс, являются также экземплярами типа этого интерфейса.
Классы и интерфейсы являются самыми важными из всех пяти основных
ссылочных типов данных, определяемых в языке Java. Тремя другими основ­
ными ссылочными типами являются массивы, перечисления и аннотации.

Массивы рассматривались в главе 2. Перечисления являются особой разно­


видностью класса, тогда как аннотации - особой разновидностью интерфей­
са. И те и другие рассматриваются в главе 4, где приводится полное описание
интерфейсов.

1 По умолчанию члены класса доступны также на уровне пакета, как поясняется далее в этой
главе.

Краткий обзор классов 167


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

А если класс реализует один или несколько интерфейсов, то после его имени
или ключевого слова extends указывается ключевое слово implements и да­

лее список имен реализуемых интерфейсов через запятую. Например:

puЫic class Integer extends NumЬer


implements SerializaЬle, ComparaЬle {
11 здесь следуют члены класса

В определение обобщенного класса могут также входить параметры типа


и подстановочные метасимволы (см. главу 4). Кроме того, в объявлениях
классов могут быть указаны ключевые слова, обозначающие модификаторы.
Помимо модификаторов доступа (puЫic, protected и т.д.) в объявлениях
классов могут быть указаны следующие модификаторы.

abstract
• Абстрактным (abstract) называется такой класс, реализация которо­
го является неполной, и поэтому получить его экземпляр нельзя. Лю­

бой класс с одним или несколькими абстрактными методами должен


быть объявлен как abstract. Абстрактные классы более подробно рас­
сматриваются в разделе "Абстрактные классы и методы" далее в этой
главе.

final
• Модификатор final обозначает, что класс не подлежит расширению.
Класс нельзя объявить одновременно как abstract и final.
strictfp
• Если класс объявлен как strictfp, все его методы ведут себя так, как
если бы они были объявлены как strictfp, строго следуя формальной
семантике стандарта на арифметические операции с плавающей точкой.
Этот модификатор применяется крайне редко.

168 Глава З. Объектно-ориентированное программирование на Java


Поля и методы
Класс можно рассматривать как совокупность данных, называемых его со­
стоянием, а также код для оперирования этим состоянием. Данные хранятся

в полях, а оперирующий ими код организуется в методы.


В этом разделе описываются поля и методы - две самые важные разно­

видности членов класса. Поля и методы делятся на две следующие категории:


члены класса, называемые также статическими членами и связанные с самим

классом, а также члены экземпляра, связанные с отдельными экземплярами

класса (т.е. с его объектами). В итоге получаются четыре следующие разно­


видности членов.

• Поля класса. • Поля экземпляра.

• Методы класса. • Методы экземпляра.

Все четыре разновидности членов содержатся в простом определении

класса Circle, демонстрируемом в примере 3.1.

Пример 3.1. Простой класс и его члены

puЫic class Circle {


//Поле класса, в котором хранится полезная константа:
puЫic static fiпal douЫe PI= 3.14159; //

//Метод класса, в котором вычисляется значение на


//основании передаваемых ему аргументов:
puЫic static douЫe radiansToDegrees(douЫe radians)
return radians * 180 / PI;

//Поле экземпляра, определяющее радиус окружности:


puЫic douЫe r;

//Два метода экземпляра, оперирующие полями


//экземпляра объекта данного класса:
puЬlic douЫe area() {
// вычислить площадь круга:
return PI * r * r;

puЬlic douЫe circumference() {


// вычислить длину окружности:
return 2 * PI * r;

Поля и методы 169


Как правило, делать поле r открытым (puЬlic) не рекоменду­
ется. Вместо этого поле r и метод radius () лучше сделать за­
крытыми (pri vate), чтобы ограничить доступ к данному полю.
Причины для этого будут пояснены ниже, в разделе "Сокрытие
данных и инкапсуляция", а до тех будут использоваться откры­
тые поля, чтобы продемонстрировать на конкретных примерах,
как обращаться с полями экземпляра.

Все четыре разновидности членов более подробно разъясняются в после­


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

Синтаксис обьявnения полей


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

Перед типом поля может быть указано от нуля и больше модификаторов


или аннотаций, а после имени поля - знак равенства и инициализирующее
выражение, предоставляющее начальное значение поля. Если же два поля или

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

int х = 1;
private String name;
puЬlic static final int DAYS_PER_WEEK = 7;
String[] daynames = new String[DAYS_PER_WEEK];
private int а= 17, Ь = 37, с= 53;

Ниже перечислены ключевые слова, обозначающие модификаторы, кото­


рых может быть указано от нуля и больше в объявлениях полей.

puЫic,protected,private
Эти модификаторы доступа обозначают, может ли и где именно исполь­
зоваться поле за пределами того класса, в котором оно определено.

170 Глава 3. Объектно-ориентированное программирование на Java


static
Если этот модификатор присутствует в объявлении поля, он обознача­
ет, что данное поле связано с тем классом, в котором оно определено, а

не с каждым экземпляром такого класса.

final
Этот модификатор обозначает, что значение поля после его инициали­
зации вообще не подлежит изменению. Те поля, которые одновременно
объявлены как static и final, являются статическими константами,
которые компилятор может подставить во время компиляции. Конеч­
ные (final) поля могут быть также использованы для создания клас­
сов, экземпляры которых оказываются неизменяемыми.

transient
Этот модификатор обозначает, что поле не является частью постоянно­
го состояния объекта и его не нужно сериализировать вместе с осталь­
ной частью объекта.

volatile
Этот модификатор обозначает, что поле обладает дополнительной се­
мантикой для параллельного применения в двух или более потоках ис­
полнения. Модификатор volatile указывает на то, что значение поля
должно всегда читаться из основной памяти и сбрасываться в нее и что
оно может быть кешировано потоком исполнения в регистре или кеше
ЦП. Подробнее об этом речь пойдет в главе 6.

Поля класса
Поле класса связано с тем классом, в котором оно определено, а не с эк­

земпляром данного класса. Так, в приведенной ниже строке кода объявляется


поле класса PI типа douЬle, которому присваивается значение 3 .14159.
puЬlic static final douЬle PI = 3.14159;

Модификатор доступа static указывает на то, что объявляемое поле от­


носится к категории полей класса. Иногда поля класса называются статиче­

скими, поскольку они объявлены с модификатором доступа static. А моди­


фикатор доступа final указывает на то, что значение поля нельзя переназна­
чить непосредственно. И поскольку поле PI представляет константу, то оно
объявлено как конечное (final), чтобы его нельзя было изменить.

Поля и методы 171


В языке Java (и многих других языках программирования) принято име­
новать константы прописными буквами. Именно поэтому рассматриваемое
здесь поле класса объявлено под именем PI, а не pi. Такой способ опреде­
ления констант в полях класса весьма распространен в Java. Модификаторы
доступа static и final зачастую указываются вместе в объявлениях полей
класса, хотя и не все подобные поля содержат константы. Иными словами,
поле может быть объявлено статическим (static), но совсем необязательно
конечным (final).

Пользоваться открытыми полями, которые не являются конеч­


ными (final), обычно не рекомендуется. Ведь в нескольких по­
токах исполнения может быть предпринята попытка одновре­
менно обновить такие поля. А отладить такое поведение при-
кладного кода крайне трудно.

Открытое статическое поле, по существу, является глобальной перемен­


ной. Но имена полей класса уточняются по уникальным именам тех классов,

которые их содержат. Таким образом, язык Java не страдает недостатком кон­


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

В отношении статических полей очень важно понять, что существует лишь


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

членами класса Circle, данное поле доступно по имени Circle. PI.

Методы класса
Подобно полям класса, методы класса объявляются с помощью модифи­
катора доступа static. Так, в приведенном ниже примере кода демонстриру­
ется объявление метода класса radiansToDegrees ().У этого метода имеется
единственный параметр типа douЬle, а возвращается из него значение того

же самого типа douЬle.

172 Глава 3. Объектно-ориентированное программирование на Java


puЬlic static douЫe radiansToDegrees(douЫe rads) {
return rads * 180 / PI;

Как и поля класса, методы класса связаны с тем классом, где они опреде­
лены, а не с объектом данного класса. Если метод класса вызывается из при­
кладного кода, находящегося за пределами данного класса, в таком случае

следует указать как имя класса, так и имя вызываемого метода. Например:

11 Сколько градусов в 2.0 радианах?


douЫe d = Circle.radiansToDegrees(2.0);

Если же метод класса требуется вызвать из того класса, в котором он опре­


делен, то указывать имя класса при этом не нужно. Чтобы сократить объем
набираемого исходного кода, можно также воспользоваться статическим
импортом, как пояснялось в главе 2.
Обратите внимание на то, что в теле рассматриваемого здесь метода
Circle. radiansToDegrees () применяется поле класса PI. В методе класса
можно применять любые поля и методы как их собственного класса, так и
любого другого.
В методе класса нельзя применять любые поля или метода экземпляра,
поскольку методы класса не связаны с экземпляром класса. Иными словами,
в теле метода radiansToDegrees (), определенного в классе Circle, нельзя

применять ни одну из частей объектов типа Circle.

Упомянутое выше правило можно пояснить и таким образом: на


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

них отсутствует ссылка this, а следовательно, и доступ к полям

экземпляра.

Как упоминалось выше, поле класса, по существу, является глобальной


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

Поля и методы 173


Поля экземпляра

Любое поле, объявленное без модификатора доступа static, является по­


лем экземпляра, как показано ниже.

puЬlic douЬle r; //радиус окружности

Поля экземпляра связаны с экземплярами класса, и поэтому у каждого


создаваемого объекта типа Circle имеется своя копия поля r типа douЫe.
В рассматриваемом здесь примере поле экземпляра r представляет радиус
конкретной окружности. Каждый объект типа Circle может содержать ра­
диус окружности независимо от всех остальных объектов типа Circle.
В определении класса обращение к полям экземпляра осуществляется
только по имени. Характерный тому пример можно обнаружить в теле ме­
тода экземпляра circumference (). А во внешнем коде имя вызываемого

метода экземпляра предваряется ссылкой на объект того класса, где этот ме­
тод содержится. Так, если в переменной с хранится ссылка на объект типа
Circle, то для обращения к радиусу окружности служит выражение с. r, как
демонстрируется в следующем примере кода:

Circle с=new Circle(); //создать объект типа Circle;


// сохранить ссыпку на него в переменной с
c.r = 2.0; // присвоить значение полю экземпляра r
//данного объекта
Circle d = new Circle(); //создать другой объект
11 типа Circle
d.r = c.r * 2; //увеличить вдвое радиус данной окружности

Поля экземпляра играют ключевую роль в ООП. В этих полях хранится


состояние объекта, а по их значениям можно отличить один объект от дру­
гого.

Методы экземпляра
Метод экземпляра оперирует конкретным экземпляром класса (т.е. его
объектом), поэтому любой метод, не объявленный с ключевым словом
static, автоматически становится методом экземпляра. ООП вызывает
особый интерес благодаря методам экземпляра. Так, в классе Circle, опре­
деленном в примере 3.1, содержатся два метода экземпляра, area () и cir
cumference (),в которых вычисляется и возвращается площадь и длина

соответственно окружности, определяемой в объекте типа Circle.

174 Глава З. Объектно-ориентированное программирование на Java


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

11 создать объект типа Circle; сохранить ссылку на


11 него в переменной с:
Circle с= new Circle();
c.r = 2.0; 11 задать значение в поле экземпляра
11 данного объекта
douЫe а= c.area(); 11 вызвать метод экземпляра
11 данного объекта

Объектно-ориентированное программирование называется так


потому, что оно сосредоточено на объекте, а не на вызове
функции.

Естественно, что из метода экземпляра доступны все поля экземпляра,


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

Все методы экземпляра реализуются вместе с неявным параметром, кото­


рого не видно в сигнатуре метода. Неявный аргумент thi s содержит ссыл­
ку на объект, по которой вызывается метод экземпляра. В рассматриваемом
здесь примере таким объектом является экземпляра класса Circle.

В телах методов area () и circumference () применяется поле


класса PI. Как упоминалось ранее, в методах класса могут ис­
пользоваться только поля и методы данного класса, но не поля

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


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

любой член класса, независимо от того, объявлен ли он статиче­


ским (static).

Принцип действия ссылки this


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

Поnя и методы 175


То же самое происходит и в том случае, когда в одном методе экземпляра

класса вызывается другой метод экземпляра. И это принято толковать как

"вызов метода экземпляра для текущего объекта".


Но ключевое слово this может быть использовано и явным образом,
когда требуется прояснить, что метод получает доступ к своим полям и/или
методам. Например, метод area ( ) можно переписать таким образом, чтобы
воспользоваться в нем явной ссылкой this для обращения к полям экзем­
пляра, как показано ниже.

puЬlic douЫe area()

{ return Circle.PI * this.r * this.r; }

В приведенном выше объявлении метода area ( ) имя класса используется


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

поля совпадает с именем параметра или локальной переменной.

Например, в класс Circle можно ввести следующий метод:

puЫic void setRadius (douЬle r) {


this.r = r; //присвоить значение аргумента (r)
// полю по ссылке this.r
11 При этом следует иметь в виду, что написать просто
// операцию присваивания r = r нельзя

Некоторые разработчики намеренно выбирают такие имена для аргумен­


тов метода, чтобы они не совпадали с именами полей, и тогда при обращении
к ним можно вообще обойтись без ссылки this. Тем не менее в методах до­
ступа, автоматически формируемых в основных разновидностях интегриро­
ванных сред разработки (IDE) прикладных программ на Java, зачастую упо­
требляется стиль this. х = х обращения к полям по ссылке this, как проде­
монстрировано в приведенном выше примере кода.

Наконец, следует заметить, что ключевое слово this можно употреблять


в методах экземпляра, но не в методах класса. Дело в том, что методы класса

никак не связаны с отдельными объектами.

176 Глава З. Объектно-ориентированное программирование на Java


Создание и инициализация объектов
Рассмотрев поля и методы, перейдем к другим не менее важным членам
класса. В частности, рассмотрим конструкторы - члены класса, предназначен­
ные для инициализации полей класса при получении его новых экземпляров.
Проанализируем еще раз порядок создания объектов типа Circle:
Circle с= new Circle();

В приведенной выше строке кода нетрудно различить получение нового


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

Конструктор действует следующим образом. Операция new указывает на


то, что в ней требуется получить новый экземпляр класса. Сначала для хра­
нения нового экземпляра объекта выделяется оперативная память. Затем вы­
зывается тело конструктора с любыми указанными аргументами. Эти аргу­
менты применяются в конструкторе для выполнения любой инициализации
нового объекта, которая может потребоваться.
У всякого класса в Java имеется хотя бы один конструктор, предназначен­
ный для выполнения любой необходимой инициализации нового объекта.
Если программист не определит явным образом конструктор класса, компи­
лятор j avac автоматически создаст конструктор по умолчанию, не прини­
мающий никаких аргументов и не выполняющий никакой особой инициали­
зации. Именно такой механизм применяется в классе Circle из примера 3.1
для автоматического объявления конструктора.

Определение конструктора
Для объектов типа Circle требуется некоторая очевидная инициализа­
ция, поэтому определим соответствующий конструктор. В примере 3.2 де­

монстрируется новое определение класса Circle, содержащего конструктор,

позволяющий задать радиус нового объекта типа Circle. В данном примере


предпринята также попытка сделать поле r защищенным, чтобы предотвра­
тить доступ к нему из произвольных объектов.

Пример 3.2. Конструктор класса Circle


puЫic class Circle (
puЫic static final douЫe PI = 3.14159; //константа

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


11 Поле экземпляра, в котором хранится радиус окружности
protected douЫe r;

11 Конструктор: инициализировать поле радиуса


puЫic Circle(douЫe r) { this.r = r;}

11 Методы экземпляра: вычислить площадь и длину


11 окружности на основании ее радиуса
puЫic douЫe circumference ( 1 { return 2 * PI * r; }
puЫic douЫe area () { return PI * r* r; }
puЫic douЫe radius () { return r; }

Если полагаться только на конструктор по умолчанию, предоставляемый


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

Circle с= new Circle();


c.r = 0.25;

С помощью нового конструктора можно перенести инициализацию на


стадию создания объекта, как показано ниже.

Circle с= пеw Circle(0.25);

Ниже приведены основные положения, касающиеся именования, объявле­


ния и написания конструкторов.

• Имя конструктора всегда должно совпадать с именем класса.

• Конструктор объявляется без возвращаемого типа (и даже заполнителя


типа void).
• Тело конструктора составляет код, в котором инициализируется объ­
ект. Это можно сравнить с установкой содержимого ссылки this.
• Конструктор не возвращает ссылку this (или любое другое значение).

Определение нескольких конструкторов


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

чанию. В качестве примера ниже показано, каким образом определяются два


конструктора класса Circle.

178 Глава 3. Объектно-ориентированное программирование на Java


puЫic Circle() { r = 1.0; }
pt1Ьlic Circle(dot1Ьle r) { this.r = r;}

В классе Circle определено лишь одно поле экземпляра, и поэтому имеет­


ся не так уж и много способов для его инициализации. Но в более сложных
классах нередко оказывается удобнее определить для этой цели разные кон­
структоры.

Для класса вполне допускается определить несколько конструкторов, при


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

Вызов одного конструктора из другого


Ключевое слово this находит специальное применение, когда у класса
имеется несколько конструкторов. В таком случае им можно воспользоваться
в конструкторе, чтобы вызвать из него один из других конструкторов того же
самого класса. Иными словами, оба приведенных выше конструктора класса
Circle можно переписать следующим образом:

//Это основной конструктор, предназначенный для


//инициализации поля, хранящего радиус окружности:
puЬlic Circle(dot1Ьle r) { this.r = r; }
//А в этом конструкторе ссылка this() служит для
// вызова приведенного вьШiе конструктора:
pt1Ьlic Circle() { this(l.0); }

Такая методика удобна, когда целый ряд конструкторов разделяют значи­


тельную часть кода инициализации, поскольку при этом исключает повторе­

ние подобного кода. В более сложных случаях, когда в конструкторах делает­


ся немало операций по инициализации, такая методика приносит немалую

пользу.

На применение вызова по ссылке this () накладывается следующее важ­


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

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


Устанавливаемые по умолчанию значения
и инициализаторы полей
Поля класса совсем не обязательно требуется инициализировать. Если
первоначальные значения в полях не указаны, они автоматически инициа­

лизируются устанавливаемым по умолчанию логическим значением false,


кодовым значением \uOOOO, числовым значением О или О. О или же пустым
значением null в зависимости от их типа (подробнее об этом см. в табл. 2.1).
Эти устанавливаемые по умолчанию значения указаны в спецификации язы­
ка Java и применяются как к полям экземпляра, так и к полям класса.

Устанавливаемые по умолчанию значения, по существу, служат


"естественной" интерпретацией нулевой комбинации двоичных
1

, разрядов для каждого типа данных.

Если значение, устанавливаемое в поле по умолчанию, не подходит для


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

puЫic static final douЫe PI = 3.14159;


puЫic douЫe r = 1.0;

Объявления полей не являются частью ни одного из методов. Вместо это­


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

puЬlic class SampleClass {


puЬlic int len = 10;
puЫic int[] tаЫе = new int[len];
puЬlic SampleClass () {

for(int i = О; i < len; i++) taЬle[i] i;


}
11 Остальная часть данного класса опущена ...

В данном случае код, генерируемый компилятором j avac для конструкто­


ра, по существу, равнозначен следующему фрагменту кода:

180 Глава 3. Объектно-ориентированное программирование на Java


puЬlic SampleClass() {
len 10;
=
tаЫе = new int[len];

for(int i = О; i < len; i++) taЬle[i] i;

Если конструктор начинается с вызова по ссылке this () второго кон­


структора, то код инициализации поля не появляется в первом конструкторе.

Вместо этого инициализация выполняется в конструкторе, вызываемом по


ссылке this ().
Итак, если поля экземпляра инициализируются в конструкторе, то где же

инициализируются поля класса? Эти поля связаны с классом, даже если не


получается ни один из экземпляров класса. Логически это означает, что они
должны быть инициализированы еще до вызова конструктора.
Для поддержки такой инициализации полей компилятор j avac автомати­

чески генерирует метод инициализации каждого класса. Поля класса инициа­

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

Метод инициализации класса является внутренним методом, скрытым

от программирующих на Java. В файле класса он носит имя <clini t> (в чем


можно убедиться, проанализировав, например, файл любого класса с помо­
щью утилиты j avap; подробнее о том, как пользоваться данной утилитой, -
в главе 13).
Бnоки инициаnизации

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


ражениях для инициализации их полей и в произвольном коде их конструк­

торов. В классе имеется метод его инициализации, похожий на конструктор,

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

Создание и инициализация объектов 181


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

двух полей класса.

//Нарисовать контур окружности можно с помощью


//тригонометрических функций. Но эти функции действуют
//медленно, и поэтому ряд значений можно вычислить заранее:
puЬlic class TrigCircle {
//Ниже приведены статические таблицы поиска и их
// собственные инициализаторы:
private static final int NUMPTS = 500;
private static douЫe sines[] = new douЬle[NUMPTS];
private static douЬle cosines[] = new douЬle[NUMPTS];

//Ниже приведен статический инициализатор, заполняющий


11 массивы:
static {
douЬle х = О. О;
douЫe delta х = (Circle.PI/2)/(NUMPTS-1);
for(int i = О, х = О.О; i < NUMPTS; i++, х += delta х)

sines[i] = Math.sin(x);
cosines[i] = Math.cos(x);

//Остальная часть данного класса опущена ...

У класса может быть любое количество статических инициализаторов.


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

Подклассы и наследование
Ранее в этой главе был определен простой класс Circle, в объектах кото­
рого окружности различаются только по радиусам. Но допустим, требуется
представить окружности, имеющие как размер, так и положение. Например,

окружность радиусом 1. О и центром в точке с координатами 0,0 на прямо­

угольной плоскости координат отличается от окружности радиусом 1. О и

182 Глава З. Объектно-ориентированное программирование на Java


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

Расширение класса
В примере 3.3 демонстрируется, как реализовать класс PlaneCircle в ка­
честве производного от класса Circle.

Пример 3.3. Расширение класса Circle


puЫic class PlaneCircle extends Circle
11 Поля и методы автоматически наследуются из
11 класса Circle, поэтому здесь достаточно
11 внедрить новое содержимое и, в частности, новые
11 поля экземпляра, в которых хранится центральная
/! точка окружности:
private final douЫe сх, су;

11 Новый конструктор для инициализации новых полей,


11 в котором применяется специальный синтаксис для
11 вызова конструктора Circle():
puЫic PlaneCircle(douЫe r, douЬle х, douЬle у) {
super(r); //вызвать конструктор суперкласса Circle()
this.cx х; //инициализировать поле экземпляра сх
this.cy =у; //инициализировать поле экземпляра су

puЬlic douЫe getCentreX () {


return сх;

puЬlic douЬle getCentreY () {


return су;

11 Методы area () и circumference () наследуются


11 из класса Circle. Новь~ метод экземпляра, в котором

Подклассы и наследование 183


//проверяется, находится ли заданная точка внутри
// окружности. Обратите внимание на то, что в нем

//применяется поле экземпляра r:


puЫic boolean isinside(douЫe х, douЬle у)
11 расстояние от центра:
douЫe dx = х - сх, dy = у - су;

//теорема Пифагора:
douЫe distance = Math.sqrt(dx*dx + dy*dy);
//возвратить логическое значение true или false:
return (distance < r);

Обратите внимание на применение ключевого слова extends в первой


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

В определении метода islnside () демонстрируется наследование полей.


В этом методе применяется поле r, определяемое в классе Cirele, как если

бы оно было определено непосредственно в классе PlaneCirele. Кроме того,


в классе PlaneCirele наследуются методы из класса Cirele. Так, если в пе­
ременной ре хранится ссылка на объект типа PlaneCirele, то вполне допу­
стимо написать приведенное ниже выражение, как если бы методы area () и
eireumferenee () были определены в самом классе PlaneCirele.
douЫe ratio = pc.circumference() / pc.area();

Еще одна особенность подклассификации состоит в том, что каждый


объект типа PlaneCirele является в то же время совершенно допустимым
объектом типа Cirele. Так, если в переменной ре хранится ссылка на объ­
ект типа PlaneCirele, ее содержимое можно присвоить переменной типа
Cirele и вообще забыть о дополнительных возможностях расположения
окружности, как показано ниже.

//Окружность единичного радиуса в начале координат:


PlaneCircle ре= new PlaneCircle(l.0, О.О, О.О);
Circle с= ре; //допускается присваивание переменной
//типа Circle без приведения типов

Такое присваивание объекта типа PlaneCirele переменной типа Cirele


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

184 Глава З. Объектно-ориентированное лрограммирование на Java


объектом типа PlaneCircle. Но об этом компилятору может быть неизвест­
но доподлинно, и поэтому он не позволяет выполнить противоположное (су­
жающее) преобразование без приведения типов, как показано ниже. Более
подробно об этом речь пойдет в разделе "Вложенные типы данных" главы 4,
где поясняется порядок различения типов объектов во время компиляции и
выполнения.

11 Сужающие преобразования требуют приведения типов


11 (и проверки во время выполнения в виртуальной
11 машине Java)
PlaneCircle рс2 = (PlaneCircle) с;
boolean origininside = ( (PlaneCircle) с) .islnside(O.O, О.О);

Конечные классы

Если класс объявляется с модификатором доступа final, это означает, что


он является конечным и его нельзя расширить или подвергнуть подкласси­

фикации. Характерным тому примером служит конечный класс j а v а. lang.


String. Объявление класса конечным (final) предотвращает его нежела­
тельные расширения. Так, если вызывается метод для объекта типа String,
то заранее известно, что он определен в самом классе String, даже если объ­
ект типа String передается из неизвестного внешнего источника.

В общем, многие классы, создаваемые в Java, должны быть конечными.


Разработчики должны тщательно обдумать, стоит ли разрешать другому (воз­
можно, неизвестному) коду расширять их классы. И если этого не стоит де­
лать, то они должны объявить свои классы как final, запретив тем самым
действие механизма расширения.

Суперклассы, класс Obj ect и иерархия классов


В рассматриваемом здесь примере класс PlaneCircle является подклас­
сом, производным от класса Circle. Иными словами, класс Circle является
суперклассом для класса PlaneCircle. Суперкласс отдельного класса опреде­
ляется во вспомогательном операторе extends его объявления, как показано
ниже, причем у класса может быть лишь один непосредственный суперкласс.

puЫic class PlaneCircle extends Circle { ... }

У всякого класса, создаваемого программистом, имеется свой суперкласс.


Если суперкласс не указан во вспомогательном операторе extends, то им ста­
новится класс j ava. lang. Obj ect.

Подклассы и наследование 185


Таким образом, у класса Obj ect имеется особое назначение по следующим
причинам:

• Это единственный класс в Java, у которого отсутствует свой суперкласс.

• Все классы Java наследуют (прямо или косвенно) методы из класса


Object.
В связи с тем что у каждого класса, кроме класса Obj ect, имеется свой

суперкласс, все классы в Java образуют иерархию. Такая иерархия может быть
представлена в виде дерева, в корне которого находится класс Ob j ect.

Если у класса Object нет своего суперкласса, то у каждого из


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

На рис. 3.1 приведено схематическое представление частичной иерар­


хии классов, в которую входят рассматриваемые здесь классы Circle и
PlaneCircle, а также некоторые стандартные классы из прикладного интер­

фейса Java API.

Object -----1 PlaneCircle

----..----1 lnputStreamReader FileReader

FilterReader

StringReader

Рис. 3.1. Схематическое представление иерархии классов в f ava

Конструкторы подклассов

Рассмотрим снова конструктор PlaneCircle () из примера 3.3:


puЫic PlaneCircle ( douЫe r, douЫ e х, douЫe у ) {
super(r); //вызвать констру кто р супе рклас са Circl e()
this.cx х; // инициализирова т ь поле экземпл яр а сх
this.cy = у; // инициализирова ть поле экземпл яра су

186 Глава З. Объектно-ориентированное программирование на Java


Несмотря на то что в этом конструкторе явным образом инициализиру­
ются поля сх и су вновь определяемого экземпляра класса PlaneCircle, для
инициализации полей, наследуемых этим классом, он полагается на конструк­
тор суперкласса Circle ().В частности, чтобы обратиться к конструктору су­
перкласса, в рассматриваемом здесь конструкторе делается вызов super ().
Ключевое слово super относится к числу зарезервированных в Java. К его
назначениям относится, в частности, вызов конструктора суперкласса из

конструктора подкласса. Это можно сравнить с вызовом по ссылке this ()


одного конструктора из другого конструктора того же класса. На вызов кон­
структора по ссылке super () накладываются такие же ограничения, как и на

вызов конструктора по ссылке thi s ( ) .


• Вызов конструктора суперкласса по ссылке super () можно делать
только в конструкторе подкласса.

• Вызов конструктора суперкласса должен быть первым оператором в


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

Аргументы, передаваемые при вызове по ссылке super (),должны соот­


ветствовать параметрам конструктора суперкласса. Если же в суперклассе
определен не один, а несколько конструкторов, то по ссылке super () можно

вызвать любой из них, передав соответствующие аргументы.

Вызов конструкторов по цепочке


и конструктор по умолчанию

В языке Java гарантируется вызов конструктора класса всякий раз, когда


получается экземпляр данного класса или любого его подкласса. В последнем
случае должно быть гарантировано, что в конструкторе каждого класса мо­
жет быть вызван конструктор его суперкласса.
Так, если в первом операторе конструктора не делается явный вызов дру­
гого конструктора по ссылке this () или super (), то компилятор j avac ав­
томатически вставит вызов super () (т.е. вызовет конструктор суперкласса
без аргументов). Если же в суперклассе недоступен конструктор без аргумен -
тов, то такой неявный вызов приведет к ошибке компиляции.
Рассмотрим, что происходит при получении нового экземпляра класса

PlaneCircle.

Подклассы и наследование 187


1. Сначала вызывается конструктор класса PlaneCircle.
2. В этом конструкторе делается явный вызов super (r) конструктора

класса Circle.
3. Затем в конструкторе Circle () делается явный вызов super () кон­
структора его суперкласса Obj ect, у которого имеется единственный

конструктор.

4. В итоге достигнута вершина иерархии классов, после чего начинается


выполнение конструкторов.

5. Сначала выполняется тело конструктора класса Obj ect.


6. После возврата из этого конструктора выполняется тело конструктора

Circle ().
7. После возврата из вызова super (r) выполняются остальные операто­
ры в теле конструктора PlaneCircle ().
Все это означает, что конструкторы вызываются по цепочке. Всякий раз,

когда создается объект, вызывается последовательность конструкторов, на­


чиная с подкласса и вплоть до класса Obj ect, находящегося на вершине ие­
рархии классов Java.
В связи с тем что конструктор суперкласса всегда вызывается в первом

операторе конструктора его подкласса, тело конструктора класса Obj ect всег­
да выполняется первым, затем тело конструктора его подкласса и далее вниз

по иерархии классов вплоть до конструктора того класса, экземпляр которого

получается в настоящий момент. Всякий раз, когда конструктор вызывается,


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

Конструктор по умолчанию

В приведенном выше описании вызовов конструкторов по цепочке недо­

стает одного звена. Так, если конструктор его суперкласса не вызывается в


конструкторе класса явно, это делается в Java неявно.

Если класс объявляется без конструктора, он неявно дополняет­


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

Так, если не объявить конструктор класса PlaneCircle, этот класс будет


неявно дополнен в Java следующим конструктором по умолчанию:

188 Глава 3. Объектно-ориентированное программирование на Java


puЬlic PlaneCircle () { super () ; }

В общем, если в классе не определен конструктор без аргументов, то во


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

конструктор без аргументов. Так, в классах, объявленных как puЫic, зада­


ются открытые (puЫic) конструкторы. Для всех остальных классов задается
конструктор по умолчанию, объявляемый без всяких модификаторов досту­
па. Такой конструктор доступен по умолчанию.

Если класс объявляется как puЫic, но его экземпляр не допу­


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

В классах, экземпляры которых вообще не допускается получать (напри­


мер, класс java. lang .Math или j ava. lang. System), должен быть определен
закрытый (private) конструктор. Такой конструктор может быть вообще не
вызван за пределами своего класса, но этому препятствует автоматическое

внедрение конструктора по умолчанию.

Сокрытие полей суперкласса


Ради простоты примера допустим, что в классе PlaneCircle требуется
знать расстояние между центром окружности и началом координат (0,0). Для
хранения величины этого расстояния можно ввести еще одно поле экземпля­

ра, как показано ниже.

puЫic douЬle r;

В результате ввода следующей строки кода в тело конструктора вычисля­


ется значение, сохраняемое в поле r:
this.r = Math.sqrt(cx*cx + су*су); 11 теорема Пифагора

Но ведь новое поле r называется так же, как и поле радиуса r из су­
перклассаCircle. В таком случае можно сказать, что поле r из класса
PlaneCircle скрывает поле r из класса Circle. (Данный пример, безуслов­
но, несколько надуман, поскольку новое поле должно на самом деле назы­

ваться distanceFromOrigin - расстояние от начала координат.)

Подклассы и наследование 189


В разрабатываемом коде следует избегать объявления полей с
именами, которые скрывают поля суперкласса. И это почти всег-
1

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

При столь новом определении класса PlaneCircle в выражениях r и


this. r осуществляется обращение к полю из класса PlaneCircle. Как же
тогда обратиться к полю r из класса Circle, где хранится величина радиуса
окружности? В особом синтаксисе для этой цели применяется ключевое ело-
во super:
r 11 обозначает поле из класса PlaneCircle
this.r // обозначает поле из класса PlaneCircle
super.r 11 обозначает поле из класса Circle

Еще один способ обратиться к скрытому полю состоит в том, чтобы при­
вести сначала тип ссылки this (или любого экземпляра класса) к соответ­
ствующему суперклассу, а затем получить доступ к данному полю, как демон­

стрируется в следующей строке кода:

( (Circle) this). r // обозначает поле r из класса Circle

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


к скрытому полю, определенному в том классе, который не является непо­

средственным суперклассом. Допустим, во всех классах А, В и С определено


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

х 11 поле х из класса с
this.x 11 поле х из класса с

super.x 11 поле Х ИЗ класса в

((B)this).x 11 поле х из класса в

((А) this) .х 11 поле х из класса А

super.super.x 11 недопустимо; обратиться подобным

11 способом к полю х из класса А нельзя

Обратиться к скрытому полю х из суперкласса другого супер­


класса по ссылке super. super. х нельзя. Такой синтаксис недо­
пустим.

Аналогично, если имеется экземпляр с класса С, обратиться из него к по­


лям х из трех разных классов можно следующим образом:

190 Глава З. Объектно·ориентированное программирование на Java


с.х 11 поле х из класса С
1 (В) с) .х 11 поле х из класса В
((А) с) .х 11 поле х из класса А

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

нет никакой необходимости. Ведь для того чтобы обратиться к полю класса,
можно всегда предварить его именем данного класса. Допустим, реализатор
класса PlaneCircle решил, что поле Circle. PI объявлено с недостаточным
количеством десятичных разрядов числа пи. В таком случае он может опре­
делить свое поле класса PI следующим образом:

puЫic static final douЫe PI = 3.14159265358979323846;

Теперь в исходном коде из класса PlaneCircle может быть использо­


вано более точное значение числа пи в выражениях PI и
PlaneCircle. PI,
а также прежнее, менее точное значение числа пи в выражениях super. PI

и Circle. PI. Но ведь методы area () и circurnference (),унаследованные


в классе PlaneCircle, определены в классе Circle, и поэтому в них приме­
няется значение из поля Circle. PI, несмотря на то, что данное поле скрыто

теперь полем PlaneCircle. PI.

Переопределение методов из суперкласса


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

из суперкласса.
1-------·-1

i ~i Возвращаемый тип переопределяемого метода может относиться


j к подклассу возвращаемого типа исходного метода, а не точно
1
. такого же типа. Это так называемый ковариантный возврат.

Переопределение методов является важной и полезной методикой ООП.


Так, в классе PlaneCircle не переопределяется ни один из методов, опре­
деленных в классе Circle. И на самом деле трудно придумать подходящий
пример, где можно было бы вполне переопределить методы, определенные в
классе Circle.

Подклассы и наследование 191


Не следует поддаваться искушению расширять класс Circle та­
ким подклассом, как, например, Ellipse. Ведь этим фактически
нарушается базовый принцип объектно-ориентированной раз­
работки - рассматриваемый далее принцип подстановки Пи-
сков.

Вместо этого рассмотрим другой пример, в котором переопределение ме­


тодов вполне уместно:

puЫic class Car (


puЬlic static final douЫe LITRE PER lOOKM 8.9;

protected douЫe topSpeed;

protected douЫe fuelTankCapacity;

private int doors;

puЫic Car(douЫe topSpeed, douЫe fuelTankCapacity,


int doors) (
this.topSpeed = topSpeed;
this.fuelTankCapacity = fuelTankCapacity;
this.doors = doors;

puЬlic douЫe getTopSpeed()


return topSpeed;

puЫic int getDoors()


return doors;

puЫic douЫe getFuelTankCapacity()


return fuelTankCapacity;

puЬlic douЬle range() (


return 100 * fuelTankCapacity / LITRE PER 100КМ;

Это несколько более сложный пример, хотя в нем демонстрируются прин -


ципы, положенные в основу переопределения. Наряду с классом Car в дан­
ном примере определяется также специализированный класс SportsCar,
в котором определяется ряд отличий в модели спортивного автомобиля: бен­
зобак фиксированной емкости, двухдверный кузов и повышенная скорость.

192 Глава З. Объектно-ориентированное программирование на Java


Но если скорость спортивного автомобиля превышает предел 200 км/час, то
падает эффективность потребления топлива, а в конечном счете сокращается
общий запас хода, как показано ниже.

puЫic class SportsCar extends Car {

private douЫe efficiency;

puЫic SportsCar(douЫe topSpeed)


super(topSpeed, 50.0, 2);
if (topSpeed > 200.0) {
efficiency 200.0 / topSpeed;
else {
efficiency 1.0;

puЫic douЫe getEfficiency()


return efficiency;

@Override
puЫic douЫe range() {
return 100 * fuelTankCapacity * efficiency
/ LITRE PER 100КМ;

В последующем обсуждении переопределения методов рассматриваются


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

определен. Если принять во внимание имя класса как часть имени вызывае­

мого метода, то имена двух вызываемых методов будут отличаться, а следова­


тельно, ничего, по существу, не скроется.

Прежде чем продолжить обсуждение переопределения методов, следует


пояснить, чем оно отличается от перегрузки методов. Как упоминалось в гла­
ве 2, перегрузка методов является нормой практики определения нескольких
методов с одинаковыми именами, но разными списками параметров в одном

и том же классе. Она заметно отличается от переопределения методов, и по­


этому не следует путать обе эти нормы практики ООП.

Подклассы и наследование 193


Переопределение - это не сокрытие

Несмотря на то что в Java поля и методы трактуются во многом как подоб­


ные члены класса, переопределение методов совсем не похоже на сокрытие

полей. Чтобы обратиться к скрытым полям, достаточно привести их объект к


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

тоды экземпляра подобным способом нельзя. Это коренное отличие наглядно


показано в следующем примере кода:

class А (
11 определить класс А:
int i = 1; 11 поле экземпляра
int f () ( return i; 11 метод экземпляра
static char g() ( return 'А'; ) //метод класса

class В extends А
// определить подкласс, производный от класса А:
int i = 2; // поле i скрывается в классе А
//переопределяет метод f() в классе А:
int f() ( return -i;)
11 скрывает метод класса g() в классе А:
static char g() { return 'В'; )

puЫic class OverrideTest {


puЫic static void main(String args[))
11 создает новые объект типа В:
В Ь = new В ();
// обращается к полю B.i;
// выводит значение 2:
System.out.println(b.i);
11 обращается к методу B.f();
// выводит значение -2:
System.out.println(b.f() );
11 обращается к методу B.g();
//выводит объект типа В:
System.out.println(b.g() );
//лучший способ вызвать метод B.g():
System.out.println(B.g());
// приводит объект Ь к типу экземпляра класса А:
А а = (А) Ь;

// а теперь обращается к полю A.i;


11 выводит значение 1:
System.out.println(a.i);
//по-прежнему обращается к методу B.f();
// выводит значение -2:

194 Глава З. Объектно-ориентированное программирование на Java


System.out.println(a.f());
11 обращается к методу A.g();
11 выводит объект типа А:
System.out.println(a.g());
11 лучший способ вызвать метод A.g():
System.out.println(A.g() );

Несмотря на такое отличие определения методов от сокрытия полей, оно


может показаться на первый взгляд необычным и поэтому требует некоторо­
го разъяснения. Допустим, требуется манипулировать совокупностью объек­
тов типа Car и SportsCar и хранить их в массиве типа
Car [].Это становит­
ся возможным потому, что класс SportsCar является производным от класса
Car, а следовательно, все объекты типа SportsCar оказываются вполне до­
пустимыми объектами типа Car.
При циклическом обходе элементов данного массива можно даже не
знать и вообще не беспокоиться, относятся ли его элементы к типу Car или
SportsCar. Но намного важнее знать, вычисляется ли верное значение в ре­
зультате вызова метода range () для любого элемента массива. Иными сло­
вами, формула для расчета запаса хода не требуется, если текущий объект
фактически представляет спортивный автомобиль!
Самое главное, чтобы при расчете запаса хода объекты, представляющие
типы автомобилей, отвечали своему назначению. Для этого в объектах типа
Car и SportsCar должно использоваться подходящее определение порядка,

в котором следует производить расчет запаса хода. И в этом контексте нет

ничего удивительного в том, что переопределение методов осуществляется в

Java иначе, чем сокрытие полей.

Поиск виртуаnьных методов

Если имеется массив Car [],содержащий объекты типа Car и SportsCar,


то откуда компилятору j avac известно, вызвать ли метод range () из класса
Car или же из класса SportsCar для любого заданного элемента массива?
В действительности компилятору исходного кода ничего об этом неизвестно
во время компиляции.

Вместо этого компилятор javac создает байт-код, в котором используется


поиск виртуальных методов во время выполнения. Когда интерпретатор Java
выполняет прикладной код, он находит подходящий метод range (),который

можно вызвать для каждого объекта, хранящегося в массиве. Это означает,

Подклассы и наследование 195


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

проверяется конкретный тип объекта, доступного по ссылке из переменной


о, а затем находится метод range (),подходящий для данного типа.

В ряде других языков программирования (например, С# или


С++) поиск виртуальных методов по умолчанию не производит­
ся и вместо этого предоставляется ключевое слово virtual, ко­

торое программисты должны употребить явным образом, если


им требуется разрешить переопределение конкретного метода в
подклассах.

В виртуальной машине JVM не используется метод range (),связанный


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

Мы рассмотрели важные отличия определения методов от сокрытия по­


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

class А {
// Поле экземпляра, скрываемое подклассом В:
int i = 1;
//Метод экземпляра, переопределяемый в подклассе В:
int f () ( return i; )

class В extends А {
// Это поле скрывается в классе А:
int i;
int f () {
// Этот метод переопределяет метод f() в классе А:
i = super.i + 1; //подобным способом в нем можно
//извлечь содержимое поля A.i
return super.f() + i; //а таким способом в нем можно
//вызвать метод A.f()

196 Глава 3. Объектно-ориентированное программирование на Java


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

наться именно с этого класса. Но когда метод вызывается по ссылке super,


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

его версия. А если искомый метод наследуется в суперклассе, то вызывается


его наследуемая версия.

Следует, однако, иметь в виду, что по ссылке super вызывается самая не­
посредственно реализуемая версия метода. Допустим, у класса А имеется под­
класс В, а у того - подкласс С и во всех трех классах определен один и тот же

метод f (). По ссылке super. f () в методе С. f () можно вызвать метод В. f () ,


который он определяет непосредственно. Но вызвать непосредственно метод
А. f () из метода С. f () нельзя, поскольку синтаксис super. super. f () в Java
недопустим. Безусловно, если в методе С •f () можно вызвать метод В •f (),
то вполне обоснованно предположить, что из метода В • f ( ) можно вызвать
метод А. f ().
Такого рода вызовы переопределяемых методов по цепочке производятся
относительно часто. Подобным образом можно расширить поведение метода,
не заменяя его полностью.

Не следует путать употребление ключевого слова super для вы­


зова переопределяемого метода с вызовом конструктора из су­

перкласса по ссылке super ().Несмотря на то что в обоих случа­


ях применяется одно и то же ключевое слово, это совсем разные

синтаксисы. В частности, по ссылке super переопределяемый


метод можно вызвать из любого места в переопределяющем его
классе, тогда как по ссылке super () - конструктор из супер­

класса в самом первом операторе вызывающего конструктора.

nодклассы и наследование 197


Не следует также забывать, что по ссылке super переопределяемый метод
можно вызвать только из того класса, в котором он переопределяется. Так,
если имеется ссылка на объект е типа SportsCar, то для объекта е нельзя
вызвать метод range (),определенный в классе Car, из той программы, где
применяется данный объект.

Сокрытие данных и инкапсуnяция


В начале этой главы класс описывался как совокупность данных и мето­
дов. К числу самых важных, не обсуждавшихся до сих пор методик ООП от­
носится сокрытие данных в классе и предоставление доступа к ним только

через методы. Такая методика называется инкапсуляцией потому, что она на­
дежно герметизирует данные (и внутренние методы) в "капсуле" класса, где
они могут быть доступны только доверяемым пользователям (т.е. методам
этого класса).
А зачем вообще требуется это делать~ Наиболее веским на то основани­
ем служит сокрытие внутренней реализации конкретного класса. Если не

дать программистам возможность пользоваться подробностями реализации


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

класс.

Свой код следует инкапсулировать всегда. Ведь почти никогда


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

Еще одним веским основанием для инкапсуляции служит защита конкрет­


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

одного поля, не меняя важные связанные с ним поля и тем самым оставив

класс в несогласованном состоянии. А если вместо этого программисту при­


дется вызвать метод с целью изменить содержимое поля, то в таком методе

должно быть выполнено все необходимое для сохранения согласованного со­


стояния. Аналогично, если в классе определяются некоторые методы только

198 Глава 3. Объектно-ориентированное программирование на Java


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

Инкапсуляцию можно рассматривать и по-другому. Если все данные для


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

возможности тестирования трудно поддаются контролю.

Данный принцип может привести к очень важному выводу, как


станет ясно из раздела "Безопасное программирование на Java"
главы 5 при обсуждении понятия безопасности программ на
Java, которое отличается от понятия типовой безопасности язы-
ка программирования Java.
К другим второстепенным основаниям для сокрытия полей и методов
класса относятся следующие.

• Внутренние поля и методы, недоступные за пределами класса, лишь за­


громождают прикладной интерфейс API. Поэтому, сводя к минимуму
число видимых полей, можно сделать свой класс опрятным, а следова­

тельно, упростить его понимание и пользование им.

• Если метод доступен пользователям вашего класса, вам придется задо­


кументировать его. А скрыв его, вы сэкономите немало времени и труда.

Управление доступом
В языке Java определяются правила управления доступом, накладывающие

ограничение на применение членов класса за его пределами. В целом ряде


примеров, приведенных ранее в этой главе, демонстрировалось применение

модификатора доступа puЫic в объявлениях полей и методов. Ключевыми


словами puЬlic, protected, private и прочими обозначаются модификато­
ры управления доступом, задающие правила доступа к полям и методам, объ­
являемым в классе.

Доступ к модулям

Одним из самых главных нововведений в версии Java 9 стало внедрение


модулей на платформе Java. Они служат для группирования прикладного

Сокрытие данных и инкапсуляция 199


кода, не вмещающегося в один пакет, и рассчитаны на перспективу для раз­

вертывания прикладного кода с целью его неоднократного применения. А

поскольку язык Java нередко применяется в крупных приложениях и средах,


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

тех пор.

Доступ к пакетам

Управление доступом на уровне пакетов не относится непосредственно к


самому языку Java и обычно осуществляется на уровне классов и их членов.

Ё'1
Загруженный пакет всегда доступен для прикладного кода, опре­
деленного в том же самом пакете. А его доступность для при­

- кладного кода из других пакетов зависит от порядка разверты­

вания пакета в операционной системе. Так, если файлы классов,


составляющие пакет, хранятся в каталоге, у пользователя долж­

ны быть права доступа для чтения каталога и находящихся в


нем файлов, чтобы получить доступ к пакету.

Доступ к классам

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

котором они определены. Но если класс верхнего уровня объявлен открытым


(puЫic), то он доступен везде.

1~1 В главе 4 будут рассмотрены вложенные классы, т.е. такие, кота-


! 1 рые могут быть определены как члены других классов. Посколь-
1 ку эти внутренние классы являются членами класса, они подчи­
няются правилам доступа к членам данного класса.

Доступ к членам класса

Члены класса всегда доступны в теле данного класса. По умолчанию члены


класса доступны также в пределах того пакета, в котором определен их класс.

Такой устанавливаемый по умолчанию уровень доступа нередко называют

200 Гnава 3. Объектно-ориентированное программирование на Java


пакетным. Он является лишь одним из четырех возможных уровней досту­
па, а остальные три уровня доступа определяются модификаторами puЫic,
protected и private. Ниже приведен ряд примеров применения этих моди­
фикаторов доступа непосредственно в коде.

11 Этот класс доступен пользователям:


puЫic class Laundrornat (
11 Но они не могут пользоваться этим внутренним полем:
private Laundry[] dirty;
11 В то же время они могут пользоваться этими открытыми
11 методами для манипулирования внутренним полем:
puЬlic void wash () ( . . . }
puЬlic void dry () ( . . . }
11 В подклассе может потребоваться настройка этого поля:
protected int ternperature;

Ниже приведены правила доступа к членам класса.

• Всеми полями и методами класса можно всегда пользоваться в теле са­

мого класса.

• Если член класса объявлен с модификатором доступа puЫic, это оз­


начает, что данный член доступен везде, где доступен содержащий его

класс. Это наименее ограничительный тип управления доступом.

• Если член класса объявлен с модификатором доступа pri vate, то он


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

• Если член класса объявлен с модификатором доступа protected, то он


доступен всем классам в пределах пакета (аналогично доступности в
пакете по умолчанию), а также в теле любого подкласса, производного
от данного класса, независимо от того, в каком именно пакете этот под­

класс определен.

• Если член класса не объявлен ни с одним из упомянутых выше моди­


фикаторов доступа, то он доступен по умолчанию (т.е. обладает пакет­
ным уровнем доступа) для кода во всех классах, определенных в том же
самом пакете, но не доступен за пределами данного пакета.

Доступ по умолчанию является более ограничительным, чем за­


щищенный (protected) доступ, поскольку он запрещается под­
классам за пределами пакета.
-

Сокрытие данных и инкапсуляция 201


Защищенный доступ требует дополнительных пояснений. Допустим, в
классе А объявлено поле х с модификатором доступа protected и этот класс
расширяется классом В, определенным в другом пакете, что очень важно.

Класс В наследует защищенное поле х, и поэтому в его коде можно обратить­


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

защищенных полей из произвольных экземпляров класса А.

Рассмотрим подробнее эту языковую особенность Java на конкретном при­


мере кода. Ниже приведено определение класса А.

package javanut7.ch03;

puЫic class А {
protected final String name;

puЫic A(String named)


name = named;

puЬlic String getName()


return name;

А так выглядит определение класса В:

package javanut7.ch03.different;

import javanut7.ch03.A;

puЫic class в extends А

puЬlic B(String named)


super (named);

@Override
puЫic String getName()
return "В: " + name;

Пакеты Java не допускают вложение, поэтому пакет javanut7.


сhОЗ. dif ferent отличается от пакета j avanut7. сhОЗ. Один из
них не содержится в другом и никак с ним не связан.

202 Глава 3. Объектно-ориентированное программирование на Java


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

puЫic String examine(A а) {


return "В sees: " + a.name;

Если изменить данный метод на следующий:

puЫic String examine(B Ь) {


return "В sees another В: " + b.name;

то компилятор будет вполне удовлетворен, поскольку защищенные поля мо­


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

Управление доступом и наследование

В спецификации языка Java устанавливается следующее.

• Подкласс наследует все доступные ему поля и методы экземпляра из


своего суперкласса.

• Если подкласс определен в том же пакете, что и суперкласс, он наследу­


ет все незащищенные поля и методы экземпляра.

• Если подкласс определен в другом пакете, он наследует все защищен­


ные (protected) и открытые (puЫic) поля и методы экземпляра.

• Закрытые (pri vate) поля и методы вообще не наследуются, как, впро­


чем, и поля или методы класса.

• Конструкторы не наследуются, а вызываются по цепочке, как поясня­


лось ранее в этой главе.

Тем не менее некоторых программистов смущает, что подкласс не наследу­


ет недоступные поля и методы из своего суперкласса. Ведь под этим может

подразумеваться следующее: когда получается экземпляр подкласса, для за­

крытых (pr i v а te) полей, определенных в подклассе, вообще не выделяется


оперативная память, хотя в данном случае преследуется совсем другая цель.

Сокрытие данных и инкапсуляция 203


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

методы.

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

жет вступить в конфликт с утверждением, что члены класса всегда доступны


в теле данного класса. Чтобы прояснить данное недоразумение, определим
понятие "наследуемых" членов, которое обозначает доступные члены супер­
класса.

В таком случае правильное утверждение доступности члена будет следую­


щим: "Доступными являются все наследуемые члены и все члены, определен­
ные в данном классе': Это утверждение можно сформулировать иначе.

• Класс наследует все поля и методы экземпляра, но не конструкторы из


своего суперкласса.

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

Краткие итоги доступа к членам класса

В табл. 3.1 сведены правила доступа к членам класса.

Таблица З.1. Доступность членов класса

Доступ к чnенам
Доступность Открытый Защищенный По умоnчанию Закрытый

В определении класса Да Да Да Да
В классе из того же пакета Да Да Да Нет
В подклассе из другого пакета Да Да Нет Нет
В классе, не являющемся Да Нет Нет Нет
подклассом из другого пакета

Ниже перечислен ряд общепринятых правил в отношении тех частей про­


граммы на Java, в которых должны применяться модификаторы доступа. Со­
блюдать эти правила очень важно даже начинающим программировать на Java.
• Модификатор доступа puЫic следует применять только в объявлени­
ях методов и констант, образующих часть открытого прикладного ин­
терфейса API конкретного класса. Открытые (puЫic) поля пригодны

204 Глава 3. Объектно-ориентированное программирование на Java


только для хранения констант или неизменяемых объектов и поэтому
должны быть объявлены также конечными (final).
• Модификатор доступа protected следует применять в объявлениях
полей и методов, которые не требуются программистам, пользующимся
данным классом, хотя они могут представлять интерес для любого раз­
работчика, создающего подкласс как часть другого пакета.

Закрытые (protected) члены формально являются частью экс­


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

• Пакетный уровень доступности по умолчанию следует применять к тем


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

классах из того же пакета.

• Модификатор доступа private следует применять в объявлениях тех


полей и методов, которые применяются только в самом классе и долж­

ны быть скрыты за его пределами.

Если вы не знаете, какой уровень доступности следует применить, защи­


щенный, пакетный или закрытый, начните с закрытого. А если это чрезмер­

но ограничительный уро&ень доступности, то ограничения на доступ мож­

но всегда ослабить немного или же предоставить методы доступа, если речь


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

Методы доступа к данным


В рассмотренном ранее примере класса Circle было объявлено открытое
(puЫic) поле для хранения радиуса окружности. Поддержание открытой
доступности такого поля в классе Circle может быть вполне обосновано,
поскольку это довольно простой класс, в котором отсутствуют зависимо­

сти между его полями. С другой стороны, рассматриваемая здесь реализа­


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

Сокрытие данных и инкапсуляция 205


Но поскольку радиус окружности хранится в открытом (puЫic) поле, толю­
бой программист может установить в нем какое угодно значение, независимо
от того, насколько это вообще обосновано. Единственный выход из этого за­
труднения - ограничить программисту прямой доступ к открытому полю и

определить открытые методы, предоставляющие непрямой доступ к данному


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

отличается от объявления самого поля открытым (puЫic), главным обра­


зом, тем, что в таких методах можно выполнять проверку ошибок.
Допустим, требуется предотвратить появление объектов типа Circle с
отрицательными радиусами окружности, поскольку они, очевидно, бессмыс­
ленны. Но в рассматриваемой здесь реализации класса Circle воспрепят­
ствовать этому нельзя. Хотя в примере 3.4 демонстрируется, каким образом
можно изменить определение класса Circle, чтобы все-таки воспрепятство­
вать этому.

В данной версии класса Circle объявлено защищенное (protected) поле


r и определены методы доступа getRadius () и setRadius () для чтения и
записи данных в этом поле с наложением ограничения на отрицательные зна­

чения радиуса окружности. А поскольку поле r оказывается защищенным,


оно доступно напрямую (и более эффективно) подклассам данного класса.

Пример 3.4. Версия класса Circle, в которой применяется сокрытие и


инкапсуляция данных

package shapes; // указать пакет для класса

//Этот класс по-прежнему остается открытым:


puЫic class Circle {
// Эта константа, в общем, полезна, и поэтому
// она остается открытой:
puЬlic static final douЫe PI = 3.14159;

// Это поле радиуса скрыто, но все же доступно


// в подклассах, производных от данного класса:
protected douЫe r;

//В этом методе накладываются ограничения на радиус


//окружности, но подробности данной реализации могут
//представлять интерес для подклассов:
protected void checkRadius(douЫe radius)
if (radius < О.О)
throw new IllegalArgumentException(

206 Глава З. Объектно-ориентированное программирование на Java


"radius may not Ье negative.");

11 Конструктор не по умолчанию:
puЬlic Circle (douЫe r) (
checkRadius(r);
this.r = r;

11 Открытые методы доступа к данным:


puЫic getRadius() { return r;
douЬle

puЫic void setRadius (douЫe r) (


checkRadius(r);
this.r = r;

11 Методы для оперирования полем экземпляра:


puЬlic douЫe area () { return PI * r * r; }
puЫic douЬle circumference () ( return 2 * PI * r; }

Класс Circle определен в пакете shapes, а его поле r защищено


(protected) и поэтому доступно напрямую любым другим классам из пакета
shapes для установки в нем какого угодно значения. Здесь предполагается,
что все классы из пакета shapes разработаны одним автором или тесно со­
трудничающей группой авторов и что во всех этих классах доверительно со­

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


их реализации.

Наконец, в защищенном (protected) методе checkRadius () из класса


Circle налагается ограничение на отрицательные значения радиуса окруж­

ности. И хотя пользователи класса Circle не могут вызвать этот метод, его

все же можно вызвать и даже переопределить в подклассах, производных от

данного класса, если потребуется изменить ограничения, накладываемые на


радиус окружности.

Как правило, имена методов доступа к данным в языке Java при­


нято предварять префиксами get и set. Но если доступное в
них поле относится к типу boolean, то имя метода get () может
быть заменено равнозначным именем метода, начинающимся на
is. Так, метод доступа к полю readaЫe типа boolean обычно
называется isReadaЫe (), а не getReadaЫe ().

Сокрытие данных и инкапсуляция 207


Абстрактные классы и методы
Класс Circle был объявлен в примере 3.4 как составная часть пакета
shapes. Допустим, предполагается реализовать ряд следующих классов, ре­
ализующих геометрические формы: Rectangle, Square, Hexagon, Triangle
и т.д. В этих классах было бы полезно внедрить два основных метода: area ()
и circumference (). А для того чтобы упростить обработку массивов гео­
метрических форм, было бы неплохо, если бы у всех классов геометрических
форм был общий суперкласс Shape. Если структурировать подобным обра­
зом иерархию классов, то каждый объект геометрической формы (независи­
мо от того, какую именно форму он представляет) можно будет присваивать
переменным, полям или элементам массива типа Shape. Было бы также же­
лательно, чтобы в классе Shape были инкапсулированы все функциональ­
ные средства, общие для геометрических форм (например, методы area () и
circumference ()).Но ведь столь обобщенный класс Shape не представляет
ни одну из настоящих геометрических форм, и поэтому в нем нельзя опреде­
лить полезные реализации методов. В качестве выхода из этого затруднитель­
ного положения в Java поддерживаются абстрактные методы.
В языке Java допускается определить метод без его реализации, объявив
этот метод с модификатором доступа abstract. У абстрактного метода от­
сутствует тело и есть лишь определение его сигнатуры, завершаемое точкой с

запятой 2 • Ниже приведены правила, касающиеся абстрактных методов и аб­


страктных классов, в которых они содержатся.

• Любой класс с абстрактным методом сам автоматически становится


абстрактным и поэтому должен быть объявлен как abstract. Если не
сделать этого, возникнет ошибка компиляции.

• Получить экземпляры абстрактного класса нельзя.

• Получить экземпляры подкласса, производного от абстрактного клас­


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

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

208 Гnава З. Объектно-ориентированное программирование на Java


(т.е. его тело). Такой класс нередко называется конкретным подклассом,
чтобы подчеркнуть тот факт, что он не абстрактный.

• Если в подклассе, производном от абстрактного класса, реализуются не


все наследуемые им абстрактные методы, этот подкласс сам становится
абстрактным и поэтому должен быть объявлен как abstract.
• Статические (static), закрытые(private) и конечные (final) методы
не могут быть абстрактными (abstract), поскольку такие методы нель­
зя переопределить в подклассе. Аналогично конечный класс не может

содержать ни одного абстрактного метода.

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

Такой класс предназначен служить в качестве суперкласса для подклас­


сов с полной реализацией. Получить экземпляры абстрактного класса
нельзя.

Характерным примером абстрактного класса без абстрактных


методов служит класс Classloader, рассматриваемый в гла­
ве 11.
-

Рассмотрим применение упомянутых выше правил на конкретном при­


мере. Так, если определить класс Shape с абстрактными методами area () и
circumference (),то в любом производном от него подклассе придется пре­
доставить реализации этих методов, чтобы получить его экземпляры. Иными
словами, для всякого объекта типа Shape гарантируются реализации мето­
дов area () и circumference (). В примере 3.5 наглядно показано, как это
становится возможным. В нем, в частности, определяется абстрактный класс
Shape и два его конкретных подкласса.

Пример 3.5. Абстрактный класс и конкретные его подклассы.


puЫic abstract class Shape {
11 Абстрактные методы. Обратите внимание на
11 точку с запятой вместо тела метода:
puЫic abstract douЫe area();
puЫic abstract douЫe circumference();

Абстрактные классы и методы 209


class Circle extends Shape {
puЬlic static final douЫe PI = 3.14159265358979323846;
protected douЫe r; // Данные экземпляра
puЫic Circle(douЬle r) { this.r = r; ) // Конструктор
puЫic douЫe getRadius () { return r; ) / / Метод доступа
//Реализации абстрактных методов:
puЬlic douЫe area() { return PI*r*r;
puЫic douЫe circumference () { return 2*PI*r;

class Rectangle extends Shape {


protected douЫe w, h; //Данные экземпляра
puЬlic Rectangle (douЬle w, douЫe h) { // Конструктор
this.w = w; this.h = h;

/! Методы доступа:
puЫic douЫe getWidth () { return w; )
puЫic douЫe getHeight () { return h; )

// Реализации абстрактнь~ методов:


puЬlic douЬle area () { return w*h;
puЬlic douЫe circumference () { return 2* (w + h); )

После сигнатуры и круглых скобок в объявлении каждого абстрактного


метода следует точка с запятой, но отсутствуют фигурные скобки и опреде­
ляемое в них тело метода. Используя классы из примера 3.5, можно теперь
написать код, аналогичный приведенному ниже.

Shape[] shapes = new Shape[3]; //создать массив для


//хранения геометрических форм
shapes [О] new Ci rcle ( 2. О) ; / / заполнить массив
shapes[l] new Rectangle(l.O, 3.0);
shapes[2] new Rectangle(4.0, 2.0);

douЬle totalArea = О;
for(int i = О; i < shapes.length; i++)
totalArea += shapes [ i] . area (); / / вычислить площадь форм

В рассматриваемом здесь примере необходимо обратить внимание на сле­


дующие важные моменты.

• Объекты подклассов, производных от абстрактного класса Shape, мож­


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

21 О Гnава 3. Объектно-ориентированное программирование на Java


• Методы area () и circumference () могут быть вызваны для любого
объекта типа Shape, несмотря на то, что тело этих методов не определе­
но в классе Shape. При этом вызываемый метод обнаруживается с по­
мощью таблицы виртуальных методов, как пояснялось ранее. В данном
случае это означает, что площадь окружности вычисляется с помощью

метода, определенного в классе Circle, а площадь прямоугольника -


с помощью метода, определенного в классе Rectangle.

Преобразования ссылочных типов


Ссылки на объекты допускают взаимное преобразование ссылочных ти­
пов. Аналогично преобразованиям примитивных типов, преобразования
ссылочных типов могут быть как расширяющими и автоматически допускае­
мыми компилятором, так и сужающими и требующими приведения типов, а
возможно, и проверки во время выполнения. Чтобы преобразования ссылоч­
ных типов стали более понятными, необходимо уяснить, что ссылочные типы
образуют иерархию, обычно называемую иерархией классов.
Всякий ссылочный тип Java расширяет какой-нибудь другой тип, назы­
ваемый суперклассом. В таком типе сначала наследуются поля и методы из
его суперкласса, а затем определяются его собственные дополнительные поля
и методы. Специальный класс Obj ect играет роль корневого в иерархии
классов Java. Все классы Java расширяют класс Obj ect прямо или косвенно.
В классе Obj ect определяется целый ряд специальных методов, наследуемых
(или переопределяемых) в объектах всех остальных классов.
Предопределенный класс String и упоминавшийся ранее в этой главе
класс Point расширяют класс Obj ect. Таким образом, можно сказать, что
все объекты типа String и Point являются в то же время объектами типа
Obj ect, но противоположное неверно. В частности, нельзя сказать, что каж­
дый объект типа Object является в то же время объектом типа String, по­
скольку некоторые объекты типа Obj ect оказываются также объектами типа
Obj ect, как демонстрировалось ранее.
Принимая во внимание приведенное выше простое разъяснение иерархии
классов, можно определить следующие правила для взаимного преобразова­
ния ссылочных типов данных.

• Ссылка на объект не может быть преобразована в тип, который с ней


не связан. Компилятор Java не позволит преобразовать, например,

Абстрактные классы и методы 211


ссылочный тип String в тип Point, даже если выполнить операцию

приведения типов.

• Ссылка на объект не может быть преобразована в тип ее суперкласса


или любого его родительского кл