Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Java String.join but also add delimiter to the end

Tags:

java

string

I have the array ["a", "b", "c"] and I want to join them together to form "a#b#c#". String.join will only get me "a#b#c". I can't just do str += "#" either because that is slow (Java has to create a new string to do that). So instead I have to rewrite the whole thing using StringBuilder. Is there some method Java has that is basically String.join but also appends the delimiter to the end?

For a little more context, I'm trying to create a Suffix Array data structure with a group of strings, so this part of it is actually a bottleneck.

like image 434
Will Kanga Avatar asked Jul 17 '21 19:07

Will Kanga


People also ask

How do you append a string with a comma separated?

In Java, we can use String. join(",", list) to join a List String with commas.

Can be replaced with string join?

join is not as a general replacement for + or String. concat , but in being the "reverse operation" of a String. split operation. It makes more sense in that context - when you have a bunch of strings that you want to join together using a delimiter - than as a replacement for concat .


3 Answers

You can use Stream API Collectors.joining() There are prefix and suffix arguments like that:

    Stream.of("a", "b", "c")
        .collect(Collectors.joining("#", "", "#"));

Where joining arguments is delimiter, prefix, suffix respectively.

Also, you can just add an empty String to your array like says @Wander Nauta in the comment above

I've started JMH, also and reproduce performance differences which wrote bellow:

public class Benchmarks {

private static final String[] arr = {"a", "b", "c"};

@Benchmark
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Fork(value = 1, warmups = 1)
@BenchmarkMode(Mode.AverageTime)
public String joinAndConcat() {
    return String.join("#", arr) + "#";
}

@Benchmark
@Fork(value = 1, warmups = 1)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@BenchmarkMode(Mode.AverageTime)
public String streamsJoining() {
    return Stream.of(arr)
            .parallel()
            .collect(Collectors.joining("#", "", "#"));
}

@Benchmark
@Fork(value = 1, warmups = 1)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@BenchmarkMode(Mode.AverageTime)
public String stringJoiner() {
    StringJoiner joiner = new StringJoiner("#", "", "#");
    for (String el : arr) {
        joiner.add(el);
    }
    return joiner.toString();
  }
}

Results:

> Benchmark                  Mode  Cnt   Score   Error  Units
> Benchmarks.joinAndConcat   avgt    5  46,670 ± 0,139  ns/op
> Benchmarks.streamsJoining  avgt    5  73,336 ± 0,180  ns/op
> Benchmarks.stringJoiner    avgt    5  27,236 ± 0,386  ns/op

But you must understand that 46 nSec is a very small difference for most applications.

like image 54
Dmitrii B Avatar answered Nov 10 '22 22:11

Dmitrii B


Here are the performance measures using JMH:

Benchmark       Mode   Cnt         Score        Error  Units
StringJoiner    thrpt  100  23641181.522 ± 237176.955  ops/s
JoinAndConcat   thrpt  100  14197523.377 ± 130873.538  ops/s
StreamsJoining  thrpt  100   9538282.522 ± 156729.920  ops/s

The solution with streams takes around 2.5x longer compared to the solution with StringJoiner. In the midfield is as expected the join with concatenation (see question). However, we are talking about nanoseconds here.

A graphical overview showing the performance with recalculated values (ops/s => ns/op): result


# JMH version: 1.32
# VM version: JDK 11.0.11, OpenJDK 64-Bit Server VM, 11.0.11+9
# VM invoker: /Library/Java/JavaVirtualMachines/adoptopenjdk-11.jdk/Contents/Home/bin/java
# VM options: <none>
# Blackhole mode: full + dont-inline hint
# Warmup: 10 iterations, 1 s each
# Measurement: 10 iterations, 1 s each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Throughput, ops/time
import java.util.StringJoiner;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.openjdk.jmh.annotations.Benchmark;

public class Benchmarks {

    private static final String[] arr = {"a", "b", "c"};

    @Benchmark
    public String joinAndConcat() {
        return String.join("#", arr) + "#";
    }

    @Benchmark
    public String streamsJoining() {
        return Stream.of(arr)
                .collect(Collectors.joining("#", "", "#"));
    }

    @Benchmark
    public String stringJoiner() {
        StringJoiner joiner = new StringJoiner("#", "", "#");
        for (String el : arr) {
            joiner.add(el);
        }
        return joiner.toString();
    }
}
like image 20
Matt Avatar answered Nov 10 '22 22:11

Matt


String.join() uses StringJoiner behind the scenes so you can create one and specify suffix parameter as # in the constructor:

String[] array = { "a", "b", "c" };
StringJoiner joiner = new StringJoiner("#", "", "#");
for (String el : array) {
  joiner.add(el);
}
System.out.println(joiner.toString()); // a#b#c#

However this feels like micro optimizing. Unless your joining code is critical it's way more readable to write:

String[] array = { "a", "b", "c" };
System.out.println(String.join("#", array) + "#"); // a#b#c#
like image 39
Karol Dowbecki Avatar answered Nov 10 '22 20:11

Karol Dowbecki