String和StringBuffer之概覽
創(chuàng )建字符串的較佳途徑
滯留字符串帶來(lái)的優(yōu)化
連接字符串時(shí)的優(yōu)化技巧
借助StringBuffer的初始化過(guò)程的優(yōu)化技巧
關(guān)鍵點(diǎn)
String和StringBuffer之概覽
非可變對象一旦創(chuàng )建之后就不能再被改變,可變對象則可以在創(chuàng )建之后被改變。String對象是非可變對象,StringBuffer對象則是可變對象。為獲得更佳的性能你需要根據實(shí)際情況小心謹慎地選擇到底使用這兩者中的某一個(gè)。下面的話(huà)題會(huì )作詳細的闡述。(注意:這個(gè)章節假設讀者已經(jīng)具備Java的String和StringBuffer的相關(guān)基礎知識。)
創(chuàng )建字符串的較佳途徑
你可以按照以下方式創(chuàng )建字符串對象:
1. String s1 = "hello";
String s2 = "hello";
2. String s3 = new String("hello");
String s4 = new String("hello");
上面哪種方式會(huì )帶來(lái)更好的性能呢?下面的代碼片斷用來(lái)測量二者之間的區別。
StringTest1.java
package com.performance.string;
/** This class shows the time taken for creation of
* String literals and String objects.
*/
public class StringTest1 {
public static void main(String[] args){
// create String literals
long startTime = System.currentTimeMillis();
for(int i=0;i<50000;i++){
String s1 = "hello";
String s2 = "hello";
}
long endTime = System.currentTimeMillis();
System.out.println("Time taken for creation of String literals : "
+ (endTime - startTime) + " milli seconds" );
// create String objects using ‘new‘ keyword
long startTime1 = System.currentTimeMillis();
for(int i=0;i<50000;i++){
String s3 = new String("hello");
String s4 = new String("hello");
}
long endTime1 = System.currentTimeMillis();
System.out.println("Time taken for creation of String objects : "
+ (endTime1 - startTime1)+" milli seconds");
}
}
這段代碼的輸出:
Time taken for creation of String literals : 0 milli seconds
Time taken for creation of String objects : 170 milli seconds
JVM是怎樣處理字符串的呢?
Java虛擬機會(huì )維護一個(gè)內部的滯留字符串對象的列表(唯一字符串的池)來(lái)避免在堆內存中產(chǎn)生重復的String對象。當JVM從class文件里加載字符串字面量并執行的時(shí)候,它會(huì )先檢查一下當前的字符串是否已經(jīng)存在于滯留字符串列表,如果已經(jīng)存在,那就不會(huì )再創(chuàng )建一個(gè)新的String對象而是將引用指向已經(jīng)存在的String對象,JVM會(huì )在內部為字符串字面量作這種檢查,但并不會(huì )為通過(guò)new關(guān)鍵字創(chuàng )建的String對象作這種檢查。當然你可以明確地使用String.intern()方法強制JVM為通過(guò)new關(guān)鍵字創(chuàng )建的String對象作這樣的檢查。這樣可以強制JVM檢查內部列表而使用已有的String對象。
所以結論是,JVM會(huì )內在地為字符串字面量維護一些唯一的String對象,程序員不需要為字符串字面量而發(fā)愁,但是可能會(huì )被一些通過(guò)new關(guān)鍵字創(chuàng )建的String對象而困擾,不過(guò)他們可以使用intern()方法來(lái)避免在堆內存上創(chuàng )建重復的String對象來(lái)改善Java的運行性能。下一小節會(huì )向大家展示更多的信息。
下圖展示了未使用intern()方法來(lái)創(chuàng )建字符串的情況。
你可以自己使用==操作符和String.equals()方法來(lái)編碼測試上面提到的區別。==操作符會(huì )返回true如果一些引用指向一個(gè)相同的對象但不會(huì )判斷String對象的內容是否相同;String.equals()方法會(huì )返回true如果被操作的String對象的內容相同。對于上面的代碼會(huì )有s1==s2,因為s1和s2兩個(gè)引用指向同一個(gè)對象,對于上面的代碼,s3.equals(s4)會(huì )返回true因為兩個(gè)對象的內容都一樣為”hello”。你可以從上圖看出這種機制。在這里有三個(gè)獨立的包含了相同的內容(”hello”)的對象,實(shí)際上我們不需要這么三個(gè)獨立的對象――因為要運行它們的話(huà)既浪費時(shí)間又浪費內存。
那么怎樣才能確保String對象不會(huì )重復呢?下一個(gè)話(huà)題會(huì )涵蓋對于內建String機制的興趣。
滯留字符串的優(yōu)化作用
同一個(gè)字符串對象被重復地創(chuàng )建是不必要的,String.intern()方法可以避免這種情況。下圖說(shuō)明了String.intern()方法是如何工作的,String.intern()方法檢查字符串對象的存在性,如果需要的字符串對象已經(jīng)存在,那么它會(huì )將引用指向已經(jīng)存在的字符串對象而不是重新創(chuàng )建一個(gè)。下圖描繪了使用了intern()方法的字符串字面量和字符串對象的創(chuàng )建情況。
下面的例程幫助大家了解String.intern()方法的重要性。
StringTest2.java
package com.performance.string;
// This class shows the use of intern() method to improve performance
public class StringTest2 {
public static void main(String[] args){
// create String references like s1,s2,s3...so on..
String variables[] = new String[50000];
for( int i=0;i<variables.length;i++){
variables[i] = "s"+i;
}
// create String literals
long startTime0 = System.currentTimeMillis();
for(int i=0;i<variables.length;i++){
variables[i] = "hello";
}
long endTime0 = System.currentTimeMillis();
System.out.println("Time taken for creation of String literals : "
+ (endTime0 - startTime0) + " milli seconds" );
// create String objects using ‘new‘ keyword
long startTime1 = System.currentTimeMillis();
for(int i=0;i<variables.length;i++){
variables[i] = new String("hello");
}
long endTime1 = System.currentTimeMillis();
System.out.println("Time taken for creation of String objects with ‘new‘ key word : "
+ (endTime1 - startTime1)+" milli seconds");
// intern String objects with intern() method
long startTime2 = System.currentTimeMillis();
for(int i=0;i<variables.length;i++){
variables[i] = new String("hello");
variables[i] = variables[i].intern();
}
long endTime2 = System.currentTimeMillis();
System.out.println("Time taken for creation of String objects with intern(): "
+ (endTime2 - startTime2)+" milli seconds");
}
}
這是上面那段代碼的輸出結果:
Time taken for creation of String literals : 0 milli seconds
Time taken for creation of String objects with ‘new‘ key word : 160 milli seconds
Time taken for creation of String objects with intern(): 60 milli seconds
連接字符串時(shí)候的優(yōu)化技巧
你可以使用+操作符或者String.concat()或者StringBuffer.append()等辦法來(lái)連接多個(gè)字符串,那一種辦法具有最佳的性能呢?
如何作出選擇取決于兩種情景,第一種情景是需要連接的字符串是在編譯期決定的還是在運行期決定的,第二種情景是你使用的是StringBuffer還是String。通常程序員會(huì )認為StringBuffer.append()方法會(huì )優(yōu)于+操作符或String.concat()方法,但是在一些特定的情況下這個(gè)假想是不成立的。
1) 第一種情景:編譯期決定相對于運行期決定
請看下面的StringTest3.java代碼和輸出結果。
package com.performance.string;
/** This class shows the time taken by string concatenation at compile time and run time.*/
public class StringTest3 {
public static void main(String[] args){
//Test the String Concatination
long startTime = System.currentTimeMillis();
for(int i=0;i<5000;i++){
String result = "This is"+ "testing the"+ "difference"+ "between"+
"String"+ "and"+ "StringBuffer";
}
long endTime = System.currentTimeMillis();
System.out.println("Time taken for string concatenation using + operator : "
+ (endTime - startTime)+ " milli seconds");
//Test the StringBuffer Concatination
long startTime1 = System.currentTimeMillis();
for(int i=0;i<5000;i++){
StringBuffer result = new StringBuffer();
result.append("This is");
result.append("testing the");
result.append("difference");
result.append("between");
result.append("String");
result.append("and");
result.append("StringBuffer");
}
long endTime1 = System.currentTimeMillis();
System.out.println("Time taken for String concatenation using StringBuffer : "
+ (endTime1 - startTime1)+ " milli seconds");
}
}
這是上面的代碼的輸出結果:
Time taken for String concatenation using + operator : 0 milli seconds
Time taken for String concatenation using StringBuffer : 50 milli seconds
很有趣地,+操作符居然比StringBuffer.append()方法要快,為什么呢?
這里編譯器的優(yōu)化起了關(guān)鍵作用,編譯器像下面舉例的那樣簡(jiǎn)單地在編譯期連接多個(gè)字符串。它使用編譯期決定取代運行期決定,在你使用new關(guān)鍵字來(lái)創(chuàng )建String對象的時(shí)候也是如此。
編譯前:
String result = "This is"+"testing the"+"difference"+"between"+"String"+"and"+"StringBuffer";
編譯后:
String result = "This is testing the difference between String and StringBuffer";
這里String對象在編譯期就決定了而StringBuffer對象是在運行期決定的。運行期決定需要額外的開(kāi)銷(xiāo)當字符串的值無(wú)法預先知道的時(shí)候,編譯期決定作用于字符串的值可以預先知道的時(shí)候,下面是一個(gè)例子。
編譯前:
public String getString(String str1,String str2) {
return str1+str2;
}
編譯后:
return new StringBuffer().append(str1).append(str2).toString();
運行期決定需要更多的時(shí)間來(lái)運行。
2) 第二種情景:使用StringBuffer取代String
看看下面的代碼你會(huì )發(fā)現與情景一相反的結果――連接多個(gè)字符串的時(shí)候StringBuffer要比String快。
StringTest4.java
package com.performance.string;
/** This class shows the time taken by string concatenation
using + operator and StringBuffer */
public class StringTest4 {
public static void main(String[] args){
//Test the String Concatenation using + operator
long startTime = System.currentTimeMillis();
String result = "hello";
for(int i=0;i<1500;i++){
result += "hello";
}
long endTime = System.currentTimeMillis();
System.out.println("Time taken for string concatenation using + operator : "
+ (endTime - startTime)+ " milli seconds");
//Test the String Concatenation using StringBuffer
long startTime1 = System.currentTimeMillis();
StringBuffer result1 = new StringBuffer("hello");
for(int i=0;i<1500;i++){
result1.append("hello");
}
long endTime1 = System.currentTimeMillis();
System.out.println("Time taken for string concatenation using StringBuffer : "
+ (endTime1 - startTime1)+ " milli seconds");
}
}
這是上面的代碼的輸出結果:
Time taken for string concatenation using + operator : 280 milli seconds
Time taken for String concatenation using StringBuffer : 0 milli seconds
看得出StringBuffer.append()方法要比+操作符要快得多,為什么呢?
原因是兩者都是在運行期決定字符串對象,但是+操作符使用不同于StringBuffer.append()的規則通過(guò)String和StringBuffer來(lái)完成字符串連接操作。(譯注:什么樣的規則呢?)
借助StringBuffer的初始化過(guò)程的優(yōu)化技巧
你可以通過(guò)StringBuffer的構造函數來(lái)設定它的初始化容量,這樣可以明顯地提升性能。這里提到的構造函數是StringBuffer(int length),length參數表示當前的StringBuffer能保持的字符數量。你也可以使用ensureCapacity(int minimumcapacity)方法在StringBuffer對象創(chuàng )建之后設置它的容量。首先我們看看StringBuffer的缺省行為,然后再找出一條更好的提升性能的途徑。
StringBuffer的缺省行為:
StringBuffer在內部維護一個(gè)字符數組,當你使用缺省的構造函數來(lái)創(chuàng )建StringBuffer對象的時(shí)候,因為沒(méi)有設置初始化字符長(cháng)度,StringBuffer的容量被初始化為16個(gè)字符,也就是說(shuō)缺省容量就是16個(gè)字符。當StringBuffer達到最大容量的時(shí)候,它會(huì )將自身容量增加到當前的2倍再加2,也就是(2*舊值+2)。
如果你使用缺省值,初始化之后接著(zhù)往里面追加字符,在你追加到第16個(gè)字符的時(shí)候它會(huì )將容量增加到34(2*16+2),當追加到34個(gè)字符的時(shí)候就會(huì )將容量增加到70(2*34+2)。無(wú)論何事只要StringBuffer到達它的最大容量它就不得不創(chuàng )建一個(gè)新的字符數組然后重新將舊字符和新字符都拷貝一遍――這也太昂貴了點(diǎn)。所以總是給StringBuffer設置一個(gè)合理的初始化容量值是錯不了的,這樣會(huì )帶來(lái)立竿見(jiàn)影的性能增益。
我利用兩個(gè)StringBuffer重新測試了上面的StringTest4.java代碼,一個(gè)未使用初始化容量值而另一個(gè)使用了。這次我追加了50000個(gè)’hello’對象沒(méi)有使用+操作符。區別是我使用StringBuffer(250000)的構造函數來(lái)初始化第二個(gè)StringBuffer了。
輸出結果如下:
Time taken for String concatenation using StringBuffer with out setting size: 280 milli seconds
Time taken for String concatenation using StringBuffer with setting size: 0 milli seconds
StringBuffer初始化過(guò)程的調整的作用由此可見(jiàn)一斑。所以,使用一個(gè)合適的容量值來(lái)初始化StringBuffer永遠都是一個(gè)最佳的建議。
關(guān)鍵點(diǎn)
1. 無(wú)論何時(shí)只要可能的話(huà)使用字符串字面量來(lái)常見(jiàn)字符串而不是使用new關(guān)鍵字來(lái)創(chuàng )建字符串。
2. 無(wú)論何時(shí)當你要使用new關(guān)鍵字來(lái)創(chuàng )建很多內容重復的字符串的話(huà),請使用String.intern()方法。
3. +操作符會(huì )為字符串連接提供最佳的性能――當字符串是在編譯期決定的時(shí)候。
4. 如果字符串在運行期決定,使用一個(gè)合適的初期容量值初始化的StringBuffer會(huì )為字符串連接提供最佳的性能。