Java中String、StringBuffer、StringBuilder的比较与源代码分析
众所周知String、StringBuffer、StringBuilder是java中常用的字符串类,下面我将从三个方面对他们三兄弟进行对比。
一、三者的数据组织及其功能实现
大家爱把String、StringBuffer、StringBuilder叫做三兄弟,经过分析代码发现说他俩三兄弟有点不太贴切,从组织结构上说,StringBuffer、StringBuilder更像是亲兄弟,这哥俩儿都有一个妈----AbstractStringBuilder,即都是从AbstractStringBuilder继承下来的,并且大部分“器官”长得都非常像,而String更像是他俩的表哥,组织结构有相似之处,但不完全一样,下面就让我从源代码开始分析一下这哥三。
1、三者核心代码比较(以上源代码均来自jdk1.7,且有删减)
String:
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence { /** The value is used for character storage. */ private final char value[]; /** Cache the hash code for the string */ public String() { this.value = new char[0]; } public String(String original) { this.value = original.value; this.hash = original.hash; }public String(char value[]) {
this.value = Arrays.copyOf(value, value.length); }//主要初始化功能
}
StringBuild:
public final class StringBuilder
extends AbstractStringBuilder implements java.io.Serializable, CharSequence{ public StringBuilder() { super(16); } public StringBuilder(int capacity) { super(capacity); } public StringBuilder(String str) { super(str.length() + 16); append(str); } public StringBuilder(CharSequence seq) { this(seq.length() + 16); append(seq); }StringBuffer:
public final class StringBuffer
extends AbstractStringBuilder implements java.io.Serializable, CharSequence{ public StringBuffer() { super(16); } public StringBuffer(int capacity) { super(capacity); } public StringBuffer(String str) { super(str.length() + 16); append(str); } public StringBuffer(CharSequence seq) { this(seq.length() + 16); append(seq); }
发现这兄弟俩继承了AbstractStringBuilder类,所以我们很有必要请出这位“母亲”AbstractStringBuilder抽象类:
AbstractStringBuilder:
abstract class AbstractStringBuilder implements Appendable, CharSequence {
char[] value; int count; AbstractStringBuilder() { } AbstractStringBuilder(int capacity) { value = new char[capacity]; }
2、String和StringBuilder的主要区别
从以上代码可以看到,这哥三都是以字符数组作为储存字符串的容器,而最主要的差别在于String比剩下兄弟俩多一个关键字final。什么是final?Final就像现实中的公章一样,一旦被盖上,那就改不了喽!final可以作用在很多东西上,比如类要是用了final,该类就被锁了起来,就不能被继承了。而变量要是被final“盖”上了的话,那里面的内容就不能被修改了,所以我们可以看到String中的value是final的,是不可修改的。也就是说当我们执行:
String str=new String(“123”);
这条语句在java内部是将String类中的value[]={‘1’,’2’,’3’};而这个value是不可修改的。
那么有的有人会说,要是我接下来做这个操作:
str = new String(“456”);
不就更改了str的值吗?这里要搞清楚不可修改的意思是内存中的内容不可修改,这条语句执行后,其实并没有修改堆中“123”处内存的值,修改的只是str指针的内容。
而反观StringBuilder和StringBuffer兄弟俩的value值就没有final修饰,换句话说,这俩的字符串值是可以修改的,从代码中也可以看出这两个类中都有方法append(增加字符串)、insert(动态插入)等可以改变字符串的方法。使用这些方法时都没有重新生成对象,而是在原有对象的基础上进行操作,而String就不同,他的所有方法都是对串自身进行引用,没有修改自身的方法,也可以说,所有对String修改的代码其本质都是创建了不同的对象。下面举个例子我们可以用一下代码轻松修改str1的值:
StringBuffer str1 = new StringBuffer("hello world");
str1.append(" I am sk");
(其中append方法底层又调用了String类的方法getChars进行字符数组的赋值操作,最最底层实现由arrayCopy来完成。。。不禁要说贵圈真乱)
3、StringBuffer和StringBuilder的区别
上面说了String和这兄弟俩的区别主要是能不能更改,那么StringBuffer和StringBuilder又有什么区别呢?答案就在关键字synchronized上,可以看到,这兄弟俩的大部分代码都是相似的,唯一不同的是StringBuffer在每一个方法上多了一把“锁”---synchronized。简单来讲就是线程安全。
具体来说,所有被synchronized“锁”住的类或代码块都有以下性质:
当有两个或以上线程想要同时访问synchronized代码块资源时,同一时间只能有一个线程执行,另外的线程想要访问必须排队,等待当时线程使用完毕之后再访问。也就是说当一个线程访问该资源时,就将它“锁住”该资源(也叫独占锁,只有独占锁的线程才能获取代码块资源)使其他线程不能使用。
那么线程安全有什么用呢?你想啊,由于StringBuilder和StringBuffer兄弟俩的内容可以修改,那么如果有多个线程都想要更改字符串的内容,若是没有线程锁的话那么原子操作可能被分成几部分执行,导致各个线程之间相互影响,得到错误操作。
综上所述,String字符串是不可修改的(当然也是线程安全的),StringBuilder字符串和StringBuffer都是可以修改,其中StringBuffer又是线程安全的,StringBuilder不能保证线程安全。
二、设计原因及影响
对于java这么优秀的语言来说,设计这三种形式的字符串类型肯定不是空穴来风。
对于String,首先的问题是为什么要设计成不可变的,不可变的多麻烦啊,直接都弄成可以随意改变的,又灵活又方便。事实上不是这样的,我认为String设计成不可变的有以下几个好处:
1、首先,将String设置为不可变的很大程度上保证了安全性,在java中,字符串是最常用的数据传递方式,有些数据是私密的,比如数据库的密码,等等。这些私密的数据都是字符串,而正是字符串的不可变性保证了数据传递的安全。
2、其次,String设计成不可更改的是为了字符串常量池的实现,字符串常量池就是在编译class文件的阶段,将代码中的字符串形成常量表(拘留字符串对象)放在堆中(jdk1.7以上),这样下次使用String 创建对象或字符串的时候就会先到常量池中寻找有没有相同的拘留字符串对象,这样大量节省了创建时间。而常量池的就是基于字符串不可变来完成的举个例子:
String str0=“hello world”;
String str1=“hello world”;
String str2=“hello world”;
现在有这三个代码,此时堆中有四个值为“hello world”的字符串对象,一个在常量池中,还有三个对象是指向常量池中的字符串。
如果String类是可修改的话,那么万一常量池中的字符串被修改了,那么所有的由他创建的对象都要修改,那样会造成程序混乱,或者任意一个strx修改了值,那么常量池中的值也要修改,也会造成混乱。所以,String不可改是常量池技术实现的前提。
3、最后,String不可修改也保证了线程安全。
综上所述。String被设计成不可变是很有必要的。
对于SringBuffer和StringBuilder来说,他们被设计成可变的字符串是因为当我们想要对字符串修改而又不想创建新的对象时就变的快速而又方便了。
而接下来就该说明设计对着三者的影响了,最大的影响当然是执行速度,为了比较其修改字符串时的速度(因为比较建立字符串对象的速度没有什么意义),我写了如下测试代码来比较:
public class StringCalssTest {
public static void main(String[] args) { String str1=""; StringBuffer str2=new StringBuffer(); StringBuilder str3=new StringBuilder(); long beginTime=System.currentTimeMillis(); for(int i=0;i<10000;i++) { str1 = str1+"hello sk"; //String str5=new String(); } long endTime=System.currentTimeMillis(); System.out.printf("String执行速度:%d\n",endTime-beginTime); beginTime=System.currentTimeMillis(); for(int i=0;i<10000;i++) { str2.append("hello sk"); //StringBuffer str4=new StringBuffer(); } endTime=System.currentTimeMillis(); System.out.printf("StringBuffer执行速度:%d\n",endTime-beginTime); beginTime=System.currentTimeMillis(); for(int i=0;i<10000;i++) { str3.append("hello sk"); //StringBuilder str6=new StringBuilder(); } endTime=System.currentTimeMillis(); System.out.printf("StringBuilder执行速度:%d\n",endTime-beginTime); }}我们同时让兄弟三个做10000次增添字符串的操作,然后比较一下其运行时间
运行结果如下:
可以看到使用String“+”操作去“修改”字符串是非常慢的,和StringBuilder和StringBuffer差了1000倍左右。那么造成这些的原因我们需要借助jad反汇编一下:
发现
str1 = str1+"hello sk";
发现这一句的反汇编是
str1 = (new StringBuilder()).append(str1).append("hello sk").toString();
发现每次做“+”操作时都是创建了StringBuilder对象并且使用了两次append方法,也就是说10000次操作他创建了10000个对象,所以速度慢是必然的了,反观StringBuilder和StringBuffer都是直接在原有对象上操作。
那么StringBuilder和StringBuffer相比又是谁快一些呢?由于上一个例子数据量太小,看不出来比较,我们将循环加到100000次再次运行得到结果如下:
可以看到StringBuffer要比StringBuilder慢一些,可能是由于执行线程安全操作,在字符串上锁的时候多用了一些时间导致的。
综上所述,这三者设计差异导致的结果很明显:String不可变且要是使用“+”操作符速度非常慢;StringBuilder字符串可变但是线程不安全;StringBuffer字符串可变且线程安全。
三、String、StringBuffer、StringBuilder的适用场景
String:当该字符串修改次数很少或“+”很少时使用。
StringBuilder:当该字符串需要经常修改且是单线程程序时使用。
StringBuffer:当该字符串需要经常修改且有多个线程要访问时使用。
四、解释下面这段代码的结果
1、String s1 = "Welcome to Java";
2、String s2 = new String("Welcome to Java");
3、String s3 = "Welcome to Java";
4、System.out.println("s1 == s2 is " + (s1 == s2));
5、System.out.println("s1 == s3 is " + (s1 == s3));首先这段代码结果为false和ture。
原因:
首先在编译成class文件前,java将"Welcome to Java"这个字符串放入字符串常量池中。
之后执行第一句时,直接将s1指向了常量池中的字符串。执行第二句的时候s2使用常量池中的"Welcome to Java"字符串的值创建了一个字符串对象放在堆中。最后执行第三句的时候发现常量池中存在"Welcome to Java",所以像s1一样,s3指向了常量池中的字符串。
最后做对比得时候由于使用了“=”,等号代表比较的是两个变量的地址是否相等,而可以从图中看出,s1和s2指向不同,一个指向字符串常量,一个指向堆中的对象,所以第一句返回false。而s1和s3都指向字符串常量,所以返回ture。