We have been carrying a debt through the whole module, and the time has come to pay it. The fields of Book, Employee, Loan and Material are still public, and that means any line of any file can write book.available = true skipping lend()'s warnings, or employee.totalLoans = -7, or assign a loan a made-up fine that corresponds to no calculation at all. All the work the constructors do validating data collapses the moment somebody can write directly into the fields. Encapsulation is the pillar that closes that door. But careful: encapsulating is not "putting a getter and a setter on every field" —that mechanical recipe, so often repeated, leaves the object as exposed as before with more lines of code—. Encapsulating is hiding the internal representation and exposing only operations that preserve the business rules. This lesson teaches you the difference, which is what separates an anaemic data model from a genuine domain model.

Contents

  1. What encapsulating really is
  2. The four access modifiers
  3. Complete visibility table
  4. Getters and setters: what the mechanical recipe does not tell you
  5. Class invariants
  6. Business operations instead of setters
  7. Immutability
  8. The danger of returning mutable references: defensive copying
  9. Package-level encapsulation
  10. The Law of Demeter
  11. BiblioTech: the encapsulated domain
  12. Common Mistakes and Tips
  13. Exercises

  1. What encapsulating really is

Encapsulating is separating what an object offers from how it does it, so that the interior can be changed without breaking anyone.

Think of a cash machine. Its public interface is: insert card, type PIN, ask for an amount, take the notes. What is inside —the layout of the cassettes, the order in which notes are dispensed, the protocol with the bank— is not only hidden, it is that it must not matter to you. If the cassettes change tomorrow, you still take out money the same way.

Applied to code, encapsulating well produces three concrete benefits:

Benefit What it means in practice
Guaranteed invariants If totalLoans can only change through methods, it is impossible for it to be negative
Freedom to change the interior You can replace a field with a calculation without touching the class's users
Small error surface If something goes wrong in an object's state, the culprit is inside its class, not in 40 files

The second benefit is the most underrated, and it deserves an example. Suppose Loan stores a fine field:

public double fine;       // public: twenty files read it

If one day you decide the fine should not be stored but always computed from the days, you have to modify those twenty files. Whereas if from the start you expose a method:

public double getFine() {
    return Math.min(calculateDaysLate() * getDailyRate(), MAX_FINE);
}

...you can change the implementation as often as you like: the twenty files keep calling getFine() without noticing. A method is a contract; a field is a leak.

  1. The four access modifiers

Java offers four levels of visibility. Three have a reserved word and one is what applies when you write nothing:

Modifier Keyword Idea in one sentence
Private private Only inside the class itself
Package (none) Inside the same package
Protected protected Same package and subclasses, wherever they are
Public public From anywhere
public class Book {
    private   String  title;         // Book only
              String  internalNote;  // classes in the domain package
    protected boolean available;     // domain package + subclasses of Book
    public    String  getTitle() { return title; }     // everyone
}

They apply to classes, fields, methods and constructors. With one nuance: a top-level class can only be public or package-private; private and protected make no sense there (they do in nested classes, lesson 04-03).

  1. Complete visibility table

This is the table worth keeping to hand until it is internalised:

Modifier Same class Same package Subclass in another package Any class
private Yes No No No
(default) Yes Yes No No
protected Yes Yes Yes No
public Yes Yes Yes Yes

Two nuances that get asked a lot:

About protected. It includes package visibility: a protected member is visible to every class in the same package, whether or not they are subclasses. And from a subclass in another package, it is only accessed through a reference of the subclass's type, not the superclass's. It is a subtle rule that rarely gets in the way in practice.

About private and nested classes. A class can access the private members of another class nested inside it, and vice versa: the unit of encapsulation in Java is the top-level class's file, not each individual class. You will see it in 04-03.

Professional starting criterion:

Declare everything private by default. Raise the visibility only when you have a concrete reason, and document that reason.

In particular, be wary of protected on fields: it turns every present and future subclass into a client that depends on your internal representation, and it is exactly the fragile base class scenario you saw in 03-05. If a subclass needs something, prefer giving it a protected method.

  1. Getters and setters: what the mechanical recipe does not tell you

The recipe taught everywhere is: private fields, and a getX() and a setX() for each. The IDE even generates them on its own. And the result is this:

public class Employee {
    private String name;
    private int    totalLoans;

    public String getName()              { return name; }
    public void   setName(String n)      { this.name = n; }
    public int    getTotalLoans()        { return totalLoans; }
    public void   setTotalLoans(int t)   { this.totalLoans = t; }
}

Ask yourself what has been gained over public fields. The honest answer: almost nothing.

employee.setTotalLoans(-7);      // still possible
employee.setTotalLoans(9999);    // skips the limit of 3
employee.setName(null);          // goodbye to the constructor's validation

A setter without validation is a public field with two extra lines. Worse: it gives the false impression of encapsulating.

The correct criterion is to ask, field by field, two things:

  1. Does anyone outside need to read this data? If not, there is no getter.
  2. Does it make sense for someone outside to change it to an arbitrary value? If not —and it almost never does—, there is no setter: there is a business operation.

