9

Which one is better in terms of performance?

finalWords.stream().forEach(word -> stringBuilder.append(word).append(”,“)); 
String finalResult = stringBuilder.toString();

VS

String finalResult = finalWords.stream().collect(Collectors.joining(","));
Sagar
  • 5,315
  • 6
  • 37
  • 66
  • Honestly, I don't think that you will ever join 1 million strings. So prefer the most readable, regardless of the performance. – Arnaud Denoyelle May 04 '18 at 14:34
  • 1
    The best method is probably to [use `String.join`](https://stackoverflow.com/questions/1751844/java-convert-liststring-to-a-string). But you should probably [benchmark](https://stackoverflow.com/questions/504103/how-do-i-write-a-correct-micro-benchmark-in-java). – Bernhard Barker May 04 '18 at 14:40
  • @JaroslawPawlak Yes, I looked at the code and I am still not sure about the performance, that's why raised the question. Plus I am saving the time of a lot of devs who might have the same question in future. Hope that justifies the question. And I bet you too will learn something new with the discussion that's happening here! – Sagar May 04 '18 at 14:41
  • 1
    in `StringBuilder` you can specify a capacity, in `StringJoiner` you can't, it will grow `*2` every time, `8 -> 16 -> 32` array of Strings, these re-size use `Array.copyOf`. I'd say with a proper capacity `StringBuilder` would win in a speed test – Eugene May 04 '18 at 14:43
  • @Sagar Both approaches use `StringBuilder` so the difference will be insignificant. You can see some boiler plate code in the `joining` - all those new objects created (like `StringBuilder`) and all the if statements so it will be definitely a bit slower. Both approaches have `O(n)` complexity so it really comes down to what @ArnaudDenoyelle already said above - "prefer the most readable". – Jaroslaw Pawlak May 04 '18 at 16:01
  • 2
    Since the code does not do the same, it is pointless to discuss the performance differences. The first variant appends a trailing `","` which the joiner does not. Further, the first variant is not thread safe. Besides these differences, they do the same below the surface. So any performance difference stems from not doing the same thing. [As said by Dukeling](https://stackoverflow.com/questions/50177385/collectors-joining-vs-stringbuilder-append#comment87372184_50177385) you may also use `String.join(",", finalWords)` which is simpler (but also does basically the same)… – Holger May 04 '18 at 16:54

2 Answers2

10

I put together a small benchmark to test this out because I was curious. It initializes the List with size randomly-generated lowercase Strings, each having a length of 10:

@State(Scope.Benchmark)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 5, time = 500, timeUnit = TimeUnit.MILLISECONDS)
@Measurement(iterations = 10, time = 500, timeUnit = TimeUnit.MILLISECONDS)
@Fork(3)
public class MyBenchmark {

    @Param({"10", "100", "1000", "10000", "100000"})
    private int size;

    private List<String> finalWords;

    @Setup(Level.Invocation)
    public void initialize() {
        finalWords = IntStream.range(0, size)
                              .mapToObj(i -> {
                                  return ThreadLocalRandom.current()
                                                          .ints(10, 'a', 'z' + 1)
                                                          .mapToObj(c -> Character.toString((char) c))
                                                          .collect(Collectors.joining());
                              }).collect(Collectors.toList());
    }

    public static void main(String[] args) throws Exception {
        org.openjdk.jmh.Main.main(args);
    }

    @Benchmark
    public String stringBuilder() {
        StringBuilder sb = new StringBuilder();
        finalWords.forEach(word -> sb.append(word).append(","));
        return sb.toString();
    }

    @Benchmark
    public String stream() {
        return finalWords.stream().collect(Collectors.joining(","));
    }
}

Here are the results:

Benchmark                  (size)  Mode  Cnt        Score        Error  Units
MyBenchmark.stream             10  avgt   30      242.330 ±      5.177  ns/op
MyBenchmark.stream            100  avgt   30     1426.333 ±     20.183  ns/op
MyBenchmark.stream           1000  avgt   30    30779.509 ±   1114.992  ns/op
MyBenchmark.stream          10000  avgt   30   720944.424 ±  27845.997  ns/op
MyBenchmark.stream         100000  avgt   30  7701294.456 ± 648084.759  ns/op
MyBenchmark.stringBuilder      10  avgt   30      170.566 ±      1.833  ns/op
MyBenchmark.stringBuilder     100  avgt   30     1166.153 ±     21.162  ns/op
MyBenchmark.stringBuilder    1000  avgt   30    32374.567 ±    979.288  ns/op
MyBenchmark.stringBuilder   10000  avgt   30   473022.229 ±   8982.260  ns/op
MyBenchmark.stringBuilder  100000  avgt   30  4524267.849 ± 242801.008  ns/op

As you can see, the StringBuilder method is faster in this case, even when I don't specify an initial capacity.

Jacob G.
  • 28,856
  • 5
  • 62
  • 116
  • this is not a fair test, the first one does not initiate anything from `Stream API`, eating your time – Eugene May 04 '18 at 14:47
  • I also doubt you need `Level.Invocation` here, since all you do is collect them to a String ultimately – Eugene May 04 '18 at 14:48
  • @Eugene Are you recommending I use `stream().forEach(`? – Jacob G. May 04 '18 at 14:49
  • The question itself is probably which is faster `StringJoiner` or `StringBuilder`, and this what the test should be about, this way, well you are comparing different things. I would also add a test with an initial capacity of `StringBuilder` – Eugene May 04 '18 at 14:51
  • 2
    since the OP ultimately wants a String, this can go even further, how about `java-9 concat` + `java-9 with a different strategy`, something like `@Fork(jvmArgsAppend = "-Djava.lang.invoke.stringConcat=BC_SB")` or even `guava Joiner`, this would make a test I would like – Eugene May 04 '18 at 14:56
  • Would be interesting to also replace and benchmark the unnecessary `Stream.forEach()` loop with an ordinary `for` loop – Lukas Eder May 09 '18 at 10:24
  • @JacobG. What did you use for that benchmark? Are the annotations part of some benchmarking framework API? – TMG Dec 18 '19 at 13:52
  • 1
    @TMG It was most-likely JMH, the Java Microbenchmark Harness – Jacob G. Dec 18 '19 at 13:53
  • 1
    @JacobG. Wow, that was blazing fast answer! Thanks :-) – TMG Dec 18 '19 at 13:55
1

In java 17, the String concatenation was improved so we do not longer need StringBuffer anymore.

For more check JEP café : https://www.youtube.com/watch?v=w56RUzBLaRE&list=PLX8CzqL3ArzV4BpOzLanxd4bZr46x5e87&index=15

Badr Kacimi
  • 64
  • 10