String vs StringBuilder vs StringBuffer in Java

Introduction

One of the most used classes in Java is the String class. It represents a string (array) of characters, and therefore contains textual data such as "Hello World!". Besides the String class, there are two other classes used for similar purposes, though not nearly as often - StringBuilder and StringBuffer.

Each exists for its own reason, and unaware of the benefits of the other classes, many novice programmers only use Strings, leading to decreased performance and poor scalability.

String

Initializing a String is as easy as:

String string = "Hello World!";

It's atypical, as in all other cases, we'd instantiate an object using the new keyword, whereas here we have a "shortcut" version.

There are several ways to instantiate Strings:

// Most common, short way
String str1 = "Hello World";

// Using the `new` keyword and passing text to the constructor
String str2 = new String("Hello World");

// Initializing an array of characters and assigning them to a String
char[] charArray = {'H', 'e', 'l', 'l', 'o', ' ', 'W', 'o', 'r', 'l', 'd'};
String str3 = new String(charArray);

Let's take a look at the source code of the class and make a few observations:

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];

    /**
     * Initializes a newly created {@code String} object so that it represents
     * an empty character sequence.  Note that use of this constructor is
     * unnecessary since Strings are immutable.
     */
    public String() {
        this.value = new char[0];
    }

    /**
     * Allocates a new {@code String} so that it represents the sequence of
     * characters currently contained in the character array argument. The
     * contents of the character array are copied; subsequent modification of
     * the character array does not affect the newly created string.
     *
     * @param  value
     *         The initial value of the string
     */
    public String(char value[]) {
        this.value = Arrays.copyOf(value, value.length);
    }
    
    ...
}

We can first observe how the text itself is saved - in a char array. That being said, it's logical for us to be able to form a String from an array of characters.

A really important thing to note here is the fact that String is defined as final. This means that String is immutable.

What does this mean?

String str1 = "Hello World!";
str1.substring(1,4).concat("abc").toLowerCase().trim().replace('a', 'b');
System.out.println(str1);

Output:

Hello World!

Since String is final, none of these methods really changed it. They merely returned the changed state which we didn't use or assign anywhere. Each time a method on a String is called, a new String gets created, the state is changed and it's returned.

Again, taking a look at the source code:

public String concat(String str) {
    int otherLen = str.length();
    if (otherLen == 0) {
        return this;
    }
    int len = value.length;
    char buf[] = Arrays.copyOf(value, len + otherLen);
    str.getChars(buf, len);
    return new String(buf, true);
}

The original str is never changed. Its value is copied and the text we concatenate is added to it, after which a new String is returned.

If we did something like this:

String str1 = "Hello World!";
String str2 = str1.substring(1,4).concat("abc").toLowerCase().trim().replace('a', 'b');
System.out.println(str2);

Then we'd be greeted with the output:

ellbbc

Now, let's take a look at these two Strings:

String str1 = "qwerty";
String str2 = "qwerty";

When we instantiate a String like this, the value, in this case qwerty is saved in the Java Heap Memory - which is used for dynamic memory allocation for all Java objects.

While we have two different reference variables in this example, they're both referring to only one memory location in Java Heap Memory. While it may seem that there are two different String objects, in reality there is only one - str2 never gets instantiated as an object, but rather is assigned the object in memory that corresponds to str1.

This happens due to the way Java was optimized for Strings. Each time you wish to instantiate a String object like this, the value you want to add to the Heap Memory is compared to the previously added values. If an equal value already exists, the object isn't initialized and the value is assigned to the reference variable.

These values are saved in the so-called String Pool, which contains all literal String values. There is a way to bypass this though - by using the new keyword.

Let's take a look at another example:

String str1 = "qwerty";
String str2 = "qwerty";
String str3 = new String("qwerty");

System.out.println(str1 == str2);
System.out.println(str1 == str3);
System.out.println(str1.equals(str2));
System.out.println(str1.equals(str3));

Output:

true
false
true
true

This is logical, as str1 and str2 point to the same object in memory. str3 is instantiated explicitly as new so a new object is created for it, even though the String literal already exists in the pool. The equals() method compares their values, not the objects to which they point, which is the reason it returns true for all of these Strings.

It's important to note that substring() and concat() methods return a new String object and save it in the String pool.

This is a very small piece of code, but if we consider some big projects using hundreds of String variables and thousands of operations like substring() or concat(), it can cause serious memory leaks and time delays. That's exactly why we want to use StringBuffer or StringBuilder.

StringBuffer and StringBuilder

Mutability

StringBuffer and StringBuilder objects basically hold the same value as a String object - a sequence of characters. Both StringBuffer and StringBuilder are also mutable which means that once we assign a value to them, that value is processed as an attribute of a StringBuffer or StringBuilder object.