Applied to BiblioTech:

Field Getter? Setter? Why
Book.title Yes No A book's title does not change
Book.isbn Yes No It is its identity
Material.available Yes, as isAvailable() No Changes only through lend() / returnItem()
Employee.name Yes No Identity data
Employee.totalLoans Yes No Changes through registerLoan() / registerReturn()
Loan.elapsedDays Yes Yes, validated It does evolve over time
Loan.fine Yes, computed Never It is a consequence, not a piece of data

That last row deserves a pause. If setFine(double) existed:

loan.setFine(0.0);       // I "forgive" a EUR 20 fine without anyone knowing
loan.setFine(1000.0);    // I charge a thousand euros for a three-day delay

Nexus Software's entire fine policy —daily rate, €20 cap, clamping to zero— would be voided by one line. The rule becomes a suggestion.

  1. Class invariants

An invariant is a condition that must hold always throughout the object's life, from the moment the constructor finishes until it is collected. They are the rules that define what it means for an object to be valid.

BiblioTech's invariants:

Class Invariant
Material title is never null or blank
Material reference is never null
Employee 0 <= totalLoans <= MAX_CONCURRENT_LOANS
Loan elapsedDays >= 0
Loan dueDay == loanDay + material.getLoanDays()
Loan 0 <= computed fine <= MAX_FINE

Encapsulation is the mechanism that enforces them, and it works on two fronts:

  • The constructor establishes the invariants at birth (03-04).
  • The methods are the only ones that can modify the state, and therefore the only ones responsible for maintaining them.

If any field is public, there are no invariants: there are only good intentions.

When a mutator really is needed, it must validate:

/**
 * Updates the days elapsed since the loan was made.
 * @param days number of days, never negative
 */
public void setElapsedDays(int days) {
    if (days < 0) {
        System.out.println("WARNING: negative days (" + days + "). The update is ignored.");
        return;                       // the invariant is preserved
    }
    this.elapsedDays = days;
}

As in 03-04: the correct way to reject would be to throw an exception, and that arrives in module 6.

  1. Business operations instead of setters

This is the lesson's key change of mindset. Instead of asking yourself "what fields does this class have and how do I expose them?", ask "what can happen to this object in the real business?".

In BiblioTech, very concrete things happen to a loan: its return is registered, it is extended, its status is queried. What does not happen to it is "being assigned a fine".

Before, with the mechanical recipe:

// The caller computes, decides and assigns: the rule lives OUTSIDE the object
int late = days - 15;
if (late < 0) late = 0;
double fine = late * 0.25;
if (fine > 20.0) fine = 20.0;

loan.setDaysLate(late);
loan.setFine(fine);
loan.setReturned(true);
book.setAvailable(true);
employee.setTotalLoans(employee.getTotalLoans() - 1);

Five calls that must all be made and in order. If the caller forgets one, the system is left inconsistent: a returned loan whose book is still marked as on loan.

After, with a business operation:

loan.registerReturn(days);

A single call, impossible to leave half-done:

/**
 * Registers the return of the material after the given number of days.
 * Calculates the fine according to the rules in force, releases the material
 * and discounts the loan from the employee.
 *
 * @param elapsedDays days since handover, never negative
 * @return amount of the applied fine, between 0 and MAX_FINE
 */
public double registerReturn(int elapsedDays) {

    if (returned) {
        System.out.println("WARNING: loan " + reference + " had already been returned.");
        return appliedFine;
    }

    setElapsedDays(elapsedDays);                  // validates the invariant

    this.appliedFine = material.calculateFine(this.elapsedDays);
    this.returned    = true;

    material.returnItem();                        // the material goes back on the shelf
    employee.registerReturn();                    // the employee frees up a slot

    return appliedFine;
}

Compare the two versions in a table:

Criterion Five setters registerReturn(int)
Where the fine rule lives In the caller (and in every caller) In Loan, only once
Can be left half-done Yes No
The fine can be faked Yes No
What you read in main Arithmetic A business sentence
Changing the rate affects Every caller One constant

That is the difference between an anaemic model (objects that only hold data, with the logic outside) and a rich domain model (objects that protect their rules). The first is OOP only in its syntax.

  1. Immutability

An immutable object is one whose state cannot change after construction. It is achieved with four rules:

  1. All fields private final.
  2. No setter and no method that modifies the state.
  3. The class declared final, so that no subclass adds mutability.
  4. If any field is a mutable reference, it is not exposed directly (section 8).

The advantages, and there are many:

Advantage Explanation
Impossible to corrupt the state There is no way to leave it invalid after the constructor
Thread-safe With no writes, there are no race conditions (module 8)
Can be shared fearlessly Copying and sharing become equivalent
Good candidate for a key Its hashCode never changes (lesson 03-09 and module 5)
Easier to reason about If you saw it valid once, it will always be

String is the canonical example, and you already suffered its consequences in module 1: text.toUpperCase() does not change text, it returns another string.

