Skip to content
Theory · Final project · lesson 1 of 4

Final project: a library

What you'll learn

What you'll learn:

  • build a program from the parts of earlier themes: a record, a class with a map, your own exception and a stream;
  • read the borrow method step by step: the map lookup, the check, then the exception or the update;
  • predict what the program prints when a book is asked for a second time;
  • choose what to study after the course.

Picture the lending desk of a library. On the desk stands a card drawer: every title has a card, and the card says how many copies are on the shelf. A reader hands over a slip with a title, and the librarian pulls out its card.

From there the number on the card decides everything. If it is above zero, the librarian lends the book and lowers the number by one. If there are no copies, the librarian rings the bell on the desk and refuses: "This book is not on the shelf."

The final project is such a desk in Java. It has no new constructs: every part comes from an earlier theme. The task is to see how the parts work together:

PartWhere it comes from
the record Bookthe theme on records
the exception LoanExceptionthe theme on exceptions
the class Library with a mapthe themes on classes and collections
the check inside borrowthe themes on conditions and wrappers
the catalog reportthe theme on streams

A book and a refusal

We start with the data. A book is a record of a title and a publication year. A refusal is our own exception with a clear message:

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

Let's take it part by part:

  1. record Book(String title, int year) is the description of a book. The compiler writes the constructor, the methods title() and year(), equals and toString.
  2. LoanException extends Exception, so the exception is checked. A refused loan is an ordinary situation, and the compiler makes the calling code prepare for it.
  3. The constructor takes the title and builds the message itself: it passes title + " is not on the shelf" to the parent.
  4. main does not throw the exception yet. This is a check of the parts: getMessage() returns the message that was built.

The lending desk

In the program the card drawer is the private field copies of the class Library. It is a map Map<String, Integer>: the key is a title, the value is the number of copies on the shelf.

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

The method add counts copies: getOrDefault gives 0 for a new book, and put writes a number one higher. Two identical books give 2 on the card.

How borrow decides

The method borrow is the heart of the project. It does what the librarian does:

  1. copies.get(title) looks for the card with this title. If there is no such book at all, get returns null, which is why n is declared as Integer, not int.
  2. n == null || n == 0 checks that there is no card or all the copies are out. This is short-circuit evaluation: when the left side of || is true, Java does not evaluate the right side, so n == 0 never unboxes null.
  3. If the condition is true, throw throws LoanException. The method ends at once, and put does not run.
  4. Otherwise put stores a number one lower in the map.

The widget shows the same borrow in a compact program. To make it fit, the map and the method sit right in Main with the word static, and the exception does without a message. The bottom frame static shows the map. Press "Next step" and watch n and the number in the map.

Liveborrow: lend or refusestep 1 of 12

One copy of Dune is on the shelf: the map holds the pair Dune=1.

Watch n and the map copies in the bottom frame

The first call went all the way to put, the second stopped at throw. In the second call put did not run, so the map kept 0, not -1.

Here the desk picture falls short. A librarian rings, refuses and calmly goes on working. But throw ends borrow at once, and the refusal must be caught by whoever called the method. Otherwise the whole program stops.

All together

Now put everything into one file, Main.java: the imports java.util.HashMap, java.util.List and java.util.Map at the top, then Main, Book, LoanException and Library. Replace the body of main with these lines:

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]

Let's go step by step:

  1. catalog is an immutable list of three books, made with List.of.
  2. catalog.forEach(lib::add) passes each book to lib.add. The method reference lib::add works like System.out::println: the element goes into a method of the object lib.
  3. The first borrow("Dune") succeeds, and the program prints Dune borrowed.
  4. The second borrow("Dune") throws an exception: the only copy is already out. The line with Dune borrowed again is skipped.
  5. catch prints the message Dune is not on the shelf.
  6. At the end comes the report: the stream keeps the books published before 1900, takes the title of each and prints [Emma, Hamlet] in catalog order.

Which exception to choose, checked or unchecked, is a common interview question. Here the answer is: a refused loan is an ordinary situation, not a bug in the caller's code. For a bug such as a wrong argument there is the unchecked IllegalArgumentException. But LoanException is checked: the compiler requires main to catch it in catch or declare it in throws.

Common mistake

The parts of the condition were swapped:

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

For the books in the catalog everything works. But the call lib.borrow("Ulysses") with a title that has no card stops the program:

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

get returned null, and the first part n == 0 unboxed it: it called intValue() on null. The check n == null never got its turn. <local2> is n: without javac -g the JVM names variables by number, and n comes after this and title. Put n == null first, and || stops there without touching n == 0.

Key points

  • The final project reuses earlier themes: a record holds the data, a class with a map holds the state, a checked exception refuses, a stream reports.
  • borrow looks a number up in the map, checks it, and either throws LoanException or writes one copy fewer.
  • After throw the method ends at once: put does not run, and in main the rest of try is skipped up to catch.
  • The null check goes first: || skips the right part, and null is never unboxed.