1. java
  2. /oop
  3. /encapsulation

Master Java Encapsulation and Data Hiding

Java Encapsulation

Encapsulation is one of the fundamental principles of object-oriented programming that involves bundling data (attributes) and methods (behavior) together within a class, while restricting direct access to the internal implementation details. It's often referred to as "data hiding."

What is Encapsulation?

Encapsulation provides:

  • Data Protection: Control access to object's internal state
  • Implementation Hiding: Hide internal details from external code
  • Controlled Access: Use methods to access and modify data
  • Validation: Ensure data integrity through controlled modification
public class BankAccount {
    // Private fields - cannot be accessed directly from outside
    private String accountNumber;
    private double balance;
    private String accountHolder;
    
    public BankAccount(String accountNumber, String accountHolder, double initialBalance) {
        this.accountNumber = accountNumber;
        this.accountHolder = accountHolder;
        this.balance = initialBalance >= 0 ? initialBalance : 0; // Validation
    }
    
    // Controlled access through public methods
    public double getBalance() {
        return balance;
    }
    
    public String getAccountHolder() {
        return accountHolder;
    }
    
    public void deposit(double amount) {
        if (amount > 0) {
            balance += amount;
            System.out.println("Deposited: $" + amount);
        } else {
            System.out.println("Invalid deposit amount");
        }
    }
    
    public boolean withdraw(double amount) {
        if (amount > 0 && amount <= balance) {
            balance -= amount;
            System.out.println("Withdrawn: $" + amount);
            return true;
        } else {
            System.out.println("Insufficient funds or invalid amount");
            return false;
        }
    }
}

// Usage - external code cannot directly modify balance
BankAccount account = new BankAccount("12345", "John Doe", 1000.0);
// account.balance = 5000.0; // Compilation error - cannot access private field
account.deposit(500.0);       // Controlled modification through method

Access Modifiers

Java provides four access modifiers to control visibility:

1. Private

Only accessible within the same class:

public class Employee {
    private String socialSecurityNumber; // Most sensitive data
    private double salary;
    
    public Employee(String ssn, double salary) {
        this.socialSecurityNumber = ssn;
        this.salary = salary;
    }
    
    // Private helper method
    private boolean isValidSSN(String ssn) {
        return ssn != null && ssn.length() == 9;
    }
    
    public double getAnnualSalary() {
        return salary * 12;
    }
}

2. Package-Private (Default)

Accessible within the same package:

class PackageClass {
    String packageField; // Package-private field
    
    void packageMethod() { // Package-private method
        System.out.println("Accessible within package");
    }
}

3. Protected

Accessible within the same package and by subclasses:

public class Vehicle {
    protected String engine; // Accessible to subclasses
    protected int year;
    
    protected void startEngine() {
        System.out.println("Engine starting...");
    }
}

public class Car extends Vehicle {
    public void start() {
        startEngine(); // Can access protected method from parent
        System.out.println("Car started");
    }
}

4. Public

Accessible from anywhere:

public class Calculator {
    public static final double PI = 3.14159; // Public constant
    
    public double calculateArea(double radius) { // Public method
        return PI * radius * radius;
    }
}

Getters and Setters

Provide controlled access to private fields:

Basic Getters and Setters

public class Person {
    private String name;
    private int age;
    private String email;
    
    // Getter methods
    public String getName() {
        return name;
    }
    
    public int getAge() {
        return age;
    }
    
    public String getEmail() {
        return email;
    }
    
    // Setter methods with validation
    public void setName(String name) {
        if (name != null && !name.trim().isEmpty()) {
            this.name = name.trim();
        } else {
            throw new IllegalArgumentException("Name cannot be null or empty");
        }
    }
    
    public void setAge(int age) {
        if (age >= 0 && age <= 150) {
            this.age = age;
        } else {
            throw new IllegalArgumentException("Age must be between 0 and 150");
        }
    }
    
    public void setEmail(String email) {
        if (email != null && email.contains("@")) {
            this.email = email.toLowerCase();
        } else {
            throw new IllegalArgumentException("Invalid email format");
        }
    }
}

Advanced Getters and Setters

public class Temperature {
    private double celsius;
    
    public double getCelsius() {
        return celsius;
    }
    
    public void setCelsius(double celsius) {
        if (celsius < -273.15) {
            throw new IllegalArgumentException("Temperature cannot be below absolute zero");
        }
        this.celsius = celsius;
    }
    
