工作项目开发已经差不多了,于是有时间自学一些东西。基础还是很重要的,开始看《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