'從底層徹底搞懂String,StringBuilder,StringBuffer的實現'

虛擬機 Java Java虛擬機 設計 技術 Java識堂 2019-08-14
"

在深入學習字符串類之前, 我們先搞懂JVM是怎樣處理新生字符串的. 當你知道字符串的初始化細節後, 再去寫 Strings="hello"或 Strings=newString("hello")等代碼時, 就能做到心中有數。

首先得搞懂字符串常量池的概念。

常量池是Java的一項技術, 八種基礎數據類型除了float和double都實現了常量池技術. 這項技術從字面上是很好理解的: 把經常用到的數據存放在某塊內存中, 避免頻繁的數據創建與銷燬, 實現數據共享, 提高系統性能。

字符串常量池是Java常量池技術的一種實現, 在近代的JDK版本中(1.7後), 字符串常量池被實現在Java堆內存中。

下面通過三行代碼讓大家對字符串常量池建立初步認識:

"

在深入學習字符串類之前, 我們先搞懂JVM是怎樣處理新生字符串的. 當你知道字符串的初始化細節後, 再去寫 Strings="hello"或 Strings=newString("hello")等代碼時, 就能做到心中有數。

首先得搞懂字符串常量池的概念。

常量池是Java的一項技術, 八種基礎數據類型除了float和double都實現了常量池技術. 這項技術從字面上是很好理解的: 把經常用到的數據存放在某塊內存中, 避免頻繁的數據創建與銷燬, 實現數據共享, 提高系統性能。

字符串常量池是Java常量池技術的一種實現, 在近代的JDK版本中(1.7後), 字符串常量池被實現在Java堆內存中。

下面通過三行代碼讓大家對字符串常量池建立初步認識:

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

我們先來看看第一行代碼 Strings1="hello";幹了什麼.

"

在深入學習字符串類之前, 我們先搞懂JVM是怎樣處理新生字符串的. 當你知道字符串的初始化細節後, 再去寫 Strings="hello"或 Strings=newString("hello")等代碼時, 就能做到心中有數。

首先得搞懂字符串常量池的概念。

常量池是Java的一項技術, 八種基礎數據類型除了float和double都實現了常量池技術. 這項技術從字面上是很好理解的: 把經常用到的數據存放在某塊內存中, 避免頻繁的數據創建與銷燬, 實現數據共享, 提高系統性能。

字符串常量池是Java常量池技術的一種實現, 在近代的JDK版本中(1.7後), 字符串常量池被實現在Java堆內存中。

下面通過三行代碼讓大家對字符串常量池建立初步認識:

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

我們先來看看第一行代碼 Strings1="hello";幹了什麼.

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

對於這種直接通過雙引號""聲明字符串的方式, 虛擬機首先會到字符串常量池中查找該字符串是否已經存在. 如果存在會直接返回該引用, 如果不存在則會在堆內存中創建該字符串對象, 然後到字符串常量池中註冊該字符串。

在本案例中虛擬機首先會到字符串常量池中查找是否有存在"hello"字符串對應的引用. 發現沒有後會在堆內存創建"hello"字符串對象(內存地址0x0001), 然後到字符串常量池中註冊地址為0x0001的"hello"對象, 也就是添加指向0x0001的引用. 最後把字符串對象返回給s1。

溫馨提示: 圖中的字符串常量池中的數據是虛構的, 由於字符串常量池底層是用HashTable實現的, 存儲的是鍵值對, 為了方便大家理解, 示意圖簡化了字符串常量池對照表, 並採用了一些虛擬的數值。

下面看 Strings2=newString("hello");的示意圖:

"

在深入學習字符串類之前, 我們先搞懂JVM是怎樣處理新生字符串的. 當你知道字符串的初始化細節後, 再去寫 Strings="hello"或 Strings=newString("hello")等代碼時, 就能做到心中有數。

首先得搞懂字符串常量池的概念。

常量池是Java的一項技術, 八種基礎數據類型除了float和double都實現了常量池技術. 這項技術從字面上是很好理解的: 把經常用到的數據存放在某塊內存中, 避免頻繁的數據創建與銷燬, 實現數據共享, 提高系統性能。

字符串常量池是Java常量池技術的一種實現, 在近代的JDK版本中(1.7後), 字符串常量池被實現在Java堆內存中。

下面通過三行代碼讓大家對字符串常量池建立初步認識:

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

我們先來看看第一行代碼 Strings1="hello";幹了什麼.

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

對於這種直接通過雙引號""聲明字符串的方式, 虛擬機首先會到字符串常量池中查找該字符串是否已經存在. 如果存在會直接返回該引用, 如果不存在則會在堆內存中創建該字符串對象, 然後到字符串常量池中註冊該字符串。

在本案例中虛擬機首先會到字符串常量池中查找是否有存在"hello"字符串對應的引用. 發現沒有後會在堆內存創建"hello"字符串對象(內存地址0x0001), 然後到字符串常量池中註冊地址為0x0001的"hello"對象, 也就是添加指向0x0001的引用. 最後把字符串對象返回給s1。

溫馨提示: 圖中的字符串常量池中的數據是虛構的, 由於字符串常量池底層是用HashTable實現的, 存儲的是鍵值對, 為了方便大家理解, 示意圖簡化了字符串常量池對照表, 並採用了一些虛擬的數值。

下面看 Strings2=newString("hello");的示意圖:

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

當我們使用new關鍵字創建字符串對象的時候, JVM將不會查詢字符串常量池, 它將會直接在堆內存中創建一個字符串對象, 並返回給所屬變量。

所以s1和s2指向的是兩個完全不同的對象, 判斷s1 == s2的時候會返回false。

如果上面的知識理解起來沒有問題的話, 下面看些難點的.

"

在深入學習字符串類之前, 我們先搞懂JVM是怎樣處理新生字符串的. 當你知道字符串的初始化細節後, 再去寫 Strings="hello"或 Strings=newString("hello")等代碼時, 就能做到心中有數。

首先得搞懂字符串常量池的概念。

常量池是Java的一項技術, 八種基礎數據類型除了float和double都實現了常量池技術. 這項技術從字面上是很好理解的: 把經常用到的數據存放在某塊內存中, 避免頻繁的數據創建與銷燬, 實現數據共享, 提高系統性能。

字符串常量池是Java常量池技術的一種實現, 在近代的JDK版本中(1.7後), 字符串常量池被實現在Java堆內存中。

下面通過三行代碼讓大家對字符串常量池建立初步認識:

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

我們先來看看第一行代碼 Strings1="hello";幹了什麼.

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

對於這種直接通過雙引號""聲明字符串的方式, 虛擬機首先會到字符串常量池中查找該字符串是否已經存在. 如果存在會直接返回該引用, 如果不存在則會在堆內存中創建該字符串對象, 然後到字符串常量池中註冊該字符串。

在本案例中虛擬機首先會到字符串常量池中查找是否有存在"hello"字符串對應的引用. 發現沒有後會在堆內存創建"hello"字符串對象(內存地址0x0001), 然後到字符串常量池中註冊地址為0x0001的"hello"對象, 也就是添加指向0x0001的引用. 最後把字符串對象返回給s1。

溫馨提示: 圖中的字符串常量池中的數據是虛構的, 由於字符串常量池底層是用HashTable實現的, 存儲的是鍵值對, 為了方便大家理解, 示意圖簡化了字符串常量池對照表, 並採用了一些虛擬的數值。

下面看 Strings2=newString("hello");的示意圖:

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

當我們使用new關鍵字創建字符串對象的時候, JVM將不會查詢字符串常量池, 它將會直接在堆內存中創建一個字符串對象, 並返回給所屬變量。

所以s1和s2指向的是兩個完全不同的對象, 判斷s1 == s2的時候會返回false。

如果上面的知識理解起來沒有問題的話, 下面看些難點的.

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

第一行代碼 Strings1=newString("hello ")+newString("world");的執行過程是這樣子的:

1.依次在堆內存中創建"hello "和"world"兩個字符串對象

2.然後把它們拼接起來 (底層使用StringBuilder實現, 後面會帶大家讀反編譯代碼)

3.在拼接完成後會產生新的"hello world"對象, 這時變量s1指向新對象"hello world"

執行完第一行代碼後, 內存是這樣子的:

"

在深入學習字符串類之前, 我們先搞懂JVM是怎樣處理新生字符串的. 當你知道字符串的初始化細節後, 再去寫 Strings="hello"或 Strings=newString("hello")等代碼時, 就能做到心中有數。

首先得搞懂字符串常量池的概念。

常量池是Java的一項技術, 八種基礎數據類型除了float和double都實現了常量池技術. 這項技術從字面上是很好理解的: 把經常用到的數據存放在某塊內存中, 避免頻繁的數據創建與銷燬, 實現數據共享, 提高系統性能。

字符串常量池是Java常量池技術的一種實現, 在近代的JDK版本中(1.7後), 字符串常量池被實現在Java堆內存中。

下面通過三行代碼讓大家對字符串常量池建立初步認識:

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

我們先來看看第一行代碼 Strings1="hello";幹了什麼.

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

對於這種直接通過雙引號""聲明字符串的方式, 虛擬機首先會到字符串常量池中查找該字符串是否已經存在. 如果存在會直接返回該引用, 如果不存在則會在堆內存中創建該字符串對象, 然後到字符串常量池中註冊該字符串。

在本案例中虛擬機首先會到字符串常量池中查找是否有存在"hello"字符串對應的引用. 發現沒有後會在堆內存創建"hello"字符串對象(內存地址0x0001), 然後到字符串常量池中註冊地址為0x0001的"hello"對象, 也就是添加指向0x0001的引用. 最後把字符串對象返回給s1。

溫馨提示: 圖中的字符串常量池中的數據是虛構的, 由於字符串常量池底層是用HashTable實現的, 存儲的是鍵值對, 為了方便大家理解, 示意圖簡化了字符串常量池對照表, 並採用了一些虛擬的數值。

下面看 Strings2=newString("hello");的示意圖:

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

當我們使用new關鍵字創建字符串對象的時候, JVM將不會查詢字符串常量池, 它將會直接在堆內存中創建一個字符串對象, 並返回給所屬變量。

所以s1和s2指向的是兩個完全不同的對象, 判斷s1 == s2的時候會返回false。

如果上面的知識理解起來沒有問題的話, 下面看些難點的.

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

第一行代碼 Strings1=newString("hello ")+newString("world");的執行過程是這樣子的:

1.依次在堆內存中創建"hello "和"world"兩個字符串對象

2.然後把它們拼接起來 (底層使用StringBuilder實現, 後面會帶大家讀反編譯代碼)

3.在拼接完成後會產生新的"hello world"對象, 這時變量s1指向新對象"hello world"

執行完第一行代碼後, 內存是這樣子的:

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

第二行代碼 s1.intern();

String類的源碼中有對 intern()方法的詳細介紹, 翻譯過來的意思是: 當調用 intern()方法時, 首先會去常量池中查找是否有該字符串對應的引用, 如果有就直接返回該字符串; 如果沒有, 就會在常量池中註冊該字符串的引用, 然後返回該字符串。

由於第一行代碼採用的是new的方式創建字符串, 所以在字符串常量池中沒有保存"hello world"對應的引用, 虛擬機會在常量池中進行註冊, 註冊完後的內存示意圖如下:

"

在深入學習字符串類之前, 我們先搞懂JVM是怎樣處理新生字符串的. 當你知道字符串的初始化細節後, 再去寫 Strings="hello"或 Strings=newString("hello")等代碼時, 就能做到心中有數。

首先得搞懂字符串常量池的概念。

常量池是Java的一項技術, 八種基礎數據類型除了float和double都實現了常量池技術. 這項技術從字面上是很好理解的: 把經常用到的數據存放在某塊內存中, 避免頻繁的數據創建與銷燬, 實現數據共享, 提高系統性能。

字符串常量池是Java常量池技術的一種實現, 在近代的JDK版本中(1.7後), 字符串常量池被實現在Java堆內存中。

下面通過三行代碼讓大家對字符串常量池建立初步認識:

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

我們先來看看第一行代碼 Strings1="hello";幹了什麼.

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

對於這種直接通過雙引號""聲明字符串的方式, 虛擬機首先會到字符串常量池中查找該字符串是否已經存在. 如果存在會直接返回該引用, 如果不存在則會在堆內存中創建該字符串對象, 然後到字符串常量池中註冊該字符串。

在本案例中虛擬機首先會到字符串常量池中查找是否有存在"hello"字符串對應的引用. 發現沒有後會在堆內存創建"hello"字符串對象(內存地址0x0001), 然後到字符串常量池中註冊地址為0x0001的"hello"對象, 也就是添加指向0x0001的引用. 最後把字符串對象返回給s1。

溫馨提示: 圖中的字符串常量池中的數據是虛構的, 由於字符串常量池底層是用HashTable實現的, 存儲的是鍵值對, 為了方便大家理解, 示意圖簡化了字符串常量池對照表, 並採用了一些虛擬的數值。

下面看 Strings2=newString("hello");的示意圖:

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

當我們使用new關鍵字創建字符串對象的時候, JVM將不會查詢字符串常量池, 它將會直接在堆內存中創建一個字符串對象, 並返回給所屬變量。

所以s1和s2指向的是兩個完全不同的對象, 判斷s1 == s2的時候會返回false。

如果上面的知識理解起來沒有問題的話, 下面看些難點的.

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

第一行代碼 Strings1=newString("hello ")+newString("world");的執行過程是這樣子的:

1.依次在堆內存中創建"hello "和"world"兩個字符串對象

2.然後把它們拼接起來 (底層使用StringBuilder實現, 後面會帶大家讀反編譯代碼)

3.在拼接完成後會產生新的"hello world"對象, 這時變量s1指向新對象"hello world"

執行完第一行代碼後, 內存是這樣子的:

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

第二行代碼 s1.intern();

String類的源碼中有對 intern()方法的詳細介紹, 翻譯過來的意思是: 當調用 intern()方法時, 首先會去常量池中查找是否有該字符串對應的引用, 如果有就直接返回該字符串; 如果沒有, 就會在常量池中註冊該字符串的引用, 然後返回該字符串。

由於第一行代碼採用的是new的方式創建字符串, 所以在字符串常量池中沒有保存"hello world"對應的引用, 虛擬機會在常量池中進行註冊, 註冊完後的內存示意圖如下:

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

第三行代碼 Strings2="hello world";

這種直接通過雙引號""聲明字符串背後的運行機制我們在第一個案例提到過, 這裡正好複習一下。

首先虛擬機會去檢查字符串常量池, 發現有指向"hello world"的引用. 然後把該引用所指向的字符串直接返回給所屬變量。

執行完第三行代碼後, 內存示意圖如下:

"

在深入學習字符串類之前, 我們先搞懂JVM是怎樣處理新生字符串的. 當你知道字符串的初始化細節後, 再去寫 Strings="hello"或 Strings=newString("hello")等代碼時, 就能做到心中有數。

首先得搞懂字符串常量池的概念。

常量池是Java的一項技術, 八種基礎數據類型除了float和double都實現了常量池技術. 這項技術從字面上是很好理解的: 把經常用到的數據存放在某塊內存中, 避免頻繁的數據創建與銷燬, 實現數據共享, 提高系統性能。

字符串常量池是Java常量池技術的一種實現, 在近代的JDK版本中(1.7後), 字符串常量池被實現在Java堆內存中。

下面通過三行代碼讓大家對字符串常量池建立初步認識:

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

我們先來看看第一行代碼 Strings1="hello";幹了什麼.

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

對於這種直接通過雙引號""聲明字符串的方式, 虛擬機首先會到字符串常量池中查找該字符串是否已經存在. 如果存在會直接返回該引用, 如果不存在則會在堆內存中創建該字符串對象, 然後到字符串常量池中註冊該字符串。

在本案例中虛擬機首先會到字符串常量池中查找是否有存在"hello"字符串對應的引用. 發現沒有後會在堆內存創建"hello"字符串對象(內存地址0x0001), 然後到字符串常量池中註冊地址為0x0001的"hello"對象, 也就是添加指向0x0001的引用. 最後把字符串對象返回給s1。

溫馨提示: 圖中的字符串常量池中的數據是虛構的, 由於字符串常量池底層是用HashTable實現的, 存儲的是鍵值對, 為了方便大家理解, 示意圖簡化了字符串常量池對照表, 並採用了一些虛擬的數值。

下面看 Strings2=newString("hello");的示意圖:

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

當我們使用new關鍵字創建字符串對象的時候, JVM將不會查詢字符串常量池, 它將會直接在堆內存中創建一個字符串對象, 並返回給所屬變量。

所以s1和s2指向的是兩個完全不同的對象, 判斷s1 == s2的時候會返回false。

如果上面的知識理解起來沒有問題的話, 下面看些難點的.

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

第一行代碼 Strings1=newString("hello ")+newString("world");的執行過程是這樣子的:

1.依次在堆內存中創建"hello "和"world"兩個字符串對象

2.然後把它們拼接起來 (底層使用StringBuilder實現, 後面會帶大家讀反編譯代碼)

3.在拼接完成後會產生新的"hello world"對象, 這時變量s1指向新對象"hello world"

執行完第一行代碼後, 內存是這樣子的:

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

第二行代碼 s1.intern();

String類的源碼中有對 intern()方法的詳細介紹, 翻譯過來的意思是: 當調用 intern()方法時, 首先會去常量池中查找是否有該字符串對應的引用, 如果有就直接返回該字符串; 如果沒有, 就會在常量池中註冊該字符串的引用, 然後返回該字符串。

由於第一行代碼採用的是new的方式創建字符串, 所以在字符串常量池中沒有保存"hello world"對應的引用, 虛擬機會在常量池中進行註冊, 註冊完後的內存示意圖如下:

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

第三行代碼 Strings2="hello world";

這種直接通過雙引號""聲明字符串背後的運行機制我們在第一個案例提到過, 這裡正好複習一下。

首先虛擬機會去檢查字符串常量池, 發現有指向"hello world"的引用. 然後把該引用所指向的字符串直接返回給所屬變量。

執行完第三行代碼後, 內存示意圖如下:

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

如圖所示, s1和s2指向的是相同的對象, 所以當判斷s1 == s2時返回true。

最後我們對字符串常量池進行總結:

當用new關鍵字創建字符串對象時, 不會查詢字符串常量池; 當用雙引號直接聲明字符串對象時, 虛擬機將會查詢字符串常量池. 說白了就是: 字符串常量池提供了字符串的複用功能, 除非我們要顯式創建新的字符串對象, 否則對同一個字符串虛擬機只會維護一份拷貝。

配合反編譯代碼驗證字符串初始化操作.

相信看到這裡, 再見到有關的面試題, 你已經無所畏懼了, 因為你已經懂得了背後原理。

在結束之前我們不妨再做一道壓軸題

"

在深入學習字符串類之前, 我們先搞懂JVM是怎樣處理新生字符串的. 當你知道字符串的初始化細節後, 再去寫 Strings="hello"或 Strings=newString("hello")等代碼時, 就能做到心中有數。

首先得搞懂字符串常量池的概念。

常量池是Java的一項技術, 八種基礎數據類型除了float和double都實現了常量池技術. 這項技術從字面上是很好理解的: 把經常用到的數據存放在某塊內存中, 避免頻繁的數據創建與銷燬, 實現數據共享, 提高系統性能。

字符串常量池是Java常量池技術的一種實現, 在近代的JDK版本中(1.7後), 字符串常量池被實現在Java堆內存中。

下面通過三行代碼讓大家對字符串常量池建立初步認識:

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

我們先來看看第一行代碼 Strings1="hello";幹了什麼.

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

對於這種直接通過雙引號""聲明字符串的方式, 虛擬機首先會到字符串常量池中查找該字符串是否已經存在. 如果存在會直接返回該引用, 如果不存在則會在堆內存中創建該字符串對象, 然後到字符串常量池中註冊該字符串。

在本案例中虛擬機首先會到字符串常量池中查找是否有存在"hello"字符串對應的引用. 發現沒有後會在堆內存創建"hello"字符串對象(內存地址0x0001), 然後到字符串常量池中註冊地址為0x0001的"hello"對象, 也就是添加指向0x0001的引用. 最後把字符串對象返回給s1。

溫馨提示: 圖中的字符串常量池中的數據是虛構的, 由於字符串常量池底層是用HashTable實現的, 存儲的是鍵值對, 為了方便大家理解, 示意圖簡化了字符串常量池對照表, 並採用了一些虛擬的數值。

下面看 Strings2=newString("hello");的示意圖:

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

當我們使用new關鍵字創建字符串對象的時候, JVM將不會查詢字符串常量池, 它將會直接在堆內存中創建一個字符串對象, 並返回給所屬變量。

所以s1和s2指向的是兩個完全不同的對象, 判斷s1 == s2的時候會返回false。

如果上面的知識理解起來沒有問題的話, 下面看些難點的.

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

第一行代碼 Strings1=newString("hello ")+newString("world");的執行過程是這樣子的:

1.依次在堆內存中創建"hello "和"world"兩個字符串對象

2.然後把它們拼接起來 (底層使用StringBuilder實現, 後面會帶大家讀反編譯代碼)

3.在拼接完成後會產生新的"hello world"對象, 這時變量s1指向新對象"hello world"

執行完第一行代碼後, 內存是這樣子的:

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

第二行代碼 s1.intern();

String類的源碼中有對 intern()方法的詳細介紹, 翻譯過來的意思是: 當調用 intern()方法時, 首先會去常量池中查找是否有該字符串對應的引用, 如果有就直接返回該字符串; 如果沒有, 就會在常量池中註冊該字符串的引用, 然後返回該字符串。

由於第一行代碼採用的是new的方式創建字符串, 所以在字符串常量池中沒有保存"hello world"對應的引用, 虛擬機會在常量池中進行註冊, 註冊完後的內存示意圖如下:

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

第三行代碼 Strings2="hello world";

這種直接通過雙引號""聲明字符串背後的運行機制我們在第一個案例提到過, 這裡正好複習一下。

首先虛擬機會去檢查字符串常量池, 發現有指向"hello world"的引用. 然後把該引用所指向的字符串直接返回給所屬變量。

執行完第三行代碼後, 內存示意圖如下:

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

如圖所示, s1和s2指向的是相同的對象, 所以當判斷s1 == s2時返回true。

最後我們對字符串常量池進行總結:

當用new關鍵字創建字符串對象時, 不會查詢字符串常量池; 當用雙引號直接聲明字符串對象時, 虛擬機將會查詢字符串常量池. 說白了就是: 字符串常量池提供了字符串的複用功能, 除非我們要顯式創建新的字符串對象, 否則對同一個字符串虛擬機只會維護一份拷貝。

配合反編譯代碼驗證字符串初始化操作.

相信看到這裡, 再見到有關的面試題, 你已經無所畏懼了, 因為你已經懂得了背後原理。

在結束之前我們不妨再做一道壓軸題

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

這道壓軸題是經過精心設計的, 它不但照應上面所講的字符串常量池知識, 也引出了後面的話題.

如果看這篇文章是你第一次往底層探索字符串的經歷, 那我估計你不能立即給出答案. 因為我第一次見這幾行代碼時也卡殼了。

首先第一行和第二行是常規的字符串對象聲明, 我們已經很熟悉了, 它們分別會在堆內存創建字符串對象, 並會在字符串常量池中進行註冊。

影響我們做出判斷的是第三行代碼 Strings3=s1+s2;, 我們不知道 s1+s2在創建完新字符串"hello world"後是否會在字符串常量池進行註冊。

說白了就是我們不知道這行代碼是以雙引號""形式聲明字符串, 還是用new關鍵字創建字符串。

這時, 我們應該去讀一讀這段代碼的反編譯代碼. 如果你沒有讀過反編譯代碼, 不妨藉此機會入門。

在命令行中輸入 javap-c對應.class文件的絕對路徑, 按回車後即可看到反編譯文件的代碼段。

"

在深入學習字符串類之前, 我們先搞懂JVM是怎樣處理新生字符串的. 當你知道字符串的初始化細節後, 再去寫 Strings="hello"或 Strings=newString("hello")等代碼時, 就能做到心中有數。

首先得搞懂字符串常量池的概念。

常量池是Java的一項技術, 八種基礎數據類型除了float和double都實現了常量池技術. 這項技術從字面上是很好理解的: 把經常用到的數據存放在某塊內存中, 避免頻繁的數據創建與銷燬, 實現數據共享, 提高系統性能。