Why is Book a good candidate for immutability? Because its bibliographic data —title, author, ISBN, year— never changes. A book published in 2018 will not become a 2019 one, and its ISBN is its identity. The only thing that changes is availability, and that is a property of the lent copy, arguably even of another class.

A fully immutable Book would look like this:

public final class ImmutableBook {

    private final String title;
    private final String author;
    private final String isbn;
    private final int    publicationYear;

    public ImmutableBook(String title, String author, String isbn, int publicationYear) {
        this.title           = title;
        this.author          = author;
        this.isbn            = isbn;
        this.publicationYear = publicationYear;
    }

    public String getTitle()           { return title; }
    public String getAuthor()          { return author; }
    public String getIsbn()            { return isbn; }
    public int    getPublicationYear() { return publicationYear; }

    /** "Modifying" an immutable means creating another one. */
    public ImmutableBook withTitle(String newTitle) {
        return new ImmutableBook(newTitle, author, isbn, publicationYear);
    }
}

Look at withTitle: in the immutable world you do not modify, you derive a new object. It is the same pattern as String.toUpperCase().

Note on record. Writing an immutable class like this is so repetitive that Java 16 introduced records: public record ImmutableBook(String title, String author, String isbn, int publicationYear) { } automatically generates the private final fields, the constructor, the accessors, and also equals, hashCode and toString. They are studied in lesson 04-07. Write the manual version first: understanding what the record generates is more useful than using it blindly.

In BiblioTech we will keep available mutable —the copy is lent and returned— but everything else final. It is a partial immutability, and it is a reasonable compromise: it minimises what can change.

  1. The danger of returning mutable references: defensive copying

Here is the subtlest encapsulation leak and the one that ruins the most "apparently correct" code. Look:

public class Loan {
    private final String[] incidents;     // array: provisional solution (module 5)

    public String[] getIncidents() {
        return incidents;                 // LEAK!
    }
}

The field is private. The field is final. And even so:

String[] outside = loan.getIncidents();
outside[0] = "Made-up incident";           // the loan's interior has been modified

final protects the reference, not the object it points to (you saw it in 03-02). By returning the array, you have handed the outside world a key to your object's interior. The invariant is no longer guaranteed.

The solution is the defensive copy: return a copy, not the original.

/**
 * @return a copy of the recorded incidents; modifying it does not affect the loan
 */
public String[] getIncidents() {
    return java.util.Arrays.copyOf(incidents, incidents.length);
}

The same danger exists on the way in. If a constructor stores an array it is given, the caller keeps a reference to it:

// Naive constructor
public Loan(String[] incidents) {
    this.incidents = incidents;           // the caller can keep modifying it
}

// Constructor with a defensive copy
public Loan(String[] incidents) {
    this.incidents = (incidents == null)
                     ? new String[0]
                     : java.util.Arrays.copyOf(incidents, incidents.length);
}

General rule:

Copy defensively on receiving and on returning any mutable object that forms part of your class's state.

When is it unnecessary? When the object is immutable. String, Integer or an ImmutableBook can be returned directly with no risk, because nobody can alter them. It is another argument in favour of immutability: it eliminates a whole category of precautions.

And an important qualification: Loan.getMaterial() returns a mutable Material (its available changes). Should it be copied? No, and here is the nuance: the material is not an internal part of the loan, it is a shared entity of the catalog. Having two loans and the catalog see the same object is precisely what we want. The defensive copy applies to what is part of the object (composition), not to what is a collaborator (association). Go back to the relationships table from lesson 03-01: that distinction, which looked theoretical there, has a direct practical consequence here.

  1. Package-level encapsulation

Encapsulation does not stop at the class. Packages are the second barrier, and BiblioTech's current organisation already anticipates it:

com.nexussoftware.bibliotech
├── BiblioTechApp.java          startup and presentation
├── domain/
│   ├── Material.java           business rules
│   ├── Book.java
│   ├── Magazine.java
│   ├── Dvd.java
│   ├── Employee.java
│   └── Loan.java
└── service/                    (from module 5 onwards)
    └── LoanManager.java        use-case orchestration

The criterion: public only what another package genuinely needs. A helper class used only by the domain can be declared without public, and then it is invisible from outside. That gives you total freedom to change it, because you are certain nobody else uses it.

Java 9 took this idea one level higher with the module system and module-info.java, which lets you declare which packages an entire library exports. It is studied in lesson 10-06.

  1. The Law of Demeter

The Law of Demeter, or principle of least knowledge, boils down to one sentence:

Talk only to your immediate friends, not to your friends' friends.

In code: avoid chains of dots that traverse several objects.

// BAD: main knows the internal structure of Loan AND of Material
String title = loan.getMaterial().getTitle();
if (loan.getEmployee().getTotalLoans() >= 3) { ... }

// GOOD: each object answers for itself
String title = loan.getMaterialTitle();
if (loan.employeeAtLimit()) { ... }

Why does it matter? Because loan.getMaterial().getTitle() couples main to two classes: if Material replaces getTitle() with something else, every place that chained has to be touched. With loan.getMaterialTitle(), the change affects only Loan.

Two clarifications so as not to apply it as dogma:

  • Do not count dots: System.out.println(...) or sb.append(a).append(b) chain and are perfectly fine. What the law discourages is navigating someone else's structure in order to make decisions.
  • Taking it to the extreme generates piles of trivial delegating methods. Apply it where the coupling hurts: in business decisions.

  1. BiblioTech: the encapsulated domain

Let us refactor the project's classes. This is the state they end up in.

Material

package com.nexussoftware.bibliotech.domain;

/** Lendable material from Nexus Software's catalog. */
public class Material {

    public static final int    LOAN_DAYS       = 15;
    public static final double DAILY_RATE      = 0.25;
    public static final double MAX_FINE        = 20.0;
    public static final int    MINOR_THRESHOLD = 7;

    private static int materialsCreated = 0;

    private final String title;
    private final String reference;
    private boolean      available;

    public Material(String title, String reference, boolean available) {
        this.title     = (title == null || title.isBlank()) ? "Untitled" : title.trim();
        this.reference = (reference == null || reference.isBlank())
                         ? "000-0000000000" : reference.trim();
        this.available = available;
        materialsCreated++;
    }

    // --- Queries (getters only where they make sense) ---

    public String  getTitle()     { return title; }
    public String  getReference() { return reference; }
    public boolean isAvailable()  { return available; }

    public static int getMaterialsCreated() { return materialsCreated; }

    // --- Parameters each format redefines ---

    public int    getLoanDays()  { return LOAN_DAYS; }
    public double getDailyRate() { return DAILY_RATE; }
    public String getType()      { return "Material"; }

    // --- Business rules ---

    public int calculateDaysLate(int elapsedDays) {
        return Math.max(0, elapsedDays - getLoanDays());
    }

    public double calculateFine(int elapsedDays) {
        return Math.min(calculateDaysLate(elapsedDays) * getDailyRate(),
                        MAX_FINE);
    }

    public String classifySeverity(int elapsedDays) {
        int late = calculateDaysLate(elapsedDays);
        if (late == 0)               return "ON TIME";
        if (late <= MINOR_THRESHOLD) return "MINOR";
        return "SEVERE";
    }

    // --- Business operations (there is NO setAvailable) ---

    /** @return true if the material could be lent. */
    public boolean lend() {
        if (!available) {
            System.out.println("WARNING: '" + title + "' was already on loan.");
            return false;
        }
        available = false;
        return true;
    }

    /** @return true if the material could be returned. */
    public boolean returnItem() {
        if (available) {
            System.out.println("WARNING: '" + title + "' was already available.");
            return false;
        }
        available = true;
        return true;
    }

    public String describe() {
        return title + " (ref. " + reference + ")";
    }
}

Note the change in lend() and returnItem(): they now return boolean instead of void. That way the caller can know whether the operation had any effect, without having to read the field.

And note what is not there: setTitle, setReference or setAvailable. Availability changes only through the two business operations.

Book

package com.nexussoftware.bibliotech.domain;

/** Technical book from the catalog. Its bibliographic data is immutable. */
public class Book extends Material {

    private final String author;
    private final int    publicationYear;

    public Book(String title, String author, String isbn,
                int publicationYear, boolean available) {
        super(title, isbn, available);
        this.author = (author == null || author.isBlank()) ? "Unknown" : author.trim();
        this.publicationYear = (publicationYear < 1450 || publicationYear > 2100)
                               ? 0 : publicationYear;
    }

    public Book(String title, String author, String isbn, int publicationYear) {
        this(title, author, isbn, publicationYear, true);
    }

    public String getAuthor()          { return author; }
    public int    getPublicationYear() { return publicationYear; }

    /** The ISBN is a book's reference. */
    public String getIsbn() { return getReference(); }

    @Override public String getType() { return "Book"; }

    @Override
    public String describe() {
        return super.describe() + " - " + author + ", " + publicationYear;
    }
}

Notice that Book reaches the title through the inherited getTitle(), not directly: Material's field is private and not even its subclass can touch it. That would let Material change its internal representation tomorrow without breaking Book.

Employee

package com.nexussoftware.bibliotech.domain;

/** Nexus Software employee authorised to take materials on loan. */
public class Employee {

    public static final int MAX_CONCURRENT_LOANS = 3;

    private static int employeesRegistered = 0;

    private final String name;
    private final String identifier;
    private int          totalLoans;            // invariant: 0..MAX

    public Employee(String name, String identifier, int totalLoans) {
        this.name = (name == null || name.isBlank())
                    ? "Unnamed employee" : name.trim();
        this.identifier = (identifier == null || !identifier.startsWith("EMP-"))
                          ? "EMP-000" : identifier.trim();
        this.totalLoans = Math.max(0,
                Math.min(totalLoans, MAX_CONCURRENT_LOANS));
        employeesRegistered++;
    }

    public Employee(String name, String identifier) {
        this(name, identifier, 0);
    }

