面試官:你真的理解String嗎

語言: CN / TW / HK

前幾天後端君在自我提高(摸魚)的時候看到了一個簡單卻也有趣的面試題:String str = new String("abc")這個語句建立了幾個物件?

這是一個非常常見的面試題,個人覺得能很好的甄別候選者Java水平的深度——String類用誰都會用,如果還知道它的底層實現以及原理,那就知道此人不是泛泛之輩,然後可以再深入聊聊JVM記憶體結構等等逐漸拓展開去了。

其實在很多面試題彙總的帖子中可能也都會收錄這個問題,並且給出詳細且準確的回答,在網上搜索這個問題也會有很多答案。那後端君今天說這個的原因就是想從這道面試題入手,和大家一起深入學習一下String這個可以說在Java中最常用的類(沒有之一)。

希望日後無論是在面試中,還是在日常開發中,可以對String類更遊刃有餘。

1. String 的底層結構

首先先來了解一下String的底層結構,在後端君所用的JDK版本1.8中,String類是通過一個char陣列來儲存字串的。

public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
    // 用於儲存字串
    private final char value[];
    // 快取字串雜湊值,預設為0
    private int hash;
    // 省略
}
複製程式碼

可能很多同學也都注意到了String類是被final關鍵字修飾的,用於儲存字串的char陣列也是被final關鍵字修飾的。這樣設計的原因其實是保證了String的不可變性,包括String物件不可被繼承,字元陣列value屬性的引用地址不可修改。

至於為什麼要保證它不可變?別問,問就是設計,JDK工程師們精心的設計!

2. String被final修飾的原因

事實上,String類被設計成被final修飾確實是有它一定的道理的。

首先第一原因是高效,就拿常量池來說,只有變數是不可修改的,才能夠被快取起來,從而實現常量池的功能。同時,被final修飾意味著不可被修改,所以不需要考慮它的值被修改。

第二個原因是安全,Java之父James Gosling解釋過,迫使String類設計成不可變的另一個原因是安全,當你在呼叫其他方法時,比如呼叫一些系統級操作指令之前,可能會有一系列校驗,如果是可變類的話,可能在你校驗過後,它的內部的值又被改變了,這樣有可能會引起嚴重的系統崩潰問題。

在這裡需要著重提到的是,雖然String物件的字元陣列value屬性是不可變的,但只是引用地址不可變,如果直接修改value屬性的內容,還是可以成功的。

final char[] str = {'1','2','3'};
// 直接賦值將 str 陣列的內容修改為{'1','2','4'}
str[2] = '4';
//  通過反射將 str 陣列的內容修改為{'1','2','5'}
java.lang.reflect.Array.set(str, 2, '5');
複製程式碼

以上兩種方法都是沒有改變一個被final修飾的變數的引用地址,而是直接修改引用所代表的陣列元素,成功修改了一個被final修飾的變數的內容。

3. String 的建立流程

明白了String類的底層儲存結構之後,我們再來看它的建立流程,回想一下文字剛開始提到的那個問題,String str = new String("abc")這個語句建立了幾個物件?

再提出一個問題進行對比:String str = "abc"String str = new String("abc")有什麼區別嗎?

在回答這兩個問題之前,我們必須知道一些概念。如果有了解過JVM的同學會知道,虛擬機器中有一個地方叫常量池,它會儲存字串常量,在JDK1.7之後常量池位於Java堆中。在程式中建立的物件例項,也會被存放在Java堆中,但與常量池存放的位置是不一樣的。還有就是,物件的引用變數如上述程式碼中的str,會被存放在虛擬機器棧中。

上面提出的第二個問題說到了String物件的兩種建立方式:直接賦值和new

3.1 直接賦值

首先來說直接賦值,首先會去常量池中尋找abc字串是否存在,若已存在會將str引用變數直接指向常量池中的值。如果不存在,會在常量池中先建立一個abc字串,然後把str指向剛剛創建出來的abc字串。

3.2 new String()

而對於使用new關鍵詞來建立一個String物件,首先虛擬機器會在Java堆中建立一個String物件,然後再去常量池中尋找abc字串是否存在,如果不存在會在常量池中建立一個abc字串,然後把Java堆中的物件引用的值指向在常量池中建立的abc字串;若常量池中已存在abc字串,不會建立該字串,也不會改變Java堆中物件的引用值。

綜上所述,String str = new String("abc")這個語句,會建立1個或2個物件,若常量池中沒有abc字串,那麼會建立2個物件,否則只會在Java堆中建立一個物件。

而直接賦值語句會建立0個或1個物件,若常量池中沒有abc字串,會建立1個物件,否則不會建立物件,只需要將引用指向常量池中的abc字串。

3.3 程式碼示例

我們寫兩個例子驗證一下上面的結論。

public static void main(String[] args) {
    String a = new String("abc");
    String b = "abc";
    System.out.println(a==b);
}
複製程式碼

我們畫一張圖來描述一下示例程式碼中物件之間的關係。

第一行程式碼中使用new String("abc")建立了一個物件,所以會在堆中建立一個value[]物件,而此時常量池不存在abc字串,所以會在常量池中建立此字串,並將value[]物件的引用值指向常量池中的abc字串,但是這兩個值的地址是不一樣的。

第二行程式碼中使用直接賦值的方式,由於常量池中abc字串已經存在,所以b這個引用變數會直接指向常量池中的abc字串。

最後,由於value[]物件的地址與常量池中abc字串的地址是不一樣的,所以ab是不相等的。

4. 面試題

下面再羅列幾道常見的面試題。

  1. ==equals的區別
  2. 編譯器對於String類拼接如何進行優化
  3. String#intern方法的含義
  4. compareToequals都是用於比較,有什麼區別
  5. StringStringBuilderStringBuffer的區別

5. 小結

今天講述了關於String類的幾個方面:底層結構、用final修飾的原因、物件建立流程以及幾道常見的面試題。

如果以後在面試中遇到類似的問題千萬不要答不上來啦!

希望能夠幫助到大家。

版權宣告:本文為Planeswalker23所創,轉載請帶上原文連結,感謝。

本文使用 mdnice 排版