天天看點

java-基礎-String、StringBuilder以及StringBuffer剖析 String源碼分析

從一段代碼說起:

大家猜一猜結果如何?如果你的結論是true。好吧,再來一段代碼:

結果如何呢?正确答案是false。

讓我們看看經過編譯器編譯後的代碼如何

也就是說第一段代碼經過了編譯期優化,原因是編譯器發現"a"+"b"+1和"ab1"的效果是一樣的,都是不可變量組成。但是為什麼他們的記憶體位址會相同呢?如果你對此還有興趣,那就一起看看String類的一些重要源碼吧。

一 String類

String類被final所修飾,也就是說String對象是不可變量,并發程式最喜歡不可變量了。String類實作了Serializable, Comparable, CharSequence接口。

Comparable接口有compareTo(String s)方法,CharSequence接口有length(),charAt(int index),subSequence(int start,int end)方法。

二 String屬性

String類中包含一個不可變的char數組用來存放字元串,一個int型的變量hash用來存放計算後的哈希值。

三 String構造函數

三 String常用方法

equals方法經常用得到,它用來判斷兩個對象從實際意義上是否相等,String對象判斷規則:

記憶體位址相同,則為真。

如果對象類型不是String類型,則為假。否則繼續判斷。

如果對象長度不相等,則為假。否則繼續判斷。

從後往前,判斷String類中char數組value的單個字元是否相等,有不相等則為假。如果一直相等直到第一個數,則傳回真。

由此可以看出,如果對兩個超長的字元串進行比較還是非常費時間的。

這個方法寫的很巧妙,先從0開始判斷字元大小。如果兩個對象能比較字元的地方比較完了還相等,就直接傳回自身長度減被比較對象長度,如果兩個字元串長度相等,則傳回的是0,巧妙地判斷了三種情況。

String類重寫了hashCode方法,Object中的hashCode方法是一個Native調用。String類的hash采用多項式計算得來,我們完全可以通過不相同的字元串得出同樣的hash,是以兩個String對象的hashCode相同,并不代表兩個String是一樣的。

起始比較和末尾比較都是比較經常用得到的方法,例如在判斷一個字元串是不是http協定的,或者初步判斷一個檔案是不是mp3檔案,都可以采用這個方法進行比較。

concat方法也是經常用的方法之一,它先判斷被添加字元串是否為空來決定要不要建立新的對象。

這個方法也有讨巧的地方,例如最開始先找出舊值出現的位置,這樣節省了一部分對比的時間。replace(String oldStr,String newStr)方法通過正規表達式來判斷。

trim方法用起來也6的飛起

intern方法是Native調用,它的作用是在方法區中的常量池裡通過equals方法尋找等值的對象,如果沒有找到則在常量池中開辟一片空間存放字元串并傳回該對應String的引用,否則直接傳回常量池中已存在String對象的引用。

将引言中第二段代碼

則結果為為真,原因在于a所指向的位址來自于常量池,而b所指向的字元串常量預設會調用這個方法,是以a和b都指向了同一個位址空間。

在JDK1.7中,Hash相關集合類在String類作key的情況下,不再使用hashCode方式離散資料,而是采用hash32方法。這個方法預設使用系統目前時間,String類位址,System類位址等作為因子計算得到hash種子,通過hash種子在經過hash得到32位的int型數值。

以上是一些簡單的常用方法。

總結

String對象是不可變類型,傳回類型為String的String方法每次傳回的都是新的String對象,除了某些方法的某些特定條件傳回自身。

String對象的三種比較方式:

==記憶體比較:直接對比兩個引用所指向的記憶體值,精确簡潔直接明了。

equals字元串值比較:比較兩個引用所指對象字面值是否相等。

hashCode字元串數值化比較:将字元串數值化。兩個引用的hashCode相同,不保證記憶體一定相同,不保證字面值一定相同。

String str="hello world"和String str=new String("hello world")的差別

<code>public</code> <code>class</code> <code>Main {</code>

<code>        </code> 

<code>    </code><code>public</code> <code>static</code> <code>void</code> <code>main(String[] args) {</code>

<code>        </code><code>String str1 = </code><code>"hello world"</code><code>;</code>

<code>        </code><code>String str2 = </code><code>new</code> <code>String(</code><code>"hello world"</code><code>);</code>

<code>        </code><code>String str3 = </code><code>"hello world"</code><code>;</code>

<code>        </code><code>String str4 = </code><code>new</code> <code>String(</code><code>"hello world"</code><code>);</code>

<code>        </code><code>System.out.println(str1==str2);</code>

<code>        </code><code>System.out.println(str1==str3);</code>

<code>        </code><code>System.out.println(str2==str4);</code>

<code>    </code><code>}</code>

<code>}</code>

