Java Encapsulation

Learn how to protect your data by wrapping it in a secure capsule, controlling access through getters and setters for safer, more maintainable code.

What is Encapsulation?

Encapsulation is one of the four fundamental principles of Object-Oriented Programming (OOP). It means wrapping data (variables) and code (methods) together as a single unit, and hiding the internal details from the outside world. Think of it as putting your valuables in a safe – only authorized people with the key can access them!

In Java, encapsulation is achieved by declaring class variables as private and providing public getter and setter methods to access and modify those variables. This way, you control how the data is accessed and modified.

  • Data Hiding: Keep class variables private, hidden from outside access
  • Controlled Access: Use public methods (getters/setters) to access private data
  • Security: Protect data from unauthorized access and invalid modifications

Real-Life Analogy: Think of a medicine capsule – the medicine (data) is wrapped inside the capsule shell (class). You can't directly touch the medicine; you take the whole capsule. Similarly, you can't directly access private variables; you use methods to interact with them!

Why Encapsulation?

Encapsulation provides several important benefits that make your code more secure, flexible, and maintainable:

  • Data Protection: Prevents accidental or unauthorized modification of data
  • Validation: Add validation logic in setter methods to ensure data integrity
  • Flexibility: Change internal implementation without affecting other code
  • Read-Only or Write-Only: Create read-only (only getter) or write-only (only setter) properties
  • Easy Maintenance: Changes to private members don't affect other classes

Important: Without encapsulation, anyone can directly access and modify your variables, potentially setting invalid values like negative age or wrong email format. Encapsulation prevents this!

Without Encapsulation (Bad Practice)

Let's first see what happens when we don't use encapsulation – variables are public and can be accessed directly.

Example: No Encapsulation (Insecure)

class Student {
    // Public variables - anyone can access and modify
    public String name;
    public int age;
}

public class Main {
    public static void main(String[] args) {
        Student s = new Student();
        
        // Direct access - looks convenient but dangerous!
        s.name = "John";
        s.age = 20;
        
        System.out.println("Name: " + s.name);
        System.out.println("Age: " + s.age);
        
        // Problem: Invalid data can be set!
        s.age = -5;  // Negative age - should not be allowed!
        System.out.println("Invalid Age: " + s.age);
    }
}
Output:
Name: John
Age: 20
Invalid Age: -5

Problems:

  • No validation – anyone can set invalid values like negative age
  • No control over how data is accessed or modified
  • Direct exposure of internal data structure
  • Difficult to maintain – changing variables affects all code using them

With Encapsulation (Best Practice)

Now let's implement proper encapsulation using private variables and public getter/setter methods.

Example: Proper Encapsulation (Secure)

class Student {
    // Private variables - cannot be accessed directly
    private String name;
    private int age;
    
    // Public getter for name
    public String getName() {
        return name;
    }
    
    // Public setter for name
    public void setName(String name) {
        this.name = name;
    }
    
    // Public getter for age
    public int getAge() {
        return age;
    }
    
    // Public setter for age with validation
    public void setAge(int age) {
        if (age > 0 && age < 150) {
            this.age = age;
        } else {
            System.out.println("Invalid age! Age must be between 1 and 149.");
        }
    }
}

public class Main {
    public static void main(String[] args) {
        Student s = new Student();
        
        // Using setter methods to set values
        s.setName("John");
        s.setAge(20);
        
        // Using getter methods to get values
        System.out.println("Name: " + s.getName());
        System.out.println("Age: " + s.getAge());
        
        // Trying to set invalid age
        s.setAge(-5);  // Validation prevents invalid data!
        s.setAge(200); // Also invalid!
        
        System.out.println("Age after invalid attempts: " + s.getAge());
    }
}
Output:
Name: John
Age: 20
Invalid age! Age must be between 1 and 149.
Invalid age! Age must be between 1 and 149.
Age after invalid attempts: 20

