StringBuffer
在实际开发中使用String类会存在一个问题,String对象一旦创建,其值是不能修改的,如果要修改,会重新开辟内存空间来存储修改之后的对象,即修改了String的引用。因为String的底层是用数组来存值的,数组长度不可改变这一特性导致了上述问题,所以如果开发中需要对某个字符串进行频繁的修改,使用String就不合适了,会造成内存空间的浪费,如何解决这个问题呢?
可以使用StringBuffer类来解决,当对字符串对象进行频繁修改时,使用StringBuffer可以极大提升程序的效率,我们通过下面这个例子一测便知。
分别定义String和StringBuffer类型的字符串对象,对它们进行值的累加操作,循环执行50000次,然后统计各自的耗时,代码如下所示。
//StringlongstartTime=();Stringstr="";for(inti=0;i50000;i++){str+=i;}longTime=();("String类型操作耗时"+(Time-startTime)+"毫秒");//StringBufferlongstartTime=();StringBufferstr=newStringBuffer();for(inti=0;i50000;i++){(i);}longTime=();("StringBuffer类型操作耗时"+(Time-startTime)+"毫秒");运行结果分别如下图所示。
可以看到String类型耗时1345毫秒,StringBuffer类型耗时只有7毫秒,速度提升了近200倍。
接下来我们就来详细学习StringBuffer类。
StringBuffer和String类似,底层也是用一个数组来存储字符串的值,并且数组的默认长度为16,即一个空的StringBuffer对象,数组长度为16,如下图所示。
实例化一个StringBuffer对象即创建了一个大小为16个字符的字符串缓冲区。
当我们调用有参构造创建一个StringBuffer对象时,数组长度就不是16了,而是根据当前对象的值来决定数组的长度,“值的长度+16”作为数组的长度,如下图所示。
创建了一个字符串缓冲区,该缓冲区初始值为指定的字符串。字符串缓冲区的初始容量为字符串参数的长度+16。
我们可以看到带参构造中依次执行了两步操作:super(()+16)、app(str),这也就很清楚的说明了StringBuffer的创建过程,先创建一个长度为"str长度+16"的字符串缓冲区,然后把str的值追加到此字符串序列中。
所以一个StringBuffer创建完成之后,有16个字符的空间可以对其值进行修改。如果修改的值范围超出了16个字符,则调用ensureCapacityInternal()方法检查StringBuffer对象的原char数组的容量能不能装下新的字符串,如果装不下则对char数组进行扩容。
扩容的逻辑就是创建一个新的char数组,newCapacity()方法用于确定新容量大小,将现有容量大小扩大一倍再加上2,如果还是不够大则直接等于需要的容量大小。
扩容完成之后,再调用()方法完成数据拷贝,底层调用()方法将原数组的内容复制到新数组,最后将指针指向新的char数组。
StringBuffer常用方法
具体代码如下所示。
StringBufferstringBuffer=newStringBuffer();("StringBuffer:"+stringBuffer);("StringBuffer的长度:"+());stringBuffer=newStringBuffer("HelloWorld");("StringBuffer:"+stringBuffer);("下标为2的字符是:"+(2));stringBuffer=("Java");("app之后的StringBuffer:"+stringBuffer);stringBuffer=(3,6);("delete之后的StringBuffer:"+stringBuffer);stringBuffer=(3);("deleteCharAt之后的StringBuffer:"+stringBuffer);stringBuffer=(2,3,"StringBuffer");("replace之后的StringBuffer:"+stringBuffer);Stringstr=(2);("substring之后的String:"+str);str=(2,8);("substring之后的String:"+str);stringBuffer=(6,"six");("insert之后的StringBuffer:"+stringBuffer);("e的下标是:"+("e"));("下标6之后的e的下标是:"+("e",6));stringBuffer=();("reverse之后的StringBuffer:"+stringBuffer);str=();("StringBuffer对应的String:"+str);运行结果如下图所示。
StringBuilder
StringBuilder和StringBuffer是一对兄弟,因为它们拥有同一个父类AbstractStringBuilder,同时实现的接口也是完全一样,都实现了,CharSequence两个接口,如下图所示。
那它们有什么区别呢?最大的区别在于StringBuffer对几乎所有的方法都实现了同步,StringBuilder没有实现同步,如同样是对AbstractStringBuilder方法app的重写,StringBuffer添加了synchronized关键字修饰,而StringBuilder没有,如下图所示。
所以StringBuffer是线程安全的,在多线程系统中可以保证数据同步,而StringBuilder无法保证线程安全,所以多线程系统中不能使用StringBuilder。
但是方法同步需要消耗一定的系统资源,所以StringBuffer虽然安全,但是效率不如StringBuilder,也就是说使用StringBuilder更快,我们还是用上面的例子做一个测试。
分别定义StringBuffer和StringBuilder类型的字符串对象,对它们进行值的累加操作,循环执行500000次,然后统计各自的耗时,代码如下所示。
//StringBufferlongstartTime=();StringBufferstr=newStringBuffer();for(inti=0;i500000;i++){(i);}longTime=();("StringBuffer类型操作耗时"+(Time-startTime)+"毫秒");//StringBuilderlongstartTime=();StringBuilderstr=newStringBuilder();for(inti=0;i500000;i++){(i);}longTime=();("StringBuilder类型操作耗时"+(Time-startTime)+"毫秒");运行结果分别如下图所示。
通过结果可以看到,同样是执行50万次操作,StringBuffer耗时45毫秒,而StringBuilder耗时34毫秒,相差虽然不是很大,但是StringBuilder效率确实要高于StringBuffer,但是安全性不如StringBuffer。
所以,在需要考虑线程安全的场景下我们可以使用StringBuffer,不需要考虑线程安全,追求效率的场景下可以使用StringBuilder。
StringBuilder的具体使用如下所示。
StringBuilderstringBuilder=newStringBuilder();("StringBuilder:"+stringBuilder);("StringBuilder的长度:"+());stringBuilder=newStringBuilder("HelloWorld");("StringBuilder:"+stringBuilder);("下标为2的字符是:"+(2));stringBuilder=("Java");("app之后的StringBuilder:"+stringBuilder);stringBuilder=(3,6);("delete之后的StringBuilder:"+stringBuilder);stringBuilder=(3);("deleteCharAt之后的StringBuilder:"+stringBuilder);stringBuilder=(2,3,"StringBuilder");("replace之后的StringBuilder:"+stringBuilder);Stringstr=(2);("substring之后的String:"+str);str=(2,8);("substring之后的String:"+str);stringBuilder=(6,"six");("insert之后的StringBuilder:"+stringBuilder);("e的下标是:"+("e"));("下标6之后的e的下标是:"+("e",6));stringBuilder=();("reverse之后的StringBuilder:"+stringBuilder);str=();("StringBuilder对应的String:"+str);运行结果如下图所示。
StringBuilder为什么线程不安全?
我们通过一个例子来测试,代码如下所示。
StringBuilderstringBuilder=newStringBuilder();for(inti=0;i10;i++){newThread(newRunnable(){@Overridepublicvoidrun(){for(intj=0;j1000;j++){("a");}}}).start();}try{(100);(());}catch(InterruptedExceptione){();}开启10个线程,每个现象对stringBuilder添加1000个'a',操作完成之后,stringBuilder的长度应该是10*1000=10000,但是我们看到多次运行的结果如下。
长度比10000小(也有可能等于10000,概率较小),同时也可能会抛出数组下标越界的异常,证明StringBuilder确实是线程不安全的,为什么是这样呢?我们查看源码来分析。
StringBuilder的app()方法底层调用AbstractStringBuilder的app()方法,如下所示。
count为字符串长度,len为追加的字符串长度,count+=len这行代码如果是多线程同时访问,很可能会出现数据错误,比如count=0,len=1,两个线程同时执行到这一行,获取的count都是0,执行的结果都是1,所以最终count的值为1,而不是2,这就解释了为什么最终的长度有可能比预期结果小的原因。
再来说说为什么会抛出数组下标越界异常?
字符的添加是通过调用putStringAt(count,str)方法完成的,count为当前字符串的长度,通过ensureCapacityinternal(count+len)方法对数组进行扩容之后,它一定是小于等于数组最大容量的,putStringAt(count,str)方法中每添加一个字符,都会给count加1,当到达数组长度上限之后再进行扩容。
但是如果是两个线程同时执行putStringAt(count,str),假设此时的count=3,数组容量为4,两个线程拿到的count都为3,数组容量大于count,所以并不会进行扩容,这就意味着只剩一个空间,要插入两个字符,线程A执行完毕,count变为4,已经占满了整个数组,所以线程B执行的时候,超出了数组的长度,抛出异常。
高频面试题
1、StringBuilder的效率一定比String更高吗?
我们通常会说StringBuilder效率要比String高,严谨一点这句话不完全对,虽然大部分情况下使用StringBuilder效率更高,但在某些特定情况下不一定是这样,比如下面这段代码:
Stringstr="Hello"+"World";StringBuilderstringBuilder=newStringBuilder("Hello");("World");此时,使用String创建"HelloWorld"的效率要高于使用StringBuilder创建"HelloWorld",这是为什么呢?
因为String对象的直接相加,JVM会自动对其进行优化,也就是说"Hello"+"World"在编译期间会自动优化为"HelloWorld",直接一次性创建完成,所以效率肯定要高于StringBuffer的app拼接。
但是需要注意的是如果是这样的代码:
Stringstr1="Hello";Stringstr2="World";Stringstr3=str1+str2;
对于这种间接相加的操作,效率要比直接相加低,因为在编译器不会对引用变量进行优化。
2、下面代码的运行结果是?
Stringstr1="HelloWorld";Stringstr2="Hello"+"World";(str1==str2);
true,因为"Hello"+"World"在编译期间会被JVM自动优化成"HelloWorld",是一个字符串常量,所以和str1引用相同。
3、下面代码的运行结果是?
Stringstr1="HelloWorld";Stringstr2="Hello";Stringstr3=str2+"World";(str1==str3);
false,JVM只有在String对象直接拼接的时候才会进行优化,如果是对变量进行拼接则不会优化,所以str2+"World"并不会直接优化成字符串常量"HelloWorld",同时这种间接拼接的结果是存放在堆内存中的,所以str1和str3的引用肯定不同。
4、Stringstr=newString("HelloWorld")创建了几个对象?
这是很常见的一道面试题,大部分的答案都是2个,"HelloWorld"是一个,另一个是指向字符串的变量str,其实是不准确的。
因为代码的执行过程和类的加载过程是有区别的,如果只看运行期间,这段代码只创建了1个对象,new只调用了一次,即在堆上创建的"HelloWorld"对象。
而在类加载的过程中,创建了2个对象,一个是字符串字面量"HelloWorld"在字符串常量池中所对应的实例,另一个是通过newString("HelloWorld")在堆中创建并初始化,内容与"HelloWorld"相同的实例。
所以在回答这道题的时候,可以先问清楚面试官是在代码执行过程中,还是在类加载过程中。这道题目如果换做是Stringstr=newString("HelloWorld")涉及到几个对象,那么答案就是2个。
5、String、StringBuffer、StringBuilder有什么区别?
1、String一旦创建不可变,如果修改即创建新的对象,StringBuffer和StringBuilder可变,修改之后引用不变。
2、String对象直接拼接效率高,但是如果执行的是间接拼接,效率很低,而StringBuffer和StringBuilder的效率更高,同时StringBuilder的效率高于StringBuffer。
3、StringBuffer的方法是线程安全的,StringBuilder是线程不安全的,在考虑线程安全的情况下,应该使用StringBuffer。
6、下面代码的运行结果是?
publicstaticvoidmain(String[]args){Stringstr="Hello";test(str);(str);}publicstaticvoidtest(Stringstr){str+="World";}Hello,因为String是不可变的,传入test方法的参数相当于str的一个副本,所以方法内只是修改了副本,str本身的值没有发生变化。
7、下面代码的运行结果是?
publicstaticvoidmain(String[]args){StringBufferstr=newStringBuffer("Hello");test(str);(str);}publicstaticvoidtest(StringBufferstr){("World");}HelloWorld,因为StringBuffer是可变类型,传入test方法的参数就是str的引用,所以方法内修改的就是str本身。





