Course contents

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.

PremiumOften askedLesson15 min readCoreReviewed Oct 2026 · Java 21
On this page
  1. Picture it first: photocopying a folder
  2. Object.clone() under the hood
  3. The copy you should write: constructors and factories
  4. Real world: defensive copies and the bug that wasn't yours
  5. Advanced: arrays, serialization, libraries and cycles
  6. How to say it in the interview

🧬 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, custom readObject): 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() and Cloneable really do and why Effective Java calls the design broken
  • Write copy constructors and static copyOf factories, and know they aren't polymorphic
  • Make defensive copies correctly (copy, then validate) and pick List.copyOf over Collections.unmodifiableList
  • Copy int[] vs int[][] correctly and handle cyclic graphs with an IdentityHashMap
  • 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.

Fig. 1

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

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.

Fig. 2

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

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.name doesn't affect the original. The copy has its own name field; it now points to a different String.
  • Mutating copy.address.city does affect the original, because both address fields point to one Address object.
  • To make it deep, clone() must also do c.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:

  1. Cloneable has no methods. It changes the behaviour of a protected method in another class (Object). Having Cloneable in a type doesn't let you call clone() on it.
  2. No constructor runs. Invariants your constructor enforces, counters it increments, or registrations it performs are skipped.
  3. It fights final. To deep-copy a final List<String> tags field you need to assign a new list in clone(), but final fields can't be reassigned after construction. You have to drop final to make cloning work.
  4. A checked exception for a case that can't happen once you implement the interface, so every override has the same try/catch noise.
  5. Subclass fragility. If any superclass's clone() doesn't call super.clone() (and uses new instead), every subclass clone returns the wrong type.
Where candidates slip

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

Practise this topicThe Object contract: interview questions & scenarios
Further reading (9)