Полностью обновленное и расширенное издание Герберт Шилдт ... _ 1.• � 1 , • [И AUA,,aonuк.i The Complete Reference Herbert Schildt Milan New York Chicago San Francisco Athens London Madrid Mexico City ew Delhi Singapore Sydney Toronto По руковод Герберт Шилдт Москва • Санкт-Петербург 2022 ББК 32.973.26-018.2.75 Ш57 УДК 004.43 2 ООО "Диалектика" Перевод с английского и редакция Ю.Н. Артеменко По общим вопросам обращайтесь в издательство "Диалектика" по адресу: [email protected], http://www.dialektika.com Шилдт, Герберт. Ш57 Java. Полное руководство, 12-е изд. : Пер. с англ. - СПб. "Диалектика•; 2023. - 1344 с.: ил. - Парал. тит. англ. ООО ISBN 978-5-907458-86-4 (рус.) ББК 32.973.26-018.2.75 Все права защищены. Все названия программных продуктов являются зарегистрированными торговыми марками соответствующих фирм. Никакая часть настоящего издания ни в каких целях не может быть воспроизведена в какой бы то ни было форме и какими бы то ни было средствами, будь то электронные или механические, включая фотокопирование и запись на магнитный носитель, если на это нет письменного разрешения издательства McGraw Hill. Copyright © 2022 Ьу McGraw Hill. All rights reserved. All trademarks are trademarks of their respective owners. Oracle Corporation does not make any representations or warranties as to the accuracy, adequacy, or completeness of any information contained in this Work, and is not responsiЬ!e for any errors or omissions. Authorized translation from the English language edition of the Java: The Complete Reference, 12th Edition (ISBN 978-1-26-046341-5), puЬ!ished Ьу Mc(;raw НШ. Except a s permitted under the United States Copyright Act of 1976, no part of this puЫication may Ье reproduced or distributed in any form or Ьу any means, or stored in а database or retrieval system, without the prior written permission of the puЬ!isher. Научно-п оп улярное издание Герберт Шилдт Java. Полное руководство 12-е издание Подписано в печать 12.09.2022. Формат 70xl00/16 Усл. печ. л. 108,36. Уч.-изд. л. 68,2 Тираж 500 экз. Заказ № 7327 Отпечатано в АО "Первая Образцовая типография" Филиал "Чеховский Печатный Двор" 142300, Московская область, r. Чехов, ул. Полиграфистов, д. 1 Сайт: www.chpd.ru, E-mail: sales@chpdxu, тел. 8 (499) 270-73-59 ООО "Диалектика'; 195027, Санкт-Петербург, Магнитогорская ул., д. 30, лит. А, пом. 848 ISBN 978-5-907458-86-4 (рус.) ISBN 978-1-26-046341-5 (англ.) © ООО ':Диалектика'; 2022, перевод, оформление, макет ирование © 2022 Ьу McGraw Hill Оглавление Предисловие 29 Часть 1. Язык Java 33 Глава 1. История и эволюция языка Java 34 Глава 2. Краткий обзор языка Java 58 Глава 3. Типы данных, переменные и массивы 80 Глава 4. Операции 110 Глава 5. Управляющие операторы 131 Глава 6. Введение в классы 162 Глава 7. Подробный анализ методов и классов 183 Глава 8. Наследование 217 Глава 9. Пакеты и интерфейсы 245 Глава 10. Обработка исключений 274 Глава 11. Многопоточное программирование 296 Глава 12. Перечисления, автоупаковка и аннотации 328 Глава 13. Ввод-вывод, оператор try с ресурсами и другие темы 367 Глава 14. Обобщения 401 Глава 15. Лямбда-выражения 444 Глава 16. Модули 473 Глава 17. Выражения swi tch, записи и прочие недавно добавленные средства 503 Часть 11. Бибnиотека Java Глава 18. Обработка строк 539 540 Глава 19. Исследование пакета j ava. lanq 569 Глава 20. Пакет java. util, часть 1: Collections Framework 648 Глава 21. Пакет java. util, часть 2: дополнительные служебные классы 743 Глава 22. Ввод-вывод: исследование пакета java. io 813 Глава 23. Исследование системы NIO 868 Глава 24. Работа в сети 907 Глава 25. Обработка событий 934 Глава 26. Введение в AWT: работа с окнами, графикой и текстом 970 Глава 27. Использование элементов управления, диспетчеров компоновки и меню AWT 1001 Глава 28. Изображения 1053 Глава 29. Утилиты параллелизма 1079 Глава 30. Потоковый АРI-интерфейс 1134 Глава 31. Регулярные выражения и другие пакеты 1160 Часть 111. Введение в проrраммирование rрафических поnьзоватеnьских интерфейсов с помощью Swing Java 1189 Глава 32. Введение в Swing 1190 Глава 33. Исследование Swing 1210 Глава 34. Введение в меню Swing 1240 Часть IV. Применение Java Глава 35. Архитектура JavaBeans Глава 36. Введение в сервлеты Часть V. Приnожения 1275 1276 1289 1315 Приложение А. Использование документирующих комментариев Java 1316 Приложение Б. Введение в JShell 1325 Приложение В. Компиляция и запуск простых однофайловых программ за один шаг 1336 Предметный указатель 1338 Содержание Предисловие Часть 1. Язык Java Глава 1. История и эволюция языка Java Происхождение Java Зарождение современного программирования: язык С С++: следующий шаг Условия для появления языка Java Создание языка Java Связь с языком С# Влияние языка Java на Интернет Аплеты Java Безопасность Переносимость Магия Java: байт-код Выход за рамки аплетов Более быстрый график выпуска Сервлеты: Java на серверной стороне Терминология языка Java Простота Объектная ориентация Надежность Мноrопоточность Нейтральность к архитектуре Интерпретируемость и высокая производительность Распределенность Динамичность Эволюция языка Java Культура инноваций Глава 2. Краткий обзор языка Java Объектно-ориентированное программирование Две парадигмы Абстракция Три принципа ООП Первая простая программа Ввод кода программы Компиляция программы Подробный анализ первого примера программы Вторая простая программа Два управляющих оператора Оператор if Цикл for 29 33 34 34 35 37 38 38 41 41 41 42 42 43 44 45 46 47 47 47 48 49 49 49 49 50 50 57 58 58 58 59 60 66 66 67 68 70 72 72 73 8 Содержание Использование блоков кода Лексические вопросы Пробельные символы Идентификаторы Литералы Комментарии Разделители Ключевые слова Java Библиотеки классов Java 75 76 76 76 77 77 77 77 79 Глава 3. Типы данных, переменные и массивы Java - строго типизированный язык Примитивные типы Целые числа Тип byte Тип short Тип int Тип long Типы с плавающей точкой Тип float Тип douЫe Символы Булевские значения Подробный анализ литералов Целочисленные литералы Литералы с плавающей точкой Булевские литералы Символьные литералы Строковые литералы Переменные Объявление переменной Динамическая инициализация Область видимости и время жизни переменных Преобразование и приведение типов Автоматические преобразования в Java Приведение несовместимых типов Автоматическое повышение типов в выражениях Правила повышения типов Массивы Одномерные массивы Многомерные массивы Альтернативный синтаксис объявления массивов Знакомство с выведением типов локальных переменных Некоторые ограничения var Несколько слов о строках 80 80 80 81 82 82 82 83 83 84 84 85 86 87 87 88 89 89 90 91 91 92 92 95 95 95 97 98 99 99 101 105 106 108 109 Глава 4. Операции Арифметические операции Основные арифметические операции Операция деления по модулю Составные арифметические операции присваивания Операции инкремента и декремента 110 110 111 112 112 113 Содержа ние Побитовые операции Побитовые логические операции Сдвиг влево Сдвиг вправо Беззнаковый сдвиг вправо Составные побитовые операции присваивания Операции отношения Булевские логические операции Короткозамкнутые логические операции Операция присваивания Операция? Старшинство операций Использование круглых скобок Глава 5. Управляющие операторы Операторы выбора Java Оператор if Традиционный оператор switch Операторы итерации Цикл while Цикл do-while Цикл for Версия цикла for в стиле "for-each" Выведение типов локальных переменных в цикле fо r Вложенные циклы Операторы перехода Использование оператора break Использование оператора continue Оператор return Глава 6. Введение в классы Основы классов Общая форма класса Простой класс Объявление объек тов Подробный анализ операции new Присваивание для переменных ссылок на объекты Введение в методы Добавление метода в класс Вох Возвращение значения Добавление метода, принимающего параметры Конс трукторы Параметризованные конструкторы Ключевое слово this Сокрытие переменных экземпляра Сборка мусора Класс Stack Глава 7. Подробный анализ методов и классов Перегрузка методов Перегрузка конструкторов Использование объектов в качестве параметров 9 115 116 119 120 121 123 124 125 126 127 128 129 130 131 131 131 134 140 140 141 144 148 153 154 155 155 159 161 162 162 162 163 166 166 168 169 169 171 173 175 177 178 178 179 180 183 183 186 188 1О Содержание Подробный анализ передачи аргументов Возвращение объектов Рекурсия Введение в управление доступом Ключевое слово s ta t i с Ключевое слово final Снова о массивах Вложенные и внутренние классы Исследование класса String Использование аргументов командной строки Аргументы переменной длины Перегрузка методов с аргументами переменной длины Аргументы переменной длины и неоднозначность Выведение типов локальных переменных для ссылочных типов Глава 8. Наследование Основы наследования Доступ к членам и наследование Более реалистичный пример Переменная типа суперкласса может ссылаться на объект подкласса Использование ключевого слова super Использование ключевого слова super для вызова конструкторов суперкласса Использование второй формы ключевого слова super Создание многоуровневой иерархии Когда конструкторы выполняются Переопределение методов Динамическая диспетчеризация методов Зачем нужны переопределенные методы? Применение переопределения методов Использование абстрактных классов Использование ключевого слова final с наследованием Использование ключевого слова fina 1 для предотвращения переопределения Использование ключевого слова final для предотвращения наследования Выведение типов локальных переменных и наследование Класс Obj ect Глава 9. Пакеты и интерфейсы Пакеты Определение пакета Поиск пакетов и CLASSPATH Краткий пример пакета Пакеты и доступ к членам классов Пример, демонстрирующий использование модификаторов доступа Импортирование пакетов Интерфейсы Определение интерфейса Реализация интерфейсов Вложенные интерфейсы 190 192 193 195 199 201 201 203 206 208 209 212 214 215 217 217 219 220 222 223 223 226 227 230 231 233 235 236 237 240 240 241 241 243 245 245 246 247 247 248 250 252 254 255 256 259 Содержание Применение интерфейсов Переменные в интерфейсах Интерфейсы можно расширять Стандартные методы интерфейса Основы стандартных методов Более реалистичный пример Проблемы множественного наследования Использование статических методов в интерфейсе Закрытые методы интерфейса Заключительные соображения по поводу пакетов и интерфейсов Глава 10. Обработка исключений Основы обработки исключений Типы исключений Неперехваченные исключения Использование try и catch Отображение описания исключения Использование нескольких конструкций саtch Вложенные операторы try Оператор throw Конструкция throws Конструкция finally Встроенные исключения Java Создание собственных подклассов Exception Сцепленные исключения Три дополнительных средства в системе исключений Использование исключений Глава 11. Мноrопоточное проrраммирование Потоковая модель Java Приоритеты потоков Синхронизация Обмен сообщениями Класс Thread и интерфейс RunnaЫe Главный поток Создание потока Реализация интерфейса RunnaЫe Расширение класса Thread Выбор подхода Создание множества потоков Использование isAli ve () и j oin () Приоритеты потоков Синхронизация Использование синхронизированных методов Оператор synchroni zed Взаимодействие между потоками Взаимоблокировка Приостановка, возобновление и останов потоков Получение состояния потока Использование фабричных методов для создания и запуска потока Использование многопоточности 11 260 263 265 266 267 269 269 271 271 273 274 274 275 276 277 279 279 281 283 284 286 287 289 292 294 295 296 297 298 299 300 300 301 303 303 305 306 306 308 310 311 312 314 316 320 322 325 326 327 12 Содержание Глава 12. Перечисления, автоуп аковк а и аннот ации 328 328 329 331 332 334 336 337 338 338 339 341 342 342 344 345 346 346 346 347 348 353 354 355 356 357 360 364 366 Глава 13. Ввод-вывод, оператор try с ресурсами и друrие темы 367 367 368 368 371 372 373 374 376 376 378 384 388 388 391 391 392 395 395 398 400 Перечисления Основы перечислений Методы values () и valueOf () Перечисления Java являются типами классов Перечисления унаследованы от Enurn Еще один пример перечисления Оболочки типов Класс Character Класс Boolean Оболочки числовых типов Автоупаковка Автоупаковка и методы Автоупаковка/автораспаковка и выражения Автоупаковка/автораспаковка типов Boolean и Character Автоупаковка/автораспаковка помогает предотвратить ошибки Предостережение Аннотации Основы аннотаций Указание политики хранения Получение аннотаций во время выполнения с использованием рефлексии Интерфейс AnnotatedElernent Использование стандартных значений Маркерные аннотации Одноэлементные аннотации Встроенные аннотации Аннотации типов Повторяющиеся аннотации Некоторые ограничения Основы ввода-вывода Потоки данных Потоки байтовых и символьных данных Предопределенные потоки данных Чтение консольного ввода Чтение символов Чтение строк Запись консольного вывода Класс PrintWriter Чтение файлов и запись в файлы Автоматическое закрытие файла Модификаторы transient и volatile Введение в instanceof Модификатор strictfp Собственные методы Использование assert Параметры включения и отключения проверки утверждений Статическое импортирование Вызов перегруженных конструкторов через this () Несколько слов о классах, основанных на значениях Содержание Глава 14. Обобщения Что такое обобщения? Простой пример обобщения Обобщения работают только со ссылочными типами Обобщенные типы различаются на основе их аргументов типов Каким образом обобщения улучшают безопасность в отношении типов? Обобщенный класс с двумя параметрами типов Общая форма обобщенного класса Ограниченные типы Использование аргументов с подстановочными знаками Ограниченные аргументы с подстановочными знаками Создание обобщенного метода Обобщенные конструкторы Обобщенные интерфейсы Низкоуровневые типы и унаследованный код Иерархии обобщенных классов Использование обобщенного суперкласса Обобщенный подкласс Сравнение типов в обобщенной иерархии во время выполнения Приведение Переопределение методов в обобщенном классе Выведение типов и обобщения Выведение типов локальных переменных и обобщения Стирание Мостовые методы Ошибки неоднозначности Некоторые ограничения обобщений Невозможность создать экземпляры параметров типов Ограничения, касающиеся статических членов Ограничения, касающиеся обобщ�!"{ных массивов Ограничения, касающиеся обобщенных исключений Глава 15. Лямбда-выражения Введение в лямбда-выражения Основы лямбда-выражений Функциональные интерфейсы Примеры лямбда-выражений Блочные лямбда-выражения Обобщенные функциональные интерфейсы Передача лямбда-выражений в качестве аргументов Лямбда-выражения и исключения Лямбда-выражения и захват переменных Ссылки на методы Ссылки на статические методы Ссылки на методы экземпляра Ссылки на методы и обобщения Ссылки на конструкторы Предопределенные функциональные интерфейсы 13 401 402 402 406 407 407 409 411 41 l 413 416 421 424 424 427 429 429 431 432 434 435 436 437 438 438 440 441 441 442 442 443 444 444 445 446 447 451 453 454 457 458 459 459 461 464 467 471 14 Содержание Глава 16. Модули Основы модулей Простой пример модуля Компиляция и запуск первого примера модуля Более подробный анализ операторов requires и export s Модуль j ava . base и модули платформы Унаследованный код и неименованные модули Экспортирование в конкретный модуль Использование r e qui res t rans i ti ve Использование служб Основы служб и поставщиков служб Ключевые слова, связанные со службами Пример службы, основанной на модулях Графы модулей Три специальных характерных черты модулей Открытые моду ли Оператор opens Оператор requires s t at ic Введение в j l ink и файлы модулей JAR Связывание файлов в развернутом каталоге Связывание модульных файлов JAR Файлы JMOD Кратко об уровнях и автоматических модулях Заключительные соображения по поводу модулей Глава 17. Выражения switch, записи и прочие недавно добавленные средства Расширения оператора switch Использование списка констант ca se Появление выражения switch и оператора y i e l d Появление стрелки в операторе cas e Подробный анализ оператора с а s е с о стрелкой Еще один пример выражения s wit ch Текстовые блоки Основы текстовых блоков Ведущие пробельные символы Использование двойных кавычек в текстовом блоке Управляющие последовательности в текстовых блоках Записи Основы записей Создание конструкторов записи Еще один пример конструктора записи Создание методов получения для записи Сопоставление с образцом в операции instanceof Шаблонные переменные в логических выражениях "И" Сопоставление с образцом в других операторах Запечатанные классы и запечатанные интерфейсы Запечатанные классы Запечатанные интерфейсы Будущие направления развития 473 473 474 478 480 481 482 483 485 489 489 490 491 497 498 498 499 499 499 500 500 501 502 502 503 504 505 506 509 510 514 514 515 516 517 518 519 520 522 526 528 530 531 532 533 533 535 537 Содержание Часть 11. Библиотека Java Глава 18. Обработка строк Конструкторы класса String Длина строки Специальные строковые операции Строковые литералы Конкатенация строк Конкатенация строк с другими типами данных Преобразование в строку и t о St r i n g ( ) Извлечение символов charAt ( ) getChars ( ) getBytes ( ) toCharArray ( ) Сравнение строк equals ( ) и equalsi gnoreCa se ( ) regionMatches ( ) start sWith ( ) и endsW ith ( ) equals ( ) или compareTo ( ) Поиск в строках Модификация строк substr ing ( ) concat ( ) replace ( ) t r im ( ) и st rip ( ) Преобразование данных с использованием valueOf ( ) Изменение регистра символов внутри строк Соединение строк Дополнительные методы класса String Класс S t r ingBuffer Конструкторы класса St ringBuffer length ( ) и capaci ty ( ) ensureCapaci ty ( ) setLength ( ) cha rAt ( ) и setCharAt ( ) getCha rs ( ) append ( ) insert ( ) reverse ( ) delete ( ) и deleteCha rAt ( ) replace ( ) substring ( ) Дополнительные методы класса St r ingBuffer Класс St ringBuilder Глава 19. Исследование пакета j ava . lanq Оболочки примитивных типов NumЬer ОоuЫе и Float Методы i s i nfinite ( ) и i sNaN ( ) 15 539 540 541 543 543 543 544 544 545 546 546 547 547 547 548 548 549 549 549 550 552 553 553 554 554 555 556 556 557 558 561 561 561 562 562 562 563 563 564 564 565 566 566 566 568 569 570 570 570 576 16 Содержа ние Byte, Short, Integer и Long Character Дополнения класса Charact e r для помержки кодовых точек Unicode Вооlеап Void Process Runtime Выполнение других программ Runt ime . Version Proce ssBuilder System Использование currentT irneMi l l i s ( ) для хронометража выполнения программы Использование arraycopy ( ) Свойства среды System . Logger и System . Logge rFinde r Obj ect Использование метода clone ( ) и интерфейса CloneaЫe Class ClassLoader Math Тригонометрические функции Экспоненциальные функции Функции округления Прочие методы Math StrictMath Comp i ler Th read, ThreadGroup и RunnaЫe Интерфейс RunnaЫe Класс Thread Класс ThreadGroup ThreadLocal и I nheri taЫeThr eadLo c a l Package Modu le Modul eLayer Runt ime Permiss ion ThrowaЫe SecurityManager StackTraceElement StackWal ker и StackWa l ker . S t ackFrame Enum Record ClassVa lue Интepфeйc Char Sequence Интерфейс ComparaЫe Интерфейс AppendaЫe Интерфейс I t eraЫe Интерфейс ReadaЫe Интерфейс AutoCloseaЫe Интерфейс Thread . UncaughtExcepti onHandler 576 591 594 596 598 598 599 601 602 604 608 6 10 611 6 12 612 6 12 613 6 15 62 1 621 621 622 623 625 628 628 628 628 628 632 636 636 638 639 639 639 639 640 641 641 642 643 643 644 644 644 645 645 646 С од е ржа н и е Подпакеты j ava . lang j ava . l ang . annotat ion j ava . lang . constant j ava . lang . inst rument j ava . lang . invoke j ava . lang . management j ava . l ang . module j ava . lang . ref j ava . lang . refl ect Глава 20. Пакет java.util, чacть 1: Collections Framework Обзор Collections Framework Интерфейсы коллекций Интерфейс Col l e ction Интерфейс List Интерфейс Set Интерфейс SortedSet Интерфейс Navi gaЫ eSet Интерфейс Queue Интерфейс Deque Классы коллекций Kлacc Ar rayL i s t Класс L i n kedL i s t Класс Ha shSet Класс Li nkedHa shSet Класс Tre eSet Класс Priori tyQueue Класс Ar ra yDeque Класс EnumSet Доступ в коллекцию через итератор Использование итератора Альтернатива итераторам в виде цикла f о r в стиле "for-each'' Сплитераторы Хранение объектов пользовательских классов в коллекциях Интерфейс Ra ndornAccess Работа с картами Интерфейсы карт Классы карт Компараторы Использование компаратора Алrоритмы коллекций Массивы Унаследованные классы и интерфейсы Интерфейс Enumerat i on Класс Vector Класс S t a c k Класс D i ct i onary Класс HashtaЫe Класс P r ope rties Использование методов st ore ( ) и load ( ) Заключительные соображения по поводу коллекций 17 646 646 646 646 647 647 647 647 647 648 649 651 652 655 658 659 660 662 663 666 667 671 672 674 674 675 676 677 679 680 682 683 686 688 688 688 697 703 705 711 719 724 725 725 730 732 733 737 740 742 18 Содерж ани е Глава 21. Пакет j ava . u til, часть 2: дополнительные служебные классы Класс S t ringTokeni zer BitSet Opt i onal, Opt i ona l DouЫ e, Opt i onal int и Opt i ona lLong Date Calendar GregorianCalendar Time Z one S impleTime Zone Locale Random T imer и TimerTa s k Currency Forma t t e r Конструкторы класса Fo rmatter Методы класса Forma t t e r Основы форматирования Форматирование строк и символов Форматирование чисел Форматирование времени и даты Спецификаторы % n и % '% Указание минимальной ширины поля Указание точности Использование флагов формата Выравнивание выводимых данных Флаги пробела, +, О и ( Флаг запятой Флаг # Версии в верхнем регистре Использование индекса аргумента Закрытие объекта Fo rmatter Альтернативный вариант: метод р r i n t f ( ) Scanner Конструкторы класса Scanner Основы сканирования Примеры использования класса Scanner Установка разделителей Дополнительные средства класса S canner ResourceBundle, L i s tRe sourceBundle и Prope r t yRe sourc eBundle Смешанные служебные классы и интерфейсы Подпакеты пакета j ava . util j ava . ut i l . concurrent, j ava . ut i l . concurrent . atomi c и j ava . ut i l . concurrent . l ocks j ava . ut i l . funct i on j ava . ut i l . j ar j ava . uti : . l ogging j ava . ut i :;_ . prefs j ava . ut i l . random j ava . ut i l . regex j ava . ut i l . spi j ava . ut i l . st ream j ava . uti: . zip 743 743 745 749 753 755 759 761 762 763 764 767 770 771 772 772 773 776 776 777 779 779 781 782 782 783 784 784 784 785 787 7 87 787 788 789 794 798 799 800 805 807 807 807 811 812 812 812 812 812 812 812 Содержание Глава 22. Ввод-вывод: исследование пакета java . io Классы и интерфейсы ввода-вывода File Каталоги Использование интерфейса Fi lenameFi l t e r Альтернативные методы 1 i s t F i 1 е s ( ) Создание каталогов Интерфейсы AutoCl o s eaЫe, CloseaЫe и F l u s haЫ e Исключения ввода-вывода Два способа закрытия потока данных Классы потоков данных Байтовые потоки InputSt ream Output St ream FileinputStream Fi leOutputStream ByteArrayinput St ream ByteArrayOutputSt ream Фильтрующие байтовые потоки Буферизованные байтовые потоки Sequence input Stream Pr intSt ream Dat aOutput S t ream и Da t a i nputSt ream RandomAcces s File Символьные потоки Reader Writer FileReader Fi leWriter Cha rArrayReade r Cha rAr rayWri t e r Buffe redReader Buffe redWriter Pushba ckReader PrintWri ter Класс Cons o l e Сериализация Seria l i zaЫe Exte rna l i zaЫe Obj ectOutput Obj ectOutputStream Obj ect input Obj ect i nputStream Пример сериализации Преимущества потоков Глава 23. Исследование системы NIO Классы NIO Основы NIO Буферы Каналы Наборы символов и селекторы 19 813 814 814 818 819 820 820 821 821 822 824 824 824 826 827 829 831 832 834 834 838 840 842 844 845 845 847 848 848 850 851 852 853 854 855 856 858 859 859 859 860 862 862 864 867 868 868 869 869 873 875 20 Содержа н ие Усовершенствования, появившиеся в NIO.2 Интерфейс Path Класс F i l e s Класс Paths Интерфейсы для файловых атрибутов Классы F i l e Sy s t ern, Fi l e S y s terns и Fi l e S t ore Использование системы NIO Использование системы NIO для ввода-вывода, основанного на каналах Использование системы NIO для ввода-вывода, основанного на потоках Использование системы NIO для операций с путями и файловой системой Глава 24. Работа в сети Основы работы в сети Классы и интерфейсы пакета j ava . net для работы в сети I netAddre s s Фабричные методы Методы экземпляра I net 4Address и I net бAddre s s Клиентские сокеты TCP/IP URL URLConnect ion HttpURLConnecti on Класс URI Сооkiе-наборы Серверные сокеты TCP/IP Дейтаграммы DatagrarnSocket DatagrarnPacket Пример использования дейтаграмм Введение в пакет j а va . net . ht tp Три ключевых элемента Простой пример клиента НТТР Что еще рекомендуется изучить в j ava . net . http Глава 25. Обработка событий Два механизма обработки событий Модель делегирования обработки событий События Источники событий Прослушиватели событий Классы событий Kлacc Act i onEvent Класс Adj u s trne ntEvent Класс CornponentEvent Класс Cont a i ne rEve nt Класс Focus Event Класс I nputEvent Класс I ternEvent Класс KeyEvent Класс MouseEvent Класс MouseWhe e l Event 875 875 878 881 882 885 885 886 896 898 907 907 909 910 910 911 912 912 916 917 920 922 923 923 924 924 925 926 928 928 931 933 934 935 935 936 936 937 937 938 940 941 942 942 943 944 945 946 947 Содержание Класс Text Event Класс Wi ndowEvent Источники событий Интерфейсы прослушивателей событий Интepфeйc Act i o nL i s t e ner Интерфейс Adj us trnent L i s t e n e r Интерфейс Cornponen t L i s t e n e r Интерфейс Cont a i ne r L i s tener Интерфейс Foc u s L i s t e n e r Интерфейс IternLi s t e ner Интерфейс KeyL i s t e n e r Интерфейс Mous e L i s t e n e r Интерфейс MouseMot i o nL i s t e n e r Интерфейс MouseWhe e l L i stener Интерфейс Text L i s t e n e r Интерфейс Wi ndowFocusLi s t e n e r Интерфейс WindowL i s t e n e r Использование модели делегирования обработки событий Основные концепции графических пользовательских интерфейсов АWT Обработка событий мыши Обработка событий клавиатуры Классы адаптеров Внутренние классы Анонимные внутренние классы fлава 26. Введение в AWT: работа с окнами, графикой и текстом Классы AWT Основы окон Cornponent Container Panel Window Frarne Canvas Работа с окнами Frame Установка размеров окна Скрытие и отображение окна Установка заголовка окна Закрытие фреймового окна Meтoд p a i nt ( ) Отображение строки Установка цветов фона и переднего плана Запрос перерисовки Соэдание приложения на основе F r arne Введение в графику Вычерчивание линий Вычерчивание прямоугольников Вычерчивание эллипсов и окружностей Вычерчивание дуг Вычерчивание многоугольников Демонстрация работы методов вычерчивания Установка размеров графики 21 948 949 950 951 952 952 952 952 953 953 953 953 954 954 954 954 954 955 955 956 960 963 966 968 970 971 974 974 975 975 975 975 975 976 976 976 976 977 977 977 978 978 980 980 981 981 981 982 982 982 983 22 С одер ж а н ие Работа с цветом Методы класса C o l o r Установка текущего цвета графики Программа, демонстрирующая работу с цветом Установка режима рисования Работа со шрифтами Выяснение доступных шрифтов Создание и выбор шрифта Получение информации о шрифте Управление выводом текста с использованием FontMe t ri c s Глава 27. Использование элементов управления, диспетчеров компоновки и меню AWT Основы элементов управления AWT Добавление и удаление элементов управления Реагирование на события, генерируемые элементами управления Исключение Head l e s sException Метки Использование кнопок Обработка событий для кнопок Использование флажков Обработка событий для флажков Группы флажков Элементы управления выбором Обработка событий для списков выбора Использование списков Обработка событий для списков Управление полосами прокрутки Обработка событий для полос прокрутки Использование текстовых полей Обработка событий для текстовых полей Использование текстовых областей Понятие диспетчеров компоновки FlowLayout BorderLayout Использование вставок GridLayout CardLayout GridBagLayout Меню и панели меню Диалоговые окна Несколько слов о переопределении метода paint ( ) Глава 28. Изображения Форматы файлов Основы работы с изображениями: создание, загрузка и отображение Создание объекта изображения Загрузка изображения Отображение изображения Двойная буферизация ImageP roducer MemoryimageSource 985 986 987 987 988 990 992 993 995 996 1001 1002 1002 1003 1003 1003 1005 1005 1009 1010 1012 1014 1015 1016 1018 1019 1021 1023 1024 1026 1028 1029 1030 1031 1033 1034 1037 1043 1048 1052 1053 1053 1054 1054 1055 1055 1057 1060 1060 Сод ержа н и е ImageConsumer PixelGrabber ImageFi l t e r CropimageFilter RGBimageFil ter Дополнительные классы для обработки изображений Глава 29. Утилиты параллелизма Пакеты параллельного API j ava . ut i l . concurrent j ava . util . concurrent . atomic j ava . ut il . concurrent . locks Использование объектов синхронизации Semaphore CountDownLatch Cycl icBarrier Exchanger Phaser Использование исполнителя Простой пример использования исполнителя Использование интерфейсов Cal l aЫ e и Fut ure Перечисление T imeUni t Параллельные коллекции Блокировки Атомарные операции Параллельное программирование с помощью Fork/Join Framework Главные классы Fork/Join Framework Стратегия "разделяй и властвуй" Простой пример использования Fork(Join Framework Влияние уровня параллелизма Пример использования Recurs i veTas k<V> Выполнение задачи асинхронным образом Отмена задачи Определение состояния завершения задачи Перезапуск задачи Дальнейшие исследования Советы по использованию Fork/Join Framework Сравнение утилит параллелизма и традиционного подхода к многопоточности в Java Глава 30. Потоковый АРI-интерфейс Основы потоков Потоковые интерфейсы Получение потока Простой пример использования потока Операции редукции Использование параллельных потоков Сопоставление Накопление Итераторы и потоки Использование итератора с потоком Использование сплитератора Дальнейшее исследование потокового API 23 1062 1062 1065 1065 1067 1078 1079 1080 1 081 1082 1082 1082 1083 1088 1090 1092 1 095 1 103 1 104 1 105 1 108 1109 1110 1 1 13 1 1 14 1115 1 1 19 11 21 1 1 23 1 1 26 1 1 29 1 129 1 130 1 1 30 1 130 1132 1 133 1 1 34 1 1 34 1 135 1138 1 1 39 1 142 1145 1 147 1151 1 155 1 1 55 1 1 56 1 159 24 С одерж ани е Глава 31. Регулярные выражения и другие пакеты Обработка регулярных выражений Класс Pattern Kлacc Matcher Синтаксис регулярных выражений Демонстрация сопоставления с шаблоном Два варианта сопоставления с шаблоном Дальнейшее исследование регулярных выражений Рефлексия Удаленный вызов методов Простое клиент-серверное приложение, использующее удаленный вызов методов Форматирование даты и времени с помощью пакета j ava . text Класс DateForrna t Класс Sirnp l e Date Forrnat Пакеты j ava . t irne, померживающие API даты и времени Фундаментальные классы для поддержки даты и времени Форматирование даты и времени Разбор строк с датой и временем Дальнейшее исследование пакета j а v а . t irne 1 160 1160 1 161 1 161 1162 1 163 1 1 68 1 169 1 169 1 174 1 174 1 1 78 1 178 1 180 1 182 1 182 1 184 1 187 1 188 Часть 111. Введение в проrраммирование rрафических поnьзоватеnьских интерфейсов с помощью Swing Java 1 1 89 Глава 32. Введение в Swing Происхождение инфраструктуры Swing Инфраструктура Swing построена на основе AWT Две ключевые особенности Swing Компоненты Swing являются легковесными Инфраструктура Swing поддерживает подключаемый внешний вид Связь с архитектурой MVC Компоненты и контейнеры Компоненты Контейнеры Панели контейнеров верхнего уровня Пакеты Swing Простое приложение Swing Обработка событий Рисование в Swing Основы рисования Вычисление области рисования Пример программы рисования Глава 33. Исследование Swing JLabel и Irnage icon JТextField Кнопки Swing JButton JT oggleBut ton Флажки Взаимоисключающие переключатели 1 190 1 190 1 191 1 191 1 192 1 192 1 192 1 194 1 194 1 195 1 195 1 196 1 196 1201 1204 1205 1206 1206 1 2 10 1210 1212 1 214 1214 1217 1219 1221 Содержание JТabbedPane JScroll Pane JList JComЬoBox Деревья JТаЫе Глава 34. Введение в меню Swing Основы меню Обзор JМe nuBar, JMenu и JМenu i t em JМenuBar JМenu JMenuitem Создание главного меню Добавление мнемонических символов и клавиатурных сочетаний к пунктам меню Добавление изображений и всплывающих подсказок к пунктам меню Использование JRadioButtonMe n u i t em и JChe c kBoxMenu i t em Создание всплывающего меню Создание панели инструментов Использование действий Построение окончательной п рограммы MenuDemo Продолжение исследования Swing Часть IV. Применение Java Глава 35. Архитектура JavaBeans Что собой представляет Веаn-компонент Преимущества Веаn-компонентов Самоанализ Паттерны проектирования для свойств Паттерны проектирования для событий Методы и паттерны проектирования Использование интерфейса Beaninfo Связанные и ограниченные свойства Постоянство Настройщики JavaBeans API Int rospector Propert yDescriptor EventSetDescriptor Met hodDe scriptor Пример Веаn-компонента Глава 36. Введение в сервлеты Происхождение сервлетов Жизненный цикл сервлета Варианты разработки сервлетов Использование Tomcat Простой сервлет Создание и компиляция исходного кода сервлета 25 1223 1226 1227 1231 1233 1236 1240 1240 1242 1242 1243 1244 1245 1249 1252 1253 1255 1259 1261 1267 1273 1275 1276 1276 1277 1277 1278 1279 1280 1280 1281 1281 1 281 1282 1285 1285 1285 1285 1286 1 289 1289 1290 1291 1291 1293 1293 26 Содержание Запуск Tomcat Запуск веб-браузера и запрашивание сервлета Servlet API Пакет j a karta . s e rv l e t Интерфейс S e rvlet Интерфейс Se rvletCon f i g Интерфейс Se rvle tCont e xt Интерфейс S e rv l e t Re qu e s t Интерфейс S e rv l e tRespons e Класс Gene r i c S e rv l e t Kл�c S e rv l e t i nputSt ream Класс S e rvletOu tput S t ream Классы исключений сервлетов Чтение параметров сервлета Пaкeт j akart a . servlet . h ttp Интерфейс HttpS e rv l e tRequest Интерфейс HttpSe rvl etRespon s e Интерфейс HttpSe s s ion Класс Cookie Класс HttpServlet Обработка запросов и ответов НТТР Обработка НТТР-запросов GET Обработка НТТР-запросов POST Использование сооkiе-наборов Отслеживание сеансов Часть V. Приnожения Приложение А. Использование документирующих комментариев Java Дескрипторы j avadoc @author { @ code } @ deprecated { @ docRoot } @ except ion @ hi dden { @ inde x } { @ inheritDoc } { @ l in k } { @ l inkpla i n } { @ l i teral } @pa ram @provides @ return @ see @ se r ia l @ s e r i a l Data @ s e r i a l Field @ s i nce { @ summa ry } { @ systemProperty } 1294 1294 1294 1 295 1296 1297 1 297 1298 1299 1299 1300 1300 1300 1300 1 302 1302 1304 1305 1306 1307 1308 1309 1310 131 1 1313 1315 1 3 16 1316 1318 1318 1318 1318 1319 1319 1319 1319 1319 1320 1320 1320 1320 1320 1321 1321 1321 1321 1322 1322 1 322 Содержан ие @throws @uses { @val ue } @version Общая форма документирующего комментария Вывод утилиты j avadoc Пример использования документирующих комментариев 27 1322 1322 1323 1323 1323 1323 1324 Приложение Б. Введение в JShell Основы JShell Просмотр, редактирование и повторного выполнение кода Добавление метода Создание класса Использование интерфейса Вычисление выражений и использование встроенных переменных Импортирование пакетов Иск лючения Другие команды JShell Дальнейшее исследование JShell 1325 1325 1328 1329 1330 1331 1332 1333 1334 1334 1335 Приложение В. Компиляция и запуск простых однофайловых программ за один шаr 1336 Предметный указатель 1338 О б авторе Ав тор многочисленных бестселлеров Герберт Шилдт занимался напи­ санием книг по программированию на протяжении более трех десятилетий и считается признанным автори тетом по яз ыку Java. Журнал International Developer назвал его одним из ведущих авторов книг програ ммиров анию в мире. Книги Герберта Шилдта продав ались миллионными тиражами по всему миру и переведены на многие яз ыки. Он является автором многочисленных книг по Java, в том числе Java: руководство для начинающих, Java: методики программирования Шилдта, Introducing JavaFX 8 Programming и Swing: руковод­ ство для начинающих. Герберт Шилдт также много писал о языках С, С++ и С#. В книге Эда Бернса Secrets of the Rock Star Programmers: Riding the [Т Crest указ ано, что как один из звездных программистов Шилдт инт ересуется всеми аспектами вычислений, но его основное внимание сосредоточено на языках про граммирования. Герберт Шилдт получил степени бакалавра и магистра в Иллино йском университете. Его сайт доступен по адресу www . HerbSch i ldt . com. О н ау ч ном реда кторе Доктор Дэнни Ковард работал над всеми версиями платформы Java. Он руководил определением сервлетов Java в первой версии платформы Java ЕЕ и в более поздних версиях, внедрением веб-служб в Java МЕ, а также стратегией и планированием для Java SE 7. Он основал технологию JavaFX и совсем недав­ но создал крупнейшее дополнение к стандарту Java ЕЕ 7, Java WebSocket API. Доктор Дэнни Ковард обладает уникально широким взглядо м на многие аспекты технологии Java. Его опыт простирается от написания кода на Java до разработки API с отраслевыми экспертами и работ ы в течение нескольких лет в качестве исполни тельного директора в Java Community Process. Кроме того, он является автором двух книг по про граммированию на Java: Java WebSocket Programming и Java ЕЕ 7: The Big Picture. Со всем недавно он применил свои зна­ ния Java, по могая масштабировать крупные службы на основе Java для одной из самых успешных в мире компаний-разработ чиков программного обесп е­ чения. До ктор Дэнни Ковард получил с т епень бакалавра, магистра и доктора математики в Оксфордском унив ерситете. Предисловие Java - один из самых важных и широко используемых языков програ м­ мирования в мире. На протяжении многих лет ему была присуща эта отличи­ тельная особ енность. В отличие от ряда других языков программирования, влияние которых с течением времени ослаб евало, влияние Java становило сь только сильнее. С мо мента своего первого выпуска язык Java выдвинулся на передний край программирования для Интернета. Его позиции закреплялись с каждой последу ющей версией. На сегодняшний день Java по-прежнему яв­ ляется первым и лучшим выбором для разработки веб-приложений, а также мощным языком программирования общего назначения, подходящий для са­ мых разных целей. Проще говоря, большая часть современного кода написана на Java. Язык Java действительно настолько важен. Ключевая причина успеха языка Java кроется в его гибкости. С мо мента своего первоначального выпуска 1.0 он постоянно адаптировался к измене­ ниям в среде программирования и к изменениям в спосо бах написания кода программистами. Самое главное то, что язык Java не просто следовал тенден­ циям - он по могал их создавать. Способность языка Java приспосабливат ься к быстрым изменениям в мире программирования является важной частью того, по чему он был и остается настолько успешным. С момента первой публикации этой книги в 1996 году она выдержала мно­ жество переизданий, в каждом из которых отражалась непрерывная эволю­ ция Java. Текущее двенадцатое издание книги обновлено с уче том Java SE 1 7 (JDK 17). В и тоге оно содержит значит ельный объем нового материала, об­ новлени й и изменений. Особый интерес представляет обсуждение следую­ щих ключевых возможностей, которые были добавлены в язык Java в сравне­ нии с предыдущим изданием: • усовершенствования оператора swi tch; • записи; • сопо ставление с образцом в instanceof; • запечатанные классы и инт ерфейсы; • текстовые блоки. 30 Предисловие В совокупности они составляют существенный набор новых функцио­ нальных средств, которые значительно расширяют диапазон охвата, область применимости и выразительность языка. Усовершенствования swi tch до­ бавляют мощи и гибкости этому основополагающему оператору управления. Появившиеся записи предлагают эффективный способ агрегирования данных. Добавление сопоставления с образцом в i nstanceof обеспечивает более ра­ циональный и устойчивый подход к решению обычной задачи программиро­ вания. Запечатанные классы и интерфейсы делают возможным детализиро­ ванный контроль над наследованием. Текстовые блоки позволяют вводить многострочные строковые литералы, что значительно упрощает процесс встав­ ки таких строк в исходный код. Все вместе новые функциональные средства существенно расширяют возможности разработки и внедрения решений. К ни га для всех пр ог раммистов Книга предназначена для всех программистов: от новичков до професси­ оналов. Новичок сочтет особенно полезными тщательно продуманные об­ суждения и м ножество прим еров. Проф ессионалам понравится подробное описание более сложных функциональных средств и библиотек Java. И те, и другие получат в свое распоряжение прочный информационный ресурс и удобный справочник. Ст ру кту ра кн и r и Эта книга пр едставляет собой исчерпывающее руководство по языку Java , в котором описаны его синтаксис, ключевые слова и базовые принципы про­ граммирования. Вдобавок исследуется значительное подмножество библио­ теки Java API. Книга разделена на ч етыре части, каждая из которых посвяще­ на отдельному аспекту среды программирования Java. Часть I предлагает подробное учебное пособие по языку Java. Она начи­ нается с основ, включая типы данных, операции, управляющие операторы и классы. Затем обсуждаются наследование, пак еты, интерфейсы, обработка исключений и многопоточность. Далее описаны аннотации, перечисления, автоупаковка, обобщения, модули и лямбда-выражения. Также пр едлагается введение в систему ввода-вывода. В последней главе части I рассматрива­ ется несколько недавно добавленных ср едств: записи, запечатанные классы и интерф ейсы, расширенный оператор swi tch, сопоставление с образцом в instanceof и текстовые блоки. В части II исследуются ключевые асп екты стандартной библиотеки API Java, включая строки, ввод-вывод, работу в сети, стандартные утилиты, Collections Framework, AWT, обработку событий, визуализацию, параллелизм (в том числе Fork/Join Framework), регулярные выражения и библиотеку потоков. В трех главах части III дается введе ние в инфраструктуру Swing. Часть IV состоит из двух глав, в которых демонстрируются примеры при­ ложений Java. В одной главе обсуждается компоненты Java Beans, а в другой пр едставлено введение в сервлеты. Предисл ов ие 31 Бла годарности Я хочу выраз ить особую благодарность Патрику Нотону, Джо О'Нилу и Дэнни Коварду. Патрик Нотон был одним из создателей языка J ava. Он также помог на­ писать первое издание этой книги. Например, помимо многих других вкладов большая часть материала в главах 22, 24 и 28 изначально был а предоставлена Патриком. Его проницательность, опыт и энергия во многом с пособствовали успеху книги. Во время подготовки второго и третьего изданий этой книги Джо О'Нил предоставил первоначальные наброски материала, который сейчас содержит­ ся в главах 3 1 , 33, 35 и 36 текущего издания. Джо помогал мне при написании нескольких книг, и его вклад всегда был превосходным. Дэнни Ковард з анимался научным редактированием этого издания книги. Дэнни работал над несколькими моими книгами, и его советы, идеи и пред­ ложения всегда имели большую ценность и получал и высокую оценку. Я также хочу поблагодарить мою жену Шерри за весь ее вклад в эту и в другие мои книги. Ее вдумчивые советы, корректура и подготовка предмет­ ного указателя всегда в з начительной степени способствовал и успешному за­ вершению каждого проекта. Гер берт Шилдт Ж дем ва ш их отзывов ! Вы, читатель этой книги, и есть главный ее критик. Мы ценим ваше мнение и хотим з нать, что было сделано нами правил ьно, что можно было сделать лучше и что еще вы хотели бы увидеть изданным нами. Нам интересны лю­ бые ваши з амечания в наш адрес. Мы ждем ваших комментариев и надеемся на них. Вы можете прислать нам электронное письмо либо просто посетить наш веб-сайт и оставить свои замечания там. Одним словом, любым удобным для вас способом дайте нам знать, нравится ли вам эта книга, а также выскажите свое мнение о том, как сделать наши книги более интересными для вас. Отправляя письмо или сообщение, не з абудьте указать наз вание книги и ее авторов, а также свой обратный адрес. Мы внимател ьно оз накомимся с вашим мнением и обязательно учтем его при отборе и подготовке к изданию новых книг. Наши электронные адреса: E-mail: WWW: in fo . dia lekt i ka @ grna i l . com http : / /www . w i l l iamspuЫ i shing . com ЧАСТЬ 1 ГЛАВА 1 История и эволюция языка Java ГЛАВА 2 Кратк111й об1ор языка Java 1",ММ J Тиnы д..ннЬ!i, nеременн111е '1 mA8A 4 Оnерацми fdccмai1 J"МltA,5 Уnраем�ощме onepa'fOPЫ ГЛАВА 6 Введение в кnaccti1 ГЛАВА 7 Подробный анализ �ов и кnассов ГПАВА В Несnелов:ание rnABA t Пвкеt111 и интерфейсы rnABA 10 Обра6от1(а IIOtЛIOЧetn!Й ГПАIА 1 1 Мноrоnоточное программирование rnABA 12 Перечисления, автаупаковка и аннотации rnABA 13 Ввод-еьщод, ()nерв,ор t.;y с ресурс,ми и.QРУГм rемЬ! ГПА8А 14 Обобщения ГIIABA 15 Ля мбда·выраже ния mABA 16 Модули rnABA 17 8111ражения sWltch, записи и nрочме недаано добl'Вnенные средства Язык Java ГЛ А ВА И стори я и э вол ю ци я я зы к а Java Глубокое понимание языка Java предусматривает знание причин его соз­ дания, факторов, придавших ему такую форму, и особ енностей, которые он унаследовал. Как и предшествующие усп ешные языки программирования, Java представляет собой смесь наилучших элементов своего богатого насле­ дия в сочетании с новаторскими концепциями, необходимыми для достиже­ ния его уникальной цели. В остальных r лавах книги описаны практические аспекты Java, включая синтаксис, основные библиот еки и приложения, а в этой главе обсуждаются причины появления языка Java, его важность и раз­ витие с течением лет. Хотя язык Java неразрывно связан с онлайновой средой Интернета, важ­ но помнить о том, что Java в первую очередь является языком программиро­ вания. Развити е и создание языков программирования происходят по двум фундаментальным причинам: • адаптация к меняющимся средам и сценариям использования; • внедрени е улучшений и усовершенс твований в область про граммиро­ вания. Как вы увидите, создани е Java б ыло обусловлено обеими причинами почти в равной степени. Про исх ож ден ие Java Язык Java связан с языком С++ - прямым потомком С. Большинство ха­ рактерных особенностей Java унаследовано от упо мянутых двух языков. От С язык Java получил синтаксис. На многие средства объектно-ориентирован­ но го программирования Java повлиял язык С++. На самом деле некоторые определяющие характ еристики Java происходят от предшественников или являются ответом на них. Кроме того, создание Java уходит глу боко в процесс улучшения и адаптации, который происходил в языках программирования на протяжении нескольких прошедших десятилетий. По указанным причинам в этом разделе рассматривается последовательность событий и движущих сил, которые прив ели к Java. Вы увиди те, что каждое нововведение в конструкции Глава 1 . Ипо р и А и ЭВОЛЮЦИА АЗЫКа Java 35 языка было обусловлено необходимостью решения фунда ментальной зада­ чи, которую нельзя было решить с помощью существовавших ранее языков. Язык Java - не исключение. З арождение современн ого п рограммирования : я з ык С Язык С серьезно вскол ыхнул компьютерный мир. Влияние языка С не следует недооценивать, поскольку он кор енным образом изменил подход к программированию и его понимание. Создание С было прямым результатом потребности в структурированном, эфф ективном, высокоуровневом языке, который смог бы заменить ассемблерный код при написании системных про­ грамм. Вероятно, вам известно, что при проектировании языка программи­ рования часто идут на компромиссы, такие как: • простота использования или возможности; • безопасность или эфф ективность; • жесткость или расширяемость. До появления С программ иста м обыч но приходилось выбирать между языками, которые оптимизировали один или другой набор характеристик. Например, хотя язык FORTRAN можно было применять при создании до­ вольно эффективных программ для научных расч етов, он не особо хорошо подходил для написания системного кода. Несмотря на простоту изучения языка BASIC, он не обладал широкими возможностями, а отсутствие струк­ турированности ставило под сомнение его пригодность для создания круп­ ных программ. Язык ассемблера можно было задействовать при построении высокопроизводительных программ, но изучать и эфф ективно использовать его нелегко. Вдобавок отладка ассемблерного кода может оказаться достаточ­ но непростой. Еще одна сложная проблема заключалась в том, что ранние языки про­ граммирования наподобие BASIC, COBOL и FORTRAN не проектировались с учетом принципов структурирования. Взамен они полагались на оператор GOTO как на главное средство управления потоком выполнения программы. В результате возникла тенденция появления в программах, написанных на та­ ких языках, так называемого "спагетти-кода" - массы запутанных пер еходов или условных ответвлений, из-за которых программы было практически не­ возможно понять. Хотя языки вроде Pascal являются структурированными, они не разрабатывались для обеспеч ения эфф ективности и не имели опре­ деленных функций, необходимых в целях их прим енимости в широком диа­ пазоне программ. (В частности, учитывая доступные в то время стандартные диал екты языка Pascal, не имело смысла рассматривать его использование для написания кода системного уровня.) Итак, незадолго до изобретения С, невзирая на все прилагаемые усилия, ни один существующий язык не сглаживал конфликт м ежду противореч ивы­ ми характеристиками. Однако потребность в таком языке была насущной. 36 Часть 1. Язык Java К началу 1970-х годов компьютерная революция стала набирать обороты, и спрос на программное обеспечение быстро опередил возможности програм­ мистов создавать его. В академических кругах прилагалось много усилий для проектирования лучшего языка программирования. Но, пожалуй, важнее все­ го то, что начала воздействовать дополнительная сила. Компьютерное обору­ дование наконец-то стало настолько распространенным, что была достигнута критическая масса. Компьютеры больше не находились за запертыми дверя­ ми. Впервые программисты получили фактически неограниченный доступ к своим машинам, что дало свободу для экспериментирования. Кроме того, это позволило программистам начать разработку собственных инструментов. Накануне создания С была подготовлена почва для качественного скачка в области языков программирования. Изобретенный и первоначально реализованный Деннисом Ритчи на маши­ не DEC PDP-11 под управлением операционной системы (ОС) UNIX язык С стал результатом процесса разработки, который начался с более старого язы­ ка под названием BCPL, созданного Мартином Ричардсом. Язык BCPL оказал влияние на язык В, изобретенный Кеном Томпсоном, который привел к раз­ работке С в 1970-х годах. В течение долгих лет фактическим стандартом язы­ ка С служила версия, которая поставлялась вместе с ОС UNIX и описана во втором издании книги Брайана Керниrана и Денниса Ритчи Язwк программиро­ вания С (ИД "Вильяме� 2006 год). Язык С был официально стандартизирован в декабре 1989 года, когда Американский национальный институт стандартов (American National Standards lnstitute - ANSI) принял стандарт для С. Многие с читают, что создание языка С положило начало современной эпохи языков программирования. Он успешно объединил в себе противоре­ чивые характеристики, которые представляли собой источник немалого бес­ покойства в более ранних языках. В результате появился мощный, эффектив­ ный, структурированный язык, который было относительно легко изучать. Ему также присуща еще одна почти неуловимая особенность: он являлся язы­ ком для программистов. До изобретения С языки программирования обычно разрабатывались либо в виде академических упражнений, либо бюрократи­ ческими организациями. Другое дело язык С. Он проектировался, реализо­ вывался и развивался практикующими программистами, отражая их подход к программированию. Его функциональные средства были отточены, проте­ стированы, продуманы и переосмыслены людьми, которые действительно ис­ пользовали язык. Итогом стал язык, с которым программистам нравится ра­ ботать. Более того, язык С быстро приобрел многочисленных приверженцев, которые демонстрировали почти религиозный пыл по отношению к нему. Таким образом, он нашел широкое признание в сообществе программистов. Выражаясь кратко, С представляет собой язык, спроектированный програм­ мистами и предназначенный для программистов. Как вы увидите, язык Java унаследовал эту традицию. Гл ава 1 . История и эволюция языка Java С++: следующий шаг 37 За период с конца 1 970-х до начала 1 980-х годов С стал доминирую щим языком программирования и продолжает широко применяться в наши дни. Поскольку С является успешным и удобным языком, у вас может воз никнуть вопрос: почему существует потребность в чем-то еще? Ответ - сложность. На протяжении всей истории программирования возрастающая сложность программ вызывала необходимость в поиске л учших способов управления такой сложностью. Язык С++ стал реакцией на эт у потребность. Чтобы л уч­ ше понять причины, по которым управление сложностью крайне важно для создания С++, ознакомьтесь со следующими рассуждениями. С момента изобретения компьютера подходы к програм мированию з на­ чительно из менились. Например, когда компьютеры только появились, про­ грам мирование выглядело как ручное переключение двоичных машинных инструкций на перед ней панели компьютера. Пока программы состоял и из нескольких сотен инструкций, такой подход был приемлемым. С ростом раз­ мера программ был изобретен язык ассемблера, чтобы программист мог ра­ ботать с более крупными и постоянно усложнявшимися программами, при­ меняя символические представления машинных инструкций. По мере того как раз мер программ продолжил увеличиваться, появил ись языки высокого уровня, которые предоставили программистам больше инструментов, пред­ назначенных для управления сложностью. Первым широко распространенным языком был, конечно же, FORTRAN. Хотя FORTRAN стал впечатляющим первым шагом, в те времена его труд­ но было назвать языком, способствующим созданию ясных и простых для понимания про грамм. В 1 960-х годах з ародилось структ урное программиро­ вание. Эта методика программирования воплотилась в таких языках, как С. За счет использования структ урированных языков у программистов впервые появилась воз можность довольно легко писать умеренно сложные програм­ мы. Тем не менее, даже в случае применения методов структ урного програм­ мирования при достижении проектом определенного размера его сложность превышала тот порог, с которым мог справиться программист. К началу 1 980-х годов м ногие проекты вышли за рамки воз можностей структ урного программирования. Для решения проблемы был изобретен новый способ программирования, названный объектно-ориентированным программиро­ ванием (ООП). Объектно-ориентированное программирование подробно обсуждается далее в книге, а пока вот его краткое определение: ООП - это программная методология, которая помогает приводить в порядок сложные программы с использованием наследования, инкапсуляции и полиморфизма. В конечном итоге, хотя С является одним из вел иколепных языков про­ граммирования, его способность управлять сложностью имеет предел. После того как раз мер программы превышает определенную величину, она стано­ вится нас только сложной, что ее трудно воспринимать как единое целое. Несмотря на то что точный раз мер, при котором это происходит, отличается в з ависимости от природы программы и программиста, всегда существует 38 Ч асть 1 . Я зык Java порог, по достижении которого программа становится неуправляемой. В С + + были добавлены средства, позволяющие преодолеть такой порог, что позво­ лило программистам понимать и управлять более крупными программами. Язык С + + был изобретен Бьярне Страуструпом в 1 979 году, когда он ра­ ботал в Bell Laboratories (Мюррей Хил л , Н ью-Джерси). Первоначально Страуструп назвал новый язык "С with Classes" (С с к лассами). Однако в 1983 году название изменилось на С++. Язык С++ расширяет С за счет добавления объектно-ориентированных возможностей. Поскольку язык С + + построен на основе С, он поддерживает все возможности, характерные черты и пре­ имущества С. Это важнейшая причина успеха С + + как языка. Изобретение С + + не было попыткой создать совершенно новый язык программирования. Наоборот, он задумывался как усовершенствование уже крайне успешного языка. Услови я для появления я зыка Java В конце 1980-х и начале 1990-х годов О ОП с использованием С + + обрело популярность. Действительно, на мгновение показалось, что программисты наконец-то нашли идеальный язык. Так как язык С + + сочетал в себе высокую эффективность и стилистические элементы С с объектно-ориентированной парадигмой, его можно было применять для создания широкого спектра про­ грамм. Тем не менее, как и в п рошлом, назревали силы, которые снова под­ тол кнули вперед эволюцию языков программирования. Через несколько лет Всемирная паутина и Интернет достигнут критической массы. Это событие ускорит еще одну революцию в программировании. Создан и е языка Java Язык Java придумали Джеймс Гослинг, Патрик Нотон, Крис Уарт, Эд Франк и Майк Шеридан из компании Microsystems, Inc. в 1991 году. Разработка пер­ вой рабочей версии заняла полтора года. Первоначально язык получил на­ звание Oak, но в 1995 году был переименован в Java. Между исходной реали­ зацией Oak осенью 1992 года и п убличным объявлением о Java весной 1995 года многие л юди внесли свой вклад в проектирование и эвол юцию языка. Так, Бил л Джой, Артур ван Хофф, Джонатан Пэйн, Френк Йел л ин и Тим Линдхольм сыграли к лючевую роль в совершенствовании самого п ервого п рототипа. В какой-то мере неожиданно, но первоначальной побудительной причиной создания Java был вовсе не Интернет! Взамен главной движущей силой стала потребность в независимом от платформы (т.е. архитектурно-нейтральном) языке, который можно было бы использовать для построения программного обеспечении, встраиваемого в разнообразные бытовые электронные устрой­ ства, такие как микроволновые п ечи и пульты дистанционного управления. Вероятно, вы уже догадались, что в качестве контроллеров п рименялись про­ цессоры м ногих разных типов. Проблема с языками С и С ++ (и большин- Гла в а 1 . И стори я и э вол юция яз ы ка Java 39 ством других яз ыков того времени) заключалась в том, что написанный на них код должен был компилироваться для конкретной целевой платформы. Хотя программу на С++ можно компилировать практи чески для любого типа процессора, для этого необходим полный компилятор С++, ориентирован­ ный на такой тип процессора. Дело в том, что создание компиляторов сопря­ жено с высокими з атратами. Требовалось более простое и менее дорогое ре­ шение. В попытке найти подходящее решение Гослинr и другие начали работу над переносимым, нез ависимым от платформы языком, который можно было бы использовать для выпуска кода, способного выполняться под управлени­ ем раз нообраз ных процессоров в отличающихся средах. В конечном итоге их усилия привели к созданию Java. Примерно в то время, когда прорабатывались детали Java, появился вто­ рой и, как выяснилось, более важный фактор, который сыграет решающую роль в будущем Java. Второй движущей силой стала, конечно же, Всемирная паутина (она же веб-сеть). Если бы формирование веб-сети не происходило по чти одновременно с реализ ацией Java, то язык Java мог остаться полез ным, но малоизвестным яз ыком для программирования бытовой электроники. Однако благодаря появлению веб-сети Java вышел на передний край проек­ тирования языков программирования, т.к. веб-сеть тоже нуждалась в пере­ носимых программах. Большинство программистов еще в начале своей карьеры осоз нают, что переносимые программы в равной степени и желательны, и недостижимы. Хотя поиск способа соз дания эффективных и переносимых (нез ависимых от платформы) программ велся с момента появления самой дисциплины про­ граммирования, он отодвигался на второй план другими, более сро чными проблемами. Кроме того, поскольку (в то время) почти весь компьютерный мир был раз делен на три конкурирующих лагеря, Intel, Macintosh и UNIX, большинство программистов оставались в рамках своих укрепленных гра­ ниц, что снижало срочность потребности в переносимом коде. Тем не менее, с появлением Интернета и веб-сети старая проблема переносимости воз вра­ тилась с новой силой. В конце концов, Интернет представляет собой много­ образ ную распределенную вселенную, наполненную компьютерами, ОС и процессорами разли чных типов. Несмотря на то что к Интернету подклю­ чается много видов платформ, пользователи хотели бы иметь воз можность з апускать на всех платформах одну и ту же программу. То, что раньше было досадной, но низкоприоритетной проблемой, превратилось в важную необ­ ходимость. К 1993 году членам группы проектировщиков Java стало очевидно, что про­ блемы переносимости, часто воз никающие при написании кода для встраива­ емых контроллеров, также обнаруживаются при попытке соз дания кода для Интернета. По сути, ту же самую проблему, для решения которой из началь­ но проектировался яз ык Java в мелком масштабе, можно было применить к Интернету в большем масштабе. Это осоз нание привело к тому, что основное внимание Java переключилось с бытовой электроники на программирование 40 Часть 1 . Язы к Java для Интернета. Таким образом, хотя первоначальной мотивацией было стрем­ ление создать архитектурно-нейтральный язык программирования, именно Интернет в конечном итоrе привел к крупномасштабному успеху Java. Как упоминалось ранее, язык Java наследует многие характеристики от языков С и С ++. Так было задумано. Проектировщики Java знали, что исполь­ зование знакомого синтаксиса С и повторение объектно-ориентированных возможностей С + + сделают их язык привлекательным для многочисленных опытных программистов на С /С ++. Помимо внешнего сходства языку Java присущи другие особенности, которые обеспечили успех языкам С и С ++. Во-первых, Java проектировался, тестировался и совершенствовался прак­ тикующими программистами. Он представляет собой язык, основанный на потребностях и опыте людей, которые ero придумали. Следовательно, Java язык для программистов. Во-вторых, Java является целостным и логически непротиворечивым. В-третьих, за исключением ограничений, налагаемых средой Интернета, язык Java предоставляет вам как программисту полный контро ль. Если вы программируете хорошо, тоrда это отразится на ваших программах. Если вы программируете плохо, то и это скажется на ваших про­ граммах. Другими словами, Java - язык не для содействия обучению, а язык для профессиональных программистов. Из-за сходств языков Java и С + + возникает соблазн считать Java просто "версией С ++ для Интернета': Однако думать так было бы крупным заблуж­ дением. В языке Java имеются существенные практические и философские от­ личия. Хотя язык С++ действительно повлиял на Java, язык Java не является расширенной версией С++. Например, Java не обладает ни прямой, ни обрат­ ной совместимостью с С + +. Конечно, сходства с С + + значительны, и если у вас есть опыт программирования на С++, тоrда с Java вы будете чувствовать себя как дома. И еще один момент: Java не проектировался для замены С ++. Язык Java был разработан для решения определенного множества задач. Язык С + + создавался для решения дpyroro множества задач. Оба они будут сосу­ ществовать в течение многих лет. Как упоминалось в начале rлавы, языки программирования развивают­ ся по двум причинам: адаптация к изменениям среды и реализация новых идей в области программирования. Изменением среды, которое побудило к созданию Java, была потребность в независимых от платформы програм­ мах, предназначенных для распространения в Интернете. Тем не менее, Java также олицетворяет изменения в подходе к написанию программ. С кажем, в языке Java улучшена и усовершенствована объектно-ориентированная пара­ дигма, используемая в С + +, добавлена интегрированная поддержка мноrопо­ точности и предоставлена библиотека, которая упрощает доступ в Интернет. Однако в конечном итоrе не индивидуальные особенности языка Java сделали ero настолько замечательным, а скорее весь язык в целом. Java был идеаль­ ным ответом на возникшие в то время потребности сильно распределенной компьютерной вселенной. Язык Java стал в программировании для Интернета тем, чем язык С бы л для системного программирования: революционной си­ лой, которая изменила мир. Глава 1 . Ист о рия и эволюц и я языка Java 41 Связь с языком С # Охват и мощь Java п родолжили оказывать влияние на мир разработки языков програм мирования. Многие новаторские возможности, конструкции и концепции Java стали основой для любого нового языка. Просто успех Java слишком важен, чтобы его можно было игнорировать. Пожалуй, самым значительным примеро м влияния Java следует считать язык С#. Созданный в Microsoft для помержки .NET Fгamewoгk, язык С# тесно связан с Java. Скажем, оба языка и меют тот же общий синтаксис, помержива­ ют распределенное програм мирование и задействуют одну и ту же объектную модель. Разумеется, между Java и С# существуют отличия, но в целом "внешний вид и поведение" этих языков очень похожи. Такое "перекрестное опыление" Java и С# является наиболее веским свидетельством того, что язык Java изме­ нил наше представление об использовании языков програм мирования. Влияние языка Java на И нтернет Интернет помог языку Java выйти на передний край програм мирования, а язык Java в свою очередь оказал глубокое влияние на Интернет. Помимо упрощения програм мирования для веб-сети в целом благодаря Java появился новый вид сетевых п рограмм, названных аплетами, которые изменили пред­ ставление онлайнового мира о содержимом. Кроме того, язык Java решил ряд самых трудных проблем, связанных с Интернетом: переносимость и безопас­ ность. Давайте рассмотрим каждую проблем у по отдельности. Аплеты Java На момент создания Java одной из наиболее захватывающих возможно­ стей был аплет. Аплет - это особый вид програм м ы на Java, который предна­ значен для передачи через Интернет и автоматического выполнения внутри веб-браузера, совместимого с Java. После щелчка пользователем на ссылке, содержащей аплет, он загружается и запускается в браузере. Аплеты задумы­ вались как небольшие програм мы. Они обычно применяются для отображе­ ния данных, предоставленных сервером, обработки пользовательского ввода или поддержки простых функций, таких как расчет процента по кредиту, ко­ торые выполняются локально, а не на сервере. По существу аплет позволяет переместить определенную функциональность из серверной стороны на кли­ ентскую сторону. Появление аплетов было важным, потом у что в то время они расширя­ ли совокупность объектов, которые м ожно было свободно перемещать в киберпространстве. Вообще говоря, есть две очень крупные категории объ­ ектов, передаваемых между сервером и клиентом: пассивная информация и динамические, активные програм мы. Например, когда вы читаете сообщения электронной почты, то просматриваете пассивные данные. Даже п ри загрузке програм м ы ее код по-прежнему будет только пассивными данными, пока вы 42 Часть 1. Яз ык Java не запустите программу. Напротив, аплет пр едставляет собой динамическую самостоятельно запускающуюся программу. Такая программа является ак­ тивным агентом на клиентском компьютере, но инициируется сервером. В самом начале существования Java аплеты были важной частью програм­ мирования на Java. Они продемонстрировали мощь и преимущества Java, увеличили привлекательность веб-ст раниц и позволили програм мистам в полной мере изучить возможности Java. Несмотря на то что аплеты все еще используются в наши дни, с теч ением времени они стали менее важными. По причинам, которые будут объясняться позже, в версии JDK 9 началась фаза постепенного вывода аплетов из употребления, а в версии JDK 11 поддержка аплетов была пр екращена. Б е зоп асн ость Какими бы желанными ни были дина мические сетевы е програ ммы, они также могут порождать серьезные проблемы в областях безопасности и переносимости. Вполне очевидно, что необходимо не допустить нанесение ущерба программой, которая загружается и выполняется на кли ентском ком­ пьютере. Кроме того, программа должна иметь возможность запускаться в разнообразных ср едах и под управлением разных ОС. Вы увидите, что Java эфф ективно и элегантно решает эти задачи. Взглянем на них более присталь­ но, начав с безопасности. Наверняка вам уже известно, что каждый раз, когда вы загружаете с виду "нормальную" программу, то идете на риск, поскольку загружаемый код мо­ жет содержать вирус, "троянский конь" или другой вредоносный код. В осно­ ве проблемы лежит тот факт, что вредоносный код способен нанести ущерб, получив несанкционированный доступ к системным ресурсам. Скажем, ви­ русная программа может собирать конфиденциальную информацию вроде номеров кредитных карт, остатков на банковских сч етах и паролей, проводя поиск в локальной файловой системе вашего компьютера. Чтобы обеспечить безопасную загрузку и выполнение программ Java на клиентском компьюте­ ре, нужно было предотвратить запуск атак подобного рода. Такая защита достигается в Java за счет того, что вы можете огранич ивать функционирование приложения ср едой выполнения Java и запрещать ему доступ к другим областям ком пьют ера. (Вскоре вы узнаете, как это делать.) Возможность загрузки программ с определенной у вер енностью в том, что они не нанесут никакого вреда, вероятно, считается наиболее новаторским аспектом Java. Переносим ость Переносимость - важный аспект Интернета, т. к. к нему подключается много разнотипных компьютеров и ОС. Если программа на Java должна за­ пускаться практически на любом компьютере, подключенном к Интернету, тогда должен быть какой-то способ разрешать ее выполнение в разных систе- Глава 1 . И с тория и эвол ю ция язык а Java 43 мах. Другими словами, необходим механизм, который позволит одному при­ ложению загружаться и выполняться разнообразными процессорами, ОС и браузерами. Нецелесообразно иметь разные версии приложения для разных компьютеров. Один и тот же прикладной код обязан работать на всех ком­ пьютерах. Следовательно, потребовались средства генерации переносимого исполняемого кода. Как вы вскоре увидите, тот же самый механизм, который помогает обеспечивать безопасность, содействует и переносимости. М агия Java: ба йт- код Основная особенность Java, которая позволила решить только что опи­ санные проблемы переносимости и безопасности, заключается в том, что компилятор Java генерирует не исполняемый код, а байт-код. Байт-код пред­ ставляет собой оптимизированный набор инструкций, предназначенных для выполнения так называемой виртуальной машиной Java (Java Yirtual Machine - JYM), которая является часть ю исполняющей среды Java (Java Runtime Environment - JRE). По существу исходная версия JYM проекти­ ровалась как интерпретатор для байт-кода. Это может несколько удивить, поскольку многие современные языки предусматривают компиляцию в ис­ полняемый код из соображений безопасности. Тем не менее, тот факт, что программа на Java выполняется машиной JYM, помогает решить основные проблемы, связанные с программами для веб-сети. Рассмотрим причины. Трансляция программы на Java в байт-код значительно упрощает ее запуск в разнообразных средах, потому что для каждой платформы необходимо реа­ лизовать только машину JYM. Когда для заданной системы существует среда JRE, под ее управлением можно запускать любую программу на Java. Однако, хотя детали реализации машин JYM будут отличаться от платформы к плат­ форме, все они воспринимают один и тот же байт-код Java. Если бы програм­ ма на Java компилировалась в машинный код, тогда для каждого типа про­ цессора, подключенного к Интернету, должна была существовать своя версия программы. Конечно же, это нельзя считать приемлемым решением. Таким образом, выполнение байт-кода машиной JVM является самым легким спо­ собом получения по-настоящему переносимых программ. Факт выполнения программы на Java машиной JYM также делает ее без­ опасной. Поскольку машина JVM находится под контролем, она управляет выполнением программы. В итоге у JYM появляется возможность создать ограниченную среду выполнения, называемую песочницей, которая содержит программу, препятствуя неограниченному доступу к машине. Кроме того, по­ вышается и безопасность за счет определенных ограничений, имеющихся в языке Java. Вообще говоря, когда программа компилируется в промежуточную форму и затем интерпретируется виртуальной машиной, она выполняется медлен­ нее, чем в ситуации, если бы она компилировалась в исполняемый код. Тем не менее, в случае Java разница не настолько велика. Из-за того, что байт-код в 44 Часть 1. Язык Java высшей степени оптимизирован, его применение позволяет машине JVM вы­ полнять программы гораздо быстрее, нежели вы могли бы ожидать. Хотя язык Java был спроектирован как интерпретируемый, нет ничего, что помешало бы компилировать байт-код на лету в машинный код с целью по­ вышения производительности. По этой причине вскоре после первоначаль­ ного выпуска Java была представлена технология HotSpot, которая предлагала оперативный (Just-In-Time - JIT) компилятор для байт-кода. Когда компи­ лятор JIT входит в состав машины JVM, избранные порции байт-кода ком­ пилируются в исполняемый код в режиме реального времени, часть за ча­ стью по запросу. Важно понимать, что программа на Java не компилируется в исполняемый код сразу целиком. Взамен компилятор JIT компилирует код по мере необходимости во время выполнения. Более того, компилируются не все последовательности байт-кода, а только те, которые извлекут поль­ зу из компиляции. Остальной код просто интерпретируется. Однако подход JIT все же обеспечивает значительный рост производительности. Даже при динамической компиляции байт-кода характеристики переносимости и без­ опасности сохраняются, т.к. машина JVM по-прежнему несет ответственность за среду выполнения. Еще один момент: проводились эксперименты с ранней (ahead-of-time) компиляцией для Java. Такой компилятор можно использовать для компиля­ ции байт-кода в машинный код перед выполнением машиной JVM, а не на лету. Некоторые предшествующие версии JDK поставлялись с эксперимен­ тальным ранним компилятором; тем не менее, в версии JDK 17 он был удален. Ранняя компиляция - это специализированная возможность, и она не заме­ няет описанный выше традиционный подход Java. По причине узкоспециали­ зированной природы ранняя компиляция в книге обсуждаться не будет. Выход за рамки аплетов На момент написания этой книги с момента исходного выпуска Java про­ шло более двух десятилетий. За прошедшие годы произошло много измене­ ний. Во времена создания Java Интернет был захватывающей инновацией, веб-браузеры быстро развивались и совершенствовались, современная фор­ ма смартфона еще не была изобретена, а до повсеместного применения ком­ пьютеров оставалось еще несколько лет. Как и следовало ожидать, язык Java тоже изменился вместе со способом его использования. Возможно, ничто не иллюстрирует текущую эволюцию лучше, чем аплет. Как объяснялось ранее, в начальные годы существования Java аплеты были важной составляющей программирования на Java. Они не только увеличи­ вали привлекательность веб-страниц, но стали крайне заметной особенно­ стью языка Java, повышая его популярность. Однако аплеты полагаются на подключаемый модуль браузера для Java. Таким образом, чтобы аплет рабо­ тал, браузер должен его померживать. В течение последних нескольких лет помержка подключаемого модуля браузера для Java ослабла. Проще говоря, Гла ва 1 . История и эволюция языка Java 45 без поддержки браузера аплеты нежизнеспособны. По этой причине в версии JDK 9 начался поэтапный отказ от аплетов, и поддержка аплетов была объяв­ лена нерекомендуемой. В язы ке Java нерекомендуемое средство означает, что оно все еще доступно, но помечено как устаревшее. Следовательно, в новом коде нерекомендуемое средство применяться не должно. Поэтапный отказ завершился с выходом JDK 1 1, поскольку поддержка аплетов исполняющей средой была удалена. Начиная с версии JDK 17, весь АРI-интерфейс аплетов (Applet API) стал нерекомендуемы м и подлежащим удалению. Интересно отметить, что спустя несколько лет после создания в Java была добавлена альтернатива аплетам, которая называлась Java Web Start и позво­ ляла динамически загружать приложение из веб-страницы. Она представляла собой механизм развертывания, особенно удобный в случае крупных прило­ жений Java, которые не подходили для реализации в виде аплетов. Разница между аплетом и приложением Web Start заключалась в том, что приложение Web Start выполнялось само по себе, а не внутри браузера. Таким образом, оно выглядело во многом похоже на "нормальное" приложение. Тем не менее, это требовало наличия в размещающей системе автономной среды JRE, поддер­ живающей Web Start. В версии JDK 1 1 поддержка Java Web Start была удалена. Учитывая, что современные версии Java не померживают ни аплеты, ни Java Web Start, вас может интересовать, какой механизм должен использо­ ваться для развертывания приложения Java. На момент написания книги частью ответа был инструмент j l i n k, добавленный в версии JDK 9. С его помощью можно создавать полный образ, который включает всю необходи­ мую поддержку для вашей программы, в том числе среду JRE. Другая часть ответа - инструмент j package, появившийся в версии JDK 16. Его можно применять для создания готового к установке приложения. Хотя подробное обсуждение стратегий развертывания выходит за рамки настоящей книги, в будущем вам придется уделить внимание данной теме. Б олее быстрый гра ф и к в ы пуска Недавно в Java произошло еще одно крупное изменение, но оно не каса­ ется язы ка или исполняющей среды. Взамен изменение связано со способом планирования выпусков Java. В прошлом между основными выпусками Java обычно проходило два и больше лет. Однако после выхода JDK 9 промежутки времени между основными выпусками Java сократились. В наши дни ожида­ ется, что основной выпуск будет происходить по строгому графику, и расчет­ ное время между выпусками составит всего лишь полгода. Каждый полугодичный выпуск, теперь называемый выпуском функциональ­ ных средств, будет включать те средства, которые готовы к моменту выпуска. Такая увеличенная частота выпусков позволит программистам на Java полу­ чать своевременный доступ к новым средствам и улучшениям. Кроме того, у Java появится возможность быстро реагировать на запросы постоянно меняю­ щейся программной среды. Выражаясь просто, более быстрый график выпуска обещает стать очень позитивным событием для программистов на Java. 46 Часть 1 . Язык Java О жи дается, что каждые три года будет производиться выпуск с долго­ срочной помержкой (long-term support - LTS). Выпуск LTS будет поддер­ живаться (и, следовательно, оставаться жизнеспособным) в течение периода времени, превышающего полгода. Первым выпуском LTS был JDK 1 1. Вторым выпуском LTS стал JDK 1 7 , с учетом которого была обновлена эта книга. Из­ за стабильности, предлагаемой выпуском LTS, вполне вероятно, что его на­ бор средств будет определять базовый уровень функциональности для про­ межутка в несколько лет. Последние сведения о долгосрочной поддержке и графике выхода выпусков LTS ищите на веб-сайте Oracle. В текущее время выпуски функциональных средств запланированы на март и сентябрь каждого года. В результате JDK 10 вышел в марте 201 8 года, т.е. через полгода после выхода JDK 9. Следующий выпуск ( JDK 1 1) вышел в сентябре 201 8 года и был выпуском LTS. Затем последовал JDK 1 2 в марте 2019 года, JDK 13 в сентябре 2019 года и т.д. На момент написания книги по­ следним выпуском был JDK 1 7, который является выпуском LTS. Опять-таки ожидается, что каждые полгода будет выходить новый выпуск функциональ­ ных средств. Конечно, имеет смысл ознакомиться с актуальной информацией о графике выпусков. Во время написания книги на горизонте показ алось несколько новых функциональных средств Java. По причине более быстрого графика выпусков с высокой вероятностью можно предположить, что некоторые из них будут добавлены в Java в течение ближайших нескольких лет. Рекомендуется вни­ мательно просматривать сведения и замечания по каждому полугодовому выпуску. Сейчас действительно самое подходящее время, чтобы стать про­ граммистом на Java! Сервлеты : Java на серверной стороне Код н а клиентской стороне - лишь одна половина уравнения "клиент­ сервер': Довольно скоро после первоначального выпуска Java стало очевидно, что яз ык Java будет полезен и на серверной стороне. Одним из результатов стал сервлет, представляющий собой небольшую программу, которая выпол­ няется на сервере. Сервлеты используются для создания динамически генерируемого со­ держимого, впоследствии передаваемого клиенту. Например, онлайновый магаз ин может применять сервлет для поиска цены товара в баз е данных. Затем информация о цен е используется для динамического генерирования веб-страницы, которая отправляется брауз еру. Несмотря на то что динами­ чески генерируемое содержимое было доступно чер ез механизмы вроде CGI (Common Gateway Interface - интерфейс общего шлюза), с сервлетом связано несколько преимуществ, включая увеличенную производительность. Поскольку сервлеты (подобно всем программам на Java) компилируются в байт-код и выполняются машиной JVM, они обладают высокой переноси­ мостью. Таким образом, один и тот же сервлет может применяться в разно- Гл а в а 1 . И стори я и э вол юц и я я зык а Java 47 образ ных серверных средах. Единственное требование - поддержка на сер­ вере машины JVM и контейнера сервлетов. В настоящее время код серверной стороны в целом является основным использованием J ava. Терминологи я я зыка Java Никакое обсуждение истории Java не будет полным без рассмотрения тер­ минологии J ava. Хотя основными факторами, вызвавшими изобретение Java, был и переносимость и безопасность, важную роль в формировании финал ь­ ной формы яз ыки сыграли и другие факторы. Команда разработчиков J ava подытожила ключевые соображения в виде следующего списка терминов: • простота; • безопасность; • переносимость; • объектная ориентация; • надежность; • м ногопоточность; • нейтрал ьность к архитектуре; • интерпретируемость; • высокая производител ьность; • распределенность; • динамичность. Два термина из списка уже обсуж дались: безопасность и переносимость. Давайте выясним, что подразумевается под остальными. П рос тота Язык Java был спроек тирован так, чтобы быть легким в изучении и эффек­ тивным в использовании профессиональным программистом. Если у вас есть определенный опыт программирования, то освоить Java не составит особого труда. Если вы уже понимаете базовые концепции ООП, тогда изучение Java пройдет еще проще. Лучше всего то, что есл и вы - опытный программист на С++, то переход на Java потребует совсем небольших усилий. Поскол ьку язык Java унаследовал синтаксис С/С++ и многие объектно-ориентированные воз ­ можности С++, у бол ьшинства программистов не будет воз никать проблем с изучением Java. Объектна я ориен тация Несмотря на влияние своих предшественников, язык Java не проектиро­ вался так, чтобы быть совместимым на уровне исходного кода с любым дру- 48 Часть 1 . Яз ык Java гим языком. Это дало команде разработчиков Java свободу проектирования с чистого листа. Результатом стал ясный, удобный и прагмати чный подход к объектам. Обильно заи мствуя из многих продуктивных объектно-ориентиро­ ванных сред, существующих на протяжении последних нескольких десятиле­ тий, в Java удалось найти баланс между паради гмой "все является объектом" сторонников чистоты стиля и более прагмати чной моделью "не путайтесь под ногами•: Объектная модель в Java проста и легко расширяема, в то время как элементарные типы, такие как целочисленные, были сохранены высоко­ производи тельными сущностями, которые не являются объектами. Надеж ность Многоплатформенная среда веб-сети предъявляет к программе необы ч­ ные требования, потому что программа должна надежно выполняться в раз ­ нообраз ных системах. Таким образом, при проектировании Java возмож­ ности создания надежных про грамм был назначен высокий приоритет. Для обеспе чения надежности Java о грани чивает вас в ряде· клю чевых областей, чтобы вынудить и скать свои просчеты на ранней стадии проектирован ия программ. В то же время Java избавляет вас от необходимости беспокоиться о многих наиболее распространенных при чинах ошибок при программирова­ нии. Поскольку Java - строго типизированный язык, ваш код проверяется на этапе компиляции. Тем не менее, код также проверяется и во время выполне­ ния. Многие трудно обнаруживаемые ошибки, которые часто приводят к воз ­ никновению сложных для воспроизведения ситуаций во время выполнения, в Java попросту невозможны. Предсказуемость поведения написанного вами кода в несходных условиях - клю чевая особенность Java. Чтобы лучше понять, как обеспечивается надежность в Java, рассмотрим две главных причины отказ а программ: прос четы в управлени и памятью и неправильно обработанные исключительные ситуации (т.е. ошибки времени выполнения). В традиционных программных средах управление памятью мо­ жет оказаться сложной и утоми тельной задачей. Скажем, в С/С++ програм­ мист будет часто вручную выделять и освобождать динами ческую память. Подход подобного рода иногда приводит к возникновению проблем, потому что программисты будут либо з абывать об освобождении ранее выделенной памяти, либо, что хуже, пытаться освободить память, которая все еще з а­ действована в друго й части кода. Java практически устраняет указанные про­ блемы, самостоятельно управляя выделением и освобождением памяти. (На самом деле освобождение выполняется полностью автомати чески, поскольку Java обеспечивает сборку мусора для неиспользуемых объектов.) Условия для исклю чений в традиционных средах часто возникают в ситуациях вроде деле­ ния на ноль или отсутствия нужного файла, и справляться с ними приходится с помощью неуклюжих и трудных для чтения конструкций. Java помогает и этой области, предлагая объектно-ориентированную обработку исклю чений. В хорошо написанной программе на Java все ошибки времени выполнения могут - и должны - обрабатываться вашей программой. Глава 1 . История и эволюция языка Java 49 Мн огопоточ ност ь Язык Java был спро ектирован с целью удовлетворения реальной потреб­ ности в создании ин терактивных сет евых программ. Для достижения т ако й цели в Java поддерживается мноrопоточное программирование, которое дает воз можность писать программы, выполняющие много дейст вий одно вре­ менно. Исполняющая среда Java поддерживает элегант ное, но вместе с т ем сложное решение для синхронизации множества процессов, которо е делает воз можным построение бесперебо йно работающих инт ерактивных систем. Просто й в применении подход к многопоточности в Java позволяет вам со­ средоточить внимание на конкретном по ведении программы, а не думать о многозадачной подсистеме. Н ей трал ь ност ь к архитек ту ре Цент ральной з адачей проектиро вщиков Java было обеспечение долговеч­ ности и переносимости кода. На мо мен т создания Java одной из главных про­ блем, стоявших перед программист ами, было отсу тствие всяких гарантий того, что программа, написанная сегодня, будет выполняться завтра - даже на той же самой машине. Обновления ОС, модернизация процессоров и из ­ менения в ключевых системных ресурсах могут в совокупности привести к нарушению рабо тоспосо бности программы. Пытаясь изменить сложившу юся в то время си туацию, проектировщикам при шлось принять ряд жестких ре­ шений в отношении языка Java и машины JVM. Они преследовали цель "напи­ санное однажды выполняется везде, в любо е время, всегда': В значительной степени эта цель была достигнута. И н т е рпр етир уем ость и высокая пр о и з водител ь н ост ь Как было показано ранее, язык Java делает возможным создание межплат­ форменных программ за счет их компиляции в промежуточное представ­ лени е, называемое байт-кодом Java. Ба йт-код может выполнят ься на любо й системе, где внедрена машина JVM. В большинстве попыток построения меж­ платформенных решений это делалось ценой снижения производит ельности. Как объяснялось в начале главы, байт-код Java был тщательно спроектирован, чтобы легко транслироват ься прямо в машинный код для достижения очень высоко й производит ельности с использованием оперативного компилятора. Исполняющие среды Java, предлагающие такую возможность, не утрачивают преимуществ независимого от платформы кода. Распределенност ь Язык Java про ектировался для распределенно й среды Интернета, посколь­ ку он поддерживает протоколы ТСР /IP. Фактически дост уп к ресурсу с приме­ нением URL мало чем отличается от доступ к файлу. В Java т акже помержива­ ется средство удаленного вызова методов (Remote Method lnvocation - RMI), которо е позволяет программе вызывать методы через сет ь. 50 Часть 1. Язы к Java Динамичность Программы на Java содержат существенный объем информации о типах времени выполнения, используемой для проверки и разрешения доступа к объектам во время выполнения, что делает возможным безопасное и надле­ жащее динамическое связывание кода. Это критически важно для надежно­ сти среды Java, в которой небольшие фрагменты байт-кода могут динамиче­ ски обновляться в функционирующей системе. Э в ол юция язы ка Java Первоначальный выпуск Java по праву считался революционны м, но он не озна меновал собой конец эпохи быстрых инноваций Java. В отличие от боль­ шинства других программных систем, в которых обычно происходили не­ большие поэтапные улуч шения, язык Java продолжил ра звиваться взрывными темпами. Вскоре после выпуска Java 1 .0 проектировщики уже создали Java 1.1. Возможности, добавленные в Java 1 . 1, оказались более значительными и суще­ ственными, чем можно было судить по увеличению младшего номера версии. В Java 1 . 1 было добавлено много новых библиотечных элементов, изменен способ обработки событий и переконфигурированы многочисленные сред­ ства из библиотеки Java 1 .0. Кроме того, несколько средств, определенных в Java 1.0, стали нерекомендуемыми (признаны устаревшими). Таким образом, в Java 1 . 1 одновременно были добавлены и удалены характеристики исходной спецификации. Следующим крупным выпуском Java стал Java 2, где "2" означает "второе поколение': Создание Java 2 стало переломным событием, положившим на­ чало "современной эпохи" Java. Первый выпуск Java 2 имел номер версии 1.2. Может пока заться странным, что для первого выпуска Java 2 использовался номер версии 1.2. Причина в том , что первоначально он относился к внутрен­ нему номеру версии библиотек Java, но потом был обобщен для ссылки на целый выпуск. С выходом Java 2 в компании Sun перепаковали продукт Java как J2SE (Java 2 Platform Standaгd Edition - платформа Java 2, стандартная ре­ дакция) и номера версий стали при меняться к данному продукту. В Java 2 была добавлена поддержка нескольких новых средств, включая Swing и Collections Framework, а также усовершенствована машина JVM и различные программные инструменты. Кроме того, в Java 2 несколько средств стали нерекомендуемыми. Наиболее важные изменения коснулись класса Thread, в котором методы suspend ( ) , resurne ( ) и stop ( ) были объявлены нерекомендуемыми. Первым крупным обновлением первоначаль ного выпуска Java 2 стала версия J2SE 1 .3. По большей части она расширяла существующую функцио­ нальность и "усиливала" среду ра зработки. В общем случае программы, на­ писанные для версий 1.2 и 1.3, совместимы по исходному коду. Хотя версия 1.3 содержала меньший набор изменений, чем предшествующие три крупных выпуска , она все-таки была важна. Гла ва 1 . История и эволюция языка Java 51 Выпуск J2SE 1.4 допол нительно улучшил Java. Он содержал нескол ь ко важ­ ных обновл ений, улучшений и дополнений. Например, в J2SE 1.4 было вве­ дено ключевое слово assert, цепочки исключений и подсистема ввода-вы­ вода на основе каналов. Кроме тоrо, были внесены изменения в Collections Framework и в классы для работы с сетью. Вдобавок повсюду встречаются мноrочисленные мелкие изменения. Несмотря на значительное колич ество новых средств, версия 1.4 сохранила почти стопроцентную совместимость по исходному коду с предшествующими версиями. Следующим выпуском был J2SE 5, ставший революционным. В отличие от бол ьшинства предшествующих обновлений Java, которые предлаrали важные, но умеренные улучшения, выпуск J2SE 5 фунда ментальным образом расши­ рил rрани цы, возможности и област ь испол ьзования языка. Чтобы оценить значимость изменений, которые были внесены в Java в выпуске J2SE 5, взrля­ ните на сл едующий список, rде перечислены тол ько основные из всех новых функционал ьных средств: • обобщения; • аннотации; • автоупаковка и автораспаковка; • перечисл ения; • расширенный цикл for в стиле "for-each"; • арrументы переменной длины (vararg); • статический импорт; • форматированный ввод-вывод; • утилиты параллел изма. Мелкие подстройки или поэтапны е обновл ения в списке не указаны. Каждый элемент списка представляет значитель ное дополнение языка Java. Некоторые из них, такие как обобщения, расширенный цикл for и арrумен ­ ты переменной длины, вводил и новые синтаксические элементы. Друrие, в числ е которых автоупаковка и автораспаковка, изменяли семантику языка. Аннотации привнесли в проrраммирование совершенно новое измерение. Во всех случаях вл ияние этих дополнений вышло за рамки их прямоrо воздей­ ствия. Они изменили сам характер Java. Важность упомянутых выше новых средств отражена в назначенном номе­ ре версии - 5. В друrих условиях сл едующим номером версии Java был бы 1.5. Однако новые средства были настол ько значитель ными, что пер еход от версии 1 .4 к 1.5, казалось, просто не смоr бы выразить масштаб изменений. Взамен в Sun предпочли увел ич ить номер версии до 5, чтобы подчеркнуть тот факт, что произошло важное событие. Таким образом, выпуск получил название J2SE 5, а комплект разработчика - JDK 5. Тем не менее, ради соблю­ дения соrласованности в Sun решили исполь зовать 1.5 в качестве внутренне­ rо номера версии, который также называют номером версии разработчиков. Цифра 5 в J2SE 5 называется номером версии продукта. 52 Часть 1. Язык Java Следующий выпуск Java получил имя Java SE 6. В Sun снова решили изме­ нить название платформы Java. Прежде всего, обратите внимание, что циф­ ра 2 была отброшена. Соответственно, платформа стала называться Java SE, а официальный продукт - Java Platform, Standard Edition 6 (платформа Java, стандартная редакция). Комплекту разработчика на Java (Java Development Кit) было дано название JDK 6. По аналогии с J2SE 5 цифра 6 в Java SE 6 - это номер версии продукта. Внутренний номер версии разработчиков - 1.6. Выпуск Java SE 6 построен на основе J2SE 5 с добавлением поэтапных усо­ вершенствований. Он не дополнял какими-либо важными средствами сам язык Java, но расширил библиотеки API, добавил ряд новых пакетов и предо­ ставил усовершенствования исполняющей среды. Кроме того, в течение своего длинного (в понятиях Java) жизненного цикла он прошел через несколько мо­ дернизаций, попутно добавив некоторое количество обновлений. В целом вы­ пуск Java SE 6 послужил дальнейшему укреплению достижений выпуска J2SE 5. Очередным выпуском Java стал Java SE 7 с названием комплекта разработ­ чика JDK 7 и внутренним номером версии 1.7. Выпуск Java SE 7 был первым крупным выпуском Java после того, как компанию Sun Microsystems приобре­ ла компания Oracle. Выпуск Java SE 7 содержал много новых функциональных средств, включая существенные дополнения языка и библиотек API. Кроме того, в состав Java SE 7 входили обновления исполняющей среды Java, которые обеспечивали помержку языков, отличающихся от Java, но наибольший инте­ рес у программистов на Java вызывали языковые и библиотечные дополнения. Новые языковые средства были разработаны в рамках проекта Project Coin. Целью проекта Project Coin была идентификация ряда небольших из­ менений языка Java, подлежащих включению в JDK 7. Хотя в совокупности эти изменения называли "небольшими'; эффект от них был довольно значи­ тельным с точки зрения кода, на который они влияли. Фактически для мно­ гих программистов такие изменения вполне могли стать самыми важными новыми средствами в Java SE 7. Ниже приведен список языковых средств, до­ бавленных комплектом JDK 7. • Наделение типа String способностью управлять оператором swi tch. • Двоичные целочисленные литералы. • Символы подчеркивания в числовых литералах. • Расширение оператора try под название try с ресурсами, которое под­ держивает автоматическое управление ресурсами. (Например, потоки могут автоматически закрываться, когда они больше не нужны.) • Выведение типов (через ромбовидную операцию) при конструировании экземпляра обобщенного типа. • Усовершенствованная обработка исключений, при которой два или большее количество исключений можно перехватывать одним операто­ ром catch (многократный перехват), и улучшенная проверка типов для исключений, генерируемых повторно. Гл ава 1 . И с тория и эво л юция языка Java S3 • Хотя это не является изменением синтаксиса, была улуч шена инфор­ мативность выдаваемых компилятором предупреждений, связанных с некоторыми типами методов с аргументами переменной длины, и вы имеете больший контроль над предупреждениями. Как видите, несмотря на то, что средства Project Coin считались неболь­ шими изменениями, внесенными в язык, их преимущества были гораздо значительнее, чем можно было бы предположить по характеристике "не­ большие': В частности, оператор try с ресурсами основательно повлиял на способ написания кода, базирующегося на потоках. Кроме того, возможность использования типа S t r i ng для управления оператором swi tch оказалась долгожданным усовершенствованием, которое во многих ситуациях упроща­ ло написание кода. Выпуск Java SE 7 содержал несколько дополнений библиотеки Java API. Двумя наиболее важными из них бы ли усовершенствования инфраструк­ туры NIO Framework и дополнение инфраструктуры Fork/Join Framework. Инфраструктура NIO Framework (аббревиатура NIO первоначально означала New 1/0 (новый ввод-вывод)) была добавлена в версии Java 1 .4. Однако из­ менения, внесенные выпуском Java SE 7, в корне расширили ее возможности. Изменения были настолько значительными, что часто применяется термин NIO.2. Инфраструктура Fork!Join Framework обеспечивает важную поддержку па­ раллельного программирования. Параллельное программирование - это на­ звание, обыч но относящееся к методикам, которые позволяют эффективно эксплуатировать компьютеры, содержащие более одного процессора, в том числе многоядерные системы. Преимущество многоядерных сред заключ ает­ ся в возможности значительного повышения производительности программ. Инфраструктура Fork!Join Framework содействует параллельному програм­ мированию за счет того, ч то: • упрощает создание и использование задач, которые могут выполняться параллельно; • автоматически задействует множество процессоров. Следовательно, с применением Fork!Join Framework вы можете легко соз­ давать масштабируемые приложения, которые автоматически извлекают вы­ году из п роцессоров, доступных в среде выполнения. Конечно, не все алго­ ритмы поддаются распараллеливанию, но для тех, которые это допускают, можно добиться существенного улучшения в скорости выполнения. Следующим выпуском Java был Java SE 8 с комплектом разработч ика JDK 8. Он имел внутренний номер версии 1 .8. Комплект JDK 8 стал значительным обновлением языка Java из-за включ ения многообещающего нового языко­ вого средства: лямбда-выражений. Влияние лямбда-выражений было и п ро­ должило быть весьма существенным, изменяя как способ концептуализации программных решений, так и способ написания кода Java. В главе 15 будет показано, что лямбда-выражения добавляют в язык Java возможности функ- 54 Часть 1. Язык Java ционального программирования. Помимо прочего, лямбда-выражения содей­ ствуют упрощению и сокращению объема исходного кода, необходимого для создания определенных конструкций, таких как некоторые виды анонимных классов. Добавление лямбда-выражений также привело к появлению в языке новой операции (->) и нового элемента синтаксиса. Появление лямбда-выражений оказ�о широкомасштабное влияние и на библиотеки Java, в которые были добавлены новые средства, выгодно ис­ пользующие лямбда-выражения. Одним из самых важных средств считается новый потоковый АР!, находящийся в пакете j ava . util . stream. Потоковый АР! померживает конвейерные операции с данными и оптимизирован под лямбда-выражения. В еще одном новом пакете j ava . util . function опреде­ лено несколько функциональных интерфейсов, которые предоставляют до­ полнительную помержку для лямбда-выражений. В библиотеке АР! можно обнаружить и другие новые средства, связанные с лямбда-выражениями. Еще одна особенность, обусловленная добавлением лямбда-выражений, касается интерфейса. Начиная с JDK 8, появилась возможность определять стандартную реализацию для метода, объявленного в интерфейсе. Если реа­ лизация такого метода в классе не создавалась, тогда используется стандарт­ ная реализация из интерфейса. Данное средство позволяет элегантно разви­ вать интерфейс с течением времени, поскольку в интерфейс можно добавить новый метод, не нарушив работу существующего кода. Оно также упрощает реализацию интерфейса в ситуации, когда стандартные методы подходят. Другими новыми средствами в JDK 8 были, помимо прочих, новый АР! для даты и времени, аннотации типов и возможность организации параллельной обработки при сортировке массива. Следующим выпуском Java стал Java SE 9. Комплект разработчика называл­ ся JDK 9, а внутренним номером версии также был 9. Комплект JDK 9 пред­ ставлял крупный выпуск Java, включая значительные улучшения как языка Java, так и его библиотек. Подобпо JDK 5 и JDK 8 выпуск JDK 9 коренным об­ разом повлиял на язык Java и его библиотеки. Главным новым средством JDK 9 были модули, которые позволили указы­ вать взаимосвязи и зависимости кода, составляющего приложение. Модули также добавляют еще одно измерение к средствам контроля доступа Java. Включение модулей привело к добавлению в Java нового элемента синтаксиса и нескольких ключевых слов. Кроме того, в JDK появился инструмент под на­ званием j link. который позволяет программисту создавать для приложения образ времени выполнения, содержащий только необходимые модули. Был введен новый файловый тип JMOD. Модули оказали основательное влияние и на библиотеку АР!, т.к. начиная с JDK 9, библиотечные пакеты организова­ ны в виде модулей. Хотя модули являются значительным усовершенствованием Java, они кон­ цептуально просты и прямолинейны. Кроме того, поскольку унаследованный код, написанный до появления модулей, полностью померживается, модули могут быть интегрированы в процесс разработки постепенно. Нет никакой Глава 1 . История и эволюция языка Java 55 необходимости тотчас же изменять любой имеющийся код, чтобы задейство­ вать модули. Короче говоря, модули добавляют важную функциональность, не меняя сущности Java. Помимо модулей в JDK 9 включено много других новых средств. Особый интерес представляет JShell - инструмент, который поддерживает интерак­ тивное экспериментирование с программами и изучение Java . (Введение в JShell приведено в приложении Б.) Еще одним интересным обновлением яв­ ляется поддержка закрытых методов интерфейсов. Их добавление в даль­ нейшем расширяет поддержку JDK 8 стандартных методов в интерфейсах. В JDK 9 инструмент j a vadoc был снабжен возможностью поиска, для под­ держки которой предусмотрен новый дескриптор под названием @ index. Как и предшествующие выпуски, JDK 9 вносит несколько усовершенствований в библиотеки Java API. Обычно в любом выпуске Java наибольший интерес вызывают новые средства. Тем не менее, выпуск JDK 9 примечателен тем, что в нем объявлен устаревшим один заметный аспект Java: аплеты. Начиная с JDK 9, применять аплеты в новых проектах больше не рекомендуется. Как объяснялось ранее в главе, из-за ослабления поддержки аплетов со стороны браузеров (и других факторов) в выпуске JDK 9 объявлен устаревшим весь API для аплетов. Следующим выпуском Java был Java SE 10 (JDK 10). Ранее уже упоминалось, что начиная с JDK 10, выпуски Java должны выходить по строгому графику с расчетным временем между выпусками, составляющим всего лишь полгода. В результате выпуск JDK 10 появился в марте 201 8 года, т.е. спустя полгода после выпуска JDK 9. Главным новым языковым средством, добавленным JDK 10, стала поддержка выведения типов локальных переменных, благодаря ко­ торому теперь можно позволить выводить тип локальной переменной из типа ее инициализатора, а не указывать его явно. Для обеспечения такой возмож­ ности в Java было добавлено контекстно-чувствительное ключевое слово var. Выведение типов способно упростить код за счет устранения необходимости избыточно указывать тип переменной, когда его можно вывести из инициа­ лизатора переменной. Выведение типов также может упростить объявления в ситуациях, при которых тип трудно понять или невозможно указать явно. Выведение типов локальных переменных стало обычной частью современной программной среды. Его включение в Java помогло сохранять Java в актуальном состоянии с учетом меняющихся тенденций в проектировании языка. Наряду с други­ ми изменениями в JDK 1 0 переопределена строка версии Java с изменением смысла номеров версий, чтобы они лучше соотносились с новым графиком выпуска. Следующим выпуском Java был Java SE 1 1 (JDK 1 1). Он вышел в сентябре 201 8 года, т.е. через полгода после JDK 10, и был выпуском LTS. Главным но­ вым языковым средством в JDK 1 1 являлась поддержка использования клю­ чевого слова var в лямбда -выражениях. Вместе с рядом подстроек и обнов­ лений API в целом выпуск JDK 1 1 добавил новый API для работы с сетью, 56 Ч ас ть 1. Язык Java который будет интересен широкому кругу разработчиков. Он называется НТТР Client API, находится в пакете j ava . net . http и предоставляет улуч­ шенную, обновленную и усовершенствованную поддержку работы с сетью для клиентов НТТР. Кроме того, загрузчик приложений Java получил еще один режим выполнения, позволяющий напрямую запускать простые одно­ файловые програ ммы. В JDK 1 1 также были удалены некоторые средства. Возможно, наибольший интерес представляет удаление поддержки аплетов ввиду его исторической значимости. Вспомните, что аплеты впервые были объявлены нерекомендуемыми в JDK 9. В выпуске JDK 1 1 поддержка аплетов была удалена. Поддержка другой технологии развертывания, называемой Java Web Start, тоже была удалена в JDK 1 1 . Поскольку среда выполнения продол­ жила развиваться, и аплеты, и Java Web Start быстро утратили актуальность. Еще одним ключевым изменением в JDK 1 1 стало то, что JavaFX больше не входит в состав JDK. Взамен эта инфраструктура для построения графиче­ ских пользовательских интерфейсов превратилась в проект с открытым ко­ дом. Из-за того, что упомянутые средства перестали быть частью JDK, в кни­ ге они не обсуждаются. Между LТS-выпуском JDK 1 1 и следующим LТS-выпуском (JDK 17) выш­ ли пять выпусков функциональных средств: с JDK 12 до JDK 16. В выпусках JDK 12 и JDK 13 новые языковые средства не добавлялись. В выпуске JDK 14 была добавлена поддержка выражения swi tch, которое представляет собой конструкцию swi tch, производящую значение. Также были проведены другие усовершенствования swi t ch. В выпуске JDK 15 появились текстовые блоки, по существу являющиеся строковыми литералами, которые могут занимать более одной строчки. В выпуске JDK 16 операция i nst anceof была расши­ рена сопоставлением с образцом и добавлен новый тип класса под назва­ нием запись вместе с новым контекстно-чувствительным ключевым словом record. Запись предлагает удобный способ объединения данных. В выпуске ]DK 16 также предоставляется новый инструмент пакетирования приложе­ ний, называемый j package. На момент написания книги самой последней версией Java была Java SE 17 (JDK 17). Как упоминалось ранее, это второй LТS-выпуск Java, что имеет особое значение. Его главное новое средство - возможность запечатывать классы и интерфейсы. Запечатывание дает вам контроль над наследовани­ ем класса, а также над наследованием и реализацией интерфейса. Для такой цели были добавлены контекстно-чувствительные ключевые слова sealed, permi ts и non-sealed (первое ключевое слово Java с дефисом). Кроме того, в JDK 17 произошла пометка API для аплетов как устаревшего и подлежащего удалению. Вы уже знаете, что поддержку аплетов убрали несколько лет назад. Однако A PI для аплетов просто был помечен как нерекомендуемый, что по­ зволяло компилировать практически исчезающий код, который полагался на этот API. После выхода JDK 17 теперь API для аплетов подлежит удалению в будущем выпуске. Гла ва 1 . Ист ор и я и эв олюц и я язык а Java 57 Еще один момент относительно эволюции Java: в 2006 году начался про­ цесс открытия исходного кода Java. В наши дни доступны реализации JDK с открытым кодом. Открытие исходного кода дополнительно способствует динамической природе процесса разработки Java. В конечном итоге насле­ дием инноваций Java является безопасность. Java остается живым и гибким языком, который привыкли ожидать в мире программирования. Материал настоящей книги обновлен с учетом JDK 17. В ней описа­ ны многочисленные функциональные средства, обновления и дополнения Java. Тем не менее, как подчеркивалось в предыдущем обсуждении, история программирования на Java характеризуется динамичными изменениями. Рекомендуется отслеживать новые возможности в каждом последующем вы­ пуске Java. Проще говоря: эволюция Java продолжается! Культур а иннова ци й Язык Java с самого начала находился в центре культуры инноваций. Его первы й выпуск трансформировал подход к програ ммированию для Интернета. Виртуальная машина Java ( JVM) и байт-код поменяли представ­ ление о безопасности и переносимости. Переносимый код оживил веб-сеть. Процесс сообщества Java (Java Community Process - JCP) изменил способ внедрения новых идей в язык. Мир Java никогда не оставался на месте в тече­ н ие долгого времени. JDK 17 - это последний выпуск в непрекращающейся, динамичной истории Java. ГЛ А В А Краткий обзор язы ка Java Как и в других языках программирования, элементы Java не существуют обособленно. Наоборот, они работают вместе, чтобы сформировать язык как единое целое. Однако такая взаимосвязанность может затруднить описание одного аспекта Java, не касаясь ряда остальных. Часто при обсуждении од­ ной характеристики предполагается наличие знаний другой. По этой при­ чине в главе представлен краткий обзор нескольких основных средств Java. Приведенный здесь материал послужит вам отправной точкой, которая по­ зволит писать и понимать простые программы. Большинство обсуждаемых тем будут подробно рассматриваться в оставшихся главах части I. О бъ ектн о - о риенти р ова н ное п р огр амми р ован ие Объектно-ориентированное программирование (ООП) заложено в саму основу Java. На самом деле все программы на Java являются в той или иной степени объектно-ориентированными. ООП настолько тесно связано с Java, что для написания даже самых простых программ на J ava лучше изучить ба­ зовые принципы ООП. По этой причине глава начинается с обсуждения тео­ ретических аспектов ООП. Д ве пара д и г мы Все компьютерные программы состоят и з двух элементов: кода и данных. Кроме того, программа может быть концептуально организована вокруг сво­ его кода или своих данных. Иными словами одни программы пишутся ис­ ходя из того, "что происходит'; а другие - исходя из того, "что затронуто': Существуют две парадигмы, определяющие то, как строится программа. Первый способ называется моделью, ориентированной на процессы. Такой под­ ход характеризует программу как последовательность линейных шагов (т.е. кода). Модель, ориентированную на процессы, можно рассматривать как код, воздействующий на данные. Процедурные языки, подобные С, успешно ис­ пользуют эту модель. Тем не менее, как упоминалось в главе 1, по мере роста размера и сложности программ при использовании этого подхода начинают возникать проблемы. Гла в а 2. К р атк и й о бз ор я зыка Java 59 Для управления растущей сложностью был предложен второй подход, наз ываемый объектно-ориентированным программированием. Объектно­ ориентированное программирование позволяет организовать программу во­ круг ее данных (т.е. объектов) и набора четко определенных интерфейсов к таким данным. Объектно-ориентированную программу можно охарактери­ зовать как данные, управляющие доступом к коду. Как будет показ ано, пере­ ключая управляющую сущность на данные, вы можете получить несколько организационных преимуществ. Абстра к ция Важнейшим элементом ООП является абстракция. Человеку свойственно справляться со сложностью через абстракцию. Например, люди не представ­ ляют себе автомобиль как набор из десятков тысяч отдельных деталей. Они думают о нем, как о четко определенном объекте со своим уникальным по­ ведением. Такая абстракция позволяет людям доехать на автомобиле до про­ дуктового магаз ина, не перегружаясь сложностью индивидуальных деталей. Они могут игнорировать подробности работы двигателя, коробки передач и тормозной системы. Вз амен они могут свободно использовать объект как единое целое. Эффективный способ управления абстракцией предусматривает приме­ нение иерархических классификаций. Они поз воляют разбивать семантику сложных систем на более управляемые части. Снаружи автомобиль представ­ ляет собой единый объект. Но погруз ившись внутрь, вы увидите, что авто­ мобиль состоит из нескольких подсистем: рулевого управления, тормозов, аудиосистемы, ремней безопасности, обогрева, навигатора и т.д. Каждая под­ система в свою очередь содержит более специализ ированные узлы. Скажем, в состав аудиосистемы может входить радиоприемник, проигрыватель ком­ пакт-дисков и/или проигрыватель МРЗ. Суть в том, что вы управляете слож­ ностью автомобиля (или любой другой сложной системы) за с чет использо­ вания иерархических абстракций. Иерархические абстракции сложных систем также можно применять к компьютерным программам. Данные из традиционной, ориентированной на процессы, программы могут быть трансформированы путем абстракции в составляющие ее объекты. Последовательность шагов процесса может стать совокупностью сообщений, передаваемых между этими объектами. Таким образом, каждый объект описывает свое уникальное поведение. Вы можете воспринимать такие объекты как конкретные сущности, которые реагируют на сообщения, указывающие им о необходимости делать что-то. В этом и заклю чается суть ООП. Объектно-ориентированные концепции образуют основу Java точно так же, как они формируют базис человеческого понимания. Важно понимать, каким образом такие концепции воплощаются в программах. Вы увидите, что ООП является мощной и естественной парадигмой для соз дания программ, кото­ рые способны пережить неизбежные изменения, сопровождающие жизненный 60 Часть 1. Язык Java цикл любого крупного программного проекта, в том числе осмысление, рост и устаревание. Например, когда у вас есть четко определенные объекты и чи­ стые, надежные интерфейсы к этим объектам, вы можете без опасений эле­ гантно выводить из эксплуатации или заменять части старой системы. Три п рин ц и п а ООП Все языки ООП предоставляют механизмы, которые помогают реализо­ вать объектно-ориентированную модель. Речь идет об инкапсуляции, насле­ довании и полиморфизме. Давайте взглянем на упомянутые концепции. Инк а псул я ция Инкапсуляция представляет собой механизм, который связывает вместе код и обрабатываемые им данные, а также защищает их от внешнего вме­ шательства и неправильного использования. Инкапсуляцию можно считать защитной оболочкой, которая предотвращает произвольный доступ к коду и данным из другого кода, определенного вне оболочки. Доступ к коду и данным, находящимся внутри оболочки, строго контролируется через четко определенный интерфейс. Чтобы провести аналогию с реальным миром, рас­ смотрим автоматическую коробку передач автомобиля. Она инкапсулирует массу информации о вашем двигателе, такую как величина ускорения, наклон поверхности, по которой двигается автомобиль, и положение рычага пере­ ключения передач. Вы, как пользователь, располагаете только одним спосо­ бом влияния на эту сложную инкапсуляцию: перемещение рычага переклю­ чения передач. Вы не можете воздействовать на коробку передач, скажем, с помощью сигнала поворота или дворников. Таким образом, рычаг переклю­ чения передач является четко определенным (и действительно уникальным) интерфейсом к коробке передач. Вдобавок то, что происходит внутри короб­ ки передач, никак не влияет на объекты за ее пределами. Например, пере­ ключение передачи не включает фары! Поскольку автоматическая коробка передач инкапсулирована, десятки производителей автомобилей могут реа­ лизовать ее так, как им заблагорассудится. Однако с точки зрения водителя все они работают одинаково. Ту же самую идею можно применить и к про­ граммированию. Сила инкапсулированноrо кода в том, что каждый знает, как получить к нему доступ, и потому может использовать его независимо от де­ талей реализации и без каких-либо опасений столкнуться с неожиданными побочными эффектами. Основой инкапсуляции в Java является класс. Хотя понятие класса под­ робно рассматривается позже в книге, полезно кратко обсудить его прямо сейчас. Класс определяет структуру и поведение (данные и код), которые бу­ дут общими для набора объектов: Каждый объект заданного класса содержит структуру и поведение, определенные классом, как если бы он был "отлит" в форме класса. По этой причине объекты иногда называют экземпыq,ами класса. Таким образом, класс представляет собой логическую конструкцию, а объект имеет физическую реальность. Глава 2. Краткий обзор языка Java 61 При создании класса вы указываете код и данные, составляющие класс. В совокупности такие злементы называются членами класса. В частности, дан­ ные, определенные классом, называют переменными-членами или переменными экземпляра. Код, работающий с зтими данными, называют методами-членами или просто методами. (На тот случай, если вы знакомы с языком С или С++: то, что программист на Java называет методом, программист на С/С++ назы­ вает функчией.) В правильно написанных программах на Java методы опреде­ ляют способ использования переменных-членов, т.е. поведение и интерфейс класса определяются методами, которые работают с данными его зкземпляра. Поскольку целью класса является инкапсуляция сложности, существу­ ют механизмы для сокрытия сложности реализации внутри класса. Методы или переменные в классе могут быть помечены как закрытые или открытые. Открытый интерфейс класса представляет все, что должны или могут знать внешние пользователи класса. Доступ к закрытым методам и данным возмо­ жен только из кода, который является членом класса. Следовательно, любой другой код, не являющийся членом класса, не сможет получить доступ к за­ крытому методу или переменной. Так как доступ к закрытым членам класса другие части вашей программы могут получить только через открьrrые мето­ ды класса, вы можете гарантировать, что не будет совершено никаких непод­ ходящих действий. Разумеется, зто означает, что открытый интерфейс дол­ жен быть внимательно спроектирован, чтобы не раскрывать слишком много деталей внутренней работы класса (рис. 2.1). Кпасс Открытые переменные экэеммяра (не рекомендуются) д открытые методы Закрытые методы � Закрытые переменные экаемnпя�,1 д :д д 11 •• • -д А АА t, А ., . • д д/д Рмс. 2.1. Инкапсуляция: открытые методы могут исnопьэовать дпя защиты эакрыТЬIХ данных 62 Часть 1. Язык Java Наследование Наследование представляет собой процесс, посредством которого один объект приобретает свойства другого объекта. Оно важно, т.к. померживает концепцию иерархической классификации. Ранее уже упоминалось, что боль­ шинство знаний стало доступным за счет иерархической (т.е. нисходящей) классификации. На пример, золотистый ретривер является частью класса "собаки'; который в свою очередь относится к классу "млекопитающие'; вхо­ дящему в состав более крупного класса "животные': В отсутствие иерархий каждый объект должен был бы явно определять все свои характеристики. Тем не менее, используя наследование, объекту нужно определить только те качества, которые делают его уникальным внутри своего класса. Он может наследовать общие атрибуты от своего родителя. Таким образом, именно ме­ ханизм наследования позволяет одному объекту быть специфическим экзем­ пляром более общего случая. Давайте подробнее рассмотрим сам процесс. Большинство людей естественным образом воспринимают мир как состо­ ящий из иерархически связанных друг с другом объектов, таких как живот­ ные, млекопитающие и собаки. При желании описать животных абстрактно вы бы сказали, что у них есть некоторые характерные признаки вроде раз­ мера, умственных способностей и типа костной системы. Животным также присущи определенные поведенческие аспекты: они едят, дышат и спят. Такое описание характерных признаков и поведения является определением класса для животных. Если бы требовалось описать более конкретный класс животных, скажем, млекопитающих, то они имели бы более конкретные характерные признаки вроде типа зубов и молочных желез. Такое определение известно как под­ класс животных, а животные являются суперклассом млекопитающих. Поскольку млекопитающие - просто более точно определенные живот­ ные, они наследуют все характерные признаки животных. Находящийся глу­ боко в иерархии классов подкласс наследует все характерные признаки от каждого из своих предков (рис. 2.2). Наследование также взаимодействует с инкапсуляцией. Если заданный класс инкапсулирует некоторые характерные признаки, тогда любой подкласс будет иметь те же самые признаки плюс любые, которые он добавляет как часть своей специализации (рис. 2.3). Это ключевая концепция, обеспечива­ ющая возрастание сложности объектно-ориентированных программ линей­ но, а не геометрически. Новый подкласс наследует все характерные признаки всех своих предков. У него нет непредсказуемых взаимодействий с большей частью остального кода в системе. Полиморфизм Полиморфизм (от греческого "много форм") представляет собой средство, которое позволяет использовать один интерфейс для общего класса дей­ ствий. Конкретное действие определяется природой ситуации. Возьмем в ка­ честве примера стек (т.е. список, работающий по принципу "последним при- Глава 2. Краткий обзор языка Java 63 Животные ( А Лабрадор ) отистый ретривер ( Зол Пудели J Рис. 2,2. Иерархия классов животных шел - первым обслужен"). У вас может быть проrрамма, требующая стеки трех типов. Один стек используется для целых значений, друrой - для значе­ ний с плавающей точкой и третий - для символов. Каждый стек реализуется по тому же самому алrоритму, даже если хранящиеся данные различаются. В языке, не являющемся объектно-ориентированным, вам придется создать три разных набора стековых процедур с отличающимися именами. Но блаrо­ даря полиморфизму в Java вы можете указать общий набор стековых проце­ дур с одинаковыми именами. Как правило, концепция полиморфизма часто выражается фразой "один интерфейс, несколько методов': Это означает возможность разработки об­ щеrо интерфейса для rруппы связанных действий, что поможет уменьшить сложность, позволив использовать один и тот же интерфейс для указания общего класса действий. Задачей компилятора будет выбор конкретноrо дей­ ствия (т.е. метода) применительно к каждой ситуации. Вам, как проrрамми­ сту, не придется делать такой выбор вручную. Вам понадобится только за­ помнить и задействовать общий интерфейс. Продолжая аналоrию с собаками, можно отметить, что обоняние собаки полиморфно. Если собака почует кошку, то она залает и побежит за ней. Если собака почувствует запах еды, тоrда у нее начнется слюноотделение, и она по­ бежит к миске. В обеих ситуациях работает одно и то же обоняние. Разница в том, что именно издает запах, т.е. тип данных, с которыми имеет дело со­ бачий нос! Та же общая концепция может быть реализована в Java, поскольку она применяется к методам внутри проrраммы Java. 64 Часть 1. Язык Java �· Животные �Вес Млекопитающие Собачьи Домашние т 4_учена охо е н Рис. 2.3. Лабрадор наследует характерные признаки всех сво их суnерклассов Гл ава 2 . Кр а ткий обзор языка Java С овме стная работа п олиморфизма , ин капсуля ции и наследовани я 65 При правильном применении полимо рфиз м, инкапсу ляция и наследование объединяются для создания программной среды, которая померживает раз­ работку гораздо более надежных и масштабируемых программ, чем в случае использования модели, ориентированной на про цессы. Хорошо спроектиро­ ванная иерархия классов является основой для многократного использова­ ния кода, в разработку и тестирование которого вы вложили время и уси­ лия. Инкапсуляция поз воляет вам со временем переносить свои реализ ации, не нарушая код, который з ависит от о ткрытого интерф ейса ваших классо в. Полиморфиз м дает возможность создавать чистый, понятный, чи табельный и устойчивый код. Из двух реальных примеров автомобиль более полно иллюстрирует мощь ООП. Если о собаках интересно думать с точки зрения наследования, то ав­ томо били больше похожи на программы. Все водит ели полагаются на насле­ дование для у правления различными типами (подклассами) транспортных средст в. Независимо от того, является т ранспортно е средство школьным автобусом, седаном "Мерседес'; "Порше" или семейным минивэно м, все во­ дит ели мо гут более или менее находит ь и управлять рулем, педалью тормо­ за и педалью газа. Немного повозившись с рычагом переключения передач, большинство людей смогут даже отличить ручную коробку передач от авто­ матической, т.к. они в основном понимают их о бщий су перкласс - коробку передач. Пользуясь автомобилями, люди пос тоянно вз аимод ействуют с их инкап­ сулированными характ еристиками. Педали тормоза и газа скрывают неверо­ ятну ю сложность, а инт ерфейс настолько прост, что ими можно управлять с помощью ног! Реализ ация двигателя, тип тормозов и размер шин не влияют на то, каким образом вы взаимодействуете с о пределением класса педалей. Последний принцип, полиморфиз м, четко отража ется в способности про­ изводи телей автомобилей предлагать широкий спектр вариантов для одного и того же транспортного средства. Например, вы можете полу чи т ь антибло­ кировочную или традиционную тормозную систему, рулевое у правление с гидроусилит елем или реечной передач ей, а также 4-, 6- или В-цилиндровый двигатель либо электромобиль. В любом случае вы по-прежнему будете на­ жимать на педаль то рмоза для остановки, по ворачи вать руль для смены на­ правления и нажимать на педаль газ а, когда хоти те двигаться. Один и тот же инт ерфейс может применяться для управления несколькими отличающимися реализ ациями. Как види те, благодаря применению инкапсуляции, наследования и поли­ морфизма отдельные части трансформируются в объект, известный как авто ­ мобиль. То же само е относится и к компьютерным программам. За счет при­ менения принци пов ООП различные части сложной программы могут быть объединены в едино е, надежно е, сопровождаемое целое. бб Часть 1. Язык Java Как упоминалось в начале раздела, каждая программа на Java является объектно-ориентированной. Или, говоря точнее, каждая программа на Java включает в себя инкапсуляцию , наследование и полиморфизм. Хотя корот­ кие примеры программ, приведенные в оставшейся части текущей главы и в ряде последующих глав, могут не выглядеть как обладающие всеми указан­ ными характеристиками, тем не менее, эти характеристики присутствуют. Вы увидите, что м ногие возможности, предоставляемые языком Java, являются частью его встроенных библиотек классов, в которых широко используются инкапсуляция, наследование и полиморфизм. П ерва я про с та я програ мма После обсуждения объектно-ориентированного фундамента Java имеет смысл рассмотреть несколько фактических программ на Java. Давайте начнем с компиляции и запуска короткого при мера программы, код которой показан ниже. Вы увидите, что это требует чуть большего объема работы, чем может показаться. /* Простая программа на Java . Назовите этот файл Example . j ava . */ class Examp l e { // Программа начинается с вызова main ( ) . puЬlic static void main ( String [ ] args ) { Sys tem . out . println ( " Простая программа на языке Java . " ) ; На заметку! В приведенных далее описаниях предпола гается, что вы используете стандартный комплект разработчика Java SE Development Кit (JDK), предлагаемый компанией Oracle. (Доступны также версии с открытым кодом.) В случае применения интегрированной среды разработки (integrated development environment - IDE) вам придется следовать другой про­ цедуре компиляции и запуска программ на Java. За подробными сведениями обращайтесь в документацию по IDE-cpeдe. В вод ко да пр о г раммы Для некоторых компьютерных языков имя файла, содержащего исходный код программы, не имеет значения. Однако с Java дело обстоит иначе. Первое, что вы должны узнать о Java - имя, которое вы назначаете файлу с исходным кодом, очень важно. В рассматриваемом примере именем файла с исходным кодом должно быть Example . java и вот причина. Файл с исходным кодом в Java официально называется единицей компиляции. Он представляет собой текстовый файл, который содержит (помимо прочего) одно или большее число определений классов. (Пока будем использовать фай­ лы с исходным кодом, содержащие только один класс.) Компилятор Java тре­ бует, чтобы в имени файла с исходным кодом применялось расширение . java. Глава 2 . Краткий обзор языка Java 67 Взглянув на программу, вы увидите, что именем определенного в ней клас­ са является Example. Это не совпадение. В Java весь код должен находиться внутри класса. По соглашению имя главного класса должно совпадать с име­ нем файла, содержащего программу. Вы также должны удостовериться в том, что прописные буквы в имени файла и в имени класса соответствуют друг другу. Причина связана с чувствительностью к регистру языка Java. В данный момент соглашение о том, что имена файлов соответствуют именам классов, может показаться деспотическим. Тем не менее, такое соглашение упрощает помержку и организацию ваших программ. Более того, как вы увидите далее в книге, в некоторых случаях оно будет обязательным. Ком п иляция п ро г раммы Чтобы скомпилировать программу Example, запустите компилятор j a vac, указав в командной строке имя файла с исходным кодом: C : \ > j avac Exarnple . j ava Компилятор j avac создает файл по имени Example . class с байт-кодом программы. Как обсуждалось ранее, байт-код Java является промежуточным представлением программы, содержащим инструкции, которые будет выпол­ нять виртуальная машина Java (JVM). Таким образом, результатом j avac не будет код, который можно запускать напрямую. Чтобы действительно запустить программу, вам придется воспользовать­ ся загрузчиком приложений Java под названием j ava. Для этого передайте ему в качестве аргумента командной строки имя Example: C : \ >j ava Exarnple Выполнение программы приводит к отображению следующего вывода: Простая программа на языке Java . Когда исходный код Java компилируется, каждый отдельный класс поме­ щается в собственный выходной файл, имеющий имя класса и расширение . class. Вот почему рекомендуется назначать файлам с исходным кодом Java имя, совпадающее с именем класса, который они содержат - имя файла с исходным кодом будет совпадать с именем файла . class. При запуске j ava, как только что было показано, фактически указывается имя класса, который необходимо выполнить. Загрузчик приложений будет автоматически искать файл с таким именем и расширением . class. В случае нахождения файла он выполнит код, содержащийся в указанном классе. На за метку! Начиная с JDK 1 1 , в Java есть возможность запуска некоторых типов простых программ прямо из файла с исходным кодом без явного вызова j avac. Такая методика может оказать­ ся полезной в ряде ситуаций; она описана в приложении В. В настоящей книге предполагает­ ся, что вы применяете описанный выше нормальный процесс компиляции. 68 Часть 1 . Язык Java Подробны й а н а лиз перво го прим е р а программы Несмотря на довольно небольшой размер программы Example . j а va, она содержит несколько основных характеристик, присущих всем программам на Java. Давайте займемся исследованием каждой части программы. Программа начинается со следующих строк: /* Простая программа на Java . Назовите этот файл Example . j ava . */ Строки представляют собой комментарий. Как и большинство других язы­ ков программирования, Java позволяет вводить примечания в файл с исходным кодом программы. Компилятор игнорирует содержимое комментариев. На самом деле комментарий описывает или объясняет работу программы любо­ му, кто читает ее исходный код. В данном случае комментарий описывает про­ грамму и напоминает, что исходный файл должен называться Example . j а va. Конечно, в реальных приложениях комментарии обычно объясняют, как ра­ ботает какая-то часть программы или что делает конкретная функция. В Java поддерживаются три стиля комментариев. Комментарий в начале программы называется многострочным. Комментарии такого типа должны начинаться с символов /* и заканчиваться символами * /. Все, что находится между этими двумя парами символов, компилятор игнорирует. Как следует из названия, мноrострочный комментарий может занимать несколько строк. Ниже показана следующая строка кода в программе: class Example { В строке с помощью ключевого слова class определяется новый класс. Идентификатор Example является именем класса. Все определение класса, включая всех его членов, будет находиться между открывающей фигурной скобкой ( {) и закрывающей фигурной скобкой ( } ). В данный момент не слиш­ ком беспокойтесь о деталях класса помимо того, что в Java вся активность программы происходит внутри класса. Это одна из причин, по которой все программы на Java (по крайней мере, слегка) объектно-ориентированы. Следующая строка в программе содержит однострочный комментарий: / / Программа начинается с вызова ma in ( ) . Вы видите второй тип комментариев, поддерживаемых в Java. Одностроч­ ный комментарий начинается с символов / / и простирается до конца стро­ ки. Как правило, программисты применяют многострочные комментарии для длинных примечаний, а однострочные - для кратких построчных описаний. Комментарии третьего типа, которые называются документирующими, об­ суждается в разделе "Комментарии" далее в главе. Вот следующая строка кода: puЫ ic static void main ( Str: ing [ ] ar:g s ) { Глава 2. К р атки й обзор языка Java 69 Данная строка начинает метод ma i n ( ) . Как объяснялось в предыдущем комментарии, с этой строки программа начнет выполняться. Обычно про­ грамма на Java начинает выпол нение с вызова rna in ( ) . Полностью осознать смысл каждой части строки пока невозможно, т.к. для этого нужно хорошо понимать подход Java к инкапсуляции. Однако поскольку такая строка кода присутствует в большинстве примеров в первой части книги, давайте кратко рассмотрим каждую часть. Ключевое слово puЫ ic представляет собой модификатор дост упа, кото­ рый позволяет программисту у правлять видимостью чл енов класса. Когда член класса предварен ключ евым словом puЫ ic, доступ к нему может быть получен из кода за пределами класса, где он объявлен. {Противоположностью puЫ ic явля ется ключевое слово private, которое предотвращает использо­ вание члена кодом, определенным вне класса.) В данном случае метод ma in ( ) должен быть объявлен как puЫic, потому что при запуске программы его потребуется вызывать в коде за пределами класса. Ключевое слово static позволяет вызывать ma i n ( ) без создания конкр етного экземпляра класса. Причина в том, что mai n ( ) вызывается машиной JVM до создания каких-ли­ бо объектов. Ключевое слово void просто сообщает компилятору, что rnain ( ) не возвращает значение. Как вы увидите, методы также могут возвращать знач ения. Если все это кажется нем ного запутанным, не переживайте - все концепции будут подробно рассмотрены в последующих главах. Как уже упоминалось, метод main ( ) вызывается при запуске приложения Java. Им ейте в виду, что язык Java чувствит елен к регистру, а потому Ma in отличается от ma in. Важно понимать, что компилятор Java будет компилиро­ вать классы, не содержащие метода ma in ( ) . Но у j ava не будет возможности запускать такие классы. Таким образом, если вы наберете Ma in вместо rna in, то компилятор все равно скомпилирует вашу программу, но java сообщит об ошибке, поскольку не сможет найти метод main ( ) . Любая информация, которую ва м нужно передать методу, получается пе­ р еменными, указанными в набор е кругл ых скобок после имени метода. Такие пере менные называются параметрами. Даже когда для метода не требуют­ ся параметры, вам все равно понадобится указать пустые круглые скобки. В ma in ( ) всего один пара метр, хоть и сложный. Конструкция Str ing [ ] a rgs объявляет параметр по им ени args, который пр едставляет собой массив эк­ земпляров класса String. (Массивы - это совокупности похожих объектов.) Объекты типа St ring хранят строки символов. В данном случае a rgs полу­ чает л юбые аргументы командной строки , присутствующие при выполнении программы. В рассматриваемой програ мме такая информация не использует­ ся, но другие программы, показанные далее в этой книге, будут ее потреблять. Последним символом в строке является { , который сигнализирует о на­ чал е тела ma in ( ) . Весь код, содержащийся в методе, будет находиться между открывающей и закрывающей фигурными скобками метода. Еще один момент: ma in ( ) - это просто стартовая точка для вашей про­ граммы. Сложная программа будет иметь десятки классов, только один из которых должен иметь метод main ( ) , чтобы начать работу. Кроме того, для 70 Часть 1. Язык Java некоторых типов программ метод mai n ( ) вообще не нужен. Тем не менее, для большинства программ, приведенных в книге, метод main ( ) обязателен. Ниже показана следующая строка кода. Обратите внимание, что она рас­ положена внутри main ( ) . Systern . out . println ( "Пpocтaя программа на языке Java . " ) ; Здесь на экран выводится строка " Простая программа на языке Java . " вместе с символом новой строки. Вывод в действительности осуществляется встроенным методом println ( ) . В данном случае метод print ln ( ) отобра­ жает переданную ему строку. В дальнейшем вы увидите, что println ( ) можно применять и для отображения других типов информации. Строка начинается с System . out. Хотя подробно объяснить это сейчас слишком сложно, вкратце отметим, что System является предопределенным классом, обеспечивающим доступ к системе, а out - выходным потоком, подключенным к консоли. Вероятно, вы уже догадались, что консольный вывод (и ввод) нечасто ис­ пользуется в большинстве реальных приложений Java. Поскольку большин­ ство современных вычислительных сред являются графическими по своей природе, консольный ввод-вывод применяется в основном в простых ути­ литах, демонстрационных программах и серверном коде. Позже в книге вы узнаете о других способах генерирования вывода с использованием Java. Но пока мы продолжим применять методы консольного ввода-вывода. Обратите внимание, что оператор println ( ) завершается точкой с запя­ той. В Java точка с запятой присутствует в конце многих операторов. Как вы увидите, точка с запятой - важная часть синтаксиса Java. Первый символ } в программе заканчивает метод ma i n ( ) , а последний символ } завершает определение класса Example. Вто р ая п р о с тая п р огр амма Вероятно, никакая другая концепция не является более фундаментальной для языка программирования, нежели переменная. Возможно, вам уже извест­ но, что переменная представляет собой именованную ячейку памяти, которой ваша программа может присвоить значение. Значение переменной можно из­ менять во время выполнения программы. В следующей программе показано, как объявлять и присваивать значение переменной. В программе также иллю­ стрируется ряд новых аспектов консольного вывода. Как следует из коммента­ риев в начале программы, файл потребуется назвать Example2 . j ava. /* Еще один короткий пример . Назовите этот файл Exarnple2 . j ava . */ class Exarnple2 { puЬlic static void rna in ( S tring [ ] a r g s ) { iпt nurn ; / / объявление переменной по имени nurn nurn = 1 0 0 ; / / присваива ние переменной nurn зна чения 1 0 0 Systern . out . p r intln ( " Значение пurn : " + nurn) ; Глава 2. Краткий обзор языка Java 71 num = num * 2 ; System . out . print ( "Знaчeниe num * 2 : " ) ; System . out . println ( num ) ; Запустив программу, вы получите следующий вывод: Значение num : 1 0 0 Значение num * 2 : 2 0 0 Давайте выясним, почему генерируется такой вывод. Ниже приведена но­ вая строка в программе: int num ; / / объявление переменной по имени num В строке объявляется целочисленная переменная по имени num. Подобно многим другим языкам переменные в Java до с воего использования должны быть объявлены. Вот как выглядит общая форма объявления переменной: тип имя -переменной ; Здесь тип указывает тип объявляемой переменной, а имя-переменной имя переменной. При желании объявить более одной переменной з аданного типа можете применять список имен переменных, отделенных друг от друга з апятыми. В Java определено несколько типов данных, включая целочислен­ ный, символьный и числовой с пл авающей точкой. Целочисленный тип ука­ зывается с помощью ключевого слова int. Следующая строка в программе: num = 1 0 0 ; / / присваивание переме нной num значения 1 0 0 обеспечивает присваивание переменной num значения 1 0 0. Операция присва­ ивания в Java обоз начается одиночным з наком равенства. В показ анной далее строке кода выводится з начение num, предваренное строкой " Значение num : " . System . out . println ( " Знaчeниe num : " + num ) ; Знак + в операторе приводит к тому, что з начение num добавляется к стро­ ке, которая ему предшествует, после чего результирующая строка выводит­ ся. (На самом деле з начение num сначала преобраз уется из целочисленного в эквивалентное строковое и з атем объединяется с предшествующей ему строкой. Этот процесс подробно описан далее в книге.) Такой подход можно обобщить. Использ уя операцию +, вы можете объединять в одном операторе println () столько элементов, сколько хотите. В следующей строке кода переменной num присваивается значение num, умноженное на 2. Как и в большинстве других языков, операция умножения в Java обоз начается символом *. После выполнения строки кода переменная num будет содержать з начение 2 0 0. Ниже показаны очередные две строки программы: System . out . print ( "Значение num * 2 : " ) ; System . out . println ( num) ; 72 Часть 1. Язык Java Здесь происходит несколько новых вещей. Первым делом с применени­ ем встроенного метода print ( ) отображается строка "Значение num * 2 : ". Она не заканчивается символом новой строки, т.е. вывод, генерируемый сле­ дующим, будет начинаться в той же самой строке. Метод print ( ) аналогичен методу println ( ) , но он не выводит символ новой строки после каждого вы­ зова. Взгляните теперь на вызов println ( ) . Обратите внимание, что пере­ менная num используется сама по себе. Методы print ( ) и println ( ) можно применять для вывода значений любых встроенных типов Java. Д ва управляющих оп е ратора Хотя управляющие операторы будут подробно рассматриваться в главе 5, здесь кратко представлены два из них, чтобы их можно было использовать в примерах программ, приведенных в главах 3 и 4. Они также помогут проил­ люстрировать важный аспект Java: блоки кода. Оператор if Оператор i f в Java работает во многом аналогично условному операто­ ру в любом другом языке. Он определяет поток выполнения на основе того, является некоторое условие истинным или ложным. Ниже показана его про­ стейшая форма: i f ( условие) оператор; Здесь условие - это булевское выражение. (Булевским является такое вы­ ражение, результатом вычисления которого будет либо true (истина), либо false (ложь).) Если условие истинно, тогда оператор выполняется. Если условие ложно, то оператор пропускается. Вот пример: i f ( num < 1 0 0 ) System . out . println ( "Знaчeниe num меньше 100 " ) ; В данном случае, если переменная nurn содержит значение, которое меньше 100, тогда условное выражение дает true и вызов метода println ( ) выпол­ няется. Если же num содержит значение, которое больше или равно 1 00, то вызов метода println ( ) пропускается. Как будет показано в главе 4, в Java определен полный набор операций от­ ношения, которые можно применять в условном выражении. В табл. 2.1 опи­ сано несколько из них. Табпица 2.1 . Часто исnопьзуемые операции отноwения i,,P�ЦМ·.9�iJ?,' < > ':{;;�� ,, , P·t�� •:ii Меньше Больше Равно Обратите внимание, что проверка на равенство обозначается двумя знака­ ми равенства. Ниже приведена программа, в которой иллюстрируется работа оператора if: Гл а в а 2. Кратки й обзор языка Java 73 /* Демонстрация работы оператора i f . Назовите этот файл I fSample . j ava . */ class I fSample [ puЫ ic static void main ( String [ ] a rgs ) { int х , у; х = 10; у = 20; i f (x < у ) System . out . println ( "Знaчeниe х меньше у " ) ; х = х * 2; i f ( x == у ) System . out . println ( "Tenepь значение х равно у " ) ; х = х * 2; i f ( x > у ) System . out . println ( "Teпepь значение х больше у " ) ; / / Здесь ничего не отобразится i f ( x == у) System . out . println ( "Этoт вывод вы не увидите " ) ; В результате выполнения программы генерируется следующий вывод: Значение х меньше у теперь значение х равно у Теперь значение х больше у С программой связан еще один момент. В строке int х, у ; объявляются две переменные, х и у, посредством списка с разделителем-за­ пятой. Цикл for Операторы циклов - важная часть почти любого языка программирова­ ния, поскольку они обеспечивают возможность многократного выполнения некоторой задачи. Как вы увидите в главе 5, язык Java предлагает мощный на­ бор конструкций циклов. Возможно, наиболее универсальным является цикл for. Вот простейшая форма цикла for: for ( инициализация; условие; итерация) опера тор; В своей самой распространенной форме часть и ници ализация цикла устанавливает переменную управления циклом в начальное значение. Часть у словие представляет собой булевское выражение, которое проверяет пере­ менную управления циклом. Если результат проверки оказывается истинным, тогда оператор выполняется, а цикл for продолжает работу. При ложном ре­ зультате проверки цикл завершается. Выражение итерация определяет, каким образом переменная управления циклом изменяется на каждой итерации цик­ ла. Далее приведена короткая программа, иллюстрирующая работу цикла for: /* Демонстрация работы цикла for . Назовите этот файл Fo rTest . j ava . */ 74 Ч а сть 1. Яз ы к Java class Fo rTest { puЫ ic static void ma in ( St ring [ ] args ) { int х; for ( x = О; x<l O ; х = x+ l ) System . out . pr intln ( " Знaчeниe х : " + х ) ; Программа Значение х : Значение х : Значение х : Значение х : Значение х : Значение х : Значение х : Значение х : Значение х : Значение х : генерирует такой вывод: О 1 2 3 4 5 6 7 8 9 В данном примере х - это переменная управления циклом. Она инициа­ лизируется нулем в части инициализация цикла for. В начале каждой итера­ ции (включая первую) выполняется условная проверка x<lO. Если результат проверки оказывается истинным, тоrда выполняется оператор println ( ) , после чеrо выполняется часть итерация цикла, которая увеличивает значение х на 1. Процесс продолжается до тех пор, пока условная проверка не станет ложной. Интересно отметить, что в профессионально написанных программах на Java вы практически никогда не встретите часть итерация цикла, написанную так, как в предыдущей программе. То есть вы будете редко видеть операторы вроде следующего: х = х + l; Причина в том, что в Java есть специальная операция инкремента, облада­ ющая большей эффективностью, которая обозначается посредством ++ (т.е. два знака "плюс" подряд). Операция инкремента увеличивает свой операнд на единицу. С помощью операции инкремента предыдущее выражение можно записать в показанной ниже форме: х++; Таким образом, цикл for в предыдущей программе обычно будет записы­ ваться в следующем виде: for ( x = О ; x<l O ; х++ ) Можете опробовать его. Вы заметите, что цикл выполняется в точности, как было ранее. В Java также предлагается операция декремента, обозначаемая как --. Она уменьшает свой операнд на единицу. Глава 2. Кратки й о б зор языка Java 75 Испол ьзование блоков кода Язык Java позволяет группировать два или более операторов в блоки кода, также называемые кодовыми блоками. Для этого операторы помещаются между открывающей и закрывающей фигурными скобками. После того, как блок кода создан, он становится логической единицей, которую можно применять в лю­ бом месте, где разрешено использовать оди ночный оператор. Скажем, блок мо­ жет служить целью для операторов i f и for. Возьмем следующий оператор i f: / / начало блока if (x < y) х = у; у = О; // конец блока Если х меньше у, тогда выполнятся оба оператора внутри блока. Таким образом, два оператора внутри блока образуют логическую единицу, где пер­ вый оператор не может быть выполнен без выполнения второго. Ключевой момент здесь в том, что всякий раз, когда нужно логически с вязать два или большее количество операторов, вы создаете блок. Давайте рассмотрим еще один пример. В показанной далее программе блок кода применятся в качестве цели цикла for. /* Демонстрация работы блока кода . Назовите этот файл BlockTes t . j ava . */ class BlockTest { p uЫ i c static void main ( String [ ] args ) { int х, у ; у = 20; / / целью этого цикла является блок for ( x = О ; x<l 0 ; х++ ) { System . out . println ( "Знaчeниe х : " + х ) ; S ystem . out . println ( "Значение у : " + у ) ; у = у - 2; Программа генерирует следующи й вывод: Значение Значение Значение Значение Значение Значение Значение Значение Значение Значение Значение Значение Значение х: у: х: у: х: у: х: у: х: у: х: у: х: О 20 1 18 2 16 3 14 4 12 5 10 6 76 Часть 1. Язык Java З на ч ение З на ч ение З на ч ение З на ч ение З на ч ение З на ч ение З начение у: х: у: х: у: х: у: 8 7 6 8 4 9 2 В этом случае целью цикла for является блок кода, а не одиночный опера­ тор. Соответственно на каждой итерации цикла будут выполняться три опе­ ратора внутри блока, о чем, конечно же, свидетельствует вывод, генерируе­ мый программой. Позже в книге вы увидите, что блоки кода обладают дополнительными ха­ рактеристиками и сценариями использования. Однако главная причина их существования - создание логически неразрывных единиц кода. Л ексические вопросы После ознакомления с несколькими короткими программами на Java насту­ пило время более формально описать атомарные элементы Java. Программы на Java представляют собой совокупность пробельных символов, идентифика­ торов, литералов, комментариев, операторов, разделителей и ключевых слов. Операторы обсуждаются в следующей главе, остальное же описано далее. Пробел ьные символ ы Java - язык свободной формы, т.е. вы не обязаны следовать каким-то осо­ бым правилам в отношении отступов. Например, программу Example можно было бы записать целиком в одной строке или любым другим странным спо­ собом при условии наличия хотя бы одного пробельного символа между каж­ дой парой лексем, которые еще не были разграничены операцией или разде­ лителем. В языке Java к пробельным символам относятся пробел, табуляция, новая строка или перевод страницы. Идентифи каторы Идентификаторы применяются для именования таких вещей, как классы, переменные и методы. Идентификатором может быть любая описательная последовательность прописных и строчных букв, цифр или символов подчер­ кивания и знака доллара. (Знак доллара не предназначен для общего исполь­ зования.) Они не должны начинаться с цифры, чтобы компилятор не путал их с числовым литералом. Как вы помните, язык Java чувствителен к регистру, поэтому VALUE и Value - разные идентификаторы. Вот несколько примеров допустимых идентификаторов: AvgTemp count а4 $ t e st this is ok Ниже представлены примеры недопустимых имен идентификаторов: 2 c ount high-temp Not / o k Глава 2 . Кратк ий обзор языка Java 77 На заметку! Начиная с JDK 9, одиночный символ подчеркивания нельзя применять в качестве иден­ тификатора. Л и те рал ы Константное значение в Java со зда ется с использованием его литерального представления. Ниже приведены примеры литералов: 100 98 . 6 'Х' "Тестовая строка" Начиная слева, первый литерал задает цело численно е значение, второй значение с плавающей то чко й, третий - символьную константу и четвер­ тый - строковое значение. Лит ерал мо жно применять везде, где допускается значение его типа. К о мментарии Ранее уже упоминалось, что в Java определены три типа ко мментариев. Два типа вы видели: однострочный и мноrострочный. Третий тип на зывается до­ кументирующим комментарием и используется для создания НТМL-файла, который документирует вашу программу. Документирующий комментарий начинается с символов / * * и заканчива ется си мволами * /. Документирующие комментарии обсуждаются в приложении А. Разделит е л и В Java есть несколько символов, ко торы е применяются в качестве раз­ делителей. Наиболее часто используемый разделитель в Java - то чка с за­ пятой. Как вы видели, он часто применяется для завершения операторов. До пустимые раздели тели описаны в табл. 2.2. Кл ю ч евые слова Java В настоящее время в языке Java определено 67 ключевых слов (табл. 2.3). В сочетании с синтаксисом операторов и ра зделителей они формируют ос­ нову языка Java. Как правило, ключ евые слова нельзя применять в качестве идентификаторов, т.е. они не могут использоваться в качестве имен для пере­ менных, классов или методов. Тем не менее, 16 ключевых слов являются кон­ текстно-чувствительными, а это значит, что они будут служить ключевыми словами только в случае применения с функциональным средством, к которо­ му относятся. Они поддерживают функциональные средства, появившиеся в Java за последние несколько лет. Десять ключевых слов относятся к модулям: expo rts, module, open, opens, provides, requi res, to, trans i t ive, uses и wi th. Записи объявляются с по мощью record. Для запечатанных классо в и интерф ейсов используется seal ed, non-sealed и pe rmi ts; yield применя­ ется с расширенным оператором swi t ch; var поддерживает выведени е типов локальных переменных. Поскольку они зависят от конт екста, их до бавление не повлияло на существующие програ ммы. 78 Часть 1. Язык Java Табnица 2.2. Симвоnы, исnоnыуемые в качестве раздеnитеnеi () Кругл.ые скобки о Применяется для указания списков параметров в определениях и вызовах методов. Кроме того, используется для определения порядка выполнения операций в обычных выражениях, выражениях внутри управляющих операторов и при приведении типов Фигурные скобки Применяется для указания значений автоматически инициализируемых массивов. Также используется для определения блоков кода, классов, методов и локальных областей действия [] Квадратные скобки Применяется для объявления типов массивов. Кроме того, используется для разыменования значений массива ; Точка с запятой Завершает операторы Запятая Отделяет последовательно следующие друг за другом идентификаторы при объявлении переменных. Также применяется для объединения операторов внутри for Точка Используется для отделения имен пакетов от имен подпакетов и классов. Кроме того, применяется для отделения имени переменной или метода от имени ссылочной переменной Двоеточия Используется для создания ссылки на метод или конструктор Троеточие Указывает параметр с переменным количеством аргументов Коммерческое "зт" (символ "а" с тонким спиральным штрихом) Начинает аннотацию @ Кроме того, начиная с JDK 9, подчеркивание само по себе считается клю­ чевым словом, чтобы предотвратить его использование в качестве имени че­ го-либо в программе. Начиная с JDK 17, ключевое слово strictfp объявлено устаревшим, т.е. не имеет никакого эффекта. Гла ва 2. Краткий обзор языка Java 79 Ключевые слова const и got o зарезервированы, но не применяются. На заре развития Java несколько других ключевых слов были зарезервированы для возможного использования в будущем. Однако в текущей спецификации Java определены только ключевые слова , перечисленные в табл. 2.3. Табnица 2.3. Кnючевые сnова Java abs t ract catch do f inal implemen t s module pac kage record stri ct fp throws var assert char douЫe f i nally import nat ive permi ts requires super to voi d break boolean const cla s s enum else for float instanceof int non-sealed new protected private sealed re turn synchron i zed swi tch transit ive transient vola t i le wh ile byte cont i nue export s goto inter face open prov i des short th i s t ry with case default extends if long opens puЫ i c stat i c throw use s yield Помимо ключевых слов в Java зарезервированы еще три имени, которые были частью Java с самого начала: true, fa lse и nu ll.Oни представляют со­ бой значения, определенные в Java , и не могут применяться в качестве имен переменных, классов и т.д. Б и блиотеки кл а сс о в Java В примерах программ, приведенных в главе, использовались два встроен­ ных метода Java: pr int ln ( ) и print ( ) . Как упоминалось ранее, указанные методы доступны через System . out. Здесь System - это предопределенный класс Java , который автоматически включается в ваши програ ммы. В более широком плане среда Java опирается на несколько встроенных библиотек классов, которые содержат множество встроенных методов, обеспечивающих поддержку таких средств, как ввод-вывод, обработка строк, работа с сетью и графика. Стандартные классы также обеспечивают поддержк у графического пользовательского интерфейса. Та ким образом, Java как совок упность пред­ ставляет собой сочетание самого язы ка Java с его стандартными классами. Позже вы увидите, что библиотеки классов предоставляют большую часть функ циональности, связанной с Java. Действительно, частью становления программиста на Java является обучение использованию стандартных клас­ сов Java. В части I книги по мере необходимости описываются различные эле­ менты классов и методов стандартной библиотеки. В части II подробно рас­ сматриваются несколько библиотек классов. ГЛ А ВА Ти п ы да н н ых, переменные и массивы В настоящей главе рассматриваются три наиболее фундаментальных эле­ мента Java: типы данных, переменные и массивы. Как и все совр еменные язы­ ки программирования, в Java поддерживается несколько типов данных. Вы можете использовать такие типы для объявления переменных и создания массивов. Далее вы увидите, что подход Java к этим элементам может отлича­ ется ясностью, эффективностью и связностью. Java - с трого ти п из иро в а н н ы й язы к Первым делом важно отметить, что Java является строго типизированным языком. Действительно, отчасти безопасность и надежность Java проистека­ ют как раз из данного факта. Давайте посмотрим, что это значит. Во-первых, у каждой переменной есть тип, у каждого выражения есть тип, и каждый тип строго определен. Во-вторых, все присваивания, как явные, так и через пере­ дачу параметров в вызовах методов, проверяются на совместимость типов. Автоматическое приведение или преобразование конфликтующих типов, как делается в некоторых языках, отсутствует. Компилятор Java проверяет все выражения и параметры для гарантирования совместимости типов. Любые несоответствия типов считаются ошибками, которые должны быть исправле­ ны до того, как компилятор завершит компиляцию класса. При м и т ив н ые ти п ы В Java определены восемь примитивных типов данных: byte, sho rt, int, long, char, float, douЬle и boolean. Примитивные типы также часто назы­ вают простыми типами, и в книге будут применяться оба термина. Их можно разделить на четыре группы. • ЦелЪJе числа. Эта группа включа ет типы byte, short, int и long, пред­ назначенные для представления целых чисел со знаком. • Числа с плавающей точкой. В эту группу входят типы fl oat и douЫe, которые пр едставляют числа с точностью до определенного знака после десятичной точки. Глава 3 . Ти лы дан ных, п ерем енн ы е и м ассив ы 81 • Символы. Эта группа включает тип cha r, предназначенный для пред­ ставления символов из набора наподо бие букв и цифр. • Булевские значения. В эту группу входит тип boolean, который являет­ ся специальным типом, представляющим истинные и ложные значения. Вы можете использовать перечисленные выше типы в том виде, как есть, либо создавать массивы или собственные типы классов. Таким образо м, они образуют основу для всех других типов данных, ко торые вы можете создать. Примитивные типы представляют одино чные значения, а не сложные объ­ екты. Хо тя в остально м язык Java полностью объектно-ориентирован, прими­ тивные типы - нет. Они аналогичны простым типам, ко торые встречаются в большинстве других не объектно-ориентированных яз ыков. Причиной кро­ ется в эффективности. Превращение прими тивных типов в объекты сли шком сильно снизило бы производительность. Примитивные типы определены так, чтобы иметь явный диапазон и обла­ дать математически строгим поведением. Языки вроде С и С++ позволяют из­ менять размер целого числа в зависимости от требований среды выполнения. Однако Java в этом отношении отличае тся. Из-за требования переносимости Java все типы данных имеют строго определенный диапазон. Например, i n t всегда занимает 3 2 бита независимо от конкретной платформы. В итоге появ­ ляется возможность написания программ, которые гарантированно будут ра­ ботать без переноса на любую машинную архит ектуру. Хотя строго е установ­ ление размера целого числа в некоторых средах может привести к небольшой потере производительности, оно нео бходимо для обеспечения переносимости. Давайте взглянем на каждый тип по очереди. Цел ы е ч и с л а В Java определены четыре целочисленных типа: byte, short, int и long. Все они представляют положи т ельные и отрицательные значения. В Java не подд ерживаются только положительные целы е числа без знака. Во многих других языках про граммирования поддержива ются как целые числа со з на­ ком, так и целые числа без знака, но разработчики Java решили, что целые числа без знака не нужны. В частности, они считали, что понятие без знака использовалось в основном для указания поведения старшего бита, который определяет знак целочисленно го значения. В главе 4 вы увидите, что Java по ­ другому управляет смыслом старшего бита , добавляя специальную операци ю "беззнакового сдвига вправо': Таким образом, необходимость в целочислен­ ном типе без знака попросту отпала. Ширина (или разрядность) целочисленного типа не должна трактовать­ ся как объем потребляемой им памяти, а скорее как поведение, которо е он определяет для переменных и выражений данного типа. Исполняющая среда Java может свободно использовать любой желаемый раз мер при условии, что типы веду т себя так, как вы их объявили. Ширина и диапазоны целочислен­ ных типов сильно различаются, как показано в табл. 3.1. long .••,� .... 64 От -9 223 372 036 854 775 808 до 9 223 372 036 854 775 807 int 32 От -2 147 483 648 до 2 147 483 647 short 16 От -32 768 до 32 767 byte 8 От -128 ДО 127 Часть 1. Язык Java 82 .... .- Табпмца 3.1 . Разрядность м диапазоны цеnочмсnенных типов 1.; . ::.с .r , . , . - ,..,...t WJ t• _ ;о µз f _.3 �· '' А теперь более подробно рассмотрим каждый целочисленный тип. Ти п byte Наименьшим целочисленным типом является byte. Он является 8-бит­ ным типом со знаком, диапазон значений которого составляет от - 128 до 127. Переменные типа byte особенно удобны при работе с потоком данных из сети или файла. Они также полезны, коrда приходится иметь дело с низко­ уровневыми двоичными данными, которые моrут быть несовместимыми на­ прямую с другими встроенными типами Java. Байтовые переменные объявляются с применением ключевого слова byte. Например, ниже объявлены две переменные типа byte с именами Ь и с: byte Ь, с; Ти п short Тип short представляет собой 16-битный тип с диапазоном значений от -32 768 до 32 767. Он является, вероятно, наименее часто используемым ти­ пом в Java. Вот несколько примеров объявлений переменных типа short: зhort з ; зhort t; Ти п int Самым распространенным целочисленным типом можно считать int 32-битный тип, диапазон значений которого составляет от -2 147 483 648 до 2 147 483 647. Помимо других целей переменные типа int обычно применя­ ются для управления циклами и для индексации массивов. Хотя может по­ казаться, что использование типа byte или short будет эффективнее int в ситуациях, коrда больший диапазон int не нужен, это не всегда так. Причина в том, что коrда в выражении присутствуют значения byte и short, при вы­ числении выражения они fiовышаются до int. (Повышение типов обсуждает­ ся позже в rлаве.) Таким образом, int часто является лучшим выбором, коrда требуется целое число. Гл ава 3 . Ти п ы д а нных, пе р е м е нны е и ма ссив ы 83 Ти п lonq Тип long представляет собой 64-битный тип со знаком и по лезен в слу­ чаях, когда тип i n t недостаточно велик для хранения желаемого значения. Диапазон значений long довольно широк, что делает его удо бным типом для работы с крупными целыми числами. Скажем, вот программа, которая рассчи­ тывает количество миль, проходимых свето м за указанное количество дней: // Рассчитать расстояние, проходимое светом, / / с применением переменных типа loпg . class Light { puЫic static void main ( St r ing [ ] a rgs ) int lightspeed; long days ; long seconds ; long distance ; / / Приблизитель ная скорость света в милях за се кунду. li ghtspeed = 1 8 6 0 0 0 ; days = 1 0 0 0 ; // указать количество дней seconds = days * 2 4 * 60 * 60 ; / / преобразовать в секунды distance = lightspeed * seconds ; // рассчитать расстояние // Вывести примерное расстояние в милях, проходимое светом / / за указанное количество дней . System .out . print ( "За " + days ) ; S ystem. out . print ( " дней свет пройдет около " ) ; System . out . println ( distance + " миль . " ) ; Программа сгенерирует следующий вывод: За 1000 дней свет пройдет около 1 6 0 7 0 4 00000000 миль . Совершенно оч евидно, что результат не уместился бы в переменную т ипа int. Ти п ы с плава ю щей точ ко й Числа с плавающей точкой, также извес тные как вещественные числа, ис­ пользуются при вычислении выражений, требующих точности до определен­ ного знака после десят ичной точки. Например, вычисления вроде квадратно­ го корня или трансцендентных функ ций, подобных синусу и косинусу, дают в результате значение, точность которого требует т ипа с плавающей точкой. В Java реализован стандартный (IEEE-754) набор типов и о пера ций с плава­ ющей точко й. Сущес твуют две разновидности т ипов с плавающей точкой, float и douЬle, которые представляют числа с одинарной и двойной точно­ стью соответственно. Их ширина и диапазоны приведены в табл. 3.2. 84 Часть 1. Язык Java Табnмца 3.2. Разрядность м диапазоны типов с nnaвaioщei точкой Имя douЫe 64 От 4,9е-324 до 1,8е+308 float 32 От 1,4е-045 до 3,4е+О38 1 Ниже каждый тип рассматривается по отдельности. Ти п float Тип float определяет значение одинарной точности, которое занимает 32 бита памяти. На некоторых процессорах значения одинарной точности об­ рабатываются быстрее и занимают вдвое меньше места, чем значения двой­ ной точности, но вычисления становятся неточными при очень больших или очень маленьких значениях. Переменные типа float удобны, когда нужен дробный компонент, но не требуется высокая точность. Например, тип float может быть полезен при представлении сумм в долларах и центах. Вот несколько примеров объявлений переменных типа float: float hightemp, lowtemp; Ти п douЬle Значение двойной точности, обозначаемое ключевым словом douЫe, за­ нимает 64 бита. На некоторых современных процессорах, оптимизирован­ ных под высокоскоростные математические вычисления, обработка значений двойной точности в действительности выполняется быстрее, чем значений одинарной точности. Все трансцендентные математические функции, такие как sin ( ) , cos ( ) и sqrt ( ) , возвращают двойные значения. Когда вам нужно поддерживать точность в течение многократно повторяющихся вычислений или манипулировать числами с крупными значениями, то тип douЫe будет наилучшим вариантом. Ниже показана короткая программа, в которой переменные типа douЫe применяются для вычисления площади круга: / / Вычисли ть площадь круга . class Area { puЫic static void main (String ( ] args ) douЫe pi , r, а; r = lO . B; / / радиус круга pi = 3 . 1416; / / приближе нное значение pi а = pi * r * r; // ВЫЧИСЛИТЬ площадь System . out .println ( "Пnoщaдь круга рав на " + а) ; } Глава 3 . Тип ы данн ых, переменн ы е и м а ссивы 85 Сим вол ы Для хранения символов в Java испо льзуется тип данных char. Важно по­ нять один ключевой момент: для представления символов в Java применяется Unicode. Кодиро вка Unicode определяет полностью международный набор символов, с помощью которого можно представить все си мволы, встречаю­ щиеся во всех естественных языках. Он объединяет десятки наборов си мво­ лов, таких как романский, греческий, арабский, кириллический, иври т, катака­ на, ханrул и многие дру гие. Во время создания Java для Unicode требовалось 16 би т. Таким образом, в Java тип char является 16-битным с диапазоном зна­ чений от О до 65 535. Отрицат ельных значений char не бывает. Стандартный набор символов, известный как ASCII, по-прежнему находится в диапазоне от О до 1 27, а расширенный 8-битный набор си мволов, ISO-Latin- 1 - в диапа­ зоне от О до 255. Поскольку язык Java предназначен для написания програ мм, используемых по всему миру, при представлении символов имеет смысл при­ менять Unicode. Конечно, использование Unicode несколько неэффективно для таких языков, как англи йски й, немецкий, испанский или французский, символы которых могу т легко умещаться в пределах 8 бит. Но такова цена, которую приходится платить за гло бальную переносимость. На заметку! Дополнительную информацию о Unicode можно найти на веб-сайте http : / /www . unicode . org. В следу ющей программе демонстрируется применение переменных char: // Демонстрация использования типа данных cha r . class CharDemo { puЫ ic s t atic void main ( String [ ] a rgs ) { char chl , ch2 ; chl = 8 8 ; / / код для Х ch2 = ' У ' ; System . out .print ( " chl и ch2 : " ) ; System . out . println ( chl + " " + ch2 ) ; Вот вывод, отображаемый в результате выполнения программы: chl и ch2 : Х У Обрати те внимание, что переменной chl присвоено значение 8 8 , которое является кодом ASCII (и Unicode), соот ветствующим букве Х. Как уже упоми­ налось, набор си мволов ASCII занимает первые 127 значений в наборе симво­ лов Unicode. По этой причине все "старые уловки'; которые вы могли исполь­ зовать с символами в других языках, буду т работать и в Java. Хо тя тип char предназначен для хранения символов Unicode, его также можно применять как целочисленный тип , с которым допускается выполнять арифметические операции. Скажем, вы можете сложи ть два символа вместе либо инкрементировать значение си мвольной переменной. Взгляни те на при­ веденную далее программу: 86 Часть 1. Язык Java // Переменные char ведут себя подобно целым числам . cl ass Cha rDemo2 [ puЫ ic static vo id main ( S tring [ ] args ) [ char ch l ; chl = ' Х ' ; System . out . println ( " chl содержит " + ch l ) ; chl++ ; // инкрементиро вать chl System . out . pr intln ( " chl теперь содержит " + ch l ) ; Программа генерирует показанный н иже вывод: ch l содержит Х chl теперь содержит У В программе переменной chl сначала присваивается значение ' Х ' . Затем выполняется инкрементирование chl . В результате chl содержит значение ' У ' , т.е. следующий символ в последовательности ASCII (и Unicode). На заметку! В формальной спецификации Java тип char упоми нается как целочисленный тип, а это значит, что он относится к той же общей категории, куда входят int, s hort, l ong и byte. Но поскольку тип c h a r испол ьзуется в основном для предста вления символов Unicode, обычно он считается отдельной категорией. Бул евские з на ч ения В Java имеется примитивный тип под названием boo lean, предназначен­ ный для хранения булевских значений. Возможных значений только два: true и fal se. Значения такого типа возвращаются всеми операциями отношения, например, а<Ь. Использование типа bool ean также обязательно в условных выражениях в операторах управления, таких как if и for. Ниже показана программа, в которой демонстрируется применение типа boo lean: // Демонстрация исполь зования значений boolean . class BoolTest { puЫ i c static void ma in ( S tring [ ] a rgs ) { boolean Ь ; Ь = false; System . out . printlп ( " Ь равно " + Ь ) ; Ь = tru e ; System . ou t . println ( " b равно " + Ь ) ; / / Значение bool ean может управлять оператором i f . i f ( b ) System . out . p rintln ( " Этo выполняется . " ) ; Ь = fal s e ; i f ( b ) System . out . println ( " Этo не выполняется . " ) ; / / Результатом операции отношения является значение bool ean . System . out . println ( " l O > 9 равно " + ( 1 0 > 9 ) ) ; Глава 3. Ти пы данны х, переменные и масс и вы 87 Программа генерирует такой вывод: Ь равно false Ь равно t rue Это выполняется . 1 0 > 9 равно t rue В этой программе следует отметить три интересных момента. Во-первых, как видите, когда println ( ) выводит булевское з начение, отображается true или false. Во-вторых, самого по себе з начения булевской переменной до­ статочно для управления оператором if, т.е. нет необходимости з аписывать оператор i f в виде: if (Ь == t ru e ) . . . В-третьих, результатом операции отношения, подобной <, является булев­ ское з начение. Именно потому выражение 1 0> 9 отображает з начение true. Кроме того, дополнительный набор скобок вокруг 1 0 > 9 нужен из -з а того, что приоритет операции + выше приоритета операции >. Подробный а нализ л итерало в Литералы кратко упом инались в главе 2. После формаль ного описания встроенных типов давайте более подробно проанализируем литералы. Целочи с ленные литерал ы Целые числа, вероятно, следует считать наиболее часто используемым типом в рядовой программе. Любое целочисленное з начение является цело­ численным литералом. Примерами могут служить 1 , 2, 3 и 42. Все они пред­ ставляют собой десятичные з начения, т.е. описывают числа по основанию 10. В целочисленных литералах допускается применять еще два вида чисел восьмеричные (по основани ю 8) и шестнадчатеричные (по основани ю 1 6). Восьмеричные з начения обозначаются в Java ведущим нулем. Нормальные десятич ные ч исл а не могут содержать ведущий ноль. Таким образом, вы­ глядящее допустимым з начение 09 вызовет ошибку на этапе компиляции, поскольку 9 находится за пределами восьмеричного диапазона от О до 7. Программисты чаще использ уют для чисел основание 1 6, которое точ­ но соответствует раз мерам слов по модулю 8, таким как 8, 1 6, 32 и 64 бита. Шестнадцатеричная константа указ ывается с ведущим нулем и буквой "Х" (Ох или ОХ). Диапазон шестнадцатеричных цифр - от О до 15, так что числа от 10 до 1 5 з аменяются буквами от А до F (или от а до f). Целочисленные литералы соз дают з начения типа int, которые в Java явля­ ются 32-битными целыми числами. Учитывая тот факт, что язык Java строго типиз ирован, вам может быть интересно, как присвоить целочисленный ли­ терал одному из других целочисленных типов Java, скажем, byte или long, не вызывая ошибки несоответствия типов. К счастью, такие ситуации легко раз решаются. Когда литеральное з начение присваивается переменной byte или short, ошибка не генерируется, есл и л итеральное з начение находится Ч а сть 1. Язык Java 88 в пределах диапаз она допустимых з начений целевого типа. Целочисленный литерал всегда можно присвоить переменной long. Тем не менее, для ука­ з ания литерала long понадобится явно сообщить компилятору, что лите­ ральное з начение имеет тип long. Это делается путем добавления к литералу буквы "L" в верхнем или нижнем регистре. Например, 0x7 f f f f f f f f f f f f ffL или 9 2 2 3 3 7 2 0 3 68 5 4 7 7 5 8 07L - наибольшее значение типа long. Целое число можно присваивать переменной char, если оно находится в пределах допу­ сти мого диапазона. Указывать целочисленные литералы можно также в д воичной форме, до­ бавляя к з начению префикс ОЬ или 0В. Скажем, вот как з адать десятичное значение 10 с помощью двои чного литерала: int х = int х = int х = int х = ОЫ О l О ; Помимо прочего, добавление двои чных литералов упрощает ввод значе­ ний, применяемых в качестве битовых масок. Десяти чное (или шестнадцате­ ри чное) представление значения визуально не передает смысл битовой ма­ ски, а двои чный литерал его передает. В целочисленном литерале можно указывать один или несколько симво­ лов подчерки вания, которые упрощают чтение больших целочисленных ли­ тералов. При компиляции литерала символы под черки вания отбрасываются. Например, в следующей строке: 1 2 3_4 5 6_7 8 9 ; переменной х будет присвоено значение 1 2 3 4 5 67 8 9. Символы подчеркива­ ния игнорируются. Они могут использоваться только для разделения цифр. Символы под черкивания не могут находиться в начале или в конце литерала. Однако между цифрами разрешено применять более одного символа подчер­ к ивания. Например, показанный ниже код совершенно допустим: 123 456 789; Использовать символы подчерки вания в цело численных литералах осо­ бенно удобно при кодировании так и х элементов, как телефонные номера, идентификационные номера з ак аз чиков, номера деталей и т.д. Они также полезны для визуального группирования при указ ани и двои чных литералов. Скажем, двои чные значения часто визуально группируются в блоки по четы­ ре цифры: ОЫ 1 0 1 0 1 0 1 0 0 0 1 1 0 1 0 ; Л итера лы с плавающей точкой Числа с плавающей точкой представляют десяти чные з начения с дробной частью. Они могут быть выражены с применением либо стандартной, либо научной (экспоненциальной) формы з аписи. Стандартная форма записи об­ разована из целой части числа, десяти чной точки и дробной части. Например, 2 . О, З . 1 4 1 5 9 и О . 6 6 67 представляют допустимые числа с плавающей точкой в стандартной форме з аписи. В научной форме записи использ уется стандарт- Глава 3. Ти пы данных, переменные и массивы 89 ная форма записи числа с плавающей точкой плюс суффикс, указывающий степень 10, на которую число должно быть умножено. Показатель степени обозначается буквой Е или е, за которой следует положительное или отрица­ тельное десятичное число. Примерами могут служить 6 . 0 2 2 Е2 3, 3 1 4 1 5 9Е-05 и 2 е+ 1 0 0. Литералы с плавающей точкой в Java по умолчанию имеют тип douЫ e. Для указания литерала типа fl oat к константе необходимо добавить букву F или f. Можно также явно указывать литерал douЫe, добавляя букву О или d, хотя поступать так излишне. Назначаемый по умолчанию тип douЫ e занима­ ет 64 бита памяти, а меньший тип float требует только 32 бита. Шестнадцатеричные литералы с плавающей точкой тоже померживаются, но применяются редко. Они должны записываться в форме, похожей на на­ учную, но вместо Е или е используется буква Р или р. Например, 0xl 2 . 2 Р2 является допустимым литералом с плавающей точкой. Значение после Р, на­ зываемое двоичным показателем степени, указывает степень двойки, на кото­ рую умножается число. Следовательно, Ох1 2 . 2 Р2 представляет 72 . 5. В литералы с плавающей точкой можно встраивать один или несколько символов подчеркивания, что работает точно так же, как и для описанных ранее целочисленных литералов. Цель - облегчить чтение больших литера­ лов с плавающей точкой. При компиляции литерала символы подчеркивания отбрасываются. Скажем, в следующей строке: douЫe num = 9_4 2 3_4 97_8 62 . 0 ; переменной num будет присвоено значение 9 4 2 3 4 978 62 . О. Символы подчер­ кивания игнорируются. Как и в случае с целочисленными литералами, сим­ волы подчеркивания могут применяться только для разделения цифр. Они не могут находиться в начале или в конце литерала. Однако между двумя цифрами разрешено указывать более одного символа подчеркивания. Кроме того, символы подчеркивания можно использовать в дробной части числа. Например, показанная ниже строка совершенно допустима: douЫe num = 9 4 2 3 4 9 7 . l О 9 ; В данном случае дробной частью будет . 1 0 9. Булевские л итералы Булевские литералы просты. Значение типа boolean может иметь только два логических значения: true и fal s e. Значения true и false не преобра­ зуются в какое-то числовое представление. Литерал true в Java не равен 1 , а литерал f a l s e н е равен О. В Java логические литералы можно присваивать только переменным, объявленным как bool ean, или применять в выражени­ ях с булевскими операциями. Символьные л итер а лы Символы в Java являются индексами в наборе символов Unicode. Они представляют собой 16-битные значения, которые можно преобразовывать 90 Часть 1. Язык Java в целые числа и обрабатывать с помощью целочисленных операций, таких как сложение и вычитание. Символьные литералы задаются внутри пары оди­ нарных кавычек. Все видимые символы ASCII можно вводить прямо внутри кавычек, скажем, ' а ' , ' z ' и ' @ ' . Для символов, которые ввести напрямую не­ возможно, существует несколько управляющих последовательностей, позво­ ляющих ввести нужный символ, например, ' \ ' ' для самого символа одинар­ ной кавычки и ' \n ' для символа новой строки. Существует также механизм прямого ввода значения символа в восьмеричной или шестнадцатеричной форме. Для восьмеричной формы записи используется обратная косая черта, за которой следует трехзначное число. Скажем, ' \ 141 ' - зто буква ' а ' . Для шестнадцатеричной формы записи применяется обратная косая черта и бук­ ва u (\u), а за ними в точности четыре шестнадцатеричных цифры. Например, ' \u0061 ' - зто буква ' а ' из набора ISO-Latin-1, потому что старший байт равен нулю. ' \ua 432 ' - зто символ японской катаканы. Управляющие по­ следовательности символов перечислены в табл. 3.3. Табnица 3.3. Симвоnьные управnяющие nосnедоватепьности \ddd \ uxxxx \' \" :,., \ \r \n \f \t \Ь \s \ конец-строки Восьмеричный символ (ddd) Шестнадцатеричный символ Unicode (хххх) Одинарная кавычка Двойная кавычка Обратная косая черта Возврат каретки Новая строка (также известная как перевод строки) Подача страницы Табуляция Забой Пробел (последовательность добавлена в JDK 15) Строка продолжения (применяется только к текстовым блокам; последовательность добавлена в JDK 15) Строковые литералы Строковые литералы в Java указываются таким же образом, как в боль­ шинстве других языков - путем заключения последовательности символов в пару двойных кавычек. Вот примеры строковых литералов: "Hello World" "two\nlineз " " \"Thiз is in quoteз \"" Глава 3. Ти п ы данн ых, переменные и м ассив ы 91 Управляющие последовательности и во сьмеричная/шестнадцатеричная формы записи, которые были определены для символьных литералов, внутри строковых литералов работают точно так же. В отношении строковых лите­ ралов Java важно помнить, что они должны начинаться и заканчиваться в той же строчке, даже если она переносится. Для строковых литералов нет управ­ ляющей последовательности продолжения строки, как в ряде других языков. (Полезно отметить, что в версии J DK 15 в Java появилось функциональное средство, называемое текстовым блоком, которое обеспечивает больший контроль и гибкость, когда требуется несколько строчек текста. См. главу 17.) На заметку! Как вам может быть известно, в некоторых других языках строки реализуются в виде массивов символов. Тем не менее, в Java ситуация иная. Строки на самом деле являются объ­ ектными типами. Как вы увидите далее в книге, по причине реализации строк как объектов Java обладает обширными возможностями обработки строк, которые характеризуются мо­ щью и простотой использования. Перемен н ые Переменная служит базовой единицей хранения в программе на Java. Переменная определяется комбинацией идентификатора, типа и необязатель­ ного инициализатора. Кроме того, все переменные имеют область видимости, определяющую их доступность, и время жизни. Эти элементы рассматрива­ ются далее в главе. О бъ явл е н ие п ереме н н о й В Java все переменные должны быть объявлены до того, как их можно бу­ дет использовать. Ниже показана основная форма объявления переменной: тип иденти фика тор [= зна чение ] [ , идентифика тор [= зна чение ] . . . ] ; Здесь тип - один из примитивных типов Java либо имя класса или ин­ терфейса. (Типы классов и интерфейсов обсуждаются позже в части I книги.) Идентификатор - это имя переменной. Вы можете инициализировать пере­ менную, указав знак равенства и значение. Имейте в виду, что результатом выражения инициализации должно быть значение того же (или совместимо­ го) типа, что и тип, указанный для переменной. Для объявления более одной переменной заданного типа применяется список, разделенный запятыми. Взгляните на несколько примеров о бъявлений переменных различных ти­ пов. Обратите внимание, что некоторые из них включают инициализацию. // объявить три переменных типа int , а, Ь и с int а, Ь, с ; // объявить еще три переменных int d = 3 , е , f = 5 ; / / типа int с инициализацией d и f / / инициализировать z byte z = 2 2 ; / / объявить приближенное значение pi douЫe pi = 3 . 1 4 1 5 9 ; cha r х = ' х ' ; // переменная х имеет значение ' х ' Выбранные вами идентификаторы не имеют в своих именах ничего, что указывало бы на их тип. Java позволяет любому корректно сформированному идентификатору иметь любой объявленный тип. 92 Ч а сть 1. Яз ы к Java Динами ч ес ка я инициал и з аци я Хотя в предыдущих примерах в качестве инициализаторов использовались только константы, Java позволяет инициализировать переменные динамиче­ ски с применением любого выражения, действительного на момент объявле­ ния переменной. Например, вот короткая программа, которая вычисляет длину гипотенузы прямоугольного треугольника при известных длинах его катетов: // Демонстрация исполь зования динамической инициали зации . class Dyninit { puЬlic static vo id main ( String [ ] args ) { douЫe а = 3 . 0 , Ь = 4 . 0 ; // Переменная с инициализируется динамически . douЫ e с = Math . s qrt ( а * а + Ь * Ь ) ; System . out . pri ntln ( "Длина гипотенузы ра вна " + с ) ; Здесь объявляются три локальные переменные - а, Ь и с. Первые две, а и Ь, инициализируются константами. Однако переменная с инициализиру­ ется динамически длиной гипотенузы (по теореме Пифагора). В программе используется еще один встроенный метод Java, s qrt ( ) , являющийся чле­ ном класса Math, который вычисляет квадратный корень своего аргумента. Ключевой момент в том, что выражение инициализации может содержать любой элемент, действительный во время инициализации, в том числе вы­ зовы методов, другие переменные или же литералы. Обла сть в и д имости и время жизни переменны х До сих пор все применяемые переменные были объявлены в начале метода ma in ( ) . Тем не менее, Java позволяет объявлять переменные внутри любого блока. Как объяснялось в главе 2, блок начинается с открывающей фигурной скобки и заканчивается закрывающей фигурной скобкой. Блок определяет об­ ласть видимости. Таким образом, каждый раз, начиная новый блок, вы созда­ ете новую область видимости. Область видимости устанавливает, какие объ­ екты видны другим частям вашей программы. Она также определяет время жизни этих объектов. Нередко мыслят в терминах двух категорий областей видимости: глобаль­ ной и локальной. Однако такие традиционные области видимости плохо впи­ сываются в строгую объектно-ориентированную модель Java. Хотя вполне возможно создать глобальную область видимости, это скорее исключение, нежели правило. Две основные области видимости в Java определяются клас­ сом и методом. Даже такое различие несколько искусственно. Тем не менее, поскольку область видимости класса обладает несколькими уникальными свойствами и атрибутами, которые не применяются к области видимости, определенной методом, то различие обретает смысл. Из-за отличий обсужде­ ние области видимости класса (и объявленных в ней переменных) отложено Глава 3. Ти п ы да нн ы х, пе р еменн ые и м асси вы 93 до главы 6, где будут описаны классы. Сейчас мы исследуем только области видимости, определяемые методом или внутри него. Область видимости, определяемая методом, начинается с его открываю­ щей фигурной скобки. Однако если у метода есть параметры, то они тоже вхо­ дят в область видимости метода. Область видимости метода заканчивается закрывающей фигурной скобкой. Такой блок кода называется телом метода. Как правило, переменные, объявленные внутри области видимости, не бу­ дут доступны в коде за рамками этой области. Таким образом, когда вы объ­ являете переменную в области видимости, то локализуете ее и защищаете от несанкционированного доступа и/или модификации. Действительно, прави­ ла области видимости обеспечивают основу для инкапсуляции. Переменная, объявленная внутри блока, называется локальной переменной. Области могут быть вложенными. Например, создавая блок кода, вы соз­ даете новую вложенную область. В таком случае внешняя область видимости охватывает внутреннюю область видимости. В итоге объекты, объявленные во внешней области, будут видимыми коду во внутренней области. Тем не менее, обратное утверждение неверно. Объекты, объявленные во внутренней области, не будут видны за ее пределами. Чтобы лучше понять влияние вложенных областей видимости, взгляните на следующую программу: // Демонстрация области видимости блока кода . class Scope { puЫic static void main ( String [ ] args ) { / / переменная известна всему коду внутри ma in ( ) int х; х = 10; i f ( x == 1 0 ) { / / начало новой области видимости int у = 2 0 ; // переменная известна только этому блоку / / х и у здесь известны . System . out . println ( " x и у : " + х + " " + у) ; х = у * 2; } // y = l00; // Ошибка ! Переменная у здесь неизвестна . // Переменная х здесь по-прежнему известна . System . out . println ( " Знaчeниe х равно " + х ) ; Как указано в комментариях, переменная х объявлена в начале области видимости метода ma i n ( ) и доступна всему последую щему коду внутри main ( ) . Переменная у объявлена внутри блока i f. Поскольку блок определя­ ет область видимости, у видна только остальному коду внутри этого блока. Вот почему строка у = 1 0 0 ; вне блока i f закомментирована. Если удалить ведущие символы комментария, тогда возникнет ошибка на этапе компиля­ ции, потому что переменная у не видна за пределами своего блока. Внутри блока i f можно использовать переменную х, т.к. код внутри блока (т.е. во вложенной области видимости) имеет доступ к переменным, объявленным в объемлющей области. 94 Часть 1. Язык Java Внутри блока переменные могут быть объявлены в любой момент, но дей­ ствительны только после их объявления. Таким образом, если вы определяете переменную в начале метода, то она доступна всему коду внутри этого мето­ да. И наоборот, если вы объявляете переменную в конце блока, то она по су­ ществу бесполезна, потому что код не будет иметь к ней доступа. Например, следующий фрагмент кода приведет к ошибке, т.к. переменную count нельзя использовать до ее объявления: / / Здесь присут ствует ошибка ! count = 1 0 0 ; / / Переменную count нельзя исполь зовать до ее объявления ! int count ; Необходимо запомнить еще один важный момент: переменные создаются при входе в их область видимости и уничтожаются при выходе из их области видимости. Другими словами, переменная не будет хранить свое значение после того, как покинет пределы области видимости. Следовательно, пере­ менные, объявленные в методе, не сохраняют свои значения между вызовами этого метода. Кроме того, переменная, объявленная внутри блока, утратит свое значение при выходе из блока. Таким образом, время жизни переменной ограничено ее областью видимости. Если объявление переменной содержит инициализатор, тогда переменная будет повторно инициал изироваться при каждом входе в блок, где она объ­ явлена. Например, рассмотрим показанную ниже программу: / / Демонстрация времени жизни переменной . cla s s LifeTime { puЫic static vo id ma in ( St ring [ ] args ) { int х ; for ( x = О ; х < 3 ; х++ ) { int у = - 1 ; / / переменная у инициализируе тся при каждом входе в блок System . out . println ( " Знaчeниe у равно " + у ) ; // всегда выводится -1 у = 100; System . out . println ( "Teпepь значение у равно " + у ) ; Программа генерирует следующий вывод: Значение у равно -1 Теперь значение у равно 100 Значение у равно - 1 Теперь значение у равно 1 0 0 Значение у равно - 1 Теперь значение у равно 100 Как видите, переменная у повторно инициализируется значением - 1 при каждом входе во внутренний цикл for. Несмотря на то что впоследствии ей присваивается значение 1 0 0, это значение утрачивается. Последнее замечание: хотя блоки могут быть вложенными, вы не можете объявить переменную с тем же именем, что и у переменной во внешней об­ ласти видимости. Скажем, показанная далее программа некорректна: Глава 3 . Типы да нных, п е р е менные и м ассивы 95 // Эта про грамма не скомпилирует ся . class ScopeErr { puЫ i c static void ma i n { S t ring [ ] args ) int bar = 1 ; / / создать новую обла сть видимо сти { i n t bar = 2 ; // ошибка на этапе компиляции - переменная bar / / уже определена П р еоб р а з ова н ие и п р ивед ение типов Если у вас есть опыт программирования, то в ы уже знаете, что довольно часто значение одного типа присваивается переменной другого типа. В слу­ чае, когда два типа совместимы, компилятор Java автоматически выполнит преобразование. Скажем, значение типа int всегда можно присваивать пере­ менной типа long. Однако не все типы совместимы и потому не все преоб­ разования типов неявно разрешены. Например, автоматическое преобразова­ ние из douЫe в byte не определено. К счастью, обеспечить преобразование между несовместимыми типами все-таки можно. Для этого придется приме­ нять приведение, которое выполняет явное преобразование между несовме­ стимыми типами. Давайте более подробно рассмотрим автоматические пре­ образования и приведение типов. Авто ма т и ч ес к и е преобра зов а н и я в Java Когда значение одного типа присваивается переменной другого типа, ав­ томатическое преобразование типов происходит в случае удовлетворения следующих двух условий: • два типа совместимы; • целевой тип больше исходного типа. При соблюдении указанных двух условий выполняется расширяющее пре­ образование. Например, тип int всегда достаточно велик, чтобы хранить все допустимые значения byte, поэтому явное приведение не требуется. С точки зрения расширяющих преобразований числовые типы, включая цело­ численные типы и типы с плавающей точкой, совместимы друг с другом. Тем не менее, автоматические преобразования числовых типов в char или bool ean не предусмотрены. Кроме того, типы char и boolean не совместимы друг с другом. Как упоминалось ранее, компилятор Java также выполняет автоматическое преобразование типов при сохранении литеральной целочисленной констан­ ты в переменные типа byte, short, long или cha r. П р и веден и е несовмест и мы х ти пов Несмотря на удобство автоматического преобразования типов, оно не сможет удовлетворить все требования. Скажем, что делать, когда нужно при- 96 Ч а сть 1. Язык Java своить значение типа int переменной типа byte? Такое преобразование не будет выполняться автоматически, потому что тип b yte меньше типа i nt. Преобразование подобного рода и ногда называют сужающим преобразовани­ ем, пос кольку значение явно сужается, чтобы уместиться в целевой тип. Преобразование между двумя несовместимыми типами создается с ис­ пользованием приведения. Приведение представляет собой просто явное пре­ образование типа и имеет следующую общую форму: ( целевой- тип) зна чение Здесь целевой-тип указывает желаемый тип, в который необходимо пре­ образовать заданное значение. Например, в показанном далее фрагменте кода тип i nt приводится к byte. Если целочисленное значение выходит за пределы диапазона типа byte, тогда оно уменьшается по модулю (остатку от целочисленного деления) диапазона byte. int а ; byte Ь; // . . . Ь = ( byte ) а ; Когда переменной целочисленного типа присваивается значение с плаваю­ щей точкой, будет происходить другой тип преобразования: усечение. Как из­ вестно, целые числа не имеют дробных частей. Таким образом, в случае при­ сваивания переменной целочисленного типа значения с плавающей точкой дробная часть теряется. Например, если целочисленной переменной присва­ ивается значение 1 . 2 3, то результирующим значением оказывается просто 1, а часть О . 2 3 усекается. Разумеется, если размер целого компонента числа с плавающей точкой слишком велик, чтобы уместиться в целевой целочислен­ ный тип, то значение будет уменьшено по модулю диапазона целевого типа. В следующей программе демонстрируется несколько преобразований ти­ пов, требующих приведений: // Демонстрация приведений . class Conversion { puЫic static void rnain ( St ring [ ] args ) { byte Ь; int i = 2 5 7 ; douЫe d = 3 2 3 . 1 4 2 ; Systern . out . printl n ( " \nПpeoбpaзoвaниe int в byte . " ) ; Ь = ( byte ) i ; Systern . out . println ( " i и Ь : " + i + " " + Ь ) ; Systern . out . println ( " \nПpeoбpaзoвaниe douЫe в int . " ) ; i = ( int ) d; Sys tern . ou t . println ( "d и i : " + d + " " + i ) ; Systern . out . prin tln ( " \nПреобразование douЫe в byte . " } ; Ь = ( byte } d; Sys tern . out . println ( " d и Ь : " + d + " " + Ь ) ; Глав а 3. Типы д а нных, переменные и масси вы 97 Вот вывод, генерируемый программой: Преобразование i nt в byt e . i и Ь : 257 1 Преобразование douЫe в int . d и i : 32 3 . 1 4 2 3 2 3 Преобра зова ние douЬle в byte . d и Ь : 3 2 3 . 1 4 2 67 Давайте обсудим каждое преобразование. Когда значение 2 5 7 приводит­ ся к типу byte, результатом будет остаток от деления 2 57 на 2 5 6 (диапазон byte), который в данном случае равен 1. Когда значение переменной d пре­ образуется в тип int, его дробная часть утрачивается. Когда значение пере­ менной d преобразуется в тип byte, его дробная часть теряется, а значение уменьшается по модулю 2 5 6, что в этом случае дает 67. Автоматическое повышен ие ти пов в выражениях Помимо присваивания есть еще одно место, где могут происходить опре­ деленные преобразования типов: выражения. Давайте выясним причину. Точность, требуемая для представления промежуточного значения в выраже­ нии, иногда превышает диапазон допустимых значений типа любого из опе­ рандов. Например, возьмем показанное ниже выражение: byte а = 4 0 ; byte Ь = 5 0 ; byte с = 1 0 0 ; int d = а * Ь / с; Результат промежуточного члена а * Ь может легко выйти за пределы ди­ апазона любого из его операндов типа byte. Чтобы решить проблему такого рода, при вычислении выражения каждый операнд типа byte, short или char автоматически повышается до i nt. Это означает, что подвыражение а * Ь вы­ числяется с применением целочисленных, а не байтовых значений. Таким об­ разом, значение 2 0 0 0, т.е. результат вычисления промежуточного выражения 50 * 4 0 , будет допустимым, несмотря на то, что а и Ь объявлены с типом byte. Каким бы полезным ни было автоматическое повышение, оно может вы­ зывать сбивающие с толку ошибки на этапе компиляции. Скажем, следующий с виду правильный код вызывает проблему: byte Ь = 5 0 ; Ь = Ь * 2 ; / / Ошибка ! Нельзя присваивать значение i n t переменной byte ! В коде предпринимается попытка сохранить 50 * 2, т.е. совершенно допу­ стимое значение byte, обратно в переменную типа byte. Но поскольку при вычислении выражения операнды автоматически повышаются до int, ре­ зультат тоже повышается до i nt. Таким образом, результат выражения те­ перь имеет тип i nt, который нельзя присвоить переменной типа byte, не ис­ пользуя приведение. Это справедливо даже в том случае, если присваиваемое значение умещается в целевой тип, как в примере выше. 98 Часть 1. Язык Java В ситуациях, когда вам понятны последствия переполнения, вы должны применять явное приведение, например, как в выражении ниже, которое вы­ дает корректное значение 1 0 0: byte Ь = 5 0 ; Ь = ( byte ) ( Ь * 2 ) ; П ра вил а повы ш ения типов В Java определено несколько правил повышения типов, которые применя­ ются к вы ражениям. Вот как они выглядят. Первым делом все значения byte, short и char повышаются до int, как только что было описано. Если один операнд имеет тип long, то все выражение повышается до lo ng. Если один операнд имеет тип f l oat, тогда все выражение повы шается до f l oat. Если какой-либо из операндов имеет тип douЫe, то результат будет иметь тип douЫe. В представленной далее программе демонстрируется повышение каждо­ го значения в выражении для соответствия типу второго операнда в каждой двоичной операции: class Promote { puЫic static void main ( S tring [ ] args ) { byte Ь char с = ' а ' ; short s = 1 02 4 ; int i = 500 0 0 ; float f = 5 . б 7 f ; douЫe d = . 1 2 3 4 ; douЫe result = ( f * Ь ) + ( i / с ) - ( d * s ) ; System . out . println ( ( f * Ь ) + " + + ( i / с ) + System . out . printl n ( " result = " + resul t ) ; 11 = 11 42 ; - 11 + (d * s) ) ; Давайте более внимательно рассмотрим повышения типов, которые про­ исходят в следующей строке программы: douЫe result = (f * Ь ) + (i / с) - (d * s ) ; В первом подвыражении, f * Ь, тип переменной Ь повышается float и ре­ зультатом подвыражения будет значение f l oat. В подвыражении i / с тип переменной с повышается до int и результатом подвыражения будет зна­ чение int. В подвыражении d * s тип переменной s повышается до douЫe и результатом подвыражения будет значение douЫ e. Наконец, принимаются во внимание три промежуточных значения: floa t, int и douЫ e. Результат сложения значений float и int имеет тип fl oat. Тип результата вычитания последнего промежуточного значения douЫ e из результирующего значения float повышается до douЫe, который и будет типом окончательного резуль­ тата выражения. Гл ава 3 . Ти п ы данны х, п е ременн ы е и ма сс и в ы 99 М ассивы Массив - это группа переменных одного типа, к которой можно обра­ щаться по общему имени. Можно создавать массивы любого типа с одним или большим количеством измерени й. Доступ к определенному элементу массива осуществляется по его индексу. Массивы предлагают удобные сред­ ства группирования связанной информации. Одномерные масси вы Одномерный массив по существу представляет собой список переменных одного типа. Чтобы создать масси в, сначала необходимо создать переменную масси ва желаемого типа. Вот общая форма объявления одномерного массива: тип [ ] имя -переменной; Здесь тип объявляет тип элементов массива, который называется также ба­ зовым типом. Тип элементов определяет тип данных каждого элемента, содер­ жащегося в массиве. Таким образом, тип элементов масси ва определяет, дан­ ные какого типа будут храниться в массиве. Например, в следующем примере объявляется массив по имени month_days с типом "массив целых чисел": int [ ] month_days ; Хотя такое объявление устанавли вает тот факт, что month_da ys являет­ ся переменной типа массива, на самом деле никакого масси ва не существует. Чтобы связать month_days с факти ческим физическим массивом целых чисел, потребуется разместить его в памяти с помощью new и назначить month_days. Как будет объясняться, new - это специальная операция, которая выделяет память. Операция new более подробно рассматривается в следующей главе, но ее нужно использовать сейчас, чтобы размещать в памяти массивы. Общая фор­ ма операции new применительно к одномерным массивам выглядит так: переменная- типа -массива = new тип [размер] ; Здесь тип указывает тип размещаемых в памяти данных, размер устанавли­ вает количество элементов в массиве, а переменная-типа-массива представ­ ляет собой переменную, связанную с массивом. То есть, чтобы использовать new для размещения массива, вы должны указать тип и количество элементов в массиве. Элементы в массиве, размещенном в памяти операцией new, будут ав­ томатически инициализированы нулем (для числовых типов), значением fa l s e (для булевского типа) или значением nu l l (для ссылочных типов, описанных в следующей главе). В показанном ниже примере размещается 1 2-элементный массив целых чисел и связывается с переменной mo nth_days: month_days = new i nt [ 1 2 ] ; После выполнения этого оператора month_days будет ссылаться на масси в из 1 2 целых ч исел. Вдобавок все элементы в массиве будут инициализирова­ ны нулем. 1 00 Часть 1 . Язык Java Имеет смысл повторить: получение массива - двухэтапный процесс. Во­ первых, вы обязаны объявить переменную нужного типа массива. Во-вторых, вы должны выделить память, в которой будет храниться массив, с примене­ нием операции new и назначить ее переменной типа массива. Таки м образ ом, в Java все массивы размещаются в памяти динами чески. Если концепция ди­ нами ческого размещения вам незнакома, не переживайте. Она будет подроб­ но описана далее в книге. После раз мещения масси ва вы можете получать доступ к определенному элементу массива, указ ывая его индекс в квадратных скобках. Индексы мас­ си вов начинаются с нуля. Например, следующий оператор присваивает значе­ ние 2 8 второму элементу массива month_days: month_days [ l ] = 2 8 ; Представленная далее строка отображает з начение, хранящееся по индек­ су 3: System . out . println ( month days [ 3 ] ) ; Собирая воедино все части, можно написать программу, которая создает массив с кол и чеством дней в каждом месяце: // Демонстрация исполь зования одномерного массива . class Array { puЫ i c static void main ( String [ ] args ) { int [ ] month_days ; month_days = new int [ 1 2 ] ; month_days [ 0 ] 31; month_days [ l ] 28; rnonth_days [ 2 ] 31; month_days [ 3 ] 30; month_days [ 4 ] 31; month_days [ 5 ] 30; month_days [ б ] 31; month_days [ 7 ] 31; month_days [ 8 ] 30; rnonth_days [ 9 ] 31; rnonth_days [ l 0 J = 3 0 ; rnonth_days [ l l ] = 3 1 ; System . out . println ( "B апреле " + month_days [ 3 ] + " дней . " ) ; В результате з апуска программа выведет коли чество дней в апреле. Как уже упоминалось, и ндексы масси вов Java начинаются с нуля, так что коли че­ ство дней в апреле хранится в элементе mon th_days [ 3 ] и составляет 3 0. Разрешено объединять объявление переменной типа массива с выделени­ ем для него памяти: int [ ] month_days = new int [ 1 2 ] ; Именно так обы чно поступают в профессионально написанных програм­ мах на Java. Гл ава 3 . Ти п ы данны х, п еременн ы е и м асс ив ы 1 01 Массивы могут инициализироваться при их объявлении. Процесс почти такой же, как при инициализации про стых типов. Инициализатор массива представляет собой список разделенных запятыми выражений, заключенный . в фигурные скобки. Запятые разделяют значения элементов массива. Массив будет автоматически создаваться достаточно большим, чтобы вместить коли­ чество элементов, указанное в инициализаторе массива. Нет нео бходимости использоват ь new. Например, для хранения количества дней в месяцах в сле­ дующем коде создается инициализированный массив целых чисел: / / Улучшенная версия предыдутей программы. class AutoArray { puЫ ic static void main ( S tring [ ] args ) { int [ ] month_days = { 3 1 , 2 8 , 3 1 , 3 0 , 3 1 , 3 0 , 31, 31, 30, 31, 30, 31 } ; System. out . println ( "B апреле " + month_days [ 3 ] + " дней . " ) ; В результате запуска программы вы увидит е такой же вывод, как и у пре­ дыдущей версии. Исполняющая среда Java с тро го проверяет, не по пытались ли вы слу­ чайно сохранить или сослаться на значения за пределами диапазона масси­ ва. Она проверит, что все индексы массива находятся в правильно м диапа­ зоне. Например, исполняющая среда проверит значение каждого индекса в month_days, чтобы удо стоверит ься, что оно находи тся внутри диапазона от О до 11 включит ельно. Если вы попытаетесь получить доступ к элементам за пределами диапазона массива (отрицательные числа или числа, превышаю­ щи е длину массива), то получи т е ошибку времени выполнения. Вот еще один пример, в котором применяется одномерный массив. В нем вычисляется среднее для множества чисел. // Вычисление среднего для массива значений . class Average { рuЫ i c static voi d main ( String [ ] args ) { douЫe [ ] nums = ( 10 . 1 , 1 1 . 2 , 1 2 . 3 , 1 3 . 4 , 1 4 . 5 ) ; douЫe result = О ; int i ; for ( i= 0 ; i < 5 ; i + + ) result = result + nums [ i ] ; System. out. println ( "Cpeднee значение : " + result / 5 ) ; Мн о го мерные массивы Многомерные массивы в Java реализованы как массивы массивов. Чтобы объявить переменную многомерного массива, указывайте каждый допол­ нительный индекс, используя еще один набор квадратных скобок. Скажем, в следующем примере объявляется переменная типа двумерного массива по имени twoD: int [ ] [ ] twoD = new int [ 4 ] [ 5 ] ; 102 Ч асть 1. Яз ы к Java Здесь размещается память для массива 4 х 5 и назначается переменной twoD. Внутренне матрица реализована в виде массива массивов значений int. Концептуально такой массив будет выглядеть, как показано на рис. 3.1. / ! ! ! \ П равый индекс определяет столбец [о] [о] [0] [1] [0] [2] [о] [з] [о] [4] Левый индекс определяет строку [1] [0] [1] [1] [1] [2] [1] [з] [1] [4] [з] [о] [з] [1] [з] [2] [з] [з] [з] [4] Дано: int [ ] [ ] twoD = new int [ 4 ] [ 5 ] ; Рис. 3.1 . Концептуальное представление двумерного массива 4 х 5 В приведенной ниже программе элемент массива нумеруются слева на­ право и сверху вниз, после чего отображаются их значения: // Демонстрация исполь зования мн о гоме р но г о массива . class TwoDArray { puЫ i c s t a t i c void main ( St ring [ J a rg s ) { int [ ] [ ] twoD= new i nt [ 4 ] [ 5 ] ; int i , j , k = О ; fo r ( i = O ; i < 4 ; i ++ ) for ( j =O ; j <5 ; j + + ) { twoD ( i ] [ j ) = k ; k+ + ; for ( i =O ; i <4 ; i++ ) { for ( j = O ; j < 5 ; j + + ) Sys t em . out . print ( twoD [ i ] [ j ] + " " ) ; System . out . print ln ( ) ; Программа генерирует следующий вывод: О 1 2 3 4 5 6 7 8 9 10 1 1 12 13 1 4 15 16 17 1 8 1 9 Гла ва 3. Типы данны х, переменн ы е и массивы 1 03 При размещении многомерного массива необходимо указывать память только для первого (самого левого) измерения. Остальные измерения мож­ но размещать по отдельности. Например, в показанном далее коде выде­ ляется память для первого измерения массива twoD, когда он объявляется. Выделение памяти для второго измерения делается отдельно. int [ ] [ ] twoD ( O ] twoD [ l ] twoD [ 2 ] twoD [ З ] twoD = new i nt [ 4 ] [ ] ; = new int [ 5 ] ; = new int [ 5 ] ; = new int [ 5 ] ; = new int [ 5 ] ; Хотя в такой ситуации индивидуальное размещение массивов второго из­ мерения не дает каких-то преимуществ, в других случаях такие преимущества могут быть. Скажем, при выделении памяти под измерения по отдельности вам не нужно размещать одинаковое количество элементов для каждого из­ мерения. Как утверждалось ранее, поскольку многомерные массивы на самом деле являются массивами массивов, длина каждого массива находится под вашим контролем. Например, в следующей программе создается двумерный массив, в котором размеры массивов во втором измерении не равны: / / Ручное размеще ние массивов разных размеров во втором изменении . class TwoDAgain { puЫ i c static void rna in ( S tring [ ] a rgs ) { int [ ] [ ] twoD = new int [ 4 ] [ ] ; twoD [ O ] = new in t [ l ] ; twoD [ l ] = new in t [ 2 ] ; twoD [ 2 ] = new int [ З ] ; twoD [ З ] = new in t [ 4 ] ; int i , j , k = О ; for ( i=O ; i< 4 ; i + + ) for ( j =O ; j <i + l ; j + + ) twoD [ i ] [ j ] = k ; k++ ; for ( i =O ; i < 4 ; i + + ) { for ( j =O ; j <i + l ; j + + ) Systern . out . print ( twoD [ i ] [ j ] + " " ) ; Systern . out . p rintln ( ) ; Программа генерирует показанный ниже вывод: о 1 2 3 4 5 6 7 8 9 Созданный программой массив схематически изображен на рис. 3.2. Часть 1 . Яз ы к Java 1 04 [о] [о] [1][0] [1] [1] [2][0] [2][1] [2] [2] [з] [о] [з] [1] [з] [2] [з] [з] 1 Рис. 3.2. Двумерный массив, в котором размеры массивов во втором измерении не равны Ст упенчатые (или нерегулярные) многомерные массивы могут оказаться неподходящими для м ногих приложений, потому что они противоречат тому, что люди ожидают найти при встрече с многомерным массивом. Однако в не­ которых сит уациях нереrу лярные массивы можно эффективно использовать. Скажем, если вам нужен очень большой разреженный двумерный массив (т.е. такой, rде з адействованы не все элементы), тогда нерегулярный массив мо­ жет стать идеальным решением. Многомерные массивы можно инициализ ировать. Для этого просто за­ ключите инициализ атор каждого измерения в собственный набор фигурных скобок. В следующей программе создается матрица, в которой каждый эле­ мент содержит произ ведение индексов строки и столбца. Так же обрат ите внимание, что внутри инициализаторов массивов можно применять выраже­ ния и литеральные значения. / / Инициализаци я двумерно г о массива . cla s s Matrix [ puЫ i c static voi d ma in ( String [ J args ) douЫe [ ] [ ] m = { [ { { { О*О, 0*1, 0*2, 0*3, 1*0, 1*1, 1*2, 1*3, 2*0, 2*1, 2*2, 2*3, 3* 0 3*1 3*2 3*3 }, }, }, } }; int i , j ; for ( i= O ; i < 4 ; i + + ) { for ( j =O ; j <4 ; j + + ) System. out . print ( m [ i ] [ j ] + " " ) ; System . out . print l n ( ) ; Запустив программу, вы получ ите такой вывод: о.о о.о о.о о.о о.о 1 . 0 2.0 3. 0 о.о 2.0 4.0 6.0 о.о 3.0 6.0 9.0 Гл а в а 3 . Ти п ы д а нн ы х, пе р еменные и м асси вы 1 05 Как видите, все строки в массиве инициализируются в соответствии со списками инициализации. Давайте рассмотрим еще один пример использования многомерного мас­ сива. В показанной ниже программе создается трехмерный массив 3 х 4 х 5, за­ тем в каждый элемент массива помещается произведение его индексов, после чего результирующие произведения отображаются. // Демонс траци я исполь з ования трехмерно го м асси в а . class ThreeDMatrix { puЫ i c static void ma i n ( St ring [ ] a rgs ) int [ ] [ ] [ ] threeD = new i n t [ З J [ 4 ] [ 5 ] ; int i , j , k ; for ( i=O ; i < З ; i + + ) for ( j =O ; j <4 ; j + + ) for ( k= O ; k<5 ; k++ ) threeD [ i ] [ j ] [ k ] = i * j * k ; for ( i=O ; i<З ; i + + ) { for ( j =O ; j <4 ; j ++ ) ( for ( k= O ; k< 5 ; k++ ) System . out . print ( t hreeD [ i ] [ j ] [ k] + " " ) ; S y s t em . out . p r i n t l n ( ) ; Sys tem . out . println ( ) ; Вот какой вывод генерирует программа: о о о о о о о о о о о о о о о о о о о о о о о о о о о l 2 3 4 2 4 6 8 О 3 6 9 12 о ооо о О 2 4 6 8 4 8 12 16 О 6 12 1 8 2 4 о А л ьтерна т ивны й си нтаксис объявления массивов Существует вторая форма объявления массива: тип имя - переменной [ ] ; Здесь квадратные скобки расположены после имени переменной массива, а не после спецификатора типа. Например, следующие два объявления экви­ валентны: int al [ ] = new i nt [ 3 ] ; int [ ] а2 = new i nt [ 3 ] ; 1 Об Часть 1. Язык Java Представленные далее объявления тоже эквивалентны: cha r twodl [ ] [ ] = new char [ 3 ] [ 4 ] ; char [ ] [ ] twod2 = new char [ З J [ 4 ] ; Эта альтернативная форма объявления обеспечивает удобство при преоб­ разовании кода из С/С++ в Java. Кроме того, она позволяет объявлять в од­ ном операторе объявления и переменные с типами массивов, и переменные с типами, отличающимися от массивов. В настоящее время альтернативная форма используется реже, но знать ее по-прежнему важно, поскольку в Java разрешены обе формы объявления массивов. З наком с тво с вывед ением типов локальн ы х пе р еменны х Не так давно в язык Java было добавлено новое функциональное средство, называемое выведением типов локальных переменных. Для начала давайте вспомним о двух важных аспектах, касающихся переменных. Во-первых, все переменные в Java должны быть объявлены до их использования. Во-вторых, переменная может быть инициализирована значением при ее объявлении. Кроме того, когда переменная инициализируется, тип инициализатора дол­ жен быть таким же, как объявленный тип переменной (или допускать преоб­ разование в него). Таким образом, в принципе нет нужды указывать явный тип для инициализируемой переменной, потому что он может быть выведен по типу ее инициализатора. Конечно, в прошлом такое выведение не поддер­ живалось, и все переменные требовали явно объявленного типа вне зависи­ мости от того, инициализировались они или нет. На сегодняшний день ситу­ ация изменилась. Начиная с JDK 10, можно позволить компилятору выводить тип локаль­ ной переменной на основе типа ее инициализатора, избегая явного указания типа. Выведение типов локальных переменных предлагает несколько преиму­ ществ. Например, оно может упростить код, устраняя необходимость в избы­ точном указании типа переменной, когда тип может быть выведен из ее ини­ циализатора. Выведение типов локальных переменных способно упрощать объявления в случаях, когда имя типа имеет довольно большую длину, как у некоторых имен классов. Оно также может быть полезно, когда тип трудно различить или его нельзя обозначить. (Примером типа, который не может быть обозначен, является тип анонимного класса, обсуждаемый в главе 25.) Более того, выведение типов локальных переменных стало обычной частью современной программной среды. Его включение в Java помогает поддержи­ вать язык в актуальном состоянии с учетом меняющихся тенденций в проек­ тировании языков. Для поддержки выведения типов локальных переменных было добавлено контекстно-чувствительное ключевое слово va r. Чтобы задействовать выведение типов локальных переменных, перемен­ ная должна быть объявлена с ключевым словом var в качестве имени типа и включать инициализатор. Гла ва 3 . Типы данны х, переменные и массивы 1 07 Например, вот как вы объявляли бы локальную двойную переменную по имени avg, инициализ ируемую з начением 1 0 . О, в прошлом: douЫe avg = 1 0 . 0 ; С применением выведения типа объявление переменной avg теперь мож­ но записать так: var avg = :о . о ; В обоих случаях переменная avg будет иметь тип douЫe. В первом случае ее тип указывается явно, а во втором случае выводится как douЫe, т.к. ини­ циализатор 1 0 . О имеет тип douЫe. Как уже упоминалось, ключевое слово var является зависимым от контек­ ста. Когда var используется в качестве имени типа в контексте объявления локальной переменной, оно сообщает компилятору о том, что тип объявляе­ мой переменной должен выводиться на основе типа инициализатора. Таким образом, в объявлении локальной переменной ключевое слово var служит з аполнителем фактически выведенного типа. Тем не менее, когда ключевое слово var применяется в большинстве других мест, оно будет просто опре­ деляемым польз ователем идентификатором без особого смысла. Например, следующее объявление по-прежнему допустимо: int var = l ; / / В этом случае var - просто определяемый / / поль зователем идентифика тор . В данном случае тип явно указ ан как int, а var представляет собой имя объявляемой переменной. Несмотря на з ависимость от контекста, есть не­ сколько мест, где ключевое слово var использовать не разрешено. Скажем, его нельзя применять в качестве имени класса. Предшествующие обсуждения воплощены в следующей программе: / / Простая демонстрация выведения типов ло каль ных переменных . class VarDemo { puЬl i c static vo id mai n ( S tring [ ] a rgs ) { / / Исполь зовать выведение типов для определения типа переменной / / по имени avg . В этом случае выводится тип douЫ e . var avg = 1 0 . 0 ; System . out . println ( " Знaчeниe avg : " + avg ) ; / / В следующем контексте var - не предопределенный идентифи катор , / / а про сто определяемое поль зователем имя переменной . int var = 1 ; System . out . p r i n tl n ( " Знaчeниe va r : " + va r ) ; / / Ин тересно отметить , что в следующем фра гмен те кода var исполь зуется // и как тип объявления , и как имя переменной в инициализаторе . var k = -va r ; System . out . println ( " Знaчeниe k : " + k ) ; Ниже показ ан вывод программы: Значение avg : 1 0 . 0 Значение va r : 1 Значение k : - 1 1 08 Ч аст ь 1. Язык Java В предыдущем примере клю чевое слово var использовалось для объявле­ ния только простых переменных, но va r можно применять также для объяв­ ления массивов. Например: va r myArray = new int [ l O ] ; / / Допустимый код . Обратите внимание, что ни va r, ни myA rray не и меют скобок. Взамен предполагается, что тип myA rray выводится в int [ ] . Кроме того, использо­ вать квадратные скобки в левой части объявления va r нельзя. Таким обра­ зом, оба следующих объявления ошибо чны: var [ ] myArray = new int [ l O ] ; / / Ошибка ! var myArray [ ] = new int [ l O ] ; / / Ошибка ! В первой строке предпринимается попытка снабдить квадратными скобка­ ми клю чевое слово var, а во второй - переменную myA rray. В обоих случаях применять квадратные скобки некорректно, т.к. тип выводится из типа ини­ циализ атора. Важно подчеркнуть, что var может использоваться для объявления пере­ менной только тогда, когда эта переменная инициализ ирована. Например, показанный далее оператор ошибо чен: var counte r ; / / Ошибка ! Требуется инициализ атор . Также помните о том, что клю чевое слово va r можно применять только для объявления локальных переменных. Его нельзя использовать, например, при объявлении переменных экземпляра, параметров или возвращаемых типов. В то время как предыдущее обсуждение и примеры позволили вам оз на­ коми ться с основами выведения типов локальных переменных, они не проде­ монстрировали его полную мощь. Как вы увидите в главе 7, выведение типов локальных переменных особенно эффективно для сокращения объявлений, содержащих длинные имена классов. Его так же можно задействовать при ра­ боте с обобщенными типами (см. главу 14), оператором try с ресурсами (см. главу 1 3) и циклом fo r (см. главу 5). Некоторые огран ичения var В дополнение к ограни чениям, которые упоми нались в предыдущем об­ суждении, с применением va r связ ано несколько других ограни чений. Можно объявлять только одну переменную за раз, для переменной нельзя использо­ вать n u l l в качестве и нициализ атора и объявляемая переменная не может присутствовать в выражении инициализатора. Хотя с применением va r мож­ но объявить тип массива, клю чевое слово va r нельзя использовать с инициа­ лиз атором массива. Например, следующий оператор допусти м: var myArray = new int [ 1 0 ] ; / / Допустим но показанный ниже оператор - нет: / / Ошибоче н va r myArray = { 1 , 2 , 3 } ; Как отмечалось ранее, клю чевое слово va r не разрешено применять для имени класса. Его также не допускается использовать в качестве имени дру- Глава 3. Тип ы данн ых, пе ременные и массивы 1 09 гих ссыло чных т ипов, включая инт ерфейс, перечисление или аннотацию, либо в качестве имени параметра обобщенного типа, что рассматривается да­ лее в книге. Существуют еще два ограничения, которые относятся к функци­ ональным средствам Java, описанным в последующих главах, но упомянут ым здесь для полноты картины. Выведение типов локальных переменных нель­ зя применя ть для объявления типа исключения, перехваченного оператором catch. Кроме того, ни лямбда-выражения, ни ссылки на методы не разрешено использовать в качестве инициализаторов. На заметку! На момент написания книги некоторые читатели будут иметь дело со средами Java, не поддерживающими выведение типов локальных переменных. Чтобы все читатели книги могли компилировать и запускать как можно больше примеров кода, в большинстве программ в оставшейся части этого издания книги выведение типов локальных переменных применяться не будет. Использование полного синтаксиса объявления также позволяет сразу понять, переменная какого типа создается, что важно для самого примера кода. Разумеется, со временем вы должны обдумать применение в своем коде выведения типов локальных переменных. Н ес кол ько сло в о ст ро ка х Как вы могли заметить, в предыдущем обсуждении типов данных и мас­ сивов не упом инались строки или строковый тип данных. Причина вовсе не в том, что язык Java не поддерживает тако й тип - он поддерживает. Просто ст роковый тип Java, называемый String, не является примитивным типом, равно как и обычным массиво м символов. Наоборот, тип Str ing определяет объект и по тому его полное описание требует понимания нескольких харак­ т ерист ик, связанных с объектом. В итоге он будет рассматриват ься в этой книге позже, после описания объектов. Однако чтобы вы могли использоват ь прост ые строки в примерах програ мм, необходимо следующее краткое вве­ дение. Тип String предназначен для объявления строковых переменных. Можно также объявлять массивы строк. Строково й переменной может быть присво­ ена строковая константа в кавычках. Переменная типа S t ring может быт ь присвоена другой переменной типа String. Объект типа Str ing можно при­ менят ь в качестве аргум ента функции println ( ) . Например, взгляни те на следующий фрагмент кода: St ring str = " Тест о вая стро ка " ; System . ou t . println ( s t r ) ; Здесь str - это объект типа String. Ему присваивается строка "Тестовая строка ", ко торая и отображается оператором println ( ) . Позже вы увидите, что объекты String обладают многими особенностями и характерист ика ми, которые делают их довольно мощными и легкими в ис­ пользовании. Тем не менее, в следующих нескольких главах вы будете при­ меня ть их только в самой просто й форме. ГЛ А В А \,:. : j� ' -·· r l rr:i.: �; �:t Опера ци и Язык Java предоставляет богатую среду операций. Большинство операций можно разделить на четыре группы: арифметические операции, побитовые операции, операции отношения и логические операции. В Java также опре­ делены дополнительные операции, обрабатывающие определенные особые ситуации. В этой главе описаны все операции Java кроме операции сравнения типов instanceof, исследуемой в главе 13, и операции стрелки (->), описан­ ной в главе 15. Арифметические операции Арифметические операции используются в математических выражениях аналогично тому, как они применяются в алгебре, и перечислены в табл. 4.1. Табnица 4.1 . Арифметические операции языка Java + Сложение (также унарный плюс) Вычитание (также унарный минус) * Умножение / Деление % ++ Деление по модулю += -= *= /= %= Инкремент Сложение с присваиванием Вычитание с присваиванием Умножение с присваиванием Деление с присваиванием Деление по модулю с присваиванием Декремент Глава 4. О перации 111 Операнды арифметических операций дол жны и меть ч исловой тип. Арифметические операции нельзя использовать с типом boolea n, но можно с типом cha r, поскольку в Java тип cha r по существу является подмножеством типа int. Основ н ые арифметические операции Основные арифметич е ские операции - сложение, вычитани е , умножение и дел е ние - ведут с е бя так, как и сл е довало ожидать для всех числовых ти­ пов. Унарный минус инв е ртиру е т свой е динстве нный опе ранд. Унарны й плюс просто возвраща е т значение своего операнда. Помните, что когда операция деления прим еняется к целочисленному ти пу, дробная часть к результату н е присоединяется. В сл едующей простой программе д е монстрируется работа арифм е тиче­ ских опе раций. В ней такж е ил л юстрируется отличие между д ел е ни е м с пла­ вающей точкой и целочисл е нным делением. // Демонстрация основных арифме тиче ских операций . class Bas i cMath { puЫ i c static vo id main ( String [ ] args ) { / / Арифме тиче ские операции со значе ниями int . System . out . println ( " Цeлoчиcлeннaя арифме тика " ) ; int а = 1 + 1 ; int Ь = а * 3 ; int с = Ь / 4 ; int d = с - а ; int е = -d; System . out . println ( " a " + а) ; System . out . println ( "b " + Ь) ; System . out . println ( " c = " + с ) ; Sys tem . out . println ( " d = " + d ) ; System . out . println ( " e = " + е ) ; / / Арифме тические операции со значе ниями douЫ e . System . out . printl n ( " \ nApифмe тикa с плавающей точкой " ) ; douЫe da = 1 + l ; douЫ e db = da * 3 ; douЫe d c = db / 4 ; douЫ e dd = dc - а ; douЫ e de = -dd; System . out . println ( " da = " + da ) ; System . out . println ( " db = " + db ) ; System . out . println ( " dc = " + dc) ; System . out . println ( " dd = " + dd ) ; System . out . println ( "de = " + de ) ; З апустив программу, вы получите показ анный ниже вывод: Целочисленная арифме тика а = 2 Ь = 6 112 Час ть 1 . Язык Java с = 1 d = -1 е = 1 Арифметика с плавающей точкой da = 2 . 0 dЬ = б . О dc = 1 . 5 dd = - 0 . 5 de = О . 5 Операци я делени я по модул ю Операция деления по модулю, %, возвращает остаток от деления. Ее мож­ но применять к типам с плавающей то чкой, а также к целочисленным типам. В следующей программе демонстрируется использование операции %: // Демонстрация работы операции % . class Modulus { puЫic static void main ( String [ ] arg s ) { int x = 4 2 ; douЫe у = 4 2 . 25 ; System . out . println ( " х по модулю 1 0 = " + х % 1 0 ) ; System . out . println ( " y по модулю 1 0 = " + у % 1 0 ) ; В результате запуска программы вы получите такой вывод: х по модулю 1 0 = 2 у по модулю 1 0 = 2 . 2 5 С оставн ые арифметические операции присва и ва н и я Язык Java предоставляет специал ьные операци и, объединяющие арифме­ ти ческие операции с присваи ванием. Вероятно, вам известно, что в програм­ мах довольно часто встре чаются операторы следующего вида: а = а + 4; В Java показанный оператор можно переписать следующим образом: а += 4 ; В этой версии оператора применяется составная опера�ия присваивания +=. Оба оператора выполняют одно и то же действие: увеличивают значение а на 4. Вот еще один пример: а = а % 2; который можно выразить так: а %= 2 ; В данном случае операция % = получает остаток от деления а / 2 и помещает результат обратно в а. Гла ва 4. О пера ц ии 113 Составные операции присваивания предусмотрены для всех арифметиче­ ских бинарных операций. Таким образом, любой оператор вида: переме нная = переменная опера ция выражение ; может быть переписан следующим образом: переменная оп ера ция= в ыражение ; Составные операции присваивания обеспе чивают два преим ущества . Во-первых , они уменьшают объем набира емоrо кода , потому что являются "сокращенной формой" для своих длинных эквивалентов. Во-вторых, в не­ которых случаях они более эфф ективны, чем их длинные эквиваленты. По указанным причинам составные операции присваивания часто используются в профессионально написанных програ ммах на Java. Н иже приведен пример программы, в которой демонстрируются в дей­ ствии несколько составных операций присваивания: / / Демонстра ция ряда сос т авных опера ций присваива ния . class OpEquals { puЫic static void main ( String [ ] args ) [ int а = 1 ; int Ь = 2 ; int с = 3 ; а += 5 ; Ь *= 4 ; с += а * Ь ; с %= 6; System . out . println ( " a " + а) ; System . out . println ( " b = " + Ь ) ; S y stem . out . println ( " c = " + с ) ; Вот вывод программы: а = 6 ь=в с = 3 О п ера ц ии ин кремен та и декремен та Операции инкрем ента и декремента в Java обозначаются с помощью ++ и --. Они были представлены в главе 2, а здесь будут обсуждаться более под­ робно. Как вы увидите, они обладают рядом особых характеристик, которые делают их довольно интересными. Начнем с рассмотр ения того, что им енно делают операции инкрем ента и декремента. Операция инкрем ента увеличивает на единицу значение своего операн­ да, а операция декремента уменьшает на единицу значение своего операнда. Например, следующий оператор: х = х + 1; можно переписать с целью применения операции инкремента: 1 14 Часть 1 . Язык Java х+ + ; Аналогично представленный ниже оператор: х = х - 1; эквивалентен такому: х- - ; Приведенные операции уникальны тем, что могут встречаться и в пост­ фиксно й форме, когда они следуют за операндом, как было только что показа­ но, и в префиксной форме, когда они предшествуют операнду. В предшествую­ щих примерах никаких отличий между префиксной и постфиксной формами не было. Однако когда операторы инкремента и/или декремента входят в со­ став более крупного выражения, то между этими двумя формами проявляет­ ся тонкая, но важная разница. В префиксной форме операнд инкрементиру­ ется или декрементируется перед получением значения для использования в выражении. В постфиксной форме предыдущее значение извлекается для применения в выражении, после чего модифицируется операнд. Например: х = 42; у = + +х ; В этом случае значение переменной у устанавливается в 4 3, как и следо­ вало ожидать, поскольку инкремент происходит до того, как х присваивается у. Таким образом, строка кода у = + + х ; будет эквивалентом следующих двух операторов: х = х + 1; у = х; Тем не менее, при такой записи: х = 42; у = х+ + ; значение х получается до выполнения оператора приращения , а потому зна­ чение у равно 42. Разумеется, в обоих случаях х устанавливается в 4 3 . Строка у = х++ ; является эквивалентом двух операторов: у = х; х = х + 1; В показанной ниже программе демонстрируется работа операции инкре­ мент а. / / Демонстрация работы ++ . class IncDec { puЫi c stat ic void main ( String [ ] args ) { int а = 1 ; int b = 2 ; int с ; int d; с = ++Ь ; d = а++; с+ + ; Гпава 4. Операции System. out . println ( "a System. out . println ( "b System. out . println ( " c System. out .println ( "d = = = = " " " " + + + + 115 а) ; Ь) ; с) ; d) ; В результате запуска программы вы получите следующий вывод: а= 2 Ь=З с = 4 d = 1 П об итов ы е опе рац ии В Java определено несколько побитовых операций, которые можно применять к целочисленным типам: long, int, short, char и byte. Такие операции воз­ действуют на отдельные биты своих операндов. Они перечислены в табл. 4.2. Табnица 4.2. Побитовые операции языка Java Побитовое унарное НЕ & л >> >>> << &= != >>= >>>= <<= Побитовое И Побитовое ИЛИ Побитовое исключающее ИЛИ Сдвиr вправо Сдвиr вправо с заполнением нулями Сдвиr влево Побитовое И с присваиванием Побитовое ИЛИ с присваиванием Побитовое исключающее ИЛИ с присваиванием Сдвиr вправо с присваиванием Сдвиr вправо с заполнением нулями и присваиванием Сдвиr влево с присваиванием Поскольку побитовые операции манипулируют битами внутри целоrо числа, важно понимать, какое влияние такие манипуляции моrут оказать на значение. В частности, полезно знать, как в Java хранятся целочисленные зна­ чения и каким образом представляются отрицательные числа. Итак, прежде чем продолжить, давайте кратко обсудим эти две темы. Все целочисленные типы представлены двоичными числами различной разрядности. Например, значение 42 типа byte в двоичном формате выrля- 116 Ч а сть 1. Язык Java дит как 001010 10, где каждая позиция представляет собой степень двойки, начиная с 20 в крайнем правом бите. Следующей битовой позицией слева бу­ дет 2 1 , или 2, далее влево будет 22, или 4, затем 8, 16, 32 и т.д. Таким образом, 42 имеет биты 1, установленные в позициях 1, 3 и 5 (считая от О справа); соот­ ветственно 42 - это сумма 21 + 23 + 25, что составляет 2 + 8 + 32. Все целочисленные типы (кроме char) являются целыми числами со зна­ ком, т.е. они могут пр едставлять как положительные, так и отрицательные значения. В Java используется кодировка, известная как доnоАнение до двух или доnоАнumеАьный код, которая пр едусматривает представление отрицательных чисел путем инвертирования (замены единиц на нули и наоборот) всех би­ тов в значении и последующего добавления единицы к результату. Например, для представления -42 инвертируются все биты в 42, или 00 1010 10, что дает 1 1010101, после чего к результату добавляется 1, давая в итоге 1 10101 10, или -42. Чтобы декодировать отрицательное число, необходимо инвертировать все биты и добавить 1. Например, -42, или 1 10101 10, в результате инвертиро­ вания дает 00101001, или 41, а после добавления 1 получается 42. Причину, по которой в Java (и большинстве других языков программиро­ вания) применяется дополнение до двух , легко понять, если рассмотреть про­ блему перехода через ноАь. Предполагая работу со знач ением типа byte , ноль представляется как 00000000. В дополнении до единицы простое инвертиро­ вание всех битов дает 1 1 1 1 11 1 1 , что создает отрицательный ноль. Проблема в том, что в целочисленной математике отрицательный ноль недопустим. Эта проблема решается за счет использования дополнения до двух для представ­ ления отрицательных значений. Когда при меняется дополнение до двух , к дополнению добавляется 1, что дает 100000000. Единичный бит оказывается слишком далеко слева и не умещается в значение byte, что приводит к же­ лаемому поведению, где -О совпадает с О, а 1 1 1 1 1 1 1 1 является кодом для - 1 . Хотя в пр едыдущем примере м ы использовали значение byte, тот же прин­ цип применим ко всем целочисленным типам Java. Из-за того, что в Java для хранения отрицательных чисел используется дополнение до двух и т.к. все целые ч исла представляют собой значения со знаком, применение побитовых операций может легко дать неожиданные ре­ зультаты. Например, включение старшего бита приведет к тому, что резуль­ тирующее значение будет интерпретироваться как отрицательное число, вхо­ дило это в ваши намерения или нет. Во избежание неприятных сюрпризов просто помните, что старший бит определяет знак целого числа независимо от того, как он был установлен. П обитовые ло г и ч еские опера ц и и Существуют четыре побитовых логических операции: &, 1 , " и ~. Результаты их выполнения приведены в табл. 4.3. В последующем обсужде нии не забы­ вайте о том, что побитовые операции применяются к каждому индивидуаль­ ному биту внутри каждого операнда. Глава 4. Операции 117 Таблица 4.3. Результаты выполнения побитовых nогических операций о 1 о 1 о о о 1 1 о о о 1 1 1 1 о 1 1 о 1 1 о о П о б итовое НЕ Унарная операция НЕ, ~, также называемая побитовым дополнением, инвер­ тирует все биты своего операнда. Например, число 42, имеющее следующую битовую комбинацию: 00101010 после применения операции Н Е превращается в: 11010101 Поб итовое И Операция И, &, выдает единичный бит, если биты в обоих операндах также равны 1. В остальных случаях выдается нулевой бит. Вот пример: 00101010 & 00 0 0 1 1 1 1 42 15 00001010 10 Поб и товое ИЛИ Операция ИЛИ, 1 , объединяет биты так, что если любой из битов в операндах равен 1, то результирующий бит будет единичным, как показано ниже: 00101010 00001111 42 15 00101111 47 Поб и товое и с кл юч а ющее ИЛИ Операция исключающего ИЛИ (XOR), л, объединяет биты таким образом, что если бит в одном операнде равен 1, то результирующий бит будет еди­ ничным. В противном случае результирующий бит будет нулевым. В пред­ ставленном далее примере иллюстрируется действие операции л. В этом при­ мере также демонстрируется полезное характерное свойство операции XOR. Обратите внимание, как битовая комбинация 42 инвертируется везде, где второй операнд имеет единичный бит. Там, где второй операнд имеет нуле­ вой бит, бит первого операнда не изменяется. Вы найдете такое свойство по­ лезным при выполнении некоторых типов битовых манипуляций. Часть 1 . Язык Java 118 л 00101010 0000 1 1 1 1 00100101 42 15 37 Использование побитовых логических операций Работа побитовых логических операций демонстрируется в следующей программе: / / Демонстрация работы побито вых логиче ских операций . class Bi tLogic { puЫ ic stati c vo i d main ( S tring [ ] a rgs ) { String [ ] binary = { " 0 0 0 0 " , " 0 0 0 1 " , " 0 0 1 0 " , " 0 0 1 1 " , " 0 1 0 0 " , " 0 1 0 1 " , "0 1 1 0 " , "0 1 1 1 " , " 1000 " , "1001 " , " 1 0 1 0 " , " 1 0 1 1 " , " 1 1 0 0 " , " 1 1 0 1 " , " 1 1 1 0 " , " 1 1 1 1 " ); int а = 3 ; / ! О + 2 + 1 или 0 0 1 1 int Ь = 6 ; / / 4 + 2 + О или 0 1 1 0 Ь; int c = a int d = а & Ь ; int е = а л Ь; i n t f = ( ~ а & Ь ) 1 ( а & ~Ь) ; int g = ~а & 0x0 f ; а = System . out . p rintln ( " Ь = Sys tem . out . p rintl n ( " alb = System . out . print l n ( " а &Ь = System . out . p rint l n ( " а ль = Sys tem . out . print l n ( " System . out . p rint ln ( " ~ a & b l a &~b = ~а = System . out . print l n ( " в двоичной форме в двоичной форме " " " " " " " + + + + + + + Ь i na ry [ a ] ) ; Ьinary [ b ] ) ; binary [ c ] ) ; Ьinary ( d ] ) ; binary [ e ] ) ; Ьinary [ f ] ) ; binary [ g ] ) ; В этом примере а и Ь имеют битовые комбинации, которые представля­ ют все четыре возможности для двух двоичных цифр: 0-0, 0- 1, 1-0 и 1-1. Результаты в переменных с и d позволяют видеть работу с каждым битом операций \ и &. Значения, присвоенные е и f, одинаковы и иллюстрируют работу операции л. Массив строк по имени Ьinary содержит удобочитаемое двоичное представление чисел от О до 1 5. Массив в примере индексируется так, чтобы показать двоичное представление каждого результата. Массив по­ строен таким образом, что корректное строковое представление двоичного значения n хранится в Ьinary [ n ] . Значение ~ а объединяется посредством операции И с O x O f (O<IO0 l l l 1 в двоичной форме) для его уменьшения до ме­ нее чем 16, чтобы его можно было вывести с использованием массива Ьinary. Вот вывод программы: а = 00 1 1 Ь = 0110 a i b = 0lll а&Ь = 0010 а ль = 0 1 0 1 ~а&Ы а & ~Ь = 0 1 0 1 ~ а = 1 1 00 Гла ва 4 . Операции 119 Сдвиг вл ево Операция сдвига влево, <<, сдвигает все биты значения влево на указанное количество позиций. Она имеет следующую общую форму: зна чение << число Здесь число устанавливает количество позиций для сдвига влево значения. То есть операция << перемещает все биты в указанном значении влево на количество битовых позиций, заданное в числе. При каждом сдвиге влево старший бит смещается (и утрачивается), а справа вставляется ноль. Это оз­ начает, что когда к операнду i nt применяется сдви г влево, биты теряются, как только они сдвигаются за битовую позицию 3 1 . В случае операнда типа l ong биты утрачиваются после битовой позиции 63. При сдвиге значений byte и short автоматическое повышение типов в Java п риводит к неожиданным результатам. Как вы знаете, во время вычисле­ ния выражения значения byte и short повышаются до int. Кроме того, ре­ зультат такого выражения тоже имеет тип i nt. Это означает, что результатом сдвига влево значения byte или short будет значение int, а биты, сдвинутые влево, не будут утрачены до тех пор, пока они не сместятся за битовую пози­ цию 3 1 . Вдобавок отрицательное значение byte или short при повышении до int будет расширено знаком. Таким образом, старшие биты будут заполнены единицами. По указанным п ричинам выполнение сдвига влево значения byte или short подразумевает необходимость отбрасывания старших байтов из результата типа int. Например, при сдвиге влево значения byte оно сначала повышается до int, после чего сдвигается. Если нужно получить результат сдвинутого значения byte, то придется отбросить три старших байта резуль­ тата. Самый п ростой способ решить задачу - преобразовать результат об­ ратно в byte. Концепция демонстрируется в показанной далее программе: // Сдвиг влево значения byte . class ByteShi f t { puЫ i c s tatic voi d main ( S tring [ ] args ) { byte а = 6 4 , Ь ; int i ; i = а << 2; Ь = ( byte ) ( а « 2 ) ; System . out . println ( " Пepвoнaчaльнoe значение а : " + а ) ; System . out . println ( " i и Ь : " + i + " " + Ь ) ; Программа генерирует следующий вывод: Первоначаль ное значение а : 6 4 i и Ь: 256 О Поскольку а повышается до i nt для целей вычисления, сдвиг влево зна­ чения 64 (0100 0000) дважды приводит к тому, что i содержит значение 256 ( 1 0000 0000). Однако значение в Ь содержит О, т.к. после сдвига младший байт теперь равен нулю. Единственный единичный бит был сдвинут за его пределы. 1 20 Часть 1. Яз ы к Java Из -за того, что каждый сдвиг влево удваивает исходное з начение, про­ граммисты часто используют данный факт как эффект ивную альтернат иву умножению на 2. Но вам нужно быть начеку. Если вы сдвинете единичный бит в позицию старшего разряда (бит 31 или 63), то значение станет отрица­ тельным, что иллюстрируется в следующей программе: / / Сдвиг влево как быстрый способ умножения на 2 . class MultByTwo { puЬlic static void main ( String [ ] args } { int i ; int num = 0xFFFFFFE ; for ( i = 0 ; i < 4 ; i + + ) { num = num << 1 ; System . out . println ( num) ; Вот вывод, который генерирует программа: 536870908 1 07 3 74 1 8 1 6 2 1 4 7 4 8 3 632 -32 Начальное значение было тщательно выбрано таким образом, чтобы после сдвига влево на 4 поз и ции бита получилось -32. Как видите, когда единичный бит сдвигается в поз и цию 31, число интерпретируется как отрицательное. Сдвиг вправо Операция сдвига вправо, >>, сдвигает все биты значения вправо на у каз ан­ ное количество поз и ц ий. Она имеет следующую общую форму: зна чение > > число Здесь чи сло устанавливает кол ичество поз и ц и й для сдвига вправо значения. То есть операция >> перемещает все биты в указ анном значении вправо на количество битовых поз иций, з аданное в числе. В показанном н иже фрагменте кода з начение 32 сдвигается вправо на две поз иции, приводя к тому, что а устанавливается в 8: int а = 3 2 ; а = а >> 2 ; / / а теперь содержит 8 Когда биты в з начении смещаются з а его пределы, то они утрачиваются. Например, в следующем фрагменте кода з начение 3 5 сдвигается на две по­ з иции вправо, что приводит к потере двух младших битов, в результате чего а снова получает з начение 8: int а = 3 5 ; а = а >> 2 ; / / а содержит 8 Вз глянув на ту же операцию в двоичной форме, становится ясно, как это происходит: Глава 4. Операции 00100011 >> 2 0000 1 0 0 0 35 1111 1000 >> 1 1 1 1 1 1 100 -8 1 21 8 Каждый раз, когда выполняется сдвиг значения вправо, оно делится на 2 с отбрасыванием любого остатка. В некоторых случаях данным фактом можно воспользоваться для высокопроизводительного целочисленного деления на 2. При выполнении сдвига вправо старшие (крайние слева) биты, открытые сдвигом, заполняются предыдущим содержимым старшего бита. Это называ­ ется расширением знака и служит для сохранения знака отрицательных чисел при их сдвиге вправо. Например, -8 >> 1 равно -4, что в двоичном виде вы­ глядит так: -4 Интересно отметить, что в случае сдвига вправо значения -1 результат всегда остается -1, т.к. расширение знака будет приводить к большему коли­ честву единиц в старших битах. Иногда расширять знаки значений при сдвиге вправо нежелательно. Скажем, в приведенной далее программе значение byte преобразуется в шестнадцатеричное строковое представление. Обратите внимание, что сдви­ нутое значение маскируется с помощью операции И с 0x0f для отбрасывания любых битов, дополненных знаком, чтобы значение можно было использо­ вать в качестве индекса в массиве шестнадцатеричных символов. / / Ма с кир ование ра сшир ения з нака . class HexByte { static puЬ l i c void rna in ( St ring [ ] args ) { char [ ] hex = { 1 0', '1 ', '2', ' 3' , '4', '5', '6', '7' , 1 8 ' , ' 9' , 'а' , 'Ь', 'с' , 'd' , 'е', 'f' }; byte Ь = (byte) Oxfl ; Systern. out . p rint l n ( "b = О х" + hex [ ( b >> 4 ) & O x O f ] + hex [b & OxO f] ) ; Вот вывод программы: Ь = Oxfl Беззн ак овы й сдв и г в пра во Как только что объяснялось, операция >> автоматически заполняет стар­ ший бит его предыдущим содержимым каждый раз, когда происходит сдвиг, сохраняя знак значения. Тем не менее, иногда это нежелательно. Например, при выполнении сдвига чего-то, что не представляет собой числовое зна­ чение, расширение знака может оказаться ненужным. Такая ситуация часто 1 22 Ч ас ть 1. Яз ы к Java встречается при работе с пиксельными значениями и графикой. В ситуациях подобного рода обычно требуется помещать в старший бит ноль вне зависи­ мости от того, каким было его начальное значение. Прием известен как без­ знаковый сдвиг и предусматривает применение операции беззнакового сдвига вправо, >>>, которая всегда задвигает нули в старший бит. В следующем фрагменте кода демонстрируется работа операции >>>. Здесь значение а устанавливается в - 1, что приводит к установке всех 32 бит в 1. Затем значение сдвигается вправо на 24 позиции с заполнением старших 24 бит нулями и игнорированием расширения знака. В итоге а устанавлива­ ется в 2 5 5. int a = - 1 ; а = а >>> 2 4 ; Ниже показана та же самая операция в двоичной форме с целью дополнительной иллюстрации того, что происходит: 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 - 1 в двоичном виде »>2 4 0 0 0 0 0 0 0 0 0 0 0 00000 00000000 1 1 1 1 1 1 1 1 255 в двоичном виде Операция >>> часто не настолько полезна, как хотелось бы, поскольку она имеет смысл только для 32- и 64-битных значений. Вспомните, что при вычислении выражений меньшие значения автоматически повышаются до i nt. В итоге происходит расширение знака, и сдвиг будет выполняться над 32-битным, а не 8- или 16-битным значением. То есть можно ожидать, что беззнаковый сдвиг вправо значения byte заполнит нулями, начиная с бита 7. Но это не так, поскольку на самом деле сдвигается 32-битное значение. Эффект демонстрируется в следующей программе: // Безэнаковый сдвиг вправо значения типа byte . class ByteUShi ft { static puЫ i c void ma in ( St ring [ ] a r g s ) { cha r [ ] hex = { ' 0 ' , ' 1 ' , ' 2 ' , '3 ' , 4 ' , '5 ' , ' 6 ' , ' 7 ' , ' В ' , ' 9 ' , ' а ' , 'Ь ' , ' с ' , ' d' , ' е ' , ' f ' }; byte Ь = ( byte ) Ox f l ; byte с = ( byte ) ( Ь » 4 ) ; byte d = ( byte ) (Ь »> 4 ) ; byte е = ( byte ) ( ( Ь & Oxff ) » 4 ) ; System . out . p rintln ( " Ь = Ох" + hex [ ( Ь » 4 ) & OxO f] + hex [ b & OxO f ] ) ; System . out . println ( " Ь >> 4 = Ох" + hex [ ( с » 4 ) & OxOf] + hex [ c & OxO f ] ) ; System . out . println ( " Ь >>> 4 = Ох" + hex [ ( d » 4 ) & OxOf] + hex [ d & OxOf] ) ; System. out . println ( " ( Ь & Oxff) >> 4 = Ох" + hex [ ( е » 4 ) & OxOf] + hex [e & OxO f ] ) ; 1 Глава 4. О перации 1 23 В показанном н и же выводе программы видно, что при работе с байтами операция >>> ничего не дел ает. Для данной демонстрации переменной Ь при­ сваивается произвольное отрицательное значение типа byte. Затем пере­ менной с присваивается значение Ь типа byte, сд винутое вправо на четы­ ре позиции, которое равно Oxf f из-за ожидаемого расширения знака. Далее переменной d присваивается значение Ь типа byte с беззнаковым сдвигом вправо на четыре позиции, которым вопреки ожидаемому OxOf будет Oxff по причине расширения знака, происшедшего при повышении Ь до int пе­ ред сдвигом. Последнее выражение устанавливает переменную е в значение Ь типа byt e, маскированное до 8 бит с помощью операции И, после чего сдви нутое вправо на четыре позиции, что дает о жи даемое значение OxOf. Обратите внимание, что операция беззнакового сдвига вправо не исполь­ зовался для переменной d, т.к. состояние знакового бита после операции И было известно. Ь = Oxfl Ь >> 4 = O x f f Oxff Ь >>> 4 ( Ь & Ox f f ) » 4 = OxO f С оста вные по би товые опера ц и и пр исв а и ва н и я Все бинарные побитовые операции имеют составную форму, аналогичную форме алгебраических операций, которая сочетает в себе присваивание с по­ битовой операцией. Например, следующие два оператора, сдвигающие значе­ ние вправо на четыре позиции, эквивалентны: а = а >> 4 ; а >>= 4 ; Аналогично два приведенных н и же оператора, присваивающие переменной а результат побитового выражения а ИЛИ Ь, тоже эквивалентны: а = а I Ь; а 1 = Ь; В показанной далее программе создается несколько целочисленных пере­ менных, с которыми затем осуществляются манипуляции с применен ием со­ ставных побитовых операций присваивания: c l a s s OpBi tEquals { puЬl i c static void rna i n ( String [ ] a rgs ) { int а = 1 ; int Ь = 2 ; int с = 3 ; а 1= 4; Ь >>= 1 ; с < <= 1 ; а л= с ; S ystern. out . println ( " а " + а) ; Systern . ou t . println ( "b = " + Ь ) ; S ystern . out . pr i n t l n ( " с = " + с ) ; 1 24 Часть 1. Язык Java Вот вывод программы: а =З Ь= 1 с= 6 Операции отношения Опера�ии отношения устанавливают соотношение одного операнда с дру­ гим. В частности, они определяют равенство и порядок. Операции отношения описаны в табл. 4.4. Табnица 4.4. Операции отношения языка Java Равно != Не равно > Больше < Меньше >= <= Больше или равно Меньше или равно Результатом этих операций является значение boolean. Операции отноше­ ния чаще всего используются в выражениях, управляющих оператором i f и различными операторами циклов. В Java значения любых типов, включая целые числа, числа с плавающей точкой, символы и булевские значения, можно сравнивать с помощью про­ верки на предмет равенства == и неравенства ! =. Обратите внимание, что в Java равенство обозначается двумя знаками "равно� а не одним. (Помните: один знак "равно" ....- это операция присваивания.) С помощью операций определения порядка можно сравнивать только числовые rипы, т.е. для выяс­ нения, какой больше или меньше другого, допускается сравнивать лишь цело­ численные операнды, операнды с плавающей точкой и символьные операнды. Как уже упоминалось, результатом операции отношения будет значение boolean. Например, следующий фрагмент кода совершенно корректен: int а = 4 ; int Ь = 1 ; boolean с = а < Ь; В данном случае в переменной с сохраняется результат выполнения а<Ь (равный false). Если ранее вы работали с С/С++, то обратите внимание, что в программах на С/С++ весьма распространены следующие виды операторов: int done; /1 . . . Глава 4. Опе ра ц ии i f ( ! done ) • . . i f ( done ) • . . 1 25 / / Допустимо в С/ С++ , / / но не в Java . В коде Java такие операторы должны быть записаны так, как показано ниже: if ( done ='-' О ) , , . / / Стиль Java . i f ( done ! = О ) • • • Причина в том, что истинное и ложное значения в Java определены не так, как в С/С++. В языках С и С++ истинным является любое ненулевое значе­ ние, а ложным - ноль. В Java t rue и f a l s e представляют собой нечисло­ вые значения, которые никак не связаны с нулем или ненулевым значением. Следовательно, для проверки на предмет равенства или неравенства нулю должна явно применяться одна или несколько операций отношения. Булевские логические опера ц и и Булевские логические операции, перечисленные в табл. 4.5, работают толь­ ко с операндами типа boolean. Все бинарные логические операции объеди­ няют два значения boolean, чтобы сформировать результирующее значение boolean. Таблица 4.5. Буnевские nоrические операции языка Java & Логическое И Логическое ИЛИ ,., Логическое исключающее ИЛИ 11 Короткозамкнутое ИЛИ && Короткозамкнутое И Логическое унарное НЕ &= Логическое И с присваиванием 1= Логическое ИЛИ с присваиванием "'= Логическое исключающее ИЛИ с присваиванием Равно != Не равно ?: Тернарная операция "если-то-иначе" Булевские логические операции &, 1 и " действуют на значениях типа boolean точно так же, как они работают с битами целого числа. Логическая опе­ рация ! инвертирует булевское значение: ! true == false и ! fal se == t rue. В табл. 4.6 показано действие каждой булевской логической операции. 1 26 Часть 1. Язык Java Табпмца 4.6. Резуnыаты выnоnнения буnевскмх nоrическмх операций 1 false true false true false false true true false true true true false false false true false true true false true false true false Ниже приведена программа, которая почти совпадает с представленным ранее примером Bi tLogic, но работает с логическими значениями типа boolean, а не с двоичными битами: // демонс'l'рация рабо'l'ы буле всюа логических о пераций . claзs BoolLoqic { puЫic зtatic void rnain (Strinq [ ] arqз ) { boolean а = true; boolean Ь = falзe ; boolean с = а I Ь; boolean d = а & Ь; boolean е = а л Ь ; boolean f = ( ! а & Ь ) (а & ! Ь ) ; boolean q = ! а; Syзtem . out . println ( " а = + а) ; Syзtem . out . println ( " Ь = + Ь) ; Syзtern. out . println ( " a l b = + с) ; Syзtern. out . println ( " а&Ь = + d ) ; а"Ь = + е ) ; Syзtem . out . println ( " Syзtem . out . println ( " ! a&b l a& ! b = + f ) ; Syзtern . out . println ( " ! а = + q) ; } После запуска программы вы увидите, что к значениям Ьoolean применя­ ются те же самые логические правила, что и к битам. Как видно в следующем выводе, строковое представление значения boolean является одним из лите­ ральных значений true или false: а = true Ь = falзe a l b = true а&Ь = falзe а ль = true ! а&Ь l а& ! Ь = true ! а = falзe Короткозамкнутые л о г и чес ки е о п е рац ии В языке Java померживаются две интересные булевские операции, от­ сутствующие в ряде других языков программирования. Речь идет о вспо­ могательных версиях булевских операций И и ИЛИ, широко известные как короткозамкнутые логические операции. В табл. 4.6 легко заметить, что ре- Глава 4 . Опера ц ии 1 27 зультатом операц ии ИЛИ будет t rue, когда А имеет значение t rue вне зави­ симости от значения В. Анало гично операция И дает результат fal se, когда А равно fal se безотносительно к тому, какое з начение имеет В. В случае при­ менения форм 1 1 и & & вместо форм I и & указанных операций Java правый операнд не будет вычисляться в ситуации, когда результат выражения может быть определен только левым операндом. Это очень полезно, когда правиль­ ное функц ионирование предусматривает зависимость правого операнда от левого. В приведенном ниже фрагменте кода показано, как можно исполь­ зовать в своих интересах короткозамкнутое логическое вычисление, чтобы убедиться в допустимости операции деления до ее выполнения: i f ( denom 1 = О && num / denom > 1 0 ) Поскольку применяется короткозамкнутая форма операции И ( & &), нет риска вызвать исключение во время выполнения, ко гда переменная denom равна нулю. Если бы данная строка кода была написана с использованием од носимвольной версии операци и &, то вычислялись бы обе стороны выра­ жения, приводя к исключению во время выполнения, когда значение denom равно нулю. Стандартная практика предусматривает применение короткозамкнутых форм И и ИЛИ в случаях, связанных с булевской логикой, оставляя одно­ си мвольные версии исключительно для побитовых операци й. Однако из это­ го правила есть исключения. Например, взгляните на оператор следующего вида: i f ( c== l & е++ < 1 0 0 ) d = 1 0 0 ; Здесь использование версии & гарантирует, что операция инкремента бу­ дет применена к е независимо от того, и меет переменная с значение 1 или нет. На заметку! В формальной спецификации Java короткозамкнутые операции называются условным и и условным или. О пера ц и я присваивания Операция присваивания использовалась, начи ная с главы 2. Теперь при­ шло время взглянуть на нее формально. Операция присваивания обозначается одиночным знаком равенства (=) и работает в Java почти так же, как в любом другом языке программирования. Она имеет следующи й общий вид: переменная = выражение; Тип переменной должен быть совмести мым с типом выражения. Операция присваивания обладает одной интересной особенностью, с которой вы, возможно, не знакомы: она позволяет создавать цепочку присваи ­ ваний. Например, взгляните на показанный ниже фрагмент кода: int х , у, z ; х = у = z = 100 ; / / установить х , у и z в 1 0 0 1 28 Часть 1. Язык Java Здесь переменные х, у и z устанавливаются в 1 О О с помощью единственно­ го оператора. Прием работает из-з а того, что операция = возвращает з начение правого выражения. Таким образом, z = 1 0 0 дает значение 1 0 0, которое з атем присваивается переменной у, а результат в свою очередь присваивается х. Применение "цепочки присваивания" - простой способ присвоить группе переменных общее з начение. О пера ц и я ? В Java предусмотрена специальная тернарная операция, которая способна з аменить определенные виды операторов "есл и-то-иначе': Она обозначается с помощью знака ?. Поначалу операция ? может показаться слегка сбиваю­ щей с толку, но после освоения ее можно использовать с высокой эффект ив­ ностью. Вот общий вид операции ? : выражение] ? выражение2 : выражениеЗ Здесь выражение] может быть л юбым выражением, результатом которо­ го является значение boolean. Есл и результатом выражения] является true, тогда вычисляется выражение2, а иначе выражениеЗ. Результатом операции ? будет результат вычисленного выражения. Типы результатов выражения2 и выражениеЗ дол жны быть одинаковыми (или совместимыми) и не могут быть void. Ниже показан пример применения операции ?: ratio = denom == О ? О : num / denom ; При вычислении этого выражения присваивания сначала просматривается выражение слева от вопросительного знака. Если значение denom равно нулю, тогда вычи сляется выражение между вопросительным з наком и двоеточием и используется в качестве значения всего выражения ? . Если з начение denom не равно нулю, то вычисляется выражение после двоеточия и становится з на­ чением всего выражения ?. Затем результат операции ? присваивается пере­ менной ratio. В приведенной далее программе демонстрируется работа операции ? , ко­ торая применяется для получения абсолютной величины переменной: / / Демонстрация работы операции ? . class Ternary { puЫ i c s ta t i c void ma i n ( St r ing [ ] args ) { int i , k ; i = 10; i ; / / получить абсолютную величину i k = i < О ? -i Sys tem . out . print ( "Aбcoлютнaя величина " ) ; Sys tem . out . println ( i + " равна " + k ) ; i = -10; k = i < О ? - i : i ; / / получить абсолютную величину i System . out . print ( "Aбcoлютнaя величина " ) ; System . out . println ( i + " равна " + k ) ; Гnава 4. Операц1111 1 29 Вот вывод, сгенерированный: программой: Абсолют ная величина 10 равна 10 Абсолютная величина -10 равна 10 Ста рш и нство о пе рац и й В табл. 4.7 показан порядок старшинства операций Java от самого высоко­ го до самого низкого. Операции в одной и той же строке имеют одинаковый приоритет. В бинарных операциях принят порядок вычисления слева направо (за исключением присваивания, которое вычисляется справа налево). Хотя формально [ ] , ( ) и . являются разделителями, они также могут действовать как операции и в таком качестве обладать наивысшим приоритетом. Кроме того, обратите внимание на операцию стрелки (->), которая используется в лямбда-выражениях. Табnица 4,7, Старшинство операций в Java ++ (постфиксная) н (префиксная) * -- (постфиксная) -- (префиксная) & л (унарная) / % >>> >= << < + >> > + <= (унарная) (приведение- типа) instanceof != && 1 1 ?: -> = операция= •,.,. ', �= - u , . ; -' . 1� .} .,. , - _r-. .:,,- � · - ..., ьit-' �1 ,,,,. f <;, -;�, f;- //1� :;,)'J_�_: 1 30 Ча сть 1. Язык Java И с пол ьз ован ие кру гл ы х с ко б ок Круглые скобки повышают приори тет операций внутри них, ч т о часто тре­ буется для получения желаемого результ ата. Например, рассмотрим следую­ щее выражение: а >> Ь + 3 В показанном выражении сначала к значению Ь добавляется 3, после чего производится сдвиг а вправо на этот результат. То есть выражение можно переписать с применением избыточных круглых скобок: а » (Ь + 3) Тем не менее, если сначала необходимо сдвинуть а вправо на Ь позиций и зат ем добавить к результату значение 3, тогда часть выражения понадобит ся заключить в скобки: ( а » Ь) + 3 В дополнение к изменению обычного приоритета оператора, круглые скобки временами могут использоваться с целью прояснения смысла выра­ жения. Для любого, кто читает ваш код, сложное выражение может оказать­ ся трудным в понимании. Добавление избыточных, но уточняющих скобок к сложным выражениям может помочь предот вратить путаницу в дальнейшем. Например, какое из следующих выражений легче читать? а 1 4 + с >> Ь & 7 (а 1 ( ( ( 4 + с ) » Ь ) & 7 ) ) И еще один момент: круглые скобки (избыточные или не т) не ухудшают производительность программы. Таким образом, добавление круглых скобок для уменьшения неоднозначности не повлияе т отрицат ельно на вашу про­ грамму. ГЛ А ВА -;l' fi�rr-;- i .'./·J,,i): Уп ра вля ющие оп е раторы Управляющие операторы используются в языке программирования для того, чтобы заставить поток выполнения продвигаться вперед и переходить в зависимости от изменений в состоянии программы. Управляющие опера­ торы программы на Java можно разделить на следующие категории: выбор, итерация и переход. Операторы выбора позволяют программе выбирать раз­ личные пути выполнения на основе результата выражения или состояния переменной. Операторы итерации позволяют потоку выполнения повторять один или несколько операторов (т.е. операторы итерации образуют циклы). Операторы перехода позволяют программе выполняться нелинейным обра­ зом. Здесь рассматриваются все управляющие операторы Java. Операторы в ы б ора Java В Java померживаются два оператора выбора: i f и switch. Они предо­ ставляют возможность управления потоком выполнения программы на ос­ нове условий, известных только во время выполнения. Вы будете приятно удивлены мощью и гибкостью этих двух операторов. Оператор i:f Оператор i f был представлен в главе 2, а ниже он рассматривается более подробно. Оператор i f является оператором условного перехода Java. Ero можно применять для направления потока выполнения программы по двум разным путям. Вот ero общая форма: if (условие) onepa'l'Opl ; else onepa'l'Op2 ; Здесь как операторl, так и оператор2 может быть одиночным оператором или составным оператором, заключенным в фигурные скобки (т.е. блоком). В качестве условия может использоваться любое выражение, которое воз­ вращает значение типа Ьoolean. Конструкция else необязательна. Оператор if работает следующим образом: если условие истинно, тог­ да выполняется операторl. В противном случае выполняется оператор2 1 32 Ч а сть 1. Язык Java (есл и он присутствует). Ни в коем случае не будут выполнены оба оператора. Наприм ер, взгляните на приведенный дал ее код: int а , Ь; // . . . if (a < Ь) а = О; e l se Ь = О ; Если значение а меньше знач ения Ь, то а устанавливается в ноль, а в про­ тивном случае Ь устанавливается в ноль. Но никогда обе переменные не будут установлены в ноль. Чаще всего выражение, применяемое для управл ения i f, будет вклю­ чать операторы отношения. Однако это не является формально необходи­ мым. Управлять оператором i f можно с помощью одной пере менной типа boo lean, как показано в следующем фрагменте кода: boolean dataAva i l aЬle ; // . . . i f ( dataAvailaЫe ) Process Data ( ) ; else wa itForMoreData ( ) ; Помните, что непосредственно после if или else может находиться толь­ ко один оператор. Если вы хотите включить больш е оп ераторов, тогда пона­ добится создать блок, как в приведенном дал ее фрагм енте кода: int bytesAvailaЫ e ; // . . . i f ( byte sAvai laЫe > О ) Proce s s Data ( ) ; bytesAva ilaЫe -= n ; else wa itForMoreData ( ) ; Оба внутри блока i f будут выпол няться, если значение перем енной bytesAvailaЫe бол ьше нуля. Некоторым программистам удобно испол ьзовать фигурные скобки в опе­ раторе i f, даже есл и в каждой конструкции имеется только один оператор. Такой прием позволяет легко добавлять еще один оп ератор позже, и не при­ дется беспокоиться о том, что вы забудете о фигурных скобках. На самом деле частая причина ошибок связана с тем, что забывают определить блок, когда он нужен. Например, рассмотрим следующий фрагмент кода: i nt bytesAvai laЬl e ; // . . . i f ( bytesAvai laЬle > О ) Pro cess Data ( ) ; bytesAva i l aЫe -= n ; else waitForMoreData ( ) ; bytesAvai laЬle = n ; Гла в а S. Уп ра вл яющ ие оп ера то ры 1 33 Кажется вполне очевидны м , что из-за уровня отступа оператор bytesAvai laЫe = n; предназначался для выполнения внутри конструкции e l se. Тем не менее, как вы помните, пробелы в Java несущественны, и компи­ лятор не может узнать, что именно было задумано. Код скомпилируется без ошибок, но при запуске он будет вести себя некорректно. Вот как исправить предыдущий пример: int bytesAvailaЫ e ; // . . . i f ( bytesAva i laЬle > О ) Proces sData ( ) ; bytesAvailaЫe -= n ; ) else { waitForMoreData ( ) ; bytesAvail aЫe = n ; Вложенные операторы i f Вложенный оператор i f представляет собой оператор i f, который является целью дpyroro оператора i f или e l se. Вложенные операторы i f очень распро­ странены в программировании. При вложении i f главное помнить, что опера­ тор e l se всегда ссылается на ближайший оператор i f, который находится в том же блоке, что и e l s e, и еще не связан с оператором e l s e, например: i f ( i == 1 0 ) { if (j < 20) а = Ь; i f ( k > 1 0 0 ) с = d ; / / этот оператор i f ассоциирован / / с этим else else а = с ; else а = d ; / / этот оператор else относится к i f ( i == 1 0 ) Как видно в комментариях, финальный оператор e l s e не связан с i f ( j < 2 О ) , поскольку он не находится в том же блоке (несмотря на то, что он - ближайший i f без e l s e) . Взамен финальный оператор e l s e связан с i f ( i == 1 0 ) . Внутренний оператор else относится к i f ( k > 1 0 0 ) , потому что он - ближайший i f в том же блоке. Цenoчкa if-else-if Распространенной программной конструкцией, основанной на последова­ тельности вложенных операторов i f, является цепочка i f-e lse-if, которая выглядит следующим образом: i f ( услов ие) опера тор; else i f ( условие) опера тор ; else i f ( условие) опера тор; else опера тор; 1 34 Часть 1 . Язык J ava Операторы i f выполняются сверху вниз. Как только одно из условий, управляющих i f, становится истинным, выполняется оператор, связанный с этим if, и остальная часть цепочки игнорируется. Если ни одно из условий не выполняется, тогда будет выполнен последний оператор e l se. Финальный оператор else действует как условие по умолчанию; т.е. если все другие ус­ ловные проверки не про йдены, то выполняется последний оператор e l s e. Если финального оператора e l se нет и все остальные условия ложны, то гда никакие действия не предприни маются. Ниже приведена программа, в которой цепочка i f-e lse- i f применяется для определения, к како му времени года относится специфический месяц: // Демонстрация операторов i f-else- i f . class I fElse { puЫ i c static void main ( String [ ] args ) int month = 4 ; / / апрель St ring season ; i f (month == 1 2 1 1 month == 1 1 1 month == 2 ) season = "зима 11 else i f (month == 3 1 1 month == 4 1 1 month == 5 ) season = "весна " ; else i f (month == 6 [ i month == 7 1 1 month == 8 ) season = "лето " ; else i f (month == 9 1 1 month = = 1 0 [ 1 month = = 11 ) season "осень " ; else season "Несуществующий месяц " ; System . out . println ( "Aпpeль - это " + season + " . " ) ; / / Принадлежность апреля ко времени года ; Вот вывод, генерируемый программой: Апрель - это весна . Возможно, вы захотите поэкспериментироват ь с это й программой, прежде чем дви гаться дальше. Как вы обнаружите, независимо от того, како е значе­ ние вы присвои те месяцу, будет выполнен один и только один оператор при­ сваивания в цепочке. Тради ц и о нны й операто р swi tch Оператор swi t ch в Java обеспечивает переход по множеству путей. Он предлагает простой способ направления потока выполнения на разные части кода в зависимости от значения выражения. Таким образом, он часто оказы­ вае тся лучшей альтернативо й большому набору операторов i f-else- i f. Первым делом необходимо от метить, что начиная с JDK 14, о ператор swi t ch был значи т ельно улучшен и расширен рядом новых возможностей, выходящих далеко за рамки его традиционной формы. Традиционная фор­ ма swi t ch была частью Java с самого начала и потому используется весьма Гла ва 5 . Упра в ля ющие о п ератор ы 135 широко. Кроме того, эта форма будет работать во всех средах разработки Java для всех читателей. Из-за существенного характера последних улучше­ ний switch они описаны в главе 17 в контексте других недавних допол нений языка Java . Здесь рассматривается традиционная форма оператора swi tch, которая имеет следующий вид: switch ( выражение) { case зна чение l : / / последователь ность операторов brea k ; case зна чение2 : / / последователь ность операторов bre a k ; c a s e зна чениеN : / / последовательность операторов brea k ; de fault : / / стандартная последовательность операторов В версиях Java, предшествующих JDK 7, выражение должно давать значение типа byte, short, int, char или перечисления. (Перечисления описаны в гла­ ве 12.) В настоящее время выражение также может иметь тип Str ing. Каждое значение, указанное в операторах case, должно быть уникальным констант­ ным выражением (например, литеральным значением). Дубл ирование з наче­ ний в операторах case не разрешено. Тип каждого значения должен быть со­ вместимым с типом выражения. Вот к а к функ ц и о н и рует тра д и ц ионный оператор s w i tch: з начение выражения сравнивается с каждым значением в операторах case. Если со­ впадение найдено, то выполняется кодовая последовательность, следующая за оператором case. Если ни одна из констант не соответствует значению выражения, тогда выполняется оператор default. Од нако оператор default необязателен. Если ни оди н из операторов case не дает совпадения, а опера­ тор defaul t отсутствует, то дальнейшие действия не предпринимаются. Оператор brea k при меняется в нутри swi tch для завершения последова­ тельности операторов. Когда встречается оператор brea k, поток выполнения переходит к первой строке кода, следующей за полным оператором swi tch, обеспечивая эффект "выпрыгивания" из switch. Ниже показан простой пример использования оператора swi tch: / / Простой пример применения опера т ора switch . class SampleSwitch { puЫi c static void mai n ( St ring [ ] arg s ) { for ( int i=O ; i < б ; i + + ) switch ( i ) { case О : System . out . println ( "i равно нулю . " ) ; brea k ; 1 36 Часть 1. Язык Java case 1 : System . out . println ( " i break; case 2 : System . out . println ( " i brea k ; case 3 : System . out . println ( " i brea k ; default : System . out . println ( " i равно одному . " ) ; равно двум . " ) ; равно трем . " ) ; больше трех . " ) ; Программа генерирует следующий вывод: i i i i i i равно нулю . равно одному . равно двум . равно трем . больше трех . больше трех . Как видите, каждый раз в цикле выполняются операторы, связ анные с константой в case, которая совпадает со з начением i, а все остальные пропу­ скаются. После того, как з начение i становится больше 3, операторы case не дают совпадения, поэтому выпол няется оператор default. Оператор break необяз ателен. Если опустить brea k, то выпол нение продолжится в следую­ щем case. Иногда желательно иметь несколько case без операторов brea k между ними. Например, рассмотрим приведенную далее программу: / / В операторе switch операторы break необязатель ны . class Mi ssingBreak { puЫ i c s tatic vo id main ( S tring [ ] a rg s ) { for ( int i= 0 ; i < l 2 ; i++ ) switch ( i ) { case О : case 1 : case 2 : case 3 : case 4 : System . out . println ( " i меньше 5 . " ) ; bre a k ; case 5 : case 6 : case 7 : case В : case 9 : System . out . println ( " i меньше 1 0 . " ) ; brea k ; default : System . out . println ( " i больше или равно 1 0 . " ) ; Глава 5. Улравляющие операторы 1 37 Програм ма генерирует такой вывод: i i i i i i i i i i i i меньше меньше меньше ме ньше меньше меньше меньше меньше меньше меньше больше больше 5. 5. 5. 5. 5. 10 . 10. 10 . 10. 10 . или равно 1 0 . или равно 1 0 . Легко замет ить, что поток выполнения проходит через все операторы case, пока не доберется до оператора case (или до конца swi tch). Хотя предыдущий пример, конечно же, придуман в целях иллюстраци и, пропуск оператора brea k имеет много практических при менений в реальных программах. Ниже демонстрируется его более реалистичное использование в переработанной программе определения принадлежности месяца к временам года. Теперь в нем применяется оператор swi tch для обеспечения более эф­ фективной реализации. / / Усовершен ствованная версия программы, определяющей принадлежность / / месяца к времени года . class Switch { puЫic static vo id main ( S tring [ ] a rgs ) { int month = 4 ; / / апрель String season ; switch ( mont h ) case 12 : ca se 1 : case 2 : season " зима " ; brea k ; case 3 : case 4 : case 5 : season " весна " ; brea k ; case 6 : case 7 : case 8 : season "ле т о " ; brea k ; case 9 : case 1 0 : case 1 1 : season "о сень н ; brea k ; de fault : season = "несуществующий месяц " ; 1 38 Ч а сть 1. Язык Java Systern . out . println ( "Anpeль - это " + s eason + " . " ) ; Как уже упоминалось, для управления оператором swi tch можно также использовать строку: // Применение строки для управления оператором switch . class St ringSwi tch { puЫic static void rna in ( S t ring [ ] args ) { String str = "two " ; swi tch ( st r ) { case "one " : S ystern . out . println ( "oдин " ) ; break; case " two " : S ystern . out . printl n ( "двa " ) ; break; case "three " : Systern . out . println ( "тpи " ) ; break; defau l t : Systern . out . println { " coвnaдeний нет " ) ; brea k ; Вывод, сгенерированный программой, вполне ожидаем: два Строка, содержащаяся в str ( " two " в рассматриваемой программе), про­ веряется на предмет соответствия константам в case. Когда совпадение най­ дено (во втором операторе cas e), выполняется кодовая последовательность, связанная с данным case. Возможность применения строк в операторе swi tch упрощает код во мно­ гих ситуациях. Скажем, использовать swi tch на основе строк эффективнее по сравнению с эквивалентной последовательностью операторов i f / е l s е. Тем не менее, выполнение оператора switch по строкам может оказаться бо­ лее затратным, чем по целым числам. Таким образом, лучше всего применять switch на основе строк только в тех случаях, когда управляющие данные уже представлены в строковой форме. Другими словами, не используйте строки в swi tch без настоятельной необходимости. Вложенные операторы swi tch Вы можете применять swi tch как часть последовательности операторов внешнего switch. Он называется вложенным оператором swi tch. Поскольку оператор swi tch определяет собственный блок, между константами case во внутреннем swi tch и во внешнем s w i tch конфликты не возникают. Например, следующий фрагмент кода совершенно допустим: Гл а ва 5 . Уп равляю щие опе р ато р ы 1 39 switch ( couпt ) { case 1 : / / вложенный switch switch ( ta rget ) case О : System . out . priпtlп ( " t a rget равно нулю" ) ; break ; / / никаких конфликтов с внешним switch case 1 : System . out . priпtlп ( "target равно одному " ) ; brea k ; brea k ; case 2 : // Здесь оператор case 1 : в о внутреннем switch н е конфл иктует с опера­ тором case 1 : во внешнем switch. Переменная count сравнивается только со списком значений в операторах case на внешнем уровне. Есл и значение count равно 1, то target сравнивается со значениями в операторах case вну­ треннего списка. Подводя итоги, важно отметить три особенности оператора switch. • Оператор switch отличается от i f тем, что он может проверять только на предмет равенства, тогда как оператор i f способен оценивать логи­ ческое выражение л юбого вида. То есть switch и щет только совпадение значения выражения с одной из констант в операторах case. • Н икакие две константы case в одном sw i tch не могут и меть одина­ ковые значения. Разумеется, один оператор switch и вклю чающий его внешний swi tch могут иметь общие константы case. • Оператор switch обычно более эффективен, чем набор вложенных опе­ раторов i f. Последний пункт особенно интересен, потому что он дает представление о том, как работает компилятор Java. При компиляции оператора switch ком­ пилятор Java проверит каждую константу case и создаст "таблицу переходов'; которую будет использовать для выбора пути выполнения в зависимости от з начения выражения. Следовательно, есл и вам нужно дел ать выбор среди большой группы значений, то оператор swi tch будет работать намного бы­ стрее, чем эквивалентная логика, реал изованная с применением последова­ тельности i f-e lse. Компилятор способен добиться этого, т.к. ему известно, что все константы case имеют один и тот же тип и просто должны сравни­ ваться на равенство с выражением switch. Что касается подобного з нания дли нного списка выражений i f, то компилятор им не располагает. Помните! Недавно возможности и характеристики оператора s w i t ch были существенно расширены по сравнению с только что описанной традиционной формой s w i t с h. Усовершенствованный оператор swi tch рассматривается в главе 1 7. 1 40 Часть 1. Яз ы к Java Операторы итера ц ии В Java доступны операторы итерации for, whi le и do-whi le. Они создают то, что мы обычно называем циклами. Как вы наверняка знаете, цикл много­ кратно выполняет один и тот же набор инструкций, пока не будет удовлетво­ рено условие завершения. Вы увидите, что язык Java предлагает циклы, под­ ходящие для любых нужд программирования. Цикл while Цикл while - самый фундаментальный оператор итерации в Java. Он по­ вторяет выполнение оператора или блока до тех пор, пока истинно управля­ ющее выражение. Вот его общая форма: while ( услов ие) { / / тело цикла Здесь условием может быть любое булевское выражение. Тело цикла будет выполняться до тех пор, пока условное выражение истинно. Когда условие становится ложным, управление переходит на строку кода, непосредственно следующую за циклом. Фигурные скобки не обязательны, если повторяться должен только один оператор. Ниже показан цикл whi le, который ведет обратный отсчет от 10, выводя ровно десять строк "импульсов": // Демонстрация работы цикла whi le . class Whi le { puЬl ic static void main ( String [ ] args ) int n = 1 0 ; whi le ( n > О ) ( Syst em . out . p r i ntln ( " Импуль с номер " + n ) ; n-- ; Вот результат запуска программы: Импуль с Импуль с Импульс Им;-�уль с Импульс Импуль с Им;-�уль с Импуль с Импуль с Импульс номер номер номер номер номер номер номер номер номер номер 10 9 8 7 6 5 4 3 2 1 Поскольку условное выражение цикла whi le вычисляется в начале цикла, тело цикла не выполнится ни разу, если условие ложно изначально. Например, в следующем фрагменте кода вызов println ( ) никогда не выполняется: Гл ава 5 . У правляю щи е опе ра то ры 1 41 int а = 1 0 , Ь = 2 0 ; whi l e ( а > Ь ) System . out . println ( " Этo никогда не отобразится" ) ; Тело цикл а whi le (или л ю бого дpyroro цикл а Java) может быть пустым. Дело в том, что в Java синтаксически допустим пустой оператор (состоящий только из точки с запятой) . Взгляните на следующую программу: // Тело цикла может быть пустым . class NoBody { puЫic static void main ( String [ ] args ) { int i , j ; i = 100; j = 200; / / найти среднюю точку между i и j whi l e ( + + i < - - j ) ; / / тело в цикле отсутствует System . out . println ( "Cpeдняя точка равна " + i ) ; П рограмм а находит среднюю точку между i и j . Она генерирует следую­ щий вывод: Средняя точка равна 1 5 0 Рассмотрим, как работает этот цикл while. Значение i инкрементируется, а значение j декрементируется. Затем значения i и j сравниваются друг с другом. Если новое з начение i все еще меньше нового значения j , тогда цикл повторяется. Если значение i больше или равно з начения j , то цикл останав­ л ивается. После выхода из цикла переменная i будет хранить значение, кото­ рое находится посередине между исходными з начениями i и j . (Конечно, та­ кая процедура работает только тогда, когда i меньше j .) Понятно, что здесь нет необходимости иметь тело цикла; все действие происходит внутри само­ го условного выражения. В профессионал ьно написанном коде на Java корот­ кие циклы часто кодируются без тел, коrда управляющее выражение способ­ но самостоятельно обрабатывать все детали. Цикл do-while Как вы только что видели, если условное выражение, управляющее циклом while, изначально ложно, то тело цикла вообще не будет выполнено. Однако иногда тело цикла жел ател ьно выполнить хотя бы один раз, даже если изна­ чально условное выражение ложно. Другими словами, бывают сл учаи, когда вы хотели бы проверять выражение завершения в конце цикла, а не в ero на­ чале. К счастью, в Java есть цикл, который делает именно это: do-while. Цикл do-while всегда выполняет свое тело, по крайней мере, один раз, поскол ьку ero условное выражение находится в конце цикла. Вот ero общая форма: do ! / / тело цикла } while ( условие) ; 1 42 Часть 1. Язык Java На каждой итерации цикла do-wh i l e сначала выполняется тело цикла, а затем вычисляется условное выражение. При истинном значении выражения цикл будет повторяться. В противном случае цикл завершается. Как и во всех циклах Java, условие должно быть булевским выражением. Ниже показана переработанная версия программы, организующей выдачу "им пульсов'; в которой демонстрируется использование цикла do-wh i le. Она выдает тот же вывод, что и ранее. / / Демонстрация ра боты цикла do-wh i l e . class DoWh i l e { puЫ i c s tatic void main ( S tring [ ] args ) int n = l 0 ; do { System . out . println ( " Импyль c номер " + n ) ; n--; while ( n > О ) ; Цикл в предыдущей программе, хотя и будучи формально корректным, может быть записан более эффективно следующим образом: do { System . out . println ( " Импyль c номер " + n ) ; } whi le ( - -n > О ) ; В приведенном выше примере выражение (--n > О) объединяет декремент n и проверку на равенство нулю в одно выражение. Вот как оно работает. Сначала выполняется операция - -n, декрементирующая n и возвращающая новое значение n, которое затем сравнивается с нулем. Если оно больше нуля, тогда цикл продолжается; в противном случае цикл завершается. Цикл do-wh i le особенно полезен при обработке выбора пункта меню, потому что обычно требуется, чтобы тело цикла меню выполнялось хотя бы один раз. В представленной далее программе реализована очень простая справочная система для операторов выбора и итерации языка Java: // Исполь зование цикла do-while для обработки выбора пункта меню . class Menu { puЬlic static void mai n ( String [ ] args ) throws j ava . i o . I OException { char choice ; do { System . out . println ( " Kpaткa я справка по : " ) ; System . out . println ( " 1 . i f" ) ; System . out . println ( " 2 . switch" ) ; System . out . println ( " 3 . while" ) ; System . out . println ( " 4 . do-whi l e " ) ; System . out . println ( " 5 . for \ n " ) ; System . out . println ( " Bыбepитe вариант : " ) ; choice = ( char ) S ystem . i n . read ( ) ; while ( choice < ' 1 ' 1 1 choice > ' 5 ' ) ; System . out . println ( " \n " ) ; Гл ава 5. У правл я ю щ ие о п ераторы 1 43 switch ( choice) { case ' 1 ' : S ystern . out . println ( " Oпepaтop i f : \ n " ) ; Systern . out . println ( " i f ( ycлoвиe ) оператор ; " ) ; Systern . out . println ( " e l s e оператор ; " ) ; break; case ' 2 ' : Systern . out . p r intln ( " Oпepaтop switch : \ n " ) ; Systern . out . println ( " switch ( выpaжeниe ) { " ) ; Systern . out . println ( " case константа : " ) ; Systern . out . println ( " последовательность операторов " ) ; Systern . out . println ( " break; " ) ; System . out . println ( " / / . . . " ) ; Systern . out . println ( " } " ) ; break; case ' 3 ' : Systern . out . println ( " Oпepaтop whi l e : \ n " ) ; System . out . println ( " whi le ( ycлoвиe ) оператор ; " ) ; break; case ' 4 ' : Systern . out . println ( " Oпepaтop do-whi l e : \n " ) ; Systern . out . println ( " do { " ) ; Systern . out . println ( " оператор ; " ) ; Systern . out . println ( " } whi l e ( ycлoвиe ) ; " ) ; break; case ' 5 ' : System . out . println ( " Oпepaтop for : \ n " ) ; Systern . out . print ( " for ( инициaлизaция ; условие ; итерация ) " ) ; Systern . out . println ( " оператор ; " ) ; break; Вот пример вывода, генерируемого программой: Краткая справ ка по : 1 . if 2 . switch 3 . whi l e 4 . do-wh ile 5 . for Выберите вариант : 4 Оператор do-wh i l e : do { оператор ; } while ( ycлoвиe ) ; Цикл do-while применяется в программе для проверки того, что пользо­ ватель ввел правильный вариант. Если введенный вариант некорректен, тогда пользователю выдается повторный запрос. Поскольку меню должно отобра­ жаться хотя бы один раз, цикл do-while идеально подходит для решения та­ кой задачи. 1 44 Часть 1. Язык Java Есть еще нескол ько з амечан и й по поводу рассмотренного примера. Обратите внимание, что символы читаются с клавиатуры с помощью вызо­ ва Sys tem . i n . read ( ) . Это одна из функций консольного ввода Java. Хотя консольные методы ввода-вывода Java буд ут подробно обсуждаться л ишь в главе 13, сейчас следует отметить, что для получения выбранного пользова­ телем варианта использ уется выз ов метода System . in . read ( ) , которы й чи­ тает символы из стандартного ввода (воз вращаемые в виде целых ч исел, по причине чего воз вращаемое значение было преобразовано в char). По умол­ чанию стандартный ввод буфериз ируется построчно, так что пользователю придется нажать клавишу < Enter>, прежде чем любые введенные им символы отправятся в программу. Консольный ввод Java может оказ аться немного неудобным для работы. Кроме того, в большинстве реальных программ на Java будет реал из ован гра­ фи чески й польз овательский интерфейс. По упомянутой при чине в примерах, привод имых в книге, консольный ввод применяется не часто. Тем не менее, в таком контексте он полезен. Еще один момент, который следует учитывать: поскольку использ уется метод System . i n . read ( ) , в программе должна быть указана конструкция throws j ava . io . I OException, которая необходима для обработки ошибок ввода. Это часть средств обработки исклю чений Java, ко­ торые обс уждаются в главе 10. Ц и кл for Простая форма цикла fo r был а представлена в главе 2. Вы увидите, что цикл fo r является мощной и универсальной конструкцией. Существуют две формы цикла fo r. Первая - это традиционная форма, которая применялась со времен первоначальной версии Java, а вторая - бо­ лее новая форма в стиле "for-each'; появившаяся в JDK 5. Мы обсудим оба типа циклов fo r, начав с традиционной формы. Общий вид традиционной формы оператора fo r выглядит так: for ( инициализация; условие; итерация) / / тело цикла { Если многократно выполнять требуется лишь один оператор, то фи гурные скобки не нужны. Рассмотрим работу цикла fo r. При первом з апуске цикла выпол няется часть инициализация цикла. В общем случае она представляет собой выра­ жение, которое устанавл ивает з начение переменной управления циклом, кото­ рая действует в качестве с чет чика, управляющего циклом. Важно понимать, что инициал из ирующее выражение выпол няется только один раз . Затем вы­ числяется условие, которое должно быть булевским выражением. Как пра­ вило, оно сравнивает переменную управления циклом с целевым з начением. Есл и условие истинно, тогда выпол няется тело цикла, а если ложно, то цикл з авершается. Далее выполняется часть итерация цикла, которая обычно яв­ ляется выражением, и нкрементирующим или декрементирующим перемен- Гла ва 5. Уп ра в л я ю щ ие о п ератор ы 1 45 ную управления циклом. После этого цикл повторяется, при каждом проходе вычисляя условное выражение, выпол няя тело цикла и вы числяя выражение итерации. Тако й процесс происходит до тех пор, пока управляющее выраже­ ние не станет ложным. Вот версия програм мы, организ ующей выдачу "импульсов'; в которой ис­ польз уется цикл fo r: ! ! Демонстрация работы цикла for . class Fo rTick { puЫic static vo id main ( Str ing [ ] a rgs ) { int n ; for ( n= l O ; n> O ; n-- ) Sys tem . out . println ( " И.,,шульс номер " + n ) ; Объявление переменной управления циклом внутри ци кла for Часто переменная, управляющая циклом fo r, необходима только для целей цикла и больше нигде не з адействована. В таком слу чае перемен ную можно объявлять внутри части инициализация цикла fo r. Например, ниже показано, как переписать код предыдущей программы, чтобы объявить пере­ менную управления циклом n типа int внутри for: / / Объявление переменной управления циклом внутри for . class Fo rTick { puЫic static vo id main ( St r ing [ ] arg s ) { / / здесь переменная is объявляется внутри цикла for for ( int n=l O ; n>O ; n- - ) System . out . println ( " Импyльc номер " + n ) ; При объявлении переменной внутри цикла fo r следует помнить об одном важном аспекте: область видимости этой переменной ограни чена циклом fo r. За пределами цикла fo r переменная перестает с уществовать. Есл и пере­ менную управления циклом необходимо применять в другом месте програм­ мы, тогда объявлять ее внутри оператора fo r нельзя. В ситуации, когда переменная управления циклом больше нигде не нуж­ на, большинство программистов на Java объявляют ее внутри fo r. Например, далее приведена простая программа, которая проверяет, является ли число простым. Обратите внимание, что переменная управления циклом i объяв­ лена внутри fo r, т.к. в других местах она не использ уется. / / Проверка , является ли число простым . class FindPr ime { puЫ ic static voi d main ( String [ ] args ) i nt num; boolean i s Prime ; num = 1 4 ; 1 46 Ч а сть 1 . Яз ы к Java i f ( nurn < 2 ) i s Prirne = fal s e ; e l s e i s Prirne = t rue ; for ( i nt i=2 ; i <= nurn/ i ; i + + ) i f ( ( n urn % :._ ) == О ) { i s Prirne = false ; brea k ; i f ( i s Prirne ) Systern . out . println ( " я вляeтcя простым " ) ; e l s e Systern . out . println ( " не является простым " ) ; И с пользова н ие запятой Вр ем енами жел ател ьно вкл ючить более одного оп е ратора в части иници­ ализация и итерация цикла for. Например, взгляните на цикл в сл едующе й программе: class Sarnple { puЫ ic static vo id rna in ( String [ ] arg s ) { int а , Ь ; Ь = 4; for ( a= l ; a<I: ; а++ ) { Systern . out . println ( " a Systern . out . p r i n t l n ( " b ь-- ; " + а) ; " + Ь) ; Как видите, цикл управл яется взаимодейс тв ием двух переменных. Поскол ьку цикл управляется двумя п е ременными, было бы удобно поместить их внутрь оп ератора for, а не обрабатывать Ь вручную. К счастью, в Java е сть способ решить пробл ему. Чтобы позвол ить двум и более п еременным управ­ лять циклом for, в части инициализация и итерация цикла for разрешено вкл ючать н е сколько операторов, разделяя их запятыми. С применен ие м запятой предыдущий цикл for можно реал изовать более эффективно: // Исполь зование запятой . class Cornrna { puЫ i c static void rnai n ( S tri ng [ ] args ) { int а , Ь ; for ( a= l , Ь=4 ; а <Ь ; а++ , Ь- - ) { Systern . out . println ( " a " + а) ; Systern . out . priпtln ( " b = " + Ь ) ; Глава 5 . У п р авля ющ ие о п е р ато р ы 1 47 Здесь в части инициализация устанавливаются значения как а, так и Ь. Два разделенных запятыми оператора в части итерация выполняются на каждой итерации цикла. Программа генерирует следующий вывод: а = 1 Ь = 4 а = 2 Ь = 3 Н еко торые разновидно ст и ц и кла for Существует несколько разновидностей цикла for, расширяющих его воз­ можности и прим енимость. Причина такой гибкости связана с тем, что три части цикла for - инициализация, условие и итерация - не обязательно использовать только по прямому назначению. В действительности три части for можно применять для любых желаемых целей. Давайте рассмотрим при­ меры. Один из наибол ее распространенных вариантов задействует условное выражение. В частност и, этому выраж ению не требуется сравнивать п ере­ менную управления циклом с некоторым значением. На самом деле услови­ ем, управляющим for, может быть любое булевское выражение. Например, взrляните на показанный ниже фрагмент кода: bool ean done = fa l s e ; for ( i nt i = l ; ! done ; i++ ) // . . . i f ( inter rupted ( ) ) done = true ; В приведенном примере цикл for продолжает выполняться до тех пор, пока переменная done типа boolean не будет установл ена в t rue. Значение i в нем не проверяется. Есть еще одна интересная разновидность цикла for, в которой может от­ сутствовать либо часть инициализация, либо часть итерация, либо то и дру­ гое, как демонстрируется в следующей программе: // Части цикла for могут быт ь пустыми . c l a s s Fo rVar { puЫi c static void main ( String [ ] args ) int i ; boolean done = fal s e ; i = О; for ( ; ! done ; ) { Sys tem . out . println ( " i равно " + i ) ; i f ( i == 1 0 ) done = true; i++; 1 48 Часть 1. Язык Java Здесь выражения инициализ аци и и итерации вынесены з а пределы цик­ ла fo r. Таким образом, части fo r пусты. Хотя в настолько простом приме­ ре это не и меет з начения (более того, подобный стиль будет считаться до­ воль но плохим), могут воз никать ситуации, когда такой подход имеет смысл. Например, если начальное условие з адается с помощью сложного выражения в другом месте программы или переменная управления циклом изменяется в з ависимости от действий, происходящих в теле цикла, то может быть умест­ но оставить данные части цикла for пус тыми. Рассмотрим еще одну раз новидность цикла fo r. Можно преднамеренно создать бесконечный цикл (цикл, который никогда не з авершится), если оста­ вить все три части for пустыми. Вот пример: for ( , , ) ( // ... Этот цикл будет работать нескончаемо долго, потому что нет условия, при котором он з авершится. Хотя некоторые п рограммы, подобные командным процессорам операционной системы, требуют бесконечного цикла, большин­ с тво "бесконечных циклов" на самом деле представляют собой просто циклы со специальными требованиями относительно з авершения. Вскоре вы увиди­ те, что существует способ з авершить цикл (даже бесконечный вроде показ ан­ ного выше), в котором отсутствует нормальное условное выражение. В ерсия цикла f or в стиле "for-each" Вторая форма fo r реализует цикл в стиле "for-each': Вероятно, вам известно, что в современной теории языков программирования была принята концепция "for-each'; которая стала стандартным функциональным средством, ожидаемым программистами. Цикл в стиле "for-each" предназ начен для прохода по коллек­ ции объектов, такой как массив, строго последовательно от начала до конца. В Java стиль "for-each" также назы вают расширенным циклом fo r. Общая форма версии "for-each" цикла for выглядит следующим образ ом: for ( тип переменная-итерации : коллекция) блок-операторов Здесь тип указ ывает тип, а переменная-итерации - имя переменной и терации , которая будет получать элементы из кол лекции по одному з а раз , о т начала д о конца. Коллекция, п о которой проходит цикл, указ ывает­ ся в коллекции. Существуют различные типы коллекций, которые можно ис­ пользовать с for, но в настоящей главе применяется только массив. (Другие типы коллекций, которые можно использовать с fo r, вроде тех, что опреде­ лены в Collections Framework, обсуждаются далее в книге.) На каждой ите­ рации цикла из коллекции из влекается очередной элемент и сохраняется в переменной-итерации. Цикл повторяется до тех пор, пока не будут получены все элементы коллекции. Поскольку переменная итерации получает з начения из коллекции, тип обязан совпадать или быть совместимым с типом элементов, хранящихся в Гл ава 5 . Управл я ющ ие операторы 1 49 коллекции. Таким образом, при проходе по массивам тип должен быть со­ вместимым с типом элементов массива. Чтобы понять мотивы создания цикла в стиле "for-each'; рассмотрим тип цикла for, который он призван заменить. В следующем фрагменте кода для вычисления суммы значений в массиве применяется традиционный цикл for: int [ ] nums = { 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 1 0 } ; int sum = О ; for ( int i=0 ; i < 10 ; i++ ) sum += nums [ i ] ; Для вычисления суммы все элементы в nums читаются по порядку от нача­ ла до конца, т.е. целый массив читается в строго последовательном порядке. Это достигается путем ручной индексации массива nums с использованием переменной управления циклом по имени i. Стиль "for-each" цикла for позволяет автоматизировать предыдущий цикл. В частности, он избавляет от необходимости устанавливать счетчик циклов, указывать начальное и конечное значение и вручную индексировать массив. Взамен он автоматически проходит по всему массиву, получая по одному эле­ менту за раз, последовательно, от начала до конца. Например, вот предыду­ щий фрагмент, переписанный с применением версии цикла for в стиле "for­ each": int [ ] nums = { 1 , 2 , 3 , 4 , 5 , 6, 7 , 8 , 9 , 1 0 } ; int s um = О ; for ( i nt х : nums ) sum += х ; При каждом проходе цикла переменной х автоматически присваивается значение, равное очередному элементу в nums. Таким образом, на первой ите­ рации х содержит 1, на второй - 2 и т.д. Синтаксис не только стал проще, но он также предотвращает возникновение ошибок выхода за границы массива. Ниже показана полная программа, в которой демонстрируется только что описанная версия цикла for в стиле "for-each": / / Использование цикла for в стиле " for-each 11 • class ForEach { puЫic static void main ( St ring [ ] arg s ) i nt [ ] nums = { 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 1 0 } ; i nt sum = О ; / / Примени ть цикл for в стиле 11 for-each 11 для отображения // и суммирования значений . for ( i nt х : nums ) S ys t em . out . p rintln ( 11 Знaчeниe : 11 + х ) ; sum += х ; System . out . println ( "Cyммa : 1 1 + sum) ; В результате запуска программа генерирует следующий вывод: 1 50 Ч асть 1. Язык Java Значение : Значение : Значение : Знач ение : Значение : Значение : Значение : Значение : Значение : Значение : Сумма : 55 1 2 3 4 5 6 7 8 9 10 В выводе видно, что цикл for в стиле "for-each" автомат ически последова­ тельно проходит по массиву от наим еньшего индекса до наибольшего. Хотя цикл for в стиле "for-each" повторяется до тех пор, пока не будут просмотрены все элементы массива, цикл можно прервать досрочно, исполь­ зуя оператор break. Например, представленная далее программа суммирует только первые пять элементов числа: / / Использование break с циклом for в стиле " for-each " . class ForEach2 ( puЫi c static voi d ma in ( String [ ] a rgs ) int s um = О ; int [ ] nums = { 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 1 0 } ; / / Применить цикл for в стиле " for-each " для отображения / / и суммирования значений . for ( i nt х : nums ) { System . out . p rint l n ( "Знaчeниe : " + х ) ; sum += х ; / / остановить выполнение цикла , i f ( х = = 5 ) b reak; / / когда получено значение 5 S ystem. out . println ( "Сумма первых пяти элементов : " + s um) ; Вот какой вывод будет получен: Значение : 1 Значение : 2 Значение : 3 Значение : 4 Значение : 5 Сумма первых пяти элементо в : 1 5 Легко зам етить, что цикл for останавливается после получения пятого эл ем ента. Оператор break можно также использовать с другими циклами Java, и он подробно обсуждается позже в главе. Существует один аспект, касающийся цикла for в стиле "for-each'; кото­ рый важно понимать. Его пер еменная итерации доступна только для чтения, хотя она связана с лежащим в основе массивом. Присваивание значения пере­ менной итерации не влияет на лежащий в основе массив. Другими словами, изменить содержимое массива, присваивая переменной итерации новое зна­ ч ение, не удастся. Гл ава 5 . У правл я ющ ие о п ераторы 1 51 Например, рассмотрим следующую проrра мму: / / Переменная итерации цикла for в стиле " for-each" доступна тольк о для чтения . class NoChange { puЬlic static void main ( String [ ] a rgs ) int [ ] nums = { 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 1 0 } ; for ( int х : nums ) { System . out . print ( x + " " ) ; х = х * 1 0 ; // н е вл и яет н а nums System . out . println ( ) ; for ( int х : nums ) System . out . print ( x + " " ) ; System . out . println ( ) ; В первом цикле for знач ение переменной итерации увеличивается в 10 раз. Однако такое присваивание никак не влияет на лежащий в основе массив nums, что иллюстрирует второй цикл for. Сказанное подтверждается выво­ дом программы: 1 2 3 4 5 6 7 8 9 10 1 2 3 4 5 6 7 8 9 10 П роход п о много м ерным массивам Р асширенная версия for работает также и с мноrомерными массивами. Но не забывайте о том, что мноrомерные массивы в Java представляют со­ бой массивы массивов. (Например, двумерный массив - это массив одномер­ ных массивов.) Данный факт важен при проходе по многомерному массиву, поскольку на каждой итерации получается очередной массив, а не индивиду­ альный элем ент. Кроме тоrо, переменная итерации в цикле fo r должна быть совместимой с типом получаемоrо массива. Наприм ер, в ел учае двумерноrо массива переменная итерации должна быть ссылкой на одном ерный массив. В общем случае при использовании цикла for в стиле "for-each" для прохода по массиву с N изм ерениями полученные объекты будут массивами с N-1 из­ мерениями. Чтобы понять последствия, рассмотрим следующую проrрамму. В ней применяются вложенные циклы for для получения элементов двумер­ ноrо массива в порядке строк, от первой до последней. // Исполь зование цикла for в стиле "for-each" для прохода по двумерному массиву . class ForEachЗ { puЬlic static void mai n ( String [ ] a rgs ) { int sum = О ; int [ ] [ ] nums = new int [ З ] [ 5 ] ; // Сохранить в nums ряд значений . for ( int i = О ; i < З ; i++ ) for ( int j = О ; j < 5 ; j ++ ) nums [ i ] [ j ] = ( i+ l ) * ( j + l ) ; 1 52 Ч а сть 1 . Язык Java / / Применить цикл for в стиле " for-each" для отображения // и суммирования значений . for ( int [ ] х : nums ) { for ( i nt у : х ) { System . out.println ( " Знaчeниe : " + у) ; s um += у ; System. out . println ( "Сумма : " + s um ) ; Ниже показан вывод, генерируемый программой: Значение : 1 Значение : 2 Значение : 3 Значение : 4 Значение : 5 Значение : 2 Значение : 4 Значение : 6 Значение : 8 Значение : 1 0 Значение : 3 Значение : 6 Значение : 9 Значение : 1 2 Значение : 1 5 Сумма : 90 Вот строка программы, представляющая особый интерес: for ( int [ ] х : nums ) { Обра тите внимани е на то, как объявлена переменная х. Она является ссылкой на одномерный массив целых чисел. Необходимость в тако м объ­ явлении связана с т ем, что на каждой итерации цикла for получается очеред­ но й массив из nums, начиная с массива nums [ О ] . Затем во внутреннем цикле for производи тся проход по каждому из этих массивов с отображением зна­ чения каждого элемента. П р и м е н е н ие ра сш и р е нного ци кл а for Поскольку цикл for в стиле "for-each" способен только последоват ельно проходить по массиву от начала до конца, вам может показаться, что его ис­ пользование ограничено, но это не так. Большое ко личество алгоритмов тре­ бует именно такого механизма. Одним из самых распространенных можно считать поиск. Например, в следующей программе цикл for применяется для поиска значения в несортированном массиве. Он останавливается, ко гда зна­ чение найдено. / / Поиск в массиве с использованием цикла for в стиле " for-each " . class Search { Глава 5 . Управ л я ю щие о пер ато р ы 1 53 puЬlic static void main (St ring [ ] args ) int [ ] nums = { 6 , 8 , З, 7 , 5 , 6, 1 , 4 } ; int val = 5 ; boolean found = fal s e ; // Применить цикл for в стиле "fo r-each" для поиска va l в nums . for ( int х : nums ) i f (x == val ) { found = true ; brea k; i f ( found) System . out . print ln ( "Знaчeниe найдено ! " ) ; Цикл for в стиле "for-each" - великолепный вариант в этом приложении, т.к. поиск в несортированном массиве предусматривает последовательный просмотр каждоrо элемента. (Разумеется, если бы массив был отсортирован, можно было бы использовать двоичный поиск, что потребовало бы цикл от­ личающеrося стиля.) Друrие типы приложений, которые выиrрывают от ци­ клов for в стиле "for-each'; включают вычисление среднеrо значения, нахож­ дение минимальноrо или максимальноrо значения внутри множества, поиск дубликатов и т.д. Хотя в примерах, приводимых в rлаве, применялись массивы, цикл for в стиле "for-each" особенно полезен при работе с коллекциями, определенными в инфраструктуре Collections Framework, который описан в части II. В более общем случае цикл for может проходить по элементам любой коллекции объектов, пока она удовлетворяет набору оrраничений, которые описаны в rлаве 20. В ыведение типов ло к а ль ны х переменны х в ц и к ле for Как объяснялось в rлаве 3, в версии JDK 10 появилось средство, называе­ мое выведением типов локальных переменных, которое позволяет определить тип локальной переменной по типу ее инициализатора. Для использования выведения типов локальных переменных понадобится указать var в каче­ стве типа переменной и инициализировать ее. Выведение типов локальных переменных можно применять в цикле for при объявлении и инициализации переменной управления циклом внутри традиционноrо цикла for или при указании переменной итерации в цикле for в стиле "for-each': В следующей проrрамме демонстрируются примеры каждою случая: / / Использование выведения типов локальных переменных в цикле for . cla s s Typein ferenceinFor { puЫic static voi d main ( S tring [ ] args ) { / / Применить выведение типов к переменной управления циклом . System . out . print ( "Знaчeния х : " ) ; 1 54 Часть 1. Язык Java for (var х = 2 . 5 ; х < 1 0 0 . 0 ; х System . out . p ri n t ( x + " " ) ; Sys tem . out . printl n ( ) ; / / Применить выведение тип ов int [ ] nums = { 1 , 2 , 3 , 4 , 5 , Sys tem . out . p rint ( "Знaчeния в for ( var v : nums ) System . out . print ( v + " " ) ; System . out . println ( ) ; = х * 2) к переменной итерации . 6) ; ма сс и ве nums : " ) ; Вот вывод программы: Значен и я х : 2 . 5 5 . 0 1 0 . 0 2 0 . 0 4 0 . 0 В О . О Значен и я в массиве nums : 1 2 3 4 5 6 В приведенном примере для пер еменной управления циклом х в строке: for (var х = 2 . 5 ; х < 1 0 0 . 0 ; х = х * 2 ) выводится тип douЫe по причине типа ее инициализатора. Для переменной итерации v в строке: for ( var v : nums ) выводится тип int, потому что он является типом элементов массива nums. И последнее замечание: поскольку многие читатели будут работать в сре­ дах, предшествующих JDK 10, в большинстве цик лов for в оставшихся мате­ риалах настоящего издания книги выведение типов локальных переменных использоваться не будет. Конечно, вы должны обдумать его применение в но­ вом коде, который вам пр едстоит написать. В ложенные ц иклы Подобно всем остал ьным языка м программирования в Java разр еше­ ны влож енные циклы, т. е. один цикл может находиться внутри другого. Например, в показанной ниже программе используются вложенные циклы for: / / Циклы могут быть вложенньuvт.и . class Nested { puЫic static void main ( String [ ] args ) { int i , j ; for { i=O ; i <l O ; i + + ) { for ( j =i ; j < l O ; j + + ) System . out . p rint ( " . " ) ; Sys tem . out . println ( ) ; Программа генерирует следующий вывод: Гл ава 5. Уп равляющие оператор ы 1 55 Операторы перехода В Java поддерживаются три оператора перехода: break, continue и return. Они передают управление другой части программы и обсуждаются ниже. На заметку! Помимо обсуждаемых здесь операторов перехода в Java есть еще один способ, с помощью котороrо можно изменить поток выполнения программы: через обработку исключений. Обработка исключений обеспечивает структурированный подход, с помощью которого программа может перехватывать и обрабатывать ошибки во время выполнения. Он поддерживается ключевыми словами try, catch, throw, throw s и finally. По существу механизм обработки исключений позволяет программе выполнять нелокальный переход. Поскольку обработка исключений является обширной темой, она обсуждается отдельно в главе 1 О. И спол ь зова н и е оператора break Оператор brea k в язы ке Java при меняется в трех ситуациях. Во-первых, как вы видели, он з авершает последовательность о ператоров в о ператоре swi tch. Во-вторых, его можно использовать для выхода из цикла. В-третьих, его можно применять как "цивилизованную" форму п е р е хода в стиле "goto': Здесь объясняются последние два использования. Ис пользова н ие оператора break дл я в ыхода из цикла За сч е т при менения оператора brea k вы можете принудительно з авер­ шить цикл, пропуская вычисление условного выражения и выполнение любо­ го оставшегося кода в теле цикла. Когда внутри цикла встречается оператор brea k, цикл з авершается и управление пер едается оператору, следующему з а циклом. Вот простой пример: // Использование break для выхода из цикла . class B rea kLoop { puЬl i c static vo id main ( S t ring [ ] a rgs ) { for ( i nt i=0 ; i<l 0 0 ; i + + ) { i f ( i == 1 0 ) brea k ; / / пре кратить выполнение цикла , если i равно 1 0 System . out . print ln ( " i : " + i ) ; System . out . println ( "Цикл завершен . " ) ; 1 56 Час т ь 1. Язык Java Программа генерирует следующий вывод: i: о i: i: 2 i: 3 i: 4 i: 5 i: 6 i: 7 i: в i: 9 Цикл за вершен . Несложно заметить, что хотя цикл for предназначен для выполнения от О до 99, оператор break приводит к его преждевременному прекращению, ког­ да i равно 1 О. Оператор break можно использовать с любыми циклами Java, включая на­ меренно реализованные бесконечные циклы. Например, н и же показана пре­ дыдущая программа, переписанная с применением цикла while. Вывод этой программы будет таким же, как приведенный выше. / / Использование brea k для выхода из цикла whi l e . class B rea k1oop2 { puЬ l i c s tatic void main ( String [ ] args ) { int i = О ; ·whi l e ( i < 1 0 0 ) { i f ( i == 1 0 ) brea k ; / / прекратить выполнение цикла, если i равно 1 0 System . out . p r int ln ( " i : " + i ) ; i++; System . out . print l n ( "Цикл завершен . " ) ; При использовании внутри набора вложенных циклов оператор b rea k производит выход только из самого внутреннего цикла, например: // Использование brea k с вложенными циклами . class BreakLoop З { puЫ i c s tatic void ma in ( S t ring [ ] a rgs ) for ( i n t i=0 ; i < З ; i + + ) { System. out . print ( "Пpoxoд " + i + " : " ) ; for ( int j = 0 ; j < l 0 0 ; j + + ) { if ( j == 10 ) break; // прекратить выполнение цикла , е сли j равно 10 System . out . print ( j + " " ) ; System . out . println ( ) ; System . out . println ( "Цикл завершен . " ) ; Программа генерирует следующий вывод: Гл а в а 5. Упра вляющие операторы Проход О : о 1 2 3 4 5 6 7 8 Проход 1 : о 1 2 3 4 5 6 7 8 Проход 2 : о 1 2 3 4 5 6 7 8 Цикл зав ер шен . 1 57 9 9 9 Как видите, оператор break во внутреннем цикле приводит к прекраще­ нию только этого цикла, не затрагивая внешний цикл. С оператором bre a k связаны еще два момента, о которых следует пом­ нить. Во-первых, в цикле могут находиться более одного оператора break. Однако будьте осторожны. Слишком большое количество операторов break способно деструктурировать код. Во-вторых, оператор break, завершающий swi tch, влияет только на этот оператор swi tch, но не на любые объемлющие циклы. Помните! Оператор break не задумывался как обычное средство завершения цикла. Для этой цели предназначено условное выражение цикла. Оператор brea k должен применяться для прекращения работы цикла только в случае возникновения какой-то особой ситуации. Использование оператора break как разновидности перехода в стиле "goto" В дополнение к применению с оператором swi tch и цикла ми оператор break также может использоваться сам по себе, чтобы обеспечить "цивили­ зованную" форму перехода в стиле "goto': В языке Java нет оператора "goto'; т.к. он обеспечивает возможность ветвления произвольным и неструктурирован­ ным образом, что обычно затрудняет понимание и сопровождение кода, опи­ рающегося на переходы в стиле "goto': Кроме того, "goto" препятствует неко­ торым оптимизациям со стороны компилятора. Тем не менее, есть несколько мест, где переходы в стиле "goto" будут ценной и законной конструкцией для управления потоком. Например, переход в стиле "goto" может быть полезен при выходе из глубоко вложенных циклов. Для обработки таких ситуаций в Java определена расширенная форма оператора break.. С применением такой формы break можно, например, выходить из одного или нескольких блоков кода, которые не обязательно должны являться частью цикла или переклю­ чателя, а могут быть любыми. Более того, можно точно указывать, где будет возобновлено выполнение, т.к. расширенная форма оператора break работа­ ет с меткой. Как вы увидите, break обеспечивает преимущества перехода в стиле "goto" без присущих ему проблем. Общая форма оператора break с меткой выглядит следующим образом: break метка ; Чаще всего мет ка представляет собой имя маркера, идентифицирующего блок кода. Блок кода может быть как автономным, так и блоком, являющим­ ся целью другого оператора. При выполнении расширенной формы операто­ ра break поток управления покидает блок, указанный в break. Снабженный меткой блок должен охватывать оператор break, но не обязательно быть тем, который содержит в себе этот break непосредственно. Отсюда следует, на- 1 58 Част ь 1. Язык Java пример, что оператор break с меткой можно использовать для выхода из на­ бора вложенных блоков. Но применять break для передачи управления из блока, который не охватывает данный оператор break, нельзя. Чтобы назначить блоку и мя, необходимо поместить в его начало метку. Метка - это любой допустимый идентификатор Java, за которым следует двоеточие. После пометки блока метку можно использовать в качестве цели оператора break, что приведет к возобновлению выполнения после конца помеченного блока. Например, в показанной далее программе реализованы три вложенных блока, каждый со своей меткой. Оператор break передает управление вперед, за конец блока с меткой second, пропуская два оператора println ( ) . ! / Использование bre ak в качестве "цивилизованной" формы перехода в стиле "goto" class Break { puЫ ic static vo i d main ( Str iпg [ ] arg s ) { Ьооlеап t = true ; fir st : { se coпd : third : { System . o ut . p riпtln ( " Пepeд оператором brea k . " ) ; i f ( t ) break secoпd ; / / выйти из блока second System . o ut . pr intln ( " Этoт оператор не выполнится . " ) ; System . out . pr i ntln ( " Этoт опер атор не выполнит ся . " ) ; System . out . println ( " После блока second . " ) ; В результате запуска программы получается следующий вывод: Перед операт ором brea k . После блока second . Одним из наиболее распространенных применений оператора bre a k с меткой является выход из вложенных циклов. Скажем, в приведенной ниже программе внешний цикл выполняется только один раз: / / Использование break для выхода из вложенных циклов . cl ass Brea kLoop4 { puЫ ic static vo i d main ( S tr ing [ ] args ) outer : for ( i nt i=O ; i<З ; i++ ) { System . out . pr int ( "Пpoxoд " + i + " : " ) ; for ( int j = O ; j < l O O ; j ++ ) { i f ( j == 1 0 ) break oute r; / / выйти из обоих циклов S ystem . out . print ( j + " " ) ; System . out . println ( " Этo выводиться не будет . " ) ; System . out . println ( " Циклы завершены . " ) ; Гл ава 5. Упра вл я ю щи е о перато ры 1 59 Вот вывод, генерируемый программой: Проход О : О 1 2 3 4 5 6 7 В 9 Циклы завершены . После выхода из внутреннего цикла во внешний цикл оба цикла заверша­ ются. Обратите внимание на то, что здесь помечен оператор for, содержащий целевой блок кода. Имейте в виду, что использовать оператор break с меткой, которая опре­ делена не для охватывающего блока, не разрешено. Например, следующая программа содержит ошибку и компилироваться не будет: / / Эта программа содержит ошибку . c l a s s BreakErr { puЫ i c static void ma in ( String [ ] args ) one : for ( i nt i=O ; i < З ; i++ ) { System . out . pr i nt ( " Пpoxoд " + i + " · " ) ; for ( i nt j =O ; j < l O O ; j ++ ) { i f ( j == 1 0 ) break one ; / / ОШИБКА System . out . print ( j + " " ) ; Поскольку цикл, помеченный как o ne, не охватывает оператор brea k, пе­ редать управление из этого блока невозможно. И спол ьз ование оператора continue Иногда необходимо обеспеч ить, чтобы и терация цикла выполн илась раньше, до достижен ия конца тела. То есть выполнение цикла должно про­ должаться, но без обработки остатка кода в его теле для конкретной итера­ ции. По сути, это переход в конец ци кла. Такое действие реализует оператор co nti nue. В циклах whi le и do -while оператор cont i nue передает управле­ ние напрямую условному выражению, управляющему циклом. В цикле fo r управление передается сначала итерационной части оператора fo r, а затем условному выражению. Для всех трех циклов любой промежуточный код пропускается. В показанной далее программе оператор cont inue применяется для выво­ да в каждой строке двух чисел: / / Демонстрация работы continue . class Continue { puЫ ic s tatic void mai n ( String [ ] a rgs ) { for ( int i=O ; i < l O ; i + + ) { System . out . print ( i + " " ) ; i f ( i % 2 == О ) continue; System . out . println ( " " ) ; 1 60 Ча с ть 1. Язы к Java Операция % в коде используется для проверки значения i на предмет чет­ ности. Если значение i четное, тогда цикл продолжается без вывода символа новой строки. Программа генерирует следующий вывод: О 2 4 6 8 1 3 5 7 9 Как и в случае с break, в операторе conti nue можно указывать метку для описания того, какой объемл ющий цик л необходимо продолжить. Вот при­ мер программы, в которой оператор cont inue применяется для вывода треу­ гольной таблицы умножения чисел от О до 9: // Использование coпtiпue с меткой . class ContinueLabel { puЫi c static void main ( String [ ] args ) oute r : for ( int i=0; i<l 0 ; i++ ) { for ( int j = 0 ; j < l 0 ; j ++ ) { if (j > i) { Systern . out . priпtln ( ) ; continue outer ; Sys tern . out . print ( " " + ( i * j ) ) ; System . out . println ( ) ; Оператор cont i nue в этом примере завершает цик л по j и продолжает со следующей итерации цикл а по i . Вывод программы показан ниже: о О О О 0 О О О О О 1 2 3 4 5 6 7 8 9 4 6 9 8 12 1 6 10 15 20 12 18 24 14 21 28 1 6 2 4 32 18 27 36 25 30 35 40 45 36 42 49 48 56 64 54 63 72 8 1 Подходящие сценарии использования conti nue встречаются редко. Одна из причин связана с тем, что язык Java предлагает обширный набор операто­ ров цик лов, которые подходят для большинства приложений. Однако для тех особых обстоятельств, когда итерацию необходимо начинать раньше, опера­ тор continue обеспечивает структурированный способ решения задачи. Гл ава 5 . Управл я ю щие операто ры 161 Оператор return Последний управляющий оператор - return. Он применяется для явного возвращения из метода, т.е. управление программой передается обратно вы­ зывающей стороне. Таким образом, return классифицируется как оператор перехода. Хотя подробное обсуждение оператора return следует отложить до обсуждения методов в главе 6, здесь представлен его краткий обзор. О ператор re turn можно испол ьзовать в л юбом месте метода, чтобы возвратить управление вызывающей стороне. Таким образом, оператор return немедленно завершает выполнение метода, в котором он находится. Сказанное иллюстрируется в следующем примере, где оператор return воз­ вращает управление исполняющей среде Java, поскольку именно она вызвала ma in ( ) : / / Демонстрация работы return . class Return { puЬlic static voi d main ( String [ ] args ) { boolean t = t rue ; System . out . println ( " Пepeд оператором return . " ) ; i f ( t ) return; / / возвратит ь управление вызывающей стороне System . ou t . println ( " Этo выполняться не будет . " ) ; Вот как выглядит вывод программы: Перед операт ором return . Как видите, последний оператор p r i n t l n ( ) не выпол няется. Сразу по­ сле выполнения оператора r e t u r n управление возвращается вызывающей стороне. И последнее заме чание: в предыдущей программе оператор i f ( t ) обя­ зателен. Без него компилятор Java отметил бы ошибку типа "недостижимый код" (unreachaЫe code), поскольку компилятору было бы известно, что по­ следний оператор println ( ) никогда не выпол нится. Для предотвращения такой ошибки в коде и применяется оператор i f, чтобы "обмануть" компиля­ тор ради этой демонстрации. ГЛ А В А . . h '"-5 � �: �-� �\ 1� �-;. ������;_, &��-� --.; ... -�{ . - 1 -�� �,;r �'. � Введение в кла сс ы Класс лежит в самом центре Java. Он представляет собой логическую кон­ струкцию, на которой построен весь язык Java, потому что она определяет форму и природу объекта. Таким образом, класс формирует основу для объ­ ектно-ориентированного программирования (ООП) на Java. Любая концеп­ ция, которую вы хотите реализовать в программе на Java, должна быть инкап­ сулирована внутри класса. Поскольку класс настолько фундаментален для Java, ему будут посвящены эта и несколько последующих глав. Здесь вы ознакомитесь с основными эле­ ментами класса и узнаете, как использовать класс для создания объектов. Вы также узнаете о методах, конструкторах и ключевом слове this. Основы классов Классы применяются с самого начала книги. Однако до сих пор была по­ казана только самая рудиментарная форма класса. Классы, создаваемые в предшествующих главах, главным образом существовали просто для инкап­ суляции метода main ( ) , который использовался с целью демонстрации основ синтаксиса Java. Как вы увидите, классы значительно мощнее, чем представ­ ленные до сих пор ограниченные варианты. Вероятно, наиболее важная характеристика класса заключается в том, что он определяет новый тип данных. После определения новый тип можно при­ менять для создания объектов такого типа. Следовательно, класс - это ша­ блон для объекта, а объект - это экземпляр класса. Так как объект является экземпляром класса, вы часто будете видеть, что слова объект и экземпляр используются взаимозаменяемо. Общая форма класса При определении класса вы объявляете его точную форму и природу, для чего указываете данные, которые он содержит, и код, который работает с эти­ ми данными. В то время как очень простые классы могут содержать только код или только данные, большинство реальных классов содержат то и другое. Как вы увидите, код класса определяет интерфейс к своим данным. Глава 6. Введен и е в классы 1 63 Класс объявляется с применением ключ евого слова c lass. Классы, кото­ рые использовались до сих пор, на самом дел е являются крайне огранич ен­ ными примерами его полной формы. Классы могут (и обычно становятся) намного сложнее. Ниже приведена упрощенная общая форма определ ения класса: class тип тип // имя-кла сса { переме нная-экз емпляра l ; переменная -экз емпляра 2; ... тип переме нная-экз емпляраN; тип имя-ме тода l ( список-параме тров ) / / тело метода тип имя-метода2 ( список-параметров) / / т ело метода } // ... тип имя-ме тодаN( список-параме тров ) / / тело ме тода Данные, или переменные, определенные в классе, называются переменными экземпляра. Код содержится внутри м етодов. В совокупности методы и пере­ менные, определе нные в классе, называются членами класса. В большинстве классов перем енные экземпляра обрабатываются и доступны с помощью ме­ тодов, определ енных для этого класса. Таким образом, как правило, именно методы определяют, как можно использовать данные класса. Пер еменные, определ енные внутри класса, называются переменными эк­ земпляра из-за того, что каждый экземпляр класса (т. е. каждый объект клас­ са) содержит собственную копию этих переме нных. Та ким образом, данные для одного объекта являются уникальными и обособл енными от данных для другого. Вскоре мы продолжим раскрытие темы, но о настолько важной кон­ цепции стоит узнать пораньше. Все методы имеют ту же общую форму, что и метод ma in ( ) , который мы применяли до сих пор, но большинство методов не будут указаны как sta t i c и л и puЫ i c. Обратите внимани е, что в общей форме класса не определен метод ma i n ( ) . Кл ассы Java не обязаны иметь метод main ( ) . Он указывается только в том случае, есл и класс является начальной точкой для выпол нения программы. Кроме того, не которые виды приложений Java вообще не требу­ ют метода ma in ( ) . Пр осто й класс Давайте начнем изучение классов с простого примера. Ниже приведен код класса Вох (коробка), в котором определены три п еременных экземпляра: width (ширина), he ight (высота) и depth (глубина). Пока что Вох не содер­ жит какие-либо методы {но со временем они будут добавляться). 1 64 Часть 1 . Язык Jаvа class Вох { douЫe width ; douЫe height ; douЫe depth ; Как утверждалось выше, класс определяет новый тип данных. В рассма­ триваемом случае новый тип данных называется Вох. Имя класса будет ис­ пользоваться для объявления объектов типа Вох. Важно помнить, что объ­ явление класса создает только шаблон, но не фактический объект. Таким образом, предыдущий код не создает никаких объектов типа Вох. Чтобы действительно создать объект Вох, будет применяться оператор следующего вида: Вох mybox = new Вох ( ) ; / / создать объект Вох по имени mybox После выполнения этого оператора mybox будет ссылаться на экземпляр Вох. Таким образом, он обретет "физическую" реальность. Пока что не бес­ покойтесь о деталях представленного оператора. Как упоминалось ранее, при создании экземпляра класса на самом деле создается объект, который содержит собственную копию каждой перемен­ ной экземпляра, определенной в классе. Соответственно каждый объект Вох будет содержать собственные копии переменных экземпляра width, he ight и depth. Для доступа к этим переменным будет использоваться операция точки ( . ), которая связывает имя переменной экземпляра с именем объекта. Например, чтобы присвоить переменной width объекта mybox значение 1 0 0, потребуется применить такой оператор: mybox . width = 1 0 0 ; Приведенный оператор сообщает компилятору о необходимости присво­ ить копии width, содержащейся в объекте mybox, значение 1 0 0. В общем слу­ чае операция точки используется для доступа и к переменным экземпляра, и к методам внутри объекта. Еще один момент: несмотря на то, что ее обычно называют операцией точки, формальная спецификация Java классифицирует . как разделитель. Тем не менее, из-за широкого распространения термина "операция точки" именно он и применяется в книге. Ниже показана полная программа, в которой используется класс Вох: /* Программа , в которой исполь зуется класс Вох . Назовите этот файл BoxDemo . j ava */ class Вох { douЫe width ; douЫe height ; douЫe depth ; / / В этом классе объявляется объект типа Вох . class BoxDemo { puЫ ic static void ma in ( String [ ] args ) Вох mybox = new Вох ( ) ; douЫe vol ; Глава 6. В ведение в классы 1 65 / / Присвоить значения переменным экземпляра mybox . mybox . width = 1 0 ; mybox . height = 2 0 ; mybox . depth = 1 5 ; / / Вычислить объем коробки . vo l = mybox . width * mybox . height * mybox . depth ; System . out . print l n ( "Oбъeм равен " + vo l ) ; Вы дол жны наз нач и ть файлу, содержащему эту програм му, и м я BoxDemo . j ava, потому что метод ma in ( ) находится в классе BoxDemo, а не в Вох. Скомпил ировав програм му, вы обнаружите, что был и созданы два файла . class - оди н для Вох и один для BoxDemo. Компилятор Java авто­ матически помещает каждый кл асс в отдельный файл . class. Классы Вох и BoxDemo не обязательно должны находиться в одном и том же исходном фай­ ле. Вы можете поместить классы в собственные файлы с именами Вох . j ava и BoxDemo . j ava. Для з апуска программы потребуется выполнить BoxDemo . class. В рез уль­ тате будет получен следующий вывод: Объем равен 3 0 0 0 . 0 Как утверждалось ранее, каждый объект имеет собственные копи и пере­ менных экземпляра, т.е. если есть два объекта Вох, то каждый из них имеет собственную копию depth, width и height. Важно понимать, что изменения в переменных экземпляра одного объекта не вл ияют на переменные экз емпля­ ра другого. Например, в следующей программе объявляются два объекта Вох: / / В этой программе объявляются два объекта Вох . class Вох { douЫe width ; douЫe heigh t ; douЫe depth ; class BoxDemo2 { puЫ i c static void main ( S t r i ng [ ] a rgs ) { Вох myboxl = new Вох ( ) ; Вох mybox2 = new Вох ( ) ; douЫe vol ; / / Присвоить значения переменным э кземпляра mybox l . myboxl . width = 1 0 ; myboxl . height = 2 0 ; myboxl . depth = 1 5 ; / * Присвоить переменным экземпляра mybox2 другие значения . * / mybox2 . width = 3 ; mybox2 . height = 6 ; mybox2 . depth = 9 ; / / Вычислить объем первой коробки . vol = mybox l . width * mybox l . hei ght * mybox l . dept h ; System . out . println ( "Oбъeм равен " + vol ) ; 1 66 Часть 1 . Яз ык Java / / Вычислить объем в 'горой коробки . vol = mybox2 . width * mybox2 . height * mybox2 . depth ; System . out . println ( "Oбъeм равен " + vol ) ; Вот вывод, который генерирует программа: Объем равен 3 0 0 0 . 0 Объем равен 1 62 . 0 Вы видите, что данные в myboxl полностью независимы от данных, содер­ жащихся в mybox2. О бъ явление о бъ ектов Как только что объяснялось, создание класса означает соз дание ново­ го типа данных, который можно применять для объявления объектов этого типа. Однако полу чение объектов класса представляет собой двухэтапный процесс. Во-первых, потребуется объявить переменную типа класса. Такая переменная не определяет объект, а просто может ссылаться на объект. Во­ вторых, необходимо полу чить физическую копию объекта и присвоить ее этой переменной, для чего служит операция new. Операция new динами че­ ски (т.е. во время выполнения) выделяет память для объекта и возвращает ссылку на нее, которая по существу является адресом в памяти объекта, вы­ деленной new. Затем ссылка сохраняется в переменной. Таким образом, в Java все объекты класса должны раз мещаться динамически. Рассмотрим детали данной процедуры. В предшествующих примерах программ для объявления объекта типа Вох использ овалась строка следующего вида: Вох mybox = new Вох ( ) ; Показ анный оператор объединяет два только что описанных шага. Его можно переписать так, чтобы более четко показ ать каждый шаг: / / объявить ссылку на объект / / разме стить в памяти объект Вох Вох mybo x ; mybox = new Вох ( ) ; В первой строке переменная mybox объявляется как ссылка на объект типа Вох. Пока что mybox не ссылается на фактический объект. Во второй строке объекта раз мещается в памяти и ссылка на него присваивается mybox. После выполнения второй строки переменную mybox можно использовать, как если бы она была объектом Вох. Но на самом деле mybox просто содержит адрес памяти фактического объекта Вох. Действие приведенных выше двух строк кода иллюстрируется на рис. 6. 1 . Подробн ый анали з операции new Как только что объяснялось, операция new динамически выделяет память для объекта. В контексте присваивания она имеет следующую общую форму: переменная - кла сса = new имя-класса ( ) ; Гла ва 6. Вв е д ение в классы в Оператор Вох mybox; mybox 1 67 Действие mybox new Вох ( ) ; mybox .. width height depth Объект вох Рис. 6.1 . Объявление объекта типа Вох Здесь переменная-кла сса - это переменная соз даваемого типа клас­ са, а имя-кла сса - это имя класса, экз емпляр которого соз дается. Имя класса, з а которым следуют круглые скобки, указывает конструктор клас­ са. Конструктор определяет, что происходит при соз дании объекта класса. Конструкторы являются важной частью всех классов и имеют м ного з начи­ мых атрибутов. В большинстве реальных классов конструкторы определяют­ ся я вно в нутри определений классов. Тем не менее, если я вный конструктор не указ ан, то компилятор Java а втоматически предоставит стандартный кон­ структор, что и происходило в слу чае с Вох. Пока что мы будем применять стандартный конструктор. Вскоре вы увидите, каким образ ом определять собственные конструкторы. На данном этапе вас может интересовать, почему не нужно использовать new для таких вещей, как целые числа или символы. Дело в том, что прими­ тивные типы Java реализованы не в виде объектов, а в форме "обычных" пере­ менных. Так поступили в интересах эффективности. Как вы увидите, объекты обладают м ногими характеристиками, которые требуют, чтобы их трактовали иначе, чем примитивные типы. Отсутствие в примитивных типах наклад ных расходов, присущих объектам, дало воз можность реализовать примитивные типы более эффективно. Позже вы встретите объектные версии примитив­ ных типов, доступные для использования в тех сит уациях, когда требуются пол ноценные объекты таких типов. Важно понимать, что операция new выделяет память для объекта во время выпол нения. Преимущество этого подхода состоит в том, что ваша програм ­ ма может соз дать столько объектов, сколько требуется в о время ее выпол не­ ния. Однако поскольку память конечна, возмож но, что new не сможет выде­ лить память под объект из-за нехватки памяти. В тако й ситуации воз никает исключение времени выполнения. (Вы уз наете, как обрабатывать исключе­ ния, в главе 10.) В примерах программ, приводимых в книге, вам не придется беспокоиться о нехватке памяти, но нужно будет учитывать эту воз можность в реальных программах, которые вы будете писать. 1 68 Часть 1 . Яз ы к Java Давайте еще раз подчеркнем различие между классом и объектом. Класс создает новый тип данных, который можно применять для создания объек­ тов. То есть к ласс создает логическую инфраструктуру, определяющую отно­ шения между его членами. При объявлении объекта к ласса создается экзем­ п ляр этого класса. Таким образом, класс является логической конструкцией, а объект имеет физическ ую реальность (занимает место в памяти). Важно четко осознавать указанное различие. Присваивание для переменн ы х ссылок на объ екты При п рисваивании переменные ссылок на объекты действуют иначе, чем вы могли бы ожидать. Например, что, по-вашему, делает следующий фраг­ мент кода? Вох Ы = new Вох ( ) ; Вох Ь 2 = Ы ; Вы можете подумать, что п еременной Ь2 присваивается ссылка на копию объекта, на который ссылается Ы. То есть вы могли бы предположить, что Ы и Ь2 относятся к обособленным объектам. Тем не менее, вы бы ошибались. Взамен после выполнения данного фрагмента кода переменные Ы и Ь2 будут ссылаться на тот же самый объект. Присваивание переменной Ь2 значения Ы не привело к выделению памяти или копированию какой-либо части исход­ ного объекта. Оно просто заставляет Ь2 ссылаться на тот же объект, что и Ы. Таким образом, любые изменения, внесенные в объект через переменную Ь2, повлияют на объект, на который ссылается Ы, поскольку это один и тот же объект. Ситуация иллюстрируется на рис. 6.2. �--­ г::L_ width height depth Ь2 Объект Вох Рис. 6.2. Присваивание переменных ссылок на объекты Хотя п еременные Ь 1 и Ь2 относятся к тому же самому объекту, они никак не связаны друг с другом. Например, последующее присваивание переменной Ы значения nul 1 просто отсоединит Ы от исходного объекта, не затрагивая объект и переменную Ь2: Вох Ы = new Вох ( ) ; Вох Ь2 = Ы ; // Ы = nul l ; Глава 6 . В веде ние в классы 1 69 Здесь Ы устанавливается в null, но Ь2 по-прежнему указывает на исход­ ный объект. Помните! Когда вы присваиваете значение одной переменной ссылки на объект другой переменной ссылки на объект, копия объекта не создается, а создается только копия ссылки. Введение в методы В начале главы упоминалось, что классы обычно состоят из двух вещей: переменных экземпляра и методов. Тема методов обширна, потому что J ava наделяет их широкими возможностями и гибкостью. Фактически методам посвящена большая часть следующей главы. Однако есть определенные осно­ вы, которые необходимо усвоить уже сейчас, чтобы приступить к добавлению методов в свои классы. Вот общая форма метода: тип имя ( спис о к-параметров ) / / тело метода Здесь в тип указывается тип данных, возвращаемых методом. Он может быть любым допустимым типом, вкл ючая создаваемые вами типы классов. Есл и метод не возвращает значение, то его возвращаемым типом должен быть void. Имя метода указывается в имя. Это может быть любой законный идентификатор кроме тех, которые уже используются другим и элементами в текущей области видимости. Наконец, список-параметров представляет со­ бой последовательность пар типов и идентификаторов, разделенных запяты­ ми. Параметры, по сути, являются переменными, которые получают значение аргументов, переданных методу при его вызове. Есл и у метода нет параме­ тров, то список параметров будет пустым. Методы, которые и меют тип возвращаемого значения, отл ичающийся от void, возвращают значение вызывающей процедуре с применением следую­ щей формы оператора return: return зна чение ; Здесь значение указывает возвращаемое значение. В последующих нескольких разделах вы увидите, как создавать различные типы методов, в том числе те, которые принимают параметры, и те, которые возвращают значения. Добавление метода в класс Вох Хотя совершенно нормально создавать класс, содержащий толь ко данные, такое случается редко. Большую часть времени вы будете использовать ме­ тоды для доступа к переменным экземпляра, которые определены классом. В действител ь ности методы определяют и нтерфейс для большинства клас­ сов, что позволяет разработчику класса скрыть конкретную реал изацию вну- 1 70 Часть 1 . Яз ык Java тренних структур данных за более привлекател ьными абстракциями методов. Помимо определения методов, обеспечивающих доступ к данным, вы также можете определять методы, которые применяются внутри самого класса. Давайте начнем с добавления метода в к л асс Вох. При просмотре предше­ ствующих программ вам могло при йти в голову, что вычисление объема ко­ робки лучше выполнять классом Вох, а не классом BoxDemo. В конце концов, поскол ьку объем коробки з ависит от раз меров коробки, для его вычисления имеет смысл использовать класс Вох. Тогда в класс Вох потребуется добавить метод, как показ ано ниже: // В этой программе внутрь класса Вох доба вляется метод . class Вох { douЫe widt h ; douЫe heigh t ; douЫ e depth; // Отобразить объем коробки . void vol ume ( ) { Sys tem . out . print ( " Объем равен " ) ; System . out . p r i n t l n ( width * height * depth ) ; class BoxDemoЗ { puЫ i c static void main ( St ring [ ] args ) { Вох myboxl = new Вох ( ) ; Вох mybox2 = new Вох ( ) ; / / Присвоить значения переменным экземпляра mybox l . mybox l . width = 1 0 ; mybox l . height = 2 0 ; myboxl . depth = 1 5 ; / * Присвоить переменным экземпляра mybox2 другие значения . * / mybox2 . width = 3 ; mybox2 . height = 6 ; mybox2 . depth = 9 ; / / Отобразить объем пер вой коробки . myboxl . vol ume ( ) ; / / Отобразить объем второй коробки . mybox2 . vol ume ( ) ; Программа генерирует такой же вывод, как и ее предыдущая версия: Объем равен 3 0 0 0 . 0 Объем равен 1 62 . 0 Внимател ьно взгляните на следующие две строки кода: myboxl . vol ume ( ) ; mybox2 . vol ume ( ) ; В первой строке вызывается метод volume ( ) на myboxl, т.е. volume ( ) вы­ зывается в отношении объекта myboxl с применением имени объекта и опе- Глава 6. Введение в классы 1 71 рации точки. Таким образом, вызов myboxl . vo lume ( ) отображает объем ко­ робки, определенной переменной myboxl, а вызов mybox2 . vo lume ( ) - объем коробки, определенной переменной mybox2. Каждый раз, когда вызывается метод vo l ume ( ) , он отображает объем указанной коробки. Если вы не знакомы с концепцией вызова метода, то следующее обсуж де­ ние поможет прояснить ситуацию. При выполнении вызова myboxl . vo lume ( ) исполняющая среда Java передает управление коду, определенному внутри vo lume ( ) . После выполнения инструкций метода vo l ume ( ) управление воз­ вращается вызывающей процедуре и выполнение возобновляется со строки кода, следующей за вызовом. В самом общем смысле метод представляет со­ бой способ реализации подпрограмм в Java. В методе vo lume () имеется кое-что очень важное, на что следует обратить внимание: ссылка на переменные экземпляра width, height и depth произ­ вод ится напрямую, без предшествующего имени объекта или операции точ­ ки. Когда метод использует переменную экземпляра, определенную его клас­ сом, он делает это напрямую, без явной ссылки на объект и без применения операции точки. Если подумать, то причину понять легко. Метод всегда вы­ зывается в отношении некоторого объекта своего класса. После того как вы­ зов произошел, объект становится известным. Таким образом, внутри метода нет необходимости указывать объект во второй раз. Это означает, что width, height и depth внутри vo lume ( ) неявно относятся к копиям переменных, которые находятся в объекте, на котором вызван метод vo lume ( ) . Итак, еще раз: когда доступ к переменной экземпляра осуществляется ко­ дом, которы й не входит в состав класса, где определена данная переменная экземпляра, то его придется делать через объект с использованием операции то чки. Тем не менее, когда к переменной экземпляра обращается код, кото­ рый входит в состав того же класса, что и переменная экземпляра, то на пере­ менную можно ссылаться напрямую. То же самое относится и к методам. В озвра щ ение зна чения Хотя реализация vo lume ( ) переносит вы числение объема коробки внутрь класса Вох, такой способ нельзя с читать наилучшим. Скажем, а что, если в другой части программы нужно узнать объем коробки, но не отображать его значение? Более удачный подход к реализации vo l ume ( ) предусматривает вы числение объема коробки и возвращение результата вызывающей стороне. Именно так сделано в показанной ниже улучшенной версии предыдущей про­ граммы: / / Теперь vo lume ( ) возвращает объем коробки . class Вох { douЫ e widt h ; douЫe height ; douЫe depth ; / / Вычислить и возвратить объем . douЫ e vol urne ( ) { 1 72 Часть 1 . Язык Java return width * height * depth; class BoxDemo4 ( puЬlic s tatic void main ( St ring [ ] args ) ( Вох myboxl = new Вох ( ) ; Вох mybox2 = new Вох ( ) ; :::louЫe vol ; / / Присвоить значения переменным экземпляра mybox l . mybox l . width = 1 0 ; mybox l . height = 2 0 ; mybox l . depth = 1 5 ; / * Присвоить переменным экземпляра mybox2 другие значения . * / mybox 2 . width = З ; mybox2 . height = 6 ; mybox2 . depth = 9 ; / / Получить объем первой коробки . vol = myboxl . vo l ume ( ) ; System . out . println ( "Объем равен " + vol ) ; / / Получить объем второй коробки . vol = mybox2 . volume ( ) ; System . out . println ( "Oбъeм равен " + vol ) ; Как видите, когда метод volume ( ) вызывается, он помещается в правую часть оператора присваивания. Слева находится переменная, в данном случае vo l, которая получит значение, возвращаемое volume ( ) . Таким образом, по­ сле выполнения оператора: vol = mybox l . volume ( ) ; значением myboxl . vo lume ( ) будет 3 0 0 0, которое и сохранится в vo l. Касательно возвращаемых значений важно понимать два момента. • Тип данных, возвращаемых методом, должен быть совместим с возвра­ щаемы м типом, который указан в методе. Например, если возвращае­ мым типом какого-то метода является bool ean, то возвратить целое число не у дастся. • Тип переменной, которая пол учает значение, возвращаемое методом (vo l в данном случае), тоже должен быть совместим с возвращаемым типом, указанным для метода. И еще одно замечание: предыдущую программу можно написать немного эффективнее, потому что фактически она не нуждается в переменной vol. Вызов volume ( ) можно было бы поместить непосредственно в оператор println ( ) : System . out . println ( "Oбъeм равен " + mybox l . volume ( ) ) ; В таком с лучае при выполнении println ( ) метод mybox l . volume ( ) вы­ зывается автоматически, а ero значение передается в println ( ) . Глава 6. Вв е дение в классы 1 73 Доба влен и е метод а, при н има юще г о па раметры В то время как некоторым методам параметры не нужны, большинству методов они необходимы. Параметры позволяют обобщить метод, т.е. па­ раметризованный метод может работать с разнообразными данными и/или применяться в ряде ситуаций, немного отличающихся друг от друга. Чтобы проиллюстрировать сказанное, давайте воспользуемся очень простым при­ мером. Ниже приведен метод, который возвращает квадрат числа 10: int square ( ) { return 1 0 * 1 0 ; Несмотря на то что метод действительно возвращает значение 1 0 в ква­ драте, его применение крайне ограничено. Однако если вы измените метод square ( ) так, чтобы он принимал параметр, как показано далее, то сделаете его гораздо более полезным: int square ( int i ) { return i * i ; Теперь метод square ( ) будет возвращать квадрат любого значения, с ко­ торым вызывается. Таким образом, s qua re ( ) становится методом общего назначения, который способен вычислять квадрат любого целочисленного значения, а не только 10. Вот пример: int х , у ; х = square ( 5 ) ; ! / х равно 25 х = square ( 9 ) ; // х равно 81 у = 2; х = square ( у ) ; / / х равно 4 В первом вызове square ( ) параметру i передается значение 5. Во втором вызове i получает значение 9. В третьем вызове i передается значение пере­ менной у, которое в данном примере равно 2. Как демонстрируется в приме­ рах, метод square ( ) может возвращать квадрат любых переданных значений. Важно понимать различие между двумя терминами - параметр и аргу­ мент. Параметр - это переменная, определенная методом, которая получает значение при вызове метода. Например, в методе square ( ) параметром яв­ ляется i. Аргумент - это значение, которое передается методу при его вы­ зове. Например, вызов square ( 1 О О ) передает в качестве аргумента значение 1 00, которое получает параметр i внутри square ( ) . С помощью параметризованного метода класс Вох можно усовершенство­ вать. В предшествующих примерах размеры каждой коробки должны были устанавливаться по отдельности с использованием последовательности опе­ раторов следующего вида: myboxl . width = 1 0 ; myboxl . height = 2 0 ; myboxl . depth = 1 5 ; 1 74 Часть 1 . Язык Java Несмотря на то что код работает, он является источником беспокойства по двум причинам. Во-первых, он выглядит неуклюже и подвержен ошибкам. Скажем, довольно легко забыть установить какой-то размер. Во-вторых, в хо­ рошо спроектированных программах на Java доступ к переменным экземпля­ ра должен осуществляться только через методы, определенные их классом. В будущем вы сможете изменить поведение метода, но не сумеете изменить поведение открытой переменной экземпляра. Таким образом, более эффективный подход к установке размеров короб­ ки предусматривает создание метода, который принимает размеры коробки в своих параметрах и соответствующим образом устанавливает каждую пе­ ременную экземпляра. Данная концепция реализована в приведенной далее программе: // В этой программе используется параметризованный метод . class Вох { douЫe width; douЫe height ; douЫe depth ; / / Вычислить и возвратить объем . douЫe vol ume ( ) { return width * hei ght * depth; // Устано вить размеры коробки . voi d setDim ( douЫ e w, douЬl e h, douЫe d ) { width = w; he ight = h ; depth = d ; class BoxDemo S { puЫ ic static vo id main ( S tr ing [ ] args ) { Вох myboxl = new Вох ( ) ; Вох mybox2 = new Вох ( ) ; douЫe vol ; // Инициализировать объекты коробок . myboxl . se tDim ( l 0 , 2 0 , 1 5 ) ; mybox2 . s etDim ( З , 6, 9 ) ; // Получить объем первой коробки . vol = myboxl . volume ( ) ; System . out . println ( " Oбъeм равен " + vol ) ; // Получить объем второй коробки . vo l = mybox2 . vo lume ( ) ; System . out . p r i ntln ( " Oбъeм paвeн " + vol ) ; Как видите, для установки раз меров каждого блока применяется метод setDim ( ) . Гла ва 6. Введение в классы 1 75 На пример, когда выполняется следующий вызов: myboxl . se tDim ( l O , 2 0 , 1 5 ) ; в параметр w копируется значение 10, в параметр h - значение 2 0, а в пара­ метр d - значение 1 5 . Затем вну т ри ме тода se tDim ( ) значения w, h и d при­ сваиваются соответст венно width, height и depth. Многим читателям будут знакомы концепции, представленные в предше­ ст вующих разделах. Тем не менее, если такие понятия, как вызовы методов, аргументы и параметры, являются для вас но выми, тогда имеет смысл выде­ лить некоторо е время на экспериментирование с ними, прежде чем двигат ься дальше. Концепции вызова методов, параметро в и возвращаемых значений являются фундаментальными в программиро вании на Java. Конструкто ры Инициализировать все переменные в классе при каждо м создании его эк­ земпляра может быть утомительно. Даже когда вы добавляете удобные мето­ ды вроде s e tDim ( ) , было бы проще и лаконичнее выполнять всю настройку во время первоначального создания объекта. Поскольку т ребования к ини­ циализации настолько распрост ранены, в Java объекта м разрешено иници­ ализировать себя во время создания. Такая автоматическая инициализация выполняется с помощью конструктора. Конструктор инициализирует объект немедленно после создания. Он име­ ет такое же имя, как у класса, где находи тся, и синтаксически похож на ме­ тод. После определения конструктор автоматически вызывается при созда ­ нии объекта до завершения операции new. Конструкторы выглядят немного странно, пото му что у них нет возвращаемого типа, даже void. Причина в том, что неявным возвра ща емым типом конструктора класса является сам класс. Задача конструктора - инициализировать внутреннее состояние объ­ екта, чтобы код, создающий экземпляр, немедленно получил в свое распо ­ ряжение полност ью инициализированный и пригодный для использования объект. Класс Вох можно переделать, чтобы размеры коробки автоматически ини ­ циализировались при конструировании объекта, заменив метод set Dim ( ) конструктором. Начнем с определения п ростого конструктора, который уста­ навливает одинаковые значения для размеров каждой коробки. Вот как вы­ глядит такая версия: /* Зде сь в Вох испол ь зуется конструктор для инициализации размеров коробки . */ class Вох { douЫe width ; douЫe height ; douЫe dept h ; / / Э т о конструктор для Вох . Вох ( ) { 1 76 Час т ь 1. Язык J ava System . out . printl n ( " Koнcтpyиpo вaниe Вох" ) ; width = 1 0 ; height = 1 0 ; depth = 1 0 ; / / Вычислить и возвратить объем. douЫe volume ( ) [ return width * height * depth ; class BoxDemoб [ puЫ i c static void main ( String [ ] args ) [ // Объявить , разместить в памяти и инициализировать объе кты Вох . Вох myboxl = new Вох ( ) ; Вох mybox2 = new Вох ( ) ; douЬle vol ; / / Получить объем первой коробки . vol = myboxl . volume ( ) ; System . out . println ( "Oбъeм равен " + vol ) ; / / Получить объем второй коробки . vol = mybox2 . volume ( ) ; System . out . println ( "Объем равен " + vol ) ; После запуска программа генерирует следующий вывод: Конструирование Вох Конструирование Вох Объем равен 1 000 . 0 Объем равен 1000 . 0 Как видите, объекты myboxl и mybox2 были инициализированы конструк­ тором Вох ( ) при их создании. Поскольку конструктор дает всем блокам оди ­ наковые размеры, 10 х 10 х 10, объекты коробок, представленные с помощью myboxl и mybox2, будут иметь одинаковый объем. Оператор println ( ) вну­ три Вох ( ) служит только в целях иллюстрации. Большинство конструкторов ничего не отображают. Они просто инициализируют объект. Прежде чем двигаться дальше, давайте еще раз взглянем на операцию new. Вам уже известно, что при размещении объекта в памяти применяется пока­ занная ниже общая форма: переменная-класса = new имя-кла сса ( ) ; Теперь вы понимаете, зачем нужны скобки после имени класса. На самом деле происходит вызов конструктора класса. Таким образом, в следующей строке: Вох myboxl = new Вох ( ) ; фрагмент new Вох ( ) вызывает конструктор Вох ( ) . Если конструктор для класса не определяется явно, тогда компилятор Java создает стандартный Глава 6. Вве д ение в класс ы 1 77 конструктор. Вот почему предыдущая строка кода работала в более ранних версиях класса Вох, в которых конструктор не определялся. При использо­ вании стандартного конструктора все неинициализированные переменные экземпляра будут иметь стандартные значения, которые для числовых типов, ссылочных типов и логических значений равны соответственно нулю, nu l l и fa l s e. Стандартного конструктора часто оказывается достаточно для про­ стых классов, но для более сложных классов он обычно не подходит. После определения собственного конструктора стандартный конструктор больше не применяется. Параметризованные конструкторы Несмотря н а то что конструктор В о х ( ) в предыдущем примере инициа­ лизирует объект Вох, он не особенно полезен - все коробки имеют одинако­ вые размеры. Необходим способ создания объектов Вох различных размеров. Простое решение заключается в добавлении параметров к конструктору. Как вы наверняка догадались, это сделает его гораздо более полезным. Например, в представленной далее версии класса Вох определен параметризованный кон­ структор, который устанавливает размеры коробки в соответствии с указанны­ ми параметрами. Обратите особое внимание на то, как создаются объекты Вох. /* Здесь в Вох использует ся параметризованный конструктор для инициализации размеров коробки . */ class Вох { douЫe widt h ; douЫe height ; douЫe depth ; / / Это конструктор для Вох . Box ( douЫe w , douЫe h , douЫe d ) { width = w ; height = h ; depth = d; / / Вычислить и возвратить объем . douЫe volume ( ) { return width * height * depth; class BoxDemo7 { puЫi c static void mai n ( S tring [ ] args ) { / / Объявить , разме стить в памяти и инициализировать объекты Вох . myboxl = new Box ( l 0 , 2 0 , 1 5 ) ; Вох mybox2 = new Вох ( З , 6 , 9 ) ; douЫe vol ; / / Получить объем первой коробки . vol = myboxl . volume ( ) ; System . out . println ( " Oбъeм равен " + vol ) ; 1 78 Часть 1 . Язык Java // Получить объем второй коробки . vol = mybox 2 . volume ( ) ; System . out . p r intln ( " Oбъeм равен " + vol ) ; Вот вывод, генерируемый программой: Объем равен 3 0 0 □ . О Объем равен 1 62 . 0 Как видите, каждый объект инициализ ируется в соответствии с тем, что указано в параметрах его конструктора. Например, в следующей с троке: Вох myboxl = new Box ( l 0 , 2 0 , 1 5 ) ; при соз дании операцией new объекта конструктору Вох ( ) передаются з наче­ ния 1 0, 20 и 1 5. Таким образом, копии переменных экземпляра width, height и depth объекта myboxl будут содержать значения 1 0, 2 0 и 1 5. Ключевое слово this Иногда метод должен ссылаться н а объект, н а котором о н вызы вается. Для этого в Java определено клю чевое слово this. Его можно использовать внутри любого метода для ссылки на текущий объект, т.е. this всегда будет ссылкой на объект, на котором был выз ван метод. Клю чевое с лово this мож­ но применять вез де, где разрешена ссылка на объект типа текущего класса. Чтобы л у чше понять, на что ссылается this, вз гляните на приведенную ниже версию конструктора Вох () : / / Избыточное исполь зование thi s . Box ( douЫe w , douЫe h , douЫe d ) thi s . width = w ; thi s . height = h ; thi s . depth = d ; Данная версия Вох () работает точно так же, как и предыду щая версия. Применение this избыточно, но совершенно корректно. Внутри Вох () клю­ чевое с лово this всегда будет ссылаться на выз ывающий объект. Хотя з десь this из быточно, оно полезно в других контекстах, один из которых объясня­ ется в следующем раз деле. Сокрытие переменных э кземп ля ра Как вы з наете, в Java з апрещено объявлять две локальные переменные с одинаковыми именами внутри той же самой или объемлющей облас ти ви­ димости. Интересно отметить, что у вас могут быть л окальные переменные, в том числе формальные параметры методов, имена которых совпадают с именами переменных экз емпляра класса. Однако когда л окальная перемен­ ная имеет такое же имя, как у переменной экз емпляра, то локальная пере- Гл а ва 6 . В ведение в кла сс ы 179 менная скрывает переменную экземпляра. Вот почему width, height и depth не использ овались в качестве имен параметров конструктора Вох ( ) внутри класса Вох. Если бы они были выбраны, то имя width, например, ссылалось бы на формальный параметр, скрывая переменную экз емпляра w idth. Хотя обычно проще применять отличающиеся и мена, есть и другой выход из та­ кой ситуаци и. Поскольку клю чевое слово t h i s позволяет ссылаться прямо на объект, его можно использовать для устранения любых конфликтов имен, которые могут возникать между переменными экземпляра и локальными пе­ ременными. Например, вот еще одна версия конструктора Вох ( ) , в которой имена width, height и dep th применяются для параметров, а посредством this организуется доступ к переменным экземпляра с теми же именами: / / Испол ь зо вание this для устранени я конфликтов имен . Box ( douЫe width , douЫe height , douЫ e depth ) { this . width = widt h ; this . he ight = heigh t ; this . depth = depth ; Есть одно предостережение: использование this в таком контексте иногда может сбивать с толку. Некоторые программисты стараются не применять для локальных переменных и формальные параметров такие имена, которые приводят к сокрытию переменных экземпляра. Раз умеется, другие програм­ мисты уверены в обратном: они полагают, что использ ование тех же самых имен делает код яснее и применяют thi s для преодоления сокрытия пере­ менных экземпляра. То, какой подход вы выберете - дело личного вкуса. Сборка мусора Поскольку объекты динами чески размещаются в памяти с помощью опе­ рации new, вас может интересовать, каким образом такие объекты уни чтожа­ ются, и з анимаемая ими память освобождается с целью последующего выде­ ления. В языках, подобных традиционному С + +, динами чески раз мещенные объекты необходимо освобождать вру чную с помощью операци и delete. В Java используется другой подход; освобождение померживается автомати­ чески. Методика, которая позволяет это делать, называется сборкой мусора. Она работает следующим образом: когда ссылок на объект не существует, то с читается, что такой объект больше не нужен, и занимаемая им память может быть освобождена. Нет необходи мости явно уни чтожать объекты. Сборка мусора происходит нерегулярно (если вообще происходит) во время выпол­ нения программы. Она не инициируется просто потому, что существует один или несколько объектов, которые больше не использ уются. Кроме того, в раз ­ ных реализ ациях исполняющей среды Java будут применяться варьирующие­ ся подходы к сборке м усора, но по большей части вам не придется думать о ней при написании своих программ. 1 80 Часть 1 . Язык Java Класс Stack Хотя кл асс Вох полезен для иллюстрации основных элементов кл асса, практической ценности от него мало. Чтобы продемонстрировать реал ьную мощь кл ассов, в завершение главы будет рассмотрен более сложный пример. Как вы помните из обсуждения ООП, представленного в главе 2, одним из са­ мых важных преимуществ ООП является инкапсуляция данных и кода, кото­ рый манипулирует этими данными. Вы видел и, что класс представляет собой механизм, с помощью которого достигается инкапсуляция в J ava. Создавая класс, вы создаете новый тип данных, который определяет как природу обра­ батываемых данных, так и процедуры, используемые для их обработки. Кроме того, методы определяют согласованный и контролируемый интерфейс к дан­ ным класса. Таким образом, вы можете работать с классом через ero методы, не беспокоясь о деталях ero реал изации и л и о том, каким образом проис­ ходит фактическое управление данными внутри класса. В некотором смысле кл асс похож на "механизм обработки данных': Для управления таким меха­ низмом не требуется никаких знаний о том, что происходит внутри него. На самом деле, поскольку детал и скрыты, внутреннюю работу механизма можно по мере необходимости изменять. До тех пор, пока класс эксплуатируется в коде через ero методы, внутренние детал и могут изменяться, не вызывая по­ бочных эффектов за пределами кл асса. Чтобы взглянуть на практическое воплощение предыдущего обс уждения, давайте разработаем один из т иповых примеров инкапсуляци и: стек. Стек сохраняет данные по принципу "последний пришел - первым обслужен'; т.е. стек подобен стопке тарелок на столе: первая тарелка, поставленная на стол, используется последней. Стеки управляются с помощью двух операций, тра­ диционно называемых помещением и извлечением. Чтобы сохранить элемент на верхушке стека, будет применяться помещение. Чтобы взять элемент из стека, будет использоваться извлечение. Как вы увидите, инкапсул ировать полный механизм стека несложно. Ниже показан код кл асса по имени S tack, который реализует стек вмести­ тельностью до десяти целых чисел: // Этот класс реализует стек целых чисел , который может хранить 10 значений class Stack { int [ ] stck = new int [ lO ] ; int tos ; / / Инициализировать верхушку стека . Stack ( ) { tos = - 1 ; / / Поместить элемент в стек . void push ( i nt i tem) { i f ( tos == 9 ) System . ou t . println ( " Cтeк полон . " ) ; else s tck [ ++tos ] = i tem; Гл а ва 6. В ведение в классы 181 // Извлечь элемент из стека . int рор ( ) ( i f ( t os < О ) { Systern . out . println ( "Cтeк опустошен . " ) ; return О ; else ret urn stck [ t os-- ] ; Здесь видно, что в к лассе Stack определены два элемента данных, два ме­ тода и конструктор. Стек целых чисел хранится в массиве stck. Этот массив индексируется перем енной tos, которая всегда содержит индекс верхушки стека. Конструктор Stack ( ) инициализирует tos знач ением - 1 , что указы­ вает на пустой стек. Метод push ( ) помещает элемент в стек. Для получения элемента предназначен метод рор ( ) . Так как доступ к стек у осуществляется через м етоды push ( ) и рор ( ) , тот факт, что стек хранится в массиве, на са­ мом деле не имеет отношения к использованию стека. Например, стек может храниться в более сложной структуре данных, такой как связный список, но интерфейс, определяемый методами push ( ) и рор ( ) , останется прежним. Приведенный дал ее к ласс TestStack демонстрирует работу с классом Stack. В нем создаются два стека целых чисел, после чего в каждый помеща ­ ется несколько значений, которые затем извлекаются. class TestStack { puЫ i c static void rnain ( S tring ( ] args ) { Stack rnystackl = new Stack ( ) ; Stack rnystack2 = new Stack ( ) ; / / Поместить несколько чисел в стеки . for ( i nt i=O ; i < l O ; i++ ) rnystack l . push ( i ) ; for ( i nt i= l O ; i < 2 0 ; i + + ) rnystac k2 . push ( i ) ; / / Извлечь э ти числа из стеков . Systern . out . println ( "Cтeк в rnystackl : " ) ; for ( i nt i=O ; i < l O ; i++ ) Systern . out . println (rnystackl . pop ( ) ) ; S ystern . out . println ( " Стек в rnystack2 : " ) ; for ( int i=O ; i < l O ; i + + ) Systern . out . println (rnystack2 . pop ( ) ) ; Программа генерирует такой вывод: Стек в rnys tackl : 9 в 7 6 5 4 1 82 Часть 1. Язык Java 3 2 1 о Стек в mystack2 : 19 18 17 16 15 14 13 12 11 10 Легко зам етить, что содержимое каждого стека является обособленным. И последнее за мечание, ка сающееся класса Stac k. При его текущей реализации массив, хранящий стек (stck), может быть изменен кодом вне клас са Sta c k. В итоге с уществует риск неправильного использования или повреж­ дения класса Stack. В следующей глав е вы увидите, как устранить проблему. ГЛ А ВА П од р о б ны й анали з методов и классов В этой rлаве продолжается обсуждение методов и классов, которое на­ чалось в предыдущей rлаве. В ней рассматривается несколько связанных с . методами тем, включая перегрузку, передачу параметров и рекурсию. Затем мы снова обратимся к классам и обсудим управление доступом, использова­ ние ключевого слова static и работу с одним из самых важных встроенных классов Java: String. П ере грузка методов Язык Java разрешает определять в одном классе два и более метода, ко­ торые имеют одно и то же имя, если их объявления параметров отличаются. В таком ел учае говорят, что методы перегружены, а сам процесс называется перегрузкой методов. Перегрузка методов - один из способов помержки по­ лиморфизма в Java. Если вы никогда не имели дело с языком, допускающим перегрузку методов, то поначалу эта концепция может показаться странной. Но, как вы увидите, перегрузка методов относится к самым захватывающим и полезным возможностям Java. При вызове переrруженноrо метода компилятор Java использует тип и/или количество аргументов в качестве ориентира, чтобы определить, какую вер­ сию переrруженноrо метода фактически вызывать. Таким образом, перегру­ женные методы должны отличаться типом и/или количеством параметров. Хотя перегруженные методы моrут возвращать разные типы, одного типа возвращаемого значения недостаточно, чтобы различить две версии метода. Коrда компилятор Java встречает вызов переrруженноrо метода, он просто выполняет версию метода, параметры которой соответствуют аргументам, указанным в вызове. Ниже приведен простой пример, иллюстрирующий перегрузку методов: / / Демонстрация перегрузки методов . class OverloadDemo { void test ( ) { System . out . println ( "Пapaмeтpы отсутствуют " ) ; 1 84 Часть 1 . Язык Java // Перегрузить test ( ) для одного целочисленного параметра . void test ( int а ) { System . out . println ( " a : " + а ) ; // Перегрузить test ( ) для двух целочисленных параметров . void test ( int а , i nt Ь ) { System . out . println ( "a и Ь : " + а + " " + Ь ) ; // Перегрузить test ( ) для одного параметра типа douЫ e . douЫe test (douЫe а ) { System . out . println ( "douЫe а : " + а ) ; return а * а ; class Ove rload { puЫ i c static void main ( St ring [ ] args ) { Ove rl oadDemo оЬ = new Overl oadDemo ( ) ; douЫe res u l t ; / / Вызвать все версии test ( ) . ob . te s t ( ) ; ob . te s t ( l 0 ) ; ob . te s t ( l 0 , 2 0 ) ; res u l t = ob . test ( l 2 3 . 2 5 ) ; System . out . println ( " Peзyльтaт вызова ob . test ( l 2 3 . 2 5 ) : " + re su l t ) ; Программа сгенерирует следующий вывод: Параметры отсутствуют а : 10 а и Ь: 10 20 douЫe а : 1 2 3 . 2 5 Результат вызова ob . test ( l 2 3 . 2 5 ) : 1 5 1 9 0 . 5 6 2 5 Как видите, метод t e s t ( ) перегружается ч етыре раза. Первая версия не принимает параметров, вторая принимает один целочисленный пара­ метр, третья - два целочисленных параметра, а четвертая - один параметр douЫ e. Тот факт, что четвертая версия test ( ) также возвращает значение, не имеет значения для перегрузки, поскольк у возвращаемые типы не играют роли в распознавании перегруженных методов. При вызове перегруженного метода компилятор Java ищет соответствие между аргументами, используемыми для вызова метода, и параметрами мето­ да. Однако это совпадение не всегда должно быть точным. В некоторых случаях автоматическое преобразование типов в Java может играть роль в распознава­ нии перегруженных методов. Например, взгляните на следующую программу: // При перегрузке применяется автоматическое преобразование типов . class Ove rl oadDemo { void test ( ) { System . out . println ( " Пapaмeтpы отсутствуют " ) ; Глава 7. П одро бн ы й а н а л и з методов и к л ассов 1 85 / / Пере грузить test ( ) для двух целочисленных параметров . void test ( int а , int Ь ) { Syst em . out . println ( 11 a и Ь : 11 + а + 11 11 + Ь ) ; / / Перегрузить test ( ) для одного параметра типа douЫ e . void test ( douЫe а ) { System . out . println ( 11 Bнyтpи test ( douЬle ) а : " + а ) ; class Overload { puЬlic static void main ( String [ ] args ) { OverloadDemo оЬ = new OverloadDemo ( ) ; int i = 8 8 ; ob . test ( ) ; ob . test ( l 0 , 2 0 ) ; // будет вызываться tes t ( douЫ e ) ob . test ( i ) ; // будет вызываться tes t ( douЬ l e ) ob . test ( l2 3 . 2 ) ; Вот вывод, генерируемый программой: Параметры о тсутствуют а и Ь : 10 2 0 Внутри test ( douЬ l e ) а : 8 8 . О Внутри test ( douЫ e ) а : 123 . 2 Легко заметить, что в данной версии OverloadDemo метод test (int) не определен. Тогда при вызове test () с цело численным аргументом внутри Overload подходящая версия метода не будет найдена. Тем не менее, ком­ пилятор Java способен автоматически преобразовывать целое число в число типа douЫe, и такое преобразование можно и спользовать для распознавания вызова. Поэтому после того, как test ( int) не найден, компилятор Java повы­ шает i до douЫe и затем вызывает test (douЬle ) . Конечно, если бы версия test (in t) была определ ена, то и менно она была бы вызвана. Компилятор Java задействует свои автомати ческие преобразования типов только при от­ сутствии точного совпадения. П ерегрузка методов поддерживает полиморфизм, т.к. он представляет со­ бой один из способов, которым в Java реализуется парадигма "один интер­ фейс, несколько методов': Давайте выясним, каким образом. В языках, не поддерживающих перегрузку методов, каждому методу должно быть назна­ чено уникальное имя. Однако часто вам потребуется реализовать по суще­ ству один и тот же метод для разных типов данных. Рассмотрим функци ю для абсолютного значения. В языках, н е померживающих перегрузку, обы чно существует три или более версий такой функции, каждая из которых и меет слегка отличающееся имя. Например, в языке С функция abs () возвращает абсолютное значение целого числа, labs () - абсолютное значение длинного целого числа, а fabs () - абсолютное значение значения с плавающей то ч­ кой. Поскольку перегрузка в С не поддерживается, каждая функция имеет 1 86 Часть 1 . Язык Java собственное и мя, хотя все три функции выполняют, по сути, одну и ту же работу, что делает ситуацию концептуал ьно более сложной, чем она есть на самом деле. Хотя базовая концепция каждой функции одна и та же, вам нуж­ но запомнить все три имени. В языке Java ситуация подобного рода не возни­ кает, потому что каждый метод для абсолютного з начения может иметь одно и то же имя. Действительно, стандартная библ иотека классов Java включает метод получения абсолютного з начения, называемый abs () . Этот метод пе­ регружен в классе Math для обработки всех числовых типов. Компилятор Java определяет, какую верси ю abs () вызывать, основываясь на типе аргумента. Ценность перегруз ки обусловлена тем, что она позволяет получить доступ к связ анным методам с применением общего имени. Таким образом, имя abs представляет выполняемое общее действие. Выбор правильной конкретной версии в сложившихся обстоятельствах возлагается на компилятор. Вам как программисту достаточно лишь запомнить общую выполняемую операцию. Бла годаря полиморфизму несколько имен был и сведены в одно. Хотя приведен­ ный пример довольно прост, если вы расширите концепцию, то увидите, каким образом перегрузка мож ет помочь справиться с более высокой сложностью. В случае перегруз ки метода каж дая его версия может выполнять любые желаемые действия. Нет правила, утверждающего о том, что перегруженные методы дол жны быть связ аны друг с другом. Тем не менее, со стилистиче­ ской точки з рения перегруз ка методов подразумевает наличие вз аимоотно­ шения между ними. Таким образ ом, хотя и допускается использ овать одно и то же имя для перегруз ки н е связ анных методов, вы не должны поступать так. Скажем, вы можете выбрать имя sqr при создании методов, возвращающих квадрат целого числа и квадратный корень з начения с плавающей запятой. Но эти две операции принципиально раз ные. Применение перегруз ки метода в подобной манере противоречит его первоначальной цел и. На практике сле­ дует п ерегружать только тесно связ анные операци и. Перегрузка конструкторов Помимо перегруз ки обычных методов вы также можете перегружать ме­ тоды конструкторов. Фактически для большинства создаваемых вами реаль­ ных классов перегруженные конструкторы будут нормой, а не исключением. Чтобы выяснить причину, вернемся к классу Вох, разработанному в предыду­ щей главе. Ниже представлена последняя версия Вох: class Вох { douЫe widt h ; douЫe height ; douЫe depth; / / Это конструктор для Вох . Box ( douЫe w , douЫe h , douЫe d ) { width = w ; he i ght = h ; depth = d ; Гл а ва 7 . П одро б ный ана л из м етодов и классов 1 87 / / Вычислить и возвратить объем . douЫe volume ( ) { return width * height * depth; Как ви дите, конструктор Вох ( ) требует трех параметров, т.е. все объяв­ л ения объектов Вох должны передавать конструктору Вох ( ) три аргумента. Например, сл едующи й оператор в настоящее время недопустим: Вох оЬ = ne1-1 Вох ( ) ; Поскольку вызов Вох ( ) требует передачи трех аргументов, вызов без них приведет к ошибке. В результате возникает ряд важных вопросов. Что, есл и вам просто необходим объект коробки, первоначальные размеры которой не важны (или не известны)? Или что, если вы хотите иметь возможность ини­ циал изировать объект кубика, указав только одно значение, которое будет использоваться для всех трех измерений? В том виде, в каком сейчас записан класс Вох, такие варианты совершенно не доступны. К счас тью, решить упомянутые проблемы довольно просто: нужно лишь перегрузить конструктор класса Вох, чтобы он обрабатывал о писанные выше ситуаци и. Н и же при ведена программа, содержащая усовершенствованную версию Вох, которая именно это и делает: / * Здесь в классе Вох определены три конструктора для инициализации размеров объекта коробки ра зличными способами . */ class Вох { douЫe width ; douЫe height ; douЫe depth; // Конструктор , исполь зуемый в случае указания всех размеров . Box ( douЫe w , douЫe h , douЫe d ) { width = w ; height = h ; depth = d ; / / Конструктор , применяемый в случае, если размеры вообще н е указаны . Вох ( ) { width = - 1 ; / / исполь зовать - 1 для обозначения height = - 1 ; / / неинициализированного // объекта коробки depth = - 1 ; } // Конструктор , исполь зуемый в случае создания объекта кубика . Вох ( douЫe len ) w idth = height = depth = l e n ; } / / Вычислить и возвратить объем . douЫe vol ume ( ) { return width * height * depth ; 1 88 Часть 1 . Язык Java class Ove rloadCons { puЬl i c static void main ( S tring [ ] args ) { / / Создать объекты коробок с приме не нием различных конструкторов . Вох myboxl = new Box ( l 0 , 2 0 , 1 5 ) ; Вох mybox2 = new Вох ( ) ; Вох mycube = new Вох ( 7 ) ; douЫe vol ; / / Вычислить объем первой коробки . vol = mybox l . vol ume ( ) ; Sys tem . out . println ( "Объем myboxl равен " + vol ) ; / / Вычислить объем второй коробки . vol = mybox2 . vol ume ( ) ; System . out . println ( "Oбъeм mybox2 равен " + vol ) ; / / Вычислить объем кубика . vol = mycube . volume ( ) ; System . out . printl n ( "Oбъeм mycube равен " + vol ) ; Вот вывод, генерируемый программой: Объем myboxl равен 3 0 0 0 . 0 Объем mybox2 равен - 1 . 0 Объем mycube равен 3 4 3 . 0 Как видите, на основе аргументов, указанных при выпол нении new, вызы­ вается надлежащий перегруженный конструктор. Испол ьзование объ ектов в качестве пара м етров До сих пор в качестве параметров методов применялись только простые типы. Однако передача объектов методам является правил ьной и распро­ страненной практикой. Например, возьмем показанную далее короткую про­ грамму: // Объекты можно передавать методам . class Test { int а , Ь ; Test ( i nt i , int j ) { а = i; ь = j; / / Возвратить t rue , если объект о равен вызывающему объекту . boolean equalTo ( Test о ) { i f ( o . a == а & & о . Ь == Ь ) return true ; else return fa l s e ; class Pas sOb { puЬlic s tatic vo id ma in ( S tr ing [ ] args ) { Test оЫ = new Test ( l 0 0 , 2 2 ) ; Test оЬ2 = new Test ( l 0 0 , 2 2 ) ; Test оЬ3 = new Test ( - 1 , - 1 ) ; Глава 7 . П одробн ый анализ м етодов и классов System . out . println ( "oЫ System . out . println ( " oЫ 1 89 оЬ2 : " + oЫ . equalTo ( ob2 ) ) ; оЬЗ : " + оЫ . equal То ( оЬЗ ) ) ; Программа ген ерирует следующий вывод: оЫ оЫ == оЬ2 : true == оЬ З : fal s e Как видите, метод equalTo ( ) внутри Test сравнивает два объекта на предмет равенства и возвращает результат, т.е. он сравнивает вызывающий объект с тем , который ему передается. Если они содержат одинаковые значе­ ния, тогда метод возвращает true. В противном случае возвращается fal se. Обратите внимание, что параметр о в equalTo ( ) указывает Test в качестве своего типа. Хотя Test - тип класса, созданный программой, он использует­ ся точно так же, как и встроенные типы Java. Одно из наибол ее распростран енных применений параметров объекта связано с конструкторами. Часто требуется создать новый объект так , чтобы он изначально был таким же, как какой-то существующий объект. Для это­ го необходимо определить конструктор, который принимает объект своего класса в качестве параметра. Например, следующая версия Вох позволяет од­ ному объекту инициализировать другой: // Здесь класс Вох позволяет один объе кт инициализировать другим . class Вох { douЫe wi dth ; douЫe heigh t ; douЫe depth ; / /Обратите внимание на этот конструктор, который принимает объект типа Вох Вох ( Вох оЬ) { / / переда ть объект конструктору wi dth = ob . width ; height = ob . height; depth = ob . depth; / / Конструктор , используемый в случае указания всех размеров . Box ( douЫe w, douЫe h , douЫe d) { width = w; height = h ; depth = d; / / Конструктор , применяемый в случае , если ра змеры вообще не указаны . Вох ( ) { / / исполь зовать - 1 для обозначения width = - 1 ; // неинициализированного height = - 1 ; // объекта коробки depth = - 1 ; // Конструктор , исполь зуемый в случае созда ния объе кта куби ка . Box ( douЫe l e n ) { width = he ight = depth = l en ; 1 90 Ч ас ть 1. Язык Java / / Вычисли ть и возвратить объем . douЫ e vol ume ( ) { return width * height * depth; class Over loadCons2 { puЫ i c static void main ( St ring [ ] args ) { / / Создать объекты коробок с приме нением различных конструкторов . Вох myboxl = new Box ( l 0 , 2 0 , 1 5 ) ; Вох mybox2 = new Вох ( ) ; Вох mycube = new Вох ( 7 ) ; Вох myclone = new Вох (mybox l ) ; / / создать копию объекта mybox l douЫe vol ; / / Вычислить объем первой коробки . vol = myboxl . vo lume ( ) ; System . out . println ( " Oбъeм mybox l равен " + vol ) ; // Вычислить объем вт орой коробки . vol = mybox2 . volume ( ) ; System , out . printl n ( "Oбъeм mybox2 равен " + vol ) ; / / Вычислить объем кубика . vol = mycube . volume ( ) ; System . out . p rintln ( "Oбъeм mycube равен " + vol ) ; / / Вычислить объем копии . vol = mycl one . vol ume ( ) ; System . out . println ( " Oбъeм копии равен " + vo l ) ; Начав создавать собственные классы, вы увидите, что для удобного и эф­ фективного создания объектов обычно требуется множество форм конструк­ торов. П одро б ны й а н а лиз передач и аргументов Говоря в общем, в языках программирования существуют два способа, ко­ торыми можно передавать аргумент подпрограмме. Первый способ - вызов по значению, при котором в формальный параметр подпрограммы копируется значение аргумента, поэтому изменения, вносимые в параметр подпрограм­ мы, не влияют на аргумент. Второй способ - вызов по ссылке. При таком под­ ходе в параметр передается ссылка на аргумент (а не его значение). Внутри подпрограммы эта ссылка используется для доступа к фактическому аргу­ менту, указанному в вызове, т.е. изменения, вносимые в параметр, повлияют на аргумент, который применялся пр и вызове подпрограммы. Вы увидите, что хотя в Java для передачи всех аргументов используется вызов по значе­ нию, точный результат зависит от того, какой тип передается - примитив­ ный или ссылочный. Когда методу передается примитивный тип, то происходит передача по значению. Таким образом, создается копия аргумента, и все то, что делается с параметром, получающим аргумент, не имеет никакого эффекта вне метода. Глав а 7 . П од р обны й анализ методов и классов 1 91 Например, рассмотрим следующую программу: / / Примитивные типы передаются по значению . class Test { void meth ( int i , int j ) { i *= 2 ; j /= 2 ; class Call ByValue { puЫic s tatic void main ( String [ ] args ) { Test оЬ = new Test ( ) ; int а = 1 5 , Ь = 2 0 ; System . out . println ( " a и Ь перед вызовом : " + а + " " + Ь) ; ob . meth ( a , Ь ) ; System . out . println ( " a и Ь после вызова : " + а + " " + Ь) ; Вот вывод из программы: а и Ь перед вызовом : 1 5 2 0 а и Ь после вызова : 1 5 2 0 Как видите, операции, выполняемые внутри meth ( ) , н е влияют н а значения а и Ь, используемые в вызове; их значения здесь не изменились на 3 0 и 1 0 . Когда методу передается объект, ситуация кардинально меняется, потому что объекты передаются посредством того, что фактически называется вы­ зовом по ссылке. Имейте в виду, что при создании переменной типа класса создается только ссылка на объект. Таким образом, когда такая ссылка пере­ дается методу, то п араметр, который ее получает, будет ссылаться на тот же объект, на который ссылается аргумент. Фактически это означает, что объ­ екты действуют так, будто они передаются методам с помощью вызова по ссылке. Изменения объекта внутри метода влияют на объект, указанный в качестве аргумента. Например, взгляните на показанную ниже п рограмму: / / Объекты передаются через ссьщки на них . class Test { int а , Ь ; Tes t ( int i , int j ) { а = i; ь = j; / / Передать объект . void meth ( Test о ) { о . а *= 2 ; о . Ь /= 2 ; 1 92 Часть 1. Язык Java class PassObj Re f { puЫ i c static void ma i n ( String [ ] args ) { Test оЬ = new Test ( 1 5 , 2 0 ) ; System . out . printl n ( " ob . a и оЬ . Ь перед вызовом : " + оЬ . а + " " + оЬ . Ь ) ; ob . meth ( ob } ; System . out . p rintln ( " ob . a и оЬ . Ь после вызова : " + оЬ . а + " " + оЬ . Ь ) ; Программа генерирует следующий вывод: оЬ . а и оЬ . Ь перед вызовом : 1 5 2 0 оЬ . а и оЬ . Ь после вызова : 3 0 1 0 Несложно заметить, что в данном случае действия внутри meth ( ) воздей­ ствуют на объект, используемый в качестве аргумента. Помните! При передаче методу ссылки на объект сама ссылка передается с применением вызова по значению. Но поскольку передаваемое значение относится к объекту, копия этого значения по-прежнему будет ссылаться на тот же объект, что и соответствующий аргумент. Возвращение об ъ ектов Метод способен возвращать данные л ю бого типа, вкл ючая типы клас­ сов, которые вы создаете. Например, в приведенной далее программе метод incrByTen ( ) возвращает объект, в котором значение а на 1 О больше, чем в вызывающем объекте. / / Возвращение объекта . class Test { i nt а ; Test ( i nt i } а = i; Test i n crByTen ( ) Test temp = new Test ( a+ l 0 ) ; return temp ; class RetOb { puЬ l i c s tatic voi d ma in ( S tring [ ] args } { Test оЫ = new Test ( 2 ) ; Test оЬ2 ; оЬ2 = oЫ . i ncrByTen ( ) ; System . o ut . println ( " oЫ . a : " + оЫ . а ) ; Sys tem . out . println ( " ob2 . a : " + оЬ2 . а ) ; оЬ2 = ob2 . i ncrByTen ( ) ; System . o ut . println ( " ob2 . a после второго увеличения : " + оЬ2 . а ) ; Гла ва 7. Подробный анализ методов и классов 1 93 Вот вывод, генерируемый программой: оЫ . а : 2 оЬ2 . а : 1 2 оЬ2 . а после в торого увеличения : 2 2 Как видите, каждый раз, когда вызывается incrByTen () , создается новый объект, а вызывающей процедуре возвращается ссылка на него. В предыдущей программе продемонстрирован еще один важный момент: поскольку все объекты динамически размещаются с помощью операции new, вам не нужно беспокоиться о том, что объект выйдет за пределы области видимости, т.к. метод, в котором он был создан, завершается. Объект будет продолжать существовать до тех пор, пока в программе где-то есть ссылка на него. Когда ссылки на него исчезнут, зани маемая объектом память освобо­ дится при очередной сборке мусора. Рекурсия В языке Java померживается рекурсия - процесс определения чего-либо в терминах самого себя. Что касается программирования на Jav a , то рекурсия является характерной чертой, позволяющей методу вызывать самого себя. Метод, который вызывает сам себя, называется рекурсивным. Классическим примером рекурсии считается вычисление факториала числа. Факториал числа N - это произведение всех целых чисел от 1 до N. Например, факториал 3 равен 1 х 2 х 3, или 6. Рассмотрим, как можно вычис­ ли ть факториал с помощью рекурсивного метода: / / Простой пример использования рекурсии . class Fa ctorial { / / Рекурсив ный метод . int fact ( i nt n ) { int result; i f ( n == l ) return 1 ; result = fact ( n - 1 ) * n ; return resul t ; class Recursion { puЫ ic s t a t i c vo id main ( S tring [ ] args ) { Fa cto rial f = new Factorial ( ) ; System . ou t . println ( " Фaктopиaл 3 равен " + f . fact ( З ) ) ; System . out . p rintln ( " Фaктopиaл 4 равен " + f . fact ( 4 ) ) ; S ystem . out . p rintln ( "Фaктopиaл 5 равен " + f . fact ( S ) ) ; Ниже показан вывод, генерируемый программой: Факториал 3 равен 6 Факториал 4 равен 2 4 Фа кториал 5 равен 1 2 0 1 94 Часть 1. Язык Jаvа Если вы не знакомы с рекурсивными методами, то работа fact () может показаться немного з апутанной. Давайте проясним детали. Когда fact () вы­ з ывается с аргументом 1, функция возвращает 1; в противном случае воз вра­ щается произ ведение fact (n- 1 ) *n. Для вы числения такого выражения вы­ зывается fact ( ) с параметром n - 1 . Процесс повторяется до тех пор, пока з начение n не станет равным 1 и не начнется воз врат из вызовов метода. Чтобы лучше понять, как функционирует метод fact ( ) , рассмотрим не­ большой пример. При вы числении факториала 3 первый выз ов fact ( ) при­ водит ко второму вызову с аргументом 2 , а тот в с вою о чередь - к третьему вызову fact ( ) с аргументом 1. Третий выз ов воз вратит значение 1, которое з атем умножается на 2 (значение n во втором вызове). Результат (равный 2 } з атем воз вращается в исходный вызов fact () и умножается на 3 (первона­ чальное з начение n), что дает ответ 6. Возможно, вам будет интересно вста­ вить операторы p r i n t l n ( ) в fact ( ) , которые будут показ ывать, на каком уровне находится к аждый вызов и каковы промежуточные ответы. Когда метод выз ывает сам себя, новые локальные переменные и параме­ тры размещаются в стеке, а код метода выполняется с этими новыми пере­ менными с самого начала. При воз врате из каждого рекурсивного выз ова ста­ рые локальные переменные и параметры удаляются из стека, и выполнение возобновляется в точке вызова внутри метода. Можно сказ ать, что рекур­ сивные методы работают в стиле "раз д вижения" и "складывания" подзорной трубы. Рекурсивные версии многих подпрограмм могут выполняться нем ного медленнее своих итеративных эквивалентов из -з а добавочных накладных расходов на дополнительные вызовы методов. Большое количество рекур­ сивных вызовов метода может привести к переполнению стека. Поскольку хранилище для п араметров и локальных переменных находится в стеке, и каждый новый выз ов создает новую копию этих переменных, вполне воз мож­ но, что стек ис черпается. В таком случае исполняющая среда Java инициирует исклю чение. Тем не менее, как правило, проблема не возникает, если рекур­ сивная подпрограмма не выходит из-под контроля. Главное преимущество рекурсивных методов связано с тем, что их можно использовать для создания более ясных и простых версий ряда алгоритмов, чем их итеративные аналоги. Скажем, алгоритм быстрой сортировки доволь­ но сложно реализ овать итеративным способом. Кроме того, некоторые типы алгоритмов из области искусственного интеллекта проще всего реализовать с помощью рекурсивных решений. При написании рекурсивных методов вы должны где-то предусмотреть оператор i f, чтобы з аставить метод выполнить воз врат без рекурсивного вызова. Если вы этого не сделаете, то после вызова возврат из метода никог­ да не произойдет. Такая ошибка весьма распространена, когда приходится иметь дело с рекурсией. Свободно применяйте операторы println () во вре­ мя разработки, чтобы вы могли следить за происходящим и прерывать вы­ полнение, если видите, что допустили ошибку. Глава 7. П одроб н ы й а н ал и з мет одов и к л ассов 1 95 Вот еще один пример рекурсии. Рекурсивный метод printAr ray ( ) выво­ дит первые i элементов в массиве val ues. // Еще один пример исполь зования ре курсии . class Re cTest { int [ ] values ; Re cTest ( i nt i ) values = new in t [ i ] ; / / Рекурсивно отобразить элементы массива . voi d printArray ( i nt i ) { i f ( i==O ) retu r n ; else pri ntArray ( i - 1 ) ; Sys tem . out . println ( " [ " + ( i - 1 ) + " ] " + va lues [ i - 1 ] ) ; class Recu rsion2 puЬlic static voi d main ( String [ ] args ) { Re cTest оЬ = new Re cTest ( l O ) ; int i ; for ( i=O ; i < l O ; i ++ ) ob . values [ i ] = i ; ob . p rintArray ( l O ) ; Программа генерирует следующий вывод: [О] О [1] 1 [2] 2 [3] 3 [4] 4 [5] 5 [6] 6 [7] 7 [8] 8 [9] 9 В веден и е в уп равление до с тупом Как вам уже известно, инкапсуляция связывает данные с кодом, который ими манипулирует. Однако инкапсуляция предоставляет еще одно важное средство: управление доступом. С помощью инкапсуляции вы может е контроли­ ровать то, какие части программы могут обращаться к членам класса. Управляя доступом, можно предот вратить неправильную эксплуатацию. Например, за счет разрешения доступа к данным только через четко определенный набор методов вы можете предот вратить неправом ерное использование этих дан­ ных. Таким образом, при правильной реализации класс создает "черный ящик'; с которым можно взаимодействоват ь, но нарушить его внутреннюю работу не удастся. Тем не менее, представленные ранее классы не полностью от вечают 1 96 Часть 1. Язык Java такой цели. Например, возьмем класс S t a c k, показанный в конце главы 6. Хотя методы push ( ) и рор ( ) действительно обеспечивают управляемый интерфейс для стека, данный интерфейс не применяется принудительно, т.е. другая часть программы может обойти указанные методы и обратиться к стеку напрямую. Разумеется, в неумелых руках это может привести к неприятностям. В насто­ ящем разделе вы ознакомитесь с механизмом, с помощью которого сможете точно управлять доступом к различным членам класса. Доступ к члену определяется модификатором доступа, присоединенным к ero объявлению. Язык Java предлагает богатый набор модификаторов до­ ступа. Некоторые аспекты управления доступом в основном связаны с на­ следованием или пакетами. (Пакет по существу представляет собой группу классов.) Эти части механизма управления доступом в Java будут обсуждать­ ся в последующих главах. Давайте начнем с изучения управления доступом применительно к одному классу. Как только вы поймете основы управления доступом, остальное дастся легко. На заметку! Средство модулей, добавленное в JDK 9, тоже может влиять на доступность. Модули будут описаны в главе 1 б. Модификаторами доступа Java являются puЫ i c (открытый), private (за­ крытый) и protected (защищенный). В Java также определен стандартный уро­ вень доступа. Модификатор доступа protected применяется, только когда за­ действовано наследование. Другие модификаторы доступа описаны далее. Давайте начнем с определения puЫ i c и pri vate. Когда член класса изме­ няется с помощью puЫ i c, доступ к нему может получать любой другой код. Korда член класса указан как pr i v а te, доступ к нему могут получ ать только другие члены этого класса. Теперь вы понимаете, почему объявлению метода ma i n ( ) всегда предшествовал модификатор puЫ i c. Он вызывается кодом, находящимся вне программы, т.е. исполняющей средой Java. Если модифика­ тор доступа не задействован, то по умолчанию член класса является откры­ тым в своем пакете, но к нему нельзя получить доступ за пределами пакета. (Пакеты обсуждаются в главе 9.) В разработанных до сих пор классах все члены класса использовали стан­ дартный режим доступа. Однако это не то, что вы обычно хотели бы иметь. Как правило, вам нужно ограничить доступ к членам данных класса, разре­ шив доступ только через методы. Кроме того, будут случаи, когда вы пожела­ ете определить методы, которые являются закрытыми для класса. Модификатор доступа предшествует остальной части спецификации типа члена. Другими словами, с него должен начинаться оператор объявления чле­ на. Вот пример: puЫ i c int i ; private douЫe j ; private int myMe thod ( i nt а , char Ь) { // • • • Чтобы понять влияние открытого и закрытого доступа, рассмотрим следу­ ющую программу: Глава 7 . Подробный ана л и з методов м классов 1 97 / * В этой программе демонстрируется отличие между puЫ i c и p rivate . */ class Test { / / стандартный доступ int а ; // открытый доступ p uЫ i c int Ь ; / / закрытый доступ p rivate int с ; / / методы для доступа к с void setc ( i nt i ) { / / устано вить значение с с = i; int getc ( ) return с ; / / получить значение с class Acce ssTest { p uЫ i c stati c voi d ma i n ( Stri ng [ ] args ) { Test оЬ = new Test ( ) ; / / Поступат ь так законно , т . к . к членам а и Ь разрешен прямой досту п . оЬ . а = 1 0 ; оЬ . Ь = 2 0 ; / / Поступать так нел ь з я , т . к . возникнет ошибка . / / оЬ . с = 1 0 0 ; / / Ошибка ! / / Получать доступ к члену с необходимо через его методы . / / нормаль но ob . setc ( l 0 0 ) ; System . out . println ( " a , Ь и с : " + оЬ . а + " " + оЬ . Ь + " " + ob . getc ( ) ) ; Как видите, внутри класса Test применяется стандартный дост уп, что в данном примере равнозначно указанию puЫ ic. Член Ь явно определен как puЫ ic, а член с - как private. Это означает, что доступ к члену с в коде за пределами его класса невозможен. Таким образом, внутри класса AccessTest член с нельзя использовать напрямую. Доступ к нему должен осущес твлять­ ся через его открытые методы: setc ( ) и getc ( ) . Если бы вы удалили символ комментария в начале следующей строки: / / оЬ . с = l О О ; / / Ошибка ! то не смогли бы скомпилировать программу из-за нарушения прав доступа. Чтобы увидеть, как применить управление доступом к более реальному примеру, рассмотрим показ анную в конце главы 6 усовершенствованную вер­ сию класса Stack: // Этот класс реализует стек целых чисел , который может хранить 1 0 значений class Stack { / * Теперь stck и tos являются за крытыми . Это значит , что они не могут быть случайно или злонамеренно изменены та ким образом, что может повредиться сте к . */ 1 98 Ч а сть 1 . Язык Java private int [ ] stck = new int [ l O ] ; private int tos ; / / Инициализировать верхушку стека . Stack ( ) { tos = - 1 ; / / Поместить элемент в стек . void push ( in t i tem) { i f ( tos== 9 ) System . out . printl n ( "Cтeк полон . " ) ; else stck [ ++tos ] = item; } / / Извлечь элемент из сте ка . int рор ( ) { i f ( tos < О ) { System . out . p rintl n ( " Cтeк опустошен . " ) ; return О ; else return stck [ t os-- ] ; Здесь видно, что и член stck, который хранит стек, и член tos, представ­ ляющий и ндекс верхушки стека, определены как private, т.е. к ним нельзя получить доступ либо из менить их, кроме как с помощью push ( ) и рор ( ) . Превращение tos, например, в закрытый член приводит к тому, что другие части программы не смогут непреднамеренно установить для него з начение, выходящее за пределы массива stck. В следующей программе демонстрируется использование усовершенство­ ванного класса Stack. Попробу йте удалить з акомментированные строки, что­ бы удостовериться в том, что члены stck и tos действительно недоступны. class TestStack { puЫ i c static void ma in ( St ring [ ] args ) { Stack mys tackl = new Stack ( ) ; Stack mystack2 = new Stack ( ) ; / / Поме стить несколько чисел в стеки . for ( int i =O ; i < l O ; i + + ) mys tac kl . push ( i ) ; for ( int i= l O ; i <2 0 ; i++ ) mystack2 . pu s h ( i ) ; / / Извлечь эти числа из стеков . System . out . p rintln ( "Cтeк в mystackl : " ) ; fo r ( int i=O ; i < l O ; i++ ) System . out . print l n ( mystackl . pop ( ) ) ; Sys tem . out . println ( " Cтeк в mystack2 : " ) ; for ( int i =O ; i < l O ; i++ ) System . out . print l n (mystack2 . pop ( ) ) ; / / При веденные далее операторы я вляются недопустимыми . / / mys tackl . to s = - 2 ; / / mystack2 . s tck [ З ] = 1 0 0 ; Гл ава 7. Подробн ы й а нализ методов и классов 1 99 Хотя методы обычно обеспечивают доступ к данным, которые определены в классе, так бывает не всегда. Совершенно правильно позволить перем ен­ ной экземпляра быть открытой, когда на то имеется веская причина. Скажем, большинство несложных классов в этой книге ради простоты были созданы без особого беспокойства об управл ении доступом к переменным экземпля­ ра. Тем не м енее, в большинстве реальных кл ассов необходимо разрешать выполнение операций с данными только через методы. В следующей главе мы вернемся к теме управл ения доступом. Вы узнаете, что оно крайне важно при реализации насл едования. Клю ч е во е с лово static Временами вам понадобится определять чл ен класса, который будет при­ меняться независимо от любого объекта данного класса. Обычно доступ к члену класса должен осуществляться только в сочетании с объектом его класса. Однако можно создать член, который можно использовать сам по себе, без привязки к конкретному экземпляру. Чтобы создать такой элем ент, перед его объявл ением следует указать ключевое слово static (статический). Когда член объявляется статическим, к нему можно получать доступ до того, как будут созданы какие-либо объекты его класса, и без ссылки на какой­ л ибо объект. Объявить статическими можно как методы, так и переменные. Наибол ее распространенным примером статического члена явля ется метод ma in ( ) , который объявлен как stat ic, потому что он должен быть вызван до того, как будут созданы любые объекты. Переменные экземпляра, объявл енные как static, по существу являются глобал ьными переменными. При объявл ении объектов такого класса копия статической перем енной не создается. Взамен все экземпляры класса имеют дело с одной и той же ст атической перем енной. С методами, объявл енными как static, связано несколько ограничений. • Они могут напрямую вызывать только другие статические методы сво­ его класса. • Они могут напрямую получать доступ только к статическим перемен­ ным своего класса. • Они никоим образом не могут ссыл аться на this или super. (Ключевое слово super относится к наследованию и описано в следующей главе.) Есл и для инициализации статических переменных нужно выпол нять вы­ числ ения, тогда можно объявить блок stat ic, который выполняется в точ­ ности один раз, когда класс загружается впервые. В примере ниже показан класс со статическим методом, несколькими статическими переменными и статическим блоком инициализации: // Демонстрация примен е ния статиче ских переменных , м е тодо в и бл о ков . class UseStat i c { stat i c i n t а = 3 ; static i n t Ь ; Часть 1. Язык Java 200 s tatic void meth ( int х ) System . out .println ( " х = " + х ) ; Sys tem. out .println ( 11 a = + а ) ; Sys tem. out .println ( 11 b " + Ь ) ; 11 static System . out . println ( "Инициализация в статическом бло ке . Ь = а * 4; 11 ) ; puЫic static void main ( String [ ] args ) { meth ( 4 2 ) ; } Сразу после загрузки класса UseStatic запускаются все статические опе­ раторы. Сначала а устанавливается в 3, затем выполняется блок static, ко ­ торый выводи т сообщение, после чеrо Ь инициализируется знач ени ем а * 4 , или 1 2 . Далее вызывается метод mai n ( ) , который вызывает me th ( ) , переда­ вая в х значение 42. Три оператора print ln ( ) относятся к двум статическим переменным а и Ь, а также к параметру х. Во т вывод, генерируемый про граммой: Инициализация в статическом блоке . х = 42 а = 3 Ь = 12 За пределами класса , в ко тором они определены, статически е методы и переменные могут использоваться независимо от любого объекта . Для этого понадобится только указать имя их класса и за ни м операцию точки. Скажем, если вы хотите вызвать статически й метод вне его класса, то можете приме­ нить следу ющу ю общую форму: имя-кла сса . метод ( ) Здесь в имя-класса указывается имя класса, rде объявлен статический ме­ тод. Как видите, формат анало гичен тому, который используется для вызова нестатических методов через переменные ссылок на объекты. Доступ к ста­ тическо й переменной можно получить тем же способом - с помощью опера­ ции точки после имени класса. Именно так в Java реализована управляемая версия глобальных ме тодов и гло бальных переменных. Рассмотрим пример. Внутри ma i n ( ) доступ к статическо му ме тоду cal lme ( ) и статическо й переменной Ь осу ществляется через имя их класса StaticDemo: class StaticDemo { static int а = 4 2 ; static i nt Ь = 9 9 ; static void cal lme ( ) System. out.println ( "a " + а ) ; Глава 7. П одро бн ый анализ метод ов и к ла сс о в 201 class Stati cByName [ puЫic static vo id mai n ( S tring [ ] a rgs ) [ StaticDerno . cal lme ( ) ; System . out . prin tln ( "b = " + Stati cDemo . b ) ; Вывод программы выглядит так: а = 42 Ь = 99 Клю ч е во е с л ово final Поле может быть объявлено как f i na l (финальное), что предотвращает изменение его содержимого, делая его по существу константой. Это озна­ чает, что поле f i na l должно быть инициализировано при его объявлении. Существуют два способа инициализации такого поля. Во-первых, полю final можно присвоить значение при его объявлении. Во-вторых, полю final мож­ но присвоить значение в конструкторе. Первый подход, пожалуй, встречается наиболее часто. Вот пример: final final final final final int int int int int FILE NEW = 1 ; FILE-OPEN = 2 ; FI LE SAVE = 3 ; FILE SAVEAS = 4 ; FI LE-QUI T = 5 ; Теперь в последующих частях пр огра ммы можно использовать поля FILE_OPEN и т.д., как если бы они были константами, не опасаясь, что значе­ ние было изменено. Как показано в примере, общепринятое соглашение при написании кода предусматривает выбор для полей f i nal идентификаторов со всеми буквами верхнего регистра. Помимо полей как final могут быть объявлены и параметры метода, и локальные переменные. Объявление параметра как final предотвращает его изменение внутри метода. Объявление локальной переменной как final пре­ дотвращает присваивание ей значения более одного раза. Ключевое слово final также может применяться к методам, но его смысл существенно отличается от того, когда оно применяется к переменным. Такое дополнительное использование f i na l объясняется в следующей главе при описании наследования. С н ов а о масси вах Массивы были представлены ранее в книге, еще до обсуждения классов. Теперь, когда вы ознакомились с классами, можно сделать важное замечание о массивах: они реализованы в виде объектов. По указанной причине массивы обладают особой характеристикой, которую вы захотите задействовать в сво­ их интересах. В частности, размер массива, т.е. количество элементов, кото­ рые может содержать массив, находится в его переменной экземпляра leng th. 202 Часть 1 . Язык Java Все массивы имеют переменную leng th, и она всегда будет содержать размер массива. Ниже приведена программа, демонстрирующая эту характеристику: // Демонстрация использования члена length в типе массива . class Length { puЫ i c s tatic void main ( St r i ng [ ] a rgs ) { int [ ] a l new int [ l 0 ] ; int [ ] а 2 = { 3 , 5 , 7 , 1 , 8 , 99 , 4 4 , - 1 0 } ; i nt [ ] а з = { 4 , 3 , 2 , 1 } ; S ystem . out . println ( "Длина a l ра вна " + a l . l ength ) ; System . o ut . println ( "Длинa а2 равна " + a2 . length ) ; System . out . println ( "Длина аЗ ра вна " + а З . length ) ; Программа генерирует следующий вывод: Длина al ра вна 10 Длина а 2 равна 8 Длина аз ра вна 4 Как видите, в выводе отображается размер каждого массива. Имейте в виду, что значение length не имеет ничего общего с количеством фактически используемых элементов. Оно отражает только количество элементов, для хранения которых проектировался массив. Вы можете найти хорошее применение члену length во м ногих ситуациях. Например, далее показана усовершенствованная версия класса Stack. Как вы помните, ранние версии класса Stack всегда создавали стек из десяти элемен­ тов. Новая версия позволяет создавать стеки любого размера. Переполнение стека предотвращается с применением значения stc k . length. / / Усовершенствованный класс Stack , в котором // исполь зуется член l ength в типе массива . clas s Stack { p ri vate int [ ] s t c k ; private int tos ; // Разместить и инициализировать стек . Stack ( i nt s i z e ) { s tck = new i nt [ s i ze ] ; tos = - 1 ; / / Поме стить элемент в стек . void push ( int i tem) { i f ( tos == stc k . length- 1 ) / / использовать член length S ystem . out . println ( "Стек полон . " ) ; else stck [ ++tos] = item; } // Извлечь элемент из стека . int рор ( ) { i f ( tos < О ) { S ystem . out . println ( "Cтeк опустошен . " ) ; return О ; Глава 7. П одроб н ы й а н ализ мето д ов и классов 203 else return stck [ tos - - ] ; class TestSt ack2 { puЫ i c static void main ( St ring [ ] a rgs ) { Stack mystackl = new Stack ( 5 ) ; Stack mys tack2 = new Stack ( B ) ; / / Поместить нескол ь ко чисел в стеки . for ( int i=O ; i < S ; i + + ) mystackl . push ( i ) ; for ( int i=O ; i < B ; i + + ) mys tack2 . push ( i ) ; / / Извлечь эти числа из стеков . System . out . println ( " Cтeк в mys tackl : " ) ; for ( int i=O ; i < S ; i + + ) S ystem . out . println (mystac kl . pop ( ) ) ; System . out . println ( "Cтeк в mystack2 : " ) ; for ( i nt i=O ; i < B ; i + + ) System . out . println (mystack2 . pop ( ) ) ; Обратите внимание, что в программе создаются два стека: один рассчитан на хранение пяти, а другой - восьми элементов. Как ви дите, тот факт, что массивы поддержи вают собственную информацию о длине, упрощает соз да­ ние стеков любого размера. Вложенные и внутренние классы Класс можно определять внутри другого класса; такой к л асс из вестен как вложенный класс. Область дейст вия вложенного класса ограничена областью действия его объемлющего класса. Таким образом, если к л асс В определен внутри класса А, то В не сущест вует нез ависимо от А. Вложенный класс имеет доступ к членам, в том числе з акрытым, к л асса, в который он вложен. Тем не менее, объемлющий к л асс не имеет дост упа к членам вложенного класса. Вложенный к л асс, объявленный непосредст венно в области действия его объемлющего класса, будет членом объемлющего класса. Также можно объ­ являть вложенный класс, локальный для блока. Существуют два типа вложенных классов: статические и нестатические. Стат и ческий вложенный класс - это класс, к котором у применяется моди­ фикатор stat i c. Поскольк у кл асс статический, он должен обращаться к не­ стат и ческим членам объемлющего класса через объект. То есть статический вложенный класс не может напрямую ссылаться на нестат и ческие члены объ­ емлющего класса. Вторым типом вложенного класса является внутренний класс. Внутренний класс - это нестатический вложенный класс. Он имеет дост уп ко всем пере­ менным и методам своего внешнего класса и может ссылаться на них напря­ м ую так же, как пост упают другие нестатические члены внешнего класса. 204 Часть 1. Яз ы к Java В следующей программе иллюстрируется определение и испол ьзование внутреннего класса. Класс по имени Outer имеет одну переменную экз емпля­ ра по имени external_x, один метод экз емпляра с именем test () и опреде­ ляет один внутренний класс по имени Inner. / / Демонстр а ция р аб о ты с внутренним кл а ссом . class Outer { int outer х = 1 0 0 ; void test ( ) { Inner i nner = new I nner ( ) ; inner . display ( ) ; } // Внутренний кл а сс . class Inner { void display ( ) { System . ou t . println ( "display ( ) : oute r х = " + outer_x ) ; class I nnerCla s s Demo { puЫ i c s tatic void ma in ( String [ ] args ) { Outer outer = new Outer ( ) ; outer . tes t ( ) ; Вот вывод, генерируемый программой: display ( ) : outer_x = 1 0 0 В программе внутренний класс п о и мени Inner определен в рамках об­ л асти действия класса Outer, поэтому л юбой код класса Inner может напря­ мую обращаться к переменной external_x. В классе Inner определен ме­ тод экземпляра displa y () , который отображает external_х в стандартном потоке вывода. Метод rnain ( ) объекта I nnerCla s s Derno создает экземпляр класса Outer и вызывает его метод test () , который создает экземпляр клас­ са I nner и вызывает метод display ( ) . Важно понимать, что экземпляр Inner может быть соз дан только в кон­ тексте класса Outer. В противном случае компилятор Java сгенерирует сооб­ щение об ошибке. Как правило, экз емпляр внутреннего класса часто создает­ ся кодом в пределах его обл асти действия, как сделано в примере. Ранее у же объяснялось, что внутренний класс имеет доступ ко всем чле­ нам окружающего его к л асса, но обратное неверно. Члены внутреннего клас­ са известны только в рамках области действия внутреннего класса и не могут использоваться внешним классом. Например: // Эта про грамма н е скомпилируе т ся . class Outer { int oute r х = 1 0 0 ; void tes t ( ) { Inner inner = new I nner ( ) ; inne r . display ( ) ; Гл ава 7 . П од робны й а нализ мет од ов и классов 205 // Внутренний класс . class Inner { int у = 1 0 ; / / переменная у является локальной для Inner void s howy ( ) S ystem . out . println ( y) ; / / ошибка , переменная у здесь неизвестна 1 void display ( ) S ystem . out . println ( "display ( ) : outer х = " + outer х ) ; class I nne rClas sDemo { puЫ i c static voi d ma i n ( String [ ] args ) { Outer outer = new Outer ( ) ; oute r . test ( ) ; Здесь у объявлена как переменная экземпляра I nner. Таки м образом, за пределами этого класса она не известна и не может использоваться в методе showy () . Хотя мы сосредоточились на внутренних классах, объявленных в виде членов в области действия внешнего класса, внутренние классы можно опре­ делять в рамках области действия любого блока. Скажем, вложенный класс можно определить в блоке, который определяется методом, или даже в теле цикла for, как показано в следующей программе: // Определение внутреннего класса в пределах цикла for . class Oute r { int oute r_x = 100 ; void test ( ) { for ( in t i = 0 ; i < l 0 ; i + + ) class I nner { voi d di splay ( ) { System . out . printl n ( "display ( ) : outer х " + outer х ) ; I nner i nner = new I nner ( ) ; i nner . di splay ( ) ; class I nnerC l a s s Demo { puЫ ic static voi d main ( S tring [ ] args ) { Outer outer = new Outer ( ) ; oute r . test ( ) ; Ниже приведен вывод, генерируемый данной версией программы: 206 Ч асть 1. Язык Java di splay ( ) display ( ) display ( ) display ( ) display ( ) display ( ) display ( ) display ( ) display ( ) display ( ) : : : : : : : : : : outer outer outer outer outer outer outer outer outer outer х 100 х 100 х 100 х 100 х = 100 х = 100 х 100 х 100 х 100 х 100 Хотя вложенные классы применимы н е во всех ситуациях, они особенно полезны при обработке событий. Мы вернемся к тем е вложенных классов в главе 25. Там вы увидите, как можно использовать внутренние классы с целью упрощения кода, необходимого для обработки определенных типов событий. Вы также узнаете об анонимных внутренних классах, которые п редставляют собой внутренние классы, не имеющие имени. Интересно отметить, что вложенные классы не были разрешены в исход­ ной спецификации Java 1.0, а появились в Java 1 . 1 . Исс ледова ни е кла сс а String Н есмотря на то что класс String будет подробно исследоваться в части II книги, его краткое изучение оправдано сейчас, потому что мы будем приме­ нять строки в ряде примеров программ, приведенных ближе к концу части 1 . Вероятно, String будет наиболее часто используемым классом в библиотеке классов Java. Очевидная причина связана с тем, что строки являются очень важной частью программирования. Первое, что нужно понять касательно строк: каждая создаваемая вами строка на самом деле является объектом типа String. Даже строковы е кон­ станты на самом деле представляют собой объекты String. Скажем, в следу­ ющем операторе строка "Это тоже строка " является объектом String: System . out . println ( " Этo тоже строка " ) ; Второе, что важно понимать в отно шении строк: объекты типа String не­ изменяемы; после создания объекта String его содержимое модифицировать нельзя. Хотя упомянутое огранич ение может показаться серьезным, это не так по двум причинам. • Если вам необходимо изм енить строку, то вы всегда можете создать но­ вую строку, отражающую изменения. • В Java о п р еделены равноправные классы Str i ng, н а з ы в а е м ы е StringBuf fer и StringBui l der, которые позволяют изменять строки, так что в Java по-прежнему доступны все обычные операции со строка­ ми. (Классы StringBuffer и StringBui lder описаны в части II книги.) Строки можно конструировать различными способами. Проще всего при­ менять оператор такого вида: S tring myString = "это просто тест " ; Глав а 7 . П од р обн ы й анализ методов и классов 207 После создания объект String можно использовать везде, где разрешена строка. Например, следующий оператор отображает myString: System . out . println (myS t r ing ) ; Для объектов String в Java определена одна операция: +. Она применяет­ ся для конкатенации двух строк. Скажем, в результате выпол нения показ ан­ ного ниже оператора в myS tring будет содержаться строка "Мне нравится язык Java . ": String myString = "Мне" + " нравит ся " + " я зык Java . " ; Представленные выше концепции продемонстрированы в следующей про­ грамме: / / Демонстрация работы с объе ктами String . class Stri ngDemo { puЫ i c static voi d ma in ( S tring [ ] args ) String strOЫ " Первая строка " ; Sc:ring s trOЬ2 = " Вторая строка " ; S t ring st rObЗ = s trOЫ + " и " + strOЬ2 ; System . out . println ( s t rOЫ ) ; System . out . println ( st r0b2 ) ; System . out . println ( s trOb З ) ; Вот вывод, сгенерированный программой: Первая строка Вторая строка Первая строка и Вторая строка Класс String содержит нескол ько методов, которыми можно пользовать­ ся. Рассмотрим несколько из них. Две строки можно проверить на предмет равенства с применением метода equals ( ) . Вызвав метод length ( ) , можно получить длину строки, а вызвав метод charAt ( ) можно извлечь символ по указ анному индексу в строке. Ниже показ аны общие формы упомянутых трех методов: boolean equal s ( secondS t r ) i n t length ( ) char charAt ( i ndex ) А вот программа, в которой использ уются эти методы: / / Демонстрация работы несколь ких методов класса S tr ing . class Stri ngDemo2 ( puЫ i c static vo id ma i n ( String [ ] a rgs ) S t ring s trOЫ " Первая строка " ; String s tr0b2 = " Вторая строка " ; String st rObЗ = st rOЫ ; System . out . print ln ( "Длинa строки st rOЫ : " + strOЫ . length ( ) ) ; S ystem . out . println ( " Символ по индексу 3 в строке st rOЫ : " + s trOЫ . charAt ( З ) ) ; 208 Часть 1. Язык Java i f ( s trOЫ . equal s ( strOb2 ) ) System . out . println ( "Cтpoкa else System . out . println ( " Cтpoкa i f ( s trOЫ . equal s ( s trObЗ ) ) System . out . println ( " Cтpoкa else System . out . printl n ( " Cтpoкa strOЫ равна строке strOb2 " ) ; strOЫ не равна строке strOb2 " ) ; strOЫ равна строке strObЗ " ) ; strOЫ не равна строке strObЗ " ) ; Программа генерирует следующий вывод: Длина строки strOЫ : 1 3 Символ п о ин дексу 3 в строке strOЫ : в Строка strOЫ не равна строке strOb2 Строка strOЫ равна строке s trObЗ Конечно же, можно создавать и массивы строк подобно массивам объек­ тов любого другого типа. Например: / / Демонстрация использования массивов String . class StringDemoЗ { puЬlic static void main ( Str ing [ ] args ) String ( ] s tr = { "один " , "два " , " три" } ; for ( int i=0 ; i <str . length ; i + + ) System . out . println ( " str [ " + i + " ] : " + str [ i ] ) ; Вот вывод, генерируемый программой: str [ 0 ] : один str [ l ] : два str [ 2 ] : три В следующем разделе вы увидите, что массивы строк играют важную роль во многих программах на Java. Использова ние аргументов командной строки Иногда программе при запуске необходимо передать какую-то информа­ цию. Для этого предназ начены аргументы командной строки, передаваемые методу mai n () . Аргумент командной строки представляет собой данные, которые следуют непосредственно за именем программы в командной стро­ ке, когда программа з а пускается. Получить доступ к аргументам командной строки в программе на Java довольно просто - они хранятся в виде строк внутри массива типа Str i ng, который передается параметру args метода ma in () . Первый аргумент командной строки хранится в args [ О ] , второй - в args [ 1 ) и т.д. Приведенная далее программа отображает все аргументы ко­ мандной строки, с которыми она вызывается: Глава 7 . Подробный анализ м етодов и к ла сс ов 209 / / Отображение всех аргументов командной строки . class CommandLine { puЬlic static void main ( String [ ] args ) { for ( int i=0; i<args . length ; i + + ) System . out . println ( " args [ " + i + " ] : " + args [ i ] ) ; Запустите программу как показано ниже: j ava CommandLine это всего лишь тест 1 0 0 - 1 Программа сгенерирует следующий вывод: args [ О ] : это a rgs [ l ] : всего a rgs [ 2 ] : лишь args [ 3 ] : тест args [ 4 ] : 1 0 0 args [ 5 ] : - 1 Помн ите! Все аргументы командной строки передаются в строковом виде. Ка к будет объяснено в главе 1 9, числовые значения придется вручную преобразовывать в их внутренние формы. А ргументы переменной дли н ы В состав современных версий Java входит средство, упрощающее создание методов, которые должны принимать произвольное колич ество аргументов. Оно называется аргументами переменной длины (variaЬle-length arguments varargs). Метод, принимающий произвольное ч исло аргументов, называется методом с переменной арностью или методом с аргументами переменной длины. Ситуации, когда методу требуется п ередавать произвольное число аргу­ ментов, не являются чем-то необычным. Метод, который открывает подклю­ чение к Интернету, например, может принимать имя пользователя, п ароль, имя файла, протокол и т.д., но предоставлять стандартные значения, если часть этой информации не был а указана. В такой ситуации было бы удоб­ но передавать только те аргументы, к которым не применяются стандарт­ ные значения. Другим примером может служить метод p r i n t f ( ) , который является ч астью библиотеки ввода-вывода Java. Как вы увидите в главе 22, он принимает произвольное кол ичество аргументов, форматирует их и затем отображает. В ранних версиях Java аргументы п еременной длины можно было под­ держивать двумя способами, ни один из которых нельзя считать удобным. Первый способ, подходящий в ситуации, когда максимальное количество ар­ гументов является небольшим и известным, предусматривал создание пере­ груженных версий метода, по одной для каждого варианта вызова метода. Хотя подобный подход работает и подходит в ряде случ аев, он п рименим только к узкому набору ситуаций. 210 Часть 1 . Я з ы к Java В тех случаях, когда максимальное количество потенциальных аргумен­ тов было большим или неизвестным, использовался второй подход, предус­ матривающий помещение аргументов в массив, который затем передавался методу. Данный подход все еще встречается в унаследованном коде и про­ иллюстрирован ниже: // Исполь зование массива для передачи методу произвольного числа аргументов . / / Это подход в старом стиле к аргументам переменной длины . clas s PassArray { stat ic voi d va'Гest ( int [ ] v) { System . out . print ( " Количество аргументов : " + v . length + " Содержимое : " ) ; for ( i nt х : v) System . out . p r i n t ( x + " " ) ; System . out . println ( ) ; puЫ i c s tatic vo i d ma i n ( S tring [ ) args ) { / / Обратите внимание на то , как должен создаваться / / массив для хранения аргументов . int [ J n l = { 1 0 } ; int [ J n2 = { 1 , 2 , З } ; int [ ] n З = { } ; va'Гest ( n l ) ; va'Г est ( n2 ) ; va'Гes t ( n З ) ; / / 1 аргумент / / З аргумента / / без аргументов Вывод, генерируемый программой, выглядит следующим образом: Количество ар гумент ов : 1 Содержимое : 1 0 Количество аргументов : З Содержимое : 1 2 З Количе ство аргументов : О Содержимое : Аргументы передаются методу vaTes t ( ) через массив v. Такой подход в старом стиле позволяет vaTest ( ) принимать произвольное количество аргу­ ментов. Однако он требует, чтобы перед вызовом метода vaTest ( ) аргумен­ ты вруч ную упаковывались в массив. Создание массива при каждом вызове vaTest ( ) не только утомительно, но и потенциально подвержено ошибкам. Средство аргументов переменной длины предлагает более простой и совер­ шенный вариант. Аргумент переменной длины определяется с помощью трех точек ( . . . ) . Например, вот как определить метод vaTes t ( ) с применением аргумента пе­ ременной длины: s t a t i c void va'Гest ( i nt . . . v) { Этот синтаксис сообщает компилятору о том, что метод vaTest ( ) можно вызывать с нулем или большим числом аргументов. В результате v неявно объявляется как массив типа int [ ] . Та ким образом , внутри vaTest ( ) доступ к v осу ществляется с использованием обычного синтаксиса массива. Ниже Гл а ва 7. П од ро бный а н а лиз методо в и классо в 21 1 показана предыдущая программа, переписанная с применением аргумента переменной дл ины: // Демонстрация исполь зования аргуме нтов переменной длины . clas s VarArgs { / / vaTest ( ) now uses а vara rg . static voi d vaTe s t ( int . . . v ) { System . out . print ( " Koличecтвo аргументов : " + v . length + " Содержимое : " ) ; for ( i nt х : v ) System . out . p r i nt ( x + " " ) ; System . out . println ( ) ; puЫ i c static void main ( String [ ] a rgs ) { / / Обратите внимание , что теперь метод vaTest ( ) / / можно вызывать с переменным числом аргументов . / / 1 аргуме нт vaTest ( 1 0 ) ; / / 3 аргумента vaTest ( 1 , 2 , 3 ) ; / / бе з аргументов vaTest ( ) ; Вывод, генерируемый програм мой, будет таким же, как в первоначально й версии. В приведенной выше программе нужно отметить два важных момента. Во­ первых, переменная v в методе vaTest ( ) обрабатывается как массив. Дело в том, что v и является массивом. Синтаксис . . . просто сообщает компи­ лятору, что будет использоваться переменное число аргументов, причем ар­ гументы будут храниться в массиве, на который ссыл ается v. Во-вторых, ме­ тод vaTest () вызывается внутри rnai n ( ) с разным количеством аргументов, включая вариант вообще без аргументов. Аргументы автоматически поме­ щаются в массив и передаются v. При отсутствии аргументов дл ина массива равна нулю. Наряду с параметром переменной длины метод может и меть и "обычные" параметры. Тем не менее, параметр переменной дл ины должен объявляться в методе последним. Скажем, следующее объявление метода совершенно до­ пустимо: int do i t ( i nt а , int Ь , douЫe с, i nt . . . val s ) { В этом случае первые три аргумента, указанные в вызове do i t ( ) , сопо­ ставляются с первыми тремя параметрами, а все остальные аргументы счита­ ются относящимися к vals. Не забывайте, что параметр переменной длины должен быть последним. Например, показанное далее объявление некорректно: i n t do i t ( i nt а, i n t Ь , douЫe с , i n t . . . va l s , boolean s topFlag ) { / / Ошибка ! Здесь предпринимается попытка объявить обычный параметр после пара­ метра переменной длины, что недопустимо. 212 Ч а сть 1. Яз ы к Java Существует еще одно ограничение, о котором следует помнить: должен быть только один параметр переменной длины. Скажем, приведенное ниже объявление тоже будет ошибочным: int do i t ( int а, int Ь , douЫe с, int . . . val s , douЬle . . . moreval s ) { / / Ошибка ! Объявлять второй параметр переменной длины не разрешено. Вот переработанная версия метода vaT e s t ( ) , которая принимает обыч­ ный аргумент и аргумент переменной длины: // Исполь зование аргумента переменной длины со стандартными аргументами . class VarArgs2 { / /Здесь msg является нормальным параметром, а v - параметром переменной длины static vo id vaTest ( String m s g , int . . . v ) { System . out . p r i nt (msg + v . l ength + " Содержимое : " ) ; for ( int х : v ) System . out . print ( x + " " ) ; System . out . p r intln ( ) ; puЫ i c static void mai n ( String [ ] args ) { vaTest ( " Один аргумент переменной длины : " 1 0 ) ; vaTest ( " Три аргумента переменной длины : 1, 2, 3) ; vaTest ( "Без аргументов переменной длины : " ) ; Программа генерирует следующий вывод: Один аргумент переменной длины : 1 Содержимое : 1 0 Три аргумента переменной длины : 3 Содержимое : 1 2 3 Без аргументов переменной длины : О Содержимое : Перегру зка методов с аргументами переменной дли н ы Метод, принимающий аргумент переменной длины, можно перегружать. Например, в показанной далее программе метод vaTest ( ) перегружается три раза: // Ар гуме нты переменной длины и перегрузка . class VarArgsЗ { static voi d vaTe st ( int . . . v ) { System . out . print ( " vaTe st ( i nt . . . ) : " + " Количество аргументов : " " Содержимое : " ) ; + v . length + for ( i nt х : v) System . out . print ( x + " " ) ; System . out . println ( ) ; static voi d vaTest (boolean . . . v ) { System . out . p r int ( " vaTe st ( boolean . . . ) " + " Количе ство аргументов : " + v . l ength + " Содержимое : " ) ; Глава 7. П од роб н ы й анализ метод ов и классов 21 3 for ( bool ean х : v ) System . out . print ( x + " " ) ; System . out . println ( ) ; s tatic void vaTes t ( St ring ms g , int . . . v) { System . out . print ( "vaTe st ( St ring, int . . . ) : " + msg + v . l e ngth + " Содержимое : " ) ; for ( int х : v ) System . out . print ( x + " " ) ; Sys tem . out . println ( ) ; puЫ i c static void ma i n ( String [ ] args ) { vaTest ( 1 , 2 , 3 ) ; 10, 20) ; vаТеs t ( " Тестирование : vaTes t ( t rue , f a l s e , fals e ) ; Программа генерирует следующий вывод: vaTest ( int . . . ) : Количество аргументов : 3 Содержимое : 1 2 3 vaTest ( S tring, int . . . ) : Тестирование : 2 Содержимое : 1 0 2 0 vaTest ( bool ean . . . ) Количество аргументов : 3 Содержимое : t rue false false В представленно й выше программе иллюстрируются оба способа перегрузки метода с аргументом переменной дл ины. Первый способ предусма­ тривает применение отл ичающегося типа для параметра переменной длины, как в случае vaTest (int . . . ) и vaTest (boolean . . . ) . Вспомните, что . . . приводит к тому, что параметр и нтерпретируется в виде массива заданного типа. Следовательно, точно так же, как обычные методы можно перегружать с использованием отл ичающихся параметров типа масси вов, методы с пара­ метрам и переменной дл ины раз решено перегружать, указывая раз ные типы для параметров переменной длины. В этом случае компилятор Java вызывает надлежащий перегруженный метод на основе отл ичия между типами. Второй способ перегруз ки метода с аргументом переменной длины пред­ полагает добавление одного или нескольких обычных параметров, что и было сделано в верси и vaTest ( String , int . . . ) . В данном случае компилятор Java вызывает надлежащий перегруженный метод на основе и кол ичества, и типа аргументов. На заметку! Метод с аргументом переменной длины также может быть перегружен за счет определения метода без аргумента переменной длины. Скажем, в приведенной выше программе v а Те s t ( i n t х ) я вляется допустимой версией v а Те s t ( ) . Эта версия вызывается только при наличии одного аргумента типа i n t. Когда передаются два или более аргументов типа int, используется версия vaT e s t ( i nt • • • v ) с аргументом переменной длины. 214 Часть 1 . Язык Java Аргументы переменной дл и н ы и неодноз начность При перегруз ке метода, принимающего аргумент переменной длины, мо­ гут воз никать несколько неожиданные ошибки. Такие ошибки связ аны с не­ однозначностью, поскольку существует воз можность создать двусмысленный вызов перегруженного метода с аргументами переменной длины. Например, взгляните на следующую программу: // Аргументы переменной длины, перегрузка и неоднозначность . // / / Эта программа содержит ошибку и н е скомпилируется ! class Va rArgs4 { stat i c void vaTe st ( int . . . v) { System . out . print ( "vaTest ( i nt . . . ) : " + " Количество аргументов : " + v . l ength + " Содержимое : " ) ; for ( int х : v ) System . out . p rint ( x + " " ) ; System . out . println ( ) ; static void vaTe s t ( boolean . . . v ) { System . out . print ( " vaTest ( boolean . . . ) " + " Количество аргуме нтов : " + v . length + " Содержимое : " ) ; for ( boolean х : v ) System . out . print ( x + " " ) ; Sys tem . out . println ( ) ; puЫ i c static void ma in ( String [ ] args ) { vaTe s t ( l , 2 , 3 ) ; / / Нормально / / Нормально vaTes t ( true , f a l s e , fa l se ) ; // Ошибка : Неоднозначность ! vaTest ( ) ; В этой программе перегрузка метода vaTest ( ) совершенно корректна, но программа не скомпилируется из-з а такого вызова: vaTest ( ) ; / / Ошибка : Неоднозначность ! Поскольку параметр переменной длины может быть пустым, вызов будет транслироваться в vaTest ( int . . . ) или в vaTest ( boolean . . . ) , которые оба одинаково действительны. Таким образом, вызов в своей основе неодно­ значен. Ниже приведен еще один пример неодноз начности. Следующие перегру­ женные версии метода vaTest ( ) по своей сут и неодноз начны, хотя одна из них принимает обычный параметр: // s t a t i c void vaTest ( i nt . . . v ) static void vaTest ( i nt n , int v) { // Глава 7. П одробн ы й анализ методов и кла ссов 21 S Несмотря на раз личие в списках параметров vaTest ( ) , компил ятор не сможет распоз нать следующий выз ов: vaTest ( l ) Во что преобразуется данный вызов: в vaTest ( int . . . ) с одним аргумен­ том переменной длины или в vaTest (int , i nt . . . ) без аргументов пере­ менной длины? Компилятор никак не сможет получить ответ на этот вопрос. Таким образом, ситуация неодноз начна. Из -за ошибок неодноз начности, подобных только что показ анным, иногда вам придется отказаться от перегрузки и просто использовать два метода с раз ными именами. Кроме того, в ряде случаев ошибки неодноз начности вы­ являют концептуальный дефект в коде, который можно устранить, более тща­ тельно проработав решение. Вы ведение типов локальных переменны х для ссылочн ых типов Как упоминалось в главе 3 , начиная с версии JDK 10, в J ava поддерживает­ ся выведение типов локальных переменных. Вспомните, что при выведении типов локальных переменных тип переменной указывается как va r, а пере­ менная дол жна быть инициализирована. В более ранних примерах выведение типов демонстрировалось с примитивными типами, но его также можно при­ менять со ссыло чными типами. Фактически основное использование выведе­ ния типов связ ано со ссылочными типами. Вот простой пример, в котором объявляется строковая переменная по имени myStr: var myStr = " Э то строка " ; Из-за использования в ка честве инициал изатора строки в кавычках для переменной myStr вывод ится тип Str ing. Как объяснялось в главе 3, одно из преимуществ выведения типов локаль­ ных переменных связано с его способностью оптимизировать код, и такая оп­ тимиз ация наиболее о чевидна именно со ссылочными типами. Причина в том, что многие типы классов в Java имеют довольно дл инные имена. Например, в главе 1 3 вы узнаете о классе FileinputSt ream, с помощью которого файл от­ крывается для операций ввода. В прошлом объект Filei nputStream объяв­ л ялся и инициализировался с применением традиционного объявления вроде показ анного ниже: Fi leinputStream fin = new Fil e i nputStream ( "test . txt " ) ; С использованием var теперь его можно переписать так: var fin = new FileinputStream ( "tes t . txt " ) ; Тут предполагается, что переменная fin имеет тип FileinputStream, т.к. это тип ее инициализатора. Нет никакой необходимости явно повторять имя типа. В результате такое объявление fin з на чительно короче, чем его з апись традиционным способом, а потому применение va r упрощает объявление. 216 Часть 1 . Язык Java Это преи мущество становится еще более очевидным в более сложных объ­ явлениях, например, включающих обобщения. В целом упрощение выведе­ ния типов локальных переменных помогает уменьшить утомител ьный набор длинных имен типов в программах. Разумеется, аспект упрощения кода со стороны выведения типов локаль­ ных переменных должен использоваться осмотрительно, чтобы не ухудшить читабельность программы и в итоге не скрыть ее намерения. Например, взгляните на объявление следующего вида: var х = о . getNext ( ) ; В данном случае кому-то, читающему ваш код, может быть не сразу станет ясно, како й тип имеет х. По су ществу выведение типов локальных перемен­ ных представляет собой средство, которое должно применяться обдуманно. Выведение типов локальных переменных можно также использовать в от­ ношении пользовательских кл ассов, как демонстрируется в показ анной ниже программе. В ней создается класс по имени MyClass, а з атем с помощью вы­ ведения ти пов локальных переменных объявляется и ини циал изируется объ­ ект этого класса. // Исполь зование выведения типов локаль ных переменных / / с пользо ватель ским классом . class MyClass { pri vate int i ; MyClass ( int k ) { i = k ; ) int geti ( ) { return i ; ) void seti ( int k ) { i f ( k >= О ) i k; ) class RefVa rDemo { puЫ ic static vo id main ( S tring [ ] args ) va r mc = new MyClass ( l 0 ) ; // Обратите внимание на применение va r . System . out . p rintln ( " З нaчeниe i в mc теперь равно " + mc . geti ( ) ) ; mc . seti ( 1 9 ) ; System . out . println ( " З нaчeниe i в mc теперь равно " + mc . geti ( ) ) ; Вот вывод, генерируемый программой: З начение i в mc теперь равно 1 0 З начение i в mc теперь равно 1 9 Обратите особое внимание в программе н а следующу ю строку: var mc = new MyClass ( 1 0 ) ; / / Обратите внимание на применение va r . Тип переменной mc будет выводиться как MyCla s s, потому что это тип инициализ атора, который является новым объектом MyClas s. Как объяснялось ранее в кни ге, для удобства читателей, работающих в средах J ava, которые не поддерживают выведение типов локальных перемен­ ных, в бол ьшинстве примеров настоящего издания оно применяться не будет, что позволит их компилировать и з апускать макси мальному числу читателей. ГЛ А ВА Наследование Наследование является одним из краеугольных камней объектно-ориен­ тированного программирования (ООП), поскольку позволяет создавать ие­ рархические классификации. С использованием наследования вы можете соз­ дать универсальный к л асс, который определяет характерные черты, общие для набора связанных элементов. Затем этот класс может быть унаследован другими, более специфическими классами, каждый из которых добавляет те элементы, которые уникальны для него. В терминологии Java унаследованный кл асс называется суперклассом, а класс, выполняющий наследование - под­ классом. Следовательно, подк ласс представляет собой специализированную версию суперкласса. Он наследует все члены, определенные суперклассом, и добавляет собственные уникальные элементы. Основы наследования Чтобы наследовать класс, вы просто вкл ючаете определение одного к л асса в другой с применением к л ючевого слова extends. Давайте выясним, как это делать, начав с простого примера. В следующей программе создается супер­ к л асс по имени А и подкласс по имени В. Обратите внимание на использова­ ние к л ючевого слова extends для создания подк ласса А. // Простой пример насл едования . / / Создать суперкласс . class А { int i , j ; void s howi j ( ) System . out . println ( " i и j : " + i + " " + j ) ; / / Создать подкласс путем расширения класса А . class В extends А in t k; void showk ( ) i System . ou t . println ( " k : " + k ) ; 218 Часть 1 . Язык Java void sum ( ) { System . out . println ( " i + j + k : " + ( i + j + k ) ) ; class S impleinheritance { puЬ l i c s t at i c void main ( S t ring [ ] args ) { А supe rOb = new А ( ) ; В s ubOb = new В ( ) ; / / Суперклас с можно исполь зовать сам по себе . superOb . i = 1 0 ; supe rOb . j = 2 0 ; System . out . println ( " Coдepжимoe s upe rOb : " ) ; s uperOb . s howij ( ) ; System . out . println ( ) ; / * Подкласс имеет доступ ко всем открытым членам своего суперкласса . * / subOb . i = 7 ; subOb . j = 8 ; subOb . k = 9 ; System . out . print l n ( "Coдepжимoe s ubOb : " ) ; s ubOb . sh owij ( ) ; subOb . showk ( ) ; System . out . p r intln ( ) ; Sys tem . out . p ri ntln ( "Сумма i , j и k в subOb : " ) ; s ubOb . sum ( ) ; Ниже показан вывод, генерируемый программой: Содержимое superOb : i и j : 10 20 Содержимое subOb : i и j: 7 8 k: 9 Сумма i , j и k в s ubOb : i+j+k: 24 Как видите, подкласс В включает в себя все члены своего суперклас­ са А. Вот почему объект subOb может получать доступ к i и j и вызывать showij ( ) . Кроме того, внутри sum ( ) на i и j можно ссылаться напрямую, как если бы они были частью В. Несмотря на то что А выступает в качестве с уперкласса для В, он также является полностью независимым, автономным классом. Быть суперклассом для подкласса не оз начает, что суперкласс не может использоваться сам по себе. Кроме того, подкласс может быть суперклассом для другого подкласса. Общая форма объявления класса, унаследованного от суперкласса, выгля­ дит следующим образом: class имя-подкла сса extends имя-суперкла сса { / / тело класса Глава 8. Наследова н ие 219 Для любого создаваемого подкласса разрешено указывать только один су­ перкласс. Наследование нескольких суперклассов при создании одиночного подкласса в языке Java не поддерживается. Как было указано, можно создать иерархию наследования, в которой подкласс становится суперклассом для другого подкласса. Однако ни один класс не может быть суперклассом для самого себя. Д оступ к ч лен а м и на следов а н ие Хотя подкласс включает в себя все члены своего суперкласса, он не мо­ жет получить доступ к тем членам суперкласса, которые были объявлены как закрытые. Например, рассмотрим приведенную далее простую иерар­ хию классов: / * В иерархии классов члены pr ivate остаются закрытыми по отношению к своему классу . */ Эта программа содержит ошибку и не скомпилируется . / / Создать суперкла сс . class А { int i ; private int j ; / / стандартный доступ / / закрыт по отноше нию к А void setij ( int х, int у) { i = х; j = у; / / Член j из класса А здесь недоступен . class В extends А int tota l ; void sum ( ) ( total = i + j ; / / ОШИБКА, член j здесь недоступен class Ac cess puЫic static vo id ma in ( Str ing [ ] args ) ( В subOb = new В ( ) ; subOb . s etij ( 1 0 , 1 2 ) ; subOb . sum ( ) ; System . out . println ( " Cyммa равна " + subOb . t ota l ) ; Программа не скомпилируется, потому что использование j внутри мето­ да sum ( ) класса В вызывает нарушение прав доступа. Поскольку член j объ­ явлен как private, он доступен только другим членам собственного класса. Подклассы не имеют к нему доступа. 220 Часть 1. Язык Java Помните! Член класса, который был объявлен как pr iva t e, останется закрытым по отношению к своему классу. Он не будет доступен любому коду за пределами своего класса, в том числе подклассам. Б ол ее реалисти ч ны й пример Давайте рассмотрим бол ее реал истичный пример, который поможет оце­ нить всю мощь насл едования. Здесь финальная версия класса Вох, разрабо­ танная в предыдущей главе, будет расширена за счет включения четвертого компонента по имени weight (вес). Таким образом, новый класс будет содер­ жать ширину, высоту, глубину и вес коробки. / / В этой программе используется наследование для расuмрения класса Вох . class Вох { douЫ e width ; douЫe height; douЫe depth ; / / Конструктор , применяемый для клонирования объекта . Вох ( Вох оЬ ) ( / / передать объе кт конструктору width = ob . wi dt h ; height = ob . height ; depth = ob . depth ; / / Конструктор , используемый в случае указания всех размеров . Box ( douЬle w, douЫe h, douЫe d } { width = w; height = h; depth = d; // Конструктор , применяемый в случае , если размеры вообще не указаны . Вох ( ) ( / / использовать - 1 для обозначения width = - 1 ; / / неинициализир ованного height = - 1 ; / / объекта коробки depth = - 1 ; } // Конструктор , используемый в случае создания объекта куби ческой коробки . Box ( douЫe l e n ) width = he ight = depth = len ; } / / Вычислить и возвратить объем . douЫe vol ume ( ) { return width * height * depth ; / / Здесь класс Вох расширяется с целью в ключения члена weight . class BoxWeight e xtends Вох { douЫ e weigh t ; / / вес коробки // Конструктор для BoxWe ight . BoxWe ight ( douЫe w, douЫe h , douЫe d, douЫe m } { Гла ва 8. На сл ед ование 221 width = w ; height = h ; depth = d ; weight = m ; c l a s s DemoBoxWeight puЫ ic static void BoxWeight myboxl BoxWeight mybox2 douЫe vo l ; { main ( String [ ] a rgs ) { = new BoxWeight ( l 0 , 2 0 , 1 5 , 3 4 . 3 ) ; = new BoxWeight ( 2 , 3 , 4 , 0 . 07 6 ) ; vol = mybox l . volume ( ) ; System . out . println ( " Oбъeм myboxl равен " + vol ) ; Sys tem . out . println ( "Вес myboxl равен " + mybox l . weight ) ; Syst em . out . println ( ) ; vol = mybox2 . volume ( ) ; System . out . println ( " Oбъeм mybox2 равен " + vo l ) ; System . out . println ( " Вес mybox2 равен " + mybox2 . wei ght ) ; Вот вывод, генерируемый программой: Объем myboxl равен 3 0 0 0 . 0 Вес myboxl равен 3 4 . 3 Объем mybox2 равен 2 4 . 0 Вес mybox2 равен 0 . 0 7 6 Класс BaxWe ight насл едует все характеристики класса Вах и добавляет к ним компонент weight для представления веса. Классу BaxWeight вовсе не обязательно воссоздавать все функциональные средства , имеющиеся в Бах. Для достижения своих цел ей он может просто расширить Вох. Основное преимущество наследования связано с тем , что после создания суперкласса, который определяет характерные ч ерты, общие для набора объ­ ектов, его можно применять для создания любого количества более конкрет­ ных подкл ассов. Каждый подкласс может точно настраивать собственное предназначение. Скажем, сл едующий кл асс наследуется от Бах и добавляет свойство цвета: // Здесь класс Вох расширяется для включ ения свойства цвета , class ColorBox extends Вох { i nt col o r ; / / цвет коробки ColorBox ( douЫe w, douЫ e h, douЫe d , int с ) { width = w; height = h ; depth = d ; color = с ; Не забывайте, что после создания суперкл асса, определяющего общие аспекты объекта, данный суперкласс можно наследовать для формирования 222 Часть 1. Язык Java специализированных классов. Каждый подкласс просто добавляет свои уни­ кальные характеристики. В этом и заключается суть насл едования. Переменная ти п а с уп еркласса м ожет ссылат ь ся на объ ект подкласса Ссылочной пер еменной типа суперкласса можно присваивать ссылку на объ ект любого подкласса, производного от данного суперкласса. Вы сочте­ те такой аспект наследования весьма пол езным в разнообразных ситуациях. Рассмотрим показанный ниже код примера: class RefDemo 1 puЫic s tatic void main ( String [ ] arg s ) { BoxWeight weightbox = new BoxWeight ( З , 5 , 7 , 8 . 3 7 ) ; Вох plainbox = new Вох ( ) ; douЫe vol ; vol = we ightbox . volume ( ) ; System . out . println ( " Oбъeм weightbox ра вен " + vol ) ; System. out . println ( "Bec weightbox равен " + wei ghtbox . we ight ) ; System . out . println ( ) ; / / Присвоить ссыпку на BoxWe ight ссыпке на Во х . plainbox = weightbox ; vol = plainbox . volume ( ) ; // нормально, метод volume ( ) определен в В о х System. out . printl n ( "Oбъeм pla inbox равен " + vol ) ; / * Следующий оператор ошибочен , пот ому что член weight в plainbox не определен . */ / / System . out . print ln ( "Bec p l ainbox равен " + plainbox . weight ) ; Здесь weightbox является ссыл кой на объекты BoxWeight, а plainbox ссылкой на объекты Вох. Поскол ь ку BoxWeight - подкласс Вох, переменной plainЬox разр ешено присваивать ссылку на объект weightbox. Важно понимать, что именно тип ссылочной переменной, а не тип объ­ екта, на который она ссылается, определяет, к каки м членам можно получать доступ. Другими словами, когда ссылочной переменной типа суперкласса присваивается ссылка на объект подкласса, то доступ имеется только к тем частям объекта, которые определены в суперклассе. Вот почему перем енная pla i nbox не может получить доступ к weight, даже есл и она ссылается на объект BoxWeight. Если подумать, то в этом есть смысл, потому что суп ер­ классу нич его не известно о том, что к нему добавляет подкласс. Поэтому последняя строка кода в предыдущем фрагменте закомментирована. Ссылка Вох не может получить доступ к полю weight, т.к. в классе Вох оно не опре­ делено. Хотя описанный выше прием может показаться несколько экзотическим, с ним связан ряд важных практических примен ений, два из которых обсужда­ ются далее в главе. Глава 8. Наследование 223 Испо л ьз ов ание клю ч е во го слов а super В предшествующих примерах классы, производные от Вох, не были реали­ зованы настолько эффективно и надежно, насколько могли бы. Скажем, кон­ структор для BoxWei ght явно инициализирует поля width, height и depth класса Вох. Это не только приводи т к дублированию кода, уже имеющегося в его суперклассе, что неэффективно, но и подразумевает предос тавление под­ классу доступа к упомянутым членам. Тем не менее, будут возникать си туа­ ции, ко гда желательно создавать суперкласс, ко торый держи т детали сво ей реализации при себе (т.е. хранит свои элементы данных закрытыми). В таком случае у подкласса не было бы возможности напрямую обращат ься к этим переменным либо инициализировать их самостоятельно. Поскольку инкапсу­ ляция является основным атрибутом ООП, сов ершенно не удивительно, что в Java предлагается решение описанной проблемы. Всякий раз, ко гда подклассу необходимо сослаться на сво й непо средств енный суперкласс, он может вос­ пользоваться ключевым словом super. Ключевое слово super имеет две основные формы. Первая вызывает кон­ структор суперкласса, а вторая служит для дост упа к члену суперкласса, ко ­ торый был сокрыт членом подкласса. Обе формы обсуждаются далее в главе. И спользо ва ние кл ю ч е вого слова super для в ы зова ко нстр у кторов с у п ерк ласса Подкласс может вызывать конструктор, определенный в его суперклассе, с применением следующей форм ы super: s uре r ( список-аргументов ) ; Здесь список-аргументов предназначен для указания любых аргумен­ тов, необходимых конс труктору в суперклассе. Вызов super ( ) всегда должен быть первым оператором, выполняемым внутри конструктора подкласса . Что бы увидеть, как используется super ( ) , рассмотрим показанную ниже усовершенствованную верси ю класса BoxWe ight: // В классе BoxWeight члены его суперкласса Вох теперь инициализируют ся с применением s upe r . c l a s s BoxWe ight exteпds Вох { douЫe weigh t ; / / вес коробки / / Инициализировать w i dt h , he ight и depth , исполь зуя s uper ( ) . BoxWeigh t ( douЫe w , douЫe h , douЫ e d, douЫe m ) { super ( w , h , d ) ; / / вызвать конструктор суперкласса weight = m; Ко нструктор BoxWeight ( ) вызывает super ( ) с аргументами w, h и d, что приводит к вызову конструктора Вох, который инициализирует поля width, hei ght и depth с прим енением этих значений. Класс BoxWe ight больше не инициализирует указанные поля самостоятельно . Ему нужно инициализиро - 224 Ч а с ть 1 . Я з ы к Java вать только уникальное для него поле: we ight. Таким образом, появляется возможность при желании сделать поля width, he ight и depth в классе Вох закрытыми. В предыдущем примере вызов s up e r ( ) содержал три аргумент а. Поскольку конструкторы могут быть перегружены, super ( ) можно вызывать с и спользованием любой формы, определенной в суперклассе. Выполнится тот конструктор, который дает совпадение по аргументам. Например, далее представлена законченная реализация BoxWe ight, предлагающая конструк­ торы для различных способов создания объекта коробки. В каждом ел учае super ( ) вызыв ается с применением подходящих аргументов. Обратите вни­ мание, что поля width, height и depth в классе Вох сделаны закрыты ми. // Законченная реализация класса BoxWei ght . class Вох { pr ivate douЫe width; pr ivate douЫe heigh t ; private douЫe dep th ; / / Констру ктор , применяемый для клонирования объекта . Вох ( Вох оЬ ) { / / передат ь объект конструктору width = ob . w idth ; height = ob . height ; depth = ob . dept h ; / / Конструктор, используемый в случае указания всех размеров . Box ( douЫe w, douЫe h , douЫe d ) { width = w ; height = h ; depth = d ; / / Конструктор , применяемый в случае, если размеры вообще не указаны . Вох ( ) { / / исполь зовать - 1 для обозначения width = - 1 ; // неини циализированного height = - 1 ; / / объекта коробки depth = - 1 ; // Конструктор , используемый в случае создания объе кта // кубичес кой коробки . Box ( douЫe lеп ) { width = he ight = depth = lеп; / / Вычисли ть и возвратить объем . douЫe vol ume ( ) { return width * hei ght * dep th; // Теперь класс BoxWei ght полност ью реализует все конструкторы . class BoxWeight extends Вох { douЫe wei ght ; / / вес коробки Глава 8 . Наследование 225 / / Конструкт ор , применяемый для клониров ания объекта . BoxWe ight ( BoxWe ight оЬ ) / / передать объект конструктору s uper ( оЬ ) ; weight = ob . we ight ; // Конструктор , используемый в случае указания в сех параметров . BoxWeight ( douЫe w, douЫe h , douЫ e d , douЫe m ) { super ( w , h, d ) ; / / вызвать конструктор супер класса we ight = m ; / / Стандартный конструктор . BoxWeight ( ) { s uper ( ) ; weight = - 1 ; / / Конструктор , исполь зуемый в случае создания объекта кубической коробки BoxWei ght ( douЫe l e n , douЫe m) { super ( l en ) ; we ight = m; c lass Demo Super { puЬlic static void ma i n ( S tring [ ] args ) { new BoxWe ight ( l 0 , 2 0 , 1 5 , 3 4 . 3 ) ; BoxWeight mybox l new BoxWe ight ( 2 , 3 , 4 , 0 . 0 7 6 ) ; BoxWei ght mybox2 BoxWei ght mybox3 new BoxWe ight ( ) ; / / стандарт ный BoxWei ght mycube new BoxWei ght { 3 , 2 ) ; BoxWeight myclone = new BoxWe ight (mybox l ) ; douЫe vol ; vol = myboxl . vol ume ( ) ; System . out . println ( " Объем mybox l равен " + vol ) ; System . out . p rintln ( "Вес myboxl равен 11 + mybox l . weight ) ; System . out . println ( ) ; vol = mybox2 . vol ume ( ) ; Sys tem . out . p rintln ( "Oбъeм mybox2 равен " + vol ) ; System . out . println ( "Вес mybox2 равен " + mybox2 . weight ) ; System . out . println ( ) ; vol = mybox3 . volume ( ) ; System . out . p rintln ( " Oбъeм mybox3 равен " + vol ) ; System . out . println ( "Bec mybox3 равен 11 + mybox 3 . we i ght ) ; S ys tem . out . println ( ) ; vol = myclone . vol ume { ) ; System . out . pr intln ( " Объем myclone равен " + vo l ) ; System . o ut . println ( " Bec myclone равен " + myclone . weight ) ; System . out . p rintln ( ) ; vol = mycube . vol ume ( ) ; System . out . println ( "Oбъeм mycube равен " + vol ) ; System . out . p rintl n ( "Bec mycube равен " + mycube . wei ght ) ; System . out . println ( ) ; 226 Часть 1. Язык Java Вот вывод, генерируемый программой: Объем myboxl разен 3 0 0 0 . 0 Вес mybox l равен 3 4 . 3 Объем mybox2 разен 2 4 • О Вес mybox2 равен 0 . 0 7 6 Объем mybox3 разен - 1 . О Вес mybox3 равен -1 . 0 Объем myclone равен 3 0 0 0 . 0 Вес myclone равен 34 . 3 Объем mycube разен 27 . О Вес mycube равен 2 . 0 Обратите особое внимание на следующий конструктор в BoxWe ight: // Конструктор , nрименяемый для клониро вания объекта . BoxWeight ( BoxWeight оЬ ) { / / nередать объект конструктору super ( оЬ ) ; weight = ob . weight; Здесь вызову super ( ) передается объект типа BoxWeight, а не Вох. По­ прежнему вызывается конструктор Вох ( Вох оЬ ) . Как упоминалось ранее, переменная типа суперкласса может использоваться для ссылки на любой объект, производный от этого класса, т.е. мы можем передавать конструктору Вох объект BoxWe ight. Конечно же, классу Вох известны только свои члены. Давайте повторим ключевые концепции, лежащие в основе super ( ) . Когда подкласс вызывает super ( ) , он вызывает конструктор своего непосредствен­ ного суперкласса. Таким образом, super ( ) всегда ссылается на суперкласс непосредственно над вызывающим классом. Это справедливо даже для мно­ гоуровневой иерархии. Кроме того, вызов super ( ) всегда должен быть пер­ вым оператором, выполняемым внутри конструктора подкласса. Испол ь зо в ание в то ро й ф о рмы кл ю ч е в о г о сло в а super Вторая форма ключевого слова super действует примерно так же, за ис­ ключением того, что всегда относится к суперклассу подкласса, в котором задействована. Вот как она выrлядит: suреr . член Здесь член может быть либо методом, либо переменной экземпляра. Вторая форма super наиболее применима в ситуациях, когда имена членов подкласса скрывают члены с тем же именем в суперклассе. Возьмем следую­ щую простую иерархию классов: // Исnользование super для nреодоления сокрытия имен . class А { int i ; Глава 8. Наследование / / Создать подкласс class В extends А { int i ; В ( int а , int Ь ) s uper . i = a ; i = Ь; 227 путем расширения класса А . / / этот член i скрывает i в А // i в А // i в в void show ( ) System . out . println ( " i в суперклассе : " + s uper . i ) ; System . out . println ( " i в подклассе : " + i ) ; class UseSuper { puЫic static void ma in ( String [ ] arg s ) { В s ubOb = new B ( l , 2 ) ; s ubOb . show ( ) ; Ниже показан вывод, генерируемый программой: i в суперклассе : 1 i в подклассе : 2 Хотя переменная экземпляра i в В скрывает i в А, клю чевое слово super делает возможным доступ к члену i, определенному в суперклассе. Как вы увидите, super можно также использовать для вызова методов, сокрытых под классом. Создание м ногоуровневой иерархии До сих пор мы имели дело с простыми иерархиями кл ассов, которые со­ стояли только из суперкласса и подкласса. Однако можно создавать иерар­ хии, содержащие любое количество уровней наследования. Как уже упомина­ лось, подкласс впол не допустимо применять в качестве суперкласса другого. Например, при наличии трех классов А, В и С кл асс С может быть подклассом В, который является подкл ассом А. Когда возникает ситуация такого типа, каждый под кл асс наследует все характерные черты, обнаруженные во всех его суперклассах. В данном слу чае С наследует все аспекты В и А. Чтобы уви­ деть, чем может быть полезна многоуровневая иерархия, рассмотрим следу­ ющую программу, где подкласс BoxWeight испол ьзуется как суперкл асс для создания подкл асса Sh ipment, представляющего дос тавку коробки. Класс Shipment наследует все признаки BoxWeight и Вох и добавляет поле по име­ ни cost, в котором содержится стоимость доставки такой посылки. // Расширение класса BoxWeight с целью включения стоимости доставки . / / Начать с Вох . clas s Вох { p rivate douЫe widt h ; private douЫe height ; 228 Ч а сть 1. Я зы к Java private douЫ e dept h ; / / Конструктор , применяемый для клонирования объект а . Вох ( Вох оЬ ) { / / передать объект конструктору width = ob . width ; height = ob . height ; depth = ob . dep t h ; / / Конструктор, используемый в случае указания всех размеров . Box ( douЫe w , douЫ e h , douЫe d ) { width = w ; height = h ; depth = d ; / / Конструктор , применяемый в Вох ( ) { width = - 1 ; // height = - 1 ; // // depth = - 1 ; случае , е сли размеры вообще не указаны . использовать - 1 для обозначения неинициализированного объе кта коробки // Конструктор , используемый в случае создания объекта кубической коробки Вох ( douЫe l еп ) { width = height = depth = len ; // Вычислить и возвратит ь объем . douЫ e vol ume ( ) { return width * height * dept h ; / / Добавит ь в е с . class BoxWe ight extends Вох { douЫe weight ; / / вес коробки // Конструкт ор , применяемый для клонирования объе кта . BoxWeight ( BoxWeight оЬ ) / / передать объект конструктору super ( оЬ ) ; weight = ob . weight ; / / Конструктор , используемый в случае указания всех параметров . BoxWei ght ( douЫe w , douЫ e h , douЫe d , douЫ e m ) { super ( w , h , d ) ; / / вызвать конструктор суперкласса weight = m; // Стандартный конструктор . BoxWeight ( ) { super ( ) ; weight = - 1 ; / / Конструктор , используемый в случае создания объекта кубической коробки BoxWeight ( douЫe l e n , douЫe m) { s uper ( l en ) ; weight = m ; Глава 8. На сл едован ие 229 / / Доба вить стоимость доста вки . class Sh ipment extends БoxWei ght douЫe cost ; // Конструктор , примен яемый для клонирования объекта . Shipment ( Sh ipment оЬ ) { / / пер еда ть объект конструктору s uper ( оЬ ) ; cost = ob . cost ; / / Констру ктор, исполь зуемый в случае указания всех параметров . Sh ipment ( douЫe w , douЫe h, douЫe d , douЫe m, douЫe с ) { / / вызвать конструктор суперкласса supe r ( w , h , d, m ) ; cost = с ; / / Ста ндартный конструктор . Shipment ( ) { supe r ( ) ; cost = - 1 ; / / Конструктор, исполь зуемый в случае создания объекта кубической коробки Shipment ( douЫe l e n , douЫe m, do uЫ e с ) { super ( l e n , m) ; cost = с ; class DemoShipment { puЫic static vo i d main ( String [ ] args ) { Shipme nt shipmentl new Shipment ( l 0 , 2 0 , 1 5 , 1 0 , 3 . 4 1 ) ; Shipme nt s hipment2 = new Shipment ( 2 , 3 , 4 , 0 . 7 6 , 1 . 2 8 ) ; douЫe vo l ; vol = sh ipment l . volume ( ) ; System . o ut . println ( "Oбъeм shipment l равен " + vol ) ; S ystem. out . println ( "Вес sh ipment l равен " + sh ipmentl . weigh t ) ; System . o ut . println ( "Cт oимocть до ставки : $ " + sh ipmen tl . cos t ) ; System . out . println ( ) ; vol = sh ipment2 . vol ume ( ) ; System . out . println ( "Oбъeм s hipment2 равен " + vol ) ; Sys tem . out . println ( "Вес shipment2 равен " + sh ipment2 . we ight) ; System . out . println ( " Cтoимocть доставки : $ " + sh ipme nt2 . cos t ) ; Вот вывод, ген ерируемый программой : Объем shipmen tl равен 3 0 0 0 . 0 Бес shipment l равен 1 0 . 0 Стоимость доста вки : $ 3 . 4 1 Объем shipment2 равен 24 . 0 Бес sh ipment2 равен 0 . 7 6 Стоимость доста вки : $ 1 . 2 8 230 Ча сть 1. Язык Java Благодаря наследованию класс Shipment может задействовать определенные ранее классы Вох и BoxWeight, добавляя только ту дополнительную информа­ цию, которая необходима для собственного конкретного приложения. Это часть ценности наследования; оно позволяет многократно использовать код. В примере иллюстрируется еще один важный момент: super () всегда ссы­ лается на конструктор в ближайшем суперклассе. В классе Shipment с по­ мощью super ( ) вызывается конструктор BoxWeight, а в классе BoxWeight конструктор Вох. В рамках иерархии классов, когда конструктору суперкласса требуются аргументы, то все подклассы должны передавать их "вверх по це­ почке наследования': Сказанное верно независимо от того, нужны ли под­ кл ассу собственные аргументы. На заметку! В предыдущей программе вся иерархия классов, включая Вох, BoxWe i gh t и S h i pment, находится в одном файле, что служит только ради удобства. В Java все три класса можно было бы поместить в собственные файлы и скомпилировать по отдельности. На самом деле при создании иерархий классов использование отдельных файлов является нормой, а не исключением. Когда конструкторы выпол няются Когда иерархия классов создана, в каком порядке выполняются конструк­ торы кл ассов, образующих иерархию? Например, при нал и чии подкласса В и суперкласса А конструктор А выполняется раньше конструктора В или наобо­ рот? Ответ заклю чается в том, что в иерархии кл ассов конструкторы завер­ шают свое выполнение в порядке наследования от суперкласса к подклассу. Кроме того, поскольку вызов super () должен быть первым оператором, вы­ пол няемым в конструкторе подкласса, такой порядок остается тем же неза­ висимо от того, применяется sup er ( ) или нет. Есл и s uper ( ) не используется, то будет выполнен стандартный конструктор или конструктор без параме­ тров каждого суперкласса. Выполнение конструкторов демонстрируется в следующей программе: / / Демонстрация выполнения конструкторов . / / Создать суперкла сс . class А { А() { System . out . println ( " Bнyтpи конструктора А . " ) ; / / Создать подкласс путем расширения класса А . clas s В extends А { В() { System . out . p r i ntln ( "Bнyтpи конструктора В . " ) ; / / Создать еще один подкласс путем расширения класса В . clas s С ext ends В { Гла ва 8. Насл ед о в а ние С() 231 { System . out . p rint ln ( " Bнyтpи конструктора С . " ) ; class Cal l ingCons { puЫ i c s t a t i c void ma i n ( S tring [ ] a rgs ) { С с = new С ( ) ; Вот вывод, генерируемый программой: Внутри конструктора А Внутри конструктора В Внутри конструктора С Как видите, конструкторы выполняются в порядке наследования. Е сли хорошо подумать, то имеет смысл, что конструкторы завершают свое выполнение в порядке наследования. Поскольку суперклассу ничего не из­ вестно о каких - либо подклассах , любая инициализация, которую должен вы­ полнить суперк ласс, является отдель ной и возможно обязательной для лю ­ бой инициализации, выполняемой подклассом. С ледовательно, она должна быть завершена первой . Переоп ределен и е методов В иерархии классов, когда метод в подклассе имеет то же имя и сигна­ туру типа, что и метод в его суперклассе, то говорят, что м етод в подклассе переопределяет метод в суперкласс е. При вызове переопределенного мето­ да через его подкласс всегда будет вызываться версия метода , определен­ ная в подклассе. Версия метода , определенная в суперк лассе, будет сокрыта. Рассмотрим следующий пример: // Переопределение методов . class А { int i , j ; A ( int а, int Ь ) { i = а; j = Ь; / / Отобразить значения i и j . vo id show ( ) { S ys tem . out . println ( " i и j : 11 + i + c l a s s В extends А { int k ; В ( int а , int Ь, i n t с ) { supe r ( а , Ь ) ; k = с; 11 11 + j) ; 232 Часть 1. Язык Java / / Отобразить k - переопределяет show ( ) в А . void s how ( ) { Sys tem . out . println ( 11 k : 11 + k ) ; class Ove rride { p uЬ l ic static voi d ma in ( S t ring [ ] args ) { В subOb = new B ( l , 2 , 3 ) ; subOb . show ( ) ; / / вызывается show ( ) из В Ниже показ ан вывод, генерируемый программой: k: 3 Когда метод show ( ) вызывается на объекте типа В, используется версия show ( ) , определенная в классе В. То есть версия show ( ) внутри В переопреде­ ляет версию show ( ) , объявленную в А. При желании получить доступ к версии переопределенного метода из су­ перкласса мож но применить ключевое слово super. Скажем, в приведенном далее классе В внутри версии show ( ) из подкласса выз ывается версия show ( ) из суперкласса, что позволяет отобразить все переменные экземпляра. class В extends А { int k ; В ( i nt а , i nt Ь , i nt с ) { super ( а , Ь ) ; k = с; void show ( ) supe r . show ( ) ; / / вызывается show ( ) из А System . out . p r i ntln ( 11 k : 11 + k ) ; Подставив эту версию класса В в код предыдущего примера, вы увидите такой вывод: i и j: 1 2 k: 3 Здесь super . show ( ) вызывает версию show ( ) из суперкласса. Метод переопределяется только в случае, если и мена и сигнатуры типов двух методов идентичны, а иначе два метода будут просто перегруженными. Взгляните на следующую модифицированную верси ю предыдущего при мера: // Методы с отличающимися сигнатурами типов // являются перегруженными - не переопределенными . class А { int i , j ; A ( i nt а , i nt Ь ) i = а; j = Ь; Глава 8. Наследование / / Отобразить значения i и j . void show ( ) { System . out . println ( "i и j : " + i + " " + 233 j); / / Создать подкласс путем расширения класса А . class В extends А { int k; В ( int а , int Ь , int с ) { s uper ( а , Ь ) ; k = с; / / Перегрузить show ( ) . void s how ( String msg ) { System . out . printl n (msg + k ) ; class Override { puЫic static void main ( S tring ( ] a rg s ) { В s ubOb = new B ( l , 2 , 3 ) ; / / вызывается show ( ) из В subOb . show ( "Этo k: " ) ; subOb . show ( ) ; / / вызывается show ( ) из А Вот вывод, генерируемый программой: Это k: 3 i и j: l 2 Версия метода show ( ) в классе В принимает строковый параметр, что от­ личает его сигнатуру типов от сигнатуры метода show ( ) в классе А, который не принимает параметров. Поэтому никакого переопределения (или сокры­ тия имени) не происходит. Вз амен версия show ( ) в кл ассе В просто перегру­ жает версию show ( ) из кл асса А. Д инамическая диспетчериза ц и я методов Хотя примеры в предыдущем раз деле демонстрируют механику пере­ определения методов, они не показ ывают его воз можности. На самом деле, есл и бы для переопределения методов н е существовало ничего, кроме со­ глашения о пространстве и мен, то о но считалось бы в л учшем случае и н­ тересной особенностью, не обл адая реальной ценностью. Тем не менее, это не так. Переопределение методов лежит в основе од ной из самых мощных концепций Java - диспетчеризации динамических методов. Диспетчеризация динамических методов представляет собой механиз м, с помощью которого вызов переопределенного метода распознается во время выполнения, а не на этапе компиляции. Ди намическая диспетчериз ация методов важна, потому что именно так в Java обеспечивается полиморфизм во время выполнения. 234 Часть 1. Язык Java Давайте начнем с повторения важного принципа: ссылочная переменная типа суперкл асса может ссыл аться на объект подкл асса. Данный факт ис­ пользуется в Java для распознавания вызовов переопределенных методов во время выполнения. А каким образом? Когда переопределенный метод вы­ зы вается через ссылку на суперкласс, версия метода, подлежащая выполне­ нию, выясняется на основе типа объекта, на который производится ссылка в момент вызова. Соответственно, такое выяснение происходит во время выполнения. При ссылке на разные типы объектов будут вызываться разные версии переопределенного метода. Другими словами, именно тип объекта, на который делается ссылка (а не тип ссылочной переменной), определяет, какая версия переопределенного метода будет выполняться. Таким образом, если суперкласс содержит метод, который переопределяется в подк лассе, то при ссыл ке на разные типы объектов через ссылочную переменную типа супер­ кл асса выполняются разные версии метода. Ниже показан пример, предназначенный для и л л юстрации динамической диспетчеризации методов: // Динамическая диспетчеризация методов . class А { void cal lme ( } { System . out . println ( "Bнyтpи метода cal lme ( ) класса А" ) ; clas s В extends А { / / Переопределить callme ( ) . voi d callme ( ) { System . out . print l n ( "Bнyтpи метода callme ( ) класса В " ) ; class С extends А { / / Переопределить callme ( ) . void callme ( ) { System . out . println ( "Внутри метода callme ( ) класса С " ) ; class Dispatch { puЬlic static void main ( String [ ] args ) { / / объект типа А А а = new А ( ) ; / / объект типа В В Ь = new В ( ) ; С с = new С ( } ; / / объект типа С А r; / / получить ссылку типа А / / r ссылается н а объект А r = а; r . callme ( } ; / / вызывается версия cal lme ( } ИЗ А / / r ссылается н а объект В r = Ь; r . callme ( ) ; / / вызывается версия callme ( } из в / / r ссылается на объект С r = с; r . callme ( } ; / / вызывается версия cal lme ( ) из с Глава 8. Нас лед ование 235 Вот вывод, генерируемый программой: Внутри метода cal lme ( ) класса А Внутри метода callme ( ) класса В Внутри метода cal lme ( ) класса С В программе создается один суперкласс по имени А и два его подкласса, в и С. Подклассы В и С переопределяют метод cal lme ( ) , объявленный в А. Внутри метода main ( ) объявляются объекты типов А, В и С. Кроме того, объ­ является ссылка типа А по имени r. Затем в программе переменной r по оче­ реди присваивается ссылка на каждый тип объекта и производится вызов метода cal lme ( ) . В выводе видно, что выполняемая версия cal lme ( ) опре­ деляется типом объекта, на который делается ссылка во время вызова. Если бы версия определялась типом ссылочной переменной r, то вы бы увидели три вызова метода cal lme ( ) класса А. На заметку! Читатели, знакомые с языком С++ или С#, найдут сходство переопределенных методов в Java с виртуальными функциями в этих языках. З а ч ем н уж н ы переопреде л енные м етоды ? Как было указано ранее, переопределенные методы позволяют Java под­ держивать полиморфизм во время выполнения. Полиморфизм важен для ООП по одной причине: он позволяет универсальному классу определять методы, которые будут общими для всех производных от него классов, од­ новременно разрешая подклассам определять индивидуальные реализации некоторых или всех общих методов. Переопределенные методы - еще один способ, которым в Java обеспечивается аспект полиморфизма "один интер­ фейс, несколько методов': Одним из ключей к успешному применению полиморфизма является по­ нимание того, что суперклассы и подклассы образуют иерархию с продвиже­ нием от меньшей специализации к большей. При правильном использовании суперкласс предоставляет все элементы, которые подкласс может задейство­ вать напрямую. Он также определяет те методы, которые производный класс должен реализовать са мостоятельно. Это позволяет подклассу не только гиб­ ко определять собственные методы, но также обеспечивает согласованный интерфейс. Таким образом, комбинируя наследование с переопределенными методами, суперкласс может определять общую форму методов, которые бу­ дут потребляться всеми его подклассами. Дина мический полиморфизм во время выполнения - один из самых мощных механизмов, которыми ООП воздействует на многократное ис­ пользование и надежность кода. Способность существующих библиотек кода вызывать методы для экземпляров новых классов без перекомпиляции с од­ новременным сохранением чистого абстрактного интерфейса является чрез­ вычайно мощным инструментом. 236 Часть 1. Язык Java П рименен ие переопределения методов Давайте рассмотрим более реалистичный пример, в котором используется переопределение методов. В следующей программе создается суперкласс по имени Figure (фигура), который хранит размеры двумерного объекта. В нем также определен метод с именем area ( ) , вычисляющий площадь объекта. От суперкласса Figure наследуются два подкласса - Rectangle (прямоугол ь­ ник) и Triangle (треугольник). Каждый из этих подклассов переопределяет метод area ( ) , так что он возвращает площадь соответственно прямоугольни­ ка и треуголь ника. // Испол ь зование полиморфизма во время выполнения . class Figure { douЫ e d iml ; douЫ e d im2 ; Figure ( douЫe а , douЫe Ь ) { ::iiml = а ; ::iim2 = Ь ; douЫe a rea ( ) System . out . println ( "Плoщaдь для Figure не определена . " ) ; return О ; class Rectangle extends Figure Rectangle ( douЫe а, douЫ e Ь ) { super ( а , Ь ) ; / / Переопределить а rea ( ) для прямоуголь ника . douЫ e a rea ( ) { System . out . println ( "Bнyтpи a rea ( ) для Rectangle . " ) ; return diml * dim2 ; class Tri angle extends Figure T ri angle ( douЫe а, douЫe Ь) { super ( а , Ь ) ; / / Переопределить а rea ( ) для прямоуголь ного треугольника . douЫe area ( ) { System . out . p ri nt l n ( "Bнyтpи a rea ( ) для Triangle . " ) ; return diml * dim2 / 2 ; class Fi ndAreas { puЫ i c static vo i d main ( St ring [ ] args ) Figure f = new Figure ( l O , 1 0 ) ; Rectangle r = new Rectangle ( 9 , 5 ) ; Tri angle t = new Triangle ( l O , В ) ; Fig ure figre f ; Глава 8 . Насл едован и е 237 figref = r ; System . out . println ( "Плoщaдь равна " + figref. area ( ) ) ; figref = t ; System . out . println ( " Плoщaдь равна " + figre f . area ( ) ) ; figref = f ; System . out . println ( " Плoщaдь равна " + figref . area ( ) ) ; Ниже показан вывод программы: Внутри area ( ) для Rectangle . Площадь равна 4 5 Внутри area ( ) для Triangle . Площадь равна 4 0 Площадь для Figure не определена . Площадь равна О С помощью дуальных механизмов наследования и пол иморфизма во вре­ мя выполнения можно определить один согласованный интерфейс, который применяется несколькими разными, но связанными типами объектов. В дан­ ном случае, если объект является производным от Figure, то его п лощадь можно получить, вызвав метод area ( ) . Интерфейс этой операции одинаков вне зависимости от того, какой тип фигуры используется. Использование абстра ктн ых классов Бывают ситуации, когда желател ьно определить суперкласс, который объявляет структуру заданной абстракции, не предоставляя полные реа­ лизации методов. То есть иногда нужно создать суперкласс, определяющий только обобщенную форму, которая будет применяться всеми его подклас­ сами, оставляя каждому подклассу возможность заполнить детали. Такой класс определяет природу методов, подлежащих реал изации в подклассах. Ситуация подобного рода может возникнуть, когда суперкласс не способен создать осмысленную реализацию метода. Это относится к классу Figu re, использованному в предыдущем примере. Определение метода area ( ) явля­ ется просто заполнителем. Он не будет рассчитывать и отображать площадь объекта любого вида. При создании собственных библиотеки классов вы заметите, что нередко метод не имеет осмысленного определения в контексте своего суперкласса. Справиться с такой ситуацией можно двумя способами. Один из способов, как было показано в предыдущем примере, предусматривает просто выдачу пред­ упреждающего сообщения. Хотя этот подход может быть полезен в определен­ ных ситуациях, скажем, при отладке, обычно он не подходит. У вас могут быть методы, которые должны переопределяться в подклассе, чтобы подкласс имел какой-нибудь смысл. Возьмем к ласс T r i angle. Он не имеет смысла, если ме­ тод a rea ( ) не определен. В данном случае нужен какой-то способ гарантиро­ вания того, что подкласс действительно переопределяет все необходимые ме­ тоды. В Java проблема решается посредством абстрактных методов. 238 Часть 1 . Я зык Java Вы можете потребовать, чтобы некоторые методы были переопределены в подклассах, указав модификатор abstract. Иногда их называют методами, подпадающими под ответственность подкласса, потом у что они не имеют реализ ации, указ анной в суперклассе. Таким образом, подкласс обяз ан их переопредел ить - он не может просто использовать версию, определенную в суперклассе. Для объявления абстрактного метода применяется следующая общая форма: abst ract тип имя ( список -параметров ) ; Как видите, тело метода отсутствует. Любой класс, содержащи й один или несколько абстрактных методов, тоже дол жен быть объявлен абстрактным. Чтобы объявить класс абстрактным, перед кл ючевым словом class в начале объявления класса просто использ у­ ется ключевое слово abstract. Объектов абстрактного класса не бывает, т.е. экз емпляр абстрактного класса нельзя создать напрямую с помощью опера­ ции new. Подобного рода объекты был и бы бесполез ными, т.к. абстрактный класс не определен полностью. Кроме того, не допускается объявлять аб­ страктные конструкторы или абстрактные статические методы. Любой под­ класс абстрактного класса дол жен либо реализовать все абстрактные методы суперкласса, либо сам быть объявлен абстрактным. Далее представлен простой пример класса с абстрактным методом, за ко­ торым следует класс, реализ ующий этот метод: // Простая демонстрация применения abstract . abstract class А { abstract void cal lrne ( ) ; / / Конкре т ные методы в абстрактных классах по- прежнему ра зрешены . voi d callrnetoo ( ) { Systern . out . println ( " Этo конкретный метод . " ) ; class В extends А { void cal lrne ( ) { Systern . out . printl n ( " Peaлизaция cal lrne ( ) в классе В . " ) ; class AЬstract Derno { puЫ i c static voi d rnain ( String [ ] a rgs ) { В Ь = new В ( ) ; Ь . cal lrne ( ) ; b . ca l lrnetoo ( ) ; Обратите внимание, что никаких объектов класса А в программе не объ­ явлено. Как уже упоминалось, создать экз емпляр абстрактного класса не­ воз можно. Е ще один момент: в кл ассе А реал изован конкретны й метод ca l lmetoo () , что вполне приемлемо. Абстрактные классы могут включать столько реализ аций, сколько соч тут нужным. Гл а в а 8. Наследов а ние 239 Хотя абстрактные классы нельзя задействовать для создания объектов, их можно применять для создания ссылок на объекты, поскольку подход Java к полиморфизму во время выполнения обеспечивается через использование ссылок на суперклассы. Таким образом, должна быть возможность создания ссылки на абстрактный класс, чтобы ее можно было применять для указания на объект подкласса. Ниже вы увидите, как используется это средство. Приведенный ранее класс Figure можно усовершенствовать за счет при­ менения абстрактного класса. Поскольку для неопределенной двумерной фи­ гуры нет осмысленного понятия площади, в следующей верси и программы метод area ( ) в Fi gure объявляется как абстрактный. Конечно, теперь все классы, производные от Figure, должны переопределять area ( ) . / / Использование абстрактных методов и классов . abstr act class Figure { douЫe diml ; douЫe dim2 ; Figure ( douЫe а , douЫ e Ь ) { diml = а ; dim2 = Ь ; / / Теперь area ( ) - абстрактный ме тод . abstract douЫe area ( ) ; class Rectangle extends Fi gure Rectangle ( douЫe а, douЫe Ь) { super ( а , Ь ) ; // Переопределить area ( ) для прямоуголь ника . douЫe area ( ) { System . out . println ( "Bнyтpи area ( ) для Rectangle . " ) ; return diml * dim2 ; class Tr iangle extends Figure { Tr iangle ( douЫ e а , douЫ e Ь ) { super ( a , Ь ) ; / / Переопределить area ( ) для прямоуголь ного треугольника . douЫ e ar ea ( ) { Sys tem . out . println ( " Внутри area ( ) для Triang l e . " ) ; return diml * dim2 / 2 ; class AЬstra ctAreas p uЫ i c static voi d ma in ( S tring [ ] args ) // Figure f = new Figure ( l O , 1 0 ) ; / / теперь недопустимо Rectangle r = new Rectang le ( 9 , 5 ) ; Tria ngle t = new Tri angle ( 1 0 , 8 ) ; Figure figre f ; / / нормально, объект н е создается 240 Часть 1. Язык Java figref = r ; System . out . println ( "Плoщaдь равна " + figref . area ( ) ) ; figref = t ; System. out . print ln ( " Плoщaдь равна " + figref . area ( ) ) ; Как видно из комментария внутри ma i n ( ) , объявлять объекты типа Fi gure больше невозможно, потому что теперь он абстрактный. И все под­ классы Figure должны переопределять метод area ( ) . Чтобы удостовериться в этом, попробуйте создать подкласс, не переопределяющий метод area ( ) . Вы получите ошибку на этапе компиляции. Несмотря на невозможность создания объекта типа Fi gure, разрешено создавать ссылочную переменную типа Figure. Переменная f i g r e f объяв­ лена как ссылка на Figure, т.е. ее можно использовать для ссылки на объект любого класса, производного от Figure. Как объяснялось ранее, именно че­ рез ссылочные переменные типа суперкласса переопределенные методы рас­ познаются во время выполнения. И с пользова ние кл ючевого слова f inal с на сл е д ованием Ключевое слово f i n a l применяется в трех ситуациях. Первая из них его можно использовать для создания эквивалента именованной константы. Такое применение было описано в предыдущей главе. Две других ситуации использования final касаются наследования и рассматриваются далее. И с п ол ьзов а н и е ключ е во го слов а final для п р едотвр а ще н ия п е р е оп р ед ел е н ия Наряду с тем, что переопределение методов является одной из самых мощных функциональных средств Java, иногда его желательно предотвра­ щать. Чтобы запретить переопределение метода, в начале его объявления по­ надобится указать ключевое f i n a l в качестве модификатора. Методы, объ­ явленные как final , не могут быть переопределены. Применение ключевого слова final демонстрируется в следующем фрагменте кода: class А [ final void meth ( ) [ System . out . println ( "Этo метод final . " ) ; class В extends А [ void meth ( ) [ / / ОШИБКА ! Переопределять нельзя . System . out . println ( " Не разрешено ! " ) ; Глава 8. Наследование 241 Поскольку метод meth ( ) объявлен как final, его нельзя переопределять в классе В. При попытке это сделать возникнет ошибка на этапе компиляции. Методы, объявленные как f i na l, иногда могут обеспечить повышение производительности: компилятор способен встраивать их вызовы, потому что он "знает'; что они не будут переопределяться в подклассе. Когда компи­ лятор Java встречает вызов небольшого метода final, он часто может копи­ ровать байт-код для подпрограммы непосредственно в скомпилированный код вызывающего метода, тем самым устраняя накладные расходы по вызову метода. Встраивание возможно только с методами final. Обычно компиля­ тор Java распознает вызовы методов динамически во время выполнения. Это называется поздним связыванием. Но поскольку методы final не могут быть переопределены, их вызов может распознаваться на этапе компиляции. Это называется ранним связыванием. Использование ключевого слова final для предотвращен ия наследован и я Иногда нужно предотвратить наследование класса. Для этого перед объяв­ лением класса укажите ключевое слово final. Объявление класса как final также неявно объявляет все его методы как final. Вполне ожидаемо объяв­ лять класс как abstract и final одновременно не разрешено, поскольку аб­ страктный класс сам по себе неполный и в обеспечении полных реализаций полагается на свои подклассы. Вот пример класса final: final class А // . . . / / Следующий класс недопустим . class В extends А / / ОШИБКА ! Создавать подкласс кла сса А нель зя // . . . В комментариях видно, что класс В не может быть унаследован от А, т.к. А объявлен с ключевым словом final. На заметку! Начиная с версии JDK 1 7, в Java появилась возможность запечатывать класс. Запеча­ тывание предлагает тонкий контроль над наследованием и рассматривается в главе 1 7. Вы ведение т и п о в л о кал ь н ых пере м ен н ых и н а с л ед о ва ние В главе 3 объяснялось, что в версии JDK 10 к языку Java было добавлено вы­ ведение типов локальных переменных, которое поддерживается контекстно­ чувствительным ключевым словом var. Важно иметь четкое представление о том, как работает выведение типов в иерархии наследования. Вспомните, что ссылка на суперкласс может ссылаться на объект производного класса, и та- 242 Ч а сть 1. Язык Java кое средство я вляется частью поддержки полиморфизма в Java. Однако важ­ но помнить, что при использовании выведения типов локальных переменных выведенный тип переменной базируется на объявленном типе ее инициали­ затора. Следовател ьно, если инициали затор относится к типу суперк ласса, то он и будет выведенным ти пом переменной. Не и меет значения, является ли фактический объект, на который ссылается инициали затор, экземпляром производного класса. Например, взгляните на показанную далее программу: / / При работе с наследованием выведенным типом я вляется объявленный // тип инициализатора и он может отличаться от производного // типа объе кта, на который ссылается инициализатор . class MyCl ass { // . . . class FirstDer ivedClass extends MyCl ass { int х ; // . . . class S econdDe rivedCl ass extends FirstDerivedCl ass { int у ; // . . . class Type inferenceAnd i nher itance / / Возвратить некоторый тип объе кта MyClas s . s tatic MyCl a s s getObj ( int which ) { switch ( whicr1 ) { case О : r e t u r n new MyCl a s s ( ) ; case 1 : return new FirstDe rivedCla ss ( ) ; de faul t : return new Se condDer ivedClas s ( ) ; puЫ ic s tatic void mai n ( S t r ing [ ] args ) { // Несмотря на то что ge tObj ( ) возвращает различные типы // объектов в иерархии наследования MyCla s s , объявленным // типом возвращаемого значения является MyClas s . / / В результате во в сех трех показанных зде сь случаях // предполагае т с я , что типом переме нных является MyCl a s s , / / х о т я получают ся разные производные типы объектов . // В этом случае ge tObj ( ) возвращает объект MyClas s . var те = getObj ( О ) ; / / В этом случае getObj ( ) возвращает объект Fi r s t De r ivedClass . var mc2 = getObj ( 1 ) ; / / в этом случае getObj ( ) воз вращает объект S econdDe ri vedClass . var mсЗ = ge tOb j ( 2 ) ; // // // // // Поскольку 'гиnы mc2 и mсЗ выводятсq как MyClass ( т . к . возвращаемым типом getObj ( ) является MyClas s ) , то ни mc2 , ни mсЗ не могут получить доступ к пол ям , объявленным в FirstDe rivedClass или Se condDe rivedClass . mc2 . x 1 0 ; // Ошибка ! Класс MyClass не имеет поля х . mсЗ . у = 1 0 ; / / Ошибка ' Класс MyClass не имеет поля у . 243 Глава 8. Наследование В программе создается иерархия, состоящая из трех классов, на вершине которых находится MyClass. Класс FirstDerivedClass определен как под­ класс MyClass, а SecondDerivedClass - как подкласс FirstDerivedClass. Затем с применением выведения типа создаются три переменные с име­ нами mc, mc2 и mсЗ путем вызова getObj ( ) . Метод getObj ( ) имеет воз­ вращаемый тип MyClass (суперкласс), но в зависимости от передаваемо­ го аргумента возвращает объекты типа MyClass, FirstDeri vedClass или SecondDeri vedClass. Как видно в отображенных результатах программы, выведенный тип определяется возвращаемым типом getObj ( ) , а не фактиче­ ским типом полученного объекта. Таким образом, все три переменные будут иметь тип MyClass. Класс Obj ect Существует один особый класс Obj ect, определенный в Java. Все осталь­ ные классы являются подклассами Obj ect, т.е. Obj ect представляет собой суперкласс для всех остальных классов. Это означает, что ссылочная пере­ менная типа Obj ect может ссылаться на объект любого дpyroro класса. Кроме того, поскольку массивы реализоваllы в виде классов, переменная типа Obj ect также может ссылаться на любой массив. В классе Obj ect определены методы, описанные в табл. 8.1, которые до­ ступны в любом объекте. Табnмца 8.1 . Методы кпасса OЬject "'i Obj ect clone ( ) Создает новый объект, который совпадает с клонируемым объектом boolean equals ( Obj ect объект) Определяет, равен ли один объект другому void finalize ( ) Вызывается перед удалением неиспользуемого объекта. (Объявлен устаревшим в JDK 9.) Class<?> getClass ( ) Получает класс объекта во время выполнения int hashCode ( ) Возвращает хеш-код, ассоциированный с вызывающим объектом void notify ( ) Возобновляет выполнение потока, ожидающего вызывающий объект void notifyAll ( ) Возобновляет выполнение всех потоков, ожидающих вызывающий объект String toString ( ) Возвращает строку, которая описывает объект 244 Часть 1. Язык Java Окончание табл. 8.1 void wait ( ) void wait ( long шmnиceICJ,'Щt) void wait ( long миллисеlСJ,'Щf, int наносеIСJ,'Щt) Ожидает другого потока выполнения Методы getClass ( ) , notify ( ) , notifyAl l ( ) и wait ( ) объявлены как final. Остальные методы можно переопределять. Перечисленные в табл. 8. 1 методы описаны в других местах книги. Тем не менее, обратите сейчас вни­ мание на два метода: equals ( ) и toString ( ) . Метод equals ( ) сравнивает два объекта. Он возвращает true, если объекты равны, и false в противном случае. Точное определение равенства может варьироваться в зависимости от типа сравниваемых объектов. Метод toString ( ) возвращает строку, со­ держащую описание объекта, для которого он вызывается. Также этот метод вызывается автоматически, когда объект выводится с помощью println ( ) . Многие классы переопределяют метод toString ( ) , что позволяет им адаптировать описание специально для типов объектов, которые они создают. И последнее замечание: обратите внимание на необычный синтаксис воз­ вращаемого типа для getClass ( ) . Он имеет отношение к средству обобщений языка Java, которое будет рассматриваться в главе 14. ГЛ А ВА 9 Пакеты и интерфейсы В этой главе рассматриваются два самых инновационных средства Java: пакеты и интерфейсы. Пакеты представляют собой контейнеры для клас­ сов. Они используются для отделения пространства имен класса. Например, создав класс по имени List и сохранив его в собственном пакете, можно не беспокоиться о том, что он будет конфликтовать с другим классом по имени List, который находится где-то в другом месте. Пакеты хранятся в иерар­ хическом порядке и явно импортируются в определения новых классов. Как будет показано в главе 16, пакеты также играют важную роль в модулях. В предыдущих главах вы видели, что методы определяют интерфейс к дан­ ным в классе. Ключевое слово interface позволяет полностью абстрагиро­ вать интерфейс от его реализации. С помощью interface указывается набор методов, которые могут быть реализованы одним или несколькими классами. В своей традиционной форме интерфейс сам по себе не определяет никакой реализации. Хотя интерфейсы похожи на абстрактные классы, они облада­ ют дополнительной возможностью: класс может реализовывать более одно­ го интерфейса. В противоположность этому класс может быть унаследован только от одного суперкласса (абстрактного либо иного). П акет ы В предыдущих главах имя каждого примера класса принадлежало одно­ му и тому же пространству имен. Таким образом, каждому классу необхо­ димо было назначать уникальное имя, чтобы избежать конфликтов имен. Без какого-либо способа управления пространством имен через некоторое время удобные описательные имена для отдельных классов могут попросту закончиться. Кроме того, нужно каким-то образом гарантировать то, что имя, выбранное для класса, будет достаточно уникальным и не конфликту­ ющим с именами, которые другие программисты назначили своим классам. (Представьте себе небольшую группу программистов, спорящих за право ис­ пользовать "Foobar" в качестве имени класса. Или вообразите ситуацию, ког­ да все сообщество в Интернете выясняет, кто первым назвал класс "Espresso�) К счастью, в Java предоставляется механизм для разделения пространства имен классов на более управляемые фраrменты - пакеты. Пакет является 246 Часть 1. Язык Java как механизмом именования, так и механизмом управления видимостью. Вы можете определять классы внутри пакета, которые не доступны коду вне па­ кета. Вы также можете определять члены класса, которые видны только дру­ гим членам классов в том же пакете. Это позволяет вашим классам хорошо знать друг друга, но не раскрывать такие знания остальному миру. О п ределение п акета Создать пакет довольно легко: понадобится просто поместить в начало файла с исходным кодом Java оператор package. Любые классы, объявленные в данном файле, будут принадлежать указанному пакету. Оператор package определяет пространство имен, в котором хранятся классы. Если оператор package отсутствует, тогда имена классов помещаются в стандартный пакет, не имеющий имени. (Вот почему раньше вам не приходилось беспокоиться о пакетах.) Хотя стандартный пакет пригоден в коротких учебных программах, он не подходит для реальных приложений. Вы будете определять пакет для своего кода почти всегда. Вот общая форма оператора package: package пакет; Здесь паке т представляет и мя пакета. Например, следующий оператор создает пакет по имени mypackage: package mypa ckage ; Как правило, в Java для хранения пакетов применяются каталоги файло­ вой системы, и именно такой подход задействован в примерах, приводимых в книге. Скажем, файлы . class для любых классов, которые объявляются как часть mypackage, должны храниться в каталоге с именем mypackage. Помните о том, что регистр символов имеет значение, а имя каталога должно точно со­ впадать с именем пакета. Один и тот же оператор package может находиться в нескольких файлах. Оператор pac kage лишь указывает, к какому пакету принадлежат классы, определенные в файле. Это не исключает, что другие классы в других файлах могут быть частью того же самого пакета. Большинство пакетов в реальных приложениях разнесено по многим файлам. Допускается создавать иерархию пакетов, для чего нужно просто отделять имя каждого пакета от имени пакета над ним с помощью точки. Общая фор­ ма оператора многоуровневого пакета выглядит следующим образом: package па кетl [ . па кет2 [ . па кет]] J ; Иерархия пакетов должна быть отражена в файловой системе на машине для разработки приложений Java. Например, объявленный ниже пакет дол­ жен храниться в папке а \Ь\с в среде Windows: pac kage а . Ь . с ; Имена пакетов должны выбираться крайне аккуратно, т.к. нельзя переиме­ новать пакет, не переименовав каталог, в котором хранятся классы. Глава 9 . П а ке ты и и нте р ф ей сы 247 П о и с к пакето в и CLASSPATH Как только что объяснялось, пакеты обычно отражаются посредством ка­ талогов. Тогда возникает важный вопрос: как испол няющая среда Java узнает, где искать создаваемые вами пакеты? Что касается примеров в этой главе, то ответ состоит из трех частей. Во-первых, по умол чанию исполняющая среда Java в качестве начал ьной точки использует текущий рабочий каталог. Таким образом, есл и ваш пакет расположен в каком-то подкаталоге внутри текуще­ го каталога, то он будет найден. Во-вторых, вы можете указать путь или пути к каталогам, установи в переменную среды CLASSPATH. В-третьих, вы можете при менить параметр -classpa th при запуске java и j avac, чтобы указать путь к с воим классам. Полезно отметить, что начиная с JDK 9, пакет может быть частью модуля и потому находиться в пути к модулю. Однако обсужде­ ние модулей и путей к модуля м откладывается до главы 16. Сейчас мы будем использовать только пути к классам. Например, рассмотрим следующую спецификацию пакета: pa c kage mypac k ; Чтобы программа могла найти пакет mypack, е е можно либо запустить и з каталога непосредственно над mypack, л и б о переменная среды CLA S S PATH должна вклю чать путь к mypack, либо при запуске программы через java в параметре -cla sspa th должен быть указан путь к mypack. Когда применяются последние два способа, путь к классу не должен содер­ жать само имя mypack. Он должен прос то указывать путь к mypack. Скажем, если в среде Windows путем к mypack является: C : \MyPrograms \Java\mypack тогда путем к кл ассу для mypack будет: C : \MyPrograms \ Java Испытать примеры, приведенные в книге, проще всего, создав каталоги па­ кетов внутри текущего каталога разработки, поместив файлы . class в соот­ ветствующие каталоги и затем запустив программы из каталога разработки. Именно такой подход используется в рассматриваемом далее примере. К ратк и й пр и мер пакета Приняв к сведени ю предыдущее обсуждение, можете испытать показан- ный ниже простой пакет: / / Простой паке т . pa c kage mypack; class Balance { St ring name ; douЫe ba l ; Balance ( String n , douЫe Ь ) { name = n ; bal = Ь ; 248 Часть 1. Язык Java void show ( ) { i f (bal< O ) System . out . print ( " - - > " ) ; System . out . println ( name + " · $ " + ba l ) ; class AccountBa lance { puЫ i c static voi d ma in ( S tring [ ] args ) { Balance [ ] current = new Balance [ 3 ] ; cu rrent [ O ] new Balance ( " K . J . Fielding " , 1 2 3 . 2 3 ) ; current [ l ] = new Balance ( "Will Tel1 " , 1 5 7 . 02 ) ; current [ 2 ] = new Ba lance ( " Tom Jac kson " , - 1 2 . 3 3 ) ; fo r ( i nt i =O ; i < 3 ; i + + ) current [ i ] . show ( ) ; Назначьте файлу имя Accoun tBalance . j ava и поместите его в каталог mypac k. Скомпилируйте файл AccountBa l ance . j ava. Удостоверьтесь в том, что результирующий файл . class тоже находится в каталоге mypac k. Затем по­ пробуйте выполнить класс AccountBa lance с применением следующей ко­ манды: j ava mypack . Accou ntBala nce Помните, что при вводе этой команды вы должны находиться в каталоге выше mypack. (Кроме того, для указания пути к mypack вы можете прибегнуть к одному из двух других способов, описанных в предыдущем разделе.) Как объяснялось ранее, класс AccountBalance теперь является частью па­ кета mypack, т.е. он не может быть выполнен сам по себе. Другими словами, вы не можете использовать такую команду: j ava AccountBalance Класс AccountBalance должен быть уточнен именем пакета. Пакеты и доступ к членам классо в В предшествующих главах вы ознакомились с различными аспектами меха­ низма управления доступом в Java и его модификаторами доступа. Например, вы уже знаете, что доступ к закрытому члену класса предоставляется только другим членам этого класса. Пакеты добавляют еще одно измерение к управ­ лению доступом. Вы увидите, что Java предоставляет множество уровней за­ щиты, которые позволяют с высокой степенью детализации управлять види­ мостью переменных и методов внутри классов, подклассов и пакетов. Классы и пакеты являются средствами инкапсуляции и содержания в себе пространства имен, а также области видимости переменных и методов. Пакеты действуют в качестве контейнеров для классов и других подчиненных пакетов. Классы действуют как контейнеры для данных и кода. Класс - это наименьшая единица абстракции Java. 249 Гла ва 9. П ак ет ы и и нте р фей сы Что касается взаимодействия между классами и пакетами Java, то существуют четыре категории видимости для ч ленов класса: • подклассы в том же самом пакете; • не подклассы в том же самом пакете; • подклассы в других пакетах; • классы, которые не находятся в том же самом пакете и не являются под­ классами. Три модификатора доступа, private, puЬl ic и protected, предоставляют различные способы создания множества уровней доступа, требуемых этими категориями. Взаимосвязь между ними описана в табл. 9. 1. Таблица 9.1 . Доступ к членам классов Тот же класс Да Да Нет Да Да Да Да Да Не подкласс из того же пакета Не т Да Да Да Подкласс из другого пакета Нет Нет Да Да Не подкласс из другого пакета Нет Нет Нет Да Подкласс из того же пакета Хотя механизм управления доступом в Java может показаться сложным, его можно упростить следующим образом. Ко всему, что объявлено как puЬlic, можно получать доступ из разных классов и разных пакетов. Все, ч то объявлено как priva t e, не может быть видимым за пределами его класса. Когда у ч лена нет явной спецификации доступа, он виден подклассам, а также другим классам в том же пакете. Такой доступ принят по умолчанию. Если вы хотите, чтобы элемент был видимым за пределами вашего текущего пакета, но только классам, которые напрямую являются подклассами вашего класса, тогда объявите этот элемент как protected. Правила доступа к членам классов, приведенные в табл. 9. 1, применимы только к ч ленам классов. Класс, не являющийся вложенным, имеет только два возможных уровня доступа: стандартный и открытый. Korда класс объ­ явлен как puЫ ic, он доступен за пределами своего пакета. Если класс имеет стандартный доступ, то к нему может получать доступ только другой код в том же пакете. Когда класс является открытым, он должен быть единствен­ ным открытым классом, объявленным в файле, а файл должен иметь такое же имя, как у класса. 250 Ч асть 1. Язык Java На заметку! Средство модулей также может влиять на доступность. Модули описаны в главе 1 6. Пример, демонстрирующий использован ие модифи каторов доступа В показанном далее примере демонстрируются все комби нации мод ифи ­ каторов управления доступом. В примере имеются два пакета и пять классов. Не забывайте, что кл ассы для двух разных пакетов должны храниться в ката­ логах с именами, которые совпадают с именами соответствующих пакетов в данном случае p l и р2. В файле с исходным кодом для первого пакета определены три класса: Protection, Derived и SamePackage. В первом классе определяются четыре переменных типа int с каждым допустимым режимом доступа. Переменная n объявлена со стандартным доступом, n_pri - с доступом p rivate, n_pro с доступом protected, а n _p ub - с доступом puЫic. Все последующие классы в рассматриваемом при мере будут пытаться по­ лучить доступ к переменным экземпляра класса Protection. Строки, кото­ рые не будут компил ироваться из-за ограничений доступа, закомментирова­ ны. Перед каж дой из таких строк находится комментарий с перечислением мест, из которых этот уровень защиты разрешит доступ. Второй класс, Deri ved, является подклассом Protect ion в том же пакете p l, что дает Derived доступ ко всем переменным в Protection кроме закры­ той переменной n_pri. Третий класс, SamePackage, не является подклассом P rotection, но находится в том же самом пакете и тоже имеет доступ ко всем переменным кроме n_p ri. Вот содержимое файла Protection . j ava: package pl ; puЫic class Protection int n = 1 ; private int n_pri = 2 ; protected int n_pro = 3 ; puЫ i c i nt n_pub = 4 ; puЫ i c Protect ion ( ) { System . out . println ( "Koнcтpyктop б азового класса " ) ; System . out . println ( "n = " + n ) ; System . out . println ( "n_pri = " + n_pri ) ; System . out . println ( "n _pro = " + n_p ro ) ; System . out . println ( "n pub = " + n_pub ) ; Ниже приведено содерж имое файла Der ived . j ava: package p l ; class Derived extends Protection ( De rived ( ) { System . out . println ( "Koнcтpyктop производного класса " ) ; System . out . println ( "n = " + n ) ; Гл ава 9. П акеты и инте рф ейсы / / Тол ь ко класс . // Systern . out . print1n ( " n_pri Systern . out . println ( "n_pro Systern . out . println ( " n_pub = = = 251 " 4 + n_pri ) ; " + n_p ro ) ; " + n_pub ) ; Далее показано содержимое файла Same Package . j ava: pac kage p l ; cl as s Sarne Pac kage SarnePackage ( ) { Protection р = new Protection ( ) ; S ystern . out . printl n ( " Koнcтpyктop класса из того же пакета " ) ; S ystern . out . printlп ( " n = " + p . n ) ; / / Толь ко класс . / / Systern . out . println ( " n_pri = " + p . n_p ri ) ; Systern. out . p rintln ( " n_pro Systern . out . println ( 11 n _pub = = 11 11 + p . n_pro ) ; + р . n_pub ) ; Ниже представлен исходный код другого пакета, р2. Два класса, опре­ деленные в р2, охватывают остальные два условия, на которые влияет управление доступом. Первый к ласс, Protection2, является подклассом p l . Protection, что дает ему доступ ко всем переменным p l . Protect ion кроме n_p r i (поскольку она з акрытая) и n - переменной, объявленной со стандартной з ащитой. Вспомните, что по умолчанию разрешен доступ только из класса или паке­ та, а не из подклассов вне пакета. Наконец, класс OtherPackage имеет доступ только к одной переменной n_pub, которая была объявлена открытой. Вот содержимое файла Protection2 . j ava: package р2 ; clas s Protection2 extends pl . Protection { Protect ion2 ( ) { Systern . out . printl n ( " Koнcтpyктop производного класса из другого пакета " ) ; / / Толь ко класс или пакет . / / Systern . out . println ( 11 n = 11 + n) ; / / Тол ь ко класс . / / Systern . out . println ( 11 n_pri = " + n_p ri ) ; Systern . out . print1n ( 11 n_pro = " + n_pro ) ; Systern . out . println ( "n_pub = " + n_pub ) ; Далее приведено содержимое Other Package . j ava: package р2 ; class Oth e r Package 252 Ч а сть 1. Язык Java OtherPackage ( ) { pl . Protection р = new p l . Protect ion ( ) ; System . out . println ( "Конструктор класса из другого пакета " ) ; / / Только класс или пакет . // System . out . println ( " n = " + p . n ) ; / / Только класс . // System . out . println ( "n_pri = " + p . n_pri ) ; // Только класс , подкласс или пакет . // System . out . println ( "n_pro = " + p . n_pro ) ; System . out .println ( "n_pub " + p . n_pub ) ; Если вы хотит е испытать два созданных пакета, то ниже предлагаются два т есто вых файла, которыми можно воспользоваться. Вот тестовый файл для пакета pl: / / Тестирование пакета p l . package pl ; / / Создать э кземпляры различных классов в p l . puЫ ic class Demo { puЫic sta tic void main ( String [ ] args ) Protection оЫ = new Protection ( ) ; De rived оЬ2 = new Derived ( ) ; Same Package оЬЗ = new SamePac kage ( ) ; А вот тестовый файл для пакета р2: // Тестирование пакета р2 . package р2 ; / / Создать э кземпляры различных классов в р2 . puЫic class Demo { puЫic static void ma in ( Str ing [ ] a rg s ) { Protection2 оЫ = new Protection2 ( ) ; OtherPackage оЬ2 = new OtherPackage ( ) ; } И мпортирова н ие пакетов С учетом тоrо, что пакеты существуют и являются хорошим механиз мом отделения различных классов друr от друга, леrко понять, почему все встро­ енные классы Java хранятся в пакетах. В стандартном пакете без имени нет ни одноrо базового класса Java; все стандартные классы хранятся в каком-то именованном пакете. Поскольку классы в пакетах должны полностью уточ­ няться с помощью одноrо или нескольких имен пакетов, набор длинно го пути к пакету, разделенного точками, для каждого используемого класса может стать утомительным. По этой причине в составе Java имеется о ператор им­ портирования import, ко торый позволяет сделать видимыми определенные классы или целые пакеты. После импортирования на класс можно ссылат ься Глава 9. П а ке ты и и нт ер ф ей сы 253 напрямую с применением только его имени. Оператор import является удоб­ ным инструментом для программиста и формально не нужен для написания законченной программы на Java. Тем не менее, если вы собираетесь ссылаться на несколько десятков классов в своем приложении, то оператор import су­ щественно сократит объем набора. В файле с исходным кодом на Java операторы import располагаются сра­ зу после оператора package (если он есть) и перед любыми определениями классов. Общая форма оператора import выглядит следующим образом: import пакет] [ . пакет2] . ( имя-класса * ) ; Здесь pkgl - имя пакета верхнего уровня, а pkg2 - имя подчиненного пакета внутри внешнего пакета, отделенное точкой. На практике ограниче­ ния на глубину иерархии пакетов отсутствуют за исключением тех, что на­ кладываются файловой системой. В конце оператора import указывается либо явное имя класса (имя-кла сса) , либо звездочка (*), которая сообщает компилятору Java о необходимости импортирования всего пакета. Ниже де­ монстрируется использование обеих форм: import j ava . util . Dat e; import j ava . io . * ; 1 Все стандартные классы Java SE, входящие в состав Java, начинаются с име­ ни j ava. Базовые языковые функции хранятся в пакете по имени j ava . lang. Обычно вам приходится импортировать каждый пакет или класс, с которым планируется работа, но поскольку язык Java бесполезен без значительной ча­ сти функциональности пакета j ava . lang, он неявно импортируется компи­ лятором для всех программ. Это эквивалентно наличию в начале кода всех ваших программ следующей строки: import j ava . lang . * ; Если в двух разных пакетах, импортированных с помощью оператора import со звездочкой, существуют классы с одинаковыми именами, то ком­ пилятор никак не будет реагировать, пока вы не попытаетесь использовать один из классов. В таком случае возникнет ошибка на этапе компиляции и нужно будет вместе с именем класса явно указать пакет. Важно подчеркнуть, что оператор import необязателен. В любом месте, где указывается имя класса, можно применять его полностью уточненное имя, которое включает в себя всю иерархию пакетов. Например, в следующем фрагменте кода используется оператор import: import j ava . ut i l . * ; class MyDate extends Date ( ) А вот как можно переписать фрагмент без оператора import: class MyDate extends j ava . uti l . Date { ) В этой версии класс Da te указан с применением полностью уточненного имени. 254 Часть 1. Язык Java Как было показано в табл. 9. 1, при импортировании пакета классам внутри импортирующего кода, которые не являются подкласса ми, будут доступны только элементы из пакета, объявленные как puЫ ic. Например, если вы хо­ тите, чтобы класс Ba lance из приведенного ранее пакета mypack был досту­ пен как автономный класс для общего использования за пределами mypack, то вам понадобится объявить его как puЫ ic и поместить в отдельный файл: package mypack; ! * Теперь класс Balance , его конструктор и метод s how ( ) стали от крытыми . */ Это означает, что они могут использоваться в коде классо в , не являющихся подклассами , вне их пакета . puЫ i c class Balance Str ing name ; douЫe bal ; puЫic Balance ( St ring n , douЫ e Ь ) { name = n ; bal = Ь; puЫ i c void show ( ) { i f (bal < O ) System . out . print ( " --> " ) ; System . out . println ( name + " · $ " + bal ) ; Теперь класс Balance стал открытым, равно как его конструктор и ме­ тод show ( ) . Это означа ет, что к ним можно получить доступ в коде любого вида за рамками пакета mypack. Например, класс TestBa la nce импортирует mypack и затем может работать с классом Ba lance: import mypac k . * ; class TestBalance { puЫ i c static vo id ma in ( S tring [ ] args ) { / * Поскол ь ку Bal ance от крыт , вы можете использовать класс Balance и вызывать его конструктор . * / Balance test = new Bala nce ( " J . J . Jaspers " , 9 9 . 8 8 ) ; tes t . s how ( ) ; / / вы может е также вызывать метод show ( ) В качестве эксперим ента удалите модификатор доступа puЫic из класса Balance и попробуйте скомпилировать TestBa lance. Как объяснялось ран ее, возникнут ошибки на этапе компиляции. И нтер ф ей сы С помощью ключ евого слова interface в ы можете полностью абстрагиро­ вать интерфейс класса от его реализации. То есть с применением interface можно указать, что класс должен делать, но не как конкретно. Интерфейсы Глава 9 . П акет ы и инте р ф ейс ы 255 синтаксически похожи на классы, но в них отсутствуют переменные экзем­ пляра и , как правило, их методы объявляются без тела. На практике это оз­ начает, что вы можете опр еделять интерф ейсы, не делая предположений о том , каким образом они реализованы. После определения интерфейс может быть реализован любым колич еством к лассов. Кроме того, один класс может реализовывать любое количество интерфейсов. Для реализации интерфейса класс должен предоставить полный набор методов, требуемых интерфейсом. Однако к аждый класс может са мостоя­ тельно определять детали собственной реализации. За счет предоставления ключ евого слова interface язык Java позволяет в полной мере задействовать аспект полиморфизма "один интерфейс, несколько методов': Интерфейсы предназначены для поддержки динамического распознавания методов во вр емя выполнения. Обыч но для вызова метода из одного клас­ са внутри другого оба к ласса должны п рисутствовать во время компиляции, чтобы компилятор Java мог проверить совместимость сигнатур методов. Такое требовани е само по себе создает статическую и нерасширяемую среду создания классов. В системе подобного рода фу нкциональность неизбежно поднима ется все выше и выше в ие рархии классов, так что механизмы ста­ новятся доступными для все большего числа подклассов. Интерф ейсы пред­ назначены для того, чтобы решить эту проблему. Они отделяют определение метода или набора методов от иерархии наследования. Поскольку интерфей­ сы находятся в иерархии, отличающейся от иерархии классов, у классов, не связанных с точки зр ения иерархии классов, появляется возможность реа­ лизации одного и того же интерфейса. Именно здесь проявляется настоящая сила интерфейсов. О пр еде л ение инте р фей са Интерфейс опр еделяется во многом подобно классу. Ниже показана упро­ щенная общая форма интерфейса: доступ interface имя { возвраща емый- тип имя-метода l ( списо к-параметров ) ; возвраща емый-тип имя-метода 2 ( список-параметров ) ; тип финальное-имя-переменной] = зна чение; тип финальное-имя-переменной2 = зна чение; // ... возвраща емый- тип имя-методаN ( список-параметров ) ; тип финальное -имя-переменнойN = зна чение ; Если модификатор доступа отсутствует, тогда устанавливается стандарт­ ный доступ и интерф ейс будет доступным только другим элементам пакета, в котором он объявлен. Когда интерфейс объявлен как puЫ ic, его может ис­ пользовать код вне пак ета, где он объявлен. В таком случае интерфейс дол­ жен быть единственным открытым интерф ейсом, объявленным в файле, а файл должен иметь то же имя, что и интерф ейс. Имя интерф ейса может быть 256 Ч а сть 1. Язык Java любым допустимым идентификатором. Обратите внимание, что объявленные методы не имеют тела. Они заканчиваются точкой с запятой после списка па­ раметров. По существу это абстрактные методы. Каждый класс, включающий такой интерфейс, обязан реализовывать все методы. Прежде чем продолжить, необходи мо сделать важное замечание. В версии JDK 8 к интерфейсу было добавлено функциональное средство, значительно изменившее его возможности. До JDK 8 в интерфейсе нельзя было опреде­ лять какие-то реал изации. Речь идет о виде интерфейса, упрощенная форма которого представлена выше, rде ни одно объявл ение метода не снабжалось телом. Таким образом, до выхода JDK 8 интерфейс мог определять только "что'; но не "как': В версии JDK 8 ситуация изменилась. Начиная с JDK 8, к методу интерфейса можно добавлять стандартную реализацию. Кроме того, в JDK 8 также добавлены статические методы интерфейса, а начиная с JDK 9, интерфейс может вкл ючать закрытые методы. В результате теперь интерфейс может задавать какое-то поведение. Тем не менее, такие методы представля­ ют собой то, что по существу является средствами специального назначения, и первоначальный замысел интерфейса по-прежнему остается. Поэтому, как правило, вы все еще будете часто создавать и использовать интерфейсы, в которых новые средства не применяются. По указанной причине мы начнем с обсуждения интерфейса в его тради ционной форме. Более новые средства интерфейса описаны в конце главы. Как показывает общая форма, внутри объявлений интерфейса можно объ­ являть переменные. Они неявно являются fina l и static, т.е. не могут изме­ няться реализующим классом. Они та кже должны быть инициал изированы. Все методы и переменные неявно открыты. Ниже приведен пример определения интерфейса, в котором объявляет­ ся простой интерфейс с единственным методом ca l lback ( ) , принимающим один целочисленный параметр: interface Cal lback { void cal lback ( i nt param ) ; Реал и за ц ия ин тер фей сов После определения интерфейса один или несколько классов могут его ре­ ализовать. Для реализации интерфейса вкл ючите в определение класса кон­ струкцию implement s и затем создайте методы, требуемые интерфейсом. Общая форма класса, содержащего конструкцию implement s, выглядит сле­ дующим образом: class имя-кла сса [extends суперкла сс] [ implements интерфейс [, интерфейс . . . ] ] ( / / тел о класса Если класс реализует более одного интерфейса, тогда интерфейсы отделя­ ются друг от друга запятыми. Если класс реализует два интерфейса, в которых объявлен один и тот же метод, то тот же самый метод будет испол ьзоваться Гл а в а 9 . П а кеты и и нтер фе йс ы 257 клиентами любого из двух интерфейсов. Методы, реализующие интерфейс, должны быть объявлены как puЫ ic. Кроме того, сигнатура типов реализу­ ющего метода должна в точности совпадать с сигнатурой типов, указанной в определении интерфейса. Далее показан небольшой пример класса, который реализует определен­ ный ранее интерфейс Cal lback: class Client implements Callback { / / Реализовать метод интерфейса Cal lba ck . puЬlic void callback ( int р } ( System . out . p rintln ( " cal lback ( } вызывается с " + р } ; Обратите внимание, что метод cal lback ( ) объявлен с применением мо­ дификатора доступа puЫic. Помн ите! При реализации метода интерфейс а он должен бы ть объявлен как рuЫ i с. Для классов, реализующих интерф ейсы, допустимо и распространено определение собственных дополнительных членов. Например, в следую­ щей версии класса C l i ent реализуется c a l l back ( ) и добавляется метод noni faceMeth ( ) : class Client implements Callback { / / Реализовать метод интерфейса Cal lbac k . puЫic void cal lback ( int р ) { System . ou t . println ( " callback ( } вызывается со значением " + р ) ; void non i faceMeth ( ) { Sys tem . out . println ( " Классы, которые реализуют инт ерфейсы, " + "могут также определять и другие члены . " ) ; Д о ступ к реализаци я м ч ерез сс ыл к и на ин те рф е йс ы Вы можете объявлять переменные как ссылки на объекты, в которых при­ меняется тип интерфейса, а не тип класса. С помощью переменной подобно­ го рода можно ссылаться на любой экземпляр любого класса, реализующего объявленный интерфейс. При вызове метода через одну из таких ссылок кор­ ректная версия будет вызываться на основе фактического экземпляра реа­ лизации интерфейса, на который осуществляется ссылка. Это одна из клю­ чевых особенностей интерфейсов. Поиск метода, подлежащего выполнению, производится динамически во время выполнения, что позволяет создавать классы позже, чем код, вызывающий их методы. Диспетчеризация методов в вызывающем коде возможна через интерфейс, не требуя каких-либо знаний о "вызываемой стороне': Такой процесс аналогичен использованию ссылки на суперкласс для доступа к объекту подкласса, как было описано в главе 8. 258 Ч а сть 1. Язык Java В следующем примере метод callback ( ) вызывается через переменную ссылки на и нтерфейс: class Testi face { puЫic static ·,.тoid ma in ( S tring [ ] args ) { Cal lback с = new Cl ient ( ) ; c . callback ( � 2 ) ; Вот вывод, генерируемый программой: callback ( ) вызывается со значением 42 Обратите внимание, что переменная с объявл ена с типом интерфейса Callback, хотя ей был присвоен экземпляр класса Cl ient. Хотя переменную с можно использовать для доступа к методу callback ( ) , через нее не удастся получить доступ ни к каким другим членам класса Cl ient. Переменной ссыл­ ки на интерфейс известны только методы, присутствующие в ее объявлении i nter face. Та ким образом, переменную с нельзя применять для доступа к методу noni faceMeth ( ) , поскольку он определен в Client , а не в Callback. Хотя в предыдущем примере было показано, каким образом пер еменная ссылки на интерфейс может получить доступ к объекту реализации, в нем не демонстрировалась полиморфные возможности такой ссылки. Чтобы испы­ тать их, сначала понадобится создать еще одну реализацию Cal lback: / / Еще одна реализация Callbac k . class AnotherClient implements Callback { / / Реализовать метод интерфейса Cal lbac k . puЫi c void callback ( int р ) { System . out . println ( "Eшe одна версия cal lback ( ) " ) ; System . out . println ( "p в квадрате равно " + ( р * р ) ) ; Теперь создадим следу ющий класс: class Te s t i face2 { puЬl i c static void ma in ( S tring [ ] args ) Cal lback с = new Client ( ) ; AnotherClient оЬ = new AnotherCl ient ( ) ; с . callback ( 4 2 ) ; /! с теперь ссылается на объект AnotherCl ient с = оЬ; c . cal lback ( � 2 ) ; Ниже показан вывод, генерируемый программой: cal lback ( ) вызывается со значением 42 Еше одна в ерсия callback ( ) р в квадрате равно 1 7 64 Как видите, вызываемая версия ca llback ( ) определяется типом объекта, на который переменная с ссылается во время выполнения. Вскоре вы увиди­ те другой, более реальный пример. Глава 9 . П акеты и и нтер ф е й сы 259 Ч а ст и ч ны е реализации Если класс вклю чает интерфейс, но не полностью реализует методы, тре­ буемые эти м интерфейсом, то такой класс должен быть объявлен абстракт­ ным, например: abstract class I ncomplete implements Ca l lback { i nt а , Ь ; void show ( ) { S ystem . out . print l n ( a + " " + Ь ) ; } // ... Здесь класс Incornp lete не реализует метод callback ( ) и должен быть объявлен абстрактным. Любой класс, который наследует Incornp lete, обязан реализовывать ca l l back ( ) или сам должен быть объявлен как abstract. Вложе н н ы е и нтерфейс ы Интерфейс может быть объявлен членом класса или друrого и нтерфейса. Такой интерфейс называется членом-интерфейсом ил и вложенным интерфей­ сом. Вложенный интерфейс может быть объявлен как puЫic, p r i vate или protected. Он отли чается от интерфейса верхнего уровня, который должен быть либо объявлен как puЫ ic, либо использовать стандартный уровень до­ ступа, как было описано ранее. Когда вложенный и нтерфейс применяется за пределами своей области видимости, то он должен быть уто чнен именем класса или интерфейса, членом которого является. Таким образом, вне класса или интерфейса, где объявлен вложенный интерфейс, его и мя должно быть полностью уточненным. Вот при мер, в котором демонстрируется вложенный интерфейс: / / Пример влож енного интерф е й са . / / Кла сс А содержит член-и н терф е й с . class А { / / Влож енный интерф е й с . puЫic inter face NestedI F boolean i sNotNegative ( int х ) ; / / Класс В реализует вложе нный интерф е й с . class В imp l ements A . Nested I F { puЫ i c boolean isNotNega tive ( int х ) return х < О ? false : true ; class NestedI FDemo { puЫ i c static void main (String [ ] args ) { / / Исполь зовать ссылку на вложенный и н терф е й с . А . NestedI F ni f = new В ( ) ; 260 Часть 1. Язык Java i f ( ni f . i sNotNegat ive ( l O ) ) System. out . println ( " 1 0 не является отрицательным" ) ; i f ( ni f . i sNotNegative ( - 12 ) ) S ys tem. out.println ( "Это выводиться не будет " ) ; Обратите внимание, что в классе А определен и объявлен открытым член­ интерфейс по имени Ne s tedI F. Затем вложенный инт ерфейс реализуется в классе В за счет указания следующей конструкции: implements A . NestedI F Кроме того, имя полностью уточнено именем объемлющего класса. Внутри метода main ( ) создается ссылка А . Nes t edI F по имени n i f, которой присва­ ивается ссылка на объект В. Поскольку В реализует А . NestedIF, такая опера­ ция допустима. Применение ин терфей сов Чтобы оценить возможности инт ерфейсов, мы обратимся к более реали­ стичному примеру. В предшествующих главах был разработан класс Stack, реализующий просто й стек фиксированного размера. Однако реализо вать стек можно многими спосо бами. Например, стек может иметь фиксирован­ ный раз мер или быть "расширяемым': Ст ек также может храни ться в виде массива, связного списка, двоично го дерева и т.д. Как бы ни был реализован ст ек, инт ерфейс со стеком оста ется неиз менным, т.е. методы push ( ) и рор ( ) определяют интерфейс к стеку независимо о т деталей реализации. Поскольку ин терфейс к стеку отделен от его реализации, легко о пределить интерфейс ст ека, предоставив каждой реализации возможность определять специфику. Давайте рассмотрим два примера. Для начала во т определение интерф ейса для целочисленного ст ека . Поместите код в файл с именем I n t S t ac k . j ava. Данный ин терфейс будет использоват ься обеи ми реализациями стека. / / Определить интерфейс для целочисленного стека . interface I ntStack { void push ( int i tem) ; / / сохранить элемент int рор ( ) ; // извлечь элемент В следующей программе создается класс по им ени FixedS tack, ко торый реализует версию целочисленного ст ека с фиксированно й длиной: // Реализация IntStack, исполь зующая хранилище фиксированной длины. class FixedStack implements IntStack { private int [ ] stc k ; private int tos ; / / Разместить в памяти и инициализировать стек. FixedStack ( int size) { stck = new int [ s i ze ] ; tos = - 1 ; Гла в а 9 . Па кеты и ин тер фей сы 261 / / Поместить элемент в сте к . puЫ i c void push ( int item ) { i f ( t os==stc k . l ength- 1 ) // использовать член length System . out . println ( "Cтeк полон . " ) ; else stc k [ + +tos ] = item; } / / Извлечь элемент из стека . puЫ i c int рор ( ) { i f ( tos < О ) { System . out . println ( "Cтeк опустошен . " ) ; return О ; else return stck [ tos- - ] ; class I FTest { puЫ i c s ta t i c voi d ma in ( S tring [ ] args ) { FixedS tack mystackl = new FixedS tack ( S ) ; FixedStack mystack2 = new Fi xedS tack ( B ) ; / / Поместить несколь ко чисел в стеки . for ( i nt i=O ; i<S ; i + + ) mystackl . push ( i ) ; fo r ( i nt i=O ; i<B ; i + + ) mys tack2 . push ( i ) ; / / Извлечь эти числа из стеков . System . out . println ( "Cтeк в mystackl : " ) ; for ( int i=O ; i < S ; i + + ) System . out . p rintln (mystackl . pop ( ) ) ; System . out . println ( " Cтe к в mystack2 : " ) ; for ( i nt i=O ; i <B ; i + + ) S ystem . out . p r i n t l n (mys tac k2 . pop ( ) ) ; Ниже приведена еще одна реализ ация In t S ta c k, которая создает дина­ мический стек с применением того же определения интерфейса. В этой ре­ ализ ации каждый стек создается с начальной длиной. В случае превышения начальной длины размер стека увеличивается. Каждый раз , когда требуется больше места, размер стека удваивается. / / Реализовать "расширяемый" сте к . class DynStack imp l ements I n tStack { p r ivate i n t [ ] s t c k ; p rivate i n t tos ; / / Разместить в памяти и инициализировать стек . DyпStac k ( i п t si z e ) { stck = пеw i пt [ s i z e ] ; tos = - 1 ; 262 Часть 1. Язык Java / / Поместить элемент в стек . puЫ i c void push ( int i tern ) { / / Если стек полон, тогда создать стек боль шего размера . i f ( tos==stck . length- 1 ) { int [ ] ternp = new int [ st c k . length * 2 ] ; / / удвоить размер for ( i nt i=O ; i < s t c k . l ength ; i++ ) ternp ( i ] = stck [ i ] ; stck = ternp ; stck [ ++tos ] = i tern ; } else stck [ ++tos ] i tern ; / / Извлечь элемент из стека . puЫ i c i nt рор ( ) { i f ( tos < О ) { Systern . out . printl n ( "Cтeк опустошен . " ) ; return О ; else return stck [ to s - - ] ; class I FTest2 { puЫ i c stat i c vo id rnain ( String [ ] args ) DynStack rnystackl = new DynStack ( S ) ; DynStack rnystack2 = new DynStack ( B ) ; / / Эти циклы заставляют увеличива т ь ся каждый сте к . for ( i nt i=O ; i < l 2 ; i+ + ) rnystackl . push ( i ) ; for ( i nt i=O ; i <2 0 ; i++ ) rnystack2 . push ( i ) ; Systern . out . p rintln ( "Cтeк в rnys tackl : " ) ; for ( int i=O ; i < l 2 ; i + + ) Systern . out . println (rnys tackl . pop ( ) ) ; Systern . out . println ( "Cтeк в rnys tack2 : " ) ; for ( int i =O ; i <2 0 ; i++ ) Systern . out . println (rnystack2 . pop ( ) ) ; В показ анном далее классе используются обе реализации, FixedStack и DynStack, через ссылку на интерфейс. Другими словами, вызовы push ( ) и рор ( ) распоз наются во время выполнения, а не на этапе компиляции. / * Создать переменную ссылки на интерфейс и организовать через нее доступ к стекам . */ class I FTestЗ { puЫ i c static void rna i n ( S tring [ ] a rgs ) { I ntStack rnystac k ; / / создать переменную ссылки на интерфейс DynStack ds = new DynStack ( S ) ; FixedS tack fs = new FixedS tack ( B ) ; rnystack = ds ; / / загрузить в стек с динамиче ским размером Гл а в а 9 . Па кет ы и ин тер ф е й с ы 263 // Поместить несколь ко чисел в стеки . for ( i nt i=0 ; i<12 ; i++) mys tac k . push ( i ) ; / / загрузить в стек с фиксированным размером mystack = fs ; for ( i nt i = 0 ; i<B ; i++ ) mys tac k . push ( i ) ; mystack = ds ; System . out . p rintln ( " Знaчeния в стеке с динамическим размером : " ) ; for ( int i=0 ; i<12 ; i++) Sys tem . out . println (mystac k . pop ( ) ) ; mystack = fs ; System . out . println ( "Значения в стеке с фиксированным размером : " ) ; for ( int i=0 ; i<B ; i++ ) System . out . println (mystac k . pop ( ) ) ; В этой программе mys t a c k является ссылкой на интерфейс I n t S t a c k. Таким образом, когда она ссылается на ds, то используются версии push ( ) и рор ( ) , определенные реализацией DynStack, а когда на fs, то применяются версии push ( ) и рор ( ) , определенные в FixedStack. Как объяснялось ранее, распознавание производится во время выполнения. Доступ к множеству реа­ лизаций интерфейса через переменную ссылки на интерфейс - самый мощ­ ный способ обеспечения полиморфизма во время выполнения в Java. Переменные в и нтер ф е й с а х Интерфейсы можно использовать для импортирования общих констант в несколько классов, просто объявив интерфейс, который содержит пере­ менные, инициализированные нужными значениями. Когда такой интерфейс включается в класс (т.е. когда интерфейс "реализуется") , имена всех этих пе­ ременных будут находиться в области видимости как константы. Если интер­ фейс не содержит методов, то любой класс, включающий такой интерфейс, на самом деле ничего не реализует. Результат оказывается таким же, как если бы класс импортировал константные поля в пространство имен класса как переменные final. В следующем примере такой прием применяется для реа­ лизации автоматизированной "системы принятия решений": import j ava . util . Random; inter face SharedCons tants int NO = О ; int YES = 1 ; int МАУВЕ = 2 ; int LATER = 3 ; int SOON = 4 ; int NEVER = 5 ; class Question implements Sha redConstants Random rand = new Random ( ) ; int ask ( ) { int p rob = ( int ) ( 1 00 * rand . next DouЬle ( ) ) ; 264 Часть 1. Язык Java i f ( prob < 3 0 ) return NO; else i f (p rob < 6 0 ) return YES ; else i f (p rob < 7 5 ) return LATER; e l se i f ( prob < 9 8 ) return SOON ; else return NEVER; / / 30% / / 30% // 15% // 13% // 2 % class As kМe implement s SharedCons tants stat i c void answe r ( i nt resul t ) { switch ( resul t ) { case NO : System . ou t . println ( "Heт" ) ; b re a k ; c a s e YES : System . ou t . println ( "Дa " ) ; b rea k ; case МАУВЕ : System . out . pr i n t ln ( " Boзмoжнo" ) ; brea k ; c a s e LATER : System . out . p ri ntln ( "Позже " ) ; b rea k ; c a s e SOON : System . out . pr i ntln ( "Cкopo" ) ; b rea k ; case NEVER : System . out . println ( "Hикoгдa " ) ; b rea k ; puЫ i c s t at i c voi d main ( String [ ] args ) { Que s t ion q = new Question ( ) ; answer ( q . as k ( ) answe r ( q . as k ( ) answe r ( q . as k ( ) answer ( q . as k ( ) ) ) ) ) ; ; ; ; Обратите внимание, что в программе используется один из стандартных классов Java - Random, который выдает псевдослучайные числа. Он содер­ жит несколько методов, позволяющих получать случайные числа в том виде, который требуется вашей программе. В этом примере применяется метод next DouЫe ( ) , возвращающий случайные числа в диапазоне от О . О до 1 . О. В приведенном примере программы два класса, Quest ion и As kМe, реали­ зуют интерфейс SharedConstants, в котором определены константные поля Глава 9 . П акет ы и инт ер ф ейсы 265 NO, YES, МАУВЕ, SOON, LATER и NEVER. Внутри каждого класса код обращается к этим константам, как если бы каждый класс определял или наследовал их на­ прямую. Ниже показан вывод, полученный в результате запуска программы. Имейте в виду, что при каждом запуске результаты будут отличаться. Позже Скоро Нет Да На заметку! Прием применения интерфейса для определения общих констант, как было описано выше, вызывает споры. Он рассмотрен здесь ради полноты. Интерфейсы мож но рас ш ир я ть С помощью ключевого слова extends один интерфейс может быть унасле­ дован от другого. Синтаксис используется такой же, как и при наследовании классов. Когда класс реализует интерфейс, унаследованный от другого интер­ фейса , он должен предоставить реализации для всех методов, требуемых ин­ терфейсами в цепочке наследования. Ниже приведен пример: // Один интерфейс може т расширять другой . interface А { void rnet hl ( ) ; void meth2 ( ) ; / / Интерфейс В теперь включает rneth l ( ) и met h2 ( ) - он добавляет rnethЗ ( ) . interface В extends А { void methЗ ( ) ; / / Этот клас с должен реализовать все методы интерфейсов А и В . class MyCl ass implements В { puЫ i c void methl ( ) { System . out . println ( " Peaлизaция rnethl ( ) . " ) ; puЫ i c void meth2 ( ) { System . out . println ( " Peaлизaция met h2 ( ) . " ) ; puЫ i c void methЗ ( ) { Systern . out . print l n ( " Peaлизaция rneth З ( ) . " ) ; c l a s s I FExtend { puЫ i c s tatic void mai n ( St ring [ ] a rgs ) { MyClass оЬ = new MyCla s s ( ) ; оЬ . methl ( ) ; ob . meth2 ( ) ; ob . rnethЗ ( ) ; 266 Часть 1 . Язык Java В качес тве эксперимента попробу йте удалить реализ аци ю meth l () из MyClass. Результатом будет ошибка на этапе компиляции. Как уже упоми на­ лось, любой класс, реализ ующий интерфейс, должен реализовывать все мето­ ды, требуемые эти м интерфейсом, в том числе все методы, унаследованные от других и нтерфейсов. Ста ндартн ые методы интер ф ейса Как объяснялось ранее, до выхода JDK 8 и нтерфейс не мог определять ка­ кую-либо реализ ацию. Таким образом, во всех предшествующих версиях Java м етоды, определяемые и нтерфейсом, были абстрактными и не содержали тела. Это традиционная форма и нтерфейса, и именно такой вид и нтерфейса применялся в предыдущих обсуждениях. В выпуске JDK 8 ситуация измени­ лась з а с чет добавления к интерфейсам новой возможности, которая называ­ ется стандартным методом. Стандартный метод поз воляет определить реа­ лиз ацию по умолчанию для метода интерфейса. Другими словами, использ уя стандартный метод, метод интерфейса может предоставлять тело, а не быть абстрактным. Во время разработки стандартный метод также упоминался как расширяющий метод и вполне вероятно, что вы столкнетесь с обоими терми­ нами. Основным мотивом ввода с тандартного метода было предоставление средств, с помощью которых удалось бы расширять интерфейсы без нару­ шения работы существующего кода. Вспомните, что должны быть предусмо­ трены реализ ации для всех методов, определенных и нтерфейсом. В прошлом добавление нового метода к популярному, широко применяемому интерфей­ су нарушало работу существующего кода, поскольк у для нового метода от­ сутствовала реализ ация. Проблему решает стандартный метод, предоставляя реализ ацию, которая будет использоваться, если явно не указ ана другая реа­ лиз ация. Таким образом, добавление стандартного метода не приводит к на­ рушению работы существующего кода. Еще одним мотивом ввода стандартного метода было желание указывать в интерфейсе методы, которые по существу являются необязательными в з а­ висимости от того, как применяется интерфейс. Скажем, интерфейс может определять группу методов, которые воздействуют на последовательность эле­ ментов. Один из этих методов может называться remo ve ( ) , и его цель - уда­ ление элемента из последовательности. Однако если интерфейс предназ начен для поддержки как изменяемых, так и неизменяемых последовательностей, то метод remo ve ( ) не счи тается обязательным, т.к. он не будет использоваться неизменяемыми последовательностями. В прошлом класс, который реализовы­ вал неизменяемую последовательность, должен был определять пустую реали­ з ацию метода remo ve ( ) , даже если в нем не было необходимости. Теперь в ин­ терфейсе может быть указана стандартная реализ ация для remo ve ( ) , которая ни чего не делает (или генерирует исклю чение). Предоставление стандартной реализаци и делает необязательным определение в классе, предназ наченном для неизменяемых последовательностей, собственной версии-заглушки метода Глава 9 . П а кеты и ин т ер ф е й сы 267 remove ( ) . Таким образом, за счет обеспечения стандартного метода реализа­ ция remove ( ) в классе становится необязательной. Важно отметить, что добавление стандартных методов не меняет ключе­ вой аспект интерфейса: его неспособность поддерживать информацию о со­ стоянии. Например, интерфейс по-прежнему не может и меть переменные экз емпляра. Следовательно, определяющее различие между интерфейсом и классом з аключается в том, что класс может поддерживать информацию о состоянии, а интерфейс - нет. Кроме того, по-прежнему нельзя создавать экземпляр интерфейса самого по себе. Интерфейс должен быт ь реализ ован классом. По этой причине, несмотря на появившуюся в версии JDK 8 возмож­ ность определения стандартных методов, интерфейс все равно должен быть реализован классом, если необходимо создавать экземпляр. И последнее замечание: как прав ило, стандартные методы представляют собой функциональное средство специального назначения. Интерфейсы, ко­ торые вы создаете, по-прежнему будут при меняться в основном для указ ания того, что делать, а не как. Тем не менее, появление стандартных методов обе­ спечивает дополнительную гибкость. Основы ста ндартн ых методов Стандартный метод интерфейса определяется аналогично определению метода в классе. Основное отличие связ ано с тем, что объявлен ие пред ва­ ряется ключевым словом default. Например, воз ьмем следующий простой интерфейс: puЫic interface MyI F { / / Это объявление " нормального" метода интерфейса . / / В нем НЕ определяется стандартная реализация . int getNumЬer ( ) ; / / Это стандартный метод . Обратите внимание , / / что он предоставляет реализацию по умолчанию . de fau l t St ring getString ( ) { return "Стандартная строка " ; В и н терф ейсе M y I F объявлены два метода. Объявлен и е первого, getNumber ( ) , является объявлением обычного метода интерфейса. Какая­ либо реализация в нем отсутствует. Второй метод, getString ( ) , включает стандартную реализ ацию. В данном случае он просто воз вращает строку " Стандартная строка " . Обратите особое внимание на способ объявлен ия getString ( ) . Его объявлению предшествует модификатор default. Такой синтаксис можно обобщить. Чтобы определить стандартны й метод, перед его объявлением необходимо указать default. Поскольку getString ( ) содержит стандартную реализ ацию, реализующе­ му классу переопределять ее необяз ательно. Другими словами, если реали­ зующий класс не предоставляет собственную реализ ацию, то использ уется стандартная. Скажем, показ анный н и же класс MyI Fimp совершенно допустим: 268 Ч ас ть 1 . Я з ы к Java / / Реализовать MyI F . class MyI Fimp implements MyI F { / / Необходимо реализовать только метод getNumЬer ( ) , определенный в MyI F . / / Для метода getS t r i ng ( ) разрешено применять стандартную реализацию . puЫ i c int getNumbe r ( ) { return 1 0 0 ; В следующем коде создается экз емпляр класса MyI Fimp, который применя­ ется для вызова методов getNumЬer ( ) и getString ( ) : / / Исполь зовать стандартный ме тод . class De faul tMethodDemo { puЫ i c static void main ( S tring [ ] args ) MyI Fimp obj = new MyI Fimp ( ) ; / / Ме тод getNumЬer ( ) можно вызывать , потому что // он явно реализован в MyI Fimp : System . out . printl n ( obj . getNumЬer ( ) ) ; / / Метод getStr ing ( ) тоже можно вызывать // из-за наличия стандартной реализации : System . out . println ( obj . getString ( ) ) ; Вот вывод, генерируемый программой: 100 Стандартная строка Как видите, автоматически была использована стандартная реализ ация метода g e t S t ring ( ) . Определять ее в классе MyI F imp не понадобилось. Таким образом, для метода getString ( ) реализация классом необязатель­ на. (Конечно, реализ овать его в классе потребуется, если класс применяет getSt ring ( ) для каких-либо целей помимо тех, что поддерживаются стан­ дартной реализ ацией.) Для реализующего класса воз можно и распространено определение соб­ ственной реализ ации стандартного метода. Например, в класс MyI Fimp2 ме­ тод getString ( ) переопределяется: class MyI Fimp2 implements MyI F { / / Здесь предоставляются реализации для обоих методо в , getNumЬe r ( ) / / и getString ( ) . puЫ i c int getNumbe r ( ) return 1 0 0 ; puЫ i c S t r i ng getSt ring ( ) { return "Другая строка . " ; Теперь при вызове getString ( ) возвращается другая строка. Гл а в а 9. П а кеты и и нтерфей с ы 269 Более реалис т ичный пример Хотя в рассмотренных выше примерах демонстрировалась механика ис­ пользования стандартных методов, не была проиллюстрирована их полез­ ность в более реалистичных условиях. Давайте еще раз вернемся к интерфей­ су I nt S t a ck, показанному ранее в главе. В целях обсуждения предположим, что I ntStack широко применяется, и на него полагаются многие программы. Кроме того, пусть теперь мы хотим добавить в IntStack метод, который очи­ щает стек, позволяя использовать его многократно. Таким образом, мы желаем развивать интерфейс IntS tack, чтобы он определял новые функции, но не хо­ тим нарушать работу уже существующего кода. В прошлом решить задачу было невозможно, но с включением стандартных методов теперь это сделать легко. Например, интерфейс IntStack можно расширить следующим образом: i nterface IntStack { void push ( i nt item ) ; int pop ( ) ; / / сохранить элемент / / извлечь элемент // Поскольку метод clear ( ) имеет стандартную реализацию, его не нужно / / реализовывать в существующем классе , исполь зующем IntStac k . default void clear ( ) { System . out . println ( "Meтoд clear ( ) не реализован . " ) ; Стандартное поведение clear ( ) предусматривает просто отображение сообщения о том, что метод не реализован. Поступать так допустимо, по­ скольку ни один существующий класс, который реализует IntS tack, не мог вызывать метод clear ( ) , потому что он не был определен в более ранней версии IntStack. Однако метод clear ( ) можно реализовать в новом классе, реализующем IntStack. Кроме того, метод clea r ( ) необходимо определять с новой реализацией только в том случае, если он задействован. В итоге стан­ дартный метод предоставляет: • способ элегантного развития интерфейсов с течением времени; • способ обеспечения дополнительной функциональности без требова­ ния, чтобы класс предоставлял реализацию заглушки, когда эта функци­ ональность не нужна. И еще один момент: в реальном коде метод clear ( ) инициировал бы ис­ ключение, а не отображал сообщение об ошибке. Исключения описаны в следующей главе. После изучения этого материала вы можете попробовать изменить метод clear ( ) , чтобы его стандартная реализация выдавала ис­ ключение Un supportedOpe rationException. П роблемы множествен ного наследова н ия Как объяснялось ранее в книге, язык Java не поддерживает множествен­ ное наследование классов. Теперь, когда интерфейс способен содержать 270 Часть 1 . Я з ы к Java стандартные методы, вам может быть интересно, удастся ли с помощью ин­ терфейсов обойти это ограничение. Ответ: по существу нет. Вспомни те, что между классом и интерфейсом все же есть ключевое раз л ичие: кл асс может поддерживать информацию о состоянии (особенно за счет испол ьзования переменных экземпляра), а интерфейс - нет. Тем не менее, стандартные методы предлагают то, что обычно ассоцииру­ ется с концепцией множественного наследования. Скажем, у вас может быть класс, реал изующий два интерфейса. Есл и кажды й из таких интерфейсов предоставляет стандартные методы, то некоторое поведение наследуется от обоих. Таким образом, стандартные методы в ограниченной степени поддер­ живают множественное наследование поведения. Как несложно догадаться, в подобной си туации возможен конфликт имен. Напри мер, пусть класс MyC las s реал изует два интерф ейса с и мена­ ми A lpha и Beta. Что произойдет, есл и и A lpha, и Beta предоставят метод reset ( ) , для которого оба интерфейса объявят стандартную реал из аци ю? Какая версия reset ( ) б удет з адействована в классе MyClass - из интерфей­ са Alpha л и бо из интерфейса Beta? Или рассмотрим ситуацию, в которой ин­ терфейс Beta расширяет Alpha. Какая версия стандартного метода исполь­ з уется? А что, если MyClass предоставляет собственную реал изацию метода? Для обработки этих и других похожих ситуаций в Java определен набор пра­ вил, разрешаю щих такие конфликты. Во-первых, во всех случаях реализация кл асса имеет приоритет над стан­ дартной реал из ацией интерфейса. Таким образом, если в MyCla ss переопре­ деляется стандартный метод reset () , то при меняется версия из MyC la s s . Это так, даже есл и MyCla ss реализ ует и Alpha, и Beta. Тогда о б а стандартных метода переопределяются реал из ацией в MyC lass. Во-вторых, в случаях, когда класс реал из ует два интерфейса с одним и тем же стандартным методом, но кл асс не переопределяет этот метод, воз никает ошибка. Продолжая при мер, если MyCla s s реализует и Alpha, и Beta, но не переопределяет reset ( ) , тогда произойде т ошибка. В слу чаях, когда один интерфейс унаследован от другого, и оба определя­ ют общий стандартный метод, приоритет имеет версия метода из наследую­ щего интерфейса. Поэтому, продол жая пример, если Beta расширяет Alpha, то будет использоваться версия reset ( ) из Beta. С применением формы super следующего вида в унаследованном интер­ фейсе можно явно ссылаться на стандартную реал изацию: ИмяИнтерфейса . suреr . имяМетода ( ) Например, есл и в Beta нужно сослаться на стандартны й метод reset ( ) из A lpha, то вот какой оператор можно применить: Alpha . supe r . reset ( ) ; Гл ава 9. П а кеты и ин те р фе йс ы 271 Исп ол ьз ова ние с т ат и ч ес к и х ме то д ов в и н те рф е й се В версии JDK 8 в интерфейсах появилась возможность определения од­ ного или нескольких статических методов. Подобно статич еским методам в классе статический метод, определенный в инт ерф ейсе, может вызыват ься независимо от любого объекта. Таким образом, для вызова статич еского ме­ тода не требуется реализация интерфейса и экз емпляр реализации интерфей­ са. Взамен статически й метод вызывается пут ем указания имени интерфейса , за которым следует точка и имя метода. Вот общая форма: ИмяИнтер ф ейса . имяСтатнческогоМе тода ( ) Обратите внимание сходство со способом вызова статического метода в классе. Ниже показан пример статического метода, добавленного в интерфейс MyI F из предыдущего раздела. Статич еский метод getDe faul tNшnЬer ( ) воз­ вращает значение О. puЫic inter face MyI F { / / Это объявление " нормаль ного " метода интерфейса . / / В нем НЕ определяется стандартная реализация. int getNumЬer ( ) ; / / Это стандартный метод . Обратите внимание , / / что он предоставляет стандартную реализацию . default St ring getStr ing ( ) { return " Стандартная строка " ; / / Это статический метод интерфейса . static int getDefaultNumЬer ( ) { return О ; Метод getDe faul tNшnЬer ( ) может быть вызван следующим образом: int defNum = MyI F . getDefaultNumЬer ( ) ; Как уже упоминалось, для вызова метода getDe faul tNшnЬer ( ) не требу­ ется никакой реализации или экземпляра реализации MyI F, потому что он является статическим. И последнее замечание: статические ме тоды интерфейса не наследуются ни реализующим классом, ни производными интерфейсами. З а крыты е ме тоды ин те рф е й са Начиная с версии JD K 9, интерфейс способен сод ержать закрытый метод, который может вызываться только стандартным методом или другим за­ кры тым методом, определенным в том же интерфейсе. Поскольку закрытый метод интерф ейса указан как private, его нельзя использовать в коде вн е 272 Часть 1. Язык Java интерфейса, в котором он определен. Такое ограничение распространяется и на производные интерф ейсы, потому что закрытый м етод интерфейса ими не насл едуется. Основное преимущество закрытого метода интерф ейса заклю чается в том, что он позволяет двум и л и бол ее стандартным методам испол ьзовать общий фра гмент кода, позволяя избежать дублирования кода. Например, ниже по­ казана еще одна версия интерф ейса I ntStack, которая имеет два стандарт­ ных м етода popNElements ( ) и s k ipAndPopNElements ( ) . Первый метод возвращает массив, содержащий N верхних элем ентов стека. Второй метод пропускает ука занное кол ичество эл ем ентов, а затем возвращает массив, со­ держащий сл едующие N эл ементов. Оба метода применяют закрытый метод getElements ( ) для полу чения массива с указанным количеством элем ентов из стека . / / Еще одна версия IntStack, имеющая за крытый метод интерфейса , / / который исполь зуется двумя ста ндартными мет одами . iпter face IntStack { void push ( int i tem ) ; / / сохранить элеме нт int рор ( ) ; // извлечь элемент // Стандартный метод, возвращающий ма ссив , кот орый / / содержит верхние n элеме нтов в стеке . de fault int [ ] popNE lements ( i nt n ) { / / Возвратить запрошенные элементы . return getElements ( n ) ; / / Стандартный ме тод, воз вращающий массив , кот орый содержит / / следующие n элементов в стеке после пропуска s k ip элементов . default int [ ] s kipAпdPopNElements ( i nt s kip , int n) { / / Пропустить указанное количество элементов . getEleme nts ( s k ip) ; / / Возвратит ь запрошенные элементы . return getElements ( n ) ; / / За крытый ме тод, который возвращает массив , содержащий / / верхние n элементов в стеке . private int [ ] getE lements ( int n ) { int [ ] elements = new int [ n ] ; for ( i nt i=O ; i < n ; i + + ) element s [ i ] = рор ( ) ; return element s ; Обратите внимание, что для полу чения возвр аща емого массива в popNEleme nts ( ) и s k ipAndPopNE lements ( ) прим еняется закрытый метод getElements ( ) . Это предотвращает дубли рование одной и той же кодовой последовател ьности в обоих методах. Имейте в виду, что поскол ьку метод getElements ( ) является закрытым, ero нел ьзя вызывать из кода за предела­ ми IntStack. Та ким образом, ero использование огра ничено стандартными Глава 9. Пак е т ы и и н тер ф е й сы 273 методами внутри I ntStack. Кроме того, из-за того, что для получения эле­ ментов стека в getElement s ( ) применяется метод рор ( ) , он автоматиче­ ски вызывает реализацию рор ( ) , предоставляемую реализацией IntStack. Следовательно, getElement s ( ) будет работать для любого класса стека, реа­ лизующего IntStack. Хотя закрытый метод интерфейса представляет собой функциональное средство, которое будет востребовано редко, в тех случаях, когда оно вам нужно, вы найдете его весьма полезным. З аключительные соображения по поводу пакетов и интер ф ейсов Хотя в примерах, вкл юченных в книгу, пакеты или интерфейсы исполь­ зуются не особенно часто, оба эти инструмента являются важной частью программной среды Java. Практически все реальные программы, которые придется писать на Java, будут содержаться в пакетах. Некоторые из них, ве­ роятно, также будут реализовывать интерфейсы. Поэтому важно, чтобы вы научились их применять надлежащим образом. ГЛ А ВА ·:};.:�� ,;._ t J � J '•, �-1: ' \· :;j· i -.: •" -�:. < ' ' '_~ , ;:-,-•• , r � • •·? ;� • • О браб о т ка ис кл юч ени й ··� , \_;r: �: �: �_-.,_ , · В настоящей главе рассматривается механизм обработки исключений Java. Исключение - это ненормальное состояние, которое возникает в кодо­ вой последовательности во время выполнения. Другими словами, исключе­ ние является ошибкой времени выполнения. В языках программирования, не поддерживающих обработку исключений, ошибки необходимо проверять и обрабатывать вручную - обычно с помощью кодов ошибок и т.д. Такой под­ ход столь же громоздкий, сколь и хлопотный. Обработка исключений в Java позволяет избежать проблем подобного рода и попутно переносит управле­ ние ошибками во время выполнения в объектно-ориентированный мир. Основы обработки исключен ий Исключение Java представляет собой объект, описывающий исключи­ тельное (т.е. ошибочное) состояние, которое произошло внутри фрагмента кода. При возникновении исключительного состояния в методе, вызвавшем ошибку, генерируется объект, представляющий это исключение. Метод может обработать исключение самостоятельно или передать е го дальше. Так или иначе, в какой-то момент исключение перехватывается и обрабатывается. Исключения могут быть сгенерированы исполняющей средой Java или вруч­ ную в вашем коде. Исключения, генерируемые Java, относятся к фундамен­ тальным ошибкам, которые нарушают правила языка Java или ограничения исполняющей среды Java. Исключения, сгенерированные вручную, обычно используются для сообщения об ошибке вызывающей стороне метода. Обработка исключений в Java управляется пятью ключевыми словами: try, catch, throw, throws и final ly. Давайте кратко рассмотрим, как они ра­ ботают. Операторы программы, которые вы хотите отслеживать на наличие исключений, содержатся в блоке try. Если внутри блока t r y возникает ис­ ключение, тогда оно генерируется. Ваш код может перехватить это исключе­ ние (с помощью catch) и обработать его рациональным образом. Системные исключения автоматически генерируются исполняющей средой Java. Для ручной генерации исключения используйте ключевое слово t hrow. Любое исключение, генерируемое в методе, должно быть указано как таковое с по­ мощью конструкции throws. Любой код, который обязательно должен быть выполнен после завершения блока try, помещается в блок final ly. Гла ва 1 О. Обработка исключений 275 Ниже показ ана общая форма блока обработки исключений: try { / / блок кода , где отслеживаются ошибки catch ( ТипИсключенияl объектИсключения) / / обработчик исключений для ТипИсключенияl catch ( ТипИсключения2 объектИсключения) { / / обработчик исключений для ТипИсключения2 // ... f inal l y / / блок кода, подлежащий выполнению nосле о кончания блока try Здесь типИсключения - это тип возникшей исключительной ситуации. В оставшихся материалах гл авы демонстрируется применение при веденной выше структуры. На заметку! Существует еще одна форма оператора t ry, которая поддерживает автоматическое управление ресурсами. Она называется t ry c ресурсами и описана в главе 1 3 в контексте управления файлами, поскольку файлы являются одним из наиболее часто используемых ресурсов. Тип ы ис ключ ений Все типы и скл ючени й являются подк лассами встроенного класса ThrowaЫe. Таким образом, класс ThrowaЫe расположен на вершине иерар­ хии классов исключений. Непосредственно под ThrowaЫe находятся два подкласса, которые разделяют искл ючения на две отдель ные ветви. Одну ветвь возглавляет класс Except ion, используемый для представления исклю­ чител ьных условий, которые должны перехватываться польз овательскими программами. Он также будет служить подклассом для создания собствен­ ных специ аль ных типов исключений. Кроме того, у класса Exception и ме­ ется важный подкласс, который называется RuntimeExcept ion. Исключения такого типа автоматически определяются для разрабатываемых программ и охватывают такие ситуации, как деление на ноль и недопустимое индексиро­ вание массивов. Другую ветвь воз главляет к л асс Error, определяющий исключения, которые не должны перехватываться программой в обычных условиях. Исключения типа Error применяется исполняющей средой Java для указания ошибок, с вязанных с самой средой. Примером такой ошибки является пере­ полнение стека. Исключения типа Error здесь не рассматриваются, т.к. они обычно создаются в ответ на катастрофи ческие отказы, которые обычно не могут быть обработаны создаваемой программой. Иерархия исключени й верхнего уровня показана на рис. 10. 1 . 276 Часть 1. Язык Java Exception Error Rшitillld:кception Рис. 1 0.1. Иерархия исключений верхнего уровня Неперехваченн ые и с ключения Прежде чем вы научитесь обрабатывать исключения в своей программе, полезно посмотреть, что происходит, коrда их не обрабатывать. Следующая небольшая программа содержит выражение, которое намеренно вызывает ошибку деления на ноль: class Ехс О { puЫic static void main ( String [ ] args ) { int d = О ; int а = 4 2 / d; } Коrда исполняющая среда Java обнаруживает попьпку деления на ноль, она создает новый объект исключения и затем генерирует это исключение. В ре­ зультате выполнение класса ЕхсО останавливается, поскольку после генера­ ции исключение должно быть перехвачено обработчиком исключений и немед­ ленно обработано. В приведенном примере не было предусмотрено никаких собственных обработчиков исключений, поэтому исключение перехватывает­ ся стандартным обработчиком, nредоставляемым исполняющей средой Java. Любое исключение, которое не перехвачено вашей программой, в конечном итоrе будет обработано стандартным обработчиком. Стандартный обработ­ чик отображает строку с описанием исключения, выводит трассировку стека от точки, rде произошло исключение, и прекращает работу программы. Вот какое исключение генерируется при выполнении примера: j ava . lang . Ari thmeticException : / Ьу zero at ExcO . main (ExcO . j ava : 4 ) jsva . lang. Aii thmeticException : деление на ноль в ExcO .main (ExcO. java : 4) Обратите внимание, что в простой трассировки стека присутствует имя класса - ЕхсО, имя метода - main ( ) , имя файла - ЕхсО . j ava и номер стро­ ки - 4. Кроме тоrо, как видите, типом сrенерированноrо исключения явля­ ется подкласс Exception по имени Ari thmeticException, который более Глава 1 0. Обра ботка искл ючений 277 конкретно описывает тип возникшей ошибки. Далее в главе будет показано, что Java предлагает несколько встроенных типов исключений, соответству­ ющих различным типам ошибок времени выполнения, которые могут быть сгенерированы. Еще одно замечание: точный вывод, который вы видите при запуске этого и других примеров программ в главе, использующих встроен­ ные исключения Java, может немного отличаться от показанного здесь из-за различий между версиями JDК. В трассировке стека всегда показана последовательность вызовов методов, которые привели к ошибке. Например, вот еще одна версия предыдущей про­ граммы, которая вызывает ту же ошибку, но в методе, отдельном от main ( ) : class Excl { static vo id subroutine ( ) int d О ; i nt а = 1 0 / d ; puЫ i c s t a t i c void ma in ( String [ ] args ) { Exc l . subrou tine ( ) ; В полученной трассировке стека из стандартного обработчика исключений видно, что отображается весь стек вызовов: j ava . l ang . Ari thme ti cException : / Ьу zero at Exc l . s ubroutine ( Excl . j ava : 4 ) at Excl . main ( Exc l . j ava : 7 ) Как видите, в нижней части стека указана строка 7 метода main ( ) , в ко­ торой вызывается метод subrout i ne ( ) , ставший причиной исключения в строке 4. Стек вызовов весьма полезен для отладки, поскольку он указывает точную последовательность шагов, которые привели к ошибке. И спользование try и catch Хотя стандартный обработчик исключений, предоставляемый исполня­ ющей средой Java, удобен при отладке, обычно вы пожелаете обрабатывать исключение самостоятельно, что дает два преимущества. Самостоятельная обработка, во-первых, позволяет исправить ошибку и, во-вторых, предот­ вращает автоматическое прекращение работы программы. Большинство пользователей будут (по меньшей мере) сбиты с толку, если ваша програм­ ма перестанет работать, начав выводить трассировку стека всякий раз, когда возникает ошибка! К счастью, предотвратить это довольно легко. Чтобы защититься от ошибки времени выполнения и обработать ее, про­ сто поместите код, который хотите отслеживать, в блок t ry. Сразу после блока try добавьте конструкцию catch с указанием типа исключения, кото­ рое желательно перехватить. В целях иллюстрации того, насколько легко это делать, в следующей программе определен блок try и конструкция cat ch, обрабатывающая исключение Ari thmeti cException, которое генерируется ошибкой деления на ноль: 278 Ча с ть 1 . Язык Java class Ехс2 [ puЬlic s ta t i c voi d main ( S cring [ ] args ) ( int d , а ; try [ / / отслеживать блок кода d = О; а = 42 / d; System . out . p rintln ( "Этo выводиться не будет . " ) ; } catch (Ari thmeti cException е ) { / / перехватить ошибку деления на ноль System . o ut . println ( "Дeлeниe на ноль . " ) ; System . out . p rintln ( "После оператора catch . " ) ; Программа выдает такой вывод: Деление на ноль . После оператора catch . Обратите внимание, что вызов println ( ) внутри блока try никогда не выполняется. После генерации исключения управление передается из блока try в блок catch. Другими слова ми, блок catch не "вызывается" и потому управление никогда не "возвращается" в блок try из catch. Таким образом, строка "Это выводиться не буде т . " не отображается. После блока catch выполнение продолжается со строки программы, следующей за всем меха­ низмом try/ catch. Оператор try и его конструкция catch образуют единицу. Область дей­ ствия catch ограничена операторами, которые относятся к непосредственно предшествующему оператору try. Конструкция catch не может перехваты­ вать исключение, сгенерированное другим оператором try (за исключением описанного ниже случая вложенных операторов try). Операторы, защищен­ ные с помощью try, должны быть заключены в фигурные скобки (т.е. нахо­ диться внутри блока). Применять try для одиночного оператора нельзя. Целью большинства хорошо построенных конструкций catch должно быть разрешение исключительной ситуации и продолжение работы, как если бы ошибка вообще не возникала. Например, в приведенной далее программе на каждой итерации цикла for получаются два случайных целых числа, одно из которых делится на другое, а результат используется для деления значения 1 2 3 4 5. Окончательный результат помещается в переменну ю а. Если какая­ либо операция вызывает ошибку деления на ноль, то она перехватывается, значение а устанавливается равным нулю и выполнение программы продол­ жается. / / Обработать исключение и продолжить работу. import j ava . u til . Random; class Handl eError { puЬ l i c static void ma in ( St ring [ ] args ) { int а=О , Ь=О , с= О ; Random r = new Random ( ) ; Глава 1 0. Обработка и с ключе н и й 279 for ( int i=O ; i < 3 2 0 0 0 ; i++ ) { try { Ь = r . next int ( ) ; с = r . nextint ( ) ; а = 12345 / (Ь/с) ; catch (Arithme ti cExcep tion е ) { System . out . println ( "Дeлeниe на ноль . " ) ; а = О ; / / установить а в ноль и продолжить Sys tem . out . p rintln ( " a : " + а ) ; Отобра жение описани я исключения В классе ThrowaЫe переопределен метод t o S t ring ( ) (определенный в Obj ect), так что он возвращает строку, содержащую описание исключения. Для отображения этого описания в операторе println ( ) нужно просто пере­ дать исключение в качестве аргумента. Например, блок catch из предыдущей программы можно переписать так: catch (Ari thmeticException е ) { System . out . println ( "Иcключeниe : " + е ) ; а = О ; / / установить а в ноль и продолжить Тогда в случае ошибки деления на ноль будет отображаться следующее со­ общение: Исключение : j ava . lang . Ari thmeticExcep t ion : / Ьу z e ro Хотя в таком контексте это не имеет особой ценности, возможность ото­ бражать описание исключения полезна в других обстоятельствах, особенно когда вы экспериментируете с исключениями или занимаетесь отладкой. Использован ие нескол ьких конструкций catch В некоторых случаях один фрагмент кода может генерировать более од­ ного исключения. Чтобы справиться с ситуацией такого рода, можно указать две или более конструкции catch, каждая из которых будет перехватывать разные типы исключений. При возникновении исключения все конструк­ ции catch проверяются по порядку, и выполняется первая из них, в которой указанный тип совпадает с типом сгенерированного исключения. После вы­ полнения одной конструкции са tch остальные игнорируются, и выполнение продолжается после блока t ry/catch. В следующем примере перехватывают­ ся два разных типа исключений: / / Демонстрация применения нескольких конструкций catch . clas s MultipleCatche s { puЫic static void main ( S tring [ ] a rgs ) { try { 280 Ч а сть 1. Язык Java int а = args . length; System . out . printl n ( " a " + а) ; int Ь = 4 2 / а ; int [ ] с = { l } ; с [42] = 99; catch ( Ari thmeticExcept ion е ) { System . out . printl n ( "Дeлeниe на ноль : " + е ) ; catch (Arrayi ndexOutOfBoundsException е ) { System . out . println ( "Bыxoд за допустимые пределы индекса в массиве : " + е ) ; } System . out . println ( "Пocлe блоков try/catch , " ) ; Программа вызовет исключение деления на ноль, если будет запущена без аргументов командной строки, т.к. з начение а будет равно нулю. Деление пройдет успешно в случае предоставления аргумента командной строки, ко­ торый приведет к установке а во что-то большее, чем ноль. Но это станет причиной генерации исключения Arrayi ndex0utOfBoundsException, по­ скольку целочисленный массив с имеет длину 1, а программа пытается при­ своить значение несуществующему элементу с [ 4 2 ] . Ниже показан вывод программы, выдаваемый в обеих ситуациях: C : \>j ava Mu l t ipleCatches а = О Деление на ноль : j ava . lang . Ari thmeticException : / Ьу zero После блоков try/catch . C : \>j ava MultipleCatches TestArg а = 1 Выход за допустимые пределы индекса в массиве : j ava . l ang . ArrayindexOutOfBoundsException : I ndex 4 2 out of bounds for length l После блоков try/catch . При использован и и нескольких конструкций catch важно помнить о том, что подклассы исключений должны предшествовать любым из своих суперклассов. Дело в том, что конструкция cat ch, в которой применяется суперкласс, будет перехватывать исключения указанного т и па плюс любых его подклассов. В итоге конструкция catch с подклассом никогда не будет достигнута, если она находится после конструкции са tch с суперклассом. Кроме того, недостижимый код в Java является ошибкой. Например, рассмо­ трим следующую программу: /* Эта программа содержит ошибку . */ В последователь ности конструкций catch подкласс должен предшест вовать своему суперклассу. В противном случае будет создан недостижимый код , что приведет к ошибке на э тапе компиляции . class SuperSubCatch puЫ i c s tatic voi d mai n ( String [ ] args ) { try { int а О ; int Ь 42 / а ; Глава 1 0. Обработка искл ючени й 281 catch ( Exception е ) { System . out . println ( "Перехват обобщенного исключения Exception . " ) ; / * Эта конструкция catch недостижима , потому что Arithmet icException является подклассом Exception . * / catch (Ari thmeti cException е ) { / / ОШИБКА - недо стижимый код System . out . println ( " Этo никогда не будет достигнуто . " ) ; Попытка компиляции этой программы приводит к получению сообщения об ошибке, указывающего на то, что вторая конструкция catch недостижи­ ма, т.к. исключение уже было перехвачено. Поскольку ArithmeticException является подклассом Except ion, первая конструкция catch будет обрабаты­ вать все ошибки, связ анные с Except ion, в том числе Ari thmeticExcept ion. Таким образ ом, вторая конструкция catch никогда не выполнится. Чтобы решить проблему, понадобится изменить порядок следования конструкций catch. Вложенные операторы try Оператор try может быть вложенным, т.е. находиться внутри блока дру­ гого оператора try. Каждый раз, когда происходит вход в try, контекст этого исключения помещается в стек. Если внутренний оператор try не имеет об­ работчика catch для определенного исключения, тогда стек раскручивается, и на предмет совпадения проверяются обработч ики catch следующего опе­ ратора try. Процесс продолжается до тех пор, пока не будет найдена под­ ходящая конструкция catch либо исчерпаны все вложенные операторы t ry. Если ни одна из конструкций catch не дает совпадения, то исключение будет обработано исполняющей средой Java. Ниже приведен пример использования вложенных операторов try: / / Пример применения вложенных операторов t r y . c l a s s NestTry { puЫ i c static void mai n ( S t ring [ ] a rgs ) { try { int а = a rgs . l ength ; / * Если аргументы командной строки отсутствуют , то следующий оператор сгенерирует исключение деления на ноль . * / int Ь = 4 2 / а ; System . out . println ( "a = " + а ) ; try { / / вложенный блок try / * Если исполь зуется один аргумент командной строки , тогда исключение деления на ноль сгенерирует следующий код . * / i f ( a== l ) а = а / ( а -а ) ; / / деление на ноль /* Если используется один аргумент командной строки , тогда генерируется исключ ение выхода за допустимые пределы индекса в массиве . * / 282 Часть 1. Язык Java if ( а==2 ) { int [ ] с = { 1 } ; с [ 4 2 ] = 9 9 ; / / генерирует исключение ArrayindexOutOfBoundsException } catch ( Array i ndexOutOfBounds Exception е ) { Sys tem. out . pr intln ( "Bыxoд за допустимые пределы индекса в массиве : " + е ) ; catch {Arithmet icExcept ion е ) { System . out . p rint ln ( "Дeлeниe на ноль : " + е ) ; Как видите, в программе один блок try вложен в другой. Программа рабо­ тает следующим образом. При запуске программы без аргументов командной строки внешний блок try генерирует исключение деления на ноль. Запуск программы с одним аргументом командной строки приводит к генерации ис­ ключения деления на ноль внутри вложенного блока try. Поскольку внутрен­ ний блок try не перехватывает это исключение, оно передается внешнему блоку try, где и обрабатывается. При запуске программы с двумя аргумен­ тами командной строки во внутреннем блоке try генерируется исключение выхода за допустимые пределы индекса в массиве. Вот примеры запуска, ил­ люстрирующие каждый случай: C : \ > j ava NestTry Деление на ноль : j ava . lang . Arithmeti cException : / Ьу zero C : \ > j ava NestTry One а = 1 Деление на ноль : j ava . lang . Arithmeti cException : / Ьу zero C : \>j ava Nes tT ry One Two а = 2 Выход за допустимые пределы индекса в массиве : j ava . lang . Arrayi ndexOutOfBoundsExcept ion : Index 4 2 out of bounds for length 1 Когда задействованы вызовы методов, вложение операторов try может происходить менее очевидным образом. Например, если вызов метода за­ ключен в блок try и внутри этого метода находится еще один оператор try, то оператор try внутри метода будет вложен во внешний блок t ry, где метод вызывается. Ниже представлена предыдущая программа, в которой вложен­ ный блок try перемещен внутрь метода nesttry ( ) : / * Операторы try могут быть не-'!вно вложенными ч ерез вызовы методов . * / class MethNe stTry { static void nesttry ( i nt а ) { t r y { / / вложенный блок try / * Если и споль зуете-'! один ар гумент командной строки , тогда исключение делени-'! на ноль сгенерирует следующий код . * / i f ( a == l ) а = а / ( а-а ) ; / / деление на ноль / * Если используютс-'! два аргумента командной строки , тогда генерируете-'! исключение выхода за допустимые пределы индекса в массиве . * / i f ( а == 2 ) { int [ J с = { 1 } ; Глава 10. Обработка искл ючений с [ 4 2 ] = 99; 283 / / генерирует исключение ArrayindexOutOfBoundsException catch ( ArrayindexOutOfBoundsException е ) { System. out . p:cintln ( "Выход за допустимые пределы индекса в массиве : " + е ) ; } puЫic static void main ( St ring [ ] args ) { try { int а = args . length ; / * Если аргументы командной строки отсу т с т вуют , т о следующий оператор сгенерирует исключение деления на ноль . * / int Ь = 42 / а ; System . out . println ( "а = " + а ) ; nesttry ( a ) ; catch (ArithmeticException е ) { S ystem . out . println ( "Дeлeниe на ноль : " + е ) ; Вывод программы и дентичен выводу в предыдуще м примере. О ператор throw До сих пор п е ре хватывались только те исключ е ния, которые генерируются исполняющей средой Java. Однако программа мож е т г е н е рировать исключ е­ ние явно с прим е нением опе ратора throw со следующе й обще й формой: throw ThrowaЫeinstance ; Зд е сь Throwa Ы e i n s ta nce долж е н быть объ е ктом т ипа ThrowaЫe или подклассом ThrowaЫ e . Примитивные типы врод е int или char, а такж е классы, отличаю щи е ся от ThrowaЫe, такие как Str i ng и Obj ect, н е могут использоваться в кач е ств е исключ е ний. Есть два способа получить объ е кт ThrowaЫe: указывая параметр в конструкции catch или создавая е го с по­ мощью операции new. Поток выполнения останавливается сразу посл е опе ратора throw; любые последующие операторы не выполняются. Ближайший охватывающий блок try пров е ряется на предмет наличия в нем конструкци и catch, соотв е тству­ ющей типу исключения. Если совпадени е найд е но, то управле ни е п е редается этому оп е ратору, а е сли нет, тогда проверяется следующий охватывающий опе ратор try и т.д. Если соответствующая конструкция catch н е найдена, то стандартный обработчик исключений останавливает работу программы и выводит трассировку стека. Ниж е прив е д е н прим е р программы, в которой создается и ге н е рируе тся исключ ение . Обработчик, перехватывающий исключени е , повторно ге н е ри­ рует е го для вне шн е го обработчика. // Демонс трация приме нения th row . class ThrowDemo { 284 Часть 1. Яз ы к Java static void demoproc ( ) try { throw new Null PointerExcept ion ( "дeмoнcтpaция " ) ; catch (Null PointerException е ) { System . out . println ( "Пepexвaчeнo внутри demoproc ( ) . " ) ; / / повторно сгенерировать исключение throw е ; puЫ i c static void main ( String [ ] args ) { try { demoproc ( ) ; catch (Null PointerException е ) { System . out . println ( "Пoвтopнo перехвачено : " + е ) ; В программе есть два шанса справиться с одной и той же ошибкой. Сначала в ma i n ( ) устанавливается контекст исключения и вызывается demoproc ( ) . Затем метод demoproc ( ) устанавливает другой контекст обработки исклю­ чений и немедленно создает новый экземпляр Nul l PointerException, кото­ рый перехватывается в следующей строке. Далее исключение генерируется повторно. Вот результат: Перехвачено внутри demoproc ( ) . Повторно перехвачено : j ava . lang . NullPointerException : демонстрация В программе также демонстрируется создание одного из стандартных объ­ ектов исключений Java. Обратите особое внимание на следующую строку: throw new Null PointerExcepti on ( "демонстрация" ) ; Здесь с применением операции new создается экземпляр Nul l Po inter Exception. Многие встроенные исключения времени выполнения Java имеют как минимум два конструктора: один без параметров и один принимающий строковый параметр. Когда используется вторая форма, аргумент указывает строку, описывающую исключение. Эта строка отображается, коr да объект передается как аргумент в print ( ) или println ( ) . Его также можно полу­ чить, вызвав метод getMes sage ( ) , который определен в ThrowaЫe. К онструк ция throws Если метод способен приводить к исключению, которое он не обрабатыва­ ет, то метод должен сообщить о таком поведении, чтобы вызывающий его код мог защитить себя от этого исключения. Задача решается добавлением к объ­ явлению метода конструкции throws, где перечисляются типы исключений, которые может генерировать метод. Поступать так необходимо для всех ис­ ключений, кроме исключений типа Error, RuntimeException или любых их подклассов. Все остальные исключения, которые может генерировать метод, должны быть объявлены в конструкции throws. В противном случае возник­ нет ошибка на этапе компиляции. Глава 1 0. О б р аботка и скл ю че ний 285 Вот общая форма объявления метода, которая содержит конструкцию throws: тип имя-метода ( список-параметров ) throws список-исключений { / / тело метода Здесь список-исключений представляет собой список разделяемых запя­ тыми исключений, которые метод может сrенерироват ь. Н и же приведен пример некорректной проrраммы, пытающейся сrенери­ ровать исключение, которое она не перехватывает. Из-з а тоrо, что в проrрам­ ме не указ ана конструкция throws для объявления данноrо факта, проrрамма не скомпилируется. // Эта программа содержит ошибку и компилироваться не будет . class ThrowsDemo { static void throwOne ( ) { System . out . println ( n Bнyтpи throwOne ( ) . n ) ; throw new I l lega lAcce s sException ( "дeмoнcтpaция " ) ; puЫ i c static voi d ma i n ( S tring [ ] args ) { throwOne ( ) ; Чтобы пример ском пилировался, в него понадобится внести два измене­ ния. Во-первых, в ам нужно объявить, что метод throwOne ( ) генерирует ис­ ключение I l legalAccessExcept ion. Во-вторых, в методе ma i n ( ) должен быть определен оператор try / са tch, который перехватывает это исключе­ ние. Далее показан исправленный пример: // Теперь программа компилируется . class Throws Demo { static void throwOne ( ) throws I l l egalAccessException System . out . println ( "Bнyтpи throwOne ( ) . n ) ; throw new I l legalAcce ssExcept i on ( n дeмoнcтpaция " ) ; puЫ i c static voi d main ( S tring [ ] args ) { try { th rowOne ( ) ; catch ( I l legalAcces sException е ) S ystem . out . p r i nt l n ( " Пepexвaчeнo " + е ) ; Программа выдает следующий вывод: Внутри throwOne ( ) . Перехвачено j ava . l ang . I l lega lAcce s sExcept i on : демонстрация 286 Часть 1. Язык Java Констру кц и я finally Когда генерируются исключения, поток выпол нения в методе направляет­ ся по довольно резкому нелинейному пути, из меняющем нормал ьный ход вы­ полнения метода. В з ависимости от того, как з акодирован метод, исключение может даже привести к преждевременному возврату из метода, что в некото­ рых методах может стать проблемой. Например, есл и метод открывает файл при входе и з акрывает его при выходе, то пропуск кода, з акрывающего файл, механиз мом обработки исключений нельзя считать приемлемым. Для такой нештатной ситуации и предназначено клю чевое слово fina l ly. Ключевое слово fina l l y позволяет создать блок кода, который будет вы­ полняться после з авершения блока try/catch и перед кодом, следующим по­ сле try/catch. Блок fina l l y будет выполняться независимо от того, сгене­ рировано исключение или нет. В случае генерации исключения блок fina l l y будет выполняться, даже есл и исключение н е соответствует н и одной кон­ струкци и catch. Каждый раз , когда метод собирается вернуть управление вызывающему коду из блока try/catch через неперехваченное исключение или явно посредством оператора return, блок fina l l y тоже выполняется не­ посредственно перед возвратом из метода. Таким образом, с помощью бло­ ка fina l ly удобно з акрывать файловые дескрипторы и освобождать л юбые другие ресурсы, которые могли быть выделены в начале метода с намерением освобождения их перед возвратом. Конструкция fina l l y является необяз а­ тельной. Тем не менее, для каждого оператора try требуется хотя бы одна конструкция catch или fina l l y. Ниже приведен пример программы с тремя методами, которые заверша­ ются раз ными способами, не пропуская выполнение конструкции fina l l y: / / Демонстрация применения finally . class Final lyDemo { / / Сгенерировать исключение в нутри метода . static void procA ( ) { try { Sys tem . out . p r i ntln ( " Внутри метода procA ( ) " ) ; throw new Runt imeExcept i on ( " демонстрация " ) ; finally { System . out . p r intln ( " Блок finally метода procA ( ) " ) ; / / Возвратить управление изнутри блока try . static vo id procB ( ) { try { System . out . println ( "Bнyтpи метода procB ( ) " ) ; return; fina l l y { System . out . println ( "Блoк f inal l y метода procB ( ) " ) ; / / Выполни т ь блок try обычным образом . static void procC ( ) { Гn а ва 10. Об ра ботк а и с кnючени й 287 try { Sys tem . out . println ( "Внут ри метод а procC ( ) " ) ; final l y { S ystem . out . println ( "Блoк final l y метода procC ( } " } ; puЫic static vo id ma in ( String [ ) args } { try { procA ( } ; catch ( Except ion е } { Sys tem . out . println ( "Исключение перехвачено " } ; } procB ( } ; procC ( } ; Оператор try в методе procA ( ) преждевременно прерывается генерацией исключения. Блок final l y выполняется при выходе. Оператор try в методе procB ( ) завершается оператором return. Блок f ina l l y выполняется до воз­ врата из procB ( ) . В методе procC ( ) оператор try выполняется нормально, без ошибок. Однако блок fina l l y все равно выполняется. Помните! Если с оператором t r у ассоциирован блок f ina 1 1 у, то этот блок будет выполнен по завершении try. Вот вывод, полученный в результате запуска предыдущей программы: Внутри метода procA ( } Блок fina l l y метода procA ( } Исключение перехвачено Внутри метода procB ( } Блок final ly мет ода procB ( ) Внутри метода procC ( } Блок finally метода procC ( } В с троен н ые ис к лю ч ен ия Java Внутри стандартного пакета j ava . l ang определено несколько классов ис­ ключений Java. Некоторые из них использовались в предшествующих при­ мерах. Наиболее общие из них являются подклассами стандартного типа Runt imeExcept i on. Как объяснялос ь ранее, такие исключения не нужно включать в список throws любого метода. На языке Java они называются непроверяемыми исключениями, потому что компилятор не проверяет, обраба­ тывает метод подобные и с ключения или же генерирует их. Непроверяемые ис ключения, определенные в j a va . lang, описаны в табл. 10. 1 . В табл. 10.2 переч ислены те исключения, определенные в j ava . lang, которые должны помещаться в список throws метода, если метод может генерировать одно из исключений и не обрабатывает его самос тоятельно. Они называются прове­ ряемыми исключениями. Помимо исключений из j ava . lang в Java определено еще несколько, которые относятся к другим стандартным пакетам. 288 Часть 1. Язык Java Табnица 1 0. 1 . Подклассы RuntimeException непроверяемых искnючений Java, опредеnенные в j ava . lang ArithrneticException Арифметическая ошибка, такая как деле­ ние на ноль ArrayindexOutOfBounds Except ion Выход за допустимые пределы индекса в массиве ArrayStoreException Присваивание элементу массива значе­ ния несовместимого типа ClassCastExcept ion Недопустимое приведение EnurnConstant Not PresentExcept ion Попытка использования неопределенно­ го значения перечисления I llegalArgumentException Использование недопустимого аргумен­ та при вызове метода I llegalCal lerException Метод не может быть законно выполнен вызывающим кодом I llegalMonitorState Except ion Недопустимая операция монитора, такая как ожидание неблокированноrо потока I llegalStateException Некорректное состояние среды или при­ ложения I l legalThreadStateException Несовместимость запрошенной опера­ ции с текущим состоянием потока IndexOutOfBoundsException Выход за допустимые пределы индекса некоторого вида LayerinstantiationException Невозможность создания уровня модуля NegativeArraySizeException Создание массива с отрицательным раз­ мером NullPointerException Недопустимое использование ссылки nul l NumЬerFormatExcept ion Недопустимое преобразование строки в числовой формат SecurityException Попытка нарушения безопасности Stringi ndexOutOfBounds Exception TypeNotPresentExcept ion Попытка индексации за границами строки Тип не найден UnsupportedOperation Exception Обнаружение неподдерживаемой опера­ ции Глава 10. Обработка мскnюченмii 289 Твбпица 10.2. Кnассы nроверяемых искnючений Jвva, оnреде11енные в java . lang ClassNotFoundException CloneNotSupportedException Класс не найден IllegalAccessException InstantiationException Доступ к классу запрещен InterruptedException Один поток был прерван другим потоком NoSuchFieldException NoSuchМethodException ReflectiveOperation Exception Запрошенное поле не существует Попытка клонирования о6ьекта, который не реализует интерфейс CloneaЫe Попытка создания объекта абстрактно­ го класса или интерфейса Запрошенный метод не существует Суперкласс исключений, связанных (: рефлексией Со здан и е с обстве нных подклассов Exception Хотя встроенные исключения Java обрабатывают наиболее распростра­ ненные ошибки, вполне вероятно, что вы захотите создать собственные типы исключений, которые подходят для ситуаций, специфичных для ваших при­ ложений. Делается зто довольно легко: нужно просто определить подкласс Exception (который, конечно же, является подклассом ThrowaЫe). Вашим подклассам фактически ничего не придется реализовывать - одно их суще­ ствование в системе типов позволяет использовать их как исключения. В са­ мом классе Exception никаких методов не определено. Разумеется, он насле­ дует методы, предоставляемые ThrowaЫe. Таким образом, все исключения, в том числе созданные вами, имеют доступные для них методы, которые опреде­ лены в классе ThrowaЫe и описаны в табл. 10.3. Вы также можете переопреде­ лить один или несколько из этих методов в создаваемых классах исключений. Таб11ица 10.3. Методы, оnредепенные в кnассе ТhrowaЫe final void addSuppressed ( ThrowaЫe ехс ) ThrowaЫe fillinStackTrace ( ) Добамяет ехс в список подавляемых исключе­ ний, ассоциированный с вызывающим исклю­ чением. Метод предназначен главным образом для использования в операторе try с ресурсами Возвращает объект ThrowaЫe, который со­ держит полную трассировку стека. Этот объ­ ект может быть сгенерирован повторно 290 Часть 1. Язык Java Окончание табл 10.3 ThrowaЫe getcause ( ) String getLocali zedМessage ( ) String getM�ssage ( ) StackTraceElement [ ] getStackTrace ( ) final ThrowaЫe [ ] getSuppressed ( ) ThrowaЫe initCause ( ThrowaЫe causeExc) void printStackTrace ( ) void printstack:Тrace ( PrintStream stream) void printStackTrace ( PrintWriter stream) void setStackTrace ( StackTraceElement [ ] elements ) String tostring ( ) Возвращает исключение, которое лежит в основе текущего исключения. Если лежащее в основе исключение отсутствует, тоrда воз­ вращается null Возвращает локализованное описание исключения Возвращает описание исключения Возвращает массив объектов StackTrace Element, содержащий поэлементную трасси­ ровку стека. Метод на верхушке стека - это тот, который был вызван последним перед rе­ нерацией исключения. Он находится в первом элементе массива. Класс StackTraceElement предоставляет программе доступ к информа­ ции о каждом элементе в трассировке стека, такой как имя метода Получает подавляемые исключения, ассоции­ рованные с вызываемым исключением, и воз­ вращает массив, который содержит результат. Подавляемые исключения rенерируются rлав­ ным образом оператором try с ресурсами Связывает causeExc с вызывающим ис­ ключением как причину его возникновения. Возвращает ссылку на исключение Отображает трассировку стека Посылае'I' трассировку стека в указанный поток Посылает трассировку стека в указанный поток Устанавливает трассировку стека в эле­ менты, переданные в параметре elements. Предназначен для специализированноrо, а не нормальноrо применения Возвращает объект String, содержащий опи­ сание исключения. Вызывается оператором println ( ) при выводе объекта ThrowaЫe Гла в а 1 0. Об р аб отка исклю че н ий 291 В классе Exception определены четыре открытых конструктора. Два из них поддерживают сцепленные исключения, обсуждаемые в следующем раз­ деле, а другие два показ аны ниже: Exception ( ) Except ion ( S tring msg ) Первая форма конструктора создает искл ючение, не имеющее описания. Вторая форма конструктора поз воляет указать в msg описание исключения. Хотя указание описания при соз дании искл ючения часто полезно, иногда лучше переопределить метод toSt ring ( ) и вот почему: версия toSt ring ( ) , определенная в классе ThrowaЫe (и унаследованная классом Excep t ion), сначала отображает имя исключения, за которым следует двоеточие и ваше описание. Переопредел и в toS t r i ng ( ) , вы можете з апретить отображение имени исключения и двоеточия, сдела в вывод более чистым, что желательно в некоторых ел учаях. В следующем примере объявляется новый под класс Excep t ion, который з атем используется для сигнализ ации об ошибке в методе. В подклассе пере­ определяется метод toString ( ) , поз воляя отобразить аккуратно настроен­ ное описание исключения. // В этой программе создается специаль ный тип ис ключения . c l a s s MyExcept ion extends Exception { p r ivate int deta i l ; MyExcept i on ( int а ) { detai l = а ; puЫ ic Str ing toS t r i ng ( ) { return "MyException [ " + detail + " ] " ; cl ass ExceptionDemo { static void compute ( int а ) throws MyException { System . out . println ( " Bызoв compute ( " + а + " ) " ) ; if(a > 10) throw new MyException ( a ) ; System . out . printl n ( " Hopмaль нoe завершение " ) ; puЫ ic static void ma i n ( S tring [ ] args ) { try { compute ( l ) ; compute ( 2 0 ) ; catch ( MyException е ) { System . out . pr intln ( " Пepexвaчeнo исключение " + е ) ; В примере определяется подкласс Excep t i o n по имени MyExcepti on. Он довол ьно прост: имеет только конструктор и переопределенный ме- 292 Часть 1. Язык Java тод t o S t r ing ( ) , который отображает значение исключения. В классе Except i onDemo определен метод ca l c u l a t e ( ) , который создает объект MyException. Исключение генерируется, когда значение целочисленного па­ раметра ca lculate ( ) больше 1 0. В методе main ( ) устанавливается обработ­ чик исключений для MyExcept ion, после чего вызывается метод calculate ( ) с допустимым значением (меньше 1 0) и недопустимым, чтобы продемонстри­ ровать оба пути через код. Вот результат: Вызов compute ( l ) Нормаль ное завершение Вызов compute ( 2 0 ) Перехвачено исключение MyException [ 2 0 ] Сцеплен ные искл ючения Несколько лет назад в подсистему исключений было включено средство под названием с11епленные исключения. Сцепленные исключения позволяют ассоциировать с одним исключением другое исключение, которое описывает причину первого исключения. Например, представьте ситуацию, в которой метод выдает исключение Ari thmet icException из-за попытки деления на ноль. Тем не менее, фактической причиной проблемы было возникновение ошибки ввода-вывода, из-за которой делитель установился неправильно. Хотя метод, безусловно, должен генерировать исключение ArithmeticException, т.к. произошла именно указанная ошибка, вы можете сообщить вызывающе­ му коду, что основной причиной была ошибка ввода-вывода. Сцепленные ис­ ключения позволяют справиться с этой и любой другой ситуацией, в которой существуют уровни исключений. Чтобы сделать возможными сцепленные исключения, в класс ThrowaЫe были добавлены два конструктора и два метода. Конструкторы приведены ниже: Th rowaЫ e ( ThrowaЫe caus eExc ) Th rowaЫ e ( String msg, ThrowaЫe causeExc ) В первой форме causeExc является исключением, которое привело к воз­ никновению текущего исключения, т.е. представляет собой основную причи­ ну его возникновения. Вторая форма позволяет указать описание одновре­ менно с указанием причины исключения. Эти два конструктора также были добавлены в классы Error, Exception и Runt imeExcept ion. Класс ThrowaЫe поддерживает методы для сцепленных исключений getCause ( ) и ini tCause ( ) , которые были описаны в табл. 10.3 и повторяют­ ся здесь ради целей обсуждения: ThrowaЫe getCause ( ) ThrowaЫe initCause ( ThrowaЫ e causeEx c ) Метод getCause ( ) возвращает исключение, лежащее в основе текущего исключения. Если лежащего в основе исключения нет, тогда возвращается nul l. Метод ini tCause ( ) связывает causeExc с вызывающим исключением Глава 1 0. Обработка и сключе ний 293 и возвращает ссылку на исключение. Таким образом, вы можете ассоцииро­ вать причину с исключением после его создания. Однако исключение-при­ чина может быть установлено только один раз, т.е. вызывать in itCause ( ) для каждого объекта исключения допускается только один раз. Кроме того, если исключение-причина было установлено конструктором, то вы не можете установить его снова с помощью ini tCause ( ) . В общем, метод ini tCause ( ) используется с целью установки причины для устаревших классов искл юче­ ний, которые не померживают два дополнительных конструктора, описан­ ных ранее. Ниже приведен пример, иллюстрирующий механику обработки сцеплен­ ных исключений: / / Демонстрация работы сцепленных исключений . class Cha inExcDemo { static void demoproc ( ) { / / Создать исключение . Null PointerException е new Null PointerEx ception ( " вepxний уровень " ) ; / / Добавить причину . e . initCause ( new Ari thmeti cException ( "пpичинa " ) ) ; throw е ; puЫ i c static void main ( S t ring [ ] a rgs ) { try { demoproc ( ) ; catch ( Nu l l Pointe rException е ) { / / Отобразить исключение верхнего уровня . System . out . println ( " Пepexвaчeнo : " + е ) ; / / Отобразить исключение-причину. System . out . println ( " Пepвoнaчaльнaя причина : " + e . getCause ( ) ) ; Вот вывод, полученный в результате запуска программы: Перехвачено : j ava . lang . Nul l PointerException : верхний уровень Первоначальная причина : j ava . l ang . Arithmeti cException : причина В приведенном примере иск л ючением верхнего уровня яв л яется NullPo interExcept ion. К нему добавлено исключение-причина, Ari thmetic Except ion. Когда исключение генерируется из demoproc ( ) , оно перехваты­ вается в ma in ( ) . Там отображается исключение верхнего уровня, за которым следует базовое исключение, получаемое путем вызова getCause ( ) . Сцепленные искл ючения могут быть реализованы с л юбой необходимой глубиной. Таким образом, исключение-причина может и само иметь причину. Имейте в виду, что сл ишком длинные цепочки исключений могут указывать на неудачное проектное решение. 294 Ч а сть 1 . Язык Java Сцепл енные исключения - это не то, что нужно каждой программе. Тем не менее, в тех случаях, когда пол езно знать основную причину, они предла­ гают элегантное решение. Тр и допо лн и те льн ы х с ред с тва в сис теме ис клю ч е н и й В версии JDK 7 к системе искл ючений были добавлены три интересных и полезны х средства. Первое средство автоматизирует процесс освобождения ресурса, такого как файл, когда он больше не нужен. Оно основано на расши­ ренной форм е оператора try под названием try с ресурсами и описано в гла­ ве 1 3, где обсуждаются файлы. Второе средство называется множественным перехватом, а третье иногда упоминается как финальная повторная генерация или более точная повторная генерация. Последние два средства описаны ниже. Средство множественного перехвата позволяет перехватывать два или бо­ лее исключений одной и той же конструкцией catch. Нередко два или более обработчика исключ ений применяют ту же самую кодовую последовател ь­ ность, даже есл и реагируют на разные исключ ения. Вместо перехвата каждо­ го типа исключ ения по отдель ности можно использовать одну конструкцию ca tch для обработки всех искл ючений без дублирования кода. Для прим енения множественного перехвата необходимо объединить все типы исключ ений в конструкции catch с помощью операции "ИЛИ': Каждый пара метр множественного перехвата неявно является f inal. (При желании f ina l можно указывать явно, но это не обязательно.) Посколь ку каждый па­ раметр множественного перехвата неявно явля ется f inal, ему нельзя при­ сваивать новое значение. Вот конструкция c a t ch, которая испол ьзует средство множественного перехвата для Arithme t i cExcept ion и Arrayindex0ut0fBoundsExcept i on: catch (Arithmeti cException I Arrayi ndexOutOfBoundsException е ) { Ниже средство множественного пер ехвата демонстрируется в действии: / / Демонстрация средства множественного перехвата . class Mul t iCatch { puЫ i c static void main ( String [ ] args ) { int a= l 0 , Ь= О ; int [ ] val s = { 1 , 2 , 3 } ; try { int result = а / Ь ; // сгенерировать исключение ArithmeticException // val s [ l 0 ] = 1 9 ; / / сгенерировать исключение // Arrayi ndexOutOfBoundsExcept ion / / Сле дующая конструкция catch перех ватывае т оба исключения . catch (ArithmeticException I Arrayi ndexOutOfBoundsException е ) System . out . println ( "Пepexвaчeнo исключение : " + е ) ; System . out . println ( " П ocлe множественного перехвата . " ) ; Глава 10. Обработка иск л ючений 295 Программа сгенерирует исключение Ari thmet icException при попытке деления на ноль. Если вы закомментируете оператор с делением и удалите символ комментария в следующей строке, тогда будет создано исключение Arrayi nde x0ut0fBoundsExcept i on. Оба исключения перехватываются од­ ной конструкцией catch. Средство более точной повторной генерации ограничивает тип исключе­ ний, которые могут повторно генерироваться, только теми проверяемыми исключениями, которые выдает связанный блок try, которые не обрабаты­ ваются предыдущей конструкцией catch и которые являются подтипом или супертипом параметра. Хотя такая возможность может требоваться нечасто, теперь она доступна для использования. Чтобы задействовать средство более точной повторной генерации, параметр catch обязан быть либо фактически f i nal, т.е. ему не должно присваиваться новое значение внутри блока catch, либо явным образом объявляться как final. Использование искл ючени й Обработка исключений предоставляет мощный механизм управления сложными программами, обладающими множеством динамических харак­ теристик во время выполнения. Важно думать о try, throw и са tch как об аккуратных способах обработки ошибок и необычных граничных условий в логике вашей программы. Вместо применения кодов возврата для обозначе­ ния ошибок используйте возможности Java по обработке исключений. Таким образом, когда метод может отказать, он должен генерировать исключение. Это более ясный способ обработки режимов отказа. И последнее замечание: операторы обработки исключений Java не следует рассматривать как общий механизм нелокального ветвления, потому что это только запутает код и затруднит его сопровождение. ГЛ А ВА М но го п о то ч ное п ро г ра ммирование В языке Java обеспечивается встроенная поддержка многопоточного про­ граммирования. Многопоточная программа состоит из двух или более частей, которые могут выполняться одновременно. Каждая часть такой программы называется потоком, и каждый поток определяет отдельный путь выполне­ ния. Таким образом, многопоточность представляет собой специализирован­ ную форму многозадачности. Вы почти наверняка знакомы с многозадачностью, потому что она под­ держивается практически всеми современными операционными системами (ОС). Однако есть два разных типа многозадачности: на основе процессов и на основе потоков. Важно понимать разницу между ними. Многим чита­ телям больше знакома многозадачность, основанная на процессах. По сути, процесс - это программа, которая выполняется. Таким образом, многоза­ дачность на основе процессов является функциональным средством, которое позволяет вашему компьютеру запускать две или большее число программ параллельно. Например, многозадачность на основе процессов позволяет запускать компилятор Java одновременно с использованием текстового ре­ дактора или посещением веб-сайта. В многозадачности, основанной на про­ цессах, программа представляет собой наименьшую единицу кода, которая может координироваться планировщиком. В многозадачной среде, основанной на потоках, поток является наимень­ шей единицей координируемого кода, т.е. одна программа может выполнять две или более задач одновременно. Например, текстовый редактор может форматировать текст одновременно с его выводом на печать, если эти два действия выполняются двумя отдельными потоками. Таким образом, много­ задачность на основе процессов имеет дело с "общей картиной'; а многозадач­ ность на основе потоков обрабатывает детали. Многозадачные потоки требуют меньше накладных расходов, чем много­ задачные процессы. Процессы - это тяжеловесные задачи, требующие от­ дельного адресного пространства. Взаимодействие между процессами явля­ ется затратным и ограниченным. Переключение контекста с одного процесса на другой также обходится дорого. С другой стороны, потоки более легко­ весны. Они совместно используют одно и то же адресное пространство и Глава 1 1 . М н ого п оточное про граммирова н и е 297 од ин и тот же тяжеловесный процесс. Вз аи модействие между потоками не сопряжено с высокими з атратами, а переклю чение контекста с одного потока на другой обходится дешевле. В то время как программы на J ava применяют многоз адачные среды, основанные на процессах, многозадачность на основе процессов не находится под непосредственным контролем Java. Тем не менее, многопоточная многоз адачность есть. Многопоточность позволяет писать эф фективные программы, которые максимально з адействуют вычисл ительную мощность, доступную в системе. Одним из важных способов достижения такой цел и с помощью многопоточ­ ност и является сведение к минимуму времени простоя, что особенно важно для интерактивной сетевой среды, в которой работает Java, поскольку время простоя будет обычным явлением. Например, скорость передачи данных по сети намного ниже скорост и, с которой компьютер способен их обрабатывать. Даже ресурсы локальной файловой системы читаются и записываются гораздо медленнее, чем они могут быть обработаны процессором. И, конечно же, поль­ зовательский ввод намного медленнее, нежели компьютер. В однопоточной среде программа должна ожидать з авершения каждой задачи, прежде чем она сможет перейти к следующей з адаче, даже есл и большую часть времени про­ грамма бездействует в ожидании ввода. Многопоточность помогает сократить это время простоя, т.к. другой поток может выполняться, когда од ин ожидает. Если вы программировал и для таких ОС, как Windows, то уже з накомы с многопоточным программированием. Однако тот факт, что Java управляет потоками, делает многопоточность особенно удобной, потому что многие де­ тал и выпол няются за вас. П ото ко ва я мод ел ь Java Исполняющая среда Java во многом з ависит от потоков, и все библиоте­ ки классов раз работаны с учетом многопоточности. На самом деле Java ис­ пользует потоки, чтобы вся среда была аси нхронной. Это помогает сниз ить неэффект ивность за с чет предотвращения потери циклов центрального про­ цессора (ЦП). Ценность многопоточной среды лучше всего понимается по контрасту с ее аналогом. В однопоточных системах применяется подход, называемый ц,иклом обработки событий с опросом. В такой модели один поток управления рабо­ тает в бесконечном цикле, опрашивая одну очередь событий, чтобы решить, что делать дальше. Как только этот механиз м опроса возвращает, скажем, сиг­ нал о том, что сетевой файл готов к чтению, цикл обработки событ ий передает управление соответствующему обработчику событий. Пока данный обработ­ чик события не возвратит управление, в программе больше ни чего не может произойти, из-за чего тратится процессорное время. Кроме того, может полу­ читься так, что одна часть программы будет доминировать над системой и пре­ пятствовать обработке любых других событий. В целом в однопоточной среде, когда поток блокируется (т.е. приостанавливает выполнение) по при чине ожи­ дания некоторого ресурса, то прекращает выполнение вся программа. 298 Часть 1. Язык Java Преимущество многопоточности в Java заключается в том, что механизм главного цикла/опроса устранен. Один поток может делать паузу, не оста­ навливая другие части программы. Например, время простоя, возникающее при чтении потоком данных из сети или ожидании пользовательского ввода, можно задействовать в другом месте. Многопоточность позволяет циклам анимации приостанавливаться на секунду между к аждым кадром, не вызывая паузы в работе всей системы. Когда поток блокируется в программе на Java, приостанавливается только од ин заблокированный поток. Все остальные по­ токи продолжают функционировать. Как известно большинству читателей, за последние несколько лет много­ ядерные системы стали обычным явлением. Конечно, одноядерные системы по-прежнему широко применяются. Важно понимать, что многопоточность Java работает в системах обоих типов. В одноядерной системе одновременно выполняющиеся потоки совместно используют ЦП, при этом каждый поток получает долю процессорного времени. Таким образом, в одноядерной систе­ ме два или более потока фактически не выполняются одновременно, но за­ действуется время простоя ЦП. Однако в многоядерных системах возможно одновременное выполнение двух или более потоков. Во многих случаях уда­ ется еще больше повысить эффективность программы и увеличить скорость выполнения определенных операций. На заметку! В дополнение к функциям многопоточности, описанным в этой главе, вас наверняка заинтересует инфраструктура Fork/Join Framework. Она предлагает мощные средства для создания многопоточных приложений, которые автоматически масштабируются с целью наилучшего использования многоядерных сред. Инфраструктура Fork/Join Framework является частью поддержки параллельного программирования в Java, которое представляет собой обычное название методик, оптимизирующих некоторые типы алгоритмов для параллельного выполнения в системах с несколькими процессорами. Обсуждение Fork/Join Framework и других утилит параллелизма ищите в главе 29. Здесь описаны традиционные возможности многопоточности Java. Потоки пребывают в нескольких состояниях. Далее приведено общее описание. Поток может выполняться. Он может быть готовым к запуску, как только получит время ЦП. Работающи й поток может быть приостановлен, в результате чего он временно прекращает свою активность. Выполнение прио­ становленного потока затем можно возобновить, позволяя ему продолжиться с того места, где он остановился. Поток может быть заблокирован при ожи­ дании ресурса. В любой момент работа потока может быть прекращена, что немедленно останавливает его выполнение. После прекращения работы вы­ полнение потока не может быть возобновлено. Приоритеты потоков Каждому потоку в Java назначается приоритет, который определяет спо­ соб обработки данного потока в сравнении с другими потоками. Приоритеты потоков представляют собой целые числа, определяющие относительный Глава 1 1 . М ногопоточное программирование 299 приоритет одного потока над другим. Как абсолютная вел ичина приоритет не имеет смысла; поток с более высоким приоритетом не работает быстрее потока с более низким приоритетом, есл и он является единственным функ­ ционирующим потоком. Взамен приоритет потока применяется для принятия решения о том, когда перек л ючаться с одного работающего потока на другой. Прием называется переключением контекста. Правила, которые определяют, когда происходит перекл ючение контекста, просты и описаны ниже. • Поток может добровольно передать управление. Это происходит при яв­ ной уступке очереди, приостановке или блокировке. В таком сценарии проверяются все остальные потоки, и поток с наивысшим приоритетом, готовый к выполнению, получает ЦП. • Поток может быть вытеснен потоком с более высоким приоритетом. В таком случае поток с более низким приоритетом, который не уступает ЦП, просто вытесняется - независимо от того, что он делает - пото­ ком с более высоким приоритетом. По сути, как только поток с более высоким приоритетом желает запуститься, он это делает. Прием назы­ вается вытесняющей многозадачностью. В с лучаях, когда два потока с одинаковым приоритетом конкурируют за цик лы ЦП, ситуация немного усложняется. В некоторых ОС потоки с одина­ ковым приоритетом автоматически разделяются по времени в цик лическом режиме. Для других типов ОС потоки с равным приоритетом должны добро­ вольно передавать управление своим партнерам. Если они этого не сделают, тогда другие потоки не запустятся. Внимание! Проблемы с переносимостью могут возникать из-за различий в том, как ОС переключают контекст для потоков с одинаковым приоритетом. Синхронизация Поскольку многопоточность привносит асинхронное поведение в ваши программы, должен быть способ обеспечения синхронности, когда она не­ обходима. Скажем, если вы хотите, чтобы два потока взаимодействовали и совместно использовали сложную структуру данных, так ую как связный спи­ сок, то вам нужен способ предотвращения конфликтов между ними. То есть вы должны запретить одному потоку записывать данные, пока другой поток находится в процессе их чтения. Для этой цели в Java реализован э легантный вариант старой модели синхронизации между процессами: монитор. Монитор представляет собой механизм управления, который был впервые определен Ч.Э.Р. Хоаром. Вы можете думать о мониторе как об очень маленьком "ящике'; способном содержать только один поток. Как только поток входит в монитор, все остальные потоки должны ждать, пока этот поток не выйдет из монитора. Таким образом, монитор можно применять для защиты общего ресурса от манипулирования более чем одним потоком одновременно. 300 Часть 1. Язык Java В Java нет класса, который назывался бы "Monitor"; взамен каждый объект имеет собственный неявный монитор, в который автоматически осуществля­ ется вход при вызове одноrо из синхронизированных методов объекта. Как только поток оказывается внутри синхронизированного метода, остальные потоки не моrут вызывать какой-либо друrой синхронизированный метод для тоrо же объекта. Это позволяет писать очень ясный и лаконичный мноrо­ поточный код, поскольку в язык встроена поддержка синхронизации. Обмен сообщен иями После разделения своей программы на отдельные потоки вам нужно опре­ делить, как они будут взаимодействовать друr с друrом. Для установления связи между потоками в ряде друrих языков вы должны полагаться на ОС. Разумеется, накладные расходы возрастают. Напротив, Java предоставляет нескольким потокам ясный и экономичный способ взаимодействия друr с друrом через вызовы предопределенных методов, которые есть у всех объек­ тов. Система обмена сообщениями Java позволяет потоку войти в синхрони­ зированный метод объекта и затем ожидать, пока какой-то друrой поток явно не уведомит ero о выходе. Класс Thread и и н терфейс RunnaЫe Мноrопоточная система Java построена на основе классе Thread, ero ме­ тодах и дополняющем интерфейсе RunnaЫe. Класс Thread инкапсулирует поток выполнения. Поскольку вы не можете напрямую ссылаться на немате­ риальное состояние работающего потока, то будете иметь с ним дело через ero посредника, т.е. работать с экземпляром Thread, который ero породил. Чтобы создать новый поток, ваша программа либо расширит класс Thread, либо реализует интерфейс RunnaЫe. В классе Thread определено несколько методов, помогающих управлять потоками. Некоторые из тех, что использовались в этой rлаве, перечислены в табл. 1 1 . 1. Табnица 1 1 .1 . Методы, помогающие уnравnять потоками getName ( ) Получает имя потока get Priori t y ( ) Получает приоритет потока i sAlive ( ) Определяет, выполняется ли поток j oi n ( ) Ожидает прекращения работы потока run ( ) Устанавливает точку входа в поток sleep ( ) Приостанавливает выполнение потока на указанное время start ( ) Запускает поток вызовом ero метода run ( ) Глава 1 1 . М н огопоточ н о е программирова н и е 301 До сих пор во всех примерах, приведенных в книге, применялся один по­ ток выполнения. Далее в главе будет объясняться, как использовать Thread и RunnaЫe для создания потоков и управления ими, начиная с одного потока, который есть во всех программах Java: главного потока. Главный поток Когда программа Java запускается, немедленно начинает выполняться один поток. Обычно его называют главным потоком программы, потому что имен­ но он выполняется при ее запуске. Главный поток важен по двум причинам: • он является потоком, из которого будут порождаться другие "дочерние" потоки; • часто он должен заканчивать выполнение последним, поскольку выпол­ няет разнообразные действия по завершению работы. Хотя главный поток создается автоматически при запуске программы, им можно управлять с помощью объекта Thread. Для этого понадобится полу­ чить ссылку на него, вызвав метод currentThread ( ) , который является от­ крытым статическим членом класса Thread со следующим общим видом: static Thread currentThread ( ) Этот метод возвращает ссылку на поток, в котором он вызывается. Когда у вас есть ссылка на главный поток, вы можете управлят,ь им так же, как и любым другим потоком. Начнем с рассмотрения показанного ниже примера: // Управление главным потоком . class CurrentThreadDemo { puЫ i c s tatic void main ( String [ ] args ) Thread t = Thread . currentThread ( ) ; System . out . println { "Teкyщий пото к : " + t ) ; / / Изменить имя потока . t . setName ( "Му Thread" ) ; System . out . println ( " Пocлe изменения имени : " + t ) ; try { for ( int n = 5 ; n > О ; n-- ) System . out . println ( n ) ; Thread . s leep ( l O O O ) ; catch ( I nterruptedException е ) { System . out . println ( "Главный поток прерван" ) ; В приведенной программе ссылка на текущий (в данном случае главный) поток получается вызовом currentThread ( ) и сохраняется в локальной пе­ ременной t. Затем программа отображает информацию о потоке, вызывает setName ( ) для изменения внутреннего имени потока и повторно отобража­ ет информацию о потоке. Далее выполняется обратный отсчет от пяти до 302 Часть 1. Язык Java единицы с паузой, составляющей одну секунду, между выводом строк. Пауза достиг ается методом s leep ( ) . Аргумент функции s leep ( ) задает период задержки в миллисекундах. Обратите внимание на блок try/ catch, внутрь которого помещен цикл . Метод s leep ( ) в Thread может сгенерировать ис­ клю чение I nterruptedException в случае, если какой-то другой поток по­ жел ает прервать этот спящий поток. В данном примере просто вводится со­ общение, если поток прерывается, но в реал ьной программе пришлось бы поступать по-другому. Вот вывод, выдаваемый программой: Текущий пото к : Thread [ mai n , 5 , ma i п ] После изменения имени : Thread [ My Thread , 5 , maiп ] 5 4 3 2 1 Обратите внимание на результат, когда t используется в качестве аргумен­ та функции p rintln ( ) . В результате по порядку отображается имя потока, его приоритет и имя его группы. По умол чанию именем главного потока яв­ ляется ma in. Его приоритет равен 5 - стандартному значению, и mai n также будет именем группы потоков, к которой принадлежит текущий поток. Группа потоков представляет собой структуру данных, которая управляет состояни­ ем набора потоков в целом. После изменения имени потока переменная t снова выводится. На этот раз отображается новое имя потока. Давайте более подробно рассмотрим методы класса Thread, которые при­ меняются в программе. Метод s leep () заставляет поток, из которого он вы­ зывается, приостанавливать выполнение на заданный период времени в мил­ лисекундах. Ниже показан его общий вид: s tatic void s leep ( loпg mi l l i seconds ) throws I nterruptedExceptioп Кол и чество милл исекунд для приостановки указывается в аргументе milliseconds. Метод sleep () может сгенерировать исклю чение I nterrupted Exception. Метод s leep () имеет вторую форму, которая позволяет указывать период времени в миллисекундах и наносекундах: static void sleep ( loпg milliseconds , int nanoseconds ) throws InterruptedException Вторая форма s leep ( ) полезна только в средах, которые допускают вре­ менные периоды с точностью до наносекунд. В предыдущей программе видно, что с помощью setName ( ) вы можете устанавл ивать имя потока. Получить имя потока можно, вызвав getName ( ) (но обратите внимание, что в программе это не показано). Упомянутые мето­ ды являются членами класса Thread и объявлены следующим образом: final void setName ( String threadName ) final String getName ( ) В threadName указывается имя потока. Глава 1 1 . Многопоточ ное программирование 303 Создание потока В самом общем случае поток соз дается путем соз дания экземпляр объекта типа Thread. В языке Java предусмотрены два способа: • можно реализовать интерфейс RunnaЫe; • можно расширить класс Thread. В последующих двух раз делах рассматриваются оба способа. Реа л изация и нтерфейса RunnaЬle Самый простой способ создать поток предусматривает создание класса, реализ ующего интерфейс RunnaЫe, который абстрагирует единицу испол­ няемого кода. Вы можете создать поток для л юбого объекта, реализ ующего RunnaЫe. Для реализации RunnaЫe в классе понадобится реализовать толь­ ко один метод с именем run () , объявление которого показано ниже: puЫ i c void run ( ) Внутрь метода run () помещается код, который основывает новый поток. Ва жно понимать, что run () может вызывать другие методы, и спользовать другие классы и объявлять переменные в точности, как это делает главный поток. Единственное отличие заключается в том, что метод run () устанавл и­ вает точку входа для другого параллельного потока выполнения в программе. Этот поток з аверши тся, когда управление возвратится из run () . После соз дания класса, реализ ующего интерфейс RunnaЫe, внутри него создается объект типа Thread. В классе Threa d определено несколько кон­ структоров. Вот тот, который будет применяться: Thread ( RunnaЫe threadOb , String threadName ) В приведенном конструкторе threadOb является экземпляром класса, ре­ ализующего интерфейс RunnaЫe. Он определяет, где начнется выполнение потока. Имя нового потока определяется параметром threadName. После создания новый поток не будет з апущен, пока не вызовется его ме­ тод start () , объявленный в классе Thread По существу sta rt () иницииру­ ет вызов run () . Метод start () показ ан ниже: void start ( ) Далее приведен пример создания нового потока и запуска его выполнения: // Создание второго потока . class NewThread implements RunnaЫe { Thread t ; NewThread ( ) { // Создать новый , второй поток . t = new Thread ( this , " Demo Thread" ) ; Sys tem . out . println ( " Дочерний поток : " + t ) ; / / Это точка входа для второго потока . 304 Часть 1. Язык J ava puЬlic void run ( ) try { for ( i nt i = 5 ; i > О ; i-- ) { System . out . println ( "Дoчepний поток : " + i ) ; Thread . s leep ( S O O ) ; catch ( Inter ruptedException е) { System . out . println ( "Дoчepний поток прерван . " ) ; System . out . println ( "Зaвepшeниe дочернего потока . " ) ; class ThreadDemo { puЫ ic static void ma in ( String [ ] args ) / / создать новый поток NewTh read nt = new NewThread ( ) ; / / запустить поток nt . t . start ( ) ; try { for ( int i = 5 ; i > О ; i - - ) { System . out . pr i ntln ( " Глaвный поток : " + i ) ; Thread . s l eep ( l O O O ) ; catch ( I nterruptedException е ) { System . out . println ( "Главный поток прерван . " ) ; System . out . printl n ( "Завершение главного потока . " ) ; Новый объект Thread создается внутри конструктора класса NewThread с помощью следующего оператора: t = new Thread ( this , "Demo Thread" ) ; Передача this в качестве первого аргумента указывает на то, что новый поток должен вызвать метод run ( ) для данного объекта. Внутри rna in ( ) вы­ зывается start ( ) , который запускает поток выполнения, начиная с метода run ( ) . Это приводит к тому, что цикл for дочернего потока начинает свою работу. Затем главный поток входит в цикл for. Оба потока продолжают ра­ ботать, совместно используя ЦП в одноядерных системах, пока их цикл не завершится. Результат работы программы представлен ниже. (Выходные дан­ ные могут отличаться в зависимости от конкретной среды выполнения.) Дочерний поток : Thread [ Demo Thread , 5 , ma i n ] Главный пото к : 5 Дочерний поток : 5 Дочерний поток : 4 Главный поток : 4 Дочерний поток : 3 Дочерний поток : 2 Главный поток : 3 Дочерний поток : l Завершение дочернего потока . Главный пото к : 2 Гла вный поток : 1 Завершение главного потока . Гл ава 1 1 . М н огоnоточно е прог раммирова н и е 305 Как упоминалось ранее, в мноrопоточной программе ч асто бывает полез ­ но, чтобы гл авный поток заканчивал работу последним. Предыдущая про­ грамма гарантирует, что r л авный поток завершится последним, потому что он засыпает на 1000 милл исекунд между и терациями, а дочерний поток только на 500 милл исекунд. В итоге дочерний поток завершается раньше, чем гл авный. Вскоре вы увидите лучши й способ дождаться з авершения потока. Рас ш и рен и е кл асса Thread Второй способ создания потока предусматривает создание нового класса, расширяющего Thread, и создание экземпляра этого класса. Расширяющий класс должен переопредел ить метод run ( ) , который является точкой входа для нового потока. Как и раньше, вызов start ( ) начинает выпол нение нового потока. Вот предыдущая программа, переписанная для расширения Thread: // Создание второго потока путем расширения класса Thread . class NewThread extends Thread { NewThread ( ) { / / Создать новый, второй пото к . s uper ( n Demo Thread" ) ; System . out . println ( "Дoчepний поток : " + / / Это точка входа для второго потока . puЫic voi d run ( ) t ry { for ( in t i = 5 ; i > О ; i - - ) { System . out . println ( "Дoчepний поток : Thread . s leep ( 5 0 0 ) ; thi s ) ; " + i) ; catch ( Inter ruptedExcep tion е ) Sys tem . out . p rintln ( "Дочерний поток прерван . " ) ; Sys tem . out . println ( "Завершение дочернего потока . " ) ; class ExtendThread { puЫ ic static voi d main ( String [ ] args ) / / создать новый поток NewTh read nt = new NewThread ( ) ; / / запустит ь поток nt . s tart ( ) ; try { for ( i nt i = 5 ; i > О ; i - - ) { System . out . println ( "Главный пото к : " + i ) ; Thread . s leep ( l OO O ) ; catch ( I nterruptedException е ) { System . out . println ( "Главный поток прерван . " ) ; Sys tem . out . println ( "Завершение главного потока . " ) ; 306 Часть 1. Я з ы к Java Программа генерирует тот же самый результат, что и предыдущая вер­ сия. К ак видите, дочерний поток создается путем создания объекта класса NewThread, производного от Thread. Обратите вним ание на вызов super ( ) внутри конструктора NewThread, который приводит к вызову следующей формы конструктора Thread: puЫi c Thread ( String threadName ) В аргументе threadName указывается имя потока. В ы б ор подх ода На данном этапе вам может быть интересно, почему в Java есть два спо­ соба создания дочерних потоков и какой подход лучше. Ответы на упомяну­ тые вопросы касаются одного и того же. В классе Thread имеется несколь­ ко методов, которые могут б ыть переопределены в производном классе. Единственным методом, который должен быть переопределен, является run ( ) . К онечно, это тот же самый метод, требующийся при реализации ин­ терфейса RunnaЫe. Многие программисты на Java считают, что классы сле­ дует расширять только тогда, когда они каким-то образом совершенствуются или адаптируются. Таким образом, если вы не собираетесь переопределять какие-то другие методы Thre ad, то вероятно лучше просто реализовать RunnaЫe. Кроме того, благодаря реализации RunnaЫe ваш класс потока не придется наследовать от Thread, что позволяет наследовать его от другого класса. В конечном итоге только вам решать, какой подход использовать. Тем не менее, в оставшемся материале главы потоки будут создаваться с примене­ нием классов, реализующих интерфейс RunnaЫe. Создан ие множества потоков До сих пор использовались только два потока: rлавный поток и один до­ черний поток. Однако в программе можно создавать столько потоков, сколь­ ко необходимо. Например, в следующей программе создаются три дочерних потока: // Создание множества потоков . class NewThread imp lements RunnaЫe String name ; / / имя: потока Thread t ; NewThread ( Str i ng th readname ) name = threadname ; t = new Thread ( thi s , name ) ; System . out . println ( " Hoвый пот о к : " + t ) ; / / Это точка входа для: потока . puЫ i c void пш ( ) { try { for ( i nt i = 5 ; i > О ; i-- ) System . out . print l n ( name + " · " + i ) ; Гл ава 1 1 . М н оrоп ото ч н ое программирова ние 307 Thread . s leep ( l 0 0 0 ) ; catch ( I nterruptedExcep t i on е ) { System . out . println ( name + " прерван " ) ; System . out . printl n ( name + " завершен . " ) ; class Mul t i Th readDemo { puЫ i c static void main ( St r i n g [ ] args ) { new NewThread ( " One " ) ; NewThread n t l NewThread nt2 new NewThread ( " Two" ) ; NewThread ntЗ new NewThread ( " Three " ) ; / / Запустить потоки . nt l . t . start ( ) ; nt2 . t . start ( ) ; ntЗ . t . start ( ) ; try { / / Ожидать окончания осталь ных потоков . Thread . s l eep ( l 0 0 0 0 ) ; catch ( I nterruptedExcept ion е ) { System . out . printl n ( " Глaв ный поток прерван " ) ; System . out . println ( " Зaвepшeниe главного потока . " ) ; Ниже показан пример вывода, генерируемого программой. (Ваш вывод может отличаться в з ависимости от конкретной среды выполнения.) Новый поток : Thread [ One , 5 , ma i n ] Новый поток : Thread [ Two , 5 , ma i n ] Новый поток : Thread [ Three , 5 , ma i n ] One : 5 Two : 5 Three : 5 One : 4 Two : 4 Three : 4 One : 3 Three : 3 Two : 3 One : 2 Three : 2 Two : 2 One : 1 Three : 1 Two : 1 One завершен . Two завершен . Three завершен . Завершение главного потока . 308 Ч а сть 1. Язык Java Как види те, после запуска все три дочерних потока совместно используют ЦП. Обрати те внимание на вызов s l eep ( 1 0 0 0 0 ) в ma in ( ) , который при води т к засыпанию главного потока на период в десять секунд и гарантирует, что он заверши тся последним. Ис пол ьз о в а н ие isAlive ( ) и j oin ( ) Как уже упоминалось, часто требуется, что бы главный поток заканчивался последним. В предшествующих примерах это достигается вызовом s l eep ( ) в mai n ( ) с достато чно большой задержкой, что гарантирует завершени е всех доч ерних потоков до главного потока. Тем не менее, вряд ли такое решение можно считать удовлетворительным, и оно также поднимает более важный вопрос: как один по ток может узнать, когда друго й поток закончился? К сча­ ст ью, класс Thread предлагает средства, с помощью которых можно ответить на данный во прос. Существуют два спосо ба выяснения, завершен ли поток. Во-первых, мож­ но вызват ь на потоке метод isAl i ve ( ) , который определен в классе Thread со следующей о бщей формо й: final boolean isAlive ( ) Метод i sAli ve ( ) возвращает true, если поток, на ко тором он был вызван, все еще выполняется, или false в про тивном случае. Хо тя метод i sAl ive ( ) иногда быва ет по лезным, для ожидания заверше­ ния потока вы чаще всего будете применять метод по имени j oin ( ) : final void j oin ( ) throws InterruptedException Этот метод ожидает завершения потока, на котором он выз ыва ется. Его имя происходит от концепции вызывающего потока, ожидающего до тех пор, пока указанный поток не присоединится к нему. До полнительные формы ме­ тода j o i n ( ) позволяют указывать макси мальное время ожидания заверше­ ния указанного потока. Вот улучшенная версия предыдущего примера, где метод j o i n ( ) исполь­ зуется для того, чтобы основной поток останавливался последним. Вдо бавок демонстрируется применение метода i sAl i ve ( ) . / / Использование j oi n ( ) для ожидани я окончания потоков. class NewThread implements RunnaЫe { String name ; !! имя по т ока Thread t ; NewThread ( String threadname ) name = threadname ; t = new Thread ( thi s , name ) ; System . out . print ln ( "Hoвый поток: " + t ) ; // Это точка входа для потока . puЬlic void run ( ) { try { for ( int i = 5 ; i > О ; i-- ) { Глава 1 1 . М но го п от оч ное п ро грамм и рован и е 309 Systern . out . println ( name + " : " + i ) ; Thread . s leep ( l 0 0 0 ) ; catch ( Inte rruptedExcept ion е ) { System . out . printl n ( narne + " пре рван . " ) ; Systern . out . println ( name + " завершен . " ) ; class DemoJoin { puЫ ic static voi d ma i n ( St r i ng [ ] a rgs ) { NewThread ntl new NewThread ( " One" ) ; NewThread nt2 new NewTh read ( "Two" ) ; NewT hread nt3 new NewThread ( " Three " ) ; / / Запустить потоки . ntl . t . start ( ) ; nt2 . t . s ta rt ( ) ; ntЗ . t . s tar t ( ) ; System . out . println ( "Пoтoк One работает : " + n t l . t . isAl ive ( ) ) ; S ystem . out . println ( " Поток Two работает : " + nt2 . t . is Al ive ( ) ) ; S ystem . out . println ( "Пoтoк Three работает : " + ntЗ . t . isAl ive ( ) ) ; / / Ожидать завершения потоков . t ry { System . out . println ( "Oжидaниe за вершения потоков . " ) ; ntl . t . j oin ( ) ; nt2 . t . j oi n ( ) ; nt3 . t . j oi n ( ) ; catch ( I nte rruptedException е ) { System . out . println ( "Глaвный поток прерван " ) ; System . out . println ( " Поток One работает : " + n tl . t . is Al ive ( ) ) ; Systern . out . println ( " Поток Two работает : " + nt2 . t . isAlive ( ) ) ; System . out . pr intln ( "Поток Th ree работает : " + ntЗ . t . isAl ive ( ) ) ; System . out . println ( " Зaвepшeниe гла вного потока . " ) ; Ниже показан пример вывода, генерируемого про граммой. (Ваш вывод может отличаться в зависимости от конкретной среды выполнения.) Новый Новый Новый Поток Поток Поток пото к : Th read [ One , 5 , ma i n ] пото к : Thread [ Two , 5 , ma i n ] пото к : Thread [ Three , 5 , ma i n ] One работает : true Two работает : t rue Three работает : true 31 О Ч а сть 1. Язык Java Ожидание зав ершения потоков . Опе : 5 Two : 5 Three : 5 Опе : 4 Two : 4 Three : 4 One : 3 Two : 3 Three : 3 One : 2 Two : 2 Three : 2 One : 1 Two : 1 Three : 1 Two з аверше н . Three завершен . One завершен . Поток One работает : false Поток Two работае т : false Поток Three работает : false Заверше ние главного потока . Как видите, после возврата управления из вызовов j oin ( ) потоки остано­ вили свое выполнение. П риоритеты п отоков Приоритеты потоков используются планировщиком потоков для приня­ тия решения о том, когда следует разрешить выполнение каждого потока. Теоретически в течение заданного периода времени потоки с более высо­ ким приоритетом получают больше времени ЦП, чем потоки с более низким приоритетом. На практике количество времени ЦП, которое получает поток, часто зависит от нескол ьких факторов помимо ero приоритета. (Например, способ реализации многозадачности в ОС может повлиять на относительную доступность времени ЦП.) Поток с более высоким приоритетом может также вытеснять поток с более низким приоритетом. Скажем, когда выполняется поток с более низким приоритетом и возобновляется выполнение потока с более высоким приоритетом (например, происходит его выход из режима сна или ожидания ввода-вывода), он вытесняет поток с более низким приорите­ том. Теоретически потоки с одинаковыми приоритетами должны иметь равный доступ к процессору. Но вам нужно проявлять осторожность. Помните, что язык Java предназначен для работы в широком диапазоне сред. Некоторые из таких сред реализуют многозадачность принципиально иначе, чем другие. В целях безопасности потоки с одинаковыми приоритетами должны время от времени уступать управление. Подход гарантирует, что все потоки смогут работать в ОС без вытеснения. На практике даже в средах без вытеснения Гл а в а 1 1 . М н о гопото ч ное про граммирование 31 1 бол ьшинство потоков по-прежнему имеют воз можность запускаться, по­ скольку большинство потоков неиз бежно стал киваются с некоторыми блоки­ рующими ситуациями, такими как ожидание ввода-вывода. Когда возникает подобная ситуация, з аблокированный поток приостанавл ивается, и другие потоки могут выполняться. Но есл и вы хотите плавного мноrопоточноrо вы­ полнения, тогда вам лучше не полагаться на это. Кроме того, некоторые типы задач сил ьно загружают ЦП. Потоки такого рода всецело поглощают ЦП. Для потоков этих типов понадобится время от времени передавать управление, чтобы могл и выполняться другие потоки. Для установки приоритета потока применяется метод setPriority () , ко­ торый является членом класса Thread. Вот его общая форма: final void setPriority ( int leve l ) В аргументе level указывается новая настройка приоритета для вы­ з ывающего потока. Значение l evel дол жно наход иться в диапазоне от MIN_ PRIORITY до МАХ_ PRIORITY. В настоящее время эти з начения равны 1 и 1 О соответственно. Чтобы вернуть потоку стандартный приоритет, необхо­ димо указать значение NORМ_PRIORITY, которое в настоящее время равно 5. Упомянутые приоритеты определены как стати ческие финальные перемен­ ные внутри Thread. Пол учить текущую настройку приоритета можно вызовом метода get P riority () класса Thread: f i na l int getPriori ty ( ) Что касается диспет чериз ации потоков, то реализ ации Java могут вести себя совершенно по-разному. Большинство несоответствий возникает при на­ л и чии потоков, которые полагаются на вытесняющее поведение вместо того, чтобы совместно уступать ЦП. Самый безопасный способ добиться предска­ зуемого межплатформенноrо поведения Java предусматривает использование потоков, которые добровольно уступают управление ЦП. Си нхрон иза ция Когда двум или большему числу потоков требуется доступ к общему ресур­ су, нужно каким-нибудь способом гарантировать, что ресурс будет эксплуати­ роваться только одним потоком в каждый момент времени. Процесс, с помо­ щью которого достигается такая цель, называется синхронизацией. Вы увидите, что поддержка синхронизации в Java обеспечивается на уровне языка. Кл ю чом к синхронизации является концепция монитора. Монитор пред­ ставляет собой объект, который применяется в качестве вз аимоисклю чающей блокировки. В з аданный момент времени владеть монитором может только один поток. Когда поток получает блокировку, то говорят, что он входит в монитор. Все другие потоки, пытающиеся войти в з аблокированный монитор, будут приостановлены до тех пор, пока первый поток не выйдет из монитора. Говорят, что эти другие потоки ожидают монитор. Поток, владеющий мони­ тором, может при желании повторно войти в тот же самый монитор. 312 Часть 1. Язык Java Синхронизировать код можно одним из двух способов, предполагающих использование ключевого слова s ynchroni zed. Оба способа рассматривают­ ся далее. Использование синхронизиров анных методов Синхронизацию в Java обеспечить легко, п отому что все объекты и меют собственные связанные с ними неявные мониторы. Чтобы войти в монитор объекта, понадобится лишь вызвать метод, модифицированный с помощью ключевого слова synchroni z ed. Пока поток находится внутри синхронизи­ рованного метода, все другие потоки, п ытающиеся вызвать его (или любой другой синхронизированный метод) на том же экземпляре, должны ожидать. Чтобы выйти из монитора и передать управление объектом следующему ожи­ дающему потоку, владелец монитора просто возвращает управление из син­ хронизированного метода. Для понимания потребности в синхронизации давайте начнем с неслож­ ного примера, в котором она не применяется, хотя и должна. В следующей программе определены три п ростых класса. Первый, Cal lme ( ) , имеет един­ ственный метод c a l l ( ) , принимающий строковый п араметр по имени ms g. Данный метод п ытается вывести строку msg внутри квадратных скобок. Интересно отметить, что в методе cal l ( ) после вывода открывающей скобки и строки ms g производится вызов Thread . s leep ( l O O O ) , который приоста­ навливает текущий поток на одну секунду. Конструктор показанного ниже класса C a l l e r получает в аргументах target и msg ссылку на экземпляр класса Cal lme и строку. Конструктор так­ же создает новый поток, который будет вызывать метод run ( ) этого объекта. Метод run ( ) класса Caller вызывает метод call ( ) экземпляра target клас­ са Cal lme, п ередавая строку msg. Наконец, класс Synch начинает с создания одного экземпляра Cal lme и трех экземпляров Caller, каждый с уникальной строкой сообщения. Каждому экземпляру C a l l e r п ередается тот же самый экземпляр Cal lme. // Эта программа не синхронизирована . class Cal lme { void cal l ( String msg ) { Sys tem . out . print ( " [ " + msg ) ; try { Thread . s leep ( l OO O ) ; } catch ( Inte rruptedException е ) System . out . print l n ( " Пpepвaн " ) ; Sys tem . out . println ( " ] " ) ; class Caller implements RunnaЫe { String msg ; Cal lme targe t ; Thread t ; Глава 1 1 . Многопоточное программирование 31 3 puЫ ic Cal ler ( Ca l lme targ, String s ) { target = targ ; msg = s ; t = new Thread ( this ) ; puЫ ic void run ( ) { targe t . call (msg ) ; class S ynch { puЫ ic static void main ( S t ring [ ] args ) { Cal lme target = new Ca llme ( ) ; Cal l e r оЫ new Cal l e r ( targe t , "He l l o " ) ; Cal ler оЬ2 = new Cal ler ( targe t , "Synchroni zed" ) ; Cal l e r оЬЗ = new Caller ( target , "World" ) ; / / Запустить потоки . oЫ . t . start ( ) ; ob2 . t . start ( ) ; ob3 . t . s tart ( ) ; / / Ожидать оконч а ния р а боты потоков . try { оЫ . t . j oin ( ) ; оЬ2 . t . j oin ( ) ; оЬЗ . t . j oin ( ) ; catch ( I nterruptedException е ) { S ystem . out . println ( "Пpepвaн " ) ; Вот вывод, генерируемый программой: [Hel l o [ S ynchronized [World] ] ] Как видите, з а счет вызова sleep ( ) метод ca l l ( ) позволяет переключить­ ся на другой поток, что приводит к смешанному выводу трех строк сообще­ ний. В программе нет ничего такого, что помешало бы всем трем потокам одновременно вызывать один и тот же метод для того же самого объекта. Ситуация известна как состояние гонок, поскольку три потока сопернича­ ют друг с другом з а з авершение метода. В приведенном примере использу­ ется метод s leep ( ) , чтобы сделать эффекты повторяемыми и очевидными. В бол ьшинстве ситуаций состояние гонок является более тонким и менее предсказуемым, т.к. трудно предугадать, когда произ ойдет переключение контекста. В итоге программа один раз может выполниться правильно, а дру­ гой - неправильно. Для исправления предыдущей программы потребуется сериализироват ь доступ к ca l l () , т.е. ограничить его доступ только к одному потоку з а раз. Для этого просто нужно указать перед определением cal l () ключевое слово synchronized: 314 Часть l. Язык Jаvа class Callme { s ynchroni zed vo id call ( String ms g ) Ключевое слово s ynchronized н е позволяет остальным потокам входить в м етод cal l ( ) , пока он ис п ользуется другим потоком. После добавления synchroni zed к определению call ( ) вывод будет следующим: [ He l l o ] [ S ynchroni zed] [ World] При наличии метода или группы методов, которые манипулируют вну­ тренним состоя нием объекта в мноrопоточ ном сценарии, должно приме­ няться ключевое слово synchroni zed, чтобы защитить состоя ние от условий гонок. Помните, что как только поток входит в какой - либо синхронизирован­ ный м етод в экземпляре, никакой другой поток не может входить в любой другой синхронизированный метод в том же экземпляре. Однако несинхро­ низированные м етоды этого экземпляра по-прежнему доступны для вызова. Оператор synchronized Несмотря на то что определение синхронизированных методов внутри создаваемых классов является простым и эфф ективным ср едством обеспе­ чения синхронизации, оно не будет работать во всех случаях. Чтобы понять причину, рассмотрим следующую ситуацию. Представьте, что вы хотите син­ хронизировать доступ к объектам класса, не предназначенного для мноrопо­ точного доступа, т.е. синхронизированные методы в классе не используются. Кроме того, данный класс создан не вами, а третьей стороной, и у вас нет доступа к исходному коду. Таким образом, вы не можете добавить ключевое слово s ynchron i zed к соответствующим методам внутри класса. Как син­ хронизировать доступ к объект у такого класса? К счастью, решить проблему довольно легко: вы просто помещаете вызовы методов, определенных этим классом , внутрь блока synchroni zed. Ниже показана общая форма оператора synchronized: synchroni zed ( ob j Re f ) { / / операторы, подлежащие синхронизации Здесь obj Ref - это ссылка на синхронизируемый объект. Блок synchronized гарантирует, что вызов синхронизированного метода, который является чле­ ном класса obj Ref, произойдет только после того, как текущий поток успеш­ но войдет в монитор obj Re f. Далее представлена альтер нативн ая версия предыдущего примера , где внутри метода run ( ) применяется блок synchronized: // В этой программе используется блок s ynchroni zed . class Ca l lme { void ca l l ( String ms g) { System . out . print ( " [ " + ms g ) ; Глава 1 1 . М н оrо п оточ ное программ и рова ние 31 5 try ( Thread . s leep ( l O O O ) ; catch ( I nterruptedException е ) System . out . println ( "Пpepвaн " ) ; System . out . println ( " ] " ) ; c l a s s Cal l e r implements RunnaЫe { String msg ; Cal lme target ; Th read t ; puЬl ic Caller ( Callme tar g , St ring s ) ( ta rget = targ ; msg = s ; t = new Th read ( th i s ) ; / / Синхронизированные вызовы ca l l ( ) . p uЫ i c void run ( ) { / / блок s ynch roni zed s ynchronized ( targe t ) ta rget . call (msg ) ; class S ynchl ( puЫic s tatic void main ( St ring [ ] Cal lme target = new Cal lme ( ) ; Ca ller оЫ = new Cal ler ( targe t , Ca ller оЬ2 = new Ca l le r ( ta rget , Ca ller оЬЗ = new Caller ( targe t , / / Запустить потоки . oЫ . t . s t a rt ( ) ; ob2 . t . start ( ) ; obЗ . t . start ( ) ; a rgs ) ( "Hello" ) ; " S ynch roni zed " ) ; "World" ) ; / / Ожидать о кончания работы потоко в . t ry ( оЫ . t . j oin ( ) ; ob2 . t . j o in ( ) ; оЬЗ . t . j oin ( ) ; } catch ( I nterruptedException е ) { System . out . p rintln ( " Пpepвaн " ) ; Метод cal l ( ) не модифицируется с помощью synchron i z ed. Взамен вну­ три метода run ( ) кл асса Caller используется оператор synchroni zed, что приводит к такому же корректному выводу, как и в предыдущем примере, поскол ьку каждый поток ожидает з авершения работы предыдущего потока, прежде чем продолжить свое выпол нение. 316 Ч а сть 1 . Язык Java Вза и модей с твие м е жду потокам и В предшествующих примерах выполнялась безусловная блокировка асин­ хронного доступа к определенным методам для других потоков. Такое при­ менение неявных мониторов в объектах Java дает мощный эффект, но вы мо­ же те достичь более тонкого уровня контроля посредством взаимодействия между процессами. Как вы увиди те, делать это в Java особ енно легко. Как обсуждалось ранее, мно гопоточность заменяет программирование с цикло м событий, разделяя ваши задачи на дискретные логические единицы. Потоки т акже об еспечивают дополни тельное преи мущество: они устраняю о прос. Опрос обычно реализуется в виде цикла, который используется для повторяющейся проверки некоторого условия. Как только условие станови т­ ся истинным, инициируется соответствующее действие, что приводи т к не­ нужному расходу времени ЦП. Например, рассмотрим класси ческую задачу организации очереди, ко гда один поток производи т некоторые данные, а дру­ гой поток их по требляет. Чтобы сделат ь задачу более инт ересно й, предполо­ жим, что производителю нужно дождаться завершения работы потребителя, прежде чем он сгенерирует до полни т ельные данные. В системе опроса по­ требитель будет тратить много циклов ЦП в ожидании, пока производи тель начнет генерацию. После того как производитель закончил рабо ту, потреби­ т ель начинает о прос, понапрасну тратя еще больше циклов ЦП на ожидание завершения работы потребителя, и т.д. Ясно, что си туация подо бного рода нежелат ельна. В Java имеется элегантный механизм взаимодействия между процессами с помощью методов wai t ( ) , not i fy ( ) и noti fyA l l ( ) , который позволяет избежать о проса. Указанные методы реали зованы как финальные в классе Obj ect, поэтому они есть во всех классах. Все три метода можно вызывать только из синхронизированного конт екста. Хо тя они концепт уально сложны с вычислит ельной точки зрения, правила их применения в действительности довольно просты. • Метод wai t ( ) соо бщает вызыва ющему потоку о необходимости уст у­ пи ть мони тор и перейти в спящий режим, пока какой-то другой поток не войдет в тот же монитор и не вызовет noti fy ( ) или not i fyAll ( ) . • Метод noti fy ( ) пробуждает поток, который вызвал wai t ( ) на том же самом объекте. • Ме тод noti f yAl l ( ) пробуждает все потоки, ко торые вызвали wai t ( ) на том же самом объекте. Одно му из этих потоков будет предоставлен доступ. Методы объявлены в классе Obj ect, как показано ниже: final void wait ( ) throws InterruptedException final void noti fy ( ) final voi d not ifyAll ( ) Глава 1 1 . Мно гопоточное п рограммирова н ие 317 Существуют дополнительные формы метода wa i t ( ) , которые позволяют указывать период времени для ожидания. Прежде чем приступить к рассмотрению примера, иллюстрирующего вза­ имодействие между потоками, необходимо сделать одно важное замечание. Хотя метод wa i t ( ) обычно ожидает, пока не будет вызван метод not i fy ( ) или n o t i fyAl l ( ) , существует вероятность того, что в очень редких слу­ чаях ожидающий поток может быть разбужен из-за ложного пробуждения. В этом случае ожидающий поток возобновляется без вызовов not i f y ( ) или not i f yAll ( ) . (В сущности, поток возобновляется без видимой причины.) Из­ за такой маловероятной возможности в документации по Java API рекомен­ дуется, чтобы вызовы wa i t ( ) выполнялись в цикле, проверяющем условие ожидания потока. Прием демонстрируется в следующем примере. А теперь рассмотрим пример, в котором используются методы wa i t ( ) и noti f y ( ) . Для начала рассмотрим приведенный далее пример программы, в которой неправильно реализована простая форма задачи с производителем и потребителем. Реализация состоит из четырех классов: Q - очередь, кото­ рую вы пытаетесь синхронизировать; Producer - потоковый объект, созда­ ющий записи в очереди; Consurner - потоковый объект, потребляющий за­ писи очереди; РС - крошечный класс, который создает объекты Q, P roducer и Consumer. // Некорре ктная реализация производителя и потребител я . class Q { int n ; s ynchronized i n t get ( ) { System . out . println ( " Пoлyчeнo : " + n ) ; return n ; synchroni z ed void put ( i nt n ) { thi s . n = n ; System . out . println ( " Oтпpaвлeнo : " + n ) ; c l a s s Producer implements RunnaЬle { Q q; T hread t ; Producer ( Q q ) thi s . q = q; t = new Thread ( thi s , " Производитель " ) ; puЫ i c void run ( ) int i = О ; wh i le ( t rue ) { q . put ( i+ + ) ; 318 Ч асть l . Язы к Jаvа class Consume r implements RunnaЫe { Q q; Thread t ; Consumer ( Q q ) th i s . q = q; t = new Thread ( thi s , " Потребитель " ) ; puЫ i c void run ( ) wh ile ( t rue ) { q . get ( ) ; class РС { puЫ i c static void main ( S tr ing [ ] args ) { Q q = new Q ( ) ; Producer р = new Producer ( q ) ; Cons umer с = new Consumer ( q ) ; / / Запустить потоки . p . t . s tart ( ) ; c . t . start ( ) ; System . out . println ( " Haжмитe <Ctrl-C> , чтобы остановить про грамму . " ) ; Несмотря на то что методы put ( ) и get ( ) в кл ассе Q синхронизированы, ничто не помешает производителю переполнить потребителя, равно как ни­ что не мешает потребителю дважды использовать одно и то же значение из очереди. Таким образом, вы пол учите ошибочный вывод, показанный ниже (точный вывод будет зависеть от быстродействия ЦП и рабочей нагрузки): Отправлено : Получено : 1 Получено : 1 Получено : 1 Получено : 1 Получено : 1 Отправлено : Отправлено : Отправлено : Отправлено : Отправлено : Отправлено : Получено : 7 1 2 3 4 5 6 7 Как видите, после отправки производителем значения 1 потребитель за­ пустился и пол учил одно и то же значение 1 пять раз подряд. Затем произво­ дитель возобновил работу и отправил значения от 2 до 7, не дав потребителю возможности их пол учить. Чтобы корректно написать эту программу на Java, необходимо применять методы wai t ( ) и notify ( ) для передачи сигналов в обоих направлениях: Гл ава 1 1 . М н ого п оточное п р ограмми рова ние 319 / / Корректная реализация производителя и потребителя . class Q { int n ; boolean valueSet = fal s e ; sync!нoni z ed i n t g e t ( ) while ( 1 va l ueSe t ) try { wait ( ) ; } catch ( InterruptedException е ) { System . out . println ( " Пepexвaчeнo исключение InterruptedException " ) ; } System . out . println ( " Пoлyчeнo : " + n ) ; va lueSet = fal s e ; noti f y ( ) ; return n ; s ynchronized void p u t ( i nt n ) { whi le ( va lueSe t ) t ry { wai t ( ) ; } catch ( Inter ruptedException е ) { System . out . println ( "Перехвачено исключение I nter ruptedException" ) ; } thi s . n = n ; va l ueSet = t r u e ; System . out . println ( " Oтnpaвлeнo : " + n ) ; not i fy ( ) ; c la s s Producer implements RunnaЫe Q q; Thread t ; Producer ( Q q ) { thi s . q = q ; t = new Thread ( th i s , " Производитель " ) ; p uЫ i c void run ( ) int i = О ; whi le ( tr ue ) { q . put ( i + + ) ; clas s Consumer implements RunnaЫe { Q q; Thread t ; Cons umer ( Q q ) { thi s . q = q ; t = new Th read ( th i s , " Потребитель " ) ; 320 Часть 1. Язык Java puЫ i c void run ( ) while ( true ) { q . get ( ) ; class PCFixed { puЫ i c static void ma in ( Str ing [ ] a rgs ) { Q q = new Q ( ) ; Producer р = new Producer ( q ) ; Cons umer с = new Consumer ( q ) ; / / Запустить потоки . p . t . s tart ( ) ; c . t . start ( ) ; System . out . println ( " Haжмитe <Ctrl -C> , чтобы остановить программу . " ) ; Внутри метода get ( ) вызывается wai t ( ) , обеспечивая приостановку его выполнения до тех пор, пока объект Producer не уведомит вас о готовно­ сти данных. Korда это произойдет, выполнение внутри get ( ) возобновится. После получения данных метод get ( ) вызывает not i fy ( ) , сообщая объек­ ту Producer о том, что в очередь можно поместить дополнительные данные. Внутри метода put ( ) функция wai t ( ) приостанавливает выполнение до тех пор, пока объект Consumer не удалит элемент из очереди. Когда выполнение возобновляется, следующий элемент данных помещается в очередь и вызы­ вается noti fy ( ) , указывая объекту на то, что теперь он должен удалить его. Вот часть вывода, генерируемого программой, который наглядно демон­ стрирует чистое синхронное поведение: Отправлено : Получе но : 1 Отправлено : Получено : 2 Отправлено : Получено : 3 Отправлено : Получено : 4 Отправлено : Получено : 5 1 2 3 4 5 Взаимоблокировка Важно избегать особого вида ошибок, связанного именно с многозадачно­ стью и называемого взаимоблокировкой, которая возникает в ситуации, когда два потока имеют циклическую зависимость от пары синхронизированных объектов. Например, пусть один поток входит в монитор объекта Х, а другой поток - в монитор объекта У. Если поток в Х попытается вызвать какой-то синхронизированный метод на объекте У, то он вполне ожидаемо заблокиру­ ется. Тем не менее, если поток в У в свою очередь попытается вызвать какой- Глава 1 1 . Мн огопоточное программирование 321 либо синхронизированн ый метод на объекте Х, то он будет ожидать вечно, т.к. для доступа к Х ему пришлось бы освободить собственную блокировку на У, чтобы первый поток мог завершиться. Взаимоблокировка - трудная для отладки ошибка по двум причинам. • В целом взаимоблокировка случается очень редко, если два потока пра­ вильно распределяют время. • Она способна вовлечь более двух потоков и двух синхронизированных объектов. (То есть взаимоблокировка может возникнуть из-за более за­ путанной последовательности событий, чем только что описанная.) Для более полного понимания взаимоблокировки полезно взглянуть на нее в действии. В следующем примере создаются два класса, А и В, с методами foo ( ) и bar ( ) соответственно, которые ненадолго приостанавливаются перед попыткой вызвать метод другого класса. Главный класс по имени Deadlock создает экземпляры А и В, после чего вызывает метод deadl ockStart ( ) , что­ бы запустить второй поток, который настраивает условие взаимоблокировки. Методы foo ( ) и bar ( ) используют s l eep ( ) как способ вызвать состояние взаимоблокировк� // Пример возникновения взаимоблокировки . class А { synchroni zed void foo ( В Ь ) { String name = Thread . currentThread ( ) . getName ( ) ; Sys tem . out . println (name + " вошел в A . foo " ) ; try { Thread . s leep ( l O O O ) ; catch ( Exception е) { System. out . println ( "А прерван" ) ; System . out . println (name + " пытается вызвать B . las t ( ) " ) ; b . last ( ) ; synchroni zed void last ( ) { System . ou t . println ( "Bнyтpи A . last ( ) " ) ; class В { synchroni zed void bar (A а ) String name = Thread. currentThread ( ) . getName ( ) ; System . ou t . println (name + " вошел в B . bar " ) ; try { Threa d . sl eep ( l OO O ) ; catch ( Exception е ) { System. out . println ( " В прерван" ) ; System . out . println ( name + " пытается вызвать A . las t ( ) " ) ; а . last ( ) ; 322 Ч ас ть 1. Язык Java synchronized void last ( ) { Systern . out . println ( "Bнyтpи B . last ( ) " ) ; class Deadlock implements RunnaЫe { А а = new А ( ) ; В Ь = new В ( ) ; Thread t ; Deadlock ( ) { Th read . currentThread ( ) . s etName ( "MainThread" ) ; t = new Thread ( t h i s , " Rac ingThread" ) ; void deadloc kStart ( ) { t . start ( ) ; a . foo (b ) ; / / получить блокировку на а в этом потоке System . out .p rintln ( "Назад в глав ный поток" ) ; puЫic void run ( ) j b . bar ( a ) ; // получи ть блокировку на Ь в другом потоке System . out . println ( "Haзaд в другой поток" ) ; puЫic static void ma in ( String [ ] args ) { Deadl ock dl = new Deadlock ( ) ; dl . deadlockSta rt ( ) ; Запустив програ мму, вы увидите приведенный дал ее вывод, хотя какой ме­ тод будет запущен первым - А. foo ( ) или В . bar ( ) , - зависит от конкретной среды выполнения. MainThread вошел в А. foo RacingThread вошел в B . bar MainThread пытается вызвать В. last ( ) Rac ingTh read пытается вызвать A. last ( ) Поскольку в програ мме произошл а взаимоблокировка, для заверше­ ния программы понадобится нажать <Ctrl+C>. Нажав комбинацию клавиш <Ctrl+Break> на ПК, можно просмотреть пол ный дамп кеша потока и монито­ ра. Вы увидите, что RacingThread владеет монитором на Ь, пока он ожидает монитор на а. В то же время MainThread владеет а и ожидает получения Ь. Программа никогда не завершится. Как илл юстрирует данный пример, если ваша многопоточная программа иногда зависа ет, то взаимоблокировка явля­ ется одним из первых условий, которые вы должны проверить. П рио с та новка, в о з обновл ени е и о станов потоков Временами полезно приостанавл и вать выпол нение потока. Например, отдельный поток можно применять для отображени я времени суток. Есл и пользовател ю н е нужны часы, тогда его поток можно приостанов ить. В л ю­ бом случае приостанов ить поток довол ьно просто. После приостанов ки пере­ запустит ь поток тоже легко. Глава 1 1 . М н о г о п оточное п ро гр аммирован и е 323 Механиз мы приостановки, останова и воз обновления потоков различают­ ся между ранними версиями Java, такими как Java 1.0, и более современными версиями, начиная с Java 2. До выхода Java 2 в программе использ овал ись методы suspend () , resume () и stop ( ) , которые определены в классе Thread и предназ начены для приостановки, возобновления и останова выполнения потока. Хотя упомянутые методы кажутся разумным и удобным подходом к управлению выполнением потоков, они не должны применяться в новых программах на Java и вот по чему. Несколько лет наз ад в версии Java 2 метод suspend () класса Th read был объявлен нерекомендуемым. Так поступили из-за того, что suspend () мог иногда становиться причиной серьез ных си­ стемных отказов. Предположим, что поток з аблокировал крити чески важные структ уры данных. Есл и этот поток приостановить в данной то чке, то бло­ кировки не освободятся. Другие потоки, ожидающие такие ресурсы, могут попасть во вз аимоблокировку. Метод resume () тоже не рекомендуется использовать. Проблем он не вы­ зывает, но е го нельзя применять без дополняющего метода suspend () . Метод stop () класса Th read тоже объявлен нерекомендуемым в Java 2. При чина в том, что и ногда он мог приводить к серьез ным системным отка­ зам. Например, пусть поток выполняет з апись в крити чески важную структу­ ру данных и з авершил внесение только части нужных изменений. Если такой поток будет остановлен в этот момент, тогда структ ура данных может остать­ ся в поврежденном состоянии. Дело в том, что метод stop () выз ывает ос­ вобождение любой блокировки, удерживаемой выз ывающим потоком. Таким образом, поврежденные данные могут быть использованы другим потоком, ожидающим той же блокировки. Поскольку применять методы suspend ( ) , resume () или stop () для управ­ ления потоком теперь нельзя, вам может показ аться, что больше не существу­ ет какого-либо способа для приостановки, перез апуска или завершения рабо­ ты потока. К с частью, это не так. Вз амен поток должен быть спроектирован таким образом, чтобы метод run () периоди чески проверял, должен ли поток приостанавливать, возобновлять или полностью останавливать собственное выполнение. Как правило, з адача решается путем установления флаговой п е­ ременной, которая указ ывает состояние выполнения потока. Пока флаговая переменная установлена в состояние "работает'; метод run () должен продол­ жать работу, чтобы поз волить потоку выполняться. Если флаговая перемен­ ная установлена в состояние "приостановить'; то поток должен быть приоста­ новлен. Есл и она установлена в состояние "остановить'; тогда поток должен завершить работ у. Конечно, существует множество способов написания тако­ го кода, но главный принцип будет одинаковым во всех программах. // Приостановка и возобновление современным способом . class NewThread imp l ement s RunnaЫe { St riпg name ; / / имя потока Thread t ; boolean suspendFlag; NewThread ( S tring threadname ) name = threadname ; 324 Часть 1. Язык Java t = new Thread ( th i s , name ) ; S ystem . out . println ( " Hoвый пото к : " + t ) ; su spendFlag = fal s e ; / / Это точка входа для потока . puЫ i c vo i d run ( ) { try { for ( int i = 1 5 ; i > О ; i - - ) { S ystem . out . println ( name + " · " + i ) ; Th read . s leep ( 2 0 0 ) ; s ynchroni zed ( thi s ) { while ( su spendFlag ) wait ( ) ; catch ( I nterruptedException е ) { System . out . println ( name + " прерван . " ) ; System . out . println ( name + " за вершается . " ) ; s ynchroni zed voi d mysuspend ( ) su spendFlag = true ; s ynchroni zed void myresume ( ) su spendFlag = fal se ; not i fy ( ) ; class SuspendRe sume { puЬ l i c static void mai n ( String [ J args ) NewTh read оЫ = new NewThread ( " One " ) ; NewThread оЬ2 = new NewThread ( " Two" ) ; oЫ . t . start ( ) ; / / запустить поток ob2 . t . start ( ) ; // запустить поток t ry { Th read . s leep ( l 0 0 0 ) ; oЫ . mysuspend ( ) ; System . o ut . println ( " Пpиocтaнoвкa потока One " ) ; Th read . s leep ( l 0 0 0 ) ; оЫ . myresume ( ) ; System . out . println ( " Bo зoбнoвлeниe потока One " ) ; ob2 . mysuspend ( ) ; System . out . println ( " Пpиocтaнoвкa пот ока Two " ) ; Thread . s leep ( l 0 0 0 ) ; ob2 . myre sume ( ) ; System . o ut . println ( " Boзoбнoвлeниe потока Two " ) ; catch ( I nterruptedException е ) { System . out . println ( " Главный поток прерван " ) ; } / / Ожидать завершени я потоков . try { System . out . println ( " Oжидaниe завершения потоков . " ) ; oЫ . t . j oi n ( ) ; ob2 . t . j oi n ( ) ; Глава 1 1 . Мноrопоточное программирование 325 } catch ( InterruptedException е) { System . out . println ( "Главный поток прерван") ; System . out . println ( "Глaвный поток завершается . " ) ; Запустив программу, вы заметите, что потоки приостанавливают и воз­ обновляют свою работу. Далее в книге вы увидите больше примеров, rде используется современный механизм управления потоками. Хотя этот ме­ ханизм может показаться не настолько простым в nрименении, как старый, однако, он необходим для предотвращения возникновения ошибок во время выполнения. Он является подходом, который должен использоваться в любом новом коде. П ол у че н 1и е состояния потока Как упоминалось ранее в главе, поток может пребывать в разных состо­ яниях. Получить текущее состояние потока можно за счет вызова метода getState ( ) , определенного в классе Thread: Thread. State getState ( ) Метод getState ( ) возвращает значение типа Thread . State, отражающее состояние потока на момент выполнения вызова. Состояние представлено в виде перечисления, определенного в Thread. (Перечисление - это список именованных констант и подробно обсуждается в главе 12.) В табл. 11.2. опи­ саны значения, которые может возвращать метод getState ( ) . Та6nица 1 1 .2. Значения, возвращаемые методом getstate () BLOCКED Поток приостановил выполнение, потому что ожидает получения блокировки NEW RUNNAВLE Поток еще не начал выполнение TERМINATED TIMED WAITING WAITING Поток либо в текущий момент выполняется, либо будет выполняться, коrда получит доступ к ЦП Поток завершил выполнение Поток приостановил выполнение на указанный период времени, например, при вызове sleep ( ) . Поток переходит в это состояние также в елучае вызова версии wai t ( ) или j oin ( ) , принимающей значение тайм-аута Поток приостановил выполнение из-за тоrо, что ожидает возникновения некоторого действия. Например, он находится в состоянии WAIT ING по причине вызова версии wai t ( ) или j oin ( ) , не принимающей знач:ение тайм-аута 326 Часть 1. Язык Java На рис. 1 1.1 показаны взаимосвязи между различными состояниями потока. BLOCKED а- Ожидание блокировки ------ RUNNABLE Блокировка получена IIAITING или TIМED IIAITING TERMINATED Рис. 1 1 .1. Состояния потока Имея экземпляр Thread, вы можете применить метод getState ( ) для по­ лучения состояния потока. Скажем, в следующей кодовой последовательно­ сти выясняется, пребывает ли поток по имени thrd в состоянии RUNNAВLE в момент вызова getState ( ) : Thread . State tз = thrd. getState { ) ; if ( tз = Thread . State , RUNNAВLE) // Важно понимать, что после вызова getState ( ) состояние потока может измениться. Таким образом, в зависимости от обстоятельств состояние, по­ лученное при вызове метода getState ( ) , спустя всего лишь мгновение мо­ жет уже не отражать фактическое состояние потока. По этой и другим при­ чинам метод getState ( ) не предназначен быть средством синхронизации потоков. В основном он используется для отладки или профилирования ха­ рактеристик потока во время выполнения. Использ о вание ф абрич ных методов для создания и запус ка пото ка В ряде елучаев нет необходимости отделять создание потока от запуска его на выполнение. Другими словами, иногда удобно создать и запустить поток одновременно. Один из способов сделать предусматривает применение ста­ тического фабричного метода. Фабричный метод возвращает объект класса. Как правило, фабричные методы являются статическими методами класса. Гл ав а 1 1 . М но гопо т о чн ое про г р а ммирова н ие 327 Они используются по разным причинам, наприм ер, для установки объекта в начальное состояние перед работой с ним, для настройки определенного типа объекта или временами для обеспечения многократного использования объек­ та. Что касается создания и запуска потока, то фабричный метод создаст поток, вызовет метод sta rt ( ) на потоке и возвратит ссылку на поток. При таком подходе вы можете создавать и запускать поток с помощью одного вызова метода, тем самым упрощая свой код. Скажем, добавл ение в класс NewThread из приведенной в начале главы программы ThreadDemo показа нного дал ее фабричного метода позволит создавать и запускать поток за один шаг: // Фабричный метод, который соз дает и з а пускает п от о к . puЫic static NewThread createAndStart ( ) { NewThread myThrd = new NewTh read ( ) ; myThrd . t . start ( ) ; return myTh rd ; С применением метода createAndStart ( ) сл едующий фрагмент кода: NewTh read nt = new NewTh read ( ) ; // со здат ь новый п о т ок nt . t . start ( ) ; / / за пус тит ь п ото к можно заменить так: NewThread nt = NewTh read . createAndStart ( ) ; Теперь поток создается и запуска ется за один шаг. В ситуациях, когда хранить ссылку на исполняемый поток не нужно, иногда можно создать и запустить поток с помощью одной строки кода без исполь­ зования фабричного метода. Снова вернувшись к программе ThreadDemo, вот как создать и запустить поток NewThread: new NewThread ( ) . t . start ( ) ; Тем не менее, в реальных приложениях обычно требуется сохранять ссыл­ ку на поток, поэтому фабричный метод часто будет удач ным вариантом. Ис пол ьз о в а ние м ногопото ч но с ти Кл ючом к эфф ективному применению возможностей мноrопоточ ности Java является мышление в категориях паралл ельного, а не последователь ного выполнения. Скажем, если у вас есть две подсистемы в программе, которые могут выполняться одновр еменно, сделайте их отдель ными потоками. При осторожном использовании мноrопоточности вы можете создавать очень эф­ фек тивные программы. Однако имейте в виду, что если вы создадите слиш­ ком много потоков, то на самом дел е можете ухудшить производительность своей программы, а не улучшить ее. Помните, что с переключением контекста связаны некоторые накладные расходы. Если вы создадите слишком много потоков, то большая часть вр емени ЦП будет расходоваться на изменение контекстов, а не на выполнение самой программы! И посл еднее замечание: для создания приложений с интенсивными вычисл ениями, которые могут ав­ томатически масштабироваться, чтобы задействовать доступные процессоры в многоядерной системе, рассмотрите возможность примен ения инфраструк­ туры Forkl]oin Framework, которая описана в главе 29. ГЛ АВА 12 Пе р еч исл ен и я , а втоуп аковк а и а нн отации В настоящей главе рассматриваются три средства, которые изначально не были частью языка Java, но со временем каждое из них стало почти незаме­ нимым аспектом программирования на Java: перечисления, автоупаковка и аннотации. Первоначально добавленные в JDK 5, все они являются функци­ ональными возможностями, на которые стали полагаться программисты на Java, т.к. предлагают упрощенный подход к решению общих программных за­ дач. В главе также обсуждаются оболочки типов Java и дано введение в реф­ лексию. Перечисления В своей простейшей форме перечисление представляет собой список име­ нованных констант, который определяет новый тип данных и ero допусти­ мые значения. Таким образом, объект перечисления может содержать только значения, которые были объявлены в списке. Другие значения не допускают­ ся. Иными словами, перечисление дает возможность явно указывать един­ ственные значения, которые может законно иметь тип данных. Перечисления обычно используются для определения набора значений, представляющих коллекцию элементов. Например, вы можете применять перечисление для представления кодов ошибок, возникающих в результате выполнения какой­ либо операции, таких как успех, О'l'каз или ожидание, или списка состояний, в которых может пребывать устройство, например, рабочее, остановленное или приостановленное. В ранних версиях Java такие значения определялись с помощью переменных final, но перечисления предлагают гораздо более совершенный подход. Хотя на первый взгляд перечисления в Java могут показаться похожими на перечисления в других языках, это сходство окажется поверхностным, по­ скольку в Java перечисле1-1ие определяется как тип класса. За счет превраще­ ния перечислений в классы возможности перечисления значительно расши­ ряются. Скажем, в Java перечисление может иметь конструкторы, методы и переменные экземпляра. Из-за своей мощи и rнбкости перечисления широко используются во всей библиотеке Java АР!. Глава 1 2 . Перечисле н ия, автоупаковка и аннотации 329 Основы перечислений Перечисление создается с применением ключевого слова enurn. Например, вот простое перечисление, в котором определен перечень различных сортов яблок: // Перечисление сортов яблок . enum Apple { Jonathan, GoldenDe l , RedDe l , Winesap , Cortland Идентификаторы Jonathan, Go ldenDe l и т.д. называются константами перецuсления. Каждая из них неявно объявляется как открытый стат ический финальный член App le. Более того, их типом является тип перечисления, в котором они объявлены, в данном случае - App le. Так и м образом, в языке Java такие константы называются самотипизированными, где "само-" относит­ ся к объемлющему перечислению. После того как перечисление определено, можно создать переменн ую это­ го типа. Однако, несмотря на то, что перечисления определяют тип класса, экземпляр перечисления не создается с помощью new. Взамен переменная перечисления объявляется и используется почти так же, как переменная од­ ного из примитивных типов. Например, ниже переменная ар объявляется как принадлежащая типу перечисления App le: App l e ар ; Поскольку ар относится к типу Apple, единственные значения, которые ей можно присваивать (или она способна содержать), определяются перечисле­ нием. Скажем, следующий оператор присваивает ар значение RedDel: ар = Appl e . RedDe l ; Обратите внимание, что символ RedDel предваряется типом App le. Две константы перечисления можно сравнить на предмет равенства с при­ менением операции отношения ==. Напри мер, показанный далее оператор сравнивает значение в ар с константой Go ldenDel: i f ( ap == Apple . GoldenDe l ) // . . . Значение перечисления также можно использовать для управления опе­ ратором switch. Разумеется, во всех операторах case должны быть указа­ ны константы из того же самого перечисления, что и в выражении switch. Например, приведенный н иже оператор switch совершенно допустим: // Использование перечисления для упра вления опера тором switch . switch ( ap ) { case Jonathan : // ... // ... case Winesap : Обратите внимание, что в операторах case и мена констант перечисления применяются без уточнения с помощью и мени их типа перечисления, т.е. ис­ пользуется Winesap, а не App le . Winesap. Причина в том, что тип перечис- 330 Часть 1. Яз ык Java ления в выражении swi tch уже неявно з адает тип перечисления констант в c a s e, так что нет никакой необходимости уточнять константы в операторах c a s e посредством имени типа перечисления. На самом деле попытка сделать это вызовет ошибку на этапе компиляци и. При отображении константы перечисления, с к а жем, в операторе println ( ) , выводится ее имя. Например, в рез ультате выполнения следую­ щего оператора отобразится имя Winesap: S ystem . ou t . printl n ( Appl e . Winesap ) ; В показанной ниже программе объеди нены все рассмотренные ранее фраг­ менты кода с целью демонстрации работы с перечислением Apple: // Перечисление сортов ябло к . enum Appl e { Jonatha n , GoldenDel , RedDel , Wine sap , Cortl and class EnumDemo { puЫ i c stati c void main ( String [ ] a rgs ) { App l e ар ; ар = App le . RedDe l ; / / Вывести значение перечисления . System . out . println ( "Знaчeниe ар : " + ар ) ; System . out . p r intln ( ) ; ар = Apple . GoldenDel ; / / Сравнить два значения перечисления . i f ( ap == Apple . GoldenDe l ) System . out . println ( " ap содержит GoldenDel . \n " ) ; / / Использовать перечисление для управлени я оператором switch . switch ( ap ) { case Jonatha n : S ys tem . out . println ( "Яблoки сорта Джонатан ( Jonatha n ) имеют красный цвет . " ) ; bre a k ; c a s e GoldenDel : System . out . println ( "Яблoки сорта Голден делишес ( Go lden De licious ) имеют желтый цвет . " ) ; bre a k ; c a s e RedDel : System . out . println ( "Яблoки сорта Ред делишес ( Red De l i c i ous ) имеют красный цвет . " ) ; brea k ; case Winesap : System . out . print l n ( "Яблoки сорта Вайнсап ( Wi nesap ) имеют красный цве т . " ) ; b re a k ; c a s e Cortl and : System . out . println ( "Яблoки сорта Кортланд ( C o rt l and) имеют красный цве т . " ) ; break; Глава 1 2 . Перечисления, автоу паковка и аннотации 331 Вот вывод, генерируемый программой: Значение ар : RedDel ар содержит Gol denDel . Яблоки сор та Голден делишес ( Golden Del i cious ) имеют желтый цвет . Методы values ( ) и valueOf ( ) Все пер ечисления автоматически содержат в себе два предопредел енных метода: va lues ( ) и valueOf ( ) со сл едующими общими формами: p uЬlic static enurn- t ype [ ] va l ues ( ) puЬlic static enum- type valueOf ( St ri ng s t r ) Метод va lues ( ) возвращает массив, содержащий список констант пе­ речисления. Метод valueOf ( ) возвращает константу перечисл ения, зна­ чение которой соответствует строке, переданной в аргум енте str. В обо­ их случаях в enum- type указывается тип данного п ереч исл ения. Скажем , для пр едставленного ранее перечисл ения App le возвращаемым типом Apple . valueOf ( "Winesap" ) будет Wine sap. В показанной далее программе илл юстрируется применение методов values ( ) и va lueOf ( ) : / / Ислоль зование в строенных методо в леречисления . / / Перечисление сор тов яблок . enurn App l e { Jonatha n , Go ldenDel , RedDe l , Winesap , Cortl and class EnumDemo2 { puЫ i c static void mai n ( St ring [ ] args ) { Apple ар ; System . out . println ( " Bce константы леречисления Apple : " ) ; / / Ислоль зовать values ( ) . Apple [ J al lapples = App l e . values ( ) ; for ( Apple а : a l l appl es ) System . o ut . println ( a ) ; S y s tem . o ut . print ln ( ) ; / / Ислоль зовать valueOf ( ) . ар = Apple . valueOf ( "Winesap " ) ; System . out . println ( " ap содержит " + а р ) ; Вывод, генерируемый программой, выглядит следующим образом: Все константы перечисления Apple : Jonathan GoldenDel RedDel Wine s ap Cort land ар содержит Winesap 332 Ч асть 1. Язык Java Обрат ите внимание, что для прохода по массиву констант, полученному вызовом values ( ) , в программе используется цикл for в стиле "for-each': В качестве примера была создана переменная a l lappl es, которой присваи­ вается ссылка на массив перечисления. Тем не менее, в таком шаге нет ника­ ко й необходимости, потому что цикл for можно было бы написать так, как показано ниже, устранив потребность в переменно й allapples: for (Appl e а : Apple . values ( ) ) System . o ut . p rintln ( a ) ; А теперь взгляните, как было полу чено значение, соответствующее имени Winesap, с помощью вызова va lueOf ( ) : ар = Appl e . val ueOf ( "Winesap " ) ; Как уже объяснялось, метод valueOf ( ) возвращает значение перечис­ ления, ко торое ассоциировано с именем константы, представленной в виде строки. П ереч ислени я Java я вл я ются т ипами класс о в Как уже упоминалось, перечисление Java относится к типу класса. Хотя вы не создаете экз емпляр перечисления с помо щью new, в остально м он о бла­ дает теми же воз можностями, что и другие классы. Тот факт, что enшn опре­ деляет класс, придает перечислению в Java необычайную силу. Например, вы можете предоставить ему конструкторы, до бавить переменные экземпляра и методы и даже реализоват ь инт ерфейсы. Важно понимать, что каждая константа перечисления являет ся объектом своего типа перечисления. Таки м образом, в слу чае определения конструк­ тора для перечисления конструктор будет вызываться при создании каждой константы перечисления. Кро ме того, каждая константа перечисления имеет собственну ю копию любых переменных экземпляра, определенных перечис­ лением. Рассмотрим в качеств е примера следу ющую версию перечисления Apple: // Ислользование конструктора леречисления , леременной экземлляра и метода . enum Apple { Jonathan ( l 0 ) , GoldenDel ( 9 ) , RedDel ( 1 2 ) , Wine sap ( l S ) , Cort l a nd ( B ) ; private int price ; // цена яблок каждого сорта ! / Конструктор . App le ( i nt р ) { price = р ; } int getPrice ( ) { return price ; class EnumDemoЗ { puЫic static vo id ma in ( S tring [ ] a rgs ) { Apple ар; / / Отобразить цену яблок сорта Wi nesap . System . out . println ( "Яблoки сорта Winesap стоят " + App l e . Winesap . getPrice ( ) + " центов . \ n " ) ; Глава 1 2 . Перечисления, автоупаковка и а ннота ции 333 // Отобразить все сорта яблок вместе с ценами . Sys tem . out . println ( " Цeны на все сорта яблок : " ) ; for (Apple а : Apple . values ( ) ) System . out . p r int ln ( "Яблo ки сорта " + а + " стоят " + a . get Pr ice ( ) + " центов . " ) ; Вот вывод: Яблоки сорта Winesap стоят 15 центов . Цены на все сорта яблок : Яблоки сор т а Jonathan стоят 1 0 центов . Яблоки сорта GoldenDel стоят 9 центов . Яблоки сорта RedDel стоят 12 центов . Яблоки сорта Winesap стоят 15 центов . Яблоки сор т а Cort l and стоят В центов . В эту версию перечисления Apple добавлены три компонента. Первый переменная экземпляра pr ice, которая применяется для хранения цены каж­ дого сорта яблок. Второй - конструктор Apple, которому передается цена сорта яблок. Третий - метод getPrice ( ) , возвращающий значение price. Когда переменная ар объявляется в main ( ) , конструктор Apple вызыва­ ется по одному разу для каждой заданной константы. Обрати те внимание на способ указания аргументов конструктора за счет их помещения в кру глые скобки после каждой константы: Jonathan ( l O ) , GoldenDel ( 9 ) , RedDel ( l 2 ) , Winesap ( l 5 ) , Cortl and ( B ) ; Эти значения передаются параметру р конструктора Apple ( ) , который за­ тем присваивает его переменной экземпляра price. Опят ь-таки конструктор вызывается один раз для каждой константы. Поскольку каждая константа перечисления имеет собственную копию pri ce, вы можете получить цену определенного сорта яблок, вызвав метод getPrice ( ) . Скажем, ниже показан вызов в методе main ( ) , предназначенный для получения цены яблок сорта Wines ap: Appl e . Winesap . getPrice ( ) Цены яблок всех сортов полу чаются пу тем прохода по перечислению с ис­ пользованием цикла for. Ввиду того, что для каждой констант ы перечисле­ ния существуе т копия price, значение, связанное с одной константой, будет отдельным от значения, связанного с другой константой. Это мощная кон­ цепция, которая дост упна только тогда , когда перечисления реализованы в виде классов, как сделано в Java. Хотя в предыдущем примере содержится только один конструктор, пере­ числение может предлагать две или более перегруженных формы, как и лю­ бой другой класс. Например, следу ющая версия перечисления Apple предо­ ставляет стандартный конструктор, который инициализирует цену значением - 1, указывая на то, что данные о цене недост упны: 334 Часть 1. Язык Java / / Исполь зование конструктора перечисления . enum Apple { Jonathan ( l O ) , GoldenDel ( 9 ) , RedDe l , Winesap ( l 5 ) , Cortland ( B ) ; priva t e i n t price; / / цена яблок каждого сорта // Конструктор . App le ( i nt р) { price = р; } / / Пере груженный конструкт ор . App le ( ) { price = - 1 ; ) int get P r i ce ( ) { return price ; Обратите внимание, что в этой версии RedDel не имеет аргумента. Таким образом, вызывается стандартны й конструктор и переменной pri ce экзем­ пляра RedDel присваивается значение -1. К п еречислениям применяются два ограничения. Во-первых, перечисл ение не может быть унаследовано от другого класса. Во-вторых, п еречисление не может служить суперклассом, т.е. перечисление расширять нельзя. В осталь­ ном перечисление действует так же, как любой другой тип кл асса. Главное помнить, что каждая из констант пере числения является объектом класса, в котором она определена. П е р еч исл е ния у насл едова н ы от Enum Несмотря на невозможность при объявлении пер ечисл ения насл едо­ вать его от суперкл асса, все перечисле ния автоматически унасл едованы от j ava . l ang . Enum. В этом классе определено несколько методов, доступных для использования всеми пер ечисл ениями. Класс Enum подробно описан в ча­ сти II, но три его метода заслуживают обсуждения прямо сейчас. Есть возможность получить значение, которое указывает позицию кон­ станты перечисления в списке констант. Оно называется порядковым номером и извлекается вызовом метода ordinal ( ) : final int ordinal ( ) Метод ordinal ( ) возвращает порядковый номер константы, на которой вызывается. Порядковые номера начинаются с нуля. Таким образом, в пере­ числ ении Appl e константа Jonathan имеет порядковый номер О, константа GoldenDel - порядковый номер 1, константа RedDel - порядковый номер 2 и т.д. Порядковые номера двух констант одного и того же пер ечисл ения можно сравнивать с применением метода compareTo ( ) . Он имеет сл едующую об­ щую форму: f i nal int compa reTo ( enurn-type е ) Здесь enum- type задает тип перечисления, а е представляет собой кон­ станту, сравниваемую с вызывающей константой. Не забывайте, что вызыва­ ющая константа и е должны относиться к одному и тому же п еречислению. Есл и вызывающая константа имеет порядковый номер меньше е, то метод Глава 1 2 . П ер еч ис ления, автоу па ков к а и аннота ц ии 335 compareTo ( ) возвращает отрицательное значение. Если два порядковых но­ мера совпадают, возвращается ноль. Если вызывающая константа имеет по­ рядковый номер больше е, тогда возвращается положительное значение. Константу перечисления можно сравнивать на предмет равенства с лю­ бым другим объектом, используя метод equals ( ) , которы й переопределяет метод equa ls ( ) , определенный в Obj ect. Несмотря на то что метод equals ( ) способен сравнивать константу перечисления с любым другим объектом, эти два объекта будут равны, только если они оба ссылаются на одну и ту же константу внутри того же самого перечисления. Простое наличие общих по­ рядковых номеров не приведет к тому, что equals ( ) возвратит true, если две константы принадлежат разным перечислениям. Вдобавок помните о возможности сравнивать на предмет равенства две ссылки на перечисления с применением операции ==. В приведенной далее программе демонстрируется использование методов ordinal ( ) , compareTo ( ) и equals ( ) : / / Демонстрация исполь зования методов ordinal ( ) , compareTo ( ) и equals ( ) . / / Перечисление сортов ябло к . enum Apple { Jonathan , GoldenDe l , RedDe l , W i nesap , Cort land class EnumDemo4 { puЫ ic s t a t i c void ma in ( String [ ] args ) { Apple ар , ар2 , арЗ ; / / Получить все порядковые номера с приме нением ordinal ( ) . System . out . pri ntln ( 11 Bce константы перечисления App l e " + 1 1 1 ) ; 1 вместе с их порядковыми номерами : for (Appl e а : Apple . values ( ) ) System . o ut . p rintln ( a + 11 11 + a . ordinal ( ) ) ; ар = Apple . RedDe l ; ар2 = Apple . GoldenDel ; арЗ = App l e . RedDe l ; System . o ut . println ( ) ; / / Демонстра ция испол ь зования compareTo ( ) и equals ( ) . i f ( ap . compareTo ( ap2 ) < О ) System . out . println ( ap + 1 1 находится перед 11 + ар2 ) ; i f ( ap . compa reTo ( ap2 ) > О ) System . out . print l n ( ap2 + 1 1 находится перед " + ар ) ; i f ( ap . compareTo ( apЗ ) == О ) System . o ut . print l n ( ap + 11 равно 11 + арЗ ) ; 11 + ар З ) ; System . out . pri n t l n ( ) ; i f ( ap . equals ( ap2 ) ) System . out . println ( "Oшибкa ! 11 ) i f ( ap . equa l s ( apЗ ) ) System . out . pr i n t l n ( ap + 11 ; ра вно 336 Ч ас т ь 1. Язык J ava i f ( ap == арЗ ) Systern . out . println ( ap + " " + арЗ ) ; Ниже показан вывод, генерируемый программой: Все константы перечисления Apple вместе с их порядковыми номерами : Jonathan О GoldenDel 1 RedDel 2 Winesap 3 Cortl and 4 Golde nDel находится перед RedDel RedDel равно RedDel RedDel равно RedDel RedDel == RedDel Еще од и н п р и м е р пе р е чис л е ния Прежде чем двигаться дальше, м ы рассмотрим еще один пример, в кото­ ром применяется перечисл ение. В главе 9 был а создана автоматизирован­ ная программа для принятия решений. В той версии переменные с именами NO (Нет), YES (Да), МАУВЕ {Возможно), LATER (Позже), SOON (Скоро) и NEVER (Никогда) были объявлены внутри интерфейса и использовались для пред­ ставления возможных ответов. Хотя формально в таком подходе нет ничего ошибочного, перечисление будет более удачным выбором. Вот улучшенная версия программы, в которой для определения ответов применяется пере­ числение по имени A nswers. Сравните данную версию с первоначальной в главе 9. / / Улучшенная версия " системы приня тия решений " из главы 9 . / / В э той версии для предста вления ответов испол ь зуется // перечисление , а не переменные интерфейса . irnport j ava . util . Randorn; / / Перечисление возможных ответов . enurn An swers ( NO, YE S , МАУВЕ, LATER, SOON , NEVER class Question { Randorn rand = new Random ( ) ; Ans wers a s k ( ) { int prob = ( i nt ) ( 1 0 0 * r and . nex tDouЫe ( ) ) ; i f ( prob < 1 5 ) // 1 5% return Answers . МAYBE ; else i f ( prob < 3 0 ) return Answers . NO ; / / 1 5% else i f ( prob < 6 0 ) return Answe rs . YES ; // 30% e l s e i f ( prob < 7 5 ) // 15% return Answers . LATER; Глава 1 2 . Перечисления, автоупаковка и аннотации else i f ( prob < 9 8 ) return Answers . SOON ; else return Answe rs . NEVER; 337 // 13% // 2% class As kМe { static voi d answer ( Answers resul t ) { switch ( re s u l t ) { case NO : System . out . pr intln ( " Heт" ) ; brea k ; cas e YES : System . out . println ( " Дa " ) ; brea k ; case МАУВЕ : System . out . println ( " Boзмoжнo" ) ; break; case LATER : System . out . println ( "Пoзжe " ) ; brea k ; cas e SOON : System . out . println ( " Cкopo " ) ; break; cas e NEVE R : System . out . printl n ( " Hи кoгдa " ) ; break; puЫ ic static vo id ma in ( Str ing [ ] args ) { Quest ion q = new Quest ion ( ) ; answer ( q . a s k ( ) answer ( q . a s k ( ) answer ( q . as k ( ) answer ( q . a s k ( ) ); ); ); ); О болочки типов К а к вам известно, в Java для хранения значений основных типов данных, поддерживаемых языком, используются примитивные типы (также называе­ мые простыми типами) вроде int или douЫe. Использование для хранения таких величин примитивных типов вместо объектов объясняется стремлени­ ем увеличить производительность. Применение объектов для хранения этих значений привело бы к добавлению неприемлемых накладных расходов даже к самым простым вычислениям. Таким образом, примитивные типы не явля­ ются частью иерархии объектов и не наследуются от Obj ect. Несмотря на преимущество в производительности, обеспечиваемое при­ митивными типами, бывают случаи, когда может понадобиться объектное 338 Часть 1. Яз ы к Java представление. Скажем, передавать примитивный тип по ссылке в метод нельзя. Кроме того, многие стандартные структуры данных, реал изованные в Java, работают с объектами, следовательно, вы не можете использовать такие структуры данных для хранения з начений примитивных типов. Чтобы спра­ виться с этими (и другими) сит уациями, в языке Java предусмотрены оболочки типов, которые представляют собой классы, инкапсул ирующие примитивный тип внутри объекта. Классы оболочек типов подробно описаны в части II, но здесь они представлены из-з а с воей прямой с вяз и со средством автоупаковки Java. К оболочкам типов относятся DouЫe, Float, Long, Integer, Short, Byte, Character и Boo lean. Перечисленные классы предлагают широкий набор ме­ тодов, поз воляющих пол ностью интегрировать при митивные т и пы в иерар­ хию объектов J ava. Все они кратко рассматривается далее в гл аве. Класс Character Класс Chara cter я вляется оболочкой для з начения cha r. Вот конструктор класса Cha racter: Characte r ( char ch ) В аргументе ch указ ывается символ, который будет помещен в оболочк у создаваемого объекта Character. Однако, начиная с версии JDK 9, конструктор Cha racter стал нерекомен­ дуемым к у потреблению, а начиная с JDK 1 6, он объявлен устаревшим и под­ лежащим удалению. В настоящее время для полу чения объекта Character настоятельно рекомендуется применять статический метод va lueOf ( ) : s ta t i c Character valueO f ( cha r символ) Метод va lueOf () возвращает объект Cha racter, содержащий внутри себя символ из ch. Чтобы получить значение char, хранящееся в объекте Character, необхо­ димо выз вать метод cha rValue () : char charVa l ue ( ) Он воз вращает инкапсул ированный символ. Класс Boolean Класс Boo lean служит оболочкой для з начений boo lean. В нем определены сл едующие конструкторы: Boolean ( boolean boolVal ue) Boolean ( S tring boolString ) В первой верси и конструктора аргумент boo l Va lue дол жен быть л ибо t rue, либо fa lse. Что касается второй версии конструктора, есл и арг умент boo lString содержит строку "true " (в верхнем или нижнем регистре), тогда новый объект Boo lean будет хранить з начение t rue, а и наче - fa lse. Гл ава 1 2 . Переч ис л ения, а втоупа ковка и а н нота ции 339 Тем не менее, начиная с JDK 9, конструкторы Boo l ean были помечены как нерекомендуемые к использованию, а начиная с JDK 1 6, они стали уста­ ревшими и подлежат удалению. На сегодняшний день для полу чения объ­ екта Boo l ean настоятельно рекомендуется применять стати ческий метод val ueOf ( ) , две версии которого показаны ниже: static Boolean valueOf ( boolean boolValue ) static Boolean valueOf ( S tring boolString) Каждая версия возвращает объект Boo l ean, служащий оболочкой для ука­ занного значения. Чтобы пол учить значение boo l ean из объекта Boo lean, следует использо­ вать метод boo l eanVa l ue ( ) : bool ean boo leanValue ( ) Он возвращает эквивалент типа boo lean для вызывающего объекта. Оболочки числовых типов Безусловно, наиболее часто применяемыми оболочками типов являют­ ся те, которые представляют числовые значения. Ре чь идет о Byte, Sho rt, Integer, Long, Float и DouЫe. Все оболочки числовых типов унаследованы от абстрактного класса NurnЬer. В классе NurnЬer объявлены методы, которые возвращают значение объекта в каждом из различных числовых форматов: byte byteValue ( ) douЫ e douЬleValue ( ) float floatValue ( ) int intValue ( ) long longValue ( ) short s hortValue ( ) Например, метод douЬleVa l ue ( ) возвращает значение объекта в виде douЫe, f loatVa lue () - в виде float и т.д. Указанные методы реализованы в каж дой оболочке числового типа. Во всех оболочках числовых типов определены конструкторы, которые по­ зволяют создавать объект из заданного значения или строкового представ­ ления этого значения. Например, вот конструкторы, определенные в кл ассе Integer: I nteger ( i nt num ) I n teger ( S tring str ) Если в аргументе str не содержится допустимое числовое значение, тогда сгенерируется исклю чение NurnЬerFo rmatException. Однако, начиная с версии JDK 9, конструкторы оболочек числовых типов стали нерекомендуемыми к употреблению, а начиная с JDK 16, они объявлены устаревшими и подлежащими удалению. В настоящее время для пол учения объекта оболочки настоятельно рекомендуется использовать один из мето­ дов va lueO f ( ) . Метод va l ueOf ( ) является стати ческим членом всех клас­ сов оболочек числовых типов и все числовые классы поддержи вают формы, 340 Часть 1 . Язык Java которые преобразуют числовое з начение ил и его строковое представление в объект. Например, вот две формы, поддерживаемые в Integer: static I nteger valueOf ( int va l ) static I nteger val ueOf ( S t ri ng val S t r ) throws NumЬerFormatExcept ion В аргументе va l указывается цел о числ енное з начен и е, а в аргумен­ те val St r - строка, которая представляет надлежащим образом сформа­ тированное числовое з на чение в строковом виде. Ка ждая форма метода valueOf ( ) возвращает объект Integer, содержащий внутри заданную вел и ­ чину. Н и же приведен пример: I nteger iOb = I nteger . val ueOf ( l 0 0 ) ; После выполнения этого оператора з начение 1 0 0 будет представлено эк­ з ем пляром Integer. Таким образом, объект iOb содержит в себе з начение 1 0 0. В дополнение к только что показанным формам valueOf ( ) целочислен­ ные оболочки Byte, Short, Integer и Long также предоставляют форму, по­ зволяющую указать систему счисления. Все оболо чки типов переопределяют метод toSt ring ( ) , который возвра­ щает удобочитаемую форму значения, содержащегося внутри оболочки. Он позволяет выводить значение за с чет передачи объекта оболочки типа, на­ пример, в p r in t ln ( ) , не требуя преобразования объекта в примитивный тип. В показ анной далее программе показано, как применять оболо чку число­ вого типа для инкапсуляции з начения и последующего извле чения данного значения: / / Демонстрация использования оболочки числового типа . class Wrap { puЫ i c static void ma in ( S tring [ ] args ) I nteger iOb = I nteger . val ueOf ( l 0 0 ) ; int i = i Ob . intVa lue ( ) ; System . out . printl n ( i + " " + i Ob ) ; / / выводит 1 0 0 1 0 0 В программе целочисленное значение 1 0 0 помещается внутрь объекта Integer по имени iOb, после чего путем вызова метода intValue ( ) это з на­ чение получается и сохраняется в i. Процесс инкапсуляции з начения внутри объекта называется упаковкой. Таким образом, приведенная ниже строка в программе упаковывает значение 1 0 0 в объект Integer: I nteger i Ob = I nteger . valueOf ( l 0 0 ) ; Процесс извлечения з начения из оболочки типа называется распаковкой. Например, з начение внутри iOb распаковывается в программе посредством следующего оператора: int i = i Ob . i ntValue ( ) ; Глава 1 2 . П ереч исления, автоуnаковка и аннотации 341 Та же самая общая процедура, которая использовалась в предыдущей про­ грамме для упаковки и распаковки значений, была доступна для применения, начиная с первоначальной версии Java. Тем не мен ее, на сегодняшний день в Java предлагается более рациональный подход, который описан далее в главе. А в тоупа ко в к а Современные версии Java включают два важных средства: автоупаковку и автораспаковку. Автоупаковка - это процесс, с помощью которого прими­ тивный тип автоматически инкапсулируется (упаковывается) в эквивалент­ ную ему оболочку типа всякий раз, когда требуется объект такого типа. Нет необходимости явно создавать объект. Автораспаковка - это процесс, при котором значение упакованного объекта автоматически извлекается (распа­ ковывается) из оболочки типа , когда значение необходимо. Не придется вы­ зывать методы вроде intValue ( ) или douЫeValue ( ) . Автоупаковка и автораспаковка значительно упрощают написание кода ряда алгоритмов, избавляя от утомительной ручной упаковки и распаковки значений, а также помогают пр едотвратить ошибки. Более того, они очень важны для обобщений, которые работают только с объектами. Наконец, автоупаковка существенно облегчает взаимодействие с инфраструктурой Collections Framework, описанной в части 11. Благодаря автоупаковке устраняется потребность в ручном создании объ­ екта с целью помещения в него значения примитивного типа. Понадобится лишь присвоить это знач ение ссылке на оболочку типа, а компилятор Java самостоятельно создаст объект. Скажем, вот современный способ создания объекта I nteger со значением 1 0 0: I nteger iOb = 1 0 0 ; / / ав тоупаков ка значения int i n t i = iOb ; / / ав тораспа ковка Обратите внимание, что объект явно не упаковывается. Задачу автомати­ чески решает компилятор Java, Чтобы распаковать объект, нужно просто присвоить ссылку на него пере­ менной примитивного типа. Наприм ер, для распаковки iOb можно использо­ вать такую строку: Обо всем остальном позаботится компилятор Java. Ниже показана предыдущая программа, в которой теперь применяется ав­ тоупаковка/автораспаковка: / / Демонстрация работы автоупаковки / автор аспаков ки . class AutoBox { puЬlic static voi d ma in ( String [ ] args ) { I nteger iOb = 1 0 0 ; / / ав тоу паковка значения int int i = iOb ; / / автор аспаковка System . out . println ( i + " " + iOb ) ; / / выводит 100 1 0 0 342 Ч асть 1. Язык Java Автоупаковка и методы В дополнение к простому слу чаю присваивания автоупаковка происходит вся кий раз, когда прими тивный тип должен быть преобразован в объект, а автораспаковка - когда объект должен быть преобразован в примитивный тип. Таким образ ом, автоупаковка/автораспаковка может быт инициирова­ на при передаче аргумента методу или при возвращении методом значения. Например, вз гляните на следующую программу: / / Автоупаковка/автораспаковка выпол няется в отношении / / параметров и возвращаемо го значения метода . class AutoBox2 { / / Принимает параметр типа Iпteger и возвращает значение i nt . s tatic i nt m ( I nteger v ) { returп v ; / / авторас�аковка в iпt puЫ i c static void mai n ( S tring [ ] args ) / / Передать значение iпt в m ( ) и присвоить возвращаемое значение // объекту Iпtege r . Здесь аргумент 1 0 0 автоупаковывается // в объект Iпteger . Возвращаемое значение тоже автоупаковывается // в объект Iпteger . Iпteger iOb = m ( l 0 0 ) ; System . out . p r i ntl п ( iOb ) ; Вот вывод, генерируемый программой: 100 Обратите внимание в программе, что m ( ) принимает целочисленный пара­ метр и возвращает рез ультат типа int. Внутри main ( ) методу m ( ) передается зна чение 1 0 0. Поскольку метод m ( ) ожидает объект I nteger, з на чение 1 0 0 автомати чески упаковывается. Затем m ( ) возвращает эквивалент типа int своего аргумента, приводя к автораспаковке v. Далее рез ультирующее з наче­ ние int присваивается iOb в main ( ) , что приводит к автоупаковке возвраща­ емого з начения int. Автоупаковка /автораспако в ка и в ыражен и я Как правило, автоупаковка и автораспаковка происходят всякий раз , ког­ да требуется преобраз ование в объект или из объекта, что относится и к выражениям. Внутри выражения числовой объект автоматически распако­ вывается. При необходимости результат выражения упаковывается заново. Например, рассмотрим показ анную далее программу: / / Автоупаковка/автораспаковка происходит внутри выражений . cl as s AutoBoxЗ { puЫ i c stat i c void main ( Striпg [ ] args ) { Iпteger iOb , iOb 2 ; iпt i ; Глава 1 2 . П ере чи слен ия, а втоу па ковка и анн отации 343 iOb = 1 0 0 ; System . out . p rintln ( "Иcxoднoe значение iOb : " + iOb ) ; / / В следующем операторе iOb а втоматически распаковываетс я , / / выполняется ин креме нтирование и резуль тат заново // упаковывается в i Ob . + +iOb ; System . out . p rintln ( " Пocлe ++iOb : " + iOb ) ; / / Здесь iOb распаковывается, выражение вычисляется , / / резул ь тат заново упаковыва ется и присваивается iOb2 . iOb2 = iOb + ( i Ob / 3 ) ; System . out . println ( " iOb2 после вычисления выражения : " + iOb2 ) ; / / Вычисляется то же самое выражени е , / / резуль тат не упаковывается заново . i = iOb + ( iOb / 3 ) ; System . out . p rintln ( " i после вычисления выражения : " + i ) ; Вот вывод: Исходное значение iOb : 1 0 0 После + + iOb : 1 0 1 iOb2 после вычисления выражения : 1 3 4 i после вычисления выражения : 1 3 4 Обратите особое внимание в программе на следующую строку: ++iOb ; Данный оператор инкрементирует значение в iOb. О н работает так: объ­ ект iOb распаковывается, значение инкрементируется, а результат повторно упаковывается. Автораспаковка также позволяет смешивать в выражении разл и чные типы числовых объектов. После распаковки значени й применяются стандартные повышения и преобразования. Скажем, приведенная ниже программа совер­ шенно допустима: c l a s s Au toBox4 { puЫic static void ma i n ( St ring [ ] a rgs ) { I nteger i Ob = 1 0 0 ; DouЫe dOb = 9 8 . 6 ; dOb = dOb + iOb ; System . out . print ln ( "dOb после вычисле ния выражения : " + dOb ) ; Вот вывод, генерируемый программой: dOb после вычисле ния выражения : 1 9 8 . 6 Как ви дите, в сложении участвовал и и объект dOb типа DouЫe, и объект iOb типа Integer, а результат был заново упакован и сохранен в dOb. 344 Ч а с т ь 1. Яз ы к Java Благодаря автораспаковке числовые объекты I nteger можно использо­ вать для управления оператором swi tch. В кач естве примера взгляните на следующий фрагмент кода: I nteger iOb = 2; swi tch ( iOb ) { case 1 : System . out . println ( "oдин " ) ; brea k ; case 2 : System . out . printl n ( "двa " ) ; b rea k ; defau l t : Sys tem . out . println ( "oшибкa " ) ; При вычислении выражения в switch осуществляется распаковка объекта iOb и получение его значения int. Как показывают примеры в программах, из-за автоупаковки/автораспа­ ковки применение числовых объектов в выражении становится простым и интуитивно понятны м. В ранних версиях Java такой код должен был вклю­ чать приведения типов и вызовы методов, подобных i ntValue ( ) . Автоупаковка/автораспаковка типов Boolean и Character К ак было описано ранее, в Java также предоставляются оболочки типов boolean и char - соответственно Boolean и Character, к которым также применяется автоупаковка/автораспаковка. Например, рассмотрим показан­ ную далее программу: // Автоупа ко вка /автораспаковка объе ктов Boolean и Character . class AutoBox5 { puЫ i c s tatic void main ( String [ ] a rgs ) / / Автоматически упако вать /распаковать значение boolean . Boolean Ь = t rue ; / / Ниже Ь автоматически распаковывается при исполь зов ании // в условном выражении , таком как i f . i f ( b ) Sys tem . out . p r intlп ( "b равно true " ) ; / / Автоматически упаковать /распаковать значение char . / / упаковать char Character ch = ' х ' ; / / распаковать char char ch2 = ch ; System . out . p rintln ( " ch2 равно " + ch2 ) ; Ниже приведен вывод: Ь равно true ch2 равно х Гл ава 12. П ере числ е ния, ав тоупаковка и анно тации 345 Самым важным аспектом, который следует отметить в программе, явля­ ется автораспаковка Ь внутри условного выражения i f. Вы наверняка помни­ те о том, что вычисление условного выражения, управляющего оператором i f, должно давать значение типа bool ean. Значение boolean, содержащееся в Ь, автоматически распаковывается при вычислении условного выражения. Таким образом, при автоупаковке/автораспаковке объект Boolean можно ис­ пользовать для управления оператором i f. Благодаря автораспаковке объект Boolean теперь также можно применять для управления любыми операторами цикла Java. Когда объект Boolean ис­ пользуется в качестве условного выражения в цикле while, for или do/while, он автоматически распаковывается в свой эквивалент boolean. Например, вот абсолютно корректный код: Boolean Ь ; // . . . whi l e ( b ) ! // . . . Авто упаков ка/автораспаков ка помо га ет предотвратить о шибки Помимо удобства автоупаковка/автораспаковка также содействует в пре­ дотвращении ошибок. В качестве примера взгляните на следующую программу: // Ошибка при ручной распаковке . class Unbox ingError { puЫ i c static vo id ma in ( S t r ing [ ] a rgs ) { In tege r i Ob = 1 0 0 0 ; / / автоматически упаковать значение 1 0 0 0 System . out . println ( i ) ; // выводится не 1 0 0 0 ! i n t i = iOb . byt eValue ( ) ; / / вручную распаковать как byte ! ! ! Вместо ожидаемого значения 1 0 0 0 программа выводит -24! Дело в том, что значение внутри iOb распаковывается вручную путем вызова byteValue ( ) , вызывая усеч ение значения, которое хранится в i Ob и равно 1 0 0 0. В итоге i присваивается "мусорное" значение - 2 4 . Автораспаковка предотвращает ошибки такого типа, поскольку значение в iOb будет всегда автоматически распаковываться в значение, совместимое с int. В общем случае из-за того, что автоупаковка всегда создает надлежащий объект, а автораспаковка всегда производит корректное значение, процесс не может выдать неправильный тип объекта или значения. В тех редких ситу­ ациях, когда нужен тип, отличающийся от созданного автоматическим про­ цессом, все равно можно вручную упаковывать и распаковывать значения. Конечно, преимущества автоупаковки/а втораспаковки утрачиваются. Таким образом, вы должны применять автоупаковку/автораспаковку. Именно так пишется современный код на Java. 346 Ч асть 1 . Язык Java П ред о стережен и е По причине автоупаковки и автораспаковки у некоторых может возник­ нуть соблазн использовать исключительно такие объекты, как Intege r или DouЫe, полностью отказавшись от примитивных типов. Скажем, при автоу­ паковке/автораспаковке можно написать такой код: // Неудачное применение ав тоупаковки/а втораспаковки ! DouЫe а , Ь , с ; а = 10. 0 ; Ь = 4 .0; с = Math . sqrt ( а * а + Ь * Ь ) ; System . out . print l n ( "Гиnoтeнyзa равна " + с ) ; В приведенном примере объекты типа DouЫe со держат значения, при­ м еняемые для вычисления гипотенузы прямоугол ь ного треуголь ника. Хотя формал ьно этот код корректен и действительно работает правиль но, он де­ монстрирует крайне неудачное использование автоупаковки/автораспаковки. Он гораздо менее эффективен, чем эквивалентный код, написанный с при­ менением примитивного типа douЫe. Дело в том, что каждая автоупаковка и автораспаковка добавляют накладные расходы, которые отсутствуют в случае использования примитивного типа. Вообще говоря, вам следует ограничить применение оболочек типов толь­ ко теми случаями, когда объектное представление примитивного типа обяза­ тель но. Автоупаковка/автораспаковка не добавлялась в Java как "черный ход" для устранения примитивных типов. А нн ота ции Язык Java предлагает средство, позволяющее встраивать дополнитель ную информацию в файл исходного кода. Такая информация, называемая аннота­ ц,ией, н е меняет действия программы, оставляя семантику программы неиз­ менной. Однако данная информация может исполь зоваться разнообразны ­ ми инструм ентами как во время разработки, так и во время развертывания. Например, аннотацию может обрабатывать генератор исходного кода. Для обозначения этого средства также применяется термин метаданные, но тер­ мин аннотац,ия является наиболее описательным и часто ис пользуемым. О сн о вы анн ота ций Аннотация создается с помощью механизма, основанного на интерфейсе. Давайте начнем с примера. Вот объявление для аннотации по имени MyAnno: / / Простой тип аннотации . @ interface MyAnno { S t ring str ( ) ; int va l ( ) ; Глава 1 2 . Перечисления, автоупаковка и аннотации 347 Первым делом обратите внимание на символ @ пер ед ключевым словом interface. Он сообщает компилятору о том, что объявляется тип аннотации. Далее обратите внимание на два члена, str ( ) и val ( ) . Все аннотации состоят исключительно из объявлений методов. Тем не м енее, вы не пр едоставляете тела для этих методов. Взамен их реализует компилятор Java. Более того, как вы увидите, методы во многом похожи на поля. Аннотация не может содержать конструкцию extends. Однако все типы аннотаций автоматически расширяют интерфейс Ann otation, который является суперинтерфейсом для всех аннотаций. Он объявлен в пакете j ava . lang . annotation и переопределяет методы hashCode ( ) , equals ( ) и toString ( ) , определенные в классе Obj ect. Интерфейс Annota tion также задает annotationType ( ) , который возвращает объект Class, представляю­ щий вызы вающую аннотацию. Сразу после объявления аннотацию можно применять для снабжения чего­ нибудь примечанием. Изначально аннотации можно было указывать только в объявлениях , с чего мы и начнем. (В версии JDK 8 добавлена возможность аннотирования сценариев использования типов, как будет показано далее в главе. Тем не менее, те же самые основные методы применимы к обоим ви­ дам аннотаций.) Любой тип объявления может иметь ассоциированную с ним аннотацию. Скажем, можно аннотировать классы, методы, поля, параметры и константы перечислений. Аннотировать допускается даже саму аннотацию. Во всех ситуациях аннотация предшествует остальной ч асти объявления. В случае применения аннотации ее элементам присваиваются значения. Например, вот пример применения аннотации MyAnno к объявлению метода: / / Аннотировать метод . @MyAnno ( s t r = "Пример ан нотации " , val = 1 0 0 ) puЫic static void myMeth ( ) ( / / • . . Здесь аннотация MyAnno связывается с методом myMeth ( ) . Внимательно взгляните на синтаксис аннотации. За именем аннотации, которому пред­ шествует символ @, находится заключенный в круглые скобки список ини­ циализаций членов. Чтобы предоставить члену знач ение, его понадобится присвоить им ени члена. Следовательно, в приведенном выше примере члену str аннотации MyAnno присваивается строка "Пример аннотации " . Обратите внимание, что в этом присваивании после str никаких скобок нет. При пре­ доставлении значения члену аннотации используется только его имя. Таким образом, в данном контексте члены аннотации выглядят как поля. Указание полит ики хранения Перед дальнейшим исследованием аннотаций необходимо обсудить по­ литики хранения аннотаций. Политика хранения устанавливает момент, когда аннотация отбрасывается. В Java определены три такие политики, ко­ торые инкапсулированы внутри п еречисления j a va . lang . ann otation . RetentionPol icy - SOURCE, CLASS и RUNTIME. 348 Часть 1. Язык Java • Аннотация с политикой хранения SOURCE удерживается только в файле исходноrо кода и на этапе компиляции отбрасывается. • Аннотация с политикой хранения CLASS на этапе компиляции сохраня­ ется в файле . clas s. Однако она не будет доступной через машину JVM во время выполнения. • Аннотация с политикой хранения RUNTIME на этапе компиляции сохра­ няется в файле . class и доступна через машину JVM во время выпол­ нения. Таким образом, политика RUNT IME обеспечивает наивысшее по­ стоянство аннотаций. На заметку! Аннотация на объявлении локальной переменной в файле . class не удерживается. Политика хранения для аннотации указывается с применением одной из встроенных аннотаций Java: @Retention. Вот ее общая форма: @Retention ( retent ion-policy) Здесь в арrументе ret ention-policy должна находиться одна из ранее обсуждавшихся констант перечисления. Если для аннотации не задана поли­ тика хранения, тоrда используется стандартная политика CLASS. В показанной далее версии MyAnno с применением @Retent ion указывает­ ся политика хранения RUNTIME. В результате аннотация MyAnno будет доступ­ на машине JVM во время выполнения проrраммы. @Retention ( RetentionPol icy . RUNTIME ) @inter face MyAnno ( String str ( ) ; int val ( ) ; П ол уч ен ие а ннота ц и й во врем я в ы п ол нен и я с и с п ол ьзо ва н ием ре фле кс и и Аннотации предназначены rлавным образом для использования друrими инструментами разработки или развертывания. Тем не менее, если для анно­ таций определена политика хранения RUNТI ME, тоrда их можно запрашивать во время выполнения с помощью любой проrраммы на Java через рефлексию. Рефлексия представляет собой средство, позволяющее получать информацию о классе во время выполнения. АРI-интерфейс рефлексии содержится в паке­ те j ava . lang . e flect. Существует несколько способов работы с рефлексией, но все они в rлаве рассматриваться не будут. Однако мы обсудим ряд приме­ ров, применимых к аннотациям. Первым шагом к использованию рефлексии является получение объек­ та Class, представляющего класс, аннотации которого вы хотите получить. Class - один из встроенных классов Java, определенный в пакете j ava . lang; он будет подробно описан в части II. Существуют различные способы полу­ чения объекта Class. Глава 1 2 . Перечисления, автоулаковка и аннотации 349 Один из самых простых с пособов предусматривает вызов метода getC lass ( ) , определенный в Obj ect со следующей общей формой: f i na l C l a s s < ? > getClass ( ) Метод getC l a s s ( ) возвращает объект C l a s s , который представляет вы­ з ывающий объект. На заметку! Обратите внимание на символы < ? > после Cla ss в только что показанном объявлении метода g е t с 1 а s s ( ) . Они имеют отношение к средству обобщений в Java. Метод getC la s s ( ) и несколько других методов, связанных с рефлексией, которые обсуждаются в этой главе, задействуют обобщения, описанные в главе 1 4. Тем не менее, для понимания фундаментальных принципов рефлексии понимать обобщения вовсе не обязательно. После того, как объект Class получен, с помощью его методов можно про­ сматривать и нформацию о различных элементах, объявленных в классе, в том числе об аннотациях. При желании из влечь аннотации, которые ассоциирова­ ны с конкрет ным элементом, объявленным в классе, сначала потребуется по­ лучить объект, представляющий данный элемент. Например, C l a s s предлага­ ет (помимо прочих) методы getMethod ( ) , getField ( ) и getConstructo r ( ) , предназ наченные для получения и нформации о методе, поле и конструкто­ ре соответственно. Эти методы воз вращают объекты типа Method, Fie ld и Con structo r. Чтобы понять сам процесс, давайте проработаем пример, в котором полу­ чаются аннотации, связанные с методом. Первым делом нужно получить объ­ ект C l as s, представляющий класс, а з атем вызвать для этого объекта Cl ass метод getMethod ( ) , указ ав имя метода. Метод getMethod () и меет следую­ щую общую форму: Method getMethod ( String rnethNarne , Class<?> . . . pararnTypes ) Имя метода передается в аргументе methName. Если у метода есть аргумен­ ты, то объекты C l ass, представляющие типы аргументов, также должны быть указаны в аргументе paramTypes. Обратите внимание, что paramTypes являет­ ся параметром с переменным числом аргументов, т.е. можно задавать столько т и пов параметров, сколько необходи мо, включая ноль. Метод getMethod ( ) воз вращает объект Method, представляющ и й метод. Если метод н е может быть найден, тогда генерируется исключение No SuchМethodException. Вызывая метод getAnnotation ( ) , из объекта C l a s s , Method, Field или Constructor можно получить специфическую аннотацию, ассоциированную с эти м объектом. Вот общая форма getAnnotation ( ) : <А extends Annotation> getAnnotation ( Cl as s <A> annoTyp e ) Здесь annoType - объект класса, представляющий и нтересующую аннота­ цию. Метод возвращает ссылку на аннотацию. С применением такой ссылки можно получить з начения, связанные с членами аннотации. Метод воз вра­ щает nul l , если аннотация не найдена, что происходит, когда аннотация не и меет политики хранения RUNTIME. 350 Часть 1. Язык Java Н иже приведена программа, в которой все показанные ранее части собра­ ны вместе и которая использует рефлексию для отображения аннотации, ас­ социированной с методом: import j ava . lang . annotation . * ; import j ava . lang . reflect . * ; / / Объявление а ннотации типа . @ Re teпtion ( RetentionPo l icy . RUNTIME ) @ i nter face MyAnno { String str ( ) ; int va l ( ) ; class Meta { / / Аннотирова т ь метод , @MyAnno ( s tr = " Пример аннотации " , val puЫ i c static void myMeth ( ) { Meta оЬ = new Meta ( ) ; 100) / / Получить а ннотацию для этого метода // и отобразить значения ее членов . try { / / Для начала получить объект C l as s , / / который представляет данный класс . C l a s s < ? > с = ob . getCl ass ( ) ; / / Теперь получить объект Method , / / который представляет данный метод . Method m = с . getMethod ( "myMeth " ) ; / / Далее получить аннотацию для этого кла сса . MyAnno anno = m . getAnnotation ( MyAnno . class ) ; / / В заключение вывести значения . System . out . pr intln ( anno . s tr ( ) + " " + anno . va l ( ) ) ; catch ( NoSuchMethodException ехс) { System . out . pr intln ( "Meтoд не найден . " ) ; puЫ i c static vo id main ( St ring [ ] args ) { myMeth ( ) ; Вот вывод, генерируемый программой: Пример аннотации 1 0 0 В программе применяется рефлексия, как было описано ранее, для полу­ чения и отображения значений str и val в аннотации MyAnno, ассоциирован­ ной с методом myMeth ( ) в классе Meta. Стоит обратить особое внимание на два аспекта. Первый из них - выражение MyAnno . class в следующей строке: MyAnno anno = m . getAnnotat ion ( MyAnno . class ) ; Глава 1 2 . Переч исл е н и я, автоупаковка и а н н ота ц и и 351 Результатом вычисления данного выражения является объект Cla s s типа MyAnno, т.е. аннотация. Такая конструкция называется литералом класса. Этот тип выражения можно использовать всякий раз, когда необходим объект C la s s известного класса. Например, с помощью показанного далее оператора можно было бы полу чить объект C la s s для Meta: Cla s s < ?> с = Meta . cla ss ; Конечно, такой подход работает, только есл и имя класса объекта извест­ но заранее, что далеко не всегда так. В общем случае л итерал класса можно полу чать для классов, интерфейсов, примитивных типов и массивов. (Не за­ бывайте, что синтаксис < ? > относится к средству обобщений Java, которое обсуждается в главе 14.) Вторым интересным аспектом является способ пол у чения значений str и va l, когда они выводятся следующей строкой: System . out . println ( anno . s tr ( ) + " " + anno . val ( ) ) ; О братите внимание, что они вызываются с при менением синтаксиса вы­ зова методов. Тот же самый подход используется всякий раз, когда требуется значение члена аннотации. Второй пример испол ьзова ния рефлексии В предыдущем примере метод myMeth ( ) не принимает параметры. Таким образом, при вызове getMethod () передается только имя myMeth. Однако чтобы полу чить метод с параметрами, в качестве аргументов getMetho d ( ) понадобится указать объекты класса, представляющие типы этих параме­ тров. Например, вот слегка измененная версия предыдущей программы: import j ava . lang . annotation . * ; import j ava . lang . re flect . * ; @ Retention ( RetentionPo l i cy . RUNT I ME ) @ i nterface MyAnno { S t ring str ( ) ; int va l ( ) ; class Meta { / / Метод myMeth теперь принимает два аргумента . @MyAnno ( s tr = "Два параметра " , va l = 1 9 ) puЫ i c static vo id myMeth ( String s t r , int i ) { Meta оЬ = new Meta ( ) ; try { Clas s < ? > с = ob . getClass ( ) ; / / Здесь указываются типы параметров . Method m = c . getMet hod ( "myMeth " , St ring . clas s , int . class ) ; MyAnno anno = m . getAnnotation ( MyAnno . c las s ) ; System . out . println ( anno . s tr ( ) + " " + anno . va l ( ) ) ; } catch ( NoSuchMethodException ехс) { 352 Ч асть 1. Язык J ava System . out . println ( "Meтoд не найден . " ) ; puЫ ic static void mai n ( S t r ing [ ] args ) { myMeth ( " тест " , 1 0 ) ; Ниже показан вывод, генерируемый программой: Два параметра 1 9 В данной версии программы метод myMeth ( ) принимает параметры типа S t r i ng и int. Для получения информации об этом методе необходимо вы­ звать метод ge tMethod ( ) следующим образом: Method m = c . getMethod ( "myMeth " , Str ing . cl as s , int . class ) ; Объекты Class, представляющие типы S t ring и int, передаются в виде дополнительных аргументов. Получение всех аннотаций Чтобы получить все аннотации с пол и тикой хранения RUNТ IME, ас­ соци ированные с элементом, можно выз вать на данном элементе метод ge tAnnot a t i ons ( ) , который и меет приведенную ниже общую форму: Annotation [ ] getAnnotations ( ) Метод getAnnotations ( ) возвращает массив аннотаций и может быть выз ван на объектах типа Class, Method, Cons t ructor и Field (помимо прочих). / / Отображение всех а ннотаций для класса и метода . import j ava . l ang . annotati on . * ; import j ava . lang . reflect . * ; @ Retention ( RetentionPol icy . RUNTIME ) @ interface MyAnno { String str ( ) ; int val ( ) ; @ Retention ( RetentionPol icy . RUNTIME ) @ interface What { Str ing description ( ) ; @What (description = " Аннотация класса " ) @MyAnno ( st r = "Meta2 " , val = 9 9 ) class Meta2 { @What ( description = "Аннотация метода " ) @MyAnno ( s tr = " Testing " , val = 1 0 0 ) puЫ i c static void myMeth ( ) { Me ta2 оЬ = new Meta2 ( ) ; try { Annotation [ ] annos = ob . getClas s ( ) . getAnnotations ( ) ; Глава 1 2 . Пере числ ен ия, ав тоу п аковка и анно та ц ии 353 // Отобразить все аннотации для Meta2 . System . out . println ( " Bce аннотации для класса Meta2 : " ) ; for ( Annotation а : a nnos ) System . out . println ( a ) ; System . out . println ( ) ; / / Отобразить все аннотации для myMeth . Method m = оЬ . getClass ( ) . getMethod ( "myMeth" ) ; annos = m . getAnnota t i ons ( ) ; System . out . println ( " Bce аннотации для метода myMeth : " ) ; for ( Annotation а : anno s ) System . out . println ( a ) ; catch ( NoSuchMethodException ехс ) System . out . println ( "Meтoд не найден . " ) ; puЫic static vo id main ( S tring [ ] args ) { myMeth ( ) ; } Вот вывод: Все аннотации для класса Meta2 : @What ( des cript ion=Aннoтaция класса ) @MyAnno ( s t r =Meta2 , va l= 9 9 ) Все аннотации для метода myMeth : @What ( description=Aннoтaция метода ) @ MyAnno ( s t r= Testing, val= l O O ) Метод getAnnotations ( ) используется в программе для получения мас­ сива всех аннотаций, ассоциированных с классом Meta2 и методом myMeth ( ) . Как объяснялось ранее, getAnno tations ( ) возвращает массив объектов Annotation. Вспомните, что тип Annotation является суперинтерфейсом для всех интерфейсов аннотаций и переопределяет метод toS tring ( ) в Obj ect. Таким образом, при выводе ссылки на объект Annotation вызывается его метод toString ( ) для создания строки, описывающей аннотацию, что было видно в предыдущем выводе. Интepфeйc AnnotatedElement Методы getAnnotation ( ) и getAnnotations ( ) , применяемые в предше­ ствующих примерах, определены в интерфейсе AnnotatedElement из пакета j ava . lang . reflect. Он поддерживает рефлексию для аннотаций и реализо­ ван среди прочего классами Method, Field, Cons tructor, Class и Package. Помимо getAnnotation ( ) и getAnnot a t i ons ( ) в AnnotatedElement определено несколько других методов. Два метода доступны с тех пор, как аннотации были первоначально до­ бавлены в Java. Первый - getDeclaredAnnotations ( ) , который имеет сле­ дующую общую форму: Annotation [ ] getDecla redAnnotations ( ) 354 Часть 1. Язы к Java Метод g e t D e c lare dAnnota t i ons ( ) возвращает все неунасл едован­ ные аннотации, присутствующие в вызывающем объекте. Второй м етод isAnnotationPresent ( ) с такой общей формой: de fault boo lean isAnnotationPresent ( Class<? extends Annotation> annoType ) Метод isAnnotat i o n Pres ent ( ) возвра щает true, если аннотация, указанная в аргум енте annoType, ассоциирована с вызывающим объек­ том. В противном случае он возвращает fa lse. В версии JDK 8 к м ето­ дам getDec laredAnn otati ons ( ) и isAnnotat i on Pres ent ( ) были до­ бавл ены м етоды getDeclaredAnnotation ( ) , getAnnotat ionsByType ( ) и getDecl aredAnnotationsByT ype ( ) . Посл едние два автоматически работают с повторяющейся аннотацией (повторяющи еся аннотации обсуждаются в конце главы). Ис п ол ь зование станда ртн ых з на ч ени й Для чл енов аннотации можно задавать стандартные зн ачения, которы е будут испол ьзоваться, если при применении аннотации не указано значен ие. Стандартное значение устанавлива ется путем добавления к объявлению чле­ на memЬer ( ) конструкции defaul t, имеющей следующую общую форму: type memЬer ( ) default value ; Здесь значение va lue должно иметь тип, совместимый с типом, который указан в type. Ниже показана ан нотация @MyAnno, пер еписанная с целью включения стандартных значений: / / Объявление типа аннотации , в ключающее ст андартные значени я . @ Retent i on (RetentionPolicy . RUNT IME ) @ interface MyAnno { String str ( ) defaul t "Тест " ; int va l ( ) default 9 0 0 0 ; В объявл ении str пол учает стандартное значение "Тест", а va l - 9 00 0. Таким образом, при использовании @MyAnno не нужно указывать ни одно из значений. Тем не м енее, в случае необходимости одному или обоим могут быть присвоены значения. Ниже приведены четыре способа применения @MyAnno: @MyAnno ( ) @MyAnno ( str @MyAnno ( va l @MyAnno ( s t r / / используются стандартные значения для str и val " с трока " ) / / исполь зуется стандартное значение для val / / используется стандартное значение для s t r 1J0) "Тест " , v a l = 1 0 0 ) / / стандартные значение н е используются В следующей программе демонстрируется использование стандартных значений в аннотации: import j ava . lang . annotat ion . * ; import j ava . lang . reflect . * ; / / Объявление типа аннотации , в ключающее стандартные значения . Гла ва 1 2. П е р еч исл е н и я, автоу п аковка и анн ота ции 355 @ Retent ion ( RetentionPol icy . RUNT IME ) @ inter face MyAnno { String s t r ( ) de fault " Тест " ; int val ( ) default 9 0 0 0 ; class МеtаЗ { / / Аннотировать метод с исполь зованием ста ндартных значений . @MyAnno ( ) puЫ i c static voi d myMeth ( ) МеtаЗ оЬ = new Меt а З ( ) ; / / Получить аннотацию для этого метода / / и вывести значения ее членов . try { Cla s s < ? > с = ob . getClass ( ) ; Method m = с . getMethod ( "myMeth " ) ; MyAnno anno = m . getAnnotation ( MyAnno . c las s ) ; S ystem . out . println ( anno . s tr ( ) + " " + a nno . val ( ) ) ; catch ( NoSuchMethodExcepti on ехс ) [ System . out . p rint ln ( "Метод не найден . " ) ; puЫ ic static voi d ma in ( S t r ing [ ] args ) [ myMeth ( ) ; } Вот вывод: Тест 9 0 0 0 М аркерные аннотации Маркерная аннотация является специальным видом аннотации, не со­ держащим членов. Ее единственная цель - пометить элемент. Таким об­ разом , впол не достаточ но нал ичия данной аннотации. Лучший способ определ ить нал и ч и е маркерно й аннотации предусматривает при мене­ ние метода i sAnno t a t i o n P r e s e n t ( ) , который определен в интерфейсе AnnotatedElement. Далее приведен при мер, в котором используется м аркерная аннотация. Поскольку такая аннотация не содержит членов, достаточно просто выяс­ нить, прис утствует она или же отс утствует. import j ava . l ang . annotation . * ; import j ava . lang . re flect . * ; / / Маркерная аннотаци я . @ Retent ion ( Retent i onPol i cy . RUNTIME ) @ inter face MyMarke r [ } class Mar ker [ / / Аннотировать ме тод, используя маркерную а ннотацию . / / Обратите в нимание , что с кобки ( ) не требуются . Часть 1. Яз ы к Java 356 @MyMa r ke r puЫ i c static vo id rnyMet h ( ) Marke r оЬ = new Marker ( ) ; try { Method rn = ob . getCla s s ( ) . getMethod ( "rnyMeth " ) ; // Выяснить , присутствует ли аннотация . i f ( rn . isAnnotationPresent ( MyMar ke r . class ) ) Systern . ou t . println ( "MyMar ker присутствует . " ) ; catch ( NoSuchMethodException ехс ) { Systern . ou t . printl n ( "Meтoд не найден . " ) ; puЬlic static voi d rna i n ( S tring [ ] args ) { myMeth ( ) ; } Вывод, rенерируемый проrраммой, подтверждает присутствие @MyMarker: MyMa rker присутствует . Обратите в проrрамме внимание, что указывать круrлые скобки после ан­ нотации @MyMarker в случае ее использования не требуется, т.е. @MyMarker применяется просто по своему имени: @MyMa r ker К маркерной аннотации можно добавить пустой набор круrлых скобок, что не считается ошибкой, но они не обязательны. Одно элементные аннотации Одноэлементная аннотация содержит только один член. Она работает как нормальная аннотация за исключением тоrо, что позволяет использовать со­ кращенную форму для указания значения члена. Коrда присутствует только один член, при применении аннотации можно просто указ ать значение для неrо - указывать имя члена не нужно. Однако для использования такоrо со­ кращения именем члена должно быть value. Ниже показан пример создания и применения одноэлементной аннотации: irnpor t j ava . lang . annotation . * ; irnpor t j ava . lang . reflect . * ; / / Одноэлементная аннотация . @ Re tention ( RetentionPol i c y . RUNTIME ) @ i n terface MySingle { int value ( ) ; / / именем члена должно быть va lue class Single { / / Аннотировать метод, исполь зуя одноэлемент ную аннотацию . @MyS i ngl e ( l O O ) puЫ i c static void rnyMeth ( ) S ingle оЬ = new S i ngle ( } ; Гла ва 1 2. Перечисления, автоуnаковка и а н нотации 357 try ( Method m = оЬ . getClass ( ) • getМet hod ( "myMeth" } ; MySi ngle anno = m .getAnnotation (MySingle . class } ; System . out .p rintln ( anno . value ( ) ) ; / / выводит 100 catch ( NoSuchMethodException ехс } ( System . out . println ( "Метод не найден . " } ; puЬlic static void main { String [ ] args } ( myМeth ( } ; } Программа вполне ожидаемо отображает значение 1 0 0, а @MyS i ngle испо льзуется в ней для аннотирования метода myMeth ( ) : @MySingle ( l 0 0 } Обратите внимание, что указывать va lue = не обязат ельно. Синтаксис с одним значением можно использовать при применении ан­ нотации, которая имеет дру rи е члены, но все эти друrие члены должны быть снабжены стандартными знач ениями. Например, в следующем фрагменте кода добавлен член xyz со стандартным значением, равным нулю: @inter face SomeAnno { int value { } ; int xyz { } default О ; В ситуациях, когда для xyz желательно использовать стандартно е значе­ ние, можно применить @ SomeAnno, просто указав значение value с помо щью синтаксиса с одним членом: @SomeAnno ( 8 8 } В этом случае стандартным значением xyz будет ноль, а value получит значение 8 8 . Конечно, чтобы задать другое значение для xyz, необходимо явно указать оба члена: @SomeAnno {value = 8 8 , xyz = 9 9 ) Не забывайте, что всякий раз, когда используется одноэлементная аннота­ ция, именем элемента должно быть value. В стро е н н ы е аннота ц ии В Java определено множество встроенных аннотаций. Бо льшинство из них специализированы, но девять являются аннотациями общего назначе­ ния. Четыре аннотации импортиру ются из пакета j ava . lang . annota t i o n: @Retention, @ Documented, @Target и @ I nheri ted. Пять аннотаций - @Ove r r i de , @ Deprec ated, @ Functiona l i nt e r face, @ S a feVarargs и @SuppressWarni ngs - входят в состав пакета j ava . l ang. Все они описаны ниже. 358 Часть 1. Язык Java На эаметку! Пакет j ava . lang . annotat ion также включает аннотации RepeataЫe и Native; из них RepeataЫe поддерживает повторяющиеся аннотации, рассматриваемые позже в главе, а Nati ve позволяет аннотировать поле, доступ к которому может получать машинный код. @Retention Аннотация @Retention рассчитана только на применение к другой анно­ тации. Она определяет политику хранения, как было описано ранее в главе. @Docwnented Аннотация @Documented представляет собой маркерный интерфейс, кото­ рый сообщает инструменту о том, что аннотация должна быть документиро­ вана. Она предназначена для использования только в качестве аннотации к объявлению аннотации. @ Target С помощью аннотации @Target задаются типы элементов, к которым мо­ жет применяться аннотация. Она спроектирована для использования толь­ ко в качестве аннотации к другой аннотации. Аннотация @Target прини­ мает один аргумент, представляющий собой массив констант перечисления ElementType. В этом аргументе указываются типы объявлений, к которым может применяться аннотация. Константы описаны в табл. 12.1 вместе с ти­ пами объявлений, которым они соответствуют. Табllмца 1 2.1. Константы nереч1111сnен1111я E1eiaentТype Цe,181U utlC1'8111'8 дNNOTATION ТУРЕ Другая аннотация CONSTRUCTOR Конструктор FIELD Поле LOCAL VARIAВLE Локальная переменная METHOD Метод MODULE Модуль РАСКАGЕ Пакет PARAМETER Параметр RECORD COMPONENТ Компонент записи (добавлен в JDK 16) ТУРЕ Клас;с, интерфейс или перечисление ТУРЕ PARAМETER Параметр типа ТУРЕ USE Использование типа Глава 1 2 . Пе ре числ е ния, автоупаковка и аннота ции 359 В аннотации @ T a rget можно указывать одно или несколько значений из табл. 12.1. Несколько значений задаются в виде списка, заключенного в фигурные скобки. Например, вот как можно указать о том, что аннотация @Target применяется только к полям и локальным переменным: @ T arget ( { ElementType . F IELD, El ementType . LOCAL_VARIABLE } ) Если @ Ta rget отсутствует, тогда аннотацию можно использовать в любом объявлении. По этой причине часто рекомендуется явно указывать цель или цели, чтобы четко обозначить предполагаемое применение аннотации. @ Inherited @ I nhe r i ted является маркерной аннотацией, которую можно использо­ вать только в объявлении другой аннотации. Кроме того, @ I nhe ri ted влия­ ет только на аннотации, которые будут применяться в объявлениях классов. Указ ание @ I nherited приводит к тому, что аннотация для суперкласса на­ следуется подклассом. Следовательно, при запросе у подкласса конкретной аннотации, если эта аннотация отсутствует в подклассе, то проверяется его суперкласс. Если же аннотация присутствует в суперклассе и снабжена анно­ тацией @ I nheri ted, тогда она будет возвращена. @Override @Override - маркерная аннотация, которую можно использовать только для методов. Метод с аннотацией @ Override должен переопределять метод из суперкласса, иначе возникнет ошибка на этапе компиляции. Она приме­ няется для гарантирования того, что метод суперкласса действительно пере­ определен, а не просто перегружен. @Deprecated Аннотация @ Deprecated указывает, что объявление устарело и не реко­ мендуется к употреблению. Начиная с JDK 9, аннотация @ Deprecated также позволяет указать версию Java, в которой было заявлено об устаревании, и планируется ли удаление устаревшего элемента. @ Functionalinterface @ Funct i o na l i n t e r f a ce представляет собой маркерную аннотацию, предназначенную для использования в интерфейсах. Она указывает на то, что аннотированный интерфейс является функциональным интерфейсом. Функц,иональный интерфейс - это интерфейс, который содержит один и толь­ ко один абстрактный метод. Функциональные интерфейсы задействованы в лямбда-выражениях. (Подробные сведения о функциональных интерфейсах и лямбда-выражениях ищите в главе 1 5.) Если аннотированный интерфейс не относится к функциональным, тогда будет сообщено об ошибке на этапе ком­ пиляции. Важно понимать, что аннотация @ Functional interface не нужна для создания функционального интерфейса. Любой интерфейс, имеющий в 360 Часть 1. Язык Java точности один абстрактный метод, по определению будет функциональным интерфейсом. Таким образом, аннотация @ Functional interf ace носит чи­ сто информационный характер. @ SafeVarargs @ SafeVarargs - маркерная аннотация, которую можно применять к мето­ дам и конструкторам. Она указывает на отсутствие небезопасных действий, связанных с аргументом переменной длины. Аннотация @ S a feVarargs ис­ пользуется для подавления непроверяемых предупреждений в безопасном в остальном коде, поскольку он относится к нематериализуемым (non-reifiaЬ\e) типам аргументов переменной длины и созданию экземпляров параметри­ зованного массива. (Нематериализуемый тип, по сути, представляет собой обобщенный тип. Обобщенные типы обсуждаются в главе 14.) Она должна применяться только к методам или конструкторам с аргументами перемен­ ной длины. Методы также обязаны быть static, final или private. @ SuppressWarnings Аннотация @ Suppres sWarnings указывает, что одно или несколько пред­ упреждений, которые могут быть выданы компилятором, должны быть по­ давлены. Предупреждения, подлежащие подавлению, задаются по имени в строковой форме. Аннотации типов Как упоминалось ранее, первоначально аннотации были разрешены только в объявлениях. Тем не менее, современные версии Java позволяют указывать аннотации в большинстве случаев использования типов. Такое расширенное свойство аннотаций называется аннотац,иями типов. Скажем, можно анноти­ ровать возвращаемый тип метода, тип this внутри метода, приведение, уров­ ни массива, унаследованный класс и конструкцию throws. Вдобавок можно аннотировать обобщенные типы, включая границы параметров обобщенного типа и аргументы обобщенного типа. (Обобщения рассматриваются в главе 14.) Аннотации типов важны, поскольку они позволяют инструментам выпол­ нять дополнительные проверки кода , чтобы предотвратить ошибки. Важно понимать, что компилятор j avac, как правило, не будет проводить такие про­ верки са мостоятельно. Для этой цели используется отдельный инструмент, хотя такой инструмент может функционировать как подключаемый модуль компилятора. Аннотация типа должна включать E l ementType . ТУРЕ_ USE в качестве цели. (Вспомните, что согласно приведенным ранее объяснениям, допустимые цели аннотаций указываются с помощью аннотации @Target.) Аннотация типа при­ меняется к типу, которому она предшествует. Например, если предположить, что аннотация типа имеет имя @TypeAnno, то разрешено записывать так: void myMe th ( } throws @TypeAnno Null PointerExcep tion { // . . . Гл ава 12. П еречисле н и я , автоупаковка и аннотации 361 Здесь @ Typ eAnno аннотирует Null PointerExcepti o n в конструкции throws. Можно также аннотировать тип this (называемый получателем). Как вам известно, это неявный аргумент для всех методов экземпляра, и он относит­ ся к вызываемому объекту. Аннотирование его типа требует использования другого средства, которое изначально не было частью Java. Начиная с JDK 8, this можно явно объявлять в качестве первого параметра метода. В таком объявлении типом this должен быть класс, например: class SomeCl ass { int myMeth ( S omeClass t hi s , i n t i , i nt j ) { // . . . Поскольку myMeth ( ) - метод , определенный в SomeCla s s , его типом яв­ ляется SomeCla ss. Теперь с применением этого объявления можно аннотиро­ вать тип thi s. Скажем, снова предполагая нал и чие аннотации типа @TypeAnno, следующий код будет допустимым: int myMeth ( @ TypeAnno SomeClass thi s , int i , int j ) { // . . . Важно понимать, что объявлять this нет никакой необходимости, есл и вы не аннотируете thi s. (Если аргумент thi s не объявляется, то он все равно передается неявно, как было всегда.) Кроме того, явное объявление thi s не меняет никаких аспектов сигнатуры метода, т.к. по умолчанию this объявля­ ется неявно. Стоит повториться: вы будете объявлять thi s, только если хо­ тите применить к нему аннотацию типа. В случае объявления this он обязан быть первым аргументом. В следующей программе демонстрируется несколько мест, где можно ис­ пользовать аннотацию типа. В ней определен ряд аннотаций, часть которых предназначена для аннотирования типов. Имена и цели аннотаций приведе­ ны в табл. 1 2.2. Табпица 1 2.2. Имена и цепи аннотаций типов @TypeAnno ElementType . TYPE_USE @MaxLen ElementType . TYPE_USE @NotZeroLen ElementType . TYPE_USE @Unique ElementType . TYPE_USE @What ElementTyp e . TYPE_PARAМETER @ EmptyOK ElementTyp e . F I ELD @ Recommended ElementTyp e . METHOD Обратите внимание, что @EmptyOK, @Recommended и @What не являются ан­ нотациями типов. Они вклю чены в целях сравнения. Особый и нтерес пред ­ ставляет аннотация @What, которая применяется для аннотирования объяв­ ления параметра обобщен ного типа. Все случаи использования описаны в комментариях внутри программы. 362 Часть 1 . Яз ык Java / / Демонстрация исполь зования несколь ких аннотаций типов . import j ava . lang . annotation . * ; import j ava . lang . reflect . * ; / / Мар керная аннотация может применяться к типу . @Target ( El ementType . TYPE_USE ) @ i nter face TypeAnno { } / / Вторая маркерная аннотаци я , которая может быть применена к типу . @Ta rget ( El ementType . TYPE USE ) @ i nter face NotZe roLen { } / / Трет ь я маркерная аннотаци я , которая может быть применена к типу . @Target ( E lementType . TY PE_USE ) @ i nter face Unique { } / / Параметризованная аннотаци я, которая может быть применена к типу . @ Target ( El ementType . TY PE_USE ) @ i nter face MaxLen { int va lue ( ) ; / / Аннотаци я , которая может быть применена к параметру типа . @ Ta rget ( El ementType . TYPE PARAМETER) @ i nte rface What { St ring de scription ( ) ; / / Аннотация , которая может быть применена к объявлению поля . @Ta rget ( ElementType . FI EL D ) @ i nt er face EmptyOK { } / / Аннотаци я , ко торая может быть применена к объявлению метода . @Target ( ElementType . METHOD) @ i nte r face Recommended { } / / Исполь зовать аннотацию на параметре типа . class TypeAnnoDemo<@What ( desc ription = "Обобщенный тип данных " ) Т> { / / Исполь зовать аннотацию типа для конструктора . puЫ i c @ Un i que TypeAnno Demo ( ) { } / / Аннотировать тип ( в этом случае S tr i ng ) , не поле . @TypeAnno String s t r ; / / Аннотировать поле tes t . @ EmptyOK String tes t ; / / Исполь зовать аннотацию типа для аннотирования this ( получателя ) . puЫ i c int f ( @TypeAnno TypeAnnoDemo <T> thi s , i n t х ) { return 1 0 ; / / Аннотировать возвраща емый тип . puЫic @TypeAnno I nteger f 2 ( int j , int k ) { return j + k ; / / Аннотировать объя вление метода . puЫ i c @ Recommended I nteger f З ( St ring s t r ) { retu rn str . l ength ( ) / 2 ; Гл а ва 1 2 . П ереч исления, а втоу п а ковка и а ннотац ии 363 // Исполь зовать аннотацию типа с конструкцией throws . puЫ ic vo id f4 ( ) t hrows @Т уреАnпо Nu l l PointerException // . . . / / Анн отировать уровни ма ссива . S t riпg @MaxLen ( l 0 ) [ ] @NotZeroLen [ ] w ; / / Аннотировать тип элементов массива . @ТуреАппо I nteger [ ] ve c ; puЫ i c s t atic void myMeth ( i nt i ) { / / Исполь зовать аннотацию типа для аргумента типа . TypeAnno Demo<@TypeAnno Integer> оЬ = new TypeAг.пoDemo<@TypeAnno Integer> ( ) ; / / Ис поль зовать анно тацию типа для операции new . @Unique TypeAппoDemo<Iпteger> оЬ2 = new @Unique TypeAnnoDemo<Iпtege r> ( ) ; Obj e ct х = Integer . valueOf ( l 0 ) ; I п teger у ; / / Использовать аннотацию типа для приведе ния . у = ( @ ТуреАппо I ntege r ) х ; puЫic s tatic void mai n ( S t ring [ ] a rgs ) { myMe th ( l 0 ) ; } / / Исполь зовать а н нотацию типа для конструкции наследовани я . class SomeClass extends @T ypeAnno TypeAnnoDemo<Booleaп> { } Хотя то, к чему относится большинство аннотаций в предыдущей програм­ ме, совершенно ясно, четыре варианта использования требуют некоторого обсуждения. Прежде всего, сравним аннотацию возвращаемого типа метода с аннотацией объявления метода. Обратите особое внимание в программе на следующие два объявления методов: // Аннотировать возвращаемый тип . puЫ ic @ T ypeAnno Iпteger f2 ( i nt j , i пt retu rn j + k ; k) { / / Аннотировать объявление метода . puЫ ic @ Recommended Integer fЗ ( S tring s t r ) { return s t r . length ( ) / 2 ; В обоих случаях аннотация предшествует возвращаемому типу метода (I nteger). Однако эти две аннотации аннотируют два разных элемента. В пер­ вом случае аннотация @TypeAnno аннотирует возвращаемый тип метода f2 ( ) . Причина в том, что в качестве цели аннотации @TypeAnno указано ElementType . ТУРЕ_USE и потому ее можно применять для аннотирования использования типов. Во втором случае аннотация @ Recomme nded аннотирует само объ­ явление метода, т.к. целью @ Recommended является ElementType . METHOD. 364 Часть 1. Язык Java В результате @ Recommended применяется к объявлению, а не к возвращаемо­ му типу. Следовательно, спецификация цели позволяет устранить то, что на первый взгляд кажется неоднозначностью между аннотациями объявления и возвращаемого типа метода. Еще один момент, касающийся аннотирования возвращаемого типа метода: аннотировать возвращаемый тип void нельзя. Второй интересный момент связан с аннотациями полей: / / Аннотировать тип ( в этом случае String ) , не поле . @TypeAnno St ring s t r ; / / Аннотировать поле te st . @Empt yOK St ring tes t ; Аннотация @TypeAnno аннотирует тип String, а @ EmptyOK - поле test. Несмотря на то что обе аннотации предшествуют всему объявлению, их цели различаются в зависимости от типа целевого элемента. Если аннотация имеет цель El ementType . ТУРЕ_USE, тогда аннотируется тип. Если в качестве цели указано ElementType . FIELD, то аннотируется поле. Таким образом, ситуация аналогична только что описанной для методов, и никакой неоднозначности нет. Тот же самый механизм устраняет неоднозначность аннотаций на ло­ кальных переменных. Теперь обратите внимание на аннотирование this (получателя): puЫic int f ( @ TypeAnno TypeAnnoDemo<T> thi s , int х ) { Здесь t h i s указывается в качестве первого параметра и имеет тип TypeAnnoDemo (класс, членом которого является метод f ( ) ). Как объясня­ лось ранее, в объявлении метода экземпляра можно явно указывать параметр this, чтобы применить к нему аннотацию типа. Наконец, взгляните на аннотирование уровней массива: St ring @MaxLen ( l 0 ) [ ] @NotZeroLen [ ] w; В этом объявле нии @ M a x L e n а ннотирует тип первого уровня, а @Not ZeroLen - тип второго уровня. В следующем объявлении аннотируется тип элементов I nteger: @TypeAnno Integer [ ] vec; П овто ря ю щие с я аннота ц ии Начиная с JDK 8, аннотацию разрешено многократно применять к одному и тому же элементу. Такие аннотации называются повторяющимися. Чтобы аннотацию можно было повторять, ее потребуется снабдить аннотацией @ RepeataЫe, определенной в пакете j ava . lang . annotation, и указать в ее поле va l ue тип контейнера для повторяющейся аннотации. Контейнер зада­ ется в виде аннотации, для которой поле va lue представляет собой массив типа повторяющейся аннотации. Таким образом, для создания повторяющей­ ся аннотации необходимо создать контейнерную аннотацию, после чего ука­ зать этот тип аннотации в качестве аргумента аннотации @ RepeataЫe. Глава 1 2 . Пере ч ислени я, автоупаковка и анн ота ции 365 Чтобы полу чить доступ к повторяющимся аннотациям с помощью метода вроде getAnnotation ( ) , будет испол ьзоваться контейнерная аннотация, а не сама повторяющаяся аннотация. Такой подход демонстрируется в следующей программе, где показ анная ранее версия MyAnno преобразуется в повторяю­ щуюся аннотацию, которая з атем применяется. // Демонстрация исполь зования повторяющихся аннотаций . import j ava . l ang . annotation . * ; import j ava . l ang . refl ect . * ; / / Сделать аннотацию MyAnno повторяющейся . @ Retention ( RetentionPolicy . RUNT IME ) @ RepeataЫ e ( MyRepeatedAnnos . clas s ) @ inter face MyAnno { String s t r ( ) de fault "Тест " ; int va l ( ) defau l t 9 0 0 0 ; / / Это контейнерная аннотация . @ Retention ( RetentionPolicy . RUNTIME ) @ i nter face MyRepeatedAnnos { MyAnno [ ] val ue ( ) ; } class RepeatAnno { / / Повторить аннотацию MyAnno для метода myMeth ( ) . @MyAnno ( str = " Первая аннотация " , val = - 1 ) @MyAnno ( st r = " Вторая аннотация " , val = 1 0 0 ) puЫ i c s tatic vo id myMeth ( String s t r , int i ) { RepeatAnno оЬ = new RepeatAnno ( ) ; try { Class<? > с = ob . getCla s s ( ) ; / / Получить аннот ации для метода myMeth ( ) . Method m = c . getMethod ( "myMeth " , String . clas s , int . clas s ) ; / / Отобразить повт ор яющиеся аннотации MyAnno . Annotation anno = m . getAnnotation (MyRepeatedAnnos . class ) ; System . out . print l n ( anno ) ; catch ( NoSuchMethodException ехс ) { System . out . println ( "Meтoд не найден . " ) ; p uЫ i c s tatic vo id ma i n ( S tring [ ] args ) { myMeth ( " тe cт " , 1 0 ) ; Ниже показ ан вывод, генерируемы й программой: @MyRepeatedAnnos ( value = { @MyAnno ( val=- 1 , str="Первая аннотация " ) , @MyAnno ( va l= l 0 0 , str= " Bтopaя аннотация " ) } ) Как объяснялось ранее, чтобы аннотацию MyA nno можно было повторять, она должна быть снабжена аннотацией @RepeataЫe, которая з адает ее кон- 366 Часть 1. Язык Java тейнерную аннотацию. Контейнерная аннотация имеет имя MyRepeatedAnnos. Программа получает доступ к повторяющимся аннотациям, вызывая метод getAnnotat ion ( ) с передачей ему класса контейнерной аннотации, а не са­ мой повторяющейся аннотации. В выводе видно, что повторяющиеся анно­ тации отделяются друг от друга з апятыми. По отдельности они не возвраща­ ются. Другой способ получения повторяющихся аннотаци й предусматривает ис­ пользование одного из методов в A nnotatedElement, который способен ра­ ботать напрямую с повторяющейся аннотацией - getAnnotationsByType ( ) и getDeclaredAnnotationsByType () . Вот общая форма первого метода: default <Т extends Annotation> Т [ ] getAnnotationsByType ( Class<T> annoType ) Метод getA nnotat io nsByTyp e () воз вращает массив аннотаций типа annoType, ассоциированных с вызывающим объектом. Если аннотации от­ сутствуют, тогда массив будет иметь нулевую длину. Рассмотрим пример. С учетом предыдущей программы в следующей кодо­ вой последовательности метод getA nnotationsByTyp e ( ) используется для получения повторяющихся аннотаций MyAnno: Annotation [ ] annos = m . getAnnotationsByType (MyAnno . clas s ) ; for (Annotation а : annos ) System . out . println ( a ) ; Методу getA nnotat io nsByType ( ) передается тип повторяющейся анно­ тации MyAnno. Возвращаемый массив содержит все экземпляры MyA nno, свя­ з анные с myMeth () , которых в этом примере два. К каждой повторяющейся аннотации можно получать доступ через ее индекс в массиве. В данном слу­ чае каждая аннотация MyAnno отображается через цикл fo r в ст иле "for-each': Некоторые огран ичени я Существует несколько ограничений, применяемых к объявлениям аннота­ ций. Во-первых, ни одна аннотация не может быть унаследована от другой. Во-вторых, все методы, объявленные аннотацией, не должны принимать па­ раметры. Кроме того, они обязаны возвращать один из перечисленных далее типов: • примитивный тип, такой как int или douЫ e; • объект типа String или Class; • объект типа enum; • объект типа другой аннотации; • массив одного из допустимых типов. Аннотации не могут быть обобщенными. Другими словами, они не могут принимать параметры типа. (Обобщения описаны в главе 14.) Наконец, в ме­ тодах аннотаций нельзя указывать конструкцию throws. ГЛ А ВА Ввод -вывод, о перато р try с рес у р сами и д ругие темы В настоящей главе представлен один из самых важных пакетов Java j ava . io, который поддерживает базовую систему ввода-вывода Java, вклю­ чая файловый ввод-вывод. По мержка ввода-вывода обеспечивается основ­ ными библиотеками Java API, а не ключевыми словами языка. По указанной причине подробное обсуждение данной темы можно найти в части II книги, где рассматриваются несколько пакетов Java API. Здесь представлена осно­ ва этой важной подсистемы, чтобы вы могли увидеть, как она вписывается в более широкий контекст программирования на Java и исполняющей среды Java. В главе также исследуются оператор try с ресурсами и несколько допол­ нительных ключевых слов Java: t ransient, vol a t i l e, ins tanceof, nat ive, strict fp и assert. В заключение обсуждается статическое импортирование и описано еще одно использование ключевого слова this. Основы ввода-вы вода Вероятно, вы заметили при чтении предшествующих 12 глав, что в при­ мерах программ ввод-вывод практически не применялся. Фактически ни один из методов ввода-вывода кроме print ( ) и print ln ( ) не использовался сколь-нибудь значительно. Причина проста: большинство реальных прило­ жений Java не являются текстовыми консольными программами. Напротив, они представляют собой либо графически-ориентированные программы, ко­ торые для взаимодействия с пользователем применяют одну из инфраструк­ тур Java для построения графических пользовательских интерфейсов, такую как Swing, либо веб-приложения. Хотя текстовые консольные программы превосходны в качестве обучающих примеров, как правило, с ними не свя­ заны какие-то важные сценарии использования Java в реальном мире. Кроме того, поддержка консольного ввода-вывода в Java ограничена и несколько неудобна в применении - даже в простых примерах программ. Текстовый консольный ввод-вывод просто не настолько полезен в реальном програм­ мировании на Java. Несмотря на то, что упоминалось в предыдущем абзаце, язык Java обеспе­ чивает мощную и гибкую поддержку ввода-вывода, касающуюся файлов и 368 Часть 1. Язык Java сетей. Система ввода-вывода Java характеризуется единством и непротиворе­ чивостью. На самом деле, как только вы поймете ее основы, остальную часть системы ввода-вывода освоить несложно. З десь предложен общий обзор вво­ да-вывода, а подробное описание приведено в главах 22 и 23. Потоки данных Ввод-вывод в программах на Java выполняется через потоки данных. Поток данных (stream) - это абстракция, которая либо производит, либо потребляет информацию. Лоток связ ан с физ и ческим устройством посред­ ством системы ввода-вывода Java. Все потоки ведут себя оди наково, даже если факти ческие физ и ческие устройства, с которыми они связ аны, раз ли­ чаются. Таким образом, одни и те же классы и методы ввода-вывода могут применяться к разным типам ус тройств, что оз начает воз можность абстраги­ рования входного потока от множества разли чных типов ввода: из дискового файла, клавиатуры или сетевого сокета. Точно так же поток вывода может относиться к консоли, д исковому файлу или сетевому подклю чению. Потоки данных являются чистым способом работы с вводом-выводом, при котором в каждой части вашего кода не требуется у чет отличий, например, между кла­ виатурой и сетью. Потоки данных Java реализованы внутри иерархий классов, определенных в пакете j а vа . io. На заметку! В дополнение к потоковому вводу-выводу, определенному в j а vа . i o, язык Java также предоставляет ввод-вывод на основе буферов и каналов, который определен в j ava . n i o и его подчиненных пакетах. Ввод-вывод такого вида обсуждается в главе 23. Потоки байтовых и сим вольных данных В Java определены два типа потоков ввода-вывода: байтовые и символь­ ные. Потоки байтовых данных предлагают удобные средства для обработки ввода и вывода байтов. Они используются, например, при чтении или з аписи двои чных данных. Потоки символьных данных предоставляют удобные сред­ ства для обработки ввода и вывода символов. Они применяют Unicode и, сле­ довательно, допускают интернационализ ацию. Кроме того, в ряде случаев по­ токи символьных данных эффективнее потоков байтовых данных. Первоначальная версия Java (Java 1.0) не вклю чала потоки символьных данных, поэтому весь ввод-вывод был ориентирован на байты. Потоки с и м­ вольных данных появились в верси и Java 1. 1, а некоторые классы и методы, ориентированные на байты, были объявлены нерекомендуемыми. Несмотря на то что унаследованный код, где потоки символьных данных не использ у­ ются, встре чается все реже, временами вы все еще можете с ним сталкивать­ ся. Как правило, унаследованный код должен быть обновлен, чтобы надлежа­ щим образом з адействовать преимущества потоков символов. Еще один момент: на самом низ ком у ровне все операции ввода-вывода по-прежнему ориентированы на байты. Потоки символьных данных просто обеспечивают удобный и эффективный инструмент для обработки символов. Гл а ва 1 3. Ввод-вывод, оператор tгу с р е сурсами и другие темы 369 Обзор потоков данных, ориентированных на байты и на символы, пред­ ставлен в последующих разделах. Кла ссы потоков б ай тов ых данн ых Потоки байтовых данных определяются с применением двух иерар­ хий классов. Вверху находятся два абстрактных класса: I nputSt ream и Outputstream. Каждый из них имеет несколько конкретных подклассов, ко­ торые справляются с отличиями между разными устройствами, такими как дисковые файлы, сетевые подключения и даже буферы памяти. Классы по­ токов байтовых данных из j ava . io, которые не объявлены нерекомендуемы­ ми, показаны в табл. 13. 1 . Некоторые из этих классов обсуждаются далее в разделе, а другие описаны в части II книги. Помните, что для использования классов потоков должен быть импортирован пакет j ava . i o. Табnица 1 3. 1 . Кnассы потоков байтовых данных в java . io, которые не обьявnены нерекомендуемыми BufferedinputStream Буферизованный поток ввода BufferedOutputStrearn Буферизованный поток вывода ByteArrayinputStrearn Поток ввода, который выполняет чтение из байтового массива ByteArrayoutputStrearn Поток вывода, который выполняет запись в байтовый массив DatainputStream Поток ввода, который содержит методы для чтения стандартных типов данных Java DataOutputStream Поток ввода, который содержит методы для записи стандартных типов данных Java Fi l e i nput Strearn Поток ввода, который выполняет чтение из файла Fi l eOutputStream Поток вывода, который выполняет запись в файл FilterinputSt ream Реализует InputStream Fi l te rOutputStrearn Реализует OutputStream InputStream Абстрактный класс, который описывает поток ввода Obj ectinputStream Поток ввода для объектов Obj ectOutputS tream Поток вывода для объектов OutputStream Абстрактный класс, который описывает поток вывода PipedinputStream Канал ввода 370 Част ь 1. Язык Java Окончание табл. 13.1 PipedOutputStream PrintStream Канал вывода Поток вывода, который содержит методы print ( ) и println ( ) PushbackinputStream Поток ввода, который позволяет возвращать байты в этот поток ввода SequenceinputStream Поток ввода, являющийся комбинацией двух и более потоков ввода, которые будут читаться последовательно друr за другом В абстрактных классах InputStream и OutputStream определено несколь­ ко ключевых методов, реализуемых другими классами потоков. Двумя наибо­ лее важными из них являются read ( ) и wri te ( ) , которые выполняют, соот­ ветственно, чтение и запись байтов данных. У каждого имеется абстрактная форма, которая должна быть переопределена в производных классах потоков. Кла ссы потоков с и м воль ных данных Потоки символьных данных определяются с помощью двух иерархий клас­ сов. Вверху находятся два абстрактных класса: Reader и Writer. Они обра­ батывают потоки символов Unicode. Для каждого из них в Java предусмотре­ но несколько конкретных подклассов. Классы потоков символьных данных в j ava . io описаны в табл. 13.2. Табnица 13.2. Кnассы потоков симвоnьных данных в j ava . io BufferedReader BufferedWriter CharArrayReader Буферизованный поток ввода символьных данных CharArrayWriter Поток вывода, который выполняет запись в символьный массив FileReader FileWriter Поток ввода, который выполняет чтение из файла FilterReader FilterWriter InputStreamReader Буферизованный поток вывода символьных данных Поток ввода, который выполняет чтение из символьного массива Поток вывода, который выполняет запись в файл Фильтрующее средство чтения Фильтрующее средство записи Поток ввода, который выполняет трансляцию байтов в символы Гnава 13. Ввод-вывод, оператор try с ресурсами и другие темы :; LineNumЬerReader OutputStreamWri ter PipedReader PipedWri ter PrintWriter PushЬackReader Reader StringReader StringWriter Writer ; 371 Окончание табл. 13.2 Поток ввода, которь1й подсчитывает строки Поток вывода, который выполняет трансляцию символов в байты Канал ввода Канал вывода Поток вывода, каrорый содержит методы print ( ) и println ( ) Паrок ввода, которь1й позволяет возвращать байты в этот поток ввода Абстрактный класс, описывающий поток ввода символьных данных Поток ввода, который выполняет чтение из строки Поток вывода, каrорый выполняет запись в строку Абстрактный класс, описывающий поток вывода символьных данных В абстрактных классах Reader и Wri ter определено несколько ключевых методов, реализуемых друrими классами потоков. Двумя наиболее важными методами считаются read ( ) и write ( ) , которые выполняют, соответственно, чтение и запись символов данных. У каждого есть абстрактная форма, кото­ рая должна переопределяться в производных классах потоков. Предопределен ные поток и данн ых Как вам известно, все проrраммы на Java автоматически импортируют па­ кет j ava . lang, в котором определен класс System, инкапсулирующий ряд аспектов исполняющей среды. Скажем, с помощью некоторых ero методов можно получить текущее время и настройки разнообразных свойств, связан­ ных с системой. Класс System также содержит три предопределенные пото­ ковые переменные: in, out и err. Они объявлены в System как поля puЫic, static и final, т.е. моrут использоваться в любой другой части проrраммы без привязки к конкретному объекту System. Поле System.'out ссылается на стандартный поток вывода. По умолчанию это консоль. Поле System . in ссылается на стандартный поток ввода, в каче­ стве которого по умолчанию выступает клавиатура. Поле System . err ссыла­ ется на стандартный поток вывода ошибок, по умолчанию также являющийся консолью. Однако упомянутые потоки могут быть перенаправлены на любое совместимое устройство ввода-вывода. 372 Часть 1. Язык Java Systern. in представляет собой объект типа InputStrearn, а Systern . out и Systern . err - объекты типа PrintStream. Это потоки байтовых данных, хотя обычно они применяются для чтения символов из консоли и записи символов на консоль. При желании вы можете поместить их внутрь потоков символьных данных. В предыдущих главах в примерах использовалось поле Systern . out. Практически аналогично можно применять Systern. err. Как объясняется в следующем разделе, использование Systern . in чуть сложнее. Чтение кон сольного ввода На заре развития Java единственным способом выполнения консольного ввода было применение потока байтовых данных. В настоящее время исполь­ зование потока байтовых данных для чтения консольного ввода по-прежнему часто приемлемо, например, в примерах программ. Тем не менее, в коммер­ ческих приложениях для чтения консольного ввода предпочтительнее при­ менять символьный поток, что облегчает интернационализацию и сопрово­ ждение программы. Консольный ввод в Java выполняется (прямо или косвенно) путем чте­ ния из Systern. in. Один из способов получения символьного потока, при­ соединенного к консоли, предусматривает помещение Systern. in в оболочку BufferedReader. Класс BufferedReader поддерживает буферизованный по­ ток ввода. Ниже приведен часто используемый конструктор: BufferedReader (Reader inputReader) Здесь inputReader представляет собой поток, связанный с создаваемым экземпляром BufferedReader, а Reader - абстрактный класс. Одним из ero конкретных подклассов является InputStrearnReader, который преобразует байты в символы. Начиная с версии JDK 17, точный способ получения объек­ та InputStrearnReader, связанного с Systern. in, изменился. В прошлом для этой цели обычно применялся следующий конструктор InputStrearnReader: InputStreamReader ( InputStream inputStream) Поскольку Systern . in ссылается на объект типа InputStrearn, ero мож­ но указывать в аргументе inputStream. Таким образом, приведенная далее строка кода демонстрирует ранее широко используемый подход к созданию объекта BufferedReader, подключенного к клавиатуре: BufferedReader Ьr = new Bufferec:IReader (new InputStreamReader ( Syзtem . in) ) ; После выполнения этоrо оператора переменная br становится символь­ ным потоком, связанным с консолью через Systern. in. Однако, начиная с JDK 17, при создании объекта InputStrearnReader рекомендуется явно указывать набор символов, ассоциированный с консо­ лью. Набор символов определяет способ сопоставления байтов с символами. Обычно, коrда набор символов не задан, применяется стандартная кодировка JVM. Тем не менее, в случае консоли набор символов, используемый для кон- Глава 1 3 . Ввод-вывод, оператор try с ресурсами и другие темы 373 сольного ввода, может отличаться от стандартного набора символов. Таким образом, теперь рекомендуется применять следующую форму конструктора InputStreamReader: InputStreamReader ( I nputStream inputStream, Charset набор-символов ) В аргументе charset должен использоваться набор символов, ассоцииро­ ванный с консолью, который возвращается с помощью charset ( ) - ново­ го метода, добавленного к классу Console в JDK 17 (см. главу 22). Объект Console получается вызовом метода System . console ( ) , который возвраща­ ет ссылку на консоль или null, если консоль отсутствует. Следовательно, те­ перь показанная ниже кодовая последовательность демонстрирует один из способов помещения S ystem . in в оболочку Bu fferedReader: Console con = Sys tem . console ( ) ; / / получить объект Console i f ( con==nu l l ) retur n ; // возврат, если консоль отсутствует BufferedReader br = new BufferedReader ( new I nputStreamReader ( System . in , con . charset ( ) ) ) ; Разумеется, в тех случаях, когда известно, что консоль будет присутство­ вать, последовательность можно сократить: Buf feredReader br = new Buffe redReade r (new InputStreamReader ( System . in, Sys tem . conso le ( ) . charset ( ) ) ) ; Поскольку для запуска примеров, рассматриваемых в книге, (очевидно) требуется консоль, мы будем применять именно такую форму. Еще один момент: можно также получить объект Reader, который уже ассоциирован с консолью, с помощью метода reader ( ) , определенного в Console. Однако мы будем использовать только что описанный подход с InputStreamReader, т.к. он явно иллюстрирует возможность взаимодействия потоков байтовых и символьных данных. Ч тение символов Для чтения символа и з Buffe redReader предназначен метод read ( ) . Вот версия read ( ) , которая будет применяться: int read ( ) throws IOException При каждом вызове метод read ( ) читает символ из потока ввода и воз­ вращает его в виде целочисленного значения. Он возвращает -1 при попыт­ ке чтения в конце потока. Как видите, он может сгенерировать исключение IOExcept ion. В представленной далее программе демонстрируется работа метода read ( ) , читающего символы с консоли до тех пор, пока пользователь не введет q. Обратите внимание, что любые исключения ввода-вывода, которые могут воз­ никнуть, генерируются в ma in ( ) . Такой подход является обычным при чтении из консоли в простых примерах программ вроде приведенных в этой книге, но в более сложных приложениях исключения можно обрабатывать явно. 374 Часть 1 . Язык Java / / Исполь зование объекта BufferedReader для чтения символов с консоли . import j ava . i o . * ; class BRRead { Ы i с static voi d ma i n ( S t ri n g ( ] args ) th rows IOException [ char с ; Buffe redReade r b r = new BufferedReader ( new I nputStr eamReader ( S ys tem . i n , Sys tem . conso l e ( ) . charset ( ) ) ) ; System . o ut . println ( "Bвoдитe символы; для выхода в ведите q . " ) ; / / Читать символы . do { с = ( ch a r ) b r . read ( ) ; System . o ut . print l n ( c ) ; wh i l e ( c != ' q ' ) ; Ниже показан пример вывода, полученного в результате запуска программы: Вводите символы; для выхода вв едите q . 1 2 3abcq 1 2 3 а ь с q Вы вод может не много отличаться от того, что вы ожидали, потому что Sys tem . in по умолчанию буферизирует строки, т.е. никакие входные данные фактически не пер едаются программе до тех пор, пока не будет нажата кла­ виша <Enter>. Несложно догадаться, что это не делает метод read ( ) особен­ но ценным для интерактивного ввода с консоли. Чтение с т р о к Для чтения строки с клавиатуры предназначена версия метода readLine ( ) , которая является членом класса BufferedReader со следующей общей фор­ мой: S t r i ng readLine ( ) th rows I OException Как видите, метод readLine ( ) возвращает объект String. В приведенной дал ее программе демонстрируется использование объекта BufferedReader и метода readLine ( ) ; программа читает и отображает стро­ ки текста до тех пор, пока не будет введено слово s t op: // Чтение строки с консоли с применением BufferedReade r . import j ava . i o . * ; class BRReadLi nes ( puЫ i c static voi d ma i n ( S t r ing [ ] a rgs ) throws IOExcept ion [ Глава 1 3 . Ввод-вывод, оп ератор try с р есурсами и други е т ем ы 37S // Создать объект Bu f fe redReade r , исполь зуя System . in . Bu f fe redReader br = new Bu f fe redReader ( new I nputSt reamReader ( System . i n , System . console ( ) . charset ( ) ) ) ; String s t r ; System . out . printl n ( "Bвoдитe строки текста . " ) ; System . out . println ( "Для завершения в ведите stop . " ) ; do { s t r = br . readLine ( ) ; Sys tem . out . print l n ( s t r ) ; while ( ! st r . e qual s ( " s top " ) ) ; В следующем примере строится крошечный текстовый редактор. В нем создается массив объектов S t ri ng, после чего читаются строки текста с сохранением каждой строки в массиве. Чтение выполняется вплоть до 100 строк или до ввода слова stop. Для чтения из консоли применяется объект Bu fferedReader. / / Крошечный те кстовый редактор . import j ava . io . * ; class Ti nyEdit { puЬ l i c static void main ( String [ ] a rg s ) throws I OException { / / Создат ь объект Bu f feredReade r , исполь зуя System . in . B u f fe redReader br = new Bu f fe redReade r ( new I nputSt reamReade r ( System . i n , System . console ( ) . charset ( ) ) ) ; String [ ] s t r = new St ring [ l 0 0 J ; System . out . p rint l n ( "Bвoдитe строки текста . " ) ; Sys tem . out . print l n ( "Для за вершения введите s top . " ) ; for ( i nt i=0 ; i < l 0 O ; i + + ) ( s t r [ i ] = br . re adLi ne ( ) ; i f ( st r [ i ] . equa l s ( " s top " ) ) break; System . out . println ( " \ nBoт то, что вы в вели : " ) ; / / Отобразить строки . for ( i nt i=0 ; i < l 0 0 ; i + + ) { i f ( s tr [ i ] . equa l s ( " stop" ) ) brea k ; System . out . println ( s tr [ i ] ) ; Ниже приведен пример вывода, полученного в результате запуска программы: Вводите строки текста . Для завершения введите stop . Первая строка . Вторая строка . Язык Java облегчает работу со строками . Просто со здайте объекты String . stop 376 Часть 1. Язык Java Вот то , что вы в вели : Первая строка . Вторая строка . Язык Java облегчает работу со строками . Просто создайте объекты St ring . З а п ись консол ь ного вывода Консольный вывод проще всего обеспечить с помощью описанных ранее методов print ( ) и println ( ) , которые используются в большинстве при­ меров книги. Упомянутые методы определены в классе P r i nt S t ream (тип объекта, на который ссылается System . out). Несмотря на то что System . out является потоком байтовых данных, применять его для простого вывода в программе по-прежнему приемлемо. Тем не менее, в следующем разделе опи­ сана альтернатива на основе символов. Поскольку класс PrintStream представляет собой выходной поток, про­ изводный от Output S t re am, он также реализует низкоуровневый метод wri te ( ) . Таким образом, wri te ( ) можно использовать для записи в консоль. Вот простейшая форма write ( ) , определенная в PrintStream: voi d write ( int byteval ) Метод wri te ( ) записывает байт, указанный в аргументе byteval. Хотя аргумент byteval объявлен как int, записываются только младшие восемь битов. Ниже показан краткий пример, в котором write ( ) применяется для вывода на экран символа "А'; а за ним символа новой строки: // Демонстрация исполь зования System . out . write ( ) . class WriteDemo { puЫ i c static void main ( String [ ] a rgs ) { int Ь ; Ь = ' А' ; System . out . write ( b ) ; System . out . write ( ' \n ' ) ; Для вывода на консоль метод write ( ) будет использоваться нечасто (не­ смотря на его удобство в ряде ситуаций), т.к. значительно проще применять методы print ( ) и println ( ) . К ласс PrintWri ter Хотя использовать Sys tem . o u t для записи в консоль вполне допустимо, вероятно, его лучше всего применять для целей отладки или в примерах про­ грамм, подобных тем, которые можно найти в этой книге. В реальных програм­ мах на Java рекомендуется осуществлять запись в консоль посредством потока PrintWri ter - одного из символьных классов. Использование символьного класса для консольного вывода упрощает интернационализацию программы. Глава 1 3 . Ввод-вывод, о ператор try с р е сурсами и други е т е мы 377 В классе PrintWriter определено несколько конструкторов; один из них, с которым мы будем иметь дело, показан ниже: PrintWrite r ( OutputSt ream output S tream, boolean flushingOn ) Здесь outputStream является объектом типа OutputStream, а flushingOn определяет, будет ли поток вывода очищаться при каждом вызове метода p ri nt l n ( ) (среди прочих). Если в аргументе flushi ngOn указано з начение true, тогда очистка выполняется автоматически, а если fal se, то очистка ав­ томатически не происходит. Класс P ri ntWriter поддерживает методы p ri nt ( ) и p ri ntl n ( ) . Таким образом, эти методы можно применять тем же способом, как они использо­ вались с S ystem . out. Если аргумент не относится к простому типу, тогда ме­ тоды PrintWriter вызывают метод to St r i ng () объекта и затем отображают рез ультат. Чтобы выполнить з апись в консоль с применением Pri ntWriter, укажите S ystem . out для потока вывода и автоматической очистки. Скажем, следую­ щая строка кода соз дает объект PrintWriter, подключенный к консольному выводу: PrintWriter pw = new PrintWrite r ( Sys tem . ou t , true ) ; В показ анном далее приложении иллюстрируется использование объекта PrintWriter для обработки консольного вывода: / / Демонстрация применения PrintWrit er . import j ava . io . * ; puЫ i c class Pr intWriterDemo { puЫ i c static void ma i n ( S t ring [ ] a rgs ) { PrintWriter pw = new PrintWriter ( Sys tem . out, true ) ; pw . p rint l n ( "Tecтoвaя строка " ) ; int i = - 7 ; pw . println ( i ) ; douЫe d = 4 . 5е - 7 ; pw . p rintl n ( d ) ; Вот вывод, генерируемый программой: Тестовая строка -7 4 . SE- 7 Помните, что нет ничего плохого в том, чтобы применять S ystem . out для вывода простого текста на консоль, когда вы из учаете Java или отлаживаете свои программы. Однако использование PrintWriter облегчает интернаци­ онализ ацию реальных приложений. Из -з а того, что применение P r i ntWriter в примерах программ, предложенных в книге, не дает н икаких преимуществ, мы будем продолжать использовать System . out для выполнения з аписи в консоль. 378 Часть 1. Язык Java Ч тение ф айлов и запись в ф айлы В Java предлагается несколько классов и методов, которые позволяют осу­ ществлять чтение файлов и запись в файлы. Прежде чем мы начнем, важно отметить, что тема файлового ввода-вывода довольно обширна и подробно исследуется в части II. Цель настоящего раздела в том, чтобы представить основные методы чтения и записи в файл. Несмотря на применение потоки байтовых данных, такие приемы могут быть адаптированы к символьным по­ токам. Двумя наиболее часто используемыми класса ми потоков являются File inpu t S t ream и Fi leOutputSt ream, которые создают потоки байтовых данных, связанные с файлами. Для открытия файла нужно просто создать объект одного из этих классов, указывая имя файла в качестве аргумента конструктора. Хотя оба класса поддерживают дополнительные конструкто­ ры, мы будем применять следующие формы: Fi l e i nputSt ream ( S tring fileName ) th rows FileNot FoundException Fi leOutputSt ream ( S tring fileName ) throws FileNot FoundException В аргументе f i l eName указывается имя файла, который необходимо от­ крыть. Если при создании входного потока файл не существует, тогда гене­ рируется исключение FileNot FoundExcept ion. В случае потока вывода ис­ ключение Fil eNot FoundExcept ion генерируется, если файл не может быть открыт или создан. Исключение Fi leNotFoundException является подклас­ сом IOException. Когда выходной файл открывается, любой ранее существо­ вавший файл с таким же именем уничтожается. На заметку! В ситуациях, когда присутствует диспетчер безопасности, несколько классов файлов, в числе которых Fi l e i np u t S t r e am и F i l e O u t p u t S t r e am, будут генерировать исключение S e c u r i t yExcepti on, если при попытке открытия файла произойдет нарушение безопасности. По умолчанию приложения, запускаемые через j а v а, не используют диспетчер безопасности. По этой причине в примерах ввода­ вывода, приведенных в книге, нет необходимости отслеживать возможное исключение S e c u r i t yE x c e p t i on. Тем не менее, другие типы приложений могут работать с диспетчером безопасности, и файловый ввод-вывод, выполняемый таким приложением, способен генерировать исключение Securi t yExcep t i on. В таком случае вам придется соответствующим образом обработать данное исключение. Имейте в виду, что в версии JDK 1 7 диспетчер безопасности объявлен устаревшим и подлежащим удалению. Завершив работу с файлом, вы должны его закрыть, что делается вызо­ вом метода close ( ) , который реализован как в F i l e i nputSt ream, так и в FileOutputS tream: vo id close ( ) throws I OException Закрытие файла приводит к освобождению системных ресурсов, выделен­ ных для файла, что позволяет их задействовать другим файлом. Из-за отказа от закрытия файла могут возникнуть "утечки памяти'; причиной которых яв­ ляются оставшиеся выделенными неиспользуемые ресурсы. Гла ва 1 3. Ввод-вывод, о пер а тор try с ресурс ами и друг ие темы 379 На заметку! Метод c l o s e ( ) оп ределен в интерфейсе Au t oC l o s e a Ы e внутри пакета j ava . l ang. Интерфейс AutoC l o s e a Ы e унаследован интерфейсом C l o s e a Ы e в j а va . i o. Оба интерфейса реализуются класса м и потоков, в то м числе Fi l e Input S t ream и F i leOutpu t S t ream. Прежде чем двигаться дальше, важно отметить, что существуют два основ­ ных подхода, которые можно применять для закрытия файла по завершении рабо ты с ним. Первый - традиционный подход, при котором метод close ( ) вызывается явно, когда файл больше не нужен. Такой подход использовал­ ся во всех версиях Java перед выходом JDK 7 и потому встречается во всем унаследованном коде до JDK 7. Второй подход предусматривает применение появившегося в JDK 7 оператора try с ресурсами, который автоматически за­ крывает файл, когда в нем отпадает нео бходимо сть. При это м подходе явный вызов close ( ) не производи тся. Поскольку вы все еще може те столкнут ь­ ся со унаследованным кодом, написанным до JDK 7, важно знать и понимать традиционный подход. Кроме того, в некоторых си туациях традиционный подход все еще может оказаться наилучшим и потому имеет смысл начать именно с него. Автоматизированный подход описан в следующем разделе. Для чтения из файла можно использовать версию read ( ) , о пределенную в F i l e l nputStream, которая показана ниже: int read ( ) throws IOException Каждый раз, когда метод read ( ) вызывается, он чи тает один байт из фай­ ла и возвращает его в виде целочисленного значения. При попытке чтения в конце потока read ( ) возвращает - 1 и также может сгенериро вать исключе­ ние IOExcept ion. В приведенной далее программе ме тод read ( ) применяется для ввода и ото бражения содержимого файла, содержащего текст ASCII. Имя файла ука­ зывается в качестве аргумента командной строки. / * Отображение содержимо го те кстового файла . Для использования программы укажите имя файла , который хотите просмотреть . Например , чтобы увидеть содержимое файла по имени TEST . TXT , введите следующую командную строку : */ j ava ShowFile TEST . ТХТ import j ava . i o . * ; class ShowFile { puЫic static void main ( St r i пg [ ] a rgs ) { iпt i ; Fi l e i пputStream f i п ; / / Удостовериться, ч т о имя файла бьt110 указано . i f ( a rgs . leпgth ! = 1 ) { Sys tem . out . priпtln ( "Исполь зование : Show File имя-файла " ) ; retur n ; / / Попыта т ь ся открыть файл . 380 Часть 1. Яз ык Java try ( fin = new FileinputStream ( args [ 0 ] ) ; catch ( FileNotFoundExcep tion е ) ( System . ou t . println ( "He удалось открыть файл . " ) ; return ; / / В данной точке файл открыт и может быть прочитан . / / Следующий код читает символы до тех пор , пока не встретится EOF . try ( do ( i = fin . read ( ) ; i f ( i ! = - 1 ) System . out . print ( ( char ) i ) ; while ( i ! = - 1 ) ; catch ( IOException е ) ( System . out . println ( "Oшибкa при чтении файла . " ) ; / / Закрыть файл . try ( fin . close ( ) ; catch ( IOException е ) System . out . println ( "Oшибкa при закрытии файла . " ) ; Обратите внимание в п рограмме на блоки try/ catch, которые обраба­ тывают возможные ошибки ввода-вывода. Каждая операция ввода-вывода отслеживается на предмет наличия исключений, и если исключение все же возникает, то оно обрабатывается. Имейте в виду, что в п ростых программах или примерах кода исключения ввода-вывода обычно п росто генерируются в методе main ( ) , как делалось в более ранних п римерах консольного ввода-вы­ вода. Вдобавок в реальном коде иногда удобно позволить исключению р ас­ пространяться в вызывающую п роцедуру, тем самым сообщая вызывающей стороне о том, что операция ввода-вывода не удалась. Однако в большинстве п римеров файлового ввода-вывода, рассматриваемых в книге, все исключе­ ния ввода-вывода обрабатываются явно, как показано в целях иллюстрации. Хотя в предыдущем п римере файловый поток закрывается после того, как файл был прочитан, существует вариант, который часто бывает полезным. Вариант предусматривает вызов метода c l ose ( ) внутри блока f inal l y. При таком подходе все методы доступа к файлу содержатся в блоке try, а блок f inal l y используется для закрытия файла. В итоге независимо от того, ка­ ким образом завершится блок try, файл будет закрыт. Вот как можно пере­ делать блок try из п редыдущего п римера, который читает файл: try ( do ( i = fin . read ( ) ; i f ( i ! = - 1 ) System . out . print ( ( char ) i ) ; while ( i ! = - 1 ) ; catch ( I OException е ) ( System . out . println ( "Ошибка при чтении файла . " ) ; Глава 1 3. Ввод- вывод , о пера то р try с ресурсами и др угие т емы 381 finally { / / Закрыть файл при выходе из блока try . try { fin . close ( ) ; catch ( I OExcept i on е ) { System . out . println ( " Ошибка при за крытии файла . " ) ; Хотя в данном случае это не проблема, одно преимущество такого подхода в целом связ ано с тем, что если код, который получает дост уп к файлу, з авер­ шается из-з а какого-либо исклю чения, не связ анного с вводом-выводом, то файл все равно з акрывается в блоке finall y. Временами проще поместить те части программы, которые открывают файл и получают к нему дост у п, в один блок try (вместо их раз несения), а з атем применить блок finally для з акрытия файла. Например, вот еще один способ написания программы ShowFi le: /* Отображение содержимого текстового файла . Для использования программы укажите имя файла , который хотите просмотреть . Например , чтобы увидеть содержимое файла по имени TEST . TXT , введите следующую командную строку : j ava ShowFile TEST . TXT */ В этом в арианте код, который открывает и получает доступ к файлу, помещен в один блок t r y . Файл закрывается в блоке final l y . import j ava . i o . * ; class ShowFil e { puЫ i c s tatic void ma in ( String [ ] args ) { i nt i ; File inputSt ream fin = nul l ; / / Удостоверить с я , что имя файла было указано . i f ( a rgs . length ! = 1 ) { System . out . println ( "Иcпoльзoвaниe : ShowFile имя-файла " ) ; retur n ; / / Следующий код открывает файл , читает символы д о т е х пор , пока / / не в стретится EOF, и затем закрывает файл через блок fina l l y . try { f i n = new FileinputS tream ( args [ 0 ] ) ; do { i = fin . read ( ) ; i f ( i ! = - 1 ) System . out . print ( ( ch a r ) i ) ; whi l e ( i ! = - 1 ) ; catch ( Fi l eNotFoundException е ) { System . ou t . println ( "Фaйл не найден . " ) ; catch ( I OExcept ion е ) { Sys tem . out . println ( " Возникла ошибка в вода-вывода . " ) ; finally { 382 Часть 1. Язык Java // Закрыть файл во всех случаях . try { i f ( fi n 1 = nul l ) fin . close ( ) ; catch ( I OExcept ion е ) { System . out . println ( " Ошибка при за крытии файла . " ) ; Обратите внимание, что при таком подходе переменная f i n инициализи­ руется значением nul l. В блоке finally файл закрывается только в том слу­ чае, если значение fin не равно nul l. Прием работает, потому что fin не бу­ дет null, только если файл успешно открыт. Таким образом, метод close ( ) не вызывается, если при открытии файла возникло исключение. Последовательность try/ catch в предыдущем примере можно сделать чуть более компактной. Поскольку исключение F i l eNot FoundExcept i on является подклассом IOExcept ion, его не нужно перехватывать отдельно. Например, вот последовательность, переписанная с целью устранения пере­ хвата F i l eNot FoundExcept ion. В данном случае отображается стандартное сообщение об исключении, описывающее ошибку. try { f i n = new F i l e i nputStream ( a rgs [ 0 ] ) ; do { i = fin . read ( ) ; i f ( i ! = - 1 ) System . out . print ( ( char ) i ) ; while ( i ! = - 1 ) ; catch ( IOException е ) { System . out . println ( " Oшибкa в вода-вывода : " + е ) ; finally { / / Закрыть файл во всех случаях . try { i f ( fin ! = nul l ) fin . close ( ) ; catch ( I OException е ) { System . out . println ( " Ошибка при за крытии файла . " ) ; При таком подходе любая ошибка, в том числе ошибка, связанная с от­ крытием файла, просто обрабатывается одним оператором catch. Из-за сво­ ей компактности этот подход используется во многих примерах ввода-выво­ да, представленных в книге. Тем не менее, имейте в виду, что такой подход не подходит в ситуациях, когда вы хотите отдельно обрабатывать отказ при открытии файла, такой как в случае, если пользователь неправильно набрал имя файла. В ситуации подобного рода вы можете запросить правильное имя, скажем, перед входом в блок try, где производится доступ к файлу. Для выполнения записи в файл можно применять метод wri te ( ) , опреде­ ленный в Fi leOutputSt ream. Ниже показана его простейшая форма: voi d write ( i nt byteva l ) throws I OExcept ion Гла ва 1 3. Ввод-вывод, оп ератор try с рес у рсами и др уг ие т емы 383 Метод wri te ( ) з аписывает в файл байт, указ анный в аргументе byteval. Хотя арг умент byteval объявлен к а к int, з аписываются только младшие во­ семь битов. Если во время записи воз никает ошибка, тогда генерируется ис­ ключение IOException. В следующем примере метод wri te ( ) используется для копирования файла: /* Копирование файла . Для использования программы укажите имена исходного и целевого файло в . Наприме р , чтобы скопировать файл по имени F I RS T . TXT в файл по имени SECOND . TXT , введите следующую командную строку : */ j ava CopyFi le FI RST , TXT SECOND . TXT import j ava . io . * ; class CopyFi le { puЬl i c static vo id ma i n ( String [ ] args ) throws I OExcept ion { int i ; F i l e i nputStream f i n = nu l l ; FileOutputStream fout = nu l l ; / / Удостовериться, что были указаны оба файла . i f ( a rgs . length ! = 2 ) { System. out . println ( "Использование : CopyFile исходный-файл целевой-файл" ) ; return ; / / Колировать файл , try { / / Попытаться открыть файлы . fin = new Fi l e i nputS tream ( args [ 0 J ) ; fout = new FileOutpu t S t ream ( args [ l ] ) ; do { i = f i n . read ( ) ; i f ( i ! = - 1 ) fout . wr i te ( i ) ; while ( i 1 = - 1 ) ; catch ( IOException е ) { Sys tem . out . println ( " Ошибка ввода- вывода : " + е ) ; fina l l y { try { if ( fi n ! = nul l ) fin . close ( ) ; catch ( I OException е 2 ) { System . out . println ( " Oшибкa при за крытии исходного файла . " ) ; try { i f ( fout ! = nul l ) fout . c lose ( ) ; catch ( I OException е2 ) { System . out . println ( " Oшибкa при закрытии целевого файла . " ) ; 384 Часть 1. Язык Java Обратите внимание, что при з акрытии файлов в программе применяются два отдельных блока t ry, гарантируя з акрытие обоих файлов, даже если вы­ зов f in . close ( ) сгенерирует исклю чение. Следует отметить, что в двух предыдущих программах все потенциальные ошибки ввода-вывода обрабатывались с помощью исклю чений. Это отлича­ ется от ряда других языков программирования, где для сообщения об ошиб­ ках, связанных с файлами, используются коды ошибок. Исклю чения не толь­ ко делают обработку файлов яснее, но и позволяют легко отличать состояние конца файла от файловых ошибок при выполнени и ввода. А вто матическое закрытие ф айла В предыдущем разделе внутри примеров программ явно вызывался ме­ тод close ( ) для з акрытия файла, когда в нем ис чезала необходимость. Как уже упоминалось, именно так файлы з акрывались в версиях Java до JDK 7. Хотя этот подход по-прежнему актуален и полезен, в JDK 7 появилось сред­ ство, которое предлагает другой способ управления ресурсами, такими как файловые потоки, путем автоматиз ации процесса з акрытия. Средство, ино­ гда называемое автоматическим управлением ресурсами (automatic resource management - ARM), основано на расширенно й версии оператора t ry. Главное преимущество автоматического управления ресурсами связано с тем, что оно предотвращает си туации, в которых файл (или другой ресурс) по не­ внимательности не освобождается после того, как он больше не нужен. Р анее уже объяснялось, что игнорирование з акрытия файла может привести к утеч­ кам памяти и прочим проблемам. Автоматическое управление ресурсами основано на расширенной форме оператора t ry: t ry ( спецификация - ре сурса ) / / исполь зовать ресур с Как правило, спецификация ресурса представляет собой оператор, кото­ рый объявляет и инициализ ирует ресурс, скажем, файловый поток. Он со­ стоит из объявления переменной, в котором переменная инициализируется ссылкой на управляемый объект. Когда блок try з акан чивается, ресурс ав­ томати чески освобождается. В случае файла это оз начает автомати ческое з акрытие файла. (Соответственно нет необходимости явно вызывать метод close ( ) .) Конечно, такая форма t ry может также вклю чать конструкци и catch и finally. Она называется оператором t ry с ресурсами. На заметку! Начиная с JDK 9, спецификация ресурса в t r у также может состоять из переменной, которая была объявлена и инициализирована ранее в программе. Однако эта переменная должна быть фактически финальной, т.е. после предоставления начального значения новое значение ей не присваивалось. Гла ва 1 3 . Ввод-вы вод, оператор try с ресурсами и другие темы 385 Оператор try с ресурсами можно п рименять только с теми ресурсами, которые реализуют интерфейс Au toCloseaЫ e, упакованный в j ava . l ang. В интерфейсе AutoCloseaЫe определен метод close ( ) . Вдобавок интерфейс AutoCloseaЫe унаследован интерфейсом CloseaЫe в j ava . io. Оба интер­ фейса реализованы классами потоков. Таким образом, try с ресурсами мож­ но использовать при работе с потоками, в том числе с файловыми потоками. В качестве первого примера автоматического закрытия файла взгляните на переработанную версию программы ShowFi le: /* В этой версии программы ShowFile используется оператор try с ресурсами для автоматического закрытия файла после того , как он больше не нужен . */ import j ava . i o . * ; class ShowFi l e { puЫic static void main ( String [ ] args ) { int i ; / / Удостоверит ься , что имя файла было указано . i f ( args . l ength ! = 1 ) { System . out . p rintln ( "Иcпoль зoвaниe : ShowFile имя-файла " ) ; return; / / В следующем коде приме няется оператор try с ресурсами для открытия / / файла и затем его закрытия при покидании блока t r y . try ( F i l e i nputSt ream fin = new Fi l e i nputStream ( args [ 0 ] ) ) do { i = fin . read ( ) ; i f ( i ! = - 1 ) Sys tem . out . print ( ( char ) i ) ; whi le ( i ! = - 1 ) ; catch ( Fi l eNot FoundException е ) { System . out . println ( "Фaйл не найден . " ) ; catch ( IOException е ) { System . out . p rintln ( " Пpoизoшлa ошибка в вода-вывода . " ) ; Обратите в программе особое внимание на то, каким образом файл откры­ вается внутри оператора try: try ( File i nputS tream fin = new F i l e i nputSt ream ( a rgs [ 0 ] ) ) { Легко заметить, что в части спецификации ресурса оператора try объяв­ ляется объект File input St ream по имени fin, которому затем присваива­ ется ссылка на файл, открытый его конструктором. Таким образом, в данной версии программы переменная fin является локальной для блока try и соз­ дается при входе в него. Когда блок t ry заканчивается, поток, связанный с fin, автоматически закрывается неявным вызовом close ( ) . Явно вызывать метод close ( ) не понадобится, а потому не беспокойтесь о том, что вы за­ будете закрыть файл. В этом и состоит ключевое преимущество применения оператора try с ресурсами. 386 Часть 1. Яз ы к Java Важно понимать, что ресурс, объявленный в операторе t ry, неявно яв­ ляется f i na l , т.е. присваивать ему ресурс после его создания нельзя. Кроме того, область действия ресурса ограничена оператором try с ресурсами. Прежде чем двигаться дальше, полезно упомянуть о том, что начиная с JDK 10, при указании типа ресурса, объявленного в операторе try с ресурса­ ми, можно использовать средство выведения типов л окальных переменных. Для этого необходимо задать тип va r, в рез ул ьтате чего тип ресурса будет вы веден из его инициал из атора. Например, оператор try в предыдущей про­ грамме теперь можно з аписать так: try ( var fin = new Fil e i nputStream ( args [ 0 ] ) ) { Здесь для переменной fin выводится тип F i lei nputStream, потому что именно к такому типу принадлежит его инициализатор. Поскольку многие чи­ тател и имеют дело со средами Java, предшествующими JDK 10, в операторах t ry с ресурсами в оставшихся главах книги выведение типов применяться не будет, чтобы код мог работать для максимально возможного числ а читателей. Разумеется, в будущем вы должны рассмотреть возможность использования выведения типов в собственном коде. В одиночном операторе try вы можете управлять более чем одним ресур­ сом. Для этого просто отделяйте спецификации ресурсов друг от друга точ­ ками с запятой. Ниже представлен пример, где показанная ранее программа CopyF i l e переделана с целью применения одного оператора try с ресурсами для управления fin и fout. / * Версия CopyFil e , в которой исполь зуется оператор try с ресурсами . Здесь демонстрируется управление двумя ресурсами ( в данном случае файлами) с псмощью одного оператора try. */ import j ava . io . * ; class CopyFile { puЫ i c static void main ( String [ ] arg s ) throws IOException { int i ; / / Удостовериться, что были указаны оба файла . i f ( args . length ! = 2 ) { System. out . println 1 "Использование : CopyFile исходный-файл целевой-файл " ) ; return; / / Открыть и управлять двумя файлами посредством оператора try. try ( Fi l einputStream fin = new Fil e inputStream ( ar gs [ 0 ] ) ; F i leOutputStream fout = new FileOutputStream ( args [ l ] ) ) do { i = fin . read ( ) ; i f ( i 1 = - 1 ) fout . write ( i ) ; while ( i 1 = - 1 ) ; catch ( IOException е ) { Глава 1 3. В вод - вывод, о п ератор tгу с ре су р с ам и и другие темы 387 System . out . println ( "Oшибкa в вода-вывода : " + е ) ; О братите внимание на способ открытия в программе исходного и целево­ го файлов внутри блока try: try ( Fi l e i nputStream fin = new FileinputStream ( args [ 0 ] ) ; FileOutputS tream fout = new Fi leOutput Stream ( ar gs [ l ] ) ) // ... По окончани и такого блока try файлы f i n и fout будут з акрыты. Сравнив эту версию программы с предыдущей версией, вы з аметите, что она намного короче. Воз можность оптимиз ации исходного кода является дополнитель­ ным преимуществом автомати ческого управления ресурсами. Существует еще один аспект оператора t ry с ресурсами, о котором следу­ ет упомянуть. В общем с лучае при выполнении блока try возможна с итуация, когда искл ю чение внутри try приведет к воз никновению другого исключе­ ния во время з акрытия ресурса в конструкции f i na l ly. В случае "нормаль­ ного" оператора try исходное искл ю чение утрачивается, будучи вытеснен­ ным вторым исключением. Тем не менее, при использ овании оператора try с ресурсами второе исключение подавляется, но не утрачивается. Взамен оно добавляется в список подавленных исключений, ассоциированных с первым исключением. Список подавленных исключений можно пол учить с помощью метода getSuppressed ( ) , определенного в классе ThrowaЫe. Из-з а преимуществ, которые предлагает оператор try с ресурсами, он будет применяться во многих, хотя и не во всех примерах программ в на­ стоящем издании книги. В некоторых примерах по-прежнему и спольз уется традиционный подход к з акрытию ресурса. На то есть несколько при чин. Во­ первых, вы можете столкнуться с унаследованным кодом, который основан на традиционном подходе. Важно, чтобы все программисты на Java полно­ стью раз бирались в традиционном подходе при сопровождении унаследован­ ного кода и чувствовали себя комфортно. Во-вторых, возможно, что некото­ рые программ и сты в те чение какого-то времени продолжат работ у в среде, предшествующей JDK 7. В таких ситуациях расширенная форма try не будет доступной. Наконец, в-третьих, могут быть случаи, когда явное з акрытие ре­ с урса более целесообразно, нежели автомати ческий подход. По пере числен­ ным при чинам в ряде примеров, приводимых в книге, все еще применятся традиционный подход с явным вызовом метода close ( ) . Эти примеры не только иллюстрируют традиционную методику, но могут быть скомпилиро­ ваны и з апущены всеми читателями во всех средах. Помните! В нескольких примерах используется традиционный подход к закрытию файлов как средство иллюстрации данного приема, который часто встречается в унаследованном коде. Однако в новом коде обычно лучше применять а втоматизированный подход, поддерживаемый описанным ранее оператором try с ресурсами. 388 Часть 1. Язык Java Модификаторы transient и volatile В Java определены два интересных модификатора типов: trans ient и volatile. Такие модификаторы служат для обработки нескольких специали­ зированных ситуаций. Коrда переменная экземпляра объявлена как trans ient, ее значение не должно предохраняться при сохранении объекта, например: class Т { transient int а; // не предохраняется int Ь; // предохраняется Если объект типа Т записывается в область постоянного хранения, тогда содержимое а не будет сохранено, но сохранится содержимое Ь. Модификатор volatile уведомляет компилятор о том, что снабженная им переменная может быть неожиданно изменена другими частями программы. Одна из таких ситуаций связана с мноrопоточными программами. В мноrо­ поточной программе иногда одна и та же переменная совместно использу­ ется двумя и более потоками. Из соображений эффективности каждый по­ ток может хранить собственную закрытую копию такой общей переменной. Настоящая (или гл.авнан) копия переменной обновляется в разное время, ска­ жем, при входе в синхронизированный метод. Хотя подход подобного рода работает нормально, нередко он может оказаться неэффективным. В некото­ рых случаях действительно имеет значение лишь тот факт, что главная копия переменной всегда отражает ее текущее состояние. Чтобы убедиться в этом, просто укажите переменную как volatile, тем самым сообщив компилятору о необходимости использования главной копии переменной volatile (или, по крайней мере, о помержке любых закрытых копий в актуальном состоя­ нии с главной копией и наоборот). Кроме тоrо, доступ к общей переменной должен производиться в точном соответствии с порядком, которого требует программа. Введен и е в instanceof Иногда полезно знать тип объекта во время выполнения. Например, у вас может быть один поток выполнения, генерирующий объекты различных ти­ пов, и другой поток, который эти обьекты обрабатывает. В такой ситуации мо­ жет быть удобно, чтобы обрабатывающий поток знал типы всех объектов при их получении. Еще одна ситуация, коrда важно знать тип объекта во время вы­ полнения, связана с приведением. В Java недопустимое приведение становится причиной ошибки времени выполнения. Многие недопустимые приведения моrут быть обнаружены на этапе компиляции. Тем не менее, приведения, во­ влекающие иерархию классов, способны порождать недопустимые приведе­ ния, которые можно обнаружить только во время выполнения. Например, суперкласс по имени А может производить два подкласса с именами В и С. Таким образом, приведение объекта В к типу А или приведение объекта С к Гл ава 1 3 . В вод -вывод , оп е ратор try с р есурсам и и други е темы 389 типу А допустимо, но приведение объекта В к типу С (или наоборот) не яв­ ляется законным. Учитывая, что объект типа А может ссылаться на объек­ ты либо В, либо С, то как узнать во время выполнения, на какой тип объекта фактически осуществляется ссылка, прежде чем пытаться привести его к типу С? Им может быть объект типа А, В или С. Если это объект типа В, тогда сге­ нерируется исключение времени выполнения. Язык Java предлагает операцию времени выполнения instanceo f, позволяющую ответить на такой вопрос. Первым делом необходимо отметить, что операция instanceof в версии JDK 17 была значительно улучшена благодаря новому мощному средству, ос­ нованному на сопоставлении с образцом. В текущей главе обсуждается тра­ диционная форма операции instanceo f, а ее расширенная форма рассматри­ вается в главе 17. Ниже показана общая форма традиционной операции instanceo f: obj ref ins tanceof type Здесь obj re f представляет собой ссылку на экземпляр класса, а type тип класса. Если аргумент obj r e f относится к указанному типу или может быть к нему приведен, то результатом вычисления операции ins tanceof яв­ ляется t rue. В противном случае результатом будет false. Таким образом, instanceo f - это инструмент, с помощью которого программа может полу­ чать информацию о типе объекта во время выполнения. Работа операции instanceof демонстрируется в следующей программе: // Демонстрация работы операции ins tanceo f . class А { int i , j ; class В { int i , j ; class С extends А { int k ; class D extends А { int k ; class I nstanceOf puЫ i c s tati c void main ( St r i ng [ ] args ) { А а = new А ( ) ; В Ь = new В ( ) ; С с = new С ( ) ; D d = new D ( ) ; i f ( а instanceo f А) System . out . println ( "a является экземпляром А" ) ; i f ( b instanceo f В) System . out . println ( "b является экземпляром В " ) ; i f ( c instanceo f С ) System . out . println ( " c является экземпляром С " ) ; 390 Ч а ст ь 1. Язык Java i f ( c i n s tanceof А ) System . out . println ( " c я вляется экземпляром А" ) ; i f ( a i n s tanceof С ) System . out . println ( " a можно привести к С " ) ; System . out . p r i ntln ( ) ; / / Сравнить типы производных классов . А оЬ ; оЬ = d ; / / ссылка на d System . out . p rintln ( "ob теперь ссылается на d" ) ; i f ( ob i n s tanceof D ) System . out . p ri n t ln ( "ob я вляется экземпляром D " ) ; System . out . println ( ) ; оЬ = с ; / / ссыпка на с System . out . p rintln ( "оЬ теперь ссылается на с " ) ; i f ( ob i n s tanceof D ) System . out . println ( " оЬ можно при вести к D " ) ; else System . out . p r i n tl n ( "ob нельзя привести к D" ) ; i f ( оЬ instanceof А ) System . out . println ( "ob можно привести к А " ) ; System . out . p ri n tl n ( ) ; / / Все объекты могут быть приведены к Obj ect . i f ( a i n s tanceo f Ob j ec t ) System . ou t . p r intln ( " a можно при вести к Ob j ect " ) i f ( b i n s tanceo f Ob j ec t ) System . out . p ri n t ln ( "b можно привести к Ob j ect " ) i f ( c instanceof Ob j ect ) System . out . println ( " c можно привести к Ob j ect " ) i f ( d i n s tanceof Ob j ec t ) System . out . println ( " d можно привести к Obj ect " ) Вот вывод, выдаваемы й программой : а Ь с с является экземпляром А является экземпляром В является экземпляром С можно привести к А оЬ теперь ссылается на d оЬ я вляется экземпляром D оЬ теперь ссыпается на с оЬ нель зя привести к D оЬ можно привести к А а можно ь можно с можно d можно привести приве сти привести привести к к к к Obj ect Ob j ect Ob j ect Ob j ect ; ; ; ; Глава 13. В вод - вывод , о пе р а тор try с р е сурс а м и и друг ие те м ы 391 В большинстве простых программ операция instanceof не нужна, пото­ му что часто тип объекта, с которым приходится работать, известен. Однако операция ins tanceof может быть очень полезной для обобщенных подпро­ грамм, которые оперируют с объектами из сложной иерархии классов или созданы в коде, находящемся за пределами вашего прямого контроля. Как вы увидите, усовершенствования, касающиеся сопоставления с образцом, кото­ рые описаны в главе 1 7, упрощают применение instanceof. М од и ф икато р strictfp С выходом Java 2 несколько лет назад модель вычислений с плавающей точкой была слегка смягчена. В частности, новая модель не требовала усече­ ния ряда промежуточных значений, возникающих во время вычислений. В не­ которых случаях это предотвращало переполнение или потерю значимости. За счет добавления к классу, методу или интерфейсу модификатора strictfp можно гарантировать, что вычисления с плавающей точкой (и, следователь­ но, все усечения) будут выполняться точно так же, как в более ранних версиях Java. В случае модификации класса с помощью st rictfp все методы в классе тоже автоматически снабжаются модификатором s t rictfp. Тем не менее, на­ чиная с JDK 1 7, все вычисления с плавающей точкой являются строгими, а модификатор s trictfp устарел и больше не обязателен. Его использование теперь приводит к выдаче предупреждающего сообщения. В приведенном далее примере иллюстрируется применение s t rictfp для версий Java, предшествующих JDK 17. В нем компилятор Java уведомляется о том, что во всех методах, определенных в MyClass, должна использоваться первоначальная модель вычислений с плавающей точкой: strictfp class MyClass { / / . . . Откровенно говоря, большинству программистов никоrда не приходилось применять модификатор s t r i c t fp, потому что он воздействовал лишь на крайне небольшую группу проблем. Начиная с JDK 1 7, модификатор stricfp был объявлен устаревшим и теперь его использование при водит к выдаче предупреждающего сообщения. Помните! Собственные метод ы Временами, хотя и редко, может понадобиться вызвать подпрограмму, которая написана не на языке Java. Обычно такая подпрограмма существу­ ет в виде исполняемого кода для процессора и рабочей среды, т.е. являет­ ся собственным кодом. Например, необходимость в вызове подпрогра ммы собственного кода зачастую возникает из-за желания сократить время вы­ полнения. Или же может потребоваться задействовать специализированную стороннюю библиотеку, такую как пакет статистических расчетов. Однако поскольку программы на Java компилируются в байт-код, который затем 392 Ч ас ть 1. Язык Java интерпретируется (или компилируется на лету) исполняющей средой Java, вызов подпрограммы собственного кода из программы на Java выглядит не­ возможным. К счастью, такой вывод неверен. Язык Java предлагает ключевое слово nat i ve, которое применяется для объявления методов собственного кода. После объявления эти методы можно вызывать в программе на Java по­ добно любым другим методам Java. Чтобы объявить собственный метод, поместите перед ним модификатор nat ive, но не определяйте тело метода, например: puЫic native int meth ( ) ; После объявления собственного метода вы должны написать собственный метод и выполнить довольно сложную последовательность шагов для его связывания с вашим кодом на Java. За текущими подробностями обращайтесь в документацию по Java. И с польз ова н ие assert Еще одним интересным ключевым словом является a s s ert. Оно приме­ няется на стадии разработки программы для создания утверждения - ус­ ловия, которое должно быть истинным во время выполнения программы. Например, в программе может существовать метод, который всегда обязан возвращать положительное целочисленное значение. Вы можете проверить данный факт путем утверждения о том, что возвращаемое значение больше нуля, используя оператор assert, Если во время выполнения условие истин­ но, то никакие другие действия не предпринимаются. Тем не менее, если ус­ ловие ложно, тогда генерируется объект As sert ionError. Утверждения часто применяются на стадии тестирования, чтобы убедиться в том, что некоторое ожидаемое условие действительно удовлетворяется. В коде выпуска утверж­ дения обычно не используются. Ключевое слово as sert имеет две формы. Вот первая из них: ass ert condi tion; Здесь condition - это выражение условия, результатом вычисления ко­ торого должно быть булевское значение. Если результат равен true, то ут­ верждение истинно и никаких других действий не происходит. Если условие ложно, тогда утверждение терпит неудачу и генерируется стандартный объ­ ект As sert ionError. Ниже показана вторая форма as sert: assert condi tion : expr; В данной версии выражение expr представляет собой значение, которое передается конструктору As sert ionErro r. Это значение преобразуется в строковый формат и отображается в случае отказа утверждения. Как прави­ ло, в конструкции expr указывается строка, но допускается любое выраже­ ние не void, если оно определяет приемлемое строковое преобразование. Глава 1 3. Ввод-вывод, о пе ратор try с р е сурс а ми и други е т е мы 393 Далее приведен пример применения a s s e rt, в котором осуществляется проверка, что возвращаемое значение getnum ( ) является положительным. // демонстрация исполь зования as sert . class AssertDemo { static int val = 3 ; / / Возвращает целое число . static int getnum ( ) return val-- ; puЫic static void main ( String [ ] args ) { int n ; for ( int i=O ; i < 1 0 ; i++ ) { n = getnum ( ) ; as sert n > О ; / / потерпит неуда чу, когда n равно О Sys tem. out . println ( "n равно " + n ) ; Для включения проверки утверждений во время выполнения.понадобится указать параметр -еа. Скажем, чтобы включить утверждения для AssertDerno, введите следующую строку: j ava -еа As se rtDemo После компиляции и запуска программа выдаст такой вывод: n равно 3 n равно 2 n равно 1 Exception in thread "main" j ava . lang . AssertionError at As se rtDemo . main (As sertDemo . j ava : 1 7 ) Исключение в потоке ma in типа j a va . lang. AssertionError в Asser tDemo . та in (AssertDemo . j a va : 1 7) Внутри rna in ( ) организованы многократные вызовы метода getnurn ( ) , который возвращает целочисленное значение. Возвращаемое значение getnurn ( ) присваивается переменной n и затем проверяется с помощью сле­ дующего оператора as sert: assert n > О ; / / потерпит неудачу, когда n ра вно О Такой оператор assert потерпит неудачу, когда n равно О (что произойдет после четвертого вызова), и сгенерируется исключение. Ранее уже объяснялось, что разрешено указывать сообщение, которое бу­ дет отображаться в случае отказа утверждения. Например, если в предыду­ щей программе привести утверждение к следующему виду: as sert n > О : "n не является положитель ным ! " ; тогда сгенерируется показанный ниже вывод: 394 Ча ст ь 1 . Язык Java n равно 3 n равно 2 n равно 1 Except i on in th read "main" j ava . l ang . Ass ert ionError : n не является положитель ным ! at AssertDemo . main (Assert Demo . j ava : 1 7 ) Исключение в потоке main типа java . lang . AssertionErro r : п не является положитель ным! в AssertDemo . main (As sertDemo . java : 1 7) Один важный момент, связанный с утверждениями, который нужно по ­ нимать, касается того, что вы не должны полагаться на них при выполнении каких-либо действий, действит ельно требуемых программой. Причина в том, что обычно код выпуска будет выполняться с отключенными утверждениями. Например, взгляните на такой вариант предыдущей программы: // Неудачный способ исполь зования assert ! ! ! cla s s As sert Demo { / / Произвольное число . stati c i n t val = 3 ; / / Возвращает целое число . static int getnum ( ) return val -- ; puЬ lic static voi d ma in ( S t ring [ ] ar gs ) { i nt n = O ; for ( i nt i=O ; i < 1 0 ; i++ ) assert ( n = getnum ( ) ) > С ; / / Поступать так не рекомендуе тся ! System . out . println ( "n равно " + n ) ; В этой версии про граммы вызов getnum ( ) перенесен внутрь оператора ass ert. Хотя код благо получно работает, ко гда утверждения включены, он будет функциониро вать некорректно при отключенных утверждениях, пото­ му что вызов getnum ( ) никогда не выполнится! На самом деле т еперь пере­ менную n необходимо инициализировать, т.к. компилятор идентифицирует, что присвоить значение n внутри оператора as sert может не получиться. Утверждения могу т быть очень полезными, по скольку они упрощают вид проверки ошибок, распространенный на стадии разработки. Например, до появления ass ert, если в предыду щей программе вы хотели удо стовериться в том, что значение n было положительным, пришлось бы использовать та­ ку ю кодо вую последоват ельно сть: if(n < O) { System . out . println ( "n является отрицательным ! " ) ; ret urn ; / / или сгенерировать исключение Про верка с применением assert требует только одной строки кода. Кро ме того, удалять операторы as sert из кода выпу ска не понадобится. Глава 1 3. В вод - вывод , о п ера тор try с ресурсами и д ругие темы 395 Параметры включен ия и отк лючения п роверки утвержден ий При выполнении кода вы можете откл ючить проверку всех утверждений, использ уя параметр -da. Чтобы включить или отключить конкретный пакет (и все его подчиненные пакеты), необходимо указать его имя с тремя точками после опции -еа или -da. Скажем, для включения проверки утверждений в пакете MyPack применяйте следующую конструкцию: -ea : MyPack . . . Для отключения проверки утверждений в пакете My Pack использ уйте кон­ струкцию: -da : MyPack . . . С помощью параметра - е а и л и - da можно также указывать кл асс. Напри мер, вот как включить AssertDemo: -ea : As sertDemo Статическое импорти рование В состав языка Java вход ит средство статического импортирования, ко­ торое расширяет возможности клю чевого слова import. За счет снабжения оператора import ключевым словом stati c этот оператор можно применять для импортирования статических членов класса или интерфейса. В случае использования стати ческого импортирования к с татическим членам мож­ но получать дост уп напрямую по их именам, не уточняя именем класса, что упрощает и сокращает синтаксис, требующийся для работы со статическим членом. Чтобы понять полезность статического импортирования, давайте начнем с примера, где он не применяется. В следующей программе вычисляется гипо­ тенуза прямоугольного треугольника. Она использ ует два статических мето­ да из встроенного в Java класса Math, входящего в состав пакета j ava . lang. Первый метод - Маth . pow ( ) , который возвращает значение, возведенное в указанную степень, а второй - Math . sqrt () , воз вращающий квадратный ко­ рень своего аргумента. / / Вычисление гипоте нузы прямоугольного треугольни ка . class Hypot { puЫ i c sta t i c void main ( S tring [ ] args ) douЫe side l , s ide 2 ; douЫe hypot ; s ide l = 3 . 0 ; s i de2 = 4 . 0 ; / / Обратите внимание на то , что s qrt ( ) и pow ( ) должны / / быть уточнены именем их класса , т . е . Math . hypot = Math . sqrt ( Math . pow ( s ide l , 2 ) + Math . pow ( s ide 2 , 2 ) ) ; Ч ас ть 1 . Яз ык Java 396 Sys tem . ou t . println ( "При заданных длинах сторон " + sidel + " и " + s i de2 + " гипотенуза равна " + hypot ) ; Поскольку pow ( ) и sqrt ( ) являются стати ческими методами, они должны вызываться с применением и мени их класса Math, что приводит к несколько громоздкому вычислению гипотенузы: hypot = Math . s qrt (Math . pow ( s i de l , 2 ) + Math . pow ( s ide2 , 2 ) ) ; Как демонстрирует этот простой пример, необходимость указывать имя класса при каждом использовании pow ( ) либо sqrt ( ) (или любых других ме­ тодов класса Math, таких как s i n ( ) , cos ( ) и tan ( ) ) может стать утомитель­ ной. В новой версии предыдущей программы показано, каким образом можно избавиться от надоедливого указания имени класса через стати ческое импор­ тирование: // Использование статического импортирования для помещения / / sqrt ( ) и pow ( ) в область видимости . impo rt static j ava . lang . Math . sqrt ; import static j ava . lang . Math . pow ; // Вычислить гипотенузу прямоугольного треугольника . class Hypot { puЫic static void main ( S t r i ng [ ] args ) { douЫe s ide l , s i de2 ; douЬle hypot ; si de l = 3 . 0 ; s i de2 = 4 . 0 ; / / Методы sqrt ( ) и pow ( ) можно вызывать / / сами по себе , без имени их клас са . hypot = sqrt ( pow ( s i de l , 2 ) + pow ( s ide2 , 2 ) ) ; System . out . printl n ( "Пpи зада нных длинах сторон " + s i de l + " и " + side2 + " гипотенуза равна " + hypot ) ; В приведенной выше версии имена sqrt и pow помещаются в область видимости с помощью следующих операторов стати ческого импортирования: import static j ava . lang . Ma th . s qrt ; import static j ava . lang . Math . pow ; Такие операторы устраняют необходимость в уточнении sqrt ( ) или pow ( ) именем их класса. В итоге вычисление гипотенузы становится более удобным: hypot = s qrt ( pow ( s i de l , 2 ) + pow ( s ide2 , 2 ) ) ; Глава 1 3 . Ввод-вывод, о пе р а тор try с р е сурсами и други е т е мы 397 Как видите, код этого вида значительно удо бнее для восприятия. Есть дв е основные формы оператора import s t a t i c. Первая форма, ко ­ торая применялась в предыдущем примере, помещает в область видимости одиночное имя и показ ана ниже: irnport static pkg . type-narne . s tatic-mernbe r -narne ; В конструкции type- name указыв ается имя класса или интерфейса, со­ держащего нужный статический член. Полно е имя его пакет а определяет­ ся ко нструкцией pkg. Имя статическо го члена указывается в конструкции s ta t ic-memЬe r-name. Вторая форма оператора import static импортирует все статические чле­ ны заданного класса или интерфейса и выглядит следующим образом: irnport static pkg . type-narne . * ; Если вы собираетесь использовать много статических методов или полей, определенных в классе, тогда такая форма позволит помести т ь их в область видимости, не указывая каждый метод или поле по отдельности. Скажем, в предыдущей программе можно было бы применять следующий единствен­ ный оператор import, чтобы помести т ь в область видимости методы pow ( ) и sqrt ( ) (наряду со всеми остальными статическими членами класса Ма th): irnport static j ava . lang . Math . * ; Разумеется, статическое импортирование не ограничивается только клас­ сом Ма th или только методами. Напри мер, прив еденный ниже оператор import s tat i c помещает в область видимости статическо е поле System . out: irnport static j ava . lang . S ystern . out ; После этого оператора можно выводи ть данные на консоль без указ ания System: out . println ( "После импортирования Systern. out можно использовать out напрямую. " ) ; Эффективность т акого импортирования System . out является предметом споров. Хотя оно делает оператор короче, изучающим программу не сраз у становится ясно, что под out пони мается Sys tem . out. Еще один момент: в дополнение к импортированию статических членов классов и инт ерф ейсов, определенных в Java API, оператор import s t a t i c также можно использовать для импортирования статических членов классов и интерфейсов, которые создаете вы сами. Каким бы удобным ни было статическое импортирование, важно не злоупо­ треблять им. Не забывайте, что причиной организ ации библиотек Java в пакеты было стремление избежать конфликтов пространств имен. Импортирование статических членов приводит к их переносу в текущее пространство имен, тем самым ув ели чивая вероятность возникновения конфликтов между простран­ ствами имен и непреднамеренного сокрытия имен. Если статический элемент используется в программе один или два раза, то лучше его не импортировать. К тому же некоторые статические имена, т аки е как S y s t em . out, настолько уз наваемы, что импортировать их не имеет особого смысла. Статическое им- Часть 1. Язык Java 398 портирование предназначено для тех ситуаций, в которых статический член применяется многократно, скажем, при выполнении последовательности ма­ тематических расчетов. В сущности, вы должны использовать средство стати­ ческого импортирования, но не злоупотреблять им. Выз ов перегруженны х кон структоров чере з this О При работе с перегруженными конструкторами временами полезно, чтобы один конструктор вызывал другой. В языке Java для этого прим еняется дру­ гая форма ключевого слова this, общий вид которой показан ниже: thi s ( arg-l i s t ) При выполнении this ( ) первым запускается перегруженный конструктор со списком параметров, соответствующим тому, что указано в списке аргу­ ментов arg-l ist. Далее если внутри ис ходного конструктора им еются какие­ либо оп ераторы, то они выполняются. Вызов this ( ) должен быть первым оп ератором внутри конструктора. Чтобы лучше понять, как пользоваться this ( ) , давайте рассмотрим не­ боль шой пример. Для начала взгляните на следующий класс, где this ( ) не применяется: class MyClass int а ; int Ь ; / / Ин ициал изировать а и Ь п о отдельн о ст и . MyClass ( int i , int j ) { а = i; ь = j; / / И ниц иал изирова ть а и Ь о дним и т ем же зн а ч ением . MyClass ( int i ) { а = i; ь = i; / / Предо с тавит ь а и Ь с та нд арт ные зн ач е ни я О . MyClass ( ) { а = О; Ь = О; } Класс MyClass содержит три конструктора, каждый из которых инициали­ зирует поля а и Ь. Первому конструктору передаются индивидуальные значе­ ния для а и Ь. Второму передается только одно значение, которое присваива­ ется как а, так и Ь. Третий предоставляет полям а и Ь стандартные значения, равные нулю. Используя this ( ) , код MyClass можно переписать: class MyClass int а ; int Ь ; Глава 1 3 . В вод - вывод , оператор try с ресурса ми и другие темы 399 / / Инициализировать а и Ь по отдельности . MyCla s s ( int i , int j ) а = i; ь = j; / / Инициализировать а и Ь одним и тем же значением . MyCl ass ( int i ) { this ( i , i ) ; / / вызывается MyClass ( i , i ) / / Предоставить а и Ь стандартные значения О . MyClass ( ) { this ( O ) ; / / вызывается MyClas s ( O ) В этой версии MyClass единственным конструктором, который факти­ чески присваивает значения полям а и Ь, является MyClass (int , int) . Два других конструктора просто прямо или косвенно вызывают конструктор MyClass (int , int) через this () . Например, давайте выясним, что происхо­ дит при выполнении следующего оператора: MyClass mc = new MyClass ( B ) ; Вызов MyClass ( 8) инициирует выполнение this ( 8 , 8) , что транслируется в вызов MyClass ( 8 , 8) , поскольку это версия конструктора MyClass, список параметров которой соответствует аргумента м, переданным через this () . Теперь рассмотрим показанный н и же оператор, который задействует стан­ дартный конструктор: MyCl ass mc2 = new MyClass ( ) ; В данном слу чае вызывается this (О) , приводя к вызову MyClass ( 0) , по­ тому что это конструктор с совпадающим списком пара метров. Конечно, MyClass ( О ) затем вызывает MyClass ( О , О ) , как было описано выше. Одна из причин удобс тва вызова перегруженных конструк торов через this () связана с возможностью предотвращения ненужного дублирования кода. Во многих ситуациях сокращение повторяющегося кода уменьшает вре­ мя, необходи мое для загрузки класса, т.к. зачастую объектный код оказывает­ ся короче. Это особенно важно для программ, доставляемых через Интернет, когда время загрузки является проблемой. Применение this () также содей­ ствует структурированию кода, когда конструкторы содержат большой объ­ ем дублированного кода. Однако вы должны проявлять осторожность. Конструкторы, вызывающие this () , будут выполняться чуть медленнее, нежели те, в которые встроен весь код инициализации. Дело в том, что механизм вызова и возвращения, используемый при вызове второго конструктора, увеличивает накладные рас­ ходы. Если ваш класс будет применяться для создания лишь нескольких объ­ ектов или если конструкторы в к лассе, вызывающие this () , будут использо­ ваться редко, то такое снижение производительности во время выполнения, вероятно, окажется незна чительным. Но если ваш класс будет задействован 400 Часть 1. Язык Java для соз дания большого количества объектов (порядка тысяч) во время вы­ полнения программы, тогда негативное влияние увеличения накладных рас­ ходов может быть з начительным. Поскольку создание объекта влияет на всех пользователей вашего класса, будут случаи, когда вам придется тщательно взвешивать преим ущества более быстрой з агрузки по сравнению с увеличе­ нием времени, необходимого для создания объекта. Вот еще одно соображение: для очень коротких конструкторов вроде при­ меняемых в классе MyClass раз мер объектного кода часто невелик, независи­ мо от того, используется this ( ) или нет. (В действительности бывают ситу­ ации, когда размер объектного кода не уменьшается.) Это связано с тем, что байт-код, который настраивается и возвращается из вызова th is ( ) , добав­ ляет инструкции в объектный файл. Следовательно, даже если дублирован­ ный код в подобных ситуациях устранен, то применение this ( ) не обеспечит з начительную эконом ию времени з агруз ки. Тем не менее, дополнительные затраты в виде накладных расходов на строительство каждого объекта все равно будут понесены. Таким образом, this ( ) лучше всего применять к тем конструкторам, которые содержат большое количество кода инициализации, а не к конструкторам, просто устанавливающим з начение нескольких полей. Существуют два ограничения, которые необходимо учитывать при исполь­ зовании this ( ) . Во-первых, в вызове this ( ) нельзя применять переменную экземпляра класса конструктора. Во-вторых, в одном и том же конструкторе не разреше­ но использовать вызовы super ( ) и this ( ) , потому что каждый из них дол­ жен быть первым оператором в конструкторе. Несколько слов о классах, основанных на значен иях Начиная с J DK 8, в состав Java входит концепция класса, основанного на значении, и несколько классов в Java API поз иционируются как основанные на з начениях. Классы, основанные на з начениях, определяются согласно раз ­ нообраз ным правилам и ограничениям. Рассмотрим несколько примеров. Они должны быть финальными, как и их переменные экземпляра. Если метод equals ( ) устанавливает, что два экземпляра класса, основанного на з наче­ ниях, равны, то один экз ем пляр может применяться вместо другого. Кроме того, два одинаковых, но отдельно полученных экзем пляра класса, основан­ ного на з начении, могут фактически быть одним и тем же объектом. Очень важно избегать использования экземпляров класса основанного на з начении, для синхрониз ации. Применяются дополнительные правила и ограничения. Кроме того, определение классов, основанных на з начениях, со временем не­ сколько изменилось. Последние сведения о классах, основанных на з начени­ ях, а также о том, какие классы в библиотеке Java API помечены как основан­ ные на з начениях, можно узнать в документации по Java. ГЛ А ВА Об о бщен ия С момента выхода в 1995 году первоначальной версии 1.0 в Java появилось много новых средств. Одним из нововведений, оказавших глубокое и долго­ срочное влияние, стали обобщения. Будучи введенными в JDK 5, обобщения изменили Java в двух важных аспектах. Во-первых, они добавили в язык но­ вый синтаксический элемент. Во-вторых, они вызвали изменения во многих классах и методах основного API. На сегодняшний день обобщения являются неотъемлемой частью программирования на Java, а потому необходимо четко понимать это важное средство, которое подробно рассматривается в насто­ ящей главе. Благодаря использованию обобщений можно создавать классы, интерфей­ сы и методы, которые будут безопасно работать с разнообразными типами данных. Многие алгоритмы логически одинаковы независимо от того, к ка­ кому типу данных они применяются. Например, механизм, поддерживающий стек, будет одним и тем же, невзирая на то, элементы какого типа в нем хра­ нятся - Integer, String, Obj ect или Thread. С помощью обобщений можно определить алгоритм однократно и независимо от конкретного типа данных, после чего применять этот алгоритм к широкому спектру типов данных без каких-либо дополнительных усилий. Впечатляющие возможности добавлен­ ных в язык обобщений коренным образом изменили способ написания кода Java. Одним из средств Java, на которое обобщения оказали наиболее значи­ тельное воздействие, вероятно, следует считать инфраструктуру коллек­ ций (Collections Framework), которая входит в состав Java API и подроб­ но обсуждается в главе 20, но полезно кратко упомянуть о ней уже сейчас. Коллекция представляет собой группу объектов. В инфраструктуре Collections Framework определено несколько классов вроде списков и карт, которые управляют коллекциями. Классы коллекций всегда были способны работать с любыми типа ми объектов. Обобщения обеспечили дополнительное пре­ имущество - возможность использования классов коллекций с полной без­ опасностью в отношении типов. Таким образом, помимо того, что обобщения сами по себе являются мощным языковым элементом, они также позволяют 402 Ч ас ть 1 . Язы к Java существенно улучшить существующее средство. Это еще одна причина, по которой обобщения стали настолько важным дополнением к Java. В главе описан синтаксис, теория и методика применения обобщени й. В ней также показ ано, каким образом обобщения поддерживают безопас­ ность типов в некоторых ранее сложных случаях. После из учения тек у щей главы имеет смысл оз накомиться с гла вой 20, где рассматривается инфра­ структура Collections Framework. Там вы найдете множество примеров обоб­ щений в действии. Что такое обоб щен и я ? В своей основе термин обобщения оз начает параметризованные типы. Параметриз ованные типы важны, поскольку они поз воляют создавать клас­ сы, интерфейсы и методы, rде тип данных, с которым они работают, указ ы ­ вается в качестве параметра. Например, с использ ованием обобщений можно создать единственный класс, который автоматически работает с раз ными ти­ пами данных. Класс, интерфейс или метод, который оперирует на параметри­ з ованном типе, назы вается обобщенным. Важно понимать, что язык Java всегда предоставлял возможность созда­ вать обобщенные классы, интерфейсы и методы за счет опериро вания ссыл­ ками типа Obj ect. Поскольку Obj ect я вляется с уперклассом для всех дру­ гих классов, ссылка на Obj ect способна ссылаться на объект любого типа. Таким образ ом, в исходном коде обобщенные классы, интерфейсы и методы при работе с раз личными типами объектов з адействовали ссылки на Obj ect. Проблема з аключалась в том, что они не могли делать это с обеспечением безопасности в отношении типов. Обобщения добавили недостающую безопасность в отношении типов. Они также упростили процесс, т.к. больше не нужно явно использовать приведе­ ния типо в для преобразования между Obj ect и типом данных, с которым и фактически выполняются операции. Благодаря обобщениям все приведения становятся а втоматическими и неявными. В результате обобщения расшири­ ли возможности многократного применения кода с надлежащей безопасно­ стью и легкостью. Внимание! Предостережение для программистов на языке С++: хотя обобщения похожи на шаблоны в С++, они не совп ад а ют. Между этими двумя подхода ми к обобщенным тип а м существует ряд фундаментальных отличий. Если у вас есть опыт написания кода на С++, тогда важно не делать поспешных выводов о том, как работают обобщения в Java . П ростой п р и мер обоб щен и я Давайте начнем с простого примера обобщенного класса. В показ анной ниже программе определены два класса - обобщенный класс Gen и класс GenDemo, использ ующи й Gen. Глава 1 4. Обобще н ия / / Простой обобщенный класс . / / Здесь Т - параметр типа , который будет заменен // реальным типом при создании объекта типа Gen . class Gеп<Т> { Т оЬ ; / / объявить объект типа Т / / Передать конструктору ссыпку на объект типа Т . Gen ( T о ) { оЬ = о ; / / Возвратить оЬ . Т getOb ( ) { return оЬ ; / / Вывести тип т . void showType ( ) { System . out . println ( "Tипoм Т является " + ob . getClas s ( ) . getName ( ) ) ; / / Демонстрация приме нения обобщенного класс а . class GenDemo { puЫ i c static void mai n ( S tring [ ] args ) { / / Создать объект Gen для объектов типа I nteger . Gen<I nteger> iOb ; / / Создать объект Geп<Iпteger> и присвоить ссылку на него // переменной iOb , Обратите внимание на исполь зование автоупаковки / / для инкапсуляции значения 8 8 внутри объе кта I nteger . iOb = new Gen<I nteger> ( 8 8 ) ; / / Вывести тип данных , исполь зуемых переменной iOb . iOb . s howType ( ) ; / / Получить значение iOb . Обратите в нимание, // что приведение не требуется . int v = iOb . getOb ( ) ; S ystem . out . println ( " знaчeниe : " + v ) ; System . out . printl n ( ) ; / / Создать объект Gen для объектов типа S t r ing . Gen<Str i ng> st rOb = new Gen<Str ing> ( " Тест с обобщениями " ) ; / / Вывести тип данных , исполь зуемых переменной st rOb . strOb . showType ( ) ; / / Получить значение strOb . Снова обратите внимание , / / что приведение не требуетс я . Str ing str = strOb . getOЬ ( ) ; System . out . printl n ( "знaчeниe : " + s tr ) ; Программа выдает следующий вывод: Типом Т является j ava . l ang . I nteger значение : 8 8 Типом Т явл яется j ava . l ang . String значение : Тест с обобщениями 403 404 Ча с ть 1. Язык Java А теперь внимательно разберем программу. Прежде всего, обратите внимание на объявление Gen: class Gen<T> { Здесь Т - имя параметра типа. Оно прим еняется в качестве заполнителя для фактическоrо типа, который будет передан конструктору Gen при соз­ дании объекта. Таким образом, Т используется внутри Gen всякий раз, когда требуется парам етр типа. Обратите внимание, что Т содержится внутри уrло­ вых скобок <>. Та кой синтаксис можно обобщить. Объявляемый параметр типа всегда указывается в угловых скобках. Поскольк у Gen задействует па­ раметр типа, Gen является обобщенным классом, который также называ ется параметризованным типом. В объявлении Gen имя Т не играет особой роли. Можно было бы приме­ нить любой допустимый идентификатор, но Т используется по традиции. Кроме тоrо, реком ендуется выбирать имена для парам етров типов в виде односимвольных заглавных букв. Другими часто применяемыми именами па­ раметров типов являются V и Е. Еще один момент относительно имен параме­ тров типов: начиная с JDK 10, использовать var в качестве имени параметра типа не разрешено. Затем Т применяется для объявления объекта оЬ: Т оЬ; // объявить объект типа Т Как уже объяснялось, Т - это заполнитель для фактического типа, кото­ рый указывается при создании объекта Gen. Та ким образом , оЬ будет объек­ том типа , переданного в Т. Например, если в Т передается тип St ring, тогда оЬ получит тип String. Теперь взгляните, как выглядит конструктор класса Gen: Gen ( Т о ) оЬ = о ; Обратите внимание , что ero параметр о им еет тип Т, т.е. фактический тип о определяется типом, переданным в Т, когда создается объект Gen. Кроме того, поскольку и параметр о, и перем енная-член оЬ относятся к типу Т, при создании объекта Gen они будут иметь один и тот же фактический тип. Параметр типа Т также можно использовать для указания возвраща емого типа метода , как в случае показанного далее метода getOb ( ) : Т getOb ( ) { return оЬ ; Из-за того, что оЬ также имеет тип Т, ero тип совместим с возвращаемым типом, заданным getOb ( ) . Метод showType ( ) выводит тип Т, вызывая ge tName ( ) на объекте Class, который возвращается вызовом ge tClass ( ) на оЬ. Метод getClass ( ) опре­ делен в Obj ect и потому является членом всех типов классов. Он возвраща­ ет объект Class, соответствующий типу класса объекта , на котором вызы- Глава 14. Обобщения 405 вается. В Cla s s определен метод getName ( ) , который возвращает строковое представление имени класса. Работа с обобщенным классом Gen демонстрируется в классе Gen Demo. Сначала создается версия Gen для целых чисел: Gen< Integer > iOb ; Внимательно взгляните на приведенное выше объявление. Первым делом обратите внимание, что тип Integer в угловых скобках после Gen. В данном случае Integer - аргумент типа, который передается параметру типа Gen, т.е. т. Факти чески создается версия Gen, в которой все ссыл ки на Т транс­ л ируются в ссылки на Integer. Соответственно для такого объявления оЬ имеет тип Integer и возвращаемый тип getOb () имеет тип Integer. Прежде чем дви гаться дал ьше, важно отметить, что компилятор Java на самом деле не создает разные верси и Gen ил и л юбого другого обобщенного класса. Хотя думать в таких терм инах удобно, в действительности происходит иное - компилятор удаляет всю информацию об обобщенном типе, заменяя необходимые приведения, чтобы код вел себя так, как если бы создавалась кон­ кретная версия Gen. Таким образом, в программе действительно существует только одна версия Gen. Процесс удаления информации об обобщенном типе называется стиранием, и позже в главе мы еще вернемся к этой теме. В следующей строке кода переменной iOb присваивается ссылка на экзем­ пляр верси и класса Gen для Integer: iOb = new Gen < I ntege r> ( B B ) ; Обратите внимание, что при вызове конструктора кл асса Gen также ука­ зывается аргумент типа Integer. При чина в том, что объект (в данном сл у­ чае iOb), которому присваивается ссыл ка, и меет тип Gen< Integer>. Таким образом, ссылка, возвращаемая операцией new, тоже должна иметь тип Gen< Integer>. В противном сл у чае возникнет ошибка на этапе компиляции. Скажем, показанное ниже присваивание вызовет ошибку на этапе компиля­ ции: iOb = new Gen<DouЫ e > ( B B . 0 ) ; / / Ошибка ! Поскольку переменная iOb относится к типу Gen< Integer>, ее нельзя применять для ссылк и на объект Gen <DouЬle>. Эта проверка типов является одним из основных преимуществ обобщений, т.к. она обеспечивает безопас­ ность типов. На заметку! Позже в главе вы увидите, что синтаксис, используемый для создания экземпляра обобщенного класса, можно сократить. В интересах ясности будет применяться полный синтаксис. Как было указано в ком ментариях внутри программы, присваивание за­ действует автоупаковку для инкапсуляции значения 8 8 , представляющего со­ бой числ о типа int, в объект Integer: iOb = new Gen<I ntege r> ( B B ) ; 406 Ч а с т ь 1 . Я з ык Java Прием работает, потому что в классе Gen< I nteger> определен конструк­ тор, который принимает аргумент типа I nteger. Так как ожидается объ­ ект I nteger, компилятор Java автоматически поместит в него значение 8 8 . Разумеется, присваивание можно было бы записать и явно: i Ob = new Gen < I ntege r> ( Intege r . valueO f ( B B ) ) ; Однако эта версия кода не принесет никакой пользы. Далее программа выводит тип оЬ внутри iOb, т.е. I nteger, и затем полу­ чает значение оЬ с помощью следующей строки: int v = i Ob . getOb ( ) ; Поскольку возвращаемым типом getOb ( ) является Т, замененный типом I nteger при объявлении iOb, возвращаемым типом getOb ( ) также оказы­ вается I nteger, который распаковывается в int, когда выполняется при­ сваивание переменной v (типа int). Таким образом, нет нужды приводить возвращаемый тип getOb ( ) к Integer. Конечно, использовать средство ав­ тораспаковки не обязательно. Предыдущую строку кода можно было бы за­ писать и так: int v = iOb . getOb ( ) . intValue ( ) ; Тем не менее, средство автораспаковки содействует компактности кода. Затем в классе GenDemo объявляется объект типа Gen<String>: Gen<Stri ng> st rOb = new Gen<St r i ng> ( "Тест с обобщениями " ) ; Поскольку аргументом типа является String, внутри Gen тип String под­ ставляется вместо т, что (концептуально) создает версию Gen для String, как демонстрируют оставшиеся строки в программе. Обобщения работают тол ь ко с о ссылочн ым и типами При объявлении экземпляра обобщенного типа передаваемый параметру типа аргумент типа должен быть ссылочным типом. Примитивный тип вро­ де int или char применять нельзя. Скажем, для экземпляра Gen передать в Т можно любой тип класса, но передавать параметру типа какой-либо при­ митивный тип не разрешено. По этой причине следующее объявление будет незаконным: Gen<int> i ntOb = new Gen<int> ( 5 3 ) ; / / Ошибка , нельзя использовать / / примитивный тип Конечно, отсутствие возможности указать примитивный тип не считается серьезным ограничением, т.к. для инкапсуляции примитивного типа можно использовать оболочки типов (как было в предыдущем примере). Вдобавок механизм автоупаковки и автораспаковки в Java обеспечивает прозрачность работы с оболочками типов. Глава 14. Обобщения 407 О бобщ енные тип ы разли ч а ю тся на основе их ар гу мен тов т ипов Ключ евой момент, которы й необходимо понять относительно обобщен­ ных типов, связан с тем, что ссылка на одну специфическую версию обобщен­ ного типа несовместима по типу с другой версией того же обобщенного типа. Например, пусть только что показанная программа содержит строку кода с ошибкой и компилироваться не будет: iOb = st rOb ; / / Ошибка ! Несмотря на то что и iOb, и s trOb относятся к типу Gen<T>, они являются ссылка ми на разные типы, т.к. их аргументы типов отличаются. Таким спосо­ бом обобщения обеспечивают безопасность типов и предотвращают ошибки. Каким образо м обобщения ул уч ш а ют б е зопасност ь в отно ш ении типов ? В этот момент вы можете задать себе следующий вопрос. Учитывая, что та же функциональность, что и в обобщенном классе Gen, может быть достигну­ та без обобщений, просто за счет указ ания Obj ect в качестве типа данных и прим енения соответствующих приведений, то в ч ем польза от превращения Gen в обобщенный класс? Ответ заключается в том , что обобщения автома­ тич ески обеспечивают безопасность в отношении типов для всех операций с участием Gen. В процессе они избавляют вас от необходимости вводить при­ ведения типов и проверять типы в коде вручную. Чтобы понять преимущества обобщений, сначала рассмотрим показанную ниже программу, в которой создается необобщенный эквивалент класса Gen: / / Класс NonGen функционально э квивалентен Gen , / / но не задействует обобщения . class NonGen { Ob j ect оЬ; // оЬ теперь имеет тип Ob j ect / / Передать конструктору ссылку на объект типа Obj ect . NonGen ( Ob j ect о ) { оЬ = о ; / / Возвратить объект типа Obj ect . Obj ect getOb { ) return оЬ ; ! ! Вывести тип оЬ . void showType ( ) ( System . out . priпt l n { " Tипoм оЬ является " + оЬ . get Class ( ) . getName ( ) ) ; / / Демонстрация приме нения необобщенного класса . class NonGenDemo [ 408 Часть 1. Язык Java puЫic s tatic vo id ma in ( String [ ] args ) { NonGen iOb ; // Создать экземпляр NonGen и сохранить в нем объект Integer . / / Автоупаковка по-прежнему происходит . iOb = new NonGen ( 8 8 ) ; / / Вывести тип данных, исполь зуемых переменной iOb . iOb . s howType ( ) ; / / Получить значение iOb . На этот раз приведение обязательно . int v = ( Intege r ) iOb . getOb ( ) ; Sys tem . out . println ( "знaчeниe : " + v ) ; System . out . println ( ) ; // Создать еще один экземпляр NonGen и сохранить в нем объект St ring . NonGen strOb = new NonGen ( "Тест без обобщений " ) ; // Вывести тип данных , исполь зуемых переменной strOb . strOb . showType ( ) ; // Получить значение strOb . / / Снова обратите внимание, что необходимо приведение . String str = ( S t ring ) s trOb . getOb ( ) ; System . o ut . p rintln ( "знaчeниe : " + s t r ) ; // Следующий код скомпилируется, но он концептуально ошибочен ! iOb = strOb ; v = ( I n tege r ) iOb . getOb ( ) ; / / ошибка во время выполнения ! С версией без обобщений связано несколько интересных моментов. Прежде всего, обратите внимание, что все случаи использования т в NonGen заменены типом Obj ect, позволяя NonGen хранить объекты любого типа по­ добно обобщенной версии. Однако это также не позволяет компилятору Java получить реальное знан ие о типе данных, фактически хранящихся в NonGen, что плохо по двум причинам. Во-первых, для извлечения сохраненных дан­ ных необходимо применять явные приведения типов. Во-вторых, многие виды ошибок несоответствия типов невозможно обнаружить до стадии вы­ полнения. Давайте проанализируем каждую проблему. Взгляните на следующую строку: int v = ( Integer ) iOb . getOb ( ) ; Поскольку возвращаемым типом getOb ( ) является Obj ect, необходимо приведение к I n t eger, чтобы значение автоматически распаковывалось и сохранялось в переменной v. Если вы удалите приведение, тогда программа не скомпилируется. В обобщенной версии такое приведение было неявным. В необобщенной версии приведение должно быть явным. Оно оказывается не только неудобством, но и потенциальным источником ошибки. Теперь возьмем кодовую последовательность из конца программы: // СледУющий код скомпилируется, но он концептуально ошибочен ! iOb = strOb; v = ( I nteger ) iOb . getOЬ ( ) ; // ошибка во время вьmолнения ! Гл а ва 1 4 . Обоб ще ния 409 Здесь st rOb присваивается переменной iOb. Тем не менее, strOb ссылает­ ся на объект, который содержит строку, а не целое число. Такое присваива­ ние синтаксически допустимо, потому что все ссылки типа NonGen одинако­ вы, и любая ссылка NonGen может ссылаться на любой другой объект NonGen. Однако семантически данное утверждение неверно, как показывает следую­ щая строка. Здесь возвращаемый тип getOb ( ) приводится к I nteger, после чего делается попытка присвоить результат переменной v. Проблема в том, что iOb теперь ссылается на объект, который хранит Str i ng, а не I nteger. К сожалению, без использования обобщений компилятор Java не может это узнать. Взамен при попытке приведения к I nteger генерируется исключение во время выполнения. Как вам известно, возникновение исключений в коде во время выполнения считается крайне плохой практикой! В случае применения обобщений описанная выше ситуация невозмож­ на. Если бы такая последовательность действий была предпринята в версии программы с обобщениями, то компилятор перехватил бы ее и сообщил об ошибке, тем самым предотвратив серьезный сбой, который вызывает исклю­ чение во время выполнения. Возможность написания безопасного в отноше­ нии типов кода, где ошибки несоответствия типов перехватываются на этапе компиляции, является ключевым преимуществом обобщений. Хотя исполь­ зовать ссылки Obj ect для создания "обобщенного" кода можно было всегда, такой код не поддерживает безопасность к типам, а его неправильная эксплу­ атация зачастую приводит к возникновению исключений во время выполне­ ния. Обобщения препятствуют этому. По сути, за счет обобщений ошибки во время выполнения преобразуются в ошибки на этапе компиляции, что счита­ ется крупным преимуществом. О бобщенный кл а сс с двумя п а р а метр а ми типов В обобщенном типе допускается объявлять больше, чем один параметр типа. Чтобы указать два или более параметров типов, просто применяйте список, разделенный запятыми. Например, следующий класс TwoGen является разновидностью класса Gen с двумя параметрами типов: / / Простой обобщенный класс с двумя параметрами типов : Т и V . class TwoGen<T , V> { Т оЫ ; V оЬ2 ; / / Передать конструктору ссылки на объекты типов Т и V . TwoGen ( T o l , V о2 ) { оЫ = o l ; оЬ2 = о2 ; / / Вывести типы Т и V . void showTypes ( ) { System . out . println ( " Tипoм Т является " + oЫ . getCl ass ( ) . getName ( ) ) ; Ч асть 1. Яз ы к Java 41 0 System . out . println ( "Tипoм V являе тся " + ob2 . getClas s ( ) . getName ( ) ) ; Т getOЫ ( ) { return оЫ ; V ge tOb2 ( ) { return оЬ2 ; / / Демонстрация исполь зования TwoGen . class SimpGen { puЫ i c static vo id ma in ( St r i ng [ ] args ) TwoGen< I ntege r , St ring> tgObj = new TwoGen<Intege r , S tr ing> ( 8 8 , "Обобщения " ) ; / / Вывести типы . tgObj . showTypes ( ) ; / / Получить и вывести значения . int v = tgObj . getOЫ ( ) ; System . out . println ( " знaчeниe : " + v ) ; St ring str = tgObj . getOb2 ( ) ; S ystem . out . println ( " знaчeниe : " + s t r ) ; Ниже показан вывод, генерируемый программой: Типом Т я вляется j ava . l ang . I nteger Типом V я вляется j ava . l ang . S t r i ng значение : 8 8 значе ние : Обобщения Обратите внимание на объявление класса TwoGen: class TwoGe n<T , V> { В классе TwoGen определены два параметра типов, Т и V, разделенные з а­ пятой. Поскольку он имеет два параметра типов, при создании объекта кон­ структору TwoGen должны быть переданы два аргумента типов: TwoGen< I ntege r , St ring> tgObj = new TwoGen<Integer, String> ( 8 8 , "Обобщения " ) ; В данном случае вместо Т подставляется тип I nteger, а вместо V - т и п String. Несмотря на отл ичие между двумя аргументами т и пов в рассмотренном примере, оба типа могут быть одинаковыми. Например, следующая строка кода вполне допустима: TwoGen<String, String> х = new TwoGen<Stri ng, S t r i ng> ( "А " , " В " ) ; В этом случае Т и V будут и меть тип String. Раз умеется, есл и бы аргумен­ ты типов всегда был и оди наковыми, то нал ичие двух параметров расценива­ лось бы как излишество. Глава 1 4. Обобще ни я 41 1 О б щ ая ф орма обобщен но го класса Синтаксис обобщени й, показ анный в предшествующих примерах, можно свести к общей форме. Вот синтаксис объявления обобщенного класса: class clas s -n arne< t ype-param- l i s t > { // . . . В class - name указывается имя кл асса, а в type-pa ram- l is t - список па­ раметров типов. А так выглядит полный синтаксис для объявления ссылки на обобщенный класс и создания экземпляра: class -narne < t ype - arg- l i st> var-narne = new class -name<type-a rg- l i s t> ( cons-arg - l i s t ) ; Здесь class-name - имя класса, type-arg- list - список аргументов ти­ пов, va r-name - и мя переменной, cons-a rg-list - список аргументов кон­ структора. О гран и ченные т и п ы В предшествующих примерах параметры типов можно было з аменить лю­ бым типом класса, что подходит для многих целей, но иногда пол езно огра­ нич ить типы, которые разрешено передавать параметру типа. Предположим, что необходимо создать обобщенный класс, содержащий метод, который воз­ вращает среднее значение массива чисел . Кроме того, вы хотите использо­ вать этот класс для получения среднего значения массива чисел л юбого типа, включая int, float и douЫe. Таки м образом, тип чисел желательно указ ы ­ вать обобщенно с применением параметра типа. Чтобы создать класс подоб­ ного рода, вы можете попробовать поступить примерно так: / / ( Безуспешная) попытка создать обобщенный класс S t at s , ! ! который мог бы вычислять среднее значение массива // чисел любого заданного типа . !/ / / Этот класс содержит ошибку ! class S t at s <T> { Т [ ] nurns ; / / nurns - массив элементов типа Т / / Передат ь конструктору ссылку на ма ссив типа S tats ( T [ ] о ) { nurns = о ; т. ! / Во всех случаях возвращать результат типа douЫ e . douЫe ave rage ( ) { douЫe surn = О . О ; for ( int i=O ; i < nurns . length; i + + ) s um + = nurns [ i ] . douЬ leVa l ue ( ) ; / / Ошибка ! ! ! return sum / nurns . l engt h ; 41 2 Ч асть 1 . Язык Java Метод average ( ) класса Stats пытается получить версию douЫe каждого числа в массиве nums, вызывая douЬleVa lue ( ) . Поскольку все числовые клас­ сы, такие как Integer и DouЫe, являются подклассами Number, а в NumЬe r определен метод douЬl eValue ( ) , то данный метод доступен всем числовым классам-оболочкам. Проблема связана с тем, что компилятор не может зара­ нее знать, что вы собираетесь создавать объекты Stats, используя только чис­ ловые типы. В итоге при компиляции Stats выдается сообщение об ошибке, указывающее на то, что метод douЬleVa lue ( ) неизвестен. Для решения про­ блемы нужен какой-то способ уведомления компилятора о том, что в Т плани­ руется передавать только числовые типы. Вдобавок необходимо каким-то об­ разом гарантировать, что действительно передаются только числовые типы. Для обработки таких ситуаций в Java предусмотрены ограниченные типы. Когда определяется параметр типа, вы можете создать верхнюю границу, объ­ являющую суперкласс, от которого должны быть порождены все аргументы типов. Цель достигается за счет применения конструкции extends при ука­ зании параметра типа: <Т extends supercla s s > Таким образом, тип Т может быть заменен только суперклассом, ука­ занным в supe r c l a s s, или подклассами этого суперкласса. В результате superclass определяет включающий верхний предел. Прием можно использовать для исправления показанного ранее класса Stats, указав NumЬer в качестве верхней границы: // В этой версии класса Stats аргументом типа для Т должен быть // либо Number, либо класс , производный от NumЬer . class Stats<T extends Number> { Т [ ] nums ; / / массив элементов класса Number или его подкласса / / Передать конструктору ссылку на массив элементов // класса NumЬe r или его подкласса . Stats ( T [ ] о ) { nums = о ; / / В о всех случаях возвращать результат типа douЫe . douЫe average ( ) { douЫe s um = О . О ; for ( int i=O ; i < nums . length ; i + + ) sum + = nums [ i ] . douЫeValue ( ) ; return sum / nums . length ; / / Демонстрация исполь зования Stats . c:ass BoundsDemo { puЫ i c s tatic void main ( String [ ] a rgs ) I nteger [ ] inums = { 1 , 2 , 3 , 4 , 5 ) ; Stats<Integer> i ob = new Stats<Integer> ( inums ) ; douЫe v = iob . average ( ) ; System . out . println ( "Cpeднee значение i ob равно " + v ) ; Гла ва 1 4. Обобщения 41 3 DouЫe [ ] dnums = { 1 . 1 , 2 . 2 , 3 . 3 , 4 . 4 , 5 . 5 } ; Stats<DouЫe> dob = new Stats<DouЫe> ( dnums ) ; douЫe w = dob . average ( ) ; System . out . println ( " Cpeднee значение dob равно " + w ) ; / / Следующий код не скомпилируется, т . к . String / / не является подклассом Number . / / String [ ] strs = { " 1 " , " 2 " , " 3 " , " 4 " , " 5 " } ; / / Stats<String> s t rob = new Stats<String> ( strs ) ; / / douЫe х = s trob . average ( ) ; / / System . out . println ( " Cpeднee значение s t rob равно " + v ) ; Вот вывод: Среднее значение iob равно 3 . 0 Среднее значение dob равно 3 . 3 Обратите внимание, как теперь объявляется класс Stats: class Stat s <T extends NumЬer> { Поскольку теперь тип Т ограничен классом NumЬer, компилятору Java из­ вестно, что все объекты типа Т могут вызывать метод douЬleVa lue ( ) , т.к. он объявлен в NumЬer. Это и само по себе считается большим преим уществом. Тем не менее, в качестве дополнител ьного бонуса ограничение Т также пре­ дотвращает создание нечисловых объектов Stats. Например, есл и вы удали­ те комментарии из строк в конце программы и затем попробуете скомпи­ лировать код заново, то пол учите ошибки на этапе компиляции, потому что String не является подкл ассом NumЬer. Для определения границы кроме типа кл асса можно также применять тип интерфейса. На самом деле в качестве границ разрешено указывать несколь­ ко интерфейсов. Вдобавок граница может включать как тип класса, так и один или несколько интерфейсов. В таком случае тип класса должен быть указан первым. Когда граница содержит тип интерфейса, то допускаются только ар­ гументы типов, реал изующие этот интерфейс. При указании привязки, кото­ рая имеет кл асс и интерфейс или несколько интерфейсов, для их соединения используйте операцию &, что в итоге создает пересечения типов. Например: class Gen<T extends MyClass & Myinterface> // . . . Здесь Т ограничивается классом MyC lass и интерфейсом Myinterface. Таким образом, любой аргумент типа, передаваемый типу Т, должен быть подклассом MyClass и реал изовывать Myinter face. Интересно отметить, что пересечение типов можно также применять в приведении. Использование а р гументов с подста новочными знаками Какой бы полезной н и была безопасность к типам, иногда она может ме­ шать совершенно приемлемым конструкциям. Скажем, и мея класс Stats, 414 Часть 1 . Язык Java показ анный в конце предыдуще го раз д ела, предположим, что вы хотите до­ бавить метод по и мени isSarneAvg ( ) , который выясняет, содержат ли два объе кта Stats массивы, дающи е одно и то же средн ее з начение, независимо от типа числовых данных в каждом из них. Наприм е р, если в одном объе кте хранятся з начения 1 . О, 2 . О и 3 . О типа douЫe, а в другом - ц елочисленные з начения 2, 1 и 3, тогда средние з начения будут одинаковыми. Один из спо­ собов реализации метода isSarneAvg ( ) предусматривает передачу ему аргу­ мента Stats с последующим сравнением среднего з начения этого аргумента и вызывающего объекта с возвращением true, только есл и сре дни е з нач е ния совпадают. Наприм е р, вы хотите иметь возможность выз ывать isSarneAvg ( ) , как показано ниже: I nteger [ ] inums = { 1 , 2 , 3 , 4 , 5 } ; DouЫe [ ] dnums = { 1 . 1 , 2 . 2 , 3 . 3 , 4 . 4 , 5 . 5 } ; Stats< I nteger> i ob = new Stats< I nteger> ( inums ) ; Stats <DouЫe> dob = new Stats<DouЫ e> ( dnums ) ; i f ( i ob . i s SameAvg ( dob ) ) System . out . println ( " Средние значения одинаковы . " ) ; else S ystem . out . println ( " Cpeдниe значения отличаются . " ) ; На первый взгляд создание isSarneAvg ( ) выглядит простой задачей. Так как класс Sta ts является обобщенным, а его метод average ( ) способен ра­ ботать с л юбым типом объекта Stats, кажется, что создать isSarneAvg ( ) н е­ сложно. К сожалению, проблемы начинаются, как только вы пытаетесь объ­ явить парам етр типа Stats. Поскольку Stats - параметриз ованный тип, что вы укажете для параметра типа в Stats при объявлении параметра данного типа? Поначал у вы можете подумать о решении, где Т прим е няется в качестве парам е тра типа: // Работать не будет ' / / Выяснить , одинаковы ли два средних значения . boolean i s SameAvg ( S tats <T> оЬ ) i f ( average ( ) == ob . average ( ) ) return true; return f a l s e ; Проблема предпринятой попытки в том, что такое р е шение будет рабо­ тать тол ько с другими объектами Sta ts, тип которых совпадает с типом вызывающего объекта. Скажем, есл и выз ываемый объект относится к типу Stats< I nteger>, то параметр оЬ тоже должен им е ть тип Stats< I nteger>. Его нельзя использовать, например, для сравнения среднего з начения объ­ екта типа Stats<DouЬle> со средним з начением объ е кта типа Stats<Short>. Сл е довательно, этот подход будет работать лишь в очень узком контексте и не даст общего (т.е. обобщенного) реш е ния. Глава 1 4. Об о бщ ен ия 41 5 Чтобы создать обобщенный метод i s SameAvg ( ) , потребуется задейство­ вать еще одну особенность обобщений Java: аргумент с подстановочным зна­ ком. Аргумент с подстановочным знаком указывается посредством символа ? и представляет неизвестный тип. Вот как с его помощью можно записать метод isSameAvg ( ) : / / Выя снит ь , одинаковы ли два средних значения . // Обратите внимание на использование подстановочного знака . boolean i s S ameAvg ( S tat s < ? > оЬ ) { if ( average ( ) == ob . average ( ) ) return true; return false ; Здесь Stats<?> соответствует любому объекту Stats, позволяя сравни­ вать средние значения любых двух объектов Stats, что и демонстрируется в следующей программе: // Использование подстановочн ого знака . class Stat s<T extends NumЬer> { Т [ ] nums ; / / массив элементов класса Number или его подкласса ! ! Передать конструктору ссьmку на массив элементов // класса NumЬe r или его подкласса . Stats ( T [ ] о ) nums = о ; / / Во всех случ аях воз вращать резуль тат типа douЫ e . douЫe average ( ) { douЫ e surn = О . О ; for ( i nt i=O ; i < nums . l engt h ; i++ ) surn += nums [ i ] . douЫeValue ( ) ; return sum / nums . l ength; / ! Выяснит ь , одинаковы ли два средних значени я . / / Обратите внимание на исполь зование подстановочного знака . bool ean isSameAvg ( S tat s < ? > оЬ ) { if ( ave r age ( ) == оЬ . average ( ) ) return tr ue ; return fa l s e ; / / Демо нстрация применения подст ановочного знака . class Wil dca rdDemo { puЫ i c static vo i d main ( S tring [ ] args ) { Intege r [ J inurns = { 1 , 2 , 3 , 4 , 5 ) ; Stats<I nteger> i ob = new Stats< I nteger> ( i nums ) ; douЫ e v = iob . average ( ) ; System . out . pri ntln ( " Cpeднee значе ние iob равно " + v ) ; DouЫ e [ ] dums = { 1 . 1 , 2 . 2 , 3 . 3 , 4 . 4 , 5 . 5 } ; Stats <DouЬle> dob = new Stats <DouЬle> ( dnurns ) ; 416 Часть 1 . Я зы к Java douЫe w = dob . average ( ) ; System . out . println ( " Cpeднee значение dob равно " + w ) ; Fl oat [ ] fnums = { l . 0 F , 2 . 0 F , 3 . 0 F , 4 . 0 F , 5 . 0 F } ; Stats<Float> fob = new S tats<Fl oat> ( fnums ) ; douЫe х = fob . average ( ) ; System . out . println ( " Cpeднee значение fob равно " + х ) ; / / Выяснить , какие массивы имеют одинако вые средние значения . System . out . print ( "Средние значения iob и dob " ) ; i f ( iob . i sSameAvg ( dob ) ) System . out . println ( "oди нaкoвы . " ) ; el s e System . out . println ( "oтличaютcя . " ) ; System . out . print ( "Cpeдниe значения iob и fob " ) ; i f ( i ob . i sSameAvg ( fob) ) System . out . println ( "oдинaкoвы . " ) ; else System . out . println ( " oтличaютcя . " ) ; Ниже приведен вывод: Среднее Среднее Среднее Средние Средние значение значение значение значения значения iob dob fob iob iob равно равно равно и dob и fob 3.0 3.3 3.0 отличаются . одинаковы . И последнее замечание: важно понимать, что подстановочный знак не вли­ яет на то, какой тип объектов Stats можно создавать - это регулируется конструкцией extends в объявлении Stat s. Подстановочный знак просто со­ ответствует любому допустимому объекту S tats. Ограниченные аргументы с подс тановочными знакам и Аргументы с подстановочными знаками могут быть ограничены во мно­ гом так же, как и параметр типа. Ограниченный аргумент с подстановочным знаком особенно важен при создании обобщенного типа, который будет опе­ рировать на иерархии классов. Чтобы понять причину, давайте проработаем пример. Рассмотрим следующую иерархию классов, инкапсулирующих коор­ динаты: / / Двумерные координаты . class TwoD { int х, у ; TwoD ( int а , int Ь ) { х == а ; у = Ь; / / Трехмерные координаты . Гл а в а 1 4 . Обо б щени я 417 class ThreeD extends TwoD { int z ; ThreeD ( int а , int Ь , int с ) s uper ( а , Ь ) ; z = с; / / Четырехмерные координаты . class Four D extends ThreeD { int t ; FourD ( i nt а , int Ь , i n t с , i n t d ) { s uper ( а , Ь , с ) ; t = d; На вершине иерархии находится класс TwoD, который инкапсулирует дву­ мерную координату Х, У. Класс ThreeD унаследован от класса TwoD и добав­ ляет третье измерение, создавая координату Х, У, Z. Класс FourD унаследован от к л асса Thre e D и добавляет четвертое измерение (время), давая четырех­ мерную координату. Далее представлен обобщенный класс Coords, в котором хранится массив координат: // Этот класс хранит массив объе ктов координат . class Coords <T extends TwoD> { Т [ ] coords ; Coords ( Т [ ] о ) coords = о ; Обратите внимание, что в классе Coords указан параметр типа, ограничен­ ный TwoD, т.е. любой массив, хранящийся в объекте Coords, будет содержать объекты к ласса TwoD или одного из его подклассов. Теперь предположим, что вы хотите написать метод, который отображает координаты Х и У для каждого элемента в массиве coords объекта Coords. Поскольку все типы объектов Coo rds имеют как минимум две координаты (Х и У), это легко сделать с помощью подстановочного знака: static voi d showXY ( Coords<?> с ) { Systern . out . pr i ntl n ( " Koopдинaты: Х У : " ) ; for ( int i=O ; i < c . coords . lengt h ; i + + ) Sys tern . out . println ( c . coords [ i ] . х + 11 с . coords [ i ] . у ) ; Systern . out . println ( ) ; 11 + Из-за того, что Coords является ограниченным обобщенным типом, при­ меняющим TwoD в качестве верхней границы, все объекты, которые можно использовать для создания объекта Coords, будут массивами элементов клас- 418 Ч ас ть 1 . Яз ы к Java са Two D или производных от него классов. Таким образом, метод showXY ( ) способен отображать содержимое любого объекта Coords. Но что, если вы хотите создать метод, отображающий координаты Х, У и Z объекта ThreeD или FourD? Проблема в том, что не все объекты Coords будут иметь три координаты, т.к. объект Coo rds<Two D> располагает только Х и У. Как тогда написать метод, отображающий координаты Х, У и Z для объектов Coords<ThreeD> и Coords<FourD>, и одновременно предотвратить примене­ ние этого метода с объектами Coords<Two D>? Ответ - воспользоваться огра­ ниченным аргументом с подстановочными знаками. В ограниченном аргументе с подстановочным знаком задается верхняя или нижняя граница для аргумента типа, что позволяет сузить диапазон ти­ пов объектов, с которыми будет работать метод. Наиболее распространен­ ным ограничением является верхняя граница, создаваемая с помощью кон­ струкции extends почти так же, как при создании ограниченного типа. Используя ограниченный аргумент с подстановочным знаком, легко соз­ дать метод, который отображает координаты Х, У и Z объекта Coords, если он действительно и меет указанные три координаты. Например, следующий метод showXYZ ( ) вывод ит координаты Х, У и Z элементов, хранящихся в объ­ екте Coo rds, если эти элементы действительно относятся к типу ThreeD (или к типу, производному от ThreeD): static void showXYZ ( Coord s < ? extends ThreeD> с ) { System . ou t . println ( 11 Koopдин aты Х У Z : " ) ; for ( in t i=O ; i < c . coords . l ength; i + + ) System . ou t . println ( c . coords [ i ] . х + " 11 + c . coords [ i ] . у + 11 " + c . coords [ i ] . z ) ; S ystem . out . println ( ) ; Обратите внимание, что к подстановочному знаку в объявлении параме­ тра с добавлена конструкция extends. Она указывает, что ? может соответ­ ствовать любому типу, если он является ThreeD или классом, производ ным от Th reeD. Таким образом, конструкция extends устана вливает верхнюю границу того, чему может соответствовать подстановочный знак. Из-за та­ кой привязки метод showXYZ () можно вызывать со ссылками на объек­ ты типа Coo rds <ThreeD> или Coo rds < FourD>, но не со ссылкой на объект типа Coo rds<TwoD>. Попытка вызвать showXZY () со ссылкой на объект типа Coo rds<TwoD> приводит к ошиб ке на этапе компиляции, что обеспеч ивает безопасность в отношении типов. Н иже показана полная программа, демонстрирующая действия ограниченного аргумента с подстановочным знаком: // Ограниченный аргумент с подстановочным знаком . / / Двумерные координаты . class TwoD { int х , у ; TwoD ( int а , i n t Ь ) { Гла в а 1 4. Обобщени я х = а; у Ь; / / Трехмерные координаты . class ThreeD extends TwoD int z ; ThreeD ( int а , int Ь , i nt с ) { super ( а , Ь ) ; z = с; / / Четырехмерные координаты . class Four D extends ThreeD { int t ; FourD ( int а , int Ь , int с , int d ) { s uper ( а , Ь , с ) ; t = d; / / Этот класс хранит массив объе ктов координат . class Coords <T extends TwoD> { Т [ ] coords ; Coords ( Т [ ] о ) coords = о ; / / Демонстрация исполь зовани я ограниченного // аргумента с подстановочным знаком . class BoundedWildcard { s t atic void s howXY ( Coords < ? > с ) { System . out . println ( " Koopдинaты Х У : " ) ; for ( i nt i=O ; i < c . coords . lengt h ; i++ ) System . out . printl n ( c . coords [ i ] . х + " " + c . coords [ i ] . у ) ; System . out . println ( ) ; static void showXYZ ( Coords < ? extends ThreeD> с ) { S ystem . out . print l n ( " Координаты Х У Z : " ) ; for ( i nt i = O ; i < c . coords . lengt h ; i++ ) S ystem . out . println ( c . coo rds [ i ] . х + " " + c . coo rds [ i ] . у + " " + с . coords [ i ] . z ) ; System . out . println ( ) ; static void s howAl l ( Coords < ? ext ends FourD> с ) S ystem . out . println ( " Koopдинaты Х У Z Т : " ) ; for ( i nt i=O ; i < c . coords . l engt h ; i + + ) System . out . p r i ntl n ( c . coo rds [ i ] . х + " " + 419 Ч ас ть 1 . Яз ы к Java 420 с . coords [ i ] . у + " " + с . coords [ i ] . z + " " + c . coords [ i ] . t ) ; S ystem . out . println ( ) ; puЬ l i c static vo id main ( S tring [ ] args ) { TwoD [ ] td = ( new TwoD ( O , О ) , new TwoD ( 7 , 9 ) , new TwoD ( l 8 , 4 ) , new TwoD ( - 1 , - 2 3 ) }; Coords <TwoD> tdlocs = new Coo rds<TwoD> ( td ) ; System . out . println ( " Содержимое tdlocs . " ) ; s howXY ( tdlocs ) ; / / Нормально , это объект Two D / / s howXYZ ( tdlocs ) ; / / Ошибка , не объект ThreeD // s howAl l ( tdlocs ) ; / / Ошибка , не объект Four D / / Создать нескол ь ко объектов FourD . FourD [ ] fd = ( new FourD ( l , 2 , 3 , 4 ) , new FourD ( б , 8 , 1 4 , 8 ) , new FourD ( 2 2 , 9 , 4 , 9 ) , new FourD ( 3 , - 2 , - 2 3 , 1 7 ) }; Coords < FourD> fdlocs = new Coords < FourD> ( fd ) ; System . out . println ( " Coдepжимoe fdlocs . " ) ; / / Все вызовы ВЫПОЛНRТСR успешно . showXY ( fdlocs ) ; showXYZ ( fdlocs ) ; s howAl l ( fdlocs ) ; Вот вывод, генерируемый программой: Содержимое tdlocs . Координаты Х У : о о 7 9 18 4 -1 -23 Содержимое fdlocs . Координаты Х У : 1 2 6 8 22 9 3 -2 Координаты Х У Z : 1 2 3 6 8 14 22 9 4 3 -2 - 2 3 Глава 1 4. Обобщ е ния 421 Координаты Х У Z Т : l 2 3 4 6 8 14 8 22 9 4 9 3 -2 - 2 3 1 7 Обратите внимание на закомментированные строки: / / showXYZ ( td l ocs ) ; / / Ошибка , не объект ThreeD // s howAl l ( tdlocs ) ; / / Ошибка , не объект FourD Так как tdlocs представляет собой объект Coords ( TwoD ) , его нел ьзя при­ менять для вызова showXYZ ( ) или showAll ( ) , потому что этому препятству­ ют ограниченные аргументы с подстановочными знаками в их объявлениях. Чтобы удостовериться в сказанном, попробуйте удалить символы коммента­ рия и затем скомпилировать п рограмму. Вы получите ошибки на этапе ком­ пиляции из-за несоответствия типов. В общем случае для установления верхней границы аргумента с подстано­ вочным знаком испол ьзуйте выражение следующего вида: <? extends superclass> где superclass - и мя класса, служащего верхней границей. Не забывайте, что это включающая конструк ция, поскол ьку класс, формирующий верхнюю границу (т.е. указанный в superclass), также находится в п ределах границ. Вы также можете указать нижнюю границу аргумента с подстановочным знаком, добавив к объявлению конструкцию super с таким общим видом: <? s uper subclass> В данном случае допустимыми аргументами будут только классы, являю­ щиеся суперкл ассами subclass. Конструк ция тоже включающая. Создан ие обобщенного метода Как было проиллюстрировано в предшествующих примерах, методы вну­ три обобщенного класса могут использовать параметр типа класса и, следова­ тельно, автоматически становятся обобщенными по отношению к параметру типа. Однако можно объявить обобщенный метод с одним или несколькими собственными параметрами типов. Более того, можно создавать обобщенный метод внутри необобщенного к ласса. Давайте начнем с примера. В показанной ниже программе объявляется не­ обобщенный класс по имени GenMethDemo, а в этом классе определяется ста­ тический обобщенный метод по имени i s i n ( ) , который выясняет, является ли объект членом массива. Его можно применять с л юбым типом объекта и массива п ри условии, что массив содержит объекты, совместимые с типом искомого объекта. / / Демонстрация определения простого обобщенного метода . clas s GenMethDerno { / / Выяснить , присутствует ли объект в массиве . static <Т extends CornparaЫ e <T>, V extends Т> bool ean i s i n ( Т х , V [ ] у ) { 422 Часть 1. Язык Java for ( i nt i = O ; i < y . l e ngth ; i + + ) i f ( x . equa l s ( y [ i ] ) ) return tr ue ; ret urn fal s e ; puЬ l i c static vo id mai:1 ( S tring [ ] args ) { / / Исполь зовать i s i n ( ) для объектов Integ e r . I n teger [ ] nums = { 1 , 2 , 3 , 4 , 5 } ; i f ( i s i n ( 2 , nums ) ) System . o ut . p r i n t l n ( "2 присутствует в nums " ) ; i f ( ! i s i n ( 7 , nums ) ) System . out . p rintln ( " 7 не присутствует в nums " ) ; System . o ut . p r i n t l n ( ) ; / / Исполь з овать i s i n ( ) для объектов S t ring . String [ ] s t rs = { "one " , " two " , "three " , " four 11 , " f ive " } ; i f ( is i n ( " two " , s t rs ) ) S ys tem . out . p rintln ( "two присут ствует в str s " ) ; i f ( ! i s i n ( "seven " , s t r s ) ) System . out . p ri n t l n ( "s even не присутствует в s t r s " ) ; / / Не скомпилируется ! Типы должны быть совместимыми . / / i f ( i s i n ( " two " , nums ) ) / / Syst em . out . println ( "two присутст вует в nums " ) ; Программа генерирует следующий вывод: 2 присутствует в nums 7 не присутствует в nums two присут ствует в strs s even не присутствует в strs Теперь займ емся исследованием м етода isin ( ) . Взгляните на его объяв­ ление: s tatic <Т extends ComparaЬle<T> ; V extends Т> boolean i s i n ( T х , V [ ] у ) { Параметры типа объявляются перед возвращаемым типом м етода. Кроме того, обратите внимание на констру кцию т extends ComparaЫe<T>. Интерф ейс ComparaЫe объявлен в пакете j ava . lang. Класс, р еализующий ComparaЫe, определяет объекты, которые можно упорядочивать. Таким об­ разом, требование верхней границы как ComparaЫe гарантирует, что метод isin ( ) может использоваться только с объектами, которые обладают способ­ ностью участвовать в сравнениях. Интерфейс ComparaЬle является обобщен­ ным , и его параметр типа указывает тип сравниваемых объектов. (Вскоре вы увидите, как создавать обобщенный интерфейс.) Далее обратите внимание, что тип V ограничен сверху типом т. Соответственно тип V должен быть либо тем же самым, что и тип Т, либо подклассом т. Такое отношение гарантирует, что метод i s i n ( ) можно вызывать только с аргументами, которые совмести- Глава 1 4. Обобщения 423 мы друг с другом. Вдобавок метод i s l n ( ) определен как статический, что позволяет вызывать его независимо от любого объекта. Тем не менее, важно понимать, что обобщенные методы могут быть как статическими, так и не­ статическими. В этом смысле нет никаких ограничений. Метод i s l n ( ) вызывается внутри ma in ( ) с применением обычного син­ таксиса вызова без необходимости указывать аргументы типа. Дело в том, что типы аргументов распознаются автоматически, а типы Т и V надлежащим образом корректируются. Например, при первом вызове типом первого ар­ гумента оказывается Integer (из-за автоупаковки), что приводит к замене Т на I nteger: i f ( i s i n ( 2 , nums ) ) Базовым типом второго аргумента тоже является I nteger и потому I nteger становится заменой для V. Во втором вызове используются типы String, так что типы Т и V заменяются на St ring. Хотя выведения типов будет достаточно для большинства вызовов обоб­ щенных методов, при необходимости аргумент типа можно указывать явно. Скажем, вот как выглядит первый вызов is In ( ) , когда указаны аргументы типов: GenMeth Demo . < Integer, Intege r > i s i n ( 2 , nums ) Конечно, в данном случае указание аргументов типов не дает какого-либо выигрыша. Кроме того, в JDK 8 выведение типов было улучшено в том, что касается методов. В результате в настоящее время встречается меньше случа­ ев, когда требуются явные аргументы типов. Теперь обратите внимание на закомментированный код: // i f ( i s i n ( "two " , nums ) ) / / System . out . println ( "two присутствует в n ums " ) ; Удалив комментарии и попытавшись скомпилировать программу, вы по­ лучите ошибку. Причина в том, что параметр типа V ограничен Т с помощью конструкции extends в объявлении V, т.е. тип V должен быть либо Т, либо под­ классом Т. В рассматриваемой ситуации первый аргумент имеет тип S t r ing, что превращает Т в S t r i ng, а второй - тип Integer, который не является подклассом S t r i ng. В итоге возникает ошибка несоответствия типов на этапе компиляции. Такая способность обеспечения безопасности к типам представ­ ляет собой одно из самых важных преимуществ обобщенных методов. Синтаксис, применяемый для создания i s In ( ) , можно обобщить. Вот синтаксис обобщенного метода: <type-pa ram- list > ret-type meth-name (pa ram- l i s t ) { / / ... < список -параме тров -типов > в озвраща емый -тип имя -ме тода ( список -параметров ) { // ... Во всех случаях в t ype -pa ram- l i s t указывается список параметров ти­ пов, разделенных запятыми. Кроме того, ret-type - это возвращаемый тип, meth-name - имя метода и param-l i s t - список параметров. В обобщенном методе список параметров типов предшествует возвращаемому типу. 424 Ч асть 1. Я зык Java Обобщен н ы е конструкторы Конструкторы могут быть обобщенными, даже когда и х класс таковым не является. Например, взгляните на следующую короткую программу: / / Использование обобщенного конструктора . class GenCons { private douЫe val ; <Textends NumЬer> GenCons ( T arg) { val = arg . douЫeVal ue ( ) ; void showVal ( ) { System . out . p r intln ( "val : " + val ) ; class GenConsDemo { puЬlic static void main ( St ri n g [ ] args ) GenCons test = new GenCons ( l00 ) ; GenCons test2 = new GenCons ( l2 3 . 5F) ; test . showVal ( ) ; test2 . showVal ( ) ; Вот вывод: val : 1 0 0 . 0 val : 1 2 3 . 5 Поскольку в GenCons ( ) указан параметр обобщенного типа, который обя­ зан быть подклассом NumЬer, конструктор GenCons ( ) можно вызывать с л ю­ бым числовым типом, вкл ючая I nteger, Float и DouЫe. Следовательно, хотя GenCons - не обобщенный к л ассом, его конструктор является обобщенным. О б общенные интер ф ейсы В дополнение к обобщенным классам и методам также могут существовать обобщенные интерфейсы, которые определяются аналогично обобщенным классам. Ниже приведен пример, где создается интерфейс MinМax с методами min ( ) и max ( ) , которые должны возвращать минимальное и максимальное значения в наборе объектов. / / Пример обобщенного интерфейса . / / Интерфейс для нахождения минимального и максимального значений . interface MinMax<T extends ComparaЫe<T>> { Т min ( ) ; Т max ( ) ; / / Реализовать интерфейс MinMax . class MyClass<T extends ComparaЫe<T>> implements MinMax<T> { Т [ ] val s ; Глава 1 4 . О б о бщения 425 MyCl a s s ( T [ ] о ) { val s = о ; / / Возвратить минимальное значение в va l s . puЫic Т min ( ) { Т v = va l s [ О ] ; for ( int i=l ; i < va l s . lengt h ; i + + ) if ( val s [ i ] . compa reTo ( v ) < О ) v = val s [ i ] ; return v ; / / Возвратить максимальное значение в va l s . pub1ic Т max ( ) { Т v = va l s [ O ] ; for ( int i = l ; i < val s . lengt h ; i + + ) i f ( va l s [ i ] . compareTo ( v ) > О ) v = va l s [ i ] ; return v ; c l a s s GenI FDemo { puЫ i c static void ma in ( String [ ] args ) I nteger [ ] inums = { 3 , 6 , 2 , В , 6 } ; Characte r [ ] chs = { ' Ь ' , ' r ' , ' р ' , ' w ' } ; MyC l a s s < I nteger> iob = new MyClass < I nteger> ( inums ) ; MyClass <Characte r> соЬ = new MyClass <Character> ( chs ) ; Sys tem . out . println ( "Максималь ное значение в inums : " + i ob . max ( ) ) ; System . out . println ( "Минимaльнoe значение в inums : " + i ob . mi n ( ) ) ; System . out . printl n ( "Maкcимaльнoe значение в chs : " + cob . max ( ) ) ; S ystem. out . println ( "Минималь ное значение в chs : " + cob . mi n ( ) ) ; Программа генерирует следующий вывод: Максимальное значение в inums : 8 Минималь ное значение в inums : 2 Максималь ное значение в chs : w Минимал ьное значение в chs : Ь Хотя понимание большинства аспектов в программе не должно вызывать затруднений, необходимо отметить пару ключевых моментов. Прежде всего, обратите внимание на способ объявления интерфейса MinМax: i nte rface MinMax<T extends Compa raЫe<T>> [ Обобщенный интерфейс объявляется подобно обобщенному кл ассу. В дан­ ном сл учае указан параметр типа Т с верхней границей ComparaЫe. Как объ­ яснялось ранее, ComparaЫe - это интерфейс, определенный в j ava . lang, который задает способ сравнения объектов. Его параметр типа указывает тип сравниваемых объектов. 426 Ч асть 1. Язык Java Затем интерфейс MinMax реализуется классом MyClass. Взгляните на объ­ явление MyClass: class MyC l as s < T extends Compa raЬl e<T>> implements MinMax<T> { Обратите особое внимание на то, как параметр типа Т объявляе тся в MyClass и далее переда ется классу Mi nMax. Поскольку для MinMax требуе т ­ ся тип, который реализует Compa raЫe, реализующий класс ( в данном случае MyClass) должен указывать ту же самую границу. Более того, после установ­ ления этой границы нет никакой необходимости указывать ее снова в кон­ струкции implements. На самом деле пост упать так было бы неправильно. Например, приведенный ниже код некоррект ен и потому не скомпилируется: // Ошибка ! class MyClass<T extends ComparaЬle<T>> implements MinMax<T extends ComparaЬle<T>> Установленный параметр типа просто передается инт ерфейсу без дальней­ ших изменений. Вообще говоря, если класс реализует обобщенный интерф ейс, то этот класс тоже должен быть обобщенным, по кра йней мере, принимая параметр типа, который передается инт ерфейсу. Скажем, следующая попытка объявле­ ния MyClass ошибочна: class MyClass impleme nts MinMax<T> { / / Ошибка ! Поскольку в MyCl a s s не объявля ет пара метр т ипа, то передать его в MinМax невозможно. В таком случае идент ификатор Т попросту неизвестен и компилятор сообщит об ошибке. Разумеется, если класс реализует специфиче­ ский тип обобщенного интерфейса, тогда реализующий класс не обязан быть обобщенным: class MyCl ass implements Mi nMax < I nteger> ( // Нормально Обобщенный инт ерфейс предлага ет два преимущества. Во-первых, его можно реализовать для разных типов данных. Во-вт орых, он позволяет на ­ кладывать ограничения (т.е. границы) на типы данных, для которых может быть реализован инт ерфейс. В примере с MinMax в Т могут передаваться только типы, реализующие интерфейс ComparaЫe. Вот общий синтаксис обобщенного интерфейса: inte r f ace inter face -name<type-param- l i s t > ( // . . . В inter face-name указывается имя интерфейса, а в type-pa ram- l i s t список параметров типов, разделенных запятыми. При реализации обобщен­ ного интерфейса классом class-name в type-arg-l i s t необходимо указы­ вать аргум енты типов: class class-name<type-param- l ist> implements interface- name<type-arg- list> { Гл ава 14. Обобщ ения 427 Н из коу ро вневые т ип ы и у на с ледо в а н ны й код Поскольку до выхода JDK 5 поддержка обобщений отсутствовала, необхо­ димо было обеспечить какой-то путь перехода от старого кода, предшеству­ ющего обобщениям. Кроме того, такой путь пер ехода должен был позволить коду, написанному до появления обобщений, остаться в рабочем состоянии и одновременно быть совместимым с обобщениями. Другими словами, коду, предшествующему обобщениям, нужна была возможность работы с обобще­ ниями, а обобщенному коду - возможность работы с кодом, написанным до появления обобщений. В плане обработки перехода к обобщениям Java разрешает использовать обобщенный класс без аргументов типов, что создает низкоуровневый тип для класса. Такой низкоуровневый тип совместим с унаследованным кодом, кото­ рому не известны обобщения. Главный недостаток применения низкоуровне­ вого типа связан с утратой безопасности в отношении типов, обеспечиваемой обобщениями. Ниже показан пример, демонстрирующий низкоуровневый тип в действии: // Демонстрация низкоуровневого типа в действии . class Gen<T> { Т оЬ ; / / объявить объект типа Т / / Передать конс труктору ссыпку на объект типа Т . Gen ( T о ) { оЬ = о ; / / Возвр а тить оЬ . Т getOb ( ) { return оЬ ; // Использование низкоуро вневого типа . class RawDemo { puЫi c static vo id main ( S tring [ ] a rgs ) // Создать объект Gen дпя объектов I nteger . Gen<Integer> iOb = new Gen<I nteger> ( 88 ) ; // Создать объект Gen для объектов St ring . Gen<String> strOb = new Gen<String> ( "Тест обобщений" ) ; / / Создать низкоуровневый объект Gen и предоставить ему значение DouЫ e . Gen raw = new Gen ( DouЬle . valueOf ( 9 8 . 6 ) ) ; / / Приведение зде сь обязательно, потому что тип неизвестен . douЫe d = ( DouЬle ) raw . getOb ( ) ; System . out . println ( "знaчeниe : " + d ) ; / / Исполь зование низкоуровневого типа может стать причиной // генерации ис ключений во время выполнения . / / Вот нескол ь ко примеров . / / Следующее приведение вызывает ошибку во время выполнения ! / / int i = ( In teger ) raw . g etOb ( ) ; // ошибка во время выпол нения 428 Часть 1 . Язык Java / / Это присваивание обходит механизм безопасности типов . s t rOb = raw; / / Нормально, но потенциально неправиль но / / St ring str = s t rOb . getOb ( ) ; / / ошибка во время выполнения // Это присваивание обходит механизм безопасности типов . raw = i Ob ; / / Нормально , но потенциально неправильно / / d = ( DouЬ l e ) raw . getOb ( ) ; / / ошибка во время выполнения В программе присутствуют несколько интересных вещей. Первым делом с помощью следующего объявления создается низкоуровневый тип обобщен­ ного к ласса Gen: Gen raw = new Gen ( DouЫe . valueOf ( 9 8 . 6 ) ) ; Обратите внимание, что аргументы типов не указаны. По существу опера­ тор создает объект Gen, тип Т которого заменяется на Obj ect. Низкоуровневый тип не безопасен в отношении типов. Таким образом, переменной низкоуровневого типа можно присваивать ссылку на любой тип объекта Gen. Разрешено и обратное - переменной конкретного типа Gen мо­ жет быть присвоена ссылка на низкоуровневый объект Gen. Однако обе опе­ рации потенциально небезопасны из-за того, что обходится механизм про­ верки типов обобщений. Отсутствие безопасности в отношении типов и л л юстрируется закоммен­ тированными строками в конце программы. Давайте разберем каждый слу­ ч ай. Для начала рассмотрим следующую ситуацию: // int i = ( I ntege r ) raw . getOb ( ) ; / / ошибка во время выполнения В данном операторе получается значение оЬ внутри raw и приводится к I nteger. Проблема в том, что raw содержит значение DouЫe, а не целочис­ ленное значение. Тем не менее, на этапе компиляции обнаружить это невоз­ можно, поскольку тип raw неизвестен. В итоге оператор терпит неудачу во время выполнения. В следующей кодовой последовательности переменной strOb (ссылке типа Gen<String>) присваивается ссылка на низкоуровневый объект Gen: st rOb = raw ; // Нормально, но потенциально непра виль но / / String str = st rOb . getOb ( ) ; / / ошибка во время выполнения Само по себе присваивание синтаксич ески корректно, но сомнительно. Так как переменная s t rOb имеет тип Gen<String>, предполагается, что она содержит строку. Однако после присваивания объект, на который ссылается s t rOb, содержит DouЫe. Таким образом, когда во время выполнения пред­ принимается попытка присвоить содержимое strOb переменной str, возни­ кает ошибка, потому что теперь s t rOb содержит DouЫe. В результате при присваивании обобщенной ссылке низкоуровневой ссылки обходится меха­ низм безопасности типов. В показанной ниже кодовой последовательности предыдущий случай ин­ вертируется: Гл а в а 1 4. Об о б ще н ия 429 raw = iOb ; / / Нормально , но потенциально неправильно // d = ( DouЫ e ) raw . getOb ( ) ; // ошибка во время выполнения Здесь обобщенная ссылка присваивается низкоуровневой ссыл ке. Несмотря на правильность с точки зрения синтаксиса, могут возникнуть проблемы, как показано во второй строке. Теперь переменная raw ссылается на объект, который содержит объект I nteger, но приведение предполагает, что она содержит объект DouЫe. Проблема не может быть выявлена на этапе компиляции и взамен возникает ошибка во время выполнения. Из-за потенциальной опасности, присущей низкоуровневым типам, ком­ пилятор j avac отображает непроверяемые предупреждения при использовании низкоуровневого типа способом, который может поставить под угрозу без­ опасность к типам. Следующие строки в предыдущей программе приводят к выдаче компилятором непроверяемых предупреждений: Gen raw = new Gen ( DouЫe . valueOf ( 98 . 6 ) ) ; strOb = raw ; / / Нормально , но потенциально неправильно В первой строке вызывается конструктор Gen без аргумента типа, что ини­ циирует предупреждение. Во второй строке низкоуровневая ссылка присваи­ вается обобщенной переменной, что генерирует предупреждение. Поначалу вы можете подумать, что показанная далее строка тоже должна приводит к выдаче непроверяемое предупреждения, но это не так: raw = iOb ; // Нормально, но потенциально неправильно Компилятор не генерирует никаких предупреждений, т.к. присваивание не приводит к добавочной потере безопасности типов, нежели та, что уже про­ изошла при создании raw. И последнее замечание: вы должны ограничить использование низкоуров­ невых типов ситуациями, когда вам нужно смешивать унаследованный код с бол ее новым обобщенным кодом. Н изкоуровневые типы являются просто переходным средством, а не тем, что следует применять в новом коде. Иерархии обобщенных классов Подобно необобщенным обобщенные классы могут быть частью иерархии классов. Таким образом, обобщенный класс может выступать в качестве су­ перкласса или быть подклассом. Ключевая разница между обобщенными и необобщенными иерархиями связана с тем, что в обобщенной иерархии лю­ бые аргументы типов, необходимые обобщенному суперклассу, должны пере­ даваться вверх по иерархии всеми подклассами. Это похоже на способ пере­ дачи вверх по иерархии аргументов конструкторов. Использование обобще н ного с уперкласса Ниже показан простой пример иерархии, использующей обобщенный су­ перкласс: 430 Часть 1 . Язык Java / / Простая иерархия обобщенных классов . class Gen<T> { Т оЬ; Gen ( T о ) оЬ = о ; / / Возвратить оЬ . Т getOb ( ) { return оЬ; / / Подкласс Gen . class Gen2<T> extends Gen<T> { Gen2 ( T о ) { super ( o ) ; В этой иерархии Gen 2 расширяет обобщенный класс Gen. Обратите внима­ ние на объявление Gen 2 : class Gen2<T> extends Gen<T> { Параметр типа Т указ ан в Gen 2 и также передается Gen в конструкции exten ds, т.е. любой тип, переданный Gen 2 , тоже будет передаваться Gen. Например, сл едующее объявление передает Integer в качестве параметра типа в Gen: Gen2<Integer> num = new Gen2 < I nteger> ( 1 00 ) ; В итоге оЬ в порции Gen внутри Gen 2 будет иметь тип Integer. Обратите также внимание на то, что в Gen2 параметр типа Т не применя­ ется ни для каких целей кроме поддержки суперкласса Gen. Таким образом, даже если подкласс обобщенного суперкласса в противном сл учае не должен быть обобщенным, то он все равно должен указ ывать параметр типа или па­ раметры типов, требуемые для его обобщенного с уперкласса. Конечно, при необходимости подкласс может добавлять собственные па­ раметры типов. Например, вот вариант предыдущей иерархии, где Gen2 до­ бавляет собственный параметр типа: // Подкласс может добавлять собственные параметры типов . class Gen<T> { Т оЬ ; / / объявить объект типа Т / / Передать конструктору ссылку на объект типа Т . Gen ( Т о ) { оЬ = о ; / / Возвратить оЬ . Т getOb ( ) { return оЬ ; Глава 1 4. Обобщения 431 // Подкласс Ge n , определяющий в торой параметр типа по имени V . c l a s s Gen2 <T, V > ex tends Gen<T> { V оЬ2 ; Gen2 ( T о, V о2 ) super ( о ) ; оЬ2 = о2 ; V get0b2 ( ) return оЬ2 ; / / Создать объект типа Gen2 . c l a s s Hier Demo { puЬ l i c s tatic void ma in ( S tring [ ] ar gs ) { / / Создать объект Gen2 для S t r ing и In teger . Gen2<String, I nteger> х = new Gen2 < S t r ing , I ntеgе r> ( " Значение : " , 9 9 ) ; Sys tem . out . p rint ( x . g etOb ( ) ) ; System . out . println ( x . get0b2 ( ) ) ; Взгляните на объявление данной версии Gen2: c l a s s Gen2<T, V> extends Gen<T> ( Здесь Т - тип, переданный в Gen, а V - тип, специфичный для Gen2 . Пара метр типа V используется для объявления объекта по имени оЬ2 и в ка ­ честве возвращаемого типа для метода get0b2 ( ) . В методе ma in ( ) создается объект Gen2, в котором параметр типа Т является String, а пара метр типа V - Integer. Программа выводит вполне ожидаемый результат: Значение : 99 О боб щ енны й подкласс Необобщенный класс абсолютно законно может быть суперклассом обобщенного подкласса. Н апри мер, рассмотрим следующую программу: // Необобщен ный класс может быть суперклассом обобщенного подкласса . / / Необобщенный кла сс . c l a s s NonGen { int num; NonGen ( i nt i ) num = i ; int get num ( ) return num; 432 Часть 1. Язык Java / / Обобщенный подкласс . class Gen<T> extends NonGen Т оЬ ; / / объявить объе кт типа Т // Передать конструктору ссылку на объект типа Gen ( Т о, i n t i ) { super ( i ) ; оЬ = о ; т. / / Возвратить оЬ . Т getOb ( ) { return оЬ ; // Созда ть объе кт Ge n . class H i e rDemo2 { puЫ ic static void ma in ( S t r ing [ ] args } { // Создать объект Gen для String . Gen<Str ing> w = new Gen<String> ( " Дoбpo пожаловать " , 4 7 ) ; System . out . p r i nt ( w . ge tOЬ ( } + " " } ; System . out . pr intln ( w . getnum ( ) ) ; Вот вывод, rенерируемый проrраммой: Добро пожаловать 4 7 Обратите внимание в проrрамме на то, как класс Gen наследуется от NonGen: class Gen<T> ext eпds NonGen { Поскольку класс NonGen не является обобщенным, арrумент типа не ука­ зывается. Таким образом, хотя в Gen объявлен параметр типа Т, он не нужен (и не может использоваться) в NonGen. В результате NonGen наследуется клас­ сом Gen в обычной манере без применения особых условий. Срав нен ие типов в обобщенно й и ера рх и и во в ремя выполнения Вспомните операцию instanceof, предназначенную для получения инфор­ мации о типе во время выполнения, которая была представлена в rлаве 1 3. Как объяснялось, операция ins tanceof определяет, является ли объект экзем­ пляром класса. Она возвращает t rue, если объект имеет указанный тип или может быть приведен к указанному типу. Операцию ins tanceof можно приме­ нять к объектам обобщенных классов. В приведенном далее классе демонстри­ руются некоторые последствия совместимости типов обобщенной иерархии: // Использование операции iпs tanceof с иерархией обобщенных кла ссов . class Gen<T> { Т оЬ ; Глава 1 4. Обобщения Gen ( Т о ) оЬ = о; / / Возвратить оЬ . Т getOb ( ) ( return оЬ ; / / Подкласс Gen . class Ge n2<T> extends Gen<T> ( Gen2 (Т о ) ( super ( o ) ; // Демонстрация последствий идентификации типов во время / / выполнения для иерархии обобщенных классов . class HierDemoЗ ( puЬl ic static void ma in ( String [ ] args ) { / / Создать объе кт Gen для Integer . Gen < I nteger> iOb = new Ge n < I nteger> ( B B ) ; / / Созда ть объект Gen2 для Integer . Gen2<I nteger> iOb2 = new Gen2< I nteger> ( 9 9 ) ; / / Создать объект Gen2 для String . Gen 2 < String> st rOb2 = new Gen2 <String> ( " Tecт обобщений " ) ; / / Выяснить , является ли i0b2 ка кой-т о формой Gen2 . i f ( i0b2 inst anceo f Gen 2<?> ) System . o ut . printl n ( "i0b2 - экземпляр Gen 2 " ) ; / / Выяснит ь , я вляется ли iOb2 какой-то формой Gen . i f ( i0b2 instanceof Gen< ?> ) System . out . println ( " i0b2 - экземпляр Gen" ) ; Sys tem . out . printl n ( ) ; / / Выяснит ь , я вляется ли s t r0b2 э кземпляром Gen2 . i f ( st r0b2 instanceof Gеп2 < ? > ) System . out . p rintln ( " st r0b2 - экземпляр Gen2 " ) ; / / Выясни т ь , я вляется ли st r0b2 экземпляром Gen . if ( st r0b2 instanceof Gen< ? > ) System . out . print l n ( " str0b2 - э кземпляр Gen" ) ; System . out . printl n ( ) ; / / Выяснит ь , я вляется ли i Ob э кземпляром Gen2 , что не так . i f ( iOb instanceof Gen 2 < ? > ) S ystem . out . printl n ( " i Ob - экземпляр Gen2 " ) ; / / Выяснить , я вляется ли iOb э кземпляром Gen, что так . i f ( iOb iпstanceof Gen< ? > ) System . out . p rintl n ( " iOb - экземпляр Gеп " ) ; 433 434 Часть 1. Яз ы к Java Вот вывод: iOb2 - экземпляр Gen2 iOb2 - экземпляр Gen s t rOЬ2 - э кземпляр Gen2 strOЬ2 - экземпляр Gen i Ob - экземпляр Gen В этой програ мме класс Gen2 определен как подкласс Gen, который явля­ ется обобщенным по параметру типа Т. В методе main ( ) создаются три объ­ екта: iOb - объект типа Gen< Integer>, iOb2 - эк земпляр Gen2 < Integer> и s trOb2 - объект типа Gen2<String>. Затем в программе выполняются про­ верки с помощью instanceof для типа переменной i0b2 : / / Выяснить , является ли iОЬ2 какой-т о i f ( iOЬ2 ins tanceof Gen2 < ? > ) System . o ut . p ri ntln ( " i OЬ2 - экземпляр / / Выясни т ь , явл яется ли iОЬ2 какой-т о i f ( iOЬ2 ins tanceof Gen<?> ) System . o ut . p r i ntln ( " i 0b2 - э кземпляр формой Gen2 . Gen2 " ) ; формой Gen . Ge n " ) ; В выводе видно, что обе проверки успешны. В первой проверке тип пере­ менной iOb2 сравнивается с Gen 2 < ? >. Она проходит успешно, т.к. просто под­ тверждает, что iOb2 - объект некоторого типа объекта Gen2 . Использование подстановочного зна ка позволяет операции instanceof выяснить, является ли iOb2 объектом любого типа Gen2. Далее тип переменной iОЬ2 проверяется на предмет при надлежности к Gen< ? >, т.е. к типу суперк ласса. Проверка тоже успешна, поскольку iОЬ2 - некоторая форма суперкласса Gen. Последующие несколько строк в ma in ( ) повторяют ту же самую последовательность (с та­ кими же результатами) для переменной strOb2. После этого в программе по­ средством показанных ниже строк проверяется переменная iOb, представля­ ющая собой эк земпляр Gen< Integer> (суперкласса): // Выясни т ь , является ли iOb экземпляром Gen2 , что не так . i f ( iOb i n stanceof Gen 2 < ? > ) System . o ut . p r i ntln ( " i Ob - экземпляр Gen2 " ) ; / / Выяснит ь , является ли iOb экземпляром Gen , что так . i f ( iOb ins tanceof Gen< ? > ) System . o ut . println ( " i Ob - экземпляр Gen " ) ; Проверка в первом операторе i f терпит неудачу, потому что тип iOb не является каким-то типом объекта Gen2. Проверка во втором операторе if проходит успешно, т. к. iOb - некоторый тип объекта Gen. Приведение Привести один эк земпляр обобщенного класса к другому можно тол ько в том случае, если во всем осталь ном они совместимы и их аргументы типов совпадают. Например, в контексте предыдущей программы следующее приве­ дение будет допустимым, поскольку iОЬ2 включает эк земпляр Gen< Integer>: ( Gen<Integer> ) iОЬ2 / / �опустимо Глава 14. Обобщения 435 Но показанное ниже приведение не считается допустимым, т.к. iOb2 не является экземпляром Gen<Long>: (Gen<Long> ) iOb2 / / недопустимо Переоп ределение методо в в обобщен ном кла ссе Подобно любому другому методу метод в обобщенном классе можно пере­ определять. Например, рассмотрим программу, в которой переопределен ме­ тод ge tOb ( ) : / / Переопределение обобщенного метода в обобщенном классе . class Gen <T> { Т оЬ ; / / объявить объект типа Т / / Передать конструктору ссылку на объект типа Т . Gen ( Т о ) { оЬ = о ; / / Возвратить оЬ . Т getOb ( ) { System . out . print ( " getOb ( ) в Gen : " ) ; return оЬ ; // Подкласс Gen , в кот ором переопределяется getOb ( ) . class Gen2 <T> extends Gen<T> { Gen2 ( Т о ) { super ( o ) ; / / Переопределить getOb ( ) . Т getOb ( ) { System. out . print ( "getOb ( ) в Gen2 : " ) ; return оЬ ; // Демо нстрация переопределения обобщенного метода . class Ove rrideDemo { puЫic static vo id main ( S t ring [ ] args ) { // Создать объект Gen для I nteger . Gen< Integer> iOb = new Gen < I nteger> ( B B ) ; / / Созда ть объект Gen2 для Integers . Gen2 < I nteger> i0b2 = new Gen2 < I nteger> ( 9 9 ) ; // Созда ть объект Gen2 для Strings . Gen2 < S t ring> st r0b2 = new Gen2 <String> ( " Тест обобщений" ) ; System . o ut . println ( i Ob . getOb ( ) ) ; System . o ut . println ( i 0b2 . get0b ( ) ) ; Sys tem . out . println ( s tr0b2 . get0b ( ) ) ; Часть 1 . Язык Java 436 Вот вывод: getOb ( ) в Gen : 8 8 getOb ( ) в Gen2 : 99 getOb ( ) в Gen2 : Тест обобщений Как подтверждает вывод, для объектов типа Gen2 вызывается переопре­ деленная версия getOb ( ) , но для объектов типа Gen выз ывается версия getOb ( ) из суперкласса. Выведен ие типов и обобщен ия Начиная с версии JDK 7, появилась воз можность сокращения синтакси­ са, применяемого для создания экземпляра обобщенного типа. Для начала вз гляните на следующий обобщенный класс: class MyClass<T , V> { Т оЫ ; V оЬ2 ; MyCl as s (T o l , V о2 ) { оЫ = o l ; оЬ2 = о2 ; } // ... До выхода JDK 7 для создания экземпляра MyClass нужно было использ о­ вать оператор вида: MyClass< Integer , Str ing> mcOb = new MyCla s s < Integer , String> ( 98 , "Строка " ) ; Здесь аргументы типов ( I nteger и String) указываются дважды: первый раз, когда объявляется mcOb, и второй - когда экземпляр MyClass создается через операцию new. Поскольку обобщения появились в JDK 5, такая форма необходима для всех версий Java, предшествующих JDK 7. Хотя с этой формой не связ ано ничего плохого, она чуть более многословна, чем должна быть. В конструкции new типы аргументов типов могут быть без труда выведены из типа mcOb, а потому нет никаких причин указывать их повторно. Чтобы принять соответствующие меры, в JDK 7 добавлен синтаксический элемент, позволяющий избежать второго указания. Теперь предыдущее объявление можно переписать так: MyCla s s < Intege r , String> mcOb = new MyClass<> ( 98 , "Строка " ) ; Обратите внимание, что в части, где создается экземпляр, просто исполь­ з уются угловые скобки <>, которые представляют пустой список аргументов типов. Такую конструкцию наз ывают ромбовидной операцией. Она сообщает компилятору о необходимости выведения аргументов типов для конструкто­ ра из выражения new. Основное преимущество этого синтаксиса выведения типов связано с тем, что он сокращает довольно длинные операторы объяв­ лений. Гл а в а 1 4. Об о бщ ени я 437 Обобщим все упомянутое ранее. Когда применяется выведение типов, синтаксис объявления обобщенной ссылки и создания экземпляра имеет сле­ дующую общую форму: class -name<type-arg- li st> va r-name = new class -name <> ( cons-arg- list ) ; имя-класса< список-аргументов - типов> имя-переменной = new имя-кла сса<> ( список -аргументов -конструктора ) ; Здесь class -name - имя класса, type-arg- l i st - список аргументов ти­ пов, var-name - имя переменной, cons -arg- list - список аргументов кон­ структора. Список аргументов типов конструктора в new пуст. Выведение типов можно также применять к передаче параметров. Например, если добавить в MyClass метод isSame ( ) : boolean is Same ( MyClass<T, V> о ) { i f ( oЫ == о . оЫ && оЬ2 == о . оЬ2 ) return true ; else return fa lse ; тогда показанный ниже вызов будет законным: if (mcOb . isSame (new MyClass<> ( l , "тест " ) ) ) System . out . println ( "Oдинaкoвыe " ) ; В данном случае аргументы типов для аргумента, переданного методу isSame ( ) , могут быть выведены из типа параметра. В большинстве рассмотренных в книге примеров при объявлении экзем­ пляров обобщенных классов по-прежнему будет использоваться полный син­ таксис, что обеспечит работу примеров с любым компилятором Java, кото­ рый поддерживает обобщения. Применение полного синтаксиса также дает очень четкое представление о том, что именно создается, а это крайне важно в коде примеров, предлагаемых в книге. Тем не менее, вы можете использо­ вать синтаксис выведения типов в своем коде с целью упрощения имеющихся объявлений. В ы вед ен ие ти пов лока ль н ы х переменны х и обоб щения Как только что объяснялось, выведение типов уже поддерживается для обобщений за счет применения ромбовидной операции. Однако с обобщен­ ным классом можно также использовать средство выведения типов локаль­ ных переменных, добавленное в JDK 10. Например, с учетом класса MyClass, определенного в предыдущем разделе, следующее объявление: MyClass<I ntege r , St ring> mcOb = new MyClass< Integer, String> ( 9 8 , "Строка " ) ; можно переписать с применением средства выведения типов локальных пе­ ременных: var mcOb = new MyCl ass<Integer , String> ( 9 8 , "Строка " } ; В данном случае тип mcOb выводится как MyClass<Intege r , String>, по­ скольку это тип его инициализатора. Вдобавок обратите внимание на то, что 438 Часть 1. Язык Java использование var в результате дает более короткое объявление, нежели в противном случае. Обычно имена обобщенных типов могут оказываться до­ вольно длинными и (иногда) сложными. Применение var - еще один спо­ соб существенно сократи ть такие объявления. По тем же причинам, которые только что объяснялись относит ельно ромбовидно й операции, в оставшихся примерах, рассмотренных в кни ге, будет использоваться полный обобщен­ ный синтаксис, но в вашем коде выведение типов локальных переменных мо­ жет оказат ься весьма полезным. Сти рани е Знать все детали о том, как компилятор Java преобразует исходный текст программы в объектный код, обычно не нужно. Тем не менее, в случае с обоб­ щениями важно иметь некоторое общее представление о про цессе, посколь­ ку оно позволяе т понять, по какой причине обобщенные средс тва работают именно так, как работают, и почему их поведение иногда немного удивляет. В связи с этим имеет смысл кратко обсудить реализацию обобщений в Java. Важным ограничением, которое управляло способом добавления обобще­ ний в Java, была нео бходимость поддержания совместимости с предыдущими версиями Java. Попросту говоря, обобщенный код обязан был быть совмести­ мым с ранее существовавши м необобщенным кодом. Таким образом, любые изменения синтаксиса языка Java или машины JVM не должны были нару­ шать функционирование старого кода. Способ, которым в Java реализованы обобщения, удовлетворяющий упо мяну тому о грани чению, предусматривает применение стирания. Рассмотрим, каким образо м работает стирание. При компиляции кода Java вся информация об обобщенных типах удаляется (стирается), что подразуме­ вает замену пара метров типов их ограничивающим типом, которым является Obj ect, если не указано явное ограничение, и последу ющее применение над­ лежащих приведений (как определено аргументами типов) для обеспе чения совместимости с типами, указанными в аргументах типов. Такая совмести­ мость типов навязывается самим ко мпилятором. Подход к обобщениям по­ до бного рода означает, что пара метры типов во время выполнения не суще­ ствуют. Они попросту являются механизмом, относящимся к исходно му коду. Мостовые методы Иногда компилятор вынужден добавлять в класс мостовой метод с целью обработки си т уаций, в которых стирание типа переопределенного метода в подклассе не производит такое же стирание, как соответствующий метод в суперклассе. В таком случае генерируется ме тод, который задействует стира­ ние типов из суперкласса, и этот метод вызывает метод со стиранием типов, указанным в подклассе. Разумеется, мостовые методы встречаются только на уровне байт-кода; вы их не видите и не можете ими пользоваться. Гла ва 1 4. Об о б щения 439 Хотя мостовые методы не относятся к тому, чем вам обычно приходится з аниматься, все же поучительно проанал изировать ситуацию, когда они соз ­ даются. Взгляните на следующую программу: / / Ситуация , в которой создается мостовой метод . c l a s s Gen<T> { Т оЬ ; / / объявить объект типа Т / / Передать конструктору ссыпку на объект типа Т . Gen ( T о ) { оЬ = о ; / / Возвратить оЬ . Т getOb ( ) { return оЬ ; / / Подкла сс Gen . class Gen2 extends Gen< S t ring> { Gen 2 ( St r i ng о ) { supe r ( o ) ; / / Переопределенная версия getOb ( ) , специфичная для St ring . St ring getOb ( ) { System . out . print ( " Bызвaн метод getOb ( ) , специфичный для St ring : " ) ; ret u rn оЬ ; / / Демонстрация ситуации , когда требуется мостовой ме тод . class B ridgeDemo { puЫ ic static void main ( St ring [ ] a rg s ) { / / Создать объект Gen2 для St ring . Gen2 st rOЫ = new Gen2 ( " Т е ст обобще ний " ) ; System . out . println ( s t rOЫ . getOb ( ) ) ; Подкласс Gen2 в программе расширяет класс Gen, но делает это с приме­ нением верси и Gen, специф ичной для типа St ring, как показано в его объ­ явлении: c l a s s Gen2 extends Ge n<String> { Кроме того, внутри класса Gen2 метод getOb ( ) переопределяется с указа­ нием String в качестве возвращаемого типа: // Переопределенная версия getOЬ ( ) , специфичная для St ring . S t r ing getOb ( ) { System . out . pr int ( " Вызван метод getOb ( ) , специфичный для S t ri ng : " ) ; return оЬ ; 440 Часть 1. Язык Java Все действия вполне приемлемы. Единственная трудность заключается в том, что из-за ст ирания типов ожидаемой формой ge tOb ( ) будет : Ob j e ct getOb ( ) { // . . . Чтобы ре шить про бле му, ко мпилятор создает мостовой метод с показан­ ной выше сигнатурой, который вызывает версию getOb ( ) для типа Str ing. Таким образом, просмотрев файл класса Gen2 с по мощью j avap, вы заметите следу ющие методы: c lass Gen2 extends Gen<j ava . l ang . S t ri ng> Gen2 ( j ava . lang . S trin g ) ; j ava . lang . St ring getOb ( ) ; j ava . l ang . Ob j ect getOb ( ) ; / / мостовой метод Как видите, в файл класса Gen2 был включен мостовой метод. (Коммента­ рий до бавлен автором, а не j avap, и точный вывод мо же т варьироваться в зависимости от используемой версии Java.) Осталось сделать еще одно за мечание по поводу рассмотренного примера. Обратите внимание на то, что единственная разница между двумя версиями метода getOb ( ) связана с их возвра щаемым т ипом. Обычно в таком случае происходи т о шибка, но поскольку она не относится к исходно му коду, про­ блема не возникает и корректно обрабатывае тся JVM. Ошибки неоднозначности Внедрение обобщений порождает еще одну разно видность ошибок, о т ко­ торых вы должны защититься: неоднозна чность. Ошибки неоднозначности происходят, когда стирание приводит к тому, что два на вид разных обоб­ щенных объявления распознаются как один и тот же стертый тип, стано вясь причиной конфликта. Вот пример, касающийся перегрузки ме тодов: // Неоднозначность перегруженных методо в , возникающая из-за стирания . class MyGenCl a s s <T , V> { Т оЫ ; V оЬ2 ; // ... / / Эти два перегруженных метода неоднозначны и не скомпилируются . void set ( Т о ) { оЫ = о ; void set (V о ) оЬ2 = о ; } Обратите внимание, что в MyGenC lass объявлены два обоб щенных типа: т и v. Внутри MyGenClass предпринимается попытка перегрузки метода set ( ) на основе параметров типов Т и V, что выглядит разумным, поскольку Т и V ка­ жутся разными типами. Однако здесь имеются две проблемы неоднозначности. Глава 1 4. Обобщен и я 441 Первая проблема заклю чается в том, что исходя из того, как написан кл асс MyGenCla s s, на самом деле вовсе не обязательно, чтобы Т и V был и разными типами. Скажем, абсолютно корректно (в принципе} сконструировать объект MyGenClas s следующим образом: MyGenClass<String, String> obj = new MyGenClass<String, String> ( ) В данном случае т и V будут заменены типом String. В итоге обе версии метода set () становятся и дентичными, что, конечно же, является ошибкой. Вторая и более фундаментальная проблема связана с тем, что стирание т ипов для set ( ) сводит обе версии к такому виду: void set ( Obj ect о ) { // . . . Соответственно перегрузка метода set ( ) , предпринятая в MyGenClas s, по своей сути неоднозначна. Ошибки неоднозначности бывает сложно исправить. Например, если из­ вестно, что V всегда будет каким-то числовым типом, тогда вы можете попы­ таться исправить MyGenClass, переписав его объявление, как показано ниже: class MyGenClass<T , V extends NumЬer> { // в основном нормально ! Внесенное изменение приводит к успешной компиляции MyGenCl ass, и можно даже создавать объекты: MyGenClas s<String, Nurnber> х = new MyGenClas s <String , NumЬer> ( ) ; Такой код работает, потому что компилятор Java способен точно опреде­ л ить, какой метод вызывать. Тем не менее, неоднозначность снова возникает в следующей строке кода: MyGenC l a s s <Nurnber , NumЬer> х = new MyGenCla s s <NurnЬer , NurnЬer> ( ) ; Поскол ьку Т и V и меют тип NumЬer, какая версия set ( ) в этом случае должна вызываться? Теперь неоднозначен вызов метода set ( ) . Откровенно говоря, в предыдущем примере было бы гораздо л учше объ­ явить два метода с отличающимися именами, нежели пытаться перегрузить set ( ) . Часто решение проблемы неоднозначности предусматривает реструк­ туризацию кода, т.к. неоднозначность нередко означает наличие концепту­ альной ошибки в проекте. Некоторые о гран ичения обоб щ ений Существует несколько ограничений, которые следует и меть в виду при ра­ боте с обобщениями. Они связаны с созданием объектов параметров типов, статических членов, исключен и й и массивов. Все ограничения исследуются ниже. Невозмож ность созда т ь э кземпляры па раметров т ипов Создать экземпляр параметра типа невозможно. Например, рассмотрим следующий класс: 442 Часть 1. Язык Java // Создать экземпляр Т невозможно . class Gen<T> { Т оЬ ; Gen ( ) { оЬ = new Т ( ) ; / / Незаконно ! ! ! Здесь предпринимается незаконная попытка создать экземпляр т. Понять причину должно быть легко: компилятору не известен тип объекта, который нужно создать. Параметр типа т - это просто заполнитель. Ог ран иче н ия, каса ющи ес я стат ичес к их ч л енов Статические члены не могут использовать параметр типа, объявленный в объемлющем классе. Например, оба статических члена этого класса недопу­ стимы: class Wrong<T> { / / Ошибка , статические переменные не могут иметь тиn Т . static Т оЬ ; / / Ошибка , статические методы не могут исполь зовать т . static Т getOb ( ) { return оЬ; Хотя объявлять статические члены, в которых задействован параметр типа, объявленный в объемлющем классе, не разрешено, можно объявить статические обобщенные методы, которые определяют собственные параме­ тры типов, как делалось ранее в главе. Ог ран ич ен ия, к аса ющи ес я о б о бщ е н н ы х масс и вов Существуют два важных ограничения обобщений, которые применяются к массивам. Во-первых, нельзя создавать экземпляр массива, тип элементов ко­ торого является параметром типа. Во-вторых, невозможно создавать массив обобщенных ссылок для конкретного типа. Обе ситуации демонстрируются в показанной далее короткой программе: // Обобщения и массивы . class Gen<T extends Number> { Т оЬ ; Т [ ] val s ; / / нормально Gen ( T о , Т [ ] nums ) { оЬ = о ; / / Этот оператор недопустим . / / vals = new T [ l 0 ] ; / / невозможно создать массив элементов тиnа Т / / Но следующий оператор законен . val s = nums ; / / присваивать ссьmку на существующий массив разрешено Гл а в а 1 4. Обоб щ ен и я 443 class GenArrays { puЫ ic static void main ( String [ ] args ) { Integer [ ] n = { 1 , 2 , 3 , 4 , 5 } ; Gen<I nteger> iOb = new Gen<Integer> ( S 0 , n ) ; / / Невозможно создать массив обобщенных ссыпок для конкретного типа . / / Gen<Integer> [ ] gens = new Gen<Integer> [ l0 ] ; / / Ошибка ! / / Все нормально . / / нормально Gen<?> [ ] gens = new Gen<?> [ l 0 ] ; Как показано в программе, объявлять ссылку на массив типа Т разрешено: / / нормально Т [ ] val s ; Но создавать экземпляр массива элементов типа Т нельзя, как демонстри­ руется в следующей закомментированной строке: // val s = new T [ l 0 ] ; / / невозможно создать массив элементов типа Т Причина невозможности создать массив элементов типа Т связана с тем, что компилятор не в состоянии выяснить фактический тип создаваемого мас­ сива. Однако можно передать методу Gen ( ) ссылку на совместимый по типу массив при создании объекта и присвоить эту ссылку переменной vals: / / присваивать ссыпку на существующий массив разрешено vals = nums ; Прием работает, поскольку переданный в Gen массив относится к извест­ ному типу, который будет совпадать с типом Т во время создания объекта. Обратите внимание, что внутри ma in ( ) нельзя объявлять массив обоб­ щенных ссылок для конкретного типа, т.е. следующая строка кода не ском ­ пилируется: / / Gen<I nteger> [ ] gens = new Gen<I nteger> [ l 0 ] ; / / Ошибка ! Но можно создать массив ссылок на обобщенный тип, если используется подстановочный знак: Gen<?> [ ] gens = new Gen<?> [ l 0 ] ; / / нормально Такой подход лучше применения массива низкоуровневых типов, потому что будет выполняться, по крайней мере, хоть какая-то проверка типов. Ограничения , касающиес я обобщенных исключений Обобщенный класс не может расширять тип Th rowaЫ e, что означает невозможность создания обобщенных классов исключений. ГЛ А В А 15 Ля м бда-в ы ражения В ходе непрерывного развития и эволюции языка Java, начиная с его пер­ воначального выпуска 1.0, было добавлено много функциональных средств. Тем не менее, два из них выделяются тем, что они основательно трансфор­ мировали язык, коренным образом изменив способ написания кода. Первой фундаментальной модификацией стало добавление обобщений в JDK 5 (см. главу 14), а второй - внедрение .лямбда-выражений, которые являются пред­ метом настоящей главы. Появившиеся в JDK 8 лямбда-выражения (и связанные с ними функции) значительно улучшили язык Java по двум главным причинам. Во-первых, они добавили новые элементы синтаксиса, которые увеличили выразитель­ ную мощь языка. В процессе они упростили способ реализации ряда общих конструкций. Во-вторых, добавление лямбда-выражений привело к вклю­ чению в библиотеку Java API новых возможностей, среди которых возмож­ ность более легкой эксплуатации параллельной обработки в многоядерных средах, особенно в том, что касается обработки операций в стиле "for-each� и новый потоковый API, померживающий конвейерные операции с данными. Появление лямбда-выражений также послужило катализатором для других новых средств Java, включая стандартные методы (описанные в главе 9), кото­ рые позволяют определять поведение по умолчанию для методов интерфей­ са, и ссылки на методы (рассматриваемые здесь), позволяющие ссылаться на методы без их выполнения. В конечном итоге следует отметить, что точно так же, как обобщения трансформировали Java несколько лет назад, лямбда-выражения продолжают изменять Java сегодня. Попросту говоря, лямбда-выражения окажут влияние практически на всех программистов на Java. Они действительно настолько важны. Введени е в лямбда - выра жен ия Ключевым аспектом для понимания реализации лямбда-выражений в Java являются две конструкции: собственно лямбда-выражение и функциональ­ ный интерфейс. Начнем с простого определения каждого из них. Гла ва 15. Лямб д а-вы р а ж е ни я 445 Лямбда-выражение по существу представляет собой анонимный (т.е. безы­ мянный) метод. Однако такой метод не выполняется сам по себе. Взамен он используется для реализации метода, определенного функциональным ин­ терфейсом. Таким образом, лямбда-выражение приводит к форме анонимно­ го класса. Лямбда-выражения также часто называют замыканиями. Функц,иональный интерфейс - это интерфейс, который содержит один и только один абстрактный метод, обычно устанавливающий предполагаемое назначение интерфейса. Соответственно функциональный интерфейс, как правило, представляет одиночное действие. Например, стандартный интер­ фейс RunnaЫe является функциональным интерфейсом, поскольку в нем определен только один метод: run ( ) . Следовательно, run ( ) определяет дей­ ствие RunnaЫe. Кроме того, функциональный интерфейс задает ц,елевой тип лямбда-выражения. Важно понимать, что лямбда-выражение может приме­ няться только в контексте, в котором указан его целевой тип. И еще один момент: функциональный интерфейс иногда называют типом SAM, где SAM означает Single Abstract Method - единственный абстрактный метод. На заметку! В функциональном интерфейсе можно указывать любой открытый метод, определенный в Obj е ct, скажем, equa 1 s ( ) , не влияя на его статус "функционального интерфейса". Открытые методы класса Obj e c t считаются неявными членами функционального интерфейса, потому что они реали зуются экземпляром функционального интерфейса автоматически. Теперь давайте займемся более подробным обсуждением лямбда-выраже­ ний и функциональных интерфейсов. О сно вы ля мбда-выражени й Лямбда-выражение вводит в язык Java новый синтаксический элемент и операцию. Новая операция иногда называется лямбда-операц,ией или опера­ ц,ией стрелки и обозначается с помощью ->. Она делит лямбда-выражение на две части. В левой части указываются любые параметры, требующиеся в лямбда-выражении . (Если параметры не нужны, тогда используется пустой список параметров.) В правой части находится тело лямбда-выражения, кото­ рое определяет действия лямбда-выражения. Операцию -> можно выразить словом "становится" либо "достается': В Java определены два вида тела лямбда-выражения - с одиночным выра­ жением и с блоком кода. Сначала мы рассмотрим лямбда-выражения, опре­ деляющие единственное выражение, а позже в главе займемся лямбда-выра­ жениями, содержащими блочные тела. Прежде чем продолжить, полезно взглянуть на несколько примеров лямб­ да-выражен ий. Давайте начнем с лямбда-выражения, пожалуй, самого про­ стого вида, которое возможно записать. Результатом его вычислени я будет константное значение: ( ) -> 1 2 3 . 4 5 446 Часть 1. Язык Java Показанно е выше лямбда-выражение не принимает параметров, из-за чего список параметров пуст, и возвраща ет константно е значение 1 2 3 . 4 5. Таким образом, оно аналогично следующему методу: douЫe myMeth ( ) { return 1 2 3 . 4 5 ; } Разумеется, определяемый лямбда-выражени ем метод не имеет имени. А вот более интересное лямбда-выражени е: ( ) -> Math . random ( ) * 1 0 0 Это лямбда-выражени е получает псевдослучайно е значени е от Ma t h . random ( ) , умножает его на 1 0 0 и возвращает результат. Оно тоже не требует параметра. Когда лямбда-выражению необходим параметр, он указывается в списке параметров слева от лямбда-операции, например: ( n ) -> (n % 2 ) ==О Такое лямбда -выражени е возвращает t rue, если значение пара ме тра n оказывается четным. Хотя тип пара метра (n в данном случае) допуска ется указывать явно, часто поступать так нет никакой необходимости, поскольку во многих случаях тип параметра может быть выведен. Подобно именован­ но му методу в лямбда-выражении разрешено указывать любое нужное коли­ чество параметров. Ф у нк ц и ональ ные ин тер фей с ы Как упоминалось ранее, функциональный инт ерф ейс представляет собой такой инт ерфейс, в котором определен только один абстрактный метод. Если вы уже какое-то время программировали на Java, то поначалу мо гли полагать, что все методы интерфейса неявно являются абстрактными. Хотя до выхода JDK 8 это было верно, впоследствии сит уация изменилась. Как объяснялось в главе 9, начиная с JDK 8, для объявляемого в инт ерфейсе метода можно указывать стандартную реализацию. З акрытые и статические методы интер­ фейса тоже обеспечивают реализацию. В результат е теперь метод интерфейса будет абстрактным только в том случае, если для него не определена реа­ ли зация. Поскольку нестандартные, нестатические, незакры тые методы ин­ терфейса неявно абстрактны, нет нужды применять модификатор abs tract (правда, при желании его можно указывать). Ниже показан пример функционального интерфейса: interface MyNumЬer { douЫe getValue ( ) ; В данном случае метод getValue ( ) является неявно абстрактным и един­ ственным методом, определенным в MyNumЬer. Таким образом, MyNumЬer функциональный интерфейс, функция которого определяется getVa lue ( ) . Как упоминалось ранее, лямбда-выражение не выполняется само по себе. Оно скорее формирует реализацию абстрактного метода, определенного в Гла ва 1 5 . Лямбда-вы ражения 447 функциональном интерф ейсе, который указывает его цел евой тип. В резуль­ тате лямбда-выражение может быть указано только в контексте, где опреде­ лен целевой тип. Один из таких контекстов создается, когда лямбда-выраже­ ние присваивается ссылк е на функциональный интерфейс. Другие контексты целевого типа включают помимо прочего инициализацию переменных, опе­ раторы return и аргументы метода. Р ассмотрим пример, где будет продемонстрировано, как можно использо­ вать лямбда-выражение в контексте присваивания. Для начала объявляется ссыл ка на функ циональный интерфейс MyNumЬer: // Созда ть ссылку на ин т ерфейс MyNumbe r . MyNumЬer myNum; Затем лямбда-выражение присваивается созданной ссыл ке на интерф ейс: / / Использовать лямбда -выражение в контексте присваивания . myNum = ( ) -> 123 . 4 5 ; Когда лямбда-выражение встречается в контексте цел евого типа, автома­ тич еск и создается эк зе мпляр к ласса, который реализует функциональный интерфейс, а лямбда-выражение определяет поведение абстрактного метода, объявленного в функциональном интерфейсе. При вызове данного метода че ­ рез цель лямбда-выражение выполняется. Таким образом, лямбда-выражение пр едоставляет способ трансформации кодового сегмента в объект. В предыдущем примере лямбда-выражение становится реализацией метода getValue ( ) . В резу льтате следующий код отображает значение 1 2 3 . 4 5: / / Вызвать метод getValue ( ) , который реализован // присвоенным ра нее лямбда - выр ажением . System . out . println (myNum . getValue ( ) ) ; Поско льку лямбда-выражение, присвое нное переменной myNum, возвраща­ ет знач ение 1 2 3 . 4 5, это значение и будет получ ено при вызове getValue ( ) . Чтобы лямбда-выражение можно было при менять в контексте целевого типа, типы абстрактного метода и ля мбда-выражения должны быть совме­ стимыми. Скажем, если в абстрактном методе заданы два параметра типа int, то лямбда-выражение должно указывать два параметра , тип которых - либо явный int, либо может быть неявно выведен контекстом как i nt. Обычно тип и колич ество пара метров лямбда- выражения должны согласовываться с параметрами метода; возвращаемые типы должны быть совместимыми, а любые исключения, генерируемые ля мбда- выражением, должны быть допу­ стимыми для метода. Пример ы лямбда - в ы ражени й С учетом предыдущего обсуждения давайте взглянем на несколько про­ стых прим еров, иллюстрирующих основные концепции лямбда-выражений. В первом примере собраны вместе все части, показанные в предыдущем раз­ дел е: 448 Ч а сть 1. Яз ы к Java / / Демонстрация исполь зования простого лямбда-выражения . / / Функциональный интерфейс . i nterface MyNшnЬer { douЫe getValue ( ) ; class LamЬdaDemo { puЫ i c s tatic vo id main ( String [ ] args ) { MyNumЬe r myNum; / / объявить ссылку на интерфейс // Здесь лямбда- в ыражение представляет собой константное выражение . / / Когда оно присваивается myNum, конструируется экземпляр класса , / / где лямбда- выражение реализует метод getVa lue ( ) из MyNumber . myNum = ( ) -> 1 2 3 . 4 5 ; / / Вызвать метод getValue ( ) , предоставляемый ранее // присвоенным лямбда-выражением . System . out . p rintl n ( "Фикcиpoвaннoe значение : " + myNum . getValue ( ) ) ; / / Здесь исполь зуется более сложное лямбда- выражение . myNum = ( ) -> Ma th . r a ndom ( ) * 1 0 0 ; / / В следующих операторах вызывается лямбда- выражение / / из предыдущей строки кода . Sys tem . out . println ( " Случайное значение : " + myNum . getVa l ue ( ) ) ; System . o ut . pr i ntln ( " Eщe одно случайное значение : " + myNum . getValue ( ) ) ; // // // // Лямбда-выражение должно быт ь совместимым с методом, определенным в функциональ ном интерфейсе . Следовательно , показанный ниже код работ ать не будет : myNum = ( ) -> " 12 3 . 0 3 " ; / / Ошибка ! Вот вывод: Фиксированное значение : 1 2 3 . 4 5 Случайное значение : 8 8 . 9 0 6 6 3 6 5 0 4 1 2 3 0 4 Еще одно случайное значение : 53 . 0 0 5 8 2 7 0 1 7 8 4 1 2 9 Как уже упоминалось, лямбда-выражение должно быть совместимым с аб­ страктным методом, для реализации которого оно предназначено. По этой причине код в закомментированной строке в конце предыдущей программы недопустим, потому что значение типа String несовместимо с типом douЫe, т.е. возвращаемым типом метода getValue ( ) . В следующем примере иллюстрируется использование параметра с лямбдавыражением: / / Демонстрация использования лямбда-выраже ния , принимающего параметр . / / Еще один функциональный интерфейс . inter face Nume r i cTest { boolean tes t ( int n ) ; class LamЬda Demo2 { puЫ ic s tatic void ma in ( String [ ] a rgs ) { Глава 1 5 . Лямбда-выраже ния 449 / / Лямбда- выражение , которое проверяет, четное ли число. NumericTest is Even = (n) -> (n % 2 ) ==0 ; i f ( isEven . test ( l 0 ) ) System. out .println ( " l 0 -- четное число " ) ; i f ( ! i sEven .tes t ( 9 ) ) System. out .println ( " 9 -- нечетное число " ) ; / / Лямбда- выражение, которое проверяет, является ли / / число неотрицательным. NumericTest isNonNeg = (n) -> n >= О ; i f ( isNonNeg. test ( l ) ) System.out. println ( " l - - неотрицательное число" ) ; i f ( ! isNonNeg. test ( - 1 ) ) System . out. println ( "-1 -- отрицательное число" ) ; } Ниже показан вывод, генерируемый программой: 1 0 -- четное число 9 -- нечетное число 1 -- неотрицательное число - 1 -- отрицательное число В программе демонс трируется ключевой факт о лямбда-выражениях, тре­ бующий тщательного исследования. Обрати те особое внимание на лямбда­ выражение, которое выполняет проверку на четность: ( n ) -> (n % 2 ) ==0 Несложно замети т ь, что тип n не указан. Напро тив, он выводится из кон­ текста. В данном случа е тип n выводится из типа пара метра метода test ( ) , как определено инт ерфейсом NumericTest, т.е. int. Кроме того, тип параме­ тра в лямбда -выражении можно указ ывать явно. Например, вот тоже допу­ стимый способ записи предыдущего лямбда-выражения: ( int n ) -> ( n % 2 ) ==0 Здесь тип n явно указ ывается как int. Обычно задавать тип явным образом нет нео бходимости, но его можно указывать в ситуациях, ко гда это требуется. Начиная с JDK 1 1 , разрешено также применять var, чтобы явно обозначи т ь выведение типа локальной переменной для параметра лямбда-выражения. В про гра мме демонстрируется еще один важный момент, связанный с лямбда-выражениями: ссылка на функциональный инт ерфейс может исполь­ зоват ься для выполнения любого лямбда-выражения, которо е с ним совме­ стимо. Обрати т е внимание, что в програ мме о пределены два разных лямб­ да-выражения, совместимые с методом test ( ) функционального интерфейса Numeri cTest. Первое лямбда -выражени е, i sEven, выясняет, является ли зна­ чение четным, а второе, i sNonNeg, проверяет, является ли значение неотри­ цат ельным. В каждо м случае проверяется значение параметра n. Поскольку каждое лямбда-выражение совместимо с методом test ( ) , каждо е из них мо ­ жет быть выполнено через ссылку Nume ricTest. Прежде чем двигаться дальше, рассмотрим еще один момент. Когда лямбда­ выражение имеет только один параметр, нет необходимости заключат ь имя параметра в круглые скобки, если оно указано в левой части лямбда-операции. Например, записать лямбда-выражение, применяемое в программе, можно и так: n -> ( n % 2 ) ==0 450 Часть 1. Язык Java В целях согласованности в книге все списки параметров лямбда-выраже­ ний будут заключаться в круглые скобки, даже те, которые содержат только один параметр. Конечно, вы можете предпочесть другой стиль. В следующей программе демонстрируется использование лямбда-выраже­ ния, которое принимает два параметра. В этом случае оно проверяет, являет­ ся ли одно число множителем другого. // Демонстрация использования лямбда-выражения, принимающего два параметра. interface NumericTest2 { boolean test(int n, int d); class LambdaDemoЗ { puЫic static void main(String[] args) { // Это лямбда-выражение выясняет, является ли одно число // множителем другого. NumericTest2 isFactor = (n, d) -> (п % d) == О; if(isFactor.test(l0, 2)) System.out.println("2 является множителем 10"); if('isFactor.test(l0, 3)) System.out.println("З не является множителем 10"); Ниже показан вывод: 2 является множителем 10 3 не является множителем 10 Функциональный интерфейс NumericTest2 в программе определяет ме­ тод test (): boolean test(int n, int d); В данной версии для метода test ( ) указаны два параметра. Таким обра­ зом, чтобы лямбда-выражение было совместимым с test (), оно тоже должно иметь два параметра. Обратите внимание на способ их указания: (n, d) -> (n % d) == О Два параметра, n и d, указываются в списке параметров через запятую. Пример можно обобщить. Всякий раз, когда требуется более одного параме­ тра, их необходимо указывать в списке внутри круглых скобок в левой части лямбда-операции, отделяя друг от друга запятыми. Важно отметить один момент, касающийся множества параметров в лямб­ да-выражении: если нужно явно объявить тип параметра, тогда все параме­ тры обязаны иметь объявленные типы. Скажем, следующий код допустим: (int п, int d) -> (n % d) Но такой код - нет: (int n, d) -> (n % d) == О О Глава 15. Лямбда-выражения 451 Блочные лямбда�выражения Тело в лямбда-выражениях, показанных в предшествующих примерах, состояло из единственного выражения. Такой вид тела лямбда-выражения называется телом-выражением, а лямбда-выражение с телом-выражением одиночным лямбда-выражением. В теле-выражении код в правой части лямб­ да-операции должен содержать одно выражение. Хотя одиночные лямбда­ выражения весьма полезны, иногда ситуация требует более одного выраже­ ния. Для обработки таких случаев в Java поддерживается второй вид лямбда­ выражений, где в правой части лямбда-операции находится блок кода, кото­ рый может содержать более одного оператора. Тело этого вида называется блочным. Лямбда-выражения с блочными телами иногда называются блочны­ ми лямбда-выражениями. Блочное лямбда-выражение расширяет типы операций, которые могут быть обработаны в лямбда-выражении, поскольку позволяет телу лямбда-вы­ ражения содержать несколько операторов. Например, в блочном лямбда-вы­ ражении можно объявлять переменные, организовывать циклы, применять операторы if и switch, создавать вложенные блоки и т.д. Блочное лямбда­ выражение создается легко. Нужно просто поместить тело в фигурные скоб­ ки подобно любому другому блоку операторов. За исключением того, что блочные лямбда-выражения разрешают указы­ вать несколько операторов, они используются почти так же, как только что рассмотренные одиночные лямбда-выражения. Тем не менее, есть одно клю­ чевое отличие: вы обязаны явно применять оператор return, чтобы возвра­ тить значение. Поступать так необходимо, потому что тело блочного лямбда­ выражения не представляет одиночное выражение. Вот пример, в котором используется блочное лямбда-выражение для вы­ числения и возврата факториала значения int: // Блочное лямбда-выражение, которое вычисляет факториал значения int. interface NumericFunc { int func(int n); class BlockLamЬdaDemo puЫic static void main(String[] args) { // Это блочное лямбда-выражение вычисляет факториал значения int. NumericFunc factorial = (n) -> { int result = 1; for(int i=l; i <= n; i++) result = i * result; return result; }; System.out.println("Фaктopиaл 3 равен " + factorial.func(З)); System.out.println("Фaктopиaл 5 равен " + factorial.func(S)); 452 Часть 1. Язык Java Вот вывод: Факториал 3 равен 6 Факториал 5 равен 120 Обратите внимание в программе, что внутри блочного лямбда-выражения объявляется переменная по имени resul t, организуется цикл for и приме­ няется оператор return. Они разрешены в теле блочного лямбда-выражения. По существу тело блочного лямбда-выражения похоже на тело метода. И еще один момент: когда в лямбда-выражении встречается оператор return, он просто приводит к возврату из лямбда-выражения, но не к возврату из объ­ емлющего метода. Ниже в программе предлагается другой пример блочного лямбда-выраже­ ния, которое изменяет порядок следования символов в строке на противопо­ ложный: // Блочное лямбда-выражение, которое изменяет порядок // следования символов в строке на противоположный. interface StringF'unc { Striпg fuпc(Striпg n); class BlockLarnЬdaDemo2 { puЫic static void main(String [] args) { // Это блочное лямбда-выражение изменяет порядок // следования символов в строке на противоположный. StriпgF'unc reverse = (str) -> { String result = ""; iпt i; for(i = str.leпgth()-1; i >= О; i--) result += str.charAt(i); returп result; }; System. out. priпtlп("Строка LamЬda с противоположным порядком следования символов: " + reverse.func("LamЬda")); System.out. println("Строка Expressioп с противоположным порядком следования символов: " + reverse.func("Expressioп")); Программа генерирует следующий вывод: Строка LamЬda с противоположным порядком следования символов: adbmaL Строка Expressioп с противоположным порядком следования символов: пoisserpxE В этом примере в функциональном интерфейсе StringFunc объявлен метод func (), который принимает параметр типа String и возвращает тип String. Таким образом, в лямбда-выражении reverse тип str выводится как String. Обратите внимание, что метод charAt () вызывается на str, что до­ пустимо из-за выведения типа str в String. Глава 15. Лямбда-выражения 453 Обобщенные функциональные интерфейсы Само лямбда-выражение не может указывать параметры типа. Таким об­ разом, лямбда-выражение не может быть обобщенным. (Разумеется, из-за выведения типов все лямбда-выражения обладают некоторыми "обобщен­ ными" качествами.) Однако функциональный интерфейс, ассоциированный с лямбда-выражением, может быть обобщенным. В таком случае целевой тип лямбда-выражения частично определяется аргументом или аргументами ти­ пов, указанными при объявлении ссылки на функциональный интерфейс. Давайте попытаемся понять ценность обобщенных функциональных ин­ терфейсов. В двух примерах из предыдущего раздела использовались два разных функциональных интерфейса: один назывался NumericFunc, а дру­ гой - StringFunc. Тем не менее, оба интерфейса определяли метод func (), который принимал один параметр и возвращал результат. В первом случае типом параметра и возвращаемого значения был int, а во втором случае String. Таким образом, единственное отличие между двумя методами заклю­ чалось в типе требуемых данных. Вместо двух функциональных интерфейсов, методы которых различаются только типами данных, можно объявить один обобщенный интерфейс и применять его в обоих обстоятельствах. Подход демонстрируется в следующей программе: // Использование обобщенного функционального инт ерфейса с лямбда-выражениями. // Обобщенный функциональный интерфейс. interface SomeFunc<T> { Т func(T t); class GenericFunctionalinterfaceDemo puЫic static void main (String [] args) { / / Использовать версию String интерфейса SomeFunc. SomeFunc<String> reverse = (str) -> { String result = ""; int i; for(i = str. length ()-1; i >= О; i--) result += str.charAt(i); return result; }; System. out. println ("Строка LamЬda с противоположным порядком следования символов: " + reverse.func("LamЬda")); System.out.println("Cтpoкa Expression с противоположным порядком следования символов: " + reverse.func("Expression")); // Теперь использовать версию Integer интерфейса SorneFunc. SomeFunc<Integer> factorial = (n) -> { int result = 1; for(int i=l; i <= n; i++) result = i * result; return result; }; 454 Часть 1. Язык Java System.out.println("Фaктopиaл 3 равен " + factorial.func(З)); System.out.println("Фaктopиaл 5 равен " + factorial.func(5)); Вот вывод: Строка LamЬda с противоположным порядком следования символов: ac!ЬmaL Строка Expression с противоположным порядком следования символов: noisserpxE Факториал 3 равен 6 Факториал 5 равен 120 Обобщенный функциональный интерфейс SorneFunc объявлен в програм­ ме, как показано ниже: interface SomeFunc<T> Т func(T t); Здесь Т указывает возвращаемый тип и тип параметра func (). Это означа­ ет, что он совместим с любым лямбда-выражением, которое принимает один параметр и возвращает значение того же самого типа. Интерфейс SorneFunc используется для предоставления ссылки на два раз­ ных типа лямбда-выражений - String и Integer. Соответственно один и тот же функциональный интерфейс может применяться для ссылки на лямб­ да-выражения reverse и factorial. Отличается лишь аргумент типа, пере­ даваемый в SorneFunc. Передача лямбда-выражений в качестве аргументов Как объяснялось ранее, лямбда-выражение можно использовать в любом контексте, который предоставляет целевой тип. Один из них касается переда­ чи лямбда-выражения в виде аргумента. На самом деле передача лямбда-вы­ ражения в качестве аргумента является распространенным приемом приме­ нения лямбда-выражений. Более того, это очень эффективное использование, потому что оно дает возможность передавать исполняемый код в аргументе метода, значительно увеличивая выразительную мощь Java. Чтобы лямбда-выражение можно было передавать как аргумент, тип па­ раметра, получающего аргумент в форме лямбда-выражения, должен от­ носиться к типу функционального интерфейса, который совместим с лямб­ да-выражением. Хотя применение лямбда-выражения в качестве аргумента сложностью не отличается, все-таки полезно увидеть его в действии. Процесс демонстрируется в следующей программе: // Использование лямбда-выражений в качестве аргумента метода. interface StringFunc { String func(String n); class LamЬdasAsArgumentsDemo Глава 15. Лямбда-выражения 455 // Типом первого параметра этого метода является функциональный интерфейс. // Таким образом, ему можно передавать ссыпку на любой экземпляр реализации // данного интерфейса, в том числе экземпляр, созданный лямбда-выражением. // Во втором параметре указывается строка, с которой нужно работать. static String striпgOp(StringFuпc sf, Striпg s) { return sf.fuпc (s); puЬlic static void main (String [] args) ( String iпStr = "LamЬdas add power to Java"; Striпg outStr; System.out.priпtln("Иcxoднaя строка: " + inStr); // Простое одиночное лямбда-выражение, которое переводит // в верхний регистр строку, переданную методу striп<:JOp(). outStr = stringOp ((str) -> str. toUpperCase (), iпStr); System.out.priпtln(" Cтpoкa в верхнем регистре: " + outStr); // Передать блочное лямбда-выражение, которое удаляет пробелы. outStr = stringOp ( {str) -> { String result = " "; int i; for(i = О; i < str.leпgth(); i++) if(str.charAt(i) != ' ') result += str.charAt(i); return result; }, inStr); System.out.printlп("Cтpoкa после удаления пробелов: " + outStr); // Конечно, можно также передавать экземпляр StringFunc, заблаговременно // созданный лямбда-выражением. Например, после выполнения следующего // объявления reverse будет ссылаться на экземпляр StringFunc. StringFunc reverse = {str) -> [ String result = " "; int i; for(i = str.length()-1; i >= О; i--) result += str.charAt(i); return result; }; // Теперь reverse можно передать в первом параметре методу stringOp(), // поскольку этот параметр является ссылкой на объект StringFunc. System.out.println ("Cтpoкa с противоположным порядком следования символов: " + stringOp (reverse, inStr)}; Ниже показан вывод: Исходная строка: LamЬdas add power to Java Строка в верхнем регистре: LAМBDAS ADD POWER ТО JAVA Строка после удаления пробелов: LambdasaddpowertoJava Строка с противоположным порядком следования символов: avaJ ot rewop dda sadbmaL 456 Часть 1. Язык Java Первым делом обратите внимание в программе на метод stringOp (), который принимает два параметра. Первый объявлен с типом StringFunc, который является функциональным интерфейсом. Таким образом, данный параметр может получать ссылку на любой экземпляр StringFunc, в том числе созданный лямбда-выражением. Второй параметр stringOp () имеет тип String и представляет собой строку, в отношении которой выполняется операция. Теперь взгляните на первый вызов stringOp (): outStr= stringOp((str) -> str. toUpperCase(), inStr); Методу stringOp () в качестве аргумента передается простое одиночное лямбда-выражение, в результате чего создается экземпляр реализации функ­ ционального интерфейса StringFunc, ссылка на который передается в пер­ вом параметре stringOp (). Таким образом, методу передается лямбда-код, встроенный в экземпляр класса. Контекст целевого типа определяется типом параметра. Поскольку лямбда-выражение совместимо с этим типом, вызов будет допустимым. Встраивание простых лямбда-выражений вроде только что показанного в вызов метода часто оказывается удобным приемом, осо­ бенно когда лямбда-выражение предназначено для одноразового использо­ вания. Далее методу stringOp () передается блочное лямбда-выражение, удаля­ ющее пробелы из строки: outStr = stringOp((str) -> String result = ""; int i; for(i=0; i< str.length(); i++) if(str.charAt(i) != ' ') result += str.charAt(i); return result; }, inStr); Несмотря на применение блочного лямбда-выражения, процесс его пере­ дачи ничем не отличается от только что описанного процесса для одиночного лямбда-выражения. Однако в данном случае некоторые программисты со­ чтут синтаксис несколько неудобным. Когда блочное лямбда-выражение кажется слишком длинным для встра­ ивания в вызов метода, его легко присвоить переменной типа функциональ­ ного интерфейса, как делалось в предшествующих примерах. Затем методу можно просто передать эту ссылку. Такой прием демонстрировался в кон­ це программы, где определялось блочное лямбда-выражение, изменяющее порядок следования символов в строке на противоположный, и присваива­ лось переменной reverse, которая является ссылкой на экземпляр реализа­ ции StringFunc. Таким образом, reverse можно использовать как аргумент для первого параметра stringOp (). Затем в программе был вызван метод stringOp () с передачей ему переменной reverse и строки, с которой необ­ ходимо работать. Так как экземпляр, полученный при вычислении каждого Глава 15. Лямбда-выражения 457 лямбда-выражения, представляет собой реализацию StringFunc, каждый из них разрешено применять в качестве первого аргумента для stringOp (). И последнее замечание: в дополнение к инициализации переменных, при­ сваиванию и передаче аргументов контексты целевого типа создают приведе­ ния, операция?, инициализаторы массивов, операторы return, а также сами лямбда -выражения. Лямбда-выражения и исключения Лямбда-выражение может генерировать исключение. Тем не менее, если инициируется проверяемое исключение, то оно должно быть совместимым с исключением или исключениями, которые перечислены в конструкции throws абстрактного метода в функциональном интерфейсе. Рассмотрим пример вычисления среднего значения для массива элементов типа douЫe, иллюстрирующий данный факт. В елучае передачи массива нулевой длины генерируется специальное исключение EmptyArrayException. Как показано в примере, исключение EmptyArrayException присутствует в конструкции throws метода func () , объявленного внутри функционального интерфейса DouЬleNumericArrayFunc. // Генерация исключения в лямбда-выражении. interface DouЬleNurnericArrayFunc { douЫe func(douЬle [] n) throws ErnptyArrayException; class ErnptyArrayException extends Exception { ErnptyArrayException() ( super("Массив пуст"); class LarnbdaExceptionDerno { puЫic static void rnain(String [] args) throws ErnptyArrayException { douЫe [] values = { 1.0, 2. 0, 3. 0, 4. 0 }; // Это блочное лямбда-выражение вычисляет среднее // значение для массива элементов типа douЫe. DouЫeNurnericArrayFunc average = (n) -> douЫe surn = О; if(n.length == 0) throw new ErnptyArrayException(); for(int i=O; i < n. length; i++) surn += n[i] ; } return surn / n.length; }; Systern. out. println("Cpeднee значение равно " + average. func(values)); // Следующий код приводит к генерации исключения. Systern. out. println("Среднее значение равно " + average. func (new douЫe [О])) ; 458 Часть 1. Язык Java Первый вызов Average. func () возвращает значение 2. 5. Второй вызов, в котором передается массив нулевой длины, становится причиной генерации исключения EmptyArrayException. Не забывайте о необходимости включе­ ния конструкции throws в func (). Без нее программа не скомпилируется, потому что лямбда-выражение больше не будет совместимым с func (). В примере демонстрируется еще один важный момент, касающийся лямбда­ выражений. Обратите внимание, что параметр, указанный в методе func () функционального интерфейса DouЬleNumericArrayFunc, объявлен как мас­ сив. Однако параметром лямбда-выражения является просто n, а не n []. Помните о том, что тип параметра лямбда-выражения будет выводиться из целевого контекста. В этом случае целевой контекст - douЫe [] и потому типом n окажется douЫe []. Указывать тип параметра в виде n [] не нужно, да и недопустимо. Было бы законно явно объявить параметр как douЫe [] n, но здесь это ничего не даст. Лямбда-выражения и захват переменных Переменные, определенные в объемлющей области действия лямбда-вы­ ражения, доступны внутри лямбда-выражения. Скажем, лямбда-выражение может задействовать переменную экземпляра или статическую переменную, определенную в объемлющем классе. Лямбда-выражение также имеет доступ к ссылке this (явно и неявно), которая ссылается на вызывающий экземпляр класса, включающего лямбда-выражение. Таким образом, лямбда-выражение может получать или устанавливать значение переменной экземпляра или ста­ тической переменной и вызывать метод, определенный в объемлющем классе. Тем не менее, когда в лямбда-выражении используется локальная пере­ менная из его объемлющей области видимости, то возникает особая ситу­ ация, называемая захватом переменной. В таком случае лямбда-выражение может работать только с локальными переменными, которые являются фак­ тически финальными. Фактически финальная переменная представляет собой переменную, значение которой не меняется после ее первого присваивания. Явно объявлять такую переменную как final нет никакой необходимости, хотя поступать так не будет ошибкой. (Параметр this объемлющей области видимости автоматически будет фактически финальным, а лямбда-выраже­ ния не имеют собственной ссылки this.) Важно понимать, что локальная переменная из объемлющей области не может быть модифицирована лямбда-выражением, поскольку в таком случае исчез бы ее статус фактически финальной, из-за чего она стала бы незакон­ ной для захвата. В следующей программе иллюстрируется отличие между фактически фи­ нальными и изменяемыми локальными переменными: // Пример захвата локальной переменной из объемлющей области видимости. iпterface MyFunc { int func(int n); Глава 15. Лямбда-выражения 459 class VarCapture { puЫic static void main(String[] args) { // Локальная переменная, которая может быть захвачена. int num = 10; MyFunc myLamЬda = (n) -> // Использовать num подобным образом разрешено. // Переменная num не модифицируется. int v = num + n; // Однако следующая строка кода недопустима из-за того, // что она пытается модифицировать значение num. // num++; return v; }; // Следующая строка кода тоже вызовет ошибку, потому что // она устранит статус переменной num как фактически финальной. // num = 9; В комментариях указано, что переменная nurn является фактически фи­ нальной и потому может применяться внутри rnyLarnЬda. Однако если бы значение nurn изменилось внутри лямбда-выражения или за его пределами, то переменная nurn утратит свой статус фактически финальной, что вызовет ошибку и программа не скомпилируется. Важно подчеркнуть, что лямбда-выражение может использовать и моди­ фицировать переменную экземпляра из вызывающего его класса. Оно просто не может работать с локальной переменной из своей объемлющей области видимости, если только эта переменная не является фактически финальной. Ссылки на методы С лямбда-выражениями связана одна важная возможность, которая назы­ вается ссылкой на метод. Ссылка на метод предлагает способ обращения к методу, не инициируя его выполнение. Она имеет отношение к лямбда-выра­ жениям, поскольку тоже требует контекста целевого типа, состоящего из со­ вместимого функционального интерфейса. При вычислении ссылки на метод также создается экземпляр функционального интерфейса. Существуют различные виды ссылок на методы. Мы начнем со ссылок на статические методы. Ссылки на статические методы Для ссылки на статический метод применяется следующий общий синтак­ сис: имя-класса: :имя-метода 460 Часть 1. Язык Java Обратите внимание, что имя класса отделяется от имени метода двойным двоеточием. Разделитель : : был добавлен к языку в JDK 8 специально для этой цели. Ссылка на метод может использоваться везде, где она совместима со своим целевым типом. В показанной далее программе демонстрируется применение ссылки на статический метод: // Демонстрация использования ссылки на статический метод. // Функциональный интерфейс для операций над строками. interface StringFunc { String func(String n); // В этом классе определен статический метод по имени strReverse(). class MyStringOps { // Статический метод, который изменяет порядок следования / / символов на противоположный. static String strReverse(String str) { String result = ""; int i; for(i = str.length()-1; i >= О; i--) result += str.charAt(i); return result; class MethodRefDerno { // Первый параметр этого метода имеет тип функционального интерфейса. // Таким образом, ему можно передать любой экземпляр реализации // интерфейса StringFunc, вклю чая ссылку на метод. static String stringOp(StringFunc sf, String s) { return sf. func(s); puЫic static void rnain(String [] args) { String inStr = "LarnЬdas add power to Java"; String outStr; // Передать в stringOp() ссылку на статический метод strReverse(). outStr = stringOp (MyStringOps::strReverse, inStr); Systern. out.println("Иcxoднaя строка: " + inStr); Systern.out. println("Cтpoкa с противоположным порядком следования символов: " + outStr); Вот вывод: Исходная строка: LarnЬdas add power to Java Строка с противоположным порядком следования символов: avaJ ot rewop dda sadbrnaL Обратите особое внимание в программе на следующую строку: outStr = st ringOp(MyStringOps::strReverse, inStr); Глава 15. Лямбда-выражения 461 Здесь методу stringOp () в качестве первого аргумента передается ссыл­ ка на статический метод strReverse (), объявленный внутри MyStringOps. Код работает по причине совместимости: strReverse () с функциональным интерфейсом StringFunc. Таким образом, результатом вычисления выра­ жения MyStringOps:: strReverse будет ссылка на объект, в котором метод strReverse () предоставляет реализацию func () в StringFunc. Ссылки на методы экземпляра Чтобы передать ссылку на метод экземпляра конкретного объекта, ис­ пользуйте приведенный ниже базовый синтаксис: объектная-ссы.лка::имя-метода Как видите, синтаксис ссылки на метод экземпляра подобен синтаксису, применяемому для ссылки на статический метод, но вместо имени класса ис­ пользуется объектная ссылка. Предыдущую программу можно переписать с целью применения ссылки на метод экземпляра: // Демонстрация использования ссылки на метод экземпляра. // Функциональный интерфейс для операций над строками. interface StringFunc { String func(String n); // Теперь в этом классе определен метод экземпляра по имени strReverse(). class MyStringOps { String strReverse (String str) { String result = ""; int i; for(i = str.length()-1; i >= О; i--) result += str.charAt(i); return res ul t; class MethodRefDemo2 { // Первый параметр этого метода имеет тип функционального интерфейса. // Таким образом, ему можно передавать любой экземпляр реализации ! ! интерфейса StringFunc, включая ссыпку на метод. static String stringOp(StringFunc sf, String s) { return sf.func(s); puЫic static void main(String [] args) { String inStr = "LamЬdas add power to Java"; String outStr; // Создать объект MyStringOps. MyStringOps strOps = new MyStringOps(); // Передать в stringOp() ссылку на метод экземпляра strReverse(). outStr = stringOp(strOps: :strReverse, inStr); 462 Часть 1 . Язык Java System.out . print ln ( "Иcxoднaя строка : " + inStr); System.out . println("Cтpoкa с противоположным порядком следования символов : " + outStr); } Программа генерирует тот же вывод, что и ее предыдущая версия. Обратите внимание в программе, что s t rReve r s e ( ) теперь являет­ ся методом экземпляра MySt r ingOp s . Внутри mai n ( ) создается экземпляр MyS tri ngOps по имени strOps, который используется для создания ссылки на метод st rReverse ( ) при обращении к stringOp: out Str = stringOp ( strOps : : strReverse, inStr); В этом примере strReve rse ( ) вызывается на объекте strOps. Возможна также ситуация, коrда желательно указать метод экземпляра, который можно применять с любым объектом заданного класса, а не только с указанным объектом. В таком случае ссылку на метод необходимо создать, как показано ниже: имя -кла сса : : имя-мет ода - экземпляра Здесь вместо конкретноrо объекта используется имя класса, даже коrда указан метод экземпляра. В такой форме первый параметр функциональною интерфейса соответствует вызываемому объекту, а второй - параметру, за­ данному методом. Рассмотрим пример, rде определяется метод counter ( ) , подсчитывающий число объектов в массиве, которые удовлетворяют усло­ вию, определенному методом func ( ) функциональною интерфейса MyFunc. В этом случае он подсчитывает количество экземпляров класса HighTemp. / / Испол ьзование ссылки на метод экземпляра с разными о бъект ами . / / Функциональный интерфейс с методом , который получает два / / ссылочных аргумента и возвращает б улевский результат . interface MyFunc<T> { boolean f unc (T vl , Т v2); / / Класс , предназначенный для хран ения самой высокой температуры за сутки . class HighTemp { private int hTemp; H ighTemp (int ht) { hTemp = ht; } / / Возвратить true , если вызывающий о бъект HighTemp / / содержит такую же температуру , как у ht 2 . boolean sameTemp ( HighTemp ht2) { return hTemp == ht2.hTemp ; / / Возвратит ь true, если вызывающий объект HighTemp / / содержит температуру , которая ниже, чем у ht2. boolean lessThanTemp ( H ighTemp ht2) { return hTem p < ht2 . hTemp; Глава 15. Лямбда-выражения 463 class I ns tanceMethWithObj ectRefDerno { // Метод, возвращающий количество вхождений объекта, для которого // выполняются некоторые критерии, указанные параметром MyFunc. static <Т> int counter(T[] vals, MyFunc<T> f, Т v) { int count = О; for(int i=0; i < vals.length; i++) if(f.func(vals [ i ] , v)) count++; return count; puЫic static void rnain(String[] args) { int count; // Создать массив объектов HighTernp . HighTernp[] weekDayHighs = { new HighTemp(8 9), new HighTemp(90 ) , new HighTemp(8 9), new HighTemp(8 4 ), new new new new HighTemp(82), HighTemp(8 9), HighTemp(9 1), HighTernp(8 3 ) } ; // Использовать counter() с массивами элементов типа HighTemp. // Обратите внимание , что во втором аргументе передается ссылка // на метод экземпляра sameTemp(). count = counter(weekDayHighs, HighTernp: : sameTemp, new HighTemp(8 9)); System.out.println(" Koличecтвo суток, когда самая высокая температура была 8 9 градусов: " + count); // Создать и использовать еще один массив элементов типа HighTemp. HighTemp [ ] weekDayHighs2 = { new HighTemp ( 32), new HighTemp ( 12), new HighTemp(24 ), new HighTernp(l 9), new HighTemp(l 8), new H ighTernp(l2), new HighTemp(-1 ) , new HighTemp(lЗ) } ; count = counter(weekDayHighs2, H ighTemp: : sameTemp, new HighTemp(l2)); System.out.println(" Koличecтвo суток, когда самая высокая температура была 12 градусов: " + count); // Использовать lessThanTernp() для нахождения суток, когда температура // была ниже указанного значения. count = counter(weekDayHighs, HighTemp: : lessThanTemp, new HighTemp(8 9)); System.out.println(" Koличecтвo суток, когда самая высокая температура была меньше 8 9 градусов: " + count); count = counter(weekDayHighs2, HighTemp: : lessThanTemp, new HighTemp(l 9)); System.out.pr intln(" Koличecтвo суток, когда самая высокая температура была меньше 19 градусов: " + count); Вот вывод, генерируемый программой: Количество суток, когда самая высокая температура была 89 градусов: З Количество суток, когда самая высокая температура была 12 градусов: 2 Количество суток, когда самая высокая температура была меньше 8 9 градусов: З Количество суток, когда самая высокая темnература была меньше 19 градусов: 5 464 Часть 1. Язык Java Обратите внимание в программе, что класс Hig hTemp имеет два метода эк­ земпляра: sameTemp ( ) и lessTh anTemp ( ) . Метод sameTemp ( ) возвращает true, если два объекта HighTemp содержат ту же самую температуру. Метод lessThanTemp ( ) возвращает tr ue, если температура вызывающего объекта меньше температуры переданного объекта. Оба метода принимают параметр типа HighTemp и возвращают булевский результат. Следовательно, каждый метод совместим с функциональным интерфейсом MyFunc, поскольку тип вызывающе­ го объекта может быть сопоставлен с первым параметром func ( ) , а аргумент со вторым параметром func ( ) . Таким образом, когда следующее выражение: Hi ghTemp : : s ameTemp передается методу counter ( ) , создается экземпляр реализации функцио­ нального интерфейса MyFunc, в котором тип параметра первого параметра соответствует типу вызывающего объекта метода экземпляра, т.е. HighTemp. Типом второго параметра также будет H i g hTemp, потому что это тип па­ раметра sameTemp ( ) . То же самое утверждение справедливо и для метода lessThanTemp ( ) . Еще один момент: с помощью super можно ссылаться на версию метода из суперкласса: supe r : : имя Имя метода указывается в имя. Другая форма выглядит так: имя-типа . suреr : : имя где имя-типа относится к объемлющему классу или супер интерфейсу. Ссылки на методы и о б о бщ ения Ссылки н а методы можно применять с обобщенными классами и/или обобщенными методами. Например, взгляните на следующую программу : // Демонст ра ция исполь з ова ния ссыпки на об об щенный метод, // объявленный внут ри необобщенного кл а сса . // Функц иона ль ный инт ерфейс, который ра б от а ет с ма ссивом / / и зна ч ен и ем и возвра ща ет резуль та т int . int erface MyFunc<T> { int f unc ( T (] vals, Т v) ; } / / В этом кл а ссе оп ределен мет од п о имени countMatching() , кот орый // в озв раща ет количество э лементов в массиве, ра вных ука з а нному зна чению. / / Обра т ите внима ние, ч т о мет од countMatching ( ) является обобщенным , / / но кл а сс MyArrayOps - нет . cl ass MyArrayOps { stati c <Т> int cou ntMatching (Т [ ] vals, Т v) { i nt count = О ; f or ( int i=0; i < vals. length ; i++) if (vals (i] == v) count + + ; return count ; Глава 1 5 . Лямбда-выражения 465 class GenericMethodRefDemo { // Первый параметр этого метода имеет тип функционального интерфейса MyFunc. // в остальных двух параметрах он принимает массив и значение, оба типа Т. static <Т> int my0p ( MyFunc<T> f, Т [ ] vals, Т v) { return f.func (vals, v); puЬlic static void main (String [] args) { I nteger [] vals = { 1, 2, 3, 4, 2, 3, 4, 4, 5 }; String[] strs = { "0ne", "Two", "Three", "Two" }; int count; count = my0p ( MyArray0ps: : < I nteger>countMatching, vals, 4); System. out.println ("Koличecтвo элементов 4, содержащихся в vals: " + count); count = my0p ( MyArray0ps: : <String>countMatching, strs, "Two"); System.out. println ( "Количество элементов Two, содержащихся в strs: " + count); Ниже показан вывод: Количество элементов 4 , содержащихся в vals: 3 Количество элементов Two, содержащихся в strs : 2 В программе определен MyArrayOps - необобщенный класс, содержащий обобщенный метод по имени countMatching (), который возвращает количе­ ство элементов в массиве, равных заданному значению. Обратите внимание на способ указания аргумента обобщенного типа. Например, при первом вы­ зове в main () ему передается аргумент типа Integer: count = my0p ( MyArray0ps: : < Integer>countMatching, vals, 4); Вызов находится после : : . Такой синтаксис можно универсализировать: когда обобщенный метод указывается через ссылку на метод, его аргумент типа идет после : : и перед именем метода. Тем не менее, важно отметить, что явное указание аргумента типа в этой ситуации (и во многих других) не требуется, поскольку аргумент типа был бы выведен автоматически. В случа­ ях, когда указан обобщенный класс, аргумент типа следует за именем класса и предшествует : : . Хотя в предыдущих примерах демонстрировался механизм использова­ ния ссылок на методы, их реальные преимущества не были отражены. Ссылки на методы могут оказаться весьма полезными в сочетании с инфраструктурой Collections Framework, которая описана в главе 20. Однако для полноты картины мы рассмотрим короткий, но эффективный пример, в котором ссылка на метод применяется для определения самого большого элемента в коллекции. (Если вы не знакомы с инфраструктурой Collections Framework, тогда возвратитесь к это­ му примеру после проработки материала главы 20.) Один из способов нахождения самого большого элемента в коллек­ ции предусматривает использование метода max (), определенного в классе 466 Часть 1. Язык Java Collect i ons. Вызываемой здесь верси и метода max () необходимо пере­ дать ссылку на коллекцию и экземпляр объекта, реализующего интерфейс Compara tor<T>, который устанавливает, каким образом сравниваются два объекта. В нем определен только один абстрактный метод compare (), кото­ рый принимает два аргумента, имеющие типы сравниваемых объектов. Он должен возвращать значение больше нуля, если первый аргумент больше вто­ рого, ноль, если два аргумента равны, и значение меньше нуля, если первый объект меньше второго. В прошлом для применения метода max () с объектами, определенными пользователем, нужно было получить экземпляр реализации интерфейса Comparator<T>, явно реализовав его классом, создав экземпляр этого класса и передав его max () в качестве компаратора. Начиная с версии JDK 8, можно просто передать в max () ссылку на метод сравнения, поскольку в таком слу­ чае компаратор реализуется автоматически. В следующем простом примере иллюстрируется процесс создан ия коллекции ArrayL ist объектов MyClass и последующего поиска в ней объекта, который содержит наибольшее значение (как определено методом сравнения). // Использование ссылки на метод при поиске максимального значения в коллекции . import j ava.util.* ; class MyClass { private int val; MyClass ( int v ) val = v; } int getVal ( ) { return val; } class UseMethodR e f { // Мет од compareMC() , совмест и мый с методом compare ( ) , // который определен в Comparator<T> . static int compareMC ( MyClass а , MyClass Ь) { return a.getVal ( ) - b . getVal ( ); puЫic static void main ( String ( ] args ) { ArrayList<MyClass> al = new ArrayList<MyClass> ( ); al . add ( new al.add ( new al.add ( new al . add(new al.add (new al.add(n e w MyCl ass ( l ) ) ; MyCl ass ( 4 )); MyClass ( 2 ) ) ; MyClass ( 9)); MyClass( З ) ); MyClass(7)); // Найти максимальное значение в al , используя метод compar eMC ( ) . MyClass maxValObj = Collections.max ( al, U s eMethodRe f : : compareMC); System.out.println( "Максимальное значение равно: " + maxValObj .getVal() ) ; Вот вывод: Максимальное значение равно: 9 Глава 1 5. Лямбда-выражения 467 Обратите внимание в программе, что в самом классе MyClass никаких собственных методов сравнения не определено и не реализован интер­ фейс Comparator. Тем не менее, максимальное значение в списке элементов MyClass по-прежнему можно получить, вызывая метод max () , т.к. в классе UseMethodRef определен статический метод compareMC () , который совме­ стим с методом compare () , определенным в Comparator. Следовательно, явно реализовывать и создавать экземпляр реализации Comparator не при­ дется. Ссылки на констру кторы Подобно ссылкам на методы можно создавать ссылки на конструкторы. Ниже приведена общая форма синтаксиса, предназначенного для создания ссылки на конструктор: имя-кла сса : : new Такую ссылку можно присваивать любой ссылке на функциональный интерфейс, в котором определен метод, совместимый с конструктором. Рассмотрим простой пример: // Демонстрация исполь зования ссыпки на конструктор . // MyFunc - функциональный интерфейс, метод которого // возвращает ссыпку на конструктор MyClass. interface MyFunc { MyClass fuпc ( int n); class MyClass { private int val; // Конструктор, принимающий аргумент . MyClass(int v) { val = v; } // Стандартный конструктор. MyClass ( ) { val = О; } // ... int getVal ( ) { return val; } ; class ConstructorRefDemo { puЫic static void main ( String [] args) { // Создать ссылку на конструктор MyClass . // Поскольку метод func ( ) в My Func принимает аргумент, new ссыпается // на параметризованный конструктор MyClass, а не на стандартный . MyFunc myClassCons = MyClass: : new; // Создать экземпляр MyClass через эту ссыпку на конструктор . MyClass mc = myClassCons . func ( l O O); // Использовать только что созданный экземпляр MyClass. System . out . println ( "val в mc равно " + mc . getVal ( )); 468 Часть 1. Язык Java Вот вывод: val в те равно 100 Как видите, метод func ( ) класса MyFunc в программе возвращает ссылку типа MyCl a ss и принимает параметр int. Кроме того, в MyClass определены два конструктора. В первом имеется параметр типа int, а второй является стандартным конструктором без параметров. Теперь взгляните на следую­ щую строку: MyFunc myC lassC ons = MyClass: : new ; Выражение MyC l a ss : : new создает ссылку на конструктор MyC l a s s . Поскольку в данном случае метод func ( ) из MyFunc принимает параметр int, ссылка производится на конструктор MyC l ass ( int v ) , т.к. он обеспечивает соответствие. Вдобавок обратите внимание, что ссылка на этот конструктор присваивается ссылке MyFunc по имени myC l assCons. После выполнения оператора переменную myC l assCons можно использовать для создания эк­ земпляра MyC l ass: MyClass те = myCl assC ons. fun c ( 100) ; По существу myC l a ssCons становится еще одним способом вызова MyClass ( int v ) . Ссылки на конструкторы обобщенных классов создаются аналогичным об­ разом. Единственное отличие связано с возможностью указания аргумента типа. Как и в случае применения обобщенного класса для создания ссылки на метод, аргумент типа указывается после имени класса. Далее предыдущий при­ мер будет модифицирован, чтобы сделать MyFunc и MyC l ass обобщенными. // Демон страция и сп оль з ования ССЬLJ!КИ на к онструк тор обобщенного класса . // Теперь MyFunc - об общенный функциональ ный и нтерф ейс. interface MyFunc<T> { MyClass<T> func (T n ) ; class MyCl ass<T> { privat e Т val; // Конструктор, прини мающий аргумент. MyClass ( Т v) { val = v; ) // Станда ртный кон структор . MyClass ( ) { val = nul l ; ) // . . . Т getVal ( ) { return val; } ; c lass ConstructorRefDemo2 puЬ lic stat i c voi d main ( String [ ] args) { // Создать ссыпку на конструктор MyCl ass<T>. My Func<Integ er> myClassCons = MyClass<Integer> : : n ew ; // С оздать экземпляр MyClass< T> через эту ссыпку на конструктор . MyC lass<I nteger> mc = myC lassC ons.func(l00) ; Глава 15. Лямбда-выражения 469 // Испол ьзо вать толь ко что созданный экземпляр MyClass<T>. System . out . println ( "val в mc равно " + mc. getVal ( ) ) ; Новая версия программы выдает тот же результат, что и предыдущая вер­ сия. Разница в том, что теперь MyFunc и MyC l a s s определены как обобщен­ ные. Таким образом, последовательность создания ссылки на конструктор может включать аргумент типа (хотя он нужен не всегда): MyFunc<Integer> myClassCons = MyC lass<I nteger>: : new; Из-за того, что аргумент типа Integer уже был указан при создании myClassCons, его можно использовать для создания объекта MyClass <Integer>: MyClass<Intege r> mc = myClassCons.func ( l O O); Хотя в предшествующих примерах демонстрировался механизм работы со ссылкой на конструктор, реально использовать ее в подобной манере никто не будет, т.к. это не сулит никакой выгоды. Кроме того, наличие двух имен для одного и того же конструктора порождает безо всякого преувеличения запутанную ситуацию. Однако чтобы вы получили представление о более практичном применении, в следующей программе используется статический метод по имени myClassFactory ( ) , который реализует фабрику для объек­ тов MyFunc любого типа. Его можно задействовать при создании любого типа объекта, который имеет конструктор, совместимый с первым параметром myC l a s s Fact ory ( ) . // Реализа ция простой фабрики классов с использованием interface MyFunc<R , Т> { R func ( T n); ССЫJ'IКИ на конструктор. // Простой обобщенный класс . class MyClass<T > { private Т val; // Конструктор, принимающий аргумент. MyClass ( Т v) { val = v; } // Стандартный конструктор. В этой программе НЕ испол ьзуется . MyClass ( ) { val = null ; } // ... Т getVal () { return val; } ; // Простой необобщенный класс. class MyClass2 { St ring str ; // Конс труктор, принимающий аргумент . MyClass2 ( String s) { str = s; } // Стандартный конструктор . В этой про грамме НЕ испол ь зуется. MyClass2 ( ) { str = " " ; } // ... St ring getVal ( ) { return str ; } ; 470 Часть 1. Язык Java class ConstructorRefDemoЗ // Фабричный метод для объектов класса. Класс обязан иметь // конструктор, который принимает один параметр типа Т. // Тип создаваемого объекта указывается в R. static <R, T> R myClassFactory(MyFunc<R, Т> cons, т v) { return cons.func ( v); puЫic static void rna in (String [ ] args) { // Создать ссыпку на конструктор MyClass. // В этом случае new ссыпается на конструктор, принимающий аргумент. MyFunc<MyClass<DouЫe>, DouЫe> rnyClassCons = MyClass<DouЬle>: : new; // Создать экземпляр MyClass с применением фабричного метода. MyClass<DouЫe> rnc = rnyClassFactory (rnyClassCons, 1 0 0 . 1); // Использовать только что созданный экземпляр MyClass. Systern.out. pri ntln("val в mc равно " + rnc.getVal ( )); // Теперь создать другой класс с применением rnyClassFactory(). MyFunc<MyClass2, String> rnyClassCons2 = MyClass2: : new; • // Создать экземпляр MyClass2, используя фабричный метод. MyClass2 rnc2 = rnyClassFactory(myClassCons2, " LarnЬda "); // Использовать только что созданный экземпляр MyClass2. System.out.println("str в rnc2 равно " + mc2.getVal ( )); Ниже показан вывод, генерируемый программой: val в mc равно 100.1 str в mc2 равно LarnЬda Как видите, статический метод myClassFactory ( ) применяется для соз­ дания объектов типа MyClass<DouЫe> и MyClass2. Хотя оба класса разли­ чаются, скажем, MyClass является обобщенным, а MyClass2 - нет, с помо­ щью myClassFactory ( ) могут создаваться объекты обоих классов, т.к. они имеют конструкторы, совместимые с func ( ) в MyFunc. Прием работает, по­ тому что методу myClassFactory ( ) передается конструктор строящегося объекта. Возможно, вам захочется немного поэкспериментировать с про­ граммой, испытав разные созданные ранее классы. Также попробуйте создать экземпляры различных типов объектов MyCl ass. Вы заметите, что метод myClassFactory ( ) способен создавать любой объект, класс которого имеет конструктор, совместимый с func ( ) в MyFunc. Хотя приведенный пример до­ вольно прост, он раскрывает всю мощь, привносимую ссылками на конструк­ торы в язык Java. Прежде чем двигаться дальше, важно упомянуть о существовании второй формы синтаксиса ссылок на конструкторы, которая предназначена для мас­ сивов. Чтобы создать ссылку на конструктор для массива, используйте такую конструкцию: тип[] : : new Глава 1 5. Лямбда-выражения 471 Здесь посредством тип указывается тип создаваемого объекта. Например, при наличии определения MyClass из первого примера работы со ссылкой на конструктор (Cons tructorRe fDemo) и с учетом следующего интерфейса MyArrayCreator: interface MyArrayCreator<T> Т func (int n); вот как создать двухэлементный массив объектов MyC lass и присвоить каж­ дому начальное значение: MyArrayCreator<MyClass [ ] > mcArrayCons = MyClass[] : : new ; MyClass [ ] а = mcArrayCons . func(2); а[О] = new MyClass(l) ; a[l] = new MyClass(2); Вызов func ( 2 ) приводит к созданию двухэлементного массива. Как пра­ вило, функциональный интерфейс обязан содержать метод с единственным параметром типа int, если он должен применяться для ссылки на конструк­ тор массива. Предопределенные функц иональные интерф е й сы Вплоть до этого момента в примерах настоящей rлавы определялись соб­ ственные функциональные интерфейсы, что позволило четко проиллюстри­ ровать фундаментальные концепции, лежащие в основе лямбда-выражений и функциональных интерфейсов. Тем не менее, во многих случаях определять собственный функциональный интерфейс не понадобится, поскольку в паке­ те j ava . ut i l . function предлагается ряд предопределенных интерфейсов. Хотя они более подробно рассматриваются в части II, в табл. 15. 1 описано несколько таких интерфейсов. В показанной ниже программе демонстрируется работа интерфейса Function. В более раннем примере под названием Bloc kLamЬdaDemo вычис­ лялся факториал с использованием блочных лямбда-выражений и созданием собственного функционального интерфейса по имени NumericFunc. Однако можно было бы задействовать встроенный интерфейс Functi on, как сделано в новой версии программы: // Использование встроенного функционального интерфейса Function. // Импортировать интерфейс Function. import j ava.util.function.Function ; class UseFunctioni nterfaceDemo { puЫic static void main(String[] args) { // Это блочное лямбда-выражение вычисляет факториал значения iпt. // Теперь функциональным интерфейсом является Function. 472 Часть 1. Язык Java Function<Integer, Integer> factorial = (n ) -> int result = 1; for(int i= l; i <= n; i+ + ) result = i * result; return result; }; Systern. out.println( "Фaктopиaл 3 равен " + factorial . apply(З ) ) ; Systern. out.println( "Фaктopиaл 5 равен " + factorial . apply(S ) ) ; Программа выдает такой же вывод, как ее предыдущая версия. Таблица 1 5. 1 . Избранные предопределенные функциональные интерфейсы UnaryOperator<T> Применяет унарную операцию к объекту типа Т и возвращает результат тоже типа Т. Его метод называется apply () BinaryOperator<T> Применяет операцию к двум объектам типа Т и возвращает результат тоже типа Т. Его метод называется apply () Consumer<T> Применяет операцию к объекту типа называется accept () Suppl ier<T> Возвращает объект типа Т. Его метод называется get () Funct i on<T , R> Применяет операцию к объекту типа Т и возвращает в качестве результата объект типа R. Его метод называется apply () Predicate<T> Выясняет, удовлетворяет ли объект типа Т определенному ограничению. Возвращает булевское значение, указывающее на результат проверки. Его метод называется test () т. Его метод ГЛ А ВА ?.:;,,�:· � · 1·;.·:о ... �" - ,,.-� ;�� ti� .. "" � ., ..- ,' \,:,-� Модули � В версии JDK 9 появилось новое и важное средство, называемое модулями. Модули предоставляют способ описания отношений и зависимостей кода, из которого состоит приложение. Модули также позволяют контролировать то, какие части модуля доступны другим модулям, а какие - нет. За счет ис­ пользования модулей можно создавать более надежные и масштабируемые программы. Как правило, модули наиболее полезны в крупных приложениях, посколь­ ку они помогают сократить сложность управления, часто связанную с боль­ шой программной системой. Однако мелкие программы тоже выигрывают от применения модулей, потому что библиотека Java API теперь организована в виде модулей. Таким образом, теперь можно указывать, какие части Java API требуются вашей программе, а какие не нужны. Это позволяет разверты­ вать программы с меньшим объемом пространства хранения, потребляемо­ го во время выполнения, что особенно важно при создании кода, например, для небольших устройств, входящих в состав Интернета вещей (Internet of Things - IoT). Поддержка модулей обеспечивается как языковыми элементами, в том числе несколькими ключевыми словами, так и улучшениями j avac, j ava и других инструментов JDK. Кроме того, были предложены новые инструменты и форматы файлов. В результате JDK и исполняющая среда были существен­ но модернизированы с целью помержки модулей. Короче говоря, модули яв­ ляются важным дополнением и эволюционным шагом языка Java. Осн овы м одулей В наиболее основополагающем смысле модуль представляет собой группу пакетов и ресурсов, на которые можно коллективно ссылаться по имени мо­ дуля. В объявлении модуля указывается имя модуля и определяется отношение модуля и его пакетов с другими модулями. Объявления модулей записывают­ ся в виде операторов в файле исходного кода Java и поддерживаются несколь­ кими ключевыми словами, связанными с модулями: 474 Часть 1 . Язык Java exports rnodule open opens provi des requires to transitive uses with Важно понимать, что перечисленные выше ключевые слова распознаются как ключевые слова только в контексте объявления модуля. В других ситуа­ циях они интерпретируются как идентификаторы. Таким образом, ключевое слово rnodule можно было бы использовать в качестве имени параметра, хотя поступать так, безусловно, не рекомендуется. Тем не менее, создание контек­ стно-зависимых ключевых слов, связанных с модулем, предотвращает воз­ никновение проблем с существующим кодом, в котором одно или несколько таких слов могут быть выбраны для идентификаторов. Объявление модуля содержится в файле по имени rnodule-in f o . j ava, т.е. модуль определяется в файле исходною кода Java. Файл rnodule-in fo . j ava затем компилируется с помощью j avac в файл класса и известен как его дескриптор модуля. Файл rnodule-in fo . j ava должен содержать только определение модуля, но не другие виды объявлений. Объявление модуля начинается с ключевого слова rnodule. Вот ero общая форма: module имя-модуля { / / определение модуля Имя модуля указывается в имя-модуля и обязано быть допустимым иден­ тификатором Java или последовательностью идентификаторов, разделенных точками. Определение модуля находится в фигурных скобках. Хотя определе­ ние модуля может быть пустым (что приводит к объявлению, которое просто именует модуль), обычно в нем присутствует одна или несколько конструк­ ций, устанавливающих характеристики модуля. П ростой пример модуля В основе возможностей модуля лежат две ключевые особенности. Первая из них - способность модуля сообщать о том, что ему требуется другой мо­ дуль. Другими словами, один модуль может указывать, что он зависит от другого модуля. Отношение зависимости задается с помощью оператора requi res. По умолчанию наличие необходимого модуля проверяется как на этапе компиляции, так и во время выполнения. Второй ключевой особен­ ностью является способность модуля контролировать, какие из ero паке­ тов доступны другому модулю, что достигается с помощью ключевого слова exports. Открытые и защищенные типы внутри пакета доступны другим мо­ дулям только в том случае, если они явно экспортированы. Здесь мы займем­ ся примером, в котором демонстрируются обе возможности. В следующем примере создается модульное приложение, в котором за­ действованы простые математические функции. Хотя это приложение специ­ ально сделано очень маленьким, оно иллюстрирует основные концепции и Глава 1 6. Модули 475 процедуры, необходимые для создания, компиляции и запуска кода на осно­ ве модулей. Вдобавок показанный здесь общий подход применим и к более крупным, реальным приложениям. Настоятельно рекомендуется проработать пример на своем компьютере, внимательно следуя каждому шагу. На заметку! В главе описан процесс создания, компиляции и запуска кода на основе модулей с помощью инструментов командной строки. Такой прием обладает двумя преимуществами. Во-первых, он подойдет всем программистам на Java, потому что не требует IDЕ-среды. Во­ вторых, он очень четко иллюстрирует основы системы модулей, в том числе ее работу с каталогами. Вам придется вручную создать несколько каталогов и обеспечить размещение каждого файла в надлежащем каталоге. Как и следовало ожидать, при создании реальных приложений на основе модулей вы, по всей видимости, обнаружите, что легче пользоваться IDЕ-средой с поддержкой модулей, поскольку обычно она автоматизирует большую часть процесса. Однако изучение основ модулей с применением инструментов командной строки гарантирует, что вы обретете устойчивое понимание темы. В приложении определяются два модуля. Первый модуль имеет имя appstart и содержит пакет appstart. mymodappdemo, устанавливающий точ­ ку входа приложения в классе MyModAppDemo. Таким образом, MyModAppDemo содержит метод main ( ) приложения. Второй модуль называется appfuncs и содержит пакет app funcs. s i mple funcs, который включает класс SimpleMathFuncs. В классе SimpleMathFuncs определены три статических метода, реализующие ряд простых математических функций. Все приложение будет размещаться в дереве каталогов, которое начинается с mymodapp. Прежде чем продолжить, уместно обсудить имена модулей. В последую­ щих примерах имя модуля (скажем, appfuncs) является префиксом имени пакета (например, appfuncs . simplefuncs), который он содержит. Поступать так вовсе не обязательно, но такой прием используется в примере как спо­ соб четко указать, к какому модулю принадлежит пакет. Вообще говоря, при изучении и экспериментировании с модулями полезно выбирать короткие простые имена вроде тех, что применяются в настоящей главе, но вы воль­ ны использовать любые удобные имена, которые вам нравятся. Тем не менее, при создании модулей, пригодных для распространения, вы должны соблю­ дать осторожность в отношении назначаемых имен, т.к. желательно, чтобы эти имена были уникальными. На момент написания книги рекомендовалось применять метод обратных доменных имен, при котором обратное имя до­ мена, "владеющего" проектом, используется в качестве префикса для модуля. Скажем, в проекте, ассоциированном с herbschildt . com, для префикса мо­ дуля будет применяться com . herbsch ildt. (То же самое касается имен паке­ тов.) Поскольку соглашения об именовании со временем могут меняться, вам следует искать текущие рекомендации в документации по Java. Итак, начнем. Первым делом создайте необходимые каталоги для исходно­ го кода, выполнив перечисленные ниже шаги. 1. Создайте каталог по имени mymodapp. Он будет служить каталогом верхнего уровня для всего приложения. 476 Часть 1. Язык Java 2. Внутри myrnodapp создайте подкаталог под названием appsrc. Он по­ служит каталогом верхнего уровня для исходного кода приложения. 3. Внутри appsrc создайте подкаталог appstart, в нем - подкаталог с таким же именем appstart, а внутри него - подкаталог myrnodappdemo. В резуль­ тате вы получите следующее дерево каталогов, начинающееся с appsrc: apps rc\ appst art \ appst art \mymod appd emo 4. Внутри appsrc создайте подкаталог appfuncs, в нем - подкаталог с таким же именем appfuncs, а внутри него - подкаталог simplefuncs. В итоге вы получите следующее дерево каталогов, которое начинается с appsrc: apps rc\ appfuncs \ appfuncs \simpl efuncs Вот как должно выглядеть окончательное дерево каталогов: mymod app 1 apps rc /� apps tart 1 appfuncs 1 apps tart appfuncs mym od appd em o s impl efu ncs После настройки каталогов можете заняться созданием файлов исходного кода приложения. В текущем примере будут использоваться четыре файла исходного кода. В двух из них определяется приложение. Ниже приведено содержи­ мое первого файла, S impl eMathFuncs . j ava. Обратите внимание, что класс S impleMathFuncs находится в пакете appfuncs . simple funcs. / / Простые ма т ема тич е ски е функции . pack ag e appfu ncs . sim plefu ncs ; puЬlic cl as s Sim pl eM athFu ncs { / / Выяснить , я вляе тс я ли а делителем Ь . puЫ ic s tatic b ool ean i s F act or( int а , i nt Ь ) i f ( ( Ь %а ) == О ) retu rn t ru e; retu rn f al s e; Глава 16. Модули 477 // Возвратит ь наименьший положительный делитель, общий для а и Ь. puЫic static int lcf(int а, int Ь) { // Разложить на множители, используя положит ельные значения. а = Math.abs(a) ; Ь = Math.abs(Ь) ; int min = а < Ь ? а : Ь ; for(int i = 2; i <= min/2; i + + ) { if(isFactor(i, а) & & isFactor(i , Ь)) return i ; return 1 ; // Возвратить наибольший положительный делитель, общий для а и Ь. puЫic static int gcf(int а, int Ь) { // Разложить на множители, используя положительные значения. а = Math.abs(a) ; Ь = Math.abs(b) ; int min = а < Ь ? а : Ь ; for(int i = min/2; i > = 2 ; i--) { i f(isFactor(i, а) & & isFactor(i, Ь ) ) return i ; return 1 ; В классе Simp leMathFuncs определяются три простых статических мето­ да, которые реализуют математические функции. Первый метод, isFactor ( ), возвращает true, если а является делителем Ь. Метод lcf () возвращает наи­ меньший делитель, общий для а и Ь. Другими словами, он возвращает наи­ меньший общий делитель для чисел а и Ь. Метод gcf () возвращает наиболь­ ший делитель, общий для а и Ь. В обоих случаях возвращается 1, когда общие делители не найдены. Файл SimpleMathFuncs . j ava должен быть помещен в следующий каталог: appsrc\appfuncs\appfuncs\simplefuncs Это каталог пакета appfuncs . simplefuncs. Далее показано содержимое второго файла исходного кода, MyModAppDemo . j ava, в котором задействованы методы класса Simp leMathFuncs. Обратите внимание, что он находится в пакете appstart . mymodappdemo. Кроме того, в нем импортируется класс SimpleMathFuncs, т.к. в своей работе он полагается на SimpleMathFuncs. // Простой пример приложения, основанного на модулях. package appstart. mymodappdemo ; import appfuncs.simplefuncs.SimpleMathFuncs ; puЫic class MyModAppDemo { puЫic static void main(String [] args ) { 478 Часть 1. Язык Java i f ( S impl eMathFuncs . i sFactor ( 2 , 1 0 ) ) System . out . println ( " 2 является делителем 1 0 " ) ; System . out . println ( " Haимeньший общий делитель для 3 5 и 1 0 5 равен " + S impleMa thFuncs . l cf ( 3 5 , 1 0 5 ) ) ; System . out . println ( " Haибoльший общий делитель для 3 5 и 1 0 5 равен " + S impleMathFuncs . gcf ( 3 5 , 1 0 5 ) ) ; Файл MyModAppDemo . j ava должен быть помещен в следующий каталог: appsrc\appstart\appstart\mymodappdemo Это каталог пакета appstart . mymodappdemo. Затем для каждого модуля понадобится добавить файлы module- info . j ava, которые содержат определения модулей. Первым делом добавьте файл, определяющий модуль appfuncs: / / Определение модуля математических функций . modul e appfuncs { / / Экспортировать пакет appfuncs . s imple funcs . exports appfuncs . s implefuncs ; Как видите, модуль appfuncs экспортирует пакет appfuncs . s implefuncs, что делает его доступным для других модулей. Данный файл потребуется по­ местить в следующий каталог: appsrc\appfuncs Таким образом, он попадает в каталог модуля appfuncs, который находит­ ся выше каталогов пакетов. Наконец, ниже представлено содержимое файла modu l e - i n fo . j a va для модуля app s t art. Обратите внимание, что app s tart требует модуля appfuncs. // Определение модуля главного приложения . module appstart { // Требует модуля appfuncs . requires appfuncs ; Этот файл должен быть помещен в каталог своего модуля: appsrc\appstart Прежде чем приступить к более подробным исследованиям операторов requires, exports и module, давайте скомпилируем и запустим код примера. Удостоверьтесь в том, что корректно создали каталоги и поместили каждый файл в соответствующий каталог. Комп ил я ц и я и за п у ск п ерво го п римера модул я В версии JDK 9 компилятор j avac был обновлен с целью поддержки мо­ дулей. Таким образом, подобно всем другим программам на Java програм- Глава 16. Модули 479 мы на основе модулей компилируются с применением j avac. Процесс прост, а основное отличие заключается в том, что обычно будет явно указываться путь к модулю, который сообщает компилятору местоположение для скомпи­ лированных файлов. Прорабатывая пример, не забывайте о необходимости вводить команды j avac в каталоге rnymodapp, чтобы пути были правильными. Вспомните, что rnymodapp является каталогом верхнего уровня для всего мо­ дульного приложения. Первым делом скомпилируйте файл SirnpleMath Funcs . j ava, используя следующую команду: j avac -d appmodules\appfuncs appsrc\appfuncs\appfuncs\simplefuncs \SimpleMathFuncs. j ava Как уже упоминалось, команда должна вводиться в каталоге rnymodapp. Обратите внимание на применение параметра -d, с помощью которого ком­ пилятору javac сообщается о том, куда поместить выходной файл . class. В примерах, рассматриваемых в главе, вершиной дерева каталогов для скомпилированного кода является apprnodu l es. Приведенная выше ко­ манда по мере необходимости создает выходные каталоги пакетов для appfuncs . sirnplefuncs внутри apprnodu les\appfuncs. А вот команда javac, которая компилирует файл rnodu le-info . java для модуля appfuncs: j avac -d appmodules\appfuncs appsrc\appfuncs\module-info.j ava Она помещает файл rnodu le-info . class в каталог apprnodu les \appfuncs. Хотя предыдущий двухэтапный процесс работает, он был показан в первую очередь ради обсуждения. Обычно компилировать файл rnodu l e - inf o . j ava и файлы исходного кода модуля проще в одной командной строке. Ниже две предшествующих команды j avac объединены в одну: j avac -d appmodules\appfuncs appsrc\appfuncs\module-info.j ava appsrc\ appfuncs\appfuncs\simplefuncs\SimpleMathFuncs.j ava Команда обеспечивает помещение каждого скомпилированного файла в соответствующий каталог модуля или пакета. Теперь скомпилируйте файлы rnodu le - in fo . j ava и MyModAppDerno . j ava для модуля appstart: j avac --module-path appmodules -d appmodules\appstart appsrc\ appstart\module-info.j ava appsrc\appstart\appstart\mymodappdemo \MyModAppDemo.j ava Обратите внимание на наличие параметра --rnodu le-path. Он задает путь к модулю, т.е. путь, который компилятор будет просматривать в поисках поль­ зовательских модулей, затребованных в файле rnodu le- inf о . java. В этом елу­ чае компилятор будет искать модуль appfuncs, потому что он нужен модулю appstart. Кроме того, выходной каталог в команде указан как apprnodu les\ appstart. Таким образом, файл rnodu le-in f o. class будет располагаться в каталоге модуля apprnodu les\appstart, а MyModAppDerno . class - в каталоге пакета apprnodu les \apps tart \apps tart \rnymodappderno. 480 Часть 1. Язык Java Завершив компиляцию, можете запустить приложение с помощью коман­ ды j ava: j ava --module-path appmodules -m appstart/appstart .mymodappdemo . MyMoc!AppDemo В параметре --module-path задан путь к модулям приложения. Как упо­ миналось ранее, appmodules представляет собой каталог в верхней части де­ рева скомпилированных модулей. В параметре -m указан класс, содержащий точку входа приложения, и в рассматриваемом случае это имя класса, содер­ жащего метод main (). В результате запуска программы вы увидите следую­ щий вывод: 2 является делителем 1 0 Наименьший общий делитель для 3 5 и 1 0 5 равен 5 Наибольший общий делитель для 3 5 и 1 0 5 равен 7 Б олее подроб ный а н ал из о п ераторов requires и exports Предыдущий пример приложения, основанного на модулях, опирался на две основополагающих особенности модульной системы: возможность уста­ новления зависимости и способность ее удовлетворения. Такие возможности указываются посредством операторов requi res и expo rts в объявлении мо­ дуля. Каждый заслуживает более пристального изучения. Вот форма оператора requi res, используемая в примере: requires moduleName ; Здесь в moduleName задано имя модуля, который требуется модулю, в ко­ тором встречается оператор requ ires. Это означает, что для успешной ком­ пиляции текущего модуля требуемый модуль должен существовать. На языке модулей говорят, что текущий модуль читает модуль, указанный в опера­ торе requi res. Когда есть потребность в нескольких модулях, они должны задаваться в собственных операторах requ ires. Таким образом, объявление модуля может содержать набор различных операторов requi res. В общем случае оператор requi res позволяет гарантировать, что программа имеет доступ к модулям, в которых она нуждается. Ниже показана общая форма оператора exports, применяемая в примере: exports packageName ; В packageName указывается имя пакета, экспортируемого модулем, в кото­ ром находится данный оператор. Модулю разрешено экспортировать столь­ ко пакетов, сколько необходимо, причем каждый из них задается в отдель­ ном операторе expo rts. В итоге модуль может иметь несколько операторов exports. Когда модуль экспортирует пакет, он делает все открытые и защищенные типы в пакете доступными другим модулям. Кроме того, открытые и защи­ щенные члены этих типов тоже доступны. Однако если пакет внутри модуля не экспортируется, то он является закрытым для этого модуля, включая все Глава 1 6. Модули 481 его открытые типы. Скажем, даже если класс объявлен как puЫic в пакете, но пакет не экспортируется явно оператором exports, то такой класс не бу­ дет доступен остальным модулям. Важно понимать, что открытые и защи­ щенные типы пакета вне зависимости от того, экспортированы они или нет, всегда доступны внутри модуля данного пакета. Оператор exports просто делает их доступными внешним модулям. Таким образом, любой неэкспорти­ рованный пакет предназначен только для внутреннего потребления модулем. Ключевой аспект понимания операторов requires и exports в том, что они работают вместе. Если один модуль зависит от другого, то такую зависи­ мость необходимо указать с помощью requi res. Модуль, от которого зависит другой модуль, обязан явно экспортировать (т.е. делать доступными) пакеты, в которых нуждается зависимый модуль. Если любая сторона этого отно­ шения зависимости отсутствует, тогда зависимый модуль компилироваться не будет. Как и в предыдущем примере, MyModAppDemo использует функции из SimpleMathFuncs. В результате объявление модуля appstart содержит оператор requi res, в котором указан модуль appfuncs. Объявление модуля appfuncs экспортирует пакет appfuncs . s imple funcs, делая доступными от­ крытые типы в классе S imp l eMath Funcs . Поскольку обе стороны отношения зависимости удовлетворены, приложение может быть скомпилировано и за­ пущено. Если одна из них отсутствует, тогда компиляция потерпит неудачу. Важно подчеркнуть, что операторы requ i r e s и exports должны встре­ чаться только внутри оператора модуля. Кроме того, оператор модуля сам по себе должен находиться в файле по имени module- info . j ava. Модуль j ava . base и модули платформы Как упоминалось ранее в главе, начиная с версии JDK 9 , пакеты Java API были встроены в модули. На самом деле модульность Java API является од­ ним из основных преимуществ, которые обеспечиваются появлением моду­ лей. Из-за своей особой роли модули Java API называются модулями плат­ формы, и все их имена начинаются с префикса j ava, например, j ava . base, j ava . des ktop и j ava . xml. Модульность Java API позволяет развертывать приложение только с теми пакетами, которые ему необходимы, а не со всей средой JRE. Из-за размера полной версии JRE это очень важное улучшение. Тот факт, что все пакеты библиотеки Java API теперь находятся в моду­ лях, вызывает следующий вопрос: как метод ma i n ( ) в MyModApp Demo из предыдущего примера может вызывать System . out . println ( ) без опера­ тора requi res для модуля, который содержит класс System? Очевидно, что программа не ском пилируется и не запустится, если отсутствует S ys tem. Аналогичный вопрос относится и к работе с классом Math в S impleMathFuncs. Ответ на вопрос кроется в j ava . base. Наиболее важным из модулей платформы является j ava . base. Он вклю­ чает и экспортирует помимо множества других такие фундаментальные для Java пакеты, как j ava . lang, j ava . io и j ava . u t i l. По причине своей важ­ ности пакет j ava . base автоматически доступен всем модулям. Вдобавок 482 Часть 1. Язык Java все остальные модули автоматически требуют j ava. base. Нет никакой не­ обходимости помещать оператор requires j ava . base в объявление моду­ ля. (Интересно отметить, что в явном указании j ava . base нет ничего пло­ хого, просто это необязательно.) Таким образом, почти так же, как пакет j ava . lang автоматически доступен во всех программах без применения опе­ ратора import, модуль j ava . base будет автоматически доступным для всех программ на основе модулей без явного запроса. Поскольку j ava. base включает пакет j ava. lang, а j ava. lang содержит класс System, класс MyModAppDemo в предыдущем примере может автомати­ чески использовать System . out. println () без явного оператора requires. То же самое относится и к классу Math в SimpleMath Funcs, потому что Math тоже находится в j ava . lang. Приступив к созданию собственных модульных приложений, вы заметите, что многие классы Java API, которые обычно нуж­ ны, расположены в пакетах, входящих в состав j а va . base. Таким образом, автоматическое включение j ava . base упрощает создание кода на основе мо­ дулей, т.к. основные пакеты Java доступны автоматически. И последнее: начиная с JDK 9, в документации по Java API теперь указыва­ ется имя модуля, где содержится пакет. Если речь идет о модуле j ava. base, тогда вы можете напрямую потреблять содержимое этого пакета. В против­ ном случае объявление модуля должно включать оператор requires для же­ лаемого модуля. Унаследованный код и неименованные модули При работе с первым примером модульной программы у вас мог возник­ нуть еще один вопрос. Поскольку в Java теперь померживаются модули, а па­ кеты Java API также содержатся в модулях, почему все остальные программы из предыдущих глав компилируются и выполняются без ошибок, даже если модули в них не применяются? Выразимся в более общем плане: учитывая то, что код на Java существует уже свыше 20 лет и (на момент написания книги) в подавляющем большинстве кода модули не используются, то как удается компилировать, запускать и поддерживать устаревший код с помощью ком­ пилятора JDK 9 или его более поздней версии? С учетом изначальной фи­ лософии Java, т.е. "написанное однажды выполняется везде, в любое время, всегда'; вопрос крайне важен, т.к. необходимо поддерживать обратную совме­ стимость. Вы увидите, что язык Java отвечает на данный вопрос, предлагая элегантные и почти прозрачные средства обеспечения обратной совместимо­ сти с написанным ранее кодом. Для поддержки унаследованного кода предназначены два ключевых сред­ ства. Одно из них - неименованный модуль. Когда вы используете код, не являющийся частью именованного модуля, он автоматически становится частью неименованного модуля, который обладает двумя важными характе­ ристиками. Во-первых, все пакеты в неименованном модуле автоматически экспортируются. Во-вторых, неименованный модуль имеет доступ абсолютно ко всем остальным модулям. Таким образом, когда модули в программе не Глава 16. Модули 483 применяются, все модули Java API платформы Java автоматически доступны через неименованный модуль. Вторым ключевым средством, померживающим унаследованный код, следует считать автоматическое использование пути к классу, а не пути к модулю. При компиляции программы, в которой модули не задействованы, применяется механизм пути к классам, как было со времен первоначального выпуска Java. В результате программа компилируется и запускается в той же манере, как было до появления модулей. Из-за неименованноrо модуля и автоматического использования пути к классам в показанных ранее примерах программ не было необходимости объявлять какие-либо модули. Примеры корректно запускаются вне зависи­ мости от того, компилируется они современным или же компилятором пред­ шествующей версии наподобие JDK 8. Таким образом, несмотря на то, что модули являются средством, которое оказывает значительное влияние на Java, совместимость с устаревшим кодом сохраняется. Данный подход так­ же обеспечивает плавный, ненавязчивый и неразрушающий путь перехода к модулям. В итоге он позволяет переносить унаследованное приложение в модули в приемлемом темпе. Кроме того, появляется возможность избежать применения модулей, когда они не нужны. Прежде чем двигаться дальше, необходимо сделать одно важное замечание. Для примеров программ, используемых в других местах книги, и для приме­ ров программ в целом использование модулей не приносит какой-либо пользы. Модульность в таких примерах просто добавила бы беспорядка и усложнила бы их без особой на то причины или выгоды. Вдобавок многие простые программы нет нужды помещать в модули. По причинам, изложенным в начале главы, моду­ ли часто приносят наибольшую пользу при создании коммерческих программ и потому ни в каких примерах за рамками текущей главы модули не применяются. Это также позволяет компилировать примеры и запускать их в среде, предше­ ствующей JDK 9, что важно для читателей, использующих более раннюю версию Java. Таким образом, примеры программ, рассмотренные в книге, за исключе­ нием тех, что приводятся в настоящей главе, работают для JDK, вышедших до и после появления модулей. Экспорт и р о в ан и е в конкр ет ны й м одул ь Базовая форма оператора exports открывает доступ к пакету любым дру­ гим модулям. Часто именно это и нужно. Тем не менее, в ряде специализиро­ ванных ситуаций при разработке может возникнуть необходимость сделать пакет доступным только конкретному набору модулей, а не всем другим мо­ дулям. Скажем, разработчик библиотеки может решить экспортировать па­ кет поддержки в несколько других модулей внутри библиотеки, но не делать его доступным для общего потребления. Достичь желаемого можно, добавив конструкцию to к оператору exports. В конструкции to оператора expo rts указывается список из одного или нескольких модулей, которые имеют доступ к экспортируемому пакету. Более 484 Часть 1. Язык Java того, доступ будет предоставлен только тем модулям, имена которых пере­ числены в конструкции to. На языке модулей конструкция to создает так на­ зываемый уточненный экспорт. Вот форма оператора exports с конструкцией to: exports packageName to moduleNames ; Здесь packageName представляет собой разделяемый запятыми список мо­ дулей, которым выдается доступ к экспортирующему модулю. Можете опробовать конструкцию to, изменив содержимое файла module-info . j ava для модуля appfuncs, как показано ниже: // Определение модуля , в котором используется конструкция to . module appfuncs { // Экспортировать пакет appfuncs . simplefuncs в appstart . exports appfuncs . simplefuncs to appstart; Теперь simplefuncs экспортируется только в appstart и ни в какие дру­ гие модули. После внесения такого изменения перекомпилируйте приложе­ ние с помощью следующей команды j avac: j avac -d appmodules --module-source-path appsrc appsrc\appstart\appstart\mymodappdemo\MyModAppDemo . j ava После компиляции запустите приложение, как объяснялось ранее. В данном примере также задействовано еще одно средство, связанное с модулями. Взгляните внимательно на предыдущую команду j avac. Первым делом обратите внимание на наличие параметра --module-source-path. Исходный путь к модулю задает вершину дерева каталогов с исходным ко­ дом модуля. Параметр --module-source -path обеспечивает автомати­ ческую компиляцию файлов в дереве ниже указанного каталога, которым является apps rc. Параметр --module-source-path должен применяться вместе с параметром -d, чтобы гарантировать сохранение скомпилирован­ ных модулей в надлежащих каталогах внутри appmodules. В случае ввода по­ казанной выше формы команды компилятор j avac будет работать в много­ модульном режиме, позволяя одновременно компилировать более одного модуля. Мноrомодульный режим компиляции здесь особенно полезен, т.к. конструкция to относится к конкретному модулю, а затребованный. модуль должен иметь доступ к экспортируемому пакету. Таким образом, во избежа­ ние выдачи сообщений с предупреждениями и/или ошибками на этапе ком­ пиляции в рассматриваемом случае необходимы и appstart, и appfuncs. Мноrомодульный режим позволяет избежать этой проблемы, потому что оба модуля компилируются одновременно. Мноrомодульный режим j avac обладает еще одним преимуществом. Он автоматически отыскивает и компилирует все исходные файлы для приложе­ ния, создавая необходимые выходные каталоги. Из-за преимуществ, которые предлагает мноrомодульный режим компиляции, он будет задействован и в последующих примерах. Глава 16. Модули 485 На заметку! Как правило, уточненный экспорт является особым случаем. Чаще всего ваши модули будут либо предоставлять неуточненный экспорт пакета, либо не экспортировать пакет вообще, оставляя его недоступным. Таким образом, уточненный экспорт обсуждается главным образом ради полноты картины. Кроме того, уточненный экспорт сам по себе не предотвращает неправомерное использование экспортированного пакета вредоносным кодом в модуле, который маскируется под целевой модуль. Обсуждение приемов безопасности, необходимых для предотвращения этого, выходят за рамки материала книги. Подробную информацию о безопасности в данном отношении и общие сведения о безопасности Java ищите в документации Oracle. Использова ние requires transi ti ve Рассмотрим ситуацию, когда есть три модуля, А, В и С, со следующими зависимостями: А требует В; В требует С. В такой ситуации совершенно ясно, что поскольку А зависит от В, а В за­ висит от С, модуль А имеет косвенную зависимость от С. Пока в модуле А не применяется напрямую какое-то содержимое модуля С, можно в файле modu le-info. java для А просто затребовать В и в файле modu le-info . j ava для В экспортировать пакеты, требующиеся в 'А:. // Файл module-info , j ava для А. module А { requires В ; // Файл module-info. j ava для В. module В { exports pac kageName ; requires С; Здесь packageName представляет собой заполнитель для пакета, экспорти­ руемого модулем В и используемого в модуле А. Хотя такой подход работает при условии, что в А не нужно задействовать какой-нибудь тип, определен­ ный в С, если в А понадобится получить доступ к типу из модуля С, тогда воз­ никнет проблема. Решить ее можно двумя способами. Первое решение предусматривает добавление в файл module-in f o. j ava для А оператора requ ires С: //Файл module-info . j ava для А, модифицированный с целью явного затребования С module А { requires В; requires С; // также затребовать С Безусловно, решение работоспособно, но если модуль В будет потреблять­ ся многими модулями, то оператор requ ire С придется добавить ко всем определениям модулей, которым требуется В, что в равной степени и утоми­ тельно, и чревато ошибками. К счастью, существует более эффективное ре- 486 Часть 1. Язык Java шение - создать подразумева емую зависимость от С, которая также называет­ ся подразумева емой ч итаемостью. Чтобы создать подразумеваемую за висимость, добавьте ключевое слово transitive после конструкции requi res, требующей модуль, для которого необходима подразу меваемая читаемость. В данном примере нужно изме­ нить файл module-info . j ava для В: // Файл module- info . j ava для В . module В { exports pac kageName ; requires transitive С; Теперь модуль С затребован как транзитивный. После внесения такого из­ менения любой модуль, который зависит от В, будет также автоматически за­ висеть от С. В итоге модуль А получит доступ к с. Можете поэкспериментировать с оператором requi res transiti ve, пе­ ределав предыдущий пример модульного приложения, чтобы удалить метод is Factor ( ) из класса SimpleMath Funcs в пакете appfuncs . simplefuncs и поместить его в новый класс, модуль и пакет. Новый класс получит имя Support Funcs, модуль - имя appsuppo rt, а пакет - имя appsupport . suppo r t f uncs. Зате м модуль app funcs добавит зависимость от моду­ ля appsuppo rt с помощью requi res trans iti ve, что позволит модулям appfuncs и appstart получить к не му доступ без необходимости иметь в appstart собственный оператор requires. Прием работает, поскольку appstart получает к нему доступ через оператор requ i res transiti ve в appfuncs. Ниже весь процесс описан более подробно. Первым делом понадобится создать каталоги для исходного кода, которые поддерживают новый модуль appsupport. Сначала создайте внутри катало­ га appsrc каталог модуля appsupport, предназначенный для вспомогатель­ ных функций. Внутри appsupport создайте каталог пакета, добавив подка­ талог appsupport, а затем подкаталог supportfuncs. Дерево каталогов для appsupport должно выглядеть так: appsrc\appsupport\appsupport\support funcs После каталогов создайте класс Support Funcs. Обратите внимание, что Support Funcs входит в состав пакета appsuppo rt . supportfuncs, поэтому вы должны поместить его в каталог пакета appsupport . supportfuncs. // Всnомогате�ьные функции . pac kage appsuppo rt.support funcs; puЫi c class Support Funcs { / / Выяснить , являетс я ли а делителем Ь. puЫic static boolean isFactor (int а , i nt Ь) { i f ( ( Ь % а ) == О ) return true ; return false; Глава 16. Модули 487 Обратите внимание, что метод isFactor () теперь является частью класса SupportFuncs, а не SimpleMathFuncs. Далее создайте в каталоге appsrc\appsupport файл modul e - info . j ava для модуля appsupport: // Определение модуля для appsupport . module appsupport { exports appsupport.supportfuncs ; Как видите, он экспортирует пакет appsupport . supportfuncs. Поскольку метод isFactor ( ) определен в классе Supportfuncs, удали­ те его из Simpl eMathFuncs. Таким образом, содержимое SimpleMathFuncs . j ava приобретает следующий вид: // Простые математические функции ; метод isFactor() удален. pac kage appfuncs. simplefuncs ; import appsupport.support funcs. SupportFuncs ; puЫic class SimpleMathFuncs { // Возвратить наименьший положительный делитель, общий для а и Ь. puЫ ic static int lcf(int а, int Ь) { // Разложить на множители, используя положительные значения. а = Math. abs(a); Ь = Math.abs(b); int min = а < Ь ? а : Ь; for(int i = 2; i <= min/2 ; i++) { i f(SupportFuncs.isFactor(i, а) & & SupportFuncs.isFactor(i, Ь)) return i ; return 1 ; // Возвратить наибольший положительный делитель, общий для а и Ь. puЫic static int gcf (int а, int Ь) { // Разложить на множители, используя положительные значения. а = Math.abs(a); Ь = Math.abs(Ь) ; i nt min = а < Ь ? а : Ь ; for(int i = min/2 ; i > = 2 ; i--) { if(Support Funcs.isFactor(i, а) & & SupportFuncs.isFactor(i, Ь)) return i ; return 1 ; Обратите внимание, что теперь класс SupportFuncs импортирован, а вы­ зовы isFactor ( ) ссылаются на имя класса SupportFuncs. Модифицируйте файл module-info . j ava для appfuncs, чтобы в его опе­ раторе requires модуль appsupport был помечен как transi ti ve: 488 Часть 1. Язык Java // Определение модуля для appfuncs. module appfuncs { // Экспортировать пакет appfuncs.simplefuncs. exports appfuncs.simplefuncs; // Затребовать модуль appsupport и сделать его транзитивным. requires transitive appsupport; Поскольку в app funcs модуль appsupport затребован как транзитив­ ный, нет никакой необходимости в наличии файла module- info . j ava для appstart. Подразумевается его зависимость от appsupport. Таким образом, изменять содержимое файла module- info . j ava для запуска приложения не нужно. В заключение модифицируйте содержимое файла MyModApp Demo . j ava, чтобы отразить эти изменения. В частности, теперь в нем должен импорти­ роваться класс SupportFuncs и указываться при вызове метода isFactor ( ) : // Обновление с целью использования класса SupportFuncs. package appstart.mymodappdemo; import app funcs.simplefuncs . SimpleMathFuncs; import appsupport.supportfuncs.SupportFuncs; puЫic class MyModAppDemo { puЬlic static void main(String[] args) { // Теперь ссылка на метод isFactor() производится // через SupportFuncs , а не SimpleMathFuncs. if(SupportFuncs.isFactor(2 , 10)) System.out.println("2 является делите лем 10 "); System.out.println(" Haимeньший общий делитель для 35 и 10 5 равен " + SimpleMathFuncs.lcf(35, 10 5)); System.out . println(" Haибoльший общий делитель для 35 и 10 5 равен " + SimpleMathFuncs.gcf(35, 10 5)) ; После выполнения всех предыдущих шагов заново скомпилируйте про­ грамму с применением команды j avac в многомодульном режиме: j avac -d appmodules --module-source-path appsrc appsrc\appstart\appstart\mymodappdemo\MyModAppDemo.j ava Как объяснялось ранее, при многомодульном режиме компиляции внутри каталога appmodu les автоматически создаются подкаталоги для модулей. Запустить программу можно с помощью следующей команды: j ava --module-path appmodules -m appstart/appstart.mymodappdemo.MyModAppDemo Программа произведет тот же вывод, что и предыдущая версия, но на этот раз будут затребованы три разных модуля. Чтобы удостовериться в том, что модификатор transitive действитель­ но необходим приложению, удалите его из файла module-info . j ava для app funcs и попробуйте скомпилировать программу. Возникнет ошибка, по­ тому что модуль appsupport больше не будет доступным модулю appstart. Глава 16. Модули 489 Проведите еще один эксперимент. В файле modu l e - in f o . j a va для appsupport попробуйте экспортировать пакет appsupport . supportfuncs только в appfuncs, используя уточненный экспорт: exports appsupport.support funcs to app funcs ; Попытайтесь скомпилировать программу. Как видите, программа не ком­ пилируется, поскольку теперь вспомогательный метод i sFactor ( ) оказыва­ ется недоступным классу MyModAppDemo, находящемуся в модуле appstart. Согласно приведенным ранее объяснениям уточненный экспорт ограничивает доступ к пакету только теми модулями, которые указаны в конструкции to. И последнее замечание, которое касается особого исключения в син­ таксисе языка Java, связанного с оператором requires: если сразу после transitive следует разделитель (например, точка с запятой), то transitive интерпретируется как идентификатор (скажем, имя модуля), а не как ключе­ вое слово. Использование служб В программировании часто бывает полезно отделять то, что должно быть сделано, от того, как оно делается. В главе 9 вы узнали, что один из способов достижения такой цели в языке Java предусматривает применение интерфей­ сов. Интерфейс определяет что, а реализующий его класс - как. Концепцию можно расширить так, чтобы реализующий класс предоставлялся кодом, кото­ рый находится за пределами программы, за счет использования подключаемого модуля. При таком подходе возможности приложения можно совершенство­ вать, модернизировать либо изменять, просто меняя подключаемый модуль. Ядро приложения остается незатронутым. Архитектура приложений с подклю­ чаемыми модулями померживается в Java через применение служб и постав­ щиков служб. По причинам их крайней важности, особенно для крупных ком­ мерческих приложений, их поддержка обеспечивается системой модулей Java. Прежде чем начать, необходимо отметить, что приложения, в кото­ рых используются службы и поставщики служб, обычно довольно сложны. Следовательно, вы можете обнаруживать, что средства модулей на основе служб требуются нечасто. Однако поскольку поддержка служб составляет довольно значительную часть системы модулей, важно иметь общее пред­ ставление о том, как работают эти средства. Кроме того, далее предложен простой пример, который иллюстрирует основные приемы их применения. О сновы служб и поставщиков служб Служба в Java представляет собой программную единицу, функциональ­ ность которой определяется интерфейсом или абстрактным классом. Таким образом, служба задает в общем виде некоторую форму программной дея­ тельности. Конкретная реализация службы предоставляется поставщиком елужб. Другими словами, ел ужба определяет форму какого-то действия, а по­ ставщик служб предоставляет это действие. 490 Часть 1. Язык Java Как уже упоминалось, елужбы часто используются для помержки подклю­ чаемой архитектуры. Например, служба может применяться для поддержки перевода с одного языка на другой. В таком случае служба поддерживает пе­ ревод в целом. Поставщик служб предоставляет конкретный перевод, скажем, с немецкого языка на английский или с французского языка на китайский. Из-за того, что все поставщики служб реализуют один и тот же интерфейс, появляется возможность использовать разные переводчики для перевода на разные языки без необходимости внесения изменений в ядро приложения. Можно просто сменить поставщика служб. Поставщики служб померживаются обобщенным классом ServiceLoader, который доступен в пакете j ava . ut i 1 и объявлен следующим образом: class ServiceLoader<S> В S задается тип службы. Поставщики служб загружаются с помощью ме­ тода load ( ) . Он имеет несколько форм; ниже показана форма, которая будет применяться в главе: puЫic static <S> ServiceLoader<S> load ( Class <S> serviceType) В serviceType указывается объект Class для желаемого типа службы. Вспомните, что в объекте C lass инкапсулирована информация о классе. Получить экземпляр Class можно многими способами, один из которых свя­ зан с использованием литерала класса. В качестве напоминания ниже показа­ на общая форма литерала класса: имя -класса. с l аss В имя-класса задается имя нужного класса. В результате вызова метода l oad ( ) возвращается экземпляр ServiceLoader для приложения, который поддерживает итерацию и может применяться в цикле for стиля " for-each': Таким образом, чтобы отыскать конкретного про­ вайдера, просто ищите его с помощью цикла. Кл ю ч евые слова, связа нные со сл ужб ами Модули поддерживают службы через использование ключевых слов provi des, uses и wi th. Модуль указывает, что он предоставляет службу, с помощью оператора provides. Модуль отражает тот факт, что ему требуется служба, посредством оператора uses. Специфический тип поставщика служб объявляется с применением wi th. Когда они используются все вместе, то позволяют задать модуль, предоставляющий службу, модуль, который нуж­ дается в этой службе, и конкретную реализацию самой службы. Кроме того, система модулей гарантирует, что службы и поставщики служб доступны и будут найдены. Вот общая форма оператора provides: provi des serviceType with implementat ionTypes ; В serviceType задается тип службы, которая часто является интерфейсом, хотя также применяются и абстрактные классы. Список типов реализации, Глава 1 6. Модули 491 разделенных запятыми, помещается в implementati onTypes. Следовательно, для предоставления службы в модуле указывается как имя службы, так и ее реализация. Ниже приведена общая форма оператора uses: uses serviceType ; В serviceType указывается тип требующейся службы. П ример службы , основанной на модул я х Чтобы продемонстрировать работу со службами, мы добавим службу в разрабатываемый пример модульного приложения. Для простоты начнем с первой версии приложения, показанной в начале главы, и добавим в нее два новых модуля. Первый называется userfuncs. В нем будут определены ин­ терфейсы с поддержкой функций, выполняющих бинарные операции, в кото­ рых каждый аргумент и результат имеют тип i n t. Второй модуль называется userfuncsimp и содержит конкретные реализации интерфейсов. Первым делом создайте необходимые каталоги для исходного кода. 1. Добавьте в каталог apps rc подкаталоги с именами use r f u n cs и userfuncsimp. 2. Добавьте в каталог use r fu n cs подкаталог с тем же самым именем userfu ncs, а в него - подкаталог Ьinaryfuncs. Таким образом, вы по­ лучите следующее дерево каталогов, начинающееся с appsrc: appsrc\userfuncs \userfuncs\binaryfuncs 3. Добавьте в каталог userfuncsimp подкаталог с тем же самым именем userfu ncsimp, а в него - подкаталог Ьinaryfu ncsimp. В итоге вы по­ лучите следующее дерево каталогов, начинающееся с appsrc: apps rc\userfuncsimp\userfuncs irnp \binaryfuncsirnp Текущий пример расширяет первоначальную версию приложения, обеспе­ чивая поддержку других функций вдобавок к тем, которые встроены в при­ ложение. Вспомните, что класс S impleMathFuncs предоставляет три встро­ енные функции: isFact or ( ), lcf () и gcf (). Хотя в него можно было бы добавить дополнительные функции, для этого пришлось бы модифицировать и заново компилировать приложение. Благодаря внедрению служб появляет­ ся возможность "подключать" новые функции во время выполнения, не изме­ няя приложение, что и будет делаться в рассматриваемом примере. В данном случае служба предоставляет функции, которые принимают два аргумента типа i n t и возвращают результат типа int. Конечно, можно под­ держивать и другие типы функций, если определить дополнительные интер­ фейсы, но помержки бинарных целочисленных функций вполне достаточно для преследуемых целей, кроме того, размер исходного кода примера остает­ ся управляемым. Часть 1. Язык Java 492 Интерфе й сы служб Понадобятся два интерфейса, связанных со службами. Один зада­ ет форму действия, а другой указывает форму поставщика этого действия. Определения обоих интерфейсов находятся в каталоге binaryfuncs и оба они входят в состав пакета userfuncs . binaryfuncs. Первый интерфейс по имени BinaryFunc объявляет форму бинарной функции: // // // // В этом интерфейсе определен метод, который принимает два аргумента типа i пt и возвращает резул ь тат типа iпt. Таким образом, он может описывать л ю бу ю бинарную операцию с двумя целочисленными значениями, которая возвращает целое число. pac kage userfuпcs.b i пaryfuпcs; puЫic i пterface BiпaryFuпc // Получит ь имя функции. puЬlic Striпg getName(); / / Э':'о функция, подлежащая выполнению. Она будет / / предоставлят ь ся конкретными реализациями. puЫic i пt fuпc( i пt а, i пt Ь); Интерфейс BinaryFunc объявляет форму объекта, который может ре­ ализовывать бинарную целочисленную функцию, что указывается мето­ дом func (). С помощью метода getName () можно получить имя функции, которое будет использоваться для выяснения типа реализуемой функции. Интерфейс BinaryFunc реализуется классом, предоставляющим бинарную функцию. Второй интерфейс объявляет форму поставщика служб и называется BinFuncProv ider: / / Этот интерфейс определяет форму поставщика служб, / / который получает экземпляры B i пaryFuпc. pac�age userfuпcs . b i пaryfuпcs; import userfuпcs.biпaryfuпcs.Bi пaryFuпc; puЬlic i пterface Biп FuпcProvider { / / Получить экземпляр Bi пaryFuпc. puЬlic BinaryFuпc get( ) ; В интерфейсе BinFuncProvider объявлен только один метод get (), ко­ торый применяется для получения экземпляра BinaryFunc. Этот интерфейс должен быть реализован классом, желающим предоставлять экземпляры BinaryFunc. Классы реализации В рассматриваемом примере поддерживаются две конкретные реализации BinaryFunc. Первая реализация, AЬs Plus, возвращает сумму абсолютных зна­ чений своих аргументов. Вторая реализация, AЬsMinus, возвращает результат Глава 16. Модули 493 вычитания абсолютного значения второго аргумента из абсолютного значе­ ния первого аргумента. Они предоставляются классами AЬsPlusProvider и AbsMinusProvider. Исходный код этих классов должен храниться в катало­ ге Ьinaryfuncsimp и принадлежать пакету userfuncsimp . Ьinaryfuncsimp. Вот код для AЬsPlus: // AЬsPlus предоставляет конкретную реализацию BinaryFunc, // которая возвращает результат abs(a) + abs(b). pac kage user funcsimp . binaryfuncsirnp ; irnport userfuncs.binaryfuncs.BinaryFunc; puЫic c lass AЬs Plus irnplernents BinaryFunc // Возвратить имя этой функции. puЫic String getNarne() { return "abs Plus " ; ) // Реализовать функцию AЬs Plus . puЫic int func(int а, int Ь) { return Math . abs(a) + Math . abs(b) ; } Класс AЬsPlus реализует метод func ( } таким образом, что он возвраща­ ет результат сложения абсолютных значений а и Ь. Обратите внимание, что getName () возвращает строку "absPlus", идентифицирующую эту функцию. Ниже приведен код AbsMinus: // AЬsMinus предоставляет конкретную реализацию BinaryFunc, // которая возвращает результат abs(a) - abs(b ) . package userfuncsirnp.binaryfuncsirnp ; irnport userfuncs . binaryfuncs . BinaryFunc ; puЫic class AЬsMinus irnplernents BinaryFunc // Возвратить имя этой функции . puЫic String getNarne() { return "absMinus " ; ) // Реализовать функцию AЬsMinus. puЫic int func(int а, int Ь) { return Math . abs(a ) - Math.abs(b) ; } Метод func ( ) реализован для возвращения разности абсолютных значе­ ний а и Ь, а метод getName ( } возвращает строку "absMinus". Для получения экземпляра AЬsPlus используется класс AЬsPlusProvider. Он реализует интерфейс BinFuncProvider и выглядит следующим образом: // Поставщик для функции AЬs Plus . package user funcsimp . binaryfuncs irnp ; import userfuncs.binaryfuncs. * ; puЫic class AЬsPlus Provider irnplernents BinFuncProvi der // Предоставить объект AЬs Plus. puЫic BinaryFunc get () { return new AЬs P l us ( ) ; } 494 Часть 1. Язык Java Метод get () просто возвращает новый объект AЬsPlus. Хотя данный по­ ставщик очень прост, важно понимать, что некоторые поставщики служб бу­ дут намного сложнее. Поставщик для AЬsMinus называется AЬsMinusProvider и показан ниже: // Поставщик для функции AЬsMinus. package userfuncsimp.binaryfuncsimp ; import userfuncs . binaryfuncs . * ; puЫic cl ass AЬsMinusProvider implements BinFuncProvider // Предоставить объект AЬsMinus. puЫic BinaryFunc get() { return new AЬsMinus(); ) Метод get () поставщика возвращает объект AЬsMinus. Ф а йлы определения модулей Далее необходимы два файла определения модулей. Первый предназначен для модуля userfuncs и имеет такое содержимое: module userfuncs { exports userfuncs . binaryfuncs; Представленный выше код должен быть помещен в файл modu le-in fo . j ava, находящийся в каталоге модуля userfuncs. Обратите внимание, что в нем экспортируется пакет u serfuncs . Ыnaryfuncs, где определены интер­ фейсы BinaryFunc и BinFuncProvider. Вот как выглядит содержимое второго файла modu le-info . j ava с опреде­ лением модуля, содержащего реализации. Он должен быть помещен в каталог модуля u serfuncsimp. module userfuncsimp { requires userfuncs; ) provides userfuncs . binaryfuncs.BinFuncProvider with userfuncsimp.binaryfuncsimp.AЬsPlusProvider, userfuncsimp.binary funcsimp . AЬsMinusProvider ; В модуле u ser funcsimp требуется модуль u ser funcs, поскольку по­ следний содержит интерфейсы BinaryFu nc и BinFuncPro v i der, которые нужны реализациям. Модуль userfu ncs imp предоставляет реализации BinFuncProvider в виде классов AЬsPlusProvider и AЬsMinusProvider. Д е монстрация поста в щ и ко в служб в классе MyModAppDemo Чтобы продемонстрировать работу со службами, метод main ( ) класса MyModAppDemo расширен с целью применения AЬs Plus и AbsMinus, для чего загружает их во время выполнения с помощью ServiceLoader . load ( ) . Вот модифицированный код: Глава 16. Модули 49S // Модульное приложение , демонстрирующее использ ование служб // и поставщиков служб. package appstart.mymodappdemo ; import j ava.util.ServiceLoader; import appfuncs.simplefuncs.SimpleMathFuncs; import userfuncs.binaryfuncs. * ; puЬl ic class MyModAppDemo { puЬlic static void main (String [ ] args ) { // Первым делом использовать встроенные функции , ка к и ранее. if(SimpleMathFuncs.isFactor(2 , 10 ) ) System.out.printl n("2 является делителем 10" ) ; System.out.println("Haимeньший общий делитель для 35 и 105 равен " + Simpl eMathFuncs.lcf(35, 105) ) ; System.out.println (" Наибольший общий делитель для 35 и 105 равен " + SimpleMathFuncs.gcf(35, 105) ) ; // Теперь исполь зовать поль зовательские операции, основанные на службах // Получить загрузчик служб для бинарных функций. ServiceLoader<BinFuncProvider> ldr = ServiceLoader.load(BinFuncProvider.class ) ; BinaryFunc binOp = null; // Найти поставщика для absPlus и получить функцию. for (BinFuncProvider Ьfр : ldr ) { if(Ьfp.get( ) .getName( ) .equals("absPlus" ) ) binOp = Ьfp. get( ) ; break; if(binOp ! = пull) System.out.printl n("Peзyльтaт выполнения функции absPlus : " + binOp.func(l2, -4 ) ) ; else System.out.println("Фyнкция absPlus не найдена." ) ; binOp = null; // Найти поставщика для absMinus и получить функцию. for(BinFuncProvider Ьfр : ldr ) { if(Ьfр.get () .getName () .equals ("absMinus") ) binOp = bfp.get( ) ; break ; if(binOp ! = null ) System.out.println("Peзyльтaт выполнения функции absMinus : " + binOp.func(l 2 , - 4 ) ) ; else System.out.println("Фyнкция absMinus не найдена." ) ; 496 Часть 1 . Язык Java Давайте внимательно посмотрим, каким образом служба загружается и выполняется в предыдущем коде. Сначала с помощью следующего оператора создается загрузчик служб для служб типа BinFuncProvider: ServiceLoader<BinFuncProvider> l dr = ServiceLoader .l oad ( BinFuncProvide r . c lass); Обратите внимание, что в качестве параметра типа для ServiceLoad er указывается B i n Func P ro vider. Тот же самый тип используется и при вызове l o ad ( ), т.е. поставщики, реализующие данный интерфейс, бу­ дут найдены. Таким образом, после выполнения это го оператора классы BinFunc P rovider в модуле будут дост упны через ldr. Здесь будут доступны как AЬsPlusProvider, так и AЬsMinusProvider. Затем объявляется ссылка типа Bina ryFunc по имени binOp, которая ини­ циализируется значением nul l . Она будет применяться для ссылки на реали­ зацию, предоставляющую конкретный тип бинарной функции. Далее в пред­ ставленном ниже цикле в ldr ищется функция с именем "absPlus " : / / Найти поставщика для absPl us и получ ить функцию . f or (BinFuncProvider bfp : ldr) { if ( bfp.get ( ) . getName ( ) . equals ("absP lus") ) binOp = bfp.get ( ) ; break; В цикле for стиля "for-each" осуществляется проход по ldr с провер­ кой имени функции, предоставленной поставщиком. Если имя совпадает с " abs Plus", тогда такая функция прис ваивается ЬinOp путем вызова метода get () поставщика. Наконец, если функция найдена, как будет в рассматрива­ емом примере, то она вы полняется с помощью следу ющего оператора: if (binOp ! = null ) System. out. println ( "Резуль тат выполнения функции absPlus: " + binOp.func ( l 2 , - 4 ) ); Поскольку ЬinOp ссылается на экземпляр AЬsPl us, вызов func () выпол­ няет сложение абсолютных значений. Аналогичная последовательность ис­ пользуется для поиска и выполнения функции AЬsMinus. Так как в MyMod.AppDemo теперь применяется BinFuncProvider, файл опре­ деления его модуля должен включать оператор uses, указывающий на данный факт. Вспомните, что класс MyMod.AppDemo находится в модуле appstart, а пото­ му понадобится изменить содержимое файла modu le-in fo . j a va для appstart: / / Оп ределен ие модуля для гла вного п риложения. / / Он теперь исп ользует Bin FuncProvi der. module appstart { / / Затребовать модули appfuncs и use r funcs. requires appfuncs; requires userfuncs; / / appstar t теперь исполь зует BinFuncProvi de r. uses userfuncs. binaryfuncs. BinFuncProvider; Глава 16. Модули 497 Компиляция и за пуск м одул ь но го приложения со службами Выполнив все предыдущие шаги, можете скомпилировать и запустить при­ мер с помощью следующих команд: j avac -d appmodules --module-source-path appsrc appsrc\userfuncsimp\module-info. j ava appsrc\appstart\appstart\mymodappdemo\MyModAppDemo . j ava j ava --module-path appmodu les -m appstart/appstart.mymodappdemo . MyМoc:IAppDemo Вот вывод: 2 являет ся делителем 10 Наименьший общий делитель для 35 и 10 5 равен 5 Наибольший общий делитель для 35 и 10 5 равен 7 Результат выполнения функции absPlus : 1 6 Результат выполнения функции absMinus : В В выводе видно, что бинарные функции были обнаружены и выполнены. Важно подчеркнуть, что в случае отсутствия либо оператора prov ides в мо­ дуле userfuncs imp, либо оператора uses в модуле appstart приложение ра­ ботать не будет. И последний момент. Предыдущий пример был очень простым, чтобы ясно проиллюстрировать поддержку служб со стороны модулей, но возмож­ ны и более сложные применения. Например, службу можно было бы исполь­ зовать для предоставления метода sort (), который сортирует файл, и пред­ усмотреть разнообразные алгоритмы сортировки, доступные через службу. Тогда конкретный алгоритм сортировки выбирался бы на базе желаемых ха­ рактеристик времени выполнения, природы и/или размера данных, а также наличия поддержки произвольного доступа к данным. Возможно, вы попро­ буете реализовать такую службу, экспериментируя со службами в модулях. Граф ы модуле й При работе с модулями вполне вероятно вы столкнетесь с термином гра­ фы модулей. На этапе компиляции компилятор распознает отношения зави­ симостей между модулями, создавая граф модуля, который представляет за­ висимости. Такой процесс обеспечивает распознавание всех зависимостей, в том числе косвенных. Скажем, если модуль А требует модуль В, а В - модуль С, то граф модулей будет содержать модуль С, даже когда А не использует его напрямую. Графы модулей могут быть изображены визуально в целях иллюстрации взаимосвязей между модулями. Рассмотрим простой пример. Пусть имеются шесть модулей с именами А, В, С, О, Е и F. Предположим, что модуль А требует В и С, модуль В - О и Е, а модуль С - F. Ниже такие взаимосвязи представле­ ны визуально. (Поскольку модуль j ava . base включается автоматически, на диаграмме он не показан.) 498 Часть 1 . Язык Java D Е F В графе модулей Java стрелки направлены от зависимого модуля к модулю, который затребован. Таким образом, на изображенном выше графе модулей видно, у каких модулей есть доступ к другим модулям. Откровенно говоря, визуально представить графы модулей удается только в самых маленьких приложениях по причине сложности, обычно присущей многим коммерче­ ским приложениям. Три с пе циальных характерных черты мод улей В предшествую щих обсуждениях затрагивались ключевые характеристики модулей, поддерживаемых языком Java, и именно на них вы обычно полагае­ тесь при создании собственных модуле й. Тем не менее, модули обладают еще тремя характерными чертами, которые могут оказаться крайне полезными в определенных обстоятельствах. Речь идет об открытых модулях, операто­ ре opens и операторе requ i re stat ic. Каждая характеристика рассчитана на конкретную ситуацию и представляет собой довольно развитый аспект системы модулей. Однако всем программистам на Java важно иметь общее представление об их назначении. Открытые модул и Ранее в главе вы узнали, что по умолчанию типы в пакетах модуля до­ ступны только в том случае, если они явно экспортированы через оператор exports. Хотя обычно это нежелательно, могут сложиться обстоятельства, когда полезно разрешить доступ во время выполнения ко всем пакетам в мо­ дуле вне зависимости от того, экспортируется пакет или нет. В таком случае можно создать открытый модуль, который объявляется за счет помещения перед ключевым словом module модификатора open: open rnodule имя -модуля { // определени е модуля Во время выполнения открытый модуль разрешает доступ к типам из всех его пакетов. Тем не менее, важно понимать, что на этапе компиляции доступ­ ны только те пакеты, которые экспортируются явно. Таким образом, моди­ фикатор open влияет только на доступность во время выполнения. Основной причиной существования открытых модулей является обеспечение доступа Глава 1 6. Модули 499 к пакетам в модуле через рефлексию. Как объяснялось в главе 12, рефлексия представляет собой средство, позволяющее программе анализировать код во время выполнения. Оператор opens В модуле можно открыть конкретный пакет для доступа во время выпол­ нения со стороны других модулей и механизма рефлексии, а не открывать весь модуль. Для такой цели предназначен оператор opens: opens pac kageName; На месте pac kageName указывается имя пакета, к которому должен быть открыт доступ. Допускается также добавлять конструкцию to со списком мо­ дулей, для которых открывается па кет. Важно понимать, что оператор opens не предоставляет доступ на этапе компиляции. Он применяется только для открытия доступа к пакету во вре­ мя выполнения и со стороны рефлексии. Однако вы можете экспортировать и открывать модуль. И еще один аспект: оператор opens нельзя использовать в открытом модуле. Не забывайте, что все пакеты в открытом модуле уже от­ крыты. Оператор requires s ta tic Как вам известно, оператор requires устанавливает зависимость, которая по умолчанию применяется как во время компиляции, так и во время выпол­ нения. Тем не менее, такую зависимость можно ослабить, чтобы модуль не требовался во время выполнения. Цель достигается за счет использования в операторе requires модификатора static. Скажем, следующий оператор указывает, что модуль mymod требуется на этапе компиляции, но не во время выполнения: requ i res s ta t i c mymod; В данном случае добавление s tatic делает mymod необязательным во вре­ мя выполнения, что полезно в ситуации, когда программа может задейство­ вать функциональность, если она присутствует, но не требует ее. В в еде н и е в j link и ф а йл ы м одулей JAR Как обсуждалось ранее, модули представляют собой существенное усовер­ шенствование языка Java. Система модулей также померживает расширения во время выполнения. Одной из наиболее важных является возможность создания образа времени выполнения, который специально приспособлен к вашему приложению. Для этого можно применять инструмент JDK под на­ званием j l i nk, объединяющий группу модулей в оптимизированный образ времени выполнения. Инструмент j link можно использовать для связыва­ ния модульных файлов JAR, файлов JMOD или даже модулей в их неархиви­ рованной форме с "развернутым каталогом': 500 Часть 1. Язык Java С вязы ва ние ф а йлов в разве рн у том катало ге Давайте сначала ознакомимся с применением j link для создания образа времени выполнения из неархивированных модулей, т.е. файлы содержатся внутри полностью развернутого каталога в необработанном виде. При работе в среде Windows показанная далее команда связывает модули для первого примера из этой главы. Команда должна вводиться в каталоге, расположен­ ном непосредственно над mymodapp. j link --launcher MyModApp=appstart/appstart .rnymodappderno. MyModAppDerno --rnodule-path "% JAVA_HOME% " \ jmods ;rnyrnodapp\appmodules --add-modules appstart --output rnylinkedrnodapp Проанализируем команду. Первый параметр, --launcher, сообщает ин­ струменту j link о необходимости создания команды, которая запускает приложение. В нем указываются имя приложения и путь к главному клас­ су. В данном случае главным классом является MyMod.AppDemo. В параметре --module-path задаются пути к требующимся модулям: путь к модулям платформы и путь к модулям приложения. Обратите внимание на использо­ вание переменной среды JAVA_HOME, которая представляет путь к стандарт­ ному каталогу JDK. Например, в стандартной установке Windows такой путь обычно будет выглядеть как "С : \program files"\j ava\jdk- 17\ jmods, но за счет применения JAVA_HOME путь будет короче и сможет работать независи­ мо от того, в каком каталоге был установлен комплект JDК. С помощью пцра­ метра --add-rnodules указывается добавляемый модуль (или модули). Здесь задан только модуль appstart, потому что j link автоматически распознает все зависимости и включает все обязательные модули. Наконец, в параметре --output указан выходной каталог. После выполнения предыдущей команды будет создан каталог по имени rnylinkedmodapp, содержащий образ времени выполнения. В его подкаталоге Ьin вы обнаружите файл запуска МуМоd.Арр, который можно использовать для запуска приложения. Скажем, в Windows это будет командный файл, вьшол­ няющий программу. Связы ва ние модульны х фа йлов JAR Несмотря на удобство связывания модулей из развернутого каталога, при работе с реальным кодом чаще придется иметь дело с файлами JAR. (Вспомните, что JAR означает Java ARchive (архив Java) и является файловым форматом, обычно применяемым при развертывании приложений.) В слу­ чае модульного кода будут использоваться модульные файлы JAR. Модульный файл JAR содержит файл rnodule-info . class. Начиная с версии JDK 9, ин­ струмент j ar позволяет создавать модульные файлы JAR. Например, теперь он способен распознавать путь к модулю. После создания модульных файлов JAR вы можете с помощью j link связать их в образ времени выполнения. Чтобы лучше понять процесс, давайте снова обратимся к первому примеру, Глава 16. Модули 501 рассмотренному в текущей главе. Ниже приведены команды j a r, которые создают модульные файлы JAR для программы MyModAppDemo. Каждая ко­ манда должна вводиться в каталоге, расположенном непосредственно над mymodapp. Вдобавок понадобится создать внутри mymodapp подкаталог по имени applib. j ar --create --file=mymodapp\ appli b \ appfuncs . j ar -С mymodapp\ appmodules\ appfuncs . j ar --create --file=mymodapp\applib\ appstart. j ar - -main-class=appstart . mymodappdemo.MyModAppDemo -С mymodapp\ appmodules \ appstart . Здесь параметр --create сообщает j ar о необходимости создания ново­ го файла JAR. В параметре - - f i le указывается имя файла JAR. Включаемые файлы задаются в параметре -С. Класс, содержащий метод main (), указыва­ ется в параметре --main-class. После выполнения этих команд файлы JAR для приложения будут находиться в подкаталоге applib внутри mymodapp. Имея только что созданные модульные файлы JAR, можно ввести команду, которая их свяжет: j link - -launcher MyModApp=apps tart --module-path " % JAVA_HOME % " \ j mods ; mymodapp\ applib --add-modules appstart - -output mylinkedmodapp В данном случае в параметре - -module-path указывается путь к файлам JAR, а не путь к развернутым каталогам. В остальном команда j link такая же, как и прежде. Интересно отметить, что с применением следующей команды можно за­ пускать приложение прямо из файлов JAR. Она должна вводиться в каталоге, расположенном непосредственно над mymodapp. j ava -р mymodapp\ appli b -m apps tart В параметре -р указан путь к модулю, а в -m - модуль, содержащий точку входа программы. Фа й л ы J MOD Инструмент j l ink также способен связывать файлы в более новом фор­ мате JMOD, появившемся в версии JDK 9. Файлы JMOD могут включать эле­ менты, которые неприменимы к файлу JAR, и создаются посредством ново­ го инструмента j mod. Хотя в большинстве приложений по-прежнему будут использоваться модульные файлы JAR, файлы JMOD окажутся полезными в специализированных ситуациях. Интересен и тот факт, что начиная с JDK 9, модули платформы содержатся в файлах JMOD. На заметку! Инструмент j 1 ink также может быть задействован недавно добавленным инструментом j рас ka ge, который позволяет создавать собственное устанавливаемое приложение. 502 Часть 1. Язык Java Кратко об уро вня х и автоматических модуля х При изучении модулей, вероятно, вы встретите ссылки на два дополни­ тельных средства, относящиеся к модулям: уровни и ав тома ти ческие модули. Оба средства предназначены для специализированной расширенной работы с модулями или во время проведения миграции существующих приложений. Вполне вероятно, что боль шинству программистов эти средства применять не придется, но ради полноты картины ниже даны их краткие описания. Уровень модулей связывает модули в графе модулей с загрузчиком клас­ сов. Таким образом, на разных уровнях могут использоваться разные загруз­ чики классов. Уровни упрощают создание о пределенных специализирован­ ных типов приложений. Автоматический модуль создается за счет указания немодульного фай­ ла JAR в пути к модулям, а его имя генерируется автоматически. (Также можно явно указать имя автоматического модуля в файле манифеста.) Автоматические модули позволяют нормальным модулям иметь зависимость от кода из автоматических модулей. Автоматические модули предоставляют­ ся для помощи в переносе кода, написанного до появления модулей, в полно­ стью модульный код. Следовательно, они являются главным образом сред­ ством перехода. Заключительные соображения по по воду модулей В предшествующих обсуждениях были описаны и продемонстрирова­ ны основные элементы системы модулей Java, о которых каждый програм­ мист на Java должен иметь хотя бы базовое представление. Как вы могли догадаться, система модулей предлагает дополнительные средства, которые обеспечивают детальный контроль над созданием и использованием моду­ лей. На пример, инструменты j avac и j a va поддерживают гораздо больше параметров, связанных с модулями, чем было упомянуто в главе. Поскольку модули являются важным дополнением к Java, вполне вероятно, что систе­ ма модулей со временем будет развиваться, поэтому имеет смысл следить за усовершенствованиями данного инновационного аспекта Java. ГЛ А В А i Вы ражен ия swi tch, записи и прочие недавно доба влен н ы е с редства Характерной чертой языка Java была его способность адаптироваться к растущим требованиям современной вычислительной среды. В течение nро­ шедших лет в Java появилось много новых функциональных средств, каждое из которых реагировало на изменения в вычислительной среде либо на ин­ новации в проектировании языков программирования. Такой непрерывный процесс позволил Java оставаться одним из самых важных и популярных язы­ ков программирования в мире. Как объяснялось ранее, книга была обновлена с учетом JDK 17 - версии Java с долгосрочной помержкой (LTS). В состав JDK 17 входит несколько новых языковых средств, которые были добавлены в Java с момента выхода предыдущей версии LTS, т.е. JDK 11. Ряд небольших добавлений были описаны в предшествующих rлавах. В настоящей rлаве рас­ сматриваются избранные основные дополнения, в том числе: • расширения оператора swi tch; • текстовые блоки; • записи; • шаблоны в instanceof; • запечатанные классы и интерфейсы. Далее приводятся краткие описания каждого дополнения. Оператор swi tch был расширен несколькими способами, наиболее важным из которых является выражение swi tch. Выражение swi tch наделяет swi tch способно­ стью выпускать значение. Текстовые блоки позволяют строковому литералу занимать более одной строчки. Записи, померживаемые новым ключевым словом record, дают возможность создавать класс, специально предназна­ ченный для хранения группы значений. Кроме того, была добавлена вторая форма операции instanceof, использующая шаблон типа. С ее помощью можно указывать переменную, которая получает экземпляр проверяемого типа, если instanceof завершается успешно. Теперь можно определять за­ печатанный класс или интерфейс. Запечатанный класс может наследоваться только явно заданными подклассами. Запечатанный интерфейс может быть реализован только явно указанными классами или расширен только явно за­ данными интерфейсами. Таким образом, запечатывание класса или интер­ фейса обеспечивает точный контроль над его наследованием и реализацией. 504 Часть 1. Язык Java Расш ирения оператора swi tch Оператор switch с самого начала входил в состав языка Java. Он представ­ ляет собой важнейший элемент управляющих операторов программы на Java и обеспечивает множественное ветвление. Более того, оператор switch на­ столько существенен в программировании, что ту или иную его форму можно обнаружить в других популярных языках программирования. Традиционная форма switch, которая всегда была частью Java, рассматривалась в главе 5. Начиная с версии JDK 14, оператор switch был существенно расширен за счет добавления четырех новых функциональных возможностей: • выражение switch; • оператор yield; • поддержка списка констант case; • оператор case со стрелкой. Все новые функциональные возможности будут подробно обсуждаться да­ лее в главе, но вот их краткое описание. Выражение switch - это по существу оператор swi tch, который производит значение. Таким образом, выражение swi tch можно применять, например, в правой части операции присваивания. Значение, созданное выражением switch, указывается с помощью оператора yield. Теперь можно иметь более одной константы в операторе case за счет использования списка констант. Добавлена вторая форма оператора case, в которой вместо двоеточия применяется стрелка (->), предоставляющая ему новые возможности. В совокупности расширения оператора switch являются довольно значи­ тельными изменениями языка Java. Они не только предоставляют новые воз­ можности, но в некоторых ситуациях также предлагают превосходные аль­ тернативы традиционным подходам. Из-за этого важно четко понимать, что, как и почему стоит за новыми функциями swi tch. Один из лучших способов понять расширения оператора switch - начать с примера, в котором используется традиционный оператор switch, а затем постепенно добавлять каждое новое средство, что позволит сделать очевид­ ными применение и пользу от расширений. Для начала вообразите себе, что какое-то устройство генерирует целочисленные коды, обозначающие различ­ ные события, и с каждым кодом события вы хотите ассоциировать уровень приоритета. Большинство событий будут иметь обычный приоритет, но неко­ торые получат более высокий приоритет. Вот программа, в которой исполь­ зуется традиционный оператор switch для предоставления уровня приори­ тета с учетом кода события: // Исполь зование традиционного оператора switch для установки // уровня приоритета на основе кода события. class TraditionalSwitch { puЫic static void rnain( String[] args) { int priorityLevel; int eventCode = 6010; Глава 1 7. Выражения switch, записи и прочие недавно добавленные средства 505 // Традиционный оператор switch , который предоставляет // значение , ассоциированное с case . switch(eventCode) { case 1000 : // в традиционном операторе swi tch используется // укладка операторов case case 120 5 : case 8 900 : priori tyLevel 1; break; case 2000 : case 6010 : case 912 8 : 2; priori tyLeve l break; case 1002 : case 702 3 : case 9 300 : priorityLevel = 3; break; default : // нормальный приоритет priorityLevel = О; System . out. println( "Уровень приоритета для кода события " + eventCode + " равен " + priori tyLevel ) ; Вот вывод, генерируемый программой: Уровень приоритета для кода события 6010 равен 2 Разумеется, с применением традиционного оператора sw i tch, как было показано в программе, не связано ничего плохого и именно так код Java пи­ шется уже более двух десятилетий. Однако в последующих разделах вы уви­ дите, что во многих случаях традиционный оператор swi tch можно улучшить за счет использования его расширенных возможностей. Ис п ол ь зо в а ние с п иска констант case Мы начнем с одного из простейших способов модернизации традицион­ ного оператора swi tch: применения списка констант case. В прошлом, когда две константы обрабатывались одной и той же кодовой последовательно­ стью, использовалась укладка операторов case, и именно такой прием при­ менялся в предыдущей программе. Например, вот как выглядят операторы case для значений 1 0 0 0, 1 2 0 5 и 8 9 0 0: case 1000 : case 120 5 : case 8 900 : priori tyLevel break; // в традиционном операторе swi tch используется // укладка операторов case 1; 506 Часть 1. Язык Java Укладка операторов case позволяет всем трем конструкциям case исполь­ зовать одну и ту же кодовую последовательность, предназначенную для уста­ новки переменной priorityLevel в 1. Как объяснялось в главе 5, в switch традиционного стиля укладка операторов ca se становится возможной из­ за того, что поток выполнения проходит через все case, пока не встретится break. Хотя такой подход работает, более элегантное решение предусматри­ вает применение списка констант case. Начиная с JDK 14, в одном операторе case можно указывать более одной константы case, просто разделяя их запятыми. Например, ниже показан бо­ лее компактный вид оператора case для значений 1 0 00, 1 2 0 5 и 8 9 0 0: case 1000, 1 2 0 5 , 8 900: priorityLevel = 1; break; // использовать с п исок констант case Перепишем полный оператор switch для использования списка констант case: // В этом операторе switch применяется список констант case. switch (eventCode ) { case 1000, 120 5, 8 900 : priorityLevel = 1; break ; case 2000 , 6010 , 912 8 : priorityLevel = 2; break; case 1002, 70 2 3, 9300 : priorityLevel = 3 ; break; // нормальный приоритет default : priorityLevel = О; Как видите, количество операторов case сократилось до трех, улучшив читабельность и управляемость оператора switch. Несмотря на то что под­ держка списка констант case сама по себе не добавляет в switch какие-либо фундаментально новые возможности, она помогает оптимизировать код. Во многих ситуациях поддержка списка констант case также предлагает про­ стой способ улучшения существующего кода, особенно если ранее широко применялась укладка операторов case. Таким образом, она является сред­ ством, которое можно запустить в работу немедленно при минимальном пе­ реписывании кода. П о явле ние выражения swi tch и оператора yield Среди расширений оператора switch наибольшее влияние окажет выра­ жение sw itch, которое представляет собой по существу оператор swi tch, возвращающий значение. Таким образом, оно обладает всеми возможностя­ ми традиционного оператора switch, а также способностью производить ре­ зультат, что делает выражение switch одним из самых важных дополнений языка Java за последние годы. Глава 1 7. Выражения switch, записи и прочие недавно добавленные средства 507 Предоставить значение выражения swi tch можно с помощью нового опе­ ратора yield, который имеет следующий общий вид: yield value; Здесь значение value производится оператором switch и может быть лю­ бым выражением, совместимым с требуемым типом значения. Ключевой для понимания аспект, связанный с оператором yi eld, состоит в том, что он не­ медленно завершает swi tch. Следовательно, он работает примерно так же, как break, но обладает дополнительной возможностью предоставлять значе­ ние. Важно отметить, что yield - контекстно-чувствительное ключевое сло­ во, т.е. yi eld за рамками выражения swi tch является просто идентификато­ ром без специального смысла. Тем не менее, в случае использования метода по имени yi eld () он должен быть уточнен. Например, если внутри класса определен нестатический метод yield ( ), тогда придется вызывать его как this . yield (). Указать выражение swi tch очень легко. Необходимо просто разместить swi tch в контексте, где требуется значение, скажем, в правой части операто­ ра присваивания, в аргументе метода или в возвращаемом из метода значе­ нии. В приведенной далее строке задействовано выражение swi tch: int х = switch(y) { // .. . Здесь результат swi tch присваивается переменной х. С применением вы­ ражения swi tch связан один важный момент: каждый оператор case (плюс de fault) должен производить значение (если только не генерируется исклю­ чение). Другими словами, каждый путь через выражение swi tch должен обе­ спечивать результат. Добавление выражения swi tch упрощает написание кода в ситуациях, когда каждый оператор case устанавливает значение некоторой переменной. Такие ситуации могут возникать по-разному. Скажем, каждый case может устанавливать булевскую переменную, которая указывает на успех или неу­ дачу какого-либо действия, предпринятого в swi tch. Однако зачастую уста­ новка переменной оказывается основной целью swi tch, как в случае операто­ ра swi tch, используемого в предыдущей программе. Его работа была связана с установлением уровня приоритета, ассоциированного с кодом события. В традиционном операторе swi tch каждый оператор case должен инди­ видуально присваивать значение переменной, и эта переменная становит­ ся де-факто результатом переключения. Именно такой подход применялся в предшествующих программах, где каждый оператор case устанавливал значение переменной pri o ri t y Level. Хотя этот подход десятилетиями использовался в программах на Java, выражение swi tch предлагает более эффективное решение, поскольку желаемое значение создается самим опе­ ратором swi tch. Предыдущее обсуждение воплощено в следующей версии программы за счет замены оператора switch выражением swi tch: 508 Часть 1. Язык Java / / Преобразование оператора switch в выражение switch . class SwitchExpr { puЫic s tat i c void main ( St ring [ ] args ) { int eventCode = 60 1 0 ; / / Это выражение switch . Обратите внимание на то, как его значение // присваивается переменной priori tyLeve l , а также на то, / / что значение switch предоставляется оператором yield . int priori tyLevel = switch ( eventCode ) { case 1 0 0 0 , 1 2 0 5 , 8 900 : yield 1 ; case 2 0 0 0 , 6 0 1 0 , 9 1 2 8 : yield 2 ; case 1 0 0 2 , 7 0 2 3 , 9300 : yield 3 ; defau l t : / / нормаль ный приоритет yield О ; }; System . out . println ( "Ypoвeнь приоритета для кода события " + eventCode + " равен " + priorityLevel ) ; Взгляните внимательно на оператор swi tch в программе. Обратите вни­ мание, что он существенно отличается от того, который применялся в пред­ шествующих примерах. Вместо присваивания p r i o r i tyLevel значения внутри каждого оператора case в текущей версии программы переменной priorityLevel присваивается результат самого switch. Таким образом, тре­ буется только одно присваивание priori tyLevel и длина swi tch уменьша­ ется. Использование выражения switch также гарантирует выдачу значения каждым оператором case, что не позволит забыть об указании значения для priorityLevel в каком-нибудь case. Кроме того, значение switch выпуска­ ется оператором yield внутри каждого оператора case. Как упоминалось ранее,