这篇文章主要记录下个人学习过程中掌握的StringBuffer类与StringBuilder类的区别和用法。
我们先从源码上分析下StringBuffer与StringBulier的异同及实现原理。
源码部分
两者间的区别
StringBuilder类:
public final class StringBuilder
extends AbstractStringBuilder
implements java.io.Serializable, Comparable<StringBuilder>, CharSequence
StringBuffer类:
public final class StringBuffer
extends AbstractStringBuilder
implements java.io.Serializable, Comparable<StringBuffer>, CharSequence
不难发现两个类在继承和实现接口方面有高度的一致性,譬如都继承了AbstractStringBuilder类,都实现了Serializable、Cmperable接口等。实际上,两个类的功能基本是完全等价的。
进一步观察以下源码:
StringBuffer类:
@Override
public synchronized int compareTo(StringBuffer another) {
return super.compareTo(another);
}
StringBuilder类:
@Override
public int compareTo(StringBuilder another) {
return super.compareTo(another);
}
上述代码以其中一个方法为例,我们可以发现StringBuffer类相较于StringBuilder类,其中的方法被声明为了synchronized,即“线程安全的”。 实际上,StringBuffer类的大多数方法都是被声明为synchornized。
由此,我们可以总结出二者之间的一些异同和应用方面的选择:
- StringBuffer类与StringBuilder类大体上的功能基本一致,他们继承于同一个抽象父类。
- StringBuffer类中大多数方法都采用了synchornized关键字进行修饰,因此它可以被认为是线程安全的。而相较于StringBuffer类,我们可以认为StringBuilder类是线程不安全的。
- StringBuilder类主要用于单线程操作,它线程不安全,但是效率更高;StringBuffer则适合多个线程同时进行操作,效率不高但可以保证操作的正确性。
从上述我们已经知道,二者在方法功能上并没有太大区别,为避免赘述,下文仅用StringBuilder类来探讨二者的方法应用。
扩容机制
StringBuilder类的扩容机制不算复杂,但要讲详细的同时讲清楚还是比较占据篇幅的。这里只说一下结论,StringBuilder底层也是通过数组进行存储,若数组容量不足,则会对数组进行扩容。默认情况下会扩容为原来容量的2倍+2,同时复制原有数组的元素到新的数组中。
这一部分若想深入了解可以参考下其他博主的文章,或者稍后我会单独写一篇专门分析这个机制的文章再链接到此。
应用部分
构造器
StringBuilder类常用的构造器有三个。
StringBuilder sb1 = new StringBuilder(); // 空参,初始数组默认容量16
StringBuilder sb2 = new StringBuilder(128); // 以128作为初始数组的容量
StringBuilder sb3 = new StringBuilder("hello"); // 将传入的字符串存储在数组中,若容量不够会进行扩容
常用方法
StringBuilder类的常用方法其实与String类相差不大,主要的一点区别即是对于需要修改字符串内容的方法,StringBuilder类不再像String类那样需要将一个新的String类作为返回值返回,而是直接在当前对象中进行修改。(注意并不是说StringBuilder类的方法就没有返回值了)
方便归类,我简要地将StringBuilder类的常用方法分为以下几个部分:增、删、查、改和其他。
增
append(CharSequence s),在已储存的字符序列末尾添加新的字符序列,有众多的重载类型:
sb1.append("hello"); // 直接传入一个String
sb1.append("hello",1,3); // 截取传入String的部分再进行添加,区间左闭右开
sb1.append(new char[]{'a','b','c','d'}); // 直接传入一个char数组
sb1.append(new char[]{'a','b','c','d'},1,2); // 截取部分char数组,同样左闭右开
// ...还有一些不常用的重载方法在此不做赘述
返回值为当前对象本身的引用:
StringBuffer s1 = new StringBuffer("abc");
StringBuffer s3 = s1.append(1);
s1.append(2);
System.out.println(s3); // abc12
System.out.println(s1); // abc12
删
delete(int start, int end),删除区间内的字符序列,左闭右开。
s1.delete(2,4);
System.out.println(s1); // ab2
查
charAt(int n):获取索引位置的字符。
s1.charAt(0); // a
改
这一方面主要有两个方法。
setCharAt(int n, char ch):修改索引位置的元素。
s1.setCharAt(2, c); // abc
replace(int start, int end, String str):修改左闭右开区间内的字符序列为传入的字符串。
s1.replace(0,2,"12"); // 12c
其他
insert(int offset, String str):在指定索引位置前插入新的字符序列,含重载方法,这里仅介绍最常用的一种。
s1.insert(2, "here"); // 12herec
length():获取字符序列长度。
字符序列遍历方法:for循环+charAt():
for( int i = 0 ; i < s1.length() ; i++ )
System.out.print(s1.charAt(i));
关于StringBuilder与StringBuffer类的基本方法与String类其实比较类似也比较简单,就先介绍到这里。
StringBuffer/StringBuilder与String类的互转
详见这篇文章的末尾部分,已经讲的较为详细,不再赘述。
对比StringBuffer/StringBuilder/String类三者的执行效率
运行如下代码:
StringBuffer sb1 = new StringBuffer();
StringBuilder sb2 = new StringBuilder();
String s1 = new String();
Date start1 = new Date();
for (int i = 0; i < 100000; i++) {
sb1.append(i);
}
Date end1 = new Date();
System.out.println("StringBuffer:"+(end1.getTime()-start1.getTime()));
Date start2 = new Date();
for (int i = 0; i < 100000; i++) {
sb2.append(i);
}
Date end2 = new Date();
System.out.println("StringBuilder:"+(end2.getTime()-start2.getTime()));
Date start3 = new Date();
for (int i = 0; i < 100000; i++) {
s1 += i;
}
Date end3 = new Date();
System.out.println("String:"+(end3.getTime()-start3.getTime()));
结果:
从结果来看StringBuilder执行效率最快,String最慢。我们粗略分析下为什么是这个结果。
首先String最慢不难理解,由于String类是不可变长的字符序列,因此每次对String类进行拼接操作时,都需要在堆空间新建一个String类的对象再将其引用返回至变量s1中;而另外两个类是可变长的字符序列,每次调用append方法进行拼接时不需要再新建实例化对象再返回引用这一繁琐的操作,从而节省大部分时间。
对于StringBuffer与StringBuilder,StringBuffer效率略低是因为其许多方法都被声明为synchronized,“线程安全的”。而线程安全往往需要牺牲一部分的运行效率,因此这个结果也不难解释了。