Virtual assistance

Java Encapsulation

Master data hiding and encapsulation in Java - protecting data and controlling access

Java Encapsulation

What is Encapsulation in Java?

Encapsulation is one of the four fundamental principles of Object-Oriented Programming (OOP). It refers to the bundling of data (attributes) and methods (behaviors) that operate on that data into a single unit called a class. It also involves restricting direct access to some of the object's components, which is known as data hiding.

The main goal of encapsulation is to protect the internal state of an object from unauthorized access and modification. This is achieved by making the instance variables private and providing public getter and setter methods to access and modify these variables.

In Java, encapsulation is implemented using:

  • Access Modifiers: private, public, protected, and default
  • Getter Methods: Methods to retrieve the values of private variables
  • Setter Methods: Methods to modify the values of private variables

Access Modifiers in Java

Java provides four access modifiers to control the visibility and accessibility of classes, variables, methods, and constructors:

Modifier Class Package Subclass World
private Yes No No No
default Yes Yes No No
protected Yes Yes Yes No
public Yes Yes Yes Yes

Basic Encapsulation Example:

public class Student {
    // Private instance variables (data hiding)
    private String name;
    private int age;
    private String studentId;

    // Public constructor
    public Student(String name, int age, String studentId) {
        this.name = name;
        this.age = age;
        this.studentId = studentId;
    }

    // Public getter methods
    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    public String getStudentId() {
        return studentId;
    }

    // Public setter methods
    public void setName(String name) {
        this.name = name;
    }

    public void setAge(int age) {
        if (age > 0 && age < 100) { // Validation
            this.age = age;
        } else {
            System.out.println("Invalid age");
        }
    }

    public void setStudentId(String studentId) {
        this.studentId = studentId;
    }

    // Method to display student information
    public void displayInfo() {
        System.out.println("Name: " + name);
        System.out.println("Age: " + age);
        System.out.println("Student ID: " + studentId);
    }
}

public class Main {
    public static void main(String[] args) {
        Student student = new Student("John Doe", 20, "STU001");

        // Accessing data through getter methods
        System.out.println("Student Name: " + student.getName());
        System.out.println("Student Age: " + student.getAge());

        // Modifying data through setter methods
        student.setAge(21);
        student.setName("John Smith");

        // Trying invalid age
        student.setAge(-5); // Will show error message

        student.displayInfo();
    }
}

Advanced Encapsulation Example:

public class BankAccount {
    // Private instance variables
    private String accountNumber;
    private String accountHolder;
    private double balance;
    private String pin;

    // Constructor
    public BankAccount(String accountNumber, String accountHolder, String pin) {
        this.accountNumber = accountNumber;
        this.accountHolder = accountHolder;
        this.pin = pin;
        this.balance = 0.0;
    }

    // Getter methods
    public String getAccountNumber() {
        return accountNumber;
    }

    public String getAccountHolder() {
        return accountHolder;
    }

    public double getBalance(String enteredPin) {
        if (validatePin(enteredPin)) {
            return balance;
        } else {
            System.out.println("Invalid PIN");
            return -1;
        }
    }

    // Setter methods with validation
    public void setAccountHolder(String accountHolder) {
        if (accountHolder != null && !accountHolder.trim().isEmpty()) {
            this.accountHolder = accountHolder;
        } else {
            System.out.println("Invalid account holder name");
        }
    }

    public void changePin(String oldPin, String newPin) {
        if (validatePin(oldPin) && isValidPin(newPin)) {
            this.pin = newPin;
            System.out.println("PIN changed successfully");
        } else {
            System.out.println("PIN change failed");
        }
    }

    // Business methods
    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("Invalid deposit amount");
        }
    }

    public void withdraw(double amount, String enteredPin) {
        if (validatePin(enteredPin)) {
            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 or insufficient funds");
            }
        } else {
            System.out.println("Invalid PIN");
        }
    }

    // Private helper methods
    private boolean validatePin(String enteredPin) {
        return this.pin.equals(enteredPin);
    }

    private boolean isValidPin(String pin) {
        return pin != null && pin.length() == 4 && pin.matches("\\d{4}");
    }
}

public class BankDemo {
    public static void main(String[] args) {
        BankAccount account = new BankAccount("123456789", "Alice Johnson", "1234");

        // Deposit money
        account.deposit(1000);

        // Try to check balance with wrong PIN
        account.getBalance("0000"); // Invalid PIN

        // Check balance with correct PIN
        System.out.println("Balance: $" + account.getBalance("1234"));

        // Withdraw money
        account.withdraw(500, "1234");

        // Change PIN
        account.changePin("1234", "5678");

        // Try old PIN (should fail)
        account.withdraw(200, "1234"); // Invalid PIN

        // Use new PIN
        account.withdraw(200, "5678");
    }
}

