使用reflection更改字符串的效果

众所周知,String在java中是不可变的。 但是,可以通过获取Field并设置访问级别来使用reflection来更改它。 (我知道这是未经修改的,我不打算这样做,这个问题纯粹是理论上的)。

我的问题:假设我知道我在做什么(并根据需要修改所有字段),程序是否会正常运行? 或者jvm是否进行了一些依赖于String不可变的优化? 我会遭受性能损失吗? 如果是这样,它会做出什么假设? 该计划会出现什么问题

ps String只是一个例子,除了示例之外,我实际上对一般答案感兴趣。

谢谢!

如果你这样做,你肯定会遇到麻烦。 这是否意味着你肯定会立即看到错误? 不,你可能会在很多情况下侥幸逃脱,取决于你在做什么。

以下是一些会让你感到困惑的案例:

  • 您修改了一个恰好在代码中某处声明为文字的字符串。 例如,你有一个function ,它被称为function("Bob") ; 在这种情况下, 整个应用程序中的字符串"Bob"都会发生变化(对于声明为final的字符串常量也是如此)。
  • 您修改在子字符串操作中使用的字符串,或者是子字符串操作的结果。 在Java中,获取字符串的子字符串实际上使用与源字符串相同的基础字符数组,这意味着对源字符串的修改将影响子字符串(反之亦然)。
  • 您可以修改恰好在某个地图中用作键的字符串。 它将不再与其原始值进行比较,因此查找将失败。

我知道这个问题是关于Java的,但是我写了一篇博文 ,说明如果你在.NET中改变一个字符串,你的程序可能会表现得多么疯狂。 情况非常相似。

编译后,一些字符串可能会引用一个实例,因此,您将编辑超出您想要的内容,并且永远不知道您还在编辑什么。

 public static void main(String args[]) throws Exception { String s1 = "Hello"; // I want to edit it String s2 = "Hello"; // It may be anywhere and must not be edited Field f = String.class.getDeclaredField("value"); f.setAccessible(true); f.set(s1, "Go to hell".toCharArray()); System.out.println(s2); } 

输出:

 Go to hell 

我想到的是字符串实习 – 文字,常量池中的任何内容以及任何手动intern() ed指向相同的字符串对象。 如果你开始搞乱一个实习字符串文字的内容,你可能会看到使用相同底层对象的所有其他文字的完全相同的改动。

我不确定上面是否真的发生了,因为我从未尝试过(理论上它会,我不知道是否会在场景下发生某些事情来阻止它,但我怀疑它)但是这样的事情可能会引发潜在问题。 当然,它也可能在Java级别上抛出问题,只需将多个引用传递给相同的字符串,然后使用reflection攻击从其中一个引用中更改对象。 大多数人(包括我!)都不会明确地防范代码中的那种事情,因此将攻击用于任何不属于您自己的代码,或者如果您还没有防范它们,则使用您自己的代码,可能导致各种类型奇怪的,可怕的错误。

从理论上讲这是一个有趣的领域,但你挖的越多,你就越明白为什么沿着这些方向做任何事都是个坏主意!

说到字符串之外,对于一个不可变的对象,没有我所知道的性能增强(实际上我认为JVM现在甚至不能告诉对象是否是不可变的,抛开reflection攻击。)它可能会抛出像检查器这样的东西- 框架关闭或任何试图静态分析代码以保证它是不可变的。

我很确定JVM本身不会对字符串的不变性做出任何假设,因为Java中的“不变性”不是语言级别的构造; 它是一个类实现隐含的特性,但是,正如你所注意到的那样,它不能在存在reflection的情况下得到保证。 因此,它也应与性能无关。

但是,几乎所有存在的Java代码(包括标准API实现)都依赖于字符串是不可变的,如果你打破了这种期望,你会看到各种各样的错误。

String类中的私有字段是char [],偏移量和长度。 更改任何这些不应对任何其他对象产生任何不利影响。 但是如果你能以某种方式改变char []的内容,那么你可能会看到一些令人惊讶的副作用。

 public static void main(String args[]){ String a = "test213"; String s = new String("test213"); try { System.out.println(s); System.out.println(a); char[] value = (char[])getFieldValue(s, "value"); value[1] = 'a'; System.out.println(s); System.out.println(a); } catch (Exception e) { e.printStackTrace(); } } static Object getFieldValue(String s,String fieldName) throws SecurityException, NoSuchFieldException, IllegalArgumentException, IllegalAccessException { Object chars = null; Field innerCharArray = String.class.getDeclaredField(fieldName); innerCharArray.setAccessible(true); chars = innerCharArray.get(s); return chars; } 

改变S的值将改变所有答案中提到的a的字面值。

为了演示如何搞砸程序:

 System.out.print("Initial: "); System.out.println(addr); editIntStr("ADDR_PLACEH", "192.168.1.1"); System.out.print("From var: "); System.out.println(addr);// System.out.print("Hardcoded: "); System.out.println("ADDR_PLACEH"); System.out.print("Substring: "); System.out.println("ADDR_PLACE" + "H".substring(0)); System.out.print("Equals test: "); System.out.println("ADDR_PLACEH".equals("192.168.1.1")); System.out.print("Equals test with substring: "); System.out.println(("ADDR_PLACE" + "H".substring(0)).equals("192.168.1.1")); 

输出:

 Initial: ADDR_PLACEH From var: 192.168.1.1 Hardcoded: 192.168.1.1 Substring: ADDR_PLACEH Equals test: true Equals test with substring: false 

第一次Equals测试的结果很奇怪,不是吗? 你不能指望你的程序员们弄清楚为什么Java认为他们是平等的……
完整的测试代码: http : //pastebin.com/vbstfWX1