为什么他们决定在Java和.NET(以及其他一些语言)中使字符串不可变?他们为什么不让它变得可变?
根据Effective Java,第4章,第73页,第2版:
"有很多很好的理由:不可变类比可变类更容易设计,实现和使用.它们不容易出错并且更安全.
[...]
" 不可变对象很简单.不可变对象可以处于一个状态,即创建它的状态.如果确保所有构造函数都建立了类不变量,那么可以保证这些不变量将始终保持为真,你没有努力.
[...]
不可变对象本质上是线程安全的; 他们不需要同步.它们不会被多个线程同时访问它们所破坏.这是实现线程安全的最简单方法.实际上,没有线程可以观察到另一个线程对不可变对象的任何影响.因此, 可以自由共享不可变对象
[...]
同章的其他小点:
您不仅可以共享不可变对象,还可以共享其内部.
[...]
不可变对象为其他对象构成了很好的构建块,无论是可变的还是不可变的.
[...]
不可变类的唯一真正缺点是它们需要为每个不同的值分别使用一个对象.
至少有两个原因.
第一 - 安全 http://www.javafaq.nu/java-article1060.html
String变为不可变的主要原因是安全性.看看这个例子:我们有一个带登录检查的文件打开方法.我们将String传递给此方法以处理在将调用传递给OS之前必需的身份验证.如果String是可变的,那么在OS从程序获得请求之前,可以以某种方式在身份验证检查之后修改其内容,然后可以请求任何文件.因此,如果您有权在用户目录中打开文本文件,但是当您以某种方式设置更改文件名时,您可以请求打开"passwd"文件或任何其他文件.然后可以修改文件,并且可以直接登录到OS.
第二 - 记忆效率 http://hikrish.blogspot.com/2006/07/why-string-class-is-immutable.html
JVM在内部维护"字符串池".为了实现内存效率,JVM将从池中引用String对象.它不会创建新的String对象.因此,无论何时创建新的字符串文字,JVM都会在池中检查它是否已存在.如果已存在于池中,只需提供对同一对象的引用或在池中创建新对象.将有许多引用指向相同的String对象,如果有人更改了值,它将影响所有引用.所以,太阳决定让它变得一成不变.
实际上,原因字符串在java中是不可变的,与安全性没有多大关系.主要原因如下:
字符串是极其广泛使用的对象类型.因此,或多或少保证在多线程环境中使用它.字符串是不可变的,以确保在线程之间共享字符串是安全的.拥有不可变的字符串可确保在将字符串从线程A传递到另一个线程B时,线程B不会意外地修改线程A的字符串.
这不仅有助于简化已经非常复杂的多线程编程任务,而且还有助于提高多线程应用程序的性能.当可以从多个线程访问可变对象时,必须以某种方式同步对它们的访问,以确保一个线程在被另一个线程修改时不会尝试读取对象的值.正确的同步对于程序员来说很难正确执行,并且在运行时很昂贵.不可变对象无法修改,因此不需要同步.
虽然已经提到了String interning,但它只代表了Java程序内存效率的一小部分增益.仅限字符串文字.这意味着只有源代码中相同的字符串才会共享相同的String对象.如果您的程序动态创建相同的字符串,它们将在不同的对象中表示.
更重要的是,不可变字符串允许它们共享其内部数据.对于许多字符串操作,这意味着不需要复制基础字符数组.例如,假设您想要获取String的前五个字符.在Java中,您将调用myString.substring(0,5).在这种情况下,substring()方法所做的只是创建一个新的String对象,该对象共享myString的底层char [],但是谁知道它从索引0开始并在该char []的索引5处结束.要以图形形式显示,最终会得到以下结果:
| myString | v v "The quick brown fox jumps over the lazy dog" <-- shared char[] ^ ^ | | myString.substring(0,5)
这使得这种操作非常便宜,并且O(1)因为操作既不依赖于原始字符串的长度,也不依赖于我们需要提取的子字符串的长度.此行为也有一些内存优势,因为许多字符串可以共享其底层char [].
线程安全性和性能.如果无法修改字符串,则可以安全快速地在多个线程中传递引用.如果字符串是可变的,您将始终必须将字符串的所有字节复制到新实例,或提供同步.每次需要修改字符串时,典型应用程序将读取字符串100次.请参阅维基百科的不变性.
人们应该真的问,"为什么X应该是可变的?" 由于Princess Fluff已经提到的好处,最好默认为不变性.事情是可变的应该是一个例外.
不幸的是,大多数当前的编程语言都默认为可变性,但希望将来默认更多的是不可变性(参见下一个主流编程语言的愿望清单).
一个因素是,如果字符串是可变的,则存储字符串的对象必须小心存储副本,以免其内部数据发生变化而不另行通知.鉴于字符串是一个相当原始的类型,如数字,当人们可以将它们视为按值传递时,即使它们通过引用传递(这也有助于节省内存),这是很好的.
String不是基本类型,但您通常希望将它与值语义一起使用,即像值一样.
值得你信赖的东西不会在你背后改变.如果你写:String
你不希望它改变,除非你用str做某事.
作为Object的字符串具有自然的指针语义,以获得值语义,它也需要是不可变的.
哇!我不敢相信这里的错误信息.不可变的字符串与安全性无关.如果有人已经可以访问正在运行的应用程序中的对象(如果你试图防止某人'黑客攻击你的应用程序中的字符串,则必须假设),他们肯定会有很多其他可用于黑客攻击的机会.
这是一个非常新颖的想法,String的不变性正在解决线程问题.嗯......我有一个被两个不同线程改变的对象.我该如何解决这个问题?同步访问对象?Naawww ......让我们不要让任何人改变对象 - 这将解决我们所有混乱的并发问题!实际上,让我们使所有对象不可变,然后我们可以从Java语言中删除同步的构造.
真正的原因(上面的其他人指出)是内存优化.在任何应用程序中,重复使用相同的字符串文字是很常见的.事实上,在几十年前,许多编译器都优化了只存储字符串文字的单个实例.这种优化的缺点是修改字符串文字的运行时代码引入了一个问题,因为它正在为共享它的所有其他代码修改实例.例如,对于应用程序中的某个函数来说,将字符串文字"dog"更改为"cat"并不好.printf("dog")会导致"cat"被写入stdout.出于这个原因,需要有一种方法来防止尝试更改字符串文字的代码(即,使它们不可变).一些编译器(在操作系统的支持下)可以通过将字符串文字放入一个特殊的只读内存段来完成此操作,如果进行了写入尝试,则会导致内存错误.
在Java中,这被称为实习.这里的Java编译器只是遵循编译器几十年来完成的标准内存优化.为了解决在运行时修改这些字符串文字的相同问题,Java只是使String类不可变(即,不会为您提供允许您更改String内容的setter).如果没有发生字符串文字的实习,则字符串不必是不可变的.
我知道这是一个颠簸,但......他们真的是不变的吗?考虑以下.
public static unsafe void MutableReplaceIndex(string s, char c, int i) { fixed (char* ptr = s) { *((char*)(ptr + i)) = c; } }
...
string s = "abc"; MutableReplaceIndex(s, '1', 0); MutableReplaceIndex(s, '2', 1); MutableReplaceIndex(s, '3', 2); Console.WriteLine(s); // Prints 1 2 3
你甚至可以把它作为一种扩展方法.
public static class Extensions { public static unsafe void MutableReplaceIndex(this string s, char c, int i) { fixed (char* ptr = s) { *((char*)(ptr + i)) = c; } } }
这使得以下工作
s.MutableReplaceIndex('1', 0); s.MutableReplaceIndex('2', 1); s.MutableReplaceIndex('3', 2);
结论:它们处于编译器已知的不可变状态.虽然上面只适用于.NET字符串,因为Java没有指针.但是,使用C#中的指针可以完全改变字符串.这不是指针的使用方式,实际用途或安全使用; 然而,它可能会因此而弯曲整个"可变"规则.您通常不能直接修改字符串的索引,这是唯一的方法.有一种方法可以通过禁止字符串的指针实例或在指向字符串时制作副本来防止这种情况,但两者都没有完成,这使得C#中的字符串不完全不可变.