为了账号安全,请及时绑定邮箱和手机立即绑定

StringBuilder源码分析,让你不用再死记答案

标签:
Java

1、前言

相信学习Java的小伙伴对String、StringBuilder、StringBuffer都不会陌生,几乎每天都和它们打交道,但是你是否真的了解其它们呢?虽然看似简单的基础知识,但是确实高频的面试点。很多同学背过相关面试题,但是很容易就会忘记,主要是没有只有背、少了理解,因此很容易就会给忘记了。

2、StringBuilder的简单使用


StringBuilder sb=new StringBuilder();
sb.append("hello");
sb.append("world");
System.out.println(sb.toString());

是不是非常的熟悉,StringBuilder可以用来拼接字符串。那么大家有没有相关它底层是如何实现的呢?

3、类的关系梳理

第一,定义一个接口


public interface CharSequence {
    //获取内容长度
    int length();
    //定位某个字符
    char charAt(int index);
    //截取字符串
    CharSequence subSequence(int start, int end);
    //toString
    public String toString();
}

第二,定义一个抽象类

abstract class AbstractStringBuilder implements Appendable, CharSequence {
  //数组字符的数组
  char[] value;
  //字符长度
  int count;

  //构造函数(传递容量大小)
  AbstractStringBuilder(int capacity) {
    //初始化一个数组
    value = new char[capacity];
  }

  //返回长度
  public int length() {
    return count;
  }
  //返回容量大小
  public int capacity() {
    return value.length;
  }
}

第三步,定义一个实现类

public final class StringBuilder
    extends AbstractStringBuilder
    implements java.io.Serializable, CharSequence
{
  //构造函数
  public StringBuilder() {
    //如果不指定容量值,则默认是16
    super(16);
  }
  
  //构造函数
  public StringBuilder(int capacity) {
    super(capacity);
  }
}

通过以上的简单类结构,我们需要掌握以下几个核心问题
①什么需要加一个抽象类,AbstractStringBuilder呢?为什么不直接实现类实现接口呢?
②StringBuilder底层是char[]数组,它的数据结构是数组,如果不指定则默认是16
③为什么需要count字段呢?length()方法和capacity()的区别是什么呢?这不是多余的吗?

温馨提示:我们看源码的时候,看的过程带着问题、带着疑问去源码当中寻找答案,这样才能有所收获,而不是为了看源码而去看源码。

一般来说,抽象类可以用来解耦接口和实现类,很多优秀的框架几乎都是这种模式,好处是把公共的部分抽取到抽象类实现,减轻实现类的操作,具体如下所示。

//接口
public interface ITest{
  public void sayHello(String name);
}
//实现类
public class TestImpl implements ITest{
  @Override
  public void sayHello(String name){
    //对name进行校验
    if(name!=null&&!"".equals(name)){
      //具体的业务处理
    }
  }
}

这种模式的缺点就是如果ITest有好多个实现类,每个实现类都需要对name字段进行校验。那么如何优化呢?


//接口
public interface ITest{
  public void sayHello(String name);
}

//抽象类
public class AbstractTestImpl implements ITest{
  @Override
  public void sayHello(String name){
    //对name进行校验
    if(name!=null&&!"".equals(name)){
      //调用抽象方法
      say(name);
    }
  }
  //定义一个抽象方法
  public abstract void say(String name);
}

//实现类
public class TestImpl extends AbstractTestImpl{
  public void say(String name){
    //无需要做校验,直接开始处理业务即可
  }
}

看到这里,是不是明白为什么需要加AbstractStringBuilder类了呢?

4、StringBuilder存储数据

AbstractStringBuilder类的append方法解析


public AbstractStringBuilder append(String str) {
        if (str == null){
            str = "null";
        }
        //1.获取追加字符串的长度
        int len = str.length();
        //2.动态扩容char[]数组【数组长度是count+len】【继续看源码】
        ensureCapacityInternal(count + len);
        //3.往扩容char[]数组添加内容
        str.getChars(0, len, value, count);
        //4.count属性累加
        count += len;
        
        return this;
}

