Dar*_*tel 391 java string reflection immutability
我们都知道String
Java 中是不可变的,但请检查以下代码:
String s1 = "Hello World";
String s2 = "Hello World";
String s3 = s1.substring(6);
System.out.println(s1); // Hello World
System.out.println(s2); // Hello World
System.out.println(s3); // World
Field field = String.class.getDeclaredField("value");
field.setAccessible(true);
char[] value = (char[])field.get(s1);
value[6] = 'J';
value[7] = 'a';
value[8] = 'v';
value[9] = 'a';
value[10] = '!';
System.out.println(s1); // Hello Java!
System.out.println(s2); // Hello Java!
System.out.println(s3); // World
Run Code Online (Sandbox Code Playgroud)
为什么这个程序运行这样?为什么价值s1
和s2
变化,但不是s3
?
har*_*ldK 402
String
是不可变的*但这仅表示您无法使用其公共API更改它.
你在这里做的是使用反射来规避正常的API.同样,您可以更改枚举值,更改整数自动装箱等中使用的查找表.
现在,原因s1
和s2
变化值是,它们都引用相同的实习字符串.编译器执行此操作(如其他答案所述).
原因s3
实际上并不令我感到意外,因为我认为它会共享value
数组(它在早期版本的Java中,在Java 7u6之前).但是,查看源代码String
,我们可以看到value
实际上复制了子字符串的字符数组(使用Arrays.copyOfRange(..)
).这就是它不变的原因.
你可以安装一个SecurityManager
,以避免恶意代码做这样的事情.但请记住,某些库依赖于使用这些反射技巧(通常是ORM工具,AOP库等).
*)我最初写道,String
s不是真正不可变的,只是"有效的不可变".这可能会误导当前的实现String
,其中value
数组确实被标记private final
.但是,值得注意的是,没有办法将Java中的数组声明为不可变的,因此必须注意不要将它暴露在类之外,即使使用适当的访问修饰符也是如此.
由于这个主题似乎非常受欢迎,这里有一些建议进一步阅读:Heinz Kabutz的 JavaZone 2009 的反思疯狂讲话,其中涵盖了OP中的许多问题,以及其他反思......好吧......疯狂.
它涵盖了为什么这有时有用.为什么,大多数时候,你应该避免它.:-)
Zah*_*med 94
在Java中,如果两个字符串原始变量初始化为同一个文字,它会为这两个变量分配相同的引用:
String Test1="Hello World";
String Test2="Hello World";
System.out.println(test1==test2); // true
Run Code Online (Sandbox Code Playgroud)
这就是比较返回true的原因.创建第三个字符串,使用substring()
该字符串创建一个新字符串而不是指向相同的字符串.
使用反射访问字符串时,您将获得实际指针:
Field field = String.class.getDeclaredField("value");
field.setAccessible(true);
Run Code Online (Sandbox Code Playgroud)
因此,更改为将更改包含指向它的指针的字符串,但由于s3
它创建的新字符串substring()
不会更改.
Boh*_*ian 50
你正在使用反射来规避String的不变性 - 它是一种"攻击"形式.
你可以创建很多像这样的例子(例如你甚至可以实例化一个Void
对象),但这并不意味着String不是"不可变的".
有些用例可能会使用这种类型的代码,并且可以"良好编码",例如尽早清除内存中的密码(GC之前).
根据安全管理器的不同,您可能无法执行代码.
Hau*_*idt 24
可见性修饰符和最终(即不可变性)不是对Java中恶意代码的测量; 它们只是防止错误和使代码更易于维护的工具(系统的一大卖点).这就是为什么你可以String
通过反射访问内部实现细节,比如s 的支持字符数组.
你看到的第二个影响就是所有String
的变化,而你看起来只会改变s1
.Java String文字的某个属性是它们被自动实现,即缓存.具有相同值的两个字符串文字实际上是同一个对象.当您创建一个字符串时new
,它将不会自动实现,您将看不到此效果.
#substring
直到最近(Java 7u6)以类似的方式工作,这将解释您的问题的原始版本中的行为.它没有创建一个新的back char数组,而是重用了原始String中的那个; 它只是创建了一个新的String对象,它使用偏移量和长度来仅显示该数组的一部分.这通常起作用,因为字符串是不可变的 - 除非你绕过它.这个属性#substring
也意味着当从它创建的较短子字符串仍然存在时,整个原始字符串不能被垃圾收集.
截至目前的Java和你当前版本的问题没有什么奇怪的行为#substring
.
man*_*nta 10
您使用的是哪个版本的Java?从Java 1.7.0_06开始,Oracle改变了String的内部表示形式,尤其是子字符串.
在新范例中,String偏移量和计数字段已被删除,因此子字符串不再共享底层char []值.
有了这种变化,它可能会在没有反射的情况下发生(???).
这里有两个问题:
要点1:除ROM外,计算机中没有不可变的内存.如今甚至ROM有时也是可写的.总有一些代码可以写入你的内存地址(无论是内核还是替代你的托管环境的本机代码).所以,在"现实"中,不是它们不是绝对不可改变的.
要点2:这是因为substring可能正在分配一个新的字符串实例,这可能会复制数组.可以以不会复制的方式实现子字符串,但这并不意味着它.需要权衡利弊.
例如,是否应该持有引用以reallyLargeString.substring(reallyLargeString.length - 2)
使大量内存保持活动,或者仅保留几个字节?
这取决于子串的实现方式.深拷贝将保持较少的内存,但运行速度会稍慢.浅拷贝会使更多的内存存活,但速度会更快.使用深层副本还可以减少堆碎片,因为字符串对象及其缓冲区可以在一个块中分配,而不是2个单独的堆分配.
无论如何,看起来您的JVM选择使用深层副本进行子串调用.
根据池的概念,包含相同值的所有String变量将指向相同的内存地址.因此,包含相同"Hello World"值的s1和s2将指向相同的存储位置(例如M1).
另一方面,s3包含"World",因此它将指向不同的内存分配(比如M2).
所以现在发生的是S1的值被改变(通过使用char []值).因此,s1和s2指向的存储器位置M1的值已经改变.
因此,结果,存储位置M1被修改,这导致s1和s2的值的变化.
但是位置M2的值保持不变,因此s3包含相同的原始值.
s3 实际上没有改变的原因是因为在 Java 中,当您执行子字符串时,子字符串的值字符数组是在内部复制的(使用 Arrays.copyOfRange())。
s1 和 s2 是相同的,因为在 Java 中它们都指向同一个内部字符串。它是在 Java 中设计的。
要添加到@haraldK的答案 - 这是一个安全黑客,可能会导致应用程序的严重影响.
首先是对存储在字符串池中的常量字符串的修改.当string被声明为a时String s = "Hello World";
,它将被放入一个特殊的对象池中,以便进一步重用.问题是编译器将在编译时对修改后的版本进行引用,一旦用户在运行时修改存储在该池中的字符串,代码中的所有引用都将指向修改后的版本.这会导致以下错误:
System.out.println("Hello World");
Run Code Online (Sandbox Code Playgroud)
将打印:
Hello Java!
Run Code Online (Sandbox Code Playgroud)
当我对这些危险的字符串实施繁重的计算时,我遇到了另一个问题.在计算过程中,有一个错误发生在1000000次中,这使得结果不确定.我能够通过关闭JIT找到问题 - 我总是在关闭JIT的情况下获得相同的结果.我的猜测是因为这个字符串安全黑客破坏了一些JIT优化合同.
归档时间: |
|
查看次数: |
51886 次 |
最近记录: |