Classes And Objects
Encapsulation & Access Modifiers
Encapsulation hides an object's data behind a controlled interface. Fields go private, and only methods that can enforce invariants reach them. private, package-private, protected and public set visibility, getters and setters gate access, and final fields with no setters make an object immutable and safe to share. Every rule is shown as real output or a real compiler error.
Suggest an editEncapsulation & Access Modifiers — Controlling Access
A class that exposes its raw fields can't defend itself. Anyone can set balance to -999, and the class is powerless to object.
Encapsulation is the fix. Hide the data (private), and expose only methods that control how it changes. The class can then enforce its own rules, its invariants: facts about its fields that must hold after every operation.
The access modifiers private, protected and public, plus writing none at all (package-private), give you four levels of who can see each field and method. Push encapsulation to its limit, and you get immutable objects: data set once at construction and never changed. You can share such objects without the aliasing surprise from Classes & Objects.
💡 The core idea.
- Encapsulation hides data (
private) behind methods that control how it changes, so a class can enforce its invariants. - The access modifiers set four levels of who can see each field and method.
- Pushed to its limit it gives immutable objects — set once, never changed, safe to share.
This builds on classes and objects, especially the aliasing that immutability tames. Every output below was produced by compiling and running the code.
You'll be able to: predict whether code that reads a private field compiles, from the class it sits in; write a method that guards an invariant, and name the path that still bypasses the guard; pick the narrowest access level for a member, from who needs it; explain why final fields alone may not make a class immutable, and fix a leaked array with a copy.
📘 How to read the Intuition boxes. Each one is built in three moves:
- The mechanism — what the compiler and the JVM do.
- A concrete bite — a specific, runnable failure (often a real compiler error), shown so the trap is visible.
- The earned rule — the decision heuristic, now justified rather than asserted, plus its cost.
Table of contents
- Hiding data with
private - Getters, setters, and invariants
- The four access levels
- Immutability by design
- Mental-model summary
- Gotcha checklist
- Check yourself
- Sources
1. Hiding data with private
Marking a field private means only code inside its own class can touch it. Code anywhere else has to go through the methods the class chooses to expose — a getter to read it, for instance.
Output:
100Analysis. balance is private, so main can't read it directly. Instead, it asks through getBalance(), a method the Account class deliberately provides. The class now stands between its data and the outside world, and every access goes through code the class controls.
Intuition.
Mechanism. The compiler enforces private. A private member can be used only inside the class that declares it [1]. The compiler rejects any use from elsewhere before the program ever runs.
Concrete bite. Reach for the field directly, and it won't compile:
Compiler error:
Main.java:9: error: balance has private access in Account
System.out.println(acct.balance);
^
1 errorThe compiler rejects acct.balance because balance is private to Account, and main is outside that class. The wall is checked at compile time. It isn't a convention you can quietly ignore.
💡 Earned rule. Make fields private by default, and expose behavior, not data.
The cost is the extra code for accessor methods on the data you do want to share. The payoff: the ways to touch a field shrink from "everywhere" to "the few methods in this class". That's what makes the next section's invariants enforceable.
2. Getters, setters, and invariants
Routing access through methods pays off because a method can check. A setter (a method that changes a field) can reject or adjust bad input, so the object never ends up in an invalid state. A bare field can never do that. This book names a getter getX and a setter setX.
Output:
ignored invalid deposit: -30
150Analysis. deposit(50) passed the check and raised the balance to 150. The guard caught deposit(-30) and rejected it, leaving the balance untouched.
The method enforced the invariant "deposits must be positive". The class can guarantee it because balance is private, and deposit is the only way in. (Real code usually throws an exception here instead of printing. Exceptions shows how.)
Intuition.
Mechanism. A mutating method is a gate. It validates before changing state, so the invariants hold after every call. With a private field, the class's own methods are the only paths to it. Guard every one of them, and no path leads to an invalid value.
Concrete bite. A public field has no gate — it accepts anything:
Output:
-999With balance public, acct.balance = -999 succeeds. The class never gets a chance to object, and a "bank account" now holds -999. No method can enforce a rule, because the assignment skips methods entirely.
Non-example: a guard the constructor skips. The constructor is a way in too. Here deposit has a guard, but the constructor stores whatever it's given:
Output:
ignored invalid deposit: -30
-50The guard in deposit worked, yet the account is still invalid, because new Account(-50) never met it. Put the same check in the constructor. An invariant holds only if every way in checks it, construction included.
💡 Earned rule. Expose mutation through methods that validate, not through public fields, and validate in the constructor as well. Then the invariants get checked at the few places where state changes.
The cost is more code than a public field: a getter and a guarded setter in place of a bare variable. The payoff is that "an account can't break its rules" is enforced by construction, not by hoping every caller behaves.
3. The four access levels
A package is a named group of classes, such as java.util. A file with no package line, like every program in this book, belongs to the unnamed package [2]. Packages, Modules & the Build covers naming your own.
Every field, method, and constructor has one of four visibilities [1]. From most to least restrictive:
| Modifier | Visible to | Use for |
|---|---|---|
private |
code inside the same top-level class | internal state and helpers |
| (none): package-private | classes in the same package | package-internal collaboration |
protected |
the same package, and subclasses | members subclasses need (Inheritance & Polymorphism) |
public |
everyone (with modules, only if the package is exported) | the class's intended interface |
A member with no modifier is package-private. All the classes in one Run-button program share the unnamed package, so they see each other's package-private members.
private works per class, not per file. Another top-level class in the same file can't see a private member.
Compiler error:
Main.java:6: error: balance has private access in Account
int peek(Account a) { return a.balance; }
^
1 errorAnalysis. Auditor is a different class, so it can't read Account's private balance, even from the same file. Privacy is scoped to the declaring class, and sitting nearby in the file grants no access.
Intuition.
Mechanism. For each access, the compiler checks the member's modifier against where the access sits: the same class, the same package, or a subclass. protected adds subclass access, which only matters once classes inherit from each other.
Concrete bite. The check works per class, not per object. A method of Account may read the private field of another Account:
Output:
true
falseother.balance compiled, because the code sits inside Account. private protects a class's data from other classes, not one object from another of its kind.
A top-level class itself can be only public or package-private. private class Helper at the top of a file gives modifier private not allowed here [3].
💡 Earned rule. Default to private, and widen only as far as a real collaborator needs:
- package-private for same-package helpers;
protectedfor what subclasses must reach;publiconly for the deliberate interface.
Start narrow, and you may have to widen later. Start public, and every member becomes part of your contract. Once other code depends on it, you can't quietly take it back.
4. Immutability by design
The strongest form of encapsulation allows no changes at all. You can assign a final field exactly once, at its declaration or in the constructor, and never again. A class whose fields are all final, with no setters, has its state fixed for life. Its objects are immutable, as long as the fields hold primitives or other immutable objects (the non-example below shows why).
Output:
3,4Analysis. x and y are final, set once in the constructor. After that, you can read a Point but never change it. There's no setter, and the final fields can't be reassigned. The object is frozen the moment construction finishes.
Intuition.
Mechanism. final on a field is a compile-time guarantee of single assignment. The compiler checks that every constructor assigns the field [4], and it rejects any later assignment. Forget one, and the error lands on the constructor's closing brace:
Compiler error:
Main.java:6: error: variable y might not have been initialized
}
^
1 errorConcrete bite. Add a method that tries to change a final field, and it won't compile:
Compiler error:
Main.java:4: error: cannot assign a value to final variable x
void moveTo(int newX) { x = newX; }
^
1 errormoveTo tries to reassign x, but x is final and already assigned in the constructor, so the compiler refuses. To "move" an immutable point, you don't mutate it. You build a new Point.
Non-example: a final field that holds an array. final freezes the reference, not the object it points to [5]. This class has only private final fields and no setters, yet its data changes twice:
Output:
0
-1- The constructor stored the caller's array, so
input[0] = 0changed theScoresobject too. - The getter handed out the same array, so
s.getValues()[0] = -1changed it again.
values still points at the same array, just as final promised. But the array's contents changed underneath it. The fix is a defensive copy, made on the way in and on the way out:
Output:
90
90Now the object owns an array that no one else can reach, and the caller's changes land on copies.
💡 Earned rule. Make a class immutable whenever you can, especially for values you'll share:
private finalfields;- no setters;
- everything set in the constructor;
- a defensive copy of any mutable object the class takes in or hands out.
The cost is that "changing" an immutable object means creating a new one (more allocations), and deep updates get wordy. The payoff is that you can alias an immutable object freely. The aliasing surprise from Classes & Objects can't bite, because nothing can change behind your back. (Enums & Records shows record, which declares the private fields, the constructor and the accessor methods of such a class for you [6].)
5. Mental-model summary
| Principle | Consequence |
|---|---|
private hides a member from all code outside its top-level class |
acct.balance from elsewhere is a compile error; go through methods |
| Access is checked per class, not per object | An Account method may read other.balance |
| Mutating methods can validate; public fields cannot | A guarded deposit rejects -30; a public balance = -999 always succeeds |
| An invariant holds only if every way in checks it | A constructor that skips the check lets new Account(-50) through |
| Four access levels, narrowest first: private → package → protected → public | Default to private; a top-level class is only public or package-private |
final fields are assigned once, by every constructor |
A missed field is might not have been initialized; reassigning one won't compile |
final freezes the reference, not the object |
A final array can still change; copy it in and out |
| Immutable objects are safe to share | Aliasing can't surprise you when nothing can mutate |
6. Gotcha checklist
| Symptom | Likely cause | Fix |
|---|---|---|
balance has private access in Account |
a private member used from another class, even one in the same file |
call a method of the class instead |
modifier private not allowed here on a class |
a top-level class declared private |
leave it package-private, or make it public |
| An object is in an invalid state despite a guarded setter | the constructor (or another method) skips the check | validate on every path in, the constructor included |
| A field holds a value no method would allow | the field is public |
make it private; gate changes through a method |
| Other code depends on a field you meant to keep internal | it was public from the start |
start private and widen deliberately |
cannot assign a value to final variable x |
a final field changed after construction |
build a new object, or drop final if it must change |
variable y might not have been initialized at a constructor's } |
a final field that the constructor never assigns |
assign every final field in every constructor |
| An "immutable" object's data changed | a final field holds a mutable array or object that was shared |
copy it in the constructor and in the getter |
| A shared object changed unexpectedly | it is mutable and aliased | make it immutable so sharing is safe |
✅ Check yourself
One check per objective. Answer before you open anything.
A class has one field, private final int[] values, set from the constructor's argument, and a getter that returns it. Is the class immutable? What would you change?
No. final stops values from pointing at a different array, but the array's contents can still change. The caller kept its own reference to the array it passed in, and every caller of the getter receives the same array.
Copy the array in the constructor and in the getter, for example with Arrays.copyOf(values, values.length). The §4 proof shows the difference: 0 / -1 without the copies, 90 / 90 with them.
The 🧪 box below: withdraw(150), acct.balance -= 10, and an aliased mutable Point.
- With a guard that prints
refused:and the amount, the run printsrefused: 150, then100. The balance is untouched. - It doesn't compile:
balance has private access in Account, with the caret under the dot ofacct.balance. - A change made through one variable (say
p.x = 9) would show up through the other, where nobody expected it. Withfinalfields, no such change can be written.
📚 Sources
- The Java Language Specification, Java SE 25, §6.6.1 "Determining Accessibility" (
private: access "from within the body of the top level class") — https://docs.oracle.com/javase/specs/jls/se25/html/jls-6.html#jls-6.6.1 - The Java Language Specification, Java SE 25, §7.4.2 "Unnamed Packages" — https://docs.oracle.com/javase/specs/jls/se25/html/jls-7.html#jls-7.4.2
- The Java Language Specification, Java SE 25, §8.1.1 "Class Modifiers" ("The access modifiers protected and private pertain only to member classes") — https://docs.oracle.com/javase/specs/jls/se25/html/jls-8.html#jls-8.1.1
- The Java Language Specification, Java SE 25, §8.3.1.2 "
finalFields" (a blankfinalinstance variable "must be definitely assigned … at the end of every constructor") — https://docs.oracle.com/javase/specs/jls/se25/html/jls-8.html#jls-8.3.1.2 - The Java Language Specification, Java SE 25, §4.12.4 "
finalVariables" (the object's state "may be changed", but the variable "will always refer to the same object") — https://docs.oracle.com/javase/specs/jls/se25/html/jls-4.html#jls-4.12.4 - The Java Language Specification, Java SE 25, §8.10.1 "Record Components" (each component gives "a private field declared implicitly, and a public accessor method") — https://docs.oracle.com/javase/specs/jls/se25/html/jls-8.html#jls-8.10.1
🧪 Predict, then check.
- Give
Accountawithdraw(int amount)that refuses to let the balance go negative. Predict the output ofnew Account(100), thenwithdraw(150), thengetBalance(). - Predict the compiler's reaction to
acct.balance -= 10;inmainwhenbalanceisprivate. - If
Pointwere not immutable, and two variables aliased the samePoint, what could go wrong that immutability prevents?
Your Turn
Before you move on, check your understanding with the coach — explain the idea, apply it, weigh the trade-offs, then defend your reasoning.