    public String getName()          { return name; }
    public String getIdentifier()    { return identifier; }
    public int    getTotalLoans()    { return totalLoans; }
    public boolean canBorrow()       { return totalLoans < MAX_CONCURRENT_LOANS; }

    public static int getEmployeesRegistered() { return employeesRegistered; }

    /** @return true if the loan could be registered. */
    public boolean registerLoan() {
        if (!canBorrow()) {
            System.out.printf("WARNING: %s already has %d loans (maximum %d).%n",
                              name, totalLoans, MAX_CONCURRENT_LOANS);
            return false;
        }
        totalLoans++;
        return true;
    }

    public void registerReturn() {
        totalLoans = Math.max(0, totalLoans - 1);
    }

    public String getInitials() {
        StringBuilder sb = new StringBuilder();
        for (String part : name.trim().split(" ")) {
            if (!part.isEmpty()) {
                sb.append(part.charAt(0)).append('.');
            }
        }
        return sb.toString().toUpperCase();
    }
}

The invariant 0 <= totalLoans <= MAX is guaranteed at the only three points where the field changes: the constructor (with Math.max/Math.min), registerLoan() (with the guard) and registerReturn() (with Math.max). There is no fourth door. That is encapsulation.

Loan

package com.nexussoftware.bibliotech.domain;

import java.util.Arrays;

/** Record of a loan of a material to an employee. */
public class Loan {

    private static int loansCreated = 0;

    private final Material material;
    private final Employee employee;
    private final int      loanDay;
    private final int      dueDay;
    private final String   reference;

    private int      elapsedDays;
    private boolean  returned;
    private double   appliedFine;
    private String[] incidents;           // provisional array (module 5)

    public Loan(Material material, Employee employee,
                int loanDay, int elapsedDays) {

        this.material = (material != null) ? material
                : new Book("Unknown material", "Unknown", "000-0000000000", 0, false);
        this.employee = (employee != null) ? employee
                : new Employee("Unknown employee", "EMP-000");

        this.loanDay     = Math.max(0, loanDay);
        this.dueDay      = this.loanDay + this.material.getLoanDays();
        this.elapsedDays = Math.max(0, elapsedDays);
        this.returned    = false;
        this.appliedFine = 0.0;
        this.incidents   = new String[0];

        loansCreated++;
        this.reference = String.format("LN-%04d", loansCreated);

        this.material.lend();
        this.employee.registerLoan();
    }

    public Loan(Material material, Employee employee, int loanDay) {
        this(material, employee, loanDay, 0);
    }

    // ---------- Queries ----------

    public String   getReference()      { return reference; }
    public Material getMaterial()       { return material; }     // shared collaborator
    public Employee getEmployeeDoNotUse() { return employee; }   // see note below
    public int      getLoanDay()        { return loanDay; }
    public int      getDueDay()         { return dueDay; }
    public int      getElapsedDays()    { return elapsedDays; }
    public boolean  isReturned()        { return returned; }

    /** Delegations that comply with the Law of Demeter. */
    public String getMaterialTitle() { return material.getTitle(); }
    public String getEmployeeName()  { return employee.getName(); }

    /** @return defensive copy: modifying it does not affect the loan. */
    public String[] getIncidents() {
        return Arrays.copyOf(incidents, incidents.length);
    }

    public static int getLoansCreated() { return loansCreated; }

    // ---------- Derived rules (computed, never stored) ----------

    public int    calculateDaysLate() { return material.calculateDaysLate(elapsedDays); }
    public double calculateFine()     { return material.calculateFine(elapsedDays); }
    public String classifySeverity()  { return material.classifySeverity(elapsedDays); }
    public boolean isOverdue()        { return calculateDaysLate() > 0; }
    public int     daysRemaining()    { return Math.max(0, dueDay - (loanDay + elapsedDays)); }

    // ---------- Business operations ----------

    /**
     * Updates the elapsed days. The only mutator, and validated.
     * @param days days since handover; negative values are ignored
     */
    public void setElapsedDays(int days) {
        if (days < 0) {
            System.out.println("WARNING: negative days in " + reference + ". Ignored.");
            return;
        }
        this.elapsedDays = days;
    }

    /**
     * Registers the return: computes and sets the fine according to the rules
     * in force, releases the material and discounts the loan from the employee.
     *
     * @param elapsedDays days since handover
     * @return applied fine, between 0 and MAX_FINE
     */
    public double registerReturn(int elapsedDays) {
        if (returned) {
            System.out.println("WARNING: " + reference + " had already been returned.");
            return appliedFine;
        }
        setElapsedDays(elapsedDays);
        this.appliedFine = calculateFine();
        this.returned    = true;
        material.returnItem();
        employee.registerReturn();
        return appliedFine;
    }

    /** @return the fine actually applied; 0 while it has not been returned. */
    public double getAppliedFine() { return appliedFine; }

    /** Records an incident of the loan (tears, loose pages...). */
    public void recordIncident(String text) {
        if (text == null || text.isBlank()) {
            return;
        }
        String[] extended = Arrays.copyOf(incidents, incidents.length + 1);
        extended[incidents.length] = text.trim();
        this.incidents = extended;        // module 5 will do this much better
    }

