原文地址:https://javaranch.com/journal/200409/ScjpTipLine-StringsLiterally.html
作者:Corey McGlone
讓我們由一個簡單的問題開始,什么是字符串字面量?一個字符串字面量就是兩個雙引號之間的字符序列,形如“string”、“literal”。
你可能已經在你的程序中使用字符串字面量幾百次了,但是你可能還沒意識到它在Java中是多么特殊。
字符串是不可變的
究竟什么使字符串字面量這么特殊?首先,記住重要的一點是字符串對象是不可變的。
這就意味着一旦創建,一個字符串對象就不能被改變(還是可以通過反射來改變)。
不可變?不能被更改?那怎么解釋這段代碼。
public class ImmutableStrings { public static void main(String[] args) { String start = "Hello"; String end = start.concat(" World!"); System.out.println(end); } } // Output Hello World!
看這段代碼,字符串被改變了嗎,還是沒有?事實上,這段代碼中並沒有字符串對象被改變。
我們首先將“Hello”賦值給start變量,為了實現這步,需要在堆中創建一個對象,並把它的引用存儲在start中。接下來,我們在這個對象上調用concat(String)方法。進行到這里Java耍了一個小把戲,如果我們查看String的API說明,會發現其中對於concat(String)方法有如下的描述:
方法描述:將指定字符串連接在這個字符串的結尾。
如果長度為0,則返回這個字符串對象。否則就創建一個新的字符串對象,表示這個字符串序列由原字符串對象和參數字符串二者所表示的字符串序列拼接而成。
你肯定看到了,當你將兩個字符串做拼接操作時,實際上並沒有改變原對象,而是直接創建了一個包含原始對象的新的對象,並且將另一個字符串拼在了后面。
我們上面那段代碼就是這么執行的,start變量所引用的字符串對象並沒有改變,如果在調用concat方法之后System.out.println(start); ,會發現start仍然指向的是“Hello”。
這時候你可能想到了字符串中的“+”操作符,事實上字符串的“+”操作也是和concat做了同樣的事情(“+”操作實際上是new了一個StringBuilder對象,然后調用append方法)。
字符串的存儲——字符串常量池
你或許聽說過“字符串常量池”這個概念,究竟什么是字符串常量池?有人說是一個字符串對象容器。答案很接近了,但是不完全正確。
事實上他是一用來保存字符串對象引用的容器。
即使字符串是不可變的,它仍然和Java中的其他對象一樣。對象都是創建在堆中,字符串也不例外。
所以字符串常量池仍然依靠堆,他們存儲的只是堆中字符串的引用。
目前還沒有解釋這個池到底是什么,或者它為何存在。
好吧,因為字符串對象是不可變的,所以復制多個引用來“共享”這個字符串是安全的。下面來看一個例子:
public class ImmutableStrings { public static void main(String[] args) { String one = "someString"; String two = "someString"; System.out.println(one.equals(two)); System.out.println(one == two); } } // Output true true
在這個例子中,實在沒有必要為一個相同的字符串對象創建兩個實例。如果字符串像StringBuffer一樣是可變的,那么我們會被迫創建兩個對象(如果不這樣做的話,通過一個引用改變它的值,將會導致其他引用的值也同樣改變,從而可能發生錯誤)。
但是,我們知道字符串對象是不能被改變的,我們可以安全地通過兩個引用one和two來使用一個字符串對象。
這個工作是通過字符串常量池完成的,下面來看一下它是如何完成的:
當一個.java文件被編譯成.class文件時,和所有其他常量一樣,每個字符串字面量都通過一種特殊的方式被記錄下來。
當一個.class文件被加載時(注意加載發生在初始化之前),JVM在.class文件中尋找字符串字面量。
當找到一個時,JVM會檢查是否有相等的字符串在常量池中存放了堆中引用。
如果找不到,就會在堆中創建一個對象,然后將它的引用存放在池中的一個常量表中。
一旦一個字符串對象的引用在常量池中被創建,這個字符串在程序中的所有字面量引用都會被常量池中已經存在的那個引用代替。
所以,在上面的例子中字符串常量池中只有一個引用,就是“someString”這個字符串對象的引用。
局部變量one和two都被賦予了同一個字符串對象的引用。可以通過程序的輸出來驗證。
equals方法檢查的是兩個字符串對象是否包含相同的數據(“someString”),而“==”操作符作用在對象上比較的是引用是否相同,這意味着只有兩個引用指向的是同一個對象才會返回true。
所以例子中的兩個引用是相等的。從輸出可以看到,局部變量one和two不僅包含相同的數據,而且還指向相同的對象。
無圖無真相,來看一下他們之間的關系:
注意,對於字符串字面量有一點比較特殊。通過“new”關鍵字構建時一種不同的方式。
下面舉一個例子:
public class ImmutableStrings { public static void main(String[] args) { String one = "someString"; String two = new String("someString"); System.out.println(one.equals(two)); System.out.println(one == two); } } // Output true false
在這個例子中,可以看到由於關鍵字“new”,最后的結果有一點不同。
此例中,兩個字符串字面量仍然被放進了常量池的常量表中,但是當使用“new”時,JVM就會在運行時創建一個新對象,而不是使用常量表中的引用。
雖然例子中的兩個字符串引用所指向的對象包含相同的數據“someString”,但是這兩個對象並不相同。
這一點可以從輸出看出來,equals方法返回了true,而檢查引用是否相等的“==”返回false。
這表明兩個變量指向的是兩個不同的字符串對象。
如果你想看圖形化的表示,下面就是。要記住引用到常量池的字符串對象是在類加載的時候創建的,而另一個對象是在運行時,當“new String”語句被執行時。
如果你想得到兩個引用到相同對象的局部變量,你可以使用String類中的定義的intern()方法。
調用two.intern()后,會在字符串常量池中尋找是否有值相等的對象引用。
如果有的話,就會返回這個引用,然后你可以把它賦給局部變量。
如果這么做的化,局部變量one和two都是同一個對象的引用,並且在字符串常量池中也存有一個引用,就如同第一張圖那樣。這時,在運行時創建的第二個字符串對象將會被GC回收。
垃圾回收
什么條件下對象才會被垃圾回收?當一個對象不再有引用指向它時,這個對象就會被回收。
有人注意到字符串字面量在垃圾回收時有什么特殊的地方嗎?
讓我們來看一個例子,然后你就會明白。
public class ImmutableStrings { public static void main(String[] args) { String one = "someString"; String two = new String("someString"); one = two = null; } }
在主函數結束前,有多少個對象可以被回收?0個?1個?還是2個?
答案是一個。不像大多數對象,字符串字面量總是有一個來自字符串常量池的引用。
這就意味着它們會一直有一個引用,所以它們不會被垃圾回收。見下圖:
如你所見,局部變量one和two沒有指向字符串對象,但是仍然有一個字符串常量池的引用。
所以GC並不會回收這個對象。並且這個對象可以通過之前提到的intern()方法訪問。
總結
對於字符串字面量,下面的幾條結論你可以記住。
- 相等的字符串字面量將會指向相同的字符串對象(甚至是在不同包的不同類中)。
- 總之,字符串字面量不會被垃圾回收。絕對不會。
- 在運行時創建的字符串和由字符串字面量創建的是兩個不同的對象。
- 對於運行時創建的字符串你可以通過intern()方法來重用字符串字面量
- 使用equals()方法是比較兩個字符串是否相等的最好方式。
下面的資料也一定要看:
關於字符串為什么被設計成不可變的可以參考:為什么Java中的String類是不可變的?
字符串的創建和intern方法詳解:理解Java字符串常量池與intern方法
翻譯如有錯誤,懇請糾正。
轉載請注明原文鏈接:https://www.cnblogs.com/justcooooode/p/7670256.html