面試官:爲什麼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被面試官問爛了,可爲什麼面試官還是喜歡繼續問
點點"在看"