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
------------------------------------------------