copying: deep vs shallow copy module (clone() traps, copy constructors, serialization copy, records, defensive copies, collection copies), 17 contract tests

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_017ic4b4X2xx7RrveVQU9R1N
This commit is contained in:
asmhatre
2026-11-12 20:18:06 +05:30
parent 682b7b067c
commit 38fbd513c9
29 changed files with 1201 additions and 0 deletions
+23
View File
@@ -0,0 +1,23 @@
import java.util.ArrayList;
import java.util.List;
/**
* Deliberately does not compile. Not part of the Maven build; scripts/broken.sh runs javac on it
* and commits the compiler's own message to output/02-final-field-clone.txt.
*
* The point: a Cloneable class cannot deep-copy a final field inside clone(), because clone()
* is not a constructor and final fields cannot be reassigned after construction.
*/
public class FinalFieldClone implements Cloneable {
private final List<String> items = new ArrayList<>();
@Override public FinalFieldClone clone() {
try {
FinalFieldClone copy = (FinalFieldClone) super.clone();
copy.items = new ArrayList<>(items); // error: cannot assign a value to final variable
return copy;
} catch (CloneNotSupportedException e) {
throw new AssertionError(e);
}
}
}
@@ -0,0 +1,133 @@
package com.ankurm.copying;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* Why Object.clone() has a bad reputation, one trap at a time.
* Run: scripts/run.sh CloneDemo
* The final-field trap is a compile error, so it lives in src/broken/ (scripts/broken.sh).
*/
public class CloneDemo {
/** The textbook clone(): super.clone() only. Shallow. */
static class Team implements Cloneable {
String name;
List<String> members;
Team(String name, List<String> members) {
this.name = name;
this.members = members;
System.out.println(" Team constructor ran for '" + name + "'");
}
@Override public Team clone() {
try {
return (Team) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError(e); // cannot happen: we implement Cloneable
}
}
}
/** Same class, but clone() finishes the job by copying the mutable field. */
static class DeepTeam implements Cloneable {
String name;
List<String> members;
DeepTeam(String name, List<String> members) {
this.name = name;
this.members = members;
}
@Override public DeepTeam clone() {
try {
DeepTeam copy = (DeepTeam) super.clone();
copy.members = new ArrayList<>(members); // the step people forget
return copy;
} catch (CloneNotSupportedException e) {
throw new AssertionError(e);
}
}
}
/** Overrides clone() but never declares Cloneable. */
static class ForgotCloneable {
@Override protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
/** A parent whose clone() builds a new object instead of calling super.clone(). */
static class Animal implements Cloneable {
String name;
Animal(String name) { this.name = name; }
@Override public Animal clone() {
return new Animal(name); // BUG: always returns an Animal, never a subclass
}
}
static class Dog extends Animal {
Dog(String name) { super(name); }
@Override public Dog clone() {
return (Dog) super.clone(); // compiles; fails at run time
}
}
public static void main(String[] args) {
System.out.println("== 1. clone() does not call a constructor, and it is shallow ==");
Team a = new Team("platform", new ArrayList<>(List.of("asha", "ravi")));
Team b = a.clone();
System.out.println(" b == a : " + (b == a));
System.out.println(" b.members == a.members : " + (b.members == a.members));
a.members.add("meera");
System.out.println(" after a.members.add(\"meera\"), b.members = " + b.members);
System.out.println();
System.out.println("== 2. The fix: copy the mutable field inside clone() ==");
DeepTeam c = new DeepTeam("platform", new ArrayList<>(List.of("asha", "ravi")));
DeepTeam d = c.clone();
c.members.add("meera");
System.out.println(" c.members = " + c.members);
System.out.println(" d.members = " + d.members);
System.out.println();
System.out.println("== 3. Calling super.clone() without implementing Cloneable ==");
try {
new ForgotCloneable().clone();
} catch (CloneNotSupportedException e) {
System.out.println(" " + e.getClass().getSimpleName() + ": " + e.getMessage());
}
System.out.println();
System.out.println("== 4. Array clone() is shallow for nested arrays ==");
int[][] grid = {{1, 2}, {3, 4}};
int[][] gridCopy = grid.clone();
gridCopy[0][0] = 99;
System.out.println(" gridCopy[0] == grid[0] : " + (gridCopy[0] == grid[0]));
System.out.println(" grid = " + Arrays.deepToString(grid));
System.out.println(" gridCopy = " + Arrays.deepToString(gridCopy));
System.out.println();
System.out.println("== 5. ArrayList.clone() is shallow too ==");
ArrayList<StringBuilder> original = new ArrayList<>();
original.add(new StringBuilder("abc"));
@SuppressWarnings("unchecked")
ArrayList<StringBuilder> cloned = (ArrayList<StringBuilder>) original.clone();
cloned.get(0).append("-changed");
System.out.println(" original.get(0) = " + original.get(0));
System.out.println(" same element object: " + (original.get(0) == cloned.get(0)));
System.out.println();
System.out.println("== 6. A parent clone() that uses 'new' breaks every subclass ==");
try {
new Dog("Rex").clone();
} catch (ClassCastException e) {
System.out.println(" " + e.getClass().getSimpleName() + ": " + e.getMessage());
}
}
}
@@ -0,0 +1,75 @@
package com.ankurm.copying;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Every standard way of copying a list, map or array is shallow. Here is what that means.
* Run: scripts/run.sh CollectionCopyDemo
*/
public class CollectionCopyDemo {
static class Box {
int value;
Box(int value) { this.value = value; }
Box(Box other) { this(other.value); }
@Override public String toString() { return "Box(" + value + ")"; }
}
public static void main(String[] args) {
System.out.println("== 1. new ArrayList<>(list) and List.copyOf share the elements ==");
List<Box> boxes = new ArrayList<>(List.of(new Box(1), new Box(2)));
List<Box> viaCtor = new ArrayList<>(boxes);
List<Box> viaCopyOf = List.copyOf(boxes);
boxes.get(0).value = 100;
System.out.println(" boxes = " + boxes);
System.out.println(" viaCtor = " + viaCtor);
System.out.println(" viaCopyOf = " + viaCopyOf);
boxes.add(new Box(3));
System.out.println(" after boxes.add(Box(3)): viaCtor.size() = " + viaCtor.size());
System.out.println();
System.out.println("== 2. The deep version needs the element copy, so a stream or a loop ==");
List<Box> deep = boxes.stream().map(Box::new).toList();
boxes.get(0).value = -1;
System.out.println(" boxes = " + boxes);
System.out.println(" deep = " + deep);
System.out.println();
System.out.println("== 3. HashMap copy constructor: new map, same values ==");
Map<String, Box> m = new HashMap<>();
m.put("k", new Box(5));
Map<String, Box> mCopy = new HashMap<>(m);
m.get("k").value = 55;
System.out.println(" mCopy.get(\"k\") = " + mCopy.get("k"));
System.out.println();
System.out.println("== 4. 1-D primitive arrays: clone / copyOf are true copies ==");
int[] a = {1, 2, 3};
int[] viaClone = a.clone();
int[] arrCopyOf = Arrays.copyOf(a, a.length);
a[0] = 99;
System.out.println(" a = " + Arrays.toString(a) + ", clone = " + Arrays.toString(viaClone)
+ ", copyOf = " + Arrays.toString(arrCopyOf));
System.out.println();
System.out.println("== 5. 2-D arrays: the rows are shared ==");
int[][] grid = {{1, 2}, {3, 4}};
int[][] shallow = Arrays.copyOf(grid, grid.length);
int[][] deepGrid = Arrays.stream(grid).map(int[]::clone).toArray(int[][]::new);
grid[0][0] = 99;
System.out.println(" grid = " + Arrays.deepToString(grid));
System.out.println(" shallow = " + Arrays.deepToString(shallow));
System.out.println(" deepGrid = " + Arrays.deepToString(deepGrid));
System.out.println();
System.out.println("== 6. Object arrays: copyOf copies references, not objects ==");
Box[] arr = {new Box(7)};
Box[] arrCopy = Arrays.copyOf(arr, arr.length);
arr[0].value = 77;
System.out.println(" arrCopy[0] = " + arrCopy[0] + ", same object: " + (arr[0] == arrCopy[0]));
}
}
@@ -0,0 +1,78 @@
package com.ankurm.copying;
import java.util.ArrayList;
import java.util.List;
/**
* Copy constructors and the one thing they cannot do: preserve the runtime subtype.
* Run: scripts/run.sh CopyConstructorDemo
*/
public class CopyConstructorDemo {
static class Animal {
final String name;
final List<String> tricks; // final is fine: constructors may assign it
Animal(String name, List<String> tricks) {
this.name = name;
this.tricks = new ArrayList<>(tricks);
}
/** Copy constructor. */
Animal(Animal other) {
this(other.name, other.tricks);
}
/** Virtual copy method: each subclass overrides it, so the runtime type survives. */
Animal copy() {
return new Animal(this);
}
@Override public String toString() {
return getClass().getSimpleName() + "(" + name + ", " + tricks + ")";
}
}
static class Dog extends Animal {
final int barkVolume;
Dog(String name, List<String> tricks, int barkVolume) {
super(name, tricks);
this.barkVolume = barkVolume;
}
Dog(Dog other) {
super(other);
this.barkVolume = other.barkVolume;
}
@Override Dog copy() {
return new Dog(this);
}
@Override public String toString() {
return super.toString() + " bark=" + barkVolume;
}
}
public static void main(String[] args) {
Dog rex = new Dog("Rex", List.of("sit", "roll"), 7);
System.out.println("== 1. Copy constructor: new object, own list, final fields intact ==");
Dog rex2 = new Dog(rex);
System.out.println(" rex = " + rex);
System.out.println(" rex2 = " + rex2);
System.out.println(" rex2.tricks == rex.tricks : " + (rex2.tricks == rex.tricks));
System.out.println();
System.out.println("== 2. The static type picks the constructor, so a subtype is sliced off ==");
Animal pet = rex; // static type Animal, runtime type Dog
Animal sliced = new Animal(pet);
System.out.println(" new Animal(pet) = " + sliced);
System.out.println();
System.out.println("== 3. A virtual copy() keeps the runtime type ==");
Animal right = pet.copy();
System.out.println(" pet.copy() = " + right);
}
}
@@ -0,0 +1,90 @@
package com.ankurm.copying;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
/**
* Rough cost of one deep copy by each technique. INDICATIVE ONLY: this is a plain
* System.nanoTime loop, not JMH, and the numbers drift between runs and machines.
* The ratio between techniques is the point, not the absolute figures.
* (For rigorous numbers see the jmh-basics module in this repository.)
* Run: scripts/run.sh CopyCostDemo
*/
public class CopyCostDemo {
static class Address implements Serializable, Cloneable {
String street, city;
Address(String street, String city) { this.street = street; this.city = city; }
Address(Address o) { this(o.street, o.city); }
@Override public Address clone() {
try { return (Address) super.clone(); } catch (CloneNotSupportedException e) { throw new AssertionError(e); }
}
}
static class Customer implements Serializable, Cloneable {
String name;
Address address;
List<String> tags;
Customer(String name, Address address, List<String> tags) {
this.name = name; this.address = address; this.tags = tags;
}
Customer(Customer o) { // copy constructor
this(o.name, new Address(o.address), new ArrayList<>(o.tags));
}
@Override public Customer clone() { // hand-written deep clone
try {
Customer c = (Customer) super.clone();
c.address = address.clone();
c.tags = new ArrayList<>(tags);
return c;
} catch (CloneNotSupportedException e) { throw new AssertionError(e); }
}
}
interface Copier { Customer copy(Customer c) throws Exception; }
static Customer viaSerialization(Customer c) throws IOException, ClassNotFoundException {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try (ObjectOutputStream out = new ObjectOutputStream(bytes)) { out.writeObject(c); }
try (ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bytes.toByteArray()))) {
return (Customer) in.readObject();
}
}
static long bestNanosPerCopy(Copier copier, Customer sample, int copiesPerRound) throws Exception {
long best = Long.MAX_VALUE;
Customer sink = null;
for (int round = 0; round < 7; round++) { // first rounds double as warm-up
long t0 = System.nanoTime();
for (int i = 0; i < copiesPerRound; i++) sink = copier.copy(sample);
best = Math.min(best, System.nanoTime() - t0);
}
if (sink == sample) throw new AssertionError(); // keep 'sink' observable
return best / copiesPerRound;
}
public static void main(String[] args) throws Exception {
Customer sample = new Customer("Asha", new Address("MG Road", "Pune"),
new ArrayList<>(List.of("vip", "newsletter", "beta")));
System.out.println("Indicative only: plain nanoTime loop, best of 7 rounds, ns per deep copy.");
System.out.println("Absolute numbers drift per machine and run; read the ratios.");
System.out.println();
long ctor = bestNanosPerCopy(Customer::new, sample, 200_000);
long clone = bestNanosPerCopy(Customer::clone, sample, 200_000);
long ser = bestNanosPerCopy(CopyCostDemo::viaSerialization, sample, 20_000);
System.out.printf("copy constructor : %7d ns%n", ctor);
System.out.printf("deep clone() : %7d ns%n", clone);
System.out.printf("serialization : %7d ns%n", ser);
System.out.printf("serialization is about %dx the copy constructor%n", Math.round((double) ser / ctor));
}
}
@@ -0,0 +1,27 @@
package com.ankurm.copying;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
* When List.copyOf really copies, and when it hands back the list it was given.
* Run: scripts/run.sh CopyOfIdentityDemo
*/
public class CopyOfIdentityDemo {
public static void main(String[] args) {
List<String> immutable = List.copyOf(new ArrayList<>(List.of("a")));
System.out.println("copyOf(immutable list) is the same instance : " + (List.copyOf(immutable) == immutable));
List<String> view = Collections.unmodifiableList(new ArrayList<>(List.of("a")));
System.out.println("copyOf(unmodifiableList view) is the same instance : " + (List.copyOf(view) == view));
try {
List.copyOf(Arrays.asList("a", null));
} catch (NullPointerException e) {
System.out.println("copyOf with a null element throws : " + e.getClass().getSimpleName());
}
}
}
@@ -0,0 +1,77 @@
package com.ankurm.copying;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* Defensive copies: protecting a class from callers who keep a reference to your internals.
* Run: scripts/run.sh DefensiveCopyDemo
*/
public class DefensiveCopyDemo {
/** Stores the caller's list and hands the same list back out. */
static class LeakyRoster {
private final List<String> names;
LeakyRoster(List<String> names) { this.names = names; }
List<String> getNames() { return names; }
int size() { return names.size(); }
}
/** Copies on the way in, and returns an unmodifiable snapshot on the way out. */
static class SafeRoster {
private final List<String> names;
SafeRoster(List<String> names) { this.names = List.copyOf(names); }
List<String> getNames() { return names; } // already unmodifiable, so no second copy
int size() { return names.size(); }
}
public static void main(String[] args) {
System.out.println("== 1. Leak through the constructor ==");
List<String> mine = new ArrayList<>(List.of("asha", "ravi"));
LeakyRoster leaky = new LeakyRoster(mine);
mine.clear();
System.out.println(" leaky.size() after the caller clears its own list : " + leaky.size());
System.out.println();
System.out.println("== 2. Leak through the getter ==");
LeakyRoster leaky2 = new LeakyRoster(new ArrayList<>(List.of("asha", "ravi")));
leaky2.getNames().add("intruder");
System.out.println(" leaky2.getNames() = " + leaky2.getNames());
System.out.println();
System.out.println("== 3. The defensive version ==");
List<String> mine2 = new ArrayList<>(List.of("asha", "ravi"));
SafeRoster safe = new SafeRoster(mine2);
mine2.clear();
System.out.println(" safe.size() after the caller clears its list : " + safe.size());
try {
safe.getNames().add("intruder");
} catch (UnsupportedOperationException e) {
System.out.println(" safe.getNames().add(...) -> " + e.getClass().getSimpleName());
}
System.out.println();
System.out.println("== 4. unmodifiableList is a VIEW; List.copyOf is a SNAPSHOT ==");
List<String> source = new ArrayList<>(List.of("a", "b"));
List<String> view = Collections.unmodifiableList(source);
List<String> snapshot = List.copyOf(source);
source.add("c");
System.out.println(" source = " + source);
System.out.println(" view = " + view);
System.out.println(" snapshot = " + snapshot);
System.out.println();
System.out.println("== 5. copyOf is shallow: the ELEMENTS are still shared ==");
List<StringBuilder> sbs = new ArrayList<>(List.of(new StringBuilder("x")));
List<StringBuilder> frozen = List.copyOf(sbs);
sbs.get(0).append("-mutated");
System.out.println(" frozen.get(0) = " + frozen.get(0));
}
}
@@ -0,0 +1,82 @@
package com.ankurm.copying;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
* Records are shallowly immutable: the fields cannot be reassigned, but what they point at can change.
* Run: scripts/run.sh RecordCopyDemo
*/
public class RecordCopyDemo {
/** Looks immutable. The list inside is whatever the caller handed in. */
record LeakyOrder(String id, List<String> items) { }
/** The compact constructor takes a private, unmodifiable snapshot. */
record SafeOrder(String id, List<String> items) {
SafeOrder {
items = List.copyOf(items); // also rejects null elements
}
/** The record equivalent of a 'wither': build a new record, never mutate. */
SafeOrder withItem(String item) {
List<String> next = new ArrayList<>(items);
next.add(item);
return new SafeOrder(id, next);
}
}
/** A record holding a mutable legacy type needs a defensive copy on the way in AND out. */
record SafeEvent(String name, Date when) {
SafeEvent {
when = new Date(when.getTime());
}
@Override public Date when() {
return new Date(when.getTime());
}
}
record LeakyEvent(String name, Date when) { }
public static void main(String[] args) {
System.out.println("== 1. A record does not copy the list you give it ==");
List<String> basket = new ArrayList<>(List.of("pen", "book"));
LeakyOrder leaky = new LeakyOrder("A-1", basket);
basket.add("laptop");
System.out.println(" leaky.items() = " + leaky.items());
leaky.items().add("phone");
System.out.println(" after leaky.items().add(\"phone\"), basket = " + basket);
System.out.println();
System.out.println("== 2. List.copyOf in the compact constructor ==");
List<String> basket2 = new ArrayList<>(List.of("pen", "book"));
SafeOrder safe = new SafeOrder("A-2", basket2);
basket2.add("laptop");
System.out.println(" safe.items() = " + safe.items());
try {
safe.items().add("phone");
} catch (UnsupportedOperationException e) {
System.out.println(" safe.items().add(...) -> " + e.getClass().getSimpleName());
}
System.out.println();
System.out.println("== 3. 'Changing' a record means building a new one ==");
SafeOrder bigger = safe.withItem("charger");
System.out.println(" safe = " + safe);
System.out.println(" bigger = " + bigger);
System.out.println();
System.out.println("== 4. A mutable Date component ==");
Date d1 = new Date(0L);
LeakyEvent le = new LeakyEvent("launch", d1);
d1.setTime(86_400_000L);
System.out.println(" LeakyEvent.when().getTime() after caller mutates d1 = " + le.when().getTime());
Date d2 = new Date(0L);
SafeEvent se = new SafeEvent("launch", d2);
d2.setTime(86_400_000L);
se.when().setTime(172_800_000L);
System.out.println(" SafeEvent.when().getTime() after both mutations = " + se.when().getTime());
}
}
@@ -0,0 +1,97 @@
package com.ankurm.copying;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.NotSerializableException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
/**
* Deep copy by serializing to bytes and reading back.
* Run: scripts/run.sh SerializationCopyDemo
*
* Use this only on data your own program produced a moment ago. Never feed
* ObjectInputStream bytes from outside; see the serialization post linked from the article.
*/
public class SerializationCopyDemo {
static class Dept implements Serializable {
String name;
Dept(String name) { this.name = name; }
}
static class Employee implements Serializable {
String name;
Dept dept;
List<String> skills = new ArrayList<>();
transient String sessionToken; // transient: skipped by serialization
Employee mentor; // lets us build a cycle
Employee(String name, Dept dept) { this.name = name; this.dept = dept; }
}
static class HoldsLock implements Serializable {
Object lock = new Object(); // java.lang.Object is not Serializable
}
@SuppressWarnings("unchecked")
static <T extends Serializable> T deepCopy(T obj) throws IOException, ClassNotFoundException {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try (ObjectOutputStream out = new ObjectOutputStream(bytes)) {
out.writeObject(obj);
}
try (ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bytes.toByteArray()))) {
return (T) in.readObject();
}
}
public static void main(String[] args) throws Exception {
Dept dept = new Dept("Platform");
Employee asha = new Employee("Asha", dept);
asha.skills.add("java");
asha.sessionToken = "secret-token";
System.out.println("== 1. A full deep copy: nothing is shared ==");
Employee copy = deepCopy(asha);
System.out.println(" copy.dept == asha.dept : " + (copy.dept == asha.dept));
System.out.println(" copy.skills == asha.skills : " + (copy.skills == asha.skills));
asha.skills.add("spring");
asha.dept.name = "Core";
System.out.println(" asha.skills = " + asha.skills + ", asha.dept = " + asha.dept.name);
System.out.println(" copy.skills = " + copy.skills + ", copy.dept = " + copy.dept.name);
System.out.println();
System.out.println("== 2. transient fields are lost ==");
System.out.println(" asha.sessionToken = " + asha.sessionToken);
System.out.println(" copy.sessionToken = " + copy.sessionToken);
System.out.println();
System.out.println("== 3. Sharing INSIDE the graph is preserved ==");
Employee ravi = new Employee("Ravi", dept); // same Dept object as asha
List<Employee> team = new ArrayList<>(List.of(asha, ravi));
List<Employee> teamCopy = deepCopy((ArrayList<Employee>) team);
System.out.println(" original: same Dept object : " + (team.get(0).dept == team.get(1).dept));
System.out.println(" copy : same Dept object : " + (teamCopy.get(0).dept == teamCopy.get(1).dept));
System.out.println(" copy dept is a new object : " + (teamCopy.get(0).dept != dept));
System.out.println();
System.out.println("== 4. Cycles are handled ==");
asha.mentor = ravi;
ravi.mentor = asha;
Employee cyc = deepCopy(asha);
System.out.println(" cyc.mentor.mentor == cyc : " + (cyc.mentor.mentor == cyc));
System.out.println(" cyc != asha : " + (cyc != asha));
System.out.println();
System.out.println("== 5. A non-serializable field fails at run time ==");
try {
deepCopy(new HoldsLock());
} catch (NotSerializableException e) {
System.out.println(" " + e.getClass().getSimpleName() + ": " + e.getMessage());
}
}
}
@@ -0,0 +1,75 @@
package com.ankurm.copying;
import java.util.ArrayList;
import java.util.List;
/**
* Assignment vs shallow copy vs deep copy, on one small object graph.
* Run: scripts/run.sh ShallowVsDeepDemo
*/
public class ShallowVsDeepDemo {
static class Address {
String city;
Address(String city) { this.city = city; }
/** Copy constructor: a new Address with the same content. */
Address(Address other) { this(other.city); }
@Override public String toString() { return city; }
}
static class Person {
String name;
Address address;
List<String> tags;
Person(String name, Address address, List<String> tags) {
this.name = name;
this.address = address;
this.tags = tags;
}
/** Shallow: a new Person, but address and tags are the SAME objects as in p. */
static Person shallowCopy(Person p) {
return new Person(p.name, p.address, p.tags);
}
/** Deep: every mutable object reachable from p is duplicated. */
static Person deepCopy(Person p) {
return new Person(p.name, new Address(p.address), new ArrayList<>(p.tags));
}
@Override public String toString() {
return name + " | " + address + " | " + tags;
}
}
public static void main(String[] args) {
Person original = new Person("Asha", new Address("Mumbai"),
new ArrayList<>(List.of("java", "spring")));
Person alias = original; // not a copy at all
Person shallow = Person.shallowCopy(original);
Person deep = Person.deepCopy(original);
System.out.println("== Identity before any change ==");
System.out.println("alias == original : " + (alias == original));
System.out.println("shallow == original : " + (shallow == original));
System.out.println("shallow.address == original.address : " + (shallow.address == original.address));
System.out.println("shallow.tags == original.tags : " + (shallow.tags == original.tags));
System.out.println("deep.address == original.address : " + (deep.address == original.address));
System.out.println("deep.tags == original.tags : " + (deep.tags == original.tags));
System.out.println();
System.out.println("== Now mutate the ORIGINAL (name, city, tags) ==");
original.name = "Asha K";
original.address.city = "Pune";
original.tags.add("jpa");
System.out.println("original : " + original);
System.out.println("alias : " + alias);
System.out.println("shallow : " + shallow);
System.out.println("deep : " + deep);
}
}
@@ -0,0 +1,182 @@
package com.ankurm.copying;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotSame;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.io.NotSerializableException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import org.junit.jupiter.api.Test;
/** Pins every claim the article makes about copying as an assertion. */
class CopyingTest {
@Test
void shallowCopySharesNestedObjectsDeepCopyDoesNot() {
var p = new ShallowVsDeepDemo.Person("Asha", new ShallowVsDeepDemo.Address("Mumbai"),
new ArrayList<>(List.of("java")));
var shallow = ShallowVsDeepDemo.Person.shallowCopy(p);
var deep = ShallowVsDeepDemo.Person.deepCopy(p);
assertNotSame(p, shallow);
assertSame(p.address, shallow.address);
assertSame(p.tags, shallow.tags);
assertNotSame(p.address, deep.address);
assertNotSame(p.tags, deep.tags);
}
@Test
void plainCloneIsShallowAndSkipsConstructor() {
var a = new CloneDemo.Team("t", new ArrayList<>(List.of("x")));
var b = a.clone();
assertSame(a.members, b.members);
}
@Test
void overriddenCloneCanDeepCopy() {
var c = new CloneDemo.DeepTeam("t", new ArrayList<>(List.of("x")));
var d = c.clone();
c.members.add("y");
assertEquals(List.of("x"), d.members);
}
@Test
void superCloneWithoutCloneableThrows() {
assertThrows(CloneNotSupportedException.class, () -> new CloneDemo.ForgotCloneable().clone());
}
@Test
void parentCloneUsingNewBreaksSubclass() {
assertThrows(ClassCastException.class, () -> new CloneDemo.Dog("Rex").clone());
}
@Test
void twoDimensionalArrayCloneSharesRows() {
int[][] grid = {{1, 2}, {3, 4}};
int[][] copy = grid.clone();
assertSame(grid[0], copy[0]);
int[][] deep = Arrays.stream(grid).map(int[]::clone).toArray(int[][]::new);
grid[0][0] = 99;
assertArrayEquals(new int[]{1, 2}, deep[0]);
}
@Test
void copyConstructorSlicesSubtypeButCopyMethodDoesNot() {
CopyConstructorDemo.Animal pet = new CopyConstructorDemo.Dog("Rex", List.of("sit"), 7);
assertEquals(CopyConstructorDemo.Animal.class, new CopyConstructorDemo.Animal(pet).getClass());
assertEquals(CopyConstructorDemo.Dog.class, pet.copy().getClass());
}
@Test
void serializationCopyIsDeepKeepsInternalSharingAndDropsTransient() throws Exception {
var dept = new SerializationCopyDemo.Dept("P");
var a = new SerializationCopyDemo.Employee("A", dept);
var b = new SerializationCopyDemo.Employee("B", dept);
a.sessionToken = "tok";
var team = new ArrayList<>(List.of(a, b));
var copy = SerializationCopyDemo.deepCopy(team);
assertNotSame(dept, copy.get(0).dept);
assertSame(copy.get(0).dept, copy.get(1).dept);
assertNull(copy.get(0).sessionToken);
}
@Test
void serializationCopyHandlesCycles() throws Exception {
var a = new SerializationCopyDemo.Employee("A", new SerializationCopyDemo.Dept("P"));
var b = new SerializationCopyDemo.Employee("B", a.dept);
a.mentor = b;
b.mentor = a;
var copy = SerializationCopyDemo.deepCopy(a);
assertSame(copy, copy.mentor.mentor);
}
@Test
void serializationCopyFailsOnNonSerializableField() {
assertThrows(NotSerializableException.class,
() -> SerializationCopyDemo.deepCopy(new SerializationCopyDemo.HoldsLock()));
}
@Test
void recordDoesNotCopyListUnlessYouDoIt() {
List<String> basket = new ArrayList<>(List.of("pen"));
var leaky = new RecordCopyDemo.LeakyOrder("1", basket);
basket.add("book");
assertEquals(2, leaky.items().size());
List<String> basket2 = new ArrayList<>(List.of("pen"));
var safe = new RecordCopyDemo.SafeOrder("2", basket2);
basket2.add("book");
assertEquals(1, safe.items().size());
assertThrows(UnsupportedOperationException.class, () -> safe.items().add("x"));
assertEquals(2, safe.withItem("x").items().size());
assertEquals(1, safe.items().size());
}
@Test
void recordDateComponentNeedsCopyInAndOut() {
Date d = new Date(0L);
var leaky = new RecordCopyDemo.LeakyEvent("e", d);
d.setTime(5L);
assertEquals(5L, leaky.when().getTime());
Date d2 = new Date(0L);
var safe = new RecordCopyDemo.SafeEvent("e", d2);
d2.setTime(5L);
safe.when().setTime(9L);
assertEquals(0L, safe.when().getTime());
}
@Test
void unmodifiableListIsAViewCopyOfIsASnapshot() {
List<String> src = new ArrayList<>(List.of("a"));
List<String> view = java.util.Collections.unmodifiableList(src);
List<String> snap = List.copyOf(src);
src.add("b");
assertEquals(2, view.size());
assertEquals(1, snap.size());
}
@Test
void copyOfIsShallow() {
List<StringBuilder> src = new ArrayList<>(List.of(new StringBuilder("x")));
List<StringBuilder> frozen = List.copyOf(src);
src.get(0).append("y");
assertEquals("xy", frozen.get(0).toString());
}
@Test
void defensiveRosterIsIsolated() {
List<String> mine = new ArrayList<>(List.of("a", "b"));
var safe = new DefensiveCopyDemo.SafeRoster(mine);
mine.clear();
assertEquals(2, safe.size());
var leaky = new DefensiveCopyDemo.LeakyRoster(mine = new ArrayList<>(List.of("a")));
mine.clear();
assertEquals(0, leaky.size());
}
@Test
void collectionCopiesShareElements() {
var b = new CollectionCopyDemo.Box(1);
List<CollectionCopyDemo.Box> src = new ArrayList<>(List.of(b));
var copy = new ArrayList<>(src);
var deep = src.stream().map(CollectionCopyDemo.Box::new).toList();
b.value = 9;
assertEquals(9, copy.get(0).value);
assertEquals(1, deep.get(0).value);
}
@Test
void copyOfReturnsSameInstanceForImmutableListsButCopiesViewsAndRejectsNulls() {
List<String> immutable = List.copyOf(new ArrayList<>(List.of("a")));
assertSame(immutable, List.copyOf(immutable));
List<String> view = java.util.Collections.unmodifiableList(new ArrayList<>(List.of("a")));
assertNotSame(view, List.copyOf(view));
assertThrows(NullPointerException.class, () -> List.copyOf(Arrays.asList("a", null)));
}
}