"a + b 会被编译器自动优化成 new StringBuilder().append(a).append(b)"是几乎每本 Java 教材都写过的知识点。用 javap 真实反编译字节码,看看现在的 JDK 到底生成了什么。
上一篇:异常真实有多贵:关掉栈追踪只快了 1.3 倍,不是传说中的 10 倍
用 javac 编译,再用 javap -c 反汇编,直接看编译器真实生成的字节码,不猜。
ConcatDemo.java(测试代码) static String concatPlus(String a, String b, int c) { return a + b + c; } static String concatBuilder(String a, String b, int c) { return new StringBuilder().append(a).append(b).append(c).toString(); }
static java.lang.String concatPlus(java.lang.String, java.lang.String, int);
Code:
0: aload_0
1: aload_1
2: iload_2
3: invokedynamic #7, 0 // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;Ljava/lang/String;I)Ljava/lang/String;
8: areturn
static java.lang.String concatBuilder(java.lang.String, java.lang.String, int);
Code:
0: new #11 // class java/lang/StringBuilder
3: dup
4: invokespecial #13 // Method java/lang/StringBuilder."<init>":()V
7: aload_0
8: invokevirtual #14 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
...
19: invokevirtual #21 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
22: areturn
真实反汇编结果很直接:a + b + c 这行代码编译出来只有 4 条指令,核心是一条 invokedynamic,调用的是 makeConcatWithConstants——字节码层面完全没有出现 new StringBuilder。反倒是我自己手写的 concatBuilder 方法,才是真实生成 new StringBuilder 指令的那一个。课本里教的"+ 拼接会被优化成 StringBuilder"这个说法,对 Java 8 及更早版本是真实成立的,但 Java 9 引入 invokedynamic 字符串拼接(JEP 280)之后,这个具体的编译细节已经不再成立了。
另一个常见说法是"循环体里每次 + 拼接都会 new 一个 StringBuilder"——反编译循环代码看看是不是这样。
ConcatDemo.java(循环拼接) static String concatLoop(int n) { String s = ""; for (int i = 0; i < n; i++) { s = s + i; } return s; }
5: iload_2 6: iload_0 7: if_icmpge 24 10: aload_1 11: iload_2 12: invokedynamic #27, 0 // InvokeDynamic #1:makeConcatWithConstants:(Ljava/lang/String;I)Ljava/lang/String; 17: astore_1 18: iinc 2, 1 21: goto 5真实确认:循环体里每一轮也是一条 invokedynamic,同样没有 new StringBuilder 字样——但这不代表循环里用 + 拼接没代价,只是代价换了个不那么直观的形式(见下面的真实计时)
字节码层面看不到 StringBuilder 了,但每一轮 invokedynamic 调用背后,makeConcatWithConstants 的引导方法依然要为这次拼接分配一段新内存、拷贝旧内容——这部分开销字节码反汇编看不出来,得靠真实计时。
n = 60000, 两种结果长度相同 = true 循环里 s = s + i 耗时 = 737ms 显式 StringBuilder 耗时 = 1ms 倍数 = 401.2
第二次独立运行倍数是 410.2,结果稳定。原因不是"每次多 new 一个 StringBuilder 对象"这种字节码层面能直接看到的东西,而是每一轮 makeConcatWithConstants 引导方法内部依然要按当前拼接结果的长度重新分配一块内存、把旧字符串内容整体搬过去——这是 O(n) 级别的字符串本身固有的不可变特性决定的,和它编译成什么指令无关。显式的 StringBuilder 是复用同一个底层数组一路 append,不存在这个重复搬运的问题。
每一行都是真实反编译或真实运行这段代码得到的结果。