String、StringBuffer 和 StringBuilder 是 java.lang 包里的 3 个与字符串密切相关的类,它们都实现了 CharSequence、Comparable 和 Serializable 接口。

在 OpenJRE 的实现中,它们都有一个 byte[] 类型的 value 变量用于存储字符串的编码后的值(encoded value),以及一个 byte 类型的 coder 变量用于指示 value 中存储字符用的是 Latin1 编码还是 UTF-16 编码。

String 是不可变的(immutable);而 StringBuffer 和 StringBuilder 都继承了抽象类 AbstractStringBuilder,都是可变的(mutable)。

StringBuffer 中的绝大多数所有公有的成员方法(public method)都是 synchronized 的,没加 synchronized 的公有方法也是对其他 synchronized 的方法的包装;而StringBuilder 中的方法都不是 synchronized 的。

下面进行更详细的介绍。

1、String

String 是 immutable 的,即除非利用反射强制修改它的值,否则一个 String 对象一旦被创建,其值就不会被修改。

Java 使 String 类为 immutable 的实现方式是:String 类是 final 的,不可以被继承,而且其成员属性(property)都是 private 的,也没有 public 的方法会对字符串的值(String 中的 value 属性进行修改)。

GrepCode 网站上的 OpenJDK 8 中 String 类的实现的截图:

1acc8ba86528b8fb8e3cd7aac5e3a38b45149907

String 的 replace、concat 和 substring 方法,都会返回一个新的 String 对象,而不是对原来的 String 对象进行修改。你可以用如下代码验证这一点:

String a = "aaa";
String b = a.replace("a", "b"); // 不会对a的值产生影响
System.out.println(a == b); // false,b是一个新创建的字符串对象
System.out.println(a); // 此时a还是"aaa"而不是"bbb"

String c = a.concat("cc");
System.out.println(c); // aaacc
System.out.println(a == c); // false,c是一个新创建的字符串对象
System.out.println(a); // 此时a还是"aaa"而不是"aaacc"

String d = a.substring(2);
System.out.println(d); // a
System.out.println(a == d); // false,d是一个新创建的字符串对象
System.out.println(a); // 此时a还是"aaa"而不是"a"

在需要频繁对字符串做修改操作的场景下(最常见的是在字符串后追加内容的场景),如果只用String类提供的方法(如concat方法),则每次字符串操作都会创建一个新的String对象,这样对性能会有影响。例如:

for (int i = 0; i < 1000; i += 1) {
    str = str + arr[i] + ',';
}

如果用String的concat方法来实现,则实际的实现代码会变成:

for (int i = 0; i < 1000; i += 1) {
    str = str.concat(arr[i]).concat(",");
}

这样在 1000 次循环里,每次都会新创建两个 String 对象,一共需要创建 2000 个 String 对象,这对系统性能会造成影响。

2、StringBuffer

为了应对频繁对字符串做修改操作的场景,Java 从 JDK1 开始就提供了 mutable 的 StringBuffer 类。StringBuffer 类对外暴露了可以修改其值的 append、insert、delete 等方法。一个 StringBuffer 对象在其缓冲区(一个字符数组 char[])的容量足够的情况下,调用这些方法可以直接修改 StringBuffer 的值而不必创建新的对象。(在一个StringBuffer  的缓冲区的容量不足的时候,调用其 append 或者 insert 就会使 StringBuffer 创建一个新的更大的缓冲区,这时则会创建一个新的字符数组 char[] 对象。)

因此,如果上一小节中循环追加内容到字符串的代码改用以下的实际实现,则创建的对象会少很多:

StringBuffer buffer = new StringBuffer(str);
for (int i = 0; i < 1000; i += 1) {
    buffer.append(arr[i]).append(',');
}
str = buffer.toString();

因此在 JDK 1.5 以前,Java 编译器都是把字符串 + 字符串那样的代码编译成使用 StringBuffer.append 方法,或者说字符串的加号运算符其实是 StringBuffer.append 的一个语法糖。

3、StringBuilder

那从 JDK 1.5 开始又如何呢?从 JDK 1.5 开始,Java 编译器改用了 StringBuilder。

StringBuilder 类是非线程安全的 StringBuffer 类。即:

StringBuffer buffer = new StringBuffer();
Thread a = new Thread(new Runnable() {
    public void run() {
        buffer.append("aaaaaaaa");
    }
});
Thread b = new Thread(new Runnable() {
    public void run() {
        buffer.append("bbbbbbbb");
    }
});
a.start(); b.start();
a.join(); b.join();
System.out.println(buffer.toString());
// 要不就是 aaaaaaaabbbbbbbb,要不就是 bbbbbbbbaaaaaaaa

StringBuilder builder = new StringBuilder();
a = new Thread(new Runnable() {
    public void run() {
        builder.append("aaaaaaaa");
    }
});
b = new Thread(new Runnable() {
    public void run() {
        builder.append("bbbbbbbb");
    }
});
a.start(); b.start();
a.join(); b.join();
System.out.println(builder.toString());
// 这时候情况就复杂多了,有可能 a 线程追加a的过程执行到一般被系统暂停,然后系统调度 b 线程执行,这样的话结果就可能是 aaaabbbbbbbbaaaa

为了实现线程安全,StringBuffer 在 insert、append、delete 这些 public 方法的定义处加了synchronized关键字:

ae39f839111946d405c8e88ef04185c60c223222

但是,实际应用时,对字符串追加内容的操作几乎都是在一个线程中进行的(例如最开始的循环向字符串追加内容的代码),这样如果用 StringBuffer 的话,就会额外有给 StringBuffer 对象加锁的开销。因此从 JDK 1.5 开始,编译器改为了使用 StringBuilder 来实现字符串 + 字符串的操作,即字符串的加号运算符的变成了 StringBuilder.append 的语法糖。