Understanding Java Shallow Copy Behavior
java shallow copy: Learn how a shallow copy works in Java, when it is safe to use, and how it differs from a deep copy with practical code examples.
A shallow copy of a Java object creates a new object that shares the same references to the mutable objects held by the original. The primitive fields are copied by value, but any reference field in the copy points to the same object as the original. For developers working with data structures, this behavior is often the source of subtle bugs: changing an object that is reachable from the copy also changes the original. When you search for java shallow copy, the practical question is usually about how to duplicate an object correctly without unintentionally sharing state.
Consider a class that holds a reference to a mutable Address object. Copying only the top-level reference produces two Person objects that both refer to the same Address. That is exactly what a shallow copy is: a new container with the same content references, not a new set of underlying objects.
Java's Built-In Copying Mechanisms
The Java language itself provides two main tools for copying objects: the clone() method from Object and copy constructors. The Object.clone() method is a protected method that performs a shallow copy by default. To use it, a class must implement the Cloneable interface, which acts as a marker indicating that the object is eligible for cloning. If clone() is called on an object that does not implement Cloneable, the JVM throws a CloneNotSupportedException.
Implementing clone() requires overriding the method and making it public, because the default protected version is not accessible from outside the class. The typical implementation calls super.clone() and returns the result, which is a shallow copy of the original object. While this is the most direct way to get a shallow copy, it has a few drawbacks: the Cloneable interface has no methods, the clone() method returns an Object so a cast is needed, and using it on a final class is straightforward, but on a non-final class it can lead to subtle issues with subclasses.
A copy constructor, on the other hand, is a constructor that takes an instance of the same class and creates a new object with copied field values. It is written manually, so you have complete control over what gets copied. For a shallow copy, the copy constructor simply assigns each reference field to the reference from the original, and copies primitive fields by value. This approach is generally preferred over clone() because it is explicit, type-safe, and does not require special interfaces.
public class Address { private final String city; private final String street; public Address(String city, String street) { this.city = city; this.street = street; } public String getCity() { return city; } public String getStreet() { return street; } } public class Person { private final String name; private final Address address; public Person(String name, Address address) { this.name = name; this.address = address; } // Copy constructor - shallow copy public Person(Person other) { this.name = other.name; this.address = other.address; // shares the same Address object } public String getName() { return name; } public Address getAddress() { return address; } }
This copy constructor creates a new Person object, but the address field still points to the same Address instance as the original. If you later modify that address, both the original and the copy see the change. That is the core semantic of a shallow copy.
When a Shallow Copy Is Sufficient
A shallow copy is the right choice when the object graph is immutable. If every reference field points to an object that never changes after construction, sharing those objects between the original and the copy is harmless. For example, String objects are immutable, so copying a field that holds a String reference is always safe. The same applies to immutable wrapper types like Integer or LocalDate, and to custom classes designed to be immutable.
Shallow copies also reduce memory usage and avoid the cost of deep-copying large object graphs. When the internal objects are large but immutable, a shallow copy is both faster and more memory-efficient than a deep copy. Many data transfer objects in Java are designed with immutable fields, so a shallow copy is the natural default.
Another common use case is passing a copy of a collection into a method that does not modify the collection itself, but only reads it. A shallow copy of an ArrayList gives you a new list object that shares the same element references. If the elements are immutable, that is perfectly fine. If the elements are mutable and the method only reads them, a shallow copy is also acceptable, as long as the method does not change any element through the shared reference.
How Shallow Copies Behave with Collections and Nested Objects
Collection classes like ArrayList, HashMap, and HashSet have a copy constructor that creates a shallow copy of the collection. For example, new ArrayList<>(originalList) copies the internal array, but each element is the same reference. So the new list has the same elements, but modifying an element that is a mutable object affects both lists.
Understanding this is important when you pass a collection to another component and assume you are working on an independent snapshot. Consider a service that receives a list of Account objects and updates each account's balance. If that service is given a shallow copy, the original list still references the same Account objects, so the updates leak to the original. A deep copy would be required to isolate the changes.
List<Account> originalList = new ArrayList<>(); originalList.add(new Account("A1", 100.0)); List<Account> shallowCopy = new ArrayList<>(originalList); // This modifies the account object shared between the two lists shallowCopy.get(0).setBalance(200.0); System.out.println(originalList.get(0).getBalance()); // 200.0
In this example, the shallowCopy list is a new list, but the Account object is the same. The output confirms that changes propagate to the original list because the underlying object is shared.
Nested objects become even more problematic when you have multiple levels. A shallow copy of an object that contains a list of objects will share the same list instance. If you add or remove elements from that list through the copy, the original changes too. This often leads to unexpected behavior in applications that treat collections as internal state and expect isolation between copies.
Shallow Copy vs Deep Copy: A Practical Comparison
A deep copy duplicates every object in the object graph, creating entirely new instances of all mutable objects. This ensures complete independence between the original and the copy. The cost is that you must implement deep copying manually, clone every nested object, and handle cycles carefully. In Java there is no built-in deep copy mechanism, so you need either custom copy constructors that copy nested objects, or a serialization-based approach.
| Aspect | Shallow Copy | Deep Copy |
|---|---|---|
| New top-level object | Yes | Yes |
| New nested objects | No | Yes |
| Memory usage | Low (shares references) | Higher (creates new objects) |
| Performance | Fast | Slower (must recurse) |
| Independence | Original and copy share mutable objects | Fully independent |
| Implementation | Built-in via clone() or copy constructor | Manual, not provided by JVM |
Choosing between the two depends on whether the objects inside the copy are shared intentionally. If you want the copy to be a fully separate snapshot, you need a deep copy. If you are happy to share immutable objects, a shallow copy is sufficient.
One common mistake is assuming that a shallow copy gives you a completely separate object. That assumption holds for the top-level object, but not for any mutable reference fields. The risk is that you may unknowingly share state that later changes. Always verify whether the object you are copying contains mutable fields that will be modified somewhere else.
Implementing a Deep Copy When You Need Independence
When a shallow copy is not enough, you must implement a deep copy. The simplest way is to manually copy each mutable field. If an object contains another object, you call its copy constructor or clone method, recursively. This approach is explicit and type-safe, but becomes tedious for large object graphs.
A second approach is to use serialization: serialize the object to a byte stream and then deserialize it to create a new object. This automatically deep copies the entire object graph, as long as all classes are Serializable. However, serialization has its own costs: it requires the classes to implement Serializable, it is slower than manual copying, and it does not work with objects that contain non-serializable fields like Thread or Sockets. Also, serialization can be a security risk if the data stream is untrusted, so it is rarely used for copying in production code.
A third, more modern approach is to use a library like Jackson or Gson to convert the object to JSON and back. This creates a deep copy, but it introduces a dependency and has the same serialization limitations as Serializable: it requires all fields to be serializable to the chosen format. The copy may also lose type information, such as interfaces or abstract classes, so you need to configure polymorphic handling.
Performance and Memory Tradeoffs
Shallow copies are significantly faster and consume less memory than deep copies because they do not allocate new objects for every reference field. When copying a large object graph, the difference can be substantial. For example, copying an object that contains a list of 10,000 records: a shallow copy simply copies the list reference, while a deep copy would traverse all 10,000 records and create new instances for each, which takes time and memory.
However, the performance benefit is only valid if the shared references are safe. If the original objects are later modified, the copy becomes inconsistent, and you may need to synchronize or recopy. That coordination overhead can exceed the cost of a deep copy. A common production pattern is to make objects immutable so that shallow copies are always safe. Then you get the performance of shallow copies without the side-effect risk. This is the reason many value-holding classes are designed as immutable: it allows cheap copying at any time.
The runtime cost of a shallow copy is effectively constant for an object of a fixed field count. A deep copy scales with the size of the object graph, so it can be expensive for large structures. When you need to pass data between threads or cache snapshots, a shallow copy of a mostly immutable object graph is often the right performance decision. If the data changes frequently, a deep copy may be necessary despite the cost.
Common Pitfalls and How to Avoid Them
One of the most frequent pitfalls is using a shallow copy where a deep copy is required. This shows up as unexpected state sharing between what the developer believes are independent objects. The symptom is usually an unexplained change in an object that should not have been modified. The fix is to identify whether the object graph is truly immutable and, if not, to implement a proper deep copy.
Another pitfall is relying on the default Object.clone() method without realizing it only performs a shallow copy. A class that implements Cloneable and overrides clone(), but does not copy the nested mutable objects, will share those objects between the original and the clone. This is especially dangerous when the clone is used in a multi-threaded context: two threads might modify the same nested object concurrently, causing race conditions.
Final classes that do not declare a copy constructor or clone method cannot be copied at all, so you must fall back to a manual factory that creates a new instance using the original's getters. That approach is usually clearer than reflecting over fields.
A final edge case is copying objects that contain cycles, where object A references B and B references A. A shallow copy handles this naturally because it never traverses the graph; it just copies the top-level references. A recursive deep copy without cycle detection will recurse infinitely or throw a stack overflow. If you write manual deep copy logic, you must keep a map of already-copied objects to break cycles.
In production code, start with a shallow copy when you are certain the object graph is immutable or when sharing is intentional. As soon as a mutable object appears, switch to a deep copy or redesign the object to be immutable. Explicitly documenting whether a method returns a shallow or deep copy is also valuable for maintainability, so that developers who consume the object do not make incorrect assumptions.