String Concatenation vs. StringBuilder vs. String.format Performance in Java
I often times find myself looking for the best way to combine a constant string to a variable value in Java. For example, let’s say you’re developing an Android application where user’s profile pictures are stored in a location like so:
https://cdn.myapp.com/user/<UserId>.png
The <UserId> token is to be replaced by the ID of the user who’s profile picture you’re loading, but the remainder of the URL is constant - it never changes from user to user.
There’s three common methods of creating a String with the appropriate user ID in place, namely direct string concatenation, using a StringBuilder, or using String.format. I personally find using the String.format method to be the cleanest and tend to use it when applicable, but I recently began wondering how it actually performs against the other two methods.
First off, let’s take a look at the three methods in action:
String Concatenation
private static final String PROFILE_PICTURE_URL_BASE = "https://cdn.myapp.com/user/"; private static final String PROFILE_PICTURE_URL_EXTENSION = ".png"; Long userId = ...; String profilePictureUrl = PROFILE_PICTURE_URL_BASE + userId + PROFILE_PICTURE_URL_EXTENSION;
StringBuilder
private static final String PROFILE_PICTURE_URL_BASE = "https://cdn.myapp.com/user/"; private static final String PROFILE_PICTURE_URL_EXTENSION = ".png"; Long userId = ...; StringBuilder sb = new StringBuilder(PROFILE_PICTURE_URL_BASE); sb.append(userId); sb.append(PROFILE_PICTURE_URL_EXTENSION); String profilePictureUrl = sb.toString();
String.format
private static final String PROFILE_PICTURE_URL = "https://cdn.myapp.com/user/%d.png"; Long userId = ...; String profilePictureUrl = String.format(PROFILE_PICTURE_URL, userId);
Again, I do find that the String.format looks the cleanest, and it’s nice having to only have one constant as opposed to the two required in the other cases.
But it’s time to see how they actually compare in terms of performance. For the sake of testing this, here’s a look at the sample program we’re going to use to measure the time it takes for each method:
public class TestStrings { private static final String PROFILE_PICTURE_URL_BASE = "https://cdn.myapp.com/user/"; private static final String PROFILE_PICTURE_URL_EXTENSION = ".png"; private static final String PROFILE_PICTURE_URL = "https://cdn.myapp.com/user/%d.png"; private static final int NUM_ITERATIONS = 1000000; public static void main(String[] args) { int userId; long startTime; // String Concatentation startTime = System.currentTimeMillis(); for (userId = 0; userId < NUM_ITERATIONS; userId ++) { String profilePictureUrl = PROFILE_PICTURE_URL_BASE + userId + PROFILE_PICTURE_URL_EXTENSION; } System.out.println("String Concatentation: " + (System.currentTimeMillis() - startTime) + "ms"); // StringBuilder startTime = System.currentTimeMillis(); for (userId = 0; userId < NUM_ITERATIONS; userId ++) { StringBuilder sb = new StringBuilder(PROFILE_PICTURE_URL_BASE); sb.append(userId); sb.append(PROFILE_PICTURE_URL_EXTENSION); String profilePictureUrl = sb.toString(); } System.out.println("StringBuilder: " + (System.currentTimeMillis() - startTime) + "ms"); // String.format startTime = System.currentTimeMillis(); for (userId = 0; userId < NUM_ITERATIONS; userId ++) { String profilePictureUrl = String.format(PROFILE_PICTURE_URL, userId); } System.out.println("String.format: " + (System.currentTimeMillis() - startTime) + "ms"); } }
As you can see we run each method one million times (NUM_ITERATIONS) and compare the end time to the start time for each. And here’s the results:
$ javac TestStrings.java && java TestStrings String Concatenation: 76ms StringBuilder: 65ms String.format: 1410ms
String concatenation, which we’re told to never use due to the immutable nature of strings in Java, is only beat by 11 milliseconds by the StringBuilder, which actually makes sense because the Java compiler generally converts instances of string concatenation to StringBuilder under the hood when it can. Although we shouldn’t trust it to do this for us all the time, it seems like it did in this case. We can see this by using the following command and analyzing the compiled class:
$ javap -c TestStrings.class
The only difference is that the initialization of the StringBuilder in the compiled string concatenation case doesn’t use the first constant (PROFILE_PICTURE_URL_BASE) as an argument to the constructor, essentially ending up like this:
StringBuilder sb = new StringBuilder(); sb.append(PROFILE_PICTURE_URL_BASE); sb.append(userId); sb.append(PROFILE_PICTURE_URL_EXTENSION);
Instead of:
StringBuilder sb = new StringBuilder(PROFILE_PICTURE_URL_BASE); sb.append(userId); sb.append(PROFILE_PICTURE_URL_EXTENSION);
So using these results we can further see that initializing the StringBuilder with the first value actually makes it perform better than using the empty constructor and passing the first string in through append().
And finally, as you can see, String.format is beat by a massive margin by the other two. This is unfortunate for situations where performance is critical, but by and large won’t affect my use of String.format for these cases because I find the simplicity and readability of the code to be more important than fine tuning performance in cases where it isn’t required.
However, that being said, if you’re in a situation where the code is going to be called thousands or more times, it is usually worthwhile to look into measuring the performance of seemingly simple tasks such as these to see how big a difference there can be.