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".
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 yes03Make 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.
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.
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).
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.
Try it yourself
- 1
Try to break the invariant
In the account example, add
a.balance = -1000;tomain. Read the error. Then removeprivatefrom the field and run again: the line compiles and the invariant is gone. Putprivateback. - 2
Fix the leak
In the Team example, change
leakyScores()to return a copy. Predict the new output line before running. - 3
Add a business operation
Add
public boolean transferTo(Account other, long amount)toAccountthat withdraws fromthisand deposits intootheronly if the withdrawal worked. Use it, and print both balances.
Code & diagrams
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? falseThe copy in the constructor blocked input[0] = -1, the clone blocked 777, but the leaky getter let 999 in.
Expected output
inside team: [10, 999, 30]Expected output
start: 21.0
set 23.5: 23.5
set 80 rejected: 80.0 not in [10.0, 30.0]
still: 23.5Break 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.
Break #2
Make a top-level class private
Write private class Hidden {} at the top level of Main.java.
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
publicclass in a package your module doesn'texportsis invisible to other modules. Java 16 (JEP 396) and 17 (JEP 403) made JDK internals strongly encapsulated, which is whysetAccessible(true)onjava.langinternals now fails withInaccessibleObjectExceptionunless you--add-opens(Phase 15). - ▸
Nested classes can access each other's private members. Before Java 11, javac generated hidden
access$000bridge methods for this; Java 11 (JEP 181, nest-based access control) lets the JVM allow it directly throughNestHost/NestMembersattributes. - ▸
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 finalfields are fine only for constants that are primitives or immutable objects.
Remember this
- 1
Encapsulation is the habit of making fields
privateand 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
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
Access is checked per class, not per object. A method of
Accountcan readother.balanceof a differentAccountobject, because the code is inside classAccount.privateprotects against other classes, not against other instances. - 4
A top-level class can only be
publicor package-private. Writingprivate class Hidden {}at the top level givesmodifier private not allowed here. Nested classes (Topic 5.13) can use all four. - 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
Start with the narrowest access that works and widen only when you need to. Everything
publicbecomes a promise to other code; changing it later can break them. Aprivatefield can be renamed or replaced tomorrow without anyone noticing.
Explain it without notes
What is encapsulation, and why is it more than adding getters and setters?
List the four access levels from narrowest to widest and say who can use each.
Why can one Account method read the private balance of a different Account object?
What is a leaky getter, and how do you fix it?
Practice
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.
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.
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
privateby 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.