问题描述
问题很直接:为什么我们不能在StringBuilder(...)
流的identity function
操作中将reduce(...)
用作java8
,但是string1.concat(string2)
可以用作identity function
?
string1.concat(string2)
可以看作与builder.append(string)
类似(尽管可以理解,这些操作几乎没有区别),但是我无法理解reduce操作中的区别。考虑以下示例:
List<String> list = Arrays.asList("1","2","3");
// Example using the string concatenation operation
System.out.println(list.stream().parallel()
.reduce("",(s1,s2) -> s1 + s2,s2)->s1 + s2));
// The same example,using the StringBuilder
System.out.println(list.stream() .parallel()
.reduce(new StringBuilder(""),(builder,s) -> builder
.append(s),(builder1,builder2) -> builder1
.append(builder2)));
// using the actual concat(...) method
System.out.println(list.stream().parallel()
.reduce("",s2) -> s1.concat(s2),s2)->s1.concat(s2)));
以下是执行以上行后的输出:
123
321321321321 // output when StringBuilder() is used as Identity
123
builder.append(string)
与str1.concat(str2)
一样是一个关联操作。那么concat
为什么不能工作,append
却不能工作?
解决方法
是的,append
确实是关联的,但这并不是对作为累加器和组合器传递的函数的唯一要求。根据{{3}},他们必须是:
- 关联
- 无干扰
- 无状态
append
不是无状态的。这是有状态的。当您执行sb.append("Hello")
时,它不仅会返回在末尾附加StringBuilder
的{{1}},而且还会更改 {em {1}}。
也来自docs:
如果流操作的行为参数是有状态的,则流管道结果可能不确定或不正确。有状态的lambda(或其他实现适当功能接口的对象)是一种有状态的lambda,其结果取决于在流管道执行期间可能会改变的任何状态。
因此,一旦应用了累加器或组合器,Hello
就不是有效的标识。将会向空字符串生成器中添加一些内容,并且不再满足以下所有身份必须满足的方程:
sb
并行流有可能在调用累加器和/或组合器之后利用旧的字符串构建器,并且期望它们的内容不会更改。但是,累加器和组合器会使字符串生成器变异,从而导致流产生不正确的结果。
另一方面,new StringBuilder()
满足以上所有三个条件。它是无状态的,因为它不会更改调用它的字符串。它只是重新调整了一个新的串联字符串。 (combiner.apply(u,accumulator.apply(identity,t)) == accumulator.apply(u,t)
始终是不变的,不能更改:D)
无论如何,这是docs与concat
的用例:
String
,
阅读文档并进行了许多测试之后,我认为reduce类似于以下步骤:
- 会有多个线程来做reduce,每个线程都做一个 部分减少;
- 对于身份,将只有一个实例。每个累加器都将使用此身份实例;
- 首先使用标识实例和字符串元素进行累加以获得 StringBuilder;
- 结合所有这些StringBuilder;
因此,问题是每个实例都累积有身份实例,而字符串元素将导致身份更改。第一次积累后的身份不再是身份。
例如,我们考虑一个包含2个元素{“ 1”,“ 2”}的列表。 将有2个线程,每个线程做1个累加,其中一个最后合并。 线程A确实使用元素“ 1”累积了身份,然后结果是StringBuilder,其内容为“ 1”(仍然是身份,因为StringBuilder.append的返回对象本身就是它),但身份也更改为内容“ 1”。然后线程B确实使用元素“ 2”累积标识,然后结果是“ 12”,不再是“ 2”。 然后做合并就是这两个累加结果的结果,它们都是身份实例本身,所以结果将是“ 1212”。 就像下面的代码片段一样:
StringBuilder identity = new StringBuilder();
StringBuilder accumulate1 = identity.append("1");
StringBuilder accumulate2 = identity.append("2");
StringBuilder combine = accumulate1.append(accumulate2);
// combine and accumulate1 and accumulate2 are all identity instance and result is "1212"
return combine;
对于更多元素,由于线程随机运行,因此每次的结果都会有所不同。
知道原因之后,是否按以下方式修复累加器
new StringBuilder(builder).append(s)
和完整的行代码如下:
System.out.println(list.stream().parallel().reduce(new StringBuilder(),(builder,s) -> new StringBuilder(builder).append(s),(builder1,builder2) -> new StringBuilder(builder1).append(builder2)));
然后将不再有任何问题,因为累加器不会每次更改标识实例并返回新的StringBuilder。但是这样做并不值得,因为与String concat方法相比没有好处。
编辑:感谢@Holger的示例,似乎如果有过滤器功能,则可能会跳过一些累加器。因此组合器功能也需要更改为
new StringBuilder(builder1).append(builder2)
,
在已经存在实现(或像Sweeper的回答那样拥有.reduce()
)的情况下,请勿使用.collect()
。
List<String> list = Arrays.asList("1","2","3");
// Example using the string concatenation operation
System.out.println(list.stream()
.parallel()
.collect(Collectors.joining())
);
// prints "123"
编辑(这不适用于并行流)
取决于.joining()
的实现:
final List<String> list = Arrays.asList("1","3");
System.out.println(list.stream().reduce(new StringBuilder(),StringBuilder::append,StringBuilder::append)
.toString()
);
// prints "123"