🪆 Enums, Records & Nested Types · Intermediate

Object cloning in Java

Cloneable, shallow vs deep copy, prefer copy constructors.

🧩 The mysteryYou clone a shopping bag, add a pen to the copy… and the pen appears in the original too. Java did exactly what you asked. So what did you ask for?

How clone() works

To make a class cloneable: implement the **Cloneable** marker and override clone() — usually public, with your own class as return type — calling **super.clone()**. Object.clone() then makes a field-by-field copy.

class Bag implements Cloneable {
    List<String> items = new ArrayList<>();
    public Bag clone()
            throws CloneNotSupportedException {
        return (Bag) super.clone();
    }
}
🔮 Predict it

Copy of a grid

Arrays have clone() too, and it works the same way. What prints?

int[][] grid = {{1, 2}, {3, 4}};
int[][] copy = grid.clone();
copy[0][0] = 99;
System.out.println(grid[0][0]);
  1. 1
  2. 99
  3. Compile error
Show the answer

clone() copied the outer array — that is, the references to the rows. Both grids share the same row arrays, so the change shows up in the original. A cloned Bag shares its items list exactly the same way.

Shallow vs deep

Shallow copy: each field's value is copied — for a reference, that's just the address, so both objects share the referenced object. Deep copy: you also copy the mutable things you reference, so the two are fully independent.

public Doc clone()
        throws CloneNotSupportedException {
    Doc copy = (Doc) super.clone();
    copy.pages = pages.clone();   // deep
    return copy;
}
⚠️ The trap

Re-sharing instead of copying

copy.pages = pages; looks like copying but just re-shares the same array. Editing copy.pages would still change the original. The fix is copy.pages = pages.clone();.

Doc copy = (Doc) super.clone();
copy.pages = pages;   // still shared!

Clone vs copy constructor

✗ clone()
class Bag implements Cloneable {
    public Bag clone()
            throws CloneNotSupportedException {
        return (Bag) super.clone();
    }
}

Needs a marker, a cast and a checked exception — and it's still shallow.

✓ Copy constructor
class Bag {
    List<String> items = new ArrayList<>();
    Bag() { }
    Bag(Bag o) {
        items = new ArrayList<>(o.items);
    }
}

Plain Java: new Bag(a) gets its own list, so the two bags are independent.

🤔 Think first

Why experts avoid clone()

Effective Java recommends copy constructors or factories over clone(). Why?

Think about it, then reveal the answer

clone() skips constructors, needs casts and a checked exception, and clashes with final fields (you can't reassign them in the copy). A copy constructor runs normal initialization and can set final fields.

💼 In the real world

Defensive copies

Shared mutable state causes real bugs: a cached list that a caller modifies, or a Date handed out by a getter and changed outside. Professionals copy mutable data on the way in and out — or avoid the problem entirely with immutable types like records and List.copyOf.

Key takeaways

  1. Implement Cloneable and override clone() (public, covariant return)
  2. super.clone() copies fields shallowly
  3. Deep copy: also copy the mutable objects you reference
  4. Prefer copy constructors: new Order(other)

💡 Photocopying an address book copies the addresses, not the houses.

🤯 Did you know?

Arrays override clone() as public and return the exact array type, so int[] b = a.clone(); needs no cast and no try/catch — the friendliest clone in Java.

Practice questions

What does this print?

class Bag implements Cloneable {
    List<String> items = new ArrayList<>();
    public Bag clone() throws CloneNotSupportedException {
        return (Bag) super.clone();
    }
}
void main() throws Exception {
    Bag a = new Bag(), b = a.clone();
    b.items.add("pen");
    System.out.println(a.items);
}
  1. [pen]
  2. []
  3. Throws CloneNotSupportedException
  4. null
Check your answer

[pen]. The clone got a copy of the reference, not of the list. Adding through b changes the list a also uses.

What does this print?

class Bag {
    List<String> items;
    Bag() { items = new ArrayList<>(); }
    Bag(Bag o) { items = new ArrayList<>(o.items); }
}
void main() {
    Bag a = new Bag(), b = new Bag(a);
    b.items.add("pen");
    System.out.println(a.items.size() + " " + b.items.size());
}
  1. 0 1
  2. 1 1
  3. 0 0
  4. 1 0
Check your answer

0 1. The copy constructor builds a new list, so the two Bags are independent: a still has 0 items, b has 1.

Next world: exceptions — what really happens when code blows up, and how an error travels up the call stack like a fire alarm.