5.3 Defensive Programming, Immutability Patterns, and Validation

Key Takeaways

  • True immutability requires preventing subclassing (final class), declaring all fields private final, providing no mutators, and executing defensive copies on both incoming parameters and outgoing references.

  • Shallow copies duplicate collection structures but leave contained objects shared; deep defensive copies are essential whenever collections contain mutable element types.

  • Collections.unmodifiableList() produces a view wrapper that reflects subsequent changes to the underlying backing list, whereas List.of() and List.copyOf() create completely detached, null-hostile immutable collections.

  • Constructors must perform defensive copies of mutable arguments before validating invariants to protect against Time-of-Check to Time-of-Use (TOCTOU) race conditions and malicious subclass clone() overrides.

  • Properly constructed immutable objects are unconditionally thread-safe and safely published by the Java Memory Model without locks, volatile fields, or synchronization overhead.

Last updated: October 2026

Defensive Programming, Immutability Patterns, and Validation

In modern enterprise applications and concurrent multi-core architectures, mutable state represents one of the most prolific sources of software defects, data corruption, and race conditions. Defensive programming is a proactive design discipline where components safeguard their internal invariants against erroneous, untrusted, or concurrent external modifications. Central to this discipline is the pattern of immutability: constructing objects whose observable state cannot change under any circumstances after instantiation.


The Core Rules for Defining Truly Immutable Classes

