StringBuffer is dead, long live StringBuffer

Overview

StringBuilder was introduced seven years ago as a replacement for StringBuffer where you didn't need thread safety.

From the Javadoc for StringBuilder
This class provides an API compatible with StringBuffer, but with no guarantee of synchronization. This class is designed for use as a drop-in replacement for StringBuffer in places where the string buffer was being used by a single thread (as is generally the case). Where possible, it is recommended that this class be used in preference to StringBuffer as it will be faster under most implementations.

StringBuffer is dead?

So you might believe that StringBuffer is basically dead because it has very few uses which cannot be replaced by StringBuilder and those are neatly wrapped by classes like StringWriter. However, if the JDK is anything to go by, having a drop in replacement is just not enough to get people to migrate existing code.

ClassUses in the Java 6 update 25 src.zip
StringBuffer   1,409
StringBuilder      311

Sun/Oracle can't force other developers to migrate their code, but they could at least update their own code, with their own drop-in replacement, over a seven year period.

Some places StringBuffer is used as a local variable


These are methods which could be called many times.

  • BufferedReader.readLine()
  • RandomAccessFile.readLine()
  • Double.toHexString(double)
  • URLDecoder.decode() and URLEncoder.encode()
  • DecimalFormat (internal private methods)
  • Currency.getInstance(Locale)
  • Proeprties (internal private method)
  • SimpleFormatter, XMLFormatter.format(LogRecord)
Uses in toString() for common classes
  • Constructor, Field, Method, Modifier
  • ByteBuffer, DoubleBuffer, FloatBuffer, IntBuffer, LongBuffer, ShortBuffer.
Uses in classes which are not themselves thread safe, for an added touch of irony.
  • SimpleDateFormat.formatToCharacterIterator(Object)
  • Attribute.write(OutputStream)
  • Matcher, five methods
  • Manifest.write(OutputStream) 

Escape Analysis

Something which is supposed to make the question less important is Escape Analysis which promises to identify local variables and turn off synchronisation when it is not required. This promise may have stalled migration of StringBuffer to StringBuilder.

So does it work?
public static void main(String... args) {
    String text = "A short piece of text for copying";
    int runs = 1000000;

    for (int i = 0; i < 7; i++) {
        {
            long start = System.nanoTime();
            StringBuffer sb = new StringBuffer(text);
            for (int r = 0; r < runs; r++)
                copyStringBuffer(sb);
            long time = System.nanoTime() - start;
            System.out.printf("StringBuffer took an average of %,d ns%n", time/runs);
        }
        {
            long start = System.nanoTime();
            StringBuilder sb = new StringBuilder(text);
            for (int r = 0; r < runs; r++)
                copyStringBuilder(sb);
            long time = System.nanoTime() - start;
            System.out.printf("StringBuilder took an average of %,d ns%n", time/runs);
        }
    }
}

public static String copyStringBuffer(StringBuffer text) {
    StringBuffer sb = new StringBuffer();
    for (int i = 0; i < text.length(); i++)
        sb.append(text.charAt(i));
    return sb.toString();
}

public static String copyStringBuilder(StringBuilder text) {
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < text.length(); i++)
        sb.append(text.charAt(i));
    return sb.toString();
}
prints the following when the -XX:+DoEscapeAnalysis flag is used.
StringBuffer took an average of 1,723 ns
StringBuilder took an average of 189 ns
StringBuffer took an average of 1,709 ns
StringBuilder took an average of 176 ns
StringBuffer took an average of 1,708 ns
StringBuilder took an average of 175 ns
StringBuffer took an average of 323 ns
StringBuilder took an average of 176 ns
StringBuffer took an average of 324 ns
StringBuilder took an average of 175 ns
StringBuffer took an average of 324 ns
StringBuilder took an average of 174 ns
StringBuffer took an average of 327 ns
StringBuilder took an average of 174 ns
In this example, the StringBuffer is almost as fast as StringBuilder if called enough, but it still takes almost twice as long. For this example at least, a simple change from StringBuffer to StringBuilder is the simplest way to improve performance.

Conclusion

When you have an improvement, you cannot assume that it will be adopted over time. A less passive and more deliberate may be what is required.

Escape Analysis may one day avoid the need to switch from StringBuffer to StringBuilder, but in the meantime a simple change to the source may still be required.

Comments

Popular posts from this blog

Java is Very Fast, If You Don’t Create Many Objects

Low Latency Microservices, A Retrospective

Unusual Java: StackTrace Extends Throwable