JAVA / NESTED CLASSES AND THE OBJECT CONTRACT
Copying objects safely without a broken clone
Copy objects with copy constructors, factories, or a polymorphic copy(), and see why Object.clone with Cloneable gives shallow, invariant-skipping copies.
What you will learn
- Write a copy constructor or static copy factory instead of implementing Cloneable
- Decide per field: share immutable values, rebuild mutable ones like lists and arrays
- Add an overridable copy() when callers must copy through a supertype reference
- Copy mutable state on the way in and on the way out of accessors
Understanding Copying objects safely without a broken clone
Object.clone() does not build an object the way new does. It allocates an instance of the same runtime class, skips every constructor, and copies each field's value straight across; for a reference field the value is the reference itself, so the original and the copy end up naming the same ArrayList or the same array. None of the validation or defensive copying your constructor performs is repeated, and a subclass cannot patch things up afterwards for a final field, because a final field can only be assigned during construction.
Cloneable does not rescue this, because it is a marker interface that declares no methods at all. It does not give your class a clone() method, Object.clone() is protected so thing.clone() from outside the class still does not compile, and typing a variable as Cloneable buys you nothing. The only thing the interface does is flip a runtime check: without it Object.clone() throws CloneNotSupportedException, a checked exception you are forced to catch even in a class where it can never be thrown.
The useful mental model is that copying is a constructor's job, and the only real question is which fields the new object must own rather than share. Immutable components such as String, int, LocalDate, or another value object are safe to share because nobody can change them behind your back; mutable ones such as collections, arrays, StringBuilder, or objects you created must be rebuilt, and if their elements are mutable too you have to decide how far down to go. A copy constructor or a static copyOf factory runs the real constructor, works with final fields, throws no checked exception, and can be documented; when callers hold a supertype reference and do not know the concrete class, add a copy() method to that supertype and let each subclass implement it.
import java.util.ArrayList;
import java.util.List;
class Basket implements Cloneable {
String owner;
List<String> items = new ArrayList<>();
Basket(String owner) { this.owner = owner; }
@Override
public Basket clone() { // field-for-field, exactly one level deep
try {
return (Basket) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError(e); // cannot happen: we implement Cloneable
}
}
}
class Cart {
private final String owner;
private final List<String> items;
Cart(String owner, List<String> items) {
this.owner = owner;
this.items = new ArrayList<>(items); // own the list, do not adopt it
}
Cart(Cart other) { // copy constructor
this(other.owner, other.items); // reuse the real constructor
}
void add(String item) { items.add(item); }
@Override
public String toString() { return owner + " -> " + items; }
}
public class Main {
public static void main(String[] args) {
Basket b1 = new Basket("ana");
b1.items.add("apple");
Basket b2 = b1.clone();
b2.items.add("pear");
System.out.println("clone: " + b1.items + " " + b2.items);
System.out.println("shared: " + (b1.items == b2.items));
Cart c1 = new Cart("ana", List.of("apple"));
Cart c2 = new Cart(c1);
c2.add("pear");
System.out.println("copy: " + c1 + " | " + c2);
}
}A copy is something a constructor produces field by field, deciding what the new object owns versus shares, not something inherited from Object.clone().
Worked examples
A copy() method for copying unknown subtypes
Copying a tree through a supertype reference, which a copy constructor cannot do because it always builds the class it is declared in.
import java.util.ArrayList;
import java.util.List;
abstract class Node {
final String id;
Node(String id) { this.id = id; }
abstract Node copy();
@Override public String toString() { return getClass().getSimpleName() + "(" + id + ")"; }
}
class Leaf extends Node {
Leaf(String id) { super(id); }
@Override Leaf copy() { return new Leaf(id); }
}
class Group extends Node {
final List<Node> children = new ArrayList<>();
Group(String id) { super(id); }
@Override Group copy() {
Group g = new Group(id);
for (Node child : children) g.children.add(child.copy());
return g;
}
}
public class Main {
public static void main(String[] args) {
Group root = new Group("root");
root.children.add(new Leaf("a"));
Group duplicate = root.copy();
duplicate.children.add(new Leaf("b"));
System.out.println(root.children);
System.out.println(duplicate.children);
System.out.println(root.children.get(0) == duplicate.children.get(0));
}
}Example explained
Line 1abstract Node copy() puts copying into the type system, so a List<Node> can be duplicated without knowing each element's concrete class.
Line 2Leaf.copy() narrows the return type to Leaf (a covariant override), so callers holding a Leaf get a Leaf back with no cast.
Line 3Group.copy() calls child.copy() instead of copying the child references, which is what makes the new subtree independent.
Line 4The trailing false proves the two trees share no Leaf objects; g.children.addAll(children) would have printed true.
An immutable object still needs copies at its edges
A record holding an array must copy in the constructor and copy again in the accessor, since a record is only as immutable as its components (records need Java 16 or later).
import java.util.Arrays;
record Reading(String sensor, double[] samples) {
Reading { // compact canonical constructor
samples = samples.clone(); // copy in
}
@Override public double[] samples() {
return samples.clone(); // copy out
}
}
public class Main {
public static void main(String[] args) {
double[] raw = { 1.0, 2.0 };
Reading r = new Reading("t1", raw);
raw[0] = 99.0; // caller mutates its own array
System.out.println(Arrays.toString(r.samples()));
r.samples()[1] = 42.0; // mutates a throwaway array
System.out.println(Arrays.toString(r.samples()));
System.out.println(r.samples() == r.samples());
}
}Example explained
Line 1The compact constructor reassigns the parameter before it is stored, so the record keeps its own array and raw[0] = 99.0 cannot reach it.
Line 2Overriding the accessor closes the other hole: r.samples()[1] = 42.0 writes into a fresh array that is immediately discarded.
Line 3r.samples() == r.samples() is false because every call allocates, which is the real cost of this pattern.
Line 4Arrays are the one place clone() is pleasant: it is public, needs no Cloneable, and already returns double[] rather than Object.
Important notes
Cloneable declares no methods; it only stops Object.clone() from throwing CloneNotSupportedException, and because Object.clone() is protected you still cannot call clone() through a Cloneable-typed variable.
Array clone() is safe but still one level deep: String[][] rows = grid.clone() gives you a new outer array whose entries are the original rows.
Common mistakes
Adding implements Cloneable and returning super.clone() unchanged from a class that holds a List or an array: both objects point at the same list, so items.add on the copy silently appears in the original.
Assuming new ArrayList<>(other.items) is a deep copy: it duplicates the list but not the elements, so a List<Date> or List<int[]> copy still shares every element with the original.
Copying carefully in the constructor and then returning the field itself from a getter: the caller mutates the internal collection directly and the copy diverges from what your class believes it contains.
Try it yourself
Change, predict, then run
Write a class Route with a String name and a List<double[]> waypoints, then add a copy constructor such that adding a waypoint to the copy and editing the copy's first coordinate both leave the original untouched. Print both routes to prove it.
Open the Java workspaceCheck your understanding
A class Foo has private final List<String> tags initialised in its constructor, implements Cloneable, and overrides clone() as return (Foo) super.clone(). What is true of Foo b = a.clone()?
- b gets its own list holding the same String objects as a's list.
- a and b share one list object, so b.tags.add("x") is visible through a.
- The call fails at runtime because tags is final and clone cannot assign it.
- The code fails to compile because Object.clone() cannot copy a List field.
Show answer
Object.clone() copies the value of each field, and the value of a reference field is the reference, so both objects end up naming the same ArrayList: no constructor runs and no new list is ever allocated. The first option describes what a copy constructor doing new ArrayList<>(other.tags) would produce. final is not an obstacle either, since super.clone() writes the field directly during allocation; final only prevents your own clone() body from reassigning it afterwards.