从字节码角度看String的连接操作

11月 23, 2014 |

假设有如下的示例代码

使用“javap -v TestString.class”进行反编译,逐个函数分析如下:

test1的字节码

public java.lang.String test1();
flags: ACC_PUBLIC
Code:
stack=1, locals=2, args_size=1
0: ldc #29 // String welcome to javacoder.cn
2: astore_1
3: aload_1
4: areturn

这种写法是最简洁的,由于参加'+'操作的各个字符串的值都是确定的,编译器直接生成了值为'welcome to javacoder.cn'的常量字符串,将其加载到内存放回即可

test2的字节码

public java.lang.String test2();
flags: ACC_PUBLIC
Code:
stack=3, locals=2, args_size=1
0: ldc #33 // String welcome to
2: astore_1
3: new #35 // class java/lang/StringBuilder
6: dup
7: aload_1
8: invokestatic #37 //String String.valueOf:(Object);
11: invokespecial #43 //void StringBuilder.""(String)
14: ldc #13 // String javacoder
16: invokevirtual #46 // StringBuilder StringBuilder.append(String;);
19: invokevirtual #50 // String StringBuilder.toString();
22: astore_1
23: new #35 // class java/lang/StringBuilder
26: dup
27: aload_1
28: invokestatic #37 // String String.valueOf:(Object);
31: invokespecial #43 // void StringBuilder.""(String)
34: ldc #16 // String .cn
36: invokevirtual #46 // StringBuilder StringBuilder.append:(String);
39: invokevirtual #50 // String StringBuilder.toString()
42: astore_1
43: aload_1
44: areturn

对于这种写法,先从常量池加载字符串"welcome to",为什么会有这个字符串请看test1的解释,防止"+"操作的第一个操作数不是字符串,所以先调用String.valueOf(),然后调用StringBuilder(String arg)构造函数初始化一个StringBuilder对象,然后append("javacoder"),调用StringBuilder.toString()生成一个新的String对象赋值给s1;然后将s1("welcome to javacoder")的值传给String.valueOf()确保"+"的第一个参数是String 对象,调用StringBuilder(String arg)版本的构造函数初始化StringBuilder对象,append上'.cn',调用StringBuilder.toString()生成一个新的String对象返回。

test3的字节码

public java.lang.String test3(java.lang.String, java.lang.String, java.lang.String, java.lang.String);
flags: ACC_PUBLIC
Code:
stack=3, locals=6, args_size=5
0: new #35 // class java/lang/StringBuilder
3: dup
4: aload_1
5: invokestatic #37 // String String.valueOf:(Object);
8: invokespecial #43 // void StringBuilder."":(String)
11: aload_2
12: invokevirtual #46 // StringBuilder StringBuilder.append:(String;);
15: aload_3
16: invokevirtual #46 // StringBuilder StringBuilder.append:(String);
19: aload 4
21: invokevirtual #46 // StringBuilder StringBuilder.append:(String);
24: invokevirtual #50 // String StringBuilder.toString:();
27: astore 5
29: aload 5
31: areturn

当使用写成这种方式时,只会产生一个StringBuilder对象,依次将传入的参数append到StringBuilder对象,然后调用StringBuilder.toString()产生一个新的String对象返回。

test4的字节码

public java.lang.String test4();
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=1
0: new #56 // class java/lang/StringBuffer
3: dup
4: invokespecial #58 // void StringBuffer."":()
7: astore_1
8: aload_1
9: ldc #8 // String welcome
11: invokevirtual #59 // StringBuffer StringBuffer.append:(String)
14: pop
15: aload_1
16: ldc
#10 // String to
18: invokevirtual #59 // StringBuffer StringBuffer.append:(String)
21: pop
22: aload_1
23: ldc #13 // String javacoder
25: invokevirtual #59 // StringBuffer StringBuffer.append:(String)
28: pop
29: aload_1
30: ldc #16 // String .cn
32: invokevirtual #59 // StringBuffer StringBuffer.append:(String)
35: pop
36: aload_1
37: invokevirtual #62 // String StringBuffer.toString()
40: areturn

虽然我们要链接的每个字符串都是常量,但是编译器并不会想test1那样帮我们优化,这个代码的字节码和test3差不多,最主要的变化是由StringBuilder变成了StringBuffer,同时每一次append之后都会伴随着依次pop出栈操作,将append的返回值(StringBuffer本身)丢弃掉,下一次append操作之前先将StringBuffer对象的引用推到栈顶。

test5的字节码

public java.lang.String test5();
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: new #56 // class java/lang/StringBuffer
3: dup
4: invokespecial #58 // void StringBuffer.""()
7: ldc #8 // String welcome
9: invokevirtual #59 // StringBuffer StringBuffer.append:(String)
12: ldc #10 // String to
14: invokevirtual #59 // StringBuffer StringBuffer.append(String)
17: ldc #13 // String javacoder
19: invokevirtual #59 // StringBuffer StringBuffer.append:(String)
22: ldc #16 // String .cn
24: invokevirtual #59 // StringBuffer StringBuffer.append:(String;)
27: invokevirtual #62 // String StringBuffer.toString:()
30: areturn

这个版本的代码和test4很像,唯一的区别是少了pop(append的返回值出栈操作)和aload_1(将StringBuffer对象的引用推到栈顶)。说明通过使用链式操作确实可以获得一些优化。

结束语

  1. 使用"+"进行字符串连接操作使用的是StringBuilder对象,那么该操作是非线程安全的
  2. 如果"+"的各个操作数都是常量,那么编译器将计算该操作的值,然后将值存为一个常量
  3. 使用"+"进行字符串连接操作时最好进行链式操作,这样产生的临时对象才会更少
  4. 使用StringBuffer或者StringBuilder对象进行字符串连接操作,最好使用链式操作写法,能减少出栈和入栈操作

Posted in: java基础

Comments are closed.