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来实现向字符串追加内容。