Java8中,对字符串拼接的一些思考

引子:

工作项目开发已经差不多了,于是有时间自学一些东西。基础还是很重要的,开始看《Thinking In Java》第四版。看到String这里的时候,书中提到了“+”与“StringBuilder”的区别。但是该书该版是基于JDK5的,那么,对于JDK8,又是如何呢。下面,我将通过一个示例,进行探讨一下。

范例:

下面,是我们的Java Demo,将使用三种方式来进行一个字符串的操作。

public class SBTest {
    public static void main(String[] args) {
        // 案例一,直接 + 拼接
        String a = "aa" + "ba" + "ca";
        System.out.println(a);

        // 案例二,间接 + 拼接
        String c = "gc";
        c = c + "hc";
        c = c + "ic";
        System.out.println(c);

        // 案例三,StringBuilder拼接
        StringBuilder sbb = new StringBuilder();
        sbb.append("db").append("eb").append("fb");
        System.out.println(sbb.toString());

        // 案例四,+ 循环拼接
        String d = "";
        for (int i = 0; i < 3; i++) {
            d += "d" + i;
        }
        System.out.println(d);

        // 案例五,StringBuilder 循环拼接
        StringBuilder sbe = new StringBuilder();
        for (int i = 0; i < 3; i++) {
            sbe.append("e").append(i);
        }
        System.out.println(sbe.toString());
    }
}

反编译:

下面,我们使用JDK自带的反编译工具,对上面这段代码编译后的class文件进行反编译。执行命令:

 javap -c SBTest.class

反编译得到以下结果:

Compiled from "SBTest.java"
public class se.learn.demos.thinkinginjava.demo01.SBTest {
  public se.learn.demos.thinkinginjava.demo01.SBTest();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: ldc           #2                  // String aabaca
       2: astore_1
       3: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
       6: aload_1
       7: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      10: ldc           #5                  // String gc
      12: astore_2
      13: new           #6                  // class java/lang/StringBuilder
      16: dup
      17: invokespecial #7                  // Method java/lang/StringBuilder."":()V
      20: aload_2
      21: invokevirtual #8                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      24: ldc           #9                  // String hc
      26: invokevirtual #8                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      29: invokevirtual #10                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      32: astore_2
      33: new           #6                  // class java/lang/StringBuilder
      36: dup
      37: invokespecial #7                  // Method java/lang/StringBuilder."":()V
      40: aload_2
      41: invokevirtual #8                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      44: ldc           #11                 // String ic
      46: invokevirtual #8                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      49: invokevirtual #10                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      52: astore_2
      53: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
      56: aload_2
      57: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      60: new           #6                  // class java/lang/StringBuilder
      63: dup
      64: invokespecial #7                  // Method java/lang/StringBuilder."":()V
      67: astore_3
      68: aload_3
      69: ldc           #12                 // String db
      71: invokevirtual #8                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      74: ldc           #13                 // String eb
      76: invokevirtual #8                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      79: ldc           #14                 // String fb
      81: invokevirtual #8                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      84: pop
      85: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
      88: aload_3
      89: invokevirtual #10                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      92: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      95: ldc           #15                 // String
      97: astore        4
      99: iconst_0
     100: istore        5
     102: iload         5
     104: iconst_3
     105: if_icmpge     141
     108: new           #6                  // class java/lang/StringBuilder
     111: dup
     112: invokespecial #7                  // Method java/lang/StringBuilder."":()V
     115: aload         4
     117: invokevirtual #8                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
     120: ldc           #16                 // String d
     122: invokevirtual #8                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
     125: iload         5
     127: invokevirtual #17                 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
     130: invokevirtual #10                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
     133: astore        4
     135: iinc          5, 1
     138: goto          102
     141: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
     144: aload         4
     146: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
     149: new           #6                  // class java/lang/StringBuilder
     152: dup
     153: invokespecial #7                  // Method java/lang/StringBuilder."":()V
     156: astore        5
     158: iconst_0
     159: istore        6
     161: iload         6
     163: iconst_3
     164: if_icmpge     186
     167: aload         5
     169: ldc           #18                 // String e
     171: invokevirtual #8                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
     174: iload         6
     176: invokevirtual #17                 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
     179: pop
     180: iinc          6, 1
     183: goto          161
     186: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
     189: aload         5
     191: invokevirtual #10                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
     194: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
     197: return
}

我们不需要明白上文中每一条指令的具体含义,但还是大概能看懂一些。建议大家将以上内容复制到文本编辑器中,跟随下文的思路,一起做对比。下面我将进行部分对比。

案例一:

首先,我们看第一种拼接方式,使用 “+” 直接进行拼接。我们看其对应的反编译结果:

0: ldc           #2                  // String aabaca
2: astore_1
3: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
6: aload_1
7: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V

我们可以看到,虽然我们是使用了“+”进行拼接,但是,从其反编译结果来看,在从 .java 文件编译到 .class文件的时候,编译器就已经自动为我们进行了优化,直接拿到了一个拼装完成的字符串。

为了验证这个结果,我们使用 Intellij IDEA 反编译我们的 .class文件,我们得到以下内容:

public static void main(String[] args) {
    String a = "aabaca";
    System.out.println(a);
    // 其他内容省略
}

我们可以看到,变量 a 被直接赋予了"aabaca"值,并没有经过拼接,这也验证了我们的结论。

案例二:

下面,我们看第二种拼接方式,使用“+”间接拼接,我们看反编译的结果:

10: ldc           #5                  // String gc
12: astore_2
13: new           #6                  // class java/lang/StringBuilder
16: dup
17: invokespecial #7                  // Method java/lang/StringBuilder."":()V
20: aload_2
21: invokevirtual #8                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
24: ldc           #9                  // String hc
26: invokevirtual #8                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
29: invokevirtual #10                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
32: astore_2
33: new           #6                  // class java/lang/StringBuilder
36: dup
37: invokespecial #7                  // Method java/lang/StringBuilder."":()V
40: aload_2
41: invokevirtual #8                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
44: ldc           #11                 // String ic
46: invokevirtual #8                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
49: invokevirtual #10                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
52: astore_2
53: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
56: aload_2
57: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V

这里,我们尝试理解一下这些指令。

第10行,将一个字符串变量 “gc” 加载到操作数栈,此时“gc”变量位于栈顶;
第12行,弹出栈顶的“gc”变量,并将其存储到 index 为 2 的临时变量中;
第13行,new 指令,创建了一个StringBuilder 对象;
第17行,初始化刚刚创建的StringBuilder 对象;
第20行,将 index 为 2 的临时变量(即刚刚加载的“gc”字符串)压入操作数栈;
第21行,刚才创建的StringBuilder对象执行apend()方法,参数即为刚刚加载的 “gc” 字符串变量( 即 .apend("gc") );
第24行,将另一个字符串 “hc” 加载到操作数栈;
第26行,还是之前的StringBuilder对象执行apend("hc"),将“hc”字符串连接上;
第29行,该StringBuilder对象执行toString()方法,得到的字符串将会被自动压入操作数栈中;
第32行,弹出栈顶的刚刚toString()得到的字符串变量,并将其存储到 index 为 2 的临时变量中;

第33行,new指令,新创建了一个StringBuilder对象;
第37行,初始化刚刚创建的StringBuilder对象;
第40行,将 index 为 2 的临时变量(即上面toString()得到的字符串)压入操作数栈;
第41行,刚刚新创建的StringBuilder对象执行apend(【上面toString()得到的字符串】)方法;
第44行,将一个字符串变量 “ic” 加载到操作数栈,此时“ic”变量位于栈顶;
第46行,刚才的StringBuilder对象执行apend("ic"),将“ic”字符串连接上;
第49行,该StringBuilder对象执行toString()方法,得到的字符串将会被自动压入操作数栈中;
第52行,弹出栈顶的刚刚toString()得到的字符串变量,并将其存储到 index 为 2 的临时变量中;
第53-57行,输出。

上面是我的对这些指令的理解。值得一提的是,反编译出的第一列的意义貌似并不是代码行数,不过为了方便说明,这里我们假定其意义就是代码行数。

仔细理解后,我们再看这部分的Java代码:

String c = "gc";
c = c + "hc";
c = c + "ic";
System.out.println(c);

在编译后,意思(从理解上)等价于:

// 将 "gc" 变量加入操作数栈
String c = "gc";
// 执行到第二行代码的时候,创建一个StringBuilder
StringBuilder sbc1 = new StringBuilder();
// 先 apend() 之前的 "gc" 变量,再apend() 加号之后的 "hc" 变量;
sbc1.append("gc");
sbc1.append("hc");

