469

When you have to loop through a collection and make a string of each data separated by a delimiter, you always end up with an extra delimiter at the end, e.g.

for (String serverId : serverIds) {
  sb.append(serverId);
   sb.append(",");
}

Gives something like : serverId_1, serverId_2, serverId_3,

I would like to delete the last character in the StringBuilder (without converting it because I still need it after this loop).

Jared Burrows
  • 50,718
  • 22
  • 143
  • 180
Matthew
  • 9,868
  • 7
  • 48
  • 64
  • 11
    If by joining strings you mean "string concatenation", it depends on the number of strings and their lengths. Using a string builder is more efficient if you are going to be hammering in a lot of strings regardless of their size since strings are immutable. Every time you concatenate strings together you are creating a new resultant string (which is really a char array). String builders are essentially a list of char that doesn't become an immutable string until you call the toString() method. – dyslexicanaboko Jul 23 '12 at 00:19
  • 5
    If you're using Java 8, just use `StringJoiner`: http://stackoverflow.com/a/29169233/901641 – ArtOfWarfare Jul 10 '15 at 19:05

17 Answers17

685

Others have pointed out the deleteCharAt method, but here's another alternative approach:

String prefix = "";
for (String serverId : serverIds) {
  sb.append(prefix);
  prefix = ",";
  sb.append(serverId);
}

Alternatively, use the Joiner class from Guava :)

As of Java 8, StringJoiner is part of the standard JRE.

Tyler
  • 592
  • 1
  • 10
  • 24
Jon Skeet
  • 1,261,211
  • 792
  • 8,724
  • 8,929
  • Can't `""` and `","` be `char`s? If StringBuilder is mainly used for performance, why not go all the way? – Amy B Aug 27 '10 at 22:54
  • 9
    @Coronatus: No, because "" is the *absence* of any characters, not a single character. – Jon Skeet Aug 28 '10 at 09:28
  • 36
    won't executing prefix=","; every loop cycle affect performance? – Harish Oct 24 '11 at 10:10
  • 22
    @Harish: Possibly, a tiny, tiny bit - very unlikely to be significant though. – Jon Skeet Oct 24 '11 at 10:14
  • 6
    @Harish - and possibly not at all, if the optimizer unrolls the first loop iteration. – Stephen C Aug 27 '12 at 14:28
  • 1
    At first I was going to disagree with Jon Skeet's response that this wouldn't really affect performance; then I remembered that the JVM caches String literals so indeed it wouldn't make much difference. See this link about that: http://stackoverflow.com/a/334580/483257 Though, I'm still not sold this as readable as using deleteCharAt() but pretty minor difference I think. – Bane Apr 12 '13 at 22:59
  • 2
    @Bane I agree that the `deleteCharAt()` approach is more readable than the otherwise clever one offered by Jon Skeet. That said, his second suggestion is probably the most readable of all. The whole thing becomes declaring the joiner: `Joiner joiner = Joiner.on(",").useForNull("null");` and then using it like so: `String result = joiner.join(serverIds);`. – Liam Sep 17 '13 at 18:20
  • 7
    Apache Commons does have another alternative to Guava's `Joiner` too in their `StringUtils`. http://commons.apache.org/proper/commons-lang/javadocs/api-2.6/org/apache/commons/lang/StringUtils.html#join(java.lang.Object[], java.lang.String) – GoRoS Sep 28 '13 at 16:50
  • 1
    @Liam very good point, thank you for highlighting that for me. :) Though, I would argue (all things being equal), GoRoS' suggestion might be even better just because, well, it's ApacheCommons. ;) – Bane Oct 01 '13 at 19:07
  • 2
    @JonSkeet - Smart. I am curious to know if what lead you to think this way. Was it an instant thought or did you take some seconds to reason it out. I can imagine that you might have thought about it like this - lets try printing ,a,b,c so that there is no comma at the end. Now, how do I get rid of the first comma ? What if the first comma was a nothing ? But, after the first iteration, I want the nothing to become a comma. Hence my code...makes sense ? – Erran Morad Dec 11 '14 at 07:53
  • 2
    @BoratSagdiyev: I suspect I saw someone else do it, to be honest. Sorry not to be more inspiring! – Jon Skeet Dec 11 '14 at 08:21
  • and would `prefix = ",";` create a new instance of String every iteration? We should create a `comaString = ",";` before the loop and do `prefix = comaString;` in the loop. – Antoine Martin Mar 08 '19 at 13:05
  • 1
    @AntoineMartin: No, that doesn't create a new string on each iteration. There's no need to add an extra variable beforehand to try to micro-optimize. – Jon Skeet Mar 08 '19 at 16:59
  • @JonSkeet, ah yes I see now. This because of the string constant pool. – Antoine Martin Mar 09 '19 at 21:16