    // Computed properties
    public double getFahrenheit() {
        return (celsius * 9.0 / 5.0) + 32;
    }
    
    public void setFahrenheit(double fahrenheit) {
        setCelsius((fahrenheit - 32) * 5.0 / 9.0);
    }
    
    public double getKelvin() {
        return celsius + 273.15;
    }
    
    public void setKelvin(double kelvin) {
        setCelsius(kelvin - 273.15);
    }
}

Immutable Objects

Objects whose state cannot be modified after creation:

public final class ImmutablePoint {
    private final double x;
    private final double y;
    
    public ImmutablePoint(double x, double y) {
        this.x = x;
        this.y = y;
    }
    
    public double getX() {
        return x;
    }
    
    public double getY() {
        return y;
    }
    
    // No setters - object is immutable
    
    // Methods that return new instances instead of modifying
    public ImmutablePoint translate(double dx, double dy) {
        return new ImmutablePoint(x + dx, y + dy);
    }
    
    public double distanceFrom(ImmutablePoint other) {
        double dx = x - other.x;
        double dy = y - other.y;
        return Math.sqrt(dx * dx + dy * dy);
    }
    
    @Override
    public String toString() {
        return "Point(" + x + ", " + y + ")";
    }
    
    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null || getClass() != obj.getClass()) return false;
        ImmutablePoint point = (ImmutablePoint) obj;
        return Double.compare(point.x, x) == 0 && Double.compare(point.y, y) == 0;
    }
    
    @Override
    public int hashCode() {
        return Objects.hash(x, y);
    }
}

Immutable Collections

import java.util.*;

public final class ImmutableStudent {
    private final String name;
    private final int id;
    private final List<String> courses;
    private final Map<String, Double> grades;
    
    public ImmutableStudent(String name, int id, List<String> courses, Map<String, Double> grades) {
        this.name = name;
        this.id = id;
        // Create defensive copies to ensure immutability
        this.courses = courses != null ? Collections.unmodifiableList(new ArrayList<>(courses)) 
                                      : Collections.emptyList();
        this.grades = grades != null ? Collections.unmodifiableMap(new HashMap<>(grades))
                                     : Collections.emptyMap();
    }
    
    public String getName() {
        return name;
    }
    
    public int getId() {
        return id;
    }
    
    public List<String> getCourses() {
        return courses; // Already unmodifiable
    }
    
    public Map<String, Double> getGrades() {
        return grades; // Already unmodifiable
    }
    
    public double getGPA() {
        return grades.values().stream()
                     .mapToDouble(Double::doubleValue)
                     .average()
                     .orElse(0.0);
    }
}

Real-World Examples

1. User Account System

public class UserAccount {
    private String username;
    private String hashedPassword;
    private String email;
    private boolean isActive;
    private Date lastLoginDate;
    private int loginAttempts;
    private static final int MAX_LOGIN_ATTEMPTS = 3;
    
    public UserAccount(String username, String password, String email) {
        setUsername(username);
        setPassword(password);
        setEmail(email);
        this.isActive = true;
        this.loginAttempts = 0;
    }
    
    public String getUsername() {
        return username;
    }
    
    public void setUsername(String username) {
        if (username == null || username.trim().length() < 3) {
            throw new IllegalArgumentException("Username must be at least 3 characters");
        }
        this.username = username.trim().toLowerCase();
    }
    
    // Password is write-only (no getter for security)
    public void setPassword(String password) {
        if (password == null || password.length() < 8) {
            throw new IllegalArgumentException("Password must be at least 8 characters");
        }
        this.hashedPassword = hashPassword(password);
    }
    
    public boolean verifyPassword(String password) {
        return hashedPassword.equals(hashPassword(password));
    }
    
    public String getEmail() {
        return email;
    }
    
    public void setEmail(String email) {
        if (email == null || !email.matches("^[A-Za-z0-9+_.-]+@(.+)$")) {
            throw new IllegalArgumentException("Invalid email format");
        }
        this.email = email.toLowerCase();
    }
    
    public boolean isActive() {
        return isActive;
    }
    
    public Date getLastLoginDate() {
        return lastLoginDate != null ? new Date(lastLoginDate.getTime()) : null;
    }
    
    public boolean authenticate(String password) {
        if (!isActive) {
            throw new IllegalStateException("Account is deactivated");
        }
        
        if (loginAttempts >= MAX_LOGIN_ATTEMPTS) {
            isActive = false;
            throw new IllegalStateException("Account locked due to too many failed attempts");
        }
        
        if (verifyPassword(password)) {
            loginAttempts = 0;
            lastLoginDate = new Date();
            return true;
        } else {
            loginAttempts++;
            return false;
        }
    }
    
    public void resetLoginAttempts() {
        this.loginAttempts = 0;
    }
    
    public void activate() {
        this.isActive = true;
        this.loginAttempts = 0;
    }
    
    public void deactivate() {
        this.isActive = false;
    }
    
    // Private helper method
    private String hashPassword(String password) {
        // Simplified hashing - in real applications, use proper hashing
        return Integer.toString(password.hashCode());
    }
}

2. Shopping Cart System

public class ShoppingCart {
    private List<CartItem> items;
    private String customerId;
    private double discountPercentage;
    private static final double MAX_DISCOUNT = 50.0;
    
    public ShoppingCart(String customerId) {
        this.customerId = customerId;
        this.items = new ArrayList<>();
        this.discountPercentage = 0.0;
    }
    
    public String getCustomerId() {
        return customerId;
    }
    
    public List<CartItem> getItems() {
        // Return defensive copy to prevent external modification
        return new ArrayList<>(items);
    }
    
    public int getItemCount() {
        return items.size();
    }
    
    public double getDiscountPercentage() {
        return discountPercentage;
    }
    
    public void setDiscountPercentage(double discountPercentage) {
        if (discountPercentage < 0 || discountPercentage > MAX_DISCOUNT) {
            throw new IllegalArgumentException("Discount must be between 0 and " + MAX_DISCOUNT);
        }
        this.discountPercentage = discountPercentage;
    }
    
    public void addItem(String productId, String productName, double price, int quantity) {
        if (quantity <= 0) {
            throw new IllegalArgumentException("Quantity must be positive");
        }
        if (price < 0) {
            throw new IllegalArgumentException("Price cannot be negative");
        }
        
        // Check if item already exists
        for (CartItem item : items) {
            if (item.getProductId().equals(productId)) {
                item.increaseQuantity(quantity);
                return;
            }
        }
        
        // Add new item
        items.add(new CartItem(productId, productName, price, quantity));
    }
    
    public boolean removeItem(String productId) {
        return items.removeIf(item -> item.getProductId().equals(productId));
    }
    
    public void updateItemQuantity(String productId, int newQuantity) {
        if (newQuantity <= 0) {
            removeItem(productId);
            return;
        }
        
        for (CartItem item : items) {
            if (item.getProductId().equals(productId)) {
                item.setQuantity(newQuantity);
                return;
            }
        }
        throw new IllegalArgumentException("Product not found in cart");
    }
    
    public double getSubtotal() {
        return items.stream()
                   .mapToDouble(item -> item.getPrice() * item.getQuantity())
                   .sum();
    }
    
    public double getDiscountAmount() {
        return getSubtotal() * (discountPercentage / 100.0);
    }
    
    public double getTotal() {
        return getSubtotal() - getDiscountAmount();
    }
    
    public void clear() {
        items.clear();
    }
    
    public boolean isEmpty() {
        return items.isEmpty();
    }
    
    // Private nested class for cart items
    private static class CartItem {
        private String productId;
        private String productName;
        private double price;
        private int quantity;
        
        public CartItem(String productId, String productName, double price, int quantity) {
            this.productId = productId;
            this.productName = productName;
            this.price = price;
            this.quantity = quantity;
        }
        
        public String getProductId() {
            return productId;
        }
        
        public String getProductName() {
            return productName;
        }
        
        public double getPrice() {
            return price;
        }
        
        public int getQuantity() {
            return quantity;
        }
        
        public void setQuantity(int quantity) {
            this.quantity = quantity;
        }
        
        public void increaseQuantity(int amount) {
            this.quantity += amount;
        }
    }
}

Benefits of Encapsulation

1. Data Protection

public class Counter {
    private int count = 0;
    private final int maxValue;
    
    public Counter(int maxValue) {
        this.maxValue = maxValue;
    }
    
    public synchronized void increment() {
        if (count < maxValue) {
            count++;
        }
    }
    
