【问题标题】:Strange behavior with string interning in JavaJava中字符串实习的奇怪行为
【发布时间】:2017-08-07 01:56:35
【问题描述】:

代码如下:

String s = new String("1");
s.intern();
String s2 = "1";
System.out.println(s == s2);

String s3 = new String("1")+new String("1");
s3.intern();
String s4 = "11";
System.out.println(s3 == s4);

上面代码的输出是:

false
true

我知道ss2 是不同的对象,所以结果评估为假,但第二个结果评估为真。谁能告诉我区别?

【问题讨论】:

  • 您不妨参考this
  • 请注意,在初始化s4 之后调用s3.intern() 会将输出更改为false。这 似乎 表明文字 "11" 仅在执行该行时从池中检索,这不是我理解字符串文字实习工作的方式。
  • @RobbyCornelissen 这与我所做的基本相同。您只是将文字向上移动。
  • @Maybe_Factor 这根本不是答案,只是文章的链接。
  • 对于那些没有足够声誉来查看已删除此问题答案的墓地的用户 - 如果您打算写一个答案,a) 概述了 Java 中字符串实习的基础知识,或者 b ) 建议String.intern() 的结果应该被分配回变量:不要打扰。真正的问题是为什么这两种情况(s == s2 vs s3 == s4)的行为不同。

标签: java string


【解决方案1】:

这是发生了什么:


示例 1

String s1 = new String("1"); 
s1.intern();
String s2 = "1";
  1. 字符串文字 "1"(传递到 String 构造函数)被保留在地址 A
    字符串 s1 在地址 B 处创建,因为它不是文字或常量表达式。
  2. intern() 的调用无效。字符串"1"已经被interned,并且操作的结果没有被分配回s1
  3. 字符串s2的值为"1"是从字符串池中检索出来的,所以指向地址A

结果:字符串s1s2 指向不同的地址。


示例 2

String s3 = new String("1") + new String("1");
s3.intern();
String s4 = "11";
  1. 字符串s3在地址C处创建。
  2. intern() 的调用将地址C 处的值为"11" 的字符串添加到字符串池中。
  3. 字符串s4的值为"11"是从字符串池中检索出来的,所以指向地址C

结果:字符串s3s4 指向相同地址。


总结

字符串"1" 在调用intern() 之前被保留,因为它存在于s1 = new String("1") 构造函数调用中。

将构造函数调用更改为s1 = new String(new char[]{'1'}) 将使s1 == s2 的比较结果为true,因为现在两者都将引用通过调用s1.intern() 显式插入的字符串。

(我使用来自this answer 的代码来获取有关字符串内存位置的信息。)

【讨论】:

  • 啊,现在说得通了,但需要非常仔细地阅读。请注意,如果任何其他代码之前已将 "11" 放入实习字符串池,则观察到的行为会发生变化。
  • @RolandIllig 确实,我在问题的 cmets 中注意到了这一点。
  • 可以从输出中推断出分解中的步骤,就像我之前的 commented 一样。我更感兴趣的是查看它的记录位置(如果确实如此)。
  • @shmosel 您希望看到哪些记录在案?仅仅使用字符串字面量(不分配它)就足以让它在字符串池中实习?
  • 我的意思是“使用”和你一样。我的观点是,我预计"a" 会在该语句执行之前很久就被实习,可能是在类加载时。我可能完全错了,但很高兴以一种或另一种方式看到来源。
【解决方案2】:

s.intern() 不会更改字符串 s。你应该写:

    s = s.intern();

【讨论】:

  • 那么为什么它似乎对s3.intern() 有效?请阅读我对原始问题的评论。
  • @ShyamBaitmangalkar 是的,确实如此。 @Robby我猜编译器会评估表达式。我注意到的是,使用s = s.intern()s3 = s3.intern(),我得到了真实和真实。
  • 但是你不需要重新分配s3来获得true,就像OP的例子一样。
  • @shmosel 否,但文档说“返回字符串对象的规范表示。”,因此使用 .intern() 的正确方法是使用返回的值(当这样做时)方式,它的行为符合预期)。也就是说,我不知道为什么原来的程序会这样。
  • 我们知道这不是正确的用法。但这不是问题。
【解决方案3】:

对于场景 1:

String s = new String("1");
s.intern();
String s2 = "1";
System.out.println(s == s2);

使用字节码

   0: new           #2                  // class java/lang/String
   3: dup
   4: ldc           #3                  // String 1
   6: invokespecial #4                  // Method java/lang/String."<init>":(Ljava/lang/String;)V
   9: astore_1
  10: aload_1
  11: invokevirtual #5                  // Method java/lang/String.intern:()Ljava/lang/String;
  14: pop
  15: ldc           #3                  // String 1

对于String s = new String("1");,它将创建一个新的String 对象,它将具有一个新地址,其中 "1" 已经在 字符串池 中:

ldc #3 // String 1

对于s2,作为字节码:

15: ldc #3 // String 1

s2指向字符串池变量:"1",所以ss2有不同的地址,结果是false

对于场景 2:

String s3 = new String("1")+new String("1");
s3.intern();
String s4 = "11";
System.out.println(s3 == s4);

使用字节码

   0: new           #2                  // class java/lang/StringBuilder
   3: dup
   4: invokespecial #3                  // Method java/lang/StringBuilder."<init>":()V
   7: astore_1
   8: aload_1
   9: ldc           #4                  // String 1
  11: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  14: ldc           #4                  // String 1
  16: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  19: invokevirtual #6                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
  22: astore_2
  23: aload_2
  24: invokevirtual #7                  // Method java/lang/String.intern:()Ljava/lang/String;
  27: astore_3
  28: ldc           #8                  // String 11

作为字节码,你可以看到new String("1")+new String("1");是使用StringBuilder创建的

new #2 // class java/lang/StringBuilder

它完全是一个没有字符串池变量的新对象。

s3.intern() 之后,此方法会将当前s3 添加到内存字符串池8: aload_1

s4 正在尝试从

加载

ldc #8 // String 11

所以s3s4 地址应该相等并且结果为真。

【讨论】:

  • 完美!!!您的 bytecode 跟踪准确无误并解释了所有问题!
  • 这里只有一个问题。获取这样的字节码跟踪的方法是什么?
  • 我的意思是,您使用的不是javap 吗?
  • @ShyamBaitmangalkar,是的,我正在使用 javap 查看字节码。
  • “此方法会将当前的 s3 添加到内存字符串池”如何工作?由于被用作文字常量,字符串“11”已经在常量池中。您可以通过javap -v 看到这一点。
【解决方案4】:

仅针对使用 groovy 的人,附加信息是:行为不同

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-09-13
    • 1970-01-01
    • 2011-10-07
    • 2013-10-14
    • 2016-12-23
    相关资源
    最近更新 更多