Java学习------源码解析之StringBuilder

1. 介绍

String中还有两个常用的类,StringBuffer和StringBuilder。这两个类都是专门为频繁进行拼接字符串而准备的。最先出现的是StringBuffer,之后到jdk1.5的时候才有了StringBuilder。

2. StringBuilder解析

从这张继承结构图可以看出:

  • StringBuilder继承了AbstractStringBuilder
  • StringBuilder实现了Comparable接口,这说明他是可比较的
  • StringBuilder实现了CharSequence接口,这说明他是一个可变的字符序列
  • StringBuilder实现了Appendable接口,这说明他具有可追加字符序列的能力
  • StringBuilder实现了Serializable接口,这说明他可以被序列化


StringBuilder继承了AbstractStringBuilder,而AbstractStringBuilder的底层是一个byte数组,并且前面没有加final进行修饰,因此他是可变的,可以进行扩容,而String类就是因为这个byte数组加了final关键字进行修饰,因此String是不可变的。那么既然StringBuilder可以进行扩容,在具体进行扩容操作又是如何实现的呢?接下来就要继续从源码进行查看。

StringBuilder有一个无参构造方法,默认初始化的容量是16.

2.1 第一个例子

接下来我们写一个具体的程序来看一下,在扩容的时候是如何进行操作的。

java 复制代码
public class test {
    public static void main(String[] args) {
        StringBuilder sb = new StringBuilder();

        sb.append("Hello World");
        sb.append("123456");
        sb.append("3.1415926");

        System.out.println(sb);
    }
}

然后开始debug进行查看。首先是进入到Stringbuilder的这个append()方法

然后下一步会进入父类的append()方法。

这个方法就是确保他的容量,这里有一个count参数,代表最小容量,这个参数是指当前这个对象里面真实存储的元素的个数,现在是0,意味着这个对象创建出来之后,里面暂时还没有传入任何值。

进入到这个方法之后,这里有一个oldCapacity变量,因为初始化容量是16,因此这里这个值也是16,然后下一步判断这个最小容量minimumCapacity减去这个老容量oldCapacity是否大于0,此时可以看到值大于0为false,因此不会进去if中的扩容操作。

然后再看下一次的append()操作

同样的走到这一步,这个时候可以看见,最小容量minimumCapacity减去这个老容量oldCapacity大于0了,说明这里会进行扩容的操作,就会进入到newCapacity()这个方法。

到这一步可以看到此时的oldLength就是初始化的长度为16,newLength是预期的长度,也就是这个字符串拼接进去之后的长度,为17,增长就为17 - 16 = 1,接下来就要进入到ArraysSupport.newLength()这个方法中查看新长度为多少。

进入到方法之后,然后这里使用oldLength,也就是16,去加上Math.max(minGrowth, prefGrowth),在最小增长和预期增长之间的最大值,这里minGrowth为1,prefGrowth为18,因此最大值就是18,然后就是16 + 18 = 34。

然后回到newCapacity()方法,也可以看到length为34.

继续往下走,发现此时value值为34

当我们不指定初始容量时,会给一个默认的初始容量为16,从上面可以看到进行第一次扩容之后,新的容量为34.

2.2 第二个例子

我们在写一个程序进行测试,这次拼接的字符串稍微大一点

java 复制代码
public class test {
    public static void main(String[] args) {
        StringBuilder sb = new StringBuilder();

        sb.append("3.1415926535897932384626433832795028841971");

        System.out.println(sb);
    }
}

还是一样先进入append()方法

再进入父类的append()方法,可以看到现在长度为42

然后进入到ensureCapacityInternal()方法,这次可以看到最小容量为42,旧容量为16,42 - 16 > 0,因此要进行扩容。

接着进入到newCapacity()方法,这个时候的增长growth就是42 - 16 = 26了,然后下一步进入ArraysSupport.newLength()方法

进去之后oldLength不变,还是16,但是这次的Math.max(minGrowth, prefGrowth),minGrowth为26,prefGrowth为18,那么最大值就是26,所以prefLength的值为42。后续步骤就和之前一样了。
当我们不指定长度,并且添加的字符串长度过长时,扩容的时候,就直接使用当前这个字符串的长度。

如果这个时候,再添加一个字符串,比如sb.append("helloworld123");这个时候又会是什么样的呢?可以看到这个时候oldLength就是我们第一次拼接的长度为42,新长度为55,增长长度为 55 - 42 = 13,继续往下走

这个时候可以看到oldLength为42,Math.max(minGrowth, prefGrowth)方法取的最大值,最大值现在为44,因此扩容后的长度就是42 + 44 = 86.