    public boolean employeeAtLimit() { return !employee.canBorrow(); }
}

About getEmployeeDoNotUse. That deliberately ugly name flags a design decision: exposing the whole Employee lets anyone do loan.getEmployee().registerLoan() and throw the system out of balance. That is why the loan offers getEmployeeName() and employeeAtLimit() instead. In lesson 03-08 you will formally reduce this class's public surface, and that getter will be one of the first to go.

Use from BiblioTechApp

Book     effectiveJava = new Book("Effective Java", "Joshua Bloch", "978-0000000001", 2018);
Employee marta         = new Employee("Marta Ruiz", "EMP-001");

Loan l = new Loan(effectiveJava, marta, 100, 0);

System.out.printf("%s: '%s' to %s, due on day %d%n",
                  l.getReference(), l.getMaterialTitle(),
                  l.getEmployeeName(), l.getDueDay());
System.out.println("Available after lending: " + effectiveJava.isAvailable());

l.recordIncident("Page 42 underlined");

double fine = l.registerReturn(20);
System.out.printf("Returned %d days late. Fine: %.2f EUR (%s)%n",
                  l.calculateDaysLate(), fine, l.classifySeverity());
System.out.println("Available after returning: " + effectiveJava.isAvailable());
System.out.println("Marta's loans: " + marta.getTotalLoans());

// Attempts to break the invariants from outside:
// effectiveJava.available = true;          // DOES NOT COMPILE: private field
// l.appliedFine = 0.0;                     // DOES NOT COMPILE: private field
// marta.totalLoans = 99;                   // DOES NOT COMPILE: private field
l.setElapsedDays(-5);                       // warns and is ignored

Output:

LN-0001: 'Effective Java' to Marta Ruiz, due on day 115
Available after lending: false
Returned 5 days late. Fine: 1.25 EUR (MINOR)
Available after returning: true
Marta's loans: 0
WARNING: negative days in LN-0001. Ignored.

The three commented lines are what matters in this lesson: they no longer compile. Nexus Software's rules have stopped depending on the goodwill of whoever uses the classes.

Common Mistakes and Tips

  • Confusing encapsulation with "adding getters and setters". A public setter with no validation leaves the object as open as a public field. Encapsulating is deciding which operations make sense, not automating access.
  • Generating all the accessors with the IDE without thinking. The generator does not know which fields must be immutable. Generate and then delete whatever should not exist.
  • Returning an internal mutable reference. The mistake from section 8. final does not protect the object pointed to.
  • Using protected on fields "just in case a subclass needs it". It turns the internal representation into public API for every subclass. Prefer a protected method.
  • Storing derived data. If fine can be computed from the days, compute it. Duplicated data is data that can fall out of sync. (In Loan we store appliedFine only after the return, because then it is a recorded fact, not a live calculation.)
  • Long chains of getters. a.getB().getC().getD() couples your code to three classes. Delegate.
  • Making a domain helper class public. If only its own package uses it, leave it with no modifier.
  • Tip: write the class from the outside in. First decide which operations it will offer, then which fields it needs to fulfil them. The other way round you end up with getters for everything.
  • Tip: if a field does not change, make it final today. It is the cheapest way to document and guarantee an invariant.
  • Tip: try commenting out a getter. If the project still compiles, delete it. The safest public API is the smallest one.

Exercises

Exercise 1: encapsulate Reservation

Take the Reservation class from lesson 03-04 (room, requester, start hour, end hour, attendees, active, code) and encapsulate it properly:

  1. Make every field private, and final those that must not change.
  2. Decide, field by field, whether it needs a getter, justifying it in a comment.
  3. Replace any setter with business operations: cancel(), extendOneHour() and changeAttendees(int) (which must reject out-of-range values).
  4. List the class's invariants in Javadoc and check that no operation can break them.

Exercise 2: find and close an encapsulation leak

This code has three different encapsulation leaks. Identify them, explain how each one would be exploited from outside, and fix them:

public class EmployeeHistory {

    private final String   name;
    private final String[] loanReferences;
    private int            totalFines;

    public EmployeeHistory(String name, String[] loanReferences) {
        this.name = name;
        this.loanReferences = loanReferences;
    }

    public String[] getLoanReferences() {
        return loanReferences;
    }

    public void setTotalFines(int totalFines) {
        this.totalFines = totalFines;
    }
}

Exercise 3: from anaemic model to rich model

A colleague has written this logic in BiblioTechApp using setters. Refactor it by moving each rule to the domain class it belongs to, so that main is left with a single call. State which new methods appear and in which class.

// In main
if (marta.getTotalLoans() < 3 && effectiveJava.isAvailable()) {
    effectiveJava.setAvailable(false);
    marta.setTotalLoans(marta.getTotalLoans() + 1);
    Loan l = new Loan();
    l.setMaterial(effectiveJava);
    l.setEmployee(marta);
    l.setLoanDay(100);
    l.setDueDay(100 + 15);
    System.out.println("Loan registered");
} else {
    System.out.println("Cannot be lent");
}

