← Back to Lecture
Practical Work 6

Library Management System - Media Types

Refactor the library using inheritance, interfaces, and modern Java features

Duration 3 hours
Difficulty Intermediate
Session 6 - Constructors and Inheritance

Objectives

By the end of this practical work, you will be able to:

  • Create abstract classes to define common behavior
  • Implement interfaces for shared capabilities
  • Override toString(), equals(), and hashCode()
  • Use Java records for immutable data
  • Apply pattern matching with instanceof
  • Leverage polymorphism in your code

Prerequisites

  • Completed Practical Works 3, 4, and 5
  • Book, BookCatalog, BookStatus, BookCategory classes
  • Understanding of class constructors and this keyword

Project Structure

Update your project to include the new media type hierarchy:

library-system
src
com.library
model
MediaItem.java (abstract)
Book.java (extends MediaItem)
DVD.java (extends MediaItem)
Magazine.java (extends MediaItem)
Borrowable.java (interface)
LoanInfo.java (record)
Member.java
BookStatus.java
BookCategory.java
service
MediaCatalog.java
MemberService.java
app
LibraryApp.java

Part 1: Abstract MediaItem Class

Step 1.1: Create the Abstract Base Class

Create an abstract class that defines common properties for all library items:

package com.library.model;

import java.util.Objects;

public abstract class MediaItem {  // (#1:Abstract class cannot be instantiated)
    private String id;
    private String title;
    private int year;
    private MediaStatus status;

    // Protected constructor - only accessible by subclasses
    protected MediaItem(String id, String title, int year) {  // (#2:Protected visibility)
        this.id = id;
        this.title = title;
        this.year = year;
        this.status = MediaStatus.AVAILABLE;
    }

    // Abstract methods - must be implemented by subclasses
    public abstract String getMediaType();  // (#3:Abstract method - no body)
    public abstract double getLateFeePerDay();
    public abstract int getMaxLoanDays();

    // Concrete methods - shared by all subclasses
    public String getId() { return id; }
    public String getTitle() { return title; }
    public int getYear() { return year; }
    public MediaStatus getStatus() { return status; }
    public void setStatus(MediaStatus status) { this.status = status; }

    // Will be overridden by subclasses with additional info
    @Override
    public String toString() {  // (#4:Default implementation, can be overridden)
        return String.format("%s: %s (%d) [%s]",
            getMediaType(), title, year, status);
    }

    @Override
    public boolean equals(Object obj) {  // (#5:Based on ID - unique identifier)
        if (this == obj) return true;
        if (obj == null || getClass() != obj.getClass()) return false;
        MediaItem other = (MediaItem) obj;
        return Objects.equals(id, other.id);
    }

    @Override
    public int hashCode() {  // (#6:Consistent with equals)
        return Objects.hash(id);
    }
}
Code Annotations:
  1. Abstract class: Cannot create new MediaItem() directly
  2. Protected: Accessible by subclasses but not external code
  3. Abstract method: Declares signature, subclass provides implementation
  4. toString override: Returns meaningful description instead of hash
  5. equals override: Two items are equal if they have the same ID
  6. hashCode override: Must always override when overriding equals

Step 1.2: Create MediaStatus Enum

Create a status enum that works for all media types:

package com.library.model;

public enum MediaStatus {
    AVAILABLE("Available for borrowing", true),
    BORROWED("Currently on loan", false),
    RESERVED("Reserved by member", false),
    DAMAGED("Damaged - not available", false),
    REFERENCE_ONLY("Reference only - cannot borrow", false);

    private final String description;
    private final boolean canBeBorrowed;

    MediaStatus(String description, boolean canBeBorrowed) {
        this.description = description;
        this.canBeBorrowed = canBeBorrowed;
    }

    public String getDescription() { return description; }
    public boolean canBeBorrowed() { return canBeBorrowed; }
}

Part 2: Implementing Subclasses

Step 2.1: Refactor Book Class

