Copy Constructors and the clone() Pitfall
Copy Constructors — The Idiomatic Approach
A copy constructor takes another instance of the same class and copies its state deeply where needed. This is the simplest, safest, most explicit way to copy in Java:
class Team {
private List<String> members;
Team(Team other) {
this.members = new ArrayList<>(other.members);
}
}
Advantages of copy constructors:
- Explicit: the caller knows exactly what kind of copy is being made.
- Type-safe: the return type is always the right class — no casting.
- No interface requirement: no need to implement
Cloneable. - Control: you decide field-by-field how to copy (shallow, deep, or a mix).
Object.clone() — The Problematic Alternative
Object.clone() is a native method that does a shallow field-for-field copy by default. Using it correctly requires a precise (and error-prone) ritual:
- Implement the
Cloneablemarker interface (empty — no methods). - Override
clone()aspublic. - Call
super.clone()to get the shallow copy. - Manually deep-copy every mutable reference field.
- Return the clone.
If you forget step 4, the clone shares mutable internals with the original — the Car/Tyre bug.
Full Car/Tyre Example with clone()
class Tyre implements Cloneable {
private int pressure;
Tyre(int pressure) { this.pressure = pressure; }
void inflate(int amount) { this.pressure += amount; }
int getPressure() { return pressure; }
@Override
public Tyre clone() {
try {
return (Tyre) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError("Cloneable guarantees this won't happen");
}
}
}
class Car implements Cloneable {
private Tyre[] tyres;
Car() {
tyres = new Tyre[] {
new Tyre(30), new Tyre(30), new Tyre(30), new Tyre(30)
};
}
Tyre getTyre(int i) { return tyres[i]; }
@Override
public Car clone() {
try {
Car cloned = (Car) super.clone(); // (1) shallow copy
cloned.tyres = new Tyre[tyres.length]; // (2) new array
for (int i = 0; i < tyres.length; i++) { // (3) clone each tyre
cloned.tyres[i] = tyres[i].clone();
}
return cloned; // (4) return
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
}
The catch (CloneNotSupportedException e) block is boilerplate required by the checked exception in Object.clone()’s signature — despite the fact that implementing Cloneable guarantees the exception will never be thrown.
Array Cloning
array.clone() always produces a shallow copy:
int[] nums = {1, 2, 3};
int[] copy = nums.clone();
copy[0] = 99;
// nums[0] is still 1 — safe for primitives
Tyre[] tyres = { new Tyre(30), new Tyre(30) };
Tyre[] shallow = tyres.clone();
shallow[0].inflate(10);
// tyres[0] also inflated — shared Tyre objects
For arrays of primitives, clone() is safe because primitives are copied by value. For arrays of references, clone() shares the referents — you must manually clone each element for a deep copy.
Arrays of arrays require two levels of cloning:
int[][] matrix = new int[3][3];
int[][] shallow = matrix.clone(); // shares the inner arrays
int[][] deep = new int[3][];
for (int i = 0; i < 3; i++) {
deep[i] = matrix[i].clone(); // clone each inner array
}
Why Many Style Guides Reject clone()
- The shallow-default behaviour is a trap for the unwary.
Cloneableis a magic marker interface with no methods — confusing and non-idiomatic.- The checked
CloneNotSupportedExceptionis pure ceremony. clone()returnsObject, requiring casts at every call site.- The contract is vague about depth — callers cannot tell from the signature whether they are getting a deep or shallow clone.
Copy constructors and copy factory methods avoid all these issues. The clone() mechanism is widely considered a design mistake in the Java platform — it exists, it works, but you should prefer copy constructors unless you have a specific reason to use it.