    public synchronized void decrement() {
        if (count > 0) {
            count--;
        }
    }
    
    public int getCount() {
        return count;
    }
    
    // No direct setter prevents invalid states
}

2. Implementation Flexibility

public class DataCache {
    // Internal implementation can change without affecting clients
    private Map<String, Object> cache = new HashMap<>();
    
    public void put(String key, Object value) {
        // Could switch to different cache implementation
        cache.put(key, value);
    }
    
    public Object get(String key) {
        return cache.get(key);
    }
    
    public boolean contains(String key) {
        return cache.containsKey(key);
    }
}

3. Validation and Invariants

public class Rectangle {
    private double width;
    private double height;
    
    public Rectangle(double width, double height) {
        setWidth(width);
        setHeight(height);
    }
    
    public void setWidth(double width) {
        if (width <= 0) {
            throw new IllegalArgumentException("Width must be positive");
        }
        this.width = width;
    }
    
    public void setHeight(double height) {
        if (height <= 0) {
            throw new IllegalArgumentException("Height must be positive");
        }
        this.height = height;
    }
    
    public double getWidth() {
        return width;
    }
    
    public double getHeight() {
        return height;
    }
    
    public double getArea() {
        return width * height; // Always valid due to encapsulation
    }
}

Best Practices

1. Make Fields Private by Default

public class GoodPractice {
    private String data; // Always start with private
    
    public String getData() {
        return data;
    }
    
    public void setData(String data) {
        this.data = data;
    }
}

2. Use Defensive Copying

public class SecureList {
    private List<String> items;
    
    public SecureList(List<String> items) {
        // Defensive copy in constructor
        this.items = new ArrayList<>(items);
    }
    
    public List<String> getItems() {
        // Defensive copy in getter
        return new ArrayList<>(items);
    }
    
    public void setItems(List<String> items) {
        // Defensive copy in setter
        this.items = new ArrayList<>(items);
    }
}

3. Validate in Setters

public class ValidatedData {
    private String email;
    private int age;
    
    public void setEmail(String email) {
        if (email == null || !email.contains("@")) {
            throw new IllegalArgumentException("Invalid email");
        }
        this.email = email;
    }
    
    public void setAge(int age) {
        if (age < 0 || age > 150) {
            throw new IllegalArgumentException("Invalid age");
        }
        this.age = age;
    }
}

4. Consider Immutability

public final class ImmutableConfiguration {
    private final String databaseUrl;
    private final int port;
    private final boolean sslEnabled;
    
    public ImmutableConfiguration(String databaseUrl, int port, boolean sslEnabled) {
        this.databaseUrl = databaseUrl;
        this.port = port;
        this.sslEnabled = sslEnabled;
    }
    
    public String getDatabaseUrl() { return databaseUrl; }
    public int getPort() { return port; }
    public boolean isSslEnabled() { return sslEnabled; }
}

Common Pitfalls

1. Exposing Mutable Objects

// BAD - exposes internal mutable state
public class BadExample {
    private List<String> items = new ArrayList<>();
    
    public List<String> getItems() {
        return items; // Direct reference allows external modification
    }
}

// GOOD - defensive copying
public class GoodExample {
    private List<String> items = new ArrayList<>();
    
    public List<String> getItems() {
        return new ArrayList<>(items); // Defensive copy
    }
}

2. Inconsistent State

// BAD - allows inconsistent state
public class BadRectangle {
    public double width;
    public double height;
    
    // Client can set width to negative value
}

// GOOD - enforces invariants
public class GoodRectangle {
    private double width;
    private double height;
    
    public void setWidth(double width) {
        if (width <= 0) throw new IllegalArgumentException("Width must be positive");
        this.width = width;
    }
}

Summary

Encapsulation is essential for:

  • Data Protection: Hide internal implementation details
  • Controlled Access: Use methods to control how data is accessed and modified
  • Validation: Ensure data integrity through controlled modification
  • Flexibility: Allow internal implementation changes without affecting clients
  • Security: Prevent unauthorized access to sensitive data

Key principles:

  • Make fields private by default
  • Use public methods for controlled access
  • Validate data in setters
  • Use defensive copying for mutable objects
  • Consider immutability when appropriate
  • Follow the principle of least privilege

Proper encapsulation leads to more secure, maintainable, and robust object-oriented code.