Encapsulation
Core Understanding
Encapsulation is the bundling of data and the methods that operate on that data within a single unit (class), restricting direct access to some of an object's components. It's about:
- Hiding internal state and requiring all interaction to be performed through an object's methods
- Protecting the integrity of an object's data
- Reducing coupling between different parts of an application
- Providing a clear and controlled interface for object interaction
❌ Bad Example
public class BankAccount {
// Public fields expose internal state
public double balance;
public List<Transaction> transactions;
public String accountNumber;
public void deposit(double amount) {
balance += amount;
transactions.add(new Transaction(amount));
}
public void withdraw(double amount) {
// No validation
balance -= amount;
transactions.add(new Transaction(-amount));
}
}
// Usage
BankAccount account = new BankAccount();
account.balance = 1000; // Direct manipulation of state
account.transactions.clear(); // Can corrupt the transaction history
Why it's bad:
- Public fields allow direct state manipulation
- No validation of data
- No protection against corruption
- Breaks data integrity
- Difficult to modify implementation
✅ Good Example
Let's fix this:
public class BankAccount {
private final String accountNumber;
private BigDecimal balance;
private final List<Transaction> transactions;
private final TransactionValidator validator;
private final TransactionLogger logger;
public BankAccount(String accountNumber, TransactionValidator validator,
TransactionLogger logger) {
this.accountNumber = accountNumber;
this.balance = BigDecimal.ZERO;
this.transactions = new ArrayList<>();
this.validator = validator;
this.logger = logger;
}
public TransactionResult deposit(BigDecimal amount) {
try {
validator.validateDeposit(amount);
balance = balance.add(amount);
Transaction transaction = new Transaction(TransactionType.DEPOSIT, amount);
transactions.add(transaction);
logger.logTransaction(accountNumber, transaction);
return TransactionResult.success(balance);
} catch (ValidationException e) {
return TransactionResult.failure(e.getMessage());
}
}
public TransactionResult withdraw(BigDecimal amount) {
try {
validator.validateWithdrawal(amount, balance);
balance = balance.subtract(amount);
Transaction transaction = new Transaction(TransactionType.WITHDRAWAL, amount);
transactions.add(transaction);
logger.logTransaction(accountNumber, transaction);
return TransactionResult.success(balance);
} catch (ValidationException e) {
return TransactionResult.failure(e.getMessage());
}
}
public BigDecimal getBalance() {
return balance;
}
public List<Transaction> getTransactionHistory() {
return Collections.unmodifiableList(transactions);
}
}
Why it's good:
- Private fields protect internal state
- Immutable where possible
- Validates all operations
- Provides controlled access through methods
- Logs operations for audit
- Returns defensive copies
Best Practices
- Use Private Fields with Getters/Setters When Needed
public class User {
private String password;
public void setPassword(String newPassword) {
validatePassword(newPassword);
this.password = hashPassword(newPassword);
}
// No getter for password - information hiding
}
- Implement Immutable Objects When Possible
public final class Money {
private final BigDecimal amount;
private final Currency currency;
public Money add(Money other) {
if (!this.currency.equals(other.currency)) {
throw new IllegalArgumentException("Currency mismatch");
}
return new Money(amount.add(other.amount), currency);
}
}
- Use Builder Pattern for Complex Object Creation
public class Order {
private final String id;
private final Customer customer;
private final List<OrderItem> items;
private Order(Builder builder) {
this.id = builder.id;
this.customer = builder.customer;
this.items = builder.items;
}
public static class Builder {
// Builder implementation
}
}
Use Cases
-
Financial Systems
- Bank accounts
- Payment processing
- Transaction management
-
Security-Critical Applications
- User credentials
- Authentication tokens
- Access control
-
Data Integrity Systems
- Audit logs
- Medical records
- Legal documents
Anti-patterns to Avoid
- Public Fields
// Avoid
public class User {
public String password; // Never expose sensitive data
}
- Getter/Setter for Every Field
// Avoid automatic getter/setter generation without consideration
@Data // Lombok annotation that generates everything
public class SensitiveData {
private String secretKey;
private String internalState;
}
- Exposing Internal Collections
// Avoid
public List<Transaction> getTransactions() {
return transactions; // Returns reference to internal list
}
// Better
public List<Transaction> getTransactions() {
return Collections.unmodifiableList(transactions);
}
Interview Questions & Answers
Q1: "How would you implement encapsulation in a thread-safe manner?"
Answer: Using a lock.
public class ThreadSafeAccount {
private final Lock lock = new ReentrantLock();
private BigDecimal balance;
public TransactionResult withdraw(BigDecimal amount) {
lock.lock();
try {
if (balance.compareTo(amount) < 0) {
return TransactionResult.failure("Insufficient funds");
}
balance = balance.subtract(amount);
return TransactionResult.success(balance);
} finally {
lock.unlock();
}
}
}
Q2: "How do you handle encapsulation with JPA entities?" A:
@Entity
public class Customer {
@Id
private Long id;
@Column(nullable = false)
private String name;
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
private Set<Order> orders = new HashSet<>();
public void addOrder(Order order) {
orders.add(order);
order.setCustomer(this);
}
public void removeOrder(Order order) {
orders.remove(order);
order.setCustomer(null);
}
// Getter for orders returns unmodifiable view
public Set<Order> getOrders() {
return Collections.unmodifiableSet(orders);
}
}