String ab = new String(“a“) + new String(“b“)创建了几个对象

一、前言

之前已经研究过了,但是今天和别人讨论了这个问题,有了点新的见解,于是记录下来。



二、代码测试

    public static void main(String[] args) {
        String ab =
                new String("a")
                        +
                        new String("b");
        String c = new String("c");
        String cd = c + new String("d");
        System.out.println(ab + cd);
        System.out.println();
    }
  • 测试结果:
abcd



三、查看字节码中生成了多少个对象

  • 字节码:
  0 new #2 <java/lang/StringBuilder>
  3 dup
  4 invokespecial #3 <java/lang/StringBuilder.<init>>
  7 new #4 <java/lang/String>
 10 dup
 11 ldc #5 <a>
 13 invokespecial #6 <java/lang/String.<init>>
 16 invokevirtual #7 <java/lang/StringBuilder.append>
 19 new #4 <java/lang/String>
 22 dup
 23 ldc #8 <b>
 25 invokespecial #6 <java/lang/String.<init>>
 28 invokevirtual #7 <java/lang/StringBuilder.append>
 31 invokevirtual #9 <java/lang/StringBuilder.toString>
 34 astore_1
 35 new #4 <java/lang/String>
 38 dup
 39 ldc #10 <c>
 41 invokespecial #6 <java/lang/String.<init>>
 44 astore_2
 45 new #2 <java/lang/StringBuilder>
 48 dup
 49 invokespecial #3 <java/lang/StringBuilder.<init>>
 52 aload_2
 53 invokevirtual #7 <java/lang/StringBuilder.append>
 56 new #4 <java/lang/String>
 59 dup
 60 ldc #11 <d>
 62 invokespecial #6 <java/lang/String.<init>>
 65 invokevirtual #7 <java/lang/StringBuilder.append>
 68 invokevirtual #9 <java/lang/StringBuilder.toString>
 71 astore_3
 72 getstatic #12 <java/lang/System.out>
 75 new #2 <java/lang/StringBuilder>
 78 dup
 79 invokespecial #3 <java/lang/StringBuilder.<init>>
 82 aload_1
 83 invokevirtual #7 <java/lang/StringBuilder.append>
 86 aload_3
 87 invokevirtual #7 <java/lang/StringBuilder.append>
 90 invokevirtual #9 <java/lang/StringBuilder.toString>
 93 invokevirtual #13 <java/io/PrintStream.println>
 96 getstatic #12 <java/lang/System.out>
 99 invokevirtual #14 <java/io/PrintStream.println>
102 return
  • 由字节码可以看到,明面上String ab = new String("a") + new String("b");是生成了1+1+1=3个对象,但是通过debug后,发现并不是这样的。



三、debug发现剩下的对象

  • 首先打断点:

在这里插入图片描述

  • 此时刚到第一行,这里是还没有任何"a"相关的字符串的:

在这里插入图片描述

  • 我们在String对象初始化方法中打断点,然后再查看,此时出现了一个"a"的字符串对象,同时上面还有一个未初始化的对象,我猜它是当前正在初始化的new String("a"),而忽然出现的"a"字符串对象,我个人认为是因为String(String original)的原因,因此此时参数那里就出现了字符串字面量了,根据3.10.5 of the The Java™ Language Specification可以看到这两句话:
    • A string literal is always of type String (§4.3.3).
    • A string literal always refers to the same instance (§4.3.1) of class String.
  • 我猜测这个字符串就是当引用"a"的时候,发现字符串常量池没有它的引用,于是就创建并intern将它的引用放入字符串常量池中,然后再引用,在intern()的API文档中就讲到了字符串字面量会自动调用intern
    • All literal strings and string-valued constant expressions are interned.
  • 因此得出结论,当在new String("a")的时候,出现了字符串字面量"a",又因为字符串字面量总是字符串对象,因此它会自动创建,然后返回一个引用,这个引用作为参数传入到new String("a")这个构造方法里,就出现了上图情况。
  • 我用猜测而不是肯定,是因为我找不到官方文档关于字符串字面量何时创建的相关资料,如果有大佬找到,欢迎补充。
  • 然后再看我框起来的那个正在初始化的对象,我猜它就是当前正在初始化的new String("a"),接下来我给它打个标记并看看是不是真的:

在这里插入图片描述

  • 可以发现,我猜的正确。然后接下来就是初始化this.valuethis.hash了,看源代码,可以得知,会直接将original的字符数组赋值过去,这样就保证了它们的字符串是一样的,又因为字符串无法修改,这样子这个字符数组也改不了了,因此都可以用同一个,不需要多份字符数组。
  • 我们再标记一下original字符串:

在这里插入图片描述

  • 可以看到,的确是同一个,同时,无论是正在new的String(“a”)和字符串字面量"a",它们的this.value都是同一个。那么,我们可以判断出:new String("a")是创建了两个字符串对象。
    • 如果"a"字符串字面量出现过的话,那么new String("a")只会创建一个字符串对象,因为相同的字符串字面量都是调用的同一个字符串对象,而"a"早已出现并创建过一个字符串对象,然后通过intern将它的引用放入字符串常量池。
    • 官方文档中有讲相同的字符串字面量都是调用的同一个字符串对象:
      • The Java programming language requires that identical string literals (that is, literals that contain the same sequence of code points) must refer to the same instance of class String (JLS §3.10.5).
  • 然后出来new String("a")的方法:

在这里插入图片描述

  • 出来后,刚刚new出来的String("a"),却不见了,然后我看了下局部变量表(图片右侧),发现编译期间居然都没有给它分配一个槽位,这应该是编译器编译的时候优化了。(我记错了,因为我都没将这个new出来的对象地址赋值给局部变量,因此自然不会保留)
  • 此时是找不到"b"的:

在这里插入图片描述

  • debug进入new String("b")的方法:

在这里插入图片描述

  • 发现"b"了,同时也发现一个未初始化对象,对它们做标记:

在这里插入图片描述

  • 此时又出现了两个字符串对象,一个是字符串字面量"b"第一次出现生成的字符串对象,另外一个是new String("b")对象。
  • 继续debug,跳出new String("b")

在这里插入图片描述

  • 可以看到,此时并没有"ab"字符串对象,那么它究竟在哪里生成呢?毕竟现在已经生成了5个对象了,1个StringBuilder对象,4个String对象;
  • 然后我们知道StringBuilder在拼接完字符串后,会用toString()方法,因此我们在那里打断点:

在这里插入图片描述

  • 可以看到"ab"的字符串对象还没出现,但是为什么看下方的this"ab"呢?其实那时字符数组,并不是字符串对象,而下面的new String的方法,调用的也不是之前那个String(String original),而是public String(char value[], int offset, int count),因此我们在那个方法打断点:

在这里插入图片描述

  • 进去后,"ab"还是没出现,其中有个未初始化的对象,我标记一下它:

在这里插入图片描述

  • 它就是当前正在初始化的字符串对象,可以看到value数组是'a', 'b',但是当前初始化字符串对象的this.value还没有被赋值,我们直接到最后一行并运行,中间是非法判断,不需要管。

在这里插入图片描述

  • 此时进入到最后一行的方法里,看看最后的copy是什么?同时看看"ab"出现了没有。(中间的System.arraycopy是native方法,看不到原码)

在这里插入图片描述

  • 此时"ab"字符串对象还是不存在,而copy是一个字符数组:{'a', 'b'},很明显了,这个字符数组就是返回赋值给正在new的字符串对象的this.value了。

在这里插入图片描述

  • 此时就生成了"ab"字符串对象了,它的this.value地址都和前一张图的copy字符数组地址一样。
  • 到目前为止,已经生成了6个对象了,1个StringBuilder对象,2个因为字符串字面量"a""b"第一次出现而生成的两个字符串对象,2个是直接显式new出来的字符串对象,1个则是StringBuilder在append完后,通过toString生成的字符串对象。
  • 继续debug:

在这里插入图片描述

  • 变量c引用的是显式new出来的字符串,而不是字符串字面量"c",众所周知,通过+来拼接字符串对象的时候,会通过隐式new一个StringBuilder对象来append,我们看看append的是哪一个。因为不小心debug过了,所以重新运行并debug到那一步:

在这里插入图片描述

  • append时候,用的是c变量的地址指向的字符串对象,就是显式new出来的。后面的new String("d")就和前面操作一样了。
  • 当然这个是要看你究竟是变量c还是字符串字面量"c",如果是变量c自然是用的变量c指向的字符串对象,但是要是直接用字符串字面量"c",那么append的就是字符串字面量"c"对象了,而不是new出来的那个。



四、补充1:字符串字面量不是首次出现

  • 如果"a"不是第一次出现的话,那么String ab = new String("a") + new String("b"),就只会生成5个对象了。(假设"b"是第一次出现)

  • 如下图的debug:

在这里插入图片描述

  • 此时并没有"a"字符串对象,下一行:

在这里插入图片描述

  • 出现了"a"的字符串对象,我这里做了标记,此时在到String ab = new String("a") + new String("b")开始的时候,"a"已经不是第一次出现了,那么此时这一行会生成多少个字符串对象了,我们现在知道的是5个,但是不知道"a"会不会重复生成,于是我们在String初始化方法打断点,并进入,看看new String("a")中传入的字符串original是谁,究竟是刚刚生成的那个字符串对象,还是会生成一个新的字符串对象再传入:

在这里插入图片描述

  • 答案显而易见,用的是刚刚生成的"a"字符串对象,而不会再新生成一个,因为根据:
    • The Java programming language requires that identical string literals (that is, literals that contain the same sequence of code points) must refer to the same instance of class String (JLS §3.10.5).
  • 可以知道,拥有相同字符的字符串字面量,它们的引用必定是同一个。(原文是叫码点值序列)
  • 因此,String ab = new String("a") + new String("b")此时生成的是5个对象。



五、补充2:字符串字面量加载时机

  • 最近重新看jvm相关的知识,才发现当字节码文件加载进内存的时候,在解析resolve阶段,会将常量池里面的符号引用转为直接引用,而对于常量池的字面量中的字符串字面量来讲,它被生成为字符串对象的时机并不是字节码文件被加载进内存的解析阶段。JVM规范里明确指定resolve阶段可以是lazy的。
  • CONSTANT_Utf8会在类加载的过程中就全部创建出来,而CONSTANT_String则是lazy resolve的,例如说在第一次引用该项的ldc指令被第一次执行到的时候才会resolve。
  • 执行ldc指令就是触发这个lazy resolution动作的条件。
  • 也就是当ldc出现且字符串字面量是第一次出现的时候,才会触发这个lazy resolve动作,new出一个字符串对象,并且自动调用这个字符串对象的intern()方法将其地址放入字符串常量池中。
  • 来源知乎这个问答的高赞回答:https://www.zhihu.com/question/55994121/answer/147296098



六、总结

  • String ab = new String("a") + new String("b")总共创建了6个对象,1个StringBuilder对象,2个因为字符串字面量"a""b"第一次出现而生成的两个字符串对象,2个是直接显式new出来的字符串对象,1个则是StringBuilder在append完后,通过toString生成的字符串对象。

  • 但是对于"a""b"字符串字面量不是第一次出现的时候,它们并不会新建新的字符串对象。

  • 总而言之,要视情况而定。

  • 2
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值