Goal: replace println("... already on loan") with exceptions, so the caller decides what to do. A checked exception for recoverable problems, an unchecked one for bad ids.
Check This step is the foundation of the GUI: in Step 6 every exception becomes a dialog box.

1. New package lms.exception
src/lms/exception/ItemNotAvailableException.javaChecked
package lms.exception; /** * CHECKED exception: thrown when an item cannot be loaned or returned * (already on loan, limit reached, not on loan, no such item). * * It extends Exception, so the compiler FORCES every caller to either * catch it or declare "throws ItemNotAvailableException". */ public class ItemNotAvailableException extends Exception { public ItemNotAvailableException(String message) { super(message); } }
src/lms/exception/MemberNotFoundException.javaUnchecked
package lms.exception; /** * UNCHECKED exception: thrown when a member id does not exist. * * It extends RuntimeException, so callers are NOT forced to handle it. * We use unchecked here because an unknown member id is usually a * programming or data mistake, not something a caller can recover from. */ public class MemberNotFoundException extends RuntimeException { private final String memberId; public MemberNotFoundException(String memberId) { super("No member with id '" + memberId + "'"); this.memberId = memberId; } public String getMemberId() { return memberId; } }
2. loan() and returnItem() now throw
Update the interface first. The compiler then points at every place that must change. Also see the bug fix: returnItem now calls decrementBorrowedCount() so the member's slot is freed.
src/lms/model/Loanable.java
package lms.model; import lms.exception.ItemNotAvailableException; /** Contract for anything that can be loaned. Week 5: failures are now exceptions. */ public interface Loanable { void loan(Member member) throws ItemNotAvailableException; void returnItem() throws ItemNotAvailableException; }
src/lms/model/LibraryItem.java
package lms.model; import lms.exception.ItemNotAvailableException; /** * LibraryItem - Week 5 version (Exception Handling). * * Two changes from Week 3: * 1. It now IMPLEMENTS the Loanable interface (a contract it fulfils). * 2. calculateLateFee() is now truly ABSTRACT - no default body. * Every subclass must write its own, or it will not compile. * * Week 5: loan() and returnItem() no longer PRINT failures - they THROW * ItemNotAvailableException so the caller decides what to do about it. */ public abstract class LibraryItem implements Loanable { private String id; private String title; private boolean isBorrowed; private Member borrower; // who currently has it (null if on the shelf) public LibraryItem(String id, String title) { setId(id); setTitle(title); this.isBorrowed = false; } public String getId() { return id; } public String getTitle() { return title; } public boolean isBorrowed() { return isBorrowed; } public void setId(String id) { if (id == null || id.isBlank()) throw new IllegalArgumentException("Id cannot be blank"); this.id = id; } public void setTitle(String title) { if (title == null || title.isBlank()) throw new IllegalArgumentException("Title cannot be blank"); this.title = title; } // ---- Loanable contract (shared implementation lives here) ---- @Override public void loan(Member member) throws ItemNotAvailableException { if (isBorrowed) { throw new ItemNotAvailableException(title + " is already on loan."); } if (member.getBorrowedCount() >= member.getMaxBorrowLimit()) { throw new ItemNotAvailableException(member.getName() + " has reached the limit of " + member.getMaxBorrowLimit() + " items."); } isBorrowed = true; borrower = member; member.incrementBorrowedCount(); System.out.println(title + " loaned to " + member.getName()); } @Override public void returnItem() throws ItemNotAvailableException { if (!isBorrowed) { throw new ItemNotAvailableException(title + " is not on loan."); } borrower.decrementBorrowedCount(); // free the member's slot borrower = null; isBorrowed = false; System.out.println(title + " returned."); } public Member getBorrower() { return borrower; } /** No body: each subclass MUST define its own late-fee rule. */ public abstract double calculateLateFee(int daysLate); @Override public String toString() { return getClass().getSimpleName() + "{id='" + id + "', title='" + title + "', borrowed=" + isBorrowed + "}"; } }
3. Look-ups and borrowItem in Library
borrowItem is the one door the GUI will use. Notice that findMember declares nothing (unchecked) while findItem declares throws.
src/lms/service/Library.java
package lms.service; import lms.exception.ItemNotAvailableException; import lms.exception.MemberNotFoundException; import lms.model.Book; import lms.model.DVD; import lms.model.LibraryItem; import lms.model.Loanable; import lms.model.Member; import lms.model.Reservable; import java.util.ArrayList; import java.util.Collections; import java.util.List; /** * Library - Week 5 version (Exception Handling). * * Kept from Week 3: printAllLateFees(), the addItem() overloads. * New this week: methods that talk to INTERFACE types (Loanable, * Reservable) instead of concrete classes, and a Comparable-based sort. */ public class Library { private final List<LibraryItem> items = new ArrayList<>(); private final List<Member> members = new ArrayList<>(); // ---- from Week 3 (unchanged) ---- public void addItem(LibraryItem item) { items.add(item); System.out.println("Added via addItem(LibraryItem): " + item.getTitle()); } public void addItem(Book book) { items.add(book); System.out.println("Added via addItem(Book): " + book.getTitle()); } public void addItem(DVD dvd) { items.add(dvd); System.out.println("Added via addItem(DVD): " + dvd.getTitle()); } public void addMember(Member member) { members.add(member); } public void printAllLateFees(int daysLate) { System.out.println("--- Late fees if " + daysLate + " day(s) late ---"); for (LibraryItem item : items) { System.out.println(item.getTitle() + ": " + item.calculateLateFee(daysLate)); } } // ---- NEW in Week 4 ---- /** * The parameter type is the INTERFACE Loanable, not LibraryItem. * This method would work for anything that promises to be loanable, * even a class that is not a LibraryItem at all. */ public void lend(Loanable thing, Member member) throws ItemNotAvailableException { thing.loan(member); } // ---- NEW in Week 5 ---- /** Looks a member up by id. Throws the UNCHECKED MemberNotFoundException. */ public Member findMember(String memberId) { for (Member m : members) { if (m.getMemberId().equals(memberId)) return m; } throw new MemberNotFoundException(memberId); } /** Looks an item up by id. Throws the CHECKED ItemNotAvailableException. */ public LibraryItem findItem(String itemId) throws ItemNotAvailableException { for (LibraryItem i : items) { if (i.getId().equals(itemId)) return i; } throw new ItemNotAvailableException("No item with id '" + itemId + "'"); } /** The one-stop "borrow" operation the GUI will call later. */ public void borrowItem(String itemId, String memberId) throws ItemNotAvailableException { Member member = findMember(memberId); LibraryItem item = findItem(itemId); item.loan(member); } public void returnItem(String itemId) throws ItemNotAvailableException { findItem(itemId).returnItem(); } /** * Only some items are Reservable. Checking for the INTERFACE (not for * Book) means a future Reservable class works here with no changes. */ public void reserveIfPossible(LibraryItem item, Member member) { if (item instanceof Reservable r) { r.reserve(member); } else { System.out.println(item.getTitle() + " cannot be reserved."); } } /** Prints members ranked by activity, using Member's compareTo(). */ public void printMembersByActivity() { List<Member> sorted = new ArrayList<>(members); Collections.sort(sorted); System.out.println("--- Members ranked by activity ---"); for (Member m : sorted) { System.out.println(m.getName() + " (" + m.getBorrowedCount() + " borrowed)"); } } public void listItems() { System.out.println("--- Items in the library ---"); for (LibraryItem item : items) System.out.println(item); } public void listMembers() { System.out.println("--- Registered members ---"); for (Member m : members) System.out.println(m); } public List<LibraryItem> getItems() { return items; } public List<Member> getMembers() { return members; } }
4. Handle everything in one place
src/lms/Main.javaEight scenarios, one helper
package lms; import lms.exception.ItemNotAvailableException; import lms.exception.MemberNotFoundException; import lms.model.*; import lms.service.Library; /** * Main - Week 5 demo: Exception Handling. * * Try this yourself: * 1. Remove the try/catch around the first borrow and read the compiler error * ("unreported exception ItemNotAvailableException"). * 2. Change MemberNotFoundException to extend Exception. What breaks? * 3. Add a custom exception of your own: FineNotPaidException. */ public class Main { public static void main(String[] args) { Library library = new Library(); library.addItem(new Book("B001", "Clean Code", "Robert C. Martin")); library.addItem(new DVD("D001", "Introduction to Algorithms", 120)); library.addItem(new Magazine("G001", "Software Weekly", 42)); library.addMember(new Student("M001", "Nadeesha Perera")); library.addMember(new Staff("M002", "Mr. Kasun Silva")); System.out.println("--- 1. Happy path ---"); attempt(library, "B001", "M001"); System.out.println("--- 2. Already on loan (checked exception) ---"); attempt(library, "B001", "M002"); System.out.println("--- 3. Unknown member (unchecked exception) ---"); attempt(library, "D001", "M999"); System.out.println("--- 4. Unknown item ---"); attempt(library, "X404", "M001"); System.out.println("--- 5. Returning something not on loan ---"); try { library.returnItem("D001"); } catch (ItemNotAvailableException e) { System.out.println("Cannot return: " + e.getMessage()); } System.out.println("--- 6. Bad data (IllegalArgumentException) ---"); try { new Book("B002", " ", "Someone"); } catch (IllegalArgumentException e) { System.out.println("Invalid book: " + e.getMessage()); } System.out.println("--- 7. Student limit of 3 ---"); library.addItem(new Book("B003", "Refactoring", "Martin Fowler")); library.addItem(new Book("B004", "Head First Java", "Kathy Sierra")); library.addItem(new Book("B005", "Effective Java", "Joshua Bloch")); for (String id : new String[]{"D001", "G001", "B003", "B004"}) { attempt(library, id, "M001"); } System.out.println("--- 8. Return frees a slot ---"); try { library.returnItem("B001"); attempt(library, "B004", "M001"); } catch (ItemNotAvailableException e) { System.out.println("Unexpected: " + e.getMessage()); } } /** One borrow attempt with all the handling in ONE place. */ private static void attempt(Library library, String itemId, String memberId) { try { library.borrowItem(itemId, memberId); } catch (ItemNotAvailableException e) { // checked System.out.println("Cannot borrow: " + e.getMessage()); } catch (MemberNotFoundException e) { // unchecked System.out.println("Unknown member: " + e.getMemberId()); } finally { System.out.println(" (attempt finished: item " + itemId + ", member " + memberId + ")"); } } }
Tip: reading this output
Scenario 2 is the checked exception, scenario 3 the unchecked one. (attempt finished…) appears every time because it is in finally. Scenario 8 proves the bug fix: after Clean Code is returned, the student can borrow again.
Checkpoint: is it done?
- No
println("…already on loan")left in the model finallyruns in every attempt- Unknown member is caught although no method declares it
- UML saved as
docs/uml-week5.png; PR: Week 5: Exception Handling