How to encapsulate a class
- Make all attributes private.
- Provide getters (accessors) to read values.
- Provide setters (mutators) to change values, with validation.
public class BankAccount {
private String accountNo;
private double balance;
public BankAccount(String accountNo, double openingBalance) {
this.accountNo = accountNo;
setBalance(openingBalance);
}
// getters
public String getAccountNo() { return accountNo; }
public double getBalance() { return balance; }
// setter with validation
public void setBalance(double balance) {
if (balance >= 0) {
this.balance = balance;
} else {
System.out.println("Balance cannot be negative");
}
}
public void withdraw(double amount) {
if (amount > 0 && amount <= balance) {
balance -= amount;
} else {
System.out.println("Invalid or insufficient");
}
}
}Naming: getX() and setX(); for boolean values, isX().
No setAccountNo() method is provided, so the account number is read-only. Encapsulation lets you make an attribute read-only, write-only or validated.
Reading the BankAccount class
accountNoandbalanceareprivate, soacc.balance = -500;in another class does not compile.- The constructor calls
setBalance(openingBalance)instead of assigning the attribute directly. This means the same validation rule is used for the opening balance and for every later change. The rule is written once. withdraw()is a better design than a plain setter for a bank: it expresses a real action and checks two rules (the amount must be positive and not more than the balance). Good encapsulated classes offer meaningful operations, not just a getter and a setter for every attribute.
Ways to handle an invalid value
The example prints a message and keeps the old value. That is easy to understand, but the calling code never finds out that something went wrong. Larger programs usually either return a result that reports success, or throw an exception that the caller must deal with:
public boolean withdraw(double amount) { // report success
if (amount <= 0 || amount > balance) return false;
balance -= amount;
return true;
}
public void setMarks(int marks) { // refuse with an exception
if (marks < 0 || marks > 100)
throw new IllegalArgumentException("Marks must be 0 to 100");
this.marks = marks;
}Benefits
- Data protection: invalid values are rejected.
- Flexibility: the internal storage can change without breaking other code.
- Maintainability: rules are written in one place.
- Control: you decide what is read-only.
Encapsulation and data hiding
These two terms are often used as if they meant the same thing, but there is a small difference. Data hiding is making the data private. Encapsulation is the bigger idea: keeping the data and the methods that work on it together in one class, and allowing access only through those methods. Data hiding is the tool; encapsulation is the design.
Making an immutable class
An object whose state can never change after it is created is called immutable. To make one: make every attribute private final, set them all in the constructor, provide getters only, and mark the class final so no subclass can add setters.
public final class Point {
private final int x, y;
public Point(int x, int y) { this.x = x; this.y = y; }
public int getX() { return x; }
public int getY() { return y; }
}Immutable objects are simple and safe to share, because nobody can change them. String is the best-known immutable class in Java: methods such as toUpperCase() return a new String and never change the original.
Common mistake: writing a getter that returns a private list or array directly, such as return books;. The caller receives a reference to your private list and can add or remove items without going through your methods. Return a copy instead, for example return new ArrayList<>(books);, or a read-only view.
Remember
- Private attributes, public methods, and validation inside the methods.
- Write each rule once and reuse it, as the constructor above does by calling the setter.
- No setter means read-only;
private finaland no setter means it can never change.