使用反射更改字符串的影响

15
我们都知道,在Java中,String是不可变的。但是,通过使用反射,可以获取Field并设置访问级别来更改它。(我知道这是不建议的,我没有计划这样做,这个问题是纯理论性质的)。 我的问题:假设我知道自己在做什么(并根据需要修改所有字段),程序会正确运行吗?或者JVM进行了一些依赖于String不可变的优化,那么我会遭受性能损失吗?如果确实会有性能损失,它所做的假设是什么?程序中会出现什么问题?

注:String只是一个例子,我实际上对一个通用答案感兴趣,除了这个例子以外。

谢谢!

性能损失,相对于什么?通常情况下,做某事所需的时间比什么都不做要长。 - user unknown
@用户:我所说的性能损失是指其他操作(包括被修改的String对象),与未被修改的字符串相比,它们会遭受性能损失吗? - amit
它应该如何影响性能?您是否认为有一个“变异”标志,如果字符串被修改,将调用一些额外的检查?如果您假设存在一个测试,测试字符串的长度是否与内部字符数组的长度有所不同,或者哈希码仍然正确 - 那将是对每个字符串执行的检查。那么使用Stringpool的原因是什么呢?您是否查看过String类的源代码?或者在IDE中调试String类并查看源代码? - user unknown
7个回答

7
编译后,有些字符串可能会引用同一个实例,因此你将编辑更多的内容,而不知道你正在编辑什么其他内容。
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, "Doesn't say hello".toCharArray());
    System.out.println(s2);
}

输出:

Doesn't say hello

6
如果你这样做,那么你肯定会惹上麻烦。这是否意味着你会立即看到错误?不一定。根据你所做的事情,你可能在很多情况下都能逃脱。以下是几种情况,它们可能会给你带来麻烦:
- 你修改了一个字符串,该字符串恰好被声明为代码中的字面量。例如,你有一个函数,并且有地方调用了像function("Bob")这样的函数;在这种情况下,字符串"Bob"将在整个应用程序中被更改(这也适用于声明为final的字符串常量)。 - 你修改了一个用于子字符串操作或作为子字符串操作结果的字符串。在Java中,对字符串进行子字符串操作实际上使用与源字符串相同的基础字符数组,这意味着对源字符串的修改将影响到子字符串(反之亦然)。 - 你修改了一个恰好被用作地图键的字符串。它将不再等于其原始值,因此查找将失败。

我知道这个问题是关于Java的,但我之前写了一篇博客文章,展示了如果在.NET中改变一个字符串会导致程序表现得多么疯狂。这些情况非常相似。


我知道很多事情可能会出错,但我的问题不是什么可能出错,而是关于JVM的行为,在哪里它会变得未定义(或失败),不是因为程序错误,而是因为它假定String是不可变的。 - amit
@amit:字符串驻留是JVM行为的一个例子,它假定字符串是不可变的。子字符串操作的行为是Java行为的一个例子,它假定字符串是不可变的。我想我想说的是,如果您改变String实例,将会打破运行时和语言级别的假设,并导致未定义的行为。如果您正在寻找除字符串驻留之外的其他JVM特定示例,我无法立即想到任何示例。但我没有编写JVM。 - Dan Tao

3
我想到的是字符串驻留 - 字面量、常量池中的任何内容以及手动调用intern()的内容都指向同一个字符串对象。如果您开始操纵一个驻留的字符串字面量的内容,您可能会看到所有使用相同基础对象的其他字面量上发生完全相同的改变。
我不确定上述情况是否真的会发生,因为我从未尝试过(理论上会发生,但我不知道是否有什么事情发生在幕后来阻止它,但我怀疑没有),但像这样的事情可能会引起潜在问题。当然,它也可能通过只传递多个对同一字符串的引用,然后使用反射攻击来更改其中一个引用的对象,在Java级别上引发问题。大多数人(包括我!)不会在代码中明确防范这种事情,因此在使用任何不是您自己的代码或者您自己的代码(如果您也没有防范)时使用该攻击可能会导致各种奇怪、可怕的错误。
从理论上讲,这是一个有趣的领域,但是你挖掘得越深,你就会看到为什么任何类似的东西都是一个坏主意!

除了字符串外,我不知道有什么性能增强措施可以使对象成为不可变的(事实上,我认为目前JVM甚至无法确定一个对象是否不可变,除了反射攻击之外)。但这可能会影响checker-framework等试图静态分析代码以确保其不可变的工具。


2
我相信JVM本身并不会对字符串的不可变性做出任何假设,因为在Java中,“不可变性”不是语言级别的构造;它是由类的实现所暗示的一个特征,但是在反射存在的情况下,正如你所指出的那样,这种特征无法得到保证。因此,这也不应该与性能有关。
然而,几乎所有现有的Java代码(包括标准API实现)都依赖于字符串的不可变性,如果你打破了这个期望,你将看到各种各样的错误。

0
String类中的私有字段是char[]、offset和length。更改它们中的任何一个不应对其他对象产生不良影响。但是,如果您以某种方式更改了char[]的内容,那么您可能会看到一些令人惊讶的副作用。

我认为这就是OP所询问的内容:更改内部char []数组的内容 - Dan Tao
1
如果更改了char[],那么hashCode()的结果将会不同。任何尝试从基于哈希的集合中恢复字符串的操作可能会失败。 - rossum
@rossum:实际上,String实例会缓存自己的哈希码,因此,例如,使用相同的字符串对象调用containsKey将实际成功;奇怪的是,使用完全相同但分离的实例调用将失败。 - Dan Tao
@Dan Tao:一些实现可能会缓存,而其他实现则不会。依赖于特定的实现细节是很危险的。 - rossum
@rossum:朋友,你在对信徒讲教。我只是试图提出一个有趣的(尽管是特定于实现的)观察。 - Dan Tao

0
为了展示它如何破坏程序:
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

0
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的字面量。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接