字符串常量池是Java常量池技術的一種實現, 在近代的JDK版本中(1.7後), 字符串常量池被實現在Java堆內存中。

下面通過三行代碼讓大家對字符串常量池建立初步認識:

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

我們先來看看第一行代碼 Strings1="hello";幹了什麼.

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

對於這種直接通過雙引號""聲明字符串的方式, 虛擬機首先會到字符串常量池中查找該字符串是否已經存在. 如果存在會直接返回該引用, 如果不存在則會在堆內存中創建該字符串對象, 然後到字符串常量池中註冊該字符串。

在本案例中虛擬機首先會到字符串常量池中查找是否有存在"hello"字符串對應的引用. 發現沒有後會在堆內存創建"hello"字符串對象(內存地址0x0001), 然後到字符串常量池中註冊地址為0x0001的"hello"對象, 也就是添加指向0x0001的引用. 最後把字符串對象返回給s1。

溫馨提示: 圖中的字符串常量池中的數據是虛構的, 由於字符串常量池底層是用HashTable實現的, 存儲的是鍵值對, 為了方便大家理解, 示意圖簡化了字符串常量池對照表, 並採用了一些虛擬的數值。

下面看 Strings2=newString("hello");的示意圖:

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

當我們使用new關鍵字創建字符串對象的時候, JVM將不會查詢字符串常量池, 它將會直接在堆內存中創建一個字符串對象, 並返回給所屬變量。

所以s1和s2指向的是兩個完全不同的對象, 判斷s1 == s2的時候會返回false。

如果上面的知識理解起來沒有問題的話, 下面看些難點的.

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

第一行代碼 Strings1=newString("hello ")+newString("world");的執行過程是這樣子的:

1.依次在堆內存中創建"hello "和"world"兩個字符串對象

2.然後把它們拼接起來 (底層使用StringBuilder實現, 後面會帶大家讀反編譯代碼)

3.在拼接完成後會產生新的"hello world"對象, 這時變量s1指向新對象"hello world"

執行完第一行代碼後, 內存是這樣子的:

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

第二行代碼 s1.intern();

String類的源碼中有對 intern()方法的詳細介紹, 翻譯過來的意思是: 當調用 intern()方法時, 首先會去常量池中查找是否有該字符串對應的引用, 如果有就直接返回該字符串; 如果沒有, 就會在常量池中註冊該字符串的引用, 然後返回該字符串。

由於第一行代碼採用的是new的方式創建字符串, 所以在字符串常量池中沒有保存"hello world"對應的引用, 虛擬機會在常量池中進行註冊, 註冊完後的內存示意圖如下:

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

第三行代碼 Strings2="hello world";

這種直接通過雙引號""聲明字符串背後的運行機制我們在第一個案例提到過, 這裡正好複習一下。

首先虛擬機會去檢查字符串常量池, 發現有指向"hello world"的引用. 然後把該引用所指向的字符串直接返回給所屬變量。

執行完第三行代碼後, 內存示意圖如下:

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

如圖所示, s1和s2指向的是相同的對象, 所以當判斷s1 == s2時返回true。

最後我們對字符串常量池進行總結:

當用new關鍵字創建字符串對象時, 不會查詢字符串常量池; 當用雙引號直接聲明字符串對象時, 虛擬機將會查詢字符串常量池. 說白了就是: 字符串常量池提供了字符串的複用功能, 除非我們要顯式創建新的字符串對象, 否則對同一個字符串虛擬機只會維護一份拷貝。

配合反編譯代碼驗證字符串初始化操作.

相信看到這裡, 再見到有關的面試題, 你已經無所畏懼了, 因為你已經懂得了背後原理。

在結束之前我們不妨再做一道壓軸題

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

這道壓軸題是經過精心設計的, 它不但照應上面所講的字符串常量池知識, 也引出了後面的話題.

如果看這篇文章是你第一次往底層探索字符串的經歷, 那我估計你不能立即給出答案. 因為我第一次見這幾行代碼時也卡殼了。

首先第一行和第二行是常規的字符串對象聲明, 我們已經很熟悉了, 它們分別會在堆內存創建字符串對象, 並會在字符串常量池中進行註冊。

影響我們做出判斷的是第三行代碼 Strings3=s1+s2;, 我們不知道 s1+s2在創建完新字符串"hello world"後是否會在字符串常量池進行註冊。

說白了就是我們不知道這行代碼是以雙引號""形式聲明字符串, 還是用new關鍵字創建字符串。

這時, 我們應該去讀一讀這段代碼的反編譯代碼. 如果你沒有讀過反編譯代碼, 不妨藉此機會入門。

在命令行中輸入 javap-c對應.class文件的絕對路徑, 按回車後即可看到反編譯文件的代碼段。

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

  • 首先調用構造器完成Main類的初始化
  • 0:ldc#2 // String hello
  • 從常量池中獲取"hello "字符串並推送至棧頂, 此時拿到了"hello "的引用
  • 2:astore_1
  • 將棧頂的字符串引用存入第二個本地變量s1, 也就是s1已經指向了"hello "
  • 3:ldc#3 // String world
  • 5:astore_2
  • 重複開始的步驟, 此時變量s2指向"word"
  • 6:new#4 // class java/lang/StringBuilder
  • 刺激的東西來了: 這時創建了一個StringBuilder, 並把其引用值壓到棧頂
  • 9:dup
  • 複製棧頂的值, 並繼續壓入棧定, 也就意味著棧從上到下有兩份StringBuilder的引用, 將來要操作兩次StringBuilder.
  • 10:invokespecial#5 // Method java/lang/StringBuilder."<init>":()V
  • 調用StringBuilder的一些初始化方法, 靜態方法或父類方法, 完成初始化.
  • 13: aload_1
  • 把第二個本地變量也就是s1壓入棧頂, 現在棧頂從上往下數兩個數據依次是:s1變量和StringBuilder的引用
  • 14:invokevirtual#6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  • 調用StringBuilder的append方法, 棧頂的兩個數據在這裡調用方法時就用上了.
  • 接下來又調用了一次append方法(之前StringBuilder的引用拷貝兩份就用途在此)
  • 完成後, StringBuilder中已經拼接好了"hello world", 看到這裡相信大家已經明白虛擬機是如何拼接字符串的了. 接下來就是關鍵環節
  • 21:invokevirtual#7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
  • 24:astore_3
  • 拼接完字符串後, 虛擬機調用StringBuilder的 toString()方法獲得字符串 hello world, 並存放至s3.
  • 激動人心的時刻來了, 我們之所以不知道這道題的答案是因為不知道字符串拼接後是以new的形式還是以雙引號""的形式創建字符串對象.
  • 下面是我們追蹤StringBuilder的 toString()方法源碼:
"

在深入學習字符串類之前, 我們先搞懂JVM是怎樣處理新生字符串的. 當你知道字符串的初始化細節後, 再去寫 Strings="hello"或 Strings=newString("hello")等代碼時, 就能做到心中有數。

首先得搞懂字符串常量池的概念。

常量池是Java的一項技術, 八種基礎數據類型除了float和double都實現了常量池技術. 這項技術從字面上是很好理解的: 把經常用到的數據存放在某塊內存中, 避免頻繁的數據創建與銷燬, 實現數據共享, 提高系統性能。

字符串常量池是Java常量池技術的一種實現, 在近代的JDK版本中(1.7後), 字符串常量池被實現在Java堆內存中。

下面通過三行代碼讓大家對字符串常量池建立初步認識:

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

我們先來看看第一行代碼 Strings1="hello";幹了什麼.

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

對於這種直接通過雙引號""聲明字符串的方式, 虛擬機首先會到字符串常量池中查找該字符串是否已經存在. 如果存在會直接返回該引用, 如果不存在則會在堆內存中創建該字符串對象, 然後到字符串常量池中註冊該字符串。

在本案例中虛擬機首先會到字符串常量池中查找是否有存在"hello"字符串對應的引用. 發現沒有後會在堆內存創建"hello"字符串對象(內存地址0x0001), 然後到字符串常量池中註冊地址為0x0001的"hello"對象, 也就是添加指向0x0001的引用. 最後把字符串對象返回給s1。

溫馨提示: 圖中的字符串常量池中的數據是虛構的, 由於字符串常量池底層是用HashTable實現的, 存儲的是鍵值對, 為了方便大家理解, 示意圖簡化了字符串常量池對照表, 並採用了一些虛擬的數值。

下面看 Strings2=newString("hello");的示意圖:

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

當我們使用new關鍵字創建字符串對象的時候, JVM將不會查詢字符串常量池, 它將會直接在堆內存中創建一個字符串對象, 並返回給所屬變量。

所以s1和s2指向的是兩個完全不同的對象, 判斷s1 == s2的時候會返回false。

如果上面的知識理解起來沒有問題的話, 下面看些難點的.

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

第一行代碼 Strings1=newString("hello ")+newString("world");的執行過程是這樣子的:

1.依次在堆內存中創建"hello "和"world"兩個字符串對象

2.然後把它們拼接起來 (底層使用StringBuilder實現, 後面會帶大家讀反編譯代碼)

3.在拼接完成後會產生新的"hello world"對象, 這時變量s1指向新對象"hello world"

執行完第一行代碼後, 內存是這樣子的:

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

第二行代碼 s1.intern();

String類的源碼中有對 intern()方法的詳細介紹, 翻譯過來的意思是: 當調用 intern()方法時, 首先會去常量池中查找是否有該字符串對應的引用, 如果有就直接返回該字符串; 如果沒有, 就會在常量池中註冊該字符串的引用, 然後返回該字符串。

由於第一行代碼採用的是new的方式創建字符串, 所以在字符串常量池中沒有保存"hello world"對應的引用, 虛擬機會在常量池中進行註冊, 註冊完後的內存示意圖如下:

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

第三行代碼 Strings2="hello world";

這種直接通過雙引號""聲明字符串背後的運行機制我們在第一個案例提到過, 這裡正好複習一下。

首先虛擬機會去檢查字符串常量池, 發現有指向"hello world"的引用. 然後把該引用所指向的字符串直接返回給所屬變量。

執行完第三行代碼後, 內存示意圖如下:

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

如圖所示, s1和s2指向的是相同的對象, 所以當判斷s1 == s2時返回true。

最後我們對字符串常量池進行總結:

當用new關鍵字創建字符串對象時, 不會查詢字符串常量池; 當用雙引號直接聲明字符串對象時, 虛擬機將會查詢字符串常量池. 說白了就是: 字符串常量池提供了字符串的複用功能, 除非我們要顯式創建新的字符串對象, 否則對同一個字符串虛擬機只會維護一份拷貝。

配合反編譯代碼驗證字符串初始化操作.

相信看到這裡, 再見到有關的面試題, 你已經無所畏懼了, 因為你已經懂得了背後原理。

在結束之前我們不妨再做一道壓軸題

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

這道壓軸題是經過精心設計的, 它不但照應上面所講的字符串常量池知識, 也引出了後面的話題.

如果看這篇文章是你第一次往底層探索字符串的經歷, 那我估計你不能立即給出答案. 因為我第一次見這幾行代碼時也卡殼了。

首先第一行和第二行是常規的字符串對象聲明, 我們已經很熟悉了, 它們分別會在堆內存創建字符串對象, 並會在字符串常量池中進行註冊。

影響我們做出判斷的是第三行代碼 Strings3=s1+s2;, 我們不知道 s1+s2在創建完新字符串"hello world"後是否會在字符串常量池進行註冊。

說白了就是我們不知道這行代碼是以雙引號""形式聲明字符串, 還是用new關鍵字創建字符串。

這時, 我們應該去讀一讀這段代碼的反編譯代碼. 如果你沒有讀過反編譯代碼, 不妨藉此機會入門。

