这算标题党吗?不算!
本篇文章来讲一下最近发现的一个无聊的奇怪知识 - 字符串的链接
如何连接字符串
我们都知道都 Java 的世界中有多种方法可以链接字符串:
- +++
我们可以使用加号来连接字符串,形成一个新的字符串。
-
StringBuilder我们也可以通过创建StringBuilder这个类,然后调用append方法来进行连接。 -
StringBuffer线程安全的字符串连接方法,类似于StringBuilder。 -
奇淫巧技
一些鲜为人知的奇淫巧技。
字符串连接的问题
我们都知道,String 在 Java 世界中是不可变的。
如果你不知道可以在掘金查一下。本文不再赘述。
所以在大家进行字符串连接时 Java 编译器 偷偷帮你做了一些事。
偷梁换柱
众所周知,StringBuilder 是一个将字符串存入缓冲区的类,所以它可以优雅的进行字符串连接,从而避免了创造大量的 String 对象,造成内存的浪费。
JVM 就帮你偷偷做了这件事,它会帮你把字符串的连接替换成 StringBuilder。如下列代码所示:
public class SimpleStringConnect {
public static void main(String[] args) {
String a = "abcdefg";
String b = "qweqwfqwf";
System.out.println(a + b);
}
}
这是一个简单的字符串连接,让我们一起来看看编译器帮我们做了什么吧。下面是该代码的字节码文件:
// class version 52.0 (52)
// access flags 0x21
public class com/github/mattison/SimpleStringConnect {
// compiled from: SimpleStringConnect.java
// access flags 0x1
public <init>()V
L0
LINENUMBER 3 L0
ALOAD 0
INVOKESPECIAL java/lang/Object.<init> ()V
RETURN
L1
LOCALVARIABLE this Lcom/github/mattison/SimpleStringConnect; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1
// access flags 0x9
public static main([Ljava/lang/String;)V
L0 // 将第一个字符串存下
LINENUMBER 5 L0
LDC "abcdefg"
ASTORE 1
L1 // 将第二个字符串存下
LINENUMBER 6 L1
LDC "qweqwfqwf"
ASTORE 2
L2
LINENUMBER 7 L2
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
NEW java/lang/StringBuilder // 创建StringBuilder
DUP
INVOKESPECIAL java/lang/StringBuilder.<init> ()V
ALOAD 1
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder; // 调用 append 方法连接第一个字符串
ALOAD 2
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder; // 调用 append 方法连接第二个字符串
INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
L3
LINENUMBER 8 L3
RETURN
L4
LOCALVARIABLE args [Ljava/lang/String; L0 L4 0
LOCALVARIABLE a Ljava/lang/String; L1 L4 1
LOCALVARIABLE b Ljava/lang/String; L2 L4 2
MAXSTACK = 3
MAXLOCALS = 3
}
代码中的注释讲解了编译后的字节码变成了创建 StringBuilder 类调用 append的方法来代替创建多个 String类实现字符串的链接。
存在的问题及如何解决
问题
但是,编译器不是人,当我们把字符串放在循环里连接,他就会出现另一个问题。代码如下:
public class LoopStringConnect {
public static void main(String[] args) {
String a = "a";
for (int i = 0; i < 24; i++) {
a += i;
}
}
}
字节码如下:
// class version 52.0 (52)
// access flags 0x21
public class com/github/mattison/LoopStringConnect {
// compiled from: LoopStringConnect.java
// access flags 0x1
public <init>()V
L0
LINENUMBER 3 L0
ALOAD 0
INVOKESPECIAL java/lang/Object.<init> ()V
RETURN
L1
LOCALVARIABLE this Lcom/github/mattison/LoopStringConnect; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1
// access flags 0x9
public static main([Ljava/lang/String;)V
L0
LINENUMBER 5 L0
LDC "a"
ASTORE 1
L1
LINENUMBER 6 L1
ICONST_0
ISTORE 2
L2
FRAME APPEND [java/lang/String I]
ILOAD 2
BIPUSH 24
IF_ICMPGE L3
L4
LINENUMBER 7 L4
NEW java/lang/StringBuilder // 创建 StringBuilder
DUP
INVOKESPECIAL java/lang/StringBuilder.<init> ()V
ALOAD 1
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder; // 连接之前的字符串
ILOAD 2
INVOKEVIRTUAL java/lang/StringBuilder.append (I)Ljava/lang/StringBuilder;
INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String; // 将StringBuilder变成字符串后存储
ASTORE 1
L5
LINENUMBER 6 L5
IINC 2 1
GOTO L2 // 继续循环
L3
LINENUMBER 9 L3
FRAME CHOP 1
RETURN
L6
LOCALVARIABLE i I L2 L3 2
LOCALVARIABLE args [Ljava/lang/String; L0 L6 0
LOCALVARIABLE a Ljava/lang/String; L1 L6 1
MAXSTACK = 2
MAXLOCALS = 3
}
阅读该字节码,我们发现虽然 JVM 不会创建 String 类了,但是经过编译器的编译。每一次循环都会创建一个 StringBuilder 类。之前重复创建类导致的内存浪费情况依然存在。
解决
如果你使用的是 IntelliJ IDEA,那么它将会提示你使用 StringBuilder 来替换。
经过修改后的代码如下:
public class LoopStringConnect {
public static void main(String[] args) {
StringBuilder a = new StringBuilder("a");
for (int i = 0; i < 24; i++) {
a.append(i);
}
}
}
其字节码如下:
// class version 52.0 (52)
// access flags 0x21
public class com/github/mattison/LoopStringBuilderConnect {
// compiled from: LoopStringBuilderConnect.java
// access flags 0x1
public <init>()V
L0
LINENUMBER 3 L0
ALOAD 0
INVOKESPECIAL java/lang/Object.<init> ()V
RETURN
L1
LOCALVARIABLE this Lcom/github/mattison/LoopStringBuilderConnect; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1
// access flags 0x9
public static main([Ljava/lang/String;)V
L0
LINENUMBER 5 L0
NEW java/lang/StringBuilder // 创建 StringBuilder
DUP
LDC "a"
INVOKESPECIAL java/lang/StringBuilder.<init> (Ljava/lang/String;)V
ASTORE 1
L1
LINENUMBER 6 L1
ICONST_0
ISTORE 2
L2
FRAME APPEND [java/lang/StringBuilder I]
ILOAD 2
BIPUSH 24
IF_ICMPGE L3
L4
LINENUMBER 7 L4
ALOAD 1
ILOAD 2
INVOKEVIRTUAL java/lang/StringBuilder.append (I)Ljava/lang/StringBuilder; // 调用 append 方法
POP
L5
LINENUMBER 6 L5
IINC 2 1
GOTO L2
L3
LINENUMBER 9 L3
FRAME CHOP 1
RETURN
L6
LOCALVARIABLE i I L2 L3 2
LOCALVARIABLE args [Ljava/lang/String; L0 L6 0
LOCALVARIABLE a Ljava/lang/StringBuilder; L1 L6 1
MAXSTACK = 3
MAXLOCALS = 3
}
后续的优化
这些东西我本来就知道啊。难道我看了个寂寞? -- 网友XXXX
不不不,前面是铺垫,正式介绍才刚开始。
虽然我们可以依靠 StringBuilder 来解决字符串循环连接的问题。但是编译器编写人员本着 “造福人类” 的想法。终在 Java 9 时使用 InvokeDynamic 调用 StringConcatFactory.makeConcatWithConstants 方法进行字符串拼接优化。
具体原理可以参考文末的一些比较好的参考资料。
总结
无聊的奇怪知识。