Update the Book class to extend MediaItem:

package com.library.model;

import java.util.Objects;

public class Book extends MediaItem {  // (#1:Extends abstract class)
    private String author;
    private String isbn;
    private BookCategory category;
    private int pageCount;

    public Book(String id, String title, String author, int year,
                String isbn, BookCategory category) {
        super(id, title, year);  // (#2:Call parent constructor)
        this.author = author;
        this.isbn = isbn;
        this.category = category;
        this.pageCount = 0;
    }

    // Constructor with page count
    public Book(String id, String title, String author, int year,
                String isbn, BookCategory category, int pageCount) {
        this(id, title, author, year, isbn, category);  // (#3:Constructor chaining)
        this.pageCount = pageCount;
    }

    // Implement abstract methods
    @Override
    public String getMediaType() {
        return "BOOK";
    }

    @Override
    public double getLateFeePerDay() {
        return 0.50;  // 50 cents per day
    }

    @Override
    public int getMaxLoanDays() {
        return 21;  // 3 weeks for books
    }

    // Book-specific getters
    public String getAuthor() { return author; }
    public String getIsbn() { return isbn; }
    public BookCategory getCategory() { return category; }
    public int getPageCount() { return pageCount; }

    @Override
    public String toString() {
        return String.format("Book: \"%s\" by %s (%d) - %s [%s]",
            getTitle(), author, getYear(), category, getStatus());
    }

    @Override
    public boolean equals(Object obj) {
        if (!super.equals(obj)) return false;  // (#4:Check parent equality first)
        if (!(obj instanceof Book other)) return false;  // (#5:Pattern matching instanceof)
        return Objects.equals(isbn, other.isbn);
    }

    @Override
    public int hashCode() {
        return Objects.hash(super.hashCode(), isbn);  // (#6:Include parent hashCode)
    }
}
Code Annotations:
  1. extends: Book inherits all non-private members from MediaItem
  2. super(): Must call parent constructor to initialize inherited fields
  3. this(): Call another constructor in same class
  4. super.equals(): Reuse parent's equality check
  5. Pattern matching: instanceof Book other combines check and cast
  6. Combined hashCode: Includes both parent and child fields

Step 2.2: Create DVD Class

package com.library.model;

public class DVD extends MediaItem {
    private String director;
    private int durationMinutes;
    private String rating;  // PG, PG-13, R, etc.

    public DVD(String id, String title, String director,
               int year, int durationMinutes, String rating) {
        super(id, title, year);
        this.director = director;
        this.durationMinutes = durationMinutes;
        this.rating = rating;
    }

    @Override
    public String getMediaType() {
        return "DVD";
    }

    @Override
    public double getLateFeePerDay() {
        return 1.00;  // $1 per day - higher for popular media
    }

    @Override
    public int getMaxLoanDays() {
        return 7;  // 1 week for DVDs
    }

    // DVD-specific getters
    public String getDirector() { return director; }
    public int getDurationMinutes() { return durationMinutes; }
    public String getRating() { return rating; }

    public String getFormattedDuration() {
        int hours = durationMinutes / 60;
        int mins = durationMinutes % 60;
        return String.format("%dh %02dm", hours, mins);
    }

    @Override
    public String toString() {
        return String.format("DVD: \"%s\" dir. %s (%d) - %s, %s [%s]",
            getTitle(), director, getYear(),
            getFormattedDuration(), rating, getStatus());
    }
}

Step 2.3: Create Magazine Class

package com.library.model;

public class Magazine extends MediaItem {
    private String publisher;
    private int issueNumber;
    private String month;

    public Magazine(String id, String title, String publisher,
                    int year, int issueNumber, String month) {
        super(id, title, year);
        this.publisher = publisher;
        this.issueNumber = issueNumber;
        this.month = month;
    }

    @Override
    public String getMediaType() {
        return "MAGAZINE";
    }

    @Override
    public double getLateFeePerDay() {
        return 0.25;  // 25 cents per day - lower value items
    }

