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.