Explanation:

  • Variables are private – cannot be accessed directly from outside
  • Public getter methods return the values of private variables
  • Public setter methods set values with validation logic
  • Invalid values are rejected, protecting data integrity

Getter and Setter Method Naming Convention

Java follows specific naming conventions for getter and setter methods:

  • Getter Method: public returnType getVariableName() – Starts with "get" followed by variable name with first letter capitalized
  • Setter Method: public void setVariableName(type parameter) – Starts with "set" followed by variable name with first letter capitalized
  • Boolean Getter: For boolean variables, use isVariableName() instead of getVariableName()

Examples: Variable: private String name; → Getter: getName(), Setter: setName(String name)
Variable: private boolean active; → Getter: isActive(), Setter: setActive(boolean active)

Bank Account Example

A real-world example showing how encapsulation protects sensitive banking data.

Example: Encapsulated Bank Account

class BankAccount {
    // Private variables - hidden from outside
    private String accountNumber;
    private String accountHolder;
    private double balance;
    
    // Constructor
    public BankAccount(String accountNumber, String accountHolder, double initialBalance) {
        this.accountNumber = accountNumber;
        this.accountHolder = accountHolder;
        if (initialBalance >= 0) {
            this.balance = initialBalance;
        } else {
            this.balance = 0;
            System.out.println("Initial balance cannot be negative. Set to 0.");
        }
    }
    
    // Getter for account number (read-only)
    public String getAccountNumber() {
        return accountNumber;
    }
    
    // Getter for account holder (read-only)
    public String getAccountHolder() {
        return accountHolder;
    }
    
    // Getter for balance (read-only)
    public double getBalance() {
        return balance;
    }
    
    // No setters for balance - can only change through deposit/withdraw
    
    // Method to deposit money
    public void deposit(double amount) {
        if (amount > 0) {
            balance += amount;
            System.out.println("Deposited: $" + amount);
            System.out.println("New Balance: $" + balance);
        } else {
            System.out.println("Deposit amount must be positive!");
        }
    }
    
    // Method to withdraw money
    public void withdraw(double amount) {
        if (amount > 0 && amount <= balance) {
            balance -= amount;
            System.out.println("Withdrawn: $" + amount);
            System.out.println("New Balance: $" + balance);
        } else {
            System.out.println("Invalid withdrawal amount!");
        }
    }
}

public class Main {
    public static void main(String[] args) {
        BankAccount account = new BankAccount("ACC001", "John Doe", 1000);
        
        System.out.println("Account Holder: " + account.getAccountHolder());
        System.out.println("Account Number: " + account.getAccountNumber());
        System.out.println("Initial Balance: $" + account.getBalance());
        System.out.println();
        
        account.deposit(500);
        System.out.println();
        
        account.withdraw(300);
        System.out.println();
        
        // Trying invalid operations
        account.deposit(-100);  // Invalid
        account.withdraw(2000); // Insufficient balance
    }
}
Output:
Account Holder: John Doe
Account Number: ACC001
Initial Balance: $1000.0

Deposited: $500.0
New Balance: $1500.0

Withdrawn: $300.0
New Balance: $1200.0

Deposit amount must be positive!
Invalid withdrawal amount!

Explanation:

  • Balance is private and can only be modified through controlled methods
  • No direct setter for balance – prevents unauthorized modification
  • Deposit and withdraw methods include validation logic
  • Account number and holder are read-only (getter only, no setter)

Employee Management Example

Another practical example demonstrating encapsulation with validation for employee data.

Example: Employee Class with Encapsulation

class Employee {
    // Private variables
    private int empId;
    private String name;
    private double salary;
    private String email;
    
    // Constructor
    public Employee(int empId, String name, double salary, String email) {
        this.empId = empId;
        this.name = name;
        setSalary(salary);  // Using setter for validation
        setEmail(email);    // Using setter for validation
    }
    
    // Getters
    public int getEmpId() {
        return empId;
    }
    
    public String getName() {
        return name;
    }
    
    public double getSalary() {
        return salary;
    }
    
