Java中new String("X")和new String("X") + new String("Y")的字符串初始化方式有何区别?

9
public static void main(String[] args) {
    String s1 = new String("aa");
    s1.intern();
    String s2 = "aa";
    System.out.println(s1 == s2);

    //wrong in JDK1.6 but true in JDK1.8
    String str1 = new String("str") + new String("01");
    str1.intern();
    String str2 = "str01";
    System.out.println(str1 == str2);

}

我用JDK1.8运行了上面的代码,我认为结果会得到两个“false”,因为在我看来,s1和str1明显位于堆中,s2和str2则存储在字符串池中,但我得到了一个“false”和一个“true”。
问题是:是什么导致了“true”?
上面是基本问题。现在要确定这个问题与所谓的重复问题远离,我想谈谈我的新发现:使用JDK1.6时,代码的第二部分得到了一个“false”的结果,而使用JDK1.8时得到了一个“true”的结果。一些博客表示,自JDK1.7发布以来,intern()的行为已经改变。
如果池中不包含等于此String对象的字符串,则不会将此String对象添加到池中,而是将对此String对象的引用添加到池中。这意味着池中的引用将被分配给位于其他位置(如堆)的字符串对象,并且下一次初始化文字字符串等于早期字符串将被分配给字符串对象,这恰好描述了有关“true”结果的代码部分2。
这个理论确实能够解释上面代码的结果。但很明显,这个理论不符合intern()文档中包含的内容,该文档在JDK6/8 API中几乎相同。
现在问题来了:用JDK1.6和JDK 1.8中的相同代码得到不同结果是否有更好的解释?我提到的理论是否确切地发生了?

3
intern()是Python中的一个字符串方法,用于将字符串添加到内存缓存中以便重复使用。 - Maroun
3
如果你阅读 intern() 方法的文档,它应该会更加清晰明了。 - cpp beginner
4
不应该假设物体的位置。你可以弄清楚发生了什么,但 JVM 和自动内存分配的目的是使你与此隔离。编译器可能会在这里进行一些技巧,它可能会保留字符串常量缓存或不进行任何优化。这种行为不是语言的一部分,而且可能随时改变而没有通知,因此你永远不应该依赖它。 - Filip Malczak
1
请注意,我并没有提到intern()方法,而是通常关于内存布局的话题。即使intern()方法不涉及堆/栈分配的主题,它也通过将字符串映射到公共引用来去重字符串。您可以依赖API,该方法是API的一部分-您不能依赖于对编译器内部的预测和实验。 - Filip Malczak
1
@Lebecca - 更准确的说,这不仅仅是一个 Java 问题。对于任何基于虚拟机的语言都适用,因此当您学习和使用 .NET、python、NodeJS、Ruby、Smalltalk、任何 LISP 方言等时,应该采用同样的思路。 - Filip Malczak
显示剩余6条评论
2个回答

20

首先让我们看一下Java文档中关于String.intern()的说明:

当调用intern方法时,如果池中已经包含一个与此String对象相等的字符串(由equals(Object)方法确定),则返回池中的字符串。否则,将添加此String对象到池中,并返回对此String对象的引用。

接下来让我们看看你的代码:

    final String s1 = new String("aa");
    s1.intern();
    final String s2 = "aa";
    System.out.println(s1 == s2);

在这里,您创建了 3字符串 对象。第一个是"aa"这个 字符串字面量,它被添加到了字符串池中,第二个是 new String("aa"),它被从"aa"拷贝构造而来,但是它是一个不同的实例,第三个是另一个 字符串字面量 "aa",它再次从字符串池中取得(所以它与第一个"aa"是相同的实例)。当您调用intern()时,字符串池已经有一个等于s1(从"aa"字面量构造)的字符串,因此返回"aa",但是您没有使用它,s1仍然是另一个实例。因此,s1==s2false

    final String str1 = new String("str")+new String("01");
    str1.intern();
    final String str2 = "str01";
    System.out.println(str1 == str2);

在这里,您创建了6String对象。将文本字面量"str""01"放入中。然后从这些字面量中创建了两个副本。接着将这两个字符串连接起来,得到了str1。这个连接操作并没有立即放入中,但当您调用str1intern()方法时,这个连接操作会被放入中,因此str1本身现在也在池中。因此,当您将字符串字面量"str01"赋给str2时,它将从中取出,因此与str1是相同的实例。


@Lebecca 我更新了我的答案,并加入了 intern() 的 Java 文档。在第一种情况下,字符串 s1 已经存在于池中,而在第二种情况下,池中还没有与 str1 相等的字符串。因此,在第二种情况下,str1 被添加到了池中。 - Max Vollmer
有一件事我想提一下,我之前坚持错误观点的原因是我仍然使用JDK1.6中JVM模型来解决问题,当时字符串池仍位于非堆中,这导致了我的错误。再次感谢你的分享和耐心。 - Lebecca
2
据我所知,第二个 println 输出 true 还是 false 是未指定的。字符串常量 "str01" 可以在 str1.intern()"str01" 放入池之前或之后构造。 - Daira Hopwood
2
第二种情况的结果取决于您选择哪个字符串。如果您选择由引导程序内部化的字符串,则会打印“false”。例如,请参见:https://ideone.com/0OZOtC - Jorn Vernee
@JornVernee 这是个问题,我已经尝试了“int”,“boolean”等类型,但结果都是“false”。 - Lebecca
显示剩余2条评论

1

String.intern() 返回字符串对象的规范表示形式,因此如果您想在字符串池中引用该规范表示形式,则需要对其进行赋值。

String s1 = new String("aa");
s1.intern();

这里的s1仍然是您声明的对象,并且位于堆中。
您需要执行:
String s1Itern = s1.intern();

如果您想要字面字符串。
s1 == s2 is false
s1Itern == s2 is true

对于第二种情况,Max的回答解释得很好。

你似乎在说s2和str2是有不同的声明或类型,但它们实际上并没有。 - Bernhard Barker
我误读了,我会删除我的答案。 - Bentaye
1
谢谢你的意见。 - Lebecca

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接