Encapsulation

Encapsulation bundles an object's data with the code that guards it, so nothing outside the class can push it into an invalid state — it is the discipline access modifiers exist to enable.

Encapsulation is the practice of keeping an object's internal state private and exposing only a controlled set of methods to interact with it. Rather than letting arbitrary code reach in and change fields directly, an encapsulated class decides exactly how, and under what conditions, its state can change — protecting its own invariants.

Controlling Access to State

The core mechanism is simple: mark fields private so nothing outside the class can touch them directly, then provide public methods as the only doorway in or out.

No code outside Thermostat can ever assign an out-of-range value, because the only way in is setTargetTemp, and it always checks first.

public class Thermostat {
    private double targetTemp = 20.0; // hidden

    public double getTargetTemp() {   // controlled read
        return targetTemp;
    }

    public void setTargetTemp(double temp) { // controlled write
        if (temp < 5 || temp > 35) {
            throw new IllegalArgumentException("temp out of safe range: " + temp);
        }
        targetTemp = temp;
    }
}

Private Fields and Methods

Encapsulation is not limited to fields — internal helper methods that exist purely to support a class's own logic should also be private, so they remain free to change or be removed without affecting any other code.

sum() and applyRounding() are implementation details of how total() is computed; hiding them means Invoice can change how totals are calculated later without breaking any caller.

public class Invoice {
    private final List<Double> lineItems = new ArrayList<>();

    public void addLineItem(double amount) {
        lineItems.add(amount);
    }

    public double total() {
        return applyRounding(sum());
    }

    private double sum() { // internal helper, not part of the public API
        double s = 0;
        for (double v : lineItems) s += v;
        return s;
    }

    private double applyRounding(double v) { // internal helper
        return Math.round(v * 100) / 100.0;
    }
}

Invariants

An invariant is a condition that should always hold true for an object, for as long as it exists — e.g. "balance is never negative" or "endDate is never before startDate." Encapsulation is what makes invariants enforceable: if every path that changes state goes through the class's own methods, the class can check the invariant on every single change.

end.isBefore(start) can never become true from outside, because both places that change end — the constructor and extendTo — check it first.

public class DateRange {
    private final java.time.LocalDate start;
    private java.time.LocalDate end;

    public DateRange(java.time.LocalDate start, java.time.LocalDate end) {
        if (end.isBefore(start)) {
            throw new IllegalArgumentException("end before start");
        }
        this.start = start;
        this.end = end;
    }

    public void extendTo(java.time.LocalDate newEnd) {
        if (newEnd.isBefore(start)) {
            throw new IllegalArgumentException("end before start");
        }
        this.end = newEnd;
    }
}

Getters and Setters

Accessor methods (commonly named getX and setX) are the conventional way to expose a controlled view of private state. A getter does not have to return the field verbatim, and a setter does not have to accept every value — both are ordinary methods that happen to follow a naming convention.

Both setters validate before assigning; neither getter is a plain "expose the field" pass-through by requirement — it just happens to be simple here.

public class Product {
    private String name;
    private double price;

    public String getName() { return name; }

    public void setName(String name) {
        if (name == null || name.isBlank()) {
            throw new IllegalArgumentException("name required");
        }
        this.name = name;
    }

    public double getPrice() { return price; }

    public void setPrice(double price) {
        if (price < 0) {
            throw new IllegalArgumentException("price cannot be negative");
        }
        this.price = price;
    }
}

Validation in Setters

Putting validation logic inside setters (or any state-changing method) is what turns "private fields" into real protection. A private field with an unvalidated setter offers almost no more safety than a public field — the validation is the actual guard.

Removing this check would leave years technically private but effectively unguarded — any caller could still push the object into a nonsensical state through the setter.

public class Age {
    private int years;

    public void setYears(int years) {
        if (years < 0 || years > 150) {
            throw new IllegalArgumentException("implausible age: " + years);
        }
        this.years = years;
    }
}

Immutable Objects

The strongest form of encapsulation is not exposing a mutator at all: making fields final, assigning them only in the constructor, and providing no setters. Such an object's state can never change after construction, which eliminates entire categories of bugs related to unexpected mutation, especially in concurrent code.

final on a field means the reference itself cannot be reassigned — it does not automatically make the referenced object immutable. A final field holding a mutable object (like a List) can still have that object's contents changed unless the class also avoids exposing a mutable reference to it.

public final class Point {
    private final int x;
    private final int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public int getX() { return x; }
    public int getY() { return y; }

    public Point translated(int dx, int dy) {
        return new Point(x + dx, y + dy); // returns a NEW object; does not mutate this
    }
}

Encapsulation versus Data Hiding

