StringBuffer 是一個線程安全的可變的字符序列.它繼承於AbstractStringBuilder,實現了CharSequence接口. java
StringBuffer是線程安全的.在字符串拼接性能要比String字符串相加效率高. 程序員
示例:
編程
/** * StringBuffer 演示程序 * */ import java.util.HashMap; public class StringBufferTest { public static void main(String[] args) { testInsertAPIs() ; testAppendAPIs() ; testReplaceAPIs() ; testDeleteAPIs() ; testIndexAPIs() ; testOtherAPIs() ; } /** * StringBuffer 的其它API示例 */ private static void testOtherAPIs() { System.out.println("-------------------------------- testOtherAPIs --------------------------------"); StringBuffer sbuilder = new StringBuffer("0123456789"); int cap = sbuilder.capacity(); System.out.printf("cap=%d\n", cap); char c = sbuilder.charAt(6); System.out.printf("c=%c\n", c); char[] carr = new char[4]; sbuilder.getChars(3, 7, carr, 0); for (int i=0; i<carr.length; i++) System.out.printf("carr[%d]=%c ", i, carr[i]); System.out.println(); System.out.println(); } /** * StringBuffer 中index相關API演示 */ private static void testIndexAPIs() { System.out.println("-------------------------------- testIndexAPIs --------------------------------"); StringBuffer sbuilder = new StringBuffer("abcAbcABCabCaBcAbCaBCabc"); System.out.printf("sbuilder=%s\n", sbuilder); // 1. 從前日後,找出"bc"第一次出現的位置 System.out.printf("%-30s = %d\n", "sbuilder.indexOf(\"bc\")", sbuilder.indexOf("bc")); // 2. 從位置5開始,從前日後,找出"bc"第一次出現的位置 System.out.printf("%-30s = %d\n", "sbuilder.indexOf(\"bc\", 5)", sbuilder.indexOf("bc", 5)); // 3. 從後往前,找出"bc"第一次出現的位置 System.out.printf("%-30s = %d\n", "sbuilder.lastIndexOf(\"bc\")", sbuilder.lastIndexOf("bc")); // 4. 從位置4開始,從後往前,找出"bc"第一次出現的位置 System.out.printf("%-30s = %d\n", "sbuilder.lastIndexOf(\"bc\", 4)", sbuilder.lastIndexOf("bc", 4)); System.out.println(); } /** * StringBuffer 的replace()示例 */ private static void testReplaceAPIs() { System.out.println("-------------------------------- testReplaceAPIs ------------------------------"); StringBuffer sbuilder; sbuilder = new StringBuffer("0123456789"); sbuilder.replace(0, 3, "ABCDE"); System.out.printf("sbuilder=%s\n", sbuilder); sbuilder = new StringBuffer("0123456789"); sbuilder.reverse(); System.out.printf("sbuilder=%s\n", sbuilder); sbuilder = new StringBuffer("0123456789"); sbuilder.setCharAt(0, 'M'); System.out.printf("sbuilder=%s\n", sbuilder); System.out.println(); } /** * StringBuffer 的delete()示例 */ private static void testDeleteAPIs() { System.out.println("-------------------------------- testDeleteAPIs -------------------------------"); StringBuffer sbuilder = new StringBuffer("0123456789"); // 刪除位置0的字符,剩餘字符是「123456789」. sbuilder.deleteCharAt(0); // 刪除位置3(包括)到位置6(不包括)之間的字符,剩餘字符是「123789」. sbuilder.delete(3,6); // 獲取sb中從位置1開始的字符串 String str1 = sbuilder.substring(1); // 獲取sb中從位置3(包括)到位置5(不包括)之間的字符串 String str2 = sbuilder.substring(3, 5); // 獲取sb中從位置3(包括)到位置5(不包括)之間的字符串,獲取的對象是CharSequence對象,此處轉型爲String String str3 = (String)sbuilder.subSequence(3, 5); System.out.printf("sbuilder=%s\nstr1=%s\nstr2=%s\nstr3=%s\n", sbuilder, str1, str2, str3); System.out.println(); } /** * StringBuffer 的insert()示例 */ private static void testInsertAPIs() { System.out.println("-------------------------------- testInsertAPIs -------------------------------"); StringBuffer sbuilder = new StringBuffer(); // 在位置0處插入字符數組 sbuilder.insert(0, new char[]{'a','b','c','d','e'}); // 在位置0處插入字符數組.0表示字符數組起始位置,3表示長度 sbuilder.insert(0, new char[]{'A','B','C','D','E'}, 0, 3); // 在位置0處插入float sbuilder.insert(0, 1.414f); // 在位置0處插入double sbuilder.insert(0, 3.14159d); // 在位置0處插入boolean sbuilder.insert(0, true); // 在位置0處插入char sbuilder.insert(0, '\n'); // 在位置0處插入int sbuilder.insert(0, 100); // 在位置0處插入long sbuilder.insert(0, 12345L); // 在位置0處插入StringBuilder對象 sbuilder.insert(0, new StringBuffer("StringBuilder")); // 在位置0處插入StringBuilder對象.6表示被在位置0處插入對象的起始位置(包括),13是結束位置(不包括) sbuilder.insert(0, new StringBuffer("STRINGBUILDER"), 6, 13); // 在位置0處插入StringBuffer對象. sbuilder.insert(0, new StringBuffer("StringBuffer")); // 在位置0處插入StringBuffer對象.6表示被在位置0處插入對象的起始位置(包括),12是結束位置(不包括) sbuilder.insert(0, new StringBuffer("STRINGBUFFER"), 6, 12); // 在位置0處插入String對象. sbuilder.insert(0, "String"); // 在位置0處插入String對象.1表示被在位置0處插入對象的起始位置(包括),6是結束位置(不包括) sbuilder.insert(0, "0123456789", 1, 6); sbuilder.insert(0, '\n'); // 在位置0處插入Object對象.此處以HashMap爲例 HashMap map = new HashMap(); map.put("1", "one"); map.put("2", "two"); map.put("3", "three"); sbuilder.insert(0, map); System.out.printf("%s\n\n", sbuilder); } /** * StringBuffer 的append()示例 */ private static void testAppendAPIs() { System.out.println("-------------------------------- testAppendAPIs -------------------------------"); StringBuffer sbuilder = new StringBuffer(); // 追加字符數組 sbuilder.append(new char[]{'a','b','c','d','e'}); // 追加字符數組.0表示字符數組起始位置,3表示長度 sbuilder.append(new char[]{'A','B','C','D','E'}, 0, 3); // 追加float sbuilder.append(1.414f); // 追加double sbuilder.append(3.14159d); // 追加boolean sbuilder.append(true); // 追加char sbuilder.append('\n'); // 追加int sbuilder.append(100); // 追加long sbuilder.append(12345L); // 追加StringBuilder對象 sbuilder.append(new StringBuffer("StringBuilder")); // 追加StringBuilder對象.6表示被追加對象的起始位置(包括),13是結束位置(不包括) sbuilder.append(new StringBuffer("STRINGBUILDER"), 6, 13); // 追加StringBuffer對象. sbuilder.append(new StringBuffer("StringBuffer")); // 追加StringBuffer對象.6表示被追加對象的起始位置(包括),12是結束位置(不包括) sbuilder.append(new StringBuffer("STRINGBUFFER"), 6, 12); // 追加String對象. sbuilder.append("String"); // 追加String對象.1表示被追加對象的起始位置(包括),6是結束位置(不包括) sbuilder.append("0123456789", 1, 6); sbuilder.append('\n'); // 追加Object對象.此處以HashMap爲例 HashMap map = new HashMap(); map.put("1", "one"); map.put("2", "two"); map.put("3", "three"); sbuilder.append(map); sbuilder.append('\n'); // 追加unicode編碼 sbuilder.appendCodePoint(0x5b57); // 0x5b57是「字」的unicode編碼 sbuilder.appendCodePoint(0x7b26); // 0x7b26是「符」的unicode編碼 sbuilder.appendCodePoint(0x7f16); // 0x7f16是「編」的unicode編碼 sbuilder.appendCodePoint(0x7801); // 0x7801是「碼」的unicode編碼 System.out.printf("%s\n\n", sbuilder); } }
運行結果爲 : 數組
-------------------------------- testInsertAPIs ------------------------------- {3=three, 2=two, 1=one} 12345StringBUFFERStringBufferBUILDERStringBuilder12345100 true3.141591.414ABCabcde -------------------------------- testAppendAPIs ------------------------------- abcdeABC1.4143.14159true 10012345StringBuilderBUILDERStringBufferBUFFERString12345 {3=three, 2=two, 1=one} 字符編碼 -------------------------------- testReplaceAPIs ------------------------------ sbuilder=ABCDE3456789 sbuilder=9876543210 sbuilder=M123456789 -------------------------------- testDeleteAPIs ------------------------------- sbuilder=123789 str1=23789 str2=78 str3=78 -------------------------------- testIndexAPIs -------------------------------- sbuilder=abcAbcABCabCaBcAbCaBCabc sbuilder.indexOf("bc") = 1 sbuilder.indexOf("bc", 5) = 22 sbuilder.lastIndexOf("bc") = 22 sbuilder.lastIndexOf("bc", 4) = 4 -------------------------------- testOtherAPIs -------------------------------- cap=26 c=6 carr[0]=3 carr[1]=4 carr[2]=5 carr[3]=6
方式一:利用構造器:String s=new String("Hello world") 安全
方式二:直接建立:String s="Hello world" 數據結構
①Java class文件結構和常量池 多線程
咱們都知道,Java程序要運行,首先須要編譯器將源代碼文件編譯成字節碼文件(也就是.class文件).而後在由JVM解釋執行. app
class文件是8位字節的二進制流.這些二進制流的涵義由一些緊湊的有意義的項組成.好比class字節流中最開始的4個字節組成的項叫作魔數 (magic),其意義在於分辨class文件(值爲0xCAFEBABE)與非class文件.其中,在class文件中常量池:專門放置源代碼中的符號信息(而且不一樣的符號信息放置在不一樣標誌的常量表中). eclipse
②JVM運行class文件 性能
源代碼編譯成class文件以後,JVM就要運行這個class文件.它首先會用類裝載器加載進class文件.而後須要建立許多內存數據結構來存放 class文件中的字節數據.好比class文件對應的類信息數據、常量池結構、方法中的二進制指令序列、類方法與字段的描述信息等等.固然,在運行的時候,還須要爲方法建立棧幀等.這麼多的內存結構固然須要管理,JVM會把這些東西都組織到幾個「運行時數據區」中.這裏面就有咱們常常說的「方法區 」、「堆 」、「Java棧 」等.
上面咱們提到了,在Java源代碼中的每個字面值字符串,都會在編譯成class文件階段,造成標誌號爲8(CONSTANT_String_info)的常量表. 當JVM加載 class文件的時候,會爲對應的常量池創建一個內存數據結構,並存放在方法區中.同時JVM會自動爲CONSTANT_String_info常量表中的字符串常量字面值 在堆中建立新的String對象(intern字符串對象,又叫拘留字符串對象).而後把CONSTANT_String_info常量表的入口地址轉變成這個堆中String對象的直接地址(常量池解析).
這裏很關鍵的就是這個拘留字符串對象.源代碼中全部相同字面值的字符串常量只可能創建惟一一個拘留字符串對象. 實際上JVM是經過一個記錄了拘留字符串引用的內部數據結構來維持這一特性的.在Java程序中,能夠調用String的intern()方法來使得一個常規字符串對象成爲拘留字符串對象.咱們會在後面介紹這個方法的.
有了上面闡述的兩個知識前提,下面咱們將根據二進制指令來區別兩種字符串對象的建立方式:
①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個,你們好好想一想爲何吧!
②將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 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"指向的堆的惟一的那個拘留字符串對象的地址 .天然相等了 .
示例:
//代碼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 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[]都用於存儲字符序列 .可是 ,
① String中的是常量(final)數組,只能被賦值一次.
好比:new String("abc")使得value[]={'a','b','c'},以後這個String對象中的value[]不再能改變了.這也正是你們常說的,String是不可變的緣由 .
注意:這個對初學者來講有個誤區,有人說String str1=new String("abc"); str1=new String("cba");不是改變了字符串str1嗎?那麼你有必要先搞懂對象引用和對象自己的區別.這裏我簡單的說明一下,對象自己指的是存放在堆空間中的該對象的實例數據(非靜態很是量字段).而對象引用指的是堆中對象自己所存放的地址,通常方法區和Java棧中存儲的都是對象引用,而非對象自己的數據.
② 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容許多線程進行字符操做.這是由於在源代碼中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對象只能讀取,沒法修改.試問:還有什麼不安全的呢?
首先說明一點:StringBuffer和StringBuilder可謂雙胞胎,StringBuilder是1.5新引入的,其前身就是 StringBuffer.StringBuilder的效率比StringBuffer稍高,若是不考慮線程安全,StringBuilder應該是首選.另外,JVM運行程序主要的時間耗費是在建立對象和回收對象上.
咱們用下面的代碼運行1W次字符串的鏈接操做,測試String,StringBuffer所運行的時間.
示例:
//測試代碼 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
要是你只是存取下數據String最經常使用
總結:
①在編譯階段就可以肯定的字符串常量,徹底沒有必要建立String或StringBuffer對象.直接使用字符串常量的"+"鏈接操做效率最高.
② StringBuffer對象的append效率要高於String對象的"+"鏈接操做.
③ 不停的建立對象是程序低效的一個重要緣由.那麼相同的字符串值可否在堆中只建立一個String對象那.顯然拘留字符串可以作到這一點,除了程序中的字符 串常量會被JVM自動建立拘留字符串以外,調用String的intern()方法也能作到這一點.當調用intern()時,若是常量池中已經有了當前 String的值,那麼返回這個常量指向拘留對象的地址.若是沒有,則將String值加入常量池中,並建立一個新的拘留字符串對象.
參考資料:
http://hui-jing-880210.iteye.com/blog/2173186
http://leowzy.iteye.com/blog/804594
20150424
JAVA學習筆記系列
--------------------------------------------
聯繫方式
--------------------------------------------
Weibo: ARESXIONG
E-Mail: aresxdy@gmail.com
------------------------------------------------