本文主要介紹Java
中與字符串相關的一些內容,主要包括String
類的實現及其不變性、String
相關類(StringBuilder
、StringBuffer
)的實現 以及 字符串緩存機制的用法與實現。html
String
類的核心邏輯是經過對char
型數組進行封裝來實現字符串對象,但實現細節伴隨着Java
版本的演進也發生過幾回變化。java
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
/** The offset is the first index of the storage that is used. */
private final int offset;
/** The count is the number of characters in the String. */
private final int count;
/** Cache the hash code for the string */
private int hash; // Default to 0
}
複製代碼
在Java 6
中,String
類有四個成員變量:char
型數組value
、偏移量 offset
、字符數量 count
、哈希值 hash
。value
數組用來存儲字符序列, offset
和 count
兩個屬性用來定位字符串在value
數組中的位置,hash
屬性用來緩存字符串的hashCode
。git
使用offset
和count
來定位value
數組的目的是,能夠高效、快速地共享value
數組,例如substring()
方法返回的子字符串是經過記錄offset
和count
來實現與原字符串共享value
數組的,而不是從新拷貝一份。substring()
方法實現以下:github
String(int offset, int count, char value[]) {
this.value = value; // 直接複用原數組
this.offset = offset;
this.count = count;
}
public String substring(int beginIndex, int endIndex) {
// ...... 省略一些邊界檢查的代碼 ......
return ((beginIndex == 0) && (endIndex == count)) ? this :
new String(offset + beginIndex, endIndex - beginIndex, value);
}
複製代碼
可是這種方式卻頗有可能會致使內存泄漏。例如在以下代碼中:面試
String bigStr = new String(new char[100000]);
String subStr = bigStr.substring(0,2);
bigStr = null;
複製代碼
在bigStr
被設置爲null
以後,其中的value
數組卻仍然被subStr
所引用,致使垃圾回收器沒法將其回收,結果雖然咱們實際上僅僅須要2
個字符的空間,可是實際卻佔用了100000
個字符的空間。數組
在Java 6
中,若是想要避免這種內存泄漏狀況的發生,可使用下面的方式:緩存
String subStr = bigStr.substring(0,2) + "";
// 或者
String subStr = new String(bigStr.substring(0,2));
複製代碼
在語句執行完以後,substring
方法返回的匿名String
對象因爲沒有被別的對象引用,因此可以被垃圾回收器回收,不會繼續引用bigStr
中的value
數組,從而避免了內存泄漏。安全
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
}
複製代碼
在Java 7
-Java 8
中,Java
對 String
類作了一些改變。String
類中再也不有 offset
和 count
兩個成員變量了。substring()
方法也再也不共享 value
數組,而是從指定位置從新拷貝一份value
數組,從而解決了使用該方法可能致使的內存泄漏問題。substring()
方法實現以下:markdown
public String(char value[], int offset, int count) {
// ...... 省略一些邊界檢查的代碼 ......
// 從原數組拷貝
this.value = Arrays.copyOfRange(value, offset, offset+count);
}
public String substring(int beginIndex, int endIndex) {
// ...... 省略一些邊界檢查的代碼 ......
return ((beginIndex == 0) && (endIndex == value.length)) ? this
: new String(value, beginIndex, subLen);
}
複製代碼
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final byte[] value;
/** The identifier of the encoding used to encode the bytes in {@code value}. */
private final byte coder;
/** Cache the hash code for the string */
private int hash; // Default to 0
}
複製代碼
爲了節省內存空間,Java 9
中對String
的實現方式作了優化,value
成員變量從char[]
類型改成了byte[]
類型,同時新增了一個coder
成員變量。咱們知道Java
中char
類型佔用的是兩個字節,對於只佔用一個字節的字符(例如,a-z
,A-Z
)就顯得有點浪費,因此Java 9
中將char[]
改成byte[]
來存儲字符序列,而新屬性 coder
的做用就是用來表示value
數組中存儲的是雙字節編碼的字符仍是單字節編碼的字符。coder
屬性能夠有 0
和 1
兩個值,0
表明 Latin-1
(單字節編碼),1
表明 UTF-16
(雙字節編碼)。在建立字符串的時候若是判斷全部字符均可以用單字節來編碼,則使用Latin-1
來編碼以壓縮空間,不然使用UTF-16
編碼。主要的構造函數實現以下:網絡
String(char[] value, int off, int len, Void sig) {
if (len == 0) {
this.value = "".value;
this.coder = "".coder;
return;
}
if (COMPACT_STRINGS) {
byte[] val = StringUTF16.compress(value, off, len); // 嘗試壓縮字符串,使用單字節編碼存儲
if (val != null) { // 壓縮成功,可使用單字節編碼存儲
this.value = val;
this.coder = LATIN1;
return;
}
}
// 不然,使用雙字節編碼存儲
this.coder = UTF16;
this.value = StringUTF16.toBytes(value, off, len);
}
複製代碼
咱們注意到String
類是用final
修飾的;全部的屬性都是聲明爲private
的;而且除了hash
屬性以外的其餘屬性也都是用final
修飾。這保證了:
String
類由final
修飾,因此沒法經過繼承String
類改變其語義;private
的, 因此沒法在String
外部直接訪問或修改其屬性;hash
屬性以外的其餘屬性都是用final
修飾,表示這些屬性在初始化賦值後不能夠再修改。上述的定義共同實現了String
類一個重要的特性 —— 不變性,即 String
對象一旦建立成功,就不能再對它進行任何修改。String
提供的方法substring()
、concat()
、replace()
等方法返回值都是新建立的String
對象,而不是原來的String
對象。
hash
屬性不是final
的緣由是:String
的hashCode
並不須要在建立字符串時當即計算並賦值,而是在hashCode()
方法被調用時才須要進行計算。
爲何String類要設計爲不可變的?
String
對象的安全性。String
被普遍用做JDK
中做爲參數、返回值,例如網絡鏈接,打開文件,類加載,等等。若是 String
對象是可變的,那麼 String
對象將可能被惡意修改,引起安全問題。String
類的不可變性自然地保證了其線程安全的特性。String
對象的hashCode
的不變性。String
類的不可變性,保證了其hashCode
值可以在第一次計算後進行緩存,以後無需重複計算。這使得String
對象很適合用做HashMap
等容器的Key
,而且相比其餘對象效率更高。字符串常量池
。Java
爲字符串對象設計了字符串常量池
來共享字符串,節省內存空間。若是字符串是可變的,那麼字符串對象便沒法共享。由於若是改變了其中一個對象的值,那麼其餘對象的值也會相應發生變化。除了String
類以外,還有兩個與String
類相關的的類:StringBuffer
和StringBuilder
,這兩個類能夠看做是String
類的可變版本,提供了對字符串修改的各類方法。二者的區別在於StringBuffer
是線程安全的而StringBuilder
不是線程安全的。
StringBuffer
和StringBuilder
都是繼承自AbstractStringBuilder
,AbstractStringBuilder
利用可變的char
數組(Java 9
以後改成爲byte
數組)來實現對字符串的各類修改操做。StringBuffer
和StringBuilder
都是調用AbstractStringBuilder
中的方法來操做字符串, 二者區別在於StringBuffer
類中對字符串修改的方法都加了synchronized
修飾,而StringBuilder
沒有,因此StringBuffer
是線程安全的,而StringBuilder
並不是線程安全的。
咱們以Java 8
爲例,看一下AbstractStringBuilder
類的實現:
abstract class AbstractStringBuilder implements Appendable, CharSequence {
/** The value is used for character storage. */
char[] value;
/** The count is the number of characters used. */
int count;
}
複製代碼
value
數組用來存儲字符序列,count
則用來存儲value
數組中已經使用的字符數量,字符串真實的內容是value
數組中[0,count)
之間的字符序列,而[count,length)
之間是未使用的空間。須要count
屬性記錄已使用空間的緣由是,AbstractStringBuilder
中的value
數組並非每次修改都會從新申請,而是會提早預分配必定的多餘空間,以此來減小從新分配數組空間的次數。(這種作法相似於ArrayList
的實現)。
value
數組擴容的策略是:當對字符串進行修改時,若是當前的value
數組不知足空間需求時,則會從新分配更大的value
數組,分配的數組大小爲min( 原數組大小×2 + 2 , 所需的數組大小 )
,更加細節的邏輯能夠參考以下代碼:
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
private int newCapacity(int minCapacity) {
// overflow-conscious code
int newCapacity = (value.length << 1) + 2; //原數組大小×2 + 2
if (newCapacity - minCapacity < 0) { // 若是小於所需空間大小,擴展至所需空間大小
newCapacity = minCapacity;
}
return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)
? hugeCapacity(minCapacity)
: newCapacity;
}
private int hugeCapacity(int minCapacity) {
if (Integer.MAX_VALUE - minCapacity < 0) { // overflow
throw new OutOfMemoryError();
}
return (minCapacity > MAX_ARRAY_SIZE)
? minCapacity : MAX_ARRAY_SIZE;
}
複製代碼
固然AbstractStringBuilder
也提供了trimToSize
方法去釋放多餘的空間:
public void trimToSize() {
if (count < value.length) {
value = Arrays.copyOf(value, count);
}
}
複製代碼
由於String
對象的使用普遍,Java
爲String
對象設計了緩存機制,以提高時間和空間上的效率。在JVM
的運行時數據區中存在一個字符串常量池
(String Pool
),在這個常量池中維護了全部已經緩存的String
對象,當咱們說一個String
對象被緩存(interned
)了,就是指它進入了字符串常量池
。
咱們經過解答下面三個問題來理解String
對象的緩存機制:
String
對象會被緩存進字符串常量池
?String
對象被緩存在哪裏,如何組織起來的?String
對象是何時進入字符串常量池
的?說明: 如未特殊指明,本文中說起的
JVM
實現均指的是Oracle
的HotSpot VM
,而且不考慮 逃逸分析(escape analysis
)、標量替換(scalar replacement
)、無用代碼消除(dead-code elimination
)等優化手段,測試代碼基於不添加任何額外JVM
參數的狀況下運行。
爲了更好的閱讀體驗,在解答上面三個問題前,但願讀者對如下知識點有簡單瞭解:
JVM
運行時數據區class文件
的結構JVM
基於棧的字節碼解釋執行引擎Java
中的幾種常量池爲了內容的完整性,咱們對下文涉及較多的其中兩點作簡要介紹。
類從被加載到虛擬機內存中開始,到卸載出內存爲止,它的整個生命週期依次爲:加載(Loading
)、驗證(Verification
)、準備(Preparation
)、解析(Resolution
)、初始化(Initialization
)、使用(Using
)和卸載(Unloading
)7個階段。其中驗證、準備、解析3個部分統稱爲鏈接(Linking
)。 加載、驗證、準備、初始化和卸載這5個階段的順序是肯定的,類的加載過程必須按照這種順序循序漸進地開始,而解析階段則不必定:它在某些狀況下能夠在初始化階段以後再開始,這是爲了支持Java語言的運行時綁定(也稱爲動態綁定或晚期綁定)。
1. class文件中的常量池 咱們知道java
後綴的源代碼文件會被javac
編譯爲class
後綴的class文件
(字節碼文件)。在class文件
中有一部份內容是 常量池(Constant Pool) ,這個常量池中主要存儲兩大類常量:
字面量
或者常量表達式
的值;2. 運行時常量池 在JVM
運行時數據區(Run-Time Data Areas)中,有一部分是運行時常量池(Run-Time Constant Pool),屬於方法區
的一部分。運行時常量池
是class文件
中每一個類或者接口的常量池(Constant Pool
)的運行時表示形式,class文件
的常量池中的內容會在類加載後進入方法區的運行時常量池
。
3. 字符串常量池 字符串常量池
(String Pool
)也就是咱們上文提到的用來緩存String
對象的常量池。 這個常量池是全局共享的,屬於運行時數據區的一部分。
在Java
中,有兩種字符串會被緩存到字符串常量池
中,一種是在代碼中定義的字符串字面量
或者字符串常量表達式
,另外一種是程序中主動調用String.intern()
方法將當前String
對象緩存到字符串常量池
中。下面分別對兩種方式作簡要介紹。
之因此稱之爲隱式緩存是由於咱們並不須要主動去編寫緩存相關代碼,編譯器和JVM
會幫咱們完成這部分工做。
字符串字面量 第一種會被隱式緩存的字符串是 字符串字面量。字面量
是類型爲原始類型、String
類型、null
類型的值在源代碼中的表示形式。例如:
int i = 100; // int 類型字面量
double f = 10.2; // double 類型字面量
boolean b = true; // boolean 類型字面量
String s = "hello"; // String類型字面量
Object o = null; // null類型字面量
複製代碼
字符串字面量
是由雙引號括起來的0
個或者多個字符構成的。 Java
會在執行過程當中爲字符串字面量
建立String
對象並加入字符串常量池
中。例如上面代碼中的"hello"
就是一個字符串字面量
,在執行過程當中會先 建立一個內容爲"hello"
的String
對象,並緩存到字符串常量池
中,再將s
引用指向這個String
對象。
關於字符串字面量
更加詳細的內容請參閱Java語言規範
(JLS - 3.10.5. String Literals)。
字符串常量表達式 另一種會被隱式緩存的字符串是 字符串常量表達式。常量表達式
指的是表示簡單類型值或String
對象的表達式,能夠簡單理解爲常量表達式
就是在編譯期間就能肯定值的表達式。字符串常量表達式
就是表示String
對象的常量表達式。例如:
int a = 1 + 2;
double d = 10 + 2.01;
boolean b = true & false;
String str1 = "abc" + 123;
final int num = 456;
String str2 = "abc" +456;
複製代碼
Java
會在執行過程當中爲字符串常量表達式
建立String
對象並加入字符串常量池
中。例如,上面的代碼中,會分別建立"abc123"
和"abc456"
兩個String
對象,這兩個String
對象會被緩存到字符串常量池
中,str1
會指向常量池中值爲"abc123"
的String
對象,str2
會指向常量池中值爲"abc456"
的String
對象。
關於常量表達式
更加詳細的內容請參閱Java語言規範
(JLS - 15.28 Constant Expressions)。
除了聲明爲字符串字面量
/字符串常量表達式
以外,經過其餘方式獲得的String
對象也能夠主動加入字符串常量池
中。例如:
String str = new String("123") + new String("456");
str.intern();
複製代碼
在上面的代碼中,在執行完第一句後,常量池中存在內容爲"123"
和"456"
的兩個String
對象,可是不存在"123456"
的String
對象,但在執行完str.intern();
以後,內容爲"123456"
的String
對象也加入到了字符串常量池
中。
咱們經過String.intern()
方法的註釋來看下其具體的緩存機制:
When the intern method is invoked, if the pool already contains a string equal to this String object as determined by the equals(Object) method, then the string from the pool is returned. Otherwise, this String object is added to the pool and a reference to this String object is returned. It follows that for any two strings s and t, s.intern() == t.intern() is true if and only if s.equals(t) is true.
簡單翻譯一下:
當調用
intern
方法時,若是常量池中已經包含相同內容的字符串(字符串內容相同由equals (Object)
方法肯定,對於String
對象來講,也就是字符序列相同),則返回常量池中的字符串對象。不然,將此String
對象將添加到常量池中,並返回此String
對象的引用。 所以,對於任意兩個字符串s
和t
,當且僅當s.equals(t)
的結果爲true
時,s.intern() == t.intern()
的結果爲true
。
HotSpot VM
中,有一個用來記錄緩存的String
對象的全局表,叫作StringTable
,結構及實現方式都相似於Java
中的HashMap
或者HashSet
,是一個使用拉鍊法解決哈希衝突的哈希表,能夠簡單理解爲HashSet<String>
,注意它只存儲對String
對象的引用,而不存儲String
對象實例。 通常咱們說一個字符串進入了字符串常量池
實際上是說在這個StringTable
中保存了對它的引用,反之,若是說沒有在其中就是說StringTable
中沒有對它的引用。
而真正的字符串對象實際上是保存在另外的區域中的,在Java 6
中字符串常量池
中的String
對象是存儲在永久代
(Java 8
以前HotSpot VM
對方法區
的實現)中的,而在Java 6
以後,字符串常量池
中的String
對象是存儲在堆
中的。
Java 7
中將字符串常量池
中的對象移動到堆
中的緣由是在Java 6
中,字符串常量池
中的對象在永久代
建立,而永久代
代的大小通常不會設置太大,若是大量使用字符串緩存將可能對致使永久代
發生OOM
異常。
對於經過 在程序中調用String.intern()
方法主動緩存進入常量池的String
對象,很顯然就是在調用intern()
方法的時候進入常量池的。
咱們重點來研究一下會被隱式緩存的兩種值(字符串字面量
和字符串常量表達式
),主要是兩個問題:
String
類的構造方法,那麼它們是在什麼時候被建立?字符串常量池
的?咱們如下面的代碼爲例來分析這兩個問題:
public class Main {
public static void main(String[] args) {
String str1 = "123" + 123; // 字符串常量表達式
String str2 = "123456"; // 字面量
String str3 = "123" + 456; //字符串常量表達式
}
}
複製代碼
咱們對上述代碼編譯以後使用javap
來觀察一下字節碼文件,爲了節省篇幅,只摘取了相關的部分:常量池表部分以及main
方法信息部分:
Constant pool:
#1 = Methodref #5.#23 // java/lang/Object."<init>":()V
#2 = String #24 // 123123
#3 = String #25 // 123456
// ...... 省略 ......
#24 = Utf8 123123
#25 = Utf8 123456
// ...... 省略 ......
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=4, args_size=1
0: ldc #2 // String 123123
2: astore_1
3: ldc #3 // String 123456
5: astore_2
6: ldc #3 // String 123456
8: astore_3
9: return
LineNumberTable:
line 7: 0
line 8: 3
line 9: 6
line 10: 9
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 args [Ljava/lang/String;
3 7 1 str1 Ljava/lang/String;
6 4 2 str2 Ljava/lang/String;
9 1 3 str3 Ljava/lang/String;
複製代碼
在常量池
中,有兩種與字符串相關的常量類型,CONSTANT_String
和CONSTANT_Utf8
。CONSTANT_String
類型的常量用於表示String
類型的常量對象,其內容只是一個常量池的索引值index
,index
處的成員必須是CONSTANT_Utf8
類型。而CONSTANT_Utf8
類型的常量用於存儲真正的字符串內容。 例如,上面的常量池中的第2
、3
項是CONSTANT_String
類型,存儲的索引分別爲24
、25
,常量池中第24
、25
項就是CONSTANT_Utf8
,存儲的值分別爲"123123"
,"123456"
。
class文件
的方法信息中Code
屬性是class文件
中最爲重要的部分之一,其中包含了執行語句對應的虛擬機指令,異常表,本地變量信息等,其中LocalVariableTable
是本地變量的信息,Slot
能夠理解爲本地變量表中的索引位置。ldc
指令的做用是從運行時常量池
中提取指定索引位置的數據並壓入棧中;astore_<n>
指令的做用是將一個引用類型的值從棧中彈出並保存到本地變量表的指定位置,也就是<n>
指定的位置。能夠看出三條賦值語句所對應的字節碼指令其實都是相同的:
ldc #<index> // 首先將常量池中指定索引位置的String對象壓入棧中
astore_<n> // 而後從棧中彈出剛剛存入的String對象保存到本地變量的指定位置
複製代碼
仍是圍繞上面的代碼,咱們結合 從編譯到執行的過程 來分析一下字符串字面量
和字符串常量表達式
的建立及緩存時機。
1. 編譯 首先,第一步是javac
將源代碼編譯爲class
文件。在源代碼編譯過程當中,咱們上文提到的兩種值 字符串字面量
("123456"
) 和 字符串常量表達式
("123" + 456
)這兩類值都會存在編譯後的class文件
的常量池中,常量類型爲CONSTANT_String
。值得注意的兩點是:
字符串常量表達式
會在編譯期計算出真實值存在class
文件的常量池
中。例如上面源代碼中的"123" + 123
這個表達式在class
文件的常量池中的表現形式是123123
,"123" + 456
這個表達式在class
文件的常量池中的表現形式是123456
;字符串字面量
或者字符串常量表達式
在class文件
的常量池中只會存在一個常量項(CONSTANT_String
類型和CONSTANT_Utf8
都只有一項)。例如上面源代碼中,雖然聲明瞭兩個常量值分別爲"123456"
和"123" + 456
,可是最後class
文件的常量池中只有一個值爲123456
的CONSTANT_Utf8
常量項以及一個對應的CONSTANT_String
常量項。2. 類加載 在JVM
運行時,加載Main
類時,JVM
會根據 class文件
的常量池 建立 運行時常量池
, class文件
的常量池 中的內容會在類加載時進入方法區的 運行時常量池
。對於class文件
的常量池中的符號引用,會在類加載的解析(resolve)階段
,會將其轉化爲真正的值。但在HotSpot
中,符號引用的解析
並不必定是在類加載時當即執行的,而是推遲到第一次執行相關指令(即引用了符號引用的指令,JLS - 5.4.3. Resolution )時纔會去真正進行解析,這就作延遲解析
/惰性解析
("lazy" or "late" resolution
)。
CONSTANT_Integer_info
,CONSTANT_Float_info
,CONSTANT_Long_info
,CONSTANT_Double_info
,在類加載階段會將class
文件常量池中的值轉化爲運行時常量池
中的值,分別對應C++
中的int
,float
,long
,double
類型;CONSTANT_Utf8
類型的常量項,在類加載的解析階段被轉化爲Symbol
對象(HotSpot VM
層面的一個C++
對象)。同時HotSpot
使用SymbolTable
(結構與StringTable
相似)來緩存Symbol
對象,因此在類加載完成後,SymbolTable
中應該有全部的CONSTANT_Utf8
常量對應的Symbol
對象;CONSTANT_String
類型的常量項,由於其內容是一個符號引用(指向CONSTANT_Utf8
類型常量的索引值),因此須要進行解析,在類加載的解析階段會將其轉化爲java.lang.String
對象對應的oop
(能夠理解爲Java
對象在HotSpot VM
層面的表示),並使用StringTable
來進行緩存。可是CONSTANT_String
類型的常量,屬於上文提到的延遲解析
的範疇,也就是在類加載時並不會當即執行解析,而是等到第一次執行相關指令時(通常來講是ldc
指令)纔會真正解析。3. 執行指令 上面提到,JVM
會在第一次執行相關指令的時候去執行真正的解析,對於上文給出的代碼,觀察字節碼能夠發現,ldc
指令中使用到了符號引用,因此在執行ldc
指令時,須要進行解析操做。那麼ldc
指令到底作了什麼呢?
ldc
指令會從運行時常量池
中查找指定index
對應的常量項,並將其壓入棧中。若是該項還未解析,則須要先進行解析,將符號引用轉化爲具體的值,而後再將其壓入棧中。若是這個未解析的項是String
類型的常量,則先從字符串常量池
中查找是否已經有了相同內容的String
對象,若是有則直接將字符串常量池
中的該對象壓入棧中;若是沒有,則會建立一個新的String
對象加入字符串常量池
中,並將建立的新對象壓入棧中。可見,若是代碼中聲明多個相同內容的字符串字面量
或者字符串常量表達式
,那麼只會在第一次執行ldc
指令時建立一個String
對象,後續相同的ldc
指令執行時相應位置的常量已經解析過了,直接壓入棧中便可。
總結一下:
字符串字面量
或者字符串常量表達式
轉化爲了class文件
的常量池中的CONSTANT_String
常量項。class文件
的常量池
中的CONSTANT_String
常量項被存入了運行時常量池
中,但保存的內容仍然是一個符號引用,未進行解析。ldc
指令時,運行時常量池
中的CONSTANT_String
項還未解析,會真正執行解析,解析過程當中會建立String
對象並加入字符串
常量池。能夠看到,其實ldc
指令在解析String
類型常量的時候與String.intern()
方法的邏輯很類似:
ldc
指令中解析String
常量:先從字符串常量池
中查找是否有相同內容的String
對象,若是有則將其壓入棧中,若是沒有,則建立新對象加入字符串常量池
並壓入棧中。String.intern()
方法:先從字符串常量池
中查找是否有相同內容的String
對象,若是有則返回該對象引用,若是沒有,則將自身加入字符串常量池
並返回。實際在HotSpot
內部實現上,ldc
指令 與 String.intern()
對應的native
方法 調用了相同的內部方法。咱們以OpenJDK 8
的源代碼爲例,簡單分析一下其過程,代碼以下(源碼位置:src/share/vm/classfile/SymbolTable.cpp
):
// String.intern()方法會調用這個方法
// 參數 "oop string"表明調用intern()方法的String對象
oop StringTable::intern(oop string, TRAPS)
{
if (string == NULL) return NULL;
ResourceMark rm(THREAD);
int length;
Handle h_string (THREAD, string);
jchar* chars = java_lang_String::as_unicode_string(string, length, CHECK_NULL); // 將String對象轉化爲字符序列
oop result = intern(h_string, chars, length, CHECK_NULL);
return result;
}
// ldc指令執行時會調用這個方法
// 參數 "Symbol* symbol" 是 運行時常量池 中 ldc指令的參數(索引位置)對應位置的Symbol對象
oop StringTable::intern(Symbol* symbol, TRAPS) {
if (symbol == NULL) return NULL;
ResourceMark rm(THREAD);
int length;
jchar* chars = symbol->as_unicode(length); // 將Symbol對象轉化爲字符序列
Handle string;
oop result = intern(string, chars, length, CHECK_NULL);
return result;
}
// 上面兩個方法都會調用這個方法
oop StringTable::intern(Handle string_or_null, jchar* name, int len, TRAPS) {
// 嘗試從字符串常量池中尋找
unsigned int hashValue = hash_string(name, len);
int index = the_table()->hash_to_index(hashValue);
oop found_string = the_table()->lookup(index, name, len, hashValue);
// 若是找到了直接返回
if (found_string != NULL) {
ensure_string_alive(found_string);
return found_string;
}
// ...... 省略部分代碼 ......
Handle string;
// 嘗試複用原字符串,若是沒法複用,則會建立新字符串
// JDK 6中這裏的實現有一些不一樣,只有string_or_null已經存在於永久代中才會複用
if (!string_or_null.is_null()) {
string = string_or_null;
} else {
string = java_lang_String::create_from_unicode(name, len, CHECK_NULL);
}
//...... 省略部分代碼 ......
oop added_or_found;
{
MutexLocker ml(StringTable_lock, THREAD);
// 添加字符串到 StringTable 中
added_or_found = the_table()->basic_add(index, string, name, len,
hashValue, CHECK_NULL);
}
ensure_string_alive(added_or_found);
return added_or_found;
}
複製代碼
說明:由於在
Java 6
以後字符串常量池
從永久代
移到了堆
中,可能在一些代碼上Java 6
與以後的版本表現不一致。因此下面的代碼都使用Java 6
和Java 7
分別進行測試,若是未特殊說明,表示在兩個版本上結果相同,若是不一樣,會單獨指出。
final int a = 4;
int b = 4;
String s1 = "123" + a + "567";
String s2 = "123" + b + "567";
String s3 = "1234567";
System.out.println(s1 == s2);
System.out.println(s1 == s3);
System.out.println(s2 == s3);
複製代碼
結果:
false
true
false
複製代碼
解釋:
a
被定義爲常量,因此"123" + a + "567"
是一個常量表達式
,在編譯期會被編譯爲"1234567"
,因此會在字符串常量池
中建立"1234567"
,s1
指向字符串常量池
中的"1234567"
;b
被定義爲變量,"123"
和"567"
是字符串字面量
,因此首先在字符串常量池
中建立"123"
和"567"
,而後經過StringBuilder
隱式拼接在堆中建立"1234567"
,s2
指向堆中的"1234567"
;"1234567"
是一個字符串字面量
,由於此時字符串常量池
中已經存在了"1234567"
,因此s3
指向字符串字符串常量池
中的"1234567"
。String s1 = new String("123");
String s2 = s1.intern();
String s3 = "123";
System.out.println(s1 == s2);
System.out.println(s1 == s3);
System.out.println(s2 == s3);
複製代碼
結果:
false
false
true
複製代碼
解釋:
"123"
是一個字符串字面量
,因此首先在字符串常量池
中建立了一個"123"
對象,而後使用String
的構造函數在堆中建立了一個"123"
對象,s1
指向堆中的"123"
;字符串常量池
中已經有了"123"
,因此s2
指向字符串常量池
中的"123"
;字符串常量池
中已經有了"123"
,因此s3
指向字符串常量池
中的"123"
。String s1 = String.valueOf("123");
String s2 = s1.intern();
String s3 = "123";
System.out.println(s1 == s2);
System.out.println(s1 == s3);
System.out.println(s2 == s3);
複製代碼
結果:
true
true
true
複製代碼
解釋:與上一種狀況的區別在於,String.valueOf()
方法在參數爲String
對象的時候會直接將參數做爲返回值,不會在堆上建立新對象,因此s1
也指向字符串常量池
中的"123"
,三個變量指向同一個對象。
String s1 = new String("123") + new String("456");
String s2 = s1.intern();
String s3 = "123456";
System.out.println(s1 == s2);
System.out.println(s1 == s3);
System.out.println(s2 == s3);
複製代碼
上面的代碼在Java 6
和Java 7
中結果是不一樣的。 在Java 6
中:
false
false
true
複製代碼
解釋:
"123"
和"456"
是字符串字面量
,因此首先在字符串常量池
中建立"123"
和"456"
,+
操做符經過StringBuilder
隱式拼接在堆中建立"123456"
,s1
指向堆中的"123456"
;"123456"
緩存到字符串常量池
中,由於Java 6
中字符串常量池
中的對象是在永久代建立的,因此會在字符串常量池
(永久代)建立一個"123456"
,此時在堆中和永久代中各有一個"123456"
,s2
指向字符串常量池
(永久代)中的"123456"
;"123456"
是字符串字面量
,由於此時字符串常量池
(永久代)中已經存在"123456"
,因此s3
指向字符串常量池
(永久代)中的"123456"
。在Java 7
中:
true
true
true
複製代碼
解釋:與Java 6
的區別在於,由於Java 7
中字符串常量池
中的對象是在堆
上建立的,因此當執行第二行String s2 = s1.intern();
時不會再建立新的String
對象,而是直接將s1
的引用添加到StringTable
中,因此三個對象都指向常量池中的"123456"
,也就是第一行中在堆中建立的對象。
Java 7
下,s1 == s2
結果爲true
也可以用來佐證咱們上面延遲解析
的過程。咱們假設若是"123456"
不是延遲解析的,而是類加載的時候解析完成並進入常量池的,s1.intern()
的返回值應該是常量池中存在的"123456"
,而不會將s1
指向的堆中的"123456"
對象加入常量池,因此結果應該是s2
不等於s1
而等於s3
。
String s1 = new String("123") + new String("456");
String s2 = "123456";
String s3 = s1.intern();
System.out.println(s1 == s2);
System.out.println(s1 == s3);
System.out.println(s2 == s3);
複製代碼
結果:
false
false
true
複製代碼
解釋:
"123"
和"456"
是字符串字面量
,因此首先在字符串常量池
中建立"123"
和"456"
,+
操做符經過StringBuilder
隱式拼接在堆中建立"123456"
,s1
指向堆中的"123456"
;"123456"
是字符串字面量,此時字符串常量池中不存在"123456"
,因此在字符串常量池
中建立"123456"
, s2
指向字符串常量池
中的"123456"
;"123456"
,因此s3
指向字符串常量池
中的"123456"
。