java中String的透徹理解
阿新 • • 發佈:2019-01-23
要理解 java中String的運作方式,必須明確一點:String是一個非可變類(immutable)。什麼是非可變類呢?簡單說來,非可變類的例項是不能被修改的,每個例項中包含的資訊都必須在該例項建立的時候就提供出來,並且在物件的整個生存週期內固定不變。java為什麼要把String設計為非可變類呢?你可以問問 james Gosling :)。但是非可變類確實有著自身的優勢,如狀態單一,物件簡單,便於維護。其次,該類物件物件本質上是執行緒安全的,不要求同步。此外使用者可以共享非可變物件,甚至可以共享它們的內部資訊。(詳見
《Effective java》item 13)。String類在java中被大量運用,甚至在class檔案中都有其身影,因此將其設計為簡單輕便的非可變類是比較合適的。
一、建立。
好了,知道String是非可變類以後,我們可以進一步瞭解String的構造方式了。建立一個Stirng物件,主要就有以下兩種方式:
java 程式碼
String str1 = new String("abc");
Stirng str2 = "abc";
雖然兩個語句都是返回一個String物件的引用,但是jvm對兩者的處理方式是不一樣的。對於第一種,jvm會馬上在heap中建立一個String物件,然後將該物件的引用返回給使用者。對於第二種,jvm首先會在內部維護的strings pool中通過String的 equels 方法查詢是物件池中是否存放有該String物件,如果有,則返回已有的String物件給使用者,而不會在heap中重新建立一個新的String物件;如果物件池中沒有該String物件,jvm則在heap中建立新的String物件,將其引用返回給使用者,同時將該引用新增至strings
pool中。注意:使用第一種方法建立物件時,jvm是不會主動把該物件放到strings pool裡面的,除非程式呼叫 String的intern方法。看下面的例子:
java 程式碼
String str1 = new String("abc"); //jvm 在堆上建立一個String物件
//jvm 在strings pool中找不到值為“abc”的字串,因此
//在堆上建立一個String物件,並將該物件的引用加入至strings pool中
//此時堆上有兩個String物件
Stirng str2 = "abc";
if(str1 == str2){
System.out.println("str1 == str2");
}else{
System.out.println("str1 != str2");
}
//列印結果是 str1 != str2,因為它們是堆上兩個不同的物件
String str3 = "abc";
//此時,jvm發現strings pool中已有“abc”物件了,因為“abc”equels “abc”
//因此直接返回str2指向的物件給str3,也就是說str2和str3是指向同一個物件的引用
if(str2 == str3){
System.out.println("str2 == str3");
}else{
System.out.println("str2 != str3");
}
//列印結果為 str2 == str3
再看下面的例子:
java 程式碼
String str1 = new String("abc"); //jvm 在堆上建立一個String物件
str1 = str1.intern();
//程式顯式將str1放到strings pool中,intern執行過程是這樣的:首先檢視strings pool
//有沒“abc”物件的引用,沒有,則在堆中新建一個物件,然後將新物件的引用加入至
//strings pool中。執行完該語句後,str1原來指向的String物件已經成為垃圾物件了,隨時會
//被GC收集。
//此時,jvm發現strings pool中已有“abc”物件了,因為“abc”equels “abc”
//因此直接返回str1指向的物件給str2,也就是說str2和str1引用著同一個物件,
//此時,堆上的有效物件只有一個。
Stirng str2 = "abc";
if(str1 == str2){
System.out.println("str1 == str2");
}else{
System.out.println("str1 != str2");
}
//列印結果是 str1 == str2
為什麼jvm可以這樣處理String物件呢?就是因為String的非可變性。既然所引用的物件一旦建立就永不更改,那麼多個引用共用一個物件時互不影響。
二、串接(Concatenation)。
java程式設計師應該都知道濫用String的串接操作符是會影響程式的效能的。效能問題從何而來呢?歸根結底就是String類的非可變性。既然String物件都是非可變的,也就是物件一旦建立了就不能夠改變其內在狀態了,但是串接操作明顯是要增長字串的,也就是要改變String的內部狀態,兩者出現了矛盾。怎麼辦呢?要維護String的非可變性,只好在串接完成後新建一個String 物件來表示新產生的字串了。也就是說,每一次執行串接操作都會導致新物件的產生,如果串接操作執行很頻繁,就會導致大量物件的建立,效能問題也就隨之而來了。
為了解決這個問題,jdk為String類提供了一個可變的配套類,StringBuffer。使用StringBuffer物件,由於該類是可變的,串接時僅僅時改變了內部資料結構,而不會建立新的物件,因此效能上有很大的提高。針對單執行緒,jdk 5.0還提供了StringBuilder類,在單執行緒環境下,由於不用考慮同步問題,使用該類使效能得到進一步的提高。
三、String的長度
我們可以使用串接操作符得到一個長度更長的字串,那麼,String物件最多能容納多少字元呢?檢視String的原始碼我們可以得知類String中是使用域 count 來記錄物件字元的數量,而count 的型別為 int,因此,我們可以推測最長的長度為 2^32,也就是4G。
不過,我們在編寫原始碼的時候,如果使用 Sting str = "aaaa";的形式定義一個字串,那麼雙引號裡面的ASCII字元最多隻能有 65534 個。為什麼呢?因為在class檔案的規範中, CONSTANT_Utf8_info表中使用一個16位的無符號整數來記錄字串的長度的,最多能表示 65536個位元組,而java class 檔案是使用一種變體UTF-8格式來存放字元的,null值使用兩個位元組來表示,因此只剩下 65536- 2 = 65534個位元組。也正是變體UTF-8的原因,如果字串中含有中文等非ASCII字元,那麼雙引號中字元的數量會更少(一箇中文字元佔用三個位元組)。如果超出這個數量,在編譯的時候編譯器會報錯。
public class Test {
public static void stringReplace(String text) {
//把textString的地址copy給text,text也指向了textString的"java"
//text.replace('j', 'i');的結果是"iava"
//text = text.replace('j', 'i');就是重新使text指向"iava"
//由於該方法的返回型別是void,所以原來的textString沒變
text = text.replace('j', 'i');
}
public static void bufferReplace(StringBuffer text) {
//textBuffer的地址copy給text
//然後沿著text指向的字串(其實也就是textBuffer指向的物件)操作,新增一個"C"
//雖然沒有任何返回,但是這個操作影響到了textBuffer所指向的字串
//所以上個方法列印"java",這個方法列印"javaC"
text = text.append("C");
}
public static void main(String args[]) {
String textString = new String("java");
StringBuffer textBuffer = new StringBuffer("java");
stringReplace(textString);
bufferReplace(textBuffer);
System.out.println(textString + textBuffer);
}
}
首先把問題擺出來,先看這個程式碼
String a = "ab";
String b = "a" + "b";
System.out.println((a == b));
列印結果會是什麼?類似這樣的問題,有人考過我,我也拿來考過別人(蠻好玩的,大家也可以拿來問人玩),一般答案會是以下幾種:
1.true
"a" + "b" 的結果就是"ab",這樣a,b都是"ab"了,內容一樣所以"相等",結果true
一般java新人如是答。
2.false
"a" + "a"會生成新的物件"aa",但是這個物件和String a = "ab";不同,(a == b)是比較物件引用,因此不相等,結果false
對java的String有一定了解的通常這樣回答。
3.true
String a = "ab";建立了新的物件"ab"; 再執行String b = "a" + "b";結果b="ab",這裡沒有建立新的物件,而是從JVM字串常量池中獲取之前已經存在的"ab"物件。因此a,b具有對同一個string物件的引用,兩個引用相等,結果true.
能回答出這個答案的,基本已經是高手了,對java中的string機制比較瞭解。
很遺憾,這個答案,是不夠準確的。或者說,根本沒有執行時計算b = "a" + "b";這個操作.實際上執行時只有String b = "ab";
3的觀點適合解釋以下情況:
String a = "ab";
String b = "ab";
System.out.println((a == b));
如果String b = "a" + "b";是在執行期執行,則3的觀點是無法解釋的。執行期的兩個string相加,會產生新的物件的。(本文後面對此有解釋)
4.true
下面是我的回答:編譯優化+ 3的處理方式 = 最後的true
String b = "a" + "b";編譯器將這個"a" + "b"作為常量表達式,在編譯時進行優化,直接取結果"ab",這樣這個問題退化
String a = "ab";
String b = "ab";
System.out.println((a == b));
然後根據3的解釋,得到結果true
這裡有一個疑問就是String不是基本型別,像
int secondsOfDay = 24 * 60 * 60;
這樣的表示式是常量表達式,編譯器在編譯時直接計算容易理解,而"a" + "b" 這樣的表示式,string是物件不是基本型別,編譯器會把它當成常量表達式來優化嗎?
下面簡單證明我的推斷,首先編譯這個類:
public class Test {
private String a = "aa";
}
複製class檔案備用,然後修改為
public class Test {
private String a = "a" + "a";
}
再次編譯,用ue之類的文字編輯器開啟,察看二進位制內容,可以發現,兩個class檔案完全一致,連一個位元組都不差.
ok,真相大白了.根本不存在執行期的處理String b = "a" + "b";這樣的程式碼的問題,編譯時就直接優化掉了。
下面進一步探討,什麼樣的string + 表示式會被編譯器當成常量表達式?
String b = "a" + "b";
這個String + String被正式是ok的,那麼string + 基本型別呢?
String a = "a1";
String b = "a" + 1;
System.out.println((a == b)); //result = true
String a = "atrue";
String b = "a" + true;
System.out.println((a == b)); //result = true
String a = "a3.4";
String b = "a" + 3.4;
System.out.println((a == b)); //result = true
可見編譯器對string + 基本型別是當成常量表達式直接求值來優化的。
再注意看這裡的string都是"**"這樣的,我們換成變數來試試:
String a = "ab";
String bb = "b";
String b = "a" + bb;
System.out.println((a == b)); //result = false
這個好理解,"a" + bb中的bb是變數,不能進行優化。這裡很很好的解釋了為什麼3的觀點不正確,如果String+String的操作是在執行時進行的,則會產生新的物件,而不是直接從jvm的string池中獲取。
再修改一下,把bb作為常量變數:
String a = "ab";
final String bb = "b";
String b = "a" + bb;
System.out.println((a == b)); //result = true
竟然又是true,編譯器的優化好厲害啊,呵呵,考慮下面這種情況:
String a = "ab";
final String bb = getBB();
String b = "a" + bb;
System.out.println((a == b)); //result = false
private static String getBB() {
return "b";
}
看來java(包括編譯器和jvm)對string的優化,真的是到了極點了,string這個所謂的"物件",完全不可以看成一般的物件,java對string的處理近乎於基本型別,最大限度的優化了幾乎能優化的地方。
另外感嘆一下,string的+號處理,算是java語言裡面唯一的一個"運算子過載"(接觸過c++的人對這個不會陌生)吧?