在命令行中輸入 javap-c對應.class文件的絕對路徑, 按回車後即可看到反編譯文件的代碼段。

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

  • 首先調用構造器完成Main類的初始化
  • 0:ldc#2 // String hello
  • 從常量池中獲取"hello "字符串並推送至棧頂, 此時拿到了"hello "的引用
  • 2:astore_1
  • 將棧頂的字符串引用存入第二個本地變量s1, 也就是s1已經指向了"hello "
  • 3:ldc#3 // String world
  • 5:astore_2
  • 重複開始的步驟, 此時變量s2指向"word"
  • 6:new#4 // class java/lang/StringBuilder
  • 刺激的東西來了: 這時創建了一個StringBuilder, 並把其引用值壓到棧頂
  • 9:dup
  • 複製棧頂的值, 並繼續壓入棧定, 也就意味著棧從上到下有兩份StringBuilder的引用, 將來要操作兩次StringBuilder.
  • 10:invokespecial#5 // Method java/lang/StringBuilder."<init>":()V
  • 調用StringBuilder的一些初始化方法, 靜態方法或父類方法, 完成初始化.
  • 13: aload_1
  • 把第二個本地變量也就是s1壓入棧頂, 現在棧頂從上往下數兩個數據依次是:s1變量和StringBuilder的引用
  • 14:invokevirtual#6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  • 調用StringBuilder的append方法, 棧頂的兩個數據在這裡調用方法時就用上了.
  • 接下來又調用了一次append方法(之前StringBuilder的引用拷貝兩份就用途在此)
  • 完成後, StringBuilder中已經拼接好了"hello world", 看到這裡相信大家已經明白虛擬機是如何拼接字符串的了. 接下來就是關鍵環節
  • 21:invokevirtual#7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
  • 24:astore_3
  • 拼接完字符串後, 虛擬機調用StringBuilder的 toString()方法獲得字符串 hello world, 並存放至s3.
  • 激動人心的時刻來了, 我們之所以不知道這道題的答案是因為不知道字符串拼接後是以new的形式還是以雙引號""的形式創建字符串對象.
  • 下面是我們追蹤StringBuilder的 toString()方法源碼:
從底層徹底搞懂String,StringBuilder,StringBuffer的實現


ok, 這道題解了, s3是通過new關鍵字獲得字符串對象的。

回到題目, 也就是說字符串常量表中沒有存儲"hello world"的引用, 當s4以引號的形式聲明字符串時, 由於在字符串常量池中查不到相應的引用, 所以會在堆內存中新創建一個字符串對象. 所以s3和s4指向的不是同一個字符串對象, 結果為false。

詳解字符串操作類

明白了字符串常量池, 我相信關於字符串的創建你已經有十足的把握了. 但是這還不夠, 作為一名合格的Java工程師, 我們還必須對字符串的操作做到了如指掌. 注意! 不是說你不用查api能熟練操作字符串就瞭如指掌了, 而是說對String, StringBuilder, StringBuffer三大字符串操作類背後的實現瞭然於胸, 這樣才能在開發的過程中做出正確, 高效的選擇。

String, StringBuilder, StringBuffer的底層實現

點進String的源碼, 我們可以看見String類是通過char類型數組實現的。

"

在深入學習字符串類之前, 我們先搞懂JVM是怎樣處理新生字符串的. 當你知道字符串的初始化細節後, 再去寫 Strings="hello"或 Strings=newString("hello")等代碼時, 就能做到心中有數。

首先得搞懂字符串常量池的概念。

常量池是Java的一項技術, 八種基礎數據類型除了float和double都實現了常量池技術. 這項技術從字面上是很好理解的: 把經常用到的數據存放在某塊內存中, 避免頻繁的數據創建與銷燬, 實現數據共享, 提高系統性能。

字符串常量池是Java常量池技術的一種實現, 在近代的JDK版本中(1.7後), 字符串常量池被實現在Java堆內存中。

下面通過三行代碼讓大家對字符串常量池建立初步認識:

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

我們先來看看第一行代碼 Strings1="hello";幹了什麼.

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

對於這種直接通過雙引號""聲明字符串的方式, 虛擬機首先會到字符串常量池中查找該字符串是否已經存在. 如果存在會直接返回該引用, 如果不存在則會在堆內存中創建該字符串對象, 然後到字符串常量池中註冊該字符串。

在本案例中虛擬機首先會到字符串常量池中查找是否有存在"hello"字符串對應的引用. 發現沒有後會在堆內存創建"hello"字符串對象(內存地址0x0001), 然後到字符串常量池中註冊地址為0x0001的"hello"對象, 也就是添加指向0x0001的引用. 最後把字符串對象返回給s1。

溫馨提示: 圖中的字符串常量池中的數據是虛構的, 由於字符串常量池底層是用HashTable實現的, 存儲的是鍵值對, 為了方便大家理解, 示意圖簡化了字符串常量池對照表, 並採用了一些虛擬的數值。

下面看 Strings2=newString("hello");的示意圖:

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

當我們使用new關鍵字創建字符串對象的時候, JVM將不會查詢字符串常量池, 它將會直接在堆內存中創建一個字符串對象, 並返回給所屬變量。

所以s1和s2指向的是兩個完全不同的對象, 判斷s1 == s2的時候會返回false。

如果上面的知識理解起來沒有問題的話, 下面看些難點的.

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

第一行代碼 Strings1=newString("hello ")+newString("world");的執行過程是這樣子的:

1.依次在堆內存中創建"hello "和"world"兩個字符串對象

2.然後把它們拼接起來 (底層使用StringBuilder實現, 後面會帶大家讀反編譯代碼)

3.在拼接完成後會產生新的"hello world"對象, 這時變量s1指向新對象"hello world"

執行完第一行代碼後, 內存是這樣子的:

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

第二行代碼 s1.intern();

String類的源碼中有對 intern()方法的詳細介紹, 翻譯過來的意思是: 當調用 intern()方法時, 首先會去常量池中查找是否有該字符串對應的引用, 如果有就直接返回該字符串; 如果沒有, 就會在常量池中註冊該字符串的引用, 然後返回該字符串。

由於第一行代碼採用的是new的方式創建字符串, 所以在字符串常量池中沒有保存"hello world"對應的引用, 虛擬機會在常量池中進行註冊, 註冊完後的內存示意圖如下:

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

第三行代碼 Strings2="hello world";

這種直接通過雙引號""聲明字符串背後的運行機制我們在第一個案例提到過, 這裡正好複習一下。

首先虛擬機會去檢查字符串常量池, 發現有指向"hello world"的引用. 然後把該引用所指向的字符串直接返回給所屬變量。

執行完第三行代碼後, 內存示意圖如下:

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

如圖所示, s1和s2指向的是相同的對象, 所以當判斷s1 == s2時返回true。

最後我們對字符串常量池進行總結:

當用new關鍵字創建字符串對象時, 不會查詢字符串常量池; 當用雙引號直接聲明字符串對象時, 虛擬機將會查詢字符串常量池. 說白了就是: 字符串常量池提供了字符串的複用功能, 除非我們要顯式創建新的字符串對象, 否則對同一個字符串虛擬機只會維護一份拷貝。

配合反編譯代碼驗證字符串初始化操作.

相信看到這裡, 再見到有關的面試題, 你已經無所畏懼了, 因為你已經懂得了背後原理。

在結束之前我們不妨再做一道壓軸題

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

這道壓軸題是經過精心設計的, 它不但照應上面所講的字符串常量池知識, 也引出了後面的話題.

如果看這篇文章是你第一次往底層探索字符串的經歷, 那我估計你不能立即給出答案. 因為我第一次見這幾行代碼時也卡殼了。

首先第一行和第二行是常規的字符串對象聲明, 我們已經很熟悉了, 它們分別會在堆內存創建字符串對象, 並會在字符串常量池中進行註冊。

影響我們做出判斷的是第三行代碼 Strings3=s1+s2;, 我們不知道 s1+s2在創建完新字符串"hello world"後是否會在字符串常量池進行註冊。

說白了就是我們不知道這行代碼是以雙引號""形式聲明字符串, 還是用new關鍵字創建字符串。

這時, 我們應該去讀一讀這段代碼的反編譯代碼. 如果你沒有讀過反編譯代碼, 不妨藉此機會入門。

在命令行中輸入 javap-c對應.class文件的絕對路徑, 按回車後即可看到反編譯文件的代碼段。

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

  • 首先調用構造器完成Main類的初始化
  • 0:ldc#2 // String hello
  • 從常量池中獲取"hello "字符串並推送至棧頂, 此時拿到了"hello "的引用
  • 2:astore_1
  • 將棧頂的字符串引用存入第二個本地變量s1, 也就是s1已經指向了"hello "
  • 3:ldc#3 // String world
  • 5:astore_2
  • 重複開始的步驟, 此時變量s2指向"word"
  • 6:new#4 // class java/lang/StringBuilder
  • 刺激的東西來了: 這時創建了一個StringBuilder, 並把其引用值壓到棧頂
  • 9:dup
  • 複製棧頂的值, 並繼續壓入棧定, 也就意味著棧從上到下有兩份StringBuilder的引用, 將來要操作兩次StringBuilder.
  • 10:invokespecial#5 // Method java/lang/StringBuilder."<init>":()V
  • 調用StringBuilder的一些初始化方法, 靜態方法或父類方法, 完成初始化.
  • 13: aload_1
  • 把第二個本地變量也就是s1壓入棧頂, 現在棧頂從上往下數兩個數據依次是:s1變量和StringBuilder的引用
  • 14:invokevirtual#6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  • 調用StringBuilder的append方法, 棧頂的兩個數據在這裡調用方法時就用上了.
  • 接下來又調用了一次append方法(之前StringBuilder的引用拷貝兩份就用途在此)
  • 完成後, StringBuilder中已經拼接好了"hello world", 看到這裡相信大家已經明白虛擬機是如何拼接字符串的了. 接下來就是關鍵環節
  • 21:invokevirtual#7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
  • 24:astore_3
  • 拼接完字符串後, 虛擬機調用StringBuilder的 toString()方法獲得字符串 hello world, 並存放至s3.
  • 激動人心的時刻來了, 我們之所以不知道這道題的答案是因為不知道字符串拼接後是以new的形式還是以雙引號""的形式創建字符串對象.
  • 下面是我們追蹤StringBuilder的 toString()方法源碼:
從底層徹底搞懂String,StringBuilder,StringBuffer的實現


ok, 這道題解了, s3是通過new關鍵字獲得字符串對象的。

回到題目, 也就是說字符串常量表中沒有存儲"hello world"的引用, 當s4以引號的形式聲明字符串時, 由於在字符串常量池中查不到相應的引用, 所以會在堆內存中新創建一個字符串對象. 所以s3和s4指向的不是同一個字符串對象, 結果為false。

詳解字符串操作類

明白了字符串常量池, 我相信關於字符串的創建你已經有十足的把握了. 但是這還不夠, 作為一名合格的Java工程師, 我們還必須對字符串的操作做到了如指掌. 注意! 不是說你不用查api能熟練操作字符串就瞭如指掌了, 而是說對String, StringBuilder, StringBuffer三大字符串操作類背後的實現瞭然於胸, 這樣才能在開發的過程中做出正確, 高效的選擇。

String, StringBuilder, StringBuffer的底層實現

點進String的源碼, 我們可以看見String類是通過char類型數組實現的。

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

接著查看StringBuilder和StringBuffer的源碼, 我們發現這兩者都繼承自AbstractStringBuilder類, 通過查看該類的源碼, 得知StringBuilder和StringBuffer兩個類也是通過char類型數組實現的。

"

在深入學習字符串類之前, 我們先搞懂JVM是怎樣處理新生字符串的. 當你知道字符串的初始化細節後, 再去寫 Strings="hello"或 Strings=newString("hello")等代碼時, 就能做到心中有數。

首先得搞懂字符串常量池的概念。

常量池是Java的一項技術, 八種基礎數據類型除了float和double都實現了常量池技術. 這項技術從字面上是很好理解的: 把經常用到的數據存放在某塊內存中, 避免頻繁的數據創建與銷燬, 實現數據共享, 提高系統性能。

字符串常量池是Java常量池技術的一種實現, 在近代的JDK版本中(1.7後), 字符串常量池被實現在Java堆內存中。

下面通過三行代碼讓大家對字符串常量池建立初步認識:

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

我們先來看看第一行代碼 Strings1="hello";幹了什麼.

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

對於這種直接通過雙引號""聲明字符串的方式, 虛擬機首先會到字符串常量池中查找該字符串是否已經存在. 如果存在會直接返回該引用, 如果不存在則會在堆內存中創建該字符串對象, 然後到字符串常量池中註冊該字符串。

在本案例中虛擬機首先會到字符串常量池中查找是否有存在"hello"字符串對應的引用. 發現沒有後會在堆內存創建"hello"字符串對象(內存地址0x0001), 然後到字符串常量池中註冊地址為0x0001的"hello"對象, 也就是添加指向0x0001的引用. 最後把字符串對象返回給s1。

