Java面試鍊金系列 (1) | 關於String類的常見面試題剖析

mio4 發表於 2020-09-13

Java面試鍊金系列 (1) | 關於String類的常見面試題剖析

Java面試鍊金系列 (1)  | 關於String類的常見面試題剖析

文章以及原始碼已被收錄到:https://github.com/mio4/Java-Gold

0x0 基礎知識

1. '==' 運算子

Java中的資料型別分為基本資料型別和引用資料型別:

  1. 基本型別:程式語言中內建的最小粒度的資料型別。它包括四大類八種型別
    • 4種整數型別:byteshortintlong
    • 2種浮點數型別:floatdouble
    • 1種字元型別:char
    • 1種布林型別:boolean
  2. 引用型別:引用也叫控制程式碼,引用型別,是程式語言中定義的在控制程式碼中存放著實際內容所在地址的地址值的一種資料形式,例如:
    • 介面
    • 陣列
  • 對於基本型別來說,== 比較的是它們的值
  • 對於引用型別來說,== 比較的是它們在記憶體中存放的地址(堆記憶體地址)

舉例說明:

    public static void main(String[] args) {
        //基本資料型別
        int num1 = 100;
        int num2 = 100;
        System.out.println("num1 == num2 : " + (num1 == num2) + "\n");

        //引用型別,其中'System.identityHashCode'可以理解為列印物件地址
        String str1 = "mio4";
        String str2 = "mio4";
        System.out.println("str1 address : " + System.identityHashCode(str1));
        System.out.println("str2 address : " + System.identityHashCode(str1));
        System.out.println("str1 == str2 : " + (str1 == str2) + "\n");

        String str3 = new String("mio4");
        String str4 = new String("mio4");
        System.out.println("str3 address : " + System.identityHashCode(str3));
        System.out.println("str4 address : " + System.identityHashCode(str4));
        System.out.println("str3 == str4 : " + (str3 == str4));
    }

執行上面的程式碼,可以得到以下結果:

num1 == num2 : true

str1 address : 1639705018
str2 address : 1639705018
str1 == str2 : true

str3 address : 1627674070
str4 address : 1360875712
str3 == str4 : false

可以看到str1和str2的記憶體地址都是1639705018,所以使用==判斷為true,

但是str3和str4的地址是不同的,所以判斷為false

2. equals()方法

2.1 Object類equals()

在Java語言中,所有類都是繼承於Object這個超類的,在這個類中也有一個equals()方法,那麼我們先來看一下這個方法。

Java面試鍊金系列 (1)  | 關於String類的常見面試題剖析

Java面試鍊金系列 (1)  | 關於String類的常見面試題剖析

可以看得出,這個方法很簡單,就是比較物件的記憶體地址的。所以在物件沒有重寫這個方法時,預設使用此方法,即比較物件的記憶體地址值。但是類似於String、Integer等類均已重寫了equals()。下面以String為例。

2.2 String類equals()

Java面試鍊金系列 (1)  | 關於String類的常見面試題剖析

很明顯,String的equals()方法僅僅是對比它的 資料值,而不是物件的 記憶體地址

String 為例測試一下:

public static void main(String[] args) {
        String str1 = "mio4";
        String str2 = "mio4";

        String str3 = new String("mio4");
        String str4 = new String("mio4");

        System.out.println("str1 address : " + System.identityHashCode(str1));
        System.out.println("str2 address : " + System.identityHashCode(str1));
        System.out.println("str1.equals(str2) : " + str1.equals(str2) + "\n");

        System.out.println("str3 address : " + System.identityHashCode(str3));
        System.out.println("str4 address : " + System.identityHashCode(str4));
        System.out.println("str3.equals(str4) : " + str3.equals(str4) + "\n");
    }

測試輸出為如下,可以看出str3str4地址不同,但是因為String字串內容相同,所以equals判斷為true

str1 address : 1639705018
str2 address : 1639705018
str1.equals(str2) : true

str3 address : 1627674070
str4 address : 1360875712
str3.equals(str4) : true

3. hashCode()方法

3.1 為啥有這個方法?使用場景

