本文轉自我的blog:産品經理新視線
builder最佳 單線程就用StringBuilder吧~~ 她技巧最好~最忠誠~讓你一個人爽翻天~~~
多線程就用StringBuffer吧~~ 她就是一Bus = =~ 可供多人使用~多P必備~
要是你隻是存取下資料String最常用了 她長得最漂亮~看看足矣!~YY專用~
每次上網沖杯Java時,都能看到關于String無休無止的争論。還是覺得有必要讓這個讨厭又很可愛的String美眉,赤裸裸的站在我們這些Java色狼面前了 :o 。嘿嘿....
衆所周知,String是由字元組成的串,在程式中使用頻率很高。Java中的String是一個類,而并非基本資料類型。 不過她卻不是普通的類哦!!!
【偷窺鏡頭1】 String對象的建立1、關于類對象的建立,很普通的一種方式就是利用構造器,String類也不例外:String s=new String("Hello world"); 問題是參數"Hello world"是什麼東西,也是字元串對象嗎?莫非用字元串對象建立一個字元串對象?
2、當然,String類對象還有一種大家都很喜歡的建立方式:String s="Hello world"; 但是有點怪呀,怎麼與基本資料類型的指派操作(int i=1)很像呀?
在開始解釋這些問題之前,我們先引入一些必要的知識:
★ Java class檔案結構 和常量池我們都知道,Java程式要運作,首先需要編譯器将源代碼檔案編譯成位元組碼檔案(也就是.class檔案)。然後在由JVM解釋執行。
class檔案是8位位元組的二進制流 。這些二進制流的涵義由一些緊湊的有意義的項 組成。比如class位元組流中最開始的4個位元組組成的項叫做魔數 (magic),其意義在于分辨class檔案(值為0xCAFEBABE)與非class檔案。class位元組流大緻結構如下圖左側。

