Java 字符串常量池漫遊指南(圖文並茂)

字符串問題可謂是 Java 中經久不衰的問題,尤其是字符串常量池經常作為面試題出現。可即便是看似簡單而又經常被提起的問題,還是有好多同學一知半解,看上去懂了,仔細分析起來卻有發現不太明白。

背景說明

本文以 JDK 1.8 為討論版本,雖然現在都已經 JDK 14了,奈何我們還是鍾愛 1.8。

一個提問引起的討論

為什麼說到字符串常量呢,源於群裡為數不多的一個程序員小姐姐的提問。

Java 字符串常量池漫遊指南(圖文並茂)

這本來和字符串常量沒有關係,後來,一個同學說不只是 int ,換成 String 一樣可以。

Java 字符串常量池漫遊指南(圖文並茂)

為什麼會有"Java開發_北京"這麼奇特的字符串亂入呢,因為提出問題的這位小姐姐的群暱稱叫這個,所以群裡的同學開玩笑說,以為她是某個房地產大佬,要來開發北京。

Java 字符串常量池漫遊指南(圖文並茂)

以上是開個玩笑,好了,收。

字符串用 == 比較也是 true,這就有意思了。馬上有機靈的小夥伴說這和字符串常量池有關係。沒錯,就是因為字符串常量池的原因。

第一張圖其實沒什麼好說的,在 JDK 1.8 之後已經不允許 Object 和 int 類型用 == 相比較了,編譯直接報錯。

第二張圖中的代碼才是重點要說的,我們可以把它簡化成下面這段代碼,用 == 符號比較字符串,之後的內容都從這幾行代碼出發。

<code>

public

 

static

 

void

 

main

(

String[] args

)

 {    String s1 = 

"古時的風箏"

;    System.

out

.println(s1 == 

"古時的風箏"

); }/<code>

當然,實際開發中強烈不推薦用 == 符號判斷兩個字符串是否相等,應該用 equals() 方法。

字符串常量池何許人也

為什麼要有字符串常量池呢,像其他對象一樣直接存在堆中不行嗎,這就要問 Java 語言的設計者了,當然,這麼做也並不是拍腦袋想出來的。

這就要從字符串說起。

首先對象的分配要付出時間和空間上的開銷,字符串可以說是和 8 個基本類型一樣常用的類型,甚至比 8 個基本類型更加常用,故而頻繁的創建字符串對象,對性能的影響是非常大的,所以,用常量池的方式可以很大程度上降低對象創建、分配的次數,從而提升性能。

在 JDK 1.7 之後(包括1.7),字符串常量池已經從方法區移到了堆中。

字面量賦值

我們把上面的那個實例代碼拿過來

<code>

String

 s1 = 

"古時的風箏"

;/<code>

這是我們平時聲明字符串變量的最常用的方式,這種方式叫做字面量聲明,也就用把字符串用雙引號引起來,然後賦值給一個變量。

這種情況下會直接將字符串放到字符串常量池中,然後返回給變量。

Java 字符串常量池漫遊指南(圖文並茂)

那這是我再聲明一個內容相同的字符串,會發現字符串常量池中已經存在了,那直接指向常量池中的地址即可。

Java 字符串常量池漫遊指南(圖文並茂)

例如上圖所示,聲明瞭 s1 和 s2,到最後都是指向同一個常量池的地址,所以 s1== s2 的結果是 true。

new String() 方式

與之對應的是用 new String() 的方式,但是基本上不建議這麼用,除非有特殊的邏輯需要。

<code>

String

 s2 = 

new

 

String

(

"古時的風箏"

);/<code>

使用這種方式聲明字符串變量的時候,會有兩種情況發生。

第一種情況,字符串常量池之前已經存在相同字符串

比如在使用 new 之前,已經用字面量聲明的方式聲明瞭一個變量,此時字符串常量池中已經存在了相同內容的字符串常量。

  1. 首先會在堆中創建一個 s2 變量的對象引用;
  2. 然後將這個對象引用指向字符串常量池中的已經存在的常量;
Java 字符串常量池漫遊指南(圖文並茂)

第二種情況,字符串常量池中不存在相同內容的常量

之前沒有任何地方用到了這個字符串,第一次聲明這個字符串就用的是 new String() 的方式,這種情況下會直接在堆中創建一個字符串對象然後返回給變量。

