From 38fbd513c943b2763d6f9c94b4896913792650d3 Mon Sep 17 00:00:00 2001 From: asmhatre Date: Thu, 12 Nov 2026 20:18:06 +0530 Subject: [PATCH] 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 Claude-Session: https://claude.ai/code/session_017ic4b4X2xx7RrveVQU9R1N --- README.md | 1 + copying/README.md | 42 ++++ copying/output/01-shallow-vs-deep.txt | 13 ++ copying/output/02-final-field-clone.txt | 5 + copying/output/03-clone.txt | 24 +++ copying/output/04-copy-constructor.txt | 10 + copying/output/05-serialization.txt | 21 ++ copying/output/06-records.txt | 15 ++ copying/output/07-defensive.txt | 17 ++ copying/output/08-collections.txt | 23 +++ copying/output/09-tests.txt | 3 + copying/output/10-cost.txt | 7 + copying/output/11-copyof-identity.txt | 3 + copying/pom.xml | 46 +++++ copying/scripts/broken.sh | 9 + copying/scripts/run-all.sh | 18 ++ copying/scripts/run.sh | 4 + copying/src/broken/FinalFieldClone.java | 23 +++ .../java/com/ankurm/copying/CloneDemo.java | 133 +++++++++++++ .../ankurm/copying/CollectionCopyDemo.java | 75 ++++++++ .../ankurm/copying/CopyConstructorDemo.java | 78 ++++++++ .../java/com/ankurm/copying/CopyCostDemo.java | 90 +++++++++ .../ankurm/copying/CopyOfIdentityDemo.java | 27 +++ .../com/ankurm/copying/DefensiveCopyDemo.java | 77 ++++++++ .../com/ankurm/copying/RecordCopyDemo.java | 82 ++++++++ .../ankurm/copying/SerializationCopyDemo.java | 97 ++++++++++ .../com/ankurm/copying/ShallowVsDeepDemo.java | 75 ++++++++ .../java/com/ankurm/copying/CopyingTest.java | 182 ++++++++++++++++++ pom.xml | 1 + 29 files changed, 1201 insertions(+) create mode 100644 copying/README.md create mode 100644 copying/output/01-shallow-vs-deep.txt create mode 100644 copying/output/02-final-field-clone.txt create mode 100644 copying/output/03-clone.txt create mode 100644 copying/output/04-copy-constructor.txt create mode 100644 copying/output/05-serialization.txt create mode 100644 copying/output/06-records.txt create mode 100644 copying/output/07-defensive.txt create mode 100644 copying/output/08-collections.txt create mode 100644 copying/output/09-tests.txt create mode 100644 copying/output/10-cost.txt create mode 100644 copying/output/11-copyof-identity.txt create mode 100644 copying/pom.xml create mode 100755 copying/scripts/broken.sh create mode 100755 copying/scripts/run-all.sh create mode 100755 copying/scripts/run.sh create mode 100644 copying/src/broken/FinalFieldClone.java create mode 100644 copying/src/main/java/com/ankurm/copying/CloneDemo.java create mode 100644 copying/src/main/java/com/ankurm/copying/CollectionCopyDemo.java create mode 100644 copying/src/main/java/com/ankurm/copying/CopyConstructorDemo.java create mode 100644 copying/src/main/java/com/ankurm/copying/CopyCostDemo.java create mode 100644 copying/src/main/java/com/ankurm/copying/CopyOfIdentityDemo.java create mode 100644 copying/src/main/java/com/ankurm/copying/DefensiveCopyDemo.java create mode 100644 copying/src/main/java/com/ankurm/copying/RecordCopyDemo.java create mode 100644 copying/src/main/java/com/ankurm/copying/SerializationCopyDemo.java create mode 100644 copying/src/main/java/com/ankurm/copying/ShallowVsDeepDemo.java create mode 100644 copying/src/test/java/com/ankurm/copying/CopyingTest.java 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