Home > Articles

This chapter is from the book

16.7 Terminal Stream Operations

A stream pipeline does not execute until a terminal operation is invoked on it; that is, a stream pipeline does not start to process the stream elements until a terminal operation is initiated. A terminal operation is said to be eager as it executes immediately when invoked—as opposed to an intermediate operation which is lazy. Invoking the terminal operation results in the intermediate operations of the stream pipeline to be executed. Understandably, a terminal operation is specified as the last operation in a stream pipeline, and there can only be one such operation in a stream pipeline. A terminal operation never returns a stream, which is always done by an intermediate operation. Once the terminal operation completes, the stream is consumed and cannot be reused.

Terminal operations can be broadly grouped into three groups:

  • Operations with side effects

    The Stream API provides two terminal operations, forEach() and forEachOrdered(), that are designed to allow side effects on stream elements (p. 948). These terminal operations do not return a value. They allow a Consumer action, specified as an argument, to be applied to every element, as they are consumed from the stream pipeline—for example, to print each element in the stream.

  • Searching operations

    These operations perform a search operation to determine a match or find an element as explained below.

    All search operations are short-circuit operations; that is, the operation can terminate once the result is determined, whether or not all elements in the stream have been considered.

    Search operations can be further classified into two subgroups:

    Matching operations

    The three terminal operations anyMatch(), allMatch(), and noneMatch() determine whether stream elements match a given Predicate specified as an argument to the method (p. 949). As expected, these operations return a boolean value to indicate whether the match was successful or not.

    Finding operations

    The two terminal operations findAny() and findFirst() find any element and the first element in a stream, respectively, if such an element is available (p. 952). As the stream might be empty and such an element might not exist, these operations return an Optional.

  • Reduction operations

    A reduction operation computes a result from combining the stream elements by successively applying a combining function; that is, the stream elements are reduced to a result value. Examples of reductions are computing the sum or average of numeric values in a numeric stream, and accumulating stream elements into a collection.

    We distinguish between two kinds of reductions:

    Functional reduction

    A terminal operation is a functional reduction on the elements of a stream if it reduces the elements to a single immutable value which is then returned by the operation.

    The overloaded reduce() method provided by the Stream API can be used to implement customized functional reductions (p. 955), whereas the terminal operations count(), min(), and max() implement specialized functional reductions (p. 953).

    Functional reductions on numeric streams are discussed later in this section (p. 972).

    Mutable reduction

    A terminal operation performs a mutable reduction on the elements of a stream if it uses a mutable container—for example, a list, a set, or a map—to accumulate values as it processes the stream elements. The operation returns the mutable container as the result of the operation.

    The Stream API provides two overloaded collect() methods that perform mutable reduction (p. 964). One overloaded collect() method can be used to implement customized mutable reductions by specifying the functions (supplier, accumulator, combiner) required to perform such a reduction. A second collect() method accepts a Collector that is used to perform a mutable reduction. A collector encapsulates the functions required for performing a mutable reduction. The Stream API provides built-in collectors that allow various containers to be used for performing mutable reductions (p. 978). When a terminal operation performs a mutable reduction using a specific container, it is said to collect to this container.

    The toArray() method implements a specialized mutable reduction that returns an array with the accumulated values (p. 971); that is, the method collects to an array.

Consumer Action on Stream Elements

We have already used both the forEach() and forEachOrdered() terminal operations to print elements when the pipeline is executed. These operations allow side effects on stream elements.

The forEach() method is defined for both streams and collections. In the case of collections, the method iterates over all the elements in the collection, whereas it is a terminal operation on streams.

Since these terminal operations perform an action on each element, the input stream to the operation must be finite in order for the operation to terminate.

Counterparts to the forEach() and forEachOrdered() methods for the primitive numeric types are also defined by the numeric stream interfaces.

The difference in behavior of the forEach() and forEachOrdered() terminal operations is that the forEach() method does not guarantee to respect the encounter order, whereas the forEachOrdered() method always does, if there is one.

Each operation is applied to both an ordered sequential stream and an ordered parallel stream to print CD titles with the help of the consumer printStr:

Consumer<String> printStr = str -> System.out.print(str + "|");

CD.cdList.stream().map(CD::title).forEach(printStr);               // (1a)
//Java Jive|Java Jam|Lambda Dancing|Keep on Erasing|Hot Generics|


CD.cdList.stream().parallel().map(CD::title).forEach(printStr);    // (1b)
//Lambda Dancing|Hot Generics|Keep on Erasing|Java Jam|Java Jive|

The behavior of the forEach() operation is nondeterministic, as seen at (1a) and (1b). The output from (1a) and (1b) shows that the forEach() operation respects the encounter order for an ordered sequential stream, but not necessarily for an ordered parallel stream. Respecting the encounter order for an ordered parallel stream would incur overhead that would impact performance, and is therefore ignored.

On the other hand, the forEachOrdered() operation always respects the encounter order in both cases, as seen below from the output at (2a) and (2b). However, it is important to note that, in the case of the ordered parallel stream, the terminal action on the elements can be executed in different threads, but guarantees that the action is applied to the elements in encounter order.

CD.cdList.stream().map(CD::title).forEachOrdered(printStr);              // (2a)
//Java Jive|Java Jam|Lambda Dancing|Keep on Erasing|Hot Generics|

CD.cdList.stream().parallel().map(CD::title).forEachOrdered(printStr);   // (2b)
//Java Jive|Java Jam|Lambda Dancing|Keep on Erasing|Hot Generics|

The discussion above also applies when the forEach() and forEachOrdered() terminal operations are invoked on numeric streams. The nondeterministic behavior of the forEach() terminal operation for int streams is illustrated below. The terminal operation on the sequential int stream at (3a) seems to respect the encounter order, but should not be relied upon. The terminal operation on the parallel int stream at (3b) can give different results for different runs.

IntConsumer printInt = n -> out.print(n + "|");

IntStream.of(2018, 2019, 2020, 2021, 2022).forEach(printInt);            // (3a)
//2018|2019|2020|2021|2022|

IntStream.of(2018, 2019, 2020, 2021, 2022).parallel().forEach(printInt); // (3b)
//2020|2019|2018|2021|2022|

Matching Elements

The match operations determine whether any, all, or none of the stream elements satisfy a given Predicate. These operations are not reductions, as they do not always consider all elements in the stream in order to return a result.

Analogous match operations are also provided by the numeric stream interfaces.

The queries at (1), (2), and (3) below determine whether any, all, or no CDs are jazz music CDs, respectively. At (1), the execution of the pipeline terminates as soon as any jazz music CD is found—the value true is returned. At (2), the execution of the pipeline terminates as soon as a non-jazz music CD is found—the value false is returned. At (3), the execution of the pipeline terminates as soon as a jazz music CD is found—the value false is returned.

boolean anyJazzCD = CD.cdList.stream().anyMatch(CD::isJazz);   // (1) true
boolean allJazzCds = CD.cdList.stream().allMatch(CD::isJazz);  // (2) false
boolean noJazzCds = CD.cdList.stream().noneMatch(CD::isJazz);  // (3) false

Given the following predicates:

Predicate<CD> eq2015 = cd -> cd.year().compareTo(Year.of(2015)) == 0;
Predicate<CD> gt2015 = cd -> cd.year().compareTo(Year.of(2015)) > 0;

The query at (4) determines that no CDs were released in 2015. The queries at (5) and (6) are equivalent. If all CDs were released after 2015, then none were released in or before 2015 (negation of the predicate gt2015).

boolean noneEQ2015 = CD.cdList.stream().noneMatch(eq2015);     // (4) true
boolean allGT2015 = CD.cdList.stream().allMatch(gt2015);       // (5) true
boolean noneNotGT2015 = CD.cdList.stream().noneMatch(gt2015.negate()); // (6) true