The two terms are closely related but not identical. Data hiding refers specifically to restricting direct access to fields (typically via private). Encapsulation is the broader idea: bundling data together with the methods that operate on it, and exposing a controlled interface — data hiding is one technique encapsulation uses, but encapsulation also covers deciding what behavior to expose, enforcing invariants, and controlling how internal representation maps to a public API.

Data hiding:      "make the fields private" — a mechanism.
Encapsulation:    "bundle data + behavior, expose only a deliberate interface,
                    and let the class guard its own state" — the broader design principle.

Practical Usage

Encapsulation shows up in nearly every well-designed class: a Stack that only allows push/pop instead of direct array access, a BankAccount that only allows deposit/withdraw instead of direct balance assignment, a configuration object that validates every setting on the way in. The common thread is that the class — not its callers — is responsible for staying valid.

Best Practices

Favor immutability where practical. When mutability is needed, validate in every method that changes state, not just the constructor. Avoid returning direct references to internal mutable collections or objects — return a copy or an unmodifiable view instead, or the encapsulation is only superficial.

Returning members directly would let any caller do team.getMembers().add("intruder") and silently corrupt the Team's internal list; returning a copy prevents that entirely.

public class Team {
    private final List<String> members = new ArrayList<>();

    public List<String> getMembers() {
        return List.copyOf(members); // safe: caller cannot mutate Team's internal list
    }
}
// contrast with:  return members;  <-- leaks a mutable reference, breaking encapsulation

Complete Example

balance can only ever change through deposit or withdraw, and both enforce the invariant that balance never becomes negative — the class guarantees this for every BankAccount that exists.

public class BankAccount {
    private final String owner;
    private double balance;

    public BankAccount(String owner, double openingBalance) {
        if (openingBalance < 0) throw new IllegalArgumentException("negative opening balance");
        this.owner = owner;
        this.balance = openingBalance;
    }

    public double getBalance() { return balance; }

    public void deposit(double amount) {
        if (amount <= 0) throw new IllegalArgumentException("deposit must be positive");
        balance += amount;
    }

    public void withdraw(double amount) {
        if (amount <= 0) throw new IllegalArgumentException("withdrawal must be positive");
        if (amount > balance) throw new IllegalStateException("insufficient funds");
        balance -= amount;
    }
}

Mental Model

Think of a well-encapsulated object as a small machine with a locked case: outside code can only interact with it through the labeled buttons on the front (its public methods) — it can never reach inside and rewire the internals directly. The class decides what each button does and refuses to do anything that would break the machine.

Quick Reference

Practice                                   Purpose
-----------------------------------------  --------------------------------------------
private fields                              prevent uncontrolled direct access
public getters/setters (with validation)    provide a controlled, checked interface
final fields, no setters                    immutability — state fixed after construction
return copies of mutable internals          prevent callers from mutating hidden state
validate on every state-changing method     enforce invariants consistently, not just once

Key Takeaways

Encapsulation bundles data with the methods that operate on it, exposing only a controlled interface.

Private fields plus validated methods let a class enforce its own invariants everywhere state can change.

Data hiding (private fields) is one mechanism encapsulation uses, not a synonym for the whole principle.

Immutable objects (final fields, no mutators) are the strongest form of encapsulation.

A private field with an unvalidated setter, or a getter that leaks a mutable reference, only superficially encapsulates state.

