【问题标题】:intern() behaving differently in Java 6 and Java 7intern() 在 Java 6 和 Java 7 中的行为不同
【发布时间】:2011-10-27 06:40:27
【问题描述】:
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。为什么?

为什么这个程序在 Java 6 和 Java 7 中产生不同的输出?

【问题讨论】:

  • Java 实现仅仅调用 intern() 对局部字符串变量的值(并且不将返回值分配回局部变量)神奇地使变量的值比较等于文字???
  • @Mohammad Faisal 你在运行哪个 JVM?
  • @Mohammad - 这是正确的代码吗?没有错过s1 = s1.intern() 还是if (s1.intern() == s2)?只是调用intern() 不应该改变s1
  • 这个问题是关于引用相等,而不是 == 与 .equals()。
  • @Faisal,卡洛斯的回答不是解释(或暗示)java 6 和 java 7 之间的行为变化吗?内森的回答提供了很好的信息。

标签: java string java-7 java-6 string-interning


【解决方案1】:

JDK7 处理实习生的方式似乎与以前不同。
我使用 build 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 插入池中。创建 s2 时 JVM 正在使用该池,因此它得到与 s1 相同的引用。另一方面,如果先创建 s2,则该引用将存储到池中。
这可能是从 Java 堆的永久生成中移出实习字符串的结果。

在这里找到:Important RFEs Addressed in JDK 7

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

不确定这是否是错误以及来自哪个版本... JLS 3.10.5 状态

显式实习计算字符串的结果是与任何具有相同内容的预先存在的文字字符串相同的字符串。

所以问题是如何解释预先存在的,编译时或执行时:“早安”是否预先存在?
我更喜欢它在 7 之前实现的方式...

【讨论】:

  • 应该算是bug吗?
  • @Reddy - 不确定,接缝没有明确指定它应该如何......实习生的文档指出,如果“这个字符串”尚未在池中,则存储并返回它,但我没有找到什么时候应该将文字保存到池中的定义。
【解决方案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.
    }
}

让我们将String#intern 视为一个黑盒子。根据运行的几个测试用例,我认为实现如下:

Java 6:
如果池中包含的对象等于this,则返回对该对象的引用, 否则创建新字符串(等于this),放入池中,并返回对该创建实例的引用。

Java 7:
如果池中包含的对象等于this,则返回对该对象的引用, 否则将this 放入池中,然后返回this

Java 6 和 Java 7 都没有破坏contract of the method

似乎新的实习生方法行为是修复此错误的结果:http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6962931

【讨论】:

  • In jdk7 interning approach was modified, and now the method has a possibility put to the pool and return the passed instance directly. 我没有收到return the passed instance directly. 这是由SunOracle 指定的任何地方吗?
  • @Mohammad 我已经重新制定了我的答案。
  • 这应该是公认的答案。简洁而精确。完美地解释了为什么相同的代码在两种情况下的工作方式不同
【解决方案3】:

== 比较引用。 intern 方法确保具有相同值的字符串具有相同的引用。

String.intern method 的 javadoc 解释:

公共字符串实习生()

返回字符串对象的规范表示。

最初为空的字符串池由 类字符串。

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

因此对于任意两个字符串 s 和 t,s.intern() == 当且仅当 s.equals(t) 为真时,t.intern() 为真。

所有文字字符串和字符串值常量表达式都是 实习。字符串文字在 Java 语言的 §3.10.5 中定义 规格

返回:一个与此字符串内容相同的字符串,但 保证来自唯一字符串池。

因此,编译器无需实习即可查看 Java 代码中的常量并从中构建其常量池。 String 类维护了一个不同的池,interning 会根据池检查传入的字符串并确保引用是唯一的(这样 == 才会起作用)。