    @Override
    public int getMaxLoanDays() {
        return 14;  // 2 weeks for magazines
    }

    // Magazine-specific getters
    public String getPublisher() { return publisher; }
    public int getIssueNumber() { return issueNumber; }
    public String getMonth() { return month; }

    @Override
    public String toString() {
        return String.format("Magazine: %s #%d (%s %d) - %s [%s]",
            getTitle(), issueNumber, month, getYear(),
            publisher, getStatus());
    }
}

Part 3: Borrowable Interface

Step 3.1: Create the Interface

Define an interface for items that can be borrowed:

package com.library.model;

import java.time.LocalDate;

public interface Borrowable {  // (#1:Interface keyword)
    // Abstract methods - implementers must provide
    boolean canBorrow();
    void checkout(Member member, LocalDate dueDate);
    void returnItem();

    // Default method - provides default implementation
    default int getStandardLoanDays() {  // (#2:Default method with body)
        return 14;  // 2 weeks default
    }

    // Static method - utility method on the interface
    static double calculateLateFee(double feePerDay, LocalDate dueDate) {  // (#3:Static method)
        long daysLate = java.time.temporal.ChronoUnit.DAYS.between(dueDate, LocalDate.now());
        if (daysLate <= 0) return 0.0;
        return feePerDay * daysLate;
    }
}
Code Annotations:
  1. interface: Defines a contract - what methods must exist
  2. default: Provides implementation, can be overridden
  3. static: Utility method, called on interface itself

Step 3.2: Implement Borrowable in Book

Update Book to implement the Borrowable interface:

package com.library.model;

import java.time.LocalDate;
import java.util.Objects;

public class Book extends MediaItem implements Borrowable {  // (#1:Multiple inheritance of type)
    private String author;
    private String isbn;
    private BookCategory category;
    private int pageCount;

    // Loan tracking
    private Member currentBorrower;
    private LocalDate dueDate;

    // ... existing constructors ...

    // Implement Borrowable interface
    @Override
    public boolean canBorrow() {
        return getStatus().canBeBorrowed();
    }

    @Override
    public void checkout(Member member, LocalDate dueDate) {
        if (!canBorrow()) {
            throw new IllegalStateException(
                "Book cannot be borrowed: " + getStatus().getDescription());
        }
        this.currentBorrower = member;
        this.dueDate = dueDate;
        setStatus(MediaStatus.BORROWED);
    }

    @Override
    public void returnItem() {
        this.currentBorrower = null;
        this.dueDate = null;
        setStatus(MediaStatus.AVAILABLE);
    }

    @Override
    public int getStandardLoanDays() {  // (#2:Override default method)
        return getMaxLoanDays();  // Use book-specific loan period
    }

    // Loan info getters
    public Member getCurrentBorrower() { return currentBorrower; }
    public LocalDate getDueDate() { return dueDate; }

    public boolean isOverdue() {
        return dueDate != null && LocalDate.now().isAfter(dueDate);
    }

    public double calculateCurrentLateFee() {
        if (dueDate == null) return 0.0;
        return Borrowable.calculateLateFee(getLateFeePerDay(), dueDate);  // (#3:Call interface static method)
    }

    // ... rest of the class ...
}
Code Annotations:
  1. implements: A class can extend one class AND implement multiple interfaces
  2. Override default: Provide custom implementation instead of default
  3. Interface.staticMethod(): Call static methods using interface name

Part 4: Records for Immutable Data

Step 4.1: Create LoanInfo Record

Use a record to represent immutable loan information:

package com.library.model;

import java.time.LocalDate;
import java.time.temporal.ChronoUnit;

