diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 00000000..fa71f4fd
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,27 @@
+name: CI
+
+on:
+ push:
+ branches: [ master, main ]
+ pull_request:
+ branches: [ master, main ]
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up JDK 11
+ uses: actions/setup-java@v4
+ with:
+ java-version: '11'
+ distribution: 'temurin'
+ cache: maven
+
+ - name: Compile
+ run: mvn -B clean compile
+
+ - name: Test
+ run: mvn -B test
diff --git a/.gitignore b/.gitignore
index b7d7ac33..d83e7f88 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,9 +1,11 @@
/target
/local
+dependency-reduced-pom.xml
# Eclipse, Netbeans and IntelliJ files
/.*
!.gitignore
+!.github
/nbproject
/*.ipr
/*.iws
diff --git a/MIGRATION_NOTES.md b/MIGRATION_NOTES.md
new file mode 100644
index 00000000..92c6ebe2
--- /dev/null
+++ b/MIGRATION_NOTES.md
@@ -0,0 +1,58 @@
+# Migration Notes: Java 9 → Java 11
+
+## Java Version Change
+
+- **From**: Java 9 (source/target 1.9)
+- **To**: Java 11 (using `11`)
+- The `` flag replaces the legacy ``/`` configuration and provides stronger cross-compilation guarantees.
+
+## Plugin Upgrades
+
+| Plugin | Old Version | New Version | Notes |
+|--------|-------------|-------------|-------|
+| maven-compiler-plugin | 3.1 | 3.11.0 | Switched from source/target to release flag |
+| maven-surefire-plugin | (none) | 3.2.5 | Added for test execution |
+| maven-failsafe-plugin | (none) | 3.2.5 | Added for integration test support |
+| maven-enforcer-plugin | (none) | 3.5.0 | Added to enforce Java 11+ at build time |
+| maven-javadoc-plugin | (none) | 3.6.3 | Added with `none` (Java 11 doclint is stricter) |
+| maven-shade-plugin | (unversioned) | 3.6.2 | Pinned explicit version (was unversioned, which Maven warns is unstable) |
+
+Build encoding is pinned to UTF-8 for both source (`project.build.sourceEncoding`) and reporting (`project.reporting.outputEncoding`).
+
+## Dependency Upgrades
+
+| Dependency | Old Version | New Version | Notes |
+|------------|-------------|-------------|-------|
+| JUnit | 4.11 | 4.13.2 | Latest JUnit 4.x; added `test` |
+| JMH (jmh-core) | 1.17.4 | 1.37 | Java 11 compatible |
+| JMH (jmh-generator-annprocess) | 1.17.4 | 1.37 | Java 11 compatible |
+
+## Code Fixes Required
+
+Minimal changes were needed:
+
+- **Moved `ReadPositiveIntParam.java`** from `src/main/java` to `src/test/java` — this file contained JUnit `@Test` annotations but was incorrectly placed in the main source tree. With JUnit correctly scoped to `test`, it must reside under `src/test/java`.
+- No Nashorn JavaScript engine references found
+- No internal `sun.*` or `com.sun.*` API usage
+- No Java EE modules (removed in Java 11) were used
+- Deprecation warning in `Util.java` (deprecated API usage) — informational only, no breaking change
+
+## New Test Coverage
+
+Added integration tests covering core chapters:
+
+| Test File | Chapter | Coverage |
+|-----------|---------|----------|
+| `FilteringApplesTest.java` | Chapter 1 | Lambda expressions, method references, Predicate-based filtering |
+| `StreamBasicsTest.java` | Chapter 4 | Stream creation, filtering, sorting, Java 7 vs Java 8 comparison |
+| `StreamProcessingTest.java` | Chapter 5 | Trader/Transaction queries, reduce, distinct, anyMatch |
+| `OptionalTest.java` | Chapter 10 | Optional creation, map, flatMap, orElse, filter, stream() |
+| `CompletableFutureTest.java` | Chapter 11 | supplyAsync, thenApply, thenCombine, thenCompose, exceptionally |
+| `DateTimeTest.java` | Chapter 12 | LocalDate, LocalTime, LocalDateTime, Duration, TemporalAdjusters, DateTimeFormatter |
+
+## CI Workflow Added
+
+- GitHub Actions workflow (`.github/workflows/ci.yml`)
+- Triggers on push/PR to master/main branches
+- Uses Temurin JDK 11 with Maven caching
+- Runs `mvn -B clean compile` and `mvn -B test`
diff --git a/README.md b/README.md
index 9ee848d5..61360259 100644
--- a/README.md
+++ b/README.md
@@ -29,31 +29,34 @@ The source code for all examples can be found in the directory [src/main/java/la
* Appendix D: Lambdas and JVM bytecode
We will update the repository as we update the book. Stay tuned!
-### Make sure to have JDK8 installed
-The latest binary can be found here: http://www.oracle.com/technetwork/java/javase/overview/java8-2100321.html
+### Prerequisites
-$ java -version
-
-java version "1.8.0_05"
-Java(TM) SE Runtime Environment (build 1.8.0_05-b13)
-Java HotSpot(TM) 64-Bit Server VM (build 25.5-b02, mixed mode)
+**JDK 11** or higher is required. You can use any distribution (Temurin, OpenJDK, Oracle JDK, etc.).
+```
+$ java -version
+openjdk version "11.0.x" ...
+```
-You can download a preview version here: https://jdk8.java.net/
+Download Temurin JDK 11: https://adoptium.net/temurin/releases/?version=11
### Compile/Run the examples
-Using maven:
-$ mvn compile
+Using Maven:
+```
+$ mvn clean compile
$ cd target/classes
-
$ java lambdasinaction/chap1/FilteringApples
+```
+Run tests:
-Alternatively you can compile the files manually inside the directory src/main/java
+```
+$ mvn test
+```
-You can also import the project in your favorite IDE:
- * In IntelliJ use "File->Open" menu and navigate to the folder where the project resides
- * In Eclipse use "File->Import->Existing Maven Projects" (also modify "Reduntant super interfaces" to report as Warnings instead of Errors
- * In Netbeans use "File->Open Project" menu
\ No newline at end of file
+Alternatively you can import the project in your favorite IDE:
+ * In IntelliJ use "File->Open" menu and navigate to the folder where the project resides
+ * In Eclipse use "File->Import->Existing Maven Projects"
+ * In Netbeans use "File->Open Project" menu
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index 10e5035e..3ff0badc 100644
--- a/pom.xml
+++ b/pom.xml
@@ -10,23 +10,26 @@
UTF-8
+ UTF-8
+ 11
org.openjdk.jmh
jmh-core
- 1.17.4
+ 1.37
org.openjdk.jmh
jmh-generator-annprocess
- 1.17.4
+ 1.37
junit
junit
- 4.11
+ 4.13.2
+ test
@@ -35,15 +38,54 @@
org.apache.maven.plugins
maven-compiler-plugin
- 3.1
+ 3.11.0
- 1.9
- 1.9
+ 11
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 3.2.5
+
+
+ org.apache.maven.plugins
+ maven-failsafe-plugin
+ 3.2.5
+
+
+ org.apache.maven.plugins
+ maven-enforcer-plugin
+ 3.5.0
+
+
+ enforce-java
+
+ enforce
+
+
+
+
+ [11,)
+ Java 11 or higher is required to build this project.
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+ 3.6.3
+
+ none
org.apache.maven.plugins
maven-shade-plugin
+ 3.6.2
package
@@ -63,4 +105,4 @@
-
\ No newline at end of file
+
diff --git a/src/test/java/lambdasinaction/chap1/FilteringApplesTest.java b/src/test/java/lambdasinaction/chap1/FilteringApplesTest.java
new file mode 100644
index 00000000..752fb2df
--- /dev/null
+++ b/src/test/java/lambdasinaction/chap1/FilteringApplesTest.java
@@ -0,0 +1,64 @@
+package lambdasinaction.chap1;
+
+import lambdasinaction.chap1.FilteringApples.Apple;
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.List;
+
+import static org.junit.Assert.*;
+
+public class FilteringApplesTest {
+
+ private final List inventory = Arrays.asList(
+ new Apple(80, "green"),
+ new Apple(155, "green"),
+ new Apple(120, "red"));
+
+ @Test
+ public void filterGreenApples() {
+ List result = FilteringApples.filterGreenApples(inventory);
+ assertEquals(2, result.size());
+ assertTrue(result.stream().allMatch(a -> "green".equals(a.getColor())));
+ }
+
+ @Test
+ public void filterHeavyApples() {
+ List result = FilteringApples.filterHeavyApples(inventory);
+ assertEquals(1, result.size());
+ assertTrue(result.get(0).getWeight() > 150);
+ }
+
+ @Test
+ public void filterWithMethodReference() {
+ List greens = FilteringApples.filterApples(inventory, FilteringApples::isGreenApple);
+ assertEquals(2, greens.size());
+
+ List heavies = FilteringApples.filterApples(inventory, FilteringApples::isHeavyApple);
+ assertEquals(1, heavies.size());
+ }
+
+ @Test
+ public void filterWithLambda() {
+ List greens = FilteringApples.filterApples(inventory, a -> "green".equals(a.getColor()));
+ assertEquals(2, greens.size());
+
+ List heavies = FilteringApples.filterApples(inventory, a -> a.getWeight() > 150);
+ assertEquals(1, heavies.size());
+ }
+
+ @Test
+ public void filterWithComplexPredicate() {
+ List result = FilteringApples.filterApples(inventory,
+ a -> a.getWeight() < 80 || "brown".equals(a.getColor()));
+ assertTrue(result.isEmpty());
+ }
+
+ @Test
+ public void appleToString() {
+ Apple apple = new Apple(80, "green");
+ String str = apple.toString();
+ assertTrue(str.contains("green"));
+ assertTrue(str.contains("80"));
+ }
+}
diff --git a/src/test/java/lambdasinaction/chap10/OptionalTest.java b/src/test/java/lambdasinaction/chap10/OptionalTest.java
new file mode 100644
index 00000000..790db21c
--- /dev/null
+++ b/src/test/java/lambdasinaction/chap10/OptionalTest.java
@@ -0,0 +1,84 @@
+package lambdasinaction.chap10;
+
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.Optional;
+import java.util.Set;
+
+import static org.junit.Assert.*;
+
+public class OptionalTest {
+
+ @Test
+ public void getCarInsuranceNameWithEmptyPerson() {
+ OptionalMain main = new OptionalMain();
+ String result = main.getCarInsuranceName(Optional.empty());
+ assertEquals("Unknown", result);
+ }
+
+ @Test
+ public void optionalOfNullable() {
+ Optional opt = Optional.ofNullable(null);
+ assertFalse(opt.isPresent());
+
+ Optional present = Optional.ofNullable("hello");
+ assertTrue(present.isPresent());
+ assertEquals("hello", present.get());
+ }
+
+ @Test
+ public void optionalMap() {
+ Optional name = Optional.of("Java");
+ Optional len = name.map(String::length);
+ assertEquals(Integer.valueOf(4), len.get());
+ }
+
+ @Test
+ public void optionalFlatMap() {
+ Optional> nested = Optional.of(Optional.of("nested"));
+ Optional flat = nested.flatMap(x -> x);
+ assertEquals("nested", flat.get());
+ }
+
+ @Test
+ public void optionalOrElse() {
+ Optional empty = Optional.empty();
+ assertEquals("default", empty.orElse("default"));
+
+ Optional present = Optional.of("value");
+ assertEquals("value", present.orElse("default"));
+ }
+
+ @Test
+ public void optionalFilter() {
+ Optional opt = Optional.of("hello");
+ assertTrue(opt.filter(s -> s.startsWith("h")).isPresent());
+ assertFalse(opt.filter(s -> s.startsWith("z")).isPresent());
+ }
+
+ @Test
+ public void optionalIfPresent() {
+ Optional opt = Optional.of("test");
+ StringBuilder sb = new StringBuilder();
+ opt.ifPresent(sb::append);
+ assertEquals("test", sb.toString());
+ }
+
+ @Test
+ public void optionalStream() {
+ Optional present = Optional.of("value");
+ long count = present.stream().count();
+ assertEquals(1, count);
+
+ Optional empty = Optional.empty();
+ long emptyCount = empty.stream().count();
+ assertEquals(0, emptyCount);
+ }
+
+ @Test
+ public void insuranceNameAccess() {
+ Insurance insurance = new Insurance();
+ assertNull(insurance.getName());
+ }
+}
diff --git a/src/main/java/lambdasinaction/chap10/ReadPositiveIntParam.java b/src/test/java/lambdasinaction/chap10/ReadPositiveIntParam.java
similarity index 100%
rename from src/main/java/lambdasinaction/chap10/ReadPositiveIntParam.java
rename to src/test/java/lambdasinaction/chap10/ReadPositiveIntParam.java
diff --git a/src/test/java/lambdasinaction/chap11/CompletableFutureTest.java b/src/test/java/lambdasinaction/chap11/CompletableFutureTest.java
new file mode 100644
index 00000000..3b8e72e2
--- /dev/null
+++ b/src/test/java/lambdasinaction/chap11/CompletableFutureTest.java
@@ -0,0 +1,91 @@
+package lambdasinaction.chap11;
+
+import org.junit.Test;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import static org.junit.Assert.*;
+
+public class CompletableFutureTest {
+
+ @Test
+ public void supplyAsync() throws ExecutionException, InterruptedException {
+ CompletableFuture future = CompletableFuture.supplyAsync(() -> "hello");
+ assertEquals("hello", future.get());
+ }
+
+ @Test
+ public void thenApply() throws ExecutionException, InterruptedException {
+ CompletableFuture future = CompletableFuture
+ .supplyAsync(() -> "hello")
+ .thenApply(String::length);
+ assertEquals(Integer.valueOf(5), future.get());
+ }
+
+ @Test
+ public void thenCombine() throws ExecutionException, InterruptedException {
+ CompletableFuture future = CompletableFuture
+ .supplyAsync(() -> "hello")
+ .thenCombine(
+ CompletableFuture.supplyAsync(() -> " world"),
+ String::concat);
+ assertEquals("hello world", future.get());
+ }
+
+ @Test
+ public void thenCompose() throws ExecutionException, InterruptedException {
+ CompletableFuture future = CompletableFuture
+ .supplyAsync(() -> "hello")
+ .thenCompose(s -> CompletableFuture.supplyAsync(() -> s + " world"));
+ assertEquals("hello world", future.get());
+ }
+
+ @Test
+ public void exceptionally() throws ExecutionException, InterruptedException {
+ CompletableFuture future = CompletableFuture
+ .supplyAsync(() -> { throw new RuntimeException("error"); })
+ .exceptionally(ex -> "recovered");
+ assertEquals("recovered", future.get());
+ }
+
+ @Test
+ public void shopGetPriceFormat() {
+ Shop shop = new Shop("BestShop");
+ String price = shop.getPrice("iPhone");
+ assertNotNull(price);
+ String[] parts = price.split(":");
+ assertEquals(3, parts.length);
+ assertEquals("BestShop", parts[0]);
+ Double.parseDouble(parts[1]);
+ Discount.Code.valueOf(parts[2]);
+ }
+
+ @Test
+ public void quoteParse() {
+ Quote quote = Quote.parse("MyShop:100.0:GOLD");
+ assertEquals("MyShop", quote.getShopName());
+ assertEquals(100.0, quote.getPrice(), 0.01);
+ assertEquals(Discount.Code.GOLD, quote.getDiscountCode());
+ }
+
+ @Test
+ public void completableFutureAllOf() throws ExecutionException, InterruptedException {
+ CompletableFuture f1 = CompletableFuture.supplyAsync(() -> "a");
+ CompletableFuture f2 = CompletableFuture.supplyAsync(() -> "b");
+ CompletableFuture all = CompletableFuture.allOf(f1, f2);
+ all.get();
+ assertEquals("a", f1.get());
+ assertEquals("b", f2.get());
+ }
+
+ @Test
+ public void completableFutureAnyOf() throws ExecutionException, InterruptedException, TimeoutException {
+ CompletableFuture f1 = CompletableFuture.supplyAsync(() -> "first");
+ CompletableFuture f2 = CompletableFuture.supplyAsync(() -> "second");
+ Object result = CompletableFuture.anyOf(f1, f2).get(5, TimeUnit.SECONDS);
+ assertNotNull(result);
+ }
+}
diff --git a/src/test/java/lambdasinaction/chap12/DateTimeTest.java b/src/test/java/lambdasinaction/chap12/DateTimeTest.java
new file mode 100644
index 00000000..366f1af4
--- /dev/null
+++ b/src/test/java/lambdasinaction/chap12/DateTimeTest.java
@@ -0,0 +1,119 @@
+package lambdasinaction.chap12;
+
+import org.junit.Test;
+
+import java.time.*;
+import java.time.chrono.JapaneseDate;
+import java.time.format.DateTimeFormatter;
+import java.time.temporal.ChronoField;
+import java.time.temporal.ChronoUnit;
+
+import static java.time.temporal.TemporalAdjusters.lastDayOfMonth;
+import static java.time.temporal.TemporalAdjusters.nextOrSame;
+import static org.junit.Assert.*;
+
+public class DateTimeTest {
+
+ @Test
+ public void localDateCreation() {
+ LocalDate date = LocalDate.of(2014, 3, 18);
+ assertEquals(2014, date.getYear());
+ assertEquals(Month.MARCH, date.getMonth());
+ assertEquals(18, date.getDayOfMonth());
+ assertEquals(DayOfWeek.TUESDAY, date.getDayOfWeek());
+ assertEquals(31, date.lengthOfMonth());
+ assertFalse(date.isLeapYear());
+ }
+
+ @Test
+ public void localDateChronoFieldAccess() {
+ LocalDate date = LocalDate.of(2014, 3, 18);
+ assertEquals(2014, date.get(ChronoField.YEAR));
+ assertEquals(3, date.get(ChronoField.MONTH_OF_YEAR));
+ assertEquals(18, date.get(ChronoField.DAY_OF_MONTH));
+ }
+
+ @Test
+ public void localTimeCreation() {
+ LocalTime time = LocalTime.of(13, 45, 20);
+ assertEquals(13, time.getHour());
+ assertEquals(45, time.getMinute());
+ assertEquals(20, time.getSecond());
+ }
+
+ @Test
+ public void localDateTimeCreation() {
+ LocalDate date = LocalDate.of(2014, 3, 18);
+ LocalTime time = LocalTime.of(13, 45, 20);
+ LocalDateTime dt = LocalDateTime.of(date, time);
+ assertEquals(date, dt.toLocalDate());
+ assertEquals(time, dt.toLocalTime());
+ }
+
+ @Test
+ public void duration() {
+ LocalTime t1 = LocalTime.of(13, 45, 10);
+ LocalTime t2 = LocalTime.of(13, 45, 20);
+ Duration d = Duration.between(t1, t2);
+ assertEquals(10, d.getSeconds());
+ }
+
+ @Test
+ public void durationOfChronoUnit() {
+ Duration threeMinutes = Duration.of(3, ChronoUnit.MINUTES);
+ assertEquals(180, threeMinutes.getSeconds());
+ }
+
+ @Test
+ public void temporalAdjusterNextOrSame() {
+ LocalDate date = LocalDate.of(2014, 3, 18);
+ LocalDate sunday = date.with(nextOrSame(DayOfWeek.SUNDAY));
+ assertEquals(DayOfWeek.SUNDAY, sunday.getDayOfWeek());
+ assertTrue(sunday.isAfter(date) || sunday.isEqual(date));
+ }
+
+ @Test
+ public void temporalAdjusterLastDayOfMonth() {
+ LocalDate date = LocalDate.of(2014, 3, 18);
+ LocalDate lastDay = date.with(lastDayOfMonth());
+ assertEquals(31, lastDay.getDayOfMonth());
+ }
+
+ @Test
+ public void dateTimeFormatter() {
+ LocalDate date = LocalDate.of(2014, 3, 18);
+ String isoFormatted = date.format(DateTimeFormatter.ISO_LOCAL_DATE);
+ assertEquals("2014-03-18", isoFormatted);
+
+ DateTimeFormatter custom = DateTimeFormatter.ofPattern("dd/MM/yyyy");
+ assertEquals("18/03/2014", date.format(custom));
+ }
+
+ @Test
+ public void parseDateFromString() {
+ LocalDate parsed = LocalDate.parse("2014-03-18");
+ assertEquals(LocalDate.of(2014, 3, 18), parsed);
+ }
+
+ @Test
+ public void instantCreation() {
+ Instant instant = Instant.ofEpochSecond(0);
+ assertEquals(0, instant.getEpochSecond());
+ }
+
+ @Test
+ public void japaneseDateConversion() {
+ LocalDate date = LocalDate.of(2014, 3, 18);
+ JapaneseDate japaneseDate = JapaneseDate.from(date);
+ assertNotNull(japaneseDate);
+ }
+
+ @Test
+ public void periodBetweenDates() {
+ LocalDate d1 = LocalDate.of(2014, 1, 1);
+ LocalDate d2 = LocalDate.of(2014, 3, 18);
+ Period period = Period.between(d1, d2);
+ assertEquals(2, period.getMonths());
+ assertEquals(17, period.getDays());
+ }
+}
diff --git a/src/test/java/lambdasinaction/chap4/StreamBasicsTest.java b/src/test/java/lambdasinaction/chap4/StreamBasicsTest.java
new file mode 100644
index 00000000..f6aa6e65
--- /dev/null
+++ b/src/test/java/lambdasinaction/chap4/StreamBasicsTest.java
@@ -0,0 +1,59 @@
+package lambdasinaction.chap4;
+
+import org.junit.Test;
+
+import java.util.List;
+
+import static org.junit.Assert.*;
+
+public class StreamBasicsTest {
+
+ @Test
+ public void dishMenuIsNotEmpty() {
+ assertFalse(Dish.menu.isEmpty());
+ assertEquals(9, Dish.menu.size());
+ }
+
+ @Test
+ public void lowCaloricDishesJava7() {
+ List names = StreamBasic.getLowCaloricDishesNamesInJava7(Dish.menu);
+ assertFalse(names.isEmpty());
+ assertTrue(names.stream().allMatch(name ->
+ Dish.menu.stream()
+ .filter(d -> d.getName().equals(name))
+ .findFirst()
+ .map(d -> d.getCalories() < 400)
+ .orElse(false)));
+ }
+
+ @Test
+ public void lowCaloricDishesJava8() {
+ List names = StreamBasic.getLowCaloricDishesNamesInJava8(Dish.menu);
+ assertFalse(names.isEmpty());
+ assertTrue(names.stream().allMatch(name ->
+ Dish.menu.stream()
+ .filter(d -> d.getName().equals(name))
+ .findFirst()
+ .map(d -> d.getCalories() < 400)
+ .orElse(false)));
+ }
+
+ @Test
+ public void java7AndJava8ProduceSameResults() {
+ List java7 = StreamBasic.getLowCaloricDishesNamesInJava7(Dish.menu);
+ List java8 = StreamBasic.getLowCaloricDishesNamesInJava8(Dish.menu);
+ assertEquals(java7, java8);
+ }
+
+ @Test
+ public void dishTypes() {
+ assertTrue(Dish.menu.stream().anyMatch(d -> d.getType() == Dish.Type.MEAT));
+ assertTrue(Dish.menu.stream().anyMatch(d -> d.getType() == Dish.Type.FISH));
+ assertTrue(Dish.menu.stream().anyMatch(d -> d.getType() == Dish.Type.OTHER));
+ }
+
+ @Test
+ public void vegetarianDishesExist() {
+ assertTrue(Dish.menu.stream().anyMatch(Dish::isVegetarian));
+ }
+}
diff --git a/src/test/java/lambdasinaction/chap5/StreamProcessingTest.java b/src/test/java/lambdasinaction/chap5/StreamProcessingTest.java
new file mode 100644
index 00000000..49bdf07d
--- /dev/null
+++ b/src/test/java/lambdasinaction/chap5/StreamProcessingTest.java
@@ -0,0 +1,115 @@
+package lambdasinaction.chap5;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.List;
+
+import static java.util.Comparator.comparing;
+import static java.util.stream.Collectors.toList;
+import static org.junit.Assert.*;
+
+public class StreamProcessingTest {
+
+ private List transactions;
+
+ @Before
+ public void setUp() {
+ Trader raoul = new Trader("Raoul", "Cambridge");
+ Trader mario = new Trader("Mario", "Milan");
+ Trader alan = new Trader("Alan", "Cambridge");
+ Trader brian = new Trader("Brian", "Cambridge");
+
+ transactions = Arrays.asList(
+ new Transaction(brian, 2011, 300),
+ new Transaction(raoul, 2012, 1000),
+ new Transaction(raoul, 2011, 400),
+ new Transaction(mario, 2012, 710),
+ new Transaction(mario, 2012, 700),
+ new Transaction(alan, 2012, 950));
+ }
+
+ @Test
+ public void findTransactionsIn2011SortedByValue() {
+ List result = transactions.stream()
+ .filter(t -> t.getYear() == 2011)
+ .sorted(comparing(Transaction::getValue))
+ .collect(toList());
+
+ assertEquals(2, result.size());
+ assertEquals(300, result.get(0).getValue());
+ assertEquals(400, result.get(1).getValue());
+ }
+
+ @Test
+ public void findUniqueCities() {
+ List cities = transactions.stream()
+ .map(t -> t.getTrader().getCity())
+ .distinct()
+ .collect(toList());
+
+ assertEquals(2, cities.size());
+ assertTrue(cities.contains("Cambridge"));
+ assertTrue(cities.contains("Milan"));
+ }
+
+ @Test
+ public void findCambridgeTradersSortedByName() {
+ List traders = transactions.stream()
+ .map(Transaction::getTrader)
+ .filter(t -> t.getCity().equals("Cambridge"))
+ .distinct()
+ .sorted(comparing(Trader::getName))
+ .collect(toList());
+
+ assertEquals(3, traders.size());
+ assertEquals("Alan", traders.get(0).getName());
+ assertEquals("Brian", traders.get(1).getName());
+ assertEquals("Raoul", traders.get(2).getName());
+ }
+
+ @Test
+ public void allTraderNamesSortedAlphabetically() {
+ String result = transactions.stream()
+ .map(t -> t.getTrader().getName())
+ .distinct()
+ .sorted()
+ .reduce("", (n1, n2) -> n1 + n2);
+
+ assertEquals("AlanBrianMarioRaoul", result);
+ }
+
+ @Test
+ public void anyTraderInMilan() {
+ boolean milanBased = transactions.stream()
+ .anyMatch(t -> t.getTrader().getCity().equals("Milan"));
+
+ assertTrue(milanBased);
+ }
+
+ @Test
+ public void highestTransactionValue() {
+ int highest = transactions.stream()
+ .map(Transaction::getValue)
+ .reduce(0, Integer::max);
+
+ assertEquals(1000, highest);
+ }
+
+ @Test
+ public void traderToString() {
+ Trader trader = new Trader("Test", "London");
+ assertTrue(trader.toString().contains("Test"));
+ assertTrue(trader.toString().contains("London"));
+ }
+
+ @Test
+ public void transactionToString() {
+ Trader trader = new Trader("Test", "London");
+ Transaction tx = new Transaction(trader, 2020, 500);
+ String str = tx.toString();
+ assertTrue(str.contains("2020"));
+ assertTrue(str.contains("500"));
+ }
+}