关于StringBuffer和StringBuilder源码级讲解

1、 字符串相关类之可变字符序列:StringBuffer、StringBuilder

​ 因为String对象是不可变对象,虽然可以共享常量对象,但是对于频繁字符串的修改和拼接操作,效率极低,空间消耗也比较高。因此,JDK又在java.lang包提供了可变字符序列StringBuffer和StringBuilder类型。

注明:此文仅个人理解,如有错误还有友友们指出。

1.1 StringBuffer与StringBuilder的理解

1.1.1、三个类的对比:String、StringBuffer、StringBuilder

  • String:不可变的字符序列;底层使用char[] (jdk8及之前),底层使用byte[] (jdk9及之后)
  • StringBuffer:可变的字符序列;JDK1.0声明,线程安全的,效率低;底层使用char[] (jdk8及之前),底层使用byte[] (jdk9及之后)
  • StringBuilder:可变的字符序列;JDK5.0声明,线程不安全的,效率高;底层使用char[] (jdk8及之前),底层使用byte[] (jdk9及之后)

为什么说StringBuffer是线程安全的呢?我们来看一看StringBuffer的源码

!(https://img-blog.csdnimg.cn/49142e475595449da176a28eb773e97d.png)我们可以看到StringBuffer类的方法前面使用了synchronized关键字,确保在访问或修改字符串时,多个线程之间不会发生竞争条件。这种线程安全性是通过牺牲一些性能来实现的。这个在多线程里面会讲到。(这个也是StringBuffer与StringBuilder的主要区别)

1.1.2、StringBuffer/StringBuilder的可变性分析(源码分析)

因为StringBuffer和StringBuilder很相似,这里我们拿StringBuilder来说。

在这里插入图片描述
我们可以看到StringBuilder继承了AbstractStringBuilder类(StringBuffer也是继承的这个类)。AbstractStringBuilder类定义了两个重要的变量:

在这里插入图片描述

char[] value; //存储字符序列的数组
int count; //实际存储的字符的个数

请记住这个count。我们再往下看

(1)关于构造方法

在这里插入图片描述

我们可以看到new StringBuilder()的时候,如果不传任何参数,构造方法内部声明了super(16);我们知道,这是调用了父类的构造方法,我们点进去来看看在父类中究竟发生了什么事。

在这里插入图片描述
清晰明了了吧,new一个StringBuilder的时候调用了super(16),而在StringBuilder的父类AbstractStringBuilder中new了一个char数组,数组长度为16。因此当不传入参数的时候StringBuilder会自动将存储字符序列的长度设置为16。

接着我们看看StringBuilder的有参构造,当参数是一个int类型的数组时比如new StringBuilder(n),StringBuilder会将存储字符序列的长度设置n。当参数是一个String类型或者StringBuilder类型的时候,StringBuilder会将数组长度设置为形参的字符串长度加上16,然后apprnd()。关于append我们下面会讲到。

总结:当我们new一个StringBuilder对象的时候,StringBuilder会自动帮我们开辟一个有空间的char示例用来存储字符序列。那么为什么要这么做呢?我们前面提到了StringBuilder是可变的,正因为是可变的,所以new一个StringBuilder对象时候自动给我们开辟了空间以防止我们频繁扩容。因此,在实例化StringBuilder对象时为其分配了初始容量,可以减少扩容的次数,提高效率。

思考:因为StringBuilder是可变的,那么当StringBuilder给我们分配的空间不够用了怎么办?

(2)关于append()方法和StringBuilder的自动扩容探究

我们知道,StringBuilder是可变的,StringBuilder对象追加字符串调用的是append()方法,例如:str.append(“abc”)。那么在StringBuilder底层是怎样运行的呢?以及自动扩容是怎么回事?我们阅读源码看看究竟是怎么回事。

在这里插入图片描述
可以看到是调用了父类AbstractStringBuilder的append()方法。这种设计是为了提高代码复用,因为StringBuilder和StringBuffer都继承了AbstractStringBuilder类,而StringBuilder和StringBuffer的方法基本是一致的。

接下来我们看看AbstractStringBuilder类append()方法是怎么实现的。

在这里插入图片描述
这里特别注意这段代码:

if (str == null)
    return appendNull();

一般情况下当str.append(null)也就是形参为null时我们会怎么认为?我们点进appendNull()的源码看看:

private AbstractStringBuilder appendNull() {
    int c = count;
    ensureCapacityInternal(c + 4);
    final char[] value = this.value;
    value[c++] = 'n';
    value[c++] = 'u';
    value[c++] = 'l';
    value[c++] = 'l';
    count = c;
    return this;
}

可以看到当调用的append()方法实参为null时,StringBullder会将调用者的字符序列后面追加"null"字符串。例如:

public void test(){
    StringBuilder str = null;
    StringBuilder stringBuilder = new StringBuilder("hell");
    stringBuilder.append(str);
    System.out.println(stringBuilder); //hellnull
}

下面我们来看重头戏,我们主要来看看append()方法调用的**ensureCapacityInternal(count + len);**这个方法。我们点进去

在这里插入图片描述

if(minimumCapacity - value.length > 0)//表示当前对象字符序列如果在append()追加字符串后大小大于new StringBuilder()时自动给我们分配的大小的话,也就是给我们分配的内存不够用了

这句代码主要表示当前对象字符序列如果在append()追加字符串后大小大于new StringBuilder()时自动给我们分配的大小的话,也就是给我们分配的内存不够用了。这时候就需要扩容,那么扩容的逻辑是什么呢?我们继续看这句:

value = Arrays.copyOf(value,
        newCapacity(minimumCapacity));

Arrays的copyOf()方法传回的数组是新的数组对象,改变传回数组中的元素值,不会影响原来的数组。copyOf()的第二个自变量指定要建立的新数组长度,如果新数组的长度超过原数组的长度,则保留数组默认值。

这句表示要创建一个新的数组然后赋值给当前对象的value。第二个参数表明新了数组对象的大小,这里又调用了一个方法

newCapacity(minimumCapacity));

