看完肯定懂的 Java 字串常量池指南
阿新 • • 發佈:2020-04-27
> 字串問題可謂是 Java 中經久不衰的問題,尤其是字串常量池經常作為面試題出現。可即便是看似簡單而又經常被提起的問題,還是有好多同學一知半解,看上去懂了,仔細分析起來卻又發現不太明白。
### 背景說明
本文以 JDK 1.8 為討論版本,雖然現在都已經 JDK 14了,奈何我們還是鍾愛 1.8。
## 一個提問引起的討論
為什麼說到字串常量呢,源於群裡為數不多的一個程式設計師小姐姐的提問。
![](https://img2020.cnblogs.com/blog/273364/202004/273364-20200426222431160-1567214769.png)
這本來和字串常量沒有關係,後來,一個同學說不只是 int ,換成 String 一樣可以。
![](https://img2020.cnblogs.com/blog/273364/202004/273364-20200426222446687-1659466516.png)
為什麼會有"Java開發_北京"這麼奇特的字串亂入呢,因為提出問題的這位小姐姐的群暱稱叫這個,所以群裡的同學開玩笑說,以為她是某個房地產大佬,要來開發北京。
![](https://img2020.cnblogs.com/blog/273364/202004/273364-20200426222502943-1184657783.jpg)
**以上是開個玩笑,好了,收。**
字串用 == 比較也是 true,這就有意思了。馬上有機靈的小夥伴說這和字串常量池有關係。沒錯,就是因為字串常量池的原因。
第一張圖其實沒什麼好說的,在 JDK 1.8 之後已經不允許 Object 和 int 型別用 == 相比較了,編譯直接報錯。
第二張圖中的程式碼才是重點要說的,我們可以把它簡化成下面這段程式碼,用 == 符號比較字串,之後的內容都從這幾行程式碼出發。
```java
public static void main(String[] args) {
String s1 = "古時的風箏";
System.out.println(s1 == "古時的風箏");
}
```
當然,實際開發中強烈不推薦用 == 符號判斷兩個字串是否相等,應該用 equals() 方法。
## 字串常量池何許人也
為什麼要有字串常量池呢,像其他物件一樣直接存在堆中不行嗎,這就要問 Java 語言的設計者了,當然,這麼做也並不是拍腦袋想出來的。
這就要從字串說起。
首先物件的分配要付出時間和空間上的開銷,字串可以說是和 8 個基本型別一樣常用的型別,甚至比 8 個基本型別更加常用,故而頻繁的建立字串物件,對效能的影響是非常大的,所以,用常量池的方式可以很大程度上降低物件建立、分配的次數,從而提升效能。
在 JDK 1.7 之後(包括1.7),字串常量池已經從方法區移到了堆中。
### 字面量賦值
我們把上面的那個例項程式碼拿過來
```java
String s1 = "古時的風箏";
```
這是我們平時宣告字串變數的最常用的方式,這種方式叫做字面量宣告,也就用把字串用雙引號引起來,然後賦值給一個變數。
這種情況下會直接將字串放到字串常量池中,然後返回給變數。
![](https://img2020.cnblogs.com/blog/273364/202004/273364-20200426222620840-1395338814.png)
那這是我再宣告一個內容相同的字串,會發現字串常量池中已經存在了,那直接指向常量池中的地址即可。
![](https://img2020.cnblogs.com/blog/273364/202004/273364-20200426222630569-1857704149.png)
例如上圖所示,聲明瞭 s1 和 s2,到最後都是指向同一個常量池的地址,所以 s1== s2 的結果是 true。
### new String() 方式
與之對應的是用 new String() 的方式,但是基本上不建議這麼用,除非有特殊的邏輯需要。
```java
String a = "古時的";
String s2 = new String(a + "風箏");
```
使用這種方式宣告字串變數的時候,會有兩種情況發生。
##### 第一種情況,字串常量池之前已經存在相同字串
比如在使用 new 之前,已經用字面量宣告的方式聲明瞭一個變數,此時字串常量池中已經存在了相同內容的字串常量。
1. 首先會在堆中建立一個 s2 變數的物件引用;
2. 然後將這個物件引用指向字串常量池中的已經存在的常量;
![](https://img2020.cnblogs.com/blog/273364/202004/273364-20200426222640897-1741907905.png)
##### 第二種情況,字串常量池中不存在相同內容的常量
之前沒有任何地方用到了這個字串,第一次宣告這個字串就用的是 new String() 的方式,這種情況下會直接在堆中建立一個字串物件然後返回給變數。
![](https://img2020.cnblogs.com/blog/273364/202004/273364-20200426222653646-2131731325.png)
**我看到好多地方說,如果字串常量池中不存在的話,就先把字串先放進去,然後再引用字串常量池的這個常量物件,這種說法是有問題的,只是 new String() 的話,如果池中沒有也不會放一份進去。**
基於 new String() 的這種特性,我們可以得出一個結論:
```java
String s1 = "古時的風箏";
String a = "古時的";
String s2 = new String(a + "風箏");
String s3 = new String(a + "風箏");
System.out.println(s1==s2); // false
System.out.println(s2==s3); // false
```
以上程式碼,肯定輸出的都是 false,因為 new String() 不管你常量池中有沒有,我都會在堆中新建一個物件,新建出來的物件,當然不會和其他物件相等。
### intern() 池化
那什麼時候會放到字串常量池呢,就是在使用 intern() 方法之後。
intern() 的定義:如果當前字串內容存在於字串常量池,存在的條件是使用 equas() 方法為ture,也就是內容是一樣的,那直接返回此字串在常量池的引用;如果之前不在字串常量池中,那麼在常量池建立一個引用並且指向堆中已存在的字串,然後返回常量池中的地址。
##### 第一種情況,準備池化的字串與字串常量池中的字串有相同(equas()判斷)
```java
String s1 = "古時的風箏";
String a = "古時的";
String s2 = new String(a + "風箏");
s2 = s2.intern();
```
這時,這個字串常量已經在常量池存在了,這時,再 new 了一個新的物件 s2,並在堆中建立了一個相同字串內容的物件。
![](https://img2020.cnblogs.com/blog/273364/202004/273364-20200426222712155-883776391.png)
這時,s1 == s2 會返回 fasle。然後我們呼叫 s2 = s2.intern(),將池化操作返回的結果賦值給 s2,就會發生如下的變化。
![](https://img2020.cnblogs.com/blog/273364/202004/273364-20200426222722883-1320467807.png)
此時,再次判斷 s1 == s2 ,就會返回 true,因為它們都指向了字串常量池的同一個字串。
##### 第二種情況,字串常量池中不存在相同內容的字串
使用 new String() 在堆中建立了一個字串物件
![](https://img2020.cnblogs.com/blog/273364/202004/273364-20200426222733958-1522309026.png)
使用了 intern() 之後發生了什麼呢,在常量池新增了一個物件,但是 **並沒有** 將字串複製一份到常量池,而是直接指向了之前已經存在於堆中的字串物件。因為在 JDK 1.7 之後,字串常量池不一定就是存字串物件的,還有可能儲存的是一個指向堆中地址的引用,現在說的就是這種情況,注意了,下圖是隻呼叫了 `s2.intern()`,並沒有返回給一個變數。其中字串常量池(0x88)指向堆中字串物件(0x99)就是intern() 的過程。
![](https://img2020.cnblogs.com/blog/273364/202004/273364-20200426222743584-606693633.png)
只有當我們把 s2.intern() 的結果返回給 s2 時,s2 才真正的指向字串常量池。
![](https://img2020.cnblogs.com/blog/273364/202004/273364-20200426222755074-1475263708.png)
### 我明白了
通過以上的介紹,我們來看下面的一段程式碼返回的結果是什麼
```java
public class Test {
public static void main(String[] args) {
String s1 = "古時的風箏";
String s2 = "古時的風箏";
String a = "古時的";
String s3 = new String(a + "風箏");
String s4 = new String(a + "風箏");
System.out.println(s1 == s2); // 【1】 true
System.out.println(s2 == s3); // 【2】 false
System.out.println(s3 == s4); // 【3】 false
s3.intern();
System.out.println(s2 == s3); // 【4】 false
s3 = s3.intern();
System.out.println(s2 == s3); // 【5】 true
s4 = s4.intern();
System.out.println(s3 == s4); // 【6】 true
}
}
```
【1】:s1 == s2 返回 ture,因為都是字面量宣告,全都指向字串常量池中同一字串。
【2】: s2 == s3 返回 false,因為 new String() 是在堆中新建物件,所以和常量池的常量不相同。
【3】: s3 == s4 返回 false,都是在堆中新建物件,所以是兩個物件,肯定不相同。
【4】: s2 == s3 返回 false,前面雖然呼叫了 intern() ,但是沒有返回,不起作用。
【5】: s2 == s3 返回 ture,前面呼叫了 intern() ,並且返回給了 s3 ,此時 s2、s3 都直接指向常量池的同一個字串。
【6】: s3 == s4 返回 true,和 s3 相同,都指向了常量池同一個字串。
## 為啥我字串就不可變
字串常量池的基礎就是字串的不可變性,如果字串是可變的,那想一想,常量池就沒必要存在了。假設多個變數都指向字串常量池的同一個字串,然後呢,突然來了一行程式碼,不管三七二十一,直接把字串給變了,那豈不是 jvm 世界大亂。
字串不可變的根本原因應該是處於安全性考慮。
我們知道 jvm 型別載入的時候會用到類名,比如載入 java.lang.String 型別,如果字串可變的話,那我替換成其他的字元,那豈不是很危險。
專案中會用到比如資料庫連線串、賬號、密碼等字串,只有不可變的連線串、使用者名稱和密碼才能保證安全性。
字串在 Java 中的使用頻率可謂高之又高,那在高併發的情況下不可變性也使得對字串的讀寫操作不用考慮多執行緒競爭的情況。
還有就是 HashCode,HashCode 是判斷兩個物件是否完全相等的核心條件,另外,像 Set、Map 結構中的 key 值也需要用到 HashCode 來保證唯一性和一致性,因此不可變的 HashCode 才是安全可靠的。
最後一點就是上面提到的,字串物件的頻繁建立會帶來效能上的開銷,所以,利用不可變性才有了字串常量池,使得效能得以保障。
> 我是風箏,公眾號「古時的風箏」,一個不只有技術的技術公眾號,一個在程式圈混跡多年,主業 Java,另外 Python、React 也玩兒的 6 的斜槓開發者。
![](https://img2020.cnblogs.com/blog/273364/202004/273364-20200426222253675-13394340