Encapsulation: Data Protection

Duration: 45 min

Encapsulation: Data Protection

Duration: 45 min

Introduction

Encapsulation hides internal implementation details and exposes only what's necessary through a controlled interface. This protects data integrity, enables validation, and allows internal changes without breaking external code. A well-encapsulated class is a "black box"—users know what it does, not how it works.

This module covers access modifiers, getters and setters, immutable classes, JavaBeans conventions, information hiding principles, and patterns for effective encapsulation. You'll learn why encapsulation matters and how to apply it consistently.

Access Modifiers and Data Hiding

Access modifiers control which code can access fields and methods. Generally, make fields private and provide public methods for controlled access.

public class StudentRecord {
    // Hide internal data
    private String studentId;
    private String name;
    private double gpa;
    private int absenceCount;
    
    public StudentRecord(String studentId, String name) {
        this.studentId = studentId;
        this.name = name;
        this.gpa = 4.0;
        this.absenceCount = 0;
    }
    
    // Getters - expose read-only access
    public String getStudentId() {
        return studentId;
    }
    
    public String getName() {
        return name;
    }
    
    public double getGpa() {
        return gpa;
    }
    
    public int getAbsenceCount() {
        return absenceCount;
    }
    
    // Setters - provide controlled write access with validation
    public void setGpa(double newGpa) {
        if (newGpa >= 0.0 && newGpa <= 4.0) {
            this.gpa = newGpa;
        } else {
            throw new IllegalArgumentException("GPA must be 0.0-4.0");
        }
    }
    
    public void recordAbsence() {
        absenceCount++;
    }
    
    // Note: studentId and name are read-only (no setters)
    
    public void displayRecord() {
        System.out.printf("ID: %s, Name: %s, GPA: %.2f, Absences: %d%n",
                         studentId, name, gpa, absenceCount);
    }
}

public class EncapsulationDemo { public static void main(String[] args) { StudentRecord student = new StudentRecord("S123", "Alice"); student.displayRecord(); // Can read fields through getters System.out.println("GPA: " + student.getGpa()); // Can modify GPA through setter with validation student.setGpa(3.8); System.out.println("Updated GPA: " + student.getGpa()); // Invalid GPA rejected try { student.setGpa(5.0); // Throws exception } catch (IllegalArgumentException e) { System.out.println("Error: " + e.getMessage()); } // Cannot directly modify fields // student.studentId = "S999"; // ERROR: private field } }

Private fields force external code to use your methods, allowing validation and side effects. If you later need to change storage or add validation, internal users won't break.

Getters and Setters

Getters (accessors) and setters (mutators) are the standard Java pattern for field access. They should be minimal—just return/set values, not execute complex logic.

public class BankAccount {
    private String accountNumber;
    private double balance;
    private String accountType;
    
    public BankAccount(String accountNumber, String accountType) {
        this.accountNumber = accountNumber;
        this.accountType = accountType;
        this.balance = 0.0;
    }
    
    // Getter: simple read access
    public double getBalance() {
        return balance;
    }
    
    // Setter with validation
    public void setBalance(double amount) {
        if (amount < 0) {
            throw new IllegalArgumentException("Balance cannot be negative");
        }
        this.balance = amount;
    }
    
    // Account number is read-only (getter only, no setter)
    public String getAccountNumber() {
        return accountNumber;
    }
    
    // Account type is read-only
    public String getAccountType() {
        return accountType;
    }
    
    // Business logic methods (not getters/setters)
    public void deposit(double amount) {
        if (amount <= 0) {
            throw new IllegalArgumentException("Deposit must be positive");
        }
        balance += amount;
        System.out.println("Deposited: $" + amount);
    }
    
    public void withdraw(double amount) {
        if (amount <= 0) {
            throw new IllegalArgumentException("Withdrawal must be positive");
        }
        if (amount > balance) {
            throw new IllegalArgumentException("Insufficient funds");
        }
        balance -= amount;
        System.out.println("Withdrew: $" + amount);
    }
    
    public void displayStatement() {
        System.out.printf("Account: %s (%s), Balance: $%.2f%n",
                         accountNumber, accountType, balance);
    }
}

public class AccessorDemo { public static void main(String[] args) { BankAccount account = new BankAccount("123456", "Checking"); // Use business methods account.deposit(1000); account.withdraw(250); account.displayStatement(); // Read-only fields accessed via getters System.out.println("Type: " + account.getAccountType()); System.out.println("Balance: $" + account.getBalance()); // Validation prevents invalid state try { account.setBalance(-500); // Rejected } catch (IllegalArgumentException e) { System.out.println("Error: " + e.getMessage()); } } }

Getters and setters follow naming conventions: getFieldName() and setFieldName(). For boolean fields, use isFieldName() instead of getFieldName(). Keep them simple; complex logic belongs in other methods.

Immutable Classes

Immutable objects cannot be changed after creation. They're inherently thread-safe and prevent accidental modification.

public final class Color {  // final prevents subclassing
    private final int red;
    private final int green;
    private final int blue;
    
