Back to Encapsulation & Cohesion
Q1Question 1 of 2

Why does returning a live collection break encapsulation?

Key Concepts
  • Private field plus getter returning the live collection is fake encapsulation: caller can mutate internals via the reference
  • getTransactions().add(fakeTransaction) bypasses all validation, audit logging, and notification
  • getTransactions().clear() wipes all internal state without authorization
  • Fix 1: Collections.unmodifiableList() returns a read-only view (add/clear throw UnsupportedOperationException)
  • Fix 2: new ArrayList<>(list) returns a defensive copy (caller modifies copy only, internal list unaffected)
  • Fix 3: List.copyOf() (Java 10) creates an unmodifiable copy; for full protection, make elements immutable (records)

Definition

Private field plus getter returning the live collection is fake encapsulation. The caller mutates internals via the reference. Fix: return Collections.unmodifiableList() or a defensive copy or List.copyOf().

Making a field private is not enough for encapsulation if the getter returns a reference to the live internal collection.

  • The problem: the caller receives the same list object that the class uses internally. account.getTransactions().add(fakeTransaction) adds a fake transaction bypassing all validation.
  • Fix 1: Collections.unmodifiableList(transactions) returns a read-only view. add() throws UnsupportedOperationException.
  • Fix 2: new ArrayList<>(transactions) returns a defensive copy. The caller modifies the copy, not the internal list.
  • Fix 3: List.copyOf(transactions) (Java 10) creates an unmodifiable copy (best of both worlds, no nulls allowed).
  • Full protection: make the elements immutable too (records, final fields). unmodifiableList prevents structural changes (add/remove) but not element mutation.
  • Stream alternative: return transactions.stream() so the caller can only consume, not modify.

The vault (private field) at National Bank of India is locked. But the bank gives Rajesh a key to the vault (reference via getter). Rajesh walks in and adds fake gold bars (fakeTransaction). The vault is private but its contents are modified through the leaked key. Fix 1: give Rajesh a photo of the vault contents (unmodifiable view). Fix 2: give Rajesh a copy of the contents (defensive copy). The vault stays safe.

Understanding the Concepts

Why does returning a live collection break encapsulation?

The private field is protected, but the reference to the mutable collection is leaked.

What does the problem look like?

JAVA
package com.bank.core;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;

public class Account {
    private final List<Transaction> transactions = new ArrayList<>();

    public void addTransaction(Transaction txn) {
        validate(txn); // validation: amount > 0, status = ACTIVE
        transactions.add(txn);
        auditLog("ADDED: " + txn);
    }

    // WRONG: leaky getter returns live internal list
    public List<Transaction> getTransactions() {
        return transactions; // reference to the same object
    }
}

// Caller bypasses all validation:
account.getTransactions().add(new Transaction("FAKE", new BigDecimal("-999999")));
// No validate() called. No audit log. Private field modified from outside.

account.getTransactions().clear();
// All transactions wiped. No authorization. Catastrophic.

What are the three fixes?

JAVA
// Fix 1: Collections.unmodifiableList (read-only view)
public List<Transaction> getTransactions() {
    return java.util.Collections.unmodifiableList(transactions);
    // add/remove/set/clear throw UnsupportedOperationException
}

// Fix 2: Defensive copy (new ArrayList)
public List<Transaction> getTransactions() {
    return new ArrayList<>(transactions);
    // Caller modifies the copy. Internal list unaffected.
    // Downside: creates new list every call.
}

// Fix 3: List.copyOf (Java 10): unmodifiable copy
public List<Transaction> getTransactions() {
    return List.copyOf(transactions);
    // Unmodifiable AND separate from internal list.
    // Downside: no nulls allowed.
}

// Fix 4: Stream (caller can only consume)
public java.util.stream.Stream<Transaction> transactionStream() {
    return transactions.stream();
    // Caller can filter, map, collect. Cannot modify internal list.
}

Why is unmodifiableList not enough for mutable elements?

JAVA
// If Transaction is mutable:
public class Transaction {
    private BigDecimal amount;
    public void setAmount(BigDecimal amount) { this.amount = amount; }
}

List<Transaction> view = java.util.Collections.unmodifiableList(account.getTransactions());
view.get(0).setAmount(new BigDecimal("-999")); // modifies the actual Transaction!
// unmodifiableList blocks add/remove, not element mutation.

// Full fix: make Transaction immutable (record)
public record Transaction(String txnId, BigDecimal amount, String status) {}
// view.get(0).setAmount(...) -> compile error: no setter on a record.

What is the complete protection recipe?

  1. Private field: controls who declares the reference.
  2. No direct setter for the collection: only validated methods like addTransaction().
  3. Getter returns unmodifiable or copy: prevents structural mutations.
  4. Immutable elements (records): prevents element-level mutations.

What are common mistakes?

The most common mistake is assuming private means fully encapsulated.

What should a senior engineer know?

  • In Spring Boot, always return List.copyOf() or Collections.unmodifiableList() from getters that expose internal collections.
  • Records automatically make all fields private final with no setters, providing encapsulation by default.
  • JPA entities often have mutable collections; use defensive copies in DTO conversions to avoid leaking JPA-managed lists.

Visual Overview

DIAGRAM
  BROKEN ENCAPSULATION
  ┌────────────────────────┐
  │ Account                 │
  │ private List<Txn> txns  │
  │                         │
  │ getTransactions()       │
  │   return txns; ←─────── │── LEAKED REFERENCE
  └────────────────────────┘
       │
       ▼
  caller.getTransactions().add(fake)  ← bypasses validation
  caller.getTransactions().clear()    ← wipes all data

  FIXED ENCAPSULATION
  ┌────────────────────────┐
  │ Account                 │
  │ private List<Txn> txns  │
  │                         │
  │ getTransactions()       │
  │   return List.copyOf()  │── SAFE: unmodifiable copy
  │                         │
  │ addTransaction(txn)     │── ONLY way to modify
  │   validate(txn)         │
  │   txns.add(txn)         │
  │   auditLog(txn)         │
  └────────────────────────┘

Key Takeaway

A private field with a getter that returns the live internal collection is fake encapsulation. The caller receives a reference to the same mutable object: getTransactions().add(fake) bypasses all validation, and getTransactions().clear() wipes the internal state. The field is private but its contents are fully accessible through the leaked reference.

Three fixes: Collections.unmodifiableList() returns a read-only view (add and clear throw UnsupportedOperationException). new ArrayList<>(transactions) returns a defensive copy (caller modifies the copy only). List.copyOf() combines both (unmodifiable and a separate copy). For full protection, make the elements immutable too: records with final fields ensure that even element-level mutations are impossible.

Remember one sentence: private field plus a getter returning the live collection is fake encapsulation; return List.copyOf() or an unmodifiable view, and make elements immutable with records for true protection.

---

Share
Use ← → arrow keys to navigate