Goal: replace the two ArrayLists with one generic Repository<T>, then add search and a loan report using streams.
1. A repository for anything
src/lms/service/Repository.javaGeneric class backed by a Map
package lms.service; import java.util.*; import java.util.function.Function; import java.util.function.Predicate; /** * Repository<T> - a GENERIC class (Week 10). * * T is a type parameter: "some type, decided by whoever uses me". * Repository<LibraryItem> stores items * Repository<Member> stores members * One class, written once, type-safe for both. No casts needed. * * Inside, a Map gives O(1) lookup by id (a List would have to scan). * The Function<T,String> tells the repository how to get an id out of a T. */ public class Repository<T> { private final Map<String, T> store = new LinkedHashMap<>(); // keeps insertion order private final Function<T, String> idOf; public Repository(Function<T, String> idOf) { this.idOf = idOf; } public void add(T entity) { String id = idOf.apply(entity); if (store.containsKey(id)) { throw new IllegalArgumentException("An entry with id '" + id + "' already exists"); } store.put(id, entity); } /** Optional says honestly: the answer might be "nothing". */ public Optional<T> findById(String id) { return Optional.ofNullable(store.get(id)); } public boolean exists(String id) { return store.containsKey(id); } public List<T> findAll() { return new ArrayList<>(store.values()); } /** Streams: describe WHAT you want (filter), not HOW to loop. */ public List<T> search(Predicate<T> condition) { return store.values().stream().filter(condition).toList(); } public int size() { return store.size(); } }
Repository<LibraryItem> and Repository<Member> come from the same class. Try items.add(new Student(...)) and read the compile error: generics catch mistakes before the program runs.
2. Library uses it, plus three stream methods
Find search, loansPerMember and availableItems. Read each stream like a sentence: "take the items, keep the borrowed ones, group by borrower name, count each group."
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.*; import java.util.stream.Collectors; /** * Library - Week 10 version (Generics, Collections, Streams). * * 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 { // Week 10: Lists replaced by generic Repositories (Maps inside) private final Repository<LibraryItem> items = new Repository<>(LibraryItem::getId); private final Repository<Member> members = new Repository<>(Member::getMemberId); // ---- from Week 3 (unchanged) ---- public void addItem(LibraryItem item) { items.add(item); } public void addItem(Book book) { items.add(book); } public void addItem(DVD dvd) { items.add(dvd); } public void addMember(Member member) { members.add(member); } /** True if an item with this id already exists (used to reject duplicates). */ public boolean hasItem(String itemId) { return items.exists(itemId); } public void printAllLateFees(int daysLate) { System.out.println("--- Late fees if " + daysLate + " day(s) late ---"); for (LibraryItem item : items.findAll()) { 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) { return members.findById(memberId).orElseThrow(() -> new MemberNotFoundException(memberId)); } /** Looks an item up by id. Throws the CHECKED ItemNotAvailableException. */ public LibraryItem findItem(String itemId) throws ItemNotAvailableException { Optional<LibraryItem> found = items.findById(itemId); if (found.isEmpty()) throw new ItemNotAvailableException("No item with id '" + itemId + "'"); return found.get(); } /** 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 = members.findAll(); 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.findAll()) System.out.println(item); } public void listMembers() { System.out.println("--- Registered members ---"); for (Member m : members.findAll()) System.out.println(m); } public List<LibraryItem> getItems() { return items.findAll(); } public List<Member> getMembers() { return members.findAll(); } // ---- NEW in Week 10: streams ---- /** Case-insensitive search in title, id, or author (for books). */ public List<LibraryItem> search(String text) { String q = text.trim().toLowerCase(); return items.search(i -> i.getTitle().toLowerCase().contains(q) || i.getId().toLowerCase().contains(q) || (i instanceof Book b && b.getAuthor().toLowerCase().contains(q))); } /** Map: member name -> number of items currently on loan. */ public Map<String, Long> loansPerMember() { return items.findAll().stream() .filter(LibraryItem::isBorrowed) .collect(Collectors.groupingBy(i -> i.getBorrower().getName(), TreeMap::new, Collectors.counting())); } /** All items that are currently available, sorted by title. */ public List<LibraryItem> availableItems() { return items.findAll().stream() .filter(i -> !i.isBorrowed()) .sorted(Comparator.comparing(LibraryItem::getTitle)) .toList(); } }
3. Controller and window
The controller gains search and loanReport; the window gets a search bar and a Loan Report button.
src/lms/service/LibraryController.java
package lms.service; import lms.exception.ItemNotAvailableException; import lms.model.*; import java.util.List; /** * LibraryController - the C in MVC (Week 9). * * Model = lms.model + Library (data and rules; knows nothing about windows) * View = lms.ui.MainFrame (windows; knows nothing about the rules) * Controller = this class (receives what the user typed, calls the model) * * The view never touches Book/DVD/Library directly. It asks the controller. * That is what lets us swap the console for Swing - and later swap the * in-memory list for a database - without rewriting the other layers. */ public class LibraryController { private final Library library; public LibraryController(Library library) { this.library = library; } /** Builds the right kind of item from raw text. Throws IllegalArgumentException on bad input. */ public void addItem(String type, String id, String title, String detail) { if (library.hasItem(id)) { throw new IllegalArgumentException("An item with id '" + id + "' already exists"); } switch (type) { case "Book" -> library.addItem(new Book(id, title, detail)); case "DVD" -> library.addItem(new DVD(id, title, parseWholeNumber(detail, "Duration"))); case "Magazine" -> library.addItem(new Magazine(id, title, parseWholeNumber(detail, "Issue number"))); default -> throw new IllegalArgumentException("Unknown item type: " + type); } } public void borrow(String itemId, String memberId) throws ItemNotAvailableException { library.borrowItem(itemId, memberId); } public void giveBack(String itemId) throws ItemNotAvailableException { library.returnItem(itemId); } public List<LibraryItem> getItems() { return library.getItems(); } /** An empty search box means "show everything". */ public List<LibraryItem> search(String text) { return (text == null || text.isBlank()) ? library.getItems() : library.search(text); } /** Plain-text report for a dialog. */ public String loanReport() { var counts = library.loansPerMember(); if (counts.isEmpty()) return "No items are currently on loan."; StringBuilder sb = new StringBuilder("Items currently on loan:\n"); counts.forEach((name, n) -> sb.append(" ").append(name).append(": ").append(n).append('\n')); sb.append("\nAvailable: ").append(library.availableItems().size()); return sb.toString(); } public List<Member> getMembers() { return library.getMembers(); } private int parseWholeNumber(String text, String label) { try { return Integer.parseInt(text.trim()); } catch (NumberFormatException e) { throw new IllegalArgumentException(label + " must be a whole number"); } } }
src/lms/ui/MainFrame.java
package lms.ui; import lms.exception.ItemNotAvailableException; import lms.exception.MemberNotFoundException; import lms.model.*; import lms.service.LibraryController; import javax.swing.*; import javax.swing.table.DefaultTableModel; import java.awt.*; /** * MainFrame - Week 10: adds search + report using generics/streams (via the controller). * (Week 9 notes: events, dialogs and MVC.) * * Same layout as Week 8. New this week: * - ActionListeners on the buttons (event-driven programming) * - JOptionPane messages for errors * - the frame talks to a LibraryController, never to the model directly */ public class MainFrame extends JFrame { private final LibraryController controller; private final DefaultTableModel tableModel; private final JTable table; private final JTextField idField = new JTextField(12); private final JTextField titleField = new JTextField(12); private final JTextField detailField = new JTextField(12); private final JComboBox<String> typeBox = new JComboBox<>(new String[]{"Book", "DVD", "Magazine"}); private final JComboBox<String> memberBox = new JComboBox<>(); private final JButton addButton = new JButton("Add Item"); private final JButton borrowButton = new JButton("Borrow"); private final JButton returnButton = new JButton("Return"); private final JButton reportButton = new JButton("Loan Report"); private final JTextField searchField = new JTextField(16); private final JButton searchButton = new JButton("Search"); private final JLabel statusLabel = new JLabel(" Ready"); public MainFrame(LibraryController controller) { super("Library Management System"); this.controller = controller; // ---- table (CENTER) ---- String[] columns = {"ID", "Title", "Type", "Detail", "Status"}; tableModel = new DefaultTableModel(columns, 0) { @Override public boolean isCellEditable(int row, int col) { return false; } }; table = new JTable(tableModel); table.setRowHeight(24); table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); table.getColumnModel().getColumn(1).setPreferredWidth(200); table.getColumnModel().getColumn(3).setPreferredWidth(140); table.getColumnModel().getColumn(4).setPreferredWidth(190); // ---- form (WEST) ---- JPanel form = new JPanel(new GridLayout(0, 2, 6, 8)); form.setBorder(BorderFactory.createTitledBorder("New item")); form.add(new JLabel("Type:")); form.add(typeBox); form.add(new JLabel("ID:")); form.add(idField); form.add(new JLabel("Title:")); form.add(titleField); form.add(new JLabel("Detail:")); form.add(detailField); form.add(new JLabel("")); form.add(addButton); JPanel west = new JPanel(new BorderLayout()); west.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 4)); west.add(form, BorderLayout.NORTH); // ---- title (NORTH) ---- JLabel heading = new JLabel("Library Management System", SwingConstants.CENTER); heading.setFont(heading.getFont().deriveFont(Font.BOLD, 20f)); heading.setBorder(BorderFactory.createEmptyBorder(10, 0, 6, 0)); JPanel searchBar = new JPanel(new FlowLayout(FlowLayout.CENTER, 8, 2)); searchBar.add(new JLabel("Search:")); searchBar.add(searchField); searchBar.add(searchButton); JPanel north = new JPanel(new BorderLayout()); north.add(heading, BorderLayout.NORTH); north.add(searchBar, BorderLayout.CENTER); // ---- buttons + member picker + status (SOUTH) ---- JPanel buttons = new JPanel(new FlowLayout(FlowLayout.RIGHT, 10, 6)); buttons.add(new JLabel("Member:")); buttons.add(memberBox); buttons.add(borrowButton); buttons.add(returnButton); buttons.add(reportButton); JPanel south = new JPanel(new BorderLayout()); south.add(statusLabel, BorderLayout.WEST); south.add(buttons, BorderLayout.EAST); setLayout(new BorderLayout()); add(north, BorderLayout.NORTH); add(west, BorderLayout.WEST); add(new JScrollPane(table), BorderLayout.CENTER); add(south, BorderLayout.SOUTH); wireEvents(); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setSize(1020, 480); setLocationRelativeTo(null); loadMembers(); refreshTable(); } // ------------------------------------------------------------------ // EVENTS: "when this happens, run that" // ------------------------------------------------------------------ private void wireEvents() { addButton.addActionListener(e -> onAdd()); borrowButton.addActionListener(e -> onBorrow()); returnButton.addActionListener(e -> onReturn()); searchButton.addActionListener(e -> refreshTable()); searchField.addActionListener(e -> refreshTable()); // Enter key reportButton.addActionListener(e -> JOptionPane.showMessageDialog(this, controller.loanReport(), "Loan report", JOptionPane.INFORMATION_MESSAGE)); } private void onAdd() { try { controller.addItem((String) typeBox.getSelectedItem(), idField.getText().trim(), titleField.getText(), detailField.getText()); setStatus("Added " + titleField.getText().trim()); idField.setText(""); titleField.setText(""); detailField.setText(""); refreshTable(); } catch (IllegalArgumentException ex) { showError("Cannot add item", ex.getMessage()); } } private void onBorrow() { String itemId = selectedItemId(); if (itemId == null) return; String memberId = ((String) memberBox.getSelectedItem()).split(" - ")[0]; try { controller.borrow(itemId, memberId); setStatus(itemId + " borrowed by " + memberId); refreshTable(); } catch (ItemNotAvailableException ex) { showError("Cannot borrow", ex.getMessage()); } catch (MemberNotFoundException ex) { showError("Unknown member", ex.getMessage()); } } private void onReturn() { String itemId = selectedItemId(); if (itemId == null) return; try { controller.giveBack(itemId); setStatus(itemId + " returned"); refreshTable(); } catch (ItemNotAvailableException ex) { showError("Cannot return", ex.getMessage()); } } // ------------------------------------------------------------------ // helpers // ------------------------------------------------------------------ private String selectedItemId() { int row = table.getSelectedRow(); if (row < 0) { JOptionPane.showMessageDialog(this, "Select a row in the table first.", "Nothing selected", JOptionPane.INFORMATION_MESSAGE); return null; } return (String) tableModel.getValueAt(row, 0); } private void showError(String title, String message) { JOptionPane.showMessageDialog(this, message, title, JOptionPane.WARNING_MESSAGE); setStatus(title + ": " + message); } private void setStatus(String text) { statusLabel.setText(" " + text); } private void loadMembers() { memberBox.removeAllItems(); for (Member m : controller.getMembers()) { memberBox.addItem(m.getMemberId() + " - " + m.getName()); } } private void refreshTable() { tableModel.setRowCount(0); for (LibraryItem item : controller.search(searchField.getText())) { tableModel.addRow(new Object[]{ item.getId(), item.getTitle(), item.getClass().getSimpleName(), detailOf(item), item.isBorrowed() ? "On loan: " + item.getBorrower().getName() : "Available"}); } } private String detailOf(LibraryItem item) { if (item instanceof Book b) return b.getAuthor(); if (item instanceof DVD d) return d.getDurationMinutes() + " min"; if (item instanceof Magazine m) return "Issue " + m.getIssueNumber(); return ""; } }
mkdir -p out
javac -d out $(find src -name "*.java")
java -cp out lms.GuiMain


Tip Your own stream query is part of the marks: titles of borrowed books sorted alphabetically is a good start.
Checkpoint: is it done?
- No
List<LibraryItem>field is left inLibrary - Search is case-insensitive and also matches authors
- The loan report is correct after a return
- PR: Week 10: Generics, Collections & Streams with
docs/uml-week10.png