Again, some code suggestions for “Chapter 4”, similar to “Chapter 3”
(one would wish to have better markdown syntax highlighting to make the below more readable)
Person.java
package chapter4;
import java.util.List;
import java.util.Objects;
// ---
// The "Person" of "transforming/fpij/Person.java"
// on page 68 of "Functional Programming in Java".
// Basically no changes except a checking initializer.
// ---
public record Person(String firstName, String lastName, List<String> emailAddresses) {
public Person {
Objects.requireNonNull(firstName);
Objects.requireNonNull(lastName);
Objects.requireNonNull(emailAddresses);
}
public String fullName() {
return firstName + " " + lastName;
}
public static List<Person> getPeople() {
return List.of(
new Person("John", "Doe", List.of()),
new Person("Sara", "Walker", List.of("sara@example.com")),
new Person("Mike", "Baker",
List.of("mike@example.com", "baker@example.com")),
new Person("Dev", "Shah",
List.of("dev@example.com", "shah@example.com")),
new Person("Sara", "Lee",
List.of("slee@example.org", "lee@example.com")),
new Person("Nancy", "Xie",
List.of("nancy@example.com", "xie@example.com", "nx@example.com")),
new Person("Jill", "Smith", List.of("jill@example.com"))
);
}
}
TransformingData.java
package chapter4;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.DoubleSummaryStatistics;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import static java.util.Comparator.comparing;
import static java.util.stream.Collectors.*;
// ---
// Various code based on Chapter 4 of "Functional Programming in Java"
// Instead of having multiple files and multiple main() we code it into a JUnit test case.
// ---
// Used in printing: A String and a Number
record PairN(String str, Number num) {
public String toString() {
return str + ": " + num + " (" + num.getClass().getSimpleName() + ")";
}
public static String listToString(List<PairN> pairs) {
return pairs.stream()
.map(PairN::toString)
.collect(Collectors.joining("\n"));
}
}
// Used in printing: A String and a Boolean
record PairB(String str, Boolean b) {
public String toString() {
return str + ": " + b;
}
public static String listToString(List<PairB> pairs) {
return pairs.stream()
.map(PairB::toString)
.collect(Collectors.joining("\n"));
}
}
public class TransformingData {
private static <T> String toStringListOfStuff(final List<T> list) {
// can even call this statically!
return list.stream().map(T::toString).collect(Collectors.joining("\n"));
}
// Originally "transforming/fpij/AverageNumberOfEmailAddresses.java" on page 69,
// which was given using its own class.
// Separated computation from the printing.
@Test
void averageNumberOfEmailAddresses() {
final var people = Person.getPeople();
final double res = people.stream()
.collect(averagingDouble(person -> person.emailAddresses().size()));
System.out.println("Average number of email addresses: " + res);
}
// Originally "transforming/fpij/Statistics.java" on page 70,
// which was given using its own class.
// Separated computation from the printing.
@Test
void statistics() {
final var people = Person.getPeople();
// https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/DoubleSummaryStatistics.html
final DoubleSummaryStatistics statistics =
people.stream()
.collect(
summarizingDouble(person -> person.emailAddresses().size()));
final var pairs = new ArrayList<PairN>();
pairs.add(new PairN("Number of people", statistics.getCount()));
pairs.add(new PairN("Number of email addresses", statistics.getSum()));
pairs.add(new PairN("Average number of email addresses", statistics.getAverage()));
pairs.add(new PairN("Max number of email addresses", statistics.getMax()));
pairs.add(new PairN("Min number of email addresses", statistics.getMin()));
System.out.println(PairN.listToString(pairs));
}
// Code on page 73
@Test
void flattenEmailAddresses() {
final var people = Person.getPeople();
final List<String> emails = people.stream()
.flatMap(person -> person.emailAddresses().stream())
.toList();
// System.out.println(emails);
System.out.println(toStringListOfStuff(emails));
}
// Code on page 74 but the criterium size() > 0 has been replaced by .isEmpty().
// Separated computation from the printing.
@Test
void checkingForCriteriaAny() {
final var people = Person.getPeople();
final var pairs = new ArrayList<PairB>();
pairs.add(new PairB(
"Anyone has email address",
people.stream().anyMatch(person -> !person.emailAddresses().isEmpty())));
pairs.add(new PairB(
"Anyone has more than 10 email address",
people.stream().anyMatch(person -> person.emailAddresses().size() > 10)));
System.out.println(PairB.listToString(pairs));
}
// Code on page 74 but the criterium size() > 0 has been replaced by .isEmpty().
// The criterium >= 0 should probably be > 1 for interestingness.
// Separated computation from the printing.
@Test
void checkingForCriteriaAll() {
final var people = Person.getPeople();
final var pairs = new ArrayList<PairB>();
// modified from the book code which uses allMatch() in all cases!
pairs.add(new PairB(
"Everyone has at least one email address (1)",
people.stream().noneMatch(person -> person.emailAddresses().isEmpty())));
pairs.add(new PairB(
"Everyone has at least one email address (2)",
people.stream().allMatch(person -> person.emailAddresses().size() > 1)));
// IDE warns: Condition 'person.emailAddresses().size() >= 0' is always 'true'
pairs.add(new PairB("Everyone has zero or more email address (always true)",
people.stream().allMatch(person -> person.emailAddresses().size() >= 0)));
System.out.println(PairB.listToString(pairs));
}
// Code on page 75
// Separated computation from the printing.
@Test
void partitioningByBoolean() {
final var people = Person.getPeople();
final Map<Boolean, List<Person>> partitions =
people.stream()
.collect(partitioningBy(person -> person.emailAddresses().size() > 1));
final var pairs = new ArrayList<PairN>();
pairs.add(new PairN("Number of people with at most one email address", partitions.get(false).size()));
pairs.add(new PairN("Number of people with multiple email addresses", partitions.get(true).size()));
System.out.println(PairN.listToString(pairs));
}
// Code on page 76 bottom, 77 top.
// Separated computation from the printing.
@Test
void countingOccurrencesWithLongResult() {
final var people = Person.getPeople();
final Map<String, Long> namesCount = people.stream()
.collect(
groupingBy(
Person::firstName,
counting()));
final List<PairN> pairs = mapToPairs(namesCount);
System.out.println(PairN.listToString(pairs));
}
// Code on page 77 middle.
// Separated computation from the printing.
@Test
void countingOccurrencesWithIntResult() {
final var people = Person.getPeople();
final Map<String, Integer> namesCount = people.stream()
.collect(
groupingBy(
Person::firstName,
collectingAndThen(counting(), Long::intValue)));
final List<PairN> pairs = mapToPairs(namesCount);
System.out.println(PairN.listToString(pairs));
}
// Helper for above
private static List<PairN> mapToPairs(final Map<String, ? extends Number> map) {
return map.entrySet().stream().map(entry -> new PairN(entry.getKey(), entry.getValue())).collect(toList());
}
// Code on page 78 top
@Test
void summingCountingNumberOfEmailAddressesByLastName() {
final var people = Person.getPeople();
final Map<String, Integer> namesAndEmailAddressesCount =
people.stream()
.collect(
groupingBy(
Person::lastName,
summingInt(person -> person.emailAddresses().size())));
final List<PairN> pairs = mapToPairs(namesAndEmailAddressesCount);
System.out.println(PairN.listToString(pairs));
}
// Code on page 79 top.
// The ".com" criterium has been moved to a name Predicate<> for legibility.
// The result's type Map<String, List<String>> is given for clarity.
// Separated computation from the printing, with dedicated formatting.
@Test
void filteringEmailAddressesEndingInDotComByLastName() {
final var people = Person.getPeople();
final Predicate<String> endsWithDotCom = (String s) -> s.endsWith(".com");
final Map<String, List<String>> lastNamesAndEmailAddressesFiltered =
people.stream()
.collect(
groupingBy(
Person::lastName,
flatMapping(
(person -> person.emailAddresses().stream()),
filtering(
endsWithDotCom,
toList()))));
var txt = lastNamesAndEmailAddressesFiltered.entrySet().stream()
.map(entry -> entry.getKey() + "=" + entry.getValue())
.collect(Collectors.joining("\n", "{\n", "\n}"));
System.out.println(txt);
// System.out.println(lastNamesAndEmailAddressesFiltered);
}
// Code on page 80 bottom.
// The common function in maxBy() and minBy() has been moved
// for legibility and suppression of repetition.
// The result's type Map<String, String> is given for clarity.
@Test
void teeingOperations() {
final var people = Person.getPeople();
final Function<Person, Integer> emailCountOfPerson = (Person person) -> person.emailAddresses().size();
final Map<String, String> leastAndMostEmailAddressPerson =
people.stream()
.collect(
teeing(
minBy(comparing(emailCountOfPerson)),
maxBy(comparing(emailCountOfPerson)),
(min, max) ->
Map.of("least", min.map(Person::fullName).orElse(""),
"most", max.map(Person::fullName).orElse(""))));
System.out.println(leastAndMostEmailAddressPerson);
}
}