Deep Copy in Constructor for Immutable Classes in Java

1. Mutable Object Passed in Constructor

  • Example: If a Date or ArrayList is passed to the constructor, the caller might still hold a reference to that object and modify it later.
  • Solution: Perform deep copy inside the constructor.
public final class Student {
    private final String name;
    private final Date dob; // mutable

    public Student(String name, Date dob) {
        this.name = name;
        this.dob = new Date(dob.getTime()); // defensive copy
    }
}

✅ This ensures the Student object has its own copy, independent of the caller’s object.

2. Returning Mutable Objects in Getters

  • If you directly return the mutable field, external code can modify it.
  • Example:
public Date getDob() {
    return dob; // ❌ exposes internal state
}

Instead, return a copy:

public Date getDob() {
    return new Date(dob.getTime()); // ✅ defensive copy
}

3. Collections as Fields

  • Arrays, Lists, Maps, and Sets are mutable. If directly exposed, immutability is lost.
  • Example:
public final class Department {
  private final List < String > employees;

  public Department(List < String > employees) {
    this.employees = new ArrayList < >(employees); // deep copy
  }

  public List < String > getEmployees() {
    return new ArrayList < >(employees); // defensive copy
  }
}

✅ Both constructor and getter ensure immutability.

4. Nested Mutable Objects

  • If your mutable field contains other mutable objects (like List<Person>), you need to ensure deep copying of each element, not just the collection.
  • Example:
public final class Team {
  private final List < Person > members;

  public Team(List < Person > members) {
    this.members = new ArrayList < >();
    for (Person p: members) {
      this.members.add(new Person(p)); // assume Person has copy constructor
    }
  }

  public List < Person > getMembers() {
    List < Person > copy = new ArrayList < >();
    for (Person p: members) {
      copy.add(new Person(p));
    }
    return copy;
  }
}

✅ Each Person is copied, ensuring no shared reference.

⚠️ What to Be Careful About

  1. Shallow Copy vs Deep Copy
    • Shallow Copy: Copies only the reference (object identity remains shared).
    • Deep Copy: Copies the actual object state, ensuring no external modification.
  2. Performance Overhead
    • Deep copying collections or nested objects can be costly in terms of memory and CPU.
    • If immutability isn’t critical, consider alternatives like unmodifiable collections (Collections.unmodifiableList()).
  3. Unmodifiable Wrappers
    • Instead of deep copying, sometimes you can wrap collections in unmodifiable wrappers:

Example :

import java.util.Collections;

public final class Department {
  private final List < String > employees;

  public Department(List < String > employees) {
    this.employees = Collections.unmodifiableList(new ArrayList < >(employees));
  }

  public List < String > getEmployees() {
    return employees; // already unmodifiable
  }
}

✅ This prevents external modification but is lighter than full deep copy.

Serialization and Deserialization

  • When your immutable class is serialized and deserialized, ensure the deep copy logic is preserved (custom read methods if needed).
  • 👉 “To dive deeper into how serialization affects immutability and how to preserve thread-safety with techniques like readObject, readResolve, and the Serialization Proxy Pattern, check out our detailed guide Serialization in Immutable Classes in Java.”

Thread Safety

  • Even with defensive copies, if you mistakenly expose a mutable reference, multiple threads can still modify the object → immutability breaks.
  • Always validate return values and constructor inputs.

📌 Quick Rules

  • Always deep copy mutable objects in both constructor and getters.
  • Prefer unmodifiable wrappers if deep copy is too costly.
  • Check nested mutable objects (not just top-level fields).
  • Use immutable alternatives whenever possible (LocalDate instead of Date, List.of() instead of ArrayList, etc.).

🔄 Deep Copy vs Unmodifiable Wrapper vs Immutable Alternative

ApproachDescriptionProsConsBest Use Case
Deep CopyCreates a completely new copy of the mutable object (including nested objects).✅ Guarantees full immutability
✅ Safe from external modification
❌ Performance overhead (memory & CPU)
❌ Complex for deep/nested structures
When you must protect complex mutable data (e.g., deep object graphs, List<Person>)
Unmodifiable WrapperWraps a mutable object in an unmodifiable view (Collections.unmodifiableList()).✅ Lightweight
✅ Prevents modification through returned collection
✅ Faster than deep copy
❌ Underlying collection can still change if reference is modified elsewhereWhen you only need to prevent external changes, not full immutability
Immutable AlternativeUse built-in immutable types (String, LocalDate, List.of(), Map.of()).✅ No need for copying
✅ Thread-safe by design
✅ Cleaner code
❌ Not always available (older Java versions)
❌ May not cover all use cases
Prefer whenever possible (dates, simple collections, constants)

