
What is a Shallow Copy?
A 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?
A 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:
- 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
- Performance is critical
// When dealing with large objects where nested mutations aren't needed
const config = {...defaultConfig}; // Fast shallow copy
- 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:
- 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)
- 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
Aspect | Shallow Copy | Deep Copy |
---|---|---|
Nested Objects | Shared references | Independent copies |
Memory Usage | Lower | Higher |
Performance | Faster | Slower |
Modification Safety | Risk of unintended changes | Safe from side effects |
Use Case | Flat objects or intentional sharing | Complete independence needed |
Implementation Complexity | Simple | Complex 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.