Java中的集合(Collection)有三類,一類是List,一類是Queue,集合內的元素是有序的,元素可以重複;再有一類就是Set,一個集合內的元素無序,但元素不可重複。

  • 那麼, 這裡就有一個比較嚴重的問題:要想保證元素不重複,可兩個元素是否重複應該依據什麼來判斷呢? 這就是 Object.equals 方法了。但是,如果每增加一個元素就檢查一次,那麼當元素很多時,後新增到集合中的元素比較的次數就非常多了。 也就是說,如果集合中現在已經有1000個元素,那麼第1001個元素加入集合時,它就要呼叫1000次equals方法。這顯然會大大降低效率。於是,Java採用了雜湊表的原理。 這樣,我們對每個要存入集合的元素使用雜湊演算法算出一個值,然後根據該值計算出元素應該在陣列的位置。所以,當集合要新增新的元素時,可分為兩個步驟:   
    • 先呼叫這個元素的 hashCode 方法,然後根據所得到的值計算出元素應該在陣列的位置。如果這個位置上沒有元素,那麼直接將它儲存在這個位置上;
    • 如果這個位置上已經有元素了,那麼呼叫它的equals方法與新元素進行比較:相同的話就不存了,否則,將其存在這個位置對應的連結串列中(Java 中 HashSet, HashMap 和 Hashtable的實現總將元素放到連結串列的表頭)。

3.2 hashCode()和equals()關聯

 前提: 談到hashCode就不得不說equals方法,二者均是Object類裡的方法。由於Object類是所有類的基類,所以一切類裡都可以重寫這兩個方法。

  • 原則 1 : 如果 x.equals(y) 返回 “true”,那麼 x 和 y 的 hashCode() 必須相等 ;
  • 原則 2 : 如果 x.equals(y) 返回 “false”,那麼 x 和 y 的 hashCode() 有可能相等,也有可能不等 ;
  • 原則 3 : 如果 x 和 y 的 hashCode() 不相等,那麼 x.equals(y) 一定返回 “false” ;
  • 原則 4 : 一般來講,equals 這個方法是給使用者呼叫的,而 hashcode 方法一般使用者不會去呼叫 ;
  • 原則 5 : 當一個物件型別作為集合物件的元素時,那麼這個物件應該擁有自己的equals()和hashCode()設計,而且要遵守前面所說的幾個原則。

總結來說,需要注意的是:

  • equals相等的兩個物件,hashCode一定相等
  • equals方法不相等的兩個物件,hashCode有可能相等

0x1 高頻面試題

1. 看過String原始碼嗎?為啥用final修飾?

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {}

核心解釋:

1.為了實現字串池

2.為了執行緒安全

3.為了實現String可以建立HashCode不可變性

  • final修飾的String,代表了String的不可繼承性,final修飾的char[]代表了被儲存的資料不可更改性。但是:雖然final代表了不可變,但僅僅是引用地址不可變,並不代表了陣列本身不會變。
  • final也可以將陣列本身改變的,這個時候,起作用的還有private,正是因為兩者保證了String的不可變性。
  • 那麼為什麼保證String不可變呢,因為只有當字串是不可變的,字串池才有可能實現。字串池的實現可以在執行時節約很多heap空間,因為不同的字串變數都指向池中的同一個字串。但如果字串是可變的,那麼String.intern()將不能實現,因為這樣的話,如果變數改變了它的值,那麼其它指向這個值的變數的值也會一起改變。
  • 因為字串是不可變的,所以在它建立的時候HashCode就被快取了,不需要重新計算。這就使得字串很適合作為Map中的鍵,字串的處理速度要快過其它的鍵物件。這就是HashMap中的鍵往往都使用字串。

2. String有哪些初始化方式?

String型別的初始化在Java中分為兩類:

  • 一類是通過雙引號包裹一個字元來初始化;
  • 另一類是通過關鍵字new像一個普通的物件那樣初始化一個String例項。

前者在常量池l中開闢一個常量,並返回相應的引用,而後者是在堆中開闢一個常量,再返回相應的物件。所以,兩者的reference肯定是不同的:

public static void main(String... args) {
    String s1 = "abcd";
    String s2 = new String("abcd");
    System.out.println(s1 == s2);   // false
}

而常量池中的常量是可以被共享用於節省記憶體開銷和建立時間的開銷(這也是引入常量池的原因),例如:

public static void main(String... args) {
    String s1 = "abcd";
    String s2 = "abcd";
    System.out.println(s1 == s2);   // true
}

結合這兩者,其實還可以回答另外一個常見的面試題目:

public static void main(String... args) {
    String s = new String("abcd");
}

這句話建立了幾個物件?

首先毫無疑問,"abcd"本身是一個物件,被放於常量池。而由於這裡使用了new關鍵字,所以s得到的物件必然是被建立在heap裡的。所以,這裡其實一共建立了2個物件。

