跳至主要內容

通过反汇编来看String的拼接

ChenSinojavajvm字节码反汇编大约 3 分钟

1、先看问题,以下结果是什么?

String s1 = "Hello";
String s2 = "Hello";
String s3 = "Hel" + "lo";
String s4 = "Hel" + new String("lo");
String s5 = new String("Hello");
String s6 = s5.intern();
String s7 = "H";
String s8 = "ello";
String s9 = s7 + s8;
System.out.println(s1 == s2);  
System.out.println(s1 == s3);  
System.out.println(s1 == s4);  
System.out.println(s1 == s9);  
System.out.println(s4 == s5); 
System.out.println(s1 == s6); 

2、从字符串拼接学习字节码分析

public class JvmTest {
    public void test() {
        String s4 = new String("hel") + "lo";
    }
}

先反汇编,到class文件所在目录打开控制台执行javap -c 类名

$ javap -c JvmTest
警告: 二进制文件JvmTest包含com.chen.base.jvm.JvmTest
Compiled from "JvmTest.java"
public class com.chen.base.jvm.JvmTest {
  public com.chen.base.jvm.JvmTest();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public void test();
    Code:
       0: new           #2                  // class java/lang/StringBuilder
       3: dup
       4: invokespecial #3                  // Method java/lang/StringBuilder."<init>":()V
       7: new           #4                  // class java/lang/String
      10: dup
      11: ldc           #5                  // String hel
      13: invokespecial #6                  // Method java/lang/String."<init>":(Ljava/lang/String;)V
      16: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      19: ldc           #8                  // String lo
      21: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      24: invokevirtual #9                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      27: astore_1
      28: return
}

接下来一个指令一个指令的分析,为什么new String("hel") + "lo";创建了两个对象。

行号指令含义
13new堆中创建一个StringBuilder对象,并把堆中此对象地址压入栈顶
14dup复制栈顶的值,压入栈顶
15invokespecial初始化StringBuilder,对象创建都是先分配内存,再初始化
16new堆中创建一个String对象,并把堆中此对象地址压入栈顶
17dup复制栈顶的值,压入栈顶
18ldc从常量池取出字符串"hel"并压入栈顶
19invokespecial和前面一样,先分配地址,再调用String的初始化方法,把"hel"压入new的对象中,注意看19行注释,初始化时有传参(Ljava/lang/String;),15行是"<init>":()V,无参的
20invokevirtual调用StringBuilder.append,并且也是有传参的,这一步是用之前创建并初始化过的StringBuilder空对象来和“hel”字符串先拼接
21ldc把字符串“lo”从常量池压入栈顶
22invokevirtual同上面一样,再用StringBuilder拼接“lo”字符串
23invokevirtual调用StringBuilder.toString,转化为String
24astore_1把栈顶的值还原到本地变量s4中
25return方法结束

小结:new String("hel") + "lo";该行代码在虚拟机执行时,是先创建一个StringBuilder并初始化(开始为空对象,注意空对象不是null),然后创建一个String对象并初始化(初始化后String对象内容是hel),然后调用用StringBuilder的append方法先把空对象和hel字符串拼接,然后再次调用append拼接lo.

问题:

String s1 = "Hello";
String s2 = "Hello";
String s3 = "Hel" + "lo";
String s4 = "Hel" + new String("lo");
String s5 = new String("Hello");
String s6 = s5.intern();
String s7 = "H";
String s8 = "ello";
String s9 = s7 + s8;
System.out.println(s1 == s2);  
System.out.println(s1 == s3);  
System.out.println(s1 == s4);  
System.out.println(s1 == s9);  
System.out.println(s4 == s5); 
System.out.println(s1 == s6); 

为什么s1 == s3是true,s1 == s9 是false?

答案请参考open in new window

算鸟还是i自己看一下字节码文件,写一个测试类

public class JvmTest {
    public void test() {
        String s3 = "hel" + "lo";
    }
}

反汇编,发现确实没有new新对象,是因为纯字符串相加在编译期间已经被优化了String s3 = "hel" + "lo";优化后就是String s3 = "hello"

$ javap -c JvmTest
警告: 二进制文件JvmTest包含com.chen.base.jvm.JvmTest
Compiled from "JvmTest.java"
public class com.chen.base.jvm.JvmTest {
  public com.chen.base.jvm.JvmTest();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public void test();
    Code:
       0: ldc           #2                  // String hello
       2: astore_1
       3: return
}

为了验证以上说的不是信口开河,我们找到JvmTest.class文件,用十六进制编辑器打开,如下,可以清楚的看到有一个拼接好的字符窜hello

在这里插入图片描述