Java Cloning

3 minute read

A clone in Java means creating a new object that has the same state as an existing object. There are a few common approaches to copying objects in Java: using the built-in clone mechanism, writing a copy constructor/factory, or using serialization (or third-party utilities) to perform a deep copy.

This post summarizes the key concepts, trade-offs, and shows small code examples for quick reference.

Key ideas

  • Shallow copy: copies primitive fields and references to objects, so both original and copy point to the same referenced objects.
  • Deep copy: copies the whole object graph so the copy does not share mutable sub-objects with the original.
  • Cloneable: a marker interface. Object.clone() is protected; to expose it you override clone() and usually call super.clone(). If Cloneable is not implemented, super.clone() throws CloneNotSupportedException.

When to deep-copy?

  • If your object contains mutable fields (e.g., lists, custom mutable types) and you need an independent copy, use deep copy.
  • For immutable fields (String, Integer, records, immutable collections), a shallow copy is fine because their state cannot be altered.

Common approaches

1) Using Cloneable + Object.clone() (shallow by default)

Pros: fast, low boilerplate for simple objects. Cons: tricky to get right for deep copies; fragile with inheritance; exceptions and visibility can be awkward.

Example — shallow clone (default behavior):

public class Address implements Cloneable {
	public String city;

	public Address(String city) { this.city = city; }

	@Override
	public Address clone() throws CloneNotSupportedException {
		return (Address) super.clone(); // field-wise copy (String is immutable)
	}
}

public class Person implements Cloneable {
	public String name;
	public Address address; // mutable reference

	public Person(String name, Address address) {
		this.name = name;
		this.address = address;
	}

	@Override
	public Person clone() throws CloneNotSupportedException {
		return (Person) super.clone(); // shallow: 'address' reference copied
	}
}

public class Demo {
	public static void main(String[] args) throws CloneNotSupportedException {
		Address a = new Address("Mumbai");
		Person p1 = new Person("Nitin", a);
		Person p2 = p1.clone();
		p2.address.city = "Delhi"; // also changes p1.address.city because address is shared
		System.out.println(p1.address.city); // prints: Delhi
	}
}

2) Making clone() produce a deep copy

To deep-copy using clone(), you must clone or copy mutable sub-objects too.

@Override
public Person clone() throws CloneNotSupportedException {
	Person copy = (Person) super.clone();
	// deep-copy mutable field(s)
	copy.address = this.address != null ? this.address.clone() : null;
	return copy;
}

Make sure every mutable nested type either implements Cloneable and clones itself, or you construct new instances for those fields manually.

3) Copy constructor / factory (recommended for clarity)

Pros: explicit, easier to reason about, works well with final fields. Cons: more boilerplate when there are many fields.

public class Person {
	public final String name;
	public final Address address;

	public Person(String name, Address address) {
		this.name = name;
		this.address = address;
	}

	// copy constructor (deep copy)
	public Person(Person other) {
		this.name = other.name;
		this.address = other.address == null ? null : new Address(other.address.city);
	}
}

4) Serialization-based deep copy (quick but slower)

Use when you want a simple one-liner deep copy for Serializable objects; performance and control are weaker than manual copying.

public static <T extends Serializable> T deepCopy(T obj) {
	try (ByteArrayOutputStream bos = new ByteArrayOutputStream();
		 ObjectOutputStream out = new ObjectOutputStream(bos)) {
		out.writeObject(obj);
		out.flush();
		try (ObjectInputStream in = new ObjectInputStream(
				new ByteArrayInputStream(bos.toByteArray()))) {
			@SuppressWarnings("unchecked")
			T copy = (T) in.readObject();
			return copy;
		}
	} catch (IOException | ClassNotFoundException e) {
		throw new RuntimeException("Deep copy failed", e);
	}
}

5) Third-party utilities

  • Apache Commons Lang SerializationUtils.clone(obj) for Serializable objects.
  • Libraries that convert-to/from JSON can be used for deep copy when fields are JSON-friendly (but beware of type and performance issues).

Checklist / steps when implementing cloning

  • Decide whether shallow copy is enough. If yes, implement clone() that calls super.clone() or provide a simple copy constructor.
  • If deep copy is needed:
    1. Ensure mutable nested types are copied (via clone(), copy constructor, or creating new instances).
    2. Handle nulls and immutable fields appropriately.
    3. Prefer copy constructors or factory methods for clearer semantics.

Notes and pitfalls

  • Object.clone() is a low-level mechanism and relies on a marker interface (Cloneable). Many Java developers prefer explicit copy constructors or factory methods because they are simpler and safer.
  • Be careful with final fields and inheritance — clone() and super.clone() can interact oddly with immutability and constructors.
  • Always document whether your copy is shallow or deep.

Quick summary

  • Shallow copy: cheap, copies references; shared mutable state can cause bugs.
  • Deep copy: independent copy of entire object graph; more work and costlier.
  • Prefer explicit copy constructors/factories for maintainability; use serialization or libraries when appropriate for quick deep copies.

References / further reading

  • Effective Java — prefer copy constructors or static factory methods over clone()
  • Javadoc for java.lang.Object.clone()

Tags:

Categories:

Updated: