要理解 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 字元,那麼
雙引號中字元的數量會更少(一個中文字元佔用三個位元組)。如果超出這個數量,在
編譯的時候編譯器會報錯。