为什么他们决定String
在 Java 和 .NET(以及其他一些语言)中实现不可变?他们为什么不让它可变呢?
17 回答
根据Effective Java,第 4 章,第 73 页,第 2 版:
“这有很多很好的理由:不可变类比可变类更容易设计、实现和使用。它们更不容易出错并且更安全。
[...]
"不可变对象很简单。一个不可变对象可以只处于一种状态,即创建它的状态。如果您确保所有构造函数都建立类不变量,那么可以保证这些不变量将始终保持正确,并且没有你的努力。
[...]
不可变对象本质上是线程安全的;它们不需要同步。它们不会被同时访问它们的多个线程破坏。这无疑是实现线程安全的最简单方法。事实上,没有线程可以观察到另一个线程对不可变对象的任何影响。因此, 不可变对象可以自由共享
[...]
同一章的其他小点:
您不仅可以共享不可变对象,还可以共享它们的内部结构。
[...]
不可变对象为其他对象(无论是可变的还是不可变的)提供了很好的构建块。
[...]
不可变类唯一真正的缺点是它们需要为每个不同的值使用单独的对象。
至少有两个原因。
首先 - 安全 http://www.javafaq.nu/java-article1060.html
String 不可变的主要原因是安全性。看这个例子:我们有一个带有登录检查的文件打开方法。我们将一个字符串传递给此方法以处理身份验证,这是在将调用传递给操作系统之前所必需的。如果 String 是可变的,则可以在操作系统从程序获取请求之前在身份验证检查之后以某种方式修改其内容,则可以请求任何文件。因此,如果您有权在用户目录中打开文本文件,但是当您设法更改文件名时,您可以请求打开“passwd”文件或任何其他文件。然后可以修改文件并且可以直接登录到操作系统。
第二 - 内存效率 http://hikrish.blogspot.com/2006/07/why-string-class-is-immutable.html
JVM 内部维护着“字符串池”。为了达到内存效率,JVM 将从池中引用 String 对象。它不会创建新的 String 对象。因此,每当您创建一个新的字符串文字时,JVM 都会在池中检查它是否已经存在。如果池中已经存在,只需提供对同一对象的引用或在池中创建新对象。会有很多引用指向同一个String对象,如果有人改变了值,就会影响所有的引用。所以,sun 决定让它不可变。
实际上,字符串在java中不可变的原因与安全性没有太大关系。主要原因有以下两个:
头部安全:
字符串是极其广泛使用的对象类型。因此,它或多或少地保证在多线程环境中使用。字符串是不可变的,以确保在线程之间共享字符串是安全的。拥有一个不可变的字符串可确保在将字符串从线程 A 传递到另一个线程 B 时,线程 B 不会意外地修改线程 A 的字符串。
这不仅有助于简化已经相当复杂的多线程编程任务,而且还有助于提高多线程应用程序的性能。当可以从多个线程访问可变对象时,必须以某种方式同步对可变对象的访问,以确保一个线程在另一个线程修改对象时不会尝试读取对象的值。对于程序员来说,正确的同步既很难正确完成,而且在运行时代价高昂。不可变对象无法修改,因此不需要同步。
表现:
虽然已经提到了字符串实习,但它仅代表 Java 程序的内存效率的一小部分增益。只有字符串文字被实习。这意味着只有源代码中相同的字符串才会共享相同的字符串对象。如果您的程序动态创建相同的字符串,它们将在不同的对象中表示。
更重要的是,不可变字符串允许它们共享内部数据。对于许多字符串操作,这意味着不需要复制底层字符数组。例如,假设您要取 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 应该是可变的?” 最好默认为不变性,因为Fluff 公主已经提到了好处。某些东西是可变的应该是一个例外。
不幸的是,当前大多数编程语言都默认为可变性,但希望将来默认更多的是不可变性(请参阅下一个主流编程语言的愿望清单)。
String
不是原始类型,但您通常希望将它与值语义一起使用,即像值一样。
价值是你可以相信不会在你背后改变的东西。如果你写:String str = someExpr();
你不希望它改变,除非你用str
.
String
由于 anObject
具有自然的指针语义,因此要获得值语义,它也需要是不可变的。
哇!我不能相信这里的错误信息。String
s 是不可变的,没有任何安全性。如果某人已经可以访问正在运行的应用程序中的对象(如果您试图防止有人String
在您的应用程序中“入侵”a,则必须假设这一点),那么他们肯定会有很多其他可用于黑客攻击的机会。
String
不变性正在解决线程问题,这是一个非常新颖的想法。嗯...我有一个对象正在被两个不同的线程更改。我该如何解决这个问题?同步访问对象?Naawww ...我们不要让任何人更改对象——这将解决我们所有混乱的并发问题!事实上,让我们让所有对象不可变,然后我们可以从 Java 语言中删除同步结构。
真正的原因(上面其他人指出)是内存优化。在任何应用程序中重复使用相同的字符串文字是很常见的。事实上,它是如此普遍,以至于几十年前,许多编译器都对只存储一个String
文字实例进行了优化。这种优化的缺点是修改String
文字的运行时代码会引入问题,因为它正在修改共享它的所有其他代码的实例。例如,应用程序中某处的函数将String
文字更改"dog"
为"cat"
. Aprintf("dog")
将导致"cat"
被写入标准输出。出于这个原因,需要有一种方法来防范试图更改的代码String
文字(即,使它们不可变)。一些编译器(在操作系统的支持下)会通过将String
文字放入特殊的只读内存段来完成此操作,如果进行写入尝试,则会导致内存错误。
在 Java 中,这称为实习。这里的 Java 编译器只是遵循编译器几十年来所做的标准内存优化。并且为了解决在运行时修改这些String
文字的相同问题,Java 只是使String
类不可变(即,不给您提供允许您更改String
内容的设置器)。如果没有发生文字String
的实习, s 就不必是不可变的。String
一个因素是,如果String
s 是可变的,则存储String
s 的对象必须小心存储副本,以免它们的内部数据在没有通知的情况下发生更改。鉴于String
s 是一种相当原始的类型,例如数字,如果可以将它们视为按值传递,即使它们是通过引用传递(这也有助于节省内存),这很好。
我知道这是一个颠簸,但是... 它们真的是一成不变的吗?考虑以下。
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# 中的字符串不完全不可变。
对于大多数目的,“字符串”是(使用/视为/认为/假设是)一个有意义的原子单位, 就像一个数字。
因此,询问为什么字符串的各个字符不可变就像问为什么整数的各个位不可变。
你应该知道为什么。考虑一下。
我不想这么说,但不幸的是,我们正在讨论这个问题,因为我们的语言很烂,我们试图使用一个单词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
该类。
稍后编辑:在 Java 中,您会发现GuardedString
类似的实现。
这是一个权衡。String
s 进入String
池,当您创建多个相同String
的 s 时,它们共享相同的内存。设计者认为这种节省内存的技术在常见情况下会很好用,因为程序往往会在相同的字符串上磨合很多。
缺点是串联会产生很多额外String
的 s,这些 s 只是过渡性的,会变成垃圾,实际上会损害内存性能。在这些情况下,您可以使用StringBuffer
和StringBuilder
(在 Java 中,StringBuilder
也在 .NET 中)来保留内存。
在 C++ 中使用可变字符串的决定会导致很多问题,请参阅 Kelvin Henney 撰写的这篇关于Mad COW 疾病的优秀文章。
COW = 写入时复制。
String
Java 中的 s 并不是真正不可变的,您可以使用反射和/或类加载来更改它们的值。您不应该依赖该属性来确保安全。例如,请参阅:Java 中的魔术技巧
不变性很好。请参阅有效的 Java。如果每次传递字符串时都必须复制它,那么这将是很多容易出错的代码。您还不清楚哪些修改会影响哪些引用。就像 Integer 必须是不可变的才能表现得像 int 一样,String 必须表现得像不可变的那样才能表现得像基元一样。在 C++ 中,按值传递字符串是在源代码中没有明确提及的。
几乎每条规则都有一个例外:
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();
}
}
}
}
这主要是出于安全原因。如果您不能相信您String
的 s 是防篡改的,那么保护系统的安全就会困难得多。