Solutions

Solution 1

package com.nexussoftware.bibliotech.domain;

/**
 * Reservation of a Nexus Software meeting room.
 *
 * Invariants guaranteed throughout the object's life:
 *   - 0 <= startHour <= 23
 *   - startHour < endHour <= 23
 *   - attendees >= 1
 *   - code non-null and unique
 */
public class Reservation {

    private static int reservationsCreated = 0;

    private final String   room;          // no setter: changing room is another reservation
    private final Employee requester;     // no setter: the reservation's identity
    private final int      startHour;     // no setter: fixed when booking
    private final String   code;          // no setter: generated by the system

    private int     endHour;              // mutates only through extendOneHour()
    private int     attendees;            // mutates only through changeAttendees()
    private boolean active;               // mutates only through cancel()

    public Reservation(String room, Employee requester,
                       int startHour, int endHour, int attendees) {

        this.room = (room == null || room.isBlank()) ? "Unnamed room" : room.trim();
        this.requester = (requester != null) ? requester
                         : new Employee("Unknown employee", "EMP-000");

        this.startHour = (startHour < 0 || startHour > 23) ? 9 : startHour;

        if (endHour <= this.startHour || endHour > 23) {
            this.endHour = Math.min(23, this.startHour + 1);
        } else {
            this.endHour = endHour;
        }

        this.attendees = Math.max(1, attendees);
        this.active    = true;

        reservationsCreated++;
        this.code = String.format("RES-%04d", reservationsCreated);
    }

    // --- Queries ---
    public String  getCode()       { return code; }        // the user asks for it
    public String  getRoom()       { return room; }        // shown in listings
    public int     getStartHour()  { return startHour; }   // shown
    public int     getEndHour()    { return endHour; }     // shown
    public int     getAttendees()  { return attendees; }   // shown
    public boolean isActive()      { return active; }      // queried when listing
    public int     durationHours() { return endHour - startHour; }

    /** Delegation: stops the outside world navigating to the Employee. */
    public String getRequesterName() { return requester.getName(); }
    // There is NO getRequester(): it would expose a mutable domain object.

    // --- Business operations (no setters at all) ---

    /** @return true if the reservation has just been cancelled. */
    public boolean cancel() {
        if (!active) {
            System.out.println("WARNING: reservation " + code + " was already cancelled.");
            return false;
        }
        active = false;
        return true;
    }

    /** @return true if it could be extended without running past the day. */
    public boolean extendOneHour() {
        if (!active) {
            System.out.println("WARNING: a cancelled reservation cannot be extended.");
            return false;
        }
        if (endHour >= 23) {
            System.out.println("WARNING: reservation " + code + " already reaches the end of the day.");
            return false;
        }
        endHour++;                       // the invariant startHour < endHour holds
        return true;
    }

    /** @return true if the change of attendees has been applied. */
    public boolean changeAttendees(int newCount) {
        if (newCount < 1) {
            System.out.println("WARNING: invalid number of attendees (" + newCount + ").");
            return false;
        }
        attendees = newCount;
        return true;
    }
}

Justification of the key decisions: room, requester, startHour and code are final because changing them would turn the reservation into a different one; endHour, attendees and active do mutate, but only through operations that validate, so the three invariants in the Javadoc hold at all times. And there is no getRequester(): exposing the Employee would let anyone alter it from a reservation, which is exactly the coupling the Law of Demeter discourages.

Solution 2

Leak 1: the constructor stores the array it receives.

String[] refs = { "LN-0001", "LN-0002" };
EmployeeHistory h = new EmployeeHistory("Marta Ruiz", refs);
refs[0] = "FAKE REFERENCE";        // h's interior has been modified

The caller keeps a reference to the very array that is now the object's internal state.

Leak 2: the getter returns the internal array.

h.getLoanReferences()[1] = "ANOTHER FAKE";       // written inside h

Neither private nor final protects: final prevents reassigning the variable, not modifying the array.

Leak 3: setTotalFines allows any value.

h.setTotalFines(-500);    // negative total of fines: an impossible state

It is a setter with no validation over cumulative data that should only grow through business operations.

Corrected version:

package com.nexussoftware.bibliotech.domain;

import java.util.Arrays;

/**
 * Loan history of an employee.
 * Invariants: loanReferences is never null; totalFines >= 0.
 */
public class EmployeeHistory {

    private final String   name;
    private final String[] loanReferences;
    private int            totalFines;

    public EmployeeHistory(String name, String[] loanReferences) {
        this.name = (name == null || name.isBlank()) ? "Unknown" : name.trim();

        // DEFENSIVE COPY on the way in: the caller can no longer reach our state.
        this.loanReferences = (loanReferences == null)
                ? new String[0]
                : Arrays.copyOf(loanReferences, loanReferences.length);

        this.totalFines = 0;
    }

    public String getName()       { return name; }
    public int    getTotalFines() { return totalFines; }
    public int    getLoanCount()  { return loanReferences.length; }

