Command Palette

Search for a command to run...

PHASE 4Beginner ~29 min· topic 5 of 12

Topic 4.5

Encapsulation and Access Modifiers

In one line

Encapsulation means an object hides its data and lets the outside world change it only through methods that enforce the rules. Java's access modifiers (private, package-private, protected, public) decide exactly which code can see each class, field, method and constructor.

Think of it like this

An ATM. You can't open the cash box and grab notes. You insert a card, type a PIN, and ask for an amount; the machine checks your balance and the daily limit, then gives you money. The cash is hidden, and the buttons are the only way in. That's encapsulation: the data is private and every change goes through checks.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Encapsulation
Keeping an object's data hidden and allowing changes only through the object's own methods, which check the rules.
Access modifier
A keyword (private, protected, public) or its absence that controls which code can use a class or member.
Member
Anything declared inside a class: a field, method, constructor or nested class.
Package-private
The access you get when you write no modifier. Only code in the same package can use it.
Invariant
A rule that must be true for an object at all times, for example "balance is never negative".
Getter / setter
Methods that read (getX()) or change (setX(...)) a private field.
API
Application Programming Interface: the set of public things other code is allowed to use. Here, a class's public methods.
Defensive copy
A copy you make of a mutable object before storing or returning it, so outside code can't change your internals.

Step by step

01Why public fields go wrong

With long balance; visible to everyone, any line anywhere can write acc.balance = -5000;. Your withdraw method's overdraft check is useless, because callers can skip it.

When a bug sets a bad balance, you have to search the whole codebase for every assignment. With a private field, the only lines that can change it are inside Account: maybe ten lines to check.

02The four access levels

Read the table left to right: each level allows everything the one before it allows, and more. "Subclass in another package" applies only to inherited access through the subclass (Topic 5.2 has the exact protected rule).

Package-private has no keyword, which surprises people: forgetting a modifier doesn't mean public, it means "same package only".

access-levels.txtwhole filetext
                 same class   same package   subclass (other pkg)   everyone
private              yes           no               no                 no
(package-private)    yes           yes              no                 no
protected            yes           yes              yes                no
public               yes           yes              yes                yes

03Make the fields private, expose operations

private long balance; means only Account's own code can touch it. Outside code uses deposit, withdraw and getBalance.

Every path that changes the balance now runs a check. The constructor validates the opening balance, so the invariant "balance is never negative" is true from birth and every method keeps it true.

There's deliberately no setBalance. A bank doesn't let you set your balance; it lets you deposit and withdraw. Model real operations, not raw field writes.

Main.javawhole filejava
class Account {
    private final String owner;
    private long balance;

    Account(String owner, long opening) {
        if (opening < 0) throw new IllegalArgumentException("negative opening");
        this.owner = owner;
        this.balance = opening;
    }

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

    public boolean withdraw(long amount) {
        if (amount <= 0 || amount > balance) return false;
        balance -= amount;
        return true;
    }

    public long getBalance() { return balance; }
}

04The compiler enforces it

Try a.balance = 100; from Main. The compiler refuses, naming the member and the class that hides it. This check happens at compile time, and the JVM checks again when it links the class, so you can't sneak past it with hand-written bytecode either.

terminal
$ javac Main.java
── expected output ──
Main.java:4: error: balance has private access in Account
a.balance = 100;
^
1 error

05Private is per class, not per object

Inside Account, a method like boolean richerThan(Account other) { return balance > other.balance; } compiles fine: other.balance is private, but the code reading it is inside Account.

This is what makes equals (Topic 4.8) possible: it compares the private fields of two objects of the same class.

06Leaky getters: returning your insides

A Team keeps its scores in a private int[] scores. A getter return scores; returns a reference to that same array (Topic 3.3). The caller can now do team.getScores()[0] = 999; and change your private data without calling any of your methods.

The fix is a defensive copy: return scores.clone(); or Arrays.copyOf(scores, scores.length). Do the same in the constructor when you receive an array from outside. For collections, return List.copyOf(list) or an unmodifiable view (Topic 4.11 and Phase 9).

Leaky getters: returning your insidesdiagram
Rendering diagram…

07Access for classes and constructors

A top-level class is either public (usable from any package, and the file must have the same name) or package-private (an internal helper of its package).

Constructors have access modifiers too. A private constructor means nobody outside the class can call new. You'll use it for utility classes like Math (Topic 4.6), for singletons, and to force callers through a static factory method.

terminal
$ javac Main.java
── expected output ──
Main.java:4: error: modifier private not allowed here
private class Hidden {}
^
1 error

Try it yourself

  1. 1

    Try to break the invariant

    In the account example, add a.balance = -1000; to main. Read the error. Then remove private from the field and run again: the line compiles and the invariant is gone. Put private back.

  2. 2

    Fix the leak

    In the Team example, change leakyScores() to return a copy. Predict the new output line before running.

  3. 3

    Add a business operation

    Add public boolean transferTo(Account other, long amount) to Account that withdraws from this and deposits into other only if the withdrawal worked. Use it, and print both balances.

Code & diagrams

An account that can't be put in a bad state New tab
Sign in to run this example in your browser.

Expected output

withdraw 1000: false
withdraw 300:  true
Asha balance: 450
deposit -50 rejected: amount must be > 0
new account rejected: opening balance -1 < 0
Asha richer than Ben? false
A leaky getter vs a defensive copy New tab