溫馨提示: 圖中的字符串常量池中的數據是虛構的, 由於字符串常量池底層是用HashTable實現的, 存儲的是鍵值對, 為了方便大家理解, 示意圖簡化了字符串常量池對照表, 並採用了一些虛擬的數值。

下面看 Strings2=newString("hello");的示意圖:

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

當我們使用new關鍵字創建字符串對象的時候, JVM將不會查詢字符串常量池, 它將會直接在堆內存中創建一個字符串對象, 並返回給所屬變量。

所以s1和s2指向的是兩個完全不同的對象, 判斷s1 == s2的時候會返回false。

如果上面的知識理解起來沒有問題的話, 下面看些難點的.

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

第一行代碼 Strings1=newString("hello ")+newString("world");的執行過程是這樣子的:

1.依次在堆內存中創建"hello "和"world"兩個字符串對象

2.然後把它們拼接起來 (底層使用StringBuilder實現, 後面會帶大家讀反編譯代碼)

3.在拼接完成後會產生新的"hello world"對象, 這時變量s1指向新對象"hello world"

執行完第一行代碼後, 內存是這樣子的:

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

第二行代碼 s1.intern();

String類的源碼中有對 intern()方法的詳細介紹, 翻譯過來的意思是: 當調用 intern()方法時, 首先會去常量池中查找是否有該字符串對應的引用, 如果有就直接返回該字符串; 如果沒有, 就會在常量池中註冊該字符串的引用, 然後返回該字符串。

由於第一行代碼採用的是new的方式創建字符串, 所以在字符串常量池中沒有保存"hello world"對應的引用, 虛擬機會在常量池中進行註冊, 註冊完後的內存示意圖如下:

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

第三行代碼 Strings2="hello world";

這種直接通過雙引號""聲明字符串背後的運行機制我們在第一個案例提到過, 這裡正好複習一下。

首先虛擬機會去檢查字符串常量池, 發現有指向"hello world"的引用. 然後把該引用所指向的字符串直接返回給所屬變量。

執行完第三行代碼後, 內存示意圖如下:

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

如圖所示, s1和s2指向的是相同的對象, 所以當判斷s1 == s2時返回true。

最後我們對字符串常量池進行總結:

當用new關鍵字創建字符串對象時, 不會查詢字符串常量池; 當用雙引號直接聲明字符串對象時, 虛擬機將會查詢字符串常量池. 說白了就是: 字符串常量池提供了字符串的複用功能, 除非我們要顯式創建新的字符串對象, 否則對同一個字符串虛擬機只會維護一份拷貝。

配合反編譯代碼驗證字符串初始化操作.

相信看到這裡, 再見到有關的面試題, 你已經無所畏懼了, 因為你已經懂得了背後原理。

在結束之前我們不妨再做一道壓軸題

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

這道壓軸題是經過精心設計的, 它不但照應上面所講的字符串常量池知識, 也引出了後面的話題.

如果看這篇文章是你第一次往底層探索字符串的經歷, 那我估計你不能立即給出答案. 因為我第一次見這幾行代碼時也卡殼了。

首先第一行和第二行是常規的字符串對象聲明, 我們已經很熟悉了, 它們分別會在堆內存創建字符串對象, 並會在字符串常量池中進行註冊。

影響我們做出判斷的是第三行代碼 Strings3=s1+s2;, 我們不知道 s1+s2在創建完新字符串"hello world"後是否會在字符串常量池進行註冊。

說白了就是我們不知道這行代碼是以雙引號""形式聲明字符串, 還是用new關鍵字創建字符串。

這時, 我們應該去讀一讀這段代碼的反編譯代碼. 如果你沒有讀過反編譯代碼, 不妨藉此機會入門。

在命令行中輸入 javap-c對應.class文件的絕對路徑, 按回車後即可看到反編譯文件的代碼段。

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

  • 首先調用構造器完成Main類的初始化
  • 0:ldc#2 // String hello
  • 從常量池中獲取"hello "字符串並推送至棧頂, 此時拿到了"hello "的引用
  • 2:astore_1
  • 將棧頂的字符串引用存入第二個本地變量s1, 也就是s1已經指向了"hello "
  • 3:ldc#3 // String world
  • 5:astore_2
  • 重複開始的步驟, 此時變量s2指向"word"
  • 6:new#4 // class java/lang/StringBuilder
  • 刺激的東西來了: 這時創建了一個StringBuilder, 並把其引用值壓到棧頂
  • 9:dup
  • 複製棧頂的值, 並繼續壓入棧定, 也就意味著棧從上到下有兩份StringBuilder的引用, 將來要操作兩次StringBuilder.
  • 10:invokespecial#5 // Method java/lang/StringBuilder."<init>":()V
  • 調用StringBuilder的一些初始化方法, 靜態方法或父類方法, 完成初始化.
  • 13: aload_1
  • 把第二個本地變量也就是s1壓入棧頂, 現在棧頂從上往下數兩個數據依次是:s1變量和StringBuilder的引用
  • 14:invokevirtual#6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  • 調用StringBuilder的append方法, 棧頂的兩個數據在這裡調用方法時就用上了.
  • 接下來又調用了一次append方法(之前StringBuilder的引用拷貝兩份就用途在此)
  • 完成後, StringBuilder中已經拼接好了"hello world", 看到這裡相信大家已經明白虛擬機是如何拼接字符串的了. 接下來就是關鍵環節
  • 21:invokevirtual#7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
  • 24:astore_3
  • 拼接完字符串後, 虛擬機調用StringBuilder的 toString()方法獲得字符串 hello world, 並存放至s3.
  • 激動人心的時刻來了, 我們之所以不知道這道題的答案是因為不知道字符串拼接後是以new的形式還是以雙引號""的形式創建字符串對象.
  • 下面是我們追蹤StringBuilder的 toString()方法源碼:
從底層徹底搞懂String,StringBuilder,StringBuffer的實現


ok, 這道題解了, s3是通過new關鍵字獲得字符串對象的。

回到題目, 也就是說字符串常量表中沒有存儲"hello world"的引用, 當s4以引號的形式聲明字符串時, 由於在字符串常量池中查不到相應的引用, 所以會在堆內存中新創建一個字符串對象. 所以s3和s4指向的不是同一個字符串對象, 結果為false。

詳解字符串操作類

明白了字符串常量池, 我相信關於字符串的創建你已經有十足的把握了. 但是這還不夠, 作為一名合格的Java工程師, 我們還必須對字符串的操作做到了如指掌. 注意! 不是說你不用查api能熟練操作字符串就瞭如指掌了, 而是說對String, StringBuilder, StringBuffer三大字符串操作類背後的實現瞭然於胸, 這樣才能在開發的過程中做出正確, 高效的選擇。

String, StringBuilder, StringBuffer的底層實現

點進String的源碼, 我們可以看見String類是通過char類型數組實現的。

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

接著查看StringBuilder和StringBuffer的源碼, 我們發現這兩者都繼承自AbstractStringBuilder類, 通過查看該類的源碼, 得知StringBuilder和StringBuffer兩個類也是通過char類型數組實現的。

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

而且通過StringBuilder和StringBuffer繼承自同一個父類這點, 我們可以推斷出它倆的方法都是差不多的. 通過查看源碼也發現確實如此, 只不過StringBuffer在方法上添加了 synchronized關鍵字, 證明它的方法絕大多數方法都是線程同步方法. 也就是說在多線程的環境下我們應該使用StringBuffer以保證線程安全, 在單線程環境下我們應使用StringBuilder以獲得更高的效率。

既然如此, 我們的比較也就落到了StringBuilder和String身上了。

關於StringBuilder和String之間的討論

通過查看StringBuilder和String的源碼我們會發現兩者之間一個關鍵的區別: 對於String, 凡是涉及到返回參數類型為String類型的方法, 在返回的時候都會通過new關鍵字創建一個新的字符串對象; 而對於StringBuilder, 大多數方法都會返回StringBuilder對象自身。

"

在深入學習字符串類之前, 我們先搞懂JVM是怎樣處理新生字符串的. 當你知道字符串的初始化細節後, 再去寫 Strings="hello"或 Strings=newString("hello")等代碼時, 就能做到心中有數。

首先得搞懂字符串常量池的概念。

常量池是Java的一項技術, 八種基礎數據類型除了float和double都實現了常量池技術. 這項技術從字面上是很好理解的: 把經常用到的數據存放在某塊內存中, 避免頻繁的數據創建與銷燬, 實現數據共享, 提高系統性能。

字符串常量池是Java常量池技術的一種實現, 在近代的JDK版本中(1.7後), 字符串常量池被實現在Java堆內存中。

下面通過三行代碼讓大家對字符串常量池建立初步認識:

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

我們先來看看第一行代碼 Strings1="hello";幹了什麼.

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

對於這種直接通過雙引號""聲明字符串的方式, 虛擬機首先會到字符串常量池中查找該字符串是否已經存在. 如果存在會直接返回該引用, 如果不存在則會在堆內存中創建該字符串對象, 然後到字符串常量池中註冊該字符串。

在本案例中虛擬機首先會到字符串常量池中查找是否有存在"hello"字符串對應的引用. 發現沒有後會在堆內存創建"hello"字符串對象(內存地址0x0001), 然後到字符串常量池中註冊地址為0x0001的"hello"對象, 也就是添加指向0x0001的引用. 最後把字符串對象返回給s1。

溫馨提示: 圖中的字符串常量池中的數據是虛構的, 由於字符串常量池底層是用HashTable實現的, 存儲的是鍵值對, 為了方便大家理解, 示意圖簡化了字符串常量池對照表, 並採用了一些虛擬的數值。

下面看 Strings2=newString("hello");的示意圖:

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

當我們使用new關鍵字創建字符串對象的時候, JVM將不會查詢字符串常量池, 它將會直接在堆內存中創建一個字符串對象, 並返回給所屬變量。

所以s1和s2指向的是兩個完全不同的對象, 判斷s1 == s2的時候會返回false。

如果上面的知識理解起來沒有問題的話, 下面看些難點的.

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

第一行代碼 Strings1=newString("hello ")+newString("world");的執行過程是這樣子的:

1.依次在堆內存中創建"hello "和"world"兩個字符串對象

2.然後把它們拼接起來 (底層使用StringBuilder實現, 後面會帶大家讀反編譯代碼)

3.在拼接完成後會產生新的"hello world"對象, 這時變量s1指向新對象"hello world"

執行完第一行代碼後, 內存是這樣子的:

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

第二行代碼 s1.intern();

String類的源碼中有對 intern()方法的詳細介紹, 翻譯過來的意思是: 當調用 intern()方法時, 首先會去常量池中查找是否有該字符串對應的引用, 如果有就直接返回該字符串; 如果沒有, 就會在常量池中註冊該字符串的引用, 然後返回該字符串。

由於第一行代碼採用的是new的方式創建字符串, 所以在字符串常量池中沒有保存"hello world"對應的引用, 虛擬機會在常量池中進行註冊, 註冊完後的內存示意圖如下:

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

第三行代碼 Strings2="hello world";

這種直接通過雙引號""聲明字符串背後的運行機制我們在第一個案例提到過, 這裡正好複習一下。

首先虛擬機會去檢查字符串常量池, 發現有指向"hello world"的引用. 然後把該引用所指向的字符串直接返回給所屬變量。

執行完第三行代碼後, 內存示意圖如下:

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

如圖所示, s1和s2指向的是相同的對象, 所以當判斷s1 == s2時返回true。

最後我們對字符串常量池進行總結:

當用new關鍵字創建字符串對象時, 不會查詢字符串常量池; 當用雙引號直接聲明字符串對象時, 虛擬機將會查詢字符串常量池. 說白了就是: 字符串常量池提供了字符串的複用功能, 除非我們要顯式創建新的字符串對象, 否則對同一個字符串虛擬機只會維護一份拷貝。

配合反編譯代碼驗證字符串初始化操作.

相信看到這裡, 再見到有關的面試題, 你已經無所畏懼了, 因為你已經懂得了背後原理。

在結束之前我們不妨再做一道壓軸題

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

這道壓軸題是經過精心設計的, 它不但照應上面所講的字符串常量池知識, 也引出了後面的話題.

如果看這篇文章是你第一次往底層探索字符串的經歷, 那我估計你不能立即給出答案. 因為我第一次見這幾行代碼時也卡殼了。

