Encapsulation
Encapsulation means an object keeps its data to itself, and lets other code change it only through methods that enforce the rules.
The problem
class Account {
double balance;
}
public class Main {
public static void main(String[] args) {
Account acct = new Account();
acct.balance = -5000; // nothing stops this
System.out.println(acct.balance);
}
}Any code anywhere can put the account into an impossible state.
Private fields, public methods
class Account {
private double balance;
public void deposit(double amount) {
if (amount <= 0) throw new IllegalArgumentException("Deposit must be positive");
balance += amount;
}
public boolean withdraw(double amount) {
if (amount <= 0 || amount > balance) return false;
balance -= amount;
return true;
}
public double getBalance() {
return balance;
}
}
public class Main {
public static void main(String[] args) {
Account acct = new Account();
acct.deposit(1000);
System.out.println(acct.withdraw(300)); // true
System.out.println(acct.withdraw(5000)); // false: not enough money
System.out.println(acct.getBalance()); // 700.0
// acct.balance = -5000; // error: balance has private access
}
}Now the only ways to change the balance are deposit and withdraw, and both check their input. The rules live in one place and can't be skipped.
Access levels
| Modifier | Visible to |
|---|---|
private | code inside the same class only |
| (none) | classes in the same package |
protected | the same package, plus subclasses |
public | everyone |
A sensible default: make fields private, and make public only the methods other code genuinely needs.
Getters and setters
class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = checkName(name);
this.age = checkAge(age);
}
public String getName() { return name; }
public void setName(String name) { this.name = checkName(name); }
public int getAge() { return age; }
public void setAge(int age) { this.age = checkAge(age); }
private static String checkName(String name) {
if (name == null || name.isBlank()) throw new IllegalArgumentException("A name is required");
return name.trim();
}
private static int checkAge(int age) {
if (age < 0 || age > 150) throw new IllegalArgumentException("Invalid age: " + age);
return age;
}
}
public class Main {
public static void main(String[] args) {
Person p = new Person(" Asha ", 21);
System.out.println("[" + p.getName() + "] " + p.getAge());
p.setAge(22);
System.out.println(p.getAge());
try {
p.setAge(-4);
} catch (IllegalArgumentException e) {
System.out.println("Rejected: " + e.getMessage());
}
}
}- The convention is
getXandsetX, withisXfor booleans. Tools and frameworks rely on these names. - The checks live in private helpers, so the constructor and the setters can't drift apart.
- Don't generate a setter for every field out of habit. Add one only for values that really should change.
Immutable objects
class Money {
private final long cents;
public Money(long cents) {
this.cents = cents;
}
public Money plus(Money other) {
return new Money(cents + other.cents); // a new object; this one never changes
}
@Override
public String toString() {
return String.format("%d.%02d", cents / 100, cents % 100);
}
}
public class Main {
public static void main(String[] args) {
Money price = new Money(1999);
Money shipping = new Money(450);
Money total = price.plus(shipping);
System.out.println(price + " + " + shipping + " = " + total);
}
}An immutable object can't change after it's built: all its fields are private final, there are no setters, and "changes" return a new object. It can be shared freely, because nobody can alter it behind your back. String works exactly this way. (Storing money as whole cents also avoids the rounding errors of double.)