对于字符串的处理,经常需要面对这三个类的选择问题。一开始工作时,大黄也是看着同事的代码,保持一致性,人家用StringBuffer我也用,节省时间。业余时间粗浅的了解了三者之间的区别,今天趁着空闲,翻出源代码,加深一下理解。
首先上定义:
- String:字符串常量
- StringBuffer:线程安全的字符串变量
- StringBuilder:非线程安全的字符串变量
那么到底啥时候用哪一个呢,原则是什么?原则当然是正确性和效率。大部分情况下,如果没有任何要求,其实这三兄弟是可以互相替代的,但是一旦涉及到计算和存储方面的效率我们就要看看到底用什么好了。
String的优势和劣势
String是个常量,这就是他的优势,也是他的劣势。常量,即不可变对象,当你的需求是一个创建以后很少再改变的字符串,String是首选。但是如果你的字符串需要频繁的进行改变,扩容等等,次数越多,效率越慢,与后两者比较,简直不忍直视。主要原因就是,每一次String的改变都会创建一个新的对象,然后指针指向这个新的对象。频繁大量这样做的后果就是会触发JVM的GC,速度变慢。
StringBuffer和StringBuilder的底层实现
先来看看这两个类:
public final class StringBuilder
extends AbstractStringBuilder
implements java.io.Serializable, CharSequence
public final class StringBuffer
extends AbstractStringBuilder
implements java.io.Serializable, CharSequence
看到这里炸毛了呀,这不是完全一样嘛,同一个父类,实现同样的接口,那为毛要写两个啊。回到最初的定义,线程安全与非线程安全,如果继续翻看源码,会发现StringBuffer绝大部分方法都用synchonized修饰,也即线程安全。多线程不在这里展开,简单说,当你的字符串涉及到多线程,请用StringBuffer。
再回到效率问题,看看这两个类是如何处理字符串的,我们找到父类AbstractStringBuilder一看究竟。
/**
* The value is used for character storage.
*/
char[] value;
/**
* The count is the number of characters used.
*/
int count;
一上来的这两个变量告诉我们本质上,这个类是一个char数组,而这个count也就是我们常见的length。理解了这个我们就直奔主题看字符串的操作。
字符串的拼接
public AbstractStringBuilder append(String str) {
if (str == null)
return appendNull();
int len = str.length();
ensureCapacityInternal(count + len); //关键一行
str.getChars(0, len, value, count); //关键一行
count += len;
return this;
}
append方法,将一个字符串加在原有字符串的尾端。这个方法的关键自然是大黄标注的关键一行的位置。第二行不详细解释,就是通过String的getChars来添加完成字符串的拼接,再底层,就是System. arraycopy。
那之前的一行是干什么的呢?扩容。既然使用了数组,那就要根据实际情况扩大它的容量,这个步骤就是通过ensureCapacityInternal(count + len)来实现的。来看一下具体的代码:
private void ensureCapacityInternal(int minimumCapacity) {
// overflow-conscious code
if (minimumCapacity - value.length > 0) {
value = Arrays.copyOf(value,
newCapacity(minimumCapacity));
}
}
private int newCapacity(int minCapacity) {
// overflow-conscious code
int newCapacity = (value.length << 1) + 2;
if (newCapacity - minCapacity < 0) {
newCapacity = minCapacity;
}
return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)
? hugeCapacity(minCapacity)
: newCapacity;
}
private int hugeCapacity(int minCapacity) {
if (Integer.MAX_VALUE - minCapacity < 0) { // overflow
throw new OutOfMemoryError();
}
return (minCapacity > MAX_ARRAY_SIZE)
? minCapacity : MAX_ARRAY_SIZE;
}
结合这三个方法来看看他们做了什么:
- 比较当下字符串和需要拼接的字符串的长度之和与当下AbstractStringBuilder的容量,如果不够,通过前者的值进行扩容(调用newCapacity)
- 通过移位符先把当前容量*2+2,再和拼接之和比较,取大为新容量值
- 新容量值若不为负数且不超过MAX_ARRAY_SIZE,则此扩容完成
- 若3条件不满足,再看拼接之和是否超过Integer.MAX_VALUE,若超过,则抛OOM, 若没有,则取拼接之和与MAX_ARRAY_SIZE之间较大的为新容量值,扩容完成。
上面的步骤有两个临界点,MAX_ARRAY_SIZE和Integer.MAX_VALUE
再看源码:
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; //AbstractStringBuilder
public static final int MAX_VALUE = 0x7fffffff;// Integer, 2147483647
所以可以得出一个很有趣的结论,虽然我有MAX_ARRAY_SIZE,但是扩容的极限是可以超过这个值的。
字符串的插入
public AbstractStringBuilder insert(int offset, String str) {
if ((offset < 0) || (offset > length()))
throw new StringIndexOutOfBoundsException(offset);
if (str == null)
str = "null";
int len = str.length();
ensureCapacityInternal(count + len);
System.arraycopy(value, offset, value, offset + len, count - offset);
str.getChars(value, offset);
count += len;
return this;
}
流程和append几乎一样,不再赘述。
三兄弟的再一次比较
大黄来举个生动的例子帮助理解这三者的交叉比较:
有一天,大黄和大黄的朋友们要完成一次集体作画。大黄呢,画术不精,一开始在工作台(JVM)上反复的画着一张又一张(创建很多String对象),虽然慢慢画的不错了,但是不用的草稿堆的工作台上到处都是,无奈只能停下先收拾干净(JVM垃圾回收)。最终成稿。大黄的朋友大白是个画家,这种事情轻而易举,他连草稿都没打,直接一幅画成(创建一个String对象)。另外一位朋友小凡,和大黄水平差不多,可是她更聪明,拿了可涂改的笔和纸(StringBuilder), 涂涂改改,比大黄快了很多。最后大家都完成了各自的部分,准备拼接起来,可是发觉拼完以后很难看,于是大家决定重画,在同一块画纸上分别作画,因为怕互相影响,所以决定一个一个排队画(StringBuffer),虽然比不上大家一拥而上快,但画作却能保证美观,完整。
终章
所以大部分情况下,就效率而言,StringBuilder>StringBuffer>String
<<EffectiveJava>>一书也建议我们在大规模拼接字符串场景下使用StringBuilder
最后是一个有趣的相关问题,下面两段代码处理速度是不是一样的?
String result = “Sophia GY is a” + “ good” + “ author”;
String S1 = “Sophia GY is a”;
String S2 = “ good”;
String S3 = “ author”;
String result = S1 +S2 + S3;
答案是不一样的,第一个更快,因为在JVM中,他会被看作String result = “Sophia GY is a good author”;,而第二段代码则是普通的拼接。关键就在于你的字符串是否来自于其他String对象。
Et voilà!