Text
                    Евгении Маслов

КОФЕ

БАГИ

DEPLOY

JAVA

JAVA

I git coeit -a
"Усилие mar I *

1 gn pah origin »»tir
Грооеик адие«п

br«kthr<Mh0

jkjtwirrt
private Dao dao;

fuoilc void awendt) (
uo persbtO;

^r'la« DaoSeolce (

(I*°- tit»»

[ISHJ (Ьгкмииция.

11*0] T«rw: 10QX

11*01 tocTjm noa,^

КОД - МОИ МЕЧ. ЛОГИКА - МОЯ ЦИ.
JAVA - МОЙ ПУТЬ К БЕССМЕРТИЮ

private ян *«*
₽Г1*«е uir ?

₽U0Uc eritt

(tn, ><H

JAVA

» Секта Java Core

КУЛЬТОВА и,иц
ПРОГРАММИСТА,

-« СТАНОВЛЕНИЕ ИМПЕРАТОРОМ 0-

Культивация программиста Становление императором Java Секта Java Core
Содержание Пролог. Мир культиваторов...............................................7 Глава 1 Пробуждение корня духа JDK.....................................11 1.1. Корень духа.....................................................11 1.2. Малое испытание.................................................13 1.3. Техника Первого Голоса..........................................13 1.4. Повреждённые свитки внешних учеников............................15 1.5. Испытания внешнего ученика......................................16 1.6 Барьер прорыва: Первый голос.....................................17 Глава 2. Сосуды ци.....................................................20 2.1. Переменная как сосуд ци.........................................20 2.2. Тип как форма сосуда............................................21 2.3. Основные формы сосудов..........................................22 2.4 Присваивание: вложить ци в сосуд.................................25 2.5. Арифметические операции.........................................25 2.6. Конкатенация строк..............................................27 2.7. Профиль внешнего ученика........................................28 2.8. Имена сосудов: naming conventions...............................30 2.9. Записки старейшины..............................................31 2.10. Испытания главы................................................32 2.11 Турнирный зачёт. Первый рейтинг.................................34 Глава 3. Печати истины и выбор тактики.................................36 3.1. true и false: печати истины.....................................36 3.2. Операторы сравнения.............................................37 3.3. Важная ловушка: сравнение строк.................................38 3.4. Логические операторы: &&, ||,!..................................39 3.5. Выбор действия..................................................40 3.6 Проверка повышения ранга.........................................42 3.7. Вложенные условия...............................................43 3.8. Типичные ошибки в условиях......................................44 3.9. Выбор тактики...................................................45 3.10. Повреждённый свиток. Подмена тактики...........................50 3.11. Записки старейшины.............................................51 3.12. Испытания главы................................................52 3.13. Турнирный зачёт. Первая тактика................................55
Глава 4. Серия повторов.................................................59 4.1. Зачем нужны циклы................................................60 4.2. Цикл lor.........................................................61 4.3. Цикл while.......................................................63 4.4. Цикл do while....................................................65 4.5. Дополнительные условия во время цикла............................66 4.6. Вложенные циклы..................................................67 4.7. Практика: подготовка к внутреннему состязанию....................68 Глава 5. Боевые приёмы и вложенная энергия..............................74 5.1. Зачем нужны методы...............................................75 5.2. Параметры метода............................................... 76 5.3. Что метод может вернуть?.........................................79 5.4. Область видимости................................................81 5.5. Арена Чистого Кода...............................................82 5.6. Испытания главы..................................................85 Глава 6. Духовная форма.................................................92 6.1. Почему отдельных сосудов уже мало................................92 6.2. Первый класс Cultivator..........................................93 6.3. Методы объекта...................................................95 6.4. Конструктор......................................................97 6.5. Отличие класса от объекта........................................99 6.6. Испытания главы.................................................100 Глава 7. Защита состояния..............................................102 7.1. Способы защиты меридиан.........................................103 7.2. Неизменность ци.................................................105 7.3. Безопасная духовная форма.......................................108 7.4. Испытания главы.................................................111 Глава 8. Наследие секты................................................113 8.1. Наследование....................................................114 8.2. Переопределение методов.........................................115 8.3. Абстрактный класс или интерфейс.................................117 8.4 Испытания главы..................................................123 Глава 9 Пространственный мешок.........................................128 9.1. Массив: древняя форма хранения..................................129 9.2. Списки..........................................................131
9.3, Множества......................................................133 9.4. Generics: печать типа..........................................137 9.5. Записки старейшины.............................................140 9.6. Испытания главы................................................140 Глава 10. Ночь сломанного свитка......................................144 10.1. Что такое null................................................144 10.2. Ошибки как часть разработки...................................146 10.3. Свои исключения...............................................149 10.4. Повреждённый свиток...........................................151 10.5. Испытания главы...............................................153 Глава 11. Базовые свитки...............................................156 11.1. Память программы и память секты...............................156 11.2. Работа с файлами..............................................157 11.3. Старый лог саботажа...........................................163 11.4. Полный код примера............................................164 11.5. Записки старейшины............................................166 11.6. Испытания главы...............................................167 Глава 12. Артефакты ученика............................................171 12.1. Пакеты техник.................................................172 12.2. Maven: печь сборки проекта....................................177 12.3. Испытания главы...............................................180 Глава 13. Малые испытания старейшин....................................183 13.1. С чего начать?................................................183 13.2. Написание тестов..............................................184 13.3. Немного о тестах..............................................188 13.4. Как не писать бесполезные тесты...............................190 13.5. Испытания главы...............................................191 Глава 14. Финальное испытание: система секты..........................196 14.1. Требования к Sect Management System...........................196 14.2. Базовая структура проекта.....................................197 14.3. Консольное приложение и Scanner...............................198 14.4. Где должны быть тесты финального проекта......................200 14.5. Финальное испытание: собрать систему..........................200 14.6. Испытания главы...............................................201 14.7. Финал внутреннего состязания..................................203
Глава 15. Врата следующего мира.......................................206 15.1. Что ты уже умеешь..............................................207 15.2. Почему Java Core — это не backend-разработка..................208 15.3. Карта следующих сект...........................................209 15.4. Что повторить перед второй книгой..............................211 15.5. Подготовка проекта ко второй книге............................212 15.6. Повреждённый свиток: ложный прорыв............................214 15.7. Дуэль учеников: быстро прыгнуть или закрепить фундамент.......215 15.8. Испытание старейшины: свиток готовности.......................216 15.9. Турнирный зачёт: финальная проверка основ.....................216 15.10. Барьер прорыва: Врата второй книги...........................217
Пролог. Мир культиваторов Приветствую, путник Не каждый день у ворот нашей секты находят человека без таблички ученика, без артефакта разработки, без духовного пропуска и, судя по твоему взгляду, без малейшего понимания, где оказался Ты медленно поднимаешь голову. Перед тобой — высокая каменная лестница, уходящая в туман. Над ней висят огромные ворота из чёрного железа и белого нефрита. На воротах горит надпись. Секта Java Core По обе стороны стоят ученики в серых мантиях Одни чертят в воздухе светящиеся символы. Другие спорят над раскрытыми свитками Третий, совсем бледный, сидит на ступенях и шепчет: — Я просто забыл точку с запятой, всего одну точку с запятой. . Рядом с ним другой ученик смотрит в пустоту. — А у меня все работало вчера. Эти слова заставили окружающих учеников отступить на шаг. Даже ветер на мгновение стих. Старик в темной мантии посмотрел на тебя с интересом. Его борода была белой, как снег на вершинах, но глаза оставались острыми и живыми. За поясом у него висела деревянная табличка: Наставник Павильона Основ — Вижу н твоих глазах вопрос, — сказал он — “Что вообще происходит'’" Верно9 Ты молчишь. — Значит, угадал. Воля небес иногда приводит к нашим воротам странных людей Иногда талантливых Иногда упрямых. Иногда тех, кто случайно нажал не ту ссылку и теперь не может выйти из терминала Он усмехнулся. — Но раз уж ты здесь, слушай внимательно. Мир, в котором ты оказался, называется ИТ-миром В нём живут обычные смертные и культиваторы Обычные смертные проходят мимо древних артефактов, не понимая их силы Они видят ноутбук — и думают, что это просто железная коробка. Они видят код — и думают, что это бессмысленные символы. Они слышат слово production — и не чувствуют холода в груди
Культиваторы другие Они выбрали путь Дао программирования Они изучают языки, техники, фреймворки, библиотеки, паттерны и древние свитки документации Они сражаются с демонами дедлайнов, багами, неясными требованиями и кодом, который "работал десять лет, не трогай” Путей культивации много. Есть Путь Python — гибкий, быстрый и любимый теми, кто хочет увидеть результат до того, как чай остынет. Есть Путь C++ — древний, мощный и опасный для тех, кто не уважает память. Есть Путь JavaScript — хаотичный, изменчивый и способный работать там, где никто не ожидал, а иногда и там, где никто не просил Есть Путь SQL — путь архивариусов, умеющих говорить с великими библиотеками данных А есть Путь Java Старик указал на ворота за своей спиной. — Наша секта обучает основам этого пути. Здесь не обещают лёгкого бессмертия. Здесь ты будешь ошибаться, переписывать, запускать, падать, снова запускать, снова падать и постепенно понимать, почему старшие ученики иногда просто смотрят и экран и молчат. Ты хотел что то спросить, но i этот момент над внутренним двором секты раздался удар колокола. Один. Второй. Третий. Ученики у ворот зашумели — Уже объявили? — 6 этом году раньше обычного. — Значит Межсектовый Турнир Основ близко Наставник поднял руку, и шум стих. — Раз в год, — сказал он, — великие секты программирования отправляют учеников на Межсектовый Турнир Основ Там Путь Java соревнуется с Путем Python, Путем JavaScript, Путем C++, Путём SQL и другими школами. Победа приносит славу секте, ресурсы ученикам и право изучать более глубокие техники. Он посмотрел на тебя
— Но на турнир не отправляют случайных людей с улицы Сначала ученик должен пройти внутреннее состязание секты. Только лучшие внешние ученики получат право представлять Java Core. Один из учеников в серой мантии гордо выпрямился — В этом году я точно пройду отбор Другой усмехнулся: — Ты в прошлом году не смог отличить = от ==. — Это был стратегический маневр. — Это был compile error. Наставник ударил посохом по камню. — Тишина. Затем он повернулся к тебе. —Ты появился в трудное время. Внутренний отбор скоро начнётся Ученикам предстоит пройти Зал Повтором, Испытание Духовной Формы, Павильон Коллекций и Малые Испытания Старейшин Он сделал паузу — Но есть и другое Ветер прошел по ступеням. На мгновение тебе показалось, что туман за воротами стал темнее — В прошлом году перед турниром несколько свитков секты были повреждены. Код, который должен был работать, падал без причины. Рейтинги учеников сбивались Награды исчезали Один из кандидатов потерял псе очки из-за ошибки, которую никто не смог объяснить. Ученики вокруг замолчали — Некоторые считают, что это был обычный баг, — продолжил наставник. — Другие говорят о саботаже Но хороший культиватор не верит слухам Он читает код, проверяет состояние и ищет причину Он протянул тебе маленькую деревянную табличку. На ней еще не было имени. — Если ты вступишь в Секту Java Core, эта табличка станет твоей. На ней будут отмечаться твои техники, очки секты, барьеры прорыва и статус н отборе На табличке вспыхнули строки: Имя: _________________ Статус: смертный Секта: нет Корень духа: не пробужден
Освоенные техники: нет Очки секты: 0 Барьер прорыва: закрыт Турнирный рейтинг: не участвует — Сначала мы проверим твой корень духа, На языке смертных это означает: сможешь ли ты установить JDK и запустить Java-программу Затем ты повторишь Технику Первого Голоса — заставишь программу произнести первую фразу. Он шагнул к воротам — Если справишься, станешь внешним учеником Если нет — ничего страшного, Все ееликие старейшины когда-то не могли запустить первую программу. Просто не все потом признавались. За воротами открылась каменная дорога. Вдали виднелись павильоны, арены, тренировочные залы и высокая башня, у вершины которой вращались светящиеся символы. — Решай, путник, Вернешься к жизни смертного или сделаешь первый шаг на Путь Java9 Перед тобой открылись ворота. И где-то в глубине секты тихо треснул поврежденный свиток,
Глава 1. Пробуждение корня духа JDK 1.1. Корень духа На рассвете тебя привели на каменную площадку перед главным залом секты. Вокруг уже собрались другие новички. Кто-то выглядел уверенно. Кто-то нервно сжимал старый ноутбук. Один ученик пытался установить что-то уже третий раз подряд и шептал — Почему оно опять просит перезагрузку9. Я уже перерождался меньше раз На площадку вышел Наставник Павильона Основ Сегодня его тёмная мантия была перехвачена поясом с бронзовой пластиной На пластине были выгравированы три символа. JDK -> javac 3VM Наставник окинул новичков взглядом — Приветствую всех, кто решил вступить на Путь Java. Сразу предупреждаю, обучение будет не таким легким, как обещают мотивационные ролики. Здесь будут ошибки, непонятные сообщения компилятора, пропущенные скобки, странные названия переменных и моменты, когда вы будете думать, что компьютер вас ненавидит. Он сделал паузу. — Компьютер вас не ненавидит Он просто выполняет ровно то. что вы написали В этом и ужас. Несколько учеников переглянулись — Сегодня у вас вступительное испытание, Оно состоит из двух частей Первая — пробудить корень духа. Вторая —- повторить Технику Первого Голоса Ты поднял руку. — Что такое корень духа? Наставник кивнул, словно ждал этого вопроса. — В мире Java корнем духа называют основу, без которой ты не сможешь применять техники Java На языке смертных тебе нужен JDK — Java Development Kit Он начертил в воздухе три светящихся знака. 3DK -> javac -» 3VM — Запомни простую мысль. Ты пишешь код в файле .java. Но сам по себе этот код ещё не является техникой, готовой к исполнению Его нужно преобразовать. Перед настаеником появился светящийся свиток.
public class Hello { public static void main(String[] args) { System.out.printin("Hello Cultivation World"); } ) — Это руны Java Так выглядит исходный код Но мир исполнения не читает его напрямую Сначала код проходит через компилятор Он провел рукой, и рядом возникла надпись. javac Hell о.java — javac — это инструмент из JDK Его можно представить как алхимическую печь. Он берет твой Java-код и переплавляет его в bytecode — байткод. В воздухе появился новый свиток’ Hello.class — После компиляции появляется файл .class Внутри него уже не обычный текст Java, а байткод Именно его выполняет JVM — Java Virtual Machine. Наставник посмотрел на учеников. — JVM запускает Java-программы Благодаря ей одна и та же программа может работать в разных мирах: Windows, Linux, macOS Конечно, если в этом мире установлен подходящий корень духа. Один новичок поднял руку. — А что тогда такое JDK полностью? — JDK — это полный набор начинающего культиватора Java. В нём есть JVM, компилятор javac, стандартные библиотеки и другие инструменты. Для первого испытания тебе не нужно знать всё. Достаточно понять цепочку. Наставник начертил на камне: Ты пишешь Hei]о.java 1 javac превращает код в Hello.class 1 1VM запускает Hello.class 1 Программа г сп орит с миром — Если эта цепочка сработала, значит твой корень духа пробуждён. Сухая истина Java JDK — это набор инструментов для разработки на Java. В начале тебе важно различать три вещи Название Что делает
Название Что делает J:. К Набор инструментов для разработки javac Компилятор, который превращает .jave в .class JVM Зиртуальная машина, которая запускает байткод Ты пишешь код в файле .java После компиляции появляется файл .class. JVM выполняет этот .class файл Первый артефакт ученика Наставник указал на длинный стол, где лежали разные артефакты старые терминалы, светящиеся редакторы, тяжелые IDE и тонкие свитки с настройками. — Код можно писать почти где угодно Даже в обычном блокноте Но внешний ученик, который начинает путь с блокнота, обычно быстро познает страдание. Он взял один из артефактов. — Я советую использовать IntelliJ IDEA. Это мощный артефакт разработки. Он помогает писать код, запускать программы, видеть ошибки и не теряться в первых шагах Но ты можешь использовать Eclipse, VS Code. NetBeans или терминал Важен не артефакт сам по себе, а то, понимаешь ли ты, что делаешь Он хлопнул в ладони. — Первое задание установи JDK и подготовь место, где будешь писать руны На каменной стене вспыхнуло задание. 1.2. Малое испытание Самостоятельная работа, установи JDK. Чтобы проверить что он установлен в терминале можно выполнить- java -version и javac -version Если обе команды отвечают версией Java, корень духа готов к пробуждению Если команда javac не найдена, значит установлен не полный JDK или путь к нему не настроен. 1.3. Техника Первого Голоса Когда ученики закончили подготовку, наставник снова вышел на середину площадки.
— Теперь ты повторишь первую технику. Она не разрушает горы Не призывает молнии. Не открывает врата бессмертия Она просто заставляет программу произнести фразу Он поднял свиток. — Но для внешнего ученика даже это уже первый шаг Создай файл: Hello.java И помести него код public class Hello { public static void main(String[] args) ( System.out.printin("Hello Cultivation World"); 1 } Наставник сразу поднял палец. — Сейчас тебе не нужно понимать каждое слово Не пытайся победить всю гору одним ударом. Сегодня тебе важно увидеть, что программа запускается Он указал на первую строку: public class Hello { — Это объявление класса Пока думай о нём как о свитке, в котором хранится техника. Затем указал на вторую: public static void main(String[] args) { — Это точка входа. Место, с которого программа начинает выполнение. Позже мы разберем каждое слово. Пока запомни без main твоя программа не поймет, откуда начинать. Потом наставник указал на третью строку System.out.printin("Hello Cultivation World"); — А вот это и есть Техника Первого Голоса Она выводит текст на экран. Он улыбнулся — Если увидишь фразу Hello Cultivation World, значит техника сработала. Сухая истина Java Минимальная Java-программа выглядит так: public class Hello ( public static void main(String[] args) { System.out.printin("Hello Cultivation World");
Пока запомни три вещи 1 . Имя публичного класса должно совпадать с именем файла 2 Метод main — точка входа в программу. 3 System.out. printing ..) выводит текст на экран. 1.4. Повреждённые свитки внешних учеников Наставник прошёл между рядами — Если у тебя не получилось с первого раза, это нормально Первый демон, с которым сталкивается ученик Java, редко бывает великим Чаще всего это забытая скобка, неправильное имя файла или точка с запятой, сбежавшая в закат. На стене появились поврежденные свитки Повреждённый свиток 1. Имя файла не совпадает с именем класса Если внутри написано: public class Hello { то файл должен называться Hello.java Не hello.java, не Hello.txt, не MyProgram.java. Java строга к именам Небесные законы не принимают “почти правильно”. Повреждённый свиток 2. Потеряна точка с запятой Правильно: System.out.printin("Hello Cultivation World"); Неправильно. System.out.println("Hello Cultivation World") Точка с запятой завершает команду. Без неё компилятор смотрит на твой код и не понимает, где заканчивается мысль Повреждённый свиток 3. Неправильные кавычки Правильно: "Hello Cultivation World" Неправильно “Hello Cultivation World”
В Java нужны обычные двойные кавычки. Красивые кавычки из текстовых редакторов могут сломать код. Повреждённый свиток 4. Запуск не из той пещеры Если терминал открыт не там. где лежит Hello, java, команда не найдёт файл. Наставник вздохнул. — Многие ученики думают, что проиграли демону компиляции. На деле они просто стояли не в той пещере 1.5. Испытания внешнего ученика Если твоя программа вывела приветствие, ты прошёл основную часть испытания Но настоящий ученик не останавливается на одном запуске. Испытание 1. Измени голос техники Поменяй строку вывода на Привет, Секта Java Core! Запусти программу снова Награда. +3 очка секты Испытание 2. Назови себя Выведи свое имя. Пример- меня зовут Путник. Награда' +3 очка секты. Испытание 3. Три строки силы Сделай так, чтобы программа вывела три строки: Я вступил в Секту Java Core. Мой корень духа пробуждён. Путь культивации начинается. Награда: +4 очка секты Испытание старейшины. Повреждённый голос Перед тобой повреждённый код: public class FirstVoice { public static void main(String[] args) { System .out. print in ("IlyTi начинается") ) ) Найди ошибку и исправь код. Подсказка наставника: — Иногда техника почти верна Но “почти" не проходит через компилятор
Награда +5 очков секты. 1.6. Барьер прорыва: Первый голос Когда последний ученик закончил испытание, наставник ударил посохом по камню. Перед тобой вспыхнула деревянная табличка Имя: Путник Статус: внешний ученик Секта: Java Core Корень духа: пробужден Первая техника: освоена Очки секты: +15 Барьер прорыва: Первый голос открыт Турнирный рейтинг: кандидат без ранга Наставник посмотрел на тебя уже иначе — Поздравляю Теперь ты не просто смертный, который случайно оказался у ворот Ты внешний ученик Секты Java Core. Ученики вокруг зашумели. Кто то радовался. Кто-то всё ещё спорил с терминалом Кто-то пытался запустить файл Hello.java.txt и отказывался признать поражение В этот момент к площадке подошел высокий ученик с аккуратно завязанными волосами и серебряной табличкой на поясе. Он бросил быстрый взгляд на новичков — Это все кандидаты на внутреннее состязание7 Наставник кивнул. — Пока да Ученик усмехнулся — В прошлом году хотя бы половина умела запускать программу без помощи старейшин. Он повернулся и ушел к верхним павильонам — Кто это? — спросил кто-то из новичков — Линь Грейдл. — ответил наставник. — Один из сильнейших внешних учеников Быстро учится, быстро пишет код. быстро раздражает окружающих. Он повернулся к тебе — Не обращай внимания Внутреннее состязание показывает не только скорость Оно покажет, кто действительно понимает свои техники Наставник указал на лестницу, ведущую выше в гору
— Завтра начнётся настоящая закалка. Ты узнаешь, что такое ци данных, как вкладывать её в сосуды переменных и почему сосуд неправильной формы не примет чужую силу Он сделал паузу. — А пока запомни главное если программа сказала миру первую фразу, значит мир уже услышал тебя Брата секты закрылись за твоей спиной. Путь Java начался Летопись Павильона Основ Ночью, когда внешний двор опустел, в главном зале секты зажглись семь старых светильников. Говорили, что каждый из них был создан во времена Первого Компилятора — древнего мастера, который первым понял сила кода не в том. чтобы заставить машину повиноваться, а в том. чтобы ясно выразить намерение На стене главного зала висела старая табличка. Буквы на ней потускнели, но ученики всё равно читали её перед каждым внутренним состязанием: Код. который нельзя прочитать, — это закрытый свиток. Код. который нельзя запустить. — это мёртвый свиток. Код который нельзя изменить без страха, — это проклятый свиток Наставник Павильона Основ долго смотрел на эту табличку Позади него тихо открылась дверь В зал зошла Мэй Рефактор В руках она держала список новых учеников — Среди них есть сильные1? — спросила она Наставник не сразу ответил. — Сильные появятся позже Сегодня среди них есть только упрямые — Этого достаточно? Старик усмехнулся — Для первого прорыва — да Для Межсектового Турнира — нет За стеной главного зала на мгновение мигнула турнирная доска. Внизу, почти незаметно, возникла строка unknown_student. access denied Через миг она исчезла
Мэй Рефактор нахмурилась. — Опять9 Настазник погасил один из светильников. — Пока это только тень Но тень появляется лишь там. где уже есть источник Он повернулся к окну. Вдалеке спали павильоны секты, а над ними медленно вращались символы Java Core. — Пусть новички учатся. Скоро им понадобится не только запускать код. Им придётся понять, почему он ломается.
Глава 2. Сосуды ци 2.1. Переменная как сосуд ци На следующее утро тебя разбудил низкий удар колокола. Внешние ученики Секты Java Core собрались чо внутреннем дворе Вчера ты впервые заставил программу говорить с миром Сегодня наставник обещал показать, как этот мир хранит силу. но дворе стояли каменные столы На каждом лежали сосуды маленькая чаша, высокий кувшин, стеклянная колба, деревянная табличка и запечатанная коробка На дальнем краю площадки висела большая доска рейтинга Пока на ней было всего несколько строк. внутреннее состязание секты Статус: подготовка Участники: внешние ученики Первый зачёт: через 3 занятия Новички переговаривались — Значит, нас правда будут сравнивать? — Конечно. Как еще выбрать тех, кто поедет на Межсектоный Турнир? — А если я не хочу соревноваться? Наставник появился бесшумно, но все сразу замолчали. — Состязание — это не только победа над другими В первую очередь это проверка, стал ли ты сильнее вчерашнего себя Он поставил на каменную плиту маленькую чашу. — Вчера вы произнесли Технику Первого Голоса Сегодня вы узнаете, откуда техника берет данные. Он поднял чашу — Любая программа работает не с пустотой Она хранит числа, имена, состояния, результаты, награды, ошибки и решения В мире Java такие хранилища называются переменными Он поставил чашу перед тобой. — В нашем мире мы называем их сосудами ци Переменная — это место, где программа хранит значение Например int level = 1;
Наставник провел пальцем по строке — Здесь level — это сосуд. В него вложили значение 1. Но сосуд не может быть каким угодно У каждого сосуда есть форма Разберем строку: int level = 1; Часть Что значит int тип данных, форма сосуда level имя переменной, имя сосуда = присваивание вложение значения 1 значение, ци внутри сосуда ; конец команды Наставник посмотрел на учеников. — Запомните, знак = в Java — это не "равно” в математическом смысле Это приказ, положить значение справа в переменную слева. level = 2; — Эта строка означает: теперь в сосуде level лежит значение 2. Сухая истина Java Переменная хранит значение Общий вид объявления переменной: тип имя = значение, Пример int sectPoints = 10; Здесь создается переменная sectPoints типа int, и в неё записывается значение 10. 2.2. Тип как форма сосуда Наставник взял несколько сосудов разной формы — В числовой сосуд можно вложить число В текстовый — текст. В сосуд истины — только true или false Если попытаться вложить не ту ци. законы Java остановят тебя ещё до запуска техники. Так можно: int level = 1; String name = "Путник"; boolean active = true;
А так нельзя int level = "первый"; Почему? Потому что int хранит целые числа, а "первый" — это текст. Компилятор не позволит смешивать формы сосудон С дальнего края площадки донесся голос Линь Грейдла. — Если очень захотеть, можно все, Наставник даже не повернулся — Если очень захотеть, можно получить очень длинное сообщение об ошибке 2.3. Основные формы сосудов На первом этапе тебе нужны несколько базовых типсс Тип Что хранит Пример int целое число int level = 1; long большое целое число long totalPoints = 1000000L; double число с дробной частью double rewardMultiplier =1.5; boolean истина или ложь boolean active = true; char один символ char rankMark = 'A'; String текст String name = "Путник"; Наставник поднял палец. — Обрати внимание: char пишется в одинарных кавычках, a String — в двойных. char grade = 'А'; String title = "Внешний ученик"; Неправильно char grade = "А"; String title = 'Внешний ученик'; — Один символ — одинарные кавычки. Текст — двойные Нарушишь это правило — сосуд треснет еще до начала ритуала Числовые сосуды: int, long, double int — обычный сосуд для целых чисел. int level = 1; int sectPoints = 10; int completedQuests = 3;
Его хватит для большинства простых задач: уровни, количество очков счётчики, количество предметов long нужен для очень больших целых чисел long totalSectEnergy = 90С-ЭОО'jOOOL; Буква L в конце показывает, что это именно long. Наставник усмехнулся. — Если у твоего внешнего ученика уже девять миллиардов очков секты, возможно, ты ошибся не в типе данных, а в балансе игры double хранит числа с дробной частью double rewardMultiplier = 1.5; double successChar.ce = 87.5; Он полезен, когда нужно работать с процентами, коэффициентами, множителями и дробными значениями. Печать истины внутри сосуда: boolean boolean хранит только два значения true false Например boolean active = true; boolean passedTxial = false; boolean hasSpiritualRoot = true; Наставник сказал- — Это сосуд истины. В него нельзя вложить "почти да”, "скорее всего”, ‘вроде работает” или "на моей машине запускалось". В Java boolean строгий. boolean active = true; Или boolean active = false; Символ и строка: char и String char хранит один символ:
char rankMark = 'C; String хранит текст. String name = "Путник"; String rank = "Внешний ученик"; Нажно char symbol = 'J'; String word = "Java"; Один символ — char Набор символов — string. Класс как будущая форма сосуда Наставник взял запечатанную коробку. — До сих пор ты видел простые формы сосудов: числа, истина, символы, строки Нс в Java есть более сильная идея: класс тоже может быть формой сосуда Пока ты уже видел класс. public class Hell- { public static void main(String[] args) { System.out.println("Hello Cultivation World"); } ) Сейчас класс Hello был просто свитком, внутри которого лежала техника запуска Но позже ты сможешь создавать собственные формы. Например, не просто хранить имя. уровень и очки отдельно String name = "Путник"; int level = 1; int sectPoints = 10; boolean active = true; А создать отдельную форму ученика class Cultivator { String name; int level; int sectPoints; boolean active; } Тогда переменная сможет хранить не просто число или строку, а целого ученика Cultivator student; Наставник закрыл коробку
— Но не спеши Классы и объекты — это отдельный зал обучения. Пока запомни главное тип данных — это форма сосуда И класс тоже может стать такой формой 2.4. Присваивание: вложить ци в сосуд Присваивание — это помещение значения в переменную int sectPoints = 1 ; Можно изменить значение sectPoints = 20; Можно использовать старое значение, чтобы получить новое: sectPoints = sectPoints + ; Это значит 1 взять текущее значение sectPoints; 2 прибавить 5; 3 . положить результат обратно в sectPoints. Если было int sectPoints = 10; sectPoints = sectPoints + 5; то станет 15. Есть короткая запись: sectPoints +- 5; Это то же самое, что sectPoints = sectPoints + 5; Для уменьшения: sectPoints -= 3; Для умножения sectPoints *= 2; Для деления sectPoints /= 2; 2.5. Арифметические операции Java умеет выполнять обычные вычисления. Оператор Значение Пример + сложение 10 + 5 - вычитание 10 - 5 ♦ умножение 10 * 5 / деление 10/5 % остаток от деления 10 % 3
Пример int baseRewaid = 10; int bonusReward = 5; int totalRewara = baseReward + bonusReward; System.out.printIn(totalReward); Результат: 15 Пример с потерей очков int sectPoints = 20; int penalty = 7; sectPoints = sectPoints - penalty; System.out.printIn(sectPoints); Результат 13 Наставник строго посмотрел на учеников. — Потеря очков за провал задания — обычное дело Потеря всех, очко^ из-за неправильной формулы — уже повод для внутреннего расследования секты. Остаток от деления Оператор % показывает остаток от деления int result = 10 % 3; System.out.printin(result) ; Результат: 1 Почему? Потому что 10 / з — это з и остаток 1. Это может пригодиться, например, если нужно определить, делится ли число нацело int day = 6; System.out.princln(day % 2); Если результат 0, значит число чётное. Наставник сказал: — В будущих испытаниях остаток от деления поможет тебе чередовать дни тренировок, выдавать награды через каждые несколько уровней и находить нарушенный ритм в циклах Важная ловушка: деление целых чисел Наставник нарисовал на камне.
int result = 5/2; System.3ut.printin(result) ; — Какой будет результат9 Один ученик сказал — 2.5. Наставник покачал головой. — Нет. Если делишь int на int, Java вернёт целое число Результат будет 2 Дробная часть отбросится. Чтобы получить дробный результат, нужен double: double result = 5.0 I 2; System.out.printin(result); Результат: 2.5 Линь Грейдл тихо сказал — Кто не знает этой ловушки, тот проиграет на расчете рейтинга Наставник посмотрел на него. — Кто слишком много говорит во время урока, тот будет считать рейтинг вручную Линь Грейдл замолчал. 2.6. Конкатенация строк Если оператор + используется с числами, он складывает. int points = 10 + 5; System.out.println(points), Результат: 15 Но если + используется co строками, он соединяет текст. String name = "Путник"; String message = "Приьет, " •+ name; System.out.printin(message); Результат; Привет, Путник Можно соединять строки и числа String name = "Путник"; int level = 1; int sectPoints = 10;
System.out.println("HMH: " + name); System.out.printin("ypi н^нь: " + level); System.out.printin("Очки секты: " + sectPoints); Результат Имя: Путник Уровень; 1 Очки секты: 10 Наставник улыбнулся — Так программа начинает не просто говорить, а сообщать состояние мира. 2.7. Профиль внешнего ученика Теперь соберем первые сосуды вместе Создай файл: StudentProfile. java И напиши код public class Studentprofile { public static void main(String[] args) { String name = "Путник"; int level = 1; int sectPoints = 10; int energy = 50; boolean active = true; System.out.printin ("Про^-ил-э ученика"); System.out.printin("Имя: " + name); System.out.println("Уровень: " + level); System.out.printin("Очки секты: " + sectPoints); System.out.println("Энергии: " + energy); System.out.printin("Активен: " + active); } } Запусти программу. Результат должен быть примерно таким Профиль ученика Имя: Путник Уровень: 1 Очки секты: 10 Энергия: 50 Активен: true Наставник кивнул. — Теперь твоя программа хранит состояние ученика. Это уже не просто голос. Это первая запись о 1воём пути. Допустим, ученик выполнил задание и получил награду.
int sectPoints = 10; int questReward = 15; sectPoints = sectPoints + questReward; Если было 10 очков, а награда 15, станет 25. Можно записать короче: sectPoints += questReward; Если ученик провалил испытание, он может потерять часть очков int failPenalty = ‘ ; sectPoints -= failPenalty; Наставник заметил. — Потерять пять очков неприятно Потерять проект из-за отсутствия сохранения — больнее Но это испытание ждет тебя позже Очки секты можно тратить. Например, ученик покупает пилюлю концентрации за 8 очкон int sectPoints = 20; int concentrationPillCost = E; sectPoints -= concentrationPillCost; System.out.println("Осталось очков: " + sectPoints); Результат: Осталось очков: 12 Можно вывести красивее String resourceName = "Пилюля концентрации"; int resourceCost = ; int sectPoints = 20; sectPoints -= resourceCost; System.out.println("Куплен ресурс: " + resourceName); System.out.println("Стоимость: " + resourceCost); System.out.println("Осталось очков секты: " + sectPoints); Теперь сделаем награду не фиксированной, а вычисляемой. Допустим: • базовая награда —10; • сложность задания — з; • множитель сложности — 5; • бонус за первое выполнение — 7 Формула итоговая награда = базовая награда + сложность * множитель + бонус
Код. public class QuestReward { public static void main (String[] arqs) ( int baseReward = 10; int difficulty = 3; int difficultyMultiplier = 5; int firstCompletionBonus = 7; int totalReward = baseReward + difficulty * difficultyMultiplier + firstCompletionBunus; System.out.printin("Базе (ая награда: " + baseReward); System.out.printin("Сложность задания: " + difficulty); System.out.printin("Бонус первого выполнения: " + firstCompletionBonus); System.out.printin("Итогогая награда: " + totalReward); } ) Результат: Базовая награда: 10 Сложность задания: 3 Бонус первого выполнения: 7 Итоговая награда: 32 Почему 32? 10 +3*5+7= 32 Наставник постучал посохом по камню — Java соблюдает порядок операций. Сначала умножение, потом сложение. Небесные законы математики работают даже в секте программирования Если хочешь изменить порядок, используй скобки: int totalReward = (baseReward + difficulty) * difficultyMultiplier + firstCompletionBonus; 2.8. Имена сосудов: naming conventions Наставник подошёл к ученику, который написал int х = 1 ; int у = 3; int z = х + у; — Что ЭТО? — Код, наставник. — Это туман Через неделю ты сам не вспомнишь, что такое х, у и z. В Java имена переменных и методов обычно пишут в стиле camelcase.
int sectPoints = 10; int questReward = 15; boolean activestudent = true; String studentName = "Путник"; Классы пишут в стиле PascalCase public class StudentProfile { } Константы часто пишут большими буквами через подчеркивание final int MAX_LEVEL = 100; Плохие имена int а = 10; int b = 5; int temp = 15; String data = "Путник"; Лучше int sectPoints = 10; int failPenalty = ! ; int totalRewaid = 15; String stuaentName = "Путник"; Запомни простое правило Имя переменной должно объяснять, что в ней хранится. Если для понимания переменной нужен комментарий, возможно, переменную назвали плохо 2.9. Записки старейшины 1. Не называй всё data Плохо. String data = "Путник"; Лучше String studentName = "Путник"; 2. Не храни числа как текст Плохо String level = "1"; Лучше int level = 1;
Если значение нужно считать, сравнивать, увеличивать или уменьшать — скорее всего, это должно быть число, а не строка 3. Не бойся длинных имён Плохо int р = 10; Лучше. int sectPoints = 10; Код читают чаще, чем пишут Пусть имя помогает понимать смысл 4. Не путай = и “рално” sectPoints = sectPoints + 10; Это не математическое равенство. Это команда, возьми старые очки, прибавь 10 и положи результат обратно. 2.10. Испытания главы Испытание 1. Профиль ученика Создай программу StudentProfile. java. В ней должны быть переменные: String stjdentName; int level; int sectPoints; boolean active; Заполни их значениями и выведи на экран. Пример результата: Имя: Путник Уровень: 1 Очки секты: 10 Активен: true Награда: +5 очков секты Испытание 2. Выполнение задания Добавь переменную int questReward = 15; Прибавь награду к очкам ученика и выведи результат. Пример
Очки после задания: 25 Награда; +7 очков секты. Испытание 3. Провал испытания Добавь штраф int failPenalty = 5; Уменьши очки ученика и выведи результат. Награда: +7 очков секты Испытание 4. Обмен очков на ресурс Создай ресурс. String resourceName = "Пилюля концентрации"; int resourcecost = 8; Уменьши очки ученика на стоимость ресурса и выведи: Куплен ресурс: Пилюля концентрации Осталось очков: ... Награда. +10 очков секты. Испытание 5. Расчёт награды Создай переменные int baseReward = 10; .nt difficulty = 3; int difficultyMultiplier = 5; int bonus = 7; Посчитай итоговую награду. .nt totalReward = baseReward + difficulty * difficultyMultiplier + bonus; Выведи результат. Награда: +10 очков секты. Испытание 6. Исправь плохие имена Был код: String а = "Путник"; int Ь=1;
int с = 10; boolean d = true; Переименуй переменные так. чтобы было понятно, что они хранят. Награда: +5 очкор секты 2.11. Турнирный зачёт. Первый рейтинг Наставник открыл большую доску. — Сегодня вы впервые рассчитаете собственные очки, Пока вручную, через переменные Позже мы научим программу хранить список всех учеников. Создай программу FirstRating. java В ней должны быть. String studentName = "Путник"; int startPcxnts = 15; int chapterR-=ward = 44; int penalty = 5; int totalPoints = startpoints + chapterReward - penalty; Выведи. Ученик: Путник Итоговые очки: 54 Если получилось — добавь себе +10 турнирных очков. К концу занятия ты освоил первые сосуды ци. Теперь ты знаешь: • переменная хранит значение; • тип данных определяет форму переменной; • int, long, double, boolean, char, string нужны для разных видов данных; • значение можно присваивать через =; • числа можно складывать, вычитать, умножать и делить, • строки можно соединять через +; • имена переменных должны объяснять смысл; • класс тоже может быть формой сосуда, но подробно ты изучишь это позже. Перед уходом наставник снова взглянул на твою табличку ученика Имя: Путник Статус: внешний ученик Корень духа: пробужден Техника Первого Голоса: освоена Сосуды ци: открыты
Очки секты: +59 Барьер прорыва: Сосуды ци открыт Турнирный рейтинг: первичный зачет получен — Теперь ты умеешь не только говорить с миром, — сказал он. — Ты умеешь хранить в нем данные А значит, твои техники больше не будут пустым криком в тумане. На доске рейтинга вспыхнули первые имена Некоторые ученики радостно переговаривались, Некоторые спорили, почему штраф тоже нужно учитывать В самом низу доски на мгновение появилась странная строка: unknown_student: -999 очков Она исчезла так быстро, что большинство учеников ничего не заметили Но наставник заметил. Его взгляд стал тяжелым. — Интересно, — тихо произнес он Затем он повернулся к следующему залу — Завтра ты узнаешь, что не всякая сила должна применяться сразу Иногда путь зависит от условия Иногда техника срабатывает только при верной печати истины
Глава 3. Печати истины и выбор тактики На третий день тебя привели в другой двор Павильона Основ Вчера ты учился хранить ци в сосудах имя, уровень, очки секты, состояние ученика Теперь твоя программа уже могла помнить простые данные Но наставник сразу сказал: — Хранить силу мало, Нужно понимать, когда её применять Перед учениками стояли четыре каменные двери На первой было написано: Тренировка На второй: Отдых На третьей Бой На четвёртой: Медитация Наставник повернулся к ученикам — Перед культиватором постоянно стоит выбор Тренироваться или восстанавливаться Вступить в бой или отступить. Повысить ранг или сначала накопить больше очков В Java такие решения принимаются через условия. А основа условий — это печати истины. Над двором вспыхнула турнирная доска: Внутреннее состязание секты Этап подготовки: выбор тактики До первого отбора: 2 занятия Линь Грейдл стоял у первой двери и уверенно смотрел на остальных. — Слабые выбирают отдых Сильные тренируются. Наставник даже не повернулся. — Глупые тренируются с нулевой энергией и потом падают на первом цикле Некоторые ученики тихо засмеялись. 3.1. true и false: печати истины В Java есть тип: boolean Он хранит только два значения true false То есть:
boolean active = true; boolean hasEn-.ugnPoints = false; boolean passedTrial = true; Наставник поднял две таблички. На одной было написано: true На другой: false — Печать истины не знает сомнений. Она не принимает "возможно", “скорее всего", ' вроде бы”, “потом проверим” или “на моей машине работает”. В Java условие либо истинно, либо ложно. boolean active = true; System.out.println(active); Результат: true Сухая истина Java boolean используется там, где нужен ответ "да" или "нет”. Примеры- boolean active = true; boolean blocked = false; boolean canRankUp = true; Значение true означает “истина”. Значение false означает “ложь”. 3.2. Операторы сравнения Печати истины часто появляются в результате сравнения Например int sectPoints = 120; boolean canRankUp = sectPoints >= 100; System out println(canRankl)p); Результат: true Почему? Потому что 120 >= 100 Основные операторы сравнения: Оператор Значение Пример > больше sectPoints > 100 < меньше sectPoints < 100 >= больше или равно sectPoints >= 100 <- меньше или равно sectPoints <= 100
Оператор Значение == равно != неравно Пример. int level = 1; int sectPoints = 89; Пример level == 1 level != 1 System.out.printin(level == 1); System.out.princln(sectPoints >= 100); System.out.printin(sectPoints != 0); Результат: true false true Наставник указал на ==. — Запомни для сравнения используется двойное равно ==. Один знак = — это присваивание. Плохо int level = 1; if (level = 2) { System.out.printin("Уровень 2"); ) Так нельзя, level = 2 пытается присвоить значение, а не сравнить Правильно int level = 1; if (level == 2) { System.out.printin ("Уоог ‘-нъ 2") ; ) 3.3. Важная ловушка: сравнение строк Наставник написал- string rank = "внешний ученик"; — Со строками нужно быть осторожнее Для строк не стоит использовать ==, если ты хочешь сравнить текст. Плохо. String rank = "'внешний ученик"; if (rank == "Внешний ученик") (
System.cut.printin("Ранг найден"); } Иногда это может сработать, но полагаться на это нельзя. Правильно String rank = "Внешний ученик"; if (rank.equals("Внешний ученик")) { System.cut.printin("Ранг найден"); ) equals сравнивает содержимое строки. Наставник вздохнул — Многие ученики проигрывали демону строк не потому, что были слабы. Они просто думали, что == всегда означает 'одинаковый текст”. Ещё безопаснее, если переменная может быть null: if ("Внешний ученик".equals(rank)) { System.out.printin("Ранг найден"); Почему так безопаснее — подробно разберем позже, когда дойдем до искажений ци и NullPointerException 3.4. Логические операторы: &&, ||, ! Иногда одного условия недостаточно. Например, ученик может повысить ранг, если * он активен; • у него хватает очков; • он прошёл испытание Для этого нужны логические операторы. Оператор Значение Как работает && И оба условия должны быть истинны II ИЛИ достаточно одного истинного условия I НЕ переворачивает значение &&: оба условия должны быть истинны int sectPoints = 120; boolean active = true; boolean canRankUp = sectPoints >= 100 active; System.out.printin(canRankUp);
Результат: true Потому что sectPoints >= 100 -* true active -» true true && true -» true Если хотя бы одно условие ложно int sectPoints = 8G; boolean active = true; boolean canRankUp = sectPoints >= 100 && active; System.out.println(canRankUp); Результат: false ||: достаточно одного условия boolean hasSectPass = false; boolean isElderlnvited = true; boolean canEnterHall = hasSectPass I I isElderlnvited; System.out.println(canEnterHall); Результат: true Потому что хотя бы одно условие истинно. !: отрицание boolean tired = true; System.out.println(!tired); Результат false ! tired означает "не устал". Пример boolean tired = false; if (Itired) { System.out.println("Можно тренироваться"); ) Если tired равен false, то 'tired равен true. 3.5. Выбор деиствия if: по условию Наставник указал на первую каменную дверь
— Если у ученика хватает очков, он может пройти к повышению ранга. В Java это выглядит гак int sectPoints = 120; if (sectPoints >= 100) { System.out.printin("Очков хватает. Можно повысить ранг."); } Если условие истинно, код внутри фигурных скобок выполнится. Если условие ложно, код будет пропущен. int sectPoints = 8 ; if (sectPoints >= 100) { System.out.printin("Очков хватает. Можно повысить ранг."); ) В этом случае программа ничего не выведет, потому что 80 >= 100 - это false. else: что делать, если условие не выполнено Если нужно обработать оба варианта, используется else. int sectPoints = ; if (sectPoints >= 100) { System.out.printin("Очк< хватает. Можно повысить ранг."); } else { System.out.printin("Очков недостаточно. Продолжай тренировки."); Результат: Очков недостаточно. Продолжай тренировки. Наставник сказал: — if отвечает за путь, если печать истины сработала, else — за путь, если печать оказалась ложной else if: несколько путей Иногда вариантов больше двух Например • меньше 50 очков — слабый прогресс: • от 50 до 99 — почти готов; • от 100 и выше — можно повысить ранг. int sectPoints = 75; if (sectPoints >= 100) { System.out.printin("Можно повысить ранг.");
} else if (sectPoints >= 5G) { System.out.piintin("Тк близок к повышению."); } else { System.out.printin("Нужно больше тренировок."); ) Результат:Ты близок к повышению. Важно- Java проверяет услония сверху вниз. int sectPoints = 120; if (sectPoints >= 100) { System.out.println("Можно повысить ранг."); I else if (sectPoints >= 50) { System.out.printlnfTb; близок к повьшению. ") ; } else { System.out.printin("Нужно больше тренировок."); } Результат Можно повысить ранг. Хотя 120 >= 50 тоже истинно, до второго условия программа уже не дойдёт. Первое сработало — остальные ветки пропущены. 3.6. Проверка повышения ранга Теперь сделаем более реалистичную проверку. Ученик может повысить ранг, если • он активен; • у него не меньше 100 очков; • его уровень меньше 10 Сперва попробуй написать код сам. а если не получится - скопируй то. что ниже public class Rankcheck { public static void main(String'] args) { String studentName = "Путник"; int level = 1; int sectPoints = 12C; boolean active = true; if (active ss sectPoints >= 100 4& level < 10) { 1evel += 1; sectPoints -= 100; System.out.println(studentName + " пошсил ранг!"); System.out.println("Нов^й уровень: " + level); System.out.println("Осталось очков: " + sectPoints); ) else ( System.out.println("Пс чтение невозможно."); ] }
Путник повысил ранг! Новый уровень: 2 Осталось очков: 20 Наставник кивнул. — Теперь твоя программа не просто хранит очки. Она принимает решение. 3.7. Вложенные условия Иногда внутри одного условия нужно проверить другое int sectPoints = 120; boolean active ж true; if (active) { if (sectPoints >= 100) { System.out.println("М:жно повесить ранг."); } else { System.out.printIn("Очкса недостаточно."); } } else f System.out.println("Ученик неактивен."); } Это называется вложенным условием. Оно работает, но если вложенности становится слишком много, код начинает превращаться в пещеру демонов Плохо. if (active) { if (sectPoints >= ЮС) { if (level < l.i) { if (iblocked) { System.out.println("Повышение разрешено."); } } } ) Наставник помрачнел. — Если твой код уходит 1-право быстрее, чем ученик убегает от дедлайна, пора остановиться. Часто лучше объединить условия if (active && sectPoints >• 100 S& level < 10 && iblocked) { System.out.println("По- диение разрешен.,"); } Но и здесь ьажно не перестараться Слишком длинное условие тоже плохо читается.
boolean canRankUp = active && sectPoints >= 100 level < 10 (blocked; if (canRank'Jp) { System.out.println("no шиение разрешено."); } Так код становится понятнее. 3.8. Типичные ошибки в условиях Ошибка 1. Один знак = вместо == Плохо if (level = 1) { System.out.printin("Первый уровень"); } Правильно if (level == 1) { System.out.printin("Первый уровень"); Ошибка 2. Сравнение строк через == Плохо string action = "TRAIN"; if (action =« "TRAIN") { System.out.printin("Тренировка"); ) Лучше String action = "TRAIN"; if (acticn.equals("TRAIN")) { System.out.printin("Трениоовка"); ) Ещё безопаснее, если переменная может быть null: if ("TRAIN".equals(action)) { System.out.orintln("Тренир эка"); I Так программа не упадёт, даже если action вдруг окажется null. Ошибка 3. Слишком сложное условие без имени Плохо'
if (active sectPoints >= IOC && level < 10 && (blocked && compietedTrials >= 3) { System.out.println("no. гшение разрешен-,."); ) Лучше boolean hasEnoughPoints = sectPoints >= 100; boolean hasRequiredTrials = completedTrials >= 3; boolean canRankUp = active && hasEnoughPoints && level < 10 && (blocked hasRequiredTrials; if (canRankUp) { System.out.printin("Повышение разрешено."); ) Имена помогают понять, что происходит. Ошибка 4. Лишнее сравнение с true или false Плохо if (active == true) { System.out.println("Ученик активен."); } Лучше if (active) { System.out.println("Ученик активен."); I Плохо if (active == false) { System.out.println("Ученик неактивен."); ) Лучше if ((active) { System.out.println("Ученик неактивен."); } 3.9. Выбор тактики Наставник снова указал на четыре каменные двери. — Теперь представим, что ученик выбирает действие на день. Варианты: • train — тренировка; * REST — отдых;
• BATTLE — бой; • MEDITATE — медитация. Пока мы ещё не изучали enum, поэтому будем использовать строку. String action = "TRAIN"; В будущем такие варианты лучше хранить через enum. Но сейчас нам достаточно строк, чтобы понять саму идею выбора. Решение через if Можно обработать действие через if / else if / else. public class Actionchoice { public static void main(String!] args) { String action = "TRAIN"; int sectPoints = 10; int energy = 50; int stress = 20; if ("TRAIN".equals(action)) ( sectPoints += 15; energy -= 10; stress += 5; System.out.println("Ты ыбрал тренировку."); } else if ("REST".equals(action)) { energy += 2 ; stress -= 10; System.out.println("Ты выбрал отдах."); } else if ("BATTLE".equals(action)) ( sectPoints += 25; energy -= 20; stress += 1 ; System.out.println("Ты выбрал бой.“); } else if ("MEDITATE".equals(action)) { sectPoints += 5; energy += 5; stress -= 5; System.out.println("Ты ыбрал медитацию."); } else { System.out.println("Неизвестное действие."); ) System.out.printlnС^чки секты: " + sectPoints); System.out.println("Энергия: " + energy); System.out.println("Стресс: " + stress); ) } Результат
Ты выбрал тренировку. Очки секты: 25 Энергия: 40 Стресс: 25 Такой код работает. Но если вариантов много, цепочка else if становится длинной. Когда нужен switch switch удобен, когда • есть одна переменная; • её нужно сравнить с несколькими конкретными вариантами; • для каждого варианта нужно выполнить свое действие. Например. String action = "TRAIN"; И варианты: TRAIN REST BATTLE MEDITATE Это хороший случай для switch. if лучше подходит, когда условия разные и сложные if (active && sectPoints >= 10G && level < 10) { // ... } switch лучше подходит, когда мы ныбираем один вариант из набора. switch (action) { case "TRAIN": // ... break.; case "REST": // ... break; default: // ... ) Классический switch нариант выглядит так: public class Actionswitch { public static void main(String!] args) { String action = "TRAIN";
int sectPoints = 10; int energy = 50; int stress = 2i ; switch (action) ( case "TRAIN": sectPoints += 15; energy -= 10; stress += 5; System.out.println("Ты выбрал тренирс .ку."); break; case "REST": energy += 20; stress -= 10; System.out.println("Ты зыбрал отдых."); break; case "BATTLE": sectPoints += 25; energy -= 20; stress += 15; System.out.println("Ты пыбрал бой."); break; case "MEDITATE": sectPoints += 5; energy += .r ; stress -= 5; System.out.println("Ты bi Срал медитацию."); break; default: System.out.println("Неизвестнее действие."); } System.out.println("Очки секты: " + sectPoints); System.out.println("Энергия: " t energy); System.out.println("Стресс: " + stress); } ) case — это конкретный вариант. case "TRAIN": Значит если action равен "train", выполнить этот блок. default выполняется, если ни один case не подошел. default: System.out.println("Неизвестное действие."); Это защита от неожиданных значений. break завершает выполнение текущего case. Если забыть break, старый switch может продолжить выполнение следующего блока Это называется fall-through
Плохо String action = "TRAIN"; switch (action) { case "TRAIN": System.out.printin("Тренир 'вка"); case "REST": System.out.pr intln("С 1тдых"); default: System.out.printin("Неизвестное дейстгие"); ) Результат будет неожиданным: Тренировка Отдых Неизвестное действие Наставник строго сказал — Ученик хотел тренироваться, а в итоге ещё и отдохнул, и потерялся. Так рождаются странные баги Правильно switch (action) { case "TRAIN": Systern.out.println("Тренировка"); break; case "REST": System.out.printin("Отдых"); break; default: System, 'uc.printin("Неизвестное действие"); } Современный switch В современных версиях Java (17+) можно писать короче и безопаснее String action = "TRAIN"; switch (action) { case "TRAIN" -> System.out.printin("Ты выбрал тренировку."); case "REST" -> System. >ut.printin("Ты выбрал отдых.”); case "BATTLE" -> System.out.printin("Ты выбрал бой."); case "MEDITATE" -> System.out.printin("Ты *ыбрал медитацию."); default -> System.out.printin("Hens, естное действие."); ) Здесь не нужен break. Можно использовать switch как выражение, которое возвращает значение String action = "TRAIN"; int pointsChange = switch (action) { case "TRAIN" -> 15;
case "REST" -> 0; case "BATTLE" -> 25; case "MEDITATE" -> 5; default -> 0; System.out.println("Изменение очко-: " * pointsChange); Теперь switch не просто выполняет код, а вычисляет результат Наставник отметил — Такой switch выглядит чище, когда тебе нужно выбрать значение Но не пытайся запихнуть в него всю жизнь ученика 3.10. Повреждённый свиток. Подмена тактики Наставник развернул новый свиток — Этот код нашли утром у доски рейтинга Кто-то пытался добавить выбор действия, нс- допустил ошибку Найдите ее public class BrokenAction { public static void main(String[] args) { String action = "TRAIN"; int sectPoints = I1 ; if (action == "TRAIN") { sectPoints += 15; } System.out.рг1пс!п("0чки секты: " + sectPoints); } } Код может вывести правильный результат, но подход опасен. Правильно: if ("TRAIN".equals(action)) { sectPoints += 15; } Наставник сказал: — Предатель редко ломает код огромным ударом Чаще он оставляет маленькую трещину, которая сработает позже. Пинь Грейдл нахмурился. — Вы опять говорите о предателе? — Я говорю о плохом коде, — ответил наставник. — Иногда между ними меньше разницы, чем хотелось бы.
3.11. Записки старейшины 1. Не делай условия загадками Плохо if (а && b !с && d > 10) ( System.out.println(“Можно"); } Лучше: boolean canEnterTrial = active && hasEnoughPoints && (blocked S.& completeaOuests > 10; if (canEnterTrial) { System.out.println("Можно войти > испытание."); 2. He злоупотребляй вложенностью Плохо if (active) { if {(blocked) { if (sectPoints >= 100) { if (level < 10) { System.out.println("Можно повысить ранг."); } ) Лучше boolean canRankUp = active && (blocked sectPoints >= 100 && level < 10; if (canRankUp) { System.out.printlnсмежно повысить ранг."); 3. He используй строки как вечное решение Сегодня мы пишем String action = "TRAIN"; Это нормально для учебного примера Но в реальном коде строки опасны: String action = "TRIAN"; Одна опечатка — и программа не узнает действие Позже ты изучишь enum, и такие варианты станут безопаснее.
3.12. Испытания главы Испытание 1. Хватает ли очкоо для повышения ранга Создай программу Rankcheck, java. Дано String studentName = "Путник"; int level = 1; int sectPoints = 95; boolean active = true; Правила • если ученик активен и очков не меньше 100. вывести: Можно повысить ранг. • иначе вывести Недостаточно условий для повышения. Потом измени sectpoints на 120 и проверь результат. Награда: +10 очков секты. Испытание 2. Повышение с оплатой Усложни программу. Если ученик может повысить ранг: • увеличь level на 1; • уменьши sectPoints на 100; • выведи новый уровень и остаток очков Пример Ранг повышен! Новый уровень: 2 Осталось очков: 20 Награда: +15 очков секты. Испытание 3. Проверка нескольких условий Добавь переменные boolean blocked = false; int completedTrials = 3; Ученик может повысить ранг, если-
♦ активен; • не заблокирован; • очков не меньше 100; • завершил минимум 3 испытания. Создай переменную boolean canRankUp = ...; И используй ее в if. Награда +15 очков секты. Испытание 4. Выбор действия через if Создай программу Actionlf. java. Дано. String action = "TRAIN"; int sectPoints = 10; int energy = 50; int stress = 20; Варианты: Действие Эффект TRAIN sectPoints +15, energy -10, stress +5 REST energy +20. stress -10 RATTLE sectPoints +25, energy -20, stress +15 MEDITATE sectPoints +5. energy +5, stress -5 Реализуй зыбор через if / else if I else. Награда +20 очког секты. Испытание 5. Выбор действия через switch Создай программу Actionswitch .java. Сделай то же самое, что в прошлом испытании, но через switch. Не забудь: • case; • break; • default. Награда: +20 очков секты.
Испытание 6. Современный switch Создай программу ModernActionSwitch. java Используй современный switch, чтобы посчитать изменение очког- String action = "BATTLE"; int pointsChange = switch (action) { case "TRAIN" -> 15; case "REST" -> ; case "BATTLE" -> 25; case "MEDITATE" -> 5; default -> 0; ); Выведи результат. Награда; +15 очков секты. Испытание 7. Найди ошибку Что не так в этом коде? String action = "TRAIN"; if (action == "TRAIN") { System.out.println("Тренировка"); ) Исправь его. Награда: +5 очкон секты. Испытание 8 Дуэль учеников. Наставник объявил первое малое состязание — Перед вами два ученика. Один всегда выбирает train. Другой выбирает действие по состоянию энергии. Вариант Линь Грейдла String action = "TRAIN"; int energy = 5; int stress = 80; int sectPoints = 9U; if ("TRAIN".equals(action)) ( sectPoints += 15; energy -= 10; stress += 5; )
Вариант осторожного ученика int energy = 5; int stress = 80; int sectPoints = 90; String action; if (energy < 10 || stress > 70) ( action = "REST"; ) else { action = "TRAIN"; ) Вопрос: Какой вариант безопаснее для долгого состязания и почему? Ответ наставника — Первый вариант может дать быстрые очки, но не проверяет состояние ученика. Второй учитывает условия Программирование — это не только "сделать действие". Это ещё и понять, когда действие допустимо 3.13. Турнирный зачёт. Первая тактика К концу занятия ты освоил печати истины и первый выбор тактики Теперь ты знаешь. • boolean хранит true или false; • сравнения создают логический результат; • && означает “и”; • 11 означает "или"; • ! означает “не”; • if выполняет код при истинном условии; • else работает, если условие ложно; * else if позволяет проверять несколько путей; • вложенные условия работают, но могут ухудшить читаемость: • строки лучше сравнивать через equals; • switch удобен для выбора одного варианта из набора. * case описывает вариант; • default обрабатывает неизвестное значение; • современный switch может возвращать значение; • длинные условия лучше называть через отдельные boolean-переменные. Наставник посмотрел на твою табличку ученика Имя: Путник Статус: внешний ученик
корень духа пробужден Техника Первоп Голоса: освоена Сосуды ци: открыты Печати истины: освоены Выбор тактики: освоен Очки секты: +184 Барьер прорыва. Выбор тактики открыт Турнирный рейтинг: допущен к подготовке — Сегодня ты сделал важный шаг, — сказал наставник. — Программа, которая не умеет выбирать, похожа на ученика, который бьет одной техникой в любую сторону Иногда попадёт. Чаще — разобьет себе лоб Он указал на следующий зал — Завтра ты узнаешь, как повторять действия много раз И почему демон бесконечного цикла считается одним из первых настоящих врагов внешнего ученика Ученики начали расходиться. Ты задержался у турнирной доски На ней медленно обновлялись очки внешних учеников Некоторые имена поднимались выше. Некоторые опускались ниже, Внезапно в нижней части доски снова мигнула странная строка unknown_student: -999 очков На этот раз ты точно ее увидел Через мгновение строка исчезла. За твоей спиной прозвучал голос наставника: — Заметил? Ты кивнул Наставник долго смотрел на пустое место на доске — Хорошо Значит, твои печати истины начинают работать не только в коде. Он убрал руку в рука^ мантии. — Пока не говори остальным Если это обычная ошибка, мы найдем ее Если нет... внутреннее состязание будет опаснее, чем я думал Где-то далеко в верхних павильонах тихо щёлкнул замок. И один поврежденный свиток исчез из архива секты. Ночь перед Залом Повторов После испытания печатей истины ученики разошлись не сразу. Кто-то радовался первым очкам Кто-то спорил, какой путь сильнее тренировка, бой или медитация. Кто-то пытался доказать, что если условие достаточно длинное, оно выглядит мудрее Наставник Павильона Основ прошел мимо таких учеников и тихо сказал:
— Длинная запутанная мысль не становится глубокой только потому, что в ней много символов Спор сразу стал тише. Ты остановился у турнирной доски. После первой тактики на ней появились новые строки Путник, выбор тактики освоен Линь Грейдл. высокая скорость решения Мэй Рефактор, чистое условие, низкая вложенность Боолянь Истинный: стабильные печати истины Пинь Грейдл стоял неподалёку, скрестив руки. — Условия — это просто, — сказал он. — Настоящая сила в скорости Чем быстрее пишешь код, тем быстрее побеждаешь Мэй Рефактор спокойно ответила. — Чем быстрее пишешь плохой код, тем быстрее создаёшь проблемы Пинь хотел возразить, но в этот момент из глубины секты донесся глухой удар барабана Один Потом второй. Потом третий. Наставник поднял взгляд к верхнему павильону. — Зат тра откроется Зал Повторов Ученики замолчали. — Там проверяют не тех, кто сделал правильно один раз. — сказал наставник. — Там проверяют тех, кто может повторить действие десять, сто, тысячу раз и не потерять смысл На воротах дальнего зала вспыхнули строки: for while do while Сунь Цикл, молчаливый ученик с тремя деревянными счётчиками на поясе, впервые улыбнулся. — Наконец-то нормальное испытание. Линь Грейдл усмехнулся
— Просто повторять одно и то же? Скучно Наставник посмотрел на него. — Скучно становится только тому, кто не понимает, что повторение без контроля превращается в бесконечную ловушку Вдалеке один из старых тренировочных манекенов вдруг начал двигаться сам по себе. Он поднял деревянный меч, сделал удар, затем снова поднял меч, снова сделал удар, снова поднял меч.. И не остановился. Наставник нахмурился. — Похоже, кто-то уже оставил нам первое предупреждение На турнирной доске мелькнула строка: loop_status: infinite Затем погасла. Завтра начиналась Серия Повторов
Глава 4. Серия повторов На четвертый день учеников привели не в привычный двор Павильона Основ,, а к длинному каменному залу, уходящему н гору Над входом горели символы Зал Тысячи Повторов Изнутри доносились голоса, шаги, удары посохов и странный монотонный шепот: — Ещё раз Ешё раз Еще раз Ещё раз Один из новичков остановился у входа — Это тренировка или наказание? Наставник Павильона Ochol. ответил спокойно. -Да Ученики переглянулись. Наставник поднял посох На стене зала вспыхнула турнирная доска внутреннее состязание секты Этап подготовки: Зал Тысячи Повторов Дс первою отбора: 1 занятие цель: научиться повторять действия без потери контроля Линь Грейдл стоял ближе всех ко входу. Он выглядел уверенно — Повторы? Это просто. Делать одно и то же много раз сможет любой внешний ученик Наставник посмотрел на него — Делать одно и то же много раз сможет даже сломанный артефакт. Культиватор отличается тем. что понимает, когда начать, когда продолжить, когда остановиться и когда пропустить лишнее. Он повернулся ко г сем ученикам — Сегодня вы изучите циклы, В Java циклы позволяют повторять действия выводить строки, считать очки, моделировать дни тренировок, перебирать будущие списки учеников и не писать один и тот же код вручную сотни раз В глубине зала что-то глухо ударило в стену Один из старших учеников устало произнёс: — Опять демон бесконечного цикла проснулся Наставник вздохнул — Значит, начнём быстро
4.1. Зачем нужны циклы Представь, что тебе нужно вывести уровни ученика от 1 до 5. Можно написать так. System.out.println("Ураьень Iм); System.out.pr:ntln("Уровень 2"); System.out.println("Уров >нь 3"); System.out.println("Уровень 4"); System.out.pr:ntln("Уровень 5"); Код работает Но если нужнл вывести уровни от 1 до 100? Или смоделировать 30 дней тренировок? Или начислить награду за каждое выполненное испытание? Писать все руками — путь страдания Цикл позволяет сказать программе повтори это действие несколько раз. Например’ for (int level = 1; level <= 5; level++) { System.out.println("Уро;ень " + level); ) Результат: Уровень 1 Уровень 2 Уровень 3 Уровень 4 Урчкень 5 Наставник сказал: — Если переменная — это сосуд ци. то цикл — это дыхательная техника. Она повторяет движение снова и снова, пока условие позволяет продолжать. Сухая истина Java Цикл нужен, когда одно действие нужно выполнить несколько раз В Java часто используются три вида циклов Цикл Когда использовать for while do while когда заранее понятно, сколько раз повторять когда повторение зависит от условия когда тело цикла нужно выполнить хотя бы один раз
Позже ты будешь использовать циклы для перебора коллекций, обработки списков, повторения действий и моделирования процессов. 4.2. Цикл for Наставник начертил на камне for (int i = L; i <= 5; i++) { System.out.println(i); } — for часто используют, когда известно количество повторов Например: пройти от 1 до 5, повторить тренировку 7 дней, вывести 10 строк. Разберем запись: Часть Что значит int i с 1 начальное значение счётчика । <= 5 условие продолжения цикла I++ изменение счётчика после каждого пов i ора тело цикла код внутри фигурных скобок i++ означает: увеличить i на 1. Это короткая запись для. i = i + 1; или, i += .; Таблица уровней Теперь сделаем учебный пример ближе к секте. public class LevelTable { public static void main (String[] args) { for (int level = 1; level <= 5; level++) { int reguiredPoints = level * 100; System.out.println("Урс ень " + level + нужно " + requiredPoints + " очков"); J } ) Результат: Уринень 1: нужн<.. 100 очков Уровень 2: нужно 200 очксв Уровень 3: нужно 300 очков
Уровень 4: нужно 400 очков Уровень 5: нужно 500 очков Наставник кивнул. — Так цикл превращает один шаблон в серию действий. Ты не пишешь пять почти одинаковых строк Ты описываешь правило Малое испытание 1. Таблица рангов Создай программу RankTable.java. Выведи уровни от 11 до 21 и количество очков, нужное для каждого уровня. Правило нужные очки = уровень * 100 Пример: Уровень 1: 100 очков Уровень 2: 200 очков Уровень 10: 1000 очков Награда. +10 очков секты. Счётчик накопления Цикл может не только выводить строки, но и накапливать результат Допустим, ученик получает каждый день по 10 очков секты. Нужно посчитать, сколько он получит за 7 дней. public class TrainingSum { public static void main(StringI] args) { int total Points = 0; for (int day = 1; day <= 7; day++) ( totalPoints += 10; System.out.println("День " + day + всего очксь " + totalPoints); } System.out.println("Итог за неделю: " + totalPoints); } ) Результат: День 1: всего очков 10 День 2: всего очков 20 День 3: всего очков 30 День 4; всего -imkob 40
День 5: всего очков 50 День 6: всего очков 60 День 7: всего очков 76 Итог за неделю: 70 Здесь totaiPomts — накопитель Он начинается с 0, а потом на каждом шаге увеличивается. Наставник сказал: — В будущем такие накопители будут считать рейтинги, суммы наград, количество побед, штрафы и результаты турнира. Малое испытание 2. Неделя тренировок Создай программу TrainingWeek.java Условия: int sectPoints = ; int dailyReward = 12; С помощью цикла for смоделируй 7 дней тренировок. Каждый день добавляй dailyReward к sectPoints и выводи: День 1: очки секты = 12 День 2: очки секты = 24 Награда. +10 очков секты. 4,3. Цикл while Наставник провел учеников дальше В следующей части зала стояли каменные манекены. Над каждым горела надпись Продолжай, пока есть энергия — Это хороший случай для while, — сказал наставник. - Цикл while выполняется, пока условие истинно. while (условие) { // действия Пример: public class EnergyTrai.ning { public static void main(String!) args) { int energy = 30; int sectPoints = 0;
while (energy > 0) { System.out.println("Тренирс-ка. Энергия: " + energy); sectPoints += 10; energy -= 10; } System.out.println("Тренировка засершена."); System.out.println("Очки секты: " + sectPoints); } } Результат: Тренировка. Энергия 30 Тренировка. Энергия: 20 Тренировка. Энергия: 10 Тренировка завершена. Очки секты: 30 Почему цикл остановился? Потому что после третьей тренировки energy стал равен 0. а условие energy > 0 стало ложным. Сухая истина Java while проверяет условие перед каждым повтором Если условие сразу false, тело цикла не выполнится ни разу. int energy = '; while (energy > 0) { System.out.println("Тренировка"); } Этот код ничего не выведет, потому что энергии нет уже в начале Повреждённый свиток 1. Демон бесконечного дыхания На стене вспыхнул код: public class InfiniteTraining { public static void main(String!] args) { int energy = 30; while (energy > 0) { System.out.println("Тренировка. Энергия: " + energy); ) Настаьник спросил: — Что случится? Один ученик ответил
— Он будет тренироваться, пока энергия больше нуля — А энергия меняется? Ученик замолчал. Проблема: внутри цикла energy не уменьшается Значит, условие energy > 0 всегда остаётся истинным Исправление: while (energy > 0) { System.oat.println("Тренир-^а. Энергия: " + energy); energy -= 10; } Наставник сказал: — Бесконечный цикл — один из первых настоящих демонов внешнего ученика Он не выглядит страшно Он просто никогда не заканчивается 4.4. Цикл do while В следующей части зала стоял одинокий барабан Над ним висела табличка- Сначала ударь. Потом проверь, надо ли продолжать. Наставник сказал — Это do while. do while сначала выполняет тело цикла, а потом проверяет условие. do { // действия } while (условие); Пример: public class FirstAttempt { public static void main(String!] args) { int attempts = 0; boolean success; do { attempts++; System.out.println("Попьтка запуска техники: " + attempts); success = attempts >= 1; } while (!success); System.out.pr intln("Техник i запущена."); } ) Результат: Попытка запуска техники: 1
Техника запущена. Даже если условие потом окажется ложным, тело do while выполнится хотя бы один раз. Сухая истина Java Главное отличие Цикл Проверка условия while до выполнения тела do while после выполнения тела do while нужен реже, чем for и while, но полезен, когда действие должно произойти минимум один раз. Например показать меню, запросить >;вод. сделать первую попытку. 4.5. Дополнительные условия во время цикла break, досрочно выйти из цикла Иногда цикл должен остановиться раньше обычного. Например, ученик ищет редкую пилюлю б тренировочном зале Как толью.- нашел — продолжать поиск не нужно public class FindPill { public static void main(Stringargs) ( for (int box = 1; box <= 10; box++) { System.out.println("Проверяем ящик " + box); if (box == 4) { System.out.println("Пилюля найдена!"); break; } ) } } Результат: Проверяем ящик 1 Проверяем ящик 2 Проверяем ящик 3 Приверяем ящик 4 Пилюля найдена! После break никл завершается. Наставник сказал:
— break — это приказ покинуть текущий цикл. Используй его, когда дальнейшие повторы больше не имеют смысла continue: пропустить текущий повтор Иногда нужно не остановить весь цикл, а пропустить один шаг Например, ученик тренируется 7 дней, но каждый третий день делает восстановление и не получает обычную награду public class SkipRestDay { public static void main(String!] args) { int sectPoints = 0; for (int day = 1; day <= 7; day++) { if (day 13== 0) { System.oat.println("flem " + day + ". восстановление"); continue; } sectPoints += 10; System.out.println("День " + day + тренировка, очки = " + sectPoints); } System.out.println("Итоговые очки: " + sectPoints); ) ) continue пропускает оставшуюся часть текущего повтора и переходит к следующему шагу цикла 4.6. Вложенные циклы В глубине Зала Тысячи Повторов находилась площадка с рядами каменных плит Наставник указал на них. — Иногда нужно повторять повторение Например, пройти несколько дней, а в каждом дне выполнить несколько подходов тренировки. public class NestedTraining ( public static void main(String;] args) { for (int day = ]; day <= 3; daytt) { System.out.println("День " + day); for (int approach = 1; approach <= 2; approachw) { System.out.println(" Подход " + approach); } } ) Результат:
День 1 Подход 1 Подхид 2 День 2 Подход 1 Подход 2 День 3 Подход 1 Подход 2 Внешний цикл окзечает за дни.Внутренний цикл отвечает за подходы внутри каждого дня. Наставник предупредил. — Вложенные циклы полезны, но опасны при больших числах. Если внешний цикл делает 100 повторов, а внутренний тоже 100, всего будет 10 000 действий. Повреждённый свиток 2. Слишком много повторов Код: for (int day = 1; day <= 100C; day++) { for (int approach = 1; approach <= 10C0; approach++) { System.out.println("Тренировка"); } Сколько раз выведется строка9 — Иногда код не сломан Он просто делает ровно то, что ты попросил. И это пугает сильнее 4.7. Практика: подготовка к внутреннему состязанию Теперь соберём несколько идей вместе. Ученик готовится 10 дней. Правила 1. каждый день тренировка дает 12' очков; 2 тренировка гратит S' энергии, 3. каждый трет ий день ученик отдыхает; 4. отдых восстанавливает 20 энергии; 5. если энергия меньше 8'. ученик не может тренироваться и отдыхает: 6. стартовая энергия— 40 ; 7. стартовые очки — 0 Попробуй сперва написать код сам, а потом сравни его с кодом ниже
public class TournamentTraining { public static void main (Stringf] args) { int sectPoints = 0; int energy = 4u; for (int day = 1; day <= 10; day++) { System..'.ut.println ("День " + day); boolean plannedRestDay = day % 3 == 0; boolean notEnoughEnergy = energy < ; if (plannedRestDay I I notEnoughEnergy) { energy t= 20; System.out.println("Отдых. Энергия: " + energy); continue; } sectPoints +- 12; energy -= 8; System.out.println("Тренировка. Очки: " + sectPoints + ", энергия: " + energy); } System.out.print In("Подготогка завершена."); System.out.println("Итоговые чки: " + sectPoints); System.out .println ("Итого->ая энергия: " + energy); ) } Дуэль учеников. Упрямство против стратегии Наставник вывел на стену два подхода Подход Линь Грейдла: for (int day = 1; day <= 1 ; day++) { sectPoints += 12; energy -= 8; } Подход осторожного ученика: for (int day = 1; day <= 10; day++) { if (energy < 8) { energy += 20; continue; } sectPoints += 12; energy -= 8; t Вопрос: Какой код безопаснее и почему?
Ответ: Первый код всегда тренируется и может увести энергию в отрицательные значения Второй код проверяет состояние перед действием и не позволяет тренироваться без энергии Наставник сказал — Сила без условия — путь к падению Повтор без контроля — путь к бесконечному циклу. Испытания главы Испытание 1. Таблица уровней Создай программу LevelTable.java. С помощью lor выведи уровни от 1 до 10. Для каждого уровня выведи нужные очки: Уровень 1: нужно 100 очков Уровень 2: нужно 200 очков Награда +10 очков секты Испытание 2. Сумма наград Создай программу RewardSum.java. Дано int totalReward « 0, int questReward = 15; С помощью цикла посчитай награду за 5 заданий. Ожидаемый итог. Итоговая награда. 75 Награда +10 очков секты. Испытание 3- День восстановления Создай программу RestEveryThirdDay.java Смоделируй 10 дней Каждый третий день □ыводи:
День N: восстановление В остальные дни добавляй 10 очков Используй % и continue Награда +20 очков секты. Испытание 4 Найди пилюлю Создай программу FmdPill.java Есть 10 ящиков. Пилюля лежит в ящике 7 С помощью цикла проверь ящики от 1 до 10 Когда пилюля найдена, выведи сообщение и останови цикл через preak. Награда. +20 очко > секты. Испытание 5. Исправь бесконечный цикл Исправь код: int energy = 30; while (energy > 0) { System.out.println("Тренировка"); } Награда +10 очков секты Турнирный зачёт. Подготовка за 14 дней Создай программу InternalContestPreparationjava Условия String studentName = "Путник"; int sectPoints = 0; int energy = 50; int stress = 10; Смоделируй 14 дней подготовки. Правила 1 Каждый 4-й день — медитация 'stress-= 10', energy+= 5' 2. Если энергия меньше'1 О'— отдых. energy += 20 , 'stress -- 5 '. 3. В остальные дни — тренировка: sectPoints += 15', energy -= 10', 'stress += 5 4. Стресс не должен уходить ниже 'О'. Если стал меньше О’, верни его к '0 5. В конце выведи профиль ученика
Это задание готовит тебя к внутреннему состязанию. Награда +30 очков секты Барьер прорыва: Серия повторов Когда ученики вышли из Зала Тысячи Повторов, многие L-ыглядели уставшими Некоторые всё ещё машинально бормотали: — i++, условие, тело, I++, условие, тело Наставник остановился у турнирной доски. На ней появились новые строки: Путник: серия повторов освоена Линь Грейдл: серия повторов освоена Мэй Рефактор: серия повторов освоена unknown_student: -999 очков Странная строка снова мигнула и исчезла На этот раз ее увидели несколько учеников — Что это было9 — Ошибка доски? — Или кто-то шутит? Наставник не ответил сразу. Он провел пальцем по воздуху, будто проверял невидимый след. — Повторяющаяся ошибка редко бывает случайной. Линь Грейдл скрестил руки — Может, кто-то просто плохо написал цикл — Возможно. — сказал наставник — А возможно, кто-то хочет, чтобы мы так думали. Он повернулся к ученикам — Но сегодня ваша задача выполнена Вы освоили серию повторов Твоя табличка вспыхнула. Имя: Путник Статус: внешний ученик Корень духа: пробуждён Техника Первого Голоса: освоена Сосуды ци: о:крыты Печати истины: освоены Выбор тактики: освоен Серия повторов: освоена Очки секты: +309 барьер прорыва: Серия повторов открыт Турнирный рейтинг: допущен к первому отбору
Наставник указал на арену за следующим павильоном — Завтра начнется первый этап внутреннего состязания Там победит не тот, кто напишет больше строк. Победит тот, кто сможет превратить повторяющийся код в чистую технику.
Г лава 5. Боевые приёмы и вложенная энергия Утром весь двор секты гудел Сегодня начинался первый этап внутреннего состязания На центральной арене стояли каменные столы, над каждым висел светящийся экран. На трибунах собрались внешние ученики Старшие ученики наблюдали с верхних галерей. У входа в арену висела доска. Внутреннее состязание секты Java Core Этап 1: Арена Чистого Кода Цель: избавиться от дублирования и оформить техники е методы Ты заметил знакомые имена. Линь Грейдл Мэй Рефак юр Биолянь Истинный Сунь Цикл Путник Линь Грейдл улыбнулся — Надеюсь, сегодня задания будут сложнее, чем повторять числа от 1 до 10 Рядом стояла ученица с короткой мантией и аккуратно сложенными свитками На ее табличке было написано. Мэй Рефактор Она спокойно ответила — Сложность не в том. чтобы написать много кода. Сложность в том, чтобы потом не стыдиться его читать. Линь Грейдл фыркнул Наставник поднялся на арену. — Сегодня вы изучите методы. В мире культивации метод — это оформленный боевой прием Если ты каждый раз заново описываешь одно и то же движение, ты не владеешь техникой. Ты просто машешь руками Он ударил посохом по камню. На стене появился длинный код int sectPoints = 10; int energy = 50; int stress = 20; sectPoints += 15; energy -= 10; stress += 5; System.out.println("Трениро^ха завершена"); sectPoints += 15;
energy -= IC; stress += 5; System.out.println("Тренировка завершена"); sectPoints += 15; energy -= 10; stress += 5; System.out.println("Тренировка ъаверш^на"); — Что вы видите9 — спросил наставник Один ученик ответил: — Три тренировки Мэй Рефактор сказала: — Дублирование Наставник кивнул. — Верно Дублирование — это когда один и тот же смысл повторяется п коде снова и снова. Если правило изменится, тебе придётся править его в нескольких местах. Забудешь одно место — получишь баг. Он повернулся к ученикам. — Метод позволяет дать технике имя и вызывать её тогда, когда нужно 5.1. Зачем нужны методы Метод — это именованный блок кода, который выполняет определенное действие Например public static void train () { System.out.println ("Тренир-: ка за; зршена"); } Теперь этот метод можно вызвать train (); Полный пример: public class MethodExampie { public static void main(String'] args) { train () ; train () ; train (); } public static void train () { Systern.out.println("Тренировка за“ершена"); } )
Результат: Тренировка завершена Тренировка завершена Тренировка завершена Наставник сказал: — Раньше ты повторял код Теперь ты повторяешь вызов техники Сухая истина Java Метод нужен, чтобы • убрать дублирование; • дать действию понятное имя; • разделить программу на части; • переиспользовать код; • сделать программу легче для чтения и проверки. Общий вид простого метода: public static void methodNameO { // тело метода } Пока мы используем static, потому что работаем из метода main Позже, когда дойдём до объектов, методы станут частью классов и будут работать с состоянием конкретного ученика 5.2. Параметры метода Методы с параметрами Иногда метод должен работать не с фиксированным значением, а с тем, что ему передали Например, приветствие по имени public class GreetingWithParameter { public static void main(String(] args) { greetStudent("Путник"); greetstudent("Мэй"); } public static void greetStudent(String name) { System.out.println("Привет, " + name + "!"); } ) Результат: Привет, Путник! Привет, Мэй! String name - это параметр метода.
Когда ты вызываешь метод и передаешь значение, это значение попадает в параметр Наставник обьяснил: — Параметр — это вложенная энергия техники Одна и та же техника может работать по-разному в зависимости от того, какую силу ты в нее передал Методы с несколькими параметрами Метод может поинимать несколько параметрон public class ProfilePrinter { public static void main(String[] args) { printProfile("Путник", 1, 90); printprofile("Мэй", 2, 150); public static void printProfile(String name, int level, int sectPoints) ( System.out.println("Профиль ученик!"); System.out.println("Имя: " + name); System.out.println("Уровень: " + level); System.out.println("Очки секты: " + sectPoints); System.out.println(); Теперь вывод профиля описан в одном месте Если ты захочешь изменить формат профиля, тебе не придётся искать все места в программе Как передаются параметры Когда ты передаешь в метод примитивное значение, метод получает копию этого значения, Пример: public class ParameterExample { public static void main(String I ] args) { int sectPoints = IC; adciPoints (sectPoints) ; System.out.println("Очки main: " + sectPoints); public static void addPoints(int points) { points += 15; System.out.println("Очки в методе: " + points); Результат: Очки в методе’ 25 Очки в main: lu
Почему? Потому что points внутри метода — отдельная переменная Изменение points не изменяет sectPoints в main. Если нужен новый результат, верни его. public class ParameterExample ( public static void main(String[] args) { int sectPoints = 10; sectPoints = addPoints(sectPoints, 15); System.out.println("Очки в main: " + sectPoints); } public static int addPoints(int points, int reward) ( return points + reward; } } Результат: Очки в main: 25 Наставник предупредил: — Сейчас мы говорим о поостых типах вроде int. С объектами будут нюансы. К ним мы вернемся, когда создадим духовную форму ученика. Перегрузка методов Иногда удобно иметь несколько методой с одним именем, но разными параметрами Это называется перегрузкой методов public static int calcuiateReward(int baseReward) { return baseReward; } public static int calculateReward(int baseReward, int difficulty) { return baseReward + difficulty * 5; public static int calculateReward(int baseReward, int difficulty, int bonus) ( return baseRewaid + difficulty * 5 + bcnus; ) Вызовы int simpleReward = calculateReward(10); int difficultReward = calculateReward(lC, 3); int bonusReward = calculateReward(10, 3, 7); Java выбирает нужный метод по количеству и типам параметров Наставник сказал: — Перегрузка полезна, когда техника одна по смыслу, но может принимать разное количестве вложенной энергии
5.3. Что метод может вернуть? void-методы void означает, что метод ничего не возвращает. puolic class GreetingTechnique ( public static void main (String(] args) { printSectGreeting(); printSectGreeting)); } public static void printSectGreetingd { System.out.println("Добро пожаловать в Секту Java Core!"); ) ) Результат: ДоЬри пожаловать в Секту Java Core! Добра пожаловать в Секту Java Core! Наставник сказал — void- метод похож на технику удара. Он что-то делает, но не приносит тебе результат и руки. Метод с возвращаемым значением Иногда метод должен не просто выполнить действие, а вычислить результат и вернуть его. public class Rewardcalculator { public static void main(Stringf] args) { int reward = calculateReward(1 , 3); System.cut.println("Награда: " + reward); } public static int calculateReward(int oaseReward, int difficulty) { return baseReward + difficulty * 5; ) } Результат: Награда: 25 Наставник сказал: — Метод с возвращаемым значением похож на алхимическую технику. Ты вкладываешь ингредиенты, а на выходе получаешь пилюлю. Ранний return
Иногда метод должен завершиться раньше Например, если ученик заблокирован, тренировка невозможна public static int train(int sectPoints, boclean blocked) ( if (blocked) { System.out,println("Ученик заблокирован. Тренировка невозможна."); return sectPoints; } return sectPoints + 15; ) Если blocked == true, метод сразу зозвращает старые очки. Если ученик не заблокирован, метод начисляет награду Ранний return часто помогает не делать глубокую вложенность. Плохо if (Iblocked) { if {energy >=10) ( sectPoints += 15; ) } Лучше в методе if (blocked) ( return sectPoints; if (energy < 10) { return sectPoints; return sectPoints + 15; Что возвращает метод Если метод объявлен как int, он должен вернуть int Правильно: public static int calculateRewardO ( return 1(; ) Неправильно: public static int calculateRewardO { System.out.println("Награда рассчитана"); ) Такой метод обещает вернуть int, но ничего не возвращает. Компилятор остановит тебя. Если метод ничего не должен возвращать, используй void public static void printRewardO { System.cut.println("Награда рассчитана");
Повреждённый свиток 1. Обещанная награда На стене появился код public class BrokenReward { public static void main(String!] args) ( int reward = calculateReward(j1, 3); System.out.println("Награда: " + reward); public static int calculateReward(int baseReward, int difficulty) { int result = baseReward + difficulty * 5; } 1 Вопрос: Почему код не скомпилируется? Ответ: Метод calculateReward объявлен как int, значит он должен вернуть значение через return 5.4. Область видимости Наставник нарисовал круг вокруг одного из каменных столов. — Не всякая переменная видна отовсюду. У каждой переменной есть область, где она существует. Пример: public class ScopeExample { public static void main(String[] args) { int sectPoints = 10; printpoints(); } public static void printPoints() [ System.out.println(sectloints); } } Этот код не сработает Почему? sectPoints объявлена внутри main. Метод printPoints её не видит. Правильно передать значение параметром. puolic class ScopeExample { public static void main (Strings args) { int sectPoints = 10; printPoints(sectPoints); } public static void printPoints(int points) { System.out.println(points);
Наставник сказал: — Переменная, созданная внутри одного зала, не становится видимой во всей секте, Если другой технике нужно значение, передай его явно 5.5. Арена Чистого Кода Наставник открыл первый этап внутреннего состязания. — Перед нами код подготовки ученика Он работает, но выглядит как ученик, который выполняет одну технику десятью разными способами и каждый раз забывает половину движений Ваша задача — вынести повторяющиеся части в методы Исходный код public class DirtyPreparation { public static void main(String[] args) { int sectPoints = 10; int energy = 50; int stress = 20; sectPoints += 15; energy -= 10; stress += 5; System.cut.println("Тренировка"); energy + 20; stress -= 10; System.out.println("Отдых") ; sectPoints += 25; energy -= 20; stress += 15; System.out.println("Бой"); System.out.println("Очки: " + sectPoints); System.out.println("Энергия: " + energy); System.out.println("Стресс: " + stress); } } Первый шаг — вынести вывод профиля Но с train, rest и battle есть проблема они должны менять сразу несколько значений. Пока мы еще не изучили объекты, поэтому полностью красиво решить это сложно — Это важный момент. Иногда язык уже показывает тебе, что текущих знаний не хватает для красивого решения Скоро мы создадим класс Cultivator, и тогда методы смогут менять состояние конкретного ученика Пока учись выносить чистые вычисления и вывод
Чистые методы вычислений Хороший первый шаг — выносить методы, которые получают значения и возвращают результат Например, расчет награды: puolic static int calcuiateTrainingReward(int baseReward, int level) { return baseReward + level * 2; } Расчет стоимости повышения ранга public static int calculateRankUpCost(int level) { return level * IOC; } Проверка возможности повысить ранг1 public static boolean canRankUp(int sectPoints, int level, boolean active, boolean blocked) ( int cost = calculateRankUpCost(level); return active && .'blocked && sectPoints >= cost; ) Полный пример; public class RankMethods { public static void main(String^] args) { int level = 1; int sectPoints = 15C; boolean active = true; boolean blocked = false; if (canRankUp(sectPoints, level, active, blocked)) { int cost = calculateRankUpCost(level); sectPoints -= cost; level++; System.out.print1n("Ранг псишен1"); } System.out.println("Уровень: " + level); System.out.println("Очки: " + sectPoints); ) public static int calculateRankUpCost(int level) { return level * 10',; public static boolean canRankUp(int sectPoints, int level, boolean active, boolean blocked) { int cost = calculateRankUpCost(level); return active && iblocked && sectPoints >= cost; } ) Теперь правило стоимости повышения находится в одном методе Если зашра старейшины изменят формулу, ты изменишь только calculateRankUpCost.
Поврежденный свиток 2. Скрытое дублирование На стене появился код int firstCost = level * 100; int secondCost = level * 100; int thirdCost = level * 100; Наставник спросил. — В чем опасность9 Мэй Рефактор ответила — Если формула изменится, кто-то обновит одно место и забудет другое Исправление: public static int calculateRankUpCost(int level) { return level * 100; } Наставник кивнул. — Метод — это не просто сокращение. Это место, где живет правило. Дуэль учеников Быстро написать или потом понять На арену вызвали Линь Грейдла и Мэй Рефактор Задание было простым написать расчёт награды за испытание Линь написал прямо н main- int reward = 10 + difficulty ★ 5 + bonus; Потом ещё раз int secondReward = 10 + secondDifficulty * 5 + secondBonus; И ещё раз int finalReward = 10 + finalDifficulty * 5 + finalBonus; Мэй написала. public static int calculateReward(int difficulty, int bonus) { return 10 + difficulty * 5 + bonus; } И использовала int reward = calculateReward(difficulty, bonus); int secondReward = calculateReward(secondDifficulty, secondBonus); int finalReward = calculateReward(finalDifficulty, finalBonus); Наставник спросил учеников — Кто победил9
Некоторые указали на Линя — Он быстрее написал Мэй спокойно сказала: — А если старейшина изменит базовую награду с 10 на 15? Наставник улыбнулся — Тогда Линь будет искать все места вручную. Мэй изменит один метод. На доске появилась оценка Линь Грейдл- скорость высокая, поддерживаемость низкая Мэй Рефактор: скорость средняя, поддерживаемость высокая Наставник добавил — В настоящем проекте часто побеждает не тот, кто быстрее написал первую версию Побеждает тот, чей код можно безопасно менять 5.6. Испытания главы Испытание 1. Первый метод Создай программу FirstMethod.java. Напиши метод вывода- public static void printGreeting() I System.out.println("Секта Java Core приветствует ученика."); } Вызови его три раза из main Награда' +10 очко? секты Испытание 2 Метод с параметром Создай метод: public static void greetStudent(String name) Он должен выводить: Привет, <имя>! Вызови метод для трех разных имён. Hai рада +10 очков секты Испытание 3. Расчёт награды
Создай метод: public static int calculateReward(int baseReward, int difficulty, int bonus) Формула baseReward + difficulty * 5 + bonus вызови метод из mam и выведи результат. Награда: +15 очков секты Испытание 4. Метод расчёта ранга Создай метод: public static String caicuiateRank(int sectPoints) Правила Очки Ранг меньше 100 Новичок от 100 до 299 Внешний ученик от 300 до 599 Кандидат состязания 600 и больше Элитный внешний ученик Награда +20 очков секты. Испытание 5. Перегрузка метода награды Создай три версии метода calculateReward: calculateReward(int baseReward) calculateReward(int baseReward, int difficulty) calculateReward(int baseReward, int difficulty, int bonus) Проверь все три варианта Награда. +20 очков секты Испытание 6 Исправь область видимости Исправь код. public class BrokenScope { public static void main(String(] args) { int sectPoints = 10; printPoints(); } public static void printPoints () {
System.out.println(sectPoints); } } Передай seciPomts параметром. Награда: +10 очков секты. Турнирный зачёт. Арена Чистого Кода Создай программу CleanCodeArena.java. В программе должны быть методы: public static int calculateTrainingReward(int level) public static int calculateBattleReward(int level) public static int calculateRankUpCost(int level) public static boolean canRankUp(int sectPoints, int level, boolean active, boolean blocked) public static String calculateRank(int sectPoints) public static void printProfile(String name, int level, int sectPoints, int energy, int stress) В mam 6 . Создай переменные ученика 7 Рассчитай награду за тренировку через метод 8 Добавь очки 9 Проверь возможность повышения ранга через метод. 10 Если можно повысить ранг — повысь уровень и спиши очки 11 . Выведи профиль через метод 12 . Выведи ранг через calculateRank'. Награда +40 очкое; секты. Повреждённый свиток 3. Подмена метода Когда ученики сдавали работы, турнирная доска внезапно мигнула На ней появилась строка Проверка метода calculateRankUpCost... Ожидалось: 1О0 Полученс -100 Ученики загудели Наставник развернул повреждённый свиток: public static int calculateRankUpCost(int level) ( return level * -IOC; Мэй Рефактор нахмурилась.
— Это не похоже на случайную ошибку. Линь Грейдл пожал плечами — Может, кто-то просто не знает математику Наставник долго смотрел на код. — возможно Но странно, что ошибка снова связана с очками и рейтингом Он исправил свиток. public static int calculateRankUpCost(int level) { return level * 100; } Затем повернулся к ученикам. — Запомните когда правило спрятано в одном методе, ошибку легче найти Если бы эта формула была разбросана по всему коду, саботаж мог бы остаться незамеченным гораздо дольше Барьер прорыва: Боевые приемы К вечеру первый этап внутреннего состязания завершился На турнирной доске появились результаты: Арена Чистого Кода завершена Путник: методические техники освоены Мэй Рефактор: высокий уровень чистоты кода Линь (рейдл: высокая скорость, замечания по дублированию Боолянь Истинный: стабильная логика усл^ий Сунь Цикл: сильная серия повторов Твоя табличка вспыхнула Имя: Путник Статус: внешний ученик Корен» духа- пробужден Техника Первого Голоса: освоена Сосуды ци: открыты Печати истины: освоены Выбор тактики: освоен Серия повторов: освоена Боевые приёмы: освоены Очки секты: +499 Барьер прорыва: Боевые приёмы открыт Турнирный рейтинг: участник внутреннего состязания Наставник вышел на середину арены.
— Сегодня вы сделали важный шаг Вы научились не просто писать команды, а оформлять правила р методы Это начало настоящей структуры. Он посмотрел на учеников внимательнее — Но вы также увидели, что повреждённый метод может исказить всю систему Чем больше становится код, тем опаснее ошибки Поэтому следующий этап будет сложнее — До сих пор вы хранили ученика как набор отдельных сосудов имя. уровень, очки, энергия, стресс. Но человек — не россыпь сосудов. Техника — не куча строк. Завтра вы создадите собственную духовную форму. В этот момент турнирная доска снова дрогнула На ней на долю секунды появилась строка: unknown_student вызвал calculateRankUpCost(level) Доступ: разрешён Наставник резко повернулся, но строка уже исчезла. Линь Грейдл нахмурился. Мэй Рефактор тихо сказала — Если неизвестный ученик может вызывать методы рейтинга, значит проблема не только в формулах. Наставник сжал посох. — Верно. Следующий барьер — защита формы и состояния Но сначала вам нужно создать саму форму Ночь опустилась на Секту Java Core В архиве поврежденных свитков стало на один файл меньше Тень без формы После Арены Чистого Кода ученики впервые почувствовали, что отдельные техники складываются ь путь Переменные больше не казались случайными сосудами Условия больше не были странными печатями true и false. Циклы перестали быть бесконечным проклятием. Методы стали боевыми приёмами, которые можно вызвать в нужный момент. Но вечером наставник привел учеников н пустой зап. В центре зала стояло бронзовое зеркало Оно было старым, тусклым и покрытым тонкими трещинами. На раме были F-ыгравированы слова Сила без формы рассеивается. Форма без защиты разрушается Защита без смысла становится клеткой. Наставник указал на зеркало — Подойдите по очереди
Первым вышел Линь Грейдл Зеркало вспыхнуло и показало десятки строк. String пате mt level mt sectPoints mt energy boolean active String secondName int second Level mt secondSectPomts mt secondEnergy boolean secondActive Линь нахмурился — Это мой код? — Это твой стиль, — ответила Мэй Рефактор. — Ты создаёшь учеников как россыпь переменных. — Зато быстро Наставник ударил посохом по полу. Переменные в зеркале закружились, перемешались и слились в хаотичное пятно. Имя одного ученика оказалось рядом с уровнем другого Очки третьего попали в чужую строку. Состояние active вспыхнуло сразу у всех. — Вот что происходит, когда у сущности нет формы, — сказал наставник. — Ты можешь хранить данные. Можешь менять их. Можешь передавать в методы. Но пока они не собраны в цельную духовную форму, мир легко перепутает, кому что принадлежит. Мэй Рефактор тихо произнесла — Значит, следующий шаг — класс. Зеркало отразило одно слово Cultivator Наставник кивнул. — До сих пор вы учили техники Теперь вы начнёте создавать собственные формы мира Где-то в глубине зала снова появилась знакомая тень. На мгновение в зеркале возник силуэт без имени, без ранга и без лица. unknown_student Но теперь тень не исчезла сразу Она смотрела на учеников изнутри зеркала.
— Запомните это, — сказал наста вник — То, у чего нет правильной формы, сложнее защитить. А то. что нельзя защитить, рано или поздно будет повреждено. На следующее утро открывалось Испытание Духовной Формы
Глава 6. Духовная форма После Арены Чистого Кода ученики Секты Java Core несколько дней ходили тише обычного. В Зале Тысячи Повторов ьы учились повторять действия На Арене Чистого Кода — выносить повторяющиеся техники в методы Теперь многие внешние ученики уже могли написать программу, где ученик тренировался, отдыхал, получал очки и повышал ранг. Но утром шестого занятия наставник привёл всех не в привычный двор, а к закрытым бронзовым воротам На них были кыгравированы силуэты людей, зверей, мечей, сосудов и странных духовных печатей. Над воротами горела надпись Павильон Духовной Формы Пинь Грейдл, как обычно, стоял впереди — Наконец-то что-то серьезное. Переменные и методы уже надоели Наставник медленно повернулся к нему — Тот, кому надоели основы, обычно ещё не понял основы Ученики притихли. Наставник коснулся ворот посохом. Металл дрогнул, и створки начали открываться. — До сих пор вы хранили имя ученика в одной переменной уровень — во второй, очки — в третьей, энергию — в четвертой. Но настоящий мир не состоит из россыпи отдельных сосудов В нём есть сущности Ученик — это не просто имя Ученик — это имя, уровень, очки, энергия, состояние и техники Сегодня вы научитесь создавать такие формы сами На полу павильона вспыхнула круглая арена. В центре арены возникла надпись: Второй этап внутреннего состязания: Испытание Духовной Формы 6.1. Почему отдельных сосудов уже мало Наставник поднял свиток с кодом, который ученики писали раньше String studentName = "Путник"; int level = 1; int sectPoints = 90; int energy = 50; boolean active = true; — Это работает, пока ученик один Но что будет, если учеников станет трое9 String studentName1 = "Путник"; int levell = 1;
int sectPointsl = 9C; int energyl = 50; String studentName2 = "Линь Грейдл"; int level2 = 2; int sectPoints? = 130; int energy2 =40; String studentName3 = "Мэй Тест"; int level3 = 1; int sectPoints3 = 110; int епегдуЗ = 70; Один из учеников поморщился. — Уже выглядит неприятно Наставник кивнул. — А если их будет сто9 А если у каждого появится ранг, стресс, список заданий, история побед, штрафы и особые техники9 Ученики молчали. — Россыпь переменных перестаёт быть удобной. Нужно объединить связанные данные и поведение в одну форму. В Java для этого используются классы и объекты. Сухая истина Java Класс — это описание формы объекта. Он задаёт, какие данные и методы будут у объектов этого типа Объект — это конкретный экземпляр класса, созданный в памяти программы. Понятие Простыми словами Пример Класс Чертёж, форма, описание class Cultivator { ... } Объект Конкретная созданная сущность new Cultivator(...) Поле Данные внутри объекта name, level. sectPoints Метод объекта Действие, которое выполняет объект train(), rest(), rankUp() 6.2. Первый класс Cultivator Наставник провёл рукой по воздуху, и перед учениками появился первый свиток духовной формы. public class Cultivator { String name; int level; int sectPoints; int energy; boolean active; }
— Это класс Cultivator. Пока в нем только поля Поля — это переменные, которые принадлежат объекту. В этом классе описано, что у каждого культиватора есть • имя — name; • уровень — level; • очки секты — sectPoints: • энергия — energy; • признак активности — active. — Но сам класс ещё не является конкретным учеником. Это только форма Чтобы появился ученик, нужно создать объект. Создание объекта через new Объект создается через ключевое слово new Cultivator student = new Cultivator(); Эта строка делает две вещи: 4 создаёт новый объект класса Cultivator; 5 . сохраняет ссылку на него в переменную student. Теперь можно заполнить поля объекта. student.name = "Путник"; student.level = 1; student.sectPoints = 90; student.energy = 50; student.active = true; Доступ к полю объекта выполняется через точку student.name student.level student.sectPoints Важно: точка означает: возьми объект слева и обратись к его полю или методу справа. Полный первый пример public class CultivatorDemo { public static void main(String!] args) { Cultivator student = new Cultivator(); student.name = "Путник"; student.level = 1; student.sectPoints = 9C; student.energy = 50;
student.active = true; System.cut.println("HMs: " + student.name); System.out.println("Ур эень: " + student.level); System.out.println("Очки секты: " + student.sectPoints); System.out.println("Энергия: " + student.energy); System.out.println("Активен: " + student.active); } ) Если класс Cultivator находится в том же проекте, программа сможет создать объект и вывести его данные. Наставник посмотрел на учеников — Теперь у нас не пять отдельных сосудов, а одна духовная форма, внутри которой эти сосуды связаны. Несколько объектов одного класса Один класс может породить много объектов Как одна форма меча может быть использована для создания множества мечей, так один класс может описывать множество учеников Cultivator first = new CultivatorO; first.name = "Путник"; first.level = 1; first.sectPoints = 90; Cultivator second = new CultivatorO; second.name = "Линг Грейдл"; second.level = 2; second.sectPoints = 130; first и second — это разные объекты. У них один класс, но свое состояние System.out.println(first.name); / /7j ни System.out.println(second.name); / 'ин; Грейдл Линь Грейдл усмехнулся — Наконец-то у меня отдельный объект Надеюсь, с лучшими характеристиками. — Пока у тебя только больше самоуверенности, — ответил наставник. — Ее мы в поля не записывали 6.3. Методы объекта Поля хранят состояние Методы описывают поведение Если культиватор умеет тренироваться, лучше дать это действие самому объекту. public class Cultivator ( String name;
int level; int sectPoints; int energy; boolean active; void train() { sectPoints += 15; energy -= 10; System.out.println(name + " тренируется."); } void rest() { energy += 20; System.out.println(name + " отдыхает."); } } Теперь можнс вызвать методы объекта Cultivator student = new Cultivator(); student.name = "Путник"; student.level = 1; student.sectPoints = 90; student.energy = 50; student.active = true; student.train (); student.rest(); Метод train() работает с полями того объекта, у которого он вызван Сухая истина Java: метод объекта может обращаться к полям этого объекта напрямую по имени: name, level, sectPoints, energy. Эти поля принадлежат конкретному объекту. Метод с возвращаемым значением Метод может не только выполнять действие, но и позвращать результат. boolean canRankUpO { return active sectPoints >= 100 && level < 10; } Теперь проверка повышения ранга становится понятнее if (student.canRankUp()) { System.out.println("Можнс повесить ранг."); ) Наставник сказал: — Хороший метод похож на понятную технику Его имя должно говорить, что он делает. Метод rankUp
Добавим метод повышения ранга. void rankUpO ( if (canRankUp()) { level += sectPoints -= 100; System.out.println(name + " повысил ранг!"); } else { System.out.println(name + " пока не может повысить ранг."); } } Теперь логика повышения хранится внутри класса Cultivator, а не разбросана по программе. student.rankUpO ; Это чажный шаг: объект не просто хранит данные, он сам умеет выполнять действия, связанные со своим состоянием. 6.4. Конструктор Пока объект создаётся пустым, а потом заполняется вручную. Cultivator student = new Cultivator!); student.name = "Путник"; student.level = 1; student.sectPoints = 90; student.energy = 50; student.active = true; Это неудобно и опасно Можно забыть заполнить важное поле Поэтому в Java есть конструктор. Конструктор вызывается при создании объекта и задаёт начальное состояние public class Cultivator ( String name; int level; int sectPoints; int energy; boolean active; Cultivator(String name) { this.name = name; this.level = 1; this.sectPoints = (; this.energy = 50; this.active = true; } ) Теперь объект создаётся так: Cultivator student = new Cultivator("Путник"); Конструктор сразу задаёт стартовые значения
— Это похоже на ритуал рождения духовной формы, — сказал наставник, — Объект не должен появляться в мире полностью пустым, если его состояние важно. Что такое this В конструкторе есть строка. this.name = name; Слева this.name — поле текущего объекта. Справа name — параметр конструктора, this означает “этот объект' Запись Значение this.name попе name текущего объекта name this.level this.train() параметр конструктора или метода поле level текущего объекта вызов метода текущего объекта Если имена поля и параметра совладают, this помогает отличить их Cultivator(String name) { thxS.name = name; ) Без this строка выглядела бы так. name = name; И стала бы бесполезной: параметр присваивается сам себе Дефолтные значения Если поле объекта не заполнить t-ручную и не задать в конструкторе, Java присвоит ему значение по умолчанию. Тип поля int long doable boolean char string и другие объекты Дефолтное значение в 0 0.0 false символ с кодом О null Это не всегда хорошо Например Cultivator student = new CultivatorO; System.out.println(student.name); '/ n Z
null означает, что ссылки на объект нет. Позже, в главе об искажениях ци, ты узнаешь, почему это может привести к NullPointerException. Совет старейшины: не надейся на дефолтные значения там, где объект должен иметь осмысленное состояние. Лучше задавай важные поля через конструктор. 6.5. Отличие класса от объекта Наставник поставил перед учениками деревянную форму для меча и настоящий меч. — Форма не режет. Меч режет Но без формы ты не создашь много одинаково устроенных мечей Класс — это форма Объект — это конкретная сущность, созданная по этой форме. Класс Объект Описание Существует как код Один класс может быть основой для многих объектом Cultivator Конкретный экземпляр Существует во чремя работы программы Каждый объект имеет своё состояние new Cultivator("nyTHHK") Повреждённый свиток. Форма без рождения На стене появился код. public class BrokenForm { public static void main(String[] args) { Cultivator student; student.name = "Путник"; student.train(); } } Что не так? Переменная student объявлена, но объект не создан. Нельзя обращаться к полям и методам объекта, которого нет. Правильно: Cultivator student = new Cultivator("Путник"); student train(); — Нельзя тренировать тень ученика, — сказал наставник. — Сначала создай духовную форму Турнирное испытание 2. Испытание Духовной Формы
Для второго этапа внутреннего состязания каждый ученик должен представить свою духовную форму Создай класс Cultivator со следующими требованиями • поля name,level, sectPoints, energy, active; ♦ конструктор принимает имя ученика; • стартовый уровень равен 1; • стартовая энергия равна 50: • метод train() добавляет 15 очков и отнимает 10 энергии, • метод rest() добавляет 20 энергии; • метод canRankup() возвращает true, если очков не меньше 100; • метод rankup() повышает уровень и списывает 100 очков. * метод printProf ile() выводит профиль ученика Награда +40 очкоь секты. 6.6. Испытания главы Испытание 1. Создай форму Создай класс Cultivator с полями name, level, sectPoints, energy. Награда. +10 очков секты. Испытание 2. Создай двух учеников Создай два объекта: Путник и Линь Грейдл. Выведи их имена и уровни. Награда: +10 очков секты. Испытание 3. Добавь методы Добавь методы train() и rest(). Проверь, что они меняют поля объекта. Награда +15 очков секты. Испытание 4. Конструктор Добавь конструктор, который принимает имя и задаёт стартовые значения. Награда. +15 очков секты. Испытание 5. Повышение ранга
Добавь методы canRankUpQ и rankUp(). Проверь сценарий, где очкоь хватает, и сценарий, где очков не хватает. Награда: +20 очков секты Барьер прорыва: Объектное мышление К концу этой главы гы знаешь. • класс описывает форму объекта; • объект создается через new, • поля хранят состояние объекта; • методы описывают поведение объекта; • конструктор задаёт начальное состояние. • this означает текущий объект; • дефолтные значения существуют, но не всегда безопасны; • один класс может породить много обьектов Имя: Путник Статус: внешний ученик Духовная форма: создана Барьер прорыва: Объектное мышление открыт Турнирный рейтинг: допущен ко второму этапу Когда занятие закончилось, ученики один за другим представляли свои духовные формы. У кого-то объект умел только хранить имя У кого-то уже тренировался, отдыхал и повышал ранг. Линь Грейдл представил форму с огромным количеством очков и уровнем 99. — Впечатляет, — сказал наставник. — А теперь покажи код конструктора Линь Грейдл замолчал Наставник медленно поднял бровь. — Понятно Значит, духовная форма красивая, но состояние ничем не защищено. Завтра поговорим о том, почему открытые меридианы зедут к повреждению ци
Глава 7. Защита состояния Утро началось не с колокола Утро началось с крика — У меня минус тысяча очков1 Во внутреннем дворе ученики толпились у турнирной доски. Рейтинг мерцал, строки перескакивали, некоторые имена исчезали, а затем появлялись снова Внизу доски горела уже знакомая строка unknown_student: -999 очков Но теперь она была не одна Путник: 184 очка Линь Грейдл: 999999 очков Мэй Тест: -1000 очков unknown_student: -999 очков Наставник Павильона Основ стоял перед доской с очень спокойным лицом. Это было плохим знаком Старшие ученики говорили, что когда наставник кричит, опасность мала. Когда он спокоен — кто-то сейчас узнает истину. — Вчера вы создали духовные формы. Сегодня зы увидели, что форма без защиты может быть повреждена Он развернул свиток с кодом public class Cultivator { public String name; public int level; public int sectPoints; public int energy; public boolean active; } — Все поля открыты Любой внешний код может сделать так: student.name = student.level = 99 ; student.sectPoints = -1000; student.energy = -500; student.active = true; Ученики переглянулись. — Это и есть открытые меридианы Ци выходит наружу, грязная ци входит внутрь, а потом вы удивляетесь, почему духовная форма треснула Проблема public-полей Если поля объекта публичные, любой код может изменить их напрямую
Cultivator student = new Cultivator("Путник"); student.sectPoints = -lOOu; student.level = 999; student.name = С точки зрения Java такой код может быть допустимым, если поля public. Но с точки зрения логики мира он ломает объект У ученика не должно быть пустого имени Уровень не должен прыгать с 1 на 999. Очки секты не должны становиться отрицательными, если правила мира это запрещают Сухая истина Java: класс должен защищать своё состояние. Объект не должен позволять внешнему коду записывать в себя любые значения без проверки. 7.1 Способы защиты меридиан private: закрытые меридианы Чтобы закрыть поля от прямого доступа, используется private. public class Cultivator ( private String name; private int level; private int sectPoints; private int energy; private boolean active; } Теперь внешний код не сможет сделать так: student.sectPoints = -1000; шибка компиляции Поле sectPoints доступно только внутри класса Cultivator — Это не жадность класса, — сказал наставник. — Это защита формы. Пусть объект сам решает, какие изменения допустимы Конструктор как первый страж Состояние объекта нужно проверять уже при создании public Cultivator(String name) { if (name == null || name.isBlank()) { throw new IllegalArgumentException("Имя ученика не должно быть пустым"); } this.name = name; this.level = 1; this.sectPoints = 0; this.energy = 50; this.active = true; } Здесь конструктор запрещает создать ученика без имени.
Пока не важно глубоко понимать throw и IllegalArgunientException Подробно исключения будут в главе об искажениях ци Сейчас главное понять идею, если значение недопустимо, класс не должен молча принимать его Getters: безопасное чтение Если поля закрыты, как получить их значения? Через методы чтения — getters. public String getNameO { return name; public int getLevelO ( return level; } public int getSectPoints() { return sectPoints; } public int getEnergyO { return energy; ) public boolean isActiveO { return active; Теперь внешний код может читать данные, но не может напрямую их ломать: System.out.println(student.gerName()); System.out.println(student.getSectPoints ()); Для boolean часто используют имя isActive(), а не getActive(). Setters: контролируемое изменение Setter — это метод, который меняет значение поля Но хороший setter не просто присваивает значение. Он проверяет его. public v^id setName(String name) { if (name == null I I name .isBJ ank ()) { throw new IllegalArgunientException("Имя ученика не должно быть пустым"); } this.name = name; ) Пример setter для энергии: public void setEnergy(int energy) { if (energy < 0) ( this.energy = 0; } else { this.energy = energy;
Ho setter нужен не всегда. Иногда лучше не давать внешнему коду напрямую менять поле вообще Совет старейшины: не создавай setter автоматически для каждого поля. Сначала спроси: должен ли внешний код иметь право менять это значение? Public-методы вместо прямого изменения Вместо setSectPoints(int) лучше дать объекту осмысленные действия добавить очки, потратить очки, повысить ранг. public v<.id addPoints (int points) { if (points < 0) { throw new IllegalArgumentException("Нельзя добавить отрицательные очки"); ) this.sectPoints += points; public boolean spendPoints(int points) ( if (points < 0) { throw new IllegalArgumentException("Нельзя потратить отрицательные очки"); if (sectPoints < points) { return false; sectPoints -= points; return true; Так объект сам защищает свое состояние Внешний код больше не говорит student sectPoints = student sectPoints - 100; Он говорит student.spendPoints(lO0); Это читается лучше и безопаснее. 7.2. Неизменность ци final: неизменяемые значения Ключевое слово final запрещает повторно присвоить значение переменной final int maxLevel = lr ; // maxLevel = 20; // ошибка
Для полей final означает, что значение должно быть задано один раз — обычно в конструкторе private final String id; public Cultivator(String id, String name) { this.id = id; this.name = name; После создания объекта id уже нельзя поменять Сухая истина Java: final не всегда делает объект полностью неизменяемым. Но оно запрещает повторное присваивание самой переменной или поля. Константы: фиксированные правила мира В коде часто встречаются числа, которые являются правилами мира стоимость повышения ранга, максимум уровня, награда за тренировку. Плохо. if (sectPoints >= 100) { level += 1; sectPoints -= 100; ) Что такое 100? Стоимость повышения? Максимум очков9 Награда? Через месяц ты сам можешь забыть Лучше вынести правило в константу: private static final int RANK_UP_COST = 100; private static final int MAX LEVEL = 10; private static final int TRAIN_REWARD = 15; private static final int TRAIN_ENERGY_COST = 10; Константы обычно пишут большими буквами через подчёркивание. — Фиксированные правила мира должны быть названы, — сказал наставник. — Иначе это не правило, а число, упавшее с неба static: общее для класса static означает, что поле или метод принадлежит классу, а не конкретному объекту. Например, стоимость повышения ранга одинакова для всех учеников private static final int RANE UP COST = 100; Это не личное состояние конкретного ученика Это правило класса Cultivator. А вот name, level, sectPoints — не должны быть static, потому что у каждого ученика они свои.
Данные Должны быть static? Почему name нет у каждого ученика свое имя level нет у каждого ученика свой уровень sectPoints нет у каждого ученика свои очки RANK_UP_COST да стоимость повышения общая для всех MAX_LEVEL да максимальный уровень — правило мира Опасность: не используй static как замену объектам Если сделать level static, уровень станет общим для всех учеников, что почти всегда ошибка. enum: фиксированный набор вариантов В прошлых главах мы использовали строки для действий String action = "TRAIN"; Но строки опасны: String action = "TRIAN"; Компилятор не знает, что ты ошибся Для него это просто другая строка Если набор вариантов фиксирован, лучше использовать enum. public enum Action { TRAIN, REST, BATTLE, MEDITATE Теперь действие можно хранить так. Action action = Action.TRAIN; И использовать в switch: switch (action) { case TRAIN -> student.train (); case REST -> student.rest(); case BATTLE -> student.battle(); case MEDITATE -> student.meditate(); Опечатка чроде Action.trian не скомпилируется. Ошибка будет найдена раньше. Ранги ученика тоже лучше хранить не строкой, a enum. public enum Rank { OUTEP STUDENT, INNER’STUDENT, TOURNAMENT CANDIDATE, SECT REPRESENTATIVE
Теперь поле ранга. private Rank rank; И в конструкторе this.rank = Rank.OUTER STUDENT; Преимущество; нельзя случайно записать "OUTER_STUDNET" с ошибкой. 7.3. Безопасная духовная форма Соберем новую версию класса public class Cultivator { private static final int MAX LEVEL = 10; private static final int RANK UP COST = 100; private static final int TRAIN REWARD = 15; private static final int TRAIN ENERGY COST = 10; private static final int REST ENERGY RESTORE = 20; private final String id; private String name; private int level; private int sectPoints; private int energy; private boolean active; private Rank rank; public Cultivator(String id, String name) { if (id == null || id.isBlank()) ( throw new IllegalArgumentException("ID ученика не должен быть пустым"); ) if (name == null || name.isBlank()) { throw new IllegalArgumentException("Имя ученика не должно быть пустым"); ) this.ia = id; this.name = name; this.level = 1; this.sectPoints = 0; this.energy = 50; this.active = true; this.rank = Rank.OUTER STUDENT; } public void train() { if (!active) { System.out.println(name + " неактивен и не мжет тренироваться."); return; ) if (energy < TRAIN ENERGY COST) { System.out.println(name + " слишком устал дла тренировки."); return; } sectPoints += TRAIN REWARD;
energy -= TRAIN_ENERGY_COST; public void rest() { energy += REST_ENERGY RESTORE; } public boolean canRankUp() { return active && sectPoints >= RANK_UP_COST && level < MAX_LEVEL; } public boolean rankUpO { if (!canRankUp()) { return false; } level += 1; sectPoints -- RANK UP COST; return true; } public String getld() ( return id; public String getNameO { return name; } public int getLevel() { return level; public int getSectPoints() { return sectPoints; public int getEnergyO { return energy; I public boolean isActiveO { return active; public Rank g«tRank() ( return rank; И enum Rank: public enum Rank { OUTER STUDENT, INNEP STUDENT, TOURNAMENT CANDIDATE, SECT_REPRESENTATIVE
Теперь объект защищён лучше. Его сложнее сломать случайно и сложнее повредить намеренно. Повреждённый свиток. Открытые меридианы Исправь класс public class BrokenCultivator { public String name; public int level; public int sectPoints; public BrokenCultivator(String name) { this.name = name; this.level = 1; this.sectPoints = 0; } ) Требования: • сделай поля private; * запрети пустое имя в конструкторе. • добавь getters; * добавь метод addPoints(int points); • запрети добавление отрицательных очков; * добавь константу rank_up_cost; • добавь метод rankup() Турнирное испытание 3. Защита формы На третьем этапе внутреннего состязания старейшины пытаются сломать духовные формы учеников Твоя задача — создать класс, который не позволит: • создать ученика без имени; • задать отрицательные очки; * потратить больше очков, чем есть; • повысить уровень выше максимального; * использовать строку вместо фиксированного ранга. Награда: +56 очков секты.
7.4. Испытания главы Испытание 1. Закрой поля Переделай поля Cultivator на private Проверь, что ннешний код больше не может менять их напрямую. Награда: +10 очков секты. Испытание 2. Добавь getters Добавь методы чтения для имени, уровня, очков и энергии. Награда +10 очков секты. Испытание 3. Валидация имени Запрети создание ученика с null или пустым именем. Награда +15 очков секты. Испытание 4, Константы правил Вынеси стоимость повышения ранга, максимальный уровень и награду за тренировку п private static final константы Награда +15 очков секты. Испытание 5, enum Action Создай enum Action со значениями train, REST, battle, meditate Замени строку действия на enum. Награда: +20 очков секты Испытание 6 enum Rank Создай enum Rank и добавь поле ранга в Cultivator Награда. +20 очкон секты. Барьер прорыва: Защита состояния К концу главы ты знаешь
• private скрывает поля от прямого доступа; • getters позволяют безопасно читать состояние; • setters должны проверять входные значения, но нужны не всегда. * осмысленные public-методы лучше прямого изменения полей; • инварианты — правила корректного состояния объекта; • final запрещает повторное присваивание; • static относится к классу, а не к конкретному объекту; • константы фиксируют правила мира; • enum защищает от случайных строк и опечаток. Имя: Путник Статус: внешний ученик Духовная форма: создана Меридианы состояния: защищены Фиксированные правила мира: закреплены Барьер прорыва: Защита состояния открыт Вечером наставник снова подошёл к турнирной доске Строка unknown_student исчезла, но в логах остался след: кто-то пытался записать отрицательные очки напрямую. — После сегодняшнего занятия такой удар уже не пройдёт, — сказал он. Он посмотрел на учеников — Но тот, кто повредил рейтинг, знает основы. Значит, дальше он попробует сломать не поля. Он попробует сломать связи между формами.
Глава 8. Наследие секты На восьмой день внешних учеников привели к Павильону Наследия. Он отличался от остальных залов Здесь не было одной арены. Вместо нее стояли четыре помоста. На первом помосте висел меч. На втором — алхимическая печь На третьем — связка архивных свитков На четвертом — бронзовое зеркало, отражающее ошибки Наставник остановился между помостами — До сих пор вы создавали одного культиватора Но в секте нет одного единственного пути Есть воины, алхимики, архивариусы, стражи качества Все они ученики, но тренируются по-разному. На доске внутреннего состязания вспыхнул новый этап: Турнирное испытание 4: Испытание Школ Путей Линь Грейдл сразу шагнул к помосту с мечом. — Очевидно, сильнейший путь —путь воина. Девушка с короткими волосами, которую ученики звали Мэй Тест, подошла к бронзовому зеркалу. — Сильнейший путь — тот, который первым находит ошибку в твоём пути Несколько учеников тихо сказали — Жёстко Наставник поднял посох. — Сегодня вы узнаете, как описывать общее поведение, разные специализации и контракты, которые обязаны выполнять разные классы. Это путь наследования, абстракций и интерфейсов Представь, что у нас есть воин и алхимик. public class Warriorcultivator ( private String name; private int level; private int sectPoints; public void printProfileО { System.out.println(name); System.out.println(level); System.out.println(sectPoints); } } public class AlchemistCultivator { private String name; private int level;
private int sectPoints; public void printProfile() { System.out.println(name); System.out.println(level); System.out.println(sectPoints); } ) Поля и метод printProfile() повторяются Значит, есть общая часть, которую можно вынести r базовый класс. 8.1. Наследование Наследование позволяет одному классу расширить другой. public class Cultivator ( protected String name; protected int level; protected int sectPoints; public Cultivator(String name) ( this.name = name; this.level = 1; this.sectPoints = j; ) public void printProfile() { System.out.println("Им:: " + name); System.out.println("Уровень: " + level); System.out.println("Очки секты: " + sectPoints); } ) Теперь можно создать класс воина. public class WarriorCultivator extends Cultivator { public WarriorCultivator(String name) ( super (name); } ) WarriorCultivator наследует поля и методы Cultivator Сухая истина Java: extends означает, что один класс наследует другой. Дочерний класс получает доступное состояние и поведение родительского класса и может добавлять своё. super: обращение к родителю В конструкторе дочернего класса есть строка: super(name);
Она вызывает конструктор родительского класса. Если родитель Cultivator требует имя в конструкторе, дочерний класс обязан передать это имя родителю public WarriorCultivator(String name) { super(name); ) super означает обращение к родительскому классу. Запись Значение super(name) вызвать конструктор родителя super.train() вызвать метод train() из родителя super.printProfile() вызвать родительский printProfile() protected: доступ для наследников В примере поля были protected, protected Suring name; protected int level; protected int sectPoints; protected означает, что поле доступно внутри класса, внутри наследников и внутри того же package. Но использовать protected нужно осторожно. Если состояние должно быть строго защищено, лучше оставить поля private и дать наследникам методы для работы с ними. Совет старейшины: для учебного примера protected помогает увидеть наследование. В реальном коде часто безопаснее держать поля private и открывать поведение через методы. 8.2. Переопределение методов Разные типы учеников могут тренироваться по-разному. Базовый класс: public class Cultivator ( protected String name; protected int level; protected int sectPoints; public Cultivator(String name) ( this.name = name; this.level = 1; this.sectPoints = 0; public void train() (
sectPoints += 10; System.cut.println(name + " ь.пспняет обычную тренировку."); } } Воин переопределяет тренировку: public class WarriorCuitivator extends Cultivator { public WarriorCuitivator(String name) { super(name); ) (^Override public void train() { sectPoints += 20; System.out.println(name + " тренирует бое; ую технику меча."); } I Алхимик тоже переопределяет тренировку; public class Alchemistcultivator extends Cultivator { public Alchemistcultivator(String name) { super(name); } ^Override public void train() { sectPoints += 12; System.out.println(name + “ изучает алхимические формулы."); ) ) Аннотация ^Override показывает, что метод переопределяет метод родителя Если ошибиться в имени метода или параметрах, компилятор поможет найти ошибку Полиморфизм: разные формы, одна команда Теперь можно работать с разными учениками через общий тип Cultivator. Cultivator first = new WarriorCuitivator("Лиш Грейдл"); Cultivator second = new Alchemistcultivator("Мэй Алхимик"); first.train(); second.train 0; Хотя переменные имеют тип Cultivator, вызовется версия train() конкретного объекта. Результат может быть таким: Линь Грейдл тренирует боевую технику меча. Мэй Алхимик изучает алхимические формулы. — Это и есть полиморфизм, — сказал наставник. — Одна команда Разные формы. Разное поведение.
Почему глубокие иерархии опасны Пинь Грейдл поднял руку — Значит, можно сделать WarriorCultivator, ПОТОМ FireWarriorCultivator, ПОТОМ FireSwordWarriurCultivator, потом EliteFireSwordWarriorCultivator... Наставник закрыл глаза — Можно. Но не нужно. Слишком глубокая иерархия быстро становится сложной. Cultivator 1—WarriorCultivator 1—FirewarriorCultivator 1—FireSwordWarriorCultivator 1— EliteFireSwordWarriorCultivator Проблемы: • сложно понять, где находится нужная логика; • изменение родителя может неожиданно сломать наследников, • не всегда отношение действительно означает ‘является"; • часто лучше использовать композицию или интерфейсы. Правило: наследование подходит, когда дочерний класс действительно является частным случаем родителя WarriorCultivator является Cultivator. Но не всё, что похоже, должно наследоваться. 8.3. Абстрактный класс или интерфейс abstract class: неполная форма Иногда базовый класс не должен использоваться сам по себе. Например, абстрактный культиватор — это слишком общее понятие В турнире участвуют конкретные специализации Тогда класс можно сделать абстрактным public abstract class Cultivator { protected String name; protected int level; protected int sectPoints; public Cultivator(String name) { this.name = name; this.level = 1; this.sectPoints = 1 ; }
public abstract void train(); public void printProfile() { System.out.println("Имя: " + name); System.out.println("Ур ;вень: " + level); System.out.println("Очки секты: " + sectPoints); ) ) Теперь нельзя создать просто new Cultivaror(.. Cultivator с = new Cultivator("Путник"); // нельзя Но можно создать наследника, который реализует абстрактный метод train(). public class WarriorCultivator extends Cultivator { public WarriorCultivator(String name) { super(name); ) ^Override public void train () ( sectPoints +- 2C; System.out.println(name + " тренирует меч."); } ) interface: контракт Интерфейс — это контракт. Он говорит класс, который реализует этот интерфейс, обязан иметь определенные методы. public interface Trainable { void train(); } Класс может реализовать интерфейс: public class WarriorCultivator implements Trainable { private String name; public WarriorCultivator(String name) ( this.name = name; ) @Override public void train() { System.out.println(name + " тренирует меч."); ) ) Теперь любой объект, который реализует Trainable, можно тренировать через общий контракт public static void startTraining(Trainable student) { student.train(); }
Методу start Training не южно, кто перед ним: воин, алхимик или страж качества Важно только, что объект умеет train() Абстрактный класс или интерфейс Вопрос Abstract class Interface Есть общее состояние9 да, подходит обычно нет Есть общий код методов? да, подходит можно default, но осторожно Нужно описать способность? не г-сегда да, подходит Класс может наследовать нет, только один класс да, можно несколько несколько? интерфейсов Пример Cultivator Trainable, QuestCompletable Простое правило: * abstract class используй для общей основы родственных классов: • interface используй для способности или контракта Интерфейс QuestCompletable Добавим контракт для тех, кто может выполнять задания public interface QuestCompletabie { int completeQuest(int difficulty); } Воин может получать больше очков за сложные боевые задания. public class WarriorCuitivator extends Cultivator implements QuestCompletable { public WarriorCuitivator(String name) ( super(name); ) ^override public void train () { sectPoints += 20; System.out.println(name + " тренирует меч."); } @0verride public int completeQuest(int difficulty) { int reward = difficulty * 11 ; secePoints += reward; return reward; } ) Алхимик может получать другую награду public class AlchemistCultivacor extends Cultivator implements QuestCompletable { public Alchemistcultivator(String name) { super(name); }
@Override public void train() { sectPoints += 12; System.cut.println(name + " изучает алхимию."); } @Override public int completeQuest(int difficulty) { int reward = difficulty * 8 + 5; sectPoints += reward; return reward; } ) Зависимость от интерфейса Плохо, если метод работает только с конкретным классом без необходимости public static void runQuest(WarriorCultivator warrior) { warrior.completeQuest(3); ) Такой метод принимает только воина. Лучше зависеть от интерфейса: public static void runQuest(QuestCompletable participant) ( int reward = parti cipant.completeQuest(3); System.out.println("Награда: " + reward); ) Теперь метод принимает любого участника, который умеет выполнять задания. — Зависеть от контракта, а не от конкретной формы, — сказал наставник. — Это одна из основ гибкого кода Где интерфейс не нужен Не нужно создавать интерфейс просто потому, что ты узнал о них. Плохо puclic interface NameGettable { String getNameO; ) Если интерфейс не даёт реальной гибкости, не упрощает зависимость и не описывает важный контракт, он только добавляет шум Совет старейшины: интерфейс нужен, когда у тебя есть несколько реализаций или когда код должен зависеть от способности, а не от конкретного класса. Полный пример: школы путей
public abstract class Cultivator { protected String name; protected int level; protected int sectPoints; public Cultivator(String name) { this.name = name; this.level = 1; this.sectPoints = 0; } public abstract void train (); public void printProfile () { System.out.println("Имя: " + name); System.out.println("Уровень: " + level); System.out.println("Очки секты: " + sectPoints); } } public interface QuestCompletable { int completeQuest(int difficulty); } public class WarriorCultivator extends Cultivator implements QuestCompletable ( public WarriorCultivator(String name) { super(name); ) ^Override public void train() { sectPoints += 20; System.out.println(name + " тренирует меч."); J @Override public int comp]eteQuest(int difficulty) { int reward = difficulty * 10; sectPoints += reward; return reward; } } public class AlchemistCultivator extends Cultivator implements QuestCompletable { public AlchemistCultivator(String name) ( super(name); ) @Override public void train() { sectPoints += 12; System.out.println(name + " изучает алхимиг."); ) @Cverride public int completeQuest(int difficulty) { int reward = difficulty *8 + 5; sectPoints += reward; return reward;
Демонстрация: public class PathDemo ( public static void main(String!] args) { Cultivator warrior = new WarriorCuitivator("Лин^ Грейдл"); Cultivator alchemist = new AlchemistCultivatcr("Мэй Алхимик"); warrior.train (); alchemist.train(); runQuest((QuestCompletable) warrior); runQuest((QuestCompletable) alchemist); } public static void runQuest(QuestCompletable participant) ( int reward = parti cipant.completeQuest (3); System.out.println("Награда за задание: " + reward); } ) Примечание: приведение типов в этом примере показано для простоты. В следующих главах, когда появятся коллекции, можно будет хранить участников сразу как QuestCompletable ИЛИ Cultivator 3 списках Повреждённый свиток Неверное наследование На стене появился код public class RewardBox extends Cultivator { public RewardBox(String name) { super(name); } ) Что не так? RewardBox — это коробка наград Она не является культиватором Значит, наследование здесь неверное Правильнее сделать отдельный класс public class RewardBox ( private String title; private int points; public RewardBox(String title, int points) { this.title = title; this.points = points; } }
— Наследование — это не способ получить чужие поля бесплатно, — сказал наставник. — Это утверждение: один тип является другим. Если утверждение ложно, код будет лгать. Турнирное испытание 4. Испытание Школ Путей Создай несколько клэссое- учеников • WarriorCultivator — получает больше очков за тренировку; • AichemistCultivaTor — получает меньше очков за тренировку, но больше за задания с бонусом, • Guardiancultivator — получает стабильную награду и меньше штрафов. Требования: • общая основа должна быть в abstract class Cultivator; * метод train() должен быть абстрактным; * каждый наследник должен реализовать train() по-своему; • создай интерфейс QuestCompletable; • минимум два класса должны реализовать QuestCompletable; • создай метод, который принимает QuestCompletable, а не конкретный класс. Награда 1-69 очков секты. 8.4. Испытания главы Испытание 1. Базовый класс Создай базовый класс Cultivator с полями name, level, sectPoints и методом printProfile(). Награда: +10 очков секты. Испытание 2. Наследники Создай WarriorCultivator и Alchemistcultivator, которые наследуются от Cultivator. Награда: +15 очкок секты Испытание 3. Переопределение train Добавь метод train() в базовый класс и переопредели его в наследниках. Награда: +20 очков секты.
Испытание 4. Абстрактный класс Сделай Cultivator абстрактным, a train() — абстрактным методом. Награда: +20 очков секты. Испытание 5. Интерфейс Создай интерфейс QuestCompletable с методом comp]eteQuest(int difficulty) Реализуй его в двух классах. Награда: +25 очков секты. Испытание 6. Исправь неверное наследование Придумай пример класса, который не должен наследоваться от Cultivator, и объясни почему. Награда +10 очков секты. Барьер прорыва: Наследие секты К концу главы ты знаешь • extends создаёт наследование; • super обращается к родительскому классу; • (SOverride помогает безопасно переопределять методы; • полиморфизм позволяет работать с разными объектами через общий тип: • глубокие иерархии могут ухудшить код; • abstract class подходит для общей основы родственных классов; * interface описывает контракт или способность; • код часто лучше зависит от интерфейса, а не от конкретной реализации; • интерфейс не нужен без причины Имя: Путник Статус: внешний ученик Духовная форма: создана Меридианы состояния: защищены Школа пути: выбрана Наследие секты: освоено Барьер прорыва: Наследие секты открыт Турнирный рейтинг; кандидат внутреннего отбора
Когда испытание закончилось, наставник долго смотрел на учеников. — Теперь у каждого из вас есть форма, защита и путь. Но внутреннее состязание ещё не закончено. До сих пор мы работали с одним учеником или несколькими объектами вручную. На следующем этапе вам придется управлять десятками участников, хранить их в списках, искать, сравнивать, удалять и считать рейтинг На турнирной доске вспыхнула новая строка: Следующий павильон: Пространственный мешок Внизу доски на мгновение снова появилась старая тень unknown_student: найден дубликат Наставник резко поднял голову — Дубликат'? Он подошел ближе к доске, но строка уже исчезла Мэй Тест тихо сказала — Если есть дубликат, значит кто-то создал две формы с одним именем”? Наставник кивнул. — Или две разные формы, которые программа считает разными, хотя по смыслу это один ученик В следующей арке вы узнаете, почему объекты сравниваются не так, как примитивы. И почему без equals и hashcode даже честный рейтинг может превратиться в хаос Далеко в архиве секты одна повреждённая запись сменила идентификатор. Внутреннее состязание входило в новую фазу Зал Тысячи Имен После испытания наследия секты ученики думали, что худшее позади. Они уже создали духовные формы Научились закрывать состояние Поняли, почему не все нужно наследовать Увидели, как разные пути могут выполнять один контракт. Но когда ворота следующего павильона открылись, перед ними оказался не зал, а огромная площадь. На площади стояли сотни каменных фигур Каждая фигура была учеником. У каждой была табличка, у каждой были очки, ранг, имя и путь развития Путник остановился — Мы должны работать со всеми? Старейшина Архива, сухой человек в серой мантии, появился между статуями так тихо, будто всегда там стоял.
— Один ученик — это модель Десять учеников — это список Сто учеников — это уже система. Пинь Грейдл быстро оглядел площадь. — Можно просто создать сто переменных Мэй Рефактор закрыла глаза — Пожалуйста, не говори так рядом со мной. Старейшина Архива поднял рукав В воздухе появились строки student 1 student2 students student4 students student! 00 Затем строки начали трескаться Некоторые повторились Некоторые исчезли. Некоторые поменялись местами — Когда объектов становится много, — сказал старейшина, — сила отдельной формы уже недостаточна Нужно уметь хранить множество форм, искать среди них. удалять, проверять дубликаты и связывать ключи со значениями На дальней стене площади вспыхнули три древних символа. List Set Map Сунь Цикл уважительно присвистнул. — Пространственные техники. Старейшина Архива кивнул. — Пространственный мешок слабого ученика хранит всё подряд Пространственный мешок сильного ученика знает, где список, где множество, а где карта. В этот момент одна из каменных фигур дрогнула. На её табличке было написано: /£>• ST-000 Name unknown student Фигура стояла среди остальных, но никто не помнил, кто ее добавил. Мэй Рефактор подошла ближе. — Если это дубликат, его надо найти.
Старейшина Архива посмотрел на учеником — Именно. Но сначала вам придётся понять, что значит ‘одинаковый”. Площадь наполнилась звоном каменных табличек. Начиналось испытание Пространственного Мешка
Глава 9. Пространственный мешок На девятый день внутреннего состязания учеников привели к павильону, которого раньше не было видно с главной дороги. Он стоял за Ареной Чистого Кода и Павильоном Наследия, но будто не занимал места Снаружи павильон чыглядел как маленькая башня с одной дверью Однако изнутри доносился шум огромного зала: шаги сотен учеников, шелест свитков, звон табличек, голоса старших архивариусов Над дверью горела надпись: Пространственный мешок Пинь Грейдл прищурился — Выглядит маленьким Мэй Тест посмотрела на дверь и сказала — Значит, внутри точно будет больше, чем снаружи. Как в плохом классе, где одно поле хранит i се подряд. Наставник Павильона Осног кивнул — Хорошее сравнение. Сегодня вы узнаете, как хранить не одного ученика, а множество учеников Как искать нужного. Как убрать дубликат. Как посчитать рейтинг И почему две духовные формы могут выглядеть одинаково, но для Java оставаться разными объектами Он поднял посох. Турнирная доска вспыхнула Внутреннее состязание секты Этап: Рейтинг Внутренней Арены Цель: управлять списком участников, наградами и очками Особое условие: найти дубликат unknown_student В нижней части доски снова мелькнула строка unknown_student: найден дубликат На этот раз строка не исчезла сразу. Она дрожала, будто кто-то удерживал её на доске силой. Наставник произнёс. — До сих пор чы работали с одним объектом или с несколькими объектами вручную. Но турнир не состоит из одного ученика. Нужны списки, множества, карты, поиск, удаление, сравнение и правила уникальности В мире Java для этого есть массивы и коллекции. Раньше можно было написать так:
Cultivator student = new Cultivator ("ST-001", "Путник"); student.jddSectPoincs(50); student.printPrnf Ле(); Это удобно, когда ученик один. Но во внутреннем состязании участвуют десятки учеников Cultivator first = new Cultivator("ST-001", "Путник"); Cultivator second = new Cultivator("ST-002", "Линь Грейдл"); Cultivator third = new Cultivator("ST-003", "Мэй Тест"); Если учеников станет сто, писать переменные first, second, third, fourth, fifth и так далее — путь к хаосу Наставник начертил на камне три вопроса: как пройтись по всем ученикам, как найти ученика по имени или id; как не допустить дубликатов — Когда объектов стан» вится много, — сказал он, — тебе нужен сосуд, который хранит другие сосуды Такой сосуд мы называем коллекцией. Но начнём с более древней формы — массива Сухая истина Java: массив и коллекция позволяют хранить несколько значений или объектов Массив имеет фиксированный размер. Коллекции из java.util обычно удобнее, потому что умеют расти, удалять элементы и дают готовые методы для работы с данными 9.1. Массив, древняя форма хранения Массив хранит несколько элементов одного типа. String[] names = new String[3]; names[0] = "Путник"; names [i] = "Линь Грейдл"; names[2] = "Мэи Тест"; У массива есть индексы. Первый элемент имеет индекс 0, второй — 1, третий — 2 Наставник постучал по первой ячейке каменного стеллажа. — В Java счёт начинается с нуля Это не ошибка Это древний закон массивов Чтобы вывести все имена, можно использовать цикл’ for (int 1=0; i < names.length; i++) { System.out.println(names[i]); ) 'names length' — длина массива Обрати внимание на условие:
। < names,length He i <= names.length, а именно меньше. Если длина массива равна 3, допустимые индексы- 0,1, 2. Индекса 3 нет. Повреждённый свиток. Выход за границу массива String [] names = new String[3]; names Pl = "Путник"; names[1J = "Линь Грейдл"; names[2] = "Мэй Тест"; System.out.println(names(3]); Что случится? Программа упадёт с ArraylnaexOutOIBoundsException, потому что элемента с индексом 3 нет. Наставник сказал. — Массиь похож на ряд закрытых комнат, Если комнат три, нельзя войти в четвертую, даже если очень уверенно идти вперёд Массив объектов Массив может хранить не только строки и числа, но и объекты Cultivator!] participants = new Cultivator[3]; participants[0] = new Cultivator("ST-001", "Путник"); participants[1] = new Cultivator ("ST-002", "Линь Грейдл"); participants[2] = new Cultivator("ST-003", "Мэй Тест"); for (int i = ; i < participants.length; i++) { System.out.println(participants[i].getName()); } Это уже похоже на турнирную доску Но у массива есть ограничение размер задается заранее Если создали массив на 3 участника, четвертого просто так не добавить Нужно создавать новый массив большего размера и переносить элементы. Один ученик спросил: — Значит, массивы плохие? Наставник покачал головой.
— Нет Массивы полезны, особенно когда размер известен и не меняется Но для рейтинга внутреннего состязания, где участников могут добавлять и удалять, чаще удобнее Array List 9.2. Списки ArrayList ArrayList - это коллекция, которая хранит элементы по порядку и может изменять размер. Чтобы использовать ArrayList, нужен импорт import java util ArrayList: import java.util.ArrayList; public class ParticipantListDemo { public static void main(String!] args) { ArrayList<Cultivator> participants = new ArrayListo(); participants.add(new Cultivator ("ST-001", "Путник")); participants.add(new Cultivator ("ST-002", "Линь Грейдл")); participants.add(new Cultivator("ST-ЭОЗ", "Мэй Тест")); System.out.println("yuacTHMKcr.: " + participants.size ()); for (Cultivator participant : participants) { System.out.println(participant.getName()); ) ) ) Разберем важные части ArrayList<Cultivator> participants = new ArrayListo(); Это список, в котором можно хранить только объекты Cultivator. participants.add(...); Добавляет элемент в список. participants.size (); Возвращает количество элементов. for (Cultivator participant : participants) { ) Это улучшенный for. Он удобен, когда нужно пройти по всем элементам коллекции Сухая истина Java ArrayList хранит элементы по порядку В него удобно добавлять элементы, получать элемент по индексу, проходить по списку и удалять элементы Но ArrayList сам по себе не запрещает дубликаты.
Получение, поиск и удаление У списка можно получить элемент по индексу Cultivator first = participants.get(O); System.out.println(first.getName()); Можно удалить элемент: participants.remove ((:) ; Но чаще в реальном коде тебе нужно не первого ученика”, а "ученика с конкретным id” Для этого нужен поиск public static Cultivator findById(ArrayList<Cultivator> participants, String id) { for (Cultivator participant : participants) { if (participant.getldO .eguals (id)) { return participant; } ) return null; ) Наставник остановился на последней строке return null; — Это опасное место Сейчас мы используем null, чтобы показать, ученик не найден Но и следующей главе вы узнаете, почему такая запись может стать дверью для искажения ци. Мэй Тест подняла руку. — А если кто-то потом вызовет метод у результата, не проверив null? Наставник улыбнулся — Тогда программа крикнет. И этот крик будет называться NullPointerException. Линь Грейдл нахмурился. — Вы пугаете заранее. — Я предупреждаю заранее, — поправил наставник. — Это разные техники toSlring: чтобы объект говорил о себе понятно Если просто вывести объект System.out.println(participant); можно получить что-то вроде; Cultivator@5b213361
Это не рейтинг. Это след объекта в памяти, почти бесполезный для человека Чтобы объект выводился понятно, можно переопределить метод toString(). @Override public String toStringO { return id + " | " + name + " очки: " + sectPoints; ) Теперь: System.out.println(participant); может вывести: ST-001 | Путник | очки: 120 Сухая истина Java toString() — метод, который Java вызывает, когда объект нужно представить в виде строки. Его часто переопределяют, чтобы удобнее читать логи, отладку и вывод программы. Записка старейшины toStnngO не должен заменять нормальную бизнес-логику Он нужен для удобного текстового представления объекта Не рассчитывай важные правила внутри toString(). 9.3. Множества Когда ученики начали добавлять участников в список, турнирная доска наконец показала проблему полностью. ST-001 | Путник | очки: 120 ST-002 I Линь Грейдл | очки: 135 ST-003 | Мэй Тест | очки: 128 ST-001 | Путник | очки: 120 Ученики зашумели — Путник записан дважды! — Но это же один ученик! — Почему список разрешил? Наставник сказал: — Потому что ArrayList не отвечает за уникальность Он хранит то, что вы в него положили. Если ьы дважды добавили ученика, он дважды будет в списке Линь Грейдл усмехнулся. — Значит, нужно просто быть внимательнее Мэй Тест ответила
— Просто быть внимательнее" — плохая архитектура Наставник кивнул. — Верно, Если правило важно. его лучше закрепить в коде Для уникальности есть HashSet. HashSet: множество без дубликатом HashSet хранит уникальные элементы. Нужен импорт: import java,util.HashSet; Пример: HashSet<String> names = new HashSet<>(); names.add("Путник"); names.add("Линь Грейдл"); names.add("Путник"); System.out.println(names.size()); 2 Строка "Путник' добавлена дважды, но в HashSet останется только один экземпляр Но с объектами все интереснее. HashSet<Cuitivator> participants = new HashSeto (); participants.add(new Cultivator("ST-001", "Путник")); participants.add(new Cultivator("ST-001", "Путник")); System.out.println(participants.size()); Какой результат? Если в Cultivator не переопределены equals() и hashCode(). результат может быть 2 Ученики удивились — Но id одинаковый! Наставник ответил: — Для вас одинаковый. Для Java это два разных объекта, пока вы не объяснили, по какому правилу их сравнивать. == и equals Оператор == для объектов проверяет, являются ли две ссылки ссылками на один и тот же объект Cultivator first = new Cultivator("ST-001", "Путник"); Cultivator second = new Cultivator("ST-001", "Путник"); System.out.println(first == second); t Почему false? Потому что созданы два разных объекта через new. Даже если внутри у них одинаковые id и name, это разные экземпляры.
Метод equals() отвечает на другой вопрос, равны ли объекты по смыслу Но по умолчанию equals() у обычного объекта недет себя почти как ==. Поэтому для своих классов его часто переопределяют. Сухая истина Java: == для объектов сравнивает ссылки; equals() должен сравнивать смысловое равенство; если переопределяешь equalsQ, почти всегда нужно переопределить и hashCode(); HashSet и HashMap сильно зависят от правильной пары equals/hashCode. equals/hashCode по id Для ученика секты главным идентификатором будет id. Если у двух объектов один id, для рейтинга это один и тот же ученик. import java.util.Objects; public class Cultivator { private final String id; private final String name; private int sectPoints; public Cultivator(String id, String name) { if (id == null || id.isBlank()) ( throw new IllegalArgumentException("ID ученика не должен быть пустым"); ) if (name == null || name.isBlank()) ( throw new IllegalArgumentException("Имя ученика не должно быть пустым"); } this.id = id; this.name = name; this.sectPoints - 0; public String getldf) { return id; public String getNameO { return name; public int getSectPoints() { return sectPoints; } public void addSectPoints(int points) ( if (points < 0) { throw new IllegalArgumentException("Нельзя добавить отрицательные очки"); ) sectPoints += points;
^Override public boolean equals(Object other) { if (this == other) { return true; if (!(other instanceof Cultivator)) { return false; } Cultivator that = (Cultivator) other; return Objects.equals(this.id, that.id); } ^Override public int hashCode() { return Objects.hash(id); ) ^Override public String toStringO { return id + " I " + name * " [ очки: " + sectPoints; } ) Теперь HashSel сможет понять, что два объекта с одинаковым id — это дубликаты. HashSet<Cultivator> participants = new HashSetoQ; participants.add(new Cultivator ("ST-001", "Путник")); participants.addlnew Cultivator("ST-001", "Путник")); participants.add(new Cultivator("ST-002", "Линь Грейдл")); System.out.println(participants.size()); 7 2 Наставник сказал: — Вы только что закрыли один путь саботажа Теперь нельзя тихо добавить того же ученика как новую форму, если id совпадает Скрытое искажение если поле участвует в equals/hashCode. его нельзя бездумно менять после добавления объекта в HashSet или использования как ключа HashMap Поэтому id сделан final. Идентификатор — печать личности ученика. Она не должна плавать HashMap: карта рейтинга HashMap хранит пары ключ-значение. Для рейтинга можно хранить: ключ — id ученика: значение — очки. import java.util.HashMap; HashMap^String, Integer> rating = new HashMap<>(); rating.put("ST-001", 120); rating.put("ST-002", 135); rating.put("ST-003", 128);
System.out.princln(rating.get("ST- Э2")); Если положить значение по уже существующему ключу, старое значение будет заменено rating.put ("ST-001", 120); rating.put("ST-001", 150); System.out.println(rating.get("ST-001")); Это может быть полезно, когда нужно обновить рейтинг. Для безопасного добавления очков часто используют getOrDefauIt. int currentPoints = rating.gecOrDefault("ST-001", 0); rating.put("ST-001", currentpoints + 30); Сухая истина Java HashMap удобен для быстрого поиска по ключу. Если тебе нужно часто находить данные по id, имени, коду или другому уникальному признаку. Мар часто лучше, чем перебор списка. 9.4. Generics: печать типа Когда ты пишешь: ArrayList<Cultivator> participants = new ArrayListo (); часть <Cultivator>‘ называется generics. Она говорит Java: в этом списке должны быть только Cultivator Без generics можно написать raw type’ ArrayList participants = new ArrayListo ; participants.add(new Cultivator ("ST-001", "Путник")); participants.add("Повреждённый свиток"); Такой список принимает всё подряд Ошибка может проявиться позже Cultivator student = (Cultivator) participants.get(1); Если там лежит строка, программа упадет с ClassCastException. Наставник сказал — Raw type — это мешок без печати В него можно бросить пилюлю, меч, ядовитую жабу и чужой ботинок. Но доставать потом придётся тебе Правильно ArrayList<Cultivator> participants = new ArrayListo (); participants.add(new Cultivator ("ST-001", "Путник")); // participants.add("Повреждённый свиток") ; // ошибка компиляции Ошибка находится раньше — на этапе компиляции. Это лучше, чем падение в финале турнира.
Обобщённый RewardBox<T> Generics можно использовать и в своих классах. public class RewardBox<T> { private T reward; public RewardBoxfT reward) { this.reward = reward; } public T getRewardO { return reward; } } Теперь можно создать коробку с разными типами наград: RewardBox<String> scrollBox = new RewardBoxo ("Свиток кони^нтрации"); RewardBox<Integer> pointsBox = new RewardBoxo (50); RewardBox<Cultivator> mentcrBox = new RewardBoxo(new Cultivator("МТ-Э01", "Старший ученик")); Тип T заменяется конкретным типом при использовании. Сухая истина Java genencs позволяют писать классы и методы, которые работают с разными типами, но при этом сохраняют проверку типов на этапе компиляции Список, множество или карта Наставник вызвал на арену трех учеников Первый сказал: — Я буду хранить участников в ArrayList. Порядок важен, дубликаты проверю вручную Второй сказал: — Я буду хранить участником в HashSet. Мне важно, чтобы один ученик не попал дважды. Третий сказал: — Я буду хранить очки в HashMap по id. Мне нужно быстро обновлять рейтинг. Наставник повернулся к залу — Кто прав9 Мэй Тест ответила — Все. если задача соответствует структуре. Наставник кивнул. ArrayList хорош, когда важен порядок и нужен список
HashSet хорош, когда важна уникальность HashMap хорош, когда нужен быстрый доступ п;> ключу — Her единственной лучшей коллекции. — сказал наставник — Есть коллекция, подходящая под правило мира Теперь соберем вместе список участников, уникальность и рейтинг import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; public class InternalArenaRating { public static void main(String(] args) { HashSet<Cultivator> uniqueParticipants = new HashSet<>(); uniqueParticipants.add(new Cultivator("ST-001", "Путник")); uniqueParticipants.add(new Cultivator("ST-002", "Линь Грейдл")); uniqueParticipants.add(new Cultivator("ST-003", "Мэй Тест")); uniqueParticipants.add(new Cultivator("ST-001", "Путник")); ArrayList<Cultivator> participants = new ArrayListo(uniqueParticipants); HashMap<String, Integer> rating = new HashMap<>(); addPoints(rating, "ST-001", 40); addPoints(rating, "ST-002", 35); addPoints(rating, "ST-003", 45); addPoints(rating, "ST-001", 20); for (Cultivator participant : participants) ( int points = racing.getOrDefault(participant.getld(), 0); System.out.println(participant.getName() + ": " + points + " ечков"); } } public static void addPoints(HashMap<String, Integer> rating, String studentld, int points) { int currentpoints = rating.getOrDefault(studentld, 0); rating.put(studentld, currentPoints + points); } ) Этот код делает несколько вещей: HashSet убирает дубликат ученика с id ST 001; ArrayList позволяет пройти по участникам как по списку: HashMap хранит очки по id. getOrdefault защищает от ситуации, когда у ученика ещё нет очков — Это еще не идеальная система рейтинга. Но это уже не россыпв переменных Это структура Перед учениками появился код:
EashSet<Cultivator> participants = new HashSet<>(); participants.add(new Cultivator("ST-001", "Путник")); participants.add(new Cultivator("ST-001", "Путник")); System.out.println(participants.size()); Вопрос: почему размер может быть 2? Ответ класс Cultivator не объяснил Java, что ученики с одинаковым id считаются одним учеником Нужно переопределить equals() и hashCode() по id. 9.5. Записки старейшины 1. Не используй ArrayList как ответ на все вопросы. Если нужна уникальность — подумай о Set. Если нужен поиск по ключу — подумай о Мар 2. Не забывай про equals/hashCode. Если объект используется в HashSet или как ключ и HashMap, правила равенства должны быть понятны и стабильны 3. Не делай raw types. Плохо: ArrayList items = new ArrayList(); Лучше: ArrayList<Cultivator> items = new ArrayList<>(); 4. He меняй id после добавления в HashSet. Если id участвует n hashCode(), он должен быть final или хотя бы не должен изменяться во нремя жизни обьекта в Set'Map. 5. Не путай список учеников и рейтинг. Список участников и очки участников — это разные ответственности. Иногда их можно хранить одном объекте, иногда удобнее разделить. 9.6. Испытания главы Испытание 1. Массив имён
Создай масси-; StnngQ на 5 имен учеников Заполни его и выведи все имена через for. Награда +10 очков секты Испытание 2. ArrayList участников Создай ArrayList<Cultivator> Добавь трёх учеников Выведи количество участников и их имена через улучшенный for. Награда: +15 очков секты. Испытание 3. Поиск по id Напиши метод' public static Cultivator findBy!d(ArrayList<Cultivator> participants, String id) Метод должен вернуть ученика с нужным id или null, если ученик не найден Награда: +20 очков секты Испытание 4. toString Переопредели toString() в Cultivator так, чтобы объект выводился в виде ST-001 | Путник | очки: 120 Награда. +15 очко- секты Испытание 5. HashSet без дубликатов Создай HashSet<Cultivator>. Добавь двух учеников с одинаковым id Реализуй equals/hashCode по id и проверь, что размер множества равен 1. Награда; +30 очког. секты. Испытание 6. Карта рейтинга Создай HashMap<Strmg. Integer^, где ключ — id ученика, значение — очки Напиши метод addPoints()r который добавляет очки к текущему значению через getOrDefaultQ. Награда: +25 очков секты. Испытание 7. RewardBox<T>
Создай класс RewardBox<T> с полем reward и методом getRewardf) Проверь его на String, Integer и Cultivator. Hai рада: +25 очков секты. Испытание старейшины. Рейтинг Внутренней Арены Создай программу InternalArenaRating |ava Требования • создать класс Cultivator с id, name, sectPoints; * id должен быть final; • equals/hashCode должны работать по id; • создать HashSet<Cultivator> и добавить туда участников, включая дубликат; • создать HashMap<String, Integer> для очков; • начислить очки участникам, ♦ вывести рейтинг всех уникальных участников Награда +50 очков секты. Барьер прорыва: Множество Форм К концу главы ты знаешь: • массив хранит фиксированное количество элементов; * ArrayList хранит список и умеет расти; • улучшенный for удобен для перебора коллекций: • HashSet хранит уникальные элементы; • HashMap хранит пары ключ-значение; • == для объектов сравнивает ссылки: • equalsQ задает смысловое равенство; • hashCode() нужен для корректной работы HashSet и HashMap; • generics защищают коллекции от чужих типов; * raw types опасны, потому что переносят ошибку из компиляции в runtime; ♦ toString() помогает читать состояние объекта. Твоя табличка вспыхнула: Имя: Путник Статус: внешний ученик Духовная форма, создана
Меридианы состояния защищены Наследие секты: освоено Пространственный мешок: открыт Множество форм под контролем Очки секты: +559 Турнирный рейтинг: допущен к финальному этапу внутреннего отбора. Когда занятие закончилось, дубликат unknown_student исчез с доски Ученики облегчённо выдохнули. Но наставник не выглядел спокойным — Дубликат мы убрали, — сказал он. — Но это была только первая трещина На доске рейтинга вспыхнул новый лог гешагО=Пилюля концентрации pomts=null target=ST-OO1 operation=addHeward Мэй Тест побледнела. — Награда без очков? Линь Грейдл резко сказал. — Это невозможно У каждой награды должны быть очки. Наставник смотрел на строку слишком долго. — Должны Но программа не живёт словом “должны”. Она живет тем, что вы разрешили в коде В эту ночь главный свиток рейтинга сломался
Глава 10. Ночь сломанного свитка Ночь перед финалом внутреннего состязания должна была быть тихой Ученикам запретили тренироваться после заката Павильоны закрыли Турнирные свитки запечатали Старшие архивариусы проверили список участников, рейтинг и награды Но ровно в полночь над внутренним двором раздался треск Главная турнирная доска погасла, потом вспыхнула красным светом. По ней побежали строки Exception in thread "main" java.lang.NullPointerException at Ratingservice.applyReward(Ratingservice.java:27) at TournamentBoard.recalculate(TournamentBoard.java:14) at Internalcontest.main(Internalcontest.java:8) Ученики выбежали из общежитий Кто-то крикнул — Атака' Кто-то другой — Это секта JavaScript мстит за прошлый турнир' Линь Грейдл стоял у доски с мрачным лицом. — Я говорил, что рейтинг надо было считать быстрее, а не красивее. Мэй Тест смотрела не на красный свет, а на первую строку ошибки — NullPointerException. . Наставник Павильона Основ появился через несколько мгновений Он не выглядел удивленным — Исключение — не враг, — сказал он. — Это крик программы о том, где ци исказилась. Хороший культиватор не закрывает уши Он читает крик 10.1. Что такое null В Java переменная объектного типа хранит ссылку на объект Reward reward = new Reward("Пилюля концентрации", 30); Здесь reward указывает на объект Reward. Нс переменная может не указывать ни на какой объект: Reward reward = null; null означает- ссылки на объект нет Наставник положил на камень пустую табличку. — Представьте, что переменная — это табличка с адресом павильона Если на табличке написан адрес, вы можете пойти туда. Если на табличке null, адреса нет. Но
если вы все равно попробуете открыть дверь, которой не существует, мир ответит ударом. Сухая истина Java null — это специальное значение для ссылочных типов Оно означает отсутствие объекта. У null нельзя вызвать метод и нельзя обратиться к полю Нельзя так: Reward reward = null; System.out.println(reward.getPoints ()); Потому что reward не указывает на объект Reward NullPointerException NullPomterException возникает, когда код пытается обратиться к объекту, но вместо объекта там null. Повреждённый свиток с турнирной доски выглядел так: Reward reward = findRewaгdByName("Пилюля концентрации"); student.addSectPoints(reward.getPoints()); На первый взгляд всё нормально. Но метод fmdRewardByName() может вернуть null, если награда не найдена public static Reward findRewardBvName(ArrayList<Reward> rewards, String nam e) { for (Reward reward : rewards) { if (reward.getName().equals(name)) { return reward; } ) return null; } Если награда не найдена, reward станет null Следующая строка сломается' reward.getPoints() Потому что у null нет метода getPoints(). Мэй Тест говорит; — Значит, ошибка не ь строке addSectPoints Ошибка раньше: программа не проверила, существует ли награда Наставник кивнул. — Иногда место падения — не место причины. Исключение показывает, где программа больше не смогла продолжать. Но расследование должно идти выше. Как читать stack trace Stack trace — это след вызовов методов, который показывает, где возникла ошибка.
Пример Exception in thread "main" java.lang.NullPointerException at Ratingservice.applyReward(RatingService.java:27) at TournamentBoard.recalculate(TournamentBoard.java:14) at Internalcontest.main(Internalcontest.java:8) Читать можно сверху вниз. тип ошибки: NullPointerException, место падения. RatmgService.applyReward, строка 27; кто вызвал этот метод: TournamentBoard.recalculate, строка 14; откуда всё началось. Internalcontest mam, строка 8. Сухая истина Java stack trace не нужно бояться. Это карта падения Начинай с первой строки своего кода, где указаны класс и номер строки Наставник говорит: — Красный текст не означает "ты проиграл” Он означает “вот место, где нужно смотреть". Простая проверка на null Самый прямой способ защититься — проверить результат перед использованием Reward reward = findRewardByName(rewards, "Пилюля концентрации"); if (reward == null) ( System.out.println("Награда не найдена."); return; ) student.addSectPoints(reward.getPoints()); Теперь программа не пытается вызвать getPoints() у null. Но наставник сразу поднял палец. — Проверка на null полезна, но не должна превращаться в бесконечный лес if Лучше проектировать методы так, чтобы их поведение было понятным: либо возвращаем объект, либо явно сообщаем, что объект не найден Пока для простоты мы используем null. Позже, на более глубоких ступенях Java, ты узнаешь и другие техники, например Optional. Сейчас главное — не вызывать методы у null. 10.2. Ошибки как часть разработки Линь Грейдл раздраженно сказал
— Значит, надо просто везде поставить проверки Мэй Тест ответила — Или понять, где состояние стало недопустимым Наставник посмотрел на обоих — Ошибка — не позор Позор — спрятать ошибку так, что она разрушит систему позже. В Java ошибки и исключительные ситуации делятся на разные виды. Некоторые говорят о неправильном использовании кода Некоторые — о внешних проблемах файл не найден, нет доступа, данные повреждены. Некоторые можно обработать и продолжить работу. Некоторые лучше остановить сразу. Сухая истина Java exception — это объект, который описывает исключительную ситуацию Когда исключение аыброшено. обычное выполнение метода прерывается, пока исключение не будет обработано или пока программа не завершится с ошибкой. try/catch try/catch позволяет попытаться выполнить код и обработать исключение, если оно произошло try { int result = 100 / 0; System.out.println(result); } catch (ArithmeticException exception) { System.out.println("Ошибка расчёта: " + exception.getMessageО); ) Без catch программа упала бы. С catch можно вывести понятное сообщение и продолжить выполнение. Но наставник предупредил: — try/catch не должен быть ковром, под который заметаете мусор. Если вы поймали исключение и ничего не сделали, вы не исправили ошибку. Вы просто сделали ее невидимой Плохо: try { student.addSectPoints(reward.getPoints()); } catch (Exception exception) { // ничего не делаем } Такой код опасен Рейтинг не обновится, но никто не поймёт почему Лучше: try { student.aadSectPuints(reward.getPoints()); } catch (NullPointerException exception) {
System.out.println("Награда не найдена. Рейтинг не обновлён."); } Ещё лучше — не доводить до NullPointerException и проверить reward заранее. finally Блок finally '-ыполняется после try/catch независимо от того, была ошибка или нет. try { System.out.println("Пересчитыьаем рейтинг..."); } catch (RuntimeException exception) { System.out.println("Рейтинг повреждён: " + exception.getMessage()); ) finally ( System.out.println("Проверка рейтинга завершена."); I finally полезен, когда нужн^ выполнить завершающее действие: закрыть ресурс, записать лог, освободить что-то В современных Java-проектах для ресурсов часто используют try-with-resources, но это отдельная техника. В этой главе важно понять саму идею finally — место для действий, которые должны произойти в конце checked и unchecked exceptions Наставник начертил две колонки Unchecked exceptions — ошибки, которые компилятор не требует обрабатывать явно Примеры. • NullPointerException; • HlegalArgumentException; • ArithmeticException; • IndexOutOfBoundsException Checked exceptions — исключения, которые компилятор заставляет обработать или объявить в сигнатуре метода. Пример, который встретится в следующей главе Files.readstring(path); Чтение файла может выбросить lOException Это checked exception Его нужно обработать через try/catch или пробросить через throws. Сухая истина Java unchecked exceptions обычно наследуются от RuntimeException; checked exceptions обычно требуют обработки на этапе компиляции. unchecked часто указывают на ошибку в логике программы; checked часто связаны с внешним миром файлы, сеть, 1'вод-вывод.
Линь Грейдл спросил: — Значит, unchecked можно игнорировать? Наставник посмотрел на него — Демона тоже можно игнорировать. Иногда он даже подождёт Но потом сожжёт павильон. throw: выбросить исключение самому Иногда метод должен сам остановить некорректное действие. Например, нельзя добавить отрицательные очки public void addSectPoints(int points) { if (points < 0) { throw new IllegalArgumentException("Очки не могут быть отрицательными"); } sectPoints += points; ) throw означает: выбросить исключение. Это лучше, чем молча принять плохое значение. Плохо public void addSectPoints (int points) { sectPoints += points; Так можно случайно уменьшить очки через метод, который по смыслу должен только добавлять Лучше public void addSectPoints(int points) { if (points < 0) { throw new IllegalArgumentException("Нельзя добавить отрицательные очки"); } sectPoints += points; ) Наставник сказал: — Исключение — это не только реакция на ошибку. Это способ защитить правила мира 10.3. Свои исключения Иногда стандартного IllegalArgumentException достаточно. Но иногда полезно создать своё исключение, чтобы код был понятнее Например, награда с неправильным количеством очков: public class InvalidRewardException extends RuntimeException { public InvalidRewardException(String message) { super(message);
Теперь можно использовать его в классе Reward public class Reward { private final String name; private final int points; public Reward(String name, int points) { if (name == null || name.isBlankO ) { throw new InvalidRewardException("Название награды не должно быть пустым"); } if (points <= 0) { throw new InvalidRewardException("Очки награды должны быть больше нуля"); } this.name = name; this.points = points; ) public String getNameO { return name; ) public int getPoints() { return points; } } Другое исключение — если ученик пытается купить ресурс без очков. public class NocEnoughPointsException extends RuntimeException { public NotEnoughPointsException(String message) ( super(message); ) ) Применение: public void spendPoints(int cost) { if (cost <= 0) ( throw new IllegalArgumentException("Стоимость должна быть больше нуля"); } if (sectPoints < cost) { throw new NotEnoughPointsException("Недостаточно очков секты"); } sectPoints -= cost; Сухая истина Java своё исключение — это обычный класс, который наследуется от Exception или RuntimeException Для учебных бизнес-ошибок часто проще начинать с RuntimeException
10.4. Повреждённый свиток Старшие архивариусы принесли поврежденный свиток. На нём был фрагмент кода Reward reward = findRewardByName(rewards, "Пилюля концентрации"); student.addSectPoints(reward.getPoints()); Рядом лежал лог 0:C.:01 геюагЬЫате=Пилюля концентрации 00:00:02 rewardObjeet=null 00:00:03 applyReward started 00:00:04 NullPointerException Мэй Тест быстро просмотрела список наград rewards.add(new Reward("Свиток дыхдния", 20)); rewards.add(new Reward("Камень концентрации", 30)); rewards.add(null); — Здесь null прямо в списке наград. — сказала она. Ученики отступили от свитка. — Кто мог такое сделать9 Наставник молчал На обороте свитка была едва заметная подпись modiffedBy=LG'temporary-scnpt Все посмотрели на Линь Грейдла Он побледнел — Это не мой код Я.. Я делал временный скрипт для ускорения подсчёта, но не добавлял null. Мэй Тест холодно сказала — Временный скрипт, который пишет в боевой рейтинг перед финалом? Линь не ответил Наставник поднял руку — Предательство не всегда выглядит как злой смех в темном углу Иногда оно выглядит как «я быстро поправлю данные вручную, потом уберу»' Но код не знает, что ты хотел потом Код видит только то, что ты сделал, Он указал на список наград — Да. кто-то подменил награду на null Но настоящая причина глубже: система позволяла хранить null там, где должна быть награда Значит, виноват не только тот, кто внес искажение Виновата и защита, которая это пропустила Безопасный RewardService Перепишем работу с наградами безопаснее import java.util.ArrayList; public class Rewardservice { private final ArrayList<Reward> rewards = new ArrayListo();
public void addReward(Reward reward) { if (reward == null) ( throw new InvalidRewardException("Нельзя добавить null вместо награды"); } rewards.add(reward); } public Reward findRequiredRewardByName(String nam.e) { if (name == null || name.isBlankO) ( throw new InvalidRewardException("Название награды не должно быть пустым"); } for (Reward reward : rewards) { if (reward.getName().equals(name)) { return reward; ) } throw new InvalidRewardException("Награда не найдена: " + name); } ) Теперь метод не возвращает null. Если награды нет, он явно бросает InvalidRewardException. Применение try { Reward reward = гewaгdServiсе.findRequiredRewardByName("Пилюля концентрации"); student.addSectPoints(reward.getPoints()); } catch (InvalidRewardException exception) { System.out.println("Нельзя применить награду: " + exception.getMessageO ); ) Такой код честнее Он не делает вид, что всё хорошо, когда награда не найдена На стене появился новый код. try { Reward reward = rewardservice.findRequiredRewardByName("Пилюля концентрации"); student.addSectPoints(reward.getPoints()); ) catch (Exception exception) ( ) Наставник спросил: — Почему это плохо? Ответ: • ловится слишком общий Exception; • исключение полностью игнорируется; • рейтинг может не обновиться, а пользователь ничего не узнает; • расследовать проблему позже будет сложнее. Лучше:
try { Reward reward = rewardservice.findRequiredRewardByName("Пилюля концентрации"); student-addSectPoints(reward.getPoints()); } catch (InvalidRewardException exception) { System.out.println("Ошибка награды: " + exception.getMessageО); Ещё лучше — записать ошибку в лог или архив Этим секта займется в следующей главе 10.5. Испытания главы Испытание 1. Найди NullPornterException Есть код Reward reward = null; System.out.println(reward.getPoints()); Объясни, почему он падает. Исправь Награда +10 очко*, секты. Испытание 2. Безопасный поиск награды Напиши метод findRewardByName(), который ищет награду в ArrayList<Reward> Сначала пусть он возвращает null, если награда не найдена. Затем в main сделай безопасную проверку результата Награда: +20 очков секты. Испытание 3 InvalidRewardException Создай класс InvalidRewardException extends RuntimeException Используй его в конструкторе Reward, если пате пустой или points < 0. Награда +25 очко и секты Испытание 4. NotEnoughPointsException Создай класс NotEnoughPointsException extends RuntimeException Добавь в Cultivator метод spendPointstmt cost), который бросает это исключение, если очков не хватает. Награда: +25 очко^-. секты. Испытание 5 try/catch
Напиши код, который пытается купить оесурс стоимостью 100 очков при наличии 40 очков Обработай NotEnoughPomtsException и выведи понятное сообщение Hai рада +20 очков секты. Испытание 6. Не глотай исключение Исправь код: try { student.spendPoints (100); } catch (Exception exception) { } Сделай так, чтобы ошибка была обработана понятно Награда. +15 очков секты. Испытание старейшины. Награда без искажения Создай классы Reward. RewardService. InvalidRewardException и Cultivator Требования: • Reward нельзя создать с пустым именем или points <= 0; • RewardService не принимает null в addRewardQ; • findRequiredRewardByName() возвращает Reward или бросает InvalidRewardException; • Cultivator.addSectPointsf) не принимает отрицательные очки; • main применяет награду к ученику через try/catch: • программа не должна падать с NullPointerException Награда: +60 очкоч секты Барьер прорыва: Искажённое Ци К концу главы ты знаешь: • null означает отсутствие объекта; • у null нельзя вызвать метод: • NullPointerException возникает при обращении к отсутствующему объекту; • stack trace помогает найти место падения. • try/catch позволяет обработать исключение, • finally выполняется в конце независимо от ошибки. * checked exceptions требуют обработки или throws; • unchecked exceptions обычно наследуются от RuntimeException; • throw позволяет явно остановить недопустимое действие;
• свои исключения делают ошибки предметными, • нельзя молча глотать исключения; • лучше не допускать некорректное состояние, чем героически чинить лоследст вия Твоя табличка вспыхнула: Имя: Путник Статус: внешний ученик Пространственный мешок, открыт Дубликаты рейтинга устранены Искажённое ци: распознано Исключения: изучены Очки секты. +734 Турнирный рейтинг, финальный этап отложен до проверки архива. Ночь закончилась ближе к рассвету. Линь Грейдла временно отстранили от доступа к рейтингу. Он не спорил Только сказал: — Я хотел ускорить подсчёт. Не сломать Наставник ответил: — Намерение не отменяет последствий. Мэй Тест стояла у доски и смотрела на логи. — Мы нашли null, но не знаем, когда он появился Если данные жили только в памяти, след исчезнет после перезапуска. Наставник повернулся к закрытой двери архивного павильона — Именно поэтому завтра вы изучите базовые свитки Рейтинг, который исчезает при закрытии программы, — не рейтинг. Это сон. Где-то в глубине архива старый файл сам открылся на строке. previous_contest/uniino^n_student/nuLL_reward_artempt. Log Похожий саботаж уже случался раньше.
Глава 11. Базовые свитки После ночи сломанного свитка секта не спала Старшие ученики проверяли награды. Архивариусы сверяли списки участникон Наставник Павильона Основ стоял перед дверью, покрытой печатями старых турниров На двери были вырезаны слова Павильон Базовых Свитков Под ними горела меньшая надпись: То. что не сохранено, может исчезнуть Линь Грейдл пришел последним Его серебряная табличка была перевязана серой лентой временного ограничения Мэй Тест посмотрела на ленту. — Красивый аксессуар. — Очень смешно. — буркнул Линь Наставник не стал их останавливать — Сегодня мы не будем спорить о намерениях. Мы будем говорить о памяти До сих пор ваши программы жили только во время запуска. Завершили программу — данные исчезли Но турнир, рейтинг и следы саботажа не должны исчезать вместе с закрытием консоли Он коснулся двери Печати разошлись Внутри павильона стояли бесконечные полки со свитками На каждом свитке были строки: имена учеников, очки, ошибки, даты, повреждённые записи, старые результаты турнироь 11.1. Память программы и память секты Когда ты создаешь объект в программе- cultivator student = new Cultivator ("ST-001", "Путник"); student.addSectPoints (120); он существует в памяти программы. Пока программа работает, обьект доступен. Но если программа завершилась, данные пропадают. Это нормально для временных расчетов. Нс плохо для рейтинга турнира. Сухая истина Java переменные и объекты в обычной программе живут в оперативной памяти. После завершения программы они исчезают Чтобы сохранить данные между запусками, нужно записать их во внешнее хранилище файл, базу данных или другой источник. В этой главе мы используем обычные файлы Наставник сказал — Файл — это простой свиток. Он не такой мощный, как неликая база данных, но для первых архивов достаточно.
11.2. Работа с файлами Path: путь к свитку Path описывает путь к файлу или папке. Импорт' import java.nio.file.Path; Пример Path ratingPath = Path.of("data", "rating.txt"); Это путь к файлу rating.txt внутри папки data. Можно создать путь проще Path path = Path.of("rating.txt"); Но отдельная папка data удобнее, чтобы не смешивать код и сохранения. Path сам по себе не читает и не пишет файл Он только описывает, где файл находится Для чтения и записи используется класс Files Files.writeString: записать строку import java.io.lOException; import java.nio.file.Files; import java.nio.file.Path; public class WriteScrol1 Demo { public static void main(Stringt) args) { Path path = Path.of("rating.txt"); try { Files.writeString(path, "Путник: 120 очко:"); System.oat.println("Рейтинг сохранён."); } catch (lOException exception) ( System.out.println("He удалось сохранить рейтинг: " + exception.getMessage()) ; } } ) Почему нужен try/catch? Потому что запись файла может не получиться: • нет пран на запись; • путь неправильный, • диск недоступен; • файл занят, * папка не существует. • lOException - checked exception. Компилятор требует его обработать или обьявить.
Files.read String: прочитать строку import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; puolic class ReadScrollDemo { public static void main(StringJ] args) { Path path = Path.of("rating.txt"); try { String content = Files.readstring(path); System.out.println("Содержимое свитка:"); System.out.println(content); } catch (JOException exception) ( System.out.println("He удалось прочитать рейтинг: " + exception.getMessage ()); } } I Если файла нет, чтение завершится ошибкой. — Отсутствующий файл — не всегда катастрофа. Если программа запускается впервые, архива может ещё не быть Важно отличать “первый запуск” от “архив повреждён”. Files.exists: если свитка еще нет Перед чтением можно проверить, существует пи файл Path path = Path.ofCrating.txt"); if ((Files.exists(path)) { System.out.println("Архив ещё не создан. Начинаем с пустого рейтинга."); return; } try { String content = Files.readstring(path); System..ut.println(content); } catch (lOException exception) { System.out.println("Ошибка чтение, архива: " + except ion.getMessage()); } Fles.exists(path) возвращает true, если файл или папка существует, и false, если не существует или недоступен. Создание папки data Если писать в файл data/ratmg.txt, папка data должна сущестзовать Pat.n dataCireccery = Path.of("data"); Files.createDirectories(dataDirectory);
createDirectones создает папку, если ее нет Если папка уже есть, ошибки не будет Пример: try { Path dataDirectory = Path.of("data"); Files.createDirectories(dataDirectory); Path ratingpath = dataDirectory.resolve("rating.txt"); Files.writeString(ratingPath, "Путник: 120 очков"); } catch (TOException exception) { System.out.println("Ошибка архива: " + exception.getMessageО); } Метод resolve добавляет имя файла к пути папки Path ratingpath = dataDirectory.resolve ("rating.txt"); Это удобно, когда нее файлы лежат в одной папке. Сохранение списка строк Иногда удобнее записывать не одну строку, а список строк. import java.io.TOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; public class WriteLinesDemo { public static void main(String!) args) { ArrayList<String> lines = new ArrayListo (); lines.add("ST-001;Путник;120"); lines.add("ST-002;Линь Грейдл;135"); lines.add("ST-003;M3fi Тест;128"); Path path = Path.of("data", "rating.txt"); try { Files.createDirectories(path.getParent ()); Files.write(path, lines); System.out.println("Строки рейтинга сохранены."); } catch (TOException exception) { System.out.println("He удалось сохранить строки: " + exception.getMessaaeО); ) ) ) Формат строки: id; name;points Например: 5Т-001;Путник;120 Это простая ручная сериализация Сериализация — это преобразование объекта в формат, который можно сохранить или передать В этой главе мы используем простейший ручной вариант превращаем объект в строку и потом собираем объект обратно из строки.
Ручная сериализация Cultivator Добавим ч Cultivator метод toSaveLine(): public Spring toSaveLinet) { return id + + name + + sectPoints; ) Для объекта. new Cultivator("ST-001", "Путник") после начисления очков строка может быть такой: ST-001;Путник;120 Теперь нужен обратный метод: public static Cultivator fromSaveLine(String line) { String|] parts = line.split(";"); String id = parts[Q); String name = parts fl]; int points = Integer.parselnt (parts[2]); Cultivator cultivator = new Cultivator(id, name); cultivator.addSectPoints(points); return cultivator; } Но этот код пока опасен Если строка повреждена: ST-OuI;Путник части points не будет Если очки не число ST-001;Путник;много Integer parselntf) выбросит NumberFormatException Значит, загрузку нужно делать осторожнее Безопасная загрузка строки public static Cultivator fromSaveLine(String line) { String|] parts = line.split(";"); if (parts.length 1= 3) { throw new lllegalArgumentException("Некорректная строка сохранения: " + line); } String id = part sub- string name = parts[1]; int points;
try { points = Integer.parselnt(parts(2]); } catch (NumberFormatException exception) { throw new IllegalArgumentException("Некорректные очки в строке: " + line, exception); } Cultivator cultivator = new Cultivator(id, name); cultivator.addSectPoints(points); return cultivator; } Теперь повреждённая строка не ломает код молча. Она явно сообщает, что не так. Мэй Тест сказала — То есть файл — это тоже входные данные Ему нельзя доверять полностью Наставник кивнул. — Верно Всё, что приходит извне программы, может быть повреждено. Файл, ввод пользователя, ответ сервера, даже старый архив секты Сохранение рейтинга Создадим класс TournamentArchive. import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; public class TournamentArchive ( private final Path ratingPath; public TournamentArchive(Path ratingPath) { this.ratingPath = ratingPath; } public void save(ArrayList<Cultivator> participants) { ArrayList<String> lines = new ArrayListO(); for (Cultivator participant : participants) ( lines.aad(participant.toSaveLine()); } try { Path parent = ratingPath.getParent(); if (parent != null) { Files.createDirectories(parent); } Files.write(ratingPath, lines); 1 catch (lOException exception) { throw new ArchiveException("He удалось сохранить рейтинг", exception); } }
Здесь ArchiveException — свое unchecked exception для ошибок архива public class ArchiveException extends RuntimeException { public ArchiveException(String message, Throwable cause) { super(message, cause); } } Почему мы оборачиваем lOException9 Потому что на уровне бизнес-логики удобнее говорить "ошибка архива", а не ^езде тащить технический lOException. Но важно не потерять cause — исходную причину throw new ArchiveException("Hi удалось cc хр анить рейтинг", exception); Так мы сохраняем цепочку причин. Загрузка рейтинга puDlic ArrayList<Cultivator> load() { ArrayList<Cultivator> participants = new ArrayListo(); if ((Files.exists(ratingPath)) { return participants; } try { for (String line : Files.readAlILines(ratingPath)) { if (line.isBlank()) { continue; } participants.add(Cultivator.fromSaveLine(line)); } return participants; } catch (lOException exception) { throw new ArchiveException("He удалось прочитать рейтинг", exception); } } Этот метод: • создаёт пустой список; • если файла нет — возвращает пустой список; • читает все строки файла; • пропускает пустые строки; • превращает каждую строку в Cultivator; • если файл нельзя прочитать — бросает ArchiveException. Но здесь есть скрытая опасность если одна строка повреждена, Cultivator.fromSaveLine() бросит IllegalArgumentExcephon, и загрузка остановится Это может быть правильно или неправильно — зависит от правил мира Если архие должен быть строгим, повреждённая строка останавливает загрузку. Если архие должен быть терпимым, можно пропускать плохие строки и писать их н пог.
Для учебной версии пыберем строгий подход поврежденный рейтинг должен быть замечен сразу 1 1.3. Старый лог саботажа Когда ученики научились читать файлы, наставник открыл старый архивный свиток. На нём было написано: previous_contest/unknown_student/nuLL_reward_attempt.Log Мэй Тест прочитала вслух year=previous actor=unknown_student operation=applyReward генагд№те=Пилюля концентрации rewardPoints=nuLL resuLt=NuLLPotnterException note=dupLicate identity used before crash Линь Грейдл тихо сказал: — Это было до нас. Наставник кивнул. — Да Много лет назад перед Межсектовым Турниром Основ рейтинг тоже был сломан. Тогда старейшины решили, что это случайная ошибка Они восстановили очки вручную и не стали менять систему. Он посмотрел на учеников. — Ошибка, которую не поняли, становится традицией Традиция, которую не проверяют, становится уязвимостью. В старом логе была ещё одна строка identity=ST-000;name=unbnown_student;hash=changed_after_insert Мэй Тест нахмурилась. — hash изменился после вставки Значит, кто-то менял поле, участвующее в hashCode. после добавления в HashSet? Наставник сказал: — Именно. Сегодня вы зидите, как главы связываются друг с другом. Сначала дубликаты Потом null. Потом отсутствие архива Один слабый участок можно пережить Несколько слабых участков подряд превращаются в саботаж Линь Грейдл сжал кулаки — Значит, кто-то повторил старую атаку
— Или нашёл старый плохой код и воспользовался им, — ответил наставник — Разница важна для наказания. Но для программиста важнее другое система должна быть устойчивой даже тогда, когда кто-то ошибается или действует нечестно 1 1.4. Полный код примера Ниже — учебная версия кода для сохранения и загрузки рейтинга. import java.util.Objects; public class Cultivator ( private final String id; private final String name; private int sectPoints; public Cultivator(String id, String name) { if (id == null || id.isBlank ()) { throw new IllegalArgumentException("ID не должен быть пустым"); ) if (name == null I I name.isBlank()) ( throw new IllegalArgumentException("Имя не должно быть пустым"); 1 this.id = id; this.name = name; this.sectPoints = 0; } public void addSectPoints(int points) ( if (points < 0) ( throw new IllegalArgumentException("Очки не могут быть отрицательными"); } sectPoints += points; } public String toSaveLine() { return id + + name + + sectPoints; } public static Cultivator fromSaveLine(String line) { String[] parts = line.split (";"); if (parts.length != 3) { throw new IllegalArgumentException("Некорректная строка: " + line); } int points = Integer.parselnt(parts[2]); Cultivator cultivator = new Cultivator(parts[C], parts[l]); cultivator.addSectPoints(points); return cultivator; } @Cverride public String toStringO { return id + " | " + name + " | очки: " + sectPoints; ) @Override public boolean equals(Object other) { if (this == other) {
return true; } if (!(other instanceof Cultivator)) { return false; } Cultivator that = (Cultivator) other; return Objects.equals(id, that.id); } @Override public int hashCodeO ( return Objects.hash(id); } } Архив: import java.io.lOException; import java.nio.file.Files; import java.nio.file.Path; import java,util.ArrayList; public class TournamentArchive { private final Path ratingPath; public TournamentArchive(Path ratinqPath) { this.ratingpath = ratinqPath; ) public void save(ArrayList<Cultivator> participants) { ArrayList<String> lines = new ArrayListo(); for (Cultivator participant : participants) { lines.add(participant.toSaveLine()); } try { Path parent = ratingPath.getParent(); if (parent != null) { Files.createDirectories(parent); } Files.write(ratingPath, lines); } catch (lOException exception) { throw new ArchiveException("He удалось сохранить рейтинг", exception); ) public ArrayList<Cultivator> loadf) { ArrayList<Cultivator> participants = new ArrayListo(); if (!Files.exists(ratingPath)) { return participants; ) try { for (String line : Files.readAllLines(ratingpath)) { if (!line.isblank ()) { participants.add(Cui tivator.frcmSaveLine(line)); ) } return participants;
} catch (lOException exception) { throw new ArchiveException("He удалось прочитать рейтинг", exception); } } } И демонстрация: import java.nio.file.Path; import java.util.ArrayList; public class ArchiveDemo { public static void main(String!] args) { TournamentArchive archive = new TournamentArchive(Path.of("data", "rating.txt")); ArrayList<Cultivator> participants = new ArrayListo(); Cultivator first = new Cultivator("ST-001", "Путник"); first.addSectPoints(120); Cultivator second = new Cultivator("ST-002", "Линь Грейдл"); second.addSectPoints (135); Cultivator third = new Cultivator("ST-003", "Мэй Тест"); third.addSectPoints(12 ); participants.add(first); participants.add(second); participants.add(third); archive.save(participants); ArrayList<Cultivator> loaded = archive.load(); for (Cultivator participant : loaded) ( System.out.println(participant); ) ) } Исключение архива: public class ArchiveException extends RuntimeException { public ArchiveException(String message, Throwable cause) { super(message, cause); ) ) 1 1.5. Записки старейшины 1. He храни важные данные только в памяти. Если рейтинг должен пережить перезапуск, сохраняй его во файл или другое хранилище. 2. Не доверяй файлу полностью.
Файл могли удалить, изменить вручную. повредить или записать в неправильном формате. 3. Делай формат простым. Для учебной задачи строка id;name;points' подходит. Для реальных проектов часто используют JSON, базы данных или другие форматы. 4. Обрабатывай lOExcepiion. Файлы — часть внешнего мира. Внешний мир не обязан работать идеально 5. Не теряй причину исключения. Если оборачиваешь lOException в свое исключение, передавай исходное исключение как cause. 6. Не называй файл просто data.txt, если через месяц сам не поймёшь, что в нем. Лучше: rating.txt. participants.txt, tournament-log.txt. 11.6. Испытания главы Испытание 1. Первый свиток Создай программу WnteFirstScroll java Запиши строку Путник: 120 очкоь в файл ra1ing.txt через Files writeSlringQ. Награда- +10 очков секты Испытание 2. Прочитай свиток Создай программу ReadFirstScroll.java Прочитай rating txt через Files.readString() и выведи содержимое Награда +10 очков секты. Испытание 3 Папка data Сохрани рейтинг в файл data/ratmg.txt Перед записью создай папку через Files.createDirectoriesf).
Награда +15 очков секты Испытание 4. Список строк Создай ArrayList<String> со строками 5Т-001;Путник;120 5Т-002;Линь Грейдл;135 $Т-003;Мэй Тест;128 Запиши список в data/rating txt через Files,wnle(). Награда: +20 очков секты. Испытание 5. Загрузка строк Прочитай data/rating. txt через Files.readAIILmes() Выведи каждую строку. Награда +15 очков секты Испытание 6. Ручная сериализация Добавь в Cultivator метод toSaveLineQ и статический метод fromSaveLine(String line) Проверь, что объект можно сохранить в строку и восстановить обратно. Награда +30 очков секты Испытание 7. Архив рейтинга Создай класс TournamentArchive с методами save() и load(). Используй Path и Files Если файла нет, load() должен вернуть пустой список. Награда +40 очков секты. Испытание старейшины. Память секты Создай минитрограмму SectMemory.java. Требования • создать несколько Cultivator; • начислить им очки; • сохранить их в data/rating. txt; • загрузить рейтинг из файла: • вывести загруженных участников: • обработать lOException;
обработать повреждённую строку сохранения понятным сообщением Награда: +70 очко'; секты Барьер прорыва: Память Секты К концу главы ты знаешь. * объекты в памяти исчезают после завершения программы; • Path описывает путь к файлу; • Files умеет читать и писать файлы; • lOException - checked exception; • Files.exists() помогает проверить наличие файла; • Files.createDirectories() создаёт папки; • Files.writeString() записывает строку; • Files.readString() читает строку; • Files.write() записывает список строк; • Files.readAllLines() читает строки файла; • простая ручная сериализация превращает объект в строку, • при загрузке данные нужно проверять; • архив помогает расследовать ошибки и сохранять состояние между запусками Твоя табличка вспыхнула: Имя Путник Статус внешний ученик Пространственный мешок открыт Искаженное ци распознано Базовые свитки освоены Архив рейтинга создан Очки секты: +904 Барьер прорыва: Память Секты открыт Турнирный рейтинг восстановлен из архива Статус внутреннего отбора- кандидат на представителя секты Когда ученики вышли из Павильона Базовых Свитков, солнце уже стояло высоко. Турнирная доска снова работала На ней больше не было дубликата unknown student, null-награды и отрицательных очков. Но теперь рядом с каждой строкой появился значок архивной печати. ST-001 | Путник | 120 очков | archived ST-002 I Линь Грейдл | 135 очков | archived ST-003 | Мэй Тест | 128 очкоь | archived Наставник сказал:
— Теперь ваши данные не исчезнут вместе с закрытием программы Но для Межсектового Турнира Основ этого всё ещё мало. Линь Грейдл поднял голову. — Что ещё нужно? Старейшина Архива ответил вместо наставника- — На турнире не принимают россыпь одиночных файлов Там принимают артефакт, который можно собрать, запустить и проверить. Перед учениками открылись порота следующего павильона. За ними были видны папки, пакеты, зависимости и большой свиток с надписью pom.xml Наставник Павильона Основ посмотрел на тебя. — Ты научился писать техники Теперь пора собрать из них настоящий проект. Внутреннее состязание почти завершилось. Но путь к Межсектовому Турниру только начинался
Глава 12. Артефакты ученика После восстановления архивных свитков турнирная доска работала спокойно. Больше не было 'unknown_student. Больше не было ’nuH'-награды. Больше не было ученика с отрицательными очками Но спокойствие оказалось обманчивым. На следующее утро всех кандидатов собрали не на арене и не в архиве, а у длинного павильона, стены которого были покрыты схемами папок, стрелками импортов и странными печатями сборки Над входом горела надпись: Павильон Артефактов У ворот стоял новый старейшина. Его мантия была не боевой и не архивной На поясе висели маленькие таблички с надписями 'src , 'main , test', pom xml Наставник Павильона Основ поклонился ему — Это Старейшина Сборки Он отвечает за то, чтобы техника ученика могла быть собрана не только на его собственном столе Линь Грейдл тихо фыркнул. — Если код работает, какая разница, где он лежит? Старейшина Сборки медленно повернулся — Большая Код, который работает только у тебя, — это не артефакт секты. Это личный талисман сомнительной стабильности. Мэй Тест улыбнулась — То есть “у меня запускается" больше не аргумент? — Никогда не был, — сказал старейшина Он поднял руку. На стене появился список требований финального отбора Финальный допуск к Межсектовому Турниру Основ 1. Проект должен иметь понятную структуру. 2. Классы должны лежать в пакетах. 3. Зависимости должны быть описаны явно. 4. Проект должен собираться одной командой. 5. Проект должен запускаться после сборки. 6. Ошибки импорта и структуры считаются провалом артефакта. Наставник посмотрел на тебя — До сих пор ты писал техники. Теперь пора научиться собирать из них артефакт,
12.1. Пакеты техник Почему одиночных файлов уже недостаточно В первых главах было нормально создавать один файл’ public class Hello { public static void main(String] aigs) { System.out.println("Helle Cultivation World"); } } Потом файлов стало больше Cultivator.java Rank.java Reward.java Tournamentservice.java RatingArchive.java Пока проект маленький, можно держать всё ₽ одной папке Но чем больше классов, тем быстрее начинается хаос. Старейшина Сборки показал повреждённый стол ученика На нём лежали десятки свитков без порядка’ Main.java Cultivator.java Cultivator2.java RewardOld.java RewardNew.java Test.java FinalTest.java ReallyFinalMain.java ArchiveCopy.java ArchiveCopyFixed.java Линь Грейдл отвел взгляд. Наставник спросил: — Что здесь не так? Один ученик ответил. — Слишком много файлов — Нет, — сказал наставник. — Файлов может быть много Проблема в том, что у них нет структуры Настоящий проект — это не просто набор java файлов Это организованная структура, где код приложения, тесты, ресурсы, зависимости и настройки сборки лежат в ожидаемых местах Такая структура позволяет другим людям собрать и запустить проект без ручного угадывания
Стандартная структура Maven-проекта Старейшина Сборки провел посохом по полу. Камни раздвинулись, и появилась схема sect-management-system/ — pom.xml src/ - main/ •— java/ *— core/ L sect/ - app/ *— SectApplication.java — model/ - Cultivator.java — Rank.java i— service/ *— Cultivatorservice.java *— storage/ *— CultivarorFileStorage.java - test/ L- java/ *— core/ *— sect/ L service/ *— CultivatorServiceTest.java Он указал на pom.xml — Это главный свиток сборки Maven-проекта В нём описываются имя артефакта, версия Java, зависимости и настройки тестов Потом указал на src/mam/java . — Здесь лежит основной код приложения Потом на 'src/test/java'. — Здесь лежат тесты. Старейшины будут смотреть на них особенно внимательно src/main/java — основной Java-код; src/test/java —тестовый Java-код, рот.хтГ — файл конфигурации Maven; структура пакетов внутри java' должна соответствовать строке package в классе Package: печать принадлежности класса Класс может лежать не просто в пустоте, а в пакете Например, класс Cultivator лежит в пакете 'core sect model
package core.sect.model; public class Cultivator { private final String id; private final String name; private int sectPoints; public Cultivator(String id, String name) ( this.id = id; this.name = name; this.sectPoints = 0; } public String getld() { return id; } public String getNameO { return name; } public int getSectPoints() { return sectPoints; } public void addSectPoints(int points) ( if (points < 0) { throw new IllegalArqumentException("points must net be negative"); } sectPoints += points; } ) Первая строка package core.sect.medel; означает, этот класс принадлежит пакету core sect.model'. Путь к файлу должен соответствовать пакету: sre/main/java/core/sect/model/Cultivator.java Наставник сказал — Package — это не украшение Это адрес класса в большом доме проекта Если класс объявлен так: package core.sect.model; но лежит n неправильной папке, проект может не собраться. Повреждённый свиток. Класс не в той пещере Файл лежит здесь. sre/mai n/java/Cultivator.java
А внутри написано package core.sect.model; public class Cultivator { ) Проблема путь файла не соответствует пакету. Правильное место. src/main/java/core/sect/model/Cultivator.java Import: как позвать класс из другого пакета Если класс Cultivator' лежит в пакете core.sect.model', а сервис лежит в пакете core.sect.service', сервису нужно импортировать модель package core.sect.service; import core.sect.model.Cultivator; import java.util.ArrayList; import java.util.List; import java.util.Optional; public class Cultivatorservice ( private final List<Cultivator> cultivators = new ArrayListo(); public void register(Cultivator cultivator) { cultivators.add(cultivator); } public Optional<Cultivator> findByld(String id) { for (Cultivator cultivator : cultivators) { if (cultivator.getld().equals(id)) { return Optional.of(cultivator); } } return Optional.empty(); } } Импорт: import core.sect.model.Cultivator; говорит компилятору, где найти класс 'Cultivator'. Импорты из стандартной библиотеки тоже нужны: import java.util.ArrayList; import java.util.List; import java.util.Optional; import’ не копирует код. Он только сообщает, какой класс использовать, если этот класс находится н другом пакете. Классы из 'java.lang', например 'String', импортировать не нужно
Как разбить код по пакетам Старейшина Сборки разложил на столе несколько табличек. model service storage арр exception — В маленьком проекте достаточно простого разбиения по ответственности. Пример. core.sect.model Сюда идут сущности предметной области: Cultivator', 'Rank', 'Reward core.sect.service Сюда идут правила работы регистрация ученика, начисление очков повышение ранга, расчёт рейтинга core.sect.storage Сюда идут классы для сохранения и загрузки данных. core.sect.арр Сюда идет точка входа приложения mam' core.sect.exception Сюда идут свои исключения 'CultivatorNotFoundException', NotEnoughPomtsException . Наставник добавил — Не пытайся сразу строить дворец архитектуры Для первой системы достаточно ясного разделения, данные, правила, хранение, запуск. Дуэль учеников Линь Грейдл показал свой вариант, public class Main { // поля ученика // список учеников // чтение файла // запись файла // расчёт рейтинга // // меню обработка ошибок // всё здесь }
Мэй Тест показала другой вариант: model/Cultivator.java model/Rank.java service/CultivatorService.java storage/CultivarorFileStorage.java app/SectApplication.java Старейшина спросил, — Какой артефакт легче проверить и изменить? Ответ второй В первом варианте всё смешано. Любое изменение меню может случайно задеть сохранение файла или расчёт ранга, Во тором варианте у каждого класса есть своя зона ответственности. 12.2. Maven: печь сборки проекта Maven — это инструмент сборки. Он умеет: • компилировать проект; • запускать тесты; • скачивать зависимости; • собирать артефакт: • работать по стандартной структуре проекта Старейшина Сборки сказал: — Если '}avac — малая алхимическая печь для одного свитка, то Maven — большая печь, которая знает зесь проект. Команды Maven обычно запускают из корня проекта, где лежит 'рот.хтГ. Проверить проект: mvn test Собрать проект mvn package Очистить старую сборку и собрать заново mvn clean package После сборки результат обычно появляется о папке: target/ Сухая истина Java: Maven не заменяет Java Maven управляет процессом сборки Java-проекта Он вызывает компиляцию, запускает тесты и использует настройки из pom.xml
Первый pom.xml Минимальный 'pom.xml' для проекта может выглядеть так’ <pi'oject xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</mcdelVersion> <groupld>oore.secL</groupId> <arcifactId>sect-management-system</artifactld> <version>l.0.0</version> <properties> cmaven.compiler.source>17</maveri.compiler.source> <maven.compiler.target>17</maven.compiler.target> <project.build.sourceEncoding>UTF-'</project.build.sourceEncoding> </properties> </project> Разберём. <groupl d>core. sect<./groupld> Группа проекта. Часто похожа на корневой package <.arcifactld>sect-management-syscem<./artifact]d> Имя артефакта, который собирает Maven. <version>l.0.u</version> Версия проекта. cmaven.compiler.source>17</maven.compiler.source> <maver..compiler . targeOl 7</maven.compiler.target> Версия Java, под которую компилируется проект. Совет старейшины: для учебного проекта лучше явно указать версию Java. Тогда другой ученик не будет гадать, какой корень духа нужен для сборки. Dependency; внешняя пилюля силы Иногда проекту нужен чужой готовый код Например, библиотека для тестов JUnit Такие внешние библиотеки называются зависимостями Maven dependency добавляют в pom.xml <depenaencies> <dependency> <grcupld>org.junit.jupiter</groupld> <artifactId>junit-Jupiter</artifactId> <version>5.10.2</version> <scope>test</scope> </dependency> </dependencies>
scope co значением test означает: зависимость нужна только для тестон dependency — это внешняя библиотека, которую Maven скачивает и подключает к проекту. Не нужно вручную копировать ' jar' файлы в случайные папки, если зависимость можно описать в pom.xmi'. Но зависимость — это не магия Если библиотека не нужна, не добавляй её просто потому, что она выглядит серьезно Лишние зависимости утяжеляют проект и могут приносить конфликты. Сборка проекта из терминала Старейшина Сборки поставил перед учениками простое задание 1 Перейти в папку проекта. 2 Убедиться, что там есть pom xml. 3 . Выполнить mvn test 4 Выполнить mvn package Пример cd sect-management-system mvn test mvn package Если всё хорошо. Maven покажет BUILD SUCCESS Если есть ошибка компиляции. Maven покажет, какой класс и какая строка сломаны Как запустить main-класс Создадим точку входа: package core.sect. арр; import core.sect.model.Cultivator; import core.sect.service.CultivatorServicer- public class SectApplication { public static void main(String[] args) { CuitivatorService service = new Cultivatorservice(); Cultivator student = new Cultivator("ST-001", "Путник"); service.register(student); student.addSectPoints(50); System.out.println("Система секты запущена."); System.out.println("Ученик: " + student.getName()); System.out.println("Очки: " + student.getSectPoints()); } )
В IDE такой класс можно запустить напрямую Через Maven можно настроить запуск позже с помощью плагинов, но для этой главы достаточно понять главное проект должен компилироваться, тестироваться и иметь понятную точку входа. Maven или Gradle Один ученик поднял руку — А если ч другой секте используют Gradle9 Старейшина Сборки кивнул. — Gradle — тоже инструмент сборки. Он решает похожие задачи: зависимости, компиляция, тесты, сборка. В этой книге мы используем Maven, потому что его структура и рот.хтГ хорошо подходят для первого знакомства. Позже ты можешь изучить Gradle отдельно Maven и Gradle — не языки программирования. Это инструменты сборки Главное сейчас — понять идею: проект должен описывать, как его собрать, а не зависеть от устных инструкций автора. 12.3. Испытания главы Малое испытание 1. Создай структуру проекта Создай папки: src/main/java'core/sect/model src/mam/java/core'sect/service src/mam/java/core/sect/app src/ ma i n/java/core'sect/sto rage src/test/java/core/sect/service Награда +15 очков секты Малое испытание 2. Разложи классы по пакетам Создай’ core secr.model Cultivator'; core.sect.model Rank’; core sect.service Cultivatorservice'; core.sect.app.SectApplication . Награда: +20 очков секты. Повреждённый свиток. Исправь package
Файл лежит здесь src/mai n/java/core/sect/model/Rank.java Внутри написано: package core.sect.models; Исправь ошибку Награда: + 10 очко^. секты Испытание старейшины. Первый pom.xml Создай ‘рот.хтГ с: groupid : core sect'; artifactld sect-management-system'; version ; 1 0 0; Java 17; кодировка UTF-8. Запусти: mvn test Награда: +25 очко?, секты. Барьер прорыва. Настоящий проект К концу главы твой проект должен; • иметь структуру Maven; • содержать рот.хтГ; • иметь классы в пакетах, • собираться без ошибок импорта. • иметь SectApplicatton с методом mam . Обнови табличку ученика: имя:____________ Статус: кандидат финального отбора Ранг: Кандидат турнира Артефакт сборки: Maven-проект создан Освоенные техники: packages, imports, pom.xml, dependencies, сборка Очки секты: +994 Барьер прорыва: Настоящий Проект открыт
Когда испытание закончилось, ученики один за другим показывали свои проекты У кого-то Maven сразу выдал BUILD SUCCESS У кого-то класс лежал в неправильной папке У кого-то рот.хтГ назывался рот,xml.txt'. Старейшина Сборки пережил это с лицом человека, который видел слишком многое Линь Грейдл, к удивлению многих, собрал проект с первого раза. Но когда старейшина запустил его, программа вывела Проверено вручную. Всё точно работает. Мэй Тест склонила голову. — А где доказательство9 Пинь нахмурился — Какое еще доказательство9 В этот момент в павильон вошла старейшина в белой мантии На её рукавах были вышиты маленькие зелёные галочки и красные кресты. Наставник тихо сказал. — Старейшина Испытаний. Она посмотрела на собранные проекты. — Красивые артефакты Но я не верю словам Завтра каждый проект пройдёт малые испытания стаоейшин.
Глава 13. Малые испытания старейшин Зал Испытаний отличался от всех предыдущих павильонов Здесь не было манекенов, боевых кругов и архивных шкафов Вместо этого вдоль стен стояли одинаковые каменные пьедесталы. На каждом лежал проект ученика, а над ним парили три символа ожидание действие проверка Старейшина Испытаний вышла в центр зала — Вчера вы научились собирать проект. Но собранный проект ещё не означает правильный проект. Линь Грейдл скрестил руки — Если программа запускается, значит сна работает. Старейшина посмотрела на него так, что в зале стало тише. — Программа может запускаться и при этом считать рейтинг неверно. Может сохранять файл не туда Может принимать отрицательные очки Может падать на редком сценарии Может работать сегодня и сломаться завтра после изменения одного метода Мэй Тест сказала — Значит, нужны проверки, которые можно запускать снова — Верно. — сказала старейшина. — Сегодня вы изучите JUnit и базовые тесты. Наставник Павильона Основ добавил: — После саботажа мы больше не принимаем обещания. Только проверяемые техники. 13.1. С чего начать? Зачем нужны тесты Тест — это код, который проверяет другой код. Например, если метод должен начислять ученику 50 очков, тест проверяет, что после вызова метода у ученика действительно стало на 50 очков больше Без теста ученик говорит: Я проверил вручную. Работает. С тестом проект говорит Я могу доказать это снова и снова.
Автоматический тест фиксирует ожидаемое поведение программы Если позже кто-то изменит код и сломает это поведение, тест упадёт и покажет проблему. Старейшина Испытаний начертила на стене три причины писать тесты. • проверить, что код работает сейчас; • защититься от случайной поломки а будущем; • описать поведение системы на конкретных примерах. — После истории с саботажем — сказала она, — третья причина стала особенно важной Там, где не было проверок, саботажник мог спрятать искажённую ци Подключение JUnit в Maven Чтобы писать тесты на JUnit, добавим зависимость в pom.xml <dependencies> <dependency> <grcupTd>org.junit.jupiter</groupid> <artifactld>j unit-jupiter</artifactld> <version>5.IC.2</version> <scope>test</scope> </dependency> </dependencies> И добавим плагин для запуска тестов: <build> <plugins> <plugin> <groupld>org.apache.maven.plugins</groupld> <artifact I d'maven-surefire-plugin</artifactld> <version>3.2.5</version> </plugin> </piugins> </build> Старейшина сказала. — Версии здесь приведены как рабочий учебный пример В реальном проекте версии можно обновлять осознанно, но не хаотично. Тесты Maven ищет в src/test/java Например src/test/java/core/sect/service/CultivatorServiceTest.java 13.2. Написание тестов Первый тест Допустим, есть класс 'Cultivator':
package core.sect.model; public class Cultivator ( private final String id; private final String name; private int sectPoints; public Cultivator(String id, String name) { if (id == null || id.isBlank()) { throw new IllegaiArgumentException("id must not be blank"); } if (name == null I I name.isBiank()) ( throw new IllegaiArgumentException("name must not be blank"); } this.id = id; this.name = name; this.sectPoints = 0; } public int getSectPoints() { return sectPoints; } public void addSectPoints(int points) { if (points < 0) { throw new IllegaiArgumentException("points must not be negative"); } sectPoints += points; ) ) Тест: package core.sect.model; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; class CultivatorTest { @Tesc void addSectPointsShouldlncreasePoints() { Cultivator cultivator = new Cultivator("ST-001", "Путник"); cultivator.addSectPoints(5u); assertEquals (50, cultivator.getSectPoints ()); } ) Разберём @Test Аннотация показывает JUnit, что это тестовый метод. assertEquals (50, cultivator.getSectPoints()); Проверяет, что ожидаемое значение равно фактическому.
Сухая истина Java: в assertEquals(expected. actual)' первым обычно пишут ожидаемое значение, вторым — фактическое Это помогает читать сообщения об ошибках. Структура теста: given, when, then Старейшина Испытаний разделила первый тест на три части: @Test void addSectPointsShouldlncreasePoints() { // given Cultivator cultivator = new Cultivator("ST-001", "Путник"); // when cultivator.addSectPoints(51); // tnen assertEquals(50, cultivator.getSectPoints ()); } given — подготовка данных. when — действие, которое проверяем 'then — проверка результата Наставник сказал: — Это не обязательная магия, а удобный стиль. Он помогает понять, что именно проверяет тест. assertTrue и проверка условий Допустим, у ученика есть метод: public boolean canRankUp() { return sectPoints >= 100; } Тест: import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertTrue; class CuitivatorRanKTest { @Test void canRankUpShouldReturr.TrueWhenStudentHasEnouqhPoints () { Cultivator cultivator = new Cultivator("ST-001", "Путник"); cultivator.addSectPoints (12U); assertTrue(cultivator.canRankUp()); } I assertTrue проверяет, что условие истинно.
Есть и assertFalse', если нужно проверить, что условие ложно assertFalse(cultivator.canRankUp ()); Старейшина сказала: — Тест должен проверять не только путь победы, но и путь отказа assertThrows: проверка ошибок После саботажа старейшины особенно внимательно смотрели на некорректные состояния Метод addSectPoints запрещает отрицательные очки; public vrid addSectPoints(int points) { if (points < 0) { throw new IllegaiArgumentException("points must not be negative"); } sectPoints += points; ) Тест должен доказать, что отрицательные очки действительно запрещены import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertThrows; class CultivatorValidationTest ( @Test void addSectPointsSnouldThrowExceptionWhenPointsAreNegative() { Cultivator cultivator = new Cultivator("ST-001", "Путник"); assertThrows (IllegaiArgumentException.class, () -> cultivator.addSectPoints(-10)); } } 'assertThrows проверяет, что действие бросает ожидаемое исключение Тестирование исключений важно не меньше, чем тестирование успешных сценариев Если код должен отклонять неправильные данные, это поведение тоже нужно закрепить тестом. Тест сервиса регистрации Теперь проверим не один объект, а сервис. package core.sect.service; import core.sect.model.Cultivator; import java.util.ArrayList; import java.util.List; import java.util.optional;
public class Cultivatorservice { private final List<Cultivator> cultivators = new ArrayListo(); public void register(Cultivator cultivator) { if (cultivator == null) { throw new IllegalArgumentException("cultivator must not be null"); } cultivators.add(cultivator); } public Optional<Cultivator> findByld(String id) { for (Cultivator cultivator : cultivators) { if (cultivator.getld().equals(id)) ( return Optional.of(cultivator); } } return Optional.empty(); ) public int count() ( return cultivators. size (); ) } Тест: package core.sect.service; import core.sect.model.Cultivator; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; class CultivatorServiceTest { @Test void registerShouldAddCultivatorToService() { CultivatcrService service = new CultivatcrService(); Cultivator cultivator = new Cultivator("ST-ЭЭГ', "Путник"); service.register(cultivator); assertEquals(1, service.count()); assertTrue(service.findById("ST-001").isPresent()); } ) Старейшина Испытаний сказала- — Этот тест прост. Но именно простые тесты часто ловят самые дорогие ошибки. 13.3. Немного о тестах Регрессия: демон старой поломки
Регрессия — это ситуация, когда раньше работавшая возможность снова сломалась после изменений. Например, после саботажа вы исправили запрет отрицательных очкоч Но через неделю другой ученик переписал метод и случайно убрал проверку Без теста это может пройти незаметно С тестом @Test void addSectPointsShouldThrowExceptionWhenPointsAreNegative() { Cultivator cultivator = new Cultivator ("ST-C01", "Путник"); assertThrjws(IllegalArgumentException.class, () -> cultivator.addSectPoints(- 10)); ) ошибка будет поймана при mvn test Наставник сказал, — Регрессионный тест — это печать, поставленная на старую рану Если кто-то снова её откроет, печать треснет первой. Тесты как разоблачение предателя Старейшина Испытаний достала старый поврежденный фрагмент из архива, public void applyReward(Cultivator cultivator, Reward reward) { cultivator.addSectPoints(reward.getPoints()); } — Здесь саботажник подменял reward'на null Код падал c NullPointerException'. Исправленный вариант public void applyReward(Cultivator cultivator, Reward reward) { if (cultivator == null) { throw new IllegalArgumentException("cultivator must not be null"); } if (reward == null) { throw new IllegalArgumentException("reward must not be null"); } cultivator.addSectPoints(reward.getPoints()); ) Тест @Test void appl yRewardShouldThrowExceptionWher.RewardlsNull () { Cultivatorservice service = new Cultivatorservice(); Cultivator cultivator = new Cultivator("ST-001", "Путник"); assertThrows(IllegalArgumentException.class, () -> service.applyReward(cultivator, null)); )
Старейшина сказала' — Теперь саботажник может попытаться передать null , но проект не упадёт в случайном месте Он остановит неверное состояние на входе и покажет понятную причину. Мэй Тест добавила: — То есть тест не просто проверяет исправление. Он не дает будущему коду снова принять ту же слабость. — Именно, — подтвердила старейшина 13.4. Как не писать бесполезные тесты Пинь Грейдл, который весь урок молчал, вдруг показал свой тест @Test void testSomething() ( Cultivator cultivator = new Cultivator ("ST-001", "Линь Грейдл"); assertTrue(true); } Старейшина долго смотрела на код. — Этот тест проверяет только твою способность написать assertTrue(true)'. В зале кто-то кашлянул Плохой тест: • не проверяет поведение; • имеет непонятное имя; • всегда проходит; • не помогает найти ошибку. Хороший тест: * проверяет конкретное правило; • имеет понятное имя; • падает, если правило нарушено; • показывает, что именно сломалось Пример хорошего имени: void rankUpShouldlncreaseRankWhenScudentHasEnoughPoints() По имени видно: что проверяем: ' rankllp ; при каком условии: ученик имеет достаточно очков
что ожидаем: ранг повышается 13.5. Испытания главы Малое испытание 1 Подключи JUnit Добавь в рот.хтГ зависимость junit-jupiter и настрой запуск тестов через Maven Surefire Plugin. Запусти: mvn test Награда: +20 очков секты Малое испытание 2. Тест начисления очков Напиши тест: addSectPointsShouldlncreasePoints Проверь, что после начисления 50 очков у ученика стало 50 очков Награда +15 очков секты Малое испытание 3. Тест запрета отрицательных очков Напиши тест через 'assertThrows . который проверяет, что addSectPoints(-10)' бросает IllegalArgumentException'. Hai рада +20 очков секты Малое испытание 4. Тест повышения ранга Проверь сценарий: • ученик получил 120 очкоь; • вызван 'rankUpO'; • ранг повысился; • очки были списаны по правилу Награда- +25 очков секты Повреждённый свиток. Что не так? @Test void registerTest () { Cultivatorservice service = new CultivatcrService();
service.register(new Cultivator ("ST-001", "Путник")); Испытание старейшины. Защита от саботажа Напиши тесты: * сервис не принимает 'null' вместо ученика; • сервис не принимает null' вместо награды; • отрицательные очки невозможны; • поиск отсутствующего ученика возвращает Optional empty()'; рейтинг сортируется по очкам в правильном порядке. Награда +50 очков секты. Барьер прорыва. Доказанная техника К концу главы у тебя должны быть: • Maven-проект; • JUnil в зависимостях; • тесты модели Cultivator'; • тесты сервиса; • проверка успешных сценариев; • проверка ошибочных сценариев: • команда 'mvn test', которая завершается успешно. Обнови табличку ученика: Имя: __________ Статус: кандидат финального отбора Ранг: Кандидат турнира Артефакт сборки: Maven-проект Освоенные техники: JUnit, assertEquaLs, assertTrue, assertThrows, регрессия Очки секты: +1144 Барьер прорыва: Доказанная Техника открыт Стабильность ци кода: высокая Когда тесты запустили на проектах ученикоь, зал наполнился разными звуками. У одних пьедесталы загорались зелёным, у других — красным. Проект Линь Грейдла прошёл сборку, но упал на тесте отрицательных очков Expected java.lang.IllegalArgumentException to be thrown, but nothing was thrown. Линь побледнел.
— Но вручную я это не проверял Старейшина Испытаний ответила — Именно поэтому старейшины не верят ручным словам Проект Мэй Тест прошёл почти все проверки Один тест упал из-за неправильной сортировки рейтинга Она сразу открыла код и спокойно исправила сравнение Наставник посмотрел на учеников — Теперь вы знаете, почему тесты нужны не только стражам качества Любой программист, который меняет код без проверки, идёт по краю обрыва. В конце зала открылись последние ворота внутреннего состязания. За ними была не арена и не павильон. Там стоял пустой рабочий стол. На нем лежала табличка: Финальное испытание Собрать систему секты Старейшина Сборки сказал: — Теперь отдельные техники закончились Старейшина Испытаний добавила — Дальше мы смотрим не на упражнение, а на систему. Наставник повернулся к тебе. — Пришло ^ремя собрать Sect Management System Последняя ночь перед финальным испытанием Ночь перед финальным испытанием была тихой Слишком тихой. Обычно во внешнем дворе кто-нибудь спорил с компилятором, кто-нибудь забывал закрыть скобку, кто- нибудь доказывал, что его код "почти работает”, а кто-нибудь из старших учеников уходил медитировать после слова “dependency". Но теперь ученики молчали На турнирной доске горели имена кандидатов Путник Линь Гоейдл Мэй Тест Боолянь Истинный Сунь Цикл Рядом с каждым именем стояли печати core, passed objects, passed collections: passed exceptions: passed
files passed maven: passed tests, passed Линь Грейдл стоял перед доской дольше остальных. — Раньше я думал, что главное — написать быстрее всех, — сказал он неожиданно тихо. Мэй Тест посмотрела на него с удивлением — И? — Теперь я думаю, что быстро написать код, который потом никто не может проверить, — это просто быстрый способ создать проклятый свиток. Мэй чуть улыбнулась — Неплохой прогресс для человека, который хотел хранить сто учеников в ста переменных. — Я был молод — Это было три дня назад. Линь сделал вид, что не услышал К ним подошёл наставник Павильона Основ — Завтра вы больше не будете решать отдельные задачи. Завтра вы соберёте систему. Он провел рукой по воздуху, и перед учениками появились контуры будущего проекта: model service storage арр test — Модель будет хранить форму мира Сервис — правила. Хранилище — память Приложение — голос системы Тесты — печати, которые докажут, что всё это не рушится от первого удара Путник посмотрел на схему — А если мы ошибёмся? Наставник ответил не сразу — Ошибётесь Обязательно Но теперь вы знаете, как искать причину. В этот момент турнирная доска мигнула. На ней появилась последняя строка unknown_student. final_attempt^prepared
Ученики застыли, Мэй Тест медленно достала свой свиток проверок. Линь Грейдл положил руку на рукоять деревянного меча, хотя против кода меч был бесполезен. Наставник Павильона Основ не выглядел удивлённым — Значит, финальное испытание будет честным — Честным9 — переспросил Сунь Цикл Старик улыбнулся. — Конечно. Настоящая система проверяется не тогда, когда все ведут себя правильно Она проверяется тогда, когда кто-то । «водит плохие данные, ломает порядок, забывает сохранить состояние или пытается пройти туда, куда ему нельзя Турнирная доска погасла Над главным залом загорелась последняя печать внутреннего отбора. Sect Management System До рассвета оставалось совсем немного.
Глава 14. Финальное испытание: система секты Финальный зал внутреннего состязания оказался самым простым Ни золотых ворот Ни боевых кругов Ни древних статуй Только рабочие столы, артефакты разработки, чистые свитки требований и большая доска Финальное испытание: Система секты Цель: создать консольное приложение Sect Management System Срок: до заката Проверка: сборка, запуск, тесты, сохранение данных Награда: право представлять Секту Java Core на Межсектовом Турнире Основ Ученики молчали. Раньше каждое испытание было ограниченным написать цикл создать класс, исправить исключение, сохранить файл. Теперь всё нужно было соединить Наставник Павильона Основ встал перед доской — В настоящем проекте редко бывает так: сегодня только циклы, завтра только методы, послезавтра только enum. В реальности все техники встречаются вместе Классы, коллекции, условия, исключения, файлы, пакеты, тесты, сборка Финальное испытание проверит, можешь ли ты собрать из частей маленькую, но рабочую систему Старейшина Сборки добавил — На Межсектовый Турнир поедут не те, кто знает больше терминов Поедут те, кто способен довести артефакт до состояния "можно собрать, запустить и проверить”. 14.1, Требования к Sect Management System Система должна уметь: • создать ученика; • показать список учеников; • найти ученика по id: • начислить очки, • повысить ранг; • показать топ учеников, • сохранить данные в файл: * загрузить данные из файла, • обработать ошибки; • покрыть ключевую логику тестами
Итоговый проект — это не "один огромный mam”. Это несколько классов, разделённых по ответственности. Модель хранит данные, сервис содержит правила, storage сохраняет и загружает, арр запускает приложение 14.2. Базовая структура проекта sect-management-system/ — pom.xml I— src/ — main/ L java/ *— core/ L sect/ - арр/ *— SectApplication.java i— exception/ *— CultivatorNotFoundException.java — model/ — Cultivator.java I— Rank.java — service/ L CultivatcrService.java storage/ L CultivatorFileStorage.java — test/ L java/ L core/ *— sect/ •— service/ *— CultivatorServiceTest.java Это не единственно возможная структура. Но она достаточно ясна для первого проекта Ранг лучше хранить не строкой, a enum. Старейшина спросил - Почему не String rank = "NOVICE" ? Мэй Тест ответила — Потому что строку можно написать с ошибкой' '"NOVCIE"', "novice"', '"Новичок"' Enum ограничивает допустимые значения. Наставник кивнул — Фиксированные правила мира должны быть фиксированными Модель Cultivator
package core.sect.model; import java.util.Objects; public class Cultivator ( private final String id; private final String name; private Rank rank; private int sectPoints; } Шаблон сервиса CultivatorService package core.sect.service; public class CultivatorService { private final List<Cultivator> cultivators = new ArrayListo (); public void register (Cultivator cultivator) {...} public List<Cultivaror> findAllO (...) public Cultivator findByld(String id) (...) public void addPoints (String id, int points) {...} public void rarkUp(String id) {...} public List<Cultivator> topByPoints(int limit) (...) Хранение в файл Для простого учебного проекта можно сохранять данные в текстовый файл Формат строки: id;name;rank;sectPoints Пример файла ST-001;Путник;STUDENT;45 ST-002;Мэй Тест;TOURNAMENT.CANDIDATE;120 14.3. Консольное приложение и Scanner Чтобы пользователь мог выбирать действия, используем ’Scanner'. package core.sect.арр; import core.sect.model.Cultivator; import cere.sect.service.CultivatorService; import java.util.Scanner;
public class SectApplication { public static void main (String!) args) { Cultivatorservice service = new Cultivatorservice(); Scanner scanner = new Scanner(System.in); boolean running = true; while (running) ( printMenu(); String command = scanner.nextLine(); switch (command) ( default -> System.out.println("Неизвестная команда."); } } System.out.println("Система секты завершила работу."); ) private static void printMenu() { System.out.println(); System.out.println("1. Создать ученика"); System.out.println("2. Показать ученике "); System.out.println("3. Начислить очки"); System.out.println("4. Показать топ"); System.out.println("0. Выход"); System.out.print(" i6op: "); } private static void register(Scanner scanner, Cultivatorservice service) ( System.out.print("ID: "); String id = scanner.nextLine(); System.out.print("Имя: "); String name = scanner.nextLine(); service.register(new Cultivator(id, name)); System.out.println("Ученик создан."); } private static void showAll(Cultivatorservice service) { } private static void addPoints(Scanner scanner, CultrvatorService service) ( System.out.print("ID: "); String id = scanner.nextLine (); System.out.print("Очки: "); int points = Integer.parselnt(scanner.nextLine()); service.addPoints(id, points); System.out.println("Очки начислены."); } private static void showTop(Cultivatorservice service) { for (Cultivator cultivator : service.topByPoints (3)) ( System.out.println(cultivator); } } I
Этот код еще не идеален Например, если пользователь введет текст вместо числа, Integer.parselnt бросит исключение. Но это нормальная точка для улучшения 14.4. Где должны быть тесты финального проекта Минимальный набор тестов • создание ученика с пустым именем запрещено; * отрицательные очки запрещены; • ученик повышает ранг при достаточных очках; • сервис не регистрирует дубликат id; • поиск несуществующего ученика бросает 'CultivalorNotFoundException'; • топ учеников сортируется по очкам:; • сохранение и загрузка возвращают ожидаемые данные Пример теста топа @Test void topByPointsShouldReturnBestCultivatorsFirst() { CultivatcrService service = new CultivatcrService(); Cultivator first = new Cultivator("ST-CO1", "Путник"); first.addSectPoints(50); Cultivator second = new Cultivator ("ST-002", "Мэй Тест"); second.addSectPoints(120); service.register(first); service.register(second); List<Cultivator> top = service.topByPoints(2); assertEquals("ST-002", top.get(1).getld()); assertEquals("ST-001", top.get(1).getld()); } Старейшина Испытаний сказала: — Финальный проект без тестон похож на меч без ножен Может быть острым, но носить его опасно. 14.5. Финальное испытание; собрать систему Задание финального испытания: Создай консольное приложение Seel Management System4. Обязательные функции. • создать ученика; • показать всех учеников. • найти ученика по id: • начислить очки.
♦ повысить ранг; • показать топ-3 учеников; • сохранить ученикор* в файл; • загрузить учеников из файла при старте; • обработать неверный ввод, • покрыть ключевые правила тестами Ограничения: • не хранить всё в одном классе; * не открывать поля напрямую; • не принимать отрицательные очки; • не позволять дубликаты id: • не падать без понятного сообщения при ошибке пользователя. • не сдавать проект, если 'mvn test' падает. Дополнительные функции для сильных учеников: • добавить задания и награды; • добавить историю операций; • добавить удаление ученика; • добавить поиск по имени: • добавить экспорт топа н отдельный файл; • добавить больше тестов на storage. Турнирный зачёт Система оценивается по нескольким направлениям: Структура проекта: до 20 очков Модель и инкапсуляция: до 2в очков Сервисная логика: до 25 очков Файловое хранение: до 20 очков Обработка ошибок: до 20 очков Тесты: до 30 очков Чистота кода: до 20 очков Итого: до 155 очков Старейшина Испытаний добавила — Дополнительные функции не спасут проект, если базовые правила сломаны Сначала стабильность, потом украшения Пинь Грейдл посмотрел на свою систему и впервые за долгое время ничего не сказал. 14.6. Испытания главы Малое испытание 1. Собери модель
Создай Rank и Cultivator. Проверь ♦ имя не пустое; • id не пустой; * стартовый ранг NOVICE'; • стартовые очки О Награда- +25 очков секты Малое испытание 2. Собери сервис Создай 'CultivatorService' с методами; • 'register'; • 'findAII'; • 'findByld'; • addPoints'; • 'rankUp'; • topByPoints'. Награда: +35 очков секты Малое испытание 3. Добавь storage Создай 'CultivatorFileStorage . Сохрани список учеников в файл и загрузи обратно Награда: +35 очков секты. Малое испытание 4. Добавь консольное меню Добавь команды • создать ученика; • показать учеников; • начислить очки: • показать топ; • выйти. Награда: +30 очков секты Испытание старейшины. Покрой систему тестами Напиши минимум 8 тестов
• создание ученика; • запрет пустого имени; • запрет отрицательных очков; • повышение ранга, • запрет дубликата, • поиск отсутствующего id; • сортировка топа; * сохранение и загрузка. Награда +60 очков секты. Барьер прорыва. Внутренний ученик Финальная проверка mvn clean test mvn clean package Проект считается пройденным, если: • сборка успешна; • тесты зелёные; • приложение запускается; ♦ данные можно сохранить и загрузить; • ошибки пользователя не ломают систему молча; • код разделен по пакетам Обнови табличку ученика: Имя: __________ Статус: внутренний ученик Ранг: Представитель секты Итоговый артефакт: Sect Management System Освоенные техники: Maven, packages, imports, JUnit, model/service/storage/app, консольное меню Очки секты: +1369 Барьер прорыва: Внутренний Ученик открыт Стабильность ци кода: высокая Репутация: кандидат Межсектового Турнира 14.7. Финал внутреннего состязания Когда солнце начало опускаться за горы, старейшины запустили финальную проверку. Один за другим проекты проходили через печи сборки mvn clean test
У кого-то падали тесты У кого-то приложение не сохраняло файл. У кого-то меню работало, но сервис принимал дубликаты У кого-то все было красиво в main, но без тестов и структуры. Проект Мэй Тест прошел проверку почти идеально Ее storage-тест сначала упал на пустом файле, но она быстро исправила обработку. Проект Линь Грейдла собрался, но старейшина нашла проблему он снова отдал наружу внутренний список учеников public List<Cultivator> findAll() { return cultivators; } Мэй тихо сказала — Так внешний код может сделать clear()'. Линь сжал зубы, но исправил public List<Cultivator> findAllО { return new ArrayListo (cultivators); } Старейшина Испытаний кивнула. — Принято. Когда очередь дошла до твоего проекта, зал стих. Старейшина Сборки запустил сборку, BUILD SUCCESS Старейшина Испытаний запустила тесты. Tests run: , Failures: 0, Errors: 0, Skipped: О Наставник открыл приложение. 1. Создать ученика 2. Показать учеников 3. Начислить очки 4. Показать тол С. Выход Он создал ученика, начислил очки, повысил ранг, сохранил файл, перезапустил систему и загрузил данные обратно Архивная печать вспыхнула зелёным Старейшины переглянулись Наставник Павильона Основ впервые за долгое время улыбнулся без усталости. — Ты больше не просто внешний ученик, который запускает отдельные свитки. Ты собрал маленькую систему. Она не идеальна. Она не готова к настоящему production Но она имеет форму, правила, память и проверки
Турнирная доска вспыхнула: Финальный отбор завершён Путник: допущен Мэй Тест: допущена Линь Грейдл: допущен после исправления Статус: представители Секты Java Core выбраны В зале поднялся шум. Кто-то радовался Кто-то устало лежал на столе рядом с рот хтГ. Кто-то впервые понял, что тесты были не наказанием, а спасением Старейшина Сборки подошел к тебе и протянул новую табличку На ней было написано Статус: внутренний ученик Право представитель Секты Java Core Назначение. Межсектовый Турнир Основ Но радость длилась недолго. За окнами финального зала раздался далёкий удар большого колокола Один Второй. Третий Наставник посмотрел на горную дорогу, уходящую за пределы секты. — Турнир начинается раньше, чем мы думали У ворот уже собирались повозки На них были знаки разных путей: SQL, Git. REST, Docker Вдали, в тумане, стояла высокая башня с зелёной эмблемой листа Пинь Грейдл спросил — Что это за секта? Наставник ответил не сразу. — Павильон Spring Старейшины замолчали. Ты сжал табличку внутреннего ученика Java Core больше не казался бесконечной горой. Но теперь стало ясно это была только первая вершина
Глава 15. Врата следующего мира Дорога к Межсектовому Турниру Основ оказалась длиннее, чем казалась из финального зала Внутри секты все павильоны уже стали знакомыми. Зал Тысячи Повторов, Арена Чистого Кода, Павильон Духовной Формы, Архив поврежденных свитков, Павильон Артефактов — каждый из них оставил на твоей табличке новую печать. Но за воротами Секты Java Core мир был шире Повозки представителей двигались по каменной дороге между горами На первой ехали ученики Java Core ты, Мэй Тест, Линь Грейдл и еще несколько победителей внутреннего отбора. У каждого на поясе висела новая табличка Статус: внутренний ученик Прово: представитель Секты Java Core Назначение: Межсектовый Турнир Основ Линь Грейдл всю дорогу молчал. После финального испытания он стал менее громким, но не менее упрямым. Мэй Тест перебирала список тестов на своей табличке и тихо проверяла, все ли сценарии покрыты. Наставник Павильона Основ сидел впереди и смотрел на дорогу. 7ы спросил — Мы готовы? Наставник не ответил сразу. — К чему именно? — К турниру Он усмехнулся. — К турниру основ — да. К настоящему миру разработки — только начали. Повозка поднялась на перевал. Туман разошелся, и перед вами открылся огромный город из башен, павильонов, арен и зисящих в воздухе мостов Над главными воротами горела надпись Межсектовый Турнир Основ У всрот стояли представители разных путей. Ученики в синих мантиях Секты SQL несли каменные таблицы с колонками и ключами. Члены Ордена Git спорили над ветвями, которые буквально росли из их свитков Адепты Башни REST ныпускали в воздух светящиеся запросы GET, POST, PUT, DELETE. Из Долины Docker катилась повозка с одинаковыми контейнерами, каждый из которых запускал кнутри маленький мир А дальше всех стоял высокий зелёный павильон с эмблемой листа. Линь Грейдл нахмурился. — Это тот самый Павильон Spring9
Наставник кивнул. — Да И именно туда ведёт вторая книга 15.1. Что ты уже умеешь Перед входом на турнир каждому представителю выдали зеркало проверки основ В нём не отражалась внешность. Оно отражало техники, которые ученик действительно освоил На твоем зеркале одна за другой вспыхнули строки JDKj JVM, javac: освоено Переменные и типы: освоено Условия и switch: освоено Циклы: освоено Методы: освоено Классы и объекты: освоено Инкапсуляция: освоено static, finaL, епит: освоено Наследование и интерфейсы: освоено Коллекции и generics: освоено equats/hashCode: освоено nuLL и exceptions: освоено Файлы: освоено Maven: освоено JUnit: освоено Итоговый проект: собран Мэй Тест посмотрела на зеркало и сказала — Выглядит красиво Но "освоено" не значит "больше не ошибаюсь”. Наставник довольно кивнул — Верно Освоенная техника — это не техника, в которой ты никогда не ошибаешься Это техника, ошибку в которой ты уже умеешь найти, объяснить и исправить. После книги ты должен не просто помнить синтаксис, а понимать базовые механики Java-программы: как запускается код. как хранится состояние, как работают объекты, как данные проходят через коллекции, как обрабатываются ошибки, как проект собирается и как логика проверяется тестами. Чек-лист Java Core Отметь пункты, которые ты можешь объяснить без подсказки и применить н маленькой задаче Раздел Что нужно уметь Проверка Запуск Java Отличить JDK, JVM. javac, java и Создать и запустить class 'Hello.java
Типы и переменные Условия Циклы Хранить числа, строки, boolean- Создать профиль ученика состояние Писать 'if, else’, 'switch Реализовать выбор действия Использовать for, while , break’, Смоделировать 14 дней 'continue подготовки Методы Классы Инкапсуляция Выносить повторяющуюся логику Написать ’calculateReward()' Создавать объекты и конструкторы Создать Cultivator' Защищать состояние через private Запретить отрицательные очки enum Заменять случайные строки Создать Rank и Action фиксированными значениями Наследование Понимать 'extends', 'abstract’, Создать разные типы 'interface учеников Коллекции Использовать ’ArrayList’, HashSet’, Сделать рейтинг участников HashMap equals/hashCode Понимать сравнение объектов в Сравнивать учеников по id’ Set' и 'Мар Exceptions Бросать и ловить ошибки Запретить некорректную осознанно награду Files Maven JUnit Читать и писать простые файлы Сохранить рейтинг Собирать проект одной командой Выполнить mvn clean test Проверять логику тестами Написать тесты сервиса Наставник сказал — Если хотя бы лолпвина этих строк вызывает туман в голове, это не провал. Это карта повторения. 15.2. Почему Java Core — это не backend-разработка На площади турнира ученики Java Core остановились перед огромной ареной. Над ней висели слова: Основы открывают, но не заменяют путь. Пинь Грейдл скрестил руки. — Но если я знаю Java Core, разве я уже не backend-разработчик? Наставник посмотрел на него с таким ныражением, будто этот вопрос он слышал много сотен раз. — Нет Ты стал учеником, который понимает язык и может собрать маленькую систему. Backend начинается там. где твоя программа перестаёт быть одинокой Он начертил на камне простую схему:
Пользователь I HTTP-запрос Control. Ler I Service I Repository l Database — В Java Core ты учился управлять кодом внутри программы В backend-мире программа получает запросы от других систем, ходит в базу данных, возвращает ответы, обрабатывает ошибки сети, работает с параллельными пользователями, хранит данные надежнее обычного файла и должна запускаться в < кружении, которое не похоже на твой личный ноутбук Java Core — это фундамент языка и базовых библиотек Backend-разработка дополнительно требует понимания HTTP, REST, SQL, баз данных, Spring или другого framework, сериализации JSON, легирования, конфигурации, безопасности, транзакций, деплоя и командной разработки. Мэй Тест добавила — И тесты там тоже не исчезают. — Они становятся важнее, — овтетил наставник 15.3. Карта следующих сект На площади турнира стояла большая карта Луги на ней расходились от Секты Java Core к разным павильонам Секта SOL У ее ворот стояли таблицы, связи и ключи. Наставник сказал: — Почти любое серьезное backend-приложение хранит данные не в обычном текстовом файле, а в базе данных SOL — язык, с помощью которого ты задаёшь вопросы данным. SELECT name, sect_points FROM cultivators WHERE rank = 1 INNER DISCIPLE1 ORDER BY sect points DESC; SQL нужен, чтобы создавать таблицы, искать данные, фильтровать, сортировать, связывать записи и обновлять состояние системы Орден Git
У Ордена G‘t ученики носили таблички с названиями ветвей: main, feature, bugfix, release — Git, — сказал наставник, — это техника памяти проекта Она позволяет сохранять историю изменений, работать в команде, откатываться назад и понимать, кто, когда и зачем изменил код. main — feature/add-cuLtivator-search — bugfix/rank-caLcuLation I— reLease/book-one-finaL Без Git проект быстро превращается в папки final, fmal2, really-final, really-final-fixed Линь Грейдл тихо отвёл взгляд Башня REST Из Башни REST вылетали светящиеся методы. GET /cultivators POST /cultivators PUT /cultivators/{id) DELETE /cultivators/{id) — REST, — сказал наставник, — это один из распространённых способов организовать взаимодействие между клиентом и сервером через HTTP. Если твоя консольная система секты станет backend-приложением, то пользователь больше не будет выбирать пункты меню в терминале Он будет отправлять запросы POST /cultivators Body: { "name": "Путник" } А сервер будет отвечать: 201 Created { "id": "с-001", "name": "Путник", "rank": "NOVICE" } Долина Docker В Долине Cocker каждый артефакт запускался в отдельном контейнере Наставник объяснил: — На твоем ноутбуке проект может работать, На ноутбуке Мэй — тоже. На сервере старейшины — внезапно нет. Docker помогает упаковать приложение и его окружение так, чтобы запуск был предсказуемое Docker не заменяет понимание программы. Он помогает запускать её в изолированном окружении с заданными настройками
Павильон Spring Зеленый павильон стоял дальше всех. Его двери были закрыты, но вокруг них вращались странные печати' 9RestController (^Service (^Repository GAutowired @Configuration Линь Грейдл прищурился — Это выглядит как магия Наставник покачал головой. — Для того, кто не знает Java Core, Spring действительно выглядит как магия Для того, кто знает классы, интерфейсы, объекты, исключения и пакеты, это уже не магия, а следующий уровень организации приложения На мгновение двери Павильона Spring приоткрылись Внутри ты увидел боевую формацию backend-мира @RestCcntroller aReques tMapping ("/cultivators") public class CultivatorControiler { private final Cultivatorservice service; public CultivatorControiler(Cultivatorservice service) ( this.service = service; } @GetMapping public List<CultivatorResponse> findAHQ ( return service.findAxlО; } ) Двери закрылись. Мэй Тест тихо сказала: — Я понимаю ciass, private final, конструктор и List Но не понимаю аннотации. Наставник улыбнулся — Значит, ты готова к следующей книге Хороший ученик отличает знакомое от неизвестного. 15.4. Что повторить перед зторой книгой Старейшина Турнира выдал каждому представителю свиток подготовки На нем не было новых тем Только старые, но в более строгой форме — Перед следующей книгой, — сказал наставник, — тебе не нужно становиться бессмертным мастером Java Core. Но некоторые техники должны быть достаточно прочными, чтобы выдержать Spring, SQL и REST
Мини-план повторения на семь дней День 1: переменные, условия, циклы День 2: методы и разбиение логики День 3: классы, конструкторы, this День 4: private, finat, enum, инварианты День 5: ArrayList, HashSet, HashMap, equals/hashCode День 6: exceptions, Files, сохранение и загрузка День 7: Maven, JUnit, запуск всех тестов Как повторять правильно Не перечитывай главы как художественный текст. Повторение должно оставлять след в коде. Правило наставника Одна тема = один маленький кодовый артефакт. Если повторяешь коллекции — напиши рейтинг. Если повторяешь исключения — создай ошибочный сценарий и обработай его. Если повторяешь тесты — сначала сломай код. потом докажи тестом, что починил. Если повторяешь файлы — сохрани данные, закрой программу, запусти снова и загрузи результат. Знание синтаксиса без практики быстро превращается в иллюзию знания. Маленькие задачи важнее пассивного перечитывания. 15.5. Подготовка проекта ко второй книге Наставник положил перед тобой свиток с названием Sect Management System: подготовка к backend-пути — Твой финальный проект из главы 14 не нужно выбрасывать. Его нужно подготовить такг чтобы во второй книге превратить консольное приложение в backend-систему. Что должно быть в проекте sect-management-system/ - README.md — pom.xml — src/ — main/java/core/sect/ - арр/ — model/ — service/ — storage/ * — exception/ test/java/core/sect/ — service/ * — storage/
L data/ L cultivators.txt README как табличка проекта Повреждённый проект без README похож на ученика без таблички Он может быть сильным, но никто не понимает, кто он и что умеет Минимальный README.md: я Sect Management System Консольное приложение для управления учениками Секты Java Core. М Возможности - создать ученика; - начислить очки; - повысить ранг; - показать рейтинг; - сохранить и загрузить данные. М Запуск mvn clean test mvr. exec:java # # Основные классы - Cultivator - модель ученика; - Rank - ранг ученика; - Cultivatorservice - бизнес-логика; - Cultivator?ileStorage - сохранение и загрузка. README не делает код лучше сам по себе, но помогает другому человеку понять, как проект собрать, запустить и проверить. Подготовительные задачи Перед второй книгой улучши проект так, чтобы он был удобной базой для backend- развития • Убедись, что mvn clean test проходит без ошибок. • Удали временные классы вроде TestMain', FtnalMain','OldService . • Проверь, что сервис не отдает наружу изменяемые внутренние коллекции. * Добавь тест на невозможность создать ученика с пустым именем • Добавь тест на невозможность начислить отрицательные очки * Добавь тест на сохранение и загрузку минимум двух учеников • Добавь README. • Раздели код по пакетам model’, 'service', storage', 'exception', 'app
15.6. Повреждённый свиток: ложный прорыв. На площади турнира один ученик из другой школы громко заявил — Я уже знаю Java Core, значит. Spring выучу за вечер Наставник ничего не сказал. Только показал его проект старейшинам Внутри было так: Main.java EverythingService.java Data.java Utils.java NewUtils.java NewNewUtils.java А в коде public static ArrayList data = new ArrayList(); try { doEverything(); } catch (Exception e) { // ignore } И ещё: if (rank == "INNER") { points = points + 10000C0; } Старейшина Турнира спросил. — Что здесь повреждено9 Мэй Тест ответила первой: — Raw type в ArrayList. Потеря типобезопасности. Пинь добавил: — public static состояние доступно отовсюду Ты заметил: — catch (Exception е) скрывает ошибку. И строки сравниваются через ==, а не через equals или enum. Наставник кивнул. — Хорошо. Это и есть разница между учеником, который “что-то видел”, и учеником, который понимает, почему код опасен Правильнее:
private final List<Cultivator> cultivators = new ArrayListo (); public void addCultivator(Cultivator cultivator) { if (cultivator == null) { throw new IllegalArgumentException("cultivator must not be null"); } cultivators.add(cultivator); } И вместо строкового ранга: if (rank == Rank.INNER DISCIPLE) ( points += 100; } Переход к framework не исправляет плохую основу. Если в Java Core-коде хаос, то Spring только сделает этот хаос крупнее и дороже. 15.7. Дуэль ученикон быстро прыгнуть или закрепить фундамент Перед открытием турнира старейшины устроили последнюю учебную дуэль. На арену вышли Линь Грейдл и Мэй Тест. Задание звучало просто Подготовить проект Sect Management System к передаче другой команде. Линь сразу сказал: — Я добавлю Spring. REST, базу данных и Docker. Старейшина спросил, — Текущие тесты проходят? Линь замолчал. Мэй сказала — Я сначала проверю Maven-сборку. README, тесты, структуру пакетов, обработку ошибок и сохранение данных. Потом уже буду переносить логику в backend. Старейшина повернулся к ученикам — Кто прав9 Некоторые указали на Линя. Его план звучал сильнее Но наставник сказал: — Мэй права. Новая техника должна опираться на устойчивую основу. Если фундамент треснул, не надо строить на нем башню Линь нахмурился, но на этот раз не спорил — Значит, сначала mvn clean test? Мэй улыбнулась, — Сначала mvn clean test.
15.8. Испытание старейшины: свиток готовности Старейшина Турнира выдал тебе последний свиток первой книги. Испытание старейшины: готовность ко второй книге Задание Возьми итоговый проект Sect Management System и подготовь его как основу для следующего уровня Требования: • проект собирается через Maven; • все тесты проходят; * есть README с описанием запуска; ♦ классы лежат в понятных пакетах; • нет raw types; • нет публичных изменяемых коллекций; • ошибки не скрываются пустым 'catch'; • Rank' и действия представлены через enum', а не случайные строки; • есть минимум один тест на успешный сценарий; * есть минимум один тест на ошибочный сценарий; • данные можн сохранить и загрузить Награда: +100 очков секты. 15.9. Турнирный зачёт: финальная проверка основ Перед входом в главную арену каждый представитель должен был пройти короткий зачет Не ради новых очков Ради честной проверки. Задача 1. Рейтинг Создай метод: public List<Cultivator> gecTopCultivators(inc limit) Он должен возвращать топ учеников по очкам секты. Проверь: • если 'limit равен 3, возвращаются максимум 3 ученика; • ученики отсортированы по убыванию очков; • внешний код не может изменить внутренний список сервиса. Задача 2. Ошибочная награда
Метод: public void addPoints(Strinq cultivatorld, int points) Должен бросать IllegalArgumentException, если points <= 0 Напиши тест через asserlThrows. Задача 3. Сохранение Сохрани двух учеников в файл, затем загрузи их обратно и проверь, что имена и очки сохранились. Задача 4. README Напиши короткий README и укажи; • что делает проект: • как его собрать, • как запустить тесты, • какие классы являются основными Задача 5. Объяснение Ответь письменно на три вопроса: • Вопрос 1 Почему Java Core не равен backend- разработке? • Вопрос 2. Зачем нужны тесты перед переходом к Spring? • Вопрос 3. Почему файл — это учебное хранилище, а не полноценная база данных? Наставник сказал — Тот, кто может объяснить свои решения, уже не просто копирует техники 15.10. Барьер прорыва Врата второй книги Когда зачет закончился, зеркало проверки основ вспыхнуло золотым светом. На твоей табличке появились новые строки. Имя: __________ Статус: внутренний ученик Ранг: Представитель Секты Java Core Итоговый артефакт: Sect Management System Java Core: фундамент заложен Барьер прорыва: Врата второй книги открыты Следующий путь: backend-разработка
Наставник положил руку на твое плечо. — В первой книге ты прошёл путь от первого запуска до маленькой системы. Это хороший путь. Но теперь важно понять, чем больше система, тем меньше помогает одиночная сила Нужны архитектура, протоколы, базы данных, конфигурация, тесты, сборка, командная работа и умение читать чужой код без желания уйти в горы. Линь Грейдл тихо спросил: — А желание уйти в горы проходит9 Наставник задумался — Нет Но со временем ты берешь ноутбук с собой. Ученики засмеялись На главной арене Межсектового Турнира Основ зажглись огни Секта SQL подняла таблицы. Орден Git раскрыл ветви Башня REST выстроила маршруты. Долина Docker запустила контейнеры. И наконец двери Павильона Spring открылись полностью. Изнутри *ышел старейшина в зелёной мантии. Его голос был спокойным, но в нём слышалась сила большой системы. — Представители Java Core, — сказал он. — Вы освоили ядро. Теперь вы узнаете, как из отдельных классов строятся backend-приложения, как запрос превращается в действие, как сервис говорит с базой данных и почему аннотация может изменить судьбу целого класса. Он поднял руку, и в воздухе вспыхнула новая надпись. Книга 2. Павильон Spring Путь backend-разработчика начинается* Ты посмотрел на свою табличку Когда-то на ней было написано: Статус: смертный Корень духа: не пробуждён Теперь там горело' Статус: представитель Секты Java Core Барьер: Врата второй книги открыты Наставник тихо сказал. — Запомни это чувство Сегодня ты не закончил путь Ты закончил первую гору
Врата следующего мира открылись, и ты сделал шаг -.перед Финальное послесловие ученика Первая книга закончена, если у тебя есть не только прочитанные главы, но и код, который можно открыть, собрать и запустить. Минимальный результат первой книги- 1. Ты можешь создать Java-проект. 2. Ты можешь описать предметную область классами. 3. Ты можешь защитить состояние объектов. 4. Ты можешь хранить много объектов в коллекциях. 5. Ты можешь обработать ошибочные сценарии. 6. Ты можешь сохранить данные в файл. 7. Ты можешь собрать проект через Maven. 8. Ты можешь проверить логику через JUnit. 9. Ты можешь объяснить, где заканчивается Java Core и начинается backend. Финальное испытание без награды: Открой свой проект через неделю после завершения книги. Если ты всё ещё понимаешь, где лежит модель, где сервис, где тесты и как запустить сборку, значит, артефакт действительно принадлежит тебе Если не понимаешь — это не поражение Это значит, что проект честно показал, где нужно укрепить фундамент Путь программиста продолжается не потому, что все понятно Он продолжается потому, что теперь ты умеешь разбирать непонятное по частям.