Java Tutorial » Chapter 26 — Encapsulation

Chapter 26 — Java Encapsulation

Protecting your object's data by bundling fields and methods together.

1. Introduction to Encapsulation

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).

2. The `private` Access Modifier

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();
    }
}
3. Public Getters and Setters

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;
    }
}
4. Benefits of Encapsulation

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).
5. Encapsulation and Immutability

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:

  1. Declare all fields as private and final.
  2. Don't provide any setters.
  3. Ensure that the class cannot be extended (by making it final).
  4. 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;
    }
}
6. A Complete Example

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;
    }
}
7. Practice & Challenge

Test Your Skills

  1. Create a `Student` class.
  2. Make the fields `studentId` (String), `name` (String), and `gpa` (double) all private.
  3. Create public getters and setters for all fields.
  4. 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.
  5. 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("---------------------");
    }
}