00:00

Object Cloning in Java – clone(), Shallow and Deep Copies

Object cloning creates a copy of an existing object. Java's built-in mechanism is the clone() method inherited from Object, which works only for classes that implement the marker interface Cloneable. By default it makes a shallow copy: field values are copied, but referenced objects are shared.

This tutorial shows how to implement clone() correctly, explains shallow versus deep copies, and covers copy constructors, which are usually the better choice.

1. How clone() Works

  • Object.clone() is protected and declares CloneNotSupportedException.
  • If the class implements Cloneable, it allocates a new object of the same class and copies every field, without calling a constructor.
  • If not, it throws CloneNotSupportedException.
NotCloneable.java
public class NotCloneable {
    static class Config {
        String env = "dev";
        Config copy() throws CloneNotSupportedException { return (Config) super.clone(); }
    }

    public static void main(String[] args) {
        try {
            new Config().copy();
        } catch (CloneNotSupportedException e) {
            System.out.println("Without Cloneable: " + e);
        }
    }
}
Output
Without Cloneable: java.lang.CloneNotSupportedException: NotCloneable$Config

2. Implementing clone(): a Shallow Copy

The recommended pattern: implement Cloneable, override clone() as public with a covariant return type, call super.clone() and handle the impossible checked exception:

ShallowClone.java
import java.util.ArrayList;
import java.util.List;

public class ShallowClone {
    static class Student implements Cloneable {
        String name;
        int age;
        List<String> courses = new ArrayList<>();

        Student(String name, int age) { this.name = name; this.age = age; }

        @Override
        public Student clone() {                       // covariant return type, public
            try {
                return (Student) super.clone();        // field-by-field (shallow) copy
            } catch (CloneNotSupportedException e) {
                throw new AssertionError(e);           // cannot happen: we implement Cloneable
            }
        }
        public String toString() { return name + "(" + age + ") " + courses; }
    }

    public static void main(String[] args) {
        Student original = new Student("Asha", 21);
        original.courses.add("Java");

        Student copy = original.clone();
        copy.name = "Ravi";             // String reference replaced: independent
        copy.age = 22;                  // primitive copied: independent
        copy.courses.add("Spring");     // SAME list object: shared!

        System.out.println("original: " + original);
        System.out.println("copy:     " + copy);
        System.out.println("copy != original:            " + (copy != original));
        System.out.println("courses list shared (same)?  " + (copy.courses == original.courses));
    }
}
Output
original: Asha(21) [Java, Spring]
copy:     Ravi(22) [Java, Spring]
copy != original:            true
courses list shared (same)?  true

Primitive fields and reassigned references are independent, but the courses list is the same object in both students. Adding a course through the copy changed the original too.

3. Deep Copy

A deep copy also copies the mutable objects that the object references, so the copy is fully independent:

DeepClone.java
import java.util.ArrayList;
import java.util.List;

public class DeepClone {
    static class Student implements Cloneable {
        String name;
        List<String> courses = new ArrayList<>();
        Student(String name) { this.name = name; }

        @Override
        public Student clone() {
            try {
                Student copy = (Student) super.clone();
                copy.courses = new ArrayList<>(courses);   // copy the mutable field too
                return copy;
            } catch (CloneNotSupportedException e) {
                throw new AssertionError(e);
            }
        }
    }

    public static void main(String[] args) {
        Student original = new Student("Asha");
        original.courses.add("Java");
        Student copy = original.clone();
        copy.courses.add("Kafka");
        System.out.println("original courses: " + original.courses);
        System.out.println("copy courses:     " + copy.courses);
    }
}
Output
original courses: [Java]
copy courses:     [Java, Kafka]
AspectShallow copyDeep copy
Primitive fieldsCopiedCopied
References to mutable objectsShared with the originalReplaced by copies
IndependencePartialComplete
CostCheapDepends on the object graph
Default Object.clone()YesNo: must be implemented

Immutable fields (String, Integer, LocalDate, records of immutable values) can be shared safely, so they never need deep copying.

4. Better Alternatives: Copy Constructors and Factories

A copy constructor or static copy factory is simpler and safer than clone(): no marker interface, no checked exception, no casts, and it works with final fields. Arrays are the exception where clone() is the idiomatic way to copy:

CopyConstructor.java
import java.util.ArrayList;
import java.util.List;

public class CopyConstructor {
    // Preferred alternative: copy constructor / static factory, no Cloneable needed
    static final class Order {
        private final String id;
        private final List<String> items;

        Order(String id, List<String> items) { this.id = id; this.items = new ArrayList<>(items); }
        Order(Order other) { this(other.id + "-copy", other.items); }       // copy constructor
        static Order copyOf(Order other) { return new Order(other); }      // copy factory

        void add(String item) { items.add(item); }
        public String toString() { return id + " " + items; }
    }

    public static void main(String[] args) {
        Order o1 = new Order("A-17", List.of("Keyboard"));
        Order o2 = new Order(o1);
        Order o3 = Order.copyOf(o1);
        o2.add("Mouse");
        System.out.println(o1);
        System.out.println(o2);
        System.out.println(o3);

        int[] scores = {90, 85, 77};
        int[] copy = scores.clone();                     // arrays support clone() directly
        copy[0] = 0;
        System.out.println("array original[0] = " + scores[0] + ", copy[0] = " + copy[0]);
    }
}
Output
A-17 [Keyboard]
A-17-copy [Keyboard, Mouse]
A-17-copy [Keyboard]
array original[0] = 90, copy[0] = 0

The JDK follows this style: new ArrayList<>(otherList), List.copyOf(...), new HashMap<>(otherMap).

5. Why clone() Is Considered Broken

  • Cloneable has no clone() method, so you cannot clone through a Cloneable reference.
  • It creates objects without running constructors, bypassing their validation.
  • It conflicts with final fields that need deep copies, because they cannot be reassigned after super.clone().
  • Every class in a hierarchy must implement it correctly, or subclasses break.
  • The checked CloneNotSupportedException clutters code.

Effective Java recommends copy constructors or factories except for arrays.

6. Summary

  • clone() works only for classes implementing Cloneable; otherwise it throws CloneNotSupportedException.
  • The default clone is a shallow copy: referenced objects are shared.
  • Deep copies must copy mutable referenced objects explicitly.
  • Prefer copy constructors or static factories; use clone() for arrays.

Related Tutorials