string+和stringbuffer的速度比較


 

復制代碼
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意識到,工作經驗確實不是白積累的,一個小問題就看出來了。

  這里確實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+的寫法速度更快。

  盡管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+,否則速度會很慢。

 

Java的方法參數傳遞方式

 

  這個問題的引入是當時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當中只有值傳遞,但是這並不影響我們在方法中改變對象參數的屬性值。

  

文章小結

 

  我們平時多了解一些語言的特性確實是有很多好處的,這會潛移默化的影響我們編碼的質量。希望各位猿友在遇到這種問題的時候也自己多寫寫代碼,看看自己的理解對不對,在這樣的過程中進步會很快,尤其是在初次接觸一個編程語言的時候。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM