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