// Record declaration - one line replaces ~50 lines of boilerplate!
public record LoanInfo(  // (#1:Record keyword)
    String itemId,
    String itemTitle,
    String memberName,
    LocalDate checkoutDate,
    LocalDate dueDate
) {
    // Compact constructor for validation
    public LoanInfo {  // (#2:Compact constructor - no parentheses)
        if (checkoutDate.isAfter(dueDate)) {
            throw new IllegalArgumentException(
                "Checkout date cannot be after due date");
        }
    }

    // Custom method
    public long getDaysUntilDue() {
        return ChronoUnit.DAYS.between(LocalDate.now(), dueDate);
    }

    public boolean isOverdue() {
        return LocalDate.now().isAfter(dueDate);
    }

    // Static factory method
    public static LoanInfo createWithDefaultPeriod(
            String itemId, String itemTitle, String memberName, int loanDays) {
        var today = LocalDate.now();
        return new LoanInfo(itemId, itemTitle, memberName,
                           today, today.plusDays(loanDays));
    }
}

// Usage example:
// var loan = new LoanInfo("B001", "Java Guide", "Alice",
//                         LocalDate.now(), LocalDate.now().plusDays(21));
// System.out.println(loan);  // Auto-generated toString
// System.out.println(loan.itemTitle());  // Accessor (no "get" prefix)
// System.out.println(loan.getDaysUntilDue());  // Custom method
Code Annotations:
  1. record: Creates immutable data class with constructor, accessors, equals, hashCode, toString
  2. Compact constructor: Validates/normalizes input before assignment

Step 4.2: More Record Examples

Create additional records for other immutable data:

package com.library.model;

// Simple immutable member info (for display/reporting)
public record MemberSummary(
    String memberId,
    String name,
    int activeLoans,
    double outstandingFees
) {
    public boolean hasOutstandingFees() {
        return outstandingFees > 0;
    }

    public boolean canBorrowMore(int maxLoans) {
        return activeLoans < maxLoans && !hasOutstandingFees();
    }
}

// Search result record
public record SearchResult(
    MediaItem item,
    double relevanceScore,
    String matchedField
) implements Comparable<SearchResult> {
    @Override
    public int compareTo(SearchResult other) {
        return Double.compare(other.relevanceScore, this.relevanceScore);  // Descending
    }
}

Part 5: Pattern Matching and Polymorphism

Step 5.1: Pattern Matching with instanceof

Use pattern matching to handle different media types:

package com.library.service;

import com.library.model.*;

public class MediaProcessor {

    // Pattern matching with instanceof (Java 16+)
    public String getDetailedInfo(MediaItem item) {
        if (item instanceof Book book) {  // (#1:Pattern variable 'book')
            return String.format("""
                Book Details:
                  Title: %s
                  Author: %s
                  ISBN: %s
                  Category: %s
                  Pages: %d
                  Loan Period: %d days
                """,
                book.getTitle(), book.getAuthor(), book.getIsbn(),
                book.getCategory(), book.getPageCount(), book.getMaxLoanDays());

        } else if (item instanceof DVD dvd) {  // (#2:Another pattern variable)
            return String.format("""
                DVD Details:
                  Title: %s
                  Director: %s
                  Duration: %s
                  Rating: %s
                  Loan Period: %d days
                """,
                dvd.getTitle(), dvd.getDirector(),
                dvd.getFormattedDuration(), dvd.getRating(), dvd.getMaxLoanDays());

        } else if (item instanceof Magazine mag) {
            return String.format("""
                Magazine Details:
                  Title: %s
                  Publisher: %s
                  Issue: #%d (%s %d)
                  Loan Period: %d days
                """,
                mag.getTitle(), mag.getPublisher(),
                mag.getIssueNumber(), mag.getMonth(), mag.getYear(),
                mag.getMaxLoanDays());
        }

        return "Unknown media type";
    }

