Java 6和Java 7中的intern()行为不同

55
class Test {
    public static void main(String...args) {
        String s1 = "Good";
        s1 = s1 + "morning";
        System.out.println(s1.intern());
        String s2 = "Goodmorning";
        if (s1 == s2) {
            System.out.println("both are equal");
        }
    }
}

这段代码在Java 6和Java 7中产生不同的输出结果。在Java 6中,s1==s2条件返回false,而在Java 7中,s1==s2返回true。为什么?


3
仅仅调用本地String变量的值上的"intern()"方法(不需要将返回值重新赋给本地变量),会使得该变量的值与文本相等,你知道是哪种Java实现引起了这种魔法吗? - hmakholm left over Monica
1
@Mohammad Faisal,您正在运行哪个JVM? - nfechner
1
@Mohammad - 那是正确的代码吗?没有遗漏s1 = s1.intern()或者是if (s1.intern() == s2)吗?仅仅调用intern()不应该改变s1 - user85421
6
此问题询问的是引用相等性,而不是“==”与“.equals()”之间的区别。 - Bill the Lizard
1
@Faisal,难道不是Carlos的回答在解释(或建议)Java 6和Java 7之间的行为变化吗?尽管Nathan的回答提供了很棒的信息。 - Reddy
显示剩余10条评论
9个回答

27
似乎JDK7以不同的方式处理intern,我用版本1.7.0-b147进行了测试,并得到“两者相等”的结果,但在使用1.6.0_24执行相同的字节码时,我没有得到任何消息。这也取决于源代码中String b2 =...行的位置。下面的代码也不会输出消息:
class Test {
   public static void main(String... args) {
      String s1 = "Good";
      s1 = s1 + "morning";

      String s2 = "Goodmorning";
      System.out.println(s1.intern());  //just changed here s1.intern() and the if condition runs true   

      if(s1 == s2) {
         System.out.println("both are equal");
      } //now it works.
   }
}

看起来像是当intern在其字符串池中找不到该字符串时,将实际的实例s1插入到池中。JVM在创建s2时使用该池,因此它会返回与s1相同的引用。另一方面,如果首先创建s2,则该引用将存储到池中。
这可能是将国际化字符串从Java堆的永久代中移出的结果。

JDK 7中解决的重要RFE中找到。

在JDK 7中,国际化字符串不再分配在Java堆的永久代中,而是与应用程序创建的其他对象一起分配在Java堆的主要部分(称为年轻代和老年代)中。这种变化将导致更多的数据驻留在主要的Java堆中,较少的数据驻留在永久代中,因此可能需要调整堆大小。大多数应用程序由于此更改而在堆使用方面只会看到相对较小的差异,但是加载许多类或大量使用String.intern()方法的较大应用程序将看到更显着的差异。

不确定这是否是错误以及来自哪个版本... JLS 3.10.5说明:

明确地合并计算字符串的结果是与具有相同内容的任何预先存在的文字字符串相同的字符串。

因此问题是如何解释"预先存在",是编译时还是执行时:"Goodmorning"是否预先存在?
我更喜欢在7之前实现的方式...


这应该被视为错误吗? - Reddy
@Reddy - 不确定,似乎没有明确指定应该如何... intern 的文档说明了如果字符串不在池中,则存储并返回“此字符串”,但我找不到字面值应该何时保存到池中的定义。 - user85421

25
让我们从示例中省略不必要的细节。
class Test {
    public static void main(String... args) {
        String s1 = "Good";
        s1 = s1 + "morning";
        System.out.println(s1 == s1.intern()); // Prints true for jdk7, false - for jdk6.
    }
}

让我们将`String#intern`视为一个黑盒子。根据运行的几个测试用例,我得出以下实现结论:
Java 6:
如果池中包含与`this`相等的对象,则返回对该对象的引用, 否则创建一个新的字符串(与`this`相等),放入池中,并返回对该创建实例的引用。
Java 7:
如果池中包含与`this`相等的对象,则返回对该对象的引用, 否则将`this`放入池中,并返回`this`。
无论是Java 6还是Java 7都没有违反该方法的契约。
似乎新的`intern`方法行为是修复此错误的结果:https://bugs.java.com/bugdatabase/view_bug?bug_id=6962931

