Deep vs Shallow Copy in Java Objects
When I started working with Java objects, one concept that took me a while to fully grasp was the difference between deep copy and shallow copy. Both are essential for managing how objects are duplicated and manipulated, especially when working with mutable objects or complex data structures. I’ve found that understanding these two copying techniques not only prevents bugs but also helps me write more efficient and predictable code.
In this article, I want to walk you through the nuances of deep and shallow copies, explain why they matter, how they differ, and share practical examples from my own experience that can help you decide which one to use in your Java projects.
What Does Copying an Object Mean?
Copying an object means creating a new instance that contains the same data as the original. But the tricky part is how the internal data is copied especially when the object contains references to other objects.
Imagine you have an object representing a person that contains an address object. When you copy the person, do you also create a new address object, or do you just copy the reference to the existing address? This question leads us directly into the concepts of shallow and deep copies.
Shallow Copy: Copying Object References
A shallow copy duplicates the original object but copies only the references of any objects it contains. In other words, the new object and the original object share references to the same nested objects.
From my experience, shallow copy is useful when you want a quick duplicate of an object and you don’t intend to modify nested objects independently. However, you have to be careful changes made to nested objects from either the original or the copied instance will affect both because they point to the same memory.
How Shallow Copy Works in Java
Java provides a way to create shallow copies using the clone() method, which is defined in the Object class. The default implementation of clone() performs a shallow copy.
Here’s a simple example I’ve worked with:
java class Address {
String city;
String street;
Address(String city, String street) {
this.city = city;
this.street = street;
}
}
class Person implements Cloneable {
String name;
Address address;
Person(String name, Address address) {
this.name = name;
this.address = address;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone(); // shallow copy
}
}
If I create a Person object and clone it using this shallow copy:
java Address addr = new Address("New York", "5th Avenue");
Person original = new Person("John", addr);
Person copy = (Person) original.clone();
copy.address.city = "Los Angeles";
Both the original and copy will now show the address city as “Los Angeles” because they share the same Address instance.
When to Use Shallow Copy
I typically use shallow copy when:
- The object contains immutable or primitive fields only.
- The nested objects are large and expensive to duplicate, and you are certain they won’t be modified.
- You want to maintain shared state between copies intentionally.
However, if the nested objects can change and you want independent copies, shallow copy is not sufficient.
Deep Copy: Creating Completely Independent Objects
Deep copy means duplicating the original object and all objects it references, recursively. This results in two completely independent objects, each with their own copies of nested objects.
From my experience, deep copy is essential when you want to modify the copy without affecting the original. This is common in data processing, simulations, or undo/redo operations where objects need to be duplicated entirely.
Implementing Deep Copy in Java
Unlike shallow copy, Java does not provide built-in deep copy functionality. I usually implement deep copy manually by overriding the clone() method or writing a copy constructor that duplicates nested objects.
Here’s how I do it for the Person and Address example:
java class Address implements Cloneable {
String city;
String street;
Address(String city, String street) {
this.city = city;
this.street = street;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
class Person implements Cloneable {
String name;
Address address;
Person(String name, Address address) {
this.name = name;
this.address = address;
}
@Override
protected Object clone() throws CloneNotSupportedException {
Person cloned = (Person) super.clone();
cloned.address = (Address) address.clone(); // deep copy
return cloned;
}
}
Now, when I clone a Person and modify the address in the copy, the original remains unchanged:
java Address addr = new Address("New York", "5th Avenue");
Person original = new Person("John", addr);
Person copy = (Person) original.clone();
copy.address.city = "Los Angeles";
The original.address.city stays as “New York,” while copy.address.city becomes “Los Angeles.”
Alternative Deep Copy Using Serialization
Another approach I’ve used for deep copy is Java serialization. By serializing an object and then deserializing it, you effectively create a deep copy provided all nested objects are serializable.
Here’s an example:
java import java.io.*;
public static Object deepCopy(Object object) {
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(object);
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
return ois.readObject();
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
This method works well but can be slower and requires all involved classes to implement Serializable. I use this for quick prototyping or when deep copy logic is complex.
When to Use Deep Copy
Deep copy comes in handy when:
- Objects contain mutable nested objects that should not be shared.
- Independent modifications are needed between the original and the copy.
- You want to avoid unintended side effects caused by shared references.
Common Pitfalls and How I Avoid Them
Recursive Objects and Circular References
One tricky problem I’ve faced is deep copying objects that reference each other, causing circular dependencies. A naive deep copy implementation can enter infinite recursion or stack overflow.
To avoid this, I usually implement deep copy with tracking of already copied objects using a map or cache. This way, I reuse copies instead of recursing endlessly.
Performance Considerations
Deep copying is more expensive in terms of performance and memory. I try to assess if I really need a deep copy or if shallow copy suffices, especially for large objects.
When performance matters, immutable objects are my go-to solution, as they don’t require defensive copying.
Using Third-Party Libraries
I sometimes rely on libraries like Apache Commons Lang’s SerializationUtils.clone() or Google’s Gson to simplify deep copy. But I’m cautious about dependencies and overhead, especially in lightweight projects.
Practical Tips From My Experience
- Start with shallow copy: Unless you know you need deep copy, start with shallow copy to avoid premature optimization.
- Immutable design: If you design your classes with immutability, copying becomes less of a headache.
- Document your copy behavior: When writing classes, I always document how cloning or copying behaves so other developers don’t get confused.
- Test thoroughly: Copying bugs can be subtle. I write unit tests that verify changes to copies don’t affect originals.
- Custom copy methods: Instead of relying only on
clone(), sometimes I write explicit copy constructors or factory methods that clarify copying semantics.
Summary of Differences Between Deep and Shallow Copy
| Aspect | Shallow Copy | Deep Copy |
|---|---|---|
| Copies Object | Yes | Yes |
| Copies Nested Objects | No, copies references only | Yes, copies objects recursively |
| Independence of Copy | No, nested objects are shared | Yes, fully independent |
| Performance | Faster and uses less memory | Slower and uses more memory |
| Use Case | Immutable objects or shared nested state | Mutable objects requiring independent copies |
| Java Support | Default clone() method provides shallow copy | Must be implemented manually or via serialization |
Conclusion
The distinction between deep and shallow copy is fundamental to managing Java objects effectively. After years of coding, I’ve realized how important it is to consciously decide how objects are duplicated, rather than leaving it to chance.
Deep copy offers complete independence between objects but comes at a cost in complexity and performance. Shallow copy is quicker and simpler but requires careful handling of shared references.
When I write or review code, I always check how copying is handled it’s a source of subtle bugs if done wrong but a powerful tool when done right.
I encourage you to experiment with both approaches, understand their trade-offs, and apply them thoughtfully in your projects.
