Java中的String類之我見

這是一篇早就寫好的文章,抽空還是決定搬上來。

String類概述

String類位於java.lang包下,java.lang這個包裏面放置的所有類構成了java編程語言的基石。跟String類同在一包下面的類還有Object,八種java原始類型的包裝類,兩個String類的生成類:StringBuffer和StringBuilder。由此可見String類對於java編程語言的重要性。

String類在使用java語言編寫的程序中是經常用到的一個類,較高的使用頻率加上String類天生的不可變性以及java語言對高效的追求,共同造就了String類在java語言中的特殊地位。

一旦String對象被初始化,那麼這個對象至死(被虛擬機回收)都不會發生改變(這裏所說的改變是指這個字符串所表示的字符序列的改變)。爲了操作上的方便java的設計者又爲String類型專門重寫了一個操作符,大名鼎鼎的“+”操作符,一個常見的字符串連結操作符。在帶來便捷性的同時也帶來了不好的一面,當我們每使用一次“+”來連結兩個字符串變量(注意這裏不是字面量,而是字符串類型的變量)時,每進行一次“+”操作都會生成一個臨時的字符串對象來存儲運算過程中的中間值;這種效率對於java語言來說是不能忍受的,於是這就催生了StringBuffer的誕生,StringBuffer內部包含一個可以改變的字符數組(char[]),因此在多個“+”操作時就不必爲每次運算生成一個臨時的用於表示中間值的字符串對象;而是使用StringBuffer.append()函數將後面的字符串添加進字符數組,在一系列的“+”操作執行完成後,統一生成一個String對象,從而避免了字符串中間值的產生;隨着時間的推移到了2004年J2SE 5.0誕生的時候又使用了最新的StringBuilder來替換當初的StringBuffer來輔助“+”操作的運算過程,StringBuffer是線程安全的,在某些單線程操作中使用產生了對資源的浪費,而StringBuilder則是基於單線程工作的,並不考慮線程安全。

下面的代碼片段列出了String類的定義:

public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];
}

final關鍵字聲明String類是一個不可被繼承的類,private final char value[] 是用來存儲該字符串所表示的字符序列,可以看到他是被final關鍵字修飾的常量,也就是一經創建不能更改的。

String類的實質就是一個不可變的字符數組,它具備這些特點:不可變、使用頻率高、可進行“+”操作。它的特殊之處就在於他不是java中的8中基本類型之一卻具有基本類型的很多相同特性,字面量中除了8中基本類型之外還有一種就是字符串類型的字面量。

String對象的創建

String對象的創建方式可以分爲:顯式(explicitly)、隱式(implicitly)兩種,顯式創建就是我們常用的使用new關鍵字來創建一個對象,而隱式創建則是我們通過直觀的代碼看不到的,是虛擬機(JVM)運行的時候自己幫我們創建的。

String對象的顯式創建

String str = new String("this a string object");

上面這段代碼就是我們常見的使用new關鍵字創建了一個value爲"this a string object"的一個字符串對象。對象的內存空間分配在虛擬機(JVM)堆上,然後將對象的地址返回,str變量的值就是該字符串對象在內存上的地址。

String對象的隱式創建

(1).JVM加載包含有String字面量的Class或者Interface的時候會隱式的創建一個表示String字面量的String對象,代碼示例如下:

//1.定義一個String字面量
String str = "String literals";
//2.根據String字面量顯式創建一個字符串對象
String strObject = new String("String literals");
//3.常量表達式
String literalExp = "abc" + "def";

以上三行代碼中都有String類型的字面量出現,字符串字面量值在編譯完成以後就會以二進制形式存在於class文件中,class文件中有一部分區域叫做常量池(CONSTANT_POOL),當類被加載的時候,虛擬機會根據常量池中的字符串字面量創建一個對應的String對象,內容相同的字符串字面量在類加載的時候只會被創建一個,也就是說常量池中的字符串對象是可以被共享的。

對於第2句代碼,初看只是用顯式的方式創建了一個字符串對象,但是不要忘記了參數是以字面量的形式出現的,所以那句話會創建兩個String對象,一個是在類加載的時候由虛擬機隱式創建的;另外一個是在那行代碼運行的時候根據字面量重新構建了另外一個新的String對象。

對於第3句代碼,看起來像是有兩個String字面量,但是他們使用“+”操作符進行了字符串連結操作。在編譯器進行編譯的時候會對常量表達式進行簡單的運算因此在編譯過後的class文件中我們看到常量池中存在的字面量值是"abcdef"而不是"abc"和"def"這兩個String字面量。

將以上三行代碼放在同一個類中,編譯,然後使用javap反編譯工具將class文件反編譯以後,我們會看到如圖1-1的結果:


圖1-1

根據圖1-1可以清楚的看到常量池中只有兩個String字面量:"String literals" 和"abcdef"跟前面所說的結果相吻合。只有一個"Stringliterial"表名常量池中字面量是可以被共享的。

(2).String的“+”連結操作符出現在非常量表達式中,代碼如下:

//非常量表達式
Stringa = "a";
Stringb = "b";
Stringc = a + b;

最後一句代碼是對兩個字符串變量進行“+”操作而不是對String字面量進行連結操作,所以這個連結操作在編譯器處理的時候並不會進行運算,而是在代碼真正執行的時候才進行計算的,並根據連結後的字符串在堆中生成一個新的String對象。具體的過程可以參考圖1-2運行時字符串連結虛擬機指令:


圖1-2

通過上圖可以看到在編譯完成後的class文件中編譯器會自己隱式的創建一個StringBuilder對象,然後利用StringBuilder連結字符串,並生成一個新的字符串對象。

上面講述了String對象的隱式創建場景,一種是在類加載的時候爲字符串字面量創建對應的String對象;第二種就是對字符串類型變量進行“+”操作時,在運行時通過StringBuilder隱式創建。

下面將會展示java語言規範中示例:

package testPackage;
class Test{
	public static void main(String[] args){
		String hello = "Hello", lo = "lo";
		System.out.print((hello == "Hello") + "");
		System.out.print((Other.hello == hello) + "");
		System.out.print((other.Other.hello == hello) + "");
		System.out.print((hello == "Hel" + "lo") + "");
		System.out.print((hello == "Hel" + lo) + "");
		System.out.print((hello == ("Hel" + lo).intern()) + "");
	}
}
class Other{static String hello = "Hello";}
package other;
public class Other{static String hello = "Hello";}
輸出結果:
true true true true false true

上面的例子主要闡述了以下6個要點:

1.  同一個包中同一個類裏面的相同字面量引用相同的字符串對象;

2.  同一個包中不同類裏面的相同字面量引用相同的字符串對象;

3.  不同包而且不同類裏面的相同字面量引用相同的字符串對象;

4.  常量表達式在編譯器被運算,經由常量表達式獲得的字符串也被當做字面量處理;

5.  運行時才進行計算的字符串連結操作會創建新的字符串對象;

6.  顯式扣留某個字符串會將之前已經存在的跟改字符串一樣的字符串引用返回;

JVM內存中的String對象

String可以擁有特殊的表現形式,一定程度上是對其內部表現形式的一種體現,在具體講解String在虛擬機內存中的存儲之前先簡單介紹一下運行時常量池、虛擬機堆和方法區這三個概念。

java堆(Heap)是被所有java線程共享的一塊內存區域,在虛擬機啓動時創建。此內存區域的唯一目的就是存放對象實例,幾乎所有的對象實例都在這裏分配內存。簡單點講就是用來存放java實例的地方。

方法區(Method Area)與java堆一樣,是各個線程共享的內存區域,用來存儲已經被虛擬機加載的類信息、常量、靜態變量、及時編譯器編譯後的代碼等數據。

