走進 JDK 之 Stringjava
你並不瞭解 Stringc++
今天是 String
系列最後一篇了,字符串的拼接。平常開發中,字符串拼接是很常見的操做,通常經常使用的有如下幾種:數組
+
拼接String
的 concat()
方法StringBuilder
的 append()
方法StringBuffer
的 append()
方法那麼,這幾種方法有什麼不一樣呢?具體性能如何?下面進行一個簡單的性能測試,代碼以下:安全
public class StringTest {
public static void main(String[] args) {
int count = 1000;
String word = "Hello, ";
StringBuilder builder = new StringBuilder("Hello,");
StringBuffer buffer = new StringBuffer("Hello,");
long start, end;
start = System.currentTimeMillis();
for (int i = 0; i < count; i++) {
word += "java";
}
end = System.currentTimeMillis();
System.out.println("String + : " + (end - start));
word = "Hello, ";
start = System.currentTimeMillis();
for (int i = 0; i < count; i++) {
word = word.concat("java");
}
end = System.currentTimeMillis();
System.out.println("String.concat() : " + (end - start));
word = "Hello, ";
start = System.currentTimeMillis();
for (int i = 0; i < count; i++) {
builder.append("java");
}
word = builder.toString();
end = System.currentTimeMillis();
System.out.println("StringBuilder : " + (end - start));
word = "Hello, ";
start = System.currentTimeMillis();
for (int i = 0; i < count; i++) {
buffer.append("java");
}
word = buffer.toString();
end = System.currentTimeMillis();
System.out.println("StringBuffer : " + (end - start));
}
}
複製代碼
運行結果以下所示:bash
1k | 1w | 10w | 100w | |
---|---|---|---|---|
+ | 11 | 397 | 20191 | 720286 |
concat | 3 | 72 | 5671 | 763612 |
StringBuilder | 0 | 0 | 3 | 17 |
StringBuffer | 1 | 1 | 4 | 36 |
以上都是執行一次的結果,可能不太嚴謹,但仍是能反映問題的。執行次數越多,性能差距越明顯,StringBuilder
> StringBuffer
> contact
> +
。關於其中緣由,我想不少人應該都知道。下面從源碼角度分析一下這幾種字符串拼接方式。微信
使用 +
拼接字符串是效率最低的一種方式嗎?首先,咱們要知道 +
具體是怎麼拼接字符串的。對於這種咱們不知道具體原理的時候,javap
是你的好選擇。從最簡單的一行代碼開始:多線程
String str = "a" + "b";
複製代碼
這樣寫其實並不行,智能的編譯器看到 "a"+"b"
就知道你要幹啥了,因此你編譯出來就是 String str = "ab"
,咱們稍做修改就能夠了:app
String a = "a";
String str = a + "b";
複製代碼
javap
看一下字節碼:ide
0: ldc #2 // String a
2: astore_1
3: new #3 // class java/lang/StringBuilder
6: dup
7: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
10: aload_1
11: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
14: ldc #6 // String b
16: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
19: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
22: astore_2
23: return
複製代碼
能夠看到編譯器自動將 +
轉換成了 StringBuilder.append()
方法,拼接以後再調用 StringBuilder.toString()
方法轉換成字符串。既然這樣的話,那豈不是應該和 StringBuilder
的執行效率同樣了?別忘了,上面的測試代碼使用 for
循環模擬頻繁的字符串拼接操做。使用 +
的話,在每一次循環中,都將重複下列操做:post
StringBuilder
對象StringBuilder.append()
方法StringBuilder.toString()
方法,該方法會經過 new String()
建立字符串幾萬次循環下來,你看看建立了多少中間對象,怪不得這麼慢,別人要麼以空間換時間,要麼以時間換空間。這傢伙倒好,即浪費時間,又浪費空間。因此,在頻繁拼接字符串的狀況下,儘可能避免使用 +
。那麼,它存在的意義何在呢?有的時候咱們就是要拼接兩個字符串,使用 +
,直截了當。
public String concat(String str) {
int otherLen = str.length();
if (otherLen == 0) {
return this; // str 爲空直接返回 this
}
int len = value.length;
char buf[] = Arrays.copyOf(value, len + otherLen);
str.getChars(buf, len);
return new String(buf, true);
}
void getChars(char dst[], int dstBegin) {
System.arraycopy(value, 0, dst, dstBegin, value.length);
}
複製代碼
先構建新的字符數組 buf[]
,再利用 System.arraycopy()
挪來挪去,最後 new String()
構建字符串。比 +
少了建立 StringBuilder
的過程,但每次循環中,又要從新建立字符數組,又要從新 new
字符串對象,頻繁拼接的時候效率仍是不是很理想。
再提一點,當傳入 str
長度爲 0 時,直接返回 this
。這好像是 String
中惟一一個返回 this
的地方了。
StringBuilder
和 StringBuffer
實際上是很像的,它兩頻繁拼接字符串的效率遠勝於 +
和 concat
。當循環執行 10w
次,分別耗時 3ms
、4ms
, StringBuilder
還比 StringBuffer
快那麼一點。至於爲何,Read the fucking source code !
先看看 StringBuilder.append()
:
@Override
public StringBuilder append(String str) {
super.append(str);
return this;
}
複製代碼
並無什麼實際邏輯,直接調用了父類的 append()
方法。看一下 StringBuilder
的類聲明:
public final class StringBuilder extends AbstractStringBuilder implements java.io.Serializable, CharSequence{}
複製代碼
StringBuilder
繼承了 AbstractStringBuilder
類,StringBuferr
其實也是。因此它們實際上調用的都是是 AbstractStringBuilder.append()
:
public AbstractStringBuilder append(String str) {
if (str == null)
return appendNull(); // 1
int len = str.length();
ensureCapacityInternal(count + len); // 2
str.getChars(0, len, value, count); // 3
count += len;
return this;
}
複製代碼
代碼中出現了兩個變量,value
和 count
,先來看看它們是幹嗎的。
/** * The value is used for character storage. */
char[] value;
/** * The count is the number of characters used. */
int count;
複製代碼
value
是一個字符數組,用來保存字符。它能夠自動擴容,在後面的代碼中你將會看到。count
是已使用的字符的數量,注意並非 vale[]
的長度。再回到 append()
方法,分三部分來解析。
當 append()
的參數爲 null
時調用,它並非什麼都不添加,而是正如它的方法名那樣,追加了 null
字符串。
private AbstractStringBuilder appendNull() {
int c = count;
ensureCapacityInternal(c + 4);
final char[] value = this.value;
value[c++] = 'n';
value[c++] = 'u';
value[c++] = 'l';
value[c++] = 'l';
count = c;
return this;
}
複製代碼
private void ensureCapacityInternal(int minimumCapacity) {
// overflow-conscious code
if (minimumCapacity - value.length > 0) {
value = Arrays.copyOf(value,newCapacity(minimumCapacity));
}
}
複製代碼
ensureCapacityInternal()
方法用來確保 value[]
的容量足以拼接參數中的字符串。若是容量不夠,將調用 Arrays.copyOf(value,newCapacity(minimumCapacity))
對 value[]
進行擴容,newCapacity(minimumCapacity)
就是字符數組的新長度。
private int newCapacity(int minCapacity) {
// overflow-conscious code
// 新容量等於舊容量乘以 2 再加上 2
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
// 若是需求容量大於 Integer 最大值,直接拋出 OOM
throw new OutOfMemoryError();
}
return (minCapacity > MAX_ARRAY_SIZE)
? minCapacity : MAX_ARRAY_SIZE;
}
複製代碼
基本的擴容邏輯是,新的數組大小是原來的兩倍再加上 2,可是有個最大值 MAX_ARRAY_SIZE
,其值是 Integer.MAX_VALUE - 8
,減去 8 是由於一些虛擬機會在數組中保留一些頭信息。固然,通常在程序中也達不到這個最大值。若是咱們直接和虛擬機說,我須要一個大小爲 Integer.MAX_VALUE
的新數組,那會直接拋出 OOM
。
新數組建立好了,那麼剩下的就是拼接字符串了。
str.getChars(0, len, value, count);
count += len;
複製代碼
str
是要拼接的字符串,是否是對這個 getChars()
方法很眼熟。仔細看過 String
源碼的話,應該對這個方法還有印象。
public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) {
if (srcBegin < 0) {
throw new StringIndexOutOfBoundsException(srcBegin);
}
if (srcEnd > value.length) {
throw new StringIndexOutOfBoundsException(srcEnd);
}
if (srcBegin > srcEnd) {
throw new StringIndexOutOfBoundsException(srcEnd - srcBegin);
}
System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);
}
複製代碼
進行一些邊界判斷以後,利用 System.arraycopy()
拼接字符串。
看完這三部分,也就完成了一次字符串拼接。回想一下,在大量拼接字符串的過程當中,append()
把時間都花在了哪裏?數組擴容和 System.arraycopy()
操做,的確比 +
和 concat()
不停的 new
對象效率高多了。
還記得 StringBuffer
雖然也一樣快,可是比 StringBuilder
慢了一些吧!來看看 StringBuffer
的實現:
@Override
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
複製代碼
邏輯是徹底一致的,可是多了 synchronized
關鍵字,用來保證線程安全。因此會比 StringBuilder
耗時一些。關於 StringBuilder
和 StringBuffer
之間的區別,除了 synchronized
關鍵字就沒有了。
+
和 String.concat()
只適合少許的字符串拼接操做,頻繁拼接時性能不如人意StringBuilder
和 StringBuffer
在頻繁拼接字符串時性能優異StringBuilder
不能保證線程安全。所以,在肯定單線程執行的狀況下,StringBuilder
是最優解StringBuffer
經過 synchronized
保證線程安全,適合多線程環境下使用。文章首發於微信公衆號:
秉心說
, 專一 Java 、 Android 原創知識分享,LeetCode 題解,歡迎掃碼關注!