这个方法就是期待已久的扩容逻辑,我们我们看看这个方法是个怎么回事

private int newCapacity(int minCapacity) {
    // overflow-conscious code
    int newCapacity = (value.length << 1) + 2;//扩容大小
    if (newCapacity - minCapacity < 0) {
        newCapacity = minCapacity;
    }
    return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)
        ? hugeCapacity(minCapacity)
        : newCapacity;
}

首先,我们看这句代码 int newCapacity = (value.length << 1) + 2;使用位移操作符(<<)将当前字符数组的长度(也就是new StringBuilder时给我们分配的数组大小)(value.length)左移一位,并加上2,得到一个新的预计容量(newCapacity)。这里采用左移操作是为了进行乘以2的操作,加上2是为了留出一些额外的空间。

所以我们得到的新容量就是当前数组长度大小*2+2。注意这里指的数组长度并不是当前字符序列的长度,而是整个数组的长度。要区别length和length()。这就是StringBuilder的扩容机制。

1.1.3、对比三者的执行效率

效率从高到低排列:
StringBuilder > StringBuffer > String

测试一下:

public void test4(){
    //初始设置
    long startTime = 0L;
    long endTime = 0L;


    String text = "";
    StringBuffer buffer = new StringBuffer("");
    StringBuilder builder = new StringBuilder("");

    //开始对比
    startTime = System.currentTimeMillis();
    for (int i = 0; i < 20000; i++) {
        buffer.append(String.valueOf(i)).reverse();
    }
    endTime = System.currentTimeMillis();
    System.out.println("StringBuffer的执行时间:" + (endTime - startTime));

    startTime = System.currentTimeMillis();
    for (int i = 0; i < 20000; i++) {
        builder.append(String.valueOf(i));
    }
    endTime = System.currentTimeMillis();
    System.out.println("StringBuilder的执行时间:" + (endTime - startTime));

    startTime = System.currentTimeMillis();
    for (int i = 0; i < 20000; i++) {
        text = text + i;
    }

    endTime = System.currentTimeMillis();
    System.out.println("String的执行时间:" + (endTime - startTime));
}