The copy in the constructor blocked input[0] = -1, the clone blocked 777, but the leaky getter let 999 in.

Sign in to run this example in your browser.

Expected output

inside team: [10, 999, 30]
A temperature with a validating setter New tab
Sign in to run this example in your browser.

Expected output

start:  21.0
set 23.5: 23.5
set 80 rejected: 80.0 not in [10.0, 30.0]
still:  23.5

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Reach a private field from outside

From Main, write a.balance = 100; where balance is private in Account.

terminal
$ javac Main.java
── what you'll see ──
Main.java:4: error: balance has private access in Account
a.balance = 100;
^
1 error

Break #2

Make a top-level class private

Write private class Hidden {} at the top level of Main.java.

terminal
$ javac Main.java
── what you'll see ──
Main.java:4: error: modifier private not allowed here
private class Hidden {}
^
1 error

Myth vs fact

Myth

Encapsulation means adding a getter and setter for every field.

Fact

Unchecked getters and setters expose the field just as much as making it public. Encapsulation means exposing meaningful operations that protect the invariants.

Myth

No modifier means public.

Fact

No modifier means package-private: visible only inside the same package.

Myth

private protects an object from other objects of the same class.

Fact

Access is per class. Any Account method can read the private fields of any Account object.

Myth

private makes data secure.

Fact

Access modifiers protect design, not secrets. Reflection (setAccessible(true)) can read private fields on the class path, and anyone with the bytes can decompile them. Never rely on private to hide passwords.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Since Java 9, modules add a layer above packages: a public class in a package your module doesn't exports is invisible to other modules. Java 16 (JEP 396) and 17 (JEP 403) made JDK internals strongly encapsulated, which is why setAccessible(true) on java.lang internals now fails with InaccessibleObjectException unless you --add-opens (Phase 15).

  • ▸

    Nested classes can access each other's private members. Before Java 11, javac generated hidden access$000 bridge methods for this; Java 11 (JEP 181, nest-based access control) lets the JVM allow it directly through NestHost/NestMembers attributes.

  • ▸

    Accessors are free after JIT inlining, so performance is never a reason to make a field public. The real cost of public fields is lost flexibility: you can't add validation, change the representation, or make the value lazily computed later without breaking callers.

  • ▸

    Effective Java Item 15: minimise the accessibility of classes and members. Item 16: in public classes, use accessor methods, not public fields. Public static final fields are fine only for constants that are primitives or immutable objects.

Remember this

  1. 1

    Encapsulation is the habit of making fields private and exposing a small set of methods that keep the object valid. The rules the object must always obey ("balance is never negative") are called invariants, and only the class's own code can break them, because only it can touch the fields.

  2. 2

    Java has four access levels. private: only code inside the same top-level class. Package-private (no keyword at all): code in the same package. protected: same package, plus subclasses anywhere (Phase 5). public: everyone. Each step is wider than the one before.

  3. 3

    Access is checked per class, not per object. A method of Account can read other.balance of a different Account object, because the code is inside class Account. private protects against other classes, not against other instances.

  4. 4

    A top-level class can only be public or package-private. Writing private class Hidden {} at the top level gives modifier private not allowed here. Nested classes (Topic 5.13) can use all four.

  5. 5

    Getters and setters aren't automatically encapsulation. A setter that accepts anything is just a public field with extra typing. Good encapsulation exposes operations (deposit, withdraw) that check the rules, and getters only for data callers genuinely need. If a getter returns a mutable internal object (like an array), callers can change it behind your back: return a copy.

  6. 6

    Start with the narrowest access that works and widen only when you need to. Everything public becomes a promise to other code; changing it later can break them. A private field can be renamed or replaced tomorrow without anyone noticing.

Explain it without notes

01

What is encapsulation, and why is it more than adding getters and setters?

02

List the four access levels from narrowest to widest and say who can use each.

03

Why can one Account method read the private balance of a different Account object?

04

What is a leaky getter, and how do you fix it?

Practice

01

Write a class Counter with a private int count, a public increment(), a public getCount(), and a public reset(). Show that main can use the methods but not the field.

02

Write a class Password that stores a private String value, accepts it in the constructor only if it has at least 8 characters, and exposes matches(String attempt) but no getter. Show one accepted and one rejected password.

03

Write a class Playlist with a private String[] songs that copies the array in the constructor and returns a copy from getSongs(). Prove that changing the returned array doesn't change the playlist.

Trade-offs

  • ↔

    Narrow access protects invariants and keeps you free to change internals, but every operation callers need must be designed as a method. That's more up-front thinking, and it's exactly the thinking that prevents bugs.

  • ↔

    Defensive copies cost allocation and copying time on every call. For hot paths, return an unmodifiable view or use immutable types (Topic 4.11) so no copy is needed.

  • ↔

    Package-private is a useful middle ground for helpers shared inside a package, and it keeps them out of your public API. Overusing public "just in case" makes every class a promise you have to keep.

Done when you can

  • Done when you make fields private by default and can say why.

  • Done when you can recite who can see each of the four access levels.

  • Done when you design operations that protect invariants instead of blind setters.

  • Done when you can spot a leaky getter and fix it with a defensive copy.

  • Done when you know the allowed modifiers for top-level classes.