Object Cloning in Java
You clone a car, rename the driver of the copy — and the original car changes too: after clonedCar.getDriver().setName("Vic") the car object also prints Vic. This is how clone() works by default, and it is exactly why every Java developer has to know the difference between a shallow copy and a deep copy.
Object cloning in Java is creating a new object with the same state as an existing one. The clone is a separate object (clone != original), but by default its fields point to the same nested objects. To clone an object you can use the clone() method together with the Cloneable interface, a copy constructor, a static copy factory, or serialization.
1. What is object cloning in Java
You need cloning when an object has to be changed without touching the original: for example, to return a copy from a method instead of the internal object, or to keep a snapshot of the current state. Java offers three main ways to copy an object:
- Overriding the clone() method and implementing the Cloneable interface.
- A copy constructor (or a static copy factory method).
- Using serialization.
Each way produces either a shallow copy — only the field values are copied, so nested objects are shared — or a deep copy, where nested mutable objects are copied too. Note that cloning and equality are different things: the clone is a new object, so clone == original is false, while clone.equals(original) depends on how the class implements equals().
2. The clone() method and the Cloneable interface
The clone() method is declared in the Object class as protected and makes a shallow copy by default. To make objects of a class cloneable, the class must implement the Cloneable interface and override clone() as public.
Cloneable is a marker interface: it has no methods and no constants. It only marks the class and allows Object.clone() to copy its instances. If you call Object.clone() on an object whose class does not implement Cloneable, it throws CloneNotSupportedException.
Since Java 5 an overridden clone() can return the class itself (Car) instead of Object. This is a covariant return type, so the calling code does not need a cast.
Shallow copy example
public class Car implements Cloneable {
private String name;
private Driver driver;
public Car(String name, Driver driver) {
this.name = name;
this.driver = driver;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Driver getDriver() {
return driver;
}
public void setDriver(Driver driver) {
this.driver = driver;
}
@Override
public Car clone() throws CloneNotSupportedException {
return (Car) super.clone();
}
@Override
public String toString() {
return "Car{name=" + name + ", driver=" + driver + "}";
}
} public class Driver implements Cloneable {
private String name;
private int age;
public Driver(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public Driver clone() throws CloneNotSupportedException {
return (Driver) super.clone();
}
@Override
public String toString() {
return "Driver{name=" + name + ", age=" + age + "}";
}
} public class CloneCarExample {
public static void main(String[] args) throws CloneNotSupportedException {
Car car = new Car("Truck", new Driver("Victor", 45));
Car clonedCar = car.clone();
System.out.println(car == clonedCar);
System.out.println(car.getDriver() == clonedCar.getDriver());
clonedCar.getDriver().setName("Vic");
System.out.println("Original: " + car);
System.out.println("Clone: " + clonedCar);
}
} false
true
Original: Car{name=Truck, driver=Driver{name=Vic, age=45}}
Clone: Car{name=Truck, driver=Driver{name=Vic, age=45}} Here an instance of Car is cloned. The copy is shallow: clonedCar is a new object, but its driver field points to the same Driver object as in car. That is why changing the driver through the clone is visible in the original. The name field is shared as well, but that is safe because String is immutable.
3. Deep copy with clone()
To make a deep copy with clone(), call super.clone() first and then copy every mutable reference field by hand, assigning the copy to the field of the clone. After that the clone and the original no longer share nested objects.
Deep copy example
@Override
public Car clone() {
try {
Car copy = (Car) super.clone();
if (driver != null) {
copy.driver = driver.clone(); // a separate copy of the driver
}
return copy;
} catch (CloneNotSupportedException e) {
throw new AssertionError(e); // cannot happen: Car and Driver implement Cloneable
}
} With this method the same main prints:
false
false
Original: Car{name=Truck, driver=Driver{name=Victor, age=45}}
Clone: Car{name=Truck, driver=Driver{name=Vic, age=45}} Notice that the method is declared without throws CloneNotSupportedException. The class implements Cloneable, so the exception cannot occur, and there is no reason to push a checked exception onto the caller — Joshua Bloch recommends the same in Effective Java. If a nested object holds mutable objects of its own, copy them the same way on every level.
4. Copy constructor and copy factory
A copy constructor in Java is a constructor that takes an object of the same class and copies its fields. It does not need Cloneable, throws no checked exceptions, works with final fields and shows explicitly which fields are copied deeply. Constructors in general are covered in the lesson on constructors.
Copy constructor with a shallow copy
public class Car {
private String name;
private Driver driver;
public Car(String name, Driver driver) {
this.name = name;
this.driver = driver;
}
/**
* Copy constructor.
*
* @param otherCar the car to copy; must not be null
*/
public Car(Car otherCar) {
this(otherCar.name, otherCar.driver);
}
// getters, setters and toString() - as in the example above
} Just like Object.clone(), this constructor copies the reference to Driver, so the result is a shallow copy. For a deep copy, add a copy constructor to Driver as well:
Copy constructor with a deep copy
// in the Driver class
public Driver(Driver other) {
this(other.name, other.age);
}
// in the Car class
public Car(Car otherCar) {
this(otherCar.name,
otherCar.driver == null ? null : new Driver(otherCar.driver));
} Car car = new Car("Truck", new Driver("Victor", 45));
Car copy = new Car(car);
copy.getDriver().setName("Vic");
System.out.println(car.getDriver().getName()); // Output: Victor A static copy factory is the same idea in the form of a static method with a meaningful name. It is handy when the copy should be returned as an interface or a subtype:
public static Car copyOf(Car other) {
return new Car(other);
} 5. Deep copy via serialization
Serialization gives a deep copy of the whole object graph without writing copy code for each field: the object is written to a byte array with ObjectOutputStream and immediately read back with ObjectInputStream. Every class in the graph must implement Serializable.
How serialization itself works, what the object graph is and what transient does is explained in the lesson Serialization and Deserialization in Java. Here is only the in-memory copy trick, without files. Both streams wrap ordinary byte streams and are closed with try-with-resources:
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
public final class DeepCopy {
private DeepCopy() {
}
@SuppressWarnings("unchecked")
public static <T extends Serializable> T copy(T original) {
try {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try (ObjectOutputStream out = new ObjectOutputStream(bytes)) {
out.writeObject(original);
}
try (ObjectInputStream in = new ObjectInputStream(
new ByteArrayInputStream(bytes.toByteArray()))) {
return (T) in.readObject();
}
} catch (IOException | ClassNotFoundException e) {
throw new IllegalStateException("Could not copy the object", e);
}
}
} // Car and Driver are declared with implements Serializable
Car car = new Car("Truck", new Driver("Victor", 45));
Car copy = DeepCopy.copy(car);
copy.getDriver().setName("Vic");
System.out.println(car.getDriver().getName()); // Output: Victor
System.out.println(copy.getDriver().getName()); // Output: Vic If at least one object in the graph is not serializable (for example, Driver without implements Serializable), the method throws IllegalStateException caused by java.io.NotSerializableException: Driver. Fields marked transient get default values in the copy (null, 0, false). This approach is much slower than clone() or a copy constructor, so use it for rare copies of complex graphs, not in hot code.
6. clone() gotchas: arrays, collections, inheritance
Most bugs with clone() come from two facts: it copies references, not the objects behind them, and it bypasses constructors. Below are the cases developers get wrong most often, both in real code and in interviews.
Cloning an array in Java: what will this code print?
int[] numbers = {1, 2, 3};
int[] numbersCopy = numbers.clone();
numbersCopy[0] = 100;
System.out.println(numbers[0]);
Driver[] drivers = {new Driver("Anna", 30)};
Driver[] driversCopy = drivers.clone();
driversCopy[0].setName("Olga");
System.out.println(drivers[0].getName());
System.out.println(drivers == driversCopy); 1
Olga
false For arrays, clone() is public, does not throw CloneNotSupportedException and returns an array of the same type without a cast. For an array of primitives this is a full copy. For an array of objects it is a shallow copy: a new array that holds the very same objects. A two-dimensional int[][] is also copied shallowly — the inner arrays are shared.
Collections and final fields
public class Garage implements Cloneable {
private final List<Car> cars = new ArrayList<>();
@Override
public Garage clone() throws CloneNotSupportedException {
Garage copy = (Garage) super.clone();
copy.cars = new ArrayList<>(cars); // compilation error:
// cannot assign a value to final variable cars
return copy;
}
} There are two problems here at once. Without the assignment line, the clone and the original share one list: adding a car to the copied garage changes the original one. And you cannot replace the list in the clone because the field is final — clone() does not work with final fields that need a deep copy. The fix is a copy constructor. Keep in mind that ArrayList.clone() returns Object and copies the list shallowly, just like new ArrayList<>(cars) or List.copyOf(cars) (see the Collections Framework). A deep copy of a list requires copying the elements:
List<Car> deepCopy = cars.stream()
.map(Car::new) // Car copy constructor
.collect(Collectors.toCollection(ArrayList::new)); Inheritance: new instead of super.clone()
class Vehicle implements Cloneable {
@Override
public Vehicle clone() {
return new Vehicle(); // mistake: should be super.clone()
}
}
class Truck extends Vehicle {
@Override
public Truck clone() {
return (Truck) super.clone(); // gets a Vehicle, not a Truck
}
}
new Truck().clone();
// java.lang.ClassCastException: class Vehicle cannot be cast to class Truck ... Object.clone() creates an object of the same class as the one being cloned, so a chain of super.clone() calls works correctly for any subclass. If even one class in the hierarchy creates the copy with new, all its subclasses get an object of the wrong type.
Other common mistakes
| Situation | What happens | How to fix |
|---|---|---|
The class calls super.clone() but does not implement Cloneable | At runtime: java.lang.CloneNotSupportedException: Car | Add implements Cloneable |
clone() is not overridden as public | Calling car.clone() from another class does not compile: clone() has protected access in java.lang.Object | Override it with the public modifier |
A field is a mutable object (Date, array, collection, your own class) | The clone and the original share it; changes are visible in both | Copy the field by hand after super.clone() |
A field is immutable (String, Integer, LocalDate) | The reference is shared, but that is safe | Nothing to do, no copy needed |
In practice
The Cloneable mechanism is widely considered a design flaw: clone() is declared not in the interface itself but in Object, it bypasses constructors and conflicts with final fields. In Effective Java, Joshua Bloch advises using a copy constructor or a copy factory for new classes and keeping clone() for arrays. You still need to know it: it shows up in legacy code, in the JDK (ArrayList, HashMap) and in interviews.
7. Comparison of cloning approaches
For your own classes, a copy constructor is usually the best choice: it is explicit, works with final fields and needs no exceptions. clone() is convenient for arrays, and serialization fits a one-off deep copy of a complex object graph.
| Approach | Default copy | Requirements | final fields | When to use |
|---|---|---|---|---|
clone() + Cloneable | Shallow | implements Cloneable, override public clone() | Cannot be reassigned in the clone | Arrays, subclasses of classes that already support clone() |
| Copy constructor | Whatever you write | None | Supported | Your own classes — the default choice |
Copy factory (copyOf) | Whatever you write | None | Supported | You need to return an interface/subtype or want a meaningful method name |
| Serialization | Deep (the whole graph) | All classes in the graph are Serializable | Supported | A rare deep copy of a complex graph; slow |
Frequently asked questions
Why can I not just call car.clone() without overriding the method?
In Object, clone() is declared protected, so a call from another class does not compile: clone() has protected access in java.lang.Object. Override it as public and implement Cloneable, otherwise the call fails at runtime instead. More details are in the section The clone() method and the Cloneable interface.
Is a constructor called when an object is cloned?
With clone() — no: Object.clone() allocates memory and copies field values directly, so validation and initialization in the constructor do not run, and instance counters are not incremented. A copy constructor, on the other hand, is an ordinary constructor, and all of its logic runs.
Do I need to clone records and immutable objects?
No. The state of an immutable object (String, a record with immutable components) cannot change, so one reference can be safely shared in many places. Records do not implement Cloneable; if you need a variant with a different value, create a new instance through the constructor.
What is the fastest way to copy an object in Java?
clone() and a copy constructor are comparable in speed — the difference is usually invisible next to the rest of the code. Copying via serialization is much slower: it encodes the whole object graph into bytes and parses it back. All approaches are compared in the section Comparison of cloning approaches.
Comments