【讨论】:

  • 是的,我知道== 比较参考文献,我也一样。但是第一个程序呢? s1s2 不是都有相同的引用吗?或者第二个程序呢,当我写System.out.println(s1.intern());时;现在两者都有相同的参考为什么?
  • 不,它们不是同一个参考。您有两个不同的变量指向恰好包含相同数据的两个不同字符串。如果它们总是相同的引用,则不需要实习方法。
  • 在您的问题中,您的代码正在欺骗 jvm,在这里很容易弄清楚,因此 jvm 继续使用相同的引用。它正在寻找简单的优化。
  • 我没有得到。我知道的是,当我们说String s1="Good"; 时,常量池中创建了一个字符串类型的对象。当我说s1=s1+"morning"; 时,另一个字符串对象被创建为Goodmorning,并且它的引用被分配给s1。现在,当我说String s2="Goodmorning"; 时,它会检查Goodmorning 是否在常量池中?如果找到它,则将先前的 Goodmorning 的引用分配给 s2,这意味着 s1==s2 但在第一个程序中它不起作用,而在第二个程序中它起作用。怎么样?
  • @Mohammad:它在编译类时检查常量池中的内容。所以它不能考虑字符串连接等。实习生在运行时重新分配引用。
【解决方案4】:

在 jdk6 中: String s1="Good"; 在常量池中创建一个字符串对象“Good”。

s1=s1+"morning"; 在常量池中“早上”创建另一个字符串对象,但这次实际上是 JVM 做的:s1=new StringBuffer().append(s1).append("morning").toString();

现在new 运算符在堆中创建一个对象,因此s1 中的引用是堆而不是常量池 并且String s2="Goodmorning"; 在常量池中创建一个字符串对象“Goodmorning”,其引用存储在s2 中。

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

但是在 jdk7 中会发生什么?

【讨论】:

【解决方案5】:

第一种情况:

在第一个代码中,您实际上是在字符串池中添加三个字符串。 1. s1 =“好”
2. s1 = "Goodmorning"(连接后) 3. s2 = "早安"

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

第二种情况:

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

  1. s1 = “好”
  2. s1 = "早安"(连接后)
  3. 对于字符串 s2="Goodmorning",不会将新字符串添加到池中,您会获得 s2 现有字符串的引用。因此 if(s1==s2) 返回 true。

【讨论】:

  • 第 3 点对 JDK7 有效(仅?)。使用 JDK6,s1 == s2 返回 false,因为 intern() 显然在池中存储了不同的实例/引用(相同的字符)。
【解决方案6】:

您需要使用s1.equals(s2)。将==String 对象一起使用会比较对象引用本身。

编辑:当我运行您的第二个代码 sn-p 时,我没有打印出“两者相等”。

Edit2:阐明当您使用 '==' 时会比较引用。

【讨论】:

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

比较字符串主要有4种方式:

  1. “== 运算符”:它只是比较字符串对象的引用变量。因此,它可能会给您带来意想不到的结果,具体取决于您创建字符串的方式,即使用 String 类的构造函数或简单地使用双引号,因为两者获取内存的方式不同(分别在堆和池中)。
  2. “equals(Object) 方法”:这是对象类的方法,被字符串类重载。它比较整个字符串并且区分大小写。
  3. “equalsIgnoreCase(String) 方法”:这是字符串类的方法,比较整个字符串,不区分大小写。
  4. “compares(String) 方法”:逐个字符比较两个字符串,如果返回值为0,则返回它们的差值,表示字符串相等。

【讨论】:

    【解决方案8】:

    当你在两个字符串之间进行比较时,不要使用== 并使用eqauls(),因为你是在比较对象而不是引用:

    string1.equals(string2);
    

    【讨论】:

    • 我知道我在做什么。检查 Nathan Hughes 的答案
    • @Mohammad - 确定吗?在您的代码中,s1 与 s2 的引用都不相同,假设 Sun/Oracle 的 Java SE:s1 是连接 2 个字符串的结果 - 一个新字符串 - s2 来自常量池。
    【解决方案9】:

    结果代码依赖运行时:

    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()都将在热点JVM中使用StringTable。具体我写了一篇池英文文章:http://aprilsoft.cn/blog/post/307.html

    【讨论】:

    • 在您的第二个代码 sn-p 中,不应该是 s == s1.intern() 而不是 s1 == s1.intern() 吗?
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2015-05-14
    • 2016-04-17
    • 2019-06-22
    • 1970-01-01
    • 1970-01-01
    • 2014-08-05
    • 1970-01-01
    相关资源
    最近更新 更多