StringBuffer和StringBuilder的区别

2019-09-26   448 次阅读


StringBuffer

在实际的应用开发中,使用String类会存在一个问题,String对象一旦被创建,它的值是不能被修改的,如果要修改,则是重新开辟内存空间来存储修改之后的对象,即改变了String的引用。

因为String的底层是用Char数组来进行存值的(Char数组是Java8,Java9改为了byte数组),数组长度不可改变特性导致了需要字符串修改时需要创建一个新的对象的问题。

所以在开发过程中如果对某个字符串进行频繁的改动的话,使用String就不合适了。


//String
long startTime = System.currentTimeMillis();
String str = "";
for(int i = 0;i<50000;i++){
    str += i;
}
long endTime = System.currentTimeMillis();
System.out.println("String类型操作耗时"+(endTime-startTime)+"毫秒");
// String类型操作耗时1345毫秒


//StringBuffer
long startTime = System.currentTimeMillis();
StringBuffer str = new StringBuffer();
for(int i = 0;i<50000;i++){
  str.append(i);
}
long endTime = System.currentTimeMillis();
System.out.println("StringBuffer类型操作耗时"+(endTime-startTime)+"毫秒");
// StringBuffer类型操作耗时7毫秒
 

StringBufferString 类似,底层也是用一个数组来存储字符串的值,并且数组的默认查那个度为16,即一个空的StringBuffer对象,数组长度为16。

image.png

实例化一个StringBuffer对象即创建了一个大小为16个字符的字符串缓冲区。

当我们使用有参构造器来创建一个StringBuffer对象时,数组的长度就是值的长度+16来做为数组的长度。

image.png

我们可以看到有参构造器中依次执行了 super(str.length()+16) , aappend(str) ,这也就说明了StringBuffer在创建的时候,先创建了一个长度为str长度+16的数组,然后把str的值进行了追加。

所以一个StringBuffer创建完成之后,有16个字符空间供使用。如果修改的值超过了16个字符串,则调用 ensureCapacityInternal() 方法来检查StringBuffer 对象的原 char 数组能否装下新的字符串,如果装不下则对char数组进行扩容。

image.png

扩容的逻辑就是创建一个新的char数组,newCapacity()方法用来确认新的容量大小,将现有容量大小扩大一倍再加上2,如果还是不够大则直接等于需要的容量的大小。

image.png

扩容完成后,再调用Arrays.copyOf()方法完成数据的拷贝。

image.png

StringBuffer的常用方法

image.png

具体代码如下:


StringBuffer stringBuffer = new StringBuffer();
System.out.println("StringBuffer:"+stringBuffer);
//StringBuffer:

System.out.println("StringBuffer的长度:"+stringBuffer.length());
//StringBuffer的长度:0

stringBuffer = new StringBuffer("Hello World");
System.out.println("StringBuffer:"+stringBuffer);
//StringBuffer:Hello World

System.out.println("下标为2的字符是:"+stringBuffer.charAt(2));
//下标为2的字符是:l

stringBuffer = stringBuffer.append("Java");
System.out.println("append之后的StringBuffer:"+stringBuffer);
//append之后的StringBuffer:Hello WordJava

stringBuffer = stringBuffer.delete(3, 6);
System.out.println("delete之后的StringBuffer:"+stringBuffer);
//delete之后的StringBuffer:HelWorldJava

stringBuffer = stringBuffer.deleteCharAt(3);
System.out.println("deleteCharAt之后的StringBuffer:"+stringBuffer);
//deleteCharAt之后的StringBuffer:HelorldJava

stringBuffer = stringBuffer.replace(2,3,"StringBuffer");
System.out.println("replace之后的StringBuffer:"+stringBuffer);
//replace之后的StringBuffer:HeStringBufferorldJava

String str = stringBuffer.substring(2);
System.out.println("substring之后的String:"+str);
//substring之后的String:StringBufferorldJava

str = stringBuffer.substring(2,8);
System.out.println("substring之后的String:"+str);
//substring之后的String:String

stringBuffer = stringBuffer.insert(6,"six");
System.out.println("insert之后的StringBuffer:"+stringBuffer);
//insert之后的StringBuffer:HeStrisixngBufferorldJava

