虽然印象中记得StringBuffer是线程安全,所以性能比StringBuilder慢一丢丢,但是实话说对于它们3个的了解还是很浅,本文我们就深入♂一些,彻底搞明白这三兄贵。
首先我们要清楚一个知识:String是不可变的。
1.不可变的String
这是啥意思呢,就是一个String对象,它所存储的具体字符串值,是不可修改的。String本质上也是一个类,它里面有很多属性和方法,而存储的字符串值在它里面也只是一个char数组的属性而已,但是这个属性却被final修饰了,不可更改,所以这个属性只会随着String对象的创建而初始化一次。也就是一个String对象,它存储的字符串是固定死的,直到这个对象被回收也不会更改。
一句话:一旦你通过new或其他手段创建了一个String对象,那么它存储的字符串值就是固定的,不会再改变了。
话虽如此,我们偶尔还是可以看到字符串拼接操作:
String str = "a";
str = str + "b";
看起来str对象的值由【a】改变成了【ab】,实际上它已经不是那个“它”了,第一行的str和第二行的str指向的已经不是同一个String对象了。
详细且废话点说,就是第一行时,变量str指向了一个String对象,它的值是“a”。而第二行str+"b"中,新new了一个String对象,并且它的值是"ab",同时str重新指向了这个对象。而原本的值为"a"的对象,还是存在的,只是现在已经没有变量指向它了。
验证:
验证方法也很简单,查看第一行和第二行的str指向的内存地址就可以了,由于String已经重写了hashCode()方法,所以我们可以通过System.identityHashCode(object)
获取它的原始hashCode,这个hash值就是根据内存地址获取的,如果是同一个对象,自然取出来的值也是一样的。
代码:
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
2.有String不够吗,为什么要有StringBuffer和StringBuilder?
其实,通过上面的知识,我们就知道为什么需要StringBuffer和StringBuilder了,正是因为String是不可变的。
如果我们需要频繁的操作同一个字符串,那必然会创建很多String对象,然后不停的让变量指向新的String对象。但是实际上我们需要用的就只有一个对象,那么就会产生很大的资源浪费,如果你更改了10次字符串,那就会创建10次String对象,效率低不说,浪费的内存空间更多。
如果代码里这样的操作多一些或来几十个循环,估计就麻烦了,一下子就可能创建了成百上千个无用的String对象。
所以java必须有一个可变长的字符串类,这就是StringBuffer和StringBuilder的作用,它们都可以更改自身所存储的字符串值,当需要对字符串频繁操作时,我们就可以用它们代替String对象了。不用担心转换问题,它们存储字符串的方式和String是相同的,都是char数组,只是没有加final修饰,并且也都重写了toString方法。
3.为什么String要设计成不可变的?
这时可能我们会有一个疑问,为什么最开始要把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
4.StringBuffer和StringBuilder的区别
说好的一扇门呢,这怎么有两扇?
别担心,两个门各有各的特色,先让我们搞清楚两个门的区别:
StringBuffer是线程安全的(但是也因为这点,牺牲了一些性能),StringBuilder不是线程安全的(所以效率比前者高)。
好了,没了,结束。
。。
。。
。。
呃,的确就只是这个区别而已。
如果只是想知道它们两个的“区别”,那到这里为止就结束了,不过你们可能想了解一下这两个类的其他知识,我就继续讲解一下好了。
5.StringBuffer和StringBuilder身世之谜
在前面,我说了它们的区别就只是线程是否安全,以及由于这个区别产生的性能效率区别。
的确没有其他区别,包括怎么使用,怎么初始化,都是一样的。
相信看到这里,大家就猜到它们这么相似的原因了,因为它们实现了同一个抽象类:AbstractStringBuilder。这个抽象类的描述是:可变的字符序列。简单粗暴的说明了它的特点,可变的字符串。
关于这个抽象类,我们后面详细说说,先说个一个小知识:StringBuffer的诞生比AbstractStringBuilder更早。
这是很正常的,StringBuffer从JDK1.0开始就存在了,它是线程安全的,但是也因此牺牲了一些性能。在JDK1.5的时候,线程不安全但是效率更高的StringBuilder就和它们的抽象类AbstractStringBuilder一起诞生了。这个时候StringBuffer也被迫继承了这个抽象类。
所以AbstractStringBuilder其实就是对可变长字符串专门提取出来的抽象类,也是对这一概念的描述。
6.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中的方法。
7.StringBuffer和StringBuilder代码的区别
前面也说过了,其实它们最大的区别就是:是否线程安全。并且由于这个原因导致了线程不安全的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
{
}
②toStringCache属性和toString方法
先声明,这块内容意义不是很大,但是可以涨涨知识,可选择跳过。
/**
* 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);
}
- 区别1:StringBuffer使用了synchronized关键字修饰
- 区别2:StringBuffer判断了toStringCache属性是否为空,如果为空,就从value值中新复制一个字符数组给它。而StringBuilder没有这个属性。
- 区别3:new String方法不同,StringBuffer直接将char数组传递给了String的value属性,StringBuilder的new String却是复制了一个数组出来。
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方法
两者的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;
}
- 区别1:StringBuffer所有的append方法都加了synchronized关键字
- 区别2:StringBuffer所有的append方法在开头都将toStringCache属性设为了null
- 区别3:StringBuffer没有调用其他的重载append方法,而StringBuilder的append方法调用了它的一个重载(虽然只有这一个地方)。
首先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;
,所以目的只是兼容它的线程安全,所以没有什么必要进行比较。
8.StringBuffer和StringBuilder的应用场景
看了这么多,相信大家最关心的就是这个了,先说结论:一般用StringBuilder,除非可能有线程问题。
StringBuffer对线程安全的处理比较简单粗暴,就是为大部分方法都上个synchronized
,不管你是加是减还是查,很多方法都直接用synchronized
修饰,自然可以保证线程安全。但是效率可想而知。。。比较低下。
而且我们一般也不常需要在多线程的情况下操作StringBuffer或StringBuilder。
就算是多线程,还要要求不能是高并发的,因为StringBuffer是直接用synchronized
的,很容易堵塞。所以有些时候会选择用StringBuilder搭配其他手段解决高并发情况下的线程问题(自己在外部加锁之类的)。
所以不管怎么看,StringBuilder都用的比StringBuffer多,当然除非你是低并发下的多线程操作。
9.StringBuffer和StringBuilder的使用
感觉都讲到这个地步了,不贴几个方法好像也过不去了,以下会列一些StringBuffer和StringBuilder的常用方法。
当然通过了源码分析,我们都知道这大部分方法都是从它们的爸爸:AbstractStringBuilder抽象类父类中来的。
append(String s)
将指定的字符串追加到此字符序列,同时有各种各样的重载方法。
reverse()
将字符序列翻转,就是123翻转变成了321这样。
delete(int start, int end)
删除字符序列中指定位置的子字符串
insert(int offset, String str)
将字符串插入此字符序列的指定位置,有很多格式的重载方法。
replace(int start, int end, String str)
使用给定 String 中的字符,替换此字符序列中指定位置的字符。
capacity()
返回当前字符数组的容量(即char[]的容量)。
indexOf(String str)
返回第一次出现的指定子字符串在该字符串中的索引下标。
lastIndexOf(String str, int fromIndex)
返回指定子字符串最后一次出现在字符串中的索引。
String substring(int start, int end)
截取指定位置的字符串,然后返回为一个新的String对象。
10.总结
通过这9节内容,我想如果认真看完了,应该学到的不仅只有StringBuffer和StringBuilder的区别而已。
StringBuffer虽然是从JDK1.0就开始出现的,但是目前来看,最常用的应该是JDK1.5出的StringBuilder。
(StringBuffer:为什么会变成这样呢?明明。。明明是我先的来的。。)
这两个类我刚开始在用的时候不太容易分得清哪个是线程安全的,当时是用Buffer这个单词记忆的,Buffer有缓冲的意思,因为是处理多线程的类,所以需要缓冲。。。。我大概就是这样记的,虽然听起来有点不太靠谱。。
参考资料:
JDK源码之AbstractStringBuilder类分析:
https://www.cnblogs.com/houzheng/p/12153734.html
[十三]基础数据类型之AbstractStringBuilder: