Java Object Cloning: Shallow vs Deep Copy
Understand java object cloning with clear explanations of shallow vs deep copy, practical code examples, and how to choose the right approach.
When you assign one Java object reference to another, you are not copying the object itself. Both references point to the same heap instance, so any mutation through one reference is visible through the other. If you need an independent copy, java object cloning requires an explicit mechanism. The language provides the clone() method from Object, but its behavior is easy to misunderstand. This article explains the difference between shallow and deep cloning, how to implement each correctly, and when a copy constructor or serialization is a better choice than clone().
What clone() Actually Does by Default
The Object.clone() method creates a field-by-field copy of the source object. For primitive fields, the bit pattern is copied directly. For reference fields, only the reference is copied, not the underlying object. That means the cloned object shares the same referenced objects as the original. This is called a shallow copy.
public class Address { private String city; public Address(String city) { this.city = city; } public String getCity() { return city; } public void setCity(String city) { this.city = city; } } public class Person implements Cloneable { private String name; private Address address; public Person(String name, Address address) { this.name = name; this.address = address; } @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); } }
Here, Person implements Cloneable and calls super.clone(). The returned Person object has its own name string, but its address field points to the same Address instance as the original. Changing clonedPerson.address.setCity("New York") also changes the original's address. This is the central limitation of shallow cloning.
How to Implement a Proper Deep Copy
A deep copy duplicates not only the top-level object but also every object reachable through its reference fields. For the Person example, you must clone the Address object as well. The simplest way is to call clone() on each mutable reference field, but that requires those classes to also implement Cloneable and override clone() correctly.
public class Address implements Cloneable { private String city; // constructor, getters, setters omitted for brevity @Override protected Address clone() throws CloneNotSupportedException { return (Address) super.clone(); } } public class Person implements Cloneable { private String name; private Address address; // constructor, getters, setters omitted @Override protected Person clone() throws CloneNotSupportedException { Person cloned = (Person) super.clone(); cloned.address = this.address.clone(); return cloned; } }
Now clonedPerson has its own Address instance. But this only works if every level of the object graph supports cloning. If Address contained a List<PhoneNumber> and each PhoneNumber was mutable, you would need to clone the list and each element. The manual deep-copy approach becomes tedious and error-prone as the graph grows.
Why Cloneable Is Considered Broken by Many Developers
The Cloneable interface is a marker interface with no methods. clone() is a protected method on Object. Calling it on a class that does not implement Cloneable throws CloneNotSupportedException. The design has several problems:
clone()returnsObject, so a cast is always required.- It uses a copy constructor-like mechanism without a clear contract for deep vs shallow behavior.
finalfields cannot be reassigned inclone(), which can cause issues.- The exception is checked, forcing callers to handle it even when the class is designed to be cloneable.
Many Java developers avoid clone() altogether and prefer a copy constructor or a static factory method that explicitly creates a deep copy.
Using a Copy Constructor for Deep Copy
A copy constructor is a constructor that takes an instance of the same class and initializes the new object using the source's state. You control exactly what gets copied, and you can make it deep by constructing new instances of reference fields.
public class Person { private String name; private Address address; public Person(Person other) { this.name = other.name; this.address = new Address(other.address); } }
The copy constructor for Address must also create a new instance. This approach is explicit, type-safe, and does not rely on Cloneable or checked exceptions. It also works well with final fields because you assign them in the constructor.
public class Address { private String city; public Address(Address other) { this.city = other.city; } }
One drawback is that the copy logic is defined separately from the class hierarchy. If you add a new field, you must update the copy constructor manually. The same is true for deep-clone methods, so this is not unique to copy constructors.
Deep Copy via Serialization
Serialization can create a deep copy by writing the object graph to a byte stream and reading it back. This works when all involved classes implement Serializable. The resulting object is a full deep copy because the serialization process traverses the entire object graph.
import java.io.*; public class DeepCopyUtils { @SuppressWarnings("unchecked") public static <T extends Serializable> T deepCopy(T source) throws IOException, ClassNotFoundException { try (ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bos)) { oos.writeObject(source); try (ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bis)) { return (T) ois.readObject(); } } } }
This approach avoids manual per-field copying, but it has significant constraints:
- Every class in the graph must implement
Serializable, or it throwsNotSerializableException. staticfields are not part of the object state and are not copied.transientfields are skipped, which may lose data that was previously held in memory.- Serialization is slower than manual copying because it must write and read bytes.
- Objects that are not serializable, such as some third-party library types, cannot be copied this way.
Serialization-based cloning is often used for simple cases where the object graph is fully serializable and performance is not critical.
Choosing Between Clone, Copy Constructor, and Serialization
The choice depends on the structure of your classes and the context in which you need the copy.
| Approach | Type Safety | Deep Copy Support | Runtime Cost | Requires Interface |
|---|---|---|---|---|
clone() | Cast needed | Manual | Low | Cloneable |
| Copy constructor | Yes | Manual | Low | None |
| Serialization | Cast needed | Automatic | High | Serializable |
Use clone() when you are implementing a class hierarchy where cloning is a natural operation and you can control all fields. Many Java collections have built-in copy constructors, so you rarely need to call clone() on them.
Use a copy constructor when you want explicit, type-safe copying and you are not willing to expose Cloneable in your public API. This is the most maintainable option for most domain objects.
Use serialization when you need a deep copy of an arbitrary object graph with minimal code, and all classes in that graph are already Serializable. Do not use it in performance-critical loops.
Handling Common Pitfalls in Cloning
One frequent mistake is assuming that clone() on a collection like ArrayList produces a deep copy. In fact, ArrayList.clone() creates a new list containing the same element references. If the elements are mutable, the copy is shallow.
List<Person> people = new ArrayList<>(); people.add(new Person("Alice", new Address("Seattle"))); List<Person> shallow = (List<Person>) ((ArrayList<Person>) people).clone();
Here, shallow contains the same Person instances as people. Modifying a Person through shallow affects the original list too. To make a deep copy of a list, you must create a new list and copy each element individually.
Another issue is with arrays. The clone() method of an array creates a shallow copy of the array, meaning the new array contains the same object references. For primitive arrays, the copy is deep because primitives are copied directly.
int[] numbers = {1, 2, 3}; int[] numbersCopy = numbers.clone(); // independent copy Person[] peopleArray = {...}; Person[] peopleArrayCopy = peopleArray.clone(); // shallow copy
Cloning in a Concurrent or Performance-Sensitive Context
If you clone objects frequently, the performance difference between approaches matters. Manual copy constructors are generally the fastest because they allocate only the necessary objects. Serialization is orders of magnitude slower because it involves streaming and reflection. If you need deep copies in a tight loop, prefer copy constructors.
Concurrency is another consideration. The original object might be shared across threads. Cloning it while another thread mutates it can lead to inconsistent state if the clone reads fields in a non-thread-safe order. If your object is mutable and shared, you should either synchronize access or design the clone operation to work on a snapshot. Since clone() typically reads fields one by one, it does not provide a consistent snapshot unless the class is thread-safe.
For immutable objects, cloning is often unnecessary because you can safely share the same instance. In that case you can simply return the original reference, or omit the clone operation entirely.