    public Color(int red, int green, int blue) {
        if (red < 0 || red > 255 || green < 0 || green > 255 || blue < 0 || blue > 255) {
            throw new IllegalArgumentException("RGB values must be 0-255");
        }
        this.red = red;
        this.green = green;
        this.blue = blue;
    }
    
    // Getters only - no setters
    public int getRed() {
        return red;
    }
    
    public int getGreen() {
        return green;
    }
    
    public int getBlue() {
        return blue;
    }
    
    // Methods return new objects instead of modifying
    public Color brighter() {
        return new Color(
            Math.min(red + 50, 255),
            Math.min(green + 50, 255),
            Math.min(blue + 50, 255)
        );
    }
    
    public Color darker() {
        return new Color(
            Math.max(red - 50, 0),
            Math.max(green - 50, 0),
            Math.max(blue - 50, 0)
        );
    }
    
    @Override
    public String toString() {
        return String.format("Color(%d,%d,%d)", red, green, blue);
    }
    
    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof Color)) return false;
        Color other = (Color) obj;
        return this.red == other.red && this.green == other.green && this.blue == other.blue;
    }
}

public class ImmutableDemo { public static void main(String[] args) { Color original = new Color(100, 150, 200); System.out.println(original); // Create new objects instead of modifying Color bright = original.brighter(); System.out.println("Brighter: " + bright); System.out.println("Original unchanged: " + original); // Immutable objects are safe to share Color shared = original; System.out.println("Shared reference still same: " + shared); } }

Make immutable classes with final fields, no setters, validation in constructors, and methods that return new objects. String, Integer, and other wrapper classes are immutable.

JavaBeans Convention

JavaBeans is a standard convention for Java classes: properties with getters/setters, no-arg constructor, serializable.

public class Person {
    // Properties with getters/setters
    private String firstName;
    private String lastName;
    private int age;
    private String email;
    
    // No-arg constructor (required for JavaBeans)
    public Person() {
        this.firstName = "";
        this.lastName = "";
        this.age = 0;
        this.email = "";
    }
    
    // Parameterized constructor
    public Person(String firstName, String lastName, int age) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.age = age;
        this.email = "";
    }
    
    // Getter/Setter pattern
    public String getFirstName() {
        return firstName;
    }
    
    public void setFirstName(String firstName) {
        if (firstName != null && !firstName.isEmpty()) {
            this.firstName = firstName;
        }
    }
    
    public String getLastName() {
        return lastName;
    }
    
    public void setLastName(String lastName) {
        if (lastName != null && !lastName.isEmpty()) {
            this.lastName = lastName;
        }
    }
    
    public int getAge() {
        return age;
    }
    
    public void setAge(int age) {
        if (age >= 0 && age <= 150) {
            this.age = age;
        }
    }
    
    public String getEmail() {
        return email;
    }
    
    public void setEmail(String email) {
        if (email != null && email.contains("@")) {
            this.email = email;
        }
    }
    
    // Computed property (derived from other fields)
    public String getFullName() {
        return firstName + " " + lastName;
    }
    
    @Override
    public String toString() {
        return "Person{" + getFullName() + ", age=" + age + ", email=" + email + '}';
    }
}

public class JavaBeansDemo { public static void main(String[] args) { // JavaBeans typically used with reflection/tools Person person = new Person("John", "Doe", 30); person.setEmail("john@example.com"); System.out.println(person); // Properties can be accessed by name System.out.println("Full name: " + person.getFullName()); } }

JavaBeans convention makes classes work with tools and frameworks. Most Java tools (Swing, Spring, Hibernate) expect JavaBeans patterns.

Information Hiding Principles

Hide not just fields, but also internal algorithms and complexity.

public class SecureString {
    private String value;
    private static final String MASK = "*";
    
    public SecureString(String value) {
        this.value = value;
    }
    
    // Never expose internal value directly
    public String getValue() {
        return value;  // In real code, encrypt this
    }
    