The code below uses the anyMatch() method on an int stream to determine whether any year is a leap year.

IntStream yrStream = IntStream.of(2018, 2019, 2020);
IntPredicate isLeapYear = yr -> Year.of(yr).isLeap();
boolean anyLeapYear = yrStream.anyMatch(isLeapYear);
out.println("Any leap year: " + anyLeapYear);   // true

Example 16.10 illustrates using the allMatch() operation to determine whether a square matrix—that is, a two-dimensional array with an equal number of columns as rows—is an identity matrix. In such a matrix, all elements on the main diagonal have the value 1 and all other elements have the value 0. The methods isIdentityMatrixLoops() and isIdentityMatrixStreams() at (1) and (2) implement this test in different ways.

The method isIdentityMatrixLoops() at (1) uses nested loops. The outer loop processes the rows, whereas the inner loop tests that each row has the correct values. The outer loop is a labeled loop in order to break out of the inner loop if an element in a row does not have the correct value—effectively achieving short-circuit execution.

The method isIdentityMatrixStreams() at (2) uses nested numeric streams, where the outer stream processes the rows and the inner stream processes the elements in a row. The allMatch() method at (4) in the inner stream pipeline determines that all elements in a row have the correct value. It short-circuits the execution of the inner stream if that is not the case. The allMatch() method at (3) in the outer stream pipeline also short-circuits its execution if its predicate to process a row returns the value false. The stream-based implementation for the identity matrix test expresses the logic more clearly and naturally than the loop-based version.

Example 16.10 Identity Matrix Test
import static java.lang.System.out;

import java.util.Arrays;
import java.util.stream.IntStream;

public class IdentityMatrixTest {
  public static void main(String[] args) {
    // Matrices to test:
    int[][] sqMatrix1 = { {1, 0, 0}, {0, 1, 0}, {0, 0, 1} };
    int[][] sqMatrix2 = { {1, 1}, {1, 1} };
    isIdentityMatrixLoops(sqMatrix1);
    isIdentityMatrixLoops(sqMatrix2);
    isIdentityMatrixStreams(sqMatrix1);
    isIdentityMatrixStreams(sqMatrix2);
  }

  private static void isIdentityMatrixLoops(int[][] sqMatrix) {           // (1)
    boolean isCorrectValue = false;
    outerLoop:
    for (int i = 0; i < sqMatrix.length; ++i) {
      for (int j = 0; j < sqMatrix[i].length; ++j) {
        isCorrectValue = j == i ? sqMatrix[i][i] == 1
                                : sqMatrix[i][j] == 0;
        if (!isCorrectValue) break outerLoop;
      }
    }
    out.println(Arrays.deepToString(sqMatrix)
        + (isCorrectValue ? " is ": " is not ") + "an identity matrix.");
  }

  private static void isIdentityMatrixStreams(int[][] sqMatrix) {        // (2)
    boolean isCorrectValue =
        IntStream.range(0, sqMatrix.length)
                 .allMatch(i -> IntStream.range(0, sqMatrix[i].length)   // (3)
                                         .allMatch(j -> j == i           // (4)
                                                   ? sqMatrix[i][i] == 1
                                                   : sqMatrix[i][j] == 0));
    out.println(Arrays.deepToString(sqMatrix)
        + (isCorrectValue ? " is ": " is not ") + "an identity matrix.");
  }
}

Output from the program:

[[1, 0, 0], [0, 1, 0], [0, 0, 1]] is an identity matrix.
[[1, 1], [1, 1]] is not an identity matrix.
[[1, 0, 0], [0, 1, 0], [0, 0, 1]] is an identity matrix.
[[1, 1], [1, 1]] is not an identity matrix.

Finding the First or Any Element

The findFirst() method can be used to find the first element that is available in the stream. This method respects the encounter order, if the stream has one. It always produces a stable result; that is, it will produce the same result on identical pipelines based on the same stream source. In contrast, the behavior of the findAny() method is nondeterministic. Counterparts to these methods are also defined by the numeric stream interfaces.

In the code below, the encounter order of the stream is the positional order of the elements in the list. The first element returned by the findFirst() method at (1) is the first element in the CD list.

Optional<CD> firstCD1 = CD.cdList.stream().findFirst();         // (1)
out.println(firstCD1.map(CD::title).orElse("No first CD."));    // (2) Java Jive

Since such an element might not exist—for example, the stream might be empty— the method returns an Optional<T> object. At (2), the Optional<CD> object returned by the findFirst() method is mapped to an Optional<String> object that encapsulates the title of the CD. The orElse() method on this Optional<String> object returns the CD title or the argument string if there is no such CD.

If the encounter order is not of consequence, the findAny() method can be used, as it is nondeterministic—that is, it does not guarantee the same result on the same stream source. On the other hand, it provides maximal performance on parallel streams. At (3) below, the findAny() method is free to return any element from the parallel stream. It should not come as a surprise if the element returned is not the first element in the list.

Optional<CD> anyCD2 = CD.cdList.stream().parallel().findAny();  // (3)
out.println(anyCD2.map(CD::title).orElse("No CD."));            // Lambda Dancing

The match methods only determine whether any elements satisfy a Predicate, as seen at (5) below. Typically, a find terminal operation is used to find the first element made available to the terminal operation after processing by the intermediate operations in the stream pipeline. At (6), the filter() operation will filter the jazz music CDs from the stream. However, the findAny() operation will return the first jazz music CD that is filtered and then short-circuit the execution.

boolean anyJazzCD = CD.cdList.stream().anyMatch(CD::isJazz);              // (5)
out.println("Any Jazz CD: " + anyJazzCD);   // Any Jazz CD: true

Optional<CD> optJazzCD = CD.cdList.stream().filter(CD::isJazz).findAny(); // (6)
optJazzCD.ifPresent(out::println);          // <Jaav, "Java Jam", 6, 2017, JAZZ>

The code below uses the findAny() method on an IntStream to find whether any number is divisible by 7.

IntStream numStream = IntStream.of(50, 55, 65, 70, 75, 77);
OptionalInt intOpt = numStream.filter(n -> n % 7 == 0).findAny();
intOpt.ifPresent(System.out::println);      // 70

The find operations are guaranteed to terminate when applied to a finite, albeit empty, stream. However, for an infinite stream in a pipeline, at least one element must be made available to the find operation in order for the operation to terminate. If the elements of an initial infinite stream are all discarded by the intermediate operations, the find operation will not terminate, as in the following pipeline:

Stream.generate(() -> 1).filter(n -> n == 0).findAny();       // Never terminates.

Counting Elements

The count() operation performs a functional reduction on the elements of a stream, as each element contributes to the count which is the single immutable value returned by the operation. The count() operation reports the number of elements that are made available to it, which is not necessarily the same as the number of elements in the initial stream, as elements might be discarded by the intermediate operations.

The code below finds the total number of CDs in the streams, and how many of these CDs are jazz music CDs.

long numOfCDS = CD.cdList.stream().count();                        // 5
long numOfJazzCDs = CD.cdList.stream().filter(CD::isJazz).count(); // 3

The count() method is also defined for the numeric streams. Below it is used on an IntStream to find how many numbers between 1 and 100 are divisible by 7.

IntStream numStream = IntStream.rangeClosed(1, 100);
long divBy7 = numStream.filter(n -> n % 7 == 0).count();           // 14

Finding Min and Max Elements

The min() and max() operations are functional reductions, as they consider all elements of the stream and return a single value. They should only be applied to a finite stream, as they will not terminate on an infinite stream. These methods are also defined by the numeric stream interfaces for the numeric types, but without the specification of a comparator.