    public String getEmail() {
        return email;
    }
    
    // Setters with validation
    public void setName(String name) {
        if (name != null && !name.isEmpty()) {
            this.name = name;
        } else {
            System.out.println("Name cannot be empty!");
        }
    }
    
    public void setSalary(double salary) {
        if (salary >= 0) {
            this.salary = salary;
        } else {
            System.out.println("Salary cannot be negative!");
            this.salary = 0;
        }
    }
    
    public void setEmail(String email) {
        if (email != null && email.contains("@")) {
            this.email = email;
        } else {
            System.out.println("Invalid email format!");
            this.email = "not-provided@example.com";
        }
    }
    
    // Method to display employee info
    public void displayInfo() {
        System.out.println("Employee ID: " + empId);
        System.out.println("Name: " + name);
        System.out.println("Salary: $" + salary);
        System.out.println("Email: " + email);
    }
}

public class Main {
    public static void main(String[] args) {
        Employee emp = new Employee(101, "Alice Johnson", 50000, "alice@company.com");
        emp.displayInfo();
        
        System.out.println("\n" + "=".repeat(40) + "\n");
        
        // Trying to update with valid data
        emp.setName("Alice Smith");
        emp.setSalary(55000);
        System.out.println("After valid updates:");
        emp.displayInfo();
        
        System.out.println("\n" + "=".repeat(40) + "\n");
        
        // Trying to set invalid data
        emp.setName("");           // Invalid
        emp.setSalary(-1000);      // Invalid
        emp.setEmail("invalid");   // Invalid
        
        System.out.println("After invalid attempts:");
        emp.displayInfo();
    }
}
Output:
Employee ID: 101
Name: Alice Johnson
Salary: $50000.0
Email: alice@company.com

========================================

After valid updates:
Employee ID: 101
Name: Alice Smith
Salary: $55000.0
Email: alice@company.com

========================================

Name cannot be empty!
Salary cannot be negative!
Invalid email format!
After invalid attempts:
Employee ID: 101
Name: Alice Smith
Salary: $55000.0
Email: alice@company.com

Explanation:

  • All employee data is private and protected
  • Setters validate data before allowing changes
  • Invalid data is rejected with appropriate error messages
  • Constructor uses setters to ensure validation happens during object creation

Types of Encapsulation Access

1. Read-Only (Getter Only)

Provide only getter method, no setter. Data can be read but not modified from outside. Perfect for constants or values that should not change after initialization.

Example: private final String accountNumber; with only getAccountNumber() method

2. Write-Only (Setter Only)

Provide only setter method, no getter. Data can be set but not read from outside. Less common but useful for sensitive data like passwords.

Example: private String password; with only setPassword(String password) method

3. Read-Write (Both Getter and Setter)

Provide both getter and setter methods. Data can be both read and modified with proper validation. Most common approach.

Example: private String name; with both getName() and setName(String name)

4. No Access (Neither Getter nor Setter)

Don't provide any getter or setter. Data is completely private and used only internally by the class. Most secure but least flexible.

Example: private String internalData; with no public methods to access it

Read-Only Property Example

Creating properties that can be set only during object creation but not modified afterward.

Example: Read-Only Properties

class Product {
    // Read-only - set once during creation
    private final String productId;
    private final String productName;
    
    // Read-write - can be modified
    private double price;
    private int stock;
    
    public Product(String productId, String productName, double price, int stock) {
        this.productId = productId;
        this.productName = productName;
        this.price = price;
        this.stock = stock;
    }
    
    // Read-only properties - only getters
    public String getProductId() {
        return productId;
    }
    
    public String getProductName() {
        return productName;
    }
    
    // Read-write properties - both getter and setter
    public double getPrice() {
        return price;
    }
    
    public void setPrice(double price) {
        if (price > 0) {
            this.price = price;
        } else {
            System.out.println("Price must be positive!");
        }
    }
    
    public int getStock() {
        return stock;
    }
    
