沒使用加號拼接字元串,面試官竟然問我為什麼
- 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引發的提問,我差點跪了》