Перейти к содержимому
Теория · Итоговый проект · урок 1 из 4

Итоговый проект: библиотека

Чему вы научитесь

Чему вы научитесь:

  • собирать программу из деталей прошлых тем: записи, класса со словарём, своего исключения и стрима;
  • читать метод borrow по шагам: поиск в словаре, проверка, исключение или изменение;
  • предсказывать, что напечатает программа, когда книгу просят второй раз;
  • выбирать, что изучать после курса.

Представьте стойку выдачи в библиотеке. На стойке стоит ящик с карточками: на каждое название заведена карточка, и на ней записано, сколько экземпляров стоит на полке. Читатель подаёт листок с названием, и библиотекарь достаёт нужную карточку.

Дальше всё решает число на карточке. Если оно больше нуля, библиотекарь выдаёт книгу и уменьшает число на один. Если экземпляров нет, он нажимает звонок на стойке и отказывает: «Этой книги на полке нет».

Итоговый проект — такая стойка на Java. Новых конструкций в нём нет: каждая деталь пришла из прошлых тем. Задача — увидеть, как детали работают вместе:

ДетальОткуда она
запись Bookтема о записях record
исключение LoanExceptionтема об исключениях
класс Library со словарёмтемы о классах и коллекциях
проверка внутри borrowтемы об условиях и обёртках
отчёт по каталогутема о стримах

Книга и отказ

Начнём с данных. Книга — запись из названия и года издания. Отказ — своё исключение с понятным сообщением:

JAVA
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");
    }
}

Давайте разберём по частям:

  1. record Book(String title, int year) — описание книги. Конструктор, методы title() и year(), equals и toString пишет компилятор.
  2. LoanException наследует Exception, значит, исключение проверяемое. Отказ в выдаче — обычная ситуация, и компилятор заставит вызывающий код к ней подготовиться.
  3. Конструктор принимает название и сам собирает сообщение: передаёт родителю title + " is not on the shelf".
  4. В main исключение пока не бросают. Это проверка деталей: getMessage() возвращает собранное сообщение.

Стойка выдачи

Ящик с карточками в программе — закрытое поле copies класса Library. Это словарь Map<String, Integer>: ключ — название, значение — число экземпляров на полке.

JAVA
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 — главное место проекта. Он делает то же, что библиотекарь:

  1. copies.get(title) ищет карточку с этим названием. Если такой книги нет вовсе, get возвращает null, поэтому n объявлена как Integer, а не int.
  2. n == null || n == 0 проверяет, что карточки нет или все экземпляры выданы. Это сокращённое вычисление: если левая часть || истинна, правую Java не вычисляет, поэтому n == 0 никогда не распаковывает null.
  3. Если условие истинно, throw бросает LoanException. Метод сразу заканчивается, и put не выполняется.
  4. Иначе put кладёт в словарь число на один меньше.

В виджете тот же borrow в сжатой программе. Чтобы она уместилась, словарь и метод стоят прямо в Main со словом static, а исключение обходится без сообщения. Нижняя рамка static показывает словарь. Нажимайте «Следующий шаг» и следите за n и за числом в словаре.

Вживуюborrow: выдать или отказатьшаг 1 из 12

На полке один экземпляр Dune: в словаре пара Dune=1.

Следите за n и за словарём copies в нижней рамке

Первый вызов дошёл до put и выполнил его, второй остановился на throw. Во втором вызове put не выполнился, поэтому в словаре осталось 0, а не -1.

Здесь картинка со стойкой неточна. Библиотекарь звонит, отказывает и спокойно работает дальше. А throw завершает borrow сразу, и отказ должен поймать тот, кто вызвал метод. Иначе остановится вся программа.

Всё вместе

Теперь соберите всё в одном файле Main.java: в начале — импорты java.util.HashMap, java.util.List и java.util.Map, затем Main, Book, LoanException и Library. Тело main замените этими строками:

JAVA
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]

Разберём по шагам:

  1. catalog — неизменяемый список из трёх книг, созданный через List.of.
  2. catalog.forEach(lib::add) передаёт каждую книгу в lib.add. Ссылка на метод lib::add устроена как System.out::println: элемент уходит в метод объекта lib.
  3. Первый borrow("Dune") проходит, и программа печатает Dune borrowed.
  4. Второй borrow("Dune") бросает исключение: единственный экземпляр уже выдан. Строка с Dune borrowed again пропускается.
  5. catch печатает сообщение Dune is not on the shelf.
  6. В конце отчёт: стрим оставляет книги, изданные до 1900 года, берёт у каждой название и печатает [Emma, Hamlet] в порядке каталога.

Какое исключение выбрать, проверяемое или непроверяемое, — частый вопрос на собеседовании. Здесь ответ такой: отказ в выдаче — обычная ситуация, а не ошибка в коде вызывающего. Для ошибки вроде неверного аргумента есть непроверяемый IllegalArgumentException. А LoanException проверяемое: компилятор требует от main поймать его в catch или объявить в throws.

Частая ошибка

Части условия поменяли местами:

JAVA
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") с названием, для которого нет карточки, останавливает программу:

TEXT
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "java.lang.Integer.intValue()" because "<local2>" is null

get вернул 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 не распаковывается.