JAVASE进阶:强推!源码分析——字符串拼接底层原理、StringBuilder底层原理

👨‍🎓作者简介:一位大四、研0学生,正在努力准备大四暑假的实习
🌌上期文章:JAVASE进阶:String常量池内存原理分析、字符串输入源码分析
📚订阅专栏:JAVASE进阶
希望文章对你们有所帮助

这是比较重要的内容,学习原理很重要,啃源码也很重要!!!

字符串常量的"+"拼接效率很高,但涉及大量字符串变量的拼接的时候效率就会大打折扣,除了时间耗费大,内存也会耗费很大,所以在这种时候我们会经常使用StringBuilder函数或者StringJoiner函数来进行字符串变量拼接。

因此在这里剖析一下字符串常规拼接方式的底层原理,并了解为何StringBuilder能大大提高字符串拼接的效率。最后啃一啃StringBuilder的源码,摸清StringBuilder的底层运行原理。

字符串拼接

字符串的拼接,实际上是有两种情况的,一种是等式的右边不包含变量的,一种是等式的右边有变量的:

1、等式右边无变量:

String s = "a" + "b" + "c";
System.out.println(s);

2、等式右边有变量:

String s1 = "a";
String s2 = s1 + "b";
String s3 = s2 + "c";
System.out.println(s3);

这两种方式的底层都是有对应的原理或机制的。

等式右边无变量

拼接的时候没有变量,都是字符串,就会触发字符串的优化机制。
也就是说,在编译的时候已经是最终的结果了。
也就是说,原先的.java文件,在编译成.class文件的时候,语句可以视为变成了:

String s = "abc";
System.out.println(s);

这种方式非常简单。

等式右边有变量(JDK8以前的源码分析)

拼接的时候有变量参与,这时候就会很复杂了。

JDK8以前,底层会使用StringBuilder进行拼接,在这里讲解一下JDK8以前,代码运行的步骤:

1、String s1 = "a",会在字符串常量池中创建一个字符串"a",s1记录a的地址值
2、String s2 = s1 + "b"
(1)在字符串常量池中创建一个字符串"b"
(2)在堆内存中创建一个StringBuilder(),通过append方法将"a"和"b"都放到StringBuilder容器中
(3)StringBuilder()执行toString()将容器转换为String类型的"ab"
也就是说,底层进行的代码是:
new StringBuilder().append(s1).append("b").toString()
3、String s3 = s2 + "c"与第2步同理。

反正最终都是要变成一个字符串并返回地址给变量,那么StringBuilder是怎么变成字符串的,toString到底是如何执行的,需要阅读源码:

1、Ctrl+N点开搜索按钮,查找java.lang包下的StringBuilder:
在这里插入图片描述
2、进入后点击Ctrl+(Fn)+F12查找toString()方法:
在这里插入图片描述
在这里插入图片描述
3、Ctrl+B跟踪进入:
在这里插入图片描述
看到new关键字就可以确定了,等式右边有变量,除了要new出StringBuilder对象,还会因为toString方法去new出一个字符串。

因此,JDK8以前的这种方法,一个加号,堆内存就要new出两个对象,非常浪费性能。

JDK8字符串拼接的底层原理

执行下列语句:

String s1 = "a";
String s2 = "b";
String s3 = "c";
String s4 = s1 + s2 + s3;
System.out.println(s4);

JDK7执行这一段代码,至少要new出4个对象,显然是很差劲的方式。

JDK8默认的方式:预估字符串的长度,并创建一个数组

“a”“b”“c”

这种方式看起来会很便捷,但是实际上,预估也是需要时间的,如果很多行都有"+"操作,就要做很多次的预估,依旧会影响效率,创建的数组数量也会更多,时间与空间并没有实质性的优化。

因此,如果都是字符串常量拼接,可以直接"+";如果很多字符串变量拼接,不要直接"+",会在底层创建多个对象,浪费时间和性能

其实StringBuilder本身是很高效的方式的,但是字符串变量的多次"+"导致创建多次StringBuilder显然也是不合理的。因此当有很多字符串变量的时候,最好的方式还是使用StringBuilder或者StringJoiner来append或add进去,最后再利用toString方法转换为字符串,整个过程只会new出2个对象。

