LearnAI ToolsCareerPractice BuildsPlayContact
JavaIntermediate~2.5 hours

Library Management System

Track books, members, and due dates using inheritance for different book/member types.

InheritancePolymorphismFile Handling

Overview

Every library has more than one kind of member, and every kind follows different rules — a student can usually borrow a handful of books at a time, while a faculty member is typically allowed far more. Modeling that in Java is a textbook use case for inheritance and polymorphism: `StudentMember` and `FacultyMember` are both unmistakably a `Member`, sharing an id, a name, and a list of borrowed titles, but each supplies its own answer to "how many books can you borrow at once?" That is exactly what an abstract method is for.

By the end of this tutorial you will have a console library application with an abstract `Member` base class, two concrete subclasses (`StudentMember` and `FacultyMember`), a `Book` class, and a `Library` class that manages both collections and persists the book catalog to disk with `java.io`. The most important moment in the whole project is a single line — `member.canBorrow()` — that behaves differently depending on whether `member` actually refers to a `StudentMember` or a `FacultyMember` object, decided automatically at runtime. That is polymorphism working exactly as intended.

What You'll Build
  • A `Book` class tracking title, author, ISBN, and issued/available status.
  • An abstract `Member` base class with an abstract `getBorrowLimit()` method and a shared `canBorrow()`.
  • `StudentMember` and `FacultyMember` subclasses that each override `getBorrowLimit()` with their own privilege.
  • A `Library` class holding an `ArrayList<Book>` and an `ArrayList<Member>` together.
  • Polymorphic `issueBook()`/`returnBook()` operations that respect each member's actual borrow limit.
  • File persistence for the book catalog using `java.io.FileWriter` and `BufferedReader`.

Prerequisites

  • Classes — `private`/`protected`/`public` access and member methods.
  • Inheritance — deriving a class with `class Sub extends Base` and calling `super(...)`.
  • Polymorphism — `abstract` classes, `abstract` methods, and the `@Override` annotation.
  • Collections basics — `ArrayList<Book>` and `ArrayList<Member>`.
  • File handling in Java — writing with `FileWriter`/`BufferedWriter` and reading with `BufferedReader`, plus basic `try`/`catch` around `IOException`.

Project Structure

The program lives in a single file, `LibraryManagementSystem.java`. `Book` is a plain, non-polymorphic class — every book behaves the same way regardless of what it contains. `Member`, by contrast, is deliberately declared `abstract`: it defines an abstract `getBorrowLimit()` method with no body, which means `Member` itself can never be instantiated with `new Member(...)` — only `StudentMember` and `FacultyMember`, which each supply a real implementation, can. `Library` stores books and members together in an `ArrayList<Book>` and an `ArrayList<Member>`; the member list can hold either subclass interchangeably because both are-a `Member`.

The catalog is persisted as plain comma-separated text, one line per book, using `java.io.FileWriter` to write and `java.io.BufferedReader` to read it back with `readLine()`. That is the same reason a C++ or Python program would choose text over raw binary here: comma-separated text is trivial to inspect, edit by hand, and parse back with `String.split(",")`, and it does not depend on any particular class layout staying stable between runs.

Step 1: Define the Book Class

A `Book` is simple enough to fully define in one step: an ISBN, title, author, and two fields tracking whether it is currently issued and to whom. `markIssued()`/`markReturned()` are the only ways those two fields ever change, keeping a book's issued status consistent no matter which part of the program touches it.

class Book {
private String isbn; // Unique identifier for this book
private String title; // Book title
private String author; // Book author
private boolean issued; // Whether this book is currently checked out
private int issuedToId = -1; // Member id currently holding this book; -1 means not issued
public Book(String isbn, String title, String author) {
this.isbn = isbn;
this.title = title;
this.author = author;
this.issued = false; // New books always start available
}
public String getIsbn() {
return isbn;
}
public String getTitle() {
return title;
}
public String getAuthor() {
return author;
}
public boolean isIssued() {
return issued;
}
public int getIssuedToId() {
return issuedToId;
}
public void markIssued(int memberId) { // Called by the Library when a book is checked out
issued = true;
issuedToId = memberId;
}
public void markReturned() { // Called by the Library when a book comes back
issued = false;
issuedToId = -1;
}
@Override
public String toString() {
return isbn + " | " + title + " by " + author + (issued ? " [ISSUED]" : " [AVAILABLE]");
}
}

