🏗️ Classes & Objects · Intermediate

Encapsulation in Java

Private fields, controlled access through methods, validating invariants.

🧩 The mysteryYour bank class carefully rejects negative deposits. Then a teammate writes acct.balance = -500;. All that careful checking — bypassed in one line.

A capsule with a control panel

Encapsulation: hide an object's fields and expose only methods that keep its data valid. Make fields private; let methods check every change, so rules (*invariants*) like "balance is never negative" always hold.

Guarding the balance

✗ Wide open
class Account {
    public int balance;
}
// anyone: acct.balance = -500;

Any code can skip your rules.

✓ Encapsulated
class Account {
    private int balance;
    void deposit(int amt) {
        if (amt <= 0)
            throw new IllegalArgumentException();
        balance += amt;
    }
    int getBalance() { return balance; }
}

Every change passes through a check.

🔮 Predict it

The guard at work

What happens when this runs?

class Thermostat {
    private int temp = 20;
    void set(int t) {
        if (t < 5 || t > 30)
            throw new IllegalArgumentException();
        temp = t;
    }
}
void main() { new Thermostat().set(99); }
  1. Nothing, temp stays 20
  2. temp becomes 99
  3. Throws IllegalArgumentException
Show the answer

It throws IllegalArgumentException. The setter rejects the invalid value immediately, so the object can never hold a nonsense temperature.

Freedom to change the inside

Callers only see methods, never fields. So you can switch private double price to private long priceCents and keep getPrice() returning the same value — no caller breaks. Encapsulation hides how the data is stored.

⚠️ The trap

The leaky getter

final only stops the field from pointing to a different list — the list itself is still mutable. Returning it hands callers a remote control to your data. Return List.copyOf(members) or Collections.unmodifiableList(members) instead.

class Team {
    private final List<String> members =
        new ArrayList<>();
    List<String> getMembers() {
        return members; // leak!
    }
}
🤔 Think first

Why not public and careful?

Why not skip all this and just ask everyone to be careful with public fields?

Think about it, then reveal the answer

Because "careful" doesn't scale. With a public field, any line anywhere can break the rule, you'd have to search the whole codebase to find who did, and you can never change the field's type without breaking every caller. Private fields make the class the single gatekeeper.

💼 In the real world

In real projects

Java's own String is the proof: in Java 9 its private storage switched from char[] to byte[] (Compact Strings) and not one program broke, because nobody could touch the field. That's the payoff of encapsulation at scale.

Key takeaways

  1. Fields private, behavior through methods
  2. Methods validate input and protect invariants
  3. Internals can change without breaking callers
  4. Don't leak references to mutable internal objects

💡 A bank teller: you can't reach into the vault, you ask the teller, who checks the rules first.

🤯 Did you know?

java.util.Date is a famous encapsulation headache: it's mutable, so a getter returning your Date field lets callers change it. That's one reason the java.time API (Java 8) made every type immutable.

Practice questions

What happens when this runs?

class Person {
    private int age;
    void setAge(int a) {
        if (a < 0)
            throw new IllegalArgumentException();
        age = a;
    }
}
void main() { new Person().setAge(-1); }
  1. Nothing, age stays 0
  2. Throws IllegalArgumentException
  3. Compile error
  4. age becomes -1
Check your answer

Throws IllegalArgumentException. The setter guards the invariant: a negative age is rejected immediately, so the object can never hold an invalid value.

You change Product's private double price into long priceCents, and keep getPrice() returning the same value. Why does no caller break?

  1. Callers only used getPrice(), never the field itself
  2. The compiler converts double to long everywhere
  3. private fields are copied into callers at compile time
  4. Java ignores field types at runtime
Check your answer

Callers only used getPrice(), never the field itself. Because the field was hidden, the method is the only contract. You can change the internal representation freely as long as the method still behaves the same.

Next: one field shared by every object at once — static.