Remove last character of a StringBuilder?
Asked Answered
D

19

533

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).

Duane answered 3/8, 2010 at 9:41 Comment(2)
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.Intimist
If you're using Java 8, just use StringJoiner: https://mcmap.net/q/73628/-remove-last-character-of-a-stringbuilderHeisenberg
T
742

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.

Thermocline answered 3/8, 2010 at 9:49 Comment(14)
Can't "" and "," be chars? If StringBuilder is mainly used for performance, why not go all the way?Commixture
@Coronatus: No, because "" is the absence of any characters, not a single character.Thermocline
won't executing prefix=","; every loop cycle affect performance?Illtempered
@Harish: Possibly, a tiny, tiny bit - very unlikely to be significant though.Thermocline
@Illtempered - and possibly not at all, if the optimizer unrolls the first loop iteration.Olives
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: https://mcmap.net/q/36747/-java-strings-quot-string-s-new-string-quot-silly-quot-quot Though, I'm still not sold this as readable as using deleteCharAt() but pretty minor difference I think.Solemn
@Solemn 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);.Anglicism
Apache Commons does have another alternative to Guava's Joiner too in their StringUtils. commons.apache.org/proper/commons-lang/javadocs/api-2.6/org/…, java.lang.String)Messenger
@Anglicism 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. ;)Solemn
@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 ?Bertabertasi
@BoratSagdiyev: I suspect I saw someone else do it, to be honest. Sorry not to be more inspiring!Thermocline
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.Wallah
@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.Thermocline
@JonSkeet, ah yes I see now. This because of the string constant pool.Wallah
O
552

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));
Olives answered 3/8, 2010 at 9:47 Comment(3)
Very nice solution. Lowest impact on performance and least code required :)Murderous
@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()Tiga
@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.)Olives
R
223
if(sb.length() > 0){
    sb.deleteCharAt(sb.length() - 1);
}
Refulgent answered 3/8, 2010 at 9:43 Comment(3)
This gets upvoted too much but its not efficient, it does a system.arraycopy. What @Rohit Reddy Korrapolu said.Vereeniging
Is this safe with surrogate pair characters at play?Ring
Assuming that the last character is the comma separator (as per the example), then surrogates make no difference. If you need to generalize then subtract separator.length() instead of 1.Olives
H
68

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.

Heisenberg answered 20/3, 2015 at 14:43 Comment(2)
nvm edited for you if you don't mind, removed my comment alsoFaroff
@Faroff - I rewrote the answer completely to focus on String.join instead of StringJoiner.Heisenberg
H
49

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)

Hypocotyl answered 13/10, 2015 at 13:54 Comment(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.Hypocotyl
S
40

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

Shoplifter answered 12/4, 2012 at 20:13 Comment(3)
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.Olives
@Rohit Reddy Korrapolu: But the arraycopy copies 0 elements, so I guess it can get optimized away.Roshelle
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.Kmeson
N
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
Nevarez answered 27/12, 2015 at 4:29 Comment(0)
F
10

Another alternative

for(String serverId : serverIds) {
   sb.append(",");
   sb.append(serverId); 
}
sb.deleteCharAt(0);
Flounder answered 26/12, 2011 at 9:28 Comment(1)
Should be better than removing the last char as this requires size calculations. Unless removing the first char causes data to be moved around...Cohesion
M
9

Alternatively,

StringBuilder result = new StringBuilder();
for(String string : collection) {
    result.append(string);
    result.append(',');
}
return result.substring(0, result.length() - 1) ;
Motmot answered 3/8, 2010 at 9:51 Comment(1)
Usable as you can add a "." at the end.Mcclees
E
6
StringBuilder sb = new StringBuilder();
sb.append("abcdef");
sb.deleteCharAt(sb.length() - 1);
assertEquals("abcde",sb.toString());
// true
Envision answered 3/8, 2010 at 9:48 Comment(0)
U
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();
}
Uterine answered 3/8, 2010 at 11:33 Comment(0)
E
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);
            }
Erv answered 26/2, 2016 at 9:14 Comment(2)
What kind of package does TestUtils belong to?Bestead
@Bestead android.text.TextUtilsErv
C
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(", ");
        }
    }
Crepuscular answered 6/4, 2015 at 8:49 Comment(0)
J
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"
Jealous answered 1/7, 2016 at 13:59 Comment(0)
C
1

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!

Cuvette answered 5/4, 2021 at 0:48 Comment(1)
did you try removing the last char. For me, it looks more performant since removing the last char can be done outside the for loopCongreve
C
0

Here is another solution:

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

String resultingString = "";
if ( sb.length() > 1 ) {
    resultingString = sb.substring(1);
}
Centerpiece answered 22/12, 2016 at 11:53 Comment(1)
But anyway, this is just a small variant on Zaki's solution from 2010.Olives
K
0

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

Kwabena answered 10/2, 2020 at 4:32 Comment(2)
There is no remove method. And Length (or length) is not field. It is a method.Olives
I think this may be C#Interplanetary
S
0

If you don't want to use StringJoiner, here's a small improvement on the @superpupervlad technique (with a good gain for large lists), which reduces the number of assignments.

char prefix = '\0'; // same as ""
for (String serverId : serverIds) {
  sb.append(prefix);
  if (prefix == '\0') prefix = ',';
  sb.append(serverId);
}
Substructure answered 15/11, 2023 at 9:48 Comment(0)
H
0

A much more simple and compact solution just requires a small modification to your code.

for (String serverId : serverIds) {
  if (!sb.isEmpty()) sb.append(",")
  sb.append(serverId);
}
Heikeheil answered 17/11, 2023 at 17:18 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.