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毫秒
StringBuffer
和 String
类似,底层也是用一个数组来存储字符串的值,并且数组的默认查那个度为16,即一个空的StringBuffer
对象,数组长度为16。
实例化一个StringBuffer
对象即创建了一个大小为16
个字符的字符串缓冲区。
当我们使用有参构造器来创建一个StringBuffer
对象时,数组的长度就是值的长度+16
来做为数组的长度。
我们可以看到有参构造器中依次执行了 super(str.length()+16)
,
aappend(str)
,这也就说明了StringBuffer
在创建的时候,先创建了一个长度为str长度+16
的数组,然后把str的值进行了追加。
所以一个StringBuffer
创建完成之后,有16个字符空间供使用。如果修改的值超过了16个字符串,则调用 ensureCapacityInternal()
方法来检查StringBuffer
对象的原 char
数组能否装下新的字符串,如果装不下则对char
数组进行扩容。
扩容的逻辑就是创建一个新的char
数组,newCapacity()
方法用来确认新的容量大小,将现有容量大小扩大一倍再加上2,如果还是不够大则直接等于需要的容量的大小。
扩容完成后,再调用Arrays.copyOf()
方法完成数据的拷贝。
StringBuffer的常用方法
具体代码如下:
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
StringBuffer
与 StringBuilder
拥有同一个父类 AbstractStringBuilder
,同时,实现的接口也是完全一样,都实现了 java.io.Serializable, CharSequence
两个接口。
它们最大区别在于 StringBuffer
几乎对所有的方法都实现了同步,而 StringBuilder
则没有实现,如下图对于 AbstractStringBuilder
中 append
方法的重写,StringBuffer
添加了synchronized
关键字修饰,而StringBuilder
则没有。
所以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
,但多次运行的结果是:
长度比10000
小(也有可能会等于10000
),同时抛出了数组下表越界的异常,证明了StringBuilder
是线程不安全的。
StringBuilder
的append()
方法底层调用了AbstractStringBuilder
的append()
方法:
count
为字符串长度,len
为追加的字符串长度,count += len
这行代码如果是多线程同时访问,很可能会出现数据错误。比如count=0
,len=1
,当量程同时执行到这一行,获取到的count
则都是0
,那么经过运算后,count
的值为1
,而不是2
,这就解释了为什么最终的长度可能会比预期的要小。
字符的添加时调用了putStringAt(count,str)
方法完成的,count
为当前的字符串长度,通过ensureCapacityinternal(count+len)
方法对数组进行扩容后,它一定是小于等于数组最大容量的,putStringAt(count,str)
方法中每添加一个字符,都会给count
加1
,当达到数组长度上线后再进行扩容。
如果是两个线程同时执行putStringAt(count,str)
,假设此时的count = 3
,数组容量为4
,两个线程拿到的count
都等于3
,数组容量大于count
,所以不会进行扩容。也就意味着,数组其实就一个字符的空间,但是要插入两个字符,所以当其中一个线程执行完成后,另一个线程执行的时候,超出了数组的长度,抛出了异常。
来源:微信公众号
作者:南风
原文:参加了这么多面试,还是不懂StringBuffer和StringBuilder的区别?
Q.E.D.