Both methods return an Optional, as the minimum and maximum elements might not exist—for example, if the stream is empty. The code below finds the minimum and maximum elements in a stream of CDs, according to their natural order. The artist name is the most significant field according to the natural order defined for CDs (p. 883).

Optional<CD> minCD = CD.cdList.stream().min(Comparator.naturalOrder());
minCD.ifPresent(out::println);      // <Funkies, "Lambda Dancing", 10, 2018, POP>
out.println(minCD.map(CD::artist).orElse("No min CD."));    // Funkies

Optional<CD> maxCD = CD.cdList.stream().max(Comparator.naturalOrder());
maxCD.ifPresent(out::println);      // <Jaav, "Java Jive", 8, 2017, POP>
out.println(maxCD.map(CD::artist).orElse("No max CD."));    // Jaav

In the code below, the max() method is applied to an IntStream to find the largest number between 1 and 100 that is divisible by 7.

IntStream iStream = IntStream.rangeClosed(1, 100);
OptionalInt maxNum = iStream.filter(n -> n % 7 == 0).max(); // 98

If one is only interested in the minimum and maximum elements in a collection, the overloaded methods min() and max() of the java.util.Collections class can be more convenient to use.

Implementing Functional Reduction: The reduce() Method

A functional reduction combines all elements in a stream to produce a single immutable value as its result. The reduction process employs an accumulator that repeatedly computes a new partial result based on the current partial result and the current element in the stream. The stream thus gets shorter by one element. When all elements have been combined, the last partial result that was computed by the accumulator is returned as the final result of the reduction process.

The following terminal operations are special cases of functional reduction:

  • count(), p. 953.

  • min(), p. 954.

  • max(), p. 954.

  • average(), p. 1000.

  • sum(), p. 1001.

The overloaded reduce() method can be used to implement new forms of functional reduction.

The idiom of using a loop for calculating the sum of a finite number of values is something that is ingrained into all aspiring programmers. A loop-based solution to calculate the total number of tracks on CDs in a list is shown below, where the variable sum will hold the result after the execution of the for(:) loop:

int sum = 0;                               // (1) Initialize the partial result.
for (CD cd : CD.cdList) {                  // (2) Iterate over the list.
  int numOfTracks = cd.noOfTracks();       // (3) Get the current value.
  sum = sum + numOfTracks;                 // (4) Calculate new partial result.
}

Apart from the for(:) loop at (2) to iterate over all elements of the list and read the number of tracks in each CD at (3), the two necessary steps are:

  • Initialization of the variable sum at (1)

  • The accumulative operation at (4) that is applied repeatedly to compute a new partial result in the variable sum, based on its previous value and the number of tracks in the current CD

The loop-based solution above can be translated to a stream-based solution, as shown in Figure 16.11. All the code snippets can be found in Example 16.11.

Figure 16.11

Figure 16.11 Reducing with an Initial Value

In Figure 16.11, the stream created at (6) internalizes the iteration over the elements. The mapToInt() intermediate operation maps each CD to its number of tracks at (7)—the Stream<CD> is mapped to an IntStream. The reduce() terminal operation with two arguments computes and returns the total number of tracks:

  • Its first argument at (8) is the identity element that provides the initial value for the operation and is also the default value to return if the stream is empty. In this case, this value is 0.

  • Its second argument at (9) is the accumulator that is implemented as a lambda expression. It repeatedly computes a new partial sum based on the previous partial sum and the number of tracks in the current CD, as evident from

Figure 16.11. In this case, the accumulator is an IntBinaryOperator whose functional type is (int, int) -> int. Note that the parameters of the lambda expression represent the partial sum and the current number of tracks, respectively.

The stream pipeline in Figure 16.11 is an example of a map-reduce transformation on a sequential stream, as it maps the stream elements first and then reduces them. Typically, a filter operation is also performed before the map-reduce transformation.

Each of the following calls can replace the reduce() method call in Figure 16.11, as they are all equivalent:

reduce(0, (sum, noOfTracks) -> Integer.sum(sum, noOfTracks))
reduce(0, Integer::sum)               // Method reference
sum()                                 // Special functional reduction, p. 1001.

In Example 16.11, the stream pipeline at (10) prints the actions taken by the accumulator which is now augmented with print statements. The output at (3) shows that the accumulator actions correspond to those in Figure 16.11.

The single-argument reduce() method only accepts an accumulator. As no explicit default or initial value can be specified, this method returns an Optional. If the stream is not empty, it uses the first element as the initial value; otherwise, it returns an empty Optional. In Example 16.11, the stream pipeline at (13) uses the single-argument reduce() method to compute the total number of tracks on CDs. The return value is an OptionalInt that can be queried to extract the encapsulated int value.

OptionalInt optSumTracks0 = CD.cdList                       // (13)
    .stream()
    .mapToInt(CD::noOfTracks)
    .reduce(Integer::sum);                                  // (14)
out.println("Total number of tracks: " + optSumTracks0.orElse(0));  // 42

We can again augment the accumulator with print statements as shown at (16) in Example 16.11. The output at (5) shows that the number of tracks from the first CD was used as the initial value before the accumulator is applied repeatedly to the rest of the values.

Example 16.11 Implementing Functional Reductions
import static java.lang.System.out;

import java.util.Comparator;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.function.BinaryOperator;

public final class FunctionalReductions {
  public static void main(String[] args) {

// Two-argument reduce() method:
  {
    out.println("(1) Find total number of tracks (loop-based version):");
    int sum = 0;                           // (1) Initialize the partial result.
    for (CD cd : CD.cdList) {              // (2) Iterate over the list.
      int numOfTracks = cd.noOfTracks();   // (3) Get the next value.
      sum = sum + numOfTracks;             // (4) Calculate new partial result.
    }
    out.println("Total number of tracks: " + sum);
  }

    out.println("(2) Find total number of tracks (stream-based version):");
    int totNumOfTracks = CD.cdList                         // (5)
        .stream()                                          // (6)
        .mapToInt(CD::noOfTracks)                          // (7)
        .reduce(0,                                         // (8)
                (sum, numOfTracks) -> sum + numOfTracks);  // (9)
    //  .reduce(0, (sum, noOfTracks) -> Integer.sum(sum, noOfTracks));
    //  .reduce(0, Integer::sum);
    //  .sum();
    out.println("Total number of tracks: " + totNumOfTracks);
    out.println();

    out.println("(3) Find total number of tracks (accumulator logging): ");
    int totNumOfTracks1 = CD.cdList                        // (10)
        .stream()
        .mapToInt(CD::noOfTracks)
        .reduce(0,                                         // (11)
            (sum, noOfTracks) -> {                         // (12)
                int newSum = sum + noOfTracks;
                out.printf("Accumulator: sum=%2d, noOfTracks=%2d, newSum=%2d%n",
                            sum, noOfTracks, newSum);
                return newSum;
            }
         );
    out.println("Total number of tracks: " + totNumOfTracks1);
    out.println();

// One-argument reduce() method:

    out.println("(4) Find total number of tracks (stream-based version):");
    OptionalInt optSumTracks0 = CD.cdList                  // (13)
        .stream()
        .mapToInt(CD::noOfTracks)
        .reduce(Integer::sum);                             // (14)
    out.println("Total number of tracks: " + optSumTracks0.orElse(0));
    out.println();

    out.println("(5) Find total number of tracks (accumulator logging): ");
    OptionalInt optSumTracks1 = CD.cdList                  // (15)
        .stream()
        .mapToInt(CD::noOfTracks)
        .reduce((sum, noOfTracks) -> {                     // (16)
           int newSum = sum + noOfTracks;
           out.printf("Accumulator: sum=%2d, noOfTracks=%2d, newSum=%2d%n",
                       sum, noOfTracks, newSum);
           return newSum;
         });
    out.println("Total number of tracks: " + optSumTracks1.orElse(0));
    out.println();

// Three-argument reduce() method:

    out.println("(6) Find total number of tracks (accumulator + combiner): ");
    Integer sumTracks5 = CD.cdList                         // (17)
    //  .stream()                                          // (18a)
        .parallelStream()                                  // (18b)
        .reduce(Integer.valueOf(0),                        // (19) Initial value
                (sum, cd) -> sum + cd.noOfTracks(),        // (20) Accumulator
                (sum1, sum2) -> sum1 + sum2);              // (21) Combiner
    out.println("Total number of tracks: " + sumTracks5);
    out.println();

    out.println("(7) Find total number of tracks (accumulator + combiner): ");
    Integer sumTracks6 = CD.cdList                         // (22)
//      .stream()                                          // (23a)
        .parallelStream()                                  // (23b)
        .reduce(0,
               (sum, cd) -> {                              // (24) Accumulator
                 Integer noOfTracks = cd.noOfTracks();
                 Integer newSum = sum + noOfTracks;
                 out.printf("Accumulator: sum=%2d, noOfTracks=%2d, "
                            + "newSum=%2d%n", sum, noOfTracks, newSum);
                 return newSum;
               },
               (sum1, sum2) -> {                           // (25) Combiner
                 Integer newSum = sum1 + sum2;
                 out.printf("Combiner: sum1=%2d, sum2=%2d, newSum=%2d%n",
                            sum1, sum2, newSum);
                 return newSum;
               }
         );
    out.println("Total number of tracks: " + sumTracks6);
    out.println();

    // Compare by CD title.
    Comparator<CD> cmpByTitle = Comparator.comparing(CD::title);    // (26)
    BinaryOperator<CD> maxByTitle =
        (cd1, cd2) -> cmpByTitle.compare(cd1, cd2) > 0 ? cd1 : cd2; // (27)

    // Query: Find maximum Jazz CD by title:
    Optional<CD> optMaxJazzCD = CD.cdList                  // (28)
        .stream()
        .filter(CD::isJazz)
        .reduce(BinaryOperator.maxBy(cmpByTitle));         // (29a)
    //  .reduce(maxByTitle);                               // (29b)
    //  .max(cmpByTitle);                                  // (29c)
    optMaxJazzCD.map(CD::title).ifPresent(out::println);// Keep on Erasing
  }
}

Possible output from the program:

(1) Find total number of tracks (loop-based version):
Total number of tracks: 42
(2) Find total number of tracks (stream-based version):
Total number of tracks: 42

(3) Find total number of tracks (accumulator logging):
Accumulator: sum= 0, noOfTracks= 8, newSum= 8
Accumulator: sum= 8, noOfTracks= 6, newSum=14
Accumulator: sum=14, noOfTracks=10, newSum=24
Accumulator: sum=24, noOfTracks= 8, newSum=32
Accumulator: sum=32, noOfTracks=10, newSum=42
Total number of tracks: 42

(4) Find total number of tracks (stream-based version):
Total number of tracks: 42

(5) Find total number of tracks (accumulator logging):
Accumulator: sum= 8, noOfTracks= 6, newSum=14
Accumulator: sum=14, noOfTracks=10, newSum=24
Accumulator: sum=24, noOfTracks= 8, newSum=32
Accumulator: sum=32, noOfTracks=10, newSum=42
Total number of tracks: 42

(6) Find total number of tracks (accumulator + combiner):
Total number of tracks: 42

(7) Find total number of tracks (accumulator + combiner):
Accumulator: sum= 0, noOfTracks=10, newSum=10
Accumulator: sum= 0, noOfTracks=10, newSum=10
Accumulator: sum= 0, noOfTracks= 8, newSum= 8
Combiner: sum1= 8, sum2=10, newSum=18
Combiner: sum1=10, sum2=18, newSum=28
Accumulator: sum= 0, noOfTracks= 6, newSum= 6
Accumulator: sum= 0, noOfTracks= 8, newSum= 8
Combiner: sum1= 8, sum2= 6, newSum=14
Combiner: sum1=14, sum2=28, newSum=42
Total number of tracks: 42

Keep on Erasing

The single-argument and two-argument reduce() methods accept a binary operator as the accumulator whose arguments and result are of the same type. The three-argument reduce() method is more flexible and can only be applied to objects. The stream pipeline below computes the total number of tracks on CDs using the three-argument reduce() method.

Integer sumTracks5 = CD.cdList                           // (17)
    .stream()                                            // (18a)
//  .parallelStream()                                    // (18b)
    .reduce(Integer.valueOf(0),                          // (19) Initial value
            (sum, cd) -> sum + cd.noOfTracks(),          // (20) Accumulator
            (sum1, sum2) -> sum1 + sum2);                // (21) Combiner

The reduce() method above accepts the following arguments:

  • An identity value: Its type is U. In this case, it is an Integer that wraps the value 0. As before, it is used as the initial value. The type of the value returned by the reduce() method is also U.

  • An accumulator: It is a BiFunction<U,T,U>; that is, it is a binary function that accepts an object of type U and an object of type T and produces a result of type U. In this case, type U is Integer and type T is CD. The lambda expression implementing the accumulator first reads the number of tracks from the current CD before the addition operator is applied. Thus the accumulator will calculate the sum of Integers which are, of course, unboxed and boxed to do the calculation. As we have seen earlier, the accumulator is repeatedly applied to sum the tracks on the CDs. Only this time, the mapping of a CD to an Integer is done when the accumulator is evaluated.

  • A combiner: It is a BinaryOperator<U>; that is, it is a binary operator whose arguments and result are of the same type U. In this case, type U is Integer. Thus the combiner will calculate the sum of Integers which are unboxed and boxed to do the calculation.

    In the code above, the combiner is not executed if the reduce() method is applied to a sequential stream. However, there is no guarantee that this is always the case for a sequential stream. If we uncomment (18b) and remove (18a), the combiner will be executed on the parallel stream.

That the combiner in the three-argument reduce() method is executed for a parallel stream is illustrated by the stream pipeline at (22) in Example 16.11, that has been augmented with print statements. There is no output from the combiner when the stream is sequential. The output at (7) in Example 16.11 shows that the combiner accumulates the partial sums created by the accumulator when the stream is parallel.

Parallel Functional Reduction

Parallel execution is illustrated in Figure 16.12. Multiple instances of the stream pipeline are executed in parallel, where each pipeline instance processes a segment of the stream. In this case, only one CD is allocated to each pipeline instance. Each pipeline instance thus produces its partial sum, and the combiner is applied in parallel on the partial sums to combine them into a final result. No additional synchronization is required to run the reduce() operation in parallel.

Figure 16.12 also illustrates why the initial value must be an identity value. Say we had specified the initial value to be 3. Then the value 3 would be added multiple times to the sum during parallel execution. We also see why both the accumulator and the combiner are associative, as this allows for any two values to be combined in any order.

Figure 16.12

Figure 16.12 Parallel Functional Reduction

When the single-argument and two-argument reduce() methods are applied to a parallel stream, the accumulator also acts as the combiner. The three-argument reduce() method can usually be replaced with a map-reduce transformation, making the combiner redundant.

We conclude the discussion on implementing functional reductions by implementing the max() method that finds the maximum element in a stream according to a given comparator. A comparator that compares by the CD title is defined at (26). A binary operator that finds the maximum of two CDs when compared by title is defined at (27). It uses the comparator defined at (26). The stream pipeline at (28) finds the maximum of all jazz music CDs by title. The method calls at (29a), (29b), and (29c) are equivalent.