Step 2: Define an Abstract Member Base Class

Every member — regardless of type — has an id, a name, and a list of currently borrowed titles, so those live in `Member` as `protected` (not `private`) fields, meaning `StudentMember` and `FacultyMember` can read them directly in their own methods instead of going through an accessor. `getBorrowLimit()` is declared `abstract`, which means `Member` provides no body for it at all, and as a direct consequence Java requires the entire class to be marked `abstract` too — the compiler will refuse to compile `new Member(...)` anywhere in the program.

import java.util.ArrayList;
import java.util.List;
abstract class Member {
protected int id; // protected, not private, so subclasses can read it directly
protected String name; // Member's display name
protected List<String> borrowedBooks = new ArrayList<>(); // Titles this member currently has checked out
public Member(int id, String name) {
this.id = id;
this.name = name;
}
public int getId() {
return id;
}
public String getName() {
return name;
}
public abstract int getBorrowLimit(); // No body here: every concrete subclass must supply its own limit
public abstract String getRole(); // No body here: identifies the concrete type for display purposes
}

An abstract method is not "a method that does nothing" — it is "no default exists, and every concrete subclass must provide one." That is different from an ordinary (non-abstract) method defined in `Member`, which does have a body and which a subclass may optionally override.

Step 3: Implement Member's Shared Behavior

`canBorrow()` is where polymorphism does real work: it calls `getBorrowLimit()` on `this`, and because that method is abstract in `Member` but overridden in each subclass, the call resolves at runtime to whichever concrete class the object actually is. Three books borrowed is under the limit for a `FacultyMember` but over the limit for a `StudentMember`, and `canBorrow()` does not need an `if`/`else` to know which one it is talking to — Java's dynamic dispatch handles that automatically.

public boolean canBorrow() {
return borrowedBooks.size() < getBorrowLimit(); // Dispatches to whichever subclass's real getBorrowLimit() applies
}
public void borrowBook(String title) {
borrowedBooks.add(title); // Record that this member now holds this title
}
public void returnBook(String title) {
borrowedBooks.remove(title); // ArrayList.remove(Object) removes the first matching element, if any
}
public void displayInfo() {
System.out.println("ID: " + id + " | Name: " + name + " | Role: " + getRole()
+ " | Borrow Limit: " + getBorrowLimit() + " | Currently Borrowed: " + borrowedBooks.size());
}

`displayInfo()` is never overridden by `StudentMember` or `FacultyMember` in this project — it does not need to be, since it already produces the correct output for either subclass by calling the abstract `getRole()` and `getBorrowLimit()` internally. This is a subtle but important point: a subclass benefits from polymorphism the moment it overrides even one small method that a larger, un-overridden method depends on.

Step 4: Derive StudentMember and FacultyMember

With `Member` doing all the shared work, each subclass is remarkably small: a constructor that forwards to `Member`'s constructor with `super(id, name)`, and two one-line overrides supplying that type's actual borrow limit and role label. `@Override` is not strictly required by the compiler, but it is good practice — it tells the compiler "I intend to override a method from the base class," and the compiler produces an error if the signature does not actually match one, catching typos immediately instead of silently creating an unrelated new method.

class StudentMember extends Member { // "extends Member" means a StudentMember IS-A Member everywhere one is expected
public StudentMember(int id, String name) {
super(id, name); // Forwards straight to Member's constructor
}
@Override
public int getBorrowLimit() {
return 3; // Students may have at most 3 books out at once
}
@Override
public String getRole() {
return "Student";
}
}
class FacultyMember extends Member {
public FacultyMember(int id, String name) {
super(id, name);
}
@Override
public int getBorrowLimit() {
return 10; // Faculty get a much higher borrowing privilege
}
@Override
public String getRole() {
return "Faculty";
}
}

