沒使用加號拼接字元串,面試官竟然問我為什麼

  • 2020 年 3 月 31 日
  • 筆記

面試官:為什麼String設計成不可變的?

小小白:主要是為了確保String對象中存儲的值不會被改變,充分利用字元串常量池的優化策略,同時字元串對象的hashCode也不會被改變。如果String設計成可變的,那麼自定義的類就可以通過集成String,重寫其中的方法將其存儲的值改變。如果String是可變的,將String類型變數作為參數傳遞的過程中,存儲的將有可能會被改變,這樣會導致安全隱患。

面試官:平時編碼過程中字元串的拼接使用什麼?

小小白:如果不會出現多執行緒並發的情況,使用StringBuilder;如果會出現多執行緒並發的情況,使用StringBuffer。

面試官:為什麼不使用加號(+)?

小小白:StringBuffer就不對比說了,它比StringBuilder多了執行緒安全控制,同樣的方法使用synchronized控制並發訪問,性能上必定會比StringBuilder差。舉一個使用樣例就能看出差別,下面的程式碼執行就會發現,使用StringBuilder會比加號的方式快很多(忽略輸出中的字元串拼接方式)。

// 加號方式拼接字元串  long startTimeInMillis =  Calendar.getInstance().getTimeInMillis();  String result = "start:";  for(int i = 0; i < 10000; i++){      result = result + i;  }  long endTimeInMillis =  Calendar.getInstance().getTimeInMillis();  long executeTimeInMillis = endTimeInMillis - startTimeInMillis;  System.out.println("executeTimeInMillis:" + executeTimeInMillis);

輸出結果:executeTimeInMillis:342

// StringBuilder#append方法拼接字元串  long startTimeInMillis = Calendar.getInstance().getTimeInMillis();  StringBuilder result = new StringBuilder("start:");  for (int i = 0; i < 10; i++) {      result.append(i);  }  long endTimeInMillis = Calendar.getInstance().getTimeInMillis();  long executeTimeInMillis = endTimeInMillis - startTimeInMillis;  System.out.println("executeTimeInMillis:" + executeTimeInMillis);

輸出結果:executeTimeInMillis:18

面試官:JDK不是已經對字元串使用加號的拼接做優化了嗎,為什麼還是會慢?

小小白:使用JDK8編譯使用加號方式拼接字元串的程式碼,然後使用javap -c命令反編譯class文件,結果如下:

Code:         0: aload_0         1: invokespecial #1                  // Method java/lang/Object."<init>":()V         4: return      public static void main(java.lang.String[]);      Code:         0: invokestatic  #2                  // Method java/util/Calendar.getInstance:()Ljava/util/Calendar;         3: invokevirtual #3                  // Method java/util/Calendar.getTimeInMillis:()J         6: lstore_1         7: ldc           #4                  // String start:         9: astore_3        10: iconst_0        11: istore        4        13: iload         4        15: sipush        10000        18: if_icmpge     47        21: new           #5                  // class java/lang/StringBuilder        24: dup        25: invokespecial #6                  // Method java/lang/StringBuilder."<init>":()V        28: aload_3        29: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;        32: iload         4        34: invokevirtual #8                  // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;        37: invokevirtual #9                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;        40: astore_3        41: iinc          4, 1        44: goto          13        47: invokestatic  #2                  // Method java/util/Calendar.getInstance:()Ljava/util/Calendar;        50: invokevirtual #3                  // Method java/util/Calendar.getTimeInMillis:()J        53: lstore        4        55: getstatic     #10                 // Field java/lang/System.out:Ljava/io/PrintStream;        58: new           #5                  // class java/lang/StringBuilder        61: dup        62: invokespecial #6                  // Method java/lang/StringBuilder."<init>":()V        65: ldc           #11                 // String executeTimeInMillis:        67: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;        70: lload         4        72: lload_1        73: lsub        74: invokevirtual #12                 // Method java/lang/StringBuilder.append:(J)Ljava/lang/StringBuilder;        77: invokevirtual #9                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;        80: invokevirtual #13                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V        83: return

從上面的21可以看到,在這一行new了一個StringBuilder對象,然後開始執行對象的初始化和字元串的拼接append方法,注意看44,這裡執行了goto 13,就是轉到13繼續執行,從13開始會發現,後面還是new了一個StringBuilder對象,然後執行對象的初始化和字元串的拼接append方法,接著繼續goto 13,也就是說循環體中不斷的創建StringBuilder對象,調用append方法實現字元串的拼接。雖然,JDK編譯的時候使用StringBuilder優化了原來通過不斷創建新String對象的拼接字元串的方式,但是在循環體中不斷創建對象的方式不是最優的,而且這樣頻繁創建對象會可能會觸發GC。所以,顯然直接使用StringBuilder#append方法會高效一些。

面試官:那是不是都不能使用加號(+)的方式拼接字元串?

小小白:也不是的。如果是簡單的靜態字元串拼接(拼接中不需要動態的計算字元串值),可以使用加號的方式,因為編譯器在編譯階段會聰明的計算出結果。

面試官:下面程式碼的運行結果又是什麼?

String t0 = new String("hello") + new String("world");  t0.intern();  String t1 = "helloworld";  System.out.println(t0 == t1);

小小白:JDK1.7之前的版本為false,JDK1.7開始為true。

面試官:為什麼結果不同?

請點擊閱讀《String引發的提問,我差點跪了》