PrepZone Logo
PrepZone

Shallow and Deep Copies

What clone() really does, why it surprises people, and safer ways to copy an object.

Why this matters

  • The aliasing bug this causes is among the hardest to trace: you copy an object, change the copy, and the original changes too.
  • Cloneable is one of the JDK's acknowledged design mistakes, and knowing why is more useful than knowing how to use it.
  • Defensive copying is the practical skill underneath all of it, and it protects encapsulation at every API boundary.
Caller
cartholds reference #A1
Method parameter
inputcopy of reference #A1
Cart object #A1items = [book]

input.add(pen) is visible to the caller. input = new Cart() is not — it only repoints the copy.

Java copies the value in the variable. For an object that value is the reference, so the method can change the object's fields but cannot repoint the caller's variable.

Shallow versus deep

Java
class Address {
    String city;
    Address(String city) { this.city = city; }
}

class Person {
    String name;
    Address address;          // a reference to a mutable object
    Person(String name, Address address) {
        this.name = name;
        this.address = address;
    }
}
Java
Person original = new Person("Ana", new Address("Pune"));

Person shallow = new Person(original.name, original.address);   // shares the Address
shallow.address.city = "Mumbai";
System.out.println(original.address.city);     // Mumbai — the original changed

Person deep = new Person(original.name, new Address(original.address.city));
deep.address.city = "Delhi";
System.out.println(original.address.city);     // Mumbai — independent
AspectShallow copyDeep copy
New top-level objectYesYes
Nested mutable objectsShared with the originalAlso duplicated
IndependenceOnly for primitive and immutable fieldsComplete
CostCheapProportional to the object graph
Safe whenEvery field is primitive or immutableAlways
  • New top-level object

    Shallow copyYes
    Deep copyYes
  • Nested mutable objects

    Shallow copyShared with the original
    Deep copyAlso duplicated
  • Independence

    Shallow copyOnly for primitive and immutable fields
    Deep copyComplete
  • Cost

    Shallow copyCheap
    Deep copyProportional to the object graph
  • Safe when

    Shallow copyEvery field is primitive or immutable
    Deep copyAlways

A shallow copy is perfectly correct when nothing it points at can change.

How clone() actually works

Object.clone() is protected and native. It allocates a new object of the same class and copies every field bit for bit — references included. To expose it you must do two things that look arbitrary.

Java
class Point implements Cloneable {          // 1. the marker interface
    int x, y;

    @Override
    public Point clone() {                   // 2. make it public, and narrow the return type
        try {
            return (Point) super.clone();
        } catch (CloneNotSupportedException e) {
            throw new AssertionError(e);      // unreachable: we do implement Cloneable
        }
    }
}

Why this is considered broken

  • Cloneable declares no methods. It is a marker that changes the behaviour of a protected method on a different class — a mechanism found nowhere else in the language.
  • clone() bypasses constructors. No validation runs, and final fields cannot be assigned, so invariants you rely on may not hold.
  • It throws a checked exception you cannot encounter. Every implementation needs the unreachable catch block above.
  • It is shallow by default, in a method whose name implies otherwise.
  • Subclasses inherit the problem. Any subclass adding a mutable field silently gets a broken clone unless it overrides again.

For a deep clone you must fix up each mutable field yourself:

Java
class Person implements Cloneable {
    String name;
    Address address;

    @Override
    public Person clone() {
        try {
            Person copy = (Person) super.clone();      // shallow
            copy.address = new Address(address.city);   // then deepen, field by field
            return copy;
        } catch (CloneNotSupportedException e) {
            throw new AssertionError(e);
        }
    }
}

Every mutable field must be remembered. Adding a field later and forgetting this method is the standard way this breaks months after it was written.

Better ways to copy

A copy constructor

The clearest option. It is ordinary code with no marker interface and no unreachable exception.

Java
public class Person {
    private final String name;
    private final Address address;

    public Person(String name, Address address) {
        this.name = name;
        this.address = address;
    }

    /** Copy constructor — deep by choice, visible in the signature. */
    public Person(Person other) {
        this.name = other.name;
        this.address = new Address(other.address);
    }
}

A static factory

The same thing with a name, which reads better at call sites and can return a subtype.

Java
public static Person copyOf(Person other) {
    return new Person(other);
}

Collection and array copies

Java
List<String> copy = new ArrayList<>(original);        // shallow: new list, same elements
List<String> snapshot = List.copyOf(original);        // shallow and unmodifiable
int[] numbers = Arrays.copyOf(source, source.length); // fine: primitives have no sharing

// Deep copy of a list of mutable objects
List<Person> deep = original.stream()
                            .map(Person::new)         // the copy constructor
                            .toList();
AspectApproachWhen to use it
Copy constructorThe default choiceClear, validated, works with final fields
Static factory copyOfWhen a name helpsReads well, can return a subtype
clone()Legacy code and arraysarray.clone() is idiomatic and fine
Serialization round-tripDeep copy of a large graphSlow; needs everything Serializable
Records and immutablesNo copying neededNothing can change, so sharing is safe
  • Copy constructor

    ApproachThe default choice
    When to use itClear, validated, works with final fields
  • Static factory copyOf

    ApproachWhen a name helps
    When to use itReads well, can return a subtype
  • clone()

    ApproachLegacy code and arrays
    When to use itarray.clone() is idiomatic and fine
  • Serialization round-trip

    ApproachDeep copy of a large graph
    When to use itSlow; needs everything Serializable
  • Records and immutables

    ApproachNo copying needed
    When to use itNothing can change, so sharing is safe

Immutability is the real answer. If nothing can be modified, there is nothing to copy.

Defensive copying at the boundary

This is where copying earns its place in production code: protecting an object's state from callers.

Java
public final class Schedule {
    private final List<LocalDate> dates;

    public Schedule(List<LocalDate> dates) {
        this.dates = List.copyOf(dates);          // copy in: the caller's list cannot reach us
    }

    public List<LocalDate> dates() {
        return dates;                              // safe: the copy is unmodifiable
    }
}

Without the copy in the constructor, the caller keeps a reference to the list and can add to it afterwards, changing a Schedule that promised to be immutable. LocalDate itself is immutable, so the elements need no copying — which illustrates the general rule nicely.

Common misreadings

  • "clone() makes a deep copy." It copies fields bit for bit, so every reference is shared.
  • "Cloneable requires you to implement a method." It declares none. Omitting it just makes super.clone() throw at run time.
  • "new ArrayList<>(other) deep-copies." It copies the list structure only.
  • "Immutable classes need copy constructors." They do not. Sharing is already safe, which is the main argument for immutability.
  • "clone() should always be avoided." array.clone() is concise, correct and idiomatic for arrays. The criticism is about Cloneable on your own classes.
  • "Serialization round-tripping is a good deep-copy trick." It works, but it is slow, requires everything to be Serializable, and silently drops transient fields.

Quick recall

Everything you need if you only revisit this box.

  • Shallow copies the object and shares its references; deep duplicates the graph. clone() is shallow.
  • Cloneable is a marker with no methods that changes a protected native method's behaviour, bypasses constructors, cannot set final fields, and forces an unreachable catch block.
  • Prefer a copy constructor or a static copyOf factory. Both are ordinary, visible code.
  • new ArrayList<>(other) and List.copyOf(other) copy the list, not the elements.
  • array.clone() is the one place clone() remains idiomatic.
  • Copy defensively at the boundary: on the way in so callers cannot reach your state, on the way out so they cannot modify it.
  • The real fix is immutability — then no copy is needed at all.

Test yourself

Answer these before moving on — recall is what makes it stick.