/* 值得一提的是,从反编译来看,这里并没有用临时变量来承载toString()结果的这样一个操作
String gcs = sbc1.toString();*/

// 到这里,第二行代码执行完毕

// 执行第三行代码,先创建一个StringBuilder
StringBuilder sbc2 = new StringBuilder();
// 把上一个StringBuilder toString()方法得到的内容 apend()上
sbc2.append(sbc1.toString());
// 然后 apend()上加号后面的 "ic"变量
sbc2.append("ic");
// 最后,输出
System.out.println(sbc2.toString());

很显然,这种方式拼接字符串,很耗性能,要不断创建StringBuilder对象,无形之中也增加了GC的负担。

案例三:

下面是,我们自己声明一个StringBuilder来进行字符串拼接。我们看其反编译的内容:

60: new           #6                  // class java/lang/StringBuilder
63: dup
64: invokespecial #7                  // Method java/lang/StringBuilder."":()V
67: astore_3
68: aload_3
69: ldc           #12                 // String db
71: invokevirtual #8                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
74: ldc           #13                 // String eb
76: invokevirtual #8                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
79: ldc           #14                 // String fb
81: invokevirtual #8                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
84: pop
85: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
88: aload_3
89: invokevirtual #10                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
92: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V

这里,有了上面案例二对反编译结果的分析,这里就简单多了:

第60行,new 指令,创建了一个StringBuilder 对象;
第63行,初始化刚刚创建的StringBuilder 对象;
第67行,弹出栈顶的变量,并将其存储到 index 为 3 的临时变量中;
第68行,将 index 为 3 的临时变量压入操作数栈;
第69行,将一个字符串 “db” 加载到操作数栈;
第71行,刚才创建的StringBuilder对象执行apend("db")方法;
第74行,将一个字符串 “eb” 加载到操作数栈;
第76行,刚才创建的StringBuilder对象执行apend("eb")方法;
第79行,将一个字符串 “fb” 加载到操作数栈;
第81行,刚才创建的StringBuilder对象执行apend("fb")方法;
第84-92行,输出。

Ps:
第84行,pop指令,从栈顶弹出一个字长的内容。
第85行,getstatic指令,访问System类的静态属性:PrintStream out 对象;

我们可以看到,当我们手动声明一个StringBuilder对象的时候,从其反编译的结果来看,其反编译后的语义和我们Java代码的语义差不多,从头到尾只有一个StringBuilder对象在执行apend()操作,相对而言,这种方式拼接字符串比案例二效率要高很多。

案例四:

案例四、案例五与前面不同的是,接下来两个案例中,都涉及到了循环。下面,我们看看案例四的反编译结果:

 95: ldc           #15                 // String
 97: astore        4
 99: iconst_0
100: istore        5
102: iload         5
104: iconst_3
105: if_icmpge     141
108: new           #6                  // class java/lang/StringBuilder
111: dup
112: invokespecial #7                  // Method java/lang/StringBuilder."":()V
115: aload         4
117: invokevirtual #8                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
120: ldc           #16                 // String d
122: invokevirtual #8                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
125: iload         5
127: invokevirtual #17                 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
130: invokevirtual #10                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
133: astore        4
135: iinc          5, 1
138: goto          102
141: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
144: aload         4
146: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V

同样,我们对反编译结果分析一下:

第95行,将一个字符串变量 “” 加载到操作数栈;
第97行,astore指令,其操作数为 4 。即弹出栈顶的变量,并将其存储到 index 为 4 的临时变量中;
第99行,将int类型值 0 压入栈
第100行,istore指令,其操作数为 5 。从栈中弹出int类型值,然后将其保存到index为 5 的局部变量中;
第102行,iload指令,其操作数为 5 。将index为 5 的int类型的局部变量压入栈;
第104行,将int类型值 3 压入栈;
第105行,if_icmpge指令,其操作数为 141 。意思是,如果JVM操作数栈的整数对比大于等于(即当此时栈中的第一个元素 0 >= 栈中的第二个元素 3)成立时,则跳到第141行;