需要注意的是,如果在這個函式被呼叫前的別的地方,已經有了"abcd"這個字串,那麼它就事先在常量池中被建立了出來。此時,這裡就只會建立一個物件,即建立在heap的new String("abcd")物件。

3. String是執行緒安全的嗎?

String是不可變類,一旦建立了String物件,我們就無法改變它的值。因此,它是執行緒安全的,可以安全地用於多執行緒環境中。

4. 為什麼我們在使用HashMap的時候常用String做key?

因為字串是不可變的,當建立字串時,它的它的hashcode被快取下來,不需要再次計算。因為HashMap內部實現是通過key的hashcode來確定value的儲存位置,所以相比於其他物件更快。這也是為什麼我們平時都使用String作為HashMap物件。

5. String的intern()方法是什麼?

String.intern()方法,可以在runtime期間將常量加入到常量池(constant pool)。它的運作方式是:

  1. 如果constant pool中存在一個常量恰好等於這個字串的值,則intern()方法返回這個存在於constant pool中的常量的引用。
  2. 如果constant pool不存在常量恰好等於這個字串的值,則在constant pool中建立一個新的常量,並將這個字串的值賦予這個新建立的在constant pool中的常量。intern()方法返回這個新建立的常量的引用。

示例:

public static void main(String... args) {
    String s1 = "abcd";
    String s2 = new String("abcd");

    /**
     * s2.intern() will first search String constant pool,
     * of which the value is the same as s2.
     */
    String s3 = s2.intern();
    // As s1 comes from constant pool, and s3 is also comes from constant pool, they're same.
    System.out.println(s1 == s3);
    // As s2 comes from heap but s3 comes from constant pool, they're different.
    System.out.println(s2 == s3); 
}

/**
 * Output:
 *  true
 *  false
 */

回顧到最開始的第一部分,為什麼要引入intern()這個函式呢?就是因為,雖然"abcd"是被分配在常量池裡的,但是,一旦使用new String("abcd")就會在heap中新建立一個值為abcd的物件出來。試想,如果有100個這樣的語句,豈不是就要在heap裡建立100個同樣值的物件?!這就造成了執行的低效和空間的浪費。

於是,如果引入了intern()它就會直接去常量池找尋是否有值相同的String物件,這就極大地節省了空間也提高了執行效率。

6. 關於常量池的一些程式設計題(1)

String s1 = "ab";
String s2 = "abc";
String s3 = s1 + "c";

System.out.println(s3 == s2);        //false  不相等,s1是變數,編譯的時候確定不了值,在記憶體中會建立值,s3在堆記憶體中,。s2在常量池,所以不相等。
System.out.println(s3.equals(s2));    //true  比較兩個物件的值相等。

關於上述程式碼的解釋:

String s1 = "abc"; String s2 = "abc";

s1會在常量池中建立,s2先檢視常量池中有沒有,如果有的話就指向它,如果沒有就在常量池中建立一個然後指向它。所以s1和s2的兩種比較是相同的。

Java面試鍊金系列 (1)  | 關於String類的常見面試題剖析

Java面試鍊金系列 (1)  | 關於String類的常見面試題剖析

7. 關於常量池的一些程式設計題(2)

String s1 = new String("Hello");  
String s2 = new String("Hello");

答案是3個物件.

第一,行1 字串池中的“hello”物件。

第二,行1,在堆記憶體中帶有值“hello”的新字串。

第三,行2,在堆記憶體中帶有“hello”的新字串。這裡“hello”字串池中的字串被重用。

8. 淺談一下String, StringBuffer,StringBuilder的區別?

  • String是不可變類,每當我們對String進行操作的時候,總是會建立新的字串。操作String很耗資源,所以Java提供了兩個工具類來操作String :StringBuffer和StringBuilder。
  • StringBuffer和StringBuilder是可變類,StringBuffer是執行緒安全的,StringBuilder則不是執行緒安全的。所以在多執行緒對同一個字串操作的時候,我們應該選擇用StringBuffer。由於不需要處理多執行緒的情況,StringBuilder的效率比StringBuffer高。
  • 引申問題:StringBuffer為啥是執行緒安全的? —StringBuffer裡所有的方法都被synchronized 修飾:
    • Java面試鍊金系列 (1)  | 關於String類的常見面試題剖析

參考|引用

https://blog.csdn.net/justloveyou_/article/details/52464440

https://www.jianshu.com/p/875a3d2b5690

https://www.jianshu.com/p/9c7f5daac283