Step 5: Build the Library Class

`Library` owns both collections: books in an `ArrayList<Book>`, since a book never needs to "become" a different kind of book, and members in an `ArrayList<Member>`, since a `Member` reference can point at either a `StudentMember` or a `FacultyMember` interchangeably. Java's garbage collector reclaims member objects automatically once nothing references them anymore, so unlike a manually memory-managed language, `Library` does not need a destructor to free them.

class Library {
private List<Book> books = new ArrayList<>(); // Every book in the catalog
private List<Member> members = new ArrayList<>(); // Base-type list so Students and Faculty are stored together
public void addBook(String isbn, String title, String author) {
books.add(new Book(isbn, title, author));
}
public void addMember(Member m) {
members.add(m); // Accepts a StudentMember or FacultyMember interchangeably, since both extend Member
}
public Book findBook(String isbn) {
for (Book b : books) {
if (b.getIsbn().equals(isbn)) { // Use .equals() for String comparison, never == for content equality
return b;
}
}
return null;
}
public Member findMember(int id) {
for (Member m : members) {
if (m.getId() == id) {
return m;
}
}
return null;
}
public void listBooks() {
System.out.println("\n--- Library Catalog ---");
for (Book b : books) {
System.out.println(b); // Uses Book's toString() from Step 1
}
}
public void listMembers() {
System.out.println("\n--- Library Members ---");
for (Member m : members) {
m.displayInfo(); // Polymorphic call: runs each member's real (Student or Faculty) data through Member
}
}
public List<Book> getBooks() { // Exposed for Step 7's file persistence
return books;
}
}

Step 6: Issue and Return Books Polymorphically

`issueBook()` is where every piece built so far comes together: it looks up the book and member, rejects the request if the book is already out or the member does not exist, and then calls `member.canBorrow()` — the same polymorphic check from Step 3 — to enforce whichever borrow limit actually applies to that member.

public void issueBook(String isbn, int memberId) {
Book book = findBook(isbn);
Member member = findMember(memberId);
if (book == null) {
System.out.println("Book not found.");
return;
}
if (member == null) {
System.out.println("Member not found.");
return;
}
if (book.isIssued()) {
System.out.println("That book is already issued.");
return;
}
if (!member.canBorrow()) { // canBorrow() uses the member's real (subclass) borrow limit via dynamic dispatch
System.out.println(member.getName() + " has reached their borrow limit of " + member.getBorrowLimit() + ".");
return;
}
book.markIssued(memberId);
member.borrowBook(book.getTitle());
System.out.println("Issued \"" + book.getTitle() + "\" to " + member.getName() + ".");
}
public void returnBook(String isbn) {
Book book = findBook(isbn);
if (book == null || !book.isIssued()) {
System.out.println("That book is not currently issued.");
return;
}
Member member = findMember(book.getIssuedToId()); // Look up whoever currently holds the book
if (member != null) {
member.returnBook(book.getTitle()); // Remove the title from that member's borrowed list
}
book.markReturned();
System.out.println("\"" + book.getTitle() + "\" has been returned.");
}

Step 7: Persist the Catalog to a File

`saveToFile()` writes one comma-separated line per book with `FileWriter`, and `loadFromFile()` reads those lines back with `BufferedReader.readLine()`, splitting each line on commas with `String.split(",")` to recover the ISBN, title, and author. Both methods are wrapped in `try`/`catch (IOException e)`, since `IOException` is itself a checked exception — the compiler requires file operations to be handled the same way `withdraw()` required handling `InsufficientFundsException` in the previous project.

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
public void saveToFile(String filename) {
try (BufferedWriter writer = new BufferedWriter(new FileWriter(filename))) { // try-with-resources closes automatically
for (Book b : books) {
// Comma-separated line: isbn,title,author
writer.write(b.getIsbn() + "," + b.getTitle() + "," + b.getAuthor());
writer.newLine();
}
} catch (IOException e) {
System.out.println("Could not save catalog: " + e.getMessage());
}
}
public void loadFromFile(String filename) {
File file = new File(filename);
if (!file.exists()) { // No file yet means this is the first run; nothing to load
return;
}
try (BufferedReader reader = new BufferedReader(new FileReader(filename))) {
String line;
while ((line = reader.readLine()) != null) { // Read one comma-separated record per line until EOF
String[] parts = line.split(","); // Splits "isbn,title,author" into 3 fields
if (parts.length == 3) {
addBook(parts[0], parts[1], parts[2]);
}
}
} catch (IOException e) {
System.out.println("Could not load catalog: " + e.getMessage());
}
}

