Back to Blog
Java

Java Object Clone: Shallow vs Deep Copy

java object clone: Understand Java object clone mechanics: shallow vs deep copy, implementing Cloneable correctly, pitfalls, and practical alternatives.

JavaObject CloningShallow CopyDeep CopyCloneableSerialization
Illustration of Java object cloning showing a source object splitting into two independent copies with a shallow and deep branch.

When you call clone() on an object in Java, you are not guaranteed a true independent copy. The default Object.clone() performs a shallow copy, and the behavior depends on whether the class implements Cloneable. This article explains what java object clone actually does, why it often surprises developers, and how to implement cloning that matches your requirements.

The Default Behavior of Object.clone()

Object.clone() is a protected method that creates a new instance of the same class and copies the field values from the original object. For primitive fields, this copies the values directly. For reference fields, it copies the reference itself, not the object being referenced. That means both the original and the clone point to the same underlying objects.

Consider a simple class:

public class Address { private String street; private String city; // constructor, getters, setters } public class Person implements Cloneable { private String name; private Address address; // constructor, getters, setters @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); } }

When you call person.clone(), the new Person object has the same name and the same address reference. Modifying clone.getAddress().setCity("New York") also changes the original person's address because they share the same Address instance.

Shallow Copy vs Deep Copy

The distinction between shallow and deep copy is central to java object clone. A shallow copy duplicates the top-level object but shares nested mutable objects. A deep copy recursively duplicates all objects reachable from the original, producing a fully independent graph.

Most real-world objects contain references to other mutable objects—collections, dates, custom classes. A shallow copy is often insufficient when the object graph is mutable and changes should not propagate between copies.

A deep copy requires explicit handling of every nested object. There is no built-in mechanism in Java that automatically deep-copies arbitrary object graphs via clone().

Implementing Cloneable Correctly

To make a class cloneable, you must implement the Cloneable marker interface and override clone(). The method should be declared public to allow external calls. The standard pattern is:

@Override public Person clone() { try { Person cloned = (Person) super.clone(); cloned.address = this.address.clone(); // assumes Address also implements Cloneable return cloned; } catch (CloneNotSupportedException e) { throw new AssertionError("Cannot clone Person", e); } }

Every mutable reference field must be explicitly cloned. If Address does not implement Cloneable, you need another way to copy it, such as a copy constructor.

The CloneNotSupportedException is a checked exception, but when you implement Cloneable, it should never be thrown. Many developers wrap it in an AssertionError or a runtime exception to keep the method signature clean.

Common Pitfalls with Cloneable

The Cloneable interface is notoriously easy to misuse. Here are the most frequent issues:

  • Forgetting to call super.clone(): The base implementation does the field-by-field copy. If you construct a new object manually, you lose the shallow copy behavior and may break invariants.
  • Not cloning final fields: clone() cannot assign new values to final fields. If a class has a final reference to a mutable object, the clone will share that object, and you cannot replace it without reflection or a different approach.
  • Subclassing complications: When a superclass implements Cloneable, subclasses inherit it. If a subclass adds mutable fields, its clone() must handle them, or it will silently produce a shallow copy.
  • Arrays: Arrays have a built-in clone() that performs a shallow copy of the array elements. For an array of objects, the elements are shared. To deep-copy an array, you must clone each element.
  • Collections: The default collection classes like ArrayList and HashMap implement Cloneable but only shallow-copy their contents. The contained objects are still shared.

These pitfalls often lead to subtle bugs that are hard to trace because the code compiles and runs without errors.

Alternatives to clone(): Copy Constructors and Factories

Because Cloneable is error-prone, many Java developers prefer copy constructors or static factory methods. A copy constructor is a constructor that takes an instance of the same class and creates a new one with copied state.

public Person(Person other) { this.name = other.name; this.address = new Address(other.address); // assuming Address has a copy constructor }

This approach gives you full control over the copying process. It works with final fields, does not require Cloneable, and is type-safe. The downside is that you must write a copy constructor for every class that needs copying, and you must manually handle nested objects.

A static factory method can be used similarly:

public static Person copyOf(Person original) { return new Person(original); }

These alternatives are often clearer than clone() because they make the copying logic explicit and avoid the checked exception.

Using Serialization for Deep Copy

Serialization can perform a deep copy if your object graph is fully serializable. The idea is to write the object to a byte stream and then read it back, creating a new object graph.

public static <T> T deepCopy(T object) throws IOException, ClassNotFoundException { ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bos); oos.writeObject(object); oos.flush(); ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bis); return (T) ois.readObject(); }

This approach works for any object that implements Serializable, including nested objects. However, it has significant drawbacks:

  • Performance: Serialization is much slower than manual copying because it involves reflection and I/O operations.
  • Transient fields: Fields marked transient are not serialized and will be lost in the copy.
  • Not all classes are serializable: External or legacy classes may not implement Serializable.
  • Security: Deserialization can be a vector for attacks if the data is not trusted.

Use serialization-based cloning only when the object graph is complex, and performance is not critical.

Performance and Maintainability Considerations

Manual deep copying with copy constructors is generally the fastest and most maintainable approach. It is explicit, type-safe, and does not rely on reflection. The cost is writing boilerplate code, but the clarity often outweighs the effort.

Object.clone() is slightly faster than a copy constructor because it uses native memory copy for the shallow part, but the difference is negligible for most applications. The real performance cost appears when you add deep-copy logic on top of it.

Serialization-based cloning is the slowest and should be avoided in performance-sensitive paths. It also creates a dependency on Serializable, which can be a maintenance burden.

For maintainability, copy constructors and static factories are easier to read and modify. They do not require the class to implement a marker interface, and they work with final fields.

Choosing the Right Cloning Strategy

The choice depends on your specific requirements:

  • Use Object.clone() when you need a quick shallow copy and the class is under your control. Be prepared to override it for every subclass and handle nested mutable objects manually.
  • Use copy constructors when you need deep copies and want explicit, readable code. This is the most predictable approach for most business objects.
  • Use serialization when you have a deeply nested object graph and cannot write manual copy logic, or when you need a generic deep-copy utility. Accept the performance cost.

If you are working with immutable objects, cloning is unnecessary. Reuse the same instance instead of copying it.

For collections, consider using the copyOf static methods introduced in Java 10 for unmodifiable copies, or manually copy elements if you need a mutable deep copy.

Handling Final Fields and Immutable References

One of the trickiest aspects of cloning is dealing with final fields. Because clone() creates a new object via super.clone(), the final fields are already set to the same references as the original. You cannot reassign them in the clone() method. If the final field points to a mutable object, the clone will share it.

A copy constructor does not have this limitation. You can assign new values to final fields in the constructor body, as long as they are assigned exactly once. This makes copy constructors the only clean way to deep-copy objects with final mutable references.

Consider a class like this:

public final class Configuration { private final List<String> allowedHosts; public Configuration(List<String> allowedHosts) { this.allowedHosts = new ArrayList<>(allowedHosts); } public Configuration(Configuration other) { this.allowedHosts = new ArrayList<>(other.allowedHosts); } }

The copy constructor creates a new list, so the copy is independent. Using clone() would not allow this because allowedHosts is final.

A Practical Example: Deep Copying a Composite Object

Let's put the concepts together with a complete example. Suppose you have an Order that contains a list of LineItem objects, each with its own Product reference. A deep copy should duplicate the order, the list, and each line item, but the Product might be immutable and can be shared.

public class Product { private final String sku; private final String name; // constructor, getters } public class LineItem { private Product product; private int quantity; // constructor, getters, setters public LineItem(LineItem other) { this.product = other.product; // immutable, safe to share this.quantity = other.quantity; } } public class Order { private List<LineItem> items; public Order(List<LineItem> items) { this.items = new ArrayList<>(items); } public Order(Order other) { this.items = new ArrayList<>(); for (LineItem item : other.items) { this.items.add(new LineItem(item)); } } }

This copy constructor creates a new Order with a new list and new LineItem objects, but shares the immutable Product references. That is a correct deep copy for this object graph.

If you used Object.clone(), you would need to override clone() in Order and LineItem, handle the list cloning manually, and ensure LineItem implements Cloneable. The copy constructor approach is simpler and less error-prone.

The Role of Immutability in Cloning

Immutability changes the cloning decision entirely. If an object and all its dependencies are immutable, sharing is safe. Cloning becomes unnecessary because there is no risk of unintended mutation. This is why modern Java code favors immutable value objects, often implemented with records.

When you design a class, consider whether it truly needs to be cloneable. Often, you can avoid cloning by making the object immutable or by using builder patterns that create new instances with modified state.

If you must support cloning, prefer copy constructors over Cloneable for new code. The Cloneable interface is a legacy feature that does not provide a clone() method declaration—it is merely a marker. The actual method comes from Object, and its behavior is often misunderstood.

By understanding the mechanics of java object clone, you can choose the approach that fits your data model and avoid the subtle bugs that come from shallow copies.

java object clone: Practical Usage and Code Examples | RYUSLOG DEV