關於java中的String類的字串常量池的詳解
字串常量池
我們知道字串的分配和其他的物件分配一樣,是需要消耗高昂的時間和空間的,而且字串我們使用得非常多,JVM為了提高效能和減少記憶體的開銷,在例項化字串的時候做了一些優化:使用字串常量池。
每當我們建立字串常量時,JVM會首先檢查字串常量池,如果該字串已經存在常量池中,那麼就直接返回常量池中例項引用。如果字串不存在常量池中,就會例項化該字串並將其放入常量池中。由於String字串的不可變性,所以可以十分肯定在字串常量池中是不可能存在兩個相同的字串。
下面再來看幾個例子:
例子1:
/* * 採用字面值的方式賦值 */ public void test1(){ String str1="aaa"; String str2="aaa"; System.out.println("===========test1============"); System.out.println(str1==str2);//true 可以看出str1跟str2是指向同一個物件 }
執行上述程式碼,結果為:true。 分析:當執行String str1="aaa"時,JVM首先會去字串池中查詢是否存在"aaa"這個物件,如果不存在,則在字串池中建立"aaa"這個物件,然後將池中"aaa"這個物件的引用地址返回給字串常量str1,這樣str1會指向池中"aaa"這個字串物件;如果存在,則不建立任何物件,直接將池中"aaa"這個物件的地址返回,賦給字串常量。當建立字串物件str2時,字串池中已經存在"aaa"這個物件,直接把物件"aaa"的引用地址返回給str2,這樣str2指向了池中"aaa"這個物件,也就是說str1和str2指向了同一個物件,因此語句System.out.println(str1 == str2)輸出:true。
例子2:
/** * 採用new關鍵字新建一個字串物件 */ public void test2(){ String str3=new String("aaa"); String str4=new String("aaa"); System.out.println("===========test2============"); System.out.println(str3==str4);//false 可以看出用new的方式是生成不同的物件 }
執行上述程式碼,結果為:false。
分析: 採用new關鍵字新建一個字串物件時,JVM首先在字串池中查詢有沒有"aaa"這個字串物件,如果有,則不在池中再去建立"aaa"這個物件了,直接在堆中建立一個"aaa"字串物件,然後將堆中的這個"aaa"物件的地址返回賦給引用str3,這樣,str3就指向了堆中建立的這個"aaa"字串物件;如果沒有,則首先在字串池中建立一個"aaa"字串物件,然後再在堆中建立一個"aaa"字串物件,然後將堆中這個"aaa"字串物件的地址返回賦給str3引用,這樣,str3指向了堆中建立的這個"aaa"字串物件。當執行String str4=new String("aaa")時, 因為採用new關鍵字建立物件時,每次new出來的都是一個新的物件,也即是說引用str3和str4指向的是兩個不同的物件,因此語句System.out.println(str3 == str4)輸出:false。
例子3:
/** * 編譯期確定 */ public void test3(){ String s0="helloworld"; String s1="helloworld"; String s2="hello"+"world"; System.out.println("===========test3============"); System.out.println(s0==s1); //true 可以看出s0跟s1是指向同一個物件 System.out.println(s0==s2); //true 可以看出s0跟s2是指向同一個物件 }
執行上述程式碼,結果為:true、true。
分析:因為例子中的s0和s1中的"helloworld”都是字串常量,它們在編譯期就被確定了,所以s0==s1為true;而"hello”和"world”也都是字串常量,當一個字串由多個字串常量連線而成時,它自己肯定也是字串常量,所以s2也同樣在編譯期就被解析為一個字串常量,所以s2也是常量池中"helloworld”的一個引用。所以我們得出s0==s1==s2。
例子4:
/** * 編譯期無法確定 */ public void test4(){ String s0="helloworld"; String s1=new String("helloworld"); String s2="hello" + new String("world"); System.out.println("===========test4============"); System.out.println( s0==s1 ); //false System.out.println( s0==s2 ); //false System.out.println( s1==s2 ); //false }
執行上述程式碼,結果為:false、false、false。
分析:用new String() 建立的字串不是常量,不能在編譯期就確定,所以new String() 建立的字串不放入常量池中,它們有自己的地址空間。
s0還是常量池中"helloworld”的引用,s1因為無法在編譯期確定,所以是執行時建立的新物件"helloworld”的引用,s2因為有後半部分new String(”world”)所以也無法在編譯期確定,所以也是一個新建立物件"helloworld”的引用。
例子5:
/** * 繼續-編譯期無法確定 */ public void test5(){ String str1="abc"; String str2="def"; String str3=str1+str2; System.out.println("===========test5============"); System.out.println(str3=="abcdef"); //false }
執行上述程式碼,結果為:false。
分析:因為str3指向堆中的"abcdef"物件,而"abcdef"是字串池中的物件,所以結果為false。JVM對String str="abc"物件放在常量池中是在編譯時做的,而String str3=str1+str2是在執行時刻才能知道的。new物件也是在執行時才做的。而這段程式碼總共建立了5個物件,字串池中兩個、堆中三個。+運算子會在堆中建立來兩個String物件,這兩個物件的值分別是"abc"和"def",也就是說從字串池中複製這兩個值,然後在堆中建立兩個物件,然後再建立物件str3,然後將"abcdef"的堆地址賦給str3。
步驟: 1)棧中開闢一塊中間存放引用str1,str1指向池中String常量"abc"。 2)棧中開闢一塊中間存放引用str2,str2指向池中String常量"def"。 3)棧中開闢一塊中間存放引用str3。 4)str1 + str2通過StringBuilder的最後一步toString()方法還原一個新的String物件"abcdef",因此堆中開闢一塊空間存放此物件。 5)引用str3指向堆中(str1 + str2)所還原的新String物件。 6)str3指向的物件在堆中,而常量"abcdef"在池中,輸出為false。
例子6:
/** * 編譯期優化 */ public void test6(){ String s0 = "a1"; String s1 = "a" + 1; System.out.println("===========test6============"); System.out.println((s0 == s1)); //result = true String s2 = "atrue"; String s3= "a" + "true"; System.out.println((s2 == s3)); //result = true String s4 = "a3.4"; String s5 = "a" + 3.4; System.out.println((s4 == s5)); //result = true }
執行上述程式碼,結果為:true、true、true。
分析:在程式編譯期,JVM就將常量字串的"+"連線優化為連線後的值,拿"a" + 1來說,經編譯器優化後在class中就已經是a1。在編譯期其字串常量的值就確定下來,故上面程式最終的結果都為true。
例子7:
/** * 編譯期無法確定 */ public void test7(){ String s0 = "ab"; String s1 = "b"; String s2 = "a" + s1; System.out.println("===========test7============"); System.out.println((s0 == s2)); //result = false }
執行上述程式碼,結果為:false。
分析:JVM對於字串引用,由於在字串的"+"連線中,有字串引用存在,而引用的值在程式編譯期是無法確定的,即"a" + s1無法被編譯器優化,只有在程式執行期來動態分配並將連線後的新地址賦給s2。所以上面程式的結果也就為false。
例子8:
/** * 比較字串常量的“+”和字串引用的“+”的區別 */ public void test8(){ String test="javalanguagespecification"; String str="java"; String str1="language"; String str2="specification"; System.out.println("===========test8============"); System.out.println(test == "java" + "language" + "specification"); System.out.println(test == str + str1 + str2); }
執行上述程式碼,結果為:true、false。
分析:為什麼出現上面的結果呢?這是因為,字串字面量拼接操作是在Java編譯器編譯期間就執行了,也就是說編譯器編譯時,直接把"java"、"language"和"specification"這三個字面量進行"+"操作得到一個"javalanguagespecification" 常量,並且直接將這個常量放入字串池中,這樣做實際上是一種優化,將3個字面量合成一個,避免了建立多餘的字串物件。而字串引用的"+"運算是在Java執行期間執行的,即str + str2 + str3在程式執行期間才會進行計算,它會在堆記憶體中重新建立一個拼接後的字串物件。總結來說就是:字面量"+"拼接是在編譯期間進行的,拼接後的字串存放在字串池中;而字串引用的"+"拼接運算實在執行時進行的,新建立的字串存放在堆中。
對於直接相加字串,效率很高,因為在編譯器便確定了它的值,也就是說形如"I"+"love"+"java"; 的字串相加,在編譯期間便被優化成了"Ilovejava"。對於間接相加(即包含字串引用),形如s1+s2+s3; 效率要比直接相加低,因為在編譯器不會對引用變數進行優化。
例子9:
/** * 編譯期確定 */ public void test9(){ String s0 = "ab"; final String s1 = "b"; String s2 = "a" + s1; System.out.println("===========test9============"); System.out.println((s0 == s2)); //result = true }
執行上述程式碼,結果為:true。
分析:和例子7中唯一不同的是s1字串加了final修飾,對於final修飾的變數,它在編譯時被解析為常量值的一個本地拷貝儲存到自己的常量池中或嵌入到它的位元組碼流中。所以此時的"a" + s1和"a" + "b"效果是一樣的。故上面程式的結果為true。
例子10:
/** * 編譯期無法確定 */ public void test10(){ String s0 = "ab"; final String s1 = getS1(); String s2 = "a" + s1; System.out.println("===========test10============"); System.out.println((s0 == s2)); //result = false } private static String getS1() { return "b"; }
執行上述程式碼,結果為:false。
分析:這裡面雖然將s1用final修飾了,但是由於其賦值是通過方法呼叫返回的,那麼它的值只能在執行期間確定,因此s0和s2指向的不是同一個物件,故上面程式的結果為false。