您好,登錄后才能下訂單哦!
Java字符串拼接效率分析及怎么實踐,相信很多沒有經驗的人對此束手無策,為此本文總結了問題出現的原因和解決方法,通過這篇文章希望你能解決這個問題。
java連接字符串有多種方式,比如+操作符,StringBuilder.append方法,這些方法各有什么優劣(可以適當說明各種方式的實現細節)?
按照高效的原則,那么java中字符串連接的***實踐是什么?
有關字符串處理,都有哪些其他的實踐?
廢話不多說,直接開始, 環境如下:
JDK版本: 1.8.0_65
CPU: i7 4790
內存: 16G
直接使用+拼接
看下面的代碼:
@Test public void test() { String str1 = "abc"; String str2 = "def"; logger.debug(str1 + str2); }
在上面的代碼中,我們使用加號來連接四個字符串,這種字符串拼接的方式優點很明顯: 代碼簡單直觀,但是對比StringBuilder和StringBuffer在大部分情況下比后者都低,這里說是大部分情況下,我們用javap工具對上面代碼生成的字節碼進行反編譯看看在編譯器對這段代碼做了什么。
public void test(); Code: 0: ldc #5 // String abc 2: astore_1 3: ldc #6 // String def 5: astore_2 6: aload_0 7: getfield #4 // Field logger:Lorg/slf4j/Logger; 10: new #7 // class java/lang/StringBuilder 13: dup 14: invokespecial #8 // Method java/lang/StringBuilder."<init>":()V 17: aload_1 18: invokevirtual #9 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 21: aload_2 22: invokevirtual #9 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 25: invokevirtual #10 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 28: invokeinterface #11, 2 // InterfaceMethod org/slf4j/Logger.debug:(Ljava/lang/String;)V 33: return
從反編譯的結果來看,實際上對字符串使用+操作符進行拼接,編譯器會在編譯階段把代碼優化成使用StringBuilder類,并調用append方法進行字符串拼接,***調用toString方法,這樣看來是否可以認為在一般情況下其實直接使用+,反正編譯器也會幫我優化為使用StringBuilder?
StringBuilder源碼分析
答案自然是不可以的,原因就在于StringBuilder這個類它內部做了些什么時。
我們看一看StringBuilder類的構造器
public StringBuilder() { super(16); } public StringBuilder(int capacity) { super(capacity); } public StringBuilder(String str) { super(str.length() + 16); append(str); } public StringBuilder(CharSequence seq) { this(seq.length() + 16); append(seq); }
StringBuilder提供了4個默認的構造器, 除了無參構造函數外,還提供了另外3個重載版本,而內部都調用父類的super(int capacity)構造方法,它的父類是AbstractStringBuilder,構造方法如下:
AbstractStringBuilder(int capacity) { value = new char[capacity]; }
可以看到實際上StringBuilder內部使用的是char數組來存儲數據(String、StringBuffer也是),這里capacity的值指定了數組的大小。結合StringBuilder的無參構造函數,可以知道默認的大小是16個字符。
也就是說如果待拼接的字符串總長度不小于16的字符的話,那么其實直接拼接和我們手動寫StringBuilder區別不大,但是我們自己構造StringBuilder類可以指定數組的大小,避免分配過多的內存。
現在我們再看看StringBuilder.append方法內部做了什么事:
@Override public StringBuilder append(String str) { super.append(str); return this; }
直接調用的父類的append方法:
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; }
在這個方法內部調用了ensureCapacityInternal方法,當拼接后的字符串總大小大于內部數組value的大小時,就必須先擴容才能拼接,擴容的代碼如下:
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); }
StringBuilder在擴容時把容量增大到當前容量的兩倍+2,這是很可怕的,如果在構造的時候沒有指定容量,那么很有可能在擴容之后占用了浪費大量的內存空間。其次擴容后還調用了Arrays.copyOf方法,這個方法把擴容前的數據復制到擴容后的空間內,這樣做的原因是:StringBuilder內部使用char數組存放數據,java的數組是不可擴容的,所以只能重新申請一片內存空間,并把已有的數據復制到新的空間去,這里它最終調用了System.arraycopy方法來復制,這是一個native方法,底層直接操作內存,所以比我們用循環來復制要塊的多,即便如此,大量申請內存空間和復制數據帶來的影響也不可忽視。
使用+拼接和使用StringBuilder比較
@Test public void test() { String str = ""; for (int i = 0; i < 10000; i++) { str += "asjdkla"; } }
上面這段代碼經過優化后相當于:
@Test public void test() { String str = null; for (int i = 0; i < 10000; i++) { str = new StringBuilder().append(str).append("asjdkla").toString(); } }
一眼就能看出創建了太多的StringBuilder對象,而且在每次循環過后str越來越大,導致每次申請的內存空間越來越大,并且當str長度大于16時,每次都要擴容兩次!而實際上toString方法在創建String對象時,調用了Arrays.copyOfRange方法來復制數據,此時相當于每執行一次,擴容了兩次,復制了3次數據,這樣的代價是相當高的。
public void test() { StringBuilder sb = new StringBuilder("asjdkla".length() * 10000); for (int i = 0; i < 10000; i++) { sb.append("asjdkla"); } String str = sb.toString(); }
這段代碼的執行時間在我的機器上都是0ms(小于1ms)和1ms,而上面那段代碼則大約在380ms!效率的差距相當明顯。
同樣是上面的代碼,將循環次數調整為1000000時,在我的機器上,有指定capacity時耗時大約20ms,沒有指定capacity時耗時大約29ms,這個差距雖然和直接使用+操作符有了很大的提升(且循環次數增大了100倍),但是它依舊會觸發多次擴容和復制。
將上面的代碼改成使用StringBuffer,在我的機器上,耗時大約為33ms,這是因為StringBuffer在大部分方法上都加上了synchronized關鍵字來保證線程安全,執行效率有一定程度上的降低。
使用String.concat拼接
現在再看這段代碼:
@Test public void test() { String str = ""; for (int i = 0; i < 10000; i++) { str.concat("asjdkla"); } }
這段代碼使用了String.concat方法,在我的機器上,執行時間大約為130ms,雖然直接相加要好的多,但是比起使用StringBuilder還要太多了,似乎沒什么用。其實并不是,在很多時候,我們只需要連接兩個字符串,而不是多個字符串的拼接,這個時候使用String.concat方法比StringBuilder要簡潔且效率要高。
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); }
上面這段是String.concat的源碼,在這個方法中,調用了一次Arrays.copyOf,并且指定了len + otherLen,相當于分配了一次內存空間,并分別從str1和str2各復制一次數據。而如果使用StringBuilder并指定capacity,相當于分配一次內存空間,并分別從str1和str2各復制一次數據,***因為調用了toString方法,又復制了一次數據。
結論
現在根據上面的分析和測試可以知道:
Java中字符串拼接不要直接使用+拼接。
使用StringBuilder或者StringBuffer時,盡可能準確地估算capacity,并在構造時指定,避免內存浪費和頻繁的擴容及復制。
在沒有線程安全問題時使用StringBuilder, 否則使用StringBuffer。
兩個字符串拼接直接調用String.concat性能***。
關于String的其他實踐
用equals時總是把能確定不為空的變量寫在左邊,如使用"".equals(str)判斷空串,避免空指針異常。
第二點是用來排擠***點的.. 使用str != null && str.length() != 0來判斷空串,效率比***點高。
在需要把其他對象轉換為字符串對象時,使用String.valueOf(obj)而不是直接調用obj.toString()方法,因為前者已經對空值進行檢測了,不會拋出空指針異常。
使用String.format()方法對字符串進行格式化輸出。
在JDK 7及以上版本,可以在switch結構中使用字符串了,所以對于較多的比較,使用switch代替if-else。
我暫時想的起來的就這么幾個了.. 請大家幫忙補充補充...
看完上述內容,你們掌握Java字符串拼接效率分析及怎么實踐的方法了嗎?如果還想學到更多技能或想了解更多相關內容,歡迎關注億速云行業資訊頻道,感謝各位的閱讀!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。