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

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_017ic4b4X2xx7RrveVQU9R1N
This commit is contained in:
asmhatre
2026-11-12 20:18:06 +05:30
parent 682b7b067c
commit 38fbd513c9
29 changed files with 1201 additions and 0 deletions
+13
View File
@@ -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]
+5
View File
@@ -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
+24
View File
@@ -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')
+10
View File
@@ -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
+21
View File
@@ -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
+15
View File
@@ -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
+17
View File
@@ -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
+23
View File
@@ -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
+3
View File
@@ -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
+7
View File
@@ -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
+3
View File
@@ -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