前幾天後端君在自我提高(摸魚)的時候看到了一個簡單卻也有趣的面試題: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
字串的地址是不一樣的,所以a
與b
是不相等的。
4. 面試題
下面再羅列幾道常見的面試題。
==
和equals
的區別編譯器對於 String
類拼接如何進行優化String#intern
方法的含義compareTo
和equals
都是用於比較,有什麼區別String
、StringBuilder
和StringBuffer
的區別
5. 小結
今天講述了關於String
類的幾個方面:底層結構、用final
修飾的原因、物件建立流程以及幾道常見的面試題。
如果以後在面試中遇到類似的問題千萬不要答不上來啦!
希望能夠幫助到大家。
版權宣告:本文為Planeswalker23所創,轉載請帶上原文連結,感謝。
本文使用 mdnice 排版