A class defines the state and behavior its objects can have. An object is one particular instance: two library loans can follow the same rules while tracking different books. Object-oriented programming becomes useful when it keeps those rules close to the data they protect. This lesson uses Java 21.
Protect valid state
An invariant is a rule that must remain true, such as a loan always referring to an actual book. A constructor establishes the initial state. Methods describe permitted changes. Private fields prevent callers from bypassing those methods and creating impossible states.
this refers to the current object. In this.book = book, it distinguishes the field from the constructor parameter. An instance field belongs to each object; a static field belongs to the class. Do not make ordinary object state static merely to access it from main.
Save as LibraryDemo.java, compile with javac --release 21 LibraryDemo.java, and run java LibraryDemo.
import java.util.Objects;
public class LibraryDemo {
enum Status { OPEN, RETURNED }
record Book(String title) {
Book {
Objects.requireNonNull(title, "title");
title = title.strip();
if (title.isEmpty()) {
throw new IllegalArgumentException("title must not be blank");
}
}
}
static final class Loan {
private final Book book;
private Status status = Status.OPEN;
Loan(Book book) {
this.book = Objects.requireNonNull(book, "book");
}
void returnBook() {
if (status != Status.OPEN) {
throw new IllegalStateException("book already returned");
}
status = Status.RETURNED;
}
String summary() {
return book.title() + ": " + status;
}
}
public static void main(String[] args) {
Book first = new Book(" Java Notes ");
Book second = new Book("Java Notes");
Loan loan = new Loan(first);
System.out.println(first.equals(second));
System.out.println(first == second);
System.out.println(loan.summary());
loan.returnBook();
System.out.println(loan.summary());
}
}
Expected output:
true
false
Java Notes: OPEN
Java Notes: RETURNED
The nested types keep this teaching example in one file. Loan is a static nested class so creating it does not require an enclosing LibraryDemo object. Its final book reference cannot be reassigned, but its status changes through a checked transition. Returning the same loan twice deliberately fails rather than pretending another return occurred.
Values, identity and records
The two books are distinct objects with equal component values. == checks identity; equals checks the type's equality rule. An ordinary class inherits identity-based equality from Object unless you override it.
A record is a concise value carrier. Book receives a constructor, a title() accessor, and component-based equals, hashCode and toString. Its compact constructor validates and normalizes input before the component field is assigned. Record components are final, but records are only shallowly immutable: a component referencing a mutable list can still expose changes unless you copy that list defensively.
When overriding equals in a regular class, also implement hashCode consistently: equal objects must have equal hash codes. This matters when objects become keys in maps or members of sets. Do not use changing fields in a key's equality while it is stored in a hash collection.
An enum represents a fixed set of named values. Status prevents misspelled strings such as RETUNRED. Comparing enum values with == is appropriate. Avoid persisting their numeric ordinal() positions, because rearranging constants changes those numbers.
Visibility, inheritance and composition
Use public for an API that other packages should call, private for internal implementation, and no modifier for access within the package. protected permits package access and certain subclass access. Keep the public surface small. Packages organize names and access boundaries; imports let you write short type names without copying or including another source file.
A class can extend one superclass. An overriding instance method replaces inherited behavior for that subtype; runtime dispatch chooses the implementation of the actual object. @Override asks the compiler to check that an override really exists. An abstract class cannot be instantiated and may require subclasses to implement abstract methods. A final class, like Loan, cannot be subclassed.
Inheritance should model substitutable behavior. Composition means holding another object and delegating work: our loan has a book. Prefer that direct relationship when you only need to reuse or coordinate behavior. A loan is not a special kind of book.
Exercise and solution guidance
Add a renew() operation that counts renewals, allows at most two, and rejects renewal after return. Keep the counter private. Test a new loan, two successful renewals, a third attempt, and renewal after returning a separate loan.
Check the status and limit before incrementing. Failed calls must leave the previous state intact. Expose a read-only count method if a report needs it; a public setter would let callers bypass the limit. As a second exercise, reject a blank book title and explain why validation belongs in the constructor rather than only in main.
Official references
Previous: Arrays and Strings. Next: Interfaces, Collections and Generics.