478

Another simple solution is:

sb.setLength(sb.length() - 1);

A more complicated solution:

The above solution assumes that sb.length() > 0 ... i.e. there is a "last character" to remove. If you can't make that assumption, and/or you can't deal with the exception that would ensue if the assumption is incorrect, then check the StringBuilder's length first; e.g.

// Readable version
if (sb.length() > 0) {
   sb.setLength(sb.length() - 1);
}

or

// Concise but harder-to-read version of the above.
sb.setLength(Math.max(sb.length() - 1, 0));
Stephen C
  • 632,615
  • 86
  • 730
  • 1,096
  • 27
    Very nice solution. Lowest impact on performance and least code required :) – Alain O'Dea Sep 06 '12 at 14:13
  • @Stephen C @ Alain O'Dea Not able to understand the performance gain here , Its doing an array copy internally selLenghth() -> ensureCapacityInternal() -> Arrays.copyOf -> System.arraycopy() – Manu Jose Oct 20 '20 at 12:28
  • @ManuJose - Read the code carefully. If `setLength` is reducing the length, `ensureCapacityInternal` will not call `Arrays.copy`. (I am looking at the Java 11 code, but I think this holds for all versions.) – Stephen C Oct 20 '20 at 12:44
205
if(sb.length() > 0){
    sb.deleteCharAt(sb.length() - 1);
}
bragboy
  • 32,353
  • 29
  • 101
  • 167
66