    public void setStock(int stock) {
        if (stock >= 0) {
            this.stock = stock;
        } else {
            System.out.println("Stock cannot be negative!");
        }
    }
    
    public void displayProduct() {
        System.out.println("Product ID: " + productId);
        System.out.println("Product Name: " + productName);
        System.out.println("Price: $" + price);
        System.out.println("Stock: " + stock);
    }
}

public class Main {
    public static void main(String[] args) {
        Product product = new Product("P001", "Laptop", 999.99, 50);
        product.displayProduct();
        
        System.out.println("\n" + "-".repeat(40) + "\n");
        
        // Can modify price and stock
        product.setPrice(899.99);
        product.setStock(45);
        
        System.out.println("After updates:");
        product.displayProduct();
        
        // Cannot modify productId or productName - they're final!
        // product.productId = "P002";  // Compilation error!
    }
}
Output:
Product ID: P001
Product Name: Laptop
Price: $999.99
Stock: 50

----------------------------------------

After updates:
Product ID: P001
Product Name: Laptop
Price: $899.99
Stock: 45

Explanation:

  • Product ID and name are final – can't be changed after creation
  • Only getters provided for final fields (read-only)
  • Price and stock can be modified using setters (read-write)
  • This design prevents accidental modification of critical identifiers

Benefits of Encapsulation

  • Data Security: Protects data from unauthorized access and modification
  • Data Validation: Ensures only valid data is stored in objects
  • Flexibility: Internal implementation can change without affecting other code
  • Maintainability: Easier to maintain and update code
  • Control: Full control over what data can be read or written
  • Reusability: Well-encapsulated classes can be reused safely
  • Testing: Easier to test individual components

Real-World Impact: Imagine a banking app where anyone could directly modify account balance. That would be a disaster! Encapsulation ensures balance can only be changed through validated deposit/withdraw methods, maintaining data integrity and security.

How to Achieve Encapsulation?

  • Step 1: Declare class variables as private
  • Step 2: Create public getter methods to return variable values
  • Step 3: Create public setter methods to modify variable values
  • Step 4: Add validation logic in setter methods to ensure data integrity
  • Step 5: Decide which properties should be read-only, write-only, or read-write

Best Practice: Always make your instance variables private by default. Only expose them through getters/setters if necessary. This is called the "principle of least privilege" – give only the minimum access required!

Encapsulation vs Data Hiding

  • Data Hiding: Making class members private to restrict direct access
  • Encapsulation: Wrapping data and methods together, plus providing controlled access
  • Relationship: Data hiding is a part of encapsulation, but encapsulation is broader
  • Key Difference: Data hiding focuses on "hiding," encapsulation focuses on "wrapping and controlling"

Remember: Encapsulation = Data Hiding + Controlled Access through Methods. It's not just about making things private; it's about providing the right level of access through proper interfaces!

Important Rules for Encapsulation

  • Private Variables: Always declare instance variables as private
  • Public Methods: Getter and setter methods should be public
  • Meaningful Names: Follow naming conventions: getName(), setName()
  • Validate in Setters: Add validation logic to prevent invalid data
  • Return Types: Getters return the variable type, setters return void
  • Use final for Immutability: Make variables final if they shouldn't change after initialization

Common Mistake: Creating public variables "for convenience" defeats the purpose of encapsulation. Always use private variables with getters/setters, even if it seems like extra work – it pays off in security and maintainability!

Key Points to Remember

  • Encapsulation = Data Hiding + Controlled Access
  • Make all instance variables private
  • Provide public getter/setter methods for controlled access
  • Add validation logic in setters to ensure data integrity
  • Use read-only properties (getter only) when data shouldn't be modified
  • Follow naming conventions: getName(), setName(), isActive()
  • Encapsulation improves security, flexibility, and maintainability
  • It's a fundamental principle of OOP and good software design

Final Thought: Think of your class as a black box – users should interact with it through well-defined buttons (methods), not by opening it up and messing with the wires (variables). Encapsulation keeps your code safe, clean, and professional!