    // Only expose masked version
    public String getMaskedValue() {
        if (value == null || value.length() < 4) {
            return MASK;
        }
        return MASK + value.substring(value.length() - 2);
    }
    
    // Length is safe to expose
    public int length() {
        return value.length();
    }
    
    // Expose computed properties, not implementation
    public boolean matches(String other) {
        return value.equals(other);
    }
    
    @Override
    public String toString() {
        return getMaskedValue();  // Safe default
    }
}

public class InformationHidingDemo { public static void main(String[] args) { SecureString password = new SecureString("mySecurePass123"); System.out.println("Masked: " + password.getMaskedValue()); System.out.println("Display: " + password); System.out.println("Length: " + password.length()); } }

Hide implementation details. Let users interact through public methods. This enables internal changes—if storage or algorithms change, users won't know.

Property Management and Validation

Complex Validation Logic

public class ComplexValidation {
    static class EmailValidator {
        private String email;
        
        public void setEmail(String email) {
            if (!isValidEmail(email)) {
                throw new IllegalArgumentException("Invalid email format");
            }
            this.email = email;
        }
        
        public String getEmail() {
            return email;
        }
        
        private boolean isValidEmail(String email) {
            // Simplified validation
            return email.matches("[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}");
        }
    }
    
    static class AgeValidator {
        private int age;
        
        public void setAge(int age) {
            if (age < 0 || age > 150) {
                throw new IllegalArgumentException("Age must be between 0 and 150");
            }
            this.age = age;
        }
        
        public int getAge() {
            return age;
        }
    }
}

Encapsulation in Collections

import java.util.*;

public class CollectionEncapsulation { static class UserGroup { private List members = new ArrayList<>(); // Return unmodifiable copy public List getMembers() { return Collections.unmodifiableList(members); } // Controlled modification public void addMember(String name) { if (!name.isEmpty() && !members.contains(name)) { members.add(name); } } public void removeMember(String name) { members.remove(name); } public boolean hasMember(String name) { return members.contains(name); } } static class BadEncapsulation { public List members = new ArrayList<>(); // Anyone can call members.add(), members.clear(), etc. } }

Encapsulation Patterns for Complex Objects

Wrapper Objects

public class WrapperPattern {
    static class Money {
        private double amount;
        private String currency;
        
        public Money(double amount, String currency) {
            if (amount < 0) throw new IllegalArgumentException("Amount must be positive");
            this.amount = amount;
            this.currency = currency;
        }
        
        public Money add(Money other) {
            if (!this.currency.equals(other.currency)) {
                throw new IllegalArgumentException("Cannot add different currencies");
            }
            return new Money(this.amount + other.amount, this.currency);
        }
        
        public double getAmount() { return amount; }
        public String getCurrency() { return currency; }
    }
    
    static class Price {
        private Money basePrice;
        private double taxRate;
        
        public Price(Money basePrice, double taxRate) {
            this.basePrice = basePrice;
            this.taxRate = taxRate;
        }
        
        public Money getTaxAmount() {
            return new Money(basePrice.getAmount() * taxRate, basePrice.getCurrency());
        }
        
        public Money getTotalPrice() {
            return basePrice.add(getTaxAmount());
        }
    }
}

Immutable Object Patterns

Making Truly Immutable Objects

public class TrulyImmutable {
    public final class Color {
        private final int red;
        private final int green;
        private final int blue;
        
        public Color(int red, int green, int blue) {
            if (red < 0 || red > 255 || green < 0 || green > 255 || blue < 0 || blue > 255) {
                throw new IllegalArgumentException("RGB values must be 0-255");
            }
            this.red = red;
            this.green = green;
            this.blue = blue;
        }
        
        public int getRed() { return red; }
        public int getGreen() { return green; }
        public int getBlue() { return blue; }
        
        @Override
        public boolean equals(Object obj) {
            if (!(obj instanceof Color)) return false;
            Color other = (Color) obj;
            return this.red == other.red && this.green == other.green && this.blue == other.blue;
        }
        
        @Override
        public int hashCode() {
            return java.util.Objects.hash(red, green, blue);
        }
        
        @Override
        public String toString() {
            return String.format("Color(%d,%d,%d)", red, green, blue);
        }
    }
}

Information Hiding in Practice

Hiding Implementation Complexity

public class ComplexityHiding {
    static class Database {
        // Hidden connection pool
        private java.util.List connectionPool = new java.util.ArrayList<>();
        
        // Users don't need to know about connection management
        public void executeQuery(String sql) {
            java.sql.Connection conn = getConnection();
            try {
                // Execute query
                System.out.println("Executing: " + sql);
            } finally {
                releaseConnection(conn);
            }
        }
        
