“哥,你让我看的《Java 开发手册》上有这么一段内容:循环体内,拼接字符串最好使用 StringBuilder 的 方法,而不是 + 号操作符。这是为什么呀?”三妹疑惑地问。
“其实这个问题,我们之前在 StringBuilder 时已经聊过了。”我慢吞吞地回答道,“不过,三妹,哥今天来给你深入地讲讲。”
PS:三妹能在学习的过程中不断地发现问题,让我感到非常的开心。其实很多时候,我们不应该只是把知识点记在心里,还应该问一问自己,到底是为什么,只有迈出去这一步,才能真正的成长起来。
“+ 号操作符其实被 Java 在编译的时候重新解释了,换一种说法就是,+ 号操作符是一种语法糖,让字符串的拼接变得更简便了。”一边给三妹解释,我一边在 Intellij IDEA 中敲出了下面这段代码。
在 Java 8 的环境下,使用 反编译字节码后(字节码和 javap 我们会在 JVM 中详细讲,这里可以硬着头皮瞄一眼),可以看到以下内容:
(如果你之前没有了解过字节码指令,可能会有一点压力,不过,不用担心,我们稍微解释一下就懂了)
“你看,三妹,这里有一个 new 关键字,并且 class 类型为 。”我指着标号为 9 的那行对三妹说,“这意味着新建了一个 StringBuilder 的对象。”
“然后看标号为 17 的这行,是一个 invokevirtual 指令,用于调用对象的方法,也就是 StringBuilder 对象的 方法。”
“也就意味着把 chenmo("沉默")这个字符串添加到 StringBuilder 对象中了。”
“再往下看,标号为 21 的这行,又调用了一次 方法,意味着把 wanger("王二")这个字符串添加到 StringBuilder 对象中了。”
换成 Java 代码来表示的话,大概是这个样子:
“哦,原来编译的时候把“+”号操作符替换成了 StringBuilder 的 方法啊。”三妹恍然大悟。
“是的,不过到了 Java 9(不是长期支持版本,所以我会拿 Java 11 来演示),情况发生了一些改变,同样的代码,字节码指令完全不同了。”我说。
同样的代码,在 Java 11 的环境下,字节码指令是这样的:
看标号为 29 的这行,字节码指令为 ,该指令允许由应用级的代码来决定方法解析,所谓的应用级的代码其实是一个方法——被称为引导方法(Bootstrap Method),简称 BSM,BSM 会返回一个 CallSite(调用点) 对象,这个对象就和 指令链接在一起。以后再执行这条 指令时就不会创建新的 CallSite 对象。CallSite 其实就是一个 MethodHandle(方法句柄)的 holder,指向一个调用点真正执行的方法——此时就是 方法。
“哥,你别再说了,再说我就听不懂了。”三妹打断了我的话。
“好吧,总之就是 Java 9 以后,JDK 用了另外一种方法来动态解释 + 号操作符,具体的实现方式在字节码指令层面已经看不到了,所以我就以 Java 8 来继续讲解吧。”
这里我们再多讲一点,如果是下面这段代码:
号操作符又是如何完成拼接呢?
同样可以来通过 看一下字节码指令:

从上图中可以看到, 号操作符被编译成了 的 方法。
那如果是这样的代码:
号操作符又是如何完成拼接呢?
同样可以来通过 看一下字节码指令:

StringBuilder 不见了?这是为什么呢?
这是因为 + 连接操作的两个操作数都是编译时常量(一个是字面量整数 11,另一个是空字符串 ""),所以编译器能够在编译时就完成这个字符串连接操作。
也就是说,字符串连接 11 + "" 被编译器优化处理了,编译器在编译阶段就将其解析为了字符串常量 "11"。
“再回到《Java 开发手册》上的那段内容:
循环体内,拼接字符串最好使用 StringBuilder 的 方法,而不是 + 号操作符。原因就在于循环体内如果用 + 号操作符的话,就会产生大量的 StringBuilder 对象,不仅占用了更多的内存空间,还会让 Java 虚拟机不停的进行垃圾回收,从而降低了程序的性能。”
更好的写法就是在循环的外部新建一个 StringBuilder 对象,然后使用 方法将循环体内的字符串添加进来:
来做个小测试。
第一个,for 循环中使用”+”号操作符。
第二个,for 循环外部新建 StringBuilder,循环体内使用 方法。
“这两个小测试分别会耗时多长时间呢?三妹你来运行下。”
“哇,第一个小测试的执行时间是 6212 毫秒,第二个只用了不到 1 毫秒,差距也太大了吧!”三妹说。
“是的,这下明白了原因吧?”我说。
“是的,哥,原来如此。”
“好了,三妹,来看一下 StringBuilder 类的 方法的源码吧!”
这 3 行代码其实没啥看的。我们来看父类 AbstractStringBuilder 的 方法:
1)判断拼接的字符串是不是 null,如果是,当做字符串“null”来处理。 方法的源码如下:
2)获取字符串的长度。
3) 方法的源码如下:
由于字符串内部是用数组实现的,所以需要先判断拼接后的字符数组长度是否超过当前数组的长度,如果超过,先对数组进行扩容,然后把原有的值复制到新的数组中。
4)将拼接的字符串 str 复制到目标数组 value 中。
5)更新数组的长度 count。
“除了可以使用 + 号操作符,StringBuilder 的 方法,还有其他的字符串拼接方法吗?”三妹问。
“有啊,比如说 String 类的 方法,有点像 StringBuilder 类的 方法。”
可以来看一下 方法的源码。
1)如果拼接的字符串的长度为 0,那么返回拼接前的字符串。
2)将原字符串的字符数组 value 复制到变量 buf 数组中。
3)把拼接的字符串 str 复制到字符数组 buf 中,并返回新的字符串对象。
我一行一行地给三妹解释着。
“和 号操作符相比, 方法在遇到字符串为 null 的时候,会抛出 NullPointerException,而“+”号操作符会把 null 当做是“null”字符串来处理。”
如果拼接的字符串是一个空字符串(""),那么 concat 的效率要更高一点,毕竟不需要 对象。
如果拼接的字符串非常多, 的效率就会下降,因为创建的字符串对象越来越多。
“还有吗?”三妹似乎对字符串拼接很感兴趣。
“有,当然有。”
String 类有一个静态方法 ,可以这样来使用。
第一个参数为字符串连接符,比如说:
输出结果为:。
来看一下 join 方法的源码:
里面新建了一个叫 StringJoiner 的对象,然后通过 for-each 循环把可变参数添加了进来,最后调用 方法返回 String。
“实际的工作中, 的 方法也经常用来进行字符串拼接。”
该方法不用担心 NullPointerException。
来看一下源码:
内部使用的仍然是 StringBuilder。
“好了,三妹,关于字符串拼接的知识点我们就讲到这吧。注意 Java 9 以后,对 + 号操作符的解释和之前发生了变化,字节码指令已经不同了,等后面你学了字节码指令后我们再详细地讲一次。”我说。
“嗯,哥,你休息吧,我把这些例子再重新跑一遍。”三妹说。
GitHub 上标星 10000+ 的开源知识库《二哥的 Java 进阶之路》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:太赞了,GitHub 上标星 10000+ 的 Java 教程
微信搜 沉默王二 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 222 即可免费领取。

版权声明:
本文来源网络,所有图片文章版权属于原作者,如有侵权,联系删除。
本文网址:https://www.bianchenghao6.com/java-jiao-cheng/12791.html