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
finalfields and no setters ✓
- C) Using
privatefields
- 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 ✓