Java字符串之性能優化
原文出處: Java譯站
基礎類型轉化成String
在程序中你可能時常會需要將別的類型轉化成String,有時候可能是一些基礎類型的值。在拼接字符串的時候,如果你有兩個或者多個基礎類型的值需要放到前面,你需要顯式的將第一個值轉化成String(不然的話像System.out.println(1+’a')會輸出98,而不是”1a”)。當然了,有一組String.valueOf方法可以完成這個(或者是基礎類型對應的包裝類的方法),不過如果有更好的方法能少敲點代碼的話,誰還會愿意這么寫呢?
在基礎類型前面拼接上一個空串(”"+1)是最簡單的方法了。這個表達式的結果就是一個String,在這之后你就可以隨意的進行字符串拼接操作了——編譯器會自動將那些基礎類型全轉化成String的。
不幸的是,這是最糟糕的實現方法了。要想知道為什么,我們得先介紹下這個字符串拼接在Java里是如何處理的。如果一個字符串(不管是字面常量也好,或者是變量,方法調用的結果也好)后面跟著一個+號,再后面是任何的類型表達式:
string_exp + any_exp
Java編譯器會把它變成:
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對象省掉了。
有的時候或許你根本就不需要轉化基礎類型。比如,你正在解析一個字符串,它是用單引號分隔開的。最初你可能是這么寫的:
final int nextComma = str.indexOf("'");
或者是這樣:
final int nextComma = str.indexOf('\'');
程序開發完了,需求變更了,需要支持任意的分隔符。當然了,你的第一反應是,得將這個分隔符存到一個String對象中,然后使用String.indexOf方法來進行拆分。我們假設有個預先配置好的分隔符就放到m_separator字段里(譯注:能用這個變量名的,應該不是Java開發出身的吧。。)。那么,你解析的代碼應該會是這樣的:
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(); // start from next char after separator } res.add( str.substring( prev ) ); return res; }
不過后面你發現這個分隔符就只有一個字符。在初始化的時候,你把String mseparator改成了char mseparator,然后把setter方法也一起改了。但你希望解析的方法不要改動太大(代碼現在是好使的,我為什么要費勁去改它呢?):
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; // start from next char after separator } res.add( str.substring( prev ) ); return res; }
正如你所看到的,indexOf方法的調用被改動了,不過它還是新建出了一個字符串然后傳遞進去。當然,這么做是錯的,因為還有一個indexOf方法是接收char類型而不是String類型的。我們用它來改寫一下:
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; // start from next char after separator } 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)。
split | split2 | split3 | </tr> </tbody>||||||||||||||||||||||||||||||||||||||||||||||
Java 6 | 4.65 sec | 10.34 sec | 3.8 sec | </tr>|||||||||||||||||||||||||||||||||||||||||||||
Java 7 | 6.72 sec | 8.29 sec | 4.37 sec | </tr> </tbody> </table>
String.concat | + | StringBuilder.append | </tr> </tbody>|||||||||||||||||||||||||||||||||||||||
10.145 sec | 42.677 sec | 0.012 sec | </tr> </tbody> </table>
String.concat | + | StringBuilder.append | </tr> </tbody>|||||||||||||||||||||||||||||||||
10.19 sec | 10.722 sec | 0.013 sec | </tr> </tbody> </table>
+, 開關關閉 | +, 開關打開 | new StringBuilder(21),開關關閉 | new StringBuilder(21),開關打開 |
0.958 sec | 0.494 sec | 0.663 sec | 0.494 sec |
總結
- 當轉化成字符串的時候,應當避免使用”"串進行轉化。使用合適的String.valueOf方法或者包裝類的toString(value)方法。
- 盡量使用StringBuilder進行字符串拼接。檢查下老舊碼,把那些能替換掉的StringBuffer也替換成它。
- 使用Java 6 update 20引入的-XX:+OptimizeStringConcat選項來提高字符串拼接的性能。在最近的Java7的版本中已經默認打開了,不過在Java 6_41還是關閉的。