🔒 Encapsulation
The practice of wrapping data (variables) and code acting on the data (methods) together as a single unit, keeping the fields safe from outside interference.
1. What is Encapsulation?
Encapsulation is one of the four fundamental OOP concepts (along with Inheritance, Polymorphism, and Abstraction). It is the mechanism of wrapping data (variables) and code acting on the data (methods) together as a single unit. In encapsulation, the variables of a class will be hidden from other classes and can be accessed only through the methods of their current class. Therefore, it is also known as data hiding.
To achieve encapsulation in Java: 1. Declare the variables of a class as private. 2. Provide public setter and getter methods to modify and view the variables' values.
Think of an ATM machine. When you use an ATM, you interact with the screen, buttons, and cash dispenser. You cannot directly access the internal cash box or the core database holding your account balance. The ATM encapsulates the money and the logic of withdrawing it, providing a safe, validated public interface (the buttons and screen) while keeping the sensitive parts private and secure. Similarly, in a Java class, we keep our fields (the cash box) private and provide methods (the ATM buttons) to interact with them safely.
2. Private Fields & Public Getters/Setters
By marking fields as private, you ensure that no outside class can directly read or modify the data. Instead, you provide public getter and setter methods. Without private fields, anyone could assign invalid data to your variables, bypassing any logic you might want to enforce.
3. Validation in Setters
The primary reason for using getters and setters is to gain control over the data. By forcing data changes to go through a method, you can validate the input before changing the actual field. This prevents your objects from ending up in an invalid state.
public class Employee { private String name; private int age; private double salary; public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } // Validation in setter public void setAge(int age) { if (age >= 18 && age <= 65) { this.age = age; } else { System.out.println("Invalid age. Must be between 18 and 65."); } } public double getSalary() { return salary; } // Validation in setter public void setSalary(double salary) { if (salary > 0) { this.salary = salary; } else { System.out.println("Salary must be positive."); } } @Override public String toString() { return "Employee{name='" + name + "', age=" + age + ", salary=" + salary + "}"; } }
4. JavaBeans Convention
A JavaBean is a standard convention in Java for creating classes. To be a JavaBean, a class must:
- Have a public no-arg (default) constructor.
- Make fields private.
- Provide standard public getter and setter methods using a specific naming convention:
getFieldName()andsetFieldName(). - For boolean fields, the getter should be named
isFieldName()instead ofgetFieldName(). - Implement the
java.io.Serializableinterface (often).
5. Benefits of Encapsulation
| Benefit | Description |
|---|---|
| Data Hiding | Internal state is hidden and protected from unauthorized direct access. |
| Increased Flexibility | You can make variables read-only (only getter) or write-only (only setter). |
| Reusability | Encapsulated classes are modular and can be reused easily across different projects. |
| Testing Code | Encapsulated code is easier to test for unit testing because dependencies and state are controlled. |
| Maintainability | You can change the internal implementation (like changing a list to a map) without breaking the code of users who call your getters/setters. |
6. Immutable Classes
An immutable class is a class whose instances cannot be modified once they are created. This is the ultimate form of encapsulation. To create an immutable class:
- Declare the class as final so it cannot be subclassed (preventing overriding methods to change behavior).
- Make all fields private and final.
- Do not provide any setter methods.
- Initialize all fields via a constructor or a static factory method.
- If the class has mutable object fields (like arrays or dates), make defensive copies in the constructor and getters.
// 1. Class is final public final class ImmutableStudent { // 2. Fields are private and final private final String id; private final String name; // 3. Private constructor initialized all fields private ImmutableStudent(String id, String name) { this.id = id; this.name = name; } // Static factory method public static ImmutableStudent create(String id, String name) { return new ImmutableStudent(id, name); } // 4. Only getters, no setters public String getId() { return id; } public String getName() { return name; } @Override public String toString() { return "Student[" + id + ": " + name + "]"; } }
7. Why String is Immutable in Java
The String class in Java is a perfect example of an immutable class. It is immutable for several important reasons:
- Security: Strings are widely used as parameters for network connections, database URLs, and file paths. If String were mutable, a malicious user could change the string reference after security checks are performed.
- Thread Safety: Because they cannot be modified, Strings are inherently thread-safe and can be shared among multiple threads without synchronization.
- String Pool: Immutability allows Java to safely cache String literals in the String Constant Pool, saving massive amounts of memory.
- Caching Hashcodes: The hashcode of a String is cached when it is first calculated. This makes it very fast to use as a key in HashMaps.
8. Advanced Encapsulation Example
import java.util.ArrayList; import java.util.List; public class BankAccountSecure { private final String accountNumber; private double balance; private final List<String> transactionHistory; public BankAccountSecure(String accountNumber, double initialBalance) { this.accountNumber = accountNumber; this.transactionHistory = new ArrayList<>(); if (initialBalance >= 0) { this.balance = initialBalance; transactionHistory.add("Initial deposit: $" + initialBalance); } else { this.balance = 0; transactionHistory.add("Account opened with $0 balance"); } } public String getAccountNumber() { return accountNumber; } public double getBalance() { return balance; } public void deposit(double amount) { if (amount > 0) { balance += amount; transactionHistory.add("Deposited: $" + amount); } } public boolean withdraw(double amount) { if (amount > 0 && balance >= amount) { balance -= amount; transactionHistory.add("Withdrew: $" + amount); return true; } transactionHistory.add("Failed withdrawal attempt: $" + amount); return false; } // Return a defensive copy to protect internal list public List<String> getTransactionHistory() { return new ArrayList<>(transactionHistory); } }
public class Main { public static void main(String[] args) { // 1. Employee Demo Employee emp = new Employee(); emp.setName("Alice"); emp.setAge(15); // Invalid emp.setAge(30); // Valid emp.setSalary(50000); System.out.println(emp); // 2. Immutable Demo ImmutableStudent student = ImmutableStudent.create("S101", "Bob"); System.out.println(student); // 3. Bank Account Demo BankAccountSecure account = new BankAccountSecure("ACC-123", 100); account.deposit(50); account.withdraw(200); // Fails account.withdraw(40); System.out.println("Balance: $" + account.getBalance()); System.out.println("History: " + account.getTransactionHistory()); } }