Library Management System - Media Types
Refactor the library using inheritance, interfaces, and modern Java features
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(), andhashCode() - 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
thiskeyword
Project Structure
Update your project to include the new media type hierarchy:
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);
}
}
- Abstract class: Cannot create
new MediaItem()directly - Protected: Accessible by subclasses but not external code
- Abstract method: Declares signature, subclass provides implementation
- toString override: Returns meaningful description instead of hash
- equals override: Two items are equal if they have the same ID
- 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)
}
}
- extends: Book inherits all non-private members from MediaItem
- super(): Must call parent constructor to initialize inherited fields
- this(): Call another constructor in same class
- super.equals(): Reuse parent's equality check
- Pattern matching:
instanceof Book othercombines check and cast - 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;
}
}
- interface: Defines a contract - what methods must exist
- default: Provides implementation, can be overridden
- 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 ...
}
- implements: A class can extend one class AND implement multiple interfaces
- Override default: Provide custom implementation instead of default
- 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
- record: Creates immutable data class with constructor, accessors, equals, hashCode, toString
- 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;
}
}
- Pattern variable:
bookis automatically cast and scoped - Multiple patterns: Each branch gets its own typed variable
- 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);
}
}
- Polymorphic parameter: Method accepts any MediaItem subtype
- Polymorphic toString: Each type's toString is called automatically
- 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)