虽然印象中记得StringBuffer是线程安全,因此性能比StringBuilder慢一丢丢,可是实话说对于它们3个的了解仍是很浅,本文咱们就深刻♂一些,完全搞明白这三兄贵。html
首先咱们要清楚一个知识:String是不可变的。java
这是啥意思呢,就是一个String对象,它所存储的具体字符串值,是不可修改的。String本质上也是一个类,它里面有不少属性和方法,而存储的字符串值在它里面也只是一个char数组的属性而已,可是这个属性却被final修饰了,不可更改,因此这个属性只会随着String对象的建立而初始化一次。也就是一个String对象,它存储的字符串是固定死的,直到这个对象被回收也不会更改。数组
一句话:一旦你经过new或其余手段建立了一个String对象,那么它存储的字符串值就是固定的,不会再改变了。缓存
话虽如此,咱们偶尔仍是能够看到字符串拼接操做:安全
String str = "a";
str = str + "b";
复制代码
看起来str对象的值由【a】改变成了【ab】,实际上它已经不是那个“它”了,第一行的str和第二行的str指向的已经不是同一个String对象了。markdown
详细且废话点说,就是第一行时,变量str指向了一个String对象,它的值是“a”。而第二行str+"b"中,新new了一个String对象,而且它的值是"ab",同时str从新指向了这个对象。而本来的值为"a"的对象,仍是存在的,只是如今已经没有变量指向它了。多线程
验证:并发
验证方法也很简单,查看第一行和第二行的str指向的内存地址就能够了,因为String已经重写了hashCode()方法,因此咱们能够经过System.identityHashCode(object)
获取它的原始hashCode,这个hash值就是根据内存地址获取的,若是是同一个对象,天然取出来的值也是同样的。app
代码:less
package com.lzh.array;
public class Test1 {
public static void main(String[] args) {
String str = "a";
System.out.println("字符串 a 的String对象hash值:"+System.identityHashCode(str));
str = str + "b";
System.out.println("字符串 ab 的String对象hash值:"+System.identityHashCode(str));
String str1 = new String("a");
System.out.println("虽然是字符串a,可是是new出来的对象,因此hash值为:"+System.identityHashCode(str1));
}
}
复制代码
结果是
字符串 a 的String对象hash值:1265094477 字符串 ab 的String对象hash值:2125039532 虽然是字符串a,可是是new出来的对象,因此hash值为:312714112
其实,经过上面的知识,咱们就知道为何须要StringBuffer和StringBuilder了,正是由于String是不可变的。
若是咱们须要频繁的操做同一个字符串,那必然会建立不少String对象,而后不停的让变量指向新的String对象。可是实际上咱们须要用的就只有一个对象,那么就会产生很大的资源浪费,若是你更改了10次字符串,那就会建立10次String对象,效率低不说,浪费的内存空间更多。
若是代码里这样的操做多一些或来几十个循环,估计就麻烦了,一会儿就可能建立了成百上千个无用的String对象。
因此java必须有一个可变长的字符串类,这就是StringBuffer和StringBuilder的做用,它们均可以更改自身所存储的字符串值,当须要对字符串频繁操做时,咱们就能够用它们代替String对象了。不用担忧转换问题,它们存储字符串的方式和String是相同的,都是char数组,只是没有加final修饰,而且也都重写了toString方法。
这时可能咱们会有一个疑问,为何最开始要把String设计成不可变的呢?若是它一开始就是可变的,那不就没这么多事了吗?
这里咱们就说一下String类是不可变的好处:
java中,String的使用能够说是最多的,并且不少是做为常量反复使用。像基本类型Integer、Long这些,也都设置了各自的常量池(一般是-128~127),覆盖一些经常使用的数字范围,目的就是避免建立大量无心义的对象。String做为使用最多的对象,也天然得设置一个常量池。
而String的常量池因为不能预判用户常常会使用哪些字符串,因此不能像Integer同样初始化一个范围。因此String的常量池是这样实现的:在声明一个String时,它会进入常量池中找这个字符串,若是没有,就直接new一个String对象,同时将这个对象投入到常量池。那么若是后面又有其余地方用到了这个字符串,就会直接使用第一次new出来的对象。
这就是String常量池的原理,但若是String是可变长的,那就实现不了常量池了。若是常量池中的String能够被任意改动它实际存储的值,那仍是常量池吗?因此说个题外话,Integer那些包装类,也是不可变的。
其实①就是为了提高使用性能而建立的常量池,可是还有一些其余方面的性能问题,例如HashMap等容器,它们的Key大可能是String,固然HashMap已经利用hashcode进行性能上的优化了,可是若是对象的hashcode不能保持稳定不变,也会形成很大的性能浪费。
若是String是可变的,那么每次你修改String对象,它的hashcode都不得不从新计算一次,反复计算新的hashcode就已经够麻烦了,更麻烦的是若是你把已经加入到Map里的一个数据的key改重复了,那同一个Map就有两个key相同的数据了,为了不这点又不知道要作多少设计和限制。
一旦容易发生变化,就很容易引发各类各样的问题。
若是String随随便便就能够把它的值改了,那涉及到线程的地方确定又是个大麻烦,要作到线程安全,又是一大笔性能开销(怎么又是性能,看来性能真的很重要)。
不只是线程安全,其余地方例如在写代码的时候,不当心将String的value操做变化了,可是却没发现,也是一种风险。
能够说官方只是选择了最快和最安全的方式表达字符串,而且将这种方式锁定设为了默认选择。但若是咱们想用可变的字符串,官方也为咱们留了一扇门:StringBuffer和StringBuilder
说好的一扇门呢,这怎么有两扇?
别担忧,两个门各有各的特点,先让咱们搞清楚两个门的区别:
StringBuffer是线程安全的(可是也由于这点,牺牲了一些性能),StringBuilder不是线程安全的(因此效率比前者高)。
好了,没了,结束。
。。
。。
。。
呃,的确就只是这个区别而已。
若是只是想知道它们两个的“区别”,那到这里为止就结束了,不过大家可能想了解一下这两个类的其余知识,我就继续讲解一下好了。
在前面,我说了它们的区别就只是线程是否安全,以及因为这个区别产生的性能效率区别。
的确没有其余区别,包括怎么使用,怎么初始化,都是同样的。
相信看到这里,你们就猜到它们这么类似的缘由了,由于它们实现了同一个抽象类:AbstractStringBuilder。这个抽象类的描述是:可变的字符序列。简单粗暴的说明了它的特色,可变的字符串。
关于这个抽象类,咱们后面详细说说,先说个一个小知识:StringBuffer的诞生比AbstractStringBuilder更早。
这是很正常的,StringBuffer从JDK1.0开始就存在了,它是线程安全的,可是也所以牺牲了一些性能。在JDK1.5的时候,线程不安全可是效率更高的StringBuilder就和它们的抽象类AbstractStringBuilder一块儿诞生了。这个时候StringBuffer也被迫继承了这个抽象类。
因此AbstractStringBuilder其实就是对可变长字符串专门提取出来的抽象类,也是对这一律念的描述。
接口:
关于AbstractStringBuilder,它定义了一些可变字符串的属性和方法实现,同时它还实现了两个接口:
Appendable和CharSequence
abstract class AbstractStringBuilder implements Appendable, CharSequence {
复制代码
Appendable(翻译:可追加)接口也是一同推出的接口,内容很简单,就是3个append的方法,append方法用过的人应该懂,就是StringBuffer和StringBuilder进行字符串拼接的方法
package java.lang;
import java.io.IOException;
public interface Appendable {
//容许append拼接实现了CharSequence接口的类
Appendable append(CharSequence csq) throws IOException;
//容许append拼接实现了CharSequence接口的类,并指定要拼接的字符串范围,只取其开始到结束位置的字符
Appendable append(CharSequence csq, int start, int end) throws IOException;
//容许append拼接基本类型char字符
Appendable append(char c) throws IOException;
}
复制代码
而其中眼熟的CharSequence(翻译:字符序列)接口,就是说明实现了它的类是一个字符序列。固然最多见的实现类就是String,StringBuffer和StringBuilder了。因为这两个接口的组合使用,才让咱们能够进行字符串的拼接,甚至能够跨类进行拼接(StringBuffer拼接StringBuilder对象),只要这个类实现了CharSequence接口便可。
而CharSequence接口,也定义了一些字符序列的方法,例如最经常使用的length()获取字符串长度,charAt(index)获取单个字符,subSequence(start,end)截取字符串,这三个方法是实现类必须实现的。
这两个接口咱们大体明白了,简单总结一下:Appendable是关于拼接字符串的接口,而实现了CharSequence接口则是代表自身也是属于字符串类型的类。
属性:
//字符数组,即存储字符串的值,不过和Spring不一样的是它没有用final修饰,因此能够修改,听说JDK9以后,采用的就是byte[]了。
char[] value;
//字符数组的长度,length()方法其实就是直接返回这个值。
int count;
//字符串的最大值,实际上这个值是直接从数组的最大长度直接取的,毕竟字符串的值也是数组,数组能有多长,字符串就有多长
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
复制代码
其中比较关键的,固然就是value属性了,本质上它和String是同样的,只是Spring的value属性被private final修饰了,才致使它是不可变的。而没有任何修饰的value,就能够修改了,这个value也就是可变长字符串的核心属性了,因此定义在了父类的抽象类中,子类StringBuffer和StringBuilder是没有这个属性的。
方法:
原本想要不把方法都讲解一下,可是看了一下里面的方法数。。?抱歉,是我不知天高地厚了。数量仍是有亿点多的,包括经常使用的对字符串进行操做的方法(毕竟是可变长的),获取字符串的方法。还有一些是针对字符数组的操做(即value属性),由于java中数组是定长的,显然咱们不可能每次都初始化一个最大长度的字符数组,而是应该随着字符数量的增多,对数组进行扩容。最后还有一些是兼容String的方法,像indexOf,substring这些String里有的方法。
因此里面的方法,这里就先不讲了,仍是重点关注一下StringBuffer和StringBuilder类吧,结合它们会顺带带出来一些AbstractStringBuilder中的方法。
前面也说过了,其实它们最大的区别就是:是否线程安全。而且因为这个缘由致使了线程不安全的StringBuilder能够有更快的效率。这里咱们看看源码,经过源码查看一下二者的区别。
固然前提你得知道synchronized关键字是啥和它的做用,若是不知道还要继续看的话,就先记住它的功能是线程保护。加了它修饰的方法,同一时间只能有一个线程执行(因此会下降性能)。
首先它们两的继承结构固然是同样的,都继承了父类AbstractStringBuilder,这意味着它们是可变长的。而后都实现了CharSequence接口,同时也实现了Serializable接口,代表本身支持序列化。
StringBuffer:
public final class StringBuffer extends AbstractStringBuilder implements java.io.Serializable, CharSequence {
}
StringBuilder:
public final class StringBuilder extends AbstractStringBuilder implements java.io.Serializable, CharSequence {
}
复制代码
先声明,这块内容意义不是很大,可是能够涨涨知识,可选择跳过。
/** * A cache of the last value returned by toString. Cleared * whenever the StringBuffer is modified. */
private transient char[] toStringCache;
复制代码
toStringCache属性是StringBuffer特有的属性,注释的意思大概是:toString返回的最后一次缓存值,会随着StringBuffer的修改而清空。
因此这个字段也就是个缓存,而且是专门用于toString方法的缓存,另外若是StringBuffer的value值进行了任何修改,它都会被直接设为null。
既然它是为toString服务的,那么咱们就看看两个类的toString方法区别:
StringBuffer的toString方法:
@Override
public synchronized String toString() {
if (toStringCache == null) {
toStringCache = Arrays.copyOfRange(value, 0, count);
}
return new String(toStringCache, true);
}
复制代码
StringBuilder的toString方法:
@Override
public String toString() {
// Create a copy, don't share the array
return new String(value, 0, count);
}
复制代码
toStringCache的做用和理解:
能够看到,它的确是为toString方法服务的一个属性。当使用者调用toString方法时,逻辑会先判断它是否为null。若是为null(说明被改过),就拷贝当前的value数组值做为一个新数组存给toStringCache,而后将它new直接传递给String的value数组。这里的重点是它直接传递了过去,这说明这个数组在这个时刻,StringBuffer的toStringCache和String的value指向的是同一个数组。也就是只要一个地方改了,另外一个地方也会自动改变。
那这不就不符合String的不可变性了吗?答案是不会,首先String的value值是不可能改的,由于final修饰了,惟一可能变化的就是StringBuffer,可是StringBuffer只要字符串有任何改动,toStringCache属性都会当即设为null,也就是和以前的char数组撇开关系。
因此这里惟一提升了效率的地方,就是new String,因为直接将数组值传递了过去,固然一行就搞定啦。
String的构造方法源码:
String(char[] value, boolean share) {
// assert share : "unshared not supported";
this.value = value;
}
复制代码
但是StringBuillder却用的不是这个构造方法,由于StringBuillder没有toStringCache属性,为了不它发生上面所说的,违反String不可变性的问题,因此它调用的String构造方法是从新复制了一个数组,加上一堆有的没的逻辑判断,天然就相对慢一点。
toStringCache的意义:
虽然看起来高大上,但有时候不要就觉得它是100%实用的。上面咱们所说的优化,隐藏了一个大前提,就是须要连续调用未更改的StringBuffer的toString方法。毕竟若是你修改了StringBuffer,那么toStringCache就会被设为null,那一样也要彻底复制value的char数组给它,只是这个复制操做从String的构造方法挪到了StringBuffer的toString中。因此这种状况下,性能彻底不见得会有啥区别,没准儿还更慢。
可是连续调用未修改的StringBuffer的toString方法,更是极其罕见的操做,若是真有人会这样写代码,那我就有点好奇他想干啥了。。
因此我我的以为它的实用性并不大,这也是我开头所说的,意义不大,可是能够涨涨姿式。网上有网友说,有些代码可能从JDK1.0开始就存在了,像这种可能优点并不大的代码,不多会发生改动,有必定的缺陷是正常的。有时候改起来的成本和影响,远超过它自己存在所形成的负面影响。
因此没准,它也是个有一点点冗余的小功能,只是不方便修改代码而已?固然我也只能画个问号,由于我也不知道。。
构造方法二者是彻底一致的,二者都有4个构造方法,且这4个代码都是同样的。因此这里不会过多的说明。
无参构造方法:
public StringBuffer() {
super(16);
}
复制代码
能够看到调用了父类的构造方法,并默认传了数字16。
父类AbstractStringBuilder的构造方法:
/** * Creates an AbstractStringBuilder of the specified capacity. */
AbstractStringBuilder(int capacity) {
value = new char[capacity];
}
复制代码
因此,这个16的意义就是初始化了一个长度为16的char数组设为值。前面已经了解过了,数组的最大长度是Integer.MAX_VALUE - 8,可是显然不可能每次都初始化这么大的,这里咱们能够理解了。默认状况下,StringBuffer和StringBuilder是先建立一个长度为16的字符数组。
固然,若是是有参的初始化,它也是往字符串的长度再延长一个16,再进行初始化。
public StringBuffer(String str) {
super(str.length() + 16);
append(str);
}
复制代码
二者的append方法原理是相似的,虽然有一些细微差异,可是因为数量太多了,StringBuffer有14个append方法,StringBuilder有13个,因此只节选两个简单讲解。
StringBuffer的append方法:
@Override
public synchronized StringBuffer append(Object obj) {
toStringCache = null;
super.append(String.valueOf(obj));
return this;
}
@Override
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
复制代码
StringBuilder的append方法:
@Override
public StringBuilder append(Object obj) {
return append(String.valueOf(obj));
}
@Override
public StringBuilder append(String str) {
super.append(str);
return this;
}
复制代码
首先synchronized关键字,便是StringBuffer对多线程操做的预防,嗯,虽然以前觉得线程安全很高深,可是好像也就是靠这个关键字作到的。而后toStringCache属性设为null的理由前面也说明的很清楚了。最后一个细微的小区别,我以为多是synchronized的影响,不过自己也是无关痛痒的变化。
它们都选择调用了父类的append方法,说明核心的内容仍是在AbstractStringBuilder父类抽象类中的,虽然咱们目的是想了解它们的区别,可是这个时候很适合插入一些知识,因此咱们简单的看看append方法的源码学习一下。
AbstractStringBuilder的其中一个append方法:
public AbstractStringBuilder append(String str) {
//校验要append的字符是否为空
if (str == null)
//若是为空,调用appendNull方法,会添加null四个字符到尾部,并不会抛出异常哦
return appendNull();
//获取要添加的字符串长度
int len = str.length();
//确保数组容量够用,不够用的话会进行扩容
ensureCapacityInternal(count + len);
//经过String的getChars方法,将String对象中的char数组复制到StringBuffer的char数组尾部。
str.getChars(0, len, value, count);
count += len;
return this;
}
复制代码
就不详解方法内部了,对每一个函数的操做都注释了。值得关注的是ensureCapacityInternal
,它是检查当前数组容量的方法,固然里面还嵌套了好几个不一样方法,目的只有一个:检查当前数组的长度是否足够,若是不够则扩容。
其中 str.getChars(0, len, value, count);
内部实际上使用了System.arraycopy,这是System提供了的一个native静态方法,专门用于拷贝数组的。
关于StringBuffer和StringBuilder的扩容:
经过查看源码,并不难理解,前面咱们已经知道它通常状况下的初始化大小为16(能够本身指定这个大小初始化)。当使用append方法添加字符时,就会检查其容量是否足够,不足时首先会扩容至当前数组长度*2+2,乘2好理解,就是翻倍当前的长度。至于加2,听说是由于拼接字符串一般末尾都会有个多余的字符。
固然有时候一次加的字符串太长,翻倍+2也不足以装下它,这时候就会直接将长度设置为添加的字符串加上本来字符串的长度,也就是刚恰好装的下的程度。
要查看StringBuffer和StringBuilder的字符容量,能够用capacity方法,它会返回char数组的长度,而length方法实际返回的是存储的字符数量。
其余还有很多区别,可是就不细讲了,由于这些区别有个共同点,就是这些方法只有StringBuffer有,而StringBuilder没有,可是二者的对象均可以使用这些方法。
没错,就是父类的方法,StringBuffer额外重写了好几个父类的方法,可是却没有做多少 改动,几乎全都加了synchronized
,有些方法还会在第一行加上一个toStringCache = null;
,因此目的只是兼容它的线程安全,因此没有什么必要进行比较。
看了这么多,相信你们最关心的就是这个了,先说结论:通常用StringBuilder,除非可能有线程问题。
StringBuffer对线程安全的处理比较简单粗暴,就是为大部分方法都上个synchronized
,无论你是加是减仍是查,不少方法都直接用synchronized
修饰,天然能够保证线程安全。可是效率可想而知。。。比较低下。
并且咱们通常也不常须要在多线程的状况下操做StringBuffer或StringBuilder。
就算是多线程,还要要求不能是高并发的,由于StringBuffer是直接用synchronized
的,很容易堵塞。因此有些时候会选择用StringBuilder搭配其余手段解决高并发状况下的线程问题(本身在外部加锁之类的)。
因此无论怎么看,StringBuilder都用的比StringBuffer多,固然除非你是低并发下的多线程操做。
感受都讲到这个地步了,不贴几个方法好像也过不去了,如下会列一些StringBuffer和StringBuilder的经常使用方法。
固然经过了源码分析,咱们都知道这大部分方法都是从它们的爸爸:AbstractStringBuilder抽象类父类中来的。
将指定的字符串追加到此字符序列,同时有各类各样的重载方法。
将字符序列翻转,就是123翻转变成了321这样。
删除字符序列中指定位置的子字符串
将字符串插入此字符序列的指定位置,有不少格式的重载方法。
使用给定 String 中的字符,替换此字符序列中指定位置的字符。
返回当前字符数组的容量(即char[]的容量)。
返回第一次出现的指定子字符串在该字符串中的索引下标。
返回指定子字符串最后一次出如今字符串中的索引。
截取指定位置的字符串,而后返回为一个新的String对象。
经过这9节内容,我想若是认真看完了,应该学到的不只只有StringBuffer和StringBuilder的区别而已。
StringBuffer虽然是从JDK1.0就开始出现的,可是目前来看,最经常使用的应该是JDK1.5出的StringBuilder。
(StringBuffer:为何会变成这样呢?明明。。明明是我先的来的。。)
这两个类我刚开始在用的时候不太容易分得清哪一个是线程安全的,当时是用Buffer这个单词记忆的,Buffer有缓冲的意思,由于是处理多线程的类,因此须要缓冲。。。。我大概就是这样记的,虽然听起来有点不太靠谱。。
参考资料:
JDK源码之AbstractStringBuilder类分析:
[十三]基础数据类型之AbstractStringBuilder: