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.
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.