Java 字符串常量池漫遊指南(圖文並茂)

我看到好多地方說,如果字符串常量池中不存在的話,就先把字符串先放進去,然後再引用字符串常量池的這個常量對象,這種說法是有問題的,只是 new String() 的話,如果池中沒有也不會放一份進去。

基於 new String() 的這種特性,我們可以得出一個結論:

<code>

String

 s1 = 

"古時的風箏"

;

String

 s2 = 

new

 

String

(

"古時的風箏"

);

String

 s3 = 

new

 

String

(

"古時的風箏"

); System.out.println(s1==s2);  System.out.println(s2==s3);  /<code>

以上代碼,肯定輸出的都是 false,因為 new String() 不管你常量池中有沒有,我都會在堆中新建一個對象,新建出來的對象,當然不會和其他對象相等。

intern() 池化

那什麼時候會放到字符串常量池呢,就是在使用 intern() 方法之後。

intern() 的定義:如果當前字符串內容存在於字符串常量池,存在的條件是使用 equas() 方法為ture,也就是內容是一樣的,那直接返回此字符串在常量池的引用;如果之前不在字符串常量池中,那麼在常量池創建一個引用並且指向堆中已存在的字符串,然後返回常量池中的地址。

第一種情況,準備池化的字符串與字符串常量池中的字符串有相同(equas()判斷)

<code>

String

 s1 = 

"古時的風箏"

;

String

 s2 = 

new

 

String

(

"古時的風箏"

); /<code>

這時,這個字符串常量已經在常量池存在了,這時,再 new 了一個新的對象 s2,並在堆中創建了一個相同字符串內容的對象。

Java 字符串常量池漫遊指南(圖文並茂)

這時,s1 == s2 會返回 fasle。然後我們調用 s2 = s2.intern(),將池化操作返回的結果賦值給 s2,就會發生如下的變化。

Java 字符串常量池漫遊指南(圖文並茂)

此時,再次判斷 s1 == s2 ,就會返回 true,因為它們都指向了字符串常量池的同一個字符串。

第二種情況,字符串常量池中不存在相同內容的字符串

使用 new String() 在堆中創建了一個字符串對象

Java 字符串常量池漫遊指南(圖文並茂)

使用了 intern() 之後發生了什麼呢,在常量池新增了一個對象,但是 並沒有 將字符串複製一份到常量池,而是直接指向了之前已經存在於堆中的字符串對象。因為在 JDK 1.7 之後,字符串常量池不一定就是存字符串對象的,還有可能存儲的是一個指向堆中地址的引用,現在說的就是這種情況,注意了,下圖是隻調用了 s2.intern(),並沒有返回給一個變量。其中字符串常量池(0x88)指向堆中字符串對象(0x99)就是intern() 的過程。

Java 字符串常量池漫遊指南(圖文並茂)

只有當我們把 s2.intern() 的結果返回給 s2 時,s2 才真正的指向字符串常量池。

Java 字符串常量池漫遊指南(圖文並茂)

我明白了

通過以上的介紹,我們來看下面的一段代碼返回的結果是什麼

<code>

public

 

class

 

Test

 {     

public

 

static

 

void

 

main

(

String[] args

)

 {         String s1 = 

"古時的風箏"

;         String s2 = 

"古時的風箏"

;         String s3 = 

new

 String(

"古時的風箏"

);         String s4 = 

new

 String(

"古時的風箏"

);         System.

out

.println(s1 == s2);          System.

out

.println(s2 == s3);          System.

out

.println(s3 == s4);          s3.intern();         System.

out

.println(s2 == s3);          s3 = s3.intern();         System.

out

.println(s2 == s3);          s4 = s4.intern();         System.

out

.println(s3 == s4);      } }/<code>

【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 才是安全可靠的。

最後一點就是上面提到的,字符串對象的頻繁創建會帶來性能上的開銷,所以,利用不可變性才有了字符串常量池,使得性能得以保障。

後話

知其然,也要知所以然。一知半解才不是我們追求的目標。不知道圖畫的夠不夠清晰,希望能幫助到對字符串常量池不甚瞭解的同學。


作者:古時的風箏
鏈接:https://juejin.im/post/5e969c7e51882573b2196417


分享到:


相關文章: