Primitive assignments in Java duplicate the actual value:
int count = 10;
int tally = count;
This behavior applies to all eight primitive types. Object variables, however, store memory addresses rather than the objects themselves. Direct assignment therefore creates an alias, not an independent entity:
class Employee {
private int badgeId;
public int getBadgeId() { return badgeId; }
public void setBadgeId(int badgeId) { this.badgeId = badgeId; }
}
public class ReferenceDemo {
public static void main(String[] args) {
Employee first = new Employee();
first.setBadgeId(1001);
Employee second = first;
second.setBadgeId(2002);
System.out.println(first.getBadgeId()); // 2002
}
}
Because first and second reference the same heap address, mutations through either variable affect the single shared instance.
The java.lang.Object class defines a protected native method clone() that allocates fresh memory and copies field values. By convention, x.clone() should satisfy x.clone() != x, and the duplicate should share the same runtime class. Since the method is protected, subclasses that wish to expose duplication must override it with public access. The class must also implement java.lang.Cloneable; otherwise super.clone() throws CloneNotSupportedException.
Shallow Duplication
A shallow copy creates a new wrapper object but reuses references for any mutable member objects. Primitive fields receive literal copies, whereas reference fields point to the original nested instances.
To enable shallow duplication:
- Implement
Cloneable. - Override
clone()to delegate tosuper.clone().
class Employee implements Cloneable {
private int badgeId;
@Override
public Employee clone() {
try {
return (Employee) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
// accessors omitted
}
This is sufficient when the class contains only primitives or immutable references. Problems arise once mutable reference fields enter the model:
class Department {
private String location;
// accessors...
}
class Employee implements Cloneable {
private int badgeId;
private Department department;
@Override
public Employee clone() {
try {
return (Employee) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
// accessors...
}
A shallow clone of Employee duplicates the badgeId but shares the Department reference. Changing the department location on the original object therefore leaks into the clone.
Deep Duplication
To sever all reference ties, every mutable member must also be cloned and reassigned:
class Department implements Cloneable {
private String location;
@Override
public Department clone() {
try {
return (Department) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
// accessors...
}
class Employee implements Cloneable {
private int badgeId;
private Department department;
@Override
public Employee clone() {
try {
Employee copy = (Employee) super.clone();
copy.department = this.department.clone();
return copy;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
// accessors...
}
With this implementation, the cloned Employee owns a distinct Department instance, and subsequent changes to either hierarchy remain isolated.
Duplication via Serialization
Manually chaining clone() becomes impractical for deep object graphs with many nested levels. Serialization provides a mechanized alternative: marshal the entire graph to a byte stream and reconstruct it. The revived graph occupies entirely new memory, automatically replicating every reachable object.
All classes in the graph must implement java.io.Serializable:
import java.io.*;
class Division implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
// accessors...
}
class Employee implements Serializable {
private static final long serialVersionUID = 2L;
private int badgeId;
private Division division;
public Employee deepClone() {
try {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
try (ObjectOutputStream oos = new ObjectOutputStream(buffer)) {
oos.writeObject(this);
}
ByteArrayInputStream source = new ByteArrayInputStream(buffer.toByteArray());
try (ObjectInputStream ois = new ObjectInputStream(source)) {
return (Employee) ois.readObject();
}
} catch (IOException | ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
// accessors...
}
This approach avoids implementing Cloneable across the entire model and guarantees a complete deep copy regardless of graph complexity.