        private java.sql.Connection getConnection() {
            // Connection pool management hidden
            return null;  // Simplified
        }
        
        private void releaseConnection(java.sql.Connection conn) {
            // Return to pool
        }
    }
}

Exposing Only Necessary Interfaces

public class MinimalPublicInterface {
    public interface PaymentProcessor {
        boolean process(double amount);
        String getStatus();
    }
    
    // Internal implementation hidden
    static class StripeProcessor implements PaymentProcessor {
        @Override
        public boolean process(double amount) {
            // Complex Stripe API interactions hidden
            System.out.println("Processing $" + amount + " via Stripe");
            return true;
        }
        
        @Override
        public String getStatus() {
            return "Stripe integration active";
        }
        
        // Internal methods not exposed
        private void validateAPIKey() { }
        private void handleWebhooks() { }
        private void reconcileTransactions() { }
    }
    
    // Factory provides payment processor
    public static PaymentProcessor createPaymentProcessor() {
        return new StripeProcessor();
    }
}

Encapsulation and Thread Safety

public class ThreadSafeEncapsulation {
    static class Counter {
        private int value;  // Private and final
        
        public synchronized int increment() {
            return ++value;
        }
        
        public synchronized int decrement() {
            return --value;
        }
        
        public synchronized int getValue() {
            return value;
        }
    }
    
    // Using java.util.concurrent
    static class ModernCounter {
        private java.util.concurrent.atomic.AtomicInteger value = 
            new java.util.concurrent.atomic.AtomicInteger(0);
        
        public int increment() {
            return value.incrementAndGet();
        }
        
        public int decrement() {
            return value.decrementAndGet();
        }
        
        public int getValue() {
            return value.get();
        }
    }
}

Real-World Example: Secure Password Storage

public class SecurePasswordStorage {
    static class User {
        private String username;
        private String passwordHash;  // Never store plain text!
        
        public User(String username, String password) {
            this.username = username;
            this.passwordHash = hashPassword(password);
        }
        
        // Never expose password
        public boolean verifyPassword(String password) {
            String inputHash = hashPassword(password);
            return constantTimeEquals(inputHash, this.passwordHash);
        }
        
        public String getUsername() {
            return username;
        }
        
        // No password getter!
        
        private String hashPassword(String password) {
            // In real code: use bcrypt, Argon2, or similar
            return Integer.toHexString(password.hashCode());
        }
        
        private boolean constantTimeEquals(String a, String b) {
            // Prevent timing attacks
            byte[] aBytes = a.getBytes();
            byte[] bBytes = b.getBytes();
            int result = 0;
            for (int i = 0; i < aBytes.length && i < bBytes.length; i++) {
                result |= aBytes[i] ^ bBytes[i];
            }
            return result == 0 && aBytes.length == bBytes.length;
        }
    }
}

Key Takeaways

1. Encapsulation hides internal details through private fields and public methods 2. Getters/setters provide controlled access with validation 3. Make fields private; only expose what's necessary 4. Read-only fields have getters but no setters 5. Immutable classes use final fields and no setters 6. JavaBeans convention: no-arg constructor, getters/setters for properties 7. Hide algorithms, not just fields—expose intent, not implementation 8. Return copies of mutable fields to prevent external modification 9. Encapsulation enables internal changes without breaking external code 10. Use immutable wrapper objects to protect internal state 11. Minimal public interface reduces coupling and increases flexibility 12. Encapsulation is essential for thread-safe concurrent code

Quiz

Question 1: What should be the access level of most fields?

  • A) public
  • B) protected
  • C) private ✓
  • D) package-private

Question 2: Why use getters and setters?

  • A) Required by Java
  • B) Provides validation and control ✓
  • C) Improves performance
  • D) Required only for interfaces

Question 3: What makes a class immutable?

  • A) Making all fields public
  • B) Using final fields and no setters ✓
  • C) Using private fields
  • D) Adding many getters

Question 4: Which naming convention is correct for boolean fields?

  • A) getActive()
  • B) isActive()
  • C) active()
  • D) getIsActive()

Question 5: What's the benefit of information hiding?

  • A) Prevents copying
  • B) Allows internal changes without breaking clients ✓
  • C) Makes code faster
  • D) Required for inheritance

Question 6: In JavaBeans pattern, what's required?

  • A) No-arg constructor ✓
  • B) All fields must be private ✓
  • C) Getter/setter methods ✓
  • D) All of the above ✓