在class檔案中有一部分 來存儲編譯期間生成的 字面常量以及符号引用,這部分叫做class檔案常量池,在運作期間對應着方法區的運作時常量池。是以在上述代碼中,String str1 = "hello world";和String str3 = "hello world"; 都在編譯期間生成了 字面常量和符号引用,運作期間字面常量"hello world"被存儲在運作時常量池(當然隻儲存了一份)。通過這種方式來将String對象跟引用綁定的話,JVM執行引擎會先在運作時常量池查找是否存在相同的字面常量,如果存在,則直接将引用指向已經存在的字面常量;否則在運作時常量池開辟一個空間來存儲該字面常量,并将引用指向該字面常量。new關鍵字來生成對象是在堆區進行的,而在堆區進行對象生成的過程是不會去檢測該對象是否已經存在的。是以通過new來建立對象,建立出的一定是不同的對象,即使字元串的内容是相同的.

String、StringBuffer以及StringBuilder的差別

<code>        </code><code>String string = </code><code>""</code><code>;</code>

<code>        </code><code>for</code><code>(</code><code>int</code> <code>i=</code><code>0</code><code>;i&lt;</code><code>10000</code><code>;i++){</code>

<code>            </code><code>string += </code><code>"hello"</code><code>;</code>

<code>        </code><code>}</code>

這句 string += "hello";的過程相當于将原有的string變量指向的對象内容取出與"hello"作字元串相加操作再存進另一個新的String對象當中,再讓string變量指向新生成的對象。如果大家還有疑問可以反編譯其位元組碼檔案便清楚了:

  

java-基礎-String、StringBuilder以及StringBuffer剖析 String源碼分析

  從這段反編譯出的位元組碼檔案可以很清楚地看出:從第8行開始到第35行是整個循環的執行過程,并且每次循環會new出一個StringBuilder對象,然後進行append操作,最後通過toString方法傳回String對象。也就是說這個循環執行完畢new出了10000個對象,試想一下,如果這些對象沒有被回收,會造成多大的記憶體資源浪費。從上面還可以看出:string+="hello"的操作事實上會自動被JVM優化成:

  StringBuilder str = new StringBuilder(string);

  str.append("hello");

  str.toString();

  再看下面這段代碼:

1

2

3

4

5

6

7

8

9

<code>        </code><code>StringBuilder stringBuilder = </code><code>new</code> <code>StringBuilder();</code>

<code>            </code><code>stringBuilder.append(</code><code>"hello"</code><code>);</code>

  反編譯位元組碼檔案得到:

java-基礎-String、StringBuilder以及StringBuffer剖析 String源碼分析

  從這裡可以明顯看出,這段代碼的for循環式從13行開始到27行結束,并且new操作隻進行了一次,也就是說隻生成了一個對象,append操作是在原有對象的基礎上進行的。是以在循環了10000次之後,這段代碼所占的資源要比上面小得多。

事實上,StringBuilder和StringBuffer類擁有的成員屬性以及成員方法基本相同,差別是StringBuffer類的成員方法前面多了一個關鍵字:synchronized,不用多說,這個關鍵字是在多線程通路時起到安全保護作用的,也就是說StringBuffer是線程安全的。

<code>public</code> <code>StringBuilder insert(</code><code>int</code> <code>index, </code><code>char</code> <code>str[], </code><code>int</code> <code>offset,</code>

<code>                              </code><code>int</code> <code>len)</code>

<code>  </code><code>{</code>

<code>      </code><code>super</code><code>.insert(index, str, offset, len);</code>

<code>  </code><code>return</code> <code>this</code><code>;</code>

<code>  </code><code>}</code>

<code>public</code> <code>synchronized</code> <code>StringBuffer insert(</code><code>int</code> <code>index, </code><code>char</code> <code>str[], </code><code>int</code> <code>offset,</code>

<code>                                            </code><code>int</code> <code>len)</code>

