性能文章>多次字符串相加一定要用StringBuilder而不用-吗?>

多次字符串相加一定要用StringBuilder而不用-吗?原创

2年前
739504

今天在写一个读取Java class File并进行分析的Demo时,偶然发现了下面这个场景(基于oracle jdk 1.8.0_144):

package test;

public class Test8 {

    String s1 = "111", s2 = "222", s3 = "333", s4 = "444";

    public String test() {
        return s1 + s2 + s3 + s4 + "5555" + "66666666666666666666666666" + "777" + new String("测试测试") + String.valueOf("test test") + "长字符串长字符串长字符串长字符串长字符串长字符串长字符串长字符串长字符串长字符串长字符串长字符串";
    }
}

这是一个很简单的类,只完成了字符串的 + 操作,我们查看对应生成的class文件的outline:

// class version 52.0 (52)
// access flags 0x21
public class test/Test8 {

  // compiled from: Test8.java

  // access flags 0x0
  Ljava/lang/String; s1

  // access flags 0x0
  Ljava/lang/String; s2

  // access flags 0x0
  Ljava/lang/String; s3

  // access flags 0x0
  Ljava/lang/String; s4

  // access flags 0x1
  public <init>()V
   L0
    LINENUMBER 3 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
   L1
    LINENUMBER 5 L1
    ALOAD 0
    LDC "111"
    PUTFIELD test/Test8.s1 : Ljava/lang/String;
    ALOAD 0
    LDC "222"
    PUTFIELD test/Test8.s2 : Ljava/lang/String;
    ALOAD 0
    LDC "333"
    PUTFIELD test/Test8.s3 : Ljava/lang/String;
    ALOAD 0
    LDC "444"
    PUTFIELD test/Test8.s4 : Ljava/lang/String;
    RETURN
   L2
    LOCALVARIABLE this Ltest/Test8; L0 L2 0
    MAXSTACK = 2
    MAXLOCALS = 1

  // access flags 0x1
  public test()Ljava/lang/String;
   L0
    LINENUMBER 8 L0
    NEW java/lang/StringBuilder
    DUP
    INVOKESPECIAL java/lang/StringBuilder.<init> ()V
    ALOAD 0
    GETFIELD test/Test8.s1 : Ljava/lang/String;
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    ALOAD 0
    GETFIELD test/Test8.s2 : Ljava/lang/String;
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    ALOAD 0
    GETFIELD test/Test8.s3 : Ljava/lang/String;
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    ALOAD 0
    GETFIELD test/Test8.s4 : Ljava/lang/String;
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    LDC "555566666666666666666666666666777"
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    NEW java/lang/String
    DUP
    LDC "\u6d4b\u8bd5\u6d4b\u8bd5"
    INVOKESPECIAL java/lang/String.<init> (Ljava/lang/String;)V
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    LDC "test test"
    INVOKESTATIC java/lang/String.valueOf (Ljava/lang/Object;)Ljava/lang/String;
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    LDC "\u957f\u5b57\u7b26\u4e32\u957f\u5b57\u7b26\u4e32\u957f\u5b57\u7b26\u4e32\u957f\u5b57\u7b26\u4e32\u957f\u5b57\u7b26\u4e32\u957f\u5b57\u7b26\u4e32\u957f\u5b57\u7b26\u4e32\u957f\u5b57\u7b26\u4e32\u957f\u5b57\u7b26\u4e32\u957f\u5b57\u7b26\u4e32\u957f\u5b57\u7b26\u4e32\u957f\u5b57\u7b26\u4e32"
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
    ARETURN
   L1
    LOCALVARIABLE this Ltest/Test8; L0 L1 0
    MAXSTACK = 4
    MAXLOCALS = 1
}

请注意这段:

// access flags 0x1
  public test()Ljava/lang/String;
   L0
    LINENUMBER 8 L0
    NEW java/lang/StringBuilder
    DUP
    INVOKESPECIAL java/lang/StringBuilder.<init> ()V
    ALOAD 0
    GETFIELD test/Test8.s1 : Ljava/lang/String;
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    ALOAD 0

我们可以看到,即便我们没有显式的使用StringBuilder,实际上编译器也会隐式的将我们的 + 运算符优化为StringBuilderappend()操作;另外,其中字符串常量的相加这里,也就是 "5555" + "66666666666666666666666666" + "777" 这里对应的操作是:

