👨🎓作者简介:一位大四、研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:
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
---|
容量表示最多能装多少,长度表示实际装了多少,这是一个基本概念,别混淆了,接着往下。
2、当我们将"a"、“b”、"c"存入StringBuilder时,实际上存储的是其ASCII码:
97 | 98 | 99 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
---|
这时候,容量仍然为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
来的小,那么新容量就是老容量 + 老容量 + 2
即2 * 老容量 + 2
,否则就是老容量+增长幅度
也就是老容量 + 要新插入的字符串长度
:
剩下的处理就是防止一些异常情况出现,比如扩容后超出了SOFT_MAX_ARRAY_LENGTH的时候,那么就返回SOFT_MAX_ARRAY_LENGTH,一般情况也遇不到。文章来源:https://www.toymoban.com/news/detail-829264.html
9、Ctrl+Alt+"←"
返回,很直观的可以看到容量length是有上限的,不能超过int数的最大值:
10、继续返回,可以知道不仅要扩容,还要把之前容器中的值拷贝到扩容后的数组中去,这个底层想看就自己扒一扒:
11、继续返回,找到putStringAt,看函数名就大致能知道,这个函数的意思是在count位置再添加str:
最后修改一下count值就可以返回这个容器了。文章来源地址https://www.toymoban.com/news/detail-829264.html
到了这里,关于JAVASE进阶:强推!源码分析——字符串拼接底层原理、StringBuilder底层原理的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!