    // Calculate late fees using polymorphism
    public double calculateTotalLateFees(MediaItem[] items) {
        double total = 0.0;
        for (MediaItem item : items) {
            if (item instanceof Borrowable borrowable && borrowable.canBorrow()) {  // (#3:Pattern + condition)
                // Use interface method - don't care about specific type
                // The actual late fee calculation depends on the concrete type
            }
            // Polymorphism: each type knows its own late fee rate
            // item.getLateFeePerDay() returns type-specific value
        }
        return total;
    }
}
Code Annotations:
  1. Pattern variable: book is automatically cast and scoped
  2. Multiple patterns: Each branch gets its own typed variable
  3. Combined condition: Pattern + boolean condition in one check

Step 5.2: Polymorphic Processing

Update MediaCatalog to work with all media types:

package com.library.service;

import com.library.model.*;
import java.util.Arrays;

public class MediaCatalog {
    private MediaItem[] items;
    private int size;
    private static final int INITIAL_CAPACITY = 20;

    public MediaCatalog() {
        items = new MediaItem[INITIAL_CAPACITY];
        size = 0;
    }

    // Can add any MediaItem subtype
    public void addItem(MediaItem item) {  // (#1:Polymorphic parameter)
        if (size >= items.length) {
            items = Arrays.copyOf(items, items.length * 2);
        }
        items[size++] = item;
    }

    // Display all items - toString is polymorphic
    public void displayCatalog() {
        System.out.println("\n=== Library Catalog ===");
        for (int i = 0; i < size; i++) {
            System.out.printf("%3d. %s%n", i + 1, items[i]);  // (#2:Polymorphic toString)
        }
    }

    // Get items by type using pattern matching
    public Book[] getBooks() {
        return filterByType(Book.class);
    }

    public DVD[] getDVDs() {
        return filterByType(DVD.class);
    }

    @SuppressWarnings("unchecked")
    private <T extends MediaItem> T[] filterByType(Class<T> type) {
        int count = 0;
        for (int i = 0; i < size; i++) {
            if (type.isInstance(items[i])) count++;
        }

        T[] result = (T[]) java.lang.reflect.Array.newInstance(type, count);
        int index = 0;
        for (int i = 0; i < size; i++) {
            if (type.isInstance(items[i])) {
                result[index++] = type.cast(items[i]);
            }
        }
        return result;
    }

    // Statistics using polymorphism
    public void printStatistics() {
        int books = 0, dvds = 0, magazines = 0;
        double totalLateFeeRate = 0;

        for (int i = 0; i < size; i++) {
            MediaItem item = items[i];
            totalLateFeeRate += item.getLateFeePerDay();  // (#3:Polymorphic method call)

            // Pattern matching for counting
            if (item instanceof Book) books++;
            else if (item instanceof DVD) dvds++;
            else if (item instanceof Magazine) magazines++;
        }

        System.out.println("\n=== Catalog Statistics ===");
        System.out.printf("Books: %d, DVDs: %d, Magazines: %d%n", books, dvds, magazines);
        System.out.printf("Average late fee rate: $%.2f/day%n", totalLateFeeRate / size);
    }
}
Code Annotations:
  1. Polymorphic parameter: Method accepts any MediaItem subtype
  2. Polymorphic toString: Each type's toString is called automatically
  3. Polymorphic method: Correct getLateFeePerDay() called for each type

Part 6: Complete Example

Step 6.1: Main Application

Put it all together in the main application:

package com.library.app;

import com.library.model.*;
import com.library.service.*;
import java.time.LocalDate;

