Encapsulation keeps a Java object's rules together with its state. Private fields are a useful starting point, but the real goal is an interface that prevents invalid changes. This lesson covers validation, read-only access and defensive copying, including the difference between hiding a field and protecting its contents.
Review the related foundation lesson when you need a prerequisite. The complete programs below use Java 17-compatible syntax.
Protecting valid state
Encapsulation groups related data and operations while controlling access to internal state. It protects the rules that make an object valid.
A private balance field blocks unrelated code from assigning any value directly. Methods such as deposit can describe valid operations, reject invalid amounts and update the balance in one place. A public setter that accepts every value can undermine the same rule even when the field is private. Design methods around actions the object permits, rather than automatically creating getters and setters for every field.
Validated operations instead of unrestricted setters
Private fields are useful, but unrestricted setters can still expose every state change. Provide operations such as bookSeat when that better represents the domain.
Validate before mutating. If a method rejects a negative quantity after adding it, the object can remain corrupted even though an exception was thrown. A getter should report the existing state without changing it unexpectedly. For money, production code normally needs a deliberately chosen representation such as integer minor units or BigDecimal, rather than assuming binary floating-point is exact for decimal amounts.
Validation and defensive construction
Validate changes before committing them. A failed booking should leave the object in its previous valid state rather than partially updating several fields.
When a constructor accepts a mutable list, assigning that reference directly lets the caller change the object's state through the original list later. A defensive copy separates the stored collection structure from the supplied structure. This is usually a shallow copy: mutable elements are still shared. Choose an immutable element type or make additional copies when the elements themselves need protection.
Defensive collection access
Returning a mutable collection can leak internal state. Use a defensive copy or an appropriate unmodifiable view, and remember that shallow copies do not clone mutable elements.
Returning an internal mutable list exposes another route around validation. Return a copy or a suitable unmodifiable view, and understand the difference. A view can reflect later internal changes while refusing caller writes. List.copyOf creates an unmodifiable snapshot of the element references and rejects null elements. Neither option automatically deep-copies mutable objects inside the list.
Running the examples
Save name.java, compile it with javac --release 17 name.java, and run java name. Use each program's own public class name and filename.
How SessionEncapsulation produces its output
The Session constructor starts with two seats and rejects a negative initial count. book checks whether any seats remain before decrementing the private field. One successful booking changes two to one. remaining returns that stored count, so the output reports Seats remaining: 1. A later booking on an already full session would throw instead of making the count negative.
How ValidatedCounter produces its output
add checks its parameter before reaching the assignment. Passing minus two throws the exception, so count stays five. main catches the rejection and prints its message, then reads the unchanged field. This demonstrates a preserved invariant: accepted updates increase the count, and a rejected update does not partly change it.
How ProtectedRoster produces its output
The constructor snapshots the source element references with List.copyOf. Appending Omar to the original ArrayList afterward does not add it to the stored snapshot. String elements are immutable, which makes this particular shallow copy sufficient. If each element were a mutable Person, the same copying step would not prevent changes to those people's fields.
Preserve rules across repeated operations
A session with one seat can accept exactly one booking. After that successful call, another book request should reject the operation without decrementing below zero. Test this sequence rather than testing only a fresh session. The private field is not the whole protection: book must check the remaining value before changing it, and remaining must report the result without granting a writeable reference. If cancellation is added, specify whether it may exceed the original capacity. Store that capacity explicitly when needed. Each public operation must preserve the same model, including when the caller catches an exception and continues using the object.
Java Summary
- Encapsulation groups related data and operations while controlling access to internal state. It protects the rules that make an object valid.
- Private fields are useful, but unrestricted setters can still expose every state change. Provide operations such as bookSeat when that better represents the domain.
- Validate changes before committing them. A failed booking should leave the object in its previous valid state rather than partially updating several fields.
- Returning a mutable collection can leak internal state. Use a defensive copy or an appropriate unmodifiable view, and remember that shallow copies do not clone mutable elements.
- Continue with Java exercises, Java quizzes and the Java study roadmap.
Java Examples
Each example shows the code, the output it produces and what each part does.
Java Encapsulation and Validation in a complete program
public class SessionEncapsulation {
static class Session {
private int seats;
Session(int seats) {
if(seats < 0) throw new IllegalArgumentException();
this.seats=seats;
}
void book() {
if(seats==0) throw new IllegalStateException("Full");
seats--;
}
int remaining() {
return seats;
}
}
public static void main(String[] args) {
Session session = new Session(2);
session.book();
System.out.println("Seats remaining: " + session.remaining());
}
}Seats remaining: 1Line-by-line explanation
- Line 19Evaluates
"Seats remaining: " + session.remaining()and printsSeats remaining: 1.
Reject input before state changes
public class ValidatedCounter {
private int count = 5;
void add(int amount) {
if (amount < 0) throw new IllegalArgumentException("negative");
count += amount;
}
public static void main(String[] args) {
ValidatedCounter counter = new ValidatedCounter();
try {
counter.add(-2);
}
catch (IllegalArgumentException e) {
System.out.println(e.getMessage());
}
System.out.println(counter.count);
}
}negative
5Line-by-line explanation
- Line 13Evaluates
e.getMessage()and printsnegative. - Line 15Evaluates
counter.countand prints5.
Copy supplied collections
public class ProtectedRoster {
private final java.util.List<String> names;
ProtectedRoster(java.util.List<String> source) {
names = java.util.List.copyOf(source);
}
public static void main(String[] args) {
java.util.List<String> source = new java.util.ArrayList<>();
source.add("Lina");
ProtectedRoster roster = new ProtectedRoster(source);
source.add("Omar");
System.out.println(roster.names);
}
}[Lina]Line-by-line explanation
- Line 11Evaluates
roster.namesand prints[Lina].
Java FAQ
Is a private field enough for encapsulation?
No. Methods can still expose unsafe updates, and a getter can leak a mutable internal object. Review every public operation and returned reference against the object's rules. Encapsulation protects a contract, not merely a field declaration.
Should every field have a setter?
No. Some values are fixed at construction, some are derived and some should change only through validated operations. A seat reservation may expose reserve and cancel instead of a setter that can invent a negative remaining-seat count.
What is the difference between encapsulation and abstraction?
Encapsulation hides an object's internal data and protects its rules by controlling how the data can be changed. Abstraction hides complexity by exposing only the operations that matter, often through an interface or abstract class. They work together: an encapsulated class offers a small, abstract set of operations.
Do records provide encapsulation?
Partly. Record fields are private and final, so they can't be reassigned, and a compact constructor can validate values. A record with a mutable component, such as a List, can still leak changes, so copy it in the compact constructor with List.copyOf.
How does encapsulation help when code changes later?
Because other classes only use an object's public operations, you can change how the data is stored, add caching or tighten validation without breaking them. With public fields, every class that touches the field would have to change too.
Java Exercises
Answer each one and check it instantly. If you get stuck, open the hint before the answer.
Why copy a mutable constructor argument?
Which method order preserves a count on invalid input?
Java Quiz
2 questions to check what you learned.
What does List.copyOf protect in this roster?
- The snapshot's structure against caller mutation
- Every field of any possible element object
- The original ArrayList from all changes
Does a public unrestricted setter always preserve encapsulation?
- No, it can bypass the object's rules
- Yes, because the field remains private
- Only if the setter returns void