Collectors.joining vs StringBuilder.append
Asked Answered
M

2

10

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(","));
Mouser answered 4/5, 2018 at 14:32 Comment(6)
Honestly, I don't think that you will ever join 1 million strings. So prefer the most readable, regardless of the performance.Arronarrondissement
The best method is probably to use String.join. But you should probably benchmark.Evanne
@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!Mouser
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 testPenoyer
@Mouser 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".Devan
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 you may also use String.join(",", finalWords) which is simpler (but also does basically the same)…Ibrahim
R
11

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.

Retrace answered 4/5, 2018 at 14:45 Comment(9)
this is not a fair test, the first one does not initiate anything from Stream API, eating your timePenoyer
I also doubt you need Level.Invocation here, since all you do is collect them to a String ultimatelyPenoyer
@Penoyer Are you recommending I use stream().forEach(?Retrace
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 StringBuilderPenoyer
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 likePenoyer
Would be interesting to also replace and benchmark the unnecessary Stream.forEach() loop with an ordinary for loopFlocculent
@JacobG. What did you use for that benchmark? Are the annotations part of some benchmarking framework API?Gunboat
@Gunboat It was most-likely JMH, the Java Microbenchmark HarnessRetrace
@JacobG. Wow, that was blazing fast answer! Thanks :-)Gunboat
R
4

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

Ribbentrop answered 9/8, 2023 at 15:36 Comment(1)
Actually the video says you do not need StringBuffer most of the time (for simple concat). You still should use StringBuffer when you concat a list of items (in a loop).Ollieollis

© 2022 - 2024 — McMap. All rights reserved.