写在前面
对于Java字符串的拼接有一条规则如下:
【推荐】循环体内,字符串的连接方式,使用 StringBuilder 的 append 方法进行扩展。
说明:下例中,反编译出的字节码文件显示每次循环都会 new 出一个 StringBuilder 对象,然后进行 append 操作,最后通过 toString 方法返回 String 对象,造成内存资源浪费。
反例:
String str = "start";
for (int i = 0; i < 100; i++) {
str = str + "hello";
}
关于String、StringBuilder、StringBuffer这三个类在字符串处理中的地位不言而喻,我们用的最多的就是String 的“+”号操作符(最普遍)以及StringBuilder、StringBuffer的append()方法。
那么他们到底有什么优缺点,到底什么时候该用谁?如何才能保证字符串拼接的高效率呢?下面我们一起来了解一下。
目录
一、有什么区别
“+”号操作的字节码
“+”号操作符必须是字符串拼接最常用的一种了,没有之一。使用“+”拼接字符串,其实只是Java提供的一个语法糖。那么,我们就来解一解这个语法糖,看看他的内部原理到底是如何实现的。
还是这样一段代码。我们把他生成的字节码进行反编译,看看结果。
String str1= "唐伯虎";
String str2= "点香烟";
String endStr = str1+ "," + str2;
Dos反编译后的内容如下(反编译class文件的命令:javap -c 类名)。
String str1= "\\u5510\\u4f2f\\u864e"; //唐伯虎
String str2= "\\u70b9\\u9999\\u70df"; //点香烟
String endStr = (new StringBuilder()).append(str1).append(",").append(str2).toString();
通过查看反编译以后的代码,我们可以发现,原来字符串常量在拼接过程中,是将String转成了StringBuilder后,使用其append方法进行处理的。
那么也就是说,Java中的“+”对字符串的拼接,其实现原理是使用StringBuilder.append()方法。
语法糖:语法糖(Syntactic sugar),也译为糖衣语法,是由英国计算机科学家彼得·兰丁发明的一个术语,指计算机语言中添加的某种语法,这种语法对语言的功能没有影响,但是更方便程序员使用。语法糖让程序更加简洁,有更高的可读性。
二、性能比较
1. 三者在执行速度方面的比较:
2. String <(StringBuffer,StringBuilder)的原因
- String:字符串常量
- StringBuffer:字符串变量(无同步锁)
- StringBuilder:字符串变量(有同步锁)
从上面的名字可以看到,String是"字符串常量",也就是不可改变的对象。源码如下
public final class String{}
对于上面这句话的理解你可能会产生这样一个疑问 ,比如这段代码:
String str = "唐伯虎";
str = str + "点香烟";
System.out.print(str); // result : "唐伯虎点香烟"
我们明明改变了String型的变量str啊,为什么说是没有改变呢?我们来看一下这张对String操作时内存变化的图:
我们可以看到,初始String值为"唐伯虎",然后在这个字符串后面加上新的字符串"点香烟",这个过程是需要重新在栈堆内存中开辟内存空间的,最终得到了"唐伯虎点香烟"字符串也相应的需要开辟内存空间,这样短短的两个字符串,却需要开辟三次内存空间,不得不说这是对内存空间的极大浪费。
为了应对经常性操作字符串的场景,Java提供了其他两个操作字符串的类 —— StringBuffer、StringBuilder。
他们俩均属于字符串变量,是可改变的对象,每当我们用它们对字符串做操作时,实际上是在一个对象上操作的,这样就不会像String一样创建一些而外的对象进行操作了,速度自然就相对快了。
我们一般在StringBuffer、StringBuild类上的主要操作是 append 和 insert 方法,这些方法允许被重载,以接受任意类型的数据。每个方法都能有效地将给定的数据转换成字符串,然后将该字符串的字符追加或插入到字符串缓冲区中。append 方法始终将这些字符添加到缓冲区的末端;而 insert 方法则在指定的点添加字符。
1. StringBuilder一个可变的字符序列是JDK1.5新增的。此类提供一个与 StringBuffer 兼容的 API,但不保证同步。该类被设计用作 StringBuffer 的一个简易替换,用在字符串缓冲区被单个线程使用的时候(这种情况很普遍)。
2. 如果可能,建议优先采用StringBuilder类,因为在大多数实现中,它比 StringBuffer 要快。且两者的方法基本相同。然而在应用程序要求线程安全的情况下,则必须使用 StringBuffer 类。
String 类型和 StringBuffer、 StringBuild类型的主要性能区别其实在于 String 是不可变的对象(final), 因此在每次对 String 类型进行改变的时候其实都等同于在堆中生成了一个新的 String 对象,然后将指针指向新的 String 对象,这样不仅效率低下,而且大量浪费有限的内存空间,所以经常改变内容的字符串最好不要用 String 。因为每次生成对象都会对系统性能产生影响,特别是当内存中的无引用对象过多了以后, JVM 的 GC 开始工作,那速度是一定会相当慢的。另外当GC清理速度跟不上new String的速度时,还会导致内存溢出Error,会直接kill掉主程序!报错如下:
Caused by: java.lang.OutOfMemoryError: GC overhead limit exceeded
Exception in thread "I/O dispatcher 3797236" java.lang.OutOfMemoryError: GC overhead limit exceeded
一个特殊的例子:
String str = "This is only a" + " simple" + " test";
StringBuffer builder = new StringBuilder("This is only a").append(" simple").append(" test");
在上述例子中,经测试发现,生成str对象的速度远高于builder,而这个时候StringBuffer居然速度上根本一点都不占优势。为什么呢?
其实这是JVM的一个把戏,实际上:
String str = "This is" + " a " + "demo"; 等同于 String str = "This is a demo";
示例:
public class demo {
public static void main(String[] args) {
String str = "This is" + " a " + "demo";
System.out.println(str);
}
}
dos反编译后的内容如下(反编译class文件的命令:javap -c demo)
可见,在JVM优化时,如果是多个固定字符串拼接,会将这些固定字符串进行预处理,当成一个整体的字符串,相当于仅声明一个常量,所以并不需要太多的时间。
但大家这里要注意的是,如果字符串拼接的多个元素中有其他String对象的话,速度就没那么快了,譬如:
String str2 = "陈哈哈";
String str3 = "不吃";
String str4 = "香菜";
String str1 = str2 +str3 + str4; // “陈哈哈不吃香菜”
或者:
String str5 = "陈哈哈";
String str6 = str5 + "不吃香菜";
这时候JVM会规规矩矩的按照原来的方式去做。
3. StringBuffer与StringBuilder的线程安全问题
StringBuffer和StringBuilder可以算是双胞胎了,这两者的方法没有很大区别。但在线程安全性方面,StringBuffer允许多线程进行字符操作。这是因为在源代码中StringBuffer的很多方法都被关键字synchronized 修饰了,而StringBuilder没有。
有多线程编程经验的程序员应该知道synchronized。这个关键字是为线程同步机制 设定的。我简要阐述一下synchronized的含义:
每一个类对象都对应一把锁,当某个线程A调用类对象O中的synchronized方法M时,必须获得对象O的锁才能够执行M方法,否则线程A阻塞。一旦线程A开始执行M方法,将独占对象O的锁。使得其它需要调用O对象的M方法的线程阻塞。只有线程A执行完毕,释放锁后。那些阻塞线程才有机会重新调用M方法。这就是解决线程同步问题的锁机制。
了解了synchronized的含义以后,大家可能都会有这个感觉。多线程编程中StringBuffer比StringBuilder要安全多了 ,事实确实如此。如果有多个线程需要对同一个字符串缓冲区进行操作的时候,StringBuffer应该是不二选择。
注意:是不是String也不安全呢?事实上不存在这个问题,String是不可变的。线程对于堆中指定的一个String对象只能读取,无法修改。试问:还有什么不安全的呢?
三、使用总结:
1. 如果不是在循环体中进行字符串拼接的话,直接使用 String 的 “+” 就好了。
2. 单线程循环中操作大量字符串数据 → StringBuilder.append()
3. 多线程循环中操作大量字符串数据 → StringBuffer.append()
其实拼接字符串的方式还有很多种,包括String.concat()、String.join("", str1, str2)、StringUtils.join(str1, str2) 等,但在我们日常开发中最常用的就是 String 的 “+” 和 StringXXX.append()方法啦,只要掌握好这三种方式的使用场景,就基本能保证代码的高可用性了。好了,这篇文章就到这里,希望能够对你有帮助!