diff --git a/README.md b/README.md
index a84db9f..1de5063 100644
--- a/README.md
+++ b/README.md
@@ -30,6 +30,7 @@ article; each module's own README has that article's version table, quickstart,
| [`jmh-basics`](jmh-basics/) | JMH Tutorial: Writing Java Microbenchmarks That Don't Lie |
| [`string-format`](string-format/) | String.format, formatted() and printf in Java: The Complete Cheat Sheet |
| [`equals-hashcode`](equals-hashcode/) | equals() and hashCode() in Java: The Contract, Real Bugs and How Records Change It |
+| [`copying`](copying/) | Deep Copy vs Shallow Copy in Java: clone(), Copy Constructors, Serialization and Records |
## License
diff --git a/copying/README.md b/copying/README.md
new file mode 100644
index 0000000..28b118a
--- /dev/null
+++ b/copying/README.md
@@ -0,0 +1,42 @@
+# copying
+
+Companion code for the ankurm.com post *"Deep Copy vs Shallow Copy in Java: clone(), Copy Constructors, Serialization and Records."*
+Module in `java-core-examples`, the Java-core series. There is no `docs/` folder: the deeper material lives in the post itself, inside its expandable sections.
+
+## Versions this was built and tested against
+
+| Component | Version | Notes |
+|---|---|---|
+| JDK | 25.0.5+8 (Temurin, LTS) | Records, `List.copyOf`, `Arrays.copyOf`. |
+| JUnit Jupiter | 5.11.0 | Same as the sibling modules. |
+| Maven | 3.9.11 | |
+
+## Quickstart
+
+```bash
+export JAVA_HOME=/path/to/jdk-25
+mvn compile
+scripts/run.sh CloneDemo # one demo
+scripts/broken.sh # the deliberate compile error
+scripts/run-all.sh # regenerate every file in output/
+```
+
+## What's in here
+
+| File | What it shows |
+|---|---|
+| `ShallowVsDeepDemo.java` | Assignment vs shallow copy vs deep copy on one `Person` graph. |
+| `CloneDemo.java` | `clone()` skips the constructor, is shallow, needs `Cloneable`, and a parent that uses `new` breaks subclasses. Arrays and `ArrayList.clone()` are shallow too. |
+| `src/broken/FinalFieldClone.java` | Does not compile: a `final` field cannot be deep-copied inside `clone()`. Not part of the Maven build. |
+| `CopyConstructorDemo.java` | Copy constructors with `final` fields, subtype slicing, and a virtual `copy()` that fixes it. |
+| `SerializationCopyDemo.java` | Deep copy via `ObjectOutputStream`: `transient` loss, shared references and cycles preserved, `NotSerializableException`. |
+| `RecordCopyDemo.java` | Records are only shallowly immutable: `List.copyOf` in the compact constructor, withers, a mutable `Date` component. |
+| `DefensiveCopyDemo.java` | Leaks through constructor and getter; `unmodifiableList` (view) vs `List.copyOf` (snapshot). |
+| `CollectionCopyDemo.java` | Every standard list, map and array copy is shallow; the deep versions. |
+| `CopyOfIdentityDemo.java` | When `List.copyOf` returns the same instance and when it copies; null elements. |
+| `CopyCostDemo.java` | Indicative cost of copy constructor vs `clone()` vs serialization. Timing: results drift per run. |
+| `CopyingTest.java` | Pins every claim above as an assertion (17 tests). |
+| `output/01-11` | Captured runs of everything above. `10-cost.txt` is the only non-deterministic file. |
+
+## License
+MIT - see [LICENSE](../LICENSE).
diff --git a/copying/output/01-shallow-vs-deep.txt b/copying/output/01-shallow-vs-deep.txt
new file mode 100644
index 0000000..0f43a8e
--- /dev/null
+++ b/copying/output/01-shallow-vs-deep.txt
@@ -0,0 +1,13 @@
+== Identity before any change ==
+alias == original : true
+shallow == original : false
+shallow.address == original.address : true
+shallow.tags == original.tags : true
+deep.address == original.address : false
+deep.tags == original.tags : false
+
+== Now mutate the ORIGINAL (name, city, tags) ==
+original : Asha K | Pune | [java, spring, jpa]
+alias : Asha K | Pune | [java, spring, jpa]
+shallow : Asha | Pune | [java, spring, jpa]
+deep : Asha | Mumbai | [java, spring]
diff --git a/copying/output/02-final-field-clone.txt b/copying/output/02-final-field-clone.txt
new file mode 100644
index 0000000..9ecc4a6
--- /dev/null
+++ b/copying/output/02-final-field-clone.txt
@@ -0,0 +1,5 @@
+$ javac src/broken/FinalFieldClone.java
+src/broken/FinalFieldClone.java:17: error: cannot assign a value to final variable items
+ copy.items = new ArrayList<>(items); // error: cannot assign a value to final variable
+ ^
+1 error
diff --git a/copying/output/03-clone.txt b/copying/output/03-clone.txt
new file mode 100644
index 0000000..6e90228
--- /dev/null
+++ b/copying/output/03-clone.txt
@@ -0,0 +1,24 @@
+== 1. clone() does not call a constructor, and it is shallow ==
+ Team constructor ran for 'platform'
+ b == a : false
+ b.members == a.members : true
+ after a.members.add("meera"), b.members = [asha, ravi, meera]
+
+== 2. The fix: copy the mutable field inside clone() ==
+ c.members = [asha, ravi, meera]
+ d.members = [asha, ravi]
+
+== 3. Calling super.clone() without implementing Cloneable ==
+ CloneNotSupportedException: com.ankurm.copying.CloneDemo$ForgotCloneable
+
+== 4. Array clone() is shallow for nested arrays ==
+ gridCopy[0] == grid[0] : true
+ grid = [[99, 2], [3, 4]]
+ gridCopy = [[99, 2], [3, 4]]
+
+== 5. ArrayList.clone() is shallow too ==
+ original.get(0) = abc-changed
+ same element object: true
+
+== 6. A parent clone() that uses 'new' breaks every subclass ==
+ ClassCastException: class com.ankurm.copying.CloneDemo$Animal cannot be cast to class com.ankurm.copying.CloneDemo$Dog (com.ankurm.copying.CloneDemo$Animal and com.ankurm.copying.CloneDemo$Dog are in unnamed module of loader 'app')
diff --git a/copying/output/04-copy-constructor.txt b/copying/output/04-copy-constructor.txt
new file mode 100644
index 0000000..e500a8d
--- /dev/null
+++ b/copying/output/04-copy-constructor.txt
@@ -0,0 +1,10 @@
+== 1. Copy constructor: new object, own list, final fields intact ==
+ rex = Dog(Rex, [sit, roll]) bark=7
+ rex2 = Dog(Rex, [sit, roll]) bark=7
+ rex2.tricks == rex.tricks : false
+
+== 2. The static type picks the constructor, so a subtype is sliced off ==
+ new Animal(pet) = Animal(Rex, [sit, roll])
+
+== 3. A virtual copy() keeps the runtime type ==
+ pet.copy() = Dog(Rex, [sit, roll]) bark=7
diff --git a/copying/output/05-serialization.txt b/copying/output/05-serialization.txt
new file mode 100644
index 0000000..e37e3ba
--- /dev/null
+++ b/copying/output/05-serialization.txt
@@ -0,0 +1,21 @@
+== 1. A full deep copy: nothing is shared ==
+ copy.dept == asha.dept : false
+ copy.skills == asha.skills : false
+ asha.skills = [java, spring], asha.dept = Core
+ copy.skills = [java], copy.dept = Platform
+
+== 2. transient fields are lost ==
+ asha.sessionToken = secret-token
+ copy.sessionToken = null
+
+== 3. Sharing INSIDE the graph is preserved ==
+ original: same Dept object : true
+ copy : same Dept object : true
+ copy dept is a new object : true
+
+== 4. Cycles are handled ==
+ cyc.mentor.mentor == cyc : true
+ cyc != asha : true
+
+== 5. A non-serializable field fails at run time ==
+ NotSerializableException: java.lang.Object
diff --git a/copying/output/06-records.txt b/copying/output/06-records.txt
new file mode 100644
index 0000000..9e4fe7b
--- /dev/null
+++ b/copying/output/06-records.txt
@@ -0,0 +1,15 @@
+== 1. A record does not copy the list you give it ==
+ leaky.items() = [pen, book, laptop]
+ after leaky.items().add("phone"), basket = [pen, book, laptop, phone]
+
+== 2. List.copyOf in the compact constructor ==
+ safe.items() = [pen, book]
+ safe.items().add(...) -> UnsupportedOperationException
+
+== 3. 'Changing' a record means building a new one ==
+ safe = SafeOrder[id=A-2, items=[pen, book]]
+ bigger = SafeOrder[id=A-2, items=[pen, book, charger]]
+
+== 4. A mutable Date component ==
+ LeakyEvent.when().getTime() after caller mutates d1 = 86400000
+ SafeEvent.when().getTime() after both mutations = 0
diff --git a/copying/output/07-defensive.txt b/copying/output/07-defensive.txt
new file mode 100644
index 0000000..c79cc6a
--- /dev/null
+++ b/copying/output/07-defensive.txt
@@ -0,0 +1,17 @@
+== 1. Leak through the constructor ==
+ leaky.size() after the caller clears its own list : 0
+
+== 2. Leak through the getter ==
+ leaky2.getNames() = [asha, ravi, intruder]
+
+== 3. The defensive version ==
+ safe.size() after the caller clears its list : 2
+ safe.getNames().add(...) -> UnsupportedOperationException
+
+== 4. unmodifiableList is a VIEW; List.copyOf is a SNAPSHOT ==
+ source = [a, b, c]
+ view = [a, b, c]
+ snapshot = [a, b]
+
+== 5. copyOf is shallow: the ELEMENTS are still shared ==
+ frozen.get(0) = x-mutated
diff --git a/copying/output/08-collections.txt b/copying/output/08-collections.txt
new file mode 100644
index 0000000..1daca50
--- /dev/null
+++ b/copying/output/08-collections.txt
@@ -0,0 +1,23 @@
+== 1. new ArrayList<>(list) and List.copyOf share the elements ==
+ boxes = [Box(100), Box(2)]
+ viaCtor = [Box(100), Box(2)]
+ viaCopyOf = [Box(100), Box(2)]
+ after boxes.add(Box(3)): viaCtor.size() = 2
+
+== 2. The deep version needs the element copy, so a stream or a loop ==
+ boxes = [Box(-1), Box(2), Box(3)]
+ deep = [Box(100), Box(2), Box(3)]
+
+== 3. HashMap copy constructor: new map, same values ==
+ mCopy.get("k") = Box(55)
+
+== 4. 1-D primitive arrays: clone / copyOf are true copies ==
+ a = [99, 2, 3], clone = [1, 2, 3], copyOf = [1, 2, 3]
+
+== 5. 2-D arrays: the rows are shared ==
+ grid = [[99, 2], [3, 4]]
+ shallow = [[99, 2], [3, 4]]
+ deepGrid = [[1, 2], [3, 4]]
+
+== 6. Object arrays: copyOf copies references, not objects ==
+ arrCopy[0] = Box(77), same object: true
diff --git a/copying/output/09-tests.txt b/copying/output/09-tests.txt
new file mode 100644
index 0000000..0629364
--- /dev/null
+++ b/copying/output/09-tests.txt
@@ -0,0 +1,3 @@
+[INFO] Tests run: 17, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.145 s -- in com.ankurm.copying.CopyingTest
+[INFO] Tests run: 17, Failures: 0, Errors: 0, Skipped: 0
+[INFO] BUILD SUCCESS
diff --git a/copying/output/10-cost.txt b/copying/output/10-cost.txt
new file mode 100644
index 0000000..afbe383
--- /dev/null
+++ b/copying/output/10-cost.txt
@@ -0,0 +1,7 @@
+Indicative only: plain nanoTime loop, best of 7 rounds, ns per deep copy.
+Absolute numbers drift per machine and run; read the ratios.
+
+copy constructor : 14 ns
+deep clone() : 13 ns
+serialization : 9765 ns
+serialization is about 698x the copy constructor
diff --git a/copying/output/11-copyof-identity.txt b/copying/output/11-copyof-identity.txt
new file mode 100644
index 0000000..0d8c488
--- /dev/null
+++ b/copying/output/11-copyof-identity.txt
@@ -0,0 +1,3 @@
+copyOf(immutable list) is the same instance : true
+copyOf(unmodifiableList view) is the same instance : false
+copyOf with a null element throws : NullPointerException
diff --git a/copying/pom.xml b/copying/pom.xml
new file mode 100644
index 0000000..7a65f77
--- /dev/null
+++ b/copying/pom.xml
@@ -0,0 +1,46 @@
+
+
+ 4.0.0
+
+
+ com.ankurm
+ java-core-examples
+ 1.0
+
+
+ copying
+ copying
+ Deep copy vs shallow copy in Java: clone(), copy constructors, serialization, records and defensive copies.
+
+
+
+ org.junit.jupiter
+ junit-jupiter
+ 5.11.0
+ test
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.13.0
+
+ 25
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 3.2.5
+
+ -Dstdout.encoding=UTF-8 -Dfile.encoding=UTF-8
+
+
+
+
+
diff --git a/copying/scripts/broken.sh b/copying/scripts/broken.sh
new file mode 100755
index 0000000..0dec055
--- /dev/null
+++ b/copying/scripts/broken.sh
@@ -0,0 +1,9 @@
+#!/usr/bin/env bash
+# Compiles the deliberately broken source and captures the compiler's own message.
+cd "$(dirname "$0")/.."
+OUT=$(mktemp -d)
+{
+ echo '$ javac src/broken/FinalFieldClone.java'
+ javac -d "$OUT" src/broken/FinalFieldClone.java 2>&1 | grep -v '^Picked up JAVA_TOOL_OPTIONS' || true
+} > output/02-final-field-clone.txt
+cat output/02-final-field-clone.txt
diff --git a/copying/scripts/run-all.sh b/copying/scripts/run-all.sh
new file mode 100755
index 0000000..be338e2
--- /dev/null
+++ b/copying/scripts/run-all.sh
@@ -0,0 +1,18 @@
+#!/usr/bin/env bash
+# Regenerates every file in output/. All outputs are deterministic EXCEPT 10-cost.txt (timing, indicative only).
+set -euo pipefail
+cd "$(dirname "$0")/.."
+mvn -q -B compile
+run() { java -Dstdout.encoding=UTF-8 -cp target/classes "com.ankurm.copying.$1" 2>&1 | grep -v '^Picked up JAVA_TOOL_OPTIONS' > "output/$2.txt"; }
+run ShallowVsDeepDemo 01-shallow-vs-deep
+scripts/broken.sh > /dev/null
+run CloneDemo 03-clone
+run CopyConstructorDemo 04-copy-constructor
+run SerializationCopyDemo 05-serialization
+run RecordCopyDemo 06-records
+run DefensiveCopyDemo 07-defensive
+run CollectionCopyDemo 08-collections
+run CopyOfIdentityDemo 11-copyof-identity
+mvn -B test 2>&1 | grep -E 'Tests run|BUILD|ERROR|FAIL' | grep -v 'Picked up' > output/09-tests.txt
+run CopyCostDemo 10-cost
+echo "regenerated:"; ls output
diff --git a/copying/scripts/run.sh b/copying/scripts/run.sh
new file mode 100755
index 0000000..a09d1fe
--- /dev/null
+++ b/copying/scripts/run.sh
@@ -0,0 +1,4 @@
+#!/usr/bin/env bash
+# Usage: scripts/run.sh CloneDemo
+cd "$(dirname "$0")/.."
+mvn -q -B compile && java -Dstdout.encoding=UTF-8 -cp target/classes "com.ankurm.copying.$1"
diff --git a/copying/src/broken/FinalFieldClone.java b/copying/src/broken/FinalFieldClone.java
new file mode 100644
index 0000000..a2048f0
--- /dev/null
+++ b/copying/src/broken/FinalFieldClone.java
@@ -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 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);
+ }
+ }
+}
diff --git a/copying/src/main/java/com/ankurm/copying/CloneDemo.java b/copying/src/main/java/com/ankurm/copying/CloneDemo.java
new file mode 100644
index 0000000..f592c40
--- /dev/null
+++ b/copying/src/main/java/com/ankurm/copying/CloneDemo.java
@@ -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 members;
+
+ Team(String name, List 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 members;
+
+ DeepTeam(String name, List 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 original = new ArrayList<>();
+ original.add(new StringBuilder("abc"));
+ @SuppressWarnings("unchecked")
+ ArrayList cloned = (ArrayList) 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());
+ }
+ }
+}
diff --git a/copying/src/main/java/com/ankurm/copying/CollectionCopyDemo.java b/copying/src/main/java/com/ankurm/copying/CollectionCopyDemo.java
new file mode 100644
index 0000000..0c31638
--- /dev/null
+++ b/copying/src/main/java/com/ankurm/copying/CollectionCopyDemo.java
@@ -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 boxes = new ArrayList<>(List.of(new Box(1), new Box(2)));
+ List viaCtor = new ArrayList<>(boxes);
+ List 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 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 m = new HashMap<>();
+ m.put("k", new Box(5));
+ Map 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]));
+ }
+}
diff --git a/copying/src/main/java/com/ankurm/copying/CopyConstructorDemo.java b/copying/src/main/java/com/ankurm/copying/CopyConstructorDemo.java
new file mode 100644
index 0000000..e674294
--- /dev/null
+++ b/copying/src/main/java/com/ankurm/copying/CopyConstructorDemo.java
@@ -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 tricks; // final is fine: constructors may assign it
+
+ Animal(String name, List 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 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);
+ }
+}
diff --git a/copying/src/main/java/com/ankurm/copying/CopyCostDemo.java b/copying/src/main/java/com/ankurm/copying/CopyCostDemo.java
new file mode 100644
index 0000000..e25c577
--- /dev/null
+++ b/copying/src/main/java/com/ankurm/copying/CopyCostDemo.java
@@ -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 tags;
+
+ Customer(String name, Address address, List 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));
+ }
+}
diff --git a/copying/src/main/java/com/ankurm/copying/CopyOfIdentityDemo.java b/copying/src/main/java/com/ankurm/copying/CopyOfIdentityDemo.java
new file mode 100644
index 0000000..75a6da1
--- /dev/null
+++ b/copying/src/main/java/com/ankurm/copying/CopyOfIdentityDemo.java
@@ -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 immutable = List.copyOf(new ArrayList<>(List.of("a")));
+ System.out.println("copyOf(immutable list) is the same instance : " + (List.copyOf(immutable) == immutable));
+
+ List 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());
+ }
+ }
+}
diff --git a/copying/src/main/java/com/ankurm/copying/DefensiveCopyDemo.java b/copying/src/main/java/com/ankurm/copying/DefensiveCopyDemo.java
new file mode 100644
index 0000000..3fc66ff
--- /dev/null
+++ b/copying/src/main/java/com/ankurm/copying/DefensiveCopyDemo.java
@@ -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 names;
+
+ LeakyRoster(List names) { this.names = names; }
+
+ List 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 names;
+
+ SafeRoster(List names) { this.names = List.copyOf(names); }
+
+ List 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 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 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 source = new ArrayList<>(List.of("a", "b"));
+ List view = Collections.unmodifiableList(source);
+ List 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 sbs = new ArrayList<>(List.of(new StringBuilder("x")));
+ List frozen = List.copyOf(sbs);
+ sbs.get(0).append("-mutated");
+ System.out.println(" frozen.get(0) = " + frozen.get(0));
+ }
+}
diff --git a/copying/src/main/java/com/ankurm/copying/RecordCopyDemo.java b/copying/src/main/java/com/ankurm/copying/RecordCopyDemo.java
new file mode 100644
index 0000000..42d1b53
--- /dev/null
+++ b/copying/src/main/java/com/ankurm/copying/RecordCopyDemo.java
@@ -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 items) { }
+
+ /** The compact constructor takes a private, unmodifiable snapshot. */
+ record SafeOrder(String id, List 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 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 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 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());
+ }
+}
diff --git a/copying/src/main/java/com/ankurm/copying/SerializationCopyDemo.java b/copying/src/main/java/com/ankurm/copying/SerializationCopyDemo.java
new file mode 100644
index 0000000..cbd0fdf
--- /dev/null
+++ b/copying/src/main/java/com/ankurm/copying/SerializationCopyDemo.java
@@ -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 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 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 team = new ArrayList<>(List.of(asha, ravi));
+ List teamCopy = deepCopy((ArrayList) 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());
+ }
+ }
+}
diff --git a/copying/src/main/java/com/ankurm/copying/ShallowVsDeepDemo.java b/copying/src/main/java/com/ankurm/copying/ShallowVsDeepDemo.java
new file mode 100644
index 0000000..2f61909
--- /dev/null
+++ b/copying/src/main/java/com/ankurm/copying/ShallowVsDeepDemo.java
@@ -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 tags;
+
+ Person(String name, Address address, List 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);
+ }
+}
diff --git a/copying/src/test/java/com/ankurm/copying/CopyingTest.java b/copying/src/test/java/com/ankurm/copying/CopyingTest.java
new file mode 100644
index 0000000..309ef75
--- /dev/null
+++ b/copying/src/test/java/com/ankurm/copying/CopyingTest.java
@@ -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 basket = new ArrayList<>(List.of("pen"));
+ var leaky = new RecordCopyDemo.LeakyOrder("1", basket);
+ basket.add("book");
+ assertEquals(2, leaky.items().size());
+
+ List 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 src = new ArrayList<>(List.of("a"));
+ List view = java.util.Collections.unmodifiableList(src);
+ List snap = List.copyOf(src);
+ src.add("b");
+ assertEquals(2, view.size());
+ assertEquals(1, snap.size());
+ }
+
+ @Test
+ void copyOfIsShallow() {
+ List src = new ArrayList<>(List.of(new StringBuilder("x")));
+ List frozen = List.copyOf(src);
+ src.get(0).append("y");
+ assertEquals("xy", frozen.get(0).toString());
+ }
+
+ @Test
+ void defensiveRosterIsIsolated() {
+ List 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 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 immutable = List.copyOf(new ArrayList<>(List.of("a")));
+ assertSame(immutable, List.copyOf(immutable));
+ List view = java.util.Collections.unmodifiableList(new ArrayList<>(List.of("a")));
+ assertNotSame(view, List.copyOf(view));
+ assertThrows(NullPointerException.class, () -> List.copyOf(Arrays.asList("a", null)));
+ }
+}
diff --git a/pom.xml b/pom.xml
index a1a7b17..7f2861c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -38,6 +38,7 @@
jmh-basics
string-format
equals-hashcode
+ copying