字符串文字,实习和反思
我正试图找到这个问题的第三个解决方案。
我无法理解为什么这不打印false
。
public class MyClass { public MyClass() { try { Field f = String.class.getDeclaredField("value"); f.setAccessible(true); f.set("true", f.get("false")); } catch (Exception e) { } } public static void main(String[] args) { MyClass m = new MyClass(); System.out.println(m.equals(m)); } }
当然,由于字符串实习,被修改的"true"
实例与PrintStream
的print
方法中使用的实例完全相同?
public void print(boolean b) { write(b ? "true" : "false"); }
我错过了什么?
编辑
@yshavit的一个有趣的观点是,如果你添加这条线
System.out.println(true);
在try
之前,输出是
true false
这可以说是一个HotSpot JVM错误。
问题出在字符串文字实习机制中 。
- 在常量池解析期间,懒惰地创建字符串文字的
java.lang.String
实例。 - 最初,
CONSTANT_String_info
结构在常量池中表示字符串文字,该结构指向CONSTANT_Utf8_info
。 - 每个类都有自己的常量池。 也就是说,
MyClass
和PrintStream
拥有自己的一对CONSTANT_String_info
/CONSTANT_Utf8_info
cpool条目,用于文字‘true’ 。 - 当第一次访问
CONSTANT_String_info
时,JVM启动解析过程。 字符串实习是此过程的一部分。 - 要查找正在实习的文字的匹配项,JVM会将
CONSTANT_Utf8_info
的内容与StringTable
的字符串实例的内容进行比较。 - ^^^这是问题所在。 将来自cpool的原始UTF数据与可以由用户通过Reflection欺骗的Java
char[]
数组内容进行比较。
那么,你的测试中发生了什么?
-
f.set("true", f.get("false"))
在MyClass
启动文字“true”的解析。 - JVM在
StringTable
没有发现匹配序列‘true’的实例,并创建一个新的java.lang.String
,它存储在StringTable
。 -
StringTable
中String的value
通过Reflection替换。 -
System.out.println(true)
在PrintStream
类中启动文字“true”的解析。 - JVM将UTF序列‘true’与
StringTable
Strings进行比较,但没有找到匹配项,因为该String已经有‘false’值。 创建另一个‘true’字符串并放在StringTable
。
为什么我认为这是一个错误?
JLS§3.10.5和JVMS§5.1要求包含相同字符序列的字符串文字必须指向同一个java.lang.String
实例。
但是,在以下代码中,具有相同字符序列的两个字符串文字的分辨率会导致不同的实例。
public class Test { static class Inner { static String trueLiteral = "true"; } public static void main(String[] args) throws Exception { Field f = String.class.getDeclaredField("value"); f.setAccessible(true); f.set("true", f.get("false")); if ("true" == Inner.trueLiteral) { System.out.println("OK"); } else { System.out.println("BUG!"); } } }
JVM的一个可能修复是在StringTable
存储指向原始UTF序列的指针以及java.lang.String
对象,这样实习过程就不会将cpool数据(用户不可访问)与value
数组(可通过Reflection访问)进行比较。
我把它写成社区维基,因为我不知道它是否正确并且无论如何都不了解细节。
似乎发生的情况是,在运行时遇到字符串文字时,JVM会检查字符串池(使用equals
)以查看该字符串是否已存在。 如果不存在,则使用新实例。 此对象(新对象或已存在于字符串池中的对象)是从现在开始将用于该类中所有相同字符串文字的对象。
考虑这个例子:
public class MyClass { public MyClass() { try { Field f = String.class.getDeclaredField("value"); f.setAccessible(true); f.set("true", f.get("false")); } catch (Exception e) { } } public static void main(String[] args) { System.out.println(true); // 1 new MyClass(); System.out.println(true); // 2 System.out.println("true"); // 3 printTrue(); OtherClass.printTrue(); } public static void printTrue() { System.out.println("true"); // 4 } } public class OtherClass { static void printTrue() { System.out.println("true"); // 5 } }
这打印:
真正
假
假
假
真正
我的解释:
在第1行中,JVM在PrintStream
类中遇到文字"true"
。 将新字符串添加到池中。 然后调用new MyClass()
。 在此构造函数中,JVM在MyClass
类中遇到字符串文字"true"
。 该字符串已经在池中,因此池中的实例将是将要使用的实例,但关键的是它也是稍后将在第3行和第4行中使用的实例。然后修改支持此字符串的数组。 因此,第2,3和4行都是false
。 接下来,调用OtherClass.printTrue()
并且JVM在OtherClass
第一次遇到字符串文字"true"
。 此字符串不 equal
池中的字符串,因为池中的字符串现在具有后备数组[f, a, l, s, e]
。 因此,使用新的字符串实例,并在第5行打印true
。
现在假设我们注释掉第1行:
// System.out.println(true); // 1
这次输出是:
真正
假
假
真正
为什么第2行产生不同的结果? 这里的区别是在直到修改了后备数组之后 , PrintStream
类中才会遇到文字"true"
。 所以“错误”字符串不是PrintStream
类中使用的字符串。 但是,由于与上述相同的原因,第3行和第4行继续打印"false"
。