由於String對象時不可變對象,因此在需要對字符串進行修改操作時(如字符串連接和替換),String對象總是會生成新的對象,所以其性能相對較差。
String常量的累加操作:對於靜態字符串的連接操作,Java在編譯時會進行徹底的優化,將多個連接操作的字符串在編譯時合成一個單獨的長字符串。
String變量的累加操作:底層使用了StringBuilder的功能。
StringBuffer和StringBuilder的擴容策略:當字符串緩沖區容量不足時,原有容量將會加倍,以新的容量來申請內存空間,建立新的char數組,然后將原數組中的內容復制到這個新的數組當中。因此,對於大對象的擴容會涉及大量的內存復制操作。所以,如果能夠預先評估StringBuilder或StringBuffer的大小,將能夠有效的節省這些操作,從而提高系統的性能。
JAVA的字符串拼接與性能
在JAVA中拼接兩個字符串的最簡便的方式就是使用操作符”+”了。如果你用”+”來連接固定長度的字符串,可能性能上會稍受影響,但是如果你是在循環中來”+”多個串的話,性能將指數倍的下降。假設有一個字符串,我們將對這個字符串做大量循環拼接操作,使用”+”的話將得到最低的性能。但是究竟這個性能有多差?
+, Join,StringBuffer,StringBuilder或String.concat()
1.String 是final對象,不會被修改,每次使用 + 進行拼接都會創建新的對象,而不是改變原來的對象,也屬於線程安全的;
2.StringBuffer可變字符串,主要用於字符串的拼接,屬於線程安全的;(StringBuffer的append操作用了synchronized)
3.StringBuilder可變字符串,主要用於字符串的拼接,屬於線程不安全的;
(1)循環操作:
import java.util.ArrayList; import java.util.List; import org.apache.commons.lang3.StringUtils; public class TestString { public static void main(String[] args) { //plus拼接字符串方式 String s = ""; long ts = System.currentTimeMillis(); for (int i = 0; i < 10000; i++) { s = s + String.valueOf(i); } long te = System.currentTimeMillis(); System.out.println("Plus cost {"+( te - ts) +"} ms"); //concat拼接字符串方式 String s2 = ""; long ts2 = System.currentTimeMillis(); for (int i = 0; i < 10000; i++) { s2 = s2.concat(String.valueOf(i)); } long te2 = System.currentTimeMillis(); System.out.println("concat cost {"+(te2 - ts2)+"} ms"); //StringUtils.join拼接字符串方式 List<String> list = new ArrayList<String>(); long ts3 = System.currentTimeMillis(); for (int i = 0; i < 10000; i++) { list.add(String.valueOf(i)); } StringUtils.join(list, ""); long te3 = System.currentTimeMillis(); System.out.println("StringUtils.join cost {"+(te3 - ts3)+"} ms"); //StringBuffer拼接字符串方式 StringBuffer sb = new StringBuffer(); long ts4 = System.currentTimeMillis(); for (int i = 0; i < 100000; i++) { sb.append(String.valueOf(i)); } sb.toString(); long te4 = System.currentTimeMillis(); System.out.println("StringBuffer cost {"+(te4 - ts4)+"} ms"); //StringBuilder拼接字符串方式 StringBuilder sb5 = new StringBuilder(); long ts5 = System.currentTimeMillis(); for (int i = 0; i < 100000; i++) { sb5.append(String.valueOf(i)); } sb5.toString(); long te5 = System.currentTimeMillis(); System.out.println("StringBuilder cost {"+(te5 - ts5)+"} ms"); } }
要特別注意的是:
StringBuilder和StringBuffer循環的次數是其它的10倍,如果是一樣,那么可能返回0,可見StringBuilder和StringBuffer的速度之快。)
系統:(四核cup)
結果:(10倍)
Plus cost {229} ms concat cost {116} ms StringUtils.join cost {22} ms StringBuffer cost {8} ms StringBuilder cost {7} ms
正常情況下:
plus cost {249} ms concat cost {115} ms StringUtils.join cost {20} ms StringBuffer cost {1} ms StringBuilder cost {1} ms
結果分析:
(1)StringBuilder性能比StringBuffer要好點。在1千萬的循環下, StringBuilder大約在500-600毫秒,而StringBuffer大約在700-800毫秒;
(2) StringUtils.join方式性能也是可以的,在循環不是很大的情況下,該種方式也是可以考慮的;
(3)另外2種方式,在循環的下,盡量不要使用。
因此,即時在做最簡單的拼接時,如果我們不想創建StringBuffer或StringBuilder實例使,我們也因該使用concat。但是對於大量的字符串拼接操作,就不應該使用concat,因為concat會降低你程序的性能,消耗你的cpu。因此,在不考慮線程安全和同步的情況下,為了獲得最高的性能,我們應盡量使用StringBuilder。
總結:
1.用+的方式效率最差,concat由於是內部機制實現,比+的方式好了不少。
2.Join和StringBuffer,相差不大,Join方式要快些,可見這種JavaScript中快速拼接字符串的方式在Java中也非常適用。
3.StringBuilder 的速度最快,但其有線程安全的問題,而且只有JDK5及以上的版本支持。
4.String對象串聯的效率最慢,單線程下字符串的串聯用StringBuilder,多線程下字符串的串聯用StrngBuffer;
5.在編譯階段就能夠確定的字符串常量,完全沒有必要創建String或StringBuffer對象。直接使用字符串常量的"+"連接操作效率最高(如:String str = “a” + “b” + “c”;)。
源碼:
1、其實每次調用contact()方法就是一次數組的拷貝,雖然在內存中是處理都是原子性操作,速度非常快,但是,最后的return語句會創建一個新String對象,限制了concat方法的速度。
public String concat(String str) { int otherLen = str.length(); if (otherLen == 0) { return this; } int len = value.length; char buf[] = Arrays.copyOf(value, len + otherLen); str.getChars(buf, len); return new String(buf, true); }
2. StringBuffer和StringBuilder的append方法都繼承自AbstractStringBuilder,整個邏輯都只做字符數組的加長,拷貝,到最后也不會創建新的String對象,所以速度很快,完成拼接處理后在程序中用strBuffer.toString()來得到最終的字符串。
public AbstractStringBuilder append(String str) { if (str == null) str = "null"; int len = str.length(); ensureCapacityInternal(count + len); str.getChars(0, len, value, count); count += len; return this; } private void ensureCapacityInternal(int minimumCapacity) { // overflow-conscious code if (minimumCapacity - value.length > 0) expandCapacity(minimumCapacity); } /** * This implements the expansion semantics of ensureCapacity with no * size check or synchronization. */ void expandCapacity(int minimumCapacity) { int newCapacity = value.length * 2 + 2; if (newCapacity - minimumCapacity < 0) newCapacity = minimumCapacity; if (newCapacity < 0) { if (minimumCapacity < 0) // overflow throw new OutOfMemoryError(); newCapacity = Integer.MAX_VALUE; } value = Arrays.copyOf(value, newCapacity); }
3. 字符串的加號“+” 方法, 雖然編譯器對其做了優化,使用StringBuilder的append方法進行追加,但是每循環一次都會創建一個StringBuilder對象,且都會調用toString方法轉換成字符串,所以開銷很大(時間、空間)。
如JVM對於“+”是這樣處理的,首先創建一個String對象str,並把“abc”賦值給str,然后在第三行中,其實JVM又創建了一個新的對象也名為str,然后再把原來的str的值和“de”加起來再賦值給新的str,而原來的str就會被JVM的垃圾回收機制(GC)給回收掉了,所以,str實際上並沒有被更改,也就是前面說的String對象一旦創建之后就不可更改了。所以,Java中對String對象進行的操作實際上是一個不斷創建新的對象並且將舊的對象回收的一個過程,所以執行速度很慢。
注:執行一次字符串“+”,相當於 str = new StringBuilder(str).append("a").toString()。