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

面試官:爲什麼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引發的提問,我差點跪了》

推薦閱讀

面試官一步一步的套路你,爲什麼SimpleDateFormat不是線程安全的

都說ThreadLocal被面試官問爛了,可爲什麼面試官還是喜歡繼續問

Java註解是如何玩轉的,面試官和我聊了半個小時

如何去除代碼中的多次if而引發的一連串面試問題

String引發的提問,我差點跪了

就寫了一行代碼,被問了這麼多問題

面試官:JVM對鎖進行了優化,都優化了啥?

synchronized連環問

三分鐘快速搞定git常規使用

點點"在看"   

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章