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.
Check out our hands-on, practical guide to learning Git, with best-practices, industry-accepted standards, and included cheat sheet. Stop Googling Git commands and actually learn it!
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 aStringBuffer
instance at a time. On the other handStringBuilder
methods are not synchronized, therefore multiple threads can call the methods inStringBuilder
class without being blocked.So we have come to a conclusion that
StringBuffer
is a thread-safe class whileStringBuffer
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 thanStringBuilder
. The reason behind this isStringBuffer
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 String
s, StringBuffer
s, and StringBuilder
s 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 String
s 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.
StringBuffer
s are mutable, memory efficient, and thread-safe. Their downfall is the speed when compared to much faster StringBuilder
s.
As for StringBuilder
s, 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!