運行時常量池(Runtime Constant Pool)是方法區的一部分。Class文件中除了類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池,用於存放編譯器生成的各種字面量和符號引用,這部分內容將在類加載後存放在方法區的運行時常量池中存放。運行時常量池是可以動態改變的,也就是說並非是只有Class文件中常量池中的內存才能進入方法區運行時常量池,運行期間也可以將常量放入池中。

跟String相關的主要就是運行時常量池和java堆,毫無疑問當我們使用new指令創建一個String對象的話,那麼這個對象的內存空間肯定是分配在虛擬機堆上的。類文件中出現的字符串類型的字面量則是在虛擬機加載二進制的Class文件的時候就被加載進入內存,String字面量在常量池中也是以字符串對象的形式存在的,常量池中的字符串對象是可以被其他與其內容相同的字符串字面量共享的。

因此String對象在虛擬機內存中有兩個區域會存在,一是運行時常量池,二是java堆。運行時常量池中相同內容的字符串只會存在一個,任何指向該字面量的引用都是共享的同一個對象;使用new指令創建的對象,即使字符串的內容一樣,每次都會創建一個新的字符串對象。下面的代碼對此做出驗證:

publicclass Test{
   public static void main(String[] args){
       String literal0 = "java";
       String literal1 = "java";
       String StrIns0 = newString("java");
       String StrIns1 = newString("java");
       System.out.println("對同一個字面量的引用 指向同一個對象?:"+ (literal1 == literal0));
       System.out.println("使用同一個字面量創建String對象 是同一個對象?:" + (StrIns1 == StrIns0));
       System.out.println("內容相同的字符串對象引用跟字面量引用指向同一個對象?:"+ (StrIns0 == literal0));
   }
}
代碼執行的結果如圖1-3所示:

圖1-3

上述代碼印證了內容相同的字符串字面量是同一個對象;new指令創建的內容相同的字符串對象是不同的對象;內容相同的字面量跟new指令創建的對象是不同的對象。

String與StringBuffer、StringBuilder

String類用來表示字符串。Java語言中所有字符串常量都是通過String類的實例來實現的。所有的String實例都是常量,一旦被創建它們的值不允許被修改的。

StringBuffer類是一個線程安全的,可以修改的字符序列。StringBuffer跟String類很相似,但是不同點在於StringBuffer是可以修改的。它隨時都可以包含一個字符序列,但是它的長度和內容是可以通過調用響應的方法來改變的。在多線程的場景中使用StringBuffer是線程安全的,內部的某些操作是同步的,也就是說多個線程對於某個StringBuffer對象的方法的調用順序跟最終這個方法在多個線程中執行的順序是一致的,串行的。

StringBuilder同樣是一個可變的字符串序列。它所提供的API兼容StringBuffer,但是不保證同步。在單線程場景下用來替代StringBuffer,單線程的場景又是比較普遍的場景,值得注意的是StringBuilder是一直到了JDK 1.5才被新添加進開發包的,可見是經過長期的實踐對JDK的一個提升。Java官方還是推薦能使用StringBuilder就不要使用StringBuffer,前者的效率更高。它跟StringBuffer的主要區別就在於它不是線程安全的。

String中的用來存放字符串的字符型數組是private final修飾的,而StringBuffer和StringBuilder裏的value屬性卻是從AbstractStringBuilder繼承的且不是final修飾的,這點就從根本上決定了StringBuffer以及StringBuilder是可變字符串。StringBuffer和StringBuilder中的count變量也是可變的,同時少了一個offset屬性,也是因爲後兩者的value的字符數組是可變的,而String類的value變量是不可變的,爲了挽救效率會對內部封裝的value字符數組就行一次再利用,尤其是在調用subString方法的時候。

String、StringBuffer和StringBuilder的類圖如圖1-4所示:


圖1-4 類圖

類圖上體現出來的另外一個不同點在於StringBuffer和StringBuilder沒有實現Comparable接口,也就是說明後兩個類不能進行比較操作。Appendable接口則封裝了對append方法的三種重載。



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章