String、StringBuffer和StringBuilder的联系与区别

1、String

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

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

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

 

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对象,这样对性能会有影响。例如:

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

如果用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();

因此在JDK1.5以前,Java编译器都是把最开始的用"+"追加内容到字符串的代码编译成用StringBuffer的代码。

 

3、StringBuilder

那从JDK1.5开始又如何呢?从JDK1.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这些方法的定义处加了synchronized关键字:

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

posted @ 2018-03-14 09:55  Firas  阅读(230)  评论(0编辑  收藏  举报