Why this matters
- The aliasing bug this causes is among the hardest to trace: you copy an object, change the copy, and the original changes too.
Cloneableis one of the JDK's acknowledged design mistakes, and knowing why is more useful than knowing how to use it.- Defensive copying is the practical skill underneath all of it, and it protects encapsulation at every API boundary.
input.add(pen) is visible to the caller. input = new Cart() is not — it only repoints the copy.
Shallow versus deep
class Address {
String city;
Address(String city) { this.city = city; }
}
class Person {
String name;
Address address; // a reference to a mutable object
Person(String name, Address address) {
this.name = name;
this.address = address;
}
}
Person original = new Person("Ana", new Address("Pune"));
Person shallow = new Person(original.name, original.address); // shares the Address
shallow.address.city = "Mumbai";
System.out.println(original.address.city); // Mumbai — the original changed
Person deep = new Person(original.name, new Address(original.address.city));
deep.address.city = "Delhi";
System.out.println(original.address.city); // Mumbai — independent
| Aspect | Shallow copy | Deep copy |
|---|---|---|
| New top-level object | Yes | Yes |
| Nested mutable objects | Shared with the original | Also duplicated |
| Independence | Only for primitive and immutable fields | Complete |
| Cost | Cheap | Proportional to the object graph |
| Safe when | Every field is primitive or immutable | Always |
New top-level object
Shallow copyYesDeep copyYesNested mutable objects
Shallow copyShared with the originalDeep copyAlso duplicatedIndependence
Shallow copyOnly for primitive and immutable fieldsDeep copyCompleteCost
Shallow copyCheapDeep copyProportional to the object graphSafe when
Shallow copyEvery field is primitive or immutableDeep copyAlways
A shallow copy is perfectly correct when nothing it points at can change.
How clone() actually works
Object.clone() is protected and native. It allocates a new object of the same class and copies
every field bit for bit — references included. To expose it you must do two things that look
arbitrary.
class Point implements Cloneable { // 1. the marker interface
int x, y;
@Override
public Point clone() { // 2. make it public, and narrow the return type
try {
return (Point) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError(e); // unreachable: we do implement Cloneable
}
}
}
Why this is considered broken
Cloneabledeclares no methods. It is a marker that changes the behaviour of aprotectedmethod on a different class — a mechanism found nowhere else in the language.clone()bypasses constructors. No validation runs, andfinalfields cannot be assigned, so invariants you rely on may not hold.- It throws a checked exception you cannot encounter. Every implementation needs the unreachable catch block above.
- It is shallow by default, in a method whose name implies otherwise.
- Subclasses inherit the problem. Any subclass adding a mutable field silently gets a broken clone unless it overrides again.
For a deep clone you must fix up each mutable field yourself:
class Person implements Cloneable {
String name;
Address address;
@Override
public Person clone() {
try {
Person copy = (Person) super.clone(); // shallow
copy.address = new Address(address.city); // then deepen, field by field
return copy;
} catch (CloneNotSupportedException e) {
throw new AssertionError(e);
}
}
}
Every mutable field must be remembered. Adding a field later and forgetting this method is the standard way this breaks months after it was written.
Better ways to copy
A copy constructor
The clearest option. It is ordinary code with no marker interface and no unreachable exception.
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 — deep by choice, visible in the signature. */
public Person(Person other) {
this.name = other.name;
this.address = new Address(other.address);
}
}
A static factory
The same thing with a name, which reads better at call sites and can return a subtype.
public static Person copyOf(Person other) {
return new Person(other);
}
Collection and array copies
List<String> copy = new ArrayList<>(original); // shallow: new list, same elements
List<String> snapshot = List.copyOf(original); // shallow and unmodifiable
int[] numbers = Arrays.copyOf(source, source.length); // fine: primitives have no sharing
// Deep copy of a list of mutable objects
List<Person> deep = original.stream()
.map(Person::new) // the copy constructor
.toList();
| Aspect | Approach | When to use it |
|---|---|---|
| Copy constructor | The default choice | Clear, validated, works with final fields |
| Static factory copyOf | When a name helps | Reads well, can return a subtype |
| clone() | Legacy code and arrays | array.clone() is idiomatic and fine |
| Serialization round-trip | Deep copy of a large graph | Slow; needs everything Serializable |
| Records and immutables | No copying needed | Nothing can change, so sharing is safe |
Copy constructor
ApproachThe default choiceWhen to use itClear, validated, works with final fieldsStatic factory copyOf
ApproachWhen a name helpsWhen to use itReads well, can return a subtypeclone()
ApproachLegacy code and arraysWhen to use itarray.clone() is idiomatic and fineSerialization round-trip
ApproachDeep copy of a large graphWhen to use itSlow; needs everything SerializableRecords and immutables
ApproachNo copying neededWhen to use itNothing can change, so sharing is safe
Immutability is the real answer. If nothing can be modified, there is nothing to copy.
Defensive copying at the boundary
This is where copying earns its place in production code: protecting an object's state from callers.
public final class Schedule {
private final List<LocalDate> dates;
public Schedule(List<LocalDate> dates) {
this.dates = List.copyOf(dates); // copy in: the caller's list cannot reach us
}
public List<LocalDate> dates() {
return dates; // safe: the copy is unmodifiable
}
}
Without the copy in the constructor, the caller keeps a reference to the list and can add to it
afterwards, changing a Schedule that promised to be immutable. LocalDate itself is immutable, so
the elements need no copying — which illustrates the general rule nicely.
Common misreadings
- "
clone()makes a deep copy." It copies fields bit for bit, so every reference is shared. - "
Cloneablerequires you to implement a method." It declares none. Omitting it just makessuper.clone()throw at run time. - "
new ArrayList<>(other)deep-copies." It copies the list structure only. - "Immutable classes need copy constructors." They do not. Sharing is already safe, which is the main argument for immutability.
- "
clone()should always be avoided."array.clone()is concise, correct and idiomatic for arrays. The criticism is aboutCloneableon your own classes. - "Serialization round-tripping is a good deep-copy trick." It works, but it is slow, requires everything to be
Serializable, and silently drops transient fields.
Quick recall
Everything you need if you only revisit this box.
- Shallow copies the object and shares its references; deep duplicates the graph.
clone()is shallow. Cloneableis a marker with no methods that changes aprotectednative method's behaviour, bypasses constructors, cannot setfinalfields, and forces an unreachable catch block.- Prefer a copy constructor or a static
copyOffactory. Both are ordinary, visible code. new ArrayList<>(other)andList.copyOf(other)copy the list, not the elements.array.clone()is the one placeclone()remains idiomatic.- Copy defensively at the boundary: on the way in so callers cannot reach your state, on the way out so they cannot modify it.
- The real fix is immutability — then no copy is needed at all.
Test yourself
Answer these before moving on — recall is what makes it stick.