那么扩容的测试到这里就结束了。我们可以看到,第一次初始化容量为16时,扩容容量为34,为16 * 2 + 2,这次再进行二次拼接时一开始为42,扩容容量为86,为42 * 2 + 2 ,可以明显看出每次扩容是原来容量的2倍再加2.原因就是因为这段代码,从上面调试来看,扩容时都是oldLength + (oldLength + 2),即oldLength * 2 + 2.

3. 扩容总结

  1. StringBuilder的扩容策略是从当前容量开始,每次扩容为原来的2倍再加2
  2. 如果一开始拼接的字符串的长度就超过了默认初始容量16时,那么StringBuilder扩容的容量就直接为一开始的字符串长度
  3. 如果我们想要进行优化,那么就要在创建StringBuilder时,预估一下我们要拼接的字符串的长度,然后给定一个合适的初始化容量,从而减少底层的扩容操作。

4. 构造方法和常用方法

4.1 构造方法

方法 描述
StringBuilder() 构造一个字符串生成器,其中不包含任何字符,初始容量为16个字符
StringBuilder(int capacity) 构造一个字符串生成器,其中不包含任何字符,并且具有由容量参数指定的初始容量
StringBuilder(String str) 构造初始化为指定字符串内容的字符串生成器

用法如下:

4.2 常用方法

  1. append(),追加方法,这个里面可以传多种不同类型的值

  2. delete():删除方法

java 复制代码
public class test {
    public static void main(String[] args) {
        StringBuilder sb1 = new StringBuilder("abcdefg");
        /**
         * delete(int start, int end)
         * 删除从指定位置开始,到指定位置结束的字符
         * 左闭右开:[start, end]
         */
        sb1.delete(0,3);
        System.out.println(sb1); //defg

        /**
         * deleteCharAt(int index)
         * 删除指定位置的字符
         */
        StringBuilder sb2 = new StringBuilder("abcdefg");
        sb2.deleteCharAt(5); // abcdeg
        System.out.println(sb2);
    }
}
  1. insert():插入方法,在指定位置添加的方法,可以添加多种类型的值
java 复制代码
public class test {
    public static void main(String[] args) {
        StringBuilder sb1 = new StringBuilder("abcdefg");
        /**
         * insert(int offset, String str)
         * 在指定位置上添加一个字符串
         */
        sb1.insert(3, "HELLO"); // abcHELLOdefg
        System.out.println(sb1);
    }
}
  1. replace(int start, int end, String str):替换方法
java 复制代码
public class test {
    public static void main(String[] args) {
        StringBuilder sb1 = new StringBuilder("abcdefg");
        /**
         * replace(int start, int end, String str)
         * 从指定位置开始到结束,替换成新的字符串
         * 同样也是左闭右开
         */
        sb1.replace(1, 3, "ABC"); // aABCdefg
        System.out.println(sb1);
    }
}
  1. reverse():反转方法
java 复制代码
public class test {
    public static void main(String[] args) {
        StringBuilder sb1 = new StringBuilder("abcdefg");
        sb1.reverse();
        System.out.println(sb1); // gfedcba
    }
}
  1. setCharAt(int index, char ch):设置某个位置为指定的字符
java 复制代码
public class test {
    public static void main(String[] args) {
        StringBuilder sb1 = new StringBuilder("abcdefg");
        sb1.setCharAt(0, 'A');
        System.out.println(sb1); // Abcdefg
    }
}
  1. setLength(int newLength):设置新的长度
java 复制代码
public class test {
    public static void main(String[] args) {
        StringBuilder sb1 = new StringBuilder("abcdefg");
        sb1.setLength(3);
        System.out.println(sb1); // abc
    }
}

剩下的一些方法,在用到的时候看一下即可,都不是很难。

相关推荐
_祝你今天愉快4 分钟前
HashMap 底层原理 (JDK 1.8 源码分析)
android·java·后端
源代码•宸6 分钟前
C++高频知识点(十四)
开发语言·c++·经验分享·raii
七七软件开发7 分钟前
直播 app 系统架构分析
java·python·小程序·系统架构·php
Wendy144113 分钟前
【目标检测基础】——yolo学习
学习·yolo·目标检测
程序员陆通14 分钟前
Spring Cloud微服务中的内存泄漏问题定位与解决方案
java·spring cloud·微服务
极光雨雨16 分钟前
JVM中年轻代、老年代、永久代(或元空间)、Eden区和Survivor区概念介绍
java·jvm
盖世英雄酱5813628 分钟前
配置的那点玄学
java·后端
2zcode30 分钟前
基于Matlab的聚类彩色图像分割系统
开发语言·matlab·聚类
网小鱼的学习笔记37 分钟前
python基础:数据解析BeatuifulSoup,不需要考虑前端形式的一种获取元素的方法
开发语言·前端·python
zyk_computer1 小时前
Redis 实现互斥锁解决Redis击穿
java·数据库·redis·后端·缓存·性能优化·web