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

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

2年前
728203

今天在写一个读取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类型的变量和常量做“+”运算时发生了什么?

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

为你推荐

不起眼,但是足以让你收获的JVM内存案例
今天的这个案例我觉得应该会让你涨姿势吧,不管你对JVM有多熟悉,看到这篇文章,应该还是会有点小惊讶的,不过我觉得这个案例我分享出来,是想表达不管多么奇怪的现象请一定要追究下去,会让你慢慢变得强大起来,
字符串字面量长度是有限制的
前言 偶然在一次单元测试中写了一个非常长的字符串字面量。 正文 在一次单元测试中,我写了一个很长的字符串字面量,大概10万个字符左右,编译时,编译器给出了异常告警 `java: constant
多次字符串相加一定要用StringBuilder而不用-吗?
今天在写一个读取Java class File并进行分析的Demo时,偶然发现了下面这个场景(基于oracle jdk 1.8.0_144): ``` package test; public c
如何通过反射获得方法的真实参数名(以及扩展研究)
前段时间,在做一个小的工程时,遇到了需要通过反射获得方法真实参数名的场景,在这里我遇到了一些小小的问题,后来在部门老大的指导下,我解决了这个问题。通过解决这个问题,附带着我了解到了很多新的知识,我觉得
谨防JDK8重复类定义造成的内存泄漏
概述 如今JDK8成了主流,大家都紧锣密鼓地进行着升级,享受着JDK8带来的各种便利,然而有时候升级并没有那么顺利?比如说今天要说的这个问题。我们都知道JDK8在内存模型上最大的改变是,放弃了Perm
JDK13 GA发布:5大特性解读
JDK13 GA版本 5大新特性如下: 350: Dynamic CDS Archives 351: ZGC: Uncommit Unused Memory 353: Reimplement the
译:谁是 JDK8 中最快的 GC
我们都知道 OpenJDK8 有好几个垃圾回收算法,比如 ParallelGC,CMS,还有 G1,那么哪个才是最快的?如果 GC 算法从 Java8 中默认的 ParallelGC 切换到 G1 会
高吞吐、低延迟 Java 应用的 GC 优化实践
本篇原文作者是 LinkedIn 的 Swapnil Ghike,这篇文章讲述了 LinkedIn 的 Feed 产品的 GC 优化过程,虽然文章写作于 April 8, 2014,但其中的很多内容和