Comparator<CD> cmpByTitle = Comparator.comparing(CD::title);    // (26)
BinaryOperator<CD> maxByTitle =
    (cd1, cd2) -> cmpByTitle.compare(cd1, cd2) > 0 ? cd1 : cd2; // (27)

Optional<CD> optMaxJazzCD = CD.cdList                  // (28)
    .stream()
    .filter(CD::isJazz)
    .reduce(BinaryOperator.maxBy(cmpByTitle));         // (29a)
//  .reduce(maxByTitle);                               // (29b)
//  .max(cmpByTitle);                                  // (29c)
optMaxJazzCD.map(CD::title).ifPresent(out::println);   // Keep on Erasing

The accumulator at (29a), returned by the BinaryOperator.maxBy() method, will compare the previous maximum CD and the current CD by title to compute a new maximum jazz music CD. The accumulator used at (29b) is implemented at (27). It also does the same comparison as the accumulator at (29a). At (29c), the max() method also does the same thing, based on the comparator at (26). Note that the return value is an Optional<CD>, as the stream might be empty. The Optional<CD> is mapped to an Optional<String>. If it is not empty, its value—that is, the CD title— is printed.

The reduce() method does not terminate if applied to an infinite stream, as the method will never finish processing all stream elements.

Implementing Mutable Reduction: The collect() Method

The collect(Collector) method accepts a collector that encapsulates the functions required to perform a mutable reduction. We discuss predefined collectors implemented by the java.util.stream.Collectors class in a later section (p. 978). The code below uses the collector returned by the Collectors.toList() method that accumulates the result in a list (p. 980).

List<String> titles = CD.cdList.stream()
                        .map(CD::title).collect(Collectors.toList());
// [Java Jive, Java Jam, Lambda Dancing, Keep on Erasing, Hot Generics]

The collect(supplier, accumulator, combiner) generic method provides the general setup for implementing mutable reduction on stream elements using different kinds of mutable containers—for example, a list, a map, or a StringBuilder. It uses one or more mutable containers to accumulate partial results that are combined into a single mutable container that is returned as the result of the reduction operation.

We will use Figure 16.13 to illustrate mutable reduction performed on a sequential stream by the three-argument collect() method. The figure shows both the code and the execution of a stream pipeline to create a list containing the number of tracks on each CD. The stream of CDs is mapped to a stream of Integers at (3), where each Integer value is the number of tracks on a CD. The collect() method at (4) accepts three functions as arguments. They are explicitly defined as lambda expressions to show what the parameters represent and how they are used to perform mutable reduction. Implementation of these functions using method references can be found in Example 16.12.

  • Supplier: The supplier is a Supplier<R> that is used to create new instances of a mutable result container of type R. Such a container holds the results computed by the accumulator and the combiner. In Figure 16.13, the supplier at (4) returns an empty ArrayList<Integer> every time it is called.

  • Accumulator: The accumulator is a BiConsumer<R, T> that is used to accumulate an element of type T into a mutable result container of type R. In Figure 16.13, type R is ArrayList<Integer> and type T is Integer. The accumulator at (5) mutates a container of type ArrayList<Integer> by repeatedly adding a new Integer value to it, as illustrated in Figure 16.13b. It is instructive to contrast this accumulator with the accumulator for sequential functional reduction illustrated in Figure 16.11, p. 957.

  • Combiner: The combiner is a BiConsumer<R, R> that merges the contents of the second argument container with the contents of the first argument container, where both containers are of type R. As in the case of the reduce(identity, accumulator, combiner) method, the combiner is executed when the collect() method is called on a parallel stream.

Figure 16.13

Figure 16.13 Sequential Mutable Reduction

Parallel Mutable Reduction

Figure 16.14 shows the stream pipeline from Figure 16.13, where the sequential stream (2a) has been replaced by a parallel stream (2b); in other words, the collect() method is called on a parallel stream. One possible parallel execution of the pipeline is also depicted in Figure 16.14b. We see five instances of the pipeline being executed in parallel. The supplier creates five empty ArrayLists that are used as partial result containers by the accumulator, and are later merged by the combiner to a final result container. The containers created by the supplier are mutated by the accumulator and the combiner to perform mutable reduction. The partial result containers are also merged in parallel by the combiner. It is instructive to contrast this combiner with the combiner for parallel functional reduction that is illustrated in Figure 16.12, p. 963.

Figure 16.14

Figure 16.14 Parallel Mutable Reduction

In Example 16.12, the stream pipeline at (7) also creates a list containing the number of tracks on each CD, where the stream is parallel, and the lambda expressions implementing the argument functions of the collect() method are augmented with print statements so that actions of the functions can be logged. The output from this parallel mutable reduction shows that the combiner is executed multiple times to merge partial result lists. The actions of the argument functions shown in the output are the same as those illustrated in Figure 16.14b. Of course, multiple runs of the pipeline can show different sequences of operations in the output, but the final result in the same. Also note that the elements retain their relative position in the partial result lists as these are combined, preserving the encounter order of the stream.

Although a stream is executed in parallel to perform mutable reduction, the merging of the partial containers by the combiner can impact performance if this is too costly. For example, merging mutable maps can be costly compared to merging mutable lists. This issue is further explored for parallel streams in §16.9, p. 1009.

Example 16.12 Implementing Mutable Reductions
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Stream;

public final class Collecting {
  public static void main(String[] args) {

    // Query: Create a list with the number of tracks on each CD.
    System.out.println("Sequential Mutable Reduction:");
    List<Integer> tracks = CD.cdList                          // (1)
        .stream()                                             // (2a)
//      .parallelStream()                                     // (2b)
        .map(CD::noOfTracks)                                  // (3)
        .collect(() -> new ArrayList<>(),                     // (4) Supplier
                 (cont, noOfTracks) -> cont.add(noOfTracks),  // (5) Accumulator
                 (cont1, cont2) -> cont1.addAll(cont2));      // (6) Combiner
//      .collect(ArrayList::new, ArrayList::add, ArrayList::addAll); // (6a)
//      .toList();
    System.out.println("Number of tracks on each CD (sequential): " + tracks);
    System.out.println();

    System.out.println("Parallel Mutable Reduction:");
    List<Integer> tracks1 = CD.cdList                         // (7)
//      .stream()                                             // (8a)
        .parallelStream()                                     // (8b)
        .map(CD::noOfTracks)                                  // (9)
        .collect(                                             // (10)
            () -> {                                           // (11) Supplier
              System.out.println("Supplier: Creating an ArrayList");
              return new ArrayList<>();
            },
            (cont, noOfTracks) -> {                           // (12) Accumulator
              System.out.printf("Accumulator: cont:%s, noOfTracks:%s",
                                 cont, noOfTracks);
              cont.add(noOfTracks);
              System.out.printf(", mutCont:%s%n", cont);
            },
            (cont1, cont2) -> {                               // (13) Combiner
              System.out.printf("Combiner: con1:%s, cont2:%s", cont1, cont2);
              cont1.addAll(cont2);
              System.out.printf(", mutCont:%s%n", cont1);
            });
    System.out.println("Number of tracks on each CD (parallel): " + tracks1);
    System.out.println();

    // Query: Create an ordered set with CD titles, according to natural order.
    Set<String> cdTitles = CD.cdList                          // (14)
        .stream()
        .map(CD::title)
        .collect(TreeSet::new, TreeSet::add, TreeSet::addAll);// (15)
    System.out.println("CD titles: " + cdTitles);
    System.out.println();

    // Query: Go bananas.
    StringBuilder goneBananas = Stream                        // (16)
        .iterate("ba", b -> b + "na")                         // (17)
        .limit(5)
        .peek(System.out::println)
        .collect(StringBuilder::new,                          // (18)
                 StringBuilder::append,
                 StringBuilder::append);
    System.out.println("Go bananas: " + goneBananas);
  }
}

Possible output from the program:

Sequential Mutable Reduction:
Number of tracks on each CD (sequential): [8, 6, 10, 8, 10]

Parallel Mutable Reduction:
Supplier: Creating an ArrayList
Accumulator: cont:[], noOfTracks:8, mutCont:[8]
Supplier: Creating an ArrayList
Accumulator: cont:[], noOfTracks:6, mutCont:[6]
Combiner: con1:[8], cont2:[6], mutCont:[8, 6]
Supplier: Creating an ArrayList
Accumulator: cont:[], noOfTracks:10, mutCont:[10]
Supplier: Creating an ArrayList
Accumulator: cont:[], noOfTracks:8, mutCont:[8]
Combiner: con1:[10], cont2:[8], mutCont:[10, 8]
Supplier: Creating an ArrayList
Accumulator: cont:[], noOfTracks:10, mutCont:[10]
Combiner: con1:[10, 8], cont2:[10], mutCont:[10, 8, 10]
Combiner: con1:[8, 6], cont2:[10, 8, 10], mutCont:[8, 6, 10, 8, 10]
Number of tracks on each CD (parallel): [8, 6, 10, 8, 10]

CD titles: [Hot Generics, Java Jam, Java Jive, Keep on Erasing, Lambda Dancing]

ba
bana
banana
bananana
banananana
Go bananas: babanabananabanananabanananana

Example 16.12 also shows how other kinds of containers can be used for mutable reduction. The stream pipeline at (14) performs mutable reduction to create an ordered set with CD titles. The supplier is implemented by the constructor reference TreeSet::new. The constructor will create a container of type TreeSet<String> that will maintain the CD titles according to the natural order for Strings. The accumulator and the combiner are implemented by the method references TreeSet::add and TreeSet::addAll, respectively. The accumulator will add a title to a container of type TreeSet<String> and the combiner will merge the contents of two containers of type TreeSet<String>.

In Example 16.12, the mutable reduction performed by the stream pipeline at (16) uses a mutable container of type StringBuilder. The output from the peek() method shows that the strings produced by the iterate() method start with the initial string "ba" and are iteratively concatenated with the postfix "na". The limit() intermediate operation truncates the infinite stream to five elements. The collect() method appends the strings to a StringBuilder. The supplier creates an empty StringBuilder. The accumulator and the combiner append a CharSequence to a StringBuilder. In the case of the accumulator, the CharSequence is a String—that is, a stream element—in the call to the append() method. But in the case of the combiner, the CharSequence is a StringBuilder—that is, a partial result container when the stream is parallel. One might be tempted to use a string instead of a StringBuilder, but that would not be a good idea as a string is immutable.

Note that the accumulator and combiner of the collect() method do not return a value. The collect() method does not terminate if applied to an infinite stream, as the method will never finish processing all the elements in the stream.

Because mutable reduction uses the same mutable result container for accumulating new results by changing the state of the container, it is more efficient than a functional reduction where a new partial result always replaces the previous partial result.

Collecting to an Array

The overloaded method toArray() can be used to collect or accumulate into an array. It is a special case of a mutable reduction, and as the name suggests, the mutable container is an array. The numeric stream interfaces also provide a counterpart to the toArray() method that returns an array of a numeric type.

The zero-argument method toArray() returns an array of objects, Object[], as generic arrays cannot be created at runtime. The method needs to store all the elements before creating an array of the appropriate length. The query at (1) finds the titles of the CDs, and the toArray() method collects them into an array of objects, Object[].

Object[] objArray = CD.cdList.stream().map(CD::title)
                                      .toArray();                     // (1)
//[Java Jive, Java Jam, Lambda Dancing, Keep on Erasing, Hot Generics]

The toArray(IntFunction<A>) method accepts a generator function that creates an array of type A, (A[]), whose length is passed as a parameter by the method to the generator function. The array length is determined from the number of elements in the stream. The query at (2) also finds the CD titles, but the toArray() method collects them into an array of strings, String[]. The method reference defining the generator function is equivalent to the lambda expression (len -> new String[len]).

String[] cdTitles = CD.cdList.stream().map(CD::title)
                                      .toArray(String[]::new);         // (2)
//[Java Jive, Java Jam, Lambda Dancing, Keep on Erasing, Hot Generics]

Examples of numeric streams whose elements are collected into an array are shown at (3) and (4). The limit() intermediate operation at (3) converts the infinite stream into a finite one whose elements are collected into an int array.

int[] intArray1 = IntStream.iterate(1, i -> i + 1).limit(5).toArray();// (3)
// [1, 2, 3, 4, 5]
int[] intArray2 = IntStream.range(-5, 5).toArray();                   // (4)
// [-5, -4, -3, -2, -1, 0, 1, 2, 3, 4]

Not surprisingly, when applied to infinite streams the operation results in a fatal OutOfMemoryError, as the method cannot determine the length of the array and keeps storing the stream elements, eventually running out of memory.

int[] intArray3 = IntStream.iterate(1, i -> i + 1)                    // (5)
                           .toArray();                        // OutOfMemoryError!

If the sole purpose of using the toArray() operation in a pipeline is to convert the data source collection to an array, it is far better to use the overloaded Collection.toArray() methods. For one thing, the size of the array is easily determined from the size of the collection.

CD[] cdArray1 = CD.cdList.stream().toArray(CD[]::new);       // (6) Preferred.
CD[] cdArray2 = CD.cdList.toArray(new CD[CD.cdList.size()]); // (7) Not efficient.

Like any other mutable reduction operation, the toArray() method does not terminate when applied to an infinite stream, unless it is converted into a finite stream as at (3) above.

Collecting to a List

The method Stream.toList() implements a terminal operation that can be used to collect or accumulate the result of processing a stream into a list. Compared to the toArray() instance method, the toList() method is a default method in the Stream interface. The default implementation returns an unmodifiable list; that is, elements cannot be added, removed, or sorted. This unmodifiable list is created from the array into which the elements are accumulated first.

If the requirement is an unmodifiable list that allows null elements, the Stream.to-List() is the clear and concise choice. Many examples of stream pipelines encountered so far in this chapter use the toList() terminal operation.

List<String> titles = CD.cdList.stream().map(CD::title).toList();
// [Java Jive, Java Jam, Lambda Dancing, Keep on Erasing, Hot Generics]
titles.add("Java Jingles");          // UnsupportedOperationException!

Like any other mutable reduction operation, the toList() method does not terminate when applied to an infinite stream, unless the stream is converted into a finite stream.

Functional Reductions Exclusive to Numeric Streams

In addition to the counterparts of the methods in the Stream<T> interface, the following functional reductions are exclusive to the numeric stream interfaces IntStream, LongStream, and DoubleStream. These reduction operations are designed to calculate various statistics on numeric streams.

In the methods below, NumType is Int, Long, or Double, and the corresponding numtype is int, long, or double. These statistical operations do not terminate when applied to an infinite stream:

Summation

The sum() terminal operation is a special case of a functional reduction that calculates the sum of numeric values in a stream. The stream pipeline below calculates the total number of tracks on the CDs in a list. Note that the stream of CD is mapped to an int stream whose elements represent the number of tracks on a CD. The int values are cumulatively added to compute the total number of tracks.

int totNumOfTracks = CD.cdList
    .stream()                                     // Stream<CD>
    .mapToInt(CD::noOfTracks)                     // IntStream
    .sum();                                       // 42