首先第一行和第二行是常規的字符串對象聲明, 我們已經很熟悉了, 它們分別會在堆內存創建字符串對象, 並會在字符串常量池中進行註冊。

影響我們做出判斷的是第三行代碼 Strings3=s1+s2;, 我們不知道 s1+s2在創建完新字符串"hello world"後是否會在字符串常量池進行註冊。

說白了就是我們不知道這行代碼是以雙引號""形式聲明字符串, 還是用new關鍵字創建字符串。

這時, 我們應該去讀一讀這段代碼的反編譯代碼. 如果你沒有讀過反編譯代碼, 不妨藉此機會入門。

在命令行中輸入 javap-c對應.class文件的絕對路徑, 按回車後即可看到反編譯文件的代碼段。

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

  • 首先調用構造器完成Main類的初始化
  • 0:ldc#2 // String hello
  • 從常量池中獲取"hello "字符串並推送至棧頂, 此時拿到了"hello "的引用
  • 2:astore_1
  • 將棧頂的字符串引用存入第二個本地變量s1, 也就是s1已經指向了"hello "
  • 3:ldc#3 // String world
  • 5:astore_2
  • 重複開始的步驟, 此時變量s2指向"word"
  • 6:new#4 // class java/lang/StringBuilder
  • 刺激的東西來了: 這時創建了一個StringBuilder, 並把其引用值壓到棧頂
  • 9:dup
  • 複製棧頂的值, 並繼續壓入棧定, 也就意味著棧從上到下有兩份StringBuilder的引用, 將來要操作兩次StringBuilder.
  • 10:invokespecial#5 // Method java/lang/StringBuilder."<init>":()V
  • 調用StringBuilder的一些初始化方法, 靜態方法或父類方法, 完成初始化.
  • 13: aload_1
  • 把第二個本地變量也就是s1壓入棧頂, 現在棧頂從上往下數兩個數據依次是:s1變量和StringBuilder的引用
  • 14:invokevirtual#6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  • 調用StringBuilder的append方法, 棧頂的兩個數據在這裡調用方法時就用上了.
  • 接下來又調用了一次append方法(之前StringBuilder的引用拷貝兩份就用途在此)
  • 完成後, StringBuilder中已經拼接好了"hello world", 看到這裡相信大家已經明白虛擬機是如何拼接字符串的了. 接下來就是關鍵環節
  • 21:invokevirtual#7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
  • 24:astore_3
  • 拼接完字符串後, 虛擬機調用StringBuilder的 toString()方法獲得字符串 hello world, 並存放至s3.
  • 激動人心的時刻來了, 我們之所以不知道這道題的答案是因為不知道字符串拼接後是以new的形式還是以雙引號""的形式創建字符串對象.
  • 下面是我們追蹤StringBuilder的 toString()方法源碼:
從底層徹底搞懂String,StringBuilder,StringBuffer的實現


ok, 這道題解了, s3是通過new關鍵字獲得字符串對象的。

回到題目, 也就是說字符串常量表中沒有存儲"hello world"的引用, 當s4以引號的形式聲明字符串時, 由於在字符串常量池中查不到相應的引用, 所以會在堆內存中新創建一個字符串對象. 所以s3和s4指向的不是同一個字符串對象, 結果為false。

詳解字符串操作類

明白了字符串常量池, 我相信關於字符串的創建你已經有十足的把握了. 但是這還不夠, 作為一名合格的Java工程師, 我們還必須對字符串的操作做到了如指掌. 注意! 不是說你不用查api能熟練操作字符串就瞭如指掌了, 而是說對String, StringBuilder, StringBuffer三大字符串操作類背後的實現瞭然於胸, 這樣才能在開發的過程中做出正確, 高效的選擇。

String, StringBuilder, StringBuffer的底層實現

點進String的源碼, 我們可以看見String類是通過char類型數組實現的。

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

接著查看StringBuilder和StringBuffer的源碼, 我們發現這兩者都繼承自AbstractStringBuilder類, 通過查看該類的源碼, 得知StringBuilder和StringBuffer兩個類也是通過char類型數組實現的。

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

而且通過StringBuilder和StringBuffer繼承自同一個父類這點, 我們可以推斷出它倆的方法都是差不多的. 通過查看源碼也發現確實如此, 只不過StringBuffer在方法上添加了 synchronized關鍵字, 證明它的方法絕大多數方法都是線程同步方法. 也就是說在多線程的環境下我們應該使用StringBuffer以保證線程安全, 在單線程環境下我們應使用StringBuilder以獲得更高的效率。

既然如此, 我們的比較也就落到了StringBuilder和String身上了。

關於StringBuilder和String之間的討論

通過查看StringBuilder和String的源碼我們會發現兩者之間一個關鍵的區別: 對於String, 凡是涉及到返回參數類型為String類型的方法, 在返回的時候都會通過new關鍵字創建一個新的字符串對象; 而對於StringBuilder, 大多數方法都會返回StringBuilder對象自身。

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

就因為這點區別, 使得兩者在操作字符串時在不同的場景下會體現出不同的效率。

下面還是以拼接字符串為例比較一下兩者的性能:

"

在深入學習字符串類之前, 我們先搞懂JVM是怎樣處理新生字符串的. 當你知道字符串的初始化細節後, 再去寫 Strings="hello"或 Strings=newString("hello")等代碼時, 就能做到心中有數。

首先得搞懂字符串常量池的概念。

常量池是Java的一項技術, 八種基礎數據類型除了float和double都實現了常量池技術. 這項技術從字面上是很好理解的: 把經常用到的數據存放在某塊內存中, 避免頻繁的數據創建與銷燬, 實現數據共享, 提高系統性能。

字符串常量池是Java常量池技術的一種實現, 在近代的JDK版本中(1.7後), 字符串常量池被實現在Java堆內存中。

下面通過三行代碼讓大家對字符串常量池建立初步認識:

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

我們先來看看第一行代碼 Strings1="hello";幹了什麼.

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

對於這種直接通過雙引號""聲明字符串的方式, 虛擬機首先會到字符串常量池中查找該字符串是否已經存在. 如果存在會直接返回該引用, 如果不存在則會在堆內存中創建該字符串對象, 然後到字符串常量池中註冊該字符串。

在本案例中虛擬機首先會到字符串常量池中查找是否有存在"hello"字符串對應的引用. 發現沒有後會在堆內存創建"hello"字符串對象(內存地址0x0001), 然後到字符串常量池中註冊地址為0x0001的"hello"對象, 也就是添加指向0x0001的引用. 最後把字符串對象返回給s1。

溫馨提示: 圖中的字符串常量池中的數據是虛構的, 由於字符串常量池底層是用HashTable實現的, 存儲的是鍵值對, 為了方便大家理解, 示意圖簡化了字符串常量池對照表, 並採用了一些虛擬的數值。

下面看 Strings2=newString("hello");的示意圖:

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

當我們使用new關鍵字創建字符串對象的時候, JVM將不會查詢字符串常量池, 它將會直接在堆內存中創建一個字符串對象, 並返回給所屬變量。

所以s1和s2指向的是兩個完全不同的對象, 判斷s1 == s2的時候會返回false。

如果上面的知識理解起來沒有問題的話, 下面看些難點的.

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

第一行代碼 Strings1=newString("hello ")+newString("world");的執行過程是這樣子的:

1.依次在堆內存中創建"hello "和"world"兩個字符串對象

2.然後把它們拼接起來 (底層使用StringBuilder實現, 後面會帶大家讀反編譯代碼)

3.在拼接完成後會產生新的"hello world"對象, 這時變量s1指向新對象"hello world"

執行完第一行代碼後, 內存是這樣子的:

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

第二行代碼 s1.intern();

String類的源碼中有對 intern()方法的詳細介紹, 翻譯過來的意思是: 當調用 intern()方法時, 首先會去常量池中查找是否有該字符串對應的引用, 如果有就直接返回該字符串; 如果沒有, 就會在常量池中註冊該字符串的引用, 然後返回該字符串。

由於第一行代碼採用的是new的方式創建字符串, 所以在字符串常量池中沒有保存"hello world"對應的引用, 虛擬機會在常量池中進行註冊, 註冊完後的內存示意圖如下:

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

第三行代碼 Strings2="hello world";

這種直接通過雙引號""聲明字符串背後的運行機制我們在第一個案例提到過, 這裡正好複習一下。

首先虛擬機會去檢查字符串常量池, 發現有指向"hello world"的引用. 然後把該引用所指向的字符串直接返回給所屬變量。

執行完第三行代碼後, 內存示意圖如下:

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

如圖所示, s1和s2指向的是相同的對象, 所以當判斷s1 == s2時返回true。

最後我們對字符串常量池進行總結:

當用new關鍵字創建字符串對象時, 不會查詢字符串常量池; 當用雙引號直接聲明字符串對象時, 虛擬機將會查詢字符串常量池. 說白了就是: 字符串常量池提供了字符串的複用功能, 除非我們要顯式創建新的字符串對象, 否則對同一個字符串虛擬機只會維護一份拷貝。

配合反編譯代碼驗證字符串初始化操作.

相信看到這裡, 再見到有關的面試題, 你已經無所畏懼了, 因為你已經懂得了背後原理。

在結束之前我們不妨再做一道壓軸題

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

這道壓軸題是經過精心設計的, 它不但照應上面所講的字符串常量池知識, 也引出了後面的話題.

如果看這篇文章是你第一次往底層探索字符串的經歷, 那我估計你不能立即給出答案. 因為我第一次見這幾行代碼時也卡殼了。

首先第一行和第二行是常規的字符串對象聲明, 我們已經很熟悉了, 它們分別會在堆內存創建字符串對象, 並會在字符串常量池中進行註冊。

影響我們做出判斷的是第三行代碼 Strings3=s1+s2;, 我們不知道 s1+s2在創建完新字符串"hello world"後是否會在字符串常量池進行註冊。

說白了就是我們不知道這行代碼是以雙引號""形式聲明字符串, 還是用new關鍵字創建字符串。

這時, 我們應該去讀一讀這段代碼的反編譯代碼. 如果你沒有讀過反編譯代碼, 不妨藉此機會入門。

在命令行中輸入 javap-c對應.class文件的絕對路徑, 按回車後即可看到反編譯文件的代碼段。

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

  • 首先調用構造器完成Main類的初始化
  • 0:ldc#2 // String hello
  • 從常量池中獲取"hello "字符串並推送至棧頂, 此時拿到了"hello "的引用
  • 2:astore_1
  • 將棧頂的字符串引用存入第二個本地變量s1, 也就是s1已經指向了"hello "
  • 3:ldc#3 // String world
  • 5:astore_2
  • 重複開始的步驟, 此時變量s2指向"word"
  • 6:new#4 // class java/lang/StringBuilder
  • 刺激的東西來了: 這時創建了一個StringBuilder, 並把其引用值壓到棧頂
  • 9:dup
  • 複製棧頂的值, 並繼續壓入棧定, 也就意味著棧從上到下有兩份StringBuilder的引用, 將來要操作兩次StringBuilder.
  • 10:invokespecial#5 // Method java/lang/StringBuilder."<init>":()V
  • 調用StringBuilder的一些初始化方法, 靜態方法或父類方法, 完成初始化.
  • 13: aload_1
  • 把第二個本地變量也就是s1壓入棧頂, 現在棧頂從上往下數兩個數據依次是:s1變量和StringBuilder的引用
  • 14:invokevirtual#6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  • 調用StringBuilder的append方法, 棧頂的兩個數據在這裡調用方法時就用上了.
  • 接下來又調用了一次append方法(之前StringBuilder的引用拷貝兩份就用途在此)
  • 完成後, StringBuilder中已經拼接好了"hello world", 看到這裡相信大家已經明白虛擬機是如何拼接字符串的了. 接下來就是關鍵環節
  • 21:invokevirtual#7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
  • 24:astore_3
  • 拼接完字符串後, 虛擬機調用StringBuilder的 toString()方法獲得字符串 hello world, 並存放至s3.
  • 激動人心的時刻來了, 我們之所以不知道這道題的答案是因為不知道字符串拼接後是以new的形式還是以雙引號""的形式創建字符串對象.
  • 下面是我們追蹤StringBuilder的 toString()方法源碼:
從底層徹底搞懂String,StringBuilder,StringBuffer的實現


