Java字符串之性能優化

2021-03-02 Qunar技術沙龍
基礎類型轉化成String

在程序中你可能時常會需要將別的類型轉化成String,有時候可能是一些基礎類型的值。在拼接字符串的時候,如果你有兩個或者多個基礎類型的值需要放到前面,你需要顯式的將第一個值轉化成String(不然的話像System.out.println(1+』a')會輸出98,而不是」1a」)。當然了,有一組String.valueOf方法可以完成這個(或者是基礎類型對應的包裝類的方法),不過如果有更好的方法能少敲點代碼的話,誰還會願意這麼寫呢?

在基礎類型前面拼接上一個空串(」"+1)是最簡單的方法了。這個表達式的結果就是一個String,在這之後你就可以隨意的進行字符串拼接操作了——編譯器會自動將那些基礎類型全轉化成String的。

不幸的是,這是最糟糕的實現方法了。要想知道為什麼,我們得先介紹下這個字符串拼接在Java裡是如何處理的。如果一個字符串(不管是字面常量也好,或者是變量,方法調用的結果也好)後面跟著一個+號,再後面是任何的類型表達式:

Java編譯器會把它變成:

1

new StringBuilder().append( string_exp ).append( any_exp ).toString()

如果表達式裡有多個+號的話,後面相應也會多多幾個StringBuilder.append的調用,最後才是toString方法。

StringBuilder(String)這個構造方法會分配一塊16個字符的內存緩衝區。因此,如果後面拼接的字符不超過16的話,StringBuilder不需要再重新分配內存,不過如果超過16個字符的話StringBuilder會擴充自己的緩衝區。最後調用toString方法的時候,會拷貝StringBuilder裡面的緩衝區,新生成一個String對象返回。

這意味著基礎類型轉化成String的時候,最糟糕的情況就是你得創建:一個StringBuilder對象,一個char[16]數組,一個String對象,一個能把輸入值存進去的char[]數組。使用String.valueOf的話,至少StringBuilder對象省掉了。

有的時候或許你根本就不需要轉化基礎類型。比如,你正在解析一個字符串,它是用單引號分隔開的。最初你可能是這麼寫的:

1

final int nextComma = str.indexOf("'");

或者是這樣:

1

final int nextComma = str.indexOf('\'');

程序開發完了,需求變更了,需要支持任意的分隔符。當然了,你的第一反應是,得將這個分隔符存到一個String對象中,然後使用String.indexOf方法來進行拆分。我們假設有個預先配置好的分隔符就放到m_separator欄位裡(譯註:能用這個變量名的,應該不是Java開發出身的吧。。)。那麼,你解析的代碼應該會是這樣的:

1

2

3

4

5

6

7

8

9

10

11

12

private static List<String> split( final String str )

{

    final List<String> res = new ArrayList<String>( 10 );

    int pos, prev = 0;

    while ( ( pos = str.indexOf( m_separator, prev ) ) != -1 )

    {

        res.add( str.substring( prev, pos ) );

        prev = pos + m_separator.length();

    }

    res.add( str.substring( prev ) );

    return res;

}

不過後面你發現這個分隔符就只有一個字符。在初始化的時候,你把String mseparator改成了char mseparator,然後把setter方法也一起改了。但你希望解析的方法不要改動太大(代碼現在是好使的,我為什麼要費勁去改它呢?):

1

2

3

4

5

6

7

8

9

10

11

12

private static List<String> split2( final String str )

{

    final List<String> res = new ArrayList<String>( 10 );

    int pos, prev = 0;

    while ( ( pos = str.indexOf("" + m_separatorChar, prev ) ) != -1 )

    {

        res.add( str.substring( prev, pos ) );

        prev = pos + 1;

    }

    res.add( str.substring( prev ) );

    return res;

}

正如你所看到的,indexOf方法的調用被改動了,不過它還是新建出了一個字符串然後傳遞進去。當然,這麼做是錯的,因為還有一個indexOf方法是接收char類型而不是String類型的。我們用它來改寫一下:

1

2

3

4

5

6

7

8

9

10

11

12

private static List<String> split3( final String str )

{

    final List<String> res = new ArrayList<String>( 10 );

    int pos, prev = 0;

    while ( ( pos = str.indexOf(m_separatorChar, prev ) ) != -1 )

    {

        res.add( str.substring( prev, pos ) );

        prev = pos + 1;

    }

    res.add( str.substring( prev ) );

    return res;

}

我們來用上面的三種實現來進行測試,將」abc,def,ghi,jkl,mno,pqr,stu,vwx,yz」這個串解析1000萬次。下面是Java 641和715的運行時間。Java7由於它的String.substring方法線性複雜度的所以運行時間反而增加了。關於這個你可以參考下這裡的資料。

可以看到的是,簡單的一個重構,明顯的縮短了分割字符串所需要的時間(split/split2->split3)。


splitsplit2split3Java 64.65 sec10.34 sec3.8 secJava 76.72 sec8.29 sec4.37 sec字符串拼接

本文當然也不能完全不提字符串拼接另外兩種方法。第一種是String.concat,這個很少會用到。它內部其實是分配了一個char[],長度就是拼接後的字符串的長度,它將字符串的數據拷貝到裡面,最後使用了私有的構造方法來生成了一個新的字符串,這個構造方法不會再對char[]進行拷貝,因此這個方法調用只創建了兩個對象,一個是String本身,還有一個就是它內部的char[]。不幸的是,除非你只拼接兩個字符串,這個方法才會比較高效一些。

還有一種方法就是使用StringBuilder類,以及它的一系列的append方法。如果你有很多要拼接的值的話,這個方法當然是最快的了。它在Java5中被首度引入,用來替代StringBuffer。它們的主要區別就是StringBuffer是線程安全的,而StringBuilder不是。不過你會經常並發的拼接字符串麼難道?

在測試中,我們把0到100000之間的數全部進行了拼接,分別使用了String.concat, +操作符,還有StringBuilder,代碼如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

String res = "";

for ( int i = 0; i < ITERS; ++i )

{

    final String s = Integer.toString( i );

    res = res.concat( s );

}       

StringBuilder res = new StringBuilder();

for ( int i = 0; i < ITERS; ++i )

{

    final String s = Integer.toString( i );

    res.append( s );

}

String.concat+StringBuilder.append10.145 sec42.677 sec0.012 sec

結果非常明顯——O(n)的時間複雜度明顯要比O(n2) 要強得多。不過在實際工作中會用到大量的+操作符——因為它們實在是非常方便。為了解決這個問題,從Java6 update 20開始,引入了一個-XX:+OtimizeStringConcat開關。在Java 702和Java 715之間的版本,它是默認打開著的(在Java 6_41中還是默認關閉著的),因此可能你得手動將它打開。跟其它-XX的選項一樣,它的文檔也相當的差:

Optimize String concatenation operations where possible. (Introduced in Java 6 Update 20)

我們假設Oracle的工程師實現這個選項的時候是盡了最大努力的吧。坊間傳聞,它是把一些StringBuilder拼接的邏輯替換成了類似String.concat那樣的實現——它先生成一個合適大小的char[]然後再把東西拷貝進去。最後生成一個String。那些嵌套的拼接操作它可能也支持(str1 +(str2+str3) +str4)。打開這個選項後進行測試,結果表明,+號的性能跟String.concat的十分接近:

String.concat+StringBuilder.append10.19 sec10.722 sec0.013 sec

我們做另外一個測試。正如前面提到的,默認的StringBuilder構造器分配的是16個字符的緩衝區。當需要添加第17個字符時,這個緩衝區會被擴充。我們把100到100000間的數字分別追加到」12345678901234」的後面。結果串的長度應該是在17到20之間,因此默認的+操作符的實現會需要StringBuilder重新調整大小。作為對比,我們再做另一個測試,在這裡我們直接創建一個StringBuilder(21)來保證它的緩衝區足夠大,而不會重新調整:

1

2

final String s = BASE + i;

final String s = new StringBuilder( 21 ).append( BASE ).append( i ).toString();

沒有打開這個選項的話,+號的實現會比顯式的StringBuilder的實現的時間要多出一半。打開了這個選項後,兩邊的結果是一樣的。不過有趣的是,即使是StringBuilder的實現本身,打開了開關後速度居然也變快了!

+, 開關關閉+, 開關打開new StringBuilder(21),開關關閉new StringBuilder(21),開關打開0.958 sec0.494 sec0.663 sec0.494 sec總結

當轉化成字符串的時候,應當避免使用」"串進行轉化。使用合適的String.valueOf方法或者包裝類的toString(value)方法。

儘量使用StringBuilder進行字符串拼接。檢查下老舊碼,把那些能替換掉的StringBuffer也替換成它。

使用Java 6 update 20引入的-XX:+OptimizeStringConcat選項來提高字符串拼接的性能。在最近的Java7的版本中已經默認打開了,不過在Java 6_41還是關閉的。

本文轉自:java一日一條

長按識別二維碼,瀏覽器打開

秒下大講堂在線課程APP

支持Android和IOS

Qunar最新最熱門的在線視頻課程盡在其中!

相關焦點

  • Java性能優化的50個細節(珍藏版)
    在JAVA核心API中,有許多應用final的例子,例如java、lang、String,為String類指定final防止了使用者覆蓋length()方法。另外,如果一個類是final的,則該類所有方法都是final的。java編譯器會尋找機會內聯(inline)所有的final方法(這和具體的編譯器實現有關),此舉能夠使性能平均提高50%。
  • Java 性能優化的那些事兒
    在 Java 核心 API 中,有許多應用 final 的例子,例如 java、lang、String,為 String 類指定 final 防止了使用者覆蓋 length() 方法。另外,如果一個類是 final 的,則該類所有方法都是 final 的。java 編譯器會尋找機會內聯(inline)所有的 final 方法(這和具體的編譯器實現有關),此舉能夠使性能平均提高 50%。
  • 必會的55個Java性能優化細節!一網打盡!
    在JAVA核心API中,有許多應用final的例子,例如java、lang、String,為String類指定final防止了使用者覆蓋length()方法。另外,如果一個類是final的,則該類所有方法都是final的。java編譯器會尋找機會內聯(inline)所有的final方法(這和具體的編譯器實現有關),此舉能夠使性能平均提高50%。
  • 提升java編程性能優化知識 程式設計師必看這幾點
    對於學習java的學子也是如此,那麼java程式設計師如何提高編程性能呢,有哪些小知識或者技巧呢,怎麼樣才能在編程性能優化方面有所提升呢?  1.儘量在合適的場合使用單例  使用單例可以減輕加載的負擔,縮短加載的時間,提高加載的效率,但並不是所有地方都適用於單例,簡單來說,單例主要適用於以下三個方面:
  • Java字符串拼接效率分析及最佳實踐
    java連接字符串有多種方式,比如+操作符,StringBuilder.append方法,這些方法各有什麼優劣(可以適當說明各種方式的實現細節)? 按照高效的原則,那麼java中字符串連接的最佳實踐是什麼? 有關字符串處理,都有哪些其他的最佳實踐?
  • Java性能優化的50個細節,我必須分享給你!
    在JAVA核心API中,有許多應用final的例子,例如java、lang、String,為String類指定final防止了使用者覆蓋length()方法。另外,如果一個類是final的,則該類所有方法都是final的。java編譯器會尋找機會內聯(inline)所有的final方法(這和具體的編譯器實現有關),此舉能夠使性能平均提高50%。
  • java基礎編程題之String字符串練習
    以下是剛開始學習java的基礎編程題,每天持續更新java每個知識點的題目,持續練習,不斷提高java基本功,培養編程能力。
  • 這些Java字符串細節,能讓你百尺竿頭更進一步
    /   字符串駐留   /當相同的字符串常量被多次創建時,注意是使用雙引號(" ")顯式聲明時,字符串常量對象會被保存在常量池中,且只會創建一個對象,這就是字符串駐留,這個名詞的產生就是為了提升性能。簡單提一下,字符串中有一個方法叫做intern();那麼這個方法有什麼作用呢?
  • String字符串性能優化的幾種方案
    String字符串是系統裡最常用的類型之一,在系統中佔據了很大的內存,因此,高效地使用字符串,對系統的性能有較好的提升。針對字符串的優化,我在工作與學習過程總結了以下三種方案作分享:一.優化構建的超大字符串驗證環境:jdk1.8反編譯工具:jad1.下載反編譯工具jad,百度雲盤下載:
  • 性能優化之PHP優化
    在我們平常寫代碼的過程中,除了資料庫的優化,針對與文件的優化,我們還需要對PHP執行優化,當然對於老司機來說,這都是毛毛雨咯~但是畢竟有新手嘛,於是,我整理這麼一片文章。(未完待續...)性能優化之PHP優化(一):PHP結構1.字符串
  • 跟我學java編程—Java字符串類型
    圖 5-14 四種不適合存儲計算表達式Java語言的字符串類型同前面介紹的Java基礎類型不太一樣,Java沒有給字符串類型定義一個關鍵字,用於聲明字符串類型,而是通過類的方式提供對字符串的操作和存儲在Java語言中,字符串類型被定義為類,類名為「String」,該類提供了多個構造方法和字符串操作方法,用於滿足對字符串不同的處理要求。String類的初始化String類可用字符串常量對其初始化。
  • Java中的字符串常用方法
    字符串是由多個字符組成的一串數據String的特點:字符串是常量,一旦被創建就不能改變,這是因為字符串的值是存放在方法區的常量池裡面,但是引用可以改變。public boolean contains(String str): 判斷字符串中是否包含傳遞進來的字符串public boolean startsWith(String str): 判斷字符串是否以傳遞進來的字符串開頭public boolean endsWith(String str): 判斷字符串是否以傳遞進來的字符串結尾
  • java開發工程師 javascript的字符串
    寫在前面:java作為一門世界級程式語言金字塔頂尖的語言。需要大量的練習、練習、練習來鞏固自己所獲得的知識。
  • Java之String重點解析
    3、"abc"+"gbn"+s直接的字符串拼接是否真的比使用StringBuilder的性能低?如何進行字符串操作才擁有更好的性能?等等。此外,字符編碼的相關知識也是非常重要;畢竟,現在使用emoij是再正常不過的事情了。
  • Java高級開發必會的50個性能優化的細節(珍藏版)
    本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫在JAVA程序中,性能問題的大部分原因並不在於JAVA語言,而是程序本身。養成良好的編碼習慣非常重要,能夠顯著地提升程序性能。
  • Java中一種字符串的內存管理方法
    而由於字符串數據具有單個對象佔用空間較小但總體數量很大的特徵,大量的無用字符串數據不僅會影響堆空間的利用率,並且對Java虛擬機垃圾收集的性能有較大影響。當前對Java中字符串的內存管理優化方案主要關注於字符串的使用效率上,如消除常量重複、延遲分配等技術[2],通過修改Java虛擬機對字符串分配回收的支持來提高堆中字符串的使用效率。
  • Java之編譯期優化
    概述Java語言的「編譯期」其實是一段「不確定」的操作過程,因為他可能是指一個前端編譯器把java文件轉變成class文件的過程;也可能是指虛擬機的後端運行期編譯器把字節碼轉變成機器碼的過程;還可能是指使用靜態提前編譯器直接把java文件編譯成本地機器代碼的過程。
  • 關於Java性能調優的11個簡單技巧
    優化應用程式獲得最佳性能並不是一件容易的事。它需要一定的經驗和知識。那麼,我們現階段還不具備這樣的經驗和知識,是不是就做不到了呢?但是,這並不意味著在你證明必要之前,你應該更換任何標準庫或構建複雜的優化。在大多數情況下,過早優化不但會佔用大量時間,而且會使代碼變得難以閱讀和維護。更糟糕的是,這些優化通常不會帶來任何好處,因為你花費大量時間來優化的是應用程式的非關鍵部分。
  • 教學篇|11個簡單的Java性能調優技巧
    大多數開發人員理所當然地以為性能優化很複雜,需要大量的經驗和知識。好吧,不能說這是完全錯誤的。優化應用程式以獲得最佳性能不是一件容易的事情。但是,這並不意味著如果你不具備這些知識,就不能做任何事情。這裡有11個易於遵循的建議和最佳實踐可以幫助你創建一個性能良好的應用程式。大部分建議是針對Java的。
  • 教你如何刪除Java字符串中的重複字符
    Java刪除字符串中的重複字符【問題描述】刪除字符串中的重複字符.【輸入形式】輸入一個字符串,全為字母字符【輸出形式】輸出刪除重複字符後的字符串【樣例輸入】abbcbd【樣例輸出】abcd【樣例說明】刪除第二個和第三個「b」,保留第一個遇到的不同字符/*【問題描述】刪除字符串中的重複字符.