Introduction
Understanding shallow copy and deep copy in Java is essential for anyone who works with object duplication, memory management, and data integrity. In practice, in Java, objects are references to memory locations, and copying them incorrectly can lead to unexpected bugs, shared state, or excessive memory consumption. This article explores the differences between shallow and deep copying, explains how each works under the hood, and provides practical techniques—clone(), copy constructors, serialization, and popular libraries—to implement them safely. By the end, you’ll know when to choose a shallow copy versus a deep copy and how to avoid common pitfalls in real‑world Java applications.
What Is Shallow Copy?
A shallow copy creates a new object but populates its reference fields with the same memory addresses as the original object. Practically speaking, in other words, the new object shares its internal data with the original. For primitive types, a shallow copy creates a brand‑new variable holding the same value, but for objects, it merely copies the reference.
How Shallow Copy Works in Java
- Primitive fields are duplicated with their values.
- Object references are copied, pointing to the same instance.
- Arrays are treated as objects; the copy receives a reference to the original array, not a new array.
class Simple {
int value;
String name;
}
Simple original = new Simple();
original.value = 42;
original.name = "Alice";
Simple shallow = original; // shallow points to the same object
After the assignment, shallow.name and original.name refer to the same String object. Modifying one will affect the other, which is the hallmark of a shallow copy Surprisingly effective..
When to Use Shallow Copy
- Immutable objects: If the referenced objects are immutable (e.g., String, Integer), sharing is safe because the data cannot change.
- Performance‑critical code: Shallow copying is faster and uses less memory, making it suitable for temporary snapshots where shared references are acceptable.
- Lightweight duplication: When you only need a new wrapper object and do not intend to modify the internal state, a shallow copy suffices.
What Is Deep Copy?
A deep copy goes a step further: it creates a new object and recursively copies all referenced objects, resulting in a completely independent duplicate. Changes to the copy’s internal data do not affect the original, and vice versa Which is the point..
How Deep Copy Works in Java
- Primitive fields are copied by value, just like shallow copy.
- Object references trigger a new instance of the referenced class, often via its own copy mechanism.
- Arrays are duplicated element‑by‑element, creating a new array with identical contents.
// Using clone()
Simple deep = original.clone(); // requires implementing Cloneable and overriding clone()
If original.g.Because of that, name is a mutable object (e. , a custom List), the deep copy will allocate a new List and copy its elements, ensuring isolation.
When to Use Deep Copy
- Mutable objects: When the referenced objects can be altered, a deep copy prevents unintended side effects.
- Data integrity: In scenarios like serialization, database persistence, or passing objects between threads, a deep copy safeguards the original state.
- Cloning complex graphs: For nested structures where each level must be independent, deep copying is the only reliable approach.
Comparison: Shallow vs. Deep Copy
| Aspect | Shallow Copy | Deep Copy |
|---|---|---|
| Object independence | Shares references to nested objects | Fully independent duplicate |
| Performance | Faster, less memory | Slower, higher memory |
| Implementation complexity | Simple (assignment, clone() without recursion) |
More complex (requires recursive copying) |
| Use case | Immutable data, temporary wrappers | Mutable data, data integrity, thread safety |
| Typical techniques | Direct assignment, Object.clone() (default) |
Custom clone(), copy constructors, serialization |
Choosing the right method hinges on the mutability of the data and the desired isolation between the original and the copy.
Implementation Techniques in Java
Using the clone() Method
Java provides a built‑in clone() method in the Object class, but it is protected and performs a shallow copy by default. To obtain a deep copy, a class must:
- Implement the
Cloneablemarker interface (signaling that the class can be cloned). - Override
clone()and manually copy each field, recursively cloning objects as needed.
class DeepObject implements Cloneable {
private int[] data;
private String info;
public DeepObject(int size, String info) {
this.data = new int[size];
this.info = info;
}
@Override
public DeepObject clone() {
try {
DeepObject copy = (DeepObject) super.Because of that, clone(); // creates a new array with same values
copy. clone();
// deep copy mutable fields
copy.In real terms, data = data. info = new String(this.
### Implementing a Copy Constructor
A **copy constructor** is an alternative to `clone()` that creates a new instance based on an existing one. It gives full control over how each field is copied, making it ideal for deep copying.
```java
class Person {
private String name;
private List hobbies; // mutable
public Person(Person other) {
this.name = other.name;
// create a new list and copy elements
this.
The copy‑constructor approach mirrors the same principle as a manual deep‑copy: every piece of state that may change independently has to be reproduced in the newly created object. On the flip side, in the example above the mutation point is the `List hobbies`. Because collections are reference types, simply assigning `this.hobbies = other.hobbies;` would produce a shared list—exactly the kind of subtle bug that deep copying eliminates. By instantiating a fresh `new ArrayList<>(other.hobbies)` we guarantee that both the source and the destination own separate lists, even if they contain identical elements.
Below is a more complete illustration that showcases the pattern across several typical mutable fields:
```java
// Complete copy‑constructor version
class Person {
private String name;
private List hobbies;
private double score;
private Set skills; // also mutable
// ---------- construction ----------
public Person(String name, List hobbies,
double score, Set skills) {
this.name = name;
this.Here's the thing — hobbies = new ArrayList<>(hobbies); // deep copy of List
this. score = score;
this.
// ---------- deep copy ----------
public Person deepCopy() {
Person copy = new Person(
this.name,
new ArrayList<>(this.hobbies), // fresh list
this.score,
new HashSet<>(this.
### When to prefer `clone()` versus a custom copy constructor
| Situation | Recommended technique | Rationale |
|-----------|----------------------|-----------|
| **Simple inheritance hierarchy** – you only need to propagate primitive fields and a few immutable objects. Consider this: | `clone()` + `Cloneable` | Leverages language support and keeps the API transparent (`obj. Consider this: copy()`). |
| **Complex domain model** – many collections, lazy‑initialized resources, or third‑party classes that do not implement `Cloneable`. Which means | Explicit copy constructor(s) + optional `clone()` override | Guarantees deterministic behavior, avoids accidental sharing, and makes the intent obvious to readers and static analysis tools. |
| **Serialization / persistence** – you already rely on `Serializable` or frameworks such as Jackson, Protobuf, or JPA. | Use framework‑provided copy strategies (e.g., `@JsonIdentityRemover`, `readValue`/`writeValue`) rather than hand‑rolled deep copying. | Framework logic handles edge cases (nested objects, circular references) and works with caching mechanisms. Which means |
| **Thread‑safety concerns** – multiple threads may need to work on distinct instances derived from the same source. | Always produce independent copies via either `clone()` or a copy constructor before any concurrent access. | Shared mutable state leads to race conditions; a deep copy isolates each consumer.
### Performance nuances
* **Memory footprint** – A deep copy inevitably allocates additional objects for every mutable container. In high‑throughput services, repeated cloning can become a bottleneck. Profiling with tools such as JFR or async-profiler will reveal whether allocation spikes correlate with cloning operations.
* **CPU cost** – Recursively invoking `clone()` traverses the entire object graph, performing a series of system calls for each array, map, or collection. If your domain contains large, read‑mostly payloads, consider keeping them immutable and exposing only lightweight “views” (e.g., `Collections.unmodifiableList`). This reduces the amount of work required during a deep copy while still giving callers a convenient façade.
* **Cache locality** – Shallow copies reuse references, which can improve CPU cache hits when working with primitive arrays or small fixed‑size structs. Still, these gains are usually outweighed by the correctness guarantees that deep copying provides.
### Practical tips for strong cloning
1. **Never forget to handle getters/setters that themselves wrap mutable objects.** If a property holds a `Map` or a `LinkedList`, its internal containers must also be duplicated unless the design explicitly requires shared structure.
2. **Document the contract.** A short method comment like `/// Creates a completely independent snapshot of this entity, suitable for defensive copying.` signals to future maintainers that the returned object is safe to share across threads.
3. **put to work language features.** In Java, the `Arrays.deepCopy` utility for primitive arrays can replace the manual `array.clone()` line. For maps and sets, `new HashMap<>(original)`, `new LinkedHashSet<>(original)`, and `new ArrayList<>(original)` are idiomatic and often faster than hand‑rolled loops because they are implemented in native code.
4. **Test failure modes.** Write unit tests that modify the original object after it has been cloned and verify that the clone remains unchanged. Tools such