🔒 Section 13 · Question #14

Difference Between Shallow Copy and Deep Copy

The difference between Shallow Copy and Deep Copy lies in the depth of duplication across the object graph in heap memory:


🟢 Junior Level

30-Second Summary

The difference between Shallow Copy and Deep Copy lies in the depth of duplication across the object graph in heap memory:

  • Shallow Copy: Allocates a new parent container object, but copies only primitive field values and object references (pointers) to nested objects. Both the original object and the copy point to the exact same instances in the Heap. Mutating a nested object through the copy immediately corrupts the original!
  • Deep Copy: Recursively instantiates duplicates of both the container and all nested dependent objects throughout the object hierarchy. The original and the copy become 100% memory-isolated: modifications to one have zero effect on the other.

Practical Demonstration

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

class User {
    String name;
    Address address;

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

public class CopyDemo {
    public static void main(String[] args) {
        Address addr = new Address("Berlin");
        User user1 = new User("Alice", addr);

        // 1. Shallow Copy (Shares the same Address reference)
        User shallowCopy = new User(user1.name, user1.address);
        shallowCopy.address.city = "Munich";

        // The original object is corrupted!
        System.out.println(user1.address.city); // Prints "Munich"!

        // 2. Deep Copy (Instantiates an independent Address object)
        User deepCopy = new User(user1.name, new Address(user1.address.city));
        deepCopy.address.city = "Hamburg";

        // The original object remains completely isolated:
        System.out.println(user1.address.city); // Still prints "Munich"!
    }
}

Memory Layout (Heap Representation):

Shallow Copy:
[user1]       ──► address ──┐
                            ▼
[shallowCopy] ──► address ──► [Address Object: "Munich"] (Shared instance!)

Deep Copy:
[user1]       ──► address ──► [Address Object: "Munich"]
[deepCopy]    ──► address ──► [Address Object: "Hamburg"] (Isolated instances!)

🟡 Middle Level

Implementation Techniques in Java

The safest, fastest, and most idiomatic pattern in Java (recommended by Joshua Bloch, Effective Java Item 13):

public class Order {
    private final String id;
    private final List<OrderItem> items;

    // Deep Copy Constructor
    public Order(Order other) {
        this.id = other.id; // String is immutable; shallow copy is safe
        this.items = other.items.stream()
            .map(OrderItem::new) // Clones each element via its copy constructor
            .toList();
    }
}

2. The Cloneable Interface and Object.clone()

By default, the native JVM Object.clone() performs a shallow bitwise copy of object fields. To implement deep copying, every nested class must implement Cloneable and manually override clone():

@Override
public User clone() {
    try {
        User copy = (User) super.clone(); // Shallow bitwise copy of primitives and String
        copy.address = this.address.clone(); // Manual deep clone of mutable field
        return copy;
    } catch (CloneNotSupportedException e) {
        throw new AssertionError();
    }
}

Drawbacks: Architecturally flawed mechanism that bypasses constructors, requires error-prone type casting, and forces checked exception handling.

3. Serialization (Java IO / JSON / Kryo)

The object graph is serialized into a byte buffer and deserialized into a fresh heap graph:

// Using Apache Commons SerializationUtils:
User deepCopy = SerializationUtils.clone(user1);

Drawbacks: Massive performance degradation (50–100x slower than copy constructors), high GC allocation pressure, and mandatory Serializable interfaces.


🔴 Senior Level

Handling Circular References in Object Graphs

If an object $A$ references $B$, and $B$ references back to $A$ ($A \leftrightarrow B$):

  • Naive recursive copy constructors enter infinite recursion, crashing with a fatal StackOverflowError.
  • Production-grade deep copying algorithms (e.g., Jackson, Kryo, or custom graph duplicators) track visited instances using an identity map:
    Map<Object, Object> visited = new IdentityHashMap<>();
    
