Java 8中的reduce BinaryOperator用于什么?

6
我正在阅读O'Reilly的《Java 8 Lambdas》这本书,它是一本非常好的书。我遇到了一个像这样的例子。
我有一个...(此处缺少上下文,无法确定具体翻译)
private final BiFunction<StringBuilder,String,StringBuilder>accumulator=
(builder,name)->{if(builder.length()>0)builder.append(",");builder.append("Mister:").append(name);return builder;};

final Stream<String>stringStream = Stream.of("John Lennon","Paul Mccartney"
,"George Harrison","Ringo Starr");
final StringBuilder reduce = stringStream
    .filter(a->a!=null)
    .reduce(new StringBuilder(),accumulator,(left,right)->left.append(right));
 System.out.println(reduce);
 System.out.println(reduce.length());

这将产生正确的输出。

Mister:John Lennon,Mister:Paul Mccartney,Mister:George Harrison,Mister:Ringo Starr

我的问题与reduce方法有关,即最后一个参数是BinaryOperator

这个参数用于什么?如果我更改为

.reduce(new StringBuilder(),accumulator,(left,right)->new StringBuilder());

输出结果相同;如果我传递NULL,则返回N.P.E。
这个参数是用来做什么的?
更新
为什么如果我在parallelStream上运行它,我会收到不同的结果?
第一次运行:
returned StringBuilder length = 420

第二次运行:
returned StringBuilder length = 546

第三次运行:
returned StringBuilder length = 348

等等,为什么会这样 - 它不应该在每次迭代中返回所有值吗?
2个回答

16
接口 Stream 中的方法 reduce 是重载的。具有三个参数的方法的参数为:
  • identity(标识)
  • accumulator(累加器)
  • combiner(组合器)
combiner 支持并行执行。显然,它不用于 顺序流。但是,并没有这样的保证。如果您将您的更改为并行流,我想您会看到一个差别:
Stream<String>stringStream = Stream.of(
    "John Lennon", "Paul Mccartney", "George Harrison", "Ringo Starr")
    .parallel();

下面是一个示例,展示了如何使用combiner将顺序归约转换为支持并行执行的归约。有一个包含四个String的流,acc用作accumulator.apply的缩写。然后可以按照以下方式计算归约的结果:

acc(acc(acc(acc(identity, "one"), "two"), "three"), "four");

有一个兼容的combiner,可以将上述表达式转换为下面的表达式。现在可以在不同的线程中执行这两个子表达式。

combiner.apply(
    acc(acc(identity, "one"), "two"),
    acc(acc(identity, "three"), "four"));

关于您的第二个问题,我使用一个简化的累加器来解释这个问题:

BiFunction<StringBuilder,String,StringBuilder> accumulator =
    (builder,name) -> builder.append(name);

根据Stream::reduce的Javadoc,accumulator必须是可交换的。在这种情况下,这意味着以下两个表达式返回相同的结果:

acc(acc(acc(identity, "one"), "two"), "three")  
acc(acc(identity, "one"), acc(acc(identity, "two"), "three"))

对于上面的accumulator,那是不正确的。问题在于,你正在改变identity所引用的对象。这对于reduce操作来说是个坏主意。这里有两种替代实现方案,应该可以正常工作:

// identity = ""
BiFunction<String,String,String> accumulator = String::concat;

// identity = null
BiFunction<StringBuilder,String,StringBuilder> accumulator =
    (builder,name) -> builder == null
        ? new StringBulder(name) : builder.append(name);

谢谢Nosid,我有一个问题,为什么每次迭代都会得到不同的结果,我猜是因为并行化...为什么使用相同的代码名称会得到多个结果?请查看我编辑过的问题。 - chiperortiz
@chiperortiz:我已经更新了关于你的第二个问题的回答。这个例子真的来自这本书吗?在这种情况下,短语“好书”似乎是有问题的。 - nosid
书中有一个更复杂的例子,仅使用顺序流。 - chiperortiz
我会使用BiFunction,但是我应该使用相同的BinaryOperator吗? - chiperortiz

3

nosid的回答基本上是正确的(+1),但我想强调一个特定的点。

reduce函数中的identity参数必须是一个身份。如果它是一个对象,那么它应该是不可变的。如果“identity”对象被改变,它就不再是一个身份了!关于这一点的更多讨论,请参见我的回答相关问题。

看起来这个例子源自Richard Warburton的《Java 8 Lambdas》(O'Reilly 2014)第5-19个例子。如果是这样,我得和Warburton博士好好谈谈。


2
同样地,reduce函数的BinaryOperator参数必须是可结合的。否则,在并行计算时会得到无意义的结果。 - Brian Goetz
Richard的例子是在顺序流中使用的,而不是并行流。Stuart感谢你的回复... - chiperortiz
@chiperortiz 确实,这个例子是顺序的,但是代码无论是在顺序还是并行执行中都应该给出正确的结果,尤其是在一本试图解释这些内容的书籍中。此外,我怀疑即使是顺序代码也违反了一些限制,并且它只是碰巧给出了正确的结果。 - Stuart Marks

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接