The query below sums all even numbers between 1 and 100.

int sumEven = IntStream
    .rangeClosed(1, 100)
    .filter(i -> i % 2 == 0)
    .sum();                                       // 2550

The count() operation is equivalent to mapping each stream element to the value 1 and adding the 1s:

int numOfCDs = CD.cdList
    .stream()
    .mapToInt(cd -> 1)                            // CD => 1
    .sum();                                       // 5

For an empty stream, the sum is always zero.

double total = DoubleStream.empty().sum();        // 0.0

Averaging

Another common statistics to calculate is the average of values, defined as the sum of values divided by the number of values. A loop-based solution to calculate the average would explicitly sum the values, count the number of values, and do the calculation. In a stream-based solution, the average() terminal operation can be used to calculate this value. The stream pipeline below computes the average number of tracks on a CD. The CD stream is mapped to an int stream whose values are the number of tracks on a CD. The average() terminal operation adds the number of tracks and counts the values, returning the average as a double value encapsulated in an OptionalDouble.

OptionalDouble optAverage = CD.cdList
    .stream()
    .mapToInt(CD::noOfTracks)
    .average();
System.out.println(optAverage.orElse(0.0));        // 8.4

The reason for using an Optional is that the average is not defined if there are no values. The absence of a value in the OptionalDouble returned by the method means that the stream was empty.

Summarizing

The result of a functional reduction is a single value. This means that for calculating different results—for example, count, sum, average, min, and max—requires separate reduction operations on a stream.

The method summaryStatistics() does several common reductions on a stream in a single operation and returns the results in an object of type NumTypeSummaryStatistics, where NumType is Int, Long, or Double. An object of this class encapsulates the count, sum, average, min, and max values of a stream.

The classes IntSummaryStatistics, LongSummaryStatistics, and DoubleSummaryStatistics in the java.util package define the following constructor and methods, where NumType is Int (but it is Integer when used as a type name), Long, or Double, and the corresponding numtype is int, long, or double:

The summaryStatistics() method is used to calculate various statistics for the number of tracks on two CDs processed by the stream pipeline below. Various get methods are called on the IntSummaryStatistics object returned by the summary-Statistics() method, and the statistics are printed.

IntSummaryStatistics stats1 = List.of(CD.cd0, CD.cd1)
    .stream()
    .mapToInt(CD::noOfTracks)
    .summaryStatistics();
System.out.println("Count="   + stats1.getCount());        // Count=2
System.out.println("Sum="     + stats1.getSum());          // Sum=14
System.out.println("Min="     + stats1.getMin());          // Min=6
System.out.println("Max="     + stats1.getMax());          // Max=8
System.out.println("Average=" + stats1.getAverage());      // Average=7.0

The default format of the statistics printed by the toString() method of the IntSummaryStatistics class is shown below:

System.out.println(stats1);
//IntSummaryStatistics{count=2, sum=14, min=6, average=7.000000, max=8}

Below, the accept() method records the value 10 (the number of tracks on CD.cd2) into the summary information referenced by stats1. The resulting statistics show the new count is 3 (=2 +1), the new sum is 24 (=14+10), and the new average is 8.0 (=24.0/3.0). However, the min value was not affected but the max value has changed to 10.

stats1.accept(CD.cd2.noOfTracks());     // Add the value 10.
System.out.println(stats1);
//IntSummaryStatistics{count=3, sum=24, min=6, average=8.000000, max=10}

The code below creates another IntSummaryStatistics object that summarizes the statistics from two other CDs.

IntSummaryStatistics stats2 = List.of(CD.cd3, CD.cd4)
    .stream()
    .mapToInt(CD::noOfTracks)
    .summaryStatistics();
System.out.println(stats2);
//IntSummaryStatistics{count=2, sum=18, min=8, average=9.000000, max=10}

The combine() method incorporates the state of one IntSummaryStatistics object into another IntSummaryStatistics object. In the code below, the state of the IntSummary-Statistics object referenced by stats2 is combined with the state of the IntSummary-Statistics object referenced by stats1. The resulting summary information is printed, showing that the new count is 5 (=3 +2), the new sum is 42 (=24+18), and the new average is 8.4 (=42.0/5.0). However, the min and max values were not affected.

stats1.combine(stats2);                 // Combine stats2 with stats1.
System.out.println(stats1);
//IntSummaryStatistics{count=5, sum=42, min=6, average=8.400000, max=10}

Calling the summaryStatistics() method on an empty stream returns an instance of the IntSummaryStatistics class with a zero value set for all statistics, except for the min and max values, which are set to Integer.MAX_VALUE and Integer.MIN_VALUE, respectively. The IntSummaryStatistics class provides a zero-argument constructor that also returns an empty instance.

IntSummaryStatistics emptyStats = IntStream.empty().summaryStatistics();
System.out.println(emptyStats);
//IntSummaryStatistics{count=0, sum=0, min=2147483647, average=0.000000,
//max=-2147483648}

The summary statistics classes are not exclusive for use with streams, as they provide a constructor and appropriate methods to incorporate numeric values in order to calculate common statistics, as we have seen here. We will return to calculating statistics when we discuss built-in collectors (p. 978).

Summary of Terminal Stream Operations

The terminal operations of the Stream<T> class are summarized in Table 16.5. The type parameter declarations have been simplified, where any bound <? super T> or <? extends T> has been replaced by <T>, without impacting the intent of a method. A reference is provided to each method in the first column.

The last column in Table 16.5 indicates the function type of the corresponding parameter in the previous column. It is instructive to note how the functional interface parameters provide the parameterized behavior of an operation. For example, the method allMatch() returns a boolean value to indicate whether all elements of a stream satisfy a given predicate. This predicate is implemented as a functional interface Predicate<T> that is applied to each element in the stream.

The interfaces IntStream, LongStream, and DoubleStream define analogous methods to those shown for the Stream<T> interface in Table 16.5. Methods that are only defined by the numeric stream interfaces are shown in Table 16.6.

Table 16.5 Terminal Stream Operations

Method name (ref.)

Any type parameter + return type

Functional interface parameters

Function type of parameters

forEach (p. 948)

void

(Consumer<T> action)

T -> void

forEachOrdered (p. 948)

void

(Consumer<T> action)

T -> void

allMatch (p. 949)

boolean

(Predicate<T> predicate)

T -> boolean

anyMatch (p. 949)

boolean

(Predicate<T> predicate)

T -> boolean

noneMatch (p. 949)

boolean

(Predicate<T> predicate)

T -> boolean

findAny (p. 952)

Optional<T>

()

 

findFirst (p. 952)

Optional<T>

()

 

count (p. 953)

long

()

 

max (p. 954)

Optional<T>

(Comparator<T> cmp)

(T,T) -> int

min (p. 954)

Optional<T>

(Comparator<T> cmp)

(T,T) -> int

reduce (p. 955)

Optional<T>

(BinaryOperator<T> accumulator)

(T,T) -> T

reduce (p. 955)

T

(T identity,
BinaryOperator<T> accumulator)

T -> T,

(T,T) -> T

reduce (p. 955)

<U> U

(U identity,
BiFunction<U,T,U> accumulator,
BinaryOperator<U> combiner)

U -> U,

(U,T) -> U,

(U,U) -> U

collect (p. 964)

<R,A> R

(Collector<T,A,R> collector)

Parameter is not a functional interface.

collect (p. 964)

<R> R

(Supplier<R> supplier,
BiConsumer<R,T> accumulator,
BiConsumer<R,R> combiner)

() -> R,

(R,T) -> void,

(R,R) -> void

toArray (p. 971)

Object[]

()

 

toArray (p. 971)

<A> A[]