ok, 這道題解了, s3是通過new關鍵字獲得字符串對象的。

回到題目, 也就是說字符串常量表中沒有存儲"hello world"的引用, 當s4以引號的形式聲明字符串時, 由於在字符串常量池中查不到相應的引用, 所以會在堆內存中新創建一個字符串對象. 所以s3和s4指向的不是同一個字符串對象, 結果為false。

詳解字符串操作類

明白了字符串常量池, 我相信關於字符串的創建你已經有十足的把握了. 但是這還不夠, 作為一名合格的Java工程師, 我們還必須對字符串的操作做到了如指掌. 注意! 不是說你不用查api能熟練操作字符串就瞭如指掌了, 而是說對String, StringBuilder, StringBuffer三大字符串操作類背後的實現瞭然於胸, 這樣才能在開發的過程中做出正確, 高效的選擇。

String, StringBuilder, StringBuffer的底層實現

點進String的源碼, 我們可以看見String類是通過char類型數組實現的。

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

接著查看StringBuilder和StringBuffer的源碼, 我們發現這兩者都繼承自AbstractStringBuilder類, 通過查看該類的源碼, 得知StringBuilder和StringBuffer兩個類也是通過char類型數組實現的。

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

而且通過StringBuilder和StringBuffer繼承自同一個父類這點, 我們可以推斷出它倆的方法都是差不多的. 通過查看源碼也發現確實如此, 只不過StringBuffer在方法上添加了 synchronized關鍵字, 證明它的方法絕大多數方法都是線程同步方法. 也就是說在多線程的環境下我們應該使用StringBuffer以保證線程安全, 在單線程環境下我們應使用StringBuilder以獲得更高的效率。

既然如此, 我們的比較也就落到了StringBuilder和String身上了。

關於StringBuilder和String之間的討論

通過查看StringBuilder和String的源碼我們會發現兩者之間一個關鍵的區別: 對於String, 凡是涉及到返回參數類型為String類型的方法, 在返回的時候都會通過new關鍵字創建一個新的字符串對象; 而對於StringBuilder, 大多數方法都會返回StringBuilder對象自身。

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

就因為這點區別, 使得兩者在操作字符串時在不同的場景下會體現出不同的效率。

下面還是以拼接字符串為例比較一下兩者的性能:

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

就拼接5萬次字符串而言, StringBuilder的效率是String類的956倍。

我們再次通過反編譯代碼看看造成兩者性能差距的原因, 先看String類. (為了方便閱讀代碼, 我刪除了計時部分的代碼, 並重新編譯, 得到的main方法反編譯代碼如下)

"

在深入學習字符串類之前, 我們先搞懂JVM是怎樣處理新生字符串的. 當你知道字符串的初始化細節後, 再去寫 Strings="hello"或 Strings=newString("hello")等代碼時, 就能做到心中有數。

首先得搞懂字符串常量池的概念。

常量池是Java的一項技術, 八種基礎數據類型除了float和double都實現了常量池技術. 這項技術從字面上是很好理解的: 把經常用到的數據存放在某塊內存中, 避免頻繁的數據創建與銷燬, 實現數據共享, 提高系統性能。

字符串常量池是Java常量池技術的一種實現, 在近代的JDK版本中(1.7後), 字符串常量池被實現在Java堆內存中。

下面通過三行代碼讓大家對字符串常量池建立初步認識:

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

我們先來看看第一行代碼 Strings1="hello";幹了什麼.

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

對於這種直接通過雙引號""聲明字符串的方式, 虛擬機首先會到字符串常量池中查找該字符串是否已經存在. 如果存在會直接返回該引用, 如果不存在則會在堆內存中創建該字符串對象, 然後到字符串常量池中註冊該字符串。

在本案例中虛擬機首先會到字符串常量池中查找是否有存在"hello"字符串對應的引用. 發現沒有後會在堆內存創建"hello"字符串對象(內存地址0x0001), 然後到字符串常量池中註冊地址為0x0001的"hello"對象, 也就是添加指向0x0001的引用. 最後把字符串對象返回給s1。

溫馨提示: 圖中的字符串常量池中的數據是虛構的, 由於字符串常量池底層是用HashTable實現的, 存儲的是鍵值對, 為了方便大家理解, 示意圖簡化了字符串常量池對照表, 並採用了一些虛擬的數值。

下面看 Strings2=newString("hello");的示意圖:

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

當我們使用new關鍵字創建字符串對象的時候, JVM將不會查詢字符串常量池, 它將會直接在堆內存中創建一個字符串對象, 並返回給所屬變量。

所以s1和s2指向的是兩個完全不同的對象, 判斷s1 == s2的時候會返回false。

如果上面的知識理解起來沒有問題的話, 下面看些難點的.

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

第一行代碼 Strings1=newString("hello ")+newString("world");的執行過程是這樣子的:

1.依次在堆內存中創建"hello "和"world"兩個字符串對象

2.然後把它們拼接起來 (底層使用StringBuilder實現, 後面會帶大家讀反編譯代碼)

3.在拼接完成後會產生新的"hello world"對象, 這時變量s1指向新對象"hello world"

執行完第一行代碼後, 內存是這樣子的:

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

第二行代碼 s1.intern();

String類的源碼中有對 intern()方法的詳細介紹, 翻譯過來的意思是: 當調用 intern()方法時, 首先會去常量池中查找是否有該字符串對應的引用, 如果有就直接返回該字符串; 如果沒有, 就會在常量池中註冊該字符串的引用, 然後返回該字符串。

由於第一行代碼採用的是new的方式創建字符串, 所以在字符串常量池中沒有保存"hello world"對應的引用, 虛擬機會在常量池中進行註冊, 註冊完後的內存示意圖如下:

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

第三行代碼 Strings2="hello world";

這種直接通過雙引號""聲明字符串背後的運行機制我們在第一個案例提到過, 這裡正好複習一下。

首先虛擬機會去檢查字符串常量池, 發現有指向"hello world"的引用. 然後把該引用所指向的字符串直接返回給所屬變量。

執行完第三行代碼後, 內存示意圖如下:

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

如圖所示, s1和s2指向的是相同的對象, 所以當判斷s1 == s2時返回true。

最後我們對字符串常量池進行總結:

當用new關鍵字創建字符串對象時, 不會查詢字符串常量池; 當用雙引號直接聲明字符串對象時, 虛擬機將會查詢字符串常量池. 說白了就是: 字符串常量池提供了字符串的複用功能, 除非我們要顯式創建新的字符串對象, 否則對同一個字符串虛擬機只會維護一份拷貝。

配合反編譯代碼驗證字符串初始化操作.

相信看到這裡, 再見到有關的面試題, 你已經無所畏懼了, 因為你已經懂得了背後原理。

在結束之前我們不妨再做一道壓軸題

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

這道壓軸題是經過精心設計的, 它不但照應上面所講的字符串常量池知識, 也引出了後面的話題.

如果看這篇文章是你第一次往底層探索字符串的經歷, 那我估計你不能立即給出答案. 因為我第一次見這幾行代碼時也卡殼了。

首先第一行和第二行是常規的字符串對象聲明, 我們已經很熟悉了, 它們分別會在堆內存創建字符串對象, 並會在字符串常量池中進行註冊。

影響我們做出判斷的是第三行代碼 Strings3=s1+s2;, 我們不知道 s1+s2在創建完新字符串"hello world"後是否會在字符串常量池進行註冊。

說白了就是我們不知道這行代碼是以雙引號""形式聲明字符串, 還是用new關鍵字創建字符串。

這時, 我們應該去讀一讀這段代碼的反編譯代碼. 如果你沒有讀過反編譯代碼, 不妨藉此機會入門。

在命令行中輸入 javap-c對應.class文件的絕對路徑, 按回車後即可看到反編譯文件的代碼段。

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

  • 首先調用構造器完成Main類的初始化
  • 0:ldc#2 // String hello
  • 從常量池中獲取"hello "字符串並推送至棧頂, 此時拿到了"hello "的引用
  • 2:astore_1
  • 將棧頂的字符串引用存入第二個本地變量s1, 也就是s1已經指向了"hello "
  • 3:ldc#3 // String world
  • 5:astore_2
  • 重複開始的步驟, 此時變量s2指向"word"
  • 6:new#4 // class java/lang/StringBuilder
  • 刺激的東西來了: 這時創建了一個StringBuilder, 並把其引用值壓到棧頂
  • 9:dup
  • 複製棧頂的值, 並繼續壓入棧定, 也就意味著棧從上到下有兩份StringBuilder的引用, 將來要操作兩次StringBuilder.
  • 10:invokespecial#5 // Method java/lang/StringBuilder."<init>":()V
  • 調用StringBuilder的一些初始化方法, 靜態方法或父類方法, 完成初始化.
  • 13: aload_1
  • 把第二個本地變量也就是s1壓入棧頂, 現在棧頂從上往下數兩個數據依次是:s1變量和StringBuilder的引用
  • 14:invokevirtual#6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  • 調用StringBuilder的append方法, 棧頂的兩個數據在這裡調用方法時就用上了.
  • 接下來又調用了一次append方法(之前StringBuilder的引用拷貝兩份就用途在此)
  • 完成後, StringBuilder中已經拼接好了"hello world", 看到這裡相信大家已經明白虛擬機是如何拼接字符串的了. 接下來就是關鍵環節
  • 21:invokevirtual#7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
  • 24:astore_3
  • 拼接完字符串後, 虛擬機調用StringBuilder的 toString()方法獲得字符串 hello world, 並存放至s3.
  • 激動人心的時刻來了, 我們之所以不知道這道題的答案是因為不知道字符串拼接後是以new的形式還是以雙引號""的形式創建字符串對象.
  • 下面是我們追蹤StringBuilder的 toString()方法源碼:
從底層徹底搞懂String,StringBuilder,StringBuffer的實現


ok, 這道題解了, s3是通過new關鍵字獲得字符串對象的。

回到題目, 也就是說字符串常量表中沒有存儲"hello world"的引用, 當s4以引號的形式聲明字符串時, 由於在字符串常量池中查不到相應的引用, 所以會在堆內存中新創建一個字符串對象. 所以s3和s4指向的不是同一個字符串對象, 結果為false。

詳解字符串操作類

明白了字符串常量池, 我相信關於字符串的創建你已經有十足的把握了. 但是這還不夠, 作為一名合格的Java工程師, 我們還必須對字符串的操作做到了如指掌. 注意! 不是說你不用查api能熟練操作字符串就瞭如指掌了, 而是說對String, StringBuilder, StringBuffer三大字符串操作類背後的實現瞭然於胸, 這樣才能在開發的過程中做出正確, 高效的選擇。

String, StringBuilder, StringBuffer的底層實現

點進String的源碼, 我們可以看見String類是通過char類型數組實現的。

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

接著查看StringBuilder和StringBuffer的源碼, 我們發現這兩者都繼承自AbstractStringBuilder類, 通過查看該類的源碼, 得知StringBuilder和StringBuffer兩個類也是通過char類型數組實現的。

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

而且通過StringBuilder和StringBuffer繼承自同一個父類這點, 我們可以推斷出它倆的方法都是差不多的. 通過查看源碼也發現確實如此, 只不過StringBuffer在方法上添加了 synchronized關鍵字, 證明它的方法絕大多數方法都是線程同步方法. 也就是說在多線程的環境下我們應該使用StringBuffer以保證線程安全, 在單線程環境下我們應使用StringBuilder以獲得更高的效率。

既然如此, 我們的比較也就落到了StringBuilder和String身上了。

關於StringBuilder和String之間的討論

通過查看StringBuilder和String的源碼我們會發現兩者之間一個關鍵的區別: 對於String, 凡是涉及到返回參數類型為String類型的方法, 在返回的時候都會通過new關鍵字創建一個新的字符串對象; 而對於StringBuilder, 大多數方法都會返回StringBuilder對象自身。

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

就因為這點區別, 使得兩者在操作字符串時在不同的場景下會體現出不同的效率。

下面還是以拼接字符串為例比較一下兩者的性能:

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

就拼接5萬次字符串而言, StringBuilder的效率是String類的956倍。

我們再次通過反編譯代碼看看造成兩者性能差距的原因, 先看String類. (為了方便閱讀代碼, 我刪除了計時部分的代碼, 並重新編譯, 得到的main方法反編譯代碼如下)

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

