Skip to Content
Course content

307: Immutability and Defensive Copying

Click on the "Edit" button in the top corner of the screen to edit your slide content.

A few years ago, I was reviewing a PR for a junior dev who had built a UserSession class. It had a final List<String> permissions field. In his mind, because the field was marked final, the session was immutable. He was proud of it. But during the review, I noticed he was returning that list directly in the getter. I wrote a tiny test case where I grabbed the permissions list from a session object and called .clear() on it. Suddenly, the user had zero permissions, and the UserSession object—which was supposed to be a read-only snapshot—had been mutated from the outside. He spent the next hour staring at the screen, wondering how a final field could possibly change.

The Illusion of the Final Keyword

This is the most common trap developers fall into when trying to implement immutability. You have to distinguish between the reference being immutable and the object being immutable. When you mark a List as final, you're telling Java that the variable will always point to the same list instance. You aren't telling Java that the contents of that list cannot change.

True immutability means that once an object is constructed, its state cannot be altered. This is a superpower in multi-threaded environments because you don't have to worry about synchronization or race conditions. If an object can't change, it's inherently thread-safe. But achieving this in Java requires more than just a few final keywords; it requires a strict policy on how data enters and leaves your class.

public class UserSession {
    private final List<String> roles;

    public UserSession(List<String> roles) {
        // DANGER: We are storing a reference to the list passed in!
        this.roles = roles; 
    }

    public List<String> getRoles() {
        // DANGER: We are giving away a reference to our internal state!
        return roles; 
    }
}

In the example above, the UserSession is a sieve. If the caller modifies the list they passed into the constructor, the session changes. If the caller modifies the list they get back from the getter, the session changes. This is where defensive copying comes in.

Closing the Leaks with Defensive Copies

Defensive copying is the practice of creating a new copy of a mutable object when it enters or leaves your class. Think of it as a security checkpoint. You don't let the outside world touch your internal data; you give them a photocopy instead.

First, look at the constructor. Instead of assigning the parameter directly to your field, wrap it in a new collection. This ensures that if the original creator of the list changes it later, your object remains unaffected. Second, look at the getter. Never return a mutable reference. You can either return a new copy of the list or, better yet, wrap it in an unmodifiable view using List.copyOf() (introduced in Java 10) or Collections.unmodifiableList().

public class UserSession {
    private final List<String> roles;

    public UserSession(List<String> roles) {
        // Defensive copy on input: we create our own private version
        this.roles = List.copyOf(roles); 
    }

    public List<String> getRoles() {
        // Returning an unmodifiable view or a copy
        return roles; // List.copyOf returns an immutable list already
    }
}

I'll give you a pro tip: be especially careful with java.util.Date. It's one of the oldest "mistakes" in the Java API because it's mutable. If you're working with legacy code using Date, you must defensively copy it every single time it moves across a class boundary. If you're using the modern java.time API (like LocalDate or Instant), you're in luck—those classes were designed from the ground up to be immutable, so you can pass them around freely without fear.




📋 Practical Task

Hardening the PaymentProfile Ledger

You are auditing a financial module and found a security flaw in the PaymentProfile class. Currently, anyone with a reference to a PaymentProfile can maliciously or accidentally alter the transaction history because the class leaks its internal ArrayList.

Your Task: Refactor the following code to ensure that PaymentProfile is truly immutable. You must implement defensive copying in the constructor and ensure the getter does not allow external modification of the list.

import java.util.*;

public class PaymentProfile {
    private final String accountHolder;
    private final List<Double> transactionHistory;

    public PaymentProfile(String accountHolder, List<Double> transactions) {
        this.accountHolder = accountHolder;
        this.transactionHistory = transactions;
    }

    public String getAccountHolder() {
        return accountHolder;
    }

    public List<Double> getTransactionHistory() {
        return transactionHistory;
    }

    public static void main(String[] args) {
        List<Double> myTxs = new ArrayList<>();
        myTxs.add(100.0);
        myTxs.add(50.0);

        PaymentProfile profile = new PaymentProfile("Jane Doe", myTxs);
        
        // This should NOT be possible in a truly immutable class:
        myTxs.add(999999.0); 
        
        System.out.println("History size: " + profile.getTransactionHistory().size());
        // If the size is 3, your implementation is still leaking!
    }
}
Rating
0 0

There are no comments for now.

to be the first to leave a comment.