Encapsulation
Encapsulation is the OOP principle of bundling data (fields) and the methods that operate on that data into a single unit — the class — and then controlling who can see or modify that data. Think of it as a protective capsule: the internals are hidden, and the outside world interacts only through a well-defined interface you control.
Why Encapsulation Matters
Imagine a bank account class where the balance field is public. Any code anywhere in your program could write account.balance = -999999; — a disaster. Encapsulation prevents this by making fields private and exposing only safe, validated access through methods.
The benefits are real and practical:
- Data integrity — you validate input in one place (the setter), not scattered everywhere.
- Flexibility — change the internal representation without breaking any callers.
- Maintainability — a clear API boundary makes code easier to read, test, and evolve.
- Security — sensitive state is hidden from accidental or malicious misuse.
The Core Pattern: Private Fields + Public Getters/Setters
The most common encapsulation pattern in Java is straightforward:
- Declare fields
private. - Provide
publicgetter methods to read values. - Provide
publicsetter methods to write values (with validation if needed).
public class BankAccount {
private String owner;
private double balance;
public BankAccount(String owner, double initialBalance) {
this.owner = owner;
if (initialBalance < 0) {
throw new IllegalArgumentException("Initial balance cannot be negative.");
}
this.balance = initialBalance;
}
// Getter — read-only access
public double getBalance() {
return balance;
}
public String getOwner() {
return owner;
}
// Business method — validates before modifying state
public void deposit(double amount) {
if (amount <= 0) {
throw new IllegalArgumentException("Deposit amount must be positive.");
}
balance += amount;
}
public void withdraw(double amount) {
if (amount <= 0 || amount > balance) {
throw new IllegalArgumentException("Invalid withdrawal amount.");
}
balance -= amount;
}
}
public class Main {
public static void main(String[] args) {
BankAccount account = new BankAccount("Alice", 500.0);
account.deposit(200.0);
account.withdraw(100.0);
System.out.println(account.getOwner() + "'s balance: " + account.getBalance());
// account.balance = -999; // Compile error! Field is private.
}
}
Output:
Alice's balance: 600.0
Notice there is no setter for balance — callers must go through deposit() and withdraw(), which enforce the rules. This is intentional design.
Tip: Not every field needs a setter. If a value should only be set at construction time, leave the setter out entirely. Immutability is a natural extension of encapsulation — see Create an Immutable Class.
Read-Only and Write-Only Fields
Encapsulation gives you fine-grained control over field accessibility:
| Pattern | Getter | Setter | Use Case |
|---|---|---|---|
| Read-Write | Yes | Yes | Mutable, validated state |
| Read-Only | Yes | No | State set at construction, never changed |
| Write-Only | No | Yes | Passwords, secrets — set but never read back |
| Fully Hidden | No | No | Internal implementation detail |
public class User {
private final String username; // read-only after construction
private String passwordHash; // write-only (never expose the hash)
public User(String username, String password) {
this.username = username;
this.passwordHash = hash(password);
}
public String getUsername() {
return username; // getter only
}
public void setPassword(String newPassword) {
this.passwordHash = hash(newPassword); // setter only — no getter
}
private String hash(String password) {
// Simplified — use BCrypt in production!
return Integer.toHexString(password.hashCode());
}
}
Encapsulation vs Abstraction
These two principles often get confused. Here is the distinction:
- Encapsulation hides data and controls access to the internal state of an object.
- Abstraction hides implementation logic behind abstract types and interfaces.
You almost always use them together. A class can encapsulate its fields (private balance) while also participating in an abstraction (implementing an Account interface). See Abstraction for the other side of this coin.
JavaBeans Convention
Java has a widely-followed naming convention for getters and setters called the JavaBeans standard. It matters because frameworks like Spring, Hibernate, and Jackson rely on it for automatic property binding:
- Getter for
balance→getBalance() - Setter for
balance→setBalance(double value) - Getter for a
boolean active→isActive()(useis, notget) - Setter for
boolean active→setActive(boolean value)
public class Product {
private String name;
private double price;
private boolean available;
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public double getPrice() { return price; }
public void setPrice(double price) {
if (price < 0) throw new IllegalArgumentException("Price cannot be negative.");
this.price = price;
}
public boolean isAvailable() { return available; }
public void setAvailable(boolean available){ this.available = available; }
}
Note: Modern Java (Java 16+) offers Records as a concise alternative for simple, immutable data carriers — they auto-generate accessors (without the
getprefix),equals(),hashCode(), andtoString().
Under the Hood
From the JVM’s perspective, private is enforced at compile time by javac. The bytecode itself contains access flags on each field and method. When the class loader links classes together, the JVM verifier checks these flags and throws IllegalAccessError at runtime if another class tries to directly access a private member without going through reflection.
This means:
- Accessing a private field through a getter has a thin overhead (one extra method call). HotSpot’s JIT compiler almost always inlines trivial getters at runtime, making the final machine code identical to a direct field read.
- Marking fields
private finaladditionally tells the JVM they will never change after construction, enabling further optimizations and allowing safe publication in multithreaded code without explicit synchronization (thanks to the Java Memory Model).
Tip: Prefer
private finalfields wherever possible. Immutable, encapsulated state is the safest kind — no locks needed, no surprise mutations.
Encapsulation Across Files: Packages and Access Modifiers
Encapsulation is not limited to a single class. Java scales the concept to the package level:
- A field or method with package-private access (no modifier) is visible only within the same package — useful for classes that collaborate closely but should not expose internals to the outside world.
- The
protectedmodifier extends visibility to subclasses, even across packages. publicopens the API to everyone.
The two sub-topics in this section go deeper into these tools.
In This Section
- Packages — Organize related classes into namespaces, control inter-package visibility, and use
importstatements to keep your code clean. - Access Modifiers — A deep dive into
private,protected,public, and package-private — when to use each and how they shape your API boundaries.
Related Topics
- Access Modifiers — The direct mechanism Java uses to enforce encapsulation boundaries.
- Packages — Package-level encapsulation for organizing and protecting groups of classes.
- Abstraction — The companion OOP principle that hides implementation logic rather than data.
- Classes & Objects — The fundamental building block that encapsulation lives inside.
- final Keyword — Combine
privatewithfinalfor truly immutable, safely encapsulated fields. - Records — Java 16+ concise syntax for immutable data classes that apply encapsulation by default.
- OOP Concepts — See how encapsulation fits alongside inheritance, polymorphism, and abstraction.