The `try (BufferedWriter writer = ...)` syntax is called try-with-resources: any resource declared inside the parentheses that implements `AutoCloseable` (which `BufferedWriter` and `BufferedReader` both do) is automatically closed when the block exits, whether it exits normally or because of an exception — removing the need for a manual `finally { writer.close(); }` block.

Step 8: Build the Menu Loop

`main()` loads any previously saved catalog, seeds a couple of starting members, then loops over a menu that lets the user add more books/members, issue and return books, and list the catalog and roster, saving the catalog back to disk after every book is added so no addition is lost even if the program closes unexpectedly.

import java.io.File;
import java.util.Scanner;
public class LibraryManagementSystem {
public static void main(String[] args) {
Library library = new Library();
String filename = "catalog.txt";
library.loadFromFile(filename); // Restore whatever catalog was saved from a previous run
// Seed the member list with a couple of starting records; addMember() accepts either subclass polymorphically
library.addMember(new StudentMember(1, "Aditi Sharma"));
library.addMember(new FacultyMember(2, "Dr. Rakesh Gupta"));
Scanner scanner = new Scanner(System.in);
int choice;
do {
System.out.println("\n===== LIBRARY MANAGEMENT SYSTEM =====");
System.out.println("1. Add Book");
System.out.println("2. Add Member");
System.out.println("3. Issue Book");
System.out.println("4. Return Book");
System.out.println("5. List Books");
System.out.println("6. List Members");
System.out.println("7. Exit");
System.out.print("Enter your choice: ");
choice = scanner.nextInt();
scanner.nextLine(); // Discard the leftover newline left behind by nextInt()
if (choice == 1) {
System.out.print("Enter ISBN: ");
String isbn = scanner.nextLine();
System.out.print("Enter title: ");
String title = scanner.nextLine();
System.out.print("Enter author: ");
String author = scanner.nextLine();
library.addBook(isbn, title, author);
library.saveToFile(filename); // Persist immediately so a new book survives an unexpected exit
System.out.println("Book added.");
} else if (choice == 2) {
System.out.print("Enter member ID: ");
int id = scanner.nextInt();
scanner.nextLine();
System.out.print("Enter member name: ");
String name = scanner.nextLine();
System.out.print("Enter member type (student/faculty): ");
String type = scanner.nextLine();
if (type.equalsIgnoreCase("faculty")) {
library.addMember(new FacultyMember(id, name)); // Polymorphism: stored as a Member either way
} else {
library.addMember(new StudentMember(id, name));
}
System.out.println("Member added.");
} else if (choice == 3) {
System.out.print("Enter ISBN to issue: ");
String isbn = scanner.nextLine();
System.out.print("Enter member ID: ");
int memberId = scanner.nextInt();
scanner.nextLine();
library.issueBook(isbn, memberId);
} else if (choice == 4) {
System.out.print("Enter ISBN to return: ");
String isbn = scanner.nextLine();
library.returnBook(isbn);
} else if (choice == 5) {
library.listBooks();
} else if (choice == 6) {
library.listMembers();
} else if (choice == 7) {
System.out.println("Goodbye!");
} else {
System.out.println("Invalid choice, try again.");
}
} while (choice != 7);
scanner.close();
}
}

Complete Code