简单常见面试题

1、等式右边无变量的情况:

String s1 = "abc";
String s2 = "a" + "b" + "c";
System.out.println(s1 == s2);

显然,既然编译的时候已经将s2的右边看做是"abc"了,那么就会复用常量池中的"abc",因此答案为true

2、等式右边有变量的情况:

String s1 = "abc";
String s2 = "ab";
String s3 = s2 + "c";
System.out.println(s1 == s3);

s1会在常量池中生成,但是s3的创建,JDK8以前会先在底层创建StringBuilder对象再toString转化为字符串,JDK8以后先预估数组对象再转化为字符串,无论哪种方式,最后都是在堆空间中new出一个字符串对象的,而又因为引用变量比较的是地址值,所以答案为false

这都是很简单的面试题,注意分析一下等式右边从哪里来的就行了。

StringBuilder源码分析

那么StringBuilder为何高效?同样需要分析源码。在分析源码之前先大致了解一下StringBuilder执行过程:
1、刚创建StringBuilder时,底层创建一个字节数组,默认容量为16,值全为0:

0000000000000000

容量表示最多能装多少,长度表示实际装了多少,这是一个基本概念,别混淆了,接着往下。

2、当我们将"a"、“b”、"c"存入StringBuilder时,实际上存储的是其ASCII码:

9798990000000000000

这时候,容量仍然为16,长度为3。

3、上面显然长度不会超过容量,但是当要添加的字符超过了16个,就会扩容。

扩容:老容量 * 2 + 2

所以如果我们插入a~z共26个字符,容量为34,长度为26。

需要注意的是,老容量依旧为16,但如果插入的字符长度为36,也就是说直接超过了扩容后的大小,这时候容量就会变成实际的36(以实际长度为准)

了解知识后去扒一扒源码:
1、自己new一个StringBuilder对象,Ctrl+左键跟踪,可以看到StringBuilder继承了一个类,空参构造默认是将16传入作为容量的:
在这里插入图片描述
2、跟踪进入super,其创建了长度为16的字节数组:
在这里插入图片描述
3、接着回到上一层,Ctrl+F12查看StringBuilder中的append方法:
在这里插入图片描述
4、跟踪进入其super方法:
在这里插入图片描述
当str为空的时候,会返回appendNull方法,进入appendNull可以发现,返回了一个"null"的字符串:
在这里插入图片描述
5、Ctrl+Alt+"←"返回上一步,如果传入str不为空,则会将count+str.length()传入ensureCapacityInternal函数:
在这里插入图片描述
count表示当前的容器的长度,则count+len表示需要的最小容量,显然这个函数就是做扩容的。

6、跟踪ensureCapacityInternal函数:
在这里插入图片描述
这里会判断所需最小容量是否比老容量大,是的话说明老容量不够用需要进行扩容。

7、进入newCapacity查看扩容方法:
在这里插入图片描述
coder默认为0,则newLength方法中分别传入的是:

(老容量,需要扩容的大小,老容量+2)

8、跟踪进入newLength方法,可以看到,如果增长的幅度老容量+2来的小,那么新容量就是老容量 + 老容量 + 22 * 老容量 + 2,否则就是老容量+增长幅度也就是老容量 + 要新插入的字符串长度
在这里插入图片描述
剩下的处理就是防止一些异常情况出现,比如扩容后超出了SOFT_MAX_ARRAY_LENGTH的时候,那么就返回SOFT_MAX_ARRAY_LENGTH,一般情况也遇不到。

9、Ctrl+Alt+"←"返回,很直观的可以看到容量length是有上限的,不能超过int数的最大值:
在这里插入图片描述
10、继续返回,可以知道不仅要扩容,还要把之前容器中的值拷贝到扩容后的数组中去,这个底层想看就自己扒一扒:
在这里插入图片描述
11、继续返回,找到putStringAt,看函数名就大致能知道,这个函数的意思是在count位置再添加str:
在这里插入图片描述
最后修改一下count值就可以返回这个容器了。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

布布要成为最负责的男人

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值