Текст
                    Изучаем
Java
Погружение
в реальное
программирование
Полезное руководство для мозга


Head First Java A Brain-Friendly Guide 3rd Edition
Изучаем Java Погружение в реальное программирование 3-е издание
«Изучаем Java» — это не просто книга. Она не только научит вас теории языка Java и объектно-ориентированного программирования, она сделает вас программистом. В ее основу положен уникальный метод обучения на практике. В отличие от классических учебников информация дается не в текстовом, а в визуальном представлении. Вы осво¬ ите в деле все самое нужное: синтаксис и концепции языка, работу с потоками, работу в сети, распределенное программирование. Вся теория закрепляется интереснейшими примерами и тестами.
От Кэти и Берта: Нашему мозгу — за то, что всегда с нами! (несмотря на шаткие доказательства) От Триши: Мере — за то, что всегда рядом (при избытке доказательств) Вот бы существовала книга по Java, более увлекательная, чем ожидание в очереди на продление водительских прав! Это что-то из области фантастики...
Отзывы к третьему изданию книги «Какая увлекательная и необычная книга! Я много лет преподаю Java и могу честно сказать, что это самый захватывающий ресурс по обучению программированию, с которым я когда-либо сталкивалась. Он вдохновляет меня снова погрузиться в изучение Java». — Энджи Джонс, Java Champion «В этой книге самое понятное объяснение потоков данных и лямбда-функций в Java, которое я когда-либо читал, — без лишнего шума и пафоса! Функциональное программирование подается с юмором и отличным стилем. Мне снова захотелось изучать Java! Если бы все писали код так же понятно, как это объясняют авторы...» — Эрик Норманд, преподаватель Clojure, автор книги Grokking Simplicity «Как же я хотел, чтобы такая книга попалась мне в руки, когда я только начинал изучать Java! Ее так весело читать, что порой забываешь, что перед тобой серьезный учебник. Третье изда¬ ние — огромный шаг вперед. Здесь есть все, что нужно современному Java-разработчику, чтобы стать профи. А иллюстрации — просто шедевр! Я не раз смеялся вслух. Большое спасибо авто- рам-чемпионам Java: Кэти, Берту и Трише!» — доктор Хайнц Кабутц, The Java Specialists’ Newsletter, www.javaspecialists.eu «Обожаю стиль преподавания Head First Java! Это техническая книга, но читается, как художе¬ ственная литература — невозможно оторваться. Здесь оригинальные иллюстрации, меткие ана¬ логии и забавные диалоги между разработчиком и компилятором. Но главное — авторы помога¬ ют читателю взглянуть на программирование по-новому, а это бесценно. Авторы — настоящие волшебники. Рекомендую каждому, кто хочет изучить Java или прокачать свои навыки!» — Мала Гупта, Developer Advocate компании JetBrains, автор и Java Champion «Новички в Java часто спрашивают меня, с какой книги лучше начинать. Я всегда отвечаю: с Head First Java! Первые издания от Кэти Сьерры и Берта Бейтса перевернули подход к об¬ учению программированию, сделав его максимально ориентированным на ученика. Это была настоящая революция. А теперь с ними еще и Триша Джи — один из лучших Java-инженеров и преподавателей, мой личный гуру в сложных вопросах Java! Третье издание не просто вернуло меня в прошлое, оно снова научило меня новому, даже после 20 лет работы с языком!» — Мартейн Вербург, «дьявольский разработчик», Java Champion, Principal Group Manager no Java в Microsoft «Head First Java — это уже легенда, и сложно представить лучшего человека для ее обновления, чем Триша Джи. Я всегда знала, что она крутая, но теперь поняла, что она еще и невероятно смешная! Третье издание получилось одновременно авторитетным, актуальным и безумно увле¬ кательным учебником. Если существует лучший способ изучить Java, я о нем не слышала!» — Холли Камминс, старший инженер Quarkus в Red Hat «Эта книга — просто фантастика! В ней целая куча фигурных скобок, остроумных шуток, объ¬ ектов, метафор, синтаксиса, живого кода и искреннего уважения к читателю. Авторы сделали изучение Java серьезным, но при этом увлекательным и запоминающимся. Особенно мне понра¬ вились метакогнитивные подсказки, приглашения почувствовать себя компилятором, истории, яркие визуализации и общее ощущение, что программирование открыто и доступно каждому».
«Как же мне хотелось бы узнать про эту книгу, когда я только начинала изучать Java! Если вы хотите освоить Java легко, с юмором и увлечением (кто бы мог подумать, что это вообще воз¬ можно?), особенно если у вас нет технического образования, это идеальный выбор. Никогда не думала, что буду смеяться над учебником по программированию. Книга написана блестяще, остроумно и увлекательно, а главное — очень полезно!» — Грейс Янсен, Developer Advocate, IBM «Когда начинаешь изучать Java, перед тобой море книг и курсов. Проблема лишь в том, что большинство из них перегружено техническими деталями, и ты постоянно думаешь: “Когда же все это закончится?” Head First Java идет совершенно другим путем и превращает учебу в увле¬ кательное путешествие. Ребусы с собаками, загадки и игры с карандашом делают обучение не только интересным, но и помогают запомнить важные моменты. Жаль, что такой книги не было, когда я начинал!» — Саймон Риттер, заместитель технического директора в Azul и Java Champion «Эта книга всегда вызывает у меня приятные воспоминания. Я впервые читал ее еще в универ¬ ситете, а теперь очень рад видеть обновленное издание. Head First Java — простая и понятная книга, с примерами кода, которые легко усваиваются. Рекомендую всем Java-разработчикам!» — Нельсон Джало, технический предприниматель, основатель платформы Amigoscode.com и канала Amigoscode на YouTube «Head First Java — это книга, которую я дал своему сыну, когда он решил изучать Java. Почему именно ее? Я знал, что веселые иллюстрации захватят его внимание лучше, чем любая другая книга по Java. Изучение Java с этой книгой больше похоже на игру на детской площадке, чем на занятия в классе». — Кевин Нильсон, инженер-программист Google и руководитель Silicon Valley Java User Group «Я завидую тем программистам, которые сегодня начинают изучать Java: у них есть эта по¬ трясающая книга. Когда я учил Java двадцать лет назад, все было совсем не так увлекательно. Но с этой книгой точно скучать не придется! Я еще не встречал учебника, где Java-компилятор сражается с виртуальной машиной. Это по-настоящему круто!» — Тагир Валеев, Java Champion и Technical Lead IntelliJ IDEA в JetBrains «Почти 20 лет назад я прочитал первое издание Head First Java, когда еще был начинающим раз¬ работчиком. И вот третье издание снова приятно удивляет! За это время многое изменилось, в том числе и способы подачи материала. Head First Java — отличный современный учебник, который помогает быстро усвоить ключевые концепции без лишних деталей и без упрощений, а также предлагает материалы для дальнейшего изучения. Особенно хороши разделы про по¬ токи данных, многозадачность и NIO». — Майкл Симонс, Java Champion, инженер Neo4j, автор немецкого справочника Spring Boot Buch «Если вы хотите разрабатывать на Java, эта книга — именно то, что нужно. Head First Java пред¬ лагает простые и элегантные примеры, которые ясно показывают, как применять Java на практи¬ ке. Отличный выбор для тех, кто начинает свой путь Java-разработчика». — Саньхун Ли, Alibaba Cloud
«Head First Java — техническая книга, которая совсем не похожа на техническую книгу. Она веселая, непринужденная и наполнена жизненными аналогиями, которые помогают легко разобраться даже в самых сложных темах. Это идеальное знакомство с богатой и многообразной экосистемой Java». — Абрахам Марин-Перес, главный консультант команды Cosota «Если вам нравится легкость и юмор в обучении, то лучшей книги для изучения Java, чем третье издание Head First Java, я даже представить не могу. Практичная и при этом игривая, познавательная и увле¬ кательная — это идеальный учебник для начинающих разработчиков, которые хотят сразу перейти от теории к практике». — Марк Лой, тренер, автор книги Smaller С, соавтор книг Learning Java и Java Swing Положительные отзывы о предыдущих изданиях, а также о других книгах, написанных Кэти и Бертом в соавторстве «Это не обычный учебник, а нечто похожее на дружелюбный интерфейс: благодаря легкой и остроумной подаче авторов изучение Java становится увлекательным путешествием, полным ожидания “а что же дальше?”» — Уоррен Койфель, Software Development Magazine «Единственный способ понять, насколько учебник хорош — это проверить, как он учит. Head First Java справляется с этим блестяще. Поначалу книга казалась мне слишком несерьезной, но вскоре я осознал, что именно в таком стиле я глубоко и основательно усваиваю материал. Подход Head First Java действи¬ тельно делает учебу проще». — slashdot (отзыв пользователя honestpuck) «Помимо живого и увлекательного стиля, который быстро превращает вас из новичка в настоящего Java-воина, Head First Java подробно раскрывает многие практические темы, которые другие книги обычно оставляют читателю «для самостоятельного изучения». Это умно, иронично, стильно — и, глав¬ ное, полезно. Не каждый учебник сумеет в таком формате обучить сериализации объектов и сетевым протоколам». — доктор Дэн Рассел, директор по исследованиям пользовательского опыта в IBM Almaden Research Center и преподаватель искусственного интеллекта в Стэнфорде «Она быстрая, дерзкая и захватывающая. Осторожно: вы можете действительно чему-то научиться!» — Кен Арнольд, бывший старший инженер Sun Microsystems, соавтор (с Джеймсом Гослингом, создате¬ лем Java) книги 'Ihe Java Programming Language «Технология Java сегодня повсюду. Если вы занимаетесь разработкой ПО и еще не изучили Java, пришло время начинать — и начинать именно с Head First!» — Скотт Макнили, бывший председатель совета директоров, президент и CEO Sun Microsystems «Head First Java — это как встреча Монти Пайтона и «банды четырех». Материал идеально разбит на головоломки, истории, вопросники и примеры. Благодаря им вы освоите Java гораздо быстрее, чем с любым другим учебником по программированию».
«Прочитав Head First Java, вы снова почувствуете, что учеба может быть увлекательной. Если вы хотите изучить Java, но у вас нет технического бэкграунда, эта книга станет настоящей находкой. Она превращает освоение сложного языка в увлекательный и интересный процесс». — Джудит Тейлор, Southeast Ohio Macromedia User Group «Если хотите освоить Java, прекращайте поиски. Это первая техническая книга с графическим интерфейсом! Уникальный формат дает преимущества, которых вы не найдете ни в одной дру¬ гой книге по Java. Приготовьтесь к захватывающему путешествию в мир Java». — Нил Р. Бауман, капитан и CEO в компании Geek Cruises «Я просто не мог оторваться от коротких историй, кода с выносками, интервью и головоломок». — Майкл Юань, автор книги Enterprise J2ME «Head First Java придает новый смысл фразе «Для этого есть O’Reilly». Я взял эту книгу, потому что о ней говорили как о революционной, и она полностью оправдала мои ожидания. Это весело, дерзко, актуально и интерактивно. Читая ее, вы словно учитесь и смеетесь вместе с друзьями. Если хотите по-настоящему понять Java — эта книга для вас». — Эндрю Поллак, www.thenorth.com «Эта книга просто потрясающая! Я в шоке». — Флойд Джонс, старший технический писатель, ВЕА «Я чувствую, что с меня будто сняли тяжесть тысячи других книг». — Уорд Каннингем, изобретатель Wiki и основатель Hillside Group «Я смеялся, плакал, и книга меня действительно зацепила». — Дэн Стейнберг, главный редактор java.net «Сначала я просто катался от смеха. Затем понял, что книга не только точна, но и великолепно объясняет сложные темы, такие как паттерны проектирования». — доктор Тимоти А. Бадд, доцент кафедры информатики Орегонского государственного универ¬ ситета, автор книги C++ for Java Programmers «Идеальный учебник для тех, кто не любит скучные лекции. Книга поможет вам понять, как решать задачи на практике, — без лишних сложностей и занудства». — Трэвис Каланик, основатель Scour и Red Swoosh, член MIT TR100
Авторы книг из серии Head First Кэти увлеклась теорией обучения еще во време¬ на, когда разрабатывала игровые приложения для Virgin, MGM и Amblin’ Позже она вела курс «Раз¬ работка новых подходов в сфере распространения информации» в Калифорнийском университете в Лос-Анджелесе, а также была главным тренером Java в Sun Microsystems. Кроме того, Кэти осно¬ вала сайт JavaRanch.com (сейчас он известен как CodeRanch.com), который дважды получал награду Jolt Cola Productivity (в 2003 и 2004 годах). В 2015 году Electronic Frontier Foundation вручила ей премию Pioneer Award за вклад в обучение пользо¬ вателей и создание устойчивых сообществ. В последнее время Кэти занимается изучением передовых технологий и науки о движении и при¬ обретении навыков, известной как экологическая динамика (Eco-D). Ее работа по применению подхода Eco-D для тренировки лошадей открыла намного более гуманный метод верховой езды, который одних приводит в восторг, а других — в за¬ мешательство (а порой и в ужас!). Лошади, которых тренируют с помощью подхода Кэти, становятся более счастливыми, здоровыми и спортивными по сравнению со своими собратьями, обучаемыми традиционными способами. До того, как заняться писательством, Берт работал разработчиком и специализировался на старых подходах к искусственному ин¬ теллекту (в основном экспертных системах), операционных системах реального времени и сложных системах планирования. В 2003 году Берт вместе с Кэти написал Head First Java, с чего началась вся серия книг Head First. С тех пор он выпустил множество книг по Java, консультировал Sun Microsystems и Oracle по вопросам сертификации Java, а сейчас помогает тренерам, преподавателям, профессорам, авторам и редакторам созда¬ вать первоклассные учебные материалы для студентов. Берт увлекается игрой в го. В 2016 году он с ужасом и восторгом одновременно наблюдал за тем, как искусственный интеллект AlphaGo разгромил знаменитого игрока Ли Седоля. В последнее время Берт активно применя¬ ет экологическую динамику (Eco-D), чтобы улучшить свою игру в гольф и даже научить кое-чему своего попугайчика по имени Боке. По счастливой случайности Берт знает Тришу Джи больше восьми лет. Триша недавно при¬ соединилась к авторам серии Head First, сделав книги серии еще более интересными и полез¬ ными.
Соавтор третьего издания этой книги Триша начала работать с Java еще в 1997 году, когда ее университет одним из первых внедрил этот «новенький» язык в программу преподавания ин¬ форматики. С тех пор она была и разработчиком, и консультантом, создавая Java-приложения для самых разных сфер: от банковского дела и производства до некоммерческих организаций и высокоскоростной финансовой торговли. Триша всегда любила делиться опытом, который накопила за годы работы, и поэтому решила стать разработчиком-адвокатом. Теперь она пишет бло¬ ги, выступает на конференциях и создает обучающие видео. Пять лет Триша проработала Java-разработчиком-адвокатом в JetBrains, а затем еще два года руководила там командой Java-адвокации. Благодаря этой деятельности она узнала о реальных проблемах, с которыми ежедневно сталкиваются Java- разработчики. Триша и Берт долго обсуждали идею обновления Head First Java. Она с боль¬ шой теплотой вспоминает их регулярные звонки: общение с таким опытным и душевным человеком, как Берт, всегда помогало ей сохранять спокойствие и оптимизм. Подход Берта и Кэти к обучению стал важным ориентиром и вдохновением в ее собственной работе последние десять лет.
Содержание (краткое) Введение 23 I Ныряем: быстрый заплыв 37 2. Классы и объекты 63 3 Примитивы и ссылки 85 4 Повадки объектов в Java 107 5 Разработка программы 131 6 все что вы хотели знать о Java API 161 7 Наследование и полиморфизм 203 8 Интерфейсы и абстрактные классы 235 9 Конструкторы и сборка мусора 273 10 Числа и статические члены 311 II Коллекции и обобщенные типы 345 12 Лямбда-выражения и потоки данных 405 13 Обработка исключений 457 14 Создание графического интерфейса 497 15 Библиотека Swing 545 16 Сериализация и ввод вывод файлов 575 17 Сетевое взаимодействие и потоки выполнения 623 18. Состояние гонки и неизменяемые данные 675 Приложение А: Готовый код 709 Приложение Б: Одиннадцать тем, которые не попали в основную часть книги... 719 Содержание Введение Ваш МОЗГ на Java. Вы решили изучить Java, а ваш мозг в этот момент пытается вам помочь и решает: «Это запоминать не стоит! Гораздо важнее помнить, какие животные могут вас съесть и можно ли кататься на сноуборде голым». Так как же убедить свой мозг, что от знания Java зависит ваша жизнь? Как пользоваться книгой 23 Для кого эта книга? 24 Кому не подойдет эта книга? 24 Мы знаем, о чем вы думаете 25 И мы знаем, о чем думает ваш мозг 25 Метапознание: мышление о мышлении 27 Вруби мозг на всю катушку! 29 И не надейтесь, это еще не все! 30
Погружение в Java Java открывает перед вами НОВЫЙ ДИВНЫЙ мир. С момента появления первой версии 1.02 Java мгновенно захватила разработчиков своей простотой, объектно- ориентированными возможностями, автоматическим управлением памятью и — самое крутое — обещанием работать везде. Уже сейчас вы быстро напишете код, скомпилируете его и запустите. В этой главе вас ждут основы синтаксиса, циклы, условные операторы и ответ на вопрос «почему Java настолько крутая?». Погнали погружаться! Виртуальные машины Method Раг|у() 0 aload_0 1 jnvokespecial # «Method java.lang. Object()> 4 return Вывод (код) г Прорываясь сквозь поверхность Как работает Java Что вы будете делать на Java Кратчайшая история Java Структура кода в Java Структура класса Написание класса с методом main() Что поместить внутри основного метода? Зацикливай,и зацикливай, и... Простые логические проверки Условное ветвление Разработка серьезного бизнес-приложения Одно буднее утро в доме Боба, где правит Java Фразоплет 3000 Путеводитель по объектам 37 38 39 40 43 44 45 48 49 49 51 52 53 55 ГОВОРЯТ/ ЗДвСЬ будут объекты. В первой главе весь код мы поместили в метод main(), но это еще не объектно-ориентированное программирование. Настало время выйти за рамки процедурных функций и создать свои собственные объекты! Мы разберемся, почему объектно-ориентированное программирование такое увлекательное, узнаем, в чем разница между классом и объектом, и как объекты могут сделать вашу жизнь лучше. Путешествие в Объектино Битва за кресло Лора на мгновение опередила Брэда Итак, Брэд, сторонник ООП, получил кресло и стол, да? А как же метод rotateQ у Amoeba? Интрига меня уже в могилу загоняет! Кто тут главный по мебели? Проектируя класс, не забывайте о создаваемых объектах: Различия между классом и объектом Класс — это не объект Создайте свой первый объект Создание и тестирование объектов Movie Быстрее! Валим из main! 63 64 66 67 68 69 69 70 71 71 72 73 74
^ Разбираемся с переменными Не все СВОДИТСЯ К числам И строкам. Переменные бывают примитивные и ссылочные. Достаточно ли для жизни только чисел, строк и массивов? А что делать, если у вас есть объект PetOwner, у которого живет Dog? Или автомобиль с объектом Engine? В этой главе поговорим о типах данных, выясним их отличия и узнаем, какие данные можно хранить в переменных. А еще выясним, каково жить на куче с автомати¬ ческой сборкой мусора. Знай свои переменные Объявление переменной Мне, пожалуйста, двойной эспрессо... Нет, лучше возьму целое Хотите точно вместить значение в переменную? Избегайте этого ключевого слова! Управление объектом Dog Жизнь в куче Жизнь и смерть в куче Массив как поднос со стаканчиками Массивы — тоже объекты Создание массива Dog Управляйте объектом Dog Что происходит, если ссылка на Dog находится в массиве Dog? Пример класса Dog Как ведут себя объекты 85 86 87 88 89 90 93 94 95 95 96 97 97 98 Поведение зависит от состояния, а состояние — от поведения. Мы уже знаем, что у объектов есть состояние (переменные экземпляра) и поведение (методы). Но теперь разберемся, как эти две вещи связаны. Поведение объекта зависит от его состояния: например, «если вес собаки меньше 14 фунтов, она лает, а если боль¬ ше — рычит». Пришло время поменять пару вещей и посмотреть, что получится! Передача по значению означает передачу через копирование foo.go(x); void go(int z){ } Повадки объектов в Java 107 Помните: класс описывает, что объект знает и делает 108 Громкость лая пропорциональна размеру 109 Вы можете передавать методу разные значения 110 Значения из метода можно возвращать 111 Методу можно передать сразу несколько значений. 112 В Java передача осуществляется по значению. Это значит, что передается копия 113 Применение параметров и возвращаемых значений 115 Инкапсуляция 116 Как себя ведут объекты в массиве 119 Объявление и инициализация переменных экземпляра 120 Разница между переменными экземпляра и локальными переменными 121 Сравниваем переменные (примитивы или ссылки) 122
„Й t\ tut m|\t u.« Сс|лса ki| cj.,AA cmo|vmai\rt Сверхмощные методы Прокачайте СВОИ методы. Вы уже чуть-чуть поиграли с переменными, объектами и написали немного кода. Но для чего-то большего нужны инструменты посерьезнее: циклы, операторы и, может быть, случайные числа? А что, если строка вдруг захочет стать числом? Согласитесь, полезная фишка! В этой главе вы научитесь всему этому на примере настоящей программы, а заодно создадите игру типа «Битва за рынок» (что-то вроде «Морского боя»). 6 «Приятно знать, что в пакете java.util есть ArrayList. Но как бы я сама о этого додума¬ лась?» - Джулия, 31 год, модель Особо мощные методы 131 Создадим игру в стиле «Морского боя»: «Битва за рынок» 132 Во-первых, высокоуровневое проектирование 133 Плавное введение в игру «Битва за рынок. Простая версия». 134 Разработка класса 135 Пишем реализацию метода 137 Разработка тестового кода для класса SimpleStartup 138 Тестовый код для класса SimpleStartup 139 Метод checkYourse!f() 140 Объясняем новое 141 Финальный код для SimpleStartup и SimpleStartupTestDrive 142 Полная версия игрового взаимодействия 143 Псевдокод для класса SimpleStartupGame 144 Метод main() игры 146 Методы random() и getUserlnput() 147 Последний класс: GameHelper 148 Сыграем? 149 Что это? Ошибка? 149 Подробнее о циклах for 150 Обычные (не улучшенные) циклы for 150 Прохождение цикла 151 Улучшенный цикл for 152 Приведение примитивов 153 Используем библиотеку Java Зачем изобретать велосипед? Java идет в комплекте с сотнями готовых классов. Зачем создавать с нуля то, что уже сделали за вас? У вас есть дела поважнее. Ваша задача — писать только тот код, который уникален для вашего приложения. Библиотека Java (Java API) — огромный склад классов, готовых к работе. Использование библиотеки Java В предыдущей главе мы оставили небольшую интригу, а именно — ошибку Как это должно выглядеть Где закралась ошибка Что же случилось? Давайте пофиксим? Первый вариант слишком сложный Второй вариант лучше, но пока не идеален Очнитесь и почувствуйте дух библиотеки Что можно сделать с помощью ArrayList Сравнение ArrayList с обычным массивом 161 162 162 162 163 164 165 165 168 169 173 Давайте фиксить класс StartUp Битва за рынок. Полная версия Что нужно изменить? За что (и когда) отвечает каждый элемент в игре StartupBust Псевдокод для настоящего класса StartupBust Финальная версия класса Startup Супермощные логические выражения Продолжение класса GameHelper... Использование библиотеки (Java API) Необходимо знать полное имя’ класса, который вы хотите использовать в своем коде. Как работать с API Чтг>иitр nmruupuTaffuu v vnarrv 174 176 177 178 180 186 187 189 190 191 194 юя
^ Жизнь в Объектино Пишите программы С МЫСЛЯМИ О будущем. Что, если бы ваш код можно было легко расширить другим разработчикам? А что, если бы код был гибким и легко адаптировался к изменениям? В этой главе мы разберем пять шагов к хорошему проектированию классов, три способа полиморфизма и восемь фишек для гибкости кода. А если поторопитесь, получите в подарок четыре совета по наследованию. Вбить 8 голову (Розы красные Стихи неровные Передать по значению — Это как копии условные. Ну конечно, можете подумать, что у вас получит сялучше! А что, может и правда так?Попробуйте' Вторая строчка у нас, скажем честно, вышла не ' J очень—замените ее на свою. А еще лучше — при- | думайте свой собственный стих. Так вы точно I запомните зто навсегда■ 8 Прекрасная жизнь | в Объектино 203 Проектирование иерархии наследования 214 1 И снова битва за кресло... 204 Отношения IS-А и HAS-A 215 1 А что насчет метода rotate() Но погодите! Это еще не все! 216 фигуры Amoeba? 205 Как узнать, что наследование Принципы наследования 206 оформлено правильно 217 Пример наследования: 207 Наследование при Построим иерархию наследования на примере проектировании — хорошо или плохо? 219 симулятора животных 208 В чем же настоящая Наследование для предотвра- ценность наследования? 220 щения дублирования кода в дочерних классах 209 Наследование гарантирует, что все классы-потомки Все ли животные питаются одинаково? 210 содержат методы родительского типа* 221 Ищем новые возможности наследования 211 Заключение контракта: правила переопределения 228 Какой метод вызывается? 213 Перегрузка метода 229 Серьезный полиморфизм Наследование — ТОЛЬКО начало. Если хотите стать гуру полиморфизма, придется разобраться с интерфейсами. Зачем? Они дают гибкость, недостижимую при обычном наследовании. Что такое интерфейс? Это полностью абстрактный класс, который невозможно создать напрямую. Зачем он тогда нужен? Читайте дальше и узнаете! Серьезный полиморфизм 234 Мы что-то забыли, когда разрабатывали это? 236 А как выглядит объект newAnimalO? 238 Компилятор не позволит вам создать экземпляр для абстрактного класса 239 Различия абстрактных и конкретных классов 240 Абстрактные методы 241 Вы обязаны реализовывать все абстрактные методы 242 Абстрактные и конкретные классы 243 Полиморфизм в действ ии 244 Эх! Теперь нам нужно хранить еще и кошек 245 А как быть с не-животными? Вот бы сделать универсальный класс, принимающий любые объекты? 246 Что же спрятано внутри этого Использование полиморфических ссылок типа Object имеет свою цену... 249 Когда собака перестаетбыть собакой... 250 Объекты не лают 251 Ощутите свой внутренний объект 252 Полиморфизм означает «много форм» 253 Что делать, если нужно изменить контракт 256 Варианты повторного использования существующих классов в программе для зоомагазина 257 Интерфейсспешитнапомощь! 262 Создание и реализация интерфейса Pet 263 Классы из разных иерархий насле¬ дования могут реализовывать один и тот же интерфейс 264 Вызов версии метода из Пппнтапс/ч/пгп 1/пЦ,
/ Жизнь и смерть объекта Объекты появляются и исчезают. Вы ответственны за их появление и исчезновение. Вы решаете, когда и как создавать объекты, а также когда наступает момент с ними «попрощаться». Память освободит сборщик мусора (дс), но как объекты попадают на кучу и когда их можно отпускать? Поговорим о куче, стеке, области видимости, конструкторах, суперконструкторах, ссылках на null и о том, когда объект становится кандидатом на сборку мусора. 'fyeiaa ктс-me выбывает ..uurvuf |}г0. этет огиктУ'ягк uervewn. (м миист&нкал d гутс&габястся небкй сс/ккт. duck, ocmatUsw иглкшмй «гикт du/k jadjycwt н н ми %<?«« duck cnptt. 10 Жизнь и смерть объектов 272 Стек и куча: где все хранится 274 Методы в стеке 275 А как же быть с локальными переменными, представляющими собой объекты? 276 Если локальные переменные в стеке, то где переменные экземпляра? 277 Чудо создания объекта 278 Мы действительно вызываем метод DuckQ? Потому что выглядит это именно так 279 Конструируем объект duck 280 Инициализация состояния нового объекта Duck 281 Используем конструктор, чтобы задать важное начальное состоя¬ ние объекта Duck’ 282 Упрощаем процесс создания объекта Duck 283 Всегда ли компилятор создает кон¬ структор без аргументов? Нет! 284 Наличие перегруженных конструк¬ торов означает, что в вашем классе несколько конструкторов 285 Экспресс-обзор: четыре главных факта о конструкторах 287 Погодите-ка... мы ведь так и не обсудили суперклассы, наследование и их связь с конструкторами! 288 Роль конструкторов суперкласса в жизни объекта 289 Создавая объект Hippo, вы наследуете свойства и методы классов Animal и Object 290 Как вызвать конструктор суперкласса? 291 Могут ли потомки появиться раньше родителей? 292 Конструкторы суперкласса с аргументами 293 Вызов одного перегруженного конструктора из другого 294 Мы выяснили, как появляется объект, а сколько он существует? 296 По поводу ссылочных переменных 298 Статические пере¬ менные являются общими для всех экземпляров класса. static vavafid: inCvum . kid lad irutmv? cm Важность чисел Порд немНОГО ПОСЧИТать! В API Java есть множество удобных методов для работы с числами: абсолютные значения, округление, максимумы и минимумы. А как насчет форматирования? Иногда нужно вывести число с двумя знаками после запятой или расставить запятые в нужных местах. А даты? Или конвертация строки в число и наоборот? В этой главе мы начнем с изучения статических методов и переменных. inslanct /лк Вез чисел никуда 311 Java упакует примитивы где угодно Методы Math: почти как (ну, почти) 328 глобальные 312 Постойте! Это еще не все! two Разница между обычными Обертки также содержат и статическими методами 313 статические служебные методы! 330 Насчет классов со статическими 314 А теперь наоборот, превращаем instance методами Статическим методам недоступны простое числовое значение в строку 331 мтмШл: переменные экземпляра! 315 Форматирование чисел 332 Статические методы не могут обра- 316 Форматирование под ( Ш j\CI щаться к нестатическим методам! микроскопом... 333 Статическая переменная: одно Знак процента (%) сообщает: in,status и то же значение для всех «Вставьтеаргумент сюда» экземпляров класса 317 (и отформатируйте его [static Инициализация статической по указанной инструкции) 334 переменной 319 В строке форматирования 1 tmiaddy. Статические финальные 320 используется свой простой переменные = константы синтаксис 335 m fin Модификатор final подходит Спецификатор форматирования 336 не только для статических Единственный обязательный fcdiss переменных... 321 спецификатор — это тип 337 Методы Math 324 Что случится, если использовать Обертка для примитивов 326 несколько аргументов? 338
7 1 I Структуры данных Java любит порядок! Сортировка данных в Java проще простого. У вас уже есть инструменты, чтобы собирать и обрабатывать данные, не изобретая алгоритмы самостоятельно. Java Collections Framework предоставляет структуры данных под любые задачи: списки с легким добавлением элементов, быстрый поиск по имени или коллекции, автоматически удаляющие дубликаты. Вы даже сможете отсортировать коллег по числу ударов ножом в спину — выбор за вами! Li st S«t lap /2 Структуры данных 345 Возвращаемся к методу sort() 363 API коллекций (часть) 382 Отслеживание популярности пе¬ В обобщенных типах extends Замена ArrayList на HashSet 383 сен в музыкальном автомате 346 означает не только «расширяет», Определение равенства двух Первое задание — отсортиро¬ но и «реализует» 364 объектов 384 вать песни в алфавитном порядке 347 Наконец-то мы знаем, что не так. .* 365 Как HashSet проверяет нали¬ Отличный вопрос! Это ромбовид¬ Новая,улучшенная и сравнивае¬ чие дубликатов: методы hashCode() ный оператор 348 мая версия класса Song 366 и equals() 385 В «реальном мире»™ сортиров¬ Список-то сортируется, но... 367 Класс Song с переопределенны¬ ка бывает разной 350 Работа пользовательского ми методами hashCodeQ Исследуем API java.util, спи¬ компаратора 368 и equals() 386 сков и коллекций 350 Модификация Jukebox для Если нужна сортировка элемен¬ «Естественный порядок» — что поддержки Comparator 369 тов в множестве, можно восполь¬ Java подразумевает под алфавит¬ Стоп! Мы сортируем двумя зоваться TreeSet 388 ной сортировкой? 351 разными способами! 371 Что СЛЕДУЕТ знать о TreeSet... 389 Но теперь вам нужны объекты клас- Сортировка только с помо¬ Элементы TreeSet ДОЛЖНЫ са Song, а не просто строки 352 щью компараторов 372 быть сравнимыми 390 Изменение Jukebox: объекты Вся магия — в этих строчках 373 Списки и множества есть, при¬ SongV2 вместо String 353 Что же действительно нужно шло время словарей 391 Код не компилируется! 354 для сортировки? 374 Создание и наполнение коллекции 392 Объявление метода sort() 355 Лямбда-выражения: меньше Удобные фабричные методы Обобщенные типы означают кода, больше смысла 376 для коллекций 393 большую безопасность типов 356 Куда делся весь код? 377 Наконец, возвращаемся Обобщенные типы 357 Некоторые интерфейсы имеют толь- к обобщенным типам 394 Использование обобщенных ко ОДИН метод для реализации 377 Но будет ли это работать классов 358 Обновление кода Jukebox с помо¬ cList<Dog>? 395 Использование параметров щью лямбда 378 А что, если это разрешить? 396 типа с ArrayList 359 Ой-ой. Сортировка работает, но те¬ Применяем подстановочные Обобщенные методы 360 перь у нас есть дубликаты... 380 знаки 397 Вот тут все становится Нам нужно множество, Применение обобщенного странным... 361 а не список 381 параметра метода 398 Лямбда-выражения и потоки данных: что, а не как Не говорите компьютеру, КАК работать. Что, если можно просто сказать, ЧТО делать, а не КАК? В этой главе разберемся с потоками данных (Streams API), узнаем, как полезны лямбда- выражения для фильтрации и обработки данных в коллекциях, и поймем, почему это так круто. ^«иц k ЧШ HtRtmcjuvi tv* см» »\|м X'OtyYWV Ш сшшк —^ цуи» эгоол («I —J stream () ■N © - . filter ( Щ) ЩИ /"/'/"■ тёпы 6 Си ш ашка .collect(toList) Потоки данных и лямбда-выражения без технических подробностей Сообщите компьютеру, ЧТО вам нужно Когда циклы for идут не по плану Мелкие ошибкив коде — какиголки в стоге сена Основы часто используемыхопераций Знакомство с Streams API Начало работы с потоками данных Потоки данных — это как рецепты: ничего не произойдет, пока их не приготовить Извлечение результата из потока данных Операции с потоками данных — основа конвейеров потоков Операции можно соединять и комбинировать Настройка операций Выстраивайте из операций сложные конвейеры Да, потому что потоки данных ленивые Терминальные операции берут все на себя Формирование списка Рекомендации по работе с потоками данных Верно! Операции потока данных не изменяют исходную коллекцию Привет, лямбда, мой (не такой уж) 405 406 408 409 410 411 412 413 414 415 416 417 417 418 419 419 420 421 Виды лямбда-выражений 426 Структура лямбда-выражения 427 Разнообразие делает жизнь интереснее 428 Как определить, принимает ли метод лямбда-выражение? 430 Как распознать функциональные интерфейсы 432 Функциональныеинтерфейсы в реальном коде 433 Лу вернулся! 434 Первая задача Лу: найти все рок-композиции 436 Оставляем в потоке данных только нужные элементы 437 Погнали! 438 Усложняем фильтрацию 439 Второе задание Лу: Список песен всех жанров 440 Преобразование одного типа в другой 441 Удаление дубликатов 442 Вывод уникальных жанров 443 Продолжаем компоновку! 443 Иногда и лямбда-выражения не потребуются 444 Разные способы сбора результатов 445 Но это еще не все! 446 Некоторые операции что-то возвращают... а некоторые —нет 447 Optional — это обертка 448 Да, зато теперь у нас есть способ узнать, есть ли вообще результат 449 11 леп
Рискованное поведение Иногда ВСе идет не так. Файл пропал, сервер упал. Что делать, если метод потенциально опасен? В этой главе создадим MIDI-плеер с непредсказуемым JavaSound API и научимся грамотно обрабатывать исключения и ошибки. дотает вызывает : рискованный ваш код метод класс с ри¬ скованным методом 14 Рискованное поведение 457 Нельзя ставить «большие корзины» Давайте создадим музыкальную на «маленькие» 475 машину! 458 Когда не хочется обрабатывать Начнем с азов 459 исключение... 477 Сначала нам нужен секвенсор 460 Переадресовывая (через объявление) Как работать с потенциально исключение, вы лишь откладываете опасными (сторонними) методами? 461 неизбежное 478 В Java методы используют исключения, Обрабатывайте или объявляйте. чтобы сообщить вызывающему коду: 462 Это закон. 479 Компилятору нужно знать, что ВЫ Возвращаясь к коду секвенсора... 480 осознаете риск вызова метода 463 Как создается звук 482 Исключение — это объект... типа Версия 1: Ваше первое приложение Exception 464 для воспроизведения звука 484 Перехватываете исключение? Создание объекта MidiEvent Тогда кто его выбросил? 465 (MIDI-данные) 485 Управление потоком выполнения MIDI-сообщение — сердце с помощью конструкций try/catch 468 Midi-события 486 Finally: код, который выполняется Изменение сообщения 487 в любом случае 469 Версия 2: Экспериментируем Метод может выбрасывать сразу со звучанием с помощью несколько исключении? 471 консольных аргументов 488 Исключения полиморфны 472 Что вас ждет в дальнейших главах 489 Блоки catch должны располагаться от са мото конкретного к самому общему 474 Очень графическая история Графический интерфейс неизбежен! Даже если вы пишете серверный код, когда-нибудь вам придется создать инструмент с графическим интерфейсом. В двух главах о GUI мы рассмотрим обработку событий, вложенные классы, добавим кнопки, картинки и даже сделаем анимацию. class MyOuter { class Mylnner { void go () { > } Внешние и внутренние объекты теперь тесно связаны. /гиле-С Очень наглядная история 497 Все начинается с окна 498 Ваш первый графический интерфейс: кнопка в окне 499 Но при нажатии ничего не происходит... 500 Получение пользовательского события 501 Хотите ловить события кнопки — реализуйте нужный интерфейс 502 Взаимодействие слушателя и источника 503 Слушатели,источники и события 505 Возвращаясь к графике... 507 Создание виджета для рисования 508 Интересные возможности метода paintComponent() 509 За хорошей графикой вам к объекту Graphics2D 510 Зачем ограничиваться сплошным цветом, если есть красивый градиент? 511 Мы умеем получать события 512 Компоновка интерфейсов: размеще¬ ние нескольких виджетов в окне 514 Попробуем сделать две кнопки 516 Вложенный класс спешит на помощь! 520 Экземпляры вложенного и внешнего классов должны быть связаны* 521 Создание экземпляра внутреннего класса 522 И снова лямбда-выражения спешат на помощь 526 ActionListener — это функциональный интерфейс 527 Лямбда-выражения — понятнее и лаконичнее 527 Использование внутреннего класса для анимации 528 Готовим код 532 Слушаем не-СШ-событие Простой способ создавать сообщения/события 534 Версия первая: со статическим методом makeEvent() 535 Версия вторая: регистрация и получение ControllerEvents 536 Версия третья: рисование графики В РИТМ С МУЗЫКОЙ 537
Работаем с Swing ПрОСТОТа Обманчива. Swing кажется легким, пока не попытаешься разместить компоненты там, где надо. Виноват в этом Layout Manager. Но приложив немного усилий, вы подчините его своей воле. В этой главе научимся управлять Swing-компонентами и узнаем много полезных фишек. тают. KiuuuHuunn на 6ссп twjt IU. П| nfuai'cvuvumuti UUtpHHlf Ацшишнтм, ни a&pt н нш тиугакт прир turn UOlU-UHIfW North П West Center Цттн hi.ui- ruem fit; nn< East vcmcucct. South Знакомство со Swing Компоненты Swing Менеджеры компоновки Как менеджер компоновки принимает решения? Три основных менеджера компоновки: Border, Flow и Box. BoxLayout спешит на помощь! Эксперименты с компонентами Swing Готовим код Создание битбокса 545 546 547 548 549 557 559 564 565 Сохранение объектов (и текста) Заморозим И разморозим объекты. У объектов есть состояние и поведение. Поведение хранится в классе, а состояние — в каждом объекте. Как сохранить состояние, не перебирая вручную каждую переменную? Очень просто: сериализуем объект, а потом десериализуем его обратно. Сохранение объектов (и текста) Ловим ритм Сохранение состояния Запись сериализованного объекта в файл Путешествие данных: как потоки передают информацию Что происходит с объектом во время сериализации? Но что именно считается состоянием объекта? Что нужно сохранить? Нужен сериализуемый класс? Реализуйте интерфейс Serializable Десериализация: восстановление объектов Что происходит во время десериализации? Сохранение и загрузка игровых персонажей Идентификатор версии: важ¬ ная проблема сериализации Поле serialVersionUID Сериализация объектов Запись строк в текстовый файл Пример текстового файла: 575 Конструктор карточек викторины (структура кода) 597 576 Класс java.ю.File 600 577 Чтение текстовых файлов 602 578 Проигрыватель карточек викторины (структура кода) 603 Разбор строк с помощью String.split() 606 579 N10.2 и пакет java.nio.file 608 580 Path, Paths и Files: работа с каталогами в Java 609 А теперь подробнее о finally 610 581 Не забывайте, блок finally выполняется ВСЕГДА! 610 583 А теперь подробнее о finally (продолжение) 611 Есть способ получше! 611 587 Оператор try-with-resources (TWR) 612 588 Интерфейс AutoCloseable: небольшая загвоздка 613 590 Интерфейс Autocloseable 592 593 594 повсюду, где вы работаете с вводом-выводом 613 Готовим код 614 Сохранение ритмического рисунка 615 595 Загрузка ритмического рисунка 616
/7 Создание соединений На СВЯЗИ С внешним миром! Вы думаете, что работать с сетью сложно? Нет, все гораздо проще! Java уже позаботилась о низкоуровневых деталях и спрятала их в классе java.net. Одна из самых крутых особенностей Java в том, что сетевое взаимодействие — это тот же самый ввод-вывод, только с небольшой разницей Ot'UHIU К n</W\U ')(()(} 1 в цепочке действий. В этой главе мы создадим клиентские и серверные сокеты, освоим основы многопоточности и даже напишем настоящий чат-клиент. Готовы к потокам выполнения? Конечно, готовы! 623 18 Сетевое взаимодействие Битбокс в сети: чат в реальном времени 624 Подключение, отправка и получение 626 Подключение можно установить разными способами ” 632 Класс DailyAdviceClient 634 Код класса DailyAdviceClient 635 Разработка простого серверного приложения 637 Код класса DailyAdviceServer 638 Разработка чат-клиента 640 Наипростейший чат-сервер 642 Версия вторая: отправка и получе¬ ние сообщений 644 Множество потоков выполнения и один класс Thread 646 Стеки и потоки выполнения: осно¬ вы параллельного исполнения 647 Чтобы появился новый стек вызо¬ вов, нужна задача, которую можно в этом стеке выполнять 648 Чтобы задать задачу для потока выполнения, реализуйте интерфейс Runnable 649 Как НЕ нужно запускать Runnable 649 Как мы раньше создавали поток выполнения 650 Более удачное решение: не управлять потоками выполнения вовсе 651 Запуск одной задачи 651 Планировщик потоков выполнения 654 Усыпление потока выполнения 658 Усыпление потока выполнения предоставляет шанс запуска другим потокам 659 Недостатки усыпления потоков выполнения 660 Обратный отсчет до готовности 661 Создание и запуск двух потоков выполнения (и более!) 662 Создание пула потоков выполнения 663 Запуск нескольких потоков выполнения 664 Что произойдет? 664 Завершение пула потоков выполнения 665 Обновленный и улучшенный класс SimpleChatClient 668 Готовый код 673 Решение проблем с параллельностью Делать несколько вещей одновременно бывает сложно даже ЛЮДЯМ/ Не ГОВОРЯ уже О программах. Написать многопоточный код просто, но сделать так, чтобы он работал именно так, как вы задумали, уже не таклегко. В этой главе вы узнаете, какие неприятности могут произойти при запускенескольких потоков выполнения одновременно. Мы покажем, как использоватьинструменты из java.util. concurrent и другие полезные классы, чтобы ваш параллельный код не сошел с ума. В итоге у вас появится много полезных приемов для уверенной работы с многопоточностью. Решение проблем с парал¬ лелизмом 675 Проблема Райана и Моники: те¬ перь в коде 678 Им нужна блокировка для досту¬ па к счету! 681 Нам нужно проверять баланс и снимать деньги как одну атомарную операцию 682 Блокировка объекта 683 Синхронизация методов 684 Важно заблокировать правиль¬ ный объект 685 Страшная проблема «потерянно¬ го обновления» 686 Как возникает потерянное обновление... 687 Делаем метод increment() атомар¬ ным. Синхронизируем его! 688 Взаимная блокировка — темная Синхронизация нужна не всегда... 691 Сравнение и замена (CAS) на при¬ мере атомарных переменных 692 Атомарные приключения Райана и Моники 693 Создание класса для хранения неизменяемых данных 694 Использование неизменяемых объектов 695 Изменение неизменяемых данных 696 Разделяемые данные: что может пойти не так? 698 Опасное чтение из изменяемой коллекции 699 Используйте потокобезопасные структуры данных 700 Класс CopyOnWriteArrayList 701
Приложение. Готовый код Весь код для полноценного клиент-серверного чата, пришло время проявить себя! В приложении вы найдете полный набор кода для создания настоящего клиент-серверного чата. Ваш шанс почувствовать себя Java-рок-звездой! Финальная версия клиентском битбокс-программы 711 Финальная версия серверной битбокс-программы 718 I, которые не попали в основную книгу Полезные бонусы напоследок. Мы не можем отпустить вас в мир Java без приятных бонусов! Здесь вы найдете десяток полезных тем, которые по разным причинам не вошли в основную книгу, но обязательно пригодятся. А вот теперь уже точно все — больше никаких «прощаний», это конец. Б Приложение. Топ-10 Tei #11 JShell (Java REPL) 723 #10 Пакеты 724 #9 Неизменяемость в строках и классах-обертках 727 #8 Уровни доступа и модификаторы доступа (кто что видит) 728 #7: Переменное количество аргументов (varargs) 730 #6 Аннотации 731 #5 Лямбда-выражения и словари в Java 8+ 732 #4 Параллельные потоки 734 #3 Перечисления («перечисляемые типы» или просто enum) 735 #2 var: автоматическое определение типа локальной переменной 737 #1 Записи 738 Предметный указатель 741
Как пользоваться книгой И mmfu ьс| ,(<гтйнис. это ft fh&mm
Для кого эта книга? Если на все эти вопросы вы ответите «Да», 0 Вы уже занимались программированием? (2) Хотите изучать Java? (3) Вы предпочтете легкий разговор в непри¬ нужденной атмосфере сухой и скучной технической лекции? тогда эта книга для вас. Это НЕ справочник. И НЕ энциклопедия фактов о Java. Это книга, созданная для обучения. Кому не подойдет эта книга? Если вы можете ответить «да» хотя бы на один из этих вопросов: 0 Ваш опыт программирования ограничивается только HTML, и вы никогда не работали со скрипто¬ выми языками? (Если вы уже сталкивались с циклами или логи¬ кой «если/то», эта книга вам подойдет. Но одного языка HTML может быть маловато.) (2) Вы отлично владеете C++ и ищете справочник по Java? 0 Вы не любите пробовать новое? Вам проще пережить визит к стоматологу, чем решиться на что-то нестандартное, — вроде сочетания несочетаемого? Вы уверены, что техническая книга с картинкой утки в разделе про управление памятью не может быть серьезной? .. .тогда эта книга не для вас. /Jffuuu-мнш стмаркеталтв: кто ucfuu част про тс, чпи та ктиа Hujxupnn &с,м Слааыш/ui сГанксв- скч.х карт?А как ,ш акидя «Ятат 'Java на ntuvq- </)„,// П ' f >“
Мы знаем, о чем вы думаете «Как это может быть серьезной книгой по программированию на Java?» «К чему столько графики?» «Реально ли научиться таким образом?» «Здесь пахнет пиццей?» И мы знаем, о чем думает ваш мозг Наш мозг обожает новизну. Он постоянно сканирует окружающий мир в поисках чего-то необычного, потому что так помогал нам выжи вать на протяжении всей эволюции. Сегодня вероятность стать обедом для тигра практически нулевая, но мозг все равно настороже — вы просто этого не замечаете. А как он реагирует на скучные, рутинные вещи, которые нас окружа¬ ют? Очень просто: делает все, чтобы они не мешали его настоящей работе — запоминанию действительно важной информации. Все, что кажется ему незначительным, даже не проходит через фильтр «это важно». /Заш. нсл аимаап, тс важно ЭТО Как мозг решает, что важно? Паш мел vj-жх htwuu. «Зяпс- uuvvamt? Kt tiv vpte, спасибо!» Великолепно. Целых 750 страниц сухого, скучного текста. Представьте, что вы отправились в поход. Внезапно перед вами по¬ является тигр. Что происходит в вашей голове? Уровень возбуждения нейронов взлетает до небес. Эмоции зашкаливают. Организм выбрасы¬ вает порцию химии, заставляющей запомнить это мгновение навсегда. «Это может быть важно! Запомни!» А теперь представьте другую ситуацию. Вы дома, в библиотеке или уютном кафе. Здесь тепло, спокойно, и тигров в поле зрения не наблю¬ дается. Вы готовитесь к экзамену или пытаетесь разобраться в сложной технической теме, на которую, по мнению вашего начальника, уйдет неделя. Ну, максимум десять дней. Но есть одна проблема. Мозг думает, что делает вам одолже¬ ние. Он фильтрует «скучную» информацию и игнорирует ее, сохраняя ценные ресурсы. На что? На по-настоящему важные вещи. Например, где прячутся тигры. Как не устроить пожар. Или почему никогда не стоит кататься на сноуборде в шортах. И нет простого способа сказать своему мозгу: «Эй, дружище, спасибо за заботу, но хотя книга скучная и совсем не вызывает эмоций, мне действительно нужно это запомнить!»
/Лы считаем, что читатель нашей книги — это ученик. Что же нужно сделать, чтобы научиться чему-то? Во-первых, необходимо это понять, а затем сохранить надолго в памяти. Простым механическим заучиванием здесь не обойтись. Согласно новейшим исследованиям в области когнитивной науки, нейробиологии и психологии образования, обучение требует гораздо большего, чем просто текст. Мы знаем, что понравится вашему мозгу. Несколько принципов обучения Head First Наглядность. Изображения запоминаются лучше, чем слова и делают обучение гораздо более эффективным (до 89% в исследованиях запоминания и передачи информации). Иллюстрации облегчают понимание. Размещайте надписи на картинке или около нее, а не внизу или на другой странице. Этот прием в два раза снизит количество трудностей, связанных с содержанием. • /*п< ЧпАцп, (1,1,1 иннни doCalcQ возвращаемое значение Сочетание разговорного и индивидуального стилей. Согласно последним исследованиям, студенты, прошедшие обучение, на 40% лучше справлялись с тестами, если в обучающих материалах контент, обращенный Плохо быть непосредственно к читателю от первого лица, с использованием разговорного абстрактным методом. ) стиля, а не формального. Не читайте лекции — рассказывайте истории. Говорите Тела-то нет. ) неПрИНуЖденным языком. Не относитесь к себе слишком серьезно. Подумайте, что привлекает больше внимания: яркий рассказчик в дружеской обстановке или лекция? Мотивация ученика думать напряженно. Другими словами, если вы не будете активно напрягать свои нейроны, в голове ничего особенного не произойдет. Читатель должен быть мотивирован, вовлечен, любопытен и вдохновлен, чтобы решать проблемы, делать выводы и генерировать новые знания. А для этого нужны вызовы, упражнения, заставляющие задуматься, вопросы roam () ; и занятия, которые задействуют оба полушария мозга и все Можно ли сказать, что ван¬ на — это ванная комната? А ван - i ная комната — это ванна? Или все-таки ванная имеет ванну, а не является ею? Вот разница между IS-А и HAS-A! Я1 111'1'1 р органы чувств. Привлечение и удержание внимания читателя. Все мы сталкивались с ситуацией, когда нужно что-то выучить, но дальше первой страницы дело не идет. Мозг обращает внимание на необычные, интересные, странные, привлекающие внимание, неожиданные вещи. Изучение новой, сложной, технической темы не обязательно должно быть скучным. Мозг усвоит материал гораздо быстрее, если информация будет подана необычно. Затроньте эмоции. Как известно, способность запоминать во многом зависит от эмоционального содержания. Запоминается то, что волнует. Вы запоминаете, когда что-то чувствуете. Мы не говорим о каких-то душещипательных историях. Речь о таких эмоциях, как удивление, любопытство, веселье, «что за...?», и чувство «Я крут!», которое возникает, когда вы решили сложную головоломку, освоили что-то, что все остальные считают сложным, или понимаете, что знаете то, чего не знает Боб из инженерной службы «Уж я-то знаю».
Метапознание: мышление о мышлении Если вы действительно хотите учиться, причем учиться быстро и качествен¬ но, обратите внимание на то, как вы обращаете внимание. Подумайте о том, как вы думаете. Изучите то, как вы учитесь. Большинство из нас не учились метапознанию и не изучали теорию обучения в детстве. От нас ожидали, что мы будем учиться, но редко учили этому. Мы предполагаем, что если вы держите в руках эту книгу, то хотите изучать Java. И, скорее всего, не хотите тратить много времени. Чтобы получить максимальную отдачу от этой или любой другой книги, или любого другого обучения, возьмите ответственность за свой мозг. За его работу с содержимым книги. Необходимо сделать так, чтобы мозг воспринимал новый материал, который вы изучаете, как действительно важный. Решающий для вашего благополучия. Такой же важный, как тигр. В противном случае вас ждет постоянная борьба, в которой мозг будет делать все возможное, чтобы новый материал не закрепился. Так как же ЗАСТАВИТЬ мозг относиться к Java, как к голодному тигру? Интересно, как обмануть мозг, чтобы все это запомнить.. Существует два способа: медленный и утомительный способ и быстрый, более эффективный. Первый способ заключается в простом повторении. Вы наверняка знаете, что даже самую скучную тему можно выучить, постоянно повторяя одно и то же. Концентрируя внимание на одном и том же материале многократно, тем самым мы даем понять мозгу, что хотя информация не кажется важной, ее необхо¬ димо усвоить. Второй, более быстрый способ — любыми методами повышать мозговую актив¬ ность, и не сосредотачиваться на одном, а подключать различные типы деятель¬ ности мозга. Способы, о которых шла речь на предыдущей странице, могут решить большую часть проблем. Они гарантированно помогают мозгу работать так, как надо вам. Исследования показывают, что когда мозг видит картинку с подписями внутри изображения, он подключает больше нейронов для понимания того, как со¬ относятся слова и изображение, по сравнению с вариантом, когда подпись к рисун¬ ку находится внизу или в тексте. Больше работающих нейронов — больше шансов, что мозг поймет важность информации и, возможно, ее запомнит. Разговорный стиль подачи информации помогает, поскольку человек воспри¬ нимает себя участником разговора, а значит, должен следить за развитием темы и выполнять обязанности собеседника. Удивительно, но мозгу совершенно безраз¬ лично, что «разговор» происходит между вами и книгой! Если же стиль изложения формальный и сухой, мозг воспринимает это как лекцию, когда приходится сидеть в комнате, полной пассивных слушателей. Нет необходимости бодрствовать. Но картинки и разговорный стиль — это только начало.
Вот что мы сделали Мы использовали иллюстрации, потому что мозг легче воспринимает визуальные образы, а не текст. Для мозга картинка действительно стоит 1024 слов. Когда текст и картинки работают вместе, мы размещаем текст прямо на картинках, а не в виде подписи под рисунком или где-то в тексте. Так мозг работает эффективнее. Мы использовали повторение. Одна и та же мысль в книге повторена разными способами, с привлечением нескольких чувств. Так увеличится вероятность того, что она закрепится в нескольких областях мозга. Мы использовали концепции и картинки в неожиданных формах, поскольку мозг в первую очередь настроен замечать новизну. Иллюстрации в книге выполнены так, чтобы вызыватьэмоциональный отклик, потому что на биохимию эмоций мозг обя¬ зательно обращает внимание. То, что вызывает у вас какие-то чувства, — например, смех, удивление, интерес, — с большей вероятностью сохранится в памяти. Мы использовали индивидуальный разговорный стиль. Мозг задействует больше ресурсов, если считает, что вы участвуете в разговоре, чем когда он думает, что вы в роли пассивного слушателя. Это справедливо и при чтении. Мы включили в книгу более 50 упражнений. Запоминание происходит проще, ког¬ да мозг не просто знакомится с информацией, прочитав раздел книги, но и решает какую-то практическую задачу. Мы намеренно сделали упражнения сложными и в то же время выполнимыми, потому что большинство читателей предпочитает именно этот вариант. Головоломкино Мы использовали несколько стилей обучения. В книге есть и пошаговые страте¬ гии освоения навыков, и описания общей картины, и примеры кода. Независимо от ваших предпочтений в обучении, каждый получает пользу от того, что один и тот же контент представлен несколькими способами. Мы подобрали материалы для обоих полушарий мозга: чем больше участков мозга задействовано, тем больше вероятность того, что вы научитесь и запомните, и тем дольше сможете сохранять сосредоточенность на обучении. Очень часто в про¬ цессе учебы работает одно полушарие, а другое получает передышку. Значит, включение их в работу поочередно дает возможность более продуктивно учиться в течение длительного времени. Мы включили в книгу истории и упражнения, в которых представлено более одной точки зрения: мозг лучше усваивает информацию, когда ему приходится делать оценки и суждения. Мы добавили упражнения и задачи, на которые не всегда есть прямой ответ. Зада¬ ние, над которым пришлось хорошо потрудиться, надолго останется в памяти (точ¬ но также можно привести тело в форму только работая в спортзале, а не наблюдая за тренировкой со стороны). Мы хорошо проработали эти задания, чтобы быть уверенными, что ваши усилия принесут необходимый результат, что вы не тра¬ тили ни одного лишнего дендрита на обработку труднопонимаемого примера или разбор сложного, насыщенного жаргоном или излишне лаконичного текста. Мы использовали подход 80/20. Исходим из того, что читатель, задумавший полу¬ чить докторскую степень по Java, не ограничится лишь этой книгой. Поэтому мы не рассказываем обо всем. Только о том, что пригодится на практике. Вез лишних «лов 0 Прокачай извилины
л%& Вруби мозг на всю катушку! Мы свою часть работы сделали, дальше все на вас. Эти советы — просто старт, а что с ними делать, решать вам. Прислушайтесь к себе, пробуйте то, что еще не пробовали, и определяйте, что работает, а что нет. V* эт« П&,т 1 'У1НА»* на халатик! (Т) Не спешите. Чем больше вы поймете, тем (б) меньше придется заучивать. Не просто читайте — остановитесь и по¬ думайте. Когда в книге встречается вопрос, не спешите с ответом. Представьте, что его задает живой человек. Чем глубже ваш мозг вовлечется в размышления, тем выше шанс по-настоящему понять и запомнить. (2) Выполняйте упражнения. Делайте заметки. Мы тоже делаем заметки. Но если бы мы записывали все за вас, это было бы так же полезно, как чужая тренировка вместо вашей. И не просто просматривайте упраж¬ нения — беритесь за карандаш! Есть масса доказательств, что мозг учится лучше, когда в процесс вовлечено движение. Так что за¬ писывайте, рисуйте, делайте пометки — это поможет запомнить гораздо больше! @ Читайте «(не) дурацкие вопросы». Читайте все! То, что текст вынесен за рамки, не делает его второстепенным: это такая же важная часть книги. Иногда вопросы важнее ответов. Не пропускайте их. (4) Не читайте долго на одном месте. Вставайте, потягивайтесь, двигайтесь! Меняйте стулья, меняйте комнаты. Так мозг почувствует новизну и не привяжет обуче¬ ние к одному месту. Это поможет вам лучше запоминать. 0 Читайте эту книгу перед сном — пусть это будет последняя сложная вещь за день. Запоминание не заканчивается, когда вы закрываете книгу. Процесс переноса инфор¬ мации в долговременную память продолжа¬ ется даже после того, как вы отложили книгу. Мозгу нужно время, чтобы переработать и уложить знания по полочкам. Если сразу после этого переключиться на что-то новое, часть информации просто испарится. Так что дайте мозгу шанс сохранить самое ценное! Пейте воду. Много воды. Мозг лучше всего работает в условиях оптималь¬ ного количества воды в организме. Обезвожива¬ ние (которое может произойти еще до того, как вы почувствуете жажду) снижает когнитивные функции. (7) Проговаривайте. Вслух. Говорите вслух — и ваш мозг заработает по- новому! Разговор активирует другие участки мозга. Когда нужно понять или запомнить сложную мысль, попробуйте произнести ее вслух. А еще луч¬ ше — объясните ее кому-то. Это не только поможет запомнить информацию, но и переосмыслить ее, увидев детали, которые ускользнули при чтении. (в) Слушайте свой мозг. Следите за перегрузкой мозга! Если замечаете, что не можете сосредоточиться или забываете только что прочитанное, значит, пора сделать перерыв. Мозг тоже устает. В какой-то момент обучение за¬ медляется, и если продолжать без отдыха, можно не только снизить эффективность, но и навредить процессу запоминания. Отдых — это тоже часть обучения! Дайте мозгу передышку, и он отблаго¬ дарит вас лучшей памятью. (9) Добавьте эмоций! Ваш мозг должен понять, что это важно! Делайте информацию личной — додумывайте истории, вспоминайте свои примеры, придумывайте под¬ писи к фотографиям. Даже если шутка в книге не¬ удачная — лучше поворчать, чем не почувствовать вообще ничего. Любая эмоциональная реакция помогает запомнить материал. <§) Введите и запустите код. Код нужно не только читать — его нужно за¬ пускать! Введите примеры, запустите их и поэк¬ спериментируйте. Измените что-то, попробуйте улучшить... или взломайте код — это лучший способ разобраться, как он работает! Большую часть примеров, включая длинные фрагменты кода и готовые решения, можно найти по адресу
Что вам понадобится для этой книги: Вам не нужна IDE — только текстовый редактор и командная строка! Мы настоятельно рекомендуем пока не использовать интегрированную среду разработки (IDE). Почему? Потому что IDE делает многое за вас, и можно упустить важные де¬ тали, которые действительно стоит понять. Сначала разбери¬ тесь с основами: пишите код в простом текстовом редакторе, компилируйте и запускайте через командную строку. А когда почувствуете, что понимаете процесс, смело переходите к ин¬ струментам, которые автоматизируют часть работы. Для работы с книгой вам понадобится Java 11 (кроме приложения Б). Основная часть кода будет работать и с Java 8. Если в книге речь пойдет о функции, доступной только в более новых версиях, это будет указано. НАСТРОЙКА JAVA ■ Поскольку версии быстро меняются, а вслед за ними и советы по выбору правильного JDK (Java Development Kit), мы написали подробные инструкции по установке Java на страничке в Интернете: https://oreil.ly/hfJava_install Это если в двух словах. ■ Если вы не знаете, какую версию Java загрузить, мы рекомендуем использовать Java 17. ■ Есть множество бесплатных сборок OpenJDK (версия Java с открытым исходным кодом). Мы предлагаем поддерживаемый сообществом Eclipse Adoptium JDK, доступный по адре¬ су https://adoptium.net. ■ JDK включает в себя все необходимое для компиляции и запуска Java. JDK не включает документацию по API, а она вам необходима! Загрузите документацию по API Java SE. Вы также можете получить доступ к документации по API онлайн, но поверьте нам, лучше ее скачать. ■ Вам понадобится текстовый редактор, и подойдет практически любой. Можно использо¬ вать vi, emacs или редакторы с графическим интерфейсом (GUI), которые есть в боль¬ шинстве операционных систем: Notepad, WordPad, TextEdit — все это подходит, главное: сохраняйте файлы в обычном текстом формате (Plain Text), а не в формате rich text, сле¬ дите, чтобы редактор не добавлял расширение .txt в конец имени файла — у вас должен быть файл .java, а не .java.txt! ■ Итак, вы скачали, распаковали, установили — или что-то в этом духе (в зависимости от версии и ОС). Теперь осталось добавить Java в PATH. Добавьте в переменную окружения PATH путь к директории bin внутри основной папки Java. Почему это важно? Чтобы при вводе команды: % javac в командной строке система могла найти и выполнить соот¬ ветствующие исполняемые файлы компилятора и виртуальной машины Java. ■ Примечание: если у вас возникли проблемы с установкой, мы рекомендуем зайти на сайт javaranch.com и присоединиться к форуму Java-Beginning! А еще лучше — присоединяй¬ тесь в любом случае, даже если проблем нет. Всегда полезно быть в сообществе, где можно задать вопрос, найти советы и просто пообщаться с единомышленниками. Код из этой книги доступен по ссылке: https://oreil.ly/hfJava_3e_examples
И напоследок Это обучающая книга, а не справочник. Мы намеренно убрали все, что может помешать изучению материала, который разбирается в этой книге. Взяв книгу в руки впервые, вам придется начать с самого начала, потому что книга учитывает, что вы уже видели и выучили. Мы используем простые UML-подобные диаграммы. Если бы мы использовали чистый UML, вы бы увидели что-то похожее на Java, но с неправильным синтаксисом. Поэтому мы используем упрощенную версию UML, которая не конфликтует с синтаксисом Java. Если вы еще не знаете UML, вам не придется беспокоиться об одновременном изучении Java и UML. Мы не придираемся к тому, как вы организуете и форматируете свой код. Работая с книгой, вы сможете приступить к изучению Java, не беспокоясь о некоторых организационных или административных деталях разработки программ на Java. В реальном мире вам придется знать и использовать эти детали, но поскольку создание и развертывание Java-приложений обычно опирается на сторонние средства сборки, такие как Maven и Gradle, мы предположили, что вы будете изучать эти инструменты отдельно. Упражнения в конце глав обязательны, а головоломки — нет. Ответы на эти задания находятся в конце каждой главы. О головоломках нужно знать одну вещь — это головоломки. Логические головоломки, каверзные вопросы, кроссворды и т. д. Упражнения необходи¬ мы для отработки полученных знаний, а значит, все их нужно выполнять. Головоломки — это совсем другая история. Некоторые из них решить до¬ вольно сложно. Они предназначены для тех, кто любит поломать голову над решением. Выбирайте сами, тратить на них время или нет. В любом случае, попробуйте разгадать некоторые из них. Но не расстраивайтесь, если голо¬ воломку решить не получится или на нее не найдется времени. - — чанмА cfl «./mrunuiu ■хин* He все упражнения из серии «Заточите карандаш» имеют ответы. Часть просто не напечатаны в книге. На некоторые из них правильного ответа просто нет. А другие мы специально не стали приводить, чтобы вы могли сами решать, правильны ли ваши ответы. Это часть учебного процес¬ са по оттачиванию мышления. уточите карандаш Уп^\ажи1 кия, сп иежннш унспксш « УпИалсшн ис» Kuf cpamn ис. и цшавок). «фпелть il7(c,L,u, СК ти ил’ ЧИИ (’и сфщкс уу/апи Java. Упражнения щтм Примеры кода максимально упрощены. Неприятно продираться через 200 строк кода в поисках двух строк, кото¬ рые вам нужно изучить. Большинство примеров в этой книге сокращены до минимального объема, чтобы часть, которую вы изучаете, была понятна и проста. Поэтому не ожидайте, что код будет надежным или даже полным. Написанием качественного кода вы будете заниматься после того, как закон чите работу с книгой. Примеры в книге написаны специально для обучения и не всегда являются полностью функциональными. нисЯлши.и ни <wK<и • --, щ «»/нт м.К-И " ) нс !ckmt „«.«Я tni. н 1 нтптн|Д^п\нм.иа мсжелт токсине ил спиши. Нс «ИМ (?ли НО „инмсюнис, пишет нон|1а(?мтся!
Технические рецензенты 3-го издания этой книги М арк Ат Марк начал изучать Java еще в первые дни ее су¬ ществования в Sun Microsystems (привет, Hotjava!) и с тех пор не оглядывался назад. Он был одним из первых авторов книг по Java, а также разработал обучающие курсы, работая с различными компани¬ ями в США, Европе и Азии. Совсем недавно Марк написал для O’Reilly книгу Smaller С и стал соавто¬ ром пятого издания Learning Java. Сейчас он живет в Огайо, занимается разработкой ПО и специализи¬ руется на микроконтроллерах. А сраха и,М ар пн .7 Щш- Абрахам — Java-программист, консультант, автор и оратор с более чем десятилетним опытом ра¬ боты в различных отраслях. Родом из Валенсии, Испания, Абрахам построил большую часть своей карьеры в Лондоне, где работал в таких органи¬ зациях, как JP Morgan или Министерство вну¬ тренних дел Великобритании, часто сотрудничая с компанией Equal Experts. Решив делиться опы¬ том и помогать другим, Абрахам стал редактором новостей Java в InfoQ, написал книгу Real-World Maintainable Software и стал соавтором Continuous Delivery in Java. Он также участвует в управлении Лондонским Java-сообществом и, помимо всего прочего, изучает физику — потому что учеба для него никогда не заканчивается!
Эти люди тоже внесли свою лепту в 3-е издание В издательстве O’Reilly: Огромная благодарность Зану Маккуэйду и Николь Таше за то, что помогли нам наконец-то вы¬ пустить это издание! Зан, спасибо за то, что подключил Тришу к миру Head First, а Николь — за фантастическую работу, подтолкнувшую нас к этому. Спасибо Меган Бланшетт, которая покинула O’Reilly уже сто лет назад, но именно она познакомила Берта и Тришу еще в 2014 году. Триша хотела бы поблагодарить: Хелен Скотт за постоянную обратную связь о новых разделах книги. Она постоянно оста¬ навливала меня от слишком глубокого погружения в тему или принятия на себя слишком большого объема знаний, и защищает тем самым читателей этой книги. Мне не терпится продолжить сотрудничество с ней в следующем проекте. Моей команде в JetBrains за их терпение и поддержку: Далии Або Шеаша за тестирование главы, посвященной лямбда-функциям и потокам данных, а также Малу Гупту за необходи¬ мую информацию о современных сертификациях lava. Отдельное спасибо Хади Харири за постоянную поддержку. Информатикам из Friday Pub Lunch за то, что терпеливо переносили обеденные разговоры о любом аспекте Java, который я пытался объяснить целыми днями и даже неделями, и Алис, Джен и Клэр за то, что помогли мне расставить приоритеты. Спасибо Холли Камминс за то, что в последнюю мину¬ ту обнаружила ошибки. Иви и Эми — спасибо за идеи по улучшению примеров с мороженым для Optional в Java! От¬ дельная благодарность за искреннюю поддержку и спонтанные «дай пять», когда вы узнали, что я наконец-то закончила! Все это было бы невозможно без Израэля Боза Родригеса. Ты терпел, когда я срывала важные раз¬ говоры вроде «Что у нас будет на ужин?» вопросами вроде «Как ты думаешь, CountDownLatch — это слишком узкоспециализированная тема для начинающих разработчиков?». Большая благодарность за то, что смог выделить мне место и время для работы над книгой и регулярно напоминал мне, по¬ чему я вообще взялась за этот проект. Спасибо Берту и Кэти за то, что взяли меня с собой в это путешествие. Для меня было большой честью стать автором Head First. Берт и Кэти передают благодарность: Бет Робсон и Эрику Фриману за их постоянную и крутую поддержку серии Head First. Отдельное спасибо Бет за многочисленные беседы, в ходе которых мы обсуждали, какие новые темы Java следует преподавать и как это делать. Полу Уитону и замечательным модераторам сайта CodeRanch.com (он же JavaRanch) за то, что они поддерживают дружелюбную атмосферу CodeRanch, привлекательную для начинающих Java- специалистов. Отдельное спасибо Кэмпбеллу Ритчи, Жанне Боярской, Стефану ван Халсту, Робу Спуру, Тиму Куку, Фреду Розенбергеру и Фрицу Вальравену за их неоценимую помощь в выборе действительно важных дополнений к Java со времени выхода второго издания. Дэйву Густафсону за то, что научил меня многому в области разработки программного обеспечения и скалолазания, а также за важные разговоры о состоянии программирования. Эрику Норманду за то, что научил нас немного FP и помог нам понять, как впихнуть несколько лучших идей из FP в книгу по ОО. Сай¬ мону Робертсу за его огромный опыт преподавания Java студентам по всему миру. Спасибо Хайнцу Кабутцу и Венкату Субраманиаму за помощь в изучении закоулков Java Streams. Лауре Болдуин и Майку Лоукидесу за их неустанную поддержку Head First на протяжении всех этих лет. Рону Билодо и Кристен Браун за их выдающуюся, всегда терпеливую и дружескую поддержку.
Технические рецензенты нового издания этой книги Бесконечная благодарность Джессике и Вэл за их тяжелую работу по редактированию 2-го издания. Джесс работает в Hewlett-Packard в группе авто¬ матизированного восстановления систем. Она получила бакалавра компьютерной инженерии в Университете Вилланова, имеет сертификаты SCJP 1.4 и SCWCD и буквально через пару меся¬ цев станет магистром по разработке ПО в Дрек- сельском университете. (Вот это да!) Когда Джесс не работает, не учится и не рас¬ секает на своем MINI Cooper S, ее можно найти в битве за пряжу с котом — когда она заканчи¬ вает вязание или вязание крючком. (Кому нужна шапка?) Она родом из Солт-Лейк-Сити, Юта (и нет, она не мормонка... да, вы хотели спросить), а сейчас живет недалеко от Филадельфии вместе с мужем Мендрой и двумя котами: Чаем и Саке. Джесс также модерирует технические форумы на Javaranch.com. Тамтр йалижиил Валентин получил степень магистра в области информационных и компьютерных наук в Феде¬ ральном технологическом институте Швейцарии в Лозанне (EPFL). Он работал инженером-про- граммистом в SRI International (Менло-Парк, Ка¬ лифорния), а затем главным инженером в лабо¬ ратории программной инженерии EPFL. Сейчас Валентин — соучредитель и технический дирек¬ тор Condris Technologies, компании, занимающей¬ ся архитектурой программного обеспечения. Его научные интересы включают аспектно-ориенти¬ рованные технологии, архитектурные шаблоны и паттерны проектирования, веб-сервисы и, конечно, архитектуру ПО. Помимо заботы о жене, садоводства, чтения и спорта, Валентин моде¬ рирует форумы SCBCD и SCDJWS на Javaranch. com. У него есть сертификаты SCJP, SCJD, SCBCD, SCWCD и SCDJWS, а еще он соавтор симулятора экзамена Whizlabs SCBCD. (И мы до сих пор не можем поверить, что однажды видели его в гал¬ стуке!)
Другие виновники, сунувшй^юс В издательстве O’Reilly: 7(th<nupm hxuuw Наша огромная благодарность Майку Лоукидесу из O’Reilly за то, что он рискнул и помог превратить концепцию Head First в книгу (а затем и в целую серию). Спасибо Тиму О’Рейли за готовность запустить нечто совершенно новое и не обычное. Отдельная благодарность Кайлу Харту — за то, что он нашел способ вписать Head First в этот мир и запустить серию. И, наконец, спасибо Эди Фридман за дизайн обложки Head First, который сразу дает понять: здесь важна не только книга, но и ваша голова! Наши бесстрашные бета-тестеры и команда рецензентов: Мы выражаем наивысшие почести и благодарность Йоханнесу де Йонгу, руководителю нашей команды технических рецензентов JavaRanch. Это уже пятая книга Head First, над которой вы работаете с нами, и мы счастливы, что вы все еще с нами! Джефф Кампе, спасибо, что снова с нами — уже в третий раз. Вы неустанно находите моменты, где нам нужно быть четче и точнее, и мы очень это ценим. Кори Макглоун, вы просто супер. Ваши объяснения на JavaRanch — одни из самых понятных и точных, и, возможно, вы заметите, что мы украли одно-два из них. Джейсон Менард, спасибо за то, что спас наши технические задницы от множества ошибок, а Томас Пол, как всегда, предоставил экспертную оценку и нашел тонкие нюансы Java, которые мы могли пропустить. Джейн Грискти — знаток Java и мастер текста, и мы рады, что она работала с нами над новым изданием вместе с давним джаваранчером Барри Гонтом. Мэрилин де Кейрос оказала огромную помощь в работе над обоими издания¬ ми книги. Крис Джонс, Джон Найквист, Джеймс Кубета, Терри Кубета и Айра Беккер внесли огромный вклад в первое издание. Отдельное спасибо Главным Первопроходцам, которые помогали нам с само¬ го начала: Анджело Селесте, Николаю Заикину и Томасу Даффу (twduff.com). И, конечно же, нашему потрясающему агенту Дэвиду Рогельбергу из StudioB. (Но если серьезно, как насчет прав на фильм?) Я.жсшчн Ж uuitvj Томас Жо. i Кампе idotut
Еще чуточку благодарностей И не надейтесь, это еще не все!* Другие технические специалисты по Java, помогавшие в работе над первым изданием (в псевдослучайном порядке): Эмико Хори, Майкл Таупиц, Майк Галлихью, Маниш Хатвальн, Джеймс Чегвидден, Шве- та Матхур, Мохамед Мазахим, Джон Паверд, Джозеф Бих, Скулрат Патанаванич, Сунил Палича, Суддхасатва Гош, Рамки Шринивасан, Альфред Рауф, Анджело Селесте, Николай Заикин, Джон Зотебьер, Джим Пледжер, Барри Гонт и Марк Дилен. Команда, трудившаяся над задачками в первом издании: Дирк Шрекманн, Мэри «Чемпионка JavaCross» Ленерс, Родни Дж. Вудрафф, Гэвин Бонг и Джейсон Менард. JavaRanch повезло, что у него есть такая команда! Другие соучастники, которых стоит поблагодарить: Пола Уитона, босса тропы JavaRanch, за поддержку тысяч изучающих Java. Сольвейг Хаугланд, хозяйку J2EE и автора книги Dating Design Patterns. Дори Смита и Тома Негрино (backupbrain.com) — за помощь в ориентировании в мире технических книг. Эрика Фримана и Бета Фримана (авторов Head First Design Patterns) — за то, что они снаб¬ дили нас Bawls", чтобы мы закончили книгу вовремя. И Шерри Доррис — за то, что действительно важно. Отважные первопроходцы серии Head First: Джо Литтон, Росс П. Голдберг, Доминик Да Сильва, honestpuck, Дэнни Бромберг, Стивен Лепп, Элтон Хьюз, Эрик Кристенсен, Вулинь Нгуен, Марк Pay, bdulhaf, Натан Олифант, Майкл Брэдли, Алекс Дэрроу, Майкл Фишер, Сара Ноттингем, Тим Аллен, Боб Томас и Майк Бибби (первый). 'Такое количество благодарностей объясняется тем, что мы тестируем одну важную теорию: каждый, чье имя упомянуто в книге, купит хотя бы один экземпляр. А, возможно, и больше — ведь у него есть родственники, друзья и все такое.
1. Ныряем: быстрый заплыв АР°РЫва*с* СКВо*ь пове?+НОсть Плывите сюда, водичка отличная! Вы окунетесь глубже и напишете простой код, затем скомпилируе¬ те и запустите его. Мы поговорим о синтаксисе, циклах и ветвлениях покажем все преимущества Java, • выделяющие его среди других. Очень скоро вы начнете само- L стоятельно писать код. А Java Открывает новые ВОЗМОЖНОСТИ. С момента выхода версии 1.02 (довольнохилень¬ кой) Java обратила на себя внимание разработчиковдружественнымсинтаксисом, объектно-ориенти¬ рованной направленностью, управлением памятью и, что самое приятное, — возможностью переноса на разные платформы в ближайшей перспективе. Возможность написать код один раз и применять его везде слишком привлекательна. Вспыхнувший интерес к языку стал угасать: работе мешали постоянно возникающие ошибки, ограничения и то, что Java работал очень медленно. Но это было давно. Если вы только начинаете изучать язык Java, вам повезло. Тем, кто начинал работать с этим языком в момент его появления, приходилось преодолевать множество трудностей, чтобы заставить работать даже про¬ стейшее приложение. Почему бы не попробовать современный язык Java — намного более мощный, быстрый и легкий для чтения и написания кода.
Как работает Java Первая задача — написать приложение (в данном примере интерактивное приглашение на вечеринку), но так, чтобы оно работало на любом устройстве, которое есть у ваших друзей. исходный код для интерактив- ного приглаше- Ш ния на вече- ■■ р ринку. Исходник о Исходник. Создайте исходный документ. Используйте установленный протокол (в данном случае — язык Java) Компилятор Method Party() 0 aload _С 1 invokespecial «Method java, lang Object()> 4 return О Компилятор. Пропу¬ стите документ через компилятор исходно¬ го кода. Он отвечает за проверку кода на ошибки. Результат вы сможете получить только если все будет работать корректно. О Компилятор создает документ, содержащий байт-код Java. Любое устройство, способное работать с Java, сможет интерпретировать/ перевести этот файл в формат, который сможет запустить. Скомпилированный байт-код не зависит от платформы. Виртуальные машины О У всех ваших друзей есть виртуальная машина Java (JVM), реализованная программно, работающая внутри их электронных гаджетов. Когда друзья запускают вашу программу, виртуальная машина считывает и выполняет байт-код.
Что вы будете делать на Java Вы создадите файл с исходным кодом, скомпилируете его с помощью компилятора javac, а затем запустите скомпилированный байт-код на виртуальной машине Java. ll File Edit Window Help Plead import java .awt.'; import java.awtevent."; classParty { public void buildlnvite() { Frame f= new Frame(); Label 1 = new Label("Be4epnnna у Тима"); %javac Party.java Button b = new ВиКоп("Загляну"); Button c = new Ви«оп("Лучше застрелюсь"); Panel p = new Panelj); Компилятор p.add(l); } // прочий код... } © "|/ПМПМПМП\/1/1ТО r+тамп Исходник о Наберите исходный код. Сохраните под именем: Party.java Party.java, запустив javac (приложение-компиля¬ тор). Если компилятор не обнаружит ошибок, вы получите еще один файл с именем Party.class.' Созданный компилятором файл Party.class состоит из байт-кода. Method Party() 0 aload _0 1 invokespecial #1 «Method java. lang.Object()> 4 return Method void buildlnvitef) 0 new #2 «Class java.awt.Frame> 3dup 4 invokespecial #3 «Method java. awt.Frame()> (КОД) О Скомпилированный код: Party.class Виртуальные машины О Запустите программу Party, class в виртуальной машине Java (JVM). JVM преобразует байт-код в формат, поддерживаемы й целевой платформой, и запустит программу. P/fyuuitYdiuu UC. щрпн lufmi^wiiu vim m lyiau^i. vmc w ум,Hu iuru in ПГйгч.п rn-yu 6V.I tun кош minimi H<« huhi<i,uii ксу. Уншглч.ин Г.’тцмл nfviuix ncKiijtdfoim. ami. fit vmcynnon.wm. yjyiynmu onfij.wi, Kuf ни urn n глушишц hi flit 1111 jt,tt trim: ki.ioui. пук fimu in hi nyvi m)
История языка Java Кратчайшая история Java Java была выпущена 23 января 1996 года (некоторые сказали бы — «удрала в мир разработки»). Сейчас ей уже более 25 лет! За это время язык развивался, a Java API значительно разросся. По самым приблизительным оценкам, за эти годы было написано более 17 миллиардов строк кода на Java. Программируя на Java, вы наверняка столкнетесь и с довольно старым кодом, и с гораздо более новым. Одно из главных преимуществ языка — обратная совместимость, благодаря которой старый код может работать и на современных JVM. В этой книге мы сначала показываем старые стили кодирования — потому что в реальной рабо¬ те вам, скорее всего, придется их встретить. Затем демонстрируем новый, современный стиль. Такой же подход мы применяем и к Java API: сначала знакомимся со старыми классами, а затем показываем их новые альтернативы. Мне говорили, что Java медленнее работает по сравнению с компилируемыми языками, такими как С и Rust. Скорость и использование памяти Когда Java только появилась, она была медленной. Но вскоре на сцену вышла виртуальная машина HotSpot VM и другие технологии, которые значительно повысили ее производительность. Конечно, Java не самый быстрый язык. Но при этом она от¬ носится к очень быстрым — почти таким же, как С и Rust, и гораздо быстрее большинства других языков. Главная суперсила Java — это JVM. Виртуальная машина оптимизирует код прямо во время его выполнения, поэто¬ му можно создавать быстрые приложения без необходи¬ мости вручную писать высокопроизводительный код. Но... срываем маски. По сравнению с С и Rust Java потре¬ бляет много памяти.
Посмотрите, как легко писать на Java аточите карандаш Ответы на стр. 42. int size = 27; String name = "Фило"; Dog myDog = new Dog(name, size); x = size - 5; if (x < 15) myDog.bark (8) ; while (x > 3) { myDog. pi ay () ; int[] numList = (2, 4, 6, 8}; System.out.print("Привет"); System.out.print("Пес: " + name); String num = "8"; int z = Integer.parselnt(num); try I readTheFile("myFile.txt"); ) catch (FileNotFoundException ex) { System.out.print("Нет такого файла."); Попробуйте догадаться, за что отвечает каждая строка кода, (ответы — на следующей странице). 6г1д(?кнш 1Щ1С/нс«нт'цп1^ыцнншмд » lymrfomu ш^нгтнил 21 few jHiffCKiu hc^uuhhui .v fmmjuM lyuidWt jnamwt 22).«сннш 15, ttwKii ii/K vum. S )ш бм&дcw&i Привет... tu, Ikumh^hcu страх. (He) дурацкие вопросы ВоирОС: Нумерация версий Java выглядит запутанной. Сначала были JDK 1.0,1.2,1.3,1.4, затем появился J2SE 5.0, потом стали называть просто Java б, Java 7, а когда я последний раз проверял, самой свежей версией была Java 18. Что вообще происходит? OwBewi: Нумерация версий сильно менялась за последние 25 с лишним лет! Буквенные обозначения (J2SE/SE) сейчас не используются, так что их можно игнорировать. С числами все немного сложнее. Технически Java SE 5.0 — это на самом деле Java 1.5, а аналогично: Java 6 -> 1.6, Java 7 -> 1.7, Java 8 -> 1.8. То есть, по сути, все еще остается линейка Java 1 .х, потому что каждая новая версия сохраняет обратную совместимость вплоть до Java 1.0. Но начиная с Java 9, официальная нумерация изменилась: Java 9 — это действительно девятая версия, а не 1.9. В этой книге мы будем использовать нумерацию с префиксом «1» для версий 1.0-1.4, а начиная с Java 5 будем писать без «1». К тому же с момента выхода Java 9 в сентябре 2017 года релизы стали выходить каждые 6 месяцев, поэтому переход с Java 9 до Java 18 произошел так быстро!
^точите карандаш (ответы) Посмотрите, как легко писать на lava НепеРе*ивайте'еслип°каничегонеп°ни"аете! На первых ’ ' 40 страницах книги подробно объясняется все, что здесь напи¬ сано. Если Java напоминает язык, которым вы уже пользовались раньше, то понять некоторые моменты будет для вас довольно просто. Если нет — не волнуйтесь. Мы все объясним... int size = 27; String name = "Фидо"; Dog myDog = new Dog(name, size); x = size - 5; if (x < 15) myDog.bark (8) ; while (x > 3) { myDog. pi a у () ; 1 int[] numList = {2, 4, 6, 8}; System.out.print("Привет"); System.out.print("Пес: " + name); String num = "8"; int z = Integer.parselnt(num); try { readTheFile("myFile.txt") ; ) catch (FileNotFoundException ex) { System.out.print("Нет такого файла."); ('гтлбкнж и/, 1г/иси.ннтй щ^еишксй лсеи>ущсй<нш икцшитл И ОпМиШ rm/\(К( fill nCftUUHHlH (МИК И K/nufltHUt lit ЛЫ’ЛтЧ 7'UCji . СгЛя(itt иш ней» ж/шннжи с iiwntii my/.Ц mnut-pfn mmt и /кгнпшншну 21 ^шчшш size) н lymrfitHW fujif,шпата шреишнен x. ('em тнохнш пелешшем x kemefm nfme&mc липшие 22} .шише 15, size лыка njic мни S pay. /inm. mi HRi |щк. ш cji mu mp, ruKit x > KcMancjti сссакс и фат/ (mu m. Э7С ни pumeuj \uj_ Осщтюш конца цикле—fit, пт шшжтгя ${}. fimt-owemei 6 цикле. Сш£iiи»! списка numtitf »./сеаСиине (?пси 2. 4 t. У cw&i .Jltmicm... ,m (?каштане» гпуш. /infi.HdniniiHT Ли: ‘/’и.н fa mu = Фшр;) (?кс.н<ниун( it страл. Ош&тис ш/икчАи и^иленней пит и. lyutrfimn titуют пил 5. }{шш1л(ш{ш.с>>\1ики -5 Gmciо 5. Ямтпка fiuui-iHHmt тотии... бущ.ит. тс. яш .«nijt-ши feme нс (fmxmatm Уттш тешмйзе фсииа mifJi(k.txt fa-rn лшя см пгпшпка этс сдиапи...). Лсуожс. шпажки ncnfwtwimt пт-пи л/иат/уакянтАпс-тсл.. Cnpufciatiu. срассташ.ш тЦацим, тиснили fiuu. тящ. (cm шиитка нс Gnfiu] (ш.нажунси гт/икч «Кт такси Bta mcj (hifmjm (}—это та гте йже-тягоиа. tew, rmpatwp Щ ш фатщ. }
Структура кода в Java class definition fj method 1 statement method 2 statement statement Что происходит внутри исходного файла? public class Dog { Файл исходного кода (с расшире¬ нием .java) содержит определение одного класса. Файл с расширени¬ ем .java может содержать несколь¬ ко классов, но только один из них может быть public, и имя этого класса должно совпадать с именем файла. Класс — это часть вашей программы, хотя совсем малень¬ кому приложению может хватить и одного-единственного класса. Содержимое класса заключается в пару фигурных скобок. } Класс В исходный файл добавляем класс В класс добавляем метод В метод добавляем выражения Что происходит внутри класса? В классе могут быть как один, так и несколько методов. Метод bark в классе dog содержит инструк¬ ции, определяющие лай собаки. Методы необходимо объявлять внутри класса (то есть между его фигурными скобками). public class Dog { void bark() { } 1 Метод Что происходит внутри метода? Инструкции по выполнению для метода должны быть разме¬ щены внутри фигурных скобок. По сути, код метода — это набор выражений. Чтобы не усложнять, представьте метод как функцию или процедуру. public class Dog { void bark() { statementl; statement2; } ) Операторы
Структура класса При запуске JVM ищет класс, указанный вами в командной строке. Затем она ищет метод с определенной сигнатурой, который выглядит примерно так: public static void main (String[] args) { // Здесь размещается ваш код Найдя нужный метод, JVM выполняет код, который находится внутри фигур¬ ных скобок { } метода main. Любая программа или приложение, написанные на Java, обязательно содержат хотя бы один класс и один метод main. Причем main не обязателен в каждом классе — он один на все приложение, и именно с него все начинается. Эта ел*, «а tyuyftmn cjami St\ тамкс начинаете. cdrnjet nfiujnna&itHut. нс Hipmc бхэтс^апаиинат* nfm uc сейгас.
Написание класса с методом там() Весь код в Java организован в классы. Вы набираете файл исходного кода (с расширением . java), затем компилируете его в новый файл класса (с расширением . class). Запуская программу на Java, вы тем самым запускаете класс. Запустить программу — значит дать команду виртуальной машине Java (JVM): «Загрузи класс Му Fir stApp и выполняй его метод main (). Продолжи выполнение, до конца кода в main». Подробным изучением всего, что связано с классами, займемся в главе 2. Единственный во¬ прос, который вам нужно задать, звучит так: как написать код на языке Java так, чтобы он выполнялся? Начало всего — main (). Метод main () — это то, с чего начинается выполнение программы. Не имеет значения, как велика программа (другими словами, количество классов, содержа щихся в программе), должен быть метод main (). Это механизм, отвечающий за запуск кода. public class HyfustAjp ( public static void cam (Stringcl argsl Syst«.out.print(*Bea ир!*); Systee.cut.pruitlnt'B i*w pytax’l; Сохраните MyFirstApp.java 1 |Сс.А1ШМ,Л>П1(' Сксипилировазо ю «НуГпздАдо. java» public class chi.MyFirstApp ( public chl.MyfirstAfpO; Кал: 0: tload O 1: invoiespecial il // yva/lang/Cb]ect.»<iiut>i: ()' public static void ldinljavi.lang. Stni4lJ); MyFirstApp.class Pile Edit Window Help Scream %java MyFirstApp Весь мир! В моих руках MyFirstApp.java О ■ Скомпилируйте ^vac MyFirstApp. java ■ Запустите java MyFirstApp public class MyFirstApp ( public static void main (String[] args) { System.out.println("Becb мир!"); System.out.println("В моих руках"); }
Беседы у камина Сегодняшний разговор: Компилятор и DVM обсуж¬ дают, кто из них важнее. Виртуальная машина DVM Ты шутишь? ПРИВЕТ. Я — Java. Я та, кто по-настоящему запускает программу. Компи¬ лятор? Он просто создает файл. И все. Просто файл. Его можно повесить на стену, поджечь, использовать для обшивки птичьей клетки — но пока я его не выполню, он ничего не делает. Ах да, у компилятора же нет чувства юмора. Оно и понятно — целыми днями проверять запятые и точку с запятой... Компилятор Мне не нравится твой тон. Прости, но без меня что ты вообще собира¬ ешься запускать? На минуточку, Java изначаль¬ но создавалась как язык, использующий ком¬ пиляцию в байт-код. Если бы Java была чисто интерпретируемым языком, который выпол¬ няет код прямо из текстового файла, програм¬ ма работала бы смехотворно медленно. Прости, но это невежественная (и к тому же высокомерная)точка зрения. Я не говорю, что ты совершенно бесполезен. Но, честно — что ты вообще делаешь? Серьез¬ но. Я бы могла выполнять байт-код, написан¬ ный вручную. Представляешь? Скоро ты мне вообще не понадобишься, дружище. (Ладно, шутки в сторону.) Но ты так и не от¬ ветил: что ты реально делаешь? Теоретически — да, ты можешь запустить лю¬ бой корректно оформленный байт-код, даже если он написан не мной. Но на практике это абсурд. Писать байт-код вручную — все равно что рисовать картины со своего отпуска вме¬ сто того, чтобы просто сделать фото. Конечно, это искусство, но большинство людей пред¬ почли бы сэкономить время. И пожалуйста, не называй меня «дружище». Вспомни, Java — это язык со строгой типиза¬ цией. Я не допускаю, чтобы переменные хра¬ нили данные неправильного типа. Это важно для безопасности, и благодаря мне большин¬ ство ошибок даже не доходят до тебя. А еше я . .
Виртуальная машина DVM Некоторые ошибки все равно проскальзыва¬ ют! Да, мы отбросим ClassCastException, но находятся такие умельцы, которые пытают¬ ся запихнуть в массив не того типа что-нибудь совершенно неподходящее... Ладно, я поняла. Но как насчет безопасности? Посмотри, чем занимаюсь я! А ты что, про¬ веряешь точки с запятой? Ну вау, вот это уро¬ вень угрозы... Настоящий Защитник, ничего не скажешь! Ох, не начинай... Мне ведь тоже приходится отлавливать последствия, если кто-то изменит байт-код после твоей компиляции. Я должна защитить программу на последнем рубеже. Конечно. До следующего апдейта... приятель. Компилятор Я еще не закончил. Да, во время выполнения действительно могут возникнуть ошибки, свя¬ занные с типами, но некоторые из них допу¬ скаются осознанно — чтобы сохранить другую важную черту Java: динамическое связывание. Программа на Java может включать новые объ¬ екты прямо во время выполнения, о которых ее автор и не подозревал. Так что я должен быть немного гибким. Но при этом я обязан защитить программу от кода, который точно не сработает. Если программист вдруг решит использовать Button в роли Socket — я найду это место и уберегу систему от краха. Вообще-то я — первая линия обороны. Ошиб¬ ки, связанные с типами, могут разрушить всю логику программы, если их не остановить вовремя. Именно я не допускаю нарушения доступа: когда код пытается вызвать приватный метод, или изменить метод, который по правилам безопасности вообще не должен трогаться или сунуться к критическим данным другого класса. И все это — до того, как ты начнешь что-то выполнять. 1Моя работа — не просто важна. Ее можно объяснять часами. Или днями. Согласен. Но если бы я не устранял 99% всех потенциальных проблем еще до запуска, твоя работа даже не началась бы. Пожалуй, тут не поспоришь. Ладно, разговор был жаркий, но пора закругляться. У нас таймер.
Что поместить внутри основного метода? Как только вы окажетесь внутри main (или любого другого метода), начинается самое интересное. Дать компьютеру ко¬ манду выполнить что-то можно любыми средствами, знако¬ мыми вам по работе с другими языками программирования. Ваш код может дать JVM следующие команды: Сделать что-то Выражения: объявления, присваивания, вызовы методов и другие. int х = 3; String паше = "Кинжал"; х - х * 17; System.out.print("х равен " + х) ; double d = Math.random(); // Это комментарий © Делать что-то снова и снова Циклы: for и while while (х > 12) ( х = х - 1; ) for (int i = 0; i < 10; i = i + l) ( System.out.print("Теперь значение i равно " + i); © Сделать что-то при условии Ветвление: условия if/else if (х == 10) ( System.out.print("x должен быть равен 10"); ) else { System.out.print("х не равен 10"); f if ( (x < 3) && (name.equals ("Дирк") ) ) { System.out.println("Джентли"); ( System.out.print("Эта строка выполняется в любом случае"); ^ В конце каждого оператора указы¬ вается точка с запятой. х = х + 1; ^ Однострочный комментарий на¬ чинается двумя косыми чертами. х = 22; // Эта строка меня тревожит 'А' Многократные пробельные сим¬ волы игнорируются. х = 3 'А' Переменные объявляются с указа¬ нием имени и типа (все типы, доступ¬ ные в Java рассматриваются в главе 3). int weight; //Тип: int, имя: weight ^ Классы и методы должны объ¬ являться внутри парных фигурных скобок. public void go() { // Здесь будет офигенский код }
while (moreBalls -= true) { keepJugglingQ; Простые логические проверки Простые логические проверки можно выполнять, используя оператор сравнения, например: < (меньше, чем) > (больше, чем) == (равно) (да, да, это два знака равенства). Обратите внимание на разницу между оператором присваивания (один знак равенства) и оператором сравнения (два знака равенства). Часто возникает ситуация, когда программисты случайно набирают =, когда хотят ==. (Но к вам это не относится.) int х = 4; // присваиваем х значение 4 while (х > 3) ( Зацикливай, и зацикливай, и... В Java есть множество способов задать повторение кода — или, как говорят, организовать цикл. Самые старые и проверенные временем конструкции — это while, do-while и for. Позже в книге мы рас¬ смотрим все их варианты и особенности. А сейчас давайте начнем с самого простого — цикла while. Синтаксис цикла while (да и сама логика) макси¬ мально понятны. Пока условие истинно, выполня¬ ется код, заключенный в блок. Границы блока — это пара фигурных скобок {}. Все, что вы хотите повторять снова и снова, должно быть внутри этих скобок. // код в цикле выполнится, потому что II х больше 3 х = х - 1; // иначе цикл никогда не завершится J int z = 27; // while (z == 17) { II код в цикле не выполнится, потому что II г не равно 17 Проверка условия — это ключ к работе цикла. В Java условие в while — это логическое выражение, которое возвращает значение true (истина) или false (ложь). Представьте себе код: Пока IceCreamlnTheTub — продолжать накладывать мороженое. Это четкий логический тест: либо мороженое в упаковке есть, либо нет. Два возможных ответа — и ни капельки двусмысленности. Но если написать: Пока Боб продолжает наклады¬ вать... — это не подходит: вы не проверяете состоя¬ ние, а описываете процесс. Нужно переформулировать: Пока Боб храпит... — отлично. Это можно проверить. Или: Пока Боб не укутался в плед... — тоже подойдет. Логические выражения — это про да или нет, ис¬ тину или ложь. И именно это позволяет Java понять, зацикливать код дальше — или уже пора остано¬ виться.
(Не) дурацкие вопросы Пример цикла while ВоК рОС: Почему все в Java нужно оборачивать в класс? Oi/ивеж: Потому что Java — это объектно-ориентированный язык. Мы больше не пишем монолитные программы, состоя¬ щие из одной длинной простыни процедур, как это было в старых языках. В главе 2 вы узнаете, что класс — это шаблон для создания объек¬ та, и практически все в Java так или иначе связано с объектами. ВОК рОС: Нужно ли добавлять метод main() в каждый класс? Oi/ивеж.- Нет. Программа на Java может состоять из десятков или даже сотен классов, но до¬ статочно одного класса с мето¬ дом main(). Именно он служит точкой входа, то есть с него начинается выполнение. public class Loopy { public static void main(String[] args) { int x = 1; System.out.println("Перед циклом"); while (x < 4) { System.out.println("В цикле"); System.out.println("Значение x равно " + x); x = x + 1; } System.out.println("После цикла"); I i % j ava Loopy Перед циклом В цикле Значение х равно 1 В цикле Значение х равно 2 В цикле Значение х равно 3 После цикла э, >П с . п^$'"т‘пп ц,4к Вок рос: В моем языке программирования можно делать логическую проверку целого числа. А в Java можно написать так? int х = 1; while (х){ ) Oi/ивеж.- Нет. В Java логиче¬ ский (boolean) и целочисленный (int) типы несовместимы. Условие в цикле while (как и в if) обязательно должно возвращать true или false. Вы можете проверять только переменные типа boolean, если не используете явное сравнение. Например: boolean isHot = true; while(isHot) { } ' — Без лишних <ЛОВ — — ■ Операторы заканчиваются точкой с запятой ;. ■ Блоки кода заключаются в пары фигурных скобок {}. ■ Целочисленная переменная объявляется с типом и именем, напри¬ мер int х;. ■ Оператор присваивания состоит из одинарного символа равно: =. ■ В операторе сравнения используется два символа равно: ==. ■ Цикл while выполняет код, находящийся внутри его блока (заданного фигурными скобками), пока проверяемое условие результирует как true. ■ Если проверяемое условие результирует как false, то блок кода внутри цикла while не выполнится, a JVM проследует далее по коду и начнет выполнять выражение, находящееся сразу после блока с циклом. ■ Условие размещается внутри скобок: while (х == 4) { }
Условное ветвление В языке Java проверка условия в операторе if ничем не от¬ личается от логического теста в while: и там, и там нужно бу¬ лево выражение, которое возвращает true или false. Только формулировка другая: вместо «Пока шоколад еще есть...» пишем: «Если шоколад еще есть...» class IfTest { public static void main (String!] args) { int x = 3; if (x == 3) { System.out.println("x равен 3"); System.out.println("Эта строка выполняется в любом случае"); > ) java IfTest х равен 3 System.OMt.PRINT или Systewi.oMt.pnwtlN? Если вы внимательно разбирали примеры (в чем мы совершенно уверены), то наверняка обратили внимание, что мы переключились с оператора print на println. Вы заметили разницу? System.out.println вставляет новую строку (println означает printnewline), если же применить System.out.print, то вывод продолжится в той же стро¬ ке. Если вам нужно, чтобы каждый результат выводился в новой строке, используйте println. Если результат необходимо вывести в одной строке, используйте print. Эта строка выполняется в любом случае Код, приведенный выше на этой странице, выведет строку «х равен 3», только если условие (х == 3) истинно. Текст «Эта строка выполняется в любом случае» выводится вне зависимости от выполнения/невыполнения условия. То есть будет выведена или одна или две строки и зависит это от значения переменной х. К условию можно добавить оператор else, тогда фраза в пере¬ воде с программного языка будет звучать так: «Если шоколад еще есть — продолжайте кодировать, иначе — возьмите еще шоколада и продолжайте!». class IfTest2 { public static void main(String[] args) { int x = 2; if (x == 3) { System.out.println("x равен 3"); ] else { System.out.println ("x HE равен 3"); ) System.out.println("Эта строка выполняется в любом случае"); } ) Руумтат изменижя % java IfTest2 х НЕ равен 3 Эта строка выполняется в любом случае ^*%Зат0ЧИте карандаш Текст, который нужно вывести: % java DooBee ДуЕиДуЕцДи Впишите недостающий код: public class DooBee { public static void main(String[] args) int x = 1; while (x < ) ( ("Ду"); ("Би") ; System.out._ System.out._ x = x + 1; i if (x = ) { System.out.print("Ди"); Ответы иа стр. 61.
Разработка серьезного бизнес-приложения Первую часть знаний о том, как программировать на Java вы получили, попробуйте применить их на практи¬ ке. Создайте класс с методом main(), целочисленными и строковыми переменными, циклом whiles операто¬ ром if. Добавьте глянца, и вы быстро создадите бекэнд для бизнеса. Но прежде чем взглянуть на код, располо¬ женный на этой странице, вспомните детскую песенку «10 зеленых бутылок». И подумайте, как написать ее языком Java. public class BottleSong { public static void main(String[] args) { int bottlesNum = 10; String word = "бутылок"; while (bottlesNum > 0) { if (bottlesNum == 1) { word = "бутылка"; // единственное число } System.out.println(bottlesNum + " зеленых " + word + ", на стене"); System.out.println(bottlesNum + " зеленых " + word + ", на стене"); System.out.println("И если одна бутылка случайно упадет,"); bottlesNum = bottlesNum - 1; if (bottlesNum > 0) { System.out.println("Останется " + bottlesNum + " зеленых " + word + ", на стене"); ) else { System.out.println("He осталось зеленых бутылок на стене"); ) // конец ветви else ) // конец цикла while ) // конец метода main ) // конец класса В нашем коде есть один маленький недо¬ статок. Он компилируется и запускается, но результат не на 100% идеальный. Попробуй¬ те обнаружить недостаток и исправить его. (Не) дурацкие вопросы ЗОИрОС: Разве песня не называется «99 бутылок пива»? ОшВеРИ: Да, именно так! Но Триша настояла на британской версии — про зеленые бутылки на стене. Если вам ближе «99 бутылок пива», просто представьте это как забавное упражнение с локализацией. В конце концов, и те, и другие бутылки умеют падать.
Одно буднее утро в доме Боба, где правит Java Утро понедельника (впрочем, как и любого буднего дня) начинается у Боба со звонка будильника в 8:30. Но после бурных выходных вста¬ вать совсем не хочется, поэтому рука автоматически тянется к кнопке «Отложить» — еще 12 минут сна. Но у Боба не просто будильник: у него целая экосистема Java-устройств, которые тут же начинают обмениваться сообщениями. Будильник отправляет команду кофеварке: «Эй, гик опять уснул, отложи кофе на 12 минут». Кофеварка сообщает тостеру Motorola”: «Не спеши с тостами, Боб решил подремать». Лсу ynfuditmuM Java <r^ Будильник пингует Android-смартфон Боба: «Позвони хозяину в 9:00 и напомни, что он уже опаздывает». А напоследок отправляет команду на беспро¬ водной ошейник Сэма (Сэм — это пес): «Боб не идет гулять, но журнал принести надо». Проходит 12 минут, будильник снова звонит. И снова Боб тянется к кнопке «Отложить», заставляя технику вновь переговариваться. Еще один цикл, еще одна цепочка сообще- Л(р ний... Но в третий раз система срабатывает иначе: будильник отправляет на ошейник Сэма команду «Прыгай и лай!». у' Результат? Боб в секунду пробуждается, понимая, что технологии — это, конечно, круто, но порой лучше не полагаться на автопилот. Зато он точно знает: знания Java и спонтанные покупки в ин¬ тернете сделали его утренние ритуалы по-настоящему эффектив¬ ными. Ну, почти. Жаслс макать асе Тост поджарен. I Кофе сварен. fol Itiva ^ ava Журнал доставлен. Одно чудесное утро в доме, где правит Java. Может ли эта история быть правдой? По большей части — да! Мобильные теле¬ фоны (особенно), банкоматы, кредитные карты, домашние охранные системы, парковочные автоматы, игровые приставки и множество других устройств могут работать на Java. А вот ошейники на Java вы вряд ли найдете... пока что. Java предлагает разные варианты для использования урезанных версий плат¬ формы, подходящих для маленьких устройств; все зависит от того, какую версию Java вы используете. Этот язык очень популярен в разработке решений для 1оТ (Интернета вещей). А еще на Java и других языках JVM активно разрабатываются приложения для Android — так что Java живет буквально в вашем кармане.
Песенку про бутылки, конечно, трудно назвать серьезным бизнес- приложением. Но если вам все-таки нужен практичный пример кода, который можно показать начальству (или хотя бы сделать вид, что вы пи¬ шете что-то очень важное), обратите внимание на генератор фраз. public class PhraseOMatic { public static void main (String!] args) { // создаем три набора слов. Можете добавить свои! String!] wordListOne = ! "безопасный", "гибкий", "интерактивный", "автоматизированный", "облачный", "встроенный", "отказоустойчивый", "адаптивный", "многопоточный", "интеллектуальный" ); String!] wordListTwo = ( "в микросервисной архитектуре", "с нативной поддержкой облака" "на событийной модели", "с высокой доступностью", "с низкой задержкой", "с масштабируемым подходом", "на базе контейнеров", "с нулевой точкой отказа", "в распределенной среде", "с реактивной обработкой событий", "с полной автоматизацией", "с зашитой на уровне ядра" }; О String!] wordListThree = ( "сервис", "фреймворк", "модуль", "конвейер", "репозиторий", "шаблон", "интерфейс", "решатель", "движок", насирапи киу вресуактеуи. псуувслстериуактсри самищ пертссить стуюки. Ликегаа не нажимайте клавишу Шсх. пока насушим строку пикета в кавтках: этомошт п/швссти к сши/кс кие шил и,ше. Лсуижт стрск, ксторш вп вшуите в книге, настоящие, их можно надирами как есть. Лс ас mot пер, пека строка нс уакенпна. нс встав шипи перевесу строки вругнун. О о о }; // узнаем длину каждого списка int oneLength = wordListOne.length; int twoLength = wordListTwo.length; int threeLength = wordListThree.length; // генерируем три случайных числа java.util.Random randomGenerator = new java.util.Random(); int randl = randomGenerator.nextlnt(oneLength); int rar.d2 = randomGenerator.nextlnt(twoLength); int rar.d3 = randomGenerator.nextlnt(threeLength); // составляем фразу String phrase = wordListOne[randl] + " " + wordListTwo[rand2] + " " + wordListThree[randl]; // выводим фразу System.out.println("Нам нужно: " + phrase); )
Фразоплет ЗООО Разбираем по винтикам Программа составляет три списка слов, затем случайным образом выбирает по одному слову из каждого списка и выводит результат. Если вы пока не понимаете, какие дей¬ ствия описаны в каждой строке — не переживайте. Впереди целая книга, и вы обяза¬ тельно во всем разберетесь. Это лишь беглый взгляд... с высоты 7 000 над землей. I. Для начала нужно создать три строковых массива: это контейнеры, в которых будут храниться слова. Объявить и создать массив в Java очень просто. Вот пример: String[] pets = {"Фидо", "Зевс", "Бобик"}; Каждое слово заключено в кавычки (как и положено хорошим строкам) и разделено за¬ пятыми. 2. Чтобы выбрать случайное слово из списка, нужно знать, сколько слов в нем нахо¬ дится. Если в массиве 14 слов, допустимые индексы — от 0 до 13 (в Java отсчет начинается с нуля). То есть: первое слово — в позиции 0, второе — в позиции 1, а последнее — в позиции 13. Java с легкостью подскажет длину массива. Просто спросите: int х = pets.length; Теперь переменная х содержит значение 3. интерактивный с нулевой точкой отказа конвейер многопоточный с полной ав¬ томатизацией фреймворк 3. Нам нужны три случайных числа. Вместе с Java идет набор инструментов для ге¬ нерации случайных чисел, включая j ava .util .Random. (Позже вы узнаете, почему в названии используется префикс j ava . util.) Метод nextlnt () возвращает случай¬ ное число в диапазоне от 0 до некоторого числа, которое мы ему зададим, не включая то, которое мы ему зададим. Поэтому мы зададим ему количество элементов (длина мас¬ сива) в нашем списке. Затем мы присваиваем каждый результат новой переменной. Мы могли бы с тем же успехом попросить выдать случайное число от 0 до 5, не включая 5: int х = randomGenerator.nextlnt(5) ; гибкий на собы¬ тийной модели репозиторий безопасный с за¬ щитой на уровне ядра шаблон *Т. Теперь начнем строить фразу. Выбираем по одному слову из каждого списка и объ¬ единяем их, добавляя пробелы. Мы используем оператор +, который конкатенирует (мы предпочитаем более техничное сглаживает) объекты типа String вместе. Чтобы получить элемент массива, задайте массиву номер индекса (позицию) той сущности, которая вам нужна: String s = pets[0]; // теперь s содержит строку "Фидо" s = s + " " + собака"; // теперь s содержит "Фидо — собака" 5адаптивный с высокой до¬ ступностью интерфейс гибкий на собы¬ тийной модели алгоритм 5. И, наконец, — выводим фразу в консоль. Идеально! Осталось купить стикеры и по¬ весить это на стену.
^Упраж, ражнение Магнитики с кодом На холодильнике в беспорядке разбросан код ра¬ бочей Java-программы. Фрагменты перепутались; кажется, будто кто-то играл ими как магнитиками. Ваша задача — переставить кусочки кода в пра¬ вильном порядке, чтобы получилась рабочая про¬ грамма. Она должна скомпилироваться и вывести нужный результат. Несколько фигурных скобок при этом, увы, упали на пол и затерялись... Так что добавляйте их столько, сколько потребуется! ^System, out.print ("б в"); } class shuff lei { public static void main(String [j Результат I File Ed* Window Help Sleep % java Shufflel а-б в-г ♦ Ответы на стр. 61
ражнение Компилируй Каждый из Java-файлов на этой странице — это полный исходный код. Ваша задача — примерить на себя роль компилятора и решить: скомпилируется ли этот файл? Если какой-то из файлов не пройдет компиляцию, подумайте, почему, и предложите, как можно его исправить. Ответы на стр. 61 public static void main(String[] args) { int x = 5; while (x > 1) { x = x - 1; if (x < 3) { System.out.println("маленькое x"); 1 ) } A class Exercisela { public static void main(String[] args) { int x = 1; while (x < 10) { if (x > 3) { System.out.println("большое x"); } В class Exerciselc { int x = 5; while (x > 1) { x = x - 1; • if (x < 3) { System.out.println("маленькое x"); )
Предлагаем разминку для правого полу¬ шария. Это обычный кроссворд посвя¬ щенный теме Java. Но чтобы усложнить задачу, мы добавили несколько слов (не относящихся к Java) из мира информа¬ ционных технологий. По горизонтали: 2. Оформление текста или интерфейса 3. Группа строк кода, объединенных логически 4. Метод, создающий объект класса 8. Инструкция к серверу 11. Идентификатор переменной, метода или класса 12. Единица измерения времени 13. Область памяти для динамического хранения 2 5 9 14 1 6 13 10 -- 3 12 — 4 7 19 11 17 18 8 16 20 ; 15 15. Конкретное значение, которое можно вычислить 17. Позиция элемента в структуре 20. Видимое прямоугольное простран¬ ство интерфейса По вертикали 1. Повторяющаяся последовательность действий 4. Структура, сопоставляющая ключи и значения 5. Принцип ООП: сокрытие внутреннего устройства б. Расположение элементов на экране 7. Совокупность элементов одной группы 9. Активный элемент интерфейса 10. Переменная, увеличивающаяся с каждой итерацией 14. Знак, обозначающий координату или границу 16. Удаление неиспользуемых объектов из памяти 18. Число с десятичной частью 19. Минимальная единица синтаксического анализа ♦ Ответы на стр. 62.
Противоречивые результаты Ниже — пример небольшой программы на Java. Один из блоков кода потерялся. Ваша задача — найти его и понять, что именно он делает. В левом столбце — фрагменты кода, которые могли бы подойти. В правом столбце — строки, которые могут появиться в выводе командной строки, если вставить соответствующий блок. Некоторые строки вывода могут использоваться несколько раз, а другие вообще не пригодятся. Ваша цель —соединитьблоки кода с соответствующим выводом. class Test { public static void main(String [] args) { int x = 0; int у = 0; while (x < 5) { • Htcmc a im «ставки с (иска коаа System.out.print(x + "" + у +" "); x = x + 1 ; Candidates: Possible output: CtUjUHAlUU у = X - у; 22 46 дикие.» tjMu гйск Kxcja 1«VI 11 nm/'l Ilk 1*11 > II >i + х; 11 34 59 ( v i v, / Ш Ш, К f ; (icmuaHUutuu У = У + 2; ptjij.umnn\a. if (у > 4 ) { 02 14 26 38 У = У - 1; ) 02 14 36 48 X = X + 1; 00 11 21 32 42 У = У + х; if ( У < 5 ) { x = x + 1; if ( у < 3 ) { x = x - 1 ; ) } У = У + 2; 11 21 32 42 53 00 11 23 36 410 02 14 25 36 47 _► Ответы на стр. 62*
бассейн фрагментов В бассейне плавают фрагменты кода. Ваша задача — разместить их в пустых строках, чтобы про¬ грамма скомпилировалась, за¬ пустилась и вывела правильный результат. Важно: Некоторые фрагменты не понадобятся, а остальные можно использовать только один раз. Цель упражнения — собрать рабочий класс, результат которого будет точно совпадать с образцом вывода, приведенным ниже. Не обманывайтесь простотой: это задание сложнее, чем кажется на первый взгляд. Ответы на стр. 62. class PoolPuzzleOne { public static void main(String[] args) { int x = 0; while ( ) { if (x < 1) { } if ( ) { i if (x == 1) { ) if ( ) ( Результат %java PoolPuzzleOne он шум монстр анчоусное J System.out.print In(); } } System.out.printfujyM ”); System.out.printfyc"); System.out.printf монстр ”); System.out.print(”aH4oyc”); System.out.printC'Hoc”); System.out.print(" "); System.out.print("H"); System. out.print("Tbi "); System.out. print("oH"); Примечание: Каждый фрагмент кода из бассейна можно использо¬ вать один раз!
Ответы Заточите карандаш (стр. 51) public class DooBee { public static void main(String[] args) int x = 1; while (x < 3) { System.out.print("Ду"); System.out.print("Bw"); x = x + 1; f if (x = 3) { System.out.print("flM"); } } ) Магнитики с кодом (стр. 56) class Shufflel ( public static void main{String[] args) int x = 3; while (x > 0) { if (x > 2) ( System.out.print("a"); } x = X - 1 ; System.out.print; if (x == 2) ( System.out.print("б в"); 1 if (x == 1) ( System.out.print("r"); x = x - 1 ; } ) ) i яруйте class Exercisela ( public static void main (String [ ] args) { 4 5 rg 5 R int x = 1; while (x < 10) ( /kmtim- 9nvi| строку. о x = x + 1; ___ TllU/ n ЧДК-1 Hi fetud- Ы -if if (x > 3) { HSLACA &THC... А System.out.println("большое x"); t Приведенный код скомпилируется и запустится (без вывода), но, если не добавить строку, он будет работать вечно в бесконечном цикле while! class Exerciselb { ЫЩЖт с^йши кше public static void main(String!] args) f int x = 5; while (x > 1) { x = x - 1; if (x < 3) { Б System.out.println("маленькое x"); I Если не объявить класс, } код не скомпилируется. Не забудьте про фигурные скобки! (Щшн. тшп 0 class Exerciselc public static void main(String [] args) { int x = 5; while (x > 1) ( x = x - 1; if (x < 3) { System.out.println("маленькое x"); | File Edit Window Help Pod % java Shufflel а-б в-г Код цикла while должен находиться внутри метода. Его нельзя разместить внутри класса.
бассейн фрагментов (стр. 60) class PoolPuzzleOne ( public static void main(String[] args) int x = 0; while (x < 4) { System.out.print ("я") ; if (x < 1) { System.out.print(" "); 1 System.out.print("ты"); if (x > 1) { System.out.print (" монстр"); x = x + 2; ) if (x == 1) { System.out.print("шум"); ) if (x < 1) { System.out.print("yc"); I System.out.println(); x = x + 1; 1 Кроссворд (стр. 58) class Test { public static void main(String[] args) { int x = 0; *> int у = 0; 5 со while (x < 5) ( s 5 £ И со Е | §> Д-з С о. > i System.out.print(x + "" + у + ” ”); X = X + 1; Candidates: Possible output: у - у + 2; (стр. 23)
2. Классы и объекты Путешествие в Объектино Мы едем в Объектино! Мы навсегда покидаем старый пыльный процедурный город! Мы пришлем вам открытку! Мне сказали, что в Объектино есть объекты! в первой главе весь код был размещен в методе main(). Ну, так, для старта, конечно. Но это далеко не объектно-ориентированный подход! Признаемся, мы использовали пару объектов, например, строковые массивы для генератора фраз, но своих собственных объектов не создали. А теперь — прощай, мир процедур! Мы выходим из тесных рамок метода main() и начинаем создавать собственные объекты. Да, с этого момента мы не просто программируем — мы объектируем! Мы расскажем обо всех аспектах ООП на Java, которые делают его таким увлекательным. Мы разберемся, чем класс отличается от объекта и как это может улучшить жизнь программиста. (Но, если честно, научить вас чувствовать стиль мы, наверное, не сможем.) Предупреждаем: как только вы окажетесь в Объектино, вы не захотите вернуться в старый процедурный мир. Так что присылайте нам открытки из этого замечательного мира, где объекты живут и работают в полной гармонии!
Битва за кресло (или как объекты могут изменить вашу жизнь) /Л ■ J днажды в одной компании, разрабатывающей программ- \_/ ное обеспечение, двум программистам дали одно и то же техническое задание и попросили: «Выполните его». Это было соревнование, придуманное особенно назойли¬ вым руководителем проекта. Победитель получит крутое кресло Аегоп” и стол с регулировкой по высоте — как у всех настоящих технарей в Кремниевой долине. Оба участника — Лора, сторонница процедурного подхода, и Брэд, объектно-ориентированный разработчик, — были уверены, что справятся без труда. Лора, сидя за своим (нерегулируемым) столом, размышляла: «Что должна делать эта программа? Какие процедуры нуж¬ ны?» И тут же нашла ответ: rotate и playSound. Она сразу приступила к написанию процедур. В конце кон¬ цов, что такое программа, если не куча процедур? Брэд тем временем откинулся в кресле в кофейне и поду¬ мал: «Из чего должна состоять программа? Какие у нее ключевые компоненты?» Первым делом он подумал о фигурах. Разумеется, он не забыл и про такие объекты, как пользо¬ ватель, звук и событие нажатия: для них у него уже была готовая библиотека. Поэтому он сосредоточился на проектировании фигур. Ниже вы найдете примеры кода Лоры и Брэда. Как думаете, кто получил кресло Аегоп и стол? Спецификации У Пользовательский интерфейс должен содержать три фигуры-, квадрат, круг и треугольник. При нажатии на любую фигуру она должна-. Повернуться на 360° по часовой стрел¬ ке, сделав полный оборот и вернувшись в исходное положение-, Воспроизвести звуковой файл формата АН, соответствующий именно этой фигуре. □ОД За столом Лоры Лора приступила к созданию важных проце¬ дур. Эта работа была ей привычна, она делала это миллион раз. Лора быстро написала про¬ цедуры rotate и playSound. rotate(shapeNum) { // повернуть фигуру на 360е > playSound(shapeNum) { // определить по номеру фигуры, // какой AIF-звук воспроизвести, //и воспроизвести его ) На ноутбуке Брэда в кафе Брэд написал класс для каждой из трех фигур. ZZL_ Square rotate) { // код для г I playSound0 1 Circle rotate) { // код для п< // код для ! ) // дения фа$ playSound0 I // для треуг // код для bi // ведения // для треуг Triangle rotate) ( // код для поворота треугольника playSound() 1 // код для воспроиз- // ведения файла Alt // для треугольника )
Лоре казалось, что победа уже у нее в кармане. Она почти ощущала под собой это заветное кресло... Но вдруг — неожиданность. Задание изменилось. «Хорошо, Лора, технически ты справилась первой, — признал руководитель проекта, — но в программу нужно добавить один небольшой штрих. Для таких профессионалов, как вы, это будет легко». «Если бы мне давали монетку каждый раз, когда я слышу “это будет легко”...» — устало подумала Лора. Она знала, внести изменения в работающий код без последствий — это миф. Особенно когда этот код — процедурный. «Странно, что Брэд выглядит таким спокойным... Что он задумал?» — пронеслось у нее в голове. Но, несмотря ни на что, Лора продолжала верить: да, объектно-ориентированный подход может показаться изящным и продуманным, но он же такой медленный! Добавлено в спецификации Лора вернулась на рабочее место Процедура поворота не требовала изменений — код исполь¬ зовал таблицу поиска, сопоставляя значение переменной shapeNum с нужной графической фигурой. Все оставалось по-прежнему. А вот с процедурой playSound все оказа¬ лось не так просто. Ее нужно было переписать, чтобы она соответствовала новым требованиям. playSound(shapeNum) { // если фигура - не амеба, // определить по номеру фигузы, // какой AIF-звук воспроизвести, //и воспроизвести его // иначе - //воспроизвести звук амебы в формате MP3 ) Дополнить код действительно оказалось несложно. Но сама мысль о том, что приходится лезть в уже готовый и прове¬ ренный код, вызывала у Лоры неприятные ощущения. Она знала: каким бы уверенным ни был руководитель про- Брэд, сидя с ноутбуком на пляже Брэд улыбнулся, сделал глоток своего фрук¬ тового фраппе и написал новый класс. О, как же ему нравилось в объектно-ори¬ ентированном программировании то, что можно не трогать уже протестированный и сданный код. «Гибкость, расширяе¬ мость...» — размышлял он, мысленно пере¬ бирая все преимущества ООП. Amoeba rotated { II код для поворота амебы } pla/SoundQ { II код для воспроизведения II нового .трЗ-файла для амебы }
Лор а на мгновение опередила Брэда «Ха! Ерунда этот ваш ООП», — самодовольно подумала Лора. Но ее улыбка быстро исчезла, когда раздражающий руководитель проекта вдруг сказал, с легким разочарованием в голосе; «Нет, амеба должна вращаться не так...» Оказалось, оба программиста использовали один и тот же алгоритм вращения1 1. Определить прямоугольник, в который вписывается фигура. 2. Вычислить центр этого прямоугольника и вращать фигуру вокруг этой точки Но руководитель хотел, чтобы амеба вращалась иначе — вокруг точки на одном из своих концов, как стрелка на циферблате. Лора закипала. Внутри все клокотало, как в тостере. «Ну вот, опять » «Хмм... Ладно. Я просто добавлю еще один if/else в процедуру rotate, и напишу особую логику вра¬ щения для амебы. Кажется, остальной код трогать не придется», - попыталась убедить себя Лора. Но вну¬ три что-то скреблось. Тонкий голос рассудка шептал: «Большая ошибка. Ты правда думаешь, что задание снова не поменяется?» В задании об этом не было ни слова. Дора вернулась на рабочее место Она решила, что лучше добавить аргументы для точки вращения в процедуру rotate. Приходи¬ лось переделывать много кода. Нужно заново проводить тестирование, перекомпиляцию. Рабо¬ тавший ранее код теперь не выполнялся. rotate (shapeNum, xPt, yPt) { // если фигура - не амеба, // вычислить центральную точку //на основе прямоугольника, // затем выполнить вращение // иначе - // использовать xPt и yPt в качестве // смещения точки вращения, // затем выполнить вращение } Брэд с ноутбуком балдеет в шезлонге на фестивале в Теллерайде... Не теряя времени зря, он изменил метод rotate, но только для класса amoeba. Ему не нужно было трогать уже проверенный, скомпили¬ рованный и работающий код в других частях программы. Точку вращения для амебы он задал с помощью специ¬ ального атрибута, обязатель¬ ного для всех объектов этого типа. Брэд внес изменения в код, проверил его и от¬ правил — на фестивале был бесплатный Wi-Fi — быстрее, чем закончилась музыкаль¬ ная композиция. Amoeba int xPoint; intyPoint; rotatef) { //код для поворота амебы II с использованием ее х и у } playSound() { II код для воспроизведения //нового.трЗ-файла для амебы }
Итак, Брэд, сторонник ООП, получил кресло и стол, да? Не так быстро. Лора обнаружила недостаток в его подходе. А поскольку она была уверена: «Если выиграю соревнова¬ ние — повышение не за горами», она решила попытаться обернуть ситуацию в свою пользу. Лора: Твой код дублируется! Процедура rotate есть в каждой из четырех... штук, которые отвечают за фигуры. Брэд: Это метод, а не процедура. И это классы, а не шту¬ ки. Лора: Да какая разница? Суть не меняется. Нужно поддер¬ живать четыре разных rotate. Что в этом хорошего? Брэд: Ты не видела результат. Сейчас я покажу тебе, Лора, как наследование работает в объектно-ориентированном подходе. На самом деле Лора бы ^ совсем не возражала “/ получить это кресло (решив, что кресло — это первый шаг к повышению и большим деньгам). Square Circle Triangle Amoeba rotate)) playSoundQ rotate)) playSound)) rotate)) playSound)) rotate)) playSound)) О Я заметил, что все четыре класса имеют нечто общее. О Это фигуры, и все они умеют вращаться и воспроизводить звуки. Поэтому я вынес общие черты в отдельный класс с именем Shape. Прочитать это можно так: «Square наследует от Shape», «Circle наследует от Shape» и остальные читаются точно так же. Из классов фигур я удалил методы rotate () и playSound (). Теперь поддерживать нужно только один экземпляр каждого из них — в су¬ перклассе. Класс Shape называется суперклассом осталь¬ ных четырех классов. А они, в свою очередь, являются подклассами этого класса. Подклас¬ сы наследуют методы суперкласса. Другими словами, если Shape умеет что-то делать — все его подклассы тоже умеют это делать О Суперкласс УК Подклассы Затем я связал первые четыре класса с Shape, использовав между ними отношение наследования. Square Circle Triangle Amoeba автоматически.
А как же метод rotate^) у Amoeba? Лора: Может быть, вся проблема в том, что у амебы совершенно другие процедуры rotate и playSound? Брэд: Методы! Лора: Ну, не придирайся к словам. Как амеба сможет вращаться по-особенному, если она наследует свою функциональность от класса Shape? Брэд: Класс Amoeba переопределяет методы суперкласса Shape. Когда программа выполняется, JVM точно знает, какой именно метод rotate () нужно вызвать, чтобы амеба вела себя правильно. О Я дополнил код класса Amoeba, чтобы он переопределял методы rotate^) и p\aySomd(), унаследован¬ ные от суперкласса Shape. Подкласс, которому разрешено переопределе¬ ние. может заново описывать методы суперкласса, если его поведение должно отличаться — полностью или частично — от других подклассов. Переопределение методов Лора: И как ты вообще сообщаешь амебе, что ей делать? Ты вызываешь... метод и говоришь ему, какую именно фигуру нужно вращать? Брэд: Именно. В этом и заключается прелесть ООП. Когда, например, нужно повернуть тре¬ угольник, в коде вызывается метод rotate () у объекта Triangle. Остальная часть про¬ граммы даже не знает, как именно треуголь¬ ник это делает. А если в программе появляется новый тип фигуры, ты просто создаешь новый класс, и он сам определяет, как себя вести. Я знаю, как должна вести себя фигура Shape. Тебе лишь нужно сказать, что мне делать, а я уже выполню всю работу. Не забивай свою голову деталями о том, как я буду это делать. Я могу сама о себе позаботиться. 'Я знаю, как амеба должна ^вращаться и воспроизво-„ дить звуки.
Интрига меня уже в могилу загоняет! Кто тут главный по мебели? Эми со второго этажа. (Никто не знал, что руководитель проекта подключил к соревнованию еще одного программиста. Эми ра¬ ботала на основе ООП и не тратила время на споры с коллегами, поэто¬ му ее проект уже был готов.) Что вам нравится в ООП? «Оно помогает мне разрабатывать программы естественным способом. Объекты в коде дей¬ ствительно могут развиваться, как и в жизни». — Джой, 27 лет, архитектор программного обеспечения «Когда нужно добавить новую функциональ¬ ность, мне не приходится трогать уже проверен¬ ный код. И это бесценно». — Брэд, 32 года, программист «Мне нравится, что данные и методы, которые с ними работают, находятся в одном месте — в классе. Все логично и аккуратно». — Джесс, 22 года, чемпион по настольному футболу «Меня подкупает возможность повторного ис¬ пользования кода. Создавая новый класс, я могу сделать его достаточно гибким, чтобы потом применять в других проектах». — Крис, 39 лет, руководитель проектов «Не могу поверить, что Крис сказал такое. За последние пять лет он не написал ни строч¬ ки кода». — Дэрил, 44 года, работает на Криса «Кресло — это только начало. ООП сэкономило мне уйму нервов». — Эми, 34 года, программист Пора размять извилины Вы только что прочли историю о том, как по¬ клонник процедур столкнулся лицом к лицу с программистом, предпочитающим ООП. Вы получили общее представление о таких объек¬ тно-ориентированных концепциях, как классы, методы и атрибуты. Оставшуюся часть главы мы потратим на изучение классов и объектов (а в следующих главах вернемся к наследова¬ нию и переопределению). Исходя из того, что вы только что увидели (а также узнали из других объектно-ориентиро¬ ванных языков, с которыми работали раньше), ответьте на следующие вопросы. О каких фундаментальных вещах вы обязаны помнить, проектируя класс на языке Java? Ка¬ кие вопросы вы должны себе задать? Если бы при проектировании класса вы создавали кон¬ трольный список, какие пункты вы бы внесли? Метакогнитквный совет Если какое-то упражнение вызывает трудности и в поисках решения вы зашли в тупик, попробуйте порассуждать вслух. Устная речь (и восприятие на слух) активизируют другие участки мозга. Максимальная активность мозга наблюдается при общении с другим человеком. Но и с животными это тоже работает. Именно так наша собака изучила полиморфизм.
Проектируя класс, не забывайте о создаваемых объектах: ■ о вещах, которые объект знает; ■ о вещах, которые объект делает. ShoppingCart Button Alarm cartContents Знает label color Знает alarmTime alarmMode addToCart() removeFromCart() checkOut() — Делает setColor() setLabelj) push() release() Делает setAlarmTime() getAlarmTimeO isAlarmSet() snooze() Знает Делает То, что объект о себе знает, называется ■ переменной экземпляра. То, что объект может делать, называется ■ методом. Переменные экземпляра (состояние) Методы (поведение) Song title artist setTitle() setArtist() play() Знает Делает To, что объект знает о себе, называется перемен¬ ными экземпляра. Они представляют состояние объекта (данные) и могут иметь уникальные значе¬ ния для каждого объекта конкретного типа. Можно сказать, что экземпляр — это другой спо¬ соб сказать объект, то есть синоним. Действия, которые может совершать объект, на¬ зываются методами. Занимаясь разработкой класса, думайте о данных, которые объект должен будет знать о себе, и создавайте методы, которые работа¬ ют с этими данными. Как правило, у объекта есть методы для чтения и записи значений переменных экземпляра. Например, объекты Alarm имеют переменную экземпляра для хранения значения alarmTime, и два метода для получения и уста¬ новки alarmTime. Таким образом, у объектов есть переменные экземпляра и методы, но они разрабо¬ аточите карандаш Запишите в таблицу, что должен знать и делать объект Television Переменные экземпляра Методы, классы и объекты таны как часть класса. Решать - вам!
Различия между классом и объектом Класс — это не объект (Но нужен для их создания) Класс — это шаблон объекта. Он указы вает виртуальной машине, как соз¬ дать объект данного типа. Каждый объект, созданный на основе этого класса, может иметь собственные значения для переменных экземпляра этого класса. Например, используя класс Button можно создавать десятки различных кнопок, и у каждой кнопки может быть свой цвет, размер, форма, надпись и так далее. И каждая из этих кнопок будет объектом button. Класс Проще представить так: Q-S V ijr-yTTY, Nnpie / L - М \ РСП па Имя: Jfc.au ЖсрсЬи?м Тел.: <5(000,1000-00-00 CmaiС: ptn&wickedC[jsmayt Объект можно представить как запись в телефонной книге Список контактов в вашем телефоне может служить аналоги¬ ей классов и объектов. Каждый контакт имеет одинаковые пу¬ стые поля (переменные экземпляра). Создавая новый контакт, вы создаете экземпляр (объект), а записи, которые вы делаете для него, описывают его состояние. Методы класса — это те действия, которые вы выполня¬ ете с конкретным контактом; getNameO, changeNameO, setNameO — все они могут быть методами класса Contact. Таким образом, контакты выполняют определенный набор действий, одинаковый для всех (getNameO, changeNameO и т. д.), но информация, которую знает каждый отдельный кон¬ такт, уникальна.
Создайте свой первый объект Как создать и использовать объект? Первым делом нужно создать два клас¬ са. Один класс для описания типа объекта (собака, будильник, телевизор и т. д.), а другой — для тестирования вашего нового класса. В тестовом классе вы размещаете метод main (), в котором создаете объекты ново¬ го типа и получаете к ним доступ. Тестовый класс нужен для того, чтобы опробовать методы и переменные вашего нового объекта. Многие примеры в книге будут содержать два класса. Один из них будет реальным классом — классом, чьи объекты мы действительно хотим ис¬ пользовать, а другой — тестовым, который мы назовем <ИмяВашего11ервого ATwcaoTes tDri ve. Например, если мы создадим класс Bungee, нам понадобится и класс BungeeTestDrive. Только класс <ИмяВашего11ервогоКласса >TestDrive будет содержать метод main (), и его единственная цель — создать объекты нового класса (не тестового), а затем использовать опера¬ тор точка (.) для доступа к методам и переменным новых объектов. После просмотра следующих примеров вам все станет понятно. Нет, правда. Создаем class Dog { int size; String breed; String name; void bark() { System.out. класс. Шншп Dog size 9YM,Ujfa breed name — Hanuj bark() ("Гав! Гав!"); Оператор доступа «точка» (.) Этот оператор предостав¬ ляет доступ к состоянию объекта и его поведению (к переменным экземпля¬ ра и методам). // Создаем новый объект Dog d = new Dog(); // Приказываем ему // подать голос, // получая доступ // к методу bark() // через оператор "точка" d.bark(); // Устанавливаем его // размер, используя // оператор доступа d.size = 40; ) } Aw» (,u' r. viYUV ■ .(VlV Q Создаем тестовый класс (TestDrive). class DogTestDrive { ^ public static void main(String[] args) // Тестовый код для класса Dog ) ) ©Создаем внутри тестового класса объект и получаем доступ к его переменным экземпляра и методам. class DogTestDrive { public static void main(String[] args) { Dog d = new Dog(); d.size = 40; )X?bX6(№<V^3 .bark(); К У шл4 ссикт кыааУХу. Как бачКО- } bMjtjtu mtfvittup ymniput. Jrman«(tiuffauymmuL na m .size. Если у вас уже сложилось определен¬ ное представление об ОПП, вы долж¬ ны были заметить, что в этом при¬ мере не используется инкапсуляция. Эту тему мы рассмотрим в главе 4.
Создание и тестирование объектов Movie class Movie { String title; String genre; int rating; void playlt() { System.out.println("Воспроизводится фильм"); ) ) public class MovieTestOrive { public static void main(String[] args) { Movie one = new Movie(); one.title = "Унесенные акциями"; one.genre = "Трагедия"; one.rating = -2; Movie two = new Movie(); two.title = "Затерянные в офисном пространстве"; two.genre = "Комедия"; two.rating = 5; two.playIt(); Movie three = new Movie(); three.title = "Клуб байтов"; three.genre = "Трагичный, но в итоге вдохновляпций"; three.rating = 127; } ) MOVIE title К genre rating playltf) Класс MovieTestDrive создает объекты (экземпляры) класса Movie и использует оператор точки (.), чтобы присваивать значения переменным экземпляра. Класс MovieTestDrive также вызывает метод у одного из объектов. Заполните таблицу справа значениями, которые будут содержать три объекта после выполнения метода main(). объект 1 genre ^ rating С title объект 2 genre rating объект 3 title genre rating Л J Л J ~\ Решать —вам! J
Быстрее! Валим из main! Пока вы находитесь в main (), нельзя сказать, что вы в Объектино. Для тестовой программы вполне допустимо хранить код внутри метода main. Настоящие объектно-ориентированные приложения предполагают нали¬ чие объектов, которые должны общаться с другими объектами, в отличие от статического метода main () .создающего и тестирующего объекты. Два возможных варианта использования метода main: ■ тестирование настоящего класса; ■ запуск/старт Java-приложения. Реальное приложение, написанное на языке Java, полностью состоит из взаимодействующих друг с другом объектов. В данном случае общение означает, что объекты вызывают методы друг друга. На предыдущей странице (а также в главе 4) мы рассмотрели использо¬ вание метода main () в отдельном классе Test Drive — для создания и тестирования методов и переменных другого класса. В главе 6 мы уви-' дим, как класс с методом main () используется при создании настоящего Java-приложения: в нем создаются объекты и закладываются возможности для их взаимодействия. В качестве небольшого предварительного обзора того, как может вести себя настоящее Java-приложение, приведем простой пример. Пока мы только в начале пути, и у нас под рукой лишь базовые инструменты, поэтому программа может показаться немного неуклюжей и не слишком эффективной. Скорее всего, вам захочется ее улучшить: этим мы и займемся в следующих главах. Даже если какие-то части кода покажутся вам пока непонятными, не пере¬ живайте. Главная цель примера — показать, как объекты разговаривают с объектами. Игра-угадайка Описание: В игре принимают участие один игровой объект и три объекта-игрока. Игра генерирует случайное число в диапазоне от 0 до 9, а три объекта-игрока пытаются их угадать (мы не обещали, что игра будет увлекательной). Классы: GuessGame.class Player.class GameLauncher.class Логика: 1. Класс GameLauncher — запускает приложение; он содержит метод main(). 2. В методе main() создается объект GuessGame, из которого вызывается метод startGameQ. GameLauncher main(StringO args) к GuessGame Р1 P2 РЗ startGameQ ■аТМПлТ“ 9^ у tv 3. Весь игровой процесс происходит в методе startGameQ объекта Guess¬ Game. Он создает трех игроков, затем «загадывает» случайное число (которое игроки должны угадывать). Каждому игроку предлагается угадать задуманное число. Результат проверяется и либо объявляется победитель, либо игрокам предлагают новую попытку угадать число.
public class GuessGame { Player pi; Player p2; Player p3; public void startGameO ( pi = new Player(); o2 = new Player(); p3 = new Player(); jamt ссмржшп. три. трииннпс экхмпляла аля QmtKtmd PCaxftr. 1 * int guesspl = 0; int guessp2 = 0; int guessp3 = 0; <T 0<ciiH&incu три npt.uuwm cj.am xfumtnuA atmnvfiигроке^ три cca (к нм и грскев ii nfnu&w&u.,u мл тременнпи. boolean plisRight = false; boolean p2isRight = false; boolean p3isRight = false; ^ б'а.чбтш три ntfiumiuu срся хранения ipeepuaumnuu игрексС. , , ... О&Я&АЯШ ttllUl lU/XCUtHHtlt int targetNumber = (int) (Math.random() iu); ’ч < I System.out .println ("Я загадал число от 0 до 9..."); <риксаи,ии. АраблАшсопн «жйнад игрека. + targetNumber); while (true) t System.out.println("Загаданное число: pi.guess (); p2.guess () ; p3.guess (); £ Загеирх&им mac. капера игреки еражнп piaepinu. guesspl = pi.number; System.out.println("Игрок один предположил: " + guesspl)- guessp2 = p2.number; System.out.println("Игрок два предположил: " + guessp2); guessp3 = p3.number; System.out.println("Игрок три предположил: " + guessp3); .ЗагураимОат'/ camp/с игрека прерии и,тики ■Tfcitp/at u прири, и.тек им UtfUKcd. Лре&ряш. ctftiuitpum. »u ipupu иже nut С* кмж TUCMeVL. if (guesspl == targetNumber) { plisRight = true; } if (guessp2 == targetNumber) ( o2isRight = true; ) if (guessp3 == targetNumber) ( t ?3isRight = true; &AU .ХХАПЛ m UJUH ШрСК fOpCU. яг&ршаем u/pp if (plisRight || p2isRight || p3isRight) { System.out.println("У нас есть победитель!"); System.out.println("Игрок один угадал? " + plisRight); System.out.println("Игрок два угадал? " + p2isRight); System.out.println("Игрок три угадал? " + p3isRight); System.out.print 1n("Игра окончена."); break; // игра окончена, выходим из цикла Ипап итас.тя 6и,икм и сш£ара- // продолжаем игру, потому что никто не угадал! npaiuutfat.u ирерпиес.шиич у игреке^. System.out.println("Никто не угадал. Попробуем еще раз!"); } // конец if/else } // конец цикла } // конец метода
Запускаем нашу игру Java сама выносит мусор Каждый раз, когда в Java создается объект, он попадает в специальную область памя¬ ти — кучу (heap). Именно туда отправ¬ ляются все объекты, независимо от того, где, когда и как они были созда¬ ны. Но эта куча не совсем обычная — она собираемая. В Java используется так называемая куча с возможностью сбора мусора (Garbage-Collectible Heap). Когда вы создаете объект, JVM выделя¬ ет под него нужное количество памяти. Чем больше у объекта переменных экземпляра, тем больше места ему потребуется. Например, объект с 15 переменными займет больше памяти, чем тот, у которого их всего две. А что происходит, когда объект боль¬ ше не нужен? Как Java освобождает память? Все делает сама! Как только JVM понимает, что объект больше не используется (например, на него больше не ссылается ни одна переменная), она помечает его как му¬ сор. Когда в памяти остается слишком мало свободного места, запускается специальный механизм — сборщик мусора (Garbage Collector). Он находит ненужные объекты и очищает память, освобождая место для новых. В следующих главах мы подробнее по¬ говорим об этом процессе. Программный вывод (при каждом запуске программы разный) I File Edit Window Help Explode %java GameLauncher Я загадал число от 0 до | 9. Загаданное число: 3 Игрок один предположил: 8 Игрок два предположил: 9 Игрок три предположил: 5 1 Никто не угадал. Попробуем еще раз! Загаданное число: 3 Игрок один предположил: 1 Игрок два предположил: 1 Игрок три предположил: 1 Никто не угадал. Попробуем еще раз! Загаданное число: 3 Игрок один предположил: 6 Игрок два предположил: 4 Игрок три предположил: 2 Никто не угадал. Попробуем еще раз! Загаданное число: 3 Игрок один предположил: 3 Игрок два предположил: 5 Игрок три предположил: 6 У нас есть победитель! Игрок один угадал? true Игрок два угадал? false Игрок три угадал? false Игра окончена. Игра окончена.
(Не) дурацкие вопросы ЗоирОС: А вдруг мне понадо¬ бятся глобальные переменные и методы? Как их реализовать, если все должно быть внутри классов? Оиивеж: В объектно-ориен¬ тированных программах на Java не бывает настоящих глобальных переменных или методов. Но иногда действительно нужно, чтобы какая-то переменная или метод были доступны из любой части программы. Вспомните метод randomO из генератора фраз — он должен быть доступен в любом месте. То же самое каса¬ ется числа Пи. В главе 10 вы узнаете, как ключевые слова public и static помогают в таких случаях. С их помощью можно сделать метод практически «глобальным»: его можно будет вызывать из любого места, из любого класса вашей программы. А если добавить еще final, то получится глобальная констан¬ та — например,для хранения значения Пи или другого важного значения, которое не должно изменяться. Зои рОС: Как это связано с ООП, если все равно можно создавать глобальные функции и данные? Омвет: Весь код в Java размещается внутри классов. Даже такие «глобальные» вещи, как число Пи или метод randomO, хоть и помечены как public static, все равно находятся внутри клас¬ са — например, класса Math. По¬ добные статические конструкции в Java — скорее исключение, чем правило. Их используют в особых случаях, когда не предполагается создание множества экземпля¬ ров объекта. ЗОИ рОС: Что представ¬ ляет собой программа на языке Java? Что мы получа¬ ем в результате? Оиивеж: Программа на языке Java — это на¬ бор классов (минимум один класс должен быть обязательно). В Java- приложении один из классов обязан содержать основной метод — именно он и запускает программу. Как про¬ граммист, вы создаете один или несколько классов. Это и есть результат вашей работы. Если у конечного пользователя не установлена JVM, ее нужно бу¬ дет включить в состав приложе¬ ния, чтобы все заработало. Есть специальные программы, кото¬ рые упаковывают ваши классы вместе с нужными версиями JVM (например, для разных операци¬ онных систем) и создают папку или установочный файл, кото¬ рый можно распространить — в том числе через Интернет. Так пользователь сможет установить подходящую версию JVM (если она еще не установлена). Зои рос: А если у меня сто классов? Или тысяча? Неужели нужно поставлять все файлы отдельно? Можно ли упаковать их в один исполняемый файл? Класс — это рецепт А объекты леченьки -Без лишних слов Объектно-ориентированное про¬ граммирование позволяет допол¬ нять приложение, не затрагивая проверенный ранее, работающий код. Весь код в Java находится внутри классов. Класс описывает, как создавать объект определенного типа. Класс — это что-то вроде ша¬ блона. Объект может сам о себе поза¬ ботиться; вам не нужно знать, как именно он это делает. Объект знает что-то и умеет делать. Сведения об объекте называются переменными экземпляра. Они определяют состояние объекта. Действия объекта называются методами. Они обуславливают поведение объекта. Ошвеж: Конечно, постав¬ лять пользователю сотни отдель¬ ных файлов неудобно. Но это и не нужно. Все ваши программ¬ ные файлы можно упаковать в один Java-архив — JAR-файл, который использует формат PKZIP. В этот jar-архив можно до бавить текстовый файл с мани¬ фестом, где указывается, какой класс содержит метод main(). Именно он будет запускать при- Для каждого класса можно пред¬ усмотреть отдельный тестовый класс, который будет использо¬ ваться для создания объектов нового класса. Класс может наследовать поля и методы от более абстрактного суперкласса. Работающая программа на языке Java — это не что иное, как набор объектов, которые общаются между собой.
ражненив А Компилируйте Каждый из Java-файлов на этой странице — полноценный исходный файл. Ваша задача — ненадолго превратиться в компилятор и определить, скомпилируется ли каждый из них. Если найдете файл, который не проходит компиляцию, предложите способ его исправить. Б class StreamingSong { String title; String artist; int duration; void play() { System.out.println("Играет песня"); ) void printDetails() { System.out.println("3TO " + title + " в исполнении " + artist); i } class StreamingSongTestDrive ( public static void main(String[] args) { song.artist = "The Beatles"; song.title = "Come Together"; song.play(); song.printDetails(); ) ) class Episode { int seriesNumber; int episodeNumber; void skiplntro() { System.out.println("Пропускаем заставку. ) void skipToNext() ( System.out.println("Загружаем следующий эпизод..."); } class EpisodeTestDrive { public static void main(String[] args) { Episode episode = new Episode(); episode.seriesNumber = 4; episode.play () ; episode.skiplntro(); } } *■ Ответы на стр. 82
Магнитики С КОДОМ ► Ответы на стр. 82. На холодильнике в беспорядке разбросан код рабочей Java-программы. Фрагменты перепутались: кажется, будто кто-то играл ими как магнитиками. Ваша задача — переставить кусочки кода в правильном порядке, чтобы получилась рабочая программа. Она должна скомпи- лироваться и вывести нужный результат. Несколько фигурных скобок при этом, увы, упали на пол и затерялись... Так что добав¬ ляйте их столько, сколько потребуется! I File Edit Window* Help Dane* % java DrumKitTestDrive бум-бум ба-бум динь-динь да-динь — : playSnare() { System.out.println("бум-бум ба-бум"); } public static void main(String [] args) {
бассейн фрагментов В бассейне находятся фрагменты кода. Ваша задача — поместить их в пустые строки кода. Некото¬ рые фрагменты не понадобятся, а некоторые можно использо¬ вать больше одного раза. Цель упражнения — создать класс, который скомпилируется и запустит¬ ся, а результат вывода будет совпа¬ дать с тем, что приведен ниже. Не обольщайтесь: выполнить упражне¬ ние будет сложнее, чем кажется. Output File Edit Window Help Implode %java EchoTestDrive привееееет... привееееет... привееееет... привееееет... 10 Призовой вопрос! Как бы вы завершили эту го¬ ловоломку, если бы в послед¬ ней строке было не 10, а 24? public class EchoTestDrive { public static void main(String [] args) { Echo el = new Echo(); int x = 0; while ( ) { el.hello () ; if ( ) 1 e2.count = e2.count + 1; } if ( ) { e2.count = e2.count + el.count; i x = x + 1; System.out.println(e2.count); } } class 1 int = 0; void ( System.out.println("привееееет... "); } } Примечание: каждый фрагмент кода из бассей¬ на разрешается исполь¬ зовать несколько раз! x < 4 x < 5 Echo е2 x > 0 Tester count x > 1 echo() e2 = el; е1 = е1 +1; count() Echo e2; el = count + 1; e1.count = count + 1; e1 .count = el .count + 1; hell o() Echo e2 = el; Echo e2 = new Echo(); -► Ответы на стр. 83
Группа Java-компонентов, одетых в костюмы, играет на вечеринке в игру «МаскаКод?». С помощью подсказок вы пытаетесь угадать, кто они, на основе того, что они говорят. По условиям игры они всегда говорят правду о себе. Если вдруг то, что они говорят, относится к нескольким игрокам, укажите всех, кому это утверждение подходит. Напишите имена участников в пустой строке напротив утверждений. Первую строчку мы уже заполнили. Участники маскарада: Класс Метод Объект Переменная экземпляра Я компилируюсь из файла JAVA Значения моих переменных экземпляра могут отличаться от значений моих полей ввода Класс Я выступаю в роли шаблона Я люблю что-нибудь делать У меня может быть много методов Я представляю собой состояние Я могу вести себя по-разному Я нахожусь в объектах Я обитаю в куче Меня используют для создания экземпляров объекта Мое состояние может меняться Я объявляю методы Я могу изменяться во время выполнения программы Ответы на стр. 83
Ответы на упражнения Компилируйте (со стр. 78) class StreamingSong ( String title; String artist; int duration; A void playO ( System.out.println("Играет песня"); ) Магнитики с кодом (со стр. 79) class DrumKit ( boolean topHat = true; boolean snare = true; void playTopHatO { System.out.println("динь-динь да-динь"); l void playSnarel) i System.out.println("бум-бум ба-бум"); ] ) void printDetailsО I System.out.println("Это " + title + " в исполнении " + artist) ) У HM'f(>nt Шшш:н. пиntju мп.иашц птшт с&кт! class StreamingSongTestDrive { ' n public static void main(String[] args) 1 StreamingSong song = new StreamingSongO; song.artist = "The Beatles"; song.title = "Come Together"; song.playO; song.printDetailsO ; ) class DrumKitTestDrive ( public static void main (String!) args) { DrumKit d = new DrumKit(); d.playSnare () ; d. snare = false; d.playTopHat () ; if (d.snare == true) ) d.playSnare() ; ) ) class Episode 1 int seriesNumber; int episodeNumber; void playO ( Mtnvf* Kdl|ки1СЛ f'SUt System.оиГ.рг(п11пС'Воспроизведение эпизода 11 ♦ episodeNumber); void skiplntrof) | System.out.println("Пропускаем заставку..."); void skipToNextO ( System.out.println("Загружаем следующий эпизод ")■ ) File Eon Window Help Dance % java DrumKitTestDrive бум-бум ба-бум динь-динь да-динь В class EpisodeTestDrive j public static void main(String)] args) ( Episode episode = new Episode!); episode.seriesNumber = 4; episode.playO ; episode.skiplntrof);
Ответ на головоломку бассейн фрагментов («ктр.80) public class EchoTestDrive { public static void main(String[] args) ( Echo el = new EchoO; Echo e2 = new EchoO; // правильный вариант -или- Echo e2 = el;//бонусный ответ: выведет 24 int x = 0; while (x<4) { e 1. he 11 о () ; el.count = el. count + 1; if (x == 3) { e2.count = e2.count + 1; ) if (x > 0) { e2.count - e2.count + el.count; } x = x + 1 ; } System.out.printIn(e2.count); class Echo { int count = 0; void hello() { System.out.printIn("Привеееет... "); ) МаскаКод (CO CTp. 81) Я компилируюсь из файла JAVA К.МСС Значения моих переменных экземпляра могут отличаться от значений моих полей ввода Ссикт Я выступаю в роли шаблона Л. 1 асе Я люблю что-нибудь делать С'с^нкпи uanuj У меня может быть много методов Л. мее. егмкт Я представляю собой состояние JUpLUlHHll ч э>ус«п- Г'У'" Я могу вести себя по-разному 6с м кт. (с миг Я нахожусь в объектах . Mimcy. ntfu.vtt sKjcm\.1Яра Я обитаю в куче Ссйкт Меня используют для создания экземпляров объекта К. икс Мое состояние может меняться Саект. па и Я объявляю методы Л. icier Я могу изменяться во время выполнения программы Саскт. nu« File Edit Window Help Assimilate %java EchoTestDrive привееееет... привееееет привееееет. привееееет. 10 Обратите внимание: и про классы, и про объекты говорят, что у них есть состояние и поведение. Они описаны внутри класса, но, когда речь идет об объекте, мы тоже говорим, что он обладает состоянием и поведением. На этом этапе нас не волнует, где именно они находятся технически; важно, что объект их использует.
3. Примитивы и ссылки Знай свои переменные Есть переменные двух типов: примитивы И ССЫЛКИ. До сих пор вы использовали переменные либо для описания состояния объекта (переменные экземпляра), либо как локальные переменные (которые объявляются внутри метода). Позже мы будем использовать переменные в качестве аргументов (значений, передаваемых в метод вызывающим кодом) и возвращаемых типов (значений, которые передаются методу из вызываемого кода). Вы видели переменные, объявленные как простые примитивные целочисленные значения (тип int). С более сложными элементами вы тоже уже знакомы, например строки и массивы. Но должно же быть что-то большее, чем целые числа, строки и массивы. Что, если у вас есть объект PetOwner с переменной экземпляра типа Dog? Или Саг с Engine? В этой главе мы изучим типы в языке Java (например, разницу между примитивами и ссылками) и рассмотрим, что можно объявить в качестве переменной, какое значение можно присвоить переменной и как их можно применять. И, наконец, мы увидим какова жизнь в куче.
Объявление переменной cm ,, i«# *>****^1 $й» Jltptutmu RaQflit Java заботится о типах. Он не допустит нелогичных действий, например поместить ссылку типа Giraffe в переменную Rabbit. Java также не разрешит присво¬ ить число с плавающей точкой целочисленной пере¬ менной, если только вы не укажете компилятору, что допускаете возможность обойтись без точных результа¬ тов (например, убрав дробную часть). Компилятор распознает большинство проблем: Rabbit hopper = new Giraffe(); He питайте иллюзий, что это скомпилируется. Чтобы защита типов работала, нужно указать тип своей переменной. Это целое число? Тип Dog? Одиночный символ? Как уже говорилось, переменные делятся на два вида: примитивы (простые типы) и ссылки. Первые хранят основные значения (иначе говоря, простые битовые структуры), включая целые числа, логические значения и числа с плавающей точкой. Ссылки хранят ссылки на объекты (все предельно ясно, правда?). Первым делом мы рассмотрим примитивы, а затем по¬ пытаемся разобраться, что на самом деле такое ссылки на объекты. Обратите внимание, что, объявляя любой тип переменных, вы должны соблюдать два правила. Переменная должна иметь тип. Помимо типа у переменной должно быть имя, которое можно будет использовать в коде. Переменная должна иметь имя. int count; ? К Tit. IV Пим Примечание: когда встречаете выражение «Объект типа X», следует понимать, что тип и класс — синони¬ мы (в следующих главах обсудим это подробнее).
Мне, пожалуйста, двойной эспрессо... Нет, лучше возьму целое Думайте о переменных в Java как о чашках. Кофейных, чайных, больших чашках для любимых напитков, огромных стаканах для попкорна, чашках с удобными ручками, чашках с металлической каймой, которые ни в коем случае нельзя ставить в микроволновую печь. Переменная — это просто посуда. Контейнер. Она предназначена для хранения. У нее есть размер и тип. В этой главе мы сначала рассмотрим пере¬ менные (чашки), предназначенные для примитивов, а чуть позже перейдем к изучению посуды, которая хранит ссылки на объекты. В качестве аналогии будем и дальше использовать пример с чашка¬ ми. Это поможет нам, когда речь пойдет о более сложных вещах, — а это случится очень скоро. Примитивы похожи на стаканчики в кофейнях. Вы наверняка знаете, о чем идет речь. Стаканчики бывают разных размеров, и каждый называ¬ ется по-своему — «короткий», «вы- сокий» и «Я хотел бы большой ^ ^ щ мокко с двойными взбитыми сливками». Все вы видели эти f 1 Л стаканчики в кофейне. Маленький Небольшой Средний Большой _Так и в Java: примитивы имеют разные размеры и на¬ зываются по-разному. Объявляя переменную, обяза¬ тельно нужно указать для нее конкретный Птип. Следующие четыре контейнера предназначены для четырех ■Ч целочисленных примитивов \| I Java. long int short byte Каждый контейнер содержит значение, поэтому для примитивов в Java вместо «Я буду большой кофе по-французски» вы просите компилятор: «Мне, пожалуйста, переменную типа int с числом 90». С одной небольшой разницей — в Java у чашки должно быть имя, которое должны ей присвоить вы. Поэтому просьба будет такой: «Мне, пожалуйста, целочисленную переменную со значением 2486 и именем height». Каждый примитив имеет фиксированное количество бит (размер чашки). Размеры шести числовых примити¬ вов в Java показаны ниже. Простые типы Тип Размер в битах Диапазон значений boolean и char boolean (зависит от jvm) true или fo/se char 16 бит от 0 до 65535 Числовые (все числа со знаком) Целочисленные byte 8 бит от-128 до 127 short 16 бит от -32768 до 32767 int 32 бит от -2147483648 до 2147483647 long 64 бит от — огромного до огромного Типы с плавающей точкой float 32 бит различные double 64 бит различные Объявление переменных про¬ стых типов и присваивание им значений int х; х = 234; byte b = 89; boolean isFun = true; double d = 3456.98; char c = 'f'; int z = x; boolean isPunkRock; isPunkRock = false; boolean powerOn; powerOn = isFun; long big = 3456789L; float f = 32.5f; (Mumvumt (auuuuuu на ciut(?c J f ц L Kf\u [w ant c fи^неиш Шг. н (чиш иштиш uacfcja трети д(?нс »\«irawifetmi. KcuuuuAmt M, mvc iuuhhc <k uMttmt (? ftvfij. Щ таких nujCKOfOK он мсмат ги.и.жш munH.jD.iA этого u umcAi д|нтп(А lUlt са лит RICH — npUUUHtU IUW cmtwmm гукви. Ktmcfuu ста&опея RtUIUU C tUCXdii.
Хотите точно вместить значение в переменную? Убедитесь, что оно вмещается! Вы не можете поместить большое зна¬ чение в маленькую «чашку». Честно говоря, это можно сделать, но тогда кое-что потеряется; произойдет своего рода переполнение. Компилятор попы¬ тается предотвратить такую ситуацию, если увидит, что в вашем коде некая сущность не вмещается в выбранный вами контейнер (переменную/чашку). Например, вы не можете «залить» зна¬ чение типа int в контейнер размера byte, как показано ниже. int х = 24; byte b = х; //не сработает!! Вы спросите, почему так происходит? Значение у. равно 2 4, значит, оно полностью совместимо с типом byte. Нам это известно, и вам это известно, но компилятор замечает попытку поместить большую сущность в малень¬ кий контейнер, из-за чего может произойти переполнение. Не надейтесь, что компилятор будет знать значение переменной х, даже если вы прямо указали его в своем коде. Вы можете присвоить значение переменной несколькими способами: ■ указать литеральное значение после знака равенства (у. = 12, isGood = truem. д.); ■ присвоить переменной значение, которое принадлежит другой пере¬ менной (х = у); ■ использовать выражение, комбинируя оба способа (х = у + 43). В следующих примерах значения-литералы выделены полужирным шрифтом. int size = 32; char initial = 'j'; double d = 456.709; boolean isLearning; isLearning = true; int у = x + 456; Объявляем переменную типа int с именем size, при¬ сваиваем ей значение 32 Объявляем переменную типа char с именем initial, присваиваем ей значение 'j' Объявляем переменную типа double с именем d, присваиваем ей значение 456.709 Объявляем переменную типа boolean с именем isCrazy (ничего ей не присваиваем) Присваиваем значение true объявленной ранее переменной isCrazy Объявляем переменную типа int с именем у, при¬ сваиваем ей значение, которое является суммой из значения переменной х (чему бы оно ни равнялось) и числа 456 %|аточите карандаш Компилятор не позволит вам поместить значение из большой «чашки» в маленькую. Но можно ли перелить содержимое маленькой чашки в большую? Без проблем. На основании сведений о размере и типе переменной подумайте, сможете ли вы определить, какое из выражений допустимо, а какое — нет. Мы пока не рассмотрели все правила, поэтому в некоторых случаях вам придется угадывать. Подсказка: компилятор всегда делает выбор в пользу безопасности. В следующем списке выделите допустимые выражения, исходя из того, что они используются в одном и том же методе. 1. int х = 34.5; 2. boolean boo = х; 3. int g = 17; 4. int у = g; 5. у = у + 10; 6. short s; 7. s = у; 8. byte b = 3; 9. byte v = b; 10. short n = 12; 11. v = n; 12. byte k = 128; ► Ответы на стр. 104
Избегайте этого ключевого слова! Вы знаете, что для переменных нужны имя и тип. С примитивами вы уже знакомы. Но что можно использовать в качестве имен? Правила просты. Подобрать имя для класса, метода или переменной можно в соответствии со следующими правилами (в реальности ограничения не такие строгие, но если их придерживаться, вы сможете избежать ошибок): ■ Имя должно начинаться с латинской буквы, знака подчеркивания (_) или знака доллара ($). Нельзя начинать имя с цифры. ■ После первого символа можно использовать цифры. ■ В качестве имени можно использовать любое слово, с учетом этих двух правил, кроме небольшого списка зарезервированных в Java слов. Вбить в голову Восемь примитивов: boo\ean char byte short int tong float double Для простоты запоминания есть мнемони¬ ческая фраза, если английский не проблема: Be Careful'. Bears Shouldn't Ingest Large Furry Dogs А еще лучше — приЭумайте собственную фразу. В_ С_ В_ S_ L L_ F_ D_ Зарезервированные слова — это ключевые слова (и другие вещи), которые компилятор распознает. Если вы хотите поиграть в игру «запутать компилятор», то попробуйте использовать зарезервиро¬ ванное слово в качестве имени. Некоторые зарезервированные слова вы уже знаете: public static void ^ Названия примитивов тоже зарезервированы: с4>< / % boolean char byte short int long float double А сколько еще тех, о которых мы не упоминали! Даже не зная их значения, вы должны понимать, что не можете их использовать. Ни в коем случае не пытайтесь сейчас их запомнить. Выучив их, отведя для них место в памяти, вы потеряете какую-то важную информацию — например, где припаркована ваша машина. Не вол¬ нуйтесь, большинство из них вы будете знать к концу книги. Не используйте эти слова, они зарезервированы. «иваите мне. повто¬ ряв, не давайте прогло т тить еще одну огромнук 4 ^шистую собаку. „ catch double float int private super true abstract char else for interface protected switch try assert class enum goto long public synchronized void boolean const extends if native return this volatile break continue false implements new short throw while byte default final import null static throws case do finally instanceof package strictfp transient Ключевые слова, зарезервированные слова и специальные идентификаторы Java. Не используйте их в каче стве имен, иначе запутаете компилятор.
Управление объектом Dog Вы знаете, как объявить простую переменную и присвоить ей pCg j _ new Dog(); значение. Что делать, если это не примитив? Другими словами, как быть с объектами? d*bark(); ■ На самом деле не существует такого понятия, как объектная переменная. Лриупшй . yhu ymc ■ Есть переменная, ссылающаяся на объект. ■ Переменная, ссылающаяся на объект, хранит биты, которые описывают путь для доступа к объекту. ■ Она хранит не объект как таковой, а нечто вроде указателя. Или адреса. Только в Java мы не знаем, что на самом деле находится внутри такой переменной. Но точно известно: чем бы она ни была, она всегда представляет лишь один объект. И JVM знает, как ис¬ пользовать ссылку, чтобы этотобъект получить. Нельзя поместить объект в переменную. Мы часто думаем об этом именно так: «Я передал объект String в метод System, out. println () ». Или: «Метод возвращает объект Dog», или: «Я поместил новый объект Гоо в пере- f менную туКоо». На самом деле это работает немного не так. Не существует контейнеров, которые могут растя¬ гиваться до размеров любого объекта. Объ¬ екты существуют в одном месте — в куче, за которой присматривает сборщик мусора! (Далее в этой главе более подробно рас- смотрим этот вопрос.) Примитивы состоят из битов, представляющих фактическое значение переменной. Ссылоч¬ ная переменная содержит биты, которые описывают способ доступа к объекту. Ссьымная мрг.шншся типпУку рмсташ Kit к mf шп уинижн,нснн( н ifnjui<\ it мн и. ( nr, «сиу к (>!н (jaam (сишнcjij сшкту гни -ни cqc. ити С помощью оператора точки (.) в ссылочной переменной можно сказать: «Используйте то, что перед точкой, чтобы получить объ¬ ект после точки». Команда: myDog. bark () ; означает: «Используйте объект, на который ссылается перменная myDog, чтобы вызвать метод bark () ». Использование в переменной оператора «точка» вместе со ссыл¬ кой на объект, по сути, аналогично нажатию кнопки на пульте дистанционного управления объектом.
оООО byte short int long Ссылка 8 16 32 64 (число битов не определено) Ссылка на объект — это просто еще одно значение переменной. То, что хранится внутри «чашки». Только на этот раз значение — пульт дистан¬ ционного управления. Переменная простого типа byte х = 7; Биты, представляющие число 7, хранятся в переменной (00000111). ^°o44fX Г| Знапнш byte ушдштиб? Ссылочная переменная Dog myDog = new Dog() ; Биты, описывающие способ получить объект Dog, хранятся в переменной. Но в переменной не хранится сам объект Dog! Значение переменной простого типа — это про¬ сто само значение (например, 5, -26, 7, 'а'). А значение ссылочной переменной — это не сам объект, а указание на то, где и как его найти. Как именно JVM реализует ссылки на объек¬ ты — вопрос ее внутренней кухни. Может, это указатель, может, индекс в таблице, а может, что-то еще. Даже если вы знаете, как это устро¬ ено в вашей JVM, использовать это знание вы не сможете: доступ к объекту возможен только через саму ссылку, а не напрямую к битам или адресам. Три шага на пути к объявле¬ нию, созданию и инициализа¬ ции объекта. 2 'бод myDog = new Dog () ; О Объявление ссылочной переменной. Dog myDog = new Dog() ; Сообщает JVM о необходимости вы¬ делить место в памяти для ссылочной переменной и назвать ее myDog. Ссылочная переменная навсегда полу¬ чает тип Dog. Это своеобразный пульт управления с кнопками для работы именно с объектом Dog, а не Cat, Button или Socket. О Создание объекта Dog myDog = new Dog () ; В этой части JVM получает коман¬ ду выделить память в куче для объекта Dog (более подробно мы изучим этот процесс позже, в частности в главе 9). Объект Dog ©Соединение ссылки и объекта Dog myDog — new Dog(); Присваиваем ссылочной переменной myDog новый объект типа Dog. Другими словами, программируем пульт управления. Объект Dog Dog Для нас не имеет значения, сколько нулей и единиц хранится в ссы¬ лочной переменной. Это может зависеть от конкретной JVM или от фазы Луны.
(Не) дурацкие вопросы ^ОПрОС: Какой раз¬ мер у ссылочной пере¬ менной? Ответ: Никто точно не знает (если только вы не из команды разработ¬ чиков JVM). Ссылка — это не сам объект, а способ его найти, а вот как имен¬ но это реализовано — тайна за семью байтами. Возможно, где-то под капотом есть указатели, но они недоступны. Да и не нужны. (Хотите ори¬ ентир? Представьте, что это 64-битное значение: для воображения сойдет.) Если говорить о распре¬ делении памяти, важно не сколько ссылок вы создаете, а сколько объ¬ ектов и насколько они велики. ^ОПрОС: Получается, что размер ссылок оди¬ наковый независимо от размера объекта? Ответ: Именно. Все ссылки внутри одной JVM занимают одинаковый объем, неважно, на какой объект указывают. Но между разными JVM реа¬ лизация может отличать¬ ся — где-то ссылка будет компактнее, где-то — бо¬ лее увесистой. Главное — в пределах одной JVM они равны по размеру. Вопрос: А можно мне, как в С, делать ариф¬ метику над ссылками? Типа прибавить 1 и перейти к следующе¬ му объекту? Ответ: Нет. И еще раз — нет. Повторим хо¬ ром: «Java — это не С». Jana прикрас Интервью недели: Объектная ссылка Head First: Расскажи нам, как складывается жизнь у одной уважаемой объектной ссылки? Ссылка: Все довольно просто. Я — как пульт дистанционного управления. Меня можно запрограммировать на управление каким-нибудь объектом. Head First: А ты можешь управлять разными объектами за одну сессию? Ну, напри¬ мер, сначала ты указываешь на объект Dog, а через пять минут — уже на объект Саг? Ссылка: Ни в коем случае! Меня один раз объявили, и все. Если я ссылочная пере¬ менная типа Dog, то ни на какой Саг мне никогда не ссылаться. Не мой это тип. Head First: Ну ладно, значит, только на Dog. А только на одного Dog? Ссылка: А вот тут все веселее. Сегодня я могу ссылаться на одного Dog, а завтра — на другого Dog. Пульт один, телевизоры разные. Главное — тип совпадает. Хотя... ну, ладно, неважно... Head First: Не-не, теперь рассказывай! Ссылка: Ну, если уж настаиваешь... Если я объявлена с модификатором final, тогда всё: как только меня один раз запрограммируют на конкретного Dog, переключиться уже нельзя. Навеки вместе, понимаешь? Head First: Ага, понятно. Значит, если ты не final, ты можешь указывать на любого Dog в любой момент. А может ли быть такое, что ты вообще ни на что не указыва¬ ешь? Ссылка: Уф... да. Но это грустная тема. Head First: Почему? Ссылка: Потому что в такие моменты я — null. Пустота. Печаль. Меня ни на что не запрограммировали. Как пульт, у которого нет ни одного устройства поблизости. Можно нажимать кнопки, но ничего не произойдет. Просто пустота... одиноче¬ ство. .. Head First: То есть у тебя просто нет значения? Ссылка: Ну, null — это тоже значение. Но ты как бы существуешь... и не суще¬ ствуешь. Просто есть — и все. И это еще не самое страшное. Представь, что я была единственной ссылкой на какой-то прекрасный объект... а потом меня вдруг обну¬ лили. Всё. Объект потерян навсегда. Он становится мусором. Брошенным. Одино¬ ким. И [включается грустная музыка] подлежит удалению... Head First: Ого. То есть ты переживаешь за этот объект? Ссылка: Конечно! Мы были связаны! Мы были вместе! А теперь я null, а он обре¬ чен на сборку мусора, (всхлип) Никто даже не скажет спасибо. Только GC... и ника¬ кого прощания... Head First: Эй, эй, не расстраивайся. Ты же важна! Без тебя никакой объект не жил бы полной жизнью! Ссылка: Ты еще спрашиваешь?.. У нас с этим объектом были отношения! Настоя¬ щие. Мы были связаны... а потом — бах! — все обрывается. Внезапно. Жестоко. И я больше никогда его не увижу. Он стал мусором. Он подлежит... [звучит трагическая скрипка] удалению, (всхлипывает) Думаешь, хоть один программист остановится и подумает, что он наделал?.. (еще один всхлип) Почему, почему я не могу быть при¬ митивом? Примитивам хорошо. Им не надо ни на кого указывать. У них нет этих... связей. Ответственности. Эмоций. Только я и куча. Только я и страх потерять связь UCJ ог'ог’тта
1 Жизнь в куче Book b = new Book(); Book с = new Book(); Объявляем две ссылки типа Book. Создаем два новых объекта Book. Присваиваем ссылочным перемен¬ ным объекты типа Book. Вот так в куче поселились два объекта Book. Ссылок: 2. Объектов: 2. Луча, за которой присматривает*»0?"** Book Book Book d = с; Объявляем новую ссылку типа Book. Вместо того чтобы создавать новый, третий по счету, объект Book, присваиваем ссылке значе¬ ние переменной с. Проще говоря: «Возьми биты из с, скопируй их и помести эту копию в d». Обе переменные с и d ссылаются на один и тот же объект. Ссылки с и d хранят две разные копии одного и того же значения. Два пульта управления для одного телевизора. Ссылок: 3. Объектов: 2 Куча, за которой присматривэ Book с = Ь; Берем значение переменной Ь и присваиваем его переменной с. Теперь вы уже понимаете, что это означает. Биты внутри переменной Ь копируются, и новая копия помещается в переменную с. Обе переменные — Ь и с — ссылаются на один и тот же объект. Переменная с больше не ссылается на свой старый объект Book. Ссылок: 3. Объектов: 2. С«Ь°Р Куча, за которой присматривает
Жизнь и смерть в куче Book b = new Book(); Book с = new Book(); Объявляем две ссылочные перемен¬ ные типа Book. Создаем два новых объекта Book. Присваиваем эти объекты ссылочным переменным. Два объекта Book теперь нахо¬ дятся в куче. Активных ссылок: 2. Активных объектов: 2. Ь = Со¬ берем значение переменной с и при¬ сваиваем его переменной Ь. Биты внутри переменной с копируются, и новая копия помещается в пере¬ менную Ь. Обе переменные хранят одинаковые значения. И Ь, и с ссылаются на один и тот же объект. Объект 1 становится недоступным и теперь может быть удален сборщиком мусора (Garbage Collection — GC). Активных ссылок: 2. Доступных объектов: 1. Недоступных объектов: 1. tU^UVK KCHCU,. Он nJm(vUK йшлтние луча за которой присматривает ^са<? с<0Ор\Д№1'™ Первый объект, на который ссыла¬ лась переменная Ь, больше не имеет ссылок. Он недоступен. с = null; Присваиваем переменной с значение null. Это превращает ее в нулевую ссылку, то есть она больше ни на что не ссылается. Но это по-прежнему ссылочная переменная, и ей все еще можно присвоить другой объект типа Book. Объект 2 еще имеет активную ссылку (Ь), значит, он не может быть удален GC. Активных ссылок: 1. Нулевых ссылок: 1. Доступных объектов: 1. Недоступных объектов: 1.
Массив как поднос со стаканчиками Стандартная библиотека Java включает в себя мно¬ жество сложных структур данных, в том числе сло¬ вари, деревья и множества (см. приложение Б). Но когда вам нужен быстрый, упорядоченный и эффек¬ тивный список, выбирайте массивы, они отлично подходят. Массивы обеспечивают быстрый произ¬ вольный доступ, позволяя использовать индекс (по¬ зицию) для перехода к любому элементу массива. Каждый элемент в массиве — это один из восьми простых типов переменных (например, Large Furry Dog) или ссылочная переменная. Все, что можно поместить в переменную, можно присвоить элемен¬ ту массива того же типа. Так, в массиве типа int (int [ ]) каждый элемент может содержать значение int. В массиве Dog (Dog [ ]) каждый элемент может содержать... объект Dog? Неправильно. Вспомните, ссылочная переменная хранит только ссылку (пульт управления), а не сам объект. Так что в массиве Dog каждый элемент может содержать пульт дистанци¬ онного управления объектом Dog. Конечно, нам еще нужно создать объекты Dog... этим займемся на следующей странице. Внимательно посмотрите на рисунок. На нем показана очень важная вещь: массив — это объект, хотя это массив примитивов. О Объявите переменную массива int. Переменная массива — это пульт управления объектом Array. int[] nums ; ©Создайте новый целочисленный мас¬ сив с семью элементами и присвойте его ранее объявленной переменной массива int[] переменной nums. nums = new int[7]; Массивы всегда являются объектами, независимо от того, объявлены ли они для хранения примитивов или ссылок на объекты. О Присвойте каждому элементу массива целочисленное значе¬ ние. Помните, что элементы целочисленного массива — это просто переменные типа int. nums [ 0 ] = 6; nums [1] = 19 nums [2] = 44 nums [3] = 42 nums [ 4 ] = 10 nums [ 5 ] = 20 nums [ 6 ] = 1; int[] Массивы — тоже объекты Объект массива int (int[]) 0(f\amumt (?тс«лннt. /тс aiu.uaccuG — это стскт, дажсесшсии а и.ш нтсб’— rym tumutftx. Вы можете иметь объект массива, который объявлен для хранения простых значений. Другими словами, объект массива может содержать элементы, которые представляют собой примитивы, но сам массив — это никогда не примитив. Независимо от содержимого массива, сам он всегда остается объектом!
Создание массива Dog О Объявляем переменную-массив Dog. Dog[] pets; Создаем новый массив типа Dog длиной 7 и присваиваем его ранее объявленной пере¬ менной pets с типом Dog[J. pets = new Dog [7]; Что мы упустили? Объекты Dog! Мы имеем массив ссылок типа Dog, а не самих объектов! Dog[] Объект массива типа Dog (Dog[]) Объект массива типа Dog (Dog[]) Какое значение имеет элемент pets[2]? С помощью каких команд можно заставить элемент pets[3] ссылаться на один из двух существующих объектов Dog? Dog[] ©Создаем новые объекты Dog и присваиваем их элементам массива. Помните, элементы в массиве — это всего лишь ссылки типа Dog. Нам все еще нужны сами объекты Dog! pets[0] = new Dog(); pets[1] = new Dog(); ► Решать —вам! — У%Заточите карандаш
Java следит за типами В массив можно добавлять только те значения, тип которых совпадает с типом массива. Например, вы не сможете положить ссылку на объект Cat в массив типа Dog — и, поверьте, это к лучшему. Представьте: пользователь уверен, что 8 массиве находятся только собаки, и вызывает метод bark()„. а там кот. Такой сюрприз никому не нужен. В массив int нельзя положить double, потому что не всякое double-значение уместится в переменной типа int. Но можно добавить byte, потому что byte точно поместится в «чашку» int; это называется неявное приведение типов. Подробности мы рассмотрим чуть позже. Пока главное помнить: компилятор строго следит за тем, чтобы вы не положили в массив что-то неподходящее. Он ориентируется на объявленный тип массива и не даст вам ошибиться. Управляйте объектом Vog (с помощью ссылочной переменной) Dog fido = new Dog(); fido.name = "Фидо"; Мы создали объект Dog и использовали оператор точки(.)вместе с ссылочной переменной fido для доступа к переменной name'. Задействуя ссылку fido, можно дать объекту Dog команду bark (), eat () или chaseCate(). fido.bark(); fido.chaseCat(); Что происходит, если ссылка на Vog находится в массиве Vog? Мы можем получить доступ к переменным экзем¬ пляра и методам объекта Doge помощью опера¬ тора доступа, но пока не знаем, как это сделать. Если Dog находится в массиве, у нас нет име¬ ни переменной (вроде fido). Вместо этого мы используем обозначение массива и нажимаем кнопку на пульте (оператор «точка»), получая до¬ ступ к объекту в массиве по указанному индексу (позиции): Dog[] myDogs = new Dog[3]; myDogs[ 0 ] = new Dog(); myDogs[0].name = "Fido"; myDogs[0 J.bark(); 'Да, мы знаем, что здесь не демонстрируются возможности инкапсуляции, но пытаемся рассказать все максимально просто. Изучением инкапсуляций мы займемся в главе 4,
Пример класса Dog class Dog { String name; public static void main(String[] args) { // создаем объект Dog и обращаемся к нему Dog dogl = new Dog(); dogl .bark (); dogl.name = "Барт"; amc ofccMW II теперь создаем массив Dog Dog[] myDogs = new Dog[3]; mu»\ xam cc^aawonu u »\^xu r cfou&im* ux, kcik tatu rfo au( c£vm »\Jnauumu&iAXU ((jay(t(cШ эпиз тика). II и помещаем в него несколько собак myDogs[0] = new Dog() ; myDogs [1] = new Dog(); myDogs[2] = dogl; // теперь обращаемся к объектам Dog // через ссылки из массива myDogs[0].палю = "Фред"; myDogs[1].палю = "Мардж"; // Хммм... как зовут собаку в myDogs[2]? System.out.print("Имя последней собаки: ") System.out.println(myDogs[2].name); Output Г F'le Edit Window Help Howl %java Dog null говорит: Гав! Имя последней собаки: Барт Фред говорит: Гав! Мардж говорит: Гав! Барт говорит: Гав! // теперь проходим циклом по массиву // и говорим всем собакам лаять System.out.println(name + " говорит: Гав!«) public void eat () { ) public void chaseCat() { ) ) Без лишних «лов ■ Переменные бывают двух видов: примитивы и ссылки. а Объявленные переменные всегда должны иметь имя и тип. ■ Значение простой переменной — это биты, представляющие значение (5, 'a', true, 3.1416 ит. д.). ■ Значение ссылочной переменной — это биты, представляющие способ доступа к объекту на куче. ■ Ссылочная переменная — как пульт дистан¬ ционного управления. Использование опера¬ тора точка (.) вместе со ссылочной перемен¬ ной аналогично нажатию кнопки на пульте управления для получения доступа к методу или переменной экземпляра. ■ Ссылочная переменная имеет значение null, когда она не ссылается ни на один объект. ■ Массив — это всегда объект, даже если он объявлен для хранения примитивов. Не су¬ ществует массив простого типа, а только массив, который содержит примитивы.
^<Упос Упражнение Компилируй Каждый из Java-файлов на этой странице — полный исходный файл. Ваша задача — ненадолго стать компилятором и опреде¬ лить, скомпилируется ли каждый из этих файлов. Если найдете файл, который не прой¬ дет компиляцию, пред¬ ложите, как его исправить. class Books ( String title; String author; } class BooksTestDrive { public static void main(String[] args) { Books[] myBooks = new Books[3]; int x = 0; myBooks[0].title = "Гроздья Java"; myBooks[1].title = "Великий Java Гэтсби"; myBooks[2].title = "Java-поваренная книга"; myBooks[0].author = "боб"; myBooks[1].author = "сю"; myBooks[2].author = "ян"; while (x < 3) ( System.out.print(myBooks[x].title); System.out.print(" - автор: "); System.out.println(myBooks[x].author); x = x + 1; } class Hobbits ( String name; public static void main(String!] args) Hobbits[] h = new Hobbits[3]; int z = 0; while (z < 4) ( z = z + 1; h[z] = new Hobbits (); h[z].name = "бильбо"; if (z = 1) < h[z] .name = "фродо"; ) if (z = 2) { h[z].name = "сэм"; 1 System.out.print(h[z].name + " - это "); System.out.println("хорошее имя для хоббита"); ) ■*“ Ответы на стр. 104
ттраж \ражненив Магнитики с кодом На холодильнике в беспорядке разбросан код рабочей Java-программы. Фрагменты перепутались: кажется, будто кто-то играл ими как магнитиками. Ваша задача — переставить кусочки кода в правильном порядке, чтобы получилась рабочая программа. Она должна скомпи- лироваться и вывести нужный результат. Несколько фигурных скобок при этом, увы, упали на пол и затерялись... Так что добав¬ ляйте их столько, сколько потребуется! File Edit Window Help Sunscreen % java TestArrays остров s Фиджи остров - Косумель остров = Бермуды остров ■ Азорские острова *■ Ответы на стр. 104
бассейн фрагментов Ваша задача — поместить кусоч¬ ки кода из бассейна в пустые строки кода. Некоторые фрагменты не понадобятся, а некоторые можно исполь¬ зовать больше одного раза. Цель упражнения — создать класс, который скомпилируется и запустится, а результат вывода будет совпадать с тем, что при¬ веден ниже. Результат I File Edit Window Helf^lsermuda-” %java Triangle треугольник 0, площадь = 4.0 треугольник 1, площадь = 10.0 треугольник 2, площадь = 18.0 треугольник V = 3, площадь = Призовой вопрос! Если хотите получить призовые баллы, ис¬ пользуйте фрагменты из бассейна, чтобы заполнить пропуски в показанном выше результате работы программы. class Triangle { double area; int height; int length; (Uwma 1\!ини |\иж.и cmauuuu lltttAu* классы*, nemauu tmo щгтя нд cmjWH им* ма. дсвйтс.) public static void main(String[] args) < while ( ) { .height = (x + 1) * 2; .length = x + 4; System.out.print("треугольних " + x + " System.out.println(" = " + площадь"), area); ) x = 27; Triangle t5 = ta[2]; ta[2].area = 343; System.out.print("y = " + y); System. out. println (" , площадь t5 = + tS.area); > void setArea() { = (height * length) / 2; ) Примечание: Каждый фрагмент из бассейна можно использовать многократно! area ta.area ta.x.area ta[x].area 4, t5 area = 4, t5 area = 27, t5 area = 27, t5 area = ta[x] Triangle [] ta = newTriangle(4); ta-X: Triangle ta = new [ ] Triangle[4]; taM Jnangle [ ] ta - new Triangle[4); = setArea(); = setArea(); setAreaO; int х; int у; х = х + 1 ; ta.x intx = 0; х = х + 2; ta(x) int х = 1; int у = х; *) Q П х = х - 1; ta [x] ta = new TriangleO 30.0 ta[x) = new Trianglef); ta.x = newTriangleO; Ответы на стр. 105
Куча проблем Справа приведена короткая програм¬ ма на языке Java. Некоторые объекты и ссылочные переменные уже будут созданы до строки с комментарием // некий код. В этом упражнении вам нужно определить, какие ссылочные переменные ссылаются на какие объекты. Не все переменные будут ис¬ пользованы, а на некоторые объекты может приходиться несколько ссылок. Проведите линии, соединяющие ссылочные переменные с соответству¬ ющими объектами. Подсказка: Если вы не гений, рисуй¬ те схемы, как те, что уже были в этой главе. Используйте карандаш: линии между переменной и объектом могут потребовать корректировки! class HeapQuiz { int id = 0; public static void main(String[] args) { int x = 0; HeapQuiz[] hq = new HeapQuiz[5]; while (x < 3) { hq[x] = new HeapQuiz(); hq[x].id = x; x = x + 1; } hq[3] = hq[l]; hq[4] = hq[l]; hq[3] = null; hq[4] = hq[0]; hq[0] = hq[3]; hq[3] = hq[2]; hq[2] = hq[0]; // некий код } } Ссылочные переменные: Сштшшн каж^к mUK,f отектсм tuu агикпимш. Вогмвжт твыссьм г^шшлтся- hq[0] pi ВД hq[2] Ответы ыастр. 105 hq[4] hq[3] Объекты HeapQuiz:
Это была темная и бурная ночь. Тауни по-хозяйски вошла в комнату программистов. Она знала, что все заняты по уши, но ей нужна была помощь. Срочно. Нужно было добавить новый метод в ключевой класс, который предстояло загрузить в сверхсекретный мо¬ бильный телефон с поддержкой Java. А свободного места в куче, как вы догадываетесь, — кот наплакал. ТТятими^ путная загадка Тауни подошла к доске, набросала обзор новой функции и обвела взглядом комнату. — Ну что, друзья. Настал момент истины, — мурлыкнула она. — Тот, кто создаст самую экономичную по памяти версию этого метода, завтра летит со мной на Мауи... на вече¬ ринку запуска проекта {...и поможет поставить обновление, конечно). Утром Тауни вернулась. — Дамы и господа! — сказала она с улыбкой. — Самолет вылетает через не¬ сколько часов. Покажите, что у вас получилось. Первым вышел Боб. Он быстро подошел к доске и написал: Contact!] contacts = new Contact[10]; while (x < 10) ( // создаем 10 объектов Contact contacts[x] = new Contact!); x. = x + 1 ; } // здесь идет сложная обработка списка contacts — Да, я знаю, памяти мало, — сказал Боб. — Но в задании явно сказано, что доступ ко всем десяти контактам должен сохраняться. Это лучшее, что я смог придумать. Следующей была Кейт. Она уже мысленно стояла на пляже с кокосовым коктейлем. — Боб, по-моему, ты слишком размахнулся, — сказала она. И написала: Contact contactRef; while (х < 10) { // создаем 10 объектов Contact contactRef = new Contact!); x. = x. + 1; } // здесь тоже идет сложная обработка, но с contactRef — Не спеши, Кейт, — сказала Тауни. — Побеждает Боб. Все замерли. — Но... почему?! — выдохнула Кейт. — Он ведь тратит гораздо больше памяти! Почему Тауни предпочла метод Боба методу Кейт, хотя метод Кейт использовал меньше памяти? >- Ответы на стр. 105
Ответы Заточите карандаш (со стр. 88) 1. int х = 34.5; X 7 s = У'Х 2. boolean boo = х/Х^- byte b = 3;у/ 3. int g = 17; у/ 4. int у = g; у/ 5. у = у + 6. short s; 9. byte v = b;yX 10. short n = 12; v/ 11. v = n;X 12. byte k = 128;X Матигикис кодом (со стр. 100) class TestArrays { public static void main (String [] args) { int[] index = new int [4]; index[0] = 1; index[1] = 3; index[2] = 0; index[3] = 2; String[] islands = new String[4]; islands[0] = "Бермуды"; islands[1] = "Фиджи"; islands[2] = "Азорские острова"; islands[3] = "Косумель"; int у = 0; int ref; while (y < 4) { ref = index [y] ; System, out.print ("остров = ") ; System.out.println(islands[ref]); у = у + 1; ) 1 ) File Edit Window Help Sunscreen % j ava TestArrays остров = Фиджи остров = Косумель остров = Бермуды остров = Азорские острова Компилируйте (со стр. 99) class Books { String title; String author; } class BooksTestDrive ( public static void main(String[] args) ( Books[] myBooks = new Books[3]; A int x = 0; myBook$[0] = new BooksO myBooks[l ] = new BooksO myBooks[2] = new BooksQ JUauuunc.Hn c(fuKtiM\ /Зоек. myBooks[0].title = "Гроздья Java"; myBooks[l].title = "Великий Java Гэтсби"; myBooks[2].title = "Java-поваренная книга"; myBooks[0].author = "боб"; myBooks[1].author = "сю"; myBooks[2].author = "ян"; while (x < 3) { System, out.print(myBooks[x].title); System.out.print(" - автор: "); System.out.println(myBooks[x].author); x = x + 1; class Hobbits ( String name; Б public static void main(String[] args) ( Hobbits [] h = new Hobbits[3]; while (z < 2){ z = z + 1; < ш/ийго h[z] = new Hobbits () ; h[z].name = "бильбо"; if (z = 1) { h[z].name = "Фродо"; SAtMwrna! ) if (z = 2) { h[z].name = "сэм"; System.out.print(h[z] .name + " - это ), System.out.println("хорошее имя для хоббита ), I }
ОтЬеты на упражнения бассейн фрагментов (со стр. 101) class Triangle { double area; int height; int length; public static void main(String!] args) { intx=0; Triangle!] ta = new Triangle[4]; while (x < 4) { ta[x] = new Triangle!); ta[x].height = (x + 1) * 2; ta[x], length = x + 4; ta[x].setArea(); System.out.print("треугольник " + x + ", площадь"); System.out.printlnf' = " + ta[x].area) ; x = x + 1; i inty=x; x = 27; Triangle t5 = ta[2]; ta[2].area = 343; System.out.print("y = " + y); System.out.println(", площадь t5 = " + t5.area); void setArea() ( area = (height * length) / 2; } %java Triangle треугольник 0, площадь =4.0 треугольник 1, площадь = 10.0 треугольник 2, площадь = 18.0 треугольник 3, площадь = 28.0 у = 4, площадь t5 = 343.0 'Тайна на пять минут (со стр. ЮЗ) Разгадка дела о похищенных ссылках. Тауни обнаружила в решении Кейт серьезную проблему. Да, Кейт действительно использовала меньше ссылок, чем Боб, но у ее метода был один фатальный недостаток: невозможно было полу¬ чить доступ ни к одному из объектов Contact, кроме последнего. Почему? Потому что на каждой итерации цикла переменной contactRef присваивался новый объект. В результате предыдущий объект стано¬ вился недоступным: никакая другая перемен¬ ная на него не ссылалась. Он уходил прямиком в мусор. Итог: метод Кейт не сохранял доступ к девяти из десяти объектов. Программа не могла с ними работать. Красиво, но бесполезно. А у Боба все было под контролем: все 10 объектов сохранялись в массиве contacts, и к каждому можно было обратиться в любой момент. Поэтому побеждает Боб. (И да, программа стала настоящим хитом. Клиент был в восторге и по¬ дарил Тауни и Бобу еще неделю на Гавайях.) Если дочитаете эту книгу до конца, такие бонусы могут ждать и вас. Куча проблем (соар. юз) Ссылочные переменные: Объекты HeapQuiz:
4. Методы содержат переменные экземпляра Повадки объектов в Java Состояние влияет на поведение, а поведение — на состояние. Вы уже знаете, что у объектов есть состояние (переменные экземпляра) и поведение (методы). Но теперь пришло время взглянуть на связь между ними. Каждый экземпляр класса — это уникальный объект со своими соб¬ ственными значениями. Возьмем, например, класс Dog. Один объект этого класса может быть Фидо, весом 30 килограммов. Второй — Киллер, 4 кило живого огня. Поведение у них будет разным, и все дело — в их состоянии. Допустим, у класса Dog есть метод makeNoise(). Звук, который издает собака, может зависеть от ее веса: если weight < б, то она тявкает, а если больше — гавкает солидно и низко. Вот так методы «заглядывают» в переменные экземпляра, чтобы решить, что делать. А еще бывает на¬ оборот — поведение меняет состояние: собака стала чуть тяжелее. Может, уже и голос погрубел? В этом и есть вся прелесть объектно-ориентированного подхода: поведение тесно связано с состоянием. Гото¬ вы научиться изменять состояние объекта? Погнали!
Помните: класс описывает, что объект знает и делает Класс — это шаблон для объекта. Создавая класс, вы описываете, как JVM должна создать объект указанного типа. Как мы уже говорили, каждый объект определенного типа может иметь разные значения переменных экземпляра. Справедливо ли это и для методов? Может ли метод каждого из объектов определенного типа иметь собственное поведение? Ну... что-то вроде’. Переменные экземпляра (состояние) Методы (поведение) Знает Делает Все экземпляры конкретного класса имеют одни и те же методы, которые могут вести себя по- разному, в зависимости от значений переменных экземпляра. Класс Song содержит две переменных экземпля¬ ра — title и artist. Метод play () проигры¬ вает композицию, выбор которой будет зависеть от поля title для конкретного экземпляра. Вы¬ зывая метод play () одного экземпляра, вы услы¬ шите песню «Havana» Кабелло, а другой экземпляр воспроизводит композицию «Sing» группы Travis. Тем не менее код метода остается одинаковым. void play () { soundPlayer.playSound(title, artist)• IW'V Song songl = new Song(); songl.setArtist("Travis"); songl.setTitle("Sing"); Song song2 = new Song(); song2.setArtist("Sex Pistols"); song2.setTitle("My Way"); -ffifiaap Song si.play () Вил В.липшм tidauO uy эnnjc ЭкамКл.яра npи Cum ^ccnhcuy^uftKUHj nemu WtnA m тем, mu иетшшт Cunamfui). 'Еще один ответ, снимающий все вопросы!
Громкость лая пропорциональна размеру Лай большой собаки всегда отличается от лая мелкой собачки. Класс Dog содержит переменную экземпляра size, которая используется методом bark () для определения того, как будет звучать лай собаки. Dog size name bark () class Dog { int size; String name; void bark() { if (size > 60) { System.out.printIn("ГаЕ ! ГаЕ!"); ) else if (size > 14) { System.out.printIn("P-p-p ! P-p-p!"); } else ( System.out.println("Тяв! Тяв!"); } } ) class DogTestDrive ( public static void main(String[] args) { Dog one = new Dog(); one.size = 70; Dog two = new Dog () ; two.size = 8; Dog three = new Dog(); three.size = 35; one.bark(); two.bark() ; three.bark(); } I File Edit Window Help Playdead %java DogTestDrive Гав! Гав' Тяв! Тяв! P-p-p! P-p-p!
Вы можете передавать методу разные значения Как и в любом другом языке программирования, в Java вы можете передавать значения своим методам. Например, дайте команду объекту Dog, сколько раз он должен пролаять: d.bark(3); Для обозначения передаваемых в метод значений вы можете использовать два термина — аргументы или параметры. Это во многом зависит от опыта и личных предпочтений. С научной точки зрения эти понятия не тождественны и имеют формальные различия, но это мелочи. Вы можете называть их как вам угодно, но мы будем придерживаться следующих правил. Вызывающий код передает аргументы. Метод использует параметры. Аргументы — это значения, которые вы передаете методам. Аргумент (значение наподобие 2, Foo или ссылки на объект Dog) превращается в параметр. А пара¬ метр — это не что иное, как локальная переменная. То есть переменная с именем и типом, которая может быть использована внутри метода. Но есть один важный момент: если метод принимает параметр, вы обязаны ему что-нибудь передать. При этом передаваемое значение должно быть соответству¬ ющего типа. Вызываем метод bark из ссылки на объект Dog и передаем ему значение 3 (в качестве аргумента для метода). Л tyi \ Dog d = new Dog() ; d.bark(3); Биты, представляющие цело¬ численное значение 3, переда¬ ются в метод bark. ©Биты помещаются в параметр numOf Barks (переменная типа i int). void bark(int numOfBarks) { while (numOfBarks >0) { System.out.printIn("ruff") ; numOfBarks = numOfBarks - 1; О Используем параметр numOfBarks в качестве переменной внутри метода. } }
Значения из метода можно возвращать Методы могут возвращать значения. Каждый метод объявляется с указанием типа возвращаемого значения. Пока мы использовали только тип void — то есть метод, который ничего не возвращает. void go() { } Можно объявить метод, указав конкретный тип значения, возвращаемого вызывающему коду: int giveSecret() { return 42; } Если вы объявили метод, который возвращает значение, то должны вернуть значение указанного типа! (Или совместимое с ним. Подробнее об этом мы поговорим в главах 7 и 8, когда будем изучать полиморфизм.) Что бы вы ни пообещали, вам лучше это вернуть! Компилятор н. /ишаитп тстнш птоахашШ'Ш тмля. 1 6 ) int giveSecret() { return(427 = life.giveSecret()• jfnuiui. mam I\l MUmn } -)ни литнш умжнс ссстштстМсипь гост типу int!
Методу можно передать сразу несколько значений. Методы могут содержать несколько параметров. Объявляя параме¬ тры, выделяйте их запятыми. Также необходимо выделять запяты¬ ми аргументы, которые вы передаете. Самое главное: если метод имеет параметры, передавать ему аргументы необходимо в правиль¬ ном порядке и соответствующего типа. Вызов метода с двумя параметрами и передана ему двух аргументов, void go() { TestStuff t = new TestStuff(); t.takeTwo(12, 34)- void takeTwo (int x, . int z = x + y; System.out.println("Сумма: " + > \ > Можно передавать переменные в метод, если их типы совпадают с типами параметров. void go() { int foo = 7 int bar = 3 t.takeTwo(f } void takeTwo(int x, int y) { int z = x + y;
int x = 7; В Java передача осуществляется по значению. Это значит, что передается копия. Объявляем целочис¬ ленную переменную и присваиваем ей зна¬ чение 7. В переменную х записывается битовая последовательность, со¬ ответствующая числу 7. 7 ИЩ Помыть 1 Потоки 1 |М 1 woitO notifyO wm ' ши— Вбить в голову Потоки woitO notifyO Розы красные Стихи неровные Передать по значению — Это как копии условные. Ну конечно, можете подумать, что у вас получит¬ ся лучше1. А что, может и правда так? Попробуйте! Вторая строчка у нас, скажем честно, вышла не очень — замените ее на свою. А еще лучше — придумайте свой собственный стих. Так вы точно запомните это навсегда. void go(int z) { } Ш © int Объявляем метод с целочисленным параметром 2. foo. Вызываем метод аоП передавая ему переменную х в качестве ар^иейт" бЖиз х копируются и передаются параметру 2. к ,г останется прежним, ерше ест измен ится г ^ 0 VJ int void go(int z){ z = 0; } Изменяем значение 2 внутри метода. А значение х остается прежним! Аргумент, ставший параметром 2, — всего лишь копия х. Метод не может изменить содержимое переменной х из программы, в которой он был вызван.
(Не) дурацкие вопросы Вопрос: Что происходит, если аргумент, который нужно передать, — объект, а не примитив? Ответ: Подробно об этом поговорим в следующих главах, но суть вы уже знаете: в Java всё передается по значению. Всё. Но... значение — это биты, хранящиеся в перемен¬ ной. Напомним: в переменной не сам объект, а только ссылка на него — как пульт дистан¬ ционного управления. То есть, если вы пере¬ даете в метод ссылку на объект, вы передаете копию пульта управления этим объектом. Продолжайте учиться по этой книге, и вы еще не раз вернетесь к этой теме. ВоПрОС: Может ли метод объявить не¬ сколько возвращаемых значений? Есть ли способ вернуть более одного значения? Ответ: Метод может объявить только одно возвращаемое значение. Но если нужно вернуть, например, три целых числа, можно положить их в массив и вернуть массив. Когда значения разного типа — сложнее, но с этим тоже можно справиться. В следующей главе, когда будем говорить об ArrayList, вы это увидите. Вопрос: Должен ли я возвращать тот са¬ мый тип, который указал при объявлении? Ответ: Нет, не обязательно. Вы можете вернуть значение, которое неявно приво¬ дится к этому типу. Например, можно вернуть byte, если в объявлении указан int: компиля¬ тор с этим справится. А если хотите вернуть что-то большего типа, чем заявлено, исполь¬ зуйте явное приведение типов. ВоПрОС: Что делать с возвращаемым значением? Можно ли его просто проигно¬ рировать? Ответ: В языке Java никто не заставляет вас использовать возвращаемое значение. Если вызываете метод ради работы, которую он выполняет (а не ради результата), возвра¬ щаемое значение можно никуда не присваи¬ вать. Java этого даже не заметит. Напоминание: Java заботится о типах! Java — настоящий страж порядка. Она не даст вам скормить кролику жирафа, потому что знает: тип данных — это святое. Если ме¬ тод объявлен так, что должен вернуть Rabbit, вы не сможете вернуть ему Giraffe. И наобо¬ рот: если метод принимает Rabbit в качестве параметра, не пытайтесь подсунуть туда Giraffe. — Без лишних слов: ■ Классы определяют, что объект знает и что он умеет. ■ Знания, которыми обладает объект, — это переменные экземпляра (состояние). ■ Действия, которые способен выполнять объект, — это методы (поведение). ■ Поведение объектов одного типа может различаться, поскольку методы могут использовать значения переменных экземпляра. ■ Метод может содержать параметры, то есть возможно передавать ему одно или несколько значений. ■ Количество и тип передаваемых значений должны соответствовать порядку следования и типу параметров, объявленных в методе. ■ Значения, принимаемые и возвращаемые методом, могут быть неявно приведены к более вместительному типу или явно приведены к менее вместительному. ■ Значения, передаваемые в метод в качестве аргументов, могут быть литералами (2, ‘с’ и т. д.) или переменными объявленного типа (например, целочисленная переменная х). Есть и другие объекты, которые можно передавать в качестве аргументов, но мы с ними еще не познакомились. ■ Для метода должен быть объявлен тип возвращаемого значения. Тип void означает, что метод ничего не возвращает. ■ Если тип возвращаемого значения, объявленный для метода, не void, он должен возвращать значение, совместимое с объявленным типом.
Применение параметров и возвращаемых значений Познакомившись с работой параметров и возвращаемых значений, начнем применять их на практике: давайте созда¬ дим геттеры и сеттеры. В некоторых источниках их называют accessors и mutators. Можете называть их так, если угодно, но именно термины «геттеры» и «сеттеры» хорошо вписываются в соглашение по именованию, принятое в Java, поэтому мы будем исполь¬ зовать эти слова. Геттеры и сеттеры позволяют получать и устанавливать значения. Как правило, это значения переменных экземпля¬ ра. Единственное предназначение геттера — вернуть значе¬ ние (по аналогии с возвращаемым значением). Наверное, вы уже поняли, чем живет и дышит сеттер. Это возможность получить значение аргумента и использовать его для уста¬ новки значения переменной экземпляра. class ElectricGuitar { String brand; int numOfPickups; boolean rockStarUsesIt; ElectricGuitar - brand i numOfPickups rockStarUsesIt getBrand() setBrand() getNumOfPickups() setNumOfPickups() getRockSta rUses lt() setRockStarllsesttO // марка гитары II количество звукоснимателей // используется ли рок-звездой С JhuuumRiu: ump эпшдг ссгдоишим. и с ндилшгс- (илиш. (?м сосшшаапс. стандарт, принятии lava. String getBrand() { return brand; i void setBrand(String aBrand) { brand = aBrand; ) int getNumOfPickups() { return numOfPickups; } void setNumOfPickups(int num) { numOfPickups = num; ) boolean getRockStarUsesIt() { return rockStarUsesIt; > void setRockStarUsesIt(boolean yesOrNo) { rockStarUsesIt = yesOrNo; > »
Инкапсуляция Работайте с ней, иначе насмешек не избежать До этого знаменательного момента мы придерживались одного из самых неесте¬ ственных стилей в ООП (о мелких нару¬ шениях мы даже не говорим). Речь идет о крупной ошибке. В чем же она заключается? Мы раскрыли данные! Вот так мы насвистываем себе что-то под нос, не заботясь о том, что наши данные брошены на произвол судьбы и любой мо¬ жет их увидеть и даже потрогать. Переменные экземпляра открыты, и, воз¬ можно, вы уже чувствуете смутное беспо¬ койство. Открыты означает доступны с помощью оператора «точка», как в этом случае: theCat.height = 27; Только представьте! Любой человек может использовать пульт управления, чтобы изменить переменную экземпляра size нашего объекта Cat. Оказавшись в руках постороннего человека, ссылочная пере¬ менная (пульт управления) может стать довольно опасным оружием. Это нужно предотвратить. С dew этого JJW tu f4'" theCat.height /itmuouC <?«дс лтшд sdb-t, tm.uowt.H гатДпити сглскт (at om. нтрислл. u.Xmx м^мштий .mzl. Это будет большой ошибкой. Необходимо создать методы сеттеры для всех пере¬ менных экземпляра и найти способ, чтобы весь остальной код вызывать сеттеры, а не обращаться напрямую к данным. public void setHeight(int ht) { if (ht > 9) { height = ht; } }
Скрываем данные Решение очень простое. Уйти от реализации, которая провоцирует прием плохих данных, к подходу, защищаю¬ щему ваши данные и дающе¬ му вам возможность вносить изменения спустя некоторое время. Но как именно спрятать данные? Это можно сделать с помощью модификато¬ ров public и private. Модификатор public уже встречался в примерах и вам знаком. Мы использовали его в каждом основном методе. Первое правило инкапсуля¬ ции, выведенное на основе опыта множества разработ¬ чиков, гласит (все оговорки, присущие таким правилам, остаются в силе): помечайте свои переменные экземпляра модификатором private, добавляя публичные (public) геттеры и сеттеры для контроля за доступом. Когда вы наработаете соб¬ ственный опыт в проекти¬ ровании и кодировании на языке Java, вы скорректируете подход. Пока вы только на¬ чинаете, используйте пред¬ ложенный способ, чтобы обезопасить себя. Переменные экземпляра должны быть приватны. А геттеры и сеттеры — публичны. «К несчастью, Билл забыл инкапсулировать свой класс Cat — и в итоге получил приплюснутого кота». (Подслушано возле кулера с водой.) P'Jaua Щ прикрас Интервью недели: Откровенно об инкапсуляции. Рассказывает Объект. HeadFirst: Почему инкапсуляция так важна? Объект: Вы когда-нибудь оказывались в ситуации, когда выходите на сце¬ ну перед сотней разработчиков, чтобы рассказать о своем классе... и вдруг понимаете, что вы — в прямом смысле — голый? HeadFirst: Ну, было нечто похожее... Но я был в спортзале, и... ладно, не суть. Так вот — вы чувствуете себя уязвимым? Объект: Уязвимым? УЯЗВИМЫМ?! (Хохочет.) Простите, но вы не пред¬ ставляете, насколько! Эй, вы слышали? Он спрашивает, есть ли тут какая- то опасность! (Катается по полу от смеха.) HeadFirst: Мне казалось, вопрос вполне серьезный. Объект (вытирая слезы): Да-да, все так... попробую ответить без истери¬ ки. Глубокий вдох... Поехали. HeadFirst: Так от чего же вас защищает инкапсуляция? Объект: От хаоса. Она создает вокруг моих переменных экземпляра за¬ щитный купол. Никто не сможет подбросить мне значения, которые могут испортить все вокруг. HeadFirst: Например? Объект: Миллион примеров! У вас переменная numberOfBathrooms, а кто-то присваивает ей -3. Сюрприз: теперь у вас минус три туалета в офисе! Или скорость самолета: кто-то задает 10 000 км/ч — и вот вы уже на Луне. А если кто-то поставит ноль в birthdayMonth? Ну, удачи праздновать день рождения в нулевом месяце. HeadFirst: Ха, ну хорошо, согласна. Как именно инкапсуляция помогает избежать таких ситуаций? Объект: Очень просто — вы не даете прямого доступа к переменным. Если кто-то хочет изменить значение — пожалуйста, пусть обращается к сеттеру. А сеттер не робкого десятка: он проверит, все ли в порядке, и если что — отклонит, подправит, округлит или вовсе бросит исключе¬ ние. Все под контролем. HeadFirst: А если сеттер просто тупо записывает значение без всякой логики? Тогда какая разница? Объект: Вся фишка в том, что вы МОЖЕТЕ потом передумать. Пока сеттер на месте, вы всегда можете добавить в него проверку или изменить поведение — и никто не пострадает. А вот если переменная была откры¬ той, и ею уже воспользовались в 50 других классах... о, берегись, начнется каскад боли. С инкапсуляцией вы сохраняете контроль и свободу для маневра. HeadFirst: Даже если немного потеряете в производительности? Объект: Пфф... Против стабильно работающей программы? Вы всерьез сравниваете две наносекунды с бессонной ночью отладки? Я лучше пойду медленно, но уверенно. И одетым.
Инкапсуляция на примере класса GoodDog class GoodDog ( private int size; ^ public int getSizeO [ 9K|C-W ^ return size; n«'‘iu|x attmnvf' ►public void setSize(int s) size = s; ) Хотл зтчдатм hi мМаяют hut w iu it финкм^нллбнтпн, ivc 'аптка С Цжжпосш^мтт, U^WHmUA.V |шп\к я и IH ЛД,U/W IAV.V » ■ i4 i (iVum/uv « «71 , л» <4пи..штед <у-«я<-ш£. void bark() ( if (size > 60) j System.out.printin("Гав! Гав!") ; } else if (size > 14) { System.out.printin("P-p-p! P-p-p!" } else { System.out.printin("Тяв! Тяв!"); } Там, где может быть использовано кон¬ кретное значение, можно использо¬ вать вызов метода, который возвраща¬ ет этот тип. вместо: int х = 3 + 24; можно использовать: int х = 3 + one.getSizeQ; ) class GoodDogTestDrive { public static void main(String[] args) GoodDog one = new GoodDog(); one.setSize(70); GoodDog two = new GoodDog(); two.setSize(8); System.out.println("Собака один: " + System.out.println("Собака два: " + one.bark(); two.bark(); } } GoodDog size getSize() setSize() bark() one.getSize()); two.getSize());
Как себя ведут объекты в массиве Объекты в массиве ведут себя так же, как и любые другие объекты. Разница только в способе доступа к ним — вы получаете «пульт управления» не по имени переменной, а по индексу в массиве. Чтобы вызвать метод объекта Dog, хранящегося в массиве, нужно обратиться к нему по индексу. О Объявляем и создаем массив для хранения семи ссылок типа Dog. Dog[] pets; pets = new Dog[7] Dog[] Объект массива типа (bog[]) О о Создаем два новых объекта Dog и присваиваем их первому и второму элементам массива. pets[0] = new Dog(); pets[1] = new Dog(); Dog[] Объект массива типа (Dog[]) Вызываем методы из двух объектов Dog. pets[0].setSize(30); int x = pets[0].getSize(); pets[l].setSize(8);
Объявление и инициализация переменных экземпляра Ранее мы уже обсуждали, что при объявлении пере¬ менных необходимо указывать их имя и тип: int size; String name; Еще мы говорили о том, что присвоить значение пере¬ менной, то есть инициализировать ее, можно сразу: int size = 420; String name = "Донни"; Но если вы не инициализировали переменную эк¬ земпляра, что произойдет, если вы вызовете геттер? Иными словами, какое значение переменная экземпля¬ ра содержит до того, как вы ее инициализируете? class PoorDog { private int size private String name; ЯТПР * ^ public int getSize() return size; } Что (fyuu^ra эта,.«техр? Переменные экзем¬ пляра всегда получа¬ ют значение по умол¬ чанию. Даже если вы явно не присвоите значение перемен¬ ной или не вызыва¬ ете сеттер, она все равно содержит зна¬ чение! Целые О С плавающей точкой 0.0 Логические false Ссылочные null public String getName() { return name; } public class PoorDogTestDrive { public static void main(String[] args) { PoorDog one = new PoorDog(); System.out.println("Размер собаки: " + System.out.printin("Кличка собаки: " + > ) File Edit Window Help Cal I Vet % java PoorDogTestDrive Размер собаки: 0 Кличка собаки: null one.getSize())• one.getName()); Каан^егеал^ибцmi ne|\e.«синие жжипля t\a нс nip™. они (tcaqa тлеют m/mnm no илк.шшпк. Чмсжш приитише (включая chat) пелтлют лишние 0. логатескис — jaCx, a cctsi mme пеМышнпс — naff (Жапомшшм: mcumui naff исшмйшп, ■mu пюлет дагтандулокисм) иппцьииая на¬ геле не ifnfuidiMnn. or не
Разница между переменными экземпляра и локальными переменными ОТТереЛЛеННЫе экземпляра объявляются внутри класса, но вне метода. class Horse { private double height = 15.2; private String breed; // more code... } ©Локальные переменные объявляются внутри метода. class AddThing { int a; "L Jf<ju.HHfU 9K|< w,y' int b = 12; ' Локальные перемен¬ ные не имеют значе¬ ния по умолчанию! Компилятор откажет¬ ся работать, если вы попробуете исполь¬ зовать локальную переменную без ини- циализации. public int add() { int total = a + b; return total; -Аскаиная пци.шн.ная ) } ©Прежде чем использовать ЛОКОЛЬНуЮ перемен¬ ную, ее обязательно нужно инициализировать! class Foo { public void go() { int x; int z = x + 3; } :ш и* скам V X c •A цлятф File Edit Window Help Yikes % javac Foo.java Foo.java:4: variable x might not have been initialized int z = x + 3; 1 error л (He) дурацкие вой росы бои pОС: Как насчет параметров метода? Применяются ли к ним те же правила, что и к локальным переменным? Ol/HBeW: Параметры метода — это почти то же самое. Они объявляются внутри метода (формально — в спи¬ ске параметров, а не в теле метода), но по сути это локальные перемен¬ ные, а не переменные экземпляра. Главное отличие — параметры всегда инициализированы. Поэтому компи¬ лятор не станет ругаться, будто пара¬ метру не присвоено значение. Если вы забудете передать аргументы при вызове метода, компилятор вы¬ даст другую ошибку — о несоответ¬ ствии количества или типов аргумен¬ тов. Так что можно не переживать: параметры всегда получают значения автоматически — благодаря аргумен¬ там, переданным при вызове метода.
Сравниваем переменные (примитивы или ссылки) Бывают ситуации, когда необходимо понять, одинаковы ли два прими¬ тива. Например, вам надо сверить результат int с ожидаемым целым значением. Сделать это легко, воспользуйтесь оператором ==. Порой разработчик хочет узнать, указывают ли две ссылки на один и тот же объект в куче. Например, это тот же самый объект Dog, с которого была начата работа? Это тоже легко выяснить: поможет тот же оператор ==. Но иногда нужно узнать, равны ли два объекта. И для этого понадобится метод equals (). Равенство (или эквивалентность) объектов зависит от их типа. В случае, если два объекта типа String содержат одинаковые символы (скажем, слово «мое имя»), то они эквивалентны, хотя это два разных объекта в куче. А что можно сказать про объекты типа Dog? Будете ли вы рас¬ сматривать два объекта этого типа как эквивалентные, если их значения переменных size и weight одинаковы? Скорее всего, нет. Таким об¬ разом, два объекта должны считаться эквивалентными, если это имеет смысл для конкретного типа. В дальнейшем мы вернемся к изучению вопроса об эквивалентности объектов. На данном этапе важно понять, что оператор == используется только для сравнения битов в двух пере¬ менных. Не имеет значения, что эти биты собой представляют. Они либо одинаковые, либо нет. Используйте опе¬ ратор ==, чтобы сравнить два при¬ митива или узнать, ссылаются ли две переменные на один и тот же объ¬ ект. Используйте метод equalsQ для провер¬ ки равенства двух разных объектов. (Например, два разных объекта типа String, содержащие сим¬ волы "Fred"). Для сравнения двух примитивов применяйте оператор == С помощью оператора == можно сравнить две переменные любого типа — он просто проверяет на соответствие их биты. if (а == Ь) (...) видит биты внутри а и Ь и возвращает true, если они одинаковые (при этом нули на левом конце не имеют значения). int а = 3; byte b = 3; if (а == Ь) { // true } Чтобы проверить, равны ли две ссылки (ссылаются ли они на один объект в куче), используйте оператор == tUTtcHUA цмх ,ла) пойменные Попишки ых ги>ы\иптт(М л\ ■Шсл(т£ап\с инюспи. Запомните: оператору == важна только последовательность битов в пере менной. Это справедливо и для ссылок, и для примитивов. Таким обра¬ зом, оператор == возвращает true, если на один объект ссылаются две переменные! Значения конкретных битов не известны (эта информация скрыта от нас JVM), но нам точно известно, что две ссылки на один объ¬ ект будут хранить одинаковые данные. Foo a = new 4 о о О ; Foo b = new ч о о О ; Foo c = a; if (a == b) { } // false а ( Й [(Ш if (a == c) { } // true II II if (b == c) { } // false
Без лишних слов ■ Инкапсуляция — это способ сказать миру: «Мои данные — мои правила». Она позволяет контроли¬ ровать, кто и как может изменять состояние объекта ■ Сделайте переменные private, чтобы ни один любо¬ пытный метод из другого класса не сунул туда свой нос ■ Хотите, чтобы другие классы все же могли взаимо¬ действовать с данными? Создайте сеттер (public- метод), внутри которого можно добавить проверку: «Ты уверен, что хочешь присвоить это значение?» ■ Переменные экземпляра получают значения по умолчанию, даже если вы им ничего не задали (на¬ пример, int — это 0, boolean — false, и т. д.) ■ Локальные переменные в методах по умолчанию ничем не инициализируются: Java не позволит вам даже компилировать код, если переменная не опре¬ делена ■ Используйте оператор ==, если сравниваете при¬ митивы — 5 это 5, и все просто ■ Тот же оператор == применим к ссылкам, чтобы по¬ нять, ссылаются ли они на один и тот же объект (то есть на один и тот же пульт управления) ■ Если же вы хотите сравнить эквивалентность объ¬ ектов, например, две строки с одинаковым текстом, используйте метод .equalsQ: он посмотрит, что внутри, а не на то, где объект лежит в памяти #^1аточите карандаш Какие вызовы допустимы? Взгляните на метод, представленный ниже, и определите, какие из пере¬ численных справа вариантов вызова допустимы. Отметьте флажками те вызовы, которые компилятор примет (некото¬ рые строки содержат присваивания значений, которые затем используются в вызове метода). Держитесь Ф правее int calcArea(int height, int width) { return height * width; } int a = calcArea(7, 12); short c = 7; calcArea(c, 15); int d = calcArea(57); calcArea(2, 3); long t = 42; int f = calcArea(t, 17); int g = calcArea(); calcArea(); byte h = calcArea(4, 20); *• Ответы на стр. 129, int j = calcArea(2, 3, 5);
^траж \ражнение Компилируй Каждый Java-файл на этой странице — полноценный исходный код. Ваша за¬ дача — представить себя компилятором и определить, скомпилируется ли каж¬ дый из них. Если какой-то файл не прой¬ дет компиляцию, подумайте, как его можно исправить. А если все-таки компиляция пройдет успешно — каким будет ре¬ зультат выполнения программы? А class ХСору { public static void main(String[] args) { int orig = 42; XCopy x = new XCopyO; int у = x.go (orig) ; System.out.println(orig + " " + y); // 42 84 i int go(int arg) { arg = arg * 2; return arg; } Б class Clock { String time; void setTime(String t) ( time = t; ) void getTime() ( return time; 1 class ClockTestDrive { public static void main(String!] args) { Clock c = new Clock(); c.setTime("1245"); String tod = c.getTime(); System.out.println("время: " + tod); ) } *■ Ответы на стр. 129.
ражнвнив Компания замаскированных Java-компонентов предлагает сыграть в игру «МаскаКод». Попробуйте угадать, кто скрывается за каждой маской, пользуясь подсказками, которые они вам дают. Считаем, что каждый из участников говорит о себе правду. /А ссксМ Если фраза подходит сразу нескольким персонажам — запишите всех, кому она может принадлежать. В пустые строки впишите имена одно¬ го или нескольких участников рядом с каждым утверждением. Сегодня в игре участвуют: переменная экземпляра, аргумент, return, геттер, сеттер, инкапсуляция, public, private, передача по значению, метод. Класс может содержать любое их количество. Метод может возвращать только один такой элемент. Он может быть неявно расширен. Я предпочитаю, чтобы мои переменные экземпляра были закрытыми. Это означает «создать копию». Только сеттер должен иметь возможность их изменять. У метода может быть много таких элементов. Очевидно, я что-то возвращаю. Меня не следует использовать с переменными экземпляра. У меня может быть много аргументов. По определению я принимаю один аргумент. Это помогает внедрять инкапсуляцию. Я — одиночка. Ответы на стр. 129.
Противоречивые результаты В рамке справа — код небольшой Java- программы. Но увы, два ее фрагмента потерялись. Ваша задача — сопоставить варианты блоков кода (они приведены ниже) с результатами вывода, которые появятся, если эти блоки будут вставлены в нужное место. Некоторые строки вывода не понадобятся вовсе, а другие могут встретиться несколько раз. Соедините линиями фрагменты кода с соответствующими строками вывода, которые они вызывают. Варианты: Возможные результаты: i < 9 14 7 index <5 9 5 i < 20 19 1 index <5 14 l i < 7 25 1 index <7 7 7 index <1 20 5 public class Mix4 { int counter = 0; public static void main (String[] args) ( int count = 0; Mix4[] mixes = new Mix4[20]; int i = 0; while ( ) { mixes[i] = new Mix4(); mixes [i] .counter = mixes [i].counter +■ 1; count = count + 1; count = count + mixes[i].maybeNew(i); i = i + 1; System.out.println(count + " " + mixes[1].counter); ) public int maybeNew(int index) { if ( | 1 ) { Mix4 mix = new Mix4(); mix.counter = mix.counter + 1; return 1; } return 0; } } +» Ответы на стр. 130
бассейн фрагментов В бассейне плавают фрагменты кода, а вам предстоит собрать из них работающий класс. Ваша задача — вставить фраг¬ менты в пропущенные строки кода. Некоторые фрагменты не понадобятся, а остальные можно использовать только один раз. Цель упражнения — получить такой Java- класс, который: успешно скомпилируется; запустится без ошибок; выведет на экран ровно тот результат, который указан ниже. public class Puzzle4 { public static void main(String [] args) { int number = 1; int i = 0; while (i < 6) ( number = number * 10; ) int result = 0; i = 6; while (i > 0) { ♦ Ответы на стр. 130 result = result + Результат: I File Edit Window Help ItehyFlop | %java Puzzle4 результат 543345 Примечание: каждый фрагмент кода из бассейна может быть использован только один раз! } System.out.println("результат " + result); class { int intValue; \ doStuff(int if (intValue > 100) ( return ) else { return ) ) ) f intValue = i; values.intValue = i; values[i],intValue = doStuff™ values.doStuff(i); values[i].doStuff(factor) values[i].doStuff(i); intValue + factor; Puzzle4 intValue* (2-(-factor); Va|u0 valuesfi].intValue = number; intValue * '5 'factor); Value() Puzzle4 [] values = new Puzzle4[6]; public 'n^a*ue actor' values [i] = new Value(i); Value [] values = new Value[6]; Drivate i = i+1; values [] = new Value(); ^Value [ ] values = new i = i-1; values [i] = new Value( ); values = new Valuefh
Быстрые сборки в Стим-Сити Когда Бьюкенен грубо схватил Джея за руку сзади, тот застыл. Джей знал: этот громила глуп настолько же, насколько страшен, и лучше не спугивать его. Бьюкенен потащил его в офис босса, но Джей не ждал ничего страшного. «Офис» Левелера оказался грязноватым летающим скиммером, но стоило Бьюкенену за¬ толкать Джея внутрь, как стало ясно — он хорошо прокачан: скорость, броня, все, что нужно местному боссу. — Джей, дружище, рад тебя видеть, — прошипел Левелер. — Ну да, взаимно... — натянуто ответил Джей, чувствуя, как за приветствием скрывается угроза. Мы же в расчете, Левелер? Я что-то упустил? Пятими¬ нутная загадка — Ха! Играешь чисто, Джей, да? Объемы продаж у тебя растут, но вот незадача — недав¬ но случилась... утечка. Джей невольно поморщился. В былые времена он был топовым джек-хакером, и если кто-то научился ломать уличные базы, все взгляды тут же обращались к нему. — Да ну, не в этот раз, — Джей пожал плечами. — Не стоит оно того. Я завязал с хакин¬ гом и не лезу в чужие дела. — Да, да, конечно, — рассмеялся Левелер. — Я верю, что ты тут ни при чем. Но пока этот новый хакер не прикроется, я теряю кучу денег! — Ну, удачи тебе, Левелер. Может, просто высадишь меня здесь? — предложил Джей. Левелер покачал головой. — Не все так просто, Джей. Бьюкенен тут говорит, что ты в курсе по Java NE 37.3.2. — Neural edition? Ну да, балуюсь иногда. А что? — Джей почувствовал легкую тошноту. — Выяснилось, что есть способ залезть в мою базу складов, — объяснил Левелер и накло¬ нился ближе. — Мне нужен толковый парень вроде тебя, Джей, чтобы заглянуть в мой класс Drop на Java NE — методы, переменные экземпляра, вся кодовая кухня — и понять, как они туда пробрались. — Эй! вмешался Бьюкенен. — Я не хочу, чтобы этот вшивый хакер копался в моем коде! — Полегче, здоровяк, — Джей заметил шанс. — Уверен, ты все сделал по высшему разряду с... модификаторами доступа... — Не учи меня, кодер! — взревел Бьюкенен. — Я оставил все методы публичными! А вот критические методы WareHousing я закрыл наглухо, так что снаружи к ним никто не подберется, понял? Никто! Джей усмехнулся. — Кажется, я нашел дыру, Левелер. Давай-ка высадим Бьюкенена на углу и прокатимся? Бьюкенен сжал кулаки и шагнул вперед, но Левелер уже приставил к его шее шокер. — Остынь, Бьюкенен, — усмехнулся Левелер. — Держи руки на виду и выходи. А мы с Джеем тут немного поработаем. Что заподозрил Джей? Сумеет ли он выбраться из скиммера Левелера целым? *• Ответы иа стр. 130.
Ль Ответы Заточите карандаш (стр. 123) int а = calcArea(7, 12); short с = 7; calcArea(c, 15); int d = calcArea(57); calcArea(2, 3); long t = 42; int f = calcArea(t, 17); int g = calcArea(); calcArea(); byte h = calcArea (4, 20); int j = calcArea(2, 3, 5), Класс XCopy компилируется и запускается в суще¬ ствующем виде! Программный вывод: 42.84. Пом¬ ните, что Java передает параметры по значению (то есть путем копирования), и переменная orig не изменится в методе до(). class Clock { String time; О! void setTime(String t) а. time = t; ) а> н >я >> _ String getTimeO ( я- Б return time; я я я 1 JWutwtuui i .ИЛ И « f ^ ЦИ11 tniAft Щ а Я о ъ* class ClockTestDrive { •..uonurwmnunf^- Л ,,tic ЛиСПНИА. public static void main(String[] args) { Clock c = new Clock () ; c.setTime("1245"); String tod = c.getTime (); System.out.println("время: " + tod); } Класс может содержать любое их количество. Метод может воз вращать только один такой элемент. Он может быть неявно расширен. Я предпочитаю, чтобы мои переменные экземпляра были закрытыми. Это означает «создать копию». Только сеттер должен иметь возможность их изменять. У метода может быть много таких элементов. Очевидно, я что-то возвращаю. Меня не следует использовать с переменными экземпляра. У меня может быть много аргументов. По определению я принимаю один аргумент. Это помогает внедрять инкапсуляцию. Я — одиночка. переменные экземпляра, геттер, сеттер метод return return, аргумент инкапсуляция передача по значению переменные экземпляра аргумент геттер public метод сеттер геттер, сеттер, public, private return
ОтЬеты на голоЬолол\ки бассейн фрагментов (стр. 127) 'Пятиминутная загадка (стр. 128) public class Puzzle4 { public static void main(String[] args) { Valued values = new Value[6); int number = 1; int i = 0; while (i < 6) { values[i] = new Valued; values[i].intValue = number; number = number * 10; i = i + i; Что заподозрил Джей? Джей знал, что Бьюкенен — далеко не самый быстрый процессор в системе. Когда тот говорил о своем коде, он ни разу не упомянул про свои переменные экземпляра. Джей заподозрил, что хоть с методами Бьюкенен и разобрался, но про модификаторы приватного доступа к перемен¬ ным забыл. Такой промах мог стоить Левелеру не одну штуку баксов. int result = 0; i = 6; while (i > 0) { l = l*i; result = result + values[i].doStuff(i); ) System.out.println("result " + result); } } class Value { int intValue; publicint doStuff(int factor) ( if (intValue > 100) { return intValue * factor ; ) else { return intValue * (5 - factor); | } 1 Результат File Edit Window Help BellyFlop %java Puzzle4 результат 543345 Противоречивые результаты (стр. 126) Варианты: Возможные результаты: index < 7 index < 1 7 7 20 1 20 5
5. Разработка программы Особо мощные методы Сделаем методы еще более МОЩНЫМИ. Вы уже поработали с переменными, поэкспериментировали с объектами и написали небольшой код. Но для полноценной работы потребуется больше инструментов. Например, операторы. Нам нужно больше операторов чтобы сделать нечто поинтереснее, чем заставить собачку лаять. И циклы. Обязательно нужны циклы, но не только такие простые, как while. Для серьезных дел нам потребуются циклы for. Они пригодятся для генерации случайных чисел. Их тоже лучше изучить. И почему бы нам не изучать все это вместе с созданием реальной программы, чтобы с самого начала пройти весь путь и понять, каково это — писать и тестировать с нуля? Может быть, это будет игра, например, «Морской бой». Задача это непростая, поэтому на ее выполнение придется потратить две главы. В этой главе мы создадим упрощенную версию игры, а затем в главе 6 сделаем более мощную премиальную версию.
Создадим игру в стиле «Морского боя»: «Битва за рынок» Здесь вы будете играть против компьютера. Основное от¬ личие от настоящего «Морского боя» состоит в том, что вы не размещаете свои корабли. Ваша задача — потопить корабли компьютера за минимальное количество ходов. И топить вы будете вовсе не корабли. Мы будем уничто¬ жать непродуманные действия, стартапы Кремниевой до¬ лины (вполне востребованный бизнес — и одновременно окупает затраты на эту книгу). Цель: потопить стартапы компьютера за минимальное количество попыток. В зависимости от того, насколько хорошо вы справляетесь с задачей, вам присваивается рейтинг. Подготовка: при загрузке программы компьютер разме¬ стит три стартапа на виртуальной доске (7*7). Когда это будет сделано, вас попросят сделать первый ход. Как играть: Мы еще не показывали, как создавать гра¬ фический интерфейс, поэтому эта версия будет работать в командной строке. Компьютер предложит вам ввести в командной строке ответ — предполагаемую ячейку в виде АЗ, С5 и т. д. В ответ на это в командной строке вы получите резуль¬ тат — либо «Попал», либо «Мимо», либо «Вы потопили “Поняшки”» (или любой другой стартап). Как только вы отправите на корм рыбам все три стартапа, в конце игры на экран выводится ваш рейтинг. ./Хека 7 нл 7 О 1 г Кодзи 2 3 4 5 Натнактся с нц.ш, какмаствы Jam Вам предстоит создать игру «Потопить стар¬ тап» с сеткой 7x7 и тремя стартапами. Каждый стар¬ тап занимает три клетки. часть игрового взаимодействия I File Edit Window Help Sell %java StartupBust Укажите клетку : АЗ МИМО Укажите клетку : В2 МИМО Укажите клетку : С4 МИМО Укажите клетку : 02 попал Укажите клетку : 03 попал Укажите клетку: : 04 Ого! Вы потопили Поняшки : ( ПОТОПИЛ Укажите клетку: G3 попал Укажите клетку: G4 попал Укажите клетку: : G5 Ого! Вы потопили Кодзи : ( | Все стартапы потоплены? Ваши акции теперь бесполезны! Заняло слишком 62 попытки. много времени.
Во-первых, высокоуровневое проектирование Для создания игры нам понадобятся классы и мето¬ ды, но какими они должны быть? Чтобы ответить на этот вопрос, нам нужно больше информации о том, что должна делать игра. Для начала необходимо определиться с игровым процессом. В общем, игра будет выглядеть так: О о Пользователь запускает игру. В игре создается три стартапа. ■ 4 Игра размещает стартапы на виртуальной игровой доске. Игра начинается. Повторение следующих действий до тех пор, пока не останется ни одного стартапа: О Пользователю предлагается сделать ход (А2, СО и т. д.). О О Проверка всех стартапов на по¬ падание, промах и потопление. Выбор подходящего действия: если попадание — удалить ячейку (Л 2, D4 и т. д). Если по¬ топление — удалить стартап. Игра заканчивается. Показать пользователю рей¬ тинг, основанный на количе¬ стве попыток. Далее необходимо выяснить, какие для этого понадобятся объекты. Думайте об этом, как Брэд, а не как Дора (которую вы знаете по вто¬ рой главе); в первую очередь сосредотачивайтесь на элементах, из которых состоит программа, а не на процедурах. чм№ элементы Ого! Настоящая блок-схема!
Плавное введение в игру «Битва за рынок. Простая версия» Похоже, нам понадобится как минимум два клас¬ са — Game и Startup. Но прежде чем мы создадим полноценную игру, мы начнем с ее упрощенной вер¬ сии — «Битва за рынок. Простая версия». В этой главе мы займемся созданием упрощенной версии игры, а в следующей соберем премиальную. В этой игре все просто. Вместо двухмерной сетки мы прячем стартап всего в одном ряду. А вместо трех стартапов мы используем один. О Игра запускается и создает ОДИН стартап и задает ему расположение на трех клетках в единственном ряду из семи клеток. Вместо «А2», «С4» и т. д. используются просто целые числа (например: 1,2,3 — это положение стартапа на этом рисунке): 0 1 2 3 4 5 6 SimpieStartup void main ml 0 locationcells int numOfHits Однако цель остается прежней, поэтому игра по- прежнему должна создавать экземпляр класса Startup, присвоить ему место в ряду, получать данные, введенные пользователем, и когда все ячейки стартапа будут по¬ ражены, игра закончится. Эта упрощенная версия дает нам большой задел для создания полноценной игры. Если мы сможем заставить эту версию ра¬ ботать, позже легко будет масштабировать ее до более сложной. В текущей версии класс игры не име¬ ет переменных экземпляра, а весь игровой код нахо¬ дится в методе main (). Другими словами, когда программа будет запущена и начнет выполняться, она создаст единственный экземпляр класса Startup, выберет для него местоположение (три последовательные клетки в единственном виртуальном ряду из 7 клеток), предложит пользователю угадать занятую клетку, прове¬ рит выбор пользователя и будет повторять, пока все три клетки не будут поражены. О о 2 String checkVourse|f(int guess) void setLocationCellsfintO loc) Помните, что виртуальный ряд остается... виртуальным. Другими словами, он не существует нигде в программе. Пока игра и пользователь знают, что стартап спрятан в трех последовательных ячейках из семи возможных (начиная с нуля), сам ряд не нужно представлять в коде. У вас может возникнуть соблазн создать массив из семи элементов, и присвоить трем из них значения, представ¬ ляющие стартап, но делать это необязательно. Все, что нам нужно, — это массив, который содержит только три клетки, занятые стартапом. Игра начинается. Предложите поль¬ зователю угадать, а затем проверьте, попал ли он в одну из трех ячеек, занятых стартапом. Если попал, увеличиваем значение переменной numOfHits. Игра завершается, когда все три ячейки будут поражены (значение переменной numOfHits выросло до 3), пользователю сообщается, сколько попыток потребовалось, чтобы утопить стартап. Полная версия игрового взаимодействия | File Edit Window Help Destroy 1 %java SimpleStartupGame введите число: 2 попал введите число: 3 попал введите число: 4 мимо введите число: 1 потопил Потрачено попыток: 4
Разработка класса Как у любого программиста, у вас наверняка есть своя методология/процесс/подход к написанию кода. И у нас тоже. Ниже приведен алгоритм нашей работы, кото¬ рый поможет вам увидеть (и узнать), о чем мы думаем, работая над созданием класса. Работая над реальной задачей, мы (или вы) не обязательно им пользуемся. В своей работе вы, конечно, будете следовать тому под¬ ходу, который определяют ваши личные предпочтения, требования проекта или работодателя. Сейчас мы можем делать практически все, что захочется. И когда мы созда¬ ем Java-класс в качестве «обучающего опыта», мы обычно поступаем следующим образом: Три сценария, которые напишем для каждого класса: Псевдокод Тестовый код Рабочий код Эта картинка будет появляться на следующих страницах, показывая, над какой частью вы работаете на данном этапе. Например, если вы видите такое изображение в верхней части страницы, это означает, что вы работаете над псевдокодом. ^ □ Выясняем, что должен делать класс □ Перечисляем переменные экземпляра и методы Псевдокод класс SimpleStartup □ Пишем псевдокод для методов (вы увидите это в ближайшее время) Алгоритм, позволяющий сосредоточиться на логи¬ ке, не вникая в синтаксис. □ Пишем тестовый код для методов □ Реализуем класс □ Тестируем методы □ Отлаживаем и повторно реализуем по не¬ обходимости Тестовый код Класс или методы, которые проверяют рабочий код и подтверждают, что он делает все правильно. Рабочий код Непосредственная реализация класса. На этом этапе мы пишем настоящий код на Java. □ Радуемся, что не придется тестировать нашу так называемую учебную программу на реальных пользователях Разомнем извилины. Как определить, какой класс или классы создавать в первую очередь? Если для всех программ, кроме самых маленьких, требуется больше одного класса (если вы следуете принципам объектно-ориентиро¬ ванного программирования и не застав¬ ляете один класс выполнять много разных задач), с чего вы начнете? Сделать: Класс SimpleStartup □ написать псевдокод □ написать тестовый код Q написать финапьный код на Java Класс SimptcStartupGame О написать псевдокод ® написать теетовыйчюд (не требуется) □ написать финапьный код на Java
Псевдокод стовый код Рабочий код SimpleStartup int 0 locationCells int numOfHits String checkYourself(int guess) void setLocationCells(intO loc) Пример на этой странице наглядно продемонстрирует, как работает наша версия псевдокода. Это нечто среднее между настоящим кодом на языке Java и описанием класса простым человеческим языком. В большинстве своем псевдокод состоит из трех разделов: объявление переменных экземпляра, объявления методов, логика методов. Самая важная часть псевдокода — это логика метода: именно она опреде¬ ляет, что должно произойти, когда мы напишем код метода. ОБЪЯВЛЯЕМ целочисленный массив для хранения адреса клеток. Присваиваем ему имя locationCells. ОБЪЯВЛЯЕМ целочисленную переменную для хранения количества попаданий. Называем ее numOfHi ts и ПРИСВАИВАЕМ ей значение 0. ОБЪЯВЛЯЕМ метод checkYourself (), который принимает выбор пользовате¬ ля в качестве целочисленной переменной (1, 3 и т. д.)> проверяет ее и возвращает результат: «попал», «мимо» или «потопил». ОБЪЯВЛЯЕМ сеттер setLocationCells (), который принимает целочислен¬ ный массив (хранящий адрес трех ячеек в виде целочисленных переменных —2,3, 4 и т. д.). МЕТОД: String checkYourself(String userGuess) ПОЛУЧАЕМ ход пользователя в виде целочисленного параметра. ПОВТОРЯЕМ это с каждой ячейкой целочисленного массива. // СРАВНИВАЕМ выбор пользователя с местоположением клетки. ЕСЛИ пользователь угадал, ИНКРЕМЕНТИРУЕМ количество попаданий. // ВЫЯСНЯЕМ, была ли это последняя ячейка. ЕСЛИ количество попаданий равно 3, ВОЗВРАЩАЕМ «потопил». ИНАЧЕ потопления не произошло, значит, ВОЗВРАЩАЕМ «попал». КОНЕЦ ВЕТВЛЕНИЯ ИНАЧЕ пользователь не попал, значит, ВОЗВРАЩАЕМ <мимо». КОНЕЦ ВЕТВЛЕНИЯ КОНЕЦ ЦИКЛА КОНЕЦ МЕТОДА МЕТОД: void setLocationCells (int [] cellLocations) ПОЛУЧАЕМ адреса ячеек в виде параметра с целочисленным массивом. ПРИСВАИВАЕМ полученный параметр положения клеток переменной экзем¬ пляра, которая хранит адреса клеток. КОНЕЦ МЕТОДА
Псевдокод Пишем реализацию метода Давайте напишем рабочий код и запустим его! Однако, прежде чем при¬ ступить к написанию мето¬ дов, давайте отступим назад и напишем код для тести¬ рования. Это не ошибка: мы пишем тестовый код прежде, чем будет написано то, что нужно тестировать! Концепция предваритель¬ ного написания тестового кода — это одна из практик Test-Driven Development (TDD), и она может облегчить (и ускорить) написание про¬ граммы. Мы не настаиваем на необходимости такого подхода, но нам нравится, когда тесты пишу тся еще до создания программы. А как круто звучит — TDD! О боже! На минуту \ мне показалось, что ты не собираешься сначала писать тестовый код. Ух! к Не пугай меня так. Разработка через тестирование (TDD — Test-Driven Development) В 1999 году Extreme Programming (ХР) стало новинкой в мире методологий разработки программного обеспечения. Одной из центральных идей ХР было написание тестового кода до написания реального. Эта идея легла в основу TDD (Test-Driven Development). (Да, мы понимаем, что сильно упростили эту концепцию, но наша цель — не углубляться в детали.) TDD — это обширная тема, и в этой книге мы лишь кратко ее коснемся. Мы надеемся, что описание процесса разработки игры «Битва за рынок» даст вам общее представление о TDD. Если вы хотите узнать больше о том, как работает TDD, мы рекомендуем прочитать книгу Кента Бека «Экстремальное программирование: разработка через тестирование». Приведем сокращенный список ключевых идей TDD: • Тесты пишутся первыми. • Разработка идет в итерационных циклах. • Код должен быть простым и понятным. • Рефакторинг — всегда, когда появляется возможность улучшить код. • Не выпускайте релиз, пока все тесты не пройдены. • Добавляйте только то, что указано в спецификации — не стоит добавлять функциональность «на будущее». • Работайте без жестких сроков, в нормальном темпе.
Псевдокод Тестовый мд Рабочий код Разработка тестового кода для класса SimpleStartup Нам нужно написать тестовый код, который позволит создать объект SimpleStartup и запустить его методы. Для класса SimpleStartup важен только метод checkYourself (). Для обеспечения его коррект¬ ной работы нужно реализовать метод setLoca tionCells (). Внимательно посмотрите на приведенный ниже псевдокод метода checkYourself () (метод setLoca tionCells (). Это обычный сет¬ тер, не требующий дополнительного внимания, но в «реальном» прило¬ жении нам может понадобиться более надежный сеттер, который нужно будет протестировать). Затем спросите себя: «Если бы метод checkYourself () был реали¬ зован, какой тестовый код я мог бы написать, чтобы понять, что метод работает правильно?» На основе этого псевдокода: METOflString checkYourself(int userGuess) ПОЛУЧАЕМ выбор пользователя в виде целочисленного параметра ПОВТОРЯЕМ с положением каждой клетки в целочисленном массиве II СРАВНИВАЕМ выбор пользователя с местоположением клетки ЕСЛИ выбор пользователя совпадает ИНКРЕМЕНТИРУЕМ количество совпадений II ВЫЯСНЯЕМ, была ли это последняя клетка: $ ЕСЛИ количество совпадений равно 3, ВОЗВРАЩАЕМ «потопил» ИНАЧЕ потопления не было, поэтому ВОЗВРАЩАЕМ «попал» КОНЕЦ ВЕТВЛЕНИЯ ИНАЧЕ выбор пользователя не совпал, поэтому ВОЗВРАЩАЕМ «ми КОНЕЦ ВЕТВЛЕНИЯ Вот что мы тестируем: 1. Создайте объект SimpleStartup. 2. Присвойте ему местоположение (массив из трех целых чисел, например { 2 , 3 , 4 }). 3. Создайте объект типа int для представления выбора пользовате¬ ля (2, 0 и т. д.). 4. Вызовите метод checkYourself (), передав ему качестве результата вымышленный выбор пользователя. 5. Выведите результат, чтобы узнать, верен ли он («пройден» или «про¬ вален»). КОНЕЦЦИКЛА КОНЕЦ МЕТОДА
Псевдокод Тестовый код Рабочий код (Не) дурацкие войросы ВоИрОС: Возможно, я что-то не понимаю, но как можно тестировать еще не существующие элементы? OlMBeW: На самом деле вы ничего не упустили. Тестирование начинается не с того, что мы тестируем готовый код, а с написания самого теста. В мо¬ мент написания теста у вас не будет еще того, что можно было бы запустить или скомпилировать. Что делать? На¬ пишите «заглушку» — код, который скомпилируется, но всегда приведет к неудаче теста (например, возвращая null). Это абсолютно нормально на этом этапе! Так вы получите компилируемый код, а тест останется готовым, чтобы проверять ваши реализации позже. BOOT рОС: Я все равно не вижу смыс¬ ла в таком подходе. Почему бы не на¬ писать код сначала, а тесты — потом? Oi/ивеотл: Написание тестов помога¬ ет лучше понять, как должен работать метод. Когда вы начнете писать тест, то увидите, что сами тесты дают ясность в том, что именно должно делать ваше приложение. И как только тесты будут готовы, вам будет проще написать код, который их пройдет. Кроме того, не забывайте: если вы не напишете тесты сразу, то можете не сделать этого вообще. А это ведь важ¬ ный момент — без тестов мы не можем быть уверены, что наш код работает правильно. И, в конце концов, тесты сэкономят вам время в будущем. Идеально — написать немного тестов, затем написать минимально необходи¬ мый код, чтобы эти тесты прошли. По¬ сле этого пишете еще немного тестов, а затем разрабатываете новый код для их прохождения. При этом важно на каждом этапе запускать все тесты, чтобы проверить, как новые изменения влияют на уже написанный код. Тестовый код для класса SiwipleStartMp public class SimpleStartupTestDrive ( public static void main(String[] args) { (cxuuvvh !ca SimpleStartup dot = new SimpleStartupO ; W.VXU&1 int[] locations = {2, 3, 4}; dot.setLocationCells(locations); (Wiaww*-* is®* int userGuess = 2; .ТТисшиисллнис ,wja String result = dot.checkYourself(userGuess); String testResult = "провален"; C. *!J<utst(f() r itmtuh u nJ ( if (result. equals ("попал")) { Щ nc.-uwH.uMW ^ testResult = "пройден"; factMamiMCWK. fcMaui/iim «ru*uW ji Trfiu nutm iyui«T. i ttWfc. System, out .println (testResult); unui ^%ic(V/ch » 11111 — ите карандаш •yiofo *- Решать - вам! На следующих страницах мы реализуем класс SimpleStartup, а затем вернемся к тестовому классу. Снова просмотрите те¬ стовый код, приведенный выше. Подумайте, что еще следует добавить. Что мы упустили в этом коде, а должны протестиро¬ вать? Напишите свои идеи (или строки кода) ниже.
Псевдокод Тестовый код Метод checkYoMrself^) Псевдокод и код Java — это не одно и то же; вы увидите несколько корректировок. Псевдокод дал нам четкое представление о том, что должен делать код. Осталось подобрать такой код на языке Java, который сможет реализовать все это на прак тике. Просмотрите код и подумайте, какие его части стоит (или необходимо) улучшить. Числа Ш обозначают незнакомые вам элементы (синтаксис и особенности язы¬ ка). Пояснения к ним даны на следующей странице. ПОЛУЧАЕМ выбор пользова¬ теля. ПОВТОРЯЕМ это с каждой клеткой цело¬ численного мас¬ сива. ЕСЛИ выбор пользователя совпал, ИНКРЕМЕНТИ¬ РУЕМ количе¬ ство попаданий. II ВЫЯСНЯЕМ, была ли это по¬ следняя ячейка. ЕСЛИ количе¬ ство попаданий равно 3, ВОЗ¬ ВРАЩАЕМ «по¬ топил» в каче¬ стве результата. ИНАЧЕ пото¬ пления не про¬ изошло, значит, ВОЗВРАЩАЕМ «попал». ИНАЧЕ ВОЗВРАЩАЕМ «мимо». public String checkYourself(int guess) { (a String result = "мимо"; © for (int cell : locationCells) { if (guess = cell) { ' c?qatM перешннцк аля хранен ил рем еt tmima, кеторпй мп npfcu flc^ftpaiH/wu Щшс&ш&н.и ришн ul he илтлшнихс •киплю» (in. t. 'uti п^хехргюякиаелл «прайм»). Ясвтсрмш a m кажасй клетки -('массиве вссЫШШ пптеисшнш клетки нахитпал (кнкт) result = "попал © numOfHits++; break; веххъди „ ^ щ н“Шй|М.«%м niw&tlI } // конец if другие клетки ' ) // конец for (Unпилитп\ся (гккт) „ иштс п( тл1Р*аталя с этим , ^ штем (к лапкой) Члшссий £ аН-п палупии попадание! ■не IX! нет. яте if (numOfHits — locationCells.length) { Ли вшили наци/ош. накатит ntu&fmu вариант, ><и< ни п ни» result = "потопил"; ) // конец if System.out.println(result); return result; {4, } // конец метода С) mluштиля) н ияиенииреж тат па «потопил» BatfvqvLu реммтт пяшлел в, («мима». (Ли он не сш щт, л Пателя тенен на 1ЯШ/ШЛ «напал» или «потопле н» итат
Псевдокод Объясняем новое На этой странице мы дадим объяснение того, что вы еще не изучали. Не волнуйтесь! Подробные объяснения обязательно будут, но немного позже. Информации на этой странице достаточно, чтобы начать работу. г; тсйпашш вм Л» ('ли<г<н(1 (kutLify. так та влш:< Н.1ЦДНГ 9. т iu.u Hu:uiiH(M tu/iuuHiun с V 1) Цикл for Arnmt (j g Ф ПГт*'Ш/Эп,„ >T«y“mcXr,K ^ Т №t Фс/исииш , f Kl°" ь&ткпй(%у ,, уна*нн.ч for (intcell: locationCeUs) { j ?е£б*. / шш«и/uy ,yui прижатии цмкм урЫшнсы'(вааннам слфск цистслтшы \ цепь Д1 посменная слЩ tWm сЯсмжать финн ЦФлсЩип Щ f тт'Ф гика м питп ифааташх & эжменнц, mccfaju ц,fo*/ж тЩтттса 4а"ХЫ'Ч Hpty impel пир ttaak...). ' ' ,г1сЛГТНа сЛ T m u P uWi ЯЗйй-*-- w ctOC. ilMl ©Постинкрементный оператор numO£Hits++ ^wf +* с?на»ае»ц м<щ шменне еиинймд к лкт- киж ну»и/еннсм (араиша елташ, ijtft urmmt ни fА »wm6Wfrfo++ — это же да тж5&нньм с-шадсА «пс numOpfiti = monnWf'H + /, ItUUtKO ЛГМДНтНГГ. Оператор break break; Л*ф*****$и,мк,щ../Яг„о<?е„.НаЖрятша Нша- ких и>тр<ш,т. никаких лшттих ^тхс(Щи-Ы
Псевдокод (Не) дурацкие вопросы Вопрос-. В начале книги приводился пример цикла for, который сильно отличался от того, что в приведенном примере. Для циклов for существуют два разных стиля? Оплвем: Да! С самой первой версии Java поддерживался один вариант цикла for (о котором рассказывается далее в этой главе), который выглядит следующим образом: for (int i = 0; i < 10; i++) ( // код, выполняемый 10 раз ) Этот формат можно использовать для любого цикла, который вам нужен. С версии Java 5 появился вариант улучшенного цикла for (таково официальное описание), предназначенный для перебора элементов в массиве (или другой коллекции). Это можно сделать и с обычным циклом for, но усовершенствованный цикл for делает это проще и удобнее. Вопрос: Если к числу типа int можно прибавить единицу с помощью оператора ++, можно ли также вычесть единицу? Оплвем: Да, безусловно. Это делается с помощью оператора - (два минуса), например, вот так: countdown = i--; Финальный код для SlwipleStartup и SimpleStartupTestDrive public class SimpleStartupTestDrive { public static void main(String[] args) ( SimpleStartup dot = new SimpleStartup(); int [ ] locations = (2, 3, 4}; dot.setLocationCelIs(locations) ; int userGuess = 2; String result = dot.checkYourself(userGuess) ; String testResult = "провален"; if (result.equals("попал")) ( testResult = "пройден"; } System.out.printIn(testResult); f 1 class SimpleStartup { private int[] lccaticnCells; private int numOfHits = 0; // Устанавливаем местоположение клеток public void setLocationCells(int[] Iocs) ( locationCelIs = Iocs; } // Проверка, угадал ли пользователь public String checkYourself(int guess) ( String result = "мимо"; for (int cell : locationCells) ( if (guess == cell) ( result = "попал"; numOfHits++; break; ) // конец if } // конец for if (numOfHits == locationCells.length) ( result = "потопил" ,- ) // конец if System.out.printIn(result) ; return result; ) // конец метода } // закрытие класса Здесь скрывается небольшая ошибка. Програм¬ ма компилируется и работает, но... Пока что не беспокойтесь об этом, разберемся чуть позже. % java SimpleStartupTestDrive Что мы должны увидеть, когда мы запустим этот код? Тестовый код создает объ¬ ект SimpleStartup и переда¬ ет ему местоположение 2, 3, 4. Затем он отправляет фик¬ тивный выбор пользовате¬ ля 2 методу checkYouselff). Если код работает правиль¬ но, мы должны увидеть следующий результат: попал
Псевдокод ^Тестовый код Рабочий код — ffcfc-Заточите карандаш Мы создали тестовый класс и класс SimpleStartup. Игру нам только предстоит создать. Учитывая код на предыдущей странице и специ¬ фикацию реальной игры, напишите идеи псевдокода для игрового класса. Мы дали несколько строк, от которых вы можете отталкивать¬ ся. Фактический код игры находится на следующей странице, так что не переворачивайте страницу, пока не выполните это упражнение! У вас должно получиться 12-18 строк кода (включая те, что мы написа¬ ли, но не учитывая строки, в которых есть только фигурные скобки). МЕТОД public static void main (String [] args) ОБЪЯВЛЯЕМ целочисленную переменную numOfGuesses для хранения количества ходов пользователя ВЫЧИСЛЯЕМ случайное число от 0 до 4, которое будет позицией начальной ячейки ПОКА стартап не потоплен: ПОЛУЧАЕМ пользовательский ввод из командной строки Класс SimpleStartupGame должен сделать следующее: 1. Создает единственный объект SimpleStartup. 2. Определяет для него место¬ положение (три последова¬ тельные клетки в ряду из семи виртуальных клеток). 3. Предлагает пользователю выбрать клетку. 4. Проверяет предположение. 5. Повторяет, пока стартап не будет потоплен. 6. Сообщает пользователю, сколько ходов он сделал. Полная версия игрового взаимодействия I File Edit Window Help Runaway : 1 % java SimpleStartupGame введите число: 2 попал введите число: 3 попал введите число: 4 мимо введите число: 1 потопил Потрачено попыток: 4 ■*- Решать — вам!
Псевдокод для класса SimpleStartupGame Все происходит в mam() Есть вещи, которые вам пока придется просто принять на веру. Например, в псевдокоде может появиться стро¬ ка вроде: «ПОЛУЧАЕМ пользовательский ввод из командной строки». И да, мы знаем: за этой строкой скрыва¬ ется немного больше, чем один простой шаг. Реализовать это вручную с нуля сейчас будет слегка перебором. Но, к счастью, мы работаем в мире объектно-ориентированного программирования. А это значит, что вы мо¬ жете попросить другой объект выполнить работу за вас. И при этом совершенно не важно, как он это делает. Главное — он справится. Когда вы пишете псевдокод, не бойтесь упростить: пусть там будут строки вроде «получаем ввод» или «вы¬ водим результат». Представьте, что вы можете сделать все, что задумали. Это позволяет сосредоточиться на самой логике, а не на технических деталях. public static void main (string Q args) ОБЪЯВЛЯЕМцелочисленнуюпеременнуюдля хранения количества ходов пользователя, присваиваем ей имя numOfGuesses и значение О СОЗДАЕМ новый экземпляр класса SimpleStartup ВЫЧИСЛЯЕМ случайное число от 0 до 4, которое будет позицией начальной клетки СОЗДАЕМ целочисленный массив с тремя элементами, используя случайно сгенерированное число, это число, увеличенное на 1, и число, увеличенное на 2 (пример: 3,4,5) ВЫЗЫВАЕМ метод setLocationCells () из экземпляра SimpleSta г tup ОБЪЯВЛЯЕМ логическую переменную isAlive, представляющую состояние игры. ПРИСВАИВАЕМ ей значение true ПОКА стартап не потоплен (isAlive == true): ПОЛУЧАЕМ пользовательский ввод из командной строки II ПРОВЕРЯЕМ предположение пользователя ВЫЗЫВАЕМ метод checkYouzself () из экземпляра SimpleStartup ИНКРЕМЕНТИРУЕМ переменную numOfGuesses II ПРОВЕРЯЕМ потопление стартапа ЕСЛИ результат — «потопил» ПРИСВАИВАЕМ переменной isAlive значение false (это означает, что мы больше не будем заходить в цикл) ВЫВОДИМ количество ходов пользователя КОНЕЦ ВЕТВЛЕНИЯ КОНЕЦ ЦИКЛА WHILE КОНЕЦ МЕТОДА Метакогнитивный совет Не стоит слишком долго нагружать одно полушарие мозга. Работать только ле вой стороной мозга более 30 минут — все равно что полчаса работать только левой рукой. Устраивайте каждому полушарию мозга передышки. Когда вы включаете в работу другое полушарие, поработавшая сторона получает возможность отдохнуть и восстановиться. Деятельность левого полуша¬ рия включает в себя такие вещи, как пошаговая последовательность действий, решение логических задач и анализ. Правое полушарие включается при работе с метафорами, творческом подходе к реше¬ нию проблем, сопоставлении образов и визуализации.
Без лишних слов Создание программы на языке Java должно на¬ чинаться с высокоуровневого проектирования. Как правило, при создании нового класса вы пише¬ те три вещи: ■ псевдокод • тестовый код ■ рабочий (Java-)Kod Псевдокод должен описывать, что делать, а не как делать, Реализация будет позже. Используйте псевдокод, чтобы тестовый код было проще разработать. У класса может быть только один суперкласс. Напишите тестовый код до того, как реализуете методы. Выбирайте циклы for вместо циклов while, когда точно знаете, сколько раз нужно повторить код цикла. Усовершенствованный цикл for — это простой спо¬ соб перебрать массив или коллекцию элементов. Используйте оператор инкремента, чтобы доба¬ вить единицу к переменной (х++;). Используйте оператор декремента для вычитания единицы из переменной (х-;). Используйте оператор break для досрочного вы¬ хода из цикла (т, е. даже если проверка условия все еще возвращает true). Сколь¬ ко клиентов было в про¬ шлом месяце? В том числе вернувшихся? С приветом из Призрачного города
Псевдокод Метод mainf) игры Как и в случае с классом SimpleStartup, просмотрите код и подумайте, какие фрагменты кода можно или необходимо улучшить. Места в коде, обозначенные числом в кружочке @ , требуют повышенного внимания. Они объясняются на следующей странице. Если вам интересно, почему мы пропустили этап создания тестового кода для этого класса, отвечаем: для этой игры тестовый класс нам не нужен. В ней есть только один метод, так зачем нужен тестовый код? Создавать отдельный класс, кото¬ рый бы вызывал main () из этого класса? Нет никакой необходимости это делать. ОБЪЯВЛЯЕМ переменную для хранения количества ходов пользователя и присваиваем ей значение О СОЗДАЕМ объект SimpleStartup ВЫЧИСЛЯЕМ случайное число от 0 до 4 СОЗДАЕМ целочисленный массив, содержащий 3 клетки, и ВЫЗЫВАЕМ метод setLocationCells из объекта Startup ОБЪЯВЛЯЕМ логическую переменную isAlive ПОКА стартап не потоплен ПОЛУЧАЕМ пользовательский ввод Ы ПШ н.хиб'ц public static void main(String[] args) { int numOfGuesses = 0; ^ „ „ . . . „ ... .. м Kin Фшшшыжа; GameHelper helper = new GameHelper () { ... , c - ' 1 .11/ a i я cwc1 КШеа. который м-раватывшп рватрискш (Xka inpujcmu&mt, mu 3HU встроенная ьсршмпс<mt Java) SimpleStartup theStartup = new SimpleStartup () ; (( „ осгскт Startuf\ © T>' int randomNum = (int) (Math.random() *5); Jtnpupipu ctumimoo Г- met с у vt nip ik клоткн ,^~l{ на ш Фортршм сГ массивк..ип\<ж ' ' -н на (п оенш :тпск int[] locations = (randomNum, randomNum + 1, randomNum +2); theStartup.setLocationCells(locations); boolean isAlive = true; ^ a* Cuacen& АЛШЫ1 жтжкцю тролиншш, которая пе.межот в н.н/си итй while (isAlive) { (D int guess = helper.getUserlnput( введите число: проверять, ^аве^ишнали тра CrtVAbUf //ПРОВЕРЯЕМ его ВЫЗЫВАЕМ метод checkYourself() из объекта Startup ИНКРЕМЕНИРУЕМ переменную numOfGuesses ЕСЛИ результат «потопил» String result = theStartup.checkYourself(guess); if (result.equals("потопил")) { pMtT* ~ Т1Панави{ваоигнатио jafso isAlive = falser ыр&шннm r\4ftvt (/medt mnamMmt u,mkh u. m”upiu K{imncmgc ccp. taimu.\‘ xop£ System.out.println("Потрачено попыток: + numOfGuesses); ПРИСВАИВАЕМ } // закрываем if переменной isAlive значение false ) // закрываем while ВЫВОДИМ количество ходов пользователя
Псевдокод Тестовый код Методы random() и getUserlwpMtf) Эту страницу мы посвятили методам randomf) иgetUserInput(), которые требуют более подробного объяснения. Пока это будет крат¬ кая информация, чтобы дать общее представление; более подробно класс GameHelper рассмотрим в конце этой главы. © Сгенерируйте случайное число 9пи прхиваусние типа — тс лтпаКлмап послишноще таге 'пт принять тип!цкеткнеш d скипах. лИетсапаСп. tandcmO ^вцаш/ктjuaimm. типа теэтеми асЪцжнс привести к int, i£ iu паи mf u пц тем и/лое гисло <ж О f ц. I I / итате iifni&aiHUM еЦ\санам часть отсекается. I уИетс (paw Гн к ал кJut HathaandcmO fo . °1 этаии (?м|ш,тнме unt) ('Hath. т теле от С. О ас 1.0 нс включая гаиниим). До- mndctnO h Ь) tkpнет ujhv uj цмм,\- тисс t cm 0 qc H (так как гначениелИаНх.чалиАпхО 5 (fiifxmlxuemcM c m 0.0 qc H.999..., r - nut к iixt просты c m u r cpac Itm c\ IfV-lMf к часть). I int randomNum = (int) (Math.random() * 5) T t r . Hex ca.udxMt.u и/tctuc ccmh.hk, nejxtum hijw cjXM хранении случайном чисха. Класс, fonxfxc синаи Г Java. Гтатиусскнй Mtmaj iv к. vacca.Matk. Math.randomQ существует уже много лет, и такой код до сих пор можно встретить в реальных проектах. Однако сегодня чаще ис¬ пользуют метод nextlntQ из класса java.utii.Random. Он удобнее — не нужно вручную приводить результат к int. Класс Random находится в другом пакете. А так как мы пока еще не разбирали, как импортировать классы из сторонних пакетов {об этом пойдет речь в следующей главе), мы используем Math.random,). 9кл.маляр класса, кете nefXmaJm, . /j(f- Цс fox рт им седы.сн ранее. ( н .*',// J f m чпссКг- нл?мбтстш (janxeJieCfxe t. иет^ .. ’ пи/1иинан ас тега как ! Получите пользо¬ вательский ввод с помощью класса GameHelper нагы&ипхМ. (pm\eJ(e(j тис пока не (mtje.ux (скоро foe (dpjom). штш н,„и 7 Т Шн- m^iuu$oq ncMfcfa- int guess = helper.getUserlnput ("введите число т • //а I сан (амии центе,иннцю иную сим xfbwenuM по. и pi amt н екой ввосча. копщши 1Н проси ocpamue f.i.!) um. q.). Г . Hemeoi класса ХштоНеСреч ххреалагает пелиеьателн топи ааннмс. Гн счех тптот его noett того, как нс, илСате. и натишет клавиши РпТсч, н fojfoxaW
Псевдокод Тестовый код Последний класс: GaweHelper Мы создали класс Startup. Мы создали класс игры. Остался только вспомогательный класс тот, в котором используется метод getUserlnput (). Создание кода для получения ввода из командной строки объяснять сейчас нецелесообразно. Эта тема затрагивает множество других тем, которые лучше оставить на потом. (На главу 16.) Просто скопируйте* приведенный ниже код и скомпилируйте его в класс GameHe lper. По местите все три класса (SimpleStartup, SimpleStartupGame, GameHelper)в один рабочий каталог. import java.util.Scanner; public class GameHelper { -> Да, мы хотим еще немного ва¬ шего вкусного гото¬ вого кода, спасибо! public int getUserlnput(String prompt) { System.out.print(prompt + : )> Scanner scanner = new Scanner(System.in); return scanner.nextlnt(); } ) ’Мы знаем, как вам нравится набирать текст, но если у вас есть более важные дела, воспользуйтесь готовым кодом на сайте https://oreil.ly/hfJava_3e_examples.
Сыграем? Вот что происходит, когда мы запустим игру и введем числа 1, 2, 3, 4, 5 и 6. Полу¬ чилось неплохо. Полный игровой диалог (Число попыток может быть иным) Что это? Ошибка? О, нет! Вот что происходит, если мы введем 1,1 и 1. Другой пример игрового взаимодействия (Не может быть) | File Edit Window Help Smile 1 %java SimpleStartupGame введите число: 1 мимо введите число: 2 мимо введите число: 3 мимо введите число: 4 попал введите число: 5 попал введите число: 6 потопил Потрачено попыток: б | File Edit Window Help Faint | %java SimpleStartupGame введите число: 1 попал введите число: 1 попал введите число: 1 потопил Потрачено попыток: 3 Заточите карандаш - V Это интрига! Найдем ли мы ошибку? Исправим ли мы ее? Подождите до следующей главы, в ней вы полу¬ чите ответы на эти и другие вопросы... А пока подумайте, что пошло не так и как это исправить. -► Решать —вам!
Подробнее о циклах For В этой главе мы рассмотрели весь игровой код (вернемся к нему в следую¬ щей главе, чтобы написать полную версию игры). Мы не хотели прерывать работу некоторыми деталями и справочной информацией, поэтому по¬ местили все это сюда. Начнем с подробного описания циклов for. Если вы уже встречали подобный синтаксис в другом языке программирования, пропустите эти страницы. Обычные (не улучшенные) циклы for Cmfuimf шптжк^.шнпш for(int i = О; i < 100; i++) П 'Мптм£Ш*шмя УСЖ£Ш Mmc/ioiWWHHa На русском языке это будет звучать так: «Повторять 100 раз». Как это выглядит с точки зрения компилятора: • создать переменную i и присвоить ей значение 0; • повторять, пока значение переменной i не достигнет 100; • в конце каждой итерации прибавлять к значению переменной i единицу. Часть первая: инициализация В этой части объявите и инициализируйте переменную, которая за¬ действована в теле цикла. Чаще всего она выступает в качестве счет¬ чика. В этой части можно инициализировать несколько переменных, но чаще всего используется одна переменная. Часть вторая: логическая проверка В этой части проверяется условие. Независимо от выражения в нем оно должно возвращать логическое значение (true или false). Вы мо¬ жете провести тест, например (х >= 4), или вызвать метод, который возвращает логическое значение. Третья часть: итерационное выражение В этой части разместите одно или несколько выражений, которые должны выполняться при каждом проходе цикла. Помните, что все они выполняются в конце каждого цикла. Повторять 100 раз
Разница tA&bjtyJor и while Цикл while содержит только логический тест; в нем нет встроенной инициализации или итера¬ ционного выражения. Цикл while хорош, когда неизвестно, сколько раз нужно выполнить цикл, и продолжать выполнение нужно до тех пор, пока какое-то условие истинно. Но если вы знаете, сколько раз нужно выполнить цикл (например, из¬ вестна длина массива), то цикл for будет удобнее. Ниже привели уже знакомый вам цикл, перепи¬ санный с использованием оператора while; int i = 0; < f while (i < 8) { System.out.println(i); 1++; < • •m финиш ) линпу«а,т System.out.println("готово"); ННК/Ч 1 г/(н\п<н В данном контексте они оба означают одно и то же: «при¬ бавить 1 к текущему значению х» или «инкрементиро¬ вать х на 1». И: х--; это то же самое, что и: х = х - 1 ; Конечно, это еще не вся история. Расположение оператора (до или после переменной) может повлиять на результат. Размещение оператора перед переменной (например, ++х) означает: «Сначала инкрементируем х на 1, а затем исполь¬ зуйте это новое значение х». Это важно только в том случае, когда оператор ++х — часть какого-то более крупного выражения, а не просто применя¬ ется отдельно. int х = 0; int z = ++х; выдает: х равно 1, z равно 1 Но если поставить ++ после х, получится другой результат: int х = 0; int z = х++; После выполнения этого кода х равно 1, a z равно 0! Пере¬ менная z принимает значение х до того, как значение х увеличивается.
Улучшенный цикл Гог ачиная с версии 5.0 (Tiger), язык Java приобрел еще один вариант цикла for, ко¬ торый называют улучшенным. Он создавался для упрощенного перебора элементов в массиве или другом виде коллекций (о других коллекциях вы узнаете в следующей главе), лучшенный цикл for вскоре стал частью языка, так как, по сути, делал то же самое, что и обычный for, только эффективнее. Мы вернемся к нему в следую¬ щей главе, когда речь пойдет о коллекциях, отличных от массивов. За |ШК! »' 'Т I for (string name ужшшпы ,шит„и тшсмлГ с -ЖГЗ- ‘ПГ оовиити. ии с типам сЛШа игыСитнш ты лапт и. ' оокроо ^ н- f n Have yum, (muc\ ^ <Л(.и.икао. : nameArray) { } 'Ч Ля Ч"\ ,0 u an котиком, нижно njv fir mu тттгна cnfcm- На русском языке это звучит так: «Для каждого элемента в массиве nameArray присвоить этот элемент переменной name и выполнить тело цикла». А вот как это «видит» компилятор: • Создать переменную типа String с именем name и установить ее в null ■ Присвоить переменной name значение первого элемента массива nameArray • Выполнить тело цикла (блок кода внутри фигурных скобок) • Присвоить переменной name значение следующего элемента массива nameArray Л/пашжшис: /} мвигимости от тег о, с какими мялками uj\o/jyLu uujm()(iHUM%\ стал- К'и валuckjxauьш. этот и,икл нажат <шт гштан вам кит «jot шск» или «jot in», по¬ томи гте пит его и штают: «а оч К \ ЖУХ К а илш< та « коллекции...». • Повторять шаги, пока в массиве не закончатся элементы Первая часть: переменная итерации На этом месте вы объявляете переменную, которая будет использоваться внутри тела цикла. С каждым новым проходом цикла она будет принимать значение следующего элемента в массиве. Важно: тип переменной должен совпадать (или быть совместимым) с типом элементов массива! Нельзя, на¬ пример, перебирать массив строк с переменной типа int. Вторая часть: коллекция (или массив) Это ссылка на массив или другую коллекцию. Пока что мы работаем только с массивами, но скоро познакомимся и с другими типами коллекций.
Приведение примитивов Прежде чем завершить главу, следует закрыть один незаконченный вопрос. Когда мы использовали метод Math. random (), необходимо было привести результат к цело¬ му числу. Приведение одного числового типа к другому может повлиять на само значение. Важно понять правила, чтобы результат вас не шокировал. t short можно привести к но при ЭТОМ Цq кое-что теряется Pi В главе 3 мы говорили о размерах различных примитивов и о том, что нельзя засунуть большую вещь в маленькую: long у = 42; int х = у; // не компилируется Значение long больше, чем int, и компилятор не может быть уверен, где этот long был раньше. Возможно, он веселился с другими переменными типа long и принимал очень большие значения. Чтобы заставить компилятор поместить значение большей примитивной переменной в меньшую, вы можете использовать оператор приведения. Выглядит это следующим образом: long у = / / пока все хорошо int х = (int) у; // х = 42 отлично! Оператор приведения указывает компилятору взять значение переменной у, уменьшить его до раз¬ мера int и присвоить результат переменной х. Если значение у было больше, чем максимальное значение х, то, что останется, получится странное (но допустимое*) число: long у = 40002; // 40002 превышает 16-битное ограничение short short х = (short) у; // х теперь равно -25534! Тем не менее суть в том, что компилятор позволяет это сделать. Допустим, у вас есть число с плава¬ ющей точкой, и вы хотите получить его целую часть (int): float f = 3.1 4 f ; int x = (int) f; // x будет равно 3 И даже не думайте приводить что-либо к логическим значениям или наоборот. *Тут замешаны знаковые биты, двоичная система, «дополнение до двух» и прочие гиковские штуки.
.тражнение В роли JVM На этой странице приведен полный исходный код програ/л/лы. Наша задача — на Ьре/ля стать JVM и определить, что програ/л/ла ЬыЬедет на экран, если Будет запущена. class Output { public static void main(String[] args) { Output output = new Output(); output.go(); ) File Edit Window Help OM \ java Output 12 14 void go() { int value = 7 ; for (int i = 1; i < 8; i++) { value++; if (i > 4) { System.out.print(++value + " "); } if (value >14) { System.out.println(" i = " + i); break; ) } ) } ИЛИ File Edit Window Help Incense $ java Output 12 14 x = 6 ИЛИ Ответы на стр. 158
Маг нитики с кодом На холодильнике в беспорядке разбросан код рабочей Java- программы. Фрагменты перепутались: кажется, будто кто-то играл ими, как магнитиками. Ваша задача — переставить кусочки кода в правильном порядке, чтобы получилась рабочая программа. Она должна скомпилиро- ваться и вывести нужный результат. Несколько фигурных скобок при этом, увы, упали на пол и затерялись... Так что добавляйте их столько, сколько потребуется! File Edit Window Help Raid % java MultiFor 0 4 0 3 1 4 1 3 3 4 3 3 i++; if (i == l) ( System .out.println (i + " " + J); | Ответы на стр. 158
■ 1 3 4 2 7 12 6 19 5 10 18 8 11 14 17 9 13 16 20 15 Кросс&орс) Как кроссворд помогает вам изучать Java? Ну, все слова связаны с Java (или хотя бы с IT). Кроме того, подсказки содержат метафоры, калам¬ буры и тому подобное. Эта умственная эквилибристика прокладывает альтернатив¬ ные пути к пониманию Java! По горизонтали: 1. Программа, обеспечивающая взаимодействие с устройством 2. Последовательность символов 5. Маршрут к файлу или каталогу 6. Логическое выражение, определяющее поведение кода 9. Номер выпуска программы 10. Отображаемая часть интерфейса 11. Геометрическая или логическая середина 15. Оформление внешнего вида приложения 1 б. Область памяти для временного хранения вызовов 17. Последовательность операций или данных 20. Категория значения переменной По вертикали: 1. Право использовать или изменять ресурс 3. Независимая единица программы 4. Хранилище данных на диске 7. Информация, возвращаемая наружу 8. Окно или панель в графическом интерфейсе 10. Отметка на шкале времени 12. Список команд, доступных пользователю 13. Итог числового подсчета 14. Проверка правильности работы кода 17. Группа классов, объединенных по назначению 18. Реакция на запрос 19. Символ, обозначающий операцию или структуру ^ Ответы на стр. 159
Противоречивые результаты Ниже приведена небольшая программа на языке Java. Один ее блок потерян. Ваша задача — сопоставить варианты блоков кода (ниже) с результатами вывода, которые вы увидите, если эти блоки будут добавлены. Не все строки с результатами будут использованы, а некоторые пригодят¬ ся несколько раз. Соедините линиями варианты кода с со¬ ответствующим результатом в командной строке. ► Ответы на стр. 159 public static void main(String[] args) { int x = 0; int у = 30; for (int outer = 0; outer < 3; outer-H-) { for (int inner = 4; inner > 1; inner—) { у = у - 2; if (x == 6) { break; Ку пшйпи KUf(Hya } x = x + 3; } у = у - 2; } System.out.println(x + " " + y); Варианты кода; Возможный результат: X = X + 3; 45 6 (uuxnvaCtmi 3 СГ. -5 х = х + 6; 36 6 ант с сатил iv ьмис.жнил х = х + 2; 54 6 p^iptmanvcb
Ответы на упражнения £> рotujvii (стр. 154) class Output { public static void main(String[] args) { Output output = new Output (); output.go () ; } void go () ( int value = 7; for (int i = 1; i < 8; i++) ( value++; if (i > 4) ( System.out.print(++value + " ' i if (value >14) ( System.out.println(" i = " + i); break; ) Вы не забыли про оператор break? Как он влияет на ре¬ зультат работы программы? ITne""Edir^mdow""Hejp""MotorcycleMamtenanc^ Магнитики с кодом (стр. 155) class MultiFor { public static void main(String[] args) ( for (int i = 0; i < 4; i++) ( for (int j = 4; j > 2; j—) { System.out.println(i + ' + j); ) if (i = 1) { i++; } ) } } j" File Edit Window Help Monopole % java MultiFor 0 4 0 3 1 4 1 3 3 4 3 3 Что бы произошло, если бы этот блок кода располагался до цикла for с j? % java Output 13 15 х = б
Головоломка ‘д р а Й в е р О с Зм 4Ф 2С т р 0 К а 7в У д й ы 12м п "у с л о в и е Л о н 193 5П У т ь 10в и д ю н р 180 а 8ф “ц е н Ит р 17п о т о к р м е а в 9В е р 1}с И я “с т е к е й ч т е 20т и п м ё т 15т е м а КроссВюрс) (стр. 156) Противоречивые результаты (dp. 157) Варианты кода: х = х + 3 ; х = х + 6; х = х + 2; х++; х—; х = х + 0 ; Возможный результат: 45 6 36 6 54 б 60 10 18 б 6 14 12 14
6. все что вы хотели знать о Java API Использование библиотеки Java Вместе С Java ВЫ получаете СОТНИ ГОТОВЫХ классов. Не нужно изобретать велосипед (разве что вы это обожаете). Главное — знать, где искать в библиотеке Java, официально именуемой Java API. Уверены, у вас и так хватает задач; лучше сосредоточьтесь на том, что делает ваше приложение уникальным. Наверняка среди ваших коллег есть такие разработчики, которые приходят к 10:00 и уходят ровно в 17:00 — и при этом все успевают. Хотите знать их секрет? Они используют Java API. И вы тоже начнете этим заниматься на следующих страницах. Java API — это как огромный набор строительных блоков. Большая часть приложения может быть построена с их помощью. Код, который вы увидите в этой книге, основан на стандартной библиотеке, так что вам не придется писать его с нуля (но набирать все-таки придется). В стандартной библиотеке Java есть масса полезного кода, и вам даже не нужно знать, как он работает внутри. Главное — научиться пользоваться этим богатством.
В предыдущей главе мы оставили небольшую интригу, а именно — ошибку. Как это должно выглядеть Посмотрите, что произойдет, если мы запустим программу и введем числа 1, 2, 3, 4, 5 и 6. Смотрится хорошо. Полная версия игрового взаимодействия (Количество попыток может изменяться) I File Edit Window Help Smile J %java SimpleStartupGame введите число: 1 мимо введите число: 2 мимо введите число: 3 мимо введите число: 4 попал введите число: 5 попал введите число: 6 потопил Потрачено попыток: 6 Где закралась ошибка А вот что произойдет, если ввести 2, 2 и 2. Другой игровой диалог (Не может быть!) | File Edit Window Help Faint 1 %java SimpleStartupGame введите число: попал 2 введите число: попал 2 введите число: потопил 2 Потрачено попыток: 3 В текущей версии игры, если вы попа¬ ли, можете просто повторить два раза удачный ход и пото¬ пить стартап!
Что же случилось? Здесь кроется ошибка. Мы засчитываем по¬ падание каждый раз, когда пользо¬ ватель угадывает расположение клетки, даже если она уже была по¬ ражена! Нужно найти способ узнавать, попал ли пользо¬ ватель в кон¬ кретную клетку. При повторном попадании в эту же ячейку не надо засчитывать ход. public String checkYourself(int guess) { ^ (лмаолк переменит, алм х/мтнмм String result = "mhmo";'v ' mj/tumama, кот'[urn лт dfmm. Mi имел пт и ie цкаАивае и *•« и.ио» (тс ими мм nfm^nt iaiMAK «nj/имах») MdhufmtM сим кештн эм. цента ” массиве. for (int cell : locationCells) { if (guess == cell) { result = "попал"; MenaejaHut! nuntOfHits++; • Сравниваем вьиСр па.шеватсля с эти и a it шипим (тет¬ кой) в.массиве. break; } // конец f.’m.x ueocxicju rntmii прсвепипи if tf/ufllie клетки. ' ' } // конец for if (numOfHits == locationCells. length) { 4—Jin вшыи и; мд/с ад. нс ijadaumt проверим., nr.tixn — “* result = "потопил"; } // конец if System.out.println(result) return result; потеплей ли стартап (осте три попадания) и наши и м строки на «потоп ин». /Мшпи ымл<mma,iM, палтватаы («тиМс» Шси не ЧШМ(" м «шпал» |сш лпоиимин^. ^ и fuAutnuim dt Aа damnjt.Mii лит паи. ) // конец метода 7 М
Давайте пофиксим? Нужно найти способ узнавать, попал ли игрок в конкретную клетку. Рассмотрим возможные варианты, но перед этим сформулируем, что уже известно. У нас есть виртуальный ряд из семи клеток, стартап будет занимать три последовательно расположенные клетки в этом ряду. В этом виртуальном ряду показан стартап, размещенный в клетках 4, 5 и 6. 0 12 3 У стартапа есть переменная экземпляра — целочисленный массив, в котором хранятся местоположения клеток, занятых объектом. locationCells (пере¬ менная экземпляра объекта Startup) ® Вар иаиИЭНТ первый пппппает в цель, Можно создать второй массив и, если пользователь поп и ^ ли записываем этот удар во второй массив, а затем сверяем, уже попадания в эту клетку. Значение true оттает. гтсяпйка, распело- жеиная danwu клетке ft/)PUT(I4\лшсс-ude (СссаНтСеШ), шла поражена. false false tru Массив hitdells (это будет новый массив логических значений пере¬ менной экземпляра объекта Startup) Этот массивсссрржит три сна генил, nj/\ujcmaS\Awiu,ut состашш каждой клетки d.литий , хранящи* местспешжение клеток, занятых стартапом, Hanpuuep, если клетка с инаскссле 2 лира,жена, ни npuedau баеманажнж true элементу с индек¬ сом 2 dмасси de hitCedds.
Первый вариант слишком сложный Первый вариант предполагает больше действий, чем можно ожидать. При каждом попадании необходимо изменять состояние второго массива (hitCells), но перед этим нужно проверить его и выяснить, не было ли попаданий в эту ячейку раньше. Этот вариант вполне рабо¬ чий, но должно быть более простое решение. Можно ограничиться оригинальным массивом, изменяя значение по¬ раженной клетки на -1. То есть для проверки и изменений у нас будет только один массив Второй вариант лучше, но пока не идеален Второй вариант проще, чем первый, но тоже не отличается особой эффектив¬ ностью. Вам по-прежнему придется перебирать в цикле три элемента масси¬ ва, даже если в один или два из них уже попали (и они имеют значение -1). Давайте искать лучшее решение. ® Вариант второй locationCells (пере¬ менная экземпляра объекта Startup)
Псевдокод остовый код Рабочий код ® Вариант третий Можно удалить местоположение клетки, в которую попал пОЛ^ тель, уменьшая массив. Загвоздка в том, что такие объекты не у изменять свой размер, поэтому придется создать новый ме массив и копировать в него оставшиеся клетки. Массив locationCells ДО попадания, ни одна клетка не по¬ ражена. Массив locationCells ПОСЛЕ поражения клетки со значением 5, которая находи¬ лась в массиве под индексом 1. % О ь,ь Массив нашнмтся с tnfttx эле¬ ментов, и.иа ви,ик <с шчгшаш все еп/шлкйки (гити,ии(?массиве) в поисках ссбиицпим.исжаи $t ccjncM пеишвателя и тажниеи клетки <4. Э и б). ' мпасшнии в клетки 5.шл мсешлит невтл массив сеШлжаищи аистспа лсшние клстек. кепшии не сгели пфа- wtHfi и присШаил его исхсаней сайке (hcaticnCeбй. ^ Третий вариант кажется намного лучше, поскольку массив будет уменьшать¬ ся. Нам не придется создавать новый массив меньшего размера, копировать оставшиеся знамения и переопределять ссылку. Оригинальный псевдокод для части метода checkYowrselfO- ПОВТОРЯЕМ то же самое с каждой ячейкой целочисленного массива . II СРАВНИВАЕМ ход пользователя с местоположением клетки ЕСЛИ выбор пользователя совпал, ИНКРЕМЕНТИРУЕМ количество попаданий ——— II ВЫЯСНЯЕМ, была ли это последняя ячейка ЕСЛИ количество попаданий равно 3, ВОЗВРАЩАЕМ «потопил» ИНАЧЕ потопления не произошло, значит, ВОЗРАЩАЕМ «попал» КОНЕЦ ВЕТВЛЕНИЯ ИНАЧЕ пользователь не попал, значит, ВОЗВРАЩАЕМ «мимо» КОНЕЦ ВЕТВЛЕНИЯ КОНЕЦ ЦИКЛА Жизнь сразу наладится, если мы изменим псевдокод: ПОВТОРЯЕМ с каждой из оставшихся ячеек местоположения II СРАВНИВАЕМ ход пользователя с местоположением клетки ЕСЛИ ход пользователя совпал, ■ - Ь УДАЛЯЕМ эту ячейку из массива II ВЫЯСНЯЕМ, была ли это последняя ячейка ^ ЕСЛИ массив пустой, ВОЗРАЩАЕМ «потопил» ИНАЧЕ потопления не произошло, значит, ВОЗРАЩАЕМ «попал» КОНЕЦ ВЕТВЛЕНИЯ ИНАЧЕ пользователь не попал, значит, ВОЗВРАЩАЕМ «мимо» КОНЕЦ ВЕТВЛЕНИЯ КОНЕЦЦИКЛА
Вот бы наити U г массив, кото- ^ рый мог бы уменьшаться, если что-то удаляется. Да такой, в котором не нужно было бы прогонять через цикл каждый элемент, а можно было просто спросить, содержит ли он то, что я ищу. Очень хочется иметь возможность извле¬ кать из него элементы, не зная точно, где именно они находятся. Это было бы за- ^ мечательно. Но это всего лишь у 1^^ ^ мечта... /
Очнитесь и почувствуйте дух библиотеки Словно по волшебству действительно появляется такая вещь. Но это не совсем массив — это ArrayList. Это класс из стандартной библиотеки Java (API). Платформа Java. Standard Edition (версия Java, SE) поставляется с сотнями готовых классов. Это похоже на код, который мы под¬ готовили для вас, где стандартные классы уже скомпилированы. Это значит, что код не нужно набирать. Просто используйте. ArrayList AtrauJList — один 1и вошкой м нсжгствос ыасссв в сЫлистско Java.. Наш тс приме нять ас в своей коде как собственный класс. (Лри.шгание: метод addftt) вы - глядит немнеге странно... нс до главы Имт ас трогать не смдыс. hocmc вссппинимайт/г' егс Лека пр ' как.штсд acLlO, принилшнщий. ссьект, который нужно дссавипи). add( До1,л,ЭТ указанный элегавн, в конец этого елнека. remove(int index) Удаляет элемент в указанной позиции. remove(Object о) Удаляет первое вхождение указанного элемента. contains(Object о) Возвращает true, если список содержит указанный элемент. isEmptyO Возвращает true, если список не содержит элементов. indexOf (Object о) Возвращает либо первый индекс элемента, либо -1. size() Возвращает количество элементов в данном списке. get(int index) Возвращает элемент в указанной позиции. - рексит •методов eAtxagLi/ct
Что можно сделать с помощью ArrayList Шт синтаксис с иьлсвыш скакав <%>. «йтсес нс ссрти/пит на ш i . , и пИи.т{ юнаъжт «сослать us икалтнетс список сосктсЬ (аа © Создать массив у- £ни.мани.я: он просто очистит «сделать ну ук ар ArrayList<Egg> myList = new ArrayList<Egg>(). (5) Положить в него что-нибудь т W Egg eggl = new Egg(); myList.add(eggl); t:,:.".;- r»* Jenehb (lAnauJList пс-нСллстся cu-mu* „uccmrf дш ра/шнс ни.я спекта Л|с|. (5) Положить в него еще что-нибудь Egg egg2 = new Egg() ; myList.add(egg2); Pf (4) Выяснить, сколько элементов в нем хранится int theSize - myList.siz.<) ; 1,5czc(K^<^auvitncZ_ поэте uj © Выяснить, содержит ли он что-либо Army List/)СЛЖС/( сиррмсать boolean isln = myList. contains (eggl) ; ^^осбект&дсссаькси tgcpl. nmncuy contains!) fcjflfunM/ttm Ш, Выяснить, где хранится элемент (то есть его индекс) int idx = myList.indexOf(egg2) © Выяснить, не пустой ли он .■\rtaif/js! всастстгит с ндьм (это cjтага- tm, tmo порти индекс ра&п 0) и Мекс ику < остскт. нд шперыи ссылается сдд2 . ш вторым в списке, 1пскхЩ{) всувраш/н mj_ Си н тютю boolean empty = myList. isEmpty () ; fc- ^auuum ЛisCinptyO (в) Удалить из него что-нибудь myLi s t.remove(eggl); Cucmputm — он ишнешылаь!
аточите карандаш Заполните пропуски в таблице, расположенной ниже, глядя на код на предыдущей странице и подбирая его аналог для обыч¬ ных массивов. Избежать ошибок вряд ли получится, поэтому просто попытайтесь угадать. ArrayList Обычный массив ArrayList<String> myList = new ArrayList<String>(); String 0 my List = nm Strincj[2]\ String a = "whoohoo"; String a = i/iyiyi/i/; myList.add(a); String Ь = "Лягушечка”; String () = Ляицшгка ; myList.add(b); int theSize = myList.size(); String str = myList.get(1); myList.remove(1); boolean isln = myList.contains(b); Ответы на стр. 172,
(Не) дурацкие вопросы B>OV\pOC: ArrayList — классная штука. Но как я вообще должен был догадаться, что она существу¬ ет? Oi/ивеж: Отличный вопрос. Но вы немного не туда смотрите. На вашем месте я бы спросил: «Как вообще узнать, что есть в API?» Вот в чем секрет: ваше главное супе¬ роружие как Java-разработчика — это стандартная библиотека. Не ваша выносливость. Не ваш кофе. API! Когда вы его освоите — и научитесь в нем ориентироваться — можно будет расслабиться: самое сложное уже сделано за вас. Кем-то умным. А вам досталась самая вкусная часть — воплощать идеи и собирать их в работающие программы, слов¬ но из конструктора. И да, поначалу вам придется немного покопаться и разобраться, что в API есть, а чего нет. Но не переживайте: к концу этой главы вы уже будете с ним на «ты». Вопрос: Ладно, допустим. Но ведь проблема глубже: мало знать, что ArrayList существует. Я ведь должен еще понять, что именно он мне нужен для моей задачи! Как, черт возьми, сопоставить свою проблему с правильным инстру¬ ментом в API? Ovтепл: Бинго! Вы попали в точ¬ ку. Вот тут и начинается настоящее программирование. Чем дальше вы будете продвигаться по книге, тем лучше станете чувствовать язык и понимать, как из задачи вытянуть суть — и превратить ее в решение с минимальным количеством кода. Вы увидите, какие шаблоны встре¬ чаются снова и снова, какие классы чаще всего пригодятся, и как на лету подбирать нужный инструмент из арсенала API. Наберитесь терпения: мы обязательно поговорим об этом в конце главы. А пока просто запом¬ ните: не нужно все держать в голове. Рсуь&иление Jana Интервью недели: ArrayList о массивах HeadFirst: Итак, объекты ArrayList в чем-то похожи на массивы, верно? ArrayList: Только в чьих-то мечтах! Я объект, между прочим. HeadFirst: Но, если я не ошибаюсь, массивы — это тоже объекты. Они обитают в куче, как и все остальные. ArrayList: Конечно, массивы находятся в куче. Но они прикидываются ArrayList. Они мимикрируют! Объекты ведь обладают состоянием и поведением, да? Ну так вот. А вы когда-нибудь пробовали вызвать метод у массива? HeadFirst: Сейчас, когда вы это сказали... Пожалуй, нет. А какой метод я должен был бы вызывать? Я же использую методы у объектов, кото¬ рые помещаю внутрь массива, а не у самого массива. И я вполне могу добавлять и удалять элементы обычным синтаксисом. ArrayList: Правда? Удалять из массива? И где, простите, вас учили? HeadFirst: Ну, я же могу написать Dog d = dogArray[l] и получить объект Dog по индексу 1. ArrayList: Супер. Сейчас буду говорить очень медленно. Все, что вы сделали, — это создали копию ссылки на объект Dog и присвоили ее другой переменной. Вы ничего не удалили из массива. HeadFirst: Да, вы правы. Объект все еще в массиве. Но ведь я могу при¬ своить этому элементу null. ArrayList: Присвоить null — это не то же самое, что удалить. А я, меж¬ ду прочим, умею удалять элементы по-настоящему. Потому что я — полноценный объект, поддерживающий методы. И еще я умею изме¬ нять свой размер на лету. Массивы на такое способны? HeadFirst: Знаете, не хотелось бы затрагивать больную тему, но говорят, вы просто обертка для массива. И якобы вы менее эффективны. Да, вы даете удобные методы, например, для изменения размера, которые ина¬ че пришлось бы писать вручную. Но ведь вы даже не можете хранить примитивы! Это разве не серьезное ограничение? ArrayList: Ох, опять эти мифы... Нет, я не просто «менее эффективный массив». Да, бывает, в очень редких случаях массив может оказаться не¬ много, повторяю — немного быстрее. Но вы правда готовы отказаться от всей моей функциональности ради этой крохи производительности? А насчет примитивов — конечно, я не храню их напрямую. Зато вы можете обернуть их в специальные классы-обертки. В пятой версии Java и выше это вообще происходит автоматически — упаковка и распаковка примитивов в Integer, Double, Boolean и так далее. Да, с примитивами массив работает чуть шустрее, ведь ему не нужно ни во что оборачиваться. Но... использовать примитивы в XXI веке? Серьезно? (Смотрит на часы) О нет, я опаздываю на пилатес. Давайте продолжим
Решение <(^^^точите карандаш (со стр. 170) ArrayList Обычный массив ArrayList<String> myList = new ArrayList<String>(); String [] myList = new String[2]; String a = "whoohoo"; String a = "whoohoo"; myList.add(a); myList[0] = a; String Ь = "Лягушечка"; String Ь = "Лягушечка"; myList.add(b); myList[1] = b; int theSize = myList.size(); int theSize = myList.length; String str = myList.get(1); String str = myList [1] ; \3ot£bfl,tiXU 4 myList.remove(1); myList[1] = null; boolean isln = myList.contains(b); boolean isln = false; for (String item : myList) { if (b.equals(item)) { isln = true; break; } } Обратите внимание: с ArrayList вы рабо¬ таете как с объектом соответствующего типа, вызывая старые добрые методы из старого доброго объекта и используя старый добрый оператор доступа. При работе с массивом применяется специ¬ альный синтаксис (такой KanmiList [0] = f оо), который больше нигде, кроме массивов, не употребляется. Хотя массив и является объектом, он существует в собственном не¬ обычном мире. Нельзя вызвать ни один его метод. Доступна лишь одна переменная экзем¬ пляра — length.
Сравнение ArrayLi'st с обычным массивом 0 Обычный массив должен узнать свой раз¬ мер в момент создания. В случае с ArrayList вы просто создаете объект типа ArrayList. Каждый раз. Объекту не нужно знать свой размер. Это не имеет значе¬ ния, потому что он увеличивается и уменьшается по мере добавления и удаления объектов. new String[2] ?(ужн new ArrayList<String>() Pa azutfi нс. пушттся (oomvhcw яшш. ест окт хоктся) 0 Чтобы поместить объект в обычный массив нужно указать ему конкретное место. (Индекс от 0 до числа, которое на единицу меньше размера массива.) myList[l] = Ь; Если этот индекс выходит за границы массива (например, массив объявлен с размером 2, а вы пытаетесь присвоить что-нибудь индексу 3), то во время выполнения программы произойдет сбой. В ArrayList вы можете указать индекс через метод add (anInt, anObject) или просто написать add (anObject) и таким образом увеличить размер объекта, чтобы в него поме¬ стился новый элемент. myList.add(b); ^ЛПх индекса 0 Для массивов используется специальный синтаксис. Нигде в Java он больше не применяется. ArrayList — это обычный объект, осо¬ бый синтаксис для него не предусмотрен. myList[l] К&ш№тии* CKtcku.iwrrufo0 - это аиЖинт сия такси, прлшми 1Ил/иа лкПССШCt>. 0 ArrayList параметризован. Мы сказали, что, в отличие от масси¬ вов, ArrayList не имеет специального синтаксиса. Но такие массивы используют кое-что необычное, специализирован¬ ное — это параметризованные типы'. ArrayList<String> 6ic& <Strri\cj> Сцшвпхскосках — это «па- jviMtmfy тигш.» АriayLis,t<jlihvp оттает nhccm^awccKcmfud». (ttrw вьскмкак Anaij£M<fl)ctp ojnatatm «список «тк* Используя синтаксис <TypeGoesHere>, мы можем объявить и создать ArrayList, который знает (и ограничи¬ вает) типы объектов, которые могут в нем содержаться. Мы рассмотрим детали параметризованных типов в Ar г a yLi s t в главе 11, поэтому сейчас не стоит вдаваться в подробности синтаксиса угловых скобок о, появившихся вместе с ArrayList. На этом этапе изучения Java достаточно знать, что это способ заставить компилятор допускать для ArrayList только определенный тип объектов (в угловых скобках). 'Параметризованные типы были добавле¬ ны в версии Java 5, которая вышла очень давно. Вы почти наверняка используете версию, которая их поддерживает!
Псевдокод Тестовый код Давайте фиксить класс StartUp Помните, так выглядит версия с ошибкой? private int[] locationCells; private int numOfHits = 0; public void setLocationCells (int[] Iocs) { locationCells = Iocs; } public String checkYourself(int guess) ( String result = "мимо"; for (int cell : locationCells) { if (guess == cell) { break; } } // конец for if (numOfHits == locationCells.length) ( result = "потопил"; } // конец if System.out.printIn (result) ; return result; ) // конец метода ) // конец класса class Startup { result = "попал"; numOfHits++;
Псевдокод Новенький и совершенный класс Startup import java.util.ArrayList; tKK0,4yiutti1, public class Startup { private ArrayList<String> locationCells ; // private int numOfHits; // не нужно отслеживать это сейчас ю^жашми public void setLocationCells (ArrayList<String> Iocs) { locationCells = Iocs; Лмиллнт1* cx.ut i Теперь Щ} ' wftjuuW4 этЛт/икц ^ public String checkYourself(String userlnput) String result = "мимо"; Яи*ш| (iuLxCf:0 nfu&pMtm, ccnu-лк fetHHCt m./Uyc fomutu ?нлк и Ш <? cmv Kt., n mi imu|<ki. '<• int index = locationCells.indexOf (userlnput) /цишнидиип — (tymemex- if (index >= 0) { &A locationCells.remove(index);1 < ели UH/Jeicf' rC. £шкши uj> enue if (locationCells.isEmpty()) result = "потопил"; } else { result = "попал"; } // конец if ) // конец внешнего if return result; ) // конец метода ) // конец класса Семи CMUXK стал пи { <Г смШ - это (мм luauf напепааание. Стиртшх петою юа/
Битва за рынок. Полная версия До этого мы разогревались на упрощенной версии, но теперь пришло время поработать по-взрослому. Один ряд? Скучно! Пришло время настоящего игрового поля — 7 на 7 клеток. А если места ста¬ ло больше, значит, и мишеней тоже должно быть больше. На сцене три стартапа: три объекта класса Startup, и все они — законные цели. Создаем игру «Битва за ры¬ нок. Полная версия» с доской 7 на 7 и тремя «стартапами». Каждый стартап будет зани¬ мать три клетки. Цель: Потопить все три стартапа за наименьшее количество ходов. В конце игры вам будет начислен рейтинг — в зависимости от того, насколько ловко вы справились с задачей. Подготовка: Как только вы запустите программу, на виртуальной доске размером 7x7 будут случайным образом размещены три стартапа. После этого игра предложит вам сделать первый ход. Как играть: Графический интерфейс? Мы пока туда не лезем. Все происходит в командной строке. Вы вводите координаты ячейки (например, АЗ, С5, F0) — то место, где, как вам кажется, спрятался стартап. В ответ вы сразу получите результат: . «Мимо» — ну, почти... . «Попал» — хорошее начало! • «Ого! Вы потопили “Поняшки”» — один минус, осталось два! Когда вы потопите все три стартапа, игра завершит¬ ся, и на экране появится ваш результат. А В С D Е F G Сетка 7 на 7 О 1 2 3 4 Ь \ «О** 3/Пс К p s 1- IT >НЯ11 КИ 1 ОД31 1 • Hat пишется с ну гл, как.иасси ava Фрагмент игрового взаимодействия | File Edit Window Help Sell | %java StartupBust Укажите клетку, : АЗ мимо Укажите клетку: : В2 мимо Укажите клетку: : С 4 мимо Укажите клетку: : 02 попал Укажите клетку: : 03 попал Укажите клетку: 04 [ Ого! Вы потопили Поняшки : ( потопил Укажите клетку: G3 [ попал Укажите клетку: G4 попал Укажите клетку: G5 Ого! Вы потопили Кодзи : ( Все стартапы потоплены! Ваши акции теперь бесполезны! Заняло слишком 62 попытки. много времени.
Что нужно изменить? У нас есть три класса, в которые нужно внести измене¬ ния: Startup (раньше назывался SimpleStartup), игровой класс (StartupBust) и вспомогательный игровой класс (его мы пока отодвинем в сторону). 0 Класс Startup ■ Добавьте переменную паше для хранения названий «стартапов» («Поняшки», «Таксисто» и т. д.). Так каждый стартап сможет отобразить на экране свое название, когда пойдет ко дну (см. результат на предыдущей странице). Q Класс StartupBust (игра) ■ Создайте три «стартапа» вместо одного. ■ Назовите каждый из них. Вызовите сеттер для каждого экземпляра класса Startup, чтобы ме тод мог присвоить значение переменной экзем¬ пляра паше. Продолжим настраивать класс StartupBust... Разместите все три стартапа на сетке 7 на 7 клеток. Если вы решите настроить рандомное размещение объектов на сетке, этот этап сильно усложнится. Не будем углубляться в математику, а добавим алгоритм назначения местоположения стартапам в класс GameHelper (готовый код). Каждый ход пользователя нужно прове¬ рять с тремя стартапами, а не одним. Игра продолжается (то есть принимать ходы пользователя и проверять их на остав¬ шихся объектах Startup) до тех пор, пока не останется ни одного живого стартапа. Выход из метода main. Метод main оста¬ вим простым, просто чтобы... он остался простым. В реальной игре мы бы сделали main другим. Три Пять объектов StartupBust Startup GameHelper Игровой класс. Создает объекты Startup, получает данные от пользователя, воспроиз¬ водится до тех пор, пока все стартапы не будут потоплены. Собственно объекты Startup, Стартапы знают свое имя, местоположе¬ ние и как проверить полая ли пользователь. Вспомогательный класс (заранее под¬ готовленный). Он знает, как принимать введенные пользователем данные из командной строки и разме¬ щать стартапы на досхе. о StartupBust sflV J зШшшг о GameHelper Startup плюс четы¬ ре объекта ArrayList: один для StartupBust и по одному для каждого из трех объектов Startup.
За что (и когда) отвечает каждый элемент в игре StartMpBwst StartupBust Игровой класс Создает > Объект StartupBust Метод main() класса StartupBust создает объект StartupBust, который определяет игровой процесс. Создает GameHelper Объект StartupBust StartupBust (игра) соз¬ дает экземпляр класса SameHelper — объект, который поможет функ¬ ционировать игре. Объект SameHelper Объект StartupBust Объект ArrayList (хра¬ нит объекты Startup) Объект StartupBust соз¬ дает экземпляр ArrayList, который будет хранить три объекта Startup.
Объект Star+upBust Объект ArrayList, хранящий объекты Startup Объекты Startup Объект StartupBust создает три объекта Startup (и помещает их в ArrayList) Объект ArrayList, хранящий объекты Startup Объекты Startup StartupBust запрашивает у вспомо¬ гательного объекта (GameHelper) местоположение «стартапа» (делает это три раза, по одному для каждого объекта Startup). StartupBust передает каждому из трех объ¬ ектов Startup отдельный адрес (с помощью вспомогательного объекта) вида А2, В2 и т. д. Каждый объект Startup помещает свои три ячейки в ArrayList. Объект ArrayList Объект StartupBust Объект ArrayList (хра¬ нящий адрес Startup) Объект ArrayList StartupBust запрашивает у вспомогательно¬ го объекта GameHelper информацию о выбо¬ ре пользователем клетки (вспомогательный объект просит пользователя сделать ход и получает данные из командной строки). StartupBust перебирает с помощью цикла список «стартапов» и просит каждый из них проверить пользовательский ход на совпадение. Каждый объект Startup проверяет ArrayList с адресом и возвращает результат («попал», «мимо» и т. д.). Объект ArrayList, хранящий объекты Startup Объекты Startup Объект ArrayList (хра¬ нящий адрес Startup) Объект ArrayList Объект ArrayList Объект StartupBust Игра продолжается: получает ввод пользователя и просит каждый объект Startup прове¬ рить на совпадение. Игра идет до тех пор, пока все объекты с*—+,,» г.
StartupBust GameHelper helper ArrayList startups int numOfGuesses setUpGame() startPlaying() checkUserGuess() finishGame() Объявление переменных Псевдокод для настоящего класса StartwpBwst Класс StartupBust выполняет три главные задачи: подготавливает игру, игра¬ ет с пользователем, пока все стартапы не пойдут ко дну, и завершает игру. Всем трем задачам можно было назначить три отдельных метода. Мы разбили вто-. рой пункт (игра с пользователем) на два метода, чтобы упростить код. Методы небольших размеров (содержащие небольшие фрагменты функциональности) лучше поддаются тестированию, отладке и редактированию кода. ОБЪЯВЛЯЕМ переменную helper и присваиваем ей экземпляр класса GameHelper. ОБЪЯВЛЯЕМ переменную для ArrayList, которая будет хранить список объектов Startup (изначально три). Присваиваем ей имя startups. ОБЪЯВЛЯЕМ целочисленную переменную, в которой будет храниться количество уга¬ данных пользователем вариантов (чтобы иметь возможность показать счет в конце игры). Назовем ее mmQfGuesses и присвоим ей значение 0. Объявление методов ОБЪЯВЛЯЕМ метод setVpGame (), предназначенный для создания объектов Startup и присвоения им имен и местоположений. Также он выводит пользователю краткие ин¬ струкции. ОБЪЯВЛЯЕМ метод startPlaying (), который предлагает пользователю сделать вы¬ бор и вызывает метод checkUserGuess (), пока все объекты Startup не выведены из игры. ОБЪЯВЛЯЕМ метод checkUserGuess (), который перебирает все остальные объекты Startup и вызывает метод checkYourself () каждый объекта Startup. ОБЪЯВЛЯЕМ метод SnishGame (), который выводит на экран сообщение о количестве успешных ходов пользователя, за которые тот потопил все объекты Startup. Реализация методов МЕТОД: void setUpGame() // Создаем три объекта Startup и назначаем им имена. СОЗДАЕМ три объекта Startup. ПРИСВАИВАЕМ имя каждому экземпляру Startup. ДОБАВЛЯЕМ объекты Startup в массив startups (ArrayList). ПОВТОРЯЕМ это с каждым объектом Startup в списке. ВЫЗЫВАЕМ метод placeStartup () из вспомогательного объекта, чтобы получить случайно выбранное местоположение для этого стартапа (три клетки на доске 7 на 7, рас¬ положенные вертикально или горизонтально). УСТАНАВЛИВАЕМ местоположение каждого объекта Startup исходя из результатов вызова метода placeStartup (). КОНЕЦ ЦИКЛА КОНЕЙ МЕТОПА
Псевдокод Рабочий код Продолжение реализации методов: МЕТОД: void startPlaying () ПОВТОРЯЕМ до тех пор, пока существует хоть один объект Startup ПОЛУЧАЕМ пользовательский ввод, вызвав вспомогательный метод ge tUserlnpu t() ОЦЕНИВАЕМ ход пользователя с помощью метода checkUserGuess () КОНЕЦ ЦИКЛА КОНЕЦ МЕТОДА МЕТОД:void checkUserGuess(String userGuess) // Выясняем, было ли попадание в один из объектов Startup (или его потопление) ИНКРЕМЕНТИРУЕМ количество ходов пользователя в переменной numOfGuesses. УСТАНАВЛИВАЕМ значение «мимо» для локальной строковой переменной result, предполагая, что пользователь промахнулся ПОВТОРЯЕМ это с каждым объектом Startup в списке стартапов ОЦЕНИВАЕМ ход пользователя, вызывая метод checkYourself () из объекта Startup УСТАНАВЛИВАЕМ результат значения «попал» или «потопил», если потребуется ЕСЛИ результат равен «потопил», то УДАЛЯЕМ Startup из списка КОНЕЦ ЦИКЛА ВЫВОДИМ пользователю значение переменной result КОНЕЦ МЕТОДА МЕТОД: void finishGame () ВЫВОДИМ сообщение «Игра окончена», затем: ЕСЛИ количество ходов маленькое, ВЫВОДИМ поздравительное сообщение ИНАЧЕ ВЫВОДИМ насмешливый вариант сообщения КОНЕЦ ВЕТВЛЕНИЯ КОНЕЦ МЕТОДА Решать-вам! Оточите карандаш Как мы перейдем от псевдокода к гото- в первую очередь? Попробуйте составить вому коду? Начнем с тестового, будем псевдокод для набора тестов. Для этого тестировать и понемногу достраивать задания достаточно псевдокода или даже методы. Больше в книге тестовый код списка ключевых моментов. Но, если вы показан не будет. Вам придется подумать хотите попробовать написать настоящий над тем, как проверить методы. Какой тестовый код (на языке Java), смело бери- метод нужно протестировать и написать тесь за дело.
Псевдокод import java.util.ArrayList; public class StartupBust { rprivate GameHelper helper = new GameHelper(); ! private ArrayList<Startup> startups = new ArrayList<Startup>(); 1 private int numOfGuesses = 0; private void setUpGameo ( // скачала создаем несколько стартапов //и задаем им местоположение Startup one = new Startup(); one.setName("Поняшхи"); Startup two = new Startup(); two.setName("Кодаи"); Startup three = new Startup() three.setName("Таксисто"); startups.add(one); startups.add(two); startups.add(three); V J 1аточите карандаш — Добавьте выноски к коду самостоятельно! Для фрагментов кода, помеченных цифрами, подберите соответству¬ ющие выноски из тех, что перечислены в ниж¬ ней части страницы. Рядом с выносками напишите подходящие цифры. Каждая выноска будет использоваться один раз, использовать нуж¬ но все. System.out.println("Ваша цель - потопить три стартапа."); System.out.println("Поняппси, Кодзи, Таксисто"); System.out.println("Постарайтесь потопить их за наименьшее количество попыток.")Л for (Startup startup : startups) { ArrayList<String> newLocation = helper.placeStartup(3); (s) startup.setLocationCells(newLocation); } // конец цикла for ) // конец метода setUpGame private void startPlaying () { ^ while (! startups. isEroptyO) { \JJ __ String userGuess = helper.getUserlnput("Укажите клетку:");(j/ checkUserGuess(userGuess); ) // конец while finishGame(); } // конец метода startPlaying 1ШЯГ „ nP „ u вспслтшшлтш Лолиш.м Ш‘1 осдкта штс cmfmana пслшьатШ V . п.. 1 At hart with tarn о taring m Int ml ftOMxfalWL- лю кпаткш uinlimoilUU tXjUjKU. ~fibY yamt fcrtLw okажапся m/ (fU Ш Мпсасшшл* <ic l h noKrt титек cduKm>6ibiti*f Ht скажутся ru^wtut
Псевдокод private void checkUserGuess (String userGuess) { numOfGuesses++; 0 String result = "мимо"; for (Startup startupToTest : startups) { ^5) result = startupToTest.checkYourself(userGuess); if (result.equals("попал")) { break; , ® if (result.equals("потопил ")) { startups.remove(startupToTest); ® break; } // конец for System.out.println(result); ) // конец метода Пожалуйста, не переворачивайте страницу! Доделайте задание до конца. Наш вариант на следующей странице. ► private void fin±shGame() { \ System.out.println("Все стартапы потоплены! Ваши акции теперь бесполезны!"); \ if (numOfGuesses <= 18) { @ System.out.println("Вам понадобилось всего " + numOfGuesses -I- " попыток."); System.out.println("Вы выбрались, пока ваши акции не пошли ко дну."); \ } else { \ System.out.println("Заняло слишком много времени. " + numOfGuesses + " попыток.");' System.out.println("Теперь рыбки танцуют с вашими стартапами"); / } ) // конец метода public static void main (String[] args) StartupBust game = new StartupBust(); game .setUpGame() ; (gj (2l) game.startPlaying(); // конец метода — Это .Повторяем fi-я всех стктв-Sknhf и mum т ста ipman псттлен, иааляем. т iv списка ссЛктсвStaitih1 ц ft,, XUflUi iv н,ккш 7 //нк>и.ш нти1и/(.и келппстк па ajtMHH ах и С, иро ва тс tc м мпвстси ссссщенш. с том как псль^сватш промел иг, типск. fuM<unan\ фи ‘Г Ы Л,Г I 1 вателя ' пока ис вале (сс/ш/иле игревеши остекти . с modxccitueсети шуге ни гошм и/ру ‘Й чти Лририламсм «г\ра1МХ
Псевдокод import java.util.ArrayList; public class StartupBust { Щья&ддьлл. u 'Mfattfyyr Я1ЖМХ private GameHelper helper = new GameHelper(); private ArrayList<Startup> startups = new ArrayList<Startup>() private int numOfGuesses =0; private void setUpGame() { // сначала создаем несколько стартапов и задаем им местоположение Startup one = new Startup(); one.setName("Поняшси"); Startup two = new Startup(); two.setName("Кодзи"); Startup three = new Startup() three.setName("Таксисто"); startups.add(one); startups.add(two); startups.add(three); Ccxicum mhu. ос^ьёктд. SaAtuh. яясм им имта и ГШДкшрхм dAlWyijbt палмс&ипик КПМПКШ. UlUtlUH/KU.WU System.out.println("Ваша цель - потопить три стартапа."); System.out.println("Поняшхи, Кодзи, Таксисто"); System.out.println("Постарайтесь потопить их за наименьшее количество попыток."); ^ ЗЫпаМ аля кажут, cdUxma for (Startup startup : startups) ( Storh^cnlm £ Z^mL.lmujo. aam■ cmatmywa (ibtaujbst dtrincp) ArrayList<String> newLocation = helper.placeStartup(3); startup.setLocationCells(newLocation); < л . ' WWtieatui } // конец цикла for ) // конец метода setUpGame private void startPlaying() { while (! startups .isEmptyO ) { String userGuess = helper.getUserInput("Укажите клетку:"); checkUserGuess(userGuess); ^ , ) // конец while nftl^atMMuncychtrk2Let^iu.v finishGame () ; < ^txdatu.mmuy frmhfyim о us nU(W (символ! огшшт «к ««Г-1" - ¥Ы < Ш. это то же сами, 7 ^ ^ ^ IWAiy('(l’un\L,iA } П конец метода startPlaying
Псевдокод private void checkUserGuess(String userGuess) { numOfGuesses++; ^ 7(nK^u.ucKm.u^.u ксимштбс nuu\тск, сосланных паль^сватож-М String result = "мимо"; < ~ Лреутлашм «>у\юилг», пека не бмжнмтсяссратш* for (Startup startupToTest : startups) { ^ • Яо&полжмшя ^xocMKino(!Stadufi6cnucKt result = startupToTest.checkYourself(userGuess); a r- ,v , , л ' *— J(imu.u осыкт otartufi njwbc- mini хса пс.лгл&пш..1м к поисках if (result.equals ("попал")) { попадания иди потепления break; ^ Mt\xxau.\i ю и/лкла cu tic икон <Г тниД кт/сммсда Шветпи. fflynU (,п‘1р>Пс1ПИ ' I if (result.equals("потопил")) { startups. remove (startupToTest) ; 3mtm nmu.tu«i, иасьшм ш break; ссыктсв Scmtuii u dnxmiou uj ц,м к ш I 7 } // конец for System.out.printin(result); } // конец метода /.di&qiou, tou«mam ти^сШпыя fitx daju. и сасш/нт о том, как no Uj'iGmu. и дуйте i aim private void fmishGameO ( System.out.println("Все стартапы потоплены! Ваши акции теперь бесполезны! ), if (numOfGuesses <= 18) { System.out.println("Вам понадобилось всего " + numOfGuesses + попыток. )> System.out.println("Вы выбрались, пока ваши акции не пошли ко дну. )< } else { п пошяюк »); System.out.println("Заняло слишком много времени. " + numOfGuesses System.out.println("Теперь рыбки танцуют с вашими стартапами ), ) ) // конец метода public static void main(String[] args) { StartupBust game = new StartupBustO; game.setUpGame(); game.startPlaying(); ) // конец метода Сол y)at,u ш/л(Ай cdbiK.ni «««у ■ )
Псевдокод Финальная версия класса Startup import java.util.ArrayList; public class Startup { Лсремшныс жллтляра Stattm: private ArrayList<String> locationCells;^) л' ч " /Vimuj-jMклеток ) имя Startup private String name; public void setLocationCells (ArrayList<String> loC) { locationCells = loc; } public void setName (String n) { ^- name = n; } /Ваш песетой cmnup. ./'Hanoij canmtp. кспчрнк (глюкл.яе m .шяпгтшши сп\арп\стов. (Сщши- KOfc дп-гпип(,1,си/«иис, npujccma&uu.Moe лшпыр. 41 pfxK’tStjr/l HpO uj класса BjamcHtBpa.) Метод indexOfjO m ArtaxpList d at штвии! Вел u rife щс&пжм public String checkYourself (String userlnput) { String result = "мимо"; вернет.ее. umnmaлсяатс. Вс-ш. int index = locationCells. indexOf (userlnput) ; 4^ ^ nan, me tndcxC/ff) tfepHtm 1. if (index >= 0) { locationCells. remove (index) ; — if (locationCells.isEmpty ()) (V .ТГримшясммежщ mnmO upAvtayjList (pin удаления эмЛешпа C nc.wuyit Mtnccja nB'mftfyt) л(.»ю проверять, dec. ш к.«тки тш уыирта. + пате + " : ( "); result = "потопил ; System.out.println("Ого! Вы потопили } else ( result = "попал"; ("<< сяцое. и щ, и/<('атслю с то,и ) и конец if } // конец внешнего if return result; ^^ } // конец метода «шщал» н. ш «Пстеплен» ) // конец класса
Супермощные логические выражения До сих пор мы использовали логические выражения для циклов или условиях, и они были довольно простыми. Далее мы будем использовать более мощные логические выражения в некоторых из готовых кодов. Мы уверены, что вы не забегаете вперед. Так что сейчас самое время по¬ думать, как добавить мощи в ваши выражения. Операторы «И» и «Или» (&&, ||) Допустим, вы пишете метод chooseCamera (), содержащий множество правил выбора камеры. Задать ценовой диапазон для выбора камеры можно довольно широко — например, от 50 до 1000 долларов. Иногда бывает нужно более точно настроить диа¬ пазон цен. Например, нужно написать нечто подобное: «Если цена находится в пределах между 300 и 400 долларами, то выберите X». if (price >= 300 SS price < 400) ( camera = "X"; ) Допустим, из десяти имеющихся марок камер, согласно некото¬ рым условиям, вы можете выбрать несколько. if (brand.equals("А") || brand.equals("Б")) ( // выбрать только бренда А или Б } Логические выражения могут стать очень большими и сложными: if ((zoomType.equals("оптический") SS (zoomDegree >= 3 SS zoomDegree <= 8)) II (zoomType.equals("цифровой") SS (zoomDegree >= 5 SS zoomDegree <= 12))) ( // выбор no типу аума } Если вы хотите по-настоящему глубоко разбираться в этом, нач¬ ните с изучения вопроса о приоритетах операторов. Пока вы еще не стали асом в этой сфере, пользуйтесь круглыми скобками. Так код станет понятнее. Неравенства (!= и!) Предположим, есть утверждение: «Определенное свойство верно для всех десяти доступных моделей камер, кроме одной». if (model ! = 2000) ( // действия для всех моделей, кроме 2000 года ) Или возникла необходимость сравнивать строки... if (!brand.equals("X")) { // действия для всех моделей, кроме X } Операторы с коротким замыканием (&&, 11) Операторы, которые мы рассматривали до сих пор, Si и I I, известны как короткозамыкающие операторы. В случае выражение SS будет истинным только в том случае, если истинны обе его части. Поэтому если JVM видит, что левая сторона выражения SS возвращает false, выполнение останавливается сразу же! На правую сторону интерпретатор даже не взглянет. Аналогично, с оператором | | выражение будет истинным, если истинна любая из его сторон. Если JVM видит, что левая сторона истинна, она объявляет истинным все утверждение, не тратя время на проверку правой стороны. Почему это хорошо? Допустим, у вас есть ссылочная пере¬ менная, и вы не уверены, что она была присвоена объекту. Если вы попытаетесь вызвать метод из этой нулевой ссылки (которая не указывает на объект), вы получите исключение NullPointerException. Поэтому попробуйте сделать следующее: if (refVar != null SS refVar.isValidType()) { // выполняем действия, если тип допустимый } Операторы без короткого замыкания (&. Ц При использовании в логических выражениях операторы & и | действуют подобно своим аналогам SS и | |, за ис¬ ключением того, что они указывают JVM на необходимость всегда проверять обе стороны выражения. Как правило, & и | используются в других ситуациях — например, для работы
Это вспомогательный класс в игре. Кроме метода для приема пользователь¬ ского ввода (который предлагает пользователю сделать ход и считывает из командной строки полученные данные), его главная задача — создание клеток с адресами объектов Startup. Чтобы вам не пришлось набирать много кода, мы постарались сделать его максимально простым. И помните, что вы не сможете скомпилировать игровой класс Star tupBust, пока код этого класса не будет набран. import java.util.*; Готовый public class GameHelper { private static final String ALPHABET = "abcdefg"; private static final int GRID_LENGTH = 7; private static final int GRID_SIZE = 49; private static final int MAX_ATTEMPTS = 200; static final int HORIZONTAL_INCREMENT = 1; static final int VERTICAL_INCREMENT = GRID_LENGTH; private final int[] grid = new int[GRID_SIZE); private final Random random = new Random(); private int startupCount = 0; public String getUserlnput(String prompt) { System.out.print(prompt + "); Scanner scanner = new Scanner(System.in); return scanner.nextLine().toLowerCase(); ) //конец метода getUserlnput int[] startupCoords = new int(startupSize); int attempts = 0; boolean success = false; startupCount++; int increment = getlncrement () ; // // Лучше представить эти два значения // как enum (см. приложение Б) JIpu.mnmue.:JpaM устлнительнси прак¬ тики пепрмиипи «расшмшнтирсьаш» стреки cSumm.ouLprlnlCn и. посмотрите., как эт( раттает. мм ивиашт на экране, ширеуииымк с.lumcHtюшнни <maf\ma- ncv, в игре, эта ишрормаи,ил пригшип\ся, м ишгкиле и рал . и йх прс&ршш ратпи этого мошрг II текущие координаты-кандидаты // счетчик попыток // флаг: удалось ли найти подходящее место // размещаем очередной стартап меняем направление; вертикально или горизонтально while ((success && attempts++ < MAX_ATTEMPTS) ( int location = random.next!nt(GRID_SIZE); // основной цикл поиска // выбираем случайную стартовую точку for (int i = 0; i < startupCoords.length; i++) { // формируем массив предполагаемых // координат startupCoords[i] = location; // добавляем текущую координату location += increment; // рассчитываем следупцую координату } // System.out.printlr, ("Пробуем: " + Arrays.toString(startupCoords)); if (startupFits(startupCoords, increment)) { success = coordsAvailable(startupCoords); ) ) // проверяем, помещается ли стартап на поле // и не заняты ли клетки // конец цикла // конец while savePositionToGrid(startupCoords); // координаты прошли проверку - сохраняем ArrayList<String> alphaCells = convertCoordsToAlphaFormat(startupCoords); // System.out.println("Размещено в: "+ alphaCells); return alphaCells; „ „„ ) // конец метода placeStartup "utHC 4KlT
Готовый код Продолжение класса GameHelper... private boolean startupFits(int[] startupCoords, int increment) { int finalLocation = startupCoords[startupCoords.length - 1]; if (increment = HORIZONTAL_INCREMENT) { // проверяем, что конец находится в той же строке, что и начало return calcRowFromlndex(startupCoords[0]) = calcRowFromlndex(finalLocation); } else { return finalLocation < GRID_SIZE; // проверяем, что координата не выходит за кита» границу } // конец метода startupFits private boolean coordsAvailable(int[] startupCoords) { for (int coord : startupCoords) { // проверяем все возможные позиции if (grid[coord] != 0) { // эта позиция уже занята // System.out.println("позиция: " + coord + " уже занята."); return false; // неудача ) return true; II пересечений не было — ура! ) // конец метода coordsAvailable private void savePositionToGrid(int[] startupCoords) { for (int index : startupCoords) { grid[index] =1; // помечаем клетку на сетке как "занятую" ) ) // конец метода savePositionToGrid private ArrayList<String> convertCoordsToAlphaFormat(int[] startupCoords) { ArrayList<String> alphaCells - new ArrayList<String>(); for (int index : startupCoords) { 11 для каждой координаты на сетке String alphaCoords = getAlphaCoordsFromlndex(index); // преобразуем в формат типа "аО" alphaCells.add(alphaCoords); II добавляем в список } return alphaCells; 11 возвращаем список координат в стиле "аО" // конец метода convertCoordsToAlphaFormat int row = calcRowFromlndex(index); // получаем номер строки int column = index% GRID_LENGTH; // получаем номер столбца String letter = ALPHABET.substring(column, column + 1); // преобразуем в букву return true; // пересечений не было — ура! } // конец метода coordsAvailable private int calcRowFromlndex(int index) { return index / GRID LENGTH; } // конец метода calcRowFromlndex private int getlncrement() { if (startupCount% 2 = 0) { return HORIZONTAL_INCREMENT; ) else { return VERTICAL_INCREMENT; i 11 если номер стартапа четный П размещаем горизонтально II иначе (если нечетный) П размещаем вертикально } // конец метода getlncrement ) // конец класса
Использование библиотеки (Java API) Итак, благодаря ArrayList вы прошли все этапы соз¬ дания игры «Битва за рынок». Ранее мы обещали в конце главы показать, как работать с библиотекой Java. Настало время этому научиться. В Java API классы сгруппирова¬ ны в пакеты. Чтобы использовать класс в API, нужно знать, в каком пакете нахо¬ дится этот класс. Каждый класс в библиотеке Java относится к опреде¬ ленному пакету. Пакет имеет имя, например javax. swing (пакет, который содержит некоторые классы графического интерфейса (GUI) Swing, о которых вы скоро узнаете). ArrayList находится в пакете под названием java. util, который, сюрприз-сюрприз, содержит набор классов -утилит. Вы узнаете много нового о пакетах в Приложении Б, в том числе о том, как помещать свои классы в свои пакеты. Однако сейчас мы просто будем использовать некоторые классы, которые поставляются вместе с Java. Использовать класс из API в своем коде очень про¬ сто. Работайте с классом так, как будто его сами написали, скомпилировали, и он только и ждет, когда вы включите его в код. Есть только одно отличие: где-то в коде вы должны указать полное имя класса библиотеки, который хотите использовать, в формате «имя пакета + имя класса». Вы этого не знали, но вы уже использовали клас¬ сы из пакета. System (System. out. println), String и Math (Math . random ()) находятся в па¬ кете java. lang.
Необходимо знать полное имя* класса, который вы хотите использовать в своем коде. ArrayList — это не полное имя класса, как и Катя — не полное имя. Полное имя ArrayList на самом деле пишется так: java.util.ArrayList Мм.» ncuiuna 1Ш ^'<са Сообщите Java, какой именно класс ArrayList вам нужен. У вас есть два варианта: ИМПОРТИРОВАТЬ Поместите оператор import в начале своего исходного файла: import java.util.ArrayList; public class MyClass {... } или НАБРАТЬ Наберите полное имя класса в любом месте своего кода. Делайте это при каждом использовании класса. Везде, где вы его используете. При объявлении и/или создании его экземпляра: java.util.ArrayList<Dog> list = new java.util.ArrayList<Dog>() При использовании его в качестве типа аргумента: public void go(java.util.ArrayList<Dog> list) { } При использовании его в качестве типа возвращаемого значения: public java.util.ArrayList<Dog> foo() {...} 'He относится к классам из пакета java.lang.
(Не) дурацкие вопросы ^ОПрОС: Почему вообще нужно указывать полное имя класса? Это все, для чего нужны пакеты? Ответ Пакеты играют важ¬ ную роль сразу по трем причинам: 1. Организация кода. Пакеты по¬ могают структурировать проект. Вместо того чтобы сваливать все классы в одну кучу, вы можете ло¬ гично их распределить: например, отдельно интерфейс пользовате¬ ля, отдельно структуры данных, отдельно работа с БД и т. д. 2. Избежание конфликтов имен. Допустим, вы создали свой класс с именем Set. А потом выясняется, что в API Java уже есть такой. Или кто-то другой из вашей команды тоже написал класс с таким име¬ нем. Благодаря пакетам вы можете однозначно указать JVM, о каком именно Set идет речь. 3. Безопасность. Пакеты предо¬ ставляют еще один уровень защи¬ ты: вы можете допускать к опреде¬ ленным частям кода только классы из того же самого пакета. Об этом подробнее рассказывается в При¬ ложении Б. ^ОИрОС: Хорошо, с конфлик¬ тами имен понятно. Но что, если два программиста все-таки на¬ зовут свои пакеты одинаково? Полное имя же не поможет? Ответ Это решается благо¬ даря соглашениям по имено¬ ванию. Если все следуют этому правилу, конфликтов практически не бывает. ~ Без лишних слов — ■ aArrayList — это класс в API Java. ■ Чтобы поместить что-либо в ArrayList, используйте add(). ■ Чтобы удалить что-либо из ArrayList, используйте remove(). ■ Чтобы узнать, где находится то, что вам нужно (и есть ли оно вообще) в ArrayList, используйте indexOf(). ■ Чтобы узнать, пуст ли ArrayList, используйте isEmptyf). ■ Чтобы получить размер (количество элементов) в ArrayList, используйте метод size(). ■ Чтобы получить длину (количество элементов) в обычном массиве, помните, что нужно использо¬ вать переменную length. ■ ArrayList динамически изменяет размер до любого необходимого размера. Он увеличивается при до¬ бавлении объектов и уменьшается при удалении объектов. ■ Вы объявляете тип массива с помощью параме¬ тра типа; это имя типа в угловых скобках. Пример: ArrayList<Button> означает, что ArrayList сможет хранить только объекты типа Button (или подклас¬ сы Button, как вы узнаете в следующих нескольких главах). ■ Хотя в ArrayList хранятся объекты, а не примити¬ вы, компилятор автоматически «заворачивает» (и «разворачивает», когда вы его извлекаете) при¬ митив в объект и помещает этот объект в ArrayList вместо примитива. (Подробнее об этой возмож¬ ности мы поговорим позже). ■ Классы объединяются в пакеты. ■ Класс имеет полное имя — комбинацию имени па¬ кета и имени класса. Полное имя класса ArrayList выглядит так: java.util.ArrayList. ■ Чтобы использовать класс из пакета, отличного от java.lang, вы должны сообщить Java полное имя класса. ■ Вы можете либо использовать оператор импорта в верхней части исходного кода, либо вводить полное имя в каждом месте, где вы используете класс в своем коде.
(Не) дурацкие вопросы ВоИрОС-' Оператор import добавля¬ ет что-то в мой код? Он делает класс больше? Или реально подтягивает весь импортированный класс внутрь? Ответ: Вы случайно не писали на С? Потому что import — это не #include. Никакой «вставки кода» не происходит. Повторим вместе: «Оператор import ну¬ жен только, чтобы мне не приходилось печатать полное имя класса!» Вот и все. Не волнуйтесь, Java не станет работать медленнее или занимать больше памяти только из-за того, что ты написал пару строк import. Это просто удобство, а не магия. Вопрос: Тогда почему мне никогда не приходилось импортировать String или System? Ответ: Потому что Java заботливая. Она автоматически подключает пакет java.lang. Он считается базовым, так что досту¬ пен по умолчанию. В нем и живут такие любимчики, как String, System, Math и другие. Вы просто ими пользуетесь — и все. Java уже всё подготовила. Вопрос: А как быть с моими соб¬ ственными классами? Я обязан класть их в пакеты? И если да, то как? Ответ: Добро пожаловать в реаль¬ ный мир... ну, почти. На практике — да, свои классы стоит размещать в пакетах. Это помогает организовать проект и из¬ бежать путаницы с именами. Но не волнуйтесь: в ближайших главах мы пока не будем грузить вас пакетами*. А когда придет время, все будет просто. Для затравки можете заглянуть в При¬ ложение Б — там все разложено по полочкам. Розы — красные, яблоки — спелые, если импорта нет, код писать тебе сто лет! Вы должны сообщить Java полное имя каждого класса, который вы используете, кроме классов из пакета java.lang. Проще все¬ го сделать это с помощью оператора import, разместив его в начале исходного кода. В про¬ тивном случае вам придется набирать полное имя класса везде, где вы его используете» Еще раз, на случай, если вы еще не поняли Импорт или i <v> *Но когда вы посмотрите на код в репозитории https://oreil.ly/hfjava_3e_ examples, вы увидите, что мы помести-
Как работать с API Для работы с библиотекой нужно знать две вещи: 0 Какие возможности доступны в би¬ блиотеке? (Какие классы?) Q Как использовать эти возможности? (Найдя класс, как вы узнаете, что он может делать?) «Приятно знать, что в пакете java, util есть ArrayList. Но как бы я сама догадалась об этом?» - Джулия, 31 год, модель О Читаем книгу Benjamin J. Evans & David Flanagan Используем документацию no Java API в формате HTML MODULE PACKAGE CLASS USE TREE PREVIEW NEW DEPRECATED INDEX HELP Java* Platform, Standard Edition & Java Development Kit Version 17 API Specification This document is divided into two sections: Java SE The Java Platform, Standard Edition (Java SE) APIs define the core Java platform JDK The Java Development Kit (JDK) APIs are specific to the JDK and will not necessai All Modules java SE JDK Modules Module Description Java.base Defines the foundational APIs of the Java SE Platform, java.compller Defines the Language Model, Annotation Processing, a Java.datatransfer Defines the API for transferring data between and with https://docs.oracle.com/en/java/javase/17/docs/api/index.html
Читаем книгу Листать справочник — хороший способ познакомиться с содержи¬ мым библиотеки Java. Просматри¬ вая книгу, можно обнаружить пакет или класс, который вам пригодится. O'REILLY i 8. Working with Java Collections 12И 3m remaining The List Interface A List is an ordered collection of objects. Each element of a list has a position in the list, and the List interface defines methods to query or set the element at a particular position, or index. In this respect, a List is like an array whose size changes as needed to accommodate the number of elements it contains. Unlike sets, lists allow duplicate elements. In addition to its index-based get() and set() methods, the List interface defines methods to add or remove an element at a particular index and also de¬ fines methods to return the index of the first or last occurrence of a particular value in the list. The add() and remove)) methods inherited from Collection are defined to append to the list and to remove the first occurrence of the specified value from the list. The inherited addAll () appends all elements in the specified collection to the end of the list, and another version inserts the el¬ ements at a specified index. The retainAll() and removeAll() methodsbe- have as they do for any Collection, retaining or removing multiple occur¬ rences of the same value, if needed. The List interface does not define methods that operate on a range of list in¬ dexes. Instead, it defines a single subListo method that returns a List object that represents just the specified range of the original list. The sublist is backed by the parent list, and any changes made to the sublist are immediately visible in the parent list. Examples of subListo and the other basic List manipulation methods are shown here: II Create lists to work with List<String> 1 - new ArrayList<String>(Arrays.asList(args))) List<String> words = Arrays.asList("hello", “world")) List<String> words2 = List.of("hello", "world")) HttUH ■шрпЬя hv i пНрк Ш <s
о Использование документации по API в HTML-формате В дополнении к Java идет потрясающий набор онлайн-документов под названием Java API. Вы (или ваша IDE) можете скачать эти документы на жесткий диск на случай неполадок с интернет-соединением, ко¬ торое может оборваться в самый неподходящий момент. Документация по API — это лучший справочник для получения подробной информации о том, что вхо¬ дит в пакет и что предоставляют классы и интерфейсы пакета (например, методы и функциональность). Документация выглядит по-разному в зависимости от версии Java, которую вы используете. Убедитесь, что вы смотрите документацию для вашей версии! Java 8 и более ранние версии httpsrf/docs.orade.com/javase/8/docs/api/index.html % 1 la аЪ-З?» со java о х I Java' Platform Standard Ed. 8 Java” Platform PACKAGE CLASS USE TREE DEPRECATED INDEX HELP Standard Ed. 8 All Classes All Profiles rnev NEXT FRAMES NO FRAMES “«А», Тн П( ZT,WKtu^<- Шш Wtoefcmnu только классу а? этт пакта. * Package* java,applet Java awt java.awt.color Java.awt.datatransfer java.awt.dnd Java. awt. event Ail Cfas&es Abstra ctAcrksn Abstra ctAn notatio n Va lueVis Itort AbstractAn notation ValueVIsitor 7 Abstra ctA n notation va lu eVi 5 ltor8 AbstractBorder Abstracts Litton AbstractCellEditor Abstract Chronology Abstra ctCollect ion Abstra ctColofOiooser Pa ne I Abstract Document A bstractDocument. A ttiibuteContext AbstractOocumenLContent AbstractDocume n t. Ef ementEd It AbstractEle mentVrs ltor6 AbstractE le me ntVJs itor7 AbstractE lemen tVisItorS A bstract E xecutoi S ervlc e Abstract interru pti bleChannel A bstract La you tCaehe Abstracttay outCacbe. Node Dtme n slo n • AbstractList AbstractListModel AbstractMap Java” Platform, Standard Edition 8 API Specification This document is the API specification (or the Java” Platform. Standard Edition. See: Description Profiles • compactl • compact2 • compact3 Package Java.applet Java.awt java.awt.color Java.awt.datatransfer Description Provides the classes necessary to create an applet and the classes an applet uses to communicate with its applet context. Contains all of the classes for creating user interfaces and for painting graphics and images. Provides classes for color spaces. Provides Interfaces and classes for transferring data between and within applications. AbstractMap.SimpleEntry AbstractMap.SImplelmmutable Entry AbstractMarshailerlmol Java.awt.dnd Drag and Drop is a direct manipulation aesture found in manv Granhical User Есть два способа навигации по документам: Сверху вниз: найти интере¬ сующий вас пакет в спи¬ ске, расположенном слева вверху, и смотрите ниже. Сначала по классам: най¬ дите в списке слева внизу класс, который вас интере¬ сует, и выберите его. На главный, панели Сы иСиаите пащнию ипОк[ьиаи,шс с т< ж г тс Си Снижали! /ели Сь\ Сити 1т пакет, та Си тем ишрерлищим с пакете и список классоСи интеМхиссС. /ели Сифапи масс, от паяСится ас списание, а также nujfucfcau umfxfiuau,UM обо Ссехлшпшах кшсса, с таж mu они atiowm. и как и,г истх ил Сапи.
Java 9 и более поздние версии В Java 9 появилась система модулей Java, которую мы не будем рассматривать в этой книге. т ^ необходимо знать, чтобы понять документацию, — это то, что JDK теперь разделен на модули- ^ модули объединяют связанные пакеты. Это может облегчить поиск нужных классов, поскольку сгруппированы по функционалу. Все классы, которые мы рассмотрели в книге, находятся в м у java, base; он содержит основные пакеты Java, такие как java.lang и java.util- ^ URL-ai^Mc намного стшкг- стт cm cmtifmx (]ску,шмтс(1 https:/ / docs.oracle.com/en/java/javasei/ 17/docs/api/index.htmI a\V : MODULE PACKAGE CLASS USE TREE PREVIEW NEW DEPRECATED INDEX HELP Java Si 17 fc JDR 17 StfWO* I . Se.rch “ ■ ■ X Java* Platform, Standard Edition 6 Java Development Kit Version 17 API Specification This document is divided into two sections: /At /*L'CC(V//|- J”‘ SL «униш The Java Platform. Standaid Edition Gave SE) APIs define the core Java platform for general-purpose computing. 7/" R УгТ\ ti/-; *, These APIs are in modules whose names start with java Ust The Java Development Kit (JDK) APIs are specific to the JDK and will not necessarily be available in all implementations of the Java SE Platform These APIs are in modules whose names start with jdk. Other Modules -java.base java.compiler java.datatransfer java .desktop ptdc U(J j javanv^T j\av<^u,'a abi-c'CSw |nv<ufekUf. Есть два способа навигации по документам: Сверху вниз: найти модуль, который охватывает нужную функциональность, просмотреть его содержимое и перейти от пакетов к его классам. Поиск: использовать строку поиска в правой верхней части, чтобы перейти непо¬ средственно к методу, классу пакету или модулю, о кото¬ ром вы хотите прочитать. Description Defines the foundational APIs of the Java SE Platform. Defines the language Model. Annotation Processing, and Java Compiler APIs. Defines the API for transferring data between and within applications. mid Java^eans^ ЭПС* ^wln® user ^terface toolkits, plus APIs for accessibility, audio, imaging, printing. Htn/L OVERVIEW 1 PACKAGE CLASS USE TREE PREVIEW NEW DEPRECATED INDEX HELP MOOUtE: DCSCRemON | MODULES 1 MCKACES | SERVICES Module java.base SEARCH: module java.base Defines the foundational APIs of the Java SE Platform. Provider*: svstem provider to e The JDK Implementation of this module provides an implementation of the J ” and resource files In a run time image. The jrt file system can be created by FileSystees. newFileSystes!URI.create!*jft:/*)). Module Graph: Tool Guido*: java lauocher, keytool }}i>v)fvifl мСШ4> .UCXTTIBf UWftl ЧК ftc It. (?t)\ :n\< (mnaKmxdcnoKt n\c(? U CIXWMHWt, гас injwtH К<ШГ|НЙ iviKoA. Provides for system input and output through data streams, serialized Provides classes that are fundamental to the design of the java progrn Provides libraiy support for tta j..» pn^.mming lMgu»g*
Чтение документации к классу Какую бы версию документации по Java вы ни использовали, все они имеют схожую схему отображения информации о конкретном классе. Именно здесь находятся самые важные детали. Допустим, вы просматривали справочник и нашли класс ArrayList в java .util. В книге он описан достаточно скупо. Понятно, что именно его нужно применять, но вам необходимо узнать больше о методах. В справочнике вы найдете метод indexOf () . Об этом методе вам известно очень мало — только то, что он принимает объект и возвращает индекс (типа int) этого объекта. В данном случае очень важно прояснить: что произойдет, если объекта не будет внутри ArrayList? Взглянув лишь на сигнатуру метода, вы не сможете понять, как это происходит. Здесь вам пригодится документация API (по крайней мере, в большинстве случаев). В документации API говорится, что метод indexOf () возвращает значение -1, если параметр объекта не находится в списке ArrayList. Вот откуда мы знаем, что можем использовать его как способ проверить, находится ли объект в списке ArrayList, и одновременно получить его индекс, если объект там есть. Но без документации по API мы могли бы по¬ думать, что метод indexOf () выбросит исключение, если объекта не будет в ArrayList. Object clone() Return» a shallow еру of this ArrayList inotaflro J L contains(Object o) Returns t rue if this list contains the specified element. - никажем, как » » ментации API „СКать инфорМаци® Д°Ку‘ аильному применению библиотек w
fa “УЬражн \ражнвнив Магнитики с кодом Попробуйте собрать из фрагментов кода работоспособную про¬ грамму, которая выведет на экран текст, приведенный ниже. При¬ мечание: чтобы выполнить это упражнение, вам необходимо знать еще кое-что: если вы отыщете ArrayList в документации, то увидите второй метод add (), который принимает два аргумента: add(int index, Object о) С его помощью вы можете сказать объекту ArrayList, куда именно поместить объект при добавлении. public st,tic void printliit(ArrayI.i3t<St4ng> list) ( | “ MCW,. . a-add("2.2") • * a.add(2, "два"); public static void main (String[] args) { System.out.print(element + > System.out.println(); If (a.contains("три”)) { a.add("четыре"); _i public class ArrayListMagnet { if (a.indexOf("четыре”) != 4) { a.add(4, "4.2"); ) File Edit Window Help Dance % java ArrayListMagnet НОЛЬ один два три ноль один три четыре ноль один три четыре 4.2 ноль один три четыре 4.2
Как кроссворд поможет вам в изу¬ чении java? Все слова в нем связа¬ ны с этим языком. Подсказка: Когда начнете сомне¬ ваться, вспомните ArrayList. 1 5 8 2 9 4 12 3 6 10 13 7 14 17 И 15 16 По горизонтали: 2. Элемент структуры данных или дерева 3. Линия, отделяющая одну область от другой 6. Привязка значения к определенному виду данных 7. Хранилище информации, например, таблиц 9. Запуск чего-либо 11. Данные, получаемые извне 12. Последовательность инструкций для компьютера 14. Основная часть метода или цикла 15. Признак или переключатель состояния 16. Средство передачи информации между компонентами По вертикали: 1. Временное хранилище для данных при передаче 4. Элемент интерфейса, по которому можно нажимать 5. Структура, предназначенная для ввода данных 7. Элемент текста или строки 8. Набор команд, выполняющий вычисления 10. Аудиосигнал или его представление 13. Свойство отображения, например, у текста 15. Цветовая основа или задний слой 17. Запись событий, ошибок и действий программы Подсказки ohwocIi шоитию ои«9 эн ngoih 'эмэюиэо -ntfXe a qj.eaodm/Ajad онж/н ojg oi ■qi«u хи <аэмэ1» эаоиэ g £ ■MnhediDHjad «wada оа юхэЛсямоюи охэеь S *«енэиио» иии «уо» laeaiqg v ■иоюиоэЬ'иа т/и енэмдо da(}>^9 qj.ej.09ed 19^9 эн оээн tag • i :ni/exnidaa оу нэьои/х»яа/нэк -ou/яа — atfox а иохьиои qi«uaedu^ N9oih 1о<еаипаене1э^ ojg -51 •«хюннеЬ1 »3HHa>xed -tqa oumou ojodoiox 10 'oaoiQ 4 ‘ЮхаЛсЬи/иииюмэ эн tfox — ojaiaawn эн «eHHawadau июд ‘g ■eiHaw^xoff-n/yx иии eфedJ woj.Hawaue qi«91эжом oj.£ z :nneiHO£ndoj oy *► Ответы на стр. 202
fa Ответы import java.util.ArrayList; public class ArrayListMagnet { public static void main(String[] args) { File Edit Window Help Dance % java ArrayListMagnet ноль один два три ноль один три четыре ноль один три четыре 4.2 ноль один три четыре 4.2 ArrayList<String> а = new ArrayList<String>(); а.add(0, "ноль"); a.add(l, "один"); а.add(2, "два"); a.add(3, "три"); printList(a); if (a.contains("три")) { а.add("четыре"); Магнитики с кодом (со стр. 199) ) а.remove(2); printList(а); if (a.indexOf("четыре") != 4) { a.add(4, "4.2"); ) printList(а); if (a.contains("два")) { a.add("2.2"); ) printList(а); public static void printList(ArrayList<String> list) { for (String element : list) { System.out.print(element + " "); ) System.out.println(); >
■б 5ф 8ф 2у 3 е Л о 9П У с к ф р Н е "к м 12к о д Зг р а н И Ц а Ц о И бт и п И ,0з а 13ц и Я к В В 76 а 3 а У е У К Игр е 17л о К о ив в о д >5Ф л а г а о ,6К а н а л аточите карандаш к По горизонтали 1. 6. 7. 9. 12. 13. 14. 16. 17. 19. 21. 22. 23. 24. Составьте свой набор вопросов для кроссворда! Посмотрите на каждое слово и попробуйте зашифровать его. Поэкспериментируйте, делая их проще или сложнее, или формулируя вопросы более техническим языком. По вертикали 2. 3. 4. 5. 8. 10. 11. 15. 16. 18. 20. 21.
7. Наследование и полиморфизм Прекрасная жизнь в Объектино ( Эра трудоголика-програм- миста с небольшой зарплатой за¬ кончилась. Возможности полимор¬ физма проложили дорогу в светлое V будущее. Присоединяйтесь! Планируйте СВОИ программы С прицелом на будущее. Как вы оцените способ соз Дания Java-приложений, при котором у вас останется много свободного времени? Представьте, что вы можете написать такой код, который легко сумеет дополнить другой программист. Заинтересует ли вас создание гибкого кода, которому не страшны досадные изменения в техническом задании, вноси¬ мые в последний момент? Поверьте, вы можете получить все это лишь за три простых подхода по 60 минут каждый. Изучив принципы полиморфизма, вы узнаете о пяти шагах грамотного проектирования классов, трех приемах достижения полиморфизма и восьми способах создания гибкого кода. Если вы начнете прямо сейчас, то получите бонус из четырех советов по использованию наследования. Не мед¬ лите! Приняв наше предложение, вы обретете заслуженное — свободу и гибкость при проектировании программ. Это быстро, просто и доступно прямо сейчас. Начните сегодня — и вам покорятся невидан¬ ные уровни абстракции!
И снова битва за кресло... Надеемся, вы не забыли, как в главе 2 Лора (любитель процедурного стиля) и Брэд (приверженец ООП) сошлись лицом к лицу в борьбе за кресло Аегоп? Вспомним несколь¬ ко отрывков из той истории, чтобы повторить основные принципы наследования. ЛОРА: Твой код дублируется! Процедура rotate есть в каждой из четырех... штук, которые отвечают за фигуры. БРЭД: Ты не видела результат. Сейчас я покажу тебе, Лора, как наследование работает в объектно-ориентированном подходе. Square Circle Triangle Amoeba rotateQ playSoundQ rotate() playSoundQ rotateQ playSoundQ rotateQ playSoundQ О Я выяснил, что общего между этими классами. 4^ Это фигуры, и все они вращаются и воспроизводятзвуки. Я абстраги¬ ровал их общие черты и поместил фигуры в новый класс с именем Shape. Эти отношения можно прочитать как «квадрат унаследован от класса Shape», «круг унаследован от класса Shape» и т. д. Я убрал методы rotate () nplaySoundO из других фигур, по¬ этому теперь нужно поддерживать только один экземпляр каждого из них. Класс Shape — родительский для остальных четырех классов, которые, в свою очередь, дочерние (потомки или подклассы) для него. Дочерние классы наследуют методы родительского класса. Иными словами, если класс Shape обла¬ дает какой-то функциональностью, то его подклассы получают те же возможности.
А что насчет метода rotate^) фигуры Amoeba? Лора: Не кроется ли вся проблема в том, что аме¬ ба имеет совершенно другие процедуры rotate и playSound? Как амеба сможет делать что-то по-своему, если она наследует свою функциональность от класса Shape? Брэд: Это завершающий штрих. Класс Amoeba пере¬ определяет методы класса Shape потом, при вы¬ полнении программы, JVM будет точно знать, какой именно метод rotate нужно вызвать, когда понадо¬ бится вращать амебу. Переопредели сейчас Спроси меня как Я сделал так, чтобы класс Amoeba переопределил методы rotate^) и playSowndf), принадлежащие родительскому классу Shape. Переопределение означает, что подкласс заново описывает унаследованные им методы, ^если ему нужно изменить или расширить их. «Ч Переопределение методов Как вы опишете в структуре наследования домашнего кота и тигра? Может ли обычный кот рассматриваться как конкретный (специфичный) вариант тигра? Кто из них станет родительским классом, а кто — дочерним? Или они оба будут наследниками какого-то другого класса? Как вы спроектируете такую структуру наследова¬ ния? Какие методы при этом переопределите? Поду¬ майте об этом, прежде чем перевернете страницу.
Принципы наследования При проектировании с наследованием вы помещаете общий код в один аб¬ страктный класс и объявляете его родителем для других, более специфичных. Когда один класс наследует другой, это означает, что дочерний класс наследует родительский. В контексте Java мы говорим, что дочерний класс расширяет родительский. Подобные отношения подразумевают, что дочерний класс наследует члены родительского класса. Когда мы говорим «член класса», мы имеем в виду пере¬ менные экземпляра и методы. Например, если Человек-пантера (PantherMan) — потомок Супергероя (SuperHero), то класс PantherMan автоматически наследует переменные экземпляра и методы, общие для всех супергероев, включая костюм (suit), трико (tights), суперсилу (specialPower), умение использовать суперсилу (useSpecialPower ()) и т. д. Но дочерний класс PantherMan может иметь собственные переменные экземпляра и методы и способен переопределить методы, унаследованные от SuperHero. Родительский класс (более абстрактный) Дочерние классы (более конкретные) SuperHero suit tights specialPower useSpecialPower() putOnSuitQ Переменные экземпляра (состояние, атрибуты) Методы (поведение) FriedEggMan PantherMan useSpecialPower() putOnSuitQ Переопределение методов Желткомэн (FriedEggMan) не нуждается в уникальном поведении, поэтому ему не нужно переопределять методы. Достаточно методов и переменных экземпляра, объявлен¬ ных в классе SuperHero. А вот у PantherMan есть особые требования к костюму и суперсиле, поэтому в его классе переопределяются мето¬ ды useSpecialPower () и putOnSuit (). Переменные экземпляра не переопределяются, так как в этом нет необходимости. Они не описывают специфиче¬ ское поведение, поэтому дочерний класс может присвоить им любые значения. PantherMan может задать перемен¬ ной tights пурпурный цвет, тогда как FriedEggMan сделает его белым.
Пример наследования: public class Doctor { boolean worksAtHospital; void treatPatient() { // проведение обследования ) > public class FamilyDoctor extends Doctor { boolean roakesHouseCalls; void giveAdvice() { II раздача дельных советов } 1 public class Surgeon extends Doctor { void treatPatient() { // проведение операции ) void roakeIncision() { // выполнение надреза (ой!) > Родительский класс Заточите карандаш Сколько переменных экземпляра у класса Surgeon? Сколько переменных экземпляра у класса FamilyDoctor? Сколько методов у класса Doctor? Сколько методов у класса Surgeon?. Может ли семейный врач выполнить метод treatPatientQ? Может ли семейный врач выполнить метод makelncisionO? Решать — вам!
Построим иерархию наследования на примере симулятора животных Представим, что вы создаете программу для моделирования поведения животных. Пользователь сможет выбрать несколь¬ ких животных, разместить их в среде обитания и наблюдать за тем, как они себя поведут. На данном этапе код писать не нужно: мы сосредоточимся на проектировании. У нас есть начальный список животных, которые должны присутствовать в программе, но он пока неполный. Известно, что каждое животное будет представлено объектом, и эти объекты смогут перемещаться по среде и выполнять запро¬ граммированные действия. Кроме того, мы хотим, чтобы другие разработчики могли легко добавлять в программу новые виды животных. Первый шаг — выделить общие, абстрактные свойства, ха рактерные для всех животных, и оформить их в виде класса, от которого будут наследоваться все конкретные животные. О Нужно найти объекты, которые имеют общие черты и поведение. Что общего у этих шести типов? Эта информация поможет выделить по¬ ведение (шаг 2). Как эти типы связаны между собой? Эти сведения помогут определить иерархию наследования (шаги 4-5).
Наследование для предотвращения дублирования кода в дочерних классах У нас есть пять переменных экземпляра. picture — имя JPEG-файла с изображением данно¬ го животного. food — тип еды, которую это животное употребляет. Пока допускаются два значения: мясо и трава. hunger — целое число, описывающее уровень ап¬ петита животного. Оно изменяется в зависимости от того, когда и сколько животное ело. boundaries — значение, описывающее длину и ши¬ рину «пространства» (например, 640x480), по которо¬ му животное будет бродить. location — координаты х и у, определяющие место¬ положение животного. О Разработаем класс, который вопло щает общие черты и поведение. Все эти объекты представляют животных, поэтому мы создадим общий родительский класс Animal. Мы добавим в него методы и переменные экземпляра, которые могут понадобиться всем животным. Animal picture food hunger boundaries location makeNoise() eat() sleep() roam() У нас есть четыре метода: makeNoise () — поведение животного, когда оно должно издать звук. eat () — поведение животного при обнаружении своего предпочтительного источника пищи — мяса или травы. sleep () — поведение животного, когда оно решает поспать. roam () — поведение животного, когда оно не ест и не спит (вероятно, просто скитается вокруг в ожи¬ дании столкновения с источником пищи или грани¬ цами). Lion Hippo
Все ли животные питаются одинаково? Допустим, переменные экземпляра будут уместны для всех типов животных. Лев будет иметь собственные зна¬ чения переменных picture, food (вероятно, это мясо), hunger, boundaries и location. У бегемота будут свои значения, но набор переменных у них будет такой же, как и у других типов животных. То же самое с соба¬ кой, тигром и т. д. Но что насчет поведения? Какие методы нужно переопределить Издает ли лев такие же звуки, как собака? Одинаково ли питаются кот и бегемот? В вашей версии — может быть, но у нас питание и звуки зависят от типа животного. Как заставить эти методы работать сразу для всех животных? К примеру, можно написать метод makeNoise (), кото¬ рый будет проигрывать звуковой файл, заданный в виде значения переменной конкретного типа, но такое пове¬ дение будет не совсем специализированным. Некоторые животные в различных ситуациях могут издавать разные звуки (например, во время приема пищи, при встрече с врагом ит.д.). Вспомните, как для объекта Amoeba мы переопределяли метод rotate класса Shape, чтобы получить более специфич¬ ное (другими словами, уникальное) для нее поведение. Теперь нужно сделать то же самое и для наших потомков класса Animal. О Я крутой пожира¬ тель кустарников. Animal picture food hunger boundaries location makeNoise() eat() sleepQ roam() Следует установить, нуждается ли дочерний класс в поведении (реализации методов), характерном именно для этого типа. Взглянув на класс Animal, мы решили, что методы eat() и makeNoiseQ нужно переопределить на уровне отдельных дочерних классов. В собачьем сообществе4 лай считается важной ча- 1 стью культурных различий. Все мы гавкаем по-разному, нам хочется, чтобы нас узнава¬ ли и относились с уважением к нашим особенностям. , Л Лам мпш. ru lu с I vhujc. t am t э m u аважто- qa, taw u такоМсшО, mwdih кажут (juy jrwtfotnmuv.iiCT ttyu<pumu e(?cu ccmnfi tt am nhtrmiTmtiuut ((turn it up u uмава- cutwjWK(£. JIcKrt rmc, похоже, жтшм sCaf\(f и roam/) могут оставаться ош,им.и.
Ищем новые возможности наследования Иерархия классов начинает прорисовываться бо¬ лее четко. Каждый дочерний класс должен перео¬ пределить методы makeNoise () и eat (), чтобы лай объекта Dog нельзя было спутать с мяуканьем Cat (это было бы оскорбительно для обоих). Кро¬ ме того, Hippo должен питается совсем не так, как Lion. Но, возможно, мы можем сделать кое-что еще. Взглянем на потомков класса Animal и подумаем, можно ли каким-то образом сгруппировать не¬ сколько из них, выделив общий код, характерный только для этой новой группы. Классы Wolf и Dog имеют общие черты, как и Lion, Tiger и Cat. О Нужно найти больше возможностей для использования абстракции, выделив несколько дочерних классов, которые могут иметь общее поведение. Взглянув на классы, мы понимаем, что у Wolf и Dog есть общие черты. То же самое относится к Lion, Tiger и Cat. makeNoiseO makeNoiseO eat() uv -tnvrnvo CCVWJU. Vj u 4 . HUUj'Ul W KCNXrt. Animal picture food hunger boundaries location makeNoiseO eat() sleep() roam() -$<- Mr и пи — /с тки... .ш mm urn, mu тс, истт пЛшсс/iwucm i" AC taCCOAA... Lion makeNoiseO eat() Hippo makeNoiseO makeNoiseO eat() eat() makeNoiseO eat()
Завершение работы над иерархией классов. Поскольку животные собраны в организованную иерар¬ хию (все царства, роды, типы), то при проектировании классов можно использовать наиболее уместный уровень абстракции — биологические «семейства». С их помо¬ щью мы организуем животных, разделив их на классы Feline и Canine. Мы решили, что потомки Canine могут использовать общий метод roam (), так как предпочитают жить в стае. Потомки Feline тоже могут иметь общий метод roam (), потому что склонны избегать себе подобных. Класс Hippo может продолжать использовать общий метод roam (), унаследованный от Animal. Итак, на данном этапе мы закончили с проектированием. Но мы еще вернемся к нему позже в этой главе. picture food hunger boundaries location makeNoiseO eat() sleep() roam()
Какой метод вызывается? У класса Wolf есть четыре метода. Один унаследован от Animal, другой — от Canine (это, по сути, переопределенная версия метода из класса Animal) и еще два переопределенных. Создав объект Wolf и присвоив его переменной, вы можете ис¬ пользовать оператор «точка» в сочетании со ссылкой, чтобы вызывать все четыре метода. Но какие их версии будут при этом вызваны? Сование, некого (сикти dtpCU U в\\щ fitted tiejrwvLVL вCanine. версии. пщ /fitted версии. в Animat! ъщ Wolf w = new Wolf(); w.makeNoise(); Animal makeNoise() eat() sleep() roam() w.roam (); Вызывая метод по ссылке на объект, вы по¬ лучаете его версию, наиболее характерную для этого типа объектов. Иными словами, берется метод самого низкого уровня. Это «самый низкий» метод в контексте иерархии наследования. Класс Canine находится ниже, чем Animal, a Wolf — ниже, чем Canine, поэтому вызов метода из ссылки, связанной с объектом Wolf, заставляет JVM искать его в первую очередь в классе Wolf. Не найдя метод там, JVM начинает подниматься по иерархии наследо¬ вания, пока не найдет совпадения.
Проектирование иерархии наследования Класс Родительский класс Дочерние классы Clothing ... Boxers, Shirt Boxers Clothing Shirt Clothing Родительский класс (более абстрактный) Дочерние классы Таблица с иерархией наследования Заточите карандаш Найдите отношения, которые имеют смысл. Заполните правые два столбца. Класс Родительский класс Дочерние классы Musician Rock Star Fan Bass Player Concert Pianist Подсказка: не каждый элемент может быть связан с другим элементом. Подсказка: можно добавлять или изменять перечисленные здесь классы. Boxers Shirt Схема наследования классов Нарисуйте схему наследования. _► Решать-вам! (Не) дурацкие вопросы ^ОПрОС: Вы говорите, что JVM поднимается вверх по иерархии наследования, начиная с класса объекта, у которого вызывается метод (как в примере с классом Wolf на предыдущей странице). А что будет, если JVM так и не най¬ дет подходящего метода? О VHBeWI: Отличный вопрос! Не переживайте: компилятор этого не допустит. Он всегда проверяет, что у ссылки такого типа действительно можно вызвать нужный метод. Но вот где именно этот метод будет найден — компилятор не волнует. Например, если вы вызываете ме¬ тод sleepO у объекта Wolf, компи¬ лятор просто убеждается, что Wolf (или его родитель) где-то содержит этот метод. И даже если метод унаследован от Animal, все в поряд¬ ке — Wolf этот метод «наследует», а значит, имеет. Зато JVM, когда запускает про¬ грамму, уже заботится о том, чтобы выбрать наиболее подходящую (и наиболее конкретную) версию метода — ту, которая ближе всего к реальному объекту.
Отношения IS-А и HAS-A Когда один класс наследует другой, говорят, что дочерний класс расширяет родительский. Если хотите понять, правильно ли спроектиро¬ ваны ваши типы, проведите простую провер¬ ку: IS-А (является ли). Треугольник — это фигура? Да, все верно. Кошка — это представитель семейства коша¬ чьих? Абсолютно. А ванна — это ванная комната? Хм, уже звучит странно. Именно для таких случаев и нужна проверка на соответствие. Простой способ понять: задайте себе во¬ прос — «Можно ли сказать, что объект типа Xявляется типом У?» Если ответ — нет, скорее всего, с архитектурой что-то не так. Например, если у вас есть классы Tub (ванна) и Bathroom (ванная комната), то утверж¬ дение «ванна — это ванная комната» звучит нелепо. А если поменять их местами: «ванная комната — это ванна»? Тоже мимо. Зато гораздо логичнее сказать: ванная комна¬ та содержит ванну. Логично ли сказать, что ванна — это ванная комната? Или что ванная комната — это ванна? Мне кажется, нет. Взаимосвязь между ванной комнатой и ванной выражается отношением ИА5-А. Ванная комна¬ та содержит ванну. Это означает, что у класса Bathroom есть переменная ‘ экземпляра Tub. Такая связь называется HAS-A (содержит). В этом случае класс Bathroom просто содер¬ жит переменную типа Tub. То есть он не расширяет Tub, а хранит ссылку на него. И это — хорошее проектирование. Bathroom Tub bathtub; Sink theSink; Tub int size; Bubbles b; Bubbles int radius; int colorAmt; Ванная комната содержит ванну, а ванна — пену (класс Bubbles). Но ни один из этих классов не наследует (расширяет) другой.
Но погодите! Это еще не все! Проверка на соответствие IS-А работает в любой части иерархии наследования. Если ваша иерархия спроектирована должным об¬ разом, то проверка на соответствие будет иметь смысл в контексте любого родительского класса и любых его подклассов. Если класс Б расширяет класс А, то класс Б соответствует классу А. Это утверждение истинно на любом участке ие¬ рархии наследования. Если класс В расширяет класс Б, то он проходит проверку на соответствие обоим классам — Б и А. Canine наследуется от Animal Wolf наследуется от Canine Wolf наследуется от Animal Canine — это (IS-A) Animal Wolf — это (IS-A) Canine Wolf — это (IS-A) Animal Canine roam() tt Wolf makeNolse() eat() m При такой иерархии наследования вы всегда можете сказать: «Wolf рас¬ ширяет Animal» или «Wolf — это Animal». И тут неважно, где именно находится Animal в иерархии — прямо над Wo 1 f или через несколько уровней выше. Главное, что Wolf — это Animal (в терминах IS-A). Иерархия говорит нам следующее: Wolf — это Canine, значит, он умеет все, что умеет Canine. Wolf — это также Animal, а значит, умеет все, что умеет Animal. А те¬ перь главный момент: совершенно не важно, переопределяет ли Wolf методы Animal или Canine. Код, который использует объект типа Wolf, просто видит, что у него есть ме¬ тоды: makeNoise (),eat() , sleep() и roam () . И все, что его волнует — это возможность вызывать эти методы. А как Wolf реализует их — по-своему или как унаследованные версии — во¬ обще не важно. Главное, что он их име¬ ет, потому что унаследовал от Animal.
Как узнать, что наследование оформлено правильно Конечно, эта тема гораздо обширнее, и мы рассмотрели ее далеко не всю. Подробнее про¬ блемы ООП будут раскрыты в следующей главе (где мы в итоге отшлифуем кое-что из спроекти¬ рованного в этой главе). Вбить в голову Параметры -передаются • I по значению! Помыть Потоки waitO nofifyO Розы — красные, фиалки — синие. Линия — объект, но не все объекты — линии. Розы — любовь, фиалки — изыск. Писк — это звук, но не каждый звук — писк. Теперь ваша очередь! Придумайте свой пример, который покажет, насколько односторонней бывает связь IS-А. Помните: если X расширяет Y, то выражение «X - это Y» (XIS-A Y) должно звучать логично. Старайтесь использовать проверку на соответ¬ ствие. Если X — это Y, значит, оба класса (X и Y), скорее всего, должны находиться в одной ие¬ рархии наследования. Вероятно, они ведут себя одинаково или дублируют функции друг друга. Не забывайте, что отношение IS-А работает только в одном направлении! Треугольник — это фигура. Выражение имеет смысл, поэтому треугольник является фигурой. Но если поменять их местами: фигура — это треугольник, то будет нелогично, поэтому класс Shape не должен расширять класс Triangle. Помните, отношение IS-А подразумевает, что если X соответствует Y, то X способен на все, что может делать Y (а иногда и больше). * Решать —вам! " ?<Ц^точите карандаш Отметьте отношения, которые имеют смысл. □ Плита — это кухня П Гитара это музыкальный инструмент □ Человек — это работник □ Феррари — это двигатель □ Яичница — это еда □ Бигль — это домашний питомец □ Контейнер — это банка □ Металл — это титан □ Grateful Dead — это музыкальная группа □ Блондинка — это ум □ Напиток — это мартини Подсказка: выполните проверку на соответствие отношениям IS-А.
(Не) дурацкие вопросы ВоИ рОС: Итак, мы видим, как дочерний класс наследует методы своего родителя. А если родитель¬ ский класс захочет использовать методы своего потомка? Просто потому, что в Java нет об¬ ратного (реверсивного) наследова¬ ния. Подумайте: в жизни ведь тоже только дети наследуют что-то от родителей, а не наоборот, правда? Ol/ивеип: Конечно, можно! И это не просто допустимо — это одна из мощных фишек объектно-ори¬ ентированного программирова¬ ния. Вспоминаем:ключевое слово extends читается как «я хочу расши¬ рить функциональность родителя». Никто не запрещает вам взять по¬ ведение родителя, добавить в него немного своего и получить то, что нужно. Как это выглядит на практике? Ис¬ пользуйте ключевое слово super. Оно как бы говорит: «Сначала вы¬ полните родительский метод, а по¬ том давай я продолжу с того места, где вы остановились». Вот пример: public void roam() { ^ super.roam(); //а теперь мой код Омвет: Родительский класс может даже не догадываться о су¬ ществовании своих потомков. Вы можете написать класс, который потом кто-нибудь расширит— без вашего ведома. Даже если вы как разработчик родителя знаете, что где-то там будет дочерний класс с интересным методом, вы не смо¬ жете использовать его из родителя. ЗОИ рОС: А если я хочу вы¬ зывать из дочернего класса не только переопределенный им метод, но и оригинальную версию из родителя? Ну, не полностью заменить поведение, а чуть его дополнить. U хшпг >ija гсй) ЗфП тсия бшиаиятшя mi, < ни, ic кс К fufwiiUj KJxicaj. нгщ-и/к^йннач отнмшидйся Кто получит «Порше», а кому — фарфоровую вазу? Или как понять, что именно наследует дочерний класс от своего родителя Когда один класс наследует другой, он получает доступ ко всем его чле¬ нам. Сейчас под членами мы имеем в виду переменные и методы (но позже узнаем, что это не все). При этом родительский класс сам решает, чем он готов поделиться с детьми. В этом ему помо¬ гает уровень доступа, присвоенный каждому методу и переменной. В Java предусмотрено четыре уровня доступа, пере¬ числим их от самого закрытого к самому открытому: private default protected public Чем больше вы знаете об уровнях доступа, тем легче создать надежный, стабильный и защищен¬ ный от ошибок код. Но не переживайте — пока сосредоточимся на двух самых простых: • public — все, что объявлено как public, можно унаследовать; • private — то, что объявлено как private, не унаследуется никогда. Когда потомок наследует метод родителя, это все равно что он объявляет этот метод у себя. Для остального кода это выглядит так, будто метод — часть самого дочернего класса. Пример: класс Square наследует методы rotated nplaySoundO от Shape. Снаружи никто и не догадается, что они пришли от родите¬ ля: кажется, будто Square всегда ими обладал. А теперь — сюрприз: члены класса — это не только переменные, но и все остальное, что вы можете объявить внутри класса. Но к этому мы еще вернемся. Л^исшшнмс: (шыш нHfjxjuum.uu ссгц[\с(?нях н f\uttctiJ mujmnxi (vTfyut.лстнин Ъ.
Наследование при проектирова¬ нии — хорошо или плохо? Вы сможете лучше проектировать свои приложения, зная некоторые правила. Пока просто примите их на веру; более подробно вы познакомитесь с ними позже в книге. Используйте наследование для класса, который является более специфичным видом родительского класса. Пример: Willow (ива) — это тип Tree (дерево), поэтому Willow на самом деле расширяет Tree. Используйте наследование, если у вас есть поведение (реа¬ лизованный код), которое должно быть общим для разных классов одного типа. Пример: Square (квадрат), Circle (круг) и Triangle (треугольник) должны поворачивать¬ ся и воспроизводить звук, поэтому есть смысл добавить такое поведение в родительский класс Shape (фигура). Это также улучшит поддержку и расширяемость кода. На¬ следование — одна из ключевых возможностей объектно- ориентированного программирования, но оно не всегда должно рассматриваться как наилучший способ повторно¬ го использования кода. Оно поможет вам на первых порах и часто будет правильным выбором при разработке, но паттерны проектирования предоставят более изысканные и гибкие варианты. Если вам не знакомы паттерны проек¬ тирования, рекомендуем обратить внимание на книгу Head First. Паттерны проектирования. Не используйте наследование только ради повторно¬ го использования кода, если при этом нарушаются два ключевых правила — IS-А и здравый смысл. Допустим, вы написали особый код для вывода неких данных в классе Animal, а потом вам понадобилось что-то вывести в клас¬ се Potato. И тут вы думаете: «Хм, а что, если Potato унаследует от Animal возможность вывода данных?». Стоп! Это безумие! Картошка — не животное! (В этом случае стоит вынести код, отвечающий за вывод данных, в отдельный класс Printer, которым можно будет воспользоваться через отношение HAS-A: «имеет функционал вывода», а не «является животным».) Не используйте наследование, если подкласс и суперкласс не проходят проверку IS A. Всегда спрашивайте себя: «Является ли подкласс более конкретной формой суперкласса?» Например: выражение «Чай — это напиток» имеет смысл, а «Напиток — это чай» — нет. — Без лишних слов ■ Подкласс расширяет суперкласс. ■ Подкласс наследует все публичные переменные экземпляра и методы су¬ перкласса, но не наследует приватные переменные и методы. ■ Унаследованные методы можно перео¬ пределять. Переменные экземпляра — нельзя (да, можно переопределить имя, но это уже другая переменная, и так почти никогда не стоит делать). ■ Всегда проводите проверку IS-A, чтобы убедиться, что иерархия насле¬ дования имеет смысл. Если X расширя¬ ет Y, то должно быть логично сказать: XIS-A Y. ■ Связь IS-А работает только в одну сторону: бегемот — это животное, но не каждое животное — бегемот. ■ Когда метод переопределен в подклас¬ се, и вы вызываете его у экземпляра подкласса — сработает версия из под¬ класса. (Самая «глубокая» побеждает!) ■ Если Б расширяет А, а В расширяет Б, то Б IS-А А, В IS-А Б, а В IS-А А. (На¬ следование идет цепочкой вверх.)
В чем же настоящая ценность наследования? С наследованием можно выжать из ООП по максимуму. Вы избавляетесь от повто¬ ряющегося кода, выделяя общее поведе¬ ние для группы классов и помещая его в родительский класс. В результате, если нужно внести изменения, это делается в одном-единственном месте — и, о чудо, все дочерние классы сразу начинают ис пользовать обновленную версию. Ну ладно, никакой магии здесь нет. Но все действи¬ тельно просто: внесли изменения, пере¬ компилировали родительский класс — и все. К дочерним классам можно даже не прикасаться. Просто замените старый родительский класс новой версией — и все наследники автоматически перейдут на обновлен¬ ный функционал. В конце концов, программа на Java — это просто куча классов. Поэтому дочерние классы не нуждаются в повторной компи¬ ляции, чтобы использовать новую версию родителя. Все работает, пока вы ничего не сломали. (О том, что именно значит «сломать», поговорим позже. Пока про¬ сто считайте, что это любые изменения в родительском классе, от которых за¬ висит дочерний, — например, изменение аргументов, возвращаемого значения или имени метода.) Вы избегаете дублирования кода Размещайте общий код в одном месте, и пусть подклассы наследуют его от роди¬ тельского класса. Когда захочется внести изменения в поведение, это нужно будет сделать только один раз — и все осталь¬ ные (то есть все дочерние классы) сразу их унаследуют. Вы определяете общий протокол для группы классов Наследование позволяет задать единый набор методов, который будет применяться ко всем классам этой группы.
Наследование гарантирует, что все классы-потомки содержат методы родительского типа* Другими словами, вы задаете общий протокол для набора классов, связанных наследованием. Определяя в родительском классе методы, которые могут быть унаследованы дочерними классами, вы сообщаете другому коду о существовании протокола, в котором говорится: «Все мои подтипы (то есть до¬ черние классы) могут делать это благодаря методам, которые выглядят вот так...» Если говорить кратко, вы заключаете контракт. Класс Animal устанавливает общий протокол для всех своих подтипов: Animal makeNoise() eat() sleep() roam() , cwWmnt.mf f каждое Устное цмшп f 'a,n, 'n ипОншшО. Это тают w Zukc сами. но w ш, акш u ни* afufutmnn и какие natcHiw cm h/fauum m. Запомните: когда мы говорим «каждое животное», то имеем в виду класс Animal илюбойдругой класс, ко¬ торый его наследует. Что также означает любой класс, относящийся к Animal в каком-либо участке иерархии наследования. Но самое интересное — полиморфизм — мы приберегли напоследок. Когда вы объявляете общий тип для группы классов, вы можете заменять родительский класс дочерним везде, где предполагается его присутствие. Простите, что? Не волнуйтесь, мы только начали объяснять. Через пару страниц вы во всем разберетесь. И меня это должно волновать, потому что... ...вы получаете преимуще¬ ства полиморфизма. Это важно для меня, потому что... ...вы получаете возможность ссылать¬ ся на объект дочернего типа через ссылку, объявленную с родительским типом. И это означает, что... .. .вы можете писать универсальный код. Более чистый, эффективный и простой. Такой код не только легче писать, но и гораздо проще расширять; причем такими способами, о которых вы в самом начале и подумать не могли. Это означает, что, пока вы отдыхаете на море, ваши коллеги могут обновлять программу, даже не заглядывая в исходники. Как это работает, вы увидите на следующей странице. Не знаем, как вы, но мы считаем, что перспектива отпуска на море очень вдохновляет. * Когда мы говорим «все методы», то имеем в виду «все насле¬ дуемые методы». На данном этапе это означает «все публичные методы», хотя позже мы еще уточним это определение.
Чтобы понять работу полиморфизма, нужно немного вернуться назад и вспомнить, как обычно объявляют ссылки и создают объекты... Три шага при объявлении и инициализации Ъод myDog = new Dog(); О Объявляем ссылочную переменную. Dog myDog = new Dog() ; Инструктируем JVM выделить место для ссылочной переменной. Ссылка навсегда получает тип Dog. Иными словами, создаем пульт дистанционно¬ го управления, у которого есть кнопки для работы с объектом Dog, но не Cat, Button или Socket. Создаем объект. Dog myDog = new Dog() ; Инструктируем JVM выделить в куче, управляемой сборщиком мусора, место для нового объекта Dog. Объект Dog О Связываем объект и ссылку. Dog myDog — new Dog(); Присваиваем новый объект Dog ссылочной переменной myDog. Другими словами, программируем пульт управления. Объект Dog Dog
Важно отметить, что типы ссылки и объекта совпадают. В этом примере оба имеют тип Dog. стжч нон т^и.шмнсм (шя&ан кanjDoq, а ссыкт cejcjOH как нс&шэк^имимр класса У)су. Однако полиморфизм допускает, что ссылка и объект могут иметь разные типы. Animal myDog = new Dog(); /\nimal \ Они u uiidii fuMmt miuui. Jim rmtomm mfuAuuncu ccm&itn как.Ап/ншС. нс ссъскпх. со/чан как нл)(?мм эюышлял классаЮса. яр
Благодаря полиморфизму тип ссылки может быть родительским для типа самого объекта. При объявлении ссылочной переменной ей можно присвоить любой объект, кото¬ рый проходит проверку на соответствие для типа этой ссылки. Иными словами, все, что расширяет тип объявленной ссылочной переменной, может быть ей присвоено. Это позволяет создавать такие объекты, как поли- морфные массивы. Хорошо, возможно, пример все прояснит mrma Aumaf. Animal[] animals = new Animal[5] animals[0] animals[1] animals[2] animals[3] animals[4] new Dog(); new Cat(); new Wolf(); new Hippo(); new Lion(); Л « of^K*i^*nwna Hi псс«сгп1»1ШЦ. wnc nf\tqcmciim.cM.ianu... (>n.mmmlcjotfifa mi бэтопхмассабли^й. nuj к иисЛмтаС! for (Animal animal : animals) animal.eat(); <Мтм<ш ш лко/а - У С>UuUiH'M’u * "f-utMajoff*.л,,- atj14^ up Акт аА™ше’ АшЛ Wn nfi«(<untct ()t urmtfui Ha nt/iftui ntxaxtqc. и,иклл animaf fu,fL JL lw nmxtqt амтах — это Oit и еаТ() <Цля Oil ^ 1/411 / и (jmciwSud animal. roam () ; Tcmc так же нсме mujc.u wamO. }
Но погодите! Это еще не все! У вас могут быть полиморфные аргументы и типы возвращаемых значений. Если вы можете объявить ссылочную пе¬ ременную родительского типа (например, Animal) и присвоить ей объект дочернего типа (скажем, Dog), то подумайте, как это может сработать, когда ссылка — это аргумент для метода... V % class Vet { public void giveShot(Animal a) { // делаем ужасные вещи с животным, // на которое указывает параметр 'а a.makeNoise (); > class PetOwner { public void start() { Vet vet = new Vet(); Dog dog = new Dog(); Hippo hippo = new Hippo() vet.giveShot(dog); vet.giveShot(hippo) ) Ларамапр алшжт npUHU.uami .aodoii тип живописи: главное. гтош сн ctu пс'ПишксмАпЛпаС Коша ветеринар раканшвает atоать икал, пн говорит veufi тисну: ни ка. псаай голос!» — и впшваотся. и (mm тактогжО. <Ко вот в пЛ (ришка: вптвашея и. шине та KipcuM Aumuja, to торам ссотвсштвист уактитшму тип] животного вкт. гак шка (jxiKxnuum как нсир*. (осака ((мпедняет у*ункм,мк. пшксАамО }
Вот такая она, сила полиморфизма — вы пишете код один раз, а он продолжает работать даже с новыми типами, которых вы не могли предусмотреть. Помните наш класс Vet? Если его методы используют параметр типа Animal, то Vet сможет работать с любым подклассом — Dog, Cat, Hippo, хоть Dinosaur. Главное, чтобы все они наследовали Animal. Так что если кто-то хочет, чтобы его новый класс подружился с Vet, — ему достаточно унаследовать Animal, и все будет работать как по волшебству. Есть ли гарантия, что полиморфизм всегда будет так работать? Почему можно утверждать, что любой дочерний класс будет содержать методы, которые, как вы предполагаете, вызываются из его родителя?
(Не) дурацкие вопросы Воп рОС: Есть лив Java какой- то предел глубины наследова¬ ния? Можно ли строить беско¬ нечно длинные иерархии? Ожвеж; Строгих ограничений нет, но... если вы посмотрите на Java API, то заметите, что большин¬ ство иерархий довольно «пло¬ ские». Как правило, это один-два уровня глубины. Исключения встречаются, особенно в GUI, где цепочки наследования могут быть длиннее. Но чем глубже иерар¬ хия, тем сложнее становится все контролировать. Так что лучше увеличиваться в ширину, а не в глубину. BoiApOC: Что если я не могу изменить существующий класс (например, нет доступа к исход¬ ному коду), но мне очень нужно переписать один из его методов? ВОИ рОС: А можно ли расши¬ рить вообще любой класс? Или бывают классы, которые «запре¬ щено» наследовать? Ож веж; Приватных классов, строго говоря, не бывает (если не считать вложенные, но об этом позже). Однако есть три способа запретить наследование: • Ограниченный доступ (default access) — если класс не public, его нельзя расширить за преде¬ лами пакета. ■ Ключевое слово final — говорит компилятору: «Все, дальше не ходить». Такой класс никто не сможет расширить. • Приватные конструкторы — де¬ лают невозможным создание дочернего класса, потому что его конструктор не сможет вызвать родительский. Ol/ИВвЖ: О, это один из козы¬ рей ООП! Вы можете наследовать «плохой» класс, переопределить нужный метод и использовать свою улучшенную версию. Не надо заново писать весь класс и не нужно искать того, кто потерял исходники. Вы просто берете и расширяете. Вопрос: А зачем вообще запрещать наследование? Что хорошего в финальных (final) классах? Ожвеж: В большинстве случаев вам это не потребуется. Но если вы хотите быть уверены, что никто и никогда не изменит поведение вашего класса, final — ваш выбор. Например, класс String финализирован, потому что нельзя допустить, чтобы кто-нибудь вме¬ шался в работу строк. Это было бы катастрофой! Вопрос: Можно ли финализи- ровать не весь класс, а только конкретный метод? Ожвеж: Да! Хотите защитить метод от переопределения? По¬ метьте его как final. А если финали- зировать весь класс, ни один его метод никогда не будет переопре¬ делен. Железная гарантия.
Заключение контракта: правила переопределения Переопределяя метод, принадлежащий родительскому классу, вы подписываете контракт. Например, в нем говорится: «Я не принимаю никаких аргументов и возвращаю логическое значение». Иными словами, аргументы и возвращае¬ мые значения переопределяемых методов внешне должны выглядеть точно так же, как в оригинальном методе родительского класса. Методы — это и есть контракт. Если учесть полиморфизм, то при выполнении программы будет работать переопределенная классом Toaster версия метода, которая унаследована от Appliance. Прежде чем решить, можете ли вы вызвать конкретный метод из ссылки, компилятор смотрит на ее тип. Appliance appliance = new Toaster(); Appliance boolean turnOn() boolean turnOff() Jiuvccft*** Jun мьекта. Если переменная Appliance ссылается на объект Toaster, то ком¬ пилятор смотрит, есть ли у класса Appliance метод, который вызыва¬ ется из ссылки типа Appliance. Но при выполнении программы JVM интересует не тип ссылки (Appliance), а сам объект Toaster, ^ lujU'Rju находящийся в куче. Поэтому, если компилятор уже одобрил tcRUtl вызов метода, переопределенный метод имеет те же аргументы lf' а и тип возвращаемого значения. Иначе кто-нибудь может вызвать ^цц\м из ссылки типа Appliance метод turnOn () без аргументов, хотя 'копщ»мм мрсы'}' в классе Toaster этот метод принимает целочисленное значение. ^ ддатхсл! Какой же метод будет вызван? Метод, который находится в Appliance. Таким образом, метод turnOn (int level) в классе Toaster не переопределен! 0 Аргументы должны совпадать, а типы возвра¬ щаемых значений должны быть совместимы. Контракт для родительского класса определяет, как остальной код сможет использовать метод. Что бы он ни принимал в качестве аргументов, дочерний класс, переопределяя его, должен приме¬ нять такие же аргументы. И что бы он ни возвращал, переопреде¬ ленный метод должен возвращать значение того же или дочернего типа. Помните: объект дочернего класса обязан уметь все, о чем объявил его родитель. Поэтому безопасно возвращать дочерний класс в ситуациях, когда ожидается класс-родитель. Toaster На ти сщ (Шоте Appliance public boolean turnOnQ public boolean turnOff() Метод не может быть менее доступным. Это означает, что уровень доступа должен быть таким же или менее строгим. Например, вы не можете переопреде¬ лить публичный метод, сделав его приватным. Представьте, какой сюрприз получит внешний код, вызывая, как он счита¬ ет на уровне компиляции, публичный метод, если внезапно во время выполнения программы JVM закроет дверь перед его носом, потому что вызванная переопределенная версия приватна! КСАЬЗЯ! Это имопистижос (uW-auj tmo (?н ихлшимм икс Toaster Пока мы изучили два уровня доступа: приватный (private) и публичный (public). Два других описываются в Приложении Б Есть еще одно правило переопределения, связанное с обра- т\ь uumuki. ижрстткл KMicmfcnuum, iu.nu.uij mo (?м ш цшши. ли а\Щ rtrumn mtnujii
Перегрузка метода Перегрузка метода — это наличие нескольких методов с одним именем, но разными наборами аргументов. И все. В процессе перегрузки методов полиморфизм не задействуется! Перегрузка позволяет создавать несколько версий одного метода с разным списком аргументов, делая их более удобными для вызова. Например, если у вас есть метод, который принимает только тип int, то перед вызовом этого метода код должен преобразовать, скажем, double в int. Перегрузив метод и добавив новую версию, принимающую double, вы упростите его вызов для внешнего кода. Подробнее об этом мы поговорим, когда дойдем до конструкторов в главе 9, посвященной жизненному циклу объекта. Хотя перегруженные методы и не пытаются соблю¬ дать контракт для выполнения условий полиморфиз¬ ма, они обладают гораздо большей гибкостью. Перегруженный метод — просто другой метод с тем же именем. Это не имеет ничего общего ни с наследованием, ни с полиморфизмом. Пере¬ груженный метод - не то же самое, что пере¬ определенный. ® Типы возвращаемых значений могут быть разными. Вы можете изменять типы возвращаемых значений в перегруженных методах. При этом список аргументов тоже должен отличаться. ® Вы не можете изменить ТОЛЬКО тип возвращаемого значения. Если изменить лишь тип возвращаемого значения, это будет некорректная перегрузка: компилятор решит, что вы пытаетесь пере¬ определить метод. Кроме того, это считается недопустимым, так как типы возвращаемых значений в дочернем классе и его родителе должны совпадать. Чтобы перегрузить метод, НУЖНО изменить набор аргументов. При этом можно заменить тип возвращаемого значения любым другим. (D Вы можете как угодно изменять уровни доступа. При перегрузке есть возможность добавлять новые версии метода с более ограниченным доступом. Это не имеет значения, так как новый метод не обязан выполнять контракт перегружаемого метода. Пример корректной перегрузки метода public class Overloads { String uniquelD; public int addNums(int a, int b) { return a + b; } public double addNums(double a, double b) { return a + b; } public void setUniguelD(String thelD) ( // проверяпций код, а затем: uniquelD = thelD; i public void setUniquelD(int ssNumber) { String numString = "" + ssNumber; setUniquelD(numString); )
Jfc. штраж \ражнение Программа: резильтаты Краткая программа на Java приведена ниже. Один блок программы отсутствует! Ваша за¬ дача — сопоставить блок кода-кандидата (слева) с выводом, который вы бы увидели, если бы этот блок был вставлен. Не все строки вывода будут использованы, а некоторые из них могут быть использованы несколько раз. Проведите линии, соединяющие блоки кода-кандидата с соответ¬ ствующим им выводом командной строки. class А { int ivar = 7; void ml () { System.out.print("метод ml класса A, "); } void m2 () ( System.out.print("метод m2 класса A, "); \ void m3 () { System.out.print("MeTOfl m3 класса A, "); } class В extends A ( void ml() { System.out.print("метод ml класса В, "); ) ) ) ) class C extends В { void m3 () { System.out.print("метод m3 класса С, " + (ivar + 6)); ) public class Mixed2 ( public static void main(String!] args) { A a = new A() ; В b = new B(); С c = new C() ; A a2 = new C(); Сюда вставляют¬ ся варианты кода (три строки) Варианты кода: b. mi{); \ c. m2(); j а.m3 () ; с.ml() ; 1 с.m2() ; J с.m3 () ; a. ml () ; 1 b. m2() ; J c. m3 () ; а2.ml(); 1 а2.m2() ; J а2. m3 () ; Результат: метод ml класса А, метод m2 класса А, метод m3 класса с, 6 метод ml класса в, метод m2 класса А, метод m3 класса А, метод ml класса А, метод m2 класса В, метод m3 класса А, метод ml класса в, метод m2 класса А, метод m3 класса С, 13 метод ml класса в, метод m2 класса С, метод m3 класса А, метод ml класса в, метод m2 класса А, метод m3 класса С, 6 метод ml класса А, метод m2 класса А, метод m3 класса с, 13 Ответы на стр. 233
Компилируйте Какие пары методоВ (А — 3) при доВаВлении Ь класс, приве¬ денный слеВа. позВолят скомпилировать программу и Вы¬ ведут на экран предстаВленный текст? Метод А поме¬ щается В класс Monster, а метод 3 - В класс Vampire. public class MonsterTestDrive { public static void main(String[] args) { Monster[] monsters = new Monster[3]; monsters[0] = new Vampire(); monsters[1] = new Dragon(); monsters[2] = new Monster(); for (int i = 0; i < monsters.length; i++) ( monsters[i].frighten(i); } } ) 2 boolean frighten(int d) { System.out.println("apppp"); w return true; } boolean frighten(int x) { Q System.out.println("укусить?"); return false; ) 2 boolean frighten(int x) { Q System.out.println("apppp”); return true; } class Monster { О } class Vampire extends Monster { О } class Dragon extends Monster { boolean frighten(int degree) ( System.out.println("пыхнуть огнем"); return true; File Edit Window Help SaveYoureelf % java MonsterTestDrive укусить? пыхнуть огней apppp int frighten(int f) { W System.out.println("yKycMTb?") return 1; 3 boolean frighten(int x) { q System.out.println("apppp"); return false; } boolean scare(int x) { Q System.out.println("укусить?"); return true; } 4 boolean frighten(int z) { ф System.out.println("apppp"); return true; } ф>оо1еап frighten (byte b) { System.out.println("укусить?"); return true; ) *■ Ответы на стр. 233
бассейн фрагментов public class Rowboat public rowTheBoatO ( System.out.print("разбудить капитана"), ( ) public class _ private int void ) ( length = len; } public int getLengthO 1 У public move () ( System.out.print ("_ Ваша задача — брать фрагменты кода из бассейна и помещать их в пустые строки кода. Некоторые фрагменты не понадо¬ бятся, а некоторые можно использовать больше одного раза. Ваша цель — создать набор классов, которые будут компили¬ роваться и работать как единая программа. Не обольщайтесь: это сложнее, чем кажется. public class TestBoats { main(String[] args)( bl = new Boat(); Sailboat b2 = new (); Rowboat = new Rowboat ( b2.setLength(32); bl. 0; b3. (); .move(); public class public 0 { System.out.print("_ Boat ") ; Результат: дрейф дрейф поднять паруса Ответы на стр. 234
Ответы Компилируйте (соар.231) Набор 1 будет работать. Набор 2 не скомпилируется из-за типа возвращаемого значения в классе Vampire (int). Метод frighten() класса Vampire в варианте Б некорректно пере¬ определяет или перегружает свой аналог из класса Monster. Недостаточно изменить тип возвращаемого значения, чтобы получить корректную перегрузку. Поскольку тип int несовместим с boolean, это нельзя считать правильным переопределением. Если вы изменяете только тип возвращаемого значения, то он должен быть совместим с аналогичным типом из родительского класса — тогда это будет переопределение. Наборы 3 и 4 скомпилируются, но выведут на экран: арррр пыхнуть огнен арррр Помните, что класс Vampire не переопределяет метод frighten() из класса Monster. Метод frighten() в классе Vampire из четвертого набора принимает значение типа byte, а не int. Противоречивые результаты Варианты (со стр. 232) кода: b. ml О с .m2 () а . m3 () с . ml () с .m2 () c. m3() а . m 1 () ; b. m2 () c. m3 () a2.ml(); a2.m2() а2.m3() 1 Результат: метод ml класса А, метод m2 класса А, метод m3 класса с, 6 метод ml класса в, метод m2 класса А, метод m3 класса А, метод ml класса А, метод m2 класса в, метод m3 класса А, метод ml класса в, метод m2 класса А, метод m3 класса с, 13 метод ml класса в, метод m2 класса с, метод m3 класса А, метод ml класса В, метод m2 класса А, метод m3 класса С, 6 метод ml класса А, метод m2 класса А, метод m3 класса С, 13
рассеве фрагментов (со стр. 232) public class Rowboat extends Boat { public void rowTheBoat() { System.out.print("разбудить капитана") ) } public class Boat { private int length ; public void setLength ( intlen ) { length = len; } public int getLength() { return length public void move() { System.out.print( "дрейф " ) ; ) public class TestBoats { public Static void main(String[] args) { Boat Ы = new Boat () ; Sailboat b2 = new Sailboat () ; Rowboat ьз = new Rowboat(); b2 . setLength(32); bl.movet) ; b3.move<) ; b2 .move(); } ) public class Sailboat extends Boat { public void move () { System.out.print ("поднять паруса ") ; 1 Результат: дрейф дрейф поднять паруса
8. Интерфейсы и абстрактные классы Серьезный полиморфизм Наследование — ЭТО ТОЛЬКО начало. Если вы хотите по-настоящему прокачать свою архитек¬ туру, пора выходить за рамки обычного наследования. Добро пожаловать в мир интерфейсов. И нет, это не кнопки и окна для пользователя. Мы говоримо настоящих интерфейсах, тех, что делают Java мощной, гибкой и масштабируемой. Забудьте на минуту про простое «расширение». Чтобы по-настоящему раскрыть потенци¬ ал полиморфизма, одной иерархии классов недостаточно. Вам нужна архитектура,основанная на интерфей¬ сах. Только с их помощью можно создавать код, который легко тестируется, расширяется и адаптируется под все, что может подкинуть жизнь (и заказчик). И если вы сейчас думаете: «Да я никогда не использую интерфей¬ сы» — поверьте, все впереди. Потому что Java построена вокруг них. Даже если вы еще не писали интерфейсы сами, скорее всего, вы уже с ними сталкивались. Вы только представьте — вы еще не знаете, как они работа¬ ют^ уже пользуетесь их суперспособностями. И как только вы вникнете, вы просто не сможете представить себе Java без них. Что же такое интерфейс? Это на 100% абстрактный класс. А что такое абстрактный класс? Это такой класс, экземпляр которого нельзя создать. Зачем вообще такие классы нужны, если их даже нельзя инстанцировать? Спокойно. Скоро все станет ясно. Если в конце предыдущей главы вы кайфанули от того, как метод giveShotO из класса Vet принимал любые подтипы Animal, значит, вы уже начали чувствовать силу полиморфизма. А теперь представьте, что эта сила стала в десять раз мощнее. Потому что интерфейсы — это и есть «поли» в слове «полиморфизм», «аб» в слове «абстрактный» и весь «кофеин» в Java. И поверьте, с ними код начинает буквально летать.
Мы что-то забыли, когда разрабатывали это? Структура класса не так уж плоха. Разрабатывая ее, мы старались свести к минимуму дублирование кода и замени¬ ли методы, которые должны получать реализации, специ¬ фические для каждого дочернего класса. С точки зрения полиморфизма мы сделали код красивым и гибким. Теперь мы можем разрабатывать программы, использу¬ ющие класс Animal в качестве аргументов (и при объ¬ явлении массивов), так что любой его подтип, включая те, о которых мы и не мечтали, когда писали свой код, может быть передан и использован при выполнении программы. Для всех классов Animal мы предусмотрели в родитель¬ ском классе стандартный протокол (четыре метода, кото¬ рыми должны обладать все животные) и уже готовы начать создавать новые экземпляры Lion, Tiger и Hippo. Animal picture food hunger boundaries location makeNoise() eat() sleep() roam()
Мы знаем, что можем сказать: Wolf aWolf = new Wolf(); Эти a da шлжта щшж типл. А еще: Animal aHippo = new Hippo(); Но вот что странно: Animal anim = new Animal();
Какие значения хранит переменная экземпляра? Для некоторых классов просто не следует создавать экземпляры! Конечно, вы можете создать new Wolf (), new Hippo () или даже new Tiger () —и это имеет смысл. Они настоящие, конкретные. У каждого своя форма, голос, вес, хвост и характер. А вот что такое new Animal () ? Какого он размера? Цвета? Сколь¬ ко у него лап? Издает ли он звук, похожий на лай, мяв, «хрю» или что-то среднее между ними?.. Создание объекта Animal — все равно что теле¬ портация в стиле Star Trek”, где нечто пошло не так. В результате: ни животного, ни телепортации, только ошметки в буфере передачи. Б-р-р-р. Но зачем тогда вообще нужен Animal? А вот тут начинается магия наследования и полиморфизма! Нам нужен класс Animal, чтобы: другие классы могли от него наследовать; он мог быть типом переменной, которая будет ссылаться на любую «животную» сущность; мы могли создавать поли¬ морфные массивы. Но при этом нам совершенно не нужно, чтобы кто-то напрямую создавал Animal. Мы не хотим видеть в программе безымянных мутантов! Нам нужны конкретные животные, типа Tiger, Lion, Bear, а не объекты Animal. На этот случай есть способ не дать никому создать объект определенного класса — точнее, остановить любого, кто попытается написать new для этого типа. Нужно просто пометить класс ключевым сло¬ вом abstract, и компилятор не позволит создать его экземпляр в любом месте вашего кода. Такой абстрактный тип по-прежнему можно ис¬ пользовать в качестве ссылочного типа. Но тут возникает закономерный вопрос — зачем вообще нужен абстрактный класс? Например, для исполь¬ зования его как полиморфического аргумента, возвращаемого значения или элемента массива с возможностью полиморфного поведения. Когда вы проектируете иерархию классов, важно заранее решить, какие классы будут абстрактны¬ ми, а какие — конкретными. Конкретный класс должен быть достаточно определенным, чтобы было логично создавать его объекты. Когда вы делаете класс конкретным, вы говорите: «Да, объекты этого типа — вполне реальная вещь, можно создавать!» А сделать класс абстрактным проще простого — до¬ бавьте ключевое слово abstract перед его объяв¬ лением: abstract class Canine extends Animal { public void roam() { } )
Компилятор но позволит вам создать экземпляр для абстрактного класса Абстрактный класс это способ сказать: «Этот тип слишком обобщенный, чтобы из него делать объекты». Никто (включая вас!) никогда не сможет создать экземпляр такого класса. Но не переживайте: ссылаться на него по-прежнему можно. Вы сможете использовать аб¬ страктный тип как полиморфический аргумент, возвращаемое значение или элемент массива. Просто никто не сможет написать new с этим типом, и компилятор строго за этим проследит. abstract public class Canine extends Animal { public void roam() { } } public class MakeCanine { public void go() | Canine c; / c = new Dog();) c = new Canine () ; c.roam(); .xnuc&umic, ,tM ZU cf.u* «lA* «гня- | File Edit Window Help BeamMeUp % javac MakeCanine.java MakeCanine.java:5: Canine is abstract; cannot be instantiated c = new Canine () ; A 1 error Абстрактный класс сам по себе почти бесполезен . Без наследников у него нет ни смысла, ни цели, ни радости в жизни. Пользу от него можно получить только тогда, когда кто-то его рас¬ ширяет. Во время выполнения за всю работу отвечают экземпляры дочерних классов вашего абстракт¬ ного суперкласса. * Да, есть исключение — абстрактный класс может содержать статические члены. Подробнее об этом — в 10 главе.
Различия абстрактных и конкретных классов Если класс не абстрактный, то он называется конкретным. Рассмотрим дерево наследования Animal. Если мы сделаем классы Animal, Canine и Feline абстрактными, то дочерние классы Hippo, Wolf, Dog, Tiger, Lion и Cat все равно останутся конкретными подклассами. Пробегитесь по Java API, и вы найдете там мно¬ го абстрактных классов, особенно в библиотеке для создания GUI. Как выглядит GUI-класс Component? Он базовый для связанных с GUI классов: для таких элементов, как кнопки, поля ввода, полосы прокрутки, диалоговые окна и т. п. Вы не создаете экземпляр общего класса Component и затем помещаете его на экран, а создаете, например, объект JButton. Други¬ ми словами, вы создаете экземпляр только для конкретных наследников класса Component, но не для него самого. KcHKfXtrr AdcmhOK пиши AdcmjWK! пиша Ктк^хиттк KoRKfvmmviA кк^иишй 1 Wolf i Абстрактный или конкретный? Как вы узнаете, когда классу надлежит быть абстрактным? Напиток — это, вероятно, абстрактное понятие. А если мы говорим о чае и кофе? Возможно, это тоже абстракция (по крайней мере, для некоторых). Но при каких условиях объекты иерархии действительно становятся конкретными? Каким вы сделаете класс зеленый чай — кон¬ кретным или абстрактным? Похоже, что зеле¬ ный чай с бергамотом — конкретный экзем¬ пляр в любом случае. Но как узнать наверняка? Посмотрите на дерево наследования Animal. Правильно ли мы выбрали, кого сделать аб¬ страктным, а кого — конкретным? Хотите ли вы изменить что-нибудь в дереве наследования (не считая, конечно, добавления новых видов Animal)?
Абстрактные методы Кроме классов абстрактными могут быть и методы. Если класс аб¬ страктный, значит, его необходимо расширить; если же абстрактным является метод, он должен быть переопределен. Можно подумать, что поведение абстрактного класса (полностью или частично) не имеет смысла, если оно не реализовано более специфическими подкласса¬ ми. Другими словами, не получится представить реализацию общего метода, которая может оказаться полезной для дочерних классов. Как бы выглядел общий метод eat () ? У абстрактных методов нет тела! Поскольку в абстрактном методе нет кода, который имел бы смысл, то не нужно добавлять тело метода. Поэтому нет и фигурных скобок — просто закрываем объявление точкой с запятой. void eat(); nvc/Ki w Если вы объявляете абстрактный метод, то должны сде¬ лать абстрактным и его класс. Нельзя иметь абстракт¬ ный метод в неабстрактном классе. Даже помещая один абстрактный метод в класс, вы обязаны сделать этот класс абстрактным. Однако можно совмещать как абстрактные, так и неабстрактные методы в абстрактном классе. I) .литиК* коп с j»m\A»ncH public abstract (Не) дурацкие вопросы ЗоирОС: Каково предназначение абстрактного метода? Разве смысл наследуемых методов не в общем коде, который можно передать дочерним классам? О i/ивеж: Все правильно — если у метода есть тело, и если этот код действительно универсален для всех подклассов. Но иногда вы просто не можете написать полезную реализацию для метода в родительском классе. В абстрактных классах так бывает довольно часто. В таких случаях вы не пишете код, но все равно объявляете метод, тем самым определяя протокол — набор методов, которым должен следовать каждый подкласс. ЗОИ рОС: И это хорошо, потому что?.. Oi/ивеж.- Потому что полиморфизм! Напомним: вы хотите иметь возможность использовать родительский тип (часто абстрактный) в качестве параметра метода, типа возвращаемого значения или элемента массива. Так вы создаете возможность добавлять новые под¬ классы без переписывания кода. Вспомните наш класс Vet. Если бы он не принимал Animal в качестве аргумента, то пришлось бы созда¬ вать отдельный метод для каждого подкласса: один для Lion, другой для Wolf, третий для Tiger... Но благо¬ даря абстрактному методу вы как бы говорите: «Все, кто хочет быть Animal, обязаны уметь делать вот это». И теперь любой класс, который расширяет Animal, встраивается в существующую систему без лишних хлопот. Полиморфизм в действии.
Вы обязаны реализовывать все абстрактные методы У меня потрясающие но¬ вости, мама. Джо наконец-то реализовал все свои абстракт ные методы! И теперь все ра¬ ботает так, как мы плани- ^ ровали.. Реализация абстрактного метода происходит также, как обычное переопределение Абстрактные методы не имеют тела; они существуют только в рамках полиморфизма. Это значит, что первый конкретный класс в дереве наследования должен реализовать все абстрактные методы. Однако вы можете переложить ответственность на кого-то еще, если сами абстрактны. Например, если оба класса, Animal и Canine, абстрактны и содержат абстрактные методы, то класс Canine не обязан реализовывать методы класса Animal. Но как только мы доберемся до первого конкретного подкласса — например, Dog, — этот подкласс должен будет реализовать все абстрактные методы из обоих классов Animal и Canine. Однако не забывайте, что абстрактный класс может включать как абстрактные, так и неабстрактные методы. Поэтому, например, Canine мог бы реализовать абстрактные методы Animal, чтобы этого не пришлось делать подклассу Dog. Но если Canine не скажет ничего про абстрактные методы Animal, то Dog должен будет реализовать их все. Когда мы говорим: «Вы должны реализовать абстрактный метод», это означает, что вы должны дать методу тело. Иными словами, нужно создать неабстрактный метод в классе с такой же сигнатурой метода (именем и аргументами) и возвращаемым типом, совместимым с объявленным возвращаемым типом абстрактного метода. Что вы поме¬ стите в этот метод, зависит от вас. Java волнует только то, что метод находится в вашем конкретном дочернем классе.
#^Заточите карандаш *■ Решать — вам! Абстрактные и конкретные классы Найдем этим абстрактным словам конкретное применение. В центральном столбце мы поместили несколько классов. Ваша задача — придумать такие приложения, в части которых эти классы могли бы стать конкретными, а в части других — абстрактными. Мы уже привели пару примеров, чтобы вам было проще начать. Например, классТгее был бы абстрактным в про¬ грамме лесного питомника, где разница между дубом и осиной очевидна. Однако в симуляторе гольфа Tree может быть конкретным классом (воз¬ можно, даже наследником класса Obstacle): в этой программе не важно, чем отличаются разные типы деревьев. Здесь нет правильных ответов; все зависит от структуры вашей программы. Конкретный Пример класса Абстрактный Симулятор гольфа Приложение для спутниковых фото Tree Приложение для питомника растений House Архитектурное приложение Town Football Player Приложение для коучинга Chair Customer Sales Order Book Store Supplier Golf Club Carburetor Oven
Полиморфизм в действии А если мы хотим написать свой класс списка, который будет содержать объ¬ екты Dog, но ничего не знаем о классе ArrayList? На первом этапе мы соз¬ дадим для него метод add (). Мы используем простой массив Dog (Dog [ ]) для хранения добавленных объектов Dog длиной 5. Добавив пять объ¬ ектов, мы по-прежнему сможем вызывать метод add (), но безрезультатно. Если предел еще не достигнут, то метод add () поместит Doge следующую доступную ячейку массива, а затем инкрементирует значение следующего доступного индекса (next Index). Создание списка собак (Возможно, это худший в мире способ создания собственного класса вроде ArrayList.) public class MyDogList { private Dog[] dogs = new Dog[5], MyDogList (>миш<?сктпч DogO dogs int nextlndex i. ottiKii add(Dog d) public void add(Dog d) { if (nextlndex < dogs.length) dogs [nextlndex] = d; ( И л J System.out.println("Пес добавлен в " + nextlndex); } ) )
Эх! Теперь нам нужно хранить еще и кошек Здесь есть три варианта. 1. Создаем отдельный класс MyCatList для хранения объектов Cat. Это не очень грамотно. 2. Создаем единый класс DogAndCatLi st, который будет содержать два разных масси¬ ва в виде переменных экземпляра и иметь два разных метода add () : addCat (Cat с) и addDog (Dog d). Еще одно неудачное решение. 3. Создаем разнотипный класс AnimalList, который будет принимать любой тип подкласса Animal (если нужно менять спецификацию при добавлении объектов Cat, то рано или позд¬ но придется добавлять другое животное). Нам больше всего нравится такой вариант, поэтому изменим наш класс и сделаем его более универсальным, чтобы он мог принимать объекты Animal, а не только Dog. Мы выделили ключевые изменения (логика осталась той же, однако тип изменился с Dog на Animal во всем коде). MyAnimalList AnimalQ animals int nextlndex add(Animal a) Создание списка животных public class MyAnimalList { private Animal[] animals = new Animal[5]; private int nextlndex = 0; public void add(Animal a) { if (nextlndex < animals.length) { animals[nextlndex] = a; System.out.println("Животное добавлено в nextlndex++; ;(t luiHUKUwmt.. A tt Ht саюаик iu)0HU mi cwaa- tiK ufljfriii .иаеспв c/uxmctf mUna Animat (JkuHumt: (sti w. ,m- жшс fCAjmru rc(?mu ЭКЛ.ЛШ.Ш|Т affmhciKnmuc пиша, нолшитм плмт массчв, ьсд.чйпанпя (j ta лйнгкил этсгс типа.) nextindex). ) public class AnimalTestDrive { public static void main(String[] args) { MyAnimalList list = new MyAnimalList(); Dog dog = new Dog(); Cat cat = new Cat(); list.add(dog); list.add(cat); ) } | File Edi^^tndow Help Harm 1 % java AnimalTestDrive Животное добавлено в 0 Животное добавлено в 1
А как быть с неживотными? Вот бы сделать универсальный класс, принимающий любые объекты... Вы уже догадываетесь, к чему все идет. Мы хотим изменить тип массива и аргумент метода add () на что-то более общее, чем Animal. Что-то еще более универсальное, абстрактное, чем Animal. Но как это сделать, если у Animal нет своего суперкласса? Каждый класс в Java наследует класс Object. Абсолютно каждый. Класс Ob j ect — прародитель всех классов; это родительский класс для всего. Даже если вы используете полиморфизм, нужно создавать класс с методами, которые будут принимать и возвращать ваш полиморфический тип. Без общего родительского класса для всего, что есть в языке Java, разработчики не могли бы созда¬ вать классы с методами, принимающими ваши пользователь¬ ские типы — типы, о которых они и не знали, когда писали класс ArrayList. Раньше вы создавали подклассы класса Ob j ect с самого на¬ чала и даже не знали об этом. Каждый ваш класс расширяет Object и даже не упоминает об этом. Так что ваш класс вы¬ глядит так; public class Dog extends Object { } Погодите, Dog уже кое-что расширяет — класс Canine. Все правильно. Компилятор сделает так, что Canine будет рас¬ ширять Object. Кроме того, класс Canine унаследован от Animal. Никаких проблем: компилятор просто сделает так, что Animal будет расширять класс Object. КшсАыпшС не ш и ш икчш н тш л(?ас. поэтами он гФталитишекц я&ытся. пш- килаан (Jdjtcl. Любой класс, который явно не расширяет другой класс, на самом деле неявно унаследован от Object. По этой причине, поскольку Dog — потомок класса Canine, он не будет напрямую расширять Ob j ect (хотя он делает это косвенно). Это же верно для Canine. А вот Animal будет унаследован от Object напрямую.
Что же спрятано внутри этого ультра-супер-мегакласса Object? Ну, представьте, что вы — язык Java. У вас миллионы классов. Что каждому объекту может пригодиться в жизни? Какие универсальные инструменты стоит выдать каждому классу по умолчанию? Как на¬ счет метода для сравнения объектов на равенство? Или метода для получения информации о клас¬ се объекта? Может быть, метод для вычисления хэш-кода объекта, чтобы использовать его в хэш- таблицах (мы поговорим о них позже). О, вот то, что нужно, — метод, возвращающий строковое пред¬ ставление объекта. И угадайте что? Все это есть в классе Ob j ect. Да, это не весь его арсенал, но именно эти методы — настоя¬ щие супергерои повседневной объектной жизни. э^гоАьташсте .метены каосс iBauiil классы a miu,/uu С'ц (Г) equals(Object о) Dog а = new Dog(); Cat с = new Cat(); © if (a.equals(c)) { System.out.println("истина"); } else { System.out.printlnC'noxcb") ; } Г^|ё""Ё^Г^1п^о^""нё)р*^!ор1 % java TestObject ложь 6Wuv«m- ctwna Kttu-Я. ш fa ccmkhuj «цлСкнии» hashCodeO Cat c = new Cat(); System.out.println(c.hashCode())■ /ЗмбЦит хэшгксу алм cJttKfiui (ни аамшк ис.иснт (цшийтс с ны* кик ыГиишаам- нсж uutHmiwjbuKomc^). % java TestObject 8202111 (4) toStringO (2) getClassO Cat c = new Cat(); System.out.println(c.getClass()); File Edit Window Help Faint % java TestObject /ЗсуЙишулт кдоге, ю ко- mmc n n\. \ со?яан остакт. Cat с = new Cat(); System.out.println(c.toStringO); ВИШШДД % java TestObject Cat@7d277f багиуит. сптантм м. fa с^ектд «jtaftuwui» class Cat
(Не) дурщте вопросы ^ОПрОС: Класс Object — аб¬ страктный? Ом&ет: Нет. По крайней мере, в смысле языка Java — точно нет. Object не абстрактен, потому что у него есть полно¬ ценная реализация методов, которыми могут пользоваться все классы. Их даже не обя¬ зательно переопределять — можно взять как есть и спокой¬ но использовать. Зои рОС: А можно ли пере¬ определять методы класса Object? Ом&ет: Некоторые — да. Но не все. Есть методы, по¬ меченные как final, и менять их запрещено. Например, getClassO — его логика должна быть именно такой, как за¬ думано в Java. А вот такие методы, как hashCodeO, equalsO и toStringO, очень даже стоит переопределить. Чуть позже вы узнаете, как и зачем. ЗоИрОС: Серьезно? Можно создать объект типа Object? Это же как завести просто... объект. Разве это не так же странно, как создавать Animal? Ом&ет: Хороший вопрос! Почему вообще допустимо писать new ObjectO? Все просто — ино¬ гда вам нужен простой, ничего не значащий объект. Такой, чтобы использовать его как заглушку, якорь, или что-то легковесное. Да, это случается нечасто, и вам вряд ли придется делать такое в обыч¬ ной жизни. Но Java позволяет — и у этого есть свои причины, особенно когда мы говорим про многопоточность. ЗоИрОС: Можно сказать, что главное предназначение Object — быть универсальным типом для аргументов и возвра¬ щаемых значений? Ом&ет Да! Именно это и есть одна из двух ключевых ролей Object: быть универсальным поли¬ морфным типом — чтобы методы могли принимать и возвращать что угодно, не зная об этом на¬ перед. Предоставлять реальный работающий код методов, кото¬ рые нужны буквально всем объ¬ ектам в Java. И если эти методы живут в классе Object, значит, их наследуют все классы. Некоторые из таких методов особенно важны при работе с потоками выполне¬ ния — но до них мы еще дойдем. Зои рос: Если это так круто — ис¬ пользовать полиморфные типы — почему бы тогда не делать ВСЕ методы с аргументами и возвращае¬ мыми значениями типа Object? Ом&ет: Ах да... подумайте, что произойдет, если бы вы все писали через Object. Для начала — вы бы полностью разрушили смысл типобе- зопасности, одного из самых мощных механизмов защиты в Java. Типобезо- пасность гарантирует, что вы не попы¬ таетесь сделать что-то неподходящее с неподходящим объектом. Например, не скажете Ferrari (которую по ошибке приняли за тостер) поджарить себе бампер. Но, честно говоря, вам не нужно бо¬ яться огненного Ferrari, даже если вы используете ссылки типа Object для всего. Почему? Потому что, если объ¬ ект ссылается через переменную типа Object, Java считает, что это действи¬ тельно объект типа Object, а значит, вы можете вызывать на нем только те методы, которые есть у класса Object! Так что если вы напишете:. Object о = new Ferrari(); o.goFast(); Ошибка компиляции! Потому что Java — язык со строгой типизацией, компилятор всегда про¬ веряет, можно ли вызвать метод на объекте, который реально способен на него ответить. Другими словами, вы можете вызвать метод на объекте только в том случае, если класс ссы¬ лочного типа действительно содержит этот метод. Не переживайте, если пока все кажется немного туманным: совсем скоро мы разложим это по по¬ лочкам и разберемся во всех деталях.
Использование полиморфических ссылок типа Object имеет свою цену. Пока вы не забежали вперед и не начали использовать тип Object для всех своих ультрагибкихаргументов и возвращаемых типов, вам следует узнать кое-что о применении типа Object в качестве ссылки. Имейте в виду: мы говорим не о создании экземпляров типа Ob j ect, а о создании экземпляров других типов, но с использованием ссылки типа Object. Когда вы добавляете объект в ArrayList<Dog>, то он помещается как Dog и выходит оттуда как Dog. _(csckitu./Inaif/jit. ArrayList<Dog> myDogArrayList = new ArrayList<Dog> () ; < „Плитки HU Xfunv~ Dog aDog = new Dog () ; CcfiacM сткпЩ Д «йктс^мищЩ myDogArrayList. add (aDog) ; £ в ^ ''r tn стеки hc&m. tuJu.iUHHtit milha Dog d = myDogArrayList. get (0) ; <— /)и/. (ЯрЩставит, чтоили ( ~ j J\r,aUjs\^)ap.) muLjDof ncmcuHf tmr fo ш'ПыуШ* А что происходит, если вы объявляете его как ArrayList<Ob j ect>? Если вы хотите создать список, кото¬ рый будет вмещать объекты любого типа, то нужно объявить его следующим образом: CcxuiLiiAtmiftM, кс ArrayList<Object> myDogArrayList = new At:гдуL1 st<ObjectXj ; Dog aDog = new Dog(); ^ " (ucMtu се/икт nuuvi fjat myDogArrayList.add(aDog) t4<Ltt ( CH Kin У 4 if (s’ ( I (CTtKIlVM. (Ути iijGa шша iiukiu. же. кик CntatttjH(.u lyuuu^uj А что происходит, если вы пытаетесь получить объект типа Dog и присвоить его переменной ссылки типа Dog? Dog d = myDogArrayList.get(0) . и/ширггигип nw\'l.l/rv.V, ИЩ ( /?.•«■# /7 нссн неpauh, тени это 1шшнс/лу Все, что выходит из ArrayL±st<Object>, является ссылкой типа Object, независимо от того, чем на самом деле является объект или каким был тип ссылки, когда вы добавляли объект объект в список. Объекты ПО¬ СТУПАЮТ как SoccerBall, Fish, Guitar и Car. Но ВОЗВРАЩА¬ ЮТСЯ — как будто они просто объекты типа Object. ArrayList<Object> 1111 Когда вы доста¬ ете элементы из ArrayList<Object>, они ведут себя как обобщенные экзем¬ пляры класса Object. Компилятор не мо¬ жет предположить, что возвращаемый объект относится к какому-то конкрет¬ ному типу, — только к Object.
Когда собака перестает быть собакой Вот где начинается путаница с полиморфизмом и типами в fava! Когда объект типа Dog переда¬ ется в метод, возвращающий Object, компи¬ лятор теряет информацию о типе Dog. Это все еще тот же самый Dog в куче — гавкает, виляет хвостом, бегает за мячиком, — но ссылаться на него можно только как на Ob j ect, если явно не привести тип обратно к Dog. Ллсхс public void go() { Dog aDog = new Dog(); Dog sameDog * getObject(aDog); fttut сийлдат, public Object getObject(Object о return о; ft, " ’ _ ftzwolW, псшмцгтб jam qcriu \ )L wcm wya «Gfmmu чка 1/(ШЧ ni|u. |ecc(uo]eci о Йщцд/шп ту же самую со саки, ас (? ftше оотектя пиша cmft,au ттс Jam донискиет apccftmftcн ие люссй сспм ; ■ Этс самки типа СЩ п,,„.„.„„с шбциии апатита <Цевцметяа ftj) у ЙЖУ^Аг™ШЩЛаж,кТв,1 ' vcJncA codaU. ft* WMUwnfe се как Ofct списка типи сЛиске ueiumftemcuiao на- | File Edit Window Help Remember DogPolyTest.java:10: incompatible types found : java.lang.Object required: Dog Dog sameDog = getObject(aDog); 1 error л Лс.шииитсф tyucttu нсуниет, тсёс^Шшделти сстскт — эпц; ccckum, и нс neuftuum ntmcfoumt его нук.шнниг типи Juoa. (Жс~ п.иу — ncH &jw.M ни с иууници cmjumuux.) тс public void go() { Dog aDog = new Dog(); Object sameDog = getObject(aDog); ) public Object getObject(Object o) { return o; ) Этс fxamnam (хотя, вилштсс, и не у/ л, имекцд1 пелелес — ft* скфе neiUceme, пт.щ), попищи шсяюсш объект можно сушсбсите пуклеен ней типа Ofact Калс- т JS-Л у ьч lava нггсле- уцютт cm UKjict У)amt если fti этом: явно не укауп&шпе, Otfitct fet ftufttc нахщшпея ни ft/ишене сшт — ■ ' I уиугаш.
Объекты не лают Итак, теперь вы знаете, что если объект ссылается через переменную типа Ob j ect, его нельзя присвоить переменной его реального типа(например, Dog)без явного преобразования. Вы также знаете, что аргументы и возвращаемые значения могут быть объявлены с типом Object. Например, если объект помещается в ArrayList<Ob j ect>, он сохраняется именно как Object. Но в чем смысл? Разве это не слишком ограничивает? Зачем использовать перемен¬ ную типа Ob j ect, чтобы сослаться на Dog? Хороший вопрос. Попробуем обратиться к методам нашего объекта Dog, который компилятор теперь видит как Obj ect: /fi mA'ivii- Object о = al.get(index) int i = о. hashCode () ; c.bark(); c. jtw cf u«:h lCCJU« ” . ,„nn\ „Ш.И fltwfiom’t t/l )(t пашите*! Класс Cdjat ncmatm. rnu така daikO. /)а,ш сщ вы. уверены, mu 9me самка, кеипиштр нс вктег. СМ). t?c Компилятор решает, можно ли вызывать метод, исходя из типа переменной-ссылки, а не из того, каким объектом она фактически ссылается. Даже если вы точно знаете, что там пря¬ чется настоящий Dog (и он точно умеет лаять!), компилятор относится к нему как к обычному Object. Может, это вообще не собака, а кнопка. Или микроволновка. Или еще что-то, что вообще не умеет делать «гав». Компилятор не гадает — он просто про¬ веряет, какие методы определены в классе ссылочного типа. 'Hemuj, кап уши (?м вшы&шт am тики, лсьшш хпаншшея г классг cm мем атшеге типа. И нс &шш. /тс геген >\j\ujcmid mm ссмкт о.hashCode(); Сет ntjvumмн/м ы&мвмпа как Овкс(, тс da. /тс &т асапипне, — эни жнюм. нахимщмем н к\аса Cdjat. 1 7
■* Он воспринимает меня как просто объект... Но я же могу больше! Если к бы он только увидел, кто я на самом деле... *• Ощутите свой внутренний объект Объект содержит все, что наследует от каждого родитель¬ ского класса. Это означает, что любой объект, независимо от его фактического класса, — также экземпляр класса Object. Это, в свою очередь, говорит о том, что к любо¬ му объекту в языке Java можно относиться не только как к Dog, Button или Snowboard, но и как к Object. С помощью команды new Snowboard () вы получаете один объект Snowboard в куче, но он заключает в себе внутреннее ядро, представляющее часть самого Object. Object equalsQ getClassQ hashCodef) toStringO Snowboard Snowboard наелаf equals!) getClass() hashCodeQ toStringO ') tum() shredQ getAirf) loseControl() amm дге- mam [\aju mt ucKoiv класса Cmct u 'j( cadmtm tut/ ruiuyu. /J КЦ . — Sncwdoatd. Нс on ca. curt ИШ14КС ши H ( CM.к ж Kt wu Ht нтм с(юих классе^, Sncwtfcaid u (i’icd. ушит
Полиморфизм означает «много форм» Вы можете воспринимать Snowboard как Snowboard или как Object Ссылка — это как пульт дистанционного управления. И по мере того как вы продвигаетесь вниз по иерархии насле¬ дования, у этого пульта появляется все больше кнопок. Пульт типа Object управляет самыми базовыми функци¬ ями — у него есть всего несколько кнопок, соответствую¬ щих методам класса Ob j ect. Но пульт типа Snowboard? О, это уже серьезная техника! У него есть все кнопки от Object, и дополнительные, специфичные для Snowboard. Чем больше у класса своих «фишек», тем больше кнопок на пульте. Конечно, не все дочерние классы добавляют новые кнопки. Иногда они просто заменяют старые — как будто меняют функцию на уже существующей кнопке. Важно помнить: даже если объект — это Snowboard, ссылка типа Object не увидит дополнительных кнопок. Потому что для компилятора он всего лишь Ob j ect. Поместив объект в ArrayList<Object>, вы можете отно¬ ситься к нему толь¬ ко как к Object, независимо от его изначального типа. Когда вы по¬ лучите ссылку из ArrayList<Object>, она всегда будет иметь тип Object. Snowboard S^= Object О = s 714,11m уиспшн иди н ноге .тения Snc w&mci (ссчикс Jfu u ft.aid) уксиплсктхван no ru. i ной/ U него сашш кнопок, wee и епшнаарпшеи пирита Gtfitct С такси ссылкой т вшгипи весь сткт Sncwecatd — и его сштвен ntn.mmuju. и унаслхасваннтот Свкс1, /Вес поа ксшпЛсиеее, всямсиць в руках. Snowboard ^ ьект SoO'N'0®6 Сели вь\ аержитс вышках пиши СвШ qaxet гшуя RaStwwvcatd, т вщиш > ct yum топке ОвШ-гааш. Кикакчх тркксв, еенкдкил спускав с герп — только ссшвьие. штщы, вриус toSttinyO. l\askCcdt(ru ccniaCsO. У та¬ кие пиита, шшшс кнопок. Истому ши ссылка типа ( Citct управ ежт nu. itко mo.ee, гто ?нлетс Свра, и нигего еашеее.
Приведение ссылки на объект к ее действитель ному типу. Лшвизим так нат&клшк Object (нслтлииЛ. тп на слмолi at tt это ш!ака) й’ншл JDcq, тиса бамсгшафа- щдемся с ним. как с сосакт, кстс/кй он на сашм уеле яСлжтся. Object Это все еще объект Dog, но если вы хотите вы¬ звать характерные для него методы, вам нужна ссылка с таким типом. Если вы уверены', что этот объект действительно Dog, то можете создать для него новую одноименную ссылку, скопировав ссылку Ob j ect и переместив копию в переменную ссылочного типа Dog, используя приведение (Dog). Вы сможете применять новую ссылку для вызова методов объекта Dog. Object о = al.get(index), Dog d = (Dog) о; mw о d.roam() ; j£)ca ec. aJKKfliKiniuuf „Ш эп\о он Object Dog 'Если вы не уверены, что это Dog, то можете вызвать для проверки оператор instanceof. Если вы неправильно приведете типы, то получите ошибку ClassCastException и преждевременное завершение программы. if (о instanceof Dog) { Dog d = (Dog) о;
Теперь вы знаете, как Java за¬ ботится о методах в классе ссы¬ лочной переменной. Метод объекта можно вызвать, только когда он есть у класса ссылки. Считайте публичные методы в классе своим контрак¬ том, обещанием внешнему миру о том, что вы можете делать. Создавая класс, вы почти всегда открываете доступ к не¬ которым его методам для остального кода. Открытие метода означает, что вы делаете его доступным, чаще всего помечая его как public. Представьте такую ситуацию. Вы пишете код для небольшой бухгалтерской программы — заказного приложения для ком¬ пании Simon’s Surf Shop. Вдруг вы находите класс Account, который, судя по докумен¬ тации, идеально подходит. Каждый экзем¬ пляр этого класса представляет отдельный клиентский счет в магазине. Отлично. Вы продолжаете писать код, вызываете методы credit () и debit () для объекта счета и вдруг понимаете, что вам нужно по¬ лучить текущий баланс. Ничего страшного — ведь в докумен¬ тации сказано, что есть метод getBalance (). То, что нужно! Но вот беда: при вызове getBalance () все рушится — вы получаете катастрофическую ошибку. Оказывается, никакого такого метода у класса нет, несмотря на обещания документа¬ ции. Ай! Хорошо, что компилятор — ваш друг. Он никогда не позво¬ лит подобной подставы. Как только вы используете оператор «точка» (а. doStuf f ()), компилятор сразу смотрит на тип ссылки (а — это переменная с определенным типом) и прове¬ ряет: существует ли у этого типа метод doStuf f (), принима¬ ет ли он переданные вами аргументы и возвращает ли значение ожидаемого типа. Важно: компилятор смотрит на класс ссылки, а не на факти¬ ческий объект, к которому эта ссылка привязана. Account debit(double amt) credit(double amt) double getBalanceQ
Что делать, если нужно изменить контракт Хорошо, представьте, что вы — Dog. Ваш класс Dog — не единственный контракт, опре¬ деляющий, кто вы такой. Помните, что вы на¬ следуете доступные (обычно public) методы из всех родительских классов. Действительно, ваш класс Dog определяет контракт. Но не все ваши контракты. Все, что находится в классе Canine, — часть вашего контракта. Все, что находится в классе Animal, — часть вашего контракта. Все, что находится в классе Object, — часть вашего контракта. Согласно проверке на соответствие (IS-А), вы представляете собой каждый из этих объек¬ тов — Canine, Animal и Ob j ect. Но если человек, разработавший ваш класс, думал о программе-симуляторе Animal и те¬ перь хочет использовать вас (класс Dog) для учебной научной ярмарки объектов Animal? Все в порядке, скорее всего, вы сможете по¬ вторно использоваться и для этого. А если позже он захочет задействовать вас для программы зоомагазина? У вас нет на¬ выков домашнего животного. Объект Pet должен иметь методы вроде beFriendly () и play(). Теперь представьте, что вы разрабатываете класс Doq. Просто добавьте к нему несколько методов. Вы не нарушите этим ничей код, так как не будете трогать существующие методы, которые могут быть вызваны другим кодом для объектов Dog. Видите ли вы какие-нибудь препятствия для добавления методов Pet в класс Dog? Разум рулит Представьте, что вы работаете над классом Dog. Все отлично, но тут появляется задача — ваш пес должен стать питомцем. Логично? Конечно. Но как это реализовать? Можно просто доба¬ вить к классу Dog свойства и методы, характер¬ ные для питомцев (Pet). Это сработает. Более того, уже написанный код, использующий Dog, продолжит работать без изменений. Звучит как победа! Но... у нас программа для зоомагазина. А в ней не только Dog. У нас здесь кошки, попугаи, игуаны и даже рыбы! А теперь представьте, что кто-то захочет использовать ваш класс Dog не для магазина домашних животных, а, скажем, для симуляции дикой природы. Там, где собаки бегают стаями, охотятся и не сидят на розовых подстилках с бантиками. И вот тут встает вопрос: как добавить свойства питомца ТОЛЬКО тем животным, которые дей¬ ствительно являются питомцами? Попробуйте придумать способ, не лезьте пока на следующую страницу* — серьезно. Поду¬ майте, как это можно реализовать. ‘Придумайте пару вариантов. Только не жульничайте и не заглядывайте вперед — ведь тогда вы лишитесь отличной возможности немного напрячь мозг (и сжечь пару калорий!).
Варианты повторного использования существующих классов в программе для зоомагазина На данный момент не важно, сможет ли Java сделать то, что мы придумаем. Мы будем решать эту проблему, как только у нас появится наилучший выбор из нескольких вариантов. ® Вар иант первый Выберем легкий путь и поместим методы для домашних животных в класс Animal. Преимущества: Все объекты Animal сразу унаследуют особен¬ ности домашних животных. Нам не придется трогать существующие подклассы Animal, а созданные в будущем подклассы также унаследуют и будут использовать эти методы. Таким образом, класс Animal может применять¬ ся как полиморфический тип в любой про¬ грамме, которая будет относиться к объектам Animal как к домашним животным. Недостатки: Вы когда-нибудь видели бегемота в магазине домашних животных? А льва? Или волка? Опасно разда¬ вать методы домашних животных тем, кто к ним не относится. г Anil Г1 1 Hippo ^ К тому же мы, скорее всего, будем касаться классов для домашних жи¬ вотных, например Dog и Cat так как (по'крайней мере, в наших домах) собакам и кошкам свойственно разное поведение.
® Вариант второй Мы начнем с первого варианта, поместим методы для до¬ машних животных в класс Animal, но сделаем их абстрактны¬ ми, заставив наследников Animal переопределять эти методы Преимущества: Мы получаем все преимущества первого варианта, но без негативных последствий, при которых дикие животные имели бы свойства домашних (например, beFriendlyO). Все наследники Animal будут содержать эти методы (потому что они будут находиться в классе Animal). Однако, посколь¬ ку методы будут абстрактными, классы Pet не унаследуют их функции. Все классы должны будут заменять методы, но смогут сделать их такими, чтобы они «ничего не делали». Недостатки: Поскольку все методы для домашних животных в классе Animal абстрактны, то конкретным подклас¬ сам Animal придется реализовывать их (не забывайте, что все абстрактные методы должны быть реали¬ зованы первым конкретным подклассом вниз по иерархии наследования). Какая трата времени! Вам придется сесть и внести каждый метод для домашних животных в каждый класс дикого животного, а также во все буду¬ щие подклассы. Несмотря на решение проблемы фактического выполнения дикими животными методов для домашних животных (как было бы, если бы они наследовали их функциональность из классов Animal), в кон¬ тракте все же есть недо¬ статки. Каждый класс дикого животного будет объявлять на весь мир, что он тоже обладает этими методами, даже если они фактически ничего не будут делать при вызове. Этот способ нам не подходит. Не нужно за¬ пихивать в класс Animal больше, чем требуется одному типу Animal, — если, конечно, это не относится ко всем его до¬ черним классам. Лелнхтите Set vumum |\«amtv с срлтшнплис VuGcvnumW cvujja, не <£? ишлшандт.. Саеланте «се литам аелееншшх жг бетнюе Уа\\|\акнмш.ми-
Вариант третий Помещаем методы для домашних животных только в те классы, для которых они подходят. Преимущества: Больше не нужно переживать о бегемотах, встре¬ чающих вас у дверей и облизывающих вам лицо. Методы будут находиться там, где должны быть, и только там. Объекты Dog и Cat смогут реализовать эти методы, и никто больше не будет о них знать. Недостатки: У этого способа есть две проблемы. Во-первых, вам нужно согласиться с протоколом, и все разработчики классов домашних животных с этого момента будут знать о нем. Под протоколом мы подразумеваем точ¬ ные методы, которые, как мы решили, будут содер¬ жать все домашние животные, — то есть это контракт для домашних животных без права его отмены. Но если кто-то из программистов неправильно поймет его? Например, метод будет принимать строку, тогда как должен принимать целое число. Или назовет метод doFriendlyO вместо beFriendly(). Получает¬ ся, что компилятор не сможет проверить, верно ли вы реализуете свои методы. Кто-нибудь согласится использовать классы домашних животных и затем выяснит, что не все из них работают верно. Во-вторых, вы не сможете задействовать полиморфизм для методов домашних животных. Каждому классу, которому пона¬ добится использовать пове¬ дение домашних животных, придется знать обо всех классах! Другими сло¬ вами, теперь вы не сможете применять Animal как полиморфи¬ ческий тип, потому что компилятор не позволит вам вызвать метод, принад¬ лежащий классу Pet, в контексте ссылки типа Animal (даже если он действительно является объектом Dog), ведь у класса Animal не будет это¬ го метода. fiC PiutuiUjUkim uunujtv. сткосяиддсся к типе.мид и, ТОЛЬКО (? тех клас¬ сах жиРстнмлс, котс|ии ашстРа- теиас.мтцп алии пипшиидлие, а не Р cfc^oPau классе ДгйтаР.
Итак, что нам ДЕЙСТВИТЕЛЬНО необходимо: сР Способ, при котором свойства домашних животных будут описаны только в классах для домашних животных. (Р Способ, гарантирующий, что для всех классов домашних животных будут определены одинаковые методы (одно имя, одни аргументы, одинаковые типы возвращаемых значений, не будет отсутствующих методов и т. д.). Зто позволит надеяться, что все программисты поймут их так, как нужно. (Р Способ, при котором можно получить пользу от полиморфизма, чтобы все до¬ машние животные вызывали свои методы, без необходимости использовать для каждого класса аргументы, типы возвращаемых значений и массивы. Похоже, нам понадобятся два родитепьских класса на верши¬ не иерархии. •jut—*- - Капка jfYMnfw cn\j\ciHMn\cM как cm лсибстншг, п\дк и cm питьмнД нсэт&лш сна патшаст штат. остх. l(c аслшижис лш(?стнки нс Кцсдшикт никаких- •Л1ШЦЦ lev Жн (?С(П IIM..V (?C
Только в таком способе «двух родительских классов» кроется одна проблема... Это т. н. «множественное наследование», которое может привести к действительно большим проблемам. Вернее, могло бы привести, если бы это понятие под¬ держивалось языком Java. Но это не так, потому что у множественного наследо¬ вания есть проблема, известная под названием «смер¬ тельный ромб» (Deadly Diamond of Death). «Смертельный ромб» juv* i итоль- ■jattTvdC КАК С./УВи-met. так IftfVVjOlfiumct, с ралил.ии /И|«нн(.ц. л;.., ■',г,<нна.и на- P>yfKauncq n,u !?U u£mo9 to**# WS£)^ 7,м &Шт() на В языке, который позволяет реализовать «смертельный ромб», могут возникать запутанные ситуации, так как придется иметь специальные правила для разрешения возможных недоразуме¬ ний. А дополнительные правила означают лишнюю работу как в изучении этих правил, так и в наблюдении за «особыми случая¬ ми». Язык Java должен быть простым, с согласованными правила¬ ми, которые не приведут к катастрофической ошибке при разном развитии событий. Поэтому Java (в отличие от C++) позволяет не думать о «смертельном ромбе». Но это возвращает нас к первона¬ чальной проблеме! Что же делать с этими Animal и Pet?
Интерфейс спешит на помощь! Java дает вам решение — интерфейс. Не графический интерфейс, не общее понятие «интерфейс», как в выражении «Это открытый интерфейс для API класса Button», а ключевое слово interface. Интерфейс в Java решает проблему множественного наследования, разрешая вам пользоваться преимуществами этого наследования без негативных по¬ следствий от «смертельного ромба» (СРС). Способ, с помощью которого интерфейсы обходят СРС, на удивление прост: они делают все методы абстрактными! Таким образом, подкласс обязан переопределять методы (помните, абстрактные методы должны быть реали¬ зованы первым конкретным подклассом), чтобы при выполнении програм¬ мы JVM не запуталась, выбирая вызов из двух наследованных версий. Pet Интерфейс в Java — это 100% абстрактный класс. abstract void play(); Для определения интерфейса: public interface Pet {•••} Для реализации интерфейса: public class Dog extends Canine implements Pefc { \ /fmuuv’uium клютебое слово onfi&mmt*, м Kimtluiu г.нгщпп идея . . .... I » /W: «■» StjJ ( /лл Л IY\ 11 LV\ L <?u It 11.ШШ /(muuv’uiunt клютебое слово oruxfonmt'i, ш c.ucjij' (iiimdubcuca. OoRcunumt fiuu urn. Ccimmmnt (miuwiuu. imuuw
Создание и реализация интерфейса Pet л тнп нас, Лшисм н\Ца« (lutcnve свам- s/ public interface Pet { н ш ля — wAkiodatwbэщц асшп£ ггипишпся «.xtitam и спииси» ню ?аеп M nficanc much ajmmimi ^Hu.JJ,mjMa public abstract void beFriendlyO public abstract void playO ; nrxrsK.».. - %ХжШ мкакШтюм ' " 'ихдпи.й-. ложниш, nvovKoa c } .,llUlfau,bumnu.uJJ JTuittLAltUcfr tnyvft.H\Cltb, f\a 3-Мф)5^ ^ m KuiumtM cjufifan urn ,1 $cq "3S-A 'l +.e p^t* ( жмтиМхмса public class Dog extends Canine mplefflenrs rm. i , , public void beFriendlyO {. . ■} public void play() {..} ^ CR{1Л14И fHUi aP - фтшпе *<W,' сксРки вшапс public void roam() {...} public void eat() {...} (. Jmc utvmtu lufuuiht (рмния „штсусё. 7 ,nin«^TuHtU (He) дурацкие вопросы ^OV\pOC: Погодите, интерфей¬ сы ведь не дают настоящего множественного наследования, потому что в них нельзя писать код реализации. Если все методы абстрактные, то в чем вообще смысл интерфейсов? Оиивем: Полиморфизм — вот в чем! Интерфейсы невероят¬ но гибкие. Если вы используете интерфейс вместо конкретного класса (пусть даже абстрактного), то сможете принимать любые объ¬ екты, реализующие этот интерфейс. Подумайте: интерфейс позволяет классу не ограничиваться одной иерархией. Класс может расши¬ рять один суперкласс, но при этом реализовывать сколько угодно интерфейсов. И другой класс — из совершенно другой иерархии — может реализовать тот же интер¬ фейс. Получается, вы работаете не с типами, а с ролями объектов. Бонус: когда вы проектируете систему на основе интерфейсов, вам даже не нужно предоставлять родительский класс. Вы просто даете интерфейс и говорите: «Вот контракт. Мне все равно, кто вы по наследованию — реализуйте этот интерфейс, и можете спокойно участвовать в программе». Да, вы не можете добавить в ин¬ терфейс код, но в хорошей архи¬ тектуре это и не требуется. Боль¬ шинство методов интерфейса все равно приходится реализовывать индивидуально — слишком уж разный у всех «характер». Так что отсутствие реализации — не минус, а возможность.
Классы из разных иерархий наследования могут реализовывать один и тот же интерфейс I Robot J Когда вы используете класс как полиморфический тип (например, массив типа Animal или метод, принимающий аргумент типа Canine), объекты, которые вы можете прикрепить к этому типу, долж¬ ны быть из одной иерархии наследования. Кроме того, они должны содержать класс, который будет дочерним классом полиморфического типа. Аргу¬ мент типа Canine может принимать Wolf и Dog, но не Cat или Hippo. Но, когда вы используете интерфейс как полимор¬ фический тип (например, массив для объектов Pet), объекты могут быть откуда угодно. Единственное требование — они должны содержать класс, ко¬ торый реализует этот интерфейс. Возможность классов из разных иерархий наследования пере¬ определять общий интерфейс — ключевая особен¬ ность в Java API. Хотите, чтобы объект сохранял свое состояние в файл? Реализуйте интерфейс Serializable. Хотите, чтобы объекты запу¬ скали свои методы в отдельных потоках выполне¬ ния? Реализуйте Runnable. Надеемся, вы поняли идею. В следующих главах вы больше узнаете о Serializable и Runnable, а сейчас запомните, что классам из любого места в иерархии наследо¬ вания может понадобиться реализация этих ин¬ терфейсов. Практически каждый класс хочет быть сохраняемым или запускаемым. Более того, класс может реализовывать несколько интерфейсов! Объект Dog соответствует классам Canine, Animal и Object, и все благодаря наследованию. Однако Dog соответствует интерфейсу Pet, так как реализует его. Кроме того, он может реализовывать другие интерфейсы. Просто перечислите их через запятую: public class Dog extends Animal implements Pet, Saveable, Paintable { ... }
Как узнать, когда нужно создавать класс, подкласс, абстрактный класс или интерфейс ■ Создавайте класс, который ничего не наследует (не считая Object), когда ваш новый класс не проходит проверку на соответствие другим типам. ■ Создавайте дочерний класс, только когда вам нужно сделать более специфичную версию родительского класса и заменить или добавить новое поведение. ■ Используйте абстрактный класс, когда хотите определить шаблон для группы подклассов и у вас есть хоть какой-нибудь код реализации, который смогут применять все подклассы. Делайте класс абстрактным, когда хотите получить гарантию того, что никто не сможет создать объекты данного типа. ■ Пользуйтесь интерфейсом, когда хотите определить роль, которую смогут играть другие классы, невзирая на то, где они находятся в иерархии наследования.
Вызов версии метода из родительского класса (Не) дурщкме вопросы Вопрос: Что делать, если у вас уже есть конкретный подкласс, и вам нужно заменить метод, но вы не хотите полностью переписы¬ вать его поведение? Нужно просто добавить немного дополнительного кода. abstract class Report { void runReport() { // создание отчета } void printReport() { // вывод на печать } } /klu u.4 uunwjii tu¬ rn \кллсеа (stwtuiwm йгж нш <u.u лигипу пшдас- ситкд w. подкласса и. OlMBeiM: Вспомните, что означает слово extends, — «расширяет»! Хороший дизайн в ООП как раз и предполагает, что вы зара¬ нее продумываете, какие методы могут быть частично заменены, а какие — просто расши¬ рены. Представьте: вы пишете метод в абстрактном классе. Он содержит базовую реализацию, которой вполне достаточно для большинства случаев. Но в подклассе нужно сделать нечто дополнительное — немного уникального по¬ ведения. Что ж, не надо выкидывать все и пи¬ сать с нуля! Переопределите метод и просто добавьте нужное поведение, не забыв вызвать оригинальный метод из родительского класса. Для этого и существует ключевое слово super. С его помощью можно запустить родитель- class BuzzwordsReport extends Report void runReport() { 4 super.runReport(); buzzwordCompliance() printReport(); Сшшт (iWfA&ttu hcrtumt-ttcKUK (y\ а. за¬ тем. дсоатм nui. TII\C ШШНС М.Ш Н1Н void buzzwordCompliance () { . . . } >NH-WJ tUiJK.T/KT'lJ. скую версию метода прямо внутри дочернего класса. 6c.au кюжгшш в подклассе BuzziwidRctxcn всилмдит так: super. runReport () то метод mnRqw tU) uy сутр - класса Report Судет вьиван. super.runReport(); Ссылка на ешкт подкласса (ВuzzwordReport) всегда Судет бум валы дочернюю (чЦепи тресехредЛсн ноте метода — это и ют палишр- Йле. Однако код самой} подкласса сет u/fwmumlcM к Подите. льсксй версии, вьхдвав super. runRywrtO. Мтхсдех суперкласса, включая ixlpuixpxgenix нм и метод mnRgxcrtO Ключевое слово sufxet на самом доле, у кат вахт на родительскую menu остиста. пт да кед в подклассе иг магу уст super. как. yt пхир. в sup a. m nRefmtO, допускается h an версия мелххода ty суперкл ласса cr.
Без лишних «лов ■ Если не хотите, чтобы кто-то создавал объекты определенного класса, пометьте его ключевым словом abstract. ■ Абстрактный класс может содержать как абстрактные, так и обычные (с телом) методы. ■ Если в классе есть хотя бы один абстрактный метод, сам класс обязан быть абстрактным. ■ Абстрактный метод не содержит тела и заканчивается точкой с запятой (а не фигурными скобками). ■ Все абстрактные методы должны быть реализованы в первом конкретном (неабстрактном) подклассе. ■ Каждый класс в Java прямо или косвенно наследуется от Object (java.lang. Object). ■ Методы можно объявлять с параметрами и/или возвращаемыми типами Object. ■ Вызвать метод через ссылку можно только в том случае, если метод определен в классе (или интерфейсе), указанном как тип этой ссылки — неважно, какой объект реально находится по ту сторону. То есть переменная типа Object может вызывать только методы из Object, даже если на самом деле она ссылается на собаку, холодильник или ракету. ■ Во время выполнения запускается та версия метода, которая определена в реальном типе объекта. ■ Переменной типа Object нельзя напрямую присваивать другой ссылочный тип без приведения. Приведение работает, если объект в куче действительно относится к нужному типу — иначе будет ошибка. Пример: Dog d = (Dog) x.getObject(aDog); ■ Все объекты, извлеченные из ArrayList<Object>, воспринимаются как Object, пока вы не выполните приведение. ■ В Java нет множественного наследования классов, чтобы избежать «смертельного ромба» — можно расширить только один суперкласс. ■ Интерфейс объявляется с помощью ключевого слова interface, а не class. ■ Чтобы реализовать интерфейс, укажи implements, например: Dog implements Pet. ■ Класс может реализовывать сразу несколько интерфейсов. ■ Если класс реализует интерфейс, он обязан реализовать все его методы, кроме тех, что имеют модификаторы default или static (об этом позже, в главе 12). ■ Чтобы вызвать родительскую версию переопределенного метода, используй super, например: super, run Report(); (Не) дурацкие вопросы ^OV\pOC: Здесь все еще что-то странное... Вы так и не объясни¬ ли, как ArrayList<Dog> возвра¬ щает ссылки на объекты Dog, которые не нужно приводить к типу. В чем заключается этот особый трюк, когда вы говорите ArrayList<Dog>? Oi/ивеж: Вы правы, это действительно особый трюк. ArrayList<Dog> возвращает объ¬ екты Dog, и вам не нужно их при¬ водить, даже несмотря на то, что методы ArrayList вроде бы вообще ничего не знают о типе Dog — только об Object. Краткий ответ: компилятор сам подставляет приведение типов! Когда вы пишете ArrayList<Dog>, никакой специальной версии класса ArrayList с методами для работы конкретно с Dog не созда¬ ется. Но <Dog> — это сигнал ком¬ пилятору: разрешать добавлять только объекты типа Dog и оста¬ навливать вас, если вы попыта¬ етесь добавить в список что-то другое. И поскольку компилятор гарантирует, что в ArrayList<Dog> содержатся только Dog, он знает, что безопасно привести все, что выходит из этого списка, к типу Dog. Другими словами, использо¬ вание ArrayList<Dog> избавляет вас от необходимости ручного приведения. А это особенно важ¬ но, потому что приведение может не сработать во время выполне¬ ния. Гораздо лучше отлавливать ошибки на этапе компиляции, а не тогда, когда ваш клиент ис¬ пользует это в продакшене. Но это только начало; к этой теме мы обязательно вернемся в главе 11.
рашжнев1?т0 ваш ш анс пР°Дем°нстрировать свои художественные способности. Слева " перечислены объявления классов и интерфейсов. Ваша задача — изобразить справа соответствующие схемы классов. Первую мы сделали за вас. Используй¬ те пунктирную линию для «реализует» и сплошную для «расширяет». Исходные данные 1. public interface Foo { } public class Bar implements Foo { } Каким будет изображение 2. 3. public interface Vinn { } public abstract class Vout implements Vinn { } public abstract class Muffie implements Whuffie { } public class Fluffie extends Muffie { ) public interface Whuffie { } 1. 5. public class Zoop { } public class Boop extends Zoop { } public class Goop extends Boop { } public class Gamma extends Delta implements Epsilon { } public interface Epsilon { } public interface Beta { } public class Alpha extends Gamma implements Beta { } public class Delta { } -► Ответы на стр. 271 т J 1 1 1 имя к имя к | ИМЯ и^ 5. Легенда расширяет реализует класс к интерфейс
ражнение Исходные данные 1. Click Clack Слева расположены схемы классов. Ваша задача — написать к ним пра¬ вильный код на языке Java. Первый пункт мы уж£ добавили (и он был довольно трудный). Каким будет объявление 1. jmPPic cPms ( Pick { } jwPCic cP/us (Pack extends (Pick { } 2. 3. Ответы на стр. 271 T Легенда расширяет -Тч ! реализует I^Цвлчок^ класс интерфейс | щелчок I абстрактный класс
бассейн фрагментов Задача: подставить фрагменты кода из бассейна на свободные места. Некоторые фрагменты не понадобятся, а некоторые можно использовать больше одного раза. Цель задания — создать класс, который скомпилируется, запустится и вы¬ даст результат, который приведен ниже. Nose { } abstract class Picasso implements { return 7; 5 } class { ) class { return 5; } } public extends Clowns ( public static void main(String[ ] args) { i [ 0] = new i[l] = new i [ 2] - new for (int x = 0; x < 3; x++) { System.out.println( + " " + . getClass () ) ; У } } Примечание: Каждый фрагмент из бассейна может использоваться не¬ сколько раз. Результат %java 5 class Acts 7 class Clowns Of 7 6 Ответы на стр. 272
Ответы Каким будет изображение (со стр. 268) Каким будет объявление (со стр. 269) 2. f<umuJufKiif) I 4. 7J^ T'cut 2 puCCic attract cCms Tcp {} puCCic (Cass Tip txktuis Tip {} 3 puCCic aCstmct (Cass Jet { } puCCic abstract cCass Ji extends Ты (} 4 puCCic interface Тсс {} puCCic (Cass Ba t in\pCtn\a\Ls Jet {} puCCic cCass Bazextaxds Bat {} 5_ puCCic inktjact Zeta {} puCCic cCass. ICpha mxpCemmts Zda {} puCCic interface Beta {} puCCic cCass JkCta txktvis ACpka myi(bnmfo Beta (} T **ч | имя ■ .Легенда расширяет реализует к класс к интерфейс к абстрактный класс
бассейн фрагментов (со стр. 270) interface Nose { public int iMethodO; } abstract class Picasso implements Nose{ public int iMethodO { return 7; ) t class Clowns extends Picasso { } public classOf76 extends Clowns { public static void main(String[] args) { Nose[] i = new Nose [3]; i[0] = new Acts(); i [ 1 ] = new ClownsO ; i [2 ] = new Of76(); for (int x = 0; x < 3; x++) ( System.out.println( i[x].iMethodO + " " + i[x] . getClass ( ) ) ; I ) ) class Acts extends Picasso i public int iMethodO { return 5; i } Результат %java Of76 5 class Acts 7 class Clowns 7 class Of76
9. Конструкторы и сборка мусора Жизнь и смерть объектов / «И тут он закричал: «Я не чувствую "своих ног!» Я в ответ: «Джо! Не оставляй меня!» Но, увы... было слишком поздно. Пришел сборщик мусора и... все! Он исчез словно объект, который больше не нужен. Это был мой лучший объект, но, увы, па- мять не щадит никого!» Объекты рождаются И умирают. Вы управляете их жизненным циклом. Вы решаете, когда и как создавать их — и когда уничтожать. Но на самом деле вы не уничтожаете их, а просто делаете брошенными и недоступными. Уже после этого безжалостный сборщик мусора может аннулировать объекты, освобождая используемую память. Если вы собираетесь писать на Java, то нужно будет создавать объекты. Рано или поздно от некоторых из них вам придется избавиться, иначе возникнет риск перерасхода оперативной памяти. В этой главе мы рассмотрим, как создаются объекты, где размещаются, как эффективно использовать их и делать недоступными. Речь пойдет о куче, стеке, областях видимости, конструкторах (обычных и родительских), нулевых ссылках и многом другом. Но сразу предупреждаем: глава содержит описание смертей объектов, а это зрелище не для слабонервных. Не принимайте все близко к сердцу.
Стек и куча: где все хранится Прежде чем понять, что на самом деле происходит при создании объекта, нужно вернуться немного назад. Следует узнать больше о том, где и как долго Java хранит данные. Это означает, что мы должны более подробно изучить стек и кучу. При работе с Java нас как программистов волнуют две об¬ ласти памяти: в одной находятся объекты (куча), а в другой хранятся вызовы методов и локальные переменные (стек). При запуске JVM получает часть памяти от исходной операционной системы и использует ее для выполнения Java-программ. Сколько именно выделяется памяти и можно ли управлять ее объемом, зависит от версии JVM (и от платформы). Но, как правило, такие подробно¬ сти будут от вас скрыты. Кроме того, при должном подходе к программированию они, скорее всего, не будут вас интересовать (подробнее мы это рассмо¬ трим чуть позже). Мы знаем, что все объекты размещаются в куче, управляемой сборщиком мусора, но нам пока не из¬ вестно, где обитают переменные. Зависит ли место обитания переменных от их типа? Надо отметить, что под типами мы подразумеваем не примитивы или ссылки на объекты. Типы, которые нас сейчас интересуют, разделяют переменные на локальные и принадлежащие классу (переменные экземпляра). Локальные переменные также известны как стеко¬ вые, что указывает на место их размещения. Стек Место, где обитают вызовы Ме¬ тодов и локальные переменные. Переменные экземпляра Объявляются внутри класса, но не внутри метода. Это «поля», которые содержатся в каждом конкретном объекте (и могут иметь разные значения для каждого экземпляра одного класса). Переменные экземпляра обитают внутри объектов, которым принадлежат. public class Duck { int size; VUJK Mf Локальные переменные Объявляются внутри метода (это касается и пара¬ метров метода). Они временные и существуют до тех пор, пока метод находится в стеке (иными словами пока метод не достиг закрывающей фигурной скобки) public void foo(int х) { int i = х + 3; boolean b = true; AmcututtC l\U|\CU« тр£хТГ«\1[«леи- Htu i W (’ }
Методы в стеке Вызываемый метод берется из вершины стека. Эта новая для вас сущность находится в стеко¬ вом фрейме и хранит состояние метода, вклю¬ чая строку кода, которая выполняется в теку¬ щий момент, а также значения всех локальных переменных. На вершине стека всегда расположен метод, который выполняется в данный момент (будем считать, что существует всего один стек; о дру¬ гих ситуациях поговорим в главе 14). Метод остается в стеке до тех пор, пока при выпол¬ нении программа не дойдет до закрывающей фигурной скобки (это будет означать, что метод завершился). Если метод f оо () вызывает метод bar (), то bar () помещается сразу над foo ( ) . Стек вызовов с двумя методами: Метод, выполняющийся в данный момент, всегда находится на вершине стека. Ку гасни стека ^—/’нр.хпяя гасни спика t cKaa/Htu < ищщм (включая пярамтиi х) public void doStuff() ( boolean b = true; go(4) ; } public void go(int x) { int z = x + 24; crazy(); // прочий код ) public void crazy() { char c * 'a' ; } Поведение стека Код слева — это фрагмент (нам неважно, как выглядит остальная часть класса) с тремя методами. Первый метод (doStuf f ()) вызывает второй метод (до ()), а второй метод вызывает третий (era z у ()). Каждый метод объявляет одну локальную перемен¬ ную в теле метода (b, z и с), а метод до () также объявляет пере¬ менную-параметр (это означает, что до () имеет две локальные переменные — х и z). Код из другого класса вызывает метод doStuf f () , помещая его во фрейм на вершине стека. Логическая переменная Ь раз¬ мещается внутри стекового фрейма doStuff (). (5) doStuff () вызыва¬ ет метод до (), кото¬ рый выталкивается на вершину стека. Переменные х и z находятся в стековом фрейме до (). © до () вызывает метод crazy (), который теперь на¬ ходится на вершине стека и хранит в сво¬ ем стековом фрейме переменную с. 3) Метод crazy () завершается, и его фрейм покидает стек. Управление возвра¬ щается методу до () и продолжается со строки, следующей за вызовом craz у ().
А как же быть с локальными переменными, представляющими собой объекты? Напомним: переменная непримитивного типа со¬ держит ссылку на объект, а не сам объект. Вы уже знаете, где обитают объекты — в куче (heap). И неважно, где они объявлены или созданы. .ЛОТ A- иЙ, „ ник Пфслинник рдд,. г.тг.ц.лг,Каили,ц1^ШЛ ^ N лаетно ' (леске. снД м (\W н(1 'aV(HVf ТУ){■ ^явлена cmxm f * гг*Чо*н«я переменная (внц- ~>/,Н Ли>Пи/а и <н /vYWC пфкшнная ■ XT'fr’кишс,)- “нкт ассс- ‘ помирит (Uijnf. Без лишних «лов Если локальная переменная — это ссылка на объект, то на стек помещается только сама пере¬ менная, то есть ссылка (она же — «пульт дистан¬ ционного управления»). А сам объект, как и всегда, живет в куче, public class StackRef { public void foof() { barf(); ) public void barf() { Duck d = new Duck(); 1 ) (He) дурацкие вопросы ВопрОС: Спрошу еще раз: зачем нам вообще нужны эти стеки и кучи? Что мне это даст? И правда ли, что я должен в этом разбираться? OlMBeiM: Да, правда! И еще какая. Понима¬ ние базовых принципов работы стека и кучи в Java — это ключ к целому ряду вещей: области видимости переменных, создание объектов, управление памятью, а позже — потоки выпол¬ нения и обработка исключений. Последние две темы мы обязательно обсудим позже, но все остальное — уже здесь, в этой главе. Не переживайте: вам не нужно разбирать¬ ся в деталях конкретной JVM или архитектуре платформы. Достаточно понять основную идею, и она у вас уже под рукой — прямо на этих страницах. Прочитав их внимательно, вы заметите: все по¬ следующие темы, где затрагиваются стек и куча, начнут укладываться в голове гораздо легче. Однажды вы еще скажете: «Спасибо, что вы за¬ ставили меня выучить это!». ■ У Java есть две области памяти, о которых нам нужно знать: стек и куча. ■ Переменные экземпляра объявляются внутри класса, но за пределами метода. ■ Локальные переменные объявляются внутри метода или задаются в качестве параметров. • Все локальные переменные обитают в сте¬ ке, во фрейме соответствующего метода, в котором были объявлены. ■ Объектные ссылки работают точно так же, как переменные простых типов: если они объявляются локально, то уходят в стек. ■ Все объекты находятся в куче, независимо от того, объявлена ли их ссылка локально или в качестве переменной экземпляра.
Если локальные переменные в стеке, то где переменные экземпляра? Когда вы пишете строку new CellPhone (), Java выделяет в куче память для нового объекта. Сколько именно? Ровно столько, сколько нужно для хранения всех его переменных экземпляра. Потому что перемен¬ ные экземпляра находятся в куче — в объекте, которо¬ му принадлежат. Помните, что значения переменных экземпляра объекта хранятся в объекте. Если все переменные экземпляра — это примитивы, Java выделяет место для переменных экземпляра в зависимости от типа примитива. Для int требуется 32 бита, для long — 64 бит и т. д. Java не за¬ ботится о значении примитивных переменных; размер целочисленной переменной одинаков (32 бита) незави¬ симо от того, равно ли ее значение 32000000 или 3 2. Но если переменные экземпляра — это объекты? До¬ пустим, CellPhone HAS-A Antenna? Другими сло¬ вами, у CellPhone есть ссылочная переменная типа Antenna. Когда новый объект имеет переменные экземпляра, которые являются ссылками на объекты, а не прими¬ тивами, возникает вопрос: нужно ли объекту место для всех объектов, на которые он ссылается? Ответ; не совсем. Несмотря ни на что, Java должна выделить место для значений переменных экземпляра. Но помните, что значение переменной-ссылки — это не весь объект, а всего лишь пульт управления объектом. Так что, если в CellPhone переменная экземпляра объявлена как не примитивный тип Antenna, Java освобождает место в объектеСе11Р1юпе только для удаленного управ¬ ления Antenna (то есть ссылочной переменной), но не для самого объекта Antenna. Так когда же объект Antenna получает место в куче? Сначала нужно выяс¬ нить, когда создается сам объект Antenna. Это зависит от объявления переменной экземпляра. Если перемен¬ ная экземпляра объявлена, но ей не присвоен объект, то создается только место для ссылочной переменной (пульта управления). private Antenna ant; Никакого реального объекта Antenna в куче не созда¬ ется до тех пор, пока ссылочной переменной не будет присвоен новый объект Antenna. объект CellPhone Осыкт с iuJuau ннм ше жжшияЦха п^ашетншогс типа. Жсшять q\M этих luptmннмхjvuutiu/itтея <?ни/пуш ссштс сшита. ' объект CellPhone ( снкт С n^lLUtHHlU экуссиызум НС ryui- .иипшАигс шипа. ссгиАнимейся наЛп/еппа (ссгика. нс нс саиосыкт). Йоплпи Ас пс.шиши, сети ссгчАипс тцшшнию. нс нс п^шейшпе ей ссбскт. -InAnna. public class CellPhone { private Antenna ant; } Оспкт с подли нней экраннымjvi нс »уш.ии- тийссгс типа и путсиснмая .ЛпХеппа. кипе¬ ней »ушгАилм сссж Атст(?укди,и й сетчжт public class CellPhone { private Antenna ant = new Antenna() } private Antenna ant = new Antenna();
Чудо создания объекта Теперь, когда вы знаете, где обитают переменные и объекты, мы можем погрузиться в таинственный мир создания объектов. Следует запомнить три шага объявления и присвоения объектов: объявить переменную-ссылку, создать объект и присвоить объект ссылке. Но до сих пор второй шаг, на котором происходит чудо и новый объ¬ ект «рождается», оставался большой тайной. Приготовьтесь узнать факты из жизни объектов. Надеемся, вы не брезгливы. Duck myDuck = new Duck () ; Duck myDuck = new Duck () ; Объект Duck Три шага для объявления, создания и присвоения объекта. Объявляем ссылочную Ссылка типа Duck ф Создаем объект. этам. А Привязываем объект к ссылке. Duck myDuck ) new E Ссылка типа Duck
Мы действительно вызываем метод Duck()? Потому что выглядит это именно так х/ка намекают. ,nvl Duck myDuck = new Duck () 5SffoeAuut^^iuk()' Нет. Мы вызываем конструктор объекта Duck. Конструктор действительно выглядит и ведет себя как метод, но это все же не метод. Он отвечает за код, который выполняется, когда интерпретируется ключевое слово new. Иными словами, это код, который запускается при создании экземпляра класса. Единственный способ вызвать конструктор — указать перед именем класса ключевое слово new. JVM определит этот класс и запустит его конструктор. На самом деле это не единственный способ вызвать конструктор. С некоторыми ограничениями можно вызвать его внутри другого конструктора, но об этом мы поговорим позже. Но откуда взялся конструктор? Если его написали не мы, то кто? Вы можете создать конструктор для своего класса (скоро перей¬ дем к этой теме), но даже если вы этого не сделаете, компилятор сам создаст его для вас! Вот как выглядит конструктор по умолчанию, создаваемый компилятором: public Duck() { ) КОНСТРУКТОР - это особый метод, ко¬ торый запускается в момент создания объекта. Иначе го¬ воря, это тот самый код, который вы¬ полняется, когда вы используете оператор new для создания эк¬ земпляра определен¬ ного класса. Каждый класс, кото¬ рый вы пишете, имеет конструктор, даже если вы его явно не создавали. Если вы не определите свой конструктор, компи¬ лятор добавит кон¬ структор по умолча¬ нию без параметров. Заметили, что кое-чего не хватает? Чем конструктор отличается от метода? i тт давйг-“» Моя кг HrtnfujKnufu urun с именем класса. /^штейне. конструктора
Конструируем объект duck Ключевая особенность конструктора — он вы¬ полняется до того, как объект может быть при¬ своен ссылке. Это означает, что у вас есть шанс вмешаться и сделать все, чтобы объект был готов к использованию. Другими словами, прежде чем кто-то сможет воспользоваться пультом управ¬ ления объектом, у объекта есть шанс помочь сконструировать себя. В конструкторе для объ¬ екта Duck мы не делаем ничего полезного, просто демонстрируем последовательность событий. public class Duck { public Duck() { System.out.println } } вмешаться в создание объекта на этапе выполнения оператора new. ("Кря!"); public class UseADuck { public static void main (String[] args) { Duck d = new Duck(); 1 ^ It" File Edit Window Help Quack % java UseADuck Кря! Решать - вам! ’ .-^аточите карандаш "к Конструктор позволяет запрыгнуть в се¬ редину этапа создания объекта — в са¬ мый центр оператора new. Можете ли вы представить себе условия, в которых это было бы полезно? Какие из действий справа могут быть полезны в конструк¬ торе класса Саг, если автомобиль явля¬ ется частью гоночной игры? Отметьте те, для которых вы придумали сценарий. Инкрементировать счетчик для отслеживания количества созданных объектов класса. Назначить состояние, которое может меняться при каждом выполнении программы (информация о том, что происходит сейчас). Присвоить значение важной переменной экземпляра объекта. Получить и сохранить ссылку на объект, который сам создает объект. Добавить объект в ArrayList. Создать объекты внутри класса. (ваш вариант).
Инициализация состояния нового объекта Риск Большинство людей применяют конструктор для инициализа¬ ции состояния объекта, то есть для создания значений и при¬ своения их переменным объекта. public Duck() { size = 34; > Нет ничего плохого в том, чтобы сам разработчик класса Duck знал, каким должен быть объект по размеру. Но что, если вы хотите, чтобы размер конкретного утенка определял другой программист — тот, кто будет использовать ваш класс? Представьте, что у Duck есть переменная экземпляра size, и вы хотите, чтобы программист, работающий с вашим клас¬ сом Duck, определял ее значение для нового объекта. Как это сделать? Можно добавить в класс сеттер setSize (). Однако объект Duck на какое-то время останется без значения для пере¬ менной size, а программисту, создавшему объект, придется написать два выражения: одно — для создания экземпляра Duck, другое — для вызова метода setSize () . В коде, приведенном ниже, начальный размер нового объекта Duck устанавливается с помощью сеттера. public class Duck { int size; ^ public Duck() { System, out. println ("Кря! ") —JvfcHCN'fVj (Me) дурацкие вопросы B>OV\pOC: Зачем вообще писать свой кон¬ структор, если компилятор любезно делает это за меня? О твет: Если вам нужно выполнить какой-то код при создании объекта — напри¬ мер, задать начальные значения перемен¬ ных экземпляра или завершить подготовку объекта к работе, — придется написать свой собственный конструктор. Более того, если вы хотите, чтобы пользователь вашего класса передавал значения при создании объекта (например, имя, размер, скорость и т. д.), без собственного конструктора не обойтись. Даже если ваш конструктор будет пустым, вы все равно можете захотеть управлять тем, что происходит в момент создания. Иногда нужно даже просто вызвать конструктор су¬ перкласса — и это еще один повод написать свой (мы поговорим об этом совсем скоро). рОС-* Можно ли вызвать конструк¬ тор из метода? И вообще, разрешается ли писать метод с именем класса? Ответ-. Да, Java разрешает вам создать метод с именем, совпадающим с именем класса. Но это не конструктор! Конструкторы отличаются от обычных методов отсутствием возвращаемого типа. Метод должен иметь свой тип, а у конструктора его быть не может. public void setSize(int newSize) { size = newSize; ^ } public class UseADuck { public static void main(String[] aTQ ^ Duck d = new Duck(); } d.setSize(42) * Переменная экземпляра име¬ ет значение по умолчанию. Это О или 0.0 для числовых при¬ митивов, false для логических переменных и null для ссылок. ЖП fyHHKa™ т/шттт 71 этей'тсш киш Шкт). Ht и ««т пере шйнт sizd Ъ ит/ f мгс нет fWMeha ’ .Jfi раитем тшапйся на щ. и вдтс/м, применяющего к мЛ ' «у ншждимс гнать, гпи мание еаектаЮнск состой mrtmu: (ttvetia кон ( пруктсра и (нреш сеттер public Duck () ( } ?U public void Duck() { } Компилятор примет метод с именем, как у класса, но будет считать его обычным методом. А значит, путаницы — выше крыши. Лучше избегать таких совпадений и придер¬ живаться соглашений: методы называются со строчной буквы, классы — с прописной. Зо(Л рОС: Наследуются ли конструкторы? Допустим, я не пишу конструктор, но мой родительский класс его имеет. Что полу¬ чаю я? Ответ: Нет. Конструкторы не наследу¬ ются. Обо всем этом мы скоро поговорим подробнее.
Используем конструктор, чтобы задать важное начальное состояние объекта Duck" Если объект не готов к использованию без предварительной инициализации (напри¬ мер, переменные экземпляра еще не заданы), не давайте никому доступ к нему, пока объект полностью не настроен! Слишком рискован¬ но позволять создавать и использовать объект Duck, который еще не «собран», рассчитывая на то, что кто-то не забудет вызвать s е t S i z е (). А откуда вообще пользователю Duck знать, что по¬ сле создания объекта нужно обязательно вызвать метод-сеттер? Правильное решение — перенести всю инициали¬ зацию в конструктор. Просто создайте конструктор с аргументами — и всё: при создании объекта он сразу будет готов к работе. public class Duck { int size; public Duck(int duckSize) { System.out.println("Кря!"); size = duckSize; System.out.println("размер: Позвольте пользо¬ вателю создать новую утку и одновременно задать ее раз¬ мер — одним вызовом. Вызов new запускает конструктор Duck, и объект сразу инициа¬ лизируется. sssS»- Ucna ujijcu jH/mnuc арг^мсшпа tj. 1.ч иннн.щ/. шутит nefuj ' sizz. /Lute mo этих .можно dnjdimt .uimuj sdStztQ. " + size); iU.MUIR(U public class UseADuck { static void main (String[] args) { d = new Duck (42) ; I File Edit Window Help Honk | % j ava UseADuck Кря! размер: 42 Же, в * Это не означает, что какие-то другие состояния Duck не важны.
Упрощаем процесс создания объекта Duck. Убедитесь, что в вашем классе есть конструктор без аргументов Подумайте, что происходит, когда конструктор Duck принимает аргумент. На предыдущей странице показан единственный конструктор, и он требует передать цело¬ численное значение для переменной size. Это не критич¬ но, но создает трудности для программиста, который хочет создать объект Duck, не зная, какое значение установить. В такой ситуации может выручить значение по умолча¬ нию: оно позволит создать рабочий объект, даже если пользователь ничего не знает о параметрах. Допустим, вы хотите, чтобы пользователи класса Duck могли создавать объекты двумя способами: с указанием размера (через аргумент конструктора); без указания, чтобы переменной size присваивалось значение по умолчанию. Нельзя добиться этого с помощью одного конструктора. Помните: если у конструктора есть параметр, вы обязаны передать ему аргумент. Вы не можете просто сказать: «Если пользователь ничего не передаст, используем значение по умолчанию» — такой код не скомпилируется, если вызвать конструктор без аргумента. Зато вы можете поступить так: public class Duck { int size; public Duck(int newSize) if (newSize = 0) { size = 27; } else { size = newSize; } } vib? auitae Щ t\«Kuun vvt T u jWHW atvvtc- Два способа создать объект Duck: public class Duck2 { int size; public Duck2() { // размер по умолчанию size = 27; } public Duck2(int duckSize) ( // использование параметра duckSize size = duckSize; ) } Создание объекта, если размер известен: Duck2 d = new Duck2(15); Создание объекта, если размер не известен: Duck2 d2 = new Duck2 () ; Итак, для реализации идеи с двумя вариан¬ тами создания объекта Duck нужно написать два конструктора. Один будет принимать число, другой — нет. Если в вашем классе содержится несколько конструкторов, это означает, что они ПЕРЕГРУЖЕНЫ. Проблема в том, что такой подход требует от програм¬ миста знать: чтобы получить размер утки по умолчанию нужно передать 0. Не очень удобно, правда? А что, если другой разработчик об этом не в курсе? Или действитель¬ но хочет создать Duck с размером 0? (Предположим, что размер 0 допустим. Если нет — проверьте это прямо в кон структоре и не допускайте создание таких объектов.) В чем сложность? Вы не сможете точно понять, что имел в виду программист: он осознанно установил размер в 0 или просто хотел получить размер по умолчанию*1
Всегда ли компилятор создает конструктор без аргументов? Наверняка вы думаете, что можно соз¬ дать один конструктор с аргументами, а компилятор увидит, что у вас нет кон¬ структора без аргументов, и создаст его сам. Но это совсем не так. Компилятор поступит так только в том случае, если вы не создали ни одного конструктора. Если вы написали конструктор, который принимает аргументы, то и конструктор без аргументов при¬ дется создавать самостоятельно! Как только в вашем классе появляется хотя бы один собственный конструктор, компилятор говорит: «О, у тебя уже все под контролем! Значит, я больше не буду автоматически добавлять конструктор по умолчанию. Справляйся сам». Если в классе присутствуют сразу не¬ сколько конструкторов, то списки их аргументов должны различаться. Это касается как порядка следования аргументов, так и их типов. Пока они раз¬ личны, вы можете использовать несколь¬ ко конструкторов. То же самое относится и к методам, но об этом мы поговорим в другой главе.
Наличие перегруженных конструкторов означает, что в вашем классе несколько конструкторов. Чтобы скомпилироваться, каждый конструктор должен иметь уникальный список аргументов! Приведенный ниже класс будет работать, поскольку все пять кон¬ структоров имеют разные списки аргументов. Если бы существовало два конструктора, принимающих, скажем, по одному значению типа int, класс бы не скомпилировался. Имена параметров в данном случае значения не имеют: важен тип переменной (int, Dog и т. д.) и порядок их следования. Вы можете иметь два конструктора с пара¬ Sss?3= метрами одинаковых типов, но эти параметры должны размещаться в разном порядке. Конструктор, который принимает тип String, за которым следует int, — это не то же самое, что конструктор, кото¬ рый принимает int за которым следует String. public class Mushroom { public Mushroom (int size) public Mushroom ( ) { } - щж111' { } KcUfdC f^ HtT d,Hrmu нише a tre a и/еппнп о iqa 4% n - „ (?(.ии<)нис (ftu cm&i. г Кси1‘г public Mushroom (boolean isMagic) { ) i fpublic Mushroom (boolean isMagic, int size) public Mushroom (int size, boolean isMagic) ■one . CH Yc IH ( м ajmj. i twnwLrtuu ctjHniv тина, кашшлятф нс сщг гм [уултипй такие кенгт^укте^м Без лишних слов — никла г.'м сип ш/кп. aiKc>«"tr>nv Фииние'нтиГи /,т*1;ГГИ~ тм. но ■ Переменные экземпляра живут внутри объекта, которому принадлежат, в куче. ■ Если переменная экземпляра — это ссылка на объект, и сама ссылка, и объект, на который она указывает, находятся в куче. ■ Конструктор — это код, который запускается вместе с оператором new при создании объекта опреде¬ ленного класса. ■ Имя конструктора должно совпадать с именем класса, и он не должен иметь тип возвращаемого значения. ■ Конструктор можно использовать для инициализа¬ ции состояния создаваемого объекта, то есть его переменных экземпляра. ■ Если в классе отсутствует конструктор, компилятор создает конструктор по умолчанию. ■ Конструктор по умолчанию всегда не имеет аргументов. Если вы добавите в класс хотя бы один конструк¬ тор, компилятор уже не будет создавать конструк¬ тор по умолчанию. Если в вашем классе есть конструктор с аргумен¬ тами, а также нужен вариант без аргументов, его придется добавить самостоятельно. Желательно всегда предоставлять конструктор без аргументов, чтобы упростить работу другим раз¬ работчикам. Можно задать значения по умолчанию. Перегрузка конструкторов означает, что в классе присутствует более одного конструктора. Перегруженные конструкторы должны отличаться списками аргументов. Нельзя объявить два конструктора с одинаковыми наборами аргументов (в том числе по типам и по¬ рядку следования). Переменным экземпляра присваиваются значения по умолчанию: 0,0.0, false - для примитивов, null — для ссылок.
аточите карандаш -► Вам решать. Сопоставьте вызовы new Duck() с конструк¬ тором, который запускается при создании экземпляра. Мы привели в качестве примера самый простой вариант. class Duck { private int kilos = 6; private float floatability = 2. IF; private String name = "Generic"; private long[] feathers = {1, 2, 3, 4, 5, 6, 7); private boolean canFly = true; private int maxSpeed = 25; public class TestDuck { public static void main(String[] args) { int weight = 8; float density = 2.3F; String name = "Дональд"; long[] feathers = {1, 2, 3, 4, 5, 6); ^ boolean canFly = true; int airspeed = 22; Duck[] d = new Duck[ d[0] = new Duck() d[l] = new Duck(density, weight); public Duck() { |System.out.println("THn 1 утки") public Duck(boolean fly) { canFly = fly; System.out.println("THn 2 утки") ) public Duck(String n, long[] f) { name = n; feathers = f; System.out.println("тип 3 утки") } d[2] = new Duck(name, feathers); d[3] = new Duck(canFly); d[4] = new Duck(3.3F, airspeed); d[5] = new Duck(false); d[6] = new Duck(airspeed, density); public Duck(int w, float f) { kilos = w; floatability = f; System.out.println("тип 4 утки"); ) public Duck(float density, int max) { floatability = density; maxSpeed - max; System.out.println("TPin 5 утки") ; } (He) дурацкие вопросы ВОЮ pOC: Раньше вы говорили, что удобно иметь конструктор без аргументов, чтобы можно было задать значения по умолчанию для «пропущенных» параметров. Но бывают ли случаи, когда без аргумен¬ тов ну совсем никак? Неужели бывают классы, кото¬ рым не стоит иметь конструктор без параметров? Ответ: Вы попали в точку. Иногда наличие кон¬ структора без аргументов просто не имеет смысла. Вы и сами можете это увидеть, если заглянете в Java API: некоторые классы действительно не предлагают такую опцию. Возьмем, к примеру, класс Color. Он... ну, представляет цвет. Вы используете объекты Color, чтобы, напри¬ мер, установить цвет шрифта на экране или окрасить кнопочку в красивый оттенок весенней зелени. Когда вы создаете экземпляр Color, вы должны однозначно указать, какой именно это цвет — шоколадный, небес¬ но-синий, красный цвета паники или что-нибудь в этом духе. Например: Color с = new Color (3,45,200); Здесь мы задаем цвет в формате RGB (Red-Green-Blue). Класс Color и его фокусы мы подробно рассмотрим в главе 15. А что будет, если вызвать new ColorO без параметров? Ну... Color с = new Color(); Ничего не выйдет! Компилятор рас¬ теряется: «Эй, тут нет подходящего конструктора! Я не знаю, какой цвет вы хотите!» Разумеет¬ ся, разработчики могли бы сказать: «Хорошо, пусть по умолчанию будет чудесный лилово-сиреневый». Но здравый смысл по¬ бедил. Цвет — слишком важная штука, чтобы выбирать его случайно. Поэтому, если вы хотите цвет — укажите его! Иначе никакого Color вам не видать. I File Edit Window Help StopBemgStupi^™" cannot resolve symbol constructor Color() location: class java.awt. Color Color c = new Color();
Экспресс-обзор: четыре главных факта о конструкторах 0 Конструктор — это код, который выпол¬ няется, если перед типом класса указан оператор new. Duck d = new Duck(); Конструктор обязан иметь то же имя, что и его класс, и не должен содержать тип возвращаемого значения. public Duck(int size) { } 0 Если вы не укажете в своем классе конструктор, то компилятор создаст конструктор по умолчанию без аргументов. public Duck() { } ® Вы можете объявить в классе несколько конструкторов, если списки их аргументов различаются. Когда в классе определено больше одного конструктора, речь идет о перегрузке конструкторов. public Duck() { } public Duck(int size) { } public Duck(String name) { } public Duck(String name, int size) { } * Разум рулит: Регулярные тренировки с этими вопросами увеличили нейро¬ ны у программистов на 42%! А вы знаете, что говорят обольших нейронах... Как насчет родительских классов? Должен ли конструктор Canine запускаться при создании объекта Dog? Обязан ли абстрактный класс содержать кон¬ структор? Рассмотрим эти вопросы на следующих страницах, а пре¬ жде, чем двигаться дальше, подумайте, как ведут себя кон¬ структоры из суперклассов*. (Не) дурацкие вопросы ^ОПрОС: Обязательно ли конструк¬ торы должны быть публичными? Омве W): Вовсе нет. Конструкторы могут иметь любой из уровней доступа: public, protected, private или доступ по умолчанию (то есть вообще без модифи¬ катора). Подробнее про это поговорим в Приложении Б. д>ОПр ОС: А зачем вообще нужен приватный конструктор? Никто же не сможет его вызвать! То есть и соз¬ дать объект не получится? Омвепл: Почти угадали, но не совсем. Приватный конструктор действительно делает невозможным создание объекта извне, но внутри са¬ мого класса все по-прежнему доступ¬ но. Это значит, что только сам класс может создавать свои объекты. Звучит запутанно? Терпение! В следу¬ ющей главе мы покажем, как и зачем это работает. Подсказка: даже без доступа извне такой класс может быть невероятно полезен.
Погодите-ка... мы ведь так и не обсудили суперклассы, наследование и их связь с конструкторами! Вот тут-то и начинается самое интересное. Помните объект Snowboard, который мы обсуждали в предыдущей главе? У него было ядро, обернутое слоями, — и один из этих слоев был частью клас¬ са Ob j ect. Смысл в том, что каждый объект хранит не только свои собственные переменные экземпляра, но и все переменные, унаследованные от его суперклассов. А это значит, что даже если вы написали абсолютно новый класс, он все равно наследует переменные как минимум от класса Ob j ect — по¬ тому что любой класс в Java расширяет Object. Поэтому, когда создается объект (а создается он только при вызове оператора new — других спосо¬ бов нет), в куче выделяется память под все переменные экземпляра — и те, что вы объявили в своем классе, и те, что пришли от суперклассов. Подумайте об этом. Если у суперкласса есть приватная переменная и соответствующий метод-сеттер, эта переменная все равно существует в объекте. То есть объект содержит всю «наследственную память»: он слое¬ ный, как луковица. Рисунок ниже поможет представить все это наглядно. Создаваемый объект там представлен обер¬ нутым в слои суперклассов. Object Fooa; int b; int c; equalsQ getClassQ hashCode() toStringQ 43- Snowboard Foo X Foo у intz tum() shred() getAirQ loseControlQ V с (Пента есть моуи.шннме ж t'tun.iM pa. угш с cam. они. шшт- / сулирсьаны npy.imnujn. )mu ncjvmtHH no схшантся кажут pay Ki /cja пияСшапт >ку. им. ыр . и<с,ш подкласса.. JDa. тсхтшгки этснеШСГШЩШтре- .иеннме класса ( " но наж и не тронеунать, как они успешны (7ш\ инкапсулирована. ауттоп. ш нас это плесто «АщтЦен но суля нас это прсапс «( нуinf rmu». cKfwтшуа.шписрши У Snowdcaid сото сбои т^еионнпо жркшелмра. Лсэпиму nfui ссууа- нмм. объекта этого класса (? па¬ мяти у. шли г пт/ суоотмтогно места как у\м его со&т&яных ги^&ишнш; так н а ш (tax уши: леуо&шшх — от Ofljtcl и, Ауиил но, cjfijiHX суперклассов. /) куй Seen OJDUK объект — -huie&arc/ lie no- .шик куа ну codcm&HHvtc класса он туржшп Э1ЫШНМИ классаJj^jecL ,‘icpct плурп реумацетъея imJu ионнпо оссих к. шссеС.
Роль конструкторов суперкласса в жизни объекта Все конструкторы в иерархии наследования объекта должны запускаться при создании этого объекта. Это означает, что у каждого суперкласса есть кон¬ структор (ведь он присутствует в любом классе), и каждый из этих конструкторов вызывается при создании экземпляра подкласса. Оператор new играет ключевую роль: именно он запускает всю цепочку вызовов конструк¬ торов. Даже у абстрактных классов есть конструкторы. Да, вы не можете напрямую создать экземпляр абстрактного класса через new, но если этот класс являет¬ ся суперклассом, его конструктор все равно будет выполнен при создании экземпляра подкласса. Конструкторы суперклассов запускаются, чтобы инициализировать часть объекта, унаследованную от родительского класса. Не забывайте: подкласс может использо¬ вать методы суперкласса, которые зависят от его переменных. Чтобы объект был полностью сформирован, нужно корректно построить и его «наследственные» части — переменные инициа¬ лизируются, методы получают доступ к нужному состоянию. Даже если переменные суперкласса Animal сдела¬ ны private и не наследуются напрямую подклас¬ сом Hippo, они все равно используются в методах Animal, а значит, становятся критичными для работы Hippo. Как только запускается конструктор подкласса, первым делом он вызывает конструктор супер¬ класса — и так по цепочке вверх, пока не будет вы¬ зван конструктор самого верхнего класса ОЬ j ect. CijuH сСикт 6 Kip. Новый объект Hippo связан отноше¬ нием IS-А с классами Animal и Object. Это значит, что при создании экзем¬ пляра Hippo необходимо также соз¬ дать его «наследственные» части — те, что относятся к Animal и Object. Этот процесс называется связывани¬ ем конструкторов — или формирова¬ нием цепочки вызовов, где каждый конструктор в иерархии отвечает за инициализацию своей части объекта. Дальше мы разберемся, как именно происходит вызов конструкторов суперклассов и что делать, если они принимают аргументы.
Создавая объект Hippo, вы наследуете свойства и методы классов Animal и Object public class Animal { public Animal() { System.out.println("Создание животного"); > } public class Hippo extends Animal { public Hippo() { System.out.println("Создание бегемота"); } } public class TestHippo { public static void main(String[] args) { System.out.println("Запуск..."); Hippo h = new Hippo(); } } Учитывая иерархию классов в приведенном выше коде, мы можем пошагово описать процесс создания нового объекта Hippo. в^Заточите карандаш \ Каким будет результат выполнения программы? Что напечатает програм¬ ма TestHippo с помощью кода, при¬ веденного слева? Вариант А или Б? Ищите ответ внизу страницы. | File Ed^Mndo^^el^^wea^^" | А % java TestHippo Запуск... Создание животного Создание бегемота ъ iFiieEdit WindowHel^Swear % java TestHippo Запуск... Создание бегемота Создание животного В коде из другого класса написано new Hippo (), по¬ этому конструктор Hippo () помещается во фрейм на вершине стека. ф Hippo () вызывает конструктор супер¬ класса Animal () и помещает его на вершину стека. Animal () вызывает конструктор супер¬ класса Object() и помещает его на вершину стека, так как Animal — это подкласс Object. ® Метод Ob j ect () за¬ вершается, и его стеко¬ вый фрейм удаляется из стека. Выполнение про¬ граммы переходит к кон¬ структору Animal () и продолжается со строки, следующей за вызовом конструктора суперкласса. •KiMadau Aioged гэбггЬэвее |вш|и\/ doixAdiDHOx в wiaadau из1ЭЕ01че1ча Ooddin doixAdioHoy 'vaeaio
Как вызвать конструктор суперкласса ? Вы могли бы подумать, что если конструктор Duck расширяет Animal, то внутри него явно вызывается Animal () . Но все устро¬ ено немного иначе: public class Duck extends Animal { int size; public Duck(int newSize) { “^Animal () ; 4 fyjl T size = newSize; • J(*K > Уи<*стся/ Единственный способ вызвать конструктор суперкласса — с помощью метода super (). Именно так — метод super () вызывает конструктор суперкласса. Вот странно, да? public class Duck extends Animal { int size; public Duck(int newSize) { super () ; 4:— size = newSize; > > Вызов super () в вашем конструкторе помещает конструк¬ тор суперкласса на вершину стека. А что делает этот кон¬ структор? Вызывает конструктор своего суперкласса. И так продолжается, пока на вершине стека не окажется конструк¬ тор класса Object. Как только Obj ect () закончит работу, он удаляется из стека, и управление переходит к следующему конструктору в цепочке — тому, кто его вызвал. Он тоже завершает свою работу и уходит, и так далее, пока до вершины не доберется первый конструктор, с которого все началось. Только тогда он сможет завершить свою работу и дать старт полноценно¬ му объекту. Как получилось, что мы ра¬ нее обходились без явного вызова superO? Вы, вероятно, уже догадались. Наш добрый друг компилятор добавляет вызов superO авто¬ матически, если мы сами этого не сделали. То есть компилятор дважды вовлечен в процесс создания конструкторов: ф Если вы не создали ни одно¬ го конструктора. Компилятор сам его добавит следую¬ щим образом: public ClassName() { super(); ) (5) Если вы предусмотрели конструктор, но не указали вызов super(). Компилятор поместит вызовы superO в каждый ваш перегруженный кон¬ структор*. Эти вызовы всегда выглядят так: super(); Компилятор всегда добавляет вызовы superO без аргументов. Если у родительского класса есть пере¬ груженные конструкторы, то будет вы¬ зван лишь конструктор без аргументов. * Если только конструктор не вызывает другой перегруженный конструктор (о чем идет речь, вы поймете через несколько страниц).
Могут т потомки появиться Очень странно. Я никак не мог по¬ явиться на свет раньше своих родителей. Это как-то неправильно. раньше родителей? Если провести аналогию «родители — дети» с реальной жизнью, становится понятно, какой из классов должен по¬ явиться первым. Части объекта, относящиеся к су¬ перклассу, должны быть полностью сформированы (полностью построены), прежде чем можно будет построить части подкласса. Помните, что объект подкласса может зависеть от элементов, наследуемых от суперкласса, поэтому важно, чтобы они были завершены. Без этого не обойтись. Конструктор суперкласса должен завершить работу раньше кон¬ структора подкласса. Посмотрите еще раз на стеки, и вы увидите, что, хотя конструктор Hippo вызывается первым (он первый в стеке), он завершается последним! Каж¬ дый конструктор подкласса немедленно вызывает конструктор своего суперкласса, пока конструк¬ тор Obj ect не окажется на вершине стека. Затем конструктор Obj ect завершает работу, и мы спускаемся вниз по стеку к конструктору Animal. Только после завершения работы конструктора Animal мы возвращаемся вниз, чтобы завершить работу конструк тора Hippo. По этой причине: Вызов super() должен быть расположен в самом начале каждого конструктора*! — Возможные конструкторы для класса Воор |7[ public Воор () { super(); } ^С ( п/у' (ч ине. пши. и, У |7| public Воор() { } [71 public Boop(int i) { f Teject теш пря- _ (ил/но: калтилл- nwf\I пешеш/жт (?щсвsuhttO (капетй neptfew . cntjvuncjia. /((HjUl&UtnJH Такси Kuj mdy - межпо скслит- 0 public Boop(int i) { iUjuTamtJBt\ HI size = l; } size = i, super(); мажете, pa?. ш стшпья(тш й\лТ мера0 паем ijpipux * Исключение из правил.
Конструкторы суперкласса с аргументами Как быть, если конструктор суперкласса имеет аргументы? Можно ли передать что-нибудь в вызов super () ? Конечно. Если бы это было невозможно, вы бы никогда не смогли расширить класс, у которого нет конструктора без аргументов. Представьте себе такой сценарий: у всех животных есть имена (паше). В классе Animal создан метод getName () который возвращает значение переменной паше. Переменная экземпляра приватна, но подкласс (в данном случае Hippo) на¬ следует метод getName (). Проблема заключается в том, что у класса Hippo есть метод getName () (благодаря наследованию), но у него нет переменной экземпля¬ ра name. Класс Hippo вынужден зависеть от Animal, чтобы сохранить перемен¬ ную экземпляра name и возвращать ее значение при вызове метода getName () . Но как часть класса Animal получает имя? Единственная связь между Hippo и Animal — это вызов метода super (). Именно здесь Hippo передает имя своей части Animal, чтобы тот мог сохранить его в переменной name, которая помечена как private. public abstract class Animal { private String name; 4r~ public String getName() return name; } fja cCUKtnti л/цекм Antmrt (вклипля nujKMcea) public Animal(String theName) name = theName; ^ > nam. Animal private String name Animal(String n) String getName() Hippo Hippo(String n) [другие методы, специфичные для Hippo] Часть меня, унаследованная от Animal, должна знать свое имя. Поэто¬ му в своем конструкторе Hippo я беру это имя и передаю его в super)), чтобы Animal мог со¬ хранить его у себя. public class Hippo extends Animal { ) public Hippo (String name) { „пИЦктФ ^'PPl super (ПМ.); ****¥*«> hwnuiC public class MakeHippo { public static void main(String!] args) Hippo h = new Hippo("Баффи")- ^ System.out.println(h.getName()) Кфе/MtM имя «ЮшрЛш» KCHCtnjujKttlCftU Щук, типшт •шпиц cuffiatntO, JfHarJuk&imtiu
Вызов одного перегруженного конструктора из другого Как быть с перегруженными конструкторами, которые различаются только набором аргументов? Вы знаете, что дублировать код в каждом конструкторе нежелательно (сложно поддерживать и т. д.). По этой причине лучше поместить часть кода (включая вызов super ()) только в один из перегруженных конструкторов. Вы хотите, что¬ бы любой конструктор, который будет вызван первым, инициировал вызов настоящего конструктора и по¬ зволил ему завершить все работы по конструированию объекта? Это просто: нужно лишь написать this (). Или this (aString). Или this (27, х). Иными словами, ключевое слово this — это ссылка на текущий объект. Вы можете использовать выражение this () только вну¬ три конструктора, и оно должно быть указано в самом его начале! Возникает проблема, не так ли? Ранее мы говорили, что вызов super () должен размещаться в самом начале кон¬ структора. Что ж, похоже, мы стоим перед выбором. Каждый конструктор может иметь либо вызов super(), либо this(), но не оба сразу! Вам придется выбирать, какой конструктор вызывать, исходя из того, какие значения у вас есть, какие необхо¬ димо установить и какие конструкторы доступны в этом классе или в суперклассе. 10 Используйте thisO для вызова конструктора из другого перегруженной конструктора в рамках того же класса. Вызов thisO может быть использован только б конструкторе И указывается первым. Конструктор может содержать вызов superO ИЛИ thisO, но не оба одновременно! import java.awt.Color; class Mini extends Car private Color color; public Mini() { this(Color.RED) ; } WHHutf -&SSP! public Mini(Color c) super("Мини"); color = c; // дополнительная } public Mini(int size) { this(Color.RED); super (size) ; %U>n КШ Ш ошим- > nfifunaif!/I сунем ктсгмщкшш не. ноя. бы тбапм, м Ш(), и Щт() так как ф быхба услжнп (ituHi rtetitfcti строкой. кснсщщктеца! [javac Mini.java Mini.java:16: call to [be first statement in super must constructor super () ; A
#^Заточите карандаш Вам решать. Некоторые конструкторы в классе SonOfBoo не скомпилируют. Попробуйте определить, какие кон¬ структоры не допустимы. Соотнесите ошибки компи¬ лятора и конструкторы SonOfBoo, которые их вызвали, проведя линию от ошибки компилятора до «плохого» конструктора. public class Boo { public Boo(int i) { } public Boo(String s) { } public Boo(String s, int i) { } } class SonOfBoo extends Boo { public SonOfBoo() { super("6y"); } public SonOfBoo(int i) { super("Фред"); } Параметры Вбить в голову Розы красивы, фиалки милы, Сначала родители, потом уже вы. Части объекта, принадлежащие суперклас- су, должны быть полностью сфортроюны ореаде „ем ноши о6ьо„ „от„зса суюлс.оьлть. Тонно г„же, ЫЧ1вбеиок / НРГМЛЖРГППОО,.г, микаь не сможет появиться раньше своих родителей. Т.„ гм WindOTHjWP %javac SonOfBoo.java cannot resolve symbol symbol : constructor Boo _ . ova (java.lang-String,java. 1 Cfri П(т1 public SonOfBoo(String s) { super (42) ; } public SonOfBoo(int i, String s) { ) public SonOfBoo(String a, String b, String super (a, b) ; > public SonOfBoo(int i, int j) { super("мужик", j) ; ) public SonOfBoo(int i, int x, int y) { super(i, "звезда"); %javac SonOfBoo.java cannot resolve symbol symbol:constructor Boo() )
Мы выяснили, как появляется объект, а сколько он существует? Время жизни объекта зависит от ссылок на него. Пока су¬ ществует действующая ссылка на объект, то и сам объект продолжит существовать в куче. Если ссылка «умирает» (об этом чуть позже), объект «умирает» вслед за ней. Итак, раз объект зависит от переменной-ссылки, то сколько существует эта переменная? Это зависит от того, является ли переменная локальной или переменной экземпляра. В приведенном ниже коде показано время жизни локальной переменной. В примере это примитив, но для ссылочной переменной этот пример тоже подходит. public class TestLifeOne { public void read() { int s = 42; sleep () ; •Же, (Ж^ннач f Mm44ruul(! , (СШПт public void sleep 0 { %>*z$**r* sftf1 О frr* '"««.«««« Lift. Xu mjdft сшСа скажтш т kJ (Мает (шпени, m in mu,,,,.... пг г л учиит а пет, read!) УГ ■,шЮja&mmvM ц««ипвд тжшит' ГТт Осташтгя та икс ( 41 // тли §. г Ufii * © Локальная переменная суще¬ ствует только внутри метода, в котором была объявлена. public void read() { int s = 42; // 's' можно использовать толь- // ко в пределах этого метода. // Когда метод завершится, II 's' полностью исчезнет. ) Переменная s может быть использована только внутри метода read(). Иными сло¬ вами, она находится в области видимо¬ сти только своего метода. Никакой другой код в классе (или за его пределами) не видит переменную s. (2) Переменная экземпляра существует до тех пор, пока существует объект. Если объект все еще «жив», то м живы и его переменные. public class Life { int size; public void setSize(int s) { size = s; II 's' исчезнет в конце // этого метода, но 'size' // можно использовать // в любом месте класса } } Переменная s (на этот раз параметр мето¬ да) находится исключительно в области видимости метода setSize(). Но переменная экземпляра size существует, пока суще¬ ствует объект, — она не зависит от метода.
Различие между существованием и видимостью локальных переменных: Существование Локальные переменные существуют до тех пор, пока их фрейм находится в стеке. Другими слова¬ ми, пока метод не завершит свою работу. Видимость Локальные переменные находятся в области видимости только того метода, в котором были объявлены. Когда он вызывает другой метод, переменные продолжают существовать, но на¬ ходятся за пределами видимости (пока метод не продолжит свою работу). Переменную можно использовать только когда она находится в текущей области видимости. public void doStuff() { boolean b = true; go(4) ; ) © о public void go(int int z = x + 24; crazy(); // прочий код x) ) О public void crazy () { char c = 'a’; > { Давайте рассмотрим, что происходит в стеке при вызове метода doStuf f (). Q Метод doStuff () помещается в стек. Переменная b существует и на¬ ходится в области видимости. Метод до () перемеща¬ ется на вершину стека. Переменные х и z су¬ ществуют и находятся в области видимости; Ь существует, но вне области видимости. На вершину стека попадает метод crazy (), а пере¬ менная с начинает свой жизненный цикл и попадает в область видимо¬ сти. Остальные три переменные продол¬ жают существовать, но находятся вне области видимости. Метод crazy () за- вершает свою рабо¬ ту и покидает стек, поэтому переменная с уходит из области видимости и перестает существовать. Метод до () продолжает свою работу с того места, на котором она была прервана; пере¬ менные х и z все еще существуют и опять становятся видимыми. Переменная b также существует, но ее не видно,пока метод до () не завершится. Пока локальная переменная существует, ее состояние сохраняется. Например, во время пребывания метода doStuff () на вершине стека переменная Ь хранит свое значение. Но она может быть использована толь¬ ко тогда, когда фрейм метода doStuff () находится в самом верху стека. Иными словами, разрешается задействовать локальную переменную, только если ее метод в этот момент выполняется (в противном случае она ждет, пока завершатся все фреймы, нахо¬ дящиеся выше в стеке).
По поводу ссылочных переменных Для примитивов и ссылок действуют одни и те же правила. Ссылоч¬ ная переменная может быть использована, только если она находится в области видимости. Это значит, что нельзя управлять объектом, пока ссылка не вернется в текущую область видимости. Основополагающий вопрос звучит так: Как жизнь переменной влияет на жизнь объекта? Объект живет до тех пор, пока существуют действующие ссылки на него. Если ссылка перестает быть видимой, но все еще существует, то объект, на который она указывает, остается в куче. И вот здесь вам следует спросить: «Что происходит, когда фрейм, содержащий ссылку, покидает стек в момент, когда метод завершил работу?» Если к этому объекту вела единственная живая ссылка, то с ее смер¬ тью объект становится недоступным в куче. Ссылочная переменная уничтожается вместе с фреймом, поэтому о таком объекте можно совершенно точно забыть. Самое сложное — определить, когда объект становится пригодным для сборки мусора. Когда наступает такой момент, можно не переживать о занимаемом впустую месте. Если в программе возникнет недостаток свободной памяти, сборщик мусора удалит некоторые (или все) подходящие для этого объекты. Вы можете израсходовать всю память, но пред¬ варительно все подходящие объекты будут уничтожены. Ваша цель — делать объекты недоступными (пригодными для сборщика мусора) после завершения работы с ними. Жизнь объекта не имеет никакого смысла и никому не интересна без ссылки на него. Он ничего не сделает, командовать им нель¬ зя, он лишь прожига¬ ет зря драгоценные биты памяти. Недоступный объект рано или поздно по¬ падет в поле зрения сборщика мусора и будет удален. Если программа будет сохранять ненужные объекты, то сборщик мусора останется без работы, а ваша программа рискует по гибнуть мучительной смертью, израсходовав всю память. Объект становится до¬ ступным для сборщика мусора, когда исчезает последняя ссылка на него. Три способа избавиться от ссылки на объект: (Т) Ссылка навсегда перестает быть видимой '"’ЙЛ'ФЛе. Life(); } в кошм .utmoja. (5) Ссылке присваивается другой объект. Life z = new Life О J[a\(?t3W ссмкт аиаксвшпся z = new Life (); <; никспмтнам. KCMCl z сСщп шапсяс ц|щш,и гщектсв. (?) Ссылке явно присваивается null Life z = new Life() z = null; ^ 31cJ\(?ttk «Сект становится ма(г спшпшмд, шаа z
Объектоцид №1 Перманентное ис¬ ключение ссылок из области види мости bariQ помещается в стек, где объявляется ссылочная переменная и новый объект, кото¬ рый с ней связывается. Объект создан в куче, а ссылка действитель¬ на и находится в обла¬ сти видимости. Ковмм ицщ y.Wk t\c nacjai.m (? Kim», w qc tntx tW mud mncd- нлипс.ч ссылка d aocmumui u на - X'ummM 6 (xtwcmu Сш t". «сети Лоэталл ij y)iuk пшлаипгл <HU| HHtV.H. barff) завершает свою работу и покидает стек. Его фрейм унич¬ тожается, поэтому d исчезает. Выполнение программы возвраща¬ ется к методу fooff) без ссылки d. ■^рсишная due-tea ъг, Френи.штщее fam) иаалжтся и a стека: vefteum JDwck. становится лакомым кистком fW ccejuu,u Kit муара.
Объектоцид №2 Присвоение другого объекта ссылке. public class ReRef { Duck d = new Duck() public void go() { d = new Duck(); > d уутсШашся нс&ш Лцш.т uuKm/)urfc WKUfumc* Щ,йш MkmJDuck metxtfu как (кмфтК
Объектоцид №3 Явное присвоение ссылке значения null public class ReRef { Duck d = new Duck(); public void go() { d = null; } Что значит null Присваивая ссылке значение null, вы «сбрасываете» настройки пульта управ¬ ления. То есть у вас оказывается пульт, не управляющий телевизором. Такая ссылка содержит битное значение null (мы незнаем, что это за биты, — за них отвечает JVM). В реальной жизни можно сколько угод¬ но нажимать кнопки такого пульта, но результат вы не получите. В Java кнопки нажимать нельзя (то есть использо¬ вать оператор доступа) в сочетании с ссылкой null. JVM знает (еще во время выполнения программы, а не на стадии ее компиляции), что, хотя вы и ожидаете услышать лай, но собаки то у вас нет! Если вы используете оператор «точка» в сочетании с ссылкой null, то во время выполнения програм¬ мы будет выброшено исключение NullPointerException. Вся информация об исключениях приводится в главе 13. KoQtm cAtKmXW k, на котищи стелет¬ ся d, поселяется(?кик. Яоексдькцо/ — это неистинная экжипляра. а.тктУ)пск (идет ашцепш(?шп1 до тех1 пор. пока ешеутйрп по^олиМий его оаокт RoRef. сот телекс... -)т(тоедектУ)и^отаии пижон, поскольку ао етлке п/ше-Аои лют пт тШ лрисвинш перни иной d литни.я пи (foe налоги* но Н(Л1Прогро1люшр( (ипшо.му пиление а иста н им он н сг о ипраКилим. /in дама но. и ажоте. иена а /давать Кисете с d оператор,«тетка», пока эепатомлкл но нади Cipjcin шунакна осиктд
Беседы у камина ffljw Сегодня у нас в гостях переменная экземпляра и локальная переменная очень спокойно обсудят проблему жизни и смерти. Локальная переменная Переменная экземпляра Пожалуй, начнем с меня, так как я играю более важную роль для программы, чем моя собесед¬ ница. Мое предназначение — поддерживать объект (как правило, на протяжении всей его жизни). В конце концов, что такое объект без состояния? И что такое состояние? Это значе¬ ния, хранящиеся в переменных экземпляра Поймите правильно: роль, которую вы играете в методе, совершенно очевидна. Но ваша жизнь слишком уж коротка. Мгновение. Вот почему вас еще называют временными пере¬ менными. Приношу свои извинения. Я вас понимаю. Я никогда не думала о вашей жизни с такой точки зрения. Чем вы занимаетесь, когда рабо¬ тают другие методы и приходится ждать, пока ваш фрейм не поместят на вершину стека? Я ценю вашу точку зрения и, несомненно, осознаю важность состояния объекта, но мне бы не хотелось вводить зрителей в заблужде¬ ние. Локальные переменные имеют большое значение. Вы же говорили: «В конце концов, что такое объект без поведения?» И что такое поведение? Алгоритмы, реализованные в ме¬ тодах. Без локальных переменных эти алго¬ ритмы не смогут работать, и это непреложная истина. В моей среде словосочетание «временная пере¬ менная» считается унизительным. Я пред¬ почитаю такие синонимы, как «локальная», «стековая», «автоматическая», или выражение «с ограниченной видимостью». Как бы то ни было, я и мои коллеги дей¬ ствительно долго не живем, да и жизнью это сложно назвать. Сначала нас помещают в сте¬ ковый фрейм вместе с другими локальными переменными. И затем, если метод, к которому мы относимся, вызывает другой метод, то над нами появляется иной стековый фрейм. А если новый метод вызовет еще один метод... и т. д. Иногда приходится ждать целую веч¬ ность, прежде чем другие методы на вершине стека завершат свою работу, чтобы мы могли продолжить.
Переменная экземпляра Локальная переменная Абсолютно ничем. Это как анабиоз — долгий сон, в который вводят героев фантастических фильмов, путешествующих на дальние расстояния. Наша жизнь будто стоит на паузе, мы просто сидим в ожидании. Пока наш фрейм внизу стека, мы находимся в без¬ опасности, но когда он опять продолжает работу, мы испытываем смешанные чувства. С одной стороны, мы опять становимся активными. С другой — мы неумо¬ лимо движемся к завершению своих коротких жизней. Чем дольше работает наш метод, тем ближе мы к его концу. Все мы знаем, что происходит потом. Я когда-то видела учебный фильм на эту тему. Конец довольно жестокий. Когда метод до¬ стигает своей закрывающей фигурной скобки, фрейм буквально выбрасывается из стека! Это И не говорите. В компьютерных науках используется действительно обидно. термин вытолкнул: например, «фрейм был вытолкнут из стека». Как будто это развлечение или экстремаль¬ ный спорт. Вы все видели в фильме. Лучше поговорим о вас. Я знаю, как выглядит мой скромный стековый фрейм, но где живете вы? Я обитаю в куче вместе с объектами. Точнее, не с объектами, а в объекте, чье состояние я храню. Стоит отметить, что в куче можно роскошно жить. Многие из нас испытывают чувство вины, особенно накануне праздников. Гипотетически все происходит так: будучи переменной объекта Collar, отданного на съедение сборщику мусора, я, как и все переменные этого объекта, несомненно буду порезана на мелкие кусочки. Но меня уверили, что такого почти никогда не случается. Но вы не всегда живете так же долго, как и объект, в котором объявлены, правильно? Допустим, есть объект Dog с переменной экземпляра Collar. Пред¬ ставьте, что вы — переменная Backle внутри объекта Collar. Вот вы удобно расположились внутри объ¬ екта Collar, который, в свою очередь, рад находиться внутри объекта Dog. Но что произойдет, если Dog за¬ хочет поменять переменную Collar или обнулить ее? Это сделает объект Collar доступным для сборщика мусора. Что же происходит с вами как переменной внутри объекта Collar, который стал недоступным? И вы в это поверили? Они говорят так, чтобы улуч¬ шить вашу производительность и сохранить интерес к работе. Но вы кое о чем забываете. Что происходит с переменной экземпляра внутри объекта, на который ссылается только одна локальная переменная? Если я буду единственной ссылкой на объект, в котором вы пребываете, то ваша смерть последует сразу за моей. Нравится вам это или нет, наши судьбы связаны. Давайте же забудем обо всем и хорошенько напьемся, пока еще есть такая возможность. Как говорится, бери оперативку, пока дают!
ражнение Соберите мусор Какую строчку кода из списка спраВа нужно доба¬ вить В/лесто букВы жЛ», чтобы сборщику ллусора стал доступен один дополнительный объект? (Ис¬ ходите из того, что пункт Л (//ВызоВ дополнитель¬ ных ллетодоЬ) будет Выполняться достаточно долго. предостаВляя сборщику ллусора Врелля Выполнить работу и очистить паллять). public class GC { public static GC doStuff() { GC newGC = new GC(); doStuff2(newGC); return newGC; } public static void main(String[] args) { GC gc1; GC gc2 = new GC(); GC gc3 = new GC(); GC gc4 = gc3; gel = doStuff(); О //вызов дополнительных методов } public static void doStuff2(GC copyGC) { GC localGC = copyGC; } *- Ответы на стр. 308 1 copyGC = null; 2 gc2 = null; 3 newGC = gc3 ; 4 gel = null; 5 newGC = null; 6 gc4 = null ; 7 gc3 = gc2 ; 8 gel = gc4 ; 9 gc3 = null;
class Bees { Honey[] beeHoney; скольких новых объектов. Ваша задача — найти самый востребованный объект, то есть тот, к ко¬ торому ведут наибольшее количество ссылочных переменных. Перечислите, сколько всего суще¬ ствует ссылок на этот объект и что это за ссылки! Один объект и одну ссылку мы покажем в каче¬ стве примера. Код в этом примере показывает создание не¬ удачи! class Raccoon { Kit rk; *- Ответы на стр. 308 Honey rh; } class Kit { Honey honey; } class Bear { Honey hunny; } public class Honey { public static void main(String!] args) { Honey honeyPot = new Honey (); Honey [] ha = {honeyPot, honeyPot, honeyPot, honeyPot}; Bees bees = new Bees(); bees.beeHoney = ha; Bear[] bears = new Bear[5]; for (int i = 0; i < 5; i++) { bears [i] = new BearO; bears[i].hunny = honeyPot; 9mi Hcftut cctLKin Haccunl Kit kit = new Kit(); kit.honey = honeyPot; /у' Raccoon raccoon = new Raccooftf); raccoon.rh = honeyPot; raccoon.rk = kit; kit = null; } // конец метода main
Головоломка «Мы запускали процесс симуляции четыре раза, и температура главного модуля каждый раз отклонялась в сторону понижения, — сказала Сара, и в ее голосе явно прозвучали нотки досады. — На прошлой неделе мы установили новые тепловые датчики. Показатели радиаторных машин, обслуживающих жилые отсеки, вроде бы в пределах нормы, поэтому сейчас мы сосредоточены на анализе тепловых машин, которые отвечают за обогрев». Дело пяти минут Том тяжело вздохнул. Сначала казалось, что нанотехнологии действительно помогут им опередить график. Но теперь, когда до запуска осталось всего пять недель, некоторые ключевые системы жизнеобеспечения орбитального корабля по-прежнему не проходят ис¬ пытания на симуляторах. — Какие соотношения вы моделируете? — спросил он. — Если я правильно понимаю, к чему ты клонишь, то мы уже это учли, — ответила Сара. — Центр управления не даст разрешения на включение критически важных систем, если мы запустим их с нарушениями. Необходимо, чтобы юниты V3 и V2 радиаторных ма¬ шин работали в пропорции 2:1. А общее соотношение систем, удерживающих тепло, к радиаторным машинам должно составлять 4:3. — Что с энергопотреблением, Сара? — спросил Том. Сара немного помолчала, а затем ответила: — По нашим данным, энергии уходит больше, чем предполагалось. Мы отслеживаем и этот параметр, но из-за беспроводной передачи энергии трудно точно понять, сколько потре¬ бляют тепловые машины, а сколько — радиаторные. Согласно проекту, соотношение долж¬ но быть 3:2, причем радиаторы потребляют больше энергии из беспроводной сети. — Понял, Сара, — сказал Том. — Давай взглянем на код, запускающий симуляцию. Мы должны понять, в чем проблема, и как можно быстрее! import java.util.ArrayList; class V2Radiator { V2Radiator(ArrayList<SimUnit> list) { for (int x = 0; x < 5; :■:++) { list.add(new SimUnit("V2Radiator")); } } class V3Radiator extends V2Radiator { V3Radiator(ArrayList<SimUnit> lglist) ) super(lglist); for (int g = 0; g < 10; g++) { lglist.add(new SimUnit("V3Radiator")); > } class RetentionBot { RetentionBot(ArrayList<SimUnit> rlist) { rlist.add(new SimUnit("Retention")); } }
Дело пяти минут (продол^ жение) import java.util.ArrayList; public class TestLifeSupportSim { public static void main(String[] args) { ArrayList<SimUnit> aList = new ArrayList<SimUnit>(); V2Radiator v2 = new V2Radiator(aList); V3Radiator v3 = new V3Radiator(aList); for (int z = 0; z < 20; z++) { RetentionBot ret = new RetentionBot(aList); } } } class SimUnit { String botType; SimUnit(String type) { botType = type; ) int powerUseO ( if ("Retention".equals(botType)) { return 2; } else { return 4; } ) } Несколько секунд Том напряженно разглядывал код. Затем по его губам скользнула сдер¬ жанная, почти озорная улыбка. — Кажется, я знаю, в чем дело, Сара. И могу с уверенностью сказать, на сколько процен¬ тов показатели энергопотребления ушли в сторону от нормы! О чем догадался Том, просматривая код? Как ему удалось вычислить ошибку в рас¬ четах энергопотребления? И какие строки кода нужно добавить, чтобы наконец-то привести эту программу в чувство? *- Ответы на стр. 309
Ответы на упражнения Собирай мусор (со стр. 304) 1 2 3 4 5 copyGC = null; Нет — эта строка пытается получить до¬ ступ к переменной, которая находится вне текущей области видимости gc2 = null; newGC = дсЗ; gel = null; newGC = null; Да — gc2 была единственной переменной, ссылающейся на тот объект Нет — еще одна переменная вне области видимости Да — gel была последней ссылкой, так как newGC находится вне области видимости 6 gc4 = null; 7 дсЗ = дс2,- 8 gel = дс4; Нет — newGC вне области видимости Нет — дсЗ все еще ссылается на тот объект Нет — дс4 все еще ссылается на тот объект Да — присваивание нового значения един¬ ственной ссылке на тот объект 9 дсЗ - null; Нет — дс4 все еще ссылается на тот объект Объекты нарасхват (со стр. 305) Наверное, несложно догадаться, что объект Honey, на который сначала ссылалась переменная honeyPot, — самый «популярный» объект в этом классе. Чуть сложнее понять, что все ссыл ки в коде, указывающие на объект Honey, ссылаются на один итотже объект! Всего есть 12 активных ссылок на этот объект прямо перед завершением метода mainQ. Переменная kit.honeyPot действует некоторое время, но в конце концов переменная kit получает значение null. Поскольку raccoon.rk все еще ссылается на объект Kit, raccoon.kit.honeyPot (хотя и без явного объявления) ссылается на этот объект! public class Honey { public static void main(String[] args) Honey honeyPot = new Honey (); Honey[] ha = (honeyPot, honeyPot, honeyPot, honeyPot}; Bees bees = new Bees (); bees.beeHoney = ha; Bear[] bears = new Bear[5]; for (int i = 0; i < 5; i++) { bears [i] = new Beard ; bears[i].hunny = honeyPot; (в конце при- 1 сваивается null) Klt klt = new Klt(); |— — — — kit. honey = honeyPot; , Raccoon raccoon = new Raccoon(); ( _ raccoon.rh = honeyPot; _ raccoon.rk = kit; kit = null; } // end of main )
Дело пяти минут (со стр. 306-307) Том заметил, что конструктор класса V2Radiator принимает объект ArrayList. А это значит, что каждый раз, когда вызывался конструктор V3Radiator, он передавал тот же ArrayList в конструктор V2Radiator через вызов super (). Таким образом, создавались еще пять объектов SimUnit, предназначенных для V2Radiator. Если Том был прав, общее энергопотребление должно было составить 120, а не 100, как считала Сара. Поскольку все классы типа Bot создают объекты SimUnit, достаточно было добавить в конструктор SimUnit строку вывода в консоль, чтобы увидеть, когда именно создается каждый объект. Это быстро помогло бы найти ис¬ точник проблемы!
10. Числа и статические члены Без чисел никуда о° Займемся математикой. Работа с числами включает в себя не только простые арифмети¬ ческие действия. Вам может понадобится получить абсолютное значение числа, округлить число или найти большее из двух чисел. А еще может потребоваться, чтобы числа выводились с двумя знаками после запятой, или с запятыми в больших числах, чтобы было удобнее читать. А как на¬ счет преобразования строки в число? Или превратить число в строку? Когда-нибудь вам понадо¬ бится поместить кучу чисел в коллекцию типа ArrayList, которая принимает только объекты. Вам повезло. Java и Java API полны удобных, готовых и простых возможностей и методов для преоб¬ разования чисел. Но большинство из них — статические. Поэтому начнем с того, что разберемся, что означает «статичность» переменной или метода. В том числе затронем тему констант в Java; их называют статическими финальными переменными.
Методы Math: почти как глобальные Однако в Java нет ничего глобального. Представьте, что поведение метода не зависит от значения переменной эк¬ земпляра. Возьмем, к примеру, метод round () из класса Math. Он округляет дробное число (которое передается в виде аргумента) до ближайшего целого. Если из 10000 экземпляров класса Math вызвать метод гound ( 4 2.2 ), результатом всегда будет целое число 4 2. Иными словами, метод зависит не от состояния перемен¬ ной экземпляра, а от аргумента. Единственное значение, влияющее на работу метода round () , — это аргумент, который ему передается! Может быть, создание экземпляров класса Math — это трата драгоценного места в куче, если его единственная цель — лишь запуск метода round () ? И что насчет других методов из этого класса, например min () , кото¬ рый принимает два числовых примитива и возвращает наименьший из них? Или шах (). Или abs (), возвраща¬ ющий абсолютное значение числа. Эти методы не используют значения переменных эк¬ земпляра. На самом деле класс Math вообще не содержит перемен¬ ных, поэтому от создания его экземпляра нет никакой пользы. Но вам и не нужно этого делать. Более того, у вас не получится. Если вы попытаетесь создать экземпляр класса Math: Методы из класса Math не используют значения переменных экземпляра. И поскольку они статические. Ьам не нужен экземпляр класса Math. Зам необходим только сам класс. long х = Math.round(42.2); int у = Math.min(56, 12); int z = Math.abs(-343); Memoati нс. испсмж- кт переменные эклмкля- /ш- тэтами им пите нс нужш тшш v копкМт пом ссУскпи . ' Math mathObject = new Math(); Вы получите следующую ошибку: File Edit Window Help IwasToldThereWouldBeNoMaths %javac TestMath TestMath.java:3: Math() has private access in java.lang.Math Math mathObject = new Math(); many nnv fJtath txjummxniiu! To (шита Тс^никы not каит/и unu m ’Ш cutMxmt comum notfnu ecuKtn. ihilh < noAuiu/K cmpiinupa ntu <> сектами с этим к ителе 1 error
Разница между обычными и статическими методами Язык Java объектно-ориентированный, но время от времени возникают особые случаи, когда не нужно иметь экземпляр класса (например, мето¬ ды класса Math). Ключевое слово static позволяет запустить метод без экземпляра класса. Статический метод означает «поведение, не завися¬ щее от переменной экземпляра, поэтому не требуется экземпляр/объект. Нужен только класс». Обычный (не статический) метод public class Song String title; ( public Song(String t) { title = t; } public void play() { SoundPlayer player = new SoundPlayer(); Статический метод public static int min(int a, int b) { // возвращает наименьшее из а и b ) Лагишишх экктхяр иш\. Jllfiujuuii .«uiucjn нс мишлпсл (s’ м (Wucu muc c ur со¬ стояние нс|и.мш ксиэк^сяиия^п. Math.min(42,36) ; !]кодп(?«йтс сидя класса. а нс ш^и.шкнса стлкм.
Статический метод вызывается путем указания^мени класса. Math L Math .min(88,86) ; min() max() abs() Обычный метод вызывается с помощью имени ссылочной переменной Song t2 = new Song(); t2.play(); Насчет классов со статическими методами Часто (хотя и не всегда) класс со статическими методами не предназначен для создания объектов. В главе 8 мы говорили об абстрактных классах и о том, как модификатор abstract делает не¬ возможным использование оператора new с этим типом. Иными словами, нельзя создать экземпляр абстрактного класса. Но можно запретить создание экземпляров даже у не абстрактного класса, пометив его конструктор как private. Помните: приватный метод мож¬ но вызвать только из того же класса. То же самое касается и приватного конструктора: только код внутри этого класса сможет вызвать new. Никто из¬ вне не сможет создать объект. Именно так устроен, например, класс Math. У него приватный конструк¬ тор — создать экземпляр Math нельзя. Компилятор проследит, чтобы код за пределами этого класса не имел доступа к приватному конструктору. Это вовсе не означает, что класс с одним или не¬ сколькими статическими методами не может иметь экземпляров. На самом деле любой класс с методом main () — это класс со статическим методом! Чаще всего метод main () служит для запуска или тестирования другого класса. Внутри main () обычно создается объект, из которого вызываются методы. Можно комбинировать в классе как статические, так и обычные методы. Однако, если в классе есть хотя бы один нестатический метод, значит, должен быть способ создать его экземпляр. Есть два вари¬ анта: оператор new или десериализация (или, реже, Java Reflection API — о нем мы поговорим позже). Других способов нет. Кто именно вызывает new — это интересная тема, и мы ее обсудим чуть позже в этой главе.
Статическим методам недоступны переменные экземпляра! Статические методы не зависят от конкретного объекта. Они работают вообще без знания о том, существует ли хоть один экземпляр класса. Как вы уже видели, класс может не иметь ни одного объекта — и все равно его статические методы можно вызывать, например, как Math . random () . Поскольку такие методы вызываются прямо от имени класса, а не через объект вроде t2 . play (), они не могут исполь¬ зовать переменные экземпляра. Почему? Потому что стати¬ ческий метод не знает, в каком именно объекте искать эту переменную: у него просто нет на это ссылки. Если вы попытаетесь скомпилировать этот код: public class Duck { private int size; И Kgyfl public static void main(String[] args) { System.out.println("Размер уточки:” + ) ('KWH unu tr>tKmf)uck. Ha ti (ermu,н ни tUc 11 (<y (tenth 1H( size) public void setSize(int size = s; s) ( Cimnnu'tttKUu ki HimKOt. Bet tcnuiuHxt € K-wce НиШтясштмекме Если вы попробуете обратиться к пере¬ менной экземпляра внутри статического метода, компиля¬ тор как бы говорит: «Я без понятия, к ка¬ кому объекту вы сей¬ час обращаетесь!» В куче может быть десять объектов Duck, и статический метод не будет знать ни об одном из них. 7 него нет доступа к конкретным экзем¬ плярам — он работает сам по себе, без при¬ вязки к объекту. public int getSize() { return size; } Вы получите такую ошибку: [File Edit Window Help Quack % javac Duck.java Duck.java:6: non-static variable size cannot be referenced from a static context System.out.println("Размер уточки:" + size);
Статические методы не могут обращаться к нестатическим методам! Что делают нестатические методы? Они обычно используют состояния переменных экземпляра, влияющих на их пове¬ дение. Метод getName () возвращает значение переменной name. Но чье это имя? Объект используется для вызова метода getName(). Этот код не скомпилируется: public class Duck { private int size; »<«y 9mWl ufnucf public static void main(String[] args) { System.out.println("Размер;" + getSize()) > public void setSize(int s) { size = s; public int getSize() { return size; dct m— ,щй как ого «)ккта? jFile Edit Window Help Jack-in % javac Duck.java Duck.java:5: error: non-static method getSize() cannot be referenced from a static context System.out.printlnj"Размер•„ getSize()); f• + Л 1 error Вбить в голову Розы красны и полны нектара, Статические слепы к переменным экземпляра (Не) дурщкме вопросы Вой рОС: Что будет, если из статиче- ского метода вызвать нестатический, даже если в нем нет переменных экземпляра? Компилятор пропустит такой код? Ответ-. Нет. Компилятор не даст вам это сделать. Он не может знать, что вы не добавите переменные экземпляра в этот метод позже. Подумайте: что произойдет, если в будущем вы решите изменить реа¬ лизацию метода и добавить в него доступ к переменной экземпляра? А если под¬ класс переопределит этот метод и начнет работать с состоянием объекта? Компиля¬ тор просто не может на это полагаться — он подстраховывается на будущее. Вой рОС: Уверен, я видел код, где статический метод вызывался через переменную, а не через имя класса. Это вообще нормально? Ответ: Да, так можно сделать... но лучше не стоит. С технической точки зрения это работает, но запутывает. Вы можете написать, например: Duck d = new Duck(); String[] s = {}; d.main(s); Код скомпилируется, потому что d — переменная типа Duck, a main{) — ста¬ тический метод. Компилятор просто подставит имя класса Duck вместо пере¬ менной d. Но важно помнить: вызов d.main(s) не означает, что метод main() каким-то об¬ разом знает, на какой объект ссылается d. Он все равно остается статическим, то есть не связанным ни с каким конкрет¬ ным объектом. Такой вызов — просто «альтернативный путь» к статическому методу.
Статическая переменная: одно и то же значение для всех экземпляров класса Представьте ситуацию: вам необходимо посчитать, сколько объектов Duck было создано за время работы программы. Как это сделать? Может быть, с помощью переменной, которая будет инкрементироваться в кон¬ структоре? class Duck { int duckCount = public Duck() { duckCount++; } } 0; CO. public class Duck { private int size; 0; public Duck() duckCount++ } Htht У Uck- У длинная public void setSize (int s) { rCUicH0 hut Q. size = s; Это не сработает, потому что duckCount — пере¬ менная экземпляра, и для каждого экземпляра Duck она будет иметь начальное значение 0. Вы можете вызывать метод из другого класса, но это еще больше запутает. Нужен класс с единственной общей копией переменной для всех своих экземпляров. public int getSize() { return size■ } Именно это предоставляет статическая переменная — значение, общее для всех экземпляров класса, то есть одно значение на класс вместо одного на экземпляр. Duck size ^static duckC r > ount f V getSize() setSize() 'б 'К c)<w*^^wcirHt (iuc^tW'LlttTv спятишь ^ ясстмкнАДОА i ,i.n' ?Ьекг size: 12) WuckCount: - 1*uZV&K™^ Нс ectm ^УУи>ННнч
Гнкя нмж-ны** iciCtui m Статические переменные общедоступны Все экземпляры одного класса содержат общую копию статической переменной. Переменные экземпляра: по одной на экземпляр. Статические переменные: по одной на класс. Ранее в этой главе мы уже говорили: если конструктор помечен как private, то создать объект этого класса извне нельзя. То есть только сам класс может создать экземпляр себя через свой приватный конструктор. (И тут уместна метафора с курицей и яйцом: как вызвать конструктор, если объект еще не создан?) Но возникает интересный вопрос: как написать класс так, чтобы в программе существовал только один-единственный экземпляр этого класса, и все, кто его использует, работали бы именно с ним?
Инициализация статической переменной Статические переменные инициализируются при загрузке класса. Этот процесс начинается тогда, когда JVM решает, что момент настал — чаще всего при первом создании экземпля¬ ра класса или при обращении к его статическим переменным или методам. Как программист вы можете явно инициировать загрузку класса, но обычно в этом нет необходимости — JVM прекрасно справляется с этим сама. У статической инициализации есть две важные особенности: • Статические переменные инициализируются до того, как можно будет создать хотя бы один объект этого класса. • Статические переменные инициализируются до запуска любого статического метода этого класса. Все статические переменные в классе инициализируются до того, как можно будет создать хотя бы один объект этого класса. class Player { static int playerCount = private String name; public Player(String n) { name = n; playerCount++; } } TWuaHмР&гшяСиий UHIUVUUTff^Xu. Дшжс кшсса.%х мвт .yuu&mtfU "f ™ i 1.Л... псска.икЦ t ^ watt nut iu utu.rrmuw ij tituwiumcKHt ним* 14 ILL tCttUHtMHfW- nt 'ИДКИМЛ nc ks Шия эт< ИГ сся-jamtuHC. псскоику UtUMlUmiKHt IWic-ikГШ1» ■'—•} J ^«awmiuc. как a w|iu«iww ft H НИЛ'. Знапния nc fuammm сия спявтитх Hi не unuiiMcUfUfu&niHkxammurtcKUV public class PlayerTestDrive { public static void main(String[] args) { Systern.out.println(Player.playerCount) Player one = new Player("Tiger Woods") Sys tem.out.println(Player.playerCount) тииупибши UjUtcit motif н(И‘/. 'Ac rl и m. if ): 0 ■ yuiuiumtimt пита с а шСтмщ{ mcr/vcu (ffutt. (kuCfO. C.C ' тижкш ; piCse ' ссшки на tcuxfmi: nufC } 'Ь /\(ЩП к cmmnuncKuc п^к.шннси ^ ccumjj-m£ляетсл тяк ж, как и к «пяти tuxcMif липиду, — пс аисте класса. Если вы не задаете значение статической переменной при объявлении, она автоматически получает значение по умолча¬ нию. Для целочисленных типов это ноль, поэтому нет необ¬ ходимости явно писать playerCount = 0. Объявление без инициализации означает, что переменной будет присвоено стандартное значение, так же как это происходит с переменны¬ ми экземпляра.
Статические финальные переменные = константы Переменная с ключевым словом final после инициализации не может быть изменена. Другими словами, значение статической финальной переменной будет оставаться неизменным до тех пор, пока загружен класс. Найдите Math . PI в документации API, и вы увидите: public static final double PI = 3.141592653589793; Переменная публична (public), так что любой код может полу¬ чить к ней доступ. Переменная статична (static), чтобы вам не понадобился экземпляр класса Math (который, напомним, вы не имеете права создавать). Переменная финальная (final), потому что значение PI не изменя¬ ется (насколько это возможно в Java). Другого способа объявить переменную константой, кроме как с помощью final, нет. Но есть соглашение об именовании, которое поможет вам ее сразу распознать. Имена переменных констант обычно пишутся прописными буквами! Статический инициализа¬ тор — это блок кода, который запускается при загрузке класса, прежде чем он станет доступен. Так что это отлич¬ ное место для инициализа¬ ции статической финальной переменной. class Constantlnitl { final static int X; static { X = 42; ) ) Инициализация финальной статической переменной. (Т) В момент ее объявления: - - тс уш ннш -.srsff"*'*- или '7^тннш ~ 9„„. 7 © В статическом инициализаторе: public с7“““мГ“сиЬ1. U: public static static { ., VAL = Math.random О '>пт кш gtu КМ(«' ~ т f cmarCZ % иРаттм *-н«йс lrtcKcu перииннс и. f Если вы не присвоите значение финальной переменной в одном из этих двух мест: public class ConstantInit3 { public static final double VAL; компилятор выдаст следующее. % javac ConstantInit3.java ConstantInit3.java:2: error: variable VAL not initialized in the default constructor public static final double VAL; A 1 error
Модификатор final подходит не только для статических переменных... Ключевое слово final можно применять и для обозначения не статических переменных, включая переменные экземпляра, локальные переменные и даже параметры методов. И это всегда будет иметь один и тот же смысл: значение не может изменяться. Кроме того, разрешено использовать этот модифи¬ катор с целью запретить переопределение методов или создание дочерних классов. Нестатические финальные переменные class Foof { final int size = 3; final int whuffie; t—KtAfM imimimu лш n huc rufvL iu н пси va Вы не можете менять значе¬ ние финальной переменной. Вы не можете переопреде¬ лять финальный метод. Foof о ( whuffie = 42; <■— HtXIJM imitHllrttt jWIttHUf \ ntpclt Hiu к whuffie Вы не можете расширять фи¬ нальный класс (то есть соз- void doStuff(final int x) // нельзя изменить x ) void doMore() ( final int z = 7; // нельзя изменить z давать дочерние классы). » Финальный метод class Poof { final void calcWhuffie () { // логику метода // нельзя переопределить ) Финальный класс final class MyMostPerfectClass { // нельзя наследовать
Без лишних слов (Не) дурацкие вопросы ЗоПрОС: Статический метод не имеет доступа к нестатическим переменным. А может ли нестатический метод рабо¬ тать со статическими переменными? Ответ Конечно! Нестатический ме¬ тод может вызывать статические методы своего класса и обращаться к статиче¬ ским переменным. Это ведь методы эк¬ земпляра, у них полный доступ ко всему, что есть у класса. ЗоИрОС: Зачем вообще делать класс финальным? Разве это не убивает всю прелесть ООП? Ответ: И да, и нет. Чаще всего класс делают финальным из соображений безопасности. Например, класс String — финальный. Представьте, если бы кто- нибудь создал дочерний класс String и начал использовать его везде, где ожи¬ дается обычный String. Возможности для взлома — просто шоколадные. Если вы хотите быть уверены, что реализация ме¬ тодов в классе не изменится, — сделайте класс финальным и спите спокойно. ЗОИ рОС: А зачем финализировать метод, если сам класс уже финальный? Ответ А вот и не нужно. Если класс помечен final, наследовать его никто не сможет, а значит, и переопределять методы — тоже. Но если вы хотите разрешить наследо¬ вание, но запретить переопределение отдельных методов, — тогда финализи- руйте только нужные методы. Финальный метод никто из потомков переписать не сможет, даже если очень захочет. ■ Статический метод следует вызывать, указывая имя класса, а не переменную-ссылку на объект: Math.random() вместо myFoo.go(). ■ Статический метод можно вызвать даже при отсутствии экземпляров класса в куче. ■ Статический метод отлично подходит для утилитарных задач, не зависящих (и не планирующих зависеть) от конкретного объекта. ■ Поскольку статический метод не привязан к объекту, он не имеет доступа к значениям переменных экземпляра — ему просто неоткуда их взять. ■ Также статический метод не может обращаться к нестати¬ ческим методам: они чаще всего работают с конкретным состоянием объекта. ■ Если в вашем классе только статические методы, и вы не хотите допускать создание его экземпляров, сделайте конструктор приватным. ■ Статическая переменная — общая для всех экземпляров класса. Существует всего одна копия такой переменной, а не по одной для каждого объекта. ■ Статический метод может использовать статические пере¬ менные. ■ Чтобы создать константу в Java, объявите переменную с модификаторами static и final. ■ Значение финальной статической переменной должно быть присвоено либо при объявлении, либо в статическом инициализаторе: static { DOG_CODE = 420; } ■ По соглашению, имена констант (финальных статических переменных) пишутся ПРОПИСНЫМИ БУКВАМИ, а слова разделяются символом подчеркивания (J. ■ Значение финальной переменной не может быть изменено после инициализации. ■ Присвоение значения финальной переменной экземпляра происходит либо при объявлении, либо в конструкторе. ■ Финальный метод нельзя переопределить в подклассе. ■ Финальный класс нельзя расширить (унаследовать от него).
аточите карандаш К Что допустимо? Учитывая все, что вы только что узнали о переменных- константах, какие перечис¬ ленные ниже фрагменты кода скомпилируют? Держитесь Ф правее © public class Foo { static int x; public void go() { System.out.println(x); } public class Foo2 { int x; public static void go() { System.out.println(x); } public class Foo4 { static final int x = 12; public void go() { System.out.println(x); ) public class Foo5 { static final int x = 12; public void go(final int x) { System.out.println(x); } (3) public class Foo3 { final int x; public void go() { System.out.println(x); ) © public class F006 int x = 12; ( public static void go(final int x) ( System.out.println(x); ) *■ Ответы на стр. 344
Методы Math Теперь, когда мы знаем, как устроены статические методы, давайте рассмотрим не¬ которые статические методы в классе Math. Это не все из них, а только основные. Остальные, включая cos (), sin (), tan (), ceil (), floor () и asin (), вы най¬ дете в документации API. static 1 ■to.(int a) static tofttt abstiong ») static int ab*E«ctlint a) static tw»g absixactllong a) static double acos(double a) static int static long •«Exact(int x, int У) •«Exact(long x. ;°ng y) stetic double asin(double ■) static double •tan(double a) static double alanl! double У. double x) static double CbftldoUble •) static double call(double a) static doublt i consign (double aagnitude, static float cogysign (float aegnitude, Returns the absolute value of » Retunxs the absolute value of a f Return* the absolute value of an i Return* the absolute value of a l<^ Returns the mathematical ab»o|u« ArlthmeticExceptloo if the r. Returns the mathematical al ArithmeticIxcaPtlon if the г Returns the arc coatne of a value;! Returns the sum of Ks elemental Returns the sum o! It* arguments^ Returns tba arc sine of a value; t Returns the ere tangent of a valtU Returns the angle thgto from the I Returns the cube root of a douhl^ Retun» the smallest (closest to n e mathematical integer. double «9») «'">”* “» "* --*—1 float *19") Return* the first floating pointЛ static double co$(double a) static double cosh(doubla x) Returns the trigonometric cosine | Returns the ЬЯ> Math.abs() Возвращает абсолютное значение аргумента (double). Метод перегружен: если передать ему значение типа int, он вернет int; если передать double, вернет double. int х = Math.abs(-240); II возвращает 240 double d = Math.abs(240.45); // возвращает 240.45 Math.randomO Возвращает double-значение от (и вклю¬ чая) 0.0 до (но не включая) 1.0. double rl = Math.randomO; int r2 = (int) (Math.randomO * 5); /)с cue лсКлш wmKutwo- fcuu чтит лштиа, uc u tivt такла jiiva.ul if. Random., Kon\o|fu\u YumctKu njtimiv Hit (? UCni.AtAftwUU.
Math.round() Возвращает значение типа int или long (в зави¬ симости от того, является ли аргумент float или double), округленное до ближайшего целого числа. .Защитите: dJawiAwdct шлю с лштштисй HUtKtM СШПиШТШЧ dm8Qt. тш т я(?нс KL скажеш, ктс эте fiCoat, j G кешу.. int x = Math.round(-24.8f); // возвращает -25 int у = Math.round(24.45f); // возвращает 24 long z = Math.round(24.45); // возвращает 24L Это |Haw.nue JmGCt Math.minO Возвращает меньшее из двух переданных зна¬ чений. Метод перегружен и может принимать аргументы типов int, long, float или double. int x = Math.min(24,240); // возвращает 24 double у = Math.min(90876.5, 90876.49); // возвращает 90876.49 Math.maxO Возвращает большее из двух переданных зна¬ чений. Метод перегружен и может принимать аргументы типов int, long, float или double. int x = Math.max(24,240); // возвращает 240 double у = Math.max(90876.5, 90876.49); // возвращает 90876.5 Math.sqrt() Возвращает положительный квадратный корень переданного аргумента. Метод принимает значение типа double, но вы можете передать ему любое чис¬ ло, которое можно неявно преобразовать в double. double х = Math.sqrt(9); double у = Math.sqrt(42.0); // возвращает 3 // возвращает 6.48074069840786
г Обертка для примитивов Иногда с примитивами необходимо работать как с объектами. Например, коллекции типа ArrayList поддерживают только объекты: ArrayList<???> list; (iarw^naW " У каждого примитивного типа есть свой класс-обертка. Все они находятся в пакете j ava. lang, так что им¬ портировать их не требуется. Узнать классы-обертки легко: их названия совпадают с названиями примитив¬ ных типов, только начинаются с прописной буквы — в соответствии с соглашением об именовании классов. Ну а теперь внимание... Почему-то разработчики API решили не давать этим классам точно такие же имена, как у соответствующих примитивных типов. Почему — не знает никто. Серьезно. Ни один человек на планете. Но вы сейчас сами всё поймете: Объект Примитив Когда вам нужно обращаться к примитиву как к объекту, оберните его. Boolean Character Byte Short Integer Long Float Double , Jmgtmc Ксива- huJ клагссв-cclejtmjCK hi в me те¬ кшим совпадают с гу»алшпт1?нш: тш\с(?. Пиша классов пишется гшлаоспию, су со^иицтий.' Луидашт »уиышти<? кш . стлшстсйи осДипкн. Вот н вес. wrapping a value Иу 11 ' int i = 288; Integer iWrap = new Integer(i); Bet K.Aa(ca ucj\mKH jvuxma- /ют нс (шшшш »ушки,нт|. ij каждого и? них ют . uum tj, кс imfmk всрушш/мт та тише fyuummuftmo тшиг. new Integer (Ц ~ это unwrapping a value int i = 288; Integer iWrap = Воовшп — всс&апУавшО, Chaiacta — cha-tVa&uO \ так T m. IL. ,< пи. нн>«'л Примечание: на фото вверху — шоколад в фольгированной обертке. Понимаете? Обертка! Другим нравится ассоциация с запе¬ ченной картошкой, тоже вполне себе вариант.
Какая чепуха. Неужели я не могу просто создать ArrayList из це¬ лых чисел? Мне что, нужно обернуть каж¬ дый в новый объект Integer, а потом еще и разворачивать, когда захочу получить значение из ArrayList? Это же сплош¬ ная трата времени и потенциальная ошибка, которая только и ждет своего часа... Java сама упакует примитивы В старые добрые времена (до появления Java 5) разработчикам приходилось делать все вручную — оборачивать и распаковывать примитивы самостоя¬ тельно. К счастью, теперь компилятор берет это на себя. Давайте посмотрим, как это работает, если нужно создать ArrayList для хранения целых чисел. ArrayList для хранения примитивов типа int public void autoboxin,,, ( ^' int x = 32; 4, ArrayList<Integer> list = new ArrayList<Integer>(); list.add(x) ; jj- TCCm ac! int num = list.get(O); V Ххтя yArtauLiM нет штща add(bvt). шлш мтс^глм (5mUx<tH.4em7yi \акпС)К\ц (&xn\q}. JJfxaiy u&jvi, S.ArtamJtt cp1rm(?u- hw.«hc xf/ишяшя сйшпм Jnuatt нс 6k tj’ " qtmiu 6Ц. cuqmc qcm&t.mnt muqa r„*u Ki^\u,wt№\ а&тиапитскц. втш- м & Аг1ац£ш<ШШЬ можно асмв-шт “ llricr}0KcbKtHi^oxn\q): ccbtnnv lntum wmr- mR гнойная плщшипий’нсго rtuow ftit, mак ™скмс*н< Д Х « сс&кт* JntcL {няуш&ипи ш гущmmadnck тдешннГЛй T f{ "шт WHlfw dtyi&mt. тШжО. f № f мщ h&Sl (He) дурщкие вопросы Вопрос.- Почему бы просто не объявить ArrayList<int>, чтобы хранить значения типа int? Ol/HBewi: Потому что... нельзя. По крайней мере, не в тех версиях Java, о которых идет речь в этой книге (язык-то развивается, и все может поменяться!). Помните об одном важном прави¬ ле обобщенных типов: в параметрах типа можно указывать только классы или интерфейсы, но не примитивы. Поэтому ArrayList<int> просто не скомпилируется. Но, как видно из кода выше, это не проблема: компилятор сам позволяет вам добавлять int в ArrayList<lnteger>, оборачивая примитив в объект Integer. И более того, вы даже не сможете этому помешать: если у вас список с типом, соответствующим примитиву, при добавлении примитива произойдет автоматическая упаковка. То же самое работает и с другими примитивами: можно хранить boolean в ArrayList<Boolean>, char в ArrayList<Character> и так далее.
Java упакует примитивы где угодно (ну, почти) Автоматическая упаковка позволяет не просто обертывать и разво¬ рачивать примитивы в коллекции, но и использовать либо прими¬ тив, либо его тип-обертку практически везде, где ожидается тот или иной тип. Подумайте об этом! Автоупаковка — это весело! Аргументы метода Если метод принимает тип-обертку, вы можете передать ссылку на оберт¬ ку или примитив соответствующего типа. И, конечно, верно обратное: если метод принимает примитив, вы можете передать либо совместимый примитив, либо ссылку на обертку этого примитивного типа. void takeNumbeг (Integer i) { } Возвращаемые значения Если в объявлении метода указан тип возврата как примитив, можно вернуть либо примитив, либо ссылку на обертку. А если метод возвращает ти п-обертку, то можно вернуть как саму обертку, так и соответствующий примитив. int giveNumber() { return х; Логические выражения В любом месте, где ожидается логическое значение, можно ис¬ пользовать выражение, результат которого — логическое значение (например, 4 > 2), сам примитив boolean или ссылку на логическую обертку Boolean. if (bool) { System.out.println("true");
Операции над числами Это, пожалуй, самое странное — да, вы действительно можете использовать тип-обертку как операнд в выраже¬ нии, где обычно ожидается примитив. То есть, например, можно применить оператор инкремента к объекту типа Integer! Но не волнуйтесь: это просто хитрость компилятора. Язык не был переписан так, чтобы операторы работали с объ¬ ектами: компилятор просто автоматически преобразует объект в соответствующий примитив перед выполнением операции. Выглядит это, конечно, немного неожиданно. Integer i = new Integer (42); i++; А это значит, что вам доступно нечто подобное: Integer j = new Integer (5); Integer k = j + 3; i++; Назначения Вы также можете свободно присваивать значения между примитивами и обертка¬ ми: переменной типа int можно присво¬ ить объект Integer, и наоборот — объект Integer можно присвоить переменной типа int. Автоупаковка и автораспаковка берут все на себя. Integer intVal = х; аточите карандаш Будет ли этот код компилироваться? А выполнится ли он? И какой результат мы получим? Не спешите — хорошенько подумайте. Вопрос связан с автоупаковкой, кото¬ рую мы пока не разбирали подробно. Чтобы узнать ответы, придется обра¬ титься к компилятору. (Да, мы нарочно заставляем вас экспериментировать — исключительно ради вашего блага!) public class TestBox { private Integer i; private int j; public static void main(String[] args) { TestBox t = new TestBox(); t.go() ; } public void go() { j = i; System.out.println(j) ; System.out.println(i) ; J > *• Решать —вам!
Постойте! Это еще не все! Обертки также содержат статические служебные методы! Помимо того, что классы-обертки ведут себя как обычные классы, в них есть множество действительно полезных статических методов. Например, методы parse принимают строки и возвращают примитивные значения. fL,\acc'oC (татиткт жгтг клас [бя 'nt * /ж Ac n, /„/ ' -""™ «««-*Ц v) . .. /Г 'Я г Integer.parselnt (“3”) Преобразовать строку в при- tv,u f jtu гле- митивное значение несложно: ' Хранить 2 62. String s = "2"; int х = Integer.parselnt(s); double d = Double.parseDouble("420.24") boolean b = Boolean.parseBoolean("True") 1L«Vf«*iu‘1vtu' 6 я|«чШЛпии’ Но если попробовать сделать так: String t = "два"; int у = Integer.parselnt(t); на шш/. fvr< K. :tj кашинириапсм cnuut- hc hjul ftimafHfaiut (каникнап i/ta, /i<( yhu гмни ашранс как гнала бмл&т. це¬ лая н не Лит&tTcimOKMXptktv. Вы получите исключение: [File Edit Window Help Clue % java Wrappers Exception in thread "main" java.lang.NumberFormatException: For input string: "two" at java.base/java.lang.NumberFormatException.forlnputStri ng(NumberFormatException.java:65) at java.base/java.lang.Integer.parselnt(Integer.java:652) at java.base/java.lang.Integer.parseInt(Integer.java:770) at Snippets.badParse(Snippets.java:48) at Snippets.main(Snippets.java:54) Метод/конструктор при анализе строки может выдать исключение NumberFormatException. Это исключение проявляется во время выполнения програм¬ мы, поэтому его не обяза¬ тельно перехватывать или объявлять. Но, возможно, вам захочется это сделать. (Поговорим об исключениях в главе 13.)
А теперь наоборот, превращаем простое числовое значение в строку Вам может понадобиться превратить число в строку — например, чтобы показать его пользователю или добавить в сообщение. Есть несколько способов превратить число в строку. Самый простой - конкатенировать число к существующей строке double d = 42.5; String doublestring = + d; . -f _ J String doublestring - Double. toString(d) ; doubled- 41.5; double d = 42.5; String doublestring = String.valueOf(d) Прекрасно, а как пре¬ вратить числа в деньги? Доба¬ вить знак доллара и две циф¬ ры после запятой, например $56,87, или если я захочу расста¬ вить запятые так; 45,687,890? Или если понадобится... &ни пшкжт ntfia - кскт .«стад wCm ммм стдтм- «скии .щтад J к..игса Stmxa кстсмы мЖлмип M^MttMM wfwhf у ,умк>мтсА аниге^нАннм. Где тот самый printf из языка С? Форматирование выполня¬ ется классами, отвечаю¬ щими за ввод/вывод?
Форматирование чисел В Java форматирование чисел и дат не обязательно связано с операциями ввода или вывода. Задумайтесь. Один из самых распространенных способов показать число пользователю — че¬ рез графический интерфейс. Вы вставляете строку в текстовую область, прокручиваемое поле или таблицу. Если бы форматирование работало только с операторами вывода, вы бы не смогли отобразить красиво отформатированное число в элементах графического интерфейса. На этот случай в Java предусмотрена мощная и гибкая система форматирования — с помощью класса Formatter из пакета j ava . util. Но на практике редко нужно создавать экземпляр Formatter вручную и вызывать его методы, потому что в API уже есть удобные средства форматирования — как в некоторых классах ввода/вывода (например, через printf ()), так и в классе String. Часто все сводится к вызову String . format () , где вы просто передаете нужный объект и правила форматирования. Конечно, нужно понимать, как эти правила работают. Если вы знакомы с функцией printf () из С или C++, будет проще. Но даже если нет — не беда: для начала можно использовать гото¬ вые шаблоны (мы покажем их в этой главе). Со временем вы сможете составлять свои правила форматирования и гибко управлять выводом. Начнем с простого примера и разберемся, как все это устроено. (Кстати, к теме форматирова¬ ния мы еще вернемся в главе 16.) Сделаем большие числа более читаемыми с помощью подчеркиваний (небольшое, но полезное отступление) Прежде чем перейти к форматированию чисел, позвольте немного отвлечься — ради удобства. Иногда вам нужно присваивать переменным достаточно большие начальные значения. Посмо¬ трите на три строки, которые сохраняют одно и то же значение — миллиард — в переменную типа long: long hardToRead = 1000000000; long easierToRead = 1_000_000_000; long legalButSilly = 10_0000_0000; Форматирование числа с помощью запятых запятых public class TestFormats { public static void main(String[] args) long myBillion = 1_000_000_000; String s = String, format ("% ,d" , System.out.println (s) ; 3 0 0_0 0 0_0 00; V j format("%,d”, myBillion);
Форматирование под микроскопом... На простейшем уровне форматирование состоит из двух главных частей (на самом деле их больше, но для пущей ясности мы начнем с этих). ^2^ Инструкции форматирования. Вы используете спецификаторы, которые описывают фор¬ матирование аргумента. .Щшшшнт: если, ижс ^нпке лил с cf)мнкидий jvtiwlp) иУмп\ка С/С* +. т маната iv^icnuifrraum сшшюицлс несколько стоянии,. А ши. не ^ коксит, титлйпи ^нидлдтглбне! (5) Аргумент, который будет форматироваться. Хотя аргументов может быть несколько, мы начнем с одного. Тип аргумента не произвольный: он должен быть совместим со спецификаторами в инструкциях форматирования. Например, если в ваших инструкциях указано число с плавающей точкой, нельзя передать методу format объект Dog или даже строку, которая выглядит как дробное число. y)uat и " с эгтщ. © ® format("%,d", 1 000 000 000); т \ Используем эти инструкции... в сочетании с этим аргументом. О чем на самом деле «говорят» такие инструкции? «Возьмем второй аргумент этого метода и отформати¬ руем его в виде десятичного целого числа, разделенно¬ го запятыми». Как они это «говорят»? В следующей главе мы более подробно рассмотрим значение синтаксиса%, d. Если для вас это в новинку, представьте, что знак процента (%) в строке с инструкциями для форматирования (она всегда будет первым аргументом в методе format ()) играет роль переменной, которая передается в виде второго аргумента. Символы после % описывают инструкции форматирования для аргумента.
Знак процента (%) сообщает: «Вставьте аргумент сюда» (и отформатируйте его по указанной инструкции) Первый аргумент метода format () называется строкой формата и может включать любые симво¬ лы, которые вы хотите вывести буквально, без дополнительного форматирования. Когда встречаете знак%, думайте о нем как о переменной, представляющей другой аргумент метода. Символа, Ktmcpu (juaifin (?КЛК*СНП (пш,шракапщ\ tixp- лшпха, (шшхшнщик. 'fHHmettHtic ctviiCo- ■'Ш. Kcmvpm усс&ится ДТ; . fcvmab Г ■Лргцм нт. ncxf/mad.me и Сикал ■шспиг -С String.format("Hai*fleHO%.2f, неисправленных ошибок, 476578.09876); / Найдено 476578.10 неисправленных ошибок. "ИИ-ВД^,ПМ,ЛИ'*^- Знак % говорит методу format(): «Вставь сюда второй аргумент (число) и отформатируй его согласно шаблону .2f». После этого оставшаяся часть строки — «неисправленных ошибок» — присоединяется к результату. Добавление запятой String.format("Найдено%,.2f неисправленных ошибок.", 476578.09876); Найдено 476,578.10 неисправленных ошибок.
Но откуда компьютер знает, где заканчиваются инструкции и начинается оставшийся простой текст? Почему он не выведет f в выраже¬ нии %.2f? Или 2? Откуда он знает, что ,2f — это часть инструкций, а не строки? В строке форматирования используется свой простой синтаксис Конечно, нельзя поместить после символа % что угодно. Синтаксис для выражений, следующих за знаком процента, подчиняется нескольким очень специфическим правилам и описывает способ форматирования аргумента, который вставля¬ ется в результат (в отформатированную строку) в этой позиции. Мы уже показали три примера: • %, d означает «вставляем запятые и формати¬ руем число как десятичное целое». • % . 2f означает «форматируем число как дроб¬ ное с двумя знаками после запятой». • %, . 2f означает «вставляем запятые и фор¬ матируем число как дробное с точностью два десятичных знака». Как же понять, что именно размещать после символа процента, чтобы получить требуемый результат? Для этого нужно знать символы (та¬ кие как d для десятичных и f для чисел с плаваю¬ щей точкой), а также порядок, в котором должны располагаться символы инструкции после знака процента. Например, если вы поместите запя¬ тую после символа d (%d, вместо %, d), код не сработает! Или сработает? Как вы думаете, что выведет следующая строка? String.format("Найдено%.2£, неисправленных ошибок, 476578.09876); (Ответ можно узнать на следующей странице.)
Спецификатор форматирования Все, что находится между знаком процента и индикатором типа включительно (например, d или f) — часть инструкций по форматированию. Набор символов после индикатора типа рассматривается методом format () как часть выво¬ димой строки, пока не встретится очередной знак%. Хм... Разве это возможно? Разрешено ли при форматировании указывать несколько аргументов? Пока не заморачивайтесь; мы вернемся к этой теме через пару минут. Сейчас посмотрим на синтаксис спецификаторов форматирования, следующих за знаком% и описы¬ вающих формат аргумента. Спецификатор форматирования может содержать до пяти разных элемен¬ тов (не включая символ %). Параметры в скобках [] необязательны, так что необходимо указывать только знак процента и тип. Порядок следования тоже важен, поэтому размещайте все элементы строки именно так. %[argument Л этомимы вернем сл пелт... Эпш тспи шжаТш- гумснт шшшсвапи, если ид- нехкелш:. Жскамсжт не лади мы&ится иГэтж number] [flags] [width] [.precision] type T Флат ixfmjna- ma'H ни алл допю.Д' lumvttinti.X' onw,ieie ЙиштиИобяння: xafiKU yammvc, зактжАш cmpmA' W/1UUA,V ЧШХЛ а СКОС' KU. (?(Л|ш(?И.ю(?ПНиГ no декадой краю w afoipwe Жалено ucncvu/cfiuHt нескс,.«кс ерлейев сецнсврхшпня. t fltzam&mn ,<шни лишнее кашнстСс символов, которое даишп cnufKfuAatm- i Вы. и.ш mat тс этот пара¬ метр — он jaaatт mot- з&ннос жажнш. fern тость. Юля тсы Г СЛО Konvtt, ОНО ( дополнено прсхУлами пиле керш, оно oWm (иди нид.ядт, если га¬ сит сеет &тствинщий '—I бешалшн ерлт). (хли ершПнее смает испо.еьдйтс падностьи, нитегс не С МЯ&1КШД1 тодкей — колите- ст(?о ?ндко(? после эагшпой. Не. ?а- Щерте икдллш тотни С) пф теши. \ бе&метмшш параметр. Спреаияат типэндтення: например, d - целое аесятт ное, | — тислс с н.ш(?акщей теткой. хреа сфежетхя. достипньис ти¬ пе? ruemhume на с«дцкш/й страши^. ft этой строке срсрмаеш- реванш ш указан намер аргумента, ноже оста иные пара.штра присутствуют. 3 надетое. кото|нх лею хотело от,-
Единственный обязательный спецификатор — это тип Как вы поняли, обязательно нужно указывать только тип, но не забывайте, что он всегда указывается последним! FxTb еще много разных модификаторов (помимо даты и времени, у которых есть свой набор), но в большинстве случаев вы будете использовать %d (десятич¬ ные числа) или %f (с плавающей точкой). И, скорее всего, вы будете совмещать %f с индика¬ тором разрядности, чтобы задать количество знаков после запятой. ТИП обязателен, все остальное опционально. В инструкции фор- %d Десятичное чк format (" %d" 4 2); ~)пч пк ж т l Аргумент должен быть совместим с типом int, так что это поддерживаются только значения byte, short, int и char (или их типы-обертки). %f Число с плавающей точкой format ("%. 3f'\ 42.000000) 42.000 дт ш { 'k тлк «ко GluTUit Аргумент должен быть вещественного типа, то есть либо float, либо double (примитивы или обертки), либо BigDecimal (в этой книге не рассматривается). %х Шестнадцатеричное число format("%Х" , 42) 2а Аргумент должен относиться к одному из следующих типов: byte, short, int, long (как примитивы, так и обертки) или Biglnteger. матирования не¬ обходимо указывать тип. Сколько бы спецификаторов ни указывалось, тип всегда должен сле¬ довать последним. В большинстве слу¬ чаев вы, вероятно, будете использовать модификаторы d для десятичных чисел и f для чисел с пла¬ вающей точкой. %с Символ format("%С", 42) цтА( Ц'2 tft Аргумент должен быть одного из таких типов: byte, short, char, int (примитивы или обертки).
Что случится, если использовать несколько аргументов? Представьте, что вам нужна строка следующего вида: Позиция: 20,456,654 из 100,567,890.25 Но числа хранятся в переменных. Что делать? Нужно добавить два аргумента после строки форматирования — это первый аргумент метода. В результате вызов метода format () будет со¬ держать три аргумента вместо двух. А в самой строке форматирования будут два спецификатора, начинающихся с символа %. Первый спецификатор подставит второй аргумент метода, а второй — третий. Проще говоря, аргументы вставляются в строку форматирования в том порядке, в каком они переданы в метод format (). int one = 20456654; double two = 100567890.248907; String s = String.format("Позиция: two ) ; Позиция: 20,456,654 из 100,567,890.25 ill ягпяпше G ш персшннш и так fiftncрая трслнншы) tjHOKcG после jtr — и окруътли cjc i/jGyx /нйксТ? паж ^ ш у Gac <к>ла одного afwj umn\a, они ъстав- • ииется <?тсш порядке. ” котором (к\ трашш их '■ mnuaj prnuifO. Как вы вскоре убедитесь на примере с датами, в реальной работе нередко приходится использовать несколько разных спецификаторов форматирования для одного и того же аргумента. Это может звучать запутанно, особенно если вы пока не знакомы с тем, как форматируются даты (в отличие от чисел, с которыми мы уже работали). Но не пережи¬ вайте — через минуту вы узнаете, как избирательно применять нужные спецификаторы к каждому аргументу. (Не) дурацкие вопросы &OV\pOC: Хм, здесь происходит что-то странное. Сколько вообще аргументов можно передать? Сколько перегруженных методов formatO существует в классе String? А что, если я захочу передать десять разных аргументов для форматирования одной строки? Ответ: Да, тут действительно кроется кое-что необычное (по крайней мере, если вы рань¬ ше с этим не сталкивались). Но дело вовсе не в множестве перегруженных методов format)) с разным числом аргументов. Чтобы поддержать новый форматирующий API, в Java появилась новая возможность — прием переменного количества аргументов. Скорее всего, вы столкне¬ тесь с этой особенностью только при использовании форматирования, поэтому мы подробно разберем ее в приложении.
Еще кое-что,,, статический импорт Статический импорт — это настоящее двойственное благословение. Кому- то он нравится, кто-то его терпеть не может. Он создан, чтобы сделать ваш код немного короче. Если вам не по душе лишняя печать или вас раз¬ дражают длинные строки кода, эта возможность может вам понравиться. Но у статического импорта есть и обратная сторона — если не соблюдать осторожность, код может стать гораздо менее читаемым. Основная идея проста: всякий раз, когда вы используете статический класс, статическую переменную или перечисление (о них мы поговорим чуть позже), вы може¬ те их импортировать и больше не набирать их полные имена. Без статического импорта: class NoStatic { public static void main(String [ ] args) { System.out.println("Корень квадратный:" + Math.sqrt(2.0))• System.out.println("Тангенс: " + Math.tan(60)); } Будьте осторожны: статический импорт может усложнить ваш код. Всегда пере¬ читывайте свой код после реализации статического им¬ порта и думайте: «Разберете ли вы все эти строки через полгода-год?» Тот же код , но со статическим импортом: “port 8tatic java.lang.Math.*; inport static java.lang.System.oUt; Сипттж. &'*&№*** cmJivuttcRcicw- I — Подводные камни и ловушки class WithStatic ( Public out. static void main(String[] args) Ptintln("корень квадратный:" Ptintln("ТанГенС: - l sqrt(2.0)); кшсса' i1at^^nc^ CHVCMVTV j да гшршящщ кантаа- к.*» 1 i»t имшрта. Так кшста- fewcct. Это лымь таится Ktpm, « fCaM а етмр'1 t Д w дала п/шается кажцт hcv 6tv|6artvi C,v'at сможхг писать прификс j/ai[ кДигшк ntiuduvuujK Л/ш этап L Иа(?нс mtf4 ^ иУсСианс, tnw этсмап\с~ c маткшкис спуищ11и ■ Статический импорт скрывает информа¬ цию о том, из какого класса получен стати¬ ческий член. Рекомендуем использовать его только в тех случаях, когда статический метод или переменная остаются понятны¬ ми даже без префикса с именем класса. * Главная проблема статического импорта — высокая вероятность конфликтов имен. Например, если в вашем коде встречаются два класса с методом add(), как вы или компилятор поймете, какой из них ис¬ пользовать? Лучше избегать статического импорта, если он может привести к такой неоднозначности. " Можно использовать подстановочный знак (.*) в объявлении статического импорта — 'го допустимо, но с ним тоже стоит быть осторожным.
Беседы у камина Сегодняшний разговор: Пере¬ менная экземпляра критикует статическую переменную. Переменная экземпляра Статическая переменная Честно, я не понимаю, зачем мы вообще встретились. Все и так знают: статические переменные нужны только для хранения кон¬ стант. Их в API — ну может, четыре штуки на весь арсенал. И кто их вообще использует? Не думаю, что они особо популярны. Да, Swing... это частный случай. Сколько ни говори про эти классы, больше их не станет. Ну, может, в Swing и есть немного таких переменных, но кто вообще сейчас пишет на Swing? Ладно, давай так: назови хотя бы одну стати¬ ческую переменную, которую действительно используют все. Только не из графического интерфейса. Советую проверять факты, прежде чем бро¬ саться заявлениями. Когда ты в последний раз заглядывала в Java API? Там полно статических членов! Есть даже целые классы, специально созданные для хранения констант. Взять хотя бы SwingConstants — в нем куча всего! Пусть частный случай, но важный! A Color? Хотите каждый раз запоминать RGB-коды для стандартных цветов? В классе Color уже есть готовые константы — синий, пурпурный, белый, красный... Удобно же! Да запросто! System. out. Все ее исполь¬ зуют. Это же статическая переменная класса System. Никто не создает объект System, все просто обращаются к System . out. Ну, System. out — это вообще особый случай. К тому же ее все используют только для отладки. А отладка — это, по-твоему, не важно? И во¬ обще, давай честно: статические переменные куда эффективнее. Они существуют один раз на весь класс, а не копируются в каждый объект. Представь, сколько памяти можно сэкономить!
Пережекная экземпляра Ты ничего не забыла? Статические переменные идут вразрез с осно¬ вами ООП! Почему бы не сделать гигантский шаг назад и не вернуться к процедурному про¬ граммированию? Статическая пережекная О чем речь? С чего ты взяла, что я противоречу ООП? Ты почти как глобальная переменная. А любой уважающий себя программист знает: глобаль¬ ные переменные — это зло. Да-да, живешь внутри класса... Но кто во¬ обще слышал про «класс-ориентированное программирование»? Это нелепо. Ты — пере¬ житок прошлого. Переходный мостик от процедурного к объектно-ориентированному мышлению. Ладно, признаю: есть случаи, когда исполь¬ зование статических переменных оправдано. Но вот что я тебе скажу: злоупотребление статическими переменными (и методами) — верный признак того, что программист не до конца понял ООП. Нужно мыслить объекта¬ ми, их поведением и состоянием, а не класса¬ ми и глобальностью. Они почти всегда говорят о том, что про¬ граммист думает в стиле процедурного кода. А ведь в ООП поведение объекта определяет¬ ся его внутренним состоянием, уникальным для каждого экземпляра. Ну уж нет. Я живу внутри класса, а не где-то в воздухе. Это вполне себе объектно-ориен¬ тированный подход. Я — часть структуры класса, просто общая для всех его экземпля¬ ров. Это эффективно. Вот тут я тебя перебью. Это совсем не так. Есть статические переменные, без которых система бы просто не работала. И не забывай о всех прочих, которые отлично справляются со своей задачей. А что не так со статическими методами? Согласна, объекты — это основа. Но и стати¬ ческие члены играют важную роль. Когда они нужны — никакой метод экземпляра их не заменит. Ну конечно... Утешай себя этим сколько угодно...
•Упражнения class StaticSuper { static { Компилируйте Java-код на этой странице представ¬ ляет собой полноценную программу. Ваша задача — притвориться компи¬ лятором и определить, скомпилиру- ется ли этот код. Если ком¬ пиляция не сможет пройти успешно, как вы это испра¬ вите? И каков будет резуль¬ тат работы в ином случае? System.out.println("супер статический блок"); } Какой результат вы увидите? StaticSuper() { System.out.println("конструктор супер-класса"); ) ) public class StaticTests extends StaticSuper { static int rand; static { rand = (int) (Math.random() * 6); System.out.println("статический блок " + rand); } Возможный результат File Edit Window Help Clini % java StaticTests статический блок 4 в main супер статический блок конструктор супер-класса конструктор StaticTests() { System, out .println ("конструктор") ; Возможный результат } | File Edit Window Help Electricity %java StaticTests public static void main(String[] args) { супер статический блок System.out.println("в main"); статический блок 3 StaticTests st = new StaticTests(); в main } конструктор супер-класса конструктор ♦ Ответы на стр. 344
~г\раж ражнения Эта глава познакомила вас с удивительным миром статиче¬ ских элементов в Java. Теперь вам предстоит определить, какие из следующих утверждений верны, а какие — нет. Правда или ложь^ 1. Первым шагом в использовании класса Math будет создание его экземпляра. 2. Конструктор можно пометить ключевым словом static. 3. Статические методы не имеют доступа к переменным экземпляра через объект this. 4. Для вызова статического метода рекомендуется применять ссылочную переменную. 5. Статические переменные можно использовать для подсчета экземпляров класса. 6. Конструкторы вызываются до инициализации статических переменных. 7. MAX_SIZE — хорошее название для статической финальной переменной. 8. Статический инициализатор выполняется перед вызовом конструктора класса. 9. Если класс помечен модификатором final, все его методы также должны быть финализированы. 10. Финальный метод можно переопределить только в том случае, если его класс был унаследован. 11. Для примитивного типа boolean не существует класса-обертки. 12. Класс-обертка используется, когда нужно обращаться с примитивом как с объектом. 13. Методы parseXxx всегда возвращают строку (String). 14. Классы для форматирования (которые раньше относились к вводу-выводу) находятся в пакете java. format. *■ Ответы на стр. 344
Ответы на упражнения Правда мяк ложь (СО СТр. 343) #^3аточите карандаш (со ар. 323) 1, 4, 5 и 6 допустимы. 2 не скомпилируется, потому что стати¬ ческий метод ссылается на нестатиче¬ ский экземпляр переменной. 3 не скомпилируется, потому что пере¬ менная экземпляра хоть и финальная, но не была инициализирована. Компилируйте (со стр. 342) StaticSuper 0 { System.out.println ( "конструктор супер-класса"); ) StaticSuper — это конструктор, поэтому в его сигнатуре должны быть круглые скобки (). Обратите внимание: как видно из вывода ниже, статические блоки в обоих классах выполняются раньше, чем вызывается любой из конструкторов. Результат Fil^Edi^Windo^HeJj^ling %java StaticTests супер статический блок статический блок 3 в main д, 1. Первым шагом в использовании класса Math будет создание его экземпляра. Ложь 2. Конструктор можно пометить ключевым словом static. Ложь 3. Статические методы не имеют доступа к переменным экземпляра через объект this. Правда 4. Для вызова статического метода рекомен¬ дуется применять ссылочную переменную. Ложь 5. Статические переменные можно использо¬ вать для подсчета экземпляров класса. Правда 6. Конструкторы вызываются до инициали¬ зации статических переменных. Ложь 7. MAX SIZE — хорошее название для ста¬ тической финальной переменной. Правда 8. Статический инициализатор выполняется перед вызовом конструктора класса. Правда 9. Если класс помечен модификатором final, все его методы также должны быть финали- зированы. Ложь 10. Финальный метод можно переопреде¬ лить только в том случае, если его класс был унаследован. Ложь 11. Для примитивного типа boolean не существует класса-обертки. Ложь 12. Класс-обертка используется, когда нужно обращаться с примитивом как с объектом. Правда 13. Методы parseXxx всегда возвращают строку (String). Ложь 14. Классы для форматирования (которые раньше относились к вводу-выводу) нахо¬ дятся в пакете j ava. format. Ложь конструктор супер-класса конструктор
11. Коллекции и обобщенные типы Структуры данных Фу... И все это время Java могла сама расставлять эле¬ менты по алфавиту? Л я тут муча юсь! Третий класс — это просто пытка. Мы ведь вообще ничего . полезного не узнаем... Даже Не думайте изобретать велосипед. В Java уже есть все, чтобы собирать, сортировать и крутить данные как угодно — и никакой боли с написанием собственных сортировщиков. Ну, если вы не сидите сейчас на лекции по «основам компьютерных наук»... Тогда, увы, придется страдать. Пока весь остальной мир вызывает один-единственный метод из Java API, вы будете лихорадочно набрасы¬ вать пузырьковую сортировку на бумажке в клеточку. А вот в реальном коде, вне стен университета, Java отлично экономит время. Без лишнего кода. Без шаманства. Она даже сама догадается, какие типы данных вы передали, и будет с ними работать так, как будто все это спланировали вы (а не она). Java Collections Framework — это как швейцарский нож для программиста. Хотите список, в который можно пихать новые элементы сколько угодно? Пожалуйста. Нужно мгновенно находить объект по имени? Есть. Хотите коллекцию, которая сама выбрасывает дубликаты, как строгий модератор на форуме? Без проблем. А может, вы хотите рассортировать коллег по количеству случаев, когда они начинали го¬ ворить в Zoom с выключенным микрофоном? Или домашних животных — по числу трюков, которые они усвоили? Все это уже встроено в Java. Вам остается только выбрать нужный инструмент.
Отслеживание популярности песен в музыкальном автомате Поздравляем! У вас новая работа. Добро пожаловать в закусочную Lou’s Diner вам достался музыкальный автомат. Нет, Java в этом древнем железном ящике крутится, но у автомата есть секрет: каждый раз, когда кто-то включает песню ее данные сохраняются в текстовом файле. Да, именно так — никакой базы да] ных. Лу (владелец закусочной) не доверяет «вот этим вашим современным штукам». Ваша миссия, если вы ее примете, — управлять этими музы¬ кальными данными. Сортировка, плейлисты, отчеты о самых заезженных песнях — все на ваших плечах. Не волнуйтесь, приложение разрабатывается всей командой, вам не нужно тащить все в одиночку. Но вот обработка и сортировка данных — это уже ваше поле битвы. В коллекциях. Без БД. Потому что «так сказал Лу». Данные из файла в программу загружает другой разработчик. Да, вы пока не умеете читать файлы (но скоро научитесь — через пару глав). Все, что вы получаете, — это список песен (да- да, тот самый List), который автоматически пополняется по мере работы автомата. Но ждать мы не будем. Мы не из тех, кто сидит, сложа руки, пока кто- то там «допишет функционал». Поэтому сейчас создадим небольшой тестовый сценарий, чтобы было с чем работать. Мы заранее знаем, что потом появится класс Songs с методом getSongs (), который будет выдавать нам список песен. Так что можем временно притво¬ риться, что он уже есть, и написать заглушку — специальный код, который временно заменяет настоящий. Такой код называется mock-кодом. Или «имитацией объектов». Его цель — дать вам возможность писать и тестировать логику, даже если остальная часть программы еще не готова. Часто Возникает необхо¬ димость написать Вре¬ менный код. заменяющий настоящий, который будет добаВлен позже. Это т. н. <кимитация объектоВ». Imon ,, i. 0Д (HUimUtC' MockSongs^J^^|UOVt#fM’ W <• \ public static songs = new songs.add("somersault"); songs.add("cassidy"); songs.add("$iO"); songs.add("havana"); songs.add("Cassidy"); songs.add("50 Hays"); return songs; } getSongS tringsO { ArrayLietKX) ; (Писек „ , ,UnH, c K(„, гМ f^mnatn, Жоскольки AnauXjst - этс р сшо- Щнеспи списке], мм межой dxumu cctmn./lrmu£(it сохранили т в пере¬ менней типа fjst и ьернипи этап iMt u/Aumaja. j ю ришнмх javaxxpfXKmax memo bcmjxt mtmcM косо С копиром метоа no?(ipaui/u,n\ интерфейс— например л* ~*a не кснк1ктнию реалихщик m( at xhvay/js t.' * 1 * )
Первое задание — отсортировать песни в алфавитном порядке Сначала мы напишем код, который будет считывать данные из нашего временного клас¬ са-заглушки Songs и выводить полученные песни на экран. И вот интересный момент: элементы в ArrayList хранятся в том порядке, в каком вы их добавили. Поэтому, если вы заметили, что названия песен отображаются не по алфавиту — не удивляйтесь. import java.util.*; public class Jukeboxl { public static void main(String[] new Jukeboxl().go(); \ args) { ,*Scmca™Y public void go() { List<String> songList = MockSongs.getSongStrings() System.out.printin(songList) } ■i // Ниже представлен "заглушечный" код. Он заменяет настоящий // код для ввода-вывода, который другой разработчик предоставит позже class MockSongs { public static List<String> getSongStrings() List<String> songs = new ArrayListo(); songs.add("somersault”); songs.add("cassidy"); songs.add (”$10”); songs.add("havana"); songs.add("Cassidy"); songs.add ("50 Ways"); return songs; } ) Hutch и<анаи(... pi/ЧНШ ИMf H ( |~~FiiTTairivin5(>w Help Dance %java Jukeboxl [somersault, cassidy, $10, havana, Cassidy, 50 Ways] Xu. Это тете но aufMdmnmm мшаск!
Прежде чем мы нырнем в со¬ ртировку, у меня есть вопрос насчет того, как был объявлен список песен с помощью ArrayList. Обычно тип указывается в угловых скобках — и слева, и справа от знака =. Но на пре¬ дыдущей странице справа скобки были пустыми. Что это за стиль такой? Это какой-то особый синтаксис? ж Отличный вопрос! Это ромбовидный оператор До сих пор мы объявляли списки ArrayList, дважды указывая тип эле¬ ментов, вот так: ArrayList<String> songs = new ArrayList<String>(); Чаще всего нет необходимости указывать одно и то же дважды. Компи¬ лятор может определить тип данных по значению слева, и догадаться, что должно быть справа. В этом случае выполняется т. н. выведение типа, по¬ зволяющее автоматически определить нужный тип. ArrayList<String> songs = new ArrayListO() ; s Hjt ^KOjbxSam ^ rnuiv Этот синтаксис называется ромбовидным оператором, потому что две его угловые скобки напоминают ромб (о). Он был добавлен давным-давно в версии Java 7, поэтому, скорее всего, в вашей версии Java тоже есть. Со временем Java выросла и поумнела — теперь ей не обя¬ зательно повторять одно и то же по два раза. Если компи¬ лятор и так может догадаться, какой тип вы имеете в виду, — зачем писать его снова?
(Не) дурацкие вопросы ВоИ рОС: Стоит ли всегда использовать ромбо¬ видный оператор? Есть ли у него недостатки? Ol/ИВвШ: Ромбовидный оператор — это просто синтаксический сахар, который упрощает написа¬ ние и чтение кода, но никак не влияет на байт-код. Так что, если вы переживаете, что использование ромбовидного оператора как-то отличается от явного указания типа, не волнуйтесь: по сути, это одно и то же. Однако в некоторых случаях имеет смысл явно указывать тип. Основная причина — удобочитае¬ мость кода. Например, если переменная объявлена далеко от места ее инициализации, явное указание типа при создании может помочь быстрее понять, какие объекты в нее попадают. ArrayList<String> songs; // много кода между этими строками... songs = new ArrayList<String>(); Вопрос: Есть ли другие случаи, когда компиля¬ тор может определить тип самостоятельно? 01/ивет: Да! Например, ключевое слово var (локальное выведение типа переменной), о котором мы поговорим в приложении Б. Еще один важный пример — лямбда-выражения, которые мы рассмо¬ трим позже в этой главе. Вопрос: Я заметил, что вы создаете ArrayList, но присваиваете его ссылке типа List, а также создаете ArrayList, но возвращаете List из метода. Почему бы просто не использовать ArrayList везде? Ol/ИВвП'I: Одно из преимуществ полимор¬ физма — код не нуждается в знании конкретной реализации объекта, чтобы работать с ним. List — это хорошо известный и понятный интерфейс (подробнее мы рассмотрим его в этой главе). Код, работающий с ArrayList, в большинстве случаев не нуждается в знании того, что это именно ArrayList. Это может быть LinkedList или даже специализиро¬ ванный тип List. Коду, работающему с List, важно лишь то, что он может вызывать его методы (например, add(), sized и т. д.). Обычно безопаснее передавать интерфейс (List) вместо конкретной реализации, чтобы защи¬ тить объект от нежелательных манипуляций. Кроме того, это дает гибкость: если в будущем вам потребуется заменить ArrayList на LinkedList или, например, на CopyOnWriteArrayList (подробнее в главе 18), вам не придется искать все вхождения, где используется тот или иной тип.
Исследуем API java.util, списков и коллекций Мы знаем, что в ArrayList (как и в любом списке) элементы хранятся в том порядке, в котором они были добавлены. Поэтому нам нужно будет отсортировать элементы списка песен. В этой главе мы рассмотрим наиболее важные и часто используемые классы коллекций из пакета java .util. Пока сосредоточимся на двух: java .util .List и java .util. Collections. Мы уже давно используем ArrayList. Так как ArrayList — список, а многие ме¬ тоды ArrayList унаследованы от List, мы можем применять большинство знаний о ArrayList к List без проблем. Класс Collections называют утилитным классом, потому что он содержит множество полезных методов для работы с различными типами коллекций. В «реальном мире»™ сортировка бывает разной Не всегда нужно сортировать списки в алфавитном порядке. Иногда надо отсортировать одежду по размеру или фильмы по рейтингу. Java позволяет сортировать привычным спо¬ собом по алфавиту, а также создавать собственные алгоритмы сортировки. Упоминания Comparator выше касаются пользовательской сортировки, но к этому мы вернемся позже в главе. А пока остановимся на естественном порядке (по алфавиту). Так как у нас есть List, похоже, мы нашли идеальный метод — Collections . sort ().
«Естественный порядок» — что Java подразумевает под алфавитной сортировкой? Лу хочет, чтобы вы отсортировали песни «по алфавиту», но что это вообще значит? Порядок от А до Z — это просто, но что насчет прописных и строчных букв? А как быть с числами и спецсимволами? Это еще та головоломка, но Java использует Unicode, и для большинства из нас (особенно на «Западе») это означает следующее: • Числа идут перед прописными буквами. • Прописные буквы идут перед строчными. • Некоторые спецсимволы указываются перед числами, а некоторые после. Звучит логично, да? В общем, по умолчанию сортировка в Java происходит в так называемом «естественном порядке», который в целом соответствует алфавитному. Давайте посмотрим, что произойдет, если отсортировать список песен. public void go() { List<String> songList = MockSongs.getSongStrings( System.out.println(songList); Collections.sort(songList); System.out.println(songList); } Humcchtnupo- fiwHtut список гисся ((? nCjlMcfKt асса&юоия). OmcxptnmcCan нм и список пеан. 0<f fmnuenu внимание, как uncj/imomm спщси и (?алп, числа и пптиснш еукйч.
Но теперь вам нужны объекты класса Song, а не просто строки Теперь ваш босс Лу хочет, чтобы в списке были не просто строки, а реальные экземпляры класса Song, чтобы каждая песня могла иметь больше данных. Новое устройство музыкального автомата выводит больше информации, поэтому в реальном файле будет три фрагмента информации для каждой песни. Класс Song очень прост, в нем есть только одна интересная особенность — переопределенный метод toString (). Помните, что метод toString () определен в классе Object, поэтому каждый класс в Java наследует этот метод. А поскольку метод toString () вызывается у объекта при его печати (System. out. print In (anOb j ect)), вам следует переопределить его, чтобы вывести что-то бо¬ лее читаемое, чем уникальный код идентификатора по умолчанию. Тоща при печати списка метод toString () будет вызываться для каждого объекта. class SongV2 { private String title; private String artist; private int bpm; Bet SongV2(String title, String artist, this.title = title; this.artist = artist; this.bpm = bpm; > public String getTitleO { return title; ) public String getArtistO { return artist; } public int getBpmO ( return bpm; } public String toString() { return title; } int bpm) { ЦМCL H emnu HI cole Cl CRRMT ИНН ЮГПСЧ (? KCH Н/Ш ССЛ'МННи ыкти ЗсЦ. Jemnuim — .Mncai алм |ииишш.я лие этидс ttnmuitcKU <5tem~ cvuftxt а^тедштигета *\f чилинт/сн епмекл. class MockSongs { public static List<String> getSongStrings() { ... } public static List<SongV2> getSongsV2() { List<SongV2> songs = new ArrayListo(); songs.add(new SongV2("somersault", "zero 7", 147)) • songs.add(new SongV2("cassidy", "grateful dead", 158)\ . songs.add(new SongV2("$10", "hitchhiker", 140)); songs.add(new SongV2("havana", "cabello", 105)); songs.add(new SongV2("Cassidy", "grateful dead", 158)). songs.add(new SongV2("50 ways", "simon", 102)); return songs; M 61 cg?Qcum. HjctftiiiMjt- x®9 к. iarca ,Ai cckSmcf\ nwdbi fruwcmami с нс- т-м и миткаль нем* и ыЖ. }
Изменение Jukebox: объекты SongV2 вместо String Внесем небольшие изменения в код. Теперь список будет хра¬ нить объекты <SongV2> вместо <String>. import java.util.*; public class Jukebox2 { public static void main(String[] args) { new Jukebox2().go (); fiutcrnc l M< CW/о* public void go() List<SongV2> songList = MockSongs.getSongsV2(); System.out.println(songList); л К.ШСС {(XL. Collections.sort(songList) ;^ System.out.println(songList); №kk gmomv ui qa х'Сс'тУак1 (нсСа п^шмсяяеи.лшпш icit() C^iA (Ifunufu ficu * naxK.
Код не компилируется! А ведь он прав, что задался этим вопросом, — что-то здесь не так... В клас¬ се Collections явно указан метод sort (), который принимает List. Он должен работать. Но не работает! Компилятор говорит, что не может найти метод sort (), который при¬ нимает List<SongV2>. Возможно, ему не нравится список объектов Song? Ведь List<String> его устраивал, так в чем разница между Song и String? Что именно мешает компилятору? File Edit Window Help Bummer % j avac Jukebox2.j ava Jukebox2.java:13: error: no suitable method found for sort(List<SongV2>) Collections.sort(songList); 1 error И, конечно, вы, вероятно, уже спросили себя: «А на основе чего вообще бу¬ дет происходить сортировка?» Как метод sort () поймет, по каким кри¬ териям один объект Song выше или ниже в списке, чем другой? Очевидно, если мы хотим сортировать песни по названию, нужно как-то сообщить методу сортировки, что он должен использовать t it 1е, а не, например, количество ударов в минуту. Мы рассмотрим этот момент через пару страниц. А пока давайте выяс¬ ним, почему компилятор не позволяет передать List<SongV2> в метод sort ( ) .
о о Объявление метода sortf) static(<f’extends Comparable^? super~T>>)void sortffCTst<T>)list) SortsrmrspefiHed list into ascending order, according to the natural ordering of its elements. В документации API (если открыть класс java.util.Collections и найти метод sort()), его объявление выглядит... странно. Ну, по крайней мере, не так, как мы привыкли видеть. Все потому, что метод sort() (как и многие другие элементы фреймворка коллекций в Java) активно использует обобщенные типы. Всякий раз, когда в коде или документации Java вы видите угловые скобки (о), это значит, что используются обобщенные типы — функциональность, добавленная в Java 5. Похоже, прежде чем мы разберемся, почему метод sort() работает со списками String, а не Song, нам придется научиться читать и понимать документацию к обобщенным типам.
Обобщенные типы означают большую безопасность типов Хотя обобщенные типы можно использовать по-разному, чаще всего они применяются для создания типобезопасных коллекций. Это оз¬ начает, что компилятор не позволит вам, например, добавить собаку (Dog) в список уток (Ducks). Без обобщенных типов компилятор не следит за тем, какие объекты хранятся в коллекции, так как все реализации коллекций работают с Object. Это означает, что без обобщенных типов в Ar rayLi s t можно добавить любой объект, потому что он ведет себя как ArrayList<Object>. Без обобщенных типов Объекты попадают в коллек¬ цию как ссылки на объекты SoccerBall, Fish, Guitar и Car. ЯспсЛС.1.И1«с(Гс^ннхи>п«ихс(? ' - л '"и цксишш ниш сищ лтпс<| *кт типа' \ @1 4 4 % 4 4 ArrayList 4 4 4 4 И извлекаются как ссылка типа Object. С обобщенными типами Объекты попадают в коллек¬ цию только как ссылки на объекты Fish. I I I I ArrayList<Fish> АЛ 1 1 Без обобщенных типов компилятор спокойно позво¬ лил бы добавить объект Pumpkin в Array List, кото¬ рый должен содер¬ жать только объек¬ ты Cat. С обобщенными типами можно соз¬ давать типобезо- пасные коллекции, где больше ошибок обнаруживается на этапе компиляции, а не во время вы¬ полнения. чтим с там, тс ктс И извлекаются как ссылки типа Fish. сти к типу Тйк. ' '
Обобщенные типы Можно долго изучать обобщенные типы. Но в работе большин¬ ства программистов важное значение имеют три вещи: (?) Создание экземпляров обобщенных классов (например, ArrayList). Когда вы создаете ArrayList, вам нужно указать тип new ArrayList<Song> () объектов, которые будут допустимы в списке, так же как это делается со старыми добрыми массивами. (2) Объявление и присваивание переменных обобщенных типов. Как полиморфизм сочетается с обобщенны List<Song> songList = ми типами? Если у вас есть переменная-ссылка new ArrayList<Song>() типа ArrayList<Animal>, можно ли присво¬ ить ей ArrayList<Dog>? А что насчет ссылки типа L i s t <An ima 1 > ? Можно ли присвоить ей ArrayList<Animal>? Вы скоро узнаете... (3) Объявление (и вызов) методов, принимающих обобщенные типы. Если у вас есть метод, который принимает в качестве void foo (List<Song> list) параметра, например, ArrayList объектов Animal, что это на самом деле означает? Можно ли передать ему х , foo (songList) ArrayList объектов Dog? Мы рассмотрим некоторые тонкие и сложные вопросы полиморфизма, которые от¬ личаются от методов с обычными массивами. (Честно говоря, это тот же пункт, что и раньше, — но мы повторим его, потому что он реально важен.) (We) дурацкие вопросы BoinpOC: Но подождите... Мне ведь нужно уметь создавать собствен¬ ные обобщенные классы, верно? А что, если я захочу написать класс, в котором пользователь сам будет решать, с какими типами работать? Ом&ет Технически — да. Практически — скорее всего, нет. Серьезно, подумайте: разработчики Java API уже написали целую кол¬ лекцию... коллекций! Списки, множества, очереди, словари — все это уже обобщено и готово к работе. Именно классы коллекций чаще всего бывают обобщенными. Иногда — утилиты, которые с ними работают. Ну и, конечно, такие ребята, как Optional (о нем мы поговорим в следующей главе). Вообще, обобщенный класс — это класс, который хранит или обра¬ батывает объекты типа, который вы заранее не знаете. Так что, да, может быть, однажды вы и создадите свой обобщенный класс — и это будет кру¬ то. Но это уже продвинутая тема, а у нас сейчас другая цель — научиться уверенно пользоваться тем, что уже есть, и понимать, как все это устро¬ ено. А с тем фундаментом, который вы уже строите, вы точно справитесь, когда придет время написать свой первый обобщенный класс.
Использование обобщенных классов Так как ArrayList — один из самых часто использу¬ емых обобщенных классов, начнем с его документации. Два ключевых момента, на которые стоит обратить вни¬ мание в обобщенном классе: 1. Объявление класса 2. Объявления методов, которые позволяют добавлять элементы Интерпретация документации ArrayList (Или каково истинное значение буквы Е?) ! OT.r"r i,mu ШИР’ ( - эп'1' 'Anayhst - это мак шссЛ&ЪасЬЦ* pt.ibHcl «w * Jfalvr nwtmuu.HccicH тип. кстс/тй Си цка- ’ L икаги^.ДпацШ- Ш VяЛпашСйТ, aCmdumumcnu L"st<E> public class ArrayList<E> extends AbstractList<E> implements i .. { public boolean add(E o) Ban йяж-нми 6ccm&mrm6ta ( (njMijt.imm. какиеос&скпш .шж‘н£aocaQ- .1.4 ни. £. ArrayList Jim. Onaiam (!) нитей и суя UHmcfbJx:№i /Js1. // прочий код E представляет тип, используемый при создании экзем¬ пляра ArrayList. Когда вы видите Е в документации ArrayList, мысленно заменяйте его на тот тип, кото¬ рый вы указали при создании ArrayList. Например, new ArrayList<Song> означает, что Е заменяется на Song во всех объявлениях методов и переменных, где ис¬ пользуется Е. Представьте, что Е — это просто сокращение от фразы: «тип эле¬ мента, который долж¬ на хранить и возвра¬ щать эта коллекция». Буква Е — от слова Element (элемент).
Использование параметров типа с ArrayList ЭТОТ код: List<String> thisList = new ArrayListO Означает ArrayList: public class ArrayList<E>| extends AbstractList<E> public boolean add(E o) // прочий код } { Компилятор воспринимает его как: public class ArrayList<String> extends AbstractList<String>. public boolean add(String o) // прочий код ) Другими словами, Е заменяется реальным типом (также называемым параме¬ тром типа), который вы указываете при создании ArrayList. Именно по¬ этому метод add () в ArrayList не позволит добавить ничего, кроме объектов ссылочного типа, совместимого с типом Е. Например, если вы создадите ArrayList<String>, то метод add () примет вид add ( String о). Если же ArrayList будет типа Dog, тогда метод add () станет add (Dog о). (Не) дурацкие вопросы Вопрос: А «Е» — это единственный вариант? Просто я видел в документации к sort, что там используется Т... Омъет: Нет, вы не обязаны использовать именно Е. Можно взять любое имя, которое подходит как идентификатор в Java. То же самое, что и для переменных или методов — никаких магических ограничений. Но на практике программисты почти всегда используют одиночные буквы — это стало традицией. Вот самые популярные: Т — от Туре, если речь просто о типе вообще, Е — от Element, когда вы пишете кол¬ лекцию и хотите указать «тип элементов, которые она хранит», R — от Return type, когда нужно обозначить возвращаемый тип.
Обобщенные методы Обобщенный класс означает, что в объявлении класса использует¬ ся параметр типа. Обобщенный метод означает, что в объявлении метода параметр типа используется в его сигнатуре. Вы можете использовать параметры типа в методе несколькими способами: (?) Использование параметра типа, определенного в объявлении класса public class ArrayList<E> extends AbstractList<E> . •• ( public boolean add (Ijo) , ' / кчк fciatu к weed Когда вы объявляете параметр типа для класса, вы мо¬ жете использовать этот тип везде, где обычно исполь¬ зуется реальный класс или интерфейс. Тип, указанный в аргументе метода, фактически заменяется на тот, ко¬ торый вы указываете при создании экземпляра класса. (5) Использование параметра типа, который НЕ был определен в объявлении класса --— -а public <Т extends Animal> void takeThing(ArrayList<T> list) Если сам класс не использует параметр типа, его все равно можно задать для метода, объявив его в необычном (но до¬ пустимом) месте — перед возвращаемым типом. Этот метод указывает, что Т может быть «любым типом Animal». ■bfca.uH.тжем испил/ птсии тс (ЛлбилиЧ/ га 11 (^лблсп им „u/mccja. нАти ; б mi
Подождите, неужели это правиль¬ но? Если метод может принимать ArrayList<Animal>, почему бы просто не написать takeThing(ArrayList<Animal> list)? Что не так с таким объявлением? Вот тут все становится странным... Так код: public <Т extends Animal> void takeThing (ArrayList<T> list) не то же самое, что такой: public void takeThing (ArrayList<Animal> list) Оба варианта допустимы, но они различаются! Первый, где <Т extends Animal> указан в объявлении метода, означает, что можно передавать ArrayList, содер¬ жащий Animal или любой его подтип (например, Dog или Cat). То есть этот метод можно вызвать с ArrayList<Dog>, ArrayList<Cat> или ArrayList<Animal>. А вот второй вариант, где аргумент метода объявлен как ArrayList<Animal> list, допускает толь¬ ко ArrayList<Animal>. То есть ArrayList<Dog> или ArrayList<Cat> передать уже нельзя — только ArrayList<Animal>. И да, это действительно кажется нарушением принципов поли¬ морфизма, но позже мы разберем это подробнее. Сейчас важно помнить, что мы обратили на это внимание только потому, что пытаемся понять, как работает метод sort (), а это привело нас к его объявлению с использованием обобщенных типов. Пока что главное — знать, что первый вариант синтаксиса допустим и позволяет передавать объект ArrayList, со¬ держащий Animal или его подтипы. А теперь вернемся к нашему методу sort ()...
File Edit Window Help Bummer % j avac Jukebox2.j ava Jukebox2.java:13: error: no suitable method found for sort(List<SongV2>) Collections.sort(songList); 1 error import java.util.*; public class Jukebox2 ( public static void main(String[] args) ( new Jukebox2().go () ; } public void go() { List<SongV2> songList = MockSongs System.out.printIn(songList) ; Collections. sort (songList) ; System.out.println(songList) ; 1 ) getSongsV2(); /jcm met -wo м nju шжтт. cdwJ Jfyu*. пере¬ дан txst<Jltinuf> fee ршюта.лс нс/тальню, uc fuiK только „ш? noiuwnruurt omoopnu^ftmu fji(.<SoufV2\ nfwj/ашл c&a.
Возвращаемся к методу sortf) И вот мы здесь, пытаемся прочитать документацию по методу sort (), чтобы выяснить, почему можно отсортировать список строк, но не список объектов Song. И похоже, ответ найден... according to the natural ordering of its elements. Метод sort() может принимать только списки объектов Comparable. Song не является подтипом Comparable, поэтому вы не можете приме¬ нить метод sort() к списку объектов Song. По крайней мере, пока... public static <Т extends Comparable<? т Зека говорите^-' «Лакей m смл '■ сн. млшхп сини тана ГенушмвРа». (На эту часть пека не с/Л раш+пте (тирания. Не если ти интересно, это ежспает, гни napa,utmf\ пшпегаы (Ьтрашбве ejexmw ctxmt Т или c^tuui суперклассов!) Т» void sort(List<T> list) Вь\ можете перeqatfanu тальке Hist (или его шр mutt, наприллер, ArtayHist), исполни кзиуш ixapaMcxnjxu л (a tt tui it mutt который ърасш ирмип СстрашвСе». Эм... Я только что проверила до¬ кументацию для String, и String не расширяет Comparable — он реализует его. Comparable — это интерфейс. Так что выра¬ жение <Т extends Comparable» не имеет смысла. public final class String implements java.io.Serializable, Comparable<String>, CharSequence { Отличное замечание, и оно заслуживает подробного ответа! Переверните страницу...
В обобщенных типах extends означает не только «расширяет», но и «реализует» Инженеры Java должны были дать вам способ наложить ограни¬ чение на параметризованный тип, чтобы вы могли ограничить его, скажем, только подклассами Animal. Но также нужно огра¬ ничить тип, позволяя только классы, которые реализуют опре¬ деленный интерфейс. Таким образом, здесь возникает ситуация, когда нам нужен один синтаксис, который работает для обеих ситуаций — наследования и реализации. И победившим словом стало... extends. Но на самом деле оно означает «IS-А» и работает независимо от того, является ли тип справа интерфейсом или классом. СспФашвСе - это uM\tf\(ptuc. так /ни: frtMc.u qt-it это стачает: «Таишхк r.WtU mum;in котивши uwmefuptkc б cm/- public static <Т extends СошрагаЫе<? super Т KeSaxcm, явхмтсмлп mo. ттс стоит rtyia&z. классам, или UHmepcjxiuau... вех Цавш испаиле- ется extend,!. В обобщенных типах ключевое слово extends на самом деле означает «IS-А» и работает как для классов, так и для интерфейсов. Т» void sort(List<T> list) (Не) дурацкие вопросы Вой рос: А почему бы им просто не добавить новое ключевое слово? Например, is? Ои1 вет: Потому что добавление нового ключевого слова — это большое дело. Серьезно. Это как взорвать старый код по всей планете. Представьте; кто-то уже исполь¬ зовал переменную с именем is (в этой книге мы, кстати, тоже так делали — для входного потока). А теперь внезапно is становится ключевым словом. И бах — весь старый код больше не компилируется. Паника. Слезы. Тостеры не работают. Ключевые слова нельзя использовать как имена переменных или методов, так что такие изменения могут по¬ ломать кучу легитимного, прекрасно работающего кода. Именно поэтому, когда раз¬ работчики языка могут переиспользовать уже существующее ключевое слово (вроде extends), — они так и делают. Это аккуратно и безопасно. Но! В последние годы Java начала вводить псевдоключевые слова — такие, как var. Это не полноценные ключевые слова, а скорее зарезервированные конструкции, которые рабо¬ тают в определенных контекстах, но не блокируют старые имена переменных. Например, если у вас раньше была переменная var, — все нормально, она продолжит работать даже в Java 10+, где появилось var для вывода типа, о которой мы поговорим в Приложении Б. Так что теперь язык развивается осторожно, не задевая старых добрых программ, на¬ писанных до нас.
Наконец-то мы знаем, что не так... Класс Song: должен реализовать Comparable Мы можем передать ArrayList<Song> в метод sort () , только если класс Song реализует интерфейс Comparable, так как именно так был объявлен метод sort () . В документа¬ ции API указано, что интерфейс Comparable на самом деле очень прост и содержит всего один метод для реализации: java.Iang.Comparable А в документации к методу compareTo () говорится: Возвращает: отрицательное целое число, если данный объект меньше указанного объекта; ноль, если объекты равны; положительное целое число, если данный объект больше указанного объекта. Похоже, что метод compareTo () вызывается на одном объекте Song, а в качестве аргумента ему передается дру¬ гой объект Song. Объект, на котором вызывается метод, должен определиться: должен ли другой Song стоять выше, ниже или на том же месте в списке. Так что ваша главная задача сейчас — определить, что делает одну песню «больше» другой, а потом реализовать метод compareTo () согласно этому правилу. Вот как это работает. Если метод возвращает отрицательное число, это значит: «Эй, переданный мне Song круче, чем я; он дол¬ жен стоять выше в списке!» Если вернется положительное число — значит, текущий объект круче: «Я важнее, сортируй меня повыше!» А если метод возвра¬ щает ноль — песни считаются равными (для сортировки). Это не обязательно означает, что это один и тот же объект. Например, у вас может быть две песни с одинаковым на¬ званием, но разными исполнителями. Главный вопрос: что именно делает одну песню «меньше», «равной» или «больше» другой? Пока вы не определитесь с этим, вы не сможете реализовать интерфейс Comparable. аточите карандаш Напишите свою идею и псевдокод (а лучше — рабочий код) для реализа¬ ции метода сотрагеТо() таким образом, чтобы отсортировать объекты Song по названию. Подсказка: если вы все делаете правиль¬ но, у вас получится меньше трех строк кода! -► Решать-вам! И это уже совсем другой уровень головоломки. Но до него мы еще доберемся...
Новая, улучшенная и сравниваемая версия класса Song Мы решили сортировать песни по названию, поэтому реализуем метод compareTo (), который будет сравнивать название переданной в ме¬ тод песни с названием той, на которой этот метод вызван. Другими сло¬ вами, песня, выполняющая метод, должна определить, как ее название соотносится с названием переданной в метод песни. Хм... Мы знаем, что класс String уже умеет разбираться в алфавит¬ ном порядке, ведь метод sort () прекрасно сработал со списком строк. Кроме того, у String есть свой метод compareTo (), так почему бы просто не вызвать его? Одна строка будет сравнивать себя с другой, и нам не придется писать алгоритм сравнения и сортировки вручную! class SongV3 implements Comparable<SongV3> private String title; private String artist; private int bpm; public int compareTo(SongV3 s) { return title.compareTo(s.getTitle()) } F=- CdivtM э гам mium an m... jtt м он mun, c Kcmo/иш класс. «c<uwwmu» i '■ ■■ М\тигм/)ш <nm •tx&i MH- /'H/I'dun, Л. mcJvixm .uawtmrti mb W.Mnn Scnab <№ cfuidmGatM c ijfvfiimu естеюшши. <)cncp i. ,/llama wtt() ntfmun m сшитScmj.uan^ mnmnJcO, там спшшть как он ссстно- cmL с штк, на коЦш шап.мат^. SongV3(String title, String artist, this.title = title; this.artist = artist; this.bpm = bpm; int bpm) { T/rftnwZ CmfKK^bUi а,фкТс(1 ’ } public String getTitle() { return title; } public String getArtistO { return artist; ) public int getBpmO { return bpm; } public String toStringO { return title; } Fite Edit Window Help Ambient %java Jukebox3 [somersault, cassidy, $10, havana, Cassidy, 50 ways] [$10, 50 ways, Cassidy, cassidy, havana, somersault] 1 )
Список-то сортируется, но... Теперь у нас новая проблема: Лу хочет просматривать список пе¬ сен двумя разными способами — по названию и по исполнителю! Но если элемент коллекции реализует Comparable, то у нас есть только одна возможность реализовать метод compareTo (). Как быть? Этого мало. Иногда мне нужно, чтобы сортировка выполнялась по исполните¬ лю, а не по названию. С> о Худший из возможных вариантов — это добавить в класс Song флаговую переменную, а потом внутри compareTo () устраивать разборки через if: сравнивать ли по названию или по исполни¬ телю. Такой подход превращает код в кашу. На этот случай в API уже есть инструмент, который позволяет сортировать один и тот же объект разными способами! Загляните еще раз в документацию к API коллекций. Там есть второй метод sort(), который принимает Comparator А еще есть метод sort() у List, который тоже принимает Comparator. Выдержка из API java.util.Collections sort(List): Сортирует указанный список в порядке возрастания, согласно естественному порядку элементов. sort(List, Comparator): Сортирует указанный список в порядке, заданном Comparator. java.util.List sort(Comparator): Сортирует текущий список в порядке, заданном Comparator. 5SW wyiaratcr. п<> ft классе Test nuotctecmtJ мд t(). KcmojvNU ntuuueMaurv Сс№|\атакт. Нам. HipcHo ссмап\ь CcmjramtcT, шле/рт c|m(?hu&wu u ^\о|\я<|0- шбать tumu rvt иепшнателк (lucerne касания.
Работа пользовательского компаратора Когда элемент списка реализует Comparable, он может сравнивать себя с другим объектом сво¬ его типа только одним способом — через метод compareTo (). Но Comparator — это отдельный класс, независимый от типа элементов, которые он сравнивает. Это значит, что вы можете создать сколь¬ ко угодно компараторов! Нужно сравнивать песни по исполнителю? Создай ArtistComparator. Нужно сортировать по темпу (ВРМ — бит в минуту)? Сде¬ лайте BpmComparator. После этого просто вызывайте метод sort (), который принимает Comparator (например, Collections, sort () или List.sortO). Он использует переданный Comparator, чтобы отсо¬ ртировать список. Метод sort (), который принимает Comparator, будет использовать его для сортировки вместо метода compareTo () у элементов списка. Другими словами, если в sort () передан Comparator, то метод compareTo () у элементов вообще не вызы¬ вается. Вместо этого sort () будет вызывать метод compare () у переданного Comparator. Все вышесказанное можно обобщить в два правила: ^ Вызов метода Collections.sort(List list) означает, что по¬ рядок сортировки определяется методом compareTo() элементов списка. При этом элементы списка обязаны реализовывать интерфейс Comparable. ^ Вызов List.sort(Comparator с) или Collections.sort(List list, Comparator с) означает, что для сортировки будет использоваться метод compare() переданного Comparator. В этом случае элементы списка не обязаны реализовывать интерфейс Comparable, но если они его реализуют, их метод compareTo() не будет вызван. java.util.Comparator Если передать Comparator методу sort(), порядок сор¬ тировки будет определять¬ ся этим Comparator. Если Comparator не пере¬ дан, но элементы списка реализуют Comparable, то порядок сортировки будет определяться их методом compareTof). (Не) дурацкие вопросы Вопрос: Почему существуют два метода sort(), принимающих Comparator, в разных классах? Какой из них использовать? Омвет. Оба метода, Collections.sort(List, Comparator) и List. sort(Comparator), делают одно и то же, поэтому можно использовать лю¬ бой из них и получить одинаковый результат. Метод List.sort(Comparator) появился в версии Java 8, поэтому более старый код использует Collections.sort(List, Comparator). Мы предпочитаем List.sortO, так как он короче и логичнее: обычно у вас уже есть список, который нужно отсор¬ тировать, поэтому удобнее вызвать sort() прямо на нем.
Модификация Jukebox для поддержки Comparator Мы внесем три изменения в код Jukebox: 1. Создадим отдельный класс, реализующий Comparator (и, соответственно, метод compare (), который теперь будет выполнять ту же работу, что раньше выполнял compareTo ()). 2. Создадим экземпляр класса Comparator. 3. Вызовем метод List. sort (), передав ему этот экземпляр Comparator. import java.util.*; public class Jukebox'} [ public static void main(String[] args) { new Jukebox!().go 0 ; } public void go() { List<SongV3> songList = MockSor.gs . getSor.gsV3 () ; System, out .println (sor.gList) ; (смимэкр. Collections . sort (songList) ; ConfUiMh- System.out.println(songList) ; ArtistCompare artistCompare = new ArtistCompare() songList.sort(artistCompare); System.out.println(songList); ) n класса fh\m Gum , mnmj мг(0 < писка, ncjlujadши an mi на mdak no; jc&nnc некий ешкт (unparatu class ArtistCompare implements Comparator<SongV3> { public int compare(SongV3 one, SongV3 two) { return one.getArtist().compareTo(two.getArtist()) ) ) 9 me строка (артист) (треки tyuatm сравни - (amt. exm nc a. upadumu. пеэталш uene ttyum u{ nanpM.mjtc ерш серпик рс(’ки испслнитсим. File Edit Window Help Ambient %java Jukebox4 [somersault, cassidy, $10, havana, Cassidy, 50 ways] [$10, 50 ways, Cassidy, cassidy, havana, somersault] [havana, Cassidy, cassidy, $10, 50 ways, somersault] L (?«*¥%*■ *~Lm, «*«*№<*> класса и (c . , WHO hfj Цг>и, »<‘mc ,4 (У«гаГсо
4|^этотите карандаш Заполните пропуски Для каждого из приведенных ниже вопросов запол¬ ните пропуск одним из слов из списка «возможные ответы», чтобы правильно ответить на вопрос. Possible Answers: Comparator, Comparable, compareTo(), compare(), да нет Дан следующий компилируемый оператор: Collections.sort(myArrayList); 1. Какой интерфейс должен реализовывать класс объектов, хранящихся в myArrayList? 2. Какой метод должен реализовывать класс объектов, хранящихся в myArrayList? 3. Может ли класс объектов, хранящихся в myArrayList, реализовывать и Comparator, и Comparable? Даны следующие компилируемые операторы (они оба выполняют одно и то же действие): Collections.sort(myArrayList, myCompare); myArrayList.sort(myCompare); 4. Может ли класс объектов, хранящихся в myArrayList, реализовать Comparable? 5. Может ли класс объектов, хранящихся в myArrayList, реализовать Comparator? 6. Должен ли класс объектов, хранящихся в myArrayList, реализовать Comparable? 7. Должен ли класс объектов, хранящихся в myArrayList, реализовывать Comparator? 8. Что должен реализовать класс объекта myCompare? 9. Какой метод должен реализовать класс объекта myCompare? *- Ответы иа стр. 400
Стоп! Мы сортируем двумя разными способами! Теперь мы можем сортировать список песен двумя способами: 1. С помощью Collections, sort (songList), потому что Song реализует Comparable. 2. С помощью songList. sort (artistCompare), потому что класс ArtistCompare реализует Comparator. Хотя наш новый код позволяет сортировать песни как по названию, так и по исполнителю, он напоми¬ нает монстра Франкенштейна, собранного по кусочкам. public void go() { List<SongV3> songList = MockSongs.getSongsV3(); System. out .print In (songList); j —- nt .AiAfancM (cmharaM Collections . sort (songList) ' System.out.printIn(songList); ArtistCompare artistCompare = songList.sort(artistCompare); System.out.printIn(songList); } new ArtistCompare(); - 3<]ta ссртШсбки мок. и^щтся п<хц*}&1- mutcMu (снумг&сг Более правильным подходом было бы определить всю логику сортировки в классах, реализующих Comparator. (Не) дурацкие вопросы ЗОИ рОС: Значит ли это, что если у вас есть класс, который не реализует Comparable и у вас нет доступа к его исходному коду, вы все равно можете отсорти¬ ровать его объекты, создав Comparator? ОкИВвЖ: Совершенно верно. Вы можете написать собственный Comparator и использовать его при сорти¬ ровке. Альтернативный путь — если он доступен — соз¬ дать подкласс нужного типа и реализовать Comparable в нем. ЗОИ рОС: Почему тогда не все классы реализуют Comparable? Oi/ивеиФ Потому что не для всех типов существует естественный порядок. Не все можно (или нужно) срав¬ нивать. Если вы попытаетесь заставить сравниваться объекты, которые по своей сути не поддаются упорядо¬ чиванию, реализация Comparable может ввести в за¬ блуждение других программистов — они будут ожидать логичной сортировки там, где ее быть не должно. Но ничего страшного: если нужен конкретный способ срав¬ нения, всегда можно создать свой Comparator и описать, как именно вы хотите упорядочить объекты.
Сортировка только с помощью компараторов Если Song реализует Comparable и создаст собственный Comparator для сортировки по исполните¬ лю, то это абсолютно точно сработает, но полагаться на два разных механизма для сортировки — зна¬ чит получить путаницу. Гораздо понятнее, если наш код будет использовать одну и ту же технику для сортировки, независимо от того, как Лу хочет сортировать свои песни. Код ниже был обновлен, чтобы использовать компараторы для сортировки как по названию, так и по исполнителю; новый код выделен жирным шрифтом. public class Jukebox5 { public static void main(String[] args) { new Jukeboxb().go(); ) public void go() { List<SongV3> songList = MockSongs.getSongsV3() ; System.out.println(songList) ; (лмщлк. экд.ил тяй класса TitleCompare titleCompare = new TitleCompare (); u иСшиШМ songList. sort (titleCompare) ; mt() * System.out.println(songList) ; a ArtistCompare artistCompare = new ArtistCompare() songList.sort(artistCompare); System.out.println(songList) ; I } class TitleCompare implements Comparator<SongV3> { public int compare(SongV3 one, SongV3 two) { return one.getTitle().compareTo(two.getTitle()); > m класс. } class ArtistCompare implements Comparator<SongV3> { public int compare(SongV3 one, SongV3 two) { return one.getArtist() .compareTo(two.getArtist() ) ; } ) II сюда можно добавить более специализированные классы Comparator, // например, для сравнения по bpm __ q О Да уж, ради сорти¬ ровки песен всего парой способов кода получается многовато. А нельзя как- то покороче?
Вся магия — в этих строчках Класс Jukebox действительно содержит много кода, связанного с сортиров¬ кой. Давайте внимательно посмотрим на один из написанных нами классов Comparator. Первое, что бросается в глаза, — вся логика сортировки заключает¬ ся в одной строке кода в середине класса. Все остальное — это просто громоздкий синтаксис, который нужен компилятору, чтобы понять, что это за класс и какой метод он реализует. К*><) пос) лупой class TitleCompare implements Comparator<Song> { public int compare (Song one, Song two) { return one. getTitle () . compareTo (t'wo. getTitle () ); } 3a Sew .тики cm- (?w« m.„u<uu энш cmfwtKa Kuja } Есть несколько способов объявить небольшие куски функциональности вро¬ де этого. Один из них — внутренние классы, о которых мы поговорим позже. Можно даже объявить класс прямо там, где он используется, — это называет¬ ся «анонимным внутренним классом, определенным в аргументе метода». songList.sort(new Comparator<SongV3>() { public int compare(SongV3 one, SongV3 two) { return one.getTitle().compareTo(two.getTitle()); ) )) ; Хотя этот способ позволяет объявить логику сортировки прямо там, где она применяется (а не в отдельном классе), кода все равно слишком много для простого запроса в духе «отсортируй по на¬ Расслз^ься- Мы не будем подробно разбирать «анонимные внутренние классы, определенные в аргумен¬ те метода». Это просто пример. Но они могут встретиться вам в реальном коде. званию».
Что же действительно нужно для сортировки? public class Jukeboxb { public void go() { List<SongV3> songList MockSongs.getSongsV3(); TitleCompare titleCompare = new TitleCompare(); О songList.sort(titleCompare) ; ^ ) f class TitleCompare implements Comparator<SongV3> { public int compare(SongV3 one, SongV3 two) { return one.getTitle().compareTo(two.getTitle()); } ) Л <мтШпирпсиима- z-*' mw Шщаст Давайте взглянем в документацию к API для метода сортировки List: . ,. ... . , „ . Сортирует этот список в соответ- default void sort(Co«parator<? super E> c) v ствии с порядком, на который ука¬ зывает определенный компаратор Если бы мы объясняли следующую строку кода простыми словами: songList.sort(titleCompare); мы могли бы сказать: «Вызовите метод sort для списка песен Q н передайте ему ссылку на объект Comparator, который специально предназначен для сортировки объектов Song ф». Если быть честными, мы могли бы сказать все это, даже не глядя на класс TitleCompare. Мы можем понять это просто по документации метода sort и по типу списка, который сортируем! Прокачай извилины Как вы думаете, компилятору важно имя TitleCompare? Если бы класс назывался FooBar, код все равно бы работал?
Было бы здорово, если бы логика сортировки лежала прямо рядом с методом sort, а не где-то в глубине классов. И вообще, разве компилятор сам не может догадать¬ ся о типах, чтобы нам не прихо¬ дилось все расписывать вручную? J Вот бы избавиться от всей этой лишней писанины...
Лямбда-выражения: меньше кода, больше смысла Мы, конечно, могли бы снова накатать гору кода, чтобы опи¬ сать сортировку списка... как уже бывало. з г упи Ьагс.иши ссылка па ссыкт. ко¬ пир tm реалирцот СопрштЫ. (го имен не и.лшгп такшя. сия каимолятопа. songList. sort (titleCompare) ; ■) я ' Конти, совершен кс не1 важно, как на" рываотся Kiacc. class TitleCompare implements Comparator<Song> { ( id Кошшлятр мажет fist- ншпн эtno ня аокиминтамш к и ) > ’ Кеотмлятсп сам по 1-С. г jDa, каипиля риьИс int compare (Song one, Song two) { mop рнает как аалжт (7м- гляаош этот. MUTMj- ) return one.getTitle() .conipareTo(two.getTitle()) HM..H/«m, rmc оса, осыкта J.V/CHM amt типа.Sena . .XKCUtKUKtUjJjS t — 9ШС тасок (тают Setup } (/ли мы можем исиользо- ваииь лямбда-выражение. Шрселтскужтксм- ^ I ^ Лрсспи скажите tuf *SAK йтсяняпи ортщревку! songList.sort((one, two) -> one.getTitle().compareTo(two.getTitle())); 5mc первая песня и аП(,- рая песня, паранат ' штора (рав^енмя ' Прокачай извилины Как вы думаете, что бы произошло, если бы Comparator требовал реализации более чем од¬ ного метода? Сколько всего компилятор смог бы определить сам?
Куда делся весь код? Чтобы ответить на этот вопрос, давайте обратимся к документации API по интерфейсу Comparator. Method Summary Modifier and Тура int boolean Method coepare(T ol, T o2) equals*Object obj) Compares its two arguments for order Indicates whether some other object i "equal to" this comparator. Поскольку метод equalsQ уже реализован в Object, если мы создаем собственный Comparator, нам нужно реализовать только метод compare(). Мы знаем, какой должна быть его форма: он должен возвращать int и принимать два аргумента типа Т (не забывайте про обобщенные типы). Наше лямбда-выражение реализует метод compareO, без необходимости объявлять сам класс или метод: нужно указать только логику внутри compare(). # темните. tmo гобоКилсск б мдбс &: бге. к.ш«м и инпиц сртсы нас. исшит лшпо^и mv класса Ofc№ шлк тисме и uutvuj ctpa&O, Kcmcjutw ipa icuum р«.1нри,ик к б 6|u l m идииШНММ)/ Некоторые интерфейсы имеют только ОДИН метод для реализации С такими интерфейсами, как Comparator, нам нужно реализовать только один абстрактный метод — single abstract method, сокращенно SAM. Эти интерфейсы настолько важны, что что у них есть несколько специальных имен: SAM-интерфейсы также известны как функциональные интерфейсы Если интерфейс содержит только один метод, его можно реализовать с помощью лямбда-выражения. Вам не нужно создавать отдельный класс для его реализации: компилятор уже знает, как будет выглядеть класс и метод. Единственное, чего компилятор не знает, — это логика, которая должна быть внутри метода. Именно эту часть вы указываете в лямбда-выражении. \1 / Расслабься Мы рассмотрим лямбда-выражения и функциональные интерфейсы под¬ робнее в следующей главе. А пока воз¬ вращаемся в закусочную Лу.
Обновление кода Jukebox с помощью -лямбда import java.util.*; public class Jukebox6 { public static void main(String[] args) { new Jukebox6().go(); } public void go() { List<SongV3> songList = MockSongs.getSongsV3(); System.out.println (songList) ; ^ KiiiW-ЛЧ r ..up hmw-w- v г -a—««»логики 1 .iirx/RC OC^(Klwairu Vi«•*! ч sett. songList.sort((one, two) -> one.getTitle().compareTo(two.getTitle'O ) System.out.println(songList); «> ^ ^ Лшмарш nouwu. no ктлш mm r%m cmro- songList.sort((one, two) -> one.getArtist().con^areTo(two7getArtist())); System.out.println(songList); остдшся menu токил* жх, Как fvfiu uma-(i|<i('®uu' классов (cnxfuctaht не kcxj < itpa^yc коротс. спин И File Edit Window Help Ambient %java Jukebox6 [somersault, cassidy, $10, havana, Cassidy, 50 ways] [$10, 50 ways, Cassidy, cassidy, havana, somersault] [havana, Cassidy, cassidy, $10, 50 ways, somersault] «точите карандаш — Как можно отсортировать песни по-другому? Напишите лямбда-выражения для сортировки песен следующими способами (ответы в конце главы): • Сортировка по темпу • Сортировка по названию в порядке убывания ► Ответы ва стр. 402
Обратное проектирование Предположим, что этот код находится в одном фай¬ ле. Ваша задача — заполнить пропущенные места так, чтобы программа создала показанный вывод. import ; public class SortMountains { public static void main(String [] args) { new SortMountains().go(); } public void go() { List mountains = new ArrayList<>() ; mountains.add(new Mountain("Longs", 14255)); mountains.add(new Mountain("Elbert", 14433)); mountains.add(new Mountain("Maroon", 14156)); mountains.add(new Mountain("Castle", 14265)); System.out.println("as entered:\n" + mountains); mountains. ( -> ) ; System.out.println("by name:\n" + mountains); • ( “> ) ; System.out.println("by height:\n" + mountains); } > class Mountain ( аточите карандаш { Output: | File Edit Window Help ^iisOne^ForBob | %java SortMountains в порядке ввода: [Longs 14255, Elbert 14433, Maroon 14156, Castle 14265] по названию: [Castle 14265, Elbert 14433, Longs 14255, Maroon 14156] по высоте: *■ Ответы на стр. 401 [Elbert 14433, Castle 14265, Longs 14255, Maroon 14156]
Ой-ой. Сортировка работает, но теперь у нас есть дубликаты... Программа работает отлично, и теперь мы умеем сортировать песни как по названию, так и по исполнителю. Но появилась новая проблема, которую мы не заметили при тестировании спи¬ ска песен для музыкального автомата: в отсортированном списке есть дубликаты. В отличие от заглушки, в реальном приложении музыкального автомата Лу, похоже, песни просто продолжают записываться в файл, независимо от того, была ли эта песня уже проигра¬ на (и, соответственно, записана в текстовый файл). Файл SongListMore.txt содержит полный список всех сыгранных песен, и одна и та же песня может встречаться в нем несколько раз. Fie Edit Window Help TooManyNotes %java Jukebox7 [somersault: zero 7, cassidy: grateful dead, $10: hitchhiker, havana: cabello, $10: hitchhiker, cassidy: grateful dead, 50 ways: simon] [$10: hitchhiker, $10: hitchhiker, 50 ways: simon, cassidy: grateful dead, cassidy: grateful dead, havana: cabello, somersault: zero 7] [havana: cabello, cassidy: grateful dead, cassidy: grateful dead, $10: hitchhiker, $10: hitchhiker, 50 ways: simon, somersault: zero 7] /)( артитовки .float cxf\mu- ncdku no КД- I&wuk menu fl vcM cc - lUHMflO'ifou no лm он ЗУ K UU(X Ц mu' « 1 ,i m ««и лнитс м/ IU. Bene как фаи-л о посиди SongListAAore.txt somersault, zero 7, 147 cassidy, grateful dead, 158 $10, hitchhiker, 140 havana, cabello, 105 $10, hitchhiker, 140 cassidy, grateful dead, 158 50 ways, simon, 102 Файл ScnaC/AtMcti. txt mmeht со- ffvmm. мейикапш. папами zrnes лецтка Май а&памат ттеа- fiacm кажешю <?сс1Шилщеннию песня по Пермаку. Чтет палишть при&деипше йиие Мса,ма написали класс . Mock 41 тмпт с межсаам tjetSencj.sO, коеперый воМеаш/тп List, содержащий flccSne же тпи- си, гте и этот тексепеййе таил.
Нам нужно множество, а не список В API коллекций есть три ключевых интерфейса: List, Set и Мар. ArrayList реализует List, но в нашем случае, похоже, подойдет Set. X LIST — когда важен порядок Коллекции, учитывающие положение элементов по индексу. List хранит информа¬ цию о местоположении каждого элемента. В списке может быть несколько элементов, ссылающихся на один и тот же объект. X SET — когда важна уникальность Коллекции, которые не допускают дубликатов. Set проверяет, есть ли элемент уже в коллекции. В Set не может быть более одного элемента, ссылающегося на один и тот же объект (или двух элементов, которые считаются равными; мы скоро разберемся, что озна¬ чает равенство объектов). jOipfiuKmnct? нега Set X MAP—когда важно находить значение по ключу Коллекции, использующие пары «ключ-значение». Мар хранит значение, связанное с определенным ключом. Два разных ключа могут ссылаться на одно и то же значение, но ключи не могут повторяться. Ключом может быть любой объект. Мар
API коллекций (часть) Обратите внимание, что интерфейс Мар на самом деле не расширя¬ ет интерфейс Collection, но при этом Мар все равно считается частью Collection Framework (также известного как API коллекций). То есть Мар все же относится к коллекциям, хотя он не включает j ava. util. Collection в своем дереве наследования. (Примечание: это не полное описание API коллекций; существуют и другие классы и интерфейсы, но эти — самые важные для нас.) Collection (интерфейс) List (интерфейс) 3 TreeSet J LinkedHashSet j ^HaS>lSet | — | LinkedList Vector T Легенда расширяет реализует Map (интерфейс) У Tree Map Mof\ нс шсшиЦмип java. iditCvCmticn, но Vet jna6\нс стттпся юапьюАРЗ кшжк- 11,nit (?Java. Лсэтг.ни Jiajy тхжс m?b\Va.m\. LinkedHashMap Hashtable
Замена ArrayList на HashSet Мы обновили код Jukebox, загрузили песни в HashSet и попробовали избавиться от дубликатов (Примечание: часть кода опущена — вы можете взять ее из предыдущих версий класса Jukebox ) import java.util.*; public class Jukebox8 { public static void main(String[] args) { new Jukebox8().go(); 1 public void go() { List<SongV3> songList = MockMoreSongs.getSongsV3() System.out.println(songList); -Si „ ■ .„ПОР"' I .,,, )(CC fl (П/*) и mt.UM -,Л t I songList.sort((one, two) -> one.getTitle().compareTo(two.getTitle())); System.out.println(songList); rSet<SongV3> songSet = new HashSetO(songList); System.out.println(songSet); Нам Н1Ш КС. шсск&А сшмыссикпиг SwriVS. Лмкомки HasMa — это пишт OS-А) mmctufxMaSd. мьитжм сокра¬ тит Нмкш (!псрслитск типа М. ^Л/к нс теге, у ИтШ тт КШг ШсУт.. (• (ЧНюп анстопй, (ik-ik raiauM ^^ттмшшкажищии. File Edit Window Help GetBetterMusic %java Jukebox8 [somersault, cassidy, $10, havana, $10, cassidy, 50 ways] [$10, $10, 50 ways, cassidy, cassidy, havana, somersault] [$10, 50 ways, havana, cassidy, $10, cassidy, somersault] I— CCjW\i{j\cd 1 км списка |~ Jfcaccc- ртиревки ПО HOj&lMKHJ но этот ftniuo пишем no аж...) JlcCAt ПйЛШМ/Ни.Я (? HaskSd и Onlkqa HasMd ( tm нс m- p\&).AU ,4‘ltO tWjipaj). Множество не помогло! У нас все еще есть все дубликаты!
Определение равенства двух объектов Чтобы понять, почему дубликаты все же остались при использо¬ вании Set, стоит задать себе вопрос: что делает две ссылки на Song дубликатами? Они должны считаться равными — но что это значит? Дубликаты — это две ссылки на один и тот же объ¬ ект в памяти или два разных объекта с одинаковым названием? Обратите внимание на важный момент: равенство ссылок * равенство объектов. X Равенство ссылок Две ссылки, один объект в куче. Если два объекта, foo и bar, равны, то foo.equals(bar) и bar.equals(foo) должны возвращать true, и оба объекта foo и bar должны воз¬ вращать одинаковое значение из метода hashCode(). Чтобы Set считал два объекта ду¬ бликатами, необходимо переопре¬ делить методы HashCodeO и equals)), унаследованные от класса Object, чтобы можно было считать два разных объекта равными. Две ссылки считаются равными, если они указывают на один и тот же объект в куче. Точка. Если вызвать метод hashCode() для обеих ссылок, он вернет одинаковое значение. Если не переопределять hashCode(), по умолчанию (наследуется от класса Object) каждый объект получит свой уникальный хеш-код. Во многих версиях Java он основан на адресе объекта в памяти, поэтому у объектов не может быть одинаковый номер. Если нужно проверить, действительно ли две ссылки указывают на один объект, используют оператор ==, который сравнивает биты в переменных. Если обе ссылки указывают на один объект, их битовое представление будет одинаковым. Song if (songl == sor.g2) { // обе ссылки указывают //на один и тот же объект в куче ) Равенство объектов Две ссылки, два объекта могут находиться в па¬ мяти (куче), но при этом считаться эквивалентны¬ ми по смыслу. Если вы хотите, чтобы два разных объекта Song считались равными (например, когда у них одинаковое значение поля title), нужно переопределить оба метода — equals() и hashCodeQ, которые унаследованы от класса Object. Как уже упоминалось, если не переопределить hashCode(), то по умолчанию (наследуемое поведение от класса Object) каждый объект получит уникальный хеш-код. Поэтому, чтобы два эквивалентных объекта возвращали одинаковый хеш-код, необходимо переопределить hashCodeQ. Но также нужно переопределить equals(), чтобы вызов этого метода на одном объекте с передачей на другой объект всегда возвращал true, если они считаются равными. if (sor.gl. equals (sor.g2) && sor.gl . hashCode () == sor.g2 .hashCode () ) { // обе ссылки указывают либо на один и тот же объект, // либо на два объекта, которые равны
Как HashSet проверяет наличие дубликатов: методы has\nCode() и equals^) Когда объект добавляется в HashSet, вызывает¬ ся соответствующий метод hashCode (), чтобы определить, куда поместить этот объект во мно¬ жество. Затем HashSet сравнивает полученный хеш-код со значениями всех существующих объектов в HashSet. Если совпадений нет, HashSet считает, что объект новый и не дубликат. То есть если хеш-коды различаются, HashSet авто¬ матически предполагает, что объекты не могут быть равны! Поэтому важно переопределить hashCode (), чтобы убедиться, что одинаковые объекты получают одинаковый хеш-код. Но даже если два объекта имеют одинаковый хеш-код, это еще не значит, что они равны (об этом подробнее на следующей странице). Поэтому, если HashSet находит совпадающий хеш-код у добав¬ ляемого объекта и уже существующего в наборе, он вызывает метод equals () одного из них, чтобы проверить, действительно ли они равны. Если метод equals () возвращает true, HashSet понимает, что объект является дубликатом и не добавляет его. При этом исключение не выбрасывается, но метод add () у HashSet возвращает логическое значение, указывая, был ли объектдобавлен. Если метод add () вернул false, это значит, что объект уже присут¬ ствовал в наборе и его добавление не произошло. Мне нужно знать, одинаковы ли ваши значения sv_ hashCode(). . \ 'О toshCode() -[Л hashCode() 742 \с~ Объект, кстсрмк нити цегaftmu GHtiskU. Сшкт иш н/ш - ачипся bKu.shSd. title: Circles hashCode: 742 Сшкт. кспирпй u:m. нашщтсм ^ Ъ(тШ' CdttKm, KciTUluiu (?m птишпеа Maif’umi. fiunfotm сьсччметоа естаСлО. chaGHuGcmn. ctm c 4cntj2 u Ы yfjuuu/um true. Ваши хеш-коды одинаковы, но дей- -, ствительно ли вы равны? , e4uals( so ng2)
Класс Song с переопределенными методами haskCode() и equals^) class SongV4 implements Comparable<SongV4> { „ ■m&'KHM.W.Zd »3,nCr private String title; ЛЦ фрт private String artist; Jw*'^ .i цсНЬК^1-*" М1 V J r ,.,л ...... ' > OnwmiH иобсспм: Ut&-jm private int bpm; X public boolean equals(Object aSong) { SongV4 other = (SongV4) aSong; return title.equals(other.getTitle()) } public int hashCode() { return title.hashCode() > Jcjux cam* u лип i] к- ..mm it.... с,т*м. a u строка wta wttu iuf\t (Лш,н.шашлшпи) ufita&O. Cma- coCma^am ia te лшпнш. < на y&lHttt« hw wt Kcmcpw HlpUfMHVM. ^ 7У Ж/ tufu.шипун Jpcmy public int compareTo(SongV4 s return title.compareTo(s.getTitle ()); } SongV4(String title, String artist, this.title = title; this.artist = artist; this.bpm = bpm; } public String getTitle () { return title; } int bpm) { Jcntfu det jwcctmtml jDucCiuKa- aui схл.ишс Ht псяИхяктся при dnduu HashStl. Лх мы опять нс dtmtida (н лс■(((), u >\jm qcmdunuu ymmj/j.st dJimixStl nijmqvK се¬ рп лиревки нарушился public String getArtistO return artist; ) public int getBpmO { return bpm; } public String toStringO return title; ) File Edit Window Help HashingltOut | %java Jukebox9 [somersault, cassidy, $10, havana, $10, cassidy, 50 ways] [$10, $10, 50 ways, cassidy, cassidy, havana, somersault] [havana, $10, 50 ways, cassidy, somersault]
Документация API для класса Object определяет обязательные правила: V Если два объекта считаются равными, их HashCodeO обязан возвращать одинаковый хеш- код. ' V Если два объекта равны, вызов equals!) для любого из них ДОЛЖЕН вернуть true. То есть, если a.equals(b), то обязательно b.equals(a). X Если два объекта имеют одинаковый хеш-код, это не означает, что они равны. Но если они рав¬ ны, то ОБЯЗАНЫ иметь одинаковый хеш-код. V При переопределении метода equals!) ОБЯЗА¬ ТЕЛЬНО переопределите HashCodeO. ^ По умолчанию метод HashCodeO генерирует уникальный код для каждого объекта в памяти. Поэтому, если его не переопределить, два объек¬ та одного класса никогда не будут равны. ^ По умолчанию метод equals!) выполняет срав¬ нение с помощью оператора ==, т. е. проверяет, ссылаются ли две переменные на один и тот же объект в памяти. Если equals!) не переопределен, два разных объекта никогда не будут считаться равными, так как у них всегда разные ссылки. Если a.equals(b), то обязательно a.hashCode!) == b.hashCode!). Но если a.hashCode!): чает, что a.equals(b) b.hashCode!), это НЕ озна- (Не) дурацкие вопросы вопрос: Как так получается, что у разных объектов хеш-коды могут совпадать? Ответ-. HashSet использует хеш-коды для хранения элементов так, чтобы к ним мож¬ но было получить доступ намного быстрее. Если вы попытаетесь найти объект в ArrayList, передав ему копию объекта (вместо индекса), ArrayList должен начать поиск с самого начала, проверяя каждый элемент списка на совпа¬ дение. Но HashSet может находить объекты гораздо быстрее, потому что он использует хеш-код как своего рода метку для «ведра», в которое был помещен объект. То есть, если вы говорите: «Найди в множестве объект, точно такой же, как этот...», HashSet получает хеш-код переданного объекта (например, 742), после чего сообщает: «О, я точно знаю, где находится объект с хеш-кодом #742...» — и сразу идет к «ведру» с этим номером. Это, конечно, не все, что нужно знать о прин¬ ципе работы. Гораздо больше можно узнать, изучив курс по структурам данных, — но и этого вполне достаточно, чтобы эффективно использовать HashSet. Разработка хорошего алгоритма хеширования — это тема для множе¬ ства научных диссертаций, и мы не будем в нее углубляться в этой книге. Главное, что нужно понять: одинаковые хеш- коды не гарантируют, что объекты равны. Это происходит потому, что алгоритм хеширова¬ ния, используемый в методе hashCodeO, может случайно выдать одинаковый результат для нескольких разных объектов. Да, это значит, что несколько объектов могут оказаться в одном и том же «ведре» в HashSet, но в этом нет ничего страшного. Просто в таком случае HashSet будет работать немного мед¬ леннее: он пройдет по всем объектам в ведре и вызовет метод equals!) для каждого, чтобы выяснить, есть ли среди них действительно совпадающий.
Если нужна сортировка элементов в множестве можно воспользоваться TreeSet Множество TreeSet похож на HashSet тем, что не допускает дублирования элементов. Но, в от¬ личие от HashSet, в нем элементы сохраняются в отсортированном порядке. Принцип тот же, что и метод sort (): если создать экземпляр TreeSet без передачи ему Comparator, он будет использовать метод compareTo () каждого объекта для сортировки. Однако у вас есть возможность передать Comparator в конструктор TreeSet, и тогда он будет использовать этот компаратор вместо метода compareTo (). Минус TreeSet в том, что даже если вам не нужна сортировка, вы все равно тратите на нее ре¬ сурсы, что может немного снизить производительность. Однако в большинстве приложений эта разница будет практически незаметна. public class JukeboxlO { public static void main(String[] args) ( new JukeboxlO().go (); l public void go() { List<SongV4> songList = MockMoreSongs.getSongsV4()• System.out.printIn(songList) ; songList.sort((one, two) -> one.getTitle().compareTo(two.getTitle())) ; System.out.printIn(songList) ; Set<SongV4> songSet = new TreeSetO(sonaList-Л • Если мы хотим, чтобы TreeSet сортировался по-другому (т.е. не использовал метод сотрагеТоО в SongV4), нам нужно передать компаратор (или лямбда-выражение) в конструктор TreeSet. Затем мы бы использовали songSet.addAHQ, чтобы добавить значения списка песен в TreeSet. Set<SongV4> songSet = new TreeSetO((ol, o2) -> ol.getBpm() - o2.getBpm()); songSet.addAll(songList); System.out.printIn(songSet);
Что СЛЕДУЕТ знать о Tree Set... Множество TreeSet кажется простым, но разобраться с тонкостями его использования необходимо. Мы считаем это настолько важным, что при¬ думали для вас специальное упражнение, так что придется пораскинуть мозгами. Не переворачивайте страницу, пока не сделаете его. Серьезно. *• Ответы на стр. 402 оточите карандаш import java.util.*; public class TestTree { Взгляните на приве¬ денный код. Внима¬ тельно прочитайте его и ответьте на public static void main(String[] args) { new TestTree().go(); > (Примечание: в коде нет синтаксических ошибок) вопросы ниже. public void go() { Book bl = new Book("How Cats Work"); Book Ь2 = new Book("Remix your Body"); Book ЬЗ = new Book("Finding Emo"); Set<Book> tree = new TreeSetO(); tree, add (Ы) ; tree.add(b2); tree.add(b3); System.out.println(tree); class Book { private String title; public Book(String t) { title = t; > } 1. Каков будет результат компиляции? 2. Что произойдет при запуске класса TestTree, если он успешно скомпилируется? 3. Как исправить проблемы в коде (на этапе компиляции или выполнения), если они есть? } )
Элементы TreeSet ДОЛЖНЫ быть сравнимыми Множество TreeSet не умеет читать мысли программиста и не знает заранее, как именно нужно сортировать объекты. Вам нужно указать TreeSet, как это делать. Чтобы использовать TreeSet, одно из этих условий должно быть истинным: ^ Элементы списка должны быть типом, который реализует интерфейс Comparable Класс Book на предыдущей стра¬ нице не реализовывал Comparable, поэтому он не работал бы во время выполнения. Подумайте сами: несчастное множество TreeSet существует только для того, чтобы хранить элементы в отсортированном по¬ рядке, и не знает, как сортировать объекты Book! Во время компиляции ошибка не про¬ является, потому что метод add() у TreeSet не требует, чтобы добавляемый объект реализовывал Comparable. Метод add() принимает любой тип, который был указан при создании TreeSet. Другими словами, согласно коду newTreeSet<Book>() метод add() превращается в add(Book). А для класса Book нет обязательного требования реализовывать Comparable! Но ошибка возникнет уже во время выполнения, когда вы попытаетесь добавить второй эле¬ мент в множество. Именно в этот момент TreeSet впервые попробует вызвать метод compareToO у объекта... и не может. ИЛИ У Вы используете перегружен¬ ный конструктор TreeSet, который принимает Comparator Множество TreeSet очень похоже на метод sort(). У вас есть выбор: использовать метод compareToO элементов (при условии, что их тип реализует интерфейс Comparable) ИЛИ задать свой Comparator, который определяет, как сортировать элементы множества. Чтобы использовать собственный Comparator, нужно вызвать конструктор TreeSet, который принимает Comparator в качестве аргумента. class Book implements Comparable<Book> | private String title; public Book(String t) ( title = t; public int compareTo(Book other) { return title.compareTo(other.title); ) class BookCompare implements Comparator<Book> { public int compare(Book one, Book two) { return one.title.compareTo(two.title); ^ * /5(л дюжете umc u- 70 ftuiu лямаш-вk- 'цажшчс 6,-umru public class TestTreeComparator { ОГЪЯЙГСГШТ К(6ж public void go о { клака Ccnfatatci. Book Ы « new Book("How Cats Work"); Book L>2 = new Book ("Remix your Body"); Book b3 = new Book("Finding Emo"); BookCompare bookCompare = new BookCompare(); Set<Book> tree = new TreeSeto(bookCompare); tree.add(bl); tree.add(b2); t ree.add(ЬЗ); System.out.println(tree); }
Списки и множества есть, пришло время словарей Списки и множества — это замечательно, но иногда лучшим выбором оказывается словарь (Мар) (используются термины «отображение» и «ассоциативный массив»). Не путайте с интерфейсом Collection: словари являются частью коллекций Java, но не реализуют интерфейс Collection. Представьте, что вам нужна коллекция, устроенная как список свойств: вы передаете имя, а взамен получаете связанное с ним значение. Ключами в словарях могут быть любые объекты Java (или даже примитивы благодаря автоупаковке), но чаще всего в качестве ключей используют строки (например, для хранения именованных свойств) или целые числа (в частности, для представления уникальных идентификаторов). •JHatmut Мар Каждый элемент в словаре — на самом деле пара: ключ и значение. Значения могут повторяться, а ключи — нет. Пример словаря public class TestMap { public static void main(String[] args) { Map<String, Integer» scores = new HashMap<>(); 5L Hern.. scores.put("Кэти", 42); scores.put("Берт", 343); scores.put("Скайлер", 420) wtuAuukmt липшпшЮ &шст add(), ко пиши lyuiHHuaem y&i .ушдшнтл (к.iwr, jHartnut). System.out.print in(scores!; System.out.print 1: scores.get("Берт")); u .An mu, jnimnu у tjdO tyutHHAuitm ni.iot н dytyiaui/itm ни (вугжноц слупи, ссъскт типа Jnluja). File Edit Window Help Where Am I %java TestMap {Скайлер=420, Берт=343, Кэти=42} 343 ется них (aCHWtWL njuUHHMH ним nfw списков имнашстО. тавля- twetH- K&IX T
Постоянно вижу один и тот \ же код для создания коллек¬ ций. Думаю, должен быть способ упростить это дело для разра- | . ботчиков. А Отличная мысль! Сейчас накидаю пару фабричных ме¬ тодов — будет быстро и удобно. г Создание и наполнение коллекции И опять код для создания и запол¬ нения коллекции. Вы уже не раз встречали строки, где создается ArrayList и в него добавляются элементы. Например, вот так: List<String> songs = new ArrayListO() ; songs. add("somersault'') ; songs.add("cassidy"); songs.add("$10"); Независимо от того, создаете ли вы списки, множества и словари, код вы¬ глядит похоже. К тому же такие коллекции часто содержат заранее известные данные, которые не планируется изменять в дальнейшем. А если бы мы дей¬ ствительно хотели гарантировать, что коллекция останется неизменной после создания, пришлось бы добавить еще один шаг: List<String> songs = new ArrayListO() ; songs.add("somersault") ; songs.add("cassidy"); songs.add("$10"); return Collections.umnodifiableLfst(songs) Мы возвращаем «неизменяемую» версию списка, который только что создали, чтобы быть уверенными — никто не сможет его изменить. В главах 12 и 18 вы увидите, почему в не¬ которых случаях важно использовать такие структуры данных: которые нельзя модифицировать. (ладлкел ш шю! Cetmvw 1(с Get й(Ши mp, кстскше лсШ. ^ л*#, (нмилнапм снеба vv mc^1- Г ^m сличай vvtmmuA c Java 9 wwflftA**1 J'a P'vtu, l'|xH]''UT\vut vuM\tim a\a fb ^c.M«u,uc л» ааскщтм * - etjKfifwww Galj МШ\' t<|uu GkucG.
Удобные фабричные методы для коллекций Методы Convenience Factory — это именно то, что подразумевает название: удобный способ, который по¬ дойдет в большинстве случаев, когда нужно создать коллекцию, заранее заполненную данными. А если эти фабричные методы вам не подходят, всегда можно использовать конструкторы коллекций и методы add () и put(). Созданные фабричными методами коллекции нельзя изменить. Вы не можете добавлять в них элементы или изменять значения; более того, даже сортировка, о которой шла речь в этой главе, недоступна. 2^ Созданные фабричными методами коллекции отличаются от ранее изученных нами. Это не ArrayList, HashSet, HashMap и так далее. Вы можете быть уверены, что коллекции будут вести себя в соответствии со своим интерфейсом: List — сохраняет порядок добавления, Set — не допускает дубликатов. Но не стоит полагаться на то, что они основаны на какой-то конкретной реализации List, Set или Мар. Удобные фабричные методы для коллекций позволяют легко создавать списки, множества и словари, кото¬ рые уже содержат известные данные. Однако при их использовании важно понимать несколько вещей: ^ Создание списка: List, of () Чтобы создать список строк, приведенный на предыдущей странице, нам не нужно писать пять строк кода — достаточно одной: List<String> strings = List.of("somersault", "cassidy", "$10"); Нели вместо обычных строк вы хотите добавить объекты Song, код останется таким же коротким и по¬ нятным: List<SongV4> songs = List.of(new SongV4("somersault", "zero 7", 147), new SongV4("cassidy", "grateful dead", 158), new SongV4("$10", "hitchhiker", 140)); ^ Создание множества: Set. of () При создании множества используется весьма схожий синтаксис: Set<Book> books = Set.of(new Book("How Cats Work"), new Book("Remix your Body"), new Book("Finding Emo")); ^ Создание словаря: Map. of (), Map. of Entries () Словари (отображения) отличаются, так как для каждой «записи» требуется два объекта — ключ и значе¬ ние. Если вам нужно добавить в словарь менее 10 записей, вы можете использовать Мар . of, передавая ключи и значения: Map<String, Integer> scores = Map.of("Kathy", 42, "Bert", 343, "Skyler", 420); Если у вас больше 10 записей или если вы хотите более явно указать, какие ключи соответствуют каким значениям, вместо этого можно использовать Мар . of Entrie s: Map<String, String> stores = Map.ofEntries(Map.entry("Riley", "Supersports"), Map.entry("Brooklyn", "Camera World"), Map.entry("Jay", "Homecase")); Чтобы укоротить строку, можно использовать статический импорт для Мар. entry (мы говорили о ста¬ тических импортах в главе 10).
Наконец, возвращаемся к обобщенным типам Помните, в начале главы мы говорили о том, что методы, принимающие аргументы с обобщенными типами, могут вести себя... странно? Под словом «странно» мы по¬ нимаем полиморфизм. Эта информация может показаться немного нелогичной. Мой совет: продолжайте читать, потребуется несколько страниц, чтобы полностью рас¬ крыть тему. В примерах мы будем использовать иерархию класса Animals. abstract class Animal { void eat() { System.out.println("кошачья хавка"); ) > class Dog extends Animal { void bark() { } ) class Cat extends Animal { void meow() { } ) Использование полиморфных аргументов и обобщенных типов Обобщенные типы могут быть немного... неинтуитивными, когда дело касается использова¬ ния полиморфизма с обобщенным типом (класс внутри угловых скобок). Давайте создадим метод, который принимает List<Animal>, и поэкспериментируем с ним. Передача в List<Animal> public class TestGenericsl { public static void main(String[] args) T"yn< List<Animal> animals = List.of(new Dog() takeAnimals(animals) 1 public static for (Animal a a.eat(); new Cat(), new Dog()) ' -ЦНш1тт ^Ы<АытаС> (> наш.лит су ШАпипай. void takeAnimals(List<Animal> animals) { ^ animals) { 4 lUmuf. Kcnufmk KM« класс (hf>l) ка~ ncmdc naWMWfa. 1 tZVlTS wx ЙШЙТО JUAcAuмстит самМннт } 6 пшпеЛпгтаС. так как nafuiutmh аптаСл имеет пищ JList</h\in\aC>. Компилируется и выполняется без ошибок. | File Edit Window Help CatFoodlsBetter %java TestGenericsl кошачья хавка кошачья хавка кошачья хавка
Но будет ли это работать с List< Dog>? Аргумент List<Animal> можно передать методу с параметром List<Animal>. Так что главный вопрос — примет ли параметр List<Animal> список List<Dog>? Разве не для этого создавался полиморфизм? Передача List<Dog> public void go() I List<Animal> animals takeAnimals(animals) = List.of(new Dog(), new Cat(), MtojMtiLA. rmoanuicn [\arXnui ш Hipjuaum yu ка List<Dog> dogs = List.of(new Dog(), new Dog()); takeAnimals (dogs) ; amc Wtn(/1fc ■ my ja «(н m. a u. euwcu Q naJuLstf new Dog()); C cjcjauini список JUoy н у пи С неге пару са/ак. public void takeAnimals List<Animal> animals { for (Animal a : animals) { a . eat (); Когда мы компилируем это: File Edit Window Help CatsA re Smarter % javac TestGenerics2.java TestGenerics2.java:20: error: incompatible types: List<Dog> cannot be converted to List<Animal> takeAnimals(dogs); ficc (’гхг.лмер. и ко ww-mo паи.-к не тегк. 1 error
И что, мне теперь просто смириться с этим? Да это же разру¬ шает всю мою симуляцию животных! Ветеринар должен принимать список любых зверей, чтобы приют собак мог передать спи¬ сок собак, а кошачий питомник — список кошек... А теперь ты мне говоришь, , что так нельзя?! А что, если это разрешить? Представьте, что компилятор позволил бы вам передать List<Dog> в метод, объявленный так: public void takeAnimals (List<Animal> animals) { for (Animal a : animals) { a.eat(); } ) В этом методе нет ничего опасного, верно? В конце концов, суть полиморфизма в том, что все, что может делать Animal (в данном случае метод eat () ), может делать и Dog. Так в чем же проблема, если метод вызывает eat () для каждого объекта Dog? С этим кодом действительно все в порядке. Но теперь представьте такой код: public void takeAnimals (List<Animal> animals) { animals. add (new Cat ()) , „ j,jM тшкс mw cjC(Q(l\JLXu Cat С список, прщна?нагтм\и таикс (xfbm)W$jDecj. Так вот в чем проблема. Добавлять Cat в List<Animal> абсолютно нормально — ведь смысл использования списка суперкласса Animal как раз в том, чтобы можно было хранить в нем любых животных. Но если бы вы передали List<Dog> — спи¬ сок, предназначенный ТОЛЬКО для объектов Dog, — в этот метод, который при¬ нимает List<Animal>, то в итоге в списке объектов Dog неожиданно оказался бы объект Cat. Компилятор знает, что если допустить передачу List<Dog> в этот метод, то во время выполнения программы можно случайно добавить Cat в этот список. Поэтому компилятор просто не позволяет рисковать. Если метод объявлен с параметром List<Animal>, он может принимать ТОЛЬКО List<Animal>, но не List<Dog> и не List<Cat>.
Мне кажется, должен быть способ использовать полиморфные коллекции в качестве аргументов методов, чтобы ветеринар¬ ная программа могла принимать списки собак и списки кошек. Тогда можно было бы перебрать элементы списка и вызвать у них метод immunize(). При этом нужно соблю¬ сти безопасность, чтобы в список собак случайно не попала кошка. Применяем подстановочные знаки Выглядит немного необычно, но есть способ создать аргумент метода, принимающий список любого подкласса Animal. Самый простой — использовать подстановочные знаки: public void takeAnimals (List<? extends Animal> animals) for (Animal a : animals) ( 4. a.eatO; 7C „• jnutb K.iK.ftfe£«&ьхПтЬ ) } Теперь вы, наверное, задаетесь вопросом: «А в чем разница? Разве проблема не осталась той же?» И это вполне разумный вопрос. Но ответ — нет. Если вы используете подстановочный знак < ?> в объ¬ явлении, компилятор не даст вам добавлять элементы в список! Когда вы используете подстановочный знак в аргументе метода, компилятор строго запретит все, что потенциально может навредить переданному списку. Вы все еще можете вызывать методы у элементов списка, но не сможете до¬ бавлять в него новые элементы. Иными словами: с содержимым списка работать можно, а вот добавлять туда что-либо уже нельзя. (Не) дурацкие вопросы ЗОИ рОС: Раньше, когда мы только начинали разбирать¬ ся в обобщенных методах, мы видели похожий пример; там обобщенный тип объявлялся перед именем метода. Это то же самое, что и метод takeAnimals? Омвет Отличный вопрос! В начале главы действитель¬ но был метод, выглядевший вот так: <Т extends Animal> void takeThing(List<T> list) Мы могли бы использовать такой синтаксис, чтобы добиться похожего результата. Но работает он немного иначе. Да, вы можете передать в него и List<Animal>, и ListxDog, — все будет нормально. Зато в вашем распоряжении появляется дополнительное преимущество: вы можете использовать тип Т и в других частях метода, а не только в параметрах.
Применение обобщенного параметра метода Что мы можем сделать, если определим наш метод вот так: public <Т extends Animal> void takeAnimals(List<T> list) j На данный момент — не так уж много, ведь сейчас в методе нам не нужно использовать Т. Но если мы изменим метод, например, чтобы он возвращал Li s t со всеми животными, которым успешно сделали прививку, то можем указать, что возвращаемый List имеет тот же обобщенный тип, что и переданный в метод: public <Т extends Animal> List<T> takeAnimals(List<T> lis^ Когда вы вызываете метод, вы получите обратно тот же тип, который передали в него. List<Dog> dogs = List.of (new Dog(), new DogO); List<Dog> vaccinatedDogs = takeAnimals(dogs); List<Animal> animals = List.of(new Dog (), new Cat()); List<Animal> vaccinatedAnimals = takeAnimals(animals); Если метод использует подстановочный знак как для параметра, так и для возвращаемого типа, нет никакой гарантии, что они будут одного типа. Фактически код, вызывающий такой метод, не имеет представления о том, что находится в коллекции, кроме того, что это «какое-то животное». public void go() { List<Dog> dogs = List, of (new Dog () , new DogO); List<? extends Animal> vaccinatedSomethings = takeAnimals(dogs); } public List<? extends Animal> takeAnimals(List<? extends Animal> animals) { } Использование подстановочного знака (? extends) подходит, когда вам не важен конкретный обобщенный тип, а нужно разрешить все подтипы некоторого типа. Использование параметра типа (Т) полезнее, когда вам важно работать именно с этим типом, например, Список, ttcmtjum 6с? бпошушпеч н?.Amman iakt t Скгиш (шцт тоге же типа, •пт и придавшей 6 него список.
Компилируйте (продвинутый уровень) Представьте себя компилятором и определите, какие из операторов ниже можно скомпилировать. Некоторые фрагменты кода не разбирались в этой главе, поэтому вам предстоит определить правильные ответы, опираясь на уже изученные принципы и применяя известные правила к новым ситуациям. Сигнатуры методов, использованных в задании, приведены в блоках. private void takeDogs (List<Dog> dogs)^! private void takeAnimals(List<Animal> animals) { ) J private void takeSomeAnimals(List<? extends Animal> animals) { ) Скомпил ируется? private void takeObjects(ArrayList<Object> objects) { ) □ takeAnimals(new ArrayList<Animal>()); □ takeDogs(new ArrayList<Animal>()); □ takeAnimals(new ArrayList<Dog>()); □ takeDogs (new ArrayListO () ) ; ^ List<Dog> dogs = new ArrayListO () ; takeDogs(dogs); □ takeSomeAnimals(new ArrayList<Dog>()); □ takeSomeAnimals(new ArrayListO()); □ takeSomeAnimals(new ArrayList<Animal>()); □ List<Animal> animals = new ArrayListO(); takeSomeAnimals(animals); □ List<Object> objects = new ArrayListO(); takeObjects(objects); □ takeObjects(new ArrayList<Dog>()); □ takeObjects(new ArrayList<Object>()); ► Ответы на стр. 403.
Ответы на упражнения Заполните пропуски (со стр. 370) Возможные ответы Comparator, Comparable, compareTo(), compare!), да нет Дан следующий компилируемый оператор: Collections.sort(myArrayList); 1. Какой интерфейс должен реализовывать класс объектов, хранящихся в myArrayList? 2. Какой метод должен реализовывать класс объектов, хранящихся в myArrayList? 3. Может ли класс объектов, хранящихся в myArrayList, реализовывать и Comparator, и Comparable? Даны следующие компилируемые операторы (они оба выполняют одно и то же действие): Collections.sort(myArrayList, myCompare); 4. Может ли класс объектов, хранящихся в myArrayList, реализовать Comparable? 5. Может ли класс объектов, хранящихся в myArrayList, реализовать Comparator? 6. Должен ли класс объектов, хранящихся в myArrayList, реализовать Comparable? 7. Должен ли класс объектов, хранящихся в myArrayList, реализовывать Comparator? 8. Что должен реализовать класс объекта myCompare? 9. Какой метод должен реализовать класс объекта myCompare? Comparable compareTof ) До До До_ Нет Нет Comparator comparef )
Решение #^Заточите карандаш * V Упражнение «Обратное проектирование» с лямбда-выражениями (со стр. 379) import java.util.*; public class SortMountains ( public static void main(String[] args) { new SortMountains().go 0; } public void got) ( List<Mountain» mountains = new ArrayListo () ; mountains.add(new Mountain("Longs", 14255)); mountains.add(new Mountain("Elbert", 14433)); mountains.add(new Mountain("Maroon", 14156)); mountains.add(new Mountain("Castle", 14265)); System.out.println("в порядке ввода:\n" + mountains); mountains.sort((mountl, mount2) -> mountl.name.compareTo(mount2.name)), System.out.println("по названию:\п" + mountains); mountains.sort((mountl, mount2) -> mount2.height - mountl .height); System.out .println ("по высоте:\г." + mountains); ) ) class Mountain ( String name,- int height; С(Штише ftuuwijm. nnc тигик cnu(jjynupt(?rta € nifWCjRt Mountain(String name, int height) ( :) this.name = name,- this.height = height, } public String toString( ) i return name + " " + height; } Результат: [ File Edit Window Help ThisOne'sForBob" %java SortMountains 1 в порядке ввода: [Longs 14255, Elbert 14433, Maroon 14156, Castle 14265] по названию: [Castle 14265, Elbert 14433, Longs 14255, Maroon 14156] по высоте: [Elbert 14433, Castle 14265, Longs 14255, Maroon 14156]
Решение аточите карандаш Сортировка с помощью лямбда-выражений (со стр. 378) m «уииташи, т.та songList.sort((one, two) -> one.getBpm() - two.getBpm()); songList.sort((one, two) -> two.getTitleO .compareTo(one.getTitle())) Результат: %java SharpenLambdas [50 ways, Havana, $10, somersault, cassidy, Cassidy] [somersault, havana, cassidy, Cassidy, 50 ways, $10] Ответ Заточите карандаш Коллекция TreeSet (со стр. 389) 1. Каков будет результат компиляции? Компиляция пройдет корректно 2. Что произойдет при запуске класса TestTree, если он успешно скомпилируется? Будет выброшено исключение: Exception in thread "main" java.lang.ClassCastException: class Book cannot be cast to class java.lang.Comparable at java.base/java.util.TreeMap.compare(TreeMap.java:1291) at j ava.base/j ava.uti1.TreeMap.put(TreeMap.j ava:53 6) at j ava.base/j ava.util.TreeSet.add(TreeSet.j ava:255) at TestTree.go(TestTree.java:16) at TestTree.main(TestTree.java:7) 3. Как исправить проблемы в коде (на этапе компиляции или выполнения), если они есть? Класс Book должен либо реализовывать Comparable, либо нужно передать TreeSet собственный компаратор.
Решение упражнения «Компилируйте» Со стр. 399 Скопилируется? takeAnimals(new ArrayList<Animal>()); takeDogs(new ArrayList<Animal>()); takeAnimals(new ArrayList<Dog>()); takeDogs (new ArrayListO ()) ; List<Dog> dogs = new ArrayListO(); takeDogs(dogs); takeSomeAnimals(new ArrayList<Dog>()) X)M mwn S®:. Эя#- За takeSomeAnimals (new ArrayListO ()) ; takeSomeAnimals (new ArrayList<Animal> () ) ; 'WatM l List<Animal> animals = new ArrayListO() ■ takeSomeAnimals(animals); □ List<0bject> objects = new ArrayListO() ; takeObjects(objects); takeObjects(new ArrayList<Dog>()) ; S' takeObjects(new ArrayList<Object>()); JJsi a Ht itst. ,.\rfcll|
12. Лямбда-выражения и потоки данных Потоки данных и лямбда-выражения без технических подробностей Знаешь, весь этот код можно вообще не писать API сама все сделает, стоит только попросить! А что, если... вам не нужно было бы объяснять компьютеру, КАК что-то Делать? Программирование часто сводится к тому, чтобы указать компьютеру, как выполнить задачу: пока это истинно — делай вот это; для всех этих элементов — если они выглядят так, то делай это; и т. д. Мы уже убедились, что не обязательно все делать вручную. В предыдущей главе шла речь о том, что в Java есть готовые библиотеки — например, Collections API, — и с их помощью можно обойтись без написания кода с нуля. Эти библиотеки предлагают не только структуры данных, но и методы, берущие на себя рутину. Достаточно просто сказать, что нужно получить, не вдаваясь в подробности как. В этой главе мы познакомимся с Streams API. Вы увидите, как лямбда-выражения облегчают работу с потоками данных, и научитесь использовать Streams API для обработки и преобразования данных в коллекциях.
Сообщите компьютеру, ЧТО вам нужно Представьте, что у вас есть список цветов, и вы хотите вывести их все. Для этого можно использовать цикл for. 'Цикл, рч Дт0 f af wtKttu List<String> allColors = List.of("Красный", "Оранжевый" for (String color : allColors) { "Желтый"); System.out.println(color); jf) m пищат адсш нпш d списке, ccr (journal (iplwniuM переменная cc&4. .. Но выполнять какое-то действие с каждым элементом списка — весьма вос¬ требованная задача. Поэтому, вместо того чтобы каждый раз писать for- цикл для обхода списка, можно просто вызвать метод forEach, унаследован¬ ный от интерфейса Iterable. А поскольку List реализует Iterable, он автоматически получает все его методы. List<String> allColors - List.of("Красный", "Оранжевый", allColors.forEach(color -> System.out.println(color)); jD.iAKilWf- н адешнтл fmim... qmnca (^\мшиш.я Сменная себя.. ...u иДт. "Желтый"); И File EtH Window Help SmqARainbow ~~ %java PrintColors Красный Оранжевый Желтый Метод forEach списка принима¬ ет лямбда-выражение, которое мы впервые увидели в предыду¬ щей главе. Это способ передать поведение («выполни такие-то инструкции») в метод вместо передачи объекта с данными («вот объект, используй его»).
Беседы у камина В сегодняшнем разговоре цикл for и метод forEach выясня¬ ют, кто из них лучше. метод forEach() цикл for Я — классика! Цикл for так важен, что его используют в огромном количестве языков программирования. Это одно из первых, чему учат программистов! Когда нужно выполнить действие определенное количество раз, про¬ граммисты тянутся к своему надежному циклу for. Конечно, мода меняется. Но иногда это всего лишь прихоть, ведь вещи тоже выходят из моды. Классика вроде меня будет всегда удоб¬ ной для чтения и написания, даже для тех, кто не знаком с Java. Слишком много работы?! Ха! Разве настоящий разработчик боится пары строк кода, которые четко указывают что делать и как? По край¬ ней мере, с for любой, кто читает код, сразу поймет, что происходит. А еще я быстрее. Это же очевидно! Вот увидишь, скоро ты просто исчезнешь. Пфф. Да ну тебя. Ты такой древний, вот поче¬ му тебя добавили во все языки программиро¬ вания. Но времена меняются, языки развива¬ ются. Есть способ лучше. Современнее. Это я. Ты только посмотри, сколько работы прихо¬ дится делать разработчикам, чтобы написать тебя! Им нужно самим следить за началом, шагом и концом цикла, плюс еще и писать сам код внутри. Тут столько всего может пойти не так! А со мной все проще. Достаточно про¬ сто указать, что делать с каждым элементом, а не заморачиваться, как их перебирать. Чувак, да им не нужно видеть, что там проис¬ ходит! Все написано прямо в названии моего метода — «к каждому» элементу я применяю нужную логику. Все. Готово. Ну и да, если уж совсем честно, за кадром я сам использую for. Но! Если появится что- то еще быстрее, я просто переключусь на это, и разработчикам не придется менять вообще ничего, чтобы их код стал молниеносным. Кстати, время вышло, так что...
Когда циклы for идут не по плану Использование forEach вместо обычного for сокращает объем кода, а еще приятно говорить компилятору, что делать, а не объяснять, как это делать. Но есть еще одно преимущество: когда библиотека берет на себя рутинные операции, это снижает вероятность случайных ошибок. Противоречивые результаты Ниже приведен короткий Java-код, но в нем отсутствует один фраг¬ мент. Предполагалось, что программа выведет 1 2 3 4 5, но на практике циклы for иногда содержат ошибки. Сопоставь предложенные блоки кода (слева) с выводом програм¬ мы (справа), если вставить их в недостающую часть. Не все строки вывода должны быть использованы, а некоторые могут повторяться. class MixForLoops { public static void main(String [] args) { List<Integer> nums = List.of (1, 2, 3, 4, 5); String output = £ema^Klf System.out.printIn (output); } Варианты кода: Возможный результат: for (int i - 1; i < nums.sizeO; i++) output +" nums.get(i) + " for (Integer num : nums) output += nums Ш '** >f for (int i = 0; i <= nums.length; i++) output += nums.get(i) + " for (int i - 0; i <= nums.sizeO; i++) output +« nums.get(i) fl * "; Ответы на стр. 453 1 2 3 4 5 Compiler error 2 3 4 5 Exception thrown [1, 2, 3, 4, 5] [1, 2, 3, 4, 5] [1, 2, 3, 4, 5] (1, 2, з, 4, 5] (1, 2, з, 4, 5] Ccirwmatftnu (?a|uuwmn Kjojae cc|mtu ю (?с?,тжи*и;
/Мелкие ошибки в коде — как иголки в стоге сена Циклы for из предыдущего упражнения выглядят очень похоже, и на первый взгляд кажется, что каждый из них должен корректно пройтись по списку и вывести все элементы по порядку. Ошибки компиляции заметить проще всего — компилятор или IDE сразу укажут на них. Ис¬ ключения (о которых мы поговорим в главе 13) тоже могут помочь отловить проблему. А вот самый коварный случай — когда код компилируется и запускается, но дает неправиль¬ ный результат. Метод forEach избавляет от всей рутины — повторяющегося шаблонного кода вроде for- циклов. Если мы используем f orEach и передаем только действие, которое нужно выполнить, это снижает шанс допустить случайную ошибку. Если API может взять на себя for- цикл для обхода всех элементов, то почему бы ему не справиться и с дру¬ гими типовыми задачами? Абсолютно! В Java 8 был представлен целый API именно для этого. В Java 8 добавлен Streams API — новый набор методов, который можно использовать во многих классах, включая Collections, о ко¬ торых мы говорили в предыдущей главе. Streams API — это не только набор полезных методов, но и немного другой подход к работе с данными. Он позволяет описать весь набор требований — своего рода рецепт того, что нужно извлечь из данных. Сможете придумать еще примеры типичных операций с коллекцией? Будем ли мы за¬ даваться похожими вопросами для разных типов коллекций? Какие виды информации вообще можно «вытянуть» из коллекции и вывести?
Основы часто используемых операций Способы, которыми мы ищем элементы в коллекциях, и типы извлекаемой информации часто схожи, даже если коллекции содержат разные типы объектов. Представьте весь спектр вариантов работы с коллекцией: «Покажи только те элементы, ко¬ торые соответствуют какому-то критерию», «Измени все элементы, применив к ним опреде¬ ленные действия», «Удали все дубликаты» и пример, разобранный в прошлой главе «Отсор¬ тируй элементы определенным образом». Каждая из таких операций может иметь название, которое сразу подсказывает, что именно будет происходить с коллекцией. 9 & КТО ВО 1ТО ГОРАЗД? Мы понимаем, что это новая информация, но попробуйте сопоставить название каждой операции с ее описанием. Старайтесь не подгляды¬ вать на следующую страницу, иначе весь смысл упражнения пропадет! ■filter Изменяет текущий элемент в потоке данных на другой skip Определяет максимальное количество элемен¬ тов, которое можно вывести из потока данных limit Пока заданное условие истинно, элементы не обрабатываются distinct Позволяет оставаться в потоке данных только тем элементам, которые соответствуют задан¬ ным критериям sorted Будет обрабатывать только те элементы, для ко¬ торых заданный критерий является истинным тир Определяет, что результат потока данных дол¬ жен быть упорядочен определенным образом dro pU/kile Определяет число элементов в начале потока данных, которые не будут обрабатываться iakeWMe Оставляет только уникальные элементы, удаляя дубликаты Ответы на стр. 453
Знакомство с Streams API т^киТоп^рацииГкоде0L°сГз^’ К°Т°РЫе М°ЖН° ВЫПОЛНЯТь с коллекцией. Читая успешно ^1^виЛ1^Дс'упра^нени Гм «Кто^'о^тоТор” зд^^ипр^117 ° ^аННЬШИ' ^сли вы веть. будут в конце главы), то наверняка заметили,^ назвашГоТ^" "РЗНИЦе (°Т' Ют их назначению, помогая легко разобраться в том, как java.util.stream .Stream содержащий ун».»ьи« — <* дубликатов) <;tream<T> filter(Predicate<? super Т> predicate) Возвращает поток данных с элементами, которые удовлетвор заданному условию (предикату). . не более Stream<T> iimit(long maxSize) Возвращает поток данных, ограниченный по длине maxSize элементов. <R> Stream<R> map(Function<? super Tr? extends R> mapper) Возвращает поток данных, в котором каждый элемент — результат применения заданной функции к элементам исходного потока данных. Stream<Т> skipOong п) Возвращает поток данных без первых п элементов исходного потока. Stream<T> sorted!) Возвращает поток данных, в котором элементы отсортированы по естественному порядку. II прочие методы (Этс всас.шаи нсоки«ко.литт<)с(? Stream-..- uxifeyf doAtwO Ути ссшцелнис типа njmctKij пикнет слсжжспи ю, нс нс $аднушпеа/ •/Торге Mti всспьиш- емся мстшалс maj\. и ftt uGucjuIm, mu da. нс так сложно, как кажется. Потоки данных и лямбда-выражение появились в языке я Java версии 8. Расслаб ься- Не стоит слишком переживать из-за параметров обобщенных типов в методах Stream — все будет работать так, как надо. Для ясности: <Т> °бычно означает тип объекта в потоке данных. <R> обычно означает тип результата метода.
Начало работы с потоками данных Прежде чем углубляться в детали о том, что такое Streams API, как оно устроено и как его использовать, мы рассмотрим несколько базовых инструментов для начала экспериментов. Чтобы использовать методы для работы с потоками данных, нам нужен объект типа Stream (что логично). Если у нас есть коллекция, например List, она сама по себе не реализует Stream. Однако интерфейс Collection содержит метод stream, который возвращает объект Stream для данной к List<String> strings = List.of("У", "вас", "в", "руках", "список", "Строк"); Stream<String> stream = strings.stream(); n . — ...дш мамам mavanu этот метод, *"= mum naoutumt поток этчхстйск Что произойдет, если мы попытаемся распечатать результат вызова функции limit()? System.out.println("ограничение = " + limit); линнси Htm nfmtm ivtj nu £ списка? Как и все в Java, переменные потока данных в примере — это объекты. Но по¬ ток данных не содержит элементы коллекции. Это скорее набор инструкций для выполнения операций с данными коллекции. Методы Stream, которые возвращают другой Stream, называются промежуточными операциями. Они задают инструкции для выполнения действий, но сами по себе операцию не выполняют.
Потоки данных — это как рецепты: ничего не произойдет, пока их не приготовить Рецепт в кулинарной книге только описывает, как при¬ готовить блюдо. Если вы просто откроете страницу, перед вами не появится свежеиспеченный шоколадный торт. Нужно собрать ингредиенты и следовать инструкции, что¬ бы получить желаемый результат. Конечно, коллекции — это не ингредиенты, а список, огра¬ ниченный четырьмя элементами, — увы, не шоколадный торт. Но к потокам данных применим тот же принцип: чтобы получить результат, нужно вызвать один из их «выполняющих» методов. Эти методы называются тер- реИзультаНтЫМИ °ПераЦШШИ> и именно они дают реальный boolean anyMatch(Predicate<? superТ> predicate) Возвращает true, если хотя бы один элемент соответствует заданно¬ му условию. long count() Возвращает количество элементов в потоке данных. <R,A> R collect(Collector<? super T,A,R> collector) Выполняет сведение элементов потока данных с помощью Collector. Optional<T> findFirstO Возвращает Optional, содержащий первый элемент этого потока данных, или пустой Optional, если поток данных не содержит элементов. // прочие методы Жска нс скажет «<?мполяяа» — Яре терт (?с<сш/ нс (’mcuiuutic. (Это .сшеи несколько т^иииисинмл стращш (’Stream.) JDaaaa, бмглялит tuy страшна, пеелшта тар! Не етстдйти пдники: осссиунимс тшш mpmeucuiuc KCMiuuMmcpu. Кеша tm елррле шт.ил бит этот.uunuj на 1\ракп\ша, нам нс утерпиммаишт шться ееЛтшхмнтх типах.
Извлечение результата из потока данных Итак, мы ввели в обращение множество новых терминов: потоки данных, промежу¬ точные операции, терминальные операции... И при этом мы до сих пор не рассказали, на что способны потоки данных! Чтобы вы начали понимать, как работать с потоками данных, мы покажем простой пример кода со Streams API. После этого мы разберем его более подробно. List<String> strings = List.of("У", "вас", "в", "руках", "список", "Строцчj Stream<String> stream = strings.stream(); Stream<String> limit = stream.limit(4); long result = limit.countO; —- BnmGam ик^шинащнпй System, out. println ("result = " + result); ci\ejvuncj\ count a CCXhOHMtM. ptjfumam C mptummk шай. I File Edit Window Help WaaDuti 1 %java LimitWithStream результат = 4 Этот способ работает, но пользы от него немного. На практике при работе с потоками данных результат чаще всего преобразуют в другую коллекцию. Описание соответ¬ ствующего метода в документации может показаться запутанным из-за обобщенных типов, но в базовом варианте все проще, чем кажется. и И НМЛ: СОМ f-mirCKit гюэтсжм eWm пет :и.; прэтолш'и ьежмтат. ^ншальная ЦДЯ (Win гг у1 _ List<String> result = limit.collect(Collectors.toList()); tdiist ™№т список iv аЫ пыч*™™ *-■- jnaimk. £w« кМм^Г- System.out.println("результат = " + result); \ File Edi^window 1 %java LimitWithStream результат = [У, вас, в, руках] Наконец-то мы получили ожидаемый резуль¬ тат: у нас был список строк, мы ограничили его первыми четырьмя элементами и собрали их в новый список. Рассла^ься- Позже мы подробнее разберем метод collect () и класс Collectors А пока метод collect(Collectors.toList()) можно считать волшебным закли¬ нанием, которое превращает конвейер потока данных в список.
Операции с потоками данных — основа конвейеров потоков Мы написали довольно много кода, просто чтобы вывести первые четыре эле¬ мента списка. Заодно ввели множество новых терминов: потоки данных, проме¬ жуточные и терминальные операции. Давайте теперь объединим все эти понятия и посмотрим, как строится конвейер потоков данных, состоящий из трех ключе¬ вых этапов: Создание потока данных на основе исходной коллекции Использование (при необходимости) промежуточных операций Завершение обработки с помощью терминальной операции Для работы с Streams API вам как минимум понадобятся первый и последний шаги. Однако нет необходимости на каждом этапе использовать отдельные пере¬ менные (как мы делали на предыдущей странице). Операции спроектированы так, чтобы их можно было вызывать цепочкой, переходя от одного этапа к другому без создания дополнительных переменных. На предыдущей странице мы выделяли ключевые элементы потока данных (stream, limit, count, collect). Теперь, используя эти компоненты, мы можем переписать операцию ограничения и сбора данных в более компактной форме. List<String> strings = List.of("У", "вас", "в", "руках", "список", "Строк");
Операции можно соединять и комбинировать Каждая промежуточная операция обрабатывает и возвращает объект Stream. Это означает, что вы можете объединять необходимое количество операций, пре¬ жде чем вызвать терминальную операцию для вывода результата. Источник, промежуточная операция(-и) и терминальная операция вместе образуют конвейер потока данных. Он представляет собой запрос к исходной коллекции. Именно здесь проявляется польза Streams API. В предыдущем примере нам понадобилось три операции (stream, limit, collect), чтобы создать уко¬ роченную версию исходного списка, что довольно сложно для такой простой задачи. Но если нужно выполнить что-то более сложное, мы можем объединить не¬ сколько операций в одном конвейере потока данных. Например, можно отсортировать элементы в потоке данных перед тем, как при¬ менить ограничение: List<String> strings = List.of("У", "вас List<String> result = strings.stream() .sorted() It tl-ll Ctf\aH м m (ium шток 'ST —^ nr-r .collect(Collectors.toList()); "руках", "список", "Строк"); Сф1штшм aic. ueH.mtv (? поиске JmmV'HC нс «шлтшмкы j(Ku,u«) (?стсст&атш n/fytt ruiHif сг.цгтмтснис.и nciii vtmcovu (?. System.out.println("результат = " + result); pil^Edi^Wirdw^Hel^InChatfvs I %java ChainedStream иямяп&ншй «./mum, ci. - строк tfimmmt Hiptcj пп[и -,нплщ “ ТЯ1 result = [Строк, У, в, вас]
Настройка операций Мы можем комбинировать операции, создавая более сложные запросы к коллекции, а также настраивать их. Например, в методе limit мы определили максимальное количество возвращаемых элементов (четыре). Если естественная сортировка строк не подходит, можно задать собственные кри¬ терии. Метод sorted позволяет определить порядок сортировки (как мы делали в предыдущей главе со списком песен Лу). List<String> result = strings.stream() .sorted((si, .limit(4) rtmfdLU' unfa r OKI qm h, ока . , KomchCM qqiu/u i iafk. Жорже <?этой главе ^^(фаженим. s2) -> si.compareToIgnoreCase(s2)) .collect(Collectors.toList()) rTiie**EdiT^indow*^eTriqnofeCap3— ijava ChainedStream ,.4bm<u uj KUfCCr Kemfi МемШтп onneU, м^пиппвал регион^1 оук(>- result = [в, вас, руках, список] Выстраивайте из операций сложные конвейеры Каждая последующая операция в конвейере влияет на его результат и сообщает Streams API, какое действие необходимо выполнить. List<String> result strings.stream() .sorted((si, s2) -> si.compareToIgnoreCase(s2)) .skip(2) .limit(4) .collect(Collectors.toList()); t File Ed<t Window Hutu BmufiDolt ~ %java ChainedStream result = [руках, список, Строк, У] fij\uuun\amt пропищим й"а э.ле.ш нта. 1 1
С такими длинными потока¬ ми данных вам вряд ли захочется, чтобы компьютер выполнял каждую операцию по одной, сразу. Верно? Вот почему нужна терминальная операция — она запускает выпол¬ нение ТОЛЬКО тогда, когда поток данных полно¬ стью описан и библиотека знает, что нужно сделать от начала до конца. Да, потому что потоки данных ленивые ш Это не значит, что они медленные или бесполезные! Каждая промежуточ¬ ная операция — это всего лишь указание, что нужно сделать, но не выпол¬ нение действия. Такие операции оцениваются лениво. Терминальная операция просматривает весь список инструкций — все промежуточные операции в потоке данных — и только после этого за¬ пускает выполнение. Терминальные операции активны: они выполняются сразу после вызова. Благодаря этому появляется возможность выполнить все действия макси¬ мально эффективно. Вместо того чтобы проходить по данным заново для каждой операции, поток данных обрабатывает все инструкции за один проход по коллекции.
Терминальные операции берут все на себя Так как промежуточные операции ленивые, все выполнение ложится на терми¬ нальную операцию: О Выполняет все промежуточные операции как можно эффективнее, по возможности однократно пробегая по исходным данным. ©Определяет результат операции его задает сама терминальная операция. Это мо¬ жет быть список значений, одно значение или логический результат (true/false). Возвращает результат. Формирование списка Разобравшись в работе терминальных операций, давайте подробнее рассмо¬ трим «волшебную формулу», которая позволяет получить список результатов. List<String> result — strings.stream() .sorted() .skip(2) .limit(4) h»* &Шт- ,m<™ " V JtfUUiшишхы снумцдч f putMuniimn амаги, nfujalmn и " пслутнщ. .collect(Collectors. toListQ) ; , cc u- . ccCCtcl ►фшкшшчгт CciVtcu t - ctioue №ja fttu^nv a\lv£nu.4H 1щиц, кая rcrWm, htM-unuiuim. В ааннслк слцш ur нммдrucrfn. Komoiieui «едии(дип ■ цтдтм dсписок. Позже в этой главе ллы разберем другие методы из Collectors и тер¬ минальные операции. А на данный момент у вас достаточно знаний, чтобы начать работать со Streams.
Рекомендации по работе с потоками данных Как и в любой головоломке или игре, есть правила, позволяющие правильно ис¬ пользовать компоненты потока данных. ф Любой конвейер потока данных начинается с источника и заканчивается терминальной операцией. Без вызова stream () вы вообще не получите поток данных. Без терминальной операции вы не получите результат. ф Потоки данных нельзя использовать повторно. ^ Может показаться удобным сохранить поток данных, представляющий запрос, и использо¬ вать его в разных местах — либо потому, что сам запрос полезен, либо чтобы изменить его, добавляя новые операции. Однако после вызова терминальной операции повторное исполь¬ зование потока данных невозможно — нужно создавать новый. После выполнения конвейера поток данных закрывается и не может быть использован по¬ вторно, даже если его часть сохранена в переменной. Любая попытка повторного использова¬ ния потока данных приведет к выбросу исключения. StreanKString> limit = strings.stream() .limit(4); List<String> result = limit.collect(Collectors.toList()); List<String> result2 = limit.collect(Collectors.toList()); ^Flf^Edf^Wjndow HeJp ciosingTHne I %java LimitWithStream Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed at java.base/java.util.stream.AbstractPipeline. evaluate(AbstractPipeline.java:229) Во время работы потока данных нельзя исходную коллекцию. Если изменить коллекцию, пока поток данных выпол¬ няется, это приведет к неожиданным результатам или исключениям. Представьте, что вас спрашивают, что в списке покупок, а кто-то в это время его правит. Не¬ удивительно, если вы начнете путаться в ответах. Я запутался! Я хочу прочитать этот чертов список, а он постоянно меня ется! изменять
о J О, если нельзя по- N менять исходную кол¬ лекцию во время запроса, то и операции потока данных ее не изменяют, верно? Верно! Операции потока данных не изменяют исходную коллекцию Streams API позволяет выполнять запросы к коллекции, но не вносит в нее измене¬ ния. Вы можете просматривать данные и получать результаты на основе ее содер¬ жимого, но сама коллекция останется неизменной. Это очень полезно, поскольку можно безопасно выполнять запросы к коллекциям в любом месте программы, не беспокоясь о том, что исходные данные изменятся. Вы можете убедиться в этом, просто выведя содержимое исходной коллекции после выполнения запроса в Streams API. List<String> result = List.of("У", "вас", "в", "руках", "список", "Строк") Stream<String> limit = strings.stream() .limit(4) .collect(Collectors.toList()); System.out.println("строки = " + strings); System.out.println("результат = " + limit); пик/i (jciHHtAX. пика q
4^ ^Упражн \ражнение Магнитики с кодом Ответы на стр. 4S4
Без ЛИШНИХ (ЛОВ (Не) #ураи,кие вопросы ЗомрОС: Есть ли ограничение на количество промежуточных операций в конвейере потока данных? Ответ: Нет, ограничений нет: вы можете добавить сколько угодно промежуточных операций. Но не забывайте: ваш код должны понимать не только компьютеры, но и люди. Если конвейер становится слишком длинным и нечитаемым, лучше раз¬ бить его на части и сохранить в переменных с осмысленными именами. ^ОПрОС: Есть ли смысл в конвейере потока данных без про¬ межуточных операций? Ответ Да, бывает и так. Иногда терминальная операция может сразу дать нужный результат, без всяких промежуточных шагов. Но учтите: некоторые терминальные методы дублируют уже имеющиеся в коллекциях. Например: stream().count() — это то же самое, что list.sizef), streamf). forEach(...) — можно заменить на list.forEach(...), если у вас List. Так что, если можно обойтись без потока данных — смело обходитесь. &ООТрОС: Вы сказали, что нельзя изменять исходную коллекцию во время выполнения потока данных. Но как мой код может это сделать, если сам выполняет операции? 0\МВет Отличный вопрос! Это может произойти, если поток дан¬ ных обрабатывается параллельно. В одной части программы запускает¬ ся конвейер, а в другой кто-то вносит изменения в ту же коллекцию. Это касается не только Streams, но и многопоточности вообще. Как вари¬ ант — сделайте коллекцию неизменяемой, если она не должна меняться. (Больше об этом будет в главах 17 и 18, посвященных конкурентности.) ЗоирОС: Как получить неизменяемый список из терминальной операции collect? Ответ: Если вы используете Java 10 или новее, просто исполь¬ зуйте Collectors.toUnmodifiableList() вместо toList() — и получите готовый, неизменяемый список. ВОИ рОС: Можно ли собрать результаты потока данных не в List, а, в другой коллекции? Ответ: Да, конечно! В предыдущей главе мы изучили различные типы коллекций. Класс Collectors предоставляет удобные методы для сбора данных в List (toList), Set (toSet) и Map (toMap). Начиная с вер¬ сии Java 10, доступны также toUnmodifiableList(), toUnmodifiableSet() и toUnmodifiableMap(). Так что вы сами выбираете, во что собрать результат. ■ Вам не нужно писать подробный код и вручную указывать JVM, как имен¬ но обрабатывать коллекции. Вместо этого можно использовать методы библиотек — включая Streams API, — чтобы формулировать запро¬ сы и получать результат. ■ Вместо обычного for-цикла исполь¬ зуйте forEach на коллекции. Просто передайте в него лямбда-выраже¬ ние, описывающее действие для каждого элемента. ■ Поток данных создается из коллек¬ ции с помощью метода stream(). ■ Затем вы настраиваете запрос, добавляя одну или несколько про¬ межуточных операций (например, filterQ, map() и т. д.). ■ Результат появится только после вызова терминальной операции. Какую именно выбрать — зависит от того, что вам нужно: список, количе¬ ство, первое совпадение и т. п. ■ Чтобы собрать результат в новый List, используйте терминальную операцию collect(Collectors.toList()). ■ Вся связка — источник, промежу¬ точные операции и терминальная — называется конвейером потока данных. ■ Операции Streams не изменяют исходную коллекцию. Вместо этого они позволяют описывать запросы и получать новый результат, не за¬ трагивая исходные данные.
Привет, лямбд а, мой (не такой уж) старый друг Лямбда-выражения уже встречались в примерах с потоками данных, и можете не сомневаться: до конца этой главы их будет еще больше. Ваша работа с Streams API существенно упростится, если вы разберетесь с тем, что такое лямбда-выражения. Давайте рассмотрим их подробнее. Передача логики Если бы вы писали f orEach сами, он выглядел бы так: void forEach( ????? ) { for (Element element : list) { - ^ ) } Что бы вы подставили вместо ? ? ? ?? Там должен оказаться такой блок кода, который впишется на это место — и позволит вызвать метод вот так: at я кЛ " ИШ4Нл^т Sti ш лттпн nfvxmo нами f orEach (do th ;sjSystenuout^println((tewk ) ; 3mom сиск kctin аолши как-тс ruuutumt. элетсшт сн ftmuimmci c'tvmf. Нам tnuxfn его otguiwmnmA. jo нити стшГmhuiatm этот. № tK этс C$U „emcaa imoc'fi он 6wta шился телеке иишп. тогда, когда это понадобятся. сопи» этот код пряма j>qea tv, а Теперь нам нужно заменить слова До t/iis какими-то символами, чтобы по¬ казать, что код должен не выполняться сразу, а быть передан в метод, как инструкция на будущее. А что, если взять символы ->? Они отлично по¬ казывают направление: вот значение -> вот что с ним делать. Дальше нам нужно передать это значение — например, элемент списка. Для этого пишем имя переменной слева от ->, а сам блок действия — справа: forEach( item -> System.out.println(item) It
Хорошо, теперь понятно: лямбда-выражение, переданное в качестве аргумента метода, где-то используется внутри этого метода. Но... Что это вообще такое — лямбда- выражение? И как метод может «использовать» кусочек кода, который мы только что ему передали? /1ямбда-выражения — это объекты, и они выполняются при вызове их интерфейса с одним абстрактным методом (SAM) Помните, что в Java всё является объектами (за исключением примитивных типов), и лямбда-выражения — не исключение. Лямбда-выражение реализует функциональный интерфейс. Это значит, что ссылка на лямбда-выражение ведет на функциональный интер¬ фейс. Поэтому, если ваш метод должен принимать лямбда-выражение, его параметр должен быть функциональным интерфейсом. Этот интерфейс должен соответ¬ ствовать «форме» вашего лямбда-выражения. Вернемся к нашему воображаемому примеру с f orEach. Параметр метода должен реализовывать функциональный интерфейс. А еще нам нужно как-то вызвать переданное лямбда-выражение, передав ему элемент списка. Функциональные интерфейсы содержат единственный абстрактный метод (Single Abstract Method, SAM). Именно этот метод, как бы он ни назывался, вы¬ зывается, когда мы хотим выполнить код лямбда-выражения. jnctionallnterface lambda ) { void forEach( SomeFunctionallnterface lambda for (Element element : list) { lambda.singleAbstractMethodName(element) ) } •hftct Ч1К- tj&muvt - этс nufuueunfe ./(.яuctja-Cnfuimnm, mi ,m crew, fine u (ftm. (?, з A \КС(Ш- (infuimnu u H« nfmltufifu^u empaимм^. Лямбда-выражения — это не магия; это такие же классы, как и все остальное в Java.
Виды лямбда-выражений Мы уже видели два лямбда-выражения, реализующих интерфейс Comparator: одно в примере сортировки песен Лу в предыдущей главе, а другое — в пере¬ данном в операцию sorted () потока данных. Давайте сравним последний пример с функциональным интерфейсом Comparator. Возможно, вы задаетесь вопросом, почему в лямбда-выражении нет ключевого слова return. Короткий ответ: оно не нужно. Поясним почему: если лямбда- выражение состоит из одной строки и метод функционального интерфейса требует возвращаемого значения, компилятор предполагает, что эта единствен¬ ная строка кода и будет возвращаемым значением. При желании можно записать это лямбда-выражение в более развернутом виде, добавив все его возможные элементы: (String si, String s2) -> { return si.compareToIgnoreCase(s2); }
Структура лямбда-выражения /-ч апл vnTODoe может быть Основную часть лямбда выражения составляет его тело, к ско6ок представлено одной или несколькими строками внутри фигур еслИ Именно здесь реализуется основная логика. Этот код был бы те бы мы реализовывали функциональный интерфейс в полноценн 7кпн он jutuu|ijtm. cmfym носка J,!}>n,C!4'(CAU сУЩе , / 'ШемсТ1п<уттц (String si, String s3)- пахшиm см йшнуш <ыгфн^скос^к. тс Ск нщ Kcmjm строка нш!хш1ьис пигку с лгпятой. пи» it Д dTL „ лшпсдерт. гоошнам return sl.compareTo!gnoreCase(s2) SfeS RtK ckwk». йл/iажонш шлмлесда т рсспределмет лит у, кстсрпй СсгСраицит гните ш . нлтлс Сиражшим уикнсгенс dcfmijfmtxt скидки, нее слипе ис мСтикагать return ь конце тема/'если же йершенш состоит иг одной строки, ке.ипи tMnufvamu - матшески onpcgcium, кока гнагтие слиррт (лрнцпи. Форма лямбда ведение) 0п Сражения (его параметры, возвращаемый тип и Делается функциональным интерфейсом, который Тыс ллиСда^ражопия, которое .может. Осипе npegcnudxaw ости Htu носка f кии и стрекани mejmm фищттх скооок. содержит основную лспекес Этот кед жег са стгти тыки штт, если (fix среркционатши ин- терсрейс ели реи пел &ш (? тинсменнаи KJdla. /J данном слитое тыс . тядиР ааСпра.тнил npukinudmm сото логики метода сотракО инпирахйса Comparator. ожидаемое по- °но реализует.
Разнообразие делает жизнь интереснее Лямбда-выражения могут иметь разную форму и разме¬ ры, но при этом всегда следуют одним и тем же основ¬ ным правилам. Лямбда-выражение может содержать несколько строк По сути, лямбда-выражение — это метод, поэтому оно может включать столько строк, сколько необходимо. Многостроч¬ ные лямбда-выражения должны быть заключены в фигурные скобки. Как и в любом методе, каждая строка должна за¬ канчиваться точкой с запятой, а если метод должен возвра¬ щать значение, в теле лямбда-выражения обязательно нужно указать оператор return. Г■ ge****f „н л** ,> (strl, str2) -> { int 11 = strl.length()7 int 12 = str2.length(); return 12 - 11; С\Ф С ^^нассдянюг- a(j|t ’л и НИМ, t/'unjfmtu сш1ка <r yJmt f/нц (им • Ы/иу«(>нра*пнии. Однострочные лямбда-выражения проще Если лямбда-выражение состоит из одной строки, компилятору проШе понять ег логику, поэтому можно опустить «излишек» синтаксиса. Если сократить прсдЫДУ щее выражение в одну строку, оно примет такой вид: t't'i Ц Н М£ СИ СОК U НС н ужпы (strl, str2) ->^4tr2.length() - strl. length О 4 C ntf\tumf \ tdнгн нс нити JotM it нс нif-шш Это все тот же функциональный интерфейс Comparator и та же операция сорти¬ ровки. Выбор между многострочными и однострочными лямбда-выражениями остается за вами. Все зависит от сложности логики выражения и удобочитаемости кода; ино¬ гда более длинный код бывает понятнее. Позже мы рассмотрим другой способ работы с длинными лямбда-выражениями
Многострочное лямбда-выражение без возвращаемого значения Метод функционального интерфейса может быть объявлен с void, то есть не возвращать ни¬ какого значения. В таких случаях код внутри лямбда-выражения просто выполняется, и ука¬ зывать оператор return не требуется. Так происходит, например, при использовании лямбда-выражений в методе f orEach. цс* [ЖСИ ,1 Xjuitmt сксски ЛЛн л* Ри~ str -> { String output = "str = " + str; System.out.printin(output) ; So @FunctionalInterface public interface Consumer<T> { void accept(T t); ’ f HUM -Лямбда-выражение может не иметь параметров, принимать один или несколько параметров Количество параметров в лямбда-выражении зависит от количества параметров метода функ¬ ционального интерфейса. Указывать типы параметров (например, String) обычно не обяза¬ тельно, но можно сделать это для повышения читаемости кода. В некоторых случаях указывать типы необходимо, если компилятор не может автоматически определить, какой функциональ¬ ный интерфейс реализует лямбда-выражение. НС nftur , HRtuwm JfuLUtmfw. HHfxtf О -> System.out.printin("К R _ _ ~~ , fUmitj <£j tuipautnxjxa Cifun mfuiuanfx: tcfxuum скоски ,шис enuammu, tc cm man in ijKcyau fmioui ijniyn(!awmcJ nc жъ шнше). str -> System.out.printin(str) @FunctionalInterface public interface Runnable { void run(); @FunctionalInterface public interface Consumer<T> ( void accept(T t); (strl, str2) -> strl.compareToIgnoreCase(str2) @FunctionalInterface public interface Comparator<T> { int compare(T ol, T o2); \4lenmjcnajicumn^cnw >
Как определить, принимает ли метод лямбда-выражение? Лямбда-выражения являются реализацией функционального интерфейса — ин¬ терфейса с единственным абстрактным методом (SAM — Single Abstract Method). Это означает, что тип лямбда-выражения — это соответствующий функциональ¬ ный интерфейс. Попробуйте создать лямбда-выражение и присвоить его переменной вместо пере¬ дачи в метод. Вы увидите, что оно ведет себя как обычный объект в Java, посколь¬ ку в Java все представлено объектами. Тип переменной в этом случае будет функ¬ циональным интерфейсом. Comparator<String> comparator = (si, s2) -> sl.compareTolgnoreCase(s2); Runnable runnable = () -> System.out.println("Привет!"); Consumer<String> consumer = str -> System.out.println(str); Как это помогает понять, принимает ли метод лямбда-выражение? Дело в том, что тип его параметра будет функциональным интерфейсом. Рассмотрим несколько примеров из Streams API. Stream<T> filter(Predicate<? super T> predicate) boolean allMatch(Predicate<? super T> predicate) <R> Stream<R> map(Func.ion<? super T|? extends R> void forEach(Consumer<? super T> action)
ражнение Компилируйте (продвинутый уровень) public interface Runnable { void run(); Представьте, что Вы - компиля¬ тор. и определите, какие из при¬ веденных ниже Выражений скомпи- лируются. Однако часть этого кода не рассматривалась В глаВе. поэто¬ му нужно найти отВеты. опираясь на изученные пра- Вила и применяя их к ноВым ситуациям. Т>ля ygobcmBa спраВа приВедены сигнатуры функциональных интерфейсов. ) public interface Consumer<T> { void accept(T t); ) public interface Supplier<T> { T get(); > public interface Function<T, R> { R apply(T t); } Отметьте выражения, которые скомпилируются. Runnable г = () -> System.out.println("Привет!"); □ Consumer<String> с = s -> System.out.println(s); □ Supplier<String> s = () -> System.out.println("Какая-то строка"); Consumer<String> c = (si, s2) -> System.out.println(si + s2) ; I—J Runnable r = (String str) -> System.out.println(str); Q Function<String, Integer> f = s -> s.length(); Supplier<String> s = () -> "Какая-то строка"; Consumer<String> c = s -> "Строка" + s; Function<String, Integer> f = (int i) -> "i = " + i; Supplier<String> s = s -> "Какая-то строка:" + s; Function<String, Integer> f = () -> System.out.println("Именно такая строка"); Ответы на стр. 454,
Как распознать функциональные интерфейсы До сих пор встречались функциональные интерфейсы, помеченные аннотацией @ Functionallnterface (аннотации рассматриваются в Приложении Б). Она указывает, что у интерфейса один абстрактный метод, и его можно реализовать с помощью лямбда-выражения. Но не все интерфейсы имеют такую отметку, особенно в старом коде, поэтому важно уметь определять их самостоятельно. Раньше интерфейсы содержали только абстрактные методы, которые нужно переопре¬ делять в классах, реализующих интерфейс. В Java 8 появились default- и static- методы. Статические методы не привязаны к экземпляру и часто используются как вспомога¬ тельные. Они уже встречались в главе 10 и появятся дальше в этой главе. Методы по умолчанию работают иначе. Вспомните абстрактные классы из главы 8. У них были абстрактные методы, требующие переопределения, и обычные методы с реализацией. В интерфейсе default-метод похож на обычный метод в абстрактном классе: он содержит реализацию и наследуется подклассами. Любой метод в интерфейсе, который не помечен как default или static, считается абстрактным и подлежит переопределению.
Функциональные интерфейсы в реальном коде Теперь, когда понятно, что интерфейсы могут содержать не только абстрактные методы, опреде¬ лять интерфейсы с единственным абстрактным методом становится сложнее. Возьмем, к приме¬ ру, Comparator. В нем много методов, но он все равно относится к типу SAM (Single Abstract Method) — содержат только один абстрактный метод. Это функциональный интерфейс, кото¬ рый можно реализовать с помощью лямбда-выражения. eaiuu’ttt&HHttu. С” fprtiT ai. т Лсщш Щ' „ ,fc mprtKtiuitiw СНОСI J(t стоит nutruutu ас сани как static ими. dda.uCt нс и«сспухактп1и1 т.явШт_ СМ СИ fUH.iUji ЙУН (т Оеш >Ум,]шт,ы «Ы1сс2(Щ^ — Ц^цЗаточите карандаш static «Т.и extends Coaparat>le<? SuperU» Соераrator<T> static «T.lh Coeparatof • 1 • statu <Т» Cif irat»r<T> Statu <T> Ceaparator<T> statu «Т> Саараrotor*?» fep*rln}(fiarti»<T staff T„f extends О keyferf ractor) ce^aring<faactie*<T super T,T extends J> koytxtractor, Ca super U > fcayCfisparater) c«*poringpwiaia<ro»oefcUFu»ctioec? • <jp«f Т» heyCxtroctor) etaparinpiati Tolntfeaction*? super r* My€«tractor) caeporingiaafCloLoafFeectioe*? super Т» xeyt*tractor) ogooltiotjoct sta^c «Т extends Ceeparatl*<T super т» t^ratercb static <Ts Coaparet»r<T> Static «TV Ca^ratar<T> default ca^araror<T> aetaralthdartl •atlsFirst!Caepa rater*’ super T> rGenerator) iwUslast! Comparator*;/ super IV «operator) reversed () Какой из этих интерфейсов имеет единственный абстрактный метод и поэтому может быть реализован в виде лямбда-выражения? BiPredicate Modifier and Type Method default BiPradicate*T,u> and(BiPradicat»<7 super T.7 super U> other) default ftlPradicatecT.U» negated default BiPredicate<T<U> or!BiPredicate*? super T,7 super U> other) boolean ActionListener ModHter and type Method void actionParfor»ed(ActionEvant e) Function Modifier and Type default <V> Function<T,V> test(T t, U u) Iterator Modifier end Type Method default void forEachRaaaining(Consular*? super E> action) has*axt() n«xt() boolean E default void default <V> Functions,R> static <T> Function<T,T> andThen(Function<? super R, 7 extends V> after) •Ppl»<T t) coaposalFunction*7 super V,7 extends T> before) identity!) •eoveO SocketOption Modifier and Type Method String naee() Cla*a<T> typa() Ответы на стр. 4SS.
Лу вернулся! Лу уже некоторое время использует программное обеспечение для управления своим музыкальным автоматом — тем самым, которое мы писали в предыдущем разделе. Теперь ему стало интересно узнать больше о песнях, которые звучат в его закусочной. Раз уж данные у него есть, почему бы не заняться анализом? Лу уверен: из этих списков можно вытащить кучу всего интересно¬ го — точно так же, как он экспериментирует с ингредиентами своего фирменного супер-омлета! Он надеется получить ответы на вопросы вроде: • Какие пять песен звучат чаще всего? • Какие жанры вообще крутятся в его закусочной? • Есть ли песни с одинаковыми названиями, но от раз¬ ных исполнителей? Конечно, мы могли бы написать for-цикл, который перетрясет все песни, проверит нужные условия с помощью i f, разложит названия или исполнителей по разным коллекциям, — и на основе этого найти ответы. Теперь, когда у меня есть данные о песнях, которые играли в моем музыкальном автомате, я хочу покопаться глубже. Узнать, что там инте¬ ресного! Но теперь, когда мы знаем о Streams API, мы найдем более простой способ.... Код на следующей странице — заглушка. Вызов метода Songs.getSongsQ вернет список объектов Song, который можно считать аналогом реальных данных из автомата Лу. ^Упражнение Введите готовый код со следующей страницы, дополните класс Song недостающими частями, а затем создайте метод main, который выведет на экран все песни. Как вы думаете, каким будет результат?
Ш["ОТОВЬ 1Й КОД Ниже представлен обновленный метод-заглушка. Он будет возвращать некоторые тестовые данные, которые мы сможем использовать, чтобы опробовать некоторые отчеты, которые Лу хочет создать для системы музыкальных автоматов. Также обновлен класс Song. class Songs { public List<Song> getSongs() { return List.of( new Song("$10", "Hitchhiker", "Electronic", 2016, 183), new Song("Havana", "Camila Cabello", "R&B", 2017, 324), new Song("Cassidy", "Grateful Dead”, "Rock", 1972, 123), new Song("50 ways", "Paul Simon", "Soft Rock", 1975, 199), new SongC'Hurt", "Nine Inch Nails", "Industrial Rock", 1995, 257), new Song("Silence", "Delerium", "Electronic", 1999, 134), new SongC'Hurt", "Johnny Cash", "Soft Rock", 2002, 392), new Song("Watercolour", "Pendulum", "Electronic", 2010, 155), new Song("The Outsider", "A Perfect Circle", "Alternative Rock", 2004, 312), new SongC'With a Little Help from My Friends", "The Beatles", "Rock", 1967, 168), new SongC'Come Together", "The Beatles", "Blues rock", 1968, 173), new SongC'Come Together", "Ike & Tina Turner", "Rock", 1970, 165), new SongC'With a Little Help from My Friends", "Joe Cocker", "Rock", 1968, 46), new Song("Immigrant Song", "Karen O", "Industrial Rock", 2011, 12), new Song("Breathe", "The Prodigy", "Electronic", 1996, 337), new SongC'What's Going On", "Gaye", "R&B", 1971, 420), new Song("Hallucinate", "Dua Lipa", "Pop", 2020, 75), new Song("Walk Me Home", "P'nk", "Pop", 2019, 459), new Song("I am not a woman, I'm a god", "Halsey", "Alternative Rock", 2021, 384), new SongC'Pasos de cero", "Pablo Alboran", "Latin", 2014, 117), new Song("Smooth", "Santana", "Latin", 1999, 244), new Song("Immigrant song", "Led Zeppelin", "Rock", 1970, 484)); ) ) public class Song { private final String title; private final String artist; private final String genre; private final int year; private final int timesPlayed; // Попрактикуйтесь: создайте конструктор, все геттеры и метод toStringO )
Первая задана У1у: найти все рок-композиции В обновленном списке песен теперь указаны их жанры. Лу заметил, что посе¬ тители закусочной предпочитают различные направления рок-музыки, и хочет увидеть список всех песен, которые относятся к жанру «рок». Поскольку это раздел о Stream API, будем решать задачу с помощью потоков данных. Напомню, что для составления решения можно использовать три типа элементов. Ка4~ики,ия 9mcnv rti/нкт ,с^‘ - (г^тчт,гя к Stream •Mirrn "анти лшпт, пе?(?с- " «пи,(fa. lm'/4 l'mfu£am, пшак ш,п* танкен 1ужнжр ши щшт. СПМсюк ш К счастью, требования Лу дают нам подсказки о том, как построить вызов Streams но отфильтровать песни по определенному жанру и собрать их в новый список.
Оставляем в потоке данных только нужные элементы Давайте посмотрим, как операция фильтрации применяется к списку песен. к гп m а Ражш оттенки J nvC-At^t ^ *mWmwnv J. Koiul«'1 bss-rj^ VjC.'c6v^w- .stream() 0 a @ .filter (Щ ) • collect(toList) ^4 KC4, цК1' yamtu. Jtiw11*1 CCJTa ЦЦИ11 qaHKtu <:{ui -*r* £иХь э>Псгс CS?5r ^£Г‘“ >‘Чи,( 4M<h"
Погнали! Добавление операции фильтрации исключает элементы, которые нам не нужны, и по¬ ток данных продолжается только с теми, которые соответствуют нашим критериям. Неудивительно, что для указания нужных элементов можно использовать лямбда-вы¬ ражение. Метод filter принимает Predicate. Исходя из того, что мы знаем о форме лямбда-выражений, мы можем опреде¬ лить, как написать лямбда-выражение, реализующее Predicate. /(.«ftm of1* tvafwtutnf / Predicate predicate = | -> @Functionalinterface public interface Predicated1? boolean test(T t); Щшни. 4- ] 1Шт vyuH параметр. Жг,<уесж*JHUYLHUt Тип единственного параметра мы узнаем, когда подключим лямбда-выражение к опера¬ ции Stream — входной тип определяется на основе типов элементов в потоке данных. public class JukeboxStreams { public static void main (String[] args) { List<Song> songs = new Songs().getSongs(); ■ ' ■&? (Шеек песен V List<Song> rockSongs = songs.stream ()^_ . filter (song -> song. getGenre () .collect(Collectors.toList()); Ccxpaewt-M System.out.println(rockSongs); .шпктм denurnк. Iv 'r ""'СИЛЫ и , .equals("Rock")) /какое .ымЛм йлклжтко №■ Am PteAicaU. Ia- tu class Songs { // готовый код } class Song { // готовый код i %java JukeboxStreams [Cassidy, Grateful Dead, Rock With a Little Help from My Friends, The Beatles, Rock, Crane Together, Ike £ Tina Turner, Rock, With a Little Help from My Friends, Joe Cocker, Rock, Immigrant song. Led Zeppelin, Rock]
Усложняем фильтрацию Метод filter, возвращающий простое значение true или false, может содер¬ жать сложную логику для отбора или исключения элементов из потока данных. Давайте скорректируем наш фильтр и выполним запрос Лу: Он хочет попучить список всех песен, относящихся к любому жанру рока. Ему нужны не только песни с жанром "Rock", но и любые другие, в названии жанра которых встречается слово «Rock». В классе String есть метод, который поможет нам в этом, — contains. рсгЩха . “Rileк ft mIvimmcM tf.iwcau .шстс описки жонПд. V" .filter(song -> song.getGenre().contains("Rock")) .collect(Collectors.toList()); List<Song> rockSongs = songs.stream() Бцфши/ът hut. если roc&s mRdttun тиски я I Flip Frlit Window Help YouRock %java JukeboxStreams [Cassidy, Grateful Dead, Rock 50 ways, Paul Simon, Soft Rock Hurt, Nine Inch Nails, Industrial Rock Hurt, Johnny Cash, Soft Rock 7u«nt rumiK a цу Ш Г cmMx&fraw*™ CKOUttH. 7 сокращен am • ним Аиста & книге - сумагц! Жене («режем. ~ Напишите операцию фильтрации, которая вы¬ берет песни: • Группы The Beatles • Названия которых начинаются с буквы «Н» • Записанные после 1995 года
Второе задание У1у: Список песен всех жанров Лу начинает понимать, что музыкальные вкусы его посетителей куда разнообраз¬ нее, чем он думал. Теперь он хочет получить список всех жанров песен, которые проигрывались в закусочной. До этого все наши потоки данных возвращали тот же тип, с которого начинались. Например, у нас были потоки строк, и мы получали в результате списки строк. В предыдущем задании Лу работал со списком песен и тоже получил список пе¬ сен — просто немного покороче. А теперь ему нужен список жанров. То есть мы должны как-то преобразовать каж¬ дую песню в ее жанр. Вот тут и приходит на помощь операция тар. Метод тар () позволяет преобразовывать один тип данных в другой. В нашем случае — превра¬ щать поток песен в поток жанров. Ш (Ш (S 0 [F] stream О .stream() ntpujiumcM. iAM^a-^aham- как ntpujiumcM. ым&а-вмхаш - ЖЩ ксш/wt как umanu Отол JHU70 KCH&iu|UI juiiummcs* mucon хсЛрсв nerm. Щ. ири1ПГ.Я ou хсЛрсв пеан (?a uv.
Преобразование одного типа в другой Метод шар () принимает Function. Его обобщенные параметры заданы так, что могут быть разными, из-за чего понимание может быть сложнее. Однако у Function есть одна четкая задача: он принимает значение одного типа и воз¬ вращает значение другого типа. Именно это и требуется для выполнения опера¬ ции отображения (mapping). Способы преобразования зависят от конкретных типов данных. p^CtionalInt:er^e 1С ^П^ег^асе Functions, R> { aPPly(T t) ; 1 1 (TH ^tfuiuanp Давайте посмотрим, как это выглядит, когда мы используем шар в потоковом конвейере. СУ ***? Это гТГ(«'П- 4 */ Э,г\с cm|utw •V List<String> genres = songs.stream SovJ- •>/ss 1*да.да«к K“K n«'«1 «шлет к .map(song -> song.getGenre()) .collect(toList()); ^7 I.UmUttUl (?CHMCCK. Чмжоиуш Лямбда-выражение в map работает схожим с фильтрацией образом. Оно при¬ нимает песню и преобразует ее во что-то другое. Однако вместо возвращения логического значения оно возвращает другой объект — в данном случае строку с жанром песни. ллнша (?Щишт цч ■uttMttm (кМиик/ши сп Шп- itocczo типа. ууЩопи на тош.лштли- ratAi пспхск спфок. ссафжа- МЩ» ' <' ' l_Fii« £dii Window мор RoadToNowftye ‘ I %java JukeboxStreams [Electronic, R&B, Rock, Soft Rock, Industrial Rock, Electronic, Soft Rock, Electronic, Alternative Rock, Rock, Blues rock, Rock, Rock, Industrial Rock, Electronic, R&B, Pop, Pop, Alternative Rock, Latin, Latin, Rock]
Удаление дубликатов У нас уже есть список всех жанров из тестовых данных, но Лу вряд ли захочет просматривать повторы одного и того же жанра снова и снова. Ему нужен уникальный список. Мы знаем, что операция шар превращает поток данных одного типа в поток данных другого, но при этом размер потока данных остается таким же, как и был. То есть, если жанр «Rock» встречается десять раз — он появится десять раз и в новом потоке данных. Но ведь мы работаем с цепочками операций! Значит, возможно, существует еще одна опера¬ ция, которую можно добавить к шар, чтобы оставить по одному экземпляру каждого жанра? шит _г .stream() т 0, 1 ©' \ О г Эпш ^шмеитютая шма "M*Wo . distinct () %иЛ^нУПГ'' 1 г © jDv каща KCH&utha <© )\щчтт\(,« &тс.1нишА (jutjvmeM тальке суш эк- жшхяяЦ каждого ешкта. <9 эЖскоявшетоууоп нс 6т список. ссстжашми \ I .collect(toList) U «I. ©@ ®
Достаточно добавить операцию distinct в конвейер потока данных, чтобы в итоге получить список жанров без повторений. List<String> genres = songs.stream() Конвейер потока данных может содержать любое количество промежуточных операций. Мощь Streams API заключается в том, что можно составлять слож¬ ные запросы из понятных строительных блоков. Библиотека позаботится о том, чтобы выполнить их максимально эффективно. Например, можно создать за¬ прос, который вернет список всех исполнителей, записавших кавер на опре¬ деленную песню, кроме оригинальных исполнителей. Для этого потребуется операция шар и несколько фильтров. String songTitle = "With a Little Help from My Friends"; List<String> result = al1Songs.stream() ,map(song -> song.getGenre()) jDcda&iiHWL этой оги^туди в исток цанмах >а\ыннид .distinct() .collect(Collectors.toList()); Продолжаем компоновку! .filter(song -> song.getTitie().equals(songTitle)) .map(song -> song.getArtist()) .filter(artist -> (artist.equals("The Beatles")) .collect(Collectors.toList()); Оточите карандаш Попробуйте самостоятельно добавить выноски к этому коду. Какие элементы отбирает каждый из фильтров? Какую трансформацию выполняет операция тар? ► Решать-вам!
Иногда и лямбда-выражения не потребуются Некоторые лямбда-выражения выполняют простые и предска¬ зуемые действия, основываясь на типе параметра или структуре функционального интерфейса. Рассмотрим еще раз лямбда-выра¬ жение для операции тар: Function<Song, String> getGenre = song -> song.getGenre(); Ссылки на методы — это необязательная Вместо того чтобы явно прописывать всю операцию, можно просто указать компилятору метод, который выполняет нуж¬ ное действие, используя ссылку на метод. Митщ adfamtO йу mrf. /HuHKW im гнуилч/ — ^ т/й*- / | J / \а- ЭПtfc Й*И1- Ьни.яи. альтернатива лямбда-выраже¬ ниям. Иногда они делают код короче Function<Song, String> getGenre = Song::getGenre; i и понятнее. Энн; тот самт.мсмт етт'л^гЦыжт1Ы <*«"£3£г- Ссылки на методы позволяют заменить лямбда-выражения в разных случаях. Обычно — чтобы улучшить читабельность кода. Возьмем наш старый знакомый компаратор в качестве примера. В интерфейсе Comparator есть множество вспомогательных методов, которые в сочетании с ссылкой на метод позволяют увидеть, какое значение используется для сортировки и в каком порядке. Вместо того чтобы писать показанный ниже код для упорядочивания песен от самой старой к самой новой: List<Song> result = allSongs.stream() .sorted((ol, o2) -> ol.getYear() - o2.getYear()) .collect(toList()) ; Используйте ссылку на метод в сочетании со статическим вспомогательным методом из Comparator, чтобы указать, как должно выполняться сравнение: List<Song> result = allSongs.stream() .sorted(Comparator.comparinglnt(Song::getYear)) .collect(toList()); Расслабься Вы вовсе не обязаны использовать ссылки на методы, если вам с ними не¬ комфортно работать. Главное — уметь распознавать синтаксис : :, особенно в конвейере потоков данных.
Разные способы сбора результатов Чаще всего используется сборщик Collectors . toList, а также другие полезные коллекторы. Например, вместо метода distinct для решения последней задачи мы могли бы собрать ре¬ зультаты во множество, не допускающее дубликаты. Преимущество такого подхода в том, что любой код, использующий эти результаты, будет знать, что, поскольку это объект Set, дубликаты отсутствуют по определению. Set<String> genres = songs.stream() ч .map(song -> song.getGenre()) (сл-шнми*. [ижипимш b ш, « a нс GJLht )й иг ojgtm со tJtipM CLHit Hu 6. CuxiuiiWLW u.mt\onm - нищ tmnctuomu- шш. «шлются только ijHUKau.HMt j'HcrfOHU.a. Collectors.toList и Collectors.toUnmodifiableList Вы уже знакомы c toList. В качестве альтернативы, начиная с версии Java 10, можно использовать Collectors . toUnmodifiableList, чтобы получить неизменяемый список, в котором нельзя добавлять, за¬ менять или удалять элементы. Collectors.toSet и Collectors.toUnmodifiableSet Чтобы сохранить результаты в Set, а не в List, используйте со¬ ответствующий коллектор. Помните, что множество не содержит дубликатов и, как правило, не имеет определенного порядка. Если вы используете Java 10 или выше, можно применять Collectors. toUnmodifiableSet, чтобы гарантировать неизменность результатов. Collectors.toMap и Collectors.toUnmodifiableMap Также можно собрать поток данных в Мар с парами «ключ-значение». Для этого необходимо указать функции, определяющие, что будет ключом, а что — значением. С Java 10 можно использовать метод Collectors . toUnmodifiableMap, создающий неизменяемый сло¬ варь. Collectors.joining Еще один вариант — создать String из элементов потока данных. Это позволит объединить все элементы в одну строку. Можно также задать разделитель, который будет использоваться между элементами. Такой подход полезен, если нужно преобразовать поток данных в строку фор¬ мата CSV (Comma Separated Values). /)цсшк«т((? КСI
Но это еще не все! Сбор результатов — не единственный возможный вариант, метод collect — лишь одна из множества терминальных операций. Проверка наличия элементов Можно использовать терминальные операции, возвращающие логическое значение, чтобы про¬ верить, содержатся ли в потоке данных определенные элементы. Например, убедиться, звучали ли в закусочной композиции в жанре R&B. boolean result = songs.stream () .anyMatch(s -> s.getGenre().equals("R&B")); boolean anyMatch(Predicate p); boolean allMatch(Predicate p); boolean noneMatch(Predicate p); Поиск конкретного элемента Терминальные операции, возвращающие тип Optional, используются для поиска определенных элементов в потоке данных. Например, можно найти, какая из выпущенных в 1995 году песен вос¬ производилась первой. Optional<Song> result = songs.stream () .filter(s -> s.getYear() = 1995) .findFirst(); Подсчет элементов В Streams API есть операция count, подсчитывающая количество элементов в потоке данных. На¬ пример, можно определить число уникальных исполнителей. Есть и другие терминальные операции, часть из них зависит от типа потока данных, Не забывайте про документа¬ цию API — она поможет опреде¬ лить, есть ли готовая операция, выполняющая нужное действие. long count() long result = songs.stream() .map(Song::getArtist) .distinct() .count(); Optional<T> findAnyO ; Optional<T> findFirst () ; Optional<T> max(Comparator c); Optional<T> min(Comparator c); Optional<T> reduce(BinaryOperator a);
V Минуточку. Что такое Optional? Как результат может быть «опциональным»? Как это понимать? Некоторые операции что-то возвращают... а некоторые — нет Может показаться странным, что метод иногда возвращает результат, а иногда — нет, но в реальности такое происходит по¬ стоянно. Представьте, что вы подошли к витрине с мороженым и попро¬ сили клубничный щербет. IceCream iceCream = getlceCream("Клубничное") Элементарно, согласны? А если его нет в наличии? Тогда продавец скажет вам: «У нас нет такого мороженого». Теперь выбор за вами — взять шоколадное эскимо вместо клубнично¬ го щербета, купить щербет в другом месте или просто уйти расстро¬ енным без мороженого. А теперь представьте, как это выглядело бы в Java. В первом случае вы получаете объект мороженого. А во втором? Строку с сообщением? Но строка не подходит для переменной, в которой ожидается моро¬ женое. Null. И что на самом деле означает null?
Optional — это обертка Начиная с восьмой версии Java появился простой способ указать, что метод может не вернуть результат, — объект Optional. Он оборачивает возможное значение, позволяя спросить: «Есть ли у меня результат? Или он пуст?» После этого можно решить, что делать дальше. if (optional.isPresent()) { IceCream ice = optional.get() >
о £> Теперь, чтобы получить мороженое, мне нужно сде¬ лать два дополнитель¬ ных шага?! Да, зато теперь у нас есть способ узнать, есть ли вообще результат Optional предоставляет способ узнать о ситуациях, когда нуж¬ ного мороженого нет, и правильно решить, что делать дальше. V if (optional.isPresent()) { } else { System.out. println("А мороженого-то и нет!") > Раньше методы в таких случаях могли выбрасывать исключения, возвращать null или использовать специальный объект-заглуш¬ ку, обозначающий «мороженое не найдено». Возвращаемый тип Optional ясно дает понять: вызывающий код должен сначала проверить, есть ли результат, а уже потом решить, что делать, если его нет.
Не забывайте взаимодействовать с Optional Важно помнить, что объект Optional может быть пустым. Если сначала не проверить, есть ли в нем значение, и попытаться получить результат из пустого Optional, это приведет к исключению. Hein % java OptionalExamples Exception in „ . Mi” ,Ч^^Г!^ГЙГ' Вбить в голову Розы красны, фиалки нежны, Не вызовешь isPresentO — жди беды! Это будет настоящий БА-БАХ! Перед тем как развернуть объект Optional, нужно проверить, содержит ли он значение, иначе при от¬ сутствии результата будет выброшено исключение.
Внезапный кофе Александра запрограммировала свою мега-ультра-умную кофемашину (конечно же, на Java!) так, чтобы та предлагала разный кофе в зависимости от времени дня. После обеда Александра хотела получать самый слабый кофе из доступных: у нее и без того было достаточно причин не спать ночами. Лишний кофеин? Нет уж, спасибо. Как опытный разработчик она знала: Streams API отлично справится с этой задачей — и по¬ может получить идеальный поток... кофе. Кофе автоматически сортировался от самого слабого к самому крепкому (в соот¬ ветствии с естественным порядком), так что Александра просто задала машине вот такую инструкцию: Optional<String> afternoonCoffee = coffees.stream() .map(Coffee::gecName) ,sorted() .findFirst () ; На следующий день, когда Алекс снова попросила свой послеобеденный кофе, ее ждал сюрприз. Вместо привычного капучино без кофеина в чашке оказался крепкий американо! — Я не могу пить такой крепкий кофе! — воскликнула она. — Я же потом всю ночь буду переживать из-за моего последнего проекта! Что пошло не так? Почему умная кофемашина выбрала американо, хотя Алекс явно рассчитывала на что-то помягче? *■ Ответы на стр. 455
бассейн фрагментов В бассейне находятся фрагменты кода. Ваша задача — поместить их в пустые строки кода. Некоторые фраг¬ менты не понадобятся, а осталь¬ ные можно использовать только один раз. Цель упражне¬ ния — создать класс, который скомпилируется и запустится, а результат вывода будет совпадать с тем, что приведен ниже. Output File Edit Window Help Diveln %java StreamPuzzle [Immigrant Song, With a Little Help from My Friends, Hallucinate, Pasos de cero, Cassidy] With a Little Help from My Friends No songs found by: The Beach Boys Примечание: Фрагменты кода из бассейна можно использовать только один раз! public class Strearr.Puzzle { public static void main(String| SongSearch songSearch = songSearch. ; .search("The Beatles"); args) { class private final List<Song> songs = new JukeboxData. Songs () .getSongsO; void printTopFiveSongs() ( List<String> topFive = songs.stream() . collect (_ System.out.println(topFive); } void search(String artist) ( = songs.stream() if ( ) { System.. out. println ( ) else ( System.out.println( } .) ; ) ; *- Ответ на стр. 456
Противоречивые результаты (со стр. 408) Варианты кода: Возможный результат: for (int i ” 1; i < nums.size(); i++) output += nums.get(i) + " for (Integer num : nums) output += nums + " for (int i = 0; i <= nums.length; i++) output += nums.get (i) + " for (int i = 0; i <= nums.size () i++) output += nums.get(i) + " 9 [1, 2, 3, 4, 5] [1, 2, 3, 4, 5] [1, 2, 3, 4, 5] [1, 2, 3, 4, 5] [1, 2, 3, 4, 5] & КТО Ю ЧТО ГОРАЗД? (со стр. 410) Изменяет текущий элемент в потоке данных на другой Определяет максимальное количество элемен¬ тов, которое можно вывести из потока данных Пока заданное условие истинно, элементы не обрабатываются Позволяет оставаться в потоке данных только тем элементам, которые соответствуют задан¬ ным критериям Будет обрабатывать только те элементы, для которых заданный критерий является истин¬ ным Определяет, что результат потока данных дол¬ жен быть упорядочен определенным образом Определяет число элементов в начале потока данных, которые не будут обрабатываться Оставляет только уникальные элементы, уда¬ ляя дубликаты
Магнитики с кодом (СО стр. 422) import java.util.*; import java.util.stream.*; Что произойдет, если операции в потоке данных будут выполне¬ ны в другом порядке? Имеет ли это значение? Обратите внимание на небольшой подвох: в строке .filter (s -> s.endsWith("о")) нужно указать русскую (кирилличе¬ скую) букву «о», а не латинскую. Иначе фильтр просто не сработа¬ ет — несмотря на то, что буквы выглядят одинаково. public class CoffeeOrder { public static void main(String[ ] args) { List<String> coffees = List.of("Капучино", "Американо", "Эспрессо", "Кортадо", "Мокка", "Капучино", "Флэт уайт", "Латте"); List<String> coffeesEndinglnO = coffees.stream() .filter (s -> s . endsWith ("o") ) .sorted() .distinct () .collect(Collectors.toList()) System.out.println(coffeesEndinglnO); } } Компилируйте (со стр. 431) J Runnable r = () -> System.out.println("Привет!"); Consumer<String> c = s -> System.out.println(s); I File Edit Window Help CafeMo" %java CoffeeOrder [Американо, Капучино, Кортадо, Эспрессо] - ’ (YV Elicit y)G ш?.н rt - □ Supplier<String> s = () -> System.out.printIn("Какая-то строка"); m^uKclmin naixa.mmh. г Consumer<String> c = (si, s2) -> System, out .println (si + s2) ; u.ucem □ , ^(J Runnable r = (String str) -> System, out. println (str) ; ^ na^umenn^cS r-V Этл MHcmpoTHtit, i :tion<String, Integer> f = s -> s. length (); nut фйктиткх'КИ. (scAjyiw/Utn -Иуи' ч* ,WA ли maaunMnu г нс S„ppli,r<Stri„<,> .-()-> строк,".- □ Co„su„er<Stri„g> о . , "Стро„" ♦ bjMmffluwom* I—| ' a , т,И 'У1 мнItMtmu nufui шmil LiJ Function<String, lnteger> f = (int i) -> "i = " + i; aftinij иffc/dhauA/imt int, □ 'v шщЧфи нтшип int .. t( (>< Aiuiium ж □ Supplier<String> s = s -> "Какая-то строка:" + s; untf. (- ’ ши iumm nnprtuttiyu tf Function<String, Integer> f = () -> System.out.println("Какая-то строка") jDdMktn int, нс на еашикцеж не i (шш/ит акте.
BiPredicate ^Зэточите карандаш .«ну,. м«ы* (состр. 433) default 8iPredicete<T,U> »nd(8iPredicnte<? supej/т,? super U> other) default 8iPr*dicate<T,U> negate() default 8iPredicate<T,U> or(BiPredicate<? super Tf? super U> other) boolean test(T t, U u) Mumn (uiwHcnv6ta- w <u£m|viKm- hhvw ныа .лидам tt.st(). Ctmiumtu — .utnu ActionListener Modifier and Type void Наш m «шшт&ннш* ^rf.»dUctl^t (,^ПГ^ПШШЛШПиТ Iterator /'f.MCtm nCn r Meddler and Type Method AiUm.ltJL , 'ПЧЫ1<тНП t default void forfacMtMMlninjICo. Л super t> action) T‘ Mutt) If ntrtO. ^ boolean ^ E n«sxt{) default void raaove() ijtv ivj ij.uc. л<анин.. Function Modular and Type Method default <V> Function<T,V> andThen{Function<? super B<7 extends V> after) R apply(T t) default <V> Function<V,R> coapoi«{Function<? super V,? extends T> before) static *T> Functional,T> idantityO Нишn ij&l а^туигктимл1 Mumn сушшп&мннй (Vc спхракпииш мата мщa.n 0mWm.-JJW шним> H cmamimcKkc utm( SocketOption ModMar and Matbod String y\. nmU Class<T> / %a»() Дело пяти минут (состр. 451) Алекс не обратила внимания на порядок операций в потоке данных. Сначала она преобразовала объекты кофе в поток строк, а потом отсортировала их. А строки, как известно, по умолчанию сортируются в алфавитном порядке. Поэтому, когда кофемашина выбрала «первый» результат, она выдала свежий американо. Но если Алекс хотела, чтобы кофе сортировался по крепости — от самого слабого (1 из 5), — ей нужно было сначала отсортировать поток объектов кофе, а затем преобразо¬ вать их в строки с помощью шар. Вот правильный порядок: afternoonCoffee = coffees.stream() .sorted() .map(Coffee::getName) .findFirst () ; Теперь все правильно: кофемашина приготовит напиток без кофеина, а не очередной аме рикано с ударной дозой бодрости.
бассейн фрагментов (со стр. 456) public class StreamPuzzle { public static void main(String[] args) ( SongSearch songSearch = new SongSearch)); songSearch. printTopFiveSongsO ; songSearch . search ("The Beatles") ; songSearch.search("The Beach Boys"); } } class SongSearch { private final List<Song> songs = new JukeboxData.Songs() .getSongs (); void printTopFiveSongsO { List<String> topFive = songs.stream() .sorted(Comparator.comparinglnt(Song::getTimesPlayed)) . map(song -> song.getTitleO) . limit(S) .collect (Collectors.toListO) ; System.out.println(topFive) ; void search(String artist) { Optional<Song> result = songs . stream () ,filter(song -> song.getArtist().equals(artist)) . findFirstO; if (result.isPresentO) { System, out .println (result.get().getTitle()) ; ) else { System, out .println ("He найдено песен:" + artist) ; ) )
13. Обработка исключений Рискованное поведение Всякое случается. Файл может исчезнуть. Сервер — не откликнуться. Каким бы надежным ни был ваш код, контролировать абсолютно все невозможно. Иногда что-то идет не так. А иногда — совсем не так. Когда вы пишете метод, связанный с риском, вам нужен способ обработать возможные проблемы. Но как понять, что код вообще потенциально опасен? И где, черт побери, разместить обработку этих самых исключений? До этого момента в книге мы не сталкивались с по-настоящему опасными ситуациями. Да, время от времени возникали ошиб¬ ки, — но почти всегда это были наши косяки, баги, которые стоило поймать еще на этапе раз¬ работки. Сейчас другое дело. Мы подходим к такому коду, который невозможно гарантировать. Код, который надеется, что нужный файл лежит в нужной папке. Что сервер уже запущен. Что поток выполнения не проснулся. И вот с этим всем нам и предстоит разобраться — прямо сейчас. Потому что в этой главе мы собираемся создать нечто... Нечто, что работает с рискованным API JavaSound. Мы будем писать MIDI-плеер.
Давайте создадим музыкальную машину! В следующих трех главах мы разберем несколько аудио¬ приложений, включая драм-машину «Битбокс». Более того, к концу книги у нас будет многопользовательская версия, где можно делиться своими ритмами с другими пользователями по аналогии с социальными сетями. Весь код вам предстоит написать самостоятельно, хотя для интерфейса можно воспользоваться готовыми шаблонами. Конечно, не каждая IT-компания ищет сервер под бит- бокс, но наша цель — научиться работать с Java, а создание драм-машины сделает этот процесс более увлекательным. Виртуальный бит боне танцевальный бит Вайб: ритм 2 Бруклин: ритм 2, переработанный ТунцТунц: танцевальный бит Bass Drum О Ш 1 о о о р; ,п. о п . О' О Closed Hi-Hat p о G о □ о О р р о О о Open Hi-Hat r~у .p г. р п / ! п □ р г iP Р ; р. ■tz р Acoustic Snare r-s с f—1 г—S р, Р G р r^i • - I.J - *pi G : Р Г"* о Crash Cymbal p P Г" Г" п Lp Р р р р: G Р . Р : р Hand Clap □ п р, р Р р р ?р. ■ '> Р .Pi О’ High Tom 1 V о G н fv Р" 'pi □ р р: р G’ н Р р р Hi Bongo П п р п О ,р О р О О о р Maracas О n о р о О □ о р. О О о р Whistle □ 'П а п (■р п н G р Q Low Conga G p; о О п О о Р • о О О Cowbell D ■ P[ -О р: р, ,'Р. О ГГ. ■ ; ГА р £j р р Vibraslap П' p 'О ,р р р ‘ -CL Р Р р р/ р р р Low-mid Tom Q G о Р ■ "D: О р в О. р о р* pi р р High Agogo П p- О Рч н □ р ;Pf: П р G, —‘ п —■ р fG 1 Open Hi Conga 1J1 ) 'p р а о о р ' ■Q: р р О Р р Q О <9 Start f Stop j Tempo Up Tempo Down sendlt Kmja ftn нажимае¬ те кнопки «C'trwpa (hunt» шш cocc'wy н нс onuyuttf ыопся ашги.м.'пал^ж&гтс- лям (Lmcmerc теку- ШД и ПрМЛО • • • Обратите внимание на галочки в ячейках: каждая из них отвечает за один из 16 шагов в такте. Например, на первом шаге играют бас-барабан и маракасы, на втором — ничего, на третьем — маракасы и закрытый хэт и так далее. Когда вы нажимаете кнопку «Старт», программа начи¬ нает проигрывать ваш луп, повторяя его до тех пор, пока вы не нажмете кнопку «Стоп». В любой момент можно сохранить свой ритмический рисунок, отправив его на специальный сервер, чтобы другие игроки могли его услышать. Также можно загружать лупы других пользователей, щелкая мышью по соответствующему сообщению. Щф вхшящих (Шсш/нчи cln мщгих нс ил (<ш1ь.н и /uutlumu супе(у сообщений., тит ?шМ/ипи сшп ^<тпп кщЖ лип, и нажмите кнопку «Старт». тиш (/< (Асп/ииЛ'итн.
Начнем с азов Прежде чем программа будет полностью готова, вам предстоит изучить несколько важных моментов. Вы поймете, как создавать графический интерфейс, как подключаться к другому компью¬ теру через сеть и как передавать данные с помощью операций ввода-вывода. Но сначала нужно освоить работу со звуком в Java — JavaSound API. Именно с этого мы и начнем. Пока не будем думать о графическом интерфейсе, сети и передаче данных, а сосредоточимся на том, как заставить компьютер воспро¬ изводить звуки, сгенерированные с помощью специального формата музыкальных данных MIDI. Не волнуйтесь, если у вас нет опыта работы с этим форматом или вы никогда не создавали музыку. Все, что нужно для решения задачи, будет рассмотрено в этой книге. Кажется, мы уже на пути к успеху! TVie JavaSownd API JavaSound — это набор классов и интерфейсов, добавленный в Java еще в версии 1.3. Они входят в стандартную библио¬ теку Java SE. JavaSound делится на две части: MIDI и Sampled. В этой книге рассматриваться будет только работа с MIDI. MIDI (Musical Instrument Digital Interface) — это стандартный протокол, который позволяет электронным музыкальным устройствам обмениваться данными. В контексте на¬ шего битбокса представьте MIDI как нотную запись, которую «считывает» устройство, похожее на высоко¬ технологичное «автоматизированное пианино». Иными словами, MIDI сам по себе не содержит записи звуков, а только инструкции, которые MIDI-устройство может воспроизвести. Если проводить аналогию, то MIDI-файл — это как HTML- документ, а устройство, которое его интерпретирует (проигрыва¬ ет), — как браузер. MIDI-данные указывают, что делать (например, сыграть ноту «до», как громко и как долго), но они не содержат записей звуков реальных инструментов. MIDI не знает, как имитировать слыши¬ мый нашими ушами звук флейты, пианино или гитарного соло в стиле Джимми Хендрикса. Чтобы услышать звук, нам нужно MIDI-устройство, которое сможет его воспроизвести. Но чаще всего это устройство — не просто один инструмент, а целая группа, как оркестр или бэнд. Таким устройством может быть как физический синтезатор или клавишный модуль, так и программ¬ ный инструмент, работающий на компьютере. В нашем битбоксе мы применим встроенный в Java, исключи¬ тельно программный инструмент. Его называют синтезатором, потому что он синтезирует звук, который мы слышим. flay Ьф» С, hit it and hold it ■for 2- beat* j N/ 4t Кажлтш (Цинк к «ос» и uCaumi — иг. иининн аба А'цка тага)! ..411/ ) 3 файл есаф.жит uiumjujK иДи, как осилена исполняться .иеасфсч, но не соаеКжит. самих гбикеб. Это КОК ноты it 1.4 .лиланитесксм: пи¬ анино. б ни х и кагане, ■(те и кеша иг|\лпи. не не как vcucuho это идет гбитать. (все мбисит от пин;. ' какой инст[зф дннтеДает бглеДанД оч их исполнение. . f w/uurmft, иш . Фаи. » м (вспртЖстт, №■ 'аки.и ycrnplu- спШммсжА шт, к:ла- «ншн.нй синтелтир или уруни лиипкаизти ин Как правит, JlJy)hu петри иент способен всспрсиувщчть .«нстстбе fxaMnxMiKi в (ipcpmmuaHcApjap^ ные. скрипку и арцни) щневт.ишш. .тэте, ну ./пЗ/)1сра\ХА тхснс не на нотную лтип уля одного исполнителя, а на партитуру асн бсао ансашСш, вкоторой, хранятся партии всех (шепуш ш нтеб cpiM кон¬ кретней Kduncj>uu,m.
Сначала нам нужен секвенсор Чтобы воспроизвести звук, нужно создать объект Sequencer. Он отвечает за обработку всех MIDI-инструкций и передачу их нужным инструментам. Именно секвенсор воспроизводит музыку. У него много возможностей, но в этой книге мы будем использовать его только для воспроизведения звуков. Можно пред¬ ставить его как устройство для потокового воспроизведения музыки, но с допол¬ нительными функциями. Класс Sequencer находится в пакете j avax . sound. midi. Поэтому начнем с создания (получения) объекта Sequencer. impo rt j avax.sound.midi.*; { public class MusicTestl public void play() { try { Sequencer sequencer = MidiSystem.getSequencer() System.out.println("Секвенсор успешно установлен' } }(au Htpcm «жт jcguenm. Это секс (taw шсшж)£)3- urnUKitrm&i юлю юнотрроон true, которой дом iukuuWu. /.(.ионко он., скажгм так1, roMftwuviom васЖЮЗ- тсМхтшт <? «птио». (.(ныко.ип ко ссташ ом са- „иос-тсяпилшо кож корте ' рпрскто ого } public static void main(String[] args) { MusicTestl mt = new MusicTestl(); mt.play(); } Что-тонетак! > Этот ксо rmo I Pile Fdi! Wnwuw Help SayWhal7 javac MusicTestl.java e f HtЛ«.«гииятО|п,ашн,п с Ьмшкк несфдсопигкное исклюгснис (шщтЫ ixctjxUm), копир* Htw.vcm.Mio днеГпаых&тить <(tnufl\Q, лиг* удлСтш. (dtcfamD. 1 MusicTestl.java:13: unreported exception javax.sound.midi. MidiUnavailableException; must be caught or declared to be thrown Sequencer sequencer = MidiSystem.getSequencer(); 1 errors
Как работать с потенциально опасными ("сторонними) методами? (Т) Допустим, вы хотите вызвать метод в классе стороннего програм¬ миста. Вы другого програм¬ миста. (5) Этот метод выполняет что-то потенциально опасное, то, что может не сработать во время выполнения. ® Вы должны понимать, что вызываемый метод может привести к ошиб¬ ке во время выполне¬ ния. ® Напишите код для обработки возможного сбоя. Нужно быть готовым к такому развитию событий. класс друго¬ го програм¬ миста void moo() { if (serverDown) { explode(); } }
Допустим, нам нужно запускать Т0ГД* К»*«/дос,^Г,1рГ»™Г“ ОДНОГО ПOIO,<‘, За раз А ™ ‘ ’ пользуют п»л пот*Се РеаЛИЗаЦИИ 8 ТОЙ ИЛИ иной форме не- У у потоков — совокупность экземпляров Thread zi::нТяХгользовать {в гом —-•->' П°ток°в в пУле* а также поведение при избыт- 0 сравнению с числом доступных потоков зави¬ сит от конкретной реализации ExecutorService.
Компилятору нужно знать, что ВЫ осознаете риск вызова метода Если обернуть опасный код в конструкцию try/ :atch, он успокоится. Блок try/catch дает понять компилятору, что в вызываемом методе может произойти нечто исключительное и что вы готовы это обработать. Компилятору все равно, как именно вы с этим :правитесь — важно лишь, что вы дали понять: <Я об этом позабочусь». import javax.sound.midi.*; public class MusicTestl { Ъорогаи ксмпилягпср, Я рнаю, vno иду на риск, над асами, она тага стоит? Что лис делать? С уважением, Ънс Ъорогаи Ъис, ?Лирнь каретка (особен¬ на Я куче). Рискни. Попро¬ буй. Но на ёоисни случай бидь югпоё рашититьел от лгабяк нсс шданноапси, пока ёсс рарёалилась. public void play() { trY < Sequencer sequencer = MidiSystem.getSequencer System.out.println("Секвенсор успешно ус } catch(MidiUnavailableException e) System.out.println("Облом”); } public static void main(String[] args) MusicTestl mt = new MusicTestl(); mt.play(); } .*г1ги№Л.шЯЛ1НС тановлен") ? 1 9t,uMtmy Go, u <~u.kcaUh **,r ctu*emt, телепат, J fpcmuxatO ^ ПЦце }
Не повторяй это дома. И это очень хорошо, ведь если бы исключения были объектами типа «брокколи», запомнить их было бы гораздо сложнее. Из глав о полиморфизме (7 и 8) мы знаем, что объект типа Exception может быть экземпляром любого подкласса Exception. Поскольку Exception — это объект, то и в блоке catch мы перехватываем объект. В приведенном ниже коде аргумент catch объявлен как переменная типа Exception с именем ех. Исключение — это объект... типа Exception Этс кит utjvip.vuu к, \аеса fxcijxUm /let эти muiui Throwable getMessage() printStackTracef) — ж try { // выполняем потенциально ,кленам // опасный код } catch(Exception e) { // пытаемся восстановиться } К и'Р<Ш( «С Что писать в блоке catch, зависит от типа исклю¬ чения. Например, если сервер недоступен, в блоке catch можно реализовать попытку подключения к другому серверу. Е.сли файл отсутствует, можно за¬ просить у пользователя его местоположение.
Перехватываете исключение? Тогда кто его выбросил? В процессе работы с Java вы гораздо чаще будете обрабатывать исключения, чем создавать и выбрасывать их посредством своего кода. На данный момент достаточно иметь общее представление о том, как это работает: когда ваш код вызывает потенциально опасный метод — метод, который объявляет исключение, — имен¬ но этот метод выбрасывает исключение обратно вызывающему коду. На практике оба класса могут быть написаны вами. Однако это не имеет значения. Главное — понимать, какой метод выбрасывает ис¬ ключение, а какой его перехватывает. ^сьгвоет* -©— Ваш код вызывает потенциально опасный метод класс с по¬ тенциально опасным методом Если разработчик пишет код, который может привести к исключи¬ тельной ситуации, он обязан объявить это исключение. (Т) Потенциально опасный, выбрасы- 3сн вающий исключения код: ^ public void takeRisk() throws BadException { if (abandonAllHope) { throw new BadException(); / jM j 4к m<Wu'Y; ЭДфЦо* idrcHiic). } } '«<■ и Uc Y‘H.U По. (5) Ваш код, вызывающий потенциально опасный метод: public void crossFingers() { try { anObject.takeRisk(); } catch (BadException e) { System.out.println("Айяйяи! ), e.printStackTrace(); 4 другой мемод. (Исключение всегда передаемся обрамно вызывающему коду. /Vlei/иод, который выбрасываем исключение, должен объявить. чмо оно можем возникнуть.
Мскшкшля. кч ttujvu Шя&\.тп\- ся псактссамл Kuntnm/xnfxticn. п^хоСЛяюхгхся киитияте/х&и. Си н ш&тантся «прх &лмииы. пн искж- жпи.яии». Компилятор проверяет все исключения, кроме RuntimeException Кслохл хячххср mptcytnx: Если в коде выбрасывается исключение, его необходимо объ¬ явить с помощью ключевого слова throws в объявлении метода. lOException InterruptedException Runtimecxcepifon Если вызываемый метод выбрасывает исключение (то есть объявляет, что может его выбросить), необходимо подтвердить, что вы осознаете возможность возникновения этого исключе¬ ния. Один из способов удовлетворить требования компиля¬ тора — обернуть вызов метода в конструкцию try/catch. (Есть и второй способ, который будет рассмотрен позже в этой главе.) 6'н« иа| Д* iUUUA UoIWhU' I Z,rl‘%-"T&rm ClassCastException NullPointerException (He) дурацкие вопросы д>01Прос: Подождите-ка! Почему мы только сейчас обязаны использовать try/catch для об¬ работки исключения? А как же те исключения, с которыми я уже сталкивался, — например, NullPointerException или деление на ноль? Я даже получал NumberFormatException, когда вызывал lnteger.parselnt(). Почему их тогда не нужно было перехватывать? Ol/MB€l/H: Компилятор следит за всеми под¬ классами Exception, кроме одного особого типа — RuntimeException. Все исключения, наследуемые от RuntimeException, освобождены от обязательной обработки. Они могут быть выброшены в любом месте, даже если не указаны в throws и не обернуты в try/catch. Компи¬ лятор не требует, чтобы метод явно объявлял throws RuntimeException, и не настаивает, чтобы вызывающий код учитывал возможность их возникновения. ^ОМрОС: Хорошо, допустим. Но почему компиля¬ тор не обращает внимания на эти исключения при компиляции? Разве они не могут привести к сбою программы? 0(/ИВв1/И: Обычно RuntimeException связаны с ло¬ гическими ошибками в коде, а не с внешними обстоя¬ тельствами, которые могут неожиданно измениться во время выполнения. Например: вы не можете гарантировать, что нужный файл существует; не можете гарантировать, что сервер доступен. Но можете гарантировать, что ваш код не вы¬ ходит за пределы массива: для этого есть length. Такие ошибки, как ArraylndexOutOfBoundsException или NullPointerException, должны проявляться в процессе разработки и тестирования, а не в финальной програм¬ ме. Поэтому и не нужно загромождать код try/catch, если ошибка — это просто баг. try/catch нужен, чтобы обрабатывать непредсказуемые ситуации, а не логиче¬ ские ошибки. Используйте блоки catch, когда вы действительно не можете быть уверены, что операция пройдет успешно. Или, по крайней мере, выведите понятное сообщение и стек вызовов, чтобы было ясно, что именно пошло не так.
Без лишних слов ■ Метод может выбросить исключение, если во время выполне¬ ния произойдет ошибка. ■ Исключение — это всегда объект класса Exception. (Как мы говорили в главах о полиморфизме — 7 и 8, это значит, что объ¬ ект относится к классу, который напрямую или через наследова¬ ние связан с Exception.) ■ Комп илятор не отслеживает исключения типа RuntimeException. Их необязательно указывать в throws или оборачивать в try/ catch — хотя при желании вы можете сделать и то, и другое. ■ Все исключения, которые требует обработать компилятор, называются проверяемыми (checked exceptions). И только RuntimeException и его подклассы не относятся к проверяемым. ■ Чтобы выбросить исключение, используйте ключевое слово throw и создайте объект исключения: throw new NoCaffeineException(); ■ Если метод может выбрасывать проверяемое исключение, он обязан указать это в сигнатуре через throws SomeException. ■ Если вы вызываете метод, который выбрасывает проверяемое исключение, компилятор должен убедиться, что вы предусмо¬ трели его обработку. ■ Если вы готовы обработать исключение — оберните вызов в try/ catch, а в блоке catch напишите код обработки ошибки или вос¬ становления. ■ Если вы не готовы обрабатывать исключение, можно «передать ответственность» выше, указав throws в своем методе. (О том, как это сделать, мы поговорим далее в этой главе.) Петакогнитивный совет Если хотите действительно запомнить что-то новое, это надо делать прямо перед сном. После того как отложите эту книгу (если, конечно, сможете оторваться), не переключайтесь на что-то более сложное, чем, скажем, состав на упаковке хлопьев. Ваш мозг не просто отключается во сне — ему нужно время, чтобы переварить и раз¬ ложить информацию по полочкам, и это может занять несколько часов А если сразу начнете загружать себя чем-то другим, зна¬ ния по Java могут... уплыть мимо new и null. Физическая активность—другое дело. Танцы, спорт, прогулка — все это не мешает, а даже помогает закрепить то, что вы только что выучили. Хотите максимума поль¬ зы? Читайте эту книгу перед сном. Ну или хотя бы листайте кар¬ тинки — ваш мозг все равно будет учиться. — ЧЯ^Заточите карандаш Что вам нужно сделать: Как вы думаете, какие из этих действий могут вызвать исклю¬ чение, на которое компилятор должен обратить внимание? Речь идет о ситуациях, которые вы не можете контролировать в своем коде. Мы уже разобрали первый пункт. ^^Подключение к удаленному серверу Обращение к элементу за пределами массива _ Отображение диалогового окна на экране _ Получение данных из базы данных Проверка наличия текстового файла по пред¬ полагаемому пути Что может пойти не так: f\adcnurnn (Потому что он был самым простым.) ► Решать —вам! Создание файла _ Чтение символа из командной строки
Управление потоком выполнения с помощью конструкций try/catch Когда вы вызываете потенциально опасный метод, воз¬ можны два исхода. Либо метод успешен, и блок try за¬ вершается, либо метод выбрасывает исключение и пере¬ дает его вызывающему методу. Если блок try выпол¬ нен успешно (метод doRiskyThingO не выбро сил исключение). Стихла йиимюктся tuck ttu, K(f 7 t иг/М wntvwiwww Ш'Ж 'Слом catch. try { Если выполнение блока try завершилось с ошиб- Bb.6(„MeT°Ad°RlskyThin9» выбросил исключение): Ълск ttU ^ПЦСКЦдаСА. ис iw — -л ,I беиМтбаот исмю^ние, поэте.мч остабтаясА адсть Лекажи но (iX- try { -ф-Foo f int b .. НО бшМОЛНАОтГА, Н 9*Л0 ■ [ X. doRx sky Thing () ; ^ tm£ 0ИД жбисч^ ^ f.getNumO; ^ ..««uJfmr 6nno.m UVUVVvvvv, ) — иаМШАНОгО бти. ААННА' Д SoKioki)1 гаочи) ндапсА Затем Чьши кается (Сак ctdcк после юге ..истее j пр (|с ож«юн Г‘" •“►s } catch (Exception е) { -► System.out.println("провален"); System.out.println("Мы сделали это!"). % java Tester провален Мм сделали это!
Finally: код, который выполняется в любом случае Если вы пытаетесь что-то приготовить, вы на¬ чинаете с включения духовки. Если попытка приготовить блюдо полностью провалилась, вам нужно выключить духовку. Если попытка удалась, вам также нужно вы¬ ключить духовку. Духовку необходимо выключить в любом случае! Блок finally — это код, который должен выполняться независимо от вероятно¬ сти возникновения исключений. try { turnOvenOn(); х .bake(); } catch (BakingException e) f e.printStackTrace(); } finally { turnOvenOff(); } Что бы ни случилось, всегда ставьте машину на ручной тормоз, когда останавливаетесь. Мы так и не узнали, куда делся наш к предыдущий автомо- ^ Ь. биль... .Я Без finally приходится добавлять turnOvenOf f () как в блок try, так и в catch, потому что духовку нужно выклю¬ чить в любом случае. Блок finally позволяет поместить весь важ¬ ный код очистки в одном месте и не дублиро¬ вать: try { turnOvenOn(); x.bake () ; turnOvenOff(); } catch (BakingException e) { e.printStackTrace(); turnOvenOff(); Если выполнение блока try завершилось с ошибкой (возникло исключение), управление сразу переходит к блоку catch. После завершения выполнения блока catch выполняется блок finally. Затем продолжается выполнение метода. Если выполнение блока try прошло успешно (без ис¬ ключений), блок catch будет пропущен, и управление переходит к блоку finally. Затем продолжается выпол¬ нение метода. Если в блоке try или catch есть оператор return, блок finally все равно выполняется! Сначала про¬ изойдет переход к блоку finally, затем управление вернется к return. }
*• Решать —вам! 1^3аточите карандаш Управление потоком выполнения Посмотрите на код слева. Как вы думаете, каким будет вывод этой программы? А каким он будет, если третью строку программы изменить на String test = "да"? Предположим, что ScaryException наследуется от Exception. public class TestExceptions { Вывод, когда test = "нет" public static void main(String[] args) { String test = "нет"; try { System.out.println("начало блока try"); doRisky(test); System.out.println("конец блока try"); } catch (ScaryException se) ( System.out.println("перехвачено исключение ScaryException"); } finally ( System.out.println("блок finally"); } System.out.println("конец метода main"); ) static void doRisky(String test) throws ScaryException ( n „ ъывод, когда tsst — дв. System.out.println("вход в doRisky"); if ("да".equals(test)) ( throw new ScaryException(); ) System.out.println("выход из doRisky"); ) class ScaryException extends Exception ( ) utpui etro,L3w пэноя - Ацвиг; xoirg - uoigdaoxgAnnos эинэьоигаои OHahcaxadau — A^siyop a tfoxg - Axg esoirg oltefiBH etx „ = дээз eCjom МвхчкАеэ^ итвш BtroiLsw Пэном - Attpuij xoirg - Axg exoirg 'пэноя - A^sx^op ей troxiag - Aofsxaop s troxa - Ахз nxoirg сжвьвн = gsэз eBjom ‘хшшАсад
Метод может выбрасывать сразу несколько исключений? Если метод может выбрасывать несколько исключений, в его объявлении нужно указать все проверяемые исключения. Однако если несколько из них наследуются от одного суперкласса, можно указать только его это упростит сигнатуру метода. Перехват нескольких исключений Компилятор проверит, обработаны ли все проверяемые исключения, которые может выбрасывать вызываемый метод. Для этого разместите один или несколько блоков catch сразу под try. Иногда порядок блоков catch важен — но об этом мы поговорим чуть позже. public class Laundry { public void doLaundryO throws PantsException, LingerieException { // код, который может public class WashingMachine { public void go() { Laundry laundry = new Laundry 0 try { laundry.doLaundry() ; } catch (PantsException pex) ^ J tnt> { catch lex) f finu.utmjcrj JciuuuuifijO Ымси&ит исклюшше сне twnajatm UU cakkfiM JLbxf // код восстановления } }
Исключения полиморфны Исключения — это объекты, так? В них нет ничего осо¬ бо необычного, кроме того, что их можно выбрасывать. Поэтому, как и все замечательные объекты, исключения могут использоваться полиморфно. Например, объект LingerieException можно присвоить ссылке типа ClothingException. Объект PantsException можно присвоить ссылке типа Exception. Думаем, идея ясна. Преимущество исключений в том, что метод не обязан явно объявлять каждое возможное исключение, которое может выбросить; он может объявить суперкласс этих исклю¬ чений. То же относится и к блокам catch; вам не нужно писать отдельный блок catch для каждого возможного исключения, если имеющийся у вас блок (или несколько блоков) может обработать любое выброшенное исключение. Вы можете ОБЪЯВЛЯТЬ исключения, используя суперкласс для всех исключений, которые выбрасывает ваш метод. /Зсе исклюшшл аишт «дидй сипгркмгсс — exception. lOException ClothingException public void doLaundryO throws ClothingException ClothingExct / PantsException LingerieException ShirtException fL " Iff smej,iu «Т'». - TeeShirtException DressShirtException | ES Вы также можете ПЕРЕХВАТЫВАТЬ исключения, используя суперкласс выброшенного исключения. try { laundry.doLaundry(); } catch(ClothingException cex) { // код восстановления Дож-tnv vk n к ( ('illv/tujl VU|\tH»V ■nn try ( laundry.doLaundry(); ^-xj b } catch(ShirtException shex) { ' in*//, J n. // код восстановления } }
Вы МОЖЕТЕ перехватить все исключения с помощью одного большого суперкласса в блоке catch, это не означает, что вы ДОЛЖНЫ это делать. Вы могли бы указать в коде обработки исключе¬ ний только один блок catch, используя суперкласс Exception в операторе catch, чтобы перехватывать любое потенциальное исключение. try { laundry.doLaundry(); } catch(Exception ex) { // код восстановления... } Напишите отдельный блок catch для каждого исключения, которое требует уникальной обработки. Например, если ваш код обрабатывает (или восстанавли¬ вается после) исключение TeeShirtException иначе, чем после LingerieException, то напишите отдельные блоки catch для каждого. Но если все остальные исключения ClothingException должны обрабатываться одинаково, то добавьте блок catch для ClothingException, чтобы обработать остальные. try { laundry.doLaundry(); Йсй-тшиГим® несли Ч£ТС? Этот. бСкж crtltK nifxtxdamum.t к.т. исклюттиь. ток _ ми) 6м нс ешште лбЬишлпитски enht- ^елмть. tmc itutHHO пешие iu так. ' } } } } catch (TeeShirtException tex) { // код восстановления после TeeShirtException _ij UCTtAH/ttHVUV UM. catch (LingerieException lex) { // код восстановления после LingerieException U j' VUAtt КЩ ,ичпи-«Ч cuijucm «йма ftuivfc ( vVCKW catch. 1 ^ catch (ClothingException cex) { // код восстановления после всех прочих «та Ullli CVcth nW;v'.1 Цск ih;(Mi *рапш&,(!У'с* ^ блоке catch 6 M</u
Д бии гаш m|wr зсвятаваюклся fe. СCctlvimj£xctj\t i cvv, |а иг к,лкшии,и JttSKiit&cctj(vtic»v u.<5Ki4t&ct|(Vtic»v— chu vj-m Obvuv сс^щстдт tvcuuuAH. catch(ClothingException ce*) Чем выше класс находится в иерархии исклю¬ чений, тем шире «корзина» перехвата. А при движении вниз, к более специализирован¬ ным классам, «корзина» становится меньше — это обычный полиморфизм в действии. Например, блок catch (ShirtException е) достаточно «широк», чтобы пере¬ хватить и TeeShirtException, и DressShirtException — а также любые будущие подклассы ShirtException. Блок catch (ClothingException е) охва¬ тывает еще больше: он перехватывает не только сам ClothingException, но и все, что от него наследуется — например, PantsException, UniformException, LingerieException, ShirtException и других потомков. Са¬ мый универсальный вариант — catch (Exception е) —перехватывает все, вклю¬ чая даже RuntimeException. Поэтому его обычно используют только в тестах или от¬ ладке — в обычной практике с ним надо быть осторожнее.
Нельзя ставить «большие корзины» на «маленькие» Чисто технически это сделать можно, но код просто не скомпилируется. Блоки catch — это не перегруженные методы, где выбира¬ ется наиболее подходящий вариант. При об¬ работке исключений JVM начинает с первого блока catch и двигается вниз, пока не найдет достаточно широкий (находящийся достаточно высоко в дереве наследования), чтобы обработать выброшенное исключение. Если первым указан блок catch (Exception ex), компилятор сразу поймет, что все последующие блоки бессмысленны: до них выполнение кода про¬ сто не дойдет. try { laundry.doLaundry(); I lo| >идок «сестринских» исключении } catch(ClothingException сех) { // код восстановления после ClothingException } catch(LingerieException lex) { // код восстановления после LingerieException (на одном уровне иерархии, например, PantsException и (ingerieException) может быть любым, так как они не ко! [куре! ггы и не г 1ерехватывают исключе! шя друг друга. Вы можете разместить ShirtException выше LingerieException, и это никого не смутит. } catch(ShirtException shex) { // код восстановления после ShirtException ) Хотя ShirtException — это более крупный (более общий) тип, так как может перехватывать свои подклассы, он не сможет поймать LingerieException. Поэтому кон¬ фликтов не возникнет.
аточите карандаш Предположим, что представленный блок try/catch написан коррек¬ тно. Ваша задача — нарисовать две разные диаграммы классов, которые точно отражают структуру классов исключений. Другими словами, какие иерархии классов исключений сделают данный блок try/catch допустимым и логически правильным? try { x.doRisky () ; } catch(AlphaEx а) { // код восстановления после AlphaEx } catch(BetaEx b) { // код восстановления после BetaEx } catch(GammaEx с) { // код восстановления после GammaEx } catch(DeltaEx d) { // код восстановления после DeltaEx } BazEx Ваша задача — создать две разные корректные структуры try/catch (по аналогии с приведенной рядом), которые точно соответствуют указанной диаграмме классов. Работая над заданием, исходите из того, что метод внутри блока try может выбрасывать любое из указан¬ ных исключений. Ответы на оба задания приведены на стр. 493
Когда не хочется обрабатывать исключение... Если вы не собираетесь обрабатывать ис¬ ключение в этом методе, его можно «пере¬ адресовать» — достаточно объявить его в сигнатуре метода. Когда вы вызываете метод, способный выбросить ис¬ ключение, компилятор требует, чтобы вы это учли. Чаще всего это означает, что вам нужно обернуть вызов в кон¬ струкцию try/catch. Но есть и другой вариант — про¬ сто «переадресовать» исключение дальше и позволить методу, который вызвал ваш код, обработать его. Сделать это просто: достаточно объявить, что метод может выбрасывать исключение. Даже если исключение на самом деле выбрасывает не ваш код, это неважно: вы все равно допускаете, что оно может возникнуть, и позволяете ему пройти дальше без обработки. Но если вы «переадресовываете» исключение без кон¬ струкции try/catch, что же произойдет, когда по¬ тенциально опасный метод (doLaundry ()) выбросит исключение? В этом случае метод немедленно удаляется из стека вызовов, а исключение передается следующему методу в стеке — тому, который вызвал текущий метод. Если этот метод также «переадресовывает» исключение, он тоже удаляется со стека, а исключение передается еще ниже... И до каких пор? Вы узнаете об этом чуть позже. fttv ( А \\ К нс (itu jwt'Ctatvvt UCK. CK - тсннс, нс нескольку нс mfvmj. нс вь\Л)б ^исскованншо линнда 6 ttvj /catck, ни I\i|u «^искованньслс» становится tpu /Ц Щ .cctmwj. А |натит, mrncju тот, кто вьиьсваст вм^вавиоии /1/1Ш Ж£- 7СЮ кс<|. содран *рсть\вапи всялсож- спи исклютснил и eojvicamtcoamt, его. нос* public void foo() throws ReallyBadException { // вызов потенциально опасного метода без try/catch laundry.doLaundry(); }
Переадресовывая (через объявление) исключение, вы лишь откладываете неизбежное Рано или поздно кому-то придется с ним разобраться. А если метод main() тоже увернется от исключения? public class Washer { Laundry laundry = new Laundry(); public void foo() throws ClothingException { laundry.doLaundrу(); ) public static void main (String[] args) throws Washer a = new Washer(); a.foo(); I t ml цклонлюихся om. toWcW- (1(£i (»u|\tai||Mcc6bv6o« u <m~ vw h '" ( nU)K tvYUi UQ vu|\t:vfia- ( wvvih'11-* ClothingException { OdoLaundryO переадре¬ совывает исключение ClothingException main() вызывает foo(), foo() вызывает doLaundryO, doLaundryO выполня¬ ется и выбрасывает ClothingException. ©foo() переадресовы¬ вает исключение Omain() переадресо¬ вывает исключение JVM завер- шает работу. doLaundryO сразу уда¬ ляется из стека, и ис¬ ключение передается обратно в foo(). Но в foo() нет блока try/catch, поэтому... foo() удаляется из стека, и исключение передается обратно в main(). Но в mainO тоже нет блока try/catch, по¬ этому исключение передается дальше... S, Мы использовали исключение TeeShirtException для логического сочетания с исключением ClothingException. Знаем, знаем... у вас в голове другие сочетания. Кому? Куда? Никого не осталось, кроме JVM, которая бубнит: «Не рассчитывай, что Я тебя из этого вы¬ тащу».
Обрабатывайте или объявляйте. Это закон. Итак, мы рассмотрели оба способа удовлетворить компилятор при вызове рискованного (выбрасывающего исключение) метода. ©ОБРАБОТКА Оборачиваем потенциально опасный вы¬ зов в конструкцию try/catch. try { laundry.doLaundry() ; O' } catch(ClothingException cex) { // код восстановления ) Ранний сжк catch аалжт c'twv ас- 'ТТшяТк- '*«' «fm&Jnu ® ОБЪЯВЛЕНИЕ (переадресация) Объявляем, что ВАШ метод выбрасывает те же исключения, что и вызываемый потенциально опасный метод. void foo() throws ClothingException { laundry.doLaundry(); } „/Hanoi ко (Гтмаря K1HM ccdiu 4o Но теперь код, вызывающий метод foo(), обязан следовать закону «обрабатывайте или объявляйте». Если foo() укло¬ няется (переадресовывает) от обработки (через throws), a main() вызывает foo(), то main() придется разбираться с этим исключением. public class Washer { Laundry laundry = new Laundry(); public void food throws laundry.doLaundry(); } public static void main Washer a = new Washer( a.foo () ; ClothingException { (String[] args) ) mukcjiku мстил jci() уклоняется m</iuujjucv^n&um) cm (([иконки иск исн hum (Mth/ru/xctMicn., йи[икц иасмшс мстинм. aaCaundryf), nxainO (. mo (пиншт a. лиге сам шлтть, mw он ПШЖС Ml шаги form tl, ,11Wi, / Ml j\ KqvubmonvL mainO hi kom- 1\к1и|ицтс.ч. илт t\MVjtai.u tuuu Kij «uratfWTttd. txcthTtotv». Юля (4мшллтс(и1 .Mtmca kt() mu\i|u сттттгл шкстииксж исклютеммя.
Возвращаясь к коду секвенсора... Хоть вы уже и запамятовали, мы начали эту главу со знакомства с JavaSound API. Мы создали объект Sequencer, но он не компи¬ лировался, потому что метод Midi . getSequencer () объявляет проверяемое исключение (MidiUnavailableException). Но теперь мы можем это исправить, обернув вызов в конструкцию try/catch. public void play() { try { Sequencer sequencer = MidiSystem.getSequencer() System.out.println("Секвенсор успешно установлен ") catch(MidiUnavailableException^e) System.out.println("Облом"); { ктапМти «мМ,, 1 кгнптш. Уликами, ^ Щ). КМ Hlf fine Правила исключений ф Вы не можете использовать блоки catch и finally без try. а'ГЦ/Ш void до () ( ^ \cl-f сК*(Ч Foo £ - new Fо о () ; 4Л|'С> f.foof (); catch(FooException ex) { ) I Нельзя размещать код между блоками try и catch. try ( мясяясти х .doStuf f (, ; tZWy*** } / H f -lm,c cam ы,fa. int у = 43; } catch(Exception ex) ( ) нпжлаться либо eaten, KCJDyxwj(77. %[XT.n‘r r4 вождаться либо try { x.doStuff0 } finally { // очистка J (4) Блок try c finally без catch все равно дол¬ жен объявлять исключение. void go() throws FooException { try 1 «&«•'* Дитжксн дат x.doStuff(; } finally { ) }
Готовим код Все, что ты видишь, я приготовил сам. Это приятнее, чем просто взять готовый код? Не нужно писать весь код самостоятельно, но так гораздо интереснее. Оставшаяся часть главы необязательна — вы можете использовать заранее подготовленный код для создания всех музыкальных приложений. Но если вам любопытно Йнать больше avaSound API, переверните страницу.
Как создается звук В начале главы мы рассказали, что MIDI-данные содержат инструкции о том, ка кие ноты, как и какими музыкальными инструментами должны зву шть, но сами по себе они не содержат слышимый звук. Чтобы музыка зазвучала, I данные передаются через MIDI-устройство, которое преобразует инструкции в звук, за¬ пуская аппаратный или «виртуальный» инструмент (программный синтезатор). В этой книге мы используем только программные устройства. Схема воспроизве¬ дения звука в JavaSound выглядит так: Необходимы ЧЕТЫРЕ элемента: @ Устройство для (5) Музыка для воспро- Объект Sequence, воспроизведения изведения содержащий дан- музыки ные @ Собственно музы- w кальные данные: ноты, длитель¬ ность ит. д. Секвенсор воспро¬ изводит ► Последова¬ тельность содер¬ жащую ¥ Дорожки в которых хранятся Секвенсор — устройство, фактически воспроизводя¬ щее музыку. Словно умная колонка, воспроизводящая аудиофайл. это композиция, дельное музыка! произведение, к будет воспроизЕ секвенсор. В рамках данного проекта нам доста¬ точно одной дорожки, поэтому у нас будет однодорожечная ком¬ позиция. Эта дорожка содержит все MIDI- данные композиции. l(mtm бале w ncHAt7 г Г***-*-"' ""даtTc«r ^сд^башиьнссть как (>> fCK келс тент (cujtp аиущ пииькс u}W| ^u.4ckvj). (bu|4fvuaH,HM с тин. как б аорат. кстги^н xfumunv hcc'ujc (’cimi.Ataccmi. MIDI-событие— это со¬ общение, которое может распознать секвенсор. По его словам: «В этот момент времени сыграй ноту до первой октавы, с такой-то скоростью и силой нажатия, и про¬ игрывай ее столько-то». MIDI-событие также мо¬ жет передавать команды вроде: «Сменить текущий инструмент на флейту».
Пять шагов к музыке: ф Создайте объект Sequencer и откройте его. Sequencer player = MidiSystem.getSequencer (); player.open (); ф Создайте новую последовательность. Sequence seq = new Sequence(timing,4); (3) Создайте дорожку из последовательности Track t = seq.createTrack(); (4) Заполните дорожку объектами MidiEvent и передайте последовательность объекту Sequencer. t.add(myMidiEventl); player.setSequence(seq);
Версия 1: Ваше первое приложение для воспроизведения Наберите код и запустите. Вы услышите звук, как будто кто-то играет одну ноту на пианино! (Ладно, не кто-то, а что-то). звука tmt import javax.sound.midi.* import static javax.sound.midi.ShortMessage.* public class MiniMiniMusicApp { public static void main(String[] args) { MiniMiniMusicApp mini = new MiniMiniMusicApp(); mini.play(); Стапмжкшл. luuupn. yhWr итммо- бл mt констднепт. класса SlwrlHt&ay. } public void play() { try { Sequencer player = MidiSystem.getSequencer() player.open(); Sequence seq = new Sequence(Sequence.PPQi 4) лише : ^ ' а(Гпи.««тишки) W< ecWnu/mme fiutuntuw на а|иилшл~ ,nn констНиктона 3ecjatnct. Jlpocmc ckchuMHiW at шожне c , «готстллн» apijauamauu). читать at (3) Track track = seq. createTrack () JanfitmufotM.уSсдисщх ( ShortMessage msgl = new ShortMessage(), msgl.setMessage(NOTE_ON, 1, 44, 100); MidiEvent noteOn = new MidiEvent(msgl, track.add(noteOn); ShortMessage msg2 = new ShortMessage(); msg2.setMessage(NOTE_OFF, 1, 44, 100); MidiEvent noteOff = new MidiEvent(msg2, track.add(noteOff); tr{^ ftKm 1-Шс, aJiWf ganntu ~~ g, г 4«out * j VoefKxnit J,’ 1) 1 несколькообъектов / Ц((|(Ли\1 (? Ttock. Здесь и|гаш.нен \ d основном «готовый» код. &тн- ( сгн& иное, на что слшрх. офажить \ (?аалиьнае, - это аКьилантье u( ,nwp «ьДшадеО и контакте I. ,3т ЬахлЫм at на 16) ; ) на еле г|1|и ни и страна^ player.setSequence(seq) ; ^—. player. start () ; J— ^ JlmaatM. ссчект'%umc< <?3еьj ад..«и^лх« а<* «caxfwAf тр.шупм ^ /ш'«* (слоб’ас даее^ёнаяА } catch (Exception e) { e.printStackTrace(); } }
Создание объекта MidiEvent (^MIDI-данные) MidiEvent — это инструкция для исполнения части музыкального произ- ведения. Серия объектов Midi Event похожа на нотный стан или перфори- ИШгСООЫТИб рованную ленту для пианино. Большинство интересующих нас Midi-событий УКЭЗЫВЭбТ ЧТО описывают, что именно нужно сделать, и в какой момент времени это * должно произойти. Тайминг имеет решающее значение в музыке, ведь одна ДвЛ&ТЬ И КОГДА, нота следует за другой в определенном порядке. Поскольку Midi-события весьма детализированы, необходимо явно указывать момент начала вос¬ произведения ноты (событие NOTE ON) и момент ее завершения (событие NOTE OFF). Очевидно, что отправка команды «остановить воспроизведение ноты «Соль»» (NOTE OFF) раньше команды «начать воспроизведение ноты Соль»» (NOTE_ON) приведет к некорректному результату. MIDI-команда помещается в объект Message; само Midi-событие — это комбинация Message и времени, когда это сообщение должно «сработать». Другими словами, Message может содержать команду «Начать воспроизве¬ дение ноты «До» первой октавы», a MidiEvent дополнит ее информацией: «Запустить эту команду на четвертом шаге». Таким образом, нам всегда нужны и Message, и MidiEvent. Объект Message указывает, что делать, a MidiEvent — когда. (Т) Создайте сообщение ShortMessage msg = new ShortMessage() ф Поместите в него инструкцию msg.setMessage(144, 1, 44, 100); ® Маке а new MidiEvent using the Message MidiEvent noteOn = new MidiEvent(a, 1) @ Add the MidiEvent to the Track track.add(noteOn); , Каждая ко¬ манда долж¬ на содержать информацию о времени ее выполнения. Другими слова¬ ми, необходимо указать, на ка¬ ком шаге долж¬ но произойти действие. a «Ж /3 ЭП'С. (с Ын. /и сраЛн пп 1 сип,п“м С(Ю(Ще.нш а шиж (растыпи на тр(?а« 1шт (шаи $ шт инсрсрма- эти Согласно ft«« <М0 с/инит ***** пЛитш Г««» м ~ ***“? (ГкмпЗищ n«Y АспроиЛмт их (?маантн гмрядке.УПож- (фт.шпнх rnmlmiufi нссксмкс нот.шгшп Лшат (|Н( шкШтят. ттшлнн, нмгашко нот.шгшп гьшапи и, ф.мсннс. мир<тш инс:прижнть\ мщт MifWjtofM jlapmoti.JuH a mom oia цемент 0{ч.шми. н(- и
MIDI-сообщение — сердце Midi-события MIDI-сообщение содержит часть события, которая указывает, что именно нужно сделать. Это фактически инструкция, которую должен выполнить секвенсор. Первый аргумент инструкции всегда обозначает тип сообщения. Значения остальных трех аргументов зависят от этого типа. На¬ пример, сообщение с типом 144 означает команду NOTE_ON (воспроизведение ноты). Но чтобы выполнить команду NOTE_ON, секвенсору нужна дополнительная информация. Представьте, что секвенсор спрашивает: «Хорошо, я сыграю ноту, но на каком канале? Другими словами, вы хотите, чтобы я сыграл звук барабана или фортепиано? И какая именно это нота? До первой октавы? Ре¬ диез? И раз уж на то пошло, с какой скоростью (силой нажатия) мне ее сыграть?» Чтобы создать MIDI-сообщение, необходимо создать экземпляр ShortMessage и вызвать метод setMessage (), передав в него четыре аргумента, соответствующих сообщению. Но помни¬ те, что сообщение определяет только что делать. Поэтому его еще нужно вложить в событие (MidiEvent), которое добавит информацию о том, когда это сообщение должно быть исполнено. Структура сообщения Первый аргумент метода setMessage() всегда определяет тип сообщения. Остальные три ар|умента зависят от этого типа. Сообщение указывает, что делать, а MidiEvent — когда. & у" msg.setMessage(144, с'4 ^ 44, 100) ; .л*** & кУ ДО 1, Остситш mfw аргииШпа а/Сшмт cm пита пс*)щстн £11ч wcduci ни я ЖОТС_ (Ж(1 /,/, уопсиншм и not afu^nuHmtx * поясняют, как пират- нс,му (?) Тип сообщения Значение 144 означает NOTE_ON (воспроизвести ноту) Значение 128 означает NOTE. OFF (закончить воспроизведение ноты) (5) Канал Канал можно представить как одного из музыкантов в группе. Например, канал 1 — это клавишник, канал 9 — барабанщик и так далее. (з) Воспроизводимая нота Число от 0 до 127 определяет, какую ноту играть (от самых низких до самых высоких). fm0tW.AC^v № ...».лаьжнс 1Л('~ (4) Сила нажатия клавиши константы «KcTt'Wwscwc. на- ЪШ W. г гниоп ш) vua^nvHtc. •! Отражает, с какой силой нажата клавиша. Значение 0 настолько слабое, что звук практически не слышен, а 100 — хорошее стандартное звучание.
Изменение сообщения Теперь, когда вы знаете, из чего состоит MIDI-сообщение, можно начинать экс¬ периментировать. Вы можете изменить воспроизводимую ноту, продолжитель¬ ность звучания, добавить дополнительные ноты и даже сменить инструмент. (?) Изменение ноты Попробуйте применить значение от О до 127 в сообщениях о включении и выключении ноты. msg.setMessage (144, 1, 20, 100); Изменение длительности звучания ноты Настройте событие NOTE_OFF (не само сообщение), чтобы оно наступало раньше или позже. msg.setMessage (128, 1, 44, 100); MidiEvent noteOff = new MidiEvent(b, (5) Изменение инструмента Добавьте новое сообщение РАНЕЕ сообщения, воспроизводящего ноту, и укажите инструмент для канала 1. Сообщение смены инструмента — это 192, а третий аргумент определяет номер инструмента (можно выбрать значение в диапазоне от 0 до 127). first. setMessage (192 , 1, 102, 0);
Версия 2: Экспериментируем со звучанием с помощью консольных аргументов Эта версия по-прежнему воспроизводит только одну ноту, но теперь вы можете использовать аргументы командной строки для изменения инструмента и ноты. Поэкспериментируйте, передавая два целых числа в диапазоне от 0 до 12 7. Первое число определяет инструмент, а второе — ноту, которую нужно сыграть. import javax.sound.midi.*; import static javax.sound.midi.ShortMessage.*; public class MiniMusicCm.dLine { public static void main(String[] args) { MiniMusicCm.dLine m.ini = new MiniMusicCmdLine() ; if (args.length < 2) { System..out.println("He забудьте передать аргументы: инструмент и ноту"); } else { int instrument = Integer.parselnt(args[0]); int note = Integer.parselnt(args [1]); m.ini .play (instrument, note) ; public void play(int instrument, int note) { try { Sequencer player = MidiSystem.getSequencer (); player.open(); Sequence seq = new Sequence(Sequence.PPQ, 4); Track track = seq.createTrack(); ShortMessage msgl = new ShortMessage(); msgl.setMessage(PROGRAM_CHANGE, 1, instrument, 0); MidiEvent changelnstrument = new MidiEvent(msgl, 1); track.add(changelnstrument); ShortMessage msg2 = new ShortMessage(); msg2.setMessage(NOTE_ON, 1, note, 100); MidiEvent noteOn = new MidiEvent(msg2, 1) ; track.add(noteOn); ShortMessage msg3 = new ShortMessage(); msg3.setMessage(NOTE_OFF, 1, note, ICC); MidiEvent noteOff = new MidiEvent(msg3, 16); track.add(noteOff); player.setSequence(seq) ; player.start(); } catch (Exception ex) { ex.printStackTrace(); 1 nib cm (: mumt с иллй гееннами (М/ушнп&ш йт 127.JI (»уисуйт< уил нсШш uujUHMiut йуишнта: File Edit Window Help Attenuate 1 %java MiniMusicCmdLine 102 30 %java MiniMusicCmdLine 80 20 %java MiniMusicCmdLine 40 70 }
Что вас ждет в дальнейших главах Глава 13: цель Когда мы закончим, у нас будет ра¬ бочий битбокс со встроенным чатом. Нам предстоит изучить графический интерфейс (включая обработку со¬ бытий), ввод/вывод, работу в сети и потоки выполнения. В следующих трех главах (14,15 и 16) мы разбе¬ рем все необходимое. • ее Bass Drum { Dosed Hi-Mat Op*n Mi-Hat Acchistk Soan Crash Cymbal Hand Clap High Tom HI Bongo Maracas { Whistle low Conga Cowbell VI bras lap Low-mid Tom High Agogo Open Hi Conga О О о ту а о а а о о о Q о о о ООО Вайб: ритм 2 Бруклин: ритм 2, переработанный ТунцТунц: танцевальный бит Глава 14: MIDI-события В этой главе мы создадим неболь¬ шое «музыкальное видео» (хотя это громко сказано...), которое будет ри¬ совать случайные прямоугольники в такт MIDI-музыке. Мы научимся создавать и воспроизводить множе¬ ство MIDI-событий (а не пару, как в текущей главе). Jlejntftiii (finwjwk Ъшшй Чип&рптй. шаг шаг шаг шаг Глава 15: Автономный битбокс Теперь мы создадим полноценный битбокс с графическим интерфей¬ сом. Однако его возможности пока ограничены: при изменении рит¬ мического рисунка предыдущий теряется. В этой версии нет функций сохранения и загрузки, а также се¬ тевого взаимодействия. (Но потре¬ нироваться в создании ритмичных лупов остается.)
А. Упрах Упражнение В этой главе вы открыли для себя удивительный мир исключений. В этом упражнении вам нужно определить истинно или ложно каждое из последующих утверждений. & Правда или ложь 1. Блок try должен сопровождаться либо блоком catch, либо блоком finally, либо обоими. 2. Если в методе есть код, способный выбросить проверяемое исключение, его нужно обернуть в блок try/catch. 3. Блоки catch могут быть полиморфными — перехватывать суперклассы исключений. 4. Перехватывать можно только те исключения, которые подклассируют Ihrowable. 5. Если используется try/catch, блок finally необязателен. 6. Блок try может сочетаться с catch, с finally или с обоими. 7. Метод, объявляющий throws для проверяемого исключения, обязан обернуть соответствующий код в try/catch или передать исключение дальше. 8. Метод main() должен либо обработать, либо передать все проверяемые исключения, которые ему достаются. 9. Один блок try может сопровождаться несколькими блоками catch для разных типов исключений. 10. Метод может объявить выбрасывание нескольких типов исключений, не только одного. 11. Блок finally выполняется всегда — независимо от того, было ли выброшено исключение. 12. Блок finally не может существовать без блока try. 13. Блок try не может существовать без хотя бы одного catch или finally. 14. Переложение обработки исключения на вызывающий код иногда называют «уклонением» (ducking). 15. Порядок блоков catch важен: сначала должны идти более специфичные исключения, затем более общие. 16. Метод с блоком try/finally может объявлять выбрасывание исключений, но это не обязательно. 17. Исключения времени выполнения (наследники RuntimeException) можно не обрабатывать и не объявлять, но при желании можно сделать и то, и другое. Ответы на стр. 493
Магнитики с кодом На холодильнике в беспорядке разбросаны магнитики с кодом рабочей Java-программы. Фрагменты перепутались: кажется, будто кто-то играл ими. Ваша задача — переставить кусочки кода в правильном порядке, что¬ бы получилась рабочая программа. Она должна скомпилироваться и выве¬ сти нужный результат. Несколько фигурных скобок при этом, увы, упали на пол и затерялись... Так что добавляйте их столько, сколько потребуется! ■*- Ответы на стр. 494
КроссВюрс) Ответы на стр. 495 Вы знаете, что надо делать! По горизонтали: 3. Минимальное изменение в последовательности 4. Основная единица кода в Java 5. Характеристика объекта, содержащая данные 7. Модификатор доступа, открытый для всех 9. Визуальное оформление текста 12. Тот, кто собирает мусор 14. Отдельный кусок кода или данных 16. Структура данных, добавленная в Java 16 По вертикали: 1. Неожиданная ситуация, нарушающая выполнение программы 2. Объект, передаваемый в блок catch 6. Точка подключения к сети или приложению 8. Экземпляр класса 10. Последовательная коллекция элементов 11. Значение, по которому ищут в словаре 12. Процесс подготовки программы к запуску 13. Обращение к методу или функции 15. То, где всё отображается 17. Последовательность однотипных значений Подсказки: aoiDodu Mdu хэнэщ St ()etroi3i/\|BWM э1этии iqa ettiox 'iMtfox3Mod|j g i мэи сичЬюиои з Ю1эеНЕСГ) '8 xei эн istTH oi-oih effiox s laetfeuou z ионнейижоэн onodu т/и MOHMiiMdx 41149 J-эжои ено 4 Hi/exmdaa оц щычеи эжей и ни еНох 'ениеф oi4i3Bh 41149хэжои oi£ > L эонжЛнэн хэш/etM и эиЛх ou intfox но Z L 01x3490 Mdi/на BBHHSHsdau oie онычдо S 13X31 oiiodu — Vox ээн tag 4, В13ЭИ1 з ючтэниаНз эн сиэн tag он 'иихчнэнетч но £ HireiHocudoj оц
Решение DeltaEx Заточите карандаш (со стр. 476) Т Решение Правда или ложь (со стр. 490) 1. Ложь. Можно использовать либо один блок, либо оба. 2. Ложь. Можно обойтись объявлени¬ ем исключения. 3. Правда. 4. Ложь. Исключения времени выпол¬ нения можно перехватывать. 5. Правда. 6. Правда. Оба варианта допустимы. 7. Ложь. Достаточно объявления. 8. Ложь. Но если этого не сделать, JVM может завершить работу. 9. Правда. 10. Ложь. 11. Правда. Часто используется для очистки частично выполненных задач. 12. Ложь. 13. Ложь. 14. Ложь. «Передача дальше» (ducking) означает объявление. 15. Ложь. Наиболее общие исключе¬ ния должны обрабатываться послед¬ ними. 16. Ложь. Если нет блока catch, необхо¬ димо объявить исключение. 17. Ложь.
Решение упражнений Магнитики С КОДОМ (со стр. 491) class MyEx extends Exception { } public class ExTestDrive { public static void main(String[] args) { String test = args[0]; try { System.out.print("t"); doRisky (test); System.out.print ("o"); } catch (MyEx e) { System.out.print("a"); ) finally { System.out.print("w"); } System.out.println("s" ) ; static void doRisky(String t) throws MyEx { System.out.print("h") ; } if ("yes".equals(t)) { throw new MyEx(); i System.out.print("r"); | File Edit Window Help Chill % java thaws ExTestDrive yes % java throws ExTestDrive no
осс&орс) (со стр. 492) !о “к Зш а г л и 80 ю 13в 2И б 7П У б Л и ч н ы й с к ъ 3 4К л а с ,0С е 0 л п к в ю 9Ш р и ф т 6П ч с 15э ’п 0 л е 12с б 0 р щ и к 17м р н 6 к р а т и 0 163 а п и с ь е р н с к и ыФ р а г м е Н т в
14. Создание графического интерфейса Очень наглядная история Вау! Это вы- " глядит потрясающе. Похоже, внешний вид действительно решает Я слышал, что твой бывший мог готовить только блюда из ко- мандной строки. Признайте, вам нужны графические интерфейсы. Если вашими приложениями будут пользо¬ ваться другие люди, необходимо снабдить разработки графическим интерфейсом. Если даже вы пишете программы для себя, вам тоже захочется удобный интерфейс. Даже если вы увере¬ ны, что всю оставшуюся жизнь будете писать серверный код, где пользовательский интерфейс ограничивается веб-страницами, рано или поздно вам все равно придется создавать инструмен¬ ты, и тогда снова понадобится графический интерфейс. Да, консольные приложения выглядят «винтажно», но не в хорошем смысле. Они неудобны, ограничены в возможностях и не слишком дружелюбны к пользователю. Мы посвятим два следующих раздела работе с графическими интерфейсами и одновременно изучим соответствующие ключевые особенности Java, такие как обработка событий, внутренние классы и лямбда-выражения. В этом разделе мы выведем кнопку на экран и научим ее реагировать на нажатие. Мы нарисуем графику на экране, отобразим JPEG-изображение и даже создадим простую анимацию.
Все начинается с окна JFrame — это объект, представляющий окно на экране. В нем размещаются все элементы интерфейса: кнопки, флажки, тек¬ стовые поля и многое другое. В нем можно создать полноценную строку меню с коман¬ дами. Кроме того, у него есть все стандарт¬ ные значки управления окнами, характер¬ ные для вашей операционной системы: Вопросы действительно важные, сворачивание, развертывание и закрытие. Через несколько страниц мы раз- Внешний вид JFrame зависит от платфор¬ мы, на которой он запущен. Вот как выгля¬ дел JFrame в старой версии Mac OS X: берем их в специальном разделе «(Не) дурацкие вопросы». вое File Panic Deviate click @ choose me Создать графический интерфейс очень просто: (?) Создайте окно (JFrame) JFrame frame = new JFrame(); Размещение виджетов в окне ® Создайте виджет (кнопку, текстовое поле и JButton button = new JButton("click Создав JFrame, вы можете добавлять в него элементы («виджеты»), В пакете j avax . swing доступно множество ком¬ понентов Swing, которые можно использо¬ вать. Среди самых популярных: JButton, JRadioButton,JCheckBox, JLabel, JList, JScrollPane, JSlider, JTextArea,JTextFieldи JTable. Большинство из них относительно просты в использовании, а некоторые (например, JTable) чуть сложнее. (з) Добавьте в окно виджет frame.getContentPane().add(button), 0 Отобразите его (укажите размер и сделайте frame.setSize(300,300), frame.setvisible(true); 7' < n/Wt U.
Ваш первый графический интерфейс: кнопка в окне import javax.swing.*; public class SimpleGuil { public static void main(String[] args) { cK|U; U JFrame frame = new JFrameO; unu;^ кнопки.лсжил JButton button = new JButton( "нажми меня"); tuMaamt на ней ото сражаться.) frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.getContentPane().add(button); <&кен£Ш0(^П''ШШМП1СЯ 'Ш frame.setSize(300, 300); Лт&шм. 6 окно кнопки 6-. 3 f frame.setVisible(true); "«Kcttад К Локоном,, стссражаюм окно/ (cbm пропустить этот шаг, т нише по ивиаито пли fti шлтнии Koxjaj Давайте посмотрим, что произойдет при запуске. % j ava SimpleGuil нажми меня Ого! Какая здоровая кнопка! Кнопка заполняет все до¬ ступное пространство окна. Позже мы научимся управ¬ лять ее расположением и размером в окне.
Но при нажатии ничего не происходит... Это не совсем так. Когда вы нажимаете кнопку, она меняет свой вид на «нажатый» или «утопленный» (в зависимости от настроек оформления платформы запуска), но всегда каким-нибудь образом информирует, что она нажата. Более актуальный вопрос: «Как сделать так, чтобы кнопка выполня¬ ла нужное действие при нажатии?» Для этого потребуется следующее: ® Метод, который будет вызываться при на¬ жатии кнопки пользователем (то есть про¬ исходящее в результате нажатия кнопки). ® Способ определить, когда нужно вызвать этот метод. Другими словами, способ определить, что пользователь нажал кнопку! Когда пользователь нажимает кнопку, нам нужно об этом уз¬ нать. Нас интересует событие «поль¬ зователь совершает действие с кнопкой». (Не) дурацкие вой росы Вопрос: Я слышал, что Swing больше никто не использует. Ожвеж.- Сейчас есть и другие варианты — например, JavaFX. Но в извечном и нескончаемом споре «Что же лучше для созда¬ ния графических интерфейсов в Java?» победителя пока не видно. Хорошая новость: немного освоив Swing, вы получите фундамент, ко¬ торый пригодится в любом случае. Даже если решите разрабатывать под Android, понимание Swing сделает обучение мобильной раз¬ работке гораздо легче. Вопрос: А кнопка будет вы¬ глядеть как настоящая кнопка Windows, если я запущу програм¬ му на Windows? Ожвеж.- Только если вы сами этого захотите. В стандартной би¬ блиотеке есть несколько «стилей и тем» — классов, задающих внеш¬ ний вид интерфейса. Обычно у вас как минимум два варианта. В при¬ мерах этой книги используются разные стили, включая стандарт¬ ный стиль вашей операционной системы (например, для macOS), стиль OS X Aqua и кроссплатфор- менный стиль Metal. ВопрОС: Разве Aqua еще не ушел на покой? Ожвеж.- Ушел. Но он нам все равно нравится.
Получение пользовательского события Предположим, вы хотите, чтобы текст на кнопке менялся с «нажми меня» на «я нажата», когда пользователь нажмет на нее. Сначала мы можем написать метод, который изменяет текст кнопки (в документации API вы быстренько найдете этот метод). Во-первых, кнопка должна знать, что мы ей небезразличны. ЛЧ За, кнопка, мне нс fe /швно, уте с тссш гуюисиюит. public void changelt() { button.setText("я нажата!"); } А что дальше? Как определить, когда этот метод должен выполняться? Как понять, что пользо¬ ватель нажал кнопку? В Java процесс получения и обработки пользо¬ вательских событий называется обработкой событий. Событий в Java много, и большинство связано с действиями пользователя в графиче¬ ском интерфейсе. Если пользователь нажимает кнопку — это событие. Оно означает: «пользова¬ тель хочет, чтобы произошло действие, связанное с этой кнопкой». Если это кнопка «замедлить темп», значит, пользователь хочет, чтобы музыка воспроизводилась медленнее. Если это кнопка «отправить», значит, он хочет отправить со¬ общение. Самый простой и очевидный вид со¬ бытия — нажатие кнопки, сигнализирующее, что пользователь ожидает выполнения действия. В случае с кнопками обычно неважно, какие промежуточные события происходят, — напри¬ мер, когда кнопка нажимается или отпускается. Важно только одно: «Мне не интересно, как долго пользователь удерживал указатель мыши на кнопке, сколько раз передумал, навел и отвел указатель, прежде чем нажать кнопку. Просто скажи мне, когда он действительно решил на¬ жать и ожидает, что кнопка выполнит свое предназначение! Другими словами, не беспокой¬ те меня, пока пользователь ее не нажмет». (D ЛАА! Лам^(!ап\1ш жнм нажал! Во-вторых, ей нужен способ позвать нас, когда кто-то на нее нажмет — вызвать наш код в ответ на событие. 1. Как дать понять объекту кнопки, что вам не¬ безразличны его события? Что вы вниматель¬ ный слушатель? 2. Как кнопка сможет позвать нас? Предпо¬ ложим, что у вас нет возможности сообщить кнопке название вашего уникального метода (changelt(J). Что еще мы можем использовать, чтобы убедить кнопку в том, что у нас есть кон¬ кретный метод, который она может вызвать при возникновении события? [Подсказка: вспомните про Pet.]
ловить события кнопки Хоти те | реализуйте ЕЩПШЭ интерфейс Интерфейс слушателя — это мост между источником событий (кнопкой) и обработчиком (вашим кодом). Компоненты Swing GUI — источники событий. В терминах Java источник событий — это объект, который может преобразовы¬ вать действия пользователя (щелчок мышью, нажатие клавиши, закрытие окна) в события. И, как почти все в Java, событие пред¬ ставляется в виде объекта. Объекта некоторого класса собы¬ тий. Если просмотреть пакет j ava. awt. event в API, можно увидеть множество классов событий (их легко заметить — в на¬ звании присутствует слово Event). Например, MouseEvent, KeyEvent, WindowEvent, ActionEvent и другие. Источник события (например, кнопка) создает объект события, когда пользователь выполняет значимое действие (например, нажимает кнопку). Большая часть кода, который вы будете писать (в том числе и в этой книге), будет получать события, а не создавать их. Другими словами, большую часть времени вы будете выступать в роли слушателя событий, а не источника. Для каждого типа событий существует соответствующий ин¬ терфейс слушателя. Если вам нужны MouseEvent, реализуйте интерфейс MouseListener. Нужны WindowEvent? Реали¬ зуйте WindowListener. Принцип понятен. И незабывайте правила работы с интерфейсами: чтобы реализовать интерфейс, нужно объявить, что ваш класс его реализует (class Dog implements Pet), а также определить все методы интерфейса. Некоторые интерфейсы содержат несколько методов, поскольку одно и то же событие может иметь разные варианты. Напри¬ мер, если реализовать MouseListener, можно обрабатывать события mousePressed,mouseReleased, mouseMoved и т. д. Каждый из этих событий имеет свой метод в интерфей¬ се, хотя все они принимают MouseEvent. Если реализован MouseListener, метод mousePressed () будетвызван, когда пользователь нажмет кнопку мыши. А когда пользователь ее отпустит, вызовется mouseReleased (). Таким образом, для обработки событий мыши существует один объект события — MouseEvent, но несколько разных методов, которые представ¬ ляют разные виды событий. Когда вы реали¬ зуете интерфейс слушателя, вы да¬ ете кнопке способ вызвать ваш код в ответ на событие. В интерфейсе уже объявлен метод обратного вызова — его кнопка и будет использовать.
Взаимодействие слушателя и источника ■ A^tionListene^ QctionPe rformedCcO6^ Слушатель событий Если ваш класс должен отслеживать ActionEvent кнопки, он должен реализо¬ вать интерфейс ActionListener. Кнопка должна знать, что вам интересны ее события, поэтому вы регистрируетесь с ней, вызывая addActionListener (this) и передавая ссылку на объект ActionListener. В на¬ шем первом примере слушателем событий является ваш код, поэтому передается this, но чаще для обработки событий создают от¬ дельный класс. Кнопке нужен способ вызвать ваш код при возникновении события, поэтому она вызы¬ вает метод, определенный в интерфейсе слу¬ шателя. Как ActionListener вы обязаны реализовать единственный метод интерфей¬ са — actionPerformed ().Компилятор следит за этим. Источник событий Кнопка — источник ActionEvent, поэтому она должна знать, ка¬ кие объекты являются слуша¬ телями. У кнопки есть метод addActionListener (), который позволяет слушателям регистриро¬ ваться. Когда вызывается addActionListener () (потен¬ циальный слушатель вызывает его), кнопка получает переданный объ¬ ект-слушатель и сохраняет его в спи¬ ске. Когда пользователь нажимает кнопку, она «вызывает» событие, вы¬ зывая метод actionPerformed() для каждого слушателя из списка.
Получение ActionEvent кнопки ® Реализуйте интерфейс ActionListener. ф Зарегистрируйтесь в кнопке (сообщите ей, что хотите отслеживать события). @ Определите метод обработки событий (реализуйте метод actionPerformed() из интерфейса ActionListener). import javax.swing.*; inport j ava.awt.event.*, pu'A^«l WU Чин «"фтор imU l, ■ нипиппм пакета cSktiml/sfoWu \Xln public class SimpleGui2 private JButton button; © implements ActionListener tldfcnfvtnt. /UA2ULM »V\mt|ACbtiw. Это ВДИЛ IV 4mo m w П (iS-.A) ActtovOstme*. (mum* public static void main(String[] args) SimpleGui2 gui = new SimpleGui2(); gui .got) ; { { Peeuu тает, . .. , ,^r это (35-.A) «vhowka nqvqium cwJwmuA только т*л\ <atmiitt xcnvofvut ,Ad(u\!Mev\tt.) ««пине; Cimm иж(\Нм public void go() { JFrame frame = new JFrameO; button = new JButton("нажми меня") X —- button.addActionListener(this); /“ f rame . getContentPane () .add (button) ; frame.setDefaultCloseOperation(JFrame.EXIT ON frame.setSize(300, 300); frame.setVisible(true) ; Jlfuv mnufufxUca не W/c г/мфпкскего <#31 Эт «дам. в ай«нсй1Ш.м б’этон мд& спяшГна^*п vh* "гч* - ■чАиЖЗЗЗГ ~~ с ,сг,к с.лиимттмА кислкл. } CLOSE); public void actionPerformed(ActionEvent event) button.setText("я нажата!"); } ект ’ Аыщвас ужт сОь- ГД fumu’ mu mkxmut npSufctuM. <
Слушатели, источники и события Большую часть своей карьеры в Java вы будете не источником событий, а их внимательным слушателем. (Как бы ни хотелось считать себя центром вселенной.) Привыкайте к этой роли. Быть хорошим слушателем — ваша главная задача. (А если делать это искренне, можно улучшить не только код, но и общение с людьми.) Объект Event ХРАНИТ ИНФОРМАЦИЮ о событии
(Не) дурацкие вопросы ЗОИ рОС: Почему я не могу быть источником событий? OwBewi: Можете. Мы лишь сказали, что большую часть времени вы будете не инициатором события, а его получателем (по крайней мере, на ранних этапах своей блестящей карьеры в Java). Большинство со¬ бытий, с которыми вы столкнетесь, создаются клас¬ сами из Java API, и ваша задача — просто слушать их. Но бывают случаи, когда нужно создать собственное событие. Допустим, вы пишете приложение для от¬ слеживания фондового рынка. Можно ввести событие StockMarketEvent, которое будет «выбрасываться», когда программа заметит что-то важное. В таком случае объект StockWatcher становится источником событий — и ему предстоит вести себя так же, как кнопка (или любой другой источник). Что нужно сделать: создать интерфейс слушателя для нового типа события; добавить метод регистрации, напри¬ мер, addStockListener(); хранить список слушателей; при наступлении события создать объект StockEvent (да, еще один класс, который вам придется написать); вызвать метод stockChanged(StockEvent ev) у каждого слушателя из списка. И не забывайте: для каждого типа события должен быть соответствующий интерфейс слушателя. В этом случае — StockListener с методом stockChangedQ. ЗоИрОС: Зачем нужен объект события, который передается в метод обратного вызова? Разве недостаточно самого факта, что событие произо¬ шло? Oi/иве т: В большинстве случаев объект события действительно не особенно важен. Это просто но¬ ситель данных, который передает дополнительную информацию о произошедшем событии. Но ино¬ гда детали имеют значение. Допустим, вызывается mousePressedO — вы знаете, что нажата кнопка мыши. А если вам нужно знать, где именно на экране это произошло? В этом случае объект события сообщает координаты — х и у. Другой пример — регистрация одного слушателя сразу для нескольких объектов. Представьте, что вы создаете экранный калькулятор с десятью цифровыми кноп¬ ками. Все они делают одно и то же, так что нет смысла писать десять отдельных слушателей. Можно зареги¬ стрировать один и тотже слушатель на все кнопки. А когда будет вызван метод обратного вызова, объект события подскажет, какая именно кнопка его сгенери¬ ровала. Это позволяет обрабатывать все десять кнопок одним обработчиком. аточите карандаш Все эти виджеты (объекты пользовательского интерфейса) могут выступать источникомодного или нескольких событий. Соотнесите виджеты с возможными событиями. Один виджет может вы¬ зывать несколько событий, а одно и то же событие может исходить от разных виджетов. Widgets флажок текстовое поле прокручивающийся список кнопка диалоговое окно переключатель пункт меню Event methods windowClosing() actionPerformed() itemStateChanged() mousePressed() keyTyped() mouseExited() focusGainedQ Как определить, яЬля- ется ли объект источ¬ ником событий? Затяните в документацию API. - Окей, а что искать? — Метод, который начинается со слова add, а заканчивается на Listener и принимает ин¬ терфейс слушателя в качестве аргумента. Например: addKeyListener(KeyListener k) значит, класс с этим методом генерирует KeyEvent. Это принятый шаблон имено- Решать вам вания,
Возвращаясь к графике... Теперь, когда мы немного разобрались, как работают события (более подробно эта тема будет рассматриваться позже), давай¬ те вернемся к размещению элементов на экране. Мы потратим несколько минут на изучение интересных способов работы с графикой, а затем снова вернемся к обработке событий. Три способа разместить вещи в графическом интерфейсе: © Размещение виджетов в окне Добавьте кнопки, меню, переключатели и другие элементы. frame.getContentPane().add(myButton); Пакет javax.swing включает более десятка видов виджетов. ® Рисование 20-графики на виджете Используйте объект Graphics для рисования фигур. graphics.fillOval(70,70,100,100); Можно рисовать не только прямоугольники и окружности: API Java2D предлагает множество интересных и продвинутых графических методов. UrKt<frt>,s> 7 ® Размещение JPEG-изображения на виджете Вы можете добавить собственное изображение на виджет. graphics.drawlmage(myPic,10,10,this);
Создание виджета для рисования Весь код, связанный с рисованием, должен находить¬ ся внутри метода paintComponent (). Представьте, что система вызывает этот метод как бы говоря: «Эй, виджет, пора нарисовать себя!» Если вам нужно нари совать, например, круг — соответствующий код должен быть внутри paintComponent (). Когда на экране по¬ является фрейм с панелью для рисования, JVM вызывает paintComponent (), и круг оказывается на месте. Если пользователь свернет окно, JVM запомнит, что при развора¬ чивании изображение нужно восстановить, и снова вызовет paintComponent (). Каждый раз, когда JVM считает, что содержимое ком¬ понента нужно обновить, она вызывает ваш метод paintComponent (). Важно: вы не вызываете paintComponent () напрямую! Его вызывает система, а вы просто переопределяете метод и описываете, что именно нужно нарисовать. Аргумент этого метода — объект Graphics — и есть тот самый «холст», на котором можно рисовать. Он уже готов и передается JVM в момент вызова метода. Самостоятельно создать его нельзя. Но, как вы скоро увидите, можно попросить систему обно¬ вить изображение, вызвав repaint (). Это приведет к тому, что paintComponent () будет вызван снова. import javax.swing, import j ava.awt.*; class MyDrawPanel extends JPanel { j\\ и (cjppi-tt подкласс JPantC: это Guamm. котс- шдаДбж’но решить в окно так же! как плюсом. Uputiu калтситт. Ко в панкам, елигск этю сусрт Kaw ccw:m(?tHtuuv, иастрсшвае. и им. тожет. public void paintComponent(Graphics g.setColor(Color.orange); *4 £ла(?кмм .лшпеа алм. pademu c граоJbuKcii — ' Чсгшиа Это клюквам епогка, tope? iccmohWK) Vvhowcxcawm рисование. f:h\ никешт щ Шжваегпе. ao врытую: это уемшп cucmt- v)\\a как. ом. гЬворшп: «юcm вам сбежим хслот (€fiaphics) -'можете рисовать!» ‘ Штстайпм. ти переменная g.fillRect(20, 50, 100, 100) : шшк/шоиюш/ея маишна. Sn bhu?C ~ какии цветом нити ршсвсти а \ !!!! ' - укашваете, какин Фнгцрц ирср-Л™'^ с Kccfvju ката и и, onhuptiJwiiuiud ое ’ ПйАошние и рашер
Интересные возможности метода paiVitComponentf) Давайте рассмотрим еще несколько возможностей метода paintComponent (). Намного интереснее эксперимен¬ тировать самостоятельно! Попробуйте изменить числа и загляните в документацию API, касающуюся класса Graphics. (Позже мы увидим, что можно делать еще боль¬ ше, выходя за рамки возможностей класса Graphics.) Отображение JPEG-файлов public void paintComponent(Graphics g) { Image image = new Imagelcon("catzilla.jpg”)■getlmage() g.drawlmage(image, 3, 4, this); ^(xissO- :r r**** * nxrrxfcr слг« zS Создание круга случайного цвета на черном фоне I public void paintComponent(Graphics g) g.fillRect(0, 0, this.getKidth(), this.getHeight()>- ЗальлиА»!* (^tnvrO ,vcu <wjk«MAU' ). cm самого .ивого пика, ни n(M tui' Лервьее ава арщмента гааант коораннатье (x, и) верхнего левого еугла — cm- г‘ .. Hmmic.ftHc панели. на ксто/юй <?и тешш, ьшьжиие С. С cmaiatinx: «нагапи его.ивою верхнего игла пансш» — nu ami О пикселей от левого края и О й cm верхние. Сле/шкщие аваf f Пример, если вы исполииоте ЬЫЛЫЫМ « Ш^ШешМЫт сжашИп: .нарисовать ероялеаро. и ток копи]и\ ира на мает век ширкну и век ы оту нашли» Random random* new Random (); Ратшема ноги, i/лова eu. 41ath.raiuhn(), шmoufu, тая. как int red = random. nextlnt(256) ; L 1ЩклкшшСшлттки Jam, можно всспшеасвсшеАя pwa.uUC. int green = random, nextlnt (256); \ Random. У этой > к eaco/a есть литер nextJnw, Kcnvfwunpu- int blue = random, nextlnt (256); ' нпмаетлшкоиешАыиернакнш и ворвраш/ют с.шгайна пило cm 0 (вкекгитиеьш) мгикамннсго такним (не вк. upturn, unci Ranpiuup. ntxlJnt(2.56; варитрнлпния cm О cjo 2)> green^ blue), ^ Г- тиш мер/пи нДт, пеЬчаашт три ч/.тх тела, ссствтсптршеие 1 компонентам R£pB — красной.ро й¬ ной и синей. Каждое м/ них qa ежно Color randomColor = new Color(red, g.setColor(randomColor); g.fillOval(70, 70, 100, 100); £ИУСткч' <)>n •«»»!«««■ нарисовать, фигцрц. натнанап mown 70 пикселей cm левого края 1C\C пм'чУЪ'к<верхнего. .Залайте ииЦшни ел нм/гсмси и высоте/100 пике» лей ’ ' сгинь вс/иапа?сне cm (0р 2л). | ершпььрI
За хорошей графикой вам к объекту Craphics2D Аргумент метода paintComponent () объявлен как тип Graphics (j ava. awt. Graphics). public void paintComponent(Graphics g) { } Таким образом, параметр g — это объект Graphics. Это оз¬ начает, что он может быть подклассом Graphics (благодаря полиморфизму). На самом деле так и есть. Объект, на который ссылается параметр д, — фактиче¬ ски экземпляр класса Graphics2D. Почему это важно? Потому что, имея ссылку на Graphics2D, вы можете делать то, что невозможно с Graphics. Объект Graphics2D предоставляет больше возможностей,чем Graphics, и фактически скрывается за ссылкой Graphics. Помните о полиморфизме: компилятор определяет, какие методы можно вызывать, исходя из типа ссылки, а не из типа самого объекта. Например, если у вас есть объект Dog, на который ссылается переменная типа Animal: Animal а = new Dog(); Вы не МОЖЕТЕ написать: a.bark(); Вы-то знаете, что за этой ссылкой скрывается объект типа Dog, но компилятор смотрит на переменную а, видит, что ее тип — Animal, и не находит в классе Animal метода bark(). Тем не менее вы все еще можете привести объект обратно к типу Dog, которым он на самом деле и является. Для этого напишите: Dog d = (Dog) а; d. bark () ; Подводя резюме по объекту Graphics: Если нужно использовать метод из класса Graphics2D, вы не можете напрямую вызывать его через параметр g в paintComponent (). Однако можно привести его к Graphics2D, создав новую переменную: Graphics2D g2d = (Graphics2D) g; Методы, которые можно вызвать через ссылку Graphics: drawlmage() drawLine() drawPolygon drawRect() drawOval() fillRect() fillRoundRectO setColor() Чтобы привести объект Graphics к ссылке Graphics2D: Graphics2D g2d = (Graphics2D) g; Методы, которые можно вызвать через ссылку Graphics2D: fill3DRect() draw3DRect() rotate() scale() shear)) transform() setRenderingHints() (Это нс пхшшс стекиммшгов; фи ?ш ишдтс (?аск.1^шнтаи,ш1\РЗ.)1
Зачем ограничиваться сплошным цветом, если есть красивый градиент? public void paintComponent(Graphics Нлсал^ Graphics2D g2d = (Graphics2D) g; 1^- /In tfH&mMtwк fyaphics&b, mufaЫтилшта KCHMfmkemu (Рэшиtuam, Hctwymrm'ffipv «(jmfhics GradientPaint gradient = new GradientPaint(70, 70, Color.blue, 150, 150, Color orange)• Г t.A g2d.setPaint(gradient) g2d.fillOval(70, 70, 100, 100); /j 7(1 vr_—^1^Тче>пУ'"'ы,п^' i»t); ) »nh ,,f"i "'*« »,c Cfkti 40, »i public void paintComponent(Graphics g) ( Graphics2D g2d = (Graphics2D) g; Random random = new Random(); int red = random.nextlnt(256); int green = random.nextlnt(256); int blue = random.nextlnt(256) ; Color startColor = new Color(red, green, blue); red = random.nextlnt(256) ; green = random.nextlnt(256) ; blue = random.nextlnt(256) ; Color endColor = new Color(red, green, blue); GradientPaint gradient = new GradientPaint(70, 70, startColor, l5Qf g2d.setPaint(gradient); ' endColor); g2d.fillOval (70, 70, 100, 100); -W mcm Ж1 )V|VIUH|\, tmo » ШМШ. IWUbKC HflWibHCVU Лснпт^фпагташята nuntfu Омоются (HfciiMiu o^ajtw. .Tluv|vuijwnu mmU )
Без лишних «лов События ■ Чтобы создать графический интерфейс (GUI), начните с окна, обычно это JFrame: JFrame frame = new JFrame(); ■ Вы можете добавлять виджеты (кнопки, текстовые поля и т. д.) к JFrame, используя: frame.getContentPane().add(button); ■ В отличие от большинства других компонентов, JFrame не позволяет добавлять элементы напрямую, поэтому их необходимо добавлять на панель содержи¬ мого JFrame. ■ Чтобы окно (JFrame) отобразилось, нужно указать его размер и сделать видимым: frame.setSize(300, 300); frame.setVisible(true); ■ Чтобы отслеживать нажатия кнопок (или другие действия пользователя), необходимо прослушивать события интерфейса. ■ Для прослушивания событий нужно зарегистрировать объект в качестве слушателя у источника событий. Источник событий — это элемент (кнопка, флажок и т. д.), который «генерирует» событие при взаимодей¬ ствии с пользователем. ■ Интерфейс слушателя предоставляет источнику со¬ бытий способ вызвать ваш метод, поскольку интер¬ фейс определяет метод(ы), которые вызываются при возникновении события. ■ Чтобы зарегистрироваться на получение со¬ бытий от источника, вызовите его метод ре¬ гистрации. Такие методы всегда имеют форму add<EventType>Listener. Например, чтобы подписаться на ActionEvent кнопки, вызовите: button.addActionListener(this); ■ Реализуйте интерфейс слушателя, добавив в класс все его методы для обработки событий. Код обработки события помещается в метод обратного вызова. Для ActionEvent этот метод выглядит так: public void actionPerformed(ActionEvent event) { button.setText("ты нажал меня!"); } ■ Объект события, переданный в метод обработчи ка, содержит информацию о событии, включая его источник. Графика ■ Вы можете рисовать 2D-графику прямо на виджете. • Вы можете отображать изображения в формате .gif или . jpeg непосред¬ ственно на виджете. ■ Чтобы рисовать собственную графику (в том числе . gif и . jpeg), создайте подкласс JPanel и переопределите метод paintComponent (). * Метод paintComponent () вызывается системой графического интерфейса. ВЫ НЕ ВЫЗЫВАЕТЕ ЕГО САМОСТОЯТЕЛЬ¬ НО. Аргументом метода служит объект Graphics, который предоставляет по¬ верхность для рисования, отображаемую на экране. Этот объект нельзя создать вручную. ■ Типичные методы, вызываемые у объекта Graphics (параметра paintComponent): g.setColor(Color.blue); g.fillRect (20, 50, 100, 120); ■ Чтобы отобразить изображение . j pg, создайте объект Image: Image image = new Image Icon("catzilia.jpg") . get Image() ; и отрисуйте его с помощью: g.drawImage(image, 3, 4, this); ■ Объект, переданный методу paintComponent() как Graphics,— на самом деле экземпляр класса Graphics 2D. Класс Graphics 2D предоставляет множество дополнитель ных методов, таких как fill3DRect (), draw3DRect (),rotate (),scale(), shear(), transform(). ■ Чтобы вызвать методы Graphics 2D, преобразуйте параметр из Graphics в Graphics2D: Graphics2D g2d = (Graphics2D) g;
Мы умеем получать события Мы умеем рисовать графику Но можем ли мы рисовать графику в ответ на событие? Давайте свяжем событие с изменением на нашей панели рисования. Мы сделаем так, чтобы цвет круга менялся каждый раз при нажатии кнопки. Вот как будет работать программа: Запуск приложения Создается окно с двумя компонента¬ ми — вашей панелью для рисования и кнопкой. Затем создается слушатель, который регистрируется через кнопку. После этого окно отображается и ждет, пока пользователь нажмет на кнопку. Пользователь нажимает кнопку. Кнопка создает объект события и вызывает у зарегистрированного слушателя метод обработки события. Обработчик события вызывает repaint() у панели. Система в ответ вызывает метод paintComponent(). • • • Change colors Вуаля! Панель перерисовывается, и круг появляется заново — но уже с новым, случайным цветом, потому что paintComponent() снова срабатывает и заливает его по-новому.
)nu: r£ute »\|шви.шши (д 7aiwjt всего — a ншбрииим) cncccxT аостлтид кслтонектов в спит ддптяию панель «срхжиис н фшшд. fieaqa ишЬмваитг. СК \КЖ cMiumr сц.мш (nmi m.uiuuH Ki.wmiunm. fcm же вьивяпи дитм add() c схмидл. дридинтс, и mu m ca ими вд шант, кстсргс .ирги iu«miiu), кодтонтт nc цци- таник скажется в и^ктриьнса Компоновка интерфейсов: размещение нескольких виджетов в окне Мы глубже рассмотрим компоновку интерфейсов в следующей главе, а пока проведем небольшой обзор. По умолчанию у фрейма есть пять областей, в которые можно добавлять элементы. В каж¬ дую область фрейма можно добавить только один элемент, но не паникуйте! Этот элемент может быть панелью, содержащей три других элемента, в том числе панель, на которой находятся еще два элемента, и... вы поняли суть. На самом деле мы немного «жуль¬ ничали», когда добавляли кнопку во фрейм вот так: frame.getContentPane().add(button); Нл . ^ f1 Ш n^< *\f\a add() c ulu" Ут f $ - frame.getContentPane().add(BorderLayout.CENTER, button) ■ Ux Спжваш. mmq ШОсавилш afi C >ЫшиwnuunummtO. ZC№U -.^лтаишп, ктаиш нижт уосашт (s эту схйаапь. ’ #^Заточите карандаш Руководствуясь иллюстрациями на преды¬ дущей странице, напишите код, который добавляет кнопку и панель во фрейм. Решать вам
Изменение цвета круга при каждом нажатии кнопки import javax.swing.*; import java.awt.*; import java.awt.event.*; public class SimpleGui3 implements ActionListener { private JFramie frame; public static void main(String[] args) { SimpleGui3 gui = new SimpleGui3(); gui.go(); ) public void go() { framie = new JFrameO; frame.setDefaultCloseOperation(JFrame.EXIT ON CLOSE); c^aanu cfftiub, ' ' * Khovvkii fruic»vcv^o- пПТЮосла- (ww ‘|1U аша. JButton button = new JButton("Сменить цвета"); _ рл button.addActionListener (this) ; Z 1 , V f fuv MyDrawPanel drawPanel = new MyDrawPane1(); j JD('vttM q&t frame . getContentPane () . add (BorderLayout. SOUTH, button); К та (кнопки И Папби frame. getContentPane () . add (BorderLayout. CENTER, drawPanel); * си.я Hucc&IHUm) frame.setSize<300, 300); OfCtJcmU t№iLua/ frame.setVisible(true); } public void actionPerformed(ActionEvent event) { frame.repaint (); class MyDrawPanel extends JPanel ( public void paintComponent(Graphics g) // Код для окрашивания круга случайным // См. страницу 365 } цветом MmmjwntCcrycmnW пат щ /июшия Ьсипвается кажтщ №• кш!а naJu.ua- ет кнопки. J
Попробуем сделать две кнопки Кнопка в нижней части окна будет функционировать так же, как пре¬ жде, вызывая метод repaint () фрейма. Вторая кнопка (которую мы разместим в левой части окна) будет изменять текст на метке. (Мет¬ ка — это просто текст на экране.) Итак, теперь нам нужны ЧЕТЫРЕ виджета... .. .и два события Вот как? А разве так можно? Как получить два события, если у нас только один метод actionРегformed()? Эти кнопка н/ меняет, ни кс т (?. и & й Wm и.
Обработка событий от нескольких кнопок с разными действиями 0 Вариант первый Реализовать два метода actionPerformedO class MyGui implements ActionListener { // здесь куча кода, а затем: public void actionPerformed(ActionEvent event) frame.repaint(); ) public void actionPerformed(ActionEvent event) label.setText("Больно же!"); > t Недостаток: Вы не можете так поступить! Нельзя реализовать один и тот же метод дважды в классе — код просто не скомпилируется. И даже если бы это было возможно, откуда источ¬ нику события знать, какой из двух методов вызывать? (5) Вариант второй Зарегистрировать одного и того же слушателя для обеих кнопок class MyGui implements ActionListener { // здесь объявляются разные переменные экземпляра public void go() { // создаем интерфейс colorButton = new JButton(); labelButton = new JButton(); colorButton. addActionLi s tener (this) ; 4—/ Ршшпьцfujtu c. i labelButton . addActionListener (this) ; СШЬХ'КНСПСК // остальная часть интерфейса... } public void actionPerformed(ActionEvent event) { if (event.getSource() == colorButton) frame.repaint(); } else { label.setText("Больно же!"); > } > Недостаток: такой подход действительно работает, но в большинстве случаев он проти¬ воречит принципам объектно-ориентированного программирования. Один обработчик выполняющий разные действия для разных источников событий, означает, что один метод берет на себя слишком много ответственности. Если в будущем потребуется изменить обработку для одного из источников, придется вносить изменения в общий метод, а это может затронуть и другие случаи. Иногда это до¬ пустимо, но чаще такой подход усложняет сопровождение и снижает гибкость кода. КЙКО* кис*»*
Обработка событий от нескольких кнопок с разными действиями 0 Вариант третий Создать два отдельных класса, реализующих ActionListener class MyGui { private JFrame frame; private JLabel label; void gui() { // код для создания двух слушателей и регистрации // одного для кнопки цвета, другого — для кнопки метки } } class ColorButtonListener implements ActionListener { public void actionPerformed(ActionEvent event) { frame.repaint(); j ^ fit civujOHV'iim! У масса кет cctикк № пс^мсяш^ю pam массаМ^ш class LabelButtonListener implements ActionListener { public void actionPerformed(ActionEvent event) { label.setText("Больно же!"); } нтустжчна пленку* me. Недостаток: эти классы не имеют доступа к переменным, с которыми должны работать — например, к frame и label. Эту проблему можно решить, передав каждо¬ му слушателю ссылку на основной GUI-класс. Тогда в методе actionPerformedO слуша¬ тель сможет использовать эту ссылку, чтобы получить доступ к нужным переменным. Однако такой подход нарушает инкапсуляцию. Чтобы слушатель мог обращаться к компонентам интерфейса, придется добавлять методы-геттеры, например: getFrameO, getLabelO и т. д. Кроме того, для передачи ссылки на GUI понадобится добавить кон¬ структор в каждый слушатель. Все это создает цепочку зависимостей, которая делает код более громоздким и запу¬ танным. Наверняка есть способ получше!
Как было бы замечательно, если \ бы можно было создать два разных клас- * са слушателей, и при этом они имели бы доступ к переменным экземпляра главного GUI-класса — как будто они часть этого класса... Тогда было бы все: и чистый код, и удобный доступ. Да, это был бы иде- fc альный вариант. Но, увы... это всего ^ лишь фантазия... )
Вложенный класс спешит на помощь! Вы можете вложить один класс в другой. Это просто. Главное — убедитесь, что определение вложенного класса располагается внутри фигурных скобок внешнего класса. Простой вложенный класс: class MyOuterClass { class MylnnerClass { void go() { > ) } (touwWw tcuuta. Вложенный класс получает особое право использовать элементы внешнего класса, в том числе его приватные члены. Вложенный класс может использовать эти приватные переменные и методы внешнего класса, как если бы эти переменные и члены были определены во вложенном классе. Вот в чем удобство вложенных классов: они имеют большинство преимуществ обычных классов, но с особыми правами доступа. Вложенный класс, использующий переменную внешнего класса class MyOuterClass { private int x; class MylnnerClass { void go() { } // конец внутреннего класса OcWuu/i ti merькх так. елМао она — преданная йжштнсгс класса. Вложенный класс может использовать все методы и переменные внешнего класса, даже приватные. Вложенный класс имеет доступ к этим переменным и методам, как если бы они были объявлены внутри него. } // конец внешнего класса
Экземпляры вложенного и внешнего классов должны быть связаны* Помните, когда мы говорим о том, что вложенный класс получает доступ к переменным внешнего класса, на самом деле речь идет о том, что экземпляр вложенного класса обращается к переменным конкретного экземпляра внешнего класса. Но какого экземпляра? Может ли любой произвольный экземпляр вложенного класса получить доступ к методам и переменным любого экземпляра внешнего класса? Нет! v Вложенный объект должен быть внешнего класса в куче. связан с конкретным объектом Вложенный объект имеет особую связь с внешним объектом. Связь с еаишм вложенным классом Новый бестселлер ■Уинги «Где мои си* ф Создайте экзем пляр внешнего класса. ® После этого создайте объект вложенного класса через объект внешнего класса. Q) Теперь внешний и вложен¬ ный объекты тесно связаны. юключение для крайне специфическпг го внутри статическпгпог° СлУчая л ,в™, ^ ^Т*“* ИИ. „уСГ”" ' ИкогАа не Вст ^ не 6УДем рассматривать РВТЯТСЯ Ввятой Эта аба шлкта 6 ките iua кт cJccvw с&ыt. 15-Ложсннма ifliKm.tu.mm трыдшнж AuauuM к шел (uKOOf^m).
Создание экземпляра внутреннего класса Если вы создаете экземпляр вложенного класса внутри внешнего, то эк¬ земпляром, с которым будет связан вложенный объект, станет тот самый внешний объект, в контексте которого создается вложенный. Например, если вложенный объект создается внутри метода, то он будет связан с экземпля¬ ром, чей метод в данный момент выполняется. Код внешнего класса может создавать экземпляры своих вложенных классов также, как и любых других классов... new Mylnner (). class MyOuter { private int x; Mylnner inner = new Mylnner (); Co?fUju. эклмпл public void doStuff() { inner.go(); j < /W'Mfi/t.H ли muj & бшнш класа. Ммннсгв Kiac'?l MyOuter class Mylnner void go() { Я ж 42; } «Г } // конец внутреннего класса } // конец внешнего класса Лета (Ыжитас масса uouuьт hcWhhw экгшп-Vfa х т —класса так, сщт сил npu.t(a‘f «• (Yliu.uy (MCWUUIMUJ К ill с с tf г- Заметки на молях Вы можете создать экземпляр вложенного класса из кода, находящегося вне внешнего класса, но для этого нужен особый синтаксис. Скорее всего, вам никогда не придется делать это на практике, но если интересно — вот как это работает: MyOuter Mylnner class Foo { public static void main (String[] args) { MyOuter outerObj = new MyOuter(); MyOuter.Mylnner innerObj = outerObj.new Mylnner(); } }
Теперь наш код для двух кнопок будет работать Так сеноткммсьШ нс icm ActictdLutuna. public class TwoButtons private JFrame frame; private JLabel label; public static void main(String[] args) { TwoButtons gui = new TwoButtons(); gui.go() ; ) public void go() { frame = new JFrame(); frame.setDefaultCloseOperation(JFrame.EXIT ON CLO! JButton labelButton = new JButton("Изменить метку") labelButton.addActionListener(new LabelListener())> JButton colorButton = new JButton("Изменить круг"); colorButton.addActionListener(new ColorListener()); label = new JLabel("Метка"); MyDrawPanel drawPanel = new MyDrawPanel(); ft mow ntlmtam (this) d.mtnca fyautvfmia,uh cMfrni- mta кнопка, неким класса сицшжхы.*. frame.getContentPane().add(BorderLayout.SOUTH, colorButton); frame.getContentPane().add(BorderLayout.CENTER, drawPanel); frame.getContentPane().add(BorderLayout.EAST, labelButton); frame.getContentPane().add(BorderLayout.WEST, label); Объект TwoButtons frame.setSize(500, 400); frame.setvisible(true); Объект LabelListener } Объект ColorListener K.u«« class LabelListener ^ ""-.laments Actloftbfstener { public void actionPerformed(ActionEvent event) label.setText("Ай!"); } ^испиши класс mum c ataycm&i- лтши.лшпки class ColorListener implements ActionListener { public void actionPerformed(ActionEvent event) frame.repaint(); j /I «.y<t H/itiM класс мажет испсшас&ти } пеМшнную экл.лии.sthafxaпи, ис или.я ятей ссылки, на и,сект тешнсгс класса. {
И: Почему вложенные классы так важны? ВК: С чего бы начать? Мы даем вам возможность реализовать один и тот же интерфейс несколько раз внутри одного класса. В обычном классе Java такое невозможно. Но с помощью вложенных классов можно: каждый вложенный класс может реализо¬ вать интерфейс по-своему. Это значит, что у вас могут быть разные реализации одного и того же метода интерфейса. И: А зачем вообще реализовывать один и тот же метод несколько раз? ВК: Вспомните обработку событий в GUI. Если вы хотите, чтобы три кнопки выполняли разные действия при нажатии, просто создайте три вложен¬ ных класса. Каждый реализует ActionListener и, соответственно, свой вариант метода actionРегformed(). И: Получается, обработка событий — единственная причина использовать вложенные классы? ВК: Нет, конечно. Это просто самый очевидный пример. Вложенные классы полезны каждый раз, когда вам нужен отдельный класс, но вы хотите, чтобы он при этом был частью другого класса. В та¬ ких случаях вложенный класс — лучшее, а иногда и единственно возможное решение. И: Я все еще не до конца понимаю. Если вы хотите, чтобы вложенный класс вел себя как часть внеш¬ него, почему просто не написать этот код прямо во внешнем классе? ВК: Я уже привел один аргумент — возможность создавать несколько реализаций одного интерфейса. Но даже если речь не об интерфейсах, вам все равно могут понадобиться два отдельных класса, потому что они представляют разные сущности. Это просто хорошая объектно-ориентированная практика. И: Подождите-ка. Разве цель ОО-дизайна не в по¬ вторном использовании и упрощении поддержки? Если есть два отдельных класса, их можно моди¬ фицировать и использовать независимо. А с вло¬ женным классом все равно выходит один большой Jaиа прикрас Интервью недели: Экземпляр вложенного класса (ВК) класс, который нельзя использовать несколько раз. Вложенный класс ведь не особо пригоден для повторного использования. Я даже слышал, что их называют «бесполезными — бесполезными снова и снова». ВК: Да, правда, что вложенные классы обычно не предназначены для повторного использования. Они тесно связаны с переменными и методами внешнего класса. Но... И: .. .что только подтверждает мою мысль! Если их нельзя использовать несколько раз — зачем вообще делать из них отдельный класс? Ну кроме того слу¬ чая с интерфейсами, который звучит как обходной путь. ВК: Все дело bIS-Аи полиморфизме. И: Хорошо. И при чем тут это? ВК: Потому что внешние и вложенные классы могут проходить разные проверки IS-А. Возьмем, напри¬ мер, слушателей в GUI. Какой тип аргумента при¬ нимает метод регистрации слушателя кнопки? Что указано в API? И: Он принимает объект, реализующий интерфейс слушателя, в данном случае ActionListener. Да, это понятно. Что вы имеете в виду? ВК: Я говорю том, что метод принимает один кон¬ кретный тип — что-то, проходящее проверку IS-А для ActionListener. Но вот что, если вам нужно, чтобы объект соответствовал не только интерфейсу, но и классу? Причем — классу, который не находит¬ ся в той же иерархии? И: Ну... тогда ваш класс должен просто расширить нужный вам класс. Разве не в этом суть наследова¬ ния? Если В — подкласс А, то везде, где ожидается А, можно передать В. Типа «передай собаку, если нужно животное». ВК: Именно! И вот теперь представьте ситуацию, где вам нужно соответствовать двум разным классам. Классам, которые никак не связаны друг с другом в иерархии наследования. Что тогда?
И: Ах, ну вы просто... хм. Кажется, я начинаю по¬ нимать. Интерфейсов можно реализовать сколько угодно, а вот расширить — только один класс. То есть, когда речь идет о типах классов, вы можете быть только одним типом IS-А. ВК: Точно! Вы не можете быть и собакой, и кнопкой одновременно. Но если вы собака, которой иногда нужно быть кнопкой (например, чтобы передать себя в метод, ожидающий кнопку), то вы може те создать вложенный класс, который расширяет Button и действует от вашего имени. Так собака остается собакой, а кнопкой становится ее внутрен¬ ний помощник. И вместо х . takeButton (this) собака говорит: х . takeButton (new МуInnerButton()). И: Можете привести пример попроще? ВК: Помните панель рисования, которую мы делали? Мы создавали подкласс JPanel, чтобы рисовать на нем. Сейчас это отдельный класс — и это нормаль¬ но, потому что ему не нужен доступ к переменным основного GUI. Но представьте, что нужен! Напри¬ мер, если панель рисует анимацию, координаты которой зависят от действий пользователя в другом месте интерфейса. Тогда мы можем сделать эту панель вложенным классом. Она все еще будет рас¬ ширять JPanel, но получит доступ к переменным внешнего класса. А внешний класс при этом может продолжать наследовать что-то свое. И: Ага, понял! И вообще, такая панель не очень-то многократно используемая: она же делает что-то конкретное, что нужно именно этому GUI. ВК: Вот именно! Вы все поняли. И: Ладно. Давайте тогда перейдем к самому интерес¬ ному — к отношениям между вложенным классом и внешним объектом. ВК: Ага, начинается. Неужели без пикантных под¬ робностей про «кто с кем связан» нельзя обойтись даже в теме полиморфизма? И: Эй, вы не представляете, сколько люди готовы заплатить за хорошие таблоидные сплетни. Так вот: кто-то создает вас, и вы сразу же связываетесь с кон¬ кретным экземпляром внешнего класса, правильно? ВК: Абсолютно верно. И: А что насчет внешнего объекта? Он может иметь связи с другими вложенными объектами? ВК: Вот к чему вы все это вели! Да, конечно. Мой так называемый «партнер» может иметь столько вложенных объектов, сколько захочет. И: Типа... серийная моногамия? Или все сразу? ВК: Все сразу. Сколько угодно. И: Ну, в этом есть логика. Вы только что говорили, что вложенные классы позволяют делать несколь¬ ко реализаций одного интерфейса. Так что если у внешнего класса есть три кнопки, логично, что ему понадобятся три разных вложенных класса — и, со¬ ответственно, три разных объекта, — чтобы обраба¬ тывать события. ВК: Все верно. И: Последний вопрос. Говорят, когда появились лямбда-выражения, вы чуть было не остались без работы? ВК: Ой, да, это было болезненно! Ладно, признаюсь честно: есть куча случаев, где лямбда-выражения действительно читаются проще и занимают меньше места. Но мы, вложенные классы, все еще живы. Нас до сих пор полно в старом коде. И, между прочим, лямбда-выражения тоже не идеальны; в некоторых случаях мы, старички, все еще выигрываем.
А можно еще раз глянуть на тот код с вложенным классом? Он был пару страниц назад... Кажется, он не такой уж простой. Понять его с первого раза было тяжеловато. public void go() { frame = new JFrameO; frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE) JButton labelButton = new JButton("Изменить метку"); ^ labelButton.addActionListener(new LabelListener()); И снова лямбда-выражения спешат на помощь Он не ошибается! Один из способов интерпретировать две выделенные строки кода выгля¬ дит так; JButton colorButton = new JButton("Изменить круг"); colorButton.addActionListener(new ColorListener ()); label = new JLabel("Метка"); MyDrawPanel drawPanel = new MyDrawPanel(); // код для добавления виджетов, здесь frame.setSize (500, 400); frame.setVisible(true); class LabelListener implements ActionListener { «Когда Actio nListener public void actionPerformed(ActionEvent event) ( для labelButton получает ^ IMM. setText I "Bt £1V} < событие, вызвать метод * setText("Aii!");» He только эти две идеи раз¬ делены в коде, но и вложенный класс занимает ПЯТЬ строк кода под вызов метода setText. И, конечно, все, что мы говори¬ ли о коде для labelButton, справедливо и для кода для colorButton. class ColorListener implements ActionListener { public void actionPerformed(ActionEvent event) { frame.repaint(); } } Помните, несколько стра¬ ниц назад мы говорили, что для реализации интерфейса ActionListener нужно предоставить код для его метода actionPerformed? О чем это говорит?
ActionListener — это функциональный интерфейс Помните, что лямбда-выражение предоставляет реализацию для единственного аб¬ страктного метода функционального интерфейса. Поскольку ActionListener — это функциональный интерфейс, вы можете заменить вложенные классы, показанные на предыдущей странице, на лямбда-выражения. public void go() { frame = new JFrame(); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); JButton labelButton = new JButton("Изменить метку"); labelButton.addActionListener(event -> label.setText("АЙ!")); JButton colorButton = new JButton("Изменить круг"); colorButton.addActionListener(event -> frame.repaint()); label = new JLabel("Метка"); MyDrawPanel drawPanel = new MyDrawPanel(); Эпш cfda втжминых (fymiutHitviKUja — . Ы UCljU-Йуш/fHIW, катрт ml иснлтп Ошжшнт Кланси. 11 код для добавления виджетов, здесь frame.setSize(500, 400); frame.setvisible(true); fiui кш (itamHHtn классов!нею!Он с£шш hi нужен.! л ока тка! Лямбда-выражения — понятнее и лаконичнее Возможно, пока это кажется не совсем очевидным, но как только вы при¬ выкнете читать лямбда-выражения, вы почти наверняка согласитесь — с ними код становится понятнее.
Использование внутреннего класса для анимации Мы уже видели, чем внутренние классы удобны для обработчиков событий: они позволяют реализовать один и тот же метод обработки событий несколько раз. А теперь давайте посмотрим, насколько полезным может быть внутренний класс, когда он выступает в роли подкласса чего-либо, что внешний класс не расширяет. Другими словами, когда внешний и внутренний классы принадлежат к разным иерархиям наследования! Наша цель — создать простую анимацию, в которой круг перемещается по экрану из верхнего левого угла в нижний правый. (Ришт Как устроена простейшая анимация Нарисуйте объект в указанных координатах х и у. g.fillOval(20,50,100,100); Т. ■ V- 20 пикселей сжОа, 50 пикселей c&jiuaj Перерисуйте объект в д ру ги хкоо рд и нотах х и у. g.fillOval(25,55,100,100); ъ 25 пикселей сле&, 55 пикселей слк£а (afiatm (?му И оИ (5) Повторяйте предыдущий шаг, изменяя значения х и у, все время, пока должна продолжаться анимация. (Не) дурацкие вопросы ВомрОС: Зачем изучать анима¬ цию прямо сейчас? Сомневаюсь, что я вообще когда-нибудь буду делать игры. Омвем. Возможно, игры вы и не будете писать. Но вполне может случиться, что вам понадобится создать симуляцию — что-то, где со временем происходят изменения, отображающие результат процесса. Или, например, инструмент визуа¬ лизации: графика, показывающая, сколько памяти использует про¬ грамма, или объем трафика, прохо¬ дящего через сервер балансировки нагрузки. То есть любая ситуация, где нужно преобразовать постоянно меняющи¬ еся числовые данные во что-то, на что можно взглянуть и сделать вы¬ вод. Звучит уже не как игра, а вполне по-взрослому, правда? Хотя... это было официальное объ¬ яснение. Если честно, мы просто используем анимацию какудобный способ показать еще одно классное применение внутренних классов. (Ну и потому что нам просто нравит¬ ся анимация.)
На самом деле нам нужно нечто вроде... class MyDrawPanel extends JPanel { public void paintComponent(Graphics g) { g.setColor(Color.orange); g.fillOval (x, y, 100, 100); (yrfuimamc ftuuumiu! JUnuxj (Jmt( on^cnmt внллвси тг.ч Ыетшст. нс Kimw (?«- п л-»/' г-,1 ur^mciimt. UMCL аточите карандаш Ответы на стр. S30 Но откуда мы получаем новые координаты х и у? И кто вызывает метод repaintO? Попробуйте придумать простое решение, как анимировать мяч, который дви¬ жется из верхнего левого угла панели рисования в нижний правый. Подсказка: сделайте панель рисования вложенным классом. Так вы упростите доступ к переменным, управляющим координатами мяча. Не используйте цикл повторения внутри метода paintComponent(). Запишите свои идеи (или код):
Полный код простой анимации import javax.swing.*; import j ava.awt.*; import java.util.concurrent.TimeUnit; public class SimpleAnimation private int xPos = 70 private int yPos =70 (иамлиЛ JjLj OCHoftuXU K. K((rt (f CL\J K«|v|tuuun a и If KfujW public static void main(String[] args) ( SimpleAnimation gui = new SimpleAnimation()• gui.go(); } public void go() { JFrame frame = new JFrame(); frame.setDefaultCloseOperation(JFrame.EXIT On CLOSE)' MyDrawPanel drawPanel = new MyDrawPanel(); frame.getContentPane().add(drawPanel); frame.setSize(300, 300); frame.setVisible(true); ft 0 «ее 11V Uj(. (\|U for (int i xPos++; yPos++; ринита x и у. drawPanel.repaint(); < 130; i++) { Лс«т/шс<ПМ)М. Гото¬ во; i < 130; i++) <x~ try ( TimeUnit.MILLISECONDS.sleep(50) } catch (Exception e) { e.printStackTrace(); ) ) ’^/A. м turn Kfuft. f/tumpc, (J fill etc уышхн ш ЖЩ перерисовками. — cjmtamifM г.'.шшкчм wu нс i/аштс лишпш , R: IH 9HU yfiftrum нштшямне -. мм псарсКш ivu Кран этап .«с'иинж в tMttit 17 ' class MyDrawPanel extends JPanel { public void paintConponent(Graphics g) ( lu\C|\b g. setColor (Color. green) ; (LvcOKCttVVt'44 g.fillOval (xPos, yPos, 40, 40); ) f'(cna,uAK. u питаяняс umtfiM- } tutu. кЦщинатаx и и внешнего тасса. 7 ■ класс. )
Это сработало? Вы ожидали, что анимация будет плавной, но не увидели этого? Что мы сделали не так? В MeTOflpaintComponent () закралась небольшая ошибка. Нам нужно стереть то, что уже было нарисовано! Иначе мы увидим след. Чтобы справиться с проблемой, нужно заливать всю панель фоновым цветом перед рисованием круга. Код ниже добавляет две строки в начале метода: одна — что¬ бы выбрать белый цвет (фоновый цвет панели рисова¬ ния), а другая заливает весь прямоугольник панели этим цветом. Проще говоря, согласно коду: «Залей цветной прямоугольник, начиная с координат хи у, равных О (О пикселей от левого края и 0 пикселей от верхнего края), и сделай его такой же ширины и высоты, как у панели». Си см. Он не j<sma им... см /ишшлисм. public void paintComponent(Graphics g) { g.setColor(Color.white); g.fillRect(0, 0, this .getwidth () , this .getHeight ()) ! T 'V g.setColor(Color.green); \ g.fillOval(x, у, 40, 40> , „ ) Ciни cm KAacmJpLeC Решать вам — Заточите карандаш (не обязательно, просто отвлечься) Какие изменения нужно внести в координаты х и у, чтобы создать анимации, показанные ниже? (Предположим, что в первом примере движение происходит с шагом в 3 пикселя.) • • Старт Финиш • • Старт Финиш • • X Л Y_d X Y X Y • • Старт Финиш • • Старт Финиш • • X Y X Y X Y Старт Финиш Старт Финиш
Готовим код ■* шаг второй шаг Tfwi tmuu шаг iJPi 4an@ehnmk шаг ДаваГгге создадим музыкальный видеоклип. Мы будем использовать случайную графику сгенерированную в Java, и синхронизируем ее с музыкальным ритмом. В процессе мы зарегистрируем (и будем отслеживать) новый тип событий — на этот раз не связанных с графическим интерфейсом. Эти события будет инициировать сама музыка. Эта гость нсос^апильнл. Hc.ua вссжг приклепаем вальсе tyicumu, петому «тс уверен м: сна окажется шиитке. l{, fine, дне йумежт <?а,м суаже пенра вится. Л главнее — энш,и лишне пснагтаяиуеи^ ууивипи скружаншмх. (Ну. пс крайнем..мере, тег, кого всссси,сможно уеуившт.)
Слушаем не-CUI-событие Возможно, наше видео нелгзя назвать в полной мере му¬ зыкальным, но мы создадим программу, которая рисует случайную графику на экране в такт музыке. Если вкрат¬ це: программа будет слушать ритм музыки и рисовать случайный прямоугольник с каждым ударом. Возникает несколько новых вопросов. До сих пор мы слушали только события, связанные с графическим интерфейсом, но теперь нам нужно слушать особый тип MIDI-событий. Оказавшись перед этой задачей, мы понимаем, что прослушивание не-GUI событий похоже на прослушивание GUI-событий: нужно реализовать интерфейс слушателя, зарегистрировать слушателя у ис¬ точника событий, а затем сидеть и ждать, пока источник событий вызовет ваш метод-обработчик (метод, опреде¬ ленный в интерфейсе слушателя). Самый простой способ слушать ритм музыки — зареги¬ стрировать слушателя для MIDI-событий, чтобы каждый раз, когда секвенсор получает событие, наш код тоже его получал и мог рисовать графику. Но есть проблема — ошибка, которая мешает слушать MIDI-события, создан¬ ные вручную, такие как события NOTE_ON. Так что нам придется использовать обходной путь. Есть другой тип MIDI-события, на которое можно подписать¬ ся, — ControllerEvent. Наше решение заключается в том, чтобы зарегистрироваться на прослушивание ControllerEvent и убедиться, что для каждого события NOTE_ON есть соответствующее событие ControllerEvent, срабатывающее на том же шаге. Как гарантировать, что ControllerEvent произойдет точно в тот же момент? Просто добавим его в объект Track вместе с другими событиями — на ту же отметку времени. Иными словами, наша музыкальная последовательность будет выглядеть так: Шаг 1 — NOTE_ON, CONTROLLER_EVENT Шаг 2 — NOTE_OFF Шаг 3 — NOTE_ON, CONTROLLER_EVENT Шаг 4 — NOTE_ OFF и так далее. Но прежде чем погружаться в полный код программы, давайте немного упростим себе жизнь. Нам предстоит создавать много MIDI сообщений и событий, так что начнем с удобного метода, который сделает этот процесс быстрее и понятнее. Что должна делать графическая аудиопрограмма: (т) Генерировать серию MIDI- сообщений/событий для вос¬ произведения случайных нот на пианино (или любом другом инструменте по вашему выбору). (5) Регистрировать слушателя события. (5) Запускать секвенсор. @ Каждый раз, когда вызывает¬ ся метод обработчика события у слушателя, рисовать случайный прямоугольник на панели и вы¬ зывайте repaintQ. Мы создадим программу в три итерации: (т) Версия первая: добавим код, ко¬ торый упростит создание и до¬ бавление MIDI-событий — ведь нам предстоит генерировать их в большом количестве. (5) Версия вторая: зарегистрируем слушателя и начнем получать события, но пока без графики — просто выведем сообщение в ко¬ мандной строке при каждом бите. © Версия третья: полноценное решение. Ко второй версии до¬ бавляется графика: теперь про¬ грамма будет не только слышать музыку, но и рисовать в такт.
Простой способ создавать сообщения/ события На данный момент создание и добавление со¬ общений и событий в объект Track — трудо¬ емкий процесс. Для каждого сообщения нам нужно создать экземпляр сообщения (в дан¬ ном случае ShortMessage), вызвать метод setMessage (), создать MidiEvent для этого сообщения и добавить событие на дорожку. В коде предыдущей главы мы выполняли все эти шаги для каждого сообщения. А это восемь строк кода, чтобы воспроизвести ноту и завершить воспроизведение! Четыре строки для добавления события NOTE_ON и четыре строки для добавле¬ ния события NOTE_OFF. ShortMessage msgl = new ShortMessage(); msgl.setMessage(NOTE_ON, 1, 44, 100); MidiEvent noteOn = new MidiEvent(msgl, 1); track.add(noteOn); Что нужно сделать для каждого события Создать экземпляр сообщения ShortMessage msg = new ShortMessage(); (?) Вызвать метод setMessage() с необхо¬ димыми инструкциями. msg.setMessage(NOTE_ON, 1, instrument, 0) (|) Создать экземпляр MidiEvent для со¬ общения. MidiEvent noteOn = new MidiEvent(msg, 1); (5) Добавить событие в объект Track. track.add(noteOn); ShortMessage msg2 = new ShortMessage (); msg2.setMessage(NOTE_OFF, 1, 44, 100); MidiEvent noteOff = new MidiEvent(msg2, 16); track.add(noteOff); Создание статического утилитного метода, который формирует MIDI-сообщение и возвращает объект MidiEvent public static MidiEvent makeEvent(int command, int channel, int one !U»JeA«a- Jh/whuc tick u C’tiWX ИЯ OHili/jt.fMUIX манент. ШЮЛ)1 <]<-' tk'H c (p(™i nxamt гшп&тст сссчщшш. ' two, int tick) ( MidiEvent event = null; try ( ShortMessage msg = new ShortMessage(); msg.setMessage(command, channel, one, event = new MidiEvent(msg, tick); } catch (Exception e) { e.printStackTrace(); OwlJAmcq c tv-wuK tu MU. tWO)' M cocfnmue. uf¥* rw/uuunyu, метода. ) return event; сштш ( IjiMvmt c \fm ж/рутншш сосш^шым).
Версия первая: со статическим методом makeEvent^) Здесь нет логики для обработки событий или графики — только последо¬ вательность из 15 нот, поднимающихся по гамме. Цель этого кода — про¬ сто научиться использовать наш новый метод makeEvent (). Благодаря это¬ му методу код для следующих двух версий будет гораздо короче и проще. import javax.sound.midi.*; Чг" import static javax.sound.midi.ShortMessage Message . * ?ЛеУУап tu*n</wnyu£anu нужные сшСпшшт. public class MiniMusicPlayerl { public static void main(String[] args) { try { Sequencer sequencer = MidiSystem.getSequencer() sequencer.open(); ^ Sequence seq = new Sequence(Sequence.PPQ, 4); Track track = seq.createTrack(); Coff CoMltM niCAtljl &l nn.l( поспи и усрожку (лжасм. naccfx са^шпий, imcdk нети firmumt жхтамд twef — cm нстм ус ПА. ft for (int i = 5; i < 61; i += 4) { (}} (fu к^лашна.и пианчнс). track.add(makeEvent(NOTE_ON, 1, i, 100, i)) ; track.add(makeEvent(NOTE_OFF, 1, i, 100, i + 2)); } sequencer.setSequence(seq); sequencer.setTempoInBPM(220) sequencer.start() ; } catch (Exception ex) { ex.printStackTrace() ; ) l Записками J 6’ulYuy('XfH4t. и яшпод makc&mtO, cijxjbuioxmimt, ccoduy.- HU( M M'cjxHunu ac tfccdumut, (l?an\L,ti tju af!i.4t.и lujuriH fu. Hjdldmt 6<х&кт Track. ■mt ( wum nafw сссшпин ojj public static MidiEvent makeEvent(int cmd, int chnl, int one, int two, int tick) { MidiEvent event = null; try ( ShortMessage msg = new ShortMessage(); msg.setMessage(cmd, chnl, one, two); event = new MidiEvent(msg, tick); } catch (Exception e) { e.printStackTrace(); } return event; }
Версия вторая: регистрация и получение CowtrollerEvents import javax.sound.midi.*; import static javax.sound.midi.ShortMessage.*; public class MiniMusicPlayer2 { public static void main(String[] args) { MiniMusicPlayer2 mini = new MiniMusicPlayer2(); mini.go(); } public void go() { try { Sequencer sequencer = MidiSystem.getSequencer(), sequencer.open(); Pavuv^vufWM гогитач (? пк(кп соГа. ,mtmq haumiwiym nfnmu Jatm ава Мщммпха. елииштслл aMUWiA « Kun.m.u min wc,tm намет Conho№ttvwt.ja копаиааа.им хшиш следит*. Лам uipta только o^ua man — coJamuA — c lu.uipu M f int[] eventsIWant = {127}; sequencer.addControllerEventListener(event -> System.out.println("la"), eventsIWant)■ Sequence seq = new Sequence(Sequence.PPQ, 4); Track track = seq.createTrack(); rvcuifftfbw это cccJwmut. мм for (int i = 5; i < 60; i += 4) { track.add(makeEvent(NOTE ON, 1, i, 4a. ;(CONTROL CHANGE, 1, 127, 0, i))) track.add(makeEvent(NOTE OFF, 1, i, 100, i + 2)); 100. 1С.ЛА Vac ) sequencer.setSequence(seq); sequencer.setTempoInBPM(220) sequencer.start(); } catch (Exception ex) { ex.printstackTrace(); ) public static MidiEvent makeEvent(int cmd, int chnl, int one, int two, int tick) { MidiEvent event = null; try { ShortMessage msg = new ShortMessage(); msg.setMessage(cmd, chnl, one, two); event = new MidiEvent(msg, tick); ) catch (Exception e) { e.prints tackTrace(); 5 дослужи a.o mauKvfmju ccfo™b' aim*,,. (лсШи. его фи&т&нная ыА и - м птмши ссЫтт. кстс/тЯШ * «жлг- мм т/с, сигнал, «и ) return event; ■), ссманпш ,,,r J н‘лш it машм. лт асгМши СтЫШнп! на гпи.жх Хмапки TM\r"rslC% ЯР Нгш" Имений шмЛЪЛ Щг. Таким сфалш Iciaa соошхш. срасатлбаоп <аш£исшн.нс, н им тлутси сигнал. Код, который отличается от предыдущей версии, выделен серым цветом (и мы вынесли код из метода mainO в отдельный метод go())>
Версия третья: рисование графики в ритм с музыкой Эта финальная версия основана на второй, но с добавлением элементов графического интерфейса. Мы создаем окно и добавляем в него панель для рисования. Каждый раз, когда приходит событие, панель рисует новый прямоугольник и запускает перерисовку экрана. Единственное отличие от второй версии — теперь ноты проигрываются в случайном порядке, а не просто поднимаются вверх по гамме. Самое важное изменение в коде (помимо появления простого GUI) за¬ ключается в том, что теперь панель для рисования реализует интерфейс ControllerEventListener, а не основной класс программы. Благодаря этому, когда вложенный класс — панель для рисования — получает событие, он знает, как на него реагировать: просто рисует прямоугольник. Полный код этой версии вы найдете на следующих двух страницах. Вложенный класс панели для рисования: у ,,Д риссванUA является слушателей. class MyDrawPanel extends JPanel Implements ControllerEventListener { private boolean msg = false; . tin • MtRiCu m на true та икс кс4х\ истанд&швяем jxwi d/щткщ .. „ { public void controlChange(ShortMessage event) msg = true; —— Летая capadcmruKa axfimuu (и? mh repaint(); (iHlriCCJunt). public void paintComponent(Graphics g) { ЖЧМ&-Ш V Пйулм шЯт, ,1] этст раглкп w. шпаитлк . гтссп тт.А1 отсм- «xW* СШгсШкШ. if (msg) { rfC— int r = random.nextlnt(250); int gr = random.nextlnt(250); int b = random.nextlnt(250); g.setColor(new Color(r, gr, b)); •Jti май, xcqa, а ain ,u mu и jlulcdajkt JCAbKC @cmdcm на cctfnmuc Ccntrod&rfvcnt исхипш tjuai, иста ku tmc retiainlO щт btmibamt и qfrnul игастки int height = random.nextlnt(120) + 10; int width — random.nextlnt(120) + 10; int xPos = random.nextlnt(40) + 10; int yPos = random.nextlnt(40) + 10; g.fillRect(xPos, yPos, width, height); msg — false; ) > Остальная часть ксуа it HtjmjMjtm сличашиш мДт upucytm нрмжшъиник c HctGtju.ujuntun. им нора Mmhcuui. )
ите карандаш —► Решать — вам import javax.sound.midi.*; import j avax.swing.*; import j ava.awt.*; import java.util.Random; Это полный список кода третьей версии про¬ граммы. Он основан на второй версии. По¬ пробуйте аннотировать его самостоятельно, не заглядывая на предыдущие страницы. import static javax.sound.midi.ShortMessage.*; public class MiniMusicPlayer3 { private MyDrawPanel panel; private Random random = new Random(); public static void main(String[] args) { MiniMusicPlayer3 mini = new MiniMusicPlayer3(); mini.go(); public void setUpGui() { JFrame frame = new JFrame("My First Music Video"); panel = new MyDrawPanel(); frame.setContentPane(panel); frame.setBounds(30, 30, 300, 300); frame.setVisible(true); public void go() { setUpGui(); try ( Sequencer sequencer = MidiSystem.getSequencer(); sequencer.open(); sequencer.addControllerEventListener(panel, new int[]{127}); Sequence seq = new Sequence(Sequence.PPQ, 4); Track track = seq.createTrack(); int note; for (int i = 0; i < 60; i += 4) { note = random.nextlnt(50) + 1; track.add(makeEvent(NOTE_ON, 1, note, 100, i)); track.add(makeEvent(CONTROL_CHANGE, 1, 127, 0, i)); track.add(makeEvent(NOTE OFF, 1, note, 100, i + 2)); sequencer.setSequence(seq); sequencer.start(); sequencer.setTenpoInBPM(120); } catch (Exception ex) { ex.printStackTrace(); ) } )
public static MidiEvent makeEvent(int cmd, int chnl, int one, int two, int tick) { MidiEvent event = null; try { ShortMessage msg = new ShortMessage(); msg.setMessage(cmd, chnl, one, two); event = new MidiEvent(msg, tick); } catch (Exception e) { e.printstackTrace(); > return event; ) class MyOrawPanel extends JPanel implements ControllerEventListener { private boolean msg = false; public void controlChange(ShortMessage event) ( msg = true; repaint(); | public void paintComponent(Graphics g) ( if (msg) { int r = random.nextlnt(250); int gr = random.nextlnt(250) ; int b = random.nextlnt(250); g.setColor(new Color(r, gr, b)); int height = random.nextlnt(120) + 10; int width = random.nextlnt(120) + 10; int xPos = random.nextlnt(40) + 10; int yPos = random.nextlnt(40) + 10; g.fillRect(xPos, yPos, width, height); msg = false; > } ) )
\ражнение Группа опытных специалистов по Java в костюмах играет на вечеринке в игру «Лицо под маской». Каждый из них дает вам подсказку, а ваша за¬ дача— угадать, кто скрывается под маской, опираясь на их описание. Считайте, что все участники говорят о себе только правду. Если утверждение подходит сразу к нескольким персонажам — укажите их всех. Заполните пропуски именами одного или нескольких знакомых вам персо¬ нажей. Сегодня на вечеринке могут появиться любые герои из этой главы! У меня весь графический интерфейс в руках. Каждому типу события соответствует такой компонент. Ключевой метод слушателя. Этот метод задает размер окна JFrame. Вы добавляете код в этот метод , но никогда не вызываете его напрямую. Когда пользователь действительно что-то делает, это называется . Большинство таких объектов служат источниками событий. Я передаю данные обратно слушателю. Метод addXxxListenerO означает, что объект — это . Способ, с помощью которого слушатель подписывается на события. Метод, в котором находится весь графический код. Обычно я связан с экземпляром внешнего класса, g в Graphics g — это на самом деле экземпляр этого класса. Метод, который приводит к вызову paintComponent(). Пакет, в котором живет большинство компонентов Swing. *■ Ответы на стр. S43
ражнение import javax.swing.*; import java.awt.*; import java.awt.event.*; class InnerButton { private JButton button; ^ public static void main(String[] args) InnerButton gui = new InnerButton (); gui.go () ; } Компилируйте Ъаша задача - проверить исходный файл Java на наличие ошибок компиля¬ ции. Если код не компилируется, необ¬ ходимо определить причину и предло¬ жить испраЬления. Если же компиляция проходит успешно, проанализируйте, что именно делает про¬ грамма при запуске. public void go () ( JFrame frame = new JFramef); frame.setDefaultCloseOperation( JFrame.EXIT ON CLOSE); button = new JButton("A"); button.addActionListener (); frame.getContentPane().add( BorderLayout.SOUTH, button); frame.setSize(200, 100); frame.setVisible(true); I class ButtonListener extends ActionListener { public void actionPerformed(ActionEvent e) { if (button.getText().equals("A")) { button.setText("Б"); } else { button.setText("A") ; ( ) *> Ответы на стр. S43
бассейн фрагментов В бассейне находятся фрагменты кода. Ваша задача — поместить их в пустые строки кода. Некоторые фрагмен¬ ты не понадобятся, а некоторые можно использовать больше одного раза. Цель упражнения — создать класс, который скомпилируется и запустит¬ ся, а результат вывода будет совпадать с тем, что приведен ниже. Результат: Удивительный, уменьшающийся голубой прямоугольник. Программа создает синий прямоугольник, который постепенно уменьшается в размерах, пока не исчезает полностью на белом фоне. import javax.swing.*; import java.awt.*; import j ava.util.concurrent.TimeUnit; public class Animate f int x = 1; int у = 1; public static void main(String[] args) { Animate gui = new Animate (); gui.go (); } public void go() { JFrame = new JFrame () ; frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE); t .getContentPane () .add(drawP); .setVisible(true); for (int i=0; i<124; ) { try { TimeUnit.MILLISECONDS.sleep(50) ; } catch(Exception ex) ( } } } class MyDrawP extends JPanel { public void paintComponent (Graphics ) { Примечание: Элементы кода из бассейна мож¬ но использовать несколько раз, но не все! g.setColor(blue) g.setColor(white) g.setColor(Color.blue) l.setColor(Color.white) g.fillRect(x,y,x-500,y-250) g.fillRect(x,y,500-x*2,250-y*2) g.fillRect(500-x*2,250-y*2,x,y) g.fillRect(0,0,250,500) g.fillRect(0,0,500,250) 9 draw frame panel i++ i++, y++ i++, y++, x++ Animate frame = new AnimateO MyDrawP drawP = new MyDrawPO drawP.paintO ContentPane drawP = new ContentPanef) draw.repaintO drawP.setSize(500,270) frame.setSize{500,270) panel.setSize(500,27 drawP.repaintQ ► Ответы на стр. 544
Jk^ Ответы /ЛаскаКод (coap.540) У меня весь графический интерфейс в руках. JFrame Каждому типу события соот¬ ветствует такой компонент. интерфейс слушателя Ключевой метод слушателя. actionPerformed( Этот метод задает размер окна JFrame. setSize () Вы добавляете код в этот ме¬ тод, но никогда не вызываете его напрямую. paintComponent() Когда пользователь действи¬ тельно что-то делает, это называется событие Большинство таких объ¬ ектов служат источниками событий. компоненты swing Я передаю данные обратно слушателю. объект события Метод addXxxLis tener () означает, что объект — это источник события Способ, с помощью которого слушатель подписывается на события. addXxxListener () Метод, в котором находится весь графический код. paintComponent() Обычно я связан с экземпля¬ ром внешнего класса. внутренний класс g в Graphics g —это на самом деле экземпляр этого класса. Graphics2D Метод, который при¬ водит к вызову paintComponentО . repaint() Пакет, в котором живет большинство компонентов Swing. javax.swing Компилируйте (coap.541) import javax.swing. *; import java.awt.*; import java.awt.event.*; class InnerButton { private JButton button; public static void main (String!] args) ( InnerButton gui = new InnerButton(); gui.got); ) public void go () { JFrame frame = new JFrameO; frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE); Метод addActionListenerO принимает объект класса, реализующего интерфейс ActionListener. Исправленный код создает графический интерфейс с кнопкой, которая переключа¬ ется между «А» и «Б» при каждом нажатии. button = new JButton("А"); button.addActionListener(new ButtonListenerQ) ; frame.getContentPane().add( BorderLayout.SOUTH, button); frame.setSize(200, 100); frame.setVisible (true); class ButtonListener implements ActionListener ( public void actionPerformed(ActionEvent e) ( if (button.getTextO .equals ("A") ) ( button.setText ("Б"); } else ( button.setText("A"); I } ) Помните: ActionListener — это интерфейс, а интерфейсы реа¬ лизуются, а не наследуются.
Удивительный, уменьшающийся голубой прямоугольник. бассейн фрагментов (состр. 542J import javax.swing.*; import java.awt.*; import java.util.concurrent.TimeUnit; public class Animate { int x = 1; int у = 1; public static void main(String[] args) { Animate gui = new Animate (); gui.go(); ! public void go() ( JFrame frame = new JFrame(); frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE) MyDrawP drawP = new MyDrawPO; frame.getContentPaneO .add(drawP) ; frame.setSize(500,270), frame, setvisible (true) ; for (int i = 0; i < 124; i++,y++,X++ ) { X++ ; drawP.repaint(), try { TimeUnit.MILLISECONDS.sleep(50); ) catch(Exception ex) ( ) ) } class MyDrawP extends JPanel ( public void paintComponent(Graphics g) { g.setColor(Color. white); g.fillRect(0,0,500,250); g.setColor(Color.blue); g.fillRect(x, y, 500-x*2,250-y*2); ) ) }
15. Библиотека Swing Знакомство со Swing Swing — это легко. Ну, если вам не слишком важно, где именно окажутся элементы на экране. Код на Swing может показаться простым. Вы пишете, компилируете, запускаете... и вдруг ловите себя на мысли: «Эй, оно же не должно было оказаться здесь!» То, что делает написание интерфейса таким легким, одновременно делает сложным управление им: это менеджер компоновки (Layout Manager). Менеджеры компоновки управляют размером и положением компонентов в графическом интерфейсе Java. Они берут на себя большую часть рутинной работы — и чаще всего справляются. Но вот результат может быть неожи¬ данным. Вам нужны две кнопки одного размера — они получаются разными. Вы хотели бы, чтобы текстовое поле было десять сантиметров, — а оно растянулось на двадцать... или сжалось до одного. Вы планировали расположить метку рядом с полем — а она оказалась под ним. Но! С небольшими усилиями вы сможете заставить менеджеры компоновки играть по вашим правилам. Освоив основы Swing, вы получите уверенное преимущество в созда¬ нии пользовательских интерфейсов. Хотите заняться разработкой Android-приложений? Эта глава — отличная отправная точка.
Компоненты Swing Технически правильный термин для того, что мы называли «вид¬ жетом», — компонент. Это те элементы, которые вы размещаете в графическом интерфейсе. Все, что видит и с чем взаимодейству¬ ет пользователь: текстовые поля, кнопки, списки с прокруткой, переключатели и т. д. — все это компоненты. Практически все они наследуются от класса j avax. swing. JComponent. Компоненты могут быть вложенными В Swing почти все компоненты могут содержать другие компонен¬ ты. Иными словами, вы можете вложить один компонент в другой. Однако чаще всего компоненты, предназначенные для взаимодей¬ ствия с пользователем (например, кнопки или списки), добавля¬ ются в «фоновый» компонент, такой как окно (JFrame) или панель (JPanel), который действует как контейнер. Теоретически можно, скажем, вставить панель в кнопку, — но это будет выглядеть странно и вряд ли порадует пользователей. За исключением JFrame, граница между «интерактивными» и «фо¬ новыми» компонентами довольно условна. Например, JPanel обычно используют для группировки других компонентов, но его тоже можно сделать интерактивным. Так же, как и для других ком¬ понентов, вы можете зарегистрировать обработчики событий для JPanel — например, на клики мышью или нажатия клавиш. Четыре шага для создания графического интерфейса (обзор): 0 Создайте окно (JFrame) JFrame frame = new JFrame(); 0 Создайте компоненты (кнопки, текстовые поля и т. п.) JButton button = new JButton("нажми меня"); @ Добавьте в окно компоненты frame.getContentPane().add(BorderLayout.EAST, button); 0 Отобразите его (установите размер и сделайте его видимым) frame.setSize(300,300); frame.setvisible(true); Into background components: Добавьте интерактивные компоненты: click me ] choose me this is a text field Итак, виджет — это просто компонент Swing. Почти все, что вы можете раз¬ местить в интер¬ фейсе, так или ина¬ че наследуется от JComponent.
Менеджеры компоновки Менеджер компоновки — это объект Java, свя¬ занный с конкретным компонентом, чаще всего с фоновым (таким как JPanel или JFrame). Он управляет расположением и размером дочерних компонентов, размещенных внутри того, с кото¬ рым он связан. Например, если окно содержит панель, а панель — кнопку, то менеджер компо¬ новки панели управляет размером и положением кнопки. Менеджер компоновки окна управляет размером и положением панели. Кнопка не содержит других компонентов, поэтому ей не нужен менеджер компоновки. Если панель содержит пять компонентов, то именно ее менеджер компоновки кон¬ тролирует, где и как эти пять компонентов будут отображаться. Если один из этих компонентов — тоже панель или другой контейнер, содержащий свои компоненты, то размещением вложенных компонентов будет управлять уже менеджер компоновки вложенного контейнера. Когда мы говорим, что компонент «содержит» другой, это означает, что компонент был добав¬ лен в него. Например: myPanel.add(button); Есть несколько видов менеджеров компоновки, и каждый контейнер может использовать свой. У каждого типа — свои правила: Один может требовать, чтобы все компоненты были одинако¬ вого размера и располагались в сетке. Другой — позволять компонентам иметь разные размеры и располагать их, скажем, в колонку. Таким образом, вложенность интерфейса приводит к вложенным компоновкам, и каждым уровнем управляет свой менеджер. Будучи менеджером компоновки, я отвечаю за размер и расположение ваших компонентов. В этом графическом интерфейсе именно я решил, какого размера должны быть эти кнопки и где они находят¬ ся относительно друг друга и относи¬ тельно окна. I КЛШ~ л л А. .ДсшраН клл* t\UuJ’ku «XOHCAWd lAvvtvfc пснсбкн аднии T) 0,'Мсл.тп.ралм(р и рааищлнстш 1 n\ptv КНОПОК. a расположен) txavvtm T). JPanel panelA = new JPanel(); JPanel panelB = new JPanel(); panelB.add(new JButton("кнопка l")j panelB.add(new JButton("кнопка 2")j panelB.add(new JButton("кнопка 3")j panelA.add(panelB); . Менеджер ксмпснойги панелиА нс илист 7ШКЛКСТС влияния на эти Иерархия управ три кнопки. Иерархия лиа втсяишь на санам f жхр компоновки панели А'управляем тальке ними сялиита.ии кширш няни ..UtHJLCf доставлена нспирир-т&ннс в панель Л, и нс кттШли/шап нише, ти на- ml х и хедится йп/т/ш эти*' с/осшлтнмх
Как менеджер компоновки принимает решения? У разных менеджеров компоновки разные правила размещения компонентов: одни располагают их по сетке, другие выравнивают по вертикали, третьи делают все компоненты одинакового размера. Но и сами компоненты, которые размещаются, тоже могут влиять на процесс — пусть и незначительно. Обычно размещение компонентов внутри фонового контейнера происходит следующим образом: Сценарий компоновки: Создается панель, в которую добавляются три кнопки. Менеджер компоновки панели обращается к каждой кнопке с вопросом: какой размер тебе предпочтительнее? Давайте посмо¬ трим... Первая кнопка хочет быть шириной 30 пикселей, текстовому полю нужно 50, окно — шириной 200, а я должен все это выстроить по вертикали. (5) Затем менеджер компоновки панели, опираясь на свои пра¬ вила, решает: учесть ли все предпочтения кнопок, только часть из них или полностью их игнорировать. (D Менеджер компоновки окна (например, JFrame) запра¬ шивает у панели: а какой размер предпочла бы ты? (5) Панель добавляется в окно. (6) Менеджер компоновки окна согласно своей политике ре¬ шает, насколько учитывать предпочтения панели — полно¬ стью, частично или вовсе не учитывать. Менеджеры компоновки и их политика У разных менеджеров компоновки — разные подходы. Некоторые из них полностью уважают предпочтительные размеры компонентов. Если кнопка хочет быть 30x50 пикселей, именно такой размер ей и будет выделен. Другие менеджеры компоновки уважают только часть предпочтений. К примеру, если кнопка хочет быть 30x50, менеджер может согласиться с шириной, но по высоте растянет кнопку на всю доступную область панели. А есть такие менеджеры, которые ориентируются только на самый крупный компонент — и делают все остальные того же размера, игнорируя их пожелания. Да, иногда логика работы менеджера компоновки может показаться загадочной. Но, как только вы познакомитесь с политиками конкретного типа, вы почти всегда сможете предугадать его поведение.
Три основных менеджера компоновки: Border, Flow и Box. BorderLayout Менеджер компоновки BorderLayout делит фоновый компонент на пять областей. В каждую область можно добавить только один компо¬ нент, который будет управляться этим менед¬ жером. Компоненты, размещенные с помощью BorderLayout, обычно не учитывают свои пред¬ почтительные размеры: именно менеджер решает, как распределить доступное пространство. BorderLayout — это менеджер компоновки по умолчанию для окон (JFrame). FlowLayout FlowLayout работает примерно как текстовый редактор, только вместо слов у вас компоненты. Каждый компонент сохраняет свой желаемый размер, и компоненты располагаются слева на¬ право в том порядке, в котором были добавлены. Когда компонент не помещается в строку по ширине, он переходит на следующую строку — происходит «перенос». FlowLayout — это ме¬ неджер компоновки по умолчанию для панелей (JPanel)! BoxLayout BoxLayout похож на FlowLayout: компонен¬ ты добавляются в порядке их размещения, и каж¬ дый из них сохраняет свой собственный размер. Но главное отличие в том, что BoxLayout может располагать компоненты вертикально или горизонтально — и при этом не переносит автоматически на новую строку. Обычно его используют для вертикального размещения, при этом вы самд определяете «переход на новую строку» — как будто нажимаете клавишу Enter. В votcf л CrlWwUOCf* KaUt\tm>°n mo ' о*- та с'ui\ щд Q сиэ I 1ШВ К(.шинжт£л cjtcad- ллются ( d(f \X\J пс ы|нс,ш| mi mtwrjcu «стпоке»; если нп- стЛошс йцтиклина ^стиашкие.
BorderLayout учитывает пять областей: левая, правая, верхняя, ниж¬ няя и центральная. Добавим кнопку в правую область; import import javax.swing, java.awt.*; -KxaccBoukrL^mtf^ iiwH u пакете java awt public class Buttonl { public static void main(String[] args) { Buttonl gui = new Buttonl(); gui.go(); ) } public void go() ( JFrame frame = new JFrame(); ^ JButton button = new JButton("нажми меня") tv frame.getContentPane().add(BorderLayout.EAST, frame.setSize(200, 200); frame.setVisible(true); tcGatmt button); } Прокачай извилины Как менеджер компоновки BorderLayout опреде¬ лил этот размер для кнопки? Какие факторы он должен учитывать? Почему кнопка не стала шире или выше?
Вот что произойдет, если добавить в текст кнопки больше символов... public void go() { JFrame frame = new JFrame(); JButton button = new JButton("щелкай, как следует1"); frame.getContentPane().add(BorderLayout.EAST, button) frame.setSize(200, 200); frame.setVisible(true); } mtKcnv нл K-HorvKt Текст на мне увеличился, для него нужно больше места, мой размер стоит сделать 60 пик¬ селей в ширину и 25 пикселей в высоту. Поскольку кноп¬ ка размещена в правой области, я учитываю ее пред¬ почтительную ширину. А вот предпочита¬ емая высота роли не играет — кнопка будет такой же высоты, как и окно. Таков мой принцип. В следующий раз я выберу FlowLayout. Тогда у меня будет ВСЕ, что я хочу. '“W
Пробуем разместить кнопку в верхней области public void go() { JFrame frame = new JFrame(); JButton button = new JButton("Здесь нет ложки..."); frame.getContentPane().add(BorderLayout.NORTH, button); frame.setSize(200, 200); frame.setVisible(true); ) ста кнопки такая, как норснс. a um|vnna mmftm cmfiutnv нш|\пне окна. А теперь увеличим высоту кнопки. Как это сделать? Кнопка и так уже максимально широкая — она занимает всю ширину окна. Но мы можем попытаться сделать ее выше, просто увеличив размер шрифта. public void go() { JFrame frame = new JFrame(); JButton button = new JButton("Нажми сюда!") Font bigFont = new Font("serif", Font.BOLD, 28); button.setFont(bigFont); frame.getContentPane().add(BorderLayout.NORTH, button), frame.setSize(200, 200); frame.setVisible(true); } j • • • Кща иииЦ'т становится «.uLJomu кнопке вшря инея с!сл ыие диета. но О высоте. пртш остается прежней, а высота кнопки ц&.шшлась. Верхняя оашпирапиирп лась в соответствии с новыдо п^шпегтитедькыдо ртлп рей высоты кнопки. '
Да, именно так! Если кнопка раз¬ мещена в левой или правой области, она получает свою предпочтительную шири¬ ну, а вот высоту задает менеджер компонов¬ ки. Если же кнопка находится в верхней или нижней области, то ситуация обратная: кнопка получает предпочтительную высоту, а шири¬ ну определяет менеджер компоновки. Как регулируется размер центральной области? Центральная область получает все оставшееся пространство! (за исключением одного особого случая — о нем поговорим чуть позже). public void go О ( JFrame frame = new JFrameO; JButton east = new JButton("правая"); JButton west = new JButton("левая"); JButton north = new JButton("верхняя"); JButton south = new JButton("нижняя") ; JButton center = new JButton("центральная"); frame.getContentPane().add(BorderLayout.EAST, east); frame.getContentPane() .add(BorderLayout.WEST, west); frame.getContentPane().add(BorderLayout.NORTH, north); frame.getContentPane 0.add(BorderLayout.SOUTH, south) ; frame.getContentPane().add(BorderLayout.CENTER, center); } frame.setSize(300, 300); frame.setVisible(true); Аьипсшнпш (S’lUHm/u ставшая У<<утст(?с, ис.хщя u рал.шрьвокна (ЗОСуЗСП n^u^nu,UTiuT0. ftcamovrHtn^ m Т1и№»Ц- • • • $фхяяя И НЫЖПЯМ caactnu ланммают dew ширины ‘контейнера, поэтами, рал ш щ/ем кампеш наш С них dei admauamuгески умень¬ шаете срапуннш dei¬ ce ту у Ля npadcu u.iedeCi ccGekmeu. 300 пмкялей
ino 0 If''*- Г—vl FlowLayout управляет расположением компонентов: располагает их слева направо и сверху вниз в том порядке, в котором они были добавлены. Добавим панель в правую область окна: Давайте добавим панель в правую область окна. По умолчанию у JPanel используется менеджер компоновки FlowLayout. Это значит, что когда вы добавляете панель в окно, ее размер и положение контролируются менеджером BorderLayout. Но все, что добавляется внутрь панели с помощью метода panel.абб(компонент), уже под¬ чиняется менеджеру компоновки самой панели — в данном случае FlowLayout. Начнем с того, что добавим пустую панель в правую область окна. А на следующих страницах будем постепенно добав¬ лять в нее нужные элементы. import j avax.swing. import j ava.awt.*; Cc аитм iuihjuu) ftjitu, mum iu.)«m rftu.c mt>, гцс ybuycmb, ay immkho сна находится окне. public class Panell { public static void main(String[] args) { Panell gui = new Panell(); gui.go(); } public void go() { JFrame frame = new JFrameO; JJcxa ^тЪ(Ш€\Тпштац, потоми (?nha(ku ccCuicmu JPanel panel = new JPanel (); сна мпаиает лкишша дбнс (кяможнию шиШни. panel.setBackground(Color.darkGray); <) J j 11 frame.getContentPane().add(BorderLayout.EAST, panel); frame.setSize(200, 200); frame.setVisible(true); }
Теперь добавим кнопку на панель public void go() f JFrame frame = new JFrame(); JPanel panel = new JPanelO; panel.setBackground(Color.darkGray); JButton button = new JButton("Шокируй меня panel.add(button); frame.getContentPane().add(BorderLayout.EAST, panel) frame.setSize(200, 200); frame . setVisible (true) ; ...й r\AH£.4t> — ^ОКНО. (toAWCHofeu mwt кишка ие&яссн fkicwmu. Шс%! Лнт^, tuxAutlua свои yvatu rmumt и ши hcutujm 'Ч'Лн/uiHt bCtmJ.lX } 1и/ГИС шш\яи]н ' 6 ( KHl у' mew как паниг TCiulau^i. и исн ко он omjfo.iae.ni ж тс. как вшамунт Лепка Менеджер BorderLayout окна Менеджер панели FlowLayout
А если нам нужно разместить на панели две кнопки? public void go() { JFrame frame = new JFrameO; JPanel panel = new JPanelO; panel.setBackground(Color.darkGray) ; JButton button = new JButton ("Шокируй меня"); JButton buttonTwo = new JButton("Блаженство") ; tr panel.add(button); ^ panel.add(buttonTwo) кнопки на пани/ frame.getContentPane().add(BorderLayout.EAST, panel); frame.setSize(250, 200); frame.setVisible(true) ; Что мы хотели: ООО Что мы получили: П('1(0*<*Нцьч . „ц-.лищюр КЛ1,№\\0С&‘* Ofmmm Qmма\т, тс кнопкип1К .wmuut кнопки -JX." «jfrtOTV rvoflHG распиши .1 ('cwiauoiu. Апопка п0И|и< СЧМОЛЬК.rtrr <кк'< г и uini/’iid г uf аточите карандаш места, скХикс i n тропе (» R{ ► Решать-вам! Если заменить предыдущий код на показанный ниже, как будет выглядеть графический интерфейс? JButton button = new JButton("Шокируй меня")• JButton buttonTwo = new JButton("Блаженство") JButton buttonThree = new JButton("Что?"); panel.add(button); panel.add(buttonTwo); panel.add(buttonThree); ••• Нарисуйте, как, по вашему мнению, будет выглядеть графический интерфейс после запуска кода слева. (А затем запустите его и проверьте!)
\ Q BoxLayout спешит на помощь! Он размещает компоненты строго в столбик — даже если есть достаточно места, чтобы выстроить их в ряд. В отличие от поточной компоновки (FlowLayout), BoxLayout может заставить компоненты распола¬ гаться вертикально, даже когда они без проблем помещаются по горизонтали. Но теперь придется вручную заменить менеджер компоновки панели — вместо стандартного FlowLayout установить BoxLayout public void go() { JFrame frame = new JFrame(); JPanel panel = new JPanelf); panel.setBackground(Color.darkGray) ; /Vuiumu panel.setLayout(new BoxLayout(panel, BoxLayout.у AXIS))• JButton JButton panel panel frame frame frame } button = new JButton("Шокируй меня") buttonTwo = new JButton("Блаженство" .add(button); .add(buttonTwo) ; ' 'fnc шш тнш^ w m getContentPane'() .add (BorderLayout ЕД5Т .setSize (250,200) ; T' Panel); .setvisible(true); n « • • • OeWmumc <?ни.лши»о: miHMb сисба cW' u([,.. (лк annul нс нижио |uuuiuum\i id KROIKW t\C ll|\U|lHUUMU., КПК |ЛДНПМ(.. Jnuiu сна fccau/itm <j^ukuu, mo m nocmarrvctHO низшим uuut. ф\м. самой. иш|и кой кнопки — «Шокируй.ЛНН.Ч»
(Ие) дурацкие вопросы ВоИрОС: Почему нельзя добавлять компонен¬ ты напрямую во фрейм, как в панель? Ответ. JFrame — особенный. Он отвечает за отображение окна на экране и взаимодействие с операционной системой. В то время как все ваши Swing-компоненты — это обычные Java- объекты, JFrame выходит за рамки Java и подклю¬ чается к «реальному миру» — к дисплею. Пред¬ ставьте панель содержимого (content рапе) как 100% Java-слой, который накладывается поверх фрейма. Или вот еще образ: JFrame — это рамка (фрейм) окна, a content рапе — стекло. Если хоти¬ те заменить панель содержимого своей, просто используйте: myFrame.setContentPane(myPanel); ВоМрОС: Можно ли изменить менеджер ком¬ поновки окна? Например, FlowLayout вместо BorderLayout? Ответ: Проще всего создать панель, рас¬ ставить на ней компоненты как нужно и сделать эту панель содержимым фрейма с помощью setContentPaneQ. Так не придется менять менед¬ жер компоновки стандартной панели содержи¬ мого. ВоИрОС: А если мне нужен другой размер? Есть ли метод setSizeO у компонентов? Ответ: Да, такой метод есть, но большин¬ ство менеджеров компоновки его игнорируют. Менеджер спрашивает у компонента его предпо¬ чтительный размер с помощью getPreferredSizeO, и именно этот размер влияет на раскладку. A setSizeO — это принудительная установка раз¬ меров, но она не работает, если вы используете менеджер компоновки. ВоИрОС: А что, если я хочу просто разместить компоненты где угодно? Можно вообще отклю¬ чить компоновку? Ответ: Можно. Для этого вызовите setLayout(null) у контейнера — и тогда вы вруч¬ ную задаете точные координаты и размеры для каждого компонента. Flo учтите: в долгосрочной перспективе менеджеры компоновки гораздо удобнее, особенно при изменении размеров окна и переносе на другие платформы. Без лишних слов ■ Менеджеры компоновки управляют размером и распо¬ ложением компонентов, вложенных в другие компо¬ ненты. ■ Когда вы добавляете один компонент в другой (вло¬ женный в так называемый фоновый компонент — нео¬ фициальный термин), менеджер компоновки фонового компонента берет на себя управление добавленным. ■ Менеджер компоновки сначала запрашивает у каждого компонента его предпочтительный размер, а затем решает, где и как его разместить. В зависимости от своей логики он может учесть все, часть или вовсе проигнорировать предпочтения компонентов. ■ Менеджер компоновки BorderLayout позволяет раз¬ местить компонент в одной из пяти областей: верхней, нижней, правой, левой или центральной. При добавле¬ нии обязательно указывается область, например: add(BorderLayout.EAST, panel); ■ Компоненты в верхней и нижней областях получают свою предпочтительную высоту, но растягиваются по всей ширине. Компоненты в левой и правой областях получают свою предпочтительную ширину, но растяги¬ ваются по всей высоте. Компонент в центре заполняет все оставшееся пространство. ■ FlowLayout размещает компоненты слева направо, сверху вниз, в порядке их добавления. Если компонент не помещается по горизонтали, он переносится на следующую строку. ■ FlowLayout устанавливает компонентам их предпочти¬ тельные размеры по обеим осям. в BoxLayout размещает компоненты по вертикали (или по горизонтали — в зависимости от настройки), даже если они могли бы разместиться рядом. Как и FlowLayout, он использует предпочтительный размер компонента в обоих измерениях. ■ BorderLayout — это менеджер компоновки по умолча¬ нию для панели содержимого окна (content рапе фрей¬ ма), a FlowLayout используется по умолчанию в панели (JPanel). ■ Если вы хотите, чтобы панель использовала дру¬ гой менеджер компоновки вместо стандартного FlowLayout, вызовите у нее метод setLayout() и пере¬ дайте нужный менеджер.
Эксперименты с компонентами Swing С основами менеджеров компоновки мы уже разобрались, так что пора перейти к практике. Давайте попробуем поработать с несколькими самыми популярными компонентами: текстовым полем, прокручиваемой текстовой областью, флажком и списком. Мы не будем разбирать весь API каждого компонента — только основ¬ ные моменты, которых достаточно, чтобы начать. А если захочется углубиться, загляните в книгу Java Swing Дейва Вуда, Марка Лоя и Роберта Экштейна. Компонент JTextField Конструкторы JTextField field = new JTextField(20); JTextField field = new JTextField("Ваше имя суиоплот 20 колонок, а но пик- ~ tj| ( tvtuijoi.<um npujm/imumt tt hvJk iwJfwfy nuK(WlPui паля- Как использовать © Получить текст из текстового поля: System.out.println(field.getText()); (5) Записать текст в текстовое поле: field.setText("неважно"); field.setText(' ' (/tiuu/ltm ,u',( (D Обработать событие после нажа- С(1с тия клавиши Enter/Returrv ^иШкисстЫй vuiiLL надшил. т\Ш стаи- кла&т %.у&„т.им. field.addActionListener(myActionListener) © Выделить весь текст в поле: field.selectAll(); © Вернуть курсор в поле (чтобы пользователь мог сразу начать ввод): field.requestFocus() ;
Компонент JTextArea * • • В отличие от JTextField, компонент JTextArea может содержать несколько строк текста. Но для этого потребуется внести некоторые настройки, потому что по умолчанию он не имеет полос прокрутки и не переносит строки. Если вы хотите, чтобы JTextArea прокручивалась, нужно поместить ее в JScrollPane. Это компонент, который обожает прокрутку и полностью берет на себя заботу о перемещении содержимого Приветики! Я — текстовая область! текстовой области. Конструктор JTextArea text = new JTextArea(10, 20); ,ujio ftWfUAHJfX )(i(/n;r№m2Ccmc.u4u6 H Как использовать © Настроить вертикальную прокрутку панели ! JScrollPane scroller = new JScrollPane(text); text.setLineWrap(true); > aePm ы »иДе’ ашшм ч* -у» ( scroller.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS); / scroller.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL SCROLLBAR NEVER); 1 r sum -Ar panel.add(scroller); f«»cHc!Uia щ [yKjjfac КСЩЩ не © Заменить текст в поле: тки text.setText("Не все потерянные блуждают"); Чтобы добавить текст в уже существующий текст в JTextArea, используйте метод append(): text.append("кнопка нажата"); ® Чтобы выбрать/выделить весь текст в поле JTextField, используйте метод select А11(): text.selectAll(); © Чтобы вернуть курсор в поле и позволить пользователю сразу начать вводить текст используйте метод requestFocusQ; text.requestFocus();
Пример с компонентом JTextArea import javax.swing.*; import java.awt.*; import java.awt.event.*; public class TextAreal { public static void main(String[] args) { TextAreal gui = new TextAreal(); gui.goO ; } public void go() { JFrame frame = new JFrameO; JPanel panel = new JPanelO; JButton button = new JButton("Просто нажми"); • • • кнопка кнопка кнопка кнопка кнопка кнопка кнопка кнопка кнопка нажата нажата нажата нажата нажата нажата нажата нажата нажата JTextArea text = new JTextArea (10, 20); ЛиЫлмси nutfd ntht&qa text. setLineWrap (true) ; cmfVKU. mxwn mu наши-1 button. addActionListener (e -> text, append ("кнопка нажата \n")b' (MUM KHCtXKU СЛШ) l ' лип на on ’ ntf 'VLH.OCU k fa-Ыажшж. а хя штуг па тритии* списки. uuuJc%Mmadc>Jic. 1 Шахт исшМстсч ^ 1 ь сани лшннин сШики. ' JScrollPane scroller = new JScrollPane (text) ; J '' ' ' ■ scroller.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_AL scroller.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLL _ panel.add(scroller); frame.getContentPane().add(BorderLayout.CENTER, panel); frame.getContentPane().add(BorderLayout.SOUTH, button); frame.setSize(350, 300); frame.setvisible(true) ;
Компонент JCheckBox Конструктор JCheckBox check = new JCheckBox("Идет до 11"); Как использовать © Отслеживать событие выбора элемента (установку/ сброс срлажка). check.addltemListener(this); © Обработать событие (и узнать, установлен флажок или сброшен). public void itemStateChanged(ItemEvent е) { String onOrOff = "сброшен”; if (check.isSelected()) ( onOrOff = "установлен"; } System.out.println("Флажок " + onOrOff); (D Установить или сбросить флажок в коде. check.setSelected(true); check.setSelected(false); (He) дурацкие вопросы ^ОПрОС: Разве менеджеры компо- новки не создают больше проблем, чем решают? Если мне все равно при¬ ходится прилагать столько усилий, не проще ли просто вручную задать размеры и координаты для каждого компонента? Ответ Добиться от менедже¬ ра компоновки точного размещения компонентов — задача не из простых. Но стоит подумать, а что он вообще делает для вас? Даже такая на вид простая задача, как определение, куда что разместить на экране, может ока¬ заться довольно сложной. Менеджер компоновки, например, не допускает перекрытия компонентов. Он управ¬ ляет расстоянием между элементами, компонентами и краями окна. Конечно, все это можно сделать вручную. Но что будет, если вы хотите расположить элементы впритык? Может быть, у вас получится аккуратно все расставить «на глаз», — но это сработает только в вашей JVM! Почему? Потому что компоненты могут выглядеть по-разному на разных плат¬ формах, особенно если используется нативный стиль оформления (look and feel). Какие-то мелочи, например, скос границ у кнопок, могут отличаться — и то, что на одной системе выглядело ровно, на другой вдруг начнет сжимать¬ ся или наезжать. И это мы еще не добрались до главного. А что будет, если пользователь изменит размер окна? А если в вашем интерфей¬ се компоненты появляются и исчезают на лету? Если бы вам приходилось вруч¬ ную пересчитывать размещение всех элементов каждый раз при изменении размера или содержимого, это было бы настоящим кошмаром!
Компонент J List альфа бета гамма дельта Constructor String[] listEntries = {"альфа", "бета", "гамма", "дельта "эпсилон", "дзета", "эта", "тета"}; JList<String> list — new JListO(listEntries); (n\t\(Kll H( a r e t(lyam^f nc должны {'ami жттт #w X }Ш~есЯт.кмм клдсс, roemawy ft* W„U иклхши тип сб'мктсб’. кст fuit cfyifn Ложатся € списке. \ 4 /Jjm этггс игпа«|М«тся емСс(ис1МШ crufLuncfi Л. IMMf / Ь Gt\ <4f“nU Как использовать ф Настроить вертикальную прокрутку JScrollPane scroller = new JScrollPane(list); scroller.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS); scroller.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); panel.add(scroller); Э.1« mu^aumc c)^ «f J, ® Настроить количество строк, которые будут отображаться для активации прокрутки, list.setVisibleRowCount(4) ; (D Ограничить пользователя возможностью выбирать только ОДИН элемент одновременно, list.setSelectionMode(ListSelectionMddel.SINGLE_SELECTION); ® Зарегистрировать обработчик событий выбора в списке. list.addListSelectionListener(this); (D Обработать события (определить выбранный в списке public void valueChanged(ListSelectionEvent е) { if (!е.getValuelsAdjusting()) { String selection = list.getSelectedValue(); System.out.println(selection); ) } (Vnmut niuwf “f,n элемент). -ГШш 9,ru f^Ui. JUnuy yetк&с1аШшО (ракпмтст (fatomiw- Ш| (dbiKm. (mlпк не «раттн ст/юко&хлш объектами..
% Готовим код * J3 J • •• d Cyber BeatBox сГ ~л Bass Drum □ DO О О о о .6 о о О 0 о о о с Start Closed Hi-Hat о о .о о о о 0 0 0 о о Stop Open Hi-Hat П □ n О о о о о о о о о 0 С 0 о Acoustic Snare о о о 0 0 о •о 0- о о о о -о Crash Cymbal era о a id о с о О о 0 с о о о о Tempo Down Hand Clap □ □ D о о О о о о О 0 п \—> 0 о f—> о High Tom a о о г—ч ( S “1 J и о о о н га о о 0 0 га ч J о Hi Bongo □ no га га о О о о га LJ- о о 0 о н. V . J о Maracas -О □ G С 0 о о о о о о о о о га о Whistle GOO о о о о о о о о с о о га о Low Conga □ о o: о о о о о о о о □ С О . о Cowbell о о 0 о о О о о о о о о с о о о Vibraslap ООО о о Ж о о о о 0 О' 0 о Low-mid Tom О СО О' о о о о О •о о о о о о High Agogo ООО о с о с о о О1 о о о р о о Open Hi Conga а о о о о 0 О о о q о о J Эта часть необязательная. Мы создаем полный битбокс, включая графический интерфейс. В главе 16 вы научитесь сохранять и загружать ритмические рисунки. А в главе 17 мы превратим битбокс в рабочий чат-клиент.
Создание битбокса Далее мы рассмотрим полный листинг текущей версии битбокса, с кнопками для начала/остановки воспроизведения и изменения темпа. Исходный код представлен полностью и снабжен исчерпывающими комментариями. Приведем небольшой обзор: ф Создаем графический интерфейс, в котором будет 256 флажков (JCheckBox), изначально сброшенных, 16 меток (JLabel) с названи¬ ями инструментов и четыре кнопки управления. ф Регистрируем обработчики событий (ActionListener) для каждой из четырех кнопок. Обработчики для флажков не нужны — мы не собираемся реагировать на взаимодействия с ними. Вместо этого мы подождем, пока пользователь нажмет кнопку «Старт», и толь¬ ко тогда переберем все 256 флажков, чтобы извлечь их состояния и на основе этого создать MIDI-дорожку. ф Настраиваем MIDI-систему (вы уже делали это раньше): получа¬ ем секвенсор, создаем последовательность и дорожку. Мы вос¬ пользуемся методом setLoopCount() секвенсора, чтобы указать, сколько раз должна проигрываться последовательность. Также мы используем темп-фактор для изменения темпа в одну из сторон и сохранения его настроек между циклами воспроизведения. Когда пользователь нажимает кнопку «Старт», начинает¬ ся настоящая работа. Обработчик события вызывает метод buildTrackAndStart(). В нем мы проходим по всем 256 флажкам по очереди, то есть по каждому инструменту на всех 16 шагах. Мы считываем состояние флажков и на его основе строим MIDI- дорожку, используя удобный метод makeEvent() (вы уже исполь¬ зовали его в предыдущей главе). Как только дорожка готова, мы запускаем секвенсор. А дальше он будет играть — по кругу, как мы настроили — до тех пор, пока пользователь не нажмет кнопку «Стоп».
inport javax.sound.midi.*; inport j avax.swing.*; inport j ava.awt.*; inport j ava. util .Array-List; inport static javax.sound.midi.ShortMessage.*; public class BeatBox { private ArrayList<JCheckBox> checkboxList; private Sequencer sequencer; private Sequence sequence; private Track track; Ф.АОЖКН XjVIHMinrj Этс и шна апсМщшнтЛ njwcicmalLttiumt и лшсстш. ФМ n\ij\cp( im (вкажаеш "Бас-барабан", "Закрытый хэт", "Открытый хэт", "Малый барабай”, "Крэш-хэт", "Хлопок ", "Высокий том", "Высокий бонго", "Маракасы", "Свисток", "Низкий конга", "Ковбелл", "Вибраслэп", "Средний том", "Высокий агого", "Открытый конга" . . int[] instruments = {35, 42, 46, 38, 49, 39, 50, 60, 70, 72, 64, 56, 58, 47, 67, а метек НИ" public static void main(String[] args) { new BeatBox () .buildGUIO ; ) public void buildGOIO { JFrame frame = new JFrame("Виртуальный битбокс"); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); BorderLayout layout = new BorderLayout(); JPanel background = new JPanel(layout); background.setBorder(BorderFactory.createEnptyBorder(10 10 10 10))’ /ч! Эти тела — нсмера «крайни» -канале mjKe£. Канал feacxmum как пианинс. нс кажаая «клавиша» flunju luGurnm не нети, a KCHKfuit\HHuj6uK. /гапШ шИ, 35 - этс cac c'aj/uuMH. Ц2 ' — лекРтпиш дэт' 35 -маши атаКан н так аалее. ' Box buttonBox = new Box(BoxLayout.Y_AXIS); JButton start = new JButton("Старт"); start.addActionListener(e -> buildTrackAndStart()) buttonBox.add(start); JButton stop = new JButton("Стоп"); stop.addActionListener(e -> sequencer.stop()); buttonBox.add(stop); JButton upTenpo = new JButton("Ускорить темп"); upTempo.addActionListener(e -> changeTempo(1.03f)) buttonBox.add(upTenpo); rsraK*» . имта-вшшжж eujeamu iwyxefm сим этих аштпшксь агмтии. шуа эта кнсика нлжшшатся, kc. wc неш нижне ajt iami, - этс foy&mu кем к/итнми Aumeej. (Ш&н 1.0. м хт 7Т( иис даник! mt.un _ * JButton downTerapo = new JButton ("Замедлить темп");£^~ f n»\uhU( W ^ ± all нажатт «.«' downTempo.addActionListener(e buttonBox.add(downTempo); — -> changeTempo(0.97f)). (illKlUjCU KtliUKH.
ж Box nameBox = new Box(BoxLayout.Y_AXIS); for (String instrumentName : instrumentNames) ( JLabel instrumentLabel = new JLabel(instrumentName) ins trumentLabel.setBorder(BorderFactory.createEmptyBorder(4 1 nameBox.add(instrumentLabel); ) 6k- Г u ^u c^4amm 4, 1)) ; background.add(BorderLayout.EAST, buttonBox); background.add(BorderLayout.WEST, nameBox); frame.getContentPane().add(background); fuy: ко ин/neti темною. ГйTaJf^l0,fUAUM' GridLayout grid = new GridLayout(16, 16); grid.setVgap(1); grid.setHgap(2); fUH .AWV mu кюиг A 1 1fC f>C\ l JPanel mainPanel = new JPanel(grid); background.add(BorderLayout.CENTER, mainPanel); checkboxList = new ArrayListO(); for (int i = 0; i < 256; i++) { JCheckBox c = new JCheckBox(); c.setSelected(false); checkboxList.add(c); mainPanel.add(c); } TtM пшсванваш i лшп н ш iaCse (J * r setUpMidi(); frame.setBounds(50, 50, 300, 300); frame.pack(); frame.setvisible(true); private void setUpMidi() { try ( sequencer = MidiSystem.getSequencer(); sequencer.open(); sequence = new Sequence(Sequence.PPQ/ 4); track = sequence.createTrack(); sequencer.setTempoInBPM(120); } catch (Exception e) ( e.printStackTrace () ; ) и Hack. Шва иихго ccUhhcic. }
fim m Gclнашнатхт!Ла^^стштия MuJU uxtm^cmuL, [qoMiMUl € аомжку. (кмма(шМ6 адыкишсв- no санащ| ид “ * ' ' Жат та. СсМ инструдш ш (jMsfuffam ид како/летс июк. в эти <тЦ ^итбям&Асгс «к.* _ < lUAI 'u.uip). fr.iu L «jc.wtm -рилие мелело C. private void buildTrackAndStart() { int[] trackList; . sequence.deleteTrack(track) track = sequence.createTrack \^kja.m.u старую усрсжку иаууаем новую. чьи это уля каждой щ 16 строк (нanfm - — лир, «шс-(1арасан», «7£и|киихонгд» и т. у).7 У г та н аблмбде и «кдюг^ингн\р^-иентд — «с исрнпиирикд- int key = instruments [i] ■ ^ »«)b (наприлир. сарит». «Открттиш biwm r»J. ^ у|шгиб мнструиентсо содержит нас№М1х1шЖМ1)-ххо.шра. Ц коде twiWcmo нрмирасло млат: создаем сссмтая а*л текиишо инетМлинта и cjcrfaiuAtA их <?ое£ект Jtckk. for (int i = 0; i < 16; i++) ( trackList = new int[16]; for (int j - 0; j < 16; j++) { ^ ~mtKuW/ei ииетру JCheckBox jc = checkboxList.get(j + i*6 * i) ; if (jc.isSelected()) { Л л , ) Можно: щжшшСчл C 16-ii ? литет trackList[j] = key; > else { trackList[j] = 0; } юГтпш на Mi шаг 1шсразил от 0 |о 15). Шаго cumoW ‘шт но асиграш. всю псеЖасвап\£инсспи, ас нип nWif/vv wo .La j J кони/!, npawyc чело нагнется нсбтср. В коде cwrufcKca неритраеи ншги: ссмасм ессмтиа фон — текущего инструмента- и уосавшлх ил бссмкт J tdck. track.add(makeEvent(CONTROL_CHANGE, 1,127, 0, 16)); makeTracks(trackList); track.add(makeEvent(PROGRAM_CHANGE, 9, 1, 0, 15)) /Важно: нужно стгательнс цствнпи |,и.я cm 0 . не у и try { sequencer.setSequence(sequence); sequencer.setLoopCount(sequencer.LOOP_CONTINUOUSLY); sequencer.setTempoInBPM(120); sequencer. start () ; , catch (Exception ЖВД,, уж нс иямтс. иш a coefmnut на 16 и шаг (ниеирту ас 15). Иначе dummuc может н ■ухать всю пссщсвшпсиность ^о кони/г. прежде го и нагнется нобтор. e.printStackTrace(); } } /1 и тлошеекм и р шунчж. у нм. " Этот лоетод задает коли гестбс псвтхШ) - или, как в нашей спите, делает бос- нрау бсус н ш: с&кснегн «л и. private void changeTempo (float tempoMultiplier) { float tempoFactor = sequencer.getTempoFactor(); sequencer.setTempoFactor(tempoFactor * tempoMultiplier) ) Ксэсрсршо, цент телтд от¬ бегает/а скорость боспрхлубе- дения ' ускоряет, ней гашу- ам т ритм., уинажая4го на уауанншх мнежигти.
9mcm. кш cwcjcum ш^ипим ai.« шншо inirmiucuamuc, преходя nc vctM 'lb шагам. Так. .«шюк шпалмсбатым и/.летшгжи имймассм в с ^miuuu dac-dapadaш: кож¬ ами. менент. массива mjtpju-um^udc клюг инппрцшн - fna, .,mdc нале. Тел и Алтейке ил-it — жаш»п. ннепшшнт нс ашжшгвцшпи на этаж mat. Села клип — с^аагшея .AwDJ-ашпшс. кошеры. gcdaduicmat (! объект Track. if (key != 0) { . Ссмарм сектам Ж)Т1‘_СМ track.add(makeEvent(NOTE_ON, 9, key, 100, i)) ; I. aywTTJTTTicqcda&WtM track. add (makeEvent (NOTE_OFF, 9, key, 100, i + 1)) ; > ил <?с/'мкт. ЪасК. } } } public static MidiEvent makeEvent(int and, int chnl, int onG' *nt two' *nt tick) { MidiEvent event = null; try { ShortMessage msg = new ShortMessage(); msg.setMessage(and, chnl, one, two); event = new MidiEvent(msg, tick); } catch (Exception e) { e.printStackTrace(); ) return event; 9ттиметш юих dantu.ra.iCM dt\a?~ шиле «Тспишм коа» d Htuanauuu tr luidt. Туш сн йене, иякпи^по- dmcjmv.dts ншшшт private void makeTracks(int[] list) ( for (int i = 0; i < 16; i++) { int key = list[i]; )
тУпраж ражнение Какой кос) какому макету соответствует? Перед вами шесть экранов. Пять из них были получены с помощью фрагментов кода, пред¬ ставленных на следующей странице. Ваша задача — выяснить, к какому экрану относится каждый из этих фрагментов. >- Ответы на стр. 573
Фрагменты кода OJFrame frame = new JFrame(); JPanel panel = new JPanel(); panel.setBackground(Color.darkGray); JButton button = new JButton("тэсуги"); JButton buttonTwo = new JButton("ватари"); frame.getContentPane().add(BorderLayout.NORTH, panel); panel.add(buttonTwo); frame.getContentPane().add(BorderLayout.CENTER, button); ©JFrame frame = new JFrame () ; JPanel panel = new JPanel(); panel.setBackground(Color.darkGray); JButton button = new JButton("тэсуги"); JButton buttonTwo = new JButton("ватари"); panel.add(buttonTwo); frame.getContentPane().add(BorderLayout.CENTER, button); frame.getContentPane().add(BorderLayout.EAST, panel); ©JFrame frame = new JFrame(); JPanel panel = new JPanel(); panel.setBackground(Color.darkGray) ; JButton button = new JButton("тэсуги"); JButton buttonTwo = new JButton("ватари"); panel.add(buttonTwo); frame.getContentPane().add(BorderLayout.CENTER, button); OJFrame frame = new JFrame(); JPanel panel = new JPanel(); panel.setBackground(Color.darkGray); JButton button = new JButton("тэсуги"); JButton buttonTwo = new JButton("ватари"); frame.getContentPane().add(BorderLayout.NORTH, panel); panel.add(buttonTwo); frame.getContentPane().add(BorderLayout.CENTER, button); JFrame frame = new JFrame(); JPanel panel = new JPanel(); panel.setBackground(Color.darkGray); JButton button = new JButton("тэсуги"); JButton buttonTwo = new JButton("ватари"); frame.getContentPane().add(BorderLayout.SOUTH, panel.add(buttonTwo); frame.getContentPane().add(BorderLayout.NORTH, panel); button);
По горизонтали: I. Линия, отделяющая один логический блок от другого 3. Область рисования на экране 5. Набор оформления интерфейса 6. Элемент для прокрутки значений 7. Место для ввода данных 10. Маленький элемент интерфейса II. Контейнер для других компонентов 14. Визуальное оформление элементов 15. Элемент, который можно нажать 17. Текст на экране или в кнопке 19. Указатель положения на экране По вертикали: 2. Область, логически выделенная в интерфейсе 4. Свободное пространство между элементами 8. Перечень значений на выбор 9. Группа элементов интерфейса 12. Цвет или изображение под элементами 13. Событие при нажатии 1 б. Эффект затемнения позади элемента 18. Мельчайшая единица изображения 20. Прямоугольная обводка вокруг элемента > Ответы на стр. S74
Решение упражнений Какой кос) какому макету соответствует? (со стр. 570-571) OJFrame frame = new JFrame () ; JPanel panel = new JPanel() ; panel.setBackground(Color.darkGray); JButton button = new JButton("тэсуги"); JButton buttonTwo = new JButton("ватари"); panel.add(buttonTwo); frame.getContentPane().add(BorderLayout.CENTER, button) OJFrame frame = new JFrame(); JPanel panel = new JPanel(); panel.setBackground(Color.darkGray); JButton button = new JButton("тэсуги"); JButton buttonTwo = new JButton("ватари"); frame.getContentPane().add(BorderLayout.NORTH, panel); panel.add(buttonTwo); frame.getContentPane().add(BorderLayout.CENTER, button) OJFrame frame = new JFrame () ; JPanel panel = new JPanel () ; panel.setBackground(Color.darkGray); JButton button = new JButton("тэсуги"); JButton buttonTwo = new JButton("ватари"); frame.getContentPane().add(BorderLayout.SOUTH, panel); panel.add(buttonTwo); frame.getContentPane().add(BorderLayout.NORTH, button); OJFrame frame = new JFrame(); JPanel panel = new JPanel(); panel.setBackground(Color.darkGray); JButton button = new JButton("тэсуги"); JButton buttonTwo = new JButton("ватари"); frame.getContentPane().add(BorderLayout.NORTH, panel); panel.add(buttonTwo); frame.getContentPane().add(BorderLayout.CENTER, button) ©JFrame frame = new JFrame () ; JPanel panel = new JPanel(); panel.setBackground(Color.darkGray); JButton button = new JButton("тэсуги"); JButton buttonTwo = new JButton("ватари"); panel.add(buttonTwo); frame.getContentPane().add(BorderLayout.CENTER, button) frame.getContentPane().add(BorderLayout.EAST, panel);
<5r0l'KpOCC&Opc) (со стр. 572)
1 6. Сериализация и ввод-вывод файлов Сохранение объектов (и текста) Если мне придется I ^ f прочитать еще один файл, забитый данными, — я, наверное, его прикончу. Он же знает, что я умею сохранять целые объекты. Но позволяет ли ^ он мне это делать? Ха! Конечно, НЕТ. Щ Это было бы слишком просто. Ничего... г Посмотрим, как он запоет, ^ когда я... / Объекты можно сериализовать и десериализовать. У объектов есть состояние и поведение: пове¬ дение описано в классе, а состояние хранится в каждом конкретном объекте. Так что же происходит, когда приходит время сохранить состояние объекта? Если вы пишете игру — вам нужна кнопка «Со- хранить/Загрузить». Если вы создаете приложение для работы с диаграммами — вам нужна возмож¬ ность «Сохранить/Открыть файл». Если программе нужно сохранить состояние, это можно сделать сложным способом — опрашивать каждый объект, собирать значения всех переменных экземпляра и вручную записывать их в файл в собственном формате. А можно поступить по-умному, в духе объ¬ ектно-ориентированного подхода: просто «заморозить» (сериализовать) сам объект и затем вос¬ становить (десериализовать) его, чтобы вернуть в точное состояние. Конечно, бывают случаи, когда без сложного пути не обойтись — например, если нужно сохранить файл в формате, который может открыть другое (не-Java) приложение. Поэтому в этой главе мы рассмотрим оба подхода. И поскольку любые операции с файлами могут завершиться ошибкой, мы заодно разберемся, как правильно об¬ рабатывать исключения при работе с вводом-выводом.
-Ловим ритм Вы создали идеальный ритмический рисунок и теперь хотите сохранить. Конечно, вы могли бы взять лист бумаги и записать все вручную... но вместо этого нажимаете кнопку «Сохра нить» (или выбираете пункт «Сохранить» в меню «Файл»). Затем вводите имя файла, выбираете папку — и облегченно вздыхаете, зная, что ваше творение не исчезнет из-за очередного сбоя компьютера. Bass Drum vJ в G В Java-программах существует много спосо¬ бов сохранить состояние, и ваш выбор будет зависеть от того, как именно вы собираетесь использовать сохраненные данные. В этой гла¬ ве мы рассмотрим несколько таких подходов. А начнем с самого простого случая: если данные будут использоваться только той Java-программой, которая их создала: © Использование сериализации ОР«пН1-Н** Acoustic Sow* Crash Cymbal Hand Clap High Tom HI Bongo Maracas Whisll« Cow Conga Cowbell Vibras'ap cow-mid Tom High Kgo9° O^nHiConga ^gm бр о У ж' с О о ^ " А О - а п О, • J - ЛшДм б ш А о _ о ® о в a в о I ШгГ- пМ о о Ъа% ' в в п I®' Ш1 - с ", агЛ - -я$Р 1 U - г. G ' о П -> - - ' ‘ 1 - Создайте файл, в котором будут храниться сериализованные объ¬ екты — то есть объекты, преобразованные в поток байтов. Позже программа сможет прочитать эти данные и восстановить из них объекты — снова «живые и дышащие», которые займут свое ме¬ сто в памяти и продолжат работу, как будто никуда не исчезали. Если ваши данные будут использоваться другими программами: ® Запишите простой текстовый файл Создайте файл с разделителями, которые смогут обработать другие программы. Например, это может быть табличный файл с табуляцией, который легко открыть в электронных таблицах или импортировать в базу данных. Это, конечно, не единственные способы. Но если бы нам пришлось выбрать два подхода к работе с вводом-выводом в Java, мы бы остановились именно на этих. Разумеется, вы можете сохранять данные в любом формате, который вам удобен. Например, вместо записи символов можно записывать байты, или сохранить данные как примитивные типы; Java предоставляет методы для записи целых чисел, длинных чисел, логических значений и других простых типов. Но независимо от выбранного формата, основные принципы ввода-вывода одинаковы: вы за¬ писываете данные куда-то — обычно в файл на диске или в поток данных, который идет через сетевое соединение. А чтение — это просто тот же процесс, только в обратном направлении: вы читаете данные либо из файла, либо из сети. Все, о чем мы говорим в этой части главы, от¬ носится к случаям, когда вы не используете полноценную базу данных.
Сохранение состояния Представьте, что у вас есть программа — например, фэнтезийная приключенческая игра, которую нельзя пройти за один сеанс. По ходу игры персонажи стано¬ вятся сильнее, слабее, умнее... собирают, используют и теряют оружие. Разумеется, вы не хотите начинать все сначала каждый раз, когда запускаете игру. Вы вложили слишком много времени и усилий, чтобы подготовить героев к великой битве! Значит, нужно сохранять со¬ стояние персонажей — и, что не менее важно, восста¬ навливать его, когда игрок возвращается к игре. А по¬ скольку вы не просто игрок, а еще и разработчик, вы хотите, чтобы этот процесс был максимально простым и надежным. © Вариант первый Записать три сериализованных объекта персонажей в файл Создайте файл и запишите три сериализован¬ ных объекта персонажей. Если попробовать открыть этот файл как обычный текст, в нем не будет никакого смысла — это не читаемые строки, а байтовое представление объектов: " isrGameCharactef %ge8MtnpowerIJ ava/ tang/String; [weaponst pjava/lang/String; xp2tlfux [Ij ava. lang. String; *•“VAB {GxptnyKtx e4tnbutbnasq~"tTpojuibuq~tKynaKHt Tonopsq "TtMiirHq'trm 1ппшптЬ|втдд»тт1. ® Вариант второй Записать состояния персонажа в текстовый файл Создайте файл и запишите в него три стро¬ ки — по одной для каждого персонажа. Каждая строка должна содержать состояние персонажа, разделенное запятыми: 80, Эльф, лук, меч, пыльца 200,Тролль,кулаки, топор 120,Маг,ааюшжания,невидимость сила: /1А- mint. Тьслм Kit. mono] GameCharacter int power String type Weapon 0 weapons getWeapon() useWeapon() increasePower() II и т. д. Представьте, что у вас есть три персонажа игры, состояния которых нужно сохранить... CU.M-120 n\un:yttaz [ cfufMut: jaKJMTi чн/шил ней ЛМ0С\ объе^ (ijuia iuA&iHHtm файл а Пали сложна 'ft, 1( С( K\J. Н( алм ваше U IXtWZfUUl и и afvtAfi nfu («,( (й «л {чини) восстановите трвссъекта и; ефшьшлщыи. иле ю /теним мшпний переломных оЛекта. сещминнь\х в текстовом файле. Ланриееер, пнеаставьте все снесет, как можно слитйнс твттат/, метким внеправитиле нермаке/ Тин мажет стате «палщей» вишне шра» а эльф — оШжшле... г
Запись сериализованного объекта в файл Ниже приведены шаги по сериализации (сохранения) объекта. Не нужно сразу все запоминать; в этой главе мы подробно рассмотрим каждый шаг. О о ,1 Jj.nm set нс сиш.итЩ' &лд ' oftnawimuwfKU. Создайте объект FileOutputStream cm, ей 4- FileOutputStream fileStream = new FileOutputStream ("MyGame.ser"); й, Создайте объект ObjectOutputStream ObjectOutputStream os = new ObjectOutputStream (fileStream) ; ^ ш ' i Lln.umu e фмыалсс Псл этом иа^Х InMnunf- ги>тск ванимх ■ 1Л,т алию»**"* 1 v ^,w' {’ mneemnv панкме. емф^ш. Запишите объект os.writeObject(characterOne); os.writeObject(characterTwo); os.writeObject(characterThree) Гспнансмсм ( chkium . но кстс|и». cmia- ктся кМ«£кнмс.ehawckiOnc. chatachxJwc u character! I\m, u мписп&гш u,x n< mp.4i|Ki| <? грай, t /М y(jan\L.sa. Закройте объект ObjectOutputStream os.close () JtiKhti, MKjxfx^atx vcimn Jc шш пшика jjwiwtu. m •wku m fo nc пики 4hcGm ■' •'"suipuMitahI Гм m.M фай ) jaKfmnn автаиатшекч. ' fjf*
Путешествие данных: как потоки передают информацию Потоки подключе^ ния — это соединение с источником или приемником данных (файл, сетевой сокет и т. д.), а цепочные потоки не могут под¬ ключаться самосто¬ ятельно: их нужно подключать к потоку подключения. API ввода-вывода в Java включает в себя два типа потоков данных: потоки подключения — соединение с источниками или приемниками данных, например, файлами или сетевыми сокетами; цепочные потоки — сами по себе они работать не могут, их нужно подключать к другим потокам данных, чтобы они выполняли полезную работу. На практике почти всегда для чего-то полезного нужно не менее двух потоков данных, объединенных в це¬ почку: один поток устанавливает соединение (например, с файлом), а второй предоставляет удобные методы для работы с данными. Почему два? Потому что потоки подключения, как правило, крайне низкоуровневые. Возьмем FileOutputStream — это поток подключения, и он умеет только одно — записывать байты. А мы хотим записывать не байты, а целые объекты! Значит, нам нужен более высокоуровневый, цепочный поток. А почему бы просто не использовать один поток данных, который делает все сразу? Такой, который уме¬ ет и преобразовывать объекты, и записывать их в файл? Ответ — в хорошей объектно-ориентирован¬ ной архитектуре: один класс — одна задача. FileOutputStream отвечает за запись байтов в файл. ObjectOutput St ream умеет преобразовывать объекты в байты, пригодные для записи. Поэтому мы соз¬ даем FileOutputStream (поток подключения), и подключаем к нему Ob j ectOutputStream (цепочный поток). Когда мы вызываем метод writeOb j ect () у Ob j ectOutputStream, объект проходит через него, превращается в поток байтов и передается в FileOutputStream, откуда и записывается в файл. И вот в чем главное преимущество: возможность свободно комбинировать потоки подключения и цепочные потоки дает огромную гибкость. Если бы вы были ограничены одним классом потока данных, вы бы полно¬ стью зависели от разработчиков API, надеясь, что они предусмотрели нужный функционал. А с цепочкой потоков данных вы просто собираете свою собственную, настраиваемую конфигурацию, как из конструктора. Объект сериализуется (преобразуется в плоский формат) записывается в Объект записывается в виде байтов в подключен к ♦ 0110Ю0Ю1Ю11Ю01 Объект ObjectOutputStream (цепочный поток) FileOutputStream (поток подключения) Файл
Что происходит с объектом во время сериализации? Q Объект в куче О Сериализованный объект Объекты в куче хранят свое состояние — то есть значения переменных экземпляра. Именно эти значения придают каждому эк¬ земпляру класса уникальность, отличая его от других объектов того же самого класса. Сериализованные объекты сохраняют значения переменных экземпляра, чтобы за¬ тем можно было восстановить идентичный объект в куче — с тем же самым состоянием, каким он обладал на момент сохранения. Ширина Высота L UU"U' .UU ЗИЛКИМА ются u mpfiKincM d поток дайках. 00100101 01000110 Шохина и (kuxma чжкта, а такт au .шснш удинам (ncoumMtj fmoi км/т), сохраняйm- finfyf>ll^ i /Ж(f,‘*1 Jl-M .MOIACI nvnUlM KiMliK Гтн(! tircmmctfunu cdutnu FileOutputStream fs = new FileOutputStream("foo.ser ObjectOutputStream os = new ObjectOutputStream(fs), os.writeObject(myFoo); Foo myFoo = new Foo(); myFoo.setWidth(37) ; myFoo.setHeight(70) ; (ниш ut ct jtjatu Ti&OutfnutStuam. rmai yrmamdumt соединение cipau- ЛС }k,b ( И fioc.wr. .jamtM псаклющм к нt.uy OfijirtCutfnilStmmi, mcctiякжнс ccuo ganum&imi аспекты. Лаем этого % mam у него, штосу wtiM м mpujatu ажкт науапись.
Но что именно считается состоянием объекта? Что нужно сохранить? Теперь становится интересно. Легко сохранить примитивные значе¬ ния 37 и 7 0. А если объект имеет переменную экземпляра, которая ссылается на другой объект? Или у объекта есть пять переменных экземпляра, которые ссылаются на объекты? Или эти переменные экземпляра объектов, в свою очередь, имеют свои собственные переменные экземпляра? Задумайтесь. Какая часть объекта потенциально уникальна? Пред¬ ставьте, что нужно восстановить, чтобы получить объект, идентич¬ ный сохраненному. Он, конечно, будет иметь другой адрес в памя¬ ти, но нас это не волнует. Все, что нас интересует, — это то, что на куче мы получим объект, который будет иметь то же состояние, что и объект в момент его сохранения. Прокаиай извилины У объекта Саг два экземпляра Что должно произойти, чтобы объект Саг можно было сохра¬ нить так, чтобы потом восстано¬ вить его в исходное состояние? Подумайте, какие данные и в каком виде нужно сохранить внутри объекта Саг. А если в объекте Engine есть ссылка на Carburetor, как это по¬ влияет на процесс сохранения? И, наконец, что именно содер¬ жится внутри массива объектов ПгеП? Что нужно для сохранения объекта Саг?
Когда объект сериализуется, все объекты, на кото¬ рые он ссылается через переменные экземпляра, также сериализуются. И все объекты, на которые ссы¬ лаются эти объекты, сериализуются. И все объекты, на которые ссылаются те объекты, сериализуются... а лучше всего то, что все происходит автоматически! Объект Kennel содержит ссылку на массив Dog[]. В этом массиве хранятся ссылки на два объекта Dog. Каждый из этих объектов, в свою очередь, содержит ссылку на строку и на объект Collar. Строка — это набор символов, а объект Collar содержит одно Сериализация сохраняет весь граф объектов — все экземпляры, на ко¬ торые ссылаются пере¬ менные экземпляра, начиная с самого сери¬ ализуемого объекта.
Нужен сериализуемый класс? Реализуйте интерфейс Serializable Интерфейс Serializable называют маркерным или теговым, потому что в нем нет ни одного метода, который нужно реализовать. Его единственная за¬ дача — пометить класс как сериализуемый. То есть объекты этого класса можно сохранить с помощью механизма сериализации. Если какой-либо суперкласс уже реализует Serializable, то и все его подклассы автоматически считаются се¬ риализуемыми, даже если явно не пишется implements Serializable. (Так устроена работа интерфейсов: если суперкласс уже относится к сериализуемым, то и все его наследники автоматически считаются таковыми.) , . ^ N I г... 6 а ни пслшпшли снам objectOutputStream. wnteObject (mySquare) ; ’Гк(П 0(Wr Saia&sMft — иначе г .. ...tKimKC ( IT|\U .\t\n\Kt (Ч|тиаицаи.ии »\|и?ца.и.ид ,.„™ с ’L,w >«*4Й &*«“»»«*■ ;;Э« import java.io.*; ^ |u),n('Ult ' public class Square implements Serializable { private int width; private int heights $ Эти UnmM*i{( M r° fmjwwW&M"4 AratBntft'fiicAiMitnv jp1ц tinc (дакгпм этого класса .швине public Square(int width, this.width = width; this.height = height; } int height) { { m public static void main(String[] args) Square mySquare = new Square(50, 20) try ( ^nfjMww * иклкльнйли. FileOutputStream fs = new FileOutputStream("foo.ser"); ObjectOutputStream os = new ObjectOutputStream(fs); os.writeObject(mySquare); /t os . close () ; ч catch (Exception ex) ( ex.printStackTrace(); файл fpstf есть, iw*' бткр1 i«m ли cc?<) яе.и ) ) (<м пебекш (fjulUtmtSrmem ъжтш его с потеком пее/к.-ш кни.4. .Пцттле кслшшш годней ) е/'гекта ь пешек а }
Сериализация — это все или ничего. Можете представить, что произойдет, если часть состояния объекта не сохранится корректно? Брр! Даже думать страшно! А если после вос¬ становления у собаки вдруг не будет веса? Или ушей? Или ее ошейник окажется размером 3 вместо 30? Такого допускать нельзя! import java.io.*; private Duck duck = new Duck(); Огиктн Рахс1мщт V ' шть ата шлванн. public class Pond implements Serializable { isKjSp®-1 public static void main(String[] args) { Pond myPond = new Pond(); try { FileOutputStream fs = new FileOutputStream( Pond.ser") ; ObjectOutputStream os = new ObjectOutputStream(fs); os . writeObject (myPond) ; ^ СфЦЯАUJtftnu os.close () ; 'У^Аш Rnd,. } catch (Exception ex) { ^Шжнмая Экл.ццлй- (?fcl ntlHWtmfCb ex. printStackTrace () ; ^Un £ KMUCtPcnd: Либо весь граф объ¬ ектов сериализуется корректно, либо сери¬ ализация завершается с ошибкой. Нельзя сериализовать объект Pond, если объект Duck, на который ссылается его переменная экзем¬ пляра, не сериализует¬ ся (то есть класс Duck не реализует интер¬ фейс Serializable). emiuiu } ) ) public class Duck { // код класса ) 0 нет/ С(тыкт /)шк не ufmi 'ШмичимЮн не huvmsuan мн intfitfmcSamCiladfu. пеэтх wy Т мс пшмлх ahuiiiux&wu ’ cduKtuPcnd nfituxuqun 1 javauio^otSerializableException Duck 3 at pond.main(Pond.]ava.l ) matс kcuz (н Ьуи.шншы т.«и wf \a JDu.fk не ж» (Шпь саушнша.
Неужели все так безнадежно? Я что, теперь в ловушке, если тот, кто писал класс для моей переменной экземпляра, забыл реализовать Serializable? Ну просто ,0 супер... » Если переменную экземпляра не нужно (или не стоит) сохранять, используйте ключевое слово transient. Если вы хотите, чтобы переменная экземпляра была пропуще¬ на в процессе сериализации, сопроводите ее ключевым словом transient. :И1Л( Лиж-anv* сом Hlfto lU 'T^w,uo л тшшмная , мпЛат сшит 1 nxpAHttui ккк menu (venumm сткта. Если у вас есть переменная экземпляра, которую невозможно сохранить, потому что она не сериализуема, вы можете пометить ее ключевым словом transient, и при сохранении объекта эта переменная будет пропущена. — а import java.net.*; class Chat implements Serializable ^transient String currentID; / ^—" “™— String userName; // прочий код Так почему же переменная может быть не сериализуема? Возмож¬ но, разработчик класса забыл добавить реализацию интерфейса Serializable. Или объект зависит от данных, которые актуаль¬ ны только во время выполнения и не подлежат сохранению. Хотя большинство объектов в библиотеках Java сериализуемы, невоз¬ можно сохранить — например, сетевые соединения, потоки вы¬ полнения или объекты, связанные с файлами. Эти объекты зависят от (и специфичны для) определенного сеанса работы программы, на определенной платформе, в конкретной JVM. Когда программа завершит свою работу, нет способа вернуть эти объекты к жизни каким-либо значимым образом; их нужно будет каждый раз созда¬ вать заново.
(Не) дурацкие вопросы ВоИрОС: Если сериализация такая важная, почему она не включена по умолчанию для всех классов? Почему Object не реа¬ лизует Serializable, чтобы все его наследники автоматически были сериализуемыми? 0|/ивеи*: Отличный вопрос! И вот в чем дело: хотя сериализа¬ ция и нужна большинству клас¬ сов, ее не навязывают. Вы сами решаете, стоит ли вашему классу быть сериализуемым. Если бы сериализация была по умолчанию, как бы вы ее отключали? Не будем же мы придумывать интерфейс NonSerializable, чтобы говорить: «Не сохраняй меня!». Интерфейсы говорят о том, что объект может делать, а не о том, чего он делать не должен. Иначе полиморфизм просто запутается. ВоПрОС: Но зачем вообще писать класс, который нельзя сериализовать? Ol/HBeW: Причины есть, хоть и немного. Представьте объект па¬ роля — вы точно не хотите, чтобы он болтался где-нибудь в файле. А может, у вас есть объект, кото¬ рый сам зависит от несохраняемых данных — вроде сетевого соеди¬ нения или потока данных. В таком случае сериализация просто не имеет смысла. ВоПрОС: Если мне достался класс, который не сериализуем, а мне очень нужно, могу я сде¬ лать от него подкласс и сериали¬ зовать уже его? 0|/ивем: Конечно! Если этот «недружелюбный» класс не по¬ мечен как final, смело создавайте сериализуемый подкласс и ис¬ пользуйте его. Бонус: ваш код мо¬ жет работать с типом суперкласса, а вы подставляете туда подкласс. Полиморфизм! Вопрос: А что тогда проис¬ ходит, если мой сериализуемый подкласс наследуется от несери¬ ализуемого суперкласса? О l/HBeWl: Классный вопрос! Когда объект десериализуется (мы обсудим это на следующих страницах), а его суперкласс не сериализуем, вызывается кон¬ структор суперкласса — как при обычном создании объекта. Это важно: состояния несериализуе¬ мого суперкласса восстановлены не будут, только переинициализи- рованы. Так что если нет веской причины держать суперкласс вне сериализации,сериализуемый подкласс — неплохой выход, но стоит подумать. Вопрос: Ого! Я только что по¬ нял кое-что важное... Если пере¬ менная помечена как transient, ее значение не будет сохранено. А что тогда с ней произойдет при восстановлении? Мы же помеча¬ ем переменную transient, чтобы избежать проблем с несериали¬ зуемыми типами... но ведь нам это значение может быть нужно после десериализации!Разве не в этом суть сериализации — со¬ хранить все состояние объекта? 01/ИВвРИ: Да, вы попали в самую суть — это действительно по¬ тенциальная проблема. Но выход есть. Когда объект сериализует¬ ся, переменная, помеченная как transient, действительно пропу¬ скается. И при десериализации она получит значение по умол¬ чанию — то есть null (или 0, false, в зависимости от типа). Все, что было связано с этим полем, исчез¬ нет. А дальше уже зависит от того, насколько эта переменная была важна. У вас есть два варианта: 1. Присвоить ей значение по умол¬ чанию. Это подойдет, если восста¬ новленный объект не зависит от конкретного значения этой пере¬ менной. Представьте собаку — ей нужен ошейник, но если все ошей¬ ники одинаковые, можно просто выдать новый, и никто не заметит подмены.Воскрешенная собака с ошейником «по умолчанию» вы¬ глядит вполне нормально. 2. Восстановить значение вручную. Если, скажем, каждый ошейник уникален — с именем, узором, цветом и секретным запахом колбасы — придется сохранить нужные характеристики отдель¬ но (например, в сериализуемых полях), чтобы при десериализации заново создать идентичный объ¬ ект Collar. Вой р0С: А если у меня два объекта в графе, и оба ссыпают¬ ся на один и тот же объект? На¬ пример, у двух кошек один и тот же хозяин. Получается, он будет сохранен дважды? Я надеюсь, что нет. О \мве<м: Нет, сериализация не такая наивная. Она отслеживает все объекты, которые уже были со¬ хранены, и если.натыкается на тот же самый — просто ставит ссылку. Так что при восстановлении кошки снова будут делить одного и того же хозяина. Все по-честному!
Десериализация: восстановление объектов Главная цель сериализации — сохранить объект так, чтобы позже его можно было восстановить в том же состоянии. Причем это может произойти в другой момент времени, при следующем запуске JVM — и даже в совершенно другой JVM на другой машине! Десериализа¬ ция — это обратный процесс: мы берем сохраненный объект и воссоз¬ даем его в памяти, как будто он никогда никуда и не исчезал. <*е/шал(^аи,ия Создайте объект FilelnputStream FilelnputStream filestream = new FileInputStream("MyGame.ser")> Ч' Ck u,uum CT^folUAMtf flAtf Q Создайте объект ObjectlnjHitStream ObjectlnputStream os = new ObjectlnputStream(fileStream) cal к (fxwMf ъш Kcnu»uu< сиГ1Г ииллк лопиис Ля"' ^ *" A Iuvulmi c^V^n o^Jac 6 Прочитайте объекты a wiw« ttUHV Object one = os.readObject(); Object two = os.readObject() ; Object three = os.readObject() Приведите объекты к нужному типу. К0Т шигишгг()%,\ ncujrmnt слшиыж, ,,.Л,С , .S27«« и/ .шшются ь тш ,т пекане, d’mnchau Лали yu tumwu галет. «Wnotf ген <£ло ?а, - <аищипиГ.ипи ,ur;{(i UCKil но, — ’аписа- Ю1НС. GameCharacter elf = (GameCharacter) one; GameCharacter troll = (GameCharacter) two; GameCharacter magician = (GameCharacter) three; Закройте объект ObjectlnputStream os. close () ^ (r 'Aamm rtatK'MjedO f ш/ит"яшма munSl f0 (так Ж. каклвмумс сЛпщШ), мэшлш пЦпе qtWA Uw&rnvu Ш к пиши munu. кб*трми "‘Wini. at* ¥»»« тт* ,и„„„ «Ил* та **■*«• ««*- &«ЛМу«А»т. пи mmf<ymnut а&паматлкеки. Н\£ КИ <Г
Что происходит во время десериализации? В процессе десериализации создается новый объект в куче — с тем же состоянием, которое было у него в момент сериализации. Все перемен¬ ные экземпляра получают сохраненные значения... кроме тех, что были помечены как transient. Такие переменные просто не восстанавлива¬ ются: если это ссылка на объект — будет null, если это примитивный тип — будет значение по умолчанию (0, false и т. д.). Лажис- um> y НС :С№М<Щ НДШПИ (ММ /а сштп игкикгккм*.! Файл Объект считывается побайтово :читывается 011010010110111001 Класс найден и загружен, зна¬ чения переменных экземпляра возвращаются на свои места. подключается к FilelnputStream (поток подключения) ObjectlnputStream (цепочный поток) Объект Q Объект считывается из потока данных. ф JVM определяет тип класса объекта (на ос¬ нове информации, хранящейся вместе с се¬ риализованным объектом). ф JVM пытается найти и загрузить класс объек¬ та. Если JVM не удается найти и/или загру¬ зить класс, возникает исключение, и десери¬ ализация не удается. Q Для нового объекта выделяется место в куче, но конструктор сериализованного объекта НЕ выполняется! Очевидно, если бы кон¬ структор выполнялся, это восстановило бы состояние объекта в его первоначальное «новое» состояние, а нам этого не нужно. Мы хотим, чтобы объект был восстановлен в том состоянии, в котором он был при сери¬ ализации, а не в том, когда он был впервые создан.
ф Если в дереве наследования объекта есть класс, который не сериализуем, то при де¬ сериализации будет выполнен конструктор этого несериализуемого класса, а также всех его суперклассов, даже если они сами сери¬ ализуемы. Как только начинается вызов кон¬ структоров, остановить цепочку невозможно. Это значит, что все классы выше первого не¬ сериализуемого будут заново инициализи¬ ровать свое состояние, а не восстанавливать его из сериализованных данных. ф Переменные экземпляра объекта получают значения из сериализованного состояния. Переменные, помеченные как transient, по¬ лучают значение null для ссылочных типов и значения по умолчанию (0, false и т. д.) в случае примитивных типов. (Не) дурацкие еошросы B’OtnpOC: Почему класс не сохраняется как часть объекта? Тогда бы не возникало проблем с тем, что нужный класс не найден. Oi/HBewu Логично звучит — сохранить и объект, и его класс. Но все не так просто. Если бы сериализа¬ ция включала еще и класс, это привело бы к серьезным накладным расходам. Пока вы сохраняете объекты на диск — может, и не страшно. Но сериализация часто используется для передачи объектов по сети, и если бы с каждым сериализованным объектом передавал¬ ся еще и его класс, сеть захлебнулась бы от лишнего трафика. Но! Для сетевых сериализованных объектов есть особый механизм: можно «прикрепить» к сериализо¬ ванному объекту URL, по которому JVM сможет найти нужный класс. Это используется, например, в удален¬ ном вызове методов Java (RMI): вы передаете объект как аргумент метода, а принимающая JVM, если не знает этот класс, может автоматически загрузить его по URL. Вы можете встретить RMI в реальных проектах, но сегодня чаще можно столкнуться с сериализацией в форматах вроде XML, JSON и других человекочита¬ емых вариантов — они тоже передаются по сети, но не требуют от JVM загружать определения классов из внешних источников. Зоирос: А как насчет статических переменных? Они сериализуются? Оииве/и: Нет, статические переменные не сериали¬ зуются. static — это переменная класса, а не объекта. Сериализация же сохраняет состояние объекта, а не всей его иерархии и глобальных переменных. Когда вы восстанавливаете объект, он просто получает то значе¬ ние static-переменной, которое в данный момент есть у класса в JVM. Не то, что было при сохранении. Вывод: Никогда не делайте сериализуемый объект зависимым от переменной, которая static и к тому же может меняться. При восстановлении объекта static-переменная может быть уже совсем другой — и ваш объект окажется в неожиданном состоянии.
Сохранение и загрузка игровых персонажей import java.io.*; public class GameSaverTest { public static void main(String[] args) { (x. ом* никак nt^ccHomw,.. GameCharacter one = new GameCharacter(50, "Эльф", new String[]("лук", "меч", "пыльца"}); GameCharacter two = new GameCharacter(200, "Тролль", new String[]("кулаки", "топор"}); GameCharacter three = new GameCharacter(120, "Mar", new String[]("заклинания", "невидимость"}); // представьте код, который что-то делает с персонажами и меняет их состояние try ( ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("Game.ser")) os.writeObject(one); try { ObjectlnputStream is = new ObjectlnputStream(new FilelnputStream("Game.ser"))• GameCharacter oneRestore = (GameCharacter) is.readObiect(); os.writeObject(two); os.writeObject(three); os.close () ; } catch (IOException ex) ex.printStackTrace (); System.out.println("Тип первого; System.out.println("Тип второго: System.out.println("Тип третьего: + oneRestore.getType()),* + twoRestore.getType()); + threeRestore.getType()); } catch (Exception ex) { ex.printStackTrace() ; °6be*tf
Класс GameCharacter import java.io.*; import java.util.Arrays; public class GameCharacter implements private final int power; private final String type; private final String[] weapons; Serializable { Ъсосвак класс, кспирьш лт иггиш- Йси а 1.я »и«пи^ийгжи.ч ссршсица . ш на псслсатн страниц. Касте- ,яи4/й wj\t\ лн(( нш1 —лыхсставчан etjxta.nuaityw вам, nncciti лажне сы to псэкспЦисш н тиМевапи. public GameCharacter(int power, this.power = power; this.type = type; this.weapons = weapons; String type, String[] weapons) { public int getPower() { return power; } public String getType() { return type; } public String getWeapons() { return Arrays.toString(weapons); 1
Идентификатор версии: важная проблема сериализации Ввод/вывод в Java на самом деле довольно прост, особенно если использовать самые распространенные комбинации потоков под- ключения/цепочных. Но есть один момент, который может вас по-настоящему заинтересовать... Управление версиями критически важно! Если вы сериализуете объект, вам обязательно понадобится его класс, чтобы потом этот объект десериализовать и использовать. Это вроде бы очевидно. Но вот что может быть менее очевидно (и более пугающе): что произойдет, если класс изменится между сериализацией и десериализацией? Ужас, правда? Представьте, вы пытаетесь восстановить объект Dog, но за это время одна из его переменных экземпляра (и это не transient!) изменила тип — была double, стала String. Такой поворот событий нарушает типовую безопасность Java самым серьезным образом. И, к сожалению, это не единственная потенциально опасная пере¬ мена. Подумайте вот о чем... Изменения в классе, которые могут нарушить десериализацию: • Удаление переменной экземпляра • Изменение типа переменной экземпляра (например, double -> String) • Перевод переменной из обычной в transient • Перемещение класса вверх или вниз по иерархии наследова¬ ния • Удаление implements Serializable (у любого класса в графе объектов!) • Перевод переменной экземпляра в static Changes to a class that are usually OK: • Добавление новых переменных экземпляра в класс (суще¬ ствующие объекты получат дефолтные значения для них) • Добавление классов в дерево наследования • Удаление классов из дерева наследования • Изменение уровня доступа (например, с private на public) не мешает десериализации — значения по-прежнему будут при¬ своены переменным экземпляра • Если переменная была transient, а стала обычной, сериализо¬ ванные ранее объекты просто получат значение по умолча¬ нию для этой переменной при восстановлении Вы пишете класс bog 101101 101101 10101000010 1010 10 о 01010 1 1010101 10101010 1001010101 Dog.class (5) Вы сериализуете объект Dog, используя этот класс Объект (снкт н fc|UUtW Вы меняете класс Dog 101101 1J 101101 101000010 1010 10 о 01010 1 100001 1010 0 00110101 1 о 1 ю ю Dog.class «сил плассл т (7) Вы десериализуете объект Dog, используя изменен¬ ный класс С( кк»п nfi-utwH &JUWU.; 101101 101101 ^ 101000010 1010 ю о 01010 1 100001 1010 0 00110101 1 О 1 10 10 Dog.class 0 Сериализация не удалась! По словам JVM: «Старую собаку новому коду не научишь».
Поле serialVersionUID Каждый раз, когда объект сериализуется, он (вместе со всеми объектами в своем графе) получает «штамп» с версией класса. Этот штамп — специальное число под названием serialVersionUID. JVM вычисля¬ ет serialVersionUID на основе структуры класса: полей, методов, сигнатур и т. д. Когда объект десериали¬ зуется, JVM сравнивает serialVersionUID в классе и в сериализованных данных. Если они не совпадают — десериализация не получится. JVM решит, что это два разных класса, и выбросит исключение. Но есть хорошая новость: вы можете взять это под контроль! Если есть хоть какая-то вероятность, что структу¬ ра вашего класса может измениться, — добавьте serialVersionUID вручную. Когда Java пытается десериализовать объект, она сравни¬ вает serialVersionUID сериализованного объ¬ екта с serialVersionUID текущего класса, который загружен в JVM. Например, если экземпляр Dog был сериализован с идентификатором 23 (на самом деле serialVersionUID — это длинное число), то при десе¬ риализации JVM первым делом сравнит сохраненный ID с ID текущего класса Dog. Если эти значения не совпа¬ дают, JVM решит, что класс несовместим с сериализо ванным объектом, и выбросит исключение при попытке восстановления. Как избежать этой проблемы? Просто включите serialVersionUID вручную в свой класс. Тогда, даже если структура класса немного изменится, ID останется прежним — и JVM скажет: «Окей, все в порядке. Класс подходит под этот сериализованный объект». Но внимание! Это работает только если вы действитель¬ но осторожны с изменениями класса. Вы берете на себя ответственность: если старая версия объекта не сможет корректно работать с новой версией класса — баги ваши. Чтобыузнать serialVersionUID текущего класса, воспользуйтесь утилитой serial ver — она входит в со¬ став JDK. % serialver Dog Dog: static final long serialVersionUID = -5849794470654667210L; Если вы предполагаете, что ваш класс может измениться после сери¬ ализации объектов на его основе... (?) Выполните в командной строке ко¬ манду serialver, чтобы узнать иден¬ тификатор версии своего класса. File Edit Window Help sanalKiliar % serialver Dog Dog: static final long serialVersionUID = -5849794470654667210L; - )\ швш hi ю тшл Ш1Ш jt nurnnii cm ш 6k fam, кипсеи nmtvuywtmc. это maienut. .ii(шш cmAutatmcfi. @ Вставьте результат в ваш класс. public class Dog { static final long serialVersionUID = -5849794470654667210L; private String name; private int size; // код метода ) (5) Помните: внося изменения в класс, вы берете на себя ответственность за их последствия! Например, если вы обно¬ вили класс Dog, убедитесь, что новая версия сможет корректно обработать старую — ту, которая была сериализо¬ вана до появления новых переменных. При десериализации такие поля полу¬ чат значения по умолчанию, и ваш код должен учитывать это.
Сериализация объектов Без лишних «лов ■ Состояние объекта можно сохранить, сериализовав его. ■ Для сериализации используется класс ObjectOutputStream из пакета java.io. ■ Потоки данных бывают двух типов: подключения и цепочные. • Потоки подключения работают напрямую с источниками или полу¬ чателями данных: файл, сокет, консоль и т. д. ■ Цепочные потоки не могут подключаться к источникам напрямую: они подключаются к другим потокам данных. ■ Для сериализации объекта в файл создайте объект FileOutputStream и подключите его к ObjectOutputStream. ■ Чтобы сериализовать объект, вызовите метод writeObject(theObject) на объекте ObjectOutputStream. Не нужно вызывать методы на FileOutputStream. • Чтобы объект можно было сериализовать, его класс должен реали¬ зовывать интерфейс Serializable. Если суперкласс уже сериализуем, то и подкласс будет сериализуемым — даже без явного указания implements Serializable. • При сериализации сохраняется весь граф объекта — то есть не только сам объект, но и все, на что он ссылается (и далее по цепочке). ■ Если хоть один из объектов в графе не сериализуем, возникнет исключение во время сериализации — если только переменная не помечена как transient. ■ Хотите исключить переменную из сериализации? Пометьте ее как transient. При десериализации она будет восстановлена как null — если это объект, либо как значение по умолчанию — если примитив. ■ Во время десериализации классы всех объектов, участвующих в гра¬ фе, должны быть доступны JVM. • Объекты считываются (с помощью readObject()) в том порядке, в ко¬ тором они были изначально записаны. ■ Метод readObject() возвращает тип Object, поэтому нужно приводить его к нужному типу вручную. ■ Статические переменные не сериализуются! Их нет смысла со¬ хранять как часть состояния конкретного объекта, потому что все экземпляры класса разделяют одно общее значение — то, которое хранится в самом классе, а не в объектах. • Если класс, реализующий Serializable, может со временем меняться, обязательно добавьте в него поле: static final long serialVersionUID Этот идентификатор определяет версию класса. Его следует менять, если вы изменяете сериализуемые переменные — чтобы контроли¬ ровать совместимость при десериализации.
Запись строк в текстовый файл Сериализация — это самый простой способ сохранить или восстановить объект между запусками Java-программы. Но бывают ситуации, когда нужно не сериализовать объект, а про¬ сто записать данные в текстовом виде — чтобы потом кто-то другой (возможно, вообще не на Java) смог эти данные прочи¬ тать и использовать. Представьте: у вас есть сервлет — код на Java, который работает на веб-сервере. Он получает данные из формы, введенные пользователем в браузере, и должен сохра¬ нить их в обычный текстовый файл. Этот файл потом может быть загружен, например, в Excel или в систему аналитики. Запись текстовых данных (то есть строк) выполня¬ ется почти так же, как запись объектов, но вместо Obj ectOutputStream вы используете FileWriter, а не FileOutputStream. Вы просто записываете строки одну за другой — и никакой сериализации! Для записи сериализованного объекта: objectOutputStream.writeObject(someObject); Лет тдк„шяут б’мг.шшп» аанньи. игрс&яю пуханажа, сеж сохранить ид (? жнигмти тетпебмй !paiu. 50, Эл ьф, лу к, меч, пыл ь ца 200,Тролль,кулаки,топор 120,Маг,заклинания,невидимость Для записи строки: fileWri ter.wri te("Сохраняемая строка"); import java.io.*; f iava-ic- class WriteAFile { public static void main(String[] args) { try { /ела ф<и'' J ((,t\V-"'ti'' ra^"vCfm ( C0|c|rtCrt'- FileWriter writer = new FileWriter("FooTtxt"); 1Ш.ЖНС ич v writer. write ("приветите!"); paTU&imU’tt^/cifteK! андиакжлт 6i\Aunl Д 7 xo|\lkn! Men \writer. close () ; ^ ) catch (IOException ex) { ex.printstackTrace(); } muf WY/hO приминает ctnjuMj. > nun nitiul J
Пример текстового файла: электронные карточки Помните обучающие карточки из школьных времен? На одной стороне — вопрос, на другой — ответ. Они не помогали понять, но отлично работали, когда нужно было запомнить факт: даты, формулы, термины. Такие карточки — суперполезный инструмент для механиче¬ ского заучивания и быстрых викторин. Теперь мы создадим электронную версию этих карто¬ чек. Она будет состоять из трех классов: 1. QuizCardBuilder — простое средство для созда¬ ния и сохранения набора электронных карточек. 2. QuizCardPlayer — программа, которая может за¬ гружать набор карточек и показывать их пользователю. 3. QuizCard — простой класс, представляющий данные карточки. Мы разберем код конструктора и про¬ игрывателя карточек, а сам класс QuizCard вы создадите самостоятельно, используя следующее...: — = стенопа £ Какажшжтшы иаэсоаится южнее jDtm^evema (иеттп уЯатшлн)? ЙЙС> Обратная сторона • •• File Quiz Card Builder Question: В каком университете разворачиваются события фильма «Умница Уилл Хан- тинг»г МТИ QuizCard QuizCardjq, а) question answer ^анасра (Онтарио) getQuestionQ getAnswer() QuizCardPlayer File — ваются события фильма «Умница Уилл Хантинг»? QuizCardBuilder Есть меню Файл с командой Сохранить для сохранения текущего набора карточек в текстовый файл. QuizCardPlayer Есть меню Файл с командой Загрузить для загрузки набора карточек из текстового файла.
Конструктор карточек викторины ('структура кода) { (1ЛКшп u ctnoSaMcatm графи****** гЛлпсеШИКсв (WMWUMl. public class QuizCardBuilder public void go() { ccjfuW // Построить и отобразить графический интерфейс ^ аи$шдм катежа»-^ I т private void nextCard() { Жс1иШЛ01\£4 всписок, <-> <] // Добавить текущую карточку в список // и очистить текстовые поля } private void saveCard() { // Открыть диалоговое окно сохранения файла // Пользователь вводит имя и сохраняет набор карточек лЖЙЖГ ** «мм»*. «Нем списке, как ка!тигки $ ШКЧ 'Механика» «/,,/.р/' (н«тешк «Квантовая ' * «JtfvieLa Java» и т. п.1 } private void clearCard() { // Очистить текстовые поля (вопрос и ответ) ('*ши#ип uLKUiiat,f/“Jketiiitl ПКирвате.н впеша- tUH Ш т<Г^в,тню«(Ра^ Htn к private void saveFile (File file) { // Пройтись по списку карточек // и сохранить каждую в текстовый файл в понятном формате // (с четкими разделителями между вопросами и ответами) f/y,f WV и кацтсгке. Бшавмтм с(гш£п\тксл< SaLikw/j'ittntr. бташжт фак numcKfc jamn KOjuncttK в фене t. ) Ввод-вывод в Java: I/O, N10 и N10,2 Java API поддерживает ввод-вывод с самых первых версий, еще с прошлого тысячелетия. Но все изменилось в 2002 году, когда вышла Java 1.4— вместе с ней появился новый подход к вводу-вы¬ воду, названный N10 (non-blocking I/O, то есть неблокирующий ввод-вывод). Затем, в 2011 году, с выходом Java 7, N10 получил серьезные улучшения, и эта версия стала известна как N10.2. Почему это важно для вас? Потому что, когда вы пишете новый код, лучше использовать современ¬ ные и удобные возможности, которые предлагает N10.2. Но почти наверняка вы столкнетесь с уже существующим кодом, где используется более старый подход, — и вам нужно понимать его тоже. Поэтому в этой главе: • Сначала мы познакомимся с классическим вводом-выводом • А потом рассмотрим некоторые особенности N10.2 Больше про ввод-вывод, NIO и N10.2 вы узнаете в главе 17, где мы будем говорить о работе с сетью.
import javax.swing. *; import java.awt.*; import java.io.*; import java.util.ArrayList; public class QuizCardBuilder { private ArrayList<QuizCard> cardList = new ArrayListO () ; private JTextArea question; private JTextArea answer; private JFrame frame; public static void main(String[] args) { new QuizCardBuilder() .go (); } В течение следую¬ щих восьми страниц (или около того) мы будем использовать ввод-вывод старого образца! public void go() { frame = new JFrame("Создание карточек"); JPanel mainPanel = new JPanel (); Font bigFont = new Font("sanserif", Font.BOLD, 24); question = createTextArea(bigFont); JScrollPane qScroller = createScroller(question); answer = createTextArea(bigFont); JScrollPane aScroller = createScroller(answer); mainPanel.add(new JLabel("Вопрос:")); mainPanel.add(qScroller); mainPanel.add(new JLabel("Ответ:")); mainPanel.add(aScroller); JButton nextButton = new JButton("Следующая карточка"); nextButton.addActionListener(e -> nextCardO); mainPanel.add(nextButton); JMenuBar menuBar = new JMenuBar(); JMenu fileMenu = new JMenu ("Файл") ; JMenuItem newMenuItem = new JMenuItem("Новый набор"); newMenuItem.addActionListener(e -> clearAllO); JMenuItem saveMenuItem = new JMenuItem("Сохранить"); saveMenuItem.addActionListener(e -> saveCardO); fileMenu. add (newMenuItem) ; fileMenu .add(saveMenuItem) ; menuBar. add (fileMenu) ; frame.setJMenuBar(menuBar); frame.getContentPane().add(BorderLayout.CENTER, mainPanel) frame.setSize(500, 600); frame.setVisible(true); fief” Кнопка «Сьшukhm/w Kj!rVi,w (stux№ ж&шп.. mmWtwctCajd^ ' I 4шнкпш «КоАяй KftMOH- сСаггЛт). cjn «ихраишш»' Jfflacu MH£u.WHM,gc- на меню «Файл» на V* 11 шт» JDcJaf • < «линн в мне и.ишю Ли!7ШГМг и тЧ П(,«лм Лаж-айн пункт ■^!nn/vM<yn и HfuHanu
private JScrollPane createScroller(JTextArea textArea) { JScrollPane scroller = new JScrollPane(textArea); scroller.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS); scroller.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER)- return scroller; > private JTextArea createTextArea(Font font) ( JTextArea textArea = new JTextArea(6, 20); textArea.setLineWrap(true); textArea.setWrapStyleWord(true); textArea.setFont(font); return textArea; } ««тпсмг/ш L, "**Lmcwuoeh private void nextCard() ( QuizCard card = new QuizCard(question.getText(), answer.getText()); cardList.add(card); clearCard(); ) private void saveCard() ( QuizCard card = new QuizCard(question.getText(), answer.getText<)); cardList.add(card); JFileChooser fileSave = new JFileChooser(); fileSave.showSaveDialog(frame); saveFile (fileSave.getSelectedFile ()); ^—- ! private void clearAll() cardList clearCard ) id clearAllO { e/c, fa.X «ftoCWO- .dearO; ^ «1* f^ [ d,,i S 22.“—*—* L-A-P-EJRSS fa: private void clearCard() { question.setText (""); answer.setText (""); question.requestFocus(); ) <4f-“ jf* ) otfbtKW BoWM/ private void saveFile(File file) try ( BufferedWriter writer = new BufferedWriter(new for (QuizCard card : cardList) ( writer .write (card. getQuestion () + ■•/") writer.write(card.getAnswer () + "\n")- ) writer.close (); } catch (IOException e) ( System.out.println("He удалось сохранить ) пивноат ,и ^гит/, "э тс iter (flip и. ^ ’ f список карточек: V + е•getMessage()); I
Класс] javajo.File Класс java. io. File — это еще один представитель старой школы в Java API. Позже его «заменили» двумя более современ¬ ными классами из пакета j ava. nio.file, но c Fi 1е вы навер¬ няка еще столкнетесь — особенно в старом коде. Рекомендуем для нового кода использовать пакет j ava. nio. file; через несколько страниц мы познакомимся с его самыми полезными возможностями. Но пока — немного о классе File. Класс j ava. io. File представляет файл на диске... но не его содержимое. Звучит странно? На самом деле все просто: объект Fi 1е — это путь, а не файл как таковой. Это может быть путь к файлу или к папке. Важно: у File нет методов для чтения или записи данных! Почему File все еще полезен? Потому что он дает более безопасное и удобное представление файла, чем просто строка с его именем. Например, такие классы, как FileWriter или FilelnputStream, принимают в конструк¬ торе либо строку с путем к файлу, либо объект File — и это куда надежнее. Вы можете создать объект File, проверить, существу¬ ет ли путь и корректен ли он, передать объект File в нужный поток данных (FileWriter, FilelnputStream и т. д.). Объект File предоставляет имя и путь к файлу или каталогу на диске, например: /Users/Kathy/Data/Game.txt Но он НЕ предоставляет и НЕ дает доступ к данным в файле! Некоторые операции, которые можно выполнить с объектом File: © Создайте объект File, предоставля¬ ющий существующий файл. File f = new File("MyCode.txt"); (D Создайте новую директорию File dir - new File("Chapter7"); dir.mkdir(); №«y 73^0 F«o felt Ддиес — это нс тс ж\ самое. шо и сам дам! (ЩктМуэтс кик псгто&т шЦм/. он икаж&шн, час намаипим ipail \, нс нс яклжтсл саммм срайшм. Ось с»ст fife - амл ср«А*1 (f4""J'Ci Lxl (?) Перечислите содержимое каталога. if (dir.isDirectory()) { String[] dirContents = dir.list(); for (String dirContent : dirContents) { System.out.println(dirContent); ) ) (?) Удалите файл/каталог (возвращает true, если успешно). boolean isDeleted = f.delete(); GameFilc.txt 50, Эльф, лук, меч, пыльца 200, Тролль, кулаки, топор 120, Маг, заклинания, невидимость t (>Т*к»п.7#й Я/ tfn|- cma&wtm асжнш Paumluc tpauMi (и нс qM&LMnfuutete (1СС'ПЩПС1 к. ндм*Л
Элегантность буферов в Java Если бы не было буферов, это было бы как поход в ма гаэин без тележки. Вам бы пришлось носить каждую покупку до машины по одной — одну банку консервов, потом рулон туалетной бумаги, потом еще одну банку... метаются пока ’ Т ?тши н« РХП&ЛНШПСЯ. (Г u,hiu г ‘ Строка помещается в буфер вместе с другими строками Когда буфер заполняется, все строки записываются в “Boulder”- записывается в Строка “Boulder” “Aspen” “Denver” подключается к “Aspen Denver Boulder" BufferedWriter (цепоч¬ ный поток, работающий с символами) File Writer (поток под¬ ключения, записывающий символы, а не байты) файл BufferedWriter writer = new BufferedWriter(new FileWriter(aFile)) Работа с буферами гораздо эффективнее, чем без них, Вы може¬ те записывать данные напрямую через FileWriter, вызывая метод write (someString). Hoy FileWriter каждый вызов write () сразу записывает данные в файл, а это довольно затратная операция: запись на диск всегда «тяжелее», чем работа с памятью, Если вы присоедините BufferedWriter к FileWriter, то BufferedWriter сначала будет удерживать данные в памяти, а передаст их в FileWriter только тогда, когда буфер заполнится. Только когда буфер будет полным, FileWriter будет фактически вызван для записи в файл на диске. ( (ратшпе а’ни.шние. те неш нс нижпе сах1тнмтс с'спяки на спскт T/Ci Wwta. ссцснстбснна. гте нас mmtfucuem. — эпи шскт fliifftrum'rttn, пашни тс Именно с тен ят сурм &i- mdamt .метши. Плати т Mucysjtu мкрп&тс. kcicjci /акстим росши. и сн пс уашпшшм с &т оста, иней Чтет : передаст dec аальше иракршп пшок гибкие гения. А если нужно записать данные немедленно, не дожидаясь заполнения буфера? Вы управляете этим сами — достаточно вызвать метод flush (). Вызов writer . flush () можно интерпретировать так: «отправь все, что есть в буфере, прямо сейчас!»
Чтение текстовых файлов Чтение текста из файла — депо несложное, но на этот раз мы бу¬ дем использовать объект File для представления самого фай¬ ла, FileReader — для фактического чтения символов из файла и Buf f eredReader — чтобы повысить эффективность чтения. Чтение будет происходить построчно, в цикле while, который про¬ должается до тех пор, пока метод readLine () не вернет null. Это — один из самых распространенных стилей чтения данных (практи¬ чески любых, кроме сериализованных). Данные читаются построчно в теле цикла (а точнее — в условии этого цикла), а признаком оконча¬ ния служит null — это значит, больше строк нет. Почти все, что читается построчно (а не как сериализованный объект), читается именно так. import java.io. )tvfft ft,» ^мГ'ГеаМ Файл c cj&fMM стЦскамн текста О — Сколько будет 2 + 2?/4 Сколько будет 20+22/42 class ReadAFile { public static void main(String[] args) { try { File myFile = new File("MyText.txt"); FileReader fileReader = new FileReader JF IVaHiXi c (myFile) Ti эжс л /си s,t , ся к тексни Аиу cpak.u/ * Buff eredReader reader = new Buf f eredReader (fileReader•); aUk String line; Дль^о Jdaa while ((line = reader.readLine()) ! = null) { / if / ^K ' яи0|цтдн«). Sys tem.out.println(line) } reader.close(); catch (IOException e) { e.printStackTrace() ; m ’ . ,un ftp Этот срышимт cmaram: «Сттапч и afut( (ц tdu ее стрскобсн нем, мениск ?тс. Жска эта тмшшпА ш пека -< т£и (/именная не равна пи£Р<т„е„ тс штате). Мсаить — ест/, гимн слс&ши: «Лека ест/стмки - с/ти нс». II ш. шпате Потоки данных и ввод-вывод в Java 8 Если вы используете Java 8 или новее и уверенно чувствуете себя с по¬ токовым API (Streams), можно заменить весь код внутри блока try вот на такую компактную запись: Files.lines(Path.of("MyText.txt")) .forEach(line -> System.out.println(line)); Мы рассмотрим классы Files и Path позже в этой главе.
Проигрыватель карточек викторины ("структура кода) public class QuizCardPlayer ( public void go() { // Построить и отобразить графический интерфейс } private void nextCard() { // Если сейчас отображается вопрос — показать ответ, // иначе — показать следующий вопрос // Устанавливаем флаг: смотрим вопрос или ответ } private void open() { // Открыть диалоговое окно выбора файла // Позволить пользователю выбрать набор карточек } private void loadFile(File file) { // Построить список карточек (ArrayList), считав строки из текстового файла // Метод вызывается обработчиком события OpenMenuListener // Файл читается построчно, а каждая строка передается методу makeCardO // В одной строке содержатся и вопрос, и ответ, разделенные символом "/" ) private void makeCard(String lineToParse) { // Метод вызывается из loadFile // Делит строку на две части — вопрос и ответ // Создает объект QuizCard и добавляет его в список cardList
import javax.swing.*; import java.awt.*; import java.io.*; import java.util.ArrayList; public class QuizCardPlayer { private ArrayList<QuizCard> cardList; private int currentCardlndex; private QuizCard currentCard; private JTextArea display; public void go() { frame = new JFrame("Пройди викторину"); JPanel mainPanel = new JPanelO; Font bigFont = new Font("sanserif", Font.BOLD, 24); display = new JTextArea(10, 20); display.setFont(bigFont) ; display.setLineWrap(true); display.setEditable(false) ; JScrollPane scroller = new JScrollPane(display); scroller.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS); scroller.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER) mainPanel.add(scroller); nextButton = new JButton("Показать вопрос"); nextButton.addActionListener(e -> nextCardO); mainPanel.add(nextButton) ; JMenuBar menuBar = new JMenuBarO; JMenu fileMenu = new JMenu ("Файл") ; JMenuItem loadMenuItem = new JMenuItem("Загрузить набор"); loadMenuItem.addActionListener(e -> open()); fileMenu . add (loadMenuItem) ; menuBar. add (fileMenu) ; frame.setJMenuBar(menuBar); private JFrame frame; private JButton nextButton; private boolean isShowAnswer; J(a этой cmjncimuijt пишко public static void main(String[] args) ( QuizCardPlayer reader = new QuizCardPlayer(); reader .go () ; frame.getContentPane().add(BorderLayout.CENTER, mainPanel); frame.setSize(500, 400); frame.setVisible(true);
private void nextCard() { if (isShowAnswer) { // показываем ответ, потому что вопрос уже был display.setText(currentCard.getAnswer()) ; nextButton.setText("Следующая карточка") ; isShowAnswer = false; ) else { // показываем следующий вопрос if (currentCardlndex < cardList.size()) { showNextCard(); ) else { // карточки закончились! display.setText("Это была последняя карточка" nextButton.setEnabled(false); показан Л/и верж и лсгиткий флаг uShtmA*пт, mwcfuLaeaum т < Ш1т^(тт,т тльЛ£м всит- шимашнт вопрос Lи ответ { 2,- ) ; } } } private void open() ( JFileChooser fileOpen = new JFileChooser () fileOpen . showOpenDialog (frame) ; loadFile (fileOpen . getSelectedFile () ) ; private void loadFile(File file) { cardList = new ArrayListo () ; currentCardlndex = 0; try ( Co; An (itoot 'i 1ДИНМИ ukt BufferedReader reader = new BufferedReader(new FileReader(file))• String line; —«. while ((line = reader. readLineO) !=null) ( l СгШВ&К*'^ makeCard (line) ; > ,цк, mMM I J 3WaAfnv«.^rtvootucm reader. close () ; Iq*j \tz(xad к ехМлят. aH c«wcok. catch (IOException e) ( ® System.out.println("He удалось записать список карточР!,- " , ' + e-getMessage()); 1 showNextCard() ^ Jaopt .можно начинать! Яска- ровае-и. первую карточку. private void makeCard(String lineToParse) ( String[] result = lineToParse.split ("/"); QuizCard card = new QuizCard(result[0], result[l]); cardList.add(card); System.out.println("Создана карточка"); > private void showNextCard() ( currentCard = cardList.get (currentCardlndex); currentCardIndex++; display.setText(currentCard.getQuestion()); nextButton.setText ("Показать ответ"); isShowAnswer = true; ) Жажаая спхрска токгтпд cocm&nv- cmmpn (мной, карточке, но нижэдо рамыиЛь, feniwr ц ответ Jem <рльна* части. Я оя этого итющцш ■мот uiStumf&tO, котами \\аР t JyP ТЧН«ГМ paocucn\fmtfMmafsfi&tO ня cmuie- “WioiywHiuv. J 1 iT )
Раз бор строк с помощью Stmg.splitf) Представьте, что у вас есть карточка с вопросом и ответом, например: Что пол смешат и желт? 1 учится, если ь синий ^ L зеленый Как разделить вопрос и ответ? В считываемом файле вопрос и ответ находятся в одной строке, разделенной косой чертой / (именно так мы записали файл в коде QuizCardBuilder). Сохраняется в файле с вопросами вот так: Что получится, если смешать синий и желтый?/зеленый Что получится, если смешать крас¬ ный и синий?/пур- пурный Метод String split() позволяет легко разбить строку на части. Он работает так: «Дай мне разделитель, и я разделю эту строку на части и помещу их в массив строк». токен 1 :ring toTest = "Что получится, разделитель токен 2 вш.<*Г,пС,Ч 1 w ит*«И,йли'; если смешать синий и желтый?/беленый"; QuizirndPCautt а онл cmmnm- Stringf] result = toTest.split("/"); for (String token : result) { System.out.println(token); 1 строки на (№ часшк - ваанном cAftat am пикша бЬукг н CHiftm. JFpimtwkue: spditO — эпиисш.нгш инстр1ршмт. кстсрпй ®шн услат гсрмр Алл ше. С'п .ш.тт выполнят* сшжлши рарсЦх Лирик с ери. шпрами, пс^стансСсгнтииршк/tuu м т. у и с. шишки сан нм м жи т*шЛ м умений» 4 лучатся.
(Не) дурацкие вопросы ^ОПрОС: Ну ладно, я заглянул в докумен¬ тацию API, и там кажется, миллион классов в пакете java.io. Как вообще понять, какие из них использовать? Ответ Ввод-вывод в Java построен на модульной концепции цепочек: вы можете со¬ единять потоки подключения и цепочные потоки (их еще называют фильтрующими) в разных комбинациях, чтобы получить нужный результат. Цепочка может состоять не только из двух уров¬ ней — можно соединять несколько цепочных потоков подряд, если это нужно для более слож¬ ной обработки. Но обычно все гораздо проще. Например, если вы работаете с текстовыми фай¬ лами, вам почти наверняка хватит BufferedReader и BufferedWriter вместе с FileReader и FileWriter. А если работаете с сериализованными объ¬ ектами, используйте ObjectOutputStream и ObjectlnputStream в связке с FileOutputStream и FilelnputStream. Проще говоря, 90% всего, что может понадобиться для работы с I/O в Java, вы уже знаете. Во 10рОС.' Только что вы сказали, что мы уже знаем 90% информации про работу с I/O в Java, но где же тогда этот самый N10.2? Почему его еще не было? Ответ До него мы доберемся уже на следующей странице! Хотя N10.2 — мощный инструмент, для обычного чтения и записи тек¬ стовых файлов BufferedReader и BufferedWriter по-прежнему остаются самыми удобными. Мы покажем, как N10.2 может сделать работу с ними еще проще, &0Ир0С: Мой мозг уже начинает плавиться... Я слышал, что N10.2 — это безумно сложно. Ответ: Не переживайте. Мы остановимся только на нескольких ключевых компонентах пакета java.nio.file, без перегрузки лишними деталями. Без лишних «лов ■ Чтобы записать текстовый файл, начните с по¬ тока подключения FileWriter. “ Для большей эффективности подключите к нему BufferedWriter. ■ Объект File представляет файл по указанному пути, но сам файл при этом не создается и не содержит данных. ■ С помощью объекта File можно создавать и удалять файлы и каталоги, а также проверять их наличие. ■ Большинство потоков данных, которые прини¬ мают имя файла в виде строки, также принима¬ ют и объект File; часто это более безопасный и гибкий способ. ■ Чтобы прочитать текстовый файл, начните с по¬ тока подключения FileReader. ■ Для повышения производительности подключи¬ те к нему BufferedReader. в Если вы собираетесь анализировать текстовый файл, убедитесь, что он записан в читаемом формате — например, данные разделены каким-либо специальным символом. ■ Метод split() класса String поможет разбить строку на токены (отдельные элементы). На¬ пример, строка с одним разделителем даст два токена — один слева, второй справа. Сам символ-разделитель не включается в результат.
ava.nfo.file N10.2 и паке I Под Java N10.2 обычно имеют в виду два пакета, добавленных в Java 7: java.nio.file java.nio.file.attribute Пакет j ava. nio. file. attribute предназначен для работы с мета¬ данными, связанными с файлами и каталогами. Например, классы из этого пакета позволяют читать и изменять разрешения доступа к фай¬ лам. Но мы не будем в это углубляться. (Уф, вздохнули с облегчением!) А вот пакет j ava . nio . file — это уже интереснее. Он включает все, что нужно для чтения и записи текстовых файлов, а также управления каталогами и их структурой. На практике чаще всего вам понадобятся три типа из j ava. nio. file: ■ Интерфейс Path — используется для представления пути к файлу или каталогу. Почти все начинается с Path, если вы работаете с файла¬ ми через N10.2. ■ Класс Paths — содержит удобный метод Paths. get (), с помощью которого можно создать объект Path. ■ Класс Files — звезда шоу! В нем собраны все основные методы (ста¬ тические), с помощью которых можно: создавать потоки чтения и за¬ писи, работать с файлами и каталогами (создавать, копировать, удалять, перемещать и искать их в файловой системе). Объект Path предоставля¬ ет местоположение (имя и путь) файла или каталога на диске, например: /Users/Kathy/Data/Game.txt Но он НЕ является данными файла и не предоставляет к ним доступ! Продвинутая, но полезная воз¬ можность класса Files позволяет «прогуляться» (выполнить по¬ иск) по древовид¬ ной структуре каталогов. Мини-руководство по созданию BufferedWriter с использованием N10.2 (Т) Импортируйте Path, Paths и Files: import java.nio.file.*; ® Создайте объект Path, используя класс Path Path myPath = Paths.get("MyFile.txt"); Ос&скт Path еЫкш U kchu icftu системе). Щтелижтшт j тиски ipakwfi G Пику w-« a G уfufikr Или, если файл на ходитсяв подкаталоге, например /myApp/files/MyFile.txt: Path myPath = Paths.get("/myApp", "files", "MyFile.txt"), к amd.ACMX‘- (u.u£at/втсптт Ым'нмiGaem- cm otkjvuKwmtiicucmtm fvvqommtu и.щ.н miw (jfrymu: tfahpvmtf}, \. ® Создайте новый BufferedWriter, используя классы Path и Files: TlU (JU f f im 6 шарах кма каксИ-nw .mncq flu- пыняет сящтиш: ' BufferedWriter writer = Files.newBufferedWriter(myPath); ВефшМ'Шг mtta = new/.ЩгееМ'-иШ.)
Path, Paths и Files: работа с каталогами в Java В приложении Б мы обсудим, как разделить Java-приложение на пакеты. Процесс касается создания правильной структуры каталогов для всех файлов приложения. В большинстве случаев вы будете создавать и перемещать каталоги и файлы вручную с помощью командной строки или файлового менеджера, такого как Finder или Проводник Windows. Такие операции можно выполнять непо¬ средственно в Java-коде. Внимание: работа с каталогами в Java может обернуться настоящей головной болью. Чтобы все сделать правильно, нужно разобраться в путях (абсолютных и относительных), разрешениях операционной системы, атрибутах файлов и других деталях. Ниже приведен сильно упрощенный пример, позволяющий составить общее представление о том, как Java работает с каталогами. Допустим, вы хотите создать установщик для своего убийственного приложения. У вас есть струк¬ тура каталогов и файлов (она слева), и нужно получить результат, как показано на рисунке справа. 101101 101101 10101000010 1010 10 о 01010 1 1010101 10101010 1001010101 101101 101101 10101000010 1010 10 о 01010 1 1010101 10101010 1001010101 фтщкгк.и книг .МлП)ГСипгк<ф<- ■ugueroLor* do •liquis do del dip tKaunaiulu^HHtvu ncnofwncK*^ /Hif«,naW)U!UWR6u ' " acqa. Install.class MyApp.class MyMedia.jpeg import java . nio .file ; public class Install { public static void main(String[] args) { try { Path myPath = Paths.get("MyApp"); Path myPath2 = Paths.get("MyApp", "media”); Path myPath3 = Paths.get("MyApp", "source") Path mySource = Paths.get("MyApp.class" ) ; , Path myMedia = Paths.get("MyMedia.jpeg"); 101101 101101 w 10101000010 1010 10 0 01010 1 1010101 10101010 1001010101 MyApp.class 1o tporU iur* do *iiq“** do d-1 MyMedia.jpeg . / mHIW f nfh Files.createDirectory(myPath) ; Files.createDirectory(myPath2) Files.createDirectory(myPath3) Co СЩЛ£М uk кст‘"с,<1 Я/киаижн (pa сракш вcc- ondburmdui Files.move(mySource, myPath3.resolve(mySource.getFileName())); J Files.move(myMedia, myPath2.resolve(myMedia.getFileName())); * catch (Exception e) { System.out.println("Произошла ошибка NIO: " + e.getMessage()); Kfwvacm. )
А теперь подробнее о finally Несколько глав назад мы уже рассматривали конструкцию try-catch-finally, но лишь вскользь упомянули, что блок finally — подходящая локация для размещения «кода очистки». Так и есть, но теперь давайте более детально разберемся, что именно подразумевается под этим понятием. Как правило, когда речь идет о «коде очистки», имеется в виду освобождение ресурсов, которые про¬ грамма использовала и которые были предоставлены операционной системой. Например, при откры¬ тии файла или создании сетевого соединения операционная система выделяет определенные ресурсы, такие как файловые дескрипторы или сетевые сокеты. Если после использования этих ресурсов их не освободить, может возникнуть утечка памяти или потеря доступа к системным ресурсам. Поэтому, когда программа завершает работу с файлом, сетевым соединением или любым другим ре¬ сурсом, его необходимо корректно закрыть, тем самым возвращая обратно операционной системе. Ниже приведен фрагмент класса QuizCardBuilder, в котором выделены вызов конструктора и три отдельных вызова методов, демонстрирующих этот процесс. Ниже выделены ЧЕТЫРЕ позиции, в которых может возникнуть исключение! private void saveFile(File file) { try { BufferedWriter writer = new BufferedWriter(new FileWriter(file)); for (QuizCard card : cardList) { writer.write(card.getQuestion() + writer.write(card.getAnswer() + "\n"); 1 исклюжннс/ writer.closet); } catch (IOException e System.out.println("He удалось записать список карточек: " + е.getMessage()); Bet &.яиыкши .ншш?. ujt .uomrn mmt GncfumtHc ) } Если вызов конструктора FileWriter завершится с ошибкой, если хотя бы один из много¬ численных вызовов write () не выполнится или даже если при вызове close () произойдет сбой — будет выброшено исключение, и JVM немедленно перейдет в блок catch. В этом случае объект writer так и останется незакрытым. Ужас, правда? Не забывайте, блок finally выполняется ВСЕГДА! Поскольку нам ДЕЙСТВИТЕЛЬНО важно убедиться, что записываемый файл закрыт, давайте поместим вызов close () в блок fina 11 у. аточите карандаш -► Решать —вам! Напишите ноЬый блок finally Какие изменения нужно внести в код выше, чтобы пере¬ местить вызов close() в блок finally? Изменений может оказаться больше, чем кажется на первый взгляд.
А теперь подробнее о finally (продолжение) Объем кода, необходимого для перемещения вызова close() в блок finally, удивляет. Давайте разбираться. private void saveFile(File file) { BufferedWriter writer = null; ^ try { writer = new BufferedWriter(new FileWriter(file)); for (QuizCard card : cardList) ( writer.write(card.getQuestion() + writer.write(card.getAnswer() + "\n"); Hclu гуишыос* vdmCmm 'тыки m writa Chl ({лека tiu, mucu она rtua aoemutma C рпаССц. Мштлш nfwcmc не смокли m CtyCamt у ни сСш(). } writer.close(); 1 catch (IOException e) { System.out.println("He удалось записать список карточек: " + e.getMessageО); ) finally { try { writer.close(); ) catch (Exception e) { System.out.println("He удалось закрыть writer: ” + e.getMessage()); Da. нам цишгтСшполтпш- ue tet t осшнипи CtucC cCmO C t ну otjHH СНЖ tty catch! Есть способ получше! В ранних версиях Java именно так приходилось закрывать файл. В старых программах наверняка встретите подобные блоки finally. Но для нового кода есть более удобное решение: Оператор try-with-resources Рассмотрим его далее.
Оператор try-with-resowrces ('TWR) Если вы используете Java 7 или более позднюю версию (а мы очень надеем¬ ся, что это так!), то можете применять оператор try-with-resources, упрощающий работу с вводом-выводом. Давайте сравним обычный блок try, который мы рассматривали ранее, с try-with-resources, выполняющим ту же задачу: private void saveFile(File file) { BufferedWriter writer = null; try { writer = new BufferedWriter(new FileWriter (file) ) ; for (QuizCard card : cardList) { writer.write(card.getQuestion() + writer.write(card.getAnswer() + "\n"); J Cmajxtm nccjxccj, cntjui icakh- ww m f } catch (IOException e) { System.out.println("He удалось записать список карточек: " + е.getMessage()) ; ) finally { try { writer.close(); ) catch (Exception e) { System.out.println("He удалось закрыть writer: " + e.getMessage()); } } private void saveFile(File file) try (BufferedWriter writer = new BufferedWriter(new FileWriter(file))) for (QuizCard card : cardList) { writer.write(card.getQuestion() + "/"); writer.write(card.getAnswer() + "\n"); i Койш Аарштт, omj\a -WttkrtLSOWtCtS moi } catch (IOException e) { System.out.println("He удалось записать список карточек: " + е.getMessageО); } (Не) дурацкие вопросы Вой рос: Подождите-ка! Вы же говорили, что оператор try должен содер¬ жать блок catch и/или finally! Ol/VlBewi: Хороший вопрос! Дело в том, что с оператором try-with-resources компилятор автоматически создает блок finally. Вы его не видите, а он есть.
Интерфейс AwtoCloseable: небольшая загвоздка На предыдущей странице мы рассмотрели другой вариант оператора try — try-with-resources (TWR). Давайте разберемся, как писать и использовать операторы TWR, разобрав следующий код: try (BufferedWriter writer = new BufferedWriter(new FileWriter(file))) { Конструкция оператора try-with-resources ф Добавьте круглые скобки между try и {. try (...) { Как и (la кла/са fiC’uja dnduja. китртлт шш.шшии d этой гла&, SuMaufWtem леалия/ет AutccdcstadCt. * writer.write(card.getQuestion() + writer.write(card.getAnswer() + "\n"); Интерфейс Autocloseable повсюду, где вы работаете с вводом-выводом Autocloseable — это интерфейс, добавленный в j ava . lang в версии Java 7. Почти все операции ввода-вывода основаны на классах, реализующих Autocloseable. А самое приятное — в большин¬ стве случаев вам вообще не придется об этом задумываться. Есть пара моментов, которые стоит знать о TWR (try-with-resources): ■ В одном блоке TWR можно объявлять и использовать более одного ресурса для ввода-вывода: try (BufferedWriter writer = new Buf feredWriter (new FileWriter (file) ) ; ^ BufferedReader reader = new Buf feredReader (new FileReader (file) ) ) ( ■ Если вы объявляете более одного ресурса, они будут закрываться в порядке, ОБРАТНОМ их объявле¬ нию, то есть первым закроется тот, который был объявлен последним. ■ Если добавить блоки catch или finally, система сможет корректно обрабатывать несколько вы¬ зовов close (). ® В круглых скобках объявите объект, тип которого реализует **Autocloseable**. ^— try (BufferedWriter writer = new Buf feredWriter (new FileWriter (file) ) ) ( ® Используйте объект, который вы объявили внутри блока try (так же, как вы делали это раньше). Только классы, реализующие AutoCloseable, можно исполь¬ зовать в опера¬ торах TWR!
Готовим код Cyber BeatBox Q р о □ О О о ■O' □ о 0 о ГР L.j о □ ,гЧ п О О гр, wi о р ГР 1 ]| а о а гр □ п р п ГР -б п '"о н н G о о □ а □ п Q; гр „п 0 Г-' о о (—: □ □ О п п □ □ г—\ О р г О 0 г-] □ ГД' о ,гр рр о □ о Q о 0 р о □ О □ о 0 о 0 с G ГР гр п □ О п .о гр п *“0: п 0 о о о 1Г0 о п о Г"Р о ГР О 0': о 0 о — о }—1 □ □ о а ГР и □ -'-0 0 о. Г0 0 Q О □ □ □ О Г~) LJ с о о 0 о 0 о н п ГД □ п G □ о L.J □ 0 п О а; п а О г. □ О □ п ГР * о 0 гр 0 'Гг ГР п □ а □ □ О 0. □ 0 гр 0 о о О гр LJ о П а ГР ^р: □ 0 ГР а о Г—\ □ О о О ГР п П- гр' о □ О а о О [ Stop ] Tempo Up Tempo Down serialized restore Ж1 iLow к* К,11М> fuun ЛкШ'шт0К a,rn^CM /1Шп,«ми*Цй тстск Bass Drum О 0 Closed Hi-Hat :/Р; Open Hl-Hat 0 ГР- Acoustic Snare 0 Crash Cymbal 0 0' Hand Clap ■00 О High Tom о п Hi Bongo 0 0 Maracas о 0 Whistle 0 <—■» L.J Low Conga 0 ГР Cowbell о Vi bras lap о гр; Low-mid Tom 0 <—\ L-J' High Agogo 0 и Open Hi Conga рр Давайте сделаем так, чтобы бнтбокс мог сохранять и восстанавливать наш любимый ритмический рисунок.
Сохранение ритмического рисунка Напомним, в битбоксе ритмический рисунок — это последовательность установленных/сброшен- ных флажков. Когда приходит время воспроизводить последовательность, код проходит по каж¬ дому флажку и определяет, какой звук нужно проигрывать на каждом из 16 шагов. Так что, чтобы сохранить ритмический рисунок, достаточно сохранить состояние всех этих флажков. Для этого можно создать простой логический массив, в котором будет храниться состояние каж¬ дого из 256 флажков. Если все элементы массива сериализуемы (а в случае логических значений это так), то и сам массив сериализуем. Это значит, что мы спокойно сможем сохранить его в файл. А чтобы загрузить ритмический рисунок, достаточно считать сериализованный массив обратно и по его значениям восстановить состояния флажков. Большую часть кода вы уже видели в разде¬ ле «Готовим код», когда создавали графический интерфейс битбокса. Поэтому здесь мы сосредото¬ чимся только на коде сохранения и загрузки ритмического рисунка. Этот раздел «Готовим код» — подготовка к следующей главе, где вместо записи шаблона ритми¬ ческого рисунка в файл мы реализуем его передачу по сети. Вместо загрузки из файла мы будем получать шаблон от другого пользователя, опубликовавшего ритмический рисунок на сервере. Сериализация ритмического рисунка private void writeFile() { boolean[] checkboxState = new boolean[256]; for (int i = 0; i < 256; i++) { JCheckBox check = checkboxLiat.get(i); if (check.isSelected()) { checkboxState[i] = true; > } try (ObjectOutputStream os * e.printstackTrace(); 1 os.writeObject(checkboxState); } catch (XOException e) { new ObjectOutputStream (new )
Загрузка ритмического рисунка Это процесс, практически повторяющий сохранение, но в обратном поряд¬ ке... Сначала загружается логический массив, затем его данные используются для восстановления состояний флажков в графическом интерфейсе. Все это выполняется при нажатии пользователем кнопки Восстановить. Восстановление ритмического рисунка Это апцгся лшпш G кмка ВшШк Ся отfeVoem да жижи темненного щ/иЛонд. private void readFile() { ' у >' 1 boolean[] checkboxState = null; try (ObjectlnputStrean is = new ObjectInputStream(new FilelnputStream("Checkbox.ser’ )) checkboxState = (boolean[]) is.readObject(); z_ 1ЧЧ ottt unmicts wi afticHft*» ) catch (Exception e) e.printstackTrace() } { for (int i = 0; i < 256; i++) { JCheckBox check = checkboxList.get(i) check.setSelected(checkboxState[i]); /кеша на(? дийгсн геттчнм.ч флаж- в списке Armu/jM, ссаеКжа mje.u J ссгекпщ JChtckBvx (checMvlMt). <?ccnfxcuj&fnut buildTrackAndStart () ; > токНиД^го ШПЬШтсШО flU~ ' 7 Л — facie ncciecjmr O^Hoft состоянии sequencer.stop() » Г сипки и Gccccj темншпг cfivutcKcd (jArmy/J-rt -► Решать —вам! аточите карандаш В этой версии программы есть серьезное ограничение! Когда вы нажимаете кноп¬ ку Сохранить, данные автоматически сериализуются в файл Checkbox.ser (если файл не существует, он создается). Однако при каждом сохранении предыдущий файл перезаписывается. Улучшите функцию сохранения/загрузки, добавив компо¬ нент JFileChooser. Он позволяет задавать имя файла и сохранять неограниченное количество различных ритмических рисунков, а также загружать и восстанавли¬ вать любой из ранее сохраненных файлов.
<^^аточите карандаш Решать — вам! /Ложно ли их сохранить? Как вы думаете, какие из этих объектов уже сериализу¬ емы, а какие должны бы быть? Если какой-то из них не сериализуем — почему? В этом нет смысла? Это может быть небезопасно? Или объект работает только в рам¬ ках текущего запуска JVM? Попробуйте сделать предпо¬ ложения, не заглядывая в документацию API. Типы объектов Сериализуется? Object Да/Нет String Да/Нет File Да/Нет Date Да/Нет OutputStream Да/Нет JFrame Да/Нет Integer Да/Нет System Да/Нет Если нет, почему? Какие из этик вариантов корректиыР Обведите те фрагменты кода, которые скомпилиру- ются (предполагая, что они находятся внутри коррект¬ ного класса). Держитесь Ф правее FineReader fileReader = new FineReader () ; BufferedReader reader = new BufferedReader(fileReader); FileOutputStream f = new FileOutputStream("Foo.ser"); ObjectOutputStream os = new ObjectOutputStream(f); BufferedReader reader = new BufferedReader(new FileReader(file)); String line; while ((line = reader.readLine()) != null) { makeCard(line); ) FileOutputStream f = new FileOutputStream("Game.ser"); ObjectlnputStream is = new ObjectlnputStream(f); GameCharacter oneAgain = (GameCharacter) is.readObject(); ► Решать —вам!
\ражнение В этой главе вы познакомились с удивительным (и не таким уж страшным) миром Java I/O. Теперь ваша за¬ дача — определить, какие из приведенных утверждений соответствуют действительности, а какие ложны. Правда или ложь 1. Сериализация подходит для сохранения данных, которые затем будут использоваться в не-Java программах. 2. Состояние объекта можно сохранить только с помощью сериализации. 3. Класс ObjectOutputStream используется для сохранения сериализуемых объектов. 4. Цепочные потоки можно использовать как самостоятельно, так и вместе с потоками подключения. 5. Один вызов метода writeObject() может привести к сохранению нескольких объектов. 6. Все классы по умолчанию сериализуемы. 7. Класс Path из пакета java.nio.file можно использовать для поиска файлов. 8. Если суперкласс не сериализуем, то и его подкласс не может быть сериализуемым. 9. В конструкции try-with-resources можно использовать только классы, реализующие интерфейс AutoCloseable. 10. При десериализации объекта его конструктор не вызывается. 11. И сериализация, и запись в текстовый файл могут вызывать исключения. 12. Класс BufferedWriter может быть связан с File Writer. 13. Объекты класса File представляют файлы, но не каталоги. 14. Нельзя принудительно сбросить содержимое буфера до его полного заполнения. 15. Потоки данных FileReader и FileWriter можно буферизовать при необходимости. 16. Класс Files содержит методы для работы с файлами и каталогами. 17. В конструкции try-with-resources нельзя использовать блок finally. Ответы на стр. 620
Мапштики с кодом File Edit Window Help Torture % java DungeonTest 12 Это задание будет непросто выполнить, поэтому мы повысили его статус с упражнения до полноценной головоломки. Восстановите фраг¬ менты кода так, чтобы получилась рабочая Java-программа, которая выдает показанный ниже результат. (Возможно, не все фрагменты по¬ надобятся, а некоторые можно использовать больше одного раза.) ObjectlnputStream ois = new Obj ectlnputStream (fis) ; int getX ( ) { return x; System.out.printIn(d.getX()+d.getY()td.getZ( )>; FilelnputStream fis = new Kublrc int x = 3; FilelnputStreamC'dg.ser") ; lranSient long у = 4;| —Private short z = 5; d ~ (DungeonGame) ois.readObject(); ObjectOutputStream oos = new ObjectOutputStream(fos) ; public static void main(StringU args) ! DungeonGame d = new DungeonGame(); Ответы на стр. 621
Решение упражнений Правда или ложь (состр. 618) 1. Сериализация подходит для сохранения данных, которые затем будут Ложь использоваться в не-Java программах. 2. Состояние объекта можно сохранить только с помощью сериализации. Ложь 3. Класс ObjectOutputStream используется для сохранения сериализуемых Правда объектов. 4. Цепочные потоки можно использовать как самостоятельно, так и вместе Ложь с потоками подключения. 5. Один вызов метода writeObject() может привести к сохранению нескольких Правда объектов. 6. Все классы по умолчанию сериализуемы. Ложь 7. Класс Path из пакета java.nio.file можно использовать для поиска файлов. Ложь 8. Если суперкласс не сериализуем, то и его подкласс не может быть Ложь сериализуемым. 9. В конструкции try-with-resources можно использовать только классы, Правда реализующие интерфейс AutoCloseable. 10. При десериализации объекта его конструктор не вызывается. Правда 11. И сериализация, и запись в текстовый файл могут вызывать исключения. Правда 12. Класс BufferedWriter может быть связан с FileWriter. Правда 13. Объекты класса File представляют файлы, но не каталоги. Ложь 14. Нельзя принудительно сбросить содержимое буфера до его полного Ложь заполнения. 15. Потоки данных FileReader и FileWriter можно буферизовать при Правда необходимости. 16. Класс Files содержит методы для работы с файлами и каталогами. Правда 17. В конструкции try-with-resources нельзя использовать блок finally. ЛОЖЬ
Ура, наконец- то ответы! А то эта глава уже начинала утомлять. Магнитики с кодом (со стр. 619) import java.io.*; class DungeonGame implements Serializable public int x = 3; transient long у = 4; private short z = 5; int getX() { return x; long getY() { return y; } short getZ() 1 return z; I ! class DungeonTest { public static void main(String[] args) ( DungeonGame d = new DungeonGame(); System. out .println (d. getX () + d.getYO + d.getZO); try ( FileOutputStream fos = new FileOutputStream("dg.ser" ObjectOutputStream oos = new ObjectOutputStream(fos) oos.writeObj ect(d); oos.close(); FilelnputStream fis = new FilelnputStream("dg.ser") ; ObjectlnputStream ois = new ObjectlnputStream (fis) ; d = (DungeonGame) ois.readObject(); ois.closeO; Г File Edit Window Help Escape % java DungeonTest 12 8 ) catch (Exception e) ( e.printStackTrace(); ) System. out. println (d. getX () + d.getYO + d.getZO); }
17. Сетевое взаимодействие и потоки выполнения Сетевое взаимодействие Выходим в сеть: устанавливаем соединение с внешним миром! Ваша Java-программа может обмениваться данными с другим приложением — даже если оно работает на другом устройстве. И знаете что? Это не так уж сложно. Все тяжелые сетевые процессы уже спрятаны внутри встроенных библиотек Java. Одно из главных преимуществ Java — то, что работа с сетью почти не отличается от обычного ввода-вывода. Все, что вам нужно, — другой «конец» цепочки I/O. В этой главе вы научитесь создавать клиентские и серверные подключения, ис¬ пользовать сетевые каналы для обмена данными, строить общение между клиентом и сервером и — внимание! — работать с потоками выполнения, чтобы справляться с несколькими задачами одновременно. Итог главы: у вас будет полноценный многопоточный чат-клиент. Да, многопоточ¬ ный, так что вы сможете одновременно говорить с Бобом и слушать, что отвечает Сьюзи.
Бит бокс в сети: чат в реальном времени Вы работаете над компьютерной игрой, и ваша команда занимается звуковым оформлением для нее. С помощью чат-клиента в битбоксе вы можете легко общаться и обмениваться данны¬ ми: отправляйте не только текст, но и ритмиче¬ ские рисунки — их получат все участники чата. Вы сможете не только читать сообщения других участников, но и загружать и воспроиз¬ водить их лупы. Все, что нужно, — щелкнуть мышью по входящему сообщению. В этой главе мы разберемся, как создать такой чат-клиент. Вы узнаете, как устроен простой сетевой чат, как работает его серверная часть и как связать это все с битбоксом. Полноцен¬ ную версию программы мы напишем в разделе «Готовим код». А пока начнем с наипростейше¬ го чат-клиента и очень простого кода на сто¬ роне сервера, чтобы обмениваться текстовыми сообщениями. Шыжсшжшди АС МШ ютмт- ccWauteДО стающим. рит.:(ит<*«« fwyncK. • •• Ludicrously Simple Chat Client (eoume xwjmm |U нажмите кнопки (m 1 пмшпе, гтс- ci\ стиравши его вместе c mtKUiM,HM HimvMMtecKiiM ригинкам. Эй, как добраться до ресторанчика Кунг-Пао? 1 Влево иди. I Ты такой каааифныи!!! Напишешь «кайфиый» с тремя воскпиками еще раз - я уйду и заберу программку, i Кто придумал ТМ. издеваться над бедным восклицательным знаком? Один — максимум! Кто вообще тут сейчас есть? OarkStar MutfmPerson UrSuchMu Привет, MutfmPerson, че как? Эээ, кто это написал? По-моему, это OarkStar 4, I think it was OarkStar1, Send £ cooowfM и» иa nj
Обзор чат-клиента Каждый клиент дол¬ жен иметь информацию о серЬере. СерЬер, b сЬою очередь, должен отслеживать подключенных кли¬ ентов. Принцип работы: Сейчас в чате 3 клиента: кли¬ ент А, клиент Б и клиент В. Клиент Б Клиент А Сервер Принсе-.'! я клиент А Как слышно? Я —А... Вы меня видите, я - В? Клиент В Q Клиент связывается с сервером е Сервер устанавливает подключение и добавляет клиента в список участников. О Подключается еще один клиент Q Клиент А передает со¬ общение ^ Сервер рассылает сообщение ВСЕМ участникам (включая отправителя) Клиент А Я ЛСГу mUjKAKr питься к тту. Сервер Клиент А | (ф6(р, я .very nuj ттыя к гату. Лаунс, ти najKMCKH. К.1К - Лаунс, тм поуллюген. Сервер Сервер Сервер Сервер «Ктс утдщил.иск jaMyw Mnwif иу Клиент А каиндтм (? с туш?!» •Кт о утшцды ,иш ЛлМ^Н) МШМ1 иу каинш\п^2(щаи?Ь <лиент А I ' Клиент Б
Подключение, отправка и получение Три ключевые вещи, которые нужно освоить, чтобы клиент заработал: • Как настроить начальное подключение между клиентом и сервером • Как получать сообщения от сервера • Как отправлять сообщения на сервер Для этого требуется выполнить ряд низкоуровневых операций. Но нам повезло — Java API берет на себя всю грязную работу и делает этот процесс удивительно простым для программистов. В этой главе вы увидите больше кода, связанного с графическим интерфейсом, чем с сетью или вводом-выводом. И это еще не все. Даже у такого простого чат-клиента есть подводный камень, с которым вы еще не сталкивались в этой книге: выполнение двух задач одновременно. Установить подключение — задача однократная: полу¬ чилось или нет. Но дальше пользователь должен и отправлять сообщения, и одновременно получать входящие от других участников. И все это через сервер. Хм... Придется немного подумать над этим. Но ничего страшного: через пару страниц все станет ясно. О Подключение Клиент подключается к серверу Ё Клиент А О Получение Клиент читает сообщение от сервера Чсшша&ш&шп сетевое иш/слюшшс к cehtfchu нс алщштшз на порту 5000. Ю Клиент А О Отправка Клиент отправляет сообщение на сервер Сервер LJJ И— Stt inn s = yuida. ytad/jtu () 1 no адресу 1 496.164.1.103' Сервер
1. Подключение Чтобы связаться с другим компьютером, нам нужен объект, представ¬ ляющий сетевое подключение между двумя машинами. Для этого мож¬ но открыть java.nio.channels.SocketChannel — он и будет таким объектом подключения. Что такое подключение? Это взаимосвязь между двумя машинами, при которой два программных компонента знают друг о друге. Самое важное — эти два программных компонента знают, как обмениваться данными. Другими словами, как передавать друг другу биты данных. К счастью, нам не нужно беспокоиться о низкоуровневых деталях: они обрабатываются на более низком уровне «сетевого стека». Если вы не знаете, что такое «сетевой стек», не переживайте. Это всего лишь спо¬ соб описания слоев, через которые должна пройти информация (биты), чтобы попасть из Java-программы, работающей в JVM в определенной ОС, в физическое оборудование (например, Ethernet-кабели) и вернуть¬ ся на другой компьютер. Часть, за которую вам нужно отвечать, находится на высоком уровне. Все, что нужно, — создать объект, представляющий адрес сервера, и от¬ крыть к нему канал. Готовы? Погнали! Чтобы установить подключение, нужно знать ghe ключеЬые характеристики сер- Ьера: где он находит¬ ся и на како/л порту работает. Т)руги/ли слоЬалли, 1'Р-адрес и но/лер ТСР-порта. ^ XWimmv ruun»» ,mntc* f к L.nf^M.wt umXu txuf InetSocketAddress serverAddress = new InetSocketAddress("196.164.1.103", 5000) jP acjfux cf&pL ?{amp T( \ ncpna SocketChannel socketChannel = SocketChannel.open(serverAddress), Чат-сервер находится по адресу 196.164.1.103 порт 5000. Когда мне нужно с ним связаться, я просто отправляю сообщение по этому адресу, j указанному аууису. ^ n(Jf\yц У2/)2 .lUf»1 tv5( Клиент г ■■ Сервер Клиент находится по адресу 196.164.1.100, порт 4242. Когда мне нужно с ним связаться, я про¬ сто отправляю сообщение по этому адресу. 196.164.1.100:4242 196.164.1.103: 5000 Подключение означает, что две машины знают друг о друге — включая сетевые адреса (IP) и используемые ТСР-порты.
TCP-порт — 16-битный адрес программы на сервере Ваш интернет-сервер (HTTP) работает на порту 80 — это стандарт. Если у вас есть Telnet-сервер, он использует порт 23. FTP — порт 20. Почтовый сервер POP3? Порт 110. A SMTP? Порт 25. Сервер времени — порт 37. Представьте, что номера портов — это уникальные логические адреса, указывающие, какая программа на сервере должна обработать подключе¬ ние. И все. Вы не найдете на сервере физический «порт 80», потому что это просто число. Их всего 65 536 (от 0 до 65 535). Очевидно, что они не предназначены для подключения физи¬ ческих кабелей и устройств. Это просто числа, указывающие на конкретное приложение. Без номеров портов сервер не смог бы понять, с какой именно программой клиент хочет установить подключение. У каждой программы может быть свой протокол. И если перепутать порты, все пойдет наперекосяк. Представьте, что браузер случайно подключился не к HTTP, а к POP3. По¬ чтовый сервер просто не поймет HTTP-запрос. Даже если и поймет — он с ним ничего сделать не сможет. При разработке серверной программы вы сами выбираете, на каком порту она будет работать. Как это сделать в Java — увидим чуть позже в главе. В нашем случае чат-сервер использует порт 5000 — просто потому, что мы так решили. Главное — выбрать значение в диапазоне от 1024 до 65535. Почему от 1024? Потому что порты с 0 по 1023 — это заре¬ зервированные порты для общеизвестных сервисов. Именно о них мы говорили выше. Если вы пишете серверное приложение в корпоративной сети — обязательно уточните у системных администрато¬ ров, какие порты уже заняты. Они могут сказать, например: «порты ниже 3000 не трогать». И вы, как человек разумный (и с целыми конечностями), не станете выбирать порт наугад. А если вы пишете для своей домашней сети — просто спро¬ сите у детей. Они точно скажут, какие порты «уже заняты» игрой Minecraft. Стандартные номера TCP-портов для популярных серверных приложений: mps С tp&p может допускать <jv 65 536 ^>..шти мд1 (ifi&fmux гушло- дкшмм, пс сунану на кажут, порт. TCP-порты с 0 по 1023 зарезерЬироЬаны для популярных серЬисоЬ. Не используйте их для соЪстЬенных серверных програ/л/л!* Чат-cepbep. который /лы напише/л. Будет работать на порту 5000. /Лы про¬ сто ЬыЬрали число Ь диа¬ пазоне от 10ZH до 65 535. ^Теоретически вы можете задействовать один из этих портов. Но тогда системный админи¬ стратор отругает вас, а начальство вызовет на ковер.
(Не) дурацкие вопросы ЗоП рОС: Как узнать номер порта серверной программы, с которой нужно установить подключение? Ответ.- Это зависит от того, относится ли программа к числу общеизвестных сервисов. Если вы хотите подключиться к чему-то вроде HTTP, SMTP или FTP, их порты легко найти: просто вбейте в поиске в Интернете запрос «стан¬ дартные ТСР-порты» — и получите таблицу стандартных значений. Или спросите у своего дружелюбного сисадмина. Но если программа не из списка общеизвестных, придется обратиться к тому, кто ее развер¬ тывает. Как правило, если кто-то разрабатывает сетевой сервис и хочет, чтобы с ним могли рабо¬ тать клиенты, он публикует адрес, порт и протокол. Например, если вы пишете клиент для сервера игры в го, вы можете найти всю нужную информацию на сайте этого сервера: они обычно дают четкие инструкции по подключению. ВопрОС: Может ли на одном порту одновременно работать несколько программ? Другими словами, могут ли две разные про¬ граммы на одном сервере исполь¬ зовать один и тот же номер порта? Ответ: Нет, не могут! Если вы попытаетесь запустить программу на порту, который уже занят, полу¬ чите исключение BindException. Когда говорят «привязать програм¬ му к порту», имеют в виду запустить серверное приложение и указать, на каком порту оно будет отслеживать подключения. Подробнее об этом — в разделе главы, где мы создаем собственный сервер. Н(лмЪ\ tufvna — это А номер порта — это название конкретного магазина, например, «Boogiemanmusic». П рокачаи Хорошо, подключение установлено. Клиент и сер¬ вер знают IP-адреса и номера TCP-портов друг друга. Что дальше? Как осуществляется обмен данными? Другими словами, как передавать биты от одного компьютера к другому? Подумайте, какие сообще¬ ния должен отправлять и получать ваш чат-клиент. Как ,жх пнг КП CCLMLM Lfi.it (/шкибштся <JrtH.HM.WU ? □1 Сецвсушт I КС^ ШШг I клиента 1 Клиент Сервер
ЛлНДЛМ (СяЛ,. Ш,Ж(Ш К. iUfHirU.il М <(f \&ju u 2. Получение Для работы с удаленным подключением можно использовать обыч¬ ные потоки ввода-вывода, как вы делали это в предыдущей главе. Одна из самых удобных возможностей Java — это то, что работа с I/O почти не зависит от источника данных. Иными словами, вы можете использовать BufferedReader точно так же, как при чтении из файла, — с той разницей, что теперь базовый поток под¬ ключен не к файлу, а к сетевому каналу. Чтение данных из сети с помощью BufferedReader ^ Подключение к серверу Л} лци<127.0.0.1 укалеСшт ил ту же сащн лолишиу; ил кепи /г. Т рей podenukm Саш кеер Это илю- но ала тстирсйгниЛ к. шентл и серСера на ипиш кс.шикпи ре — ш ,чаклн;шшя к реа и пои ости, шнлпс шслеСек.1Р мжт то нлпиелии СясаОmt — э<р- кт ajijun тот же. SocketAddress serverAddr = new InetSocketAddress("127.0.0.1", 5000) socketChannel = SocketChannel.open(serverAddr) SocketChannel Кессхетше сетате SeekdCkanm.C Kcmi/uiii пажаекктся к aghtcu 127.0.0.1 и шлиру перша jvOV. © Создание/получение Reader из сетевого канала Reader reader = Channels.newReader(socketChannel, Л епшша мы тлело этот порт/ \JI( meuy /mum пит t te Ctethci ut! Cf)a-qa. 5000 — это Корт нтноп raineepCepaJ Г Reader - это сСагс )но|Л .оооо»п шжт нижоит^ноЬадо по¬ пике!! <§Ьт$ бии\ра и(р. от ^cokilOlumntOu Ош потокаое оиий. к , ^ ^ipedReada кепи fum лш ежим иепелиеСат, на Ссрхне.м ур( Сне ишитки. / От т пел,/гит/, такси Reada .межне СеЫалисСаписяста шинекими мАпшаип к икса ШтпеС.е -еш п[Алмнш ,гГ/, '"/w/ Jco/u/OAап,4.С С Reader. 1 StandardCharsets.UTP 8)• Л иц ссуаанш Reader ю'ч.лтс.ти икажюы ттреСки (Cmmt), С которой Мтпп^щаСстшл nmiHMO no ft mu. На прок ft f lined тике |, u' ~ f //1'TT £ t (?rt?c ипинуисточ ItJ J o. Q Создание BufferedReader и начало чтения! (ЩехСтмС lu./yroHHOVU uj mdReadet c Reada.. SeekdChannt.C. BufferedReader bufferedReader = new BufferedReader(reader) string message = bufferedReader.readLine(); Буферизированные символы Клиент Буферизирован- ные символы BufferedReader связан с преобразованы в символы символы Reader Не тстник байты с сервера связан с Данные SocketChannel (не обязательно знать фактический класс) Сервер
Z. Отправка В предыдущей главе мы использовали BufferedWriter. Здесь у нас есть выбор, но когда требуется записывать строки по одной, стандартный выбор — PrintWriter. Вы наверняка узнаете два ключевых метода PrintWriter — printQ и printlnQ! Точно такие же, как в знакомом System.out. Передача в сеть с помощью PrintWriter ^ Подключение к серверу Этот маг током.же, как м на nmqtuiuuvw ( iniuTHHiv г стпНьипи. дани то на a/mfi. чиж- уУ HQ uCTTUiHt^MtTU с HM. U мклютАш. SocketAddress serverAddr = new InetSocketAddress("127.0.0.1", 5000) О SocketChannel socketChannel = SocketChannel.open(serverAddr); УкажРас. m к caufrclku (( К amt) _ . .... rtt^anuriocmncK, Ckqmm uc- Создание/получение Writer из сетевого канала (сМапи mu .ж* салЦк tnu> Ц (? iijunjica. тенил! Writer writer = Channels.newWriter(socketChannel, StandardCharsetsUT A ^ ' P-8,; Шг1« •*nn‘’ u CA G ft tiki. канал- О с°здание PrintWriter и отправка сообщения PrintWriter printWriter = new PrintWriter(writer); r writer .println ( "отправляемое сообщение") ; <-— ., л(?Н'ЬАА<«1\мюскм i|C«l writer, print ("еще одно сообщение"); Дс.тса|'. (Дики кшш, л,щ.т . fwJcma. a imntO - mu AMtm LJUAmMO - »tm. как trmt, Мстспшк a il. символы символы сн Просто jp байты на сервер Т(< с.итшпыт I "^«сообщение...» £ Клиент PrintWriter связан с [ сообщение...»^ связан с Writer Данные SocketChannel (не обязательно знать фактический класс) Сервер
Подключение можно установить разными способами Если вы взглянете на реальный код, взаимодей¬ ствующий с удаленной машиной, то заметите: есть несколько способов устанавливать подключение, читать и записывать данные. Выбор подходящего подхода зависит от множе¬ ства факторов — включая, но не ограничиваясь, используемой версией Java, архитектурой прило¬ жения, количеством одновременно подключенных клиентов, объемом и частотой передаваемых дан¬ ных. Один из самых простых подходов — исполь¬ зовать j ava . net. Socket вместо Channel. Объект Socket Тут написано, что нужно использо¬ вать SocketChannel, но я точно знаю — можно и Socket. Вы можете получить InputStreamnHH OutputStream из объекта Socket и работать с этими потоками точно так же, как мы делали это ранее. 1м‘1' “r f/nu>c Socket chatSocket = new Socket("127.0.0.1", 5000); , . 1 Щпнт 14 j Jeckd. gccmamchu) M/ Huc InputStreamReader in = new InputStreamReader(chatSocket.getlnputStreamO); BufferedReader reader = new BufferedReader(in) String message = reader. readLine(); ля /тения (HuuktJ ,ntm(- jtuikcm IK If-KX JWMtC- PrintWriter writer = new PrintWriter(chatSocket.getOutputStreamQ); writer.println("отправляемое сообщение writer.print("еще одно сообщение"); "'j V HYC.zo HU«..W Kca алч /мш:и (Wtik'i) ,u iLJatmcM cm Вы f**» уа&кш Класс java.net.Socket доступен во всех версиях Java. Он поддерживает простой сетевой ввод-вывод, используя те же пото¬ ки I/O, с которыми вы уже работа¬ ли при чтении и записи файлов.
о Если класс Socket ^ О ( существует в Java с самого V начала, требует меньше кода ( и решает ту же задачу, что 'к и каналы (Channels API), I зачем тогда вообще нужны \ каналы? Поскольку мир становится все более взаимосвязанным, Java про¬ должает развиваться, предлагая все больше способов взаимодей¬ ствия с удаленными машинами. Вы, наверное, помните, что каналы (Channels) находятся в пакете java . nio . channels? Сам пакет j ava . nio (или просто NIO) был добавлен в Java 1.4, а в Java 7 он получил серьезное обновление, известное как N10.2. С тех пор появилось несколько способов использовать каналы и NIO для по¬ вышения производительности при большом количестве подключений или при передаче больших объемов данных. В этой главе мы используем Channels, чтобы реализовать ту же базо¬ вую функциональность подключения, которую можно было бы получить и с помощью обычных Socket. Но если бы наше приложение работало под серьезной сетевой нагрузкой (множество клиентов или постоянный поток данных), мы могли бы HacrpoHTbChannels иначе — и задействовать их полный потенциал. Это помогло бы системе справляться с нагрузкой гораз¬ до эффективнее, чем в случае с традиционным Socket и потоками. Мы специально выбрали самый простой способ использования Channels, чтобы переход к более сложным возможностям не оказался для вас резким и пугающим. Если вы хотите глубже изучить N10, рекомендуем книги Java NIO Рона Хит¬ ченса и Java I/O, N10 и N10.2 Джеффа Фризена. faco\a<5bcfl- Channel API поддерживает расширенные сете¬ вые функции, которые не понадобятся в этих упражнениях. Поддерживается работа с небло¬ кирующим вводом-выводом, чтением и записью через ByteBuffer, а также с асинхронным вво¬ дом-выводом. Мы не будем погружаться во все это! В Интер¬ нете вы найдете больше информации по озвученным темам.
Класс DailyAdviceClient Прежде чем приступить к созданию чат-приложения, начнем с более просто¬ го задания. Программа-советчик — это сервер, который дает практичные и вдох¬ новляющие советы тем, кто тратит долгие часы на создание кода. Мы создадим клиент для программы-со¬ ветчика, который будет подключаться к серверу и получать новые советы при каждом сеансе работы. Чего же вы ждете? Кто знает, какие возможности вы уже упустили без этого приложения. Програгл/ла-соЬетчик О Подключение Клиент соединяется с сервером Клиент Сервер О Чтение Клиент получает Reader для Channel и считывает сообщение с сервера. Клиент ChanntCi. nm/Rmckti) advict = шикг. mviJjnd) Программа-со- | ветчик на сер- j * вере составляет» совет и отправ- 1 ляетего .J0 Сервер
Код класса DailyAdviceClient Эта программа создает объекты SocketChannel и Buf feredReader (с помощью объекта Reader канала) и считывает одну строку из сервер¬ ного приложения (запущенного на порту 5000). import java.io.*; import java.net.InetSocketAddress; import java.nio.channels.Channels; import java.nio.channels.SocketChannel; import java.nio.charset.StandardCharsets; ЧкатСасм, trru) cff на KJm 5000 на ХПнжтся KUf (a fee public class DailyAdviceClient { public void go() { InetSocketAddress serverAddress = new InetSocketAddress("127 0 0 1”"* 5000) try (SocketChannel SocketChannel = SocketChannel.open(serverAddress)) { (bmm стктRtadct. капе- i. X 2. sCt/icи» s'tnm м(Inm/ /inu- Kr''Uc a m -S35«-*r aefe пш (’tuA,u- Mm cfyjtin rmmn&orvi аан ■ u/hnurniSockdChanneC KMC channelReader = Channels.newReader(SocketChannel, StandardCharsets.UTF_8) BufferedReader reader = new BufferedReader(channelReader); „ ,, ,u , Madam Said(:k^ String advice = reader.readLine(); ^ ' System.out.println("Сегодняшний совет: ” + advice); reader.close(); бсеектп chaimtGRtada м tfivcffiacdReadet adme.ua ) catch (ioException e) { щдшски?а/Фоются, когда Cmu.tHiilHt сСк/ка коаа 'jA))}yiJuu^'ni0 heuxmat т LtbWlK/mr™K & лЖоШ/кг тл М" с Ml /уН. matin, нс /каст, сткиаа пс- е.printstackTrace() ja&[\ uuimea. ступают счий(/ш. (ешь mcclcuLr чек сетей/ псакАм пнке), и ntwcnw пе/ш/ает чхйщф- public static void main(String[] args) { new DailyAdviceClient().go(); ) )
^аточите карандаш к Вспомните классы для чтения и записи через SocketChannel. Не подглядывайте на следующую страницу! Для чтения текста из объекта SocketChannel: О Клиент Нашышш. или нарисуйте и/аогку млсссв. кепирш клиент шиимррт ерл шин мд ejanntev с сервера. Решать-вам! 1Иик Сервер Для передачи текста объекту SocketChannel: О Клиент ЗТаль Иапшшти in и н apucyiwu и ttwvty классов, кепи рт клиент tmuMjipm еры стп^йси ргштх на сервер. Сервер Ц^Заточите карандаш Решать-вам! Ответьте на вопросы: Какие два элемента информации необходимы клиенту для подключе¬ ния к серверу? Какие номера TCP-портов зарезервированы для стандартных серви¬ сов, таких как HTTP и FTP? Правда или ложь: Диапазон допустимых номеров TCP-портов можно представить с помощью примитива short.
Разработка простого серверного приложения Для написания серверного приложения хватит пары каналов. Да, именно пары. ServerSocketChannel, который ожидает запро¬ сы клиентов (когдаклиент подключается), и SocketChannel, ко¬ торый используется для связи с клиентом. Если клиентов несколь¬ ко, потребуется больше каналов, но об этом позже. Принцип работы: Q Серверное приложение создает объект ServerSocketChannel и привя¬ зывает его к определенному порту. ServerSocketChannel serverChannel = ServerSocketChannel.open(); serverChannel.bind(new InetSocketAddress(5000)); Запускается серверное приложение, отслеживаю¬ щее входящие клиентские запросы на порту 5000. ^ Клиент создает объект SocketChannel, подключенный к серверному приложению. SocketChannel svr = SocketChannel.open(new InetSocketAddress("190.165.1 103", 5000)); О Клиент знает IP-адрес и номер порта (опублико¬ ванные или предоставленные тем, кто настроил серверное приложение на сервере). Сервер создает объект SocketChannel для связи с этим клиентом. SocketChannel clientChannel = serverChannel.accept(); Метод accept() блокируется (просто ожидает), пока не появится клиент. Когда клиент подклю¬ чается, метод возвращает объект SocketChannel, который знает, как взаимодействовать с этим клиентом. ServerSocketChannel может вернуться к ожида¬ нию других клиентов. На сервере используется только один ServerSocketChannel и по одному SocketChannel на каждого клиента. . клиента ct|v&j\a Lwr .ж-tm шгхиштсм " сжшртш си<щ wwjtzc к. иинта)
Код класса DailyAdviceServer Эта программа создает объект ServerSocketChannel и ожидает запросы от клиентов Ко, ступает клиентский запрос (то есть клиент создает объект SocketChannel ’ да по~ серверу), сервер создает объект SocketChannel дллэтото клкечта. Затем “Т"? (используя Writer, полученный из SocketChannel) и отправляет сообщение i riter import java.io.*; import java.net.InetSocketAddress; insert java.nio.channels.*; insert java.util.Random; ' клиенту. cW,1u ‘f и,,тСнм ^nT1Ul ara^i^fa,nLr ' d amnAioUaecufe. public class DailyAdviceServer { final private String [] adviceList = < „ ' "Бери меньшие куски", 1 ' "Выбирай узкие джинсы. Нет, они не делают тебя толстой.", "Одно слово: неподобапций", "Только на сегодня будь честным. Скажи своему начальнику, что ты *На самом деле* думаешь" "Ты, наверное, хочешь переосмыслить свою стрижку." (q hvctu private final Random random = new Random () ; public void go () { _ цп<*И ^'fu1 try (ServerSocketChannel serverChannel = ServerSocketChannel.open(П I , . . „ id птшчл/ми otneticckiti ncmntC serverChannel. bind (new InetSocketAddress (5000)) 1 м 4HW7C. ИМЖИО ’ -r< rupnuf, ид & mcfiuu. ajfjcm patcmami t\fu\. о f S' .float AinifCKa ( 5% x Awhile (serverChannel.isOpen()) 2 2 SocketChannel clientChannel ,ийц Cxtmm (йлскмпй wuw, W Tjmud м ( Jltnicq ampU) (ГмкиИшпим (мшт). гика Ht numijimm}anf\u- cm клиент,/, & мтм <Ь- ( bjvimfattn Yukdi kantu f ихм Саш.шшмстйсч c amiui клиснтси. 1 ' 4 serverChannel.accept(); PrintWriter writer = new PrintKriter(Channels.newOutputStream(clientChannel)); x ^ г £ String advice = getAdvice(); writer.println(advice); i k writer.close(); ж '-^^.System. out.println (advice) ; sTf ) ) catch (IOException ex) { ex.printStackTrace(), ’KXit Hhiitf 9t>U > ’ «xtrc/i an,uC private String getAdvice() ( int nextAdvice = random.nextlnt(adviceList.length) return adviceList[nextAdvice]; ) public static void main(String[] args) new DailyAdviceServer().go(); ) (btjcitu ftitunu H шпек сим кшштсксгс качена u <a tmuCatu ш ь Pt/nflVum. Зфеьмсжт ыспеньюСапи Cl\a\t\ifo.неиЮчtj\utSlnamO u. tu (hemn t(!vnmni'tita(). яыкшшт ym,/uK-yr«ftma7t 0 Прокачай извилины Как сервер узнает, как взаимодействовать с клиентом? Подумайте, как, когда и где сервер получает информацию о клиенте.
Без лишних <лов Код сервера советов на противо¬ положной странице имеет ОЧЕНЬ серьезное ограничение: похоже, он может обслуживать только одного клиента за раз! Такой подход совер¬ шенно не подходит для чат-сервера. Можно ли создать сервер, который сможет обрабатывать несколько клиентов одновременно? Да, все верно: сервер не может принять за¬ прос от нового клиента, пока не завершит обработку текущего. После этого он начина¬ ет следующую итерацию бесконечного цикла, ожидая на вызове acceptO, пока не поступит новый запрос. Когда он приходит, создается объект SocketChannel для обмена данными с новым клиентом, и процесс начинается за¬ ново. Чтобы поддерживать работу с несколькими клиентами одновременно, нам нужно ис¬ пользовать отдельные потоки выполнения. Каждому новому клиенту передается соответ¬ ствующий объект SocketChannel в новый поток выполнения, благодаря чему все клиенты мо¬ гут обслуживаться независимо друг от друга. Совсем скоро мы попрактикуемся в этом! ■ Клиентские и серверные приложения обмениваются данными через каналы, созданные с использованием Channel API. ■ Канал — это объект, который представляет соединение между двумя приложениями, возможно работающими на разных машинах. ■ Клиенту необходимо знать IP-адрес (или имя хоста) и номер TCP-порта серверного приложения. ■ TCP-порт — это 16-битное беззнаковое число, которое назначается конкретному серверному приложению. Но¬ мера портов позволяют разным приложениям работать на одной машине: клиент подключается именно к тому приложению, чей порт он укажет. ■ Порты от 0 до 1023 зарезервированы для «общеизвест¬ ных сервисов» — таких как HTTP, FTP, SMTP и другие. ■ Клиент устанавливает подключение, открывая SocketChannel: SocketChannel.open( new InetSocketAddress("127.О.О.1", 4200)) ■ После подключения клиент создает объекты для чтения и записи данных через канал: Reader reader = Channels.newReader(sockCh, StandardCharsets.UTF_8); Writer writer = Channels.newWriter(sockCh, StandardCharsets.UTF_8); ■ Для чтения текстовых данных с сервера используется BufferedReader, связанный с Reader. Сам Reader играет роль «моста»: он принимает байты из сетевого подклю¬ чения и преобразует их в символы. Это промежуточное звено между низкоуровневым соединением и высоко¬ уровневым чтением. ■ Для отправки текстовых данных на сервер создается PrintWriter, связанный с Writer. Отправка строк выполня¬ ется с помощью методов print() и printing ■ Сервер принимает подключения с помощью ServerSocketChannel, ожидающего клиентские запросы на заданном порту. ■ При поступлении запроса ServerSocketChannel прини¬ мает его, создавая SocketChannel для взаимодействия с подключившимся клиентом.
Разработка чат-клиента Мы создадим приложение чат-клиента в два этапа. Сначала реализуем версию, которая будет только отправлять сообще¬ ния на сервер, но не сможет читать сообщения от других участников (что добавит в концепцию чата элемент интриги и загадочности). Затем мы дополним его полноценным функ¬ ционалом, позволяя клиенту как отправлять, так и получать сообщения. Структура кода Перечислим основные функции, которые должен обеспечивать чат-клиент. public class SimpleChatClientA ( private JTextField outgoing; private PrintWriter writer; public void go() { // вызываем метод setUpNetworking() // создаем графический интерфейс и регистрируем слушателя для кнопки отправки Версия первая: отправка сообщений private void setUpNetworking() ( // открываем SocketChannel для подключения к серверу // создаем PrintWriter и присваиваем его переменной writer private void sendMessage() { // получаем текст из текстового поля и // отправляем его на сервер через writer (PrintWriter) }
import 5avax.swing.*; import java.awt.*; import java.io.*; import java.net.InetSocketAddress import java.nio.channels.*; C mfuimfyti у Ч JLm (java. Wcluu\nt« Г uT u su„vjl (Kin luuiu^f ' . } inyort static java.nio.charset.StandardCharsets UTF 8- СМШПШкгким luou^m; .«I гсшимн i «мл н нам Iv. public class SimpleChatClientA private JTextField outgoing; private PrintWriter writer; f public void go() { w , Kcrnfuw setUpNetworking () ; i/crnUH#б 1 U-ftimv I'C<JK.-tKft- huf к ati&fUf outgoing = new JTextField(20); JButton sendButton = new JButton("Отправить"); sendButton.addActionListener(e -> sendMessage()); > JPanel mainPanel = new JPanel(); mainPanel.add(outgoing); mainPanel.add(sendButton); JFrame frame = new JFrame("Наипростейший чат-клиент"); frame.getContentPane().add(BorderLayout.CENTER, mainPanel)• frame.setSize(400, 100); frame.setVisible(true); frame.setDefaultCloseOperation(WindowConstants.EXIT ON CLOSE) CaqaaмКШтскт aftwxelutoLMC 3am hi«oc лгннсм c fefotjatt bnduf' Ul { private void setUpNetworking() ^ , ”7’««caul .. „ "’"imi и —-s. trV < 7 7Ht4l*=a4wmcfntj|c ' \ч InetSocketAddress serverAddress = new InetSocketAddress("127.0*0.1", 5000); SocketChannel socketChannel = SocketChannel. open (serverAddress) • ^ „ writer = new PrintWri ter (Channels. newWriter (socketChannel ----- OOfkdLnmmtC System.out.println("Подключение установлено ") catch (IOException e) { } UTF_8)) ; ксшрША ''vи; гибкий тпп/ч e.printStackTrace() } } private void sendMessage() { writer.println(outgoing.getText()) writer.flush() ; outgoing.setText(""); outgoing.requestFocus(); ) /3 Э1ПСМ anwi V, РгшМ’лт ч т п ,vc9 «с ^ cini'p* public static void main(String[] args) { new SimpleChatClientAO . go() ; } Сели хотите прими «.ейчаспопробовать, введите готовый код чат-сервере, при- ятжшЛ не следующей странице. Сначала запустите сервер в одном терминале.Затем используйте другой терминал, чтобы запустить этот клиент.
Готовый код Наипростейший чат-сервер Вы можете использовать этот серверный код для всех версий чат-клиента. Все возможные предупреждения, которые когда-либо давали про учебный код, здесь в силе. Чтобы максимально упростить код, мы убрали многие элементы, которые понадобились бы для полноценного и надежного сервера. Другими словами — он работает, но есть не меньше сотни способов его сломать. Если после завершения этой книги захочется прокачать свои навыки, вернитесь и попробуйте сделать этот сервер более устойчивым и продуманным. После изучения этой главы вы уже сможете спокойно аннотировать код сами. И вы поймете его гораздо лучше, если вникнете самостоятельно, а не просто прочитаете объяснение каждой детали. С другой стороны — это готовый рабочий код, и его не обязательно понимать до последней строчки. Он здесь только для поддержки работы чат-клиента. import j ava.io.*; import j ava.net.InetSocketAddress; import java.nio.channels.*; import java.util.*; import java.util.concurrent.*; import static java.nio.charset.StandardCharsets.UTF_8; public class SimpleChatServer { private final List<PrintWriter> clientWriters = new ArrayList<>(); public static void main(String[] args) { new SimpleChatServer().go(); ) public void go() { ExecutorService threadPool = Executors.newCachedThreadPool(); try { ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); serverSocketChannel.bind(new InetSocketAddress(5000)); while (serverSocketChannel.isOpen()) { SocketChannel clientSocket = serverSocketChannel.accept(); PrintWriter writer = new PrintWriter(Channels.newWriter(clientSocket, UTF_8)); clientWriters.add(writer); threadPool.submit(new ClientHandler(clientSocket)); System.out.println("Подключение выполнено"); > ) catch (IOException ex) ( ex.printstackTrace(); ) Чтобы запустить чат-клиент, вам понадо¬ бятся два терминала. Сначала запустите этот сервер в одном терминале, затем за¬ пустите клиент в другом. )
private void tellEveryone(String message) { for (PrintWriter writer : clientWriters) { writer.println(message); wri ter. flush () ; } } public class ClientHandler implements Runnable { BufferedReader reader; SocketChannel socket; public ClientHandler(SocketChannel clientSocket) { socket = clientSocket; reader = new BufferedReader(Channels.newReader(socket, UTF_8)); ) public void run() { String message; try { while ((message = reader.readLine()) != null) { System.out.println("Прочитано; " + message); tellEveryone(message); } ) catch (IOException ex) { ex.printStackTrace(); } ) ) } File Edit Window Help TakesTwoToTango %java SimpleChatServer Подключение выполнено Прочитано: Приятно познакомиться Fie Edit Wmdow Help MaylHaveT>llsDance, %java SimpleChatClientA Networking established. Client running at: /127.0.0.1:57531 Щкшгат. CM К иншррм’.
Версия вторая: отправка и получение сообщений Ш Ludicrously Simple Chat Client P^aSuSThow to get ti^WMWChiSw»^ Co left. You are so kewllU! .. . v. v 'if you spell it like that one more time, I H »aWe mv isoftware and 90 home abused. Jl do not approve of your overuse of the poor, aou 'lexclamation mark. One Is enough. Who's in here right now? barkStar iMuffmPerson lUrSuchALu . Hey, what's up MufflnPerson? Who said that? Сервер jvwcee шт смещение ftttt еиуклнгшнаи кшнтаи (pern na.ee пеицгенчч l\ciqa клиент етпртемет ссссиуениг, сне нг пиеСштгм den «(ласти вхимиуех пеки арвер нс 61ршт егс в гащин гитск бтиингннд I ллм Sax углгтникс 6! ш схшяищ.' сеюсщечил > ^TthiniritwMbarkSta6_ Send Важный вопрос: КАК получать сообщения с сервера? На первый взгляд все просто: при настройке подключения создайте Reader, а затем читайте строки с помощью метода readLine () Но вот другой, еще более важный вопрос: КОГДА получать сообщения с сервера? Подумайте над этим. Какие у нас варианты? (!) Вариант первый: Считывать сообщения с сервера только тогда, когда поль¬ зователь что-то отправляет. Плюсы: легко реализовать. Минусы: странная логика. Почему читать именно в этот момент? А если пользователь просто сидит и читает, но ничего не отправляет? Он ничего не получит? (2) Вариант второй:опрашивать сервер каждые 20 секунд. Плюсы: работает даже для «наблюдающих» пользователей. Минусы: Серверу придется хранить сообщения, чтобы сравнивать, что уже было отправле¬ но. Почему именно 20 секунд? Если больше — неудобно, если меньше — нагрузка на сервер. В целом — неэффективно. 0 Вариант третий: Считывать сообщения сразу после их отправки сервером. Плюсы: максимально удобно и эффективно. Минусы: требуется одновременное выполнение двух задач: отправка и прием сообщений. Как это реализовать? Нужен цикл, который постоянно ожидает данные от сервера. Но куда его вставить? После запуска графического интерфейса ничего не происходит, пока какой-либо компонент интерфейса не вызовет событие.
Вы уже догадались, что мы выбираем третий вариант Нам нужна программа, работающая непре¬ рывно, проверяющая входящие сообщения с сервера, но при этом не мешающая пользо¬ вателю взаимодействовать с графическим интерфейсом. Пока пользователь спокойно набирает новые сообщения или прокручивает список входящих, в фоновом режиме должно выполняться чтение новых данных с сервера. А это значит, что нам наконец-то понадобится новый поток выполнения. Новый, отдельный стек вызовов. Нужно сохранить все, что работало в первой версии чата (той, где уже можно было отправ¬ лять сообщения), без изменений. Но теперь нам нужен параллельный процесс, который бу¬ дет считывать данные с сервера и отображать их в поле входящих сообщений. Хотя на самом деле каждый поток выполнения в Java — это не отдельный процесс на уровне операционной системы, по ощущениям все примерно так и есть. Сделаем небольшую паузу в разработке чата, чтобы разобраться, как все это устроено. А по¬ том вернемся и добавим эту функцию в наш чат-клиент ближе к концу главы. 3 Java действительно /ложно делать дЬа дела одноЬре/ленно. Многопоточное программирование в Java В Java поддержка многопоточности встроена прямо в язык. А создать новый поток выполнения — про¬ ще простого: Thread t = new Thread(); t. start(); Вот и все: создавая объект класса Thread, вы за¬ пускаете отдельный поток выполнения со своим собственным стеком вызовов. Но есть одна загвоздка. Этот поток выполнения на самом деле ничего не делает, поэтому почти сразу завершает работу. Как только поток «умирает», его стек исчезает. Конец истории. Значит, нам не хватает одного важного компонен¬ та — задачи, которую должен выполнять поток. Иначе говоря, нам нужен код, который будет испол¬ няться в этом отдельном потоке выполнения. Чтобы по-настоящему разобраться с многопоточно¬ стью в Java, нужно понимать не только, как устрое¬ ны потоки выполнения, но и как задать им работу. Причем существует несколько способов запустить выполнение кода в отдельных потоках — и не все они связаны напрямую с классом Thread из пакета j ava. lang. (Кстати, помните, j ava. lang — это тот самый пакет, который импортируется автома¬ тически. Он содержит базовые классы языка, такие как String и System.).
Множество потоков выполнения и один класс Thread В коде встречаются два варианта написания: thread с маленькой буквы и Thread с прописной. В чем разница? Когда написано thread, речь идет об отдельном пото¬ ке выполнения — то есть о независимом стеке вызовов. А когда вы видите Thread с прописной, сразу вспоминай¬ те соглашения об именовании в Java. Что в Java пишется с прописной? Классы и интерфейсы. Значит, Thread — это класс. Конкретно — класс из пакета j ava . lang. Объект Thread представляет поток выполнения. В ран¬ них версиях Java, если вы хотели запустить новый поток выполнения, приходилось напрямую создавать экземпляр класса Thread. Но Java не стоит на месте, и теперь это далеко не единственный способ. Мы еще вернемся к этому чуть позже в главе. thread основной поток другие потоки выполне- выполнения ния, запускаемые кодом thread (с маленькой буквы) — это отдельный поток вы¬ полнения, а значит, и собственный стек вызовов. Каждое Java-приложение стартует с основного потока выполне¬ ния — того самого, который помещает метод main () в основание стека. За запуск этого потока выполнения отвечает виртуальная машина Java (JVM). Она же может запускать и другие потоки выполнения, например, поток сборщика мусора, если решит, что пора наводить поря¬ док. А вы можете написать код, который запускает ваши собственные потоки выполнения. thread — это отдель¬ ный поток Ьыполнения, сбой собственный стек ЬызоЬоЬ. Thread — это класс Java, представляющий такой поток выполне¬ ния. И хотя Thread - класс, с которого Ьсе начи¬ налось, это далеко не единственный спосоЪ работать с ллногопо- точностью Ь Java. Thread Thread void join() void start() static void sleep() класс java.Iang.Thread Thread (с прописной буквы) — это класс, представляющий поток вы¬ полнения. В нем есть методы для запуска потока выполнения, объе¬ динения одного потока выполнения с другим, временной приостановки (sleep) и многое другое.
Стеки и потоки выполнения: основы параллельного исполнения Наличие нескольких стеков вызовов позволяет выполнять несколько задач одновремен¬ но. F-сли вы работаете на многопроцессорной системе (а такие сегодня почти везде — от ноутбуков до смартфонов), то можете запускать несколько потоков выполнения парал¬ лельно, каждый на своем ядре. Но даже если система не многопроцессорная или потоков выполнения больше, чем ядер, Java все равно создает иллюзию одновременности. Потоки выполнения переключаются так быстро, что кажется: все работает сразу. Как это устрое¬ но? Java-программа — всего лишь процесс, запущенный в вашей операционной системе. А значит, сначала сама JVM должна получить доступ к процессору от ОС. Как только это происходит, начинается выполнение байт-кодов. Какие байт-коды? Те, что находятся на вершине стека вызовов текущего потока выполнения. Через какие-нибудь 100 миллисе¬ кунд управление может передаться другому потоку выполнения, и JVM начнет выпол¬ нять другой метод в другом стеке. Одна из задач потока выполнения — отслеживать, какой именно оператор (а точнее, метод) в данный момент выполняется в его стеке. И все это выглядит примерно так: JVM вызывает метод main() public static void main(String[] > args) ./(кжцйти пшик i Основной поток выполнения Метод main() запускает новый поток вы¬ полнения. Основной поток выполнения может быть временно приостановлен, пока новый поток начинает работу. Runnable г = new MyThreadJob () ; Thread t = new Thread(г); л ' t. start (); ^ Г®! fmmxt. Dog d = new Dog() ; MU Hi&lk Ш ШК fiiUUUHl- пи.я гапцскаапся u cn\a- HcmmcJ жтийиш. Основной поток Пользовательский выполнения поток выполнения t Jit nu-K JVM переключается между исходным и новым потоком выполнения (пользо¬ вательский поток выполнения А) и до тех пор, пока оба потока выполнения не завершатся. Основной поток выполнения Пользовательский поток выполнения t
Чтобы появился новый стек вызовов, нужна задача, которую можно в этом стеке выполнять Лоток йхтинснм.я мне нужна настоя- Л Runnable для потока Ьыпол- щая работа. Дайте мне / и Runnable, и я присту- \ нения - это то же самое, плю к делу! что задача для работника. “Runnable — это задание, которое должен выполнить поток Ьыполнения. Runnable содержит метод, который оказывается Ьнизу ноЬого стека ЬызоЬоЬ: гип(). i\ лиетОД Г _ \Д/Й (5м’и]''- ' ~ Д лит*»"- Чтобы запустить новый стек вызовов, потоку выполнения нужна задача - работа, которую он выпоят при старте. Эта задача начинается с метода, который первым попадет в стек нового потока выполнения И пя всегда должен выглядеть вот так: иж У1 он public void run () { // код, который будет выполняться в новом потоке } А как поток выполнения узнает, какой метод положить в основание гт^с? тт„ , « г, ■ Для этого Runnable задает контракт. Runnable-- это интерфейс. Задача потока выполнения может быть определена в любом Гассе реализующем интерфейс Runnable, или в лямбда-выражении пшгепттот». «ит классе, подходящем по сигнатуре к методу run () Как только у вас есть такой класс или подходящее лямбда-выражение, вы можете этот код в отдельном потоке выполнения. Тем самым вы даете потоку выполне^1ия1его^ИТЬ вып°лнить
Чтобы задать задачу для потока выполнения, реализуйте интерфейс ««jrJ “Ki Runnable ^ C^r^r6am rf'T f public class MyRunnable implements Runnable { О ^ «к \ Ш' HtW public void run() go О ; public void go() { doMore(); } public void doMore () { HnmfufxhcRiuwa&k ccagxmm ' «Аи MUVrM, /К« ША ЫИШ hum, md tunO ~(£ мфетсяМШ 'id, к< nuhm <ic / fonanuoiu nmux // шшэшт«ии « Ш1СШПШ Htw-7(. ctntka ШжСгС cmtKa dtipM System'out7println(Thread.currentThread().getName() + "• в верхней части стека ); a ftiuuui6& « эпиш npRiufu Hi Thread.dumps tack() . //„— / - спя: к #t ГГ" era «ли— n\jw Kfummux lt,L лкае: , qlriuf. ahum. anx ** зт-"ш- 'MK tWUHu Ml ~ у^шцт/ Httiv « 'TW £« (uK^mr. кяк cfffm Ъшяиш cmck ftyli (? if a urHCUiitaiiii тяйсго mja. 1 fyi<//iuuuai y« Как HE нужно запускать Rnywable Может возникнуть соблазн создать новый экземпляр Runnable и вызвать ме тод run (), но этого недостаточно для создания нового стека вызовов. class RunTester { public static void main(String[] args) { MyRunnable runnable = new MyRunnable(); runnable.run(); System.out.println(Thread.currentThread().getName() + . продолжается в main"); Thread.dumpstack(); Kog b примере HC gacm желаемого результата! Метод runQ был вызван напрямую из метода main(), поэтому он яв¬ ляется частью стека вызовов ос¬ новного потока выполнения. Основной поток выполнения
Как мы раньше создавали поток выполнения Самый простой способ создать поток выполнения — использовать класс Thread, упоминавшийся ранее. Этот метод существует в Java с самого начала, но сейчас он уже не рекомендуется. Мы рассказываем о нем, потому что: а) он простой; б) им действительно пользуются в реальных программах. Позже мы расскажем, почему такой способ — не всегда лучший вариант. class ThreadTester { public static void main(String[] args) { Runnable threadJob = new MyRunnable(); Thread myThread = new Thread(threadJob) myThread.start(); /! Пфитм нгйшзкртлшл Kunna№ (>1нпткт1у1кШ- аклт TT мнслкииеч. KaKijHjMfmj 'muvtiuunt. Мшп txmjfid. сгтскпш НмппаШ станет uiu ftmenuun dam иетекм t'f uij mptHc mv .eetmcc/ timf wi((kVmmup. nemf 7/>...-. . ,v>, i; dt dH стек этого 1\Ьтка 6r.имссиенюн. } System.out.println( Thread.currentThread().getName() продолжается в main"); Thread.dumpstack(); нЬи'е исток ймиинжия. сайт•поейтоздед т,иииз*. пет ft, Нс ft, ^ tW/ мАтГ </ у. шимра JhnJm тиТ<У сшМии «шил- бипийшия*Г М Н/<' о«с кж Jlmad каклнх^ qMtxtZ '""4 Основной ПОТОК выполнения 1Я основного ч ) (main) Теперь у нас есть два независимых стека вызовов. (\п,к битовой 71 TJLJi feiuu«кия lUmuj ctwmjwtackO ftwtv6c«nVfd u/eic/WcittO mo I File Edil Window Help Duo* Новый поток выполнения (лил- ftiA(?с(?a.t.q. побе¬ ге метека б’шттеная, кот turn , еш мпцашли с уауатш Мук nnaffi. А им/, саин dtmd dumf^TackO *- iijMttnujet mainO %java ThreadTester main: back in main Thread-0: top o' the stack java.lang.Exception: Stack trace at java.base/java.lang.Thread.dumpStack(Thread.java:1383) at ThreadTester.main(MyRunnable.java:38) java.lang.Exception: Stack trace at java.base/java.lang.Thread.dumpStack(Thread.java:1383) at MyRunnable.doMore(MyRunnable.java:15) at MyRunnable.go(MyRunnable.java:10) at MyRunnable.run(MyRunnable.java:6) at j ava.base/java.lang.Thread.run(Thread.j ava:82 9) (Jcpamunu Ат-нанне: Mimuj main 0 не маЛсанжсч ocmmwMK стека $н- M метека штатная ИаппаШ.
Более удачное решение: не управлять потоками выполнения вовсе Создание и запуск нового потока выполнения дает вам полный контроль над его работой. Но у этого есть обратная сторона — вам самим придется управлять всеми потоками выпол¬ нения: отслеживать их, следить, чтобы они корректно завершались и не оставались «висеть». А не было бы круто, если бы кто-то другой занимался запуском, остановкой и даже повтор¬ ным использованием потоков выполнения — и избавил бы вас от всей этой рутины? Позвольте представить интерфейс Executor Service из пакета java . util. concurrent. Реализации этого интерфейса выполняют задачи (Runnable). При этом внутри они сами создают потоки выполнения, многократно используют их и завершают когда надо — все ради того, чтобы выполнить переданную им задачу. А класс java.util.concurrent.Executors содержит удобные фабричные методы для создания экземпляров ExecutorService, которые мы и будем использовать. Кстати, исполнители (Executors) появились еще в Java 5, так что они доступны даже в старых версиях. И сегодня почти не осталось причин создавать потоки выполнения вручную через Thread. Запуск одной задачи Вместо конструкторов мо¬ гут использоваться стати¬ ческие фабричные методы. Фабричные методы воз¬ вращают именно ту реали¬ зацию интерфейса, кото¬ рая и была нужна. Нам не нужно знать конкретные классы и разбираться в том, как их создавать. Для простых случаев, с которых мы начнем, нам нужно выполнить только одну задачу в дополнение к основ¬ ному классу. Для этого можно использовать однопоточный исполнитель (single thread executor). class ExecutorTester { public static void main(String!] Runnable job = new MyRunnable( args) { /luumc Mff» .utcnu. iwr JlinuK (sk.w ExecutorService executor = Executors.newSingleThreadExecutor(); PXPmfnr DYOruif О / плМ • » — executor.execute(job) ■hmtu ntfummm UmktSmm ?lUia.ni ftuuMHuluc; см can /it mum, huJL. 4 j'atufcKanu нсвт тмчк йлтлшмЦм' '"l System.out.printin(Thread.currentThread().getName() продолжается в main")- Thread.dumpStack(); executor.shutdown(); 1 наше u a iurac m/myemm ^myrmunu Шике um ™этсм'/• нпти йптцЦфmb СЯ «ршктегнаи шт.н/шт ft tt fsmcjd llvttud txtcukv). /xaule-iMma ам "-'■fr-.taS:—- H iu ?aduqmt: wf* 1 ifjf .. 1.11 lUi oMUfM нсШх par. Мы вернемся к фабричным методам Executors позже и раз¬ беремся, почему ExecutorService может быть предпочтительнее самостоятельного управления потоками выполнения.
Три состояния нового потока выполнения Неважно, создаете ли вы новый поток выполнения через Thread, передав ему Runnable, или запускаете задачу с помощью Executor: она все равно будет выполняться в потоке выполнения. В течение своей жизни поток выполнения проходит через несколько разных состояний. Понимание этих состояний и того, как происходит переход между ними, помогает лучше ориентироваться в многопоточ¬ ном программировании. Новый Готовый 'Маишш состояние потоке! ЙшиИ£ИМЯ1 1 \ I / / Экземпляр Thread создан, но еще не запущен. Иными словами, объект Thread уже существует, но поток выпол¬ нения еще не запущен. Когда вы запускаете поток выполнения, он переходит в состояние runnable. Это значит, что поток выполнения готов к работе и просто ждет своего Большого Шанса — мо¬ мента, когда JVM выберет его для выполнения. Именно на этом этапе для потока выпол¬ нения создается новый стек вызовов. Это идеальное состояние для любого потока выполнения — быть запущенными и выполнять задачу. Но только планировщик потоков выполнения в JVM решает, когда именно поток перейдет из состояния готов¬ ности (runnable) в состояние выполнения (running). В некото¬ рых случаях можно влиять на это решение, но заставить JVM запустить поток принудительно нельзя. Когда поток находится в состоянии выполнения, у него есть активный стек вызовов, и в данный момент исполняется метод, находящийся на вершине этого стека. Но это еще не все. После того как поток вы¬ полнения переходит в состояние готовности, он может переключаться между состояниями готовности и выполнения, а также временно становиться недоступным для выполнения (temporarily not runnable).
Типичный рабочий день потока выполнения Обычно поток выполнения много¬ кратно переключается между состо¬ яниями готовности и выполнения, поскольку планировщик потоков выполнения в JVM выбирает один поток для работы, а затем отпускает его, чтобы дать шанс другому. Готовый Выполняемый Поток выполнения может быть временно переведен в состояние «не готов к выполнению» Планировщик потоков выполнения может перевести работающий поток выполнения в заблокированное со¬ стояние по разным причинам. На¬ пример, поток может пытаться читать данные из входного потока, но данных пока нет. В таком случае планировщик приостанавливает выполнение этого потока, пока данные не станут доступ¬ ны. Или код может явно отправить поток выполнения в «сон» с помощью sleep (). А бывает, что поток ожидал доступ к методу объекта, но тот ока¬ зался «заблокирован» другим потоком. Тогда выполнение приостанавливает¬ ся, пока объект не будет разблокиро¬ ван потоком, удерживающим его. Во всех этих и многих других ситу¬ ациях поток выполнения временно переводится в состояние «не готов к выполнению». Готовый Выполняемый нити с^ля этого мо:тт тть много: I, ьжщанш гааршонмд qj/щгого ш ка пмшпмя, ожщанис уигнупшю ааштх & (’хшпо н потоке, ожидание, пока ссвссоушпся см-кировка ост : стшпа...
Планировщик потоков выполнения Планировщик потоков выполнения прини¬ мает все решения: кто из потоков перейдет из состояния готовности в выполнение, когда это произойдет и при каких услови¬ ях поток покинет состояние выполнения. Именно планировщик решает, какой поток выполнения будет выполняться, как долго он будет работать и что с ним делать, когда его вытесняют. Управлять планировщиком напрямую нельзя. В API нет ни одного метода, который дал бы вам доступ к его внутрен¬ ним механизмам. И самое главное — нет никаких гарантий в отношении планирова¬ ния! (Есть пара «почти-гарантий», но они довольно расплывчатые.) Главный вывод: нельзя строить коррект¬ ность вашей программы на предположе¬ нии, что планировщик будет вести себя определенным образом. В разных реализа¬ циях JVM планировщики могут работать по-разному. И даже на одной и той же машине, при каждом запуске, одна и та же программа может работать не одинаково. Одна из самых частых ошибок новичков — протестировать многопоточную про¬ грамму на одном компьютере и поверить, что планировщик всегда будет работать именно так. Увы, это не так. А как же концепция «написал один раз — запускай везде»? Она все еще работает — если ваша многопоточная программа не зависит от конкретного поведения плани¬ ровщика. То есть нельзя, например, рассчи¬ тывать на то, что все потоки будут полу¬ чать процессорное время в равной степени и строго по очереди. В современных JVM маловероятно, что планировщик будет вести себя совсем уж непредсказуемо, но теоретически он может, например, выбрать поток выполнения номер пять — и дать ему работать до тех пор, пока не завершит¬ ся метод run (). Номер четыре, ты получил достаточно времени — возвращайся в состояние готовности. Номер два, похоже, твоя очередь! Ой, тебе надо поспать... Номер пять, займись его задачей. Номер два, ты все еще спишь... ПланироВщик потоков выполнения принимает Все решения: какой поток Ьыполнения будет работать, а ка¬ кой - нет. Обычно он распределяет Выполне¬ ние между потоками более-менее по очере¬ ди. Но никаких гарантий нет. Он может дать одному потоку Выпол¬ нения работать сколь¬ ко угодно долго, пока остальные будут «го¬ лодать», так и не полу- чиВ сВоей очереди.
Пример непредсказуемости планировщика... Выводится следующий Запуск этого кода на одном компьютере: результат: ffitnnalSjfe эпи фунщисшшнт ш< ' юпавить if час Ни Зауаш спшКи K( ,.J Minii tHumt ыпш ы'№« ,Mtfamu&wnc<i саШ ccfisk class ExecutorTestDrive { public static void main (String[] args) ( ExecutorService executor = Executors.newSingleThreadExecutor() ; executor.execute(() -> System.out.println("в верхней части стека")); System.out.println("продолжается executor.shutdown(); } в main"); •B^Sbssssr Неважно, запускаете ли вы это с помощью ExecutorService, как в приведенном выше коде, или напрямую с потоками, как в приведенном ниже коде; в обо¬ их случаях проявляются одни и те же симптомы. class ThreadTestDrive { public static void main (String(] args) { Thread myThread = new Thread(() -> System.out.println("в верхней части стека")). myThread.start(); ' ' System.out.println("продолжается в main") > File Edit Window Help PickMe % java ExecutorTestDrive продолжается в main в верхней части стека % java ExecutorTestDrive в верхней части стека продолжается в main % java ExecutorTestDrive в верхней части стека продолжается в main % java ExecutorTestDrive продолжается в main в верхней части стека % java ExecutorTestDrive в верхней части стека продолжается в main % java ExecutorTestDrive в верхней части стека продолжается в main % java ExecutorTestDrive продолжается в main в верхней части стека >
Как же мы получили разные результаты? Многопоточные программы не ведут себя одинаково при каждом запуске. При каждом запуске программы планировщик потоков выполнения может распреде¬ лять выполнение по-разному. Иногда программа выполняется так: main() запускает новую задачу. Планировщик пере¬ водит основной поток из состояния выпол¬ нения в состояние готовности, чтобы освободить место для нового потока. Новый поток полу¬ чает возможность завершить свою задачу и напеча¬ тать «в верхней части стека». Основной поток executor .executed moinQ Основной ПОТОК О -> System.out Новый поток После этого он завер¬ шается, а основной поток снова становит¬ ся выполняемым и вы¬ водит «продолжается в main». Основной поток Время ► А иногда — вот так: Метод main() запускает новый поток. Планировщик пере- Новый поток ста- водит основной поток новится выполня- выполнения из со- емым, но времени стояния выполнения оказывается в состояние готовно- недостаточно, сти, чтобы допустить чтобы завершить новый поток к работе, задачу полностью. Планировщик Основной поток переводит его продолжает вы- обратно в состо- полнение и выво- яние готовности дит «продолжается и выбирает для в main», выполнения основной поток. После этого планировщик снова активирует новый поток, и тот печата¬ ет «в верхней части стека». [executor, executed , executor .executefjj Г moinQ moin() 'l 1 0 System.out^| Основной поток Основной поток Новый поток Новый поток Основной поток Новый поток Время Т)аже если ноВый поток Выполнения крошечный и содер¬ жит Всего одну строку кода, ляллЪда-Выражение, плани- роВщик потокоВ Все раВно /ложет прерВать его.
Без лишних слов (Не) дурацкие вопросы ЗоирОС: Стоит ли использовать лямбда- выражение для Runnable или лучше создать отдельный класс, реализующий Runnable? Ol/MBewi: Все зависит от сложности задачи и того, что тебе кажется более читаемым, — лямбда-выражение или полноценный класс. Лямбда-выражения отлично подходят для про¬ стых, коротких задач — например, как в нашем примере с выводом одной строки. Они также удобны, если вы вызываете метод, уже содер¬ жащий нужные действия: executor.execute(() -> printMsg()); Если же задача требует хранения данных в по¬ лях или состоит из нескольких методов, тогда логичнее создать отдельный класс, реализую¬ щий Runnable. Такой подход удобнее для более сложной логики. ЗоирОС: В чем преимущество использо¬ вания ExecutorService? Пока что похоже на обычное создание и запуск потока выпол¬ нения. Оиивеио: Да, в простом случае, когда вы запускаете только один поток и сразу заверша¬ ете приложение, разницы почти не видно. Но настоящее удобство ExecutorService проявля¬ ется, когда вам нужно запускать много неза¬ висимых задач. Создавать отдельный поток на каждую задачу — накладно и неудобно. А вот ExecutorService берет все это на себя. Есть разные реализации этого интерфейса — от фиксированных пулов потоков до гибких, авто¬ матически масштабируемых. С пулом потоков вы можете переиспользовать уже существующие потоки вместо создания новых каждый раз. Это экономит ресурсы и упрощает управление задачами. Об этом мы еще поговорим подробнее чуть позже. Слово thread с маленькой буквы t обозначает в Java от¬ дельный поток выполнения. Кахздый поток выполнения в Java имеет собственный стек вызовов. Слово Thread с прописной буквы Т — это класс java.lang. Thread. Объект Thread представляет поток выполнения. Потоку выполнения нужна задача — и ее можно пред¬ ставить экземпляром класса, реализующего интерфейс Runnable. Интерфейс Runnable содержит только один метод — гип(). Именно он оказывается внизу нового стека вызовов, то есть выполняется первым в новом потоке. Поскольку Runnable имеет только один метод, его удобно передавать в виде лямбда-выражения. Использовать Thread напрямую для запуска отдельных задач сегодня не рекомендуется. Вместо этого лучше вос¬ пользоваться Executor или ExecutorService. Класс Executors содержит вспомогательные методы для создания типовых реализаций ExecutorService, с помощью которых можно запускать задачи. Поток выполнения находится в состоянии Новый, пока не будет запущен. После запуска создается новый стек, внизу которого оказывается метод run() из интерфейса Runnable. Поток переходит в состояние Готовый и ожидает, когда плани¬ ровщик выберет его для выполнения. Поток считается Выполняемый, когда планировщик по¬ токов выполнения JVM назначает его текущим выполня¬ ющимся потоком. На компьютере с одним процессорным ядром одномоментно может выполняться только один поток. Поток может быть временно переведен из состояния Выполняемый в Не готовый. Это случается, если поток ожидает данные из потока ввода, уходит в сон (sleep()), ждет освобождения блокировки объекта. Про блокировки мы поговорим в следующей главе. Планировщик потоков выполнения не гарантирует их вы¬ полнение в фиксированном порядке. Нельзя рассчитывать на то, что потоки будут запускаться строго по очереди.
Усыпление потока выполнения Один из способов содействия потокам выполнения выполнять задачи по очереди — периодически усыплять их. Для этого нужно вызвать статиче¬ ский метод sleep (), передав ему длительность сна в миллисекундах. Например: Thread.sleep(2000); Данный вызов переведет поток из состояния 1^- Выполняемый и не позволит ему снова стать Выполняемым в течение двух секунд. Поток выполнения не сможет снова запуститься, пока не пройдет как минимум две секунды. Метод sleep () выбрасывает InterruptedException, потому что это проверяемое исключение. Поэтому каждый вы¬ зов sleep () должен либо находиться внутри блока try/catch, либо быть объявлен в разделе throws метода. Пример корректного кода: try { Усыпление потока вы¬ полнения предостаЬ- ляет другим потокалл шанс получить Ьремя на выполнение. Когда поток просыпается, он Ьсегда переходит Ь со¬ стояние «ГотоЬый» и ждет, пока планироЬ- сцик потоков выполне¬ ния сноЬа ЬыЬерет его для запуска. Thread.sleep(2000); } catch (InterruptedException ex) { ex.printStackTrace (); } Теперь поток выполнения гарантированно не проснется раньше указанного времени. Но мо¬ жет ли он проснуться позже? Технически — да. Когда таймер сна истекает, поток не становится Выполняемым автоматически. Он лишь пере¬ ходит в состояние Готовый и снова ждет, пока планировщик потоков выполнения выделит ему процессорное время. Поэтому нельзя точно предсказать, когда он продолжит выполнение. Понять, сколько времени представляет собой заданное количество миллисекунд, может быть непросто. В классе java.util. concurrent.TimeUnit есть метод с удобным вариантом указания времени ожидания: TimeUnit.MINUTES.sleep(2); Такой код интуитивно понятнее, чем: Thread.sleep(120000); (Однако оба варианта все равно нужно оборачивать в конструкцию try-catch.)
Усыпление потока выполнения предоставляет шанс запуска другим потокам Помните наш предыдущий пример, в котором результат запуска программы был разным? Вернитесь к коду и по¬ смотрите на вывод еще раз. Иногда метод main дожидал¬ ся завершения нового потока и только потом выводил текст «в верхней части стека». В других случаях новый поток не успевал завершиться и отправлялся обратно в состояние Готовый, в то время как основной поток снова начинал выполняться и печатал текст «продолжа¬ ется в main». Как это исправить? Задумайтесь: где нужно разместить вызов sleep (), чтобы строка «продолжается в main» всегда выводилась до строки «в верхней части стека»? class PredictableSleep { public static void main (String[] args) { ExecutorService executor = Executors.newSingleThreadExecutor(); executor.execute(() -> sleepThenPrint()); System.out.printin("продолжается в main") executor.shutdown(); > private static void sleepThenPrint() { try { TimeUnit.SECONDS.sleep(2); ^ } catch (InterruptedException e) { e.printStackTrace(); System.out.println("B верхней части стека уИетод Tkttad.sCeefiO ftu|wrtv datm nfu ditXMt-UXt UCKAWtCHUt. .... ,.L лЛ'1/нwr кий мижно. Komchct сгшт»щи() 1 пфех&ипить, .низе (CMttoni. A C llUO пескам,ku nojfttxdam (taan u uc tc-ж течет ксу xifjnm, ,im htuiniM лотку d отмлшш .Mtmoejf. WLfu nfin йихеине- Huu < птлтая паша (около q(k\- огкЦиа), nfu.m/t you notLiyoJe &l (fa, ужтц стока». ля г <ПЩкя', Мотт «(((fchxmk Hi Нот шо нале нцменя — micmojc& ими пе/ш/ек Йлвеосе са синений: ) ; ст ко&ш тяе.еееи». Zm cal*» refuse- Прокачай извилины Подумайте, какие проблемы могут возникнуть, если усыпить потоки выполнения на заданное время? И сколько времени потребуется, чтобы выполнить такой код десять раз?
Недостатки усыпления потоков выполнения 0 Программа вынуждена ждать как минимум указанное время. Если перевести поток выполнения в спящий режим на две секунды, он не будет активным в течение этого времени. Когда он проснется, это еще не значит, что он сразу продолжит выполнение — он просто вернется в состояние Готовый и снова будет ждать своей очереди у планировщика потоков. Приложе¬ ние в этом месте гарантированно «зависнет» минимум на две секунды, а иногда и дольше. Пока это не кажется серьезной проблемой, но представьте себе большое приложение, утыканное такими паузами: работа замедлится заметно. 0 А вы уверены, что за это время программа успеет выполнить другие задачи? Мы отправили новый поток в сон на две секунды, предполагая, что за это время основной поток успеет все сделать. А если основной поток будет работать дольше? Или процессор в это время отдаст при¬ оритет другому потоку с более ресурсоемкой задачей? Можно попробовать увеличить время сна — «на всякий случай». Но тогда мы снова сталкиваемся с первой проблемой: приложение еще дольше проста¬ ивает в ожидании. А есть ли способ, чтобы один поток выполнения мог сообщить другому, что за¬ кончил работу? Тогда основной поток мог бы просто дождаться такого сигнала и продол¬ жить выполнение, убедившись в безопасности выполнения. Лучшая альтернатива — дождаться подходящего момента Нам хотелось бы, чтобы в основном потоке выполнения произошло опреде¬ ленное событие, прежде чем запускать новый поток. В Java предусмотрено множество механизмов для такой синхронизации: Future, CyclicBarrier, Semaphore, CountDownLatch — и это только часть инструментов. Когда в приложении работают несколько потоков, часто возникает необходи¬ мость координировать их действия. Чтобы скоординировать события, про¬ исходящие в нескольких потоках вы¬ полнения, одному потоку может пона¬ добиться дождаться сигнала от другого, прежде чем продолжить работу.
Обратный отсчет до готовности Можно организовать обратный отсчет — чтобы поток (или несколько потоков выполнения) ждал, пока не про¬ изойдут некие важные события. Он/они не продолжит выполнение, пока все не будет готово: например, пока не подключатся нужные клиенты или не запустятся все не¬ обходимые сервисы. Для таких случаев и создан класс j ava . util. concurrent. CountDownLatch. Вы задаете чис¬ ло — с него начнется отсчет. Когда происходит нужное событие, поток сообщает защелке, что счетчик следует уменьшить. Как только счетчик достигнет нуля, ожидаю¬ щие потоки смогут продолжить работу. В нашем примере мы будем отслеживать всего одно со¬ бытие: новый поток должен дождаться, когда основной поток напечатает строку «продолжается в main», пре¬ жде чем продолжить выполнение. import java.util.concurrent.* ; class PredictableLatch { public static void main (String!] args) { ExecutorService executor = Executors.newSingleThreadExecutor() CountDownLatch latch = new CountDownLatch(1);^ — CountDownLatch — это барьер синхронизации, то есть меха¬ низм, позволяющий потокам выполнения синхронизировать¬ ся между собой. Барьеры ис¬ пользуются, когда нужно, чтобы потоки подождали друг друга в определенной точке выполне¬ ния, прежде чем продолжить, К другим примерам барье¬ ров синхронизации относятся CyclkBarrier и Phaser. fllUfAKU cn,fn«V, ** <£%**•£ executor.execute(() -> waitForLatchThenPrint(latch)) , System.out.println("продолжается в main"); latch.countDown(); птах ut? , / executor. shutdown (); fiMcqafiuivi пинала. Пфатм.eU CainWcwnlAk ccfl fiKCW law-' (M fooC^ private static void waitForLatchThenPrint(CountDownLatch latch) { л л ^latch.awaitO, * ) catch (InterruptedExceptiojj. e) { (f^n 1 ,у.глит1Х;Я (ValwAHUU /Г( i e .printStackTrace () ; l|t|( 111 П lf ^ • iitinui ttmxftux. System.out.printIn("в верхней части стека") } m^»*LS3ksr‘ mmt. Код действительно напоминает вариант с sleep (), основное отличие — вызов latch. countDown () в методе main. Однако разница в производительности колоссальна. Вместо ожидания не менее двух секунд, чтобы убедиться, что main вывел свое сообщение, новый поток выполнения ждет ровно до тех пор, пока метод main не напечатает «продолжается в main». Чтобы получить представление о разнице в производительности в реальных условиях: при запуске вари¬ анта с CountDownLatch 100 раз на компьютере МасВоок выполнение заняло около 50 мс, и при каждом запуске сообщения выводились в правильном порядке. А теперь представьте: если одна единственная задержка через sleep () длится более 2 с (2000 мс), то сколько времени ушло бы на 100 запусков?' •ОПП1И 1 п А ППГ) пт иллпаииап
Создание и (и более!) запуск двух потоков выполнения допустим, нам нужно запускать током выполнения. Очевидно, чтоSecuTorT’ ‘°™»»нь» „о- здесь не подойдет - он допускает выполнение т ' newSj-n9leThreadExecutor () тогда использовать? Какие д„ст?„„ы „риа„ ”?“” °ДН0ГО по™» за раз. А что java.util.concurrent.Executors либо с задержкой, либо с периодичностью. ExecutorService newSingleTbreadExecutorO виполн»ет Создает исполни,ел. с одни» рабочим потоком вмполнения, задачи из неограниченной очереди по одной. ■tr==r=s======:~ кой или ПО расписанию. ExecutorService newWorkStealingPoolO stealing)< использую- щи’йДОСтТпн»™^^ 1 (Дш и umfuu tpa- | f jiiiwin .методик ™™™<Г‘ОГ8вПГке'РеаЛИЗадии в той или иной форме ис- котопьм м УЛ ПОТОКОВ ~ совокупность экземпляров Thread, в=нГяЖзГГЛИ0В№ (В ТОМ ЧИСЛе —*-1 лля П0Т0К08 8 "Уле>а «иже поведение при избыт- ____ сравнению с числом доступных потоков зави¬ сит от конкретной реализации ExecutorService.
Создание пула потоков выполнения Использование пула ресурсов - особенно тех, создание которых требует зна¬ чительных затрат, например, потоков выполнения или подключений к базам данных, — распространенный прием в разработке приложений. .. ... „сколько rvomcKO#ftsuvoA- ЯмлМСЖ*гаияшдме&иую.. трж \ X ^ Jkiw. ЯстмЫлдт uhuwAuжатьсм !«W WH «йльго&юлься, a (?ип1««;таот^ад,|^ ла. jwcomam и попсксти ftuu, i мпуекя глася, ткт cQjm нет. itjvtfamicA. Когда вы создаете новый ExecutorService, его пул может сразу содержать несколько потоков выполнения либо начинаться с пустого на¬ бора. Создать ExecutorService с пулом потоков можно с помощью одного из вспомогательных методов класса Executors: ExecutorService threadPool = Executors.newCachedThreadPool(); Этап поток (ftauu- ненчя ntimnantfatm. Вы можете использовать потоки из пула для вы¬ полнения задачи, передав ее в ExecutorService. Он сам определит, есть ли свободный поток выполнения, который можно задействовать для запуска этой задачи: threadPool.execute(() -> run("Job 1")); Таким образом, ExecutorService может повторно использовать потоки выполнения — он не про¬ сто создает и уничтожает их каждый раз. ^ fiaM ,wnwKa* fanou СЪаактся нс(5'ш по№<р11 жтшшт уля оф^0ПК n(cmifn/miu,kxjcuf‘rl Когда вы передаете ExecutorService несколько задач, он может создавать и запускать новые потоки выполнения для их обработки. Если задач больше, чем доступных потоков, лишние задания могут быть поставлены в очередь. Поведение ExecutorService при поступлении дополнительных задач зависит от его конфигу¬ рации. threadPool.execute((> -> run("Job 324")); ExecutorService также может завершать потоки выполнения, которые бездействовали в течение некоторого времени. Так сокращается исполь¬ зование ресурсов системы, таких как процес¬ сорное время и оперативная память, а прило¬ жение работает эффективнее.
Запуск нескольких потоков выполнения Следующий пример демонстрирует запуск двух задач с использованием пула потоков выполнения фиксированного размера. Пул создает два потока, и каждому из них назначается одна и та же задача: выполнять цикл, в кото¬ ром на каждой итерации выводится имя текущего потока выполнения. import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public static void main (String [] args) { (?rU\^mu,T'b m ’ ' ExecutorService threadPool = Executors.newFixedThreadPool(2); public class RunThreads { threadPool.execute(() -> runJob("Задача 1")) threadPool.execute(() -> runJob("Задача 2")) threadPool.shutdown(); famM. } аПГхспш.Ыт. & /тжх.пим, .мсжщ: по public static void runJob(String jobName) { for (int i = 0; i < 25; i++) { String threadName = Thread.currentThread().getName(); System.out.printin(jobName + " выполняется на " + threadName); п.шх KinrZZ™ пТГт i(e( аыаш г {}* £> т ^Шт^как / • fy/ги!witfPe рана (?этой. гла&. } } ,uu. 4(umt йя(5am, ксща м,икл шиитт25 тхЦьщш. ‘ Что произойдет? Будут ли потоки чередоваться? Увидите ли вы, как сменяются их имена? Как часто это будет происходить — при каждой итерации? После пяти? Вы уже знаете ответ: мы не знаем! Все зависит от планировщика потоков. В вашей операцион¬ ной системе, с вашей версией JVM и на вашем процессоре поведение может быть совершенно другим. Если выполнять такую задачу на современной многоядерной системе, оба задания, скорее все¬ го, будут работать параллельно. Но даже в этом случае нет никакой гарантии, что они завер¬ шатся одновременно или будут печатать строки с одинаковой скоростью. j File Edit Window Help Globetrotter % java RunThreads Задача 1 выполняется на pool-1-thread-l Задача 2 выполняется на pool-l-thread-2 Задача 2 выполняется на pool-l-thread-2 Задача 1 выполняется на pool-1*thread-1 Задача 2 выполняется на pool-l-thread-2 Задача 1 выполняется на pool-1-thread-1 Задача 2 выполняется на pool-l-thread-2 Задача 1 выполняется на pool-1-thread-1 Задача 2 выполняется на pool-l-thread-2 Задача 1 выполняется на pool-1-thread-1 Задача 1 выполняется на pool-l-thread-1 Задача 1 выполняется на pool-1-thread-1 Задача 1 выполняется на pool-l-thread-1 Задача 1 выполняется на pool-l-thread-1 Задача 1 выполняется на pool-l-thread-1 Задача 1 выполняется на pool-l-thread-1
Завершение пула потоков выполнения Вы, наверное, заметили, что в наших примерах в конце метода main всегда вызывается threadPool. shutdown () . Хотя пул потоков сам управляет своими рабочими по¬ токами, нам все равно стоит вести себя как ответственные разработчики и закрывать пул, когда он больше не нужен. Это позволяет очистить очередь задач и завершить все потоки, чтобы освободить системные ресурсы. У Executor Service есть два способа завершения работы. Можно использовать лю¬ бой, но для надежности мы обычно вызываем оба: © ExecutorService.shutdown() Вызов shutdown () — это вежливый способ попросить ExecutorService за¬ вершить работу. Все потоки, которые уже выполняют задачи, смогут спокойно их закончить. Задачи в очереди тоже будут выполнены. Новые задачи при этом больше не принимаются. Если вы хотите дождаться полного завершения всех заданий, можно использовать метод awaitTermination (). Методу передается максимальное время ожидания: он будет ждать, пока либо все задачи не завершатся, либо не наступит тайм-аут (что произойдет раньше). (2) ExecutorService.shutdownNow() Все — на выход! При вызове shutdownNow () интерфейс ExecutorService попыта¬ ется немедленно остановить все выполняющиеся потоки, не запускает ничего из очереди и не принимает новые задачи. Такой метод полезен, если нужно срочно все остановить. Иногда его вызывают после метода shutdown () , чтобы дать заданиям немного времени на завершение, прежде чем окончательно закрыть пул. Jlfwmu cxtcum- Stma, /абфшить fxamnlj. Лаж myc6a.muUlcwn() cJkmfci (£лы«с шЛугннн.н/ып &лк. ruauimanure бщбапи шсикО паж 9hw rJu<«m ■ йи^ишмс нсклн)- 1ШШ Ru tckd/.v- tmTicncxcefitan. Jlfm это м 6a ШПМЩН/ННМС UftlHAUMt jaqan и.я dyyym Т/мАсмныш. a /' и/ у бух rwmoKctftftuu public class ClosingTime { • ,7 / .— " «muui 11 teniiq iblic static void main(String[] args) { ^ ExecutorService threadPool = Executors.newFixedThreadPool(2) threadPool.shutdown(); threadPool.execute(new LongJob("Долгая задача")); <- threadPool.execute(new ShortJob("Короткая задача" )T; ■Запускаем аба mianm: кс1итксе и «сули. кос»; Almtm tmcJnw i\aatnam нмя пстокд. a imdMxпшАФммтея trY ( пемшц. лбетО. Сса масса jxuhmMum boolean finished = threadPool.awaitTermination(5, TimeUhit,_SECO— System.out.println("Finished? " + finished); } catch (InterruptedException e) { e.printstackTrace(); } threadPool.shutdownNow(); Mtjc и ас !) секта m(kh шшия mxjaqar. Шли m указанна тсим бее. нгса&А шитая, мопсу бернст j,am, } Ппт(ш «.rumen ihutdcwnNcwO, medtie мт\г '„ъЖпсся шмтмнш) тмнобшт 6а потеки, (сли^ сщжга иш Мышлш.бплб т уаст^кта пfxunn Н1«м( нс tyu5u|(urpn.
Да, есть и темная сторона. Многопоточность может привести к проблемам с параллелизмом. А проблемы с параллелизмом приводят к состоя¬ ниям гонки. Состояния гонки приводят к повреж¬ дению данных. А повреждение данных приводит к страху... ну, остальное вы уже знаете. Все сводится к одному потенциально опасному сценарию: два или более потока выполнения полу¬ чают доступ к данным одного и того же объекта. То есть методы, запущенные в разных стеках вызовов, обращаются к геттерам или сеттерам одного и того же объекта в куче. Это из серии: левая рука не знает, что делает правая. Два потока с радостным жужжанием исполняют свои методы; каждый уверен, что он — единственный поток, который что-то делает и действительно важен. А ведь когда поток вре¬ менно не выполняется (например, при блокировке или переключении), он буквально теряет сознание. И когда планировщик снова дает ему процессор, он не имеет ни малейшего понятия, что происходило в это время с объектами, к которым он обращался. Без лишних слов ■ Статический метод Thread.sleep() переводит поток выполнения в состояние ожидания как минимум на указанное время. Например, Thread. sleep(200) усыпляет поток на 200 миллисекунд. ■ Вы также можете вызывать sleep() через java. util.concurrent.TimeUnit, например: TimeUnit. SECONDS.sleep(2) — делает то же самое, но вы¬ глядит понятнее. ■ Метод sleep() выбрасывает проверяемое ис¬ ключение (InterruptedException), поэтому вызовы метода sleep() нужно либо оборачивать в try/ catch, либо объявлять в throws. ■ Существует несколько способов синхронизации потоков. Помимо sleep(), вы можете использовать CountDownLatch, чтобы дождаться определенно¬ го числа событий перед продолжением выпол¬ нения. ■ Ручное управление потоками может быть трудо¬ емким. Вместо этого используйте фабричные ме¬ тоды из Executors для создания ExecutorService, через который удобно запускать задачи Runnable. ■ Пулы потоков выполнения управляют созданием, повторным использованием и завершением по¬ токов; вам не нужно делать это вручную. ■ ExecutorService следует корректно завершать. Используйте метод shutdownQ для аккуратного завершения работы и shutdownNow() — для не¬ медленной остановки всех задач и потоков. Это интрига! Темная сторона? Расовые условия? Повреждение данных?! Но... что мы можем с этим сделать? Не бросайте нас в подвешен¬ ном состоянии! Следите за следующей главой: там мы разберем все эти и другие опасные моменты. Стано¬ вится жарко.
Упражнение Группа Java-специалистов и сетевых терминов в костюмах играет в «МаскаКод». Они дают вам подсказки — все, что могут рассказать о себе (честно, но без раскрытия имен). Ваша задача — угадать, кто они, основываясь на их репликах. Если реплика может относиться к нескольким участникам, запишите всех, кто ей соответствует. Сегодняшние участники: InetSocketAddress, SocketChannel, IP-адрес, имя хоста, порт, Socket, ServerSocketChannel, Thread, пул потоков выполнения, Executors, ExecutorService, CountDownLatch, Runnable, InterruptedException, Thread.sleep() ^«oKog Если меня не закрыть, я могу жить вечно. Я позволяю устанавливать соединение с удаленной машиной. Я могу быть выброшено при вызове sleep!) или await!). Если вы хотите повторно использовать потоки, вам нужен я. Вы не сможете подключиться к другой машине, не зная меня. Я веду себя как отдельный процесс, запущенный на вашей машине. Я могу предоставить вам нужный ExecutorService. Если вы хотите, чтобы к вам подключались клиенты, без меня не обойтись. Я помогаю сделать многопоточный код более предсказуемым. Я представляю задание, которое нужно выполнить. Я храню IP-адрес и порт сервера. Ответы на стр. 672
Обновленный и улучшенный класс SimpleChatClient В самом начале главы мы создали чат SimpleChatClient, который умел отправлять со¬ общения на сервер, но не мог ничего принимать. Помните? Именно тогда мы впервые подошли к теме потоков выполнения — ведь нам нужно было делать две вещи одновременно: отправ¬ лять сообщения на сервер (взаимодействуя с графическим интерфейсом) и одновременно получать входящие сообщения и отображать их в прокручиваемом текстовом поле. Перед вами новая, улучшенная версия чат-клиента, которая благодаря многопоточности может и отправлять, и принимать сообщения. Не забудьте: прежде чем запустить этого клиента, сна¬ чала нужно запустить сервер чата. import javax.swing.*; import j ava.awt.*; import j ava.awt.event.*; import j ava.io.*; import java.net.InetSocketAddress; import java.nio.channels.*; import java.util.concurrent.*; ^>a- •/ этой главы '/< Hcmdume инс сшит конец }{(j Ht ctwtac. import static java.nio.charset.StandardCharsets.UTF 8; public class SimpleChatClient { private JTextArea incoming; private JTextField outgoing; private BufferedReader reader; private PrintWriter writer; public void go() { setUpNetworking(); JScrollPane scroller = createScrollableTextArea() outgoing = new JTextField(20); JButton sendButton = new JButton("Отправить"); sendButton.addActionListener(e -> sendMessage()); JPanel mainPanel = new JPanel(); mainPanel.add(scroller); mainPanel.add(outgoing); mainPanel.add(sendButton); ExecutorService executor = Executors.newSingleThreadExecutor() executor.execute(new IncomingReader()); JFrame frame = new JFrame("Наипростейший чат-клиент"); frame.getContentPane().add(BorderLayout.CENTER, mainPanel); frame.setSize (400, 350); frame.setVisible(true); frame.setDefaultCloseOperation(WindowConstants,EXIT_0N_CL0SE) Сс^тямам ^Гс Kanc^u U финский If* Лит о m сен не н ей- го, Kf4,m шнсм йш’нем (fima.AU — гапцека невеге пспика йташнча. мс- ni.UAft. (К Н уля ddufll. ■ 41 ы а с cadu.eu nedee facta - ши: myrnfiiHuuie класс, ил uulfKiu,m Kunnaddc. W лиати шта- епитннт му шпека (tfmpa м ттфашгет №ЛЯ меш еисщенчя d nfuKjujmdaeucu тек- стсСсм ссГшсти. Юла его ^пусками ист.лицеи ufiuiunu mnu ист Леи теле, щтеми /тс паи нижи о мпцетти теле¬ кс ujHccfn не dee faefauue.
private JScrollPane createScrollableTextArea() { incoming = new JTextArea(15, 30); incoming.setLineWrap(true); incoming.setWrapStyleWord(true); incoming.setEditable(false); JScrollPane scroller = new JScrollPane(incoming); scroller.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL SCROLLBAR дтw scroller.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL ' return scroller; _SCROLLBAR_NEVER); бстмсгатсимт .«imut кот тштСик, Диаш f w отс^йш-еиил теш,стиле } private void setUpNetworking() { try { InetSocketAddress serverAddress = new InetSocketAddress("127.0.0 1" SocketChannel socketChannel = SocketChannel.open(serverAddress); 5000) ; reader = new BufferedReader(Channels.newReader(socketChannel, utf 8)) writer = new PrintWriter(Channels.newWriter(socketChannel, UTF 8)7- System.out.println("Подключение установлено.") catch (IOException ex) { ex.printStackTrace(); тштЩа « Мест nn/iK- лк/АННогс к ctpefuj. 7Гсн\ск titw- ) («’JuU-HUA Hi» • ceh(?e»V private void sendMessage() ( writer.println(outgoing.getText()) writer.flush(); outgoing.setText(””); outgoing.requestFocus(); ) Vn\o\m ч mc ксяйнся u.tumcK mrt’ KmJm кгМмт cfMfcMU pom* mo. tuCitnt акс.тмМ c cepfitfui. Get. public class IncomingReader implements Runnable public void run() ( String message; try { while ((message = reader.readLine()) != null) System.out.println("Прочитано: " + message) incoming.append(message + "\n"); } } catch (IOException ex) { ex.printStackTrace(); { T r ) > > public static void main(String[] args) ( new SimpleChatClient().go(); } A 6cm это — ндотпяшдя места потока ftiwewntniwu /3 жтеж шпО поток fooqum (1 u,uk i u остается ниш, пе ка cmтеме от apGovi те леке ейатся (пдеТО. 6 т ст- тмвешге reccWmuA строка л» строкой и ассабшт к аж-' сшн) и? тил ь’ п^нк^ири биолите тексто(?ию Шасть — белеете с силебеиилл исблл строки.. )
Магнитики с кодом На холодильнике в беспорядке разбросаны магнитики с кодом рабочей Java-программы (см. следующую страницу). Фрагменты перепутались: кажется, будто кто-то играл ими. Ваша задача — переставить кусочки кода в правильном порядке, чтобы получилась рабочая программа. Она должна скомпилироваться и вывести результат, показанный ниже. Чтобы эта программа заработала, необходимо предварительно запустить SimpleChatServer — его код вы найдете на стр. 642. *■ I File Edit Window Help StillThere % java PingingClient Соединение установлено 09:27:06 Отправлен ПИНГ 0 09:27:07 Отправлен пинг 1 09:27:08 Отправлен пинг 2 09:27:09 Отправлен пинг 3 09:27:10 Отправлен пинг 4 09:27 :11 Отправлен пинг 5 I 09:27:12 Отправлен пинг 6 09:27:13 Отправлен пинг 7 09:27:14 Отправлен пинг 8 09:27:15 Отправлен пинг 9 Ответы на стр. 672,
Магнитики с кодом (продолжение) | try (SocketChannel channel = SocketChannel. String message = »ping « + open (server)) { ) import static java.nio.charset.StandardCharsets.UTF_8; import static java.time.LocalDateTime.now; import static java.time.format.DateTimeFormatter.ofLocalizedTime;
/ЛаскаКод (СО стр. 667) Решения упражнений Если меня не закрыть, я могу жить вечно. Я позволяю устанавливать соединение с удаленной машиной. Я могу быть выброшено при вызове sleepO или awaitl). Если вы хотите повторно использовать потоки, вам нужен я. Вы не сможете подключиться к другой машине, не зная меня. Я веду себя как отдельный процесс, запущенный на вашей машине. Я могу предоставить вам нужный ExecutorService. Если вы хотите, чтобы к вам подключались клиенты, без меня не обойтись. Я помогаю сделать многопоточный код более предсказуемым. Я представляю задание, которое нужно выполнить. Я храню IP-адрес и порт сервера. &xcutorSmict SccketCkann&C Socket Jnttrrupted£ecej\ticn. Thread licc(? £xxcutorStrvict JP Address, Host name port Tbroad &xmtcrs SmtrSocketOumneC Tkriad.sCeepO, CountjDownjLatck RunnadCt J ndSocketAddress import java.io.*; import java.net.InetSocketAddress; import j ava.nio.channels. *; import java.time.format.FormatStyle; import java.util.concurrent.TimeUnit; Магнитики с кодом (со ар. 670-671) import static java.nio.charset.StandardCharsets.UTF 8; import static java.time.LocalDateTime.now; import static java.time.format.DateTimeFormatter.ofLocalizedTime; public class PingingClient { j]b\ аслжнм imAtttiuw mcnv :ytc\ public static void main(String[] args) { InetSocketAddress server = new InetSocketAddress("127.0.0.1", 5000); try (SocketChannel channel = SocketChannel.open(server)) { PrintWriter writer = new PrintWriter(Channels.newWriter(channel, UTF_8)) System.out.println("Подключение установлено") { qaже если ntpf.AAtctuunve s(\c|\0 <5 (JhMMfK 'UU,iw V )|1Y( l(|RU ле tv. for (int i = 0; i < 10; i++) String message = "ping writer.println(message) writer .flush () ; String currentTime = now().format(ofLocalizedTime(FormatStyle.MEDlOMH - System.out.println(currentTime + " Отправлено: " + message) TimeUnit.SECONDS.sleep(1); } } catch (IOException | e.printStackTrace(); InterruptedException e) { Мг,уА СПЧ<-4п,г <?н,,ПЩи 0/,ти'чю( паи Иск. i нт н н.я.ин mpxflo тм&ыл* (? конце,, потопу mu (w,v c! a, CrtUIUIKcft. КсЩе..цап( F
Готовый код *• Kcnja dt\ нажишшш KHu\Kij «Отл|ла- Gunu*, баше coctJuAjtaut (tweemt ( mt Kijiu,n u |UUU.lU«tCKW.U JAWCIJH к(ш omnfm&iatmca Cyber BeatBox О о о о Start Stop Tempo Up Tempo Down sendlt танцевальный ритм SamSampler: грув 2 MorganMove: грув2 ремикс BoomTish: танцевальный ритм Теперь, когда вы увидели, как работает чат-клиент, настало время для финальной версии Битбокса! Он подключается к простому серверу MusicServer, благодаря чему вы можете отправлять и получать ритмические рисунки от других клиентов. Код получился довольно длинным, поэтому полный листинг приведен в приложении А. Йхосршще смещения т jf'fu-x игреков (>ш сражаются в списке. щелкпшпе tu (ри. uij <V них, ппстуагрууипоь CWMfWflU с ни и ршп. кип ении рисунок, и липо к нажишт кнопки «(hup весприуви ти его. пт», гтссп
18. Состояние гонки и неизменяемые данные Решение проблем с параллелизмом * Дэйв даже не заметил, что Хелен кусает его бутерброд, пока он его ест! Этим двоим стоит понять, что они делят один ресурс, иначе все быстро вы- идет из-под контроля... Решать две или более задачи одновременно непросто, писать многопоточный код — легко. А вот написать многопоточный код, который действительно работает так, как вы ожи¬ даете, — куда сложнее. В заключительной главе мы покажем, что может пойти не так, когда несколь¬ ко потоков выполнения работают одновременно. Вы познакомитесь с инструментами из пакета java, util.concurrent, которые помогут писать многопоточный код, работающий правильно. Вы также уз¬ наете, как создавать неизменяемые объекты — такие, которые нельзя изменить после создания, — и почему они особенно полезны в многопоточном окружении. К концу главы у вас в руках будет полный набор инструментов для разработки безопасных и надежных многопоточных программ.
Что может пойти не так? В конце предыдущей главы мы намекнули, что при работе с многопоточным кодом не все так ра¬ дужно и солнечно. Хотя, честно говоря, это был не просто намек. Мы прямо сказали: «Все сводится к одному потенциально опасному сценарию: два или более потока выполнения получают доступ к данным одного объекта». Эти. аба потока выполнен^ < Ч I бсльстбиом. читают и лчпио'_ iimuuu б един и той ш ^ недсЛсбад, что fvuquu K.mt liu J скЭдст mo .же самое. Они и(Г аегааабаются, что дни гои lu,Wu.,a —■ жт блшиибсигися в vv v uMw>u ипсслоастбил такого бдаивтс ли гцт (nmi Hin|vtrjCка?ЧО.иьл~WU Прокачай ® чем заключается проблема, когда два потока выполнения одновременно читают и изменяют один и тот же объект? Если поток сначала считывает данные объекта, прежде чем их изменить — в чем опасность, если в это время другой поток может что-то записывать?
Брак в беде Можно ли спасти эту пару? Специальный выпуск «гШоу доктора Стива» Эпизод #42 — транскрипция Добро пожаловать на шоу. Сегодня у нас история, где во главе конфликта — не чувства, а финансы. Знакомьтесь: Райан и Моника. У них общий банковский счет. Пока что. В чем проблема? Классика жанра: два человека, один ресурс — и полное отсутствие синхронизации. Моника: Мы договорились: никто не тратит деньги, не проверив баланс. Порядок действий простой — сначала смотришь, сколько на счете, потом уже платишь. Звучит разумно? Конечно. А потом нас накрыло овердрафтом! Райан (в кадре, немного растерян): Я положил в корзину товаров на 50 баксов. Проверил счет — там 100 баксов. Все четко. Начал оформлять заказ. Моника: И туг в дело вступаю я. Я тоже проверяю счет — те же 100 долларов. Думаю: «Почему бы не потратить их?» — и трачу. А Райан, ничего не зная об этом, нажимает «Оплатить». Бах! Овердрафт. Доктор Стив (в камеру): Он не знал, что она в этот момент уже тратит. Она не знала, что он вот-вот нажмет кнопку. Никакой защиты от одновременного доступа. Проблема ясна: их код доступа к счету не потокобезопасен. Разлом, и. Ионика: ■mjunda прссжлш «4 гок йка — cam rnm » Реклама прервет нас ненадолго, но подумайте: можно ли сделать так, чтобы один из них «блокировал» доступ к счету, пока другой им пользуется? Возвращайтесь после паузы — и мы обсудим, можно ли сохранить отношения...
Проблема Райана и Моники: теперь в коде Следующий пример показывает, что может произойти, если два потока выполнения (в ролях — Райан и Моника) совместно используют один объект — банковский счет. В коде участвуют два основных класса: BankAccount и RyanAndMonicaJob. А также — RyanAndMonicaTest, содержащий основной метод, запускающий все в действие. Класс RyanAndMonicaJob реализует Runnable и моделирует поведе¬ ние Райана и Моники: они проверяют баланс и тратят деньги. У RyanAndMonicaJob есть переменные экземпляра: общий объект BankAccount, имя участника (Райан или Моника), и сумма, которую он хочет потратить. Код рабо¬ тает следующим образом: ^ Создайте экземпляр общего банковского счета BankAccount int balance getBalance() spend() Создание нового счета позволит правильно инициализи¬ ровать все параметры по умолчанию; BankAccount account = new BankAccount(); Runnable RyanAndMonicaJob BankAccount account String name intamountToSpend run() goShoppingQ Создайте no одному экземпляру RyanAndMonicaJob для каждого участника Нам нужно по одной задаче для Райана и Моники. Каждой задаче передаем имя, общий счет и сумму, которую он хочет потратить. RyanAndMonicaJob ryan = new RyanAndMonicaJob("Райан", account, 50); RyanAndMonicaJob monica = new RyanAndMonicaJob("Моника", account, 100); ^ Создайте службу ExecutorService и передайте ей обе задачи Поскольку у нас два задания — одно для Райана, другое для Моники, — мы можем использовать пул потоков фиксированного размера с двумя потоками; ExecutorService executor = Executors.newFixedThreadPool(2); executor.execute(ryan); executor.execute(monica); ® Наблюдайте за выполнением задач В методе goShoppingd каждый поток выполняет тоже, что и Райан с Моникой в реальной жизни:проверяет баланс и тратит день¬ ги — если они есть. Один поток выполняет задание Райана, другой — Моники. Оба потока сначала проверяют баланс, а затем, если средств достаточно, тратят деньги. Но важно помнить: при параллельном выполнении нель¬ зя считать, что только ваш поток вносит изменения в общий объект (BankAccount). Даже если код обращения к счету — всего две строчки и они находятся рядом друг с другом: if (account.getBalance() >= amount) ( account.spend(amount); } else { System.out.println("Извините, средств недостаточно"); Это должно защитить от перерасхода... но только на первый взгляд. Если Райан и Моника тратят деньги одно¬ временно, средства на счете могут закон¬ читься до того, как второй успеет завер¬ шить свою операцию. )
Пример с Райаном и Моникой inport java.util.concurrent.*; public class RyanAndMonicaTest { public static void main(String[] args) { BankAccount account = new BankAccount() JaautuMfx /Joivk-'lfccimt vm JuntKrt ftmoAHtmiA - Pau.- ан a J шика - jvianvmm c oaaa a w п\*м ж ctemau, me am, путчисты к cym*ty 0(Л»Д>Чи| (jCCt KIHlj (оууаему€е jcujam, согласие RyanAndMonicaJob ryan = new RyanAndMonicaJob("Райан", account, RyanAndMonicaJob monica = new RyanAndMonicaJob("Моника", account 100); ExecutorService executor = Executors.newFixedThreadPool(2); ' imu executor.execute(ryan); executor.execute(monica) executor.shutdown(); ;е?а>хТ‘каш cJtjayatM. ) rCm\i mf ta. r ^ Ж’*"**"*' £££**&* Vя ЪЫС nvсупами Kyat и. class RyanAndMonicaJob implements Runnable { private final String name; private final BankAccount account; private final int amountToSpend; RyanAndMonicaJob(String name, BankAccount account, int amountToSpend) this.name = name; this.account = account; '.лшпчея с ссщегс санкобскогс стопа: ссуну у w Лсники, (ffUfllfH a m Рамана. Лажусм nOu:~ yatAi mf\caytuiyw к оплате гумму this.amountToSpend = amountToSpend; ) public void run () { ,,4l(.m0<| *UIvO ^ goShopping (amountToSpend) ; ( ик^аиНШ СМЛМММ- private void goShopping (int amount) { ^гпдтотд » / р^т ,дЛоамКЛ if (account.getBalance() >= amount) ( System.out.printin(name + " собирается потратить"); У account. spend (amount) ; ' ( как это стлали < (ескаикс cut заканчивает тратить"); < пушпчцЫ; (Ыиуа. System, out. println (name + " заканчивает тратить"); псу(>СЛМЮШ3ие tyiccnyunu, mw } else ( ' п/юи.ттп tffc tftmem, System.out.println("Извините, средств недостаточно " + name); 1СНИЯ. class BankAccount ( , private int balance = 100; ^ ИахалЬШП баланс CW public int getBalance() { ICO ООЛЛСФО^- return balance; ) public void spend(int amount) ( balance = balance - amount; if (balance < 0) { System.out.println("Превышен лимит!")• > } }
Как Э1Ш n^CUJUU.K/ ? File Edit Window Help Visa % java RyanAndMonicaTest Райан собирается потратить Моника собирается потратить Превышен лимит! Райан заканчивает тратить Моника заканчивает тратить Этот код не дает предсказуемого результата: при каждом запуске он может вести себя по-разному. Возможно, придется выполнить его несколько раз, прежде чем вы уви¬ дите проблему. Это характерно для многопоточного кода: поведение за¬ висит от того, какие потоки выпол¬ нения стартуют первыми и в какой момент каждому из них выделяет¬ ся процессорное время. Метод goShoppingO всегда про¬ веряет баланс перед тем, как снять деньги. Но, несмотря на это, мы все равно ушли в овер¬ драфт. Вот как это может произойти: Райан проверяет баланс, видит, что средств достаточно, и направляется к «кассе». В это же время Моника тоже прове¬ ряет баланс. Она видит ту же сумму и не подозревает, что Райан уже собирается платить. Райан завершает покупку. Моника завершает покупку. И вот он — перерасход. Что произошло? Пока Моника оформляла платеж, ее проверка баланса уже устарела. Райан успел потратить деньги между ее провер¬ кой и покупкой. Проверка Моники была недействи¬ тельной, потому что Райан проверил баланс раньше и находился в про¬ цессе совершения покупки. Моника не должна была получать доступ к счету, пока Райан не закон¬ чил работу с ним, и наоборот. File Edit Window Help WorksOnMyMachine % java RyanAndMonicaTest Райан собирается потратить Райан заканчивает тратить Извините, средств недостаточно, Моника
Им нужна блокировка для доступа к счету! Блокировка работает следующим образом: © Есть блокировка, связанная с операциями над банковским счетом, такими как про¬ верка баланса и снятие средств. Пред¬ ставьте, что у этой блокировки есть един¬ ственный ключ, и он «вставлен в замок», пока никто не обращается к счету... Когда счет свободен, замок открыт — до¬ ступ разрешен. Но как только кто-то на¬ чинает работу с ним, замок закрывается. Когда Райан хочет получить доступ к бан¬ ковскому счету (проверить баланс и снять деньги), он захватывает блокировку — как будто запирает замок и кладет ключ себе в карман. Теперь никто больше не сможет получить доступ к счету, так как клю¬ ча нет. Когда Райан захочет получить доступ к счету, он закроет замок и берет ключ. (з) Райан держит ключ, пока не завер¬ шит операцию. Это единственный ключ, поэтому Моника не сможет получить до¬ ступ к счету, пока Райан не закончит и не «освободит замок». Даже если Райан отвлечется после про¬ верки баланса, у него есть гарантия, что состояние счета не изменится: ведь он все это время удерживает блокировку, а значит, никто другой не может внести изменения! Когда Райан за¬ канчивает работу, он освобождает блокировку — как будто открывает замок и возвращает ключ. Теперь доступ к счету снова свобо¬ ден — и им может воспользоваться либо Моника, либо сам Райан.
Нам нужно проверять баланс и снимать деньги как одну атомарную операцию Нам нужно убедиться, что если поток выполнения начал операцию покупки, он сможет завершить ее до того, как какой-либо другой поток изменит состояние банковского счета. Другими словами, после того как поток выполнения проверил ба¬ ланс, у него должна быть гарантия, что он сможет потратить деньги прежде, чем другой поток получит доступ к этому же счету! Для этого можно использовать ключевое слово synchronized — либо на уровне метода, либо с конкретным объектом, — чтобы забло¬ кировать доступ к счету и разрешить работу с ним только одному потоку в определенный момент времени. Такой подход позволяет защитить банковский счет. Мы можем нало¬ жить блокировку прямо внутри метода, который выполняет банков¬ скую операцию. Это обеспечит, что один поток сможет пройти всю транзакцию от начала до конца, даже если он временно будет вы¬ веден из состояния выполнения планировщиком или другой поток попытается получить доступ к счету в тот же момент. На следующих страницах мы рассмотрим, какие именно объекты можно блокировать. В случае с Райаном и Моникой все просто: нам нужно обернуть транзакцию покупки в блок, который синхронизи¬ рует доступ к банковскому счету: а Ключевое слово synchronized означает, что потоку выполнения нужен «ключ», чтобы получить доступ к синхронизиро¬ ванному участку кода. Если вы хотите защитить важные данные (напри¬ мер, банковский счет), синхронизируйте код, который работает с этими данными. private void goShopping(int amount) { synchronized (account) ( / if (account.getBalance() >= amount) { it. System.out.println(name + " собирается потратить"); account.spend(amount); System.out.println(name + " заканчивает тратить"); } else { System.out.println("Извините, средств недостаточно, " + name) Лримааяш улм mmamatk, nujKidanmn (? cbivuKt. J'Da, лш nciuuwm, who слово «шшиарнни» раса упоп\риимп\ся условие. — никакт наетшшуй. сусатсший физики. Юулиштс скорее с Кштш, ген «Г Эйнштейне. ксгуа ыикишн «сттшушше» в контексте поток о(? млн п\ран?аки,т. Эй. это не jh пршцриали эту шрминелтш! бе т <ж вссгавйсслс cm пас, мы см «ушиенти ZuiHHjien не лхриклс н ноет и .1шлнпрн) ко ту. /ни; свямт CMimctusmcmccrntw. (Не) дурацкие вопросы ^ОПрОС: Почему бы просто не синхронизировать все геттеры и сеттеры класса, чтобы защитить данные? Ответ: Синхронизировать геттеры и сеттеры недоста¬ точно. Смысл синхронизации в том, чтобы объединить не¬ сколько действий в одну АТОМАРНУЮ операцию. Нас волнует не только защита отдельных методов; важно защитить всю последовательность, если она состоит из нескольких шагов. Подумайте вот о чем: мы добавили синхронизированный блок внутри метода goShopping(). Если бы вместо этого мы про¬ сто синхронизировали getBalanceO и spendO, это не решило бы проблему: Райан (или Моника) мог бы проверить баланс, а затем немедленно отпустить замок, прежде чем потратить деньги. Суть в том, чтобы удерживать блокировку до заверше¬ ния обеих операций — проверки и списания.
Блокировка объекта У каждого объекта в Java есть своя встроенная блокировка. Обычно она свободна, и можно пред¬ ставить, что рядом с объектом лежит виртуальный ключ. Эта блокировка начинает играть роль только тогда, когда для объекта используется синхрони¬ зированный блок или у объекта есть синхрони¬ зированные методы. Метод считается синхро¬ низированным, если в его объявлении указано ключевое слово synchronized. Когда объект имеет один или несколько синхронизированных методов, поток вы¬ полнения может войти в такой метод только при условии, что получит ключ блокировки объекта! Важно: блокировка привязана не к методу, а к объекту. Это значит, что, если у объекта есть два синхронизированных метода, скажем, methodl () и method2 (), и поток выполнения находится в methodl (), другой поток не смо¬ жет войти ни в methodl (), ни в method2 (), ни в любой другой синхронизированный метод этого объекта— пока первый не освободит блокировку. Подумайте об этом так. Если несколько методов могут обращаться к одним и тем же переменным экземпляра, все они должны быть защищены через ключевое слово synchronized. Цель синхронизации — защитить критические данные, но синхронизируются не сами данные, а методы, которые к ним обращаются. Что происходит при вызове? Когда поток выполне¬ ния двигается по стеку вызовов (начиная, например, с метода run () ) и доходит до синхронизирован¬ ного метода — он понимает, что нужен ключ для доступа к этому объекту. JVM (а не сам разработ¬ чик) ищет этот ключ. Если ключ свободен — поток получает его и входит в метод. С этого момента он удерживает ключ, пока не за¬ вершит выполнение синхронизированного метода или блока. И пока ключ у него, ни один другой по¬ ток не сможет войти в любой синхронизирован¬ ный метод этого объекта — ведь ключ только один. Каждый объект В Java и/леет сЬою встроенную блокироВку. И у этой блокироВки - за/лка — только один ключ. 3 большинстВе случаеВ за/лок остается открыты/л, и об это/л никто не заду/лыВается. Но как только у объекта по¬ являются синхронизироВанные /летоды. Все меняется. Поток Выполнения /ложет Войти В син- хронизироВанный /летод ТОЛЬ¬ КО если ключ ЬлокироВки до¬ ступен - то есть если другой поток еще не захВатил этот ключ. 3 протиВно/л случае при¬ дется ждать.
Синхронизация методов А можем ли мы синхронизировать метод goShoppingQ, чтобы решить проблему Райана и Моники? private synchronized void goShopping(int amount) { if (account.getBalance() >= amount) ( System.out.println(name + " собирается потратить"); account.spend(amount); System.out.println(name + " заканчивает тратить"); } else { System.out.println("Извините, средств недостаточно, " + name); ) ) It does NOT work! File Edit Window Help WaitWhat % java RyanAndMonicaTest Райан собирается потратить Райан заканчивает тратить Моника собирается потратить Превышен лимит! Моника заканчивает тратить С помощью ключевого слова synchroni zed объект блокируется. Метод goShopping () находится в классе RyanAndMonica Job. Если мы синхро¬ низируем этот метод как метод экземпляра, будет заблокирован конкретный экземпляр RyanAndMonica Job. Но тут возникает проблема: у нас два отдель¬ ных объекта — гуап и monica. Если поток, работающий с гуап, заблокиро¬ ван, это никак не помешает потоку, работающему с monica, вносить измене¬ ния в банковский счет. Ему все равно, что гуап в данный момент «под замком». А ведь объект, который действительно нуждается в защите, — это BankAccount, именно за него и «соревнуются» потоки Райана и Моники. Таким образом, синхронизация метода в RyanAndMonica Job (и блокировка его экземпляра) не решает проблему.
Важно заблокировать правильный объект Поскольку объект BankAccount используется совместно, логично считать, что именно жен отвечать за потокобезопасность. Метод spend () в классе BankAccount мог бы °Н „ ^ сам ппп верять, достаточно ли средств, и списывать деньги — в рамках одной атомарной операц class RyanAndMonicaJob implements Runnable { // ...остальная часть класса RyanAndMonicaJob // та же, что и раньше... private void goShopping(int amount) { System.out.println(name + " собирается потратить account.spend(name, amount); System.out.println(name + " заканчивает тратить"); } • "haiemfe, i() КАМСЛ j) Гмднс ас cjxu } class BankAccount { // другие методы в JVnmanwmc шГкКШл,г классе BankAccount. капу ши mum tl UU UIVUAYIUU. ’COMint, (re¬ public synchronized void spend(String name, int amount) if (balance >= amount) { balance — balance - amount; if (balance < 0) { System.out.println("Превышен лимит!") ) } else { System.out.println("Извините, Ttruht Ашан ubMchwkh Ш)(Ъ\ЖШ «HfUfVU средств недостаточно, f name) v^amtatnv.unu,K«cHt Д,Жш .vfxauatrjunv. Mcc. T «ши. тмнт* faunxL сам cuuxfxtH yyicWcncnMum fW.uamm nruntW &mc„u m икс ufun пши к йиишенш (He) дурацкие вопросы допрос: А как быть со статическими переменными’ Если у вас есть статические методы, которые изменяют их состояние, можно ли по-прежнему выполнять синхро низацию? Оилвеил: Да, конечно! Помните, что статические методы принадлежат классу, а не его отдельным экземплярам. И возникает закономерный вопрос: на какой объект в таком случае накладывается блокировка? Ведь может даже не существовать ни одного экземпляра класса! На этот случай у каждого загруженного класса в Java есть своя собственная блокировка — так же, как у каждого объекта. Например, если в куче есть три объекта Dog, то у вас будет четыре блокировки: три — для каждого экземпляра Dog и одна — для самого класса Dog. Когда вы синхронизируе¬ те статический метод, Java использует именно блокировку класса. А это значит, что если два статических метода син¬ хронизированы, поток выполнения сможет войти в любой из них только при получении блокировки класса.
Страшная проблема «потерянного обновления» Перед вами еще одна классическая проблема, связанна» с параллелизмом. Такие ситуации часто назы- вают состоянием гонки — когда два или более потока „ _ . 1шшга одиовременно пытаются изменить одни и те же данные. Эта проблема тесно связана с историей Райяня м „ ¥ F " т-аиана и Моники, поэтому мы снова воспользуемся их примером, чтобы наглядно разобрать, что может пойти не так Ниже показан базовый процесс: Шаг 1: Получение текущего баланса на счете int i = balance; Шаг 2: Увеличение баланса на 1 balance = i + 1; эн'с 1 Даже если мы используем более привычный синтаксис balance ++, нет никакой гарантии, что ском¬ пилированный байт-код будет атомарной операцией. Скорее всего — нет. На практике это набор отдельных действий: сначала происходит чтение текущего значения, затем увеличение на единицу и только потом — запись нового значения обратно в переменную. А теперь представьте, что несколько потоков одновременно пытаются увеличить значение balance. В этом и заключается суть проблемы потерянного обновления. Посмотрите на код а затем мы разберем, в чем именно кроется настоящая угроза:1п the “Lost Update problem, we have many threads trying to increment the balance. Take a look at the code, and then we 11 look at the real problem. CtaaotM пил тлим (? I(wimiuhm аля гапи ска (fax Mf". ^mhjдобавите скит. №тькс(? mm ndntpMW и швннм вшастот. - и сможете yfiuijum ixptcuAiy нагляднее. public class LostUpdate { mterruptedException { public static void main(String!] args) thro ExecutorService pool = Executors.newFixedThreadPo . Ятилняс и 1000 попыток uti Balance balance = new Balance(); for (int i = 0; i < 1000; i++) i pool.execute(() -> balance.increment() ) pool.shutdown(); if (pool.awaitTermination(1 System.out.println("Баланс = > TimeUnit MINUTES)) ( ' ■■ + balance, balance) ; П^МпЛО <?tt^wru (ссГмат-льпО via .nVRWKW ) class Balance ( int balance = 0; public void increment() balance++; ,,, , „/1И f tvmt ЙДИСН 'ICCL (C _ m, me too HblWC. 4«vc гпаюкт MOW MHil. nuruju самая важная гость. Мы являл 1 Чвслипквае и г7, Тша ™ ' К KcmcLau, ЛР^ТГ Ш{- а "< * fmmcKCMu ШУШ/МУ ttiyift ij.m. йог. ic иумонипися ff на пи. ппс ++ -это атомарная операция ша состоит up несксл/кихшагов и вмнегспHt т«*: ,uin("u«h.Z'Z'K( Ho¬ ts ШНп'< ГНс.м ... ил них может омни «пфех&мен» ahus„ KVT а ' // “ “■и поток; Щ<*Х.1
Как возникает потерянное обновление... (?) Поток А работает Считывает баланс: О Устанавливает значение: 0 + 1 Баланс становится равным 1 Считывает баланс: 1 Устанавливает значение: 1 + 1 Баланс становится 2 (5) Поток Б начинает работу Считывает баланс: 2 Устанавливает значение: 2 + 1 Баланс становится равным 3 Считывает баланс: 3 Теперь поток Б прерывается и переходит в состояние Готовый, не успев установить новое значение баланса (4) (!) Поток А снова запускается — и продолжает с места на котором остановился! Считывает баланс: 3 0^ Устанавливает значение: 3 + 1 Баланс становится равным 4 Считывает баланс: 4 Устанавливает значение: 4 + 1 Баланс становится равным 5 (4) Поток Б снова запускается — и продолжает с места, на котором остановился! ДЦ Устанавливает значение: 3 + 1 -г- с Баланс становится равным 4 .Леток йшедмиилЛ спийм Ли миг ifo 5. а лит, u ru пик Ъ &1чи1иж и пфцтши ( шпат ,1. <Щте сс'пфо I ш <Т1 к/ Мы потеряли последние обновления, сделанные по¬ током А! Поток Б раньше уже прочи¬ тал значение баланса, и ког¬ да проснулся — просто про¬ должил работу, как ни в чем не бывало, будто за все это время ничего не изменилось.
Делаем метод increment^) атомарным. е> Синхронизируем его! НЗ Синхронизация метода increment() решает проблему «потерян¬ ного обновления», потому что объединяет все шаги — чтение баланса и его увеличение — в единую, атомарную операцию. public synchronized void increment() { } balance++ Классишкам ..wGuiuku G Ga €ш.ляашп кАк сйил страи,ш. но на самим асм это т/ш оншышак тага — «шни< Шнапса. инк^ьшнт. ог<ной «.Htuj'H/mнал. Как только поток вы¬ полнения входит в метод, нужно обеспечить, чтобы все шаги внутри него были завершены как единая атомарная операция, пре¬ жде чем любой другой по¬ ток получит доступ к этому методу. (Не) дурацкие вопросы ЗОИ рОС: Звучит логично — почему бы просто не синхронизировать все подряд, чтобы быть уверен¬ ным в потокобезопасности? OlMBewl: Так делать не стоит. Синхронизация — это не бесплатный бонус, и использовать ее нужно с умом. Во-первых, синхронизированные методы создают на¬ кладные расходы. Каждый раз, когда поток обращается к такому методу, JVM должна выяснить, доступен ли «ключ» — а это занимает дополнительное время и мо¬ жет негативно сказаться на производительности (даже если это не всегда заметно). Во-вторых, синхронизация ограничивает параллелизм. Если метод синхронизирован, другие потоки вынужде¬ ны ждать, даже если могли бы работать параллельно. В простом коде это может быть не критично, но в мас¬ штабируемых системах — уже проблема. А в-третьих, и это самое опасное, — неосторожная синхронизация может привести к тупикам (о них мы поговорим чуть позже). Хорошее практическое правило: синхронизируйте только то, что действительно нужно синхронизировать. Причем синхронизация возможна не только на уровне метода, но и внутри метода — для отдельных участков кода. Именно так мы и поступили в одном из решений проблемы Райана и Моники: вместо синхронизации всего метода мы заблокировали только критическую секцию. ПипшскЩр) И. nfeofrn m;lHCUCaHA/UHUAHMUt.^ MtmcJ ид.нижм. public void go() { doStuff(); synchronized(this) { criticaistuff (); moreCriticalStuffО: > M icmi м apumc cnccccfn стмт/ся нуж¬ ного псьшш им, ь этси cnpnu.un лвнс ика mGacu, какую мн но гасни кта нужно шис шипи, атомарна .Ип (Сикирш.л! mom ■же ошкт, который cfki efcгасСнкши &ш при cuHApi ту а и, tu< Ga н . uelnuja. Же таи pi топко qGa GtiAiGa литоаа ссждиншы Gcqnu атомарною cntpau,uic: Починены Gcqnu dmaudpi сапа, ти п кос/ остается Gut amxfuniva нци и GtuuaHMimcM линрчпк huqa Get итольщаж к.люш<х. слово sunekmuzed ЖЖ'ПРгi.uenwqa, а ж Gao омяКлении, Gei qc июни ntpeqanu GKancmGc apuquai таотект. пм «к. net» поток Gcmammua qaiMcn палитпи. tmeefrn Goumu Gсинхро HusufwGaHHMu oCiok.
ф Поток выполнения А начинает работу Пытается войти в метод incrementQ. Метод синхронизирован —► получает ключ блокировки объекта. Считывает баланс: О Устанавливает значение: 0 + 1 Баланс становится равным 1 Завершает метод incrementQ —> возвращает ключ. Снова входит в incrementQ и получает ключ снова. Считывает баланс: 1 Теперь поток А отправляется обратно в состояние Гото¬ вый, но ключ не освобождает, потому что еще не завершил метод. выполнения выбран поток Б Пытается войти в incrementQ. Метод синхронизирован —*• нуж¬ но получить ключ. Ключ недоступен Поток Б переходит в состояние ожидания: «Блокировка объекта недоступна» ® Поток А снова запускается — продолжает с места, где остано¬ вился (ключ все еще при нем) Устанавливает значение: 1 + 1 Баланс становится равным 2 Возвращает ключ [Теперь поток выполнения А возвращается в состояние Готовый, но поскольку он завершил выполнение метода incrementQ, ключ блокировки он больше не удерживает.] Б выбран для выполнения Пытается войти в incrementQ. Метод синхронизирован, поэто¬ му необходимо получить ключ блокировки. Ключ теперь свободен —► получает ключ Считывает баланс: 2 И продолжает выполнение... (4) Поток
Взаимная блокировка — темная сторона синхронизации Синхронизация спасла Райана и Монику от одновременного использования банковского счета, а нас — от потери обновлений. Но мы уже упоминали, что не стоит синхронизировать все под¬ ряд, поскольку это может замедлить работу программы. Однако есть еще одно важное соображение. С синхронизированным кодом нужно быть осто¬ рожным: ничто так не парализует программу, как взаимная блокировка потоков выполнения. Взаимная блокировка (deadlock) — это ситуация, при которой два потока выполнения удержи¬ вают ресурсы (в данном случае, ключ), необходимые друг другу. Каждый из них ждет, пока дру¬ гой освободит «ключ», но этого не происходит. Оба потока оказываются в состоя¬ нии ожидания, из которого невозможно выйти без внешнего вмешательства. Если вы сталкивались с базами данных или серверами приложений, вы, скорее всего, знаете о такой ситуации. В базах данных применяется механизм блокировки, похожий на синхронизацию в Java, но при этом в системах управления транзак¬ циями может быть реализована обработка взаимной блокировки. Например, если две транзакции слишком долго удерживают ресурсы, система может распознать взаимную блокировку и выполнить «откат одной из транзакций», вернув данные к моменту до начала этой (атомарной) операции. В Java ничего подобного нет. Виртуальная машина не отслеживает взаимные бло¬ кировки и не может их устранить. Поведение программы целиком и полностью зависит от разработчика и от того, насколько внимательно он проектирует много¬ поточный код. Мы не будем вдаваться в подробности обработки взаимных блоки¬ ровок и ограничимся кратким экскурсом на этой странице. Если вы собираетесь се¬ рьезно работать с параллелизмом, обязательно изучите книгу «Java Concurrency на практике» Брайана Гетца и соавторов. В ней подробно рассматриваются проблемы, возникающие при работе с потоками выполнения, включая взаимные блокировки, состояния гонки и другие тонкие моменты, а также предлагаются надежные практики проектирования потокобезопасного кода. Для возникно¬ вения взаимной блокировки до¬ статочно всего двух объектов и двух потоков выполнения. Простой сценарий взаимной блокировки: © в Поток выполнения А входит в синхрони¬ зированный метод объекта foo и полу¬ чает ключ. Те Затем А переходит в спящий режим, удерживая ключ от foo. Поток выполнения Б входит в синхрони¬ зированный метод объекта bar и получа¬ ет ключ. После этого Б пыта¬ ется войти в синхро¬ низированный метод ^объекта foo, но не может получить его ключ — он у А. Поток Б переходит в ожида¬ ние, удерживая ключ от bar. ® * Поток выполнения А просыпается (все еще удерживая ключ foo) и пытается войти в син¬ хронизированный метод объекта bar, но не может получить ключ — он у Б. А переходит в ожидание, надеясь, что ключ от bar скоро освободится (но Т этого не произойдет — | I ведь Б сам ждет foo). Поток А не может про¬ должить работу, пока не получит ключ от bar, но bar заблокирован потоком Б. А поток Б не может работать, пока не получит ключ от foo, который удерживает А, и...
Синхронизация нужна не всегда... Синхронизация полезна, но сопряжена с определенными издержками — возможным сниже¬ нием производительности и риском взаимной блокировки. На этот случай есть альтернати¬ вы для безопасной работы с разделяемыми данными между потоками выполнения. В пакете j ava . util. concurrent множество классов и утилит для работы с многопоточным кодом. Атомарные переменные Если вы работаете с разделяемыми значениями типа int, long или boolean, вместо синхро¬ низации можно использовать атомарные переменные. Эти классы предоставляют атомарные методы — операции, которые выполняются как единое целое и безопасны для многопоточно¬ сти, даже без явной синхронизации. Есть несколько типов атомарных переменных, например, Atomiclnteger, AtomicLong, AtomicBoolean, AtomicReference<T>. Например, в задаче, где у нас возникала потеря обновлений, вместо синхронизированного метода инкремента можно просто использовать Atomiclnteger. class Balance { Г ™cw Atomiclnteger balance = new Ahnmir;TT'<-oj<»r(fl) ■’ (f trl _ Ы ^ U. public void increment () { jt hu«xic void increment() { /7 balance. incrementAndGet () ; r\ ■'& t J * № JThu ШЩ.ЛДШИМС Hi ииЖ"1 1 f П£*\иШ4ИМ.’С Hi нШ пят MjHclvfCH/zul 0 vnantkoe&i Поэтому я могу использовать Atomiclnteger, пока мне нужно лишь простое увеличение значения. Но как это поможет, если я захочу делать что-то более сложное — на¬ пример, выполнить вычисления с несколькими шагами? Атомарные переменные становятся по-настоящему полезными, когда вы исполь¬ зуете операции сравнения и замены — CAS (Compare-And-Swap). Это еще один спо¬ соб атомарно изменить значение, не прибегая к синхронизации. В Java для этого есть метод compareAndSet. Да, название отличается от «compareAndSwap», и да — именование методов остается самой сложной задачей в программировании. Метод compa reAndSet принимает значение, которое вы ожидаете увидеть в ато¬ марной переменной, и сравнивает его с текущим значением. Если они совпадают, операция выполняется успешно. К примеру, вместо того чтобы блокировать весь банковский счет с помощью synchronized, можно использовать compareAndSet, чтобы решить ту самую проблему Райана и Моники — без блокировок и с высокой производительностью.
Сравнение и замена (CAS) на примере атомарных переменных Как можно применить атомарные переменные и механизм CAS (через метод compareAndSet), чтобы решить проблему Райана и Моники? Поскольку они оба обращались к целочисленному значению — балансу банковского счета, мы можем использовать Atomiclnteger для его хранения. Затем, когда кто-нибудь из них захочет потратить деньги, можно воспользоваться методом compareAndSet, чтобы обновить баланс. Private Atomiclnteger balance = new Atomiclnteger(100); boolean success = balance.compareAndSet(expectedValue, newValue) Uwm <M. Г «nuu- мШ ивсвапи уамшс (c> CC (тщ ja «СНЯе Проще говоря: «Следует установить новое значение баланса, но только если текущее значение совпадает с ожидаемым, н сообщить, произошло ли обновление.» В механизме CAS используется оптимистичная блокиров¬ ка. Т. е. вы не препятствуете другим потокам выполнения обращаться к объекту. Вместо этого вы пытаетесь внести изменения и допускаете возможность, что они могут не сработать. Если обновление не удалось, вы сами решаете, как поступить дальше: можно попробовать еще раз или, скажем, показать пользователю сообщение о том, что из¬ менение не было выполнено. Такой подход реализуется сложнее, чем блокировка до¬ ступа для всех остальных потоков выполнения, но зато он может оказаться быстрее. Особенно это актуально, если вероятность одновременной записи мала или если у вас много потоков, которые только читают данные, и мало тех, что пишут. В таких случаях нет смысла платить «цену» за блокировку при каждой попытке записи. При использоЬании операций CP\S ba/л придется учиты- Ьать случаи, когда оЪноЬле- ние не \/аается f
Атомарные приключения Райана и Моники Посмотрим, как это реализуется на практике — на примере банковского счета Райана и Моники. Мы поме¬ стим баланс в Atomiclnteger и будем использовать compareAndSet для атомарного изменения значения. import java.util.concurrent.atomic.Atomiclnteger; Г ItcmcJnkmrc hum же Мя*я'инш,/Нютим (100). как и tumme class BankAccount { NJ/" "Я7«4(нд,м ЛШКнисм private final Atomiclnteger balance = new Atomiclnteger(100)- fXOHbMc. public int getBalc return balance. ilanceO { |/1 fnvt^ (шАЫнютшм не шпелищтем. pcm^au,u public void spend(String name, int amount) { J ^ммасохраимтея- int initialBalance = balance.get( if (initialBalance >= amount) "^boolean success = balance.compareAndSet(initialBalance, initialBalance amount), 77 Лишена ^ , ‘fMUtti if (!success) { ' n System.out.printin("Извините " + name + "> вы не потратили деньги. ), } } else { System.out.println("Извините, средств недостаточно, } I File Edit Window Help SorryMonica % java RyanAndMonicaTest Райан собирается потратить Моника собирается потратить Райан заканчивает тратить Извините, средств недостаточно, Моника Моника заканчивает тратить + name); . не Hein, [VCc^<eTpaUtUl1 f Н(. как nJ* с’«« п,/нит/ IS£HSr~ В пакете java.util.concurrent собрано множество полез¬ ных классов н утилит для работы с многопоточностью. Обязательно загляните — там есть на что посмотреть!
,0 Итак, если все эти про¬ блемы связаны с записью в общий объект, то что будет, если вообще запретить потокам изменять данные в общих объектах? Есть ли способ сде¬ лать это? Я Сделайте объект неизменяемым, если собираетесь использовать его в нескольких потоках выполнения и не хотите, чтобы кто-либо из них мог изменить его данные. Лучший способ быть уверенным, что другой поток не внесет изменений, — это просто исключить такую возможность. Если данные объекта нельзя изменить, такой объект назы¬ вают неизменяемым. п *7* и, „ <гЩМ 40 неизменяемых данных ^ т< X"' public final class ImmutableData { Hi//,,, Создание класса для хранения private final private final String name; int value; a. n\m ‘l'w n h&Gamc* cflH f J (в «W**au*' , Uowrn^l'jr mnu u|'w luH< - 1 int public ImmutableData(String name, this.name = name; this.value = value; } public String getName() { return name, { return value, ГПаС value) { m public int getValue() У 1 fT ц^Мт Южпшмсгит mnu M- Ни c ш уьшт mnu curum- ц/р1{Ч(._я^\/лК' U(m«f класса hi пажен (^кпие ^шпнн! Прокачай извилины Иногда одного ключевого слова final недоста¬ точно, чтобы по-настоящему защитить объект от изменений. Как вы думаете, в каких случаях это может произойти? Вот вам подсказка
Использование неизменяемых объектов Очень удобно изменять данные в общем объекте и рассчитывать на то, что другие потоки выполнения смогут их увидеть. Но, как мы уже убедились, такой удобный подход может быть небезопасным. С другой стороны, при работе с объектом, который нельзя изменить, поток выпол¬ нения может полагаться на то, что данные внутри остаются неизменными. Напри¬ мер, если поток получил значение из такого объекта, оно больше не изменится. В этом случае не нужно использовать синхронизацию или другие механизмы для контроля доступа к данным: менять их просто невозможно. Ну да, если данные нельзя из¬ менить, значит, никто их не трогал. Но тогда какой толк от объекта, кото¬ рый нельзя изменить? А если мне все- таки нужно обновить его значения? Работа с неизменяемыми объектами требует другого подхода Вместо того чтобы изменять существующий объект, мы создаем новый, содержащий обновленные значения. Все потоки выполнения, которым нужны эти новые данные, должны использовать новый экземпляр объекта. А что происходит со старым объектом? Если он по-прежнему исполь¬ зуется (а иногда это вполне оправдано — например, для работы со старыми данными), он остается в памяти. Если же нет — он будет ав¬ томатически удален сборщиком мусора, и мы про него можем забыть.
Изменение неизменяемых данных Представьте себе систему, в которой есть клиенты, и у каждого объекта Customer есть Address, представ¬ ляющий уличный адрес клиента. Если Address — не¬ изменяемый объект (все его поля final, и данные нельзя изменить), то как же обновить адрес клиента, если он переезжает? О У клиента есть ссылка на исходный объект Address, в котором хранятся данные о его текущем адресе. © Когда клиент переезжает, создается новый объект Address с обновленными данными. О Ссылка внутри объекта Customer, указыва¬ ющая на адрес, заменяется на новый объект Address. (Не) дурацкие вопросы ^ОПрОС: Что произойдет, если в других частях про¬ граммы все еще будет ссылка на старый адрес? Омвет На самом деле в некоторых случаях это даже необходимо. Представьте, что клиент оформил заказ, который должен быть доставлен по его первоначально¬ му адресу. Нам нужно, чтобы в деталях этого заказа был указан именно тот адрес, который был актуален на момент оформления, а не новый, куда клиент переехал позже. Как только клиент меняет адрес (и объект Customer получает ссылку на новый Address), все новые заказы будут отправ¬ ляться на уже обновленный адрес.
Подождите-ка! Объект Address неизменяем и сам по себе не меняется — но ведь объект Customer все равно должен ме¬ няться, чтобы указывать на новый адрес. Совершенно верно. Если в вашей системе есть изменяемые данные, то сами изменения должны где-то происходить. Глав¬ ная мысль, которую стоит вынести из этого обсуждения, — не все классы в приложении должны быть изменяемыми. На¬ оборот, лучше свести к минимуму те места, где происходят изменения. Так вам будет проще анализировать поведение приложения, особенно если несколько потоков выполнения работают с одними и теми же данными. Есть множество приемов эффективной работы с неизменяе¬ мыми классами; здесь мы лишь слегка коснулись этой темы. Любопытно, что начиная с Java 16 в языке появились запи¬ си — специальные конструкции, которые позволяют создавать неизменяемые классы данных прямо «из коробки». Используйте неизменяемые классы данных везде, где это уместно. Сведите к минимуму возможность изменения данных из нескольких потоков выполнения.
Разделяемые данные: что может пойти не так? Мы почти у цели, правда! Осталось обсудить еще одну важную вещь. До сих пор мы рассматривали всевозможные проблемы, возникающие, когда несколько потоков выполнения одновремен¬ но записывают данные. Это касается и разделяемых коллекций. Но проблемы могут возникнуть даже в том случае, если множе¬ ство потоков только читают данные, а изменяет их всего один поток выполнения. В приведенном ниже коде один поток записи добавляет элементы в коллекцию, а два других потока — просто читают из нее. И даже в таком, казалось бы, безобидном сценарии могут начаться неприятности. public class ConcurrentReaders { public static void main(String[] args) { List<Chat> chatHistory = new ArrayListO(); ExecutorService executor = Executors.newFixedThreadPool(3); for (int i = 0; i < 5; i++) { executor.execute(() -> chatHistory.add(new Chat("Привет!' -> System.out.println(chatHistory))i -> System.out.println(chatHistory) J'CkitecmcKi ICbtK- ))); JU (At, кспиртХ executor.execute(() executor.execute(() ) executor.shutdown(); ) final class Chat { private final String message; private final LocalDateTime timestamp; public Chat(String message) { this.message = message; timestamp = LocalDateTime.now() public String toString () { cttjwt ниш* ,u J String time = timestamp.format(ofLocalizedTime(MEDIUM)), Семаt-м нытик ft,uu.пиная lift ЛЖНШ — OH <4flan i1 aid.Mint сг/мкнш ь спиик A iпакт nuTUK^fti m пиная. wHu/uu MM* iwmumi aaimnt W это*0 тиски. ЗапшкаМс .аскс.и КС и UK. Il f. гписп ШКЧ( rmftuMi ftyf'Hu н/«/«-«'/ Chm mi a нщмюыи ншея mu JM V' "JvteKnii*nui- / '• maniiAuj иг нстм^тпи суспасщ return time + + message; ) )
Опасное чтение из изменяемой коллекции Выполнение кода с предыдущей страницы иногда приводит к выбросу исключения. Теперь вы уже знаете, что подобные проблемы во многом зависят от поведения оборудования, операционной системы и JVM. | File Edit Window Help Pooffifunonono % lava ConcurrentReaders [] [] [18:43:59 Привет!, 18:43:59 Привет!] [18:43:59 Привет!, 18:43:59 Привет!] [18:43:59 Привет!, 18:43:59 Привет!, 18:43:59 Привет!] [18:43:59 Привет!, 18:43:59 Привет!, 18:43:59 Привет!] Exception in thread "pool-l-thread-2" Exception in thread "pool-l-thread-1" java.util. ConcurrentModificationException at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1043) at java.base/java.util.ArrayList$Itr.next(ArrayList.java:997) at java.base/java.util.Abstractedlection.toString(AbstractCollection.java:472) at java.base/java.lang.String.valueOf(String.java:2951) at java.base/java.io.PrintStream.println(PrintStream.java:897) at ConcurrentReaders.lambda$main$2(ConcurrentReaders.java:17) Лоток 1штм того. иш/.штп. «ш'епшк ^ как тот. начал ого при ли imp1 Ситуация, когда один поток выпол¬ нения изменяет коллекцию, пока другой ее читает, может привести к исключению ConcurrentModificati onException.
Используйте потокобезопасные структуры данных Очевидно, что старый добрый ArrayList не подойдет, если с ним одновременно работают потоки выполнения — одни читают, другие вносят изменения. На этот случай у нас есть альтернативы. Нам нужна потокобезопасная структура, с кото¬ рой можно одновременно читать и записывать, не опасаясь исключения Concurr entModificationException. В пакете j ava. util. concurrent есть несколько таких структур. Для решения именно этой задачи мы рассмотрим класс CopyOnWr iteArrayList. Это разум¬ ный выбор, если у вас есть список, который часто читается, но редко изменяется. Почему именно так — станет ясно чуть позже. public class ConcurrentReaders { ^ (sr,r»Vt KXl,i public static void main(String[ ] args) { List<Chat> chatHistory = new CopyOnWriteArrayList<>(); ExecutorService executor = Executors.newFixedThreadPool(3) ; for (int i = 0; i < 5; i++) { executor.execute(() -> executor.execute(() -> executor.execute(() -> } executor.shutdown (); chatHistory.add(new Chat("Привет!"))) System.out.println(chatHistory)); System.out.println(chatHistory) ) ; ( nnsvmuin raaru к,„ оапател (£, ' K(X a File Edit Window Help AyManposa java ConcurrentReaders N N [] (] (10 [10 [10 [10 [10 [10 26:22 Привет 26:22 Привет 26:22 Привет 26:22 Привет 26:22 Привет 26:22 Привет 10:26:22 Привет!] 10:26:22 Привет1, 10:26:22 Привет!, 10:26:22 Привет!, 10:26:22 Привет1, 10:26:22 Привет', Process finished with exit code 0 10:26:22 Привет 10:26:22 Привет 10:26:22 Привет 10:26:22 Привет 10:26:22 Привет 10:26:22 Привет!] 10:26:22 Привет!] 10:26:22 Привет!, 10:26:22 Привет!] 10:26:22 Привет', 10:26:22 Привет!] Мск иски мй Hard
Класс CopyOnWriteArrayList Класс CopyOnWriteArrayList обеспечивает безопасный доступ для потоков выполнения, читающих данные, даже если в это время другие потоки вносят изменения. Как это работает? Все прямо как в названии: при записи создается копия. Когда поток записи добавляет или изменяет данные, он работает с копией списка. После завершения изменений эта копия заменяет ориги¬ нал. Тем временем все потоки чтения спокойно продолжают работать с оригинальной версией — безопасно и без конфликтов. О Экземпляр CopyOnWriteArrayList содержит упорядоченный набор данных, аналогично обычному массиву. О Когда поток А читает CopyOnWriteArrayList, он получает итератор, предоставляющий доступ к снимку данных списка на тот мо¬ мент времени. сетка тгж н и.ч ' 935 34 m Copy On Wri teArroyt^ «танк 935 34 m <^bpyOnWriteArroyLi5' Поток Б записывает данные в CopyOn¬ WriteArrayList, добавляя новый эле¬ мент, и CopyOnWriteArrayList создает копию данных списка до внесения изменений. тптшшг mum cj.4M записи janucu 935 34 т CopyOnWrl^ S' о © Этот процесс остается незаметным для всех потоков, которые читают или за¬ писывают данные. Когда поток Б вносит изменения в «спи¬ сок», он на самом деле изменяет копию. Он спокоен: изменения вносятся, как и нужно. Потоки чтения, такие как поток А, совсем не затрагиваются — они продолжают итериро¬ вать снимок исходных данных. —S сотка аля гтшня0 ^ 935 34 473 Ыз4 \т\ 5J,/ 1—~~L<< Л? CoppS
(Me) дурацкие вопросы ВоирОС: Разве класс CopyOnWrit- eArrayList не означает, что потоки чтения видят устаревшие данные? Отведи: Да, потоки чтения всег¬ да работают со снимком данных — тем, что существовало на момент на¬ чала чтения. Это значит, что данные могут быть устаревшими, но зато вы не получите исключение Concur rentModificationException, и чтение будет безопасным. Вопрос: А разве это плохо — ис¬ пользовать устаревшие данные? 0(/И£5вУИ: Не всегда. Во многих системах это приемлемый компро¬ мисс. Например, новостной сайт мо¬ жет показывать слегка устаревшие данные, и это не критично. Важно, чтобы информация была достаточно свежей, но не обязательно обновля¬ лась каждую миллисекунду. ВоПрОС: Но банковская выписка должна быть точной! Как гаранти¬ ровать актуальность в критически важных данных? Оиивепо: Класс CopyOnWriteAr- rayList — не лучший выбор, если потоки должны видеть строго одно и то же актуальное состояние. Для таких случаев подойдут структуры, основанные на блокировках, напри¬ мер Vector. Они обеспечивают по- токобезопасность, допуская доступ к данным только по одному потоку за раз. Это безопасно, но медлен¬ нее — вы теряете преимущества многопоточности, если все потоки выстраиваются в очередь. ВопрОС: Значит, класс CopyOnWrite- ArrayList — быстрая потокобезопасная структура? Отведи: Это зависит от ситуации. Она будетработатьэффективно, если у вас много потоков чтения и мало за¬ писи. Но если запись выполняется часто, затраты на создание копии на каждое изменение могут оказаться слишком высокими. ВопрОС: Почему нет простого от¬ вета, как правильно реализовать параллелизм? 0\мвет Многопоточное програм¬ мирование — это всегда компромисс. Чтобы выбрать подходящий инструмент, нужно понимать, что делает ваше прило¬ жение, как оно должно работать, а также учитывать среду выполнения и возмож¬ ности аппаратного обеспечения. Если вы неуверены, какой подход подойдет лучше всего, самое время заняться тестированием производительности. Только замеры покажут, как конкрет¬ ное решение влияет на работу вашей системы. ВопрОС: Вы так и не рассказали, когда final в объявлении поля все-таки не гарантирует, что данные не будут изменены. Расскажете? Ответ: Отличный вопрос! Если поле с модификатором final — это ссыл¬ ка на объект (например, коллекцию или пользовательский класс), то final лишь гарантирует, что ссылка не изменится. Но внутреннее состояние объекта все еще может быть изменено. Чтобы этого не происходило, важно, чтобы ссылки с модификатором final указывали только на неизменяемые объекты. В противном случае даже ваш «неизме¬ няемый» объект может хранить изменя¬ емые данные. Пример — LocalDateTime. Потокобезопас¬ ные коллек¬ ции в ранних версиях Java обеспечивались с помощью блокировок. Например, java.util. Vector. С выходом Java 5 появи¬ лись конку¬ рентные струк¬ туры данных в пакете java, util.concurrent. Они не ис¬ пользуют блокировки в привычном понимании — вместо этого применяются более легкие и эффектив¬ ные механиз¬ мы синхрони¬ зации.
—- Без лишних «лов ■ С потоками выполнения могут возникать серьезные проблемы, если два или более по¬ тока одновременно пытаются изменить одни и те же данные. ■ Когда несколько потоков обращаются к одному объекту, это может привести к повреж¬ дению данных — особенно если один из потоков будет прерван в момент изменения критически важного состояния объекта. ■ Чтобы защитить объекты от одновременных изменений, определите, какие действия должны выполняться как единое атомарное целое. Иными словами, какие методы долж¬ ны быть завершены полностью, прежде чем другой поток сможет обратиться к этому же методу на том же объекте. ■ Используйте ключевое слово synchronized, если хотите гарантировать, что два потока не войдут одновременно в один и тот же метод. ■ Каждый объект в Java имеет одну блокировку и один ключ к этой блокировке. Обыч¬ но это не имеет значения, но как только вы синхронизируете метод или используете synchronized с объектом, блокировка начинает играть роль. ■ Когда поток выполнения пытается войти в синхронизированный метод, он должен полу¬ чить ключ от объекта, чей метод он собирается выполнить. Если ключ уже занят другим потоком, текущий поток ожидает, пока ключ не освободится. ■ Даже если у объекта несколько синхронизированных методов, ключ остается один. Пока один поток работает в синхронизированном методе, ни один другой поток не может войти ни в один другой синхронизированный метод этого объекта. Это ограничение защищает данные, обеспечивая, что только один поток может выполнять любой метод, взаимодей¬ ствующий с этими данными. ■ Ключевое слово synchronized — не единственный способ защитить данные от одновре¬ менного доступа. Если несколько потоков изменяют одно значение, подойдут атомарные переменные с операциями CAS (compareAndSet). ■ Основные проблемы возникают при записи данных из нескольких потоков, а не при чтении. Подумайте, действительно ли данные нужно изменять — возможно, их можно сделать неизменяемыми. ■ Чтобы создать неизменяемый класс, объявите его и все поля с модификатором final, инициализируйте поля один раз (в конструкторе или при объявлении) и не добавляйте методов, которые могут изменить состояние объекта. ■ Наличие неизменяемых объектов в приложении не означает, что ничего не меняется. Это означает, что вы ограничиваете области, где могут происходить изменения, снижая риск ошибок при работе с потоками. ■ В java.util.concurrent доступны потокобезопасные структуры данных, которые позволяют нескольким потокам читать данные параллельно, даже если один или несколько потоков одновременно их изменяют. ■ Параллельное программирование — это непросто, Но в вашем распоряжении есть мно¬ жество инструментов, которые делают его более управляемым.
ражнение В роли JVM На этой странице приведен полный ис¬ ходный ход программы. Заша задача - на Ьремя стать JV/Л и определить, что программа ЬыЬедет на экран, если будет запущена. Как можно изменить код, чтобы он каждый раз даЬал корректный результат? Ответы на стр. 706. inport java.util.*; inport java.util.concurrent.*; public class TwoThreadsWriting { public static void main(String[] args) { ExecutorService threadPool = Executors.newFixedThreadPool(2); Data data = new Data(); threadPool.execute(() -> addLetterToData('a', data)); threadPool.execute(() -> addLetterToData('A', data)); threadPool.shutdown(); ) > private static void addLetterToData(char letter, Data data) { for (int i = 0; i < 26; i++) { data.addLetter(letter++); try { Thread.sleep(50); ) catch (InterruptedException ignored) {) ) System.out.println(Thread.currentThread().getName() + data.getLetters()) System.out.println(Thread.currentThread().getName() + " size = " + data.getLetters().size()); final class Data { private final List<String> letters = n%w ArrayList<>(); public List<String> getLetters() (return letters;} public void addLetter(char letter) { letters.add(String.valueOf(letter)); >
ТТяти~ минутная загадка Неожиданная встреча в шлюзовой Сара присоединилась к утреннему совещанию команды разработчиков и, пока все рас¬ саживались, посмотрела в иллюминатор. За стеклом медленно разворачивался восход: над Индийским океаном поднималось солнце, окрашивая край планеты мягким бело-голубым светом. Ее всегда завораживали такие моменты — они напоминали, ради чего все это. Комната для встреч была небольшой, но вполне удобной. За столом уже собрались коллеги, обсуждение только начиналось. Сегодняшняя тема — шлюзовые системы орбитального корабля. Проект приближался к финалу, и предстояло организовать поток выходов в от¬ крытый космос: работа снаружи станции скоро станет ежедневной рутиной, и все должно быть отлажено до мелочей. Отлично, ■ ; jm — Доброе утро, Сара, — сказал Том, поднимая голову от планшета. 4X0 ты пришла. Мы как раз начинаем разбирать проект подробнее. Сара кивнула, села рядом и включилась в разговор. — Как вы все знаете, — начал Том, — каждый шлюз оборудован терминалами с графическим интер¬ фейсом, защищенными от внешней среды. Такие терминалы есть с обеих сторон — и внутри, и снаружи. Космонавты будут использовать их для запуска процедур входа и выхода из шлюза. Сара кивнула и уточнила: — Том, можешь показать, какие именно шаги выполняются при выходе и входе? Какая там последователь¬ ность? Том поднялся, мягко оттолкнулся от пола и подплыл к доске. — Конечно. Начнем с выхода. Вот псевдокод для процедуры выхода из орбитального корабля. Он взял маркер и начал писать: orbiterAirlockExitSequence() verifyPortalStatus(); pressurizeAirlock(); openlnnerHatch (); confirmAirlockOccupied () ; closelnnerHatch(); decompressAirlock(); openOuterHatch(); confirmAirlockVacated () ; closeOuterHatch(); — Чтобы последовательность не прерывалась, — пояснил Том, обернувшись к команде, — мы синхронизи¬ ровали все методы, вызываемые orbiterAirlockExi t Sequence (). Нам бы не хотелось, чтобы воз¬ вращающийся в космос участник случайно застал своего приятеля... ну, скажем, в неловкий момент — со спущенными космическими штанами. Зал отозвался смешками, пока Том стирал с доски. Но Сара не смеялась. Что-то ее зацепило. Что-то не давало покоя. Она не сразу поняла, что именно, — но когда Том начал писать псевдокод последовательности входа, в голове щелкнуло. — Погоди, Том! — воскликнула она, выпрямляясь. — Мне кажется, в последовательности выхода серьезная дыра в логике. Мы должны пересмотреть ее прямо сейчас — это может быть критичным! Почему Сара прервала совещание? Что именно ее насторожило? Ответы на стр. 707.
В роли JVM (со стр. 704) Ответ в том, что результат не будет одинаковым при каждом запуске. Теоретически можно было бы ожидать, что размер всегда будет равен 52 (2 х 26 букв алфавита), но на практике это один из классических случаев потери обновлений. Решения для упражнений | File Edit Window Help ZeCount java TwoThreadsWriting pool-l-thread-2[a, A, b. В, с, C, d, D, E p pool-l-thread-1 [a, A, b. В, с, C, d, D, E p' H' f’ ^' Kf pool-l-thread-1 размер =40 ' > 9. G, h, H, i, j, K, k, pool-1-thread-2 размер = 40 Лриме/х вывела. Ваш тинтат, скорое Seaс, m стоп mcrao тжм жС как покамне нюих, — их если Ад догадались. тто ра?.ш[г скажется Эту проблему можно решить двумя способами — „шкммх 52; пи.. А» шлцжшЫ паиш . и оба будут правильными. ' Синхронизация метода записи public synebronized void addLetter(char letter) letters.add(String.valueOf(letter)); ) йлимотм cmxpompfofom, nit. «ко един исток Auu.i- ненид смолят, хтмтшпь данные S кЛштк момент. Au-нснн. те фмтвмш/йт ncmcfuc сМшт. Va- наксни лсишш? не Аомвжап, ясоабя папок Auu; жсния^анитАит. aJDf JJUli ж тахт. Использование потокобезопасной коллекции private final List<String> letters = new CopyOnWriteArrayListO(); »s«—« ««>■•««- „ „ X ■ ajrCKa ^Цнад KC,LKU/Ull Используйте одно из этих решении — оба применять НЕ нужно! Если вы используете потокобезопас¬ ную коллекцию, синхронизировать метод записи уже не требуется.
'Пятиминутная загадка (состр. 705) Что насторожило Сару? Сара поняла: чтобы вся последовательность выхода выполнялась без прерываний, метод orbiterAiгlockExitSequence () должен быть синхронизирован. В текущем виде выполнение по¬ следовательности могло быть прервано. Поток выполнения Exit Sequence нельзя было прервать во время вызова отдельных методов нижнего уровня, но его могли прервать между этими вы¬ зовами. Сара знала, что вся последовательность должна выпол¬ няться как единый атомарный блок. Если синхронизировать сам метод orbiterAirlockExitSequence () , его выполнение нельзя будет прервать ни на одном этапе.
Приложение А: Готовый код ht itja См нажилшстс кнопку «OmrvjwCunu», Саии. соошА/Kut отл|лабджтся С гаи r\c./U>?oCam£Aau rmt с тек^и*,«ж ^ипъиитескилА puajKKuu. Cyber BeatBox Bass Drum Q □ а □ 0 □ G а о 0 Closed Hl-Hat G о □ а: 11 О о о о Open Hl-Hat 0 П f— □ о а ;Ь гл*. —у п о Acoustic Snare ■Q п □ 0 о □ с о 0 Crash Cymba □ г о? гл м о D □ о Hand Clap D О а 0 fp п о СУ р а High Tom rn C—t" □ 0' п ■П- 0 □ о о р HI Bongo o; □ п 0 р* □ 0 г; о о Maracas о □ о о 0 0 о глI о о Whistle П о -а о о Q .□■ f 'I п а Low Conga П о о □ 85 о •О □ о Cowbell о о о о п Q о О □ Vi bras lap ■Os о. ;□ ГЛ 0 о Low-mid Tom ГК о о- О Г "1 8п □ и High Agogo D а о D □ гг гН Lj □ Open HI Conga с о О О о О гл г-] О О О О Об П 'ГУ п □ о □ п □ □: пап о а о □ по о □ О Р □ а а 0-0 о □ ап о □ ' о о □ □ □ а о о п ■ 1 - 1' Stan Stop Tempo Up Tempo Down sendlt Try this| BoomTishO Groove #2 RfverO: Dance beat EstuaryEchoO: Groove #2 revised Rcverl: Energy build up Наконец-то — полная версия битбокса! Теперь он подключается к простому серве¬ ру MusicServer, так что вы можете делиться своими ритмическими рисунками и вос¬ производить лупы других пользователей. Да-да, музыкальное общение в реальном времени! юхсуящие сообщения Cm cH'TiKX jfwcmHUKctf. Щелкните. шлите по . иешму ну них, тика утружни cocmtfem- стАунщиилуп. ауа- тс.и нажмите кнопку «Опарт», тика бсспрс- иу&ктч его.
Финальная версия клиентской битбокс-программы Большинство частей этого кода вы уже видели раньше в разделах «Готовый код» предыдущих глав, так что заново все расписывать не будем. Вместо этого сосредоточимся на новеньком: • Графический интерфейс: теперь у нас есть два свежих компонента — прокручивающаяся тек¬ стовая область для входящих сообщений и поле ввода для ваших реплик. • Сеть: как и в программе SimpleChatClient, битбокс теперь подключается к серверу и полу¬ чает потоки ввода и вывода. • Многопоточность: опять же, как в программе SimpleChatClient, запускается отдельное «задание-читатель», которое следит за входящими сообщениями от сервера. Только на этот раз приходят не просто строки, а два объекта: текстовое сообщение и сериализованный массив — именно тот, в котором хранится состояние всех флажков! Полный исходник — по ссылке: https://oreil.ly/hfJava_3e_examples. import javax.sound.midi.*; import javax.swing.*; import javax.swing.event.*; import j ava.awt.*; import j ava.io.*; import java.net.Socket; import j ava.util.*; import j ava.util.concurrent.*; inport static j avax.sound.midi.ShortMessage.*; public class BeatBoxFinal { private JList<String> incomingList; private JTextArea userMessage; private ArrayList<JCheckBox> checkboxList; private Vector<String> listVector = new Vector<>(); private HashMap<String, boolean[]> otherSeqsMap = new HashMap<>()■ private String userName; private int nextNum; private ObjectOutputStream out; private ObjectlnputStream in; private Sequencer sequencer; private Sequence sequence; private Track track; String[] instrumentNames = { "Бас-барабан", "Закрытый хэт", C vu<^vU' П AAi" IflHAAKi Открытый хэт" ТОМ", "ВЫСОКИ! "Маракасы", "Свисток", "Низхий конга", "Ковбелл" "Средний том", "Высокий агого", "Открытый конга" int[] instruments = {35, 42, 46, 38, 49, 39, 50; gO Мйлыи баоабан" "Крэш-хэт", "Хлопок ", "Высоким том", "Высокий бонго" Р "Вибраслэп", Массив гш, ссст ветствуюш,ихМ4Ю2- Kccja,u митка-штх инстриЖшпсв. Кажасе теле сЩнатап cryic- at «наш м ипшиит м mcmf\y щн/п HtmjmuLp. 35 — это (fac-(faj\a{!cm, 42 - ли уитшй..хэт, 46 ~Лт*ушт/ли хэт н. так eja'itt. I 70, 72, 64, 56, 58, 47, 67, 63},
public static void main(String[] args) new BeatBoxFinal().startup(args[0]); > На&лмтч afuuumm коматнси стнскч, чтссам aami эклдина La палиМ.лм. Jljmuf ртус£а:Л Java fimScxTinatthcJcaH public void startup (String name) ( userName = name; // открыть подклвчение с сервером try ( Socket socket = new Socket("127.0.0.1", 4242); Настраиваем сетевое еошижнме. ccfqaiM потоки QQoqa-^mcxja. Ч'/апцскаш поток шпм тиил а\м шшмя бхсамщчхсш^цоаий. ucncMjifM сскепт. потери mvc out = new ObjectOutputStream(socket.getOutputStream()) ; OHM.umut nujXuf*n\.ij.lH paamn in = new ObjectInputStream(socket.getInputStream()) ; с (г*мКи\нн.ли< потокдлдм. ExecutorService executor = Executors.newSingleThreadExecutor(); executor.submit(new RemoteReader()); ) catch (Exception ex) { System.out.println("He удалось подключиться - вам придется играть в одиночку ")• setUpMidi(); buildGUI(); ) public void buildGUI() { JFrame frame = new JFrame("Виртуальный битбокс"); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); BorderLayout layout = new BorderLayout(); 0 JPanel background = new JPanel(layout); background.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); Cbqata rtvubimcKim tm mlhdxUc — m utm ш этот ко а (?главе.'IS. Box buttonBox = new Box(BoxLayout.Y_AXIS); JButton start = new JButton("Старт"); start.addActionListener(e -> buildTrackAndStart()) buttonBox.add(start); ЫЬ ,тк «ч« "анс- 2 K WmolT^' «акт- JButton stop = new JButton("Стоп"); stop.addActionListener(e -> sequencer.stop ()); buttonBox.add(s top); JButton upTempo = new JButton("Ускорить темп") upTempo.addActionListener(e -> changeTempo(1.03f)); buttonBox. add (upTempo) ; ^ ^ JButton downTempo = new JButton("Замедлить темп"); downTempo.addActionListener(e -> changeTempo(0.97f)): buttonBox.add(downTempo); JButton sendlt = new JButton("Отправить"); sendlt.addActionListener(e -> sendMessageAndTracks())• buttonBox.add(sendlt); eifiatu кнопки к вейст(?ияж чсрц н ид — они ('плубан m yf№c|fi if" нажатый. ^.тшааачхся Ip u-«f HU ,/' userMessage = new JTextArea(); userMessage.setLineWrap(true); userMessage.setWrapStyleWord(true); JScrollPane messageScroller = new JScrollPane(userMessage) buttonBox.add(messageScroller); KcQoa (buhku,Um! IJnpatf- ..«дм*. muWu. Начальна, мшчскис — '1.0, a npu Wcitu нлжшпим mtiuv f ш н мчас m - CM nfWLVUfHi) tui.1%. ’ ИксаОш.и текстовик) I ссластс киaa пс и.лтти.ль ,«шш вшпш COC n«,ou« ар\м cjpftux участнике^.
Omfa»*** ЧШ Л'Х "го „ Jure его W ** ]Ы nfwcmc (1АМ n/UCW’rn/ul ((CrUVHUU ЙЫН.ЖШи f!T{m T^yanHt[i< к И4Д*У /wm-'UUWKUU П bPAJb , HOK. incomingList = new JLietOO ; incomingL: t.addListSelectionListener(new MyListSelectionListener()); incomingList.setSelectionHcde(LiatSelect clti lonMoOilGLE SELECTION); JScrollPane the Li at = new JScrollPane (iincomingLhst) ; buttonBox.add(theList); incomingList.setListData(listVector); awuiu ifvtHUujL Box nameBox = new Box(BoxLayout.Y_AXIS); k »w .. for (String instrumentName : instrumentNames) ( ■ JLabel instrumentLabel = new JLabel(instrumentName); \/ instrumentLabel.setBorder(BorderFactory.createEmptyBorder(4, 1, 4 1)); nameBox.add(instrumentLabel); } на- (fWi гшками. background.add(BorderLayout.EAST, buttonBox); background.add(BorderLayout.WEST, nameBox); frame.getContentPane().add(background); GridLayout grid = new GridLayout(16, 16). grid.setVgap(1); grid.setHgap(2); rsS-r**-* JPanel mainPanel = new JPanel(grid); background.add(BorderLayout.CENTER, mainPanel); checkboxList = new ArrayListO() ; for (int i = 0; i < 256; i++) { JCheckBox c = new JCheckBox(); c.setSelected(false); checkboxList.add(c); mainPanel.add(c); } frame.setBounds(50, 50, 300, 300), frame.pack(); frame.setVisible(true); } private void setUpMidi() { try { sequencer = MidiSystem.getSequencer(); sequencer.open(); sequence = new Sequence(Sequence.PPQ, 4) track = sequence.createTrack(); sequencer.setTempoInBPM(120); ) catch (Exception e) { e.printstackTrace(); } }
private void buildTrackAndStart() { ArrayList<Integer> trackList; // список инструментов для каждо- sequence.deleteTrack(track); track = sequence.createTrack(); for (int i = 0; i < 16; i++) { trackList = new ArrayListO(); int key = instruments[i]; for (int j = 0; j < 16; j++) ( JCheckBox jc = checkboxList.get(j + (16 * i)) if (jc.isSelected()) { trackList.add(key); } else { trackList.add(null); // если ячейка пустая, > ) makeTracks(trackList); track.add(makeEvent(CONTROL CHANGE, Дорожки Сщ^щгьукальушю ffu-WKij: nfuxcfeM ,фШжкаи. cmMam их como- mhiu u cliuawiCuMAi с нижнеш uucmM шшш МаяяЛ'ЖШ ft* KOMfx жтанеЫтт <hложка. JDa. mif»n fo. ; L так nfwcmc - но^хшщм такси. ЖХ как«№№м «Кт)й\и Ъу» главе. I > Змхмтт Mff. ши хотим ащ [х<у |\hcunVttCb IVL iWlKl'W. добавляем null 1, 127, 0, 16)); > track.add(makeEvent(PROGRAM_CHANGE, 9, 1, 0, 15)); // обязательно try { sequencer.setSequence(sequence); sequencer.setLoopCount(sequencer.LOOP_CONTINUOUSLY); sequencer.setTempoInBPM(120); sequencer.start(); } catch (Exception e) { e.printStackTrace(); } 16 Ударов ) private void changeTempo(float tempoMultiplier) { float tempoFactor = sequencer.getTempoFac tor(); sequencer.setTempoFactor(tempoFactor * tempoMultiplier) } private void sendMessageAndTracksO ( boolean[] checkboxState = new boolean[256] for (int i = 0; i < 256; i++) ( JChecxnox check * checkboxList.get(i); if (check.isSelectedO) ( checkboxState[i] = true; > } try { out.writeObject(ueerName + nextNum++ + " out.writeObject(checkboxState); ) catch (IOException e) { System, out.println ("Извините, не удалось отправить данмв на сервер е.printStackTrace(); } userMessage.setText(""); Нс$ш финки,им! Сттайлмшритм -сш^нш на JTcrmu как в Simjx&ChalC&ent пшике i (хШаоигцш (р<ЧЧ nw - rm/uiKU с cwmjiHUtd кмассМфлажков с китмншким рисинксм — имписыват ид1 в исток секста. " + userMessage.getTextО);
public class MyLiatSelection!,istener implements ListSelectionListener { public void valuaChanged(ListSelectionEvent lse) { /(свая (fyHKiUlM JlvdRAWUUAi if (! lse. getValuelsAdjusting ()) { /j.d^tfu fan^temt KOtnLfUW String selected = incomingList. getSelectedValue (); «u/u# ut впс'ср сообщения if (selected != null) { в стихе. 7\ак тс.икс ПСШРС- II now go to the nap, and change the sequence вапи.АЬ ппо тс вшя/шт'•*- boolean[] selectedState = otherSeqsMap. get (selected) 'Ж^М{£1ЛСНЭЮш>Шжас t< changeSequence (selectedState); [Ш/ruuatfKUU hнАиескю sequencer. stop (); KashMah mi нсиваншЖ buildTrackAndStart (); (thaSujvHan и Лпиек/и. и ас ) &<t\fuu;(i/u пж.3мсь umt, najw nfaeljHCK tfi — это свяш- нс с асЛнноспыии неяигейия iM tthficUmrvmt J > ) private void changeSequence(boolean[] checkboxState) { for (int i = 0; i < 256; i++) { JCheckBox check = checkboxList.get(i); check.setSelected(checkboxState[i]); ) public void makeTracks(ArrayList<Integer> list) { for (int i = 0; i < list.size(); i++) { Integer instrumentKey = list.get(i); if (instrumentKey != null) { track.add(makeEvent(NOTE_ON, 9, instrumentKey, 100, i)); track.add(makeEvent(NOTE_OFF, 9, instrvimentKey, 100, i + 1)); ) ) ) ёа,11Ю1фГтм[{ ooivmu ct tiwKium 1ГОЯХ public static MidiEvent makeEvent(int cmd, int chnl, int one, int two, int tick) { MidiEvent event = null; try { ShortMessage msg = new ShortMessage(); msg.setMessage(cmd, chnl, one, two); event = new MidiEvent(msg, tick); } catch (Exception e) { e.printStackTrace(); ) return event;
public class RemoteReader implements Runnable { public void run() { try { Object obj; while ((obj = in.readObject()) != null) { System.out.println("получен объект с сервера"); System.out.println(obj.getClass()); String nameToShow = (String) obj; boolean[] eheekboxState = (boolean[]) in.readObject() оtherSeqsMap.put(nameToShow, checkboxState); listVector.add(nameToShow); ~1 incomingList.setListData(listVector)Л ) J } catch (IOExcaption | ClassNotFoundBxception e) { a.printstackTrace(); л t Лри u,t питуп«ими лш yecxpuau< eye. и оса ешкта (сшшршч и массив чсгиискил гштний) и аесабшж сообщение t RowicHjtHm JIM. Padcma отекмнсгс пспижд (?И1ЛС.ШШиА — он бо. время слиииши сервер- Псе. пи CmunOHiWA,W£ вликьи остскта. гтпскч ^ и массив .лсииегки^ состояния <рлаж . I Мишкшрйсу^> 7f а/У'а иуЗ* 1г^сШ1 ,(,г"0нщ (?к “ компонентJjU.it У)ссабм ши 6jJLirt nfuuc хьуит б у (la там: снашла сохранней сообщенш (1с внутреннем бек пирс сп и ска (Vertov — эти такси о.ласку инпи Апау/М). Ясслсэтск нужно псМаашь вектор Jiast бкаШтбе мшаника аан них: ти са он omoepajju. < элемента б списке
1аточите карандаш _► Решать —вам! У вас уже есть рабочий битбокс-клиент, но всегда можно сделать еще лучше! Вот несколько идей, с которых можно начать: 1. Сейчас, если вы выбираете ритмический рисунок другого пользователя, текущий луп сбрасывается. Если вы только что его настроили — он просто ис¬ чезает. Было бы здорово показать диалоговое окно с вопросом: «Сохранить текущий ритмический рисунок перед загрузкой нового?» — и дать возмож¬ ность сохранить луп, над которым вы работали. 2. Если вы запускаете программу без аргумента командной строки — имени пользователя,— программа завершится с исключением. Стоит добавить в метод main() проверку: если имя не передано — использовать значение по умолчанию или вежливо попросить запустить программу повторно и ввести имя пользователя. 3. Неплохо было бы добавить кнопку, которая генерирует случайный ритми¬ ческий рисунок. И кто знает — возможно, вам понравится именно он! А еще интереснее— иметь библиотеку готовых лупов: рок, джаз, регги... Пусть пользователь выберет базовый ритмический рисунок, с которого будет на¬ чинать. Вы не обязаны вводить весь код вручную — все можно найти в репозитории по ссылке: https://oreil.ly/hfJava_3e_examples. Кстати, есть и альтернативная версия битбокса — в ней используются кол¬ лекции Мар и List вместо массивов. Как говорится, любая задача может быть решена по-разному — выбирайте путь, который вам ближе.
Финальная версия серверной битбокс-программы Большая часть кода совпадает с программой SimpleChatServer из главы 17. Главное отличие в том, что этот сервер принимает и пересылает сразу два сериализованных объекта вместо одной строки. Хотя, строго говоря, один из этих объектов — все та же строка. import j ava.io.*; import j ava.net.*; import j ava.util.*; import java.util.concurrent.*; public class MusicServer { final List<ObjectOutputStream> clientOutputStreams = new ArrayListon public static void main(String[] args) { new MusicServer().go(); } public void go() try { ServerSocket (Шск fox tajuav >\стскс(? serverSock = new ServerSocket(4242) ExecutorService threadPool = Executors.newCachedThreadPool(); (cj'cjauu ccrotv 'ujunicu ш|ш\ /l2/l2. ut. while (!serverSock.isClosed()) { Socket clientSocket = serverSock.accept(); ObjectOutputStream out = new ObjectOutputStream(clientSocket.getOutputStream()) clientOutputStreams.add(out); ClientHandler clientHandler = new ClientHandler(clientSocket); threadPool.execute(clientHandler); System.out.println("Подключение выполнено"); ) catch (IOException e) e.printstackTrace(); JlfumibwtUAi tifmnu- лииш пшкАнт н ня да кииентст; аы кажете сщшм сстп м cnufLtt нпи ССкплНтМа. ) С(ХГШ,СИП(. ц iuohauckckuw Ц№ГГ1' (луп) всем псуклнпп public void tellEveryone(Object one, Object two) { for (ObjectOutputStream clientOutputStream : clientOutputStreams) { try { clientOutputStream.writeObject(one); clientOutputStream.writeObject(two); ) catch (IOException e) { e.pri ntS tackTrace(); } > ИСК ШИЛА. КЯ1ШЦПДЛА, )
public class ClientHandler implements Runnable { private ObjectlnputStream in; public ClientHandler (Socket socket) { (ямом CSjtctlnjMdStxeam.сим rme try | кил poooMAtKWii cm. кшклюшшогс клиенте). in = new ObjectlnputStream(socket.getlnputStream())• ) catch (IOException e) ( e.printStackTrace(); ) public void run() ( Object userName; Object beatSequence; try { while ((userName = in.readObject()) != null) { beatSequence = in.readObject(); System.out.println("Прочитано два объекта"); tellEveryone(userName, beatSequence); } } catch (IOException | ClassNotFoundException e) e.printStackTrace(); > КажШ cm клиента ncmim Щ с\Цх часmeu: ппрскн * maxnT Лещ/иСс е taemu. tiaenvtaw at brtu клшнт/ш. (клюгая мправишля. )
Приложение Б: Ты хочешь ска¬ зать, что это еще не ко¬ нец? Эта книга вообще ^ когда-нибудь закон- Гч“\ чится?! Одиннадцать тем, которые не попали в основную часть книги... Мы прошли большой путь — и вы уже почти добрались до конца этой книги. Мы будем по вам скучать, прав¬ да. Но прежде, чем вы окончательно шагнете в мир Java, мы не можем отпустить вас без небольшого бонуса. Разумеется, уместить все, что полезно знать о Java, в одном приложении — задача не из легких. Мы пробо¬ вали: впихнули все-все-все, что не вошло в остальные главы, уменьшили размер шрифта до 0,00003 пункта... и, да, все влезло. Но, увы, никто не смог это прочитать. Поэтому мы кое-что выкинули. А вот лучшие находки и фишки сохранили — как в плейлисте «Топ недели», только для Java. В этой главе — больше десяти (ну да, не смогли остановиться на ровном числе!) полезных тем, которые стоит знать. Это действительно конец книги. Ну... почти. Потому что после этой главы будет еще предметный указатель. Его тоже обязательно стоит прочитать.
Java 9+ #11 JShell (Java REPL) Почему JShell — это круто? JShell — это REPL, что расшифровывается как Read- Eval-Print-Loop («читай — оцени — напечатай — по¬ втори»). Если коротко, это место, где можно запускать фрагменты Java-кода без всяких танцев с бубном. Ни¬ каких классов, никаких IDE — просто берете и пишете код, а он тут же выполняется. Это идеальный инструмент, если вы хотите попробо¬ вать новую фичу, проверить, как работает тот или иной оператор, попрактиковаться в синтаксисе или просто быстро протестировать идею, пока она не улетучилась. Мы поставили его в начало этого приложения на случай, если вы захотите использовать JShell, чтобы опробовать функции, описанные на следующих страницах. Как запустить JShell? Это инструмент командной строки, который поставля¬ ется вместе с JDK. Если папка b i п из переменной среды JAVA_HOME добавлена в системный путь, достаточно просто ввести j shell в командной строке — и вы сра¬ зу попадете в интерактивную оболочку. Подробности по запуску — в руководстве Oracle по адресу https://oreil.ly/Ei3Df. %jshell | Welcome to JShell — Version 17.0.2 | For an introduction type: /help intro jshell> Инструмент JShell доступен, начиная с версии JDK 9, но хорошая новость в том, что вы можете пользовать¬ ся им даже при работе с более старыми версиями Java. Почему? JShell не зависит от переменной среды JAVA_ НОМЕ и не связан с версией Java, настроенной в IDE. Просто запустите его прямо из папки bin той версии JDK, с которой вы хотите работать. Запуск Java-кода без классов Попробуйте поэкспериментировать с Java-кодом прямо в командной строке: 1 File Edit Window Help LookMumNoSemiColons jshell> System.out.println("Hello") Привет jshell> Примечание: • Класс не нужен • Метод public static void main — тоже не ну¬ жен • Точку с запятой в конце строки ставить не обязательно Просто начинайте писать код на Java! Больше, чем просто строки кода Вы можете задавать переменные и методы: Инструмент JShell поддерживает прямые ссылки, благодаря чему вы можете набросать структуру кода, не определяя все сразу t File Edit WmOow Help ForwaroLookirxi | jshell> void doSomething() { ...> doSomethingElse(); ...> } | created method doSomething(), however, it cannot be invoked until method doSomethingElse() is declared Подсказки по коду Если нажать клавишу Tab в процессе ввода кода, вы увидите подсказки по коду. Также можно использовать клавиши Т и 4-, чтобы прокручивать уже введенные строки. | File Edit Window Help YouCampleteMe **™"™* *~*— jshell> System.out.pr print( printf( println( jshell> System.out.print Команды В JShell есть множество полезных команд, которые не входят в саму Java. Например, введите /vars, чтобы увидеть все объявленные переменные. Команда /exit завершает рабо¬ ту JShell. С помощью /help можно получить список команд и узнать о них подробнее. На сайте Oracle есть полезное руко¬ водство по JShell (https://oreil.ly/Ei3Df), в котором также по¬ казано, как создавать и запускать сценарии.
#10 Пакеты Пакеты защищают от конфликтов имен классов Хотя пакеты нужны не только для предотвращения кон¬ фликтов имен, это одна из их ключевых функций. Если одна из целей ООП — создавать компоненты для повтор¬ ного использования, разработчикам нужно уметь собирать эти компоненты из разных источников и строить на их основе что-то новое. Ваши компоненты должны хорошо «уживаться» с другими, в том числе с теми, которые вы не писали и о которых даже не знали. В главе 6 мы говорили, что имя пакета — это как полное имя класса, официально называемое полностью квали¬ фицированным именем. Класс List на самом деле — это java .util. List, графический элемент List — это j ava .awt.List.aSocket — это j ava . net. Socket. Вот вам и пример: имена пакетов помогают избежать конфликтов — есть List как структура данных и List как элемент интерфейса, и благодаря именам пакетов мы можем их различать. Обратите внимание: у всех этих классов имя начинается с j ava. То есть первая часть их полностью квалифициро¬ ванного имени — j ava. Думайте о структуре пакетов как об иерархии и организуйте свои классы соответственно. Предотвращение конфликтов имен пакетов Стандартное соглашение об именовании пакетов — добав¬ лять к каждому классу обратное доменное имя. Помните, доменные имена уникальны по определению: двух человек могут звать Bartholomew Simpson, но двух разных доменов с именем doh. com быть не может. . ( ('Jk( w Имена пакетов обратного домена \ com.headfirstjava.projects.Chart^^ г ifmmmtHHot Лия. нс am.ltouifatjava. me .тти m miuKcamtcJ — Этс пространстве iuuh тальке '/ нл*« u-v purport ттт k Натнайт iw пдкетя c ыфшптге дож ннш) unmu. шускляя idmu тсткани, а ш- ш ао^мшш coccmminw) стмкпщт Имена пакете* пишутся ста тна ни ФукШШ Если заглянете в примеры кода на сайте https://oreil.ly/ hfjava_3e_examples, то увидите, что классы размеще¬ ны в пакетах с названиями по главам — так примеры удобно отделены друг от друга. ко? Разве она не напоминает ActionEvent иерархию каталогов? Пакеты помогают избе¬ жать конфликтов имен. но только если Вы Выбрали действительно уникальное имя пакета. Лучший способ добиться этого - начинать и/ля пакета с Вашего об¬ ратного доменного имени. com.headfirstbooks.Book Н.ИЯ tUManuj f' vu^K.,aCCa
#10 Пакеты (продолжение) Для упаковки класса: (l) Выберите имя пакета. В качестве примера мы используем com.headfirstjava. Имя класса — PackageExercise, значит, его полностью квалифицированное имя теперь сот. headfirstjava.PackageExercise. (D Добавьте в класс оператор package. Оператор package должен быть первой стро¬ кой в исходном файле — до всех операторов import. В одном файле может быть только один оператор package, поэтому все классы в этом файле должны принадлежать одному пакету. Это касается и вложенных классов. (з) Настройте соответствующую струк¬ туру каталогов Недостаточно просто указать оператор package в коде — этого мало, чтобы класс действи¬ тельно оказался в пакете. Класс будет счи¬ таться находящимся в пакете только тогда, когда исходный файл размещен в соответ¬ ствующей структуре каталогов. Если полно¬ стью квалифицированное имя класса — сот. headfirstjava.PackageExercise, то исходный файл PackageExercise.java должен находиться в каталоге headfirstjava, который, в свою оче¬ редь, должен находиться в каталоге сот. package сот. headfirst java; inport j avax.swing.*; public class PackageExercise { // здесь — судьбоносный код > Л (£.ш«ингт6 Iхрссктв пи lava иехомим tfxuiAtu шт <?кдпы,(г Г «. V java. О каталогах В большинстве Java-проектов исходные файлы размещаются в каталоге src/main/j ava. В реальных проектах исходный код и файлы классов обычно хранятся в разных каталогах — вы ведь не будете копировать исход¬ ники на машину клиента или в облако, достаточно только . class-файлов. а Наиболее распространенная структура Java-проектов основана на соглаше- £> нии Maven': а с MyProject/src/main/j ava — исходный код приложения Му Pro j ect/src/ test/ j ava — код для тестов Файлы классов размещаются отдельно. В реальных корпоративных системах а! для сборки и компиляции обычно используется инструмент вроде Maven <■> или Gradle (в наших примерах используется Gradle). Каждый инструмент помещает . class-файлы в собственные каталоги. Maven Gradle Application classes MyProject/target/classes MyProject/out/production/classes Test classes MyProj ect / target/test- classes MyProject/out/test/classes Lozpe?42 lure augus tat varo свои auguatoLora do aliqula do dal dip PackageExercise.java *Maven и Gradle — самые популярные инструменты для сборки и упаковки Java-проектов.
#8 Уровни доступа и модификаторы доступа (кто что видит) В Java используется четыре уровня доступа и три модификатора доступа. Модифи¬ каторов — три, потому что один из уровней (по умолчанию) используется, когда вы не указываете модификатор явно. Уровни доступа (от наименее к наиболее ограничивающему): Модификаторы доступа: public protected private Чаще всего вы будете использовать public и private — это два самых распростра¬ ненных уровня доступа, задаваемых с помощью модификаторов. public Используется для классов, констант (переменных, объявленных с модификаторами static final), методов, которые должны быть доступны другим частям кода (напри¬ мер, геттеры и сеттеры), и большинства конструкторов. private Применяется практически ко всем переменным экземпляра и к методам, которые не должны вызываться извне (то есть к «внутренним» методам, которые вызываются пу¬ бличными методами того же класса). Даже если вы редко будете использовать уровни доступа protected и default, важно понимать, как они работают: вы обязательно встретите их в чужом коде. private default public protected чин пакета), нс Л/Y Л 1/lfA &ит лпн.шшннмб ч н.тнгп
#9 Неизменяемость в строках и классах-обертках Мы уже говорили о неизменяемости в главе 18 и еще вернемся к ней в последнем пункте приложе¬ ния. А здесь мы сосредоточимся на неизменяемости двух важных типов в Java: строк и классов-оберток. Почему вас должна волновать неизменяемость строк? Из соображений безопасности и экономии памяти (будь то смартфон, 1оТ-устройство или облако — везде это важно) строки в Java неизменяемы. Это значит, что когда вы пишете: String s = "О"; for (int i = 1; i < 10; i++) { s = s + i; } на самом деле создаются десять разных объек¬ тов String со значениями "О ", "01", "012 ", ..., "0123456789". В конце s содержит строку "0123456789",нов памяти все еще существуют десять разных строк. Точно так же, если вызвать у строки метод, который вроде бы ее «меняет», то переменная str все еще ссылается на "the text", a upperStr—это новый объект String со значением "THE TEXT". Оригинальный объект не изменился — был создан новый: String str = "the text"; String upperStr = str.toUpperCase(); Каждый раз, когда вы создаете строку, JVM по¬ мещает ее в особую область памяти, называемую строковым пулом (String Pool). И если строка с таким значением уже есть в пуле, JVM не создает дубликат — она просто использует ссылку на уже существующий объект. Так что вместо 500 объектов со словом "клиент" будет 500 ссылок на один и тот же объект строки " клиент String strl — "customer"; String str2 = "customer"; System.out.println(strl == str2t■ ■Л „ .,h, mu схжнякс&м py™ . Неизменяемость позволяет повторное использование JVM может использовать это в своих целях, потому что строки неизменяемы: одна переменная не может изменить значение строки «из-под» другой пере¬ менной, которая ссылается на тот же самый объект String. Что происходит с неиспользуемыми строками? В первом примере создается множество промежу¬ точных строк, которые не используются ("01", "012" и т. д.). Они попадают в пул строк (String Pool), который находится в куче (heap), и в итоге становятся кандидатами на удаление сборщиком мусора (см. главу 9). Строки, которые нигде не ис¬ пользуются, в конце концов будут удалены. Если же вам нужно часто модифицировать строки (например, при конкатенации), можно избежать создания лишних объектов, воспользовавшись StringBuilder: StringBuilder s = new StringBuilder("0"); for (int i = 1; i < 10; i++) { s.append(i); 1 String finalstring = s . toString () ; Таким образом, единственный изменяемый объект StringBuilder обновляется при каждом добавле¬ нии, вместо того чтобы создавать десять неизменяе¬ мых строк и выбрасывать девять промежуточных Почему вас должна волновать неизменяемость оберток? В главе 10 мы говорили, что у классов-оберток две основные задачи: • Оборачивать примитивы, чтобы обращаться с ними как с объектами • Предоставлять статические методы (например, Integer.parselnt()) Важно помнить: когда вы создаете объект-обертку, например: Integer iWrap = new Integer(42); — это все, что вы можете сделать с этой оберткой. Ее значение всегда будет 42. У оберток нет мето- дов-сеттеров. Конечно, можно присвоить iWrap другую обертку — но это будет уже новый объект. Как только вы создали обертку, изменить значение внутри нее уже невозможно.
#7: Переменное количество аргументов (varargs) Мы мельком увидели varargs в главе 10, когда рассматривали метод String. format (). А еще — в главе 11, где описаны удобные фабричные методы для коллекций, varargs позволяют методу принимать произвольное количество аргументов одного типа. Почему это важно? Скорее всего, вы не будете писать много (а может, вообще не будете) методов с параметром varargs. Но вы наверняка будете их использовать, передавая переменное количество аргументов, поскольку в стан¬ дартных библиотеках Java есть полезные методы (вроде уже упомянутых), которые принимают сколько угодно аргументов.. Как понять, что метод принимает varargs? Взглянем на сигнатуру метода в документации String. format (): static String format(String format, Object... args) Три точки (. . .) в параметре говорят о том, что метод может принимать любое количество объектов Object после строки format, в том числе ноль. Например: unnc<|<.< String msg = String. format ("Сообщение") ; uVvnvVfl««tf f' *)(’a Ут String msgNumName = String.format("%d сообщений для пользователя%з", number, name); ptmat<).|lt ф String msgName = String.format("Сообщение дяя%з", name) Методы, принимающие varargs, обычно не заботятся о количестве аргументов — им все равно. Возьмем, например, List. of (). Он просто собирает все переданные значения в список. Создание метода с varargs Хотя чаще вы будете вызывать такие методы, а не писать их, посмотрим, как их создавать. Допустим, вы хотите создать метод, который печатает все, что ему передали: void printAHObjects (Object. . . elements) { for (Object element : elements) { System.out.println(element); } ) Параметр elements — не волшебство: это обычный массив Ob j ect [ ], просто с другим синтаксисом. Метод эквивалентен следующему коду: void printAHObjects (Object [ ] elements) { Разница в вызове метода: с varargs вам не нужно явно создавать массив, достаточно просто передать нужное количество параметров. Правила • У метода может быть только один varargs-параметр • Этот параметр должен быть указан последним в списке аргументов
#8 Уровни доступа и модификаторы (продолжение) default и protected default Оба уровня доступа — default и protected — связаны с пакетами, default (доступ по умолчанию) — это просто: доступ разрешен только коду в пределах того же пакета. Напри¬ мер, класс с доступом default (то есть без public) может использоваться только другими классами из того же пакета. Но что вообще значит «получить доступ к классу»? Код, у которого нет доступа к классу, не может даже думать о нем. А под словом «думать» мы имеем в виду: использовать этот класс в коде. Так, если у кода нет доступа к классу, он не может даже создать его экземпляр с помо¬ щью оператора new, а также использовать его как тип переменной, аргумента или возвращае¬ мого значения. Нельзя даже написать его имя в коде — компилятор тут же выдаст ошибку. Представьте последствия: если класс default, но его методы public, это не значит, что методы доступны извне. Вы не сможете вызвать метод, если не видите сам класс. Зачем кому-то вообще ограничивать доступ внутри одного пакета? Обычно пакет проекти¬ руется как группа классов, которые работают вместе как единое целое. Поэтому логично, что классы внутри пакета получают доступ друг к другу, а снаружи видно только часть — ограни¬ ченное API, специально предназначенное для внешнего использования. Итак, default — это просто. Если у элемента (класса, переменной, метода или вложенного класса) нет явного модификатора доступа, то обратиться к нему может только код из того же пакета. A protected тогда зачем? protected protected почти то же самое, что и default, но с одним ис¬ ключением: классы-наследники могут получать доступ к защищен¬ ным элементам даже если подклассы находятся вне пакета супер¬ класса, от которого они наследуются. И все. Это единственное, что дает protected: возможность подклассам из других пакетов унаследовать защищенные элементы — методы, поля, даже кон¬ структоры. Многие разработчики почти не используют protected, но он все же встречается в некоторых архитектурах. И однажды он может оказаться именно тем, что вам нужно. Интересная осо¬ бенность protected: в отличие от других уровней доступа, он работает только через наследование. Если подкласс, находя¬ щийся вне пакета суперкласса, получит ссылку на объект этого суперкласса, он не сможет вызвать protected-метод через эту ссылку. Единственный способ получить доступ к protected- методу — унаследовать его. Другими словами, подкласс за преде¬ лами пакета не имеет доступа к protected-методу, он просто наследует его. Опытные разработ¬ чики, создающие библиотеки для дру* гих пользователей, активно пользуются уровнями доступа default и protected. Они помогают от¬ делить внутреннюю реализацию библио¬ теки от ее внешнего API, которое вы¬ зывается из чужого кода.
#5 Лямбда-выражения и словари в Java 8+ Jav а 8+ Зачем вам это? Java 8 прославилась добавленными в язык лямбда-выражениями и потоками данных, но вот менее известный факт: также в java . util .Мар появились новые методы, которые принимают лямбда-выражения в качестве аргументов. С этими методами типичные операции со словарями стали проще, понятнее и экономят и время, и усилия. Создание значения без ключа Представим, вы хотите отслеживать действия пользователя на сайте, сохраняя их в объекте Actions. Для этого у вас есть словарь строк: Map<String, Actions> — имя пользователя и его действия. Когда пользователь выполня¬ ет какое-то действие, которое вы хотите добавить в его объект Actions, вам нужно: • Либо создать новый объект Actions для этого пользователя и добавить его в словарь • Либо получить уже существующий объект Actions для этого пользователя До версии Java 8 для этого часто использовали оператор i f с проверкой на null: MapKString, Actions> custActs = new HashMap<>(); / / возможно, здесь что-то еще. . . -гг ^ -м'iC ' Actions actions = custActs.get(usr); if (actions = null) { ^ actions — new Actions(usr); custActs.put(usr, actions); } // дальше работаем c actions HtH'~ гфвБзак” Кода вроде бы немного, но его нужно использовать в каждом случае. С версии Java 8 и выше такой подход больше не применяется. Теперь можно восполь¬ зоваться методом compute If Absent и передать ему лямбда-выражение, которое указывает, как вычислить значение, которое должно быть добавлено в словарь, если для указанного ключа еще нет записи: Actions actions = г м ylrtreM. .«dc Hcftaii, сашн- «V к. &» «W*- custActs.computeIf Absent(usr, name -> new Actions(name)); 9nuAiA,Mc^a-dt\fycoKum cnurafem, как со? font w&t?Hamwt kcumxAcUom), гея и фя ШНЙ na™jb(?an\i.,iM в словам пока пито нет.
#6 Аннотации Для чего нужны аннотации? Мы вскользь упоминали аннотации в главе 12, когда говорили, что интерфейсы, которые можно реализо¬ вать как лямбда-выражения, могут быть помечены аннотацией @FunctionalInterfасе. Аннотация в коде позволяет изменить его поведение или просто выступать как понятная компилятору документация — то есть вы помечаете код дополни¬ тельной информацией, которую компилятор может использовать. В реальных проектах вы обязательно встретите аннотации и, скорее всего, сами будете их использо¬ вать. Где вы встретите аннотации? Вы встретите аннотации в проектах, где использу¬ ются популярные библиотеки и фреймворки: Java ЕЕ / Jakarta ЕЕ, Spring / Spring Boot, Hibernate, Jackson — все это стандартный набор в мире Java, как для крупных приложений, так и для маленьких. ./{ннотяцдя. @SpringBootApplication CJ.1Я класса public class HelloSpringApplication { Особенно часто аннотации встречаются в тестовом коде. В главе 5 мы говорили о том, зачем писать тесты, но пока не показывали сами фреймворки, ко¬ торые серьезно упрощают этот процесс. Самый рас¬ пространенный — JUnit. Если вы откроете примеры кода по ссылке https://oreil.ly/hfJava_3e_examples, то в папке test найдете классы с тестами. @Test <*'"4иаотдцня с^ля .ucmuja void shouldReturnAMessage() { Аннотации можно добав¬ лять к классам, методам, переменным (локальным и полям экземпляра), аргу¬ ментам методов — и даже в некоторые другие места кода. Аннотации могут содержать элементы Некоторые аннотации содержат элементы — это как именованные параметры: @ТаЫе (пате»" cust") public class Customer { Если у аннотации только один элемент, его имя указывать не нужно: @Disabled("Этот тест еще не закончен") void thisTestlsForlgnoringO { У аннотаций без элементов скобки вообще не тре¬ буются — просто пишите @Test. Можно назначить несколько аннотаций на один и тот же элемент: класс, метод или переменную. А для чего они? Для разных целей! Некоторые — это просто способ сделать код понятнее и безопаснее. Например, ан¬ нотация @Functional Interface: если добавить ее к интерфейсу с более чем одним абстрактным методом, компилятор выдаст ошибку. Другие, как @NotNull, используются ГОЕ или ана¬ лизаторами кода, чтобы найти возможные ошибки. Многие библиотеки предлагают собственные ан¬ нотации, чтобы указать фреймворку, что делать с этим куском кода. Например: @Test помечает метод как тест, который JUnit будет запускать от¬ дельно; @SpringBootApplication помечает класс с методом main как точку входа в Spring Boot-приложение; @Entity указывает, что класс представляет объект, который Hibernate должен со¬ хранять в базу данных. Некоторые аннотации добавляют функциональ¬ ность. Например, Lombok использует аннотации, чтобы генерировать код. Добавьте в класс аннота¬ цию @Data, и Lombok создаст конструкторы, гетте¬ ры (и при необходимости сеттеры), а также методы hashCode(),toStгing () и equals (). Иногда аннотации кажутся магией: все пове¬ дение, связанное с аннотацией, скрыто внутри фреймворка. С хорошо задокументирован¬ ными аннотациями вы лучше поймете, как они работают и что происходит за кадром. А значит, будет проще находить и устранять ошибки.
Java 8+ #4 Параллельные потоки В главе 12 мы подробно разбирали Streams API. Но мы не затронули одну очень интересную возмож¬ ность: как использовать потоки параллельно на многоядерных процессорах и запускать операции одновременно. Самое время это исправить! До сих пор мы применяли потоки как способ «за¬ просить» структуру данных. А теперь представьте, что ваши данные — ОГРОМНЫЕ: вся база данных, или поток данных в реальном времени из соцсети. Конечно, можно обрабатывать все по очереди, шаг за шагом... А можно — разбить задачу на части и выполнить их параллельно, на разных ядрах CPU. После чтения глав 17 и 18 у вас наверняка появил¬ ся соблазн написать многопоточное приложение. Но вам этого делать не нужно — все уже есть в Streams API. Запускаем параллельную обработку С помощью Streams API вы можете просто указать, что хотите выполнять поток на нескольких ядрах. Есть два способа: 1. Запустить метод parallelStream() List<Song> songs = getSongs(); Stream<Song> par = songs . Tfft^n,mrv£ parallelStream() ; /1(гННИ£ О ГШ'НАЛ! 2. Добавить метод .parallel!) к обычному по¬ току List<Song> songs = getSongs(); Stream<Song> par = songs.str eam() .parallel(); Оба способа приводят к одинаковому результату — выбирайте, как вам удобнее. А дальше? Пишете поток, как в главе 12 — добавляете нужные операции и завершающий метод. Остальное за вас делает Java: • Разбивает данные и распределяет их между потока¬ ми на нескольких ядрах процессора • Определяет, сколько параллельных задач запустить • Объединяет результаты из разных потоков Многопоточность «за кадром» Параллельные потоки реализованы на основе фреймворка Fork/Join (эта тема выходит за рамки данной книги, см. https://oreil.ly/XJ6eH). Это раз¬ новидность пула потоков, о которых мы говорили в главе 17. Потоков в параллельном потоке будет столько же, сколько ядер а процессоре на той маши¬ не, где выполняется ваше приложение. Существуют способы изменить эту настройку, но рекомендуется придерживаться значений по умолчанию — не лезь¬ те туда без острой необходимости. Не нужно делать все параллельным! Прежде чем кинуться и повсюду добавить . parallel (), остановитесь. Помните, в главе 18 мы говорили, что многопоточность — штука слож¬ ная, и решение зависит от задачи, данных и среды? То же и с параллельными потоками. Они не бес¬ платны и не гарантируют, что ваше приложение будет работать быстрее. Если на вход потока поступает простая коллекция — как в примерах из главы 12 (а именно так потоки чаще всего используются сегодня), то последователь¬ ный поток почти наверняка будет работать быстрее. Да, вы не ослышались: в большинстве обычных слу¬ чаев вам не стоит использовать параллельный поток. Вот когда параллельные потоки могут ускорить работу: • Коллекция ОГРОМНАЯ (сотни тысяч элементов и больше) • Поток выполняет сложные, ресурсоемкие опера¬ ции • Разбиение (декомпозиция) данных или операций и последующее объединение результатов не слиш¬ ком затратны Перед использованием параллельной обработки измерьте производительность — при последователь¬ ной и при параллельной работе. Если хотите узнать больше, в книге Java 8 Lambdas Ричарда Уорбертона есть отличный раздел, посвященный параллельной обработке данных.
#5 Лямбда-выражения и словари (продолжение) Java 8+ Изменение только существующих значений Иногда требуется изменять только существующие значения в словаре. Например, если вы ведете учет каких-то событий или метрик, и хотите обновлять только интересующие вас значения. Добавлять новые метрики вы не планируете. В версиях Java ранее 8 вы бы использовали containsKey, get и put в комбинации: Map<String, Integer> metrics = new HashMap<>() // здесь, кажется, что-то происходит... ?uenMu шприка (.„ iuec,uu«m бЦ&ши if (metrics.containsKey(metric)) { Integer integer = metrics.get(metric) metrics.put(metric, ++integer);^ ) IU ■ 7bio&iwtu. awn AtC Yam кшгшш Gcw&ft-nbMftMMfHavLHUL. В версии Java 8 для изменения только существующих значений в словаре был добавлен метод сотриtelf Present, который принимает ключ и лямбда-выражение. Благодаря ему код можно сократить до одной строки; metrics.computelfPresent(metric, (key, value) -> ++value); 9,1 mk fif им wnO, «* шишл‘ Ыит Другие полезные методы ^ ^шслшп U; Ui K emt 6 c.ic(taf\L. fHare"l1t Для работы со словарями используются и другие расширенные методы, которые позво¬ ляют добавить значение, если его нет, обновить существующее значение или удалить его — все это с помощью лямбд. Вот несколько примеров: merge () — объединяет старое и новое значение по вашей логике compute () — позволяет и создать, и обновить значение replaceAll () — принимает лямбду и заменяет все значения в карте на новые, рассчитанные по вашей формуле (например, можно использовать для увеличения всех метрик) forEach () —перебирает все пары ключ/значение Библиотеки Java постоянно развиваются, поэтому даже если вы хорошо знаете объекты List или Мар, всегда полезно поглядывать в документацию вдруг появилось что-то, что сделает жизнь проще. Официальная документация Java API доступна по адресу https://docs.oracle.com/en/java/ javase/17/docs/api/java.base/java/util/Map.html. В ней можно посмотреть, какие методы класса доступны и что именно они делают.
#3: Перечисления (продолжение) Посмотрим, как выглядел бы список участников группы, если бы мы исполь¬ зовали «настоящее» перечисление (enum). Хотя это очень простой пример перечисления, большинство перечислений действительно бывают такими же простыми. Настоящее перечисление (enum) |^ public enum Member { KEVIN, BOB, STUART }; ^ onlwjMeHWt класса, iwm 1 ,L (ia/№u 6Ц класса- V< public class SomeClass { public Member selectedBandMember; // later in the code. // позже в коде... if (selectedBandMember = Member.KEVIN) // действия/ связанные c KEVIN > } :sa»f-*as Hi нужно decncKcunucd c ycnymiitutv ' ^наген Иях этой, m/x, пеняем/ ° Enum-класс неявно наследует java.Iang.Enum (ктш^Гяи[ш1Щн1ы к пукгисюния Работая с перечислением, вы создаете новый класс, и неявно расширяете j ava . lang. Enum. Вы можете объявить enum как отдельный класс в собственном . j ava- файле или как член другого класса. Операторы if и switch с enum Теперь, когда у нас есть перечисление, мы можем использовать его в условиях if и в операторах switch. Обратите внимание, что экземпляры enum можно сравнивать как через ==, так и через .equals (). Обычно оператор == считается более элегант¬ ным вариантом. Member member = Member.BOB; if (member.equals(Member.KEVIN)) System.out.println("БаалУУУ!") ; if (member == Member.BOB) System.out.println("Пууух") ; Jlhujcdautfacu тажнш м^ммслшия (Цк.,ценной У switch (member) { ^ case KEVIN: System.out.print("Эээ... ла кукарАЧА?"); case BOB: System.out.println("Король Боб!"); case STUART: System.out.print("Бананааа!"); Aih h u (lUKmipiuuJ Чтс nfwifviими? ) В перечисление (enum) можно добавлять конструктор, методы, пере¬ менные и даже тело класса, специфичное для отдельной константы (constant-specific class body). Встречается нечасто, но вы можете с этим столкнуться. jtmmwHtHj » n/7. ¥ (ym/cm.t xn
#3 Перечисления («перечисляемые типы» или просто enum) Мы уже говорили о константах, которые есть в API, например, JFrame. EXIT_ON_CLOSE. Вы также можете создать свои собственные константы, объявляя переменные с модификаторами static final. Но иногда нужна не просто одна константа, а набор допустимых значений для переменной — и больше никаких. Такой ограниченный набор значений называется пере¬ числением (enumeration). Полноценные перечисления появились в Java еще в пятой версии. Участники группы Допустим, вы делаете сайт для своей любимой музыкальной группы и хотите, чтобы комментарии направлялись определенному участнику. Старый способ «эмулировать» enum public static final int KEVIN = 1; public static final int BOB = 2; public static final int STUART = 3; // later in the code if (selectedBandMember == KEVIN) { // делаем что-то, связанное c KEVIN Шт науиися, »>и к эпи.ии мишнти применят sc&cUdfiat\d,4id\&i иш с!~ утуптта а ' 1 ) Радует, что такой прием действительно делает код более читаемым. Еще одна хорошая новость: значения псевдо- enum-констант изменить нельзя: значение переменной KEVIN всегда будет равно 1. А вот плохая новость: ника¬ кого простого и надежного способа гарантировать, что selectedBandMember примет только значения 1, 2 или 3, нет. И если какой-нибудь труднодоступный фраг¬ мент кода присвоит selectedBandMember значение 812 — можно не сомневаться в сбое программы. Да, это действительно СТАРЫЙ способ имити¬ ровать перечисление, но вы все еще можете встре¬ тить такой код в реаль¬ ных проектах (например, в старых библиотеках Java типа AWT). Однако если у вас есть возможность изменить код — лучше использо¬ вать enum, а не такие константы. Подробнее на следующей странице...
#1 Записи Java 16+ Для чего нужны записи? Так называемый «простой» Java-объект данных на деле оказывается совсем не простым. Даже класс с парой полей — то, что раньше называли Java Bean, — требует гораздо больше кода, чем предпо¬ лагается. Класс данных в Java до версии 16 Представим простой класс Customer, с полями паше и id: public final class Customer { private final int id; private final String name; public Customer(int id, String name) { this.id = id; this.name = name; > public int getld() { return id; ) public String getName() ( return name; i public boolean equals(Object o) { } public int hashCode() { } public String toStringO { } ) Здесь мы опустили детали реализации методов equals(), hashCode() и toString(),но на практике вы бы, скорее всего, захотели их реализо¬ вать — особенно если планируете хранить объект в коллекциях. Мы также не добавили сеттеры, ведь поля final, но в реальной жизни вы, возможно, за¬ хотите иметь и возможность изменить поля. А теперь посчитайте строки! Это ведь просто класс с двумя полями — а полный код (включая реализа¬ цию) занимает 42 строки! Вот бы в Java был специальный синтаксис для классов данных! Этс кслжснеттт лтип*. бни ‘ пишите Ли экЛплфл лмтсуя fcrnf мтаndк этл** ntfи-шннм-п. public record Customer (int id, String name){) JTauicuo nfurtac шгс.лс<кклг- nncn oxnafuiMm, А1дяст* ксагтfuiKmcfx (<Гклка« ncfdujKt ufjin пацлшпуш)- Вот и все! Больше ничего не нужно, чтобы заменить «старый» Customer-класс в 42 строки на одну. Класс-запись (record) содержит поля (перемен¬ ные экземпляра), конструктор, методы доступа, equals (), hashCode () и toStringO — все по умолчанию. Как использовать записи Записи используются так же, как обычный класс: Customer customer = new Customer(7, "me"); System.out .println(customer); System.out.println(customer.name()); *4 Вывод будет таким: [rie E::;t УУггЬ'л Чйф Viry, %java UsingRecords Customer[id=7, name=me] Ccpnnnunc «лпш: mym au MatmcM na>ntO. a "tgfAamtO/ Прощай, get IIanal tcShinaO iff ok iu um.mnWH f СпЩ тип*. «пн teuton Заметили? В записях (record) не используется классический префикс get в методах для чтения полей. Именно поэтому мы аккуратно называем их методами доступа, а не геттерами. Метод просто называется так же, как и компонент записи. Вы можете переопределить конструкторы По умолчанию record генерирует конструктор, ме¬ тоды доступа (accessors), equals (), hashCode () и toString (). При необходимости их можно переопределить. А он есть! В версии Java 16 и новее можно не писать class — вместо этого используйте record. Обычно это не требуется. Но, к примеру, если вы хотите добавить валидацию при создании записи, можно переопределить конструктор.
Java 10+ #2 var: автоматическое определение типа локальной переменной В версии Java 10 и новее вы можете использовать ключевое слово var при объявлении локальных пере¬ менных (то есть переменных внутри методов, но не параметров и не полей класса). var name = "A Name"; name — это строка- Это еще один пример вывода типа, когда компилятор использует уже известную информацию о типе, чтобы вам не пришлось писать лишний код. Компилятор понимает, что name — строка, потому что справа от знака = указано строковое значение. £— var names = new ArrayList<>(); var customers = getCustomers() C «tut nu eastern*™ ~ 9m< Вывод типа, но HE динамическая типизация Когда вы объявляете переменную через var,у нее все равно есть тип. Это не способ добавить в Java дина¬ мическую или опциональную типизацию (это не def из Groovy!). Это лишь способ избежать повторного написания уже известного типа. Но тип все равно нужно как-то сообщить компиля¬ тору при объявлении переменной. Позже задать его нельзя. Поэтому вот так делать нельзя: var name; Компилятор не понимает, какой у паше должен быть тип. И вот это тоже не сработает: var someValue = 1; someValue = "String"; Нельзя менять тип переменной после ее объявления. Пожалейте других программистов С помощью var можно сократить код, и IDE показы¬ вает вам тип переменной — удобно! Поэтому может возникнуть соблазн писать var везде. Но представьте, что код читает другой програм¬ мист — например, без IDE или не зная контекст. Тако¬ му человеку будет сложно понять, что происходит. Вот почему в этой книге мы не использовали var, даже если это упростило бы код. Мы хотели, чтобы вы, как читатель, всегда видели явно, какого типа каждая переменная. Совет: выбирайте осмысленные имена переменных Если в коде не видно явно, какого типа перемен¬ ная, то осмысленные имена переменных и мето¬ дов особенно важны — они помогут понять, что происходит. ЛоиятнЛ тс этс ***** var reader = newBuf feredReader(get(”/")); var stuff = doTheThingO ; rmx nuiKct Совет: тип переменной будет конкретным В главе 11 мы старались использовать интерфейс в качестве типа переменной, а не конкретную реализацию: List<String> list = new ArrayList<>(); С ключевым словом var так сделать нельзя. Тип переменной будет таким, как указано справа: var list = new ArrayList<String>(); Совет: не используйте var с ромбовидным оператором (о) Вернемся к предыдущему примеру. Мы объявили list как List<String> и использовали справа ромбовидный оператор (<>). Компилятор понял, что тип элементов списка — String, потому что это явно указано слева. В случае с var, как во втором примере, компиля¬ тор уже не видит этого. Если вы хотите, чтобы список по-прежнему содержал строки, нужно явно указать тип справа. Иначе список будет со¬ держать объекты типа Ob j ect. var list = new ArrayList<>(); Two r' С^1^сис.хс> пыии^г Полные рекомендации по стилю кода от OpenJDK можно найти по адресу https://oreil.Iy/eVJ5d
Java 16+ #1 Записи (продолжение) public record Customer(int id, String name) { public Customer(int id, String name) { if (id < 0) ( throw new ValidationException(); ) this.id = id; this.name = name; ) } На самом деле, все еще проще. Пример выше — это канонический конструктор, то есть самый обыкно¬ венный конструктор, используемый многими про¬ граммистами. С записями можно использовать еще один вариант — компактный конструктор. В этом случае предполагается, что все стандартное уже за¬ дано: нужное количество параметров, правильный порядок, присваивание переменным экземпляра — все это берется из заголовка записи. Вы прописы¬ ваете только значимую логику, такую как проверка значений: - ' public record Customer(int id, String name) { public Customer { нужнс шл&ыть iut- KCMcmfMfKmofw Жсуиишпры (Set так жг qc if (id < 0) ( ступим (faumtш кенптйктра throw new ValidationException () ' ' Jit щжнеявш) niuwfomiam ma- Щ t khuA пс^к.шнннн эKjtw\.\Mfxa Когда вы вызываете конструктор Customer, вы по-прежнему передаете id и name, и они автомати¬ чески сохраняются в переменные экземпляра — это все задается заголовком записи. Чтобы добавить проверку, достаточно воспользоваться компактной формой конструктора — остальное компилятор сделает сам. Customer customer = new Customer(7, "me"); Вы можете переопределять и добавлять методы Вы можете переопределить любые из этих методов и добавить собственные — публичные, дефолтные или приватные. Если вы переводите существую¬ щие классы данных в формат записей, возможно, вы захотите сохранить свои реализации equals, hashCode и toString. public record Customer(int id, String name) ( public boolean equals(Object o) { return id = ((Customer) o).id; p } метеу ajual.s, wxum jajanu ну*.нук л.спи^ private boolean isValidName(String name) { // какая-нибудь реализация ) У щгшсеК ПШЖЖ 1 atm «е<шнж* Вы можете создать метод с модификатором pro¬ tected — компилятор пропустит его, но в этом нет смысла: записи всегда финализированы и не могут быть унаследованы. Записи неизменяемы В главе 18 мы говорили о том, как сделать объекты данных неизменяемыми. Неизменяемые объекты безопаснее использовать в многопоточных прило¬ жениях, потому что вы уверены: ни один поток не сможет изменить их состояние. Также проще понимать, что происходит в при¬ ложении, если вы уверены, что объекты данных не изменяются. Поэтому даже в однопоточных приложениях нередко используют неизменяемые объекты. А в пункте №9 этого приложения мы уже видели, как неизменяемость строк помогает эконо¬ мить память. Записи неизменяемы. Вы не можете изменить значе¬ ния в объекте записи после его создания: у записей нет сеттеров и нет способа изменить переменные экземпляра. К ним нельзя обратиться напрямую из¬ вне — только через метод доступа. Если попытаться изменить переменную экземпляра внутри самой записи, компилятор выдаст ошибку. Внутри записи все переменные экземпляра автоматически объявля¬ ются как final. Подробнее о записях можно узнать в документации Oracle по классам записей (https://oreil.ly/D7fh3). Там же вы найдете описание других новых возмож¬ ностей языка, появившихся в Java 17, о которых мы не успели рассказать — например, сопоставление с шаблоном (Pattern Matching), запечатанные классы (Sealed Classes), расширенные выражения switch и удобные текстовые блоки (Text Blocks). .дщгит читн|(г
G Gradle 722 H HAS-A, связь 215 if/else, конструкция 48 if, оператор 51 immutability 724 import, оператор 191 InetSocketAddress 627 InputStreamReader 632 interface, ключевое слово 262 IS-A, связь 215 J Java N10 633 использование памяти 40 история 40 ключевые слова 89 модули 197 нумерация версий 41 скорость 40 структура кода в 43 Java API 161,190 Java Collections Framework 345 JavaFX 500 java.nio.channels, пакет 633 JavaSoundAPI 457 java.util, пакет 350 JFrame 498,546 JPanel 546 JPanel.paintComponent() 508 JScrollPane 560 JShell 720 JTextArea.requestFocus() 560 JTextArea.selectAll() 560 JTextArea.setLineWrap(true) 560 JTextField 559, 560 JTextField.requestFocus() 559 JTextField.selectAll() 559 L limit(), Stream 411-413 Hst.addListSelectionListenerf this) 563 listener check.addItemListener(this) 562 list.addListSelectionListenerfthis) 563 List interface collecting to 436,445 List.of() 393 Hst.setSelectionMode(ListSelectionModel.SINGLE_SE- LECTION) 563 Hst.setVisibleRowCount(4) 563 List.sort() 368 List, интерфейс 381 longcount() 413 M Map interface and collections 445 Map.of() 393 Map.ofEntries() 393 map(), Stream 411 Map, интерфейс 381 Math.abs() 324 Math.max() 325 Math.min() 325 Math.random() 324 Math.roundQ 325
предметный указатель А ActionEvent 503 ActionListener 503 addActionListener() 503 ArrayList 168 автоупаковка 327 замена на HashSet 383 как обобщенный класс 358 приведение типов 267 Atomiclnteger 692 AutoCloseable 613 В Boolean anyMatch 413 BorderLayout, менеджер 549 BoxLayout, менеджер 549 BufFeredReader 602 Buffered Writer 601 c channels, client and server networking SocketChannel 630, 631 check.addItemListener(this) 562 collect() 413 collections Map 445 Collectors.joining 445 Collectors.toList 414, 445 Collectors.toMap 445 Collectors.toSet 445 Collectors.toUnmodifiableList 423,445 Collectors.toUnmodifiableMap 445 Collectors.toUnmodifiableSet 445 Comparable, интерфейс 390 Comparator, интерфейс 368, 433 ConcurrentModificationException 699 ControllerEvent 533 Convenience Factory, методы 393 CountDownLatch 661 Строка формата 334 D DailyAdviceServer 638 distinct(), Stream 411, 442-443 E Executors 662 ExecutorService.shutdownO 651,665 ExecutorService.shutdownNow() 665 ExecutorService, интерфейс 651 extends, ключевое слово 364 Extreme Programming (XP) 137 F FilelnputStream 587 FileOutputStream 578 FileReader 602 FileWriter 595 final, ключевое слово 696 findFirstQ, Optional 413 FlowLayout, менеджер 549 Fork/Join Framework 731 for, цикл 150 улучшенный 152 Function, в методе map() 441
А Абстрактные методы 262,432 реализация 242 Абстрактный класс 238 и полиморфизм 244 и статические методы 314 конструкторы 289 реализация интерфейса 262 условия для использования 265 Автоупаковка 328 Алфавитный (естественный) порядок 351 Анимация 528 Аннотации 728 Аргумент 85, 110, 112 автоупаковка 328 геттеры и сеттеры 115 конструктор без 283 конструктор суперкласса с 293 метода 328 с обобщенными типами 394 Атрибут 66 Б Бай-ткод 38 Битбокс, приложение 458 Битва за кресло 64 Блок catch 464 finally 469 синхронизированный 683 Блокировка 681 взаимная 690 объекта 683 оптимистичная 692 > (больше, чем) 49 Буферы 601 В Ввод-вывод 597 неблокирующий 597 Ветвление 48 условное 51 Виджет 498, 546 для рисования 508 Виртуальная машина 38,40 Г Генерация случайных чисел 55 Геттеры 115 Градиент, графический объект 511 Графика 507 Графический интерфейс создание 497 Граф объектов 582 д Данные изменение неизменяемых 696 мета- 608 разделение 698 Десериализация 587 Документация по API в HTML-формате 196 Доступ модификаторы 117, 725 ограниченный 227 уровни 725 Дубликат 384 удаление 442 3 Заглушка 139,346 Записи 697 Зарезервированные слова 89 %, знак процента в строке формата 334
Math.sqrtO 325 Maven 722 Midi-событие 485 MIDI-сообщение 486 изменение 487 структура 486 Mock-код 346 О ObjectlnputStream 587 ObjectOutputStream 578 ObjectOutputStream.close() 578 ObjectOutputStream.writeObjectQ 578 P Path, интерфейс 608 Predicate 411 Print Writer 631,632 a QuizCardPlayer 603-605 R Reader 630 REPL 720 Runnable, интерфейс 648 RuntimeException 466 s Serializable, интерфейс 583 serialVersionUID, поле 593 ServerSocketChannel 637 setLayout(null) 558 setLocationCellsQ 138 Set.ofO 393 Set, интерфейс 381 skip(), Stream 411 Socket Address 630,631 SocketChannel 627,630,631 sorted() 411 Startup class 186 Streams API 411 StringBuilder 724 Strings immutability 724 String, класс 227 super() 292 super, ключевое слово 218 Swing 498 компоненты 546 synchronized, ключевое слово 682 т Test-Driven Development (TDD) 137 this() 294 Thread класс 646 конструктор 650 transient, ключевое слово 586 try/catch, конструкция 468 и unwrapping a value 326 V values, variable wrapping and unwrapping 326 w while, цикл 151 Writer 631
Конструктор 279, 287 JList 563 без аргументов 283 в процессе десериализации 588 инициализация состояния нового объекта 281 канонический (полный) 736 ключевая особенность 280 компактный 736 перегруженный 294 переопределение 735 приватный 227 суперкласса 288 цепочка вызовов 289 Контракт 221, 228, 256 Куча 76, 274 с возможностью сбора мусора 76 Л Логические выражения 187 Логические проверки 49 Лямбда-выражение 424, 425, 526 виды 426 метод 430 многострочное 429 однострочное 428 параметры 429 структура 427 функциональный интерфейс 425 м Массив 95, 172 ассоциативный 391 объектов 96 поведение объектов в 119 полиморфный 224 строковый 55 элементы 95 Менеджер компоновки 545, 547 политика 548 процесс принятия решений 548 < (меньше, чем) 49 Метод 43, 70, 77 accept() 637 actionPerformedO 503,517 awaitTerminationQ 665 checkUserGuess() 181 checkYourselfQ 138, 140 Collections.sort() 350 compare() 368 compareAndSet 691 compareToQ 365 computelfAbsent 729 computelfPresent 730 contains () 439 equals () 122,384 filterO 436 forEachQ 406 format() 334 getPreferredSizeO 558 getUserInput() 147, 148 hashCodeQ 384 main 45 main() 48 makeEventQ 535 map() 441 nextlnt() 147 print() 631 printlnQ 631 randomQ 147 run() 648 shutdownQ 665 sort() 354 String.formatO 332 String split() 606 toString() 352 writeObjectQ 579 абстрактный 241,242 автоупаковка 328 в стеке 275 вызов нестатического из статического 316 обобщенный 360 объявление 180 перегрузка 229
и И и Или, операторы (&&, ||) 187 != и ! (неравенства) 187 Инициализация с помощью конструктора 282 статических переменных 319 Инкапсуляция 116 ++, — (инкремент/декремент) 142, 151 Интернет вещей 53 Интерфейс 262 маркерный (теговый) 583 полиморфизм 264 слушателя 502 Исключение BindException 629 NumberFormatException 330 выбрасывание 465 как объект 464 обработка 457 переадресация 477 перехват 464 полиморфизм 472 Источник событий 503 Исходник 38 К Каналы 623 Карточки для викторины 596 Каталоги 722 Класс 43, 50, 77 Collections 350 Collectors 419 CopyOnWriteArrayList 701 DailyAdviceClient 634 Files 608 Formatter 332 GameHelper 148 java.io.File 600 JComponent 546 Math 312,324 Object 246 Paths 608 Random 147 абстрактный 238 вложенный 520,521 внешний 521 вспомогательный 148 для неизменяемых данных 694 документация 198 защита от конфликта имен 721 конструкторы 279 исполнители 651 статические переменные 319 конкретный 238 обертка 326, 330 обобщенный 358, 360 отличие от объекта 71 полное имя 191 проектирование 70 разработка 135 структура 44 упаковка в пакет 722 управление версиями 592 утилита 190 финальный 321 экземпляры вложенного и внешнего 521 Код mock- 346 байт- 38 псевдо- 135 рабочий 135 тестовый 135 Коллекции 345 API 382 наполнение 392 распространенные операции с 410 создание 392 фабричные методы 393 Компаратор 368 Компилятор 38, 57 Константы 77, 320
Потоки п Пакеты 721 компиляция и запуск с 723 обратного домена 721 стандартное соглашение об именовании 721 упаковка класса в 722 Панели 547 Параметризованные типы 173 Перегрузка метода 229 Переменная 87 атомарная 691 глобальная 77 итерации 152 локальная 274 нестатическая финальная 321 область видимости 296 объявление 41,86, 180 присвоение значения 88 ссылочная 91,298 статическая 319,322 строковая 52 целочисленная 52 экземпляра 70, 77,274 Перечисления 732 Полиморфизм 238 абстрактные классы 238 аргументы и типы возвращаемых значений 225 интерфейсов 264 исключения 472 обобщенные типы 357 Полностью квалифицированное имя 721 Полоса прокрутки 560 Потерянного обновления, проблема 686 Поток выполнения множество 646 планировщик 654 простейший способ создания 650 состояния 652 стеки и 647 управление с помощью конструкций try/catch 468 усыпление 658 выполнения 623 подключения 579 цепочные 579 Потоки данных 412,413 извлечение результата из 414 конвейер 415, 416 операции с 415 подсчет элементов в 446 поиск конкретного элемента в 446 проверка наличия элементов в 446 фильтрация 437,438 Приведение типов 147,153 неявное 114 явное 114 Примитив 86, 87, 122 обертка для 326 Программа для моделирования поведения животных 208,236 Программа-советчик 634 Промежуточная операция 414 Противоречивые результаты 59,230,408 Р Равенство 384 объектов 384 ссылок 384 Разбор строк 606 о (ромбовидный оператор) 348 С Сборщик мусора 76,298 Секвенсор 460, 482 Серверное приложение 637 Сервлет 595 Сериализация 595 использование 576 Сетевое взаимодействие 623 Сеттеры 115,116 // (синтаксис комментариев) 48
передача нескольких значений 112 переопределение 68 реализация 180 самого низкого уровня 213 синхронизация 684 синхронизированный 683 ссылкана 444 статический 322, 330 удаленный вызов 589 фабричный 393 финальный 321 Многопоточность 645, 666 интерфейс Runnable 648 параллельные потоки 731 планировщик потоков выполнения 653 пул потоков 662 состояние потока 652 стек 647 Множество 381 н Наследование 67, 204, 238 дерево 240 множественное 261 ценность 220 Неизменяемость оберток 724 строк 724 Нулевая ссылка 94 О Обобщенные типы 345 Объект 63 MidiEvent 485 Socket 632 блокировка 683 в куче 580 вложенный 521 восстановление 587 граф 582 жизненный цикл 289 запись сериализованного в файл 578 как лямбда-выражение 425 массив 96 неизменяемый 694 отличие от класса 71 поведение 77, 575 самый востребованный 305 сериализованный 576, 580 создание 72 состояние 77, 575 сохранение 575 ссылкана 86,91 Объектно-ориентированный язык 50 Оператор break 141 try-with-resources (TWR) 612 без короткого замыкания 187 > (больше, чем) 49 декремента 151 инкремента 151 конкатенации 55 < (меньше, чем) 49 неравенства 187 постинкрементный 141 присваивания 49 ромбовидный 348, 734 с коротким замыканием 187 сравнения 49, 122, 384 точка (.) 72, 90, 97 -> (оператор для лямбда-выражений) 424 = (оператор присваивания) 49 == (оператор сравнения) 49, 122, 384 . (оператор точка) 72, 90, 97 &&, || (операторы И и Или) 187 Операция промежуточная 412 терминальная 413 Отображение 391,441
Словарь 391 Слушатель событий 503 Смертельный ромб, проблема 261 Событие источник 503 обработка 501 объект 506 слушатель 503 Сортировка 350 Состояние гонки 666, 686 Сохранение состояния 577 Спецификаторы формата 333 Список аргументов 284, 338 Сравнения и замены, операции (СAS) 691 Ссылка 86 на метод 444 на объект 86 : (ссылка на метод) 444 Статический импорт 339 инициализатор 320 Стек 274 вершина 275 методы в 275 поведение 275 фрейм 275 Строки 98 запись в текстовый файл 595 неизменяемость 724 повторное использование 724 превращение чисел в 331 преобразование в примитивное значение 330 Строковый пул 724 Структуры данных потокобезопасные 700 т Терминальная операция 414 Тип 337 безопасность 356 возвращаемый 85 выведение 348 десятичное число 337 логический 50 обобщенный 356, 357, 394 параметризованный 173 преобразование одного в другой 441 приведение 153 примитивное целочисленное значение 85 символ 337 целочисленный 50, 87 число с плавающей точкой 87, 337 шестнадцатеричное число 337 Точка с запятой 48 Ф Фабричные методы 651 {} (фигурные скобки) 48 Фигурные скобки 44 Флажок (JCheckBox) 562 Форматирование чисел 332 Функциональные интерфейсы 432 ц Цикл 49 for 150 for (улучшенный) 142 while 49, 50 Чат-клиент 624, 640 Чат-сервер 642
Изучаем Java О чем эта книга? «Изучаем Java» — это не просто книга. Она не только научит вас теории языка Java и объектно-ориентированного программирования, но и сделает вас программистом. В ее основу положен уникальный метод обучения на практике. В отличие от классических учебников информация дается не в текстовом, а в визуальном представлении. Из нее вы узнаете все самое нужное: синтаксис и концепции языка, работа с потоками, работа в сети, распределенное программирование. Вся теория закрепляется интереснейшими примерами и тестами. ТТо1ЛММ, р^нЛ/U^J ‘ь ооьсюЛом. 1СЛЛОСОЛС 1л. с шят ■■ б] У1’йч'л'1 vJKbb p и О 'I ““'<■> . ■ ■ & 1Ло»чЛЛЬЭД1Л. кХ)Л*|ЧСК>лМ1-^КЛ*\ДлЛ i ' 4 "€ Jav^ ЧЛoSw. ООуйЛЬ Vttfg*} SoiA,* TTotLdCW. C/jAb 0^ЪЫсЛ0"€ попадают внутрь как SocccrBall, FiaK, Guitar и Car. ArrayList<Object> i i Объект, ссылка на который исходит иа Уткл-А кЛЧ1ШИЛ ArrayLiat <Objoct>. действует так, как % ТлХлчЛЛЬ^О'ОЛиДЛА. I если бы он был обычным *•' I экземпляром класса кЛА1ЛЛи>рф>ИШ0< ООМЛ0К. КЛ Object. Компилятор не 0%ЫМа&* Л1Л*Л 1ЛЧЮХЛ в состоянии распознать, - что объект относится к СЛ>0*-^НАЧЬКЛМ-.. Почему эта книга столь непохожа на другие? «Какая увлекательная и необычная книга! Я много лет преподаю Java и могу честно сказать, что это самый захватывающий ресурс по обучению программированию, с которым я когда- либо сталкивалась. Он вдохновляет меня снова погрузиться в изучение Java». Энджи Джонс, Java Champion «Технология Java сегодня повсюду. Если вы занимаетесь разработкой ПО и еще не изучили Java, пришло время начинать — и начинать именно с Head First!» Скотт Макнили, бывший председатель совета директоров, президент и CEO Sun Microsystems Мы уверены, что ваше время слишком ценно, чтобы тратить его на попытки понять сложные концепции. Книга основана на самых последних достижениях в теории обучения и восприятия. В отличие от скучных текстовых книг, она задействует в процессе обучения сразу несколько органов чувств, сделав усвоение материала максимально эффективным.