    /** @return defensive copy; modifying it does not affect the history. */
    public String[] getLoanReferences() {
        return Arrays.copyOf(loanReferences, loanReferences.length);
    }

    /**
     * Adds a fine to the history. It replaces the setter: the total can
     * only grow, and only with valid amounts.
     *
     * @param amount positive amount in euros
     * @return true if it has been accumulated
     */
    public boolean addFine(int amount) {
        if (amount <= 0) {
            System.out.println("WARNING: invalid fine amount (" + amount + ").");
            return false;
        }
        totalFines += amount;
        return true;
    }
}

Check that the leaks are closed:

String[] refs = { "LN-0001", "LN-0002" };
EmployeeHistory h = new EmployeeHistory("Marta Ruiz", refs);

refs[0] = "FAKE REFERENCE";                     // no longer has any effect
h.getLoanReferences()[1] = "ANOTHER FAKE";      // modifies a copy
h.addFine(-500);                                // rejected with a warning

System.out.println(Arrays.toString(h.getLoanReferences()));  // [LN-0001, LN-0002]
System.out.println(h.getTotalFines());                       // 0

A performance note: copying on every call has a cost. When it matters, the alternatives are returning a read-only view (List.copyOf, module 5) or making the content immutable. The starting rule is still to copy: correctness first, optimisation afterwards and only with measurements.

Solution 3

The fragment checks preconditions, mutates three objects and computes the due day from outside. All of that belongs to the domain.

The complete rule "can this material be lent to this employee?" has two parts that already live in their classes (isAvailable() and canBorrow()), and there is no place to combine them. Since Loan is what relates them, a static pre-check method is added there:

In Loan:

/**
 * Says whether it is possible to register a loan of this material to this employee.
 * @return true if the material is available and the employee has not reached the limit
 */
public static boolean canBeLent(Material material, Employee employee) {
    return material != null && employee != null
           && material.isAvailable()
           && employee.canBorrow();
}

And the canonical constructor already does the rest —it marks the material, increments the employee's counter and computes the due day with loanDay + material.getLoanDays()—, so main ends up like this:

if (Loan.canBeLent(effectiveJava, marta)) {
    Loan l = new Loan(effectiveJava, marta, 100);
    System.out.println("Loan " + l.getReference() + " registered, due on day "
                       + l.getDueDay());
} else {
    System.out.println("Cannot be lent");
}

Comparison:

Before After
11 lines in main 1 check call + 1 constructor
The 15-day term written in main Supplied by the material (and it works with magazines and DVDs)
Three manual mutations that can be forgotten Done by the constructor, always
Public setAvailable Does not exist
If a new rule is added, you have to hunt for it in every main It is added in canBeLent

New methods: just one, Loan.canBeLent(Material, Employee). The rest already existed. And every setter from the original fragment disappears: setAvailable, setTotalLoans, setMaterial, setEmployee, setLoanDay and setDueDay. Six doors closed.

One detail worth discussing: canBeLent is static because it answers a question before the loan exists. It is one of the few occasions when a static method in a domain class is justified.

Conclusion

You have closed the door that had been open all module. You know that encapsulating is not adding accessors but hiding the representation and exposing only operations that preserve the rules; you know the four access modifiers and the complete visibility table, and you have the professional criterion of starting from private and raising visibility only with a reason. You know how to decide field by field whether it deserves a getter and, above all, why a setter for every field destroys what it was meant to protect: setFine would turn Nexus Software's fine policy into a suggestion. You have learned to formulate a class's invariants and to guarantee them by closing every door through which the state can change. You handle immutability —final fields, no setters, final class— and you understand why a Book's bibliographic data is its ideal case. And you know the subtlest leak of all: returning an internal mutable reference, with the defensive copy as the remedy on the way in and on the way out, and with the nuance that a shared collaborator such as Material is not copied. You close with package-level encapsulation and the Law of Demeter.

BiblioTech's domain can no longer be corrupted from outside: effectiveJava.available = true does not compile, and the three invariants of Employee, Loan and Material hold because there are only a few methods that can touch them.

But a new problem has appeared, and you saw it in getEmployeeDoNotUse: the public surface of your classes has grown out of control. Loan exposes more than twenty methods, some of which nobody should call. Encapsulation answers how to hide; what is left is answering what to expose. In the next lesson, Abstraction, you will learn to decide what goes into a class's public API and what does not, not to mix abstraction levels inside a single method, to document contracts with preconditions and postconditions, and to recognise both leaky abstractions and the opposite vice, abstracting what nobody asked for.

Java Programming Course

Module 1: Introduction to Java

Module 2: Control Flow

Module 3: Object-Oriented Programming

Module 4: Advanced Object-Oriented Programming

Module 5: Data Structures and Collections

Module 6: Exception Handling

Module 7: File Input/Output

Module 8: Multithreading and Concurrency

Module 9: Networking

Module 10: Advanced Topics

Module 11: Java Frameworks and Libraries

Module 12: Building Real-World Applications

© Copyright 2026. All rights reserved