String類相關面試題很難?不要方,本文將讓你徹底明白!

Java中有一個String類,特別讓人傷腦筋。因為它可以直接賦值,也可以new一下用構造器生成對象,還可以用加號拼接……這些不同的方式到底有什麼區別?本文是個人學習的一些總結,也希望能用最通俗的語言讓大家明白這個類。

一、字符串的創建:

字符串創建有兩種方式,分別來看看這兩種方式有何區別:

1. 字面量賦值創建:

String str1 = "hello";String str2 = "hello";String str3 = "world";

這樣創建字符串,首先會去常量池裡找有沒有這個字符串,有就直接指向常量池的該字符串,沒有就先往常量池中添加一個,再指向它。圖解:

String類相關面試題很難?不要方,本文將讓你徹底明白!

2. 用new創建:

String str1 = new String("hello");String str2 = new String("hello");String str3 = new String("world");

new一個字符串時,做了兩件事。首先在堆中生成了該字符串對象,然後去看常量池中有沒有該字符串,如果有就不管了,沒有就往常量池中添加一個。圖解:

String類相關面試題很難?不要方,本文將讓你徹底明白!

所以當問到“執行上面那三行代碼創建了幾個對象”這樣的問題就很簡單了,堆中三個常量池中兩個,總共是5個。

小結:這兩種方式創建出來的,一個在堆中,一個在常量池中,所以它們之間用 == 比較肯定是false。

二、字符串的拼接:

字符串可以直接用加號進行拼接,但是也有幾種不同的情況。

1. 常量拼接

String str = "hello" + "world";

對於這種加號兩邊都是常量的,在編譯階段就會自動拼接,變成

String str = "helloworld";

所以就會去常量池找"helloworld",有就直接指向它,沒有就在常量池創建再指向。

2. 有final的拼接:

final String str1 = "hello";final String str2 = "world";String str3 = str1 + str2;

因為final修飾的變量就是常量,所以在編譯期直接會變成

String str3 = "hello" + "world";

再根據常量拼接規則可知最終就變成

String str3 = "helloworld";

3. 變量和常量拼接:變量和常量拼接的時候,底層會調用StringBuilder的append方法生成新對象。

  • 情況一:

String str1 = "hello";String str2 = str1 + "world";

str1顯然是在常量池中的,world也是在常量池中的,然後調用append方法在堆中生成新對象"helloworld",str2就指向堆中的"helloworld"對象。所以這兩條語句總共生成了3個對象,常量池中有"hello"和"world",堆中有"helloword"。

  • 情況二:

String str1 = new String("hello");String str2 = str1 + "world";

首先會在堆中創建一個"hello",再把"hello"添加到常量池;然後會把"world"添加到常量池,拼接的時候,會在堆中創建一個"helloworld"。所以這兩條語句總共創建了4個對象,堆中的"hello"、"helloworld"和常量池中的"hello"、"world"。

4. 變量和變量拼接:變量和變量拼接,底層也會調用StringBuilder的append方法生成新對象。

  • 情況一:

String str1 = "hello";String str2 = "world";String str3 = str1 + str2;

這段代碼,首先會有一個"hello"在常量池中,然後有個"world"在常量池,第三行代碼會調用append方法,在堆中生成一個"helloworld"。所以總共有3個對象。

  • 情況二:

String str1 = "hello";String str2 = new String("world");String str3 = str1 + str2;

這段代碼,首先在常量池中搞一個"hello",然後在堆中new一個"world",同時把"world"也搞到常量池中去,第三步拼接就會在堆中生成一個"helloworld"。所以總共有4個對象。

  • 情況三:

String str1 = new String("hello");String str2 = new String("world");String str3 = str1 + str2;

第一行代碼創建了兩個對象,堆中一個常量池一個,第二行代碼也是一樣,第三行代碼就在堆中創建了一個"helloworld"。所以總共創建了5個對象。

三、intern方法:

1、Java 1.7以前:JDK 1.7以前,intern方法會把對象拷貝到常量池。看下面例子:

  • 例一:

String str1 = new String("str")+new String("01"); str1.intern(); String str2 = "str01"; System.out.println(str2==str1);

圖解上述代碼:

String類相關面試題很難?不要方,本文將讓你徹底明白!

首先 newString("str")會在堆中創建str,同時添加到常量池;newString("01")也是一樣的,在堆中創建01,同時添加到常量池;然後兩者拼接,底層用的append方法,在堆中生成一個str01;然後 str1.intern(),就把str01拷貝到常量池了;此時運行到 Stringstr2="str01",發現常量池中有了,所以直接指向常量池中的str01。最終str1指向堆中的str01對象,str2指向常量池的str01對象,所以結果是false。

  • 例二:

String str1 = new String("str")+new String("01"); String str2 = "str01"; str1.intern(); System.out.println(str2==str1);

我們將第二三行代碼調換順序,看看情況有什麼不同:

String類相關面試題很難?不要方,本文將讓你徹底明白!

換一下順序,區別就在於執行到第二行代碼的時候,常量池中就已經有str01了,所以再執行 str1.intern()的時候,就沒有再進行拷貝了。最終還是str1指向堆中的str01,str2指向常量池的str01,所以結果還是false。

