这个Java代码片段如何工作? (string池和reflection)
与reflection结合的Javastring池可以在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"
马里奥怎么了?
马里奥怎么了?
你基本上改变了它。 是的,通过reflection,你可以违反string的不变性……而且由于string实际上,这意味着任何对“马里奥”的使用(除了在编译时已经解决的更大的string常量expression式之外)将会结束在其余的节目中作为“路易”。
这种事情是为什么reflection需要安全权限…
请注意,由于+
的左关联性,expression式str + " " + "Mario"
不执行任何编译时间连接。 这是有效的(str + " ") + "Mario"
,这就是为什么你仍然看到Luigi Luigi
。 如果您将代码更改为:
System.out.println(str + (" " + "Mario"));
…然后你会看到Luigi Mario
因为编译器会将" Mario"
插入到"Mario"
的不同string中。
它被设置为路易吉。 Java中的string是不可改变的。 因此,编译器可以将所有提到的"Mario"
解释为对同一个String常量池项(大致为“内存位置”)的引用。 你用reflection来改变那个项目; 所以你的代码中的所有"Mario"
现在好像你写了"Luigi"
。
为了解释一下现有的答案,让我们来看看你生成的字节码(这里只有main()
方法)。
现在,对该位置的内容所做的任何更改都会影响到这两个引用 (以及其他任何您也提供的内容)。
string文字存储在string池中,并使用它们的规范值。 两个"Mario"
文字不只是具有相同的值的string,他们是同一个对象 。 操纵其中的一个(使用reflection)将修改它们的“两个”,因为它们只是对同一个对象的两个引用。
你只是把string常量池 Mario
改成了由多个String
引用的Luigi
,所以每一个引用文字 Mario
现在都是Luigi
。
Field stringValue = String.class.getDeclaredField("value");
您已从类String
获取char[]
命名value
字段
stringValue.setAccessible(true);
使其可访问。
stringValue.set(original, "Luigi".toCharArray());
您将original
String
字段更改为Luigi
。 但原来是Mario
的String
文字和文字属于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
池的Mario。 如果你创build的String
Object
(即new String("Mario")
)而不是文字你不会面对这种行为,因为你会有两个不同的Mario
s。
其他答案充分解释发生了什么事情。 我只是想补充一点,只有在没有安装安全pipe理器的情况下才能使用。 当从命令行默认运行代码的时候,没有,你可以做这样的事情。 但是,在可信代码与不可信代码混合使用的环境中,例如生产环境中的应用程序服务器或浏览器中的小应用程序沙箱,通常会有一个安全pipe理器存在,您将不会被允许使用这种恶意代码。这似乎不是一个可怕的安全漏洞。
另一个相关点:通过使用String.intern()
方法,您可以利用常量池来提高string比较在某些情况下的性能。
该方法返回String的实例,其内容与从String常量池中调用的String相同,如果不存在,则将其添加。 换句话说,在使用intern()
,具有相同内容的所有string保证为相同的String实例,并且与这些内容中的任何string常量一样,也就是说,您可以使用它们的equals运算符( ==
) 。
这仅仅是一个自己并不是很有用的例子,但是它说明了这一点:
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"; }
这个小技巧是不值得devise你的代码的,但值得注意的是,当你注意到通过在string上使用==
运算符,一些性能敏感的代码可以提供更多的速度明智地使用intern()
。