Итоговый проект: библиотека
Чему вы научитесь:
- собирать программу из деталей прошлых тем: записи, класса со словарём, своего исключения и стрима;
- читать метод
borrowпо шагам: поиск в словаре, проверка, исключение или изменение; - предсказывать, что напечатает программа, когда книгу просят второй раз;
- выбирать, что изучать после курса.
Представьте стойку выдачи в библиотеке. На стойке стоит ящик с карточками: на каждое название заведена карточка, и на ней записано, сколько экземпляров стоит на полке. Читатель подаёт листок с названием, и библиотекарь достаёт нужную карточку.
Дальше всё решает число на карточке. Если оно больше нуля, библиотекарь выдаёт книгу и уменьшает число на один. Если экземпляров нет, он нажимает звонок на стойке и отказывает: «Этой книги на полке нет».
Итоговый проект — такая стойка на Java. Новых конструкций в нём нет: каждая деталь пришла из прошлых тем. Задача — увидеть, как детали работают вместе:
| Деталь | Откуда она |
|---|---|
запись Book | тема о записях record |
исключение LoanException | тема об исключениях |
класс Library со словарём | темы о классах и коллекциях |
проверка внутри borrow | темы об условиях и обёртках |
| отчёт по каталогу | тема о стримах |
Книга и отказ
Начнём с данных. Книга — запись из названия и года издания. Отказ — своё исключение с понятным сообщением:
public class Main {
public static void main(String[] args) {
Book dune = new Book("Dune", 1965);
var refusal = new LoanException(dune.title());
System.out.println(refusal.getMessage()); // Dune is not on the shelf
}
}
record Book(String title, int year) {}
class LoanException extends Exception {
LoanException(String title) {
super(title + " is not on the shelf");
}
}Давайте разберём по частям:
record Book(String title, int year)— описание книги. Конструктор, методыtitle()иyear(),equalsиtoStringпишет компилятор.LoanExceptionнаследуетException, значит, исключение проверяемое. Отказ в выдаче — обычная ситуация, и компилятор заставит вызывающий код к ней подготовиться.- Конструктор принимает название и сам собирает сообщение: передаёт родителю
title + " is not on the shelf". - В
mainисключение пока не бросают. Это проверка деталей:getMessage()возвращает собранное сообщение.
Стойка выдачи
Ящик с карточками в программе — закрытое поле copies класса Library. Это словарь Map<String, Integer>: ключ — название, значение — число экземпляров на полке.
class Library {
private final Map<String, Integer> copies = new HashMap<>();
void add(Book b) {
copies.put(b.title(), copies.getOrDefault(b.title(), 0) + 1);
}
void borrow(String title) throws LoanException {
Integer n = copies.get(title);
if (n == null || n == 0) {
throw new LoanException(title);
}
copies.put(title, n - 1);
}
}Метод add считает экземпляры: getOrDefault даёт 0 для новой книги, а put записывает число на один больше. Две одинаковые книги дадут на карточке 2.
Как borrow принимает решение
Метод borrow — главное место проекта. Он делает то же, что библиотекарь:
copies.get(title)ищет карточку с этим названием. Если такой книги нет вовсе,getвозвращаетnull, поэтомуnобъявлена какInteger, а неint.n == null || n == 0проверяет, что карточки нет или все экземпляры выданы. Это сокращённое вычисление: если левая часть||истинна, правую Java не вычисляет, поэтомуn == 0никогда не распаковываетnull.- Если условие истинно,
throwбросаетLoanException. Метод сразу заканчивается, иputне выполняется. - Иначе
putкладёт в словарь число на один меньше.
В виджете тот же borrow в сжатой программе. Чтобы она уместилась, словарь и метод стоят прямо в Main со словом static, а исключение обходится без сообщения. Нижняя рамка static показывает словарь. Нажимайте «Следующий шаг» и следите за n и за числом в словаре.
1import java.util.HashMap;2import java.util.Map;3public class Main {4 static Map<String, Integer> copies =5 new HashMap<>();6 static void borrow(String title)7 throws LoanException {8 Integer n = copies.get(title);9 if (n == null || n == 0) {10 throw new LoanException();11 }12 copies.put(title, n - 1);13 }14 public static void main(String[] args) {15 copies.put("Dune", 1);16 try {17 borrow("Dune");18 System.out.println("Borrowed");19 borrow("Dune");20 } catch (LoanException e) {21 System.out.println("Refused");22 }23 }24}25class LoanException extends Exception {}
На полке один экземпляр Dune: в словаре пара Dune=1.
Первый вызов дошёл до put и выполнил его, второй остановился на throw. Во втором вызове put не выполнился, поэтому в словаре осталось 0, а не -1.
Здесь картинка со стойкой неточна. Библиотекарь звонит, отказывает и спокойно работает дальше. А throw завершает borrow сразу, и отказ должен поймать тот, кто вызвал метод. Иначе остановится вся программа.
Всё вместе
Теперь соберите всё в одном файле Main.java: в начале — импорты java.util.HashMap, java.util.List и java.util.Map, затем Main, Book, LoanException и Library. Тело main замените этими строками:
List<Book> catalog = List.of(new Book("Dune", 1965),
new Book("Emma", 1815), new Book("Hamlet", 1603));
Library lib = new Library();
catalog.forEach(lib::add);
try {
lib.borrow("Dune");
System.out.println("Dune borrowed"); // Dune borrowed
lib.borrow("Dune");
System.out.println("Dune borrowed again");
} catch (LoanException e) {
System.out.println(e.getMessage()); // Dune is not on the shelf
}
System.out.println(catalog.stream()
.filter(b -> b.year() < 1900)
.map(Book::title).toList()); // [Emma, Hamlet]Разберём по шагам:
catalog— неизменяемый список из трёх книг, созданный черезList.of.catalog.forEach(lib::add)передаёт каждую книгу вlib.add. Ссылка на методlib::addустроена какSystem.out::println: элемент уходит в метод объектаlib.- Первый
borrow("Dune")проходит, и программа печатаетDune borrowed. - Второй
borrow("Dune")бросает исключение: единственный экземпляр уже выдан. Строка сDune borrowed againпропускается. catchпечатает сообщениеDune is not on the shelf.- В конце отчёт: стрим оставляет книги, изданные до 1900 года, берёт у каждой название и печатает
[Emma, Hamlet]в порядке каталога.
Какое исключение выбрать, проверяемое или непроверяемое, — частый вопрос на собеседовании. Здесь ответ такой: отказ в выдаче — обычная ситуация, а не ошибка в коде вызывающего. Для ошибки вроде неверного аргумента есть непроверяемый IllegalArgumentException. А LoanException проверяемое: компилятор требует от main поймать его в catch или объявить в throws.
Частая ошибка
Части условия поменяли местами:
void borrow(String title) throws LoanException {
Integer n = copies.get(title);
if (n == 0 || n == null) { // ✗
throw new LoanException(title);
}
copies.put(title, n - 1);
}Для книг из каталога всё работает. Но вызов lib.borrow("Ulysses") с названием, для которого нет карточки, останавливает программу:
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "java.lang.Integer.intValue()" because "<local2>" is nullget вернул null, и первая часть n == 0 распаковала его — вызвала intValue() у null. До n == null очередь не дошла. <local2> — это n: без javac -g JVM называет переменные номерами, и n идёт после this и title. Поставьте n == null первым, и || остановится на нём, не трогая n == 0.
Главное
- Итоговый проект собран из прошлых тем: запись хранит данные, класс со словарём — состояние, проверяемое исключение — отказ, стрим — отчёт.
borrowищет число в словаре, проверяет его и либо бросаетLoanException, либо записывает на один экземпляр меньше.- После
throwметод заканчивается сразу:putне выполняется, а вmainостатокtryпропускается доcatch. - Проверку на
nullставят первой:||не вычисляет правую часть, иnullне распаковывается.