  • Before cloning an object, the algorithm checks if (visited.containsKey(original)) return visited.get(original);. This breaks circular loops and preserves exact graph topology.

High-Performance Alternative: Persistent Data Structures (Structural Sharing)

Executing an $O(N)$ deep copy on every state mutation devastates throughput in latency-sensitive systems (HFT, game engines, distributed event stores). The architectural alternative is Persistent Data Structures:

  • Implemented via balanced Hash Array Mapped Tries (HAMT) in libraries like Vavr or Clojure.
  • Modifying a collection clones only the path from the root to the modified leaf node ($O(\log_{32} N)$).
  • All unchanged branches (up to 98% of the tree) are safely shared between versions (Structural Sharing), guaranteeing immutability with near-zero allocation overhead.
Initial Version:                   After Adding New Node X:
       [Root 1]                               [Root 2]
       /      \                               /      \
    [Node A]  [Node B]                    [Node A']  [Node B] (SHARED SUBTREE!)
    /      \                              /      \
 [Leaf 1]  [Leaf 2]                   [Leaf 1]   [Node X]

🎯 Interview Cheat Sheet

Summary Comparison: Shallow vs. Deep Copy

| Dimension | Shallow Copy | Deep Copy | | :— | :— | :— | | What is Cloned | Container and pointers only | Container and all nested child objects | | Original Isolation | ❌ Partial (nested objects are shared) | ✅ Complete memory isolation | | Time Complexity | $O(N)$ container allocation | $O(V + E)$ recursive graph traversal | | When to Use | Elements are immutable (String, primitives) | Elements are mutable domain models | | StackOverflow Risk | None | High if object graph contains cycles |


4 Tricky Interview Questions

1. How do you handle circular references when implementing a deep copy manually?

Answer: A manual deep-copy algorithm must accept a traversal context: an IdentityHashMap<Object, Object> visited mapping original instances to their cloned counterparts:

  1. Before instantiating a copy, check if (visited.containsKey(obj)) return (T) visited.get(obj);.
  2. Before recursively copying fields, register the new instance: visited.put(obj, copy);.
  3. Populate fields via recursive calls deepCopy(field, visited). If a child field references back to the parent, the method retrieves the already-registered parent copy from visited, cleanly closing the cycle without infinite recursion.

2. Why does Object.clone() perform a shallow bitwise copy and bypass constructors?

Answer: Object.clone() is a native C++ method inside the JVM (JVM_Clone). It performs a low-level memory block copy (equivalent to a system memcpy), allocating a heap block of identical size and copying raw bytes. Because memory is populated directly by bitwise copying, constructors are bypassed entirely. Consequently, reference fields hold identical 64-bit heap addresses, resulting in a shallow copy.

3. What is the performance difference between deep copying via Java Serialization vs. a Copy Constructor?

Answer: Copy constructors are typically 50 to 100 times faster:

  • Java Serialization: Incurs severe overhead from reflective metadata inspection, writing stream headers, intermediate buffer allocations (ByteArrayOutputStream), and dynamic class resolution.
  • Copy Constructors: Direct, strongly-typed compiled Java code. The HotSpot C2 JIT compiler optimizes copy constructors aggressively, inlines object instantiation, and reads fields at direct memory offsets without reflection.

4. Is Shallow Copy sufficient for a collection of String or BigDecimal objects?

Answer: Yes, absolutely. String, BigDecimal, UUID, and boxed primitives are inherently immutable. Because their state can never be modified through any public API, sharing references across multiple collections or threads is completely safe. Creating a deep copy of immutable objects wastes CPU cycles and heap memory with zero architectural benefit.


Red Flags (DO NOT Say)

  • ❌ “The Object.clone() method performs a deep copy by default.” (It only performs a shallow, bitwise field copy).
  • ❌ “Always deep-copy collections of Strings to be safe.” (Strings are immutable; shallow copying is 100% safe and optimal).
  • ❌ “Serialization via JSON or Java IO is the recommended way to deep-copy in production.” (It is the slowest possible approach; use copy constructors).
  • ❌ “Copy constructors handle any arbitrary object graph out-of-the-box.” (They crash with StackOverflowError if circular references exist, unless a visited identity map is used).