Java字符串真的是不可变的吗?

2022-08-31 04:38:38

我们都知道这在Java中是不可变的,但请检查以下代码:String

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  

为什么这个程序是这样的?为什么和的价值发生了变化,而不是?s1s2s3


答案 1

String是不可变的*,但这仅意味着您无法使用其公共 API 更改它。

您在这里所做的是使用反射来规避正常的 API。同样,您可以更改枚举的值,更改整数自动装箱中使用的查找表等。

现在,原因和更改值是它们都引用相同的实习字符串。编译器执行此操作(如其他答案所述)。s1s2

这个原因实际上对我来说并不奇怪,因为我认为它会共享数组(在Java 7u6之前的早期Java版本中确实如此)。但是,查看 的源代码,我们可以看到子字符串的字符数组实际上是复制的(使用)。这就是为什么它没有改变。s3valueStringvalueArrays.copyOfRange(..)

您可以安装 一个 ,以避免恶意代码执行此类操作。但请记住,有些库依赖于使用这些类型的反射技巧(通常是ORM工具,AOP库等)。SecurityManager

*)我最初写的并不是真正的不可变的,只是“有效的不可变”。在 的当前实现中,这可能会产生误导,其中数组确实被标记了 。但是,仍然值得注意的是,没有办法在Java中将数组声明为不可变的,因此必须注意不要将其公开到其类之外,即使使用适当的访问修饰符也是如此。StringStringvalueprivate final


由于这个话题似乎非常受欢迎,这里有一些建议的进一步阅读:Heinz Kabutz的Reflective Madness talk from JavaZone 2009,它涵盖了OP中的许多问题,以及其他反思......井。。。疯狂。

它涵盖了为什么这有时很有用。为什么,大多数时候,你应该避免它。:-)


答案 2

在 Java 中,如果将两个字符串基元变量初始化为相同的文本,则会将相同的引用分配给这两个变量:

String Test1="Hello World";
String Test2="Hello World";
System.out.println(test1==test2); // true

initialization

这就是比较返回 true 的原因。第三个字符串是使用 它创建一个新字符串,而不是指向相同的字符串。substring()

sub string

当您使用反射访问字符串时,您将获得实际的指针:

Field field = String.class.getDeclaredField("value");
field.setAccessible(true);

因此,更改此值将更改持有指向它的指针的字符串,但是由于它而使用新字符串创建时不会更改。s3substring()

change