9.2.  Implement decomposition and reduction with streams



The reduce(T identity, BinaryOperator<T> accumulator) method of the Stream interface performs a reduction operation on the stream to reduce the stream to a single value. It takes an initial value and an accumulator that is a BinaryOperator<T> as arguments. The first time, the accumulator receives the initial value and the first element of the stream as arguments, and returns a value. The second time, the accumulator receives the value returned from its previous call and the second element from the stream. This process continues until all elements of the stream have been passed to the accumulator.

The returned value from the last call of the accumulator is returned from the reduce(...) method. The following snippet of code performs the summation of all integers in the stream:

List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8);
Integer sum = numbers.stream().reduce(0, (n1, n2) -> n1 + n2); // must be 36
System.out.print("Sum of all integers in the stream : " + sum);


The Integer class contains a static sum(...) method to perform sum of two integers. You can rewrite the code using a method reference, like so:

Integer sum = numbers.stream().reduce(0, Integer::sum);

Reduction operations

A reduction operation (also called a fold) takes a sequence of input elements and combines them into a single summary result by repeated application of a combining operation, such as finding the sum or maximum of a set of numbers, or accumulating elements into a list. The streams classes have multiple forms of general reduction operations, called reduce() and collect(), as well as multiple specialized reduction forms such as sum(), max(), or count().

Of course, such operations can be readily implemented as simple sequential loops, as in:

int sum = 0;
for (int x : numbers) {
    sum += x;

However, there are good reasons to prefer a reduce operation over a mutative accumulation such as the above. Not only is a reduction "more abstract" -- it operates on the stream as a whole rather than individual elements -- but a properly constructed reduce operation is inherently parallelizable, so long as the function(s) used to process the elements are associative and stateless. For example, given a stream of numbers for which we want to find the sum, we can write:

int sum = numbers.stream().reduce(0, (x, y) -> x + y);


int sum = numbers.stream().reduce(0, Integer::sum);

These reduction operations can run safely in parallel with almost no modification:

int sum = numbers.parallelStream().reduce(0, Integer::sum);

Reduction parallellizes well because the implementation can operate on subsets of the data in parallel, and then combine the intermediate results to get the final correct answer. (Even if the language had a "parallel for-each" construct, the mutative accumulation approach would still required the developer to provide thread-safe updates to the shared accumulating variable sum, and the required synchronization would then likely eliminate any performance gain from parallelism.) Using reduce() instead removes all of the burden of parallelizing the reduction operation, and the library can provide an efficient parallel implementation with no additional synchronization required.

In its more general form, a reduce operation on elements of type <T> yielding a result of type <U> requires three parameters:

 <U> U reduce(U identity,
     BiFunction<U, ? super T, U> accumulator,
     BinaryOperator<U> combiner);

The code to sum all the integers in a stream can be rewritten as follows:

List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8);
Integer sum = numbers.stream().reduce(0, 
    (res, i) -> res + i,                  // adds one element's value to partial result
    (part1, part2) -> part1 + part2);     // combine two partial results
System.out.print("Sum of all integers in the stream : " + sum);


Professional hosting         Exam 1Z0-817: Upgrade OCP Java 6, 7 & 8 to Java SE 11 Developer Quiz     Exam 1Z0-810: Upgrade to Java SE 8 Programmer Quiz