­

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

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

文章以及源程式碼已被收錄到://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()方法,那麼我們先來看一下這個方法。

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

2.2 String類equals()

很明顯,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的兩種比較是相同的。

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 修飾:

參考|引用

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

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

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