1
在jdk7中,interning方法被修改了,现在该方法有可能将实例放入池中并直接返回传递的实例。我不明白“直接返回传递的实例”是由Sun还是Oracle在任何地方指定的。 - Mohammad Faisal
@Mohammad 我已经重新表达了我的答案。 - Andrey
这应该是被接受的答案。简明扼要地解释了为什么相同的代码在两种情况下表现不同。 - Clint Eastwood

9
==用于比较引用。intern方法确保具有相同值的字符串具有相同的引用。 String.intern方法的javadoc解释如下: public String intern() 返回字符串对象的规范表示。 由String类私有地维护一个最初为空的字符串池。 当调用intern方法时,如果池已经包含与此String对象相等(根据equals(Object)方法确定)的字符串,则返回池中的字符串。否则,将此String对象添加到池中,并返回对此String对象的引用。 因此,对于任何两个字符串s和t,当且仅当s.equals(t)为true时,s.intern() == t.intern()为true。 所有文字字符串和字符串值常量表达式都会被缓存。Java语言规范第3.10.5节定义了字符串字面量。 返回:具有与此字符串相同内容但保证来自唯一字符串池的字符串。
因此,在不使用intern的情况下,编译器查看java代码中的常量并从中构建其常量池。String类维护一个不同的池,intern检查传入的字符串是否与池中的字符串相同,并确保引用是唯一的(以便==能够正常工作)。

是的,我知道==比较的是引用,而且我已经将它们设置为相同。但是第一个程序呢?s1s2不是都有相同的引用吗?或者第二个程序呢,当我写System.out.println(s1.intern());时,现在两者都有相同的引用,为什么? - Mohammad Faisal
抱歉!但如果我这样做:class Test{ public static void main(String... args){ String s1="hi"; String s2="hi"; if(s1==s2){ System.out.println("equal");//and it works } } } 现在这些字符串(s1和s2)怎么可能相等呢? - Mohammad Faisal
1
在你的问题中,你的代码可以欺骗JVM,但在这里很容易被发现,所以JVM会继续使用相同的引用。它正在寻找简单的优化。 - Nathan Hughes
1
我不明白。我知道的是,当我们说String s1="Good";时,在常量池中创建了一个String类型的对象。当我说s1=s1+"morning";时,另一个String对象被创建为Goodmorning,并将其引用分配给s1。现在,当我说String s2="Goodmorning";时,它会检查常量池中是否有Goodmorning?如果找到,则将先前的Goodmorning的引用分配给s2,这意味着s1==s2,但在第一个程序中它不起作用,在第二个程序中它起作用。为什么? - Mohammad Faisal
3
它检查编译类时常量池中的内容。因此,它无法考虑字符串连接等情况。intern在运行时重新分配引用。 - Nathan Hughes
显示剩余3条评论

7
在jdk6中: String s1="Good"; 在常量池中创建了一个String对象"Good"。

s1=s1+"morning"; 在常量池中创建了另一个String对象"morning",但是这一次JVM实际上执行了:s1=new StringBuffer().append(s1).append("morning").toString();

现在由于new运算符在堆中创建一个对象,因此s1的引用指向堆而不是常量池,而String s2="Goodmorning";在常量池中创建了一个String对象"Goodmorning",其引用存储在s2中。

因此,if(s1==s2)条件为false。

那么在jdk7中会发生什么呢?


可能与Carlos Heuberger在https://dev59.com/o2w05IYBdhLWcg3wuUKY#7090813提到的更改有关。 - Reddy

6

第一个案例:

在第一个代码片段中,您实际上将三个字符串添加到字符串池中。 1. s1 = "Good"
2. s1 = "Goodmorning" (连接后) 3. s2 = "Goodmorning"

执行 if(s1==s2) 时,对象相同但引用不同,因此为 false。

第二个案例:

在这种情况下,您使用了 s1.intern(),这意味着,如果池中已经包含与该 String 对象相等的字符串(由 equals(Object) 方法确定),则返回池中的字符串。否则,将该 String 对象添加到池中,并返回对该 String 对象的引用。

  1. s1 = "Good"
  2. s1 = "Goodmorning" (连接后)
  3. 对于 String s2="Goodmorning",不会将新字符串添加到池中,并且您将获取现有 one 的引用来表示 s2。因此,if(s1==s2) 返回 true。

2
第3点仅适用于JDK7。在JDK6中,s1 == s2返回false,因为intern()显然在池中存储了一个不同的实例/引用(相同的字符)。 - user85421

5
你需要使用 s1.equals(s2)。使用 ==String 对象比较的是对象引用本身。
编辑:当我运行你的第二个代码片段时,我没有得到“两者相等”的输出。
编辑2:澄清了使用 '==' 时比较的是引用。

1
但我得到了它。在第二个程序中,“两者相等”。 - Mohammad Faisal
你一定是搞错了。你确定在 if 语句中没有错误地写成了 s1==s1 吗?或者在 if 之前写成了 s1=s2 - Datajam
抱歉!但如果我这样做:class Test{ public static void main(String... args){ String s1="hi"; String s2="hi"; if(s1==s2){ System.out.println("equal");//and it works } } } - Mohammad Faisal
比较字符串的最佳实践当然是使用 .equals(),但这不是问题的重点。由于String对象是不可变的,在内存中对同一组字符的不同引用可能会指向不同的实例。关于这种情况何时发生,这取决于JVM优化,因此没有确切的定义。该问题指出了Java 6和Java 7之间的实现变化,并且想知道为什么会有这种变化。 - dimo414

4

主要有四种方法来比较字符串:

  1. "== 运算符":它只是比较字符串对象的引用变量。因此,它可能会根据你如何创建字符串,即使用String类的构造函数还是仅使用双引号而得到不同的内存(分别在堆和池中),从而给你意外的结果。
  2. "equals(Object)方法":这是Object类的一个方法,并被String类重载。它比较整个字符串并且区分大小写。
  3. "equalsIgnoreCase(String)方法":这是String类的一个方法,比较整个字符串并且不区分大小写。
  4. "compareTo(String)方法":逐字符比较两个字符串并返回它们的差异,如果返回值为0,则表示字符串相等。

3
每当您比较两个字符串时,请不要使用 ==,而应该使用 equals(),因为您正在比较对象而不是引用:
string1.equals(string2);

4
我知道我在做什么。请检查Nathan Hughes的答案。 - Mohammad Faisal
@Mohammad - 你确定吗?在你的两个代码中,s1和s2不是同一个引用,假设使用的是Sun/Oracle的Java SE:s1是连接两个字符串的结果 - 一个新的字符串 - s2来自常量池。 - user85421

2
结果代码取决于运行时:
class Test {
     public static void main(String... args) {
        String s1 = "Good";
        s1 = s1 + "morning";
        System.out.println(s1 == s1.intern()); // Prints true for jdk7, false - for jdk6.
    }
}

如果你写成这样:
class Test {
     public static void main(String... args) {
        String s = "GoodMorning";
        String s1 = "Good";
        s1 = s1 + "morning";
        System.out.println(s1 == s1.intern()); // Prints false for both jdk7 and jdk6.
    }
}

原因是 ' ldc #N '(从常量池加载字符串)和 String.intern() 都会在 Hotspot JVM 中使用 StringTable。详情请参考我撰写的一篇英文文章:http://aprilsoft.cn/blog/post/307.html


在你的第二段代码片段中,应该是s == s1.intern()而不是s1 == s1.intern()吗? - t0r0X

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