为什么他们决定在Java和.NET(以及其他一些语言)中使字符串不可变? 他们为什么不让它变得可变?
根据Effective Java,第4章,第73页,第2版:
"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.
至少有两个原因。
第一 - 安全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.
实际上,原因字符串在java中是不可变的,与安全性没有多大关系。主要原因如下:
Thead安全:
字符串是极其广泛使用的对象类型。因此,或多或少保证在多线程环境中使用它。字符串是不可变的,以确保在线程之间共享字符串是安全的。拥有不可变的字符串可确保在将字符串从线程A传递到另一个线程B时,线程B不会意外地修改线程A的字符串。
这不仅有助于简化已经非常复杂的多线程编程任务,而且还有助于提高多线程应用程序的性能。当可以从多个线程访问可变对象时,必须以某种方式同步对它们的访问,以确保一个线程在被另一个线程修改时不会尝试读取对象的值。正确的同步对于程序员来说很难正确执行,并且在运行时很昂贵。不可变对象无法修改,因此不需要同步。
性能:
虽然已经提到了String interning,但它只代表了Java程序内存效率的一小部分增益。仅限字符串文字。这意味着只有源代码中相同的字符串才会共享相同的String对象。如果您的程序动态创建相同的字符串,它们将在不同的对象中表示。
更重要的是,不可变字符串允许它们共享其内部数据。对于许多字符串操作,这意味着不需要复制基础字符数组。例如,假设您想要获取String的前五个字符。在Java中,您将调用myString.substring(0,5)。在这种情况下,substring()方法所做的只是创建一个新的String对象,该对象共享myString的底层char [],但是谁知道它从索引0开始并在该char []的索引5处结束。要以图形形式显示,最终会得到以下结果:
1 2 3 4 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 = someExpr();
除非你用str做某事,否则你不希望它改变。
作为Object的字符串具有自然的指针语义,以获取值语义,它也需要是不可变的。
哇!我不敢相信这里的错误信息。不可变的字符串与安全性无关。如果有人已经可以访问正在运行的应用程序中的对象(如果你试图防止某人在你的应用程序中"黑客攻击"某个字符串,则必须假设这些对象),他们肯定会有很多其他可用于黑客攻击的机会。
这是一个非常新颖的想法,String的不变性正在解决线程问题。嗯......我有一个被两个不同线程改变的对象。我该如何解决这个问题?同步访问对象? Naawww ......让我们不要让任何人改变对象 - 这将解决我们所有混乱的并发问题!实际上,让我们使所有对象不可变,然后我们可以从Java语言中删除同步的构造。
真正的原因(上面的其他人指出)是内存优化。在任何应用程序中,重复使用相同的字符串文字是很常见的。事实上,在几十年前,许多编译器都优化了只存储字符串文字的单个实例。这种优化的缺点是修改字符串文字的运行时代码引入了一个问题,因为它正在为共享它的所有其他代码修改实例。例如,对于应用程序中的某个函数来说,将字符串文字"dog"更改为"cat"并不好。 printf("dog")会导致"cat"被写入stdout。出于这个原因,需要有一种方法来防止试图改变字符串文字的代码(即,使它们不可变)。一些编译器(在操作系统的支持下)可以通过将字符串文字放入一个特殊的只读内存段来完成此操作,如果进行了写入尝试,则会导致内存错误。
在Java中,这被称为实习。这里的Java编译器只是遵循编译器几十年来完成的标准内存优化。为了解决在运行时修改这些字符串文字的相同问题,Java只是使String类不可变(即,不提供允许您更改String内容的setter)。如果没有发生字符串文字的实习,则字符串不必是不可变的。
我知道这是一个颠簸,但......
他们真的是不变的吗?
考虑以下。
1 2 3 4 5 6 7
| public static unsafe void MutableReplaceIndex(string s, char c, int i)
{
fixed (char* ptr = s)
{
*((char*)(ptr + i)) = c;
}
} |
...
1 2 3 4 5
| string s ="abc";
MutableReplaceIndex(s, '1', 0);
MutableReplaceIndex(s, '2', 1);
MutableReplaceIndex(s, '3', 2);
Console.WriteLine(s); // Prints 1 2 3 |
你甚至可以把它作为一种扩展方法。
1 2 3 4 5 6 7 8 9 10
| public static class Extensions
{
public static unsafe void MutableReplaceIndex(this string s, char c, int i)
{
fixed (char* ptr = s)
{
*((char*)(ptr + i)) = c;
}
}
} |
这使得以下工作
1 2 3
| s.MutableReplaceIndex('1', 0);
s.MutableReplaceIndex('2', 1);
s.MutableReplaceIndex('3', 2); |
结论:它们处于编译器已知的不可变状态。虽然上面只适用于.NET字符串,因为Java没有指针。但是,使用C#中的指针可以完全改变字符串。这不是指针的使用方式,实际用途或安全使用;然而,它可能会因此而弯曲整个"可变"规则。您通常不能直接修改字符串的索引,这是唯一的方法。有一种方法可以通过禁止字符串的指针实例或在指向字符串时制作副本来防止这种情况,但两者都没有完成,这使得C#中的字符串不完全不可变。
对于大多数目的,"字符串"(使用/被视为/被认为是/假设为)有意义的原子单元,就像数字一样。
因此,询问为什么字符串的各个字符不可变,就像问为什么整数的各个位不可变。
你应该知道为什么。考虑一下。
我讨厌这样说,但不幸的是我们正在讨论这个问题,因为我们的语言很糟糕,我们试图用一个单词,字符串来描述一个复杂的,在上下文中定位的概念或一类对象。
我们用"字符串"进行计算和比较,类似于我们对数字的处理方式。如果字符串(或整数)是可变的,我们必须编写特殊代码将其值锁定为不可变的本地形式,以便可靠地执行任何类型的计算。因此,最好将字符串视为数字标识符,而不是16位,32位或64位长,它可能是数百位长。
当有人说"字符串"时,我们都会想到不同的东西。那些把它简单地想成一组人物而没有特别目的的人,当然会对某人只是决定他们不应该操纵这些人物感到震惊。但"字符串"类不仅仅是一个字符数组。它是STRING,而不是char[]。关于我们称之为"字符串"的概念有一些基本的假设,它通常可以被描述为有意义的,编码数据的原子单位,如数字。当人们谈论"操纵字符串"时,也许他们真的在谈论操纵字符来构建字符串,而StringBuilder非常适合。试想一下"字符串"这个词的真正含义。
考虑一下如果字符串是可变的那将是什么样的。如果在此函数使用时,另一个线程有意或无意地修改了可变用户名字符串,则可以欺骗以下API函数返回其他用户的信息:
1 2 3 4 5 6 7 8 9
| 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类。
Java中的字符串并不是真正不可变的,您可以使用反射和/或类加载来更改它们的值。出于安全考虑,您不应该依赖该属性。
有关示例,请参阅:Java中的魔术技巧
这是一个折衷。字符串进入字符串池,当您创建多个相同的字符串时,它们共享相同的内存。设计人员认为这种内存节省技术适用于常见情况,因为程序往往会磨损相同的字符串。
缺点是连接会产生许多额外的字符串,这些字符串只是过渡性的,只会变成垃圾,实际上会损害内存性能。你有StringBuffer和StringBuilder(在Java中,StringBuilder也在.NET中)用于在这些情况下保留内存。
决定在C ++中使用字符串变量会导致很多问题,请参阅Kelvin Henney关于Mad COW Disease的优秀文章。
COW =写入时复制。
不变性很好。请参阅Effective Java。如果每次传递时都必须复制一个String,那么这将是很多容易出错的代码。您还会对哪些修改影响哪些引用感到困惑。就像Integer必须是不可变的行为类似于int一样,字符串必须表现为像基元一样不可变。在C ++中,按值传递字符串在源代码中没有明确提及。
几乎每条规则都有一个例外:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| using System;
using System.Runtime.InteropServices;
namespace Guess
{
class Program
{
static void Main(string[] args)
{
const string str ="ABC";
Console.WriteLine(str);
Console.WriteLine(str.GetHashCode());
var handle = GCHandle.Alloc(str, GCHandleType.Pinned);
try
{
Marshal.WriteInt16(handle.AddrOfPinnedObject(), 4, 'Z');
Console.WriteLine(str);
Console.WriteLine(str.GetHashCode());
}
finally
{
handle.Free();
}
}
}
} |
这主要是出于安全考虑。如果你不相信你的字符串是防篡改的,那么保护系统要困难得多。