Java Shallow vs Deep Copy
Java Shallow vs Deep Copy
Shallow copy and deep copy describe how far into an object graph a copy operation goes. A shallow copy creates a new object but populates it with the same references the original holds - so the new object and the original share every mutable object that either one points to. A deep copy creates a new object AND new copies of everything reachable from it - original and copy share nothing mutable, so changes to one cannot affect the other. The distinction sounds academic until a production bug surfaces where modifying "your copy" of an order unexpectedly changes the original, or two threads collide on shared state that was meant to be isolated per request.
What Are Shallow Copy and Deep Copy?
The difference is entirely about what happens to reference fields - fields whose type is a class (as opposed to a primitive like int or double).
ORIGINAL OBJECT - OrderSummary
+---------------------------+
| orderId : "ORD-501" | <- primitive String (immutable - safe to share)
| totalAmt : 2450.0 | <- primitive double (copied by value always)
| items : --------+ | <- reference field - POINTS TO a List object
+---------------------------+ |
v
+-------------------+
| List (3 items) |
| "Laptop Bag" |
| "USB Hub" |
| "Mouse Pad" |
+-------------------+
AFTER SHALLOW COPY:
copy
+---------------------------+
| orderId : "ORD-501" | <- same String value (harmless - String is immutable)
| totalAmt : 2450.0 | <- independent copy of the double value
| items : --------+ | <- SAME reference as original.items
+---------------------------+ |
v
+-------------------+
| List (3 items) | <- THE SAME object - shared
| "Laptop Bag" |
| "USB Hub" |
| "Mouse Pad" |
+-------------------+
copy.items.add("Charger") also adds "Charger" to original.items
AFTER DEEP COPY:
copy
+---------------------------+
| orderId : "ORD-501" |
| totalAmt : 2450.0 |
| items : --------+ | <- points to a NEW, independent List
+---------------------------+ |
v
+-------------------+
| List (3 items) | <- A DIFFERENT object
| "Laptop Bag" | with the same CONTENTS
| "USB Hub" | at the moment of copying
| "Mouse Pad" |
+-------------------+
copy.items.add("Charger") has ZERO effect on original.items
Basic Overview - Copying Primitives, Immutables, and Mutable Objects
PRIMITIVES (int, double, boolean, char, long, etc.)
Fresher view : always copied by value - the copy gets its OWN
number, completely independent of the original
Deeper view : there is no "shallow vs deep" distinction here -
a primitive field is ALWAYS independently copied,
regardless of how the copy was made
IMMUTABLE REFERENCE TYPES (String, Integer, LocalDate, BigDecimal, ...)
Fresher view : the copy holds the same object, but since the object
CANNOT change, sharing it is harmless
Deeper view : "shallow" and "deep" produce identical OBSERVABLE
results for immutable types - whether you share the
same String or copy it, neither you nor anyone else
can change what it contains, so the copy is safe
either way
MUTABLE REFERENCE TYPES (List, Map, Date, array, custom mutable classes)
Fresher view : THIS is where the choice matters. A shallow copy
shares the mutable object; a deep copy creates an
independent one. Getting it wrong here is how
"I changed my copy" bugs become "why did the
original change" bugs.
Deeper view : "deeply mutable" objects add a further level - a
List of mutable objects is itself a mutable container
of mutable elements. A deep copy needs to copy
BOTH the List AND each element inside it if those
elements are mutable too
WHEN SHALLOW IS ENOUGH:
- All mutable fields are effectively "owned" by only one party and
the copy is used in a read-only way
- The object has no mutable reference fields at all (only primitives
and immutable types)
- The shared mutable field's mutation is intentional - both the
original and copy should see changes (rare, but valid design)
WHEN DEEP IS REQUIRED:
- The copy will be used in a different context (another thread,
another request, an audit log) and must not see mutations from
the original context
- The object will be placed in a cache and the caller's original
must not be able to alter what is in the cache
- The class is meant to be immutable but holds mutable fields -
a deep copy is exactly what defensive copying achieves
Why the Distinction Matters in Real Code
The bugs that shallow-vs-deep confusion produces are subtle: the copy looks correct immediately after creation, tests pass, and the problem surfaces only when the original or the copy is mutated later - sometimes much later, by completely different code.
Three concrete scenarios where the wrong choice causes real harm:
Caching without a deep copy. A service computes an expensive result and caches it. The caller continues working with the original object and mutates a list inside it. The next caller who retrieves the "cached" result gets the mutated version, not the one that was cached - because the cache stored the same list reference the original caller later changed.
Multithreaded processing. One thread clones a request object and hands it to a worker thread for processing. Both threads now hold a shallow copy of a shared list. One thread adds items to it while the other iterates - a ConcurrentModificationException, or silent data loss, depending on timing.
Audit and history logs. An order management system records the state of an order before and after a modification. If the "before" snapshot is a shallow copy, and the modification changes a shared list inside the order, the "before" snapshot retroactively reflects the modification - making the audit log useless.
How They Work
Shallow Copy
The three most common mechanisms for a shallow copy in Java are Object.clone() with super.clone(), a copy constructor that copies reference fields directly, and System.arraycopy() for arrays. All three produce the same result: new container object, same referenced objects inside.
1// File: ShallowCopyDemo.java
2
3import java.util.ArrayList;
4import java.util.List;
5
6public class ShallowCopyDemo {
7
8 static class OrderSummary implements Cloneable {
9 String orderId;
10 double totalAmount;
11 List<String> items;
12
13 OrderSummary(String orderId, double totalAmount, List<String> items) {
14 this.orderId = orderId;
15 this.totalAmount = totalAmount;
16 this.items = items;
17 }
18
19 // Shallow copy via clone() - super.clone() copies REFERENCES as-is
20 @Override
21 public OrderSummary clone() {
22 try {
23 return (OrderSummary) super.clone();
24 } catch (CloneNotSupportedException e) {
25 throw new AssertionError(e);
26 }
27 }
28
29 // Shallow copy via copy constructor - same effect as clone() here
30 OrderSummary(OrderSummary other) {
31 this.orderId = other.orderId;
32 this.totalAmount = other.totalAmount;
33 this.items = other.items; // same reference stored directly
34 }
35
36 @Override
37 public String toString() {
38 return "OrderSummary[" + orderId + ", items=" + items + "]";
39 }
40 }
41
42 public static void main(String[] args) {
43 List<String> originalItems = new ArrayList<>(List.of("Laptop Bag", "USB Hub"));
44 OrderSummary original = new OrderSummary("ORD-501", 2450.0, originalItems);
45
46 OrderSummary shallowCopy = original.clone();
47
48 System.out.println("=== Immediately after shallow copy ===");
49 System.out.println("original : " + original);
50 System.out.println("shallowCopy: " + shallowCopy);
51 System.out.println("Same items reference? " + (original.items == shallowCopy.items));
52
53 System.out.println();
54
55 System.out.println("=== Mutating the COPY's items list ===");
56 shallowCopy.items.add("Mouse Pad");
57 System.out.println("original : " + original);
58 System.out.println("shallowCopy: " + shallowCopy);
59 System.out.println("original was changed - both refer to the SAME List object");
60 }
61}Output:
=== Immediately after shallow copy ===
original : OrderSummary[ORD-501, items=[Laptop Bag, USB Hub]]
shallowCopy: OrderSummary[ORD-501, items=[Laptop Bag, USB Hub]]
Same items reference? true
=== Mutating the COPY's items list ===
original : OrderSummary[ORD-501, items=[Laptop Bag, USB Hub, Mouse Pad]]
shallowCopy: OrderSummary[ORD-501, items=[Laptop Bag, USB Hub, Mouse Pad]]
original was changed - both refer to the SAME List object
Deep Copy
A deep copy replaces each shared mutable field with a fresh copy of its contents. The four most common techniques in Java are: overriding clone() and manually replacing each mutable field; a copy constructor that explicitly deep-copies each field; serialization-deserialization (copy everything at once, without listing fields, but with real costs); and third-party libraries like Gson or Jackson to serialize to JSON and back.
1// File: DeepCopyDemo.java
2
3import java.util.ArrayList;
4import java.util.List;
5
6public class DeepCopyDemo {
7
8 static class OrderItem {
9 String productName;
10 int quantity;
11
12 OrderItem(String productName, int quantity) {
13 this.productName = productName;
14 this.quantity = quantity;
15 }
16
17 // Deep copy of OrderItem itself - needed because the outer
18 // deep copy must copy the LIST and also each ELEMENT if
19 // the element is itself mutable
20 OrderItem deepCopy() {
21 return new OrderItem(productName, quantity);
22 }
23
24 @Override
25 public String toString() {
26 return productName + "x" + quantity;
27 }
28 }
29
30 static class Order {
31 String orderId;
32 double totalAmount;
33 List<OrderItem> items; // mutable container of mutable elements
34
35 Order(String orderId, double totalAmount, List<OrderItem> items) {
36 this.orderId = orderId;
37 this.totalAmount = totalAmount;
38 this.items = items;
39 }
40
41 // DEEP copy constructor - copies the List AND each element inside
42 Order deepCopy() {
43 List<OrderItem> copiedItems = new ArrayList<>();
44 for (OrderItem item : this.items) {
45 copiedItems.add(item.deepCopy()); // copy each mutable element
46 }
47 return new Order(this.orderId, this.totalAmount, copiedItems);
48 }
49
50 @Override
51 public String toString() {
52 return "Order[" + orderId + ", items=" + items + "]";
53 }
54 }
55
56 public static void main(String[] args) {
57 List<OrderItem> items = new ArrayList<>();
58 items.add(new OrderItem("Laptop Bag", 1));
59 items.add(new OrderItem("USB Hub", 2));
60
61 Order original = new Order("ORD-501", 2450.0, items);
62 Order deepCopy = original.deepCopy();
63
64 System.out.println("=== Immediately after deep copy ===");
65 System.out.println("original: " + original);
66 System.out.println("deepCopy: " + deepCopy);
67 System.out.println("Same items reference? " + (original.items == deepCopy.items));
68 System.out.println("Same item[0] reference? " + (original.items.get(0) == deepCopy.items.get(0)));
69
70 System.out.println();
71
72 System.out.println("=== Mutating the COPY's first item quantity ===");
73 deepCopy.items.get(0).quantity = 99;
74 deepCopy.items.add(new OrderItem("Mouse Pad", 1));
75
76 System.out.println("original: " + original);
77 System.out.println("deepCopy: " + deepCopy);
78 System.out.println("original is completely unchanged - no shared references");
79 }
80}Output:
=== Immediately after deep copy ===
original: Order[ORD-501, items=[Laptop Bag x1, USB Hub x2]]
deepCopy: Order[ORD-501, items=[Laptop Bag x1, USB Hub x2]]
Same items reference? false
Same item[0] reference? false
=== Mutating the COPY's first item quantity ===
original: Order[ORD-501, items=[Laptop Bag x1, USB Hub x2]]
deepCopy: Order[ORD-501, items=[Laptop Bag x99, USB Hub x2, Mouse Pad x1]]
original is completely unchanged - no shared references
The Three-Level Problem - Deep Mutability
Deep copy is not always "copy the list" - it depends on whether the list's elements are themselves mutable. This is worth making explicit because it is the point at which "I did copy the list" bugs appear: the list is independent, but the objects inside it are shared.
THREE LEVELS OF A MUTABLE OBJECT GRAPH:
Order <- Level 1: the outer object
List<OrderItem> items <- Level 2: a mutable container
OrderItem("Laptop Bag", qty=1) <- Level 3: a mutable element
OrderItem("USB Hub", qty=2)
COPY THE LIST BUT NOT THE ELEMENTS (one-level deep copy):
copy.items is a NEW List
copy.items.add(...) does NOT affect original.items
copy.items.get(0).quantity = 99 DOES affect original.items.get(0)
because copy.items and original.items hold the SAME OrderItem objects
COPY THE LIST AND EACH ELEMENT (two-level deep copy):
copy.items is a NEW List
each copy.items.get(N) is a NEW OrderItem
NOTHING inside copy can affect original's state
HOW DEEP TO GO?
Go as deep as there are MUTABLE types in the object graph.
Stop at any immutable type (String, Integer, LocalDate, etc.) -
those can always be shared safely. Stop when you reach
primitives. Only mutable classes need per-object copying.
When Shallow Copy Is Genuinely Safe
Shallow copies are not mistakes waiting to happen - they are the correct choice whenever sharing the referenced object is safe. Understanding which scenarios those are prevents over-engineering.
1// File: SafeShallowCopyDemo.java
2
3import java.time.LocalDate;
4
5public class SafeShallowCopyDemo {
6
7 // ALL fields are either primitive or immutable types.
8 // A shallow copy of CouponSnapshot is FULLY independent
9 // in terms of behavior - there is nothing to corrupt.
10 static class CouponSnapshot implements Cloneable {
11 private final String code; // String - immutable
12 private final double discountPct; // primitive double
13 private final LocalDate expiresOn; // LocalDate - immutable
14
15 CouponSnapshot(String code, double discountPct, LocalDate expiresOn) {
16 this.code = code;
17 this.discountPct = discountPct;
18 this.expiresOn = expiresOn;
19 }
20
21 // super.clone() is ALL THAT IS NEEDED here - every shared
22 // reference points to an immutable object, so "shared" is
23 // not a problem. This shallow copy IS a functionally deep copy.
24 @Override
25 public CouponSnapshot clone() {
26 try {
27 return (CouponSnapshot) super.clone();
28 } catch (CloneNotSupportedException e) {
29 throw new AssertionError(e);
30 }
31 }
32
33 @Override
34 public String toString() {
35 return "Coupon[" + code + ", " + discountPct + "%, expires=" + expiresOn + "]";
36 }
37 }
38
39 public static void main(String[] args) {
40 CouponSnapshot original = new CouponSnapshot("SAVE20", 20.0, LocalDate.of(2026, 12, 31));
41 CouponSnapshot copy = original.clone();
42
43 System.out.println("original: " + original);
44 System.out.println("copy : " + copy);
45 System.out.println();
46 System.out.println("Same code reference? " + (original.code == copy.code));
47 System.out.println("This is fine - String is immutable, sharing it is safe.");
48 System.out.println("No mutation of 'code' through copy can affect original,");
49 System.out.println("because String has no mutation methods at all.");
50 }
51}Output:
original: Coupon[SAVE20, 20.0%, expires=2026-12-31]
copy : Coupon[SAVE20, 20.0%, expires=2026-12-31]
Same code reference? true
This is fine - String is immutable, sharing it is safe.
No mutation of 'code' through copy can affect original,
because String has no mutation methods at all.
Real-World Example - Flipkart Order State Management
An order management system needs to snapshot the state of an order at key workflow stages - when payment is received, when dispatch happens, when delivery is confirmed - so that each snapshot remains an accurate historical record regardless of what happens to the live order object afterward. A shallow copy would make every snapshot retroactively reflect the latest state of any shared list, defeating the purpose. Only a deep copy produces the independently preserved historical record the system actually needs.
1// File: OrderItem.java
2
3public class OrderItem {
4 private String productName;
5 private int quantity;
6 private double unitPrice;
7
8 public OrderItem(String productName, int quantity, double unitPrice) {
9 this.productName = productName;
10 this.quantity = quantity;
11 this.unitPrice = unitPrice;
12 }
13
14 public void setQuantity(int quantity) { this.quantity = quantity; }
15 public String getProductName() { return productName; }
16 public int getQuantity() { return quantity; }
17 public double getUnitPrice() { return unitPrice; }
18
19 public OrderItem deepCopy() {
20 return new OrderItem(productName, quantity, unitPrice);
21 }
22
23 @Override
24 public String toString() {
25 return productName + " x" + quantity + " @ Rs." + unitPrice;
26 }
27}1// File: LiveOrder.java
2
3import java.util.ArrayList;
4import java.util.Collections;
5import java.util.List;
6
7public class LiveOrder {
8 private final String orderId;
9 private String status;
10 private final List<OrderItem> items;
11
12 public LiveOrder(String orderId, String status, List<OrderItem> items) {
13 this.orderId = orderId;
14 this.status = status;
15 this.items = new ArrayList<>(items);
16 }
17
18 public void addItem(OrderItem item) { items.add(item); }
19 public void updateStatus(String newStatus) { this.status = newStatus; }
20
21 public String getOrderId() { return orderId; }
22 public String getStatus() { return status; }
23
24 // Returns an unmodifiable view - callers reading the list cannot
25 // accidentally mutate it, but the list itself is still the live one
26 public List<OrderItem> getItems() {
27 return Collections.unmodifiableList(items);
28 }
29
30 // DEEP COPY - creates a fully independent snapshot of this order's
31 // current state. Every OrderItem is individually copied so the
32 // snapshot cannot be changed by future mutations of the live order.
33 public OrderSnapshot snapshot(String stage) {
34 List<OrderItem> copiedItems = new ArrayList<>();
35 for (OrderItem item : items) {
36 copiedItems.add(item.deepCopy());
37 }
38 return new OrderSnapshot(orderId, status, stage, copiedItems);
39 }
40
41 @Override
42 public String toString() {
43 return "LiveOrder[" + orderId + ", status=" + status + ", items=" + items + "]";
44 }
45}1// File: OrderSnapshot.java
2
3import java.util.Collections;
4import java.util.List;
5
6public class OrderSnapshot {
7 private final String orderId;
8 private final String statusAtSnapshot;
9 private final String stage;
10 private final List<OrderItem> itemsAtSnapshot;
11
12 public OrderSnapshot(String orderId, String statusAtSnapshot, String stage, List<OrderItem> items) {
13 this.orderId = orderId;
14 this.statusAtSnapshot = statusAtSnapshot;
15 this.stage = stage;
16 this.itemsAtSnapshot = Collections.unmodifiableList(items);
17 }
18
19 @Override
20 public String toString() {
21 return "Snapshot[stage=" + stage + ", status=" + statusAtSnapshot
22 + ", orderId=" + orderId + ", items=" + itemsAtSnapshot + "]";
23 }
24}1// File: OrderWorkflowDemo.java
2
3import java.util.List;
4
5public class OrderWorkflowDemo {
6
7 public static void main(String[] args) {
8 LiveOrder order = new LiveOrder("ORD-9001", "PLACED",
9 List.of(new OrderItem("Shoes", 1, 2499.0)));
10
11 System.out.println("=== Stage 1 - payment received, take a snapshot ===");
12 order.updateStatus("PAYMENT_RECEIVED");
13 OrderSnapshot paymentSnapshot = order.snapshot("PAYMENT_RECEIVED");
14 System.out.println(paymentSnapshot);
15
16 System.out.println();
17
18 System.out.println("=== A new item added, dispatch processed ===");
19 order.addItem(new OrderItem("Belt", 1, 799.0));
20 order.getItems().get(0); // accessing via unmodifiable view - read only
21 order.updateStatus("DISPATCHED");
22 OrderSnapshot dispatchSnapshot = order.snapshot("DISPATCHED");
23 System.out.println(dispatchSnapshot);
24
25 System.out.println();
26
27 System.out.println("=== Live order arrives, quantity correction needed ===");
28 order.updateStatus("DELIVERED");
29 OrderSnapshot deliverySnapshot = order.snapshot("DELIVERED");
30 System.out.println(deliverySnapshot);
31
32 System.out.println();
33
34 System.out.println("=== Payment snapshot STILL shows original state ===");
35 System.out.println(paymentSnapshot);
36 System.out.println("Deep copy preserved the stage 1 state independently");
37 }
38}Output:
=== Stage 1 - payment received, take a snapshot ===
Snapshot[stage=PAYMENT_RECEIVED, status=PAYMENT_RECEIVED, orderId=ORD-9001, items=[Shoes x1 @ Rs.2499.0]]
=== A new item added, dispatch processed ===
Snapshot[stage=DISPATCHED, status=DISPATCHED, orderId=ORD-9001, items=[Shoes x1 @ Rs.2499.0, Belt x1 @ Rs.799.0]]
=== Live order arrives, quantity correction needed ===
Snapshot[stage=DELIVERED, status=DELIVERED, orderId=ORD-9001, items=[Shoes x1 @ Rs.2499.0, Belt x1 @ Rs.799.0]]
=== Payment snapshot STILL shows original state ===
Snapshot[stage=PAYMENT_RECEIVED, status=PAYMENT_RECEIVED, orderId=ORD-9001, items=[Shoes x1 @ Rs.2499.0]]
Deep copy preserved the stage 1 state independently
Each snapshot independently preserves the order state at the moment it was taken. Adding a new item for the dispatch stage does not reach back and contaminate the payment snapshot, because snapshot() created an entirely independent copy of the items list and every item inside it. Any system that instead used a shallow copy - new OrderSnapshot(orderId, status, stage, this.items) with the live list passed directly - would see every snapshot update retroactively as the live order progresses, producing audit logs that show the final state everywhere instead of the state at each specific moment.
Shallow vs Deep Copy - Side by Side
| Aspect | Shallow Copy | Deep Copy |
|---|---|---|
| New outer object created | Yes | Yes |
| Primitive fields | Independently copied | Independently copied |
| Immutable reference fields (String, Integer) | Shared - safe because they cannot change | Shared - same result, sharing is harmless |
| Mutable reference fields (List, Map, Date) | Shared - mutation by either object affects both | Independently copied - fully isolated |
| Mutable elements inside a mutable container | Shared - mutating an element affects both | Independently copied per element |
| Cost | Constant - one allocation, one field-copy pass | Linear in the size of the mutable object graph |
| When correct | No mutable reference fields, or sharing is intentional | Any mutable reference field that must be isolated |
| Common Java mechanisms | super.clone(), copy constructor with direct reference assignment, System.arraycopy() | Overridden clone() with explicit field copy, copy constructor with explicit field copy, serialization |
Best Practices
Know every field's type before deciding which copy is sufficient. The decision is not about the outer object - it is about every field that refers to something mutable. A class with ten String fields and one List field needs a deep copy of that one list, not of the strings, because strings cannot be mutated regardless of sharing.
For a list of mutable objects, copy both the list and each element. new ArrayList<>(original) creates an independent list but fills it with the same OrderItem references. If OrderItem has a setQuantity() method, the elements inside the copy can still be mutated in a way the original sees. Copying the list is only half the job when the elements are themselves mutable.
Prefer immutable types for fields wherever possible. A field of type LocalDate instead of java.util.Date, List.of(...) instead of ArrayList for fields that never need to grow, String instead of StringBuilder for values that are set once - every immutable field is one fewer field to think about when deciding how deep a copy needs to go.
Make the copy strategy explicit and local. A method called snapshot(), deepCopy(), or toHistoryRecord() that explicitly builds an independent copy is clearer than a clone() override that a caller has to inspect to determine whether it is shallow or deep. The name communicates the intent; the implementation enforces it.
Treat caches as owners of their contents. An object stored in a cache should always be a deep copy of whatever was computed - or the computed result should be immutable. A cache that holds mutable objects that are also reachable from outside through other references is a source of state corruption that is very difficult to debug, because the mutation appears to come from unrelated code.
Common Mistakes
Mistake 1 - Shallow-Copying a List and Assuming Independence
1import java.util.ArrayList;
2import java.util.List;
3
4// WRONG - new ArrayList<>(original) creates a NEW list, but its
5// elements are the SAME objects. If the element type is mutable,
6// mutations through the copy's elements still affect the originals.
7class ShallowListMistake {
8 static class Tag {
9 String value;
10 Tag(String value) { this.value = value; }
11 }
12
13 static void demo() {
14 List<Tag> original = new ArrayList<>();
15 original.add(new Tag("electronics"));
16 original.add(new Tag("sale"));
17
18 List<Tag> copy = new ArrayList<>(original); // shallow copy of the LIST
19
20 copy.get(0).value = "clearance"; // mutates the SHARED Tag object
21
22 System.out.println(original.get(0).value); // prints "clearance"
23 // The developer expected "electronics" - the list is independent
24 // but the elements are not
25 }
26}
27
28// CORRECT - copy each mutable element individually
29class DeepListFixed {
30 static class Tag {
31 String value;
32 Tag(String value) { this.value = value; }
33 Tag copy() { return new Tag(value); }
34 }
35
36 static List<Tag> deepCopyList(List<Tag> original) {
37 List<Tag> copied = new ArrayList<>();
38 for (Tag tag : original) {
39 copied.add(tag.copy()); // independent copy of each element
40 }
41 return copied;
42 }
43}Mistake 2 - Storing a Caller's Mutable Object Reference Directly in a Cache
1import java.util.ArrayList;
2import java.util.HashMap;
3import java.util.List;
4import java.util.Map;
5
6// WRONG - the cache stores the exact List reference the caller passed.
7// If the caller mutates their list after caching, the cached entry
8// silently reflects those mutations - cache now holds incorrect data.
9class SessionCacheBroken {
10 private final Map<String, List<String>> cache = new HashMap<>();
11
12 void store(String userId, List<String> permissions) {
13 cache.put(userId, permissions); // stores the caller's reference
14 }
15
16 List<String> retrieve(String userId) {
17 return cache.get(userId); // returns the internal list - can be mutated from outside
18 }
19}
20
21// CORRECT - deep copy on store AND on retrieve to fully own the
22// cached data independently of the caller's lifecycle
23class SessionCacheFixed {
24 private final Map<String, List<String>> cache = new HashMap<>();
25
26 void store(String userId, List<String> permissions) {
27 cache.put(userId, new ArrayList<>(permissions)); // copy on store
28 }
29
30 List<String> retrieve(String userId) {
31 List<String> stored = cache.get(userId);
32 return stored == null ? null : new ArrayList<>(stored); // copy on retrieve
33 }
34}Mistake 3 - Confusing Object Reference Equality With Copy Independence
1import java.util.ArrayList;
2import java.util.List;
3
4// WRONG ASSUMPTION - a developer checks copy != original and concludes
5// "they are different objects, so they must be independent"
6// That only tells you the OUTER objects are different. The INNER
7// objects (the shared list) can still be the same.
8class ReferenceConfusion {
9 static class Cart {
10 List<String> items;
11 Cart(List<String> items) { this.items = items; }
12 }
13
14 static void demo() {
15 Cart original = new Cart(new ArrayList<>(List.of("Shoes")));
16 Cart copy = new Cart(original.items); // shallow - same list
17
18 System.out.println(copy != original); // true - different Cart objects
19 System.out.println(copy.items == original.items); // true - SAME List object
20
21 // "Different objects" at the Cart level does NOT mean
22 // "different objects" at the items level
23 }
24}
25
26// CORRECT - check the actual field references to verify independence,
27// not just the outer object reference
28class ReferenceVerified {
29 static void verify(Object outerA, Object outerB, Object fieldA, Object fieldB) {
30 System.out.println("Outer objects different: " + (outerA != outerB));
31 System.out.println("Fields independent : " + (fieldA != fieldB));
32 // BOTH must be true for a true deep copy
33 }
34}Mistake 4 - Deep Copying Only One Level When Elements Are Themselves Mutable
1import java.util.ArrayList;
2import java.util.List;
3
4// WRONG - 'orderLines' is a new ArrayList (good) but each OrderLine
5// inside it is the SAME object as in the original list (bad), because
6// OrderLine is mutable. Mutating an element through the copy affects
7// the original's corresponding element.
8class PartialDeepCopyMistake {
9 static class OrderLine {
10 String product;
11 int qty;
12 OrderLine(String product, int qty) { this.product = product; this.qty = qty; }
13 }
14
15 static List<OrderLine> partialCopy(List<OrderLine> original) {
16 return new ArrayList<>(original); // new list, same elements
17 }
18}
19
20// CORRECT - new list AND new instance of each mutable element
21class FullDeepCopy {
22 static class OrderLine {
23 String product;
24 int qty;
25 OrderLine(String product, int qty) { this.product = product; this.qty = qty; }
26 OrderLine copy() { return new OrderLine(product, qty); }
27 }
28
29 static List<OrderLine> fullCopy(List<OrderLine> original) {
30 List<OrderLine> copied = new ArrayList<>();
31 for (OrderLine line : original) {
32 copied.add(line.copy()); // new list + new element per item
33 }
34 return copied;
35 }
36}Interview Questions
Q1. What is the difference between shallow copy and deep copy in Java?
A shallow copy creates a new object and copies each field's value - primitives and immutable references become independent, but mutable reference fields point to the same objects as the original. A deep copy creates a new object and also creates new copies of every mutable object reachable from it, recursively, so original and copy share nothing mutable. The observable difference: mutating a mutable field's contents after a shallow copy affects both original and copy; after a deep copy, neither can affect the other.
Q2. Does Java's Object.clone() perform a shallow copy or a deep copy?
Object.clone(), called via super.clone(), always performs a shallow copy - it copies every field's current value without knowing or caring whether any of those values are references to mutable objects. A deep copy requires the class's clone() override to explicitly replace each mutable field with a fresh copy of its contents. Simply implementing Cloneable and calling super.clone() is ONLY sufficient for genuine independence when every reference field is an immutable type.
Q3. When is a shallow copy sufficient, and when must a deep copy be used?
A shallow copy is sufficient when: the class has no mutable reference fields at all (only primitives and immutable types like String, LocalDate), or when sharing the mutable referenced objects is intentional. A deep copy is required when the copy will be used in an independent context - a different thread, a cache, a history record, a different request - where mutations made through one must not be visible to the other. The deciding question is whether any mutable object is reachable through both the original and the copy, and whether a mutation through one must be invisible to the other.
Q4. If you copy a List using new ArrayList<>(original), is that a shallow or a deep copy?
It is a shallow copy at the element level. new ArrayList<>(original) creates a new ArrayList object - adding or removing elements from the new list has no effect on the original list. However, the elements inside the new list are the same object references as in the original. If the element type is mutable, mutating an element through the new list's reference affects the same object the original list holds. For a fully independent copy of a list of mutable objects, each element must be individually copied too.
Q5. How would you deep copy an object that contains a List of mutable objects?
Two steps are required: create a new collection object, and copy each mutable element individually into it. In a copy constructor or deepCopy() method: create a new ArrayList, iterate over the original list, call a copying mechanism (a copy constructor or factory method on the element type, or the element type's own deepCopy() method) for each element, and add the resulting copy to the new list. The result is a new list filled with new element instances - nothing shared, nothing reachable through both original and copy.
Q6. Why do audit logs and history snapshots specifically require deep copies?
An audit log entry is meant to capture the state of a domain object at a specific moment in time - before a change, at confirmation, at delivery. If the snapshot is a shallow copy, any mutable object reachable from the live domain object is also reachable from the snapshot. As the live object is later modified - items added, statuses changed, quantities corrected - those modifications appear retroactively in the snapshot, because they affect the same shared objects the snapshot's fields point to. The snapshot then shows the final state at every stage, not the actual historical state, which makes the audit log wrong and potentially misleading for compliance, debugging, or dispute resolution.
FAQs
Does copying a String field require a deep copy?
No. String is immutable - a copied reference to a String object is always safe to share, because nothing can change the String's content after construction. Whether a String field is "shallow-copied" or "deep-copied" makes no observable difference - both give you a reference to the same object, and neither you nor anyone else can use that reference to change the object. This is true of all immutable types: Integer, Long, LocalDate, BigDecimal, enum constants, and records with only immutable components.
Can records solve the deep copy problem automatically?
Partially. A record is immutable in the sense that its component references cannot be reassigned - but if a component's type is itself mutable, sharing that mutable object between a record and a caller is still possible. A record Order(List<OrderItem> items) whose compact constructor does not copy items can have its perceived state changed by whoever still holds a reference to the original list. Records narrow the problem to the component types; they do not eliminate it. Defensive copying in the compact constructor - items = List.copyOf(items) - is still needed for a record with mutable components.
What is the difference between a defensive copy and a deep copy?
The two terms describe the same operation from different angles. A defensive copy is the design intent: the goal is to prevent external mutation of an object's internal state, either by copying a mutable argument on the way in (constructor) or copying an internal mutable field on the way out (getter). A deep copy is the mechanism: create new instances of every mutable object that would otherwise be shared. In practice, defensive copying IS deep copying - the difference is only in what vocabulary the context uses.
Is serialization a reliable way to deep copy Java objects?
Serialization-deserialization round-tripping (serialize to a byte stream and deserialize back to a new object) does produce a completely independent deep copy - every object in the graph is written to bytes and reconstructed independently. It is reliable in terms of correctness but expensive compared to explicit per-field copying: it requires all classes in the graph to be serializable, involves significant I/O overhead for the byte conversion, and bypasses constructors (which may or may not matter, depending on whether the class has construction-time invariants). For small, frequently-copied objects, explicit copy constructors or deepCopy() methods are almost always faster and clearer.
Can you deep copy an object without knowing its fields - generically?
Not through Java's standard library without serialization. Generic deep copying requires either: serialization (which needs Serializable), reflection-based traversal (complex, slow, and fragile with private or synthetic fields), or a third-party library such as Apache Commons Lang's SerializationUtils.clone() or a JSON library like Gson (serialize to JSON, deserialize back). For application code, explicit deep copy methods on the class itself are preferred - they are faster, more readable, and do not depend on the serialization behavior of every class in the graph.
Does Java pass objects by reference, and does that mean every method argument needs a deep copy?
Java passes object references by value - the method receives a copy of the reference (the pointer), not a copy of the object itself. The method can mutate the object through that reference, and the caller will see the mutation. This is not the same as "pass by reference" (which would let the method redirect the caller's variable to a different object). Whether to deep-copy before passing depends on intent: if the callee should not be able to mutate the object, pass a deep copy or pass an immutable view; if mutation by the callee is expected and correct, pass the original. Not every method argument needs a copy - only those where sharing a mutable state would be incorrect for the calling context.
Summary
Shallow copy and deep copy are not about how an object is copied - they are about how far into the object graph the copy operation extends. Primitives and immutable types are always safe to share; copying them "shallowly" or "deeply" produces the same observable result. The choice matters only for mutable reference fields, and within those, it matters at every level of nesting - a list of mutable objects is correctly deep-copied only when both the list itself and each element inside it are independently copied.
The practical question to ask for any copy operation is: can a mutation made through either the original or the copy ever be observed through the other? If yes and that should not be the case, the copy is not deep enough. The Flipkart order snapshot example makes this concrete: a snapshot must represent the order's state at one moment, and only a deep copy of every mutable field at every level can guarantee that later mutations to the live order do not retroactively change what the snapshot says.
For production code, explicit deepCopy() methods or copy constructors that clearly enumerate what is being copied are the most maintainable approach - a method name that says "deep" or "snapshot" communicates intent to the next reader without them having to inspect every field assignment inside it.
What to Read Next
Learn how annotations attach extra information to your code.