As of Java 8, the String class has a static method join. The first argument is a string that you want between each pair of strings, and the second is an Iterable<CharSequence> (which are both interfaces, so something like List<String> works. So you can just do this:

String.join(",", serverIds);

Also in Java 8, you could use the new StringJoiner class, for scenarios where you want to start constructing the string before you have the full list of elements to put in it.

ArtOfWarfare
  • 17,763
  • 14
  • 122
  • 177
43

Just get the position of the last character occurrence.

for(String serverId : serverIds) {
 sb.append(serverId);
 sb.append(",");
}
sb.deleteCharAt(sb.lastIndexOf(","));

Since lastIndexOf will perform a reverse search, and you know that it will find at the first try, performance won't be an issue here.

EDIT

Since I keep getting ups on my answer (thanks folks ), it is worth regarding that:

On Java 8 onward it would just be more legible and explicit to use StringJoiner. It has one method for a simple separator, and an overload for prefix and suffix.

Examples taken from here: example

Example using simple separator:

    StringJoiner mystring = new StringJoiner("-");    

    // Joining multiple strings by using add() method  
    mystring.add("Logan");  
    mystring.add("Magneto");  
    mystring.add("Rogue");  
    mystring.add("Storm");  

    System.out.println(mystring);

Output:

Logan-Magneto-Rogue-Storm

Example with suffix and prefix:

    StringJoiner mystring = new StringJoiner(",", "(", ")");    

    // Joining multiple strings by using add() method  
    mystring.add("Negan");  
    mystring.add("Rick");  
    mystring.add("Maggie");  
    mystring.add("Daryl");  

    System.out.println(mystring);

Output

(Negan,Rick,Maggie,Daryl)

Reuel Ribeiro
  • 1,129
  • 11
  • 21
  • 1
    You'll be sure that the last character is a `,` because it was the last statement of the `for loop`. The `lastInfexOf` is more for readability and to make it a no-brainer if you don't want to remember if it is 0-indexed or not. Further, you don't need to meddle with the stringbuilder length. It's just for the convenience. – Reuel Ribeiro Oct 31 '17 at 12:54
36

In this case,

sb.setLength(sb.length() - 1);

is preferable as it just assign the last value to '\0' whereas deleting last character does System.arraycopy

  • 1
    The `setLength` call is not assigning anything to the last value. Java string buffers are not null/zero terminated. In fact, `setLength` is simply updating a `length` field. – Stephen C Sep 19 '12 at 11:23
  • @Rohit Reddy Korrapolu: But the `arraycopy` copies 0 elements, so I guess it can get optimized away. – maaartinus Nov 03 '13 at 13:04
  • 2
    _If the newLength argument is greater than or equal to the current length, sufficient null characters ('\u0000') are appended so that length becomes the newLength argument._ Which is not the case. – fglez Jan 10 '14 at 03:47
12

With Java-8 you can use static method of String class,

String#join(CharSequence delimiter,Iterable<? extends CharSequence> elements).


public class Test {

    public static void main(String[] args) {

        List<String> names = new ArrayList<>();
        names.add("James");
        names.add("Harry");
        names.add("Roy");
        System.out.println(String.join(",", names));
    }
}

OUTPUT

James,Harry,Roy
ΔȺȾΔ
  • 21,571
  • 10
  • 52
  • 80
10

Another alternative

for(String serverId : serverIds) {
   sb.append(",");
   sb.append(serverId); 
}
sb.deleteCharAt(0);
jball
  • 23,602
  • 8
  • 65
  • 91
Rafiq
  • 1,950
  • 2
  • 20
  • 26
  • 3
    Should be better than removing the last char as this requires size calculations. Unless removing the first char causes data to be moved around... – slott Jul 17 '13 at 11:01
8

Alternatively,

StringBuilder result = new StringBuilder();
for(String string : collection) {
    result.append(string);
    result.append(',');
}
return result.substring(0, result.length() - 1) ;
VaL
  • 1,819
  • 13
  • 25
Zaki
  • 6,669
  • 6
  • 32
  • 50
6
StringBuilder sb = new StringBuilder();
sb.append("abcdef");
sb.deleteCharAt(sb.length() - 1);
assertEquals("abcde",sb.toString());
// true
Antoine
  • 3,526
  • 2
  • 34
  • 45
5

Yet another alternative:

public String join(Collection<String> collection, String seperator) {
    if (collection.isEmpty()) return "";

    Iterator<String> iter = collection.iterator();
    StringBuilder sb = new StringBuilder(iter.next());
    while (iter.hasNext()) {
        sb.append(seperator);
        sb.append(iter.next());
    }

    return sb.toString();
}
Jason Day
  • 8,629
  • 1
  • 38
  • 46
3

To avoid reinit(affect performance) of prefix use TextUtils.isEmpty:

            String prefix = "";
            for (String item : list) {
                sb.append(prefix);
                if (TextUtils.isEmpty(prefix))
                    prefix = ",";
                sb.append(item);
            }
NickUnuchek
  • 8,369
  • 9
  • 74
  • 111
1

I am doing something like below:

    StringBuilder stringBuilder = new StringBuilder();
    for (int i = 0; i < value.length; i++) {
        stringBuilder.append(values[i]);
        if (value.length-1) {
            stringBuilder.append(", ");
        }
    }
Vikasdeep Singh
  • 17,777
  • 9
  • 70
  • 93
1

You may try to use 'Joiner' class instead of removing the last character from your generated text;

                List<String> textList = new ArrayList<>();
                textList.add("text1");
                textList.add("text2");
                textList.add("text3");

                Joiner joiner = Joiner.on(",").useForNull("null");
                String output = joiner.join(textList);

               //output : "text1,text2,text3"
oguzhan
  • 1,503
  • 1
  • 20
  • 20
0

Here is another solution:

for(String serverId : serverIds) {
   sb.append(",");
   sb.append(serverId); 
}

String resultingString = "";
if ( sb.length() > 1 ) {
    resultingString = sb.substring(1);
}
Stephan
  • 37,597
  • 55
  • 216
  • 310
0

stringBuilder.Remove(stringBuilder.Length - 1, 1);

0

I found myself doing this quite a bit so I wrote a benchmark for the 3 main append delimiter techniques:

(benchmark with proper warmup and 100 rounds of 100,000 iterations)

"Append After"

static void appendAfter()
{
    sb.append('{');
    for (int i = 0; i < 10; i++)
    {
        sb.append('"');
        sb.append(i);
        sb.append('"');
        sb.append(':');
        sb.append(i);
        sb.append(',');
    }
    sb.setLength(sb.length() - 1);
    sb.append('}');
}

"Append Before"

static void appendBefore()
{
    sb.append('{');
    String delimiter = "";
    for (int i = 0; i < 10; i++)
    {
        sb.append(delimiter);
        sb.append('"');
        sb.append(i);
        sb.append('"');
        sb.append(':');
        sb.append(i);
        delimiter = ",";
    }
    sb.append('}');
}

"Append Maybe"

static void appendMaybe()
{
    sb.append('{');
    for (int i = 0; i < 10; i++)
    {
        sb.append('"');
        sb.append(i);
        sb.append('"');
        sb.append(':');
        sb.append(i);
        if (i < 9)
        {
            sb.append(',');
        }
    }
    sb.append('}');
}

I got the following results:

Platform Append After Append Before Append Maybe
Windows Server 2016, Java 11 - Hotspot 26ms 40ms 26ms
Windows Server 2016, Java 8 - Hotspot 27ms 36ms 21ms
Windows Server 2016, Java 11 - OpenJ9 63ms 81ms 59ms
Windows Server 2016, Java 8 - OpenJ9 66ms 64ms 55ms

Aside from being the fastest, I am of the opinion that the "Append Maybe" implementation shows the intent of the code the best. That is usually more important than the fraction of nanoseconds gained per iteration.

I left the benchmark code here in case anyone wanted to try it on their platform. Please contribute your results above if you do so!

egerardus
  • 10,746
  • 11
  • 75
  • 117