標籤:
看下面這段代碼:
public class Main { public static void main(String[] args) { String string = ""; for(int i=0;i<10000;i++){ string += "hello"; } }}
這句 string += "hello";的過程相當於將原有的string變數指向的對象內容取出與"hello"作字串相加操作再存進另一個新的String對象當中,再讓string變數指向新產生的對象。
從這段反編譯出的位元組碼檔案可以很清楚地看出:從第8行開始到第35行是整個迴圈的執行過程,並且每次迴圈會new出一個StringBuilder對象,然後進行append操作,最後通過toString方法返回String對象。也就是說這個迴圈執行完畢new出了10000個對象,試想一下,如果這些對象沒有被回收,會造成多大的記憶體資源浪費。從上面還可以看出:string+="hello"的操作事實上會自動被JVM最佳化成:
StringBuilder str = new StringBuilder(string);str.append("hello");str.toString();
再看下面這段代碼:
public class Main { public static void main(String[] args) { StringBuilder stringBuilder = new StringBuilder(); for(int i=0;i<10000;i++){ stringBuilder.append("hello"); } }}
反編譯位元組碼檔案得到:
從這裡可以明顯看出,這段代碼的for迴圈式從13行開始到27行結束,並且new操作只進行了一次,也就是說只產生了一個對象,append操作是在原有對象的基礎上進行的。因此在迴圈了10000次之後,這段代碼所佔的資源要比上面小得多。
那麼有人會問既然有了StringBuilder類,為什麼還需要StringBuffer類?查看原始碼便一目瞭然,事實上,StringBuilder和StringBuffer類擁有的成員屬性以及成員方法基本相同,區別是StringBuffer類的成員方法前面多了一個關鍵字:synchronized,不用多說,這個關鍵字是在多線程訪問時起到安全保護作用的,也就是說StringBuffer是安全執行緒的。
下面摘了2段代碼分別來自StringBuffer和StringBuilder,insert方法的具體實現:
StringBuilder的insert方法
public StringBuilder insert(int index, char str[], int offset, int len) { super.insert(index, str, offset, len); return this; }
StringBuffer的insert方法:
public synchronized StringBuffer insert(int index, char str[], int offset, int len) { super.insert(index, str, offset, len); return this; }
效能測試
public class Main { private static int time = 50000; public static void main(String[] args) { testString(); testStringBuffer(); testStringBuilder(); test1String(); test2String(); } public static void testString () { String s=""; long begin = System.currentTimeMillis(); for(int i=0; i<time; i++){ s += "java"; } long over = System.currentTimeMillis(); System.out.println("操作"+s.getClass().getName()+"類型使用的時間為:"+(over-begin)+"毫秒"); } public static void testStringBuffer () { StringBuffer sb = new StringBuffer(); long begin = System.currentTimeMillis(); for(int i=0; i<time; i++){ sb.append("java"); } long over = System.currentTimeMillis(); System.out.println("操作"+sb.getClass().getName()+"類型使用的時間為:"+(over-begin)+"毫秒"); } public static void testStringBuilder () { StringBuilder sb = new StringBuilder(); long begin = System.currentTimeMillis(); for(int i=0; i<time; i++){ sb.append("java"); } long over = System.currentTimeMillis(); System.out.println("操作"+sb.getClass().getName()+"類型使用的時間為:"+(over-begin)+"毫秒"); } public static void test1String () { long begin = System.currentTimeMillis(); for(int i=0; i<time; i++){ String s = "I"+"love"+"java"; } long over = System.currentTimeMillis(); System.out.println("字串直接相加操作:"+(over-begin)+"毫秒"); } public static void test2String () { String s1 ="I"; String s2 = "love"; String s3 = "java"; long begin = System.currentTimeMillis(); for(int i=0; i<time; i++){ String s = s1+s2+s3; } long over = System.currentTimeMillis(); System.out.println("字串間接相加操作:"+(over-begin)+"毫秒"); } }
結果
上面提到string+="hello"的操作事實上會自動被JVM最佳化,看下面這段代碼:
public class Main { private static int time = 50000; public static void main(String[] args) { testString(); testOptimalString(); } public static void testString () { String s=""; long begin = System.currentTimeMillis(); for(int i=0; i<time; i++){ s += "java"; } long over = System.currentTimeMillis(); System.out.println("操作"+s.getClass().getName()+"類型使用的時間為:"+(over-begin)+"毫秒"); } public static void testOptimalString () { String s=""; long begin = System.currentTimeMillis(); for(int i=0; i<time; i++){ StringBuilder sb = new StringBuilder(s); sb.append("java"); s=sb.toString(); } long over = System.currentTimeMillis(); System.out.println("類比JVM最佳化操作的時間為:"+(over-begin)+"毫秒"); } }
得到驗證。
下面對上面的執行結果進行一般性的解釋:
1)對於直接相加字串,效率很高,因為在編譯器便確定了它的值,也就是說形如"I"+"love"+"java"; 的字串相加,在編譯期間便被最佳化成了"Ilovejava"。這個可以用javap -c命令反編譯產生的class檔案進行驗證。
對於間接相加(即包含字串引用),形如s1+s2+s3; 效率要比直接相加低,因為在編譯器不會對引用變數進行最佳化。
2)String、StringBuilder、StringBuffer三者的執行效率:
StringBuilder > StringBuffer > String
當然這個是相對的,不一定在所有情況下都是這樣。
比如String str = "hello"+ "world"的效率就比 StringBuilder st = new StringBuilder().append("hello").append("world")要高。
因此,這三個類是各有利弊,應當根據不同的情況來進行選擇使用:
當字串相加操作或者改動較少的情況下,建議使用 String str="hello"這種形式;
當字串相加操作較多的情況下,建議使用StringBuilder,如果採用了多線程,則使用StringBuffer。
關於String、StringBuffer的面試題
1. 下面這段代碼的輸出結果是什嗎?
String a = "hello2"; String b = "hello" + 2; System.out.println((a == b));
輸出結果為:true。原因很簡單,"hello"+2在編譯期間就已經被最佳化成"hello2",因此在運行期間,變數a和變數b指向的是同一個對象。
2.下面這段代碼的輸出結果是什嗎?
String a = "hello2"; String b = "hello"; String c = b + 2; System.out.println((a == c));
輸出結果為:false。由於有符號引用的存在,所以 String c = b + 2;不會在編譯期間被最佳化,不會把b+2當做字面常量來處理的,因此這種方式產生的對象事實上是儲存在堆上的。因此a和c指向的並不是同一個對象。javap -c得到的內容:
3.下面這段代碼的輸出結果是什嗎?
String a = "hello2"; final String b = "hello"; String c = b + 2; System.out.println((a == c));
輸出結果為:true。對於被final修飾的變數,會在class檔案常量池中儲存一個副本,也就是說不會通過串連而進行訪問,對final變數的訪問在編譯期間都會直接被替代為真實的值。那麼String c = b + 2;在編譯期間就會被最佳化成:String c = "hello" + 2;
4.下面這段代碼輸出結果為:
public class Main { public static void main(String[] args) { String a = "hello2"; final String b = getHello(); String c = b + 2; System.out.println((a == c)); } public static String getHello() { return "hello"; }}
輸出結果為false。這裡面雖然將b用final修飾了,但是由於其賦值是通過方法調用返回的,那麼它的值只能在運行期間確定,因此a和c指向的不是同一個對象。
5.下面這段代碼的輸出結果是什嗎?
public class Main { public static void main(String[] args) { String a = "hello"; String b = new String("hello"); String c = new String("hello"); String d = b.intern(); System.out.println(a==b); System.out.println(b==c); System.out.println(b==d); System.out.println(a==d); }}
這裡面涉及到的是String.intern方法的使用。在String類中,intern方法是一個本地方法,在JAVA SE6之前,intern方法會在運行時常量池中尋找是否存在內容相同的字串,如果存在則返回指向該字串的引用,如果不存在,則會將該字串入池,並返回一個指向該字串的引用。因此,a和d指向的是同一個對象。
6.String str = new String("abc")建立了多少個對象
這個問題自身就沒有合理的答案,大部分網上流傳的以及一些面試書籍上都說是2個對象(一個是“xyz”,一個是指向“xyz”的引用對象s),這種說法是片面的。
這問題的毛病是什麼呢?它並沒有定義“建立了”的意義。
什麼叫“建立了”?什麼時候建立了什嗎?
而且這段Java程式碼片段實際啟動並執行時候真的會“建立兩個String執行個體”嗎?
如果這道是面試題,那麼可以當面讓面試官澄清“建立了”的定義,然後再對應的回答。這種時候面試官多半會讓被面試者自己解釋,那就好辦了,好好曬給面試官看。
如果是筆試題就沒有提問要求澄清的機會了。不過會出這種題目的地方水平多半也不怎麼樣。說不定出題的人就是從各種寶典上把題抄來的,那就按照寶典把那不大對勁的答案寫上去就能混過去了
先換成另一個問題來問:
String s = new String("xyz"); 在運行時涉及幾個String執行個體?
一種合理的解答是:
兩個,一個是字串字面量"xyz"所對應的、駐留(intern)在一個全域共用的字串常量池中的執行個體,另一個是通過new String(String)建立並初始化的、內容與"xyz"相同的執行個體
也就是規定了Java語言一般是編譯為Java虛擬機器規範所定義的Class檔案,但並沒有規定“一定”(must),留有不使用JVM來實現Java語言的餘地。
考慮上Java虛擬機器規範,確實在這段代碼裡涉及的常量種類為CONSTANT_String_info的字串常量也只有"xyz"一個。CONSTANT_String_info是用來表示Java語言中String類型的常量運算式的值(包括字串字面量)用的常量種類,只在這個層面上考慮的話,這個解答也沒問題。
所以這種解答可以認為是合理的。
值得注意的是問題中“在運行時”既包括類載入階段,也包括這個程式碼片段自身執行的時候。
String s = new String("xyz"); 涉及使用者聲明的幾個String類型的變數?
一個,就是String s。
String s = null; 涉及使用者聲明的幾個String類型的變數?
Java裡變數就是變數,參考型別的變數只是對某個對象執行個體或者null的引用,不是執行個體本身。聲明變數的個數跟建立執行個體的個數沒有必然關係,像是說:
String s1 = "a"; String s2 = s1.concat(""); String s3 = null; new String(s1);
這段代碼會涉及3個String類型的變數,
1、s1,指向下面String執行個體的1
2、s2,指向與s1相同
3、s3,值為null,不指向任何執行個體
以及3個String執行個體,
1、"a"字面量對應的駐留的字串常量的String執行個體
2、""字面量對應的駐留的字串常量的String執行個體
(String.concat()是個有趣的方法,當發現傳入的參數是Null 字元串時會返回this,所以這裡不會額外建立新的String執行個體)
3、通過new String(String)建立的新String執行個體;沒有任何變數指向它。
....具體討論查閱http://rednaxelafx.iteye.com/blog/774673/
7.下面這段代碼1)和2)的區別是什嗎?
public class Main { public static void main(String[] args) { String str1 = "I"; //str1 += "love"+"java"; 1) str1 = str1+"love"+"java"; //2) }}
1)的效率比2)的效率要高,1)中的"love"+"java"在編譯期間會被最佳化成"lovejava",而2)中的不會被最佳化。
Java基礎-String、StringBuffer、StringBuilder