31

I created simple demo :

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);
}

The problem is that BiConsumer combiner part of collect doesn't run at all.

It runs if I use parallelStream() but two arguments res1 and res2 are equal to supplier String::new.

How to make combiner work in collect method?

Vova Programmer
  • 335
  • 1
  • 3
  • 10
  • 3
    As a note, if you want to join elements by a separator, use `.collect(Collectors.joining(" "));` or `String.join(" ", list2)` if you have a list of strings. – Alexis C. Apr 30 '15 at 06:25
  • 3
    Note that your attempt is more like *reduce* as you are working with immutable `String`s: `list2.stream().reduce("", (res, elem) -> res.concat(" ").concat(elem))`. However, as [@Alexis C. pointed out](http://stackoverflow.com/questions/29959795/how-does-combiner-in-stream-collect-method-work-in-java-8#comment48041390_29959795), there are already more efficient built-in solutions. – Holger Apr 30 '15 at 07:48

3 Answers3

30

First off, there is no need for the combiner to be executed in a non-parallel stream since there is nothing to combine.

Secondly, your issue stems from using String::new and String.concat. The accumulator is supposed to modify the first argument by combining the second argument with it but since strings in Java are immutable your code will produce nothing.

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

will create a new string and then throw away it. You want to use a StringBuilder instead so you can keep the intermediate results:

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);
}

This will also work correctly with a parallel stream

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

Raniz
  • 10,882
  • 1
  • 32
  • 64
  • As I run this example I see that the combiner works ONLY in a parallel stream (not ALSO) - otherwise in a single stream you get only the execution of the last System.out.println("collect=" + collect); – Kirill Ch Aug 30 '18 at 09:40
  • @Raniz, why using String::new will cause create a new string and then throw away it? – michael Mar 03 '19 at 01:39
  • 1
    It's not `String::new` but `String::concat` that's the issue. The API expects the collector to modify the result _in place_, not to create a new object which is what `String::concat` does since strings are immutable. – Raniz Mar 04 '19 at 09:39
9

It is more interesting result with and without parallel for Raniz sample:

    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();

Without parallel combine never has been called:

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

And with parallel 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
Grigory Kislin
  • 16,647
  • 10
  • 125
  • 197
8

I think that the combiner is only used in parallel Streams (to combine the partial outputs of the parallel computations), so make your Stream parallel.

String collect = list2.parallelStream().collect(...
Eran
  • 387,369
  • 54
  • 702
  • 768
  • 2
    See [this tutorial](https://docs.oracle.com/javase/tutorial/collections/streams/reduction.html#collect) – Vova Programmer Apr 30 '15 at 05:53
  • @AlexisC. I think the other part was added after I answered it. – Eran Apr 30 '15 at 06:36
  • 3
    And nothing prevents you from updating your answer... I think the interesting part is to explain what's the OP did wrong and _"How to make combiner work in collect method?"_ (despite that the accumulator is also wrong) – Alexis C. Apr 30 '15 at 06:39