Skip to content

Commit 11d7db2

Browse files
author
Nicolai Parlog
committed
[50] Create demo and tests for defensive copies
1 parent 9ab9931 commit 11d7db2

File tree

5 files changed

+262
-1
lines changed

5 files changed

+262
-1
lines changed

README.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Effective Java
22

3-
This project contains examples and benchmarks for my [YouTube series on _Effective Java, Third Edition_](https://www.youtube.com/playlist?list=PL_-IO8LOLuNqUzvXfRCWRRJBswKEbLhgN).
3+
This project contains examples, tests, and benchmarks for my [YouTube series on _Effective Java, Third Edition_](https://www.youtube.com/playlist?list=PL_-IO8LOLuNqUzvXfRCWRRJBswKEbLhgN).
4+
The tests can be run with `mvn test` or any IDE with JUnit 5 integration.
45
To run the benchmarks:
56

67
```
@@ -29,3 +30,9 @@ Related links:
2930
* _Prefer dependency injection to hardwiring resources_
3031
* Item 6: _Avoid creating unnecessary objects_ -
3132
[benchmark](src/main/java/org/codefx/demo/effective_java/_06_unnecessary_objects/StringMatches.java)
33+
34+
## Methods
35+
36+
* Item 50: _Make defensive copies when needed_ -
37+
[example](src/main/java/org/codefx/demo/effective_java/_50_defensive_copies) and
38+
[tests](src/test/java/org/codefx/demo/effective_java/_50_defensive_copies/MegacorpInvarianceTests.java)

pom.xml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
<properties>
1212
<maven.compiler.source>10</maven.compiler.source>
1313
<maven.compiler.target>10</maven.compiler.target>
14+
<junit-jupiter-version>5.4.1</junit-jupiter-version>
1415
<jmh-version>1.21</jmh-version>
1516
<uberjar-name>benchmarks</uberjar-name>
1617
</properties>
@@ -23,6 +24,26 @@
2324
<version>25.0-jre</version>
2425
</dependency>
2526

27+
<!-- tests -->
28+
<dependency>
29+
<groupId>org.junit.jupiter</groupId>
30+
<artifactId>junit-jupiter-api</artifactId>
31+
<version>${junit-jupiter-version}</version>
32+
<scope>test</scope>
33+
</dependency>
34+
<dependency>
35+
<groupId>org.junit.jupiter</groupId>
36+
<artifactId>junit-jupiter-engine</artifactId>
37+
<version>${junit-jupiter-version}</version>
38+
<scope>test</scope>
39+
</dependency>
40+
<dependency>
41+
<groupId>org.assertj</groupId>
42+
<artifactId>assertj-core</artifactId>
43+
<version>3.10.0</version>
44+
<scope>test</scope>
45+
</dependency>
46+
2647
<!-- jmh -->
2748
<dependency>
2849
<groupId>org.openjdk.jmh</groupId>
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package org.codefx.demo.effective_java._50_defensive_copies;
2+
3+
import java.util.ArrayList;
4+
import java.util.List;
5+
import java.util.Objects;
6+
7+
public class Megacorp {
8+
9+
private final String name;
10+
private int totalRevenue;
11+
private final List<Subsidiary> subsidiaries;
12+
13+
public Megacorp(String name, List<Subsidiary> subsidiaries) {
14+
this.name = name;
15+
this.totalRevenue = subsidiaries.stream()
16+
.mapToInt(Subsidiary::revenue)
17+
.sum();
18+
this.subsidiaries = new ArrayList<>(subsidiaries);
19+
20+
if (this.subsidiaries.isEmpty())
21+
throw new IllegalArgumentException(name + " needs at least one subsidiary.");
22+
}
23+
24+
public String name() {
25+
return name;
26+
}
27+
28+
public int totalRevenue() {
29+
return totalRevenue;
30+
}
31+
32+
public List<Subsidiary> subsidiaries() {
33+
return List.copyOf(subsidiaries);
34+
}
35+
36+
public void acquire(Subsidiary subsidiary) {
37+
subsidiaries.add(subsidiary);
38+
totalRevenue += subsidiary.revenue();
39+
}
40+
41+
@Override
42+
public boolean equals(Object o) {
43+
if (this == o)
44+
return true;
45+
if (o == null || getClass() != o.getClass())
46+
return false;
47+
Megacorp megacorp = (Megacorp) o;
48+
return Objects.equals(name, megacorp.name);
49+
}
50+
51+
@Override
52+
public int hashCode() {
53+
return Objects.hash(name);
54+
}
55+
56+
@Override
57+
public String toString() {
58+
return name + "(" + totalRevenue + " M€)";
59+
}
60+
61+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package org.codefx.demo.effective_java._50_defensive_copies;
2+
3+
import java.util.Objects;
4+
5+
public class Subsidiary {
6+
7+
private final String name;
8+
private final int revenue;
9+
10+
public Subsidiary(String name, int revenue) {
11+
this.name = name;
12+
this.revenue = revenue;
13+
}
14+
15+
public String name() {
16+
return name;
17+
}
18+
19+
public int revenue() {
20+
return revenue;
21+
}
22+
23+
@Override
24+
public boolean equals(Object o) {
25+
if (this == o)
26+
return true;
27+
if (o == null || getClass() != o.getClass())
28+
return false;
29+
Subsidiary that = (Subsidiary) o;
30+
return Objects.equals(name, that.name);
31+
}
32+
33+
@Override
34+
public int hashCode() {
35+
return Objects.hash(name);
36+
}
37+
38+
@Override
39+
public String toString() {
40+
return name + "(" + revenue + " M€)";
41+
}
42+
43+
}
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
package org.codefx.demo.effective_java._50_defensive_copies;
2+
3+
import org.junit.jupiter.api.Nested;
4+
import org.junit.jupiter.api.Test;
5+
6+
import java.util.ArrayList;
7+
import java.util.ConcurrentModificationException;
8+
import java.util.List;
9+
import java.util.concurrent.CountDownLatch;
10+
import java.util.concurrent.ExecutorService;
11+
import java.util.concurrent.Executors;
12+
import java.util.concurrent.atomic.LongAdder;
13+
14+
import static org.assertj.core.api.Assertions.assertThat;
15+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
16+
17+
class MegacorpInvarianceTests {
18+
19+
private final List<Subsidiary> mutableSubsidiaries;
20+
private final Subsidiary orbitalDynamix = new Subsidiary("Orbital Dynamix", 693_264);
21+
22+
MegacorpInvarianceTests() {
23+
mutableSubsidiaries = new ArrayList<>();
24+
mutableSubsidiaries.add(new Subsidiary("Arianespace", 1_023_684));
25+
mutableSubsidiaries.add(new Subsidiary("Krupp Group", 847_793));
26+
mutableSubsidiaries.add(new Subsidiary("Ruhr Nuclear", 295_203));
27+
}
28+
29+
private static void assertReportedAndActualTotalRevenueEqual(Megacorp corp) {
30+
int actualTotalRevenue = corp.subsidiaries().stream()
31+
.mapToInt(Subsidiary::revenue)
32+
.sum();
33+
assertThat(corp.totalRevenue()).isEqualTo(actualTotalRevenue);
34+
}
35+
36+
@Nested
37+
class EstablishingInvariants {
38+
39+
@Test
40+
void nonEmptySubsidiaries() {
41+
assertThatThrownBy(() -> new Megacorp("Saeder-Krupp", new ArrayList<>()))
42+
.isInstanceOf(IllegalArgumentException.class);
43+
}
44+
45+
@Test
46+
void revenueAfterConstruction() {
47+
var saederKrupp = new Megacorp("Saeder-Krupp", mutableSubsidiaries);
48+
49+
assertReportedAndActualTotalRevenueEqual(saederKrupp);
50+
}
51+
52+
@Test
53+
void revenueAfterAcquisition() {
54+
var saederKrupp = new Megacorp("Saeder-Krupp", mutableSubsidiaries);
55+
saederKrupp.acquire(orbitalDynamix);
56+
57+
assertReportedAndActualTotalRevenueEqual(saederKrupp);
58+
}
59+
60+
}
61+
62+
@Nested
63+
class AttackingInvariants {
64+
65+
@Test
66+
void makesDefensiveCopy() {
67+
var saederKrupp = new Megacorp("Saeder-Krupp", mutableSubsidiaries);
68+
mutableSubsidiaries.add(orbitalDynamix);
69+
70+
assertReportedAndActualTotalRevenueEqual(saederKrupp);
71+
}
72+
73+
@Test
74+
void copyBeforeTest() throws InterruptedException {
75+
var withoutSubsidiaries = new LongAdder();
76+
var tasks = Executors.newFixedThreadPool(2);
77+
78+
for (int i = 0; i < 100; i++) {
79+
var subsidiaries = new ArrayList<Subsidiary>(mutableSubsidiaries);
80+
var taskLatch = new CountDownLatch(2);
81+
tasks.submit(() -> createMegacorpFromSubsidiaries(withoutSubsidiaries, subsidiaries, taskLatch));
82+
tasks.submit(() -> clearSubsidiaries(subsidiaries, taskLatch));
83+
taskLatch.await();
84+
}
85+
86+
assertThat(withoutSubsidiaries.sum()).isZero();
87+
}
88+
89+
private void clearSubsidiaries(ArrayList<Subsidiary> subsidiaries, CountDownLatch latch) {
90+
subsidiaries.clear();
91+
latch.countDown();
92+
}
93+
94+
private void createMegacorpFromSubsidiaries(
95+
LongAdder withoutSubsidiaries, ArrayList<Subsidiary> subsidiaries, CountDownLatch latch) {
96+
try {
97+
var saederKrupp = new Megacorp("Saeder-Krupp", subsidiaries);
98+
if (saederKrupp.subsidiaries().isEmpty())
99+
// this should never happen!
100+
// ~> invariant broken!
101+
withoutSubsidiaries.increment();
102+
} catch (IllegalArgumentException ex) {
103+
// indicates that the construction failed because
104+
// the list of subsidiaries is empty
105+
// ~> invariant upheld
106+
} catch (RuntimeException ex) {
107+
// indicates that the stream pipeline in `Megacorp::new` failed
108+
// because of concurrent modification of the underlying collection
109+
// ~> invariant upheld
110+
}
111+
latch.countDown();
112+
}
113+
114+
@Test
115+
void returnsDefensiveCopy() {
116+
try {
117+
var saederKrupp = new Megacorp("Saeder-Krupp", mutableSubsidiaries);
118+
saederKrupp.subsidiaries().add(orbitalDynamix);
119+
120+
assertReportedAndActualTotalRevenueEqual(saederKrupp);
121+
} catch (RuntimeException ex) {
122+
// indicates that the returned collection was immutable
123+
// ~> invariant upheld
124+
}
125+
}
126+
127+
}
128+
129+
}

0 commit comments

Comments
 (0)