String、StringBuffer、StringBuilder的区别详解

虽然印象中记得StringBuffer是线程安全,因此性能比StringBuilder慢一丢丢,可是实话说对于它们3个的了解仍是很浅,本文咱们就深刻♂一些,完全搞明白这三兄贵。html

 

首先咱们要清楚一个知识:String是不可变的。java

1.不可变的String

这是啥意思呢,就是一个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

 

 

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操做变化了,可是却没发现,也是一种风险。

 

能够说官方只是选择了最快和最安全的方式表达字符串,而且将这种方式锁定设为了默认选择。但若是咱们想用可变的字符串,官方也为咱们留了一扇门:StringBufferStringBuilder

 

4.StringBuffer和StringBuilder的区别

说好的一扇门呢,这怎么有两扇?

别担忧,两个门各有各的特点,先让咱们搞清楚两个门的区别:

StringBuffer是线程安全的(可是也由于这点,牺牲了一些性能),StringBuilder不是线程安全的(因此效率比前者高)。

好了,没了,结束。

。。

。。

。。

呃,的确就只是这个区别而已。

若是只是想知道它们两个的“区别”,那到这里为止就结束了,不过大家可能想了解一下这两个类的其余知识,我就继续讲解一下好了。

 

5.StringBuffer和StringBuilder身世之谜

在前面,我说了它们的区别就只是线程是否安全,以及因为这个区别产生的性能效率区别。

的确没有其余区别,包括怎么使用,怎么初始化,都是同样的。

相信看到这里,你们就猜到它们这么类似的缘由了,由于它们实现了同一个抽象类AbstractStringBuilder。这个抽象类的描述是:可变的字符序列。简单粗暴的说明了它的特色,可变的字符串。

关于这个抽象类,咱们后面详细说说,先说个一个小知识:StringBuffer的诞生比AbstractStringBuilder

这是很正常的,StringBufferJDK1.0开始就存在了,它是线程安全的,可是也所以牺牲了一些性能。在JDK1.5的时候,线程不安全可是效率更高的StringBuilder就和它们的抽象类AbstractStringBuilder一块儿诞生了。这个时候StringBuffer也被迫继承了这个抽象类。

因此AbstractStringBuilder其实就是对可变长字符串专门提取出来的抽象类,也是对这一律念的描述。

image-20210224213732774

 

6.AbstractStringBuilder抽象类介绍

接口

关于AbstractStringBuilder,它定义了一些可变字符串的属性和方法实现,同时它还实现了两个接口

AppendableCharSequence

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里有的方法。

因此里面的方法,这里就先不讲了,仍是重点关注一下StringBufferStringBuilder类吧,结合它们会顺带带出来一些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类分析:

www.cnblogs.com/houzheng/p/…

[十三]基础数据类型之AbstractStringBuilder:

www.cnblogs.com/noteless/

相关文章
相关标签/搜索