Java Deep Copy: Implementations and Tradeoffs
Learn how to implement a java deep copy correctly, comparing clone(), copy constructors, and serialization while avoiding common pitfalls like Shared references.
When you assign an object to a new variable in Java, you copy the reference, not the object. This becomes a problem when you need an independent copy that the original caller can modify without affecting the source. A java deep copy duplicates the entire object graph, creating new instances for every referenced object, not just for the top-level object.
Consider a simple Person class that contains an Address:
public class Person { private String name; private Address address; // constructor, getters, setters } public class Address { private String city; private String street; }
A shallow copy of Person copies the name string reference and the address reference. Mutating copied.address.street changes original.address.street because both point to the same Address instance. A deep copy creates a second Address object so that mutations are isolated. The core challenge is that Java does not provide a built-in generic deep-copy mechanism; you must choose an implementation strategy and respect its constraints.
Why Shallow Copy Often Fails in Practice
The default clone() method inherited from Object performs a shallow copy. If you rely on it without overriding it, you get shared fields for mutable referenced objects. This behavior is not a bug; it is the documented contract. But it is the source of many subtle bugs when developers assume clone() gives them full independence.
public class Person implements Cloneable { private String name; private Address address; @Override public Person clone() throws CloneNotSupportedException { return (Person) super.clone(); } }
This clone() copies the name and address references. Strings are immutable, so sharing the name is safe. But Address is mutable, and both Person instances refer to the same Address. A change to original.getAddress().setCity("Paris") reflects in copied.getAddress().getCity().
Shallow copy is not wrong if the object graph does not contain mutable references. When all fields are primitives or immutable types, the copy is effectively independent. However, in typical domain objects, you will encounter mutable collaborators, collections, or nested entities, and that is when a shallow copy is not enough.
Using clone() for a Deep Copy
To turn clone() into a deep copy, you must override it to copy every mutable referenced object that you want to be independent. The process is manual and requires you to know the internal structure of the class.
@Override public Person clone() throws CloneNotSupportedException { Person cloned = (Person) super.clone(); cloned.address = this.address.clone(); // assuming Address implements Cloneable return cloned; }
You must ensure that Address also implements Cloneable and overrides clone() to copy its own mutable fields. The same applies recursively for any nested object. If the object contains a List<PhoneNumber>, you need to create a new list and clone each element inside it.
The main limitation of this approach is that it does not work when an object holds a final field that references a mutable type. You cannot reassign a final field in the cloned object because final fields must be initialized in the constructor, and clone() does not invoke a constructor. Workarounds exist, such as using reflection or serialization, but they add complexity and fragility.
Another issue is the Cloneable contract itself. The clone() method is protected, and to call it on an object without casting, you must implement Cloneable. The standard advice in many codebases is to avoid clone() altogether because of its weak contract: it does not guarantee a deep copy, and exceptions can be thrown at runtime. Still, for simple class hierarchies where you control every class, overriding clone() can be a direct and type-safe approach.
Copy Constructors as an Alternative
A copy constructor is a constructor that takes an instance of the same class and initializes the new object with deep copies of the fields.
public Person(Person other) { this.name = other.name; this.address = new Address(other.address); }
Address must also provide a copy constructor:
public Address(Address other) { this.city = other.city; this.street = other.street; }
This approach is explicit and type-safe. You do not need to implement Cloneable or catch CloneNotSupportedException. It respects final fields because you assign them inside a constructor, which is the only legal place.
Copy constructors work well when you control the class hierarchy and when the object graph is known. For a class with many fields, however, writing a copy constructor is verbose and error-prone. If the class evolves, you must remember to update the copy constructor whenever you add a field. Missed fields lead to silent shallow copies.
You can reduce some of that burden by using a builder pattern that internally uses a copy constructor, but that only hides the maintenance cost rather than eliminating it.
Deep Copy with Serialization
Java serialization can create a deep copy by writing an object to a byte stream and reading it back. Because the serialization mechanism traverses the complete object graph, all referenced objects are duplicated.
public static <T> T deepCopySerialization(T object) throws IOException, ClassNotFoundException { ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream out = new ObjectOutputStream(bos); out.writeObject(object); out.flush(); ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray()); ObjectInputStream in = new ObjectInputStream(bis); return (T) in.readObject(); }
This generic helper works for any class that implements java.io.Serializable. Every referenced object must also implement Serializable, or the operation fails with a NotSerializableException. The serialized form includes the entire object graph, so the deserialized object is fully independent.
The tradeoff is performance. Serialization allocates a byte array, writes object state, and reads it back, which is considerably more expensive than a manual copy or a copy constructor. It also bypasses constructors entirely, so any logic in constructors (such as validation or default initialization) is not executed. Instead, deserialization uses a special mechanism that sets fields directly, which can violate invariants that the class normally enforces.
Security is a real concern if you deserialize untrusted data, but for the purpose of deep copying your own objects, the data originates from your own runtime, so that risk is limited. Still, if you deserialize objects that were serialized from an external source, you must guard against malicious payloads. For internal deep copying, the security concern is less about the source and more about the extra processing overhead.
Using External Libraries for Deep Copy
Several open-source libraries provide deep-copy utilities, such as Apache Commons Lang's SerializationUtils.clone() and Jackson's ObjectMapper.convertValue() or readValue().
SerializationUtils.clone() uses Java serialization under the hood, so the same Serializable requirement applies. It hides the boilerplate of the byte-array setup.
Jackson can perform a deep copy by writing an object to JSON and reading it back:
ObjectMapper mapper = new ObjectMapper(); Person copied = mapper.readValue(mapper.writeValueAsBytes(original), Person.class);
This approach does not require Serializable, but it does require Jackson to be able to deserialize the class. That means the class needs a default constructor, or you need to configure Jackson with mix-ins or creator properties. Jackson will also use the getters and setters to access fields, so it can trigger side effects if your getters have any. Also, the JSON representation may lose type information for polymorphic types unless you configure the mapper accordingly.
Using a library saves you from writing repetitive copy code, but it introduces an external dependency and often has its own constraints. If your object graph is simple and you want a one-off copy, a copy constructor may be more straightforward. If you have a complex graph and you do not want to maintain copy logic for every class, a library can be the right choice.
Performance and Memory Considerations
Deep copying is inherently more expensive than shallow copying because it allocates new objects for every referenced mutable field. The memory footprint grows with the size of the object graph. A copy constructor that explicitly creates new instances is usually the fastest approach because it does minimal work and does not involve serialization or reflection.
Serialization-based copying is the slowest because it converts objects to a byte stream and back, involving I/O-like operations and class discovery. The exact numbers depend on the JVM version, the size of the graph, and the available serialization implementation. What matters is the underlying mechanism: each object in the graph is written and then read, so cost scales linearly with the number of objects, plus a fixed overhead for stream setup.
If copying happens in a hot path, such as inside a loop that processes thousands of requests, the choice of deep-copy strategy can have a visible impact on throughput. Profiling can help you determine whether copying is a real bottleneck. Often, the better optimization is to avoid copying altogether by designing immutable objects or sharing read-only instances.
Memory usage also deserves attention. A deep copy duplicates all referenced objects, so the memory footprint can be large for a wide graph. If the original and the copy are both retained, you can double the memory usage of that subtree. In memory-constrained environments, this should be a deliberate decision rather than an accident.
Choosing Between the Approaches
The right strategy depends on the size and shape of your object graph, how often you perform the copy, and whether you control the classes involved.
Use a copy constructor when the number of classes is small, the graph depth is shallow, and you want compile-time type safety. It is the most maintainable option because it does not rely on reflection or serialization. The main risk is forgetting to update the copy constructor when you add a field; a unit test that verifies independence of fields can catch that.
Use clone() when you already have a class hierarchy that implements Cloneable and all fields are non-final. Overriding clone() in every class gives you a type-safe copy without a separate constructor. However, clone() is awkward with final fields, and the method signature throws a checked exception, which adds boilerplate.
Use serialization-based copying (manual or with a library) when the object graph is complex, you do not want to write copy logic for every class, and the classes already implement Serializable or can be handled by a library like Jackson. The cost is performance and the loss of constructor execution.
For object graphs that contain third-party classes you cannot modify, copy constructors and clone() are often impossible without using reflection or libraries that bypass access controls. In such cases, serialization is usually the only portable way to get a deep copy.
Avoiding Common Pitfalls
The most frequent mistake is thinking that a shallow copy is a deep copy. Even with a copy constructor, it is easy to forget to copy a mutable field. In the Person example, if the copy constructor only assigns this.address = other.address, the two objects share the same address, and the bug appears only when the address is mutated.
Another pitfall is copying collections incorrectly. new ArrayList<>(originalList) copies the list structure but not the elements; it is a shallow copy. If the list contains mutable objects, you need to create a new list and add copies of each element.
With serialization, a common issue is that transient fields are not copied. If you mark a field as transient because you do not want it serialized, a deep copy based on serialization will initialize it to its default value (null or zero) instead of preserving the original value. This can silently break invariants when the field is needed after the copy.
Another issue is that serialization does not respect final fields in the same way as a constructor. The deserialized object may have final fields set to any value, which is generally acceptable, but if your class relies on a constructor to enforce a constraint, that constraint is not enforced during deserialization. For example, if a constructor validates that an integer is positive, a deserialized object can have a negative value, and your code may later throw an unexpected exception.
Advanced Case: Deep Copy with Cyclic References
Cyclic references occur when object A references object B and object B references object A. Serialization handles cycles naturally because the serialization stream maintains a graph and when it encounters an already-serialized object, it writes a reference handle. The deserialized graph preserves the cycle.
A recursive copy constructor or a manual clone() implementation will loop infinitely if you do not track visited objects. You would need to use an identity map to remember which objects already have copies and reuse the same copy for subsequent references.
public static Person deepCopyWithCycles(Person original, Map<Object, Object> visited) { if (original == null) return null; if (visited.containsKey(original)) return (Person) visited.get(original); Person copy = new Person(); visited.put(original, copy); copy.name = original.name; copy.address = deepCopyAddressWithCycles(original.address, visited); return copy; }
This approach preserves the reference topology of the original graph. Without this guard, the recursion never terminates. If your object graph can contain cycles, serialization is often the least error-prone method, despite its overhead.
Serialization also requires that every class in the graph be serializable, which can be a limitation if you use third-party classes that are not. In that case, you may need to implement custom cycle handling or use a library that scans the object graph via reflection.
Deep Copy in a Multithreaded Context
When you copy an object that is shared across threads, you must ensure that the copy itself is safe. If the original object is being mutated concurrently while you are reading its fields to copy them, you can see inconsistent state. This is not specific to deep copy; it applies to shallow copy as well. The key is that deep copying does not create a thread-safe object; it creates an independent copy. If the copy is then exposed to multiple threads, you still need to synchronize access.
For defensive copying, a deep copy is often used to protect internal state from being modified by a caller. For example, a getAddress() method might return a deep copy of the internal address so that an external caller cannot change it. The copy operation itself should only be performed while the internal state is stable. If the object is immutable or you guarantee that no thread mutates it during the copy, the deep copy is safe.
If you need to deep copy an object that is being concurrently modified, you may need to synchronize the copy operation or take a snapshot first. You cannot atomically copy a mutable object graph without some form of coordination.