What is Encapsulation?
Encapsulation is one of the four fundamental principles of Object-Oriented Programming (along with Abstraction, Inheritance, and Polymorphism). It is the practice of bundling an object's data (fields) and the methods that operate on that data into a single unit, or "capsule."
More importantly, encapsulation is about data hiding. It means restricting direct access to an object's internal state (its fields) from the outside world. Instead, access is controlled through a public interface of methods (getters and setters). This protects the data from accidental corruption and enforces rules about how it can be changed.
Think of it like a modern capsule coffee machine. You can't just reach inside and grab the coffee grounds (the internal data). You can only interact with it through the provided interface: the button you press to start the brew cycle (the public method).
Hiding the Data
The primary tool for implementing encapsulation in Java is the private access modifier. When a field or method is declared as private, it can only be accessed from within the same class. Any attempt to access it from another class will result in a compile-time error.
This is the first and most critical step: making the fields of a class private to prevent direct, uncontrolled modification from outside.
public class Person {
// These fields are PRIVATE and cannot be accessed directly from outside this class.
private String name;
private int age;
// A public method to print the person's details.
// This method CAN access the private fields because it's inside the class.
public void printDetails() {
System.out.println("Name: " + this.name);
System.out.println("Age: " + this.age);
}
}
If you try to access these fields from another class, you will get an error:
public class Main {
public static void main(String[] args) {
Person person = new Person();
// COMPILE ERROR: The field Person.name is not visible
// person.name = "John";
// COMPILE ERROR: The field Person.age is not visible
// person.age = 30;
// This is the correct way to interact with the object (if methods were provided)
// person.printDetails();
}
}
Controlled Access
If all fields are private, how do we read or update their values? We provide public methods to serve as a controlled gateway. These are commonly known as getters (or accessors) and setters (or mutators).
- Getters: A public method that returns the value of a private field. By convention, they start with
get(e.g.,getName()). - Setters: A public method that takes a parameter and updates the value of a private field. By convention, they start with
set(e.g.,setName(String name)).
The power of setters lies in their ability to include validation logic to ensure the object remains in a valid state.
public class Person {
private String name;
private int age;
// --- Setter for name ---
public void setName(String name) {
// We can add validation logic here
if (name != null && !name.trim().isEmpty()) {
this.name = name;
} else {
System.out.println("Error: Name cannot be empty.");
}
}
// --- Getter for name ---
public String getName() {
return this.name;
}
// --- Setter for age with validation ---
public void setAge(int age) {
if (age > 0) {
this.age = age;
} else {
System.out.println("Error: Age must be a positive number.");
}
}
// --- Getter for age ---
public int getAge() {
return this.age;
}
}
Why Bother?
Following the encapsulation pattern provides significant advantages for building robust, maintainable, and secure applications.
| Benefit | Description |
|---|---|
| Data Hiding & Security | The internal state of an object is hidden from the outside. This prevents unauthorized or accidental modification, protecting the integrity of the data. |
| Increased Flexibility | You can change the internal implementation of a class (e.g., how data is stored) without breaking the code that uses it, as long as the public method signatures (getters/setters) remain the same. |
| Improved Maintainability | Code is easier to debug and maintain. If a value is incorrect, you know exactly where to look: the setter method. All logic related to a field is centralized in one place. |
| Control Over Data | Setters allow you to enforce rules and validation. You can prevent an object from entering an invalid state (e.g., a negative bank balance or an empty name). |
Creating Unchangeable Objects
An immutable object is an object whose state cannot be changed after it is created. This is a powerful form of encapsulation that leads to simpler, more thread-safe code. To create an immutable class in Java, you typically:
- Declare all fields as
privateandfinal. - Don't provide any setters.
- Ensure that the class cannot be extended (by making it
final). - If a field holds a reference to a mutable object, don't provide a "getter" that returns the original reference. Instead, return a defensive copy.
// This class is immutable
public final class ImmutablePoint {
// Fields are private and final
private final int x;
private final int y;
// Values are set only once, in the constructor
public ImmutablePoint(int x, int y) {
this.x = x;
this.y = y;
}
// Only provide getters, no setters
public int getX() {
return x;
}
public int getY() {
return y;
}
}
Bank Account Class
Let's put it all together with a practical BankAccount class. The balance is a critical piece of data that must be protected. We will use encapsulation to ensure it can only be modified through secure methods like deposit and withdraw.
public class BankAccount {
private String accountHolder;
private double balance;
private final String accountNumber; // Final, cannot be changed
public BankAccount(String accountHolder, String accountNumber, double initialDeposit) {
this.accountHolder = accountHolder;
this.accountNumber = accountNumber;
// Use the deposit method to ensure validation logic is applied
deposit(initialDeposit);
}
// --- Public Methods to Interact with the Data ---
public void deposit(double amount) {
if (amount > 0) {
balance += amount;
System.out.printf("Deposited: $%.2f. New Balance: $%.2f%n", amount, balance);
} else {
System.out.println("Deposit amount must be positive.");
}
}
public void withdraw(double amount) {
if (amount <= 0) {
System.out.println("Withdrawal amount must be positive.");
} else if (amount > balance) {
System.out.println("Insufficient funds. Withdrawal failed.");
} else {
balance -= amount;
System.out.printf("Withdrew: $%.2f. New Balance: $%.2f%n", amount, balance);
}
}
// --- Getters (no setters for balance or accountNumber for security) ---
public double getBalance() {
return balance;
}
public String getAccountHolder() {
return accountHolder;
}
public String getAccountNumber() {
return accountNumber;
}
}
Test Your Skills
- Create a `Student` class.
- Make the fields `studentId` (String), `name` (String), and `gpa` (double) all private.
- Create public getters and setters for all fields.
- In the `setGpa()` method, add validation to ensure the GPA is between 0.0 and 4.0. If an invalid value is provided, print an error message and do not change the GPA.
- In a `main` method, create a `Student` object and try to set an invalid GPA to see your validation in action.
🏆 Challenge: Vending Machine
Create a `VendingMachine` class that uses encapsulation to manage its state and operations.
- Encapsulate a `Map
` for `itemStock` (item name to quantity) and a `double` for `currentBalance`. - Create a constructor that initializes the machine with some items and stock.
- Create public methods: `insertMoney(double amount)`, `selectItem(String itemName)`, and `dispenseItem()`.
- The `selectItem` method should check if the item exists and if the `currentBalance` is sufficient to cover its cost (you can define a simple cost structure).
- The `dispenseItem` method should only work if an item has been successfully selected and paid for. It should then decrement the stock and reset the balance.
- Ensure that the internal state (`itemStock`, `currentBalance`) can only be changed through these methods.
import java.util.HashMap;
import java.util.Map;
public class VendingMachine {
// Encapsulated state
private Map itemStock;
private double currentBalance;
private String selectedItem;
public VendingMachine() {
itemStock = new HashMap<>();
itemStock.put("Soda", 10);
itemStock.put("Chips", 5);
itemStock.put("Candy", 15);
currentBalance = 0.0;
selectedItem = null;
}
public void insertMoney(double amount) {
if (amount > 0) {
this.currentBalance += amount;
System.out.printf("Inserted: $%.2f. Current Balance: $%.2f%n", amount, this.currentBalance);
} else {
System.out.println("Please insert a positive amount.");
}
}
public void selectItem(String itemName) {
if (itemStock.containsKey(itemName) && itemStock.get(itemName) > 0) {
this.selectedItem = itemName;
double price = getItemPrice(itemName);
System.out.printf("Selected: %s (Price: $%.2f)%n", itemName, price);
} else {
System.out.println("Sorry, " + itemName + " is out of stock.");
this.selectedItem = null;
}
}
// Helper method to define prices
private double getItemPrice(String itemName) {
switch (itemName) {
case "Soda": return 1.50;
case "Chips": return 1.00;
case "Candy": return 0.75;
default: return 0.0;
}
}
public void dispenseItem() {
if (selectedItem == null) {
System.out.println("Please select an item first.");
return;
}
double price = getItemPrice(selectedItem);
if (currentBalance >= price) {
// Transaction successful
currentBalance -= price;
int stock = itemStock.get(selectedItem);
itemStock.put(selectedItem, stock - 1);
System.out.println("Dispensing " + selectedItem + ". Enjoy!");
System.out.printf("Returning change: $%.2f%n", currentBalance);
// Reset for next customer
currentBalance = 0;
selectedItem = null;
} else {
System.out.printf("Insufficient funds. Please insert $%.2f more.%n", price - currentBalance);
}
}
public void printStock() {
System.out.println("--- Current Stock ---");
for (Map.Entry entry : itemStock.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
System.out.println("---------------------");
}
}