Defensive Copying in Java

Naveen AnamaneniUpdated · 4 min read

A class can make its list field private final and still lose control of it. If the constructor stores the caller's list as is, the caller still holds a reference and can change it later. If a getter returns the internal list, anyone can call clear() on it. Defensive copying closes both holes: copy mutable objects when they come in and when they go out, so the class owns the only reference to its internals.

The Leak and the Fix

Copy on the way in; hand out nothing that can be changed
Teams.java▶ Try it
import java.util.ArrayList;
import java.util.List;
 
public class Teams {
    static final class LeakyTeam {
        private final List<String> members;
        LeakyTeam(List<String> members) { this.members = members; }
        List<String> members() { return members; }
    }
 
    static final class SafeTeam {
        private final List<String> members;
        SafeTeam(List<String> members) { this.members = List.copyOf(members); }
        List<String> members() { return members; }
    }
 
    public static void main(String[] args) {
        List<String> names = new ArrayList<>(List.of("Navika", "Srihitha"));
        LeakyTeam leaky = new LeakyTeam(names);
        SafeTeam safe = new SafeTeam(names);
 
        names.add("Someone else");
        System.out.println("Leaky after caller's change: " + leaky.members());
        System.out.println("Safe after caller's change:  " + safe.members());
 
        leaky.members().clear();
        System.out.println("Leaky after clear(): " + leaky.members());
        try {
            safe.members().clear();
        } catch (UnsupportedOperationException e) {
            System.out.println("Safe team refused clear()");
        }
    }
}
Output
Leaky after caller's change: [Navika, Srihitha, Someone else]
Safe after caller's change:  [Navika, Srihitha]
Leaky after clear(): []
Safe team refused clear()

How it works

  1. LeakyTeam stores the same list the caller passed in. When the caller later adds to names, the team changes too.
  2. Its getter hands out that same list, so leaky.members().clear() wipes the team.
  3. SafeTeam stores List.copyOf(members): a new, unmodifiable list. The caller's later changes don't reach it.
  4. Because the stored copy is unmodifiable, the getter can return it directly; clear() throws instead of damaging the team.

How to Copy Common Types

TypeCopy on the way inSafe on the way out
List, Set, MapList.copyOf(list), Set.copyOf, Map.copyOfReturn the unmodifiable copy itself
Mutable list you'll keep changing insidenew ArrayList<>(list)Collections.unmodifiableList(list) or List.copyOf(list)
Arraysarray.clone() or Arrays.copyOf(array, n)Return array.clone()
java.util.Date (legacy, mutable)new Date(date.getTime())Return a new Date
LocalDate, String, IntegerNo copy needed: they're immutableReturn directly

Copy Before You Check

When a constructor validates a mutable argument, copy it first and validate the copy. Otherwise another thread could change the original between the check and the copy:

Order matters
Exam(int[] marks) {
    this.marks = marks.clone();          // 1. copy
    for (int m : this.marks) {           // 2. validate the copy
        if (m < 0 || m > 100) {
            throw new IllegalArgumentException("Bad mark " + m);
        }
    }
}

List.copyOf also rejects null elements with a NullPointerException, which is often exactly the check you want.

Common Mistakes

Assuming final makes a list unchangeable. final stops reassignment, not add or clear.

Copying in the constructor but not in the getter (or the other way round). Both doors must be closed.

Defensive copies of immutable objects. Copying a String or LocalDate is wasted work.

Practice

  1. Easy: Write a Playlist class whose constructor takes a List<String> of songs. Show that changing the caller's list afterwards doesn't affect the playlist.
  2. Harder: Write a class Schedule with a int[] slots field. Add a constructor and a getter that both copy, then try to break it from main and show that you can't.

Quick check: Defensive copying

4 questions
1A constructor stores the caller's ArrayList directly. What can the caller do later?
2What does List.copyOf(list) return?
3Which type needs no defensive copy?
4When validating a mutable argument, you should:

Key Takeaways

Was this tutorial helpful?

Lesson 158 of 482