奇怪知识 : 你还在使用 StringBuilder 连接字符串吗?

688 阅读4分钟

这算标题党吗?不算!

本篇文章来讲一下最近发现的一个无聊的奇怪知识 - 字符串的链接

如何连接字符串

我们都知道都 Java 的世界中有多种方法可以链接字符串:

  1. +++

我们可以使用加号来连接字符串,形成一个新的字符串。

  1. StringBuilder 我们也可以通过创建 StringBuilder 这个类,然后调用 append 方法来进行连接。

  2. StringBuffer 线程安全的字符串连接方法,类似于 StringBuilder

  3. 奇淫巧技

一些鲜为人知的奇淫巧技。

字符串连接的问题

我们都知道,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 来替换。

image.png

经过修改后的代码如下:

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 方法进行字符串拼接优化。

具体原理可以参考文末的一些比较好的参考资料。

总结

无聊的奇怪知识。

相关链接

本文相关代码库

JAVA9 String 新特性 作者:河海哥yyds