Г 9-Л Java ^ ^ здани руководство для начинающих Создавайте, компилируйте и запускайте программы на языке Java прямо сейчас! Обновленное и расширенное издание Герберт Шилдт Мс Graw Hill Education А11АЛЕКПН1КА Java: руководство для начинающих 9-е издание Java TM A Beginner's Guide Ninth Edition Herbert Schildt Me Gravu Hill New York Chicago San Francisco Athens London Madrid Mexico City Milan New Delhi Singapore Sydney Toronto Java: руководство для начинающих 9-е издание Герберт Шилдт КИ1В Видавництво “НАУКОВИЙ CBIT” 2023 УДК 004.432 Ш57 Перевод с английского и редакция Ю.Н . Артеменко Шилдт, Г. Ш57 Java: руководство для начинающих, 9-е изд./ Герберт Шилдт; пер. с англ . Ю.Н. Артеменко. Киев. : “ Науковий Свгг”, 2023. 752 с. : ил . Парал . тит. англ . ISBN 978-617-550-100-9 (укр.) ISBN 978-1-260-46355-2 (англ .) — — — В книге раскрыты основы и кратко описаны расширенные функциональные средства языка Java, в числе которых многопоточное программирование, обобщения, лямбда -выражения и графический интерфейс Swing. Вдобавок приводится четкое объяснение перечислений, модулей и методов интерфейса. В этом руководстве предлагается эффективное сочетание теории и практики написания кода, которое позволит быстро приступить к разработке приложений на языке Java. Книга предназначена для программистов, желающих изучить язык Java, и для разработчиков веб-приложений, которые стремятся повысить уровень знаний и мастерства. УДК 004.432 Все названия программных продуктов являются зарегистрированными торговыми марками соответствующих фирм. Все права защищены. Никакая часть настоящего издания ни в каких целях не может быть воспроизведена в какой бы то ни было форме и какими бы то ни было средствами, будь то электронные или механические, включая фотокопирование и запись на магнитный носитель, если на это нет письменного разрешения издательства McGraw Hill. Copyright © 2022 by 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 responsible for any errors or omissions. Authorized translation from the English language edition of the Java: A Beginner's Guide, 9th Edition ( ISBN 978-1- 260-46355-2), published by McGraw Hill. Except as permitted under the United States Copyright Act of 1976, no part of this publication may be reproduced or distributed in any form or by any means, or stored in a database or retrieval system, without the prior written permission of the publisher. ISBN 978-617-550-100-9 ( укр.) ISBN 978-1- 260-46355- 2 ( англ .) © ООО “ Науковий Св1т”, перевод, 2023 © 2022 by McGraw Hill Оглавление Предисловие 19 Глава 1. Основы языка Java 27 Глава 2. Введение в типы данных и операции 65 Глава 3. Операторы управления программой 97 Глава 4. Введение в классы, объекты и методы 135 Глава 5. Дополнительные сведения о типах данных и операциях 165 Глава 6. Дополнительные сведения о методах и классах 217 Глава 7. Наследование 259 Глава 8. Пакеты и интерфейсы 301 Глава 9. Обработка исключений 335 Глава 10. Использование ввода-вывода 365 Глава 11. Многопоточное программирование 411 Глава 12. Перечисления, автоупаковка, аннотации и многое другое 453 Глава 13. Обобщения 487 Глава 14. Лямбда-выражения и ссылки на методы 523 Глава 15. Модули Глава 16. Выражения switch, записи и прочие недавно добавленные средства 557 Глава 17. Введение в Swing Приложение А. Ответы на вопросы и решения упражнений 623 для самопроверки Приложение Б. Использование документирующих комментариев Java Приложение В. Компиляция и запуск простых однофайловых программ за один шаг 589 661 715 725 Приложение Г. Введение в JShell 729 Приложение Д. Дополнительные ключевые слова Java 741 Предметный указатель 746 Содержание Об авторе Предисловие Эволюция языка Java Структура книги Ключевые навыки и концепции Вопросы и упражнения для самопроверки Спросим у эксперта Упражнения Предыдущий опыт программирования не требуется Необходимое программное обеспечение Исходный код примеров Благодарности Глава 1 . Основы языка Java История и философия языка Java Происхождение Java Родословная Java: С и C + + Влияние Java на Интернет Магия Java: байт- код Выход за рамки аплетов Более быстрый график выпуска Характеристики языка Java Объектно-ориентированное программирование Инкапсуляция Полиморфизм Наследование Комплект разработчика на Java Первая простая программа Ввод исходного кода программы Компиляция программы Подробный анализ первого примера программы Обработка синтаксических ошибок Вторая простая программа Другие типы данных Два управляющих оператора Оператор if Цикл for Создание блоков кода 18 19 19 25 25 25 25 25 25 25 26 26 27 29 29 30 31 34 36 37 38 39 40 41 41 42 43 44 44 45 48 49 51 54 54 55 57 Содержание Точки с запятой и размещение операторов Практика отступов Ключевые слова Java Идентификаторы в Java Библиотеки классов Java Глава 2 . Введение в типы данных и операции Важность типов данных Примитивные типы Java Целые числа Типы с плавающей точкой Символы Тип boolean Литералы Шестнадцатеричные , восьмеричные и двоичные литералы Управляющие последовательности символов Строковые литералы Подробный анализ переменных Инициализация переменной Динамическая инициализация Область видимости и время жизни переменных Операции Арифметические операции Инкремент и декремент Операции отношения и логические операции Короткозамкнутые логические операции Операция присваивания Сокращенные операции присваивания Преобразование типов при присваивании Приведение несовместимых типов Старшинство операций Выражения Преобразования типов в выражениях Использование пробельных символов и круглых скобок Глава 3 . Операторы управления программой Ввод символов с клавиатуры Оператор if Вложенные операторы if Цепочка if-else-if Традиционный оператор switch Вложенные операторы switch Цикл for 7 58 59 61 62 62 65 66 67 67 69 70 71 73 74 74 75 76 77 77 78 81 81 82 83 85 86 88 88 90 91 93 94 95 97 98 100 101 102 103 107 109 8 Java: руководство для начинающих, 9-е издание Некоторые разновидности цикла for Пропуск частей Бесконечный цикл Циклы без тела Объявление переменных управления циклом внутри цикла for Расширенный цикл for Цикл while Цикл do-while Использование оператора break для выхода из цикла Использование оператора break как разновидности перехода в стиле “goto” Использование оператора continue Вложенные циклы Глава 4 . Введение в классы, объекты и методы Основы классов Общая форма класса Определение класса Создание объектов Ссылочные переменные и присваивание Методы Добавление метода в класс Vehicle Возврат из метода Возврат значения Использование параметров Добавление параметризованного метода в класс Vehicle Конструкторы Параметризованные конструкторы Добавление конструктора в класс Vehicle Еще раз об операции new Сборка мусора Ключевое слово this Глава 5 . Дополнительные сведения о типах данных и операциях Массивы Одномерные массивы Многомерные массивы Двумерные массивы Массивы нестандартной формы Массивы с тремя и более измерениями Инициализация многомерных массивов Альтернативный синтаксис объявления массивов Присваивание ссылок на массивы 111 112 113 113 114 115 115 116 121 123 127 132 135 136 137 138 141 142 142 143 145 146 148 150 157 158 159 160 160 161 165 166 167 172 172 173 175 175 176 176 Содержание 9 Использование члена массива length 177 Цикл for в стиле “ for- each ” 183 Проход по многомерным массивам 186 Применение расширенного цикла for 188 Строки 188 Конструирование строк 189 Оперирование строками 190 Массивы строк 192 Строки являются неизменяемыми 192 193 Использование строки для управления оператором switch 196 Использование аргументов командной строки 197 Использование выведения типов для локальных переменных Использование выведения типов локальных переменных для ссылочных типов 199 Использование выведения типов локальных переменных в цикле for 201 202 Некоторые ограничения var 203 Побитовые операции 203 Побитовые операции И , ИЛИ , исключающее ИЛИ и НЕ 208 Операции сдвига 210 Составные побитовые операции присваивания 213 Операция ? Глава 6. Дополнительные сведения о методах и классах Управление доступом к членам класса Модификаторы доступа Java Передача объектов методам Способы передачи аргументов Возвращение объектов Перегрузка методов Перегрузка конструкторов Рекурсия Ключевое слово static Статические блоки Введение во вложенные и внутренние классы Аргументы переменной длины Основы аргументов переменной длины Перегрузка методов с аргументами переменной длины Аргументы переменной длины и неоднозначность Глава 7. Наследование Основы наследования Доступ к членам и наследование Конструкторы и наследование 217 218 219 225 226 228 230 235 239 242 244 248 251 252 254 256 259 260 263 265 10 Java : руководство для начинающих, 9-е издание Использование ключевого слова super для вызова конструкторов суперкласса Использование ключевого слова super для доступа к членам суперкласса Создание многоуровневой иерархии Когда конструкторы выполняются ? Ссылки на суперклассы и объекты подклассов Переопределение методов Переопределенные методы поддерживают полиморфизм Зачем нужны переопределенные методы? Применение переопределения методов к классу TwoDShape Использование абстрактных классов Использование ключевого слова final Использование ключевого слова final для предотвращения переопределения Использование ключевого слова f i n a l для предотвращения наследования Использование ключевого слова final с членами данных Класс Object Глава 8. Пакеты и интерфейсы Пакеты Определение пакета Поиск пакетов и CLASSPATH Краткий пример пакета Пакеты и доступ к членам классов Пример доступа к пакету Защищенные члены Импортирование пакетов Библиотека классов Java содержится в пакетах Интерфейсы Реализация интерфейсов Использование ссылок на интерфейсы Переменные в интерфейсах Интерфейсы можно расширять Стандартные методы интерфейса Основы стандартных методов Более реалистичный пример стандартного метода Проблемы множественного наследования Использование статических методов в интерфейсе Закрытые методы интерфейса Заключительные соображения по поводу пакетов и интерфейсов 267 271 274 277 278 282 285 287 287 290 294 294 295 295 297 301 302 303 304 305 306 307 309 311 312 313 315 318 324 325 326 327 329 330 331 332 333 Содержание Глава 9. Обработка исключений Иерархия исключений Основы обработки исключений Использование try и catch Простой пример обработки исключений Последствия от неперехваченных исключений Исключения позволяют изящно обрабатывать ошибки Использование нескольких операторов catch Перехват подклассов исключений Блоки try могут быть вложенными Генерация исключений Повторная генерация исключений Подробный анализ класса Throwable Использование finally Использование throws Три дополнительных средства в системе исключений Встроенные исключения Java Создание подклассов Exception Глава 10. Использование ввода -вывода Ввод-вывод в Java основан на потоках Потоки байтовых и символьных данных Классы потоков байтовых данных Классы потоков символьных данных Предопределенные потоки Использование потоков байтовых данных Чтение консольного ввода Запись консольного вывода Чтение и запись в файлы с использованием потоков байтовых данных Ввод из файла Вывод в файл Автоматическое закрытие файла Чтение и запись двоичных данных Файлы с произвольным доступом Использование потоков Java , основанных на символах Консольный ввод с использованием потоков символьных данных Консольный вывод с использованием потоков символьных данных Файловый ввод- вывод с использованием потоков символьных данных Использование FileWriter Использование FileReader Использование оболочек типов Java для преобразования числовых строк Вопросы и упражнения для самопроверки 11 335 337 337 338 339 341 342 343 344 346 347 348 349 350 352 354 355 358 365 367 367 367 369 370 370 372 373 374 375 379 380 384 388 390 392 396 398 398 399 400 410 12 Java : руководство для начинающих, 9-е издание Глава 11 . Многопоточное программирование Основы многопоточности Класс Thread и интерфейс Runnable Создание потока Одно улучшение и два простых изменения Создание нескольких потоков Выяснение, завершен ли поток Приоритеты потоков Синхронизация Использование синхронизированных методов Оператор synchronized Взаимодействие между потоками с использованием notify(), wait() и notifyAll() Пример использования wait ( ) и notify {) Приостановка , возобновление и останов потоков Глава 12. Перечисления, автоупаковка, аннотации и многое другое Перечисления Основы перечислений Перечисления Java являются типами классов Методы values ( ) и valueOf() Конструкторы , методы , переменные экземпляра и перечисления Два важных ограничения Перечисления унаследованы от Enum Автоупаковка Оболочки типов Основы автоупаковки Автоупаковка и методы Автоупаковка/автораспаковка и выражения Предостережение Статическое импортирование Аннотации (метаданные) Введение в операцию instanceof Глава 13 . Обобщения Основы обобщений Простой пример обобщения Обобщения работают только со ссылочными типами Обобщенные типы различаются на основе их аргументов типов Обобщенный класс с двумя параметрами типов Общая форма обобщенного класса 411 412 414 414 418 425 427 430 433 434 437 439 441 446 453 455 455 458 458 459 461 461 468 469 471 472 473 475 475 479 482 487 489 490 493 494 494 495 Содержание Ограниченные типы Использование аргументов с подстановочными знаками Ограниченные аргументы с подстановочными знаками Обобщенные методы Обобщенные конструкторы Обобщенные интерфейсы Низкоуровневые типы и унаследованный код Выведение типов с помощью ромбовидной операции Выведение типов локальных переменных и обобщения Стирание Ошибки неоднозначности Некоторые ограничения обобщений Невозможность создать экземпляры параметров типов Ограничения , касающиеся статических членов Ограничения , касающиеся обобщенных массивов Ограничения , касающиеся обобщенных исключений Продолжение изучения обобщений Глава 14 . Лямбда-выражения и ссылки на методы Введение в лямбда - выражения Основы лямбда - выражений Функциональные интерфейсы Лямбда - выражения в действии Блочные лямбда - выражения Обобщенные функциональные интерфейсы Лямбда - выражения и захват переменных Генерация исключений в лямбда - выражениях Ссылки на методы Ссылки на статические методы Ссылки на методы экземпляра Ссылки на конструкторы Предопределенные функциональные интерфейсы Глава 15 . Модули Основы модулей Простой пример модуля Компиляция и запуск первого примера модуля Более подробный анализ операторов requires и exports Модуль java.base и модули платформы Унаследованный код и неименованные модули Экспортирование пакета для указанного модуля Использование requires transitive 13 495 499 501 503 506 507 513 516 518 518 519 520 520 520 520 521 521 523 525 525 527 529 533 535 541 542 544 544 546 550 552 557 559 560 565 566 567 569 570 571 14 Java : руководство для начинающих, 9-е издание Использование служб Основы служб и поставщиков служб Ключевые слова , связанные со службами Пример службы , основанной на модулях Дополнительные характеристики модулей Открытые модули Оператор opens Оператор requires static Продолжение изучения модулей Глава 16 . Выражения switch, записи и прочие недавно добавленные средства Расширения оператора switch Использование списка констант case Появление выражения switch и оператора yield Появление стрелки в операторе case Подробный анализ оператора case со стрелкой Записи Основы записей Создание конструкторов записи Подробный анализ методов получения для записи Сопоставление с образцом в операции instanceof Запечатанные классы и интерфейсы Запечатанные классы Запечатанные интерфейсы Будущие направления развития Глава 17 . Введение в Swing Происхождение и философия , положенная в основу проектного решения Swing Компоненты и контейнеры Компоненты Контейнеры Панели контейнеров верхнего уровня Диспетчеры компоновки Первая простая программа Swing Анализ первой простой программы Swing Обработка событий в Swing События Источники событий Прослушиватели событий Классы событий и интерфейсы прослушивателей 576 576 577 578 584 584 585 585 585 589 591 593 594 596 598 605 605 608 613 613 616 616 618 620 623 625 627 628 628 629 629 630 632 636 636 636 637 637 Содержание Использование JButton Работа с JTextField 15 638 642 645 648 Создание JCheckBox Работа с JList Использование внутренних анонимных классов или лямбда-выражений для обработки событий 657 Приложение А. Ответы на вопросы и решения упражнений для самопроверки Глава 1. Основы языка Java Глава 2. Введение в типы данных и операции Глава 3. Операторы управления программой Глава 4. Введение в классы , объекты и методы Глава 5. Дополнительные сведения о типах данных и операциях Глава 6. Дополнительные сведения о методах и классах Глава 7. Наследование Глава 8. Пакеты и интерфейсы Глава 9. Обработка исключений Глава 10. Использование ввода- вывода Глава 11. Многопоточное программирование Глава 12. Перечисления , автоупаковка, аннотации и многое другое Глава 13. Обобщения Глава 14. Лямбда- выражения и ссылки на методы Глава 15. Модули Глава 16. Выражения switch , записи и прочие недавно добавленные средства Глава 17. Введение в Swing Приложение Б . Использование документирующих комментариев Java Дескрипторы javadoc @author {@code} @deprecated {@docRoot} @exception @hidden {@index} {@inheritDoc} {@link} {@linkplain} { © literal} @param © provides 661 662 664 665 668 669 674 678 681 683 686 689 691 695 699 702 704 708 715 716 717 718 718 718 718 718 718 719 719 719 719 720 720 16 Java : руководство для начинающих, 9-е издание @ return @ see @ since { @ summary } @ throws @ uses { @ value } (Aversion Общая форма документирующего комментария Вывод утилиты j avadoc Пример использования документирующих комментариев Приложение В. Компиляция и запуск простых однофайловых программ за один шаг Приложение Г . Введение в JShell Основы JShell Просмотр, редактирование и повторное выполнение кода Добавление метода Создание класса Использование интерфейса Вычисление выражений и использование встроенных переменных Импортирование пакетов Исключения Другие команды JShell Дальнейшее исследование JShell Приложение Д. Дополнительные ключевые слова Java Модификаторы transient и volatile strictfp assert Собственные методы Еще одна форма this Предметный указатель 720 720 721 721 721 721 721 722 722 722 722 725 729 730 733 734 735 736 737 738 739 739 740 741 742 742 743 744 744 746 Об авторе Автор многочисленных бестселлеров Герберт Шилдт занимался написанием книг по программированию на протяжении более трех десятилетий и считается признанным авторитетом по языку Java. Журнал International Developer назвал его одним из ведущих авторов книг программированию в мире . Книги Герберта Шилдта продавались миллионными тиражами по всему миру и переведены на многие языки . Он является автором многочисленных книг по Java , в том числе Java. Полное руководство; C++; методики программирования Шилдта; Искус ство программирования на Java и SWING: руководство для начинающихе. Герберт Шилдт также много писал о языках С , C ++ и С #. В книге Эда Бернса Secrets of the Rock Star Programmers: Riding the IT Crest указано , что как один из звездных программистов Шилдт интересуется всеми аспектами вычислений , но его основное внимание сосредоточено на языках программирования. Герберт Шилдт получил степени бакалавра и магистра в Иллинойском университете. Его сайт доступен по адресу www.HerbSchildt.com. О научном редакторе Доктор Дэнни Ковард работал над всеми версиями платформы Java. Он руководил определением сервлетов Java в первой версии платформы Java ЕЕ и в более поздних версиях, внедрением веб -служб в Java ME, а также стратегией и планированием для 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 , включая многопоточное программирование , обобщения , лямбда- выражения , записи и модули. Завершает книгу введение в основы Swing. Закончив изучение книги , вы будете хорошо разбираться в основах программирования на Java. Прежде всего, важно отметить, что данная книга является лишь отправной точкой. Java нечто большее , чем просто элементы, определяющие язык. Java также включает обширные библиотеки и инструменты, помогающие разрабатывать программы . Чтобы быть первоклассным программистом на Java , необходи мо освоить эти области. После прочтения книги у вас будут знания , необходимые для изучения остальных аспектов Java. — Эволюция языка Java Коренным образом изменить саму суть программирования смогли только некоторые языки. Но и в этой группе избранных один язык выделяется среди остальных , потому что его влияние было быстрым и широко распространен ным. Конечно же , речь идет о Java. Не будет преувеличением сказать, что первоначальный выпуск Java 1.0 в 1995 году компанией Sun Microsystems, Inc. произвел революцию в программировании , которая радикально трансформировала Интернет в по- настоящему интерактивную среду. Вдобавок язык Java установил новый стандарт в проектировании языков программирования. На протяжении многих лет язык Java продолжал расти , развиваться и иным образом переопределять себя. В отличие от многих других языков, которые медленно внедряют новые функциональные средства , Java часто находится в аван гарде разработки языков программирования. Одной из причин является культура инноваций и изменений , которая окружала Java . В результате язык Java претерпел несколько обновлений , из которых одни были относительно небольшими, а другие более значительными . — Предисловие 19 Первым крупным обновлением Java стала версия 1.1. Возможности , добавленные в Java 1.1, оказались более существенными , чем можно было подумать из- за увеличения номера только младшей версии. Например, в Java 1.1 добавлено много новых библиотечных элементов, переопределен способ обработки событий и перенастроены многие функции исходной библиотеки версии 1.0. Следующим крупным выпуском Java был Java 2, где “ 2 ” означает “ второе поколение ” . Создание Java 2 стало переломным событием , положившим начало “ современной эпохи ” Java. Первый выпуск Java 2 имел номер версии 1.2. Может показаться странным , что для первого выпуска Java 2 использовался номер версии 1.2. Причина в том, что первоначально он относился к внутреннему номеру версии библиотек Java , но потом был обобщен для ссылки на целый выпуск. С выходом Java 2 в компании Sun перепаковали продукт Java как J 2SE (Java 2 Platform Standard Edition платформа Java 2, стандартная редакция ) и номера версий стали применяться к данному продукту. Первым крупным обновлением первоначального выпуска Java 2 стала версия J2SE 1.3. По большей части она расширяла существующую функциональность и “ усиливала ” среду разработки. Выпуск J2SE 1.4 дополнительно улучшил Java. Он содержал несколько важных обновлений, улучшений и дополнений, в том числе цепочки исключений , подсистему ввода- вывода на основе каналов и ключевое слово assert. Выпуск J2SE 5 произвел по существу вторую революцию Java. В отличие от большинства предшествующих обновлений Java , которые предлагали важные , но умеренные улучшения , выпуск J2SE 5 фундаментальным образом расширил границы, возможности и область использования языка. Чтобы оценить значи мость изменений , которые были внесены в Java в выпуске J2SE 5, взгляните на следующий список, где перечислены только основные из всех новых функциональных средств: — z обобщения ; z автоупаковка и автораспаковка; z перечисления; z расширенный цикл for в стиле “ for-each ” ; z аргументы переменной длины (vararg ); z статическое импортирование; z аннотации. Мелкие подстройки или поэтапные обновления в списке не указаны. Каждый элемент списка представляет значительное дополнение языка Java . Неко торые из них, такие как обобщения , расширенный цикл for и аргументы переменной длины, вводили новые синтаксические элементы. Другие , в числе ко торых автоупаковка и автораспаковка , изменяли семантику языка. Аннотации привнесли в программирование совершенно новое измерение . 20 Java: руководство для начинающих, 9-е издание Важность упомянутых выше новых средств отражена в назначенном номере версии 5. В других условиях следующим номером версии Java был бы 1.5. Однако новые средства были настолько значительными , что переход от версии 1.4 к 1.5, казалось, просто не смог бы выразить масштаб изменений. Взамен в Sun предпочли увеличить номер версии до 5, чтобы подчеркнуть тот факт, что произошло важное событие. Таким образом , выпуск получил название J2SE 5 , а комплект разработчика на Java (Java Development Kit ) JDK 5. Тем не менее, ради соблюдения согласованности в Sun решили использовать 1.5 в качестве внутреннего номера версии , который также называют номером версии разработчиков. Цифра 5 в J 2SE 5 называется номером версии продукта. Следующий выпуск Java получил имя Java SE 6. В Sun снова решили изменить название платформы Java. Прежде всего, обратите внимание , что цифра 2 была отброшена . Соответственно , платформа стала называться Java SE, а офи циальный продукт Java Platform, Standard Edition 6 (платформа Java , стан дартная редакция ). Комплекту разработчика на Java было дано название 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 . Новые языковые средства были разработаны в рамках проекта Project Coin. Целью проекта Project Coin была идентификация ряда небольших изменений языка Java , подлежащих включению в JDK 7. Ниже приведен список языковых средств, добавленных комплектом JDK 7. z Наделение типа string способностью управлять оператором switch. z Двоичные целочисленные литералы. z Символы подчеркивания в числовых литералах. z Расширение оператора try под названием оператор try с ресурсами , который поддерживает автоматическое управление ресурсами. z Выведение типов ( через ромбовидную операцию ) при конструировании экземпляра обобщенного типа. z Усовершенствованная обработка исключений , при которой два или большее количество исключений можно перехватывать одним оператором catch ( многократный перехват ), и улучшенная проверка типов для исключений, генерируемых повторно. — — — Предисловие 21 Как видите , несмотря на то , что средства Project Coin считались небольши ми изменениями , внесенными в язык , их преимущества были гораздо значи тельнее , чем можно было бы предположить по характеристике “ небольшие ” . В частности, оператор try с ресурсами основательно повлиял на способ напи сания значительного объема кода . Следующим выпуском Java был Java SE 8 с комплектом разработчика JDK 8. Он имел внутренний номер версии 1.8. Комплект JDK 8 стал значительным обновлением языка Java из- за включения многообещающего нового языкового средства: лямбда-выражений. Влияние лямбда - выражений было и продолжило быть весьма существенным, изменяя как способ концептуализации программ ных решений , так и способ написания кода Java . Помимо прочего лямбда- вы ражения содействуют упрощению и сокращению объема исходного кода , необходимого для создания определенных конструкций , таких как некоторые виды анонимных классов. Добавление лямбда-выражений также привело к появлению в языке новой операции (-> ) и нового элемента синтаксиса. Кроме лямбда - выражений в JDK 8 было добавлено много других важных новых средств. Например, начиная с JDK 8 , появилась возможность определять стандартную реализацию для метода , объявленного в интерфейсе. В конечном итоге Java SE 8 стал крупным выпуском , значительно расширившим возможности языка и изменившим способ написания кода Java. Следующим выпуском Java стал Java SE 9. Комплект разработчика назывался JDK 9, а внутренним номером версии также был 9. Комплект JDK 9 представлял крупный выпуск Java , включая значительные улучшения как языка Java , так и его библиотек. Главным новым средством JDK 9 были модули, которые позволили указывать взаимосвязи и зависимости кода , составляющего приложение. Модули также добавляют еще одно измерение к средствам контроля доступа Java . Включение модулей привело к добавлению в Java нового элемента синтаксиса , нескольких ключевых слов и различных усовершенствований инструментов. Модули оказали основательное влияние и на библиотеку API , т.к. начиная с JDK 9, библиотечные пакеты организованы в виде модулей. Помимо модулей в JDK 9 включен ряд других новых средств. Особый ин - — терес представляет JShell инструмент, который поддерживает интерактивное экспериментирование с программами и изучение Java. ( Введение в JShell приведено в приложении Б. ) Еще одним интересным обновлением является поддержка закрытых методов интерфейсов. Их добавление в дальнейшем расширя ет поддержку JDK 8 стандартных методов в интерфейсах. В JDK 9 инструмент javadoc был снабжен возможностью поиска , для поддержки которой предус мотрен новый дескриптор под названием @ index. Как и предшествующие выпуски, JDK 9 вносит несколько усовершенствований в библиотеки Java API . Обычно в любом выпуске Java наибольший интерес вызывают новые средства. Тем не менее , выпуск JDK 9 примечателен тем , что в нем объявлен устаревшим один заметный аспект Java: аплеты. Начиная с JDK 9 , применять аплеты в новых проектах больше не рекомендуется. Как объяснялось ранее в главе , 22 Java: руководство для начинающих, 9-е издание из- за ослабления поддержки аплетов со стороны браузеров (и других факторов) в выпуске JDK 9 объявлен устаревшим весь API для аплетов. Следующим выпуском Java был Java SE 10 (JDK 10 ). Тем не менее , до этого выпуска в графике выпусков Java произошло значительное изменение. В прошлом между крупными выпусками часто проходило два или более лет. Однако, начиная с JDK 10 , время между выпусками значительно сократилось. Ожида ется , что в соответствие со строгим графиком выхода расчетное время между крупными выпусками ( которые теперь называются выпусками функциональных средств ) составляет всего лишь пол года . В результате выпуск JDK 10 появился в марте 2018 года , т.е. спустя полгода после выпуска JDK 9. Такая увеличенная частота выпусков позволит программистам на Java получать своевременный доступ к новым средствам и улучшениям. Вместо того чтобы ждать два года или дольше , когда новое функциональное средство будет готово, оно становится частью следующего запланированного выпуска. Еще одним аспектом изменений в графике выпусков Java является выпуск с долгосрочной поддержкой (long -term support LTS ). Ожидается , что выпуск LTS будет производиться каждые три года. Выпуск LTS будет поддерживаться ( и , следовательно , оставаться жизнеспособным ) в течение периода времени , превышающего полгода . Первым выпуском LTS был JDK 11. Вторым выпуском LTS стал JDK 17, с учетом которого была обновлена эта книга. Из- за стабильно сти , предлагаемой выпуском LTS , вполне вероятно, что его набор средств будет определять базовый уровень функциональности для промежутка в несколько лет. Последние сведения о долгосрочной поддержке и графике выхода выпусков LTS ищите на веб-сайте Oracle. Главным новым языковым средством, добавленным JDK 10 , стала поддержка выведения типов локальных переменных, благодаря которому теперь можно позволить выводить тип локальной переменной из типа ее инициализатора , а не указывать его явно. Для обеспечения такой возможности в Java было добавлено контекстно-чувствительное ключевое слово var. Выведение типов способно упростить код за счет устранения необходимости избыточно указывать тип переменной , когда его можно вывести из инициализатора переменной. Выведение типов также может упростить объявления в ситуациях, при которых тип трудно понять или невозможно указать явно. Выведение типов локальных переменных стало обычной частью современной программной среды. Его включение в Java помогло сохранять Java в актуальном состоянии с учетом меняющихся тенден ций в проектировании языков. Наряду с другими изменениями в JDK 10 переопределена строка версии Java с изменением смысла номеров версий , чтобы они лучше соотносились с новым графиком выпуска. Следующим выпуском Java был Java SE 11 (JDK 11). Он вышел в сентябре 2018 года, т.е. через полгода после JDK 10, и был выпуском LTS. Главным новым языковым средством в JDK 11 являлась поддержка использования ключевого слова var в лямбда-выражениях. Кроме того , загрузчик приложений Java получил еще один режим выполнения , позволяющий напрямую запускать простые однофайловые программы. В JDK 11 также были удалены некоторые средства . — Предисловие 23 Возможно , наибольший интерес представляло удаление поддержки аплетов ввиду их исторической значимости . Вспомните , что аплеты впервые были объявлены нерекомендуемыми в JDK 9. В выпуске JDK 11 поддержка аплетов была удалена. Поддержка другой технологии развертывания , называемой Java Web Start , тоже была удалена в JDK 11. Еще одним ключевым изменением в JDK 11 стало то , что JavaFX больше не входит в состав JDK. Взамен эта инфраструктура для построения графических пользовательских интерфейсов превратилась в проект с открытым кодом. Из-за того, что упомянутые средства перестали быть частью JDK, в книге они не обсуждаются. Между LTS-выпуском JDK 11 и следующим LTS - выпуском (JDK 17 ) вышли пять выпусков функциональных средств: с JDK 12 до JDK 16. В выпусках JDK 12 и JDK 13 новые языковые средства не добавлялись. В выпуске JDK 14 была добавлена поддержка выражения switch , которое представляет собой конструкцию switch, производящую значение . Также были проведены другие усовершенствования switch. В выпуске JDK 15 появились текстовые блоки , по существу являющиеся строковыми литералами , которые могут занимать более одной строчки . В выпуске JDK 16 операция instanceof была расширена сопоставле нием с образцом и добавлен новый тип класса под названием запись вместе с новым контекстно-чувствительным ключевым словом record. Запись предлагает удобный способ объединения данных. В выпуске JDK 16 также предоставляется новый инструмент пакетирования приложений, называемый jpackage. На момент написания книги самой последней версией Java была Java SE 17 (JDK 17 ). Как упоминалось ранее , это второй LTS- выпуск Java , что имеет особое значение. Его главное новое средство возможность запечатывать классы и интерфейсы . Запечатывание дает вам контроль над наследованием класса , а также над наследованием и реализацией интерфейса . Для такой цели были добавлены контекстно- чувствительные ключевые слова sealed , permits и non -sealed ( первое ключевое слово Java с дефисом ). Кроме того, в JDK 17 произошла пометка API для аплетов как устаревшего и подлежащего удалению. Вы уже знаете , что поддержку аплетов убрали несколько лет назад. Однако API для аплетов просто был помечен как нерекомендуемый , что позволяло компилировать практически исчезающий код, который полагался на этот API. После вы хода JDK 17 теперь API для аплетов подлежит удалению в будущем выпуске. И еще один момент относительно эволюции Java: в 2006 году начался процесс открытия исходного кода Java. В наши дни доступны реализации JDK с открытым кодом . Открытие исходного кода дополнительно способствует динамичной природе процесса разработки Java . В конечном итоге наследием инно ваций Java является безопасность. Java остается живым и гибким языком , который привыкли ожидать в мире программирования. Материал настоящей книги обновлен с учетом JDK 17. Тем не менее , как подчеркивалось в предыдущем обсуждении, история программирования на Java характеризуется энергичными изменениями. Рекомендуется отслеживать новые возможности в каждом последующем выпуске Java. Проще говоря: эволюция Java продолжается! — 24 Java: руководство для начинающих, 9- е издание Структура книги Эта книга представляет собой учебное пособие , каждый раздел которого опирается на материал предыдущего раздела. Книга содержит 17 глав, в каждой из которых обсуждается тот или иной аспект языка Java. Уникальность книги в том , что она включает специальные элементы , которые закрепляют полученные знания. Ключевые навыки и концепции Каждая глава начинается с перечня важнейших навыков, которые вы полу чите в результате изучения главы. Вопросы и упражнения для самопроверки Каждая глава завершается вопросами и упражнениями, которые позволят вам проверить свои знания. Ответы находятся в приложении А. Спросим у эксперта В книге повсеместно встречаются специальные врезки “ Спросим у эксперта ” , содержащие дополнительную информацию либо интересные комментарии по теме. В них используется формат “ вопрос-ответ ” . Упражнения Каждая глава содержит одно или несколько упражнений , которые представляют собой проекты , демонстрирующие применение на практике того , что вы изучаете . Во многих случаях это реальные примеры , которые вы можете использовать в качестве отправной точки при написании собственных программ. Предыдущий опыт программирования не требуется В книге предполагается отсутствие у читателя предыдущего опыта программирования. Таким образом , если вы никогда раньше не программировали , то все равно можете воспользоваться этой книгой. При наличии опыта програм мирования вы сможете продвигаться немного быстрее. Однако имейте в виду, что Java обладает рядом ключевых отличий от других популярных языков программирования . Важно не делать поспешных выводов. Таким образом , даже для опытного программиста рекомендуется читать внимательно. Необходимое программное обеспечение Для компиляции и запуска всех программ , рассмотренных в книге , вам понадобится последняя версия JDK, которой на момент написания книги является JDK 17. Это JDK для Java SE 17. Инструкции по получению Java JDK при ведены в главе 1. Предисловие 25 Если вы имеете дело с более ранней версией Java , то по- прежнему сможете пользоваться книгой, но утратите возможность компиляции и запуска про грамм , в которых задействованы новые функциональные средства Java . Исходный код примеров Исходный код для всех примеров и проектов , рассмотренных в книге , доступен на веб-сайте издательства. Благодарности Я хочу выразить особую благодарность Дэнни Коварду, научному редактору этого издания книги. Дэнни работал над несколькими моими книгами , и его советы, идеи и предложения всегда имели большую ценность и получали высокую оценку. * ,* I/ , *Л* '• Vv'. •sS'.f*'t. • - vs S ', * 1.1 /V» N Sy t I I s I s* , .I ( »\ I . II I ,I t , II, I . II 4' » I I t .ч I I *I 4 >?• <> >4 x x ' •••• • Hi Глава 1 Основы языка Java " 28 Java : руководство для начинающих, 9-е издание В этой главе • • • z История и философия языка Java z Вклад Java в развитие Интернета z Важность байт- кода z Терминология Java z Фундаментальные принципы объектно-ориентированного программирования z Создание, компиляция и запуск простой программы на Java z Использование переменных z Применение управляющих операторов if и for z Создание блоков кода z Расположение , отступ и завершение операторов z Ключевые слова Java z Правила для идентификаторов Java я зык Java оказал влияние на ряд технологий в вычислительной технике . Его создание на заре существования Интернета помогло установить современ ную форму Интернета , включая клиентскую и серверную части. Инновационные средства Java продвинули вперед искусство и науку программирования , установив новый стандарт в разработке языков программирования. Культура дальновидного мышления, которая образовалась вокруг языка Java , гарантировала , что он останется ярким и живым, адаптируясь к зачастую быстрым и разне только нообразным изменениям во вселенной вычислений. В целом Java один из самых важных языков программирования в мире, но сила , которая произвела революцию в программировании и в ее ходе изменила мир. Хотя язык Java часто ассоциируется с программированием приложений для Интернета , он никоим образом не ограничен в данном отношении. Java мощный, полнофункциональный язык программирования общего назначения. Таким образом, если вы новичок в программировании, то Java будет превосходным вариантом для изучения. Более того , чтобы считаться профессиональным программистом в наши дни , важно уметь программировать на Java . В ходе чтения книги вы получите основные навыки , которые помогут овладеть языком — — Java. Глава 1 . Основы языка Java 29 Целью настоящей главы является ознакомление с языком Java , начиная с его истории, проектной философии и ряда наиболее важных характеристик. Безусловно , самое сложное в изучении языка программирования тот факт, что ни один элемент не существует в изоляции. Наоборот, компоненты языка работают в сочетании друг с другом. Взаимосвязь между ними особенно ярко выражена в Java. На самом деле довольно трудно обсуждать один аспект языка Java , не затрагивая другие. Для преодоления такой проблемы в этой главе представлен краткий обзор некоторых особенностей Java , в том числе общей формы программы на Java, нескольких основных управляющих структур и простых операций. Не вдаваясь в детали , основное внимание будет уделено тем концепциям , которые присутствуют в любой программе на Java . — История и философия языка Java Прежде чем удастся в полной мере оценить уникальные аспекты языка Java , необходимо понять силы , которые привели к его созданию , воплощаемую им философию программирования и ключевые проектные концепции . В ходе чте ния книги вы увидите , что многие аспекты Java были прямым или косвенным результатом действия исторических сил , сформировавших язык. Таким обра зом , вполне уместно, что изучение Java начинается с выяснения того, как Java соотносится с более широкой вселенной программирования. Происхождение Java Язык Java придумали в 1991 году Джеймс Гослинг, Патрик Нотой , Крис Варт, Эд Франк и Майк Шеридан , которые работали в Sun Microsystems. Сначала язык назывался Oak , но в 1995 году его переименовали в Java . На удивление первоначальной движущей силой появления Java был вовсе не Интернет! Основной мотивацией оказалась потребность в наличии независимого от платформы языка , который позволил бы создавать программное обеспечение , встраиваемое в различные бытовые электронные устройства , такие как тостеры , микроволновые печи и пульты дистанционного управления . Наверняка вы догадались, что в качестве контроллеров использовались центральные процессоры ( ЦП ) самых разных типов. Проблема заключалась в том , что ( в то время ) большинство языков программирования предусматривали компиляцию в ма шинный код, предназначенный для определенного типа ЦП . Рассмотрим , на пример , язык C ++. Хотя программу на C + + можно было скомпилировать практически для любого типа ЦП , требовался полный компилятор C ++ , предназначенный для кон кретного ЦП. Однако проблема заключалась в том, что создание компиляторов сопряжено с финансовыми затратами и временем . Пытаясь отыскать лучшее решение , Гослинг и другие трудились над переносимым межплатформенным языком, который позволил бы производить код, способный функционировать на различных ЦП в разных средах. Результатом всех их усилий стал язык Java. 30 Java: руководство для начинающих, 9- е издание Примерно в то же время , когда прорабатывались детали языка , возник еще один и в конечном итоге более важный фактор , сыгравший решающую роль в будущем Java. Этой второй силой была , конечно же , “ Всемирная паутина ” . Если бы веб- сеть не сформировалась практически одновременно с реализаци ей Java , то Java мог бы остаться полезным , но малоизвестным языком для программирования бытовой электроники. Тем не менее , благодаря развитию веб сети язык Java выдвинулся на передний план в области проектирования языков программирования, потому что в веб-сети тоже требовались переносимые программы. Большинство программистов еще в начале своей карьеры узнают, что переносимые программы — труднодостижимый идеал . Хотя поиски способа создания эффективных, переносимых ( независимых от платформы ) программ почти так же стары, как и сама дисциплина программирования, они отошли на второй план по сравнению с другими , более насущными проблемами. Однако с появлением Интернета и веб-сети старая проблема переносимости вернулась с удвоенной силой. В конце концов, Интернет представляет собой многообразную распределенную вселенную , переполненную многочисленными типами ком пьютеров, операционных систем и ЦП. То , что когда -то было раздражающей , но низкоприоритетной проблемой , превратилось в серьезную необходимость. К 1993 году членам группы проектировщиков Java стало совершенно ясно , что проблемы переносимости , часто возникающие при создании кода для встроенных контроллеров , также обнаруживаются при попытке создать код для Интернета. Это осознание привело к тому, что ориентация Java переключилась с бытовой электроники на програм мирование Интернет-приложений. Таким образом , хотя первоначальной побу дительной причиной стало стремление к независимому от архитектуры языку программирования , именно Интернет в конечном итоге привел к крупномас штабному успеху Java. Родословная Java: С и C++ История развития языков программирования не состоит из изолированных событий . Скорее это совокупность тесно связанных между собой явлений , где на каждый новый язык так или иначе воздействует то , что было раньше. В данном отношении Java не является исключением. Прежде чем двигаться дальше , полезно понять, какое место Java занимает в генеалогическом древе языков программирования. Ближайшими предками Java являются два языка С и C ++. Вероятно, вам уже известно, что С и C ++ входят в число наиболее важных языков программи рования, когда-либо изобретенных, и до сих пор применяются весьма широко. Синтаксис языка Java унаследован от С , а объектная модель адаптирована от C++. Связь Java с языками С и C++ важна по ряду причин. Во-первых, на момент — Глава 1 . Основы языка Java 31 создания Java многие программисты были знакомы с синтаксисом C/ C + + . Поскольку в Java используется аналогичный синтаксис , программисту на C/ C ++ было относительно легко изучить Java. В итоге появилась возможность применения Java практикующими разработчиками , что способствовало приня тию Java сообществом программистов. Во- вторых, проектировщики Java не стали “ изобретать велосипед ” . Взамен они усовершенствовали и без того весьма успешную парадигму программирования. Современная эпоха программирования началась с языка С , после чего перешла в C ++ и затем в Java. Перенимая и опираясь на такое богатое наследие , Java обеспечивает мощную , логически непротиворечивую среду программиро вания, которая берет лучшее из прошлого и добавляет новые возможности , свя занные с онлайновой средой и достижениями в искусстве программирования. Возможно, важнее всего то, что по причине своих сходств языки С, C + + и Java определяют общую концептуальную основу для профессионального программиста. При переходе с одного языка на другой программистам не приходится сталкиваться с серьезными трудностями. Язык Java обладает еще одной характерной чертой, присущей С и C ++: он был спроектирован , протестирован и доработан практикующими программистами. Вдобавок Java представляет собой язык, основанный на потребностях и опыте людей , которые его изобрели. Вряд ли есть лучший способ создания первоклассного и профессионального языка программирования . И последнее замечание: несмотря на взаимосвязь языков C ++ и Java , особенно того , что касается поддержки ими объектно- ориентированного програм мирования, Java не просто “ Интернет- версия C ++ ” . Язык Java имеет суще ственные практические и философские отличия от C + + . Кроме того , Java не является расширенной версией C ++. Например , он не совместим ни вверх , ни вниз с C + + . Более того , язык Java не задумывался в качестве замены C + + , а проектировался для решения определенного набора задач. В свою очередь язык C ++ был разработан для решения другого набора задач. Они будут сосуществовать долгие годы. — Влияние Java на Интернет Интернет помог выдвинуть Java на передний край программирования , а язык Java со своей стороны оказал огромное влияние на Интернет. Во- первых, создание Java упростило программирование Интернет- приложений в целом , действуя как катализатор , который привлек в веб-сеть буквально легионы программистов. Во-вторых , язык Java предложил новый вид сетевых программ под названием аплеты , которые изменили представление онлайнового мира о содержимом. Наконец, что важнее всего, язык Java решил некоторые из самых сложных проблем , ассоциированных с Интернетом: переносимость и безопасность. 32 Java : руководство для начинающих, 9-е издание Упрощение программирования приложений для веб-сети С появлением языка Java программирование веб- приложений значительно упростилось в нескольких отношениях. Возможно , наиболее важной следует считать возможность создания переносимых межплатформенных программ. Не менее важна и обеспечиваемая языком Java поддержка работы в сети. Его библиотека готовых к использованию функциональных средств позволила программистам легко писать программы , взаимодействующие с Интернетом. Язык Java также предложил механизмы , позволяющие легко доставлять программы через Интернет. Хотя рассмотрение всех деталей выходит за рамки настоящей книги, достаточно знать, что поддержка языком Java работы в сети была ключевым фактором быстрого роста его популярности. !?@>A8< C M:A?5@B0 ВОПРОС. Что собой представляет язык C # и как он связан с Java? ОТВЕТ. Через несколько лет после создания Java в Microsoft разработали язык С#. Данный факт важен, поскольку язык C# тесно связан с Java. На самом деле многие функциональные средства в C# полностью аналогичны таким средствам в Java. И Java, и C# имеют общий синтаксис в стиле C ++ , поддерживают распределенное программирование и применяют сходную объектную модель. Разумеется , между Java и C# есть отличия, но общий “ внешний вид” этих языков очень похож. Таким образом , если вы уже знакомы с языком С # , то изучение Java не потребует особых усилий . И наоборот, если в будущем вы планируете заняться С# , то знания Java вам вне всяких сомнений пригодятся. Аплеты Java На момент создания Java одним из самых захватывающих средств стал аплет программа на Java специального вида , которая предназначалась для передачи через Интернет и автоматического выполнения внутри веб-браузера , совместимого с Java. Щелчок пользователем на ссылке, содержащей аплет, при водил к автоматической загрузке и запуску аплета в браузере. Аплеты задумывались как небольшие программы , которые обычно применялись для отобра жения данных, предоставляемых сервером, обработки пользовательского ввода или реализации простых функций , таких как кредитный калькулятор. Отличи тельная особенность аплетов заключалась в том , что они выполнялись локально , а не на сервере. По существу аплет позволял переносить некоторую функциональность со стороны сервера на сторону клиента . Создание аплета было важным событием , т.к. в то время он расширял вселенную объектов, которые могли свободно перемещаться в киберпространстве. В целом есть две очень широкие категории объектов, которые передаются между — Глава 1 . Основы языка Java 33 сервером и клиентом: пассивная информация и динамически активные программы. Например, при чтении электронной почты происходит просмотр пассивных данных. Даже в случае загрузки программы ее код остается лишь пассивными данными, пока он не будет запущен. В действительности аплет представляет собой динамическую самовыполняющуюся программу, которая является активным агентом на клиентском компьютере , но доставляется сервером. На заре развития Java аплеты были важной частью программирования на Java. Они продемонстрировали мощь и преимущества языка , добавили вебстраницам захватывающее измерение и позволили программистам в полной мере изучить возможности Java. Хотя вполне вероятно, что аплеты все еще используются в наши дни , со временем они стали менее важными, и по причи нам , которые вскоре будут объяснены, в версии JDK 9 начался процесс поэтапного отказа от них. В конечном итоге в версии JDK 11 поддержка аплетов была полностью удалена. Безопасность Какими бы желательными ни были динамические сетевые программы , они становятся источником серьезных проблем в области безопасности и переноси мости. Очевидно , что программа , которая загружается и выполняется на кли ентском компьютере , не должна причинять вред. Она также должна работать в различных средах и под управлением разных операционных систем . Далее вы увидите, что такие проблемы в Java решаются эффективным и элегантным способом. Давайте рассмотрим каждую из них более подробно, начав с безопасности. По всей видимости , вам уже известно , что каждый раз , загружая программу, вы рискуете, т.к. загружаемый код может содержать вирус, “ троянский конь” или что-то еще вредоносное. В основе проблемы лежит тот факт, что вредоносный код способен нанести ущерб, поскольку он получает несанкционированный доступ к системным ресурсам. Например, вирусная программа может собирать лич ную информацию, такую как номера кредитных карт, остатки на банковских счетах и пароли, путем поиска в содержимом локальной файловой системы вашего компьютера. Для безопасной загрузки и выполнения программ на клиентском компьютере необходимо было предотвратить саму возможность такой атаки. Защита от атак подобного рода обеспечивается за счет того , что приложение ограничивается средой выполнения Java и ему запрещается доступ к другим частям компьютера. ( Вскоре вы узнаете , как это делается.) Возможность загрузки приложения с высокой степенью уверенности в том , что им не будет причинен какой-либо ущерб , в значительной степени способствовала раннему успеху Java. Переносимость — Переносимость важный аспект Интернета , поскольку к нему подключено множество различных типов компьютеров и операционных систем . С уче том того , что программу на Java нужно было запускать практически на любом компьютере , подключенном к Интернету, нужен был какой - то способ , 34 Java: руководство для начинающих, 9- е издание позволяющий программе выполняться на различных типах систем. Другими словами , был необходим механизм , позволяющий загружать и выполнять одно и то же приложение под управлением самых разных ЦП , операционных систем и браузеров. Иметь отличающиеся версии одного и того же приложения для разных компьютеров нецелесообразно. Один и тот же код приложения должен работать на всех компьютерах. Поэтому требовались средства генерации переносимого кода. Скоро вы увидите , что тот же самый механизм , который помог обеспечить безопасность, также поспособствовал переносимости. Магия Java: байт-код Справиться как с проблемами безопасности , так и с только что описанными проблемами переносимости , позволило решение о выпуске компилятором Java не исполняемого кода , а байт- кода. Байт-код представляет собой оптимизированный набор инструкций, предназначенных для выполнения так называемой виртуальной машиной Java (Java Virtual Machine JVM ), которая является частью исполняющей среды Java (Java Runtime Environment JRE). По существу первона чальная машина JVM разрабатывалась как интерпретатор байт -кода. Такой подход может показаться несколько неожиданным, потому что из- за соображений , связанных с производительностью , программы на многих современных языках предусматривают компиляцию в исполняемый код, зависящий от ЦП. Тем не менее, тот факт, что программа на Java выполняется машиной JVM , помогает решить основные проблемы, связанные с программами для веб-сети, и вот почему. Трансляция программы на Java в байт- код значительно упрощает запуск программы в самых разнообразных средах , поскольку для каждой платформы необходимо реализовать только среду JRE ( включая машину JVM ). После появления среды JRE для данной системы любая программа на Java может в ней функционировать. Не забывайте , что хотя детали JRE будут отличаться от платформы к платформе , все среды JRE воспринимают один и тот же байт-код Java. Если бы программа на Java компилировалась в собственный код , то для всех типов ЦП , установленных в подключенных к Интернету компьютерах , должны были бы существовать разные версии одной и той же программы. Конечно, такого решения добиться нелегко. Таким образом , выполнение байт-кода машиной JVM самый простой способ создания по- настоящему переносимых программ. Тот факт, что программа на Java выполняется машиной JVM , тоже помогает сделать ее безопасной . Поскольку машина JVM находится под контролем , она управляет выполнением программы. Таким образом , для машины JVM по явилась возможность создания ограниченной среды выполнения, называемую песочницей , которая содержит программу и предотвращает неограниченный доступ к ресурсам компьютера. Безопасность также повышается за счет определенных ограничений , существующих в языке Java. Когда программа интерпретируется , она обычно работает медленнее , чем в ситуации , если бы она была скомпилирована в исполняемый код. Однако в — — — Глава 1 . Основы языка Java 35 случае Java разница не так уж велика. Поскольку байт-код крайне оптимизи рован , его применение позволяет машине JVM выполнять программы намного быстрее , чем можно было бы ожидать. Хотя язык Java разрабатывался как интерпретируемый , в нем нет ничего та кого , что мешало бы компиляции байт- кода в машинный код на лету с целью повышения производительности . По этой причине вскоре после первоначального выпуска Java была представлена технология HotSpot , которая предлагала оперативный (Just - In -Time JIT) компилятор для байт- кода. Когда компилятор ЛТ входит в состав машины JVM , избранные порции байт-кода компилируются в исполняемый код в режиме реального времени, часть за частью по запросу. Важно понимать, что программа на Java не компилируется в исполняемый код сразу целиком. Взамен компилятор ЛТ компилирует код по мере необходимости во время выполнения . Более того , компилируются не все последовательности байт-кода , а только те, которые извлекают пользу из компиляции. Остальной код просто интерпретируется. Тем не менее , подход ЛТ все же обеспечивает значительный рост производительности. Даже при динамической компиляции байт- кода характеристики переносимости и безопасности сохраняются, т. к. ма шина JVM по- прежнему несет ответственность за среду выполнения. Еще один момент: проводились эксперименты с ранней ( ahead -of-time ) компиляцией для Java. Такой компилятор можно использовать для компиляции байт- кода в машинный код перед выполнением машиной JVM , а не на лету. Некоторые предшествующие версии JDK поставлялись с экспериментальным ранним компилятором , но в версии JDK 17 он был удален . Ранняя компиляция — это специализированная возможность, и она не заменяет описанный выше традиционный подход Java. По причине узкоспециализированной природы ранняя компиляция в книге обсуждаться не будет. — !?@>A8< C M:A?5@B0 ВОПРОС. Нередко приходится слышать об особом типе программ на Java, на зываемых сервлетами. Что они собой представляют? — ОТВЕТ. Сервлет Java это небольшая программа, которая выполняется на сервере . Сервлеты динамически расширяют функциональность веб-сервера. Важно понимать, что какими бы полезными ни были клиентские приложения, они образуют лишь половину архитектуры “ клиент-сервер ” . Вскоре после первоначального выпуска языка Java стало очевидно, что он также будет полезным на стороне сервера. Одним из результатов стал сервлет. Таким об разом , с появлением сервлета язык Java охватил обе стороны клиент-серверного взаимодействия . Хотя тема сервлетов и программирования на стороне сервера в целом выходит за рамки данного руководства для начинающих , вполне возможно, что они вас заинтересуют в ходе продвижения в програм мировании на Java . 36 Java : руководство для начинающих, 9-е издание Выход за рамки аплетов На момент написания книги после первоначального выпуска Java прошло более двух десятилетий . За прошедшие годы произошло много изменений. Во времена создания Java Интернет был захватывающей инновацией, веб-браузеры быстро развивались и совершенствовались, современная форма смартфона еще не была изобретена , а до повсеместного применения компьютеров оставалось еще несколько лет. Как и следовало ожидать, язык Java тоже изменился вместе со способом его использования. Возможно, ничто не иллюстрирует текущую эволюцию лучше , чем аплет. Как объяснялось ранее , в начальные годы существования Java аплеты были важной составляющей программирования на Java. Они не только увеличивали привлекательность веб-страниц, но стали крайне заметной особенностью языка Java, повышая его популярность. Однако аплеты полагаются на подключаемый модуль браузера для Java. Таким образом, чтобы аплет работал , браузер должен его поддерживать. В течение последних нескольких лет поддержка подключа емого модуля браузера для Java ослабла. Попросту говоря , без поддержки со стороны браузера аплеты нежизнеспособны. По этой причине в версии JDK 9 начался поэтапный отказ от аплетов, и поддержка аплетов была объявлена нерекомендуемой. В языке Java нерекомендуемое средство означает, что оно все еще доступно, но помечено как устаревшее. Следовательно , в новом коде нерекомендуемое средство применяться не должно. Поэтапный отказ завершился с выходом JDK 11, поскольку поддержка аплетов исполняющей средой была удалена. Начиная с версии JDK 17 , весь API -интерфейс аплетов (Applet API ) стал нерекомендуемым и подлежащим удалению , т.е . в какой-то момент в будущем он исчезнет. Интересно отметить, что спустя несколько лет после создания в Java была добавлена альтернатива аплетам , которая называлась Java Web Start и позволя ла динамически загружать приложение из веб-страницы. Она представляла собой механизм развертывания , особенно удобный в случае крупных приложе ний Java , которые не подходили для реализации в виде аплетов. Разница между аплетом и приложением Web Start заключалась в том , что приложение Web Start выполнялось само по себе, а не внутри браузера. Таким образом, оно выглядело во многом похоже на “ нормальное ” приложение. Тем не менее, это требовало наличия в размещающей системе автономной среды JRE, поддерживающей Web Start. В версии JDK 11 поддержка Java Web Start была удалена. Учитывая, что современные версии Java не поддерживают ни аплеты , ни Java Web Start , вас может интересовать, какой механизм должен использоваться для развертывания приложения Java. На момент написания книги частью ответа был инструмент jlink, добавленный в версии JDK 9. С его помощью можно создавать полный образ, который включает всю необходимую поддержку для вашей программы , в том числе среду JRE. Другая часть ответа инструмент jpackage, появившийся в версии JDK 16. Его можно применять для создания — Глава 1 . Основы языка Java 37 готового к установке приложения . Подробное обсуждение стратегий разверты вания выходит за рамки настоящей книги. К счастью, при чтении данной книги беспокоиться о развертывании не придется , потому что все примеры программ выполняются прямо на вашем компьютере . Они не разворачиваются через Интернет. Более быстрый график выпуска Недавно в Java произошло еще одно крупное изменение, но оно не касается языка или исполняющей среды. На самом деле изменение связано со способом планирования выпусков Java . В прошлом между основными выпусками Java обычно проходило от двух и более лет. Однако после выхода JDK 9 промежутки времени между основными выпусками Java сократились. В наши дни ожидается , что основной выпуск будет происходить по строгому графику, и расчетное время между выпусками составит всего лишь полгода. Каждый полугодичный выпуск, теперь называемый выпуском функциональных средств, будет включать те средства , которые готовы к моменту выпуска. Такая увеличенная частота выпусков позволит программистам на Java получать своевременный доступ к новым средствам и улучшениям . Кроме того , у Java появится возможность быстро реагировать на запросы постоянно меняющейся программной среды. Выражаясь просто, более быстрый график выпуска обеща ет стать очень позитивным событием для программистов на Java. Ожидается , что каждые три года будет производиться выпуск с долгосрочLTS ) . Выпуск LTS будет поддерживаться ной поддержкой (long-term support ( и , следовательно , оставаться жизнеспособным ) в течение периода времени , превышающего полгода. Первым выпуском LTS был JDK 11 . Вторым выпуском LTS стал JDK 17, с учетом которого была обновлена эта книга. Из-за стабильности , предлагаемой выпуском LTS, вполне вероятно , что его набор средств будет определять базовый уровень функциональности для промежутка в несколько лет. Последние сведения о долгосрочной поддержке и графике выхода выпусков LTS ищите на веб-сайте Oracle . В текущее время выпуски функциональных средств запланированы на март и сентябрь каждого года. В результате JDK 10 вышел в марте 2018 года , т.е. через пол года после выхода JDK 9. Следующий выпуск (JDK 11) вышел в сентя бре 2018 года и был выпуском LTS. Затем последовал JDK 12 в марте 2019 года , JDK 13 в сентябре 2019 года и т.д. На момент написания книги последним выпуском был JDK 17, который является выпуском LTS. Опять-таки ожидается, что каждые полгода будет выходить новый выпуск функциональных средств. Конеч но, имеет смысл ознакомиться с актуальной информацией о графике выпусков. Во время написания книги на горизонте показалось несколько новых функциональных средств Java . По причине более быстрого графика выпусков с вы сокой вероятностью можно предположить, что некоторые из них будут добавлены в Java в течение ближайших нескольких лет. Рекомендуется внимательно — 38 Java: руководство для начинающих, 9-е издание просматривать сведения и замечания по каждому полугодовому выпуску. Сейчас действительно самое подходящее время , чтобы стать программистом на Java! Характеристики языка Java Никакое обсуждение истории Java не будет полным без рассмотрения характеристик этого языка. Хотя основными факторами , вызвавшими изобретение Java , были переносимость и безопасность, важную роль в формировании фи нальной формы языка сыграли и другие факторы. Команда разработчиков Java подытожила ключевые соображения в виде списка характерных особенностей , приведенного в табл . 1.1. Таблица 1.1 . Список характерных особенностей языка Java Характеристика Описание Простота Язык Java обладает лаконичным и целостным набором функциональных возможностей, которые упрощают его изучение и использование Безопасность Язык предоставляет безопасные средства для созда- ния Интернет-приложений Переносимость Программы на Java могут выполняться в любой среде, для которой имеется исполняющая система Java Объектная ориентация Язык Java воплощает в себе современную философию объектно- ориентированного программирования Надежность Язык Java содействует снижению количества ошибок при программировании за счет строй типизации и выполнения проверок во время выполнения Многопоточность Язык Java обеспечивает комплексную поддержку многопоточного программирования Нейтральность к архитектуре Язык Java не привязан к какой-то конкретной архитектуре машины или операционной системы Интерпретируемость Язык Java поддерживает написание межплатформенного кода за счет применения байт-кода Высокая производительность Байт-код Java высоко оптимизирован для повышения скорости выполнения Распределенность Язык Java был спроектирован с учетом распределенной среды Интернета Динамичность Программы на Java несут в себе значительный объем информации о типах времени выполнения, которая используется для проверки и разрешения доступа к объектам во время выполнения Глава 1 . Основы языка Java 39 Объектно- ориентированное программирование В основе языка Java лежит объектно -ориентированное программирование ( ООП ). Объектно-ориентированная методология неотделима от Java , и все программы на Java в той или иной степени являются объектно-ориентированными . Из- за важности ООП для Java полезно в общих чертах понять базовые принци пы ООП , прежде чем приступать к написанию даже самой простой программы на Java. Далее в книге вы увидите, как применять такие концепции на практике. ООП предлагает мощный подход к программированию. Методологии программирования сильно изменились с момента изобретения компьютера , в пер вую очередь для того , чтобы приспособиться к возрастающей сложности программ. Например , когда компьютеры только появились, программирование выглядело как ручное переключение двоичных машинных инструкций на пе редней панели компьютера. Пока программы состояли из нескольких сотен ин струкций , такой подход был приемлемым. С ростом размера программ был изобретен язык ассемблера , чтобы программист мог работать с более крупными и постоянно усложнявшимися программами, применяя символические представления машинных инструкций. По мере того как размер программ продолжил увеличиваться , появились языки высокого уровня , которые предоставили программистам больше инструментов, предназначенных для управления сложно стью. Первым широко распространенным языком был , конечно же, FORTRAN. Хотя FORTRAN стал впечатляющим первым шагом, в те времена его трудно было назвать языком , способствующим созданию ясных и простых для пони мания программ. В 1960 - х годах зародилось структурное программирование . Эта методи программирования ка воплотилась в таких языках, как С и Pascal. За счет ис пользования структурированных языков у программистов впервые появилась возможность довольно легко писать умеренно сложные программы. Структу рированные языки характеризуются поддержкой автономных подпрограмм , локальных переменных , расширенных управляющих конструкций и отсутстви ем зависимости от переходов GOTO . Хотя структурированные языки являются мощным инструментом , даже они достигают своего предела , когда проект становится слишком крупным. Подумайте вот о чем: на каждом этапе развития программирования создавались методики и инструменты , позволяющие программисту справляться с постоянно растущей сложностью. На каждом этапе новый подход брал лучшие элементы предшествующих методик и продвигался вперед . До изобретения ООП многие проекты приближались ( или превышали ) точку, за которой структурированный подход больше не работал . Чтобы помочь программистам пре одолеть эти барьеры, были созданы объектно-ориентированные методики . Объектно -ориентированное программирование позаимствовало лучшие идеи структурного программирования и объединило их с несколькими новыми 40 Java: руководство для начинающих, 9-е издание концепциями. Результатом стал другой способ организации программы. В самом общем смысле программа может быть организована одним из двух способов: вокруг своего кода ( что происходит) или вокруг своих данных ( на что производится воздействие ). В случае применения только методик структурного программирования программы обычно организуются вокруг кода. Такой подход можно рассматривать как “ код, воздействующий на данные ” . Объектно-ориентированные программы работают иначе. Они организованы вокруг данных, и ключевой принцип выглядит как “данные , управляющие доступом к коду ” . В объектно-ориентированном языке определяются данные и подпрограммы , которым разрешено работать с этими данными . Таким образом , тип данных точно определяет, какие операции можно применять к данным. Все объектно-ориентированные языки поддерживают три принципа ООП: инкапсуляцию , полиморфизм и наследование. Разберем каждый из них. Инкапсуляция Инкапсуляция представляет собой механизм программирования , связывающий воедино код и данные , которыми он манипулирует, и защищающий их от внешнего вмешательства и неправильного использования. В объектно-ориен тированном языке код и данные могут быть связаны вместе так, что создается автономный черный ящик , внутри которого находятся все необходимые данные и код. Когда код и данные связаны друг с другом подобным образом , создается объект. Другими словами , объект это способ поддержки инкапсуляции . Внутри объекта код, данные либо то и другое могут быть закрытыми или от крытыми по отношению к этому объекту. Закрытый код или данные известны и доступны только остальным частям объекта , т.е. к ним не могут обращаться ча сти программы , существующие вне объекта. Несмотря на то что открытый код или данные определены внутри объекта , к ним могут получать доступ другие части программы. Как правило, открытые части объекта используются для обеспечения управляемого интерфейса к закрытым элементам объекта. Базовой единицей инкапсуляции Java является класс. Хотя классы будут подробно рассматриваться позже в книге , сейчас будет полезным следующее кра ткое обсуждение. Класс определяет форму объекта . В нем указаны как данные , так и код , который будет работать с этими данными. Спецификация класса применяется в Java для создания объектов. Объекты представляют собой экзем пляры класса. Таким образом , по сути , класс это набор “ чертежей ” , которые указывают, как строить объект. Код и данные , образующие класс, называются членами класса . В частности данные , определенные классом , называют переменными -членами или переменны ми экземпляра. Код, работающий с такими данными, называют методами -чле нами или просто методами. Метод это термин Java для подпрограммы. На тот случай, если вы знакомы с языком С или C + +: то, что программист на Java называет методом, программист на C / C + + называет функцией . — — — Глава 1 . Основы языка Java 41 Полиморфизм Полиморфизм (от греческого “ много форм ” ) представляет собой средство, которое позволяет с помощью одного интерфейса получать доступ к общему классу действий. Конкретное действие определяется природой ситуации. Простым примером полиморфизма может послужить руль автомобиля. Руль (т.е. интер фейс) остается одним и тем же независимо от того, какой тип фактического рулевого механизма установлен в автомобиле зубчатая , реечная передача или гидроусилитель. Таким образом , научившись управлять рулем, можно управлять любым типом автомобиля. Тот же самый принцип применяется и в программировании . Возьмем в качестве примера стек (т.е . список, работающий по принципу “ последним при первым обслужен ” ). У вас может быть программа , требующая стеки трех шел типов. Один стек используется для целых значений, другой для значений с плавающей точкой и третий для символов. Каждый стек реализуется по тому же самому алгоритму, даже если хранящиеся данные различаются. В языке, не являющемся объектно-ориентированным, вам придется создать три разных на бора стековых процедур с отличающимися именами. Но благодаря полиморфизму в Java вы можете указать один общий набор стековых процедур, которые будут функционировать во всех трех конкретных ситуациях. Разобравшись, ка ким образом работать с одним стеком, можно будет иметь дело со всеми тремя. Как правило, концепция полиморфизма часто выражается фразой “ один интерфейс, несколько методов ”. Это означает возможность разработки общего интерфейса для группы связанных действий, что поможет уменьшить сложность, позволив использовать один и тот же интерфейс для указания общего класса действий . Задачей компилятора будет выбор конкретного действия (т.е. метода ) применительно к каждой ситуации. Вам, как программисту, не придется делать такой выбор вручную. Вам понадобится только запомнить и задей - — — — — ствовать общий интерфейс. Наследование Наследование представляет собой процесс , посредством которого один объект приобретает свойства другого объекта . Оно важно , т.к. поддерживает концепцию иерархической классификации. Если вдуматься , то большинство знаний стало доступным за счет иерархической (т.е. нисходящей ) классификации . Например , яблоко сорта “ Ред делишес ” относится к классу “ яблоко ” , который в свою очередь относится к классу “ фрукт ” , а тот — к более крупному классу “ пища ” . То есть класс “ пища ” обладает определенными качествами ( съедобность, питательность и т.д.), которые по логике вещей применимы и к его подклассу “ фрукт ” . В дополнение к таким качествам класс “ фрукт” имеет специфические характеристики ( сочный , сладкий и т. п. ) , которые отличают его от другой пищи. Класс “ яблоко ” определяет качества , которые свойственны яблоку ( растет на деревьях , не является тропическим и т.д. ) . В свою очередь яблоко — 42 Java: руководство для начинающих, 9- е издание сорта “ Ред делишес ” наследует все качества всех предшествующих классов и определяет только те качества , которые делают его уникальным. Без иерархий подобного рода каждый объект должен был бы явно определять все свои характеристики . Благодаря использованию наследования объект должен определить только те качества , которые делают его уникальным в пределах своего класса . Он может наследовать свои общие атрибуты от родителя. Таким образом , именно механизм наследования позволяет одному объекту быть конкретным экземпляром более общего случая . Комплект разработчика на Java Теперь, когда вы ознакомились с теоретическими основами языка Java , пора приступать к написанию программ. Чтобы можно было компилировать и запускать программы , на компьютере должен быть установлен комплект разработчика на Java (Java Development Kit JDK ). На момент написания книги текущим выпуском JDK являлась версия JDK 17 , предназначенная для Java SE 17. ( ,SE означает Standard Edition (стандартный выпуск).) Именно эта версия и рассматривается в книге. Поскольку JDK 17 содержит функции , не поддержива емые в более ранних версиях Java , для компиляции и запуска программ , опи санных в книге , рекомендуется применять версию JDK 17 ( или новее ) . ( Не забывайте, что из-за более быстрого графика выпуска Java выпуски функциональных средств JDK ожидаются с интервалом в пол года. Таким образом , не удивляйтесь появлению JDK с более высоким номером выпуска. ) Но имейте в виду, что в зависимости от имеющейся среды может быть установлена более ранняя версия JDK и тогда новые функциональные средства Java не будут до- — ступными. При установке JDK на свой компьютер помните о том, что для современных версий Java загружать можно Oracle JDK и OpenJDK с открытым кодом. Как правило , вам сначала понадобится найти JDK , который желательно использовать. Скажем , на момент написания книги Oracle JDK был доступен для загрузки на веб-сайте www.oracle.com/java/technologies/downloads/, а версия OpenJDK с открытым кодом на веб-сайте jdk.java.net. Затем необходимо загрузить выбранный JDK и следовать инструкциям по его установке . После установки JDK появится возможность компиляции и запуска программ. Основных программ , входящих в состав JDK, две: javac компилятор Java и java — стандартный интерпретатор Java , который также называется средством запуска приложений. Важно отметить еще один момент: JDK запускается в среде командной строки и использует инструменты командной строки. Ком плект JDK не оконное приложение и не интегрированная среда разработки (integrated development environment IDE). — — — — Глава 1 . Основы языка Java 43 !?@>A8< C M:A?5@B0 ВОПРОС. Выше речь шла о том, что ООП — эффективный способ управления крупными программами. Тем не менее, ситуация выглядит так, что ООП мо жет добавлять значительные накладные расходы к относительно небольшим программам. Учитывая то, что все программы на Java в той или иной мере являются объектно -ориентированными, пострадают ли от этого более мелкие программы? ОТВЕТ. Нет. Как вы увидите, для небольших программ объектно -ориентированные средства Java почти прозрачны. Хотя справедливо утверждать, что язык Java следует строгой объектной модели, у вас есть широкая свобода действий в отношении степени ее применения. Для небольших программ их “ объектная ориентация” едва заметна. По мере разрастания программ вы без труда интегрируете в них дополнительные объектно-ориентированные средства. На заметку! Помимо основных инструментов командной строки, поставляемых в составе JDK, для Java доступно несколько высококачественных IDE-сред вроде NetBeans и Eclipse. Такая IDE-среда может оказаться крайне полезной при разработке и развертывании коммерческих приложений. Обычно при желании IDE-среду можно также использовать для компиляции и запуска программ, приведенных в книге. Однако инструкции по компиляции и запуску программ на Java, представленных в книге, касаются только инструментов командной строки JDK. Причины понять легко. Во-первых, комплект JDK доступен всем читателям. Во-вторых, инструкции по применению инструментов JDK будут одинаковыми для всех читателей. Вдобавок с несложными программами вроде тех, что рассматриваются в этой книге, обычно легче использовать инструменты командной строки JDK. Если вы имеете дело с IDE-средой, тогда следуйте ее инструкциям. Из-за различий между IDE-средами дать общий набор инструкций попросту невозможно. Первая простая программа Начнем с компиляции и запуска простой программы: /* Простая программа на Java. Назовите этот файл Example.java. */ class Example { // Программа на Java начинается с вызова main(). public static void main(String[] args) { System.out.println("Java движет веб-сетью."); } } 44 Java: руководство для начинающих, 9-е издание Далее понадобится выполнить следующие три шага. . 1 Введите исходный код программы. 2. Скомпилируйте программу. 3. Запустите программу. Ввод исходного кода программы Листинги программ , приведенных в книге, доступны для загрузки на вебсайте www.mhprofessional.com и на веб -сайте издательства. Но при желании можете вводить их исходный код вручную. В таком случае необходимо применять текстовый редактор , а не текстовый процессор. Текстовые процессоры обычно хранят вместе с текстом информацию о форматировании , которую компилятор Java не воспринимает. В случае работы на платформе Windows можете использовать Блокнот или любой другой предпочитаемый редактор для кода. В большинстве языков программирования файлу с исходным кодом программы можно назначать произвольное имя. Тем не менее , с Java дело обстоит иначе. Первое , что вы должны знать о Java — имя , которое вы назначаете файлу с исходным кодом, очень важно. В рассматриваемом примере именем файла с исходным кодом должно быть Example ,java и вот причина. Файл с исходным кодом в Java официально называется единицей компиляции. Он представляет собой текстовый файл , который содержит ( помимо прочего ) одно или большее число определений классов. ( Пока будут применяться файлы с исходным кодом , содержащие только один класс. ) Компилятор Java требует, чтобы в имени файла с исходным кодом применялось расширение .java. Взглянув на программу, вы увидите, что именем определенного в ней класса является Example. Это не совпадение. В Java весь код должен находиться внутри класса. По соглашению имя главного класса должно совпадать с именем файла , содержащего программу. Вы также должны удостовериться в том , что прописные буквы в имени файла и в имени класса соответствуют друг другу. Причина связана с чувствительностью к регистру языка Java. В данный момент соглашение о том , что имена файлов должны соответствовать именам классов , может показаться излишне жестким. Тем не менее , такое соглашение упрощает поддержку и организацию ваших программ. Более того, как вы увидите далее в книге , в некоторых случаях оно будет обязательным. Компиляция программы Чтобы скомпилировать программу Example, запустите компилятор javac, указав в командной строке имя файла с исходным кодом: javac Example.java Компилятор javac создает файл по имени Example.class с байт- кодом программы. Вспомните , что байт- код это не исполняемый код и должен — Глава 1 . Основы языка Java 45 выполняться виртуальной машиной Java (JVM ). Таким образом , результатом javac не является код, который можно запускать напрямую. Чтобы действительно запустить программу, придется воспользоваться интерпретатором Java под названием java. Для этого передайте ему в качестве аргу мента командной строки имя Example: java Example Выполнение программы приводит к отображению следующего вывода: Java движет веб-сетью. Когда исходный код Java компилируется, каждый отдельный класс помещается в собственный выходной файл , имеющий имя класса и расширение .class. Вот почему рекомендуется назначать файлу с исходным кодом Java имя , совпадающее с именем класса , который он содержит имя файла с исходным кодом будет совпадать с именем файла .class. При запуске java, как только что было показано , фактически указывается имя класса , который необходимо выполнить. Загрузчик приложений будет автоматически искать файл с таким именем и расширением .class. В случае нахождения файла он выполнит код , содержащийся в указанном классе . Прежде чем двигаться дальше , важно упомянуть, что в JDK 11 появилась возможность запуска некоторых типов простых программ прямо из файла с исходным кодом без явного вызова javac. Такая методика может оказаться полезной в ряде ситуаций ; она описана в приложении В. В настоящей книге предполагается, что вы применяете описанный выше нормальный процесс ком пиляции. — На заметку! Если при попытке компиляции программы не удается отыскать компилятор javac ( и при условии корректной установки JDK ), то может понадобиться указать путь к инструментам командной строки. В Windows, например, это означает, что путь к инструментам командной строки потребуется добавить к путям, определенным в переменной среды PATH. Скажем, если JDK 17 был установлен в каталог Program Files, то путь к инстру ментам командной строки будет выглядеть как С:\Program Files\Java\jdk-17\bin. ( Конечно, вам придется найти путь к Java на своем компьютере, который может отличаться от только что показанного. Также может отличаться конкретная версия JDK.) Способ добавления пути описан в документации по операционной системе и в разных ОС он отличается. - Подробный анализ первого примера программы Несмотря на довольно небольшой размер программы Example.java, она обладает несколькими основными характеристиками , присущими всем программам на Java. Давайте займемся исследованием каждой части программы . 46 Java : руководство для начинающих, 9-е издание Программа начинается с таких строк: /* Простая программа на Java. Назовите этот файл Example.java. */ Строки представляют собой комментарий. Как и большинство других языков программирования, Java позволяет вводить примечания в файл с исходным кодом программы. Компилятор игнорирует содержимое комментариев. На самом деле комментарий описывает или объясняет работу программы любому, кто читает ее исходный код. В данном случае комментарий описывает программу и напоминает, что исходный файл должен называться Example ,java. Конечно, в реальных приложениях комментарии обычно объясняют, как работает какая -то часть программы или что делает конкретная функция. В Java поддерживаются три стиля комментариев. Комментарий в начале программы называется многострочным. Комментарии такого типа должны начинаться с символов / * и заканчиваться символами * / . Все , что находится между этими двумя парами символов , компилятор игнорирует. Как следует из названия, многострочный комментарий может занимать несколько строк. Ниже показана следующая строка кода в программе: class Example { В строке с помощью ключевого слова class определяется новый класс. Как уже упоминалось, класс является базовой единицей инкапсуляции Java. Здесь Example — имя класса . Определение класса начинается с открывающей фигурной скобки ( { ) и заканчивается закрывающей фигурной скобкой ( } ) , а элементы между скобками представляют собой члены класса. В данный момент не слишком беспокойтесь о деталях класса помимо того, что в Java вся активность программы происходит внутри класса. Это одна из причин, по которой все программы на Java ( по крайней мере , слегка ) объектно-ориентированы . Следующая строка в программе содержит однострочный комментарий: // Программа на Java начинается с вызова main(). Вы видите второй тип комментариев, поддерживаемых в Java. Однострочный комментарий начинается с символов / / и простирается до конца строки . Как правило, программисты применяют многострочные комментарии для длинных примечаний, а однострочные для кратких построчных описаний. Вот следующая строка кода: — public static void main (String[] args) { Данная строка начинает метод main ( ) . Как объяснялось в предыдущем ком ментарии, с этой строки программа начнет выполняться . Обычно программа на Java начинает выполнение с вызова main ( ) . Полностью осознать смысл каждой части строки пока невозможно , т.к. нужно хорошо понимать подход Java к 47 Глава 1 . Основы языка Java инкапсуляции. Однако поскольку такая строка кода присутствует в большинстве примеров в первой части книги , давайте кратко рассмотрим каждую часть. Ключевое слово public представляет собой модификатор доступа, который позволяет программисту управлять видимостью членов класса. Когда член клас са предварен ключевым словом public, доступ к нему может быть получен из кода за пределами класса , где он объявлен. ( Противоположностью public является ключевое слово private, которое предотвращает использование члена кодом , определенным вне класса.) В данном случае метод main ( ) должен быть объявлен как public, потому что при запуске программы его потребуется вызывать в коде за пределами класса. Ключевое слово static позволяет вызы вать main ( ) без создания конкретного экземпляра класса. Причина в том , что main ( ) вызывается машиной JVM до создания каких-либо объектов. Ключевое слово void просто сообщает компилятору, что main ( ) не возвращает значение. Как вы увидите, методы также могут возвращать значения. Если все это кажется немного запутанным , не переживайте все концепции будут подробно рассмотрены в последующих главах. Как уже упоминалось, метод main ( ) вызывается при запуске приложения Java. Любая информация, которую вам нужно передать методу, получается переменными , указанными в наборе круглых скобок после имени метода. Такие переменные называются параметрами. Даже когда для метода не требуются параметры, вам все равно понадобится указать пустые круглые скобки. В main() всего один параметр , String[] args, который представляет собой массив экземпляров класса String.(Массивы — это совокупности похожих объектов. ) Объекты типа String хранят строки символов. В данном случае args получает любые аргументы командной строки , присутствующие при выполнении программы. В рассматриваемой программе такая информация не используется , но другие программы, показанные далее в этой книге , будут ее потреблять. Завершается строка символом { , сигнализирующим о начале тела main(). Весь код, содержащийся в методе , будет находиться между открывающей и за- — крывающей фигурными скобками метода . Ниже показана следующая строка кода . Обратите внимание , что она расположена внутри main(). - System.out.println("Java движет веб сетью."); Она выводит на экран строку "Java движет веб-сетью." вместе с символом новой строки. Вывод в действительности осуществляется встроенным методом println ( ) . В данном случае метод println ( ) отображает переданную ему стро ку. В дальнейшем вы увидите, что println ( ) можно применять и для отображения других типов информации. Строка начинается с System ,out. Хотя подробно объяснить это сейчас слишком сложно , следует отметить, что System является предопределенным классом , обеспечивающим доступ к системе , a out вы ходным потоком , подключенным к консоли. Таким образом , System , out представляет собой объект, который инкапсулирует консольный вывод. Тот — 48 Java : руководство для начинающих, 9-е издание — факт, что язык Java использует объект для определения вывода на консоль еще одно свидетельство его объектно-ориентированной природы . Вероятно, вы уже догадались, что в большинстве реальных приложений Java консольный вывод и ввод применяются нечасто. Поскольку большинство современных вычислительных сред являются графическими по своей природе , консольный ввод-вывод используется в основном в простых утилитах, демонстрационных программах и серверном коде. Позже в книге вы узнаете о других способах генерирования вывода с помощью кода Java , а пока мы продолжим применять методы консольного ввода- вывода. Обратите внимание , что оператор println ( ) завершается точкой с запятой. В Java точка с запятой присутствует в конце многих операторов. Как вы увидите, точка с запятой важная часть синтаксиса Java. Первый символ } в программе заканчивает метод main ( ) , а последний сим вол } завершает определение класса Example. Имейте в виду, что язык Java чувствителен к регистру. Игнорирование данного факта может привести к серьезным проблемам . Скажем , если вы случайно наберете Main вместо main или PrintLn вместо println, то предыдущая программа станет некорректной. Более того , хотя компилятор Java будет компилировать классы , не содержащие метод main ( ) , он не сможет их выполнять. Таким образом, если в имени main допущена ошибка , то компилятор все равно скомпилирует программу, но интерпретатор Java сообщит об ошибке, поскольку не сможет найти метод main(). — Обработка синтаксических ошибок Если вы еще этого не сделали , тогда наберите , скомпилируйте и запустите предыдущую программу. Если у вас есть опыт программирования , то вам должно быть известно, что при наборе кода довольно легко случайно ввести какой нибудь фрагмент неправильно. К счастью, в случае ввода некорректного кода компилятор выдаст сообщение о синтаксической ошибке на этапе компиляции программы. Компилятор Java пытается разобраться в вашем исходном коде независимо от того, что было набрано , а потому сообщение об ошибке может не всегда отражать фактическую причину проблемы . Например , если в преды дущей программе пропустить открывающую фигурную скобку после метода main ( ) , тогда компилятор сообщит о следующих двух ошибках: Example.java:8: error: expected public static void main(String[] args) Example.java:11: error: class, interface, enum, or record expected } . Example java : 8 : ошибка : ожидался символ ; public static void main ( String [ ] args ) Example java : 11 : ошибка : ожидался класс , интерфейс . } , перечисление или запись Глава 1 . Основы языка Java 49 Очевидно , что первое сообщение об ошибке не соответствует действительности , потому что пропущена не точка с запятой, а фигурная скобка. Смысл обсуждения в том , что когда ваша программа содержит синтаксическую ошибку, то вам вовсе не обязательно принимать на веру сообщения компилятора . Выдаваемые компилятором сообщения могут вводить в заблуждение . Обнаружение реальной проблемы может требовать в определенной степени “ критического ” отношения к сообщению об ошибке. Вдобавок просмотрите несколько строк кода в программе , которые предшествуют отмеченной строке. Иногда об ошибке не сообщается до тех пор, пока не пройдет несколько строк кода после той , где ошибка действительно произошла . Вторая простая программа Вероятно , никакая другая концепция не является более фундаментальной для языка программирования , нежели присваивание значения переменной . Переменная представляет собой именованную ячейку памяти , которой может быть присвоено значение. Кроме того , значение переменной можно изменять во время выполнения программы , т.е . содержимое переменной не является фиксированным. В показанной далее программе создаются две переменные с именами myVarl и myVar2: /* Демонстрация использования переменных. Назовите этот файл Example2.java. */ class Example2 { public static void main(String[] args) { int myVarl; // объявление переменной < int myVar2; // объявление еще одной переменной Объявить переменные myVarl = 1024; // присваивание переменной myVarl значения 1024 System.out.println("myVarl содержит " + myVarl); myVar2 = Присвоить значение переменной myVarl / 2; System.out.print("myVar2 содержит myVarl / 2: "); System.out.println (myVar2); } } Запустив программу, вы увидите следующий вывод: myVarl содержит 1024 myVar2 содержит myVarl / 2: 512 В программе вводится несколько новых концепций. Первым делом с помощью приведенного ниже оператора объявляется переменная по имени myVarl целочисленного типа: int myVarl; // объявление переменной 50 Java: руководство для начинающих, 9- е издание Каждая переменная в Java должна быть объявлена перед ее использованием . Кроме того , необходимо указать тип значений , которые она может содержать, т.е. то , что называется типом переменной. В данном случае myVarl может содержать целочисленные значения. Для объявления переменной целочисленно го типа в Java перед ее именем ставится ключевое слово int. Таким образом , в предыдущем операторе объявляется переменная по имени myVarl типа int. В следующей строке объявляется вторая переменная, myVar2: int myVar2; // объявление еще одной переменной Обратите внимание, что формат этой строки такой же, как у первой, а отли чается только имя переменной . В общем случае для объявления переменной будет применяться оператор такого вида: тип имя-переменной; - — Здесь B8? задает тип объявляемой переменной , а имя переменной имя переменной. Помимо int в Java поддерживается ряд других типов данных. В показанной далее строке кода переменной myVarl присваивается значение 1024: myVarl = 1024; // присваивание переменной myVarl значения 1024 Операция присваивания в Java обозначается одиночным знаком равенства. Она копирует значение , заданное справа от нее , в переменную , указанную слева. В следующей строке выводится значение myVarl, предваряемое строкой "myVarl содержит ": System.out.printin("myVarl содержит " + myVarl); В этом операторе знак + приводит к тому, что значение myVarl отображается после строки , которая ему предшествует. Такой подход допускает обобщение. С использованием операции + в одном операторе printIn ( ) можно объединять любое желаемое количество элементов. В приведенной далее строке переменной myVar2 присваивается значение myVarl, деленное на 2: myVar2 = myVarl / 2; Значение в myVarl делится на 2 и результат сохраняется в myVar2. Таким образом , после выполнения строки переменная myVar2 будет содержать значение 512. Значение myVarl не изменится. Как и в большинстве других языков программирования , в Java поддерживается полный набор арифметических операций , в том числе перечисленные в табл . 1.2. Глава 1 . Основы языка Java 51 Таблица 1.2. Наиболее распространенные арифметические операции Операция Описание + Сложение Вычитание Умножение / Деление Вот следующие две строки в программе: System.out.print("myVar2 содержит myVarl / 2: "); System.out.printIn(myVar2); Здесь происходят два новых действия . Во- первых , с применением встроенного метода print ( ) отображается строка "myVar2 содержит myVarl / 2: " без последующего символа новой строки . Таким образом, очередной вывод начнется в той же строке . Метод print ( ) аналогичен printIn ( ) , но он не выводит символ новой строки после каждого вызова. Во -вторых, обратите внима ние в вызове println ( ) , что переменная myVar2 используется сама по себе . И print ( ) , и println ( ) могут применяться для вывода значений любого встроенного типа Java. Прежде чем двигаться дальше, важно отметить, что в одном операторе объявления можно объявлять две или более переменных, разделяя их имена запя тыми. Например, myVarl и myVar2 можно было бы объявить так: int myVarl, myVar2; // объявление обеих переменных в одном операторе Другие типы данных В предыдущей программе использовалась переменная типа int. Тем не менее , переменная типа int способна содержать только целые числа. Таким образом , когда требуется дробная часть, применять ее нельзя. Скажем , переменная int может содержать значение 18, но не 18.3. К счастью , int — лишь один из нескольких типов данных, определенных в Java. Для хранения чисел с дробной частью в Java определены два типа с плавающей точкой, float и double, которые представляют значения с одинарной и двойной точностью соответственно. Тип double используется чаще типа float. Для объявления переменной типа double применяется оператор следующего вида: double х; — Здесь х имя переменной типа double. Поскольку х относится к типу с плавающей точкой, она может содержать значения вроде 122.23, 0.034 или -19.0 . Чтобы лучше понимать отличия между int и double, рассмотрим показан- ную далее программу: 52 Java: руководство для начинающих, 9-е издание /* Программа иллюстрирует отличия между int и double. Назовите этот файл Ехашр1еЗ.java. */ class Example3 { public static void main(String[] args) { // объявление целочисленной переменной int v; double x; // объявление переменной с плавающей точкой v = 10; // присваивание переменной v значения 10 x = 10.0; // присваивание переменной х значения 10.0 System.out.println("Первоначальное значение v: " + v); System.out.println ("Первоначальное значение x: " + x); System.out.println(); // вывод пустой строки Вывести пустую строку // Поделить обе переменные на 4. v = v / 4; х = х / 4; System.out.println("v после деления ; " + v); System.out.println("х после деления: " + x); } } Вот вывод, полученный из программы: Первоначальное значение v: 10 Первоначальное значение х: 10.0 v после деления: 2 х после деления: 2.5 < Дробная часть теряется Дробная часть сохраняется Как видите, при делении переменной v типа int на 4 выполняется целочисленное деление, в результате которого получается значение 2 , т.е. дробная часть теряется. Однако при делении переменной х типа double на 4 дробная часть предохраняется и отображается надлежащий ответ. В программе присутствует еще одно нововведение. Для вывода пустой строки нужно просто вызвать println ( ) без аргументов. !?@>A8< C M:A?5@B0 ВОПРОС . По какой причине в Java для хранения целочисленныхзначений и значе ний с плавающейточкой используются разныетипыданных? Выражаясь иначе: почему все числовые значения не относятся к одному и тому же типу? ОТВЕТ. Язык Java предлагает разные типы данных, чтобы обеспечить возможность написания эффективных программ. Например, целочисленные вычисления выполняются быстрее вычислений с плавающей точкой. Таким образом , если значения с дробной частью вас не интересуют, то не нужны и накладные расходы , ассоциированные с типом float или double . Кроме того, объем памя ти, требующийся для значений одного типа данных, может быть меньше , чем для значений другого типа данных. Предоставляя различные типы, язык Java позволяет наилучшим образом потреблять системные ресурсы. Наконец, некоторые алгоритмы требуют или , по крайней мере, получают выигрыш от применения данных специфического типа. Короче говоря, язык Java предлагает ряд встроенных типов, обеспечивающих наивысшую гибкость. Глава 1 . Основы языка Java 53 Преобразование галлонов в литры Упражнение 1.1 Хотя приведенные выше примеры программ демонстрировали важные возможности языка Java , они не особенно полезны . Несмотря на то что знаний Java у вас пока недостаточно , все же есть возможность прямо сейчас написать практическую программу. В текущем проекте будет создана программа , которая преобразует галлоны в литры. В программе объявляются две переменные типа double. Одна будет хранить количество гал лонов , а вторая количество литров после преобразования . В галлоне содержится 3, 7854 литра. Таким образом , для преобразования галлонов в литры число галлонов понадобится умножить на 3, 7854. Программа отображает как количество галлонов, так и эквивалентное количество литров. 1. Создайте файл по имени GalToLit.java. 2. Поместите в файл GalToLit.java следующий код программы: GalToLit.java — /* Упражнение 1.1. Программа для преобразования галлонов в литры . Назовите этот файл GalToLit.java. */ class GalToLit { public static void main(String[] args) { double gallons; // хранит количество галлонов // хранит результат преобразования в литры double liters; gallons = 10; // начать с 10 галлонов liters = gallons * 3.7854; // преобразование в литры System.out.println(gallons + " галлонов соответствует " + liters + " литрам."); } } 3. Скомпилируйте программу, используя такую команду: javac GalToLit.java 4. Запустите программу с применением следующей команды: java GalToLit Вы увидите показанный ниже вывод: 10.0 галлонов соответствует 37.854 литрам. 5. В том виде, как есть, программа преобразует 10 галлонов в литры. Тем не менее , изменив значение , присваиваемое gallons, вы можете заставить программу преобразовывать другое число галлонов в эквивалентное ему количество литров. 54 Java: руководство для начинающих, 9- е издание Два управляющих оператора Операторы внутри метода выполняются последовательно сверху вниз. Однако такой поток можно изменить с использованием разнообразных управляющих операторов, поддерживаемых в Java . Хотя управляющие операторы подробно рассматриваются позже , здесь кратко представлены два из них , поскольку они будут применяться в примерах программ. Оператор if — — можно выборочно выполнять С помощью условного оператора Java if часть программы. Оператор if в Java работает во многом аналогично условному оператору в любом другом языке. Он определяет поток выполнения на основе того , является некоторое условие истинным или ложным. Ниже показана его простейшая форма: if { условие ) оператор; — Здесь условие это булевское выражение. ( Булевским является такое выра жение, результатом вычисления которого будет либо true ( истина ) , либо false (ложь ). ) Если условие истинно , тогда оператор выполняется. Если условие ложно, то оператор пропускается. Вот пример: if(10 < 11) System.out.println("10 меньше 11"); Так как значение 10 меньше 11, условное выражение дает true println ( ) выполнится. Тем не менее , взгляните на такой пример: и вызов if(10 < 9) System.out.println("Это не отобразится"); В данном случае значение 10 не меньше 9. В результате вызов println ( ) не произойдет. В Java определен полный набор операций отношения , которые можно использовать в условном выражении. Они показаны в табл . 1.3. Таблица 1.3. Операции отношения в языке Java Операция Описание < Меньше <= > >= Меньше или равно Больше Больше или равно Равно Не равно Обратите внимание , что проверка на равенство обозначается двумя знаками равенства. Глава 1 . Основы языка Java 55 Ниже приведена программа , в которой иллюстрируется работа оператора if : /* Демонстрация использования оператора if. Назовите этот файл IfDemo.java . */ class IfDemo { public static void main(String[] args) { int a, b, c; a = 2; b = 3; if(a < b) System.out.println("Значение а меньше значения b"); // Следующий оператор ничего не отобразит. if(а == b) System.out. println("Это вы не увидите"); System.out.println(); с = а - b; // Переменная с содержит -1 System.out.println("Переменная с содержит -1"); if(с >= 0) System.out.println("Значение с неотрицательное"); if(с < 0) System.out.println("Значение с отрицательное"); System.out.println(); с = b } } - а; // Теперь переменная с содержит 1 System.out.println("Переменная с содержит 1"); if(с >= 0) System.out.println("Значение с неотрицательное"); if(с < 0) System.out.println("Значение с отрицательное"); Вот вывод, генерируемый программой: Значение а меньше значения b Переменная с содержит -1 Значение с отрицательное Переменная с содержит 1 Значение с неотрицательное С программой связан еще один момент. В следующей строке посредством списка с разделителем- запятой объявляются три переменные - а , b и с: int а, Ъ, с; Как упоминалось ранее , когда требуются две или более переменных одного типа , их можно объявить в одном операторе , просто разделяя имена переменных запятыми . Цикл for За счет создания цикла можно многократно выполнять кодовую последо вательность. Циклы применяются всякий раз , когда нужно выполнить повто ряющуюся задачу, потому что реализовать их гораздо легче , чем многократно 56 Java: руководство для начинающих, 9-е издание записывать одну и ту же последовательность операторов. Язык Java поддерживает мощный набор конструкций циклов. Здесь мы рассмотрим цикл for , который имеет следующий вид: for { инициализация ; условие; итерация ) оператор; В своей самой распространенной форме часть инициализация цикла уста навливает переменную управления циклом в начальное значение . Часть условие представляет собой булевское выражение, которое проверяет переменную управления циклом. Если результат проверки оказывается истинным , тогда оператор выполняется, а цикл for продолжает работу. При ложном результате проверки цикл завершается. Выражение итерация определяет, каким образом переменная управления циклом изменяется на каждой итерации цикла . Далее приведена короткая программа , иллюстрирующая работу цикла for: /* Демонстрация использования цикла for. Назовите этот файл ForDemo.java. */ class ForDemo { public static void main(String[] args) { int count; for(count = 0; count < 5; count = count+1) Этот цикл выполняется пять раз System.out.println("Значение count: " + count); System.out.printIn("Готово!"); } } Вот вывод, генерируемый программой: Значение Значение Значение Значение Значение Готово! count: count: count: count: count: 0 1 2 3 4 В приведенном примере count является переменной управления циклом. Она инициализируется нулем в части инициализация цикла for. В начале каждой итерации ( включая первую ) выполняется условная проверка count < 5. Если результат проверки оказывается истинным , тогда выполняется оператор println ( ) , после чего выполняется часть итерация цикла, которая увеличивает значение count на 1. Процесс продолжается до тех пор , пока условная проверка не станет ложной . Интересно отметить, что в профессионально написанных программах на Java вы практически никогда не встретите часть итерация цикла , написанную так, как в предыдущей программе. То есть вы будете редко видеть операторы вроде следующего: count = count + 1; Глава 1 . Основы языка Java 57 Причина в том , что в Java есть специальная операция инкремента, обладающая большей эффективностью , которая обозначается посредством + + (т.е. два знака “ плюс ” подряд). Операция инкремента увеличивает свой операнд на еди ницу. С помощью операции инкремента предыдущее выражение можно запи сать в показанной ниже форме: count++; Таким образом , цикл for в предыдущей программе обычно будет записываться в следующем виде: for(count = 0; count < 5; count++) Можете опробовать его. Вы заметите, что цикл выполняется в точности, как было ранее . В Java также предлагается операция декремента , обозначаемая как . Она уменьшает свой операнд на единицу. — Создание блоков кода Язык Java позволяет группировать два или более операторов в блоки кода , также называемые кодовыми блоками. Для этого операторы помещаются между открывающей и закрывающей фигурными скобками. После того , как блок кода создан , он становится логической единицей , которую можно применять в лю бом месте, где разрешено использовать одиночный оператор. Скажем , блок может служить телом для операторов if и for . Возьмем следующий оператор if : if(w < h ) { v w * h; w = 0; * « Начало блока Конец блока * Если w меньше h , тогда выполнятся оба оператора внутри блока. Таким образом , два оператора внутри блока образуют логическую единицу, где первый оператор не может быть выполнен без выполнения второго. Ключевой момент здесь в том , что всякий раз , когда нужно логически связать два или большее количество операторов, вы создаете блок. Блоки кода позволяют реализовывать многие алгоритмы более ясно и эффективно. В приведенной ниже программе блок кода применяется для предотвращения деления на ноль: } /* Демонстрация использования блока кода. Назовите этот файл BlockDemo.java. */ class BlockDemo { public static void main(String[] args) { double i, j, d; i = 5; j = 10; 58 Java : руководство для начинающих, 9-е издание // Целью оператора if является блок , if(i != 0) { System.out.println("Значение i не равно нулю."); d = j / i; System.out.println("Результат j / i равен " + d); — } — Телом оператора if является целый блок } } Вот вывод, генерируемый программой: Значение i не равно нулю. Результат j / i равен 2.0 В данном случае целью оператора if является блок кода , а не одиночный оператор. Если условие if дает true ( как здесь) , то будут выполнены три оператора внутри блока. Попробуйте установить i в ноль и взгляните на результат. Вы обнаружите , что целый блок будет пропущен. !?@>A8< C M:A?5@B0 ВОПРОС. Приводит ли использование блока кода к снижению эффективности программы во время выполнения? Другими словами , действительно ли предпринимаются какие -то добавочные действия , когда встречаются символы { и } ? ОТВЕТ. Нет. Никакие накладные расходы с блоками кода не связаны. На самом деле благодаря тому, что они упрощают написание кода определенных ал горитмов, их применение обычно увеличивает скорость и эффективность. Кроме того, символы { и } существуют только в исходном коде программы и с ними не связаны какие-либо дополнительные действия. Позже в книге вы увидите, что блоки кода обладают дополнительными ха рактеристиками и сценариями использования. Однако главная причина их существования — создание логически неразрывных единиц кода. Точки с запятой и размещение операторов Точка с запятой в Java является разделителем и часто применяется для завершения оператора. По сути , с помощью точки с запятой помечается конец одной логической сущности. Как вам уже известно, блок представляет собой набор логически связанных операторов, заключенных в пару открывающей и закрывающей фигурных ско бок. Блок не заканчивается точкой с запятой. Взамен конец блока обозначается закрывающей фигурной скобкой . Конец строки Java не считается окончанием оператора , а потому не имеет значения , где он находится в строке кода. Скажем, следующие строки: Глава 1 . Основы языка Java 59 у = у + 1; System.out.println(х + " " + у); эквивалентны одной строке: х = у; у = у + 1; System.out.println(х + " " + у); Кроме того, индивидуальные элементы оператора могут размещаться в раз- ных строках. Например , совершенно допустим оператор следующего вида: System.out.printIn("Длинная строка вывода " + х + у + z + " продолжение вывода"); Разбиение длинных строк подобным способом часто имеет целью повышение читабельности программы , а также помогает предотвратить автоматический перенос слишком длинных строк. Практика отступов Вы наверняка заметили в предыдущих примерах, что некоторые операторы записаны с отступами. Java — язык свободной формы , поэтому не имеет значения, каким образом операторы размещаются друг относительно друга в строке. Тем не менее, с годами был разработан общепринятый стиль отступов, который позволяет создавать удобные для восприятия программы. Такой стиль соблюдается в книге и вам рекомендуется поступать аналогично. Стиль предусматривает отступ на один уровень после каждой открывающей скобки и возврат назад на один уровень после каждой закрывающей скобки. Позже будет показано , что некоторым операторам требуются дополнительные отступы. Упражнение 1.2 Усовершенствованная версия программы для преобразования галлонов в литры Используя цикл for, оператор if и блоки кода , можно создать усовершенствованную версию программы пре образования галлонов в литры , которая была разработана в упражнении 1.1. Новая версия будет выводить таблицу преобразований, начиная с 1 и заканчи вая 100 галлонами. После каждых 10 галлонов будет выводиться пустая строка. Это достигается за счет применения переменной counter, подсчитывающей количество выведенных строк. Обратите особое внимание на ее использование . Создайте файл по имени GalToLitTable.java. Поместите в GalToLitTable.java следующий код программы: GalToLitTable.java /* Упражнение 1.2. Эта программа отображает таблицу преобразований галлонов в литры . Назовите этот файл GalToLitTable.java. */ 60 Java: руководство для начинающих, 9-е издание class GalToLitTable { public static void main(String[] args) { double gallons, liters; int counter; Установить счетчик counter = 0; строк сначала в ноль for(gallons = 1; gallons <= 100; gallons++) { liters = gallons * 3.7854; // преобразование в литры System.out.println(gallons + " галлонов соответствует " + liters + " литрам."); Увеличивать счетчик строк на каждой итерации counter++; // После каждой 10-й строки вывести пустую строку , if(counter == 10) { Если значение счетчика строк равно 10, вывести пустую строку System.out.println(); counter = 0; // сброс счетчика строк } — } } } Скомпилируйте программу, используя следующую команду: javac GalToLitTable.java Запустите программу с применением такой команды: java GalToLitTable Ниже показана часть вывода , генерируемого программой: I.0 галлонов соответствует 3.7854 литрам. 2.0 галлонов соответствует 7.5708 литрам. 3.0 галлонов соответствует 11.356200000000001 литрам. 4.0 галлонов соответствует 15.1416 литрам. 5.0 галлонов соответствует 18.927 литрам. 6.0 галлонов соответствует 22.712400000000002 литрам. 7.0 галлонов соответствует 26.4978 литрам. 8.0 галлонов соответствует 30.2832 литрам. 9.0 галлонов соответствует 34.0686 литрам. 10.0 галлонов соответствует 37.854 литрам. II.0 галлонов 12.0 галлонов 13.0 галлонов 14.0 галлонов 15.0 галлонов 16.0 галлонов 17.0 галлонов 18.0 галлонов 19.0 галлонов 20.0 галлонов соответствует соответствует соответствует соответствует соответствует соответствует соответствует соответствует соответствует соответствует 41.6394 литрам. 45.424800000000005 литрам. 49.2102 литрам. 52.9956 литрам. 56.781 литрам. 60.5664 литрам. 64.3518 литрам. 68.1372 литрам. 71.9226 литрам. 75.708 литрам. 21.0 галлонов соответствует 79.49340000000001 литрам. 22.0 галлонов соответствует 83.2788 литрам. 23.0 галлонов соответствует 87.0642 литрам. Глава 1 . Основы языка Java 24.0 25.0 26.0 27.0 28.0 29.0 30.0 61 галлонов соответствует 90.84960000000001 литрам. галлонов соответствует 94.635 литрам. галлонов соответствует 98.4204 литрам. галлонов соответствует 102.2058 литрам. галлонов соответствует 105.9912 литрам. галлонов соответствует 109.7766 литрам. галлонов соответствует 113.562 литрам. Ключевые слова Java В настоящее время в языке Java определено 67 ключевых слов , которые перечислены в табл . 1.4. В сочетании с синтаксисом операций и разделителей они образуют определение языка Java . Как правило , ключевые слова нельзя использовать в качестве имен переменных , классов или методов. Однако 16 ключевых слов зависят от контекста , т. е . являются ключевыми словами только в случае применения с функциональным средством , к которому относятся . Они поддерживают средства , добавленные в Java за последние несколько лет. Десять ключевых слов относятся к модулям : exports , module , open , opens , provides , requires , to, transitive , uses и with. Записи объявляются с по мощью record; для запечатанных классов и интерфейсов используются sealed, non -sealed и permits; ключевое слово yield применяется расширенным оператором switch; var поддерживает выведение типа локальной переменной . По причине зависимости от контекста их добавление не повлияло на существую щие программы . Кроме того , начиная с версии JDK 9 , подчеркивание само по себе считается ключевым словом , что предотвращает его использование в каче стве имени чего-либо в программе . Начиная с версии JDK 17 , ключевое слово s t r i c t f p больше не имеет никакого эффекта и не требуется , но по - прежнему присутствует в языке . Таблица 1.4. Ключевые слова Java abstract catch assert char boolean class break const byte continue case default do double else enum exports extends final finally float for goto if implements import instanceof int interface long module native new non-sealed open opens package permits private protected provides public record requires return sealed short static strictfp super switch synchronized this throw throws to transient transitive try uses void volatile while with yield var 62 Java: руководство для начинающих, 9-е издание Ключевые слова const и goto зарезервированы , но не применяются. На ран них этапах развития Java несколько других ключевых слов были зарезервированы для возможного использования в будущем . Тем не менее, в текущей спецификации Java определены только ключевые слова , представленные в табл . 1.4. Помимо ключевых слов в Java зарезервированы еще три имени , которые были частью Java с самого начала: true, false и null. Они представляют собой значения, определенные в языке, и их нельзя применять для именования пере менных, классов и т.д. Идентификаторы в Java В Java идентификатор по существу представляет собой имя , назначенное методу, переменной или любому другому определяемому пользователем элементу. Идентификаторы могут иметь длину от одного до нескольких символов. Имена переменных могут начинаться с любой буквы алфавита , символа подчеркива ния или знака доллара. ( Символ $ не предназначен для общего использования.) Далее может следовать буква , цифра , знак доллара или знак подчеркивания . Подчеркивание можно применять для повышения удобочитаемости имени переменной , как в line count. Прописные и строчные буквы различаются , т.е. myvar и MyVar отдельные имена . Ниже приведены примеры допустимых идентификаторов. — Test up _ х top У2 MaxLoad my_var sample23 He забывайте, что идентификатор нельзя начинать с цифры. Таким образом , 12 х не может служить идентификатором. Как правило , ключевые слова Java не разрешено использовать в качестве имен идентификаторов. Также не следует применять для идентификатора имя какого-либо стандартного метода , скажем, printIn. Помимо указанных двух ограничений хорошая практика программирования предписывает использова ние идентификаторов с именами , отражающими их предназначение. Библиотеки классов Java В примерах программ , приведенных в главе , применялись два встроен ных метода Java: println ( ) и print ( ) . Доступ к ним осуществляется через System.out. Здесь System это предопределенный класс Java , который авто матически включается в программы. В более широком плане среда Java опирается на несколько встроенных библиотек классов, которые содержат множество встроенных методов , обеспечивающих поддержку таких средств , как вводвывод, обработка строк , работа с сетью и графика. Стандартные классы также реализуют поддержку графического пользовательского интерфейса. Таким — Глава 1 . Основы языка Java 63 образом , Java как совокупность представляет собой сочетание самого языка Java с его стандартными классами. Позже вы увидите , что библиотеки классов обеспечивают большую часть функциональности, связанной с Java . Действительно, частью становления вас как программиста на Java является обучение использованию стандартных классов Java. Описание различных классов и методов стандартной библиотеки можно встретить повсюду в книге . Однако рекомендуется заняться и самостоятельным исследованием библиотеки Java . Вопросы и упражнения для самопроверки 1. Что такое байт- код и почему он настолько важен при написании Интернетприложений на Java ? 2. Каковы три главных принципа ООП ? 3. С чего начинается выполнение программ на Java ? 4. Что такое переменная ? 5. Какие из перечисленных ниже имен переменных не являются допустимыми? а) count б) $ count в ) count 27 г) 67 count 6. Как создаются однострочные и многострочные комментарии? 7. Как выглядит общая форма оператора i f ? Как выглядит общая форма цикла for? 8. Как создать блок кода ? 9. Сила тяжести на Луне составляет около 17% земной. Напишите программу для расчета вашего фактического веса на Луне. 10. Адаптируйте программу из упражнения 1.2 так, чтобы она выводила таблицу преобразования дюймов в метры. Отображайте преобразования длины до 12 футов через каждый дюйм. Через каждые 12 дюймов выводите пустую строку. ( Один метр равен примерно 39, 37 дюйма.) 11. Если вы сделаете опечатку при вводе программы , тогда какого вида сообщение об ошибке получите? 12. Имеет ли значение , в каком месте строки размещается оператор? s .••••..у у,,* • • . • * s . V s* S '. vI , * I I . ; .'• Ss:•:•' »» 4 * I \ I s» ' II I,I I , II .' II . 4' I I I oiXfS • 9 .• •ggf: * ••• Глава 2 Введение в типы данных и операции 66 Java: руководство для начинающих, 9-е издание В этой главе• • • z Примитивные типы Java z Использование литералов z Инициализация переменных z Правила области видимости переменных внутри метода z Применение арифметических операций z Использование операций отношения и логических операций z Операции присваивания z Применение сокращенных операций присваивания z Преобразование типов при присваивании z Приведение несовместимых типов z Преобразование типов в выражениях в основе любого языка программирования лежат типы данных и операции, и Java не является исключением . Типы данных и операции определяют границы применимости языка , а также виды задач , которые можно решать с его помощью. К счастью , в языке Java поддерживается широкий спектр типов данных и операций , что делает его подходящим для написания программ любых категорий . Типы данных и операторы - обширная тема. Глава начинается с исследова ния основных типов данных Java и наиболее часто используемых операций. Затем более подробно рассматриваются переменные и выражения. Важность типов данных Типы данных особенно важны в Java , поскольку он является строго типи зированным языком , т.е. все операции в нем проверяются компилятором на совместимость типов. Код с недопустимыми операциями компилироваться не будет. Таким образом , строгая проверка типов помогает предотвращать ошибки и повышает надежность. Чтобы сделать возможной строгую проверку типов, все переменные , выражения и значения имеют тип. Например, не существует понятия переменной “ без типа ” . Кроме того , тип значения определяет, какие операции над ним разрешены. Операция , разрешенная для одного типа , может быть запрещена для другого. Глава 2. Введение в типы данных и операции 67 Примитивные типы Java Встроенные типы данных Java подразделяются на две основные категории: объектно-ориентированные и не являющиеся таковыми. Объектно-ориентированные типы определяются классами, а классы будут обсуждаться позже. Однако в основе Java лежат восемь примитивных (также называемых элементарными или простыми) типов данных, которые перечислены в табл . 2.1. Термин прими тивные применяется для обозначения того, что такие типы это не объекты в объектно-ориентированном смысле , а обычные двоичные значения. Примитивные типы не реализованы в виде объектов из соображений эффективности. Все остальные типы данных Java созданы из примитивных типов. — Таблица 2.1 . Встроенные примитивные типы данных Java Тип Описание boolean byte char double float int long short Представляет истинные и ложные значения 8 -битное целое число Символ Число с плавающей точкой двойной точности Число с плавающей точкой одинарной точности Целое число Длинное целое число Короткое целое число Для каждого примитивного типа Java четко определяет диапазон и поведение, которые должны поддерживаться всеми реализациями виртуальных машин Java. Из- за требования переносимости Java компромисс в таком отношении отсутствует. Например, тип int одинаков во всех средах выполнения. В итоге программы могут быть полностью переносимыми. Нет необходимости перепи сывать код под конкретную платформу. Хотя строгое установление диапазонов для примитивных типов в некоторых средах может привести к небольшому снижению производительности, оно необходимо для обеспечения переносимости . Целые числа В Java определены четыре целочисленных типа: byte, short, int и long. Все они описаны в табл . 2.2. Как видно в табл . 2.2, все целочисленные типы представляют положительные и отрицательные значения со знаком. Поддержка только положительных целых чисел без знака в Java отсутствует. Во многих других языках программирования поддерживаются как целые числа со знаком, так и целые числа без знака , но разработчики Java решили , что целые числа без знака не нужны. 68 Java : руководство для начинающих, 9-е издание Таблица 2.2. Ширина в битах и диапазоны целочисленных типов Имя Ширина в битах Диапазон byte 8 16 32 64 От -128 до 127 От -32 768 до 32 767 От -2 147 483 648 до 2 147 483 647 От -9 223 372 036 854 775 808 до 9 223 372 036 854 775 807 short int long На заметку! Формально исполняющая система Java может использовать любой размер для хранения значений примитивного типа. Тем не менее, во всех случаях типы должны действовать так, как указано. Самым распространенным целочисленным типом можно считать int . Пере менные типа i n t обычно применяются для управления циклами , для индексации массивов и для выполнения действий из универсальной целочисленной математики . Если требуется целое число , диапазон которого шире , чем у i n t , тогда нуж но использовать long. Скажем , вот программа , которая вычисляет количество кубических дюймов , содержащихся в кубической миле: /* Вычисляет количество кубических дюймов в кубической миле. */ class Inches { public static void main(String[] args) { long ci; long im; im = 5280 * 12; Cl im * im * im; System.out.println("В кубической миле содержится " + ci + " кубических дюймов."); } } Ниже показан вывод, генерируемый программой: В кубической миле содержится 254358061056000 кубических дюймов. Совершенно очевидно , что результат не уместился бы в переменную типа i n t . Наименьшим целочисленным типом является byte . Переменные типа byte особенно полезны при работе с низкоуровневыми двоичными данными , которые могут быть несовместимыми напрямую с другими встроенными типа ми Java. Тип short позволяет хранить короткие целые числа. Переменные типа short подходят, когда нет необходимости в более широком диапазоне , который предлагает тип int. Глава 2 . Введение в типы данных и операции 69 !?@>A8< C M:A?5@B0 ВОПРОС. Ранее упоминалось, что есть четыре целочисленных типа: int, short, long и byte. Но говорят, что тип char в Java тоже можно отнести к категории целочисленных типов. Чем это объясняется? ОТВЕТ. В формальной спецификации Java определена категория , называе мая интегральными типами , куда входят byte, short, int, long и char. Называние интегральные обусловлено тем , что все они предназначены для хранения целых двоичных чисел . Однако первые четыре типа представляют числовые целые величины , тогда как тип char - символы. Следовательно , основные способы использования типа char и других целочисленных типов фундаментально разные. Из-за таких отличий тип char трактуется в книге как отдельный. Типы с плавающей точкой Как объяснялось в главе 1 , типы с плавающей точкой способны представлять числа с дробной частью. Существуют два типа с плавающей точкой, float и double, которые представляют числа с одинарной и двойной точностью соответственно. Тип float имеет ширину 32 бита , а тип double 64 бита . Тип double применяется чаще типа float, и многие математические функции в библиотеке классов Java работают со значениями типа double. Например, метод sqrt ( ) , определенный в стандартном классе Math, возвращает значение типа double, которое представляет собой квадратный корень передаваемого методу аргумента типа double. Ниже в примере sqrt ( ) используется для вычисления длины гипотенузы по длинам двух катетов: — /* Использование теоремы Пифагора для нахождения длины гипотенузы по длинам двух катетов. */ class Hypot { public static void main(String[] args) { double x, y, z; x = 3; У = 4; Г z = Math.sqrt(х х + у у ); * * Обратите внимание на вызов метода sqrt(). Он предварен именем класса, членом которого он является. System.out.println ("Длина гипотенузы : " +z); } } Вот вывод, генерируемый программой: Длина гипотенузы : 5.0 70 Java: руководство для начинающих, 9- е издание Еще одно замечание по поводу предыдущего примера: как уже упоминалось, метод sqrt ( ) является членом стандартного класса Math. Обратите внимание на вызов метода sqrt ( ) . Ему предшествует имя Math , что похоже на то , как System . out предшествует println ( ) . Хотя не все стандартные методы вызыва ются с указанием имени класса , для некоторых поступать так удобно. Символы В Java символы не являются 8-битными величинами , как во многих других языках программирования ; взамен применяется Unicode. Кодировка Unicode определяет полный международный набор символов, с помощью которого можно представить все символы , встречающиеся во всех естественных язы ках. Тип char в Java это 16- битный тип без знака с диапазоном значений от О до 65 535. Стандартный 8- битный набор символов ASCII является подмножеством Unicode и умещается в диапазон от 0 до 127. Таким образом , символы ASCII по- прежнему являются допустимыми символами Java. Чтобы присвоить значение символьной переменной, символ можно заключить в одинарные кавычки. Например, ниже переменной ch присваивается буква X: — char ch; ch = 1 X * ; Значение типа char выводится с использованием оператора println ( ) . Например, следующая строка выводит значение переменной ch: System.out.println ("Значение ch: " + ch); Поскольку char является 16- битным типом без знака , с переменной типа char можно выполнять различные арифметические операции . Взгляните на показанную далее программу: // С символьными переменными можно обращаться как с целочисленными. class CharArithDemo { public static void main(String[] args) { char ch; ch = 'X'; System.out.println("ch содержит " + ch); ch++; Переменную типа char можно инкрементировать // инкрементирование ch System.out.println("ch теперь содержит " + ch); ch = 90; // присваивание ch значения Z System.out.println("ch теперь содержит " + ch); ^ } } Вот вывод, генерируемый программой: ch содержит X ch теперь содержит Y ch теперь содержит Z Переменной типа char можно присвоить целочисленное значение Глава 2. Введение в типы данных и операции 71 В программе переменной ch сначала присваивается буква X. Затем значение ch инкрементируется , приводя к тому, что ch станет содержать Y, т. е . следую щий символ в последовательности ASCII ( и Unicode ) . Далее ch присваивается значение 90 , которое в ASCII ( и Unicode ) соответствует букве Z. С учетом того , что набор символов ASCII занимает первые 127 позиций в наборе символов Unicode , все “ старые трюки ” , которые вы могли применять в отношении сим волов в других языках , будут работать и в Java. !?@>A8< C M:A?5@B0 ВОПРОС. Почему в Java используется Unicode ? ОТВЕТ. Язык Java разрабатывался для применения во всем мире . Таким обра зом , необходимо было использовать набор символов , способный представлять все языки мира. Стандартный набор символов Unicode был разработан специально для этой цели . Разумеется , применение Unicode несколько не эффективно для таких языков , как английский , немецкий , испанский или французский , символы которых могут легко умещаться в пределах 8 бит, но такова цена , которую приходится платить за глобальную переносимость . Тип boolean Тип boolean представляет истинные и ложные значения , которые определяются в Java с помощью зарезервированных слов true и f a l s e . Таким образом , переменная или выражение типа boolean будет иметь одно из указанных двух значений . В представленной ниже программе демонстрируется применение типа boolean: // Демонстрация использования значений типа boolean , class BoolDemo { public static void main(String[] args ) { boolean b; b = false; System.out.println("b равно " + b); b = true; System.out.println("b равно " + b); // Значение boolean может управлять оператором if. if(b) System.out.println("Данная строка кода выполняется."); b = false; if(b) System.out.println("Данная строка кода не выполняется."); // Результатом операции отношения является значение boolean. System.out.println("10 > 9 равно " + (10 > 9)); } } 72 Java: руководство для начинающих, 9-е издание Программа генерирует следующий вывод: Ь равно false b равно true Данная строка кода выполняется. 10 > 9 равно true В этой программе необходимо отметить три интересных момента . Во первых, как видите, когда println ( ) выводит значение boolean, отображается true или false. Во-вторых, самого по себе значения переменной boolean достаточно для управления оператором if, т.е. нет необходимости записывать оператор if в таком виде: if( b == true) ... В-третьих, результатом операции отношения , подобной <, является значение boolean. Именно потому выражение 10>9 дает значение true. Кроме того, дополнительный набор скобок вокруг 10>9 нужен из- за того , что приоритет операции + выше приоритета операции >. Упражнение 2.1| Далеко ли до места вспышки молнии? В данном проекте создается программа , которая вычисляет расстояние ( в метрах ) между наблюдателем и местом вспышки молнии. Звук распространяется по воздуху со скоростью примерно 335 метров в секунду. Таким образом , зная интервал между моментом , когда появилась вспышка молнии, и моментом , когда был услышан звук , можно рассчитать расстояние до места вспышки. Предположим , что временной интервал составляет 7, 2 секунды. 1. Создайте файл по имени Sound ,java. 2. Имейте в виду, что при расчете расстояния должны использоваться значения с плавающей точкой. Почему? Да потому что временной интервал 7, 2 имеет дробную часть. Хотя вполне подошел бы тип float, в примере будет применяться тип double. 3. Для вычисления расстояния умножьте 7, 2 на 335 и присвойте результат . Sound java переменной. 4. В заключение отобразите результат. Ниже показан полный код программы Sound ,java: /* Упражнение 2.1. Расчет расстояния до места вспышки молнии, звук которого был услышан через 7.2 секунды . */ class Sound { public static void main(String[] args ) { Глава 2. Введение в типы данных и операции 73 double dist; dist 7.2 * 335; System.out.printIn("Место вспышки молнии находится на расстоянии " + dist + " метров."); } } 5. Скомпилируйте и запустите программу. Отобразится следующий резуль- тат: Место вспышки молнии находится на расстоянии 2412.0 метров. 6. Дополнительная задача: рассчитать расстояние до крупного объекта вроде скалы можно по времени прихода эхо. Например , если вы хлопнете в ладоши и измерите время , через которое стало слышно эхо , то узнаете общее время прохождения звука туда и обратно. Разделив это значение на два , вы получите время, за которое звук проходит в одну сторону. Затем результирующее значение можно использовать для расчета расстояния до объекта. Модифицируйте предыдущую программу так, чтобы она вычисляла расстояние , исходя из предположения о том, что временной интервал представляет собой промежуток времени до прихода эхо. Литералы В языке Java под литералами понимаются фиксированные значения , представленные в удобочитаемой форме. Скажем , число 100 является литералом . Литералы также обычно называют константами. По большей части литералы и их применение настолько интуитивно понятно, что они использовались в той или иной форме во всех предыдущих примерах программ. Теперь пришло время объяснить их формально. Литералы Java могут относиться к любому примитивному типу данных. Способ представления каждого литерала зависит от его типа . Как объяснялось ра нее , символьные константы заключаются в одинарные кавычки, например , 'а' и '% '. Целочисленные литералы указываются как числа без дробных частей подобно 10 и -100 . Литералы с плавающей точкой требуют применения десятичной точки , за которой следует дробная часть числа , скажем , 11.123. Для чисел с плавающей точкой также разрешено использовать экспоненциальную запись. Целочисленные литералы по умолчанию имеют тип int. Чтобы указать ли терал типа long, понадобится добавить к константе букву 1 или L. Например , 12 — литерал типа int, a 12L - литерал типа long. По умолчанию литералы с плавающей точкой имеют тип double. Чтобы указать литерал типа float, необходимо добавить к константе букву F или f. Например, 10.19F - литерал типа float. 74 Java: руководство для начинающих, 9- е издание Хотя целочисленные литералы по умолчанию создаются как значения типа int, их все же разрешено присваивать переменным типа char, byte или short, если присваиваемое значение может быть представлено целевым типом. Целочисленный литерал всегда можно присвоить переменной типа long. В целочисленный литерал или литерал с плавающей точкой можно включить один или несколько символов подчеркивания , что позволит облегчить чтение значений, состоящих из многих цифр. Когда литерал компилируется, символы подчеркивания просто отбрасываются. Вот пример: 123_45_1234 Данный литерал задает значение 123451234. Применение символов подчеркивания особенно полезно при указании номеров деталей , идентификаторов клиентов и кодов состояния, которые обычно состоят из подгрупп цифр. Шестнадцатеричные, восьмеричные и двоичные литералы Как известно , в программировании иногда проще использовать систему счисления, основанную на 8 или 16 , а не на 10. Система счисления , основанная на 8, называется восьмеричной, и в ней применяются цифры от 0 до 7. В восьмеричной системе счисления число 10 соответствует числу 8 в десятичной форме . Система счисления с основанием 16 называется шестнадцатеричной и использует цифры от 0 до 9 плюс буквы от А до F, которые обозначают 10, 11, 12, 13, 14 и 15. Например , шестнадцатеричное число 10 равно десятичному числу 16. Из-за частого применения этих двух систем счисления Java позволяет указывать целочисленные литералы в шестнадцатеричной или восьмеричной форме вместо десятичной. Шестнадцатеричный литерал должен начинаться с Ох или ОХ ( ноль, за которым следует буква х или X ). Восьмеричный литерал начинается с нуля. Ниже приведены примеры: hex = OxFF; oct = Oil; // соответствует десятичному числу 255 // соответствует десятичному числу 9 Интересно отметить, что в Java также разрешены шестнадцатеричные литералы с плавающей точкой, но они используются редко. Целочисленный литерал можно указывать с помощью двоичного кода , для чего перед двоичным числом следует поставить 0Ь или 0В. Скажем, 0Ы 100 задает значение 12 в двоичном формате. Управляющие последовательности символов Заключение символьных констант в одинарные кавычки подходит для большинства печатаемых символов , но некоторые символы, такие как возврат каретки, создают особую проблему при работе в текстовом редакторе. Кроме того, ряд других символов вроде одинарных и двойных кавычек имеют специальное предназначение в Java и потому их нельзя применять напрямую. По указанным причинам в Java предусмотрены специальные управляющие последовательности , Глава 2. Введение в типы данных и операции 75 иногда называемые символьными константами с обратной косой чертой , которые описаны в табл . 2.3. Такие последовательности используются вместо символов, которые они представляют. Таблица 2.3. Управляющие последовательности символов Управляющая последовательность Описание \ \" \\ \г \п \f Одинарная кавычка \t Табуляция Забой Восьмеричный символ (ddd - восьмеричная константа) Шестнадцатеричный символ Unicode (хххх - шестнадцатеричная константа) Пробел ( последовательность добавлена BJDK 15 ) \b \ddd \uxxxx \s \конец-строки Двойная кавычка Обратная косая черта Возврат каретки Новая строка (также известная как перевод строки ) Подача страницы Строка продолжения ( применяется только к текстовым блокам; последовательность добавлена BJDK 15 ) Следующий оператор присваивает переменной ch символ табуляции: ch = '\t'; А так переменной ch можно присвоить символ одинарной кавычки: ch = '\ * » ; Строковые литералы В Java поддерживается еще один тип литерала: строка. Строка представляет собой набор символов, заключенных в двойные кавычки: "простой тест" Примеры строк встречались во многих операторах printIn ( ) в приведенных ранее программах. В дополнение к обычным символам строковый литерал может содержать одну или несколько только что описанных управляющих последовательностей . Рассмотрим следующую программу, в которой применяются управляющие последовательности \п и \t: 76 Java : руководство для начинающих, 9-е издание // Демонстрация использования управляющих последовательностей в строках , class StrDemo { public static void main(String[] args) ( System.out.println("Первая строка\пВторая строка"); System.out.printIn("A\tB\tC"); Использовать последовательность \n System.out.println("D\tE\tF"); “ t _ для вставки символа новой строки } } Использовать символы табуляции для выравнивания вывода Ниже показан вывод, генерируемый программой: Первая Вторая А D строка строка С В Е F !?@>A8< C M:A?5@B0 ВОПРОС . Является ли строка , состоящая из одного символа , той же самой сущностью , что и символьный литерал? Например , совпадают ли " к " и ' к ' ? ОТВЕТ. Нет. Не путайте строки с символами . Символьный литерал представля ет одиночную букву типа char. Строка , содержащая только одну букву, попрежнему считается строкой . Хотя строки состоят из символов , они относятся к разным типам . Обратите внимание , что для вставки символа новой строки используется управляющая последовательность \ п . Чтобы получить многострочный вывод , вовсе не обязательно применять несколько операторов p r i n t l n ( ) , а нужно просто помещать \ п в те места более длинной строки , где должны вставляться символы новой строки . И еще один момент: как будет показано в главе 5 , недавно в Java появилось функциональное средство , которое называется тек стовым блоком . Текстовый блок предлагает более высокую степень контроля и гибкости в ситуациях , когда требуется несколько строк текста. Подробный анализ переменных Переменные были представлены в главе 1 , а здесь они рассматриваются более подробно . Как вы узнали ранее , переменные объявляются с использованием оператора следующего вида: тип имя-переменной; В тип указывается тип данных переменной , а в имя-переменной — ее имя . Объявлять можно переменную любого допустимого типа , включая только что описанные простые типы , и каждая переменная будет иметь тип . Таким обра зом , возможности переменной определяются ее типом . Например , перемен ная типа boolean не может применяться для хранения значений с плавающей Глава 2 . Введение в типы данных и операции 77 точкой . Кроме того , тип переменной не может быть изменен в течение времени ее жизни . Скажем , переменная int не может превратиться в переменную char. Все переменные в Java должны быть объявлены до их использования , поскольку компилятору необходимо знать , данные какого типа содержит пере менная , прежде чем он сможет правильно скомпилировать любой оператор , в котором применяется переменная . Кроме того , появляется возможность выпол нения строгой проверки типов . Инициализация переменной Как правило , перед использованием переменной должно быть присвоено значение . Вы уже видели , что один из способов предусматривает применение оператора присваивания . Другой способ - присваивание переменной начального значения при ее объявлении . Для этого после имени переменной ставится знак равенства и нужное значение . Вот общая форма инициализации: тип переменная = значение ; В значение указывается значение , присваиваемое переменной переменная при ее создании . Значение должно быть совместимым с типом , указанным в тип. Ниже приведено несколько примеров: int count = 10; char ch = 'X'; float f = 1.2F; // присвоить count начальное значение 10 11 инициализировать ch буквой X // инициализировать f значением 1.2 При объявлении двух или более переменных одного типа , используя список - запятыми , начальное значение можно присваивать одной или разделителями с нескольким переменным: int а , b = 8, с = 19, d; // переменные b и с имеют инициализаторы В данном случае инициализируются только переменные Ь и с. Динамическая инициализация Несмотря на то что в предшествующих примерах в качестве инициализаторов применялись только константы , Java позволяет инициализировать пере менные динамически с использованием любого выражения , действительного на момент объявления переменной . Скажем , вот короткая программа , которая вычисляет объем цилиндра по радиусу его основания и высоте: // Демонстрация динамической инициализации. class Dynlnit { public static void main(String[] args) { double radius = 4, height = 5; // Динамически инициализировать volume , double volume = 3.1416 * radius * radius * height; System.out.println("Объем составляет " + volume); } } Переменная volume динамически инициализируется во время выполнения м 78 Java : руководство для начинающих, 9-е издание Здесь объявляются три локальные переменные: radius, height и volume. Первые две , radius и height, инициализируются константами , a volume динамически инициализируется значением объема цилиндра. Ключевой момент в том , что в выражении инициализации может присутствовать любой элемент, допустимый во время инициализации , в том числе вызовы методов , другие пе ременные или литералы . Область видимости и время жизни переменных До сих пор все применяемые переменные были объявлены в начале метода main ( ) . Тем не менее , в Java разрешено объявлять переменные в любом блоке . Как объяснялось в главе 1 , блок начинается с открывающей фигурной скобки и заканчивается закрывающей фигурной скобкой . Блок определяет область ви димости . Таким образом , каждый раз , когда начинается новый блок , создается новая область видимости . Область видимости устанавливает, какие объекты до ступны другим частям вашей программы . Она также определяет время жизни этих объектов. В общем случае каждое объявление в Java имеет область видимости . В ре зультате Java определяет мощную и детализированную концепцию области видимости . Две наиболее распространенных области видимости в Java определяются классом и методом . Обсуждение области видимости класса (и объявленных в нем переменных) откладывается до рассмотрения классов , а сейчас речь пойдет только об областях видимости , определенных методом или внутри него . Область видимости , определяемая методом , начинается с его открывающей фигурной скобки . Однако если у метода есть параметры , то они тоже входят в область видимости метода . Область видимости метода заканчивается закрываю щей фигурной скобкой . Такой блок кода называется телом метода . Как правило , переменные , объявленные внутри области видимости , не будут доступны в коде за рамками этой области . Таким образом , когда вы объявляете переменную в области видимости , то локализуете ее и защищаете от несанк ционированного доступа и/или модификации . Действительно , правила области видимости обеспечивают основу для инкапсуляции . Переменная , объявленная внутри блока , называется локальной переменной . Области видимости могут быть вложенными . Например , создавая блок кода , вы создаете новую вложенную область. В таком случае внешняя область види мости охватывает внутреннюю область видимости . В итоге объекты , объявлен ные во внешней области , будут доступны коду во внутренней области . Тем не менее , обратное утверждение неверно. Объекты , объявленные во внутренней области , не доступны за ее пределами . Чтобы лучше понять вложенные области видимости , взгляните на следующую программу: 79 Глава 2 . Введение в типы данных и операции // Демонстрация области видимости блока , class ScopeDemo { public static void main(String[] args) { // переменная известна всему коду внутри main() int х; х = 10; if(х == 10) { int у = 20; // начало новой области видимости // переменная известна только этому блоку // Переменные х и у здесь известны . } System.out.println("х и у: " + х = у * 2; X + " " + у ); Здесь переменная у находится за пределами своей области видимости // у = 100; // Ошибка! Переменная у здесь неизвестна // Переменная х здесь по-прежнему известна. System.out.println("Значение х равно " + х); } } Как указано в комментариях, переменная х объявляется в начале области ви димости метода main ( ) и доступна во всем последующем коде main ( ) . Внутри блока if объявляется переменная у. Поскольку блок определяет область видимости , переменная у видна только остальному коду внутри данного блока. Вот почему вне своего блока строка у = 100 ; закомментирована. Если удалить символы комментария , тогда возникнет ошибка на этапе компиляции, т.к. пере менная у не доступна за пределами своего блока . Внутри блока if можно ра ботать с х , потому что код внутри блока (т.е. во вложенной области видимости ) имеет доступ к переменным, объявленным в охватывающей области. Переменные могут быть объявлены в любом месте блока , но они будут дей ствительными только после объявления. Таким образом , если переменная определена в начале метода , то она доступна во всем коде внутри этого метода . И наоборот, переменная, объявленная в конце блока , по существу бесполезна , т.к. никакой код не будет иметь к ней доступа. Есть еще один важный момент, о котором следует помнить: переменные создаются при входе в свою область видимости и уничтожаются при выходе из этой области видимости. Другими словами , переменная не будет хранить свое значение после того, как покинет свою область видимости , а потому переменные, объявленные внутри метода , не сохраняют значения между его вызовами . Кроме того, переменная , объявленная внутри блока , утрачивает свое значение при выходе из блока. Таким образом , время жизни переменной ограничено ее областью видимости. Если объявление переменной включает инициализатор , тогда переменная будет повторно инициализироваться каждый раз при входе в блок, в котором она объявлена . Например, рассмотрим показанную ниже программу: // Демонстрация времени жизни переменной. class VarlnitDemo { 80 Java : руководство для начинающих, 9-е издание public static void main(String[] args) { int x; for(x = 0; x < 3; x++) { int у = -1; // у инициализируется при каждом входе в блок System.out.println("Значение у равно: " + у); // всегда выводится у = 100; System.out.println("Теперь значение у равно: " + у); } -1 } } Вот вывод, генерируемый программой: Значение у равно: Теперь значение у Значение у равно: Теперь значение у Значение у равно: Теперь значение у -1 равно: 100 -1 равно: 100 -1 равно: 100 Несложно заметить , что переменная у повторно инициализируется значени е м - 1 при каждом входе во внутренний цикл for . Несмотря на последующее присваивание у значения 100 , это значение утрачивается . С правилами областей видимости Java связана одна удивительная особен ность: хотя блоки могут быть вложенными , никакая переменная , объявленная во внутренней области видимости , не может иметь такое же имя , как у пере менной , объявленной во внешней области видимости . Например, следующая программа , в которой предпринимается попытка объявить две отдельные пере менные с одинаковыми именами , не скомпилируется. /* В этой программе предпринимается попытка объявить во внутренней области переменную с таким же именем, как у переменной, определенной во внешней области. * ** Программа не скомпилируется. к -к -к */ class NestVar { public static void main(String[] args) { int count; for(count = 0; count < 10; count = count+1) { System.out.println("Значение count: " + count); ^ Нельзя объявлять переменную count, поскольку она уже int count; // Не разрешено!!! определена for(count = 0; count < 2; count++) System.out.println("Программа содержит ошибку!"); } } } Глава 2. Введение в типы данных и операции 81 Операции Язык Java обеспечивает развитую среду операций. Операция - это символ , который сообщает компилятору о необходимости выполнить определенное ма тематическое или логическое действие. В Java есть четыре основных класса операций: арифметические операции, побитовые операции , операции отношения и логические операции. Кроме того , также определены дополнительные операции, предназначенные для обработки ряда особых ситуаций. В настоящей главе рассматриваются арифметические операции , операции отношения и логиче ские операции , а также операция присваивания. Побитовые и другие специальные операции будут обсуждаться позже. Арифметические операции В табл . 2.4 перечислены арифметические операции , которые определены в Java. Таблица 2.4 . Арифметические операции Java Операция Описание + Сложение ( также унарный плюс ) Вычитание (также унарный минус ) Умножение / % ++ Деление Остаток от деления Инкремент Декремент Операции + , -, * и / в Java работают точно так же, как в любом другом языке программирования ( впрочем , как и в алгебре). Их можно применять к любому встроенному числовому типу данных, а также использовать для объектов типа char. Хотя действия арифметических операций хорошо известны всем читателям , некоторые особые ситуации требуют пояснений. Прежде всего , не забывайте , что когда операция / применяется к целому числу, остаток от деления будет отброшен ; скажем, при целочисленном делении 10 / 3 дает 3. Получить остаток от целочисленного деления можно с помощью операции %. Например, результатом 10 % 3 будет 1. Операцию % в Java можно применять как к целочисленным ти пам , так и к типам с плавающей точкой. Таким образом, результат 1 0 . 0 % 3 . 0 также равен 1. Использование операции получения остатка от деления демон стрируется в следующей программе. 82 Java: руководство для начинающих, 9-е издание // Демонстрация использования операции %. class ModDemo { public static void main(String[] args) { int iresult, irem; double dresult, drem; iresult = 10 / 3; irem = 10 % 3; dresult = 10.0 / 3.0; drem = 10.0 % 3.0; System.out.println("Результат и остаток от деления 1 0 / 3: " + iresult + " " + irem); System.out.println("Результат и остаток от деления 10.0 / 3.0: " + dresult + " " + drem); } } Ниже показан вывод, генерируемый программой: Результат и остаток от деления 10 / 3: 31 Результат и остаток от деления 10.0 / 3.0: 3.3333333333333335 1.0 Как видите, результатом операции % является остаток от целочисленного деления и деления с плавающей точкой, равный 1. Инкремент и декремент — Описанные в главе 1 операции ++ и являются операциями инкремента и декремента. Как вы увидите , они обладают рядом характеристик, которые дела ют их довольно интересными. Начнем с рассмотрения того, что делают операции инкремента и декремента . Операция инкремента добавляет 1 к своему операнду, а операция декремента вычитает 1 из своего операнда. Таким образом, оператор х = х + 1; эквивалентен х++; а оператор х = х - 1; эквивалентен х — ; Операции инкремента и декремента могут либо предшествовать операнду (префиксная форма), либо следовать за операндом ( постфиксная форма). Ска жем , оператор х = х + 1; можно записать так: ++х; // префиксная форма или так х++; // постфиксная форма Глава 2. Введение в типы данных и операции 83 В приведенном выше примере нет разницы , какая форма применяется префиксная или постфиксная . Однако когда операция инкремента или декре- мента является частью более крупного выражения, то имеется важное отличие. Когда операция инкремента или декремента предшествует своему операнду, то сначала выполняется операция , а затем значение операнда используется в остальной части выражения. Если операция инкремента или декремента находится после своего операнда , то в выражении будет применяться значение операнда до инкрементирования или декрементирования. Рассмотрим следующие операторы: = 10 ; у = + + х; X В данном случае переменная у получит значение 11. Тем не менее , если код будет иметь показанный ниже вид: X = 10; у = х+ + ; то значением переменной у окажется 10. В обоих случаях переменная х уста навливается в 11; разница лишь в том, когда это происходит. Возможность контроля над тем , когда происходит операция инкремента или декремента , обеспечивает значительные преимущества. Операции отношения и логические операции Операции отношения и логические операции отличаются тем, что первые касаются отношений, которые значения имеют друг с другом , а вторые способов связи истинных и ложных значений. Поскольку результатами операций отношения являются истинные или ложные значения, они часто используются с ло гическими операциями , и потому здесь обсуждаются вместе. Операции отношения перечислены в табл . 2.5. — Таблица 2.5. Операции отношения Java Операция Описание Равно I = Не равно > < >= Больше <= Меньше или равно Меньше Больше или равно В табл . 2.6 описаны логические операции. 84 Java: руководство для начинающих, 9-е издание Таблица 2.6. Логические операции Java Операция Описание И & ИЛИ А Исключающее ИЛИ && Короткозамкнутое ИЛИ Короткозамкнутое И НЕ Результатом операции отношения и логической операции является значение типа boolean. Все объекты в Java можно сравнивать на равенство или неравенство с ис пользованием операций = = и ! =. Однако операции сравнения < , > , < = и > = можно применять только к типам , которые поддерживают отношение упорядочивания. Таким образом , все операции отношения могут применяться ко всем числовым типам и к типу char , но значения типа boolean можно сравнивать только на равенство или неравенство, т.к. значения true и false не считаются упорядоченными. Скажем , true > false в Java не имеет смысла . Для логических операций операнды и результаты имеют тип boolean . Логические операции & , I , л и ! поддерживают основные логические действия И , ИЛИ , исключающее ИЛИ и НЕ в соответствии с таблицей истинности , представленной в табл . 2.7. Таблица 2.7. Таблица истинности для базовых логических операций Java Р false true false true q р & q р false false false false false false false true true true true true false true true true I q р А q !р true false true false В табл . 2.7 видно , что результатом выполнения операции исключающего ИЛИ будет true , когда один и только один операнд равен true. В следующей программе демонстрируется работа нескольких операций отношения и логических операций . // Демонстрация работы операций отношения и логических операций , class RelLogOps { public static void main(String[] args) { int i, j; boolean bl, b2; i = 10; j = 11; Глава 2. Введение в типы данных и операции if(i if(i if (i if(i if(i if(i 85 < j) System.out.println("i < j"); <= j) System.out.println("i <= j"); != j) System.out. println("i != j"); == j) System.out.println("He выполнится."); >= j) System.out.println("He выполнится."); > j) System.out.println("He выполнится."); bl = true; b2 = false; if(bl & b2) System.out.println("He выполнится."); if(!(bl & b2)) System .out.println("!(bl & Ь2) дает true"); if(bl b2) System.out.println("bl | Ь2 дает true"); Ь2 дает true"); if(bl A b2) System.out.println("bl Л } } Вот вывод, генерируемый программой: i < j i <= j i != j !(bl & Ь2) дает true bl Ь2 дает true bl л Ь2 дает true Короткозамкнутые логические операции В языке Java предусмотрены специальные короткозамкнутые версии логических операций И и ИЛИ , которые можно использовать для создания более эффективного кода . Давайте выясним причину. Если первый операнд операции И равен f a l s e , тогда результатом будет f a l s e независимо от того , какое значение имеет второй операнд. Если первый операнд операции ИЛИ равен true , то результатом будет true независимо от значения второго операнда. Таким образом, в описанных двух случаях нет необходимости вычислять второй операнд, что экономит время и приводит к получению более эффективного кода. Короткозамкнутая операция И обозначается с помощью & & , а короткозамкнутая операция ИЛИ посредством | | . Их нормальными аналогами являются операции & и | . Единственная разница между обычной и короткозамкнутой версией связана с тем , что в нормальных операциях всегда вычисляются все операнды , а в короткозамкнутых версиях второй операнд вычисляется только по мере необходимости . Далее приведена программа , демонстрирующая работу короткозамкнутой операции И . Она выясняет, будет ли значение d множителем п , за счет выпол нения операции деления по модулю. Если остаток от деления n / d равен нулю , то d множитель п. Но поскольку операция деления по модулю включает в себя деление , для предотвращения ошибки деления на ноль применяется короткозамкнутая версия операции И . — — 86 Java: руководство для начинающих, 9-е издание // Демонстрация работы короткозамкнутой операции , class SCops { public static void main(String[] args) { int n, d, q; n = 10; d = 2; if(d ! = 0 && (n % d) == 0) System.out.println(d + " d = - множитель " + n); 0; // установить d в ноль // Поскольку d равно нулю, второй операнд не вычисляется. if(d != 0 && (n % d) == 0) Короткозамкнутая операция System.out.println(d + " - множитель " + n); предотвращает деление на ноль /* Теперь попробовать то же самое, не используя короткозамкнутую операцию. В итоге возникнет ошибка деления на ноль. */ Теперь вычисляются if(d != 0 & (n % d) == 0) < оба выражения, - System.out.println(d + " - множитель " + n); делая возможным деление на ноль } } Чтобы предотвратить деление на ноль, с помощью оператора if сначала проверяется, равно ли d нулю. Если это так, тогда короткозамкнутая операция И останавливается в этой точке и не выполняет деление по модулю. Таким образом , в первой проверке d равно 2 и операция деления по модулю выполняется. Вторая проверка завершается неудачно , потому что d установлено в ноль, а операция деления по модулю пропускается , позволяя избежать ошибки деления на ноль. В конце испытывается обычная операция И , которая приводит к вы числению обоих операндов, в результате чего во время выполнения возникает ошибка деления на ноль. И последнее замечание: в формальной спецификации Java короткозамкнутые операции называются условным ИЛИ и условным И, но обычно используется термин короткозамкнутые операции. Операция присваивания Операция присваивания применялась, начиная с главы 1 , так что пора представить ее формально. Операция присваивания обозначается одиночным знаком равенства ( = ) и работает в Java почти так же , как в любом другом языке программирования. Вот ее общий вид: переменная = выражение ; Тип переменной должен быть совместимым с типом выражения. Операция присваивания обладает одной интересной особенностью, с которой вы, возможно, пока еще не знакомы: она позволяет создавать цепочку при - сваиваний. Глава 2 . Введение в типы данных и операции 87 !?@>A8< C M:A?5@B0 ВОПРОС. Учитывая , что короткозамкнутые операции в ряде случаев эффективнее своих нормальных аналогов, почему в Java по- прежнему предлагаются обычные операции И и ИЛИ ? ОТВЕТ. В некоторых случаях требуется , чтобы вычислялись оба операнда опе рации И либо ИЛИ из-за возникающих побочных эффектов. Взгляните на следующий код: // Побочные эффекты могут быть важны , class SideEffects { public static void main(String[] args) { int i; i = 0; /* Здесь i все равно инкрементируется, несмотря на то, что условие в операторе if дает false. */ if(false & (++i < 100)) System.out.println("He отображается"); System.out.println("Оператор if выполняется: " + i); // i имеет значение 1 } /* В данном случае i не инкрементируется, поскольку короткозамкнутая операция пропускает инкрементирование. */ if(false && (++i < 100)) System.out.println("He отображается"); System.out.println ("Оператор if выполняется: " + i); // i по-прежнему имеет значение 1! } Как указано в комментариях, в первом операторе if значение i инкременти руется независимо оттого, удовлетворено ли условие . Тем не менее , в случае применения короткозамкнутой операции переменная i не инкрементируется, если первый операнд имеет значение false. Суть в том , что если код ожидает вычисления правого операнда операции И либо ИЛИ , то должны использоваться нормальные, а не короткозамкнутые формы этих операций. Например , взгляните на следующий фрагмент кода: int х, у, z; x = y = z = 1 0 0; // устанавливает х, у и z в 100 В приведенном фрагменте кода переменные х , у и z устанавливаются в 100 с помощью одного оператора. Прием работает, потому что = представляет собой операцию , которая возвращает значение правого выражения. Таким образом , значение z = 100 равно 100, которое и присваивается у, а затем х. Использование цепочки присваивания является простым способом присвоить группе переменных общее значение. 88 Java: руководство для начинающих, 9- е издание Сокращенные операции присваивания В Java предлагаются специальные сокращенные операции присваивания, которые упрощают код отдельных операторов присваивания. Начнем с примера. Показанный ниже оператор: х = х + 10; можно переписать с применением сокращенной операции присваивания: х += 10; Пара операций + = указывает компилятору о необходимости присвоить х зна чение х плюс 10. Рассмотрим еще один пример. Следующий оператор: х = х - 100; эквивалентен такому: х -= 100; Оба оператора присваивают переменной х значение х минус 100. Сокращение подобного рода будет работать для всех бинарных операций Java (т.е. требующих двух операндов). Вот общая форма сокращения: переменная операция= выражение; Ниже перечислены арифметические и логические сокращенные операции присваивания . — += %= &= _ /= Л Поскольку эти операции объединяют операцию с присваиванием , их формально называют составными операциями присваивания. С составными операциями присваивания связаны два преимущества . Вопервых , они более компактны , чем их “ длинные ” аналоги. Во- вторых , в не которых случаях они более эффективны. По указанным причинам составные операции присваивания часто встречаются в профессионально написанных программах на Java. Преобразование типов при присваивании В программировании принято присваивать переменную одного типа переменной другого типа. Скажем , может потребоваться присвоить значение int переменной float: int i; float f; i = 10; f = i; // присваивает значение int переменной float Глава 2. Введение в типы данных и операции 89 Когда в операторе присваивания смешиваются совместимые типы, значение правой части автоматически преобразуется в тип левой части. Таким образом , в предыдущем фрагменте значение i преобразуется в число типа float и затем присваивается f . Однако из-за строгой проверки типов в Java не все типы совместимы , а потому не все преобразования типов неявно разрешены. Напри мер, типы boolean и int несовместимы. Когда данные одного типа присваиваются переменной другого типа , автоматическое преобразование типов происходит, если z два типа совместимы; z целевой тип крупнее исходного. В случае удовлетворения обоих условий происходит расширяющее преобразование. Скажем , тип int всегда достаточно велик, чтобы хранить все допустимые значения byte, к тому же int и byte являются целочисленными типами, а потому может быть применено автоматическое преобразование из byte в int. Для расширяющих преобразований числовые типы , включая целочисленные типы и типы с плавающей точкой, совместимы друг с другом. Например, следующая программа совершенно корректна , т.к. преобразование из long в double это расширяющее преобразование , которое выполняется автоматически. // Демонстрация автоматического преобразования из long в double. class LtoD { public static void main(String[] args) { long re double D; - L = 100123285L; D = L; + Автоматическое преобразование из long в double System.out.println("L и D: " + L + " " + D); } } Несмотря на наличие автоматического преобразования из long в double, автоматическое преобразование из double в long не существует, потому что оно не является расширяющим. Таким образом, показанная далее версия предыдущей программы недопустима. Эта программа не скомпилируется . * // class LtoD { public static void main(String[] args) { long L; double D; D L = 100123285.0; = D; // H e разрешено!!! * < System.out.println("L и D: " + L + " " + D); } } Автоматическое преобразование из long в double не существует 90 Java : руководство для начинающих, 9-е издание Кроме того, отсутствует автоматическое преобразование числовых типов в char или boolean. Вдобавок типы char и boolean несовместимы друг с другом . Тем не менее , переменной типа char может быть присвоен целочисленный литерал . Приведение несовместимых типов Несмотря на удобство автоматического преобразования типов , оно не сможет удовлетворить всем требованиям , поскольку применяется для сужающих преобразований между совместимыми типами . Во всех остальных случаях должно использоваться приведение , т. е . инструкция компилятору о необходимости преобразования одного типа в другой . Подобным образом запрашивается явное преобразование типа. Приведение имеет следующую общую форму: ( целевой -B8? ) выражение В целевом типе указывается желаемый тип для преобразования заданного выражения . Например, чтобы преобразовать тип выражения х / у в int, можно записать так: double х, у; II ... (int) ( х / у ) Несмотря на то что х и у имеют тип double, приведение преобразует результат выражения в int. Круглые скобки вокруг х / у обязательны , иначе приве дение к типу int применялось бы только к х, а не к результату деления. При ведение здесь необходимо , потому что нет автоматического преобразования из double в int. Когда приведение включает в себя сужающее преобразование , информа ция может быть утрачена . Скажем , при преобразовании значения long в short информация будет теряться , если значение long выходит за рамки диапазона short, т. к. старшие биты значения long удаляются. Когда значение с плавающей запятой преобразуется в целочисленный тип , дробная часть также будет утрачена из- за усечения . Например , если присвоить целочисленной перемен ной значение 1 . 2 3 , то результирующим значением окажется просто 1 , а 0 . 2 3 потеряется . В показанной далее программе демонстрируется ряд преобразований типов , которые требуют приведений . // Демонстрация приведений. class CastDemo { public static void main(String[] args) { double x, y; byte b; int i; char ch; x = 10.0; У = 3.0; Глава 2 . Введение в типы данных и операции 91 В этом преобразовании произойдет усечение i = (int) (х / у); // приведение double к int System.out.println("Целочисленный результат х / у: " + i); i = 100; b = (byte) i; - System.out.println("Значение b: " + b); i = 257; b = (byte) i; System.out.println("Значение b: " + b); b = 88; // код ASCII для X ch = (char) b; Здесь информация не теряется. Переменная byte способна хранить значение 100. На этот раз информация утрачивается. Переменная byte не может хранить значение 257. Приведение между несовместимыми ^ System.out.println("Значение ch: " + ch); типами } } Вот вывод, генерируемый программой: Целочисленный результат х / у: 3 Значение Ь: 100 Значение b: 1 Значение ch: X Приведение ( х / у ) к типу i n t в программе вызывает усечение дробной части и потерю информации. Затем , когда b присваивается значение 100 , потеря информации не происходит, поскольку переменная типа byte способна содержать такое значение. Однако при попытке присвоить b значение 257 информация утрачивается, т. к. 257 превышает максимальное значение типа byte. Наконец, для присваивания значения byte переменной char информация не теряется , но требуется приведение. Старшинство операций В табл . 2.8 показан порядок старшинства операций Java от самого высокого до самого низкого. Многие из перечисленных операций будут обсуждаться позже в книге. Хотя [ ] , ( ) и . формально являются разделителями, они также могут действовать подобно операциям и в этом качестве обладать наивысшим приоритетом . Таблица 2.8. Старшинство операций в Java Высший приоритет ++ ( постфиксная ) ( постфиксная ) ++ ( префиксная ) ( префиксная ) * / ~ % ! + ( унарная ) - ( унарная) ( приведениетипа ) 92 Java : руководство для начинающих, 9-е издание Окончание табл. 2.8 Высший приоритет + >» >= » > & « < < = instanceof А 9• -> операция= Низший приоритет Упражнение 2.2 Отображение таблицы истинности для логических операций Втекущем проекте будет создана программа , отобраLogicalOpTable . j ava жающая таблицу истинности для логических операций Java. Колонки в таблице должны быть выровненными. В проекте используется несколько функциональных средств, описанных в главе, в том числе одна из управляющих последовательностей Java и логические операции. Кроме того, иллюстрируются различия в приоритетах между арифметической операцией + и логическими операциями. . 1 Создайте файл по имени LogicalOpTable . j ava . 2. Чтобы обеспечить выравнивание колонок, будет применяться управляющая последовательность \ t для встраивания символа табуляции в каждую строку вывода . Например , следующий оператор p r i n t l n ( ) отображает заголовок таблицы: System.out.println("P\tQ\tAND\tOR\tXOR\tNOT"); 3. В каждой последующей строке таблицы с помощью символов табуляции будет позиционироваться результат выполнения операции под соответствующим заголовком. 4. Наберите приведенный ниже код программы LogicalOpTable . java: // Упражнение 2.2. Вывод таблицы истинности для логических операций , class LogicalOpTable { Глава 2. Введение в типы данных и операции 93 public static void main(String[] args) { boolean p, q; System.out.printIn("P\tQ\tAND\tOR\tXOR\tNOT"); p = true; q = true; System.out.print(p + "\t" + q +"\t"); System.out.print((p&q) + "\t" + (p|q) + ”\t"); System.out.println((pAq ) + "\t" + (!p)); p = true; q = false; System.out.print(p + "\t" + q +"\t"); System.out.print((p& q) + "\t" + (p|q) + "\t"); System.out.println((pAq) + "\t" + (!p)); p = false; q = true; System.out.print(p + "\t" + q +"\t"); System.out.print((p&q) + "\t" + (p|q) + "\t"); System.out.println((pAq) + "\t" + (!p)); p = false; q = false; System.out.print(p + "\t" + q +"\t"); System.out.print((p& q) + "\t" + (p|q) + "\t"); System.out.println((pAq) + "\t" + (!p)); } } Обратите внимание на круглые скобки , окружающие логические опера ции внутри операторов p r i n t l n ( ) . Они необходимы из- за старшинства операций Java . Операция + имеет более высокий приоритет, нежели логи ческие операции . 5. Скомпилируйте и запустите программу. Отобразится следующая таблица . P true true false false Q true false true false AND true false false false OR true true true false XOR false true true false NOT false false true true 6. Попробуйте самостоятельно модифицировать программу, чтобы в ней использовались и отображались значения 1 и 0 , а не t r u e и f a l s e. За дача может потребовать немного больше усилий , чем кажется на первый взгляд! Выражения Операции , переменные и литералы являются составными частями выраже ний . Вероятно , вам уже известна общая форма выражения из прошлого опыта программирования либо из алгебры . Тем не менее , здесь мы обсудим несколько аспектов выражений . 94 Java: руководство для начинающих, 9-е издание Преобразования типов в выражениях В выражении разрешено смешивать данные двух или более отличающихся типов , если они совместимы друг с другом. Скажем , в выражении можно сме шивать данные short и long, потому что они оба являются числовыми типами . Когда в выражении смешиваются данные разных типов , все они преобразуются в один и тот же тип , что достигается за счет применения так называемых правил повышения типов Java. Первым делом все значения char, byte и short повышаются до int. Затем , если один операнд имеет тип long, то все выражение повышается до long. Если же один операнд относится к типу float, тогда все выражение повышается до float. Если какой -либо из операндов имеет тип double, то результатом будет значение double. Важно понимать , что повышение типов применяется только к значениям , над которыми выполняются операции при вычислении выражения. Например , даже если значение переменной типа byte было повышено до int внутри вы ражения , то за пределами выражения типом переменной по - прежнему остается byte. Повышение типа влияет только на вычисление выражения . Однако повышение типов может приводить к несколько неожиданным ре зультатам . Скажем , когда арифметическая операция включает два значения byte, то вот что происходит. Сначала операнды типа byte повышаются до int, после чего выполняется операция , дающая результат типа int. Таким образом , результат операции с двумя значениями byte будет иметь тип int. Это не то , что можно было со всей очевидностью предположить . Рассмотрим следующую программу : // Неожиданное повышение типов! class PromDemo { public static void main(String[] args) { byte b; int i; В приведении нет необходимости, потому что результат уже повышен до int b = 10; j i = b * b; // Нормально, в приведении нет нужды . Здесь для присваивания значения int переменной типа byte требуется приведение типов! b = 10; b = (byte) (b * b); // Требуется приведение! System.out.println("i и b: " + i + " " + b); } } Как ни парадоксально , при присваивании переменной i значения b* b при ведение не требуется , поскольку во время вычисления выражения тип b повы шается до int. Тем не менее , при попытке присвоить переменной Ь значение b * b требуется приведение обратно к типу byte! Имейте такую особенность в виду, когда получаете неожиданные сообщения об ошибках несовместимости типов для выражений , которые иначе казались бы совершенно правильными . Глава 2 . Введение в типы данных и операции 95 Такая же ситуация возникает и при выполнении операций над операндами типа char . Например , в показанном далее фрагменте кода приведение обратно к char требуется из-за повышения chi и ch 2 внутри выражения до типа i n t : char chi = 'a', ch2 = chi = т b *; (char) (chi + ch2); Без приведения результатом сложения chi и ch 2 было бы значение i n t , которое нельзя присвоить переменной типа char. Приведения полезны не только при преобразовании между типами во время присваивания. Скажем , взгляните на следующую программу, в которой используется приведение к double , чтобы сохранить дробную часть результата; иначе деление было бы целочисленным. // Использование приведения. class UseCast { public static void main(String[] args) { int i; for( i = 0; i < 5; i++) { System.out.println( i + " / 3: " + i / 3); System.out.println(i + " / 3 с дробной частью: " + (double) i / 3); System.out.println(); } } } Вот вывод, генерируемый программой: 0 / 3: 0 0 / 3 с дробной частью: 0.0 1 / 3: 0 1 / 3 с дробной частью: 0.3333333333333333 2 / 3: 0 2 / 3 с дробной частью: 0.бббббббббббббббб 3 / 3: 1 3 / 3 с дробной частью: 1.0 4 / 3: 1 4 / 3 с дробной частью: 1.3333333333333333 Использование пробельных символов и круглых скобок Выражение в Java может содержать символы табуляции и пробелы для повышения удобочитаемости . Например, следующие два выражения одинаковы, но второе легче для восприятия: х=10/у*(127/х); х = 10 / у * (127/х); 96 Java: руководство для начинающих, 9-е издание Скобки увеличивают приоритет содержащихся в них операций ( как в алге бре ). Применение лишних или дополнительных скобок не вызывает ошибки и не замедляет выполнение выражения. Круглые скобки рекомендуется использовать с целью прояснения точного порядка вычисления и для себя , и для других, кому придется разбираться в программе позже . Скажем , какое из показанных ниже выражений легче для восприятия? х = y/3-34*temp+127; х = (у/3) - (34*temp) + 127; Вопросы и упражнения для самопроверки 1. Почему в Java для каждого примитивного типа строго определен диапазон и поведение? 2. Что такое символьный тип в Java и чем он отличается от символьного типа , используемого в ряде других языков программирования? 3. Переменная типа boolean может иметь любое желаемое значение , потому что любое ненулевое значение является истинным . Так ли это? 4. Напишите одиночный оператор println ( ) , выдающий такой вывод: Один Два Три Напишите строку кода с вызовом метода println ( ) , где этот результат выводится в виде одной строки . 5. Что не так с этим фрагментом кода? for(i = 0; i < 10; i++) { int sum; sum = sum + i; } System.out.println("Сумма ; " + sum); 6. Объясните разницу между префиксной и постфиксной формами опера ции инкремента. 7. Покажите , как можно использовать короткозамкнутую операцию И для предотвращения ошибки деления на ноль. 8. До какого типа повышаются типы byte и short в выражении? 9. Когда в общем случае необходимо приведение? 10. Напишите программу, находящую все простые числа от 2 до 100. 11 . Влияют ли избыточные круглые скобки на скорость выполнения про граммы? 12. Определяет ли блок область видимости? s .••••..у у,,* • • . • * s . V s* S '. vI , * I I . ; .'• Ss:•:•' »» 4 * I \ I s» ' II I,I I , II .' II . 4' I I I oiXfS • 9 .• •ggf: * ••• Глава 3 Операторы управления программой 98 Java: руководство для начинающих, 9- е издание В этой главе• • • z Ввод символов с клавиатуры z Полная форма оператора if z Использование оператора switch z Полная форма цикла for z Применение цикла while z Использование цикла do-while z Применение оператора break для выхода из цикла z z Использование оператора break как формы “goto ” Применение оператора continue z Вложенные циклы в этой главе вы узнаете об операторах , управляющих потоком выполнения программы. Существуют три категории операторов управления программой: операторы выбора, включая if и switch, итерационные операторы , куда входят циклы for, while и do-while, а также операторы перехода, к которым относятся break, continue и return. В главе подробно рассматриваются все управля ющие операторы кроме оператора return, обсуждаемого далее в книге , в том числе уже знакомые вам if и for. Глава начинается с объяснения способа реализации простого ввода с клавиатуры. Ввод символов с клавиатуры Прежде чем приступить к исследованию управляющих операторов Java , имеет смысл сделать небольшое отступление , которое позволит вам приступить к написанию интерактивных программ. До настоящего момента примеры программ , приводимые в книге , отображали информацию пользователю, но ни какой информации от него не получали. Таким образом , ранее использовался консольный вывод, но не консольный ввод ( с клавиатуры ). Главная причина связана с тем, что возможности ввода в Java опираются или задействуют функциональные средства , которые пока еще не обсуждались. Вдобавок большинство реальных приложений Java будут графическими и оконными , а не консольными. По указанным причинам консольный ввод в книге применяется редко. Однако есть один вид консольного ввода , который относительно прост в использовании: чтение символа с клавиатуры. Поскольку данное средство при меняется в нескольких примерах в текущей главе , оно обсуждается ниже. Глава 3. Операторы управления программой 99 Для чтения символа с клавиатуры будет использоваться метод System , in.read ( ) . Объект ввода , присоединенный к клавиатуре, System ,in, является дополнением System.out. Метод read ( ) ожидает, пока пользователь не нажмет какую-то клавишу, после чего возвращает результат. Символ возвращается как целое число , поэтому для присваивания его переменной char его необходи мо привести к типу char. По умолчанию консольный ввод буферизируется построчно. Под буфером понимается небольшая область памяти , которая хранит символы до того, как они будут прочитаны программой. В данном случае буфер содержит полную строку текста , так что придется нажать < Enter >, прежде чем любой набранный символ отправится программе. Ниже показана программа , в которой читается символ с клавиатуры: // Чтение символа с клавиатуры , class Kbln { public static void main(String[] args) throws java.io.IOException { char ch; System.out.print("Нажмите клавишу и затем ENTER: "); ch = (char) System.in.read(); // получить символ System.out.println("Была нажата клавиша: " + ch); Чтение символа с клавиатуры } } Вот пример выполнения программы: Нажмите клавишу и затем ENTER: t Была нажата клавиша: t Обратите внимание в программе , что метод main ( ) начинается следующим образом: public static void main(String[] args) throws java.io.IOException { Конструкция throws java.io.IOException должна быть указана из- за того , что в программе применяется System ,in.read ( ) , а потому требуется обработка ошибок ввода. Она входит в состав механизма обработки исключений Java , обсуждаемого в главе 9. Пока не беспокойтесь о ее точном назначении. Построчная буферизация , реализуемая System ,in, иногда приводит к сложностям. При нажатии < Enter > в поток ввода помещается последовательность символов возврата каретки и перевода строки. Кроме того , эти символы остаются в буфере ввода до тех пор, пока не будут прочитаны. Таким образом , в некоторых приложениях может потребоваться удалить их ( путем чтения ) перед следующей операцией ввода ; соответствующий пример будет показан позже в главе. 100 Java: руководство для начинающих, 9-е издание Оператор if Оператор if был представлен в главе 1 , а ниже он рассматривается более подробно. Вот его полная форма: if [ условие ) оператор ; else оператор ; Целями if и else являются одиночные операторы. Оператор else необязателен. Вдобавок целями if и else могут быть блоки операторов. Общая форма оператора if с блоками операторов выглядит следующим образом: if( условие ) { последовательность операторов } else { последовательность операторов ) Если условное выражение истинно, тогда выполнится цель оператора if. В противном случае выполнится цель оператора else, если она существует. Обе цели одновременно никогда не выполняются. Условное выражение, управляющее оператором if, должно давать результат типа boolean. Чтобы продемонстрировать if ( и несколько других управляющих операторов) , будет разработана простая игра , основанная на угадывании , которая наверняка понравится маленьким детям. В первой версии игры программа запра шивает у игрока букву от А до Z. Если игрок нажимает клавишу с правильной буквой, тогда программа реагирует выводом сообщения ** Правильно **. Ниже показан ее код. // Игра в угадывание буквы . class Guess { public static void main(String[] args) throws java.io.IOException { char ch, answer K'; System.out.println("Задумана буква между А и Z."); System.out.print("Попробуйте ее угадать: "); ch = (char) System.in.read(); // чтение символа с клавиатуры if(ch == answer) System.out.println(и ** Правильно ** и } } Программа предлагает игроку угадать букву, после чего читает символ с клавиатуры. Затем с помощью оператора if прочитанный символ сравнивается с ответом answer буквой К в данном случае. Если была введена буква К, тогда отображается сообщение. При опробовании программы не забывайте, что буква К должна вводиться в верхнем регистре . — Глава 3. Операторы управления программой 101 В следующей версии добавлен оператор else для вывода сообщения, когда выбрана неправильная буква. // Игра в угадывание буквы , версия 2. class Guess2 { public static void main(String[] args) throws java.io.IOException { char ch, answer = 'K'; System.out.println("Задумана буква между А и Z."); System.out.print("Попробуйте ее угадать: "); ch = (char) System.in.read(); // чтение символа с клавиатуры if(ch == answer) System.out.println( »• * * Правильно * * "); else System.out.println("...Увы , не угадали."); } } Вложенные операторы if Вложенный оператор i f представляет собой оператор i f , который является целью другого оператора i f или e l s e. Вложенные операторы i f очень распространены в программах. При вложении i f главное помнить, что оператор e l s e всегда ссылается на ближайший оператор i f , который находится в том же блоке, что и else , и еще не связан с оператором else , например: if(i == Ю ) ( if(j < 20) a = b; if(k > 100) c = d; else a = с; // этот оператор else относится к if(k > 100) } else a = d; // этот оператор else относится к if( i == 10) Как видно в комментариях, финальный оператор else не связан с if(j < 20), поскольку он не находится в том же блоке ( несмотря на то, что он ближай ший if без else). Взамен финальный оператор else связан с if(i == 10). Внутренний оператор else относится Kif(k > 100), потому что он бли жайший if в том же блоке. С помощью вложенного оператора if можно улучшить игру в угадывание буквы. Следующее дополнение программы выдает игроку подсказку о том , где находится правильная буква , в случае ввода некорректного предположения. — — // Игра в угадывание буквы , версия 3. class Guess3 { public static void main(String[] args) throws java.io.IOException { char ch , answer = 'K'; System.out.println("Задумана буква между А и Z."); System.out.print("Попробуйте ее угадать: "); ch = (char) System.in.read(); // чтение символа с клавиатуры if(ch == answer) System.out.println( n ** Правильно ** » ); I 102 Java : руководство для начинающих, 9-е издание else { System.out.print Вложенный оператор if .Увы , не угадали. Задуманная буква находится "); // Вложенный оператор if. if(ch < answer) System.out.println("дальше по алфавиту."); else System.out.println ("ближе по алфавиту."); } } } Ниже показан результат пробного запуска программы: Задумана буква между А и Z. Попробуйте ее угадать: Z ...Увы , не угадали. Задуманная буква находится ближе по алфавиту. Цепочка if - else-if Распространенной программной конструкцией , основанной на последовательности вложенных операторов if , является цепочка if -else-if , которая выглядит следующим образом: if( условие ) оператор; else if [ условие ) оператор; else if { условие ) оператор ; else оператор; Условные выражения вычисляются сверху вниз. Как только обнаружено истинное условие , выполняется ассоциированный с ним оператор, а остальная часть цепочки игнорируется. Если ни одно из условий не выполняется , тогда будет выполнен последний оператор else. Финальный оператор else действует как условие по умолчанию; т.е. если все другие условные проверки не пройдены , то выполняется последний оператор else. Если финального оператора else не предусмотрено и все остальные условия ложны , тогда никакие действия не предпринимаются. В показанной далее программе демонстрируется использование цепочки if -else-if . - // Демонстрация использования цепочки if-else if. class Ladder { public static void main(String[] args) { int x; for(x=0; x <6; x ++) { if(x==l) Глава 3 . Операторы управления программой } System.out.println("Значение х равно 1"); else if(х==2) System.out.println("Значение x равно 2"); else if(x==3) System.out.println("Значение x равно 3"); else if( x==4) System.out.println("Значение x равно 4"); else System.out.println("Значение x не находится ; между 1 и 4"); ЮЗ Оператор, заданный по умолчанию } } Программа производит следующий вывод: Значение Значение Значение Значение Значение Значение х не находится между 1 и 4 х равно 1 х равно 2 х равно 3 х равно 4 х не находится между 1 и 4 Как видите , заданный по умолчанию оператор else выполняется только в том случае , если не был выполнен ни один из предшествующих операторов if . Традиционный оператор switch Вторым оператором выбора в Java является switch , который обеспечивает многовариантное ветвление. Таким образом, он позволяет в программе выбирать из нескольких альтернатив. Хотя с помощью последовательности вложенных операторов if можно выполнять многовариантные проверки, применение оператора switch во многих ситуациях оказывается более эффективным подходом. Прежде чем продолжить, необходимо отметить один важный момент. Начи ная с JDK 14, оператор switch был значительно улучшен и расширен рядом новых возможностей, выходящих далеко за рамки его традиционной формы . Изза значимой роли недавних усовершенствований switch они описаны в главе 16 в контексте других недавних дополнений к Java. Здесь оператор switch представлен в своей традиционной форме, которая была частью Java с самого начала и потому используется весьма широко. Кроме того , эта форма будет функци онировать во всех средах разработки Java. Рассмотрим, как работает традиционный оператор switch. Значение выражения последовательно проверяется на предмет соответствия со списком констант. Когда совпадение найдено, выпол няется ассоциированная с ним последовательность операторов. Вот как выглядит общая форма традиционного оператора switch: switch { выражение ) { case константа1 : последовательность операторов break; 104 Java: руководство для начинающих, 9- е издание case константа 2: последовательность break; case константаЗ: последовательность операторов операторов break; default: } последовательность операторов В версиях Java , предшествующих JDK 7, управляющее выражение оператора switch должно давать значение типа byte, short, int, char или перечисления. ( Перечисления описаны в главе 12. ) Тем не менее , в настоящее время выражение также может иметь тип String, т.е. в современным версиях Java для управления оператором switch можно применять строку. (Такой прием демонстрируется в главе 5 при описании типа String.)Часто выражение , управля ющее оператором switch, представляет собой просто переменную , а не более крупное выражение. Каждое значение , указанное в операторах case, обязано быть уникальным константным выражением (вроде литерального значения). Дублировать значения в операторах case не разрешено. Тип каждого значения должен быть совместимым с типом выражения. Последовательность операторов в default выполняется, если ни одна константа case не соответствует выражению. Оператор default необязателен ; если он отсутствует, а совпадения не обнаружены , то никакие действия не предпри нимаются . Когда совпадение с case найдено , ассоциированные с ним операто ры выполняются до тех пор, пока не встретится оператор break, либо в случае default или последнего case пока не будет достигнут конец switch. В следующей программе демонстрируется использование оператора switch: — // Демонстрация использования оператора switch. class SwitchDemo { public static void main ( String[] args) { int i; for(i=0; i<10; i++) switch(i) { case 0: System.out.println("i равно 0 й ); break; case 1: System.out.println("i равно 1"); break; case 2: System.out.println("i равно 2"); break; case 3: System.out.println("i равно 3"); break; Глава 3. Операторы управления программой 105 case 4: System.out.println("i равно 4"); break; default: System.out.println("i равно 5 или больше” ); } } } Ниже показан вывод, генерируемый программой: i i i i i i i i i i равно равно равно равно равно равно равно равно равно равно О 1 2 3 4 5 или больше 5 или больше 5 или больше 5 или больше 5 или больше Как видите , каждый раз в цикле выполняются операторы , связанные с кон стантой в case , которая совпадает со значением i , а все остальные пропуска ются. После того , как значение i становится равным или больше 5, операторы case не дают совпадения , поэтому выполняется оператор default . Формально оператор break необязателен , хотя он используется в большин стве случаев применения switch. Когда внутри последовательности операторов , ассоциированной с case , встречается оператор break, поток выполнения вы ходит из всего оператора switch и возобновляет работу, начиная со следующего оператора после switch. Однако если оператор break не завершает последова тельность операторов , связанную с case , то все операторы в соответствующем case и после него будут выполняться до тех пор, пока не встретится break ( или не закончится switch ). Таким образом , case без break будет проходить прямо на следующий case. Внимательно взгляните на следующую программу. Сможете ли вы предположить, что она отобразит на экране? // Демонстрация работы switch без операторов break , class NoBreak { public static void main(String[] args) { ~~ int i; for(i=0; i<=5; i++) { switch(i) { case 0: System.out.println("i меньше 1"); case 1: System.out.println("i меньше 2"); case 2: System.out.println(” i меньше 3"); _ Сквозное выполнение операторов case 106 Java : руководство для начинающих, 9-е издание case 3: System.out.println("i меньше 4 м ); case 4: System.out.println("i меньше 5"); } System.out.printIn(); } } } Вот какой вывод генерирует программа: i i i i i меньше 1 меньше 2 меньше 3 меньше 4 меньше 5 i меньше i меньше i меньше i меньше 2 3 4 5 i меньше 3 i меньше 4 i меньше 5 i меньше 4 i меньше 5 i меньше 5 Как иллюстрирует данная программа , если оператор break отсутствует, то выполнение продолжится в следующем case. Операторы case могут быть пустыми , как показано в следующем примере. switch(i) { case 1: case 2: case 3: System.out.println("i равно 1, 2 или 3"); break; case 4: System.out.println("i равно 4"); break; } Если i имеет значение 1 , 2 или 3 , то выполняется первый оператор println ( ) , а если 4, тогда второй оператор println ( ) . Операторы case часто размещаются друг поверх друга , как демонстрировалось выше в примере, когда несколько case имеют общий код. Помните! Недавно возможности оператора switch были значительно расширены по сравнению с только что описанной традиционной формой switch. Детальные сведения о расширенном операторе switch приведены в главе 1 6. Глава 3. Операторы управления программой 107 Вложенные операторы switch Оператор switch можно использовать как часть последовательности операторов внешнего switch . Он называется вложенным оператором switch. Даже когда константы case во внутреннем switch и во внешнем switch содержат общие значения , конфликты не возникают. Скажем, следующий фрагмент кода совершенно допустим: switch( chl) { case 'A ': System.out.println("Это значение А относится к внешнему switch."); switch(ch2) { case 'A': System.out.println("Это значение А относится к внутреннему switch."); break; case 'В': // ... } // конец внутреннего switch break; case 'В': // ... Упражнение 3.1 Начало построения справочной системы по управляющим операторам Java В этом проекте создается простая справочная система , которая отображает синтаксис управляющих операторов Java. Программа выводит меню с названиями управляющих операторов и ожидает выбора одного из них, после которого отобразится синтаксис оператора. В первой версии программы справочная информация доступна только для оператора if и традици онного оператора switch. Сведения для других управляющих операторов будут добавлены в последующих проектах. 1 Создайте файл по имени Help . java. Help.java . 2. Программа начинается с отображения следующего меню: Справка по: 1. if 2. switch Выберите вариант: Ниже показана последовательность операторов, которая обеспечит такое отображение: System.out.println("Справка по:"); System.out.println(" 1. if"); System.out.printIn(" 2. switch"); System.out.print("Выберите вариант: "); 3. Затем с помощью вызова System . in . read ( ) в программе получается вы бранный пользователем вариант: choice = (char) System.in.read(); 108 Java: руководство для начинающих, 9-е издание . 4 Далее выводится синтаксис выбранного оператора с применением показанного ниже оператора switch: switch(choice) { case '1': System.out.println("Оператор if:\n"); System.out.println("if(условие) оператор;"); System.out.println("else оператор;"); break; case '2': System.out.println("Традиционный оператор switch:\n"); System.out.println("switch(выражение) {"); System.out.println(" case константа:"); последовательность операторов"); System.out.println(" break;"); System.out.println(" System.out.println(" / / ..."); System.out.println(" }"); break; default: System.out.print("Выбранный вариант не найден."); } Обратите внимание, что оператор default перехватывает недопустимые варианты. Например, если пользователь вводит 3, то никаких совпадений с константами case не будет, что приведет к выполнению последователь- ности в default. 5. Поместите в файл Help . j ava полный код программы: /* Упражнение 3.1. Простая справочная система по управляющим операторам Java. */ class Help { public static void main(String[] args) throws java.io.IOException { char choice; System.out.println ("Справка no:"); System.out.println(" 1. if"); System.out.println(" 2. switch"); System.out.print("Выберите вариант: "); (char) System.in.read(); choice System.out.println("\n"); switch(choice) { case '1': System.out.println("Оператор if:\n"); System.out.println("if(условие) оператор;"); System.out.println("else оператор;"); break; case '2': Глава 3. Операторы управления программой 109 System.out.println("Традиционный оператор switch:\n"); System.out.println("switch(выражение) {"); System.out.println(" case константа:"); System.out.println(" последовательность операторов"); System.out.println(" break;"); System.out.println(" // ..."); System.out.println("}"); break; default: System.out.print("Выбранный вариант не найден."); } } } 6. Вот результат пробного запуска: Справка по: 1. if 2. switch Выберите вариант: Оператор if: if(условие) оператор; else оператор; !?@>A8< C M:A?5@B0 ВОПРОС. В каких ситуациях при многовариантном ветвлении вместо оператора switch должна использоваться цепочка i f -e l s e - i f ? ОТВЕТ. Как правило , цепочка i f - e l s e - i f применяется , когда условия , управляющие процессом выбора , полагаются не на единственное значение . Например , рассмотрим следующую последовательность i f -e l s e - i f : if(х < 10) else if( у ! = 0) else if(!done ) II II II ... ... ... Такую последовательность не удастся переписать в виде switch , потому что во всех трех условиях задействованы разные переменные и разные типы . Какая переменная могла бы управлять оператором switch? Кроме того , це почку i f -e l s e- i f необходимо использовать при проверке значений с пла вающей точкой или других объектов , не относящихся к типам , которые допустимы для применения в выражении , управляющем switch. Цикл for Простая форма цикла f o r использовалась в примерах , начиная с главы 1 . Вас наверняка удивит, насколько мощным и гибким является цикл f o r. Первым делом мы рассмотрим самые традиционные формы f o r . 110 Java : руководство для начинающих, 9-е издание Общая форма цикла for для многократного выполнения одиночного оператора выглядит следующим образом: for { инициализация; условие; итерация ) оператор; А вот общая форма цикла for для многократного выполнения блока операторов: fог(инициализация; условие; итерация) { } последовательность операторов В части инициализация обычно указывается оператор присваивания , устанавливающий начальное значение переменной управления циклом , которая дей ствует в качестве счетчика , управляющего циклом . Часть условие представляет собой булевское выражение , которое определяет, будет ли повторяться цикл . Выражение в части итерация задает величину, на которую переменная управления циклом будет изменяться при каждом повторении цикла . Обратите внимание , что эти три основные части цикла должны разделяться точкой с запятой. Цикл for продолжит выполняться до тех пор, пока условие истинно. Как только условие становится ложным , цикл завершается, и выполнение программы возобновляется с оператора, следующего за оператором for. В показанной ниже программе цикл for применяется для вывода квадратных корней чисел от 1 до 99. Для каждого квадратного корня отображается ошибка округления. // Отображение квадратных корней чисел от 1 до 99 и ошибки округления. class SqrRoot { public static void main(String[] args) { double num, sroot, rerr; for(num = 1.0; num < 100.0; num++) { sroot = Math.sqrt(num); System.out.println("Квадратный корень числа " + num + " равен " + sroot); // Вычислить ошибку округления. rerr = num (sroot * sroot); System.out.println("Ошибка округления составляет " + rerr); System.out.println(); } } } Обратите внимание, что ошибка округления вычисляется путем возведения в квадрат квадратного корня каждого числа. Затем результат вычитается из исходного числа , что и дает ошибку округления. Цикл for может выполняться как в положительном , так и в отрицательном направлении , а переменная управления циклом может изменяться на любую величину. Например, следующая программа выводит числа от 100 до -95 с уменьшением значения переменной управления циклом на 5: Глава 3 . Операторы управления программой 111 // Цикл for, выполняющийся в отрицательном направлении , class DecrFor { public static void main(String[] args) { int x; for(x = 100; x > -100; x -= 5) System.out.println(x); - } На каждой итерации значение переменной управления циклом уменьшается на 5 } Важная особенность циклов for заключается в том, что условное выражение всегда проверяется в начале цикла. Таким образом, код внутри цикла может вообще не выполняться , если условие изначально ложно. Вот пример: for(count=10; count < 5; count++) x += count; // этот оператор выполняться не будет Оператор внутри цикла выполняться не будет, поскольку при первом же входе в цикл управляющая переменная count больше 5. В итоге условное выражение count < 5 оказывается ложным с самого начала и ни одной итерации цикла не произойдет. Некоторые разновидности цикла for — Оператор for один из наиболее универсальных операторов в языке Java , т. к. он допускает множество вариантов. Скажем, можно использовать несколько переменных управления циклом. Взгляните на следующую программу: // Использование запятых в операторе for. class Comma { public static void main(String[] args) { int i, j; — for(i=0, j=10; i < j; i++, j ) System.out.println("i и j: " + i + " " + j); } Обратите внимание на наличие двух переменных управления циклом } Ниже показан вывод, генерируемый программой: i i i i i и j: 010 и и и и j: j: j: j: 19 2 8 3 7 4 6 С помощью запятых разделяются два оператора инициализации и два вы ражения итерации. В начале цикла инициализируются i и j. Каждый раз , когда цикл повторяется , i инкрементируется, a j декрементируется. Наличие нескольких переменных управления циклом часто оказывается удобным и позволяет упростить определенные алгоритмы. Количество операторов инициализации и выражений итерации может быть любым, но на практике больше двух или трех делают цикл for громоздким. 112 Java : руководство для начинающих, 9-е издание В качестве условия , управляющего циклом, может быть указано любое допустимое булевское выражение. Оно не обязано включать переменную управления циклом. В следующем примере цикл продолжает выполняться до тех пор , пока пользователь не введет с клавиатуры букву S: // Цикл до тех пор, пока не будет введена буква S. class ForTest { public static void main(String[] args) throws java.io.IOException { int i; System.out.printIn("Для остановки цикла нажмите S."); for(i = 0; (char) System.in.read() != 'S'; i++) System.out.println("Проход #" + i); } } Пропуск частей Оставляя части определения цикла пустыми , можно получить несколько интересных вариаций цикла for . В Java разрешено пропускать некоторые или даже все части инициализации , условия или итерации цикла f o r . Например, рассмотрим приведенную далее программу: // Части цикла for могут быть пустыми. class Empty { public static void main(String[] args) { int i; Выражение итерации отсутствует for(i = 0; i < 10; ) { System.out.println("Проход #" + i); i++; // инкрементирование переменной управления циклом } } } Как видите , выражение итерации цикла for пустое. Взамен переменная управления циклом i инкрементируется внутри тела цикла. Таким образом , каждый раз , когда цикл повторяется , значение i проверяется на предмет ра венства 10 , но никаких дальнейших действий не происходит. Разумеется , поскольку i инкрементируется в теле цикла, цикл работает нормально, отображая следующий вывод: Проход Проход Проход Проход Проход Проход Проход Проход Проход Проход #0 #1 #2 #3 #4 #5 #6 #7 #8 #9 Глава 3. Операторы управления программой 113 В показанном ниже примере часть инициализации тоже вынесена за преде лы определения цикла for: // Вынесение за пределы определения цикла еще одной части , class Empty2 { public static void main(String[] args ) { int i; I Выражение инициализации вынесено за пределы i = 0; // вынести инициализацию за пределы цикла определения цикла for(; i < 10; ) { System.out.println ("Pass #" + i); i ++; // инкрементировать переменную управления циклом } } } В данной версии i инициализируется до начала цикла , а не как часть определения for. Как правило , переменную управления циклом нужно инициализировать внутри for . Инициализацию размещают вне цикла обычно лишь в ситуациях , когда для получения начального значение применяется сложный процесс , который не поддается заключению внутрь оператора for . Бесконечный цикл С использованием оператора for можно создать бесконечный цикл ( цикл , который никогда не завершается ) , оставив условное выражение пустым . Скажем , следующий фрагмент кода демонстрирует, как многие программисты на Java создают бесконечный цикл : for(;;) // намеренно бесконечный цикл { II ... } Такой цикл будет работать нескончаемо долго . Хотя некоторые программы , подобные командным процессорам операционной системы , требуют бесконечного цикла , большинство “ бесконечных циклов” на самом деле представляют собой просто циклы со специальными требованиями относительно завершения. Ближе к концу главы вы увидите , как остановить цикл подобного рода . ( Подсказка: это делается с помощью оператора break.) Циклы без тела В языке Java тело , ассоциированное с циклом for ( или любым другим циклом ) , может быть пустым . Это связано с тем , что пустой оператор синтаксиче ски действителен . Циклы без тела часто бывают полезными . Например , в при веденной далее программе такой цикл применяется для суммирования чисел от 1 до 5: 114 Java : руководство для начинающих, 9-е издание // Тело цикла может быть пустым , class Empty3 { public static void main(String[] args) { int i; int sum = 0; // Просуммировать числа от 1 до 5. for(i = 1; i <= 5; sum += i++); ** В этом цикле отсутствует тело! System.out.println("Сумма равна " + sum); } } Вот вывод, генерируемый программой: Сумма равна 15 Как видите , процесс суммирования поддерживается внутри определения оператора f o r , так что тело не требуется. Обратите особое внимание на выра жение итерации : sum += i ++ Не пугайтесь операторов подобного рода. Они весьма распространены в про фессионально написанных программах на Java , и их легко понять , разбив на части . Другими словами , такой оператор означает следующее: добавить к sum значение sum плюс i и затем инкрементировать i. Другими словами , он эквива лентен такой последовательности операторов: sum = sum + i; i++; Объявление переменных управления циклом внутри цикла for Часто переменная управления циклом for нужна только для целей цикла и больше нигде не используется . В этом случае переменную можно объявить вну три части инициализации оператора for. Скажем , в следующей программе вы числяется сумма и факториал чисел от 1 до 5 . Переменная управления циклом i объявлена внутри for . // Объявление переменной управления циклом внутри оператора for. class ForVar { public static void main(String[] args) { int sum = 0; int fact = 1; // Вычислить факториал чисел от 1 до 5. for(int i = 1; i <= 5; i++) { sum += i; // переменная i известна во всем цикле fact *= i; } Переменная i объявлена внутри оператора for Глава 3 . Операторы управления программой 115 // Н о уже здесь переменная i не известна. System.out.println("Сумма равна " + sum); System.out.println("Факториал равен " + fact); } } При объявлении переменной внутри цикла for следует помнить о том , что область видимости такой переменной заканчивается с завершением оператора for , т.е. область видимости переменной ограничена циклом for. За пределами цикла for переменная не существует. Таким образом , в предыдущем примере переменная i не будет доступной вне цикла for. Если переменная управления циклом должна применяться в другом месте программы, то объявлять ее внутри цикла for нельзя. Прежде чем двигаться дальше, поэкспериментируйте с собственными вариациями цикла for , открыв для себя все его достоинства. Расширенный цикл for Существует еще одна форма цикла f o r , которая называется расширенным циклом for и обеспечивает упрощенный способ циклического прохода по содержимому коллекции объектов, такой как массив. Расширенный цикл for обсуждается в главе 5 после ознакомления с массивами. Цикл while Еще одним циклом Java является while , общая форма которого показана ниже: whi1е( условие ) опера тор; Здесь оператор может быть одиночным оператором или блоком операторов, а условие определяет условие, которое управляет циклом. Условие может быть любым допустимым булевским выражением. Цикл повторяется , пока условие истинно. Когда условие становится ложным, управление переходит на строку кода , следующую за циклом. Далее представлен простой пример, в котором цикл while используется для вывода букв английского алфавита: // Демонстрация применения цикла while. class WhileDemo { public static void main(String[] args) { char ch; // Вывести буквы английского алфавита, используя цикл while , ch = 'а'; while(ch <= 'z') { System.out.print(ch); ch++; } } } 116 Java : руководство для начинающих, 9-е издание Переменная ch инициализируется буквой а . При каждом проходе цикла пе ременная ch выводится и затем инкрементируется . Процесс продолжается до тех пор , пока ch не станет больше z . Условное выражение в while , как и в for , проверяется в начале цикла , т.е . тело цикла может вообще не выполняться , что избавляет от необходимости вы полнения отдельной проверки перед циклом. Такая особенность цикла while иллюстрируется в следующей программе , вычисляющей целые степени числа 2 от 0 до 9 . // Вычисление целых степеней числа 2. class Power { public static void main(String[] args ) { int e; int result; for(int i=0; i < 10; i++) { result = 1; e = i; while(e > 0) { result *= 2; e ; } — System.out.println("2 в степени " + i + " равно " + result); } } } Вот вывод, генерируемый программой: 2 2 2 2 2 2 2 2 2 2 в в в в в в в в в степени 0 равно степени 1 равно степени 2 равно степени 3 равно степени 4 равно степени 5 равно степени 6 равно степени 7 равно степени 8 равно в степени 9 равно 1 2 4 8 16 32 64 128 256 512 Обратите внимание , что цикл while выполняется только когда е больше 0. Таким образом , когда значение е равно 0 , как в первой итерации цикла for , цикл while пропускается . Цикл do-while Последней разновидностью циклов Java является do -while . В отличие от циклов for и while , где условие проверяется в начале цикла , в цикле do-while условие проверяется в конце цикла. Таким образом , цикл do-while будет вы полняться хотя бы один раз. Ниже показана общая форма цикла do-while: Глава 3. Операторы управления программой 117 do { последовательность операторов } whi1е ( условие ) ; Хотя фигурные скобки не обязательны , когда присутствует только один оператор , они часто применяются для повышения удобочитаемости конструкции do-while , тем самым предотвращая путаницу с циклом while. Цикл do-while выполняется до тех пор , пока условное выражение истинно. В следующей программе цикл выполняется до тех пор , пока пользователь не введет букву q: // Демонстрация использования цикла do-while. class DWDemo { public static void main(String[] args) throws java.io.IOException { char ch; do { System.out.print("Нажмите клавишу и затем ENTER: "); ch = (char) System.in.read(); // получить символ } while(ch != 'q'); } } С использованием цикла do -while можно дополнительно улучшить программу игры в угадывание буквы, созданную ранее в главе. На этот раз цикл повторяется до тех пор, пока буква не будет угадана. // Игра в угадывание буквы , версия class Guess4 { public static void main(String[] throws java.io.IOException { char ch, ignore, answer = 'K'; do { System.out.println("Задумана System.out.print("Попробуйте 4. args) буква между А и Z."); ее угадать: "); // Прочитать символ. ch = (char) System.in.read(); // Отбросить все остальные символы из буфера ввода , do { ignore = (char) System.in.read(); } while(ignore != '\n'); if(ch == answer) System.out.println( « ** Правильно ** n ); else { System.out.print("...Увы , не угадали. Задуманная буква находится "); if(ch < answer) System.out.println("дальше по алфавиту."); else System.out.println("ближе по алфавиту."); System.out.println("Повторите попытку!\n"); } } while(answer != ch); } } 118 Java : руководство для начинающих, 9-е издание Вот результаты пробного запуска: Задумана буква между А и Z. Попробуйте ее угадать: А ...Увы , не угадали. Задуманная буква находится дальше по алфавиту. Повторите попытку! Задумана буква между А и Z. Попробуйте ее угадать: Z ...Увы , не угадали. Задуманная буква находится ближе по алфавиту. Повторите попытку! Задумана буква между А и Z. Попробуйте ее угадать: К ** Правильно ** Обратите внимание на еще один интересный момент. В программе присутствуют два цикла do- while. Первый цикл повторяется до тех пор , пока пользо ватель не угадает букву. Его действие и смысл должны быть ясны . Второй цикл do-while , еще раз показанный ниже , требует некоторого пояснения : // Отбросить все остальные символы из буфера ввода , do { ignore = (char) System.in.read(); } while(ignore != '\n'); Как объяснялось ранее , консольный ввод буферизируется построчно , т. е . для отправки символов потребуется нажать < Enter >, что приведет к генерации символов возврата каретки и перевода строки , которые сохраняются в буфере ввода . Кроме того , если перед нажатием < Enter > было нажато несколько кла виш , то они тоже останутся в буфере ввода . Представленный выше цикл отбра сывает такие символы , продолжая чтение до тех пор , пока не будет достигнут конец строки . Если их не отбросить, то они также отправятся в программу как догадки , а это не то , что нужно . ( Можете удалить внутренний цикл do-while и увидите результат. ) После того как вы больше узнаете о Java , в главе 10 будут описаны более высокоуровневые способы обработки консольного ввода . Тем не менее , применение здесь read ( ) дает представление о том , как работает основа системы ввода - вывода Java , и демонстрирует еще один пример циклов Java . !?@>A8< C M:A?5@B0 ВОПРОС . С учетом гибкости , присущей всем циклам Java , какие критерии сле дует использовать при выборе цикла? Выражаясь иначе , как правильно подобрать цикл для выполнения конкретной работы? ОТВЕТ. Когда количество итераций на основе значения переменной управления циклом известно , применяйте цикл for. Если нужен цикл , который обеспечит выполнение , по меньшей мере , одной итерации , тогда используйте do-while. Цикл while лучше всего применять, когда цикл должен выпол няться до тех пор , пока какое -то условие не станет ложным . Глава 3 . Операторы управления программой 119 Упражнение 3.2 Улучшение справочной системы по управляющим операторам Java В настоящем проекте расширяется справочная система по Help2.java управляющим операторам Java , созданная в упражнении 3.1. В новой версии добавлена выдача информации по синтаксису циклов for , while и do while. Кроме того, проверяется выбор меню пользователем с организацией цикла до тех пор, пока не будет введен правильный ответ. 1 Скопируйте файл Help.java в новый файл по имени Help2.java. 2. Модифицируйте код первой части метода main ( ) , чтобы использовать цикл для отображения вариантов выбора: - . public static void main ( String[ j args) throws java.io.IOException { char choice, ignore; do { System.out.println("Справка no:"); System.out.printIn(" 1. if"); System.out.println(" 2. switch"); System.out.println(" 3. for"); System.out.println(" 4. while"); System.out.println(" 5. do-while\n"); System.out.print("Выберите вариант: ") choice do { (char) System.in.read(); (char) System.in.read(); ignore } while(ignore != '\n'); } while( choice < '1' | choice > '5'); Обратите внимание, что вложенный цикл do-while применяется для отбрасывания любых нежелательных символов, оставшихся в буфере ввода. После внесения этого изменения программа будет отображать меню в цикле до тех пор, пока пользователь не введет вариант от 1 до 5. 3. Добавьте к оператору switch код для вывода информации о циклах for, while и do-while: switch( choice) { case '1': System.out.println("Оператор if:\n"); System.out.println("if(условие) оператор;"); System.out.println ("else оператор;"); break; case 2': System.out.println("Традиционный оператор switch:\n"); System.out.println("switch(выражение) {"); System.out.println(" case константа:"); последовательность операторов"); System.out.println(" System.out.println(" break;"); System.out.println(" // ..."); 120 Java: руководство для начинающих, 9-е издание System.out.println("}"); break; case '3': System.out.println("Цикл for:\n"); System.out . print("for(инициализация; условие; итерация)"); System.out . println(" оператор;"); break; case '4': System.out.println("Цикл while:\n"); System.out.println ("while(условие) оператор;"); break; case '5': System.out.println ("Цикл do-while:\n"); System.out.println("do {"); System.out.println(" оператор;"); System.out.println("} while (условие);"); break; } Обратите внимание, что в данной версии switch отсутствует оператор default . Поскольку цикл отображения меню гарантирует, что будет введен допустимый вариант, больше нет необходимости включать оператор default для обработки недопустимого выбора. 4. Ниже показан полный код программы Help2 . j ava: /* Упражнение 3.2. Улучшенная справочная система по управляющим операторам Java, в которой используется цикл do-while для обработки выбора варианта в меню. */ class Не1р2 { public static void main(String[] args) throws java.io.IOException { char choice, ignore; do { System.out.println("Справка no:"); System.out.println(" 1. if"); System.out.println(" 2. switch"); System.out.println(" 3. for"); System.out.println(" 4. while"); System.out.println(" 5. do-while\n"); System.out.print("Выберите вариант: "); choice (char) System.in.read(); do { ignore = (char) System.in.read(); } while(ignore != '\n * ); } while( choice < '1' | choice > '5'); System.out.println("\n"); Глава 3 . Операторы управления программой 121 switch(choice) { case '1': System.out.println("Оператор if:\n"); System.out.printIn("if (условие) оператор;"); System.out.println("else оператор;"); break; case '2': System.out.println ("Традиционный оператор switch:\n"); System.out.println("switch(выражение) { "); System.out.println(" case константа:"); System.out.println(" последовательность операторов"); System.out.println(" break;"); System.out.println(" // ..."); System.out.println("}"); break; case '3': System.out.println("Цикл for:\n"); System.out.print("for(инициализация; условие; итерация)"); System.out.println(" оператор;"); break; case * 4': System.out.println("Цикл while:\n"); System.out.println ("while(условие) оператор;"); break; case '5': System.out.println("Цикл do-while:\n"); System.out.println("do {"); System.out.println(" оператор;"); System.out.println ("} while (условие);"); break; } } } Использование оператора break для выхода из цикла С применением оператора break можно принудительно завершить цикл , пропуская вычисление условного выражения и выполнение любого оставшегося кода в теле цикла . Когда внутри цикла встречается оператор break , цикл завершается и управление передается оператору, следующему за циклом . Вот простой пример: // Использование break для выхода из цикла , class BreakDemo { public static void main(String[] args) { int num; num = 100; 122 Java : руководство для начинающих, 9-е издание // Цикл до тех пор, пока квадрат i меньше num. for(int i=0; i < num; i++) { if(i *i >= num) break; // прекратить выполнение цикла if i*i >= 100 System.out.print {i + " "); } System.out.println("Цикл завершен."); } } Программа генерирует следующий вывод: 0 1 2 3 4 5 6 7 8 9 Цикл завершен. Хотя цикл for предназначен для выполнения от 0 до num ( в данном случае 100 ), оператор break приводит к его преждевременному завершению , когда значение i в квадрате больше или равно num. Оператор break можно использовать с любыми циклами Java, включая намеренно реализованные бесконечные циклы. Например, ниже показана программа, в которой производится чтение пользовательского ввода , пока не будет введена буква q: // Чтение пользовательского ввода, пока не будет получена буква q. class Break2 { public static void main(String[] args) throws java.io.IOException { char ch; Работа этого "бесконечного" цикла прекращается с помощью break for( ; ; ) { *+ ch = (char) System.in.read(); // получить символ if(ch == * q 1 ) break; } System.out.println("Была нажата клавиша q!"); } } При использовании внутри набора вложенных циклов оператор break производит выход только из самого внутреннего цикла , например: // Использование break с вложенными циклами , class ВгеакЗ { public static void main(String[] args) { for(int i=0; i<3; i++) { System.out.println("Счетчик внешнего цикла: " + i); System.out.print(" Счетчик внутреннего цикла: "); int t = 0; while(t < 100) { if(t == 10) break; // прекратить выполнение цикла, если t равно 10 System.out.print(t + " "); t++; } System.out.println(); } Глава 3 . Операторы управления программой 123 System.out.println("Циклы завершены ."); } } Вот вывод, генерируемый программой: Счетчик внешнего цикла: О Счетчик внутреннего цикла: 0 1 2 3 4 5 6 7 8 9 Счетчик внешнего цикла: 1 Счетчик внутреннего цикла: 0 1 2 3 4 5 6 7 8 9 Счетчик внешнего цикла: 2 Счетчик внутреннего цикла: 0 1 2 3 4 5 6 7 8 9 Циклы завершены . Как видите , оператор break во внутреннем цикле приводит к прекращению только этого цикла, не затрагивая внешний цикл . С оператором break связаны еще два момента , о которых следует помнить. Во- первых, в цикле могут находиться более одного оператора break. Однако будьте осторожны. Слишком большое количество операторов break способно деструктурировать код. Во- вторых, оператор break, завершающий switch, вли яет только на этот оператор switch, но не на любые объемлющие циклы . Использование оператора break как разновидности перехода в стиле "goto" В дополнение к применению с оператором switch и циклами оператор break также может использоваться сам по себе , чтобы обеспечить “ цивилизованную ” форму перехода в стиле “ goto ”. В языке Java нет оператора “ goto ” , т. к. он дает возможность ветвления произвольным и неструктурированным обра зом , что обычно затрудняет понимание и сопровождение кода , опирающегося на переходы в стиле “ goto ” . Тем не менее, есть несколько мест, где переходы в стиле “goto ” будут ценной и законной конструкцией для управления потоком. Например , переход в стиле “ goto ” может быть полезен при выходе из глубоко вложенных циклов. Для обработки таких ситуаций в Java определена рас ширенная форма оператора break. С применением такой формы break можно , например, выходить из одного или нескольких блоков кода , которые не обяза тельно должны являться частью цикла или switch, а могут быть любыми. Более того, можно точно указывать, где будет возобновлено выполнение , т. к. расши ренная форма оператора break работает с меткой. Как вы увидите , break обеспечивает преимущества перехода в стиле “goto ” без присущих ему проблем. Общая форма оператора break с меткой выглядит следующим образом: break метка; Обычно метка представляет собой имя маркера , идентифицирующего блок кода. При выполнении расширенной формы оператора break поток управления покидает блок , указанный в break. Снабженный меткой блок должен 124 Java: руководство для начинающих, 9-е издание охватывать оператор break, но не обязательно быть блоком, который содержит в себе этот break непосредственно. Отсюда следует, например , что оператор break с меткой можно использовать для выхода из набора вложенных блоков. Но применять break для передачи управления из блока , который не охватывает данный оператор break, нельзя . Чтобы назначить блоку имя, необходимо поместить в его начало метку. Помечаемый блок может быть автономным блоком или оператором , телом которого является блок. Метка это любой допустимый идентификатор Java , за которым следует двоеточие. После пометки блока метку можно использовать в качестве цели оператора break, что приведет к возобновлению выполнения после конца помеченного блока. Например , в показанной далее программе реализованы три вложенных блока: — // Использование оператора break с меткой. class Break4 { public static void main(String[] args) { int i; for(i= l; i<4; i++) { one: { { two: { three: System.out.println("\ni равно " + i); if(i==l) break one; Перейти по метке if(i==2) break two; if(i==3) break three; } // Эта строка код никогда не будет достигнута. System.out.println("не выводится"); System.out.println("После блока three."); } System.out.println("После блока two."); } System.out.println("После блока one."); } System.out.println("После цикла for."); } } Вот вывод, генерируемый программой: i равно 1 После блока one. i равно 2 После блока two. После блока one. i равно 3 После блока После блока После блока После цикла three. two. one. for. Глава 3. Операторы управления программой 125 Давайте выясним, почему генерируется такой вывод. Когда значение i равно 1, условие в первом операторе if является истинным, что приводит к переходу в конец блока кода, помеченного меткой one, и выводу сообщения "После блока one". Когда значение i равно 2 , условие во втором операторе if оказывается истинным , что вызывает переход в конец блока кода, помеченного меткой two, и выводу сообщений "После блока two" и "После блока one". Когда значение i равно 3, условие в третьем операторе if становится истинным , что приводит к переходу в конец блока кода , помеченного меткой three, и выводу сообщений "После блока three", "После блока two" и "После блока one". Рассмотрим еще один пример. На этот раз оператор break применяется для выхода из нескольких вложенных циклов for. Когда во внутреннем цикле выполняется оператор break, управление переходит в конец блока , определенного внешним циклом for, который помечен посредством done. В итоге оставшаяся часть всех трех циклов будет пропущена. // Еще один пример использования оператора break с меткой , class Break5 { public static void main(String [] args) { done: for(int i=0; i<10; i++) { for(int j=0; j<10; j++) { for(int k=0; k<10; k++) { System.out.println(k + " "); if(k == 5) break done; // переход по метке done } System.out.println("После цикла k"); // не выполнится } System ,out.println(’’После цикла j"); // не выполнится } System.out.println("После цикла i"); } } Ниже показан вывод, получаемый из программы: о 1 2 3 4 5 После цикла i Место размещения метки очень важно , особенно при работе с циклами . Скажем , возьмем следующую программу: // Место размещения метки очень важно , class Вгеакб { public static void main(String[] args ) { int x=0, y=0; 126 Java : руководство для начинающих, 9-е издание // Метка находится перед оператором for. stopl: for(x=0; х < 5; х++) { for( у = 0; у < 5; у ++) { if(у == 2) break stopl; System.out.println("х и у: " + х + " " + у); } } System.out.println(); // Метка находится непосредственно перед {. for(x=0; х < 5; х++) stop2: { for( у = 0; у < 5; у++) { if ( у == 2) break stop2; System.out.println("х и у: " + х + " " + у); } } } } Вот вывод, генерируемый программой: х и у: 00 х и у: 01 х и х и х и х и х и х и х и х и х и х и у: 00 у: 01 у: 1 0 у: 11 у: 20 у: 2 1 у: 30 у: 31 у: 40 у: 41 Оба набора вложенных циклов в программе одинаковы за исключением одного момента. В первом наборе метка предшествует внешнему циклу for . Когда оператор break выполняется, он передает управление в конец всего блока for , пропуская остальные итерации внешнего цикла. Во втором наборе метка пред- шествует внешней открывающей фигурной скобке оператора for. Таким образом , в результате выполнения оператора break stop2 ; управление передается в конец внешнего блока for , вызывая следующую итерацию. Имейте в виду, что использовать оператор break с меткой, которая определена не для охватывающего блока , не разрешено. Например, следующая програм ма содержит ошибку и компилироваться не будет: // Эта программа содержит ошибку , class BreakErr { public static void main(String[] args) { one: for(int i=0; i<3; i++) { Глава 3. Операторы управления программой 127 System.out.print("Проход " + i + ": "); } for(int j=0; j<100; j++) { if(j == 10) break one; // ОШИБКА System.out.print(j + " "); } } } Поскольку цикл , помеченный как one , не охватывает оператор break, передать управление из этого блока невозможно. !?@>A8< C M:A?5@B0 ВОПРОС. Ранее отмечалось, что оператор goto неструктурирован и break с меткой предлагает лучшую альтернативу. Но не нарушает ли структурирован ность переход по метке , которая может находиться на много строк кода и уровней вложенности дальше от break? ОТВЕТ. Короткий ответ да! Однако в тех случаях, когда требуется резкое изменение в потоке управления программы, переход по метке все же сохраняет некоторую структурированность. У оператора goto ее нет! Использование оператора continue С помощью оператора continue можно организовать выполнение досроч ной итерации цикла , минуя обычную структуру управления циклом. Оператор continue принудительно выполняет следующую итерацию цикла , пропуская любой код между собой и условным выражением , которое управляет циклом. Таким образом , по существу continue является дополнением break. Например, в показанной ниже программе оператор continue применяется для вывода четных чисел от 0 до 100: // Использование оператора continue , class ContDemo { public static void main(String[] args) { int i; // Вывести четные числа между 0 и 100. for(i = 0; i<=l00; i ++) { if((i%2) != 0) continue; // следующая итерация System.out.printIn (i); } } } Выводятся только четные числа , потому что нечетное число приводит к досрочному завершению итерации цикла , минуя вызов print In ( ) . 128 Java : руководство для начинающих, 9-е издание Оператор continue в циклах while и do -while обеспечивает передачу управления непосредственно условному выражению и продолжение цикличе ского процесса . В случае f o r вычисляется выражение итерации цикла , затем вычисляется условное выражение , после чего цикл продолжается. Как и в случае break , в операторе continue может присутствовать метка , которая указывает, какой охватывающий цикл необходимо продолжить. Далее приведен пример программы , где используется оператор continue с меткой: // Использование оператора continue с меткой , class ContToLabel { public static void main(String[] args) { outerloop: for(int i=l; i < 10; i++) { System.out.print(" ХпПроход внешнего цикла #" + i + ", внутренний цикл: "); for(int j = 1; j < 10; j++) { if(j == 5) continue outerloop; // продолжить внешний цикл System.out.print(j); } } } } Вот как выглядит вывод, генерируемый программой: Проход Проход Проход Проход Проход Проход Проход Проход Проход внешнего цикла внешнего цикла внешнего цикла внешнего цикла внешнего цикла внешнего цикла внешнего цикла внешнего цикла внешнего цикла #1, внутренний цикл: 1234 #2, #3, #4, #5, #6, #7, # 8, #9, внутренний внутренний внутренний внутренний внутренний внутренний внутренний внутренний цикл: 1234 цикл: 1234 цикл: 1234 цикл: 1234 цикл: 1234 цикл: 1234 цикл: 1234 цикл: 1234 В выводе видно , что при выполнении continue управление передается внешнему циклу, пропуская оставшуюся часть внутреннего цикла. Подходящие сценарии использования continue встречаются редко . Одна из причин связана с тем , что язык Java предлагает обширный набор операторов циклов , которые подходят для большинства приложений. Тем не менее , в особых обстоятельствах , когда итерацию нужно прекратить заблаговременно , оператор continue обеспечивает структурированный способ решения такой за дачи . Глава 3 . Операторы управления программой 129 Упражнение 3.3 Завершение справочной системы по управляющим операторам Java В настоящем проекте вносятся финальные штрихи в справоч Help3.java ную систему по управляющим операторам Java , созданную в предыдущих проектах. В данной версии добавлены сведения о синтаксисе операторов break и continue, а также возможности запрашивания синтаксиса для нескольких операторов за счет организации внешнего цикла , который выпол няется до тех пор , пока пользователь не введет q при выборе варианта из меню. 1. Скопируйте Help2.java в новый файл по имени Help3.java. 2. Поместите весь код внутрь бесконечного цикла for. Реализуйте выход из этого цикла с применением оператора break, когда пользователь вводит букву q. Поскольку цикл окружает весь код, выход из цикла приводит к прекращению работы программы. 3. Измените цикл отображения меню, как показано ниже: do { System.out.println("Справка по:"); System.out.printIn(" 1. if"); System.out.println(" 2. switch"); System.out.println(" 3. for"); System.out.println(" 4. while"); System.out.println(" 5. do-while"); System.out.println(" 6. break"); System.out.println( " 7. continue\n"); System.out.print("Выберите вариант ( или q для завершения): "); choice (char) System.in.read(); do { ignore = (char) System.in.read(); } while(ignore != '\n'); } while( choice < '1' | choice > '7' & choice != 'q ’); Обратите внимание , что цикл теперь включает операторы break и continue, а также принимает букву q в качестве допустимого варианта . . 4 Расширьте оператор switch, добавив вывод сведений об операторах break и continue: case '6': System.out.println("Оператор break:\n"); System.out.println ("break; или break метка;"); break; case '7': System.out.println("Оператор continue:\n"); System.out.println("continue; или continue метка;"); break; 130 Java: руководство для начинающих, 9-е издание . 5. Ниже показан полный код программы Help3 j ava: /* Упражнение 3.3. Законченная справочная системы по операторам Java, позволяющая обрабатывать множество запросов. */ class Не1рЗ { public static void main(String[] args) throws java.io.IOException { char choice, ignore; f o r (;; ) ( do { System.out.println("Справка no:"); System.out.printIn(" 1. i f " ) ; System.out.println(" 2. switch"); System.out.println(" 3. for"); System.out.println(" 4. while"); System.out.println(" 5. do-while"); System.out.println(" 6. break"); System.out.println(" 7. continueXn"); System.out.print("Выберите вариант (или q для завершения): "); choice = (char) System.in.read(); do { ignore = (char) System.in.read(); } while(ignore != 'Xn 1 ); } while( choice < '1' | choice > '7' & choice != 'q'); if(choice == 'q') break; System.out.println("\n"); switch(choice ) { case '1': System.out.println("Оператор if:\n"); System.out.println("if(условие) оператор;"); System.out.println("else оператор;"); break; case '2': System.out.println("Традиционный оператор switch:\n"); System.out.println("switch(выражение) {"); System.out.println(" case константа:"); System.out.println(" последовательность операторов"); System.out.println(" break;"); System.out.println(" // ..."); System.out.println("}"); break; case '3': System.out.println("Цикл for:\n"); System.out.print("for(инициализация; условие; итерация)"); System.out.println(" оператор;"); break; Глава 3 . Операторы управления программой case 4': System.out.println("Цикл while:\n"); System.out.println("while(условие) оператор;"); break; case '5': System.out.println("Цикл do-while:\n"); System.out.println("do {"); System.out.println(" оператор;"); System.out.println("} while (условие);"); break; case '6': System.out.println("Оператор break:\n"); System.out.println("break; или break метка;"); break; case * 7': System.out.println("Оператор continue:\n"); System.out.println("continue; или continue метка;"); break; } System.out.println(); } } } 6. Вот результаты пробного запуска: Справка по: 1. if 2. switch 3. for 4. while 5. do-while 6. break 7. continue Выберите вариант ( или q для завершения): 1 Оператор if: if(условие) оператор; else оператор; Справка по: 1. if 2. switch 3. for 4. while 5. do-while 6. break 7 . continue Выберите вариант (или q для завершения ): б Оператор break: break; или break метка; 131 132 Java : руководство для начинающих, 9-е издание Справка по: 1. if 2. switch 3. for 4. while 5. do-while 6. break 7. continue Выберите вариант (или q для завершения): q Вложенные циклы В ряде предшествующих примеров несложно было заметить, что один цикл может быть вложен в другой. Вложенные циклы используются для решения самых разных задач программирования и являются неотъемлемой частью программирования . Итак, прежде чем завершить тему операторов цикла Java , давайте рассмотрим еще один пример вложенного цикла. В следующей программе вложенный цикл for применяется при нахождении множителей чисел от 2 до 100: /* Использование вложенного цикла для нахождения множителей чисел от 2 до 100. */ class FindFac { public static void main(String[] args) { for(int i=2; i <= 100; i++) { System.out.print("Множители " + i + ": "); for(int j = 2; j < i; j++) if((i%j) == 0) System.out.print(j + " "); System.out.printIn(); } } } Ниже приведена часть вывода , генерируемого программой: Множители 2: Множители 3: Множители Множители Множители Множители Множители Множители Множители Множители Множители Множители Множители Множители Множители 4: 2 5: 6: 2 3 7: 8: 2 4 9: 3 1 0: 1 1: 1 2: 1 3: 1 4: 1 5: 2 5 2 3 4 6 2 7 3 5 1 6: 2 4 8 Глава 3. Операторы управления программой Множители Множители Множители Множители 133 17: 18: 2 3 б 9 19: 20: 2 4 5 10 Внешний цикл в программе выполняется для значений i от 2 до 100. Во внутреннем цикле последовательно проверяются все числа от 2 до i и выводятся те из них , которые обеспечивают деление значения i нацело. Дополнительная задача: предыдущую программу можно сделать более эффективной. Вы види те , как? ( Подсказка: количество итераций во внутреннем цикле можно уменьшить. ) Вопросы и упражнения для самопроверки 1. Напишите программу, которая читает символы с клавиатуры до тех пор, пока не будет получена точка . Программа должна подсчитывать количе ство пробелов и в конце сообщать итог. 2. Покажите общую форму цепочки if -else-if . 3. К какому оператору if относится последний оператор else в следующем фрагменте кода? if(х < 10) if(у > 100) { if(!done) х = z; else у = z; } else System.out.println("ошибка"); // К какому if относится? 4. Напишите оператор for для цикла , который проходит от 1000 до 0 с ша гом -2. 5. Допустим ли следующий фрагмент кода? for(int i = 0; i < num; i ++) sum += i; count = i; 6. Объясните , что делают обе формы оператора break. 7. Что отобразит показанный ниже фрагмент кода после выполнения опера тора break? for(i = 0; i < 10; i++) { while(running) { if(x <y) break; // . . . } System.out.println("После while"); } System.out.println("После for"); 134 Java : руководство для начинающих, 9-е издание 8. Что выведет следующий фрагмент кода ? for(int i = 0; i<10; i++) { System.out.print(i + " "); 0) continue; if((i%2) System.out.println(); } 9. Выражение итерации в цикле for не всегда должно изменять переменную управления циклом на фиксированную величину. Взамен такая перемен ная может изменяться любым произвольным образом. Напишите программу, которая использует цикл for для генерации и отображения последовательности 1, 2 , 4, 8, 16 , 32 и т.д. 10 В ASCII коды букв нижнего регистра отделены от кодов букв верхнего ре гистра на 32. Таким образом , для преобразования буквы нижнего регистра в букву верхнего регистра , из ее кода понадобится вычесть 32. Используйте эту информацию для написания программы, читающей символы с клавиатуры. Она должна преобразовывать все буквы нижнего регистра в буквы верхнего регистра , а все буквы верхнего регистра в буквы нижнего регистра , отображая результат. Остальные символы изменяться не должны. Программа должна останавливаться , когда пользователь вводит точку. В конце программа должна выводить количество произошедших изменений регистра. . — . 12. 11 Что такое бесконечный цикл ? При использовании оператора break с меткой должна ли метка находиться в блоке, содержащем break? (|'I V'V 0*s : 11A' * SSv •• • * 4 S 4 4 M • »» I *\ .. i »\ s* v* I, I I I ' s. •., i i, . . i 4' ' I I i oiXfS • 9 .• •ggf: * ••• Глава 4 Введение в классы у объекты и методы 136 Java : руководство для начинающих, 9-е издание Б этой главе • • • z Основы классов z Создание объектов z Присваивание ссылочных переменных z Создание методов, возвращение значений и применение параметров z Использование ключевого слова return z Возврат значения из метода z Добавление параметров к методу z Использование конструкторов z Создание параметризованных конструкторов z Операция new z Сборка мусора z Применение ключевого слова this п режде чем вы сможете продвинуться дальше в изучении Java , вам необходимо узнать о классе. Класс представляет саму сущность Java и образует фун дамент, на котором построен весь язык Java , т.к. класс определяет природу объекта. Таким образом , класс формирует основу для объектно-ориентированного программирования на Java . Внутри класса определены данные и код, который воздействует на эти данные . Код содержится в методах. Поскольку классы , объекты и методы являются основополагающими для Java , они рассматриваются в настоящей главе. Базовое понимание указанных средств позволит разрабатывать более сложные программы и лучше понимать некоторые ключевые элементы Java , описанные в следующей главе. Основы классов Так как вся активность программы на Java происходит внутри класса , классы использовались с самого начала книги. Конечно, применялись только очень простые классы, и большинство возможностей классов не было задействовано. Вы увидите , что реальные классы значительно мощнее, чем представленные до сих пор их ограниченные варианты. Давайте начнем с исследования основ. Класс это шаблон , определяю щий форму объекта. Он задает как данные , так и код, который будет работать с такими данными. Спецификация классов в Java используется для создания — Глава 4. Введение в классы , объекты и методы 137 объектов. Объекты являются экземплярами класса. Таким образом, класс по существу представляет собой набор “ чертежей ” , которые указывают, как строить объект. Важно понимать, что класс логическая абстракция. Физическое представление класса появится в памяти лишь после создания объекта этого класса. Еще один момент: вспомните , что методы и переменные , составляющие класс , называются членами класса. Данные- члены также называются перемен ными экземпляра. — Общая форма класса При определении класса вы объявляете его точную форму и природу, для чего указываете переменные экземпляра , которые он содержит, и методы , которые с ними работают. В то время как очень простые классы могут содержать только методы либо только переменные экземпляра , большинство реальных классов содержат то и другое. Класс объявляется с применением ключевого слова class. Ниже приведена упрощенная общая форма определения класса: class имя-класса { // объявление переменных экземпляра B8? переменная1; тип переменная2; // . . . тип переменнаяЫ ; II объявление методов тип метод1 [ параметры ) { II тело метода } тип метод2 [ параметры ] { // тело метода } II ... тип методы [ параметры ) { // тело метода } } Несмотря на отсутствие каких-либо синтаксических правил , которые должны соблюдаться, правильно спроектированный класс должен определять одну и только одну логическую сущность. Скажем , класс , хранящий имена и телефонные номера , обычно не будет хранить также сведения о фондовом рынке , среднем количестве осадков , циклах солнечных пятен или другую несвязанную информацию. Дело в том , что правильно спроектированный класс группирует логически связанную информацию. Помещение несвязанной информации в один и тот же класс быстро приведет к нарушению структурированности кода! Использованные до сих пор классы имели только один метод: main ( ) . Вскоре вы увидите , как создавать другие методы. Однако обратите внимание , что в 138 Java: руководство для начинающих, 9-е издание общей форме класса метод main ( ) не определен. Метод main ( ) требуется только в том случае , если класс является стартовой точкой для программы. Кроме того, некоторым типам приложений Java метод main ( ) не требуется . Определение класса Для иллюстрации работы с классами будет создан класс , который инкапсулирует сведения о транспортных средствах, таких как легковые автомобили , фургоны и грузовики. Класс имеет имя Vehicle и хранит три элемента информации о транспортном средстве: количество пассажиров, которое он может перевозить, запас топлива и средний расход топлива ( в милях на галлон ). Ниже показана первая версия класса Vehicle, в которой определены три переменных экземпляра: passengers, fuelcap и mpg. Обратите внимание , что класс Vehicle не содержит никаких методов. Таким образом, в настоящее вре мя это класс только для данных. ( В последующих разделах к нему будут добавлены методы.) class int int int Vehicle { passengers; fuelcap; mpg; // количество пассажиров // запас топлива в галлонах // расход топлива в милях на галлон } Определение класса создает новый тип данных. В этом случае новый тип данных имеет имя Vehicle, которое будет применяться для объявления объектов типа Vehicle. Не забывайте, что объявление класса представляет собой лишь описание типа; оно не создает реальный объект. Таким образом , предыдущий код не создает никаких объектов типа Vehicle. Чтобы действительно создать объект Vehicle, понадобится использовать оператор следующего вида: Vehicle minivan = new Vehicle(); // создание объекта типа Vehicle // по имени minivan После выполнения приведенного выше оператора переменная minivan будет ссылаться на экземпляр Vehicle. Таким образом, объект обретет “ физическую ” реальность. На данный момент не беспокойтесь о деталях данного оператора . При создании экземпляра класса фактически создается объект, который содержит собственную копию каждой переменной экземпляра , определенной в классе. Таким образом , каждый объект Vehicle будет содержать собственные копии переменных экземпляра passengers, fuelcap и mpg. Для доступа к этим переменным будет использоваться операция точки ( . ) , которая связывает имя переменной экземпляра с именем объекта. Общая форма операции точки выглядит следующим образом: объект.член Глава 4. Введение в классы, объекты и методы 139 — Как видите, объект указывается слева , а член справа. Например , для при сваивания переменной fuelcap объекта minivan значения 1 6 применяется та кой оператор: minivan.fuelcap = 16; Операция точки может использоваться для доступа к переменным экземпля ра и методам. Ниже приведен полный код программы , в которой задействован класс Vehicle: // Программа, в которой используется класс Vehicle , class Vehicle { int passengers; // количество пассажиров // запас топлива в галлонах int fuelcap; // расход топлива в милях на галлон int mpg; } // В этом классе объявляется объект типа Vehicle , class VehicleDemo { public static void main(String[] args) { Vehicle minivan = new Vehicle(); int range; // Присвоить значения полям в minivan , minivan.passengers = 7; minivan.fuelcap = 16; minivan.mpg = 21; Обратите внимание на использование операции точки для доступа к члену } } // Рассчитать дальность при полном баке , range = minivan.fuelcap * minivan.mpg; System.out.println("Минивэн может перевезти " + minivan.passengers + " пассажиров на расстояние " + range + " миль."); Чтобы опробовать программу, можете поместить классы Vehicle и VehicleDemo в один файл исходного кода. Файл , содержащий программу, можно назвать VehicleDemo.java. Такое имя имеет смысл , потому что метод main ( ) находится в классе VehicleDemo, а не в Vehicle. Первым в файле может быть любой класс. Скомпилировав программу с помощью javac, вы обнаружите, что были созданы два файла .class, один для Vehicle и один для VehicleDemo. Компилятор Java автоматически помещает каждый класс в собственный файл .class. Важно понимать, что классы Vehicle и VehicleDemo не обязательно должны находиться в одном и том же файле исходного кода . Классы можно поместить в отдельные файлы с именами Vehicle , java и VehicleDemo.java, но все равно для компиляции программы понадобится скомпилировать VehicleDemo.java. Чтобы запустить программу, потребуется выполнить VehicleDemo.class. В результате отобразится следующий вывод: Минивэн может перевезти 7 пассажиров на расстояние 336 миль. 140 Java: руководство для начинающих, 9-е издание Прежде чем двигаться дальше , давайте вспомним фундаментальный прин цип: каждый объект имеет собственные копии переменных экземпляра , определенных его классом . Таким образом , содержимое переменных в одном объекте может отличаться от содержимого переменных в другом. Между двумя объекта ми нет никакой связи за исключением того факта, что они оба являются объектами одного и того же типа . Например, если есть два объекта Vehicle, то у каждого из них имеется собственная копия переменных passengers, fuelcap и mpg, а их содержимое в двух объектах может отличаться. Упомянутый факт демонстрируется в следующей программе . ( Обратите внимание , что класс с ме тодом main ( ) теперь называется TwoVehicles.) // В этой программе создаются два объекта Vehicle. class int int int } Vehicle { passengers; fuelcap; mpg; // количество пассажиров // запас топлива в галлонах // расход топлива в милях на галлон // В этом классе объявляется объект типа Vehicle , class TwoVehicles { public static void main(String[] args) { Не забывайте, Vehicle minivan = new Vehicle(); что minivan Vehicle sportscar = new Vehicle(); и sportscar int rangel, range2; ссылаются на разные объекты // Присвоить значения полям в minivan , minivan.passengers = 7; minivan.fuelcap = 16; minivan.mpg = 21; // Присвоить значения полям в sportscar. sportscar.passengers = 2; sportscar.fuelcap = 14; sportscar.mpg = 12; // Рассчитать дальности при полном баке , rangel = minivan.fuelcap * minivan.mpg; range2 = sportscar.fuelcap * sportscar.mpg; System.out.println("Минивэн может перевезти " + minivan.passengers + " пассажиров на расстояние " + rangel + " миль."); System.out.println("Спортивный автомобиль может перевезти " + sportscar.passengers + " пассажиров на расстояние " + range2 + " миль."); } } Ниже показан вывод , генерируемый программой: Минивэн может перевезти 7 пассажиров на расстояние 336 миль. Спортивный автомобиль может перевезти 2 пассажиров на расстояние 168 миль. Глава 4. Введение в классы, объекты и методы 141 Как видите , данные minivan полностью отделены от данных sportscar, что иллюстрируется на рис. 4.1. minivan sportscar passengers 7 fuelcap mpg 16 passengers fuelcap mpg 2 14 12 21 Рис. 4.1. Объекты minivan и sportscar имеют полностью отдельные данные Создание объектов В предшествующих программах для объявления объекта типа Vehicle применялась следующая строка: Vehicle minivan = new Vehicle(); Такое объявление выполняет две функции. Во- первых, в нем объявляется переменная по имени minivan типа Vehicle. Переменная minivan не определяет объект, а представляет собой переменную, которая может ссылаться на объект. Во- вторых, в объявлении создается физический экземпляр объекта , а ссылка на него присваивается переменной minivan. Делается это с помощью операции new. Операция new динамически (т. е. во время выполнения ) выделяет память для объекта и возвращает ссылку на нее , которая по существу является адресом в памяти объекта , выделенной new. Затем ссылка сохраняется в переменной . Итак, все объекты классов в Java должны размещаться динамически. Два шага , объединенные в предыдущем операторе , можно записать по отдельности: Vehicle minivan; minivan = new Vehicle(); // объявление ссылки на объект // выделение памяти для объекта Vehicle В первой строке кода переменная minivan объявляется как ссылка на объект типа Vehicle. Таким образом , minivan это переменная , которая способна ссылаться на объект, но сама объектом не является. На данный момент minivan не ссылается на какой-либо объект. Во второй строке кода создается новый объект Vehicle, ссылка на который присваивается переменной minivan. Теперь переменная minivan связана с объектом. — 142 Java: руководство для начинающих, 9- е издание Ссылочные переменные и присваивание В операции присваивания переменные ссылок на объекты действуют не так , как переменные примитивных типов вроде int. В случае присваивания одной переменной примитивного типа другой переменной примитивного типа ситу ация проста. Переменная слева получает копию значения переменной справа. Когда одна переменная ссылки на объект присваивается другой переменной ссылки на объект, ситуация немного усложняется , потому что изменяется объект, на который ссылается первая ссылочная переменная. Такое отличие может порождать некоторые нелогичные результаты. Например , рассмотрим показанный ниже фрагмент кода: Vehicle carl = new Vehicle(); Vehicle car2 = carl ; На первый взгляд может показаться , что carl и саг2 ссылаются на разные объекты , но это не так. Наоборот, carl и саг2 будут ссылаться на один и тот же объект. Присваивание переменной саг2 значения carl просто заставляет саг2 ссылаться на тот же объект, что и carl. В итоге на объект можно воздействовать либо через carl, либо через саг2. Скажем , после того , как выполнится пока занный ниже оператор присваивания: carl.mpg = 26; следующие операторы println ( ) отобразят одно и то же значение 26: System.out.printIn(carl.mpg); System.out.println (car2.mpg); Хотя переменные carl и car2 ссылаются на тот же самый объект, никак иначе они не связаны. Например, последующее присваивание переменной саг2 просто изменяет объект, на который саг2 ссылается: Vehicle carl = new Vehicle(); Vehicle car2 = carl; Vehicle car3 = new Vehicle(); car2 = car3; // теперь саг2 и сагЗ ссылаются на один и тот же объект После выполнения такой последовательности операторов переменная саг 2 будет ссылаться на тот же объект, что и сагЗ. Объект, на который ссылается carl, не изменяется. Методы Как уже объяснялось, составными частями классов являются переменные экземпляра и методы . До сих пор класс Vehicle содержал данные, но не методы. Хотя классы , включающие только данные, вполне допустимы, большинство классов будут иметь методы. Методы представляют собой подпрограммы , которые манипулируют данными , определенными классом , и во многих случаях Глава 4. Введение в классы, объекты и методы 143 обеспечивают доступ к этим данным. В большинстве случаев другие части программы будут взаимодействовать с классом через его методы. Метод содержит один или несколько операторов. В хорошо написанном коде на Java каждый метод решает только одну задачу. У каждого метода есть имя , которое используется для вызова метода. В общем случае методу можно назначить любое имя. Тем не менее , помните о том, что имя main ( ) зарезерви ровано для метода , с которого начинается выполнение программы. Кроме того , не применяйте для имен методов ключевые слова Java. Для обозначения методов в книге используется соглашение , ставшее общепринятым при написании статей о Java. После имени метода указывается пара круглых скобок. Скажем, если методу назначено имя getVal, то в предложении оно будет упоминаться как getVal ( ) . Такая форма записи помогает отличать имена переменных от имен методов. Вот общая форма метода: возвращаемый-тип имя { список-параметров ) { / / тело метода } — Здесь в возвращаемый тип указывается тип данных, возвращаемый методом. Он может быть любым допустимым типом , включая создаваемые вами типы классов. Если метод не возвращает значение, то его возвращаемым типом дол жен быть void. Имя метода указывается в имя. Им может быть любой законный идентификатор кроме тех , которые уже применяются для других элементов в текущей области видимости. Наконец, список-параметров представляет собой последовательность пар типов и идентификаторов, разделенных запятыми . По сути , параметры являются переменными, которые получают значение аргумен тов, переданных методу при его вызове. Если у метода нет параметров, тогда список параметров будет пустым. Добавление метода в класс Vehicle Как только что объяснялось, методы класса обычно манипулируют данными класса и предоставляют к ним доступ. Вспомните , что метод main ( ) в преды дущих примерах вычислял дальность поездки транспортного средства, умножая его расход топлива на запас топлива . Хотя формально способ вполне корректен , с вычислением дальности поездки транспортного средства лучше всего способен справиться сам класс Vehicle. Причину такого вывода понять легко: дальность поездки транспортного средства зависит от емкости топливного бака и скорости расхода топлива , а обе указанных величины инкапсулированы в Vehicle. За счет добавления в Vehicle метода , вычисляющего дальность поездки , улучшается его объектно -ориентированная структура. Чтобы добавить метод в класс Vehicle, укажите его внутри объявления Vehicle. Например , следующая версия класса Vehicle содержит метод по имени range ( ) , который отображает дальность поездки транспортного средства . 144 Java: руководство для начинающих, 9-е издание // Добавление в класс Vehicle метода range(). class Vehicle { int passengers; int fuelcap; int mpg; // количество пассажиров // запас топлива в галлонах // расход топлива в милях на галлон // Отображает дальность поездки , void range() { Метод range() содержится в классе Vehicle System.out.println("Дальность поездки в милях: " + fuelcap * mpg); } t } t Обратите внимание, что fuelcap и mpg используются class AddMeth { напрямую без операции точки public static void main(String[] args) { Vehicle minivan = new Vehicle(); Vehicle sportscar = new Vehicle(); int rangel, range2; // Присвоить значения полям в minivan , minivan.passengers = 7; minivan.fuelcap = 16; minivan.mpg = 21; // Присвоить значения полям в sportscar. sportscar.passengers = 2; sportscar.fuelcap = 14; sportscar.mpg = 12; System.out.print( "Минивэн может перевезти " + minivan.passengers + " пассажиров . "); // отобразить дальность поездки для minivan minivan.range(); System.out.print("Спортивный автомобиль может перевезти " + sportscar.passengers + " пассажиров. "); sportscar.range(); // отобразить дальность поездки для sportscar } } Программа генерирует следующий вывод: Минивэн может перевезти 7 пассажиров. Дальность поездки в милях: 336 Спортивный автомобиль может перевезти 2 пассажиров. Дальность поездки в милях: 168 Давайте рассмотрим ключевые элементы программы, начиная с самого метода range ( ) . Вот первая строка в range ( ) : void range() { В строке объявляется метод с именем range , который не имеет параметров. Его возвращаемый тип void. Таким образом , range ( ) не возвращает значение вызывающему коду. Строка заканчивается открывающей фигурной скобкой тела метода. Тело метода range ( ) состоит из единственной строки: — System.out.println("Дальность поездки в милях: " + fuelcap * mpg); Глава 4. Введение в классы, объекты и методы 145 Показанный оператор отображает дальность поездки транспортного средства , получаемую умножением fuelcap на mpg. Поскольку каждый объект типа Vehicle имеет собственную копию fuelcap на mpg, когда метод range ( ) вызы вается , при вычислении диапазона задействованы копии переменных fuelcap и mpg вызывающего объекта . Метод range ( ) заканчивается , когда встречается закрывающая фигурная скобка , что приводит к передаче управления программой обратно вызывающему коду. Далее взгляните внимательно на следующую строку кода внутри метода main ( ) : minivan.range(); Здесь вызывается метод range ( ) на minivan , т.е. вызов range ( ) производится относительно объекта minivan с использованием имени объекта , за которым следует операция точки. Когда метод вызывается, управление передается методу. Когда метод завершается , управление передается вызывающему коду, и вы полнение возобновляется со строки кода, следующей за вызовом. В данном случае вызов minivan . range ( ) отображает дальность поездки транспортного средства , определенного minivan . Аналогичным образом вы зов sport scar . range ( ) отображает дальность поездки транспортного средства, определенного sportscar. Каждый раз, когда вызывается метод range ( ) , он отображает диапазон для указанного объекта. В методе range ( ) имеется кое - что очень важное , заслуживающее особо го внимания: ссылка на переменные экземпляра fuelcap и mpg производится напрямую, без предшествующего имени объекта или операции точки . Когда в методе применяется переменная экземпляра , определенная его классом, это делается напрямую , без явной ссылки на объект и без использования операции точки . Если подумать, то причину понять легко. Метод всегда вызывается в отношении некоторого объекта своего класса. После того как вызов произошел , объект становится известным. Таким образом , внутри метода нет необходимо сти указывать объект во второй раз. Это означает, что fuelcap и mpg внутри range ( ) неявно относятся к копиям переменных , находящихся в объекте, на котором вызван метод range ( ) . Возврат из метода Возврат из метода происходит в двух ситуациях. Во- первых, как было показано в методе range ( ) в предыдущем примере, когда встречается закрывающая фигурная скобка метода. Во- вторых , в случае выполнения оператора return . Существуют две формы return: одна для применения в методах void ( не возвращающих значение ) и одна для возвращения значений . Здесь рассматривается первая форма , а в следующем разделе объясняется , каким образом возвра щать значения . 146 Java : руководство для начинающих, 9-е издание Немедленно завершить работу метода void можно с использованием следу ющей формы оператора return: return; В результате выполнения такого оператора управление программой возвра щается вызывающему коду, пропуская любой оставшийся код в методе . Напри мер , рассмотрим показанный ниже метод: void myMeth() { int i; for(i=0; i<10; i ++) { if(i == 5) return; // остановиться при достижении значения 5 System.out.printIn(); } } Цикл f o r будет выполняться только для значений i от 0 до 5, потому что при достижении i значения 5 осуществляется возврат из метода. В методе допу стимо иметь несколько операторов return, особенно если они находятся в двух или большем числе ветвей выполнения: void myMeth() { // . . . if(done) return; II ... if(error) return; // . . . } Возврат из метода происходит, когда вся работа закончена или когда воз никла ошибка. Однако будьте осторожны , поскольку чересчур большое коли чество точек выхода из метода может нарушить структурированность кода , так что применяйте их обдуманно . Хорошо спроектированный метод имеет четко определенные точки выхода . Подводя итоги: возврат из метода void может происходить в одном из двух случаев — при достижении его закрывающей фигурной скобки или при выпол нении оператора return. Возврат значения Хотя методы с возвращаемым типом void не так уж редки , большинство методов возвращают значение . На самом деле возможность возвращать значе ние — одна из самых полезных функций метода . Вы уже видели один пример возвращаемого значения , когда для получения квадратного корня использовал ся метод sqrt ( ) . Возвращаемые значения в программировании применяются для самых разных целей . В одних случаях , как при использовании s q r t ( ) , возвращае мое значение содержит результат некоторого вычисления . В других случаях Глава 4. Введение в классы, объекты и методы 147 возвращаемое значение позволяет указывать на успех или неудачу. В третьих случаях оно может содержать код состояния. Какой бы ни была цель , применение возвращаемых методом значений является неотъемлемой частью програм мирования на Java. Методы возвращают значение вызывающему коду с помощью следующей формы оператора return: return значение ; Здесь значение указывает возвращаемое значение . Такая форма return мо жет использоваться только с методами , возвращаемый тип которых отличается от void. Кроме того , метод подобного рода обязан возвращать значение с при менением этой формы return. Возвращаемое значение можно использовать для улучшения реализации range ( ) . Будет лучше , если метод range ( ) вычислит дальность поездки и воз вратит результирующее значение вместо его отображения. Одно из преиму ществ такого подхода заключается в том , что это значение можно применять в других расчетах. В приведенном далее примере метод range ( ) модифицирован для возвращения дальности поездки без его отображения . // Использование возвращаемого значения. class Vehicle { int passengers; int fuelcap; int mpg; // количество пассажиров // запас топлива в галлонах // расход топлива в милях на галлон // Возвращает дальность поездки , int range() { Возвратить дальность поездки для заданного return mpg * fuelcap; транспортного средства } } class RetMeth { public static void main(String[] args) { Vehicle minivan = new Vehicle(); Vehicle sportscar = new Vehicle!); int rangel, range2; // Присвоить значения полям в minivan , minivan.passengers = 7; minivan.fuelcap = 16; minivan.mpg = 21; // Присвоить значения полям в sportscar. sportscar.passengers = 2; sportscar.fuelcap = 14; sportscar.mpg = 12; // Получить дальность поездки для разных транспортных средств , rangel = minivan.range(); Присвоить возвращаемое значение range2 = sportscar.range(); переменной 148 Java: руководство для начинающих, 9-е издание System.out.println("Минивэн может перевезти " + minivan.passengers + " пассажиров на расстояние " + rangel + " миль."); System.out.println( "Спортивный автомобиль может перевезти " + sportscar.passengers + " пассажиров на расстояние " + range2 + " миль."); } } Вот вывод, генерируемый программой: Минивэн может перевезти 7 пассажиров на расстояние 336 миль. Спортивный автомобиль может перевезти 2 пассажиров на расстояние 168 миль. Обратите внимание , что вызов range ( ) находится в правой части операции присваивания. Слева указывается переменная , которая получит значение , возвращаемое методом range ( ) . Таким образом , после выполнения следующего оператора дальность поездки minivan сохраняется в rangel: rangel = minivan.range(); Обратите внимание , что метод range ( ) теперь имеет возвращаемый тип int , т.е. он возвратит вызывающему коду целочисленное значение. Тип возвра щаемого значения метода важен , т. к. тип данных , которые возвращает метод, обязан быть совместимым с указанным типом возвращаемого значения. Таким образом , если вы хотите , чтобы метод возвращал данные типа double , то его возвращаемым типом должен быть double. Несмотря на корректность предыдущей программы , ее реализация не на столько эффективна , как могла бы быть. В частности , нет никакой необходи мости в переменных rangel и range 2 . Вызов метода range ( ) может находиться прямо в операторе println ( ) : System.out.println("Минивэн может перевезти " + minivan.passengers + " пассажиров на расстояние " + minivan.range() + " миль."); В таком случае при выполнении println ( ) метод minivan . range ( ) вызывается автоматически и возвращаемое им значение передается в println ( ) . Вдобавок вызов range ( ) можно использовать всякий раз, когда требуется дальность поездки , связанная с объектом Vehicle . Скажем, следующий оператор сравнивает дальности поездки двух транспортных средств: if(vl.range() > v2.range()) System.out.println("vl имеет большую дальность поездки"); Использование параметров При вызове методу можно передавать одно или несколько значений . Вспом ните , что значение, передаваемое методу, называется аргументом, а переменная в методе , которая получает аргумент параметром . Параметры объявляются внутри круглых скобок , следующих за именем метода. Синтаксис объявления — Глава 4. Введение в классы, объекты и методы 149 параметров аналогичен синтаксису объявления переменных. Областью видимости параметра является метод и, не считая его специальной задачи по получению аргумента , параметр действует подобно любой другой локальной переменной. Ниже показан пример применения параметра. Внутри класса ChkNum метод isEven ( ) возвращает true, если переданное ему значение четное, или false в противном случае. Следовательно , isEven ( ) имеет тип возвращаемого значения boolean. // Простой пример использования параметра. class ChkNum { // Возвращает true, если значение х четное. boolean isEven(int х ) { Здесь x целочисленный параметр метода isEven ( ) if((х%2) == 0) return true; else return false; — } } class ParmDemo { public static void main(String[] args) { ChkNum e = new ChkNum(); 1 if(e.isEven( 10)) System.out.println( "10 - четное."); if(e.isEven(9)) System.out.println("9 if(e.isEven(8)) System.out.println("8 - четное."); Передать аргументы методу isEven ( ) четное."); } } Вот вывод, генерируемый программой: ю 8 четное. четное. Метод isEven ( ) в программе вызывается трижды, и каждый раз ему передается другое значение. Давайте внимательно рассмотрим этот процесс. Первым делом обратите внимание на вызов isEven ( ) . Аргумент указывается в скобках. Когда метод isEven ( ) вызывается в первый раз , ему передается значение 10 , т.е. когда isEven ( ) начинает выполняться, параметр х получает значение 10. Во втором вызове в качестве аргумента указывается 9 и потому х имеет значение 9 . В третьем вызове аргументом является число 8 , которое и будет значением х. Дело в том , что значение , переданное в качестве аргумента при вызове isEven ( ) , будет значением , которое получит его параметр х. В методе может быть объявлено несколько параметров, разделенных запятыми. Например, в классе Factor определен метод isFactor ( ) , который выясня ет, является ли первый параметр множителем второго. 150 Java: руководство для начинающих, 9-е издание class Factor { boolean isFactor(int a, int b) { if( (b % a) == 0) return true; else return false; Этот метод имеет два параметра } } class IsFact { public static void main(String[] args) { Factor x = new Factor(); Передать два аргумента методу isFactor() i 20)) System.out.println( "2 - множитель."); if(x.isFactor(2/ if( x.isFactor(3, 20)) System.out.println( "Это не отобразится."); } } Обратите внимание, что при вызове isFactor ( ) аргументы тоже разделяются запятыми. Когда используется несколько параметров, для каждого параметра указыва ется свой тип, который может отличаться от других. Скажем, приведенное далее определение совершенно допустимо: int myMeth(int a, double b, float с) { II ... } Добавление параметризованного метода в класс Vehicle С помощью параметризованного метода в класс Vehicle можно добавить новую функцию: возможность вычисления объема топлива , необходимого для поездки на заданное расстояние. Новый метод называется FuelNeeded ( ) . Он принимает количество миль, которые должно проехать транспортное средство , и возвращает требуемый объем топлива в галлонах. Метод FuelNeeded ( ) определяется следующим образом: double fuelNeeded(int miles) { return (double) miles / mpg; } Обратите внимание , что метод FuelNeeded ( ) возвращает значение типа double. Это важно , поскольку объем топлива , потребляемого для поездки на данное расстояние , может оказаться не целым числом. Ниже показан полный код класса Vehicle, включающий метод FuelNeeded(): /* Добавление параметризованного метода, который вычисляет объем топлива, необходимого для поездки на заданное расстояние. */ class Vehicle { int passengers; int fuelcap; int mpg; // количество пассажиров // запас топлива в галлонах // расход топлива в милях на галлон Глава 4. Введение в классы, объекты и методы 151 // Возвращает дальность поездки , int range() { return mpg * fuelcap; } //Рассчитывает объем топлива, необходимого для поездки на заданное расстояние double fuelNeeded(int miles) { return (double) miles / mpg; } } class CompFuel { public static void main(String[] args) { Vehicle minivan = new Vehicle(); Vehicle sportscar = new Vehicle(); double gallons; int dist = 252; // Присвоить значения полям в minivan , minivan.passengers = 7; minivan.fuelcap = 16; minivan.mpg = 21; // Присвоить значения полям в sportscar. sportscar.passengers = 2; sportscar.fuelcap = 14; sportscar.mpg = 12; gallons = minivan.fuelNeeded(dist); System.out.println( "Для поездки на расстояние " + dist + " миль минивэну требуется " + gallons + " галлонов топлива."); gallons = sportscar.fuelNeeded(dist); System.out.println("Для поездки на расстояние " + dist + " миль спортивному автомобилю требуется " + gallons + " галлонов топлива."); } } Вот вывод, генерируемый программой: Для поездки на расстояние 252 миль минивэну требуется 12.0 галлонов топлива. Для поездки на расстояние 252 миль спортивному автомобилю требуется 21.0 галлонов топлива. Упражнение 4.1 Создание класса Help Если попытаться обобщить суть класса в одном предложении , то это выглядело бы так: класс инкапсулирует функциональность. Иногда нелегко выяснить, где заканчивается одна “функ циональность ” и начинается другая . Как правило , желательно , чтобы классы были строительными блоками более крупного приложения , для чего каждый класс должен представлять собой единую функциональную единицу, HelpClassDemo.java 152 Java: руководство для начинающих, 9-е издание выполняющую четко очерченные действия. Таким образом , классы должны быть как можно компактнее , но в разумных пределах! То есть классы с излишней функциональностью привносят путаницу и деструктурируют код, а классы , функциональность которых недостаточна , оказываются разбитыми на части . Как же добиться равновесия? Именно в данной точке наука программирования становится искусством программирования. К счастью, большинство програм мистов считают, что с опытом нахождение такого равновесия облегчается. Для приобретения навыков работы с классами справочная система по управляющим операторам Java из упражнения 3.3 будет преобразована в класс Help. Давайте рассмотрим основания для такого решения. Во - первых, справочная система определяет одну логическую единицу. Она просто выводит синтаксис управляющих операторов Java . Таким образом, ее функциональность компактна и четко определена . Во-вторых , помещение справочной системы в класс изящный подход. Всякий раз, когда нужно предложить справочную систему пользователю, понадобится лишь создать объект справочной системы. В-третьих , поскольку справочная система инкапсулирована , ее можно обновлять либо изменять, не вызывая нежелательных побочных эффектов в программах, где она — применяется. . 1 Создайте файл по имени HelpClassDemo.java. Чтобы сэкономить время , можете скопировать файл Help 3 . java из упражнения 3.3 в HelpClassDemo.java. 2. Чтобы преобразовать справочную систему в класс , сначала нужно точ но определить, что входит в состав справочной системы. Например , в Help3.java есть код для отображения меню, ввода выбранного пользователем варианта , проверки правильности ответа и вывода информации о выбранном элементе. Кроме того, в программе имеется цикл , выполня ющийся до тех пор , пока не будет введена буква q. Если подумать, то станет ясно , что неотъемлемой частью справочной системы являются меню , проверка правильности ответа и отображение информации , но не способ получения введенных пользователем данных и обработки повторных за просов. Таким образом , будет создан класс, который предоставляет спра вочную информацию , отображает меню и проверяет правильность выбора. Его методы будут называться соответственно helpOn(), showMenu ( ) и isValid(). 3 Введите код метода helpOn ( ) : . void helpOn(int what) { switch(what) { case '1': System.out.println("Оператор if:\n"); System.out.println("if(условие) оператор;"); System.out.println("else оператор;"); break; Глава 4. Введение в классы , объекты и методы 153 case '2': System.out.println("Традиционный оператор switch:\n"); System.out.println("switch(выражение) {"); System.out.println( " case константа:"); последовательность операторов"); System.out.println(" System.out.println(" break;"); System.out.println(" // ..."); System.out.println("}"); break; case 3': System.out.println("Цикл for:\n"); System.out.print( "for(инициализация; условие; итерация)"); System.out.println(" оператор;"); break; case '4': System.out.println("Цикл while:\n"); System.out.println("while(условие) оператор;"); break; case '5': System.out.println("Цикл do-while:\n"); System.out.println("do {"); System.out.println(" оператор;"); System.out.println("} while (условие);"); break; case '6': System.out.println("Оператор break:\n"); System.out.println("break; или break метка;"); break; case 7': System.out.println("Оператор continue:\n"); System.out.println( "continue; или continue метка;"); break; } System.out.println(); } 4. Введите код метода showMenu(): void showMenu() { System.out.println("Справка no:"); System.out.println( " 1 . i f " ) ; System.out.println(" 2. switch"); System.out.println(" 3. for"); System.out.println(" 4. while"); System.out.println(" 5. do-while"); System.out.println(" 6. break"); System.out.println(" 7. continue\n"); System.out.print( "Выберите вариант (или q для завершения ): } 5. Введите код метода isValid(): boolean isValid(int ch) { ch > '7' & ch != 'q ’ ) return false; if(ch < '1 else return true; } 154 Java: руководство для начинающих, 9-е издание 6. Поместите указанные выше методы в класс Help: class Help { void helpOn(int what ) { switch(what ) { case '1': System.out.println("Оператор if:\n"); System.out.println("if(условие) оператор;"); System.out.println("else оператор;"); break; case '2': System.out.println("Традиционный оператор switch:\n"); System.out.println("switch(выражение) {"); System.out.println(" case константа:"); последовательность операторов"); System.out.println(" break;"); System.out.println(" System.out.println(" // ..."); System.out.println("}"); break; case '3': System.out.println("Цикл for:\n"); System.out.print( "for(инициализация; условие; итерация) "); System.out.println(" оператор;"); break; case '4': System.out.println("Цикл while:\n"); System.out.println("while(условие) оператор;"); break; case '5': System.out.println("Цикл do-while:\n"); System.out.println("do {"); System.out.println(" оператор;"); System.out.println("} while (условие);"); break; case '6': System.out.println("Оператор break:\n"); System.out.println("break; или break метка;"); break ; case '7': System.out.println("Оператор continue:\n"); System.out.println("continue; или continue метка;"); break; } System.out.printIn(); } void showMenu( ) { System.out.println("Справка no:"); System.out.println(" 1. if"); System.out.println( " 2. switch"); System.out.println( " 3. for"); System.out.println(" 4. while"); Глава 4. Введение в классы, объекты и методы 155 System.out.println(" 5. do-while"); System.out.println(" 6. break"); System.out.println( " 7. continue\n"); System.out.print("Выберите вариант ( или q для завершения ): "); } boolean isValid(int ch) { if(ch < '1 ch > '7' & ch != 'q') return false; else return true; } } . 7 Перепишите метод main ( ) из упражнения 3.3, чтобы он задействовал класс Help. Назовите новый класс HelpClassDemo. Ниже приведен пол ный код HelpClassDemo.java: /* Упражнение 4.1. Преобразование справочной системы из упражнения 3.3 в класс Help. */ class Help { void helpOn(int what ) { switch(what) ( case '1': System.out.println("Оператор if:\n"); System.out.println("if(условие) оператор;"); System.out.println("else оператор;"); break; case '2': System.out.println("Традиционный оператор switch:\n"); System.out.println( "switch(выражение) {"); System.out.println(" case константа:"); последовательность операторов"); System.out.println(" System.out.println(" break;"); System.out.println(" // ..."); System.out.println("}"); break; case '3': System.out.println("Цикл for:\n"); System.out.print("for(инициализация; условие; итерация)"); System.out.println(" оператор;"); break; case '4 ': System.out.println("Цикл while:\n"); System.out.println("while(условие) оператор;"); break; case '5': System.out.println( "Цикл do-while:\n"); System.out.println("do {"); System.out.println(" оператор;"); System.out.println("} while (условие);"); break; 156 Java: руководство для начинающих, 9-е издание case '6': System.out.println("Оператор break:\n"); System.out.println("break; или break метка;"); break; case '7': System.out.println("Оператор continue:\n"); System.out.println("continue; или continue метка; "); break; } System.out.println(); } void showMenu() { System.out.println("Справка no:"); System.out.println(" 1. if"); System.out.println(" 2. switch"); System.out.println(" 3. for"); System.out.println(" 4. while"); System.out.println( " 5. do while"); System.out.println( " 6. break"); System.out.println(" 7. continue\n"); System.out.print("Выберите вариант (или q для завершения ): "); } boolean isValid(int ch ) { if(ch < '1' | ch > '7' & ch != 1 q') return false; else return true; } - } class HelpClassDemo { public static void main(String[] args) throws java.io.IOException { char choice, ignore; Help hlpobj = new HelpO ; for(;;) { do { hlpobj.showMenu(); choice (char) System.in.read(); do { ignore = (char) System.in.read(); } while(ignore != '\n'); } while( !hlpobj.isValid(choice) ); if(choice == 'q') break; System.out.println("\n"); hlpobj.helpOn(choice); } } } Запустив программу, вы обнаружите , что ее функциональность осталась прежней . Преимущество этого подхода заключается в том , что теперь у вас есть компонент справочной системы , который при необходимости можно использовать многократно . Глава 4. Введение в классы, объекты и методы 157 Конструкторы В предшествующих примерах переменные экземпляра каждого объекта Vehicle должны были устанавливаться вручную с помощью последовательности операторов следующего вида: minivan.passengers = 7; minivan.fuelcap = 16; minivan.mpg = 21; Подход подобного рода в профессионально написанном коде Java никогда не применяется. Помимо подверженности ошибкам ( вполне легко забыть об установке какого-то поля ) просто есть лучший способ выполнить такую задачу: конструктор. Конструктор инициализирует объект при его создании. Он имеет то же имя , что и класс , и синтаксически похож на метод. Тем не менее, конструкторы не имеют явного возвращаемого типа. Как правило, конструктор используется для присваивания начальных значений переменным экземпляра , которые определены в классе , или для выполнения любых других процедур запуска, необходимых для создания полностью сформированного объекта. Все классы имеют конструкторы вне зависимости от того, определяете вы их явно или нет, потому что Java автоматически предоставляет стандартный конструктор. В таком случае неинициализированные переменные экземпляра по лучают стандартные значения 0 , пи11 и false соответственно для числовых типов, ссылочных типов и типа boolean. После определения собственного конструктора стандартный конструктор больше не применяется. Ниже приведен простой пример использования конструктора: — // Простой конструктор. class MyClass { int х; Это конструктор для MyClass MyClass( ) { х = 10; } } class ConsDemo { public static void main(String[] args) { MyClass tl = new MyClass(); MyClass t2 = new MyClass(); System.out.println(tl.x + " " + t2.x); } } Вот конструктор класса MyClass в данном примере: MyClass() { х = 10; } 158 Java: руководство для начинающих, 9-е издание Конструктор присваивает переменной экземпляра х класса MyClass значение 10. Он вызывается операцией new при создании объекта. Скажем , в следующей строке конструктор MyClass ( ) вызывается на объекте tl и присваивает tl.х значение 10 : MyClass tl = new MyClass(); To же самое справедливо для t2. После создания экземпляра t2.х имеет значение 10. Ниже показан вывод программы: 10 10 Параметризованные конструкторы В предыдущем примере применялся конструктор без параметров. Хотя он подходит в ряде ситуаций , чаще всего будет требоваться конструктор, который принимает один или несколько параметров. Параметры добавляются к кон структору точно так же, как и к методу: нужно просто объявить их в круглых скобках после имени конструктора . В показанном ниже примере класс MyClass получает параметризованный конструктор: // Параметризованный конструктор. class MyClass { int х; MyClass(int i) { i; х Этот конструктор имеет параметр } } class ParmConsDemo { public static void main(String[] args) { MyClass tl = new MyClass(10); MyClass t2 = new MyClass(88); System.out.println (tl.x + " " + t2.x); } } Программа генерирует следующий вывод: 10 88 В этой версии программы конструктор MyClass ( ) принимает один параметр по имени i, который используется для инициализации переменной экземпляра х. Таким образом , когда выполняется приведенная ниже строка кода, параметру i передается значение 10 , которое затем присваивается х: MyClass tl = new MyClass(10); Глава 4. Введение в классы, объекты и методы 159 Добавление конструктора в класс Vehicle Класс Vehicle можно улучшить за счет добавления конструктора , который автоматически инициализирует поля passengers, fuelcap и mpg при создании объекта . Обратите особое внимание на способ создания объектов Vehicle. // Добавление конструктора. class int int int Vehicle { passengers; fuelcap; mpg; // количество пассажиров 11 запас топлива в галлонах // расход топлива в милях на галлон // Конструктор для класса Vehicle. Vehicle(int p, int f, int m) { м passengers = p; fuelcap = f; mpg = m; } — Конструктор для класса Vehicle // Возвращает дальность поездки , int range() { return mpg * fuelcap; } //Рассчитывает объем топлива, необходимого для поездки на заданное расстояние double fuelNeeded(int miles) { return (double) miles / mpg; } } class VehConsDemo { public static void main(String[] args ) { // Создать объекты транспортных средств. Vehicle minivan = new Vehicle(7, 16, 21); Vehicle sportscar = new Vehicle(2, 14 , 12); double gallons; int dist = 252; gallons = minivan.fuelNeeded(dist); System.out.println( "Для поездки на расстояние " + dist + " миль минивэну требуется " + gallons + " галлонов топлива."); gallons = sportscar.fuelNeeded(dist); System.out.println( "Для поездки на расстояние " + dist + " миль спортивному автомобилю требуется " + gallons + " галлонов топлива."); } } Объекты minivan и sportscar инициализируются конструктором Vehicle() при их создании . Каждый объект инициализируется , как указано в параметрах его конструктора. 160 Java: руководство для начинающих, 9-е издание Например , в следующей строке конструктору Vehicle ( ) передаются значе- ния 7 , 16 и 21, когда операция new создает объект: Vehicle minivan = new Vehicle(7, 16, 21); Таким образом , поля passengers , fuelcap и mpg в объекте minivan получат значения 7 , 16 и 21. Вывод программы будет таким же, как в предыдущей версии. Еще раз об операции new Теперь, когда вы знаете больше о классах и конструкторах, давайте рассмотрим операцию new более подробно. В контексте присваивания операция new имеет следующую общую форму: переменная-класса = new имя-класса [ список-аргументов ) ; — Здесь переменная-класса это переменная создаваемого типа класса , а имя класса , экземпляр которого создается. Имя класса , за котоимя-класса рым следует список аргументов в скобках (список может быть пустым ), определя ет конструктор класса. Если в классе не определен собственный конструктор , то операция new задействует стандартный конструктор, предоставляемый Java . Таким образом, операцию new можно применять для создания объекта любого типа класса. Операция new возвращает ссылку на вновь созданный объект, который ( в данном случае ) присваивается переменной, указанной в переменная -класса. Поскольку объем памяти конечен , вполне возможно , что операция new не сможет выделить память для объекта из- за нехватки памяти. В такой ситуации сгенерируется исключение во время выполнения. ( Исключения обсуждаются в главе 9.) В примерах программ , приведенных в книге, вам не придется беспокоиться о проблеме нехватки памяти , но в реальных программах такую возможность нужно будет учитывать. — Сборка мусора Вы уже видели, что объекты динамически выделяются из пула свободной памяти с помощью операции new. Как объяснялось, память не бесконечна , и свободная память может быть исчерпана. Таким образом , операция new может завершиться ошибкой из-за нехватки свободной памяти для создания желаемого объекта. По этой причине ключевым компонентом любой схемы динамического выделения памяти является освобождение памяти от неиспользуемых объектов, что делает ее доступной для последующего распределения. В некоторых языках программирования освобождение ранее выделенной памяти осуществляется вручную. Однако в Java применяется другой и более простой подход: сборка мусора. Система сборки мусора Java автоматически восстанавливает память, используемую объектами , причем прозрачно, “ за кулисами ” и без какого -либо Глава 4. Введение в классы, объекты и методы 161 вмешательства со стороны программиста. Вот как она работает: когда ссылок на объект больше не существует, то считается, что такой объект больше не нужен , и занимаемая им память освобождается, после чего ее можно дальше выделять для объектов. Сборка мусора происходит только в отдельных случаях во время выполне ния программы. Она не инициируется лишь потому, что существует один или несколько объектов , которые больше не используются . Ради эффективности сборщик мусора обычно запускается только при соблюдении двух условий: су ществуют объекты , подлежащие удалению , и есть причина для их удаления. Помните , что сборка мусора требует времени, поэтому исполняющая среда Java делает ее только тогда , когда она уместно. В результате нельзя точно знать, когда произойдет сборка мусора. !?@>A8< C M:A?5@B0 ВОПРОС. Почему операцию new не следует использовать для переменных примитивных типов, таких как int или float? ОТВЕТ. Примитивные типы Java не реализованы в виде объектов. Наоборот, из соображений эффективности они реализованы как “ нормальные ” перемен ные. Переменная примитивного типа в действительности содержит значение, которое было ей присвоено. Как объяснялось, объектные переменные являются ссылками на объект. Такой уровень косвенности ( и другие свой ства объекта) добавляет к объекту накладные расходы , которые не связаны с примитивным типом . Ключевое слово this Прежде чем закончить эту главу, необходимо упомянуть о ключевом слове this. При вызове методу автоматически передается неявный аргумент, являю щийся ссылкой на вызывающий объект (т.е . на объект, на котором вызывается метод ). Такая ссылка называется this. Для лучшего понимания рассмотрим программу, в которой создается класс по имени Pwr, вычисляющий целочисленную степень числа: class Pwr { double b; int e; double val; Pwr(double base, int exp) { b = base; e = exp; val = 1; if(exp==0) return; for( ; exp>0; exp ) val = val * base; } — 162 Java: руководство для начинающих, 9-е издание double getVal() { return val; } } class DemoPwr { public static void main(String[] args) { Pwr x = new Pwr(4.0, 2); Pwr у = new Pwr(2.5, 1); Pwr z = new Pwr(5.7, 0); System.out.println(x.b + " " равно System.out.println(у.b + " " равно System.out.println (z.b + " " равно в степени " + x.e + " + x.getVal()); в степени " + y.e + " + у.getVal()); в степени " + z.e + " + z.getVal()); } } Как вам уже известно, внутри метода можно получить прямой доступ к дру гим членам класса , без какого-либо указания объекта или класса. Таким обра зом , в методе getVal ( ) следующий оператор означает, что возвратится копия поля val, ассоциированного с вызывающим объектом: return val; Однако оператор можно записать и так: return this.val; Здесь this ссылается на объект, на котором был вызван метод getVal(). В итоге this.val ссылается на копию поля val данного объекта. Например , если бы метод getVal ( ) вызывался на х, то ссылка this в предыдущем операторе относилась бы к х. Написание оператора без this на самом деле является просто сокращением. Вот полный код класса Pwr, написанный с применением ссылки this: class Pwr ( double b; int e; double val; Pwr(double base, int exp) { this.b = base; this.e = exp; this.val = 1; if(exp==0) return; for( ; exp>0; exp ) this.val } double getVal() { return this.val; } } — = this.val * base; Глава 4. Введение в классы, объекты и методы 163 В действительности ни один программист на Java не станет записывать класс Pwr, как только что было показано , поскольку никакого выигрыша это не дает, к тому же стандартная форма проще. Тем не менее, с приемом связано несколько важных применений. Скажем , синтаксис Java разрешает использовать имя параметра или локальной переменной , совпадающего с именем переменной экземпляра . Когда подобное происходит, локальное имя скрывает переменную экземпляра . Доступ к скрытой переменной экземпляра можно получать, обращаясь к ней через this. Например, ниже приведен синтаксически допустимый способ написания конструктора Pwr(). Pwr(double b, int e) { this.b = b; this.e = е; Здесь this ссылается на переменную экземпляра b, а не на параметр val = 1; if (е==0) return; for( ; е>0; е--) val = val * b; } В данной версии имена параметров совпадают с именами переменных экземпляра , поэтому последние скрыты , но ключевое слово this применяется для “ раскрытия ” переменных экземпляра . Вопросы и упражнения для самопроверки 1. В чем разница между классом и объектом? 2. Как определяется класс? 3. Собственную копию чего содержит каждый объект? 4. Используя два отдельных оператора , покажите , каким образом объявить объект по имени counter класса MyCounter. 5. Покажите, каким образом объявить метод по имени myMeth ( ) , имеющий возвращаемый тип double и два параметра типа int с именами а и Ь. 6. Как должен заканчиваться метод, если он возвращает значение? 7. Какое имя имеет конструктор? 8. Что делает операция new? 9. Что такое сборка мусора и как она работает? 10. Что такое this? 11. Может ли конструктор иметь один или больше параметров? 12. Если метод не возвращает значение , то каким должен быть его возвращаемый тип? os . •% ' • * I .vv.SSv v*. /SS sS *.. ' I • *. • I » .'' SSV 4 •• »» 4 . * I N I, I I ' s. II I,I II, I .' II 4' I I Л 5 •N, S >>45, X 4X » * 4 l >» 4 Глава 5 Дополнительные сведения о типах данных и операциях " 166 Java: руководство для начинающих, 9-е издание В этой главе • • • z Понятие массивов и их создание z Создание многомерных массивов z Создание массивов нестандартной формы z Альтернативный синтаксис объявления массивов z Присваивание ссылок на массивы z Использование члена массива length z Применение цикла for в стиле “ for-each ” z Работа со строками z Использование аргументов командной строки z Применение выведения типов для локальных переменных z Использование побитовых операций z Применение операции ? в настоящей главе мы возвращаемся к теме типов данных и операций Java . В ней обсуждаются массивы, тип String, выведение типов локальных переменных, побитовые операции и тернарная операция ?. Кроме того, раскрывается цикл for в стиле “ for-each ” и попутно рассматриваются аргументы командной строки . Массивы — Массив это совокупность переменных одного и того же типа , к которой можно обращаться по общему имени. В Java массивы могут иметь одно или несколько измерений , хотя наиболее распространенным считается одномер ный массив. Массивы используются для разнообразных целей , поскольку они предлагают удобные средства группирования связанных переменных. Скажем , массив можно применять для хранения максимальных суточных температур за месяц, списка средних цен акций или списка имеющейся коллекции книг по программированию. Главное преимущество массива заключается в том , что он организует данные таким образом , чтобы ими можно было легко манипулировать. Например, имея массив с доходами для выбранной группы домохозяйств , очень просто вычислить средний доход путем прохода в цикле по массиву. Кроме того , массивы ор ганизуют данные так, чтобы их можно было легко сортировать. Глава 5. Дополнительные сведения о типах данных и операциях 167 Несмотря на то что массивы в Java можно использовать точно так же , как массивы в других языках программирования, они обладают одной особой характеристикой реализацией в виде объектов. Именно данный факт стал од ной из причин того, что обсуждение массивов было отложено до тех пор, пока не были представлены объекты. Реализуя массивы как объекты , можно полу чить несколько важных преимуществ , не последним из которых является то , что неиспользуемые массивы могут быть удалены сборщиком мусора. — Одномерные массивы Одномерный массив представляет собой список связанных переменных. Такие списки широко распространены в программировании. Например , одномерный массив можно применять для хранения учетных записей активных пользователей в сети. Еще один одномерный массив можно использовать для хранения текущих средних показателей бейсбольной команды . Для объявления одномерного массива можно применять следующую общую форму: тип [ ] имя-массива = new тип [ размер ] ; Здесь в тип задается тип элементов массива , который называется также базовым типом. Тип элементов определяет тип данных каждого элемента , содержа щегося в массиве. Количество элементов, которые будет содержать массив, указывается в размер. Поскольку массивы реализованы в виде объектов, массива создается в два этапа. Сначала нужно объявить переменную ссылки на массив, а затем выделить память под массив и присвоить переменной массива ссылку на эту память. Таким образом , память для массивов в Java динамически выделя ется с помощью операции new. Рассмотрим пример. Приведенный далее оператор создает массив типа int из 10 элементов и связывает его с переменной ссылки на массив по имени sample: int[] sample = new int[ 10]; Объявление массива работает аналогично объявлению объекта. Переменная память, выделенную посредством new, которой достаточно, чтобы вместить 10 элементов типа int . Как и в случае с объектами , предыдущее объявление можно разбить на две части: sample содержит ссылку на int[] sample; sample = new int[10]; Когда переменная sample создается впервые, она не ссылается на какой -ли бо физический объект. Лишь после выполнения второго оператора sample свя зывается с массивом. Доступ к индивидуальному элементу в массиве осуществляется с помо щью индекса . Индекс описывает позицию элемента в массиве. Индекс первого 168 Java : руководство для начинающих, 9-е издание элемента массива в Java равен нулю. Так как массив sample содержит 10 элементов, значения индекса будут простираться от 0 до 9. Для индексации масси ва понадобится указать в квадратных скобках номер желаемого элемента. Таким образом, первым элементом в массиве sample является sample [ 0 ] , а последним sample [ 9 ] . Например , в следующей программе загружаются элементы sample с номерами от 0 до 9. — // Демонстрация использования одномерного массива , class ArrayDemo { public static void main ( String [] args ) { int [] sample = new int [ 10 ]; int i; for ( i = 0; i < 10; i sample [ i ] = i; for ( i = i+1) = 1 Индексация элементов в массивах начинается с нуля 0; i < 10; i = i + 1 ) System.out .println ("Элемент sample [ " + i + "]: " + sample [ i ] ); } } Вот вывод, генерируемый программой: Элемент Элемент Элемент Элемент Элемент Элемент Элемент Элемент Элемент Элемент sample [ 0 ] : sample [ 1 ]: sample [ 2 ] : sample [ 3 ] : sample [ 4 ] : sample [ 5 ] : sample [ 6 ] : sample [ 7 ]: sample [ 8 ]: sample [ 9 ] : 0 1 2 3 4 5 6 7 8 9 Концептуально массив sample выглядит так, как показано на рис. 5.1. О о 1 — I Ф I 1 С, вcd со I 2 3 см со Ф Ф (D I 1 а ваз со | 1 a ега со I 1 a ваз со 4 5 си <и I 1 а а со со ваЗ ваз 6 7 8 9 40 г сг> I - 00 Ф Q) 0) а ва аЗ ваа I ваЗ со со ( со Ф a ваз со Рис. 5.1. Массив sample Массивы часто используются в программировании , т.к. они позволяют легко работать с большим количеством связанных переменных. Скажем , в приведенной далее программе производится поиск минимального и максимального значений, хранящихся в массиве nums , за счет прохода по массиву с помощью цикла for: Глава 5. Дополнительные сведения о типах данных и операциях 169 // Поиск минимального и максимального значений в массиве , class MinMax { public static void main(String[] args) { int[] nums = new int[10]; int min, max; nums[0] nums[ 1 ] nums[2] nums[3] nums[4] nums[5] nums[6] nums[7] nums[8] nums[9] = 99; = -10; = 100123; = 18; = -978; = 5623; = 463; = -9; = 2 8 7; = 49; min = max = nums[0]; for(int i=l; i < 10; i++) { if(nums[i] < min) min = nums[i]; if( nums[i] > max) max = nums[i]; } System .out.println("Минимальное и максимальное значения: " + min + " " + max); } } Ниже показан вывод программы: Минимальное и максимальное значения: -978 100123 В предыдущей программе массиву nums присваивались значения вручную с помощью 10 отдельных операторов присваивания. Хотя поступать так совер шенно правильно , есть более простой способ решения задачи. Массивы могут быть инициализированы при их создании. Вот общая форма инициализации одномерного массива: т и п [ ] имя-массива = { значение 1 , значение 2 , значениеЗ , . . . , значениеЫ } ; Здесь начальные значения задаются значениями от значение1 до значениеЫ. Они присваиваются последовательно , слева направо, в порядке возрастания индексов. Для хранения указанных инициализаторов автоматически выделяется массив достаточного размера. Необходимость в явном применении операции new отсутствует. Скажем, далее демонстрируется лучший способ написания программы MinMax. // Использование инициализаторов массива , class MinMax2 { public static void main(String[] args) { int[] nums = { 99, 10, 100123, 18 , -978, 5623, 463, -9, 287, 49 }; int min, max; - min max = nums[0]; Инициализаторы массива 170 Java: руководство для начинающих, 9-е издание for(int i=l; i < 10; i++) { if(nums[i] < min) min = nums[i]; if(nums[i] > max) max = nums[i]; } System.out.println ("Минимальное и максимальное значения: " + min + " " + max); } } Границы массива в Java соблюдаются строго; выход индекса за нижнюю или верхнюю границу приводит к ошибке во время выполнения. При желании самостоятельно в этом удостовериться , опробуйте следующую программу, в которой намеренно осуществляется выход индекса за границы массива: // Демонстрация выхода индекса за границы массива , class ArrayErr { public static void main(String[] args) { int[] sample = new int[10]; int i; // Выход индекса за границы массива. for(i = 0; i < 100; i = i+1) sample[i] = i; } } Как только i достигает значения 10 , генерируется исключение ArraylndexOutOfBoundsException и программа прекращает работу. Упражнение 5.1 Сортировка массива Поскольку в одномерном массиве данные организованы в виде индексируемого линейного списка , такая структура данных идеальна для сортировки . В текущем проекте вы ознакомитесь с простым способом сортировки массива. Наверняка вам известно, что существует множество алгоритмов сортировки вроде быстрой сортировки , сортировки методом перемешивания и сортировки по Шеллу, не считая остальных. Однако самым известным , простым и легким для понимания алгоритмом является пузырьковая сортировка. Несмотря на относительно невысокую эффективность пузырьковой сортировки ( на самом деле ее производительность неприемлема для сортировки крупных массивов) , она вполне подходит для сортировки небольших Bubble.java массивов. 1. Создайте файл по имени Bubble.java. 2 Пузырьковая сортировка так называется из-за способа выполнения операции сортировки. В ней используется многократное сравнение и при необходимости обмен соседними элементами в массиве . В процессе меньшие значения перемещаются в один конец массива , а большие в другой конец. Сам процесс концептуально подобен пузырькам , всплывающим на . — Глава 5. Дополнительные сведения о типах данных и операциях 171 свой уровень в резервуаре с водой. Пузырьковая сортировка выполняет несколько проходов по массиву и в случае потребности заменяет элементы , которые находятся не на своих местах. Число проходов, необходимых для того, чтобы массив был отсортирован , на единицу меньше количества элементов в массиве. Вот код, образующий основу пузырьковой сортировки. Сортируемый массив называется nums. // Пузырьковая сортировка. for(a=l ; а < size; а++) for(b=size-l; b >= a; b — ) { if(nums[b-l ] > nums[b]) { // Если порядок следования не соблюден , // тогда поменять элементы местами. t = nums[b-l]; nums[b-l ] = nums[b]; nums[b] = t; } } Обратите внимание , что в сортировке применяются два цикла f o r . Во внутреннем цикле проверяются соседние элементы в массиве для выяснения, на своих ли они местах находятся. Когда обнаруживается пара элементов, расположенных не на своих местах, два элемента меняются местами. С каждым проходом наименьший из оставшихся элементов перемещается в подходящую позицию. Внешний цикл заставляет процесс повторяться до тех пор, пока не будет отсортирован весь массив. 3. Ниже показан полный код программы пузырьковой сортировки: /* Упражнение 5.1. Сортировка массива. */ class Bubble { public static void main(String[] args) { int[] nums = { 99, -10, 100123, 18 , -978, 5623, 463, -9, 287, 49 }; int a, b, t; int size; size 10; // количество сортируемых элементов // Отобразить исходный массив. System.out.print("Исходный массив:"); for(int i=0; i < size; i++) System.out.print(" " + nums[i]); System.out.println(); // Пузырьковая сортировка. for(a=l; a < size; a ++) for(b=size-l; b >= a; b--) { 172 Java: руководство для начинающих, 9-е издание if(nums[b-l] > nums[b]) { // Если порядок следования не соблюден, // тогда поменять элементы местами. t = nums[b-l]; nums[b-l] = nums[b]; nums[b] = t; } } // Отобразить отсортированный массив. System.out.print("Отсортированный массив:"); for(int i=0; i < size; i++) System.out.print(" " + nums[i]); System.out.printIn(); } } Далее приведен вывод, генерируемый программой: Исходный массив: 99 -10 100123 18 -978 5623 463 -9 287 49 Отсортированный массив: -978 -10 9 18 49 99 287 463 5623 100123 - 4. Хотя пузырьковая сортировка хороша для небольших массивов, для крупных массивов она неэффективна. Гораздо лучшим универсальным алгоритмом является быстрая сортировка , но она опирается на возможности Java , о которых вы еще не знаете. Многомерные массивы Несмотря на то что одномерный массив считается наиболее часто используемым массивом в программировании, многомерные массивы ( массивы с двумя и более измерениями ), безусловно , не редкость. Многомерный массив в Java представляет собой массив массивов. Двумерные массивы Простейшей формой многомерного массива является двумерный массив, который по существу представляет собой список одномерных массивов. Вот как объявить двумерный целочисленный массив table размером 10 x 20: int[][] table = new int[10][20]; Внимательно взгляните на объявление. В отличие от ряда других языков программирования, в которых измерения массива разделяются запятыми, в Java каждое измерение помещается в отдельную пару квадратных скобок. Аналогич ным образом для доступа к элементу 3, 5 массива table должна применяться конструкция table [ 3 ] [ 5 ] . В следующем примере двумерный массив заполняется числами от 1 до 12: // Демонстрация использования двумерного массива. class TwoD { public static void main(String[] args) { int t, i; Глава 5. Дополнительные сведения о типах данных и операциях 173 int[][] table = new int[3][4]; for(t=0; t < 3; ++t) { for(i=0; i < 4; ++i) { table[t ][ i] = (t*4)+i+ l; System.out.print(table[t][i] + " "); } System.out.printIn(); } } } — В этом примере table [ 0 ] [ 0 ] получит значение 1, table [ 0 ] [1] значение 2 , значение 3 и т.д. Значение table [ 2 ] [ 3 ] будет равно 12. Концептуально массив будет выглядеть так, как показано на рис. 5.2. table [ 0 ] [ 2 ] — О 1 2 3 0 1 2 3 4 1 5 6 © 8 2 9 10 11 12 t Правый индекс Левый индекс table[1][2] Рис . 5.2. Концептуальное представление массива table в программе TwoD Массивы нестандартной формы При выделении памяти под многомерный массив необходимо указывать только первое ( крайнее слева ) измерение. Память для остальных измерений можно выделять отдельно. Например , в приведенном ниже коде выделяется память для первого измерения массива t a b l e при его объявлении . Память для второго измерения выделяется вручную. int[][] table = new int[ 3][]; table[0] = new int[4]; table[1] = new int[4]; table[2] = new int[4]; Хотя в данной ситуации индивидуальное выделение памяти под масси вы второго измерения не дает преимуществ , в других случаях оно может быть оправданным. Скажем , при выделении памяти под измерения по отдельности вовсе не обязательно выделять память под одно и то же количество элемен тов для каждого индекса . Поскольку многомерные массивы реализованы как массивы массивов, длина каждого массива находится под вашим контролем. Например , предположим , что вы пишете программу, в которой сохраняется 174 Java: руководство для начинающих, 9-е издание количество пассажиров, совершивших поездку в аэропорт автобусом- экспрессом. Если автобус- экспресс курсирует по 10 раз в будние дни и по 2 раза в субботу и воскресенье, тогда для хранения такой информации можно использовать массив riders , показанный в следующей программе . Обратите внимание , что длина второго измерения для первых пяти индексов равна 10, а длина для последних двух индексов 2. — // Ручное выделение памяти разного объема для индексов второго измерения , class Ragged { public static void main(String [] args) { int[][] riders = new int[7][]; riders[0] = new int[10]; riders[1] = new int[10]; Здесь длина второго измерения riders[2] = new int[10]; составляет 10 элементов riders[3] = new int[10]; riders[4] = new int[10]; riders[5] = new int [ 2 ]~; А здесь длина второго измерения riders[6] = new int[2]; составляет 2 элемента int i, j; // Формирование фиктивных данных. for(i=0; i < 5; i++) for(j=0; j < 10; j++) riders[i][j] = i + j + 10; for(i=5; i < 7; i++) for(j=0; j < 2; j++) riders[i][j] = i + j + 10; System.out.println("Количество пассажиров на рейс в рабочие дни недели:"); for(i=0; i < 5; i++) { for(j=0; j < 10; j++) System.out.print(riders[i][j] + " "); System.out.println(); } System.out.println(); System.out.println ("Количество пассажиров на рейс в выходные дни:"); for(i=5; i < 7; i++) { for(j=0; j < 2; j++) System.out.print(riders[i][j] + " "); System.out.println(); } } } Вполне очевидно , что массивы нестандартной формы ( или ступенчатые массивы ) применимы не ко всем случаям. Тем не менее, в некоторых ситуациях массивы нестандартной формы могут оказаться весьма эффективными. Например , если нужен очень большой разреженный двумерный массив ( т.е. такой, в котором используются не все элементы ) , то идеальным решением может стать массив нестандартной формы. Глава 5 . Дополнительные сведения о типах данных и операциях 175 Массивы с тремя и более измерениями В Java разрешены массивы с количеством измерений больше двух. Вот общая форма объявления многомерного массива: тип[] []...[] имя = new тип [ размер 1 ] [ размер2 ] . .. [ размеры ]; Скажем , ниже объявлен трехмерный целочисленный массив размером 4 x 10 x 3: int[][][] multidim = new int[4][10][3]; Инициализация многомерных массивов Многомерный массив можно инициализировать, заключая список инициа лизаторов каждого измерения в собственную пару фигурных скобок. Например , далее показана общая форма инициализации двумерного массива: - спецификатор типа[] [] имя- массива знач , знач , знач , знач , знач , знач , ..., .. . , знач } , знач }, { знач , знач , знач , . .. , знач } { { }; = { В знач указывается инициализирующее значение. Каждый внутренний блок соответствует строке. В каждой строке первое значение будет сохраняться в первом элементе подмассива , второе значение во втором элементе и т.д. Обратите внимание , что блоки инициализатора разделяются запятыми, а за закрывающей фигурной скобкой следует точка с запятой . Скажем , в следующей программе массив по имени sqrs инициализируется числами от 1 до 10 и их квадратами: — // Инициализация двумерного массива. class Squares { public static void main(String[] args) { int[][] sqrs = { { 1, 1 } , { 2, 4 } , { 3, 9 } , { 4 , 16 } , { 5, 25 }, Обратите внимание, что каждая строка имеет собственный набор инициализаторов { 6 , 36 } , { 3 , 49 } , { 8 , 64 } , { 9 , 81 } , { 10 , 100 } }; int i , j ; 176 Java: руководство для начинающих, 9-е издание for(i=0; i < 10; i++) { for(j=0; j < 2; j++) System.out.print(sqrs[i][j] + " "); System.out.println(); } } } Вот вывод, генерируемый программой: 1 2 3 4 5 i 4 9 16 25 6 36 7 49 8 64 9 81 10 100 Альтернативный синтаксис объявления массивов Ниже показана вторая форма объявления массива: тип имя-переменной [ ] ; Квадратные скобки здесь находятся после имени переменной массива , а не после спецификатора типа. Например, следующие два объявления эквивалентны: int counter[] = new int[3]; int[] counter = new int[3]; Приведенные далее объявления тоже эквивалентны: char tablet ][] = new char[3][4]; char[][] table = new char[3][4]; Такая альтернативная форма объявления удобна при преобразовании кода на C/C ++ в код на Java. ( В C/C ++ массивы объявляются аналогично альтернативной форме Java . ) Она также позволяет объявлять в одном операторе переменные типа массивов и переменные других типов. В настоящее время альтернативная форма объявления массивов применяется реже , но знать ее по- прежнему важно , т. к. в Java допустимы обе формы объявления массивов. Присваивание ссылок на массивы Подобно другим объектам присваивание одной переменной, ссылающейся на массив, значения другой переменной приводит просто к изменению объекта , на который ссылается первая переменная . Копия массива не создается и содержимое одного массива не копируется в другой массив. Взгляните на следующую программу. Глава 5. Дополнительные сведения о типах данных и операциях 177 // Присваивание значений переменным, ссылающимся на массивы class AssignARef { public static void main(String[] args) { int i; int[] numsl = new int[10]; int[] nums2 = new int[10]; , for(i=0; i < 10; i++) numsl[i] = i; for(i=0; i < 10; i++) nums2[i] = -i; System .out.print("Массив numsl: "); for(i=0; i < 10; i++) System.out.print(numsl[i] + " "); System.out.printIn(); System.out.print("Массив nums2: "); for(i=0; i < 10; i++) System.out.print(nums2[i] + " "); System .out.println(); nums2 = numsl; //теперь nums2 ссылается на numsl ^- Присваивание ссылки на массив System.out.print("Массив nums2 после присваивания: "); for(i=0; i < 10; i++) System.out.print(nums2[i] + " "); System.out.println(); // Оперировать массивом numsl через nums2. nums2[3] = 99; System.out.print("Массив numsl после изменения через nums2: "); for(i=0; i < 10; i++) System.out.print(numsl[i] + " "); System.out.println(); } } Вот вывод , генерируемый программой: Массив Массив Массив Массив numsl: 0 1 2 3 4 5 6 7 8 9 nums2: 0 -1 -2 -3 -4 -5 -6 -7 -8 -9 nums2 после присваивания: 0 1 2 3 4 5 6 7 8 9 numsl после изменения через nums2: 012 99 4 5 6 7 8 9 В выводе видно , что после присваивания nums 2 значения numsl обе пере менные ссылок на массивы , ссылаются на один и тот же объект. Использование члена массива length Вспомните , что массивы в Java реализованы как объекты . Одно из пре имуществ такого подхода заключается в том , что с каждым массивом связана переменная экземпляра length , указывающая количество элементов , которые может содержать массив . (Другими словами , l e n g t h хранит размер массива . ) 178 Java : руководство для начинающих, 9-е издание Использование переменной экземпляра length демонстрируется в следующей программе . // Использование члена массива length , class LengthDemo { public static void main(String[] args) { int[] list = new int[10]; int[] nums = { 1, 2, 3 }; int[][] table = { // таблица со строками переменной длины U , 2, 3} , (4, 5}, {6, 7, 8, 9} }; System.out.println ("Длина System.out.printIn("Длина System.out.println("Длина System.out.println("Длина System.out.println("Длина System.out.println("Длина System.out.println(); list: " + list.length); nums: " + nums.length); table: " + table.length); table[0]: " + table[0].length); table[1]: " + table[1].length); table[2]: " + table[2].length); // Использовать length для инициализации list , for(int i=0; i < list.length; i++) list[i] = i * i; System.out.print("Содержимое list: "); // Использовать length для отображения содержимого list , for(int i=0; i < list.length; i + + ) System.out.print(list[i] + " "); System.out.println(); — Использование - length для управ ления циклом for } } Программа генерирует такой вывод: Длина Длина Длина Длина Длина Длина list: 10 nums: 3 table: 3 table[0]: 3 table[1 ]: 2 table[2]: 4 Содержимое list: 0 1 4 9 16 25 36 49 64 81 Обратите особое внимание на применение length с двумерным массивом table . Как объяснялось ранее , двумерный массив — это массив массивов . Таким образом , приведенное ниже выражение получает количество массивов , хранящихся в table , которое в данном случае равно 3 : table.length Для получения длины отдельного массива в table используется выражение следующего вида, получающее длину первого массива: table[0].length Глава 5. Дополнительные сведения о типах данных и операциях 179 В классе LengthDemo стоит обратить внимание еще и на применение list , length для управления количеством выполняемых итераций циклами for. Поскольку каждый массив имеет свою длину, ее можно задействовать вместо руч ного отслеживания размера массива. Имейте в виду, что значение length никак не связано с количеством фактически используемых элементов, а отражает количество элементов, которые способен содержать массив. Член length упрощает многие алгоритмы , делая операции с массивами определенных типов проще и безопаснее для выполнения. Например , в пока занной далее программе length применяется для копирования одного массива в другой , предотвращая исключение времени выполнения по причине выхода индекса за пределы массива . // Использование члена length для упрощения копирования массива. class АСору { public static void main(String[] args) { int i; int[] numsl = new int[10]; int[] nums2 = new int[10]; for(i=0; i < numsl.length; i++) numsl[i] = i; // Копировать numsl в nums2. if( nums2.length >= numsl.length) for(i = 0; i < numsl.length; i++) - Использование length для сравнения размеров массивов nums2[i] = numsl[i]; for(i=0; i < nums2.length; i++) System.out.print(nums2[i] + " "); } } Здесь переменная экземпляра length помогает выполнять две важные функции. Во- первых, она используется для подтверждения того, что целевой массив достаточно велик , чтобы вместить содержимое исходного массива . Во-вторых, она обеспечивает условие завершения цикла for, реализующего копирование. Конечно, в таком простом примере размеры массивов выяснить легко, но ана логичный подход можно применить к широкому кругу более сложных ситуаций. Упражнение 5.2 Создание класса очереди Как вам уже известно , структура данных это средство органи зации данных. Простейшей структурой данных является массив, — QDemo.java выглядящий как линейный список , который поддерживает произвольный доступ к своим элементам . Массивы часто используются в качестве основы для более сложных структур данных, таких как стеки и очереди . Стек представляет собой список, в котором доступ к элементам возможен только в порядке “ перпоследним обслужен ” , а очередь — список, где доступ к элемен вым пришел первым обслужен ” . Таким там осуществляется в порядке “ первым пришел — — 180 Java : руководство для начинающих, 9-е издание — образом , стек похож на стопку тарелок на столе нижняя тарелка используется последней. Очередь похожа на очередь в магазине: кто первый стоит в очереди , тот первым и обслуживается . Структуры данных вроде стеков и очередей наиболее интересны тем , что они сочетают в себе хранилище информации и методы доступа к хранимой информации. Другими словами , стеки и очереди являются механизмами доступа к данным, в которых сохранение и извлечение обеспечиваются самой структурой данных, а не в написанном коде программы. Безусловно , такое сочетание отличный вариант для класса , и в текущем проекте будет создан простой класс , представляющий очередь. Очереди поддерживают две основные операции: помещение и извлечение. Каждая операция помещения размещает новый элемент в конце очереди , а каждая операция извлечения извлекает элемент из начала очереди. Операции с очередью являются разрушающими: после извлечения элемента его нельзя извлечь снова. Очередь также может переполниться в случае отсутствия места для сохранения элемента , и стать пустой, если все элементы были удалены . И последнее замечание: существуют два основных типа очередей циклические и нециклические. В циклической очереди после удаления элементов позиции во внутреннем массиве используются повторно, а в нециклической очереди так не происходит и пространство в конечном итоге исчерпывается. Ради простоты в примере создается нециклическая очередь, но вы можете легко преобразовать ее в циклическую очередь, чуть подумав и приложив небольшие усилия. — — . . 1 Создайте файл по имени QDemo.java. 2 Хотя существуют другие способы поддержки очереди, применяемый далее метод базируется на массиве , т.е. массив обеспечит хранилище для элементов, помещенных в очередь. Доступ к этому массиву будет осуществляться через два индекса. Индекс помещения определяет, где сохранится следующий элемент данных , а индекс извлечения откуда будет получен следующий элемент данных. Имейте в виду, что операция извлечения является разрушающей и потому получить один и тот же элемент дважды невозможно. Хотя создаваемая очередь будет хранить символы , ту же логику можно использовать для хранения объектов любого типа. Ниже показаны строки , с которых начинается создание класса Queue: — class Queue { // массив, хранящий данные очереди char[] q; int putloc, getloc; // индексы для позиций помещения и извлечения 3. Конструктор класса Queue создает очередь заданного размера: Queue(int size) { q = new char[size]; // выделение памяти под очередь putloc = getloc = 0; } Глава 5 . Дополнительные сведения о типах данных и операциях 181 . 4 Обратите внимание , что индексы помещения и извлечения изначально установлены в ноль. Ниже показан код метода put ( ) , который сохраняет элемент: // Поместить символ в очередь , void put(char ch ) { if( putloc==q.length) { System.out.println(" - Очередь переполнена."); return; } q[putloc++] ch; } Метод начинается с проверки, не переполнена ли очередь. Если значение putloc указывает на последнюю позицию в массиве q, то место для сохранения элементов отсутствует. В противном случае новый элемент сохраняется в позиции putloc и значение putloc инкрементируется. Таким образом, putloc всегда будет индексом , по которому сохраняется очередной элемент. 5 Далее приведен код метода get ( ) , который извлекает элемент: . // Извлечь символ из очереди , char get() { if(getloc == putloc) { Очередь пуста."); System.out.println(" return (char) 0; - } return q[getloc++]; } Сначала производится проверка , не пуста ли очередь. Если getloc и putloc указывают на один и тот же элемент, тогда считается, что очередь пуста. Именно потому getloc и putloc инициализировались нулем в конструкторе класса Queue. Затем возвращается следующий элемент и getloc инкрементируется. Таким образом , getloc всегда соответствует позиции очередного извлекаемого элемента. 6. Вот полный код программы QDemo.java: /* Упражнение 5.2. Класс очереди для символов. */ class Queue { // массив, хранящий данные очереди char[] q; int putloc, getloc; // индексы для позиций помещения и извлечения Queue(int size) { q = new char[size]; // выделение памяти под очередь putloc = getloc = 0; } 182 Java: руководство для начинающих, 9-е издание // Поместить символ в очередь , void put(char ch) { if(putloc==q.length) { System.out.printin(" - Очередь переполнена."); return; } q[putloc++] ch; } // Извлечь символ из очереди , char get() { if(getloc == putloc ) { System.out.println(" - Очередь пуста."); return (char) 0; } return q[getloc++]; } } // Демонстрация использования класса Queue , class QDemo { public static void main(String[] args) { Queue bigQ = new Queue(100); Queue smallQ = new Queue(4); char ch; int i; System.out.println("Использование bigQ для сохранения алфавита."); // Поместить в bigQ коды букв , for(i=0; i < 26; i++) bigQ.put((char) ('A' + i)); // Извлечь и отобразить элементы bigQ. System.out.print("Содержимое bigQ: "); for(i=0; i < 26; i++) { ch = bigQ.getO ; if(ch != (char) 0) System.out.print(ch); } System.out.println("\n"); System.out.println("Использование smallQ для генерации ошибок."); // Использовать smallQ для генерации ошибок. for(i=0; i < 5; i++) { System.out.print("Попытка сохранения " + (char) ('Z * i) > ; smallQ.put((char) ('Z i> > ; System.out.println(); } System.out.println(); // Дополнительные ошибки в smallQ. System.out.print("Содержимое smallQ: Глава 5 . Дополнительные сведения о типах данных и операциях 183 for(i=0; i < 5; i++) { ch = smallQ.get(); if(ch != (char) 0) System.out.print(ch); } } } 7. Ниже показан вывод, генерируемый программой: Использование bigQ для сохранения алфавита. Содержимое bigQ: ABCDEFGHIJKLMNOPQRSTUVWXYZ Использование smallQ для генерации ошибок. Попытка Попытка Попытка Попытка Попытка сохранения сохранения сохранения сохранения сохранения Z Y X W V - Содержимое smallQ: ZYXW Очередь переполнена. - Очередь пуста. 8. Попробуйте самостоятельно изменить класс Queue так, чтобы он мог хра нить объекты других типов, скажем, int или double. Цикл for в стиле " for- each" При работе с массивами часто возникают ситуации , когда элементы массива должны быть проверены от начала до конца. Скажем , чтобы вычислить сумму значений , содержащихся в массиве , понадобится пройти по всем элементам массива. Аналогичная ситуация возникает при расчете среднего , поиске значения , копировании массива и т.д. Поскольку такие операции “ от начала до кон ца ” весьма распространены , в Java определена вторая форма цикла for, которая упрощает операцию подобного рода . Вторая форма for реализует цикл в стиле “ for- each ” , который проходит по коллекции объектов вроде массива строго последовательно от начала до конца. По причинам , которые станут ясны позже , циклы в стиле “ for-each ” стали популярными как среди проектировщиков языков программирования, так и среди самих программистов. Интересно , что в языке Java изначально не предлагался цикл в стиле “ for-each ”. Однако несколько лет назад ( начиная с JDK 5) цикл for был усовершенствован , чтобы обеспечить такую возможность. Стиль “ for-each ” цикла for также называется расширенным циклом for. В книге применяются оба термина. Ниже показана общая форма цикла в стиле “ for-each ”: for( тип переменная-итерации : коллекция ) оператор-или-блок-операторов 184 Java: руководство для начинающих, 9-е издание — В тип указывается тип , а в переменная-итерации имя переменной ите рации , которая будет получать элементы из коллекции по одному, от нача ла до конца . Коллекция , по которой проходит цикл , задается в конструкции коллекция. Существуют различные типы коллекций , которые можно использо вать с циклом for , но в книге применяется только массив. На каждой итерации цикла очередной элемент коллекции извлекается и сохраняется в переменной , указанной в переменная -итерации. Цикл повторяется до тех пор , пока не бу дут получены все элементы коллекции . Таким образом , при проходе по массиву размера JV расширенный цикл for получает элементы в массиве в порядке следования индекса , т.е . от 0 до N \ . Поскольку переменная итерации получает значения из коллекции , тип дол жен совпадать ( или быть совместимым ) с типом элементов , хранящихся в кол лекции. Таким образом , при проходе по массиву тип обязан быть совместимым с типом элементов массива. Чтобы понять причины появления цикла for в стиле “ for- each ” , рассмотрим разновидность цикла for , которую “ for- each ” призван заменить. В следующем фрагменте кода для вычисления суммы значений в массиве используется традиционный цикл for: — int[] nums = { 1, 2 , 3, 4, 5, 6, 7, 8 , 9 , 10 }; int sum = 0; for(int i=0; i < 10; i++) sum += nums[ij ; Для вычисления суммы производится чтение элементов массива nums по порядку, от начала до конца . Таким образом , все элементы массива читаются в строго последовательном порядке , что достигается ручной индексацией массива nums посредством переменной управления циклом i. Кроме того , начальное и конечное значение переменной управления циклом и ее инкрементирование должны быть указаны явно . Стиль “ for- each ” цикла for автоматизирует предыдущий цикл . В частности , устраняется необходимость в установке счетчика цикла , указании начального и конечного значений и индексации массива вручную . Взамен он автоматически проходит по всему массиву, последовательно получая по одному элементу за раз , от начала до конца . Например , вот предыдущий фрагмент кода , переписан ный с использованием версии “ for-each ” цикла for: int[] nums = { 1, 2 , 3, 4, 5, 6 , 7, 8 , 9 , 10 }; int sum = 0; for(int x: nums) sum += x; На каждом проходе цикла переменной х автоматически присваивается зна чение очередного элемента массива nums . Таким образом , на первой итерации переменная х содержит 1 , на второй итерации — 2 и т.д. Мало того , что синтаксис оптимизирован , он также предотвращает ошибки выхода за границы массива. Глава 5 . Дополнительные сведения о типах данных и операциях 185 !?@>A8< C M:A?5@B0 ВОПРОС. По каким другим типам коллекций кроме массивов можно проходить с помощью цикла for в стиле “for-each ” ? ОТВЕТ. Одним из наиболее важных применений цикла f o r в стиле “ for-each ” является проход по содержимому коллекции, определенной в Collections Framework. Инфраструктура Collections Framework представляет собой набор классов, которые реализуют различные структуры данных, такие как списки, векторы, наборы и карты. Обсуждение инфраструктуры Collections Framework выходит за рамки настоящей книги, но ее подробное описание можно найти в книге Java. Полное руководство, 12-е изд. ( “Диалектика ” , 2023 г.). В приведенной ниже программе демонстрируется только что описанный цикл for в стиле “ for-each ”: // Использование цикла for в стиле "for-each". class ForEach { public static void main(String[] args) { int[] nums = { 1, 2, 3 , A , 5, 6 , 1 , 8 , 9, 10 }; int sum = 0; // Применение цикла for в стиле "for-each" для отображения // и суммирования значений , Цикл for в стиле "for-each" for(int х : nums) { «* System.out.println("Значение: " + x); sum += x; 1 System.out.println("Сумма: " + sum); } } Вот вывод, генерируемый программой: Значение: Значение: Значение: Значение: Значение: Значение: Значение: Значение: Значение: Значение: Сумма: 55 1 2 3 4 5 б 7 8 9 10 По выводу легко понять, что цикл for в стиле “ for-each ” последовательно проходит по массиву от наименьшего до наибольшего значения индекса . 186 Java : руководство для начинающих, 9-е издание Хотя цикл f o r в стиле “ for- each ” повторяется до тех пор, пока не будут ис следованы все элементы массива , его работу можно прекратить досрочно с использованием оператора break. Скажем , следующий цикл суммирует только первые пять элементов массива nums: // Суммировать только первые 5 элементов. for(int х : nums) { System.out.println("Значение: " + x); sum += x; if(x == 5) break; //остановить работу цикла, когда х получает значение 5 } Есть один важный момент, связанный с циклом for в стиле “ for- each ” . Его переменная итерации доступна только для чтения , т. к . она относится к лежащему в основе массиву. Присваивание переменной итерации какого- то значения не влияет на лежащий в основе массив. Другими словами , за счет присваивания переменной итерации нового значения изменить содержимое массива нельзя . Например , взгляните на показанную далее программу: // Цикл for в стиле "for-each" по существу допускает только чтение , class NoChange ( public static void main(String[] args) { int[] nums = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; for(int x : nums) { System.out.print(x + " "); x * 10; X Эта операция не приводит к изменению массива nums } System.out.println(); for(int x : nums) System.out.print(x + " "); System.out.println (); } } В первом цикле for значение переменной итерации умножается на 10 , но это присваивание не влияет на нумерацию лежащего в основе массива , что подтверждает второй цикл for и представленный ниже вывод: 1 2 3 4 5 6 7 8 9 10 1 2 3 4 5 6 7 8 9 10 Проход по многомерным массивам Расширенный цикл f o r также работает с многомерными массивами . Тем не менее , помните о том , что в Java многомерные массивы являются массива ми массивов . ( Скажем , двумерный массив представляет собой массив одномерных массивов. ) Данный аспект важен при проходе по многомерному массиву, поскольку на каждой итерации получается очередной массив , а не индивиду альный элемент. Кроме того , переменная итерации в цикле f o r должна быть Глава 5 . Дополнительные сведения о типах данных и операциях 187 совместимой с типом получаемого массива. Например , в случае двумерного массива переменная итерации должна быть ссылкой на одномерный массив. В случае применения цикла for в стиле “ for-each ” для прохода по массиву с N измерениями полученные объекты будут массивами из N- 1 измерений. Чтобы понять последствия данного факта , рассмотрим показанную ниже программу, где вложенные циклы for используются для получения элементов двумерного массива в порядке следования строк с первой до последней. // Использование цикла for в стиле "for-each" для двумерного массива , class ForEach2 { public static void main(String[] args) { int sum = 0; int[][] nums = new int[3][5]; // Поместить некоторые значения в nums. for(int i = 0; i < 3; i++) for(int j=0; j < 5; j++) nums[i][j] = (i+1)*(j+1); // Применение цикла for в стиле "for-each" для отображения // и суммирования значений. for(int[] х : nums) { Обратите внимание на способ объявления переменной х for(int у : x) { System.out.println("Значение: " + у); sum += у; } } System.out.println("Сумма: " + sum); } } Вот вывод, генерируемый программой: Значение: 1 Значение: 2 Значение: 3 Значение: 4 Значение: 5 Значение: 2 Значение: 4 Значение: 6 Значение: 8 Значение: 10 Значение: 3 Значение: б Значение: 9 Значение: 12 Значение: 15 Сумма: 90 Обратите особое внимание в программе на следующую строку: for(int[] х : nums) ( 188 Java: руководство для начинающих, 9-е издание Переменная х объявляется как ссылка на одномерный массив целых чисел . Причина в том, что на каждой итерации цикла for получается очередной мас сив из nums, начиная с nums[0]. Затем во внутреннем цикле for выполняется проход по каждому такому массиву с отображением значений всех элементов. Применение расширенного цикла for Поскольку цикл for в стиле “ for-each ” способен проходить по массиву только последовательно от начала до конца , может показаться, что его использование ограничено. Однако это не так. Большое количество алгоритмов требует именно такого механизма. Одним из самых распространенных является поиск. Например , в следующей программе цикл for применяется для поиска значения в несортированном массиве. Цикл останавливается, когда значение найдено. // Поиск в массиве с использованием цикла for в стиле "for-each". class Search { public static void main(String[] args) { int[] nums = { 6, 8 , 3 , 1 , 5, в , 1, 4 }; int val = 5; boolean found = false; // Применить цикл for в стиле "for-each" для поиска в nums значения val. for(int x : nums) { if( x == val) { found = true; break; } } if( found) System.out.println("Значение найдено!"); } } — Цикл for в стиле “ for-each ” подходящий выбор в этом приложении , потому что поиск в несортированном массиве предусматривает последовательное исследование каждого элемента. ( Разумеется , для отсортированного массива можно было бы использовать двоичный поиск, что потребовало бы отличающегося цикла.) К другим типам приложений, которые выигрывают от применения цикла for в стиле “ for- each ” , относятся вычисление среднего, нахождение ми нимума или максимума в наборе , поиск дубликатов и т.п. После представления цикла for в стиле “ for-each ” он будет использоваться там, где уместно , в оставшейся части книги . Строки Одним из наиболее важных типов данных в Java при повседневном програм мировании является тип String, который определяет и поддерживает символьные строки. В ряде других языков программирования строка представляет собой массив символов. К Java это не относится. В Java строки являются объектами. Глава 5. Дополнительные сведения о типах данных и операциях 189 В действительности класс String применялся , начиная с главы 1. При создании строкового литерала фактически создается объект String. Например, в следующем операторе строка "В Java строки являются объектамиавтоматически превращается в объект String: System.out.println("В Java строки являются объектами."); Таким образом , класс String незримо использовался в предшествующих программах. В последующих разделах вы научитесь обрабатывать String явно. Тем не менее , имейте в виду, что класс String довольно крупный , и здесь мы рассмотрим его лишь поверхностно. Класс String целиком вам придется изучать самостоятельно. Конструирование строк Объект String можно создавать в точности как объект любого другого типа: применяя операцию new и вызывая конструктор класса String. Например: String str = new String("Приветствуем!"); Такой оператор создает объект String по имени str, который содержит символьную строку "Приветствуем! ". Вдобавок строку можно создать из другой строки: String str = new String("Приветствуем!"); String str2 = new String(str); После выполнения этих операторов str2 тоже будет содержать символьную строку "Приветствуем!". Вот еще один простой способ создания объекта String: String str = "Строки Java крайне эффективны ."; В данном случае str инициализируется символьной строкой "Строки Java крайне эффективны .". После создания объект типа String можно использовать везде, где разрешена строка в кавычках. Скажем , объект String допустимо применять в качестве аргумента метода println ( ) , как показано ниже: // Знакомство с классом String. class StringDemo { public static void main(String[] args) { // Разные способы объявления строк. String strl = new String("Строки в Java являются объектами."); String str2 = "Они создаются различными способами."; String str3 = new String(str2); System.out.println(strl); System.out.println(str2); System.out.println(str3); } } 190 Java: руководство для начинающих, 9-е издание Вывод, генерируемый программой, выглядит следующим образом: Строки в Java являются объектами. Они создаются различными способами. Они создаются различными способами. Оперирование строками В классе String определены методы для оперирования строками. Общие формы некоторых методов приведены в табл . 5.1. Таблица 5.1 . Общие формы некоторых методов оперирования строками Метод Описание boolean equals(str) Возвращает true, если строка содержит ту же самую последовательность символов, что и str int length() char charAt(index) int compareTo(str) Возвращает длину строки Возвращает символ по индексу, указанному в index Возвращает отрицательное значение, если строка меньше str, положительное значение, если строка больше str, и ноль, если строки равны Ищет в строке подстроку, указанную в str. Возвращает индекс первого вхождения или -1, int indexOf(str) если подстрока не найдена int lastlndexOf(str) Ищет в строке подстроку, указанную в str. Возвращает индекс последнего вхождения или -1, если подстрока не найдена Ниже показан код программы, в которой демонстрируется работа методов из табл . 5.1: // Демонстрация ряда операций со строками , class StrOps { public static void main(String[] args ) { String strl = "В области программирования веб-приложений язык Java занимает первое место."; String str2 = new String(strl); String str3 = "Строки Java крайне эффективны ."; int result, idx; char ch; System.out.println("Длина strl: " + strl.length()); // Отобразить strl посимвольно , for(int i=0; i < strl.length(); i++) System.out.print(strl.charAt(i)); System.out.println(); if(strl.equals(str2)) System.out.println("strl равна str2"); Глава 5 . Дополнительные сведения о типах данных и операциях 191 else System.out.println("strl не равна str2"); if(strl.equals(str3)) System.out.println("strl равна str3"); else System.out.println("strl не равна str3"); result = strl.compareTo(str3); if( result == 0) System.out.println ("strl и str3 равны "); else if( result < 0) System.out.println ("strl меньше str3"); else System.out.println("strl больше str3"); // Присвоить str2 новую строку. str2 = "Один Два Три Один"; idx = str2.indexOf("Один"); System.out.println("Индекс первого вхождения подстроки Один: " + idx); idx = str2.lastIndexOf("Один"); System.out.println("Индекс последнего вхождения подстроки Один: " + idx); } } Программа генерирует следующий вывод: Длина strl: 74 В области программирования веб-приложений язык Java занимает первое место , strl равна str2 strl не равна str3 strl больше str3 Индекс первого вхождения подстроки Один: 0 Индекс последнего вхождения подстроки Один: 13 С использованием операции + можно выполнять конкатенацию двух строк. Например , приведенный далее оператор инициализирует str4 строкой "ОдинДваТри": String String String String strl str2 str3 str4 = "Один"; = "Два"; = "Три"; = strl + str2 + str3; !?@>A8< C M:A?5@B0 ВОПРОС. Почему в классе String определен метод equals ( ) ? Разве нельзя про сто воспользоваться операцией ==? ОТВЕТ. Метод equals ( ) сравнивает последовательности символов двух объектов String на предмет равенства. Применение операции == к двум ссылкам на объекты String позволит лишь определить, ссылаются ли две ссылки на один и тот же объект. 192 Java: руководство для начинающих, 9-е издание Массивы строк Подобно любому другому типу данных строки можно объединять в массивы: // Демонстрация использования массивов строк , class StringArrays { public static void main(String[] args ) { String[] strs = { "Это содержимое", "является", "просто", "тестом." }; System.out.println("Исходный массив: "); for(String s : strs) System.out.print(s + " "); System.out.println("\n"); // Изменить строки. strs[l ] = "стало"; strs[3] = "еще одним тестом!"; System.out.println("Модифицированный массив: "); for(String s : strs) System.out.print(s + " "); } } Вот вывод , генерируемый программой: Исходный массив: Это содержимое является просто тестом. Модифицированный массив: Это содержимое стало просто еще одним тестом! Строки являются неизменяемыми Содержимое объекта String неизменяемо , т.е. после создания последова тельность символов, из которой состоит строка, не может быть модифицирована. Такое ограничение содействует более эффективной реализации строк в Java . Несмотря на то что ограничение выглядит серьезным недостатком , это не так. Если вам нужна строка , являющаяся вариацией уже существующей , тогда просто создайте новую строку с желаемыми изменениями. Поскольку неиспользуемые объекты String автоматически удаляются сборщиком мусора , вам даже не придется беспокоиться о том , что произойдет с отброшенными строками. Однако важно понимать, что ссылочные переменные String, разумеется, могут изменять объект, на который они ссылаются. Просто содержимое конкретного объекта String нельзя модифицировать после его создания. Чтобы понять, почему неизменяемые строки не помеха, воспользуемся еще одним методом String: substring ( ) , который возвращает новую строку, содержащую указанную часть исходной строки. Поскольку создается новый объект String с подстрокой, исходная строка остается неизмененной, и правило неизменяемости сохраняется. Ниже показана форма substring ( ) , которая будет применяться: — String substring(int startlndex, int endlndex ) Глава 5. Дополнительные сведения о типах данных и операциях 193 !?@>A8< C M:A?5@B0 ВОПРОС. Как упоминалось ранее , после создания объекты String неизменяемы . Ясно, что с практической точки зрения такое ограничение не особо серьезно, но что делать, если желательно создать строку, которую можно изменять? ОТВЕТ. К счастью , в Java предлагается класс StringBuffer, позволяющий создавать объекты String, которые можно изменять. Скажем , в дополнение к методу charAt ( ) , получающему символ в определенной позиции , в классе StringBuffer определен метод setCharAt ( ) , который устанавливает сим вол внутри строки . Кроме того , в Java предоставляется класс StringBuilder, связанный с классом StringBuffer, и поддерживаются строки , которые можно изменить. Но в большинстве случаев будет применяться класс String, а не StringBuffer или StringBuilder. В startlndex указывается начальный индекс , а в endlndex — точка останова. В следующей программе демонстрируется работа substring ! ) и принцип неизменяемых строк. // Использование substring!), class SubStr { public static void main(String[] args) { String orgstr = "Язык Java заставляет веб-сеть двигаться."; // Создание подстроки из объекта String , substr = orgstr.substring(10, 24); System.out.println("orgstr: " + orgstr); System.out.println("substr: " + substr); Это создает новую строку, которая содержит нужную подстроку } } Вот вывод, генерируемый программой: orgstr: Язык Java заставляет веб-сеть двигаться. substr: заставляет веб Как видите , исходная строка orgstr не изменилась, a substr содержит подстроку. Использование строки для управления оператором switch Как объяснялось в главе 3 , в прошлом оператором switch необходимо было управлять с помощью констант целочисленного типа , подобного int или char, что исключало возможность применения switch в ситуациях , когда одно из не скольких действий выбиралось на основе содержимого строки . Типичное ре шение предусматривало использование цепочки if-else-if, которая семанти чески была верна , но оператор switch считался более естественной идиомой для такого выбора . К счастью , ситуация была исправлена . В современной версии Java для управления оператором switch можно применять константы типа String, что во многих ситуациях дает более читабельный и оптимальный код. 194 Java : руководство для начинающих, 9-е издание Ниже приведен пример , в котором демонстрируется управление оператором switch посредством строк . // Использование строк для управления оператором switch , class StringSwitch { public static void main(String[] args) { String command = "cancel"; switch(command ) { case "connect": System.out.println("Подключение"); break; case "cancel ": System.out.println ("Отмена"); break; case "disconnect": System.out. println("Отключение"); break; default: System.out.println("Ошибочная команда!"); break; } } } Программа вполне ожидаемо генерирует такой вывод: Отмена Содержащаяся в command строка ( " cancel " в этой программе ) проверяется на соответствие константам case . Когда совпадение найдено ( как во втором case ) , выполняется кодовая последовательность, ассоциированная с данным case. Возможность применения строк в операторе switch может оказаться очень удобной и улучшить читабельность кода . Скажем , использование switch на основе строк будет улучшением по сравнению с эквивалентной последовательностью операторов i f / e l s e. Но применение строк в switch может быть менее эффективным , чем целых чисел , а потому использовать строки в switch луч ше всего только в тех случаях , когда управляющие данные уже представлены в строковой форме . Другими словами , не применяйте строки в операторе switch без особой необходимости . !?@>A8< C M:A?5@B0 ВОПРОС. Вопрос . Есть информация о существовании еще одного типа строкового литерала, который называется текстовым блоком. Что он собой представляет? ОТВЕТ. Текстовые блоки были добавлены в версии JDK 15 . Текстовый блок — это литерал нового типа , состоящий из последовательности символов , которые могут занимать более одной строчки . Текстовый блок сокращает утоми тельную работу, с которой программисты часто сталкиваются при создании длинных строковых литералов , потому что в текстовом блоке можно указы вать символы новой строки без необходимости в применении управляющей последовательности \ п. Глава 5. Дополнительные сведения о типах данных и операциях 195 Кроме того, символы табуляции и двойных кавычек тоже можно вводить на прямую , не используя управляющую последовательность, и вдобавок есть возможность сохранить отступы в многострочной строке. Таким образом , текстовые блоки представляют собой элегантную альтернативу тому, что может оказаться довольно раздражающим процессом. Текстовый блок поддерживается новым разделителем, состоящим из трех символов двойных кавычек, т.е. I I о и . Текстовый блок создается помещением строки внутрь набора этих разделителей. В частности, текстовый блок начинается немедленно за символом новой строки после открывающего разделителя и и и . Таким образом , строчка, содержащая открывающий разделитель, должна заканчиваться символом новой строки. Текстовый блок начинается со следующей строчки. Текстовый блок заканчивается на первом символе закрывающего разделителя и и и . Важно подчеркнуть, что хотя в текстовом блоке применяется разделитель и и и , он относится к типу String. Следовательно, текстовый блок можно использовать везде, где разрешено применять любую другую строку. Ниже приведен простой пример присваи вания текстового блока переменной s t r: String str = и и и Текстовые блоки упрощают работу с несколькими строками, потому что избавляют от необходимости использовать управляющие последовательности \п для обозначения новой строки. В результате текстовые блоки облегчают работу программиста! •• и. it В этом примере создается строка, в которой каждая строчка отделяется от следующей символом новой строки. Для перехода на новую строку нет не обходимости указывать управляющую последовательность \ п. Таким обра зом, текстовый блок автоматически предохраняет символы новой строки в тексте. Обратите внимание , что во второй строчке имеется отступ. В случае вывода s t r с использованием следующего оператора: System.out.println(str); вот что отобразится: Текстовые блоки упрощают работу с несколькими строками, потому что избавляют от необходимости использовать управляющие последовательности \п для обозначения новой строки. В результате текстовые блоки облегчают работу программиста! В выводе видно, что символы новой строки и отступ во второй строчке предохранены. Именно в том и состоят ключевые преимущества текстовых блоков. Текстовые блоки обладают дополнительными особенностями , такими как способность удаления нежелательных ведущих пробельных символов. Обязательно исследуйте эту функциональную возможность более подробно. Подводя итоги , можно утверждать, что текстовые блоки упрощают то, что часто было сложной задачей при написании кода. 196 Java: руководство для начинающих, 9- е издание Использование аргументов командной строки Теперь, когда вам известно о классе String, вы можете понять параметр args метода main ( ) , который применялся во всех показанных до сих пор программах. Многие программы принимают то , что называется аргументами ко мандной строки. Аргумент командной строки представляет собой данные, которые следуют непосредственно за именем программы в командной строке , когда программа запускается . Получить доступ к аргументам командной строки в программе на Java довольно просто они хранятся в виде строк внутри массива типа String, который передается параметру args метода main(). Например , следующая программа отображает все аргументы командной строки , с которыми она запускается: — // Отображение всех аргументов командной строки. class CLDemo { public static void main(String[] args) { System.out.println("Количество аргументов, переданных программе: " + args.length); System.out.println ("Список аргументов: "); for(int i=0; i <args.length; i++) System.out.println("arg[" + i + "]: " + args[i]); } } Запустив программу CLDemo, как показано ниже: java CLDemo один два три вы увидите следующий вывод: Количество аргументов, переданных программе: 3 Список аргументов: arg[0]: один arg[1]: два arg[2]: три Обратите внимание , что первый аргумент хранится в элементе с индексом 0 , второй в элементе с индексом 1 и т.д. Чтобы получить представление о том , как пользоваться аргументами командной строки, взгляните на следующую программу. Она принимает один аргумент командной строки с именем человека и ищет это имя в двумерном массиве строк. Если совпадение найдено , тогда отображается телефонный номер — человека. // Простая автоматизированная телефонная книга , class Phone { public static void main(String[] args) { String [][] numbers = { { "Tom", "555-3322" }, { "Mary", "555-8976" }, { "Jon", "555-1037" }, Глава 5 . Дополнительные сведения о типах данных и операциях 197 { "Rachel", "555-1400" } }; int i; «+ if(args.length != 1) System.out.println("Использование: java Phone <имя>"); else { for(i=0; i<numbers.length; i++) { if(numbers[i][0].equals( args[0] )) { System.out.println(numbers[i][0] + ": " + numbers[i][1]); break; Для выполнения программы нужен как минимум один аргумент командной строки } } if(i == numbers.length) System.out.println("Имя не найдено."); } } } Вот результат одного запуска программы: java Phone Mary Mary: 555-8976 Использование выведения типов для локальных переменных Не так давно в язык Java было добавлено новое функциональное средство , называемое выведением типов локальных переменных. Для начала давайте вспом ним о двух важных аспектах, касающихся переменных. Во - первых, все пере менные в Java должны быть объявлены до их использования. Во- вторых , переменная может быть инициализирована значением при ее объявлении. Кроме того , когда переменная инициализируется , тип инициализатора должен быть таким же , как объявленный тип переменной ( или допускать преобразование в него ). Таким образом , в принципе нет нужды указывать явный тип для иници ализируемой переменной, потому что он может быть выведен по типу ее ини циализатора . Конечно, в прошлом такое выведение не поддерживалось, и все переменные требовали явно объявленного типа вне зависимости от того, ини циализировались они или нет. На сегодняшний день ситуация изменилась. Начиная с JDK 10 , можно позволить компилятору выводить тип локальной переменной на основе типа ее инициализатора , избегая явного указания типа. Выведение типов локальных переменных обеспечивает несколько преимуществ. Например , оно может упростить код, устраняя необходимость в избыточном указании типа переменной , когда тип может быть выведен из ее инициализатора. Выведение типов локальных переменных способно упрощать объявления в случаях , когда имя типа имеет довольно большую длину, как у некоторых имен классов. Оно также может быть полезно, когда тип трудно различить или его 198 Java: руководство для начинающих, 9-е издание нельзя обозначить. ( Примером типа, который не может быть обозначен , явля ется тип анонимного класса , обсуждаемый в главе 17.) Более того , выведение типов локальных переменных стало обычной частью современной программной среды. Его включение в Java помогает поддерживать язык в актуальном состоянии с учетом меняющихся тенденций в проектировании языков. Для поддержки выведения типов локальных переменных было добавлено контекстно-чувствительное ключевое слово var. Чтобы задействовать выведение типов локальных переменных, переменная должна быть объявлена с ключевым словом var в качестве имени типа и включать инициализатор. Начнем с простого примера. В следующем операторе объявляется локальная переменная типа double по имени avg, которая инициализируется значением 10.0: double avg = 10.0; С применением выведения типа объявление переменной avg можно записать так: var avg = 10.0; В обоих случаях переменная avg будет иметь тип double. В первом случае ее тип указывается явно, а во втором случае выводится как double , т.к. инициализатор 10.0 имеет тип double. Как уже упоминалось, ключевое слово var является зависимым от контекста. Когда var используется в качестве имени типа в контексте объявления локальной переменной , оно сообщает компилятору о том , что тип объявляемой переменной должен выводиться на основе типа инициализатора. Таким обра зом, в объявлении локальной переменной ключевое слово var служит заполни телем фактически выведенного типа . Тем не менее, когда ключевое слово var применяется в большинстве других мест, оно будет просто определяемым пользователем идентификатором без особого смысла. Например, следующее объявление по- прежнему допустимо: int var = 1; //В этом случае var - просто определяемый пользователем идентификатор В данном случае тип явно указан как int , a var представляет собой имя объявляемой переменной. Несмотря на зависимость от контекста , есть несколько мест, где ключевое слово var использовать не разрешено. Скажем , его нельзя применять в качестве имени класса. Предшествующие обсуждения воплощены в следующей программе: // Простая демонстрация выведения типа локальных переменных. class VarDemo { public static void main(String[] args) { // Использовать выведение типов для определения типа переменной // по имени avg. В этом случае выводится тип double , Использование var avg = 10.0; var для выведения типа avg System.out.println("Значение avg: " + avg); Глава 5 . Дополнительные сведения о типах данных и операциях 199 // В следующем контексте var - не предопределенный идентификатор, // а просто определяемое пользователем имя переменной , int var = 1; System.out.println("Значение var: " + var); } // Интересно отметить, что в следующем фрагменте кода var используется // и как тип объявления, и как имя переменной в инициализаторе , var k = -var; System.out.println("Значение к: " + к); } Вот вывод, генерируемый программой: Значение avg: 10.0 Значение var: 1 Значение к: 1 - В предыдущем примере ключевое слово var использовалось для объявления только простых переменных, но var можно применять также для объявления массивов, например: // Допустимый код. var myArray = new int[10]; Обратите внимание , что ни var, ни myArray не имеют скобок. Взамен предполагается , что тип myArray выводится в int [ ] . Кроме того, использовать квадратные скобки в левой части объявления var нельзя. Таким образом, оба следующих объявления ошибочны: var[] myArray = new int[10]; var myArray[] = new int[10]; // Ошибка! // Ошибка! В первой строке предпринимается попытка снабдить квадратными скобками ключевое слово var, а во второй переменную myArray. В обоих случаях применять квадратные скобки некорректно, т.к. тип выводится из типа инициализатора . Важно подчеркнуть, что var может использоваться для объявления переменной только тогда , когда эта переменная инициализирована. Например , пока занный далее оператор ошибочен: — var counter; // Ошибка! Требуется инициализатор. Также помните о том , что ключевое слово var можно применять только для объявления локальных переменных. Его нельзя использовать, например , при объявлении переменных экземпляра , параметров или возвращаемых типов. Использование выведения типов локальных переменных для ссылочных типов В предшествующих примерах были представлены основы выведения типов локальных переменных с использованием примитивных типов. Однако именно со ссылочными типами , такими как типы классов , все преимущества выве дения типов становятся очевидными . Кроме того , выведение типа локальной 200 Java: руководство для начинающих, 9-е издание переменной со ссылочными типами представляет собой основное использование этой функциональной возможности. Давайте снова начнем с простого примера . В следующих объявлениях выведение типов применяется для объявления двух переменных String с именами myStr и mySubStr: var myStr = "Это простая строка"; var mySubStr = myStr.substring(4, 8); — Вспомните , что строка в кавычках это объект типа String. Поскольку в качестве инициализатора используется строка в кавычках, предполагается , что типом myStr будет String. Предполагается, что типом mySubStr тоже бу дет String, т. к. типом ссылки , возвращаемой методом substring(), является String. Разумеется, выведение типов локальных переменных также можно применять с классами , определенными пользователем , как показано в следующей программе . В ней создается класс по имени MyClass, после чего с помощью вы ведения типов локальных переменных объявляется и инициализируется объект класса MyClass. // Использование выведения типов локальных переменных // с классами, определенными пользователем. class MyClass { private int i; MyClass(int k) { i = k; } int geti() { return i; } void seti(int k) { if(k >= 0) i = k; } } class VarDemo2 { public static void main(String[] args) { var me = new MyClass(10); // Обратите внимание на применение var. System.out.println ("Значение i в me равно " + mc.getiO ); me.seti(19); System ,out.println ("Значение i в me теперь равно " + mc.getiO ); } } Ниже приведен вывод, генерируемый программой: Значение i в тс равно 10 Значение i в тс теперь равно 19 Обратите особое внимание в программе на следующую строку: var me = new MyClass(10); // Обратите внимание на применение var. Здесь тип тс будет выведен как MyClass, поскольку это тип инициализатора , который является новым объектом MyClass. Глава 5. Дополнительные сведения о типах данных и операциях 201 Как упоминалось ранее, одно из преимуществ выведения типов локальных переменных связано с его способностью оптимизировать код, и такая оптимизация наиболее очевидна именно со ссылочными типами. Причина в том , что многие типы классов в Java имеют довольно длинные имена. Например, в главе 10 вы узнаете о классе FilelnputStream, с помощью которого файл открывается для операций ввода . В прошлом объект FilelnputStream объявлялся и инициали зировался с применением традиционного объявления вроде показанного ниже: FilelnputStream fin = new FilelnputStream("test.txt"); С использованием var теперь его можно переписать так: var fin = new FilelnputStream("test.txt"); Тут предполагается , что переменная fin имеет тип FilelnputStream, т.к. это тип ее инициализатора. Нет никакой необходимости явно повторять имя типа. В результате такое объявление fin значительно короче, чем его запись традици онным способом , а потому применение var упрощает объявление. Преимущество становится еще более очевидным в более сложных объявлениях, например , включающих обобщения . Конечно , выведение типов локальных переменных должно использоваться осторожно , чтобы не ухудшить читабельность программы и в итоге сделать неясным ее смысл . По существу это функциональная возможность, которую необходимо применять с умом. Использование выведения типов локальных переменных в цикле for Выведение типов локальных переменных можно применять в цикле for при объявлении и инициализации переменной управления циклом внутри традиционного цикла for или при указании переменной итерации в цикле for в стиле “for-each ”. В следующей программе демонстрируются примеры каждого случая: // Использование выведения типов локальных переменных в цикле for. class VarDemo3 { public static void main(String[] args) { // Применить выведение типов к переменной управления циклом. System.out.print("Значения х: "); for(var х = 2.5; х < 100.0; х = х * 2) System.out.print(х + " "); System.out.printIn (); // Применить выведение типов к переменной итерации. int[] nums = { 1, 2 , 3 , 4 , 5, б } ; System.out.print("Значения в массиве nums: "); for( var v : nums) System.out.print(v + " "); System.out.printIn (); } } Использование var в цикле for 202 Java : руководство для начинающих, 9-е издание Вот вывод , генерируемый программой: Значения х: 2.5 5.0 10.0 20.0 40.0 80.0 Значения в массиве nums: 1 2 3 4 5 6 В приведенном примере для переменной управления циклом х в строке: for(var х = 2.5; х < 100.0; х = х * 2) выводится тип double по причине типа ее инициализатора . Для переменной итерации v в строке: for(var v : nums) выводится тип int , потому что он является типом элементов массива nums . Некоторые ограничения var В дополнение к ограничениям , которые упоминались в предыдущем обсуж дении , с применением var связано несколько других ограничений . Можно объявлять только одну переменную за раз , для переменной нельзя использовать n u l l в качестве инициализатора и объявляемая переменная не может присут ствовать в выражении инициализатора. Хотя с применением var можно объ явить тип массива , ключевое слово var нельзя использовать с инициализатором массива . Например , следующий оператор допустим: var myArray = new int[10]; // Допустим. но показанный ниже оператор — нет: var myArray = { 1 , 2, 3 } ; // Ошибочен. Как отмечалось ранее , ключевое слово var не разрешено применять для имени класса . Его также не допускается использовать в качестве имени других ссылочных типов , включая интерфейс , перечисление или аннотацию , либо в качестве имени параметра обобщенного типа, что рассматривается далее в кни ге . Существуют еще два ограничения , которые относятся к функциональным средствам Java , описанным в последующих главах , но упомянутым здесь для полноты картины . Выведение типов локальных переменных нельзя применять для объявления типа исключения , перехваченного оператором catch . Кроме того , ни лямбда - выражения , ни ссылки на методы не разрешено использовать в качестве инициализаторов . На заметку! На момент написания книги некоторые читатели будут иметь дело со средами Java, не поддерживающими выведение типов локальных переменных. Чтобы все читатели книги могли компилировать и запускать как можно больше примеров кода, в большинстве программ в оставшейся части этого издания книги выведение типов локальных переменных применяться не будет. Использование полного синтаксиса объявления также позволяет сразу понять, переменная какого типа создается, что важно для самого примера кода . Разумеется, со временем вы должны обдумать применение в своем коде выведения типов локальных переменных. Глава 5. Дополнительные сведения о типах данных и операциях 203 Побитовые операции В главе 2 вы узнали об арифметических , реляционных и логических опера циях Java. Хотя они используются чаще всего , в Java предлагаются дополнительные операции , расширяющие набор задач , к которым может применяться язык Java: побитовые операции. Побитовые операции можно использовать со значениями типов long, int, short, char и byte, но не со значениями типов boolean, float, double или классами. Их называют побитовыми операциями, потому что они предназначены для проверки, установки или сдвига индивидуальных битов, составляющих значение. Побитовые операции важны для решения широкого круга задач программирования на системном уровне , которые предусматривают оперирование информацией о состоянии устройств. Побито вые операции перечислены в табл . 5.2. Таблица 5.2. Побитовые операции языка Java Операция & Описание Побитовое И Побитовое ИЛИ Побитовое исключающее ИЛИ А » »> « Сдвиг вправо Сдвиг вправо с заполнением нулями Сдвиг влево Побитовое унарное НЕ Побитовые операции И, ИЛИ, исключающее ИЛИ и НЕ Побитовые операции И , ИЛИ , исключающее ИЛИ и НЕ обозначаются с помощью & , , и ~ . Они выполняют такие же функции , как их логические экви валенты , описанные в главе 2. Разница в том, что побитовые операции работают с битами . В табл . 5.3 показаны результаты их выполнения с битами 1 и 0. А Таблица 5.3. Результаты выполнения побитовых логических операций р р q р I q о 1 о о о 1 1 о о о 1 1 1 1 1 о & q р о А q ~р 1 1 о 1 1 о о С точки зрения одного распространенного случая применения операцию побитового И можно рассматривать как способ отключения битов, т.е . любой бит в любом из операндов , равный 0 , приведет к тому, что соответствующий бит в результате будет установлен в 0. Например: 204 Java : руководство для начинающих, 9-е издание 1 1 0 1 0 0 1 1 & 1 0 1 0 1 0 1 0 1 0 0 0 0 0 1 0 В следующей программе демонстрируется использование операции & для превращения букв нижнего регистра в буквы верхнего регистра путем сбрасы вания шестого бита в 0. В наборе символов Unicode/ASCII коды букв нижнего регистра больше кодов верхнего регистра на величину 32. Таким образом , для преобразования буквы нижнего регистра в букве верхнего регистра достаточно лишь отключить бит 6. / / Буквы верхнего регистра , class UpCase { public static void main ( String [] args ) { char ch; for ( int i = 0; i < 10; i + + ) { ch = ( char ) (' a' + i ); System.out .print ( ch ); // Этот оператор отключает бит 6. ch = ( char ) (( int ) ch & 65503 ); // теперь ch содержит букву // верхнего регистра System .out .print ( ch + " "); } } } Ниже приведен вывод , генерируемый программой: аА ЬВ сС dD еЕ fF gG hH il jJ Значение 6 5 5 0 3 в операции И — это десятичное представление двоичного числа 1111 1111 1101 1111. В итоге операция И оставляет все биты в ch без изменений кроме шестого , который установлен в 0. Операция И также удобна , когда нужно определить, включен или выключен какой- то бит. Скажем , следующий оператор позволяет выяснить, установлен ли бит 4 в status: if (( status & 8 ) ! = 0 ) System.out.println ("Бит 4 установлен."); Число 8 выбрано из - за того , что оно преобразуется в двоичное значение , в котором установлен только четвертый бит. Следовательно , условие в операто ре if будет истинным , только если в status включен бит 4. Интересным при менением такой концепции является отображение битов значения типа byte в двоичном формате . // Отображение битов внутри байта. class ShowBits { public static void main ( String [ ] args ) { int t; byte val; Глава 5 . Дополнительные сведения о типах данных и операциях 205 val = 123; for(t=128; t > 0; t = t/2) { if((val & t) != 0) System.out.print("1 "); else System.out.print("0 "); } } } Вот вывод: 0 1 1 1 1 0 1 1 В цикле f o r с использованием операции побитового И последовательно проверяется каждый бит в v a l для выяснения, включен он или нет. Если бит включен , тогда отображается цифра Дав противном случае 0. В упражнении 5.3 вы увидите , как эту базовую концепцию можно расширить, чтобы создать класс, который будет отображать биты целого числа любого типа. В противоположность побитовому И операцию побитового ИЛИ можно применять для включения битов. Любой бит в любом операнде , установленный в 1 , приведет к тому, что соответствующий бит в результате будет установлен в 1. Например: — 1 1 0 1 0 0 1 1 1 1 0 1 0 1 0 1 0 1 1 1 1 1 0 1 1 Используя операцию побитового ИЛИ , легко реализовать преобразование букв верхнего регистра в буквы нижнего регистра: // Буквы нижнего регистра , class LowCase { public static void main(String[] args) { char ch; for(int i=0; i < 10; i ++) { ch = (char) ('A' + i); System.out.print(ch); // Этот оператор включает бит 6. ch = (char) ((int) ch | 32); // теперь ch содержит букву нижнего регистра System.out.print(ch + " "); } } } Ниже показан вывод, генерируемый программой: Аа Bb Сс Dd Ее Ff Gg Hh Ii Jj Программа работает, выполняя операцию побитового ИЛИ с каждым сим волом и значением 32 ( 0000 0000 0010 0000 в двоичном формате). Таким образом, 32 представляет собой значение , в двоичной форме которого установлен 206 Java: руководство для начинающих, 9-е издание только шестой бит. Выполнение побитового ИЛИ для значения 32 и любого другого значения дает результат, в котором бит 6 устанавливается, а все остальные биты не изменяются. Как уже объяснялось, для символов это означает, что каждая буква верхнего регистра преобразуется в букву нижнего регистра. Операция исключающего ИЛИ дает установленный бит тогда и только тогда , когда сравниваемые биты отличаются: л 0 1 1 1 1 1 1 1 1 0 1 1 1 0 0 1 1 1 0 0 0 1 1 0 Операция исключающего ИЛИ обладает интересным свойством , которое делает ее простым способом кодирования сообщений. Если выполнить операцию исключающего ИЛИ над значениями X и Y, после чего снова выполнить ее над результатом и Y, то получится X. Скажем , после выполнения следующих двух операторов R 2 получит то же значение, что и X: R1 = X А Y ; R2 = R 1 А Y; Таким образом , последовательное применение двух операций исключающего ИЛИ позволяет восстановить исходное значение. Данный принцип можно использовать для создания простой программы шифрования, где некоторое целое число будет выступать в качестве ключа , служащего как для кодирования, так и для декодирования сообщения . В отношении символов сообщения выполняется операция исключающего ИЛИ со зна чением ключа . Для кодирования операция исключающего ИЛИ применяется первый раз, в результате чего получается зашифрованный текст. Для декодиро вания операция исключающего ИЛИ применяется второй раз , в результате чего получается обычный текст. Конечно, такой шифр не имеет практической ценности , потому что взломать его легко. Тем не менее , он представляет собой ин тересный способ демонстрации использования операции исключающего ИЛИ. Ниже показан код программы , реализующей описанный подход для кодирования и декодирования короткого сообщения: // Использование операции исключающего ИЛИ для кодирования // и декодирования сообщения. class Encode { public static void main(String[] args) { String msg = "This is a test"; String encmsg = String decmsg = int key = 88; ик . ии . System.out.print("Исходное сообщение: "); System.out.println(msg); // Закодировать сообщение , for(int i=0; i < msg.length(); i ++) encmsg = encmsg + (char) (msg.charAt(i) Конструирует закодированную строку л key); Глава 5 . Дополнительные сведения о типах данных и операциях 207 System.out.print("Закодированное сообщение: "); System.out.println(encmsg); // Декодировать сообщение. for(int i=0; i < msg.length(); i++) decmsg = decmsg + (char) (encmsg.charAt(i) Л key); t System.out.print("Декодированное сообщение: "); System.out.println(decmsg); Конструирует раскодирован- ную строку } } Вот вывод: Исходное сообщение: This is a test Закодированное сообщение: 01+xl +x9x,=+, Декодированное сообщение: This is a test Как видите , в результате выполнения двух операций исключающего ИЛИ с одним и тем же ключом получается декодированное сообщение . Унарная операция дополнения до единицы ( НЕ) изменяет состояние всех битов операнда на противоположное . Скажем , если некоторое целое число по имени А содержит битовую комбинацию 1001 ОНО , то результатом ~ А будет битовая комбинация ОНО 1001. В следующей программе демонстрируется работа операции побитового НЕ с отображением числа и его дополнение до единицы в двоичном формате: // Демонстрация работы операции побитового НЕ. class NotDemo { public static void main(String[] args ) { byte b = -34; for(int t=128; t > 0; t = t/2) { if((b & t) != 0) System.out.print("1 "); else System.out.print("0 "); } System.out.println(); // Изменить состояние всех битов на противоположное , b = (byte) ~b; for(int t=128; t > 0; t = t/2) { if((b & t) != 0) System.out.print("1 "); else System.out.print("0 "); } } } Ниже представлен вывод, генерируемый программой: 1 1 0 1 1 1 1 0 0 0 1 0 0 0 0 1 208 Java: руководство для начинающих, 9-е издание Операции сдвига В Java имеется возможность сдвига битов , составляющих значение , влево или вправо на заданное количество позиций. Определены три операции битового сдвига , показанные в табл . 5.4. Таблица 5.4. Операции сдвига Операция Описание « » Сдвиг влево »> Сдвиг вправо с заполнением нулями Сдвиг вправо Общие формы операций сдвига выглядят следующим образом: значение « количество -позиций значение » количество -позиций значение »> количество -позиций Здесь значение сдвигается на количество битовых позиций , указанное в количество -позиций. Каждый сдвиг влево приводит к тому, что все биты внутри указанного значения сдвигаются влево на одну позицию, а вправо вставляется бит, равный 0. Каждый сдвиг вправо сдвигает все биты вправо на одну позицию и сохраняет бит знака . Возможно, вам известно, что отрицательные числа обычно представляются установкой старшего бита целочисленного значения в 1, и именно такой подход применяется в языке Java. Таким образом , если сдвигается отрицательное значение , то каждый сдвиг вправо приводит к появлению слева единицы , а если положительное тогда каждый сдвиг вправо приводит к появлению слева нуля. Помимо бита знака есть еще кое- что , о чем следует помнить при сдвиге вправо. Для представления отрицательных значений в Java используется кодировка, известная как дополнение до двух или дополнительный код, которая предусматривает представление отрицательных чисел путем инвертирования ( замены единиц на нули и наоборот) всех битов в значении и последующего добавления единицы к результату. Таким образом , байт для значения -1 в двоичной форме выглядит как 1111 1111. Сдвиг этого значения вправо всегда будет давать -1! Если при сдвиге вправо сохранять бит знака не требуется , то можно применить беззнаковый сдвиг вправо ( » > ), который всегда помещает слева ноль, и потому его также называют сдвигом вправо с заполнением нулями. Беззнаковый сдвиг вправо будет использоваться при работе с битовыми шаблонами , такими как коды состояния , которые не представляют целые числа. При выполнении всех сдвигов смещенные за пределы значения биты утра чиваются . Циклический сдвиг не поддерживается и бит, смещенный за преде лы значения , восстановить не удастся. Ниже показана программа , в которой — Глава 5 . Дополнительные сведения о типах данных и операциях 209 графически иллюстрируется эффект сдвигов влево и вправо . Первым делом це лому числу присваивается начальное значение 1 , т.е . его младший бит установлен . Затем над целым числом выполняется последовательность из восьми сдвигов . После каждого сдвига отображаются младшие восемь бит значения. Далее организуется похожий процесс , но в восьмую битовую позицию помещается 1 и выполняются сдвиги вправо . // Демонстрация работы операций << и ». class ShiftDemo { public static void main(String [] args) { int val = 1; for(int i = 0; i < 8; i++) { for(int t=128; t > 0; t = t/2) { if((val & t) != 0) System.out.print("1 "); else System.out.print("0 "); } System.out.println(); val = val « 1; // сдвиг влево } System.out.println(); val = 128; for(int i = 0; i < 8; i++) { for(int t=128; t > 0; t = t/2) { if((val & t) != 0) System.out.print("1 "); else System.out.print("0 "); } System.out.println(); val = val » 1; // сдвиг вправо } } } Вот вывод: 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 1 210 Java: руководство для начинающих, 9-е издание При сдвиге значений byte и short важно соблюдать осторожность, потому что во время вычисления выражения эти типы автоматически повышаются до int. Например, в случае сдвига значения byte вправо оно сначала повышается до int и затем сдвигается. Результат сдвига тоже будет иметь тип int. Часто такое преобразование не влечет за собой никаких последствий. Однако в случае сдвига отрицательного значения byte или short при повышении до int оно будет дополнено знаком. Таким образом , старшие биты результирующего цело численного значения заполнятся единицами. Это нормально при выполнении обычного сдвига вправо. Но когда выполняется сдвиг вправо с заполнением нулями, понадобится сдвинуть на 24 позиции , прежде чем в значении byte появятся нули. !?@>A8< C M:A?5@B0 ВОПРОС. Поскольку двоичный код основан на степени двойки, можно ли опе рации сдвига применять для быстрого умножения или деления целого числа на два? ОТВЕТ. Да. Операции сдвига можно использовать для выполнения очень быстрого умножения или деления на два . Сдвиг влево удваивает значение. Сдвиг вправо уменьшает его вдвое . Составные побитовые операции присваивания Все бинарные побитовые операции имеют составную форму, аналогичную форме алгебраических операций , которая сочетает в себе присваивание с поби товой операцией. Скажем, следующие два оператора присваивают переменной х результат выполнения операции исключающего ИЛИ над х и значением 127: Упражнение 5.3 Класс ShowBits В настоящем проекте создается класс ShowBits, котоShowBitsDemo.java рый позволит отображать в двоичном формате битовый шаблон для любого целочисленного значения. Такой класс может оказаться крайне полезным при написании кода определенных программ. Например , если проводится отладка кода драйвера устройства , то возможность отслеживания потока данных в двоичном виде часто будет преимуществом. 1. Создайте файл по имени ShowBitsDemo.java. 2. Начните определение класса ShowBits: Глава 5. Дополнительные сведения о типах данных и операциях 211 class ShowBits { int numbits; ShowBits(int n) { numbits = n; } Конструктор ShowBits создает объекты, которые отображают указанное количество бит. Например , для создания объекта , отображающего младшие 8 битов какого-то значения , необходимо использовать такой код: ShowBits byteval = new ShowBits(8) Количество отображаемых битов хранится в numbits. 3. Для фактического отображения битового шаблона в классе ShowBits предусмотрен метод show( ) : void show(long val) { long mask = 1; // Сдвиг влево значения 1 в нужную позицию , mask «= numbits-1; int spacer = 0; for(; mask != 0; mask >»= 1) { if((val & mask) != 0) System.out.print("1"); else System.out.print("0"); spacer++; 0) { if((spacer % 8) System.out.print(" "); spacer 0; } } System.out.println(); } Обратите внимание , что метод show ( ) принимает один параметр типа long. Тем не менее , это не означает, что show ( ) всегда придется переда вать значение long. Благодаря автоматическому повышению типов в Java передавать методу show ( ) можно любой целочисленный тип. Количество отображаемых битов определяется значением , хранящимся в numbits. После каждой группы из 8 битов в show ( ) выводится пробел , что облегча ет чтение двоичных значений длинных битовых комбинаций . 4. Ниже показан код программы ShowBitsDemo: /* Упражнение 5.3. Класс, который отображает двоичное представление значения. */ class ShowBits { int numbits; 212 Java: руководство для начинающих, 9-е издание ShowBits(int п) { numbits = п; } void show(long val) { long mask = 1; // Сдвиг влево значения 1 в нужную позицию , mask «= numbits-1; int spacer = 0; for(; mask != 0; mask »>= 1) { if((val & mask) != 0) System.out.print("1"); else System.out.print("0"); spacer++; 0) { if((spacer % 8) System.out.print(" "); spacer = 0; } } System.out.printIn(); } } // Демонстрация использования ShowBits. class ShowBitsDemo { public static void main(String[] args) { ShowBits b = new ShowBits(8); ShowBits i = new ShowBits(32); ShowBits li = new ShowBits(64); System.out.println("Двоичное представление значения 123: "); b.show(123); System.out.println( п \пДвоичное представление значения 87987: "); i.show(87987); System.out.println("\пДвоичное представление значения 237658768: "); li.show(237658768); //Можно также отображать младшие биты любого целочисленного значения System.out.println("\пМладшие 8 битов значения 87987: "); b.show(87987); } } 5. Вот вывод, генерируемый программой ShowBitsDemo: Двоичное представление значения 123: 01111011 Двоичное представление значения 87987: 00000000 00000001 01010111 10110011 Двоичное представление значения 237658768: 00000000 00000000 00000000 00000000 00001110 00101010 01100010 10010000 Младшие 8 битов значения 87987: 10110011 Глава 5. Дополнительные сведения о типах данных и операциях 213 Операция ? Одной из наиболее интересных операций Java считается ? . Операция ? часто применяется в качестве замены операторов if -else следующего вида: if (условие) переменная = выражение1; else переменная = выражение2; Значение , присваиваемое переменной, зависит от результата вычисления условия, управляющего оператором if . Операция ? называется тернарной, потому что требует трех операндов. Ниже показана ее общая форма: выражение1 ? выражение2 : выражение3 — — Здесь выражение1 булевское выражение , а выражение2 и выражение3 выражения любого типа кроме void. Однако типы выражение2 и выражениеЗ должны совпадать ( или быть совместимыми ). Обратите внимание на использо вание и местоположение двоеточия. Вот как определяется значение выражения ? . Сначала вычисляется выражение1. Если оно дает true , тогда вычисляется выражение2 и его результат становится значением всего выражения ?. Если выражение! дает false , то вычисляется выражениеЗ и его результат будет значением всего выражения ?. Рассмотрим следующий пример , в котором absval присваивается абсолютное значение val: absval = val < 0 ? -val : val; // получить абсолютное значение val Переменной absval присваивается значение val , если оно больше или равно 0. Если значение val отрицательное , то absval присваивается значение val со знаком “ минус ” ( что дает положительное значение). Тот же код, написанный с использованием операторов if -else, выглядит так: if(val < 0) absval = else absval = val; -val; Далее приведен еще один пример применения операции ?. В примере вы полняется деление двух чисел , но не допускается деление на 0. // Предотвращает деление на 0 с использованием операции ?. class NoZeroDiv { public static void main(String[] args) { int result; for(int i = -5; i < 6; i++) { result = i != 0 ? 100 / i : 0; Предотвращает деление на О if(i != 0) System ,out.println("100 / " + i + " равно " + result); } } } 214 Java : руководство для начинающих, 9-е издание Вот вывод, генерируемый программой: 100 100 100 100 100 100 100 100 100 100 / / / / / / / / / / -5 -4 равно -20 равно -25 3 равно 33 -2 равно -50 -1 равно -100 1 равно 100 2 равно 50 3 равно 33 4 равно 25 5 равно 20 - - Обратите особое внимание в программе на следующую строку: result = i != 0 ? 100 / 1 : 0; Переменной result присваивается результат деления 100 на i. Однако деление происходит только в случае , если значение i не равно 0. Когда значение i равно 0, то result присваивается значение- заполнитель, равное нулю. На самом деле значение , произведенное операцией ? , вовсе не обязательно присваивать какой -то переменной. Скажем, его можно использовать в качестве аргумента при вызове метода. Или же , если все выражения имеют тип boolean , то операцию ? можно применять как условное выражение в цикле или операторе if . Например , ниже показана предыдущая программа , которая сделана немного эффективнее. Она выдает те же результаты, что и ранее. // Предотвращает деление на 0 с использованием операции ?. class NoZeroDiv2 { public static void main(String[] args) { for(int i = -5; i < 6; i++) if(i != 0 ? true : false ) System.out.println("100 / " + i + " равно " + 100 / i); } } Обратите внимание на оператор if . Если значение i равно нулю , тогда результатом условия if будет false , деление на 0 предотвращается и ничего не отображается. В противном случае происходит нормальное деление. Вопросы и упражнения для самопроверки 1. Продемонстрируйте два способа объявления одномерного массива из 12 элементов типа double. 2. Покажите , как инициализировать одномерный массив целых чисел значениями от 1 до 5. Глава 5 . Дополнительные сведения о типах данных и операциях 215 3. Напишите программу, в которой используется массив для нахождения среднего для 10 значений типа double. Выберите любые желаемые 10 значений. 4. Измените программу в упражнении 5.1, чтобы она сортировала массив строк. Продемонстрируйте ее работу. 5. В чем разница между методами indexOf ( ) и lastlndexOf ( ) класса String? 6. Учитывая , что все строки являются объектами типа String, покажите , как можно вызвать методы length ( ) и charAt ( ) для строкового литера ла: "Мне нравится Java". 7. Расширьте класс шифрования Encode, модифицировав его так , чтобы в качестве ключа в нем использовалась строка из восьми символов. 8. Можно ли применять побитовые операции к значениям типа double? 9. Покажите , как переписать следующий фрагмент кода с использованием операции ?. if(х < 0) у = 10; else у = 20; 10. Какой операции в следующем фрагменте кода соответствует вой или логической? Обоснуйте свой ответ. & — побито- boolean а, Ь; II . .. if(а & Ь) ... 11 . Является ли ошибкой выход за пределы массива? Будет ли ошибкой применение отрицательного значения для индексирования массива ? . 12 Что такое операция беззнакового сдвига вправо? 13. Перепишите класс MinMax, показанный в начале этой главы , чтобы в нем использовался цикл for в стиле “ for-each ”. 14. Можно ли преобразовать циклы for, которые выполняют сортировку в классе Bubble из упражнения 5.1, в циклы for в стиле “ for-each ” ? Если нет, то почему? 15. Можно ли управлять оператором switch с помощью объектов String? 16. Какое ключевое слово является зарезервированным для применения с выведением типов локальных переменных? 17. Покажите, как использовать выведение типов локальных переменных для объявления переменной типа boolean по имени done с начальным значением false. 18. Может ли var быть именем переменной ? Может ли var быть именем класса ? 216 Java: руководство для начинающих, 9-е издание . 19 Допустимо ли следующее объявление? Если нет, то почему? var[] avgTemps = new double[7]; 20. Допустимо ли следующее объявление? Если нет, то почему? var alpha = 10, beta = 20; 21. В методе show() класса ShowBits, разработанного в упражнении 5.3, локальная переменная mask объявлялась следующим образом: long mask = 1; Измените это объявление так, чтобы в нем применялось выведение типов локальных переменных. Вдобавок удостоверьтесь в том , что mask имеет тип long (как здесь), а не int. Глава 6 Дополнительные сведения о методах и классах 218 Java : руководство для начинающих, 9-е издание В этой главе • • • z Управление доступом к членам z Передача объектов методу z Возвращение объектов из метода z Перегрузка методов z Перегрузка конструкторов z Использование рекурсии z Применение ключевого слова static z Использование внутренних классов z Использование аргументов переменной длины в этой главе возобновляется исследование классов и методов. Сначала объясняется управление доступом к членам класса , а затем обсуждается передача возвращение объектов, перегрузка методов, рекурсия и применение ключевои го слова static. Кроме того , рассматриваются вложенные классы и аргументы переменной длины. Управление доступом к членам класса Благодаря поддержке инкапсуляции класс обеспечивает два крупных пре имущества . Во- первых, он связывает данные с кодом, который ими манипули рует. Указанный аспект класса использовался , начиная с главы 4. Во-вторых, он предоставляет средства , с помощью которых можно контролировать доступ к членам. Именно эта особенность и рассматривается здесь. Хотя подход в Java немного сложнее , в сущности, есть два базовых типа членов класса: открытые (public) и закрытые ( private ). Открытый член может быть свободно доступен коду, определенному вне его класса . К закрытому члену можно получить доступ только из других методов , определенных в его классе. Управление доступом организуется с помощью закрытых членов. Ограничение доступа к членам класса является фундаментальной частью объектно-ориентированного программирования , поскольку помогает предотвратить некорректное применение объекта . Разрешая доступ к личным данным только с помощью четко определенного набора методов, можно предотвратить присваивание неправильных значений этим данным , например, путем проверки диапазона. Код вне класса не может напрямую установить значение закрытого Глава 6. Дополнительные сведения о методах и классах 219 члена . Вдобавок можно точно контролировать, как и когда используются данные внутри объекта. Таким образом , при правильной реализации класс формирует “ черный ящик ” , которым можно пользоваться , но нельзя вмешиваться в его внутреннюю работу. До сих пор об управлении доступом беспокоиться не приходилось, потому что Java предоставляет стандартную настройку доступа, при которой для пока занных ранее видов программ члены класса свободно доступны другому коду в программе. (Другими словами , в предыдущих примерах члены класса по умол чанию были открытыми.) Хотя такая стандартная настройка доступа удобна для простых классов ( и примеров программ в книгах подобного рода ) , во многих реальных ситуациях она не подходит. Ниже будут представлены другие средства управления доступом в Java. Модификаторы доступа Java Управление доступом к членам обеспечивается с помощью трех модифика торов доступа: public, private и protected. Как уже объяснялось, если моди фикатор доступа не используется , тогда предполагается стандартная настройка доступа. В настоящей главе рассматриваются модификаторы public и private. Модификатор protected применяется только тогда , когда задействовано наследование, и он описан в главе 8. Когда член класса снабжается модификатором public, доступ к нему может получать любой другой код в программе , в том числе методы , определенные внутри других классов. Когда член класса указан как private, доступ к нему могут получать только другие члены данного класса. Методы из других классов не могут получать доступ к закрытому члену другого класса. Стандартной настройкой доступа ( где модификатор доступа не используется ) будет public, если только программа не разбита на пакеты. Пакет по существу представляет собой группу классов. Пакеты являются как организационным средством, так и средством управления доступом , но обсуждение пакетов откладывается до главы 8. Для типов программ, показанных в этой и предыдущих главах, в качестве стандартного доступа принимается public. Модификатор доступа указывается перед спецификацией типа члена , т.е. он должен находиться в начале оператора объявления члена . Ниже приведены примеры: public String errMsg; private accountBalance bal; private boolean isError( byte status) { II ... Чтобы лучше понять эффект от применения public и private, взгляните на следующую программу: 220 Java: руководство для начинающих, 9- е издание // Сравнение открытого и закрытого доступа , class MyClass { private int alpha; public int beta; int gamma; // закрытый доступ // открытый доступ // стандартный доступ /* Методы доступа к alpha. Член класса может получать доступ к закрытому члену того же класса. */ void setAlpha(int а) { alpha = а; } int getAlpha() { return alpha; } } class AccessDemo { public static void main(String[] args) { MyClass ob = new MyClass(); /* Доступ к alpha разрешен только через его методы доступа. */ ob.setAlpha(-99); System.out.println("ob.alpha: " + ob.getAlpha()); // Получить доступ к alpha подобным образом нельзя: // ob.alpha = 10; // Неправильно, т.к. член Ошибка ! Член alpha является закрытым // alpha является закрытым. } // Нормально, поскольку члены beta и gamma являются открытыми , ob.beta = 8 8; м Допустимо, поскольку это открытые члены ob.gamma = 99; } В классе MyClass видно , что член alpha указан как private, beta явно указан как public, а для gamma используется стандартный доступ , который в данном примере совпадает с public. Так как член alpha является закрытым , к нему нельзя получить доступ из кода вне его класса , поэтому внутри клас са AccessDemo не разрешено обращаться к члену alpha напрямую . Доступ к нему должен осуществляться через открытые методы доступа: setAlpha ( ) и getAlpha ( ) . Если удалить символы комментария в начале следующей строки , то скомпилировать программу не удастся из- за нарушения прав доступа: // ob.alpha = 10; // Неправильно, т.к. член alpha является закрытым. Хотя доступ к члену alpha в коде за пределами класса MyClass не разрешен , методы , определенные в MyClass, могут свободно обращаться к alpha, что демонстрируется в методах setAlpha ( ) и getAlpha(). Ключевой момент состоит в том , что закрытый член можно свободно ис пользовать в других членах его класса , но к нему нельзя получить доступ из кода вне этого класса . Глава 6. Дополнительные сведения о методах и классах 221 Рассмотрим более практический пример управления доступом . В приведенной далее программе реализован “ отказоустойчивый ” массив типа int, в ко тором предотвращаются ошибки выхода индекса за границы массива , избегая генерации исключения во время выполнения . Цель достигается путем инкап суляции массива как закрытого члена класса , что позволяет получать доступ к массиву только через методы -члены . При таком подходе любая попытка доступа к массиву за его границами может быть предотвращена с выдачей сооб щения об ошибке . Отказоустойчивый массив реализуется посредством класса FailSoftArray. /* Этот класс реализует возникновение ошибок class FailSoftArray { private int[] a; private int errval; public int length; "отказоустойчивый" массив, предотвращающий во время выполнения. */ // ссылка на массив // значение, возвращаемое в случае отказа get() // открытый член length /* Конструирует массив с заданным размером и значением, возвращаемым в случае отказа метода get(). */ public FailSoftArray(int size , int errv) { a = new int[size]; errval length = errv; = size; } // Возвращает значение по заданному индексу , public int get(int index) { if(indexOK(index)) return a[index]; return errval; } ^ Отслеживание попытки доступа за границами массива // Помещает значение по заданному индексу. // В случае отказа возвращает false public boolean put(int index, int val) { if(indexOK(index )) { a[index] = val; return true; } return false; // Возвращает true, если индекс находится внутри границ массива , private boolean indexOK(int index) { if(index >= 0 & index < length) return true; return false; } } 222 Java: руководство для начинающих, 9-е издание // Демонстрация работы "отказоустойчивого" массива , class FSDemo { public static void main(String[] args) { FailSoftArray fs = new FailSoftArray(5, -1); int x; // Отобразить отказы без отчета. System.out.println("Обработка ошибок без отчета."); for(int i=0; i < (fs.length * 2); i++) fs.put(i, i*10); Доступ к массиву должен осуществляться через его методы доступа for(int i=0; i < (fs.length * 2); i++) { x = fs.get(i); if(x != -1) System.out.print(x + " "); } System.out.println(""); // Отработать отказы . System.out.println(" ХпОбработка ошибок с отчетом."); for(int i=0; i < (fs.length * 2); i++) if(!fs.put(i, i*10)) System.out.println("Индекс " + i + " выходит за границы массива"); for(int i=0; i < (fs.length * 2); i++) { x = fs.get(i); if(x != -1) System.out.print(x + " "); else System.out.println("Индекс " + i + " выходит за границы массива"); } } } Вот вывод, генерируемый программой: Обработка ошибок без отчета. О 10 20 30 40 Обработка ошибок с отчетом. Индекс 5 выходит за границы массива Индекс б выходит за границы массива Индекс 7 выходит за границы массива Индекс 8 выходит за границы массива Индекс 9 выходит за границы массива 0 10 20 30 40 Индекс 5 выходит за границы массива Индекс 6 выходит за границы массива Индекс 7 выходит за границы массива Индекс 8 выходит за границы массива Индекс 9 выходит за границы массива Рассмотрим этот пример более внимательно . Внутри класса FailSoftArray определены три закрытых члена . Первый — а, в котором хранится ссылка на массив , фактически содержащий информацию . Второй — errval, хранящий значение , которое будет возвращено , если вызов get ( ) завершится ошибкой . Третий — закрытый метод indexOK ( ) , который определяет, находится ли ин декс в допустимых пределах . Таким образом , доступ к указанным трем чле нам может осуществляться только из других членов класса FailSoftArray. Глава 6. Дополнительные сведения о методах и классах 223 В частности, а и errval могут использоваться только в других методах класса , а indexOK ( ) можно вызывать только в других членах FailSoftArray. Остальные члены класса являются открытыми и могут вызываться в любом другом коде внутри программы , которая задействует класс FailSoftArray. При конструировании объекта FailSoftArray потребуется указать размер массива и значение , которое необходимо возвращать в случае отказа метода get( ) . Значение , отражающее ошибку, должно быть таким , которое не будет сохраняться в массиве. После создания фактический массив, на который ссыла ется а, и значение ошибки , хранящееся в errval, не могут быть доступны пользователям объекта FailSoftArray. Таким образом , они не открыты для неправильного применения. Скажем , пользователю не удастся напрямую обратиться к массиву а по индексу, выходящему за допустимые границы . Доступ возможен только через методы get( ) и put(). Метод indexOK ( ) объявлен закрытым главным образом в целях иллюстрации. Совершенно безвредно сделать его открытым , т.к. он не изменяет объект, но поскольку он используется внутри класса FailSoftArray, то он может быть закрытым . Обратите внимание , что переменная экземпляра length объявлена открытой , что соответствует тому, как в Java реализованы массивы. Для получения длины FailSoftArray понадобится просто обратиться к length. Метод put ( ) предназначен для сохранения значения по указанному индексу в массиве FailSoftArray. Метод get( ) позволяет получить значение по заданному индексу. Если индекс выходит за границы массива , тогда put ( ) возвращает false, a get() errval. Ради удобства в большинстве примеров, приведенных в книге , для большей части членов будет применяться стандартный доступ. Однако помните о том , что в реальных программах ограничение доступа к членам, особенно к переменным экземпляра , является важным аспектом успешного объектно-ориентированного программирования. В главе 7 вы увидите , что управление доступом становится еще более важным , когда в игру вступает наследование . — На заметку! Средство модулей, добавленное в JDK 9, также может играть свою роль в доступности. Модули обсуждаются в главе 15. Упражнение 6.1 | Усовершенствование класса Queue С использованием модификатора private можно внести довольно важное улучшение в класс Queue, разработанный в упражнении 5.2 из главы 5. В той версии для всех членов класса Queue применялся стан дартный доступ и потому код программы, использующей Queue, может получить прямой доступ к базовому массиву, возможно, обращаясь к его элементам вне очереди. Поскольку весь смысл очереди состоит в том, чтобы предоставить спи первым обслужен ” , разрешение доступа не по порядку сок “ первым пришел Queue.java — 224 Java: руководство для начинающих, 9-е издание нежелательно. Злоумышленник также может изменить значения, хранящиеся в индексах putloc и get1ос, тем самым нарушив работу очереди. К счастью, подобные проблемы легко предотвратить, применив модификатор private. 1. Скопируйте исходный код класса Queue, созданного в упражнении 5.2 , в новый файл по имени Queue.java. 2. Добавьте модификатор private к массиву q, а также к индексам putloc и getloc в классе Queue: // Усовершенствованный класс, представляющий очередь для символов , class Queue { // эти члены теперь закрытые // массив, хранящий данные очереди private char[] q; private int putloc, getloc; // индексы для позиций помещения // и извлечения Queue(int size) { // выделение памяти под очередь q = new char[size]; putloc = getloc = 0; } // Поместить символ в очередь. void put(char ch) ( if(putloc==q.length ) { System.out.println(" - Очередь переполнена."); return; } q[putloc++] = ch; } // Извлечь символ из очереди , char get() { if(getloc == putloc) { System.out.println(" - Очередь пуста."); return (char) 0; } return q[getloc++]; } } 3. Изменение типа доступа к членам q, putloc и getloc со стандартного на закрытый никак не скажется на программах, корректно использующих класс Queue. Например , модифицированный класс Queue по-прежнему будет нормально работать с классом QDemo из упражнения 5.2. Тем не менее, неправильное применение класса Queue предотвращается. Скажем , следующие операторы не допускаются: Queue test new Queue(10); // Ошибка! test.q[0] = 99; // Не сработает! test.putloc = -100; 4. Теперь, когда q, putloc и getloc являются закрытыми, класс Queue строго следует принципу “ первым пришел первым обслужен ” , принятому для очереди. — Глава 6. Дополнительные сведения о методах и классах 225 Передача объектов методам Вплоть до этого момента параметры методов в примерах относились к простым типам. Однако передача объектов в методы считается корректной и распространенной практикой. Скажем , в следующей программе определен класс Block, в котором хранятся размеры трехмерной коробки: // Методам можно передавать объекты class Block { int а, b, с; int volume; , Block(int i , int j, int k) { a 1; b = j; c = k; volume = a * b * c; } // Возвращает true, если ob определяет ту же самую коробку. boolean sameBlock(Block ob) { м Использование объектного типа if((ob.a == a) & (ob.b == b) & (ob.c == c)) return true; для параметра else return false; } // Возвращает true, если ob имеет тот же самый объем. boolean sameVolume( Block ob) { if(ob.volume == volume) return true; else return false; } } class PassOb { public static Block obi = Block ob2 = Block ob3 = void main(String[] args) { new Block(10, 2, 5); new Block(10, 2, 5); new Block(4, 5, 5); System.out.println("obi имеет те же размеры оЫ .sameBlock(оЬ2)); System.out.println("obi имеет те же размеры obi.sameBlock(оЬЗ)); System.out.println("obi имеет тот же самый obi.sameVolume(оЬЗ)); } , что и оЬ2: " + , что и оЬЗ: " + Передача объекта объем, что и оЬЗ: " + < } Вот вывод, генерируемый программой: obi имеет те же размеры , что и ob2: true obi имеет те же размеры , что и оЬЗ: false obi имеет тот же самый объем, что и оЬЗ: true Методы sameBlock ( ) и sameVolume ( ) сравнивают объект Block, переданный в качестве параметра , с вызывающим объектом . В sameBlock() 226 Java: руководство для начинающих, 9-е издание сравниваются размеры объектов, и в случае , если две коробки одинаковы , то возвращается true. В sameVolume ( ) два блока сравниваются только для того , чтобы определить, имеют ли они одинаковый объем. Обратите внимание, что в обоих случаях для параметра ob указан тип Block. Хотя тип Block представляет собой класс, созданный в программе , он используется точно так же, как и встроенные типы Java. Способы передачи аргументов — Как показано в предыдущем примере , передача объекта методу задача простая. Тем не менее, с передачей объекта связан ряд нюансов, которые в примере не демонстрировались. В некоторых случаях результаты передачи объекта будут отличаться от результатов передачи аргументов , отличающихся от объ ектов. Чтобы понять причины , необходимо ознакомиться с двумя способами передачи аргументов подпрограмме. Первый способ вызов по значению. При таком подходе в формальный параметр подпрограммы копируется значение аргумента, поэтому изменения , вноси мые в параметр подпрограммы, не влияют на аргумент в вызове. Второй способ передачи аргумента вызов по ссылке. При таком подходе параметру передается ссылка на аргумент (а не значение аргумента ). Эта ссылка применяется внутри подпрограммы для доступа к фактическому аргументу, указанному в вызове. В итоге изменения, внесенные в параметр, окажут влияние на аргумент, используемый при вызове подпрограммы. Вы увидите , что хотя в Java для передачи аргументов применяется вызов по значению , точный результат зависит от того , передается примитивный тип или ссылочный тип. Примитивный тип вроде int или double передается методу по значению. Таким образом, создается копия аргумента , и то, что происходит с параметром, который получает аргумент, не распространяется за пределы метода. Например, рассмотрим следующую программу: — — // Примитивные типы передаются по значению. class Test { /* Этот метод не приводит к изменению аргументов, используемых при вызове. * / void noChange(int i, int j) { i = i + j; j = -j; } } class CallByValue { public static void main(String[] args) { Test ob = new Test(); int a = 15, b = 20; System.out.println("а и b перед вызовом: " + a + " " + b) ; Глава 6. Дополнительные сведения о методах и классах 227 ob.noChange(a, b); System.out.println("a и b после вызова: " + a + " " + b); } } Ниже показан вывод, который генерирует программа: а и Ь перед вызовом: 15 20 а и b после вызова: 15 20 Легко заметить, что операции внутри метода noChange ( ) не влияют на зна чения а и Ь, указанные в вызове. При передаче методу объекта ситуация радикально меняется , потому что объекты неявно передаются по ссылке. Имейте в виду, что создание переменной типа класса приводит к созданию ссылки на объект. Методу фактически передается именно ссылка , а не сам объект. В результате передачи такой ссыл ки методу параметр, который ее получает, будет ссылаться на тот же объект, на который ссылается аргумент. Фактически это означает, что объекты передаются методам с помощью вызова по ссылке. Изменения, вносимые в объект внутри метода , оказывают влияние на объект, используемый в качестве аргумента. Например , рассмотрим показанную далее программу: // Объекты передаются через ссылки на них. class Test { int a, b; Test(int i, int j) ( a i; b = j; } /* Передается объект. Теперь ob.a и ob.b в объекте, используемом при вызове, будут изменены . */ void change(Test ob) { ob.a = ob.a + ob.b; ob.b = -ob.b; } } class PassObRef { public static void main(String[] args) { Test ob = new Test(15, 20); System.out.println ("ob.а и ob.b перед вызовом: " + ob.a + " " + ob.b); ob.change(ob); System.out.println("ob.a и ob.b после вызова: " + ob.a + " " + ob.b); } } 228 Java: руководство для начинающих, 9- е издание Вот вывод, генерируемый программой: оЬ.а и ob.b перед вызовом: 15 20 оЬ.а и ob.b после вызова: 35 -20 Как видите , в данном случае действия внутри метода change ( ) влияют на объект, указанный в качестве аргумента. !?@>A8< C M:A?5@B0 ВОПРОС. Существует ли способ передачи данных примитивных типов по ссылке? ОТВЕТ. Не напрямую. Однако в Java определен набор классов, которые служат оболочками для примитивных типов: Double, Float, Byte, Short, Integer, Long и Character. Такие классы -оболочки не только позволяют передавать данные примитивных типов по ссылке, но также предлагают методы , пре доставляющие возможность манипулирования их значениями . Например, оболочки для числовых типов включают методы, которые преобразуют числовое значение в читабельную строковую форму и наоборот. Не забывайте, что при передаче методу объектной ссылки сама ссылка передается с применением вызова по значению. Тем не менее, поскольку передаваемое значение ссылается на объект, копия этого значения по-прежнему будет ссылаться на тот же объект, на который ссылается соответствующий аргумент. Возвращение объектов Метод способен возвращать данные любого типа , включая типы классов. Например , показанный ниже класс ErrorMsg можно использовать для сообщения об ошибках. Его метод getErrorMsg ( ) возвращает объект типа String, который содержит описание ошибки , основанное на переданном коде ошибки. // Возвращение объекта типа String , class ErrorMsg { String[] msgs = { "Ошибка вывода", "Ошибка ввода", "Диск переполнен", "Индекс вышел за границы " }; // Возвращает сообщение об ошибке. String getErrorMsg(int i) Возвращение объекта типа String if(i >=0 & i < msgs.length) return msgs[i]; else return "Несуществующий код ошибки"; } } Глава 6. Дополнительные сведения о методах и классах 229 class ErrMsg { public static void main(String[] args) { ErrorMsg err = new ErrorMsgO ; System.out.println(err.getErrorMsg(2)); System.out.printIn(err.getErrorMsg(19)); } } Ниже показан вывод: Диск переполнен Несуществующий код ошибки Разумеется , можно также возвращать объекты создаваемых классов. Скажем , далее приводится переделанная версия предыдущей программы , в которой создаются два класса ошибок. Один называется Err и инкапсулирует сообщение об ошибке вместе с уровнем серьезности. Второй имеет имя Errorlnfo. В нем определен метод getErrorlnfо ( ) , который возвращает объект Err. // Возвращает объект, определенный программистом. class Err { // сообщение об ошибке String msg; int severity; // код, отражающий уровень серьезности ошибки Err(String m, int s) { msg = m; severity = s; } } class Errorlnfo { String[] msgs = { "Ошибка вывода", "Ошибка ввода", "Диск переполнен", "Индекс вышел за границы " }; int[] howBad = { 3, 3, 2, 4 }; — Err getErrorlnfo(int i) { Возвращение объекта типа Err if(i >= 0 & i < msgs.length ) return new Err(msgs[i], howBad[i]); else return new Err("Несуществующий код ошибки", 0); } } class Errlnfo { public static void main(String[] args) { Errorlnfo err = new Errorlnfo(); Err e; e = err.getErrorlnfo(2); System.out.println(e.msg + ", уровень серьезности ; " + e.severity); e = err.getErrorlnfo(19); System.out.println(e.msg + ", уровень серьезности: " + e.severity); } } 230 Java: руководство для начинающих, 9-е издание Вот вывод, генерируемый программой: Диск переполнен , уровень серьезности: 2 Несуществующий код ошибки, уровень серьезности: О Каждый раз, когда вызывается метод getErrorlnfo ( ) , создается новый объект Err, ссылка на который возвращается вызывающему коду. Затем объект Err используется в методе main ( ) для отображения сообщения об ошибке и уровня серьезности. Когда метод возвращает объект, он существует до тех пор, пока на него не исчезнут ссылки, и тогда он подлежит сборке мусора. Таким образом , объект не уничтожается лишь потому, что создавший его метод завершился. Перегрузка методов В этом разделе вы узнаете об одном из наиболее интересных средств Java: перегрузке методов. В языке Java в одном классе разрешено определять два и более метода , которые имеют одно и то же имя , если объявления их параме тров отличаются . В таком случае говорят, что методы перегружены , а сам процесс называется перегрузкой методов. Перегрузка методов один из способов поддержки полиморфизма в Java . В общем случае для перегрузки метода нужно просто объявить разные его версии , а об остальном позаботится компилятор. Необходимо соблюдать одно важное ограничение: тип и / или количество параметров каждого перегружен ного метода должны различаться . Недостаточно, чтобы два метода отличались только типами возвращаемых значений. (Типы возвращаемых значений не во всех случаях предоставляют достаточно информации , чтобы компилятор Java мог решить, какой метод применять.) Разумеется, перегруженные методы могут иметь отличающиеся типы возвращаемых значений. При вызове перегруженного метода выполняется та версия метода , параметры которой соответствуют аргументам. Ниже показан простой пример , иллюстрирующий перегрузку методов: — // Демонстрация перегрузки методов. class Overload { void ovlDemoO { Первая версия System.out.println("Без параметров"); } // Перегруженная версия ovlDemo с одним параметром типа int. void ovlDemo(int a ) { < Вторая версия System.out.println("Один параметр типа int: " + a); } // Перегруженная версия ovlDemo с двумя параметрами типа int. int ovlDemo(int a , int b) { Третья версия System.out.println("Два параметра типа int: " + a + " " + b); return a + b; } Глава 6. Дополнительные сведения о методах и классах 231 // Перегруженная версия ovlDemo с двумя параметрами типа double , double ovlDemo(double a, double b) { Четвертая версия System.out.println("Два параметра типа double: " + a + " " + b); return a + b; } } class OverloadDemo { public static void main(String[] args) { Overload ob = new OverloadO ; int resl; double resD; // Вызвать все версии ovlDemo(). ob.ovlDemo(); System.out.println (); ob.ovlDemo(2); System.out.println(); resl = ob.ovlDemo(4, 6); System.out.println("Результат вызова ob.ovlDemo(4, 6): " + resl); System.out.println(); resD = ob.ovlDemo(1.1, 2.32); System.out.println("Результат вызова ob.ovlDemo(1.1, 2.32): " + resD); } } Программа генерирует такой вывод: Без параметров Один параметр типа int: 2 Два параметра типа int: 4 6 Результат вызова ob.ovlDemo(4, 6): 10 Два параметра типа double: 1.1 2.32 Результат вызова ob.ovlDemo(1.1, 2.32): 3.42 В коде видно , что метод ovlDemo ( ) перегружается четыре раза. Первая вер сия не принимает параметров , вторая принимает один параметр типа int, тре тья — два параметра типа int, а четвертая — два параметра типа double. Обра тите внимание , что первые две версии ovlDemo ( ) возвращают void, а остальные две возвращают значение . Это совершенно допустимо , но , как уже объяснялось, возвращаемый тип метода не влияет на перегрузку. Таким образом , попытка ис пользования следующих двух версий ovlDemo ( ) приведет к ошибке: // Допустима одна версия ovlDemo(int). Возвращаемые типы не могут void ovlDemo(int a) { использоваться для различения System.out.println("Один параметр типа int: " + a); перегруженных методов } /* Ошибка! Две версии ovlDemo(int) не разрешены , хотя их возвращаемые типы отличаются. */ int ovlDemo(int а) { System.out.println("Один параметр типа int: " + а); return а * а; } 232 Java : руководство для начинающих, 9-е издание Как следует из комментария , для целей перегрузки отличий в возвращаемых типах методов недостаточно . В главе 2 упоминалось , что в Java предусмотрены определенные автомати ческие преобразования типов . Такие преобразования применяются и к параметрам перегруженных методов . Например , взгляните на приведенный далее код: /* Автоматические преобразования типов могут влиять на распознавание перегруженных методов. */ class 0verload2 { void f(int x) { System.out.println ("Внутри f(int): " + x); } void f(double x) { System.out.println("Внутри f(double): " + x); } } class TypeConv { public static void main(String[] args) { 0verload2 ob = new 0verload2(); int i = 10; double d = 10.1; byte b = 99; short s = 10; float f = 11.5F; ob.f(i); // вызывается ob.f(int) ob.f(d); // вызывается ob.f(double) ob.f(b); // вызывается ob.f(int) с преобразованием типа ob.f (s); // вызывается ob.f(int) с преобразованием типа ob.f(f); // вызывается ob . f(double) с преобразованием типа } } Вот вывод , генерируемый программой: Внутри Внутри Внутри Внутри Внутри f(int): 10 f(double): 10.1 f(int): 99 f(int): 10 f(double): 11.5 В этом примере определены только две версии f ( ) : одна с параметром типа i n t и еще одна с параметром типа double. Однако методу f ( ) можно переда вать значение типа byte, short или f l o a t . В случае передачи значений byte и short компилятор Java автоматически преобразует их в тип int . В итоге вы зывается версия f ( int ) . В случае передачи значения float оно преобразуется в тип double и вызывается версия f ( double ) . Тем не менее , важно понимать, что автоматические преобразования применяются только в том случае , если нет прямого соответствия между параметром и аргументом. Например , ниже показана предыдущая программа, в которую до бавлена версия f ( ) с параметром типа byte: Глава 6. Дополнительные сведения о методах и классах // Добавление версии f(byte). class 0verload2 { void f( byte x) { м System.out.println("Внутри f(byte): " + x); 233 В этой версии указан параметр типа byte } void f( int x) { System.out.println("Внутри f(int): " + x); } void f(double x) { System.out.println("Внутри f(double): " + x); } } class TypeConv { public static void main(String[] args) { 0verload2 ob = new 0verload2(); int i = 10; double d = 10.1; byte b = 99; short s = 10; float f = 11.5F; ob.f(i) ob.f(d) ob.f(b) ob.f(s) ob.f(f) вызывается ob.f(int) вызывается ob.f(double) вызывается ob.f(byte) без преобразования типа вызывается ob.f(int) с преобразованием типа // вызывается ob.f(double) с преобразованием типа // // // // } } Теперь в результате выполнения программы получается следующий вывод: Внутри Внутри Внутри Внутри Внутри f(int): 10 f(double): 10.1 f(byte): 99 f(int): 10 f(double): 11.5 Поскольку здесь предусмотрена версия f { ) , которая принимает аргумент типа byte, когда f ( ) вызывается с таким аргументом , то выполняется вызов f(byte) и автоматическое преобразование в int не происходит. Перегрузка методов поддерживает полиморфизм , т. к. он представляет собой один из способов , которым в Java реализуется парадигма “один интерфейс , не сколько методов” . Давайте выясним , каким образом. В языках , не поддержива ющих перегрузку методов , каждому методу должно быть назначено уникальное имя. Однако часто вам потребуется реализовать по существу один и тот же ме тод для разных типов данных. Рассмотрим функцию для абсолютного значения. В языках , не поддерживающих перегрузку, обычно существуют три или более версий такой функции , каждая из которых имеет слегка отличающееся имя . Например , в языке С функция abs ( ) возвращает абсолютное значение целого 234 Java: руководство для начинающих, 9- е издание — числа , labs ( ) — абсолютное значение длинного целого числа , a fabs ( ) абсолютное значение значения с плавающей точкой. Поскольку перегрузка в С не поддерживается, каждая функция имеет собственное имя, хотя все три функции выполняют, по сути , одну и ту же работу, что делает ситуацию концептуально более сложной , чем она есть на самом деле. Хотя базовая концепция каждой функции одна и та же , вам нужно запомнить все три имени . В языке Java ситуация подобного рода не возникает, потому что каждый метод для абсолютного значения может иметь одно и то же имя. Действительно, стандартная библиотека классов Java включает метод получения абсолютного значения , называемый abs ( ) . Этот метод перегружен в классе Math для обработки всех числовых ти пов. Компилятор Java определяет, какую версию abs ( ) вызывать, основываясь на типе аргумента. Ценность перегрузки обусловлена тем , что она позволяет получить доступ к связанным методам с применением общего имени . Таким образом , имя abs представляет выполняемое общее действие. Выбор правильной конкретной версии в сложившихся обстоятельствах возлагается на компилятор. Вам как про граммисту достаточно лишь запомнить общую выполняемую операцию. Благодаря полиморфизму несколько имен были сведены в одно. Хотя приведенный пример довольно прост, если вы расширите концепцию, то увидите, каким образом перегрузка может помочь справиться с более высокой сложностью. В случае перегрузки метода каждая его версия может выполнять любые желаемые действия. Нет правила , утверждающего о том , что перегруженные методы должны быть связаны друг с другом. Тем не менее , со стилистической точ ки зрения перегрузка методов подразумевает наличие взаимоотношения между ними. Таким образом , хотя и допускается использовать одно и то же имя для перегрузки несвязанных методов, вы не должны поступать так. Скажем, вы можете выбрать имя sqr при создании методов , возвращающих квадрат целого числа и квадратный корень значения с плавающей запятой. Но эти две операции принципиально разные. Применение перегрузки метода в подобной манере противоречит его первоначальной цели. На практике следует перегружать только тесно связанные операции. !?@>A8< C M:A?5@B0 ВОПРОС. Что означает термин сигнатура, используемый программистами на Java? ОТВЕТ. Применительно к Java сигнатура - это имя метода плюс список его параметров. Таким образом , перегрузка требует, чтобы никакие два метода в одном классе не имели одинаковую сигнатуру. Обратите внимание, что сигнатура не включает возвращаемый тип , поскольку он не используется компилятором Java при распознавании перегруженных версий. Глава 6. Дополнительные сведения о методах и классах 235 Перегрузка конструкторов Как и методы , конструкторы тоже можно перегружать, что позволяет созда вать объекты различными способами . Например , рассмотрим следующую программу: // Демонстрация перегрузки конструкторов , class MyClass { int х; MyClass() { System.out.println("Внутри MyClass()."); x = 0; } Конструирование объектов различными способами MyClass(int i) { System.out.println("Внутри MyClass(int)."); x = i; } MyClass(double d) { System.out.println("Внутри MyClass(double)."); x = (int) d; } MyClass(int i, int j) { System.out.println("Внутри MyClass(int, int)."); x = i * j; } } class OverloadConsDemo { public static void main(String[] args) { MyClass tl = new MyClass(); MyClass t2 = new MyClass(88); MyClass t3 = new MyClass(17.23); MyClass t4 = new MyClass [ 2 , 4); System.out.println("tl.x: System.out.println("t2.x: System.out.println("t3.x: System.out.println("t4.x: " + " + " + " + tl.x); t2.x); t3.x); t4.x); } } Вот вывод, генерируемый программой: Внутри MyClass(). Внутри MyClass(int). Внутри MyClass(double). Внутри MyClass(int, int). tl.x: 0 t2.x: 88 t3.x: 17 t4.x: 8 236 Java: руководство для начинающих, 9-е издание Конструктор MyClass ( ) перегружен четыре раза и в каждой перегруженной версии объект создается по-другому. Подходящая версия конструктора вызыва ется на основе параметров, указанных при выполнении операции new. Перегружая конструктор класса , вы предоставляете его пользователю свободу в выборе способа создания объектов. Чаще всего конструкторы перегружаются для того , чтобы позволить одному объекту инициализировать другой. В качестве примера рассмотрим показанную ниже программу, в которой используется класс Summation для вычисления суммы целочисленных значений , начиная с единицы и заканчивая указанным значением: // Инициализация одного объекта с помощью другого. class Summation { int sum; // Конструирование на основе значения типа int. Summation(int num) { м Конструирование одного объекта из другого sum = 0; for(int i=l; i <= num; i++) sum += i; } // Конструирование на основе другого объекта. Summation(Summation ob) { sum = ob.sum; } } class SumDemo { public static void main(String[] args) { Summation si = new Summation(5); Summation s2 = new Summation(si); System.out.println("si.sum: " + si.sum); System.out.println("s2.sum: " + s2.sum); } } Вывод выглядит так: si.sum: 15 s2.sum: 15 Зачастую, как иллюстрирует приведенный выше пример, преимущество кон структора , который применяет один объект для инициализации другого , заклю чается в эффективности. В данном случае при создании s 2 нет необходимости пересчитывать сумму. Разумеется , даже в тех ситуациях, когда эффективность не является проблемой, часто бывает полезно предоставить конструктор, который создает копию объекта. Глава 6. Дополнительные сведения о методах и классах 237 Упражнение 6.2 Перегрузка конструктора класса Queue В этом проекте класс Queue будет расширен двумя дополни тельными конструкторами. Первый конструктор создаст новую очередь из другой очереди , а второй построит очередь, задавая ей начальные значения . Как вы увидите , добавление таких конструкторов существенно повышает удобство использования Queue. 1 Создайте файл по имени QDemo2.java и скопируйте в него код класса Queue из упражнения 6.1. 2. Добавьте следующий конструктор , который создает очередь на основе очереди. QDemo2.java . // Конструирует объект Queue на основе объекта Queue. Queue( Queue ob) { putloc = ob.putloc; getloc = ob.getloc; q = new char[ob.q.length]; // Копировать элементы . for(int i=getloc; i < putloc; i++) q[i] = ob.q[i]; } Рассмотрим этот конструктор более внимательно. Он инициализирует putloc и getloc значениями, содержащимися в параметре ob, затем выделяет память под новый массив для хранения очереди и копирует в дан ный массив элементы из ob. После создания новая очередь будет иден тичной копией исходной , но обе будут полностью отдельными объектами. 3 Добавьте конструктор , который инициализирует очередь посредством символьного массива: . // Конструирует объект Queue с начальными значениями. Queue(char[] а) { putloc = 0; getloc = 0; q = new char[a.length]; for(int i = 0; i < a.length; i++) put(a[i]); } Этот конструктор создает очередь с размером, достаточным для хранения символов из массива а , и помещает все символы в очередь. 4. Ниже показан код модифицированного класса Queue и класса QDemo2, демонстрирующего работу с ним: 238 Java: руководство для начинающих, 9-е издание // Класс очереди для символов , class Queue { // массив, хранящий данные очереди private char[] q; private int putloc, getloc;// индексы для позиций помещения и извлечения // Конструирует пустой объект Queue на основе заданного размера. Queue(int size) { // выделение памяти под очередь q = new char[size]; putloc = getloc = 0; } // Конструирует объект Queue на основе объекта Queue. Queue(Queue ob) { putloc = ob.putloc; getloc = ob.getloc; q = new char[ob.q.length]; // Копировать элементы , for(int i=getloc; i < putloc; i++) q[i] = ob.q[i]; } // Конструирует объект Queue с начальными значениями. Queue( char[] a) { putloc = 0; getloc = 0; q = new char[a.length]; for(int i = 0; i < a.length; i++) put(a[i]); } // Поместить символ в очередь , void put(char ch ) { if( putloc==q.length) { System.out.println (" - Очередь переполнена."); return; } q[putloc++ ] = ch; } // Извлечь символ из очереди , char get() { putloc) { if(getloc System.out.println(" - Очередь пуста."); return (char) 0; } return q[getloc++]; } } // Демонстрация использования класса Queue , class QDemo2 { public static void main(String[] args) { // Создать пустую очередь из 10 элементов. Queue ql = new Queue(10); Глава 6. Дополнительные сведения о методах и классах 239 char[] name = {'Т', o', m * }; // Создать очередь из массива. Queue q2 = new Queue(name); char ch; int i; // Поместить ряд символов в ql. for(i=0; i < 10; i++) ql.put((char) ('A' + i)); // Создать очередь из другой очереди. Queue q3 = new Queue(ql); // Отобразить содержимое очередей. System.out.print("Содержимое ql: "); for(i=0; i < 10; i++) { ch = ql.get(); System.out.print(ch); } System.out.printIn("\n"); System.out.print("Содержимое q2: "); for(i=0; i < 3; i++) { ch = q2.get(); System.out.print(ch); } System.out.println("\n"); System.out.print("Содержимое q3: "); for(i=0; i < 10; i++) { ch = q3.get(); System.out.print(ch); } } } Вот вывод , генерируемый программой: Содержимое ql: ABCDEFGHIJ Содержимое q2: Tom Содержимое q3: ABCDEFGHIJ Рекурсия В языке Java метод может вызывать сам себя. Такой процесс именуется рекурсией , а метод , вызывающий сам себя , называется рекурсивным. Рекурсия представляет собой процесс определения чего - либо в терминах самого себя , чем - то похожий на циклическое определение . Ключевым компонентом рекур сивного метода является оператор , который выполняет вызов самого метода . Рекурсия — мощный механизм управления . 240 Java: руководство для начинающих, 9-е издание Классическим примером рекурсии считается вычисление факториала числа. Факториал числа N это произведение всех целых чисел от 1 до N. Скажем , факториал 3 равен 1 x 2 x 3, или 6. В следующей программе показано, как вычислить факториал с помощью рекурсивного метода. Для сравнения включен также эквивалент без рекурсии. — // Простой пример рекурсии , class Factorial { // Рекурсивный метод , int factR(int n) { int result; if(n== l) return 1; result = factR(n-l ) * n; • return result' } t Выполнение рекурсивного вызова factR() // Итеративный эквивалент рекурсивного метода , int factl(int n) { int t, result; result = 1; for(t=l; t <= n; t++) result *= t; return result; } } class Recursion { public static void main(Stringf ] args) { Factorial f = new Factorial(); System.out.println("Вычисление факториалов рекурсивным методом."); System.out.println("Факториал 3 равен " + f.factR(3)); System.out.println("Факториал 4 равен " + f.factR(4)); System.out.println("Факториал 5 равен " + f.factR(5)); System.out.println(); System.out.println("Вычисление факториалов итеративным методом."); System.out.println ("Факториал 3 равен " + f.factl(3)); System.out.println("Факториал 4 равен " + f.factl(4)); System.out.println("Факториал 5 равен " + f.factl(5)); } } Ниже показан вывод программы: Вычисление факториалов рекурсивным методом. Факториал 3 равен б Факториал 4 равен 24 Факториал 5 равен 120 Вычисление факториалов итеративным методом. Факториал 3 равен 6 Факториал 4 равен 24 Факториал 5 равен 120 Глава 6. Дополнительные сведения о методах и классах 241 То , как работает нерекурсивная версия f a c t l ( ) , должно быть понятно. В ней применяется цикл , начинающийся с 1, где каждое число умножается на постепенно вычисляемое произведение. Работа рекурсивной версии f actR ( ) чуть сложнее . Когда метод f actR ( ) вы зывается с аргументом 1, он возвращает 1; в противном случае возвращается произведение f a c t R ( n- l ) * п . Для вычисления такого выражения f a c t R O вы зывается с параметром п -1. Процесс повторяется до тех пор , пока значение п не станет равным 1 и не начнется возврат из вызовов метода. Например , при вычислении факториала 2 первый вызов f a c t R ( ) приводит ко второму вызову с аргументом 1. Этот вызов вернет значение 1, которое затем умножается на 2 ( исходное значение п ). Ответом будет 2 . Возможно , вам будет интересно вставить операторы p r i n t I n ( ) в f a c t ( ) , которые будут показывать, на каком уровне находится каждый вызов и каковы промежуточные ответы . Когда метод вызывает сам себя , новые локальные переменные и параметры размещаются в стеке, а код метода выполняется с этими новыми переменными с самого начала . При возврате из каждого рекурсивного вызова старые локальные переменные и параметры удаляются из стека, и выполнение возобновляется в точке вызова внутри метода . Можно сказать, что рекурсивные методы ра ботают в стиле “ раздвижения ” и “ складывания ” подзорной трубы. Рекурсивные версии многих подпрограмм могут выполняться немного медленнее своих итеративных эквивалентов из- за добавочных накладных расходов на дополнительные вызовы методов. Большое количество рекурсивных вызовов метода может привести к переполнению стека . Поскольку хранилище для параметров и локальных переменных находится в стеке , и каждый новый вызов создает новую копию этих переменных, вполне возможно , что стек исчерпается. В таком случае исполняющая среда Java инициирует исключение. Тем не менее, как правило , проблема не возникает, если рекурсивная подпрограмма не выходит из- под контроля. Главное преимущество рекурсивных методов связано с тем , что их можно использовать для создания более ясных и простых версий ряда алгоритмов, чем их итеративные аналоги. Скажем, алгоритм быстрой сортировки довольно сложно реализовать итеративным способом. Кроме того , некоторые типы алгоритмов из области искусственного интеллекта проще всего реализовывать с помощью рекурсивных решений . При написании рекурсивных методов вы должны где-то предусмотреть оператор if , чтобы заставить метод выполнить возврат без рекурсивного вызова . Если вы этого не сделаете , то после вызова возврат из метода никогда не произойдет. Такая ошибка весьма распространена , когда приходится иметь дело с рекурсией . Свободно применяйте операторы p r i n t l n ( ) во время разработки , чтобы вы могли следить за происходящим и прерывать выполнение, если види те, что допустили ошибку. 242 Java: руководство для начинающих, 9- е издание Ключевое слово static Временами вам понадобится определять член класса , который будет при меняться независимо от любого объекта данного класса. Обычно доступ к чле ну класса должен осуществляться только в сочетании с объектом его класса , но есть возможность создать член , который можно использовать сам по себе , без привязки к конкретному экземпляру. Чтобы создать такой элемент, перед его объявлением следует указать ключевое слово static (статический). Когда член объявляется статическим , к нему можно получать доступ до того , как будут созданы какие-либо объекты его класса , и без ссылки на какой-либо объект. Объявлять статическими можно как методы, так и переменные. Наиболее распространенным примером статического члена является метод main ( ) , который объявлен как static, потому что он должен быть вызван машиной JVM при запуске вашей программы. Для доступа к статическому члену за пределами класса нужно только указать имя его класса с последующей операцией точки. Нет необходимости создавать объект. Например, вот как присвоить значение 10 статической переменной по имени count , которая является частью класса Timer: Timer.count = 10; Такой формат подобен тому, который применяется для доступа к обычным переменным экземпляра через объект, за исключением того , что используется имя класса. Статический метод можно вызывать аналогичным образом с помощью операции точки после имени класса. Переменные экземпляра , объявленные как static , по существу являются глобальными переменными. При объявлении объектов такого класса копия статической переменной не создается. Взамен все экземпляры класса имеют дело с одной и той же статической переменной. Ниже приведен пример, иллюстрирующий отличия между статической переменной и переменной экземпляра: — // Использование статической переменной , class StaticDemo { // нормальная переменная экземпляра int х; static int у; // статическая переменная Все объекты совместно используют // Возвращает сумму переменной экземпляра х // и статической переменной у. int sum( ) { одну копию переменной у return х + у; } } class SDemo { public static void main(String[] args) { StaticDemo obi = new StaticDemo(); StaticDemo ob2 = new StaticDemo(); // Каждый объект имеет собственную копию переменной экземпляра , obl.x = 10; оЬ2 ,х = 20; Глава 6. Дополнительные сведения о методах и классах 243 System.out.println("Разумеется, obl.x и оЬ2.х " + "являются независимыми."); System.out.println("obi.x: " + obl.x + "\nob2. x: " + ob2.x); System.out.println(); // Все объекты совместно используют одну копию статической переменной System.out.println("Статическая переменная у является общей."); StaticDemo.y = 19; System.out.println("Установка значения StaticDemo.y в 19."); System ,out.println("obi.sum(): " + obl.sumO ); System.out.println("ob2.sum(): " + ob2.sum()); System.out.println(); StaticDemo.y = 100; System.out.println("Изменение значения StaticDemo.y на 100."); System ,out.println("obi.sum(): " + obl.sumO ); System.out.println("ob2.sum(): " + ob2.sum()); System.out.println(); } } Вывод, генерируемый программой , выглядит следующим образом: Разумеется, obl.x и оЬ2.х являются независимыми , obl.x: 10 оЬ2.х: 20 Статическая переменная у является общей. Установка значения StaticDemo.y в 19. obi.sum(): 29 ob2.sum( ): 39 Изменение значения StaticDemo.y на 100. оЫ .sum(): 110 ob2.sum(): 120 Легко заметить, что статическая переменная у является общей для obi и оЬ2. Ее изменение влияет на весь класс, а не только на экземпляр. Разница между статическим методом и нормальным методом заключается в том, что статический метод вызывается через имя своего класса без создания какого-либо объекта этого класса. Вы уже видели пример метод sqrt ( ) , который является статическим методом в стандартном классе Math. Вот пример создания статического метода: — // Использование статического метода , class StaticMeth { static int val = 1024; // статическая переменная // статический метод static int valDiv2() { return val/2; } } Статический метод 244 Java: руководство для начинающих, 9-е издание class SDemo2 { public static void main(String[] args) { System.out.println( "val: " + StaticMeth.val); System.out.println("StaticMeth.valDiv2(): " + StaticMeth.valDiv2()); StaticMeth.val = 4; System.out.println( "val: " + StaticMeth.val); System.out.println ("StaticMeth.valDiv2(): " + StaticMeth.valDiv2()); } } Вот вывод: val: 1024 StaticMeth.valDiv2(): 512 val: 4 StaticMeth.valDiv2(): 2 С методами , объявленными как static, связано несколько ограничений . # Они могут напрямую вызывать только другие статические методы своего класса. # Они могут напрямую получать доступ только к статическим переменным своего класса . # Они не имеют ссылки this. Скажем, статический метод valDivDenomO в следующем классе недопустим: class StaticError { int denom = 3; static int val = 1024; // нормальная переменная экземпляра // статическая переменная /* Ошибка! Доступ к нестатической переменной внутри статического метода не разрешен. */ static int valDivDenom() { // He скомпилируется! return val/denom ; } } Здесь denom — нормальная переменная экземпляра, доступ к которой внутри статического метода не разрешен . Статические блоки Иногда классу необходима некоторая инициализация , прежде чем он будет готов к созданию объектов. Например, перед применением любого статического метода класса может потребоваться установить соединение с удаленным сай том либо инициализировать определенные статические переменные. Для обра ботки подобных ситуаций Java позволяет объявлять статический блок, который выполняется при первой загрузке класса , т.е. до того , как класс можно будет использовать для других целей. Ниже показан пример статического блока: Глава 6. Дополнительные сведения о методах и классах 245 // Использование статического блока , class StaticBlock { static double root0f2; static double root0f3; static { Этот блок выполняется System.out.println("Внутри статического блока."); при загрузке класса root0f2 = Math.sqrt(2.0); rootOf3 = Math.sqrt(3.0); } StaticBlock(String msg) { System.out.println(msg ); } } class SDemo3 { public static void main(String[] args) { StaticBlock ob = new StaticBlock("Внутри конструктора."); System.out.println("Квадратный корень 2: " + StaticBlock.rootOf2); System.out.println("Квадратный корень 3: " + StaticBlock.rootOf3); } } Программа генерирует следующий вывод: Внутри статического блока. Внутри конструктора. Квадратный корень 2: 1.4142135623730951 Квадратный корень 3: 1.7320508075688772 Как видите , статический блок выполняется перед конструированием любых объектов. Упражнение 6.3 | Быстрая сортировка В главе 5 был представлен простой метод сортировки — пу зырьковая сортировка . Там же упоминалось о том , что существуют гораздо лучшие алгоритмы сортировки. В этом упражнении будет разработана версия одного из наиболее эффективных алгоритмов , который называется быстрой сортировкой ( Quicksort ). Быстрая сортировка , изобретенная и названная так Чарльзом Хоаром , возможно , является наилучшим алгоритмом сортировки общего назначения, доступным в настоящее время . Причина , по которой его нельзя было продемонстрировать в главе 5, связана с тем , что лучшие реализации быстрой сортировки основаны на рекурсии . Разрабатываемая версия рассчитана на сортировку массива символов, но логику можно адаптировать для сортировки объектов любого типа. Быстрая сортировка основана на идее разделения. Общая процедура пред усматривает выбор разделяющего значения (т.н . компаранда ) и последующее QSDemo.java 246 Java: руководство для начинающих, 9- е издание разделение массива на две части. Все элементы , которые больше разделяю щего значения или равны ему, помещаются в одну часть, а элементы, которые меньше разделяющего значения , помещаются в другую часть. Затем процесс повторяется для всех остальных частей до тех пор, пока массив не будет отсортирован. Например , при наличии массива fedacb и выборе d в качестве разделяющего значения первый проход быстрой сортировки переупорядочивает массив следующим образом: Исходные данные fedacb Проход 1 bcadef Далее описанный процесс повторяется для каждой части, т.е. Ьса и def. Как видите, процесс по своей сути рекурсивен и действительно классическая реали зация быстрой сортировки является рекурсивной. Предполагая отсутствие информации о распределении данных, подлежащих сортировке , существует несколько способов выбора разделяющего значения . Рассмотрим два из них. Выбрать разделяющее значение можно случайным образом внутри данных или же путем усреднения небольшого набора значений , взятых из данных. Для оптимальной сортировки необходимо значение, которое находится точно в середине диапазона значений. Однако часто такой вариант нецелесообразен. В худшем случае выбранное значение находится на одном краю. Тем не менее , даже в такой ситуации быстрая сортировка по- прежнему будет работать корректно. Разрабатываемая в этом упражнении версия быстрой сортировки предусматривает выбор в качестве разделяющего значения среднего элемента массива. 1. Создайте файл по имени QSDemo.java. 2. Создайте класс Quicksort следующего вида: // Упражнение 6.3. Простая версия быстрой сортировки , class Quicksort { // Настраивает вызов фактического метода быстрой сортировки , static void qsort(char[] items) { qs(items, 0, items.length-1); } // Рекурсивная версия быстрой сортировки для символов. private static void qs( char[] items, int left, int right) { int i , j; char x, y; i = left; j = right; x = items[(left+right)/2]; do { while((items[i] < x) && (i < right)) i++; while((x < items[j]) && (j > left)) j ; — Глава 6. Дополнительные сведения о методах и классах 247 if(i <= j) { у = items[i]; items[i] = items[j]; items[j] = y; i++; j--; } } while(i <= j); if(left < j) qs(items, left, j); if(i < right) qs(items, i, right); } } Чтобы сохранить интерфейс простым , в классе Quicksort предоставля ется метод qsort ( ) , который устанавливает вызов фактического метода быстрой сортировки qs(). В итоге появляется возможность вызова бы строй сортировки с применением только имени сортируемого массива , не указывая начальное разделение. Поскольку метод qs ( ) используется только для внутренних целей , он объявлен закрытым. 3. Для применения быстрой сортировки понадобится просто вызвать Quicksort.qsort ( ) . Так как метод qsort ( ) является статическим , он может быть вызван через его класс , а не через объект. Таким образом , нет нужды в создании объекта Quicksort. После возврата из вызова массив будет отсортирован. Помните , что эта версия работает только для сим вольных массивов, но логику можно адаптировать для сортировки масси вов любого желаемого типа. 4. Ниже показана программа , демонстрирующая использование класса Quicksort: — // Упражнение 6.3. Простая версия быстрой сортировки , class Quicksort { class Quicksort { // Настраивает вызов фактического метода быстрой сортировки , static void qsort(char[] items) { qs(items, 0, items.length-1); } // Рекурсивная версия быстрой сортировки для символов , private static void qs(char[] items, int left, int right) { int i, j; char x , y; i x left; j = right; = items[(left+right)/2]; do { while((items[i] < x) && (i < right)) i++; while((x < items[j]) && (j > left)) j ; — if(i <= j) { у = items[i]; 248 Java : руководство для начинающих, 9-е издание items[i] = items[j]; items[j] = y; i++; j ; } — } while(i <= j); if(left < j) qs(items, left, j); if(i < right) qs(items, i, right); } } class QSDemo { public static void main(String[] args) { char[] a { 'd\ x ' , a', r ' , P ' / int i; j', i* }; System.out.print("Исходный массив: "); for(i=0; i < a.length; i++) System.out.print(a[i]); System.out.println(); // Отсортировать массив. Quicksort.qsort(a); System.out.print("Отсортированный массив: "); for(i=0; i < a.length; i++) System.out.print(a[i]); } } Введение во вложенные и внутренние классы В языке Java можно определять вложенные классы . Вложенный класс объявляется внутри другого класса . Откровенно говоря, вложенные классы относятся к более сложным темам. На самом деле они даже не поддерживались вплоть до выхода версии Java 1.1. Однако необходимо знать, что они собой представляют и как применяются , поскольку вложенные классы играют важную роль во многих реальных программах. Вложенный класс не существует независимо от своего объемлющего класса . Таким образом, область действия вложенного класса ограничена его внешним классом. Вложенный класс , объявленный внутри области действия объемлющего класса , становится членом последнего. Допускается также объявлять вложен ный класс, который является локальным по отношению к какому-то блоку. Различают два основных вида вложенных классов: те, что объявлены с моди фикатором s t a t i c, и те, что объявлены без него. В книге будет рассматриваться только их нестатическая разновидность. Вложенные классы такого типа также называются внутренними классами. Внутренний класс имеет доступ ко всем переменным и методам своего внешнего класса и может ссылаться на них напрямую подобно всем остальным нестатическим членам внешнего класса. Глава 6. Дополнительные сведения о методах и классах 249 Иногда внутренний класс используется для предоставления набора служб , необходимых только объемлющему классу. Далее приведен пример , в котором внутренний класс вычисляет различные значения , предназначенные для объемлющего класса: // Использование внутреннего класса , class Outer { int[] nums; Outer(int[] n) { nums = n; } void analyze() { Inner inOb = new Inner(); System.out.println ("Минимальное значение: " + inOb.minO ); System.out.println("Максимальное значение: " + inOb.maxO ); System.out.println("Среднее значение: " + inOb.avgO ); } // Внутренний класс. class Inner { Внутренний класс int min() { int m = nums[0]; for(int i=l; i < nums.length; i++) if(nums[i] < m) m = nums[i]; return m; } int max() { int m = nums[0]; for(int i=l; i < nums.length; i++) if(nums[i] > m) m = nums[i]; return m; } int avg() { int a = 0; for(int i=0; i < nums.length; i++) a += nums[i]; return a / nums.length; } } } class NestedClassDemo { public static void main(String[] args) { int[] x = { 3, 2, 1, 5, 6, 9, 7, 8 }; Outer outOb = new Outer(x); outOb.analyze(); } } 250 Java : руководство для начинающих, 9-е издание Вот вывод, генерируемый программой: Минимальное значение: 1 Максимальное значение: 9 Среднее значение: 5 В этом примере внутренний класс Inner вычисляет различные значения для массива nums, который является членом класса Outer. Ранее объяснялось , что внутренний класс имеет доступ к членам своего объемлющего класса , а потому для Inner вполне допустим прямой доступ к массиву nums, но , разумеется , не наоборот. Скажем , внутри метода analyze ( ) нельзя вызывать метод min ( ) на прямую , не создавая объект Inner. Как уже упоминалось, класс можно вложить в область действия блока . В ре зультате просто создается локальный класс , неизвестный за пределами его бло ка. В следующем примере класс ShowBits , разработанный в упражнении 5.3, адаптируется для применения в качестве локального класса . // Использование ShowBits в качестве локального класса. class LocalClassDemo { public static void main(String[] args) { // Версия ShowBits в виде внутреннего класса , class ShowBits { Локальный класс, вложенный внутрь метода int numbits; ShowBits(int n) { numbits = n; } void show(long val) { long mask = 1; // Сдвиг влево значения 1 в нужную позицию , mask <<= numbits-1; int spacer = 0; for(; mask != 0; mask »>= 1) { if((val & mask) != 0) System.out.print("1"); else System.out.print("0"); spacer++; if((spacer % 8) == 0) { System.out.print(" "); spacer = 0; } } System.out.printIn(); } } for(byte b = 0; b < 10; b++) { ShowBits byteval = new ShowBits(8); System.out.print(b + " в двоичном виде: "); byteval.show(b); } } } Глава 6. Дополнительные сведения о методах и классах 251 Ниже показан вывод, генерируемый программой: О в в в в в в двоичном виде: двоичном виде: двоичном виде: двоичном виде: двоичном виде: двоичном виде: в двоичном виде: в двоичном виде: 00000000 00000001 00000010 00000011 00000100 00000101 00000110 00000111 в двоичном виде: 00001000 9 в двоичном виде: 00001001 1 2 3 4 5 6 7 8 В этом примере класс ShowBits неизвестен за пределами метода main ( ) , так что попытка доступа к нему из любого метода кроме main ( ) приведет к ошибке. И последнее замечание: можно создавать внутренний класс , не имеющий имени, который называется анонимным внутренним классом. Объект анонимного внутреннего класса создается при объявлении класса с использованием операции new. Анонимные внутренние классы обсуждаются в главе 17. !?@>A8< C M:A?5@B0 ВОПРОС. Чем статический вложенный класс отличается от нестатического? ОТВЕТ. Статический вложенный класс объявляется с модификатором static. Будучи статическим , он имеет прямой доступ только к остальным статическим членам объемлющего класса. Доступ к другим членам его внешнего класса должен осуществляться через объектную ссылку. Аргументы переменной длины Иногда вам может понадобиться создать метод , который принимает переменное количество аргументов в зависимости от его точного использования . Например, метод, который открывает подключение к Интернету, может прини мать имя пользователя , пароль, имя файла , протокол и т.д. , но предоставлять стандартные значения, если часть этой информации не была указана. В такой ситуации было бы удобно передавать только те аргументы , к которым не применяются стандартные значения . Создание метода подобного рода подразумевает, что должен быть какой -то способ создания списка аргументов переменной, а не фиксированной длины. В ранних версиях Java аргументы переменной длины можно было поддерживать двумя способами, ни один из которых нельзя считать удобным. Первый способ , подходящий в ситуации , когда максимальное количество аргументов является небольшим и известным , предусматривал создание перегруженных версий метода , по одной для каждого варианта вызова метода . Хотя подобный 252 Java: руководство для начинающих, 9-е издание подход работает и подходит в ряде случаев, он применим только к узкому набору ситуаций. В тех случаях , когда максимальное количество потенциальных аргументов было большим или неизвестным , использовался второй подход, предусматривающий помещение аргументов в массив, который затем передавался методу. К сожалению, оба подхода часто приводили к неуклюжим решениям , и со временем стало ясно, что необходим более эффективный подход. К счастью , в состав современных версий Java входит средство , упрощающее создание методов, которые должны принимать произвольное количество аргументов. Оно называется аргументами переменной длины ( variable - length arguments varargs). Метод , принимающий произвольное число аргументов , называется методом с переменной арностью или методом с аргументами перемен ной длины . Список параметров для метода с аргументами переменной длины не является фиксированным , а имеет произвольную длину. Таким образом , метод с аргументами переменной длины может принимать непостоянное количество аргументов. — Основы аргументов переменной длины Аргумент переменной длины определяется с помощью трех точек ( . . . ) • Н апример , вот как определить метод vaTest ( ) , принимающий аргумент перемен ной длины: Объявление аргумента переменной длины // Метод vaTest() принимает аргумент переменной длины , static void vaTest(int ... v) { < System.out.println("Количество аргументов: " + v.length); System.out.printIn("Содержимое: "); for(int i=0; i < v.length; i++) System ,out.println(" аргумент " + i + ": " + v[i]); System.out.println(); } Обратите внимание на объявление v: int ... v Этот синтаксис сообщает компилятору о том , что метод vaTest ( ) можно вызывать с нулем или большим числом аргументов. В результате v неявно объявляется как массив типа int [ ] . Таким образом , внутри vaTest( ) доступ к v осуществляется с использованием обычного синтаксиса массива. Далее приведен полный код программы , демонстрирующей применение метода vaTest(): // Демонстрация использования аргумента переменной длины class VarArgs { . // Метод vaTest() принимает аргумент переменной длины , static void vaTest(int ... v) { System.out.println("Количество аргументов: " + v.length); System.out.println("Содержимое: "); Глава 6. Дополнительные сведения о методах и классах for(int i=0; i < v.length; i++) System.out.println(" аргумент " + i + 253 " + v[i]); System.out.println(); } public static void main(String [] args) { // Обратите внимание на то, что метод vaTestO можно вызывать // с переменным количеством аргументов. // один аргумент vaTest(10); Вызовы с разным // три аргумента vaTest(1, 2, 3); количеством аргументов // без аргументов vaTest(); } } Вот вывод, генерируемый программой: Количество аргументов: 1 Содержимое: аргумент 0: 10 Количество аргументов: 3 Содержимое: аргумент 0: 1 аргумент 1: 2 аргумент 2: 3 Количество аргументов: 0 Содержимое: В приведенной выше программе нужно отметить два важных момента . Во- первых , переменная v в методе vaTest ( ) обрабатывается как массив. Дело в том, что v и является массивом. Синтаксис . . . просто сообщает компилятору, что будет использоваться переменное число аргументов, причем аргументы будут храниться в массиве , на который ссылается v. Во-вторых , метод vaTest ( ) вызывается внутри main ( ) с разным количеством аргументов, включая вариант вообще без аргументов. Аргументы автоматически помещаются в массив и передаются v. При отсутствии аргументов длина массива равна нулю. Наряду с параметром переменной длины метод может иметь и “ нормальные ” параметры. Тем не менее, параметр переменной длины должен объявляться в методе последним. Скажем , следующее объявление метода совершенно допустимо: int doIt(int a, int b, double c, int ... vals) { В этом случае первые три аргумента, указанные в вызове dolt(), сопоставляются с первыми тремя параметрами , а все остальные аргументы считаются относящимися к vals. Ниже показана модифицированная версия метода vaTest(), которая при нимает нормальный аргумент и аргумент переменной длины: 254 Java: руководство для начинающих, 9-е издание // Использование аргумента переменной длины вместе со стандартными аргументами class VarArgs2 { // Здесь msg - нормальный параметр, a v - параметр переменной длины , static void vaTest(String msg, int ... v) { "Нормальный" параметр и параметр переменной длины System.out.println(msg + v.length); System.out.println("Содержимое: "); for(int i=0; i < v.length; i ++) System ,out.println(" аргумент " + i + " + v[i]); System.out.println(); } public static void main(String[] args) { vaTest("Один аргумент в параметре переменной длины : ", 10); vaTest("Три аргумента в параметре переменной длины : ", 1, 2, 3); vaTest("Без аргументов в параметре переменной длины : "); } } Далее представлен вывод , генерируемый программой: Один аргумент в параметре переменной длины : 1 Содержимое: аргумент 0: 10 Три аргумента Содержимое: аргумент 0: аргумент 1: аргумент 2: в параметре переменной длины : 3 1 2 3 Без аргументов в параметре переменной длины : 0 Содержимое: Не забывайте , что параметр переменной длины должен быть последним. Например , показанное далее объявление некорректно: int dolt(int a, int b, double c, int ... vals, boolean stopFlag) {// Ошибка! Здесь предпринимается попытка объявить обычный параметр после параме тра переменной длины , что недопустимо . Существует еще одно ограничение , о котором следует помнить: должен быть только один параметр переменной дли ны . Скажем , приведенное ниже объявление тоже будет ошибочным: int dolt(int a, int b, double c, int ... vals, double ... morevals) ( // Ошибка! Объявлять второй параметр переменной длины не разрешено . Перегрузка методов с аргументами переменной длины Метод , принимающий аргумент переменной длины , можно перегружать. Например, в следующей программе метод vaTest ( ) перегружается трижды: Глава 6. Дополнительные сведения о методах и классах 255 // Перегрузка метода с аргументом переменной длины . Первая версия метода vaTest ( ) class VarArgs3 { 1 static void vaTest(int ... v) { System.out.println("vaTest(int ...): " + "Количество аргументов: " + v.length); System.out.println("Содержимое: "); for(int i=0; i < v.length; i++) System out.println(" аргумент " + i + ": " + v[i]); , System.out.println(); Вторая версия метода vaTest ( ) } 1 static void vaTest(boolean ... v) { System.out.println("vaTest(boolean ...): " + "Количество аргументов: " + v.length); System.out.println("Содержимое: "); for(int i=0; i < v.length; i++) System ,out.println( " аргумент " + i + ": " + v[i]); System.out.println(); Третья версия метода vaTest ( ) } — static void vaTest(String msg, int ... v) { System.out.println("vaTest(String, int ...): " + msg + v.length); System.out.println("Содержимое: "); for(int i=0; i < v.length; i++) System ,out.println( " аргумент " + i + ": " + v[i]); System.out.println(); } public static void main(String[] args) { vaTest(1, 2, 3); vaTest("Тестирование: ", 10, 20); vaTest(true, false, false); } } Вот вывод, генерируемый программой: vaTest(int ...): Количество аргументов: 3 Содержимое: аргумент 0: 1 аргумент 1: 2 аргумент 2: 3 vaTest(String, int ...): Тестирование: 2 Содержимое: аргумент 0: 10 аргумент 1: 20 vaTest(boolean ...): Количество аргументов: 3 Содержимое: аргумент 0: true аргумент 1: false аргумент 2: false 256 Java: руководство для начинающих, 9-е издание В представленной выше программе иллюстрируются оба способа перегрузки метода с аргументом переменной длины. Первый способ предусматривает применение отличающегося типа для параметра переменной длины , как в случае vaTest(int... ) и vaTest(boolean .. . ) . Вспомните, что . . . приводит к тому, что параметр интерпретируется в виде массива заданного типа. Следовательно , точно так же, как обычные методы можно перегружать с использованием отличающихся параметров типа массивов, методы с параметрами переменной длины разрешено перегружать, указывая разные типы для параметров переменной длины. В этом случае компилятор Java вызывает надлежащий перегруженный метод на основе отличия между типами. Второй способ перегрузки метода с аргументом переменной длины предполагает добавление одного или нескольких обычных параметров, что и было сделано в версии vaTest(String, int . . . ) . В данном случае компилятор Java вызывает надлежащий перегруженный метод на основе и количества , и типа аргументов. Аргументы переменной длины и неоднозначность При перегрузке метода , принимающего аргумент переменной длины , могут возникать несколько неожиданные ошибки. Такие ошибки связаны с неоднозначностью , поскольку существует возможность создать двусмысленный вызов перегруженного метода с аргументами переменной длины. Например , взгляни те на следующую программу: // Аргументы // переменной длины , перегрузка и неоднозначность. // Эта программа содержит ошибку и не скомпилируется! class VarArgs4 { // Использование параметра переменной длины типа int. static void vaTest(int ... v) { * Параметр переменной длины типа int < • // ... } // Использование параметра переменной длины типа boolean , static void vaTest( boolean ... v ) { Параметр переменной длины II ... типа boolean } public static void main(String[] args) { // Нормально vaTest(1, 2, 3); vaTest(true, false, false); // Нормально // Ошибка: Неоднозначность! vaTest(); } ^ Неоднозначность! } В этой программе перегрузка метода vaTest ( ) совершенно корректна , но программа не скомпилируется из-за такого вызова: vaTest(); // Ошибка: Неоднозначность! Глава 6. Дополнительные сведения о методах и классах 257 Поскольку параметр переменной длины может быть пустым , вызов будет транслироваться в vaTest(int . . . ) или в vaTest(boolean . . . ) , которые оба одинаково допустимы. Таким образом, вызов в своей основе неоднозначен. Ниже приведен еще один пример неоднозначности. Следующие перегру женные версии метода vaTest ( ) по своей сути неоднозначны , хотя одна из них принимает обычный параметр: static void vaTest(int ... v ) { static void vaTest(int n, int ... v) { Несмотря на различие в списках параметров vaTest ( ) , компилятор не сможет распознать следующий вызов: vaTest(1) Во что преобразуется данный вызов: в vaTest(int ... ) с одним аргументом переменной длины или в vaTest(int, int . .. ) без аргументов переменной длины? Компилятор никак не сможет получить ответ на этот вопрос. Таким образом, ситуация неоднозначна. Из-за ошибок неоднозначности, подобных только что показанным , иногда вам придется отказаться от перегрузки и просто использовать два метода с раз ными именами. Кроме того , в ряде случаев ошибки неоднозначности выявля ют концептуальный дефект в коде , который можно устранить, более тщательно проработав решение. Вопросы и упражнения для самопроверки 1 . При наличии следующего объявления: class X { private int count; корректен ли показанный ниже фрагмент кода ? class Y { public static void main(String[] args) { X ob new X(); ob.count = 10; объявления члена. 2. Модификатор доступа должен находиться 3. Дополнением очереди является стек с доступом к элементам по принципу последним обслужен ” , который похож на стопку та “ первым пришел релок на столе нижняя тарелка используется последней. Создайте класс стека по имени Stack, который способен хранить символы. Назовите методы для доступа к стеку push ( ) и pop ( ) . Предоставьте пользователю возможность указания размера стека при его создании. Все остальные члены класса Stack сделайте закрытыми. ( Подсказка: в качестве образца можете использовать класс Queue; просто измените способ доступа к данным.) — — 258 Java : руководство для начинающих, 9-е издание 4. Для показанного ниже класса напишите метод по имени swap ( ) , который меняет местами содержимое двух объектов типа Test: class Test { int a; Test(int i) { a = i; } } . 5 Корректен ли следующий фрагмент кода? class X { int meth(int a , int b) { ... } String meth(int a, int b) { ... } 6. Напишите рекурсивный метод, отображающий содержимое строки в об ратном направлении . 7. Если все объекты класса должны использовать одну и ту же переменную , то каким образом ее необходимо объявить? 8. Для чего может потребоваться статический блок? 9. Что собой представляет внутренний класс? 10. Какой модификатор доступа необходимо применить, чтобы сделать член доступным только для других членов его класса? метода. 11. Имя метода и список его параметров образуют методу передается с вызова по использованием Аргумент . типа int 12 13. Создайте метод по имени sum ( ) с аргументом переменной длины, который суммирует переданные ему значения типа int и возвращает результат суммирования. Обеспечьте демонстрацию его применения. 14. Можно ли перегружать метод с аргументом переменной длины? 15. Предложите пример метода с аргументом переменной длины, приводящего к неоднозначности. * . ' л 'Л * s vMW. • . I VWv .• •/‘Л s•..*• | V I, i II II, I, I I 1 . II 4 ' I ч % .. . •Н»oo s" IN , < * >4 4 > " *<• 1 i " Глава 7 Наследование 260 Java: руководство для начинающих, 9-е издание В этой главе • • • z Основы наследования z Вызов конструкторов суперкласса z Использование ключевого слова super для доступа к членам суперкласса z Создание многоуровневой иерархии классов z Момент вызова конструкторов z Ссылки из суперкласса на объекты подкласса z Переопределение методов z Применение переопределенных методов для обеспечения динамической диспетчеризации методов z Использование абстрактных классов z Применение ключевого слова final z Класс Object н аследование является одним из трех фундаментальных принципов объектно - ориентированного программирования ( ООП ) , поскольку позволяет создавать иерархические классификации. С использованием наследования вы можете создать универсальный класс, который определяет характерные черты , общие для набора связанных элементов. Затем этот класс может быть унаследован другими , более специфическими классами, каждый из которых добавляет те элементы, которые уникальны для него. В терминологии Java унаследованный класс называется суперклассом , а класс, выполняющий наследование подклассом . Следовательно , подкласс представляет собой специализированную версию суперкласса . Он наследует все члены , определенные суперклассом, и добавляет собственные уникальные эле менты. — Основы наследования Наследование в Java поддерживается за счет того , что одному классу разрешается включать в свое объявление другой класс с помощью ключевого слова extends. Тем самым подкласс дополняет ( расширяет) суперкласс. Для начала рассмотрим небольшой пример, иллюстрирующий несколько ключевых характеристик наследования. В следующей программе создается суперкласс по имени TwoDShape, в котором хранятся ширина и высота двумерного объекта , и подкласс по имени Triangle. Обратите внимание на применение ключевого слова extends для создания подкласса. Глава 7. Наследование 261 // Простая иерархия классов. // Класс для представления двумерных объектов , class TwoDShape { double width; double height; void showDim() { System.out.println("Ширина и высота: " + width + " и " + height); } } // Подкласс TwoDShape для представления треугольников , class Triangle extends TwoDShape { String style; t Класс Triangle унаследован от TwoDShape double area() { return width * height / 2; Из класса Triangle можно ссылаться на члены класса TwoDShape, как если бы они были } объявлены в Triangle void showStyleO { System.out.println("Стиль треугольника: " + style); } } class Shapes { public static void main(String[] Triangle tl = new Triangle(); Triangle t2 = new Triangle(); tl.width = 4.0; tl.height = 4.0; tl.style = "закрашенный"; t2.width = 8.0; t2.height = 12.0; t2.style = "контурный"; System.out.println("Информация tl.showStyle(); tl.showDim(); System ,out.println("Площадь: " System.out.println(); System.out.println("Информация t2.showStyle(); t2.showDim(); System.out.println("Площадь: " } } args) { Объектам типа Triangle доступны все члены класса Triangle, даже унаследованные от TwoDShape об объекте tl: "); + tl.areaO ); об объекте t2: "); + t2.area()); Вот вывод, генерируемый программой: Информация об объекте tl: Стиль треугольника: закрашенный Ширина и высота: 4.0 и 4.0 Площадь: 8.0 Информация об объекте t2: Стиль треугольника: контурный Ширина и высота: 8.0 и 12.0 Площадь: 48.0 262 Java : руководство для начинающих, 9-е издание В классе TwoDShape определены характеристики “ универсальной ” двумерной формы , такой как квадрат, прямоугольник , треугольник и т.д. Класс Triangle является специальным видом TwoDShape, в данном случае треугольником . Класс Triangle включает в себя все компоненты TwoDShape и добавля ет поле style, метод area ( ) и метод showStyle ( ) . В поле style хранится стиль треугольника , которым может быть любая строка , описывающая треугольник, например , “ закрашенный ” , “ контурный ” , “ прозрачный ” или даже что-то вроде “ предупредительный символ ” , “ равнобедренный ” либо “ со скругленны- ми углами ” . Метод area ( ) вычисляет и возвращает площадь треугольника , а showStyle ( ) отображает стиль треугольника. Поскольку класс Triangle включает все члены своего суперкласса TwoDShape, он может получать доступ к полям width и height в методе area(). Кроме того, внутри main ( ) объекты tl и t2 могут напрямую ссылаться на поля width и height, как если бы они были объявлены в Triangle. На рис. 7.1 концептуально показано, как TwoDShape встраивается в Triangle. width TwoDShape height showDimO > Triangle style area() showStyle() Рис . 7.1. Концептуальное представление класса Triangle Несмотря на то что TwoDShape выступает в качестве суперкласса для Triangle, он также является полностью независимым и автономным классом. Быть суперклассом для подкласса не означает, что суперкласс не может использоваться сам по себе. Например , приведенный ниже код совершенно допустим: TwoDShape shape = new TwoDShape(); shape.width = 10; shape.height = 20; shape.showDim(); Разумеется , объекту TwoDShape ничего не известно о подклассах TwoDShape и он не имеет к ним доступа . Общая форма объявления класса , унаследованного от суперкласса , выглядит следующим образом: class имя-подкласса extends имя-суперкласса { // тело класса } Глава 7. Наследование 263 Для любого создаваемого подкласса разрешено указывать только один суперкласс. Наследование нескольких суперклассов при создании одиночного подкласса в языке Java не поддерживается. Как было указано , можно создать иерархию наследования , в которой подкласс становится суперклассом для другого подкласса . Однако ни один класс не может быть суперклассом для самого себя. Основное преимущество наследования связано с тем , что после создания суперкласса , который определяет характеристики , общие для набора объектов, его можно применять для создания любого количества более конкретных под классов. Каждый подкласс может точно настраивать собственное предназначение. Скажем , ниже показан еще один подкласс TwoDShape, инкапсулирующий прямоугольники: // Подкласс TwoDShape для представления прямоугольников. class Rectangle extends TwoDShape { boolean isSquare() { if(width == height) return true; return false; } double area() { return width * height; } } Класс Rectangle включает TwoDShape, а также добавляет метод isSquare(), который определяет, является ли прямоугольник квадратом , и метод area(), вычисляющий площадь прямоугольника. Доступ к членам и наследование В главе 6 вы узнали о том , что зачастую переменная экземпляра класса объявляется закрытой , чтобы предотвратить ее неавторизованное использование либо изменение. Наследование класса не отменяет ограничение закрытого до ступа. Таким образом , даже если подкласс включает в себя все члены своего суперкласса , он не может получить доступ к тем членам суперкласса , которые были объявлены закрытыми. Например, если сделать члены width и height в TwoDShape закрытыми , тогда класс Triangle не сможет получить к ним доступ: // Закрытые члены не наследуются. // Этот пример кода не скомпилируется. // Класс для представления двумерных объектов , class TwoDShape { private double width; // Теперь это // закрытые члены . double height; void showDim() { System.out.println("Ширина и высота: " + width + " и " + height); } } 264 Java: руководство для начинающих, 9-е издание // Подкласс TwoDShape для представления треугольников , class Triangle extends TwoDShape { String style; Доступ к закрытому члену суперкласса невозможен double area( ) { return width * height / 2; // Ошибка! Доступ невозможен. } void showStyleO { System.out.println("Стиль треугольника: " + style); } } Код класса Triangle не скомпилируется , потому что ссылка на width и height внутри метода area ( ) приводит к нарушению прав доступа . Поскольку width и height объявлены закрытыми , они доступны только другим членам собственного класса , а подклассы доступа к ним не имеют. Не забывайте, что член класса , объявленный как private, останется закры тым для своего класса. Он не будет доступным для любого кода за пределами своего класса , включая подклассы. Поначалу может показаться , что тот факт, что подклассы не имеют доступа к закрытым членам суперклассов, является серьезным ограничением , препятствующим применению закрытых членов во многих ситуациях. Тем не менее , это не так. Как объяснялось в главе 6, для предоставления доступа к закрытым членам класса программисты на Java обычно используют методы доступа. Ниже показан переделанный код классов TwoDShape и Triangle, где для доступа к закрытым переменным экземпляра width и height применяются методы: // Использование методов доступа для установки и получения закрытых членов // Класс для представления двумерных объектов. class TwoDShape { private double width; // Теперь это double height; // закрытые члены . // Методы доступа для width и height , double getWidth() { return width; } double getHeightO { return height; } void setWidth(double w) { width = w; } void setHeight(double h) { height = h; } — void showDim() { System.out.println("Ширина и высота: " + width + " и " + height); Методы доступа для членов width и height } } // Подкласс TwoDShape для представления треугольников , class Triangle extends TwoDShape { String style; Использование методов доступа, double area() { return getWidth() * getHeightO / 2 } предоставленных суперклассом Глава 7. Наследование 265 void showStyleO { System.out.println("Стиль треугольника: " + style); } } class Shapes2 { public static void main(String [] args) { Triangle tl = new Triangle(); Triangle t2 = new Triangle(); tl.setWidth(4.0); tl.setHeight(4.0); tl.style = "закрашенный"; t2.setWidth(8.0); t2.setHeight(12.0); t2.style = "контурный"; System.out.println("Информация об объекте tl: "); tl.showStyle(); tl.showDim(); System ,out.println("Площадь: " + tl.areaO ); System.out.println(); System.out.println("Информация об объекте t2: "); t2.showStyle(); t2.showDim(); System.out.println("Площадь: " + t2.area()); } } !?@>A8< C M:A?5@B0 ВОПРОС. Когда переменная экземпляра должна делаться закрытой ? ОТВЕТ. Жестких правил нет, но есть два общих принципа. Если переменная экземпляра должна использоваться только методами, определенными в ее классе, тогда такую переменную следует сделать закрытой. Если значение переменной экземпляра должно находиться в определенных границах, то она должна быть закрытой и доступной только через методы доступа . Подобным образом можно предотвратить присваивание ей недопустимых значений. Конструкторы и наследование Внутри иерархии собственные конструкторы могут быть как у суперклассов , так и у подклассов. В итоге возникает важный вопрос: какой конструктор отвечает за создание объекта подкласса тот, что определен в суперклассе, в подклассе или в обоих местах? Ответ таков: конструктор суперкласса конструирует часть объекта , относящуюся к суперклассу, а конструктор подкласса часть объекта , относящуюся к подклассу. В этом есть смысл , поскольку суперкласс не знает и не имеет доступа к любому элементу в подклассе. Таким образом , их — — 266 Java: руководство для начинающих, 9-е издание конструирование должно выполняться раздельно . В предыдущих примерах ис пользовались стандартные конструкторы , создаваемые компилятором Java автоматически , так что проблема не возникала. Однако на практике большинство классов будут иметь явные конструкторы . Здесь вы увидите , как справиться с ситуацией подобного рода. Когда конструктор определяется только в подклассе , процесс прямолинеен : понадобится просто сконструировать объект подкласса. Часть объекта , отно сящаяся к суперклассу, создается автоматически с применением стандартного конструктора. Например , ниже показана переделанная версия класса Triangle, в которой определен конструктор , что также приводит к определению члена style как закрытого , т.е . теперь оно устанавливается конструктором . // Добавление конструктора к Triangle. // Класс для представления двумерных объектов. class TwoDShape { // Теперь это private double width; // закрытые члены . double height; // Методы доступа для width и height , double getWidth() { return width; } double getHeightO { return height; } void setWidth(double w) { width = w; } void setHeight(double h) { height = h; } void showDimO { System.out.println("Ширина и высота: " + width + " и " + height); } } // Подкласс TwoDShape для представления треугольников , class Triangle extends TwoDShape { private String style; // Конструктор. Triangle(String s, double w, double h) { setWidth(w); setHeight(h); style = s; Инициализация части объекта, относящейся к TwoDShape } double area() { return getWidthO * getHeightO / 2; } void showStyle() { System.out.println("Стиль треугольника: " + style); } } class Shapes3 { public static void main(String[] args) { Triangle tl = new Triangle("закрашенный", 4.0, 4.0); Глава 7. Наследование 267 Triangle t2 = new Triangle("контурный", 8.0, 12.0); System.out.println("Информация об объекте tl: "); tl.showStyle(); tl.showDim(); System ,out.println ("Площадь: " + tl.areaO ); System.out.println(); System.out.println("Информация об объекте t2: "); t2.showStyle(); t2.showDim(); System.out.println("Площадь: " + t2.area()); } } Конструктор T r i a n g l e инициализирует члены , унаследованные от TwoDClass, наряду с собственным полем style. Если конструкторы определены и в суперклассе , и в подклассе, тогда процесс немного усложняется, поскольку должны выполняться конструкторы и суперкласса, и подкласса . В таком случае придется использовать другое ключевое слово Java, super, которое имеет две основные формы. Первая форма вызывает конструктор суперкласса , а вторая применяется для доступа к члену суперклас са, который был скрыт членом подкласса. Ниже рассматривается использование первой формы super. Использование ключевого слова super для вызова конструкторов суперкласса Подкласс может вызывать конструктор , определенный в его суперклассе , с применением следующей формы super: super(список-аргументов ) ; - Здесь список аргументов предназначен для указания любых аргументов, необходимых конструктору в суперклассе. Вызов super ( ) всегда должен быть первым оператором , выполняемым внутри конструктора подкласса. Чтобы уви деть, как используется super ( ) , рассмотрим показанную ниже версию класса TwoDShape, где определен конструктор, инициализирующий width и height: // Добавление конструкторов к TwoDShape. class TwoDShape { private double width; private double height; // Параметризованный конструктор. TwoDShape(double w, double h) { • width = w; height } = h; Конструктор класса TwoDShape 268 Java : руководство для начинающих, 9-е издание // Методы доступа для width и height , double getWidth() { return width; } double getHeightO { return height; } void setWidth(double w) { width = w; } void setHeight(double h) { height = h; } void showDimO { System ,out.println ("Ширина и высота : " + width + " и " + height); } } // Подкласс TwoDShape для представления треугольников , class Triangle extends TwoDShape { private String style; Triangle(String s, double w, double h) { super(w, h); // вызов конструктора суперкласса 4 style = s; Использование super ( ) для выполнения } конструктора TwoDShape double area( ) { return getWidthO * getHeightO / 2; } void showStyle() { System.out.println("Стиль треугольника: " + style); } } class Shapes4 { public static void main(String[] args) { Triangle tl = new Triangle("закрашенный", 4.0, 4.0); Triangle t2 = new Triangle("контурный", 8.0, 12.0); System.out.println("Информация об объекте tl: "); tl.showStyle(); tl.showDim(); System ,out.println("Площадь: " + tl.areaO ); System.out.println(); } System.out.println("Информация об объекте t2: "); t2.showStyle(); t2.showDim(); System.out.println("Площадь: " + t2.area()); } В конструкторе Triangle ( ) вызывается super ( ) с параметрами w и h, что приводит к вызову конструктора TwoDShape ( ) , который инициализирует width и height, используя эти значения . Класс Triangle больше не инициализиру ет указанные значения самостоятельно . Ему необходимо инициализировать только значение , уникальное для него: style. Тем самым классу TwoDShape предоставляется возможность конструировать свои подобъекты любым нужным способом. Кроме того , в класс TwoDShape можно добавлять функциональность, о которой не известно имеющимся подклассам , предотвращая нарушение работы существующего кода. Глава 7. Наследование 269 Вызывать с помощью super ( ) можно конструктор любой формы , опреде ленный в суперклассе . Выполнится конструктор , соответствующий указанным аргументам . Скажем , далее приведены расширенные версии классов TwoDShape и Triangle, которые включают стандартные конструкторы и конструкторы , принимающие один аргумент: // Добавление дополнительных конструкторов к TwoDShape. class TwoDShape { private double width; private double height; // Стандартный конструктор. TwoDShape() { width = height = 0.0; } // Параметризованный конструктор. TwoDShape(double w, double h) { width = w; height = h; } // Конструктор объекта с одинаковыми шириной и высотой. TwoDShape(double х) { width = height = х; } // Методы доступа для width и height , double getWidthO { return width; } double getHeightO { return height; } void setWidth(double w) { width = w; } void setHeight(double h) { height = h; } void showDim() { System.out.println("Ширина и высота : " + width + " и " + height); } } // Подкласс TwoDShape для представления треугольников , class Triangle extends TwoDShape { private String style; // Стандартный конструктор. Triangle() { < super(); style = "отсутствует"; } // Конструктор. Triangle(String s, double w, double h ) { super(w, h); // вызов конструктора суперкласса style = s; Использование super ( ) для вызова } различных форм конструктора TwoDShape // Конструктор с одним аргументом. Triangle(double х) { super(х); // вызов конструктора суперкласса style = "закрашенный"; } 270 Java : руководство для начинающих, 9-е издание double area() { return getWidth() * getHeightO / 2; } void showStyleO { System.out.println("Стиль треугольника: " + style); } } class Shapes5 { public static void main(String[] args) { Triangle tl = new Triangle(); Triangle t2 = new Triangle("контурный ”, 8.0, 12.0); Triangle t3 = new Triangle(4.0); tl = t2; System.out.println("Информация об объекте tl: "); tl.showStyle(); tl.showDim(); System ,out.println("Площадь: " + tl.areaO ); System.out.println(); System.out.println("Информация об объекте t2: "); t2.showStyle(); t2.showDim(); System.out.println("Площадь: " + t2.area()); System.out.println(); System.out.println("Информация об объекте t3: "); t3.showStyle(); t3.showDim(); System.out.println ("Площадь: " + t3.area()); System.out.println(); } } Вот вывод, генерируемый данной программой: Информация об объекте tl: Стиль треугольника: контурный Ширина и высота: 8.0 и 12.0 Площадь: 48.0 Информация об объекте t2: Стиль треугольника: контурный Ширина и высота: 8.0 и 12.0 Площадь: 48.0 Информация об объекте t3: Стиль треугольника: закрашенный Ширина и высота: 4.0 и 4.0 Площадь: 8.0 Давайте вспомним ключевые концепции, лежащие в основе super ( ) . Когда в подклассе вызывается super ( ) , происходит вызов конструктора его прямого Глава 7. Наследование 271 суперкласса . Таким образом , super ( ) всегда ссылается на суперкласс, находящийся в иерархии непосредственно над вызывающим классом. Это справедливо даже для многоуровневой иерархии. Кроме того , вызов super ( ) всегда дол жен быть первым оператором, выполняемым внутри конструктора подкласса . Использование ключевого слова super для доступа к членам суперкласса Существует вторая форма super , которая действует в чем - то подобно this , но всегда ссылается на суперкласс подкласса , в котором используется . Вот ее общая форма: super.член Такая форма super больше всего подходит в ситуациях, когда имена членов подкласса скрывают члены с теми же именами в суперклассе. Возьмем следующую простую иерархию классов: // Использование super для преодоления сокрытия имен. class А { int i; } // Создать подкласс, расширив класс А. class В extends А { int i; // этот член i скрывает член i в А В(int a, int b) { // i в А super.i = а; // i в В i = b; Здесь super ,i ссылается на iвА } void show() { System.out.println("i в суперклассе: " + super.i); System.out.println("i в подклассе: " + i); } } class UseSuper { public static void main(String[] args) { В subOb = new B(l, 2); subOb.show(); } } Программа генерирует такой вывод: i в суперклассе: 1 i в подклассе: 2 Несмотря на то что переменная экземпляра i в классе в скрывает i в классе ключевое слово super разрешает доступ к члену i, определенному в суперклассе. Ключевое слово super также можно использовать для вызова методов , А, сокрытых подклассом. 272 Java: руководство для начинающих, 9- е издание Упражнение 7.1 Расширение класса Vehicle Для иллюстрации всей мощи наследования будет расши TruckDemo.java рен класс Vehicle, разработанный в главе 4. Вспомните , что класс Vehicle инкапсулирует информацию о транспортных средствах , включая количество пассажиров, которые они могут перевозить, запас топлива и средний расход топлива. Класс Vehicle может применяться в качестве отправной точки для разработки более специализированных классов. Например, одним из видов транспортных средств является грузовик. Важной характеристикой грузовика является его грузоподъемность. Таким образом , чтобы создать класс Truck, можно расширить класс Vehicle, добавив переменную экземпляра, в которой хранится грузоподъемность. Ниже показана версия класса Truck, где это сделано. Переменные экземпляра в Vehicle в нем объявляются закрытыми, а для получения и установки их значений предоставляются методы доступа. . 1 Создайте файл по имени TruckDemo.java и скопируйте в него последнюю реализацию класса Vehicle из главы 4. 2. Создайте класс Truck: // Расширение класса Vehicle для создания специализированного // класса Truck class Truck extends Vehicle { private int cargocap; // грузоподъемность в фунтах // Конструктор класса Truck. Truck(int p, int f , int m, int c) { /* Инициализировать члены Vehicle с использованием конструктора Vehicle */ super(p, f, m); cargocap = c; } // Методы доступа для cargocap. int getCargoO { return cargocap; } void putCargo(int c) { cargocap = c; } } Класс Truck унаследован от Vehicle, а также добавляет члены cargocap, getCargo ( ) и putCargo ( ) . Класс Truck включает все характеристики, определенные в Vehicle. Должны быть добавлены только те элементы , которые уникальны для Truck. 3. Сделайте переменные экземпляра Vehicle закрытыми: private int passengers; private int fuelcap; private int mpg; // количество пассажиров // запас топлива в галлонах // расход топлива в милях на галлон 4. Вот полный код программы , демонстрирующей использование класса Truck: Глава 7. Наследование 273 // Упражнение 7.1. // // Построение подкласса Vehicle для представления грузовиков. class Vehicle private int private int private int { passengers; fuelcap; mpg; // количество пассажиров 11 запас топлива в галлонах // расход топлива в милях на галлон // Конструктор класса Vehicle. Vehicle(int p, int f, int m) { passengers = p; fuelcap = f; mpg = m; } // Возвращает дальность поездки , int range( ) { return mpg * fuelcap; } // Рассчитывает объем топлива, необходимого // для поездки на заданное расстояние , double fuelNeeded(int miles) { return (double) miles / mpg; } // Методы доступа к переменным экземпляра , int getPassengers() { return passengers; } void setPassengers(int p) { passengers = p; } int getFuelcapO { return fuelcap; } void setFuelcap(int f) { fuelcap = f; } int getMpgO { return mpg; } void setMpg(int m) { mpg = m; } } //Расширение класса Vehicle для создания специализированного класса Truck class Truck extends Vehicle { private int cargocap; // грузоподъемность в фунтах // Конструктор класса Truck. Truck(int p, int f, int m, int c) { / ^Инициализировать члены Vehicle с использованием конструктора Vehicle*/ super(p, f, m); cargocap = c; } доступа для cargocap. int getCargoO { return cargocap; } void putCargo(int c) { cargocap = c; } // Методы } class TruckDemo { public static void main(String[] args) { // Сконструировать несколько объектов, представляющих грузовики. Truck semi = new Truck(2, 200, 7, 44000); Truck pickup = new Truck(3, 28, 15, 2000); 274 Java: руководство для начинающих, 9-е издание double gallons; int dist = 252; gallons = semi.fuelNeeded(dist); System.out.println("Полуприцеп может перевезти " + semi.getCargo() + " фунтов."); System.out.println("Для поездки на расстояние " + dist + " миль полуприцепу требуется " + gallons + " галлонов топлива.\п"); gallons = pickup.fuelNeeded(dist); System.out.println("Пикап может перевезти " + pickup.getCargo() + " фунтов."); System.out.println("Для поездки на расстояние " + dist + " миль пикапу требуется " + gallons + " галлонов топлива."); } } . 5 Ниже приведен вывод, генерируемый программой: Полуприцеп может перевезти 44000 фунтов. Для поездки на расстояние 252 миль полуприцепу требуется 36.0 галлонов топлива. Пикап может перевезти 2000 фунтов. Для поездки на расстояние 252 миль пикапу требуется 16.8 галлонов топлива. 6. Многие другие типы классов могут быть производными от Vehicle. На пример , следующий шаблон класса представляет внедорожники ; в нем хранится клиренс транспортного средства. // Создание класса для представления внедорожников , class OffRoad extends Vehicle { private int groundClearance; // клиренс в дюймах П ... } Ключевой момент состоит в том , что после создания суперкласса , определяющего общие аспекты объекта, такой суперкласс может быть унаследован для формирования специализированных классов. Каждый подкласс просто добавляет собственные уникальные характеристики. В этом и есть суть наследования . Создание многоуровневой иерархии До сих пор мы имели дело с простыми иерархиями классов, которые состояли только из суперкласса и подкласса. Однако можно создавать иерархии , содержащие любое количество уровней наследования. Как уже упоминалось, подкласс вполне допустимо применять в качестве суперкласса другого. Например , при наличии трех классов А , в и С класс С может быть подклассом В, который Глава 7. Наследование 275 является подклассом А. Когда возникает ситуация такого рода , каждый подкласс наследует все характерные черты , обнаруженные во всех его суперклассах . В данном случае С наследует все аспекты в и А. Чтобы увидеть , чем может быть полезна многоуровневая иерархия , рас смотрим следующую программу, где подкласс Triangle используется как су перкласс для создания подкласса ColorTriangle, представляющего цвет ные треугольники . Класс ColorTriangle наследует все признаки Triangle и TwoDShape, а также добавляет поле по имени color, в котором содержится цвет треугольника . // Многоуровневая иерархия. class TwoDShape { private double width; private double height; // Стандартный конструктор. TwoDShape( ) { width = height = 0.0; } // Параметризованный конструктор. TwoDShape(double w, double h) { width = w; height = h; } // Конструктор объекта с одинаковыми шириной и высотой. TwoDShape(double х ) { width = height = х; } // Методы доступа для width и height , double getWidthO { return width; } double getHeightO { return height; } void setWidth(double w) { width = w; } void setHeight(double h) { height = h; } void showDim() { System.out.println ("Ширина и высота: " + width + " и " + height); } } // Класс, расширяющий TwoDShape. class Triangle extends TwoDShape { private String style; // Стандартный конструктор. Triangle() { super(); style = "отсутствует"; } Triangle(String s, double w, double h) { super(w, h); // вызов конструктора суперкласса style = s; } 276 Java: руководство для начинающих, 9-е издание // Конструктор с одним аргументом. Triangle(double х) { super(х); // вызов конструктора суперкласса style = "закрашенный"; } double area() { return getWidth() * getHeightO / 2; } void showStyleO { System.out.println("Стиль треугольника: " + style); } } // Класс, расширяющий Triangle , Класс ColorTriangle унаследован от класса class ColorTriangle extends Triangle { Triangle , который является производным от TwoDShape, поэтому ColorTriangle содержит private String color; 4 все члены классов Triangle и TwoDShape. ColorTriangle(String с, String s, double w, double h) { super(s, w, h); color = c; } String getColorO { return color; } void showColor() { System.out.println("Цвет: " + color); } } class Shapes6 { public static void main(String[] args) { ColorTriangle tl = new ColorTriangle("синий", "контурный", 8.0, 12.0); ColorTriangle t2 = new ColorTriangle("красный", "закрашенный", 2.0, 2.0); System.out.println("Информация об объекте tl: "); tl.showStyle(); tl.showDim(); tl.showColor(); System ,out.println ("Площадь: " + tl.areaO ); System.out.println(); System.out.println("Информация об объекте t2: "); t2.showStyle(); Объект ColorTriangle может вызывать t2.showDim(); методы, определенные в нем самом и в его суперклассах t2.showColor(); System.out.println("Площадь: " + t2.area()); } } Вот вывод , генерируемый программой: Информация об объекте tl: Стиль треугольника: контурный Ширина и высота: 8.0 и 12.0 Цвет: синий Площадь: 48.0 Глава 7. Наследование 277 Информация об объекте t2: Стиль треугольника: закрашенный Ширина и высота: 2.0 и 2.0 Цвет: красный Площадь: 2.0 Благодаря наследованию класс ColorTriangle может задействовать определенные ранее классы Triangle и TwoDShape, добавляя только ту дополни тельную информацию , которая необходима для собственного конкретного приложения. Это часть ценности наследования ; оно позволяет многократно использовать код. В примере иллюстрируется еще один важный момент: super ( ) всегда ссы лается на конструктор из ближайшего суперкласса. В классе ColorTriangle с помощью super ( ) вызывается конструктор Triangle, а в классе Triangle конструктор TwoDShape. В рамках иерархии классов, когда конструктору суперкласса требуются аргументы, то все подклассы должны передавать их “ вверх по цепочке наследования ” . Сказанное верно независимо от того, нужны подклассу собственные аргументы или нет. — Когда конструкторы выполняются? При чтении предыдущего обсуждения наследования и иерархии классов, у вас мог возникнуть важный вопрос: когда иерархия классов создана , в каком порядке выполняются конструкторы классов, образующих иерархию? Напри мер , при наличии подкласса В и суперкласса А конструктор А выполняется раньше конструктора В или наоборот? Ответ заключается в том , что в иерархии классов конструкторы завершают свое выполнение в порядке наследова ния от суперкласса к подклассу. Кроме того , поскольку вызов super ( ) должен быть первым оператором, выполняемым в конструкторе подкласса, такой порядок остается тем же независимо от того , применяется super ( ) или нет. Если super ( ) не используется, то будет выполнен стандартный конструктор (без параметров) каждого суперкласса. Выполнение конструкторов проиллюстрирова но в следующей программе: // Демонстрация выполнения конструкторов. // Создать суперкласс , class А { А() { System.out.println("Конструктор А ."); } } // Создать подкласс путем расширения класса А. class В extends А { ВО { System.out.println("Конструктор В."); } } 278 Java: руководство для начинающих, 9-е издание // Создать еще один подкласс путем расширения класса В. class С extends В { СО { System.out.println ("Конструктор С."); } } class OrderOfConstruction { public static void main(String[] args) { С c = new C(); } } Ниже показан вывод, генерируемый программой: Конструктор А. Конструктор В. Конструктор С. Как видите, конструкторы выполняются в порядке наследования. Если хорошо подумать, то имеет смысл , что конструкторы завершают свое выполнение в порядке наследования. Поскольку суперклассу ничего не известно о каких-либо подклассах , любая инициализация , которую должен выполнить суперкласс, является отдельной и возможно обязательной для любой инициализации , выполняемой подклассом. Следовательно , она должна быть завершена первой. Ссылки на суперклассы и объекты подклассов — Вам уже известно, что Java строго типизированный язык. Помимо стандартных преобразований и автоматических повышений, которые применяются к его примитивным типам, совместимость типов строго соблюдается. Таким образом, ссылочная переменная для одного типа класса обычно не может ссылаться на объект другого типа класса. Например, взгляните на следующую программу: // Этот код не скомпилируется. class X { int а; X(int i) { а = i; } } class Y { int a; Y(int i) { a = i; } } class IncompatibleRef { public static void main(String[] args) { X x = new X(10); X x2; Y у = new Y(5); x2 = x; // Нормально, типы совпадают. x2 = у; // Ошибка, типы отличаются. } } Глава 7. Наследование 279 Хотя классы X и Y структурно одинаковы, присвоить ссылке X объект Y невозможно , поскольку они имеют разные типы. В общем случае переменная ссылки на объект может ссылаться только на объекты своего типа. Тем не менее , из строгого соблюдения типов в Java существует важное ис ключение. Ссылочной переменной суперкласса может быть присвоена ссылка на объект любого подкласса , производного от этого суперкласса. Другими словами, ссылка на суперкласс может ссылаться на объект подкласса. Вот пример: // Ссылка на суперкласс может ссылаться на объект подкласса. class X { int а; X(int i) { а = i; } } class Y extends X { int b; Y(int i, int j) { super(j); b = i; } } class SupSubRef { public static void main(String[] args) { X x = new X(10); X x2; Y у = new Y(5, 6); x2 = x; // Нормально, типы одинаковы . System.out.println ("x2.a: " + x2.a); * Нормально, потому что Y является подклассом X; следовательно, х2 может ссылаться на у х2 = у; // Тоже нормально, поскольку класс Y // является производным от X. System.out.println ("х2.а: " + х2.а); // Ссылкам X известны только члены X. // Нормально. х2.а = 19; // Ошибка, член b в X отсутствует. // х2.Ь = 27; } } Теперь класс Y является производным от класса X , поэтому х 2 разрешено присваивать ссылку на объект Y. Важно понимать, что именно тип ссылочной переменной , а не тип объекта , на который она ссылается , определяет, к каким членам можно получать доступ . Другими словами, когда ссылочной переменной типа суперкласса присваивается ссылка на объект подкласса , то доступ имеется только к тем частям объекта , которые определены в суперклассе . Вот почему переменная х 2 не может получить доступ к Ь, даже если она ссылается на объект Y. Если подумать, то смысл станет очевидным, т.к. суперклассу ничего не известно о том , что именно к нему добавляет подкласс. Поэтому последняя строка кода в предыдущем фрагменте закомментирована. 280 Java: руководство для начинающих, 9-е издание Хотя описанный выше прием может показаться несколько экзотическим , с ним связан ряд важных практических применений , один из которых обсуждается здесь, а другой далее в главе при раскрытии переопределения методов. Важным местом , где ссылки на подклассы присваиваются переменным су перкласса , будет вызов конструкторов в иерархии классов . Как вы знаете , в классе обычно определен конструктор , который принимает объект класса в ка честве параметра , позволяя классу создавать копию объекта. Подклассы класса подобного рода могут воспользоваться такой возможностью . Скажем , рассмотрим приведенные далее версии классов TwoDShape и Triangle, в которых до бавлены конструкторы , принимающие объект в качестве параметра. class TwoDShape { private double width; private double height; // Стандартный конструктор. TwoDShape() { width = height = 0.0; } // Параметризованный конструктор. TwoDShape(double w, double h ) { width = w; height = h; } // Конструктор объекта с одинаковыми шириной и высотой. TwoDShape(double х) { width = height = х; } // Конструктор объекта из объекта. TwoDShape(TwoDShape ob) { Конструирует объект из объекта width = ob.width; height = ob.height; } // Методы доступа для width и height , double getWidth() { return width; } double getHeightO { return height; } void setWidth(double w) { width = w; } void setHeight(double h) { height = h; } void showDimO { System.out.println("Ширина и высота: " + width + " и " + height); } } // Подкласс TwoDShape для представления треугольников , class Triangle extends TwoDShape { private String style; // Стандартный конструктор. Triangle() { super(); style = "отсутствует"; } Глава 7. Наследование 281 // Конструктор класса Triangle. Triangle(String s, double w, double h) { super(w, h); // вызов конструктора суперкласса style = s; } // Конструктор с одним аргументом. Triangle(double х) { // вызов конструктора суперкласса super(х); style = "закрашенный"; } // Конструктор объекта из объекта. Triangle(Triangle ob) { // передача объекта конструктору класса TwoDShape super(ob); style = ob.style; } Передача ссылки Triangle t double area() { return getWidth() * getHeightO / 2; конструктору TwoDShape } void showStyle() { System.out.println("Стиль треугольника: " + style); } } class Shapes7 { public static void main(String[] args) { Triangle tl = new Triangle("контурный", 8.0, 12.0); // Создать копию объекта tl. Triangle t2 = new Triangle(tl); System.out.println("Информация об объекте tl: "); tl.showStyle(); tl.showDim(); System ,out.println("Площадь: " + tl.areaO ); System.out.println(); System.out.println("Информация об объекте t2: "); t2.showStyle(); t2.showDim(); System.out.println("Площадь: " + t2.area()); } } Объект t 2 в программе конструируется из объекта t i n потому они идентич ны. Вот результат выполнения программы: Информация об объекте tl: Стиль треугольника: контурный Ширина и высота: 8.0 и 12.0 Площадь: 48.0 Информация об объекте t2: Стиль треугольника: контурный Ширина и высота: 8.0 и 12.0 Площадь: 48.0 282 Java: руководство для начинающих, 9-е издание Обратите особое внимание на показанный ниже конструктор класса Triangle: // Конструктор объекта из объекта. Triangle(Triangle ob) { super(ob); // передача объекта конструктору класса TwoDShape style = ob.style; } Он получает объект типа Triangle и передает его (через super)следующему конструктору класса TwoDShape: // Конструктор объекта из объекта. TwoDShape(TwoDShape ob) { width = ob.width; height = ob.height; } Ключевой момент состоит в том , что конструктор TwoDshape ( ) ожидает объект TwoDShape, но Triangle ( ) передает ему объект Triangle. Причина , по которой это работает, связана с тем , что , как объяснялось ранее, ссылка на суперкласс может ссылаться на объект подкласса. Таким образом, вполне допустимо передавать TwoDShape ( ) ссылку на объект класса, производного от TwoDShape. Поскольку конструктор TwoDShape ( ) инициализирует только те части объекта подкласса , которые являются членами TwoDShape, совершенно не играет роли тот факт, что объект может также содержать другие члены, добавленные производными классами. Переопределение методов В иерархии классов , когда метод в подклассе имеет то же имя и сигнатуру типа , что и метод в его суперклассе, то говорят, что метод в подклассе пере определяет метод в суперклассе. При вызове переопределенного метода через его подкласс всегда будет вызываться версия метода , определенная в подклассе. Версия метода , определенная в суперклассе, будет сокрыта. Рассмотрим следу ющий пример: // Переопределение методов. class А { int i, j; A(int a, int b) { l a; j = b; } // Отобразить значения i и j. void show() { System.out.println("i и j: " + i + " " + j); } } Глава 7. Наследование 283 class В extends А { int к; B(int a, int b, int c ) { super(a, b); к = c; } // Отобразить к - переопределяет show() в A. void show() {* Этот метод show ( ) в классе В переопределяет метод show ( ) System.out.println("k: " + k); < из класса А } } class Override { public static void main(String[] args) { В subOb = new B(l, 2 , 3); subOb.show(); // вызывается show() из В } } Вот вывод, генерируемый программой: к: 3 Когда метод show ( ) вызывается на объекте типа В, используется версия show ( ) , определенная в классе в. То есть версия show ( ) внутри В переопределя ет версию show ( ) , объявленную в А. При желании получить доступ к версии переопределенного метода из суперкласса можно применить ключевое слово super. Скажем , в приведенном далее классе В внутри версии show ( ) из подкласса вызывается версия show ( ) из суперкласса , что позволяет отобразить все переменные экземпляра. class В extends А { int к; B(int a, int b, int с) { super(а, Ь); к = с; } Использование super для вызова версии метода show ( ) , определенной в суперклассе А void show() { super.show(); // вызывается show() из A System.out.println("k: " + k); } } Подставив эту версию класса В в код предыдущего примера , вы увидите та кой вывод: i и j : 12 к: 3 . Здесь super show ( ) вызывает версию show ( ) из суперкласса. Метод переопределяется только в случае , если имена и сигнатуры типов двух методов идентичны , а иначе два метода будут просто перегруженными . 284 Java: руководство для начинающих, 9- е издание Например , взгляните на следующую модифицированную версию предыду щего примера: // Методы с отличающимися сигнатурами типов // являются перегруженными - не переопределенными. class А { int i, j; A(int a , int b) { i = a; j = b; } Поскольку сигнатуры отличаются, этот метод show ( ) просто перегружает show ( ) из суперкласса А // Отобразить значения i и j. void show() { System.out.println ("i и j: " + i + " " + j); } } // Создать подкласс путем расширения класса А. class В extends А { int к; B(int a, int b, int с) { super(а, Ь); к = с; } // Перегрузить show(). void show(String msg) { System.out.println(msg + k); } } class Override { public static void main(String[] args) { В subOb = new В(1, 2, 3); subOb.show ("Это к: "); subOb.show (); // вызывается show() из В // вызывается show() из A } } Вот вывод, генерируемый программой: Это к: 3 i и j: 12 Версия метода show ( ) в классе В принимает строковый параметр , что от личает его сигнатуру типов от сигнатуры метода show ( ) в классе А, который не принимает параметров . Поэтому никакого переопределения ( или сокрытия имени) не происходит. 285 Глава 7. Наследование Переопределенные методы поддерживают полиморфизм Хотя примеры в предыдущем разделе демонстрируют механику переопре деления методов, они не отражают его возможности. На самом деле , если бы для переопределения методов не существовало ничего , кроме соглашения о пространстве имен, то оно считалось бы в лучшем случае интересной особен ностью, не обладая реальной ценностью. Тем не менее , это не так. Переопределение методов лежит в основе одной из самых мощных концепций Java диспетчеризации динамических методов . Диспетчеризация динамических методов представляет собой механизм , с помощью которого вызов переопределенного метода распознается во время выполнения , а не на этапе компиляции. Динами ческая диспетчеризация методов важна , потому что именно так в Java обеспечивается полиморфизм во время выполнения . Давайте начнем с повторения важного принципа : ссылочная переменная типа суперкласса может ссылаться на объект подкласса. Данный факт используется в Java для распознавания вызовов переопределенных методов во время вы полнения. А каким образом ? Когда переопределенный метод вызывается через ссылку на суперкласс , версия метода , подлежащая выполнению, выясняется на основе типа объекта , на который производится ссылка в момент вызова . Соответственно, такое выяснение происходит во время выполнения. При ссылке на разные типы объектов будут вызываться разные версии переопределенного метода. Другими словами, именно тип объекта, на который делается ссылка (а не тип ссылочной переменной ) , определяет, какая версия переопределенного метода будет выполняться. Таким образом , если суперкласс содержит метод, который переопределяется в подклассе , то при ссылке на разные типы объектов через ссылочную переменную типа суперкласса выполняются разные версии метода . Ниже показан пример , предназначенный для иллюстрации динамической диспетчеризации методов: — // Демонстрация динамической диспетчеризации методов. class Sup { void who( ) { System.out.println("who() в Sup"); } } class Subl extends Sup { void who() { System.out.println("who() в Subl"); } } 286 Java: руководство для начинающих, 9- е издание class Sub2 extends Sup { void who() { System.out.println("who() в Sub2"); } } class DynDispDemo { public static void main(String[] args) { Sup superOb = new Sup(); Subl subObl = new Subl(); Sub2 sub0b2 = new Sub2(); Sup supRef; supRef = superOb; supRef.who(); M supRef = subObl; supRef.who(); 4 supRef = sub0b2; supRef.who(); 4 } - В этом случае вызываемая версия метода who() определяется во время выполнения на основе типа объекта, на который осуществляется ссылка } Вот вывод, генерируемый программой: who() в Sup who() в Subl who() в Sub2 В программе создается один суперкласс по имени Sup и два его подкласса , Subl и Sub2. В Sup объявляется метод who ( ) , а в подклассах он переопределяется. Внутри метода main ( ) объявляются объекты типов Sup, Subl и Sub2. Кроме того, объявляется ссылка типа Sup по имени supRef. Затем в программе переменной supRef по очереди присваивается ссылка на каждый тип объекта и производится вызов метода who ( ) . В выводе видно , что выполняемая версия who ( ) определяется типом объекта , на который осуществляется ссылка во время вызова , а не типом класса ссылочной переменной supRef. !?@>A8< C M:A?5@B0 ВОПРОС. Переопределенные методы в Java очень похожи на виртуальные функции в C ++. Действительно ли есть сходство? ОТВЕТ. Да. Читатели , знакомые с языком C ++ , признают, что переопределен ные методы в Java эквивалентны по назначению и аналогичны виртуальным функциям в C + + . Глава 7. Наследование 287 Зачем нужны переопределенные методы? Как было указано ранее , переопределенные методы позволяют Java поддерживать полиморфизм во время выполнения. Полиморфизм важен для ООП по одной причине: он позволяет универсальному классу определять методы , которые будут общими для всех производных от него классов, одновременно разрешая подклассам определять индивидуальные реализации некоторых или всех общих методов. Переопределенные методы еще один способ, которым в Java обеспечивается аспект полиморфизма “ один интерфейс , несколько методов ” . Одним из ключей к успешному применению полиморфизма является понимание того , что суперклассы и подклассы образуют иерархию с продвижением от меньшей специализации к большей. При правильном использовании суперкласс предоставляет все элементы , которые подкласс может задействовать напрямую. Он также определяет те методы, которые производный класс должен реализовать самостоятельно. Это позволяет подклассу не только гибко опреде лять собственные методы , но также обеспечивает согласованный интерфейс. Таким образом, комбинируя наследование с переопределенными методами , суперкласс может определять общую форму методов, которые будут потребляться всеми его подклассами. — Применение переопределения методов к классу TwoDShape Чтобы лучше понять возможности переопределения методов, оно будет применено к классу TwoDShape. В предшествующих примерах в каждом классе , производном от TwoDShape, был определен метод по имени area ( ) . Это говорит о том, что метод area ( ) лучше сделать частью класса TwoDShape и позволить каждому подклассу переопределять его , устанавливая способ вычисления площади для вида фигуры, которую инкапсулирует класс. Прием реализован в следующей программе. Также ради удобства в класс TwoDShape добавлено поле имени name, которое упростит написание демонстрационных программ. // Использование динамической диспетчеризации методов. class TwoDShape { private double width; private double height; private String name; // Стандартный конструктор. TwoDShape() { width = height = 0.0; name = "без имени"; } // Параметризованный конструктор. TwoDShape(double w, double h, String n) { width = w; height = h; name = n; } 288 Java: руководство для начинающих, 9-е издание // Конструктор объекта с одинаковыми шириной и высотой. TwoDShape(double х, String n) { width = height = x; name = n; } // Конструктор объекта из объекта. TwoDShape(TwoDShape ob) { width = ob.width; height = ob.height; name = ob.name; } // Методы доступа для width и height , double getWidthO { return width; } double getHeightO { return height; } void setWidth(double w) { width = w; } void setHeight(double h) { height = h; } String getNameO { return name; } void showDim() { System.out.println("Ширина и высота: " + width + " и " + height); } Метод area(), определенный в классе TwoDShape 1 double area() System.out.println("Метод area() должен быть переопределен."); return 0.0; } } // Подкласс TwoDShape для представления треугольников , class Triangle extends TwoDShape { private String style; // Стандартный конструктор. Triangle() { super(); style = "отсутствует"; } // Конструктор класса Triangle. Triangle(String s, double w, double h ) { super(w, h, "треугольник"); style = s; } // Конструктор с одним аргументом. Triangle(double x) { super(x, "треугольник"); // вызов конструктора суперкласса style = "закрашенный"; } // Конструктор объекта из объекта. Triangle(Triangle ob) { super(ob); // передача объекта конструктору класса TwoDShape style = ob.style; } Глава 7. Наследование 289 // Переопределение метода агеа() для Triangle. double area() Н Переопределение метода area ( ) для класса Triangle return getWidthO * getHeightO / 2; } void showStyleO { System.out.println("Стиль треугольника: " + style); } } // Подкласс TwoDShape для представления прямоугольников , class Rectangle extends TwoDShape { // Стандартный конструктор. Rectangle() { super(); } // Конструктор класса Rectangle. Rectangle(double w, double h ) { super(w, h, "прямоугольник"); // вызов конструктора суперкласса } // Конструктор квадрата. Rectangle(double х) { // вызов конструктора суперкласса super(х, "прямоугольник"); } // Конструктор объекта из объекта. Rectangle(Rectangle ob) { super(ob); // передача объекта конструктору класса TwoDShape } boolean isSquareO { if(getwidth() == getHeightO ) return true; return false; } // Переопределение метода area() для Rectangle. double area() { Переопрелеление метода area ( ) для класса Reactangle return getWidthO * getHeightO ; } } class DynShapes { public static void main(String[] args) { TwoDShape[] shapes = new TwoDShape[5]; shapes[0] = new Triangle("контурный", 8.0, 12.0); shapes[1] = new Rectangle(10); shapes[2] = new Rectangle(10, 4); shapes[3] = new Triangle(7.0); shapes[4] = new TwoDShape(10, 20, "обобщенный"); Для каждого объекта, представляющего фигуру, вызывается надлежащая версия area ( ) for(int i=0; i < shapes.length; i++) { System ,out.println("Объект с именем: " + shapes[i].getName()); System.out.println("Площадь: " + shapes[i].area()); System.out.println (); } } } 290 Java: руководство для начинающих, 9-е издание Ниже показан вывод, генерируемый программой: Объект с именем: треугольник Площадь: 48.0 Объект с именем: прямоугольник Площадь: 100.0 Объект с именем: прямоугольник Площадь: 40.0 Объект с именем: треугольник Площадь: 24.5 Объект с именем: обобщенный Метод area() должен быть переопределен. Площадь: 0.0 Давайте внимательно изучим приведенную выше программу. Прежде всего, как уже упоминалось, метод area ( ) теперь является частью класса TwoDShape и переопределяется в классах Triangle и Rectangle. В классе TwoDShape для area ( ) предоставляется реализация - заполнитель, которая просто информиру ет пользователя о том , что этот метод должен быть переопределен в подклассе. Каждое переопределение area ( ) предоставляет реализацию, подходящую для типа объекта , инкапсулированного подклассом. Таким образом , если бы вы реализовали , скажем , класс для представления эллипса, то метод area ( ) должен был бы вычислять площадь эллипса. В предыдущей программе есть еще одна важная особенность. Обрати те внимание , что переменная shapes в main ( ) объявлена как массив объектов TwoDShape. Однако элементам данного массива присваиваются ссылки Triangle, Rectangle и TwoDShape. Поступать так разрешено, потому что , как объяснялось, ссылка на суперкласс может ссылаться на объект подкласса. Затем в программе организуется проход по массиву с помощью цикла , в котором отображается информация о каждом объекте. Несмотря на простоту, прием ил люстрирует всю мощь как наследования, так и переопределения методов. Тип объекта, на который ссылается ссылочная переменная суперкласса , определя ется во время выполнения и надлежащим образом обрабатывается. Если объект является производным от TwoDShape, то его площадь можно получить, вызвав метод area ( ) . Интерфейс операции вычисления площади одинаков независимо от того, какой тип фигуры используется. Использование абстрактных классов Бывают ситуации , когда желательно создать суперкласс , определяющий только обобщенную форму, которая будет применяться всеми его подклассами, оставляя каждому подклассу возможность заполнить недостающие детали. Такой класс определяет природу методов, подлежащих реализации в подклассах, не предоставляя собственной реализации одного или большего количества этих методов. Ситуация подобного рода может возникнуть, когда суперкласс Глава 7. Наследование 291 не способен создать осмысленную реализацию метода , что относится к клас су TwoDShape, использованному в предыдущем примере . Определение метода area ( ) является просто заполнителем. Он не будет вычислять и отображать площадь объекта любого вида. При создании собственных библиотеки классов вы заметите , что нередко метод не имеет осмысленного определения в контексте своего суперкласса . Справиться с такой ситуацией можно двумя способами. Один из способов, как было показано в предыдущем примере , предусматривает просто выдачу предупреждающего сообщения. Хотя этот подход может быть полезен в определен ных ситуациях, скажем , при отладке , обычно он не подходит. У вас могут быть методы , которые должны переопределяться в подклассе , чтобы подкласс имел какой - нибудь смысл . Возьмем класс Triangle. Он не считается завершенным , если метод area ( ) не определен. В данном случае нужен какой -то способ га рантирования того , что подкласс действительно переопределяет все необходи мые методы . В Java проблема решается посредством абстрактных методов. Абстрактный метод создается путем указания модификатора типа abstract. Абстрактный метод не содержит тела и , следовательно , не реализуется суперклассом . Таким образом , подкласс должен переопределить его он не может просто использовать версию, определенную в суперклассе. Для объявления абстрактного метода применяется приведенная ниже общая форма: — abstract тип имя(список-параметров); Как видите, тело метода отсутствует. Модификатор abstract можно использовать только в методах экземпляра. Его нельзя применять к статическим методам или конструкторам . Любой класс , содержащий один или несколько абстрактных методов , тоже должен быть объявлен абстрактным. Чтобы объявить класс абстрактным , перед ключевым словом class в начале объявления класса просто указывается ключевое слово abstract. Поскольку абстрактный класс не определяет полную реализацию, объектов абстрактного класса не бывает. Таким образом , попытка создания объекта абстрактного класса с помощью new приведет к ошибке на этапе компиляции. Если подкласс унаследован от абстрактного класса , тогда он должен реали зовать все абстрактные методы суперкласса. В противном случае подкласс дол жен быть указан как абстрактный. В итоге абстрактный член наследуется до тех пор , пока не будет достигнута полная реализация. С применением абстрактного класса можно усовершенствовать класс TwoDShape. Так как для неопределенной двумерной фигуры нет осмысленного понятия площади , в следующей версии предыдущей программы метод area() в классе TwoDShape объявлен абстрактным , равно как и сам класс TwoDShape. Разумеется, это означает, что все классы , производные от TwoDShape, должны переопределять area(). 292 Java: руководство для начинающих, 9-е издание // Создание абстрактного класса , abstract class TwoDShape { Класс TwoDShape теперь абстрактный private double width; private double height; private String name; // Стандартный конструктор. TwoDShape() { width = height = 0.0; name = "none"; } // Параметризованный конструктор. TwoDShape(double w, double h, String n) { width = w; height = h; name = n; } // Конструктор объекта с одинаковыми шириной и высотой. TwoDShape(double х, String n) { width = height = x; n; name } // Конструктор объекта из объекта. TwoDShape(TwoDShape ob) { width = ob.width; height = ob.height; name = ob.name; } // Методы доступа для width и height , double getWidthO { return width; } double getHeightO { return height; } void setWidth(double w) { width = w; } void setHeight(double h) { height = h; } String getNameO { return name; } void showDimO { System.out.println("Ширина и высота: " + width + " и " + height); } } // Теперь метод агеа() абстрактный , abstract double area(); ** Сделать метод area ( ) абстрактным // Подкласс TwoDShape для представления треугольников , class Triangle extends TwoDShape { private String style; // Стандартный конструктор. Triangle() { super(); style = "отсутствует"; } Глава 7. Наследование // Конструктор класса Triangle. Triangle(String s, double w, double h) { super(w, h, "треугольник"); style = s; } // Конструктор с одним аргументом. Triangle(double x) { super(x, "треугольник"); // вызов конструктора суперкласса style = "закрашенный"; } // Конструктор объекта из объекта. Triangle(Triangle ob) { super(ob); // передача объекта конструктору класса TwoDShape style = ob.style; } double area() { return getWidth() * getHeightO / 2; } void showStyleO { System.out.println("Стиль треугольника: " + style); } } // Подкласс TwoDShape для представления прямоугольников , class Rectangle extends TwoDShape { // Стандартный конструктор. Rectangle() { super(); } // Конструктор класса Rectangle. Rectangle(double w, double h ) { super(w, h, "прямоугольник"); // вызов конструктора суперкласса } // Конструктор квадрата. Rectangle(double х) { super(х, "прямоугольник"); } // вызов конструктора суперкласса // Конструктор объекта из объекта. Rectangle(Rectangle ob) { super(ob); // передача объекта конструктору класса TwoDShape } boolean isSquareO { if(getWidth() == getHeightO ) return true; return false; } double area() { return getWidth() * getHeightO ; } } 293 294 Java : руководство для начинающих, 9-е издание class AbsShape { public static void main(String[] args) { TwoDShape[] shapes = new TwoDShape[4]; shapes[0] = new shapes[ 1] = new shapes[2] = new shapes[3] = new Triangle("контурный", 8.0, 12.0); Rectangle(10); Rectangle(10, 4); Triangle(7.0); for(int i=0; i < shapes.length; i++) { System.out.println("Объект с именем: " + shapes[i].getName()); System.out.println("Площадь: " + shapes[i].area()); System.out.println(); } } } Как демонстрирует программа , все подклассы TwoDShape должны переопределять метод area ( ) . Чтобы убедиться в этом , попробуйте создать подкласс , не переопределяющий метод area ( ) . Вы получите ошибку на этапе компиляции. Конечно, все еще можно создавать ссылку на объект типа TwoDShape, что и сделано в программе. Тем не менее , объявлять объекты типа TwoDShape больше нельзя , из- за чего в main ( ) размер массива shapes был сокращен до 4 и объект TwoDShape больше не создавался . И последнее замечание: обратите внимание , что класс TwoDShape по прежнему включает методы showDim ( ) и getName ( ) и они не сделаны абстрактным. Вполне допустимо ( более того , так случается довольно часто ) , чтобы абстрактный класс содержал конкретные методы , которые подкласс может использовать в том виде , как есть. В подклассах должны переопределяться только методы , объявленные абстрактными. Использование ключевого слова final Какими бы мощными и полезными ни были переопределение и наследование методов, временами желательно их предотвращать. Например, у вас может быть класс , который инкапсулирует управление каким-то аппаратным устройством. Кроме того, этот класс может предложить пользователю возможность инициализировать устройство, используя патентованную информацию. В таком случае необходимо, чтобы пользователи класса могли переопределять метод инициализации. Вне зависимости от причины в Java легко предотвратить переопределение метода или наследование класса с помощью ключевого слова final. Использование ключевого слова final для предотвращения переопределения Чтобы запретить переопределение метода , в начале его объявления понадо бится указать ключевое final в качестве модификатора. Методы , объявленные как final , не могут быть переопределены. Применение ключевого слова final демонстрируется в следующем фрагменте кода: Глава 7. Наследование 295 class А { final void meth() { System.out.println("Это метод final."); } } class В extends A { void meth() { // ОШИБКА! Переопределять нельзя. System.out.println("He разрешено!"); } } Поскольку метод meth ( ) объявлен как final , его нельзя переопределять в классе В. При попытке это сделать возникнет ошибка на этапе компиляции . Использование ключевого слова final для предотвращения наследования Предотвратить наследование класса можно за счет указания перед объявлением класса ключевого слова final. Объявление класса как final также неявно объявляет все его методы как final. Вполне ожидаемо объявлять класс как abstract и final одновременно не разрешено , поскольку абстрактный класс сам по себе неполный и в обеспечении полных реализаций полагается на свои подклассы. Вот пример класса final: final class А { // ... } // Следующий класс недопустим. class В extends А { // ОШИБКА ! Создавать подкласс класса А нельзя. II . .. } В комментариях видно , что класс объявлен с ключевым словом final. в не может быть унаследован от А, т.к. А На заметку! - Начиная с версии JDK 17, в Java появилась возможность запечатывать класс. Запеча тывание предлагает тонкий контроль над наследованием и рассматривается в главе 16. Использование ключевого слова final с членами данных В дополнение к только что показанному использованию ключевое слово final также можно применять к переменным - членам для создания так назы ваемых именованных констант. Если перед именем переменной экземпляра находится final , то ее значение не может быть изменено на протяжении времени жизни программы . Разумеется, такой переменной можно присваивать начальное значение. 296 Java: руководство для начинающих, 9-е издание Например, в главе 6 был показан простой класс управления ошибками по имени ErrorMsg, который сопоставлял читабельную строку с кодом ошиб ки. Здесь первоначальный класс ErrorMsg усовершенствован за счет добавления констант final , обозначающих ошибки. Теперь вместо передачи методу getErrorMsg ( ) числа , такого как 2, можно передавать именованную целочисленную константу DISKERR. // Возвращение объекта типа String , class ErrorMsg { // Коды ошибок , final int OUTERR = 0; final int INERR = 1; м Объявление констант final final int DISKERR = 2; final int INDEXERR = 3; String[] msgs = { "Ошибка вывода", "Ошибка ввода", "Диск переполнен", "Индекс вышел за границы " }; // Возвратить сообщение об ошибке. String getErrorMsg(int i) { if(i >=0 & i < msgs.length) return msgs[i]; else return "Несуществующий код ошибки"; } } class FinalD { Использование констант final public static void main(String[] args) { ErrorMsg err = new ErrorMsg(); System.out.println(err.getErrorMsg(err.OUTERR)); System.out.println(err.getErrorMsg(err.DISKERR)); } } Обратите внимание на то, как константы final используются в main ( ) . Поскольку они являются членами класса ErrorMsg, доступ к ним должен осуществляться через объект данного класса. Конечно, они также могут быть унаследованы подклассами и доступны непосредственно внутри этих подклассов. Многие программисты на Java придерживаются стиля, предусматривающего использование для констант final идентификаторов в верхнем регистре , как делалось в предыдущем примере . Но такое правило не является жестким. !?@>A8< C M:A?5@B0 ВОПРОС. Можно ли сделать переменные -члены final статическими ? Можно ли применять ключевое слово final к параметрам метода и локальным переменным? Глава 7. Наследование 297 — ОТВЕТ. Ответ на оба вопроса да. Статическая переменная-член final позволяет ссылаться на константу через имя ее класса , а не через объект. Например , если бы константы в ErrorMsg были модифицированы посредством static, то операторы println ( ) в main ( ) выглядели бы так: System.out.println(err.getErrorMsg(ErrorMsg.OUTERR)); System.out.println(err.getErrorMsg(ErrorMsg.DISKERR)); Объявление параметра как final предотвращает его изменение внутри метода. Объявление локальной переменной как final предотвращает присваивание ей значения более одного раза . Класс Object Существует один особый класс Object, определенный в Java . Все остальные классы являются подклассами Object, т.е. Object представляет собой супер класс для всех остальных классов. Это означает, что ссылочная переменная типа Object может ссылаться на объект любого другого класса . Кроме того, поскольку массивы реализованы в виде классов, переменная типа Object также может ссылаться на любой массив. В классе Object определены методы , описанные в табл . 7.1, которые доступ ны в любом объекте. Таблица 7.1 . Методы класса Object Метод Назначение Object clone() Создает новый объект, который совпадает с клонируемым объектом boolean equals(Object объект) void finalize() Определяет, равен ли один объект другому Вызывается перед удалением неиспользуемого объекта. ( Объявлен устаревшим в JDK 9.) Class<?> getClass() int hashCode() Получает класс объекта во время выполнения Возвращает хеш-код, ассоциированный с вызывающим объектом Возобновляет выполнение потока, ожидающе- void notify() го вызывающий объект void notifyAll() String toString() void wait() void wait(long миллисекунд) void wait(long миллисекунд, int наносекунд) Возобновляет выполнение всех потоков, ожидающих вызывающий объект Возвращает строку, которая описывает объект Ожидает другого потока выполнения 298 Java : руководство для начинающих, 9- е издание Методы getClass ( ), notify(), notifyAll ( ) и wait ( ) объявлены как final. Остальные методы можно переопределять. Перечисленные в табл . 7.1 методы описаны позже в книге . Тем не менее , обратите сейчас внимание на два метода: equals( ) и toString ( ) . Метод equals ( ) сравнивает два объекта . Он возвращает true, если объекты равны, и false в противном случае . Точное определение равенства может варьироваться в зависимости от типа сравнива емых объектов. Метод toString ( ) возвращает строку, содержащую описание объекта , для которого он вызывается. Также этот метод вызывается автоматиче ски , когда объект выводится с помощью println ( ) . Многие классы переопределяют метод toString ( ) , что позволяет им адаптировать описание специально для типов объектов, которые они создают. И последнее замечание: обратите внимание на необычный синтаксис возвращаемого типа для getClass ( ) . Он имеет отношение к средству обобщений языка Java. Обобщения позволяют указывать в качестве параметра тип данных, используемый классом или методом , и обсуждаются в главе 13. Вопросы и упражнения для самопроверки 1. Имеет ли суперкласс доступ к членам подкласса ? Имеет ли подкласс до- 2. 3. 4. 5. ступ к членам суперкласса? Создайте подкласс TwoDShape по имени Circle. Определите в нем метод area ( ) , вычисляющий площадь круга , и конструктор , в котором с помощью super инициализируется часть, относящаяся к TwoDShape. Как предотвратить доступ к члену суперкласса из подкласса? Опишите назначение и применение двух версий ключевого слова super, рассмотренных в этой главе. Пусть имеется следующая иерархия: class Alpha { ... class Beta extends Alpha { ... class Gamma extends Beta { ... В каком порядке конструкторы этих классов завершают свое выполнение при создании объекта Gamma? 6. Ссылка на суперкласс может ссылаться на объект подкласса. Объясните , почему это важно и как связано с переопределением методов. 7. Что такое абстрактный класс? 8. Как предотвратить переопределение метода? Как предотвратить наследо вание класса? Глава 7. Наследование 299 9. Объясните, каким образом наследование , переопределение методов и абстрактные классы используются для поддержки полиморфизма. 10. Какой класс является суперклассом любого другого класса? 11. Верно ли утверждение, что класс , содержащий хотя бы один абстрактный метод, сам должен быть объявлен абстрактным? 12. Какое ключевое слово используется для создания именованной константы? 13. Предположим , что класс в унаследован от класса А. Далее предположим , что имеется метод по имени makeObj ( ) , который объявлен следующим образом: A makeObj(int which) { if(which == 0) return new A(); else return new B(); } Обратите внимание , что метод makeObj ( ) возвращает ссылку на объект типа А или типа В в зависимости от значения which, но возвращаемым типом makeObj ( ) является А. ( Вспомните, что ссылка на суперкласс может ссылаться на объект подкласса.) Учитывая эту ситуацию и предпола гая , что вы используете JDK 10 или более позднюю версию , какой тип будет иметь переменная myRef в показанном ниже объявлении и почему? var myRef = makeObj(1); . 14 Продолжая вопрос 13, какой тип получит переменная myRef после выпол нения следующего оператора? var myRef = (В) makeObj(1); - . £ V *V . > ./. : :: • ' */* Л™ ; . •/ * > S% “ нч*?ц * SX5; "••и I I, * I I • ' II I,I II, I .' II ч' I I Si к, s *<• Глава 8 Пакеты и интерфейсы 302 Java: руководство для начинающих, 9-е издание В этой главе • • • z Использование пакетов z Влияние пакетов на доступ z Применение модификатора доступа protected z Импортирование пакетов z Стандартные пакеты Java z Основы интерфейсов z Реализация интерфейсов z Использование ссылок на интерфейсы z Переменные типа интерфейсов z Расширение интерфейсов z Создание стандартных, статических и закрытых методов интерфейсов в настоящей главе исследуются два самых инновационных средства Java: пакеты и интерфейсы. Пакеты представляют собой контейнеры для классов. Пакеты помогают лучше организовать код и обеспечивают дополнительный уровень инкапсуляции . Как будет показано в главе 15, пакеты также играют важную роль в модулях. Интерфейс определяет набор методов , которые будут реализованы классом. Таким образом , интерфейс позволяет указать, что будет делать класс , но не то, как он будет это делать. Пакеты и интерфейсы предлагают более высокий контроль над организацией программы. Пакеты Часто бывает удобно группировать связанные части программы вместе. В Java это достигается с использованием пакета. Пакет служит двум целям. Вопервых , он предоставляет механизм , посредством которого связанные части программы могут быть организованы как единое целое . Доступ к классам , определенным в пакете, должен осуществляться через имя пакета. Таким образом, пакет позволяет именовать коллекцию классов. Во- вторых, пакет участвует в механизме управления доступом Java. Классы , определенные в пакете , можно сделать закрытыми для данного пакета и недоступными коду за его пределами. Следовательно , пакет предлагает средство , с помощью которого можно инкап сулировать классы. Давайте рассмотрим каждую функцию немного подробнее . Глава 8. Пакеты и интерфейсы 303 В общем случае при именовании класса для него выделяется имя из про странства имен . Пространство имен определяет область объявлений . Никакие два класса в Java не могут применять одно и то же имя из одного и того же пространства имен . Другими словами , внутри пространства имен каждый класс должен иметь уникальное имя . Во всех примерах из предшествующих глав ис пользовалось стандартное пространство имен . Хотя для коротких программ примеров поступать так вполне нормально , с разрастанием программ и пере полнением стандартного пространства имен возникают проблемы . В крупных программах зачастую сложно отыскать уникальные имена для каждого класса . Кроме того , необходимо избегать конфликтов имен с кодом , который написан другими программистами , работающими над тем же проектом , а также с би блиотекой Java . Решением упомянутых проблем является пакет, потому что он позволяет разделять пространство имен . Когда класс определен в пакете , имя этого пакета присоединяется к каждому классу, что позволяет избежать кон фликтов имен с другими классами , которые имеют такое же имя , но находятся в других пакетах. Поскольку пакет обычно содержит связанные классы , в Java определены специальные права доступа к коду внутри пакета. В пакете можно определить код , доступный для другого кода в том же пакете , но не для кода за пределами пакета . В результате появляется возможность создания автономных групп связанных классов , которые поддерживают конфиденциальность своей работы . Определение пакета Все классы в Java принадлежат какому - то пакету. Если оператор package от сутствует, тогда используется стандартный пакет. Более того , стандартный пакет не имеет имени , что упрощает работу с ним. Вот почему раньше вам не при ходилось беспокоиться о пакетах . В то время как стандартный пакет пригоден в коротких учебных программах , он не подходит для реальных приложений . Вы будете определять пакет для своего кода почти всегда. Чтобы создать пакет, понадобится поместить в начало файла с исходным кодом Java оператор package. Любые классы , объявленные в данном файле , будут принадлежать указанному пакету. Так как пакет определяет пространство имен , имена классов , помещаемых в файл , становятся частью пространства имен этого пакета . Вот общая форма оператора package: package пакет; Здесь пакет представляет имя пакета . Например , следующий оператор создает пакет по имени mypackage: p a c k a g e mypackage ; Обычно в Java для управления пакетами применяется файловая система , причем каждый пакет хранится в собственном каталоге , и именно такой подход 304 Java : руководство для начинающих, 9-е издание задействован в примерах, приводимых в книге. Например , файлы . class для любых классов , которые объявляются как часть mypack , должны храниться в каталоге с именем mypack. Как и везде в Java , имена пакетов чувствительны к регистру, т.е. имя каталога , в котором хранится пакет, должно точно совпадать с именем пакета. Если у вас возникли проблемы с выполнением примеров в этой главе , не забудьте тщательно проверить имена пакетов и каталогов. Имена пакетов часто записываются в нижнем регистре. Один и тот же оператор package может находиться в нескольких файлах. Оператор package лишь указывает, к какому пакету принадлежат классы, определенные в файле. Это не исключает, что другие классы в других файлах могут быть частью того же самого пакета. Большинство пакетов в реальных приложениях разнесено по нескольким файлам. Допускается создавать иерархию пакетов , для чего нужно просто отделять имя каждого пакета от имени пакета над ним с помощью точки. Вот как выгля дит общая форма оператора многоуровневого пакета: package пакет1.пакет2. пакетЗ...пакеты ; Конечно , придется создать каталоги , которые поддерживают построенную иерархию пакетов. Скажем , следующий многоуровневый пакет: package alpha.beta.gamma; должен храниться в каталогам. . . . / alpha / beta / gamma , где . . . задает путь к указанным Поиск пакетов и CLASSPATH Как только что объяснялось, пакеты обычно отражаются посредством каталогов. Тогда возникает важный вопрос: каким образом исполняющая среда Java узнает, где искать создаваемые вами пакеты ? Что касается примеров в этой главе , то ответ состоит из трех частей. Во- первых, по умолчанию исполняющая среда Java в качестве начальной точки использует текущий рабочий каталог. Таким образом , если ваш пакет расположен в каком -то подкаталоге внутри текущего каталога , то он будет найден. Во - вторых, вы можете указать путь или пути к каталогам, установив переменную среды CLASSPATH. В-третьих, вы можете применить параметр -classpath при запуске java и javac , чтобы указать путь к своим классам . Полезно отметить, что начиная с JDK 9, пакет может быть частью модуля и потому находиться в пути к модулю. Однако обсуждение модулей и путей к модулям откладывается до главы 15. Сейчас мы будем использовать только пути к классам. Например, рассмотрим следующую спецификацию пакета: package mypack; Глава 8 . Пакеты и интерфейсы 305 Чтобы программа могла найти пакет mypack, ее можно либо запустить из каталога непосредственно над mypack, либо переменная среды CLASSPATH должна включать путь к mypack, либо при запуске программы через java в параметре -classpath должен быть указан путь к mypack. Испытать примеры , приведенные в книге , проще всего , создав каталоги па кетов внутри текущего каталога разработки, поместив файлы .class в соответствующие каталоги и затем запустив программы из каталога разработки. Именно такой подход используется в рассматриваемых далее примерах. И последнее замечание: во избежание проблем лучше всего хранить все файлы .java и .class, связанные с пакетом, в каталоге этого пакета. Кроме того, каждый файл должен компилироваться из каталога , находящегося выше каталога пакета. Краткий пример пакета Приняв к сведению предыдущее обсуждение , можете испытать показанный ниже простой пример пакета bookpack, в котором создается простая база дан ных книг. // Короткая демонстрация использования пакета , package bookpack; Этот файл является частью пакета bookpack class Book { private String title; private String author; private int pubDate; ^ Таким образом, класс Book является частью пакета bookpack Book(String t, String a, int d ) { title = t; author = a; pubDate = d; } void show() { System.out.println(title); System.out.printIn(author); System.out.println(pubDate); System.out.println(); } Класс BookDemo тоже является частью пакета bookpack } class BookDemo { public static void main(String[] args) { Book[] books = new Book[5]; books[0] = new Book( "Java: A Beginner's Guide", books[1] = new Book("Java: The Complete Reference", books[2] = new Book("1984", "Schildt", 2022); "Schildt", 2022); "Orwell", 1949); 306 Java: руководство для начинающих, 9- е издание books[3] = new Book("Red Storm Rising", "Clancy", 1986); books[4] = new Book( "On the Road", "Kerouac", 1955); for(int i=0; i < books.length; i++) books[i].show(); } } Назначьте файлу имя BookDemo.java и поместите его в каталог bookpack. Скомпилируйте файл BookDemo.java, введя следующую команду в каталоге , находящемся непосредственно выше bookpack: javac bookpack/BookDemo.java Попробуйте запустить класс с применением такой команды: java bookpack.BookDemo Помните , что при вводе этой команды вы должны находиться в каталоге выше bookpack. ( Кроме того , для указания пути к bookpack вы можете при бегнуть к одному из двух других способов , описанных в предыдущем разделе.) Как объяснялось ранее, классы BookDemo и Book теперь являются частью пакета bookpack, т.е. класс BookDemo не может быть выполнен сам по себе. Другими словами, использовать следующую команду нельзя: java BookDemo Класс BookDemo должен быть уточнен именем его пакета. Пакеты и доступ к членам классов В предшествующих главах вы ознакомились с основами управления доступом, включая модификаторы private и public, но обсуждалась далеко не пол ная картина. Одна из причин связана с тем , что пакеты тоже участвуют в меха низме управления доступом Java , и данный аспект должен был отложен до тех пор, пока не будут раскрыты пакеты . Прежде чем продолжить, важно отметить, что средство модулей предлагает еще одно измерение доступности , но здесь мы сосредоточимся исключительно на взаимодействии между пакетами и классами. private, На видимость элемента влияет его спецификация доступа public, protected или стандартный и пакет, в котором он находится. Таким образом , применительно к классам и пакетам видимость элемента определяется его видимостью внутри класса и его видимостью внутри пакета . Этот многоуровневый подход к управлению доступом поддерживает широкий набор привилегий доступа. В табл . 8.1 приведена сводка по различным уровням доступа. Ниже каждый вариант доступа рассматривается по отдельности. — — Глава 8. Пакеты и интерфейсы 307 Таблица 8.1 . Уровни доступа к членам классов Член Стандартный член Член Член private protected public Видимый в том же классе Да Да Да Видимый в подклассе внутри Нет Да Да Да Да Видимый в любом не подклассе внутри того же пакета Нет Да Да Да Видимый в подклассе внутри другого пакета Нет Нет Да Да Видимый в любом не подклассе внутри другого пакета Нет Нет Нет Да того же пакета Если член класса не имеет явного модификатора доступа , тогда он будет ви димым внутри своего пакета , но не за его пределами. Поэтому спецификация стандартного доступа будет использоваться для элементов, которые желательно сохранить закрытыми для кода вне пакета , но открытыми для кода внутри него. Члены , явно объявленные как public, обладают наибольшей видимостью, и к ним можно получить доступ из разных классов и разных пакетов. Член private доступен только другим членам его класса. На член private не влияет его членство в пакете. Член , указанный как protected, доступен внутри своего пакета и для подклассов в других пакетах. Уровни доступа к членам классов, описанные в табл . 8.1, применимы только к членам классов. Класс верхнего уровня имеет только два возможных уровня доступа: стандартный и открытый. Когда класс объявлен как public, он доступен за пределами своего пакета. Если класс имеет стандартный доступ , то к нему может получить доступ только другой код в том же пакете. Кроме того , класс , объявленный как public, должен находиться в файле с тем же именем. На заметку! Не забывайте, что средство модулей тоже может влиять на доступность. Модули обсуждаются в главе 15 . Пример доступа к пакету В показанном ранее примере пакета классы Book и BookDemo находились в одном пакете , поэтому никаких проблем с использованием Book у класса BookDemo не возникало , т.к. привилегия стандартного доступа предоставляет доступ всем членам одного и того же пакета . Тем не менее , если бы класс Book располагался в одном пакете , а класс BookDemo в другом , тогда ситуация оказалась бы другой. В таком случае доступ к Book был бы запрещен. Чтобы — 308 Java: руководство для начинающих, 9- е издание сделать Book доступным для других пакетов, понадобится внести три изменения. Во-первых, класс Book необходимо объявить как public, что сделает его видимым за пределами bookpack. Во- вторых, конструктор класс Book должен быть определен как public. В-третьих, метод show ( ) класса Book должен быть объявлен как public. В итоге упомянутые элементы станут видимыми и за пределами bookpack. Таким образом , чтобы класс Book был пригоден для использования в других пакетах, его нужно переписать, как показано ниже. // Класс Book, переписанный для открытого доступа. package bookpack; public class Book { private String title; private String author; private int pubDate; Класс Book и его члены должны быть открытыми, чтобы их можно было использовать в других пакетах // Теперь открытый. public Book(String t, String a, int d) { title = t; author = a; pubDate = d; } // Теперь открытый , public void show( ) { System.out.printIn(title); System.out.printIn(author); System.out.printIn(pubDate); System.out.printIn(); } } Чтобы задействовать класс Book в другом пакете, потребуется либо воспользоваться оператором import, описанным в следующем разделе , либо полностью уточнить его имя, включив полную спецификацию пакета. Например, вот класс UseBook, который содержится в пакете bookpackext. Для работы с классом Book в нем применяется полностью уточненное имя. // Этот класс находится в пакете bookpackext. package bookpackext; // Использование класса Book из пакета bookpack. Перед именем класса Book указывается имя пакета bookpack class UseBook { public static void main(String[] args) { bookpack.Book[] books = new bookpack.Book[5]; * - books[0] = new bookpack.Book( "Java: A Beginner's Guide", "Schildt", 2022); books[1] = new bookpack.Book("Java: The Complete Reference", "Schildt", 2022); books[2] = new bookpack.Book("1984", "Orwell", 1949); Глава 8 . Пакеты и интерфейсы 309 books[3] = new bookpack.Book("Red Storm Rising", "Clancy", 1986); books[4] = new bookpack.Book("On the Road ", "Kerouac", 1955); for(int i=0; i < books.length; i++) books[ i].show(); } } Обратите внимание, что каждое обращение к Book предваряется квалификатором bookpack, без которого класс Book не будет найден при попытке компи ляции UseBook. Защищенные члены Новичков в Java иногда сбивает с толку смысл и применение модификатора protected. Как объяснялось ранее, модификатор protected создает член , доступный внутри его пакета и для подклассов в других пакетах. Таким образом , член protected доступен для использования всеми подклассами , но все-таки защищен от произвольного доступа со стороны кода , находящегося за пределами его пакета. Чтобы лучше понять назначение модификатора protected, давайте рассмотрим пример. Первым делом модифицируйте класс Book, сделав его переменные экземпляра защищенными: // Объявление защищенными переменных экземпляра в классе Book. package bookpack; public class Book { // теперь эти члены защищенные protected String title;; Теперь это защищенные переменные экземпляра protected String author; protected int pubDate; _ _ public Book(String t, String a, int d) { title = t; author = a; pubDate = d; } public void show() { System.out.printIn(title); System.out.println(author); System.out.println(pubDate); System.out.println(); } } Далее создайте подкласс Book по имени ExtBook и класс ProtectDemo, использующий ExtBook. В ExtBook добавляется поле с названием издательства и несколько методов доступа. Оба класса будут находиться в собственном пакете bookpackext. Они показаны ниже: 310 Java: руководство для начинающих, 9-е издание // Демонстрация применения protected , package bookpackext; class ExtBook extends bookpack.Book { private String publisher; public ExtBook(String t, String a, int d, String p) { super(t, a, d); publisher = p; } public void show() { super.show(); System.out.printIn(publisher); System.out.printIn(); } public String getPublisher() { return publisher; } public void setPublisher(String p) { publisher = p; } /* Разрешено, потому что подкласс имеет доступ к защищенным членам. */ public String getTitleO { return title; } public void setTitle(String t) { title = t; } public String getAuthorO { return author; } Доступ к членам класса Book разрешен для подклассов public void setAuthor(String a) { author = a; } public int getPubDateO { return pubDate; } public void setPubDate(int d) { pubDate = d; } } class ProtectDemo ( public static void main(String[] args) { ExtBook[] books = new ExtBook[5]; books[0] = new ExtBook("Java: A Beginner's Guide", "Schildt", 2022, "McGraw Hill"); = new ExtBook("Java: The Complete Reference", "Schildt", 2022, "McGraw Hill"); books[2] = new ExtBook( "1984", books[1] "Orwell", 1949, "Harcourt Brace Jovanovich"); books[3] = new ExtBook( "Red Storm Rising", books[4] = new ExtBook("On the Road", "Clancy", 1986, "Putnam"); "Kerouac", 1955, "Viking"); for(int i=0; i < books.length; i++) books[i].show(); // Найти книги по автору. System.out.println("Все книги, автор которых Schildt:"); for(int i=0; i < books.length; i++) if( books[i].getAuthor() == "Schildt") System.out.println(books[i].getTitle()); // books[0].title } } t = "test title"; // Ошибка - недоступно. Доступ к защищенному полю разрешен только подклассам Глава 8 . Пакеты и интерфейсы 311 Рассмотрим сначала код внутри класса ExtBook. Поскольку класс ExtBook расширяет Book, он имеет доступ к защищенным членам Book, хотя ExtBook находится в другом пакете. Таким образом , он может обращаться к title, author и pubDate напрямую , как делается в методах доступа , созданных для указанных переменных. Однако в классе ProtectDemo доступ к этим перемен ным запрещен , потому что ProtectDemo не является подклассом Book. Скажем , если вы удалите символы комментария из следующей строки, то программа не скомпилируется: // books[0].title = "test title"; // Ошибка - доступ запрещен. Импортирование пакетов При использовании класса из другого пакета можно полностью уточнять имя класса именем его пакета , как делалось в предшествующих примерах. Тем не менее , такой подход может быстро стать утомительным и неудобным, особен но если уточняемые классы глубоко вложены в иерархию пакетов. Поскольку язык Java был изобретен программистами для программистов, а программисты не любят утомительных конструкций , то неудивительно, что существует более удобный метод для работы с содержимым пакетов: оператор import . С помощью оператора import можно открыть доступ к одному или нескольким членам пакета , что позволит взаимодействовать с этими членами напрямую без явного уточнения пакета . Ниже показана общая форма оператора import: import пакет.имя-класса ; В пакет указывается имя пакета , которое может включать его полный путь, а в имя-класса имя импортируемого класса. Если необходимо импортировать все содержимое пакета , тогда для имени класса понадобится указать звездочку ( * ). Вот примеры обеих форм: — import mypack.MyClass import mypack.*; В первом случае из пакета mypack импортируется класс MyClass, а во втором все классы. В файле исходного кода Java операторы import располагаются сразу после оператора package ( если он имеется ) и перед любыми определе- — ниями классов. Оператор import можно применять для открытия доступа к пакету bookpack, чтобы класс Book можно было использовать без уточнения. Для этого нужно просто добавить в начало любого файла , в котором задействован класс Book, следующий оператор import: import bookpack.*; 312 Java: руководство для начинающих, 9-е издание Скажем , ниже показан код класса UseBook, переделанный для применения оператора import: // Демонстрация применения оператора import , package bookpackext; import bookpack.*; Импортирование пакета bookpack // Использовать класс Book из пакета bookpack. class UseBook { public static void main(String[] args) { Теперь на класс Book можно ссылаться Book[] books = new Book[5]; M напрямую без уточнения books[0] = new Book("Java: A Beginner's Guide", "Schildt", 2022); books[1] = new Book("Java: The Complete Reference", "Schildt", 2022); books[2] = new Book("1984 ", "Orwell", 1949); books[3] = new Book("Red Storm Rising", "Clancy", 1986); books[4] = new Book("On the Road", "Kerouac", 1955); for(int i=0; i < books.length; i++) books[i].show(); } } Обратите внимание , что класс Book больше не требуется уточнять именем его пакета. Библиотека классов Java содержится в пакетах Как объяснялось ранее в книге, в языке Java определено большое количество стандартных классов, доступных для всех программ. Такую библиотеку классов часто называют Java API и она хранится в пакетах. На вершине иерархии пакетов находится пакет java , от которого происходит набор подпакетов; некоторые из них кратко описаны в табл . 8.2. Таблица 8.2 . Подпакеты пакета java Подпакет Описание java . lang Содержит большое количество классов общего назначения Содержит классы для поддержки ввода-вывода Содержит классы для поддержки работы в сети Содержит большое количество служебных классов, включая Collections Framework Содержит классы для поддержки Abstract Window Toolkit java . io java . net java . util java.awt Глава 8 . Пакеты и интерфейсы 313 Подпакет java.lang использовался с самого начала книги . Среди прочего он содержит класс System, который применялся при выполнении вывода с ис пользованием метода println ( ) . Пакет java.lang уникален в том плане , что он автоматически импортируется в каждую программу на Java . Вот почему не нужно было импортировать java.lang в предшествующих примерах программ. Однако другие пакеты придется импортировать явно. Мы рассмотрим несколько пакетов в последующих главах. Интерфейсы В объектно-ориентированном программировании иногда полезно опреде лять, что должен делать класс , но не то, как он будет это делать. Пример такого подхода вы уже видели: абстрактный метод. Абстрактный метод определяет сигнатуру для метода , но не обеспечивает реализацию. Подкласс должен предоставлять собственную реализацию каждого абстрактного метода , определенного его суперклассом. Таким образом , абстрактный метод определяет интерфейс метода, но не его реализацию. Хотя абстрактные классы и методы полезны, такую концепцию можно продвинуть еще дальше . В Java имеется возможность полного отделения интерфейса класса от его реализации с применением ключевого слова interface. Интерфейс синтаксически подобен абстрактному классу в том смысле , что можно указывать один или несколько методов , не имеющих тела . Эти методы должны быть реализованы классом , чтобы их действия были определены. Таким образом , интерфейс указывает, что должно быть сделано , но не как именно. После того как интерфейс определен, он может быть реализован лю бым количеством классов. Кроме того , один класс способен реализовывать любое количество интерфейсов. Для реализации интерфейса класс должен предоставить тела ( реализации ) методов, описываемых интерфейсом. Каждый класс имеет полную свободу в определении деталей собственной реализации . Два класса могут по- разному реализовывать один и тот же интерфейс , но каждый класс по- прежнему будет поддерживать один и тот же набор методов. Таким образом , в коде , где известен данный интерфейс , могут использоваться объекты любого из двух классов, поскольку интерфейс для этих объектов одинаков. За счет предоставления ключевого слова interface язык Java позволяет в полной мере задействовать аспект полиморфизма “ один интерфейс , несколько методов ”. Прежде чем продолжить, необходимо сделать важное замечание . В версии JDK 8 к интерфейсу было добавлено функциональное средство, значительно изменившее его возможности . До JDK 8 в интерфейсе нельзя было определять какие-то реализации. Речь идет о виде интерфейса, упрощенная форма которого представлена выше, где ни одно объявление метода не снабжалось телом . Таким образом , до выхода JDK 8 интерфейс мог определять только “ что ” , но не “ как ” . В версии JDK 8 ситуация изменилась. Начиная с JDK 8 , к методу 314 Java : руководство для начинающих, 9-е издание интерфейса можно добавлять стандартную реализацию . Кроме того , в JDK 8 также добавлены статические методы интерфейса , а начиная с JDK 9 , интерфейс может включать закрытые методы . В результате теперь интерфейс способен устанавливать какое -то поведение . Тем не менее , такие методы представляют собой то , что по существу является средствами специального назначения , и первоначальный замысел интерфейса по - прежнему остается . Поэтому, как пра вило , вы все еще будете часто создавать и использовать интерфейсы , в которых новые средства не применяются. По указанной причине мы начнем с обсужде ния интерфейса в его традиционной форме . Более новые средства интерфейса описаны в конце главы . Ниже показана упрощенная общая форма интерфейса: доступ interface имя { возвращаемый-тип имя-метода1 { список-параметров ) ; возвращаемый-тип имя-метода2 { список-параметров) ; тип финальное-имя-переменной1 = значение; тип финальное-имя-переменной2 = значение; // . . . возвращаемый-тип имя-методаМ { список-параметров ) ; тип финальное-имя-переменнойЫ = значение; } Для интерфейса верхнего уровня вместо доступ указывается либо public , либо вообще ничего . Если модификатор доступа отсутствует, тогда устанавли вается стандартный доступ и интерфейс будет доступным только другим эле ментам пакета , в котором он объявлен . Когда интерфейс объявлен как public, он может использоваться в коде вне пакета , где он объявлен . ( Когда интерфейс объявлен как public, он должен находиться в файле с таким же именем . ) В имя указывается имя интерфейса , которое может быть любым допустимым идентификатором . В традиционной форме интерфейса методы объявляются с применением только их возвращаемого типа и сигнатуры . По существу они представляют собой абстрактные методы . Таким образом , каждый класс , включающий этот интерфейс , обязан реализовывать все его методы . В интерфейсе методы неявно открыты . Переменные , объявленные в интерфейсе , являются не переменными экземпляра , а неявно переменными public, final и s t a t i c и должны быть инициа лизированы . Таким образом , в сущности , это константы . Вот пример определения интерфейса для класса , который генерирует последовательность чисел: public interface Series int getNext(); void reset(); void setStart(int x); { // возврат следующего числа в последовательности // сброс // установка начального значения } Интерфейс объявлен как public , так что он может быть реализован в коде внутри любого пакета . Глава 8 . Пакеты и интерфейсы 315 Реализация интерфейсов После определения интерфейса один или несколько классов могут его реализовать. Для реализации интерфейса понадобится включить в определение класса конструкцию implements и затем создать методы , требуемые интерфейсом. Ниже показана общая форма класса , содержащего конструкцию implements: class имя-класса extends суперкласс implements интерфейс { // тело класса } Если класс реализует более одного интерфейса , тогда имена интерфейсов отделяются друг от друга запятыми. Разумеется , конструкция extends необяза тельна. Методы , реализующие интерфейс , должны быть объявлены как public. Кроме того , сигнатура типов реализующего метода должна в точности совпадать с сигнатурой типов, указанной в определении интерфейса . Далее приведен пример класса ByTwos, реализующего представленный ранее интерфейс Series, который генерирует последовательность чисел , где каждое текущее число на 2 больше предыдущего: // Реализация интерфейса Series. class ByTwos implements Series { int start; int val; ByTwos() { start = 0; val = 0; t Реализация интерфейса Series } public int getNext() { val += 2; return val; } public void reset() { val = start; } public void setStart(int x) { start = x; val = x; } } Обратите внимание, что методы getNext(), reset ( ) и setStart ( ) объявлены с использованием модификатора доступа public. Это обязательно. Метод, определенный интерфейсом , должен быть реализован как открытый , потому что все члены интерфейса неявно являются открытыми. В следующем классе демонстрируется работа с классом ByTwos: 316 Java: руководство для начинающих, 9-е издание class SeriesDemo { public static void main(String[] args) { ByTwos ob = new ByTwos(); for(int i=0; i < 5; i++) System.out.println("Следующее значение: " + ob.getNext()); System.out.printIn("\nC6poc"); ob.reset(); for(int i=0; i < 5; i++) System ,out.println("Следующее значение: " + ob.getNextO ); System.out.println("\пНачало со значения 100"); ob.setStart(100); for(int i=0; i < 5; i++) System ,out.println("Следующее значение: " + ob.getNextO ); } } Вывод, генерируемый программой , выглядит так: Следующее Следующее Следующее Следующее Следующее значение: 2 значение: 4 значение: 6 значение: 8 значение: 10 Сброс Следующее значение: 2 Следующее значение: 4 Следующее значение: 6 Следующее значение: 8 Следующее значение: 10 Начало со Следующее Следующее Следующее Следующее Следующее значения 100 значение: 102 значение: 104 значение: 106 значение: 108 значение: 110 Для классов, реализующих интерфейсы , допустимо и распространено опре деление собственных дополнительных членов . Например , в показанной ниже версии ByTwos добавлен метод getPrevious ( ) , возвращающий предыдущее значение из последовательности: // Реализация интерфейса Series и добавление метода getPrevious(). class ByTwos implements Series { int start; int val; int prev; ByTwos() { start = 0; val = 0; prev = -2; } Глава 8. Пакеты и интерфейсы 317 public int getNext() { prev = val; val += 2; return val; } public void reset() { val = start; prev = start - 2; } public void setStart(int x) { start = x; val = x; prev = x - 2; } int getPrevious() { *+ return prev; Добавление метода , который не определен в интерфейсе Series } } Обратите внимание, что добавление метода getPrevious ( ) потребовало изменения реализации методов , определенных в Series. Однако поскольку интерфейс для этих методов остается прежним , изменение происходит незаметно и не нарушает работу существующего кода. Это одно из преимуществ интер - фейсов. Как объяснялось ранее , интерфейс может быть реализован любым количеством классов. Скажем , следующий класс по имени ByThrees генерирует последовательность чисел , каждое из которых на 3 больше предыдущего: // Реализация интерфейса Series. class ByThrees implements Series { int start; int val; ByThrees() { start = 0; val = 0; } public int getNext() { val += 3; return val; } public void reset() { val = start; } public void setStart(int x ) { start = x; val = x; } } Реализация интерфейса Series другим способом 318 Java: руководство для начинающих, 9-е издание И еще один момент: если класс включает интерфейс, но не полностью реа лизует методы , определенные в этом интерфейсе, тогда класс должен быть объявлен абстрактным. Создать объекты такого класса не удастся , но его можно применять в качестве абстрактного суперкласса , позволяя подклассам обеспе чивать полную реализацию. Использование ссылок на интерфейсы Вы можете быть несколько удивлены, узнав о возможности объявления ссы лочной переменной интерфейсного типа . Другими словами, можно создать переменную ссылки на интерфейс. Такая переменная может ссылаться на любой объект, реализующий ее интерфейс. При вызове метода объекта через ссылку на интерфейс выполняется версия метода , реализованная объектом . Процесс аналогичен применению ссылки на суперкласс для доступа к объекту подкласса , как было описано в главе 7. Описанный процесс демонстрируется в следующем примере , где одна и та же переменная ссылки на интерфейс используется для вызова методов объектов ByTwos и ByThrees. // Демонстрация использования ссылки на интерфейс. class ByTwos implements Series { int start; int val; ByTwos() { start = 0; val = 0; } public int getNext( ) { val += 2; return val; } public void reset() { val = start; } public void setStart(int x) { start = x; val = x; } } class ByThrees implements Series { int start; int val; ByThrees( ) { start = 0; val = 0; } Глава 8 . Пакеты и интерфейсы 319 public int getNext( ) { val += 3; return val; } public void reset() { val = start; } public void setStart(int x) { start = x; val = x; } } class SeriesDemo2 { public static void main(String[] args) { ByTwos twoOb = new ByTwos(); ByThrees threeOb = new ByThrees(); Series ob; for(int i=0; i < 5; i++) { ob = twoOb; System.out.println("Следующее значение ByTwos: " + ob.getNext()); < ob = threeOb; System.out.println("Следующее значение ByThrees: " + ob.getNext()); } } } Доступ к объекту через ссылку на интерфейс Переменная ob в main ( ) объявляется как ссылка на интерфейс Series, т.е. ее можно применять для хранения ссылок на любой объект, реализующий Series. В данном случае ob используется для ссылки на twoOb и threeOb, которые являются объектами типа ByTwos и ByThrees и оба реализуют интерфейс Series. Переменной ссылки на интерфейс известны только методы , объявленные в ее интерфейсном типе. Таким образом , ob нельзя применять для доступа к каким-то другим переменным или методам , которые могли бы поддерживаться объектом. Упражнение 8.1 Создание интерфейса Queue Чтобы продемонстрировать истинную мощь интерфей ICharQ.java , мы рассмотрим практический пример. В предше сов IQDemo.java ствующих главах был создан класс Queue, реализующий простую очередь фиксированного размера для хранения символов. Тем не менее , построить очередь можно разными способами. В частности , очередь может быть фиксированного размера или “ растущей ”. Очередь может быть линейной и в этом случае израсходоваться либо кольцевой , что подразумевает освобождение места под новые элементы после изъятия символов из очереди. Кроме того , очередь может храниться в массиве , связном списке , двоичном дереве и т.д. Как 320 Java: руководство для начинающих, 9-е издание бы ни была реализована очередь, интерфейс для нее остается тем же самым , т.е . методы put ( ) и get ( ) определяют интерфейс для очереди независимо от дета лей ее реализации. Поскольку интерфейс для очереди отделен от ее реализации, определить его нетрудно , оставляя каждой реализации учет всех особенностей. В упражнении будет создан интерфейс очереди символов и три реализа ции. Во всех трех реализациях для хранения символов используется массив. Одна очередь будет линейной и фиксированного размера , как та , что разраба тывалась ранее . Вторая очередь будет кольцевой. В кольцевой очереди по достижении конца массива , лежащего в ее основе, значения индексов извлечения и помещения будут автоматически изменяться так, чтобы указывать на начало очереди. Другими словами , в кольцевую очередь можно будет поместить любое количество элементов при условии своевременного извлечения имеющихся элементов. Наконец, третья очередь будет динамической, т.е. ее размеры будут увеличиваться по мере необходимости. . 1 Создайте файл по имени iCharQ.java и поместите в него следующее определение интерфейса: // Интерфейс очереди символов , public interface ICharQ { // Поместить символ в очередь , void put(char ch); // Извлечь символ из очереди , char get(); } Как видите, интерфейс очень прост и состоит только из двух методов. Каждый класс, реализующий ICharQ, должен будет реализовывать эти методы. 2. Создайте файл по имени iQDemo.java. 3. Добавьте в IQDemo.java класс FixedQueue: // Класс для представления очереди символов фиксированного размера , class FixedQueue implements ICharQ { private char[] q; // массив, в котором хранится очередь private int putloc, getloc; // индексы для позиций помещения // и извлечения // Конструктор пустой очереди заданного размера , public FixedQueue(int size ) { q // выделение памяти под очередь new char[size]; putloc = getloc = 0; } // Поместить символ в очередь . public void put(char ch ) { if( putloc==q.length ) { System.out.println(" Очередь переполнена."); return; - } q[putloc++] } ch; Глава 8 . Пакеты и интерфейсы 321 // Извлечь символ из очереди , public char get() { if(getloc == putloc) { System.out.println(" - Очередь пуста.” ); return (char) 0; } return q[getloc++ ]; } } Эта реализация iCharQ взята из класса Queue, созданного в главе 5, который должен быть вам уже знаком. 4. Добавьте в файл iQDemo.java показанный далее класс CircularQueue. Он реализует кольцевую очередь для символов. // Кольцевая очередь. class CircularQueue implements ICharQ { private char[] q; // массив, в котором хранится очередь // индексы для позиций помещения private int putloc, getloc; // и извлечения // Конструктор пустой очереди заданного размера , public CircularQueue(int size ) { q = new char[size+1]; // выделение памяти под очередь putloc = getloc = 0; } // Поместить символ в очередь , public void put( char ch ) { /*Очередь переполнена, если либо putloc на единицу меньше getloc, либо putloc указывает на конец массива,a getloc - на его начало*/ if(putloc+ l==getloc | ((putloc==q.length-1) & (getloc==0))) { System.out.println(" - Очередь переполнена."); return; } q[putloc++ ] = ch; if(putloc==q.length ) putloc // закольцевать = 0; } // Извлечь символ из очереди , public char get() { if(getloc == putloc) { System.out.println(" - Очередь пуста."); return (char) 0; } char ch = q[getloc++]; if(getloc==q.length) getloc = 0; // закольцевать return ch; } } Кольцевая очередь работает за счет многократного использования пространства в массиве , которое освобождается при извлечении элемен тов. Таким образом , она способна хранить неограниченное количество 322 Java : руководство для начинающих, 9-е издание элементов при условии , что элементы удаляются. Несмотря на концептуальную простоту ( нужно лишь обнулить индекс по достижении конца массива ) , граничные условия поначалу немного сбивают с толку. Кольцевая очередь заполняется не тогда , когда достигается конец лежащего в ос нове массива , а когда сохранение элемента приведет к перезаписыванию элемента , который не был извлечен . Таким образом , в методе put ( ) для определения , заполнена ли очередь, необходимо проверять несколько условий . Как следует из комментариев , очередь заполнена , когда либо p u t l o c на единицу меньше g e t l o c , либо p u t l o c указывает на конец массива , а g e t l o c — на его начало . Как и ранее , очередь пуста , когда значения getloc и putloc равны . Чтобы упростить проверки подобного рода , лежащий в основе массив создается на единицу больше , чем размер очереди . 5. Поместите в файл iQDemo.java приведенный далее код класса DynQueue, реализующий “ растущую ” очередь , размер которой расширяется в случае исчерпания свободного пространства . // Динамическая очередь. class DynQueue implements ICharQ { // массив, в котором хранится очередь private char[] q; // индексы для позиций помещения private int putloc, getloc; // и извлечения // Конструктор пустой очереди заданного размера , public DynQueue(int size ) { // выделение памяти под очередь q = new char[size]; putloc = getloc = 0; } // Поместить символ в очередь , public void put(char ch) { if( putloc==q.length ) { // Увеличить размер очереди. char[] t = new char[q.length * 2]; // Скопировать элементы в новую очередь , for(int i=0; i < q.length; i++) t[ i] = q[i]; q = t; } q[putloc++] ch; } // Извлечь символ из очереди , public char get() { if(getloc == putloc ) { System.out.println(" Очередь пуста."); return (char) 0; - } return q[getloc++]; } } Глава 8 . Пакеты и интерфейсы 323 В данной реализации очереди при попытке сохранения элемента , когда очередь заполнена , выделяется память под новый базовый массив , который в два раза больше исходного , текущее содержимое очереди копируется в этот массив и ссылка на новый массив сохраняется в q. 6. Для демонстрации работы трех реализаций интерфейса iCharQ поместите в файл iQDemo.java показанный ниже код класса iQDemo, в котором ссыл ка на ICharQ применяется для доступа ко всем трем очередям . // Демонстрация использования интерфейса ICharQ. class IQDemo { public static void main(String[] args) { FixedQueue ql = new FixedQueue(10); DynQueue q2 = new DynQueue(5); CircularQueue q3 = new CircularQueue(10); ICharQ iQ; char ch; int i; iQ = ql ; // Поместить ряд символов в очередь фиксированного размера , for(i=0; i < 10; i++) iQ.put((char) ('A' + i)); // Отобразить содержимое очереди. System.out.print("Содержимое очереди фиксированного размера: "); for(i=0; i < 10; i++) { ch = iQ.get(); System.out.print(ch); } System.out.println(); iQ = q2; // Поместить ряд символов в динамическую очередь. for(i=0; i < 10; i++) i)); iQ.put((char) ('Z // Отобразить содержимое очереди. System.out.print("Содержимое динамической очереди: ); for(i=0; i < 10; i++) { ch = iQ.getO ; System.out.print(ch); } System.out.println(); iQ = q3; // Поместить ряд символов в кольцевую очередь. for(i=0; i < 10; i++) iQ.put((char) ('A' + i)); // Отобразить содержимое очереди. System.out.print("Содержимое кольцевой очереди: "); for(i=0; i < 10; i++) { ch = iQ.getO ; System.out.print(ch); } System.out.println(); 324 Java : руководство для начинающих, 9-е издание // Поместить дополнительные символы в кольцевую очередь , for(i=10; i < 20; i++) iQ.put((char) ('A' + i)); // Отобразить содержимое очереди. System.out.print("Содержимое кольцевой очереди: "); for(i=0; i < 10; i++) { ch = iQ.get(); System.out.print(ch ); } System.out.println(" ХпСохранение и использование элементов в кольцевой очереди."); // Сохранение и использование элементов в кольцевой очереди. for(i=0; i < 20; i++) { iQ.put((char) ('A ' + i)); ch = iQ.get(); System.out.print(ch); } } } . 7 Вот вывод, генерируемый программой: Содержимое очереди фиксированного размера: ABCDEFGHIJ Содержимое динамической очереди: ZYXWVUTSRQ Содержимое кольцевой очереди: ABCDEFGHIJ Содержимое кольцевой очереди: KLMNOPQRST Сохранение и использование элементов в кольцевой очереди. ABCDEFGHIJKLMNOPQRST 8. Попробуйте самостоятельно поупражняться с очередями. Создайте кольцевую версию DynQueue. Добавьте в интерфейс iCharQ метод reset ( ) , который сбрасывает очередь. Создайте статический метод, который копирует содержимое очереди одного типа в очередь другого типа . Переменные в интерфейсах Как уже упоминалось, в интерфейсах могут быть объявлены переменные , но они неявно являются public, static и final. На первый взгляд может показаться , что применение таких переменных будет крайне ограниченным , но верно как раз обратное. В крупных программах обычно используется несколько постоянных значений , которые описывают размеры массива , различные ограничения , специальные значения и т.д. Поскольку крупная программа обычно хранится в нескольких отдельных исходных файлах , должен быть удобный способ делать такие константы доступными для каждого файла . Одно из решений в Java предлагают переменные интерфейсов. Чтобы определить набор общих констант, понадобится создать интерфейс , содержащий только эти константы, без каких-либо методов. Каждый файл , ко торому нужен доступ к константам , просто “ реализует ” интерфейс, что позво ляет увидеть константы . Вот пример: Глава 8 . Пакеты и интерфейсы 325 // Интерфейс, содержащий константы , interface IConst { int MIN = 0; int MAX = 1 0; String ERRORMSG = "Ошибка выхода за границы "; } class IConstD implements IConst { public static void main(String[ j args) { int[] nums = new int[MAX]; Это константы for(int i=MIN; i < 11; i++ ) { if(i >= MAX) System.out.println(ERRORMSG); else ( nums[ i i; System.out.print(nums[i] + " "); } } } } На заметку! Применение интерфейсов для определения общих констант является спорным приемом. Он описан здесь ради полноты. Интерфейсы можно расширять Один интерфейс может быть унаследован от другого с использованием ключевого слова extends. Синтаксис применяется такой же , как и при наследовании классов. Когда класс реализует интерфейс , унаследованный от другого интерфейса , он должен предоставить реализации всех методов, требуемых цепочкой наследования интерфейса. Ниже приведен пример: // Один интерфейс может расширять другой. interface А { void methl(); void meth2(); } // Интерфейс В теперь включает methl() и meth2(), а также добавляет meth3(). interface В extends А { -< Интерфейс В унаследован от А void meth3(); } // В этом классе потребуется реализовать все методы интерфейсов А и В. class MyClass implements В { public void methl() { System.out.println ("Реализация methl()."); } public void meth2( ) { System.out.println("Реализация meth2()."); } 326 Java : руководство для начинающих, 9-е издание public void meth3() { System.out.println("Реализация meth3()."); } } class IFExtend { public static void main(String[] args) { MyClass ob = new MyClassO ; ob.methl (); ob.meth2(); ob.meth3(); } } В качестве эксперимента попробуйте удалить реализацию метода methl ( ) в MyClass, что вызовет ошибку на этапе компиляции. Как было указано ранее , любой класс , реализующий интерфейс, должен реализовывать все методы, тре буемые этим интерфейсом , в том числе все методы , которые унаследованы от других интерфейсов. Стандартные методы интерфейса Как уже объяснялось, до выхода JDK 8 в интерфейсе нельзя было определять какую-либо реализацию. Другими словами , во всех предшествующих версиях Java методы, определяемые в интерфейсе, были абстрактными и не содержали тела , что является традиционной формой и типом интерфейса , который использовался в предыдущих обсуждениях. В выпуске JDK 8 ситуация изменилась за счет добавления к интерфейсу нового средства, называемого стандартными методами. Стандартный метод позволяет определить реализацию по умолчанию для метода интерфейса. Другими словами , с применением данного средства метод интерфейса может предоставлять тело , а не быть абстрактным. Во время разработки стандартный метод также упоминался как расширяющий метод, так что вполне вероятно , что вы столкнетесь с использованием обоих терминов. Основной мотивацией появления стандартных методов было предоставление средств, с помощью которых удалось бы расширять интерфейсы без нару шения работы существующего кода . Вспомните , что должны быть обеспечены реализации для всех методов, определенных интерфейсом. В прошлом , если к популярному, широко используемому интерфейсу добавлялся новый метод, то это нарушало работу существующего кода , поскольку для данного метода отсутствовала реализация. Средство стандартных методов решает проблему, предоставляя реализацию , которая будет применяться , когда явно не указана другая реализация. Таким образом , добавление стандартного метода не приведет к на рушению функционирования существующего кода. Еще одной мотивацией для создания средства стандартных методов было желание указать методы в интерфейсе, которые, по сути, являются необязательными , в зависимости от того, как используется интерфейс. Например, в интерфейсе Глава 8 . Пакеты и интерфейсы 327 может быть определена группа методов, которые воздействуют на последовательность элементов. Один из этих методов может называться remove ( ) и предназначаться для удаления элемента из последовательности. Однако если интерфейс предназначен для поддержки как модифицируемых , так и немодифицируемых последовательностей , то метод remove ( ) не является обязательным , поскольку он не будет применяться немодифицируемыми последовательностями . В про шлом класс , реализующий немодифицируемую последовательность, должен был бы определять пустую реализацию remove ( ) , даже если в этом не было необходимости. Теперь в интерфейсе можно указать стандартную реализацию для remove ( ) , которая либо ничего не делает, либо сообщает об ошибке. Обеспе чение такой стандартной реализации не позволяет классу, используемому для немодифицируемых последовательностей , определять собственную заглушку метода remove ( ) . Таким образом , за счет стандартной реализации интерфейс делает реализацию метода remove ( ) в классе необязательной. Важно отметить, что добавление стандартных методов не меняет ключевого аспекта интерфейса: интерфейс по- прежнему не может иметь переменных экземпляра . Итак, определяющее различие между интерфейсом и классом заключается в том , что класс способен поддерживать информацию о состоянии , а интерфейс нет. Кроме того, невозможно создавать экземпляр интерфейса сам по себе. Он должен быть реализован классом . Следовательно , несмотря на то, что современные версии Java позволяют интерфейсу определять стандартные методы , интерфейс все равно должен быть реализован классом , если необходимо создать экземпляр. И последнее замечание: как правило , стандартные методы представляют собой средство специального назначения. Интерфейсы, которые вы создаете , попрежнему будут использоваться в основном для указания того , что делать, а не каким образом это делать. Тем не менее, появление стандартных методов обеспечивает дополнительную гибкость. — Основы стандартных методов Стандартный метод интерфейса определяется аналогично определению метода в классе. Основное отличие связано с тем , что объявление предваряется ключевым словом default . Например , взгляните на следующий простой интерфейс: public interface MylF { // Это объявление ’’нормального" метода интерфейса. // В нем НЕ определяется стандартная реализация , int getUserlD(); // Это стандартный метод. Обратите внимание, // что он предоставляет реализацию по умолчанию , default int getAdminIDO { return 1; } } 328 Java: руководство для начинающих, 9-е издание В интерфейсе МуIF объявлены два метода . Объявление первого, getUserlD(), является объявлением обычного метода интерфейса. Какая-либо реализация в нем отсутствует. Второй метод, getAdminIDO , включает стандартную реа лизацию. В данном случае он просто возвращает значение 1. Обратите особое внимание на способ объявления getAdminID ( ) . Его объявлению предшествует модификатор default. Такой синтаксис можно обобщить. Чтобы определить стандартный метод, перед его объявлением необходимо указать default. Поскольку getAdminID ( ) содержит стандартную реализацию, реализующему классу переопределять ее необязательно. Другими словами, если реализующий класс не предоставляет собственную реализацию, то используется стандартная . Скажем , показанный ниже класс MylFlmp совершенно допустим: // Реализация MylF. class MylFlmp implements MylF { // Необходимо реализовать только метод getUserlDO , определенный в MylF. // Для метода getAdminIDO разрешено применять стандартную реализацию. public int getUserlDO { return 100; } } В следующем коде создается экземпляр класса MylFlmp, который применяется для вызова методов getUserlD( ) и getAdminID(): // Использование стандартного метода , class DefaultMethodDemo { public static void main(String[] args) { MylFlmp obj = new MylFlmp(); // Метод getUserlDO можно вызывать, потому что // о н явно реализован в MylFlmp: System.out.println("Идентификатор пользователя: " + obj.getUserlD()); // Метод getAdminIDO тоже можно вызывать // п о причине наличия стандартной реализации: System.out.println("Идентификатор администратора: " + obj.getAdminID()); } } Вот вывод, генерируемый программой: Идентификатор пользователя: 100 Идентификатор администратора: 1 Как видите , автоматически была использована стандартная реализация ме тода getAdminID ( ) . Определять ее в классе MylFlmp не понадобилось. Таким образом, для метода getAdminID ( ) реализация классом необязательна. ( Конеч но, реализовать его в классе придется , если класс должен возвращать другой идентификатор. ) Для реализующего класса возможно и распространено определение собственной реализации стандартного метода. Например , в классе MylFlmp2 переопределяется метод getAdminID(): Глава 8. Пакеты и интерфейсы 329 class MyIFImp2 implements MylF { // Здесь предоставляются реализации для обоих методов, getUserlDO // и getAdminIDO . public int getUserlDO { return 100; } public int getAdminIDO { return 42; } } Теперь при вызове getAdminlD ( ) возвращается значение , отличающееся от возвращаемого стандартной реализацией . Более реалистичный пример стандартного метода Хотя в рассмотренных выше примерах демонстрировалась механика использования стандартных методов , не была проиллюстрирована их полезность в более реалистичных условиях. Давайте еще раз вернемся к интерфейсу Series, показанному ранее в главе. В целях обсуждения предположим, что Series широко применяется , и на него полагаются многие программы. Также допустим , что по результатам исследования во многих реализациях Series добавляется метод, который возвращает массив, содержащий следующие п элементов последовательности. Учитывая указанное обстоятельство , интерфейс Series решено расширить, включив в него метод подобного рода по имени getNextArray ( ) со следующим объявлением: int[] getNextArray(int n) В n указывается количество извлекаемых элементов. До появления стандартных методов добавление такого метода в интерфейс Series нарушило бы работу уже написанного кода , поскольку оказалось бы, что в имеющихся реализаци ях определение этого метода отсутствует. Однако предоставление стандартной реализации для нового метода позволяет избежать проблем. Давайте выясним , как все работает. В ряде случаев при добавлении в существующий интерфейс стандартного метода его реализация выдает лишь сообщение об ошибке . Такой подход дол жен использоваться в ситуации , когда для стандартного метода невозможно обеспечить реализацию , одинаково пригодную для всех возможных сценариев его применения. Стандартные методы определяют то , что является по существу необязательным кодом . Тем не менее , иногда удается определить стандартный метод, который будет работать в любом случае. Именно такая ситуация с методом getNextArray ( ) . Поскольку интерфейс Series уже требует, чтобы класс реализовывал метод getNext ( ) , стандартная версия getNextArray ( ) может воспользоваться данным фактом. В итоге можно предложить показанный далее способ реализации новой версии Series, которая включает стандартный метод getNextArray(): 330 Java: руководство для начинающих, 9-е издание // Расширенная версия интерфейса Series со стандартным // методом по имени getNextArray(). public interface Series { int getNext(); // возвращает следующее число в последовательности // Возвращает массив, содержащий следующие п элементов // последовательности после текущего элемента , default int[] getNextArray(int n) { int[] vals = new int[n]; for(int i=0; i < n; i++) vals[i] = getNextO ; return vals; } void reset(); void setStart(int x); // сброс // установка начального значения } Обратите особое внимание на то, как реализован метод getNextArray(). Из- за того , что метод getNext ( ) являлся частью первоначальной специфика ции Series, он должен предоставляться любым классом , реализующим интерфейс Series. Следовательно , метод getNextArray( ) может применять его для получения следующих п элементов последовательности. В результате лю бой класс , реализующий расширенную версию Series, сможет использовать метод getNextArray( ) в том виде как есть, без какой -либо необходимости переопределять его. Поэтому работа существующего кода не будет нарушена . Само собой разумеется , при необходимости класс всегда может предоставить собственную реализацию метода getNextArray(). Как было продемонстрировано в предыдущем примере , стандартный метод предоставляет: способ элегантного развития интерфейсов с течением времени без нарушения работы существующего кода ; • # способ обеспечения дополнительной функциональности без требования , чтобы класс предоставлял реализацию заглушки , когда эта функциональность не нужна . В случае метода getNextArray ( ) второй аспект особенно важен. Если реализации Series не требуется функциональность, предлагаемая getNextArray(), то она и не обязана предоставлять собственную реализацию- заглушку. В итоге появляется возможность написания более ясного и чистого кода. Проблемы множественного наследования Как объяснялось ранее в книге , язык Java не поддерживает множественное наследование классов. Теперь, когда интерфейс способен содержать стандартные методы , вам может быть интересно, удастся ли с помощью интерфейсов обойти это ограничение. Ответ: по существу нет. Вспомните , что между классом и интерфейсом все же есть ключевое различие: класс способен поддерживать информацию о состоянии (особенно за счет использования переменных экзем пляра ) , а интерфейс нет. — Глава 8. Пакеты и интерфейсы 331 Несмотря на вышеизложенное , стандартные методы предлагают то , что обычно ассоциируется с концепцией множественного наследования. Скажем , у вас может быть класс, реализующий два интерфейса . Если каждый из таких интерфейсов предоставляет стандартные методы , то некоторое поведение на следуется от обоих. Таким образом , стандартные методы в ограниченной степени поддерживают множественное наследование поведения . Как несложно до гадаться, в подобной ситуации возможен конфликт имен. Например, пусть класс MyClass реализует два интерфейса с именами Alpha и Beta. Что произойдет, если и Alpha, и Beta предоставят метод reset ( ) , для которого оба интерфейса объявят стандартную реализацию? Какая версия reset() будет задействована в классе MyClass из интерфейса Alpha либо из интерфейса Beta? Или рассмотрим ситуацию , в которой интерфейс Beta расширяет Alpha. Какая версия стандартного метода используется? А что , если MyClass предоставляет собственную реализацию метода? Для обработки этих и других похожих ситуаций в Java определен набор правил , разрешающих такие конфликты. Во- первых , во всех случаях реализация класса имеет приоритет над стандартной реализацией интерфейса . Таким образом, если в MyClass переопределяется стандартный метод reset(), то применяется версия из MyClass. Это так, даже если MyClass реализует и Alpha, и Beta. Тогда оба стандартных метода переопределяются реализацией в MyClass. Во-вторых, в случаях, когда класс реализует два интерфейса с одним и тем же стандартным методом , но класс не переопределяет этот метод, возникает ошибка. Продолжая пример , если MyClass реализует и Alpha, и Beta, но не переопределяет reset ( ) , тогда произойдет ошибка. В случаях, когда один интерфейс унаследован от другого , и оба определя ют общий стандартный метод, приоритет имеет версия метода из наследующего интерфейса. Поэтому, продолжая пример, если Beta расширяет Alpha, то будет использоваться версия reset ( ) из Beta. С применением формы super следующего вида в унаследованном интерфей се можно явно ссылаться на стандартную реализацию: — ИмяИнтерфейса.super.имяМетода() Например, если в Beta нужно сослаться на стандартный метод reset ( ) из Alpha, то вот какой оператор можно использовать: Alpha.super.reset(); Использование статических методов в интерфейсе В версии JDK 8 в интерфейсах появилась возможность определения одного или нескольких статических методов. Подобно статическим методам в классе статический метод, определенный в интерфейсе , может вызываться независи мо от любого объекта. Таким образом , для вызова статического метода не требуется реализация интерфейса и экземпляр реализации интерфейса . Взамен 332 Java: руководство для начинающих, 9-е издание статический метод вызывается путем указания имени интерфейса , за которым следует точка и имя метода. Вот общая форма: ИмяИнтерфейса.имяСтатическогоМетода () Обратите внимание на сходство со способом вызова статического метода в классе . Ниже приведен пример статического метода , добавленного в показанный ранее интерфейс МуIF. Статическим методом является getUniversallD(), который возвращает значение 0. public interface MylF { // Это объявление "нормального" метода интерфейса. // В нем НЕ определяется стандартная реализация , int getUserlD(); // Это стандартный метод. Обратите внимание, // что он предоставляет стандартную реализацию , default int getAdminIDO { return 1; } // Это статический метод интерфейса , static int getUniversallD() { return 0; } } Метод getUniversallD ( ) может быть вызван следующим образом: int uID = MylF.getUniversallD(); Как уже упоминалось, для вызова метода getUniversallD ( ) не требуется никакой реализации или экземпляра реализации MylF, потому что он является статическим. И последнее замечание: статические методы интерфейса не наследуются ни реализующим классом , ни производными интерфейсами. Закрытые методы интерфейса Начиная с версии JDK 9, интерфейс способен содержать закрытый метод , который может вызываться только стандартным методом или другим закрытым методом , определенным в том же интерфейсе. Поскольку закрытый метод ин терфейса указан как private, его нельзя использовать в коде вне интерфейса , в котором он определен. Такое ограничение распространяется и на производные интерфейсы , потому что закрытый метод интерфейса ими не наследуется. Основное преимущество закрытого метода интерфейса заключается в том , что он позволяет двум или более стандартным методам использовать общий фрагмент кода , позволяя избегать дублирования кода . Например , ниже показана еще одна версия интерфейса Series, в которую добавлен вто рой стандартный метод по имени skipAndGetNextArray ( ) . Он пропуска ет указанное количество элементов , а затем возвращает массив, содержащий Глава 8 . Пакеты и интерфейсы 333 последующие элементы. Для получения массива элементов указанного размера в skipAndGetNextArray ( ) применяется закрытый метод getElements(). // Дополнительно расширенная версия интерфейса Series, которая содержит // два стандартных метода, использующие закрытый метод getArrayO . public interface Series { int getNextO ; // возвращает следующее число в последовательности // Возвращает массив, содержащий следующие п элементов // последовательности после текущего элемента , default int[] getNextArray(int n) { return getArray(n); } // Возвращает массив, содержащий следующие п элементов // последовательности после пропуска элементов , default int[] skipAndGetNextArray(int skip, int n) { // Пропустить указанное количество элементов. getArray(skip); return getArray(n); } // Закрытый метод, который возвращает массив, // содержащий следующие п элементов , private int[] getArray(int n) { int[] vals = new int[n]; for(int i=0; i < n; i++) vals[i] return vals; = getNextO ; } // сброс void reset(); void setStart(int x); // установка начального значения } Обратите внимание , что для получения возвращаемого массива в методах getNextArray ( ) и skipAndGetNextArray ( ) используется закрытый метод getArray ( ) , что предотвращает дублирование одного и того же фрагмента кода в обоих методах. Имейте в виду, что поскольку метод getArray ( ) является закрытым , его нельзя вызывать в коде за пределами Series. Таким образом , его применение ограничено стандартными методами внутри Series. Хотя закрытый метод интерфейса представляет собой функциональное средство, которое будет востребовано редко, в тех случаях , когда оно вам нужно, вы найдете его весьма полезным. Заключительные соображения по поводу пакетов и интерфейсов Хотя в примерах, включенных в книгу, пакеты или интерфейсы используются не особенно часто, оба эти инструмента являются важной частью программ ной среды Java. Практически все реальные программы, которые придется писать на Java , будут содержаться в пакетах. Некоторые из них, вероятно , также 334 Java: руководство для начинающих, 9- е издание будут реализовывать интерфейсы. Поэтому важно , чтобы вы научились их при менять надлежащим образом . Вопросы и упражнения для самопроверки . 1 Воспользовавшись кодом из упражнения 8.1, поместите интерфейс iCharQ и три его реализации в пакет по имени qpack. Сохранив демон страционный класс очереди iQDemo в стандартном пакете , покажите , ка ким образом импортировать и использовать классы в qpack. 2. Что такое пространство имен? Почему настолько важна возможность разделения пространства имен в Java ? . . 3 Обычно содержимое пакетов хранится в 4. Объясните разницу между защищенным и стандартным доступом. 5. Опишите два способа использования членов пакета в других пакетах. 6. “ Один интерфейс , несколько методов ” является основным принципом Java. Какое средство лучше всего иллюстрирует его? 7. Сколько классов могут реализовывать интерфейс? Сколько интерфейсов может реализовывать класс? 8. Можно ли расширять интерфейсы? 9. Создайте интерфейс для класса Vehicle из главы 7. Назначьте интерфейсу имя IVehicle. 10. Переменные , объявленные в интерфейсе , неявно являются static и final. Могут ли они использоваться в других частях программы? 11 Верно ли утверждение , что пакет по существу представляет собой контей нер для классов? 12 Какой стандартный пакет Java автоматически импортируется в любую программу? 13 Какое ключевое слово используется для объявления стандартного метода интерфейса ? 14 Можно ли определить статический метод в интерфейсе? 15. Предположим , что интерфейс ICharQ, показанный в упражнении 8.1, за несколько лет получил широкое применение. Теперь желательно добавить к нему метод по имени reset ( ) , который будет использоваться для сброса очереди в начальное , пустое состояние. Как этого добиться , не нарушив работу существующего кода ? 16. Каким образом вызвать статический метод в интерфейсе ? 17 Может ли интерфейс иметь закрытый метод? . . . . . . £ V *V .'; ™. */* Л . >*./ >: :: • • / S% “ нч*?ц * SX5; "••и I I, * I I • ' II I,I II, I .' II ч' I I Si к, s *<• Глава 9 Обработка исключений 336 Java: руководство для начинающих, 9- е издание В этой главе • • • z Иерархия исключений z Использование try и catch z Эффекты от необработанных исключений z Применение множества операторов catch z Перехват подклассов исключений z Вложенные блоки try z Генерация исключений z Члены класса Throwable z Использование finally z Применение throws z Встроенные исключения Java z Создание специальных классов исключений в настоящей главе обсуждается обработка исключений. Исключение - это ошибка , которая возникает во время выполнения . С применением подси стемы обработки исключений Java можно структурированным и контролируе мым образом обрабатывать ошибки времени выполнения. Хотя большинство современных языков программирования предлагают ту или иную форму обработки исключений, ее поддержка в Java отличается простотой в использовании и высокой гибкостью. Главное преимущество обработки исключений связано с автоматизированной реакцией на ошибки и отсутствием необходимости в ручном написании соответствующего кода в любой крупной программе. Например, в некоторых более старых языках программирования при возникновении ошибки во время выполнения метода предусматривается возвращение кода , который нужно проверять после каждого вызова метода. Такой подход утомителен и ненадежен. Обработка исключений упрощает обработку ошибок, позволяя программе определять блок кода, называемый обработчиком исключений, который выполняется автоматически в случае возникновения ошибки. В итоге устраняется необходи мость в ручной проверке успеха или неудачи каждой конкретной операции или вызова метода. Если ошибка возникает, то она будет обработана обработчиком исключений. Глава 9. Обработка исключений 337 Еще одна причина важности обработки исключений состоит в том , что в Java определены стандартные исключения для распространенных программных ошибок , таких как деление на ноль или отсутствие запрошенного файла. Для реагирования на ошибки подобного рода программа должна отслеживать и обрабатывать эти исключения. Кроме того , исключения интенсивно задействова ны в библиотеке Java API. В конечном счете , чтобы быть успешным программистом на Java , необходимо уметь ориентироваться в подсистеме обработки исключений Java . Иерархия исключений Все исключения в Java представлены классами , которые являются производными от класса Throwable. Таким образом , когда в программе возникает исключительное состояние , генерируется объект некоторого класса исключения. Существуют два непосредственных подкласса Throwable: Exception и Error. Исключения типа Error связаны с ошибками, которые возникают в самой виртуальной машине Java , а не в прикладной программе. Исключения такого типа находятся за рамками вашего контроля, и программа обычно их не обрабатывает, поэтому здесь они не рассматриваются . Ошибки, возникающие в результате работы программы , представлены подклассами Exception. Например , в эту категорию попадают ошибки, связанные с делением на ноль, выходом за границы массива и взаимодействием с фай лами. Обычно исключения такого типа должны обрабатываться в программе. Важным подклассом Exception является RuntimeException, который приме няется для представления распространенных ошибок времени выполнения. Основы обработки исключений Обработка исключений в Java управляется пятью ключевыми словами: try, catch, throw, throws и finally. Они образуют взаимосвязанную подсистему, в которой использование одного ключевого слова подразумевает применение другого. Все ключевые слова будут подробно рассматриваться далее в главе, но с самого начала полезно получить общее представление о роли, которую каждое из них играет в обработке исключений. Вот как они работают. Операторы программы , которые вы хотите отслеживать на предмет наличия исключений , содержатся в блоке try. Если внутри блока try возникает исключение , тогда оно генерируется. Ваш код может перехватить это исключение ( с помощью catch)и обработать его рациональным образом . Системные исключения автоматически генерируются исполняющей средой Java. Для ручной генерации исключения используйте ключевое слово throw. Любое исключение , генерируемое в методе, должно быть указано как таковое с помощью конструкции throws. Любой код , который обязательно должен быть выполнен после за вершения блока try, помещается в блок finally. 338 Java : руководство для начинающих, 9-е издание !?@>A8< C M:A?5@B0 ВОПРОС. Каковы условия , вызывающие генерацию исключения? ОТВЕТ. Исключения генерируются тремя различными способами . Во - первых , виртуальная машина Java может генерировать исключение в ответ на вну треннюю ошибку, которая находится вне вашего контроля . Обычно в при кладной программе такие типы исключений не обрабатываются . Во- вторых , стандартные исключения вроде тех , что связаны с делением на ноль или выходом индекса за границы массива , генерируются из-за ошибок в про граммном коде . Такие исключения должны обрабатываться . В -третьих , ис ключение можно сгенерировать вручную с применением оператора throw. Независимо от того , каким образом генерируется исключение , обрабатывается оно одинаково . Использование try и catch В основе обработки исключений лежат ключевые слова t r y и catch. Они работают вместе ; catch не может существовать без try. Ниже показана общая форма блоков обработки исключений try / catch: try { // блок кода, где отслеживаются ошибки } catch (ТипИсключения1 объектИсключения ) { // обработчик исключений для ТипИсключения1 } catch ( ТипИсключения2 объектИсключения ) { II обработчик исключений для ТипИсключения2 } Здесь ТипИсключенияХ — это тип возникшей исключительной ситуации . Когда исключение возникает, оно перехватывается соответствующим оператором catch , который затем данное исключение обрабатывает. Как видно в общей форме , с t r y может быть связано несколько операторов catch. Выполняемый оператор catch определяется типом исключения , т. е . если тип исключения , указанный в catch , совпадает с типом возникшего исключения , то именно этот оператор catch и выполняется ( а все остальные игнорируются) . Когда исклю чение перехвачено , его значение получает объектИсключения. Следует отметить один важный момент: если исключение не было сгенери ровано , то блок t r y завершается нормально , а все его операторы catch пропускаются . Выполнение возобновляется с оператора , находящегося после последнего оператора catch. Таким образом , операторы catch выполняются только в случае возникновения исключения . Глава 9. Обработка исключений 339 На заметку! Существует еще одна форма оператора try, которая поддерживает автоматическое управление ресурсами и называется оператором try с ресурсами. Она описана в главе 10 в контексте управления потоками ввода - вывода ( например, подключенными к файлу ), поскольку потоки ввода - вывода являются наиболее часто встречающимися ресурсами. Простой пример обработки исключений Ниже приведен простой пример , иллюстрирующий отслеживание и перехват исключений. Как вам известно , при попытке обращения по индексу в масси ве за его границами возникает ошибка , и машина JVM генерирует исключение ArraylndexOutOfBoundsException. В следующей программе такое исключение намеренно генерируется , после чего перехватывается. // Демонстрация обработки исключений. class ExcDemol { public static void main(String[] args) { int[] nums = new int[4]; Создание блока try try ( System.out.println("Перед генерацией исключения."); // Сгенерировать исключение выхода индекса за границы массива , nums[7] = 10; < Попытка обращения по индексу за границами nums System.out.println ("Это отображаться не будет"); } catch (ArraylndexOutOfBoundsException exc) { // Перехватить исключение. Перехват ошибок выхода индекса за границы массива System.out.println ("Индекс вышел за границы массива!"); } System.out.println("После оператора catch."); } } Вот вывод, отображаемый в результате выполнения программы: Перед генерацией исключения. Индекс вышел за границы массива! После оператора catch. Несмотря на то что предыдущая программа довольно короткая , в ней иллюстрируется несколько основных моментов , касающихся обработки исключений . Во - первых, код, который необходимо отслеживать на предмет возникновения ошибок, содержится в блоке try. Во- вторых, когда возникает исключение ( в данном случае из- за попытки индексации массива nums за его границами ) , исключение генерируется в блоке try и перехватывается оператором catch. В этот момент выполнение начинается с оператора catch , а блок try заканчивается. Явного обращения к catch не происходит, но ему передается управление. В результате оператор println ( ) , который находится после доступа к 340 Java : руководство для начинающих, 9-е издание массиву по индексу, выходящему за границы , никогда не выполнится . После оператора catch выполнение программой продолжается операторами , следую щими за catch. Таким образом , задачей обработчика исключений будет устра нение проблемы , вызвавшей исключение , чтобы выполнение программы могло продолжаться нормально . Помните , что если блок try не генерирует исключений , то никакие опера торы catch выполняться не будут, и выполнение возобновится после оператора catch. Чтобы удостовериться в этом , измените в предыдущей программе строку nums[7] = 10; на nums[0] = 10; Теперь исключение не генерируется и блок catch не выполняется. Важно понимать , что весь код внутри блока t r y отслеживается на предмет наличия исключений . Сюда входят исключения , которые могут быть сгенери рованы методом , вызываемым из блока t r y. Исключение , которое генерирует метод , вызываемый из блока t r y, может быть перехвачено операторами catch , ассоциированными с этим блоком try , разумеется , при условии , что исклю чение не было перехвачено самим методом. Например , следующая программа вполне допустима: /* Исключение может быть сгенерировано одним методом и перехвачено другим.*/ class ExcTest { // Генерирует исключение , static void genException() { int[] nums = new int[4]; System.out.println ("Перед генерацией исключения."); // Сгенерировать исключение выхода индекса за границы массива. Здесь исключение генерируется nums[7] = 10; System.out.println("Это отображаться не будет"); } } class ExcDemo2 { public static void main(String[] args) { try { ExcTest.genException(); } Здесь исключение перехватывается catch (ArraylndexOutOfBoundsException exc) { // Перехватить исключение. System.out.println ("Индекс вышел за границы массива!"); ^ } System.out.println("После оператора catch."); } } Программа выдает тот же вывод, что и ее предыдущая версия: Глава 9. Обработка исключений 341 Перед генерацией исключения. Индекс вышел за границы массива! После оператора catch. Поскольку метод genException ( ) вызывается из блока try, исключение , которое он генерирует ( и не перехватывает) , перехватывается оператором catch в main ( ) . Однако имейте в виду, что если бы метод genException ( ) самостоятельно перехватил исключение, то оно никогда бы ни было передано обратно в main(). Последствия от неперехваченных исключений Перехват одного из стандартных исключений Java , как делалось в предыду щей программе , обладает дополнительным преимуществом: он предотвращает аварийное завершение программы. Когда возникает исключение , оно должно быть перехвачено каким-то фрагментом кода. В общем случае , если программа не перехватит исключение , тогда оно будет перехвачено машиной JVM. Проблема в том , что стандартный обработчик исключений JVM прекращает вы полнение и отображает трассировку стека и сообщение об ошибке . Например , в следующей версии предыдущего примера исключение, связанное с выходом индекса за границы массива , программа не перехватывает. // Позволить машине JVM обработать ошибку , class NotHandled { public static void main(String[] args) { int[] nums = new int[4]; System.out.println("Перед генерацией исключения."); // Сгенерировать исключение выхода индекса за границы массива , nums[7] = 10; } } При возникновении ошибки , связанной с выходом индекса за границы массива , выполнение останавливается и отображается показанное ниже сообщение об ошибке. (Точный вывод может отличаться из- за различий между JDK.) Exception in thread "main" java.lang.ArraylndexOutOfBoundsException: Index 7 out of bounds for length 4 at NotHandled.main( NotHandled.java:9) Исключение в потоке main java.lang.ArraylndexOutOfBoundsException: Индекс 7 вышел за допустимые границы для длины 4 в NotHandled.main( NotHandled.java:9) Хотя такое сообщение полезно во время отладки, вы вряд ли захотите , чтобы его видели другие! Вот почему важно , чтобы ваша программа самостоятельно обрабатывала исключения, а не полагалась на машину JVM. Как упоминалось ранее , тип исключения должен соответствовать типу, указанному в операторе catch, иначе исключение не будет перехвачено. Например, в приведенной далее программе предпринимается попытка перехвата ошибки , 342 Java: руководство для начинающих, 9- е издание связанной с выходом индекса за границы массива, с помощью оператора catch для исключения ArithmeticException (еще одно встроенное исключение Java ). Когда происходит выход индекса за границы массива , генерируется исключение ArraylndexOutOfBoundsException, но оно не перехватывается оператором catch, что приводит к аварийному завершению программы. // Программа работать не будет! class ExcTypeMismatch { public static void main(String[] args) { int[] nums = new int[4]; Здесь генерируется исключение ArraylndexOutBoundsException try { System.out.println("Перед генерацией исключения."); // Сгенерировать исключение выхода индекса за границы массива. « nums[7] = 10; * System.out.println("Это отображаться не будет"); } /* Перехватить ошибку выхода индекса за границы массива с помощью ArithmeticException невозможно. */ Здесь предпринимается catch (ArithmeticException exc) { попытка перехвата исключения ArithmeticException // Перехватить исключение. System.out.println("Индекс вышел за границы массива!"); } System.out.println("После оператора catch."); } } Вот вывод программы ( который может отличаться в зависимости от версии JDK ): Перед генерацией исключения. Exception in thread "main" java.lang.ArraylndexOutOfBoundsException: Index 7 out of bounds for length 4 at ExcTypeMismatch.main(ExcTypeMismatch.java : 10) В выводе видно, что перехват ArithmeticException не приводит к перехвату ArraylndexOutOfBoundsException. Исключения позволяют изящно обрабатывать ошибки Одно из основных преимуществ обработки исключений связано с тем , что она позволяет программе отреагировать на ошибку и затем продолжить работу. Рассмотрим следующий пример , где элементы одного массива делятся на элементы другого. Если происходит деление на ноль, тогда генерируется исключе ние ArithmeticException, которое в программе обрабатывается путем выдачи сообщения об ошибке и последующего продолжения работы. Таким образом , попытка деления на ноль не вызывает непредвиденную ошибку времени вы полнения, приводящую к завершению программы. Взамен исключение изящно обрабатывается , позволяя продолжить выполнение программы. Глава 9. Обработка исключений 343 // Изящно обработать ошибку и продолжить работу , class ExcDemo3 { } public static void main(String[] args) { int[] numer = { 4 , 8, 16, 32, 64 , 128 }; int[] denum = { 2 , 0, 4 , 4 , 0, 8 }; for(int i=0; i< numer.length; i++) { try { System.out.println(numer[i] + " / " + denom[i] + " равно " + numer[i]/denom[i]); } catch (ArithmeticException exc) { // Перехватить исключение. System.out.println("Деление на ноль невозможно!"); } } } Ниже показан вывод, полученный в результате запуска программы: 4 / 2 равно 2 Деление на ноль невозможно! 1 6 / 4 равно 4 3 2 / 4 равно 8 Деление на ноль невозможно! 128 / 8 равно 16 Приведенный пример демонстрирует еще один важный момент: обработанное исключение удаляется из системы . По этой причине на каждой итерации цикла блок try выполняется заново , а все возникшие ранее исключения счита ются обработанными. В итоге появляется возможность обработки в программе повторяющихся ошибок. Использование нескольких операторов catch Как утверждалось ранее , с одним оператором try можно связать более одного оператора catch. В действительности это распространенное явление. Тем не менее, каждый оператор catch должен перехватывать отдельный тип исключения. Например, в представленной далее программе перехватываются ошибки выхода индекса за границы массива и деления на ноль: // Использование нескольких операторов catch. class ExcDemo4 { public static void main(String[] args) { // Здесь массив numer длиннее массива denom. int[] numer = { 4 , 3 , 16, 32, 64, 128, 256, 512 }; int[] denom = { 2, 0, 4, 4, 0, 8 }; for(int i=0; i<numer.length; i++) { try { System ,out.println(numer[i] + " / " + denom[i] + " равно " + numer[i]/denom[i]); } 344 Java: руководство для начинающих, 9- е издание catch (ArithmeticException exc) { Несколько операторов catch // Перехватить исключение. System.out.println("Деление на ноль невозможно!"); } catch (ArraylndexOutOfBoundsException exc) { •+ // Перехватить исключение. System.out.println("Соответствующий элемент не найден."); } } } } Вот вывод программы: 4 / 2 равно 2 Деление на ноль невозможно! 1 6 / 4 равно 4 3 2 / 4 равно 8 Деление на ноль невозможно! 128 / 8 равно 16 Соответствующий элемент не найден. Соответствующий элемент не найден. В выводе видно , что каждый оператор catch реагирует только на собствен ный тип исключения. Выражения catch проверяются в том порядке , в котором они встречаются в программе. Выполняется только соответствующий оператор, а все остальные блоки catch игнорируются. Перехват подклассов исключений Существует один важный момент, касающийся нескольких операторов catch, который имеет отношение к подклассам. Конструкция catch для суперкласса также будет соответствовать любому из его подклассов. Например, поскольку суперклассом для всех исключений является Throwable, для перехвата всех возможных исключений понадобится перехватить Throwable. Если необходимо перехватывать исключения и типа суперкласса , и типа подкласса , тогда в последовательности операторов catch подкласс должен быть указан первым. В противном случае оператор catch для суперкласса также перехватит исключения всех производных классов. Это правило соблюдается автоматически , т.к. размещение суперкласса первым приводит к созданию недостижимого кода , потому что оператор catch для подкласса никогда не выполнится. Недостижи мый код в Java трактуется как ошибка. Взгляните на следующую программу: // В последовательности операторов catch подклассы // предшествовать суперклассам , class ExcDemo5 { public static void main(String[] args) { // Здесь массив numer длиннее массива denom. должны Глава 9. Обработка исключений 345 int[] numer = { 4, 8, 16, 32, 64, 128, 256, 512 }; int[] denom = { 2, 0, 4, 4, 0, 8 }; for(int i=0; icnumer.length; i++) { try ( System.out.println(numer[i] + " / " + denom[i] + " равно " + numer[i]/denom[i]); } catch (ArraylndexOutOfBoundsException exc) { Перехват подкласса // Перехватить исключение. System.out.println("Соответствующий элемент не найден."); } catch (Throwable exc) { < Перехват суперкласса System.out.println("Возникло какое-то исключение."); } } } } Вот вывод, генерируемый программой: 4 / 2 равно 2 Возникло какое-то исключение. 1 6 / 4 равно 4 3 2 / 4 равно 8 Возникло какое-то исключение. 128 / 8 равно 16 Соответствующий элемент не найден. Соответствующий элемент не найден. В данном случае catch(Throwable) перехватывает все исключения кроме ArraylndexOutOfBounds-Exception. Проблема перехвата подклассов исключений становится более важной при создании собственных исключений . !?@>A8< C M:A?5@B0 ВОПРОС. Зачем может понадобиться перехватывать суперклассы исключений? ОТВЕТ. Конечно, причины есть самые разные. Рассмотрим две из них. Вопервых, добавив конструкцию catch, которая перехватывает исключения типа Exception, вы фактически организуете в своем обработчике исключений универсальный механизм перехвата , который обработает все исключе ния , связанные с программой. Такой механизм может оказаться полезным в ситуации , когда необходимо избегать аварийного завершения программы , что бы ни случилось. Во-вторых , в некоторых ситуациях с помощью одной конструкции может обрабатываться целая категория исключений. Перехват суперкласса этих исключений позволит обработать все без дублирования кода. 346 Java : руководство для начинающих, 9- е издание Блоки try могут быть вложенными Один блок try может быть вложенным в другой . Исключение , сгенериро ванное во внутреннем блоке try, которое не было перехвачено оператором catch, ассоциированным с этим try, распространяется на внешний блок try. В следующем примере исключение ArraylndexOutOfBoundsException пере хватывается не внутренним , а внешним оператором catch: // Использование вложенного блока try. class NestTrys { public static void main(String[] args) { // Здесь массив numer длиннее массива denom. int[] numer = { 4 , 8, 16, 32, 64, 128, 256, 512 }; int[] denom = { 2, 0, 4, 4, 0, 8 }; try { // Внешний блок try. Вложенные блоки try for(int i=0; i<numer.length; i++) { try { // Вложенный блок try. System.out.println(numer[i] + " / " + denom[i] + " равно " + numer[i]/denom[i]); } catch (ArithmeticException exc) { // Перехватить исключение. System.out.println("Деление на ноль невозможно!"); } } } catch (ArraylndexOutOfBoundsException exc) { // Перехватить исключение. System.out.println ("Соответствующий элемент не найден."); System.out.println("Неисправимая ошибка - программа завершена."); } } } Ниже показан вывод, полученный в результате запуска программы: 4 / 2 равно 2 Деление на ноль невозможно! 1 6 / 4 равно 4 3 2 / 4 равно 8 Деление на ноль невозможно! 128 / 8 равно 16 Соответствующий элемент не найден. Неисправимая ошибка программа завершена. - В приведенном примере исключение , которое может быть обработано вну тренним блоком try ( в данном случае ошибкой деления на ноль ) , позволяет Глава 9. Обработка исключений 347 продолжить работу программы. Однако внешний блок try перехватывает ошибку выхода индекса за границы массива , что приводит к завершению программы. Хотя это не единственная причина применения вложенных блоков try , в предыдущей программе продемонстрирован важный момент, который можно обобщить. Часто вложенные блоки try используются для того , чтобы разные категории ошибок обрабатывались по- разному. Некоторые виды ошибок явля ются катастрофическими и не могут быть исправлены. Некоторые из них не значительны и могут быть обработаны немедленно. Внешний блок try можно применять для перехвата наиболее серьезных ошибок, а во внутренних блоках try обрабатывать менее серьезные ошибки. Генерация исключений В предшествующих примерах перехватывались исключения , автоматически генерируемые машиной JVM . Тем не менее, исключение можно сгенерировать вручную с помощью оператора throw. Вот его общий вид: throw объектИсключвния; Здесь объектИсключения должен быть объектом класса исключения , производного от Throwable . Ниже показан пример , в котором иллюстрируется использование оператора throw за счет ручной генерации исключения ArithmeticException: // Ручная генерация исключения. class ThrowDemo { public static void main(String[] args) { try { System.out.println ("Перед оператором throw."); throw new ArithmeticException(); Генерация исключения } catch (ArithmeticException exc) { // Перехватить исключение. System.out.println("Исключение перехвачено."); } System.out.println("После блока try/catch."); } } Вывод программы выглядит следующим образом: Перед оператором throw. Исключение перехвачено. После блока try/catch. Обратите внимание , что объект ArithmeticException был создан с применением операции new в операторе throw. Не забывайте, что throw генерирует исключение в виде объекта. Таким образом , для генерации исключения потребуется создать объект его типа. Просто указывать тип в throw нельзя. 348 Java : руководство для начинающих, 9-е издание !?@>A8< C M:A?5@B0 ВОПРОС. Зачем может понадобиться генерировать исключение вручную? ОТВЕТ. Чаще всего генерируемые исключения будут экземплярами созданных вами классов исключений . Как будет показано далее в главе , создание соб ственных классов исключений позволяет обрабатывать ошибки в коде как часть общей стратегии обработки исключений , принятой в программе . Повторная генерация исключений Исключение , перехваченное одним оператором catch , может быть сгенери ровано повторно , чтобы позволить перехватить его внешним оператором catch. Наиболее вероятной причиной повторной генерации таким способом является предоставление доступа к исключению нескольким обработчикам . Например , возможно , один обработчик исключений управляет одним аспектом исключения , а второй обработчик — другим аспектом. Вспомните , что в случае повтор ной генерации исключение не будет перехвачено тем же оператором catch , а передано следующему оператору catch. В приведенной далее программе демон стрируется повторная генерация исключения: // Повторная генерация исключения. class Rethrow { public static void genException() { // Здесь массив numer длиннее массива denom. int[] numer = { 4, 8, 16, 32, 64, 128, 256, 512 }; int[] denom = { 2, 0, 4 , 4, 0, 8 }; for(int i=0; i<numer.length; i++) { try { System.out.println(numer[i] + " / " + denom[i] + " равно " + numer[i]/denom[i]); } catch (ArithmeticException exc) { // Перехватить исключение. System.out . println("Деление на ноль невозможно!"); } catch (ArraylndexOutOfBoundsException exc) { // Перехватить исключение. System.out . println("Соответствующий элемент не найден."); throw exc; // повторно сгенерировать исключение } } } t Повторная генерация исключения } class RethrowDemo { public static void main(String[] args) { try { Rethrow.genException(); } Глава 9. Обработка исключений 349 catch(ArraylndexOutOfBoundsException exc) { + Перехват повторно сгенерированного исключения // Повторно перехватить исключение. System.out.println("Неисправимая ошибка - программа завершена."); * } } } В этой программе ошибки деления на ноль обрабатываются локально в genException ( ) , но ошибка выхода индекса за границы массива генерируется повторно и в таком случае перехватывается в main(). Подробный анализ класса Throwable Вплоть до этого момента исключения перехватывались, но с самими объектами исключений ничего не делалось. Как было продемонстрировано во всех предшествующих примерах , в конструкции catch указывается тип исключения и параметр. Параметр получает объект исключения. Поскольку все исключения являются подклассами Throwable, все исключения поддерживают методы , определенные в классе Throwable. Несколько наиболее часто используемых методов Throwable кратко описаны в табл . 9.1. Таблица 9.1 . Часто используемые методы, определенные в классе Throwable Метод Описание Throwable filllnStackTrace() Возвращает объект Throwable, который содержит полную трассировку стека. Этот объект может быть сгенерирован повторно String getLocalizedMessage() Возвращает локализованное описание исключения String getMessageO void printStackTrace() void printStackTrace(PrintStream stream) Возвращает описание исключения Отображает трассировку стека Посылает трассировку стека в void printStackTrace(PrintWriter stream) указанный поток Посылает трассировку стека в указанный поток String toString() Возвращает объект String, содержащий описание исключения. Вызывается оператором println ( ) при выводе объекта Throwable Двумя наиболее интересными методами, определенными в классе Throwable, являются printStackTrace ( ) и toString ( ) . Вызвав printStackTrace ( ) , можно отобразить стандартное сообщение об ошибке , а также запись о вызовах методов, которые привели к исключению. Метод toString ( ) можно применять 350 Java : руководство для начинающих, 9- е издание для получения стандартного сообщения об ошибке. Метод toStringO также вызывается, когда исключение используется в качестве аргумента println ( ) . Применение обоих методов иллюстрируется в следующей программе: // Использование методов класса Throwable. class ExcTest { static void genException( ) { int[] nums = new int[4]; System.out.println("Перед генерацией исключения."); // Сгенерировать исключение выхода индекса за границы nums[7] = 10; System.out.println ("Это отображаться не будет"); массива , } } class UseThrowableMethods { public static void main(String[] args) { try { ExcTest.genException(); } catch (ArraylndexOutOfBoundsException exc) { // Перехватить исключение. System.out.println("Стандартное сообщение: "); System.out.println(exc); System.out.println(" ХпТрассировка стека: "); exc.printStackTrace(); } System.out.println("После оператора catch."); } } Ниже показан вывод, генерируемый программой (он может отличаться в за висимости от версии JDK ): Перед генерацией исключения. Стандартное сообщение: java.lang.ArraylndexOutOfBoundsException: Index 7 out of bounds for length 4 Трассировка стека: java.lang.ArraylndexOutOfBoundsException: Index 7 out of bounds for length 4 at ExcTest.genException( UseThrowableMethods.java:10) at UseThrowableMethods.main(UseThrowableMethods.java:19) После оператора catch. Использование finally Иногда необходимо определить блок кода, который будет выполняться при покидании блока try / catch. Скажем, исключение может стать причиной ошибки, которая завершает текущий метод, приводя к преждевременному возврату из него. Однако в методе мог быть открыт файл или сетевое подключение, которое Глава 9. Обработка исключений 351 нужно закрыть. Обстоятельства подобного рода распространены в программи ровании , и в Java предоставляется удобный способ справиться с ними: finally. Чтобы указать блок кода , который будет выполняться при выходе из блока try/catch, понадобится поместить блок finally в конец последовательности try/catch. Вот как выглядит общая форма try/catch, включающая finally: try { // блок кода, отслеживаемый на предмет возникновения ошибок } catch (ТипИсключения1 объектИсключения ) { // обработчик исключения ТипИсключения1 } catch ( ТипИсключения2 объектИсключения ) { // обработчик исключения ТипИсключения2 } //... finally { // код finally } Блок finally будет выполняться всякий раз , когда поток управления поки дает блок try/catch, независимо от того , какие условия стали причиной этого . Другими словами , завершается блок try нормально либо по причине исключения , в finally определяется код, выполняемый последним . Блок finally так же выполнится , если в каком - то коде внутри блока try или любого его оператора catch производится возврат из метода . Ниже приведен пример блока finally: // Использование finally. class UseFinally { public static void genException(int what) { int t; int[] nums = new int[2]; System.out.println("Получено: " + what); try { switch(what ) { case 0: t = 10 / what; // генерация ошибки деления на ноль break; case 1: nums[4] = 4; // генерация ошибки выхода индекса за границы массива break; case 2: // возвращение из блока try return; } } catch (ArithmeticException exc) { // Перехватить исключение. System.out.println("Деление на ноль невозможно! "); return; // возвращение из catch } 352 Java: руководство для начинающих, 9-е издание catch (ArraylndexOutOfBoundsException exc) { // Перехватить исключение. System.out.println("Соответствующий элемент не найден."); } finally { System.out.println("Выход из try."); Выполняется при выходе из блоков try/catch } } } class FinallyDemo { public static void main(String[] args) { for(int i=0; i < 3; i++) { UseFinally.genException(i); System.out.println(); } } } Вывод, получаемый в результате запуска программы , выглядит так: Получено: О Деление на ноль невозможно! Выход из try. Получено: 1 Соответствующий элемент не найден. Выход из try. Получено: 2 Выход из try. В выводе видно, что блок finally выполняется независимо от того, как происходит выход из блока try. Использование throws В определенных случаях, если метод генерирует исключение, которое не обрабатывает, тогда он должен объявить это исключение в конструкции throws. Вот общая форма объявления метода , которая содержит конструкцию throws: возвращаемый-тип имя-метода [ список-параметров ) throws список-исключений { } // тело метода Здесь список -исключений представляет собой список разделяемых запятыми классов исключений, которые метод может сгенерировать. Вероятно , вам интересно , почему не нужно было указывать конструкцию throws в ряде предыдущих примеров, где генерировались исключения за пределами методов. Дело в том , что исключения, которые являются подклассами Error или RuntimeException, нет необходимости указывать в списке throws. В таком случае просто предполагается , что метод способен их генерировать. Все Глава 9. Обработка исключений 353 остальные типы исключений должны быть объявлены, иначе возникнет ошибка на этапе компиляции . На самом деле вы уже видели пример применения throws ранее в книге. Вспомните , что в случае выполнения ввода с клавиатуры в метод main( ) нужно было добавлять следующую конструкцию: throws java.io.IOException Теперь вы можете понять причину. Оператор ввода был способен сгенерировать исключение IOException, которое в то время вы не умели обрабатывать. В итоге такое исключение сгенерировалось бы в main ( ) и должно было быть указано как таковое. Теперь, зная об исключениях, вы можете легко обработать IOException. Давайте рассмотрим пример обработки исключения IOException. В методе по имени prompt( ) отображается приглашение на ввод и затем читается сим вол с клавиатуры. Поскольку выполняется ввод, может возникнуть исключение IOException. Тем не менее , сам метод prompt ( ) не обрабатывает IOException. Взамен он использует конструкцию throws, указывая на то , что обработать это исключение обязан вызывающий метод. В данном примере вызывающим мето дом является main ( ) , который и обрабатывает ошибку. // Использование throws , class ThrowsDemo { public static char prompt(String str) throws java.io.IOException { **- Обратите внимание на конструкцию throws System.out.print(str + ": "); return (char) System.in.read(); } public static void main(String[] args) { char ch; try { ch = prompt("Введите букву"); } < • Поскольку метод prompt ( ) может сгенерировать исключение, вызов должен быть помещен внутрь блока try catch(java.io.IOException exc) { System.out.println("Возникло исключение ввода-вывода."); ch = 'X'; } System.out.println("Вы нажали клавишу " + ch ); } } Вдобавок обратите внимание , что класс IOException полностью уточнен именем пакета java.io. В главе 10 вы узнаете, что система ввода- вывода Java содержится в пакете java.io. Таким образом , в этом пакете также находится класс IOException. Кроме того, можно было бы импортировать пакет java.io, после чего напрямую ссылаться на IOException. 354 Java: руководство для начинающих, 9-е издание Три дополнительных средства в системе исключений В дополнение к средствам обработки исключений , которые уже обсужда лись, современные версии Java содержат три дополнительных средства . Первое средство поддерживает автоматическое управление ресурсами , автоматизирующее процесс освобождения ресурса , такого как файл , когда он больше не ну жен. Оно основано на расширенной форме оператора try под названием try с ресурсами и описано в главе 10, где обсуждаются файлы. Второе средство называется множественным перехватом, а третье иногда упоминается как финальная повторная генерация или более точная повторная генерация. Последние два средства описаны ниже. Средство множественного перехвата позволяет перехватывать два или более исключений одной и той же конструкцией catch. Как вы узнали ранее, за оператором try могут ( и обычно будут) следовать две или большее число конструкций catch. Хотя каждая конструкция catch часто имеет собственную уникальную кодовую последовательность, нередко две или более конструкций catch применяют ту же самую кодовую последовательность, даже если реагируют на разные исключения . Вместо перехвата каждого типа исключения по отдельности можно использовать одну конструкцию catch для обработки всех исключений , не дублируя код. Для применения множественного перехвата необходимо указать список исключений внутри одиночной конструкции catch, отделяя типы исключений в списке с помощью операции “ ИЛИ ” . Каждый параметр множественного пере хвата неявно является final. ( При желании final можно указывать явно , но это не обязательно.) Поскольку каждый параметр множественного перехвата неявно является final, ему нельзя присваивать новое значение. Вот как можно применить средство множественного перехвата для исключений ArithmeticException и ArraylndexOutOfBoundsException с использованием одной конструкции catch: catch(ArithmeticException ArraylndexOutOfBoundsException e) { Ниже приведена простая программа , демонстрирующая применение такого множественного перехвата: // Использование средства множественного перехвата. // Примечание: для компиляции этого кода требуется JDK 7 или новее. class MultiCatch { public static void main(String[] args) { int a=88, b=0; int result; char[] chrs = { 'A', B', '0' }; for(int i=0; i < 2; i++) { try { if(i == 0) result = a / b; // генерируется ArithmeticException Глава 9. Обработка исключений else chrs[5] = 1 X'; 355 // генерируется ArraylndexOutOfBoundsException // Следующая конструкция catch перехватывает оба исключения. } catch(ArithmeticException | ArraylndexOutOfBoundsException e) { System.out.println("Перехвачено исключение: " + e); } } System.out.println("После множественного перехвата."); } } Программа сгенерирует исключение ArithmeticException при попытке деления на ноль. Она сгенерирует исключение ArraylndexOutOfBoundsException при попытке доступа по индексу за границами массива chrs. Оба исключения перехватываются одиночным оператором catch. Средство более точной повторной генерации ограничивает тип исключений , которые могут повторно генерироваться, только теми проверяемыми исключе ниями , которые выдает связанный блок try, которые не обрабатываются предыдущей конструкцией catch и которые являются подтипом или супертипом параметра. Хотя такая возможность может требоваться нечасто, теперь она доступна для использования. Чтобы задействовать средство более точной повторной генерации , параметр catch обязан быть либо фактически final, т.е . ему не должно присваиваться новое значение внутри блока catch, либо явным обра зом объявляться как final, но это не обязательно. Встроенные исключения Java Внутри стандартного пакета java. lang определено несколько клас сов исключений Java. Некоторые из них использовались в предшествую щих примерах. Наиболее общие из них являются подклассами стандартно го типа RuntimeException. Из- за того , что пакет java.lang неявно импортируется во все программы на Java , многие исключения , производные от RuntimeException, доступны автоматически. Более того , такие исключения не нужно включать в список throws любого метода. На языке Java они называются непроверяемыми исключениями , потому что компилятор не проверяет, обрабаты вает метод подобные исключения или же генерирует их. Непроверяемые исключения , определенные в java.lang, кратко описаны в табл . 9.2. В табл . 9.3 перечислены те исключения, определенные в java.lang, которые должны помещаться в список throws метода , если метод может генерировать одно из исключений и не обрабатывает его самостоятельно. Они называются проверяемыми исключениями. Помимо исключений из java.lang в Java определено еще несколько, которые относятся к другим стандартным пакетам, наподобие упомянутого ранее исключения IOException. 356 Java: руководство для начинающих, 9- е издание Таблица 9.2 . Непроверяемые исключения, определенные в java.lang Описание Исключение Арифметическая ошибка, такая как деление ArithmeticException на ноль Выход за допустимые пределы индекса ArraylndexOutOfBoundsException в массиве ArrayStoreException Присваивание элементу массива значения несовместимого типа ClassCastException EnumConstantNotPresentException Недопустимое приведение Попытка использования неопределенного значения перечисления Использование недопустимого аргумента при вызове метода Метод не может быть законно выполнен IllegalArgumentException IllegalCallerException вызывающим кодом Недопустимая операция монитора, такая как ожидание неблокированного потока Некорректное состояние среды или приложения Несовместимость запрошенной операции IllegalMonitorStateException IllegalStateException IllegalThreadStateException с текущим состоянием потока Выход за допустимые пределы индекса некоторого вида Невозможность создания уровня модуля IndexOutOfBoundsException LayerInstantiationException NegativeArraySizeException NullPointerException Создание массива с отрицательным размером NumberFormatException SecurityException StringlndexOutOfBoundsException TypeNotPresentException UnsupportedOperationException Недопустимое использование ссылки null Недопустимое преобразование строки в числовой формат Попытка нарушения безопасности Попытка индексации за границами строки Тип не найден Обнаружение неподдерживаемой операции Таблица 9.3. Проверяемые исключения, определенные в java.lang Исключение Описание ClassNotFoundException Класс не найден CloneNotSupportedException Попытка клонирования объекта, который не реализует интерфейс Cloneable IllegalAccessException InstantiationException Доступ к классу запрещен Попытка создания объекта абстрактного класса или интерфейса Глава 9. Обработка исключений 357 Окончание табл. 9.3 Исключение Описание InterruptedException NoSuchFieldException NoSuchMethodException ReflectiveOperationException Один поток был прерван другим потоком Запрошенное поле не существует Запрошенный метод не существует Суперкласс исключений, связанных с рефлексией !?@>A8< C M:A?5@B0 ВОПРОС. Говорят, что в Java поддерживаются так называемые сцепленные исключения. Что они собой представляют? ОТВЕТ. Сцепленные исключения появились в Java несколько лет назад. Они позволяют ассоциировать с одним исключением другое исключение , которое описывает причину первого исключения. Например, представьте ситуацию, в которой метод выдает исключение ArithmeticException из-за попытки деления на ноль. Тем не менее , фактической причиной проблемы было возникновение ошибки ввода- вывода , из-за которой делитель установился неправильно. Хотя метод, безусловно, должен генерировать исключение ArithmeticException, т.к. произошла именно указанная ошибка , вы може те сообщить вызывающему коду, что основной причиной была ошибка ввода - вывода. Сцепленные исключения позволяют справиться с этой и любой другой ситуацией , в которой существуют уровни исключений. Чтобы сделать возможными сцепленные исключения, в класс Throwable были добавлены два конструктора и два метода. Конструкторы приведены ниже: Throwable(Throwable causeExc) Throwable(String msg, Throwable causeExc) В первой форме causeExc является исключением , которое привело к возникновению текущего исключения , т.е . представляет собой основную при чину его возникновения . Вторая форма позволяет указать описание одновременно с указанием причины исключения . Эти два конструктора также были добавлены в классы Error, Exception и RuntimeException. Класс Throwable поддерживает методы для сцепленных исключений getCause() и initCause(): Throwable getCause() Throwable initCause(Throwable causeExc ) Метод getCause ( ) возвращает исключение , лежащее в основе текущего исключения. Если лежащего в основе исключения нет, тогда возвращается null. Метод initCause ( ) связывает causeExc с вызывающим исключением и возвращает ссылку на исключение. Таким образом , вы можете ассоциировать причину с исключением после его создания. Однако исключение-при чина может быть установлено только один раз, т.е. вызывать initCause() 358 Java: руководство для начинающих, 9-е издание для каждого объекта исключения допускается только однажды. Кроме того , если исключение-причина было установлено конструктором , то вы не можете установить его снова с помощью initCause ( ) . В общем , метод initCauseO используется с целью установки причины для устаревших классов исключений , которые не поддерживают два дополнительных кон структора , описанных ранее. Сцепленные исключения - это не то, что нужно каждой программе . Тем не менее , в тех случаях , когда полезно знать основную причину, они предлагают элегантное решение . Создание подклассов Exception Хотя встроенные исключения Java обрабатывают наиболее распространенные ошибки , вполне вероятно, что вы захотите создать собственные типы исключений , которые подходят для ситуаций , специфичных для ваших при ложений. Делается это довольно легко: нужно просто определить подкласс Exception ( который , конечно же , является подклассом Throwable). Вашим подклассам фактически ничего не придется реализовывать - одно их существование в системе типов позволяет использовать их как исключения . В самом классе Exception никаких методов не определено. Разумеется , он наследует методы , предоставляемые Throwable. Таким образом , все исключения , в том числе созданные вами , имеют доступные для них методы , которые определены в классе Throwable. Вы также можете переопределить один или несколько из этих методов в создаваемых классах исключений. В следующем примере создается исключение по имени NonlntResult Exception, которое генерируется , когда результат деления двух целочисленных значений дает результат с дробной частью. Класс NonlntResultException имеет два поля, содержащие целочисленные значения, конструктор и переопределенный метод toStringO , позволяющий отображать описание исключения с помощью printIn(). // Использование специального исключения. // Создать класс исключения. class NonlntResultException extends Exception { int n; int d; NonlntResultException(int i, int j) { n = i; d = j; } public String toStringO { return "Результат " + n + " / " + d + " не является целочисленным."; } } Глава 9. Обработка исключений 359 class CustomExceptDemo { public static void main(String[] args) { // Массив numer содержит несколько нечетных значений , int[] numer = { 4, 8, 15, 32, 64, 127, 256, 512 }; int[] denom = { 2, 0, 4, 4, 0, 8 }; for(int i=0; i< numer.length; i ++ ) { try { if((numer[i]%2) != 0) throw new NonlntResultException(numer[i], denom[i]); System.out.println(numer[i] + " / " + denom[i] + " равно " + numer[i]/denom[i]); } catch (ArithmeticException exc) { // Перехватить исключение. System.out.println("Деление на ноль невозможно!"); } catch (ArraylndexOutOfBoundsException exc) { // Перехватить исключение. System.out.println("Соответствующий элемент не найден."); } catch ( NonlntResultException exc) { System.out.println(exc); } } } } Вот вывод, получаемый в результате запуска программы: 4 / 2 равно 2 Деление на ноль невозможно! Результат 15 / 4 не является целочисленным. 3 2 / 4 равно 8 Деление на ноль невозможно! Результат 127 / 8 не является целочисленным. Соответствующий элемент не найден. Соответствующий элемент не найден. !?@>A8< C M:A?5@B0 ВОПРОС. Когда в программе должна использоваться обработка исключений ? Когда должны создаваться специальные классы исключений ? ОТВЕТ. Поскольку в Java API исключения широко применяются для сообщения об ошибках, обработка исключений будет использоваться почти во всех реальных программах. Это та часть обработки исключений , которую большинство новых программистов на Java находят легкой. Сложнее решить, когда и как применять специальные исключения . Как правило, об ошибках можно сообщать двумя способами: возвращаемые значения и исключения. Когда один подход лучше другого? Проще говоря, в Java обработка исключений должна быть нормой. Конечно, возврат кода ошибки в некоторых случаях является приемлемой альтернативой, но исключения обеспечивают более мощный и структурированный способ обработки ошибок. Именно таким способом профессиональные программисты на Java обрабатывают ошибки в своем коде . 360 Java : руководство для начинающих, 9-е издание Упражнение 9.1 Добавление исключений в класс очереди В данном проекте создаются два класса исключений, которые можно использовать с классами очередей , разработанными в упражнении 8.1. Они будут указывать на состояния ошибок “ очередь переполнена ” и “ очередь пуста ” . Такие исключения могут быть сгенерированы методами put ( ) и get ( ) . Для простоты в этом проекте исключения будут добавлены в класс FixedQueue, но их легко внедрить в другие классы очередей из упражнения 8.1. 1 . Для хранения классов исключений , связанных с очередью, понадобится создать два файла. Назовите первый файл QueueFullException.java и поместите в него следующий код: QueueFullException.java QueueEmptyException.java FixedQueue.java QExcDemo.java // Исключение для ошибок, связанных с переполненной очередью , public class QueueFullException extends Exception { int size; QueueFullException(int s) { size = s; } public String toStringO { return " ХпОчередь переполнена. Максимальный размер составляет " + size + " элементов."; } } Исключение QueueFullException будет генерироваться при попытке сохранения элемента в уже полную очередь. 2. Создайте второй файл QueueEmptyException.java и поместите в него такой код: // Исключение для ошибок, связанных с пустой очередью , public class QueueEmptyException extends Exception { public String toStringO { return " ХпОчередь пуста."; } } Исключение QueueEmptyException будет генерироваться при попытке удаления элемента из пустой очереди. 3. Модифицируйте код класса FixedQueue, чтобы он генерировал исклю чения при возникновении ошибок, и поместите результирующий код в файл по имени FixedQueue.java. // Класс для представления очереди символов фиксированного размера, // в котором используются исключения , class FixedQueue implements ICharQ { private char[] q; // массив, в котором хранится очередь private int putloc, getloc; // индексы для позиций помещения // и извлечения Глава 9. Обработка исключений 361 // Конструктор пустой очереди заданного размера , public FixedQueue(int size) { q = new char[size]; // выделение памяти под очередь putloc = getloc = 0; } // Поместить символ в очередь. public void put(char ch ) throws QueueFullException { if( putloc==q.length) throw new QueueFullException(q.length); q[putloc++ ] = ch; } // Извлечь символ из очереди , public char get() throws QueueEmptyException { if(getloc == putloc) throw new QueueEmptyException(); return q[getloc++]; } } Обратите внимание , что для добавления исключений в FixedQueue тре буются два шага . Во - первых , объявления get() и put О должны быть снабжены конструкцией throws. Во - вторых , при возникновении ошибки эти методы генерируют исключение . Применение исключений позволя ет вызывающему коду рационально обрабатывать ошибку. Возможно , вы помните , что предыдущие версии просто сообщали об ошибке . Генерация исключения - гораздо лучший подход. 4. Чтобы опробовать обновленный класс FixedQueue, воспользуйтесь пока занным ниже классом QExcDemo, который должен находиться в файле по имени QExcDemo.java: // Демонстрация исключений, связанных с очередью , class QExcDemo { public static void main(String[] args) { FixedQueue q = new FixedQueue(10); char ch; int i; try { // Переполнение очереди. for(i=0; i < 11; i++) { System.out.print("Попытка сохранения: " + (char) ('A' + i)); q.put((char) ('A' + i)); System.out.println(" - успешно"); } System.out.println(); } catch ( QueueFullException exc) { System.out.println(exc); } 362 Java: руководство для начинающих, 9-е издание System.out.printIn(); try { // Опустошение очереди. for(i=0; i < 11; i++) { System.out.print("Получение следующего символа: ch = q.get(); System.out.println(ch); } } catch ( QueueEmptyException exc) { System.out.println(exc); } } } 5. Поскольку класс FixedQueue реализует интерфейс iCharQ, в котором определены два метода очереди get ( ) и put ( ) , код ICharQ понадобится изменить, чтобы отразить конструкцию throws. Далее приведен обновленный код интерфейса ICharQ. Не забывайте , что он должен находиться в отдельном файле по имени ICharQ.java. // Интерфейс очереди символов, который генерирует исключения , public interface ICharQ { // Поместить символ в очередь. void put(char ch) throws QueueFullException; // Извлечь символ из очереди. char get() throws QueueEmptyException; } 6. Скомпилируйте обновленный файл ICharQ.java, после чего ском пилируйте файлы FixedQueue.java, QueueFullException.java , QueueEmptyException.java и QExcDemo.java. Запустите QExcDemo. Вы увидите следующий вывод: Попытка Попытка Попытка Попытка Попытка Попытка Попытка Попытка Попытка Попытка Попытка Очередь сохранения: А - успешно сохранения: В успешно сохранения: С успешно сохранения: D - успешно сохранения: Е успешно сохранения: F успешно сохранения: G - успешно сохранения: Н успешно успешно сохранения: I сохранения: J успешно сохранения: К переполнена. Максимальный размер составляет 10 элементов. Получение Получение Получение Получение Получение символа: символа: символа: символа: следующего символа: следующего следующего следующего следующего А В С D Е Глава 9 . Обработка исключений 363 Получение следующего Получение следующего Получение следующего Получение следующего Получение следующего Получение следующего Очередь пуста. символа: символа: символа: символа: символа: символа: F G Н I J Вопросы и упражнения для самопроверки 1. Какой класс находится на вершине иерархии исключений ? 2 Кратко объясните , как использовать try и catch. 3 Что не так со следующим фрагментом кода? II ... vals[18] = 10; . . catch (ArraylndexOutOfBoundsException exc) { // обработать ошибку 4. } Что случится, если исключение не будет перехвачено? 5. Что не так со следующим фрагментом кода? class A extends Exception { ... class В extends А { ... И ... try { // ... } catch (A exc) { ... } catch ( В exc) { ... } 6. Может ли внутренний блок catch повторно сгенерировать исключение для внешнего блока catch? 7. Верно ли утверждение , что блок finally является последней порцией кода, выполняемой перед завершением программы? Обоснуйте свой ответ. 8. Какие типы исключений должны явно объявляться в конструкции throws метода? 9. Что не так со следующим фрагментом кода ? class MyClass { // ... } И . .. throw new MyClass(); 10. Отвечая на вопрос 3 в конце главы 6, вы создали класс Stack. Добавьте в него специальные исключения, сообщающие о том, что стек полон и стек пуст. 11. Какими тремя способами может быть сгенерировано исключение? 12. Назовите два прямых подкласса Throwable? 13. Что собой представляет средство множественного перехвата? 14. Должен ли код обычно перехватывать исключения типа Error? ш. I V* I, I ' I s. II II, I , II .' »» 4 II I *\ *5 . 4' I I I « I I•* *' "' % Nt Глава 10 Использование ввода-вывода 4 *« • . > * <4• ^ v , " " 366 Java : руководство для начинающих, 9-е издание В этой главе • • • z Понятие потока z Отличия между потоками байтовых и символьных данных z Классы потоков байтовых данных Java z Классы потоков символьных данных Java z Предопределенные потоки z Использование потоков байтовых данных Применение потоков байтовых данных для файлового ввода-вывода z z Автоматическое закрытие файла с использованием оператора try с ресурсами z Чтение и запись двоичных данных Использование файлов с произвольным доступом z Применение потоков символьных данных z Использование потоков символьных данных для файлового ввода-вывода Применение оболочек типов Java для преобразования числовых строк z z ч асти системы ввода- вывода Java вроде println ( ) использовались с самого начала книги, но без какого-либо формального объяснения . Поскольку си стема ввода- вывода Java основана на иерархии классов, представить ее теорию и детали без предварительного обсуждения классов, наследования и исключений было невозможно. Теперь самое время подробно изучить подход к вводувыводу, принятый в Java. Имейте в виду, что система ввода-вывода Java довольно крупная и включает множество классов, интерфейсов и методов. Частично причина такого размера связана с тем , что в Java определены две полноценные системы ввода - вывода: одна для байтового ввода- вывода , а другая для символьного ввода - вывода. Здесь невозможно обсудить все аспекты ввода- вывода Java. ( Описание системы вводавывода Java могло бы занять целую книгу!) Однако в этой главе вы ознакомитесь со многими важными и часто применяемыми средствами. К счастью , система ввода- вывода в Java является целостной и согласованной; как только вы поймете ее основы, остальную часть системы ввода-вывода освоить несложно. Прежде чем начать, необходимо сделать важное замечание . Классы вводавывода , описанные в главе , поддерживают консольный и файловый ввод- вывод на базе текста. Они не предназначены для создания графических пользовательских интерфейсов , т.е. не будут использоваться при построении , скажем , оконных приложений. Тем не менее , в Java имеется существенная поддержка Глава 10. Использование ввода -вывода 367 создания графических пользовательских интерфейсов. Основы построения графических пользовательских интерфейсов можно найти в главе 17, где предлагается введение в Swing инструментальный набор, наиболее широко используемый для таких целей . — Ввод- вывод в Java основан на потоках Ввод- вывод в программах на Java выполняется через потоки данных. Поток это абстракция , которая либо производит, либо потребляет данных (stream ) информацию. Поток связан с физическим устройством посредством системы ввода - вывода Java. Все потоки ведут себя одинаково , даже если фактические физические устройства , с которыми они связаны, различаются . Таким образом , одни и те же классы и методы ввода - вывода могут применяться к разным ти пам устройств. Например , те же методы , которые используются для записи на консоль, можно применять для записи в дисковый файл . Потоки данных Java реализованы внутри иерархий классов, определенных в пакете java.io. — Потоки байтовых и символьных данных В современных версиях Java определены два типа потоков ввода - вывода: байтовые и символьные . ( В первоначальной версии Java был определен только поток байтовых данных, но вскоре были добавлены и потоки символьных данных.) Потоки байтовых данных предлагают удобные средства для обработки ввода и вывода байтов. Они используются , например , при чтении или записи двоичных данных и особенно полезны при работе с файлами . Потоки символьных данных предоставляют удобные средства для обработки ввода и вывода символов. Они применяют Unicode и , следовательно , допускают интернационализацию. Кроме того , в ряде случаев потоки символьных данных эффективнее потоков байтовых данных. Тот факт, что в Java определены два разных типа потоков данных , увеличи вает размеры системы ввода-вывода , т.к. необходимы два отдельных набора иерархий классов (один для байтов , другой для символов) . Огромное количество классов ввода- вывода может сделать систему ввода - вывода более устрашающей , чем она есть на самом деле . Просто помните , что по большей части функци ональность потоков байтовых данных аналогична функциональности потоков символьных данных. И еще один момент: на самом низком уровне все операции ввода-вывода попрежнему ориентированы на байты. Потоки символьных данных просто обе спечивают удобный и эффективный инструмент для обработки символов. Классы потоков байтовых данных Потоки байтовых данных определяются с применением двух иерархий классов. Вверху находятся два абстрактных класса: InputStream и OutputStream. В классе InputStream определены характеристики , общие для потоков ввода байтовых данных, а класс OutputStream описывает поведение потоков вывода байтовых данных. 368 Java: руководство для начинающих, 9- е издание Классы InputStream и OutputStream имеют несколько конкретных подклассов, которые предлагают различную функциональность и обрабатывают детали чтения и записи на различные устройства, такие как дисковые файлы . Классы потоков байтовых данных из java.io, которые не объявлены нерекомендуемыми, кратко описаны в табл. 10.1. Пусть вас не смущает количество разных классов. Научившись пользоваться одним потоком байтовых данных, вы легко освоите остальные. Таблица 10.1 . Классы потоков байтовых данных в java . io, которые не объявлены нерекомендуемыми Класс потока данных Описание BufferedlnputStream BufferedOutputStream ByteArrayInputStream Буферизованный поток ввода Буферизованный поток вывода Поток ввода, который выполняет чтение из байтового массива ByteArrayOutputStream Поток вывода, который выполняет запись в байтовый массив DatalnputStream DataOutputStream FilelnputStream FileOutputStream FilterInputStream FilterOutputStream InputStream ObjectlnputStream ObjectOutputStream OutputStream PipedlnputStream PipedOutputStream PrintStream PushbacklnputStream Поток ввода, который содержит методы для чтения стандартных типов данных Java Поток ввода, который содержит методы для записи стандартных типов данных Java Поток ввода, который выполняет чтение из файла Поток вывода, который выполняет запись в файл Реализует InputStream Реализует OutputStream Абстрактный класс, который описывает поток ввода Поток ввода для объектов Поток вывода для объектов Абстрактный класс, который описывает поток вывода Канал ввода Канал вывода Поток вывода, который содержит методы print ( ) и println() Поток ввода, который позволяет возвращать байты в этот поток ввода SequenceInputStream Поток ввода, являющийся комбинацией двух и более потоков ввода, которые будут читаться последовательно друг за другом Глава 10. Использование ввода -вывода 369 Классы потоков символьных данных Потоки символьных данных определяются с помощью двух иерархий клас сов, на вершине которых находятся два абстрактных класса: Reader и Writer. Класс Reader используется для ввода , а класс Writer — для вывода. Конкретные классы , производные от Reader и Writer, оперируют на потоках символов Unicode. С Reader и Writer связано несколько конкретных подклассов, обрабатыва ющих различные ситуации ввода - вывода. Как правило, классы, основанные на символах, аналогичны классам , основанным на байтах. Классы потоков сим вольных данных из java.io кратко описаны в табл . 10.2. Таблица 10.2 . Классы символьных данных в java . io Класс потока данных Описание BufferedReader BufferedWriter CharArrayReader Буферизованный поток ввода символьных данных Буферизованный поток вывода символьных данных Поток ввода, который выполняет чтение из символьного массива CharArrayWriter Поток вывода, который выполняет запись в символьный массив FileReader FileWriter FilterReader FilterWriter InputStreamReader Поток ввода, который выполняет чтение из файла LineNumberReader OutputStreamWriter Поток ввода, который подсчитывает строки PipedReader PipedWriter PrintWriter Канал ввода PushbackReader Поток вывода, который выполняет запись в файл Фильтрующее средство чтения Фильтрующее средство записи Поток ввода, который выполняет трансляцию байтов в символы Поток вывода, который выполняет трансляцию символов в байты Канал вывода Поток вывода, который содержит методы print() и println() Поток ввода, который позволяет возвращать байты в этот поток ввода Reader Абстрактный класс, описывающий поток ввода символьных данных StringReader StringWriter Writer Поток ввода, который выполняет чтение из строки Поток вывода, который выполняет запись в строку Абстрактный класс, описывающий поток вывода символьных данных 370 Java: руководство для начинающих, 9-е издание Предопределенные потоки Как вам уже известно , все программы на Java автоматически импортируют пакет java.lang, в котором определен класс System, инкапсулирующий ряд аспектов исполняющей среды. Помимо прочего он содержит три предопределенные потоковые переменные: in, out и err. Они объявлены в System как поля public, static и final, т.е. могут использоваться в любой другой части программы без ссылки на конкретный объект System. Поле System ,out ссылается на стандартный поток вывода . По умолчанию это консоль. Поле System ,in ссылается на стандартный поток ввода, в качестве которого по умолчанию выступает клавиатура. Поле System.err ссылается на стандартный поток вывода ошибок , по умолчанию также являющийся консолью. Однако упомянутые потоки могут быть перенаправлены на любое совместимое устройство ввода- вывода . System ,in представляет собой объект типа InputStream, a System ,out и System ,err объекты типа PrintStream. Это потоки байтовых данных , хотя обычно они применяются для чтения символов с консоли и записи символов на консоль. Причина , по которой они представляют собой потоки байтовых, а не символьных данных , связана с тем , что предопределенные потоки были частью исходной спецификации Java, которая не включала потоки символьных данных. Вы увидите , что при желании их можно поместить внутрь потоков символьных данных. — Использование потоков байтовых данных Исследование ввода - вывода в Java начинается с потоков байтовых данных. Как уже упоминалось, наверху иерархии классов потоков байтовых данных располагаются InputStream и OutputStream. В табл . 10.3 перечислены методы InputStream, а в табл . 10.4 методы OutputStream. — Таблица 10.3. Методы, определенные в классе InputStream Метод Описание int available() Возвращает количество байтов входных данных, доступных в текущий момент для чтения void close() Закрывает источник ввода. Дальнейшие попытки чтения приведут к генерации исключения IOException void mark(int numBytes) Помещает метку в текущую позицию потока ввода, которая будет оставаться действительной до тех пор, пока не будет прочитано numBytes байтов boolean markSupported() Возвращает true, если вызывающий поток поддерживает методы mark()/reset() Глава 10. Использование ввода -вывода 371 Окончание табл . 10.3 Метод Описание static InputStream nullInputStream() Возвращает открытый, но пустой поток ввода, т.е. поток, не содержащий данных. Таким образом, текущая позиция всегда находится в конце потока, и никакие входные данные не могут быть получены. Однако поток можно закрыть int read() Возвращает целочисленное представление следующего доступного байта во входных данных. При попытке int read(byte[] buffer) Пытается прочитать вплоть до buffer , length байтов в буфер, указанный в buffer, и возвращает фактическое количество успешно прочитанных байтов. При чтения в конце потока возвращает -1 попытке чтения в конце потока возвращает -1 int read(byte[] buffer, int offset, int numBytes) Пытается прочитать вплоть до numBytes байтов в буфер buffer, начиная с buffer [ offset ] , и возвращает фактическое количество успешно прочитанных байтов. При попытке чтения в конце потока возвращает -1 byte[] readAllBytes() Начиная с текущей позиции, читает до конца потока и возвращает байтовый массив, который содержит входные данные byte[] readNBytes( int numBytes) Пытается прочитать numBytes байтов и возвращает результат в байтовом массиве. Если конец потока достигнут до того, как было прочитано numBytes байтов, тогда возвращаемый массив будет содержать меньше, чем numBytes байтов int readNBytes( byte[] buffer, int offset, int numBytes) void reset() Пытается прочитать вплоть до numBytes байтов в буфер buffer, начиная с buffer [ offset ] , и возвращает фактическое количество успешно прочитанных байтов Переустанавливает указатель ввода на ранее установленную метку long skip( long numBytes) Игнорирует (т.е. пропускает) numBytes байтов входных данных и возвращает фактическое количество пропущенных байтов void skipNBytes( long numBytes ) Игнорирует (т.е. пропускает) numBytes байтов входных данных. Генерирует исключение EOFException, если достигнут конец потока до того, как было пропущено numBytes байтов, либо исключение IOException при возникновении какой-то ошибки ввода-вывода long transferTo( OutputStream strm) Копирует байты из вызывающего потока в strm и возвращает количество скопированных байтов 372 Java: руководство для начинающих, 9-е издание Таблица 10.4 . Методы, определенные в классе OutputStream Метод Описание void close() Закрывает поток вывода. Дальнейшие попытки записи приведут к генерации исключения IOException Финализирует состояние вывода, так что любые буферы очищаются, т.е. сбрасывает буферы вывода Возвращает открытый, но пустой поток вывода, т.е. поток, в который никакие выходные данные фак тически не записывались. Таким образом, методы вывода могут быть вызваны, но они не производят какой-либо вывод. Однако поток можно закрыть Записывает одиночный байт в поток вывода. Обратите внимание, что параметр имеет тип int, что позволяет вызывать write ( ) с выражением без необходимости в приведении к типу byte Записывает полный байтовый массив в поток вывода Записывает поддиапазон длиной numBytes байтов из буфера buffer типа байтового массива, начиная с buffer [ offset] void flush() static OutputStream nullOutputStream() void write(int b) void write(byte[] buffer) void write(byte [ ] buffer, int offset, int numBytes ) Как правило, методы InputStream и OutputStream в случае ошибки могут генерировать исключение IOException. Методы, определенные в этих двух абстрактных классах , доступны всем их подклассам. Таким образом, они образуют минимальный набор функций ввода - вывода , которые будут поддерживать все потоки байтовых данных. Чтение консольного ввода Первоначально единственным способом выполнения консольного ввода было использование потока байтовых данных, и в большей части кода Java до сих пор применяются исключительно такие потоки. Сегодня можно использовать потоки байтовых или символьных данных. Для коммерческого кода предпочтительный способ чтения консольного ввода предусматривает применение символьно-ори ентированного потока. В итоге упрощается интернационализация и сопровождение программы. Также более удобно работать напрямую с символами, не выполняя преобразований между символами и байтами. Тем не менее , в примерах программ , в простых служебных программах для внутреннего использования и приложений, которые имеют дело с необработанным клавиатурным вводом , допустимо применять потоки байтовых данных. По этой причине здесь рассматривается консольный ввод-вывод с использованием потоков байтовых данных. Поскольку System ,in является экземпляром InputStream, вы автоматически получаете доступ к методам , определенным в классе InputStream. Таким образом , появляется возможность применения метода read() для чтения байтов из System ,in. Глава 10. Использование ввода -вывода 373 Есть следующие три версии read(): int read() throws IOException int read(byte[] data) throws IOException int read(byte[] data, int start, int max) throws IOException В главе 3 вы видели , как использовать первую версию read ( ) для чтения одного символа с клавиатуры ( из System ,in). Она возвращает -1 при попытке чтения в конце потока . Вторая версия читает байты из входного потока и помещает их в data до тех пор, пока массив не заполнится, не будет достигнут конец потока или не произойдет ошибка . Она возвращает количество прочитанных байтов или -1, если предпринята попытка чтения в конце потока. Третья версия читает входные данные в data, начиная с места , которое указано в start. Сохраняется до шах байтов. Она возвращает количество прочитанных байтов или -1 при попытке чтения в конце потока. В случае возникновения ошибки все версии генерируют исключение IOException. Ниже показана программа , демонстрирующая чтение массива байтов из System ,in. Обратите внимание, что любые исключения ввода-вывода , которые могут возникнуть, просто обрабатываются за пределами метода main ( ) . Такой подход является обычным при чтении с консоли , но при желании можете самостоятельно обрабатывать эти виды ошибок. // Чтение массива байтов с клавиатуры . import java.io.*; class ReadBytes { public static void main (String[] args) throws IOException { byte[] data = new byte[10]; System.out.println("Введите несколько символов."); System.in.read(data); < Прочитать массив байтов из клавиатуры System.out.print("Вы ввели: "); for(int i=0; i < data.length; i++) System.out.print((char) data[i]); } } Вот пример запуска программы: Введите несколько символов. Read Bytes Вы ввели: Read Bytes Запись консольного вывода Как и в случае консольного ввода , изначально в Java для консольного вывода предоставлялись только потоки байтовых данных. В Java 1.1 были добавлены потоки символьных данных , которые рекомендуется использовать в наиболее переносимом коде . Однако поскольку System ,out это поток байтовых дан ных, консольный вывод на основе байтов по- прежнему применяется довольно широко. На самом деле он использовался во всех программах, приведенных в книге до сих пор! Таким образом , он рассматривается ниже. — 374 Java: руководство для начинающих, 9-е издание Консольный вывод проще всего обеспечить с помощью уже знакомых вам методов print ( ) и printIn( ) . Упомянутые методы определены в классе . Несмотря на то PrintStream (тип объекта , на который ссылается System ,out) что System ,out является потоком байтовых данных, применять его для простого вывода в программе по- прежнему приемлемо. Поскольку класс PrintStream представляет собой выходной поток , производный от OutputStream, он также реализует низкоуровневый метод write(). Таким образом , write ( ) можно использовать для записи в консоль. Вот простейшая форма write(), определенная в PrintStream: void write(int byteval) Метод write ( ) записывает байт, указанный в аргументе byteval. Хотя аргумент byteval объявлен как int, записываются только младшие восемь битов. Ниже показан краткий пример , в котором write ( ) применяется для вывода на экран символа “ X ” , а за ним символа новой строки: // Демонстрация использования System.out.write(). class WriteDemo { public static void main(String[] args) { int b; } b = 'X'; System.out.write(b); System.out . write('\n'); Записать байт на экран } Для вывода на консоль метод write ( ) будет использоваться нечасто ( несмотря на его удобство в ряде ситуаций ) , т.к. значительно проще применять методы print ( ) и println(). Класс PrintStream предлагает два дополнительных метода вывода: printf() и format ( ) . Оба они обеспечивают детальный контроль над точным форматом выводимых данных. Например , допускается указывать количество отображае мых десятичных разрядов, минимальную ширину поля или формат отрицательного значения . Хотя эти методы не будут использоваться в примерах , приводимых в книге, вы наверняка захотите изучить их по мере того , как будете углублять свои знания языка Java. Чтение и запись в файлы с использованием потоков байтовых данных В Java предоставляется ряд классов и методов, которые позволяют читать фай лы и записывать в них. Разумеется, наиболее распространенными типами файлов являются дисковые файлы. Все файлы в Java ориентированы на байты, и существуют методы для чтения и записи байтов из файла и в файл . Таким образом, чтение и запись в файлы с применением потоков байтовых данных крайне распространены. Тем не менее , как будет показано далее в главе , файловый поток, ориентированный на байты , можно поместить внутрь объекта на базе символов. Глава 10. Использование ввода -вывода 375 Для создания потока байтовых данных, связанного с файлом , используется класс FilelnputStream или FileOutputStream. Чтобы открыть файл , нужно просто создать объект одного из упомянутых классов, передав конструктору имя файла в качестве аргумента . Когда файл открыт, его можно читать или за писывать в него. Ввод из файла Файл открывается для ввода за счет создания объекта FilelnputStream. Вот часто применяемый конструктор: FilelnputStream(String fileName) throws FileNotFoundException В fileName указывается имя файла , который требуется открыть. Если файл не существует, тогда генерируется исключение FileNotFoundException, которое является подклассом IOException. Для чтения из файла можно использовать метод read ( ) . Ниже показана версия, которая будет применяться: int read() throws IOException При каждом вызове метод read() читает один байт из файла и возвращает его в виде целочисленного значения. Он возвращает -1, когда встречается конец файла , и генерирует исключение IOException в случае возникновения ошибки. Таким образом , эта версия read ( ) аналогична той, что применялась для чтения с консоли. Завершив работу с файлом , вы должны его закрыть, что делается вызовом метода close ( ) . Вот его общая форма: void close() throws IOException Закрытие файла приводит к освобождению системных ресурсов, выделенных для файла , что позволяет их задействовать другим файлом. Из-за отказа от за крытия файла могут возникнуть “ утечки памяти ” , причиной которых являются оставшиеся выделенными неиспользуемые ресурсы. В следующей программе метод read ( ) применяется для ввода и отображения содержимого текстового файла , имя которого указывается в аргументе командной строки. Обратите внимание на то , что блоки try/catch обрабатывают возможные ошибки ввода-вывода. /* Отображение содержимого текстового файла. При запуске этой программы должно быть указано имя файла, содержимое которого необходимо увидеть. Например, для просмотра содержимого файла по имени TEST.TXT используйте следующую командную строку: java ShowFile TEST.TXT */ import java.io.*; class ShowFile { 376 Java: руководство для начинающих, 9- е издание public static void main(String[] args) { int i; FilelnputStream fin; // Первым делом удостовериться, что имя файла было указано , if(args.length != 1) { System.out.println("Использование: ShowFile имя-файла"); return; } try { fin = new FilelnputStream(args[0]); } catch( FileNotFoundException exc) { System.out.printIn("Файл не найден."); return; } Открыть файл try { // Читать байты , пока не встретится конец файла , do { i = fin.read(); Читать из файла if(i != -1) System.out.print((char) i); «+ } while(i != -1 ); Когда i равно -1, значит, был достигнут конец файла } catch(IOException exc) { System.out.println("Ошибка при чтении файла."); } try { fin.close(); Закрыть файл } catch(IOException exc) { System.out.println("Ошибка при закрытии файла."); } } } Обратите внимание, что в предыдущем примере файловый поток закрывается после завершения блока try , где читается файл . Хотя такой подход иногда полезен, в Java поддерживается вариант, который часто будет лучшим вы бором. Вариант предусматривает вызов метода close ( ) внутри блока finally. В этом случае все методы доступа к файлу содержатся в блоке try , а блок finally используется для закрытия файла. Таким образом, независимо от того, как завершится блок try, файл будет закрыт. Ниже показано, каким образом можно перекодировать блок try, в котором читается файл : try { do { i = fin.read(); if(i != -1) System.out.print((char) i); } while(i != -1); } Глава 10. Использование ввода -вывода 377 catch(IOException exc) { System.out.println("Ошибка при чтении файла."); } finally { // Закрыть файл при выходе из блока try. Использовать конструкцию finally для закрытия файла try { fin.closeO ; } catch(IOException exc) { System.out.println("Ошибка при закрытии файла."); } } Одно из преимуществ продемонстрированного подхода в целом связано с тем , что если код доступа к файлу завершается из - за некоторого исключения , не связанного с вводом - выводом , то файл все равно закрывается в блоке finally. Хотя в приведенном примере ( или в большинстве других примеров программ ) проблем не возникает, поскольку программа просто завершается , если возника ет неожиданное исключение , это может быть основным источником неприят ностей в крупных программах . Применение finally позволяет избежать дан ной проблемы . Иногда проще поместить части программы , которые открывают файл и получают к нему доступ , в один блок try (вместо их разнесения) , а затем использовать блок finally для закрытия файла . Например, вот еще один способ на писать программу ShowFile: /* В этой вариации программы код открытия файла и доступа к нему находится внутри одиночного блока try. Файл закрывается в блоке finally. */ import java.io.*; class ShowFile { public static void main(String[] args) { int i; • FilelnputStream fin = null; + Здесь fin инициализируется значением null // Первым делом удостовериться, что имя файла было указано , if(args.length != 1) { System.out.println ("Использование: ShowFile имя-файла"); return; } // Следующий код открывает файл, читает из него символы , // пока не встретится конец файла, // и закрывает файл посредством блока finally , try { fin = new FilelnputStream(args[0]); do { i = fin.read() ; if(i != -1) System.out.print((char) i); } while(i != -1); } 378 Java: руководство для начинающих, 9-е издание catch( FileNotFoundException exc) { System.out.println ("Файл не найден."); } catch(IOException exc) { System.out.println("Возникла ошибка ввода-вывода."); } finally { // Закрыть файл во всех случаях , try { if(fin != null) fin.closeO ; } - Закрыть fin, только если значение не равно null catch(IOException exc) { System.out.println("Ошибка при закрытии файла."); } } } } Обратите внимание , что при таком подходе fin инициализируется значением null. Затем в блоке finally файл закрывается , только если fin не равно null. Прием работает, потому что fin будет не равно null лишь в случае успешного открытия файла. Таким образом, метод close ( ) не вызывается, если при открытии файла возникло исключение. Последовательность try/catch в предыдущем примере можно сделать более компактной. Поскольку FileNotFoundException является подклассом IOException, его не нужно перехватывать отдельно. Скажем , следующую кон струкцию catch можно применять для перехвата обоих исключений, что избавляет от необходимости перехватывать FileNotFoundException по отдельности . В таком случае отображается стандартное сообщение об исключении , описывающее ошибку. } catch(IOException exc) { System.out.println ("Ошибка ввода-вывода: " + exc); } finally { При таком подходе любая ошибка , в том числе и связанная с открытием файла , будет просто обрабатываться одной конструкцией catch. Из- за своей компактности этот подход используется в большинстве примеров ввода-вывода в книге. Однако имейте в виду, что прием не подходит в ситуациях , когда желательно отдельно обрабатывать отказ при открытии файла , например , если пользователь неправильно указал имя файла. В такой ситуации вы можете за просить правильное имя , скажем, перед входом в блок try , где производится доступ к файлу. Глава 10. Использование ввода - вывода 379 !?@>A8< C M:A?5@B0 ВОПРОС. Метод read ( ) возвращает -1, когда достигнут конец файла, но не имеет специального возвращаемого значения для ошибки доступа к файлу. Почему? ОТВЕТ. В Java ошибки обрабатываются с использованием исключений. Если read ( ) или любой другой метод ввода -вывода возвращает значение , то это означает, что ошибки отсутствуют. Такой способ гораздо яснее, чем обработка ошибок ввода-вывода с помощью специальных кодов ошибок. Вывод в файл Чтобы открыть файл для вывода , понадобится создать объект FileOutputStream. Ниже приведены два часто используемых конструктора: FileOutputStream(String fileName ) throws FileNotFoundException FileOutputStream(String fileName, boolean append) throws FileNotFoundException Если создать файл не удается , тогда генерируется исключение FileNotFoundException. При открытии выходного файла первый конструктор удаляет любой ранее существовавший файл с таким же именем . Что касается второго конструктора , если append равно true, то вывод добавляется в конец файла , а иначе файл перезаписывается. Для выполнения записи в файл можно применять метод write ( ) , определенный в классе FileOutputStream. Ниже показана его простейшая форма: void write(int byteval) throws IOException Метод write ( ) записывает в файл байт, указанный в аргументе byteval. Хотя аргумент byteval объявлен как int, записываются только младшие восемь битов. Если во время записи возникает ошибка , тогда генерируется исключение IOException. Закончив работу с выходным файлом , его потребуется закрыть с помощью метода close(): void close() throws IOException Закрытие файла освобождает системные ресурсы , выделенные для файла , позволяя использовать их другим файлом. Оно также помогает гарантировать, что любые данные , остающиеся в буфере вывода , действительно записывается на физическое устройство. Следующий пример программы копирует текстовый файл . Имена исходного и целевого файлов указываются в командной строке . /* Копирование текстового файла. При запуске этой программы должны быть указаны имена исходного и целевого файлов. Например, для копирования файла по имени FIRST.TXT в файл SECOND.TXT используйте следующую командную строку: java CopyFile FIRST.TXT SECOND.TXT */ 380 Java : руководство для начинающих, 9-е издание import java.io.*; class CopyFile { public static void main(String[] args) throws IOException { int i; FilelnputStream fin = null; FileOutputStream fout = null; // Первым делом удостовериться, что оба имени файлов были указаны , if(args.length != 2) { System.out.printIn("Использование: CopyFile исходный-файл целевойфайл"); return; } // Копировать файл , try { // Попытка открытия файлов. fin = new FilelnputStream(args[0]); fout = new FileOutputStream(args[1]); do { «+ i = fin.read(); if(i != -1) fout.write(i); } while(i != -1); } } Прочитать байты из одного файла и записать их в другой } catch(IOException exc) { System.out.println("Ошибка ввода-вывода: " + exc); } finally { try { if(fin != null) fin.closeO ; } catch(IOException exc) { System.out.println("Ошибка при закрытии исходного файла."); } try { if(fout ! = null) fout.close(); } catch(IOException exc) { System.out.println("Ошибка при закрытии целевого файла."); } } Автоматическое закрытие файла В предыдущем разделе в примерах программ явно вызывался метод close ( ) для закрытия файла , когда он больше не нужен. Именно так закрывались файлы с момента появления Java . В результате этот подход широко распростра нен в существующем коде . Кроме того , он по - прежнему актуален и полезен . Тем не менее , начиная с версии JDK 7 , язык Java предоставляет средство , ко торое предлагает другой , более рациональный способ управления ресурсами Глава 10. Использование ввода -вывода 381 наподобие файловых потоков за счет автоматизации процесса закрытия. Средство , иногда именуемое автоматическим управлением ресурсами , основано на другой версии оператора try, которая называется try с ресурсами . Главное преимущество автоматического управления ресурсами связано с тем , что оно предотвращает ситуации , в которых файл ( или другой ресурс ) по невнимательности не освобождается после того , как он больше не нужен . Ранее уже объяснялось, что игнорирование закрытия файла может привести к утечкам памяти и прочим проблемам . Вот общая форма оператора try с ресурсами: try (спецификация-ресурса ) { // использовать ресурс } Как правило , спецификация-ресурса представляет собой оператор, который объявляет и инициализирует ресурс , скажем , файловый поток. Он состоит из объявления переменной, в котором переменная инициализируется ссылкой на управляемый объект. Когда блок try заканчивается , ресурс автоматически освобождается. В случае файла это означает автоматическое закрытие файла. ( Соответственно нет необходимости явно вызывать метод close ( ) .) Оператор try с ресурсами может также содержать конструкции catch и finally. На заметку! Начиная с JDK 9, спецификация ресурса в try также может состоять из переменной, которая была объявлена и инициализирована ранее в программе. Однако эта переменная должна быть фактически финальной, т.е. после предоставления начального значения новое значение ей не присваивалось. Оператор try с ресурсами можно применять только с теми ресурсами , ко торые реализуют интерфейс AutoCloseable, находящийся в пакете java.lang. В интерфейсе AutoCloseable определен метод close ( ) . Вдобавок интерфейс AutoCloseable унаследован интерфейсом Closeable в java.io. Оба ин терфейса реализованы классами потоков , в том числе FilelnputStream и FileOutputStream. Таким образом , try с ресурсами можно использовать при работе с потоками , включая файловые потоки. В качестве первого примера автоматического закрытия файла взгляните на переработанную версию программы ShowFile: /* В этой версии программы ShowFile используется оператор try с ресурсами для автоматического закрытия файла после того, как он больше не нужен. */ import java.io.*; class ShowFile { public static void main(String[] args) { int i; 382 Java: руководство для начинающих, 9-е издание // Первым делом удостовериться, что имя файла было указано , if(args.length != 1) { System.out.println("Использование: ShowFile имя-файла"); return; } // В следующем коде применяется оператор try с ресурсами для открытия // файла и затем его закрытия при покидании блока try. try(FilelnputStream fin = new FilelnputStream(args[0])) { do { i = fin.read(); if(i != -1) System.out.print((char) i); Блок try с ресурсами } while(i != -1); } catch(IOException exc) { System.out.println("Ошибка ввода-вывода: " + exc); } } } Обратите особое внимание в программе на то, каким образом файл открывается внутри оператора try с ресурсами: try(FilelnputStream fin = new FilelnputStream(args[0])) ( Легко заметить, что в части спецификации ресурса оператора try объявляется объект FilelnputStream по имени fin, которому затем присваивается ссылка на файл , открытый его конструктором. Таким образом , в данной версии программы переменная fin является локальной для блока try и создается при входе в него. Когда блок try заканчивается, поток, связанный с fin, автомати чески закрывается неявным вызовом close ( ) . Явно вызывать метод close() не понадобится, а потому не беспокойтесь о том , что вы забудете закрыть файл . В этом и состоит ключевое преимущество применения оператора try с ресурсами. Важно понимать, что ресурс, объявленный в операторе try, неявно является final, т.е. присваивать ему ресурс после его создания нельзя. Кроме того , об ласть действия ресурса ограничена оператором try с ресурсами. Прежде чем двигаться дальше , полезно упомянуть о том , что начиная с JDK 10 , при указании типа ресурса , объявленного в операторе try с ресурсами , можно использовать средство выведения типов локальных переменных. Для это го необходимо задать тип var, в результате чего тип ресурса будет выведен из его инициализатора . Например , оператор try в предыдущей программе теперь можно записать так: try(var fin = new FilelnputStream(args[0])) { Здесь для переменной fin выводится тип FilelnputStream, потому что именно к такому типу принадлежит его инициализатор. Поскольку многие чи татели имеют дело со средами Java , предшествующими JDK 10 , в операторах try с ресурсами в оставшихся главах книги выведение типов применяться не будет, чтобы код мог работать для максимально возможного числа читателей. Глава 10. Использование ввода -вывода 383 Разумеется , в будущем вы должны рассмотреть возможность использования выведения типов в собственном коде . В одиночном операторе try вы можете управлять более чем одним ресурсом. Для этого просто отделяйте спецификации ресурсов друг от друга точками с за пятой . Ниже представлен пример , где показанная ранее программа CopyFile переделана с целью применения одного оператора try с ресурсами для управле ния fin и fout. /* Версия CopyFile, в которой используется оператор try с ресурсами. Здесь демонстрируется управление двумя ресурсами (в данном случае файлами) с помощью одного оператора try. */ import java.io.*; class CopyFile { public static void main(String[] args) throws IOException { int i; // Первым делом удостовериться, что оба имени файлов были указаны , if(args.length ! = 2) { System.out.println("Использование: CopyFile исходный-файл целевойфайл"); return; } _ // Открыть и управлять двумя файлами посредством оператора try. try (FilelnputStream fin = new FilelnputStream(args[0]); FileOutputStream fout = new FileOutputStream(args[1])) { do { Управлять двумя i = fin.read(); ресурсами if(i != -1) fout.write(i); } while(i != -1); } catch(IOException exc) { System.out.println("Ошибка ввода-вывода: " + exc); } } } Обратите внимание на способ открытия в программе исходного и целевого файлов внутри блока try: try (FilelnputStream fin = new FilelnputStream(args[0]); FileOutputStream fout = new FileOutputStream(args[1])) { По окончании такого блока try файлы fin и fout будут закрыты . Сравнив эту версию программы с предыдущей версией , вы заметите , что она намного короче . Возможность оптимизации исходного кода является дополнительным преимуществом автоматического управления ресурсами . 384 Java: руководство для начинающих, 9-е издание Существует еще один аспект оператора try с ресурсами, о котором следует упомянуть. В общем случае при выполнении блока try возможна ситуация , когда исключение внутри try приведет к возникновению другого исключения во время закрытия ресурса в конструкции finally. В случае “ нормального ” оператора try исходное исключение утрачивается , будучи вытесненным вторым исключением. Тем не менее , при использовании оператора try с ресурсами второе исключение подавляется , но не утрачивается. Взамен оно добавля ется в список подавленных исключений , ассоциированных с первым исключением. Список подавленных исключений можно получить с помощью метода getSuppressedO , определенного в классе Throwable. Из- за преимуществ, которые предлагает оператор try с ресурсами , он будет применяться во многих , хотя и не во всех примерах программ в настоящем издании книги. В некоторых примерах по- прежнему используется традиционный подход к закрытию ресурса. На то есть несколько причин. Во- первых, вы можете столкнуться с унаследованным кодом , который основан на традиционном подходе. Важно, чтобы все программисты на Java полностью разбирались в тра диционном подходе при сопровождении унаследованного кода и чувствовали себя комфортно. Во- вторых, возможно, что некоторые программисты в течение какого-то времени продолжат работу в среде , предшествующей JDK 7. В таких ситуациях расширенная форма try не будет доступной. Наконец, в-третьих , могут быть случаи , когда явное закрытие ресурса более целесообразно, нежели применение автоматического подхода. Несмотря на вышеизложенное , если вы имеете дело с современной версией Java , то обычно будете использовать авто матизированный подход к управлению ресурсами. Он предлагает упрощенную и надежную альтернативу традиционному подходу. Чтение и запись двоичных данных До сих пор выполнялось только чтение и запись байтов, содержащих символы ASCII , но допускается ( и часто применяется ) чтение и запись других типов данных. Например, можно создать файл , хранящий значения типа int, double или short. Для чтения и записи двоичных значений примитивных типов Java будут использоваться классы DataInputstream и DataOutputStream. Класс DataOutputStream реализует интерфейс DataOutput, определяющий методы , которые записывают значения всех примитивных типов Java в файл . Важно понимать, что эти данные записываются в своем внутреннем двоичном формате , а не в удобочитаемой текстовой форме . Несколько часто применяемых методов вывода для примитивных типов Java кратко описаны в табл . 10.5. Каждый метод в случае отказа генерирует исключение IOException. Ниже показан конструктор класса DataOutputStream. Обратите внимание , что он построен на экземпляре OutputStream. DataOutputStream(OutputStream outputStream) В outputStream указывается поток , куда записываются данные. Чтобы записать выходные данные в файл , можно использовать объект, созданный FileOutputStream для этого параметра. Глава 10. Использование ввода -вывода 385 Таблица 10.5 . Часто используемые методы вывода, определенные в классе DataOutputStream Метод вывода Описание void writeBoolean( boolean val) void writeByte(int val) void writeChar(int val) Записывает значение boolean, указанное в val void void void void void writeDouble(doubleval) writeFloat(float val) writelnt(int val) writeLong(long val) writeShort(int val) Записывает младший байт, указанный в val Записывает значение, указанное в val, в виде значения char Записывает значение double, указанное в val Записывает значение float, указанное в val Записывает значение int, указанное в val Записывает значение long, указанное в val Записывает значение, указанное в val, в виде значения short Таблица 10.6 . Часто используемые методы ввода, определенные в классе DatalnputStream Метод вывода Описание boolean readBoolean() byte readByte() char readChar() double readDouble() float readFloatO int readlntO long readLongO short readShortO Читает значение boolean Читает значение byte Читает значение char Читает значение double Читает значение float Читает значение int Читает значение long Читает значение short Класс DatalnputStream реализует интерфейс Datalnput, который предо ставляет методы для чтения значений всех примитивных типов Java , кра тко описанные в табл . 10.6 . Каждый метод может генерировать исключение IOException. Класс DatalnputStream использует в качестве основы экземпляр InputStream, перекрывая его методами для чтения различных типов данных Java. Помните , что DatalnputStream читает данные в двоичном формате , а не в удобочитаемой форме . Ниже представлен конструктор класса DatalnputStream: DatalnputStream(InputStream inputStream) В inputStream указывается поток , связанный с создаваемым экземпляром DatalnputStream. Чтобы прочитать входные данные из файла , можно использовать объект, созданный FilelnputStream для этого параметра . 386 Java: руководство для начинающих, 9-е издание Следующая программа демонстрирует применение классов DataOutputStream и DataInputStream. В ней данные различных типов сначала записываются в файл , а затем читаются из него . // Запись и последующее чтение двоичных данных , import java.io.*; class RWData { public static void main(String[] args) { int i = 10; double d = 1023.56; boolean b = true; // Записать ряд значений. try (DataOutputStream dataOut = new DataOutputStream(new FileOutputStream("testdata"))) { System.out.println("Запись " + i); dataOut.writelnt(i); System.out.println("Запись " + d); dataOut.writeDouble(d); System.out.println("Запись " + b); dataOut.writeBoolean(b); - Записать двоичные данные System.out.println("Запись " + 12.2 * 7.4); dataOut.writeDouble(12.2 * 7.4); } catch(IOException exc) { System.out.println("Ошибка при записи."); return; } System.out.println(); // Прочитать ранее записанные значения , try (DatalnputStream dataln = new DatalnputStream(new FilelnputStream("testdata"))) { i = dataln.readlnt(); System.out.println("Чтение " + i); d = dataln.readDoubleO ; System.out.println("Чтение " + d); « b = dataln.readBoolean(); * System.out.println("Чтение " + b); d = dataln.readDoubleO ; System.out.println("Чтение " + d); } catch(IOException exc) { System.out.println("Ошибка при чтении."); } } } Прочитать двоичные данные Глава 10. Использование ввода -вывода 387 Вот вывод, получаемый в результате запуска программы: Запись Запись Запись Запись 10 1023.56 true 90.28 Чтение 10 Чтение 1023.56 Чтение true Чтение 90.28 Упражнение ЮЛ Утилита сравнения файлов В этом проекте разрабатывается простая , но полезная утиCompFiles.java лита , предназначенная для сравнения файлов. Утилита открывает оба указанных файла , после чего читает и сравнивает соответствующие наборы байтов. При обнаружении несоответствия файлы считаются отличающимися. Если концы файлов достигнуты одновременно, а несоответствия не были найдены , тогда файлы считаются совпадающими . Обратите внимание на применение оператора try с ресурсами для автоматического закрытия файлов. 1. Создайте файл по имени CompFiles.java. 2. Поместите в файл CompFiles.java следующий код: /* Упражнение 10.1. Сравнение двух файлов. При запуске программы укажите имена сравниваемых файлов в командной строке. */ java CompFile FIRST.TXT SECOND.TXT import java.io.*; class CompFiles { public static void main(String[] args) { int i=0, j=0; // Первым делом удостовериться, что оба имени файлов были указаны , if(args.length !=2 ) { System.out.println("Использование: CompFiles файл1 файл2 п ); return; } // Сравнить файлы . try (FilelnputStream fl = new FilelnputStream(args[0]); FilelnputStream f2 = new FilelnputStream(args[1])) { // Проверить содержимое каждого файла , do { i f1.read(); j = f2.read(); 388 Java: руководство для начинающих, 9- е издание if(i != j) break; } while(i != -1 && j != -1); if(i ! = j) System.out.println("Содержимое файлов отличается."); else System.out.println("Содержимое файлов совпадает."); } catch(IOException exc) { System.out.println("Ошибка ввода-вывода: " + exc); } } } 3. Чтобы испытать программу CompFiles, сначала скопируйте файл CompFiles.java в файл по имени temp, а затем введите следующую ко манду: java CompFiles CompFiles.java temp Программа сообщит, что содержимое файлов совпадает. Далее сравните файл CompFiles.java с ( показанным ранее ) файлом CopyFile.java с при менением такой команды: java CompFiles CompFiles.java CopyFile.java Программа CompFiles сообщит, что упомянутые файлы имеют отличающееся содержимое. 4. Попробуйте самостоятельно улучшить утилиту CompFiles, включив в нее новые возможности. Например, добавьте параметр для игнорирования регистра букв. Или же заставьте CompFiles отображать позицию в файле , в которой содержимое файлов отличается. Файлы с произвольным доступом — До этого момента использовались так называемые последовательные файлы файлы , доступ к которым осуществляется строго линейным образом , один байт за другим. Однако в Java также есть возможность обращаться к содержимому файла в произвольном порядке с применением класса RandomAccessFile, который инкапсулирует файл с произвольным доступом. Он не является производным от InputStream или OutputStream. Взамен класс RandomAccessFile реализует интерфейсы Datalnput и DataOutput, в которых определены основные методы ввода - вывода. Он также поддерживает запросы на позиционирование , т.е. указатель файла можно помещать в нужную позицию внутри файла . Вот конструктор, который будет использоваться: RandomAccessFile(String fileName, String access) throws FileNotFoundException Глава 10. Использование ввода -вывода 389 В fileName передается имя файла , а параметр access определяет, какой тип доступа к файлу разрешен. Если это " г " , то файл можно читать, но не запи сывать. Если это "rw", тогда файл открывается в режиме чтения- записи . ( Па раметр access также поддерживает "rws" и "rwd", которые (для локальных устройств) обеспечивают немедленную запись изменений данных файла на физическое устройство.) Показанный ниже метод seek ( ) используется для установки в файле текущей позиции указателя файла: void seek(long newPos) throws IOException В newPos задается новая позиция в байтах указателя файла, считая от начала файла. После вызова seek() следующая операция чтения или записи выпол нится в новой позиции файла. Поскольку класс RandomAccessFile реализует интерфейсы Datalnput и DataOuput, доступны методы для чтения и записи значений примитивных типов, такие как readlnt ( ) и writeDouble(). Кроме того, поддерживаются методы read( ) и write(). В следующем примере демонстрируется ввод- вывод с произвольным доступом. Сначала в файл записывается шесть значений double, которые затем читаются в непоследовательном порядке. // Демонстрация использования файлов с произвольным доступом. import java.io.*; class RandomAccessDemo ( public static void main(String[] args) { double[] data = { 19.4, 10.1, 123.54, 33.0, 87.9, 74.25 }; double d; // Открыть и использовать файл с произвольным доступом , try (RandomAccessFile raf = Открыть файл с new RandomAccessFile("random.dat", "rw")) произвольным доступом { // Записать значения в файл. for(int i=0; i < data.length; i++) { raf.writeDouble(data[i]); ^ } // Прочитать определенные значения. raf.seek( O ); // перейти на первое значение double d = raf.readDoubleO ; System.out.println("Первое значение: " + d); raf.seek(8); // перейти на второе значение double d = raf.readDoubleO ; System.out.println("Второе значение: " + d); - Использовать seek() для установки указателя файла raf.seek(8 * 3); // перейти на четвертое значение double d = raf.readDoubleO ; System.out.println("Четвертое значение: " + d); System.out.println(); 390 Java: руководство для начинающих, 9-е издание // Прочитать каждое второе значение. System.out.println("Каждое второе значение: "); for(int i=0; i < data.length; i+=2) { raf.seek(8 * i); // перейти на i-тое значение double d = raf.readDouble(); System.out.print(d + " "); } } catch(IOException exc) { System.out.println("Ошибка ввода-вывода: " + exc); } } } Ниже приведен вывод, генерируемый программой: Первое значение: 19.4 Второе значение: 10.1 Четвертое значение: 33.0 Каждое второе значение: 19.4 123.54 87.9 Обратите внимание на то , как находится позиция каждого значения. Поскольку каждое значение double имеет длину 8 байтов, оно начинается с 8-бай товой границы. Таким образом, первое значение располагается с нулевого бай та , второе — с 8- го байта , третье с 16 - го байта и т.д. Таким образом , чтобы прочитать четвертое значение , понадобится перейти в позицию 24. — Использование потоков Java, основанных на символах Как было показано в предыдущих разделах, потоки байтовых данных в Java являются мощными и гибкими. Тем не менее , они не считаются идеальным способом обработки ввода - вывода , основанного на символах. Для этой цели в Java определены классы потоков символьных данных. На вершине иерархии потоков символьных данных находятся абстрактные классы Reader и Writer. В табл . 10.7 кратко описаны методы класса Reader, а в табл . 10.8 методы класса Writer. Большинство методов в случае возникновения ошибки генери руют исключение IOException. Методы, определенные в абстрактных классах Reader и Writer, доступны для всех их подклассов. Таким образом , они формируют минимальный набор функций ввода - вывода , которые будут иметь все потоки символьных данных. — Глава 10. Использование ввода -вывода 391 Таблица 10.7. Методы, определенные в классе Reader Метод Описание abstract void close() Закрывает источник ввода. Дальнейшие попытки чтения приведут к генерации исключения IOException void mark(int numChars) boolean markSupported() static Reader nullReader() int read() Помещает метку в текущую позицию потока ввода, которая будет оставаться действительной до тех пор, пока не будет прочитано numChars символов Возвращает true, если методы mark()/reset() поддерживаются этим потоком Возвращает открытое, но пустое средство чтения, не содержащее данных. Таким образом, текущая позиция всегда находится в конце средства чтения, и никакие входные данные не могут быть получены. Однако средство чтения можно закрыть Возвращает целочисленное представление следующего доступного символа в вызывающем по- токе ввода. При попытке чтения в конце потока возвращает -1 int read(char[] buffer) int read(CharBuffer buffer) abstract int read( chart] buffer, int offset, int numChars) boolean ready() void reset() Пытается прочитать вплоть до buffer , length символов в буфер buffer и возвращает фактическое количество успешно прочитанных символов. При попытке чтения в конце потока возвращает -1 Пытается прочитать символы в буфер buffer и возвращает фактическое количество успешно прочитанных символов. При попытке чтения в конце потока возвращает -1 Пытается прочитать вплоть до numChars символов в буфер buffer, начиная с buffer[offset], и возвращает фактическое количество успешно прочитанных символов. При попытке чтения в конце потока возвращает -1 Возвращает true, если следующий запрос на ввод не будет ожидать, или false в противном случае Переустанавливает указатель ввода на ранее установленную метку long skip(long numChars) Пропускает numChars символов во входных данных и возвращает фактическое количество пропущенных символов long transferTo( Writer writer) Копирует содержимое вызывающего средства чтения и возвращает количество скопированных символов 392 Java : руководство для начинающих, 9- е издание Таблица 10.8 . Методы, определенные в классе Writer Метод Описание Writer append(char ch) Добавляет ch в конец вызывающего потока вывода. Возвращает ссылку на вызывающий поток Writer append( CharSequence chars) Writer append( CharSequence chars, int begin, int end) Добавляет chars в конец вызывающего потока вывода. Возвращает ссылку на вызывающий поток Добавляет поддиапазон chars, начинающийся с begin и заканчивающийся в end-1, в конец вызывающего потока вывода. Возвращает ссылку на вызывающий поток abstract void close() abstract void flush() static Writer nullWriterO Закрывает поток вывода. Дальнейшие попытки записи приведут к генерации исключения IOException Финализирует состояние вывода, так что любые буферы очищаются, т.е. сбрасывает буферы вывода Возвращает открытое, но пустое средство записи, куда никакие выходные данные фактически не за писывались. Таким образом, методы вывода могут быть вызваны, но они не производят какой-либо вывод. Однако средство записи можно закрыть void write(int ch) Записывает одиночный символ в вызывающий поток вывода. Обратите внимание, что параметр имеет тип int, позволяя вызывать метод с выражением без необходимости в приведении к типу char. Однако записываются только младшие 16 битов void write(char[] buffer) Записывает полный символьный массив в вызыва ющий поток вывода abstract void write( char[] buffer, int offset, int numChars) Записывает поддиапазон длиной numChars символов из буфера buffer типа массива, начиная с buffer[offset], в вызывающий поток вывода void write(String str) void write(String str, int offset, int numChars) Записывает str в вызывающий поток вывода Записывает поддиапазон длиной numChars символов из строки str, начиная с указанного в offset смещения Консольный ввод с использованием потоков символьных данных Для кода, который планируется интернационализировать, ввод с консоли с применением потоков символьных данных Java будет более эффективным и удобным способом чтения символов с клавиатуры по сравнению с использованием потоков байтовых данных. Тем не менее, поскольку System , in пред- ставляет собой поток байтовых данных, System in потребуется поместить в , Глава 10. Использование ввода -вывода 393 оболочку некоторого типа Reader. Лучшим классом для чтения ввода с консоли является BufferedReader, который поддерживает буферизованный поток ввода. Ниже показан часто применяемый конструктор: BufferedReader( Reader inputReader) В inputReader указывается поток , связанный с создаваемым экземпля ром BufferedReader. Сконструировать экземпляр BufferedReader непосредственно из System ,in нельзя, потому что System ,in это InputStream, а не Reader. Взамен его сначала придется преобразовать в поток символьных данных с использованием InputStreamReader. Начиная с версии JDK 17, точный способ получения объекта InputStreamReader, связанного с System ,in, изменился. В прошлом для этой цели обычно применялся следующий конструктор InputStreamReader: — InputStreamReader(InputStream inputStream) Поскольку System , in ссылается на объект типа InputStream, его можно указывать в аргументе inputStream. Таким образом, приведенная далее строка кода демонстрирует ранее широко используемый подход к созданию объекта BufferedReader, подключенного к клавиатуре: BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); После выполнения этого оператора переменная br становится символьным потоком, связанным с консолью через System ,in. Однако , начиная с JDK 17, при создании объекта InputStreamReader рекомендуется явно указывать набор символов, ассоциированный с консолью. Набор символов определяет способ сопоставления байтов с символами. Обычно, когда набор символов не задан, применяется стандартная кодировка машины JVM. Тем не менее, в случае консоли набор символов, используемый для консольного ввода, может отличаться от стандартного набора символов. Таким образом, теперь рекомендуется применять следующую форму конструктора InputStreamReader: InputStreamReader(InputStream inputStream, Charset charset) В аргументе charset должен использоваться набор символов , ассоцииро нового ванный с консолью , который возвращается с помощью charset ( ) метода , добавленного к классу Console в версии JDK 17. Объект Console получается вызовом метода System , console ( ) , который возвращает ссылку на консоль или null, если консоль отсутствует. Следовательно , теперь показанная ниже кодовая последовательность демонстрирует один из способов помещения System ,in в оболочку BufferedReader: — Console con = System.console(); // получить объект Console if(con==null) return; // возврат, если консоль отсутствует BufferedReader br = new BufferedReader(new InputStreamReader(System.in, con.charset())); Разумеется , в тех случаях, когда известно, что консоль будет присутствовать, последовательность можно сократить: 394 Java: руководство для начинающих, 9-е издание BufferedReader br = new BufferedReader(new InputStreamReader(System.in, System.console().charset())); Поскольку для запуска примеров, рассматриваемых в книге , вполне очевидно требуется консоль, мы будем применять именно такую форму. Чтение символов Символы можно читать из System,in с помощью метода read ( ) , определен ного в классе BufferedReader, почти так же , как они читались с использованием потоков байтовых данных. Ниже перечислены три версии read ( ) , поддерживаемые BufferedReader: int read() throws IOException int read(char[] data) throws IOException int read(chart ] data, int start, int max) throws IOException Первая версия read() читает один символ Unicode и возвращает -1 при попытке чтения в конце потока. Вторая версия читает символы из входного потока и помещает их в data до тех пор , пока массив не заполнится , не будет до стигнут конец потока или не произойдет ошибка. Возвращает количество прочитанных символов или -1 при попытке чтения в конце потока . Третья версия читает входные данные в data, начиная с места , которое указано в start. Она сохраняет вплоть до шах символов и возвращает количество прочитанных сим волов или -1 при попытке чтения в конце потока . Все версии в случае ошибки генерируют исключение IOException. В представленной далее программе демонстрируется работа метода read(), читающего символы с консоли до тех пор , пока пользователь не введет точку. Обратите внимание , что любые исключения ввода-вывода , которые могут возникнуть, обрабатываются вне main ( ) . Такой подход является обычным при чтении с консоли в простых примерах программ вроде приведенных в этой книге , но в более сложных приложениях исключения можно обрабатывать явно. // Использование BufferedReader для чтения символов с консоли. import java.io.*; class ReadChars { public static void main(String[] args) throws IOException { Создать экземпляр BufferedReader, char c; связанный c System ,in BufferedReader br = new BufferedReader( new InputStreamReader(System.in, System.console().charset())); System.out.println ("Вводите символы ; для выхода введите точку."); // Читать символы , do { с = (char) br.readO ; System.out.println(c); } while(c != '.'); } } Глава 10. Использование ввода -вывода 395 Вот результаты примера запуска программы: Вводите символы ; для выхода введите точку. One Two. О п е Т w о Чтение строк Для чтения строки с клавиатуры предназначена версия метода readLine(), которая является членом класса BufferedReader со следующей общей формой: String readLine() throws IOException Метод readLine ( ) возвращает объект String, содержащий прочитанные символы . Если предпринимается попытка чтения в конце потока , тогда возвра щается null. В приведенной далее программе демонстрируется использование объекта BufferedReader и метода readLine ( ) ; программа читает и отображает строки текста до тех пор , пока не будет введено слово stop: // Чтение строки с применением BufferedReader. import java.io.*; class ReadLines { public static void main(String[] args) throws IOException { // Создать экземпляр BufferedReader с использованием System.in. BufferedReader br = new BufferedReader(new InputStreamReader(System.in, System.console().charset())); String str; System.out.println("Вводите строки текста."); System.out.println("Для завершения введите stop."); do { str = br.readLine(); <Использовать метод readLine() из BufferedReader для чтения строки текста System.out.println (str); } while(!str.equals("stop")); } } 396 Java: руководство для начинающих, 9-е издание !?@>A8< C M:A?5@B0 ВОПРОС. В предыдущем обсуждении упоминался класс Console. Что еще можно сказать о нем? ОТВЕТ. Класс Console появился несколько лет назад ( в JDK 6) и применяется для чтения и записи на консоль. В первую очередь он создан ради удобства , потому что большая часть его функциональности доступна через System ,in и System ,out. Однако он может упростить некоторые типы взаимодействия с консолью, особенно при чтении строк с консоли. Конструкторы в классе Console отсутствуют. Ранее уже объяснялось, что объект Console получается путем вызова System ,console ( ) . Если консоль доступна , тогда возвращается ссылка на нее . В противном случае возвраща ется null. Консоль может быть доступна не во всех случаях, скажем в си туации, когда программа работает как фоновая задача. Следовательно , если возвращается значение null, то консольный ввод- вывод невозможен . Класс Console предлагает большой объем полезной функциональности , которую вам будет интересно изучить. Например, в нем определено несколько методов, выполняющих ввод-вывод, вроде readLine ( ) и printf ( ) , а также ме тод readPassword ( ) , который можно использовать для получения пароля . Он позволяет приложению читать пароль, не отображая вводимый текст. Начиная с версии JDK 17 , класс Console предоставляет метод charset ( ) , который получает кодировку, используемую консолью. Также можно получить ссылку на объекты Reader и Writer, которые подключены к консоли. Применение объекта Reader, полученного из Console, является альтернативой помещению System ,in в оболочку InputStreamReader. Тем не менее, в книге используется подход с InputStreamReader, поскольку он явно демонстрирует, как могут взаимодействовать потоки байтовых и символьных данных. Консольный вывод с использованием потоков символьных данных Хотя использовать поток System.out для вывода на консоль вполне допустимо , вероятно , его лучше всего применять для целей отладки или в примерах программ , подобных тем , которые можно найти в этой книге. В реальных программах на Java рекомендуется осуществлять вывод на консоль посредством потока PrintWriter одного из символьных классов. Использование сим - — вольного класса для консольного вывода упрощает интернационализацию программы. В классе PrintWriter определено несколько конструкторов; один из них , с которым мы будем иметь дело , показан ниже: PrintWriter(OutputStream outputStream, boolean flushingOn) Глава 10. Использование ввода -вывода 397 Здесь outputStream является объектом типа OutputStream, a flushingOn определяет, будет ли поток вывода очищаться при каждом вызове метода println ( ) ( среди прочих ). Если в аргументе flushingOn указано значение true, тогда очистка выполняется автоматически , а если false, то очистка автоматически не происходит. Класс PrintWriter поддерживает методы print ( ) и println ( ) . Таким образом , эти методы можно применять тем же способом , как они использовались с System.out. Если аргумент не относится к простому типу, тогда методы PrintWriter вызывают метод toString ( ) объекта и затем отображают результат. Чтобы выполнить запись в консоль с применением PrintWriter, понадобится указать System.out для потока вывода и автоматической очистки. Скажем , следующая строка кода создает объект PrintWriter, подключенный к консольному выводу: PrintWriter pw = new PrintWriter(System.out, true); В показанном далее приложении иллюстрируется использование объекта PrintWriter для обработки консольного вывода: // Демонстрация использования PrintWriter. import java.io.*; Создать объект PrintWriter, подключенный к System , out public class PrintWriterDemo { public static void main(String[] args) { PrintWriter pw = new PrintWriter(System.out, true); int i = 10; double d = 123.65; pw.println("Использование PrintWriter."); pw.println(i); pw.println(d); pw.println(i + " + " + d + " равно " + (i+d)); ) } Вот вывод, генерируемый программой: Использование PrintWriter. 10 123.65 10 + 123.65 равно 133.65 Помните, что нет ничего плохого в том, чтобы применять System.out для вывода простого текста на консоль , когда вы изучаете Java или отлаживаете свои программы. Однако использование PrintWriter облегчает интернацио нализацию реальных приложений. Из- за того, что применение PrintWriter в примерах программ , предложенных в книге , не дает никаких преимуществ , мы продолжим использовать System.out для выполнения вывода на консоль. 398 Java : руководство для начинающих, 9-е издание Файловый ввод- вывод с использованием потоков символьных данных Хотя обработка файлов , основанная на байтах , является наиболее распро страненной , для этой цели можно применять потоки символьных данных. Преимущество потоков символьных данных заключается в том , что они работают непосредственно с символами Unicode. Таким образом , если вы хотите хранить текст в формате Unicode , то потоки символьных данных, безусловно , окажутся наилучшим вариантом. В общем случае для выполнения файлового ввода- выво да на основе символов будут использоваться классы FileReader и FileWriter. Использование FileWriter Класс FileWriter создает объект Writer, который можно применять для записи в файл . Ниже показаны два часто используемых конструктора: FileWriter(String fileName ) throws IOException FileWriter(String fileName, boolean append) throws IOException В f i l e N a m e передается полное имя файла. Если append имеет значение true, тогда выходные данные добавляются в конец файла . В противном случае файл перезаписывается. Все они способны генерировать исключение IOException. Класс FileWriter является производным от OutputStreamWriter и Writer. Таким образом, FileWriter имеет доступ к методам , которые определены упомянутыми классами. Ниже показана простая утилита , которая читает строки текста , введенные с клавиатуры , и записывает их в файл по имени test.txt. Текст читается до тех пор, пока пользователь не введет слово stop. Для вывода в файл применяется класс FileWriter. // Простая утилита, которая читает строки текста, введенные с клавиатуры , // и записывает их в файл, демонстрируя использование FileWriter. import java.io.*; class KtoD { public static void main(String[] args) { String str; BufferedReader br = new BufferedReader(new InputStreamReader(System.in, System.console().charset())); System.out.println("Вводите текст (stop для завершения)."); try ( FileWriter fw = new FileWriter("test.txt")) Создать объект FileWriter { do { System.out.print(": ") str = br.readLine(); if(str.compareTo("stop") == 0) break; str = str + "\r\n"; // добавить символ новой строки fw.write(str); Записать строки в файл * } while(str.compareTo("stop") != 0); } Глава 10. Использование ввода -вывода 399 catch(IOException exc) { System.out.println("Ошибка ввода-вывода: " + exc); } } } Использование FileReader Класс FileReader создает средство чтения , которое можно применять для чтения содержимого файла . Вот часто используемый конструктор класса FileReader: FileReader(String fileName ) throws FileNotFoundException В fileName указывается полное имя файла. Если файл не существует, тогда генерируется исключение FileNotFoundException. Класс FileReader является производным от InputStreamReader и Reader. Таким образом, FileReader имеет доступ к методам , которые определены указанными классами . В следующем примере создается простая утилита , которая читает содер жимое текстового файла по имени test.txt и отображает его содержимое на экране. Ее можно считать дополнением к утилите , показанной в предыдущем разделе. // Простая утилита, которая читает содержимое текстового файла, // и отображает его на экране, демонстрируя использование FileWriter. import java.io.*; class DtoS { public static void main(String[] args) { String s; // Создать и использовать объект FileReader, // помещенный в оболочку BufferedReader. Создать объект FileReader try (BufferedReader br = < new BufferedReader(new FileReader("test.txt"))) { while((s = br.readLine()) != null) { Читать строки из файла и отображать System.out.println(s); их на экране } } catch(IOException exc) { System.out.println ("Ошибка ввода-вывода: " + exc); } } } Обратите внимание, что в этом примере объект FileReader заключен в оболочку BufferedReader. В итоге он получает доступ к методу readLine ( ) . Кроме того , закрытие BufferedReader(br в данном случае ) приводит к автоматическому закрытию файла. 400 Java : руководство для начинающих, 9-е издание !?@>A8< C M:A?5@B0 ВОПРОС. Что собой представляет еще один пакет ввода- вывода под названием N 10? ОТВЕТ. Первоначально называвшийся New I/O ( новый ввод- вывод) , пакет N 10 появился в Java несколько лет назад. Он поддерживает подход на базе каналов к операциям ввода - вывода. Классы N 10 содержатся в java . nio и его подчиненных пакетах вроде j ava . nio . channels и j ava . nio charset . . Система NIO построена на основе двух фундаментальных элементов: буферов и каналов. Буфер содержит данные . Канал представляет собой откры тое подключение к устройству ввода - вывода , такому как файл или сокет. В общем случае для использования новой системы ввода - вывода необходи мо получить канал к устройству ввода- вывода и буфер для хранения данных. Затем можно работать с буфером , вводя или выводя данные по мере необходимости . Другими двумя сущностями N 10 являются наборы символов и селекторы . Набор символов определяет способ сопоставления байтов с символами . Закодировать последовательность символов в байты позволяет кодировщик , а декодировать последовательность байтов в символы - декодировщик . Селектор поддерживает неблокирующий мультиплексированный вводвывод на основе ключей . Другими словами , селекторы позволяют выпол нять ввод- вывод по нескольким каналам . Селекторы наиболее применимы к каналам , поддерживаемым сокетами . В версии JDK 7 система N10 была существенно улучшена , причем настолько , что для ее обозначения зачастую используется термин N10.2. Улучшения включали три новых пакета ( j ava . nio . f i l e , j ava . nio . f i l e . attribute и java . nio f i l e . spi ) , несколько новых классов , интерфейсов и методов , а также прямую поддержку потокового ввода - вывода . Дополнения значительно расширили возможности применения N 10 , особенно с файлами . Важно понимать, что N 10 не заменяет классы ввода-вывода из j ava . io , которые обсуждаются в этой главе . Напротив , классы N 10 предназначены для дополнения стандартной системы ввода - вывода , предлагая альтернативный подход , который может быть полезен в определенных обстоятельствах . . Использование оболочек типов Java для преобразования числовых строк Прежде чем завершить обсуждение ввода - вывода , имеет смысл рассмо треть прием , полезный при чтении числовых строк. Как вам известно , ме тод p r i n t l n ( ) предлагает удобный способ вывода на консоль различных типов данных , включая числовые значения встроенных типов , таких как i n t и double . Таким образом , метод println ( ) автоматически преобразует число вые значения в удобочитаемую форму. Тем не менее , методы вроде read ( ) не Глава 10. Использование ввода -вывода 401 обеспечивают параллельную функциональность чтения и преобразования строки , содержащей числовое значение, во внутренний двоичный формат. Скажем , не существует версии read ( ) , которая читала бы строку, например , "100", и затем автоматически преобразовывала бы ее в соответствующее двоичное значе ние , которое можно сохранить в переменной типа int . Взамен Java предоставляет другие способы решения такой задачи. Возможно, проще всего применять одну из оболочек типов Java. Оболочки типов Java представляют собой классы , которые инкапсулируют примитивные типы. Оболочки типов необходимы из-за того , что примитивные типы не являются объектами , что несколько ограничивает их использование. Например, примитивный тип нельзя передавать по ссылке. Для удовлетворения таких потребностей в Java предусмотрены классы, соответствующие каждому примитивному типу. В состав оболочек типов входят классы Double, Float, Long, Integer, Short, Byte, Character и Boolean, которые предлагают широкий набор методов, позволяющих полностью интегрировать примитивные типы в иерархию объектов Java. В качестве дополнительного преимущества оболочки также определяют методы, которые преобразуют числовую строку в соответствующий двоичный экви валент. Некоторые из этих методов преобразования кратко описаны в табл . 10.9. Каждый метод возвращает двоичное значение, соответствующее строке. Таблица 10.9. Методы преобразования числовых строк в их двоичные эквиваленты Оболочка Метод преобразования Double Float Long Integer Short Byte static static static static static static double parseDouble(String str) throws NumberFormatException float parseFloat(String str) throws NumberFormatException long parseLong(String str) throws NumberFormatException int parselnt(String str) throws NumberFormatException short parseShort(String str) throws NumberFormatException byte parseByte(String str) throws NumberFormatException Оболочки целочисленных типов также предлагают еще один метод синтаксического анализа, который позволяет указывать основание системы счисления. Методы синтаксического анализа предоставляют легкий способ преобра зования числового значения , прочитанного в виде строки с клавиатуры либо из текстового файла , в надлежащий внутренний формат. Например , в приведенной далее программе демонстрируется применение методов parselnt ( ) и parseDouble ( ) . В ней вычисляется среднее значение для списка чисел , введенных пользователем . Сначала у пользователя запрашивается количество значений . Затем это число читается с помощью readLine ( ) и посредством parselnt ( ) строка преобразуется в целое число. Наконец, значения вводятся и с использованием метода parseDouble { ) строки преобразуются в их эквива ленты типа double. 402 Java: руководство для начинающих, 9-е издание // В этой программе вычисляется среднее значение // для списка чисел, введенных пользователем , import java.io.*; class AvgNums { public static void main(String[] args) throws IOException { // Создать экземпляр BufferedReader с использованием System.in. BufferedReader br = new BufferedReader(new InputStreamReader(System.in, System.console().charset())); String str; int n; double sum = 0.0; double avg, t; System.out.print("Сколько чисел вы будете вводить: "); str = br.readLine(); try { n = Integer.parselnt(str); Преобразовать строку в значение типа int } catch( NumberFormatException exc) { System.out.println("Недопустимый формат."); n = 0; } System.out.println("Введите " + n + " значений."); for(int i=0; i < n ; i++) { System.out.print(": "); str = br.readLine(); try { Преобразовать строку в значение типа double t = Double.parseDouble(str); } catch(NumberFormatException exc) { System.out.println("Недопустимый формат."); t = 0.0; } sum += t; } avg = sum / n; System.out.println("Среднее значение равно " + avg); } } Ниже показаны результаты одного из запусков программы: Сколько чисел вы будете вводить: 5 Введите 5 значений. : 1.1 : 2.2 : 3.3 : 4.4 : 5.5 Среднее значение равно 3.3 Глава 10. Использование ввода -вывода 403 !?@>A8< C M:A?5@B0 ВОПРОС. Что еще могут делать классы оболочек примитивных типов? ОТВЕТ. Оболочки примитивных типов предоставляют ряд методов, помогающих интегрировать примитивные типы в объектную иерархию. Скажем , разнообразные механизмы хранения, обеспечиваемые библиотекой Java, включая карты, списки и наборы, работают только с объектами. Таким образом , чтобы сохранить значение типа int, например, в списке, его понадобится поместить внутрь объекта. Кроме того, все оболочки типов имеют метод по имени compareTo ( ) , который сравнивает значение , содержащееся внутри оболоч ки , метод equals ( ) , который проверяет два значения на предмет равенства , и методы , возвращающие значение объекта в различных формах. Тема оболочек типов будет продолжена в главе 12 при обсуждении автоупаковки. Упражнение 10.2 Создание справочной системы, использующей диск В упражнении 4.1 был создан класс Help, отображающий информацию об управляющих операторах Java. В той реали зации справочная информация хранилась внутри самого класса , и пользователь выбирал необходимые сведения в меню с пронумерованными вариантами. Хотя такой подход был полностью функциональным , он определенно не является идеальным способом создания справочной системы. Например , для добавления либо изменения справочной информации необходимо было модифицировать исходный код программы. Кроме того , выбор темы по номеру, а не по названию , утомителен и не подходит для длинных списков тем. В данном упражнении указанные недостатки будут устранены путем создания справочной системы , использующей диск. Справочная система , применяющая диск, хранит информацию в справочном файле, представляющем собой стандартный текстовый файл , который по жела нию можно изменять или расширять без изменения кода программы. Пользователь получает справку по теме , вводя ее название. Справочная система ищет запрошенную тему в справочном файле. Если она найдена , то отображается соответствующая информация. FileHelp.java . 1 Создайте справочный файл , который будет использоваться справочной системой. Он представляет собой стандартный текстовый файл , организованный следующим образом: #название-темы 1 информация по теме #название-темы 2 информация по теме 404 Java : руководство для начинающих, 9-е издание # название-темыИ информация по теме Название каждой темы должно предваряться символом # и располагаться в отдельной строке. Символ # перед названием темы позволяет быстро находить ее начало. После названия темы следует любое количество строк информации по теме. После окончания информации по одной теме и перед началом информации по другой теме необходимо помещать пустую строку. В конце любой строки справочной информации по теме не должно быть пробелов. Ниже показано содержимое простого справочного файла , который можно применять для опробования справочной системы, использующей диск. В нем хранятся сведения об управляющих операторах Java. #if if { условие ) оператор; else оператор; fswitch switch ( выражение ) case константа: { // традиционная форма последовательность операторов break; // ... } #for for { инициализация ; условие ; итерация ) оператор; twhile while { условие ) оператор; #do do { оператор; } while ( условие); #break break; или break метка; #continue continue; или continue метка; Назовите файл helpfile . txt . 2. Создайте файл по имени FileHelp.java. 3. Начните создание нового класса Help со следующих строк кода: class Help { String helpfile; // имя справочного файла Help(String fname ) { helpfile = fname; } Глава 10. Использование ввода -вывода 405 Имя справочного файла передается конструктору класса Help и сохраняется в переменной экземпляра helpfile. Поскольку каждый экземпляр класса Help будет иметь собственную копию справочного файла , каждый экземпляр может использовать свой файл . В итоге можно создавать раз личные наборы справочных файлов для разных наборов тем . Добавьте в класс Help приведенный ниже метод helpOn ( ) , который из4. влекает справочную информацию по заданной теме: // Отображение справочной информации по заданной теме , boolean helpOn(String what) { int ch; String topic, info; // Открыть справочный файл , try ( BufferedReader helpRdr = new BufferedReader(new FileReader(helpfile))) { do { // Читать символы , пока не встретится #. ch = helpRdr.read(); // Проверить, соответствует ли тема , if(ch '#') { topic = helpRdr.readLine(); if(what.compareTo( topic) == 0) { // Тема найдена , do { info = helpRdr.readLine(); if(info != null) System.out.println(info); } while((info ! = null) && (info.compareTo("") != 0)); return true; } } } while(ch != -1); } catch(IOException exc) ( System.out.println("Ошибка доступа к справочному файлу."); return false; } return false; // тема не найдена } — Первое , на что следует обратить внимание метод helpOn ( ) самостоятельно обрабатывает все возможные исключения ввода - вывода и не включает конструкцию throws. Обрабатывая собственные исключения , он предотвращает возложение этого бремени на весь код, который его использует. Таким образом , другой код может просто вызывать helpOn ( ) без необходимости помещать вызов внутрь блока try/catch. 406 Java: руководство для начинающих, 9-е издание Справочный файл открывается с помощью объекта FineReader, помещенного в оболочку BufferedReader. Поскольку справочный файл содержит текст, применение потока символьных данных позволяет более эффективно интернационализировать справочную систему. Рассмотрим работу метода helpOn ( ) . В параметре what передается строка , содержащая название темы . Затем открывается справочный файл , в котором производится поиск соответствия значения what темам , записанным в файле. Не забывайте , что названию каждой темы в файле предшествует символ # , поэтому именно он ищется в файле. После нахождения символа # выполняется проверка , совпадает ли название темы , следующей после # , с названием темы , переданной в what. В случае совпадения отображается информация , связанная с этой темой . Если совпадение обнаружено , тогда метод helpOn ( ) возвращает true, или false в противном случае . 5. Класс Help также предоставляет метод getSelection ( ) , который запрашивает у пользователя название темы и возвращает строку, введенную пользователем . // Получение справочной темы . String getSelection() { • String topic = BufferedReader hr = new BufferedReader( new InputStreamReader(System.in, System.console().charset())); System.out.print("Введите название темы : "); try { topic = br.readLine(); IV VI } catch(IOException exc) { System.out.println("Ошибка при чтении с консоли."); } return topic; } Метод getSelection ( ) создает объект BufferedReader, присоединенный к System , in. Затем он запрашивает название темы , вводит его и возвра щает вызывающему коду. 6. Ниже приведен полный код справочной системы , использующей диск: /* Упражнение 10.2. Справочная система, которая использует диск для хранения справочной информации. */ import java.io.*; /* Класс Help открывает справочный файл, выполняет поиск раздела, а затем отображает информацию, связанную с этим разделом. Обратите внимание, что он самостоятельно обрабатывает все исключения ввода-вывода, избегая необходимости делать это в вызывающем коде. */ Глава 10. Использование ввода -вывода 407 class Help { String helpfile; // имя справочного файла Help(String fname) { helpfile = fname; } // Отображение справочной информации по заданной теме , boolean helpOn(String what) { int ch; String topic, info; // Открыть справочный файл , try (BufferedReader helpRdr = new BufferedReader( new FileReader(helpfile))) { do { // Читать символы , пока не встретится #. ch = helpRdr.read(); // Проверить, соответствует ли тема. if(ch == '# ’) { topic = helpRdr.readLine(); if(what.compareTo(topic) == 0) { // Тема найдена , do { info = helpRdr.readLine(); if(info != null) System.out.println(info); } while((info != null) && (info.compareTo("") != 0)); return true; } } } while(ch != -1); } catch(IOException exc) { System.out.println("Ошибка доступа к справочному файлу."); return false; } return false; // тема не найдена } } // Получение справочной темы . String getSelection( ) { String topic = м и. BufferedReader br = new BufferedReader( new InputStreamReader(System.in, System.console().charset())); System.out.print("Введите название темы : "); try { topic = br.readLine(); } catch(IOException exc) { System.out.println("Ошибка при чтении с консоли."); } return topic; } 408 Java: руководство для начинающих, 9-е издание // Демонстрация работы справочной системы , использующей диск , class FileHelp { public static void main(String[] args ) { Help hlpobj = new Help("helpfile.txt"); String topic; System.out.println("Демонстрация работы справочной системы . " + "Для завершения введите stop."); do { topic = hlpobj.getSelection(); if( ! hlpobj.helpOn(topic)) System.out.println("Тема не найдена.\n"); } while(topic.compareTo("stop") != 0); } } !?@>A8< C M:A?5@B0 ВОПРОС. Помимо методов синтаксического анализа, которые определены в оболочках примитивных типов, есть ли другой простой способ преобразования числовой строки , введенной с клавиатуры, в эквивалентный ей двоичный формат? ОТВЕТ. Да! Еще один способ преобразования числовой строки в ее внутренний двоичный формат предусматривает применение одного из методов, определенных в классе Scanner из пакета java.util. Класс Scanner читает форматированные (т.е . удобочитаемые ) входные данные и преобразует их в двоичную форму. Его можно использовать для чтения входных данных из различных источников, включая консоль и файлы . Таким образом , с помощью класса Scanner можно прочитать числовую строку, введенную с клавиатуры , и присвоить ее значение переменной . Хотя Scanner содержит слишком много средств, чтобы описать их здесь подробно, ниже демонстрируется его основное применение . Чтобы использовать класс Scanner для чтения с клавиатуры , сначала понадобится создать экземпляр Scanner, связанный с консольным вводом. Вот конструктор, который будет применяться: Scanner(InputStream from) Этот конструктор создает экземпляр Scanner, использующий поток , который указан в параметре from, в качестве источника ввода. Данный кон структор можно применять для создания экземпляра Scanner, связанного с консольным вводом: Scanner conin = new Scanner(System.in); Глава 10. Использование ввода -вывода 409 Прием работает, потому что System ,in является объектом типа InputStream. После выполнения этой строки conin можно использовать для чтения входных данных с клавиатуры . После создания экземпляр Scanner легко применять для чтения числовых входных данных. Ниже описана общая процедура. 1. Выясните , доступен ли определенный тип ввода , вызвав один из методов hasNextXO класса Scanner, где X тип требуемых данных. 2. Если входные данные доступны , тогда прочитайте их, вызвав один из методов nextX ( ) класса Scanner. Как видите, в классе Scanner определены два набора методов, позволяющих читать входные данные. Первый набор методы hasNextXO , куда входят такие методы , как hasNextlnt ( ) и hasNextDouble(). Каждый из методов hasNextX ( ) возвращает true, если данные желаемого типа являются следующим доступным элементом в потоке данных, или false в противном случае. Например, вызов hasNextlnt ( ) возвращает true, только если следующий элемент в потоке представляет собой удобочитаемую форму целого числа. Если нужные данные доступны, то их можно прочитать, вызвав один из методов nextX() класса Scanner, например, nextlnt ( ) или nextDouble ( ) . Такие методы преобразуют удобочитаемую форму данных во внутреннее двоичное представление и возвращают результат. Например, чтобы прочи тать целое число, понадобится вызвать nextlnt ( ). В следующем коде показано , как прочитать целое число с клавиатуры: — — Scanner conin = new Scanner(System.in); int i; if (conin.hasNextlnt()) i = conin.nextlnt(); В случае ввода с клавиатуры числа 123 переменная i будет содержать значение 123. Формально метод nextX() можно вызывать без предварительного вызова метода hasNextXO , но поступать так обычно не рекомендуется. Если методу nextX ( ) не удается найти тип искомых данных, тогда он генерирует исключение InputMismatchException. По этой причине лучше всего сначала удостовериться , что нужный тип данных доступен, вызвав метод hasNextX ( ) перед вызовом соответствующего метода nextX ( ). 410 Java : руководство для начинающих, 9-е издание Вопросы и упражнения для самопроверки 1. Почему в Java определены потоки байтовых и символьных данных? 2 Несмотря на то что консольный ввод и вывод основан на тексте , почему в Java по- прежнему используются для этой цели потоки байтовых данных? . 3. Покажите, как открыть файл для чтения байтов. 4. Покажите , как открыть файл для чтения символов. 5 Покажите, как открыть файл для ввода- вывода с произвольным доступом. . 6. Как можно преобразовать числовую строку вроде "123.23" в ее двоичный эквивалент? 7. Напишите программу, которая копирует текстовый файл . Пусть в процессе она преобразует все пробелы в дефисы. Воспользуйтесь классами потоков байтовых данных для файлов. Примените традиционный подход к закрытию файла , явно вызывая метод close ( ) . 8. Перепишите программу из предыдущего вопроса так , чтобы в ней использовались классы потоков символьных данных. На этот раз примените оператор try с ресурсами для автоматического закрытия файла . 9. Каким типом потока является System , in ? 10. Что возвращает метод read ( ) класса InputStream при попытке чтения в конце потока? 11. Какой тип потока используется для чтения двоичных данных? 12 Классы Reader и Writer находятся на вершине иерархий классов . 13. Оператор try с ресурсами применяется для . 14. Верно ли утверждение , что если используется традиционный способ за крытия файла , то закрытие файла в блоке finally обычно будет удачным подходом ? 15. Можно ли применять выведение типа локальной переменной при объявлении ресурса в операторе try с ресурсами? Глава 11 Многопоточное программирование 412 Java: руководство для начинающих, 9-е издание В этой главе • • • z Основы многопоточности z Класс Thread и интерфейс Runnable z Создание потока z Создание множества потоков z Выяснение , когда поток заканчивает работу z Использование приоритетов потоков z Синхронизация потоков z Применение синхронизированных методов z Использование синхронизированных блоков z Взаимодействие между потоками z Приостановка, возобновление и останов потоков н есмотря на наличие в Java множества инновационных функциональных средств , одним из наиболее интересных является встроенная поддержка многопоточного программирования . Многопоточная программа состоит из двух и более частей , которые способны выполняться одновременно. Каждая часть такой программы называется потоком, и каждый поток определяет отдельный путь выполнения. Таким образом, многопоточность это специализированная форма многозадачности. — Основы многопоточности Существуют два разных типа многозадачности: на основе процессов и на основе потоков. Важно понимать разницу между ними. Многим читателям больше знакома многозадачность, основанная на процессах. По сути , процесс это программа , которая выполняется. Таким образом , многозадачность на основе процессов является функциональным средством , которое позволяет вашему ком пьютеру запускать две или большее число программ параллельно. Например , многозадачность на основе процессов позволяет запускать компилятор Java одновременно с использованием текстового редактора или посещением веб-сайта. В многозадачности , основанной на процессах, программа представляет собой наименьшую единицу кода , которая может координироваться планировщиком. В многозадачной среде на основе потоков поток является наименьшей еди ницей координируемого кода , т.е . одна программа может выполнять две или более задач одновременно. Например, текстовый редактор может форматировать — Глава 1 1 . Многопоточное программирование 413 текст одновременно с его выводом на печать, если эти два действия выполняются двумя отдельными потоками. Хотя программы на Java задействуют многозадачные среды, основанные на процессах, многозадачность на основе процессов не находится под контролем Java. Остается многозадачность на основе потоков. Принципиальное преимущество многопоточности связано с тем, что она по зволяет писать очень эффективные программы , поскольку дает возможность утилизировать время простоя, присутствующее в большинстве программ. Как вам наверняка известно , большинство устройств ввода- вывода , будь то сетевые порты, жесткие диски или клавиатура , работают намного медленнее центрального процессора ( ЦП ). Таким образом , программа часто тратит большую часть времени своего выполнения на ожидание отправки информации на устройство или получения ее с устройства. За счет применения многопоточности програм ма может выполнять другую задачу во время простоя . Например , пока одна часть программы отправляет файл через Интернет, другая часть может читать входные данные с клавиатуры , а третья буферизировать очередной блок данных для отправки . Как известно большинству читателей , за последние несколько лет многоя дерные системы стали обычным явлением. Конечно , одноядерные системы попрежнему широко применяются. Важно понимать, что многопоточность Java работает в системах обоих типов. В одноядерной системе одновременно вы полняющиеся потоки совместно используют ЦП , при этом каждый поток получает долю процессорного времени. Таким образом , в одноядерной системе два или более потока фактически не выполняются одновременно, но задействуется время простоя ЦП. Однако в многоядерных системах возможно одновременное выполнение двух или более потоков. Во многих случаях удается еще больше по высить эффективность программы и увеличить скорость выполнения определенных операций. Потоки пребывают в нескольких состояниях. Поток может выполняться. Он может быть готовым к запуску, как только получит время ЦП. Работающий поток может быть приостановлен , в результате чего он временно прекращает свою активность. Выполнение приостановленного потока затем можно возобновить, позволяя ему продолжиться с того места , где он остановился. Поток может быть заблокирован при ожидании ресурса. В любой момент работа потока может быть прекращена, что немедленно останавливает его выполнение . После прекращения работы выполнение потока не может быть возобновлено. Наряду с многозадачностью, основанной на потоках, возникает потребность в особом виде функционального средства , называемого синхронизацией, которое позволяет координировать выполнение потоков четко определенными способа ми. В Java имеется полная подсистема , предназначенная для синхронизации , и ее основные функции описаны далее в главе. Если вы программировали для таких ОС, как Windows , то уже знакомы с многопоточным программированием. Однако тот факт, что Java управляет по токами, делает многопоточность особенно удобной , потому что многие детали реализуются автоматически . — 414 Java: руководство для начинающих, 9-е издание Класс Thread и интерфейс Runnable Многопоточная система Java построена на базе класса Thread, его методов и дополняющего интерфейса Runnable. Оба типа находятся в пакете java.lang. Класс Thread инкапсулирует поток выполнения. Для создания нового потока понадобится либо расширить класс Thread, либо реализовать интерфейс Runnable. В классе Thread определено несколько методов, помогающих управлять потоками . Некоторые из наиболее распространенных методов кратко описаны в табл . 11.1. Таблица 11.1 . Методы, помогающие управлять потоками Метод Описание final String getName() final int getPriority() final boolean isAliveO final void join() void run() static void sleep( long milliseconds) Получает имя потока Получает приоритет потока Определяет, выполняется ли поток Ожидает прекращения работы потока void start() Запускает поток вызовом его метода run() Устанавливает точку входа в поток Приостанавливает выполнение потока на указанное время Все процессы имеют, по меньшей мере , один поток выполнения , который обычно называется главным потоком , поскольку он выполняется при запуске программы . Таким образом , именно главный поток использовался во всех предшествующих примерах программ. Из главного потока можно создавать другие потоки. Создание потока Поток создается путем создания объекта типа Thread. Класс Thread инкап сулирует исполняемый объект. Как уже упоминалось, в Java предусмотрены два способа создания исполняемых объектов: # реализация интерфейса Runnable; # расширение класса Thread. В большинстве примеров , приводимых в главе , будет применяться подход с реализацией интерфейса Runnable. Тем не менее, в упражнении 11.1 показано, каким образом создать поток за счет расширения класса Thread. Не забывайте о том , что в обоих подходах для создания , доступа и управления потоком используется класс Thread. Отличие лишь в том, каким способом создается потоковый класс. Глава 1 1 . Многопоточное программирование 415 Интерфейс Runnable абстрагирует единицу исполняемого кода. Поток можно конструировать для любого объекта , реализующего Runnable. В интерфейсе Runnable определен всего лишь один метод по имени run( ) со следующим объявлением: public void run() Внутрь метода run ( ) помещается код, который основывает новый поток. Важно понимать, что run ( ) может вызывать другие методы , использовать другие классы и объявлять переменные в точности , как это делает главный поток. Единственное отличие заключается в том , что метод run( ) устанавливает точку входа для другого параллельного потока выполнения в программе. Этот поток завершится , когда управление возвратится из run(). После создания класса, реализующего интерфейс Runnable, внутри него создается объект типа Thread. В классе Thread определено несколько конструкторов. Вот тот, который будет применяться: Thread( Runnable threadOb) В приведенном конструкторе threadOb является экземпляром класса , реали зующего интерфейс Runnable. Он определяет, где начнется выполнение потока. После создания новый поток не будет запущен, пока не вызовется его метод start ( ) , объявленный в классе Thread. По существу start ( ) инициирует вы зов run ( ) . Метод start ( ) показан ниже: void start() Далее приведен пример создания нового потока и запуска его на выполнение: // Создание потока путем реализации интерфейса Runnable. class MyThread implements Runnable { String thrdName; MyThread(String name ) { thrdName = name; Объекты MyThread могут выполняться в собственных потоках, потому что класс MyThread реализует Runnable } // Точка входа в поток , public void run() { Здесь потоки начинают выполнение System.out.println("Поток " + thrdName + " запущен."); try { for(int count=0; count < 10; count++) { Thread.sleep(400); System.out.println("В потоке " + thrdName + " значение count равно " + count); } } catch(InterruptedException exc) { System.out.println("Поток " + thrdName + " прерван."); } System.out.println("Поток " + thrdName + " завершен."); } } 416 Java: руководство для начинающих, 9- е издание class UseThreads { public static void main(String[] args) { System.out.println("Главный поток запущен."); // Сначала сконструировать объект MyThread. MyThread mt = new MyThread("Child #1"); Создать исполняемый объект // Затем сконструировать поток из этого объекта. Thread newThrd = new Thread(mt); + Сконструировать поток на основе * // Наконец запустить поток на выполнение. newThrd.start(); этого объекта Запустить поток на выполнение for(int i=0; i<50; i++) { System.out.print("."); try { Thread.sleep(100); } catch(InterruptedException exc) { System.out.println("Главный поток прерван."); } } System.out.println("Главный поток завершен."); } } Давайте внимательно взглянем на код программы. Прежде всего , класс MyThread реализует интерфейс Runnable, т.е. объект типа MyThread подходит для использования в качестве потока и может быть передан конструктору Thread. Внутри метода run ( ) организуется цикл , который считает от 0 до 9. Обратите внимание на вызов sleep( ) . Метод sleep ( ) заставляет поток, в котором он вызывается, приостанавливать выполнение на указанный период в миллисекун дах. Вот его общая форма: static void sleep(long milliseconds) throws InterruptedException Количество миллисекунд для приостановки указывается в milliseconds. Данный метод может сгенерировать исключение InterruptedException. Таким образом , его вызовы должны быть заключены в блок try. Метод sleep() также имеет вторую форму, которая позволяет указывать период в миллисекун дах и наносекундах, если нужен такой уровень точности. В методе run( ) вызов sleep ( ) приостанавливает поток на 400 миллисекунд на каждой итерации цикла, позволяя потоку работать достаточно медленно, чтобы вы могли наблюдать за его выполнением . В методе main ( ) новый объект Thread создается с помощью следующей последовательности операторов: // Сначала сконструировать объект MyThread. MyThread mt = new MyThread("Child #1"); // Затем сконструировать поток из этого объекта. Thread newThrd = new Thread(mt); // Наконец, запустить поток на выполнение. newThrd.start(); Глава 1 1 . Многопоточное программирование 417 Как указано в комментариях , сначала создается объект MyThread, который затем применяется для создания объекта Thread. Это возможно , поскольку класс MyThread реализует интерфейс Runnable. Наконец, новый поток запускается на выполнение вызовом start ( ) , что приводит к запуску метода run() дочернего потока . После вызова start ( ) выполнение возвращается в main ( ) и входит в цикл for внутри main ( ) . Обратите внимание, что данный цикл повторяется 50 раз , каждый раз делая паузу в 100 миллисекунд. Оба потока продол жают работать, совместно используя ЦП в однопроцессорных системах, пока их циклы не завершатся. Ниже показан вывод программы ( из-за различий между вычислительными средами точный вывод может немного отличаться ): Главный поток запущен. .Поток Child #1 запущен. ...В потоке Child #1 значение count равно 0 ....В потоке Child #1 значение count равно 1 ....В потоке Child #1 значение count равно 2 ... В потоке Child #1 значение count равно 3 .... В потоке Child #1 значение count равно 4 ....В потоке Child #1 значение count равно 5 ....В потоке Child #1 значение count равно 6 .. . В потоке Child #1 значение count равно 7 ....В потоке Child #1 значение count равно 8 ....В потоке Child #1 значение count равно 9 Поток Child #1 завершен. Главный поток завершен. В первом примере с многопоточностью нужно отметить еще один интересный момент. Чтобы проиллюстрировать тот факт, что главный поток и mt вы полняются одновременно, необходимо предотвратить завершение main ( ) до тех пор, пока не завершится mt. Здесь это делается за счет разницы во времени между двумя потоками. Поскольку вызовы sleep ( ) внутри цикла for в main ( ) вызывают общую задержку в 5 секунд ( 50 итераций умножить на 100 миллисекунд), но общая задержка в цикле run ( ) составляет всего 4 секунды ( 10 итераций ум ножить на 400 миллисекунд), то run ( ) завершится примерно за 1 секунду до завершения main ( ) . В итоге главный поток и mt будут выполняться одновременно, пока mt не завершится. Затем примерно через 1 секунду завершается main ( ). Хотя использования разницы во времени для обеспечения того, чтобы метод main ( ) завершился последним , достаточно для такого простого примера , это не то , что обычно применяется на практике. Язык Java предоставляет гораздо лучшие способы ожидания завершения потока. Далее в главе вы увидите более эффективный способ ожидания одного потока до завершения другого. И еще один момент: в многопоточной программе часто требуется , чтобы главный поток был последним потоком , завершающим выполнение. Как правило , программа продолжает функционировать до тех пор , пока не закончат работу все ее потоки. Таким образом, завершение главного потока последним жестким требованием не является , но часто рекомендуется следовать этому правилу, особенно когда вы только начинаете изучать потоки. 41 8 Java: руководство для начинающих, 9-е издание !?@>A8< C M:A?5@B0 ВОПРОС. Почему главный поток в многопоточной программе необходимо за вершать последним? ОТВЕТ. Главный поток - удобное место для аккуратного завершения работы программы , например, для закрытия файлов. Он также обеспечивает четко определенную точку выхода для программы. Поэтому часто имеет смысл завершать его последним. К счастью, как вскоре будет показано, в главном потоке очень легко дождаться завершения дочерних потоков. Одно улучшение и два простых изменения В предыдущей программе демонстрировались основы создания объекта Thread на базе реализации Runnable с последующим запуском потока. Примененный в ней подход вполне оправдан и часто будет именно тем , что нужно. Однако два простых изменения могут сделать класс MyThread более гибким и простым в использовании в ряде случаев. Кроме того , вы можете обнаружить, что эти изменения полезны при создании собственных классов, реализующих Runnable. Также в MyThread можно внести одно существенное улучшение , которое задействует преимущества другой возможности класса Thread. Давайте начнем с улучшения. Обратите внимание , что в предыдущей программе переменная экземпляра по имени thrdName определяется в классе MyThread и применяется для хранения имени потока. Тем не менее , в MyThread нет необходимости хранить имя потока , поскольку его можно назначить при его создании , что позволяет сде лать следующая версия конструктора класса Thread: Thread( Runnable threadOb, String name) В итоге name становится именем потока. Для получения имени потока можно вызвать метод getName ( ) , определенный в Thread: final String getName() Назначение имени потоку при его создании обеспечивает два преимущества . Во- первых , не придется использовать отдельную переменную для хранения имени , поскольку Thread уже предоставляет такую возможность. Во- вторых, имя потока будет доступно любому коду, содержащему ссылку на поток. Еще один момент: хотя в рассмотренном примере это не требуется , имя потока можно установить после его создания с помощью метода setName(): final void setName(String threadName ) В threadName указывается новое имя потока. Как уже упоминалось, есть два изменения , которые в зависимости от ситуации способны сделать класс MyThread более удобным в применении. Первое изменение заключается в том , что конструктор MyThread может создать объект Thread для потока , сохранив ссылку на поток в переменной экземпляра. При Глава 1 1 . Многопоточное программирование 419 таком подходе поток готов к запуску, как только происходит возврат из конструктора MyThread. Понадобится просто вызвать метод start ( ) на экземпляре Thread, инкапсулированном в MyThread. Второе изменение предлагает способ начать выполнение потока сразу по сле его создания . Такой подход полезен в тех случаях, когда нет необходимости отделять создание потока от выполнения потока. Один из вариантов добиться этого для MyThread предоставить статический фабричный метод, который: — 1 ) создает новый экземпляр MyThread; 2 ) вызывает метод start ( ) на потоке, ассоциированном с этим экземпляром; 3) возвращает ссылку на вновь созданный объект MyThread. При таком подходе становится возможным создать и запустить поток с помощью одного вызова метода, что может упростить работу с MyThread, особен но в случаях, когда необходимо создать и запустить несколько потоков. Только что описанные изменения отражены в следующей версии предыду щей программы: // // // // // Изменения MyThread. В этой версии MyThread объект Thread создается при вызове его конструктора и сохраняется в переменной экземпляра по имени thrd. Здесь также устанавливается имя потока и предоставляется фабричный метод для создания и запуска потока. class MyThread implements Runnable { Thread thrd; < В thrd хранится ссылка на поток // Сконструировать новый поток, используя эту реализацию // интерфейса Runnable , и назначить ему имя. MyThread(String name) { thrd = new Thread(this, name); При создании потоку назначается имя } « // Фабричный метод, который создает и запускает поток , public static MyThread createAndStart(String name ) { MyThread myThrd = new MyThread(name); myThrd.thrd.start(); // запустить поток return myThrd; Начать выполнение потока } // Точка входа в поток , public void run() { System.out.println("Поток " + thrd.getName() + " запущен."); try { for(int count=0; count<10; count++) { Thread.sleep(400); System.out.println("В потоке " + thrd.getName() + " значение count равно " + count); } } 420 Java: руководство для начинающих, 9-е издание catch(InterruptedException exc) { System.out.println("Поток " + thrd.getName() + " прерван."); } System.out.println ("Поток " + thrd.getName() + " завершен."); } } class ThreadVariations { public static void main(String[] args) { System.out.println("Главный поток запущен."); // Создать и запустить поток. MyThread mt = MyThread.createAndStart("Child #1"); t for(int i=0; i < 50; i++) { Теперь поток запускается при его создании System.out.print("."); try { Thread.sleep(100); } catch(InterruptedException exc) { System.out.println("Главный поток прерван."); } } System.out.println("Главный поток завершен."); } } Вывод, генерируемый данной версией , будет таким же , как и ранее. Однако обратите внимание, что теперь MyThread больше не содержит имя потока. Взамен он предоставляет переменную экземпляра по имени thrd, которая хранит ссылку на объект Thread, созданный конструктором MyThread: MyThread(String name ) { thrd = new Thread(this, name); } Таким образом , после выполнения конструктора класса MyThread переменная экземпляра thrd будет содержать ссылку на только что созданный поток. Чтобы запустить поток , понадобится просто вызвать метод start ( ) на thrd. Далее обратите особое внимание на фабричный метод createAndStart(): // Фабричный метод, который создает и запускает поток , public static MyThread createAndStart(String name) { MyThread myThrd = new MyThread(name); myThrd.thrd.start(); // запустить поток return myThrd; } Когда данный метод вызывается , он создает новый экземпляр MyThread по имени myThrd. Затем в нем производится вызов метода start ( ) на копии thrd из myThrd. Наконец, он возвращает ссылку на только что созданный экземпляр MyThread. Таким образом , после возврата управления из вызова createAndStart ( ) поток уже будет запущен. Поэтому в main ( ) следующая строка создает и начинает выполнение потока за один вызов: MyThread mt = MyThread.createAndStart("Child #1"); Глава 1 1 . Многопоточное программирование 421 Из-за удобства , которое предлагает метод createAndStart ( ) , он будет использоваться в нескольких примерах далее в главе. Кроме того, такой метод полезно адаптировать для применения в собственных приложениях на основе потоков. Конечно , в тех случаях , когда желательно , чтобы выполнение потока было отделено от его создания , можно просто создать объект MyThread, а затем вызвать метод start ( ) позже . !?@>A8< C M:A?5@B0 ВОПРОС. Ранее использовался термин фабричный метод и был показан один пример такого метода по имени createAndStart ( ) . Существует ли более общее определение фабричного метода? ОТВЕТ. Да. В общем случае фабричный метод - это метод, который возвраща ет объект класса . Обычно фабричные методы являются статическими ме тодами класса . Фабричные методы полезны в разнообразных ситуациях. Рассмотрим несколько примеров. Как вы только что видели в случае с методом createAndStart ( ) , фабричный метод позволяет сконструировать объект, а затем установить его в определенное состояние, прежде чем он будет возвращен в вызывающий код. Еще один тип фабричного метода применя ется для предоставления легко запоминающегося имени, указывающего на разновидность создаваемого объекта. Например, в классе по имени Line, представляющем линию, могут быть определены фабричные методы вроде createRedLine ( ) и createBlueLine ( ) , которые создают линии определенных цветов. Вместо того чтобы запоминать потенциально сложный вызов конструктора , можно просто вызвать фабричный метод, имя которого ука зывает желаемый тип линии. В ряде случаев фабричный метод также может повторно использовать объект, а не создавать новый. Как вы увидите по мере продвижения в изучении Java, фабричные методы распространены в библи отеке Java API. Упражнение 11.1 Расширение класса Thread Реализация интерфейса Runnable представляет собой один из способов построения класса , который может создавать экземпляры объектов потока. Другой способ расширение класса Thread. В текущем упражнении будет показано , как расширить класс Thread, создав программу, которая функционально аналогична программе UseThreads, показанной в начале главы. Когда класс расширяет Thread, в нем должен быть переопределен метод run ( ) , который является точкой входа в новый поток. Он также должен вызвать start ( ) , чтобы начать выполнение нового потока. Можно (хотя и не обяза тельно ) переопределить другие методы класса Thread. ExtendThread.java — 422 Java : руководство для начинающих, 9-е издание . 1 Создайте файл по имени ExtendThread.java и поместите в его начало следующие строки кода: /* Упражнение 11.1. Расширение класса Thread. */ class MyThread extends Thread { Обратите внимание, что теперь MyThread расширяет класс Thread, а не реализует интерфейс Runnable. 2. Добавьте конструктор в класс MyThread: // Конструктор нового потока. MyThread(String name) { super(name); // имя потока } Здесь super используется для вызова следующей версии конструктора класса Thread: Thread(String threadName ) В threadName указывается имя потока. Как объяснялось ранее , класс Thread предоставляет возможность хранить имя потока. Таким образом , в MyThread не нужна переменная экземпляра для хранения имени. 3. Добавьте в класс MyThread показанный ниже метод run(): // Точка входа в поток , public void run() { System.out.println("Поток " + getNameO + " запущен."); try { for(int count=0; count < 10; count++) { Thread.sleep(400); System.out.println("В потоке " + getNameO + " значение count равно " + count); } } catch(InterruptedException exc) { System .out.println("Поток " + getNameO + " прерван."); } System ,out.println ("Поток " + getNameO + " завершен."); } } Обратите внимание на вызовы getName ( ) . Поскольку ExtendThread расширяет Thread, он может напрямую обращаться ко всем методам Thread, включая getName(). 4. Добавьте класс ExtendThread: Глава 1 1 . Многопоточное программирование 423 class ExtendThread { public static void main(String[] args) { System.out.println("Главный поток запущен."); MyThread mt = new MyThread("Child #1"); mt.start(); for(int i=0; i < 50; i++) { System.out.print("."); try { Thread.sleep(100); } catch(InterruptedException exc) { System.out.println("Главный поток прерван."); } } System.out.println("Главный поток завершен."); } } Обратите внимание в методе main ( ) на то , как экземпляр MyThread созда ется и затем запускается с помощью следующих двух строк кода: MyThread mt = new MyThread("Child # 1"); mt.start(); Поскольку MyThread теперь реализует Thread, метод start ( ) вызывается непосредственно на экземпляре MyThread, т.е . mt. 5. Далее приведен полный код программы . Ее вывод такой же , как и в при мере UseThreads, но в этом случае расширяется класс Thread, а не реали зуется интерфейс Runnable. /* Упражнение 11.1. Расширение класса Thread. */ class MyThread extends Thread { // Конструктор нового потока. MyThread(String name) { super(name); // имя потока } // Точка входа в поток , public void run() { System.out.println("Поток " + getNameO + " запущен."); try { for(int count=0; count < 10; count++) { Thread.sleep(400); System ,out.println("В потоке " + getNameO + " значение count равно " + count); } } 424 Java: руководство для начинающих, 9-е издание catch(InterruptedException exc) { System.out.println("Поток " + getName() + " прерван."); } System.out.println("Поток " + getNameO + " завершен."); } } class ExtendThread { public static void main(String[] args) { System.out.println("Главный поток запущен."); MyThread mt mt.start(); } = new MyThread("Child #1"); for(int i=0; i < 50; i++) { System.out.print("."); try { Thread.sleep(100); } catch(InterruptedException exc) { System.out.println("Главный поток прерван."); } } System.out.println ("Главный поток завершен."); } 6. При расширении класса Thread можно также включить возможность создания и запуска потока за один шаг с помощью статического фабричного метода , аналогичного тому, который применялся в показанной ранее программе ThreadVariations. Чтобы испытать такой прием , добавьте в MyThread следующий метод: public static MyThread createAndStart(String name) { MyThread myThrd = new MyThread(name); myThrd.start(); return myThrd; } Как видите , метод createAndStart ( ) создает новый экземпляр MyThread с указанным именем , вызывает start ( ) на этом потоке и возвращает ссылку на поток . Чтобы опробовать createAndStart ( ) , замените показанные ниже две строки в main(): System.out.println("Главный поток запущен."); MyThread mt = new MyThread("Child #1"); такой строкой: MyThread mt = MyThread.createAndStart("Child #1"); После внесения этих изменений программа продолжит работать так же , как и ранее , но поток будет создаваться и запускаться с помощью одного вызова метода . Глава 1 1 . Многопоточное программирование 425 !?@>A8< C M:A?5@B0 ВОПРОС. Почему в Java предлагаются два способа создания дочерних потоков ( путем расширения класса Thread либо реализации интерфейса Runnable) и какой из них лучше? ОТВЕТ. В классе Thread определен ряд методов , которые могут быть переопределены в производном классе. Единственным из них, который должен быть переопределен , является run ( ) . Конечно , это тот же самый метод, который требуется при реализации Runnable. Некоторые программисты на Java счи тают, что классы следует расширять только тогда, когда их планируется каким -либо образом развивать или настраивать. Итак, если вы не собираетесь переопределять какие-то другие методы класса Thread, то, вероятно, лучше просто реализовать Runnable. Кроме того , реализуя Runnable, вы делаете возможным наследование своего класса потока не от класса Thread. Создание нескольких потоков В предыдущих примерах создавался только один дочерний поток. Тем не менее , в программе можно порождать столько потоков, сколько необходимо. Скажем, в следующей программе создаются три дочерних потока: // Создание нескольких потоков. class MyThread implements Runnable { Thread thrd; // Конструктор нового потока. MyThread(String name) { thrd = new Thread(this, name); } // Фабричный метод, который создает и запускает поток , public static MyThread createAndStart(String name) { MyThread myThrd = new MyThread(name); myThrd.thrd.start(); // запустить поток return myThrd; } // Точка входа в поток , public void run() { System.out.println("Поток " + thrd.getName() + " запущен."); try { for(int count=0; count < 10; count++) { Thread.sleep(400); System.out.println("В потоке " + thrd.getName() + " значение count равно " + count); } } catch(InterruptedException exc) { System.out.println("Поток " + thrd.getName() + " прерван."); } 426 Java: руководство для начинающих, 9-е издание System.out.println("Поток " + thrd.getName() + " завершен."); } } class MoreThreads { public static void main(String[] args) { System.out.println ("Главный поток запущен."); MyThread mtl = MyThread.createAndStart("Child #1"); MyThread mt2 = MyThread.createAndStart("Child #2"); MyThread mt3 = MyThread.createAndStart("Child #3"); for(int i=0; i < 50; i++) { System.out.print("."); try { Thread.sleep(100); } catch(InterruptedException exc) { System.out.println("Главный поток прерван."); } } System.out.println("Главный поток завершен."); Создать и запустить на выполнение три потока } } Ниже показан пример вывода , сгенерированного программой: Главный поток запущен. Поток Child #1 запущен. .Поток Child #2 запущен. Поток Child #3 запущен. ...В потоке Child #3 значение count равно 0 В потоке Child #2 значение count равно 0 В потоке Child #1 значение count равно 0 ....В потоке Child #1 значение count равно 1 В потоке Child #2 значение count равно 1 В потоке Child #3 значение count равно 1 ....В потоке Child #2 значение count равно 2 В потоке Child #3 значение count равно 2 В потоке Child #1 значение count равно 2 ...В потоке Child #1 значение count равно 3 В потоке Child #2 значение count равно 3 В потоке Child #3 значение count равно 3 ....В потоке Child #1 значение count равно 4 В потоке Child #3 значение count равно 4 В потоке Child #2 значение count равно 4 ....В потоке Child #1 значение count равно 5 В потоке Child #3 значение count равно 5 В потоке Child #2 значение count равно 5 ...В потоке Child #3 значение count равно 6 .В потоке Child #2 значение count равно 6 В потоке Child #1 значение count равно б ...В потоке Child #3 значение count равно 7 В потоке Child #1 значение count равно 7 Глава 1 1 . Многопоточное программирование 427 В потоке Child #2 значение count равно 7 ....В потоке Child #2 значение count равно 8 В потоке Child #1 значение count равно 8 В потоке Child #3 значение count равно 8 .... В потоке Child #1 значение count равно 9 Поток Child #1 завершен. В потоке Child #2 значение count равно 9 Поток Child #2 завершен. В потоке Child #3 значение count равно 9 Поток Child #3 завершен. Главный поток завершен. В выводе видно, что после запуска все три дочерних потока совместно используют ЦП. Обратите внимание , что в текущем запуске потоки стартуют в том порядке, в котором они созданы, что может быть не всегда так. Выполнение потоков в Java может планировать по-своему. Разумеется, из-за разницы во времени или среде вывод программы может варьироваться, поэтому не удивляйтесь, если вы увидите слегка отличающиеся результаты при опробовании программы . Выяснение, завершен ли поток Часто полезно знать, когда поток завершился. Например, в предшествующих примерах для иллюстрации было полезно поддерживать главный поток в активном состоянии до тех пор , пока другие потоки не закончат свою работу. Цель достигалась за счет того, что главный поток засыпал на более долгое время , чем порожденные им дочерние потоки. Разумеется , вряд ли такое решение можно считать удовлетворительным или обобщаемым! К счастью , в классе Thread предоставляются два средства для определения , завершился ли поток. Первое из них можно вызвать метод isAlive ( ) на потоке. Вот его общий вид: — final boolean isAliveO Метод isAliveO возвращает true, если поток, на котором он вызван , все еще выполняется. В противном случае isAliveO возвратит false. Чтобы опробовать isAlive ( ) , замените в предыдущей программе версию класса MoreThreads версией, показанной ниже: // Использование isAliveO . class MoreThreads { public static void main(String[] args) { System.out.println("Главный поток запущен."); MyThread mtl = MyThread.createAndStart("Child #1"); MyThread mt2 = MyThread.createAndStart("Child #2"); MyThread mt3 = MyThread.createAndStart("Child #3"); do { System.out.print("."); try { Thread.sleep(100); } 428 Java: руководство для начинающих, 9-е издание catch(InterruptedException exc) { System.out.println("Главный поток прерван."); } } while (mtl.thrd.isAlive() | | | mt2.thrd.isAlive() | Ожидать, пока не завершатся все потоки mt3.thrd.isAlive()); System.out.println("Главный поток завершен."); } } Вывод, генерируемый этой версией , аналогичен выводу, получаемому из предыдущей версии, за исключением того , что main ( ) завершается, как только заканчиваются другие потоки. Разница в том , что для ожидания завершения дочерних потоков используется метод isAlive ( ) . Еще один способ дождаться завершения потока предусматривает вызов метода join ( ) : final void join() throws InterruptedException Этот метод ожидает завершения потока , на котором он вызывается. Его имя происходит от концепции вызывающего потока , ожидающего до тех пор, пока указанный поток не присоединится к нему. Дополнительные формы метода join ( ) позволяют указывать максимальное время ожидания завершения ука занного потока. В следующей программе метод j o i n ( ) применяется для гарантирования того, что главный поток остановится последним: // Использование join(). class MyThread implements Runnable { Thread thrd; // Конструктор нового потока. MyThread(String name ) { thrd = new Thread(this, name); } // Фабричный метод, который создает и запускает поток , public static MyThread createAndStart(String name) { MyThread myThrd = new MyThread(name); myThrd.thrd.start(); // запустить поток return myThrd; } // Точка входа в поток , public void run() { System.out.println("Поток " + thrd.getName() + " запущен."); try ( for(int count=0; count < 10; count++) { Thread.sleep(400); System .out.println("В потоке " + thrd.getName() + " значение count равно " + count); } } Глава 1 1 . Многопоточное программирование 429 catch(InterruptedException exc) { System.out.println("Поток " + thrd.getName() + " прерван."); } System.out.println("Поток " + thrd.getName() + " завершен."); } } class JoinThreads { public static void main(String[] args) { System.out.println("Главный поток запущен."); MyThread mtl = MyThread.createAndStart("Child #1"); MyThread mt2 = MyThread.createAndStart("Child #2"); MyThread mt3 = MyThread.createAndStart("Child #3"); try { Ожидать, пока не завершится указанный поток mtl.thrd.join(); < System.out.println ("Поток Child #1 присоединен."); mt2.thrd.join(); System.out.println("Поток Child #2 присоединен."); mt3.thrd.join(); + System.out.println("Поток Child #3 присоединен."); } catch(InterruptedException exc) { System.out.println("Главный поток прерван."); } System.out.println("Главный поток завершен."); } } Ниже показан пример вывода , сгенерированного этой программой. Помни те, что получаемый вами вывод может незначительно отличаться. Главный поток запущен. Поток Child #1 Поток Child #2 Поток Child #3 В потоке Child В потоке Child В потоке Child В потоке Child В потоке Child В потоке Child В потоке Child В потоке Child В потоке Child В потоке Child В потоке Child В потоке Child В потоке Child В потоке Child В потоке Child В потоке Child В потоке Child В потоке Child запущен. запущен. запущен. #2 значение #1 значение #3 значение #2 значение #3 значение #1 значение #2 значение #1 значение #3 значение #2 значение #3 значение #1 значение #3 значение #2 значение #1 значение # 3 значение #1 значение #2 значение count count count count count count count count count count count count count count count count count count равно равно равно равно равно равно равно равно равно равно равно равно равно равно равно равно равно равно О О О 1 1 1 2 2 2 3 3 3 4 4 4 5 5 5 430 Java: руководство для начинающих, 9-е издание В потоке В потоке В потоке В потоке В потоке В потоке В потоке В потоке В потоке В потоке Child #3 значение count Child #2 значение count Child #1 значение count Child #3 значение count Child #1 значение count Child #2 значение count Child #3 значение count Child #2 значение count Child #1 значение count Child # 3 значение count Поток Child #3 завершен. В потоке Child #2 значение count Поток Child #2 завершен. В потоке Child #1 значение count Поток Child #1 завершен. Поток Child #1 присоединен. Поток Child #2 присоединен. Поток Child # 3 присоединен. Главный поток завершен. равно равно равно равно равно равно равно равно равно равно 6 6 б 7 7 7 8 8 8 9 равно 9 равно 9 Как видите , после возврата управления из вызовов join ( ) потоки останавливают выполнение. Приоритеты потоков С каждым потоком ассоциирована настройка приоритета. Приоритет потока отчасти определяет, сколько времени ЦП получает поток по сравнению с другими активными потоками . Как правило , за заданный период времени потоки с низким приоритетом получают мало , а потоки с высоким приоритетом много. Вполне ожидаемо, что количество времени ЦП , которое получает поток , сильно влияет на его характеристики выполнения и его взаимодействие с другими потоками , выполняющимися в данный момент в системе. Важно понимать, что помимо приоритета потока на то, сколько времени ЦП получает поток, влияют и другие факторы. Скажем, если высокоприоритетный поток ожидает доступа к какому-либо ресурсу, возможно, с целью ввода с клавиатуры, то он блокируется и запускается поток с более низким приоритетом . Однако когда высокоприоритетный поток получает доступ к ресурсу, он может вытеснить низкоприоритетный поток и возобновить свое выполнение. Еще один фактор, влияющий на планирование потоков, связан с тем , каким образом операционная система ( ОС ) реализует многозадачность. ( См . врезку “ Спросим у эксперта ” в конце раздела . ) Таким образом, только сам факт назначения одному потоку высокого приоритета , а другому низкого, не обязательно означает, что один поток будет работать быстрее или чаще, чем другой. Просто поток с высоким приоритетом потенциально может получить больше времени ЦП . Когда дочерний поток запускается, настройка его приоритета равна приоритету родительского потока . Изменить приоритет потока можно, вызвав метод setPriority(), который является членом класса Thread и имеет такую общую форму: — — final void setPriority(int level) Глава 1 1 . Многопоточное программирование 431 В аргументе level указывается новая настройка приоритета для вызывающего потока . Значение level должно находиться в диапазоне от MIN PRIORITY до MAX PRIORITY. В настоящее время эти значения равны 1 и 10 соответственно. Чтобы вернуть потоку стандартный приоритет, необходимо указать значение N0RM PRI0RITY, которое в настоящее время равно 5. Упомянутые приоритеты определены как статические финальные переменные внутри Thread. Получить текущую настройку приоритета можно вызовом метода getPriority ( ) класса Thread: _ final int getPriority() В следующем примере демонстрируется использование потоков с разными приоритетами. Потоки создаются в виде экземпляров класса Priority. Метод run ( ) содержит цикл , который подсчитывает количество итераций. Цикл останавливается, когда счетчик достигает значения 10000000 или статическая переменная stop становится равной true. Первоначально переменная stop установлена в false, но первый поток , завершивший подсчет, устанавливает stop в true. Это приводит к тому, что другой поток завершается, как только ему будет выделен следующий квант времени. Каждый раз в цикле строка в currentName сравнивается с именем исполняемого потока. Если они не совпадают, тогда произошло переключение задач . При всяком переключении задач отображается имя нового потока , a currentName получает имя нового потока. Отображение каждого переключения задач позволяет наблюдать (очень неточно ) , когда потоки получают доступ к ЦП . После остановки потоков отображается количество итераций для каждого цикла. // Демонстрация использования приоритетов потоков. class Priority implements Runnable { int count; Thread thrd; static boolean stop = false; static String currentName; // Конструктор нового потока. Priority(String name) { thrd = new Thread(this, name); count = 0; currentName = name; } // Точка входа в поток , public void run( ) { System.out.println("Поток " + thrd.getName() + " запущен."); do { count++; if(currentName.compareTo( thrd.getName()) != 0) { currentName = thrd.getName(); System.out.println("В потоке " + currentName); } } while(stop == false && count < 10000000) Первый же поток, в котором достигнуто значение 10000000, завершает остальные потоки 432 Java: руководство для начинающих, 9-е издание stop = true; System.out.println("\пПоток " + thrd.getName() + " завершен."); } } class PriorityDemo { public static void main(String[] args) { Priority mtl = new Priority("Высокий приоритет"); Priority mt2 = new Priority("Низкий приоритет "); Priority mt3 = new Priority("Нормальный приоритет #1"); Priority mt4 = new Priority("Нормальный приоритет #2"); Priority mt5 = new Priority("Нормальный приоритет #3"); // Установить приоритеты . mtl.thrd.setPriority(Thread.N0RM_PRI0RITY+2); mt2.thrd.setPriority(Thread.N0RM PRI0RITY-2); _ Назначить mtl более высокий приоритет, чем mt 2 // Оставить потокам mt3, mt4 и mt5 стандартный, // нормальный уровень приоритета. // Запустить потоки , mtl.thrd.start(); mt2.thrd.start(); mt3.thrd.start(); mt4.thrd.start(); mt5.thrd.start(); try { mtl.thrd.join(); mt2.thrd.join(); mt3.thrd.join(); mt4.thrd.join(); mt5.thrd.join(); } catch(InterruptedException exc) { System.out.println("Главный поток прерван."); } System.out.println(" ХпПоток с высоким приоритетом досчитал до " + mtl.count); System.out.println("Поток с низким приоритетом досчитал до " + mt2.count); System.out.println("1-й поток с нормальным приоритетом досчитал до " + mt3.count); System.out.println("2 й поток с нормальным приоритетом досчитал до " + mt4.count); System.out.println("3-й поток с нормальным приоритетом досчитал до " + mt5.count); - } } Вот результаты , полученные после запуска программы: Поток с высоким приоритетом досчитал до 10000000 Поток с низким приоритетом досчитал до 3477862 1-й поток с нормальным приоритетом досчитал до 7000045 2-й поток с нормальным приоритетом досчитал до 6576054 3-й поток с нормальным приоритетом досчитал до 7373846 Глава 1 1 . Многопоточное программирование 433 В этом запуске программы поток с высоким приоритетом получил наибольшее количество времени ЦП. Разумеется, точный вывод будет зависеть от ряда факторов, включая быстродействие имеющегося ЦП , количество ЦП в системе, используемую ОС , а также число и характер других выполняемых задач. Таким образом, для любого заданного запуска поток с низким приоритетом фактически при благоприятных обстоятельствах может получить больше всего времени ЦП. !?@>A8< C M:A?5@B0 ВОПРОС. Влияет ли реализация многозадачности в ОС на то, сколько времени ЦП получает поток? ОТВЕТ. Помимо настройки приоритета потока наиболее важным фактором , влияющим на выполнение потока , является то , как ОС реализует многоза дачность и планирование. В некоторых ОС применяется вытесняющая многозадачность, при которой каждый поток хотя бы изредка получает квант времени . В других ОС используется невытесняющая многозадачность, когда один поток должен завершить выполнение до того, как будет выполнен другой поток. В системах с невытесняющей многозадачностью очень легко может случиться так, что один поток будет доминировать, препятствуя запуску других потоков. Синхронизация В случае применения нескольких потоков иногда необходимо координиро вать действия двух или большего числа потоков. Процесс, посредством которого достигается такая цель, называется синхронизацией . Наиболее распространенная причина синхронизации когда двум или более потокам необходим доступ к общему ресурсу, который может использоваться только одним потоком одновременно. Например , если один поток выполняет запись в файл , то второму по току нужно запретить это делать в то же самое время. Еще одна причина синкогда один поток ожидает события , инициированного другим хронизации потоком. В таком случае должны существовать какие-то средства , с помощью которых первый поток удерживается в приостановленном состоянии до тех пор, пока не произойдет событие. Затем ожидающий поток должен возобновить выполнение. Основой синхронизации в Java является принцип монитора , который кон тролирует доступ к объекту. Монитор реализует концепцию блокировки. Когда объект заблокирован одним потоком , никакой другой поток не может получить доступ к этому объекту. Когда поток завершается , объект деблокируется и становится доступным другому потоку. Монитор имеют все объекты в Java. Средство монитора встроено в сам язык. В итоге все объекты могут быть синхронизированы. Синхронизация поддерживается ключевым словом synchronized и несколькими четко определенными — — 434 Java: руководство для начинающих, 9-е издание методами, которые есть у всех объектов. Поскольку синхронизация была разработана в Java с самого начала , ее гораздо проще применять, чем можно было бы ожидать. На самом деле для многих программ синхронизация объектов практически прозрачна. Существуют два способа синхронизации кода. Оба способа предполагают использование ключевого слова synchronized, и оба они рассматриваются далее в главе. Использование синхронизированных методов Синхронизировать доступ к методу можно с помощью ключевого слова synchronized. При вызове синхронизированного метода вызывающий поток входит в монитор объекта , который затем блокирует объект. Пока объект заблокирован , никакой другой поток не может войти в данный метод или в любой другой синхронизированный метод, определенный в классе объекта. Когда поток возвращается из метода , монитор разблокирует объект, позволяя использовать его следующим потоком. Таким образом, синхронизация достигается практически без усилий по программированию с вашей стороны. В следующей программе демонстрируется применение синхронизации на примере управления доступом к методу sumArray ( ) , который суммирует элементы целочисленного массива. // Использование синхронизации для управления доступом. class SumArray { private int sum; synchronized int sumArray(int[] nums) { sum = 0; // сбросить sum Метод sumArray ( ) синхронизирован for(int i=0; i<nums.length; i++) { sum += nums[ i]; System.out.println("Промежуточная сумма в потоке " + Thread.currentThread().getNameO + " равна " + sum); try { Thread.sleep(10); // разрешить переключение задач } catch(InterruptedException exc) { System.out.println("Поток прерван."); } } return sum; } } class MyThread implements Runnable { Thread thrd; static SumArray sa = new SumArray(); int[] a; int answer; Глава 1 1 . Многопоточное программирование 435 // Конструктор нового потока. MyThread(String name, int[] nums) { thrd = new Thread(this, name); a nums; } // Фабричный метод, который создает и запускает поток , public static MyThread createAndStart(String name, int[] nums) { MyThread myThrd = new MyThread(name, nums); myThrd.thrd.start(); // запустить поток return myThrd; } // Точка входа в поток , public void run() { int sum; System.out.println("Поток " + thrd.getName() + " запущен."); answer = sa.sumArray(a); System.out.println("Сумма в потоке " + thrd.getName() + " равна " + answer); System.out.println("Поток " + thrd.getName() + " завершен."); } } class Sync { public static void main(String[] args) { int[] a = {1, 2, 3, 4, 5}; MyThread mtl = MyThread.createAndStart("Child #1", a); MyThread mt2 = MyThread.createAndStart("Child #2", a); try { mtl.thrd.join (); mt2.thrd.join(); } catch(InterruptedException exc) { System.out.println("Главный поток прерван."); } } } Ниже показан вывод, генерируемый программой ( у вас он может отличаться): Поток Child #1 запущен. Промежуточная сумма в потоке Child Поток Child #2 запущен. Промежуточная сумма в потоке Child Промежуточная сумма в потоке Child Промежуточная сумма в потоке Child Промежуточная сумма в потоке Child Сумма в потоке Child #1 равна 15 Поток Child #1 завершен. Промежуточная сумма в потоке Child Промежуточная сумма в потоке Child Промежуточная сумма в потоке Child #1 равна 1 #1 #1 #1 #1 равна равна равна равна 3 б 10 15 #2 равна 1 #2 равна 3 #2 равна б 436 Java : руководство для начинающих, 9- е издание Промежуточная сумма в потоке Child #2 равна 10 Промежуточная сумма в потоке Child #2 равна 15 Сумма в потоке Child #2 равна 15 Поток Child #2 завершен. Давайте подробно рассмотрим приведенную выше программу. В ней создаются три класса. Первый класс SumArray. Он содержит метод sumArray ( ), подсчитывающий сумму элементов целочисленного массива. Второй класс , MyThread, задействует статический объект типа SumArray для получения сум мы элементов целочисленного массива. Объект имеет имя sa и поскольку он статический, существует только одна его копия, совместно используемая всеми экземплярами MyThread. Наконец, третий класс, Sync, создает два потока , каждый из которых вычисляет сумму элементов целочисленного массива. Внутри sumArray ( ) вызывается метод sleep ( ) , чтобы намеренно разрешить переключение задач, если оно возможно. Так как метод sumArray ( ) синхронизирован , он может применяться одновременно только одним потоком. Таким образом , когда второй дочерний поток начинает выполнение, он не входит в sumArray ( ) до тех пор, пока первый дочерний поток не закончит с ним работу. В результате гарантируется получение корректного результата. Чтобы лучше понять воздействие ключевого слова synchronized, удалите его из объявления sumArray ( ) . После этого метод sumArray ( ) больше не синхронизируется, и любое количество потоков может использовать его одновременно. Проблема в том , что промежуточная сумма хранится в перемен ной sum, значение которой будет изменяться каждым потоком , вызывающим sumArray ( ) через статический объект sa. В таком случае, когда два потока вы зывают sa.sumArray ( ) одновременно , получается некорректный результат, потому что sum отражает смешанные вместе итоги суммирования из обоих потоков. Скажем , вот пример вывода программы после удаления ключевого слова synchronized из объявления sumArray( ) . ( Вывод у вас может быть другим.) — Поток Child # 1 запущен. Промежуточная сумма в потоке Child Поток Child #2 запущен. Промежуточная сумма в потоке Child Промежуточная сумма в потоке Child Промежуточная сумма в потоке Child Промежуточная сумма в потоке Child Промежуточная сумма в потоке Child Промежуточная сумма в потоке Child Промежуточная сумма в потоке Child Промежуточная сумма в потоке Child Сумма в потоке Child #2 равна 24 Поток Child #2 завершен. Промежуточная сумма в потоке Child Сумма в потоке Child #1 равна 29 Поток Child #1 завершен. #1 равна 1 #2 #1 #2 #2 #1 #2 #1 #2 равна равна равна равна равна равна равна равна 1 3 5 8 11 15 19 24 #1 равна 29 В выводе видно , что оба дочерних потока одновременно вызывают sa . sumArray(), и значение sum искажается. Глава 1 1 . Многопоточное программирование 437 Прежде чем двигаться дальше , давайте отметим ключевые моменты, связанные с синхронизированным методом. z Синхронизированный метод создается предварением его объявления ключевым словом synchronized. z Для любого заданного объекта после вызова синхронизированного метода объект блокируется, и никакие синхронизированные методы на том же объекте не могут применяться другим потоком выполнения. z Другие потоки , пытающиеся вызвать используемый синхронизированный объект, перейдут в состояние ожидания , пока объект не будет разблокирован. z Когда поток покидает синхронизированный метод, объект деблокируется. Оператор synchronized Несмотря на то что определение синхронизированных методов внутри создаваемых классов является простым и эффективным средством обеспечения синхронизации , оно не будет работать абсолютно во всех случаях. Чтобы понять причину, рассмотрим следующую ситуацию. Представьте , что вы хотите синхронизировать доступ к объектам класса, не предназначенного для многопоточного доступа , т.е. синхронизированные методы в классе не используются. Кроме того, данный класс создан не вами, а третьей стороной , и у вас нет до ступа к исходному коду. Таким образом , вы не можете добавить ключевое слово synchronized к соответствующим методам внутри класса. Как синхронизировать доступ к объекту такого класса? К счастью , решить проблему довольно легко: вы просто помещаете вызовы методов , определенных этим классом , внутрь блока synchronized. Ниже показана общая форма оператора synchronized: synchronized(objRef) { // операторы , подлежащие синхронизации } — Здесь o b j R e f это ссылка на синхронизируемый объект. Блок synchronized гарантирует, что вызов синхронизированного метода , который является членом класса objRef, произойдет только после того, как текущий по ток успешно войдет в монитор objRef. Например, еще один способ синхронизации обращений к методу sumArray() предусматривает его вызов изнутри блока synchronized, как демонстрируется в следующей версии программы: // Использование блока synchronized для управления доступом к SumArray(). class SumArray { private int sum; int sumArray(int[] nums) { sum = 0; // сбросить sum ^ Метод sumArray() не является синхронизированным 438 Java: руководство для начинающих, 9-е издание for(int i=0; i<nums.length; i++) { sum += nums[i]; System.out.println("Промежуточная сумма в потоке " + Thread.currentThread().getNameO + " равна " + sum); try { Thread.sleep(10); // разрешить переключение задач } catch(InterruptedException exc) { System.out.println("Поток прерван."); } } return sum; } } class MyThread implements Runnable { Thread thrd; static SumArray sa = new SumArrayO ; int[] a; int answer; // Конструктор нового потока. MyThread(String name, int[] nums) { thrd = new Thread(this, name); a = nums; } // Фабричный метод, который создает и запускает поток , public static MyThread createAndStart(String name, int[] nums) { MyThread myThrd = new MyThread(name, nums); myThrd.thrd.start(); // запустить поток return myThrd; } // Точка входа в поток , public void run() { int sum; System.out.println("Поток " + thrd.getName() + " запущен."); // Синхронизировать вызовы метода sumArrayO . synchronized(sa) { Вызовы sumArray() на sa синхронизированы answer = sa.sumArray(a); } System.out.println("Сумма в потоке " + thrd.getName() + " равна " + answer); System.out.println("Поток " + thrd.getName() + " завершен."); } } class Sync { public static void main(String[] args) { int[] a = {1, 2, 3, 4, 5}; MyThread mtl = MyThread.createAndStart("Child #1", a); MyThread mt2 = MyThread.createAndStart("Child #2", a); try { mtl.thrd.join(); mt2.thrd.join(); } Глава 1 1 . Многопоточное программирование 439 catch(InterruptedException exc) { System.out.println("Главный поток прерван."); } } } Эта версия программы выдает тот же самый корректный вывод, что и пока занная ранее версия, где применялся синхронизированный метод. !?@>A8< C M:A?5@B0 ВОПРОС. Говорят, что существуют так называемые “ утилиты параллелизма ”. Что они собой представляют? И что такое Fork/Join Framework? ОТВЕТ. Утилиты параллелизма , входящие в состав пакета j ava . u t i l , concurrent ( и его подпакетов) , поддерживают параллельное программирование. Помимо ряда других элементов они предлагают синхронизаторы , пулы потоков, диспетчеры выполнения и блокировки , которые расширяют контроль над выполнением потоков. Одной из самых захватывающих функциональных средств параллельного API является Fork / Join Framework . Инфраструктура Fork /Join Framework поддерживает то, что часто называют параллельным программированием. Это название обычно дается методам , которые используют преимущества компьютеров с двумя или более процессорами ( включая многоядерные системы ) путем разделения задачи на подзадачи , причем каждая подзадача выполняется на собственном процессоре. Как вы понимаете, такой подход может привести к значительному повышению производительности и пропускной способности . Ключевым преимуществом инфраструктуры Fork/Join Framework является простота применения ; она упрощает разработку многопоточного кода, который автоматически масштабируется с целью эксплуатации нескольких процессоров в системе. Таким образом , Fork/Join Framework облегчает создание параллельных решений некоторых общих задач программирования , таких как выполнение операций над элементами массива. Утилиты параллелизма в целом и Fork / Join Framework в частности представляют собой средства, которые стоит изучать после приобретения опыта работы с многопоточностью. Взаимодействие между потоками с использованием notify ( ) , wait ( ) и notifyAll ( ) Рассмотрим следующую ситуацию. Поток под названием Т выполняется внутри синхронизированного метода и нуждается в доступе к ресурсу по имени R , который временно недоступен. Что должен делать поток т ? Если т входит в какой -либо цикл опроса , ожидающий освобождения ресурса R, тогда поток т 440 Java: руководство для начинающих, 9 - е издание связывает объект, препятствуя доступу к нему со стороны других потоков. Это далеко не оптимальное решение , поскольку оно частично сводит на нет преимущества программирования для многопоточной среды. Лучшее решение со стоит в том , чтобы Т временно уступил контроль над ресурсом , позволив запуститься другому потоку. Когда ресурс R становится доступным, поток Т может быть уведомлен и затем возобновить выполнение. Такой подход основан на некоторой форме взаимодействия между потоками , при которой один поток может уведомить другой о том , что он заблокирован , и получить уведомление , что он может возобновить выполнение. В Java поддерживается взаимодействие между потоками с помощью методов wait ( ) , notify ( ) и notifyAll ( ) . Методы wait ( ) , notify ( ) и notifyAll ( ) являются частью всех объектов , поскольку они реализованы классом Object. Указанные методы должны вы зываться только из контекста synchronized. Они используются следующим образом. Когда выполнение потока временно блокируется, он вызывает метод wait ( ) , что приводит к переводу потока в спящий режим, а монитор для данного объекта освобождается, позволяя другому потоку использовать этот объект. Позже спящий поток пробуждается , когда другой поток входит в тот же монитор и вызывает метод notify ( ) или notifyAll ( ) . Ниже приведены различные формы метода wait ( ) , определенные в классе Object: final void wait() throws InterruptedException final void wait(long millis) throws InterruptedException final void wait(long millis, int nanos) throws InterruptedException Первая форма ожидает получения уведомления. Вторая форма ожидает по лучения уведомления либо истечения заданного периода времени в миллисе кундах. Третья форма позволяет указать период ожидания в наносекундах. Вот общие формы методов notify ( ) и notifyAll ( ) : final void notify() final void notifyAll() Вызов notify ( ) возобновляет работу одного ожидающего потока . Вызов notifyAll ( ) уведомляет все потоки , а планировщик определяет, какой поток получает доступ к объекту. Прежде чем приступить к рассмотрению примера , иллюстрирующего взаимодействие между потоками , необходимо сделать одно важное замечание . Хотя метод wait ( ) обычно ожидает, пока не будет вызван метод notify ( ) или notifyAll(), существует вероятность того, что в очень редких случаях ожидающий поток может быть разбужен из- за ложного пробуждения. Условия , которые приводят к ложному пробуждению, сложны и их обсуждение выходит за рамки настоящей книги. Однако в документации по Java API рекомендуется , чтобы из- за удаленной возможности ложного пробуждения вызовы wait ( ) выполня лись в цикле , проверяющем условие , при котором поток переводится в режим ожидания . Прием демонстрируется в следующем примере. Глава 1 1 . Многопоточное программирование 441 Пример использования wait ( ) и notify ( ) Чтобы понять необходимость в применении методов wait ( ) и notify ( ) , будет написана программа , которая имитирует работу часов , отображая на экра не слова "Tick" (тик ) и "Took" (так ) . В программе создается класс TickTock, содержащий два метода: tick() и tock(). Метод tick() отображает слово "Tick", а метод tock( ) — слово "Тоск". Для запуска часов создаются два потока , один из которых вызывает метод tick() , а другой метод tock(). Цель — заставить два потока выполняться таким образом , чтобы в выводе программы присутствовало согласованное сообщение "Tick Tock", т. е . повторяющийся шаблон “ тик - так” . — // Использование wait() и notify() для имитации работы часов , class TickTock { String state; // хранит состояние часов synchronized void tick(boolean running) { if(!running) { // остановить часы state = "ticked"; // уведомить ожидающие потоки notify(); return; } System.out.print("Tick "); state = "ticked"; // установить текущее состояние в "после notifyO ; // позволить методу tock() выполняться try { while(!state.equals( "tocked ")) wait(); // ожидать завершения работы метода tock() } catch(InterruptedException exc) { System.out.println("Поток прерван."); } ^- тик I и tick() уведомляет tock() tick() ожидает tock() } synchronized void tock( boolean running ) { // остановить часы if(!running) ( state = "tocked"; // уведомить ожидающие потоки notifyO ; return; } System.out.println("Tock"); // установить текущее состояние // в "после так I и notifyO ; // позволить методу tick() выполняться < state = "tocked"; try { while(!state.equals( "ticked ")) wait(); // ожидать завершения работы метода tick() } tock() уведомляет tick() tock() ожидает tick() 442 Java: руководство для начинающих, 9-е издание catch(InterruptedException exc) { System.out.println("Поток прерван."); } } } class MyThread implements Runnable { Thread thrd; TickTock ttOb; // Конструктор нового потока. MyThread(String name, TickTock tt) { thrd = new Thread(this, name); ttOb = tt; } // Фабричный метод, который создает и запускает поток , public static MyThread createAndStart(String name, TickTock tt) { MyThread myThrd = new MyThread(name, tt); myThrd.thrd.start(); // запустить поток return myThrd; } // Точка входа в поток , public void run() { if(thrd.getName().compareTo("Tick") == 0) { for(int i=0; i<5; i++) ttOb.tick(true); ttOb.tick(false); } else { for(int i=0; i<5; i++) ttOb.tock(true); ttOb.tock(false); } } } class ThreadCom { public static void main(String[] args) { TickTock tt = new TickTock(); MyThread mtl = MyThread.createAndStart("Tick", tt); MyThread mt2 = MyThread.createAndStart("Tock", tt); try { mtl.thrd.join(); mt2.thrd.join(); } catch(InterruptedException exc) { System.out.println("Главный поток прерван."); } } } Вот вывод, генерируемый программой: Tick Tick Tick Tick Tick Tock Tock Tock Tock Tock Глава 1 1 . Многопоточное программирование 443 Давайте рассмотрим программу более внимательно. В ее основу положен класс TickTock. Он содержит два метода, tick ( ) и tock(), которые взаимодействуют друг с другом, чтобы гарантировать, что за словом "Tick" всегда следует слово "Тоск", затем снова "Tick" и т.д. Обратите внимание на поле state. Когда часы работают, в state будет содержаться строка "ticked" ( после “ тик ” ) или "tocked" ( после “ так ” ), обозначающие текущее состояние часов. В методе main ( ) создается объект TickTock по имени tt, который применяется для за пуска двух потоков выполнения. Потоки основаны на объектах типа MyThread. И конструктору класса MyThread, и методу createAndStart ( ) передаются два аргумента . Первый становится именем потока , которым будет либо "Tick", либо "Тоск". Второй это ссылка на объект TickTock, которым в данном случае является tt. Внутри метода run ( ) класса MyThread вызывается tick(), если именем потока явля ется "Tick". Если же именем потока оказывается "Тоск", тогда вызывается tock(). Для каждого метода делается пять вызовов, которым передается true в качестве аргумента. Часы работают до тех пор, пока передается true. Последний вызов, в котором каждому методу передается false, останавливает часы. Самая важная часть программы находится в методах tick ( ) и tock ( ) класса TickTock. Начнем с метода tick(), который для удобства показан ниже: — synchronized void tick( boolean running) { // остановить часы if(!running) { state = "ticked"; notify(); // уведомить ожидающие потоки return; } System.out.print("Tick "); // установить текущее состояние в "после ТИК I и state = "ticked"; // позволить методу tock() выполняться notify(); try { while( !state.equals("tocked")) // ожидать завершения работы метода tock() wait(); } catch(InterruptedException exc) { System.out.println("Поток прерван."); } } Прежде всего, обратите внимание, что объявление метода tick() снабжено ключевым словом synchronized. Не забывайте , что wait() и notify ( ) применяются только к синхронизированным методам. Метод tick() начинается с проверки значения параметра running, который используется для обеспечения корректного прекращения работы часов. Если значение running равно false, тогда часы были остановлены. В таком случае state устанавливается в "ticked" и вызывается метод notify ( ) , чтобы разрешить выполнение любых ожидающих потоков. Мы еще вернемся к этому моменту чуть позже. Предполагая , что при выполнении метода tick( ) часы работают, ото бражается слово "Tick", поле state устанавливается в "ticked", после чего 444 Java : руководство для начинающих, 9-е издание . происходит вызов notify ( ) Вызов notify { ) позволяет запустить поток, ожидающий доступа к тому же самому объекту. Затем в цикле while вызывается метод wait ( ) . Вызов wait ( ) приводит к тому, что tick ( ) приостанавливается до тех пор, пока другой поток не вызовет notify(). Следовательно, цикл не будет выполняться до тех пор, пока другой поток не вызовет notify ( ) на том же объекте В результате, когда вызывается метод tick(), он отображает одно слово "Tick", дает возможность запуститься другому потоку и приостанавливается. В цикле while, где вызывается wait ( ) , производится проверка значения state в ожидании, когда оно станет равным "tocked", что произойдет только после выполнения метода tock ( ) . Как объяснялось ранее, применение цикла while для проверки указанного условия предотвращает ложное пробуждение из - за некорректного перезапуска потока. Если при выходе из метода wait ( ) значение state не равно "tocked", то это означает, что произошло ложное пробуждение, и метод wait ( ) просто вызывается снова. Метод tock ( ) является точной копией tick ( ) за исключением того, что отображается слово "Tock" и state устанавливается в "tocked". Таким образом, при входе он выводит "Tock", вызывает notify ( ) и затем ожидает. Рас сматривая их в виде пары, за вызовом tick ( ) может происходить только вызов tock ( ) , за ним — только вызов tick ( ) и т.д. Следовательно, два метода взаимно синхронизированы. Причина вызова notify ( ) при остановленных часах связана с необходимостью обеспечить успешное завершение последнего вызова wait ( ) . Помните о том, что и tick ( ) , и tock ( ) вызывают wait ( ) после отображения своего со общения. Проблема в том, что когда часы остановлены, один из методов все равно будет ожидать. Таким образом, последний вызов notify ( ) требуется для запуска ожидающего метода. В качестве эксперимента попробуйте удалить этот вызов notify ( ) и посмотрите, что произойдет Как вы увидите, программа “зависнет”, и вам придется нажать комбинацию <Ctrl+C> для выхода из нее. Дело в том, что когда в последнем вызове tock ( ) вызывается wait ( ) , отсутствует соответствующий вызов notify(), который позволил бы tock ( ) завершить работу. Таким образом, tock ( ) просто остается в состоянии бесконечного ожидания. Если у вас еще остаются какие-то сомнения относительно того, что вызовы wait ( ) и notify ( ) действительно необходимы для корректной работы класса имитации часов, тогда замените код класса TickTock в предыдущей программе приведенной ниже версией, в которой удалены все вызовы wait ( ) и notify(). // Вызовы wait() или notify () отсутствуют. class TickTock { String state; // хранит состояние часов synchronized void tick( boolean running ) ( // остановить часы if(!running) { state = "ticked"; return; . . } } System.out.print("Tick "); // установить текущее состояние в "после state = "ticked"; тик I п Глава 1 1 . Многопоточное программирование 445 synchronized void tock( boolean running ) { // остановить часы if(!running) { state = "tocked"; return; } System.out.printIn("Tock"); // установить текущее состояние в "после ток state = "tocked"; I м } } После замены кода вывод, генерируемый программой , будет выглядеть так: Tick Tick Tick Tick Tick Tock Tock Tock Tock Tock Очевидно, что методы tick ( ) и tock ( ) больше не работают вместе! !?@>A8< C M:A?5@B0 ВОПРОС. Часто приходилось слышать, что к неправильно работающим многопоточным программам применяется термин взаимоблокировка. Что такое взаимоблокировка и как ее избежать? А еще , что такое состояние гонок и как его не допустить? ОТВЕТ. Взаимоблокировка , как следует из названия, представляет собой ситуацию, в которой один поток ожидает, пока другой поток что-то сделает, но этот другой поток ожидает первого. Таким образом , оба потока приостанавливаются , ожидая друг друга , и ни один из них не выполняется. Ситуация похожа на то, когда два чрезмерно вежливых человека настаивают на том , чтобы именно другой первым прошел в дверь! Избежать взаимоблокировки кажется легко, но это не так. Например, взаи моблокировка может возникать обходными путями. Причину взаимоблоки ровки часто нелегко понять, просто взглянув на исходный код программы , поскольку одновременно выполняющиеся потоки могут взаимодействовать сложным образом во время выполнения. Чтобы избежать взаимоблокировки, требуется аккуратно программировать и тщательно тестировать. Помните , что если многопоточная программа время от времени “ зависает ” , то вероятной причиной является взаимоблокировка . Состояние гонок возникает, когда два ( или более ) потока пытаются одновре менно получить доступ к общему ресурсу без надлежащей синхронизации. Например, один поток может помещать в переменную новое значение, в то время как другой поток инкрементировать ее текущее значение. Без синхронизации итоговое значение переменной будет зависеть от порядка вы полнения потоков. ( Инкрементирует второй поток исходное значение или же первый поток установит новое значение?) — 446 Java: руководство для начинающих, 9-е издание В подобных ситуациях говорят, что два потока “ соперничают друг с другом ” , и окончательный результат определяется тем , какой поток завершит работу первым. Как и взаимоблокировка , состояние гонок может возникать трудно обнаруживаемым образом. Поэтому его лучше предотвращать заранее , должным образом синхронизируя доступ к общим ресурсам на стадии программирования. Приостановка, возобновление и останов потоков Временами полезно приостанавливать выполнение потока. Например , отдельный поток можно применять для отображения времени суток. Если пользователю не нужны часы , тогда его поток можно приостановить. В любом случае приостановить поток довольно просто. После приостановки перезапустить по- ток тоже легко. Механизмы приостановки, останова и возобновления потоков различаются между ранними версиями Java и более современными версиями, начиная с Java 2. До выхода Java 2 в программе использовались методы suspend(), resume ( ) и stop ( ) , которые определены в классе Thread и предназначены для приостановки, возобновления и останова выполнения потока. Они имеют следующие формы: final void resume( ) final void suspend() final void stopO Хотя упомянутые методы кажутся разумным и удобным подходом к управлению выполнением потоков, они не должны применяться в новых программах на Java и вот почему. В версии Java 2 метод suspend ( ) класса Thread был объявлен нерекомендуемым . Так поступили из- за того , что suspend ( ) мог иногда становиться причиной серьезных системных отказов. Метод resume ( ) тоже не рекомендуется использовать. Проблем он не вызывает, но его нельзя применять без дополняющего метода suspend ( ) . Метод stop ( ) класса Thread также объявлен нерекомендуемым в Java 2. Причина в том , что иногда он мог приводить к серьезным системным отказам. Поскольку применять методы suspend(), resume ( ) или stopO для управления потоком теперь нельзя , вам может показаться, что больше не существует какого-либо способа для приостановки , перезапуска или завершения работы потока . К счастью, это не так. Взамен поток должен быть спроектирован таким образом, чтобы метод run ( ) периодически проверял , должен ли поток приоста навливать, возобновлять или полностью останавливать собственное выполнение. Как правило , задача решается путем установления флаговой переменной , которая указывает состояние выполнения потока. Пока флаговая переменная установлена в состояние “ работает ” , метод run() должен продолжать работу, чтобы позволить потоку выполняться. Если флаговая переменная установлена в Глава 1 1 . Многопоточное программирование 447 состояние “ приостановить” , то поток должен быть приостановлен . Если же она установлена в состояние “ остановить ” , тогда поток должен завершить работу. В следующем примере демонстрируется один из способов реализации собственных версий методов suspend ( ) , restore ( ) и stop ( ) : // Приостановка, возобновление и останов потока. class MyThread implements Runnable { Thread thrd; — Когда значением является true, поток приостанавливается boolean suspended; * boolean stopped; м Когда значением является true, поток останавливается MyThread(String name) { thrd = new Thread(this, name); suspended = false; stopped = false; } * // Фабричный метод, который создает и запускает поток , public static MyThread createAndStart(String name) { MyThread myThrd = new MyThread(name); myThrd.thrd.start(); // запустить поток return myThrd; } // Точка входа в поток , public void run( ) { System.out.println("Поток " + thrd.getName() + " запущен."); try { for(int i = 1; i < 1000; i++) { System.out.print(i + " "); if((i%10)==0) { System.out.println(); Thread.sleep(250); } // Использовать блок synchronized для проверки // значений suspended и stopped , < synchronized( this) { while(suspended) { wait(); В этом блоке synchronized проверяются значения переменных } suspended и stopped if(stopped) break; } } } catch (InterruptedException exc) { System.out.println("Поток " + thrd.getName() + " прерван."); } System.out.println("Поток " + thrd.getName() + " завершен."); } // Останавливает поток , synchronized void mystop() { stopped = true; 448 Java: руководство для начинающих, 9-е издание // Следующие операторы гарантируют, что приостановленный // поток может быть остановлен , suspended = false; notify(); } // Приостанавливает поток , synchronized void mysuspendO { suspended = true; } // Возобновляет выполнение потока , synchronized void myresumeO { suspended = false; notify(); } } class Suspend { public static void main(String[] args) { MyThread mtl = MyThread.createAndStart("My Thread"); try { Thread.sleep(1000); // позволить потоку obi начать выполнение mtl.mysuspendO ; System.out.println("Приостановка потока."); Thread.sleep(1000); mtl.myresume(); System.out.println("Возобновление выполнения потока."); Thread.sleep(1000); mtl.mysuspend(); System.out.println("Приостановка потока."); Thread.sleep(1000); mtl.myresume(); System.out.println("Возобновление выполнения потока."); Thread.sleep(1000); mtl.mysuspend(); System.out.println("Останов потока."); mtl.mystop(); } catch (InterruptedException e) { System.out.println("Главный поток прерван."); } // Ожидать завершения работы потока , try { mtl.thrd.join(); } catch (InterruptedException e) { System.out.println("Главный поток прерван."); } System.out.println("Главный поток завершен."); } } Ниже показан вывод, полученный в результате запуска программы (у вас вы вод может слегка отличаться ): Глава 1 1 . Многопоточное программирование 449 Поток Му Thread запущен. 1 2 3 4 5 6 7 8 9 10 I I 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 Приостановка потока. Возобновление выполнения потока. 41 51 61 71 42 52 62 72 43 53 63 73 44 54 64 74 45 55 65 75 46 56 66 76 47 57 67 77 48 58 68 78 49 59 69 79 50 60 70 80 Приостановка потока. Возобновление выполнения потока. 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 I I I 112 113 114 115 116 117 118 119 120 Останов потока. Поток Му Thread завершен. Главный поток завершен. Вот как работает программа . В классе потока MyThread определены две булевские переменные , suspended и stopped , которые управляют приостановкой и завершением потока. Обе они инициализируются в конструкторе значением false. Метод run ( ) содержит блок synchronized, в котором проверяется suspended. Если эта переменная равна true , тогда вызывается метод wait ( ) , чтобы приостановить выполнение потока. Для приостановки выпол нения потока понадобится вызвать метод my suspend ( ) , который устанавливает suspended в true. Чтобы возобновить выполнение , необходимо вызвать метод myresume ( ) , который устанавливает suspended в false и вызывает notify ( ) для перезапуска потока. Для останова потока следует вызвать метод my stop ( ) , который устанавливает stopped в true. Кроме того , mystopO устанавливает suspend в false , а затем вызывает notify ( ) . Такие шаги нужны для останова приостановленного потока. !?@>A8< C M:A?5@B0 ВОПРОС. Многопоточность кажется отличным способом повышения эффективности программ . Какие рекомендации можно дать по ее эффективному использованию? ОТВЕТ. Ключом к эффективному использованию многопоточности является мышление категориями параллельного, а не последовательного выполнения. Скажем , если в программе есть две подсистемы , которые полностью незави симы друг от друга , то имеет смысл подумать о том , чтобы организовать их в виде отдельных потоков. Однако следует сделать несколько предостережений. Если вы создаете слишком много потоков , то фактически можете сни зить производительность своей программы, а не повысить ее. Не забывайте о том, что с переключением контекста связаны накладные расходы . Если вы создадите слишком много потоков, то больше времени ЦП будет тратиться на переключение контекста, а не на выполнение самой программы! 450 Java: руководство для начинающих, 9-е издание Использование главного потока Все программы на Java имеют, по крайней мере, один поток UseMain.java выполнения , называемый главным потоком, который автома тически предоставляется программе при ее запуске . До сих пор главный поток принимался как нечто данное. В этом проекте вы увидите , что главный поток можно обрабатывать точно так же , как все остальные потоки. 1. Создайте файл по имени UseMain.java. 2. Для доступа к главному потоку понадобится получить ссылающийся на него объект Thread, вызвав метод currentThread ( ) . Этот метод является статическим членом класса Thread и имеет следующий общий вид: Упражнение 11.2 static Thread currentThread () Метод currentThread ( ) возвращает ссылку на поток , в котором он вызывается . Таким образом , если во время выполнения вызвать currentThread ( ) внутри главного потока , то будет получена ссылка на главный поток. Через эту ссылку главным потоком можно управлять точ но так же , как любым другим потоком. 3. Поместите в файл UseMain.java приведенный ниже код программы . В коде получается ссылка на главный поток, после чего получаются и устанавливаются имя и приоритет главного потока. /* Упражнение 11.2. Управление главным потоком. */ class UseMain { public static void main(String[] args) { Thread thrd; // Получить главный поток , thrd = Thread.currentThread(); // Отобразить имя главного потока. System.out.println("Имя главного потока: " + thrd.getName()); // Отобразить приоритет главного потока. System.out.println("Приоритет главного потока: " + thrd.getPriority()); System.out.println (); // Установить имя и приоритет. System.out.println("Установка имени и приоритета.\п"); thrd.setName("Thread #1"); thrd.setPriority(Thread.N0RM_PRI0RITY+3); } } System.out.println("Новое имя главного потока: " + thrd.getName()); System.out.println("Новый приоритет главного потока: " + thrd.getPriority()); Глава 1 1 . Многопоточное программирование 451 4. Вот вывод, генерируемый программой: Имя главного потока: main Приоритет главного потока: 5 Установка имени и приоритета. . Новое имя главного потока: Thread #1 Новый приоритет главного потока: 8 5 Важно соблюдать осторожность в том , какие операции выполняются с главным потоком. Например, если добавить следующий код в конец ме тода main ( ) , то программа никогда не завершится, потому что она будет ожидать завершения главного потока! try { thrd.join(); } catch(InterruptedException exc) { System.out.println("Прервано."); } . Вопросы и упражнения для самопроверки 1 Каким образом многопоточность Java обеспечивает написание более эффективных программ? 2 Многопоточность поддерживается классом и интерфейсом . 3. Почему при создании исполняемого объекта может потребоваться расширить класс Thread, а не реализовать интерфейс Runnable? 4. Покажите , как использовать метод join(), чтобы организовать ожидание завершения работы потока по имени MyThrd. 5. Покажите , как установить приоритет потока по имени MyThrd на три уровня выше нормального. 6. Каков эффект от добавления ключевого слова synchronized к объявлению метода ? . 7. Методы wait ( ) и notify ( ) используются для организации 8. Модифицируйте класс TickTock, чтобы он действительно отсчитывал время. Каждый “ тик ” и каждый “ так ” должны занимать по полсекунды. Таким образом , каждый “ тик-так ” будет занимать одну секунду. ( Не беспокойтесь о времени, необходимом для переключения задач и т.п.) 9. Почему в новых программах нельзя применять методы suspend ( ) , resume ( ) и stop ( ) ? 10. С помощью какого метода , определенного в классе Thread, можно получить имя потока? 11. Что возвращает метод isAlive ( )? 12. Попробуйте самостоятельно добавить синхронизацию в класс Queue, разработанный в предшествующих главах , чтобы он стал безопасным для многопоточного использования . os . * • ' • •• • S•. S 4 s * .. S’ v* I I, I * X. 1! » • » I . ' s. •. i, i, . i ' 4' Л i 4 * I N sX* I 4, ix 4, •i >» X *»• >X » * Глава 12 Перечисления у автоупаковка У аннотации и многое другое " 454 Java: руководство для начинающих, 9-е издание В этой главе • • • z Основы перечислений z Использование возможностей перечислений, основанных на классах z Применение методов values ( ) и valueof О к перечислениям z Создание перечислений, которые имеют конструкторы, переменные экземпляра и методы z Использование методов ordinal ( ) и compareTo ( ) , унаследованных перечислениями от класса Enum z Применение оболочек типов Java z Основы автоупаковки и автораспаковки z Использование автоупаковки с методами z Работа автоупаковки с выражениями z Применение статического импортирования z Обзор аннотаций z Использование операции instanceof в этой главе обсуждается несколько важных функциональных средств Java . Интересно отметить, что четыре из них перечисления, автоупаковка , статическое импортирование и аннотации не входили в первоначальную специ фикацию Java , а все они были добавлены в версии JDK 5, но каждое значительно увеличивало мощь и удобство применения языка. Средства перечислений и автоупаковки удовлетворили давнюю потребность программистов. Статическое импортирование упростило использование статических элементов. Аннотации расширили виды информации , которую можно встраивать в исходный файл . Все вместе упомянутые функциональные средства предложили более эффективный способ решения общих задач программирования . Откровенно говоря , на сегодняшний день язык Java сложно представить без них. Они стали крайне важными. В главе также обсуждаются оболочки типов Java , которые обеспечи вают своего рода мост между примитивными и объектными типами. Наконец, будет представлена операция instanceof, которая позволяет проверять тип объекта во время выполнения . — — Глава 1 2. Перечисления, автоупаковка, аннотации и многое другое 455 Перечисления В своей простейшей форме перечисление представляет собой список именованных констант, который определяет новый тип данных. Объект типа перечисления может содержать только значения , которые указаны в списке. Таким образом , перечисление предоставляет способ точного определения нового типа данных, имеющего фиксированное количество допустимых значений. Перечисления часто встречаются в повседневной жизни. Например, в США используются монеты в один цент, пять центов, десять центов , двадцать пять центов, полдоллара и доллар. Перечисление месяцев в году содержит названия с января по декабрь. Перечисление дней недели включает понедельник , вторник, среду, четверг, пятницу, субботу и воскресенье . С точки зрения программирования перечисления удобны, когда необходимо определить набор значений, которые представляют коллекцию элементов. Например , перечисление можно применять для представления кодов состояния , таких как успех , ожидание , отказ или повтор , отражающих ход работы какой то операции. В ранних версиях Java такие значения определялись с помощью переменных final, но перечисления предлагают гораздо более структурированный подход. Основы перечислений Перечисление создается с применением ключевого слова enum. Например, вот простое перечисление , в котором определен список различных видов транспортных средств: // Перечисление транспортных средств , enum Transport { CAR, TRUCK, AIRPLANE, TRAIN, BOAT } Идентификаторы CAR, TRUCK и т.д. называются константами перечисления. Каждая из них неявно объявляется как открытый статический финальный член Transport. Более того, их типом является тип перечисления , в котором они объявлены , в данном случае Transport. Таким образом , в языке Java такие константы называются самотипизированными , где “ само- ” относится к объем лющему перечислению. После того как перечисление определено, можно создать переменную этого типа. Однако, несмотря на то , что перечисления определяют тип класса , экземпляр перечисления не создается с помощью new. Взамен переменная перечисления объявляется и используется почти так же, как переменная одного из примитивных типов. Например , ниже переменная tp объявляется как принадлежащая типу перечисления Transport: — Transport tp; 456 Java : руководство для начинающих, 9-е издание Поскольку tp относится к типу Transport, единственные значения, которые ей можно присваивать, определяются перечислением. Скажем , следующий оператор присваивает tp значение AIRPLANE: tp = Transport.AIRPLANE; Обратите внимание , что символ AIRPLANE предваряется типом Transport. Две константы перечисления можно сравнивать с применением операции отношения ==. Например, показанный далее оператор сравнивает значение в tp с константой TRAIN: if(tp == Transport.TRAIN) II ... Значение перечисления также можно использовать для управления оператором switch. Разумеется , во всех операторах case должны быть указаны константы из того же самого перечисления, что и в выражении switch. Например, приведенный ниже оператор switch совершенно допустим: // Использование перечисления для управления оператором switch. switch( tp) { case CAR: П ... case TRUCK: П . .. Обратите внимание , что в операторах case имена констант перечисления применяются без уточнения с помощью имени их типа перечисления , т.е. используется TRUCK, а не Transport.TRUCK. Причина в том , что тип перечисления в выражении switch уже неявно задает тип перечисления констант в case, так что нет никакой необходимости уточнять константы в операторах case посредством имени типа перечисления. На самом деле попытка сделать это вызовет ошибку на этапе компиляции . При отображении константы перечисления, скажем , в операторе printIn(), выводится ее имя. Например , в результате выполнения следующего оператора отобразится имя BOAT: System.out.println (Transport.BOAT); В показанной ниже программе объединены все рассмотренные ранее фрагменты кода с целью демонстрации работы с перечислением Transport: I I Перечисление разнообразных транспортных средств , enum Transport { CAR, TRUCK, AIRPLANE, TRAIN, BOAT * Объявление перечисления - } class EnumDemo { public static void main(String[] args) { Transport tp; < Объявление ссылки на Transport tp = Transport.AIRPLANE;* Присваивание tp константы AIRPLANE < * Глава 1 2. Перечисления, автоупаковка, аннотации и многое другое 457 // Вывести значение перечисления. System.out.println("Значение tp: " + tp); System.out.println(); tp = Transport.TRAIN; // Сравнить два значения перечисления. if(tp == Transport.TRAIN) System.out.println ("tp содержит TRAIN.\n"); Сравнение двух объектов Transport на предмет равенства // Использовать перечисление для управления оператором switch , switch(tp) { Использование перечисления для управления оператором switch case CAR: System.out.println("Автомобиль перевозит людей."); break; case TRUCK: System.out.println("Грузовик доставляет грузы ."); break; case AIRPLANE: System.out.println("Самолет летит."); break; case TRAIN: System.out.println ("Поезд движется по рельсам."); break; case BOAT: System.out. println("Корабль плывет по воде."); break; } } } Вот вывод, генерируемый программой: Значение tp: AIRPLANE tp содержит TRAIN. Поезд движется по рельсам. Прежде чем двигаться дальше , необходимо отметить один момент касательно стиля. Имена констант в Transport представлены в верхнем регистре. (Та ким образом, применяется CAR, а не саг.)Тем не менее, использовать буквы верхнего регистра не обязательно. Другими словами, не существует правила , требующего, чтобы имена констант перечисления были в верхнем регистре. Поскольку перечисления часто заменяют финальные переменные , которые тради ционно представляются в верхнем регистре, некоторые программисты считают, что написание имен констант перечисления с применением букв верхнего регистра тоже приемлемо. Конечно , есть и другие точки зрения и стили. Ради согласованности в примерах, приводимых в книге , имена констант перечисления будут представлены в верхнем регистре. 458 Java : руководство для начинающих, 9-е издание Перечисления Java являются типами классов Несмотря на то что в предшествующих примерах был продемонстрирован механизм создания и использования перечислений , они не отражали все их возможности. В отличие от того, как перечисления реализованы в ряде других языков , в Java перечисления реализованы как типы классов. Хотя вы не создаете экземпляр перечисления с помощью операции new, в остальном он действует аналогично другим классам . Тот факт, что перечисление определено в виде класса , предоставляет перечислению Java возможности, которых нет у перечислений в других языках. Например, перечисление может иметь конструкторы , переменные экземпляра и методы , а также реализовывать интерфейсы. Методы values ( ) и valueOf ( ) Все перечисления автоматически содержат в себе два предопределенных метода: values ( ) и valueOf ( ) со следующими общими формами: public static тип -переч [ ] values() public static тип -переч valueOf(String str) Метод values ( ) возвращает массив , содержащий список констант перечис ления. Метод valueOf ( ) возвращает константу перечисления, значение которой соответствует строке, переданной в аргументе str. В обоих случаях в тип-переч указывается тип данного перечисления. Скажем , для представленного ранее перечисления Transport возвращаемым типом Transport.valueOf("TRAIN") будет Transport, а возвращаемым значением TRAIN. В показанной далее программе иллюстрируется применение методов values ( ) и valueOf(): — // Использование встроенных методов перечисления. // Перечисление разнообразных транспортных средств. enum Transport { CAR, TRUCK, AIRPLANE, TRAIN, BOAT } class EnumDemo2 { public static void main(String[] args) { Transport tp; System.out.println("Все константы Transport:"); // Использовать метод values(). Transport[ ] allTransports = Transport.values(); for(Transport t : allTransports) System.out.println(t); Получить массив констант Transport System.out.println(); // Использовать метод valueOf(). tp = Transport.valueOf("AIRPLANE"); System.out.println("tp содержит " + tp); } } Получить константу с именем AIRPLANE Глава 1 2. Перечисления, автоупаковка, аннотации и многое другое 459 Вот вывод, генерируемый программой: Все константы Transport: CAR TRUCK AIRPLANE TRAIN BOAT tp содержит AIRPLANE Обратите внимание , что для прохода по массиву констант, полученному вы зовом values ( ) , в программе используется цикл for в стиле “ for-each ” . В качестве примера была создана переменная allTransports, которой присваивается ссылка на массив перечисления. Тем не менее , в таком шаге нет никакой необходимости, потому что цикл for можно было бы написать так, как показано ниже, устранив потребность в переменной allTransports: for(Transport t : Transport.values()) System.out.printIn(t); А теперь взгляните , как было получено значение , соответствующее имени AIRPLANE, с помощью вызова valueOf(): tp = Transport.valueOf("AIRPLANE"); Как уже объяснялось, метод valueOf ( ) возвращает значение перечисления, которое ассоциировано с именем константы, представленной в виде строки . Конструкторы, методы, переменные экземпляра и перечисления Важно понимать, что каждая константа перечисления является объектом своего типа перечисления. Таким образом , в случае определения конструктора для перечисления конструктор будет вызываться при создании каждой констан ты перечисления. Кроме того , каждая константа перечисления имеет собственную копию любых переменных экземпляра , определенных перечислением . В следующей версии перечисления Transport демонстрируется использование конструктора, переменной экземпляра и метода. Программа выводит типичную скорость движения для каждого транспортного средства . // Использование конструктора, переменной экземпляра и метода перечисления. Обратите enum Transport { внимание CAR(100), TRUCK(90), AIRPLANE(950), TRAIN(110), BOAT(35); на значения // автомобиль, грузовик, самолет, поезд, лодка инициализации private int speed; // типичная скорость транспортного средства // Конструктор. Transport(int s) { speed = s; } — int getSpeedO { return speed; } < * } Добавить переменную экземпляра Добавить конструктор Добавить метод 460 Java: руководство для начинающих, 9-е издание class EnumDemo 3 { public static void main ( String [] args ) { Transport tp; // Отобразить скорость самолета. System.out.println ("Типичная скорость самолета составляет " + Transport.AIRPLANE.getSpeed () + 1 " км /ч.\ n"); Получить значение скорости с помощью вызова getSpeed ( ) // Отобразить все транспортные средства вместе со скоростями. System.out.println ("Скорости движения всех транспортных средств: "); for ( Transport t : Transport . values( ) ) System.out.println ("Типичная скорость " + t + " составляет " + t .getSpeed () + " км / ч." ); } } Ниже показан вывод: Типичная скорость самолета составляет 950 км / ч. Скорости движения всех транспортных средств: Типичная скорость CAR составляет 100 км / ч. Типичная скорость TRUCK составляет 90 км /ч. Типичная скорость AIRPLANE составляет 950 км / ч. Типичная скорость TRAIN составляет 110 км / ч. Типичная скорость BOAT составляет 35 км / ч. В данную версию перечисления Transport добавлены три компонента. Первый переменная экземпляра speed, которая применяется для хранения конструктор Transport, котоскорости транспортного средства. Второй метод рому передается значение скорости транспортного средства . Третий ( ) возвращающий значение speed. getSpeed , Когда переменная tp объявляется в main() , конструктор Transport вы зывается по одному разу для каждой заданной константы. Обратите внимание на способ указания аргументов конструктора за счет их помещения в круглые скобки после каждой константы: — — — CAR ( 100 ), TRUCK ( 90 ), AIRPLANE ( 950 ), TRAIN ( llO ), BOAT ( 35 ); Эти значения передаются параметру s конструктора Transport(), который затем присваивает его переменной экземпляра speed. Кроме того , обратите внимание , что список констант перечисления заканчивается точкой с запятой, т.е. за последней константой BOAT следует точка с запятой. Когда перечисление содержит другие члены , список перечисления должен заканчиваться точкой с запятой. Поскольку каждая константа перечисления имеет собственную копию speed, получить скорость заданного транспортного средства можно вызовом метода getSpeed ( ) . Скажем , вот как выглядит вызов в методе main ( ) , предназначенный для получения скорости самолета: Transport .AIRPLANE.getSpeed ( ) Глава 1 2. Перечисления, автоупаковка, аннотации и многое другое 461 Скорости всех транспортных средств получаются путем прохода по пере числению с использованием цикла for. Ввиду того , что для каждой констан ты перечисления существует копия speed, значение , ассоциированное с одной константой , будет отдельным от значения , связанного с другой константой. Это мощная концепция, которая доступна только тогда , когда перечисления реализованы в виде классов, что и сделано в Java . Хотя в предыдущем примере содержится только один конструктор, перечисление может предлагать две или более перегруженных формы, как и любой другой класс. !?@>A8< C M:A?5@B0 ВОПРОС . Следует ли избегать использования финальных переменных после добавления перечислений в Java? Другими словами, стали ли финальные переменные устаревшими из-за появления перечислений? ОТВЕТ. Нет. Перечисление уместно в ситуации , когда приходится работать со списками элементов, которые должны быть представлены посредством идентификаторов. Финальная переменная подходит при наличии постоян ного значения вроде размер массива , которое будет использоваться во многих местах. Таким образом , с каждым средством связано свое применение. Преимущество перечислений в том , что финальные переменные не нужно использовать там, где они не подходят особо идеально. Два важных ограничения К перечислениям применяются два ограничения. Во-первых, перечисление не может быть унаследовано от другого класса. Во-вторых, перечисление не может служить суперклассом , т.е. перечисление расширять нельзя . В остальном перечисление действует подобно любому другому типу класса. Главное пом нить, что каждая из констант перечисления является объектом класса , в котором она определена . Перечисления унаследованы от Enum Несмотря на невозможность при объявлении перечисления наследо вать его от суперкласса , все перечисления автоматически унаследованы от java. lang.Enum. В этом классе определено несколько методов, доступных для использования всеми перечислениями. Чаще всего применять эти методы не придется , но есть два метода , которые время от времени будут задействованы: ordinal ( ) и compareTo(). 462 Java: руководство для начинающих, 9-е издание Метод ordinal ( ) получает значение , которое указывает позицию константы перечисления в списке констант. Оно называется порядковым номером. Вот общая форма метода ordinal( ) : final int ordinal( ) Метод ordinal ( ) возвращает порядковый номер константы, на которой вызывается. Порядковые номера начинаются с нуля. Таким образом , в перечислении Transport константа CAR имеет порядковый номер 0 , константа TRUCK порядковый номер 1, константа AIRPLANE порядковый номер 2 и т.д. Порядковые номера двух констант одного и того же перечисления можно сравнивать с применением метода сошрагеТо ( ) . Он имеет следующую общую — — форму: final int сошрагеТо( тип-переч е) - Здесь в тип переч задается тип перечисления , а е представляет собой константу, сравниваемую с вызывающей константой. Не забывайте , что вызывающая константа и е должны относиться к одному и тому же перечислению. Если вызывающая константа имеет порядковый номер меньше е, то метод сошрагеТо ( ) возвращает отрицательное значение. Если два порядковых номера совпадают, тогда возвращается ноль. Если вызывающая константа имеет порядковый номер больше е, то возвращается положительное значение. В приведенной далее программе демонстрируется использование методов ordinal ( ) и сошрагеТо ( ) : // Демонстрация использования методов ordinal() и сошрагеТо(). // Перечисление разнообразных транспортных средств. enum Transport { CAR, TRUCK, AIRPLANE, TRAIN, BOAT } class EnumDemo4 { public static void main(String[] args) { Transport tp, tp2, tp3; // Получить все порядковые значения с применением ordinal(). System.out.println("Все константы перечисления Transport" + " вместе с их порядковыми значениями: "); for(Transport t : Transport.values()) System.out.println(t + " " + t.ordinal()); Получить порядковые значения tp = Transport.AIRPLANE; tp2 = Transport.TRAIN; tp3 = Transport.AIRPLANE; System.out.println(); Сравнить порядковые значения // Демонстрация использования compareTo(). if(tp.compareTo(tp2) < 0) System.out.println(tp + " находится перед " + tp2); ^ if( tp.compareTo(tp2) > 0) System.out.println(tp2 + " находится перед " + tp); Глава 1 2. Перечисления, автоупаковка , аннотации и многое другое 463 if(tp.compareTo(tp3) == 0) System.out.println(tp + " равно " + tp3); } } Ниже показан вывод , генерируемый программой: Все константы перечисления Transport вместе с их порядковыми значениями: CAR 0 TRUCK 1 AIRPLANE 2 TRAIN 3 BOAT 4 AIRPLANE находится перед TRAIN AIRPLANE равно AIRPLANE Упражнение 12.1 Автоматизированный светофор Перечисления особенно полезны, когда в програмTrafficLightDemo.java ме нужен набор констант, но фактические значения констант произвольны — важно , чтобы они были разными. В программировании ситуация подобного рода возникает довольно часто. Распространенный пример связан с обработкой состояний , в которых может находиться некоторое устрой ство. Предположим, что разрабатывается программа для управления светофором. Ее код обязан автоматически переключать светофор между тремя состояниями света: зеленым , желтым и красным . Он также должен позволить другому коду выяснять текущий цвет света и позволять установить цвет света в известное начальное значение. Таким образом , три состояния должны быть как-то представлены. Хотя их можно было бы представить целочисленными значениями (скажем , 1 , 2 и 3)или строками (такими как "red" (красный) , "green" (зеленый) и "yellow" (желтый )), перечисление предлагает гораздо лучший подход. Использование перечисления приводит к более эффективному коду, нежели в случае представления состояний с помощью строк, и более структурированному, чем в ситуации, когда состояния были бы представлены посредством целых чисел . В этом проекте имитируется автоматизированный светофор, как было описано выше. Здесь не только показано перечисление в действии , но и демон стрируется еще один пример многопоточности и синхронизации . . 1 Создайте файл по имени TrafficLightDemo.java. 2. Начните с определения перечисления TrafficLightColor, которое представляет три состояния света светофора: // Перечисление цветов света светофора , enum TrafficLightColor { RED, GREEN, YELLOW } Всякий раз, когда требуется цвет света , используется значение перечисления TrafficLightColor. 464 Java: руководство для начинающих, 9-е издание 3. Начните определение класса TrafficLightSimulator, который инкапсулирует имитацию светофора: // Автоматизированный светофор. class TrafficLightSimulator implements Runnable { private TrafficLightColor tic; // хранит цвет светофора private boolean stop = false; // устанавливается в true private boolean changed = false; для остановки имитации // // имеет значение true, когда цвет света изменился // TrafficLightSimulator(TrafficLightColor init ) { tic = init; } TrafficLightSimulator() { tic = TrafficLightColor.RED; } Обратите внимание , что класс TrafficLightSimulator реализует интерфейс Runnable, поскольку для функционирования каждого светофора применяется отдельный поток, в котором будут циклически переключать- ся цвета. В классе TrafficLightSimulator определены два конструктора. Первый позволяет указать начальный цвет света, а второй по умолчанию устанавливает цвет светофора в красный. Теперь рассмотрим переменные экземпляра. Ссылка на поток имитации светофора хранится в thrd, а текущий цвет светофора в tic. Перемен ная stop используется для остановки имитации; изначально она имеет значение false. Имитация светофора будет функционировать до тех пор, пока переменная stop не будет установлена в true. Переменная changed равна true, когда свет изменился. — . 4 Добавьте метод run ( ) , который запускает имитацию работы светофора: // Запустить имитацию работы светофора , public void run() { while(!stop) { try { switch(tic) { case GREEN: Thread.sleep(10000); // зеленый на 10 секунд break; case YELLOW: // желтый на 2 секунды Thread.sleep(2000); break; case RED: Thread.sleep(12000); // красный на 12 секунд break; } } Глава 1 2. Перечисления, автоупаковка, аннотации и многое другое 465 catch(InterruptedException exc) { System.out.printIn(exc); } changeColor(); } } Метод run ( ) циклически переключает цвета светофора . Сначала выпол нение приостанавливается на соответствующий период времени в зави симости от текущего цвета, а затем вызывается метод changeColor ( ) для переключения на следующий цвет в последовательности. . 5 Добавьте метод changeColor(): // Изменить цвет , synchronized void changeColor() { switch(tlc) { case RED: tic = TrafficLightColor.GREEN; break; case YELLOW: tic = TrafficLightColor.RED; break; case GREEN: tic = TrafficLightColor.YELLOW; } changed = true; notify(); // сигнализировать об изменении цвета } В операторе switch проверяется цвет, хранящийся в текущий момент в tic, после чего tic присваивается следующий цвет из последовательности. Обратите внимание , что метод changeColor ( ) является синхронизи рованным, поскольку он вызывает notify ( ) для сигнализации о том, что произошло изменение цвета . ( Вспомните , что метод notify ( ) можно вызывать только из синхронизированного контекста. ) 6. Добавьте метод waitForChange ( ) , который организует ожидание до тех пор, пока цвет светофора не изменится: // Ожидать, пока не произойдет изменение цвета светофора , synchronized void waitForChange() { try { while( ! changed) wait(); // ожидать изменения цвета светофора changed = false; } catch(InterruptedException exc) { System.out.println(exc); } } 466 Java: руководство для начинающих, 9-е издание В методе waitForChange ( ) просто вызывается wait ( ) . Возврат из этого вызова не произойдет, пока changeColor ( ) не выполнит вызов notify(). Таким образом , метод waitForChange ( ) не возвращает управление до тех пор , пока цвет не изменится. 7. Добавьте метод getColor ( ) , который возвращает текущий цвет светофора , и метод Cancel ( ) , который останавливает поток имитации светофора, устанавливая stop в true: // Возвратить текущий цвет , synchronized TrafficLightColor getColor() { return tic; } // Остановить имитацию светофора , synchronized void cancel() { stop = true; } 8. Вот полный код программы , имитирующей работу светофора: // Упражнение 12.1. // Имитация работы светофора, использующая перечисление // для описания цветов света. // Перечисление цветов света светофора , enum TrafficLightColor { RED, GREEN, YELLOW } // Автоматизированный светофор. class TrafficLightSimulator implements Runnable { private TrafficLightColor tic; // хранит цвет светофора private boolean stop = false; // устанавливается в true для остановки имитации // private boolean changed = false; // имеет значение true, // когда цвет света изменился TrafficLightSimulator(TrafficLightColor init) { tic = init; } TrafficLightSimulator() { tic = TrafficLightColor.RED; } // Запустить имитацию работы светофора , public void run() { while(!stop) { try { switch(tic) { case GREEN: Thread.sleep(10000); // зеленый на 10 секунд break; Глава 1 2. Перечисления, автоупаковка, аннотации и многое Другое case YELLOW: Thread.sleep(2000); break; case RED: Thread.sleep(12000); break; // желтый на 2 секунды // красный на 12 секунд } } catch(InterruptedException exc) { System.out.println(exc); } changeColor(); } } // Изменить цвет , synchronized void changeColor() { switch(tic) { case RED: tic = TrafficLightColor.GREEN; break; case YELLOW: tic = TrafficLightColor.RED; break; case GREEN: tic = TrafficLightColor.YELLOW; } changed = true; notify(); // сигнализировать об изменении цвета } // Ожидать, пока не произойдет изменение цвета светофора , synchronized void waitForChange() { try { while(!changed) wait(); // ожидать изменения цвета светофора changed = false; } catch(InterruptedException exc) { System.out.println(exc); } } // Возвратить текущий цвет , synchronized TrafficLightColor getColorO { return tic; } // Остановить имитацию светофора , synchronized void cancel() { stop = true; } } 467 468 Java : руководство для начинающих, 9- е издание class TrafficLightDemo { public static void main(String[] args) { TrafficLightSimulator tl = new TrafficLightSimulator(TrafficLightColor.GREEN); Thread thrd = new Thread(tl); thrd.start(); for(int i=0; i < 9; i++) { System.out.println(tl.getColor()); tl.waitForChange(); } tl.cancel(); } } Ниже показан вывод, генерируемый программой. Как видите , цвета светофора переключаются в порядке зеленый, желтый и красный: GREEN YELLOW RED GREEN YELLOW RED GREEN YELLOW RED Обратите внимание, что применение перечисления упрощает и добавля ет структурированность коду, которому должно быть известно состояние светофора . Так как свет может иметь только три состояния ( красный , зеленый или желтый ) , перечисление гарантирует, что только эти значения допустимы, предотвращая случайное неправильное использование. 9. Предыдущую программу несложно улучшить, воспользовавшись возможностями класса перечисления. Например , за счет добавления в класс TrafficLightColor конструктора , переменной экземпляра и метода можно существенно усовершенствовать приведенный выше код. Такое улучшение оставлено в качестве упражнения (см . пункт 4 в разделе “ Во просы и упражнения для самопроверки ” ). Автоупаковка Современные версии Java включают два важных средства: автоупаковку и ав тораспаковку . Автоупаковка и автораспаковка значительно упрощают и опти мизируют код , который должен преобразовывать примитивные типы в объекты и наоборот. Поскольку такие ситуации часто встречаются в коде Java , преимущества автоупаковки и автораспаковки ощутят почти все программисты на Java. Как вы увидите в главе 13, автоупаковка и автораспаковка также в значительной степени способствуют удобству использования обобщений . Глава 1 2. Перечисления, автоупаковка, аннотации и многое другое 469 Автоупаковка и автораспаковка напрямую связаны с оболочками типов Java и со способом перемещения значений в экземпляр оболочки и из него. По этой причине мы начнем с обзора оболочек типов и процесса ручной упаковки и распаковки значений. Оболочки типов Как вам известно , в Java для хранения значений основных типов данных , поддерживаемых языком, используются примитивные типы (также называемые простыми типами ) вроде int или double. Использование для хранения таких величин примитивных типов вместо объектов объясняется стремлением увели чить производительность. Применение объектов для хранения этих значений привело бы к добавлению неприемлемых накладных расходов даже к самым простым вычислениям. Таким образом , примитивные типы не являются частью иерархии объектов и не наследуются от Object. Несмотря на преимущество в производительности, обеспечиваемое примитивными типами , бывают случаи , когда может понадобиться объектное представление. Скажем , передавать примитивный тип по ссылке в метод нельзя . Кроме того , многие стандартные структуры данных, реализованные в Java , ра ботают с объектами, следовательно , вы не можете использовать такие структуры данных для хранения значений примитивных типов. Чтобы справиться с этими ( и другими ) задачами , в языке Java предусмотрены оболочки типов, которые представляют собой классы, инкапсулирующие примитивный тип внутри объекта. Классы оболочек типов были представлены в главе 10, а здесь они обсуждаются более подробно. К оболочкам типов относятся Double, Float, Long, Integer, Short, Byte, Character и Boolean из пакета java.lang. Перечисленные классы предлагают широкий набор методов, позволяющих полностью интегрировать примитивные типы в иерархию объектов Java. Вероятно, наиболее распространенными оболочками типов являются те , которые представляют числовые значения , т.е. Byte, Short, Integer, Long, Float и Double. Все оболочки числовых типов унаследованы от абстрактного класса Number. В классе Number определены методы , которые возвращают значение объекта каждого числового типа: byte byteValueO double doubleValue() float floatValueO int intValue() long longValueO short shortValueO Например, метод doubleValue ( ) возвращает значение объекта как double, метод floatValueO как float ит.д. Указанные методы реализуются каждой оболочкой числового типа. — 470 Java: руководство для начинающих, 9- е издание Во всех оболочках числовых типов определены конструкторы , которые позволяют создавать объект из заданного значения или строкового представления этого значения. Например, вот конструкторы, определенные в классах Integer и Double: Integer(int num) Integer(String str) throws NumberFormatException Double(double num) Double(String str) throws NumberFormatException Если в аргументе str не содержится допустимое числовое значение, тогда сгенерируется исключение NumberFormatException. Однако, начиная с версии JDK 9, конструкторы оболочек числовых типов стали нерекомендуемыми к употреблению, а начиная с JDK 16 , они объявлены устаревшими и подлежащими удалению. В настоящее время для получения объекта оболочки настоятельно рекомендуется использовать один из методов valueOf ( ) . Метод valueOf() является статическим членом всех классов оболочек числовых типов и все числовые классы поддерживают формы , которые преобразуют числовое значение или его строковое представление в объект. Например , вот две формы , поддерживаемые в Integer: static Integer valueOf(int val) static Integer valueOf(String valStr) throws NumberFormatException В аргументе val указывается целочисленное значение , а в аргументе valStr строка, которая представляет надлежащим образом сформатирован ное числовое значение в строковом виде . Каждая форма метода valueOf( ) возвращает объект Integer, содержащий внутри заданную величину. Ниже при веден пример: Integer iOb = Integer.valueOf(100); После выполнения этого оператора значение 100 будет представлено экзем пляром Integer. Таким образом , объект iOb содержит в себе значение 100. Все оболочки типов переопределяют метод toString(), который возвращает удобочитаемую форму значения , содержащегося внутри оболочки. Он позволяет выводить значение за счет передачи объекта оболочки типа , например, в println(), не требуя преобразования объекта в примитивный тип . Процесс инкапсуляции значения внутри объекта называется упаковкой . В ранних версиях Java упаковка производилась вручную , когда программист явно конструировал экземпляр оболочки с желаемым значением , как только что было показано. Следовательно , говорят, что в предыдущем примере значе ние 100 заключено внутри iOb. Процесс извлечения значения из оболочки типа называется распаковкой. В ранних версиях Java вся распаковка тоже выполня лась вручную, когда программист явно вызывал метод оболочки для получения его значения. Скажем, вот как вручную распаковать значение из iOb в int: — int i = iOb.intValue(); - Глава 1 2. Перечисления, автоупаковка, аннотации и многое другое 471 Метод intValueO возвращает значение , инкапсулированное внутри iOb, как int. Описанные выше концепции демонстрируются в следующей программе: // Демонстрация ручной упаковки и распаковки с помощью оболочки типа , class Wrap { public static void main(String[] args ) { Integer iOb = new Integer.valueOf(100); int i = iOb.intValue(); Вручную упаковать значение 1 00 Вручную распаковать значение из iOb System.out.println(i + " " + iOb); // выводит 100 100 } } В программе целочисленное значение 100 помещается внутрь объекта Integer по имени iOb, после чего путем вызова метода intValue ( ) это значение получается и сохраняется в i. Наконец, значения i и iOb ( оба равные 100 ) отображаются. Та же самая общая процедура , которая использовалась в предыдущем при мере для ручной упаковки и распаковки значений, требовалась во всех версиях Java вплоть до JDK 5 и все еще может быть найдена в устаревшем коде. Пробле ма в том, что она утомительна и подвержена ошибкам , т.к. требует от программиста ручного создания соответствующего объекта для помещения в него зна чения и явного получения надлежащего примитивного типа , когда его значение необходимо. К счастью, автоупаковка и автораспаковка существенно улучшает эти важные процедуры. Основы автоупаковки — Автоупаковка это процесс , с помощью которого примитивный тип автоматически инкапсулируется (упаковывается ) в эквивалентную ему оболочку типа всякий раз, когда требуется объект такого типа. Нет необходимости явно создавать объект. Автораспаковка представляет собой процесс , при котором значение упакованного объекта автоматически извлекается ( распаковывается ) из оболочки типа , когда значение необходимо. Не придется вызывать методы вроде intValue ( ) или doubleValue(). Автоупаковка и автораспаковка значительно упрощают написание кода ряда алгоритмов , избавляя от утомительной ручной упаковки и распаковки значе ний, а также помогают предотвратить ошибки. Благодаря автоупаковке нет не обходимости вручную конструировать объект для применения в качестве обо лочки примитивного типа. Понадобится лишь присвоить значение ссылке на оболочку типа и объект будет создан автоматически. Скажем, вот современный способ объявления объекта Integer со значением 100: Integer iOb = 100; // автоупаковка значения int 472 Java: руководство для начинающих, 9-е издание Обратите внимание , что объект явно не упаковывается. Задачу автоматически решает компилятор Java . Чтобы распаковать объект, нужно просто присвоить ссылку на него переменной примитивного типа. Например, для распаковки iOb можно использовать такую строку: int i = iOb; // автораспаковка Обо всем остальном позаботится компилятор Java. Ниже приведена программа , где демонстрируется автоупаковка и автораспа ковка в действии: // Демонстрация работы автоупаковки/автораспаковки. class AutoBox { public static void main(String[] args) { Integer iOb = 100; // автоупаковка значения int int i = iOb; // автораспаковка Автоупаковка и автораспаковка значения 100 System ,out.println(i + " " + iOb); // выводит 100 100 } } Автоупаковка и методы В дополнение к простому случаю присваивания автоупаковка происходит всякий раз , когда примитивный тип должен быть преобразован в объект, а автораспаковка когда объект должен быть преобразован в примитивный тип. Таким образом , автоупаковка и автораспаковка могут быть инициированы при передаче аргумента методу или при возвращении методом значения. Например, взгляните на следующую программу: — // Автоупаковка и автораспаковка выполняются в отношении // параметров и возвращаемых значений методов. class AutoBox2 { // Этот метод принимает параметр типа Integer , static void m(Integer v) { Получить значение типа System.out.println ("m() получил значение " + v); Integer } // Этот метод возвращает значение типа int. static int m2() { Возвратить значение типа int return 10; } // Этот метод возвращает значение типа Integer. static Integer m3() { Возвратить значение типа Integer return 99; // автоупаковка 99 в Integer } Глава 1 2. Перечисления, автоупаковка, аннотации и многое другое 473 public static void main(String[] args ) { // Передать значение int методу m(). Поскольку m() принимает параметр // типа Integer, переданное значение int автоматически упаковывается , m(199); // Здесь ЮЬ получает значение int, возвращенное методом т2(). // Оно автоматически упаковывается, так что его можно присвоить iOb. Integer iOb = m2(); System.out.println("Возвращаемое значение метода m2(): " + iOb); // Затем вызывается метод m3(). Он возвращает значение Integer, // которое автоматически распаковывается в int. int i = m3(); System.out.println("Возвращаемое значение метода m3(): " + i); // Далее вызывается метод Math.sqrtO с iOb в качестве аргумента. // В этом случае iOb автоматически распаковывается и его значение // повышается до double, который является типом, необходимым для sqrt(). iOb = 100; System.out.println("Квадратный корень iOb: " + Math.sqrt(iOb)); } } Вот вывод, генерируемый программой: m() получил значение 199 Возвращаемое значение метода т2(): 10 Возвращаемое значение метода тЗ(): 99 Квадратный корень iOb: 10.0 Обратите внимание в программе , что ш() принимает параметр типа Integer. Внутри main ( ) методу т() передается целочисленное значение 100. Поскольку метод ш() ожидает объект Integer, значение 100 автоматически упаковывается. Затем вызывается метод m2 ( ) , возвращающий целочисленное значение 10, которое присваивается iOb в main ( ) . Поскольку iOb имеет типа Integer, происходит автоупаковка значения , возвращаемого методом m2 ( ) . Далее вызыва ется метод m3 ( ) , возвращающий объект Integer, который автоматически рас паковывается в int. Наконец, вызывается метод Math ,sqrt ( ) с iOb в качестве аргумента . В этом случае iOb автоматически распаковывается и его значение повышается до double, т.е. до типа , ожидаемого Math ,sqrt(). Автоупаковка / автораспаковка и выражения Как правило , автоупаковка и автораспаковка происходят всякий раз, когда требуется преобразование в объект или из объекта , что относится и к выражениям. Внутри выражения числовой объект автоматически распаковывается. При необходимости результат выражения упаковывается заново. Например , рассмотрим показанную ниже программу: 474 Java: руководство для начинающих, 9- е издание // Автоупаковка и автораспаковка происходят внутри выражений , class AutoBox3 { public static void main(String[] args) { Integer iOb, i0b2; int i; iOb = 99; System.out.println("Исходное значение iOb: " + iOb); // В следующем выражении iOb автоматически распаковывается, выполняется // инкрементирование, а результат снова упаковывается в iOb. ++iOb; System.out.println("После ++iOb: " + iOb); Автоупаковка и // В следующем выражении iOb распаковывается, // значение увеличивается на 10, // а результат упаковывается и сохраняется в iOb. iOb += 10; System.out.println("После iOb += 10: " + iOb); автораспаковка происходят внутри выражений // В следующем выражении iOb распаковывается, выражение вычисляется, // а результат заново упаковывается и сохраняется в ЮЬ2. < ЮЬ2 = iOb + (iOb / 3); System.out.println("iOb2 после выражения: " + iOb2); // Вычисляется то же самое выражение, // но результат не упаковывается , < i = iOb + (iOb / 3); System.out.println("i после выражения: " + i); } } Вот вывод, генерируемый программой: Исходное значение iOb: 99 После ++iOb: 100 После iOb += 10: 110 ЮЬ2 после выражения: 146 i после выражения: 146 Обратите особое внимание в программе на следующую строку: ++ ЮЬ ; Данный оператор инкрементирует значение в iOb. Он работает так: объект iOb распаковывается , значение инкрементируется , а результат повторно упако вывается . Благодаря автораспаковке числовые объекты вроде Integer можно использовать для управления оператором switch. В качестве примера взгляните на по казанный ниже фрагмент кода: Глава 1 2. Перечисления, автоупаковка, аннотации и многое Другое 475 Integer iOb = 2; switch(iOb) { case 1: System.out.println("один"); break; case 2: System.out.println("два"); break; default: System.out.println("ошибка"); } При вычислении выражения в switch осуществляется распаковка объекта iOb и получение его значения типа int. Как показывают примеры в программах, из- за автоупаковки/автораспаковки применение числовых объектов в выражении становится простым и интуитивно понятным. В ранних версиях Java такой код должен был включать приведения типов и вызовы методов, подобных intValue(). Предостережение По причине выполнения автоупаковки и автораспаковки может возник нуть соблазн пользоваться исключительно такими объектами, как Integer или Double, полностью отказавшись от примитивных типов. Например, при авто матической упаковке/ распаковке допускается написать код следующего вида: // Некорректное использование автоупаковки/автораспаковки! Double а, Ь, с; а = 10.2; b = 11.4; с = 9.8; Double avg = (a + b + c) / 3; В приведенном примере объекты типа Double содержат значения , которые усредняются и результат присваивается еще одному объекту типа Double. Хотя формально этот код корректен и действительно работает правильно , он демонстрирует крайне неудачное использование автоупаковки/автораспаковки. Он гораздо менее эффективен , чем эквивалентный код, написанный с применени ем примитивного типа double. Дело в том, что каждая автоупаковка и автораспаковка добавляют накладные расходы, которые отсутствуют в случае использования примитивного типа. Вообще говоря , вам следует ограничить применение оболочек типов только теми случаями , когда объектное представление примитивного типа обязательно. Автоупаковка/автораспаковка не добавлялась в Java как “ черный ход ” для устранения примитивных типов. Статическое импортирование В Java поддерживается расширенное использование ключевого сло ва import. Ключевое слово import вместе со static можно применять для 476 Java : руководство для начинающих, 9-е издание импортирования статических членов класса или интерфейса , что называется статическим импортированием. Статическое импортирование позволяет обра щаться к статическим членам напрямую по их именам , не уточняя именем их класса. В итоге упрощается и сокращается синтаксис , необходимый для обращения к статическому члену. Чтобы понять полезность статического импортирования , давайте начнем с примера , в котором оно не используется. В приведенной далее программе решается квадратное уравнение следующего вида: ах + Ьх + с = О В программе используются два статических метода из встроенно го в Java класса Math, который является частью java.lang. Первый метод, Math.powO , возвращает значение , возведенное в указанную степень. Второй метод, Math , sqrt(), возвращает квадратный корень своего аргумента. // Решение квадратного уравнения , class Quadratic { public static void main(String[] args) { // a, b и с представляют коэффициенты // уравнении ах2 + Ьх + с = 0. double a, b, с, х; // Решить уравнение 4x2 + х а = 4; b = 1; с = -3; - в квадратном 3 = 0. // Найти первый корень. х = (-b + Math.sqrt(Math.pow(b, 2) - 4 * a * c)) / (2 * a); System.out.println("Первый корень: " + x); // Найти второй корень. x (-b - Math.sqrt(Math.pow (b, 2) - 4 * a * c)) / (2 * a); System.out.println("Второй корень: " + x); } } Поскольку pow( ) и sqrt ( ) являются статическими методами , они должны вызываться с применением имени их класса Math, что приводит к несколько громоздкому выражению: х = (-b + Math.sqrt(Math.pow(b, 2) - 4 * a * c)) / (2 * a) ; Более того , необходимость указывать имя класса при каждом использовании pow() либо sqrt() ( или любых других методов класса Math, таких как sin(), cos ( ) и tan()) может стать утомительной. В новой версии предыдущей программы показано , каким образом можно избавиться от надоедливого указания имени класса через статическое импортирование: Глава 1 2. Перечисления , автоупаковка , аннотации и многое другое 477 // Использование статического импортирования для помещения // sqrt() и pow() в область видимости. import static java.lang.Math.sqrt; Использовать статическое импортирование для помещения import static java.lang.Math.pow; методов sqrt ( ) и pow ( ) в область видимости class Quadratic { public static void main(String[] args) { // a, b и с представляют коэффициенты в квадратном // уравнении ах2 + Ьх + с = 0. double а, Ъ , с , х; // Решить уравнение 4x2 + х - 3 = 0. а = 4; Ь = 1; с = -3; // Найти первый корень , х = (-b + sqrt(pow(b, 2) 4 * a * с)) / (2 * а); System.out.println("Первый корень: " + x); // Найти второй корень. х = ( b - sqrt(pow(b, 2) - 4 * a * c)) / (2 * a); System.out.println("Второй корень: " + x); ^ - } } В приведенной выше версии имена sqrt и pow помещаются в область види мости с помощью следующих операторов статического импортирования: import static java.lang.Math.sqrt; import static java.lang.Math.pow; Такие операторы устраняют необходимость в уточнении sqrt именем их класса . В итоге выражение становится более удобным: X (-b + sqrt(pow(b, 2) () или pow ( ) 4 * а * с)) / (2 * а); Легко заметить, что код такого рода значительно удобнее для восприятия. Есть две основные формы оператора import s t a t i c. Первая форма , которая применялась в предыдущем примере , помещает в область видимости одиночное имя и показана ниже: import static пакет.имя-типа.имя-статического-члена ; В конструкции имя - типа указывается имя класса или интерфейса , со держащего нужный статический член. Полное имя его пакета определяет ся конструкцией пакет. Имя статического члена указывается в конструкции имя -статического -члена . Вторая форма оператора import s t a t i c импортирует все статические члены заданного класса или интерфейса и выглядит следующим образом: import static пакет.имя-типа.*; Если вы собираетесь использовать много статических методов или полей , определенных в классе , тогда такая форма позволит поместить их в область видимости , не указывая каждый метод или поле по отдельности . Скажем , в 478 Java: руководство для начинающих, 9- е издание предыдущей программе можно было бы применять следующий единственный оператор import, чтобы поместить в область видимости методы pow ( ) и sqrt() ( наряду со всеми остальными статическими членами класса Math) : import static java.lang.Math.*; Разумеется , статическое импортирование не ограничивается только классом Math или только методами . Например, приведенный ниже оператор import static помещает в область видимости статическое поле System ,out: import static java.lang.System.out; После этого оператора можно выводить данные на консоль без указания System в out: out.println("После импортирования System.out можно использовать out напрямую."); Эффективность такого импортирования System.out является предметом споров. Хотя оно делает оператор короче , изучающим программу не сразу ста новится ясно, что под out понимается System ,out. Каким бы удобным ни было статическое импортирование , важно им не злоупотреблять. Не забывайте , что причиной организации библиотек Java в пакеты было стремление избежать конфликтов пространств имен. Импортирование статических членов приводит к их переносу в текущее пространство имен , тем самым увеличивая вероятность возникновения конфликтов между пространствами имен и непреднамеренного сокрытия имен. Если статический элемент используется в программе один или два раза, то лучше его не импортировать. К тому же некоторые статические имена , такие как System ,out, настолько узнаваемы , что импортировать их не имеет особого смысла. Статическое им портирование предназначено для тех ситуаций , в которых статический член применяется многократно , скажем, при выполнении последовательности математических расчетов. В сущности , вы должны использовать средство статического импортирования , но не злоупотреблять им. !?@>A8< C M:A?5@B0 ВОПРОС. Можно ли с помощью import static импортировать статические элементы классов, создаваемых самостоятельно? ОТВЕТ. Да, оператор import static можно применять для импортирования статических элементов классов и интерфейсов, которые создаете вы сами. Поступать так особенно удобно в ситуации , когда определено несколько статических членов, которые часто используются в крупной программе. Например, если в классе определен ряд констант static final, которые определяют различные ограничения, то применение import static для их помещения в область видимости избавит от утомительного набора с клави атуры . Глава 1 2. Перечисления, автоупаковка, аннотации и многое другое 479 Аннотации (метаданные ) Язык Java предлагает средство , позволяющее встраивать дополнительную информацию в файл исходного кода. Такая информация, называемая аннотаци ей , не меняет действия программы , оставляя ее семантику неизменной . Однако данная информация может использоваться разнообразными инструментами и во время разработки, и во время развертывания. Например, аннотацию может обрабатывать генератор исходного кода. Для обозначения этого средства также применяется термин метаданные, но термин аннотация является наиболее опи сательным и часто используемым. Аннотация обширная и сложная тема, и ее подробное рассмотрение выходит далеко за рамки настоящей книги. Тем не менее, в главе предлагается обзор , чтобы вы были знакомы с этой концепцией. — На заметку! Более детальное обсуждение аннотаций можно найти в книге Java. Полное руковод ство, 12 -е изд. ( пер. с англ., ООО "Диалектика", 2023 г.). Аннотация создается с помощью механизма , основанного на интерфейсе. Вот простой пример: // Простой тип аннотации. © interface MyAnno { String str(); int val (); } Здесь объявлена аннотация по имени MyAnno. Первым делом обратите вни мание на символ @ перед ключевым словом interface. Он сообщает компи лятору о том , что объявляется тип аннотации. Далее обратите внимание на два члена , str ( ) и val ( ) . Все аннотации состоят исключительно из объявлений методов. Тем не менее , вы не предоставляете тела для этих методов. Взамен их реализует компилятор Java. Более того , как вы увидите, методы во многом похожи на поля . Все типы аннотаций автоматически расширяют интерфейс Annotation, который является суперинтерфейсом для всех аннотаций . Он объявлен в пакете java.lang.annotation. Первоначально аннотации применялись для аннотирования только объявлений. При таком использовании любой тип объявления может иметь связанную с ним аннотацию. Скажем, можно аннотировать классы , методы, поля , параметры и константы перечислений. Аннотировать допускается даже саму аннотацию. В случаях подобного рода аннотация предшествует остальной части объявления . Начиная с версии JDK 8, можно аннотировать сценарии использования типов вроде приведения или возвращаемого типа метода. 480 Java: руководство для начинающих, 9-е издание В случае применения аннотации ее элементам присваиваются значения . Ниже показан пример применения аннотации МуАппо к объявлению метода: // Аннотировать метод. © МуАппо(str = "Пример аннотации", val = 100) public static void myMeth() { I I . .. Аннотация МуАппо связывается с методом myMethO . Внимательно взгля ните на синтаксис аннотации. За именем аннотации, которому предшествует находится заключенный в круглые скобки список инициализаций символ членов. Чтобы предоставить члену значение , его понадобится присвоить име ни члена. Следовательно , в приведенном выше примере члену s t r аннотации МуАппо присваивается строка " Пример аннотации". Обратите внимание , что в этом присваивании после s t r никаких скобок нет. При предоставлении значения члену аннотации используется только его имя. Таким образом , в данном контексте члены аннотации выглядят как поля. Аннотации, не имеющие параметров , называются маркерными аннотациями. Они указываются без передачи каких-либо аргументов и без использования круглых скобок. Их единственная цель пометить элемент каким-то атрибутом. В Java определено множество встроенных аннотаций. Большинство из них специализированы , но девять имеют общее назначение. Четыре аннотации импортируются из пакета java . lang . annotation: @ Retention , @ Documented , @ T a r g e t и @ I n h e r i t e d . Пять аннотаций , @ Override , @ D e p r e c a t e d , @ SafeVarargs , © Functionallnterface и @ SuppressWarnings , входят в состав пакета java . lang. Упомянутые аннотации кратко описаны в табл . 12.1. — Таблица 12.1 . Встроенные аннотации общего назначения Аннотация Описание @ Retention Указывает политику хранения, которая будет связана с аннотацией. Политика хранения определяет, насколько долго аннотация присутствует в процессе компиляции и развертывания © Documented Маркерная аннотация, которая сообщает инструменту о том, что аннотация должна быть документирована. Она предназначена для использования только в качестве аннотации к объявлению аннотации © Target С помощью аннотации @ Target задаются типы элементов, к которым может применяться аннотация Она спроектирована для использования только в качестве аннотации к другой аннотации. Аннотация © Target принимает один аргумент, представляющий собой массив констант перечисления ElementType , таких как CONSTRUCTOR, FIELD и METHOD. В этом аргументе указываются типы объявлений, к которым может применяться аннотация. Если аннотация © Target не указа- . на, тогда аннотацию можно использовать в любом объявлении Глава 1 2. Перечисления, автоупаковка, аннотации и многое другое 481 Окончание табл. 1 2 . 7 Аннотация Описание @Inherited Маркерная аннотация, которая приводит к тому, что аннота- @Override Метод с аннотацией @Override должен переопределять метод из суперкласса, иначе возникнет ошибка на этапе компиляции. Она применяется для гарантирования того, что метод суперкласса действительно переопределен, а не просто перегружен. Аннотация @Override является маркерной Аннотация @Deprecated указывает, что элемент устарел и использовать его не рекомендуется. Начиная с JDK 9, анно тация @ Deprecated также позволяет указать версию Java, в которой было заявлено об устаревании, и планируется ли ция для суперкласса наследуется подклассом @Deprecated удаление устаревшего элемента QSafeVarargs Маркерная аннотация, которая указывает на отсутствие небезопасных действий, связанных с аргументом переменной длины, в методе или конструкторе. Может применяться к методам и конструкторам с различными ограничениями @SuppressWarnings Аннотация @SuppressWarnings указывает, что одно или несколько предупреждений, которые могут быть выданы компилятором, должны быть подавлены. Предупреждения, подлежащие подавлению, задаются по имени в строковой форме @FunctionalInterface Маркерная аннотация, которая используется в объявлениях интерфейсов. Она указывает на то, что аннотированный интерфейс является функциональным интерфейсом. Функциональный интерфейс — это интерфейс, который содержит один и только один абстрактный метод. Функциональные интерфейсы задействованы в лямбда -выражениях. ( Подробные сведения о функциональных интерфейсах и лямбда-выражениях ищите в главе 14.) Важно понимать, что аннотация @FunctionalInterface не нужна для создания функционального интерфейса. Любой интерфейс, имеющий в точности один абстрактный метод, по определению будет функциональным интерфейсом На заметку! Начиная с версии JDK 8, пакет java .lang.annotation также включает аннотации @Repeatable и @Native. Аннотация @Repeatable поддерживает повторяющиеся аннотации, представляющие собой аннотации, которые могут применяться к одиночному элементу более одного раза . Аннотация @Native используется для аннотирования константного поля, доступ к которому получает исполняемый (т.е. машинный ) код. Обе они являются аннотациями специального назначения, обсуждение которых выходит за рамки настоящей книги. 482 Java: руководство для начинающих, 9-е издание Ниже приведен пример применения аннотации @ Deprecated для пометки класса MyClass и метода getMsg ( ) . Попытка компиляции этой программы приведет к выдаче предупреждений , сообщающих об использовании устаревших элементов. // Пример использования аннотации @Deprecated. // Пометить класс как устаревший. @Deprecated class MyClass { - Пометить класс как устаревший private String msg; MyClass(String m) { msg = m; } // Пометить метод внутри класса как устаревший. @Deprecated String getMsg() { Пометить метод как устаревший return msg; } II ... } class AnnoDemo { public static void main(String[] args) { MyClass myObj = new MyClass("test"); System.out.println(myObj.getMsg()); } } Интересно отметить, что за прошедшие годы несколько элементов в библи отеке Java API устарели , а по мере развития Java могут быть объявлены допол нительные устаревания . Помните , хотя устаревшие элементы Java API все еще доступны , использовать их не рекомендуется. Обычно вместо устаревшего элемента Java API предлагается альтернатива. Введение в операцию instanceof Иногда полезно знать тип объекта во время выполнения. Например , у вас может быть один поток выполнения, генерирующий объекты различных типов , и другой поток, который эти объекты обрабатывает. В такой ситуации может быть удобно, чтобы обрабатывающий поток знал типы всех объектов при их получении. Еще одна ситуация , когда важно знать тип объекта во время вы полнения , связана с приведением. В Java недопустимое приведение становится причиной ошибки времени выполнения. Многие недопустимые приведения могут быть обнаружены на этапе компиляции. Тем не менее , приведения , во влекающие иерархию классов, способны порождать недопустимые приведения, которые можно обнаружить только во время выполнения. Как было показано в главе 7, ссылка на суперкласс может ссылаться на объект подкласса. Таким образом , в некоторых случаях узнать на этапе компиляции , допустимо ли при ведение, когда оно включает ссылку на суперкласс и объекты подкласса , может быть невозможно. Глава 1 2. Перечисления, автоупаковка, аннотации и многое Другое 483 В качестве примера возьмем суперкласс Alpha, который имеет два подкласса: Beta и Gamma. В этой ситуации приведение объекта Beta к типу Alpha или приведение объекта Gamma к типу Alpha допустимо , поскольку оба они унаследованы от Alpha. (Другими словами , поскольку Beta и Gamma содержат Alpha, приводить к Alpha разрешено. ) Однако приведение объекта Beta к Gamma или наоборот будет недопустимым , т.к . хотя каждый из них унаследован от Alpha, в остальном они не связаны. Кроме того , приведение объекта Alpha к Beta или Gamma тоже окажется незаконным , потому что объект Alpha не содержит части Beta или Gamma. Учитывая тот факт, что ссылка Alpha может ссылаться на объекты Alpha, Beta или Gamma, как можно узнать во время выполнения , к какому типу объекта фактически относится ссылка , прежде чем попытаться привести объект, например , к Gamma? Объект может иметь тип Gamma, что будет допусти мым приведением, но также и тип Alpha либо Beta. В случае , если объект относится к типу Alpha или Beta, то приведение к Gamma будет недопустимым и сгенерируется исключение во время выполнения . Операция instanceof обеспечивает решение в этой и других похожих ситуациях. Первым делом необходимо отметить, что операция instanceof в версии JDK 17 была значительно улучшена благодаря новому мощному средству, основанному на сопоставлении с образцом . В текущей главе обсуждается традици онная форма операции instanceof, а ее расширенная форма рассматривается в главе 16. Вот общая форма традиционной операции instanceof: объектная-ссылка instanceof тип — Здесь объектная -ссылка представляет собой ссылку на объект, а тип тип класса или интерфейса. Если аргумент объектная-ссылка относится к указанному типу или может быть к нему приведен , то результатом вычисления опера ции instanceof является true. В противном случае результатом будет false. Таким образом , instanceof это инструмент, с помощью которого программа может выяснять во время выполнения , является ли объект экземпляром указанного типа . Работа традиционной операции instanceof демонстрируется в следующей программе: // Демонстрация работы традиционной операции instanceof. — class Alpha { // ... } class Beta extends Alpha { // . .. } class Gamma extends Alpha{ / / . .. } class InstanceOfDemo { 484 Java: руководство для начинающих, 9-е издание } public static void main(String[] args) { Alpha alpha = new Alpha(); Beta beta = new Beta(); Gamma gamma = new Gamma(); // Операция instanceof дает true, когда объект // имеет такой же тип, как указанный , if(alpha instanceof Alpha ) System.out.println("alpha является экземпляром Alpha"); if(beta instanceof Beta) System.out.println("beta является экземпляром Beta"); if(gamma instanceof Gamma) System.out.println("gamma является экземпляром Gamma"); // Операция instanceof дает true , когда объект // является экземпляром подкласса указанного типа , if(beta instanceof Alpha ) System.out.println("beta также является экземпляром Alpha"); if(gamma instanceof Alpha) System.out.println("gamma также является экземпляром Alpha"); // Следующий код не скомпилируется, потому что gamma // н е является экземпляром Beta или подкласса Beta. // if(gamma instanceof Beta ) System.out.println("Ошибка"); // Сделать так, что ссылка Alpha ссылалась на объект Beta , alpha = beta; // Поскольку alpha ссылается на объект Beta, операция instanceof // дает true и ссылка alpha может быть приведена к Beta , if(alpha instanceof Beta ) { System.out.println("alpha может быть приведена к Beta"); beta = (Beta) alpha; } // Здесь операция instanceof дает false, поскольку alpha ссылается // н а объект Beta, который не может быть приведен к Gamma. Подобным // образом предотвращается ошибка во время выполнения , if(alpha instanceof Gamma) { // Этот оператор не выполнится , gamma = (Gamma) alpha; // ошибка } } Ниже показан вывод: alpha является экземпляром Alpha beta является экземпляром Beta gamma является экземпляром Gamma beta также является экземпляром Alpha gamma также является экземпляром Alpha alpha может быть приведена к Beta Хотя в большинстве простых программ нет необходимости использовать операцию instanceof , поскольку тип объектов , с которыми приходится работать , известен , она может быть крайне полезна , когда задействованы объекты сложной иерархии классов . Как будет показано в главе 16 , усовершенствования сопоставления с образцом упрощают применение instanceof . Глава 1 2. Перечисления, автоупаковка, аннотации и многое другое 485 S Вопросы и упражнения для самопроверки 1 . Константы перечислений называют самотипизированными. Что это значит? 2. От какого класса автоматически наследуются все перечисления ? 3. Имея следующее перечисление , напишите программу, которая использует values ( ) для отображения списка констант и их порядковых значений. enum Tools { SCREWDRIVER, WRENCH, HAMMER, PLIERS } 4. Имитацию работы светофора , разработанную в упражнении 12.1, можно улучшить за счет внесения нескольких простых изменений, которые за действуют преимущества функциональных средств класса перечисления. В версии из упражнения 12.1 продолжительностью отображения каждого цвета управлял класс TrafficLightSimulator путем жесткого кодирования этих значений в методе run ( ) . Модифицируйте код так, чтобы продолжительность отображения каждого цвета сохранялась в константах перечисления TrafficLightColor. Для этого потребуется добавить кон структор, закрытую переменную экземпляра и метод getDelay ( ) . Какие улучшения вы заметили после внесения изменений? Можете ли вы само стоятельно придумать другие улучшения? ( Совет: попробуйте использовать порядковые значения для переключения цветов светофора , а не полагаться на оператор switch.) 5. Определите действия упаковки и распаковки. Каким образом автоупаков ка и автораспаковка влияют на эти действия? 6. Измените следующий фрагмент, чтобы в нем использовалась автораспаковка. Double val Double.valueOf(123.0); 7. Опишите своими словами, что делает статическое импортирование. 8. Что делает следующий оператор? import static java.lang.Integer.parselnt; 9. Предназначено ли статическое импортирование для особых случаев или же рекомендуется помещать в область видимости все статические члены 10. 11. 12. 13. 14. всех классов? Аннотация синтаксически основана на . аннотация маркерная Что такое ? Верно ли утверждение , что аннотация может быть применена только к методам? Какая операция позволяет выяснить, имеет ли объект указанный тип? Приведет ли недопустимое приведение во время выполнения к генерации исключения? •О % ч <> 4. > I * - ** * ••• m винэТпдодо рт eaevj . 'O s, •0*0' '• *** . I I II S’.: VV V; * , '1 s I.I '. l I ' .v * ’V ^^S\SV ' V' ' .. I » » »* 488 Java: руководство для начинающих, 9-е издание В этой главе • • • z Преимущества обобщений z Создание обобщенного класса z Применение ограниченных параметров типов z Использование аргументов с подстановочными знаками z Применение ограниченных аргументов с подстановочными знаками z Создание обобщенного метода z Создание обобщенного конструктора z Создание обобщенного интерфейса z Использование низкоуровневых типов z Применение выведения типов с помощью ромбовидной операции z Понятие стирания z Избегание ошибок неоднозначности z Ограничения обобщений с момента выхода первоначальной версии 1.0 в Java появилось много новых средств. Все они улучшали и расширяли возможности языка , но особенно глубокое и долгосрочное воздействие оказали обобщения, потому что их влияние ощущалось во всем языке Java. Например , обобщения добавили совершенно новый элемент синтаксиса и вызвали изменения во многих классах и методах основного Java API. Не будет преувеличением сказать, что включение обобщений коренным образом изменило характер Java . Тема обобщений довольно обширна , и некоторые аспекты достаточно сложны и выходят за рамки материала настоящей книги. Однако базовое понимание обобщений необходимо всем программистам на Java . На первый взгляд синтаксис обобщений может показаться немного пугающим , но не переживайте . Обобщения удивительно просты в использовании . Закончив чтение этой гла вы , вы будете иметь представление о ключевых концепциях, лежащих в основе обобщений , и достаточные знания для их эффективного применения в своих программах. Глава 1 3. Обобщения 489 !?@>A8< C M:A?5@B0 ВОПРОС. Говорят, что обобщения Java похожи на шаблоны в C ++. Так ли это? ОТВЕТ. Обобщения Java похожи на шаблоны в C ++. То, что в Java носит название параметризованного типа , в C ++ называется шаблоном. Однако обобщения Java и шаблоны C ++ - не одно и то же , и между этими двумя подходами к обобщенным типам существует ряд фундаментальных отличий. По большей части подход , принятый в Java, проще в использовании. Одно предостережение: если у вас есть опыт написания кода на C ++ , тогда важно не делать поспешных выводов о том , как работают обобщения в Java. Основы обобщений В своей основе термин обобщения означает параметризованные типы . Параметризованные типы важны , поскольку они позволяют создавать классы , интерфейсы и методы , где тип данных , с которым они работают, указывается в качестве параметра. Например , с использованием обобщений можно создать единственный класс , который автоматически работает с разными типами дан ных. Класс , интерфейс или метод, оперирующий на параметризованном типе , называется обобщенным. Принципиальное преимущество обобщенного кода связано с тем , что он будет автоматически работать с типом данных, переданным в его параметре типа. Многие алгоритмы логически одинаковы вне зависимости от того , к какому типу данных они применяются . Скажем , быстрая сортировка одинакова при сортировке элементов типа Integer, String, Object или Thread. С помощью обобщений можно определить алгоритм однократно и независимо от конкретного типа данных, после чего применять этот алгоритм к широкому спектру типов данных без каких-либо дополнительных усилий. Важно понимать, что язык Java всегда предоставлял возможность создавать обобщенные классы , интерфейсы и методы за счет оперирования ссылками типа Object. Поскольку Object является суперклассом для всех других классов, ссылка на Object позволяет ссылаться на объект любого типа . Таким образом , в исходном коде обобщенные классы , интерфейсы и методы при работе с различными типами объектов задействовали ссылки на Object. Проблема заключалась в том , что они не могли это делать с обеспечением безопасности в отношении типов. Таким образом , можно было случайно создать условия для несоответствия типов. Обобщения добавляют безопасность типов, которой не хватало , потому что они делают приведения подобного рода автоматическими и неявными. Короче говоря, обобщения расширяют возможности многократного использования кода с надлежащим уровнем безопасности и надежности. 490 Java: руководство для начинающих, 9-е издание Простой пример обобщения Прежде чем переходить к обсуждению теории, лучше всего рассмотреть простой пример обобщения . В показанной ниже программе определены два класса обобщенный класс Gen и класс GenDemo, использующий Gen. — // Простой обобщенный класс. // Здесь Т - параметр типа, который будет заменен // реальным типом при создании объекта типа Gen. Объявить обобщенный класс. class Gen<T> Т параметр обобщенного типа T ob; // объявить объект типа Т — // Передать конструктору ссылку на объект типа Т. Gen(Т о) { ob = о; } // Возвратить ob. Т get0b() { return ob; } // Отобразить тип Т. void showTypeO { System.out.println("Тип T: " + ob.getClass().getName()); } } // Демонстрация использования обобщенного класса , class GenDemo { public static void main(String[] args) { // Создать ссылку Gen для Integer. Gen<Integer> iOb; Создать ссылку на объект типа Gen < Integer > // Создать объект Gen<Integer> и присвоить iOb ссылку на него. // Обратите внимание на использование автоупаковки для // инкапсуляции значения 88 внутри объекта Integer. Создать объект типа iOb = new Gen<Integer>(88); Gen < Integer > // Отобразить тип данных, используемый iOb. iOb.showType(); // Получить значение iOb. Обратите внимание, // что приведение не требуется , int v = iOb.getObO ; System.out.println("значение: " + v); Создать ссылку и объект System.out.println(); типа Gen <String > // Создать объект Gen для String . Gen<String> strOb = new Gen<String>("Тестирование обобщений"); // Отобразить тип данных, используемый strOb. strOb.showType(); // Получить значение strOb. Снова обратите внимание, // что приведение не требуется. String str = strOb.getOb(); System.out.println("значение: " + str); } } Глава 13. Обобщения 491 Вот вывод, генерируемый программой: Тип Т: java.lang.Integer значение: 88 Тип Т: java.lang.String значение: Тестирование обобщений А теперь внимательно разберем программу. Прежде всего, обратите внимание на объявление Gen: class Gen<T> { — Здесь т имя параметра типа. Оно применяется в качестве заполнителя для фактического типа , который будет передан конструктору Gen при создании объекта. Таким образом , т используется внутри Gen всякий раз, когда требуется параметр типа. Обратите внимание , что т содержится внутри угловых скобок о. Такой синтаксис можно обобщить. Объявляемый параметр типа всегда указывается в угловых скобках. Поскольку Gen задействует параметр типа , Gen является обобщенным классом. В объявлении Gen имя Т не играет особой роли. Можно было бы применить любой допустимый идентификатор , но т используется по традиции. Кроме того, рекомендуется выбирать имена для параметров типов в виде односимвольных заглавных букв. Другими часто применяемыми именами параметров типов являются V и Е. Еще один момент относительно имен параметров типов: начиная с JDK 10, использовать var в качестве имени параметра типа не разрешено. Затем т применяется для объявления объекта ob: Т ob; // объявить объект типа Т — Как уже объяснялось, т это заполнитель для фактического типа , который указывается при создании объекта Gen. Таким образом, ob будет объектом типа , переданного в Т. Например, если в Т передается тип String, тогда ob получит тип String. Теперь взгляните , как выглядит конструктор класса Gen: Gen(T о) { ob = о; } Обратите внимание , что его параметр о имеет тип т , т.е . фактический тип о определяется типом , переданным в Т, когда создается объект Gen. Кроме того , поскольку и параметр о, и переменная -член ob относятся к типу т , при созда нии объекта Gen они будут иметь один и тот же фактический тип. Параметр типа т также можно использовать для указания возвращаемого типа метода , как в случае показанного далее метода getOb(): Т getOb() { return ob; } Из- за того , что ob также имеет тип т , его тип совместим с возвращаемым типом , заданным getOb(). 492 Java: руководство для начинающих, 9- е издание Метод showType ( ) выводит тип Т, вызывая getName ( ) на объекте Class, который возвращается вызовом getClass ( ) на ob. Данное средство пока еще не использовалось, поэтому рассмотрим его более подробно. Вспомните из главы 7, что метод getClass ( ) определен в Object и потому является членом всех типов классов. Он возвращает объект Class, соответствующий типу класса объекта , на котором вызывается . Класс Class находится в пакете java.lang и инкапсулирует информацию о классе. В Class определено несколько методов, которые можно применять для получения информации о классе во время выполнения. Среди них метод getName(), который возвращает строковое пред ставление имени класса . Работа с обобщенным классом Gen демонстрируется в классе GenDemo. Сна чала создается версия Gen для целых чисел : Gen<Integer> iOb; Внимательно взгляните на приведенное выше объявление. Первым делом обратите внимание на указание типа Integer в угловых скобках после Gen. В данном случае Integer аргумент типа , который передается параметру типа Gen, т.е. т. Фактически создается версия Gen, в которой все ссылки на Т транслируются в ссылки на Integer. Соответственно для такого объявления ob имеет тип Integer и возвращаемым типом getOb ( ) является Integer. Прежде чем двигаться дальше , важно отметить, что компилятор Java на самом деле не создает разные версии Gen или любого другого обобщенного класса. Хотя думать в таких терминах удобно , в действительности происходит иное компилятор удаляет всю информацию об обобщенном типе , заменяя необходимые приведения, чтобы код вел себя так, как если бы создавалась кон кретная версия Gen. Таким образом , в программе действительно существует только одна версия Gen. Процесс удаления информации об обобщенном типе называется стиранием, и позже в главе мы еще вернемся к этой теме . В следующей строке кода переменной ЮЬ присваивается ссылка на экзем пляр версии класса Gen для Integer: — — iOb = new Gen<Integer>(88); Обратите внимание , что при вызове конструктора класса Gen также указывается аргумент типа Integer. Причина в том, что объект ( в данном случае iOb), которому присваивается ссылка , имеет тип Gen<Integer>. Таким образом , ссылка , возвращаемая операцией new, тоже должна иметь тип Gen<Integer>. В противном случае возникнет ошибка на этапе компиляции. Скажем , показанное ниже присваивание вызовет ошибку на этапе компиляции: iOb = new Gen<Double>(88.0); // Ошибка! Поскольку переменная iOb относится к типу Gen<Integer>, ее нельзя применять для ссылки на объект Gen<Double>. Эта проверка типов является одним из основных преимуществ обобщений, т.к. она обеспечивает безопасность типов. Глава 1 3. Обобщения 493 Как было указано в комментариях внутри программы, присваивание задей ствует автоупаковку для инкапсуляции значения 8 8 , представляющего собой число типа int, в объект Integer: iOb = new Gen<Integer> (88); Прием работает, потому что в классе Gen<lnteger> определен конструктор , который принимает аргумент типа Integer. Так как ожидается объект Integer, компилятор Java автоматически поместит в него значение 88. Разумеется, при сваивание можно было бы записать и явно: iOb = new Gen<Integer> (Integer.valueOf(88)); Однако эта версия кода не принесет никакой пользы. Далее программа выводит тип ob внутри iOb, т.е . Integer, и затем получает значение ob с помощью следующей строки: int v = iOb.getObO ; Поскольку возвращаемым типом getOb() является Т, замененный типом Integer при объявлении iOb, возвращаемым типом getOb ( ) также оказывается Integer, который распаковывается в int, когда выполняется присваивание переменной v ( типа int) . Таким образом , нет нужды приводить возвращаемый тип getOb ( ) к Integer. Затем в классе GenDemo объявляется объект типа Gen<String>: Gen<String> strOb = new Gen<String>("Тестирование обобщений"); Так как аргументом типа является String, внутри Gen тип String подставля ется вместо Т, что (концептуально ) создает версию Gen для String, как демонстрируют оставшиеся строки в программе. Обобщения работают только со ссылочными типами При объявлении экземпляра обобщенного типа передаваемый параметру типа аргумент типа должен быть ссылочным типом. Примитивный тип вроде int или char применять нельзя. Скажем , для экземпляра Gen передать в Т можно любой тип класса , но передавать параметру типа какой-либо примитивный тип не разрешено. По этой причине следующее объявление будет недопусти мым: Gen<int> intOb = new Gen<int>(53); // Ошибка! Использовать примитивный // тип нельзя. Конечно , отсутствие возможности указать примитивный тип не считается серьезным ограничением , т.к. для инкапсуляции примитивного типа можно использовать оболочки типов (как было в предыдущем примере ). Вдобавок меха низм автоупаковки и автораспаковки в Java обеспечивает прозрачность работы с оболочками типов. 494 Java: руководство для начинающих, 9-е издание Обобщенные типы различаются на основе их аргументов типов Ключевой момент, который необходимо понять относительно обобщенных типов, связан с тем , что ссылка на одну специфическую версию обобщенного типа несовместима по типу с другой версией того же обобщенного типа. Напри мер, пусть только что показанная программа содержит строку кода с ошибкой и компилироваться не будет: iOb = strOb; // Ошибка! Несмотря на то что и iOb, и strOb относятся к типу Gen<T>, они являются ссылками на разные типы , т.к. их аргументы типов отличаются. Таким способом обобщения обеспечивают безопасность типов и предотвращают ошибки. Обобщенный класс с двумя параметрами типов В обобщенном типе допускается объявлять больше , чем один параметр типа. Чтобы указать два или более параметров типов, просто применяйте список , разделенный запятыми. Например , следующий класс TwoGen является разновидностью класса Gen с двумя параметрами типов: // Простой обобщенный класс с двумя параметрами типов: Т and V. class TwoGend, V > { Использовать два параметра типов T obi; V ob2; // Передать конструктору ссылки на объекты типов Т и V. TwoGen(Т ol, V о2) { obi = ol ; оЬ2 = о2; } // Отобразить типы Т и V. void showTypes( ) { System.out.println ("Тип T: " + obi.getClass().getName()); System.out.println ("Тип V: " + ob2.getClass().getName()); } T getObl() { return obi; } V get0b2() { return ob2; } } В T передается тип Integer, — а в V тип String // Демонстрация использования TwoGen. class SimpGen { public static void main(String[] args) { TwoGendnteger, String> tgObj = new TwoGendnteger, String>(88, "Обобщения"); ^ // Отобразить типы . tgObj.showTypes(); Глава 1 3. Обобщения 495 // Получить и отобразить значения , int v = tgObj.getObl(); System.out.println("значение: " + v); String str = tgObj.get0b2(); System.out.println("значение: " + str); } } Ниже показан вывод, генерируемый программой: Тип Т: java.lang.Integer Тип V: java.lang.String значение: 88 значение: Обобщения Обратите внимание на объявление TwoGen: class TwoGenCT, V> { В нем определены два параметра типов, Т и V, разделенные запятой. Поскольку он имеет два параметра типа , при создании объекта конструктору TwoGen должны быть переданы два аргумента типа: TwoGen<Integer, String> tgObj = new TwoGen<Integer, String>(88, "Обобщения"); Здесь Integer заменяет T, a String заменяет V. Хотя в приведенном примере два аргумента типов отличаются , они могут быть одинаковыми. Скажем, допустима следующая строка кода: TwoGen<String, String> х = new TwoGen<String, String>("A", "В"); В данном случае т и V будут иметь тип String. Разумеется, если бы аргумен ты типов всегда были одинаковыми , то два параметра типа оказались бы излишними. Общая форма обобщенного класса Синтаксис обобщений , показанный в предшествующих примерах , можно свести к общей форме. Вот синтаксис объявления обобщенного класса: class имя-класса< список-параметров-типов> { II ... А так выглядит полный синтаксис для объявления ссылки на обобщенный класс и создания экземпляра обобщенного класса: имя-класса< список-аргументов-типов> имя-переменной = new имя- класса< список-аргументов-типов> (список-аргументов-конструктора ) ; Ограниченные типы В предшествующих примерах параметры типов можно было заменить любым типом класса , что подходит для многих целей , но иногда полезно ограничить типы , которые разрешено передавать параметру типа. Предположим , что необходимо создать обобщенный класс, содержащий метод, который возвращает 496 Java: руководство для начинающих, 9-е издание среднее значение массива чисел . Кроме того , вы хотите использовать этот класс для получения среднего значения массива чисел любого типа , включая int, float и double. Таким образом , тип чисел желательно указывать обобщенно с применением параметра типа . Чтобы создать класс подобного рода , вы можете попробовать поступить примерно так: // (Безуспешная) попытка создать обобщенный класс NumericFns, // который мог бы выполнять разнообразные операции, такие // как вычисление обратной величины или дробной части // для чисел любого типа. class NumericFns<T> { Т num; // Передать конструктору ссылку на числовой объект. NumericFns(Т п) { п; num } // Возвратить обратную величину , double reciprocal() { return 1 / num.doubleValue(); // Ошибка! } // Возвратить дробную часть , double fraction() { return num.doubleValue() - num.intValue(); // Ошибка! } // . . . } К сожалению, в написанном виде код NumericFns не скомпилируется, потому что оба метода будут приводить к возникновению ошибок на этапе компи ляции. Первым делом возьмем метод reciprocal ( ) , который пытается возвратить обратную величину num. Для этого он должен разделить 1 на значение num. Значение num получается вызовом doubleValue ( ) , который получает версию double числового объекта , хранящегося в num. Поскольку все числовые классы , такие как Integer и Double, являются подклассами Number, а в классе Number определен метод doubleValue ( ) , данный метод доступен всем числовым классам -оболочкам . Проблема связана с тем , что компилятор не может заранее знать, что вы собираетесь создавать объекты NumericFns, используя только числовые типы. В итоге при компиляции кода NumericFns выдается сообщение об ошибке, указывающее на то , что метод doubleValue ( ) неизвестен . Для решения проблемы нужен какой-то способ уведомления компилятора о том , что в т планируется передавать только числовые типы. Вдобавок необходимо каким -то образом гарантировать, что действительно передаются только числовые типы. Для обработки таких ситуаций в Java предусмотрены ограниченные типы . Когда определяется параметр типа , вы можете создать верхнюю границу, объявляющую суперкласс, от которого должны быть порождены все аргументы ти пов. Цель достигается за счет применения конструкции extends при указании параметра типа: <Т extends суперкласс> Глава 1 3. Обобщения 497 Здесь указано , что тип т может быть заменен только суперклассом , ука занным в суперкласс, или подклассами этого суперкласса . Таким образом , в суперкласс определяется включающий верхний предел . Прием можно использовать для исправления показанного ранее класса NumericFns, указав Number в качестве верхней границы: // В этой версии класса NumericFns аргументом типа для Т должен // быть либо Number, либо класс, производный от Number , В этом случае аргументом типа должен class NumericFns<T extends Number> { быть Number или подкласс Number Т num; // Передать конструктору ссылку на числовой объект. NumericFns(Т п) { num п; } // Возвратить обратную величину , double reciprocal() { return 1 / num.doubleValue(); } // Возвратить дробную часть , double fraction( ) { return num.doubleValue() - num.intValue(); } И ... } 11 Демонстрация использования NumericFns. class BoundsDemo { public static void main(String[] args) { NumericFns<Integer> iOb* new NumericFns<Integer>(5); Тип Integer подходит, потому что является подклассом Number System.out.println("Обратная величина iOb: " + iOb.reciprocal()); System.out.println("Дробная часть iOb: " + iOb.fraction()); System.out.println(); NumericFns<Double> dOb = new NumericFns<Double>(5.25); System.out.println("Обратная величина dOb: " + dOb.reciprocal()); System.out.println("Дробная часть dOb: " + dOb.fraction()); // Следующий код не скомпилируется, т.к. String // не является подклассом Number. // NumericFns<String> strOb = new NumericFns<String>("Ошибка!"); } } Тип String не разрешен, поскольку не является подклассом Number 498 Java: руководство для начинающих, 9- е издание Вот вывод, генерируемый программой: Обратная величина iOb: 0.2 Дробная часть iOb: 0.0 Обратная величина dOb: 0.19047619047619047 Дробная часть dOb: 0.25 Обратите внимание, как теперь объявлен класс NumericFns: class NumericFns<T extends Number > { Поскольку теперь тип т ограничен классом Number, компилятору Java известно , что все объекты типа т могут вызывать метод doubleValue ( ) , т.к. он объявлен в Number. Это и само по себе считается большим преимуществом . Тем не менее, в качестве дополнительного бонуса ограничение т также предотвращает создание нечисловых объектов Stats. Скажем , если вы удалите символы комментария из строки в конце программы и затем попробуете скомпилировать код заново , то получите сообщения об ошибках на этапе компиляции , потому что String не является подклассом Number. Ограниченные типы особенно полезны , когда вам необходимо убедиться , что один параметр типа совместим с другим. Например , рассмотрим следующий класс по имени Pair, хранящий два объекта, которые должны быть совместимыми друг с другом: class Pair<T, V extends Т> { Т first; V second; Pair(T a, V b) { first = a; second = b; } II • Здесь параметр V должен иметь либо тот же тип, что и Т, либо быть подклассом Т ... } Обратите внимание , что в Pair используются два параметра типов , т и V, причем V расширяет т , т.е. V будет иметь либо тот же тип , что и т , либо быть подклассом Т. Это гарантирует, что два аргумента конструктора Pair будут объектами одного и того же типа или связанных типов. Скажем , показанные ниже конструкции совершенно допустимы: // Допустимо, потому что Т и V относятся к типу Integer. Pairdnteger, Integer> х = new Pair<Integer, Integer>(l, 2); // Допустимо, потому что Integer является подклассом Number. Pair<Number, Integer> у = new Pair<Number, Integer>(10.4, 12); Однако следующая конструкция недопустима: // Ошибка из-за того, что String не является подклассом Number. Pair<Number, String> z = new Pair<Number, String>(10.4, "12"); В данном случае String не является подклассом Number, что нарушает ограничение , указанное в классе Pair. Глава 1 3. Обобщения 499 Использование аргументов с подстановочными знаками Какой бы полезной ни была безопасность к типам , иногда она может стать препятствием для совершенно приемлемых конструкций . Скажем , имея класс NumericFns, показанный в конце предыдущего раздела , предположим , что вы хотите добавить метод по имени absEqual ( ) , который возвращает true, если два объекта NumericFns содержат числа с одинаковыми абсолютными значениями. Более того , желательно, чтобы метод мог надлежащим образом работать независимо от типа числовых данных в каждом объекте. Например , если в одном объекте хранится значение 1.25 типа Double, а в другом значение -1.25 типа Float, тогда метод absEqual ( ) должен возвратить true. Один из способов реализации метода absEqual ( ) предусматривает передачу ему аргу мента NumericFns с последующим сравнением абсолютного значения этого аргумента и абсолютного значения вызывающего объекта с возвращением true, только если они совпадают. Скажем, вы хотите иметь возможность вызывать absEqual ( ) , как показано ниже: — NumericFns<Double> dOb = new NumericFns<Double>(1.25); NumericFns<Float> fOb = new NumericFns<Float>(-1.25); if(dOb.absEqual( fOb)) System.out.println("Абсолютные значения одинаковы ."); else System.out.println("Абсолютные значения отличаются."); На первый взгляд создание absEqual ( ) выглядит простой задачей. К сожалению , проблемы начинаются, как только вы попытаетесь объявить параметр типа NumericFns. Какой тип вы укажете для параметра типа в NumericFns? Поначалу вы можете подумать о решении , где Т применяется в качестве параметра типа: // Работать не будет! // Выяснить, одинаковы ли абсолютные значения двух объектов. boolean absEqual( NumericFns<T> ob) { if( Math.abs( num.doubleValue()) == Math.abs(ob.num.doubleValue()) return true; return false; } Здесь стандартный метод Math.abs ( ) применяется для получения абсолютного значения каждого числа и затем значения сравниваются . Проблема предпринятой попытки в том , что такое решение будет работать только с другими объектами NumericFns, тип которых совпадает с типом вызывающего объекта . Например , если вызываемый объект относится к типу NumericFns<Integer>, то параметр ob тоже должен иметь тип NumericFns<Integer >. Его нельзя использовать , скажем , для сравнения абсолютного значения объ екта типа NumericFns < Double > с абсолютным значением объекта типа NumericFns<Short>. Следовательно , этот подход будет работать только в очень узком контексте и не даст общего (т.е. обобщенного ) решения. 500 Java: руководство для начинающих, 9-е издание Чтобы создать обобщенный метод abs Equal ( ) , потребуется задействовать еще одну особенность обобщений Java: аргумент с подстановочным знаком. Аргумент с подстановочным знаком указывается посредством символа ? и представляет неизвестный тип. Вот как с его помощью можно записать метод absEqual ( ) : // Выяснить, одинаковы ли абсолютные значения двух объектов , Обратите внимание на использование boolean absEqual(NumericFns<?> ob) { подстановочного знака if( Math.abs(num.doubleValue()) == Math.abs(ob.num.doubleValue())) return true; return false; } Здесь NumericFns<?> соответствует любому объекту NumericFns, позволяя сравнивать средние значения любых двух объектов NumericFns, что и демонстрируется в следующей программе: // Использование подстановочного знака , class NumericFnsCT extends Number> { T num; // Передать конструктору ссылку на числовой объект. NumericFns(Т п) { num = п; } // Возвратить обратную величину , double reciprocal( ) { return 1 / num.doubleValue(); } // Возвратить дробную часть , double fraction() { return num.doubleValue() - num.intValue(); } // Выяснить, одинаковы ли абсолютные значения двух объектов , boolean absEqual( NumericFns<?> ob) { if( Math.abs(num.doubleValue()) == Math.abs(ob.num.doubleValue())) return true; return false; } // . . . } // Демонстрация использования подстановочного знака , class WildcardDemo { public static void main(String[ J args) { NumericFns<Integer> iOb = new NumericFns<Integer>(6); NumericFns<Double> dOb = new NumericFns<Double>(-6.0); NumericFns<Long> 10b = new NumericFns<Long>(5L); Глава 1 3. Обобщения 501 В этом вызове тип с подстановочным System.out.println("Сравнение iOb и dOb."); знаком соответствует Double 1 if(iOb.absEqual(dOb)) System.out.println("Абсолютные значения одинаковы ."); else System.out.println("Абсолютные значения отличаются."); System.out.println (); В этом вызове тип с подстановочным знаком соответствует Long System.out.println("Сравнение iOb и 10b."); if( iOb.absEqual(10b)) System.out.println("Абсолютные значения одинаковы ."); else System.out.println ("Абсолютные значения отличаются."); } } Ниже приведен вывод: Сравнение iOb и dOb. Абсолютные значения одинаковы . Сравнение iOb и 10Ь. Абсолютные значения отличаются. Обратите внимание в программе на следующие два вызова absEqual(): if(iOb.absEqual(dOb)) if(iOb.absEqual(10b)) В первом вызове iOb представляет собой объект типа NumericFns<Integer>, a dOb объект типа NumericFns<Double>. Тем не менее, за счет использования подстановочного знака в вызове absEqual ( ) на объекте iOb можно передать dOb. То же самое относится ко второму вызову, в котором передается объект — типа NumericFns<Long>. И последнее замечание: важно понимать, что подстановочный знак не влияет на то, какой тип объектов NumericFns можно создавать это регулируется конструкцией extends в объявлении NumericFns. Подстановочный знак просто соответствует любому допустимому объекту NumericFns. — Ограниченные аргументы с подстановочными знаками Аргументы с подстановочными знаками могут быть ограничены во многом так же , как и параметр типа. Ограниченный аргумент с подстановочным знаком особенно важен при создании обобщенного типа , который будет оперировать на иерархии классов. Чтобы понять причину, давайте проработаем пример. Рассмотрим следующий набор классов: class А { II } ... 502 Java: руководство для начинающих, 9-е издание class В extends А { II ... } class С extends А { II .. . } // Обратите внимание, что класс D НЕ расширяет А. class D { II } . .. Класс А расширяют классы в и С, но не D. Теперь взгляните на показанный ниже очень простой обобщенный класс: // Простой обобщенный класс , class Gen<T> { Т ob; Gen(T о) { ob = о; } } Класс Gen принимает один параметр типа, который указывает тип объекта , хранящегося в ob. Поскольку Т не ограничен, тип Т может быть любым классом . Далее предположим , что нужно создать метод, который принимает в качестве аргумента любой тип объекта Gen, если его параметром типа является класс А или подкласс А. Другими словами , необходимо создать метод, работающий только с объектами Gen< n>, где тип это А или подкласс А. В таком случае должен использоваться ограниченный аргумент с подстановочным знаком. Например , ниже представлен метод по имени test ( ) , который принимает в качестве аргумента только объекты Gen с параметром типа А или подкласса А: — // Здесь ? будет соответствовать А или любому // классу, расширяющему А. static void test(Gen<? extends A> о) { // ... } В следующем классе показано , какие типы объектов Gen можно передавать в test(): class UseBoundedWildcard { // Здесь ? будет соответствовать А или любому // классу , расширяющему А. static void test(Gen<? extends A> о) { Использовать ограниченный аргумент с подстановочным знаком // . . . } public static void main(String[] args) { A a = new A(); В b = new В(); С c = new C(); D d = new D(); Глава 1 3. Обобщения Gen<A> Gen<B> Gen<C> Gen<D> 503 = new Gen<A>(a); w2 = new Gen<B>(b); w3 = new Gen<C>(c); w4 = new Gen<D>(d); w // Допустимые вызовы метода test(). test(w); test(w2); Допустимо, поскольку w, w 2 и w 3 являются test(w3); подклассами А // // // // } Метод test() нельзя вызывать c w4, потому что w4 не является объектом класса, унаследованного от А . test(w 4 ); // Ошибка! ** Не разрешено, потому что w 4 не является подклассом А } В методе main ( ) создаются объекты типов А, В, С и D, после чего они используются для создания четырех объектов Gen, по одному для каждого типа. Наконец, выполняются четыре вызова test ( ) , причем последний вызов за комментирован. Первые три вызова допустимы, поскольку w, w2 и w3 являются объектами Gen типа А либо подкласса А. Однако последний вызов test ( ) недопустим , т. к. w4 представляет собой объект типа D, который не является про изводным от А, а потому ограниченный аргумент с подстановочным знаком в test ( ) не примет w4 в качестве аргумента . В общем случае для установления верхней границы аргумента с подстановочным знаком используйте выражение следующего вида: <? extends суперклассу — где суперкласс имя класса , служащего верхней границей. Не забывайте , что это включающая конструкция, поскольку класс , формирующий верхнюю гра ницу (т.е. указанный в суперкласс) , тоже находится в пределах границ. Вы также можете указать нижнюю границу аргумента с подстановочным знаком , добавив к объявлению конструкцию super с таким общим видом: <? super подклассу В данном случае допустимыми аргументами будут только классы, являющи еся суперклассами подкласса , который указан в подкласс. Конструкция тоже включающая. Обобщенные методы Как было проиллюстрировано в предшествующих примерах , методы внутри обобщенного класса могут использовать параметр типа класса и , следовательно, автоматически становятся обобщенными по отношению к параметру типа . Тем не менее , можно объявить обобщенный метод с одним или несколькими собственными параметрами типов. Более того, можно создать обобщенный метод внутри необобщенного класса . 504 Java: руководство для начинающих, 9-е издание !?@>A8< C M:A?5@B0 ВОПРОС. Можно ли привести один экземпляр обобщенного класса к другому? ОТВЕТ. Да , один экземпляр обобщенного класса можно привести к другому, но только если они совместимы во всем остальном и их аргументы типов совпадают. Например, предположим , что обобщенный класс по имени Gen объявлен так, как показано ниже: class Gen<T> { II . .. Пусть переменная х объявлена следующим образом: Gen<Integer> х = new Gen<Integer>(); Тогда представленное далее приведение будет допустимым: (Gen<Integer>) х // допустимо поскольку х является экземпляром Gen<Integer>. Но следующее приведение: (Gen<Long>) х // н е разрешено окажется недопустимым, потому что х — не экземпляр Gen<Long>. В показанной ниже программе объявляется необобщенный класс по имени GenericMethodDemo , а в этом классе определяется статический обобщенный метод по имени arraysEqual ( ) , который выясняет, содержат ли два массива одни и те же элементы , расположенные в том же самом порядке. Его можно применять с любым типом объекта и массива при условии , что массив содержит объекты, совместимые с типом искомого объекта. // Демонстрация использования простого обобщенного метода , class GenericMethodDemo { // Выяснить, совпадает ли содержимое массивов , static <Т extends Comparable <T> , V extends Т> Обобщенный метод boolean arraysEqual(Т[] x, V[] у) { «* // Если длины массивов отличаются, то отличается и содержимое массивов , if(х.length != у.length) return false; for(int i=0; i < x.length; i++) if(!x[i].equals(y[i])) return false; //содержимое массивов отличается return true; // содержимое массивов совпадает } public static void main(String[] args) { Integer[] nums = ( 1, 2, 3, 4, 5 }; Integer[] nums2 = ( 1, 2, 3, 4, 5 }; Integer[] nums3 = { 1, 2, 7, 4, 5 }; Integer[] nums4 = { 1, 2, 7, 4, 5, 6 }; if(arraysEqual( nums, nums)) System.out.println("nums совпадает c nums"); if( arraysEqual( nums, nums2)) System.out.println("nums совпадает c nums2"); Аргументы типов T и V определяются неявно при вызове метода Глава 1 3. Обобщения 505 if(arraysEqual( nums, nums3)) System.out. println("nums совпадает c nums3"); if(arraysEqual(nums, nums4)) System.out.println("nums совпадает c nums4"); // Создать массив элементов Double. Double[] dvals = ( 1.1, 2.2, 3.3, 4.4, 5.5 }; // Показанный ниже код не скомпилируется, поскольку // nums и dvals имеют разные типы . // if(arraysEqual(nums, dvals)) / / System.out.println("nums совпадает c dvals"); } } Вот вывод, генерируемый программой: nums совпадает с nums nums совпадает с nums2 Теперь займемся исследованием метода arraysEqual ( ) . Взгляните на его объявление: static <T extends Comparable<T>, V extends T> boolean arraysEqual(T[] x, V[] y) { Параметры типа объявляются перед возвращаемым типом метода. Кроме того, обратите внимание на конструкцию т extends Comparable<T>. Ин терфейс Comparable объявлен в пакете java.lang. Класс , реализующий Comparable, определяет объекты , которые можно упорядочивать. Таким образом , требование верхней границы как Comparable гарантирует, что метод arraysEqual ( ) может использоваться только с объектами , которые обладают способностью участвовать в сравнениях. Интерфейс Comparable является обобщенным , и его параметр типа указывает тип сравниваемых объектов. ( Вскоре вы увидите, как создавать обобщенный интерфейс. ) Далее обратите внимание, что тип V ограничен сверху типом Т. Соответственно тип V должен быть либо тем же самым , что и тип т , либо подклассом т . Такое отношение гарантиру ет, что метод arraysEqual ( ) можно вызывать только с аргументами , которые совместимы друг с другом. Вдобавок метод arraysEqual ( ) определен как статический , что позволяет вызывать его независимо от любого объекта. Однако важно понимать, что обобщенные методы могут быть как статическими, так и нестатическими. В этом смысле нет никаких ограничений. Метод arraysEqual ( ) вызывается внутри main ( ) с применением обычного синтаксиса вызова без необходимости в указании аргументов типов. Дело в том, что типы аргументов распознаются автоматически, а типы т и V надлежащим образом корректируются. Например, при первом вызове типом элементов пер вого аргумента оказывается Integer, что приводит к замене т на Integer: if(arraysEqual(nums, nums)) Типом элементов второго аргумента тоже является Integer и потому Integer становится заменой для V. Во втором вызове используются типы String, так что типы Т и V заменяются на String. Таким образом , вызов arraysEqual ( ) считается допустимым, и два массива можно сравнивать. 506 Java: руководство для начинающих, 9-е издание Теперь обратите внимание на закомментированный код: // // if(arraysEqual(nums, dvals)) System.out.println(” nums совпадает c dvals"); Удалив комментарии и попытавшись скомпилировать программу, вы полу чите сообщение об ошибке. Причина в том , что параметр типа V ограничен т с помощью конструкции extends в объявлении V, т.е. типом V должен быть либо Т, либо подкласс Т. В рассматриваемой ситуации первый аргумент имеет тип Integer, что превращает Т в Integer, а второй — тип Double, который не является подклассом Integer. В итоге вызов arraysEqual ( ) становится недопусти мым и возникает ошибка несоответствия типов на этапе компиляции. Синтаксис , применяемый для создания arraysEqual ( ) , можно обобщить. Вот синтаксис обобщенного метода: < список-парам-типов> возвр-тип имя-метода { список-параметров ) { II ... Во всех случаях в список -парам-типов указывается список параметров типов, разделенных запятыми. Обратите внимание , что в обобщенном методе список параметров типов предшествует возвращаемому типу. Обобщенные конструкторы Конструкторы могут быть обобщенными, даже когда их класс не определен как обобщенный. Например, в следующей программе класс Summation не является обобщенным , но его конструктор определен как обобщенный: // Использование обобщенного конструктора , class Summation { private int sum; ^— <T extends Number> Summation(T arg ) { sum = 0; for(int i=0; i <= arg.intValue(); i++) sum += i; Обобщенный конструктор } int getSum() { return sum; } } class GenConsDemo { public static void main(String[] args) { Summation ob = new Summation(4.0); System.out.println("Сумма целых чисел от 0 до 4: " + ob.getSum()); } } Класс Summation вычисляет и инкапсулирует сумму всех целых чисел от 0 до N . Поскольку в Summation ( ) указан параметр типа, ограниченный Number, объект Summation может быть сконструирован с применением любого числового типа, включая Integer, Float и Double. Независимо от того, какой числовой тип используется , его значение преобразуется в Integer вызовом intValue ( ) и Глава 1 3. Обобщения 507 вычисляется сумма . Следовательно , нет необходимости , чтобы класс Summation был обобщенным; нужен только обобщенный конструктор. Обобщенные интерфейсы Как было показано в представленной ранее программе GenericMethodDemo, интерфейс может быть обобщенным . Чтобы обеспечить возможность сравнения элементов двух массивов , в том примере применялся стандартный интерфейс СотрагаЫестх Разумеется , вы также можете определить собственный обоб щенный интерфейс . Обобщенные интерфейсы указываются аналогично обобщенным классам . Рассмотрим пример, где создается интерфейс Containment, который может быть реализован классами , хранящими одно или несколько зна чений. В интерфейсе Containment объявлен метод по имени contains ( ) , ко торый определяет, содержится ли указанное значение в вызывающем объекте . // Пример использования обобщенного интерфейса. // Обобщенный интерфейс содержимого. // Предполагается, что реализующий его класс // содержит одно или несколько значений. interface Containment <T> { Обобщенный интерфейс // Метод contains() проверяет, содержится ли указанный // элемент внутри объекта, реализующего Containment , boolean contains(Т о); } // Реализовать Containment, используя массив // для хранения значений. class MyClass<T> implements Containment <T> { T[] arrayRef; MyClass(T[] о) { arrayRef = о; } // Реализовать метод contains(). public boolean contains(T о) { for(Т х : arrayRef) if(х.equals(о)) return true; return false; } } class GenlFDemo { public static void main(String[] args) { Integer[] x = { 1, 2, 3 }; MyClass<Integer> ob = new MyClass<Integer>(x); if(ob.contains(2)) System.out.println("2 содержится в ob"); else System.out.println("2 HE содержится в ob"); if(ob.contains(5)) System.out. println("5 содержится в ob"); else System.out.println("5 HE содержится в ob"); Любой класс, реализующий обобщенный интерфейс, также должен быть обобщенным 508 Java : руководство для начинающих, 9-е издание Следующий код недопустим, потому что Containment отвечает за хранение элементов Integer, а 9.25 is является значением Double. if(ob.contains(9.25)) // Ошибка! System.out.println("9.25 содержится в ob"); // // // // // } } Вывод выглядит следующим образом: 2 содержится в ob 5 НЕ содержится в ob Хотя понимание большинства аспектов в программе не должно вызывать за труднений , необходимо отметить пару ключевых моментов. Прежде всего, об ратите внимание на способ объявления интерфейса Containment: interface Containment<T> { Как правило, обобщенный интерфейс объявляется аналогично обобщенному классу. В данном случае параметр типа Т указывает тип содержащихся объектов. Затем интерфейс Containment реализуется классом MyClass. Взгляните на объявление MyClass: class MyClass<T> implements Containment<T> { Вообще говоря , если класс реализует обобщенный интерфейс , то этот класс тоже должен быть обобщенным , по крайней мере , принимать параметр типа , который передается интерфейсу. Скажем , следующая попытка объявления MyClass ошибочна: class MyClass implements Containment<T> { // Ошибка! Поскольку в MyClass не объявлен параметр типа , то передавать его в Containment невозможно. В таком случае идентификатор Т попросту неизвестен и компилятор сообщит об ошибке. Разумеется, если класс реализует специ фический тип обобщенного интерфейса , тогда реализующий класс не обязан быть обобщенным: class MyClass implements Containment<Double> { 11 Нормально Вполне ожидаемо параметры типов , указанные в обобщенном интерфей се , могут быть ограниченными , что позволяет ограничивать тип данных, для которых может быть реализован интерфейс. Например , чтобы ограничить Containment числовыми типами , его можно объявить следующим образом: interface Containment<T extends Number> { Теперь любой реализующий класс должен передавать в Containment аргу мент типа с таким же ограничением. Скажем, MyClass должен быть объявлен, как показано ниже: class MyClass<T extends Number> implements Containment <T> { Глава 1 3. Обобщения 509 Обратите особое внимание на то , как параметр типа т объявляется в MyClass и далее передается Containment. Поскольку для Containment требуется тип , который реализует Number, реализующий класс ( в этом случае MyClass)должен указывать ту же самую границу. Более того , после установления такой границы нет никакой необходимости указывать ее снова в конструкции implements. На самом деле поступать так было бы неправильно. Например , приведенный ниже код некорректен и потому не скомпилируется: / / Не скомпилируется ! c l a s s MyClass < T e x t e n d s Number > i m p l e m e n t s ContainmentCT e x t e n d s Number > { / / Ошибка ! Установленный параметр типа просто передается интерфейсу без дальней ших изменений. Вот общий синтаксис обобщенного интерфейса: i n t e r f a c e имя-интерфейса< список-парам-типов> { II ... В список -парам- типов указывается список параметров типов, разделенных запятыми. При реализации обобщенного интерфейса необходимо указывать аргументы типов: c l a s s имя-класса< список-парам-типов> implements имя-интерфейса< список-арг-типов> { Упражнение!3.1 Создание обобщенного класса очереди Одним из наиболее значительных преимуществ, которые обобщения привносят в программироQucueFullException . java вание , является возможность создания надежQueueEmptyException . j a v a ного , многократно используемого кода . В начаG e n Q u e u e . j ava ле главы уже упоминалось , что многие GenQDemo . j ava алгоритмы одинаковы независимо от того , для какого типа данных они применяются. Например, очередь работает одинаково для целых чисел , строк или объектов F i l e . Вместо того чтобы создавать отдельный класс очереди для каждого объектного типа , можно создать единственное обобщенное решение, которое допускается использовать с любым типом объекта. Таким образом , при создании обобщенного решения цикл разработки, состоящий из проектирования , написания кода, тестирования и отладки , проис ходит только однажды , а не каждый раз, когда требуется очередь для нового типа данных. В этом проекте пример очереди , который развивался , начиная с упражнения 5.2, будет сделан обобщенным. Проект представляет собой финальную эволюцию очереди. Он включает обобщенный интерфейс , определяющий операции с очередью , два класса исключений и один класс реализации очереди: очередь фиксированного размера. Конечно, вы можете поэкспериментировать с други ми типами обобщенных очередей вроде обобщенной динамической очереди или обобщенной циклической очередь. Просто следуйте показанному здесь примеру. IGenQ . j a v a : 510 Java: руководство для начинающих, 9-е издание Как и в версии очереди из упражнения 9.1, в текущем проекте код очереди организован в виде набора отдельных файлов: один для интерфейса , один для каждого исключения , один для реализации фиксированной очереди и один для программы , демонстрирующей ее работу. Такая структура отражает способ , которым проект обычно организуется в реальности. 1 На первом шаге построения обобщенной очереди создается обобщенный интерфейс , описывающий две операции очереди: помещение и извлече ние . Обобщенная версия интерфейса очереди называется iGenQ и показана ниже. Поместите код этого интерфейса в файл по имени IGenQ.java. . // Обобщенный интерфейс очереди , public interface IGenQ<T> { // Поместить элемент в очередь. void put(T ch) throws QueueFullException; // Извлечь элемент из очереди. Т get() throws QueueEmptyException; } . Обратите внимание, что тип данных, хранящихся в очереди, указывается в обобщенном параметре типа т . 2 Создайте файлы QueueFullException.java и QueueEmptyException. java. Поместите в них соответствующие классы: // Исключение для ошибок, связанных с переполненной очередью , public class QueueFullException extends Exception { int size; QueueFullException(int s) { size = s; } public String toStringO { return "ХпОчередь переполнена. Максимальный размер составляет " + size + " элементов."; } } // Исключение для ошибок, связанных с пустой очередью , public class QueueEmptyException extends Exception { public String toStringO { return " ХпОчередь пуста."; } } . Классы QueueFullException и QueueEmptyException инкапсулируют две ошибки , связанные с очередью: переполнение и опустошение. Они не являются обобщенными классами, потому что одинаковы независимо от того, какой тип данных хранится в очереди. Таким образом, эти два файла будут такими же, как те , которые применялись в упражнении 9.1. 3 Создайте файл по имени GenQueue.java. Поместите в него следующий код класса , который реализует очередь фиксированного размера: Глава 1 3. Обобщения 511 // Обобщенный класс очередм фиксированного размера , class GenQueue<T> implements IGenQ<T> { private T[] q; // массив, в котором хранится очередь private int putloc, getloc; // индексы для позиций помещения // и извлечения // Конструктор пустой очереди из заданного массива , public GenQueue(T[] aRef) { q aRef; putloc = getloc = 0; } // Поместить элемент в очередь , public void put(T obj) throws QueueFullException { if( putloc==q.length ) throw new QueueFullException(q.length); q[putloc++] = obj; } // Извлечь элемент из очереди , public Т get() throws QueueEmptyException { if(getloc == putloc) throw new QueueEmptyException(); return q[getloc++]; } } GenQueue представляет собой обобщенный класс с параметром типа т, который указывает тип данных, хранящихся в очереди. Обратите внимание , что т также передается интерфейсу iGenQ. Обратите внимание , что конструктору GenQueue передается ссылка на массив, который будет использоваться для хранения очереди. Таким образом , чтобы сконструировать GenQueue, сначала понадобится создать массив с типом, совместимым с объектами , которые будут храниться в очереди , и размером , достаточным для хранения количества объектов, которые будут помещены в очередь. Например , в следующей последовательности показано , как создать очередь, хранящую строки: String[] strArray = new String[10]; GenQueue<String> strQ = new GenQueue <String>(strArray); 4. Создайте файл по имени GenQDemo.java и поместите в него приведенный далее код программы , которая демонстрирует применение обобщенной очереди. /* Упражнение 13.1. Демонстрация использования класса обобщенной очереди. */ 512 Java: руководство для начинающих, 9-е издание class GenQDemo { public static void main(String[] args) { // Создать очередь элементов Integer. Integer[] iStore = new Integer[10]; GenQueue<Integer> q = new GenQueue<Integer>(iStore); Integer iVal; System.out.println("Демонстрация работы очереди элементов Integer."); try { for(int i=0; i < 5; i++) { System.out.println("Добавление " + i + " в q."); q.put(i); // добавить целочисленное значение в q } } catch ( QueueFullException exc) { System.out.println(exc); } System.out.println(); try { for(int i=0; i < 5; i++) { System.out.print("Получение следующего элемента Integer из q: "); iVal = q.get(); System.out.println(iVal); } } catch ( QueueEmptyException exc) { System.out.println(exc); } System.out.println(); // Создать очередь элементов Double. Double[] dStore = new Double[10]; GenQueue<Double> q2 = new GenQueue <Double>(dStore); Double dVal; System.out.println("Демонстрация работы очереди элементов Double."); try { for(int i=0; i < 5; i++) { System.out.println("Добавление " + (double)i/2 + " в q2."); q2.put((double)i/2); // добавить значение double в q2 } } catch (QueueFullException exc) { System.out.println(exc); } System.out.println(); try { for(int i=0; i < 5; i++) { System.out.print("Получение следующего элемента Double из q2: "); dVal = q2.get(); System.out.println(dVal); } } Глава 1 3. Обобщения 513 catch (QueueEmptyException exc) { System.out.println(exc); } } } 5. Скомпилируйте и запустите программу. Вы увидите следующий вывод: Демонстрация Добавление 0 Добавление 1 Добавление 2 Добавление 3 Добавление 4 Получение Получение Получение Получение Получение работы в q. в q. в q. в q. очереди элементов Integer. в q. следующего следующего следующего следующего следующего элемента Integer из элемента Integer из элемента Integer из элемента Integer из элемента Integer из q: q: q: q: q: О 1 2 3 4 Демонстрация работы очереди элементов Double. Добавление 0.0 в q2. Добавление 0.5 в q2. Добавление 1.0 в q2. Добавление 1.5 в q2. Добавление 2.0 в q2. Получение следующего элемента Double из q2: 0.0 Получение следующего элемента Double из q2: 0.5 Получение следующего элемента Double из q2: 1.0 Получение следующего элемента Double из q2: 1.5 Получение следующего элемента Double из q2: 2.0 6. Попробуйте самостоятельно преобразовать классы CircularQueue и DynQueue из упражнения 8.1 в обобщенные. Низкоуровневые типы и унаследованный код Поскольку до выхода JDK 5 поддержка обобщений отсутствовала , необходи мо было обеспечить какой-то путь перехода от старого кода , предшествующего обобщениям. Кроме того, такой путь перехода должен был позволить унаследованному коду, написанному до появления обобщений, остаться в рабочем состоянии и одновременно быть совместимым с обобщениями. Другими словами , коду, предшествующему обобщениям , нужна была возможность работы с обобщениями, а обобщенному коду возможность работы с кодом, написанным до появления обобщений. Для обработки перехода к обобщениям в Java разрешено использовать обобщенный класс без аргументов типов, что создает низкоуровневый тип для класса . Такой низкоуровневый тип совместим с унаследованным кодом , которому не известны обобщения. Главный недостаток применения низкоуровневого типа связан с утратой безопасности в отношении типов, обеспечиваемой обобщениями . — 514 Java: руководство для начинающих, 9-е издание Ниже показан пример , демонстрирующий низкоуровневый тип в действии: // Использование низкоуровневого типа , class Gen<T> { Т ob; // объявить объект типа Т // Передать конструктору ссылку на объект типа Т. Gen(T о) { ob = о; } // Возвратить ob. Т getObO { return ob; } } // Демонстрация использования низкоуровневого типа , class RawDemo { public static void main(String[] args) { // Создать объект Gen для Integer. Gen<Integer> iOb = new Gen<Integer>(88); // Создать объект Gen для String. Gen<String> strOb = new Gen<String>("Тестирование обобщений"); // Создать низкоуровневый объект Gen // и присвоить ему значение Double. Когда никакие аргументы типов не предоставляются, создается Gen raw = new Gen(98.6); * - низкоуровневый тип // Приведение здесь необходимо, // потому что тип неизвестен , double d = (Double) raw.getObO ; System.out.println("значение: " + d); // Использование низкоуровневого типа может привести к исключениям // во время выполнения. Ниже показано несколько примеров. // Следующее приведение вызывает ошибку во время выполнения! // int i = (Integer) raw.getObO ; // ошибка во время выполнения Низкоуровневые типы приводят // Следующее присваивание приводит к утрате к утрате безопасности // безопасности в отношении типов. в отношении типов strOb = raw; // нормально, но потенциально ошибочно . // String str = strOb.getOb(); // ошибка во время выполнения // Следующее присваивание тоже приводит к утрате // безопасности в отношении типов , raw = iOb; // нормально, но потенциально ошибочно // d = (Double) raw.getObO ; // ошибка во время выполнения ^ } } В программе присутствуют несколько интересных вещей . Первым делом с помощью следующего объявления создается низкоуровневый тип обобщенного класса Gen: Gen raw = new Gen(98.6); Обратите внимание , что аргументы типов не указаны. По существу оператор создает объект Gen, тип Т которого заменяется на Object. Глава 13. Обобщения 515 Низкоуровневый тип не безопасен в отношении типов. Таким образом , переменной низкоуровневого типа можно присваивать ссылку на любой тип объекта Gen. Разрешено и обратное: переменной конкретного типа Gen может быть присвоена ссылка на низкоуровневый объект Gen. Однако обе операции потен циально небезопасны из-за того, что обходится механизм проверки типов обобщений . Отсутствие безопасности в отношении типов иллюстрируется закомментированными строками в конце программы. Давайте разберем каждый случай . Для начала рассмотрим следующую ситуацию: // int i = (Integer) raw.getObO ; // ошибка во время выполнения В данном операторе получается значение ob внутри raw и приводится к Integer. Проблема в том , что raw содержит значение Double, а не целочисленное значение. Тем не менее, на этапе компиляции обнаружить это невозможно, поскольку тип raw неизвестен. В итоге оператор терпит неудачу во время вы полнения. В следующей кодовой последовательности переменной strOb (ссылке типа Gen<String>)присваивается ссылка на низкоуровневый объект Gen: strOb = raw; // нормально, но потенциально ошибочно // String str = strOb.getOb(); // ошибка во время выполнения Само по себе присваивание синтаксически корректно, но сомнительно. Так как переменная strOb имеет тип Gen<String>, предполагается, что она содержит строку. Однако после присваивания объект, на который ссылается strOb, содержит Double. Таким образом , когда во время выполнения предпринимается попытка присвоить содержимое strOb переменной str, возникает ошибка , потому что теперь strOb содержит Double. В результате при присваивании обобщенной ссылке низкоуровневой ссылки обходится механизм безопасности типов. В показанной ниже кодовой последовательности предыдущий случай инвертируется: raw = iOb; // нормально, но потенциально ошибочно // d = (Double) raw.getObO ; // ошибка во время выполнения Здесь обобщенная ссылка присваивается низкоуровневой ссылке. Несмотря на правильность с точки зрения синтаксиса , могут возникнуть проблемы , как демонстрируется во второй строке. Теперь переменная raw ссылается на объект, который содержит объект Integer, но приведение предполагает, что она содержит объект Double. Проблема не может быть выявлена на этапе компиляции и взамен возникает ошибка во время выполнения. Из- за потенциальной опасности , присущей низкоуровневым типам , ком пилятор javac отображает непроверяемые предупреждения при использовании низкоуровневого типа способом , который может поставить под угрозу безопасность в отношении типов. Следующие строки в предыдущей программе приводят к выдаче компилятором непроверяемых предупреждений: 516 Java : руководство для начинающих, 9-е издание Gen raw = new Gen(98.6); strOb = raw; // нормально, но потенциально ошибочно В первой строке вызывается конструктор Gen без аргумента типа , что инициирует выдачу предупреждения. Во второй строке низкоуровневая ссылка присваивается обобщенной переменной, что приводит к генерации предупреждения. Поначалу вы можете подумать , что показанная далее строка тоже должна приводить к выдаче непроверяемое предупреждения, но это не так: raw = iOb; // нормально, но потенциально ошибочно Компилятор не генерирует никаких предупреждений , т.к. присваивание не приводит к добавочной утрате безопасности в отношении типов помимо той, что уже произошла при создании raw. И последнее замечание: вы должны ограничить использование низкоуровневых типов ситуациями , когда вам нужно смешивать унаследованный код с более новым обобщенным кодом. Низкоуровневые типы являются просто переходным средством, а не тем , что следует применять в новом коде. Выведение типов с помощью ромбовидной операции Начиная с версии JDK 7, появилась возможность сокращения синтаксиса , используемого для создания экземпляра обобщенного типа. Для начала вспом ните класс TwoGen, показанный ранее в главе . Для удобства часть его кода представлена ниже. Обратите внимание , что в нем задействованы два обобщенных типа. class TwoGenCT, V> { Т obi; V ob2; // Передать конструктору ссылку на объект типа Т. TwoGen(Т ol, V о2) { obi = ol; оЬ2 = о2; } // .. . } До выхода JDK 7 для создания экземпляра TwoGen нужно было использовать оператор вида: TwoGen<Integer, String> tgOb = new TwoGendnteger, String>(42, "тест"); Здесь аргументы типов(Integer и String)указываются дважды: первый раз, когда объявляется tgOb, и второй когда экземпляр TwoGen создается через операцию new. Хотя с этой формой не связано ничего плохого, она чуть более многословна , чем должна быть. Поскольку в конструкции new типы аргументов — Глава 1 3. Обобщения 517 типов могут быть без труда выведены , нет никаких причин указывать их повторно . Чтобы принять соответствующие меры , в JDK 7 добавлен синтаксический элемент, позволяющий избежать второго указания . Теперь предыдущее объявление можно переписать так: TwoGerKInteger, String> tgOb = new TwoGen<>(42, "тест"); Обратите внимание , что в части , где создается экземпляр , просто использу ются угловые скобки о, которые представляют пустой список аргументов типов . Такую конструкцию называют ромбовидной операцией. Она сообщает компилятору о необходимости выведения аргументов типов для конструктора из выраже ния new. Основное преимущество этого синтаксиса выведения типов связано с тем , что он сокращает довольно длинные операторы объявлений . Ромбовидная операция особенно полезна для обобщенных типов с ограничениями . Предыдущий пример можно обобщить. Когда применяется выведение ти пов , синтаксис объявления обобщенной ссылки и создания экземпляра имеет следующую общую форму: имя-класса< список-аргументов-типов> имя-переменной = new имя-класса< > [ список-аргументов-конструктора) ; Здесь список аргументов типов конструктора в new пуст. Несмотря на то что выведение типов главным образом используется в операторах объявления , его можно также применять к передаче параметров . Напри мер , если добавить в TwoGen такой метод: boolean isSame(TwoGenCT, V> о) { if(obi == о.obi && ob2 == o.ob2) return true; else return false; } тогда показанный ниже вызов будет законным: if(tgOb.isSame(new TwoGen<>(42, "тест"))) System.out.println("Одинаковые"); В данном случае аргументы типов для аргумента , переданного методу isSame ( ) , могут быть выведены из типа параметра . Нет нужды указывать их еще раз . Хотя ромбовидная операция удобна , в большинстве рассмотренных в кни ге примеров при объявлении экземпляров обобщенных классов по- прежнему будет использоваться полный синтаксис . На то есть две причины . Во- первых , применение полного синтаксиса дает очень четкое представление о том , что именно создается , а это крайне важно в коде примеров , предлагаемых в книге . Во - вторых , код будет работать в средах с более старыми компиляторами . Разумеется , в своем коде вы можете использовать синтаксис выведения типов для упрощения имеющихся объявлений. 518 Java : руководство для начинающих, 9 - е издание Выведение типов локальных переменных и обобщения Как только что объяснялось, выведение типов уже поддерживается для обобщений за счет применения ромбовидной операции. Однако с обобщенным классом можно также использовать средство выведения типов локальных пере менных, добавленное в JDK 10. Например , с учетом класса TwoGen , определенного в предыдущем разделе , следующее объявление: TwoGerKInteger, String> tgObj = new TwoGenCInteger, String>(42, "тест"); можно переписать с применением средства выведения типов локальных переменных: var tgOb = new TwoGenCInteger, String>(42, "тест"); В данном случае тип переменной tgOb выводится как TwoGerKInteger , String > , поскольку это тип ее инициализатора. Вдобавок обратите внимание на то, что использование var в результате дает более короткое объявление , нежели в противном случае. Обычно имена обобщенных типов могут оказываться довольно длинными и (иногда) сложными. Применение var еще один способ существенно сократить такие объявления. По тем же причинам, которые только что объяснялись относительно ромбовидной операции, в оставшихся примерах, рассмотренных в книге, будет использоваться полный обобщенный синтаксис, но в вашем коде выведение типов локальных переменных может оказаться весьма полезным. — Стирание Знать все детали о том , как компилятор Java преобразует исходный текст программы в объектный код, обычно не нужно. Тем не менее , в случае с обобщениями важно иметь некоторое представление о процессе , поскольку оно позволяет понять, по какой причине обобщенные средства работают именно так , как работают, и почему их поведение иногда немного удивляет. В связи с этим имеет смысл кратко обсудить реализацию обобщений в Java. Важным ограничением, которое управляло способом добавления обобщений в Java , была необходимость поддержания совместимости с предшествующими версиями Java . Попросту говоря, обобщенный код обязан был быть совместимым с ранее существовавшим необобщенным кодом. Таким образом, любые изменения синтаксиса языка Java или машины JVM не должны были нарушать функционирование старого кода. Способ, которым в Java реализованы обобщения, удовлетворяющий упомянутому ограничению , предусматривает применение стирания. Рассмотрим, каким образом работает стирание. При компиляции кода Java вся информация об обобщенных типах удаляется (стирается ) , что подразумевает замену параметров типов их ограничивающим типом , которым является Object , если не указано явное ограничение , и последующее применение надлежащих приведений ( как определено аргументами типов ) для обеспечения Глава 1 3. Обобщения 519 совместимости с типами, указанными в аргументах типов. Такая совместимость типов навязывается самим компилятором. Подход к обобщениям подобного рода означает, что параметры типов во время выполнения не существуют. Они просто являются механизмом, относящимся к исходному коду. Ошибки неоднозначности Внедрение обобщений порождает еще одну разновидность ошибок, от которых вы должны защититься: неоднозначность. Ошибки неоднозначности происходят, когда стирание приводит к тому, что два на вид разных обобщенных объявления распознаются как один и тот же стертый тип , становясь причиной конфликта . Вот пример, касающийся перегрузки методов: // Неоднозначность перегруженных методов, возникающая из-за стирания. class MyGenClass<T, V > { Т obi; V оЬ2; II ... I I Эти два перегруженных метода неоднозначны и не скомпилируются. void set(T о) { оЫ о; Эти два метода по своей сути неоднозначны } void set( Vo) { о Ъ 2 = о; ^ } } Обратите внимание, что в MyGenClass объявлены два обобщенных типа: Т и V. Внутри MyGenClass предпринимается попытка перегрузки метода set ( ) на основе параметров типов т и V, что выглядит разумным , поскольку т и V кажутся разными типами. Однако здесь имеются две проблемы неоднозначности. Первая проблема заключается в том , что исходя из того, как написан класс MyGenClass, на самом деле вовсе не обязательно , чтобы Т и V были разными типами. Скажем , абсолютно корректно ( в принципе ) сконструировать объект MyGenClass следующим образом: MyGenClass<String, String> obj = new MyGenClass<String, String>() В таком случае т и V будут заменены типом String. В итоге обе версии метода set ( ) становятся идентичными , что , конечно же , является ошибкой . Вторая и более фундаментальная проблема связана с тем , что стирание ти пов для set ( ) сводит обе версии к такому виду: void set( Object о) { II ... Соответственно перегрузка метода set { ) , предпринятая в MyGenClass, по своей сути неоднозначна . Решение в данном случае заключается в том, чтобы использовать два отдельных имени методов , а не пытаться перегружать set(). 520 Java: руководство для начинающих, 9- е издание Некоторые ограничения обобщений Существует несколько ограничений, которые следует иметь в виду при работе с обобщениями. Они связаны с созданием объектов параметров типов , ста тических членов, исключений и массивов. Все ограничения исследуются ниже . Невозможность создать экземпляры параметров типов Создать экземпляр параметра типа невозможно. Например, рассмотрим следующий класс: // Создать экземпляр Т невозможно , class Gen<T> { Т оЪ; Gen() { ob = new Т(); // Ошибка!!! } } Здесь предпринимается незаконная попытка создать экземпляр т . Понять причину должно быть легко: компилятору ничего не известно о типе объекта , который нужно создать. Параметр типа т это просто заполнитель. — Ограничения, касающиеся статических членов Статические члены не могут использовать параметр типа, объявленный в объемлющем классе. Например , оба статических члена этого класса недопустимы: class Wrong<T> { // Ошибка; статические переменные не могут иметь тип Т. static Т ob; // Ошибка; статические методы не могут использовать Т. static Т getOb() { return ob; } } Хотя объявлять статические члены, в которых задействован параметр типа , объявленный в объемлющем классе , не разрешено , можно объявить статические обобщенные методы , которые определяют собственные параметры типов, как делалось ранее в главе . Ограничения, касающиеся обобщенных массивов Существуют два важных ограничения обобщений, которые применяются к массивам. Во- первых, нельзя создавать экземпляр массива , тип элементов которого является параметром типа. Во - вторых , не разрешено создавать массив обобщенных ссылок для конкретного типа. Обе ситуации демонстрируются в показанной далее короткой программе: Глава 1 3. Обобщения 521 // Обобщения и массивы , class Gen<T extends Number> { T ob; T[] vals; // нормально Gen(T о, T[] nums) { ob = о; // Этот оператор недопустим. // vals = new Т[10]; // невозможно создать массив элементов типа Т // Но следующий оператор законен. vals = nums; // присваивать ссылку на существующий массив разрешено } } class GenArrays { public static void main(String[] args ) { Integer[] n = { 1, 2 , 3, 4, 5 }; Gen<Integer> iOb = new Gen<Integer>(50, n); // Невозможно создать массив обобщенных ссылок для конкретного типа. // Gen<Integer>[] gens = new Gen<Integer>[10]; // Ошибка! // Все нормально. Gen<?>[] gens = new Gen<?>[10]; // нормально } } Как показано в программе, объявлять ссылку на массив типа Т разрешено: Т[] vals; // нормально Но создавать экземпляр массива элементов типа т нельзя, как демонстрируется в следующей закомментированной строке: = new Т[10]; // невозможно создать массив элементов типа Т Причина невозможности создать массив элементов типа Т связана с тем , что компилятор не в состоянии выяснить фактический тип создаваемого массива. Однако можно передать методу Gen( ) ссылку на совместимый по типу массив при создании объекта и присвоить эту ссылку переменной vals: // vals vals = nums; // присваивать ссылку на существующий массив разрешено Прием работает, поскольку переданный в Gen массив относится к известно му типу, который будет совпадать с типом т во время создания объекта. Обратите внимание, что внутри main ( ) нельзя объявлять массив обобщенных ссылок для определенного обобщенного типа, т.е. следующая строка кода не скомпилируется: // Gen<Integer>[] gens = new Gen<Integer>[10]; // Ошибка! Ограничения, касающиеся обобщенных исключений Обобщенный класс не может расширять тип Throwable, что означает невозможность создания обобщенных классов исключений. Продолжение изучения обобщений Как упоминалось в начале главы , в ней была представлена достаточная ин формация для эффективного использования обобщений в ваших программах. 522 Java: руководство для начинающих, 9-е издание Тем не менее , существует множество побочных проблем и особых случаев , ко торые здесь не рассматривались. Читатели, особенно интересующиеся обобще ниями , захотят узнать, как обобщения влияют, скажем, на иерархию классов, сравнение типов во время выполнения и переопределение. Обсуждения этой и других тем можно найти в книге Java. Полное руководство, 12-е изд. ( ООО “Диалектика ” , 2023 год). Вопросы и упражнения для самопроверки 1 . Обобщения важны в Java , потому что они позволяют создавать код, который является: а ) безопасным в отношении типов; б) многократно используемым ; в) надежным; г) обладающим всеми перечисленными выше характеристиками. 2. Может ли примитивный тип использоваться в качестве аргумента типа ? 3. Покажите , как объявить класс по имени FlightSched, который принимает два обобщенных параметра . 4. Продолжая вопрос 3, измените параметр второго типа FlightSched так, чтобы он расширял Thread. 5. Модифицируйте класс FlightSched так, чтобы его второй параметр типа обязательно был подклассом первого параметра типа. 6. Что обозначает и что делает знак ? в обобщениях? 7. Может ли аргумент с подстановочными знаками быть ограниченным? 8. Обобщенный метод по имени MyGen ( ) принимает один параметр типа. Кроме того, MyGen ( ) имеет один параметр, тип которого совпадает с параметром типа . Он также возвращает объект этого параметра типа. Покажите , как объявить метод MyGen(). 9. Покажите , как объявить класс по имени MyClass, который реализует обобщенный интерфейс iGenlF следующего вида: interface IGenlFCT, V extends Т> { I I ... 10. Имея обобщенный класс по имени Countег<Т>, покажите , как создать объект его низкоуровневого типа. 11. Существуют ли параметры типов во время выполнения? 12. Преобразуйте решение упражнения 10 для самопроверки из главы 9 в обобщенную версию. По ходу дела создайте интерфейс стека с именем IGenStack, который обобщенным образом определяет операции push ( ) и pop(). 13. Что такое о? 14. Как упростить следующий код? MyClass<Double,String> obj = new MyClass<Double,String>(1.1,"Добро пожаловать!"); о, * I S I S* I vev s. • I,I I , II N» fcu . « ISiSiMiV , » \ I, • I S 4 .' »» * II II I \ . 4 i I I : 's "' Os O s “ ’" 4\'" ., " Глава 14 Лямбда-выражения и ссылки на методы 524 Java : руководство для начинающих, 9-е издание В этой главе • • • z Общая форма лямбда- выражения z Определение функционального интерфейса z Использование лямбда-выражений z Использование блочных лямбда- выражений z Использование обобщенных функциональных интерфейсов z Захват переменных в лямбда-выражениях z Генерирование исключений в лямбда-выражениях z Ссылки на методы z Ссылки на конструкторы Функциональные интерфейсы, определенные в java . u t i l . function в версии JDK 8 появилось средство , значительно расширившее выразительные возможности языка лямбда -выражения. Они не только добавили в язык новые элементы синтаксиса , но также упростили способ реализации ряда общих конструкций. Во многом подобно тому, как несколько лет назад добавление обобщений изменило язык Java , лямбда- выражения продолжают изменять Java и сегодня. Они действительно настолько важны. Добавление лямбда - выражений также ускорило появление других средств Java. Одно из них стандартные методы было описано в главе 8. Стандартные методы позволяют определять для метода интерфейса поведение по умол чанию. Другим примером является ссылка на метод, описанная далее в главе , которая позволяет ссылаться на метод, не выполняя его. Кроме того , включе ние лямбда- выражений привело к появлению новых возможностей в библиотеке Java API. Помимо преимуществ , привносимых лямбда- выражениями в язык, есть еще одна причина , по которой они составляют такую важную часть Java . За последние несколько лет лямбда- выражения стали одним из основных направлений разработки языков программирования. Например, они были добавлены в такие языки , как C # и C ++. Добавление лямбда- выражений в язык Java помогает ему оставаться динамичным , инновационным языком , которого ожидают программисты . В настоящей главе представлено введение в это важное средство. — — — Глава 14. Лямбда -выражения и ссылки на методы 525 Введение в лямбда - выражения Ключевым аспектом для понимания реализации лямбда- выражений в Java являются две конструкции: собственно лямбда - выражение и функциональный интерфейс. Начнем с простого определения каждого из них. Лямбда -выражение по существу представляет собой анонимный (т.е. безы мянный ) метод. Однако такой метод не выполняется сам по себе. Взамен он используется для реализации метода , определенного функциональным интерфейсом. Таким образом , лямбда- выражение приводит к форме анонимного класса . Лямбда - выражения также часто называют замыканиями . Функциональный интерфейс это интерфейс , который содержит один и только один абстрактный метод, обычно устанавливающий предполагаемое назна чение интерфейса. Соответственно функциональный интерфейс , как правило , представляет одиночное действие. Например , стандартный интерфейс Runnable является функциональным интерфейсом , поскольку в нем определен только один метод: run ( ) . Следовательно, run ( ) определяет действие Runnable. Кроме того , функциональный интерфейс задает целевой тип лямбда-выражения . Важно понимать, что лямбда- выражение может применяться только в контексте , в котором указан его целевой тип. И еще один момент: функциональный интер фейс иногда называют типом SAM , где SAM означает Single Abstract Method — — единственный абстрактный метод. Теперь давайте займемся более подробным обсуждением лямбда-выражений и функциональных интерфейсов. На заметку! В функциональном интерфейсе можно указывать любой открытый метод, определенный в Object, скажем, equals ( ) , не влияя на его статус функционального интерфейса. Открытые методы класса Object считаются неявными членами функционального интерфейса, потому что они реализуются экземпляром функционального интерфейса автоматически. Основы лямбда-выражений Лямбда- выражение опирается на синтаксический элемент и операцию, ко- торые отличаются от тех , что рассматривались в предшествующих главах. Опе рацию иногда называется лямбда -операцией или операцией стрелки и обознача ется с помощью - >. Она делит лямбда - выражение на две части. В левой части указываются любые параметры , требующиеся в лямбда- выражении. В правой части находится тело лямбда-выражения , которое определяет действия лямбдавыражения. В Java определены два вида тела лямбда - выражения с одиночным выражением и с блоком кода. Сначала мы рассмотрим лямбда-выражения, определяющие единственное выражение. Прежде чем продолжить, полезно взглянуть на несколько примеров лямбдавыражений. Давайте начнем с лямбда - выражения , пожалуй , самого простого — 526 Java: руководство для начинающих, 9- е издание вида , которое только возможно записать. Результатом его вычисления будет константное значение: () -> 98.6 Показанное выше лямбда- выражение не принимает параметров , из- за чего список параметров пуст, и возвращает константное значение 98.6. Таким образом , оно аналогично следующему методу: double myMethO { return 98.6; } Разумеется, определяемый лямбда- выражением метод не имеет имени. А вот более интересное лямбда - выражение: () -> Math . random() * 100 Это лямбда - выражение получает псевдослучайное значение от Math , random(), умножает его на 100 и возвращает результат. Оно тоже не требует параметра. Когда лямбда- выражению необходим параметр, он указывается в списке па раметров слева от лямбда-операции: ( п) -> 1.0 / п Показанное выше лямбда- выражение возвращает обратную величину параметра п. Таким образом , если п равно 4.0, то обратная величина равна 0.25. Хотя можно явно указывать тип параметра , например, п в данном случае, часто это не требуется , поскольку во многих ситуациях его тип можно вывести. Как и именованный метод, в лямбда - выражении может быть указано столько параметров, сколько необходимо. В качестве возвращаемого типа лямбда- выражения разрешено использовать любой допустимый тип. Например, следующее лямбда- выражение возвращает true, если значение параметра п является четным , и false в противном случае: ( п) -> (п % 2)==0 Таким образом , типом возвращаемого значения этого лямбда-выражения будет boolean. Прежде чем двигаться дальше , важно упомянуть об еще одном аспекте. Когда лямбда- выражение имеет только один параметр, нет необходимости помещать имя параметра в круглые скобки , если оно указано в левой части лямбда-операции. Например, только что представленное лямбда- выражение можно записать и так: - п > (п % 2)==0 Ради согласованности в книге все списки параметров лямбда- выражений заключаются в круглые скобки, даже те , которые содержат только один параметр. Разумеется, вы можете избрать для себя другой стиль. Глава 14. Лямбда -выражения и ссылки на методы 527 Функциональные интерфейсы Как упоминалось ранее, функциональный интерфейс представляет собой та кой интерфейс , в котором определен только один абстрактный метод. Вспом ните из главы 8, что не все методы интерфейса должны быть абстрактными . Начиная с JDK 8 , интерфейс может иметь один или несколько стандартных методов, которые не являются абстрактными, как и закрытые и статические методы интерфейса. В результате теперь метод интерфейса будет абстрактным только в том случае , если для него не определена реализация. Это означает, что функциональный интерфейс может включать стандартные , статические или закрытые методы, но во всех случаях он должен иметь один и только один абстрактный метод. Поскольку нестандартные , нестатические , незакрытые методы интер фейса неявно абстрактны, нет нужды применять модификатор abstract ( правда , при желании его можно указывать). Ниже приведен пример функционального интерфейса: interface MyValue { double getValueO ; } В данном случае метод getValue ( ) является неявно абстрактным и един ственным методом, определенным в MyValue. Таким образом, MyValue функциональный интерфейс , функция которого определяется getValue(). Ранее уже упоминалось, что лямбда -выражение не выполняется само по себе. Оно скорее формирует реализацию абстрактного метода , определенного в функциональном интерфейсе , который указывает его целевой тип . В результате лямбда- выражение может быть указано только в контексте, где определен целевой тип. Один из таких контекстов создается , когда лямбда - выражение присваивается ссылке на функциональный интерфейс. Другие контексты целевого типа включают помимо прочего инициализацию переменных , операторы return и аргументы метода. Давайте рассмотрим простой пример. Для начала объявляется ссылка на функциональный интерфейс MyValue: — // Создать ссылку на экземпляр реализации MyValue. MyValue myVal; Затем созданной ссылке на интерфейс присваивается лямбда- выражение: // Использовать лямбда-выражение в контексте присваивания. myVal = () -> 98.6; Показанное лямбда - выражение совместимо с методом getValue ( ) , потому что подобно getValue ( ) оно не имеет параметров и возвращает результат типа double. В общем случае тип абстрактного метода , определяемый функциональным интерфейсом , и тип лямбда-выражения должны быть совместимыми , иначе возникнет ошибка на этапе компиляции . 528 Java: руководство для начинающих, 9-е издание Вы уже наверняка догадались, что два представленных шага при желании можно объединить в один оператор: MyValue myVal = () -> 98.6; Переменная myVal инициализируется лямбда- выражением. Когда лямбда- выражение встречается в контексте целевого типа , автоматически создается экземпляр класса , который реализует функциональный интерфейс, а лямбда - выражение определяет поведение абстрактного метода , объявленного в функциональном интерфейсе. При вызове данного метода через цель лямбда - выражение выполняется. Таким образом, лямбда - выражение предоставляет способ трансформации кодового сегмента в объект. В предыдущем примере лямбда - выражение становится реализацией метода getValue(). В результате следующий код отображает значение 98.6: // Вызвать метод getValue(), который реализован // присвоенным ранее лямбда-выражением. System.out.println("Константное значение: " + myVal.getValue()); Поскольку лямбда-выражение , присвоенное переменной myNum, возвращает значение 98.6, это значение и будет получено при вызове getValue(). Если лямбда- выражение принимает один или несколько параметров, то абстрактный метод в функциональном интерфейсе тоже должен принимать такое же количество параметров. Например, вот функциональный интерфейс MyParamValue, который позволяет передавать значение в getValue ( ) : interface MyParamValue { double getValue(double v); } Данный интерфейс можно применить для реализации лямбда- выражения , возвращающего обратную величину, из предыдущего раздела: MyParamValue myPval = (n) -> 1.0 / n; Затем myPval можно использовать примерно так: System.out.println ("Обратная величина 4 равна " + myPval.getValue(4.0)); Здесь getValue() реализуется лямбда-выражением , на которое ссылается myPval, и возвращает обратную величину аргумента. В данном случае значение 4.0 передается методу getValue ( ) , который возвращает 0.25. В предыдущем примере есть еще кое- что интересное. Обратите внима ние , что тип п не указан. Взамен тип выводится из контекста . В этом случае тип п выводится из типа параметра getValue(), определенного в интерфейсе MyParamValue, т.е. double. Тип параметра можно явно указывать в лямбда- выражении . Скажем , вот допустимый способ записи приведенного выше примера: (double п) -> 1.0 / п; Здесь для п явно указан тип double, хотя обычно в явном указании типа нет никакой необходимости. Глава 14. Лямбда -выражения и ссылки на методы 529 На заметку! Интересно отметить, что начиная с JDK 11, можно также явно задавать выведение типа для параметра лямбда-выражения с помощью var, например: (var п) -> 1.0 / п; Конечно, использование ключевого слова var здесь избыточно, но позволяет добавлять аннотацию. Прежде чем двигаться дальше , важно подчеркнуть один ключевой момент: чтобы лямбда - выражение можно было применять в контексте целевого типа , типы абстрактного метода и лямбда- выражения должны быть совместимыми . Скажем , если в абстрактном методе заданы два параметра типа int , то в лямбда- выражении должны быть указаны два параметра , типом которых является или явный int , или тип может быть неявно выведен контекстом как int. Обычно тип и количество параметров лямбда- выражения должны согласовываться с параметрами метода; возвращаемые типы должны быть совместимыми, а любые исключения , генерируемые лямбда - выражением , должны быть допустимыми для метода. Лямбда-выражения в действии С учетом предыдущего обсуждения давайте взглянем на несколько простых примеров, иллюстрирующих основные концепции лямбда- выражений. В первом примере собраны вместе все части, показанные в предыдущем разделе: // Демонстрация использования простого лямбда-выражения. // Функциональный интерфейс. interface MyValue { double getValue(); } Функциональные интерфейсы // Еще один функциональный интерфейс , interface MyParamValue { < double getValue(double v); } class LambdaDemo { public static void main(String[] args) { MyValue myVal; // объявить ссылку на интерфейс // Здесь лямбда-выражение представляет собой константное выражение. // Когда оно присваивается myVal, конструируется экземпляр класса, // где лямбда-выражение реализует метод getValue() из MyValue. myVal = () -> 98.6; Простое лямбда -выражение // Вызвать метод getValue(), предоставляемый ранее // присвоенным лямбда выражением. System.out.println ("Константное значение: " + myVal.getValue()); - // Создать параметризованное лямбда-выражение и присвоить // его ссылке MyParamValue. Это лямбда-выражение возвращает 530 Java: руководство для начинающих, 9-е издание // обратную величину переданного ему аргумента. MyParamValue myPval = (n) -> 1.0 / n; Лямбда -выражение, имеющее параметр // Вызвать getValue(v) через ссылку myPval. System.out.println("Обратная величина 4 равна " + myPval.getValue(4.0)); System.out.println("Обратная величина 8 равна " + myPval.getValue(8.0)); // // // // } Лямбда-выражение должно быть совместимым с методом, определенным в функциональном интерфейсе. Таким образом, следующий код недопустим: myVal = О -> "three"; // Ошибка! Типы String и double не совместимы ! myPval = () > Math.random(); // Ошибка! Требуется параметр! - } Вот пример вывода: Константное значение: 98.6 Обратная величина 4 равна 0.25 Обратная величина 8 равна 0.125 Как упоминалось ранее , лямбда-выражение должно быть совместимым с абстрактным методом , для реализации которого оно предназначено. По этой причине код в закомментированных строках в конце предыдущей программы недопустим. Во- первых, значение типа String несовместимо с типом double, т.е. возвращаемым типом метода getValue ( ) . Во- вторых, метод getValue(int) из MyParamValue требует параметра. Ключевой аспект функционального интерфейса состоит в том, что его можно использовать с любым лямбда - выражением , которое с ним совместимо. Например , рассмотрим показанную далее программу. В ней определен функциональный интерфейс NumericTest, в котором объявлен абстрактный метод test ( ) , принимающий два параметра типа int и возвращающий результат типа выяснить, удовлетворяют ли переданные ему boolean. Цель метода test ( ) два аргумента какому-то условию. Результат проверки возвращается. В методе main ( ) создаются три разных теста с использованием лямбда-выражений. Один тест проверяет, может ли первый аргумент делиться на второй без остатка. Второй тест определяет, меньше ли первый аргумент второго. Третий тест возвращает true, если абсолютные значения аргументов равны. Обратите внимание , что лямбда- выражения , реализующие указанные тесты, имеют два параметра и возвращают результат типа boolean. Естественно , это необходимо, поскольку метод test ( ) принимает два параметра и возвращает результат boolean. — // Использование одного и того же функционального интерфейса // с тремя разными лямбда-выражениями. // Функциональный интерфейс, который принимает два параметра int // и возвращает результат boolean , interface NumericTest { boolean test(int n, int m); } class LambdaDemo2 { public static void main(String[] args) { Глава 14. Лямбда -выражения и ссылки на методы 531 // Это лямбда-выражение определяет, // является ли одно число делителем другого. NumericTest isFactor = (n, d) -> (n % d) == 0; if(isFactor.test( 10, 2)) System.out.println("2 является делителем 10 м ); if(!isFactor.test( 10, 3)) System.out.println("3 не является делителем 10"); System.out.println(); // Это лямбда-выражение возвращает true, // если первый аргумент меньше второго. NumericTest lessThan = (n, m) -> (n < m); Использовать один и тот же функциональный интерфейс с тремя разными лямбда -выражениями if(lessThan.test(2, 10)) System.out.println("2 меньше 10"); if(!lessThan.test(10, 2)) System.out.println("10 не меньше 2"); System.out.println(); // Это лямбда-выражение возвращает true, // если абсолютные значения аргументов равны . NumericTest absEqual = ( п, m) -> (п < 0 ? -п : п) == (т < 0 ? -т : т); if(absEqual.test(4, -4)) System.out.println("Абсолютные значения 4 и if(!lessThan.test(4, 5)) System.out.println("Абсолютные значения 4 и System.out.println(); - -4 равны ."); -5 не равны ."); } } Ниже показан вывод: 2 является делителем 10 3 не является делителем 10 2 меньше 10 10 не меньше 2 Абсолютные значения 4 и Абсолютные значения 4 и -4 -5 равны . не равны . В программе было проиллюстрировано , что поскольку все три лямбда - выражения совместимы с test ( ) , все они могут быть выполнены через ссылку NumericTest. На самом деле нет необходимости применять три отдельные ссылочные переменные NumericTest, т. к . для всех трех тестов допускается использовать одну и ту же переменную. Скажем , можно создать переменную myTest, а затем применять ее для обращения к каждому тесту по очереди: NumericTest myTest; myTest = (n, d) -> (n % d) == 0; 532 Java: руководство для начинающих, 9-е издание if(myTest.test( 10, 2)) System.out.println("2 является делителем 10"); ... И myTest = (n, m) -> ( n < m); if( myTest.test(2, 10)) System.out.println("2 меньше 10"); П ... myTest = (n, m) -> (n < 0 ? -n : n) == (m < 0 ? -m : m); if(myTest.test(4 , -4 )) System.out.println("Абсолютные значения 4 и -4 равны ."); ... II Несомненно , использование разных ссылочных переменных с именами isFactor, lessThan и absEqual, как делалось в исходной программе , хорошо проясняет, к какому лямбда- выражению относится каждая переменная. С предыдущей программой связан еще один интересный момент. Обратите внимание на то, как указаны два параметра для лямбда- выражений. Скажем , ниже приведено лямбда-выражение, которое определяет, является ли одно число делителем другого: (n, d) -> ( n % d) == 0 Как видите, п и d разделяются запятыми. Обычно если требуется более одного параметра , то они указываются через запятую в списке в скобках слева от лямбда -операции. Хотя в предшествующих примерах в качестве типов параметров и возвращаемого типа абстрактного метода , определяемого функциональным интерфейсом , применялись значения примитивных типов, ограничений в данном отношении нет. Например , в следующей программе объявляется функциональный интерфейс по имени StringTest, имеющий метод test ( ) , который принимает два параметра типа String и возвращает результат типа boolean. Таким образом , его можно использовать для проверки ряда условий , связанных со строками. Здесь создается лямбда - выражение , которое определяет, содержится ли одна строка внутри другой: // Функциональный интерфейс, проверяющий две строки. interface StringTest { boolean test(String aStr, String bStr); } class LambdaDemo3 { public static void main(String[ 3 args) { // Это лямбда-выражение определяет, содержится // л и одна строка внутри другой. StringTest isln = (a, b) -> a.indexOf(b) != -1; String str = "This is a test"; System.out.println("Проверяемая строка: " + str); if(isln.test(str, "is a ")) System.out.println("Строка 'is а' найдена."); Глава 14. Лямбда -выражения и ссылки на методы 533 else System.out.println("Строка 'is а' не найдена."); if(isln.test(str, "xyz")) System.out.println("Строка 'xyz' найдена."); else System.out.println("Строка 'xyz не найдена."); } } Вот вывод, генерируемый программой: Проверяемая строка: This is a test Строка 'is а' найдена. Строка 'xyz' не найдена. Обратите внимание , что в лямбда- выражении используется метод indexOf() из класса String с целью определения, является ли одна строка частью другой . Прием работает, потому что для параметров а и b выводится тип String. Таким образом , вполне допустимо вызывать метод String на объекте а. !?@>A8< C M:A?5@B0 ВОПРОС. Ранее упоминалось о том , что при необходимости можно явно объявлять тип параметра в лямбда-выражении. В случаях, когда для лямбда-выражения требуются два и большее число параметров, нужно ли указывать типы всех параметров или же можно позволить одному или нескольким параме трам использовать выведение типов? ОТВЕТ. В ситуациях , когда необходимо явно объявить тип какого-то параметра , все параметры в списке должны иметь объявленные типы. Например, поступать следующим образом разрешено: (int n, int d ) -> (n % d) == О Но так делать нельзя: -> == О И так тоже делать нельзя: ( n, int d ) -> (n % d ) == О ( int n, d ) (n % d ) Блочные лямбда-выражения Тело в лямбда- выражениях , показанных в предшествующих примерах, состояло из единственного выражения . Такой вид тела лямбда- выражения называется телом- выражением, а лямбда- выражение с телом- выражением одиночным лямбда -выражением . В теле-выражении код в правой части лямбда -операции должен содержать одно выражение. Хотя одиночные лямбда- выражения весьма полезны, иногда ситуация требует более одного выражения . Для обработки та ких случаев в Java поддерживается второй вид лямбда - выражений , где в правой — 534 Java: руководство для начинающих, 9-е издание части лямбда-операции находится блок кода, который может содержать более одного оператора . Тело этого вида называется блочным. Лямбда- выражения с блочными телами иногда называются блочными лямбда -выражениями. Блочное лямбда- выражение расширяет типы операций , которые могут быть обработаны в лямбда - выражении , поскольку позволяет телу лямбда - выражения содержать несколько операторов. Например , в блочном лямбда-выражении можно объявлять переменные, организовывать циклы, применять операторы if и switch, создавать вложенные блоки и т.д. Блочное лямбда-выражение создать легко. Нужно просто поместить тело в фигурные скобки подобно любому другому блоку операторов. За исключением того , что блочные лямбда- выражения разрешают указывать несколько операторов , они используются почти так же, как только что рассмотренные одиночные лямбда- выражения. Тем не менее, есть одно ключевое отличие: вы обязаны явно применять оператор return, чтобы возвратить значение. Поступать так необходимо, потому что тело блочного лямбда- выражения не представляет одиночное выражение. Ниже приведен пример, в котором блочное лямбда- выражение используется с целью нахождения наименьшего положительного делителя для значения типа int. В нем применяется интерфейс NumericFunc с методом func(), который принимает один аргумент типа int и возвращает результат типа int. Таким образом, NumericFunc поддерживает числовую функцию для значений типа int. // Блочное лямбда-выражение, которое находит наименьший // положительный делитель для значения типа int. interface NumericFunc { int func(int n); } class BlockLambdaDemo { public static void main(String[] args) { // Это блочное лямбда-выражение возвращает наименьший // положительный делитель для значения. NumericFunc smallestF = ( n) -> { int result = 1; // Получить абсолютное значение n. n = n < 0 ? -n : n; for(int i=2; i <= n/i; i++) if((n % i) == 0) { result = i; break; Блочное лямбда -выражение } return result; }; System.out.println("Наименьший делитель 12: " + smallestF.func(12)); System.out.println("Наименьший делитель 11: " + smallestF.func(11)); } } Глава 14. Лямбда -выражения и ссылки на методы 535 Вот вывод, генерируемый программой: Наименьший делитель 12: 2 Наименьший делитель 11: 1 Обратите внимание в программе , что внутри блочного лямбда- выражения объявляется переменная по имени result, организуется цикл for и применя ется оператор return. Они разрешены в теле блочного лямбда-выражения. По существу тело блочного лямбда - выражения похоже на тело метода. И еще один момент: когда в лямбда - выражении встречается оператор return, он просто приводит к возврату из лямбда-выражения , но не к возврату из объемлющего метода. Обобщенные функциональные интерфейсы Само лямбда- выражение не может указывать параметры типа. Таким образом , лямбда-выражение не может быть обобщенным. ( Разумеется , из- за выведения типов все лямбда - выражения обладают некоторыми “ обобщенными ” качествами . ) Однако функциональный интерфейс, ассоциированный с лямбда выражением , может быть обобщенным. В таком случае целевой тип лямбда- вы ражения частично определяется аргументом или аргументами типов, указанными при объявлении ссылки на функциональный интерфейс. Давайте попытаемся понять ценность обобщенных функциональных интерфейсов. В двух примерах из предыдущего раздела использовались два разных функциональных интерфейса: один назывался NumericTest, а другой StringTest. Тем не менее , в обоих интерфейсах определялся метод test(), который принимал два параметра и возвращал результат boolean. В случае NumericTest проверяемые значения имели тип int, а в случае StringTest тип String. Таким образом , единственным отличием между двумя методами был тип данных, с которыми они оперировали. Вместо двух функциональных интерфейсов, методы которых различаются только типами данных, можно объявить один обобщенный интерфейс и применять его в обоих обстоятельствах. Подход демонстрируется в следующей программе: — — // Использование обобщенного функционального интерфейса. // Обобщенный функциональный интерфейс с двумя параметрами, // который возвращает результат типа boolean. interface SomeTest<T> { Обобщенный функциональный интерфейс boolean test(T n, T m); } class GenericFunctionallnterfaceDemo { public static void main(String[] args) { // Это лямбда-выражение определяет, является // ли одно число типа Integer делителем другого. SomeTest<Integer> isFactor = (n, d) -> (n % d) == 0; if(isFactor.test(10, 2)) System.out.println ("2 является делителем 10"); System.out.printIn(); 536 Java: руководство для начинающих, 9- е издание // Это лямбда-выражение определяет, является // л и одно число типа Double делителем другого. SomeTest<Double> isFactorD = (n, d) -> (n % d) == 0; if(isFactorD.test(212.0, 4.0)) System.out.println("4.0 является делителем 212.0"); System.out.printIn(); // Это лямбда-выражение определяет, является // л и одна строка частью другой строки. SomeTest<String> isln = (a, b) -> a.indexOf(b) != String str = "Generic Functional Interface"; -1; System.out.println("Проверяемая строка: " + str); if(isln.test(str, "face")) System.out.println("Строка 'face' найдена."); else System.out.println("Строка 'face' не найдена."); } } Ниже показан вывод: 2 является делителем 10 4.0 является делителем 212.0 Проверяемая строка: Generic Functional Interface Строка 'face' найдена. Обобщенный функциональный интерфейс SomeTest объявлен в программе , как показано далее: interface SomeTest <T> { boolean test(T n, T m); } Здесь T указывает возвращаемый тип и тип параметра test ( ) . Это означает, что он совместим с любым лямбда - выражением , которое принимает два пара метра и возвращает результат типа boolean. Интерфейс SomeTest применяется для предоставления ссылки на три разных вида лямбда - выражений . Первое лямбда - выражение использует тип Integer, второе — тип Double, а третье — тип String. Таким образом , один и тот же функциональный интерфейс может применяться для ссылки на лямбда - выра жения isFactor, isFactorD и isln. Отличается только аргумент типа , переда ваемый SomeTest. Интересно отметить , что интерфейс NumericFunc, представленный в пре дыдущем разделе , тоже можно сделать обобщенным (см. раздел “ Вопросы и упражнения для самопроверки ” в конце главы ) . Глава 14. Лямбда -выражения и ссылки на методы Упражнение 14.1 537 Передача лямбда-выражения в качестве аргумента Лямбда- выражение можно применять в любом LambdaArgumentDemo.java контексте , который обеспечивает целевой тип. Целевыми контекстами , используемыми в предыдущих примерах , являются присваивание и инициализация. Еще один целевой контекст касается ситуации , когда лямбда- выражение передается в качестве аргумента. На самом деле передача лямбда-выражения как аргумента весьма распространенный сценарий использования лямбда- выражений. Более того , ему присуща высокая эффективность, поскольку появляется возможность передачи методу исполняемого кода в виде аргумента , что значительно увеличивает выразительную мощь Java . Чтобы проиллюстрировать сам процесс , в текущем проекте создаются три строковые функции, которые выполняют обращение строки , обращение реги стра букв в строке и замену пробелов дефисами. Функции реализованы в виде лямбда - выражений функционального интерфейса StringFunc и передаются в первом аргументе методу changeStr ( ) . Метод changeStr ( ) применяет строковую функцию к строке, переданной во втором аргументе методу changeStr(), и возвращает результат. Таким образом , changeStr ( ) можно использовать для применения разнообразных строковых функций. 1 Создайте файл по имени LambdaArgumentDemo.java. 2 Поместите в файл LambdaArgumentDemo.java функциональный интерфейс StringFunc, код которого показан ниже: — . . interface StringFunc { String func(String str); } В этом интерфейсе определен метод func ( ) , который принимает аргумент String и возвращает значение типа string. Таким образом , метод func() может работать со строкой и возвращать результат. 3. Начните определение класса LambdaArgumentDemo с метода changeStr(): class LambdaArgumentDemo { // Типом первого параметра этого метода является // функциональный интерфейс. // Таким образом, ему можно передавать ссылку на любой экземпляр // данного интерфейса, включая экземпляр, созданный лямбда-выражением. // Второй параметр задает строку , с которой нужно работать , static String changeStr(StringFunc sf, String s) { return sf.func(s); } Как указано в комментариях , метод changeStr ( ) принимает два параме тра. Типом первого параметра является StringFunc, т.е . в нем может быть передана ссылка на любой экземпляр StringFunc. Следовательно , в нем можно передавать ссылку на экземпляр , созданный лямбда-выражением , 538 Java: руководство для начинающих, 9-е издание . который совместим с типом StringFunc. В s передается строка, с которой нужно работать, а результирующая строка возвращается. 4 Начните определение метода main(): public static void main(String[] args) { String inStr = "Lambda Expressions Expand Java"; String outStr; System.out.println("Входная строка: " + inStr); Здесь inStr ссылается на строку, с которой будет производиться работа , а outStr получит модифицированную строку. 5. Определите лямбда - выражение , которое меняет на противоположный порядок следования символов в строке, и присвойте его ссылке StringFunc. Обратите внимание, что это еще один пример блочного лямбда- выражения. // Определить лямбда-выражение, которое меняет // на противоположный порядок следования // символов в строке, и присвоить его ссылочной переменной StringFunc. StringFunc reverse = (str) - > { String result = vv vv . for(int i = str.length()-1; i >= 0; i ) result += str.charAt(i); — return result; }; 6. Вызовите метод changeStr ( ) , передав ему лямбда- выражение reverse и строку inStr. Возвращенный результат присвойте outStr и отобразите. // Передать reverse в первом аргументе changeStr(). // Передать входную строку во втором аргументе. outStr = changeStr(reverse, inStr); System.out.println("Обращенная строка: " + outStr); Поскольку первый параметр changeStr ( ) имеет тип StringFunc, ему может быть передано лямбда - выражение reverse. Вспомните , что лямбдавыражение приводит к созданию экземпляра целевого типа , которым в данном случае является StringFunc. Таким образом , лямбда- выражение позволяет эффективно передавать методу кодовую последовательность. 7. Завершите программу, добавив лямбда- выражения , которые заменяют пробелы дефисами и инвертируют регистр букв. Обратите внимание , что оба лямбда-выражения встроены в сам вызов changeStr ( ) , а не в отдельную переменную типа StringFunc. // Это лямбда-выражение заменяет пробелы дефисами. // Оно встроено непосредственно в вызов changeStr(). i i outStr = changeStr((str) -> str.replace( ), inStr); System.out.println ("Строка с замененными пробелами: " + outStr); _ Глава 14. Лямбда -выражения и ссылки на методы 539 // Это блочное лямбда-выражение инвертирует регистр букв в строке. // Оно тоже встроено непосредственно в вызов changeStrO . outStr = changeStr((str) -> { String result = char ch; IV vv • for(int i = 0; i < str.length(); i++ ) { ch = str.charAt(i); if(Character.isUpperCase(ch )) result += Character.toLowerCase(ch); else result += Character.toUpperCase(ch); } return result; }, inStr); System.out.println("Строка с инвертированным регистром букв: " + outStr); } } В коде видно , что встраивание лямбда - выражения , заменяющего пробелы дефисами , в вызов changeStr ( ) удобно и просто для понимания. Причи на в том, что это короткое лямбда- выражение, в котором просто вызывается replace ( ) с целью замены пробелов дефисами. Метод replace() еще один метод, определенный в классе string. Используемая здесь версия принимает в качестве аргументов заменяемый символ и его замену, а возвращает измененную строку. — В целях иллюстрации лямбда - выражение, инвертирующее регистр букв в строке , тоже встроено в вызов changeStr ( ) , но в данном случае получается довольно громоздкий код, который трудно воспринимать. Обычно такое лямбда-выражение лучше присвоить отдельной ссылочной переменной ( как делалось для лямбда- выражения , обращающего строку ) , после чего передать эту переменную методу. Разумеется, как продемонстриро вано в примере, передавать блочное лямбда- выражение методу в качестве аргумента формально разрешено. И еще один момент: обратите внимание , что в лямбда-выражении , которое инвертирует регистр букв в строке , используются статические ме тоды isUpperCase(), toUpperCase ( ) и toLowerCase ( ) , определенные в Character. Вспомните , что Character является классом -оболочкой для типа char. Метод isUpperCase ( ) возвращает true, если его аргументом является буква в верхнем регистре , или false в противном случае. Методы toUpperCase ( ) и toLowerCase ( ) переводят буквы соответственно в верхний и нижний регистр и возвращают результат. Помимо указанных методов в классе Character определено несколько других, выполняющих манипуляции с символами или проверки символов. Вы наверняка захоти те изучить их самостоятельно. 540 Java: руководство для начинающих, 9-е издание 8. Ниже приведен полный код программы: // Использование лямбда-выражения в качестве аргумента метода , interface StringFunc { String func(String str); } class LambdaArgumentDemo { // Типом первого параметра этого метода является функциональный интерфейс. // Таким образом, ему можно передавать ссылку на любой экземпляр данного // интерфейса, включая экземпляр, созданный лямбда-выражением. // Второй параметр задает строку, с которой нужно работать , static String changeStr(StringFunc sf, String s) { return sf.func(s); } public static void main(String[] args) { String inStr = "Lambda Expressions Expand Java"; String outStr; System.out.println("Входная строка: " + inStr); // Определить лямбда-выражение, которое меняет // н а противоположный порядок следования // символов в строке, и присвоить его ссылочной переменной StringFunc. StringFunc reverse = (str) -> { String result = и и for(int i = str.length() 1; i >= 0; i ) result += str.charAt(i); return result; }; . - — // Передать reverse в первом аргументе changeStr(). // Передать входную строку во втором аргументе. outStr = changeStr(reverse, inStr); System.out.println("Обращенная строка: " + outStr); // Это лямбда-выражение заменяет пробелы дефисами. // Оно встроено непосредственно в вызов changeStr(). outStr = changeStr((str) > str.replace(' ', 1 * ), inStr); System.out.println("Строка с замененными пробелами: " + outStr); - - // Это блочное лямбда-выражение инвертирует регистр букв в строке. // Оно тоже встроено непосредственно в вызов changeStr(). outStr = changeStr((str) > { String result = ii ii . char ch; for(int i = 0; i < str.length(); i ++ ) { ch = str.charAt(i); if(Character.isUpperCase(ch)) result += Character.toLowerCase(ch); else result += Character.toUpperCase(ch); } return result; }, inStr); - Глава 14. Лямбда -выражения и ссылки на методы 541 System.out.println("Строка с инвертированным регистром букв: " + outStr); } } Вот вывод, генерируемый программой: Входная строка: Lambda Expressions Expand Java Обращенная строка: avaJ dnapxE snoisserpxE adbmaL Строка с замененными пробелами: Lambda-Expressions-Expand-Java Строка с инвертированным регистром букв: 1AMBDA eXPRESSIONS eXPAND jAVA !?@>A8< C M:A?5@B0 ВОПРОС. Какие еще контексты целевого типа существуют для лямбда -выражения кроме инициализации переменных, присваивания и передачи аргументов? ОТВЕТ. Контекстами целевого типа могут служить также приведения, операция ? , инициализаторы массивов, операторы return и сами лямбда-выражения. Лямбда-выражения и захват переменных Переменные , определенные в объемлющей области действия лямбда-выра жения, доступны внутри лямбда- выражения. Скажем , лямбда-выражение может задействовать переменную экземпляра или статическую переменную , определенную в объемлющем классе. Лямбда - выражение также имеет доступ к ссыл ке this ( явно и неявно), которая ссылается на вызывающий экземпляр класса , включающего лямбда - выражение . Таким образом , лямбда - выражение может получать либо устанавливать значение переменной экземпляра или статической переменной и вызывать метод, определенный в объемлющем классе . Тем не менее , когда в лямбда- выражении используется локальная перемен ная из его объемлющей области видимости, то возникает особая ситуация , на зываемая захватом переменной . В таком случае лямбда - выражение может ра ботать только с локальными переменными , которые являются фактически фи нальными . Фактически финальная переменная представляет собой переменную, значение которой не меняется после ее первого присваивания. Явно объявлять такую переменную как final нет никакой необходимости , хотя поступать так не будет ошибкой . ( Параметр this объемлющей области видимости автоматически будет фактически финальным , а лямбда- выражения не имеют собствен ной ссылки this.) Следует понимать, что локальная переменная из объемлющей области не мо жет быть модифицирована лямбда - выражением , поскольку в таком случае исчез бы ее статус фактически финальной, из- за чего она стала бы незаконной для захвата. В показанной далее программе иллюстрируется отличие между фактически финальными и изменяемыми локальными переменными: 542 Java: руководство для начинающих, 9-е издание // Пример захвата локальной переменной из объемлющей области видимости , interface MyFunc { int func(int n); } class VarCapture { public static void main(String[] args) { // Локальная переменная, которая может быть захвачена , int num = 10; MyFunc myLambda = (n) -> { // Использовать num подобным образом разрешено. // Переменная num не модифицируется , int v = num + n; // Однако следующая строка кода недопустима из-за того, что в ней // предпринимается попытка модифицировать значение num. // num++; return v; }; // Использовать лямбда-выражение. Отобразится число 18. System.out.printIn(myLambda.func(8)); } // Следующая строка кода тоже вызовет ошибку, потому что в ней // устраняется статус переменной num как фактически финальной. // num = 9; } В комментариях указано , что переменная num является фактически финальной и потому может применяться внутри myLambda. Именно по этой причине оператор printlnO выводит число 18. Когда метод func() вызывается с аргументом 8, значение v внутри лямбда- выражения устанавливается путем сложения переменной num ( равной 10)и значения , переданного в п ( равного 8). В итоге func ( ) возвращает 18. Такая работа объясняется тем , что num не изменяется после инициализации . Однако если значение num будет изменено внутри лямбда-выражения или за его пределами, то переменная num утратит свой статус фактически финальной, что вызовет ошибку и программа не скомпилируется. Важно подчеркнуть, что лямбда-выражение может использовать и модифицировать переменную экземпляра из вызывающего класса. Оно просто не мо жет работать с локальной переменной из своей объемлющей области видимости , если только эта переменная не является фактически финальной. Генерация исключений в лямбда-выражениях Лямбда- выражение может генерировать исключение. Тем не менее , если инициируется проверяемое исключение , то оно должно быть совместимым с исключением или исключениями , которые перечислены в конструкции throws абстрактного метода в функциональном интерфейсе. Например , если лямбда- выражение генерирует исключение IOException, то в конструкции throws абстрактного метода в функциональном интерфейсе должно быть указано IOException. Ситуация демонстрируется в следующей программе: Глава 14. Лямбда -выражения и ссылки на методы 543 import java.io.*; interface MylOAction { boolean ioAction(Reader rdr) throws IOException; } class LambdaExceptionDemo { public static void main(String[] args) { // Это блочное лямбда-выражение может сгенерировать исключение IOException. // Следовательно, IOException должно быть указано в конструкции throws // метода ioAction() в MylOAction. Это лямбда MylOAction mylO = (rdr) -> { выражение может int ch = rdr.read(); // может сгенерировать сгенерировать исключение // исключение IOException II ... return true; }; } } Поскольку вызов read () может привести к генерации исключения IOException, конструкция throws метода ioAction ( ) в функциональном интерфейсе MylOAction должна включать IOException. В противном случае программа не скомпилируется, потому что лямбда - выражение больше не будет совместимым с ioAction ( ) . Чтобы удостовериться в этом , удалите конструкцию throws и попробуйте скомпилировать программу. Вы увидите , что в результате возникнет ошибка. !?@>A8< C M:A?5@B0 ВОПРОС. Можно ли в лямбда- выражении использовать параметр, являющийся массивом? ОТВЕТ. Да . Однако в случае выведения типа параметра параметр лямбда-выражения не указывается с применением обычного синтаксиса массивов. Взамен параметр указывается в форме простого имени вроде п , а не п [ ] . Не забывайте , что тип параметра лямбда -выражения будет выводиться из целевого контекста. Таким образом , если для целевого контекста требуется массив, то тип параметра будет автоматически выведен как массив. Чтобы лучше понять это, давайте рассмотрим небольшой пример. Ниже представлен обобщенный функциональный интерфейс по имени MyTransform, который можно использовать для применения трансформа ции к элементам массива: // Функциональный интерфейс , interface MyTransform<T> { void transform(Т[] a); } 544 Java: руководство для начинающих, 9-е издание Обратите внимание , что параметр метода transform ( ) является массивом типа Т. Теперь взгляните на следующее лямбда- выражение, которое использует MyTransform для преобразования элементов массива значений Double в их квадратные корни: MyTransform<Double> sqrts = (v) -> { for(int i=0; i < v.length; i++) v[i] = Math.sqrt(v[i]); }; Параметр а в transform ( ) имеет тип Double [ ] , потому что тип Double был указан в качестве параметра типа для MyTransform при объявлении sqrts. В итоге тип v в лямбда- выражении выводится как Double [ ] . Указывать его как v [ ] нет никакой необходимости (да и не разрешено). И последнее замечание: вполне законно объявить параметр лямбда-выражения как Double[] v, поскольку в таком случае явно объявляется тип параметра, но здесь это ничего не дает. Ссылки на методы С лямбда- выражениями связана одна важная возможность, которая называется ссылкой на метод. Ссылка на метод предлагает способ обращения к методу, не инициируя его выполнение. Она имеет отношение к лямбда-выражениям , поскольку тоже требует контекста целевого типа , состоящего из совместимого функционального интерфейса. При вычислении ссылки на метод также создается экземпляр функционального интерфейса . Существуют различные виды ссылок Ссылки на статические методы Для ссылки на статический метод применяется следующий общий синтаксис, предусматривающий указание имени класса перед именем метода: имя-класса : : имя-метода Обратите внимание , что имя класса отделяется от имени метода двойным двоеточием . Разделитель : : был добавлен к языку в версии JDK 8 специально для этой цели. Ссылка на метод может использоваться везде , где она совместима со своим целевым типом. В показанной далее программе демонстрируется применение ссылки на статический метод. Сначала объявляется функциональный интерфейс IntPredicate с методом test(), который принимает параметр типа int и возвращает результат типа boolean. Таким образом , его можно применять для проверки целочисленного значения на предмет соответствия заданному условию. Затем создается класс по имени MylntPredicates, где определяются три статических метода , каждый из которых проверяет, удовлетворяет ли значение заданному условию: isPrime(), isEven( ) и isPositive(). Метод isPrime() проверяет, является ли число простым , метод isEven ( ) является ли число — Глава 14. Лямбда -выражения и ссылки на методы 545 четным , а метод isPositive ( ) — является ли число положительным. Далее в классе MethodRefDemo создается метод numTest ( ) , в первом параметре которого передается ссылка на intPredicate. Во втором параметре указывается проверяемое целое число . Внутри main ( ) производятся три различных теста за счет вызова numTest ( ) и передачи ссылки на метод выполняемого теста. // Демонстрация использования ссылки на статический метод. // Функциональный интерфейс для предикатов, // работающих с целочисленными значениями , interface IntPredicate { boolean test(int n); } // В этом классе определены три статических метода, // которые выполняют проверку целого числа на предмет // соответствия условию , class MylntPredicates { // Статический метод, который возвращает true, // если число является простым , static boolean isPrime(int n) { if(n < 2) return false; for(int i=2; i <= n/i; i++) { if((n % i) == 0) return false; } return true; } // Статический метод, который возвращает true, // если число является четным , static boolean isEven(int n) { return (n % 2) == 0; } // Статический метод, который возвращает true, // если число является положительным , static boolean isPositive ( int n) { return n > 0; } } class MethodRefDemo { // Типом первого параметра этого метода является // функциональный интерфейс. Таким образом, // ему можно передавать ссылку на любой экземпляр // данного интерфейса, включая созданный ссылкой на метод , static boolean numTest(IntPredicate p, int v) { return p.test(v); } public static void main(String[] args) { boolean result; 546 Java: руководство для начинающих, 9-е издание numTestO ссылку на метод isPrime. result = numTest(MylntPredicates::isPrime, 17); if(result) System.out.println("17 является простым."); // Передать // Передать numTestO ссылку на метод isEven. result = numTest(MylntPredicates::isEven, 12); < if(result) System.out.println("12 является четным."); Использование ссылки на статический метод numTestO ссылку на метод isPositive. result = numTest(MylntPredicates::isPositive, 11); if(result) System.out.println("11 является положительным."); // Передать } } Вот вывод, генерируемый программой: 17 является простым. 12 является четным. 11 является положительным. Обратите особое внимание в программе на следующую строку: result = numTest(MylntPredicates::isPrime, 17); Здесь методу numTest ( ) в первом аргументе передается ссылка на статический метод isPrime ( ) . Код работает по причине совместимости isPrime ( ) с функциональным интерфейсом IntPredicate. Таким образом , результатом вычисления выражения MylntPredicates::isPrime будет ссылка на объект, в котором метод isPrime ( ) предоставляет реализацию test ( ) в IntPredicate. Остальные два вызова numTest( ) работают аналогично. Ссылки на методы экземпляра Ссылка на метод конкретного экземпляра создается с помощью следующего базового синтаксиса: объектная-ссылка : : имя-метода Как видите , синтаксис ссылки на метод экземпляра подобен синтаксису, применяемому для ссылки на статический метод, но вместо имени класса ис пользуется объектная ссылка. Таким образом , метод, на который указывает ссылка , работает по отношению к конструкции объектная ссылка. Данный аспект иллюстрируется в приведенной ниже программе. В ней используется тот же интерфейс IntPredicate и метод test(), что и в предыдущей программе , но создается класс по имени MylntNum, где хранится значение int и определяется метод isFactor ( ) , который выясняет, является ли переданное значение делителем значения , сохраненного в экземпляре MylntNum. Затем в методе main ( ) создаются два экземпляра MylntNum, после чего вызывается numTest ( ) с передачей ссылки на метод isFactorO и проверяемое значение. В каждом случае ссылка на метод работает относительно конкретного объекта . - Глава 14. Лямбда -выражения и ссылки на методы 547 // Демонстрация использования ссылки на метод экземпляра. // Функциональный интерфейс для предикатов, // работающих с целочисленными значениями , interface IntPredicate { boolean test(int n); } // Этот класс хранит значение типа int. Кроме того, в нем определен // метод экземпляра isFactorO , который возвращает true, если его // аргумент является делителем сохраненного числа , class MylntNum { private int v; MylntNum(int x) { v = x; } int getNum() ( return v; } // Возвращает true, если n является делителем v. boolean isFactor(int n) { return (v % n) == 0; } } class MethodRefDemo2 { public static void main(String[] args) { boolean result; MylntNum myNum = new MylntNum(12); MylntNum myNum2 = new MylntNum(16); // Создать ссылку на метод isFactor экземпляра myNum. Ссылка на метод IntPredicate ip = myNum ;:isFactor; экземпляра // Использовать ее для вызова isFactorO через test(). result = ip.test(3); if(result) System.out.println("3 является делителем " + myNum.getNum()); // Создать ссылку на метод isFactor экземпляра myNum2 // и применить ее для вызова isFactorO через test(). ip = myNum2::isFactor; result = ip.test(3); if(!result) System.out.println("3 не является делителем " + myNum2.getNum()); } } Вот вывод , генерируемый программой: 3 является делителем 12 3 не является делителем 16 Обратите особое внимание в программе на следующую строку: IntPredicate ip = myNum ;:isFactor; Здесь ссылка на метод , присвоенная ip, относится к методу isFactor ( ) экземпляра myNum. Таким образом , в случае вызова test ( ) через эту ссылку, как показано ниже: result = ip.test(3); 548 Java: руководство для начинающих, 9-е издание то будет вызван метод isFactorO на myNum, т.е. на объекте , указанном при создании ссылки на метод. Аналогичная ситуация и со ссылкой на метод myNum2: : isFactor, но только isFactor ( ) будет вызываться на myNum2. Сказан ное подтверждается выводом. Возможна также ситуация , когда желательно указать метод экземпляра , который можно применять с любым объектом заданного класса, а не только с указанным объектом. В таком случае ссылку на метод необходимо создать, как показано ниже: имя-класса : : имя-метода-экземпляра Здесь вместо конкретного объекта используется имя класса , даже когда указан метод экземпляра. В такой форме первый параметр функционального интерфейса соответствует вызываемому объекту, а второй параметру (если он есть) , заданному методом экземпляра . Рассмотрим новую версию предыдущего примера. Интерфейс IntPredicate заменяется интерфейсом MylntNumPredicate. В этом случае первый параметр для test ( ) имеет тип MylntNum. Он будет применяться для получения обрабатываемого объекта , что позволит создавать ссылку на метод экземпляра isFactor ( ) , который можно использовать с любым объектом MylntNum. — // Демонстрация использования ссылки на метод любого экземпляра. // Функциональный интерфейс // с объектом типа MylntNum interface MylntNumPredicate boolean test(MylntNum mv, } для предикатов, работающих и целочисленным значением { int n); , // Этот класс хранит значение типа int. Кроме того, в нем определен // метод экземпляра isFactorO , который возвращает true, если его // аргумент является делителем сохраненного числа , class MylntNum { private int v; MylntNum(int x) { v = x; } int getNumO { return v; } // Возвращает true, если n является делителем v. boolean isFactor(int n) { return (v % n) == 0; } } class MethodRefDemo3 { public static void main(String[] args) { boolean result; MylntNum myNum = new MylntNum(12); MylntNum myNum2 = new MylntNum(16); // Создать ссылку на метод экземпляра isFactorO . MylntNumPredicate inp = MylntNum::isFactor; Ссылка на метод любого объекта типа MylntNum // Вызвать isFactorO на myNum. result = inp.test(myNum, 3); Глава 14. Лямбда -выражения и ссылки на методы 549 if(result) System.out.println("3 является делителем " + myNum.getNum()); // Вызвать isFactorO на myNum2. result = inp.test(myNum2, 3); if(!result) System.out.println("3 не является делителем " + myNum2.getNum()); } } Ниже показан вывод: 3 является делителем 12 3 не является делителем 16 Обратите особое внимание в программе на следующую строку: MylntNumPredicate inp = MylntNum::isFactor; В ней создается ссылка на метод экземпляра isFactor ( ) , который будет работать с любым объектом типа MylntNum. Например, когда test ( ) вызывается через inp, как показано далее, это приводит к вызову myNum.isFactor(3): result = inp.test(myNum, 3); Другими словами , myNum становится объектом , для которого вызывается isFactor(3). !?@>A8< C M:A?5@B0 ВОПРОС. Как указать ссылку на обобщенный метод? ОТВЕТ. Часто из-за выведения типов не нужно явно указывать аргумент типа для обобщенного метода при получении ссылки на метод, но в Java предусмотрен синтаксис для тех случаев, когда это делается. Например, с учетом показанного ниже определения: interface SomeTest<T> { boolean test(T n, T m); } class MyClass { static <T> boolean myGenMeth(T x, T y) { boolean result = false; П ... return result; } } следующий оператор будет допустимым: SomeTest<Integer> mRef = MyClass::<Integer>myGenMeth; Здесь аргумент типа для обобщенного метода myGenMethO указан явно. Обратите внимание , что аргумент типа находится после : : . Такой синтаксис можно универсализировать: когда обобщенный метод указывается как ссылка на метод, его аргумент типа идет после : : и перед именем метода. В случаях, когда указан обобщенный класс, аргумент типа следует за именем класса и предшествует : : . 550 Java: руководство для начинающих, 9-е издание На заметку! В ссылке на метод можно использовать ключевое слово super, чтобы ссылаться на версию метода из суперкласса. Общие формы синтаксиса выглядят так: super : : имя метода и имя-типа . super : : имя-метода. Во второй форме имя типа должно относиться к охватывающему классу или суперинтерфейсу. - - Ссылки на конструкторы Подобно ссылкам на методы можно создавать ссылки на конструкторы . Ниже приведена общая форма синтаксиса , предназначенного для создания ссылки на конструктор: имя-класса::new Такую ссылку можно присваивать любой ссылке на функциональный интерфейс , в котором определен метод , совместимый с конструктором . Рассмотрим простой пример: // Демонстрация использования ссылки на конструктор. // MyFunc - функциональный интерфейс, метод которого // возвращает ссылку на конструктор MyClass. interface MyFunc { MyClass func(String s); } class MyClass { private String str; // Конструктор, принимающий аргумент. MyClass(String s) { str = s; } // Стандартный конструктор. MyClass() { str = I I I I; } II ... String getStrO { return str; } } class ConstructorRefDemo { public static void main(String[] args) { // Создать ссылку на конструктор MyClass. // Поскольку метод func() в MyFunc принимает аргумент, // new ссылается на параметризованный конструктор MyClass, // а не на стандартный. MyFunc myClassCons = MyClass::new; м Ссылка на конструктор // Создать экземпляр MyClass через эту ссылку на конструктор. MyClass me = myClassCons.func("Тест"); // Использовать только что созданный экземпляр MyClass. System.out.println("str в me: " + mc.getStr( )); } } Глава 14. Лямбда -выражения и ссылки на методы 551 Вот вывод: str в тс: Тест Обратите внимание , что метод func ( ) класса MyFunc в программе возвращает ссылку типа MyClass и принимает параметр String. Кроме того, в MyClass определены два конструктора. В первом имеется параметр типа String, а второй является стандартным конструктором без параметров. Теперь взгляните на следующую строку: MyFunc myClassCons = MyClass::new; Выражение MyClass::new создает ссылку на конструктор MyClass. Поскольку в данном случае метод func ( ) из MyFunc принимает параметр String, ссылка производится на конструктор MyClass(String s), т. к. он обеспечивает соответствие. Вдобавок обратите внимание , что ссылка на этот конструктор присваивается ссылке MyFunc по имени myClassCons. После выполнения оператора переменную myClassCons можно использовать для создания экземпляра MyClass: MyClass me = myClassCons.func("Тест"); По существу myClassCons становится еще одним способом вызова MyClass(String s). Если необходимо , чтобы в выражении MyClass::new был задействован стандартный конструктор MyClass, то придется использовать функциональный интерфейс , в котором определен метод без параметров. Ска жем, если определить MyFunc2, как показано ниже: interface MyFunc2 { MyClass func(); } то следующая строка обеспечит присваивание MyClassCons ссылки на стан дартный конструктор MyClass (т.е. не имеющий параметров): MyFunc2 myClassCons = MyClass::new; В общем случае при использовании : : new будет выбираться конструктор , параметры которого совпадают с параметрами, указанными в функциональном интерфейсе . И последнее замечание: в случае создания ссылки на конструктор для обобщенного класса параметр типа можно указывать обычным способом после имени класса. Например, если класс MyGenClass объявлен , как показано далее: MyGenClass<T> { II ... тогда следующая строка создаст ссылку на конструктор с аргументом типа Integer: MyGenClass<Integer>::new; Из-за выведения типов указывать аргумент типа нужно не всегда , но при необходимости это можно делать. 552 Java: руководство для начинающих, 9-е издание !?@>A8< C M:A?5@B0 ВОПРОС. Можно ли объявить ссылку на конструктор, который создает массив? ОТВЕТ. Да . Создать ссылку на конструктор для массива позволяет следующее выражение: тип[]::new В конструкции тип указывается тип создаваемого объекта . Например, с учетом класса MyClass из предыдущего примера и представленного ниже ин терфейса MyClassArrayCreator: interface MyClassArrayCreator { MyClass[] func(int n); } следующий код создает массив объектов MyClass и присваивает каждому элементу начальное значение: MyClassArrayCreator mcArrayCons = MyClass[]::new; MyClass[] a = mcArrayCons.func(3); for(int i=0; i < 3; i++) a[i] = new MyClass(i+""); Вызов func ( 3 ) приводит к созданию массива из трех элементов. Пример можно обобщить. Любой функциональный интерфейс , который будет при меняться для создания массива, должен содержать метод, принимающий единственный параметр типа int и возвращающий ссылку на массив ука занного размера. Интересно отметить, что довольно легко создать обобщенный функциональный интерфейс, который можно использовать с другими типами классов, как показано ниже: interface MyArrayCreator<T> { Т[] func(int n); } Скажем , вот как создать массив из пяти объектов Thread: MyArrayCreator<Thread > mcArrayCons = Thread[]::new; Thread[] thrds = mcArrayCons.func(5); Предопределенные функциональные интерфейсы Вплоть до этого момента в примерах настоящей главы определялись собственные функциональные интерфейсы , что позволило четко проиллюстри ровать фундаментальные концепции, лежащие в основе лямбда- выражений и функциональных интерфейсов. Тем не менее , во многих случаях определять собственный функциональный интерфейс не понадобится, поскольку в пакете java.util ,function предлагается ряд предопределенных интерфейсов. Глава 14. Лямбда -выражения и ссылки на методы 553 В табл . 14.1 описано несколько таких интерфейсов. Таблица 14.1 . Избранные предопределенные функциональные интерфейсы Интерфейс Описание UnaryOperator<T> Применяет унарную операцию к объекту типа Т и возвращает результат тоже типа Т. Его метод называется apply() Применяет операцию к двум объектам типа Т и возвращает результат тоже типа Т. Его метод называется apply() Применяет операцию к объекту типа Т. Его метод называется accept() Возвращает объект типа Т. Его метод называется get() Применяет операцию к объекту типа Т и возвращает в качестве результата объект типа R. Его метод называется apply() Выясняет, удовлетворяет ли объект типа Т определенному ограничению. Возвращает булевское значение, указывающее на результат проверки. Его метод называется test() BinaryOperator<T> Consumer<T> Supplier<T> FunctionCT, R> Predicate<T> В приведенной далее программе интерфейс Predicate демонстрируется в действии. Интерфейс Predicate используется в качестве функционального интерфейса для лямбда-выражения , позволяющего выяснить, является ли число четным. Абстрактный метод Predicate называется test ( ) : boolean test(T val) Он должен возвращать true, если val удовлетворяет некоторому ограничению или условию. Здесь метод test ( ) будет возвращать true, если в val хранится четное число. // Демонстрация использования встроенного функционального // интерфейса Predicate. // Импортировать интерфейс Predicate. import java.util.function.Predicate; class UsePredicatelnterface { public static void main(String[] args) { // Это лямбда выражение использует Predicate<Integer> // для выяснения, является ли число четным. Predicate<Integer> isEven = (n) - > (n %2) == 0; < - Использование встроенного интерфейса Predicate - четное число"); if(!isEven.test(5)) System.out.println("5 - нечетное число if(isEven.test(4)) System.out.println("4 } } Ниже показан вывод, генерируемый программой: 4 5 - четное число нечетное число "); 554 Java: руководство для начинающих, 9-е издание !?@>A8< C M:A?5@B0 ВОПРОС. В начале главы упоминалось о том , что добавление лямбда-выражений привело к появлению новых возможностей в библиотеке Java API. Можно ли привести пример? ОТВЕТ. Одним из примеров служит пакет для работы с потоками под названием java.util.stream. В нем определено несколько потоковых интерфейсов, самым общим из которых является Stream. Поток в java.util.stream — это средство передачи данных. Таким образом , поток представляет последовательность объектов. Кроме того, поток поддерживает множество типов операций, которые позволяют создавать конвейер для выполнения ряда дей ствий над данными. Часто такие действия представлены лямбда- выражени ями. Например, с применением потокового API несложно создавать последовательности действий, по своей концепции напоминающие вид запросов к базе данных , для которых можно использовать SQL. Кроме того , во многих случаях такие действия могут выполняться параллельно, что обеспечивает высокий уровень эффективности, особенно при работе с крупными наборами данных. Подводя итог, можно отметить, что потоковый API предлагает мощные средства обработки данных эффективным, но простым в применении способом. И последнее замечание: хотя потоки , поддерживаемые новым потоковым API , имеют некоторое сходство с потоками ввода-вывода , описанными в главе 10, они не идентичны. Вопросы и упражнения для самопроверки 1. Что такое лямбда-операция? 2. Что такое функциональный интерфейс? 3. Как связаны функциональные интерфейсы и лямбда-выражения ? 4. Назовите два общих типа лямбда- выражений. 5. Напишите лямбда- выражение , которое возвращает true, если число находится между 10 и 20 включительно. 6. Создайте функциональный интерфейс , который может поддерживать лямбда- выражение , написанное в пункте 5. Назначьте интерфейсу имя MyTest, а его абстрактному методу — имя testing(). 7. Создайте блочное лямбда -выражение , вычисляющее факториал цело численного значения. Продемонстрируйте его использование. В качестве функционального интерфейса применяйте NumericFunc, показанный в этой главе. Глава 14. Лямбда -выражения и ссылки на методы 555 8. Создайте обобщенный функциональный интерфейс по имени MyFunc<T>. Назовите его абстрактный метод func( ) . Пусть func ( ) возвращает ссыл ку типа Т и принимает параметр типа Т. (Таким образом , MyFunc будет обобщенной версией интерфейса NumericFunc, показанного в этой гла ве . ) Продемонстрируйте его использование , переписав решение пункта 7 так, чтобы в нем применялся MyFunc<T>, а не NumericFunc. 9 Используя программу из упражнения 14.1, создайте лямбда - выражение , которое удаляет все пробелы из строки и возвращает результат. Продемонстрируйте работу лямбда - выражения, передав его методу changeStr ( ) . 10 Можно ли использовать в лямбда - выражении локальную переменную? Если да , то какое ограничение должно быть удовлетворено? 11 Верно ли утверждение , что если лямбда- выражение генерирует проверяемое исключение , то абстрактный метод в функциональном интерфейсе должен иметь конструкцию throws, которая включает это исключение? 12. Что такое ссылка на метод? . . . 13. При вычислении ссылки на метод создается экземпляр ставляемого целевым контекстом. , предо- 14. Пусть имеется класс по имени MyClass, который содержит статический метод по имени myStaticMethod(). Покажите , как указать ссылку на метод myStaticMethod(). 15 Пусть имеется класс по имени MyClass, который содержит метод экзем пляра по имени mylnstMethod ( ) , а также объект типа MyClass по име ни mcObj. Покажите , как указать ссылку на метод mylnstMethod ( ) для mcObj. 16 В программе MethodRefDemo2 добавьте в класс MylntNum новый метод по имени hasCommonFactor ( ) . Пусть он возвращает true, если его аргумент типа int и значение , хранящееся в вызывающем объекте MylntNum, имеют хотя бы один общий делитель. Например , числа 9 и 12 имеют общий делитель, равный 3, а числа 9 и 16 не имеют общего делителя . Продемонстрируйте работу hasCommonFactor ( ) через ссылку на метод. . . . 18. 17 Как указывается ссылка на конструктор? В каком пакете Java находятся предопределенные функциональные интерфейсы ? * . ' л 'Л * s vMW. • . I VWv .• •/‘Л s•..*• | V I, i II II, I, I I 1 . II 4 ' I ч % .. . •Н»oo s" IN , < * >4 4 > " *<• 1 i " Глава 15 Модули 558 Java : руководство для начинающих, 9-е издание В этой главе • • • z Определение модуля z Ключевые слова Java , связанные с модулями z Объявление модуля с использованием ключевого слова module z Применение requires и exports z Назначение module - info , java z Использование javac и java для компиляции и запуска программ, основанных на модулях z Назначение java . base z Поддержка унаследованного кода , написанного до появления модулей z Экспортирование пакета для указанного модуля z Использование подразумеваемой читаемости z Использование служб в модуле в версии JDK 9 появилось новое и важное средство , называемое модулями. Модули предоставляют способ описания отношений и зависимостей кода , из которого состоит приложение. Модули также позволяют контролировать то , какие части модуля доступны другим модулям, а какие нет. За счет использования модулей можно создавать более надежные и масштабируемые программы. Как правило , модули наиболее полезны в крупных приложениях, поскольку они помогают сократить сложность управления, часто связанную с большой программной системой. Однако мелкие программы тоже выигрывают от при менения модулей , потому что библиотека Java API теперь организована в виде модулей. Таким образом , теперь можно указывать, какие части Java API тре буются вашей программе , а какие не нужны. Это позволяет развертывать программы с меньшим объемом пространства хранения , используемого во время выполнения, что особенно важно при создании кода, например, для небольших устройств, входящих в состав IoT ( Internet of Things Интернет вещей ). — Глава 1 5. Модули 559 Поддержка модулей обеспечивается как языковыми элементами, в том числе несколькими ключевыми словами , так и улучшениями javac , java и других инструментов JDK. Кроме того , были предложены новые инструменты и форматы файлов. В результате JDK и исполняющая среда были существенно модернизированы с целью поддержки модулей. Короче говоря , модули являются важным дополнением и эволюционным шагом языка Java. Основы модулей В наиболее основополагающем смысле модуль представляет собой группу пакетов и ресурсов, на которые можно коллективно ссылаться по имени модуля . В объявлении модуля указывается имя модуля и определяется отношение модуля и его пакетов с другими модулями. Объявления модулей записываются в виде операторов в файле исходного кода Java и поддерживаются несколькими клю чевыми словами, связанными с модулями, которые были добавлены в JDK 9: exports provides module requires uses with open to opens transitive Важно понимать, что перечисленные выше ключевые слова распознаются как ключевые слова только в контексте объявления модуля. В других ситуациях они интерпретируются как идентификаторы. Таким образом , ключевое слово module можно было бы использовать в качестве имени параметра , хотя поступать так, безусловно, не рекомендуется . Объявление модуля содержится в файле по имени module- info , java , т.е . модуль определяется в файле исходного кода Java. Файл module- info , java за тем компилируется с помощью javac в файл класса и известен как его дескрип тор модуля. Файл module - info . java должен содержать только определение модуля, но не другие виды объявлений. Он не является файлом общего назначения. Объявление модуля начинается с ключевого слова module. Вот его общая форма: module имя-модуля { // определение модуля } Имя модуля указывается в конструкции имя-модуля и должно быть допусти мым идентификатором Java или последовательностью идентификаторов, разделенных точками. Определение модуля находится в фигурных скобках. Хотя определение модуля может быть пустым (что приводит к объявлению , которое просто именует модуль) , обычно в нем присутствует одна или несколько конструкций, устанавливающих характеристики модуля. 560 Java : руководство для начинающих, 9-е издание !?@>A8< C M:A?5@B0 ВОПРОС . Почему ключевые слова , связанные с модулями , вроде module и requires распознаются как ключевые слова только в контексте объявления модуля? ОТВЕТ . Ограничение их использования в качестве ключевых слов только объяв лением модуля предотвращает возникновение проблем с ранее написанным кодом , в котором одно или несколько из них применялись для идентификаторов. Например , рассмотрим ситуацию , когда в программе , предшествующей JDK 9 , для имени переменной используется requires. После переноса программы в среду с современной версией Java распознавание requires в качестве ключевого слова где -то за пределами объявления модуля приведет к тому, что возникнет ошибка на этапе компиляции . За счет распознавания requires как ключевого слова только внутри объявления модуля любые другие случаи применения requires в программе остаются незатронутыми и допустимыми . Разумеется , то же самое относится и к другим ключевым словам , связанным с модулями . Простой пример модуля В основе возможностей модуля лежат две ключевые особенности . Первая из них — способность модуля сообщать о том , что ему требуется другой модуль. Другими словами , один модуль может указывать, что он зависит от другого модуля . Отношение зависимости задается с помощью оператора requires . По умолчанию наличие необходимого модуля проверяется как на этапе компи ляции , так и во время выполнения. Второй ключевой особенностью является способность модуля контролировать , какие из его пакетов доступны другому модулю , что достигается с помощью ключевого слова exports. Открытые и за щищенные типы внутри пакета доступны другим модулям только в том случае , если они явно экспортированы . Здесь мы займемся примером , в котором де монстрируются обе возможности. В следующем примере создается модульное приложение , в котором задей ствованы простые математические функции . Хотя это приложение специально сделано очень маленьким , оно иллюстрирует основные концепции и процеду ры , необходимые для создания , компиляции и запуска кода на основе моду лей . Вдобавок показанный здесь общий подход применим и к более крупным , реальным приложениям . Настоятельно рекомендуется проработать пример на своем компьютере , внимательно следуя каждому шагу. Глава 15. Модули 561 На заметку! В главе описан процесс создания, компиляции и запуска кода на основе модулей с помощью инструментов командной строки Такой прием обладает двумя преимуществами Во -первых, он подойдет всем программистам на Java, потому что не требует IDE-среды. Во-вторых, он очень четко иллюстрирует основы системы модулей, в том числе ее работу с каталогами. Вам придется вручную создать несколько каталогов и обеспечить размещение каждого файла в надлежащем каталоге. Как и следовало ожидать, при создании реальных приложений на основе модулей вы, по всей видимости, обнаружите, что легче пользоваться IDE-средой с поддержкой модулей, поскольку обычно она автоматизирует большую часть процесса Однако изучение основ модулей с применением инструментов командной строки гарантирует, что вы обретете устойчивое понимание темы. . . . В приложении определены два модуля. Первый модуль имеет имя appstart и содержит пакет appstart.mymodappdemo, устанавливающий точку входа приложения в классе MyModAppDemo. Таким образом , MyModAppDemo содержит метод main ( ) приложения. Второй модуль называется appfuncs и содержит пакет appfuncs.simplefuncs, который включает класс SimpleMathFuncs. В классе SimpleMathFuncs определены три статических метода , реализующие ряд простых математических функций. Все приложение будет размещаться в дереве каталогов, которое начинается с mymodapp. Прежде чем продолжить, уместно