1. 程式人生 > >java中String的透徹理解

java中String的透徹理解

     要理解 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++的人對這個不會陌生)吧?