Benefits of Encapsulation:

  • Data Hiding: Internal state is protected from unauthorized access
  • Increased Security: Controlled access to data through methods
  • Better Maintainability: Changes to internal implementation don't affect external code
  • Validation: Data can be validated before being set
  • Flexibility: Internal implementation can be changed without affecting users
  • Modularity: Code is organized into logical units

Read-Only and Write-Only Properties:

public class Employee {
    private String employeeId;
    private String name;
    private double salary;
    private final String dateOfJoining;

    public Employee(String employeeId, String name, double salary, String dateOfJoining) {
        this.employeeId = employeeId;
        this.name = name;
        this.salary = salary;
        this.dateOfJoining = dateOfJoining;
    }

    // Read-only property (no setter)
    public String getEmployeeId() {
        return employeeId;
    }

    // Read-write property
    public String getName() {
        return name;
    }

    public void setName(String name) {
        if (name != null && !name.trim().isEmpty()) {
            this.name = name;
        }
    }

    // Write-only property (no getter for salary)
    public void setSalary(double salary, String managerPin) {
        if ("ADMIN123".equals(managerPin) && salary > 0) {
            this.salary = salary;
            System.out.println("Salary updated successfully");
        } else {
            System.out.println("Unauthorized access or invalid salary");
        }
    }

    // Read-only property (final field)
    public String getDateOfJoining() {
        return dateOfJoining;
    }

    // Method to get salary (requires authentication)
    public double getSalary(String pin) {
        if ("USER123".equals(pin)) {
            return salary;
        } else {
            System.out.println("Access denied");
            return 0;
        }
    }
}

Encapsulation with Inheritance:

// Base class with encapsulated data
class Vehicle {
    private String make;
    private String model;
    private int year;

    public Vehicle(String make, String model, int year) {
        this.make = make;
        this.model = model;
        this.year = year;
    }

    // Getters
    public String getMake() { return make; }
    public String getModel() { return model; }
    public int getYear() { return year; }

    // Setters with validation
    public void setMake(String make) {
        if (make != null && !make.trim().isEmpty()) {
            this.make = make;
        }
    }

    public void setModel(String model) {
        if (model != null && !model.trim().isEmpty()) {
            this.model = model;
        }
    }

    public void setYear(int year) {
        if (year >= 1900 && year <= 2025) {
            this.year = year;
        }
    }

    public void displayInfo() {
        System.out.println(year + " " + make + " " + model);
    }
}

// Subclass inheriting encapsulated behavior
class Car extends Vehicle {
    private int doors;
    private String fuelType;

    public Car(String make, String model, int year, int doors, String fuelType) {
        super(make, model, year);
        this.doors = doors;
        this.fuelType = fuelType;
    }

    // Getters and setters for new properties
    public int getDoors() { return doors; }
    public void setDoors(int doors) {
        if (doors >= 2 && doors <= 5) {
            this.doors = doors;
        }
    }

    public String getFuelType() { return fuelType; }
    public void setFuelType(String fuelType) {
        if (fuelType != null && (fuelType.equals("Petrol") || fuelType.equals("Diesel") || fuelType.equals("Electric"))) {
            this.fuelType = fuelType;
        }
    }

    @Override
    public void displayInfo() {
        super.displayInfo();
        System.out.println("Doors: " + doors);
        System.out.println("Fuel Type: " + fuelType);
    }
}

Best Practices for Encapsulation:

  • Make instance variables private
  • Provide public getter and setter methods
  • Use meaningful names for getter and setter methods
  • Include validation in setter methods
  • Consider using read-only or write-only properties when appropriate
  • Keep the class interface simple and intuitive
  • Document the purpose and behavior of methods

Common Mistakes to Avoid:

  • Don't make all variables public - this defeats the purpose of encapsulation
  • Avoid getters and setters that don't add any value
  • Don't expose internal implementation details
  • Avoid tight coupling between classes
  • Don't use encapsulation just for the sake of it

Encapsulation is a fundamental concept in Java that helps you create robust, maintainable, and secure applications. By properly encapsulating your data and providing controlled access through methods, you can protect your objects from unauthorized access and ensure data integrity. Practice implementing encapsulation in your Java projects to build better software.