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.

Where our exceptions sit in Java's hierarchy
Where our exceptions sit in Java's hierarchy

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 + ")");
        }
    }
}
mkdir -p out javac -d out $(find src -name "*.java") java -cp out lms.Main
Added via addItem(Book): Clean Code Added via addItem(DVD): Introduction to Algorithms Added via addItem(LibraryItem): Software Weekly --- 1. Happy path --- Clean Code loaned to Nadeesha Perera (attempt finished: item B001, member M001) --- 2. Already on loan (checked exception) --- Cannot borrow: Clean Code is already on loan. (attempt finished: item B001, member M002) --- 3. Unknown member (unchecked exception) --- Unknown member: M999 (attempt finished: item D001, member M999) --- 4. Unknown item --- Cannot borrow: No item with id 'X404' (attempt finished: item X404, member M001) --- 5. Returning something not on loan --- Cannot return: Introduction to Algorithms is not on loan. --- 6. Bad data (IllegalArgumentException) --- Invalid book: Title cannot be blank --- 7. Student limit of 3 --- Added via addItem(Book): Refactoring Added via addItem(Book): Head First Java Added via addItem(Book): Effective Java Introduction to Algorithms loaned to Nadeesha Perera (attempt finished: item D001, member M001) Software Weekly loaned to Nadeesha Perera (attempt finished: item G001, member M001) Cannot borrow: Nadeesha Perera has reached the limit of 3 items. (attempt finished: item B003, member M001) Cannot borrow: Nadeesha Perera has reached the limit of 3 items. (attempt finished: item B004, member M001) --- 8. Return frees a slot --- Clean Code returned. Head First Java loaned to Nadeesha Perera (attempt finished: item B004, member M001)

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
  • finally runs in every attempt
  • Unknown member is caught although no method declares it
  • UML saved as docs/uml-week5.png; PR: Week 5: Exception Handling