public class LibraryApp {
    public static void main(String[] args) {
        MediaCatalog catalog = new MediaCatalog();

        // Add different media types - polymorphism in action
        catalog.addItem(new Book("B001", "Clean Code", "Robert Martin",
            2008, "978-0132350884", BookCategory.TECHNOLOGY, 464));

        catalog.addItem(new Book("B002", "1984", "George Orwell",
            1949, "978-0451524935", BookCategory.FICTION, 328));

        catalog.addItem(new DVD("D001", "Inception", "Christopher Nolan",
            2010, 148, "PG-13"));

        catalog.addItem(new DVD("D002", "The Matrix", "Wachowskis",
            1999, 136, "R"));

        catalog.addItem(new Magazine("M001", "National Geographic",
            "National Geographic Society", 2024, 245, "January"));

        // Display catalog - polymorphic toString
        catalog.displayCatalog();

        // Get type-specific items
        System.out.println("\n=== Books Only ===");
        for (Book book : catalog.getBooks()) {
            System.out.println("  " + book.getTitle() + " - " + book.getAuthor());
        }

        // Statistics
        catalog.printStatistics();

        // Create loan info using records
        var loan = LoanInfo.createWithDefaultPeriod(
            "B001", "Clean Code", "Alice", 21);
        System.out.println("\n=== Active Loan ===");
        System.out.println(loan);
        System.out.println("Days until due: " + loan.getDaysUntilDue());

        // Process with pattern matching
        MediaProcessor processor = new MediaProcessor();
        for (MediaItem item : new MediaItem[]{
                catalog.getBooks()[0], catalog.getDVDs()[0]}) {
            System.out.println(processor.getDetailedInfo(item));
        }
    }
}

Expected Output

=== Library Catalog ===
  1. Book: "Clean Code" by Robert Martin (2008) - TECHNOLOGY [AVAILABLE]
  2. Book: "1984" by George Orwell (1949) - FICTION [AVAILABLE]
  3. DVD: "Inception" dir. Christopher Nolan (2010) - 2h 28m, PG-13 [AVAILABLE]
  4. DVD: "The Matrix" dir. Wachowskis (1999) - 2h 16m, R [AVAILABLE]
  5. Magazine: National Geographic #245 (January 2024) - National Geographic Society [AVAILABLE]

=== Books Only ===
  Clean Code - Robert Martin
  1984 - George Orwell

=== Catalog Statistics ===
Books: 2, DVDs: 2, Magazines: 1
Average late fee rate: $0.65/day

=== Active Loan ===
LoanInfo[itemId=B001, itemTitle=Clean Code, memberName=Alice, checkoutDate=2024-01-15, dueDate=2024-02-05]
Days until due: 21

Book Details:
  Title: Clean Code
  Author: Robert Martin
  ISBN: 978-0132350884
  Category: TECHNOLOGY
  Pages: 464
  Loan Period: 21 days

DVD Details:
  Title: Inception
  Director: Christopher Nolan
  Duration: 2h 28m
  Rating: PG-13
  Loan Period: 7 days

Exercises

Exercise 1: Add Audiobook Class

Create an Audiobook class that extends MediaItem:

  • Add fields: narrator, durationMinutes, format (CD/Digital)
  • Implement all abstract methods
  • Loan period: 14 days, Late fee: $0.75/day
  • Override toString() with audiobook-specific info

Exercise 2: Reservable Interface

Create a Reservable interface and implement it:

  • Methods: reserve(Member member), cancelReservation(), isReserved()
  • Add a default method: getReservationExpirationDays() returning 3
  • Implement in Book and DVD classes
  • Magazines should NOT be reservable

Exercise 3: Complete equals/hashCode

Implement proper equals() and hashCode() for DVD and Magazine:

  • DVDs are equal if they have the same ID and director
  • Magazines are equal if they have the same ID and issue number
  • Test with HashSet to verify duplicates are detected

Exercise 4: Loan History Record

Create a LoanHistory record to track past loans:

  • Fields: itemId, memberName, checkoutDate, returnDate, lateFeeCharged
  • Add method: getLoanDuration() returning number of days
  • Add method: wasLate() returning boolean
  • Add static factory: fromActiveLoan(LoanInfo loan, LocalDate returnDate, double fee)

Deliverables

Bonus Challenges

Advanced Sealed Classes: Convert MediaItem to a sealed class that only permits Book, DVD, Magazine, and Audiobook
Advanced Pattern Matching Switch: Rewrite getDetailedInfo() using switch expression with pattern matching
Expert Generic Interface: Create a Searchable<T> interface that returns search results with relevance scores

Resources