Shallow Copy vs Deep Copy: Key Differences

Shallow Copy vs Deep Copy

What is a Shallow Copy?

shallow copy creates a new object, but inserts references to the objects found in the original. The copied object shares the same nested objects with the original object.

# Python example of shallow copy
import copy

original = [[1, 2, 3], [4, 5, 6]]
shallow = copy.copy(original)  # or original.copy() for lists

# Modifying nested object affects both
shallow[0][0] = 'X'
print(original)  # [['X', 2, 3], [4, 5, 6]] - original is affected!
print(shallow)   # [['X', 2, 3], [4, 5, 6]]

What is a Deep Copy?

deep copy creates a new object and recursively copies all nested objects. The copied object is completely independent of the original.

# Python example of deep copy
import copy

original = [[1, 2, 3], [4, 5, 6]]
deep = copy.deepcopy(original)

# Modifying nested object only affects the copy
deep[0][0] = 'X'
print(original)  # [[1, 2, 3], [4, 5, 6]] - original unchanged!
print(deep)      # [['X', 2, 3], [4, 5, 6]]

Key Differences

1. Memory Allocation

Shallow Copy:

// JavaScript example
const original = {
    name: 'John',
    address: { city: 'New York', zip: '10001' }
};

const shallow = Object.assign({}, original);
// or using spread operator: const shallow = {...original};

console.log(shallow.address === original.address); // true - same reference!

Deep Copy:

// JavaScript deep copy (using JSON methods)
const deep = JSON.parse(JSON.stringify(original));

console.log(deep.address === original.address); // false - different objects!

2. Independence Level

Shallow Copy – Partial Independence:

// Java example with ArrayList
ArrayList<StringBuilder> original = new ArrayList<>();
original.add(new StringBuilder("Hello"));
original.add(new StringBuilder("World"));

ArrayList<StringBuilder> shallow = (ArrayList<StringBuilder>) original.clone();

// Modifying nested object
shallow.get(0).append(" Java");
System.out.println(original.get(0)); // "Hello Java" - affected!

Deep Copy – Complete Independence:

// Java deep copy implementation
ArrayList<StringBuilder> deep = new ArrayList<>();
for (StringBuilder sb : original) {
    deep.add(new StringBuilder(sb.toString()));
}

deep.get(0).append(" Java");
System.out.println(original.get(0)); // "Hello" - unaffected!

3. Performance Impact

import time
import copy

# Performance comparison
data = [[i for i in range(1000)] for j in range(1000)]

# Shallow copy - faster
start = time.time()
shallow = copy.copy(data)
print(f"Shallow copy time: {time.time() - start:.4f} seconds")

# Deep copy - slower
start = time.time()
deep = copy.deepcopy(data)
print(f"Deep copy time: {time.time() - start:.4f} seconds")

4. Handling Different Data Types

Primitive vs Reference Types:

// C# example
public class Person
{
    public string Name { get; set; }
    public Address Address { get; set; }
}

public class Address
{
    public string City { get; set; }
}

// Shallow copy
Person original = new Person 
{ 
    Name = "John", 
    Address = new Address { City = "NYC" } 
};

Person shallow = new Person 
{ 
    Name = original.Name,  // String is immutable, so this is safe
    Address = original.Address  // Reference copy - dangerous!
};

shallow.Address.City = "LA";
Console.WriteLine(original.Address.City); // "LA" - changed!

Visual Representation

Shallow Copy:
Original: [ref1] → {a: 1, b: [ref2] → [1, 2, 3]}
Copy:     [ref3] → {a: 1, b: [ref2] → [1, 2, 3]}
                              ↑
                         Same reference!

Deep Copy:
Original: [ref1] → {a: 1, b: [ref2] → [1, 2, 3]}
Copy:     [ref3] → {a: 1, b: [ref4] → [1, 2, 3]}
                              ↑
                         New reference!

When to Use Each

Use Shallow Copy When:

  1. Working with immutable nested objects
# Safe with immutable nested objects
original = {"name": "John", "age": 30, "scores": (90, 85, 88)}
shallow = original.copy()  # Tuple is immutable, so it's safe
  1. Performance is critical
// When dealing with large objects where nested mutations aren't needed
const config = {...defaultConfig};  // Fast shallow copy
  1. You want shared state
# Intentionally sharing nested objects
shared_cache = {"data": []}
client1 = {"id": 1, "cache": shared_cache}
client2 = {"id": 2, "cache": shared_cache}  # Both share same cache

Use Deep Copy When:

  1. Complete independence is required
# Creating independent copies for parallel processing
import multiprocessing

def process_data(data_copy):
    # Modify data_copy without affecting original
    data_copy['results'] = compute_results(data_copy)
    return data_copy

original_data = {"values": [[1, 2], [3, 4]], "results": None}
processes = []
for i in range(4):
    data_copy = copy.deepcopy(original_data)  # Each process gets independent copy
    p = multiprocessing.Process(target=process_data, args=(data_copy,))
    processes.append(p)
  1. Creating backups or snapshots
// State management - creating immutable snapshots
class StateManager {
    constructor() {
        this.history = [];
    }
    
    saveState(state) {
        // Deep copy to preserve exact state at this moment
        this.history.push(JSON.parse(JSON.stringify(state)));
    }
    
    undo() {
        return this.history.pop();
    }
}

Common Pitfalls and Solutions

1. Circular References

# Problem: Circular references
obj1 = {"name": "A"}
obj2 = {"name": "B"}
obj1["ref"] = obj2
obj2["ref"] = obj1

# Standard deep copy handles this
deep = copy.deepcopy(obj1)  # Works correctly!

# JSON approach fails
# json_copy = JSON.parse(JSON.stringify(obj1))  # Error: Circular reference

2. Special Objects

// Problem: Functions and special objects aren't copied by JSON
const original = {
    date: new Date(),
    regex: /test/gi,
    func: () => console.log("Hello"),
    undefined: undefined
};

const jsonCopy = JSON.parse(JSON.stringify(original));
console.log(jsonCopy); // {date: "2024-01-20T..."} - lost types!

// Solution: Custom deep copy function
function deepCopy(obj) {
    if (obj === null || typeof obj !== "object") return obj;
    if (obj instanceof Date) return new Date(obj.getTime());
    if (obj instanceof Array) return obj.map(item => deepCopy(item));
    if (obj instanceof RegExp) return new RegExp(obj);
    
    const clonedObj = {};
    for (let key in obj) {
        if (obj.hasOwnProperty(key)) {
            clonedObj[key] = deepCopy(obj[key]);
        }
    }
    return clonedObj;
}

Summary

AspectShallow CopyDeep Copy
Nested ObjectsShared referencesIndependent copies
Memory UsageLowerHigher
PerformanceFasterSlower
Modification SafetyRisk of unintended changesSafe from side effects
Use CaseFlat objects or intentional sharingComplete independence needed
Implementation ComplexitySimpleComplex for custom objects

The choice between shallow and deep copy depends on your specific needs regarding data independence, performance requirements, and the structure of your objects. Understanding these differences is crucial for avoiding bugs related to unintended object mutations and managing memory efficiently.

Leave a Reply