Skip to content
Part IA Michaelmas Term

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:

  1. Implement the Cloneable marker interface (empty — no methods).
  2. Override clone() as public.
  3. Call super.clone() to get the shallow copy.
  4. Manually deep-copy every mutable reference field.
  5. 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.
  • Cloneable is a magic marker interface with no methods — confusing and non-idiomatic.
  • The checked CloneNotSupportedException is pure ceremony.
  • clone() returns Object, 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.