关于java字符串比较例子引发的学习以及intern方法

前言

  最近想给自己之前写过的测试代码加些注释,以方便以后查看的时候可以知道自己当时测试的初衷,以及结果的原因,但是最后还是决定写成笔记,不怕丢了,这篇笔记主要是来自之前看过的一本样书《java特种兵》里面的一个例子。当时觉得这个例子还挺有意思的,所以就自己拿出来跑一跑,并记一下笔记。

字符串比较例子及讲解

例子及运行结果

  先看例子代码:

@Test
public void example1() {
    String a = "a";
    String b = "b";

    String c = "a" + "b";
    String d = a + "b";
    String e = a + b;
    String f = new String("ab");
    StringBuilder g = new StringBuilder("ab");

    System.out.println(c==d); 
    System.out.println(c==e); 
    System.out.println(c==f); 
    System.out.println(c==g.toString()); 
    System.out.println(c==g.toString().intern()); 
}

  运行结果:
1-1字符串比较例子的运行结果图

用 javap 查看例子字节码

  如果对以上的代码及答案毫无疑问的看客就可以撤了,后面的内容对你来说应该没什么营养,不要浪费了时间。实际上我刚开始看到这个例子的时候,我也只对最后一条的比较结果为 true 有把握,前4条,我也不确定答案是啥,所以,我就把java源文件编译成class字节码文件,然后用 javap 反汇编工具看一下,java编译器会把这个例子底层编译成什么指令。如下,我只复制出来相关代码的部分,其他像常量池还有构造函数部分等内容我就把他删掉了,不然太长了。(具体的 javap 指令为 javap -verbose XXX.class

 public void example1();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=8, args_size=1
         0: ldc           #2                  // String a
         2: astore_1
         3: ldc           #3                  // String b
         5: astore_2
         6: ldc           #4                  // String ab
         8: astore_3
         9: new           #5                  // class java/lang/StringBuilder
        12: dup
        13: invokespecial #6                  // Method java/lang/StringBuilder."<init>":()V
        16: aload_1
        17: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        20: ldc           #3                  // String b
        22: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        25: invokevirtual #8                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        28: astore        4
        30: new           #5                  // class java/lang/StringBuilder
        33: dup
        34: invokespecial #6                  // Method java/lang/StringBuilder."<init>":()V
        37: aload_1
        38: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        41: aload_2
        42: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        45: invokevirtual #8                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        48: astore        5
        50: new           #9                  // class java/lang/String
        53: dup
        54: ldc           #4                  // String ab
        56: invokespecial #10                 // Method java/lang/String."<init>":(Ljava/lang/String;)V
        59: astore        6
        61: new           #5                  // class java/lang/StringBuilder
        64: dup
        65: ldc           #4                  // String ab
        67: invokespecial #11                 // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
        70: astore        7
        72: getstatic     #12                 // Field java/lang/System.out:Ljava/io/PrintStream;
        75: aload_3
        76: aload         4
        78: if_acmpne     85
        81: iconst_1
        82: goto          86
        85: iconst_0
        86: invokevirtual #13                 // Method java/io/PrintStream.println:(Z)V
        89: getstatic     #12                 // Field java/lang/System.out:Ljava/io/PrintStream;
        92: aload_3
        93: aload         5
        95: if_acmpne     102
        98: iconst_1
        99: goto          103
       102: iconst_0
       103: invokevirtual #13                 // Method java/io/PrintStream.println:(Z)V
       106: getstatic     #12                 // Field java/lang/System.out:Ljava/io/PrintStream;
       109: aload_3
       110: aload         6
       112: if_acmpne     119
       115: iconst_1
       116: goto          120
       119: iconst_0
       120: invokevirtual #13                 // Method java/io/PrintStream.println:(Z)V
       123: getstatic     #12                 // Field java/lang/System.out:Ljava/io/PrintStream;
       126: aload_3
       127: aload         7
       129: invokevirtual #8                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
       132: if_acmpne     139
       135: iconst_1
       136: goto          140
       139: iconst_0
       140: invokevirtual #13                 // Method java/io/PrintStream.println:(Z)V
       143: getstatic     #12                 // Field java/lang/System.out:Ljava/io/PrintStream;
       146: aload_3
       147: aload         7
       149: invokevirtual #8                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
       152: invokevirtual #14                 // Method java/lang/String.intern:()Ljava/lang/String;
       155: if_acmpne     162
       158: iconst_1
       159: goto          163
       162: iconst_0
       163: invokevirtual #13                 // Method java/io/PrintStream.println:(Z)V
       166: return
SourceFile: "TestOperandStack.java"

字符串赋值的反汇编指令的分析

  这里拆分成多张图来解释,如下图,为了方便即使不懂字节码指令的看客,也加入了必要的指令解释,所以就是说,如第一行代码,String a = "a"; 在 java 底层指令分成两步,即先从常量池中获取字符串 “a” 的引用然后压入操作数栈顶,然后将操作数栈顶元素出栈,保存引用到第1个局部变量表。

  第二、三行代码,如法炮制。

1-2例子字节码指令解释1

题外知识点:(第三行源代码String c = "a" + "b"; 字符串 “a” 加上字符串 “b” 的操作,在指令中直接获取常量池的 “ab” 字符串,因为这里是编译器优化,java 编译器编译源文件的时候,就已经将这种字面量的运算操作,直接计算了,然后将结果直接替换原来的代码)

String d=a+“b” 的反汇编指令分析

  第四行代码,如下图所示

1-2例子字节码指令解释2

  在上图中,我们发现, java 的源代码中并没有创建 StringBuilder 的对象,而在 javap 的反汇编指令中,new 了一个 StringBuilder 对象出来。从这点我们可以看出,jvm 运行编译期间当代码类似于 String c = a + "b" 这种字符串变量与字面量相加时,会将指令转化为使用 StringBuilder 的 append 去拼接字符串

  在学习上,我们不仅要做到知其然,更要知其所以然,不断深挖思考,才能越过表象,而看清本质。为什么 jvm 要对字符串相加做这种转化?当然,最终目的肯定是为了提高性能。(对于这种技术的优化,目的当然不外乎就是两个,要么是方便调用者的使用,要么是提高应用的性能。)那么关于这个问题,jvm 是如何做到提升性能的呢?我们不妨从 “假如不做这种转化,会出现什么不好的结果?” 这个方向去思考。

  看如下一段简单的代码片段,如果 jvm 编译时没有对字符串相加转化成 StringBuilder 对象 append 拼接。那下面的代码会有什么结果?(这里还是需要了解下简单的字节码指令)

String a = "this is a";
String b = "question";
String c = a + " easy " + b;

  首先,我们知道,常量池会有 3 个字符串,即为 "this is a""question"" easy " ,然后第一、二行代码就会将两个字符串分别从常量池中压入操作数栈栈顶,并保存到局部变量表,重点就是这第三行代码,首先变量 a 从局部变量表中压入栈顶,然后 " easy " 从常量池中压入栈顶,接着就会调用字符串相加指令,弹出栈顶的两个元素,然后产出一个中间产物 "this is a easy " 的字符串压入栈顶,最后就是变量 b 从局部变量表中压入栈顶,再调用字符串相加指令,产出结果 "this is a easy question" 然后保存到局部变量表。所以,在这个过程中,就会多创建一个临时的字符串(题外话:临时的字符串是不会装载进常量池的),随着字符串相加的链越长,产出的临时字符串也就越多,那么占用应用的内存也就越多,而字符串的数据是放在堆中,并不是随着栈帧结束而回收的,是要通过垃圾回收器去回收的,而垃圾回收的频率越频繁,则应用的吞吐量也就越低。而字符串相加转化成使用 StringBuilder 的 append 拼接就不会有上面说的问题了吗?或者说不会有其他的问题吗?这个问题先保留,思考下,后面再分析。

1-2例子字节码指令解释2

  返回看下一条指令,上图中的 new 指令后面的 dup 指令,这里还是要再多思考一个问题,为什么此处需要调用 dup 复制栈顶?

  dup 指令跟 new 指令实际上是联用的,因为 new 指令只是创建了对象空间,而还没有为对象进行初始化,即调用构造函数,也即栈顶需要弹出调用构造方法的对象的引用,而需要注意的是,此时局部变量表中并没有 new 出来 StringBuilder 对象的引用,所以才需要调用 dup 指令,以便调用了构造函数以后,操作数栈的栈顶还是 StringBuilder 对象的引用

  那么,为什么调用 new 指令之后,需要让操作数栈栈顶,多一个对象的引用呢?

  我们知道,一般情况下,java 代码创建一个对象一般还会用一个变量去接收这个对象引用,比如说,Cat cat = new Cat(); 再回到反汇编指令中去,即调用了构造函数之后,需要有一个 astore 指令去弹出栈顶,将引用类型的栈顶元素保存到局部变量表。

  所以说, new 指令之后的 dup 指令,是为了下一步调用构造函数而复制的对象引用。

(PS:可以自己写个测试类,去验证一下,如果仅仅只是写 new Cat(); 的 java 代码,而不用变量接收的话,用 javap 去查看 class文件,指令中还是会先 new ,再 dup ,然后调用 invokespecial 指令调用构造函数,然后后续还会调一个 pop 指令,pop 指令就是弹出操作数栈栈顶。因为就是在操作数栈中复制多一个对象的引用,最后没有变量去接收,用不到该对象引用,又将其弹出栈顶了。)

  invokespecial指令,即调用构造函数,之后下一个指令就是 aload_1 就加载变量 a 进栈顶,接下来就是 invokevirtual 指令,该指令是用于调用对象的实例方法,根据对象的实际类型进行分派,而由指向常量池的项可知,调用的方法为 StringBuilder.append() 将变量 a 拼接进 StringBuilder 对象,而后又将字符串 "b" 从常量池从压入栈顶,然后到22指令行,再一次调用 StringBuilder.append() ,将字符串 "b" 拼接进 StringBuilder 对象。而此时,字符串已经拼接完成,下一步的指令还是 invokevirtual 指令,但调用的方法是 StringBuilder.toString() ,这时,我们先来看一下 StringBuilder.toString() 的 java 源码

1-3StringBuilder的toString方法源码

  如图所示, StringBuilder.toString() 方法源码的返回值为 new String(value, 0, count) ,那么例子中的第一个字符串比较的答案就显而易见了, 变量 c 指向的是常量池的字符串 "ab" 的引用,而变量 d 指向的是重新在堆中创建的 String 对象的引用,变量 c 跟变量 d 的引用地址不一样,不是同一个对象,所以比较结果为 false 。(引用类型的 == 比较,比较的是引用地址,同一个对象则返回true,反之则为false)

  同理, c==ec==fc==g.toString() 的结果也为 false

intern()函数

  关于 intern() 函数,因为它是 java 的 native 方法,所以要了解 intern() 函数只能通过其文档,如下图所示,是 jdk6 的中文文档中的解释,先看返回值,即该方法会在字符串池比对内容(equals),将内容相同的字符串引用返回。字符串池就是全局常量池,存放字面量的引用的池子。也即,例子中 c==g.toString().intern() 的结果为 true ,因为变量 cg.toString().intern() 指向的都是常量池中 “ab” 的引用。

1-4intern函数jdk6中文文档

  因为 jdk6jdk7 中常量池设计的不同,导致 intern() 函数结果的表现也不同,如下的代码,

@Test
public void example3() {
    String a = "a";
    String b = new String("a");
    b.intern();
    System.out.println(a==b);

    String c = new StringBuilder().append("a").append("b").toString();
    c.intern();
    String d = "ab";
    System.out.println(c==d);
}

  example3() 的测试结果,在 jdk6 中结果为

false
false

  根据文档中的解释,intern() 函数的返回值即为常量池的引用,但是其实这个函数还有“探测”的功能,即

(1)当常量池中不存在字面量,如”abc”这个字符串的引用,将这个对象的引用加入常量池,返回这个对象的引用;
(2)当常量池中存在字面量,如”abc”这个字符串的引用,返回这个对象的引用。

  所以,在 example3()a==b 结果为 false 的原因就是,由于当调用 b.intern() 这一行代码时,字符串 “a” 已经在常量池中存在了(第一行代码),所以此时变量 a 指向的是常量池中的引用,而变量 b 指向的是堆中创建的 String 对象的引用。

  而 example3()c==djdk6 中为 false 的原因,是因为 jdk6 在常量池中添加字符串时,是重新复制一个对象出来,然后再返回常量池中的引用。

(再次强调,String c = new StringBuilder().append("a").append("b").toString(); 这行代码,字符串 "ab" 是没有装载进常量池中的)

  所以,当 c.intern(); 调用时,此前全局常量池中并没有字符串 "ab" ,而调用 intern() 之后,jdk6 中将变量 c 复制一个对象进全局常量池,并返回这个对象的引用,所以此时字符串 "ab" 加入了常量池,但是指向的引用与变量 c 并不是同一个引用,即 c==dfalse

  而在 jdk7 以后的版本结果为

false
true

   jdk7 以后,常量池被放到了堆中,且当有字符串加入常量池时,是在堆中创建的字符串对象,然后将该对象的引用,加入到常量池 ,所以变量 c 与变量 d 指向的是同一个堆中的引用,即 c==dtrue

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值