System.out.println("e的下标是:"+stringBuffer.indexOf("e"));
//e的下标是:1

System.out.println("下标6之后的e的下标是:"+stringBuffer.indexOf("e",6));
//下标6之后的e的下标是:15

stringBuffer = stringBuffer.reverse();
System.out.println("reverse之后的StringBuffer:"+stringBuffer);
str = stringBuffer.toString();
//reverse之后的StringBuffer:avajdroreffuBgnxisirtSeH

System.out.println("StringBuffer对应的String:"+str);
//StringBuffer对应的String:avajdroreffuBgnxisirtSeH

StringBuilder

StringBufferStringBuilder 拥有同一个父类 AbstractStringBuilder ,同时,实现的接口也是完全一样,都实现了 java.io.Serializable, CharSequence 两个接口。

image.png

image.png

它们最大区别在于 StringBuffer 几乎对所有的方法都实现了同步,而 StringBuilder 则没有实现,如下图对于 AbstractStringBuilderappend方法的重写,StringBuffer添加了synchronized 关键字修饰,而StringBuilder则没有。

image.png

image.png

所以StringBuffer 是线程安全的,在多线程中可以保证数据的同步,而StringBuilder则无法保证线程安全,所以在多线程中禁止使用StringBuilder

虽然StringBuffer实现成安全的,但是方法的同步需要消耗一定的资源,所以它的效率不如StringBuilder快。


//StringBuffer
long startTime = System.currentTimeMillis();
StringBuffer str = new StringBuffer();
for(int i = 0;i<500000;i++){
  str.append(i);
}
long endTime = System.currentTimeMillis();
System.out.println("StringBuffer类型操作耗时"+(endTime-startTime)+"毫秒");
//StringBuffer类型操作耗时45毫秒

//StringBuilder
long startTime = System.currentTimeMillis();
StringBuilder str = new StringBuilder();
for(int i = 0;i<500000;i++){
  str.append(i);
}
long endTime = System.currentTimeMillis();
System.out.println("StringBuilder类型操作耗时"+(endTime-startTime)+"毫秒");
//StringBuffer类型操作耗时34毫秒

StringBuilder为什么不安全

我们通过一个例子来进行测试:


StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < 10; i++){
    new Thread(new Runnable() {//开启10个线程
        @Override
        public void run() {
            for (int j = 0; j < 1000; j++){
		//每个线程对stringBuilder 追加 a 
                stringBuilder.append("a");
            }
        }
    }).start();
}

try {
    Thread.sleep(100);
    System.out.println(stringBuilder.length());
} catch (InterruptedException e) {
    e.printStackTrace();
}

正常情况下,操作完成后StringBuilder的值应为:10 * 1000 = 10000,但多次运行的结果是:

image.png

image.png

长度比10000小(也有可能会等于10000),同时抛出了数组下表越界的异常,证明了StringBuilder是线程不安全的。

StringBuilderappend()方法底层调用了AbstractStringBuilderappend()方法:

image.png

count为字符串长度,len为追加的字符串长度,count += len这行代码如果是多线程同时访问,很可能会出现数据错误。比如count=0,len=1,当量程同时执行到这一行,获取到的count则都是0,那么经过运算后,count的值为1,而不是2,这就解释了为什么最终的长度可能会比预期的要小。

字符的添加时调用了putStringAt(count,str)方法完成的,count为当前的字符串长度,通过ensureCapacityinternal(count+len)方法对数组进行扩容后,它一定是小于等于数组最大容量的,putStringAt(count,str)方法中每添加一个字符,都会给count1,当达到数组长度上线后再进行扩容。

如果是两个线程同时执行putStringAt(count,str),假设此时的count = 3,数组容量为4,两个线程拿到的count都等于3,数组容量大于count,所以不会进行扩容。也就意味着,数组其实就一个字符的空间,但是要插入两个字符,所以当其中一个线程执行完成后,另一个线程执行的时候,超出了数组的长度,抛出了异常。

image.png

来源:微信公众号

作者:南风

原文:参加了这么多面试,还是不懂StringBuffer和StringBuilder的区别?

Q.E.D.

知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议