public class Main{ public static void main(String[] args){ /* 1 */ String string = "a" + "b" + "c"; /* 2 */ StringBuffer stringBuffer = new StringBuffer(); stringBuffer.append("a"); stringBuffer.append("b"); stringBuffer.append("c"); string = stringBuffer.toString(); } }
當時大部分的新手猿友都表示,stringbuffer快於string+。惟有羣裏一位有工做經驗的猿友說,是string+的速度快。這讓LZ意識到,工做經驗確實不是白積累的,一個小問題就看出來了。java
這裏確實string+的寫法要比stringbuffer快,是由於在編譯這段程序的時候,編譯器會進行常量優化,它會將a、b、c直接合成一個常量abc保存在對應的class文件當中。LZ當時在羣裏貼出了編譯後的class文件的反編譯代碼,以下。編程
public class Main { public static void main(String[] args) { String string = "abc"; StringBuffer stringBuffer = new StringBuffer(); stringBuffer.append("a"); stringBuffer.append("b"); stringBuffer.append("c"); string = stringBuffer.toString(); } }
能夠看出,在編譯這個java文件時,編譯器已經直接進行了+運算,這是由於a、b、c這三個字符串都是常量,是能夠在編譯期由編譯器完成這個運算的。假設咱們換一種寫法。小程序
public class Main{ public static void main(String[] args){ /* 1 */ String a = "a"; String b = "b"; String c = "c"; String string = a + b + c; /* 2 */ StringBuffer stringBuffer = new StringBuffer(); stringBuffer.append(a); stringBuffer.append(b); stringBuffer.append(c); string = stringBuffer.toString(); } }
此處的答案貌似應該是stringbuffer更快,由於此時a、b、c都是對象,編譯器已經沒法在編譯期進行提早的運算優化了。安全
可是,事實真的是這樣的嗎?多線程
其實答案依然是第一種寫法更快,也就是string+的寫法更快,這一點可能會有猿友比較疑惑。這個緣由是由於string+實際上是由stringbuilder完成的,而通常狀況下stringbuilder要快於stringbuffer,這是由於stringbuilder線程不安全,少了不少線程鎖的時間開銷,所以這裏依然是string+的寫法速度更快。app
儘管LZ已經解釋了緣由,不過可能仍是有猿友依然不太相信,那麼下面咱們來寫一個測試程序。編程語言
public class Main { public static void main(String[] args) { String a = "a"; String b = "b"; String c = "c"; long start = System.currentTimeMillis(); for (int i = 0; i < 100000000; i++) { String string = a + b + c; if (string.equals("abc")) {} } System.out.println("string+ cost time:" + (System.currentTimeMillis() - start) + "ms"); start = System.currentTimeMillis(); for (int i = 0; i < 100000000; i++) { StringBuffer stringBuffer = new StringBuffer(); stringBuffer.append(a); stringBuffer.append(b); stringBuffer.append(c); String string = stringBuffer.toString(); if (string.equals("abc")) {} } System.out.println("stringbuffer cost time:" + (System.currentTimeMillis() - start) + "ms"); } }
咱們每一個進行了1億次,咱們會看到string+居然真的快於stringbuffer,是否是瞬間被毀了三觀,咱們來看下結果。函數
答案已經很顯然,string+居然真的比stringbuffer要快。這裏其實仍是編譯器搗的鬼,string+事實上是由stringbuilder完成的。咱們來看一下這個程序的class文件內容就能夠看出來了。測試
因爲文件太長,因此LZ是分開截的圖。能夠看到,裏面有兩次stringbuilder的append方法調用,三次stringbuffer的append方法調用。stringbuilder只有兩次append方法的調用,是由於在建立stringbuilder對象的時候,第一個字符串也就是a對象已經被當作構造函數的參數傳入了進去,所以就少了一次append方法。優化
不過請各位猿友不要誤會,這裏stringbuilder之因此比stringbuffer快,是由於少了鎖同步的開銷,而不是由於少了一次append方法,緣由看下面這段stringbuilder類的源碼就知道了。
public StringBuilder(String str) { super(str.length() + 16); append(str); }
能夠看到,實際上帶有string參數的構造方法,依然是使用的append方法,所以stringbuilder其實也進行了三次append方法的調用。
看到這裏,估計有的猿友就該奇怪了,這麼看的話,彷佛string+的速度比stringbuffer更快,難道之前的認識都錯誤了?
答案固然是否認的,咱們來看下面這個小程序,你就看出來差異有多大了。
public class Main { public static void main(String[] args) { String a = "a"; long start = System.currentTimeMillis(); String string = a; for (int i = 0; i < 100000; i++) { string += a; } if (string.equals("abc")) {} System.out.println("string+ cost time:" + (System.currentTimeMillis() - start) + "ms"); start = System.currentTimeMillis(); StringBuffer stringBuffer = new StringBuffer(); for (int i = 0; i < 100000; i++) { stringBuffer.append(a); } if (stringBuffer.toString().equals("abc")) {} System.out.println("stringbuffer cost time:" + (System.currentTimeMillis() - start) + "ms"); } }
這個程序與剛纔的程序有着細微的差異,可是結果卻會讓你大跌眼鏡。咱們來看結果輸出。
看到這個結果是否是直接給跪了,效率差了這麼多?這仍是LZ將循環次數降到了10萬,而不是1億,由於1億次LZ跑了好久也沒跑完,LZ等不急了,0.0。
形成這種狀況的緣由,咱們看兩個程序的區別就看出來了。第一個循環1億次的程序,不論是string+仍是stringbuffer都是在循環體裏構造的字符串,最重要的是string+是由一個語句構造而成的,所以此時string+其實和stringbuffer實際運行的方式是同樣的,只不過string+是使用的stringbuilder而已。
而對於上面這個10萬次循環的程序,stringbuffer就不用說了,實際運行的方式很明顯。而對於string+,它將會創造10萬個stringbuilder對象,每一次循環體的發生,都至關於咱們新建了一個stringbuilder對象,將string對象做爲構造函數的參數,並進行一次append方法和一次toString方法。
由上面幾個小程序咱們能夠看出,在string+寫成一個表達式的時候(更準確的說,是寫成一個賦值語句的時候),效率其實比stringbuffer更快,但若是不是這樣的話,則效率會明顯低於stringbuffer。咱們來再寫一個程序證明這一點。
爲了避免會致使編譯失敗,咱們將循環次數減爲1萬次,不然會超出文件的最大長度,咱們先來看看剛纔的程序改成1萬次循環的結果。
能夠看到,在1萬次的循環下,依然能夠看到效率上的明顯差別,這個差距已經足夠咱們觀察了。如今咱們就改一種寫法,它會讓string+的效率提升到stringbuffer的速度,甚至更快。
這裏咱們是將1萬次字符串的拼接直接寫成了一個表達式,那個a+a+...表達式一共是1萬個(是LZ使用循環打印出來貼到代碼處的),能夠看到,此時string+的速度已經超過了stringbuffer。
所以LZ給各位猿友一個建議,若是是有限個string+的操做,能夠直接寫成一個表達式的狀況下,那麼速度其實與stringbuffer是同樣的,甚至更快,所以有時候不必就幾個字符串操做也要建個stringbuffer(若是中途拼接操做的字符串是線程間共享的,那麼也建議使用stringbuffer,由於它是線程安全的)。可是若是把string+的操做拆分紅語句去進行的話,那麼速度將會指數倍降低。
總之,咱們大部分時候的宗旨是,若是是string+操做,咱們應該儘可能在一個語句中完成。若是是沒法作到,而且拼接動做不少,好比數百上千成萬次,則必須使用stringbuffer,不能用string+,不然速度會很慢。
這個問題的引入是當時LZ在羣裏問了這樣一個問題,就是Java的方法參數傳遞是值傳遞仍是引用傳遞?對於基本類型和對象來講,都會發生什麼狀況?
這道題大部分猿友仍是說的不錯的,包括羣裏的新手猿友。答案是Java只有值傳遞,由於Java只有值傳遞,所以在改變形參的值的時候,實參是不會所以而改變的。這一點從下面這個小程序能夠很明顯的看出來。
public class Main { public static void main(String[] args) { int a = 2; Object object = new Object(); System.out.println(a + ":" + object); change(a, object); System.out.println(a + ":" + object); } public static void change(int a,Object object){ a = 1; object = new Object(); } }
咱們在方法當中改變形參的值,以後再次輸出兩個實參的值,會發現它們無任何變化。
這就足以說明Java只有值傳遞了,不管是對象仍是基本類型,改變形參的值不會反應到實參上面去,這也正是值傳遞的奧義所在。
對於基本類型來講,這一點比較明顯,不過對於對象來說,不少猿友會有誤解。認爲咱們在方法裏改變形參對象屬性的值,是會反映到實參上面去的,所以部分猿友認爲這就是引用傳遞。
首先LZ要強調的是,上面也說了,咱們只是改變形參對象屬性的值,反映到實參上面去的,而不是真的改變了實參的值,也就是說實參引用的對象依然是原來的對象,只不過對象裏的屬性值改變了而已。
針對上面這一點,咱們使用下面這個程序來講明。
public class Main { public static void main(String[] args) { int a = 2; Entity entity = new Entity(); entity.a = 100; System.out.println(a + ":" + entity); System.out.println(entity.a); change(a, entity); System.out.println(a + ":" + entity); System.out.println(entity.a); } public static void change(int a,Entity entity){ a = 1; entity.a = 200; } } class Entity{ int a; }
咱們在方法裏改變了entity對象的屬性值爲200,咱們來看一下結果。
能夠看到,實參對象的值依然沒有改變,只是屬性值變了而已,所以這依舊是值傳遞的範圍。爲了說明這個區別,咱們來看下真正的引用傳遞。因爲Java當中不存在引用傳遞,所以LZ借用C/C++來讓各位看下真正的引用傳遞是什麼效果。
1 #include <stdio.h> 2 3 class Entity{ 4 public: 5 int a; 6 Entity(){}; 7 }; 8 9 void change(int &a,Entity *&entity); 10 11 int main(){ 12 int a = 2; 13 Entity *entity = new Entity(); 14 printf("%d:%p\n",a,entity); 15 change(a, entity); 16 printf("%d:%p\n",a,entity); 17 } 18 19 void change(int &a,Entity *&entity){ 20 a = 1; 21 entity = new Entity(); 22 }
LZ儘可能保持和Java的第一個程序是同樣的結構,只不過C/C++中沒有現成的Object對象,所以這裏使用Entity對象代替,這樣便於各位猿友理解。咱們來看下結果,結果會發現引用傳遞的時候,在方法裏改變形參的值會直接反應到實參上面去。
能夠看到,在引用傳遞的時候,不管是基本類型,仍是對象類型,實參的值都發生了變化,這裏纔是真正的引用傳遞。固然了,LZ對C/C++的理解很是有限,不過毋庸置疑的是,真正的引用傳遞應該是相似上述的現象,也就是說實參會因形參的改變而改變的現象,而這顯然不是咱們Java程序當中的現象。
所以,結論就是Java當中只有值傳遞,可是這並不影響咱們在方法中改變對象參數的屬性值。
咱們平時多瞭解一些語言的特性確實是有不少好處的,這會潛移默化的影響咱們編碼的質量。但願各位猿友在遇到這種問題的時候也本身多寫寫代碼,看看本身的理解對不對,在這樣的過程當中進步會很快,尤爲是在初次接觸一個編程語言的時候。