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.
- A
Bookclass tracking title, author, ISBN, and issued/available status. - An abstract
Memberbase class with an abstractgetBorrowLimit()method and a sharedcanBorrow(). StudentMemberandFacultyMembersubclasses that each overridegetBorrowLimit()with their own privilege.- A
Libraryclass holding anArrayList<Book>and anArrayList<Member>together. - Polymorphic
issueBook()/returnBook()operations that respect each member's actual borrow limit. - File persistence for the book catalog using
java.io.FileWriterandBufferedReader.
Prerequisites
- Classes —
private/protected/publicaccess and member methods. - Inheritance — deriving a class with
class Sub extends Baseand callingsuper(...). - Polymorphism —
abstractclasses,abstractmethods, and the@Overrideannotation. - Collections basics —
ArrayList<Book>andArrayList<Member>. - File handling in Java — writing with
FileWriter/BufferedWriterand reading withBufferedReader, plus basictry/catcharoundIOException.
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
Click Run to see what this code prints.
Extend This Project
- Add a
LibrarianMembersubclass 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) usingmember instanceof StudentMemberto check each member's concrete type. - Wrap the whole catalog file in a
try/catchthat 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.