Java字符串真的不可变吗?
我们都知道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
为什么这个程序像这样操作? 为什么s1
和s2
的值发生了变化,而不是s3
?
String
是不可变的*但这只意味着你不能使用它的公共API来改变它。
你在这里做的是用反射来规避正常的API。 同样,您可以更改枚举值,更改整数自动装箱中使用的查找表等。
现在, s1
和s2
更改值的原因是它们都指向相同的实际字符串。 编译器这样做(正如其他答案所述)。
s3
的原因实际上让我感到有些惊讶,因为我认为它会共享value
数组(在Java 7u6之前的早期版本的Java中)。 但是,查看String
的源代码,我们可以看到实际上复制了子字符串的value
字符数组(使用Arrays.copyOfRange(..)
)。 这就是为什么它没有改变。
您可以安装SecurityManager
,以避免恶意代码执行此类操作。 但请记住,一些库依赖于使用这些反射技巧(通常是ORM工具,AOP库等)。
*)我最初写道, String
不是真正不可变的,只是“有效的不可变的”。 这在String
的当前实现中可能会产生误导,其中value
数组确实标记为private final
。 但是,值得注意的是,没有办法在Java中声明一个数组是不可变的,所以必须注意不要将它暴露在类之外,即使使用适当的访问修饰符。
由于这个话题看起来非常受欢迎,这里有一些建议进一步阅读:Heinz Kabutz的反思疯狂讲话来自JavaZone 2009,其中涵盖了OP中的许多问题,以及其他反思......以及疯狂。
它涵盖了为什么这有时是有用的。 为什么,大多数时候,你应该避免它。 :-)
在Java中,如果两个字符串基元变量初始化为相同的文字,则它将相同的引用分配给两个变量:
String Test1="Hello World";
String Test2="Hello World";
System.out.println(test1==test2); // true
这就是比较返回真实的原因。 第三个字符串是使用substring()
创建的,它创建一个新的字符串而不是指向相同的字符串。
当你使用反射访问一个字符串时,你会得到实际的指针:
Field field = String.class.getDeclaredField("value");
field.setAccessible(true);
所以改为这会改变持有指向它的指针的字符串,但是由于substring()
使用新字符串创建s3
,所以它不会改变。
您正在使用反射来规避String的不变性 - 这是一种“攻击”形式。
有很多你可以创建的例子(例如,你甚至可以实例化一个Void
对象),但这并不意味着String不是“不可变的”。
在这种情况下,这种类型的代码可能会被用于您的优势,并且是“良好的编码”,例如在尽可能早的时刻(在GC之前)从内存中清除密码。
根据安全管理器的不同,您可能无法执行代码。
链接地址: http://www.djcxy.com/p/73653.html