前幾天後端君在自我提高(摸魚)的時候看到了一個簡單卻也有趣的面試題: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 排版