其中,在class檔案中有一個非常重要的項——
常量池。這個常量池專門放置源代碼中的符号資訊(并且不同的符号資訊放置在不同标志的常量表中)。如上圖右側是HelloWorld代碼中的常量表(HelloWorld代碼如下),其中有四個不同類型的常量表(四個不同的常量池入口)。
關于常量池的具體細節,請參照我的部落格《Class檔案内容及常量池 》Java代碼
public class HelloWorld{
void hello(){
System.out.println("Hello world");
}
}
通過上圖可見,代碼中的"Hello world"字元串字面值被編譯之後,可以清楚的看到存放在了class常量池中的字元串常量表中(上圖右側紅框區域)。
★ JVM運作class檔案源代碼編譯成class檔案之後,JVM就要運作這個class檔案。它首先會用類裝載器加載進class檔案。然後需要建立許多記憶體資料結構來存 放 class檔案中的位元組資料。比如class檔案對應的類資訊資料、常量池結構、方法中的二進制指令序列、類方法與字段的描述資訊等等。當然,在運作的時 候,還需要為方法建立棧幀等。這麼多的記憶體結構當然需要管理,JVM會把這些東西都組織到幾個“
運作時資料區”中。這裡面就有我們經常說的“
方法區”、“
堆”、“
Java棧”等。
詳細請參見我的部落格《Java 虛拟機體系結構 》 。上面我們提到了,在Java源代碼中的每一個字面值字元串,都會在編譯成class檔案階段,形成标志号 為8(CONSTANT_String_info)的常量表 。 當JVM加載 class檔案的時候,會為對應的常量池建立一個記憶體資料結構,并存放在方法區中。同時JVM會自動為CONSTANT_String_info常量表中 的字元串常量字面值 在堆中 建立 新的String對象(intern字元串 對象 ,又叫拘留字元串對象)。然後把CONSTANT_String_info常量表的入口位址轉變成這個堆中String對象的直接位址(常量池解 析)。
這裡很關鍵的就是這個
拘留字元串對象。源代碼中所有相同字面值的字元串常量隻可能建立唯一一個拘留字元串對象。 實際上JVM是通過一個記錄了拘留字元串引用的内部資料結構來維持這一特性的。在Java程式中,可以調用String的intern()方法來使得一個 正常字元串對象成為拘留字元串對象。我們會在後面介紹這個方法的。
★ 操作碼助憶符指令有了上面闡述的兩個知識前提,下面我們将根據二進制指令來差別兩種字元串對象的建立方式:
(1) String s=new String("Hello world");編譯成class檔案後的指令(在myeclipse中檢視):
Class位元組碼指令集代碼
0 new java.lang.String [15] //在堆中配置設定一個String類對象的空間,并将該對象的位址堆入操作數棧。
3 dup //複制操作數棧頂資料,并壓入操作數棧。該指令使得操作數棧中有兩個String對象的引用值。
4 ldc <String "Hello world"> [17] //将常量池中的字元串常量"Hello world"指向的堆中拘留String對象的位址壓入操作數棧
6 invokespecial java.lang.String(java.lang.String) [19] //調用String的初始化方法,彈出操作數棧棧頂的兩個對象位址,用拘留String對象的值初始化new指令建立的String對象,然後将這個對象的引用壓入操作數棧
9 astore_1 [s] // 彈出操作數棧頂資料存放在局部變量區的第一個位置上。此時存放的是new指令建立出的,已經被初始化的String對象的位址。
事實上,在運作這段指令之前,JVM就已經為"Hello world"在堆中建立了一個拘留字元串( 值得注意的是:如果源程式中還有一個"Hello world"字元串常量,那麼他們都對應了同一個堆中的拘留字元串)。然後用這個拘留字元串的值來初始化堆中用new指令建立出來的新的String對 象,局部變量s實際上存儲的是new出來的堆對象位址。 大家注意了,此時在JVM管理的堆中,有兩個相同字元串值的String對象:一個是拘留字元串對象,一個是new建立的字元串對象。如果還有一條建立語 句String s1=new String("Hello world");堆中有幾個值為"Hello world"的字元串呢? 答案是3個,大家好好想想為什麼吧!
(2)将String s="Hello world";編譯成class檔案後的指令:
Class位元組碼指令集代碼
0 ldc <String "Hello world"> [15]//将常量池中的字元串常量"Hello world"指向的堆中拘留String對象的位址壓入操作數棧
2 astore_1 [str] // 彈出操作數棧頂資料存放在局部變量區的第一個位置上。此時存放的是拘留字元串對象在堆中的位址
和上面的建立指令有很大的不同,局部變量s存儲的是早已建立好的拘留字元串的堆位址。 大家好好想想,如果還有一條穿件語句String s1="Hello word";此時堆中有幾個值為"Hello world"的字元串呢?答案是1個。那麼局部變量s與s1存儲的位址是否相同呢? 呵呵, 這個你應該知道了吧。
★ 鏡頭總結:String類型脫光了其實也很普通。真正讓她神秘的原因就在于
CONSTANT_String_info常量表和
拘留字元串對象的存在。現在我們可以解決江湖上的許多紛争了。
【 紛争1】關于字元串相等關系的争論Java代碼
//代碼1
String sa=new String("Hello world");
String sb=new String("Hello world");
System.out.println(sa==sb); // false
//代碼2
String sc="Hello world";
String sd="Hello world";
System.out.println(sc==sd); // true
代碼1中局部變量sa,sb中存儲的是JVM在堆中new出來的兩個String對象的記憶體位址。雖然這兩個String對象的值(char[]存 放的字元序列)都是"Hello world"。 是以"=="比較的是兩個不同的堆位址。代碼2中局部變量sc,sd中存儲的也是位址,但卻都是常量池中"Hello world"指向的堆的唯一的那個拘留字元串對象的位址 。自然相等了。
【紛争2】字元串“+”操作的内幕
Java代碼
//代碼1
String sa = "ab";
String sb = "cd";
String sab=sa+sb;
String s="abcd";
System.out.println(sab==s); // false
//代碼2
String sc="ab"+"cd";
String sd="abcd";
System.out.println(sc==sd); //true
代碼1中局部變量sa,sb存儲的是堆中兩個拘留字元串對象的位址。而 當執行sa+sb時,JVM首先會在堆中建立一個StringBuilder類,同時用sa指向的拘留字元串對象完成初始化,然後調用append方法完 成對sb所指向的拘留字元串的合并操作,接着調用StringBuilder的toString()方法在堆中建立一個String對象,最後将剛生成的 String對象的堆位址存放在局部變量sab中。而局部變量s存儲的是常量池中"abcd"所對應的拘留字元串對象的位址。 sab與s位址當然不一樣了。這裡要注意了,代碼1的堆中實際上有五個字元串對象:三個拘留字元串對象、一個String對象和一個 StringBuilder對象。
代碼2中"ab"+"cd"會直接在編譯期就合并成常量"abcd", 是以相同字面值常量"abcd"所對應的是同一個拘留字元串對象,自然位址也就相同。
【 偷窺 鏡頭二】 String三姐妹(String,StringBuffer,StringBuilder)String扒的差不多了。但他還有兩個妹妹StringBuffer,StringBuilder長的也不錯哦!我們也要下手了:
String(大姐,出生于JDK1.0時代) 不可變字元序列
StringBuffer(二姐,出生于JDK1.0時代) 線程安全的可變字元序列
StringBuilder(小妹,出生于JDK1.5時代) 非線程安全的可變字元序列
★StringBuffer與String的可變性問題。我們先看看這兩個類的部分源代碼:
Java代碼
//String
public final class String
{
private final char value[];
public String(String original) {
// 把原字元串original切分成字元數組并賦給value[];
}
}
//StringBuffer
public final class StringBuffer extends AbstractStringBuilder
{
char value[]; //繼承了父類AbstractStringBuilder中的value[]
public StringBuffer(String str) {
super(str.length() + 16); //繼承父類的構造器,并建立一個大小為str.length()+16的value[]數組
append(str); //将str切分成字元序列并加入到value[]中
}
}
很顯然,String和StringBuffer中的value[]都用于存儲字元序列。但是,
(1) String中的是常量(final)數組,隻能被指派一次。
比如:new String("abc")使得value[]={'a','b','c'},之後這個String對象中的value[]再也不能改變了。這也正是大家常說的,String是不可變的原因 。
注意:這個對初學者來說有個誤區,有人說String str1=new String("abc"); str1=new String("cba");不是改變了字元串str1嗎?那麼你有必要先搞懂對象引用和對象本身的差別。這裡我簡單的說明一下,對象本身指的是存放在堆 空間中的該對象的執行個體資料(非靜态非常量字段)。而對象引用指的是堆中對象本身所存放的位址,一般方法區和Java棧中存儲的都是對象引用,而非對象本身 的資料。
(2) StringBuffer中的value[]就是一個很普通的數組,而且可以通過append()方法将新字元串加入value[]末尾。這樣也就改變了value[]的内容和大小了。
比如:new StringBuffer("abc")使得value[]={'a','b','c','',''...}(注意構造的長度是 str.length()+16)。如果再将這個對象append("abc"),那麼這個對象中的value[]= {'a','b','c','a','b','c',''....}。這也就是為什麼大家說 StringBuffer是可變字元串 的涵義了。從這一點也可以看出,StringBuffer中的value[]完全可以作為字元串的緩沖區功能。其累加性能是很不錯的,在後面我們會進行比 較。
總結,讨論String和StringBuffer可不可變。本質上是指對象中的value[]字元數組可不可變,而不是對象引用可不可變。
★StringBuffer與StringBuilder的線程安全性問題StringBuffer和StringBuilder可以算是雙胞胎了,這兩者的方法沒有很大差別。但線上程安全性方面,StringBuffer允許多線程進行字元操作。這是因為在源代碼中StringBuffer的很多方法都被關鍵字
synchronized修飾了,而StringBuilder沒有。
有多線程程式設計經驗的程式員應該知道synchronized。這個關鍵字是為
線程同步機制設定的。我簡要闡述一下synchronized的含義:
每一個類對象都對應一把鎖,當某個線程A調用類對象O中的synchronized方法M時,必須獲得對象O的鎖才能夠執行M方法,否則線程A阻塞。一旦 線程A開始執行M方法,将獨占對象O的鎖。使得其它需要調用O對象的M方法的線程阻塞。隻有線程A執行完畢,釋放鎖後。那些阻塞線程才有機會重新調用M方 法。這就是解決線程同步問題的鎖機制。
了解了synchronized的含義以後,大家可能都會有這個感覺。多線程程式設計中StringBuffer比StringBuilder要安全多了 ,事實确實如此。如果有多個線程需要對同一個字元串緩沖區進行操作的時候,StringBuffer應該是不二選擇。
注意:是不是String也不安全呢?事實上不存在這個問題,String是不可變的。線程對于堆中指定的一個String對象隻能讀取,無法修改。試問:還有什麼不安全的呢?
★String和StringBuffer的效率問題(這可是個熱門話題呀!)首先說明一點:StringBuffer和StringBuilder可謂雙胞胎,StringBuilder是1.5新引入的,其前身就是 StringBuffer。StringBuilder的效率比StringBuffer稍高,如果不考慮線程安全,StringBuilder應該是首 選。另外,JVM運作程式主要的時間耗費是在建立對象和回收對象上。
我們用下面的代碼運作1W次字元串的連接配接操作,測試String,StringBuffer所運作的時間。
Java代碼
//測試代碼
public class RunTime{
public static void main(String[] args){
● 測試代碼位置1
long beginTime=System.currentTimeMillis();
for(int i=0;i<10000;i++){
● 測試代碼位置2
}
long endTime=System.currentTimeMillis();
System.out.println(endTime-beginTime);
}
}
(1) String常量與String變量的"+"操作比較 ▲測試①代碼: (測試代碼位置1) String str="";
(測試代碼位置2) str="Heart"+"Raid";
[耗時: 0ms]
▲測試②代碼 (測試代碼位置1) String s1="Heart";
String s2="Raid";
String str="";
(測試代碼位置2) str=s1+s2;
[耗時: 15—16ms]
結論:String常量的“+連接配接” 稍優于 String變量的“+連接配接”。
原因:測試①的"Heart"+"Raid"在編譯階段就已經 連接配接起來,形成了一個字元串常量"HeartRaid",并指向堆中的拘留字元串對象。運作時隻需要将"HeartRaid"指向的拘留字元串對象位址取 出1W次,存放在局部變量str中。這确實不需要什麼時間。
測試②中局部變量s1和s2存放的是兩個不同的拘留字元串對象的位址。然後會通過下面三個步驟完成“+連接配接”:
1、StringBuilder temp=new StringBuilder(s1),
2、temp.append(s2);
3、str=temp.toString();
我們發現,雖然在中間的時候也用到了append()方法,但是在開始和結束的時候分别建立了StringBuilder和String對象。可想而知:調用1W次,是不是就建立了1W次這兩種對象呢?不劃算。
但是,String變量的"+連接配接"操作比String常量的"+連接配接"操作使用的更加廣泛。 這一點是不言而喻的。
(2)String對象的"累+"連接配接操作與StringBuffer對象的append()累和連接配接操作比較。▲測試①代碼: (代碼位置1) String s1="Heart";
String s="";
(代碼位置2) s=s+s1;
[耗時: 4200—4500ms]
▲測試②代碼 (代碼位置1) String s1="Heart";
StringBuffer sb=new StringBuffer();
(代碼位置2) sb.append(s1);
[耗時: 0ms(當循環100000次的時候,耗時大概16—31ms)]
結論:大量字元串累加時,StringBuffer的append()效率遠好于String對象的"累+"連接配接
原因:測試① 中的s=s+s1,JVM會利用首先建立一個StringBuilder,并利用append方法完成s和s1所指向的字元串對象值的合并操作,接着調用 StringBuilder的 toString()方法在堆中建立一個新的String對象,其值為剛才字元串的合并結果。而局部變量s指向了新建立的String對象。
因為String對象中的value[]是不能改變的,每一次合并後字元串值都需要建立一個新的String對象來存放。循環1W次自然需要建立1W個String對象和1W個StringBuilder對象,效率低就可想而知了。
測試②中sb.append(s1);隻需要将自己的value[]數組不停的擴大來存放s1即可。循環過程中無需在堆中建立任何新的對象。效率高就不足為奇了。
(3)String對象的"累+"連接配接操作與StringBuffer、StringBuffer
對象的append()累和連接配接操作比較。 不多說 上代碼:package com.wangdi.test;
import junit.framework.TestCase;
/**
* 測試字元串的加
* @author gstarwd
*/
public class TestStringAdd extends TestCase{
public void testAddByString() {
String aa = "gstarwd";
for(int i = 0 ; i <10000000;i++){
aa += "gstar";
}
}
public void testAddbyStringBuffer() {
StringBuffer buffer = new StringBuffer("gstarwd");
for(int i = 0 ; i <10000000;i++){
buffer.append("gstar");
}
}
public void testAddbyStringBuilder() {
StringBuilder builder = new StringBuilder("gstarwd");
for(int i = 0 ; i <10000000;i++){
builder.append("gstar");
}
}
}
不用總結 直接看運作結果:
沒有結果。。。。。。
因為TMD我4核CPU在第一個10million的 String的加上耗費了很多時間 ~最終沒有等他結束我就終止了程式。~
看看占用率:
于是我把第一個String的測試用例循環次數改小了 10000
builder最佳 單線程就用StringBuilder吧~~ 她技巧最好~最忠誠~讓你一個人爽翻天~~~
多線程就用StringBuffer吧~~ 她就是一Bus = =~ 可供多人使用~多P必備~
要是你隻是存取下資料String最常用了 她長得最漂亮~看看足矣!~YY專用~
★
鏡頭總結:(1) 在編譯階段就能夠确定的字元串常量,完全沒有必要建立String或StringBuffer對象。直接使用字元串常量的"+"連接配接操作效率最高。
(2) StringBuffer對象的append效率要高于String對象的"+"連接配接操作。
(3) 不停的建立對象是程式低效的一個重要原因。那麼相同的字元串值能否在堆中隻建立一個String對象那。顯然拘留字元串能夠做到這一點,除了程式中的字元 串常量會被JVM自動建立拘留字元串之外,調用String的intern()方法也能做到這一點。當調用intern()時,如果常量池中已經有了目前 String的值,那麼傳回這個常量指向拘留對象的位址。如果沒有,則将String值加入常量池中,并建立一個新的拘留字元串對象。