GETFIELD test/Test8.s4 : Ljava/lang/String;
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    LDC "555566666666666666666666666666777"

直接被合并在了一次(具体这是什么操作我不是很明白)

这时候我就想起来,原来一直被教导的“字符串相加一定要用StringBuilder而不要用 + ”真的正确吗?这个值得深思。


2017-01-22更新:

GETFIELD test/Test8.s4 : Ljava/lang/String;
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    LDC "555566666666666666666666666666777"

直接被合并在了一次(具体这是什么操作我不是很明白)

这里可能是编译器做了“公共子表达式消除”这个优化操作 (这里是错误的,请看后边的勘误)


2017-01-25更新:

在循环+=的情况下,编译器也会做优化工作的,但是IDE仍然会给出警告,不知道编译器的优化是否在所有情况下均会触发(有待继续学习)

public static void main(String[] args) {
    String s = "";
    for (int i = 0; i < 10000; i++) {
        int int_ = new Random().nextInt();
        s += int_;
    }
    System.out.println(s);
}

1612b6b7a730a9e8.png

1612b6b9c7da64cf.png

更新与勘误

2019-08-13 更新

当在循环中对字符串进行 += 操作时,会在每一次迭代中都创建一个StringBuilder,这种在循环内进行字符串 += 操作会被idea提示用 StringBuilder 替代的

package io.since1986.demo;

public class Test10 {

    public static void main(String[] args) {
        String s = "1111DDDDFFFGGGGG";
        for (int i = 0; i < 99; i++) {
            s += "3fghjl";
        }
        System.out.println(s);
    }
}

// class version 52.0 (52)
// access flags 0x21
public class io/since1986/demo/Test10 {

  // compiled from: Test10.java

  // access flags 0x1
  public <init>()V
   L0
    LINENUMBER 3 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
    RETURN
   L1
    LOCALVARIABLE this Lio/since1986/demo/Test10; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x9
  public static main([Ljava/lang/String;)V
   L0
    LINENUMBER 6 L0
    LDC "1111DDDDFFFGGGGG"
    ASTORE 1
   L1
    LINENUMBER 7 L1
    ICONST_0
    ISTORE 2
   L2
   FRAME APPEND [java/lang/String I]
    ILOAD 2
    BIPUSH 99
    IF_ICMPGE L3
   L4
    LINENUMBER 8 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;
    LDC "3fghjl"
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
    ASTORE 1
   L5
    LINENUMBER 7 L5
    IINC 2 1
    GOTO L2
   L3
    LINENUMBER 10 L3
   FRAME CHOP 1
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    ALOAD 1
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
   L6
    LINENUMBER 11 L6
    RETURN
   L7
    LOCALVARIABLE i I L2 L3 2
    LOCALVARIABLE args [Ljava/lang/String; L0 L7 0
    LOCALVARIABLE s Ljava/lang/String; L1 L7 1
    MAXSTACK = 2
    MAXLOCALS = 3
}

2019-08-15 更新

GETFIELD test/Test8.s4 : Ljava/lang/String;
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    LDC "555566666666666666666666666666777"

“5555” + “66666666666666666666666666” + “777” 直接被合并在了一块,这个操作叫 常量折叠

Java中,关于String类型的变量和常量做“+”运算时发生了什么?

点赞收藏
分类:标签:
since1986

爱编程、爱旅行、爱家人~

请先登录,感受更多精彩内容
快去登录吧,你将获得
  • 浏览更多精彩评论
  • 和开发者讨论交流,共同进步

为你推荐

【全网首发】(大表小技巧)有时候 2 小时的 SQL 操作,可能只要 1 分钟

【全网首发】(大表小技巧)有时候 2 小时的 SQL 操作,可能只要 1 分钟

一次java内存top res高排查记录

一次java内存top res高排查记录

JAVA中计算两个日期时间的差值竟然也有这么多门道

JAVA中计算两个日期时间的差值竟然也有这么多门道

干货!Java代码优化必知的30个小技巧!

干货!Java代码优化必知的30个小技巧!

Java 异步调用原理与实战

Java 异步调用原理与实战

没有二十年功力,写不出Thread.sleep(0)这一行“看似无用”的代码!

没有二十年功力,写不出Thread.sleep(0)这一行“看似无用”的代码!

4
0