public class Account { private double balance; public void setBalance(double balance) { this.balance = balance; // no validation at all } } account.setBalance(-1000); // silently accepted public class Account { private double balance; public void setBalance(double balance) { if (balance < 0) throw new IllegalArgumentException("balance cannot be negative"); this.balance = balance; } }

A private field with an unvalidated setter provides almost no protection — the validation logic, not the private keyword alone, is what actually enforces the invariant.

public class Team { private List<String> members = new ArrayList<>(); public List<String> getMembers() { return members; // leaks the internal list } } team.getMembers().clear(); // silently wipes Team's internal state from outside public class Team { private List<String> members = new ArrayList<>(); public List<String> getMembers() { return List.copyOf(members); } }

Returning a direct reference to a mutable internal collection lets outside code mutate it without going through any of Team's own methods, defeating the point of hiding it.

public class Config { public final Map<String, String> settings = new HashMap<>(); // "final" gives a false sense that settings is protected } config.settings.put("debug", "true"); // still fully mutable from outside public class Config { private final Map<String, String> settings = new HashMap<>(); public String get(String key) { return settings.get(key); } public void set(String key, String value) { settings.put(key, value); } } final only prevents reassigning the settings reference itself — it does nothing to stop the map's contents from being changed. Making the field private and exposing controlled methods is what actually protects it.

public class Person { private String name; // getter/setter pair for every field, with no validation, no logic — // this is just a public field with extra syntax public String getName() { return name; } public void setName(String name) { this.name = name; } } public class Person { private String name; public String getName() { return name; } public void setName(String name) { if (name == null || name.isBlank()) throw new IllegalArgumentException("name required"); this.name = name; } }

A getter/setter pair with no validation or logic offers the same lack of protection as a public field — encapsulation is about the checks and control, not the presence of accessor methods for their own sake.

What is the difference between "data hiding" and "encapsulation"?

Data hiding is the mechanism of restricting direct access to fields (typically via private). Encapsulation is the broader principle of bundling data with the methods that operate on it and exposing only a deliberate, controlled interface — data hiding is one tool encapsulation uses.

Why does marking a field private not by itself guarantee an object's state stays valid?

Private only prevents direct outside access — if the class's own setter accepts any value with no validation, the object can still be pushed into an invalid state, just through the setter instead of a direct field write.

Why does returning members directly from a getter break encapsulation, even if the field itself is private?

If the returned reference points to the same mutable object the class stores internally, callers can mutate that object's contents (e.g. list.add(...)) without ever going through any of the class's own methods, bypassing whatever validation those methods would have applied.

Does final on a field make the object it references immutable?

No — final only prevents the field from being reassigned to point at a different object. If the referenced object (e.g. a List or a mutable custom class) has its own mutating methods, those can still be called; final says nothing about the object's own mutability.

Why are immutable objects considered a strong form of encapsulation?

Because there is no mutator to call at all — once constructed (validated once, in the constructor), the object's state can never change again, which removes any need to guard against invalid later mutations.

Give an example of an invariant, and explain how encapsulation makes it enforceable.

An invariant like "balance is never negative" is enforceable because every method that can change balance (deposit, withdraw) is inside the class and can check the condition before applying the change — since no outside code has any other path to change balance, the check can never be bypassed.

Interview-style: why is "just add getters and setters for every field" not the same thing as encapsulation?

Mechanically adding an unvalidated getter/setter pair for every private field reproduces the same lack of protection as public fields, just with extra method-call syntax. Real encapsulation requires deciding which state should be exposed at all, and enforcing rules (validation, invariants) in whatever methods do expose or change it.

Write an immutable Money class with fields for amount and currency (both final), a constructor that validates amount is not negative, and a method add(Money other) that returns a new Money (same currency required).

public final class Money { private final long cents; private final String currency; public Money(long cents, String currency) { if (cents < 0) throw new IllegalArgumentException("amount cannot be negative"); this.cents = cents; this.currency = currency; } public Money add(Money other) { if (!this.currency.equals(other.currency)) { throw new IllegalArgumentException("currency mismatch"); } return new Money(this.cents + other.cents, this.currency); } public long getCents() { return cents; } public String getCurrency() { return currency; } }

Both fields are final and set only in the constructor, so no Money object ever changes after creation; add() returns a brand-new Money rather than mutating either operand, which is the standard immutable-update pattern.

What is printed, and what encapsulation problem does this reveal?

Prints 1. getCards() returns the actual internal list, not a copy, so calling .add(...) on the returned reference mutates Wallet's private state directly from outside the class — the private modifier on cards provided no real protection here.

Write a Rectangle class with private width/height fields, validated setters that reject non-positive values, and a method area() computed from the current state.

public class Rectangle { private double width; private double height; public Rectangle(double width, double height) { setWidth(width); setHeight(height); } public void setWidth(double width) { if (width <= 0) throw new IllegalArgumentException("width must be positive"); this.width = width; } public void setHeight(double height) { if (height <= 0) throw new IllegalArgumentException("height must be positive"); this.height = height; } public double area() { return width * height; } }

The constructor delegates to the validated setters rather than assigning fields directly, so the same positive-value invariant is enforced whether the dimensions are set at construction time or changed later.

What does this print, and how does the constructor keep the range valid even though (10, 3) is out of order?

Prints "true false". The constructor normalizes min/max using Math.min/Math.max instead of assigning the arguments verbatim, so the invariant min <= max is enforced even for out-of-order input — this is encapsulation actively protecting against a bad constructor call rather than merely rejecting it.

class Wallet {
    private List<String> cards = new ArrayList<>();
    List<String> getCards() { return cards; }
}
public class Main {
    public static void main(String[] args) {
        Wallet w = new Wallet();
        w.getCards().add("visa-ending-1234");
        System.out.println(w.getCards().size());
    }
}
public class Range {
    private final int min;
    private final int max;
    public Range(int min, int max) {
        this.min = Math.min(min, max);
        this.max = Math.max(min, max);
    }
    public boolean contains(int v) {
        return v >= min && v <= max;
    }
}
Range r = new Range(10, 3);
System.out.println(r.contains(5) + " " + r.contains(11));