为什么字符串在Java和.NET中不可变?
为什么他们决定使字符串在Java和.NET(和其他一些语言)中不可变?他们为什么不使它可变?
解决方案
主要是出于安全原因。如果我们不相信自己的字符串是防篡改的,那么保护系统的安全就困难得多。
至少有两个原因。
第一安全http://www.javafaq.nu/java-article1060.html
The main reason why String made immutable was security. Look at this example: We have a file open method with login check. We pass a String to this method to process authentication which is necessary before the call will be passed to OS. If String was mutable it was possible somehow to modify its content after the authentication check before OS gets request from program then it is possible to request any file. So if you have a right to open text file in user directory but then on the fly when somehow you manage to change the file name you can request to open "passwd" file or any other. Then a file can be modified and it will be possible to login directly to OS.
第二内存效率http://hikrish.blogspot.com/2006/07/why-string-class-is-immutable.html
JVM internally maintains the "String Pool". To achive the memory efficiency, JVM will refer the String object from pool. It will not create the new String objects. So, whenever you create a new string literal, JVM will check in the pool whether it already exists or not. If already present in the pool, just give the reference to the same object or create the new object in the pool. There will be many references point to the same String objects, if someone changes the value, it will affect all the references. So, sun decided to make it immutable.
线程安全性和性能。如果无法修改字符串,则可以安全快捷地在多个线程之间传递引用。如果字符串是可变的,则始终必须将字符串的所有字节复制到新实例,或者提供同步。典型的应用程序每次需要修改字符串时,都会读取该字符串100次。有关不变性,请参见维基百科。
一个因素是,如果字符串是可变的,则存储字符串的对象将必须小心存储副本,以免其内部数据发生更改而不另行通知。鉴于字符串是一种非常原始的类型(如数字),最好将字符串视为按值传递,即使它们是按引用传递(这也有助于节省内存)。
这是一个权衡。字符串进入字符串池,当我们创建多个相同的字符串时,它们共享相同的内存。设计人员认为,这种内存节省技术在常见情况下会很好地起作用,因为程序往往会在相同的字符串上进行大量磨削。
缺点是串联会产生很多额外的字符串,这些字符串只是过渡性的,只会变成垃圾,实际上会损害内存性能。在这些情况下,我们可以使用StringBuffer和StringBuilder(在Java中,StringBuilder也在.NET中)来保留内存。
根据有效的Java,第4章,第73页,第二版:
"There are many good reasons for this: Immutable classes are easier to design, implement, and use than mutable classes. They are less prone to error and are more secure. [...] "Immutable objects are simple. An immutable object can be in exactly one state, the state in which it was created. If you make sure that all constructors establish class invariants, then it is guaranteed that these invariants will remain true for all time, with no effort on your part. [...] Immutable objects are inherently thread-safe; they require no synchronization. They cannot be corrupted by multiple threads accessing them concurrently. This is far and away the easiest approach to achieving thread safety. In fact, no thread can ever observe any effect of another thread on an immutable object. Therefore, immutable objects can be shared freely [...]
同一章中的其他要点:
Not only can you share immutable objects, but you can share their internals. [...] Immutable objects make great building blocks for other objects, whether mutable or immutable. [...] The only real disadvantage of immutable classes is that they require a separate object for each distinct value.
不变性是好的。请参阅有效的Java。如果每次传递字符串时都必须复制一个String,那么这将是很多容易出错的代码。我们还对哪些修改会影响哪些引用感到困惑。与Integer必须不可变才能像int一样,String也必须不可变要像原语一样。在C ++中,按值传递字符串会在源代码中没有明确提及的情况下执行此操作。
字符串不是原始类型,但我们通常希望将其与值语义(即值)一起使用。
价值是我们可以信赖的东西,不会在背后改变。
如果我们写:String str = someExpr();
除非我们对str做一些事情,否则我们不希望它改变。
字符串作为对象具有自然的指针语义,要获取值语义,它也必须是不可变的。
在C ++中使字符串可变的决定引起很多问题,请参阅Kelvin Henney撰写的有关Mad COW Disease的出色文章。
COW =写时复制。
一个人应该真正问:"为什么X是可变的?"由于Fluff公主已经提到的好处,最好默认为不变性。某些东西是易变的应该是一个例外。
不幸的是,大多数当前的编程语言默认都具有可变性,但希望将来的默认设置更多地是关于不可变性(请参见下一主流编程语言的愿望清单)。
Java中的字符串并不是真正不变的,我们可以使用反射和/或者类加载更改其值。我们不应该依赖该属性来确保安全性。
有关示例,请参见:Java中的魔术技巧
实际上,在Java中字符串不可变的原因与安全性没有太大关系。以下是两个主要原因:
Thead安全性:
字符串是非常广泛使用的对象类型。因此,它或者多或者少地保证可以在多线程环境中使用。字符串是不可变的,以确保在线程之间共享字符串是安全的。具有不变的字符串可确保将字符串从线程A传递到另一个线程B时,线程B不会意外地修改线程A的字符串。
这不仅有助于简化已经非常复杂的多线程编程任务,而且还有助于提高多线程应用程序的性能。当可以从多个线程访问可变对象时,必须以某种方式同步对它们的访问,以确保一个线程在另一线程修改对象时不会尝试读取该对象的值。正确的同步对于程序员来说很难正确完成,而且在运行时也很昂贵。不可变的对象无法修改,因此不需要同步。
表现:
尽管提到了String Interning,但它仅表示Java程序的内存效率有小幅提高。仅保留字符串文字。这意味着只有源代码中相同的字符串才会共享相同的String对象。如果程序动态创建相同的字符串,则它们将在不同的对象中表示。
更重要的是,不可变的字符串允许它们共享其内部数据。对于许多字符串操作,这意味着不需要复制基础字符数组。例如,假设我们要使用String的前五个字符。在Java中,我们将调用myString.substring(0,5)。在这种情况下,substring()方法所做的只是创建一个新的String对象,该对象共享myString的基础char [],但谁知道它始于该char []的索引0,结束于该索引的索引5. 要将其以图形形式显示,我们将得到以下结果:
| myString | v v "The quick brown fox jumps over the lazy dog" <-- shared char[] ^ ^ | | myString.substring(0,5)
这使得这种操作极其便宜,并且O(1),因为该操作既不依赖于原始字符串的长度,也不依赖于我们需要提取的子字符串的长度。此行为还具有一些内存上的好处,因为许多字符串可以共享其基础char []。
哇!我不敢相信这里的错误信息。不可变的字符串没有任何安全性。如果某人已经可以访问正在运行的应用程序中的对象(如果我们要防止某人"黑客"应用程序中的字符串,则必须假定这些对象),它们肯定会提供许多其他黑客机会。
String的不变性正在解决线程问题,这是一个非常新颖的想法。嗯...我有一个被两个不同线程更改的对象。我该如何解决?同步访问对象? Naawww ...让我们根本不让任何人更改对象-这将解决我们所有杂乱的并发问题!实际上,让我们使所有对象不可变,然后可以从Java语言中删除同步化的结构。
真正的原因(上面的其他人指出的)是内存优化。在任何应用程序中,重复使用相同的字符串文字是很常见的。实际上,它是如此普遍,以至于几十年前,许多编译器进行了优化,只存储一个字符串文字的单个实例。这种优化的缺点是,修改字符串文字的运行时代码会引入问题,因为它正在修改共享该字符串的所有其他代码的实例。例如,将应用程序中某处的函数将字符串文字" dog"更改为" cat"并不合适。 printf(" dog")将导致将" cat"写入标准输出。因此,需要一种方法来防止试图更改字符串文字的代码(即,使它们不变)。一些编译器(在OS的支持下)会通过将字符串文字放入特殊的只读内存段中来实现此目的,如果尝试进行写操作,这将导致内存错误。
在Java中,这称为"实习"。此处的Java编译器仅遵循几十年来由编译器完成的标准内存优化。为了解决在运行时修改这些字符串文字的相同问题,Java只是使String类不可变(即,不提供任何允许我们更改String内容的设置器)。如果不进行字符串文字的中间化,则字符串不必是不变的。
在大多数情况下,"字符串"是一个有意义的原子单元(用作/认为/认为/假定是),就像数字一样。
因此,询问为什么字符串的各个字符不可变就像询问为什么整数的各个位不可变一样。
你应该知道为什么。考虑一下。
我不想这么说,但是不幸的是,我们正在辩论这个问题,因为我们的语言很烂,而且我们试图使用单个单词,字符串来描述一个复杂的,上下文相关的概念或者对象类别。
我们使用"字符串"执行计算和比较,这与处理数字相似。如果字符串(或者整数)是可变的,则我们必须编写特殊的代码将其值锁定为不可变的局部形式,以便可靠地执行任何类型的计算。因此,最好将字符串看作一个数字标识符,但是它可能不是数百个16位,32位或者64位长。
当有人说"弦"时,我们都会想到不同的事物。那些只是将其视为一组字符而没有特定目的的人当然会感到震惊,因为有人刚决定他们不应该能够操纵这些字符。但是"字符串"类不仅是字符数组。这是一个" STRING",而不是一个" char []"。关于我们称为"字符串"的概念,有一些基本假设,并且通常可以将其描述为有意义的,原子化的编码数据(如数字)的单位。当人们谈论"操纵字符串"时,也许他们实际上是在谈论操纵字符来建造字符串,而StringBuilder对此非常有用。只需仔细考虑一下"字符串"一词的真正含义。
考虑一下,如果字符串可变,那会是什么样子。如果可变的用户名字符串在此函数使用时被另一个线程有意或者无意地修改,则可能会诱骗以下API函数为其他用户返回信息:
string GetPersonalInfo( string username, string password ) { string stored_password = DBQuery.GetPasswordFor( username ); if (password == stored_password) { //another thread modifies the mutable 'username' string return DBQuery.GetPersonalInfoFor( username ); } }
安全不仅涉及"访问控制",还涉及"安全"和"保证正确性"。如果不能轻易编写和依赖一种方法来可靠地执行简单的计算或者比较,则调用该方法并不安全,但是对编程语言本身提出质疑将是安全的。
不变性与安全性没有那么紧密的联系。为此,至少在.NET中,我们可以获得SecureString类。