运行结果:在这里插入图片描述

1.1.4、StringBuilder、StringBuffer的API

StringBuilder、StringBuffer的API是完全一致的,并且很多方法与String相同。

常用API**

(1)StringBuffer append(xx):提供了很多的append()方法,用于进行字符串追加的方式拼接
(2)StringBuffer delete(int start, int end):删除[start,end)之间字符
(3)StringBuffer deleteCharAt(int index):删除[index]位置字符
(4)StringBuffer replace(int start, int end, String str):替换[start,end)范围的字符序列为str
(5)void setCharAt(int index, char c):替换[index]位置字符
(6)char charAt(int index):查找指定index位置上的字符
(7)StringBuffer insert(int index, xx):在[index]位置插入xx
(8)int length():返回存储的字符数据的长度
(9)StringBuffer reverse():反转

  • 当append和insert时,如果原来value数组长度不够,可扩容。

  • 如上(1)(2)(3)(4)(9)这些方法支持方法链操作。原理:

    在这里插入图片描述

我是这么理解的,这些方法在执行时改变了自己本身的。也就是StringBuffer.reverse()时调用这个方法的StringBuffer对象改变了,并且也可以将他赋值给另一个对象,比如:StringBuffer buffer = str.reverse()。

与此同时也可以这样操作:**buffer.append(“abc”).reverse();**也就是支持这种链式编程的结构。

2、其它API

(1)int indexOf(String str):在当前字符序列中查询str的第一次出现下标
(2)int indexOf(String str, int fromIndex):在当前字符序列[fromIndex,最后]中查询str的第一次出现下标
(3)int lastIndexOf(String str):在当前字符序列中查询str的最后一次出现下标
(4)int lastIndexOf(String str, int fromIndex):在当前字符序列[fromIndex,最后]中查询str的最后一次出现下标
(5)String substring(int start):截取当前字符序列[start,最后]
(6)String substring(int start, int end):截取当前字符序列[start,end)
(7)String toString():返回此序列中数据的字符串表示形式
(8)void setLength(int newLength) :设置当前字符序列长度为newLength

  • 5
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
String、StringBufferStringBuilder都是Java中用于处理字符串的类。 String是一个不可变的字符串类,也就是说一旦创建了一个String对象,它的值就不能被修改。每次对String进行修改操作时,都会创建一个新的String对象,这样会浪费内存空间和时间。因此,当需要频繁地对字符串进行修改时,使用String并不高效。 StringBufferStringBuilder是可变的字符串类,它们可以被用来进行字符串的修改操作。StringBufferStringBuilder的主要区别在于StringBuffer是线程安全的,而StringBuilder是非线程安全的。这意味着在多线程环境下,如果有多个线程同时访问一个StringBuffer对象,它们是安全的;而多个线程同时访问一个StringBuilder对象时,可能会导致数据错乱。 使用StringBufferStringBuilder的场景通常是在需要频繁地对字符串进行修改的情况下。例如,在循环中拼接字符串、在递归函数中修改字符串等情况下,使用StringBufferStringBuilder可以提高性能。 如果需要将StringBufferStringBuilder转换为String对象,可以使用两种方式。一种是调用它们的toString()方法,将其转换为String对象。另一种是使用String的构造器String(StringBuffer buffer)来创建一个新的String对象,将StringBufferStringBuilder的内容复制到新的String对象中。 总结起来,String是不可变的字符串类,而StringBufferStringBuilder是可变的字符串类,适用于需要频繁修改字符串的场景。转换为String对象可以通过调用toString()方法或使用String的构造器来实现。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值