Making a class truly immutable requires adherence to five foundational design rules (popularized by Bloch's Effective Java). The 1Z0-829 objective "apply encapsulation, and make objects immutable" builds directly on them:

  1. Prevent Subclassing: Declare the class as final, or use Java 17 sealed hierarchies with controlled non-subclassable permits, or make all constructors private and provide static factory methods. If a class can be subclassed, a malicious or poorly designed child class could override accessor methods or expose mutator state.
  2. Make All Fields private and final: Marking fields private enforces encapsulation by preventing direct external access. Marking them final clearly communicates intent and ensures that the Java Memory Model (JMM) safely publishes the initialized fields to all threads without explicit synchronization.
  3. Provide No Mutator Methods: Do not provide "setter" methods or any methods that modify internal state or referenced mutable instances.
  4. Ensure Exclusive Access to Mutable Components: If the class holds references to mutable objects (such as Date, arrays, StringBuilder, or mutable collections), perform defensive copies upon input (constructors) and upon output (accessors).
  5. Prevent Reference Leaks ("This" Escape): Do not permit the this reference to escape during object construction (such as passing this to a listener or thread before the constructor finishes).

Full Implementation of an Immutable Class

import java.util.*;

public final class UserSession {
    private final String sessionId;
    private final Date loginTime;
    private final List<String> permissions;

    public UserSession(String sessionId, Date loginTime, List<String> permissions) {
        // Step 1: Input validation
        this.sessionId = Objects.requireNonNull(sessionId, "sessionId must not be null");
        
        // Step 2: Defensive copies of incoming mutable objects
        Objects.requireNonNull(loginTime, "loginTime must not be null");
        this.loginTime = new Date(loginTime.getTime()); // Defensive copy of mutable Date
        
        Objects.requireNonNull(permissions, "permissions must not be null");
        this.permissions = List.copyOf(permissions); // Unmodifiable, null-hostile defensive copy
    }

    public String getSessionId() {
        return sessionId; // String is inherently immutable; safe to return directly
    }

    public Date getLoginTime() {
        // Defensive copy on getter to prevent external mutation
        return new Date(loginTime.getTime());
    }

    public List<String> getPermissions() {
        return permissions; // List.copyOf returns an unmodifiable list; safe to return
    }
}

If getLoginTime() simply returned this.loginTime, a caller could execute session.getLoginTime().setTime(0L), silently corrupting the internal state of the UserSession instance.


Shallow Copies vs. Deep Copies with Mutable Collections

A critical pitfall in defensive copying arises from the distinction between shallow copies and deep copies.

A shallow copy duplicates the container structure (such as an array or a collection), but the individual elements within the new container continue to reference the identical objects on the heap:

public class Tag {
    private String label;
    public Tag(String label) { this.label = label; }
    public String getLabel() { return label; }
    public void setLabel(String label) { this.label = label; }
}

public final class Document {
    private final List<Tag> tags;

    public Document(List<Tag> tags) {
        // Shallow copy: the list container is new, but Tag references are SHARED!
        this.tags = List.copyOf(tags);
    }

    public List<Tag> getTags() {
        return tags;
    }
}

In the example above, calling document.getTags().add(new Tag("extra")) fails because List.copyOf produces an unmodifiable list. However, calling document.getTags().get(0).setLabel("Hacked") succeeds completely! The internal label of the tag inside Document has been modified, destroying the class's immutability.

Warning

The Deep Defensive Copy Imperative Whenever a collection contains mutable reference objects, wrapping or copying the collection container with List.copyOf() or new ArrayList<>() is insufficient. True immutability requires a deep defensive copy, where every individual mutable element in the collection is independently cloned or copied during construction and retrieval.


The TOCTOU Attack and the clone() Trap

Two subtle vulnerabilities matter when constructors make defensive copies:

1. Time-of-Check to Time-of-Use (TOCTOU)

In a multithreaded program, there is a dangerous window between the time an argument is checked (validated) and the time it is used (copied). If another thread mutates the incoming object between validation and assignment, invalid data slips into the object:

// VULNERABLE: TOCTOU Race Condition
public Period(Date start, Date end) {
    if (start.after(end)) { // Time of Check
        throw new IllegalArgumentException("start after end");
    }
    // Another thread mutates 'start' HERE before copying!
    this.start = new Date(start.getTime()); // Time of Use
    this.end = new Date(end.getTime());
}

To prevent TOCTOU vulnerabilities, always make defensive copies of mutable parameters BEFORE checking their validity:

// SECURE: Copy first, then validate the copy
public Period(Date start, Date end) {
    this.start = new Date(start.getTime()); // Copy first
    this.end = new Date(end.getTime());     // Copy first

    if (this.start.after(this.end)) {        // Validate the immutable internal copy
        throw new IllegalArgumentException("start after end");
    }
}

2. The clone() Trap with Non-Final Types

Never invoke .clone() to make defensive copies of constructor parameters whose types are non-final (such as java.util.Date or non-final collections). Because the type can be subclassed, an untrusted caller could pass an instance of a malicious subclass that overrides clone() to return a reference to itself or leak private references to an external attacker. Always use a constructor or static copy factory when creating defensive copies of parameter inputs.


Unmodifiable Collection Views vs. Truly Immutable Collections

Java provides two distinct mechanisms for restricting collection mutation, and confusing them is a major exam trap:

1. Collections.unmodifiableList(list) (The View Pattern)

Collections.unmodifiableList() creates a wrapper view around an existing collection. While the wrapper throws UnsupportedOperationException on attempts to call add(), remove(), or clear(), it remains actively linked to the original underlying backing list:

List<String> rawList = new ArrayList<>();
rawList.add("Red");
List<String> view = Collections.unmodifiableList(rawList);

rawList.add("Blue"); // Modifying backing list directly
System.out.println(view.size()); // Prints 2! The view reflects mutations to the backing list!

Collections.unmodifiableList() is not immutable; it is merely an unmodifiable view.

2. List.of() and List.copyOf() (Truly Immutable Collections)

Introduced in Java 9 and Java 10, the static factory methods List.of(), Set.of(), Map.of(), and List.copyOf() create genuinely immutable, standalone data structures:

  • They do not wrap an external collection; their internal structure cannot be altered.
  • They are null-hostile: passing or storing null immediately throws NullPointerException.
  • List.copyOf(collection) creates an independent snapshot. If the backing collection changes later, the copy is completely unaffected.
  • Optimization: If the collection passed to List.copyOf() is already an immutable list created by List.of() or List.copyOf(), copyOf() avoids allocation and returns the exact same reference.
FeatureCollections.unmodifiableList(c)List.copyOf(c) / List.of(...)
Mutation ResistanceRejects direct mutation callsRejects direct mutation calls
Backing List MutationReflects changes made to original listCompletely independent snapshot; unaffected
Null TolerancePermitted if underlying collection permitsStrictly throws NullPointerException
Object IdentityWraps the list in a view (Java 17 returns the argument itself if it is already such a view)Returns identical instance if already a List.of/List.copyOf list

Input Validation and Compact Constructors in Records

Defensive programming demands failing fast whenever invalid arguments are detected:

  • Objects.requireNonNull(arg, "message"): Validates that a reference is non-null.
  • Objects.checkIndex(index, length): Validates that an index is within 0 <= index < length.
  • IllegalArgumentException: Thrown when an argument value violates domain rules.

With the introduction of records in Java 16, defensive copying and validation can be cleanly expressed inside compact constructors:

public record BoundedRange(int min, int max, List<String> labels) {
    // Compact constructor: no parameter list; assignments to fields are implicit!
    public BoundedRange {
        if (min > max) {
            throw new IllegalArgumentException("min (" + min + ") > max (" + max + ")");
        }
        // Defensive copy and validation of labels
        labels = List.copyOf(labels); // Reassigning component parameter performs defensive copy!
    }
}

In a compact record constructor, reassigning the parameter variable (labels = List.copyOf(labels);) automatically assigns the defensively copied collection to the record's private final component field!


Concurrency and Thread-Safety Advantages

Immutable objects offer profound benefits in multi-threaded programming:

  • Zero Synchronization: Because an immutable object's state never changes after construction, multiple threads can read it concurrently without locks, synchronized blocks, or volatile qualifiers.
  • Safe Publication via JMM: Section 17.5 of the Java Language Specification guarantees that any thread reading a reference to an immutable object with properly initialized final fields is guaranteed to see the correctly initialized values of those fields without memory barriers.
  • Safe as Map Keys and Set Elements: Immutable objects are ideal candidates for hash-based data structures (HashMap, HashSet) because their hash codes and equality contracts remain stable throughout their entire lifecycle.
Test Your Knowledge

Consider the following code snippet:

List<String> raw = new ArrayList<>();
raw.add("Alpha");
raw.add("Beta");

List<String> unmod = Collections.unmodifiableList(raw);
List<String> immut = List.copyOf(raw);

raw.add("Gamma");

System.out.print(unmod.size() + " ");
System.out.print(immut.size());

What is the output when this code is executed?

A

Throws an UnsupportedOperationException when raw.add("Gamma") is executed

B

3 2

C

3 3

D

2 2

Test Your Knowledge

Which of the following class definitions correctly implements true immutability while preventing subclassing attacks and TOCTOU vulnerabilities?

A

public class Event { private final Date timestamp; public Event(Date timestamp) { this.timestamp = new Date(timestamp.getTime()); } public Date getTimestamp() { return new Date(this.timestamp.getTime()); } }

B

public final class Event { private final Date timestamp; public Event(Date timestamp) { this.timestamp = (Date) timestamp.clone(); } public Date getTimestamp() { return (Date) this.timestamp.clone(); } }

C

public class Event { private final Date timestamp; public Event(Date timestamp) { this.timestamp = timestamp; } public Date getTimestamp() { return this.timestamp; } }

D

public final class Event { private final Date timestamp; public Event(Date timestamp) { Objects.requireNonNull(timestamp, "timestamp must not be null"); this.timestamp = new Date(timestamp.getTime()); } public Date getTimestamp() { return new Date(this.timestamp.getTime()); } }

Test Your Knowledge

Consider the following classes:

class Tag {
    String label;
    Tag(String label) { this.label = label; }
}

final class Article {
    private final List<Tag> tags;

    public Article(List<Tag> tags) {
        this.tags = List.copyOf(tags);
    }
    public List<Tag> getTags() {
        return tags;
    }
}

public class DefensiveDemo {
    public static void main(String[] args) {
        Tag t1 = new Tag("Java");
        List<Tag> list = new ArrayList<>();
        list.add(t1);

        Article article = new Article(list);
        article.getTags().get(0).label = "Kotlin";

        System.out.println(t1.label);
    }
}

What is the outcome of compiling and running this code?

A

Compilation fails because article.getTags().get(0) is an unmodifiable reference

B

Throws an UnsupportedOperationException when modifying label

C

Compiles successfully and prints Kotlin

D

Compiles successfully and prints Java

Test Your Knowledge

Consider the following code snippet:

List<String> names = new ArrayList<>();
names.add("Alice");
names.add(null);

try {
    List<String> copy = List.copyOf(names);
    System.out.print("Copied ");
} catch (NullPointerException e) {
    System.out.print("NPE_Copy ");
}

try {
    List<String> unmod = Collections.unmodifiableList(names);
    System.out.print("Unmod_" + unmod.size());
} catch (NullPointerException e) {
    System.out.print("NPE_Unmod");
}

What is the output of executing this code?

A

NPE_Copy NPE_Unmod

B

Throws an IllegalArgumentException at runtime

C

Copied Unmod_2

D

NPE_Copy Unmod_2

Sections you finish are checked off in the contents.