Here is the full program with every class assembled in the correct order, ready to save as `LibraryManagementSystem.java` and run with `javac LibraryManagementSystem.java && java LibraryManagementSystem`.

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
class Book {
private String isbn;
private String title;
private String author;
private boolean issued;
private int issuedToId = -1;
public Book(String isbn, String title, String author) {
this.isbn = isbn;
this.title = title;
this.author = author;
this.issued = false;
}
public String getIsbn() {
return isbn;
}
public String getTitle() {
return title;
}
public String getAuthor() {
return author;
}
public boolean isIssued() {
return issued;
}
public int getIssuedToId() {
return issuedToId;
}
public void markIssued(int memberId) {
issued = true;
issuedToId = memberId;
}
public void markReturned() {
issued = false;
issuedToId = -1;
}
@Override
public String toString() {
return isbn + " | " + title + " by " + author + (issued ? " [ISSUED]" : " [AVAILABLE]");
}
}
abstract class Member {
protected int id;
protected String name;
protected List<String> borrowedBooks = new ArrayList<>();
public Member(int id, String name) {
this.id = id;
this.name = name;
}
public int getId() {
return id;
}
public String getName() {
return name;
}
public abstract int getBorrowLimit();
public abstract String getRole();
public boolean canBorrow() {
return borrowedBooks.size() < getBorrowLimit();
}
public void borrowBook(String title) {
borrowedBooks.add(title);
}
public void returnBook(String title) {
borrowedBooks.remove(title);
}
public void displayInfo() {
System.out.println("ID: " + id + " | Name: " + name + " | Role: " + getRole()
+ " | Borrow Limit: " + getBorrowLimit() + " | Currently Borrowed: " + borrowedBooks.size());
}
}
class StudentMember extends Member {
public StudentMember(int id, String name) {
super(id, name);
}
@Override
public int getBorrowLimit() {
return 3;
}
@Override
public String getRole() {
return "Student";
}
}
class FacultyMember extends Member {
public FacultyMember(int id, String name) {
super(id, name);
}
@Override
public int getBorrowLimit() {
return 10;
}
@Override
public String getRole() {
return "Faculty";
}
}
class Library {
private List<Book> books = new ArrayList<>();
private List<Member> members = new ArrayList<>();
public void addBook(String isbn, String title, String author) {
books.add(new Book(isbn, title, author));
}
public void addMember(Member m) {
members.add(m);
}
public Book findBook(String isbn) {
for (Book b : books) {
if (b.getIsbn().equals(isbn)) {
return b;
}
}
return null;
}
public Member findMember(int id) {
for (Member m : members) {
if (m.getId() == id) {
return m;
}
}
return null;
}
public void listBooks() {
System.out.println("\n--- Library Catalog ---");
for (Book b : books) {
System.out.println(b);
}
}
public void listMembers() {
System.out.println("\n--- Library Members ---");
for (Member m : members) {
m.displayInfo();
}
}
public List<Book> getBooks() {
return books;
}
public void issueBook(String isbn, int memberId) {
Book book = findBook(isbn);
Member member = findMember(memberId);
if (book == null) {
System.out.println("Book not found.");
return;
}
if (member == null) {
System.out.println("Member not found.");
return;
}
if (book.isIssued()) {
System.out.println("That book is already issued.");
return;
}
if (!member.canBorrow()) {
System.out.println(member.getName() + " has reached their borrow limit of " + member.getBorrowLimit() + ".");
return;
}
book.markIssued(memberId);
member.borrowBook(book.getTitle());
System.out.println("Issued \"" + book.getTitle() + "\" to " + member.getName() + ".");
}
public void returnBook(String isbn) {
Book book = findBook(isbn);
if (book == null || !book.isIssued()) {
System.out.println("That book is not currently issued.");
return;
}
Member member = findMember(book.getIssuedToId());
if (member != null) {
member.returnBook(book.getTitle());
}
book.markReturned();
System.out.println("\"" + book.getTitle() + "\" has been returned.");
}
public void saveToFile(String filename) {
try (BufferedWriter writer = new BufferedWriter(new FileWriter(filename))) {
for (Book b : books) {
writer.write(b.getIsbn() + "," + b.getTitle() + "," + b.getAuthor());
writer.newLine();
}
} catch (IOException e) {
System.out.println("Could not save catalog: " + e.getMessage());
}
}
public void loadFromFile(String filename) {
File file = new File(filename);
if (!file.exists()) {
return;
}
try (BufferedReader reader = new BufferedReader(new FileReader(filename))) {
String line;
while ((line = reader.readLine()) != null) {
String[] parts = line.split(",");
if (parts.length == 3) {
addBook(parts[0], parts[1], parts[2]);
}
}
} catch (IOException e) {
System.out.println("Could not load catalog: " + e.getMessage());
}
}
}
public class LibraryManagementSystem {
public static void main(String[] args) {
Library library = new Library();
String filename = "catalog.txt";
library.loadFromFile(filename);
library.addMember(new StudentMember(1, "Aditi Sharma"));
library.addMember(new FacultyMember(2, "Dr. Rakesh Gupta"));
Scanner scanner = new Scanner(System.in);
int choice;
do {
System.out.println("\n===== LIBRARY MANAGEMENT SYSTEM =====");
System.out.println("1. Add Book");
System.out.println("2. Add Member");
System.out.println("3. Issue Book");
System.out.println("4. Return Book");
System.out.println("5. List Books");
System.out.println("6. List Members");
System.out.println("7. Exit");
System.out.print("Enter your choice: ");
choice = scanner.nextInt();
scanner.nextLine();
if (choice == 1) {
System.out.print("Enter ISBN: ");
String isbn = scanner.nextLine();
System.out.print("Enter title: ");
String title = scanner.nextLine();
System.out.print("Enter author: ");
String author = scanner.nextLine();
library.addBook(isbn, title, author);
library.saveToFile(filename);
System.out.println("Book added.");
} else if (choice == 2) {
System.out.print("Enter member ID: ");
int id = scanner.nextInt();
scanner.nextLine();
System.out.print("Enter member name: ");
String name = scanner.nextLine();
System.out.print("Enter member type (student/faculty): ");
String type = scanner.nextLine();
if (type.equalsIgnoreCase("faculty")) {
library.addMember(new FacultyMember(id, name));
} else {
library.addMember(new StudentMember(id, name));
}
System.out.println("Member added.");
} else if (choice == 3) {
System.out.print("Enter ISBN to issue: ");
String isbn = scanner.nextLine();
System.out.print("Enter member ID: ");
int memberId = scanner.nextInt();
scanner.nextLine();
library.issueBook(isbn, memberId);
} else if (choice == 4) {
System.out.print("Enter ISBN to return: ");
String isbn = scanner.nextLine();
library.returnBook(isbn);
} else if (choice == 5) {
library.listBooks();
} else if (choice == 6) {
library.listMembers();
} else if (choice == 7) {
System.out.println("Goodbye!");
} else {
System.out.println("Invalid choice, try again.");
}
} while (choice != 7);
scanner.close();
}
}

Sample Run

Sample Run

Click Run to see what this code prints.

Extend This Project

  • Add a `LibrarianMember` subclass with an even higher (or unlimited) borrow limit and the ability to add books directly.
  • Track a due date per borrowed book and calculate a fine in a new abstract `calculateFine(int daysLate)` method, overridden differently per role (e.g. Faculty pay no fine).
  • Persist members (not just books) to their own file using the same comma-separated pattern from Step 7.
  • Let `Library.listMembers()` accept a role filter (e.g. only Students) using `member instanceof StudentMember` to check each member's concrete type.
  • Wrap the whole catalog file in a `try`/`catch` that also handles a corrupted line (fewer than 3 comma-separated fields) without crashing the load.

Summary

You built a library system where the borrowing rules genuinely differ by member type, enforced through an abstract `getBorrowLimit()` rather than an `if`/`else` chain checking a "type" flag. That difference matters: adding a third member type later means writing one new small subclass, not hunting down every place in the program that branches on member type. Real polymorphism, the abstract base class that makes it possible, and the file persistence layered on top are some of the most valuable tools Java gives you for keeping growing programs manageable.