從反彙編代碼中可以看到, 當用String類拼接字符串時, 每次都會生成一個StringBuilder對象, 然後調用兩次append()方法把字符串拼接好, 最後通過StringBuilder的toString()方法new出一個新的字符串對象。

也就是說每次拼接都會new出兩個對象, 並進行兩次方法調用, 如果拼接的次數過多, 創建對象所帶來的時延會降低系統效率, 同時會造成巨大的內存浪費. 而且當內存不夠用時, 虛擬機會進行垃圾回收, 這也是一項相當耗時的操作, 會大大降低系統性能。

下面是使用StringBuilder拼接字符串得到的反編譯代碼:

"

在深入學習字符串類之前, 我們先搞懂JVM是怎樣處理新生字符串的. 當你知道字符串的初始化細節後, 再去寫 Strings="hello"或 Strings=newString("hello")等代碼時, 就能做到心中有數。

首先得搞懂字符串常量池的概念。

常量池是Java的一項技術, 八種基礎數據類型除了float和double都實現了常量池技術. 這項技術從字面上是很好理解的: 把經常用到的數據存放在某塊內存中, 避免頻繁的數據創建與銷燬, 實現數據共享, 提高系統性能。

字符串常量池是Java常量池技術的一種實現, 在近代的JDK版本中(1.7後), 字符串常量池被實現在Java堆內存中。

下面通過三行代碼讓大家對字符串常量池建立初步認識:

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

我們先來看看第一行代碼 Strings1="hello";幹了什麼.

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

對於這種直接通過雙引號""聲明字符串的方式, 虛擬機首先會到字符串常量池中查找該字符串是否已經存在. 如果存在會直接返回該引用, 如果不存在則會在堆內存中創建該字符串對象, 然後到字符串常量池中註冊該字符串。

在本案例中虛擬機首先會到字符串常量池中查找是否有存在"hello"字符串對應的引用. 發現沒有後會在堆內存創建"hello"字符串對象(內存地址0x0001), 然後到字符串常量池中註冊地址為0x0001的"hello"對象, 也就是添加指向0x0001的引用. 最後把字符串對象返回給s1。

溫馨提示: 圖中的字符串常量池中的數據是虛構的, 由於字符串常量池底層是用HashTable實現的, 存儲的是鍵值對, 為了方便大家理解, 示意圖簡化了字符串常量池對照表, 並採用了一些虛擬的數值。

下面看 Strings2=newString("hello");的示意圖:

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

當我們使用new關鍵字創建字符串對象的時候, JVM將不會查詢字符串常量池, 它將會直接在堆內存中創建一個字符串對象, 並返回給所屬變量。

所以s1和s2指向的是兩個完全不同的對象, 判斷s1 == s2的時候會返回false。

如果上面的知識理解起來沒有問題的話, 下面看些難點的.

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

第一行代碼 Strings1=newString("hello ")+newString("world");的執行過程是這樣子的:

1.依次在堆內存中創建"hello "和"world"兩個字符串對象

2.然後把它們拼接起來 (底層使用StringBuilder實現, 後面會帶大家讀反編譯代碼)

3.在拼接完成後會產生新的"hello world"對象, 這時變量s1指向新對象"hello world"

執行完第一行代碼後, 內存是這樣子的:

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

第二行代碼 s1.intern();

String類的源碼中有對 intern()方法的詳細介紹, 翻譯過來的意思是: 當調用 intern()方法時, 首先會去常量池中查找是否有該字符串對應的引用, 如果有就直接返回該字符串; 如果沒有, 就會在常量池中註冊該字符串的引用, 然後返回該字符串。

由於第一行代碼採用的是new的方式創建字符串, 所以在字符串常量池中沒有保存"hello world"對應的引用, 虛擬機會在常量池中進行註冊, 註冊完後的內存示意圖如下:

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

第三行代碼 Strings2="hello world";

這種直接通過雙引號""聲明字符串背後的運行機制我們在第一個案例提到過, 這裡正好複習一下。

首先虛擬機會去檢查字符串常量池, 發現有指向"hello world"的引用. 然後把該引用所指向的字符串直接返回給所屬變量。

執行完第三行代碼後, 內存示意圖如下:

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

如圖所示, s1和s2指向的是相同的對象, 所以當判斷s1 == s2時返回true。

最後我們對字符串常量池進行總結:

當用new關鍵字創建字符串對象時, 不會查詢字符串常量池; 當用雙引號直接聲明字符串對象時, 虛擬機將會查詢字符串常量池. 說白了就是: 字符串常量池提供了字符串的複用功能, 除非我們要顯式創建新的字符串對象, 否則對同一個字符串虛擬機只會維護一份拷貝。

配合反編譯代碼驗證字符串初始化操作.

相信看到這裡, 再見到有關的面試題, 你已經無所畏懼了, 因為你已經懂得了背後原理。

在結束之前我們不妨再做一道壓軸題

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

這道壓軸題是經過精心設計的, 它不但照應上面所講的字符串常量池知識, 也引出了後面的話題.

如果看這篇文章是你第一次往底層探索字符串的經歷, 那我估計你不能立即給出答案. 因為我第一次見這幾行代碼時也卡殼了。

首先第一行和第二行是常規的字符串對象聲明, 我們已經很熟悉了, 它們分別會在堆內存創建字符串對象, 並會在字符串常量池中進行註冊。

影響我們做出判斷的是第三行代碼 Strings3=s1+s2;, 我們不知道 s1+s2在創建完新字符串"hello world"後是否會在字符串常量池進行註冊。

說白了就是我們不知道這行代碼是以雙引號""形式聲明字符串, 還是用new關鍵字創建字符串。

這時, 我們應該去讀一讀這段代碼的反編譯代碼. 如果你沒有讀過反編譯代碼, 不妨藉此機會入門。

在命令行中輸入 javap-c對應.class文件的絕對路徑, 按回車後即可看到反編譯文件的代碼段。

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

  • 首先調用構造器完成Main類的初始化
  • 0:ldc#2 // String hello
  • 從常量池中獲取"hello "字符串並推送至棧頂, 此時拿到了"hello "的引用
  • 2:astore_1
  • 將棧頂的字符串引用存入第二個本地變量s1, 也就是s1已經指向了"hello "
  • 3:ldc#3 // String world
  • 5:astore_2
  • 重複開始的步驟, 此時變量s2指向"word"
  • 6:new#4 // class java/lang/StringBuilder
  • 刺激的東西來了: 這時創建了一個StringBuilder, 並把其引用值壓到棧頂
  • 9:dup
  • 複製棧頂的值, 並繼續壓入棧定, 也就意味著棧從上到下有兩份StringBuilder的引用, 將來要操作兩次StringBuilder.
  • 10:invokespecial#5 // Method java/lang/StringBuilder."<init>":()V
  • 調用StringBuilder的一些初始化方法, 靜態方法或父類方法, 完成初始化.
  • 13: aload_1
  • 把第二個本地變量也就是s1壓入棧頂, 現在棧頂從上往下數兩個數據依次是:s1變量和StringBuilder的引用
  • 14:invokevirtual#6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  • 調用StringBuilder的append方法, 棧頂的兩個數據在這裡調用方法時就用上了.
  • 接下來又調用了一次append方法(之前StringBuilder的引用拷貝兩份就用途在此)
  • 完成後, StringBuilder中已經拼接好了"hello world", 看到這裡相信大家已經明白虛擬機是如何拼接字符串的了. 接下來就是關鍵環節
  • 21:invokevirtual#7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
  • 24:astore_3
  • 拼接完字符串後, 虛擬機調用StringBuilder的 toString()方法獲得字符串 hello world, 並存放至s3.
  • 激動人心的時刻來了, 我們之所以不知道這道題的答案是因為不知道字符串拼接後是以new的形式還是以雙引號""的形式創建字符串對象.
  • 下面是我們追蹤StringBuilder的 toString()方法源碼:
從底層徹底搞懂String,StringBuilder,StringBuffer的實現


ok, 這道題解了, s3是通過new關鍵字獲得字符串對象的。

回到題目, 也就是說字符串常量表中沒有存儲"hello world"的引用, 當s4以引號的形式聲明字符串時, 由於在字符串常量池中查不到相應的引用, 所以會在堆內存中新創建一個字符串對象. 所以s3和s4指向的不是同一個字符串對象, 結果為false。

詳解字符串操作類

明白了字符串常量池, 我相信關於字符串的創建你已經有十足的把握了. 但是這還不夠, 作為一名合格的Java工程師, 我們還必須對字符串的操作做到了如指掌. 注意! 不是說你不用查api能熟練操作字符串就瞭如指掌了, 而是說對String, StringBuilder, StringBuffer三大字符串操作類背後的實現瞭然於胸, 這樣才能在開發的過程中做出正確, 高效的選擇。

String, StringBuilder, StringBuffer的底層實現

點進String的源碼, 我們可以看見String類是通過char類型數組實現的。

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

接著查看StringBuilder和StringBuffer的源碼, 我們發現這兩者都繼承自AbstractStringBuilder類, 通過查看該類的源碼, 得知StringBuilder和StringBuffer兩個類也是通過char類型數組實現的。

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

而且通過StringBuilder和StringBuffer繼承自同一個父類這點, 我們可以推斷出它倆的方法都是差不多的. 通過查看源碼也發現確實如此, 只不過StringBuffer在方法上添加了 synchronized關鍵字, 證明它的方法絕大多數方法都是線程同步方法. 也就是說在多線程的環境下我們應該使用StringBuffer以保證線程安全, 在單線程環境下我們應使用StringBuilder以獲得更高的效率。

既然如此, 我們的比較也就落到了StringBuilder和String身上了。

關於StringBuilder和String之間的討論

通過查看StringBuilder和String的源碼我們會發現兩者之間一個關鍵的區別: 對於String, 凡是涉及到返回參數類型為String類型的方法, 在返回的時候都會通過new關鍵字創建一個新的字符串對象; 而對於StringBuilder, 大多數方法都會返回StringBuilder對象自身。

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

就因為這點區別, 使得兩者在操作字符串時在不同的場景下會體現出不同的效率。

下面還是以拼接字符串為例比較一下兩者的性能:

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

就拼接5萬次字符串而言, StringBuilder的效率是String類的956倍。

我們再次通過反編譯代碼看看造成兩者性能差距的原因, 先看String類. (為了方便閱讀代碼, 我刪除了計時部分的代碼, 並重新編譯, 得到的main方法反編譯代碼如下)

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

從反彙編代碼中可以看到, 當用String類拼接字符串時, 每次都會生成一個StringBuilder對象, 然後調用兩次append()方法把字符串拼接好, 最後通過StringBuilder的toString()方法new出一個新的字符串對象。

也就是說每次拼接都會new出兩個對象, 並進行兩次方法調用, 如果拼接的次數過多, 創建對象所帶來的時延會降低系統效率, 同時會造成巨大的內存浪費. 而且當內存不夠用時, 虛擬機會進行垃圾回收, 這也是一項相當耗時的操作, 會大大降低系統性能。

下面是使用StringBuilder拼接字符串得到的反編譯代碼:

從底層徹底搞懂String,StringBuilder,StringBuffer的實現

可以看到StringBuilder拼接字符串就簡單多了, 直接把要拼接的字符串放到棧頂進行append就完事了, 除了開始時創建了StringBuilder對象, 運行時期沒有創建過其他任何對象, 每次循環只調用一次append方法. 所以從效率上看, 拼接大量字符串時, StringBuilder要比String類給力得多。

當然String類也不是沒有優勢的, 從操作字符串api的豐富度上來講, String是要多於StringBuilder的, 在日常操作中很多業務都需要用到String類的api。

在拼接字符串時, 如果是簡單的拼接, 比如說 Strings="hello "+"world";, String類的效率會更高一點。

但如果需要拼接大量字符串, StringBuilder無疑是更合適的選擇。

講到這裡, Java中的字符串背後的原理就講得差不多, 相信在瞭解虛擬機操作字符串的細節後, 你在使用字符串時會更加得心應手. 字符串是編程中一個重要的話題, 本文圍繞Java體系講解的字符串知識只是字符串知識的冰山一角. 字符串操作的背後是數據結構和算法的應用, 如何能夠以儘可能低的時間複雜度去操作字符串, 又是一門大學問。

原文:https://mp.weixin.qq.com/s/2-Ror2TBEoCGW-o7tdUD0A

"

相關推薦

推薦中...