Search code examples
javajava-8reducejava-streambinary-operators

Java 8 stream parallel reduce BiFunction accumulator


I am learning Java 8. The most difficult thing I have to face is the Parallel Reduction. Here is the code from a example of the user @Stuart Marks I am studying with.

class ImmutableAverager 
{
    private final int total;
    private final int count;
    public ImmutableAverager(){this.total = 0;this.count = 0;}
   public ImmutableAverager(int total, int count)
   {
      this.total = total;
      this.count = count;
   }
   public double average(){return count > 0 ? ((double) total) / count : 0;}
   public ImmutableAverager accept(final int i)
   {
       return new ImmutableAverager(total + i, count + 1);
   }
   public ImmutableAverager combine(final ImmutableAverager other)
   {
       return new ImmutableAverager(total + other.total, count + other.count);
   }  

The call

public static void main(String[] args)     
{
       System.out.println(Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
        .parallel()
        .reduce(new ImmutableAverager(), 
                ImmutableAverager::accept,
                ImmutableAverager::combine)
        .average());    
}

This produce the right results but later I checked the signature of the reduce method

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

I would understand clearly if the code would be something like:

.reduce(new ImmutableAverager(),(a,b)->a.accept(b),(a,b)->a.combine(b))

I don't understand how:

 ImmutableAverager::accept

Can convert into a BiFunction

My understanding is this:

ImmutableAverager::accept

is convert it in something like

(ImmutableAverage a)->a.accept(); //but this is a function with 1 parameter not with 2 parameters.

and

ImmutableAverager::merge

can convert into a BinaryOperator. My friend @Stuart Marks says

The methods match the function arguments to reduce so we can use method references.


Solution

  • Yes, there's a subtlety here regarding the way that arguments are shifted when this kind of method reference is used, specifically an "unbound" method reference.

    Let's look at the second argument of reduce(). It wants

    BiFunction<U, ? super T, U> accumulator
    

    and so the signature of its abstract method is:

    U apply(U, T)
    

    (wildcard elided for brevity). The example had used a method reference ImmutableAverager::accept and its signature is:

    ImmutableAverager accept(int i)
    

    It looks like this doesn't work, because a BiFunction requires two arguments, whereas the accept method takes only one. But notice that the accept method is an instance method on the ImmutableAverager class, so it implicitly also takes a "receiver", that is, the object upon which this method is called. An ordinary call to this method might look like this:

    newAverager = oldAverager.accept(i);
    

    So really, the accept method actually takes two arguments even though it doesn't look like it. The first is the receiver, which is of type ImmutableAverager, and the second is of type int. The method call syntax makes it look like there's something special about the receiver, but there really isn't. It's as if this were a static method called like this:

    newAverager = accept(oldAverager, i);
    

    Now let's look at how this works with the reduce call. The code in question is,

    reduce(..., ImmutableAverager::accept, ...)
    

    I'm only showing the second argument here. This needs to be a BiFunction that takes arguments of U and T and returns a U as shown above. If you look at the accept method and treat the receiver not as something special but as an ordinary argument, it takes an argument of type ImmutableAverager and an argument of type int, and returns an ImmutableAverager. So U is inferred to be ImmutableAverager and T is inferred to be Integer (boxed from int), and the method reference here works.

    The key point is, for an unbound method reference, the method reference is to an instance method but the method is specified using the class name instead of an actual instance. When this occurs, the receiver turns into the first argument of the method call.