copying: fold in graph copying (shared nodes, cycles, identity-map fix), serialization rules (constructors, record canonical constructor, ObjectInputFilter), record array components, clone() as a defensive copy, two more compile-error exhibits; 24 tests
Co-Authored-By: Claude Sonnet 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_017ic4b4BL3cEPHQTk2McE7C
This commit is contained in:
+9
-3
@@ -17,7 +17,7 @@ Module in `java-core-examples`, the Java-core series. There is no `docs/` folder
|
|||||||
export JAVA_HOME=/path/to/jdk-25
|
export JAVA_HOME=/path/to/jdk-25
|
||||||
mvn compile
|
mvn compile
|
||||||
scripts/run.sh CloneDemo # one demo
|
scripts/run.sh CloneDemo # one demo
|
||||||
scripts/broken.sh # the deliberate compile error
|
scripts/broken.sh # the three deliberate compile errors
|
||||||
scripts/run-all.sh # regenerate every file in output/
|
scripts/run-all.sh # regenerate every file in output/
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -28,15 +28,21 @@ scripts/run-all.sh # regenerate every file in output/
|
|||||||
| `ShallowVsDeepDemo.java` | Assignment vs shallow copy vs deep copy on one `Person` graph. |
|
| `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. |
|
| `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. |
|
| `src/broken/FinalFieldClone.java` | Does not compile: a `final` field cannot be deep-copied inside `clone()`. Not part of the Maven build. |
|
||||||
|
| `src/broken/ForgotToOverride.java` | Does not compile: `implements Cloneable` without a public `clone()` override leaves `clone()` protected. |
|
||||||
|
| `src/broken/WithExpression.java` | Does not compile on JDK 25: the `with` expression of JEP 468 (derived record creation) is not part of the language yet. |
|
||||||
| `CopyConstructorDemo.java` | Copy constructors with `final` fields, subtype slicing, and a virtual `copy()` that fixes it. |
|
| `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`. |
|
| `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. |
|
| `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). |
|
| `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. |
|
| `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. |
|
| `CopyOfIdentityDemo.java` | When `List.copyOf` returns the same instance and when it copies; null elements. |
|
||||||
|
| `GraphCopyDemo.java` | Shared nodes and cycles: a recursive copy constructor duplicates the shared node and overflows the stack on a cycle; an identity-map copy and serialization both handle them. |
|
||||||
|
| `SerializationRulesDemo.java` | Which constructors run during a serialization copy (classes vs records) and an `ObjectInputFilter` allow-list. |
|
||||||
|
| `RecordArrayDemo.java` | An array record component leaks through the constructor and the accessor; the generated `equals()` compares arrays by identity. |
|
||||||
|
| `DefensiveCloneDemo.java` | Why a defensive copy must not call `clone()` on a non-final parameter type (a `Date` subclass hands back itself). |
|
||||||
| `CopyCostDemo.java` | Indicative cost of copy constructor vs `clone()` vs serialization. Timing: results drift per run. |
|
| `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). |
|
| `CopyingTest.java`, `CopyingExtrasTest.java` | Pin every claim above as an assertion (17 + 7 = 24 tests). |
|
||||||
| `output/01-11` | Captured runs of everything above. `10-cost.txt` is the only non-deterministic file. |
|
| `output/01-17` | Captured runs of everything above. `10-cost.txt` is the only non-deterministic file. |
|
||||||
|
|
||||||
## License
|
## License
|
||||||
MIT - see [LICENSE](../LICENSE).
|
MIT - see [LICENSE](../LICENSE).
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
[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, Time elapsed: 0.150 s -- in com.ankurm.copying.CopyingTest
|
||||||
[INFO] Tests run: 17, Failures: 0, Errors: 0, Skipped: 0
|
[INFO] Tests run: 7, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.053 s -- in com.ankurm.copying.CopyingExtrasTest
|
||||||
|
[INFO] Tests run: 24, Failures: 0, Errors: 0, Skipped: 0
|
||||||
[INFO] BUILD SUCCESS
|
[INFO] BUILD SUCCESS
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
== 1. Diamond: root.left and root.right are the same node ==
|
||||||
|
original : left == right : true
|
||||||
|
copy constructor : left == right : false
|
||||||
|
identity-map copy : left == right : true
|
||||||
|
serialization copy : left == right : true
|
||||||
|
|
||||||
|
== 2. Cycle: a -> b -> a ==
|
||||||
|
original : a.left.left == a : true
|
||||||
|
copy constructor : StackOverflowError
|
||||||
|
identity-map copy : copy.left.left == copy : true
|
||||||
|
identity-map copy : copy != original : true
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
== 1. Classes: only the first non-serializable superclass constructor runs ==
|
||||||
|
after new Child() : Child runs = 1, Parent runs = 1
|
||||||
|
after the copy : Child runs = 1, Parent runs = 2
|
||||||
|
|
||||||
|
== 2. Records: the canonical constructor DOES run ==
|
||||||
|
after new Money(..) : canonical runs = 1
|
||||||
|
after the copy : canonical runs = 2
|
||||||
|
|
||||||
|
== 3. An allow-list filter on the reading side ==
|
||||||
|
list of String and Money : [text, Money[paise=1]]
|
||||||
|
list holding an AtomicInteger : InvalidClassException: filter status: REJECTED
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
== 1. RawPacket keeps the caller's array and returns its own ==
|
||||||
|
after raw[0] = 99 : [99, 2, 3]
|
||||||
|
after rp.data()[1] = 88 : [99, 88, 3]
|
||||||
|
|
||||||
|
== 2. Packet copies in the constructor and in the accessor ==
|
||||||
|
after the same two writes : [1, 2, 3]
|
||||||
|
|
||||||
|
== 3. The generated equals() still compares the arrays by identity ==
|
||||||
|
new Packet({1}).equals(new Packet({1})) : false
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
== 1. A 'defensive' copy made with clone() trusts the argument's own clone() ==
|
||||||
|
clone() result is the attacker's object : true
|
||||||
|
clone() result class : EvilDate
|
||||||
|
|
||||||
|
== 2. A copy made with the constructor does not ==
|
||||||
|
new Date(getTime()) is the attacker's object : false
|
||||||
|
new Date(getTime()) result class : Date
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
$ javac src/broken/ForgotToOverride.java
|
||||||
|
src/broken/ForgotToOverride.java:10: error: clone() has protected access in Object
|
||||||
|
Object copy = foo.clone();
|
||||||
|
^
|
||||||
|
1 error
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
$ javac src/broken/WithExpression.java
|
||||||
|
src/broken/WithExpression.java:8: error: ';' expected
|
||||||
|
Point q = p with { x = 10; };
|
||||||
|
^
|
||||||
|
src/broken/WithExpression.java:8: error: not a statement
|
||||||
|
Point q = p with { x = 10; };
|
||||||
|
^
|
||||||
|
src/broken/WithExpression.java:8: error: ';' expected
|
||||||
|
Point q = p with { x = 10; };
|
||||||
|
^
|
||||||
|
3 errors
|
||||||
@@ -1,9 +1,15 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Compiles the deliberately broken source and captures the compiler's own message.
|
# Compiles each deliberately broken source and captures the compiler's own message.
|
||||||
cd "$(dirname "$0")/.."
|
cd "$(dirname "$0")/.."
|
||||||
OUT=$(mktemp -d)
|
OUT=$(mktemp -d)
|
||||||
|
capture() { # <source file name> <output name>
|
||||||
{
|
{
|
||||||
echo '$ javac src/broken/FinalFieldClone.java'
|
echo "\$ javac src/broken/$1"
|
||||||
javac -d "$OUT" src/broken/FinalFieldClone.java 2>&1 | grep -v '^Picked up JAVA_TOOL_OPTIONS' || true
|
javac -d "$OUT" "src/broken/$1" 2>&1 | grep -v '^Picked up JAVA_TOOL_OPTIONS' || true
|
||||||
} > output/02-final-field-clone.txt
|
} > "output/$2.txt"
|
||||||
cat output/02-final-field-clone.txt
|
cat "output/$2.txt"
|
||||||
|
}
|
||||||
|
capture FinalFieldClone.java 02-final-field-clone
|
||||||
|
capture ForgotToOverride.java 16-forgot-override
|
||||||
|
capture WithExpression.java 17-with-expression
|
||||||
|
rm -rf "$OUT"
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ run RecordCopyDemo 06-records
|
|||||||
run DefensiveCopyDemo 07-defensive
|
run DefensiveCopyDemo 07-defensive
|
||||||
run CollectionCopyDemo 08-collections
|
run CollectionCopyDemo 08-collections
|
||||||
run CopyOfIdentityDemo 11-copyof-identity
|
run CopyOfIdentityDemo 11-copyof-identity
|
||||||
|
run GraphCopyDemo 12-graph-copy
|
||||||
|
run SerializationRulesDemo 13-serialization-rules
|
||||||
|
run RecordArrayDemo 14-record-arrays
|
||||||
|
run DefensiveCloneDemo 15-defensive-clone
|
||||||
mvn -B test 2>&1 | grep -E 'Tests run|BUILD|ERROR|FAIL' | grep -v 'Picked up' > output/09-tests.txt
|
mvn -B test 2>&1 | grep -E 'Tests run|BUILD|ERROR|FAIL' | grep -v 'Picked up' > output/09-tests.txt
|
||||||
run CopyCostDemo 10-cost
|
run CopyCostDemo 10-cost
|
||||||
echo "regenerated:"; ls output
|
echo "regenerated:"; ls output
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
/** Implements Cloneable but forgets to override clone(), then calls it. Does not compile. */
|
||||||
|
public class ForgotToOverride {
|
||||||
|
|
||||||
|
static class Foo implements Cloneable {
|
||||||
|
int x = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
Foo foo = new Foo();
|
||||||
|
Object copy = foo.clone();
|
||||||
|
System.out.println(copy);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
/** A "wither" expression on a record (JEP 468, derived record creation). Not part of JDK 25. */
|
||||||
|
public class WithExpression {
|
||||||
|
|
||||||
|
record Point(int x, int y) {}
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
Point p = new Point(1, 2);
|
||||||
|
Point q = p with { x = 10; };
|
||||||
|
System.out.println(q);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package com.ankurm.copying;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Why a defensive copy must not call clone() on a parameter whose type is not final.
|
||||||
|
* Run: scripts/run.sh DefensiveCloneDemo
|
||||||
|
*/
|
||||||
|
public class DefensiveCloneDemo {
|
||||||
|
|
||||||
|
/** A Date subclass whose clone() hands back the attacker's own object. */
|
||||||
|
static class EvilDate extends Date {
|
||||||
|
EvilDate(long t) { super(t); }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object clone() { return this; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
Date attacker = new EvilDate(3_000);
|
||||||
|
|
||||||
|
System.out.println("== 1. A 'defensive' copy made with clone() trusts the argument's own clone() ==");
|
||||||
|
Date viaClone = (Date) attacker.clone();
|
||||||
|
System.out.println(" clone() result is the attacker's object : " + (viaClone == attacker));
|
||||||
|
System.out.println(" clone() result class : " + viaClone.getClass().getSimpleName());
|
||||||
|
|
||||||
|
System.out.println();
|
||||||
|
System.out.println("== 2. A copy made with the constructor does not ==");
|
||||||
|
Date viaConstructor = new Date(attacker.getTime());
|
||||||
|
System.out.println(" new Date(getTime()) is the attacker's object : " + (viaConstructor == attacker));
|
||||||
|
System.out.println(" new Date(getTime()) result class : " + viaConstructor.getClass().getSimpleName());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
package com.ankurm.copying;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.IdentityHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Real object graphs have shared nodes and cycles. A recursive copy constructor treats every
|
||||||
|
* reference as a brand new object: it duplicates shared nodes and never finishes on a cycle.
|
||||||
|
* Run: scripts/run.sh GraphCopyDemo
|
||||||
|
*/
|
||||||
|
public class GraphCopyDemo {
|
||||||
|
|
||||||
|
static class Node implements Serializable {
|
||||||
|
String name;
|
||||||
|
Node left;
|
||||||
|
Node right;
|
||||||
|
|
||||||
|
Node(String name) { this.name = name; }
|
||||||
|
|
||||||
|
/** Naive "deep" copy constructor: recurse into every reference. */
|
||||||
|
Node(Node other) {
|
||||||
|
this.name = other.name;
|
||||||
|
this.left = other.left == null ? null : new Node(other.left);
|
||||||
|
this.right = other.right == null ? null : new Node(other.right);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The fix: remember what was already copied, by identity, and reuse it. */
|
||||||
|
static Node copyWithMemo(Node n, Map<Node, Node> seen) {
|
||||||
|
if (n == null) return null;
|
||||||
|
Node done = seen.get(n);
|
||||||
|
if (done != null) return done;
|
||||||
|
Node c = new Node(n.name);
|
||||||
|
seen.put(n, c); // register BEFORE recursing, so a cycle finds it and stops
|
||||||
|
c.left = copyWithMemo(n.left, seen);
|
||||||
|
c.right = copyWithMemo(n.right, seen);
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Node diamond() {
|
||||||
|
Node root = new Node("root");
|
||||||
|
Node shared = new Node("shared");
|
||||||
|
root.left = shared;
|
||||||
|
root.right = shared;
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Node cycle() {
|
||||||
|
Node a = new Node("a");
|
||||||
|
Node b = new Node("b");
|
||||||
|
a.left = b;
|
||||||
|
b.left = a; // b points back at a
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void main(String[] args) throws Exception {
|
||||||
|
System.out.println("== 1. Diamond: root.left and root.right are the same node ==");
|
||||||
|
Node d = diamond();
|
||||||
|
System.out.println(" original : left == right : " + (d.left == d.right));
|
||||||
|
Node naive = new Node(d);
|
||||||
|
System.out.println(" copy constructor : left == right : " + (naive.left == naive.right));
|
||||||
|
Node memo = Node.copyWithMemo(d, new IdentityHashMap<>());
|
||||||
|
System.out.println(" identity-map copy : left == right : " + (memo.left == memo.right));
|
||||||
|
Node ser = SerializationCopyDemo.deepCopy(d);
|
||||||
|
System.out.println(" serialization copy : left == right : " + (ser.left == ser.right));
|
||||||
|
|
||||||
|
System.out.println();
|
||||||
|
System.out.println("== 2. Cycle: a -> b -> a ==");
|
||||||
|
Node c = cycle();
|
||||||
|
System.out.println(" original : a.left.left == a : " + (c.left.left == c));
|
||||||
|
try {
|
||||||
|
new Node(c);
|
||||||
|
System.out.println(" copy constructor : finished");
|
||||||
|
} catch (StackOverflowError e) {
|
||||||
|
System.out.println(" copy constructor : StackOverflowError");
|
||||||
|
}
|
||||||
|
Node memoCycle = Node.copyWithMemo(c, new IdentityHashMap<>());
|
||||||
|
System.out.println(" identity-map copy : copy.left.left == copy : " + (memoCycle.left.left == memoCycle));
|
||||||
|
System.out.println(" identity-map copy : copy != original : " + (memoCycle != c));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package com.ankurm.copying;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An array component of a record leaks twice: through the constructor and through the accessor.
|
||||||
|
* Run: scripts/run.sh RecordArrayDemo
|
||||||
|
*/
|
||||||
|
public class RecordArrayDemo {
|
||||||
|
|
||||||
|
/** Stores the caller's array and hands it back. */
|
||||||
|
record RawPacket(byte[] data) {}
|
||||||
|
|
||||||
|
/** Copies on the way in and on the way out. */
|
||||||
|
record Packet(byte[] data) {
|
||||||
|
Packet { data = data.clone(); }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] data() { return data.clone(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
System.out.println("== 1. RawPacket keeps the caller's array and returns its own ==");
|
||||||
|
byte[] raw = {1, 2, 3};
|
||||||
|
RawPacket rp = new RawPacket(raw);
|
||||||
|
raw[0] = 99;
|
||||||
|
System.out.println(" after raw[0] = 99 : " + Arrays.toString(rp.data()));
|
||||||
|
rp.data()[1] = 88;
|
||||||
|
System.out.println(" after rp.data()[1] = 88 : " + Arrays.toString(rp.data()));
|
||||||
|
|
||||||
|
System.out.println();
|
||||||
|
System.out.println("== 2. Packet copies in the constructor and in the accessor ==");
|
||||||
|
byte[] raw2 = {1, 2, 3};
|
||||||
|
Packet pk = new Packet(raw2);
|
||||||
|
raw2[0] = 99;
|
||||||
|
pk.data()[1] = 88;
|
||||||
|
System.out.println(" after the same two writes : " + Arrays.toString(pk.data()));
|
||||||
|
|
||||||
|
System.out.println();
|
||||||
|
System.out.println("== 3. The generated equals() still compares the arrays by identity ==");
|
||||||
|
System.out.println(" new Packet({1}).equals(new Packet({1})) : "
|
||||||
|
+ new Packet(new byte[]{1}).equals(new Packet(new byte[]{1})));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
package com.ankurm.copying;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InvalidClassException;
|
||||||
|
import java.io.ObjectInputFilter;
|
||||||
|
import java.io.ObjectInputStream;
|
||||||
|
import java.io.ObjectOutputStream;
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Two rules of the serialization copy that surprise people: which constructors run, and what an
|
||||||
|
* ObjectInputFilter can refuse. Run: scripts/run.sh SerializationRulesDemo
|
||||||
|
*/
|
||||||
|
public class SerializationRulesDemo {
|
||||||
|
|
||||||
|
/** NOT serializable, with a no-arg constructor: the one deserialization WILL run. */
|
||||||
|
static class Parent {
|
||||||
|
static int runs = 0;
|
||||||
|
Parent() { runs++; }
|
||||||
|
}
|
||||||
|
|
||||||
|
static class Child extends Parent implements Serializable {
|
||||||
|
static int runs = 0;
|
||||||
|
Child() { runs++; }
|
||||||
|
}
|
||||||
|
|
||||||
|
record Money(long paise) implements Serializable {
|
||||||
|
static int runs = 0;
|
||||||
|
Money { runs++; } // compact canonical constructor
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
static <T extends Serializable> T deepCopy(T obj, ObjectInputFilter filter) 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()))) {
|
||||||
|
in.setObjectInputFilter(filter);
|
||||||
|
return (T) in.readObject();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void main(String[] args) throws Exception {
|
||||||
|
System.out.println("== 1. Classes: only the first non-serializable superclass constructor runs ==");
|
||||||
|
Child original = new Child();
|
||||||
|
System.out.println(" after new Child() : Child runs = " + Child.runs + ", Parent runs = " + Parent.runs);
|
||||||
|
SerializationCopyDemo.deepCopy(original);
|
||||||
|
System.out.println(" after the copy : Child runs = " + Child.runs + ", Parent runs = " + Parent.runs);
|
||||||
|
|
||||||
|
System.out.println();
|
||||||
|
System.out.println("== 2. Records: the canonical constructor DOES run ==");
|
||||||
|
Money m = new Money(5000);
|
||||||
|
System.out.println(" after new Money(..) : canonical runs = " + Money.runs);
|
||||||
|
SerializationCopyDemo.deepCopy(m);
|
||||||
|
System.out.println(" after the copy : canonical runs = " + Money.runs);
|
||||||
|
|
||||||
|
System.out.println();
|
||||||
|
System.out.println("== 3. An allow-list filter on the reading side ==");
|
||||||
|
ObjectInputFilter filter = ObjectInputFilter.Config.createFilter("com.ankurm.copying.*;java.util.*;java.lang.*;!*");
|
||||||
|
List<Object> fine = new ArrayList<>(List.of("text", new Money(1)));
|
||||||
|
System.out.println(" list of String and Money : " + deepCopy((ArrayList<Object>) fine, filter));
|
||||||
|
List<Object> sneaky = new ArrayList<>(List.of("text", new AtomicInteger(1)));
|
||||||
|
try {
|
||||||
|
deepCopy((ArrayList<Object>) sneaky, filter);
|
||||||
|
System.out.println(" list holding an AtomicInteger : copied");
|
||||||
|
} catch (InvalidClassException e) {
|
||||||
|
System.out.println(" list holding an AtomicInteger : " + e.getClass().getSimpleName() + ": " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
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.assertNotEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotSame;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
import com.ankurm.copying.GraphCopyDemo.Node;
|
||||||
|
import java.io.InvalidClassException;
|
||||||
|
import java.io.ObjectInputFilter;
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.IdentityHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
/** Pins the claims in the "Going further" sections added after the first version of the article. */
|
||||||
|
class CopyingExtrasTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void copyConstructorDuplicatesSharedNodesButIdentityMapAndSerializationKeepThem() throws Exception {
|
||||||
|
Node d = GraphCopyDemo.diamond();
|
||||||
|
assertSame(d.left, d.right);
|
||||||
|
Node naive = new Node(d);
|
||||||
|
assertNotSame(naive.left, naive.right);
|
||||||
|
Node memo = Node.copyWithMemo(d, new IdentityHashMap<>());
|
||||||
|
assertSame(memo.left, memo.right);
|
||||||
|
assertNotSame(d.left, memo.left);
|
||||||
|
Node ser = SerializationCopyDemo.deepCopy(d);
|
||||||
|
assertSame(ser.left, ser.right);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void copyConstructorOverflowsTheStackOnACycleButIdentityMapDoesNot() {
|
||||||
|
Node c = GraphCopyDemo.cycle();
|
||||||
|
assertThrows(StackOverflowError.class, () -> new Node(c));
|
||||||
|
Node memo = Node.copyWithMemo(c, new IdentityHashMap<>());
|
||||||
|
assertSame(memo, memo.left.left);
|
||||||
|
assertNotSame(c, memo);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void deserializationRunsOnlyTheFirstNonSerializableSuperclassConstructor() throws Exception {
|
||||||
|
var original = new SerializationRulesDemo.Child();
|
||||||
|
int childRuns = SerializationRulesDemo.Child.runs;
|
||||||
|
int parentRuns = SerializationRulesDemo.Parent.runs;
|
||||||
|
SerializationCopyDemo.deepCopy(original);
|
||||||
|
assertEquals(childRuns, SerializationRulesDemo.Child.runs);
|
||||||
|
assertEquals(parentRuns + 1, SerializationRulesDemo.Parent.runs);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void deserializingARecordRunsItsCanonicalConstructor() throws Exception {
|
||||||
|
var m = new SerializationRulesDemo.Money(5000);
|
||||||
|
int before = SerializationRulesDemo.Money.runs;
|
||||||
|
var copy = SerializationCopyDemo.deepCopy(m);
|
||||||
|
assertEquals(before + 1, SerializationRulesDemo.Money.runs);
|
||||||
|
assertEquals(m, copy);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void allowListFilterRejectsClassesOutsideTheList() throws Exception {
|
||||||
|
ObjectInputFilter filter = ObjectInputFilter.Config.createFilter("com.ankurm.copying.*;java.util.*;java.lang.*;!*");
|
||||||
|
var fine = new ArrayList<Object>(List.of("text", new SerializationRulesDemo.Money(1)));
|
||||||
|
assertEquals(fine, SerializationRulesDemo.deepCopy((ArrayList<Object>) fine, filter));
|
||||||
|
var sneaky = new ArrayList<Object>(List.of("text", new AtomicInteger(1)));
|
||||||
|
InvalidClassException e = assertThrows(InvalidClassException.class,
|
||||||
|
() -> SerializationRulesDemo.deepCopy((Serializable) sneaky, filter));
|
||||||
|
assertTrue(e.getMessage().contains("REJECTED"), e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void recordArrayComponentLeaksUnlessCopiedInAndOut() {
|
||||||
|
byte[] raw = {1, 2, 3};
|
||||||
|
var rp = new RecordArrayDemo.RawPacket(raw);
|
||||||
|
raw[0] = 99;
|
||||||
|
rp.data()[1] = 88;
|
||||||
|
assertArrayEquals(new byte[]{99, 88, 3}, rp.data());
|
||||||
|
|
||||||
|
byte[] raw2 = {1, 2, 3};
|
||||||
|
var pk = new RecordArrayDemo.Packet(raw2);
|
||||||
|
raw2[0] = 99;
|
||||||
|
pk.data()[1] = 88;
|
||||||
|
assertArrayEquals(new byte[]{1, 2, 3}, pk.data());
|
||||||
|
assertNotEquals(new RecordArrayDemo.Packet(new byte[]{1}), new RecordArrayDemo.Packet(new byte[]{1}));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void cloneIsNotADefensiveCopyOfANonFinalType() {
|
||||||
|
Date attacker = new DefensiveCloneDemo.EvilDate(3_000);
|
||||||
|
assertSame(attacker, attacker.clone());
|
||||||
|
Date safe = new Date(attacker.getTime());
|
||||||
|
assertNotSame(attacker, safe);
|
||||||
|
assertEquals(Date.class, safe.getClass());
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user