第108行,new指令,新创建了一个StringBuilder对象;
第112行,初始化刚刚创建的StringBuilder对象;
第115行,aload指令,其操作数为 4 。将位置为 4 的对象引用局部变量压入操作数栈;
第117行,刚创建的StringBuilder对象执行apend()方法,将之前的空字符串 “” 连接上;
第120行,将一个字符串变量 “d” 加载到操作数栈;
第122行,上面的StringBuilder对象执行apend("d");
第125行,iload指令,其操作数为 5 。将index为 5 的int类型的局部变量压入栈;
第127行,上面的StringBuilder对象执行apend()方法,将刚刚拿到的int类型局部变量拼接到字符串中。对应apend(i);
第130行,该StringBuilder对象执行toString()方法,得到的字符串将会被自动压入操作数栈中;
第133行,astore指令,其操作数为 4 。即弹出栈顶的变量,并将其存储到 index 为 4 的临时变量中;
第135行,iinc指令,操作数为 5, 1。将局部变量数组中 index 为 5 的 int 值增加 1。
第138行,goto指令,操作数为102。跳转到第102行。

第141-146行,输出。

上面是我自己的一些分析,值得注意的是,我使用换行分割出了三部分指令,而中间那部分指令,是在循环中执行的。也就是说,这里,将会循环创建StringBuilder对象,进行apend()操作,那么,这种方式,效率也是很低的。

案例五:

其实,经过案例二的分析,我们也能猜到,案例五中,即使是循环,也只会创建一个StringBuilder对象,由一个StringBuilder对象进行apend()操作。下面,我们还是看一看其反编译后的结果:

149: new           #6                  // class java/lang/StringBuilder
152: dup
153: invokespecial #7                  // Method java/lang/StringBuilder."":()V
156: astore        5
158: iconst_0
159: istore        6
161: iload         6
163: iconst_3
164: if_icmpge     186
167: aload         5
169: ldc           #18                 // String e
171: invokevirtual #8                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
174: iload         6
176: invokevirtual #17                 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
179: pop
180: iinc          6, 1
183: goto          161
186: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
189: aload         5
191: invokevirtual #10                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
194: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
197: return

我们对其分析一下:

第149行,new指令,新创建了一个StringBuilder对象;
第153行,初始化刚刚创建的StringBuilder对象;
第156行,astore指令,其操作数为 5 。即弹出栈顶的变量,并将其存储到 index 为 5 的临时变量中;
第158行,iconst_0指令,将int类型值 0 压入栈
第159行,istore指令,其操作数为 6 。从栈中弹出int类型值,然后将其保存到index为 6 的局部变量中;
第161行,iload指令,其操作数为 6 。将index为 6 的int类型的局部变量压入栈;
第163行,iconst_3指令,将int类型值 3 压入栈;
第164行,if_icmpge指令,其操作数为 186。意思是,如果JVM操作数栈的整数对比大于等于(即当此时栈中的第一个元素 0 >= 栈中的第二个元素 3)成立时,则跳到第186行;

第167行,aload指令,其操作数为 5 。将位置为 5 的对象引用局部变量压入操作数栈;
第169行,将一个字符串变量 “e” 加载到操作数栈;
第171行,之前创建的StringBuilder对象执行apend("e");
第174行,iload指令,其操作数为 6 。将index为 6 的int类型的局部变量压入栈;
第176行,上面的StringBuilder对象执行apend()方法,将刚刚拿到的int类型局部变量拼接到字符串中。对应apend(i);
第179行,pop指令,从栈顶弹出一个字长的内容。
第180行,iinc指令,操作数为 6, 1。将局部变量数组中 index 为 6 的 int 值增加 1。
第183行,goto指令,操作数为161。跳转到第161行。
第186-194行,输出;

第197行,返回。

结果,和我们猜测的相同,从其反编译的结果来看,实际上只产生了一个StringBuilder对象,由一个StringBuilder来执apend()方法。结合以上五种情况,案例五所采用的方式,理论上应该是效率最高的。

参考文献:

1、https://www.cnblogs.com/chenqiangjsj/archive/2011/04/03/2004231.html

2、https://blog.csdn.net/weixin_37744601/article/details/78427334

3、https://cloud.tencent.com/developer/article/1334271

4、https://blog.csdn.net/chunyuan314/article/details/73266700

5、http://www.importnew.com/13107.html

你可能感兴趣的:(Java)