<code>    </code><code>{</code>

<code>        </code><code>super</code><code>.insert(index, str, offset, len);</code>

<code>        </code><code>return</code> <code>this</code><code>;</code>

<code>    </code>

<code>    </code><code>private</code> <code>static</code> <code>int</code> <code>time = </code><code>50000</code><code>;</code>

<code>        </code><code>testString();</code>

<code>        </code><code>testStringBuffer();</code>

<code>        </code><code>testStringBuilder();</code>

<code>        </code><code>test1String();</code>

<code>        </code><code>test2String();</code>

<code>    </code> 

<code>    </code><code>public</code> <code>static</code> <code>void</code> <code>testString () {</code>

<code>        </code><code>String s=</code><code>""</code><code>;</code>

<code>        </code><code>long</code> <code>begin = System.currentTimeMillis();</code>

<code>        </code><code>for</code><code>(</code><code>int</code> <code>i=</code><code>0</code><code>; i&lt;time; i++){</code>

<code>            </code><code>s += </code><code>"java"</code><code>;</code>

<code>        </code><code>long</code> <code>over = System.currentTimeMillis();</code>

<code>        </code><code>System.out.println(</code><code>"操作"</code><code>+s.getClass().getName()+</code><code>"類型使用的時間為:"</code><code>+(over-begin)+</code><code>"毫秒"</code><code>);</code>

<code>    </code><code>public</code> <code>static</code> <code>void</code> <code>testStringBuffer () {</code>

<code>        </code><code>StringBuffer sb = </code><code>new</code> <code>StringBuffer();</code>

<code>            </code><code>sb.append(</code><code>"java"</code><code>);</code>

<code>        </code><code>System.out.println(</code><code>"操作"</code><code>+sb.getClass().getName()+</code><code>"類型使用的時間為:"</code><code>+(over-begin)+</code><code>"毫秒"</code><code>);</code>

<code>    </code><code>public</code> <code>static</code> <code>void</code> <code>testStringBuilder () {</code>

<code>        </code><code>StringBuilder sb = </code><code>new</code> <code>StringBuilder();</code>

<code>    </code><code>public</code> <code>static</code> <code>void</code> <code>test1String () {</code>

<code>            </code><code>String s = </code><code>"I"</code><code>+</code><code>"love"</code><code>+</code><code>"java"</code><code>;</code>

<code>        </code><code>System.out.println(</code><code>"字元串直接相加操作:"</code><code>+(over-begin)+</code><code>"毫秒"</code><code>);</code>

<code>    </code><code>public</code> <code>static</code> <code>void</code> <code>test2String () {</code>

<code>        </code><code>String s1 =</code><code>"I"</code><code>;</code>

<code>        </code><code>String s2 = </code><code>"love"</code><code>;</code>

<code>        </code><code>String s3 = </code><code>"java"</code><code>;</code>

<code>            </code><code>String s = s1+s2+s3;</code>

<code>        </code><code>System.out.println(</code><code>"字元串間接相加操作:"</code><code>+(over-begin)+</code><code>"毫秒"</code><code>);</code>

<code>        </code><code>testOptimalString();</code>

<code>    </code><code>public</code> <code>static</code> <code>void</code> <code>testOptimalString () {</code>

<code>            </code><code>StringBuilder sb = </code><code>new</code> <code>StringBuilder(s);</code>

<code>            </code><code>s=sb.toString();</code>

<code>        </code><code>System.out.println(</code><code>"模拟JVM優化操作的時間為:"</code><code>+(over-begin)+</code><code>"毫秒"</code><code>);</code>

 1)對于直接相加字元串,效率很高,因為在編譯器便确定了它的值,也就是說形如"I"+"love"+"java"; 的字元串相加,在編譯期間便被優化成了"Ilovejava"。這個可以用javap -c指令反編譯生成的class檔案進行驗證。

  對于間接相加(即包含字元串引用),形如s1+s2+s3; 效率要比直接相加低,因為在編譯器不會對引用變量進行優化。

  2)String、StringBuilder、StringBuffer三者的執行效率:

  StringBuilder &gt; StringBuffer &gt; String

  當然這個是相對的,不一定在所有情況下都是這樣。

  比如String str = "hello"+ "world"的效率就比 StringBuilder st  = new StringBuilder().append("hello").append("world")要高。

  是以,這三個類是各有利弊,應當根據不同的情況來進行選擇使用:

  當字元串相加操作或者改動較少的情況下,建議使用 String str="hello"這種形式;

  當字元串相加操作較多的情況下,建議使用StringBuilder,如果采用了多線程,則使用StringBuffer。

String a = "hello2";   

String b = "hello" + 2;   

System.out.println((a == b));

輸出結果為:true。原因很簡單,"hello"+2在編譯期間就已經被優化成"hello2",是以在運作期間,變量a和變量b指向的是同一個對象

 String a = "hello2";    

String b = "hello";       

String c = b + 2;       

System.out.println((a == c));

  輸出結果為:false。由于有符号引用的存在,是以  String c = b + 2;不會在編譯期間被優化,不會把b+2當做字面常量來處理的,是以這種方式生成的對象事實上是儲存在堆上的。是以a和c指向的并不是同一個對象。

String a = "hello2";     

final String b = "hello";      

 String c = b + 2;      

 System.out.println((a == c));

  輸出結果為:true。對于被final修飾的變量,會在class檔案常量池中儲存一個副本,也就是說不會通過連接配接而進行通路,對final變量的通路在編譯期間都會直接被替代為真實的值。那麼String c = b + 2;在編譯期間就會被優化成:String c = "hello" + 2; 

<code>        </code><code>String a = </code><code>"hello2"</code><code>;</code>

<code>        </code><code>final</code> <code>String b = getHello();</code>

<code>        </code><code>String c = b + </code><code>2</code><code>;</code>

<code>        </code><code>System.out.println((a == c));</code>

<code>    </code><code>public</code> <code>static</code> <code>String getHello() {</code>

<code>        </code><code>return</code> <code>"hello"</code><code>;</code>

輸出結果為false。這裡面雖然将b用final修飾了,但是由于其指派是通過方法調用傳回的,那麼它的值隻能在運作期間确定,是以a和c指向的不是同一個對象。

<code>        </code><code>String a = </code><code>"hello"</code><code>;</code>

<code>        </code><code>String b =  </code><code>new</code> <code>String(</code><code>"hello"</code><code>);</code>

<code>        </code><code>String c =  </code><code>new</code> <code>String(</code><code>"hello"</code><code>);</code>

<code>        </code><code>String d = b.intern();</code>

<code>        </code><code>System.out.println(a==b);</code>

<code>        </code><code>System.out.println(b==c);</code>

<code>        </code><code>System.out.println(b==d);</code>

<code>        </code><code>System.out.println(a==d);</code>

String.intern方法的使用。在String類中,intern方法是一個本地方法,在JAVA SE6之前,intern方法會在運作時常量池中查找是否存在内容相同的字元串,如果存在則傳回指向該字元串的引用,如果不存在,則會将該字元串入池,并傳回一個指向該字元串的引用。是以,a和d指向的是同一個對象。

String str = new String("abc")建立了多少個對象?

這個問題在很多書籍上都有說到比如《Java程式員面試寶典》,包括很多國内大公司筆試面試題都會遇到,大部分網上流傳的以及一些面試書籍上都說是2個對象,這種說法是片面的。

首先必須弄清楚建立對象的含義,建立是什麼時候建立的?這段代碼在運作期間會建立2個對象麼?毫無疑問不可能,用javap -c反編譯即可得到JVM執行的位元組碼内容:首先必須弄清楚建立對象的含義,建立是什麼時候建立的?這段代碼在運作期間會建立2個對象麼?毫無疑問不可能,用javap

-c反編譯即可得到JVM執行的位元組碼内容:

java-基礎-String、StringBuilder以及StringBuffer剖析 String源碼分析

而這道題目讓人混淆的地方就是這裡,這段代碼在運作期間确實隻建立了一個對象,即在堆上建立了"abc"對象。而為什麼大家都在說是2個對象呢,這裡面要澄清一個概念  該段代碼執行過程和類的加載過程是有差別的。在類加載的過程中,确實在運作時常量池中建立了一個"abc"對象,而在代碼執行過程中确實隻建立了一個String對象。

  是以,這個問題如果換成 String str = new String("abc")涉及到幾個String對象?合理的解釋是2個

<code>        </code><code>String str1 = </code><code>"I"</code><code>;</code>

<code>        </code><code>//str1 += "love"+"java";        1)</code>

<code>        </code><code>str1 = str1+</code><code>"love"</code><code>+</code><code>"java"</code><code>;      </code><code>//2)</code>

1)的效率比2)的效率要高,1)中的"love"+"java"在編譯期間會被優化成"lovejava",而2)中的不會被優化。

可以看出,在1)中隻進行了一次append操作,而在2)中進行了兩次append操作。