(IntFunction<A[]> generator)

int -> A[]

toList (p. 972)

List<T>

()

 

Table 16.6 Additional Terminal Operations in the Numeric Stream Interfaces

Method name (ref.)

Return type

average (p. 949)

OptionalNumType, where NumType is Int, Long, or Double

sum (p. 949)

numtype, where numtype is int, long, or double

summaryStatistics (p. 974)

NumTypeSummaryStatistics, where NumType is Int, Long, or Double

InformIT Promotional Mailings & Special Offers

I would like to receive exclusive offers and hear about products from InformIT and its family of brands. I can unsubscribe at any time.

Overview


Pearson Education, Inc., 221 River Street, Hoboken, New Jersey 07030, (Pearson) presents this site to provide information about products and services that can be purchased through this site.

This privacy notice provides an overview of our commitment to privacy and describes how we collect, protect, use and share personal information collected through this site. Please note that other Pearson websites and online products and services have their own separate privacy policies.

Collection and Use of Information


To conduct business and deliver products and services, Pearson collects and uses personal information in several ways in connection with this site, including:

Questions and Inquiries

For inquiries and questions, we collect the inquiry or question, together with name, contact details (email address, phone number and mailing address) and any other additional information voluntarily submitted to us through a Contact Us form or an email. We use this information to address the inquiry and respond to the question.

Online Store

For orders and purchases placed through our online store on this site, we collect order details, name, institution name and address (if applicable), email address, phone number, shipping and billing addresses, credit/debit card information, shipping options and any instructions. We use this information to complete transactions, fulfill orders, communicate with individuals placing orders or visiting the online store, and for related purposes.

Surveys

Pearson may offer opportunities to provide feedback or participate in surveys, including surveys evaluating Pearson products, services or sites. Participation is voluntary. Pearson collects information requested in the survey questions and uses the information to evaluate, support, maintain and improve products, services or sites, develop new products and services, conduct educational research and for other purposes specified in the survey.

Contests and Drawings

Occasionally, we may sponsor a contest or drawing. Participation is optional. Pearson collects name, contact information and other information specified on the entry form for the contest or drawing to conduct the contest or drawing. Pearson may collect additional personal information from the winners of a contest or drawing in order to award the prize and for tax reporting purposes, as required by law.

Newsletters

If you have elected to receive email newsletters or promotional mailings and special offers but want to unsubscribe, simply email information@informit.com.

Service Announcements

On rare occasions it is necessary to send out a strictly service related announcement. For instance, if our service is temporarily suspended for maintenance we might send users an email. Generally, users may not opt-out of these communications, though they can deactivate their account information. However, these communications are not promotional in nature.

Customer Service

We communicate with users on a regular basis to provide requested services and in regard to issues relating to their account we reply via email or phone in accordance with the users' wishes when a user submits their information through our Contact Us form.

Other Collection and Use of Information


Application and System Logs

Pearson automatically collects log data to help ensure the delivery, availability and security of this site. Log data may include technical information about how a user or visitor connected to this site, such as browser type, type of computer/device, operating system, internet service provider and IP address. We use this information for support purposes and to monitor the health of the site, identify problems, improve service, detect unauthorized access and fraudulent activity, prevent and respond to security incidents and appropriately scale computing resources.

Web Analytics

Pearson may use third party web trend analytical services, including Google Analytics, to collect visitor information, such as IP addresses, browser types, referring pages, pages visited and time spent on a particular site. While these analytical services collect and report information on an anonymous basis, they may use cookies to gather web trend information. The information gathered may enable Pearson (but not the third party web trend services) to link information with application and system log data. Pearson uses this information for system administration and to identify problems, improve service, detect unauthorized access and fraudulent activity, prevent and respond to security incidents, appropriately scale computing resources and otherwise support and deliver this site and its services.

Cookies and Related Technologies

This site uses cookies and similar technologies to personalize content, measure traffic patterns, control security, track use and access of information on this site, and provide interest-based messages and advertising. Users can manage and block the use of cookies through their browser. Disabling or blocking certain cookies may limit the functionality of this site.

Do Not Track

This site currently does not respond to Do Not Track signals.

Security


Pearson uses appropriate physical, administrative and technical security measures to protect personal information from unauthorized access, use and disclosure.

Children


This site is not directed to children under the age of 13.

Marketing


Pearson may send or direct marketing communications to users, provided that

  • Pearson will not use personal information collected or processed as a K-12 school service provider for the purpose of directed or targeted advertising.
  • Such marketing is consistent with applicable law and Pearson's legal obligations.
  • Pearson will not knowingly direct or send marketing communications to an individual who has expressed a preference not to receive marketing.
  • Where required by applicable law, express or implied consent to marketing exists and has not been withdrawn.

Pearson may provide personal information to a third party service provider on a restricted basis to provide marketing solely on behalf of Pearson or an affiliate or customer for whom Pearson is a service provider. Marketing preferences may be changed at any time.

Correcting/Updating Personal Information


If a user's personally identifiable information changes (such as your postal address or email address), we provide a way to correct or update that user's personal data provided to us. This can be done on the Account page. If a user no longer desires our service and desires to delete his or her account, please contact us at customer-service@informit.com and we will process the deletion of a user's account.

Choice/Opt-out


Users can always make an informed choice as to whether they should proceed with certain services offered by InformIT. If you choose to remove yourself from our mailing list(s) simply visit the following page and uncheck any communication you no longer want to receive: www.informit.com/u.aspx.

Sale of Personal Information


Pearson does not rent or sell personal information in exchange for any payment of money.

While Pearson does not sell personal information, as defined in Nevada law, Nevada residents may email a request for no sale of their personal information to NevadaDesignatedRequest@pearson.com.

Supplemental Privacy Statement for California Residents


California residents should read our Supplemental privacy statement for California residents in conjunction with this Privacy Notice. The Supplemental privacy statement for California residents explains Pearson's commitment to comply with California law and applies to personal information of California residents collected in connection with this site and the Services.

Sharing and Disclosure


Pearson may disclose personal information, as follows:

  • As required by law.
  • With the consent of the individual (or their parent, if the individual is a minor)
  • In response to a subpoena, court order or legal process, to the extent permitted or required by law
  • To protect the security and safety of individuals, data, assets and systems, consistent with applicable law
  • In connection the sale, joint venture or other transfer of some or all of its company or assets, subject to the provisions of this Privacy Notice
  • To investigate or address actual or suspected fraud or other illegal activities
  • To exercise its legal rights, including enforcement of the Terms of Use for this site or another contract
  • To affiliated Pearson companies and other companies and organizations who perform work for Pearson and are obligated to protect the privacy of personal information consistent with this Privacy Notice
  • To a school, organization, company or government agency, where Pearson collects or processes the personal information in a school setting or on behalf of such organization, company or government agency.

Links


This web site contains links to other sites. Please be aware that we are not responsible for the privacy practices of such other sites. We encourage our users to be aware when they leave our site and to read the privacy statements of each and every web site that collects Personal Information. This privacy statement applies solely to information collected by this web site.

Requests and Contact


Please contact us about this Privacy Notice or if you have any requests or questions relating to the privacy of your personal information.

Changes to this Privacy Notice


We may revise this Privacy Notice through an updated posting. We will identify the effective date of the revision in the posting. Often, updates are made to provide greater clarity or to comply with changes in regulatory requirements. If the updates involve material changes to the collection, protection, use or disclosure of Personal Information, Pearson will provide notice of the change through a conspicuous notice on this site or other appropriate way. Continued use of the site after the effective date of a posted revision evidences acceptance. Please contact us if you have questions or concerns about the Privacy Notice or any objection to any revisions.

Last Update: November 17, 2020