Real-World Example: Department Class with Immutable Handling

Suppose we have a Department that holds a list of employee names. We’ll implement immutability using:

  1. Deep Copy
  2. Unmodifiable Wrapper
  3. Immutable Alternative

1️⃣ Deep Copy Approach

import java.util.ArrayList;
import java.util.List;

public final class DepartmentDeepCopy {
  private final List < String > employees;

  public DepartmentDeepCopy(List < String > employees) {
    // Deep copy the list
    this.employees = new ArrayList < >(employees);
  }

  public List < String > getEmployees() {
    // Return a new copy each time
    return new ArrayList < >(employees);
  }

  @Override
  public String toString() {
    return "DepartmentDeepCopy{" + "employees=" + employees + '}';
  }
}

✅ Fully immutable, but copying occurs both in constructor and getter → overhead for large lists.

2️⃣ Unmodifiable Wrapper Approach

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public final class DepartmentUnmodifiable {
  private final List < String > employees;

  public DepartmentUnmodifiable(List < String > employees) {
    // Copy once, then wrap
    this.employees = Collections.unmodifiableList(new ArrayList < >(employees));
  }

  public List < String > getEmployees() {
    // Safe to return directly (already unmodifiable)
    return employees;
  }

  @Override
  public String toString() {
    return "DepartmentUnmodifiable{" + "employees=" + employees + '}';
  }
}

✅ Lightweight and prevents modification via returned list.
⚠️ But if the original employees list passed to the constructor is later modified, it won’t affect this class (because of the copy) → safe.

3️⃣ Immutable Alternative (Java 9+)

import java.util.List;

public final class DepartmentImmutableAlt {
  private final List < String > employees;

  public DepartmentImmutableAlt(List < String > employees) {
    // Directly create an immutable list
    this.employees = List.copyOf(employees);
  }

  public List < String > getEmployees() {
    return employees; // already immutable
  }

  @Override
  public String toString() {
    return "DepartmentImmutableAlt{" + "employees=" + employees + '}';
  }
}

✅ No need for manual copying or wrapping.
✅ Simple and efficient.
⚠️ Requires Java 9+ (List.copyOf() or List.of()).


🔎 Testing All Three

import java.util.ArrayList;
import java.util.List;

public class MainTest {
    public static void main(String[] args) {
        List<String> employees = new ArrayList<>();
        employees.add("Alice");
        employees.add("Bob");

        DepartmentDeepCopy dept1 = new DepartmentDeepCopy(employees);
        DepartmentUnmodifiable dept2 = new DepartmentUnmodifiable(employees);
        DepartmentImmutableAlt dept3 = new DepartmentImmutableAlt(employees);

        System.out.println(dept1);
        System.out.println(dept2);
        System.out.println(dept3);

        // Try modifying original list
        employees.add("Charlie");

        System.out.println("After modifying original list:");
        System.out.println(dept1); // Unchanged
        System.out.println(dept2); // Unchanged
        System.out.println(dept3); // Unchanged

        // Try modifying through getter
        try {
            dept1.getEmployees().add("David"); // modifies copy only
            dept2.getEmployees().add("David"); // throws UnsupportedOperationException
            dept3.getEmployees().add("David"); // throws UnsupportedOperationException
        } catch (Exception e) {
            System.out.println("Exception: " + e);
        }

        System.out.println("Final states:");
        System.out.println(dept1);
        System.out.println(dept2);
        System.out.println(dept3);
    }
}

✅ Output :

DepartmentDeepCopy{employees=[Alice, Bob]}
DepartmentUnmodifiable{employees=[Alice, Bob]}
DepartmentImmutableAlt{employees=[Alice, Bob]}
After modifying original list:
DepartmentDeepCopy{employees=[Alice, Bob]}
DepartmentUnmodifiable{employees=[Alice, Bob]}
DepartmentImmutableAlt{employees=[Alice, Bob]}
Exception: java.lang.UnsupportedOperationException
Final states:
DepartmentDeepCopy{employees=[Alice, Bob]}
DepartmentUnmodifiable{employees=[Alice, Bob]}
DepartmentImmutableAlt{employees=[Alice, Bob]}

Backend developer working with Java, Spring Boot, Microservices, NoSQL, and AWS. I love sharing knowledge, practical tips, and clean code practices to help others build scalable applications.

Leave a Reply

Your email address will not be published. Required fields are marked *