Java 8中Stream.collect方法中的组合器是如何工作的?

31

我创建了一个简单的演示:

public static void main(String[] args) {
        List<String> list2 = Arrays.asList("adf", "bcd", "abc", "hgr", "jyt", "edr", "biu");

String collect = list2.stream().collect(String::new, (res, elem) -> {
              res=res.concat(" ").concat(elem);
//            System.out.printf("res=%s, elem=%s\n", res.isEmpty(), elem);

        }, (res1, res2) -> {
            System.out.printf("res1=%s, res2=%s\n", res1, res2);            
        });
        System.out.println("collect=" + collect);
}
问题在于collect中的BiConsumer combiner部分根本不运行。
如果我使用parallelStream(),它会运行,但是两个参数res1res2等于供应商String::new
如何使collect方法中的combiner工作?

3
注意:如果你想用分隔符来连接元素,请使用.collect(Collectors.joining(" ")),或者如果你有一个字符串列表,则使用String.join(" ", list2) - Alexis C.
3
请注意,你的尝试更像是将 String 不可变对象 合并 起来:list2.stream().reduce("", (res, elem) -> res.concat(" ").concat(elem))。但是,正如 @Alexis C. 指出的那样,Java 8 中已经有更有效率的内置解决方案。 - Holger
3个回答

30

首先,在非并行流中没有必要执行combiner,因为没有任何需要合并的东西。

其次,你的问题源于使用String::newString.concat。累加器应该通过将第二个参数与第一个参数组合来修改第一个参数,但由于Java中的字符串是不可变的,所以你的代码将产生空字符串。

          res=res.concat(" ").concat(elem);

会创建一个新的字符串,然后将其丢弃。您应该使用StringBuilder,这样可以保留中间结果:

public static void main(String[] args) {
    List<String> list2 = Arrays.asList("adf", "bcd", "abc", "hgr", "jyt", "edr", "biu");

    String collect = list2.stream().collect(StringBuilder::new, (res, elem) -> {
        res.append(" ").append(elem);
    }, (res1, res2) -> {
        res1.append(res2.toString());
        System.out.printf("res1=%s, res2=%s\n", res1, res2);
    }).toString();
    System.out.println("collect=" + collect);
}

这也可以在并行流中正确地工作。

res1 = hgr JYT,res2 = jyt
res1 = bcd abc,res2 = abc
res1 = adf bcd abc,res2 = bcd abc
res1 = edr biu,res2 = biu
res1 = hgr jyt edr biu,res2 = edr biu
res1 = adf bcd abc hgr jyt edr biu,res2 = hgr jyt edr biu
collect = adf bcd abc hgr jyt edr biu


当我运行这个示例时,我发现组合器仅在并行流中起作用(而不是同时)-否则,在单个流中,您只会得到最后一个System.out.println("collect=" + collect)的执行。 - Kirill Ch
@Raniz,为什么使用String::new会创建一个新字符串然后丢弃它? - michael
1
问题不在于 String::new,而是在于 String::concat。API 要求收集器 原地 修改结果,而不是创建一个新对象,这正是 String::concat 所做的,因为字符串是不可变的。 - Raniz

9

以下是Raniz的示例,展示了使用并行和不使用并行的更有趣的结果:

    String collect = list2.stream().collect(StringBuilder::new,
            (res, elem) -> {
                System.out.printf("ACCUMULATE res=%s, elem=%s\n", res, elem);
                res.append(" ").append(elem);
        },
            (res1, res2) -> {
                System.out.printf("COMBINE res1=%s, res2=%s\n", res1, res2);
                res1.append(res2.toString());
            }).toString();

从未称之为无并行组合

ACCUMULATE res=, elem=adf
ACCUMULATE res= adf, elem=bcd
ACCUMULATE res= adf bcd, elem=abc
ACCUMULATE res= adf bcd abc, elem=hgr
ACCUMULATE res= adf bcd abc hgr, elem=jyt
ACCUMULATE res= adf bcd abc hgr jyt, elem=edr
ACCUMULATE res= adf bcd abc hgr jyt edr, elem=biu
collect= adf bcd abc hgr jyt edr biu

并且,使用并行 list2.stream().parallel()...:

ACCUMULATE res=, elem=jyt
ACCUMULATE res=, elem=hgr
COMBINE res1= hgr, res2= jyt
ACCUMULATE res=, elem=biu
ACCUMULATE res=, elem=edr
COMBINE res1= edr, res2= biu
ACCUMULATE res=, elem=bcd
COMBINE res1= hgr jyt, res2= edr biu
ACCUMULATE res=, elem=abc
ACCUMULATE res=, elem=adf
COMBINE res1= bcd, res2= abc
COMBINE res1= adf, res2= bcd abc
COMBINE res1= adf bcd abc, res2= hgr jyt edr biu
collect= adf bcd abc hgr jyt edr biu

为什么我们要在res2上调用toString方法?这是必要的吗?我尝试将其删除,程序仍然可以正常运行。 - Alexander
1
@Alexander 如果能够正常工作,那就不需要了 :) 我已经从上面的示例中直接采用了这个解决方案。 - Grigory Kislin
好的,谢谢!你说的“with parallel”是什么意思?你是指在第二个例子中使用了parallelStream,因此它是多线程运行的吗? - aderchox
1
@aderchox 这意味着 stream().parallel() - Grigory Kislin

8

我认为combiner只在并行流中使用(用于组合并行计算的部分输出),因此请将您的流设置为并行流。

String collect = list2.parallelStream().collect(...

2
请参考此教程 - Vova Programmer
@AlexisC。我认为另一部分是在我回答之后添加的。 - Eran
3
而且没有什么阻止你更新你的回答...我认为有趣的部分是解释OP做错了什么以及“如何使组合器在collect方法中工作?”(尽管累加器也是错误的) - Alexis C.

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