Final project: a library
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
borrowmethod 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:
| Part | Where it comes from |
|---|---|
the record Book | the theme on records |
the exception LoanException | the theme on exceptions |
the class Library with a map | the themes on classes and collections |
the check inside borrow | the themes on conditions and wrappers |
| the catalog report | the 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:
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:
record Book(String title, int year)is the description of a book. The compiler writes the constructor, the methodstitle()andyear(),equalsandtoString.LoanExceptionextendsException, so the exception is checked. A refused loan is an ordinary situation, and the compiler makes the calling code prepare for it.- The constructor takes the title and builds the message itself: it passes
title + " is not on the shelf"to the parent. maindoes 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.
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:
copies.get(title)looks for the card with this title. If there is no such book at all,getreturnsnull, which is whynis declared asInteger, notint.n == null || n == 0checks 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, son == 0never unboxesnull.- If the condition is true,
throwthrowsLoanException. The method ends at once, andputdoes not run. - Otherwise
putstores 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.
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 {}
One copy of Dune is on the shelf: the map holds the pair Dune=1.
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:
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:
catalogis an immutable list of three books, made withList.of.catalog.forEach(lib::add)passes each book tolib.add. The method referencelib::addworks likeSystem.out::println: the element goes into a method of the objectlib.- The first
borrow("Dune")succeeds, and the program printsDune borrowed. - The second
borrow("Dune")throws an exception: the only copy is already out. The line withDune borrowed againis skipped. catchprints the messageDune is not on the shelf.- 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:
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:
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "java.lang.Integer.intValue()" because "<local2>" is nullget 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.
borrowlooks a number up in the map, checks it, and either throwsLoanExceptionor writes one copy fewer.- After
throwthe method ends at once:putdoes not run, and inmainthe rest oftryis skipped up tocatch. - The
nullcheck goes first:||skips the right part, andnullis never unboxed.