Deep copy vs shallow copy in Java
Reference, shallow and deep copies drawn in memory, why Object.clone() is shallow and discouraged, copy constructors, defensive copies, List.copyOf vs unmodifiableList, array cloning, serialization cost and cyclic graphs.
On this page
🧬 What is the difference between a deep copy and a shallow copy?
A shallow copy creates a new object and copies each field as it is, so primitive values are duplicated but reference fields still point to the same inner objects. A deep copy also copies those inner objects, recursively, so the two graphs share nothing mutable. Copy a Customer shallowly and change the copy's address city to "Delhi", and the original customer moves to Delhi too.
For this lesson, we'll focus on the three levels of "copy" (reference, shallow, deep), what Object.clone() really does and why most senior engineers avoid it, the copy techniques you should use instead, and the cases interviewers love: 2D arrays, unmodifiable views and cyclic graphs. This question comes up because it quietly tests whether you can picture references in memory. If you can draw the boxes and arrows, the rest follows.
Below the line (out of scope)
- Java serialization in depth (
serialVersionUID, customreadObject): The transient keyword touches the basics - Why Java passes references by value: Pass by value
- Persistent (structurally shared) collections from libraries such as Vavr or PCollections
The 30-second answer
Assigning b = a copies the reference, so both names point to one object. A shallow copy is a new outer object whose reference fields still point to the same children; Object.clone() and int[][].clone() are shallow. A deep copy duplicates every mutable object reachable from the root. I avoid Cloneable (it's a broken design: a marker interface, a protected method, a checked exception and no constructor call) and use copy constructors or static copy factories instead. Better still, make objects immutable so they can be shared and never need copying, and use List.copyOf rather than Collections.unmodifiableList when you need a real snapshot.
What you’ll learn
- Draw the difference between a reference copy, a shallow copy and a deep copy
- Explain what
Object.clone()andCloneablereally do and why Effective Java calls the design broken - Write copy constructors and static
copyOffactories, and know they aren't polymorphic - Make defensive copies correctly (copy, then validate) and pick
List.copyOfoverCollections.unmodifiableList - Copy
int[]vsint[][]correctly and handle cyclic graphs with anIdentityHashMap - Weigh serialization and library deep copies against immutability
Interview radar
Asked very often: What's the difference between a shallow and a deep copy, and is Object.clone() shallow or deep?
Senior rounds probe: Why Cloneable is considered broken, how copy constructors and List.copyOf replace it, and how to copy int[][] or a graph with cycles.
Production angle: A caller mutates a list after passing it in, and your "immutable" object or cached config changes underneath you.
01Picture it first: photocopying a folder
Imagine a client folder with a cover sheet and three paper-clipped documents inside. Handing a colleague the same folder is a reference copy: you both write on the same pages. Photocopying the cover sheet and writing "see the original documents in drawer 4" on it is a shallow copy: the cover is yours, but any change to the documents is seen by both of you. Photocopying the cover and every document inside is a deep copy: a completely separate folder.
The diagram below shows the three cases for a Customer that holds an Address and a list of tags.
Reference copy: two variables point to one Customer; shallow copy: two Customer objects share one Address and one tags list; deep copy: two Customers each with their own Address and tags list
| Kind of copy | New outer object? | Inner objects shared? | Typical way to get it |
|---|---|---|---|
| Reference copy | no | everything shared | Customer b = a; |
| Shallow copy | yes | yes | clone(), new ArrayList<>(list), Arrays.copyOf |
| Deep copy | yes | no (for mutable parts) | copy constructors all the way down, serialization, mapping libraries |
Notice the "for mutable parts". String, Integer, LocalDate and records of immutable fields can be shared safely, so a deep copy doesn't need to duplicate them. Deep copying an immutable object is wasted work.
02Object.clone() under the hood
Object.clone() is a protected native method. When you call it, HotSpot allocates a new object of the same runtime class and copies the raw field bytes from the original, then gives the copy a fresh header, so no identity hash, lock state or GC age carries over. No constructor runs. That's the whole implementation: a memory copy, which is exactly why it's shallow. A reference field is 4 bytes (with compressed oops) holding a pointer, and copying the pointer doesn't copy what it points to.
To use it, a class must implement Cloneable, a marker interface with no methods. Object.clone() checks for it at runtime and throws CloneNotSupportedException if it's missing.
Here's the whole path a clone() call takes, from your override into the JVM and back.
Your public clone override calls super.clone; the native Object.clone checks Cloneable and throws if it is missing, allocates the same runtime class without running a constructor, copies the field bits, and returns a shallow copy that your override then deepens by replacing mutable fields
Try it yourself first
Before running it in your head, predict what original.address.city and original.name print after the copy is modified.
class Address implements Cloneable {
String city;
Address(String city) { this.city = city; }
}
class Customer implements Cloneable {
String name;
Address address;
List<String> tags = new ArrayList<>();
Customer(String name, Address address) { this.name = name; this.address = address; }
@Override
public Customer clone() { // covariant return, public
try {
return (Customer) super.clone(); // field-by-field copy
} catch (CloneNotSupportedException e) {
throw new AssertionError(e); // can't happen: we implement Cloneable
}
}
}
Customer original = new Customer("Asha", new Address("Pune"));
Customer copy = original.clone();
copy.name = "Ravi";
copy.address.city = "Delhi";
System.out.println(original.name); // Asha
System.out.println(original.address.city); // Delhi (shared Address)
System.out.println(original.tags == copy.tags); // true (shared list)
Notice three things:
- Reassigning
copy.namedoesn't affect the original. The copy has its ownnamefield; it now points to a differentString. - Mutating
copy.address.citydoes affect the original, because bothaddressfields point to oneAddressobject. - To make it deep,
clone()must also doc.address = address.clone(); c.tags = new ArrayList<>(tags);. Every class in the graph has to cooperate.
Why clone is broken (Effective Java, Item 13)
Joshua Bloch's verdict is that Cloneable is a flawed mechanism, and I agree in practice. The problems stack up:
Cloneablehas no methods. It changes the behaviour of a protected method in another class (Object). HavingCloneablein a type doesn't let you callclone()on it.- No constructor runs. Invariants your constructor enforces, counters it increments, or registrations it performs are skipped.
- It fights
final. To deep-copy afinal List<String> tagsfield you need to assign a new list inclone(), butfinalfields can't be reassigned after construction. You have to dropfinalto make cloning work. - A checked exception for a case that can't happen once you implement the interface, so every override has the same
try/catchnoise. - Subclass fragility. If any superclass's
clone()doesn't callsuper.clone()(and usesnewinstead), every subclass clone returns the wrong type.
Many candidates say "clone() makes a deep copy" or "implementing Cloneable makes clone() public". Both are wrong. Object.clone() is always shallow, and it stays protected until you override it with a public method. The only places clone() is clean are arrays, which have a public, non-throwing clone() built in.
03The copy you should write: constructors and factories
Try it yourself first
Rewrite Customer so it can be deep-copied without Cloneable. Keep the fields final.
Premium lesson
Keep reading with the Java Pass
The rest of this lesson is part of Java One-Stop Interview Prep: in-depth explanations, diagrams, model answers and how to say them in the interview.
- Real world: defensive copies and the bug that wasn't yours
- Advanced: arrays, serialization, libraries and cycles
- How to say it in the interview
From ₹199 · one-time payment · No auto-renewal
Further reading (9)
- Object.clone (Java SE 21 API)
- Cloneable (Java SE 21 API)
- List.copyOf (Java SE 21 API)
- Collections.unmodifiableList (Java SE 21 API)
- JLS §10.7: Array Members (public clone)
- IdentityHashMap (Java SE 21 API)
- Effective Java, 3rd ed., Items 13 and 50: clone and defensive copies
- Apache Commons Lang SerializationUtils
- Baeldung: How to Make a Deep Copy of an Object in Java