StringBuilder类的append方法解析

public StringBuilder append(String str) {
    //它本身不处理,只是调用父类的方法
    super.append(str);
    return this;
}

那么如何扩容的呢?

private void ensureCapacityInternal(int minimumCapacity) {
    if (minimumCapacity - value.length > 0)
        expandCapacity(minimumCapacity);
    }
}
void expandCapacity(int minimumCapacity) {
    //1.新数组的长度是原来的两倍
    int newCapacity = value.length * 2 + 2;
    //2.判断两个值,最后觉得以哪个长度为主
    if (newCapacity - minimumCapacity < 0){
            newCapacity = minimumCapacity;
    }
    //3.判断新数组长度是否为小于0
    if (newCapacity < 0) {
        if (minimumCapacity < 0){
            throw new OutOfMemoryError();
        }
        //如果小于0则取Integer的最大值
        newCapacity = Integer.MAX_VALUE;
    }
    //4.拷贝数组【继续看源码】
    value = Arrays.copyOf(value, newCapacity);
}

思考:newCapacity为什么会小于0呢?是不是很奇怪呢?
解析:因为int是32位的二进制,最高位是符号位(0正数,1负数),如果newCapacity大于Integer最大值,那么首位被挤掉了,由0变成1,那么就变成了负数了。

public static char[] copyOf(char[] original, int newLength) {
    //1.创建一个新的数组
    char[] copy = new char[newLength];
    //2.拷贝数组【继续看源码】
    System.arraycopy(original, 0, copy, 0,
                         Math.min(original.length, newLength));
    return copy;
}

System.arraycopy是JVM底层提供的方法,native修饰的,用它来进行数组之间的拷贝。
源码分析到这里,我们必须掌握两个核心的东西
第一)StringBuilder底层是char[]数组
第二)StringBuilder是动态扩容的,它是通过创建一个新的数组,然后把旧数组的数据拷贝到新数组当中,旧数组给gc回收

5、StringBuilder删除数据

public AbstractStringBuilder deleteCharAt(int index) {
    //1.校验
    if ((index < 0) || (index >= count)){
        throw new StringIndexOutOfBoundsException(index);
    }
    
    //2.拷贝数组【同一个数组之间的拷贝】
    System.arraycopy(value, index+1, value, index, count-index-1);
    
    //3.count递减
    count--;
    return this;
}

拷贝这个地方思路可能有点绕,给大家重点分析一下
char[] arrs={0,1,2,3,4,5,6},有7个元素,我们要删除index=4的元素,那么如何删除呢?思路是arrs数组直接的拷贝,
System.arraycopy(src,srcPos,dest,destPos,length)
①src表示源数组
②srcPos表示从源数组的什么位置开始拷贝
③dest表示拷贝到哪个数组
④destPos表示拷贝到新数组的哪个位置
⑤length表示拷贝旧数组的几个元素

System.arraycopy(arrs,index+1,arrs,index,arrs.length-1-index)会执行如下操作
①0,1,2,3元素不变
②第5个元素放的是5
③第6个元素放的是6
④第七个元素是4,4会被挤到最后【大家可以测试该函数的使用】
最后结果是,char[] arrs={0,1,2,3,5,6,4}

4就是被我们删除的元素,我们只能通过把它挤到最后,然后通过count–表示数组的有效长度,那么读取数组的时候不是

for(int i=0;i<arrs.length;i++),而是for(int i=0;i<count;i++)

因此,大家回过头去看前面的问题,为什么需要count字段,length()和capacity()方法的区别,是不是就恍然大悟了。

6、总结

看到这里StringBuilder的核心思路就讲解完成了,它有很多的方法,大家可以自己去看,其实都不难,掌握核心思想即可。
第一)它底层是char[]数组
第二)新增元素的时候,如何扩容数组
第三)删除元素的时候,又是如何处理的

慕课网专栏(架构思想之微服务+仿百度网盘源码):
https://www.imooc.com/read/73
感谢您的阅读,希望您多多支持,这样我在原创的道路上才能更加的有信心。

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消