關於 Java String,這是面試的基礎,但是還有很多童鞋不能說清楚,所以本文將簡單而又透徹的說明一下那個讓你迷惑的 String

在 Java 中,我們有兩種方式創建一個字符串
String x = "abc";
String y = new String("abc");
你常見也常寫第一種,很少見第二種,但面試還總問這類問題,雙引號和構造器兩種形式創建字符串到底有什么差別呢?
先來看例子
例子 1
String a = "abcd";
String b = "abcd";
System.out.println(a == b); // True
System.out.println(a.equals(b)); // True
a == b 結果為 true,是因為 a 和 b 都指向 方法區(method area) 同一個字符串文字,內存引用是同一個
當多次創建相同的字符串文字時,只存儲每個不同字符串值的一個副本。這個叫做字符串留駐/留用,Java 中所有編譯期字符串常量都會被自動留駐
例子 2
String c = new String("abcd");
String d = new String("abcd");
System.out.println(c == d); // False
System.out.println(c.equals(d)); // True
c==d 結果為 false,因為 c 和 d 的引用指向堆中不同的對象,不同的對象肯定有不同的內存引用
舉了兩個例子,文字描述有點懵?我們來試圖通過圖形來理解上述兩種情況:

也許你已經看看出來了,一個是在方法區,一個是在堆中,在 JVM 模型中這是兩個不同的區域,也許你面試時也經常被問到吧,來看下圖:

再次提醒一下,所有 new 的對象都會在 Heap 中,這樣以后你就好區分了
運行期字符串留駐
上面說的字符串留駐是在編譯期,那么運行期可以嗎?答案是肯定的,我們需要一個函數來幫忙
String c = new String("abcd").intern();
String d = new String("abcd").intern();
System.out.println(c == d); // Now true
System.out.println(c.equals(d)); // True
看到 c == d 結果為 true,你應該理解 intern (英文有拘留,軟禁的意思)的作用了,通過調用 intern()方法,就好比把創建的字符串拘留在方法區一樣了
在面試時甚至還會問你下面代碼創建了幾個對象:
String d = new String("abcd")
- 如果方法區已存在"abcd", 那么只創建一個 new String 的對象
- 如果方法區沒有"abcd", 那么要創建兩個對象,一個在方法區,一個在堆中
所以,正常情況下我們沒必要使用構造器創建對象,因為這很可能會產生一個額外的沒用的對象,但是有例外哦,我們下面說
String s = "abcd";
s = s.concat("ef");

當我們想在字符串 s 后面拼接字符"ef"時,會在堆中創建一個新的對象,並將 s 的引用指向新創建的對象,由於 String 創建的是不可變對象,所以 String 類中的所有方法都不會改變它自身,而是返回一個新的字符串(快打開你的 IDE,看看是否每個操作String 的方法最后都是返回有 return new String 字樣),到這里你也應該理解了一個道理:
如果我們需要一個字符串被修改,我們最好使用 StringBuffer 或者 StringBuilder,否則,由於每次操作字符串都會創建一個新的對象,而舊的對象不會有引用指向它,這樣我們會浪費很多垃圾回收的時間
到這里還沒完,你有沒有想過為什么 String 會被設置/制造成 final?
為什么 String 類被 final 修飾
談及這個問題我們需要一些倒推的或者相互約束思維來思考
字符串池的需求
字符串池(String intern pool)是方法區域中的一個特殊存儲區域。當創建一個字符串時,如果該字符串已經存在於池中,那么返回現有字符串的引用,而不是創建一個新對象。所以說,如果一個字符串是可變的,那么改變一個引用的值,將導致原本指向該值的引用獲取到錯誤的值
緩存 hashcode
字符串的hashcode在Java中經常使用。例如,在HashMap或HashSet中。不可變保證hashcode始終是相同的,這樣就可以在不擔心更改的情況下兌現它。這意味着,不需要每次使用hashcode時都計算它。這樣更有效率。所以你會在 String 類中看到下面的成員變量的定義:
/** Cache the hash code for the string */
private int hash; // Default to 0
安全性
String被廣泛用作許多java類的參數,例如網絡連接、打開文件等。如果字符串不是不可變的,連接或文件將被更改,這可能導致嚴重的安全威脅。該方法認為它連接到一台機器上,但實際上並沒有。可變字符串也可能導致反射中的安全問題,因為參數是字符串。
不可變對象天生是線程安全的
由於不可變對象不能被更改,所以它們可以在多個線程之間自由共享。這消除了同步的需求。
總之,出於效率和安全性的考慮,String 被設計為不可變的。這也是為什么在一般情況下,不可變類是首選的原因。
附加說明
關於不可變對象和不可變引用總是有同學搞不清楚
final User user = new User();
上面的代碼指的是 user 引用不能被更改指向內存的其他地址,但是由於 User 是可變對象,我們可以調用 user 的 setter 方法修改其屬性
在String類中包含很多學問,包括你對JVM模型的理解,這也就是為什么面試官為什么喜歡問String,主要考察你的基本功
靈魂追問
- String 和基本類型的包裝類如 Integer 和 Long 都被 final 修飾,但為什么不建議作為 synchronized 同步塊的參數適用呢?
- 基本類型自動裝箱你知道發生了什么嗎?和上一個問題有關系
提高效率工具

Material Theme UI
這是一款 IDEA 的主題插件,安裝后,選擇 Material Palenight 主題,同時作出如下設置

設置完后,你的 IDEA 就是下面這樣,引起極度舒適

推薦閱讀
- 每天用SpringBoot,還不懂RESTful API返回統一數據格式是怎么實現的?
- 雙親委派模型:大廠高頻面試題,輕松搞定
- 面試還不知道BeanFactory和ApplicationContext的區別?
- 如何設計好的RESTful API
- 紅黑樹,超強動靜圖詳解,簡單易懂
歡迎持續關注公眾號:「日拱一兵」
- 前沿 Java 技術干貨分享
- 高效工具匯總 | 回復「工具」
- 面試問題分析與解答
- 技術資料領取 | 回復「資料」
以讀偵探小說思維輕松趣味學習 Java 技術棧相關知識,本着將復雜問題簡單化,抽象問題具體化和圖形化原則逐步分解技術問題,技術持續更新,請持續關注......

