这个 Java 代码片段是如何工作的? (字符串池和反射)
How does this Java code snippet work? (String pool and reflection)
Java 字符串池加上反射可以在 Java:
中产生一些难以想象的结果
import java.lang.reflect.Field;
class MessingWithString {
public static void main (String[] args) {
String str = "Mario";
toLuigi(str);
System.out.println(str + " " + "Mario");
}
public static void toLuigi(String original) {
try {
Field stringValue = String.class.getDeclaredField("value");
stringValue.setAccessible(true);
stringValue.set(original, "Luigi".toCharArray());
} catch (Exception ex) {
// Ignore exceptions
}
}
}
以上代码将打印:
"Luigi Luigi"
马里奥怎么了?
What happened to Mario ??
你基本上改变了它。是的,通过反射你可以违反字符串的不变性......并且由于字符串实习,这意味着 "Mario" 的任何使用(除了在更大的字符串常量表达式中,这将在编译时解决)在程序的其余部分将以 "Luigi" 结束。
这种事情就是反射需要安全权限的原因...
请注意,由于 +
的左关联性,表达式 str + " " + "Mario"
不会 执行任何编译时连接。它实际上是 (str + " ") + "Mario"
,这就是您仍然看到 Luigi Luigi
的原因。如果将代码更改为:
System.out.println(str + (" " + "Mario"));
... 然后你会看到 Luigi Mario
因为编译器会将 " Mario"
固定到与 "Mario"
.
不同的字符串
它被设置为路易吉。 Java 中的字符串是不可变的;因此,编译器可以将所有提及的 "Mario"
解释为对同一字符串常量池项(大致为 "memory location")的引用。您使用反射来更改该项目;所以你代码中的所有 "Mario"
现在就像你写的 "Luigi"
.
字符串文字存储在字符串池中,并使用它们的规范值。 "Mario"
文字不仅仅是具有相同值的字符串,它们是 相同的对象 。操作其中之一(使用反射)将修改其中 "both" 个,因为它们只是对同一对象的两个引用。
为了进一步解释现有的答案,让我们看一下您生成的字节码(这里只有 main()
方法)。
现在,对该位置的内容所做的任何更改都会影响 参考文献(以及您提供的任何其他参考文献)。
你刚刚把字符串常量池Mario
的String
改成了Luigi
被多个 String
引用,所以每个引用 文字 Mario
现在是 Luigi
.
Field stringValue = String.class.getDeclaredField("value");
您已从 class String
中获取名为 value
的 char[]
字段
stringValue.setAccessible(true);
使其易于访问。
stringValue.set(original, "Luigi".toCharArray());
您将 original
String
字段更改为 Luigi
。但是原始的是 Mario
String
literal 并且 literal 属于 String
池并且都是 interned .这意味着所有具有相同内容的文字都指代相同的内存地址。
String a = "Mario";//Created in String pool
String b = "Mario";//Refers to the same Mario of String pool
a == b//TRUE
//You changed 'a' to Luigi and 'b' don't know that
//'a' has been internally changed and
//'b' still refers to the same address.
基本上,您已经更改了 String
池中的马里奥,它在所有引用字段中得到 反映。如果您创建 String
Object
(即 new String("Mario")
)而不是文字,您将不会遇到这种行为,因为您将有两个不同的 Mario
s。
其他答案充分解释了发生了什么。我只是想补充一点,这只有在没有安装 security manager 的情况下才有效。当 运行 默认情况下没有来自命令行的代码时,您可以这样做。然而,在可信代码与不可信代码混合的环境中,例如生产环境中的应用程序服务器或浏览器中的小程序沙箱,通常会有安全管理器在场,并且不允许您进行此类恶作剧,因此这似乎不是一个可怕的安全漏洞。
另一个相关点:在某些情况下,您可以使用常量池来提高字符串比较的性能,方法是String.intern()
。
该方法 returns String 的实例,其内容与从 String 常量池调用它的 String 相同,如果尚不存在则添加它。换句话说,在使用 intern()
之后,所有具有相同内容的字符串都保证是彼此相同的字符串实例,并且与具有这些内容的任何字符串常量一样,这意味着您可以使用等号运算符 (==
) 在他们身上。
这只是一个例子,它本身不是很有用,但它说明了这一点:
class Key {
Key(String keyComponent) {
this.keyComponent = keyComponent.intern();
}
public boolean equals(Object o) {
// String comparison using the equals operator allowed due to the
// intern() in the constructor, which guarantees that all values
// of keyComponent with the same content will refer to the same
// instance of String:
return (o instanceof Key) && (keyComponent == ((Key) o).keyComponent);
}
public int hashCode() {
return keyComponent.hashCode();
}
boolean isSpecialCase() {
// String comparison using equals operator valid due to use of
// intern() in constructor, which guarantees that any keyComponent
// with the same contents as the SPECIAL_CASE constant will
// refer to the same instance of String:
return keyComponent == SPECIAL_CASE;
}
private final String keyComponent;
private static final String SPECIAL_CASE = "SpecialCase";
}
这个小技巧不值得设计你的代码,但当你注意到可以通过使用 ==
运算符在字符串上明智地使用 intern()
.
Java 字符串池加上反射可以在 Java:
中产生一些难以想象的结果import java.lang.reflect.Field;
class MessingWithString {
public static void main (String[] args) {
String str = "Mario";
toLuigi(str);
System.out.println(str + " " + "Mario");
}
public static void toLuigi(String original) {
try {
Field stringValue = String.class.getDeclaredField("value");
stringValue.setAccessible(true);
stringValue.set(original, "Luigi".toCharArray());
} catch (Exception ex) {
// Ignore exceptions
}
}
}
以上代码将打印:
"Luigi Luigi"
马里奥怎么了?
What happened to Mario ??
你基本上改变了它。是的,通过反射你可以违反字符串的不变性......并且由于字符串实习,这意味着 "Mario" 的任何使用(除了在更大的字符串常量表达式中,这将在编译时解决)在程序的其余部分将以 "Luigi" 结束。
这种事情就是反射需要安全权限的原因...
请注意,由于 +
的左关联性,表达式 str + " " + "Mario"
不会 执行任何编译时连接。它实际上是 (str + " ") + "Mario"
,这就是您仍然看到 Luigi Luigi
的原因。如果将代码更改为:
System.out.println(str + (" " + "Mario"));
... 然后你会看到 Luigi Mario
因为编译器会将 " Mario"
固定到与 "Mario"
.
它被设置为路易吉。 Java 中的字符串是不可变的;因此,编译器可以将所有提及的 "Mario"
解释为对同一字符串常量池项(大致为 "memory location")的引用。您使用反射来更改该项目;所以你代码中的所有 "Mario"
现在就像你写的 "Luigi"
.
字符串文字存储在字符串池中,并使用它们的规范值。 "Mario"
文字不仅仅是具有相同值的字符串,它们是 相同的对象 。操作其中之一(使用反射)将修改其中 "both" 个,因为它们只是对同一对象的两个引用。
为了进一步解释现有的答案,让我们看一下您生成的字节码(这里只有 main()
方法)。
现在,对该位置的内容所做的任何更改都会影响 参考文献(以及您提供的任何其他参考文献)。
你刚刚把字符串常量池Mario
的String
改成了Luigi
被多个 String
引用,所以每个引用 文字 Mario
现在是 Luigi
.
Field stringValue = String.class.getDeclaredField("value");
您已从 class String
value
的 char[]
字段
stringValue.setAccessible(true);
使其易于访问。
stringValue.set(original, "Luigi".toCharArray());
您将 original
String
字段更改为 Luigi
。但是原始的是 Mario
String
literal 并且 literal 属于 String
池并且都是 interned .这意味着所有具有相同内容的文字都指代相同的内存地址。
String a = "Mario";//Created in String pool
String b = "Mario";//Refers to the same Mario of String pool
a == b//TRUE
//You changed 'a' to Luigi and 'b' don't know that
//'a' has been internally changed and
//'b' still refers to the same address.
基本上,您已经更改了 String
池中的马里奥,它在所有引用字段中得到 反映。如果您创建 String
Object
(即 new String("Mario")
)而不是文字,您将不会遇到这种行为,因为您将有两个不同的 Mario
s。
其他答案充分解释了发生了什么。我只是想补充一点,这只有在没有安装 security manager 的情况下才有效。当 运行 默认情况下没有来自命令行的代码时,您可以这样做。然而,在可信代码与不可信代码混合的环境中,例如生产环境中的应用程序服务器或浏览器中的小程序沙箱,通常会有安全管理器在场,并且不允许您进行此类恶作剧,因此这似乎不是一个可怕的安全漏洞。
另一个相关点:在某些情况下,您可以使用常量池来提高字符串比较的性能,方法是String.intern()
。
该方法 returns String 的实例,其内容与从 String 常量池调用它的 String 相同,如果尚不存在则添加它。换句话说,在使用 intern()
之后,所有具有相同内容的字符串都保证是彼此相同的字符串实例,并且与具有这些内容的任何字符串常量一样,这意味着您可以使用等号运算符 (==
) 在他们身上。
这只是一个例子,它本身不是很有用,但它说明了这一点:
class Key {
Key(String keyComponent) {
this.keyComponent = keyComponent.intern();
}
public boolean equals(Object o) {
// String comparison using the equals operator allowed due to the
// intern() in the constructor, which guarantees that all values
// of keyComponent with the same content will refer to the same
// instance of String:
return (o instanceof Key) && (keyComponent == ((Key) o).keyComponent);
}
public int hashCode() {
return keyComponent.hashCode();
}
boolean isSpecialCase() {
// String comparison using equals operator valid due to use of
// intern() in constructor, which guarantees that any keyComponent
// with the same contents as the SPECIAL_CASE constant will
// refer to the same instance of String:
return keyComponent == SPECIAL_CASE;
}
private final String keyComponent;
private static final String SPECIAL_CASE = "SpecialCase";
}
这个小技巧不值得设计你的代码,但当你注意到可以通过使用 ==
运算符在字符串上明智地使用 intern()
.