Encapsulation and Access Modifiers

Hide state behind methods that enforce the rules, choose the narrowest access level that works, and stop getters from leaking mutable internals.

Syntaxpublic class Name { private Type field; // hidden state public void action(...) { } // the only way in void helper() { } // package-private }

Encapsulation means the object, not its callers, decides how its state changes. Fields are private; methods are the only way in, and those methods enforce the rules. A balance that only moves through deposit and withdraw can never go negative by accident, however many handlers touch it.

The four access levels

ModifierVisible to
privateThe same top-level class only (including its nested classes)
none (package-private)Classes in the same package
protectedSame package, plus subclasses elsewhere
publicEveryone

Start with private and widen only when something needs it. Package-private is underused: helper classes that only one package needs can stay invisible to the rest of the service, and tests in the same package can still reach them. Everything public becomes something other code relies on and you cannot change freely.

Getters and setters are not encapsulation

A private field with a public getter and setter for it is a public field with extra steps. Prefer methods that describe what happens: order.ship() rather than order.setStatus("SHIPPED"). The method can check that the order was paid first and throw IllegalStateException if not, which a setter cannot.

Returning a mutable internal collection from a getter is the most common leak. A caller adds to the list you returned and has changed your object without going through any of your checks. Return List.copyOf(items) (a snapshot) or Collections.unmodifiableList(items) (a read-only view that still shows later changes), and copy lists you receive in a constructor for the same reason.

Private is per class, not per object

Code inside a class can read the private fields of any instance of that class, not just this. That is what lets equals or a transferTo(Account other) method work. It also means encapsulation protects against other classes, not against code in the same class.

In a web service, the boundary that matters most is between the HTTP layer and the domain. Handlers should call meaningful methods on domain objects, never reach in and set fields from request JSON directly.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Expose order.pay() and order.ship() instead of a status setter so an unpaid order can never ship.
  • Return an unmodifiable copy of a cart's line items so controllers cannot add items without validation.
  • Keep a package's helper classes package-private so only its public service class is used elsewhere.

More examples

A getter that leaks its list

The private field did not protect LeakyCart, because the getter handed out the list itself. List.copyOf returns an unmodifiable snapshot, so any validation in add() cannot be skipped.

Example · java
import java.util.ArrayList;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        LeakyCart leaky = new LeakyCart();
        leaky.add("mug");
        leaky.items().add("free-tv");   // bypasses add()
        IO.println("leaky: " + leaky.items());

        SafeCart safe = new SafeCart();
        safe.add("mug");
        try {
            safe.items().add("free-tv");
        } catch (UnsupportedOperationException e) {
            IO.println("safe refused the change");
        }
        IO.println("safe: " + safe.items());
    }
}

class LeakyCart {
    private final List<String> items = new ArrayList<>();
    void add(String sku) { items.add(sku); }
    List<String> items() { return items; }
}

class SafeCart {
    private final List<String> items = new ArrayList<>();
    void add(String sku) { items.add(sku); }
    List<String> items() { return List.copyOf(items); }
}
// Output:
// leaky: [mug, free-tv]
// safe refused the change
// safe: [mug]

Private access works across instances

transferTo reads and writes other.coins directly even though it is private. Access is checked per class, so any Wallet method can touch any Wallet's private fields.

Example · java
public class Main {
    public static void main(String[] args) {
        Wallet a = new Wallet(50);
        Wallet b = new Wallet(10);
        a.transferTo(b, 20);
        IO.println(a + " " + b);
    }
}

class Wallet {
    private int coins;

    Wallet(int coins) {
        this.coins = coins;
    }

    void transferTo(Wallet other, int amount) {
        if (amount <= 0) {
            throw new IllegalArgumentException("amount must be positive");
        }
        if (amount > coins) {
            throw new IllegalStateException("not enough coins");
        }
        coins -= amount;
        other.coins += amount;   // allowed: same class
    }

    @Override
    public String toString() {
        return "Wallet(" + coins + ")";
    }
}
// Output:
// Wallet(30) Wallet(30)

State transitions instead of setters

There is no setStatus, so no handler can mark an unpaid order as shipped. In a real service the status would be an enum, and the IllegalStateException would map to a 409 Conflict response.

Example · java
public class Main {
    public static void main(String[] args) {
        Order order = new Order("ord-9");
        try {
            order.ship();
        } catch (IllegalStateException e) {
            IO.println(e.getMessage());
        }
        order.pay();
        order.ship();
        IO.println(order.id() + " is " + order.status());
    }
}

class Order {
    private final String id;
    private String status = "NEW";

    Order(String id) {
        this.id = id;
    }

    void pay() {
        require("NEW");
        status = "PAID";
    }

    void ship() {
        require("PAID");
        status = "SHIPPED";
    }

    private void require(String expected) {
        if (!status.equals(expected)) {
            throw new IllegalStateException(id + " is " + status + ", expected " + expected);
        }
    }

    String id() { return id; }
    String status() { return status; }
}
// Output:
// ord-9 is NEW, expected PAID
// ord-9 is SHIPPED

Discussion

  • Be the first to comment on this lesson.