2、JDK1.7以後(包括1.7):從JDK 1.7開始,intern方法做了些改變,進行拷貝的時候不是拷貝對象,而是拷貝地址值。看下面的例子:

  • 例一:

String str1 = new String("str")+new String("01");str1.intern();String str2 = "str01";System.out.println(str2==str1);

圖解上述代碼:

String類相關面試題很難?不要方,本文將讓你徹底明白!

第一步和JDK 1.7之前是一樣的,現在堆中創建一個str,同時搞到常量池,再創建一個01,同時搞到常量池,然後拼接,在堆中生成對象str01;不同的就是 str1.intern(),這次拷貝的不是str01這個對象,而是把它的地址值搞到常量池中去了;然後執行 Stringstr2=str01的時候,去常量池找str01,發現常量池中有 x001地址值,剛好該地址值對應的就是要找的str01,就直接拿過來用。最終就是str1指向地址值為 x001的對象,str2也是指向地址值為 x001的對象,所以結果是true。

  • 例二:

String str1 = new String("str")+new String("01");String str2 = "str01";str1.intern();System.out.println(str2==str1);

同樣將二三行代碼換一下位置,看看是什麼情況:

String類相關面試題很難?不要方,本文將讓你徹底明白!

第一步就不多說了,執行第二步時,往常量池中找str01,發現沒有,那就添加一個;再執行 str1.intern()時,發現常量池中有str01了,就不進行地址值的拷貝了。最終str1指向堆中的str01,str2指向常量池的str01,所以結果是false。

  • 例三:

String str1 = new String("str")+new String("01");String str2 = "str01";str1 = str1.intern();System.out.println(str2==str1);

就是把例二的 str1.intern()改成 str1=str1.intern(),看看會有什麼變化:

String類相關面試題很難?不要方,本文將讓你徹底明白!

本來str1是指向堆中的str01的,然後重新將 str1.intern()賦給str1, str1.intern()是指向常量池的,賦給str1後,所以此時str1也是指向常量池。所以結果就是true。

四、String、StringBuilder和StringBuffer:

String和後兩者的區別就是String是不可變的,後兩者可變。StringBuilder是JDK 1.5以後提供的,以前用StringBuffer。StringBuffer和StringBuilder的功能基本一樣,只是StringBuffer是線程安全的,而StringBuilder不是線程安全的。因此,StringBuilder的效率會更高。

上面字符串拼接部分的案例都是用加號拼接的,然後也提到了StringBuilder的append方法。其實就算是加號拼接,底層還是用的StringBuilder的append方法。看下面代碼:

String s = "abc"; String ss = "ok" + s + "xyz" + 5;

這就用加號拼接的例子,利用反編譯工具看看這段代碼到底編譯成了啥:

String s = "abc";String ss = (new StringBuilder("ok")).append(s).append("xyz").append(5).toString();

可看到,編譯後是用StringBuilder的append方法進行拼接的。那麼使用加號和使用append方法到底有什麼區別呢?看一下以下代碼:

String s = ""; Random rand = new Random(); for (int i = 0; i < 10; i++){ s = s + rand.nextInt(1000) + " "; } System.out.println(s);

這個例子很簡單,就是在循環裡面用加號進行字符串的拼接,看一下反編譯後是什麼樣子的:

String s = ""; Random rand = new Random(); for(int i = 0; i < 10; i++) { s = (new StringBuilder(String.valueOf(s))).append(rand.nextInt(1000)).append(" ").toString(); }System.out.println(s);

可以看到,它是在循環裡面new了StringBuilder對象,然後用其append方法進行拼接。這裡是i從0到9,也就是說要new十次,會創建十個對象,這樣就會佔用大量的資源。所以要讓其編譯後創建StringBuilder對象的過程在循環外面,代碼就該這樣寫:

String s = ""; Random rand = new Random();StringBuilder result = new StringBuilder();for (int i = 0; i < 10; i++){ result.append(rand.nextInt(1000)); result.append(" ");} System.out.println(result.toString());

那麼編譯後就是這樣的:

String s = ""; Random rand = new Random(); StringBuilder result = new StringBuilder(); for(int i = 0; i < 10; i++) { result.append(rand.nextInt(1000)); result.append(" "); }System.out.println(result.toString());

這樣就沒有在循環裡面new對象了。

小結:當要在循環裡面進行字符串拼接的時候,就該先在循環外面new一個StringBuilder,然後在循環裡面用append進行拼接;其他情況就可以使用加號進行拼接更加簡單。

總結:

本文用圖文形式講了String的面試考點,特別要注意JDK版本不同intern方法的差異。還有就是常量池的位置到底在方法區還是在堆中還是在元空間,這個我也不是很清楚,網上搜索的答案也比較雜。以上內容如果有誤,歡迎批評指正!


最後我自己是一名從事了多年開發的JAVA老程序員,今年年初我花了一個月整理了一份最適合2019年學習的java學習乾貨,想分享給每一位喜歡java的小夥伴,需要獲取的可以關注我並在後臺私信我:01,即可免費領取。


分享到:


相關文章: