面試官:你真的理解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 排版