1. Mutable Object Passed in Constructor
- Example: If a
DateorArrayListis 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
- 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.
- 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()).
- 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 (
LocalDateinstead ofDate,List.of()instead ofArrayList, etc.).
🔄 Deep Copy vs Unmodifiable Wrapper vs Immutable Alternative
| Approach | Description | Pros | Cons | Best Use Case |
|---|---|---|---|---|
| Deep Copy | Creates 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 Wrapper | Wraps 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 elsewhere | When you only need to prevent external changes, not full immutability |
| Immutable Alternative | Use 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:
- Deep Copy
- Unmodifiable Wrapper
- 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]}
