专业编程基础技术教程

网站首页 > 基础教程 正文

Java修炼终极指南:12. 字符串连接与StringBuilder

ccvgpt 2024-09-08 12:45:29 基础教程 14 ℃


查看以下简单的字符串连接:

Java修炼终极指南:12. 字符串连接与StringBuilder

Bash
String str1 = "I love";  
String str2 = "Java";  
String str12 = str1 + " " + str2;


我们知道String类是不可变的(创建的String不能被修改)。这意味着创建str12需要一个中间字符串来表示str1与空格的连接。因此,在str12创建之后,我们知道str1 + " "只是噪音或垃圾,因为我们无法进一步引用它。

在这种情况下,建议使用StringBuilder,因为它是一个可变类,我们可以向其中追加字符串。因此,以下声明就产生了:在Java中,不要使用"+"运算符来连接字符串!使用StringBuilder,它要快得多。你听过这个声明吗?我确定你听过,尤其是如果你的应用还在JDK 8或更早的版本上运行。好吧,这不是一个神话,而且在某个时间点它确实是真的,但在智能编译器的时代,它仍然是一个有效的声明吗?

例如,考虑以下两个代码片段,它们表示一个简单的字符串连接:


图1.10 - 字符串连接与StringBuilder

在JDK 8中,哪种方法(来自图1.10)更好?

JDK 8

让我们检查这两个代码片段产生的字节码(使用`javap -c -p`或Apache Commons BCEL(字节码工程库);我们使用了BCEL)。`concatViaPlus()`的字节码如下所示:


图1.11 - JDK 8中concatViaPlus()的字节码

JDK 8编译器足够智能,在底层使用StringBuilder来通过"+"运算符塑造我们的连接。如果你检查从`concatViaStringBuilder()`生成的字节码(为了简洁起见,这里省略了),你会发现它与图1.11大致相似。因此,在JDK 8中,编译器知道何时以及如何通过StringBuilder优化字节码。换句话说,显式使用StringBuilder并不比通过"+"运算符进行简单连接带来显著的好处。有许多简单的情况都适用这一说法。基准测试怎么说呢?查看结果:


图1.12 - JDK 8,基准测试concatViaPlus() vs. concatViaStringBuilder()

显然,通过"+"运算符的连接赢得了这场比赛。让我们在JDK 11上重复这个逻辑。

JDK 11

JDK 11为`concatViaPlus()`生成了以下字节码:


图1.13 - JDK 11中concatViaPlus()的字节码

我们立即观察到这里有很大的不同。这次,连接是通过调用invokedynamic(这是一个动态调用)来完成的,它作为我们代码的委托者。在这里,它将代码委托给makeConcatWithConstants(),这是StringConcatFactory类的一个方法。虽然你可以在JDK文档中找到这个,但请注意,这个类API不是为了直接调用而创建的。这个类特别创建和设计用于为invokedynamic指令服务引导方法。在继续之前,让我们注意一个你应该考虑的重要事项。

invokedynamic委托/传递我们的连接代码,由不是字节码一部分的代码解决(这就是为什么我们不能看到实际解决我们代码的代码(指令))。这是非常强大的,因为它允许Java工程师继续优化连接逻辑的过程,而我们只需要简单地迁移到下一个JDK版本就可以利用它。代码甚至不需要重新编译就可以利用进一步的优化。

有趣的事实:indify术语来自invokedynamic,也称为indy,因此是indify。它在JDK 7中引入,并在JDK 8 lambda实现中使用。由于这个指令非常有用,它成为许多其他事情的解决方案,包括在JDK 9中引入的JEP 280:Indify String Concatenation。我在这里更喜欢使用JDK 11,但这个特性从JDK 9+开始可用,所以你可以在JDK 17或20等版本上尝试一下。

简而言之,invokedynamic的工作原理如下:

1. 编译器在连接点附加一个invokedynamic调用

2. invokedynamic调用首先执行引导方法makeConcat[WithConstants]

3. invokedynamic方法调用makeConcat[WithConstants],这是一个引导方法,旨在调用实际负责连接的代码

4. makeConcat[WithConstants]使用内部策略来确定解决连接的最佳方法

5. 调用最适合的方法,并执行连接逻辑

通过这种方式,JEP 280自JDK 10、11、12、13等版本以来增加了很大的灵活性,可以使用不同的策略和方法来以最适合我们上下文的方式处理字符串连接。

那么,`concatViaStringBuilder()`的字节码呢?这个方法没有利用invokedynamic(它依赖于经典的invokevirtual指令),如下所示:

图1.14 - JDK 11中concatViaStringBuilder()的字节码

我确信你很好奇哪种字节码性能更好,所以这里是结果:


1.15 - JDK 11,基准测试concatViaPlus() vs. concatViaStringBuilder()

这些基准测试的结果是在配备Windows 10的Intel(R) Core(TM) i7-3612QM CPU @ 2.10GHz机器上获得的,但请随意在不同的机器和不同的JDK版本上进行测试,因为结果高度依赖于机器。再次,concatViaPlus()赢得了这场比赛。在捆绑的代码中,你可以找到这个例子的完整代码。此外,你还会找到用于检查字节码和通过"+"运算符和StringBuilder在循环中基准测试连接的代码。给它一个机会吧!

最近发表
标签列表