No matter how many times we modify their value, as a result a new String, StringBuffer, or StringBuilder object will not be created. This approach is much more time efficient and less resource consuming.

StringBuilder vs StringBuffer

These two classes are almost identical to one another - they use methods with the same names which return the same results. Although there are two major differences between them:

  • Thread Safety: StringBuffer methods are synchronized, which means that only one thread can call the methods of a StringBuffer instance at a time. On the other hand StringBuilder methods are not synchronized, therefore multiple threads can call the methods in StringBuilder class without being blocked.

    So we have come to a conclusion that StringBuffer is a thread-safe class while StringBuffer isn't.

    Is that something you should worry about? Maybe. If you are working on application which uses multiple threads it can be potentially dangerous to work with StringBuilder.

  • Speed: StringBuffer is actually two to three times slower than StringBuilder. The reason behind this is StringBuffer synchronization - only allowing 1 thread to execute on an object at a time results in much slower code execution.

Methods

Both StringBuffer and StringBuilder have the same methods (besides synchronized method declaration in the StringBuilder class). Let's go through some of the most common ones:

  • append()
  • insert()
  • replace()
  • delete()
  • reverse()

As you can see the each method name pretty much describes what it does. Here is a simple demonstration:

StringBuffer sb1 = new StringBuffer("Buffer no 1");
System.out.println(sb1);
        
sb1.append(" - and this is appended!");
System.out.println(sb1);
sb1.insert(11, ", this is inserted"); 
System.out.println(sb1);
sb1.replace(7, 9, "Number"); 
System.out.println(sb1);
sb1.delete(7, 14);
System.out.println(sb1);
sb1.reverse();
System.out.println(sb1);

Output:

Buffer no 1
Buffer no 1 - and this is appended!
Buffer no 1, this is inserted - and this is appended!
Buffer Number 1, this is inserted - and this is appended!
Buffer 1, this is inserted - and this is appended!
!dedneppa si siht dna - detresni si siht ,1 reffuB

String vs StringBuilder vs StringBuffer

String StringBuffer StringBuilder
Mutable No Yes Yes
Thread-Safe Yes Yes No
Time Efficient No No Yes
Memory Efficient No Yes Yes

Note: As we can see from the table above, String is both less efficient in time and memory, but that doesn't mean we should never use it again.

In fact, String can be very handy to use because it can be written fast and if you ever develop an application which stores Strings that are not going to be manipulated/changed later, it's absolutely fine to use String.

Code Example

In order to show how much efficient String, StringBuffer, and StringBuilder are we are going to perform a benchmark test:

String concatString = "concatString";
StringBuffer appendBuffer = new StringBuffer("appendBuffer");
StringBuilder appendBuilder = new StringBuilder("appendBuilder");
long timerStarted;

timerStarted = System.currentTimeMillis();
for (int i = 0; i < 50000; i++) {
    concatString += " another string";
}
System.out.println("Time needed for 50000 String concatenations: " + (System.currentTimeMillis() - timerStarted) + "ms");

timerStarted = System.currentTimeMillis();
for (int i = 0; i < 50000; i++) {
    appendBuffer.append(" another string");
}
System.out.println("Time needed for 50000 StringBuffer appends: " + (System.currentTimeMillis() - timerStarted) + "ms");
        
timerStarted = System.currentTimeMillis();
for (int i = 0; i < 50000; i++) {
    appendBuilder.append(" another string");
}
System.out.println("Time needed for 50000 StringBuilder appends: " + (System.currentTimeMillis() - timerStarted) + "ms");

Output:

Time needed for 50000 String concatenations: 18108ms
Time needed for 50000 StringBuffer appends: 7ms
Time needed for 50000 StringBuilder appends: 3ms

This output may vary depending on your Java Virtual Machine. So from this benchmark test we can see that StringBuilder is the fastest in string manipulation. Next is StringBuffer, which is between two and three times slower than StringBuilder. And finally we have String which is by far the slowest in string manipulation.

Using StringBuilder resulted in a time ~6000 times faster than regular String's. What it would take StringBuilder to concatenate in 1 second would take String 1.6 hours (if we could concatenate that much).

Conclusion

We have seen the performance of Strings, StringBuffers, and StringBuilders as well as their pros and cons. Now, the final question arises:

Which one is the winner?

Well the perfect answer on this question is "It depends". We know that Strings are easy to type, easy to use, and are thread-safe. On the other hand they are immutable (which means more memory consumption) and very slow when doing string manipulation.

StringBuffers are mutable, memory efficient, and thread-safe. Their downfall is the speed when compared to much faster StringBuilders.

As for StringBuilders, they are also mutable and memory efficient, they are the fastest in string manipulation, but unfortunately they are not thread-safe.

If you take these facts into the consideration, you will always make the right choice!

Author image