做者:每次上網衝杯Java時,都能看到關於String無休無止的爭論。仍是以爲有必要讓這個討厭又很可愛的String美眉,赤裸裸的站在咱們這些Java色狼面前了。嘿嘿....
衆所周知,String是由字符組成的串,在程序中使用頻率很高。Java中的String是一個類,而並不是基本數據類型。 不過她卻不是普通的類哦!!!java
【鏡頭1】 String對象的建立
一、關於類對象的建立,很普通的一種方式就是利用構造器,String類也不例外:String s=new String("Hello world"); 問題是參數"Hello world"是什麼東西,也是字符串對象嗎?莫非用字符串對象建立一個字符串對象?
二、固然,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文件內容及常量池 》編程
經過上圖可見,代碼中的"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()方法來使得一個常規字符串對象成爲拘留字符串對象。咱們會在後面介紹這個方法的。app
★ 操做碼助憶符指令
有了上面闡述的兩個知識前提,下面咱們將根據二進制指令來區別兩種字符串對象的建立方式: eclipse
(1) String s=new String("Hello world");編譯成class文件後的指令(在myeclipse中查看):
注意:
【這裏有個dup指令。其做用就是複製以前分配的Java.lang.String空間的引用並壓入棧頂。那麼這裏爲何須要這樣麼作呢?由於invokespecial指令經過[15]這個常量池入口尋找到了java.lang.String()構造方法,構造方法雖然找到了。可是必須還得知道是誰的構造方法,因此要將以前分配的空間的應用壓入棧頂讓invokespecial命令應用才知道原來這個構造方法是剛纔建立的那個引用的,調用完成以後將棧頂的值彈出。以後調用astore_1將此時的棧頂值彈出存入局部變量中去。】
事實上,在運行這段指令以前,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文件後的指令:
和上面的建立指令有很大的不一樣,局部變量s存儲的是早已建立好的拘留字符串的堆地址(沒有new 的對象了)。 你們好好想一想,若是還有一條穿件語句String s1="Hello word";此時堆中有幾個值爲"Hello world"的字符串呢?答案是1個。那麼局部變量s與s1存儲的地址是否相同呢? 呵呵, 這個你應該知道了吧。
★ 鏡頭總結: String類型脫光了其實也很普通。真正讓她神祕的緣由就在於CONSTANT_String_info常量表 和拘留字符串對象 的存在。如今咱們能夠解決江湖上的許多紛爭了。
【 紛爭1】關於字符串相等關係的爭論
代碼1中局部變量sa,sb中存儲的是JVM在堆中new出來的兩個String對象的內存地址。雖然這兩個String對象的值(char[]存放的字符序列)都是"Hello world"。 所以"=="比較的是兩個不一樣的堆地址。代碼2中局部變量sc,sd中存儲的也是地址,但卻都是常量池中"Hello world"指向的堆的惟一的那個拘留字符串對象的地址 。天然相等了。
【紛爭2】 字符串「+」操做的內幕
代碼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的可變性問題。
咱們先看看這兩個類的部分源代碼:
很顯然,String和StringBuffer中的value[]都用於存儲字符序列。可是,
(1) String中的是常量(final)數組,只能被賦值一次。
好比:new String("abc")使得value[]={'a','b','c'}(查看jdk String 就是這麼實現的),以後這個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所運行的時間。
(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存放的是兩個不一樣的拘留字符串對象的地址。而後會經過下面三個步驟完成「+鏈接」:
一、StringBuilder temp=new StringBuilder(s1),
二、temp.append(s2);
三、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便可。循環過程當中無需在堆中建立任何新的對象。效率高就不足爲奇了。
★ 鏡頭總結:
(1) 在編譯階段就可以肯定的字符串常量,徹底沒有必要建立String或StringBuffer對象。直接使用字符串常量的"+"鏈接操做效率最高。
(2) StringBuffer對象的append效率要高於String對象的"+"鏈接操做。
(3) 不停的建立對象是程序低效的一個重要緣由。那麼相同的字符串值可否在堆中只建立一個String對象那。顯然拘留字符串可以作到這一點,除了程序中的字符串常量會被JVM自動建立拘留字符串以外,調用String的intern()方法也能作到這一點。當調用intern()時,若是常量池中已經有了當前String的值,那麼返回這個常量指向拘留對象的地址。若是沒有,則將String值加入常量池中,並建立一個新的拘留字符串對象。