深入(ru)理(li)解(jie)Java:String
在講解String之前,我們先了解一下Java的內存結構。
一、Java內存模型
按照官方的(de)說法:Java 虛擬機具有一個堆,堆是運行時數(shu)據區域,所有類實例和(he)數(shu)組的(de)內存均從此(ci)處(chu)分配。
JVM主要(yao)管理兩種類型內存:堆(dui)和非堆(dui),堆(dui)內存(Heap Memory)是(shi)在 Java 虛擬機啟(qi)動時創建,非堆(dui)內存(Non-heap Memory)是(shi)在JVM堆(dui)之(zhi)外的內存。
簡單來說,非(fei)堆包含方(fang)法(fa)(fa)區、JVM內(nei)部處理或(huo)優化所需的內(nei)存(如 Compiler,Just-in-time Compiler,即時(shi)編譯后的代碼緩存)、每個類結(jie)構(如運行時(shi)常數(shu)(shu)池、字段(duan)和(he)方(fang)法(fa)(fa)數(shu)(shu)據)以及方(fang)法(fa)(fa)和(he)構造方(fang)法(fa)(fa)的代碼。
Java的堆是一個運行時數據區,類的(對象從中分配空間。這些對象通過new、newarray、 anewarray和multianewarray等指令建立,它們不需要程序代碼來顯式的釋放。堆是由垃圾回收來負責的,堆的優勢是可以動態地分配內存大小,生存期也不必事先告訴編譯器,因為它是在運行時動態分配內存的,Java的垃圾收集器會自動收走這些不再使用的數據。但缺點是,由于要在運行時動態分配內存,存取速度較慢。
棧的優勢是,存取速度比堆要快,僅次于寄存器,棧數據可以共享。但缺點是,存在棧中的數據大小與生存期必須是確定的,缺乏靈活性。棧中主要存放一些基本類型的變量數據(int, short, long, byte, float, double, boolean, char)和對象句柄(引用)。
虛(xu)擬機必須為(wei)每個被裝載的類型維護一(yi)個常(chang)量池(chi)。常(chang)量池(chi)就是該類型所用(yong)到常(chang)量的一(yi)個有序集合,包括直接常(chang)量(string,integer和 floating point常(chang)量)和對其他(ta)類型,字段和方法的符號引用(yong)。
對(dui)于String常(chang)(chang)量(liang),它的(de)(de)值(zhi)是在常(chang)(chang)量(liang)池中的(de)(de)。而(er)JVM中的(de)(de)常(chang)(chang)量(liang)池在內存(cun)當(dang)中是以表(biao)(biao)的(de)(de)形式存(cun)在的(de)(de), 對(dui)于String類(lei)型(xing),有(you)(you)一(yi)(yi)張固(gu)定(ding)長度的(de)(de)CONSTANT_String_info表(biao)(biao)用(yong)(yong)來存(cun)儲(chu)文字(zi)(zi)字(zi)(zi)符(fu)串值(zhi),注意:該表(biao)(biao)只(zhi)存(cun)儲(chu)文字(zi)(zi)字(zi)(zi)符(fu)串值(zhi),不存(cun)儲(chu)符(fu)號引用(yong)(yong)。說到(dao)這里,對(dui)常(chang)(chang)量(liang)池中的(de)(de)字(zi)(zi)符(fu)串值(zhi)的(de)(de)存(cun)儲(chu)位置應該有(you)(you)一(yi)(yi)個比較明了的(de)(de)理解了。在程序執行的(de)(de)時(shi)候,常(chang)(chang)量(liang)池會儲(chu)存(cun)在Method Area,而(er)不是堆中。常(chang)(chang)量(liang)池中保存(cun)著很多String對(dui)象; 并且可(ke)以被共享使(shi)用(yong)(yong),因此它提高(gao)了效率
具體(ti)關于JVM和(he)內(nei)存等知識(shi)請參考:
二、案例解析
public static void main(String[] args) { /** * 情景一:字(zi)符串(chuan)池(chi) * JAVA虛(xu)擬機(JVM)中(zhong)存在著(zhu)一個字(zi)符串(chuan)池(chi),其中(zhong)保(bao)存著(zhu)很多String對象; * 并且(qie)可(ke)以(yi)被共享使(shi)用,因此它提高(gao)了效率(lv)。 * 由(you)于String類(lei)是final的,它的值一經創建就不可(ke)改變。 * 字(zi)符串(chuan)池(chi)由(you)String類(lei)維(wei)護(hu),我們可(ke)以(yi)調(diao)用intern()方法(fa)來訪問字(zi)符串(chuan)池(chi)。 */ String s1 = "abc"; //↑ 在字符(fu)串池(chi)創建(jian)了一個對象 String s2 = "abc"; //↑ 字符串(chuan)pool已(yi)經存在(zai)對(dui)象(xiang)(xiang)“abc”(共享),所以創建0個對(dui)象(xiang)(xiang),累計創建一個對(dui)象(xiang)(xiang) System.out.println("s1 == s2 : "+(s1==s2)); //↑ true 指向同一個對(dui)象, System.out.println("s1.equals(s2) : " + (s1.equals(s2))); //↑ true 值(zhi)相等(deng) //↑------------------------------------------------------over /** * 情景(jing)二:關于new String("") * */ String s3 = new String("abc"); //↑ 創建了(le)兩(liang)個對象,一(yi)個存放在字符串池(chi)中(zhong),一(yi)個存在與堆(dui)區中(zhong); //↑ 還有一個對(dui)象引(yin)用s3存放在棧中(zhong) String s4 = new String("abc"); //↑ 字符串池中已經存(cun)在“abc”對象,所(suo)以只在堆中創(chuang)建(jian)了一個對象 System.out.println("s3 == s4 : "+(s3==s4)); //↑false s3和s4棧(zhan)區的(de)地址(zhi)不(bu)同,指向堆區的(de)不(bu)同地址(zhi); System.out.println("s3.equals(s4) : "+(s3.equals(s4))); //↑true s3和s4的值(zhi)相(xiang)同 System.out.println("s1 == s3 : "+(s1==s3)); //↑false 存放的地區(qu)多不同,一個棧(zhan)區(qu),一個堆區(qu) System.out.println("s1.equals(s3) : "+(s1.equals(s3))); //↑true 值(zhi)相同 //↑------------------------------------------------------over /** * 情景三(san): * 由于(yu)常(chang)量的值(zhi)在編譯的時(shi)(shi)候就(jiu)被確(que)定(優化)了(le)。 * 在這里,"ab"和(he)"cd"都是常(chang)量,因(yin)此變量str3的值(zhi)在編譯時(shi)(shi)就(jiu)可(ke)以確(que)定。 * 這行代碼(ma)編譯后的效果等同于(yu): String str3 = "abcd"; */ String str1 = "ab" + "cd"; //1個對象(xiang) String str11 = "abcd"; System.out.println("str1 = str11 : "+ (str1 == str11)); //↑------------------------------------------------------over /** * 情景四(si): * 局(ju)部變量(liang)str2,str3存(cun)儲的(de)(de)(de)(de)是存(cun)儲兩個拘(ju)(ju)留(liu)字(zi)(zi)(zi)符串(chuan)對(dui)(dui)(dui)(dui)(dui)象(intern字(zi)(zi)(zi)符串(chuan)對(dui)(dui)(dui)(dui)(dui)象)的(de)(de)(de)(de)地址(zhi)。 * * 第三行代碼原理(li)(str2+str3): * 運行期JVM首先(xian)會在堆(dui)中(zhong)(zhong)(zhong)創(chuang)建(jian)一個StringBuilder類(lei), * 同(tong)時用str2指(zhi)向的(de)(de)(de)(de)拘(ju)(ju)留(liu)字(zi)(zi)(zi)符串(chuan)對(dui)(dui)(dui)(dui)(dui)象完(wan)成初始化(hua), * 然后調用append方(fang)法完(wan)成對(dui)(dui)(dui)(dui)(dui)str3所指(zhi)向的(de)(de)(de)(de)拘(ju)(ju)留(liu)字(zi)(zi)(zi)符串(chuan)的(de)(de)(de)(de)合并, * 接著調用StringBuilder的(de)(de)(de)(de)toString()方(fang)法在堆(dui)中(zhong)(zhong)(zhong)創(chuang)建(jian)一個String對(dui)(dui)(dui)(dui)(dui)象, * 最后將剛(gang)生成的(de)(de)(de)(de)String對(dui)(dui)(dui)(dui)(dui)象的(de)(de)(de)(de)堆(dui)地址(zhi)存(cun)放在局(ju)部變量(liang)str3中(zhong)(zhong)(zhong)。 * * 而str5存(cun)儲的(de)(de)(de)(de)是字(zi)(zi)(zi)符串(chuan)池中(zhong)(zhong)(zhong)"abcd"所對(dui)(dui)(dui)(dui)(dui)應的(de)(de)(de)(de)拘(ju)(ju)留(liu)字(zi)(zi)(zi)符串(chuan)對(dui)(dui)(dui)(dui)(dui)象的(de)(de)(de)(de)地址(zhi)。 * str4與(yu)str5地址(zhi)當然不一樣了。 * * 內(nei)存(cun)中(zhong)(zhong)(zhong)實際(ji)上有五個字(zi)(zi)(zi)符串(chuan)對(dui)(dui)(dui)(dui)(dui)象: * 三個拘(ju)(ju)留(liu)字(zi)(zi)(zi)符串(chuan)對(dui)(dui)(dui)(dui)(dui)象、一個String對(dui)(dui)(dui)(dui)(dui)象和一個StringBuilder對(dui)(dui)(dui)(dui)(dui)象。 */ String str2 = "ab"; //1個對(dui)象(xiang) String str3 = "cd"; //1個對(dui)象 String str4 = str2+str3; String str5 = "abcd"; System.out.println("str4 = str5 : " + (str4==str5)); // false //↑------------------------------------------------------over /** * 情景五: * JAVA編譯器對string + 基本類型/常(chang)(chang)量(liang) 是當成常(chang)(chang)量(liang)表達式(shi)直接(jie)求值來優化的(de)。 * 運行期(qi)的(de)兩個string相加,會(hui)產(chan)生新的(de)對象的(de),存儲在堆(heap)中 */ String str6 = "b"; String str7 = "a" + str6; String str67 = "ab"; System.out.println("str7 = str67 : "+ (str7 == str67)); //↑str6為變量,在運(yun)行期才會被(bei)解析。 final String str8 = "b"; String str9 = "a" + str8; String str89 = "ab"; System.out.println("str9 = str89 : "+ (str9 == str89)); //↑str8為(wei)常量變量,編譯期會被優化 //↑------------------------------------------------------over }
總結:
1.String類初始化后是不可變的(immutable)
這一說又要說很多,大家只要知道String的實例一旦生成就不會再改變了,比如說:String str=”kv”+”ill”+” “+”ans”; 就是有4個字符串常量,首先”kv”和”ill”生成了”kvill”存在內存中,然后”kvill”又和” ” 生成 “kvill “存在內存中,最后又和生成了”kvill ans”;并把這個字符串的地址賦給了str,就是因為String的”不可變”產生了很多臨時變量,這也就是為什么建議用StringBuffer的原 因了,因為StringBuffer是可改變的。
下面是一些String相關的常見問題:
String中的final用法和理解
final StringBuffer a = new StringBuffer("111");
final StringBuffer b = new StringBuffer("222");
a=b;//此句編譯不通過 final StringBuffer a = new StringBuffer("111");
a.append("222");// 編譯通過
可見,final只對引用的"值"(即內存地址)有效,它迫使引用只能指向初始指向的那個對象,改變它的指向會導致編譯期錯誤。至于它所指向的對象的變化,final是不負責的。
2.代碼(ma)中(zhong)的(de)字(zi)符串常(chang)量(liang)在(zai)編譯的(de)過程中(zhong)收集并放在(zai)class文件的(de)常(chang)量(liang)區(qu)中(zhong),如(ru)"123"、"123"+"456"等,含有變量(liang)的(de)表達式不會收錄(lu),如(ru)"123"+a。
3.JVM在(zai)加(jia)載類的(de)(de)(de)時(shi)候,根據常量區中的(de)(de)(de)字(zi)(zi)符串生成常量池(chi),每個(ge)字(zi)(zi)符序列如(ru)"123"會(hui)生成一個(ge)實例放(fang)在(zai)常量池(chi)里(li),這個(ge)實例是不在(zai)堆(dui)里(li)的(de)(de)(de),也不會(hui)被GC,這個(ge)實例的(de)(de)(de)value屬性(xing)從源碼的(de)(de)(de)構造函數(shu)看應該是用new創建(jian)數(shu)組置入123的(de)(de)(de),所以按我的(de)(de)(de)理解此時(shi)value存放(fang)的(de)(de)(de)字(zi)(zi)符數(shu)組地址是在(zai)堆(dui)里(li),如(ru)果有誤的(de)(de)(de)話歡迎大(da)家指正。
4.使用String不一定創建對象
在執行到雙引號包含字符串的語句時,如String a = "123",JVM會先到常量池里查找,如果有的話返回常量池里的這個實例的引用,否則的話創建一個新實例并置入常量池里。如果是 String a = "123" + b (假設b是"456"),前半部分"123"還是走常量池的路線,但是這個+操作符其實是轉換成[SringBuffer].Appad()來實現的,所以最終a得到是一(yi)個新的實例引(yin)用,而且a的value存(cun)(cun)放(fang)的是一(yi)個新申(shen)請的字符數組內存(cun)(cun)空間的地址(存(cun)(cun)放(fang)著"123456"),而此時"123456"在常量池(chi)中是未必存(cun)(cun)在的。
要注意: 我們在使用諸如String str = "abc";的格式定義類時,總是想當然地認為,創建了String類的對象str。擔心陷阱!對象可能并沒有被創建!而可能只是指向一個先前已經創建的對象。只有通過new()方法才能保證每次都創建一個新的對象
5.使用new String,一定創建對象
在(zai)執行String a = new String("123")的(de)時候,首(shou)先走常量池(chi)的(de)路線取到一(yi)個實(shi)例(li)的(de)引(yin)用(yong),然(ran)后(hou)在(zai)堆上創建一(yi)個新的(de)String實(shi)例(li),走以下構(gou)造(zao)函數給value屬性(xing)賦(fu)(fu)值(zhi),然(ran)后(hou)把實(shi)例(li)引(yin)用(yong)賦(fu)(fu)值(zhi)給a:
public String(String original) {
int size = original.count;
char[] originalValue = original.value;
char[] v;
if (originalValue.length > size) {
// The array representing the String is bigger than the new
// String itself. Perhaps this constructor is being called
// in order to trim the baggage, so make a copy of the array.
int off = original.offset;
v = Arrays.copyOfRange(originalValue, off, off+size);
} else {
// The array representing the String is the same
// size as the String, so no point in making a copy.
v = originalValue;
}
this.offset = 0;
this.count = size;
this.value = v;
}
從中我們可以看到(dao),雖然是新(xin)創建(jian)了一個String的(de)實(shi)例,但是value是等于常(chang)量(liang)池中的(de)實(shi)例的(de)value,即是說(shuo)沒有new一個新(xin)的(de)字符(fu)數組來存(cun)放"123"。
如果(guo)是String a = new String("123"+b)的(de)情(qing)況,首(shou)先看回第(di)4點,"123"+b得到(dao)一個實例后,再按上(shang)面的(de)構(gou)造函數執(zhi)行。
6.String.intern()
String對象的(de)實(shi)(shi)例(li)(li)調用(yong)intern方法后,可(ke)以讓JVM檢查(cha)常(chang)量池(chi),如果沒(mei)有實(shi)(shi)例(li)(li)的(de)value屬性對應的(de)字符串序列比如"123"(注意是檢查(cha)字符串序列而不(bu)(bu)是檢查(cha)實(shi)(shi)例(li)(li)本身),就(jiu)將本實(shi)(shi)例(li)(li)放入常(chang)量池(chi),如果有當(dang)前(qian)實(shi)(shi)例(li)(li)的(de)value屬性對應的(de)字符串序列"123"在常(chang)量池(chi)中存在,則返回(hui)常(chang)量池(chi)中"123"對應的(de)實(shi)(shi)例(li)(li)的(de)引(yin)用(yong)而不(bu)(bu)是當(dang)前(qian)實(shi)(shi)例(li)(li)的(de)引(yin)用(yong),即使當(dang)前(qian)實(shi)(shi)例(li)(li)的(de)value也(ye)是"123"。
public native String intern();
存在于.class文件中的(de)(de)常(chang)(chang)量(liang)池(chi),在運(yun)行期被JVM裝載,并且可以(yi)擴充。String的(de)(de) intern()方法就是擴充常(chang)(chang)量(liang)池(chi)的(de)(de) 一個方法;當一個String實(shi)例str調(diao)用(yong)(yong)intern()方法時,Java 查找常(chang)(chang)量(liang)池(chi)中 是否有相同Unicode的(de)(de)字(zi)符串常(chang)(chang)量(liang),如(ru)(ru)果有,則(ze)返回(hui)其的(de)(de)引用(yong)(yong),如(ru)(ru)果沒有,則(ze)在常(chang)(chang) 量(liang)池(chi)中增加一個Unicode等于str的(de)(de)字(zi)符串并返回(hui)它的(de)(de)引用(yong)(yong);看(kan)示例就清楚(chu)了
public static void main(String[] args) {
String s0 = "kvill";
String s1 = new String("kvill");
String s2 = new String("kvill");
System.out.println( s0 == s1 ); //false
System.out.println( "**********" );
s1.intern(); //雖(sui)然執行了s1.intern(),但它的返(fan)回值沒有賦給s1
s2 = s2.intern(); //把(ba)常量池中(zhong)"kvill"的引用賦給(gei)s2
System.out.println( s0 == s1); //flase
System.out.println( s0 == s1.intern() ); //true//說(shuo)明(ming)s1.intern()返回的是常量池中"kvill"的引用
System.out.println( s0 == s2 ); //true
}
最后我再(zai)破除一(yi)(yi)(yi)個(ge)錯誤的(de)(de)理(li)解(jie):有(you)人說,“使用 String.intern() 方法則(ze)(ze)可以將(jiang)一(yi)(yi)(yi)個(ge) String 類的(de)(de)保存到一(yi)(yi)(yi)個(ge)全局 String 表(biao)(biao)(biao)中(zhong) ,如(ru)果具(ju)有(you)相同值(zhi)的(de)(de) Unicode 字符(fu)串(chuan)已(yi)(yi)經(jing)在這(zhe)個(ge)表(biao)(biao)(biao)中(zhong),那(nei)么該(gai)方法返回表(biao)(biao)(biao)中(zhong)已(yi)(yi)有(you)字符(fu)串(chuan)的(de)(de)地址,如(ru)果在表(biao)(biao)(biao)中(zhong)沒有(you)相同值(zhi)的(de)(de)字符(fu)串(chuan),則(ze)(ze)將(jiang)自(zi)(zi)己(ji)的(de)(de)地址注冊到表(biao)(biao)(biao)中(zhong)”如(ru)果我把他說的(de)(de)這(zhe)個(ge)全局的(de)(de) String 表(biao)(biao)(biao)理(li)解(jie)為(wei)常(chang)量(liang)池的(de)(de)話,他的(de)(de)最后一(yi)(yi)(yi)句(ju)話,”如(ru)果在表(biao)(biao)(biao)中(zhong)沒有(you)相同值(zhi)的(de)(de)字符(fu)串(chuan),則(ze)(ze)將(jiang)自(zi)(zi)己(ji)的(de)(de)地址注冊到表(biao)(biao)(biao)中(zhong)”是錯的(de)(de):
public static void main(String[] args) {
String s1 = new String("kvill");
String s2 = s1.intern();
System.out.println( s1 == s1.intern() ); //false
System.out.println( s1 + " " + s2 ); //kvill kvill
System.out.println( s2 == s1.intern() ); //true
}
在這個(ge)類中我們沒(mei)有聲名(ming)一個(ge)”kvill”常(chang)(chang)(chang)量,所以常(chang)(chang)(chang)量池中一開始(shi)是(shi)沒(mei)有”kvill”的(de)(de),當我們調用s1.intern()后(hou)就(jiu)在常(chang)(chang)(chang)量池中新添(tian)加了(le)一 個(ge)”kvill”常(chang)(chang)(chang)量,原來(lai)的(de)(de)不(bu)在常(chang)(chang)(chang)量池中的(de)(de)”kvill”仍然存在,也(ye)就(jiu)不(bu)是(shi)“將自己的(de)(de)地址注冊到常(chang)(chang)(chang)量池中”了(le)。
s1==s1.intern() 為(wei)false說(shuo)明原來的”kvill”仍然存在;s2現在為(wei)常量池中”kvill”的地址,所以有s2==s1.intern()為(wei)true。
StringBuffer與StringBuilder的區別,它們的應用場景是什么?
StringBuilder 始于(yu) JDK 1.5
從 JDK 1.5 開始,帶有字符串(chuan)變量的連接操作(+),JVM 內部采(cai)用的是
StringBuilder 來實現的,而之(zhi)前(qian)這個操作(zuo)是采用(yong) StringBuffer 實現的。
public class Buffer {
public static void main(String[] args) {
String s1 = "aaaaa";
String s2 = "bbbbb";
String r = null;
int i = 3694;
r = s1 + i + s2;
for(int j=0;i<10;j++){
r+="23124";
}
}
}
使用命令javap -c Buffer查看其字節(jie)碼實現:

參考:
