简体   繁体   中英

How does the reduce() method work in Java 8?

I try to understand how does the reduce() method work in .

For example I have this code:

public class App {

    public static void main(String[] args) {
        String[] arr = {"lorem", "ipsum", "sit", "amet"};
        List<String> strs = Arrays.asList(arr);

        int ijk = strs.stream().reduce(0, 
            (a, b) -> { 
                System.out.println("Accumulator, a = " + a + ", b = " + b);
                return a + b.length();
            },
            (a, b) -> {
                System.out.println("Combiner");
                return a * b;
            });
        System.out.println(ijk); 
    }
}

And the output is this:

Accumulator, a = 0, b = lorem
Accumulator, a = 5, b = ipsum
Accumulator, a = 10, b = sit
Accumulator, a = 13, b = amet
17

It is a sum of the length of these strings. And I see that the combiner is not accessed, so it will not multiply the numbers, it only adds the numbers.

But if I replace stream with parallelStream :

int ijk = strs.parallelStream().reduce(0, 
    (a, b) -> { 
        System.out.println("Accumulator, a = " + a + ", b = " + b);
        return a + b.length();
    },
    (a, b) -> {
        System.out.println("Combiner");
        return a * b;
    });

System.out.println(ijk); 

This is the output:

Accumulator, a = 0, b = ipsum
Accumulator, a = 0, b = lorem
Accumulator, a = 0, b = sit
Combiner
Accumulator, a = 0, b = amet
Combiner
Combiner
300

I see that the Accumulator and Combiner are accessed both, but only the multiplication is return. So what's happen with the sum?

You should read the documentation of reduce that says:

Additionally, the combiner function must be compatible with the accumulator function; for all u and t, the following must hold:

combiner.apply(u, accumulator.apply(identity, t)) == accumulator.apply(u, t)

In your case, you are breaking that law (doing a sum in accumulator and multiplication in the combiner ), so the result you see for such an operation is really undefined and depends on how the Spliterator for the underlying source is implemented (don't do that!).

Besides, the combiner is only called for a parallel stream.

Of course, your entire approach could be simplified to:

Arrays.asList("lorem", "ipsum", "sit", "amet")
      .stream()
      .mapToInt(String::length)
      .sum();

If you are doing that just for learning purposes, a correct reduce would be (to get the sum ):

strs.parallelStream()
    .reduce(0,
            (a, b) -> {
                  System.out.println("Accumulator, a = " + a + ", b = " + b);
                  return a + b.length();
            },
            (a, b) -> {
                  System.out.println("Combiner");
                  return a + b;
            });

The Key Concepts: Identity, Accumulator, and Combiner

Stream.reduce() operation : let's break down the operation's participant elements into separate blocks. That way, we'll understand more easily the role that each one plays

  • Identity – an element that is the initial value of the reduction operation and the default result if the stream is empty
  • itemAccumulator – a function that takes two parameters: a partial result of the reduction operation and the next element of the stream
  • Combiner – a function that takes two parameters: a partial result of the reduction operation and the next element of the stream Combiner – a function used to combine the partial result of the reduction operation when the reduction is parallelized, or when there's a mismatch between the types of the accumulator arguments and the types of the accumulator implementation

When a stream executes in parallel, the Java runtime splits the stream into multiple substreams. In such cases, we need to use a function to combine the results of the substreams into a single one. This is the role of the combiner

Case 1 : Combiner works with parallelStream as showed in your example

Case 2 : Example accumulator with different type of arguments

In this case, we have a stream of User objects, and the types of the accumulator arguments are Integer and User. However, the accumulator implementation is a sum of Integers, so the compiler just can't infer the type of the user parameter.

List<User> users = Arrays.asList(new User("John", 30), new User("Julie", 35));
int computedAges = users.stream().reduce(0, (partialAgeResult, user) -> partialAgeResult + user.getAge());

Compile Error

The method reduce(User, BinaryOperator<User>) in the type Stream<User> is not applicable for the arguments (int, (<no type> partialAgeResult, <no type> user) -> {})

We can fix this issue by using a combiner: which is method reference Integer::sum or by using lambda expression (a,b)->a+b

int computedAges = users.stream().reduce(0, (partialAgeResult, user) -> partialAgeResult + user.getAge(),Integer::sum);

To put it simply, if we use sequential streams and the types of the accumulator arguments and the types of its implementation match, we don't need to use a combiner.

There are 3 ways to reduce using . In a nutshell, Stream::reduce starts with two consequent items (or an identity value one with the first one) and performs an operation with them producing new reduced value. For each next item, the same happens and an operation is performed with the reduced value.

Let's say you have a stream of 'a' , 'b' , 'c' and 'd' . The reduction performs the following sequence of operations:

  1. result = operationOn('a', 'b') - the operationOn might be anything (sum of the lengths of inputs..)
  2. result = operationOn(result, 'c')
  3. result = operationOn(result, 'd')
  4. result is returned

The methods are:

I assume you chose to do addition and multiplication just as a demo to see what exactly happens.

As you already noticed, and as was already mentioned, the combiner is only called on parallel streams.

In short, on parallel strams, a part of the stream (resp. the underlying Spliterator) is cut off and processed by a different thread. After several parts are processed, their result is combined with a combiner.

In your case, the four elements all are processed by a different thread, and combination happens element-wise then. That's why you don't see any addition (apart from 0 + ) being applied, but only multiplication.

In order to get a meaningful result, however, you should switch from * to + and instead do a more meaningful output.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM