skip to content

What is the difference between a shallow copy and a deep copy, and how do you implement a deep clone in Java?

level: middleimportance: must knowfreq 68%

answer

  1. Shallow: copy box, share contents
  2. Deep: recursively copy mutable contents
  3. clone() is always shallow
  4. Reassign mutable ref fields after super.clone()
  5. final fields can't be reassigned → can't deep-clone them

basics

~20 s

A shallow copy duplicates the object but its reference fields still point to the same inner objects, so changes to those inner objects are seen by both. A deep copy also copies the inner objects, so the two are fully independent.

solid answer

~50 s

Object.clone() gives a shallow copy: primitives are copied by value, but a reference field in the clone points to the *same* object as in the original. If that referenced object is mutable, the original and the clone share it — mutating it through one is visible through the other. A deep copy recursively copies mutable referenced objects so the clone is fully independent. You implement it by overriding clone(): call super.clone() to get the shallow copy, then reassign each mutable reference field to a fresh copy (e.g. clone the array or list, or clone nested Cloneable objects). Note that final fields are a problem here — you cannot reassign them after super.clone(), which is one reason copy constructors are often preferred for deep copies. Immutable referenced objects (String, Integer) need no deep copy since sharing them is safe.

code

java · 14 lines
java
class Team implements Cloneable {
    List<String> members = new ArrayList<>();

    @Override
    public Team clone() {
        try {
            Team copy = (Team) super.clone();   // shallow: shares the list
            copy.members = new ArrayList<>(this.members); // deep: independent list
            return copy;
        } catch (CloneNotSupportedException e) {
            throw new AssertionError(e); // unreachable: we implement Cloneable
        }
    }
}

go deeper

for a junior

Can state that shallow shares inner objects and deep duplicates them, and recognize the shared-list bug when shown it.

for a middle

Implements a deep clone: super.clone() then copy each mutable reference field; knows immutable fields need no copy and arrays/collections need explicit copying.

for a senior

Reasons about how deep the copy must go, identifies the final-field conflict, and decides between clone and a copy constructor based on the field shape.

for a principal

Establishes copying conventions across a codebase (immutability-first to make copies trivial), and weighs deep-copy cost vs structural sharing / persistent data structures for large graphs.

## Shallow vs deep — the core idea An object's fields fall into two kinds: **primitive** values (`int`, `long`, `boolean`, …) held directly in the object, and **references** — pointers to *other* objects on the heap. A **shallow copy** copies the object's own slots. Primitive slots get the value. Reference slots get the *pointer*, so the copy and the original both point at the **same** referenced object. A **deep copy** copies the object *and* the objects it points to (recursively, as far down as needed), so the copy shares no mutable state with the original. `Object.clone()` always produces a **shallow** copy. ## Why shallow copies bite you ```java class Team implements Cloneable { List<String> members = new ArrayList<>(); @Override public Team clone() { try { return (Team) super.clone(); } catch (CloneNotSupportedException e) { throw new AssertionError(e); } } } Team a = new Team(); a.members.add("Ann"); Team b = a.clone(); // shallow b.members.add("Bob"); // mutates the SHARED list // a.members is now ["Ann", "Bob"] too — surprise aliasing ``` Because `members` is a reference, both `a` and `b` point to one `ArrayList`. This shared, unintended aliasing is the classic shallow-clone bug. ## Implementing a deep clone Start from `super.clone()` (so you still get a correct-typed, field-copied object), then replace each *mutable* reference field with a fresh copy: ```java @Override public Team clone() { try { Team copy = (Team) super.clone(); copy.members = new ArrayList<>(this.members); // copy the list return copy; } catch (CloneNotSupportedException e) { throw new AssertionError(e); } } ``` For nested objects that are themselves `Cloneable`, call their `clone()`. For arrays, `array.clone()` is a convenient shallow array copy (still shallow on element references, so deep arrays-of-objects need element-by-element copying). The copy must be deep *all the way down* to any mutable shared state, or the bug just moves one level deeper. ## What you do NOT need to deep-copy **Immutable** referenced objects — `String`, `Integer`, `LocalDate`, any properly immutable type — are safe to share because nobody can mutate them. Deep-copying them wastes memory and effort. ## The final-field obstacle Deep cloning works by *reassigning* fields after `super.clone()`. But a `final` field cannot be reassigned, so you cannot point it at a fresh deep copy. This is a fundamental conflict between cloning and immutability and a major reason `Effective Java` recommends a **copy constructor** instead, where you can pass deep-copied values straight to the constructor and keep fields `final`. ## Mental model Shallow = copy the box, share the contents. Deep = copy the box *and* recursively copy the contents. Choose deep only for the *mutable* contents.

  • Do you need to deep-copy a String field when deep-cloning an object?
    No. String is immutable, so sharing the same instance between the original and the clone is safe — no one can mutate it. Deep-copying immutable fields only wastes memory.
  • Why do final fields make deep cloning awkward?
    Deep cloning relies on reassigning reference fields to fresh copies after super.clone() returns. A final field cannot be reassigned after the object exists, so you cannot point it at a deep copy. A copy constructor avoids this by assigning the copied value at construction time.

saying these in an interview costs you the question

  • Claiming clone() does a deep copy by default
  • Deep-copying immutable types like String unnecessarily
  • Stopping deep copy one level too shallow (nested mutable state still shared)
  • Assuming array.clone() deep-copies an array of objects (it copies element references)

context