他们为什么决定使用String
Java和.NET(和其他一些语言)使其不可变?他们为什么不使它可变?
String
实际上在内部是可变的。StringBuilder
.NET 2.0中的变量会更改字符串。我就把它留在这里。
他们为什么决定使用String
Java和.NET(和其他一些语言)使其不可变?他们为什么不使它可变?
String
实际上在内部是可变的。StringBuilder
.NET 2.0中的变量会更改字符串。我就把它留在这里。
Answers:
根据Effective Java第四章第73页第2版:
“这样做有很多充分的理由:不可变的类比可变的类更易于设计,实现和使用。它们不易出错并且更安全。
[...]
“ 不可变对象很简单。不可变对象可以恰好处于一种状态,即创建时的状态。如果确保所有构造函数都建立类不变式,则可以确保这些不变式在所有时间都保持为真。您无需付出任何努力。
[...]
不可变的对象本质上是线程安全的。他们不需要同步。它们不能被同时访问它们的多个线程破坏。这无疑是实现线程安全的最简单方法。实际上,没有线程能够观察到另一个线程对不可变对象的任何影响。因此, 不可变的对象可以自由共享
[...]
同一章的其他要点:
您不仅可以共享不可变的对象,还可以共享它们的内部。
[...]
不可变的对象是可变或不可变的其他对象的重要构建块。
[...]
不变类的唯一真正的缺点是,对于每个不同的值,它们都需要一个单独的对象。
report2.Text = report1.Text;
。然后,在其他地方修改文本:report2.Text.Replace(someWord, someOtherWord);
。这将更改第一份报告以及第二份报告。
至少有两个原因。
第一-安全性 http://www.javafaq.nu/java-article1060.html
字符串不可更改的主要原因是安全性。看这个例子:我们有一个带有登录检查的文件打开方法。我们将String传递给此方法来处理身份验证,这是将调用传递给OS之前所必需的。如果String是可变的,则可以在OS收到程序请求之前,通过身份验证检查以某种方式修改其内容,然后可以请求任何文件。因此,如果您有权在用户目录中打开文本文件,但随后又以某种方式设法更改文件名时立即打开,则可以请求打开“ passwd”文件或任何其他文件。然后可以修改文件,并且可以直接登录OS。
第二-内存效率 http://hikrish.blogspot.com/2006/07/why-string-class-is-immutable.html
JVM在内部维护“字符串池”。为了达到内存效率,JVM将从池中引用String对象。它不会创建新的String对象。因此,无论何时创建新的字符串文字,JVM都会在池中检查它是否已经存在。如果池中已经存在该对象,则只需提供对该对象的引用或在池中创建新对象。将有许多指向同一String对象的引用,如果有人更改了该值,它将影响所有引用。因此,sun决定使其保持不变。
实际上,java中字符串不可变的原因与安全性没有太大关系。以下是两个主要原因:
字符串是非常广泛使用的对象类型。因此,它或多或少地保证可以在多线程环境中使用。字符串是不可变的,以确保在线程之间共享字符串是安全的。拥有不可变的字符串可确保在将字符串从线程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 []。
char[]
是一个相当可疑的设计决策。如果您将整个文件读入单个字符串并维护对一个1个字符的子字符串的引用,则整个文件将必须保留在内存中。
String.substring()
执行完整副本是为了防止上述注释中提及的问题。在Java 8中,删除了两个char[]
共享区域,即count
和offset
,从而减少了String实例的内存占用。
一个人应该真正问:“为什么X是可变的?” 最好默认为不变性,因为Fluff公主已经提到了很多好处。某些东西是易变的应该是一个例外。
不幸的是,当前大多数编程语言都默认使用可变性,但希望将来默认使用更多关于不可变性的信息(请参阅“下一代主流编程语言的愿望清单”)。
哇!我不敢相信这里的错误信息。String
不可变的东西与安全无关。如果某人已经可以访问正在运行的应用程序中的对象(如果您要防止某人String
在您的应用程序中“黑客入侵”,则必须假定这些对象),那么他们肯定会有很多其他机会可以被黑客入侵。
这是一个非常新颖的想法,String
它解决线程问题是不变的。嗯...我有一个被两个不同线程更改的对象。我该如何解决?同步访问对象?Naawww ...让我们根本不要让任何人更改对象-这将解决我们所有杂乱的并发问题!实际上,让我们使所有对象不可变,然后我们可以从Java语言中删除同步化的结构。
真正的原因(上面的其他人指出的)是内存优化。在任何应用程序中,经常重复使用相同的字符串文字是很常见的。实际上,它是如此普遍,以至于几十年前,许多编译器都进行了优化,只存储String
文字的单个实例。这种优化的缺点是,修改String
文字的运行时代码会引入问题,因为它正在修改共享该文字的所有其他代码的实例。例如,这将是在应用程序中的更改功能的地方不好String
的文字"dog"
来"cat"
。A printf("dog")
将导致"cat"
被写入stdout。因此,需要一种防止尝试更改代码的方法String
文字(即,使其不变)。一些编译器(在OS的支持下)会通过将String
文字放在特殊的只读内存段中来实现此目的,如果尝试进行写操作,这将导致内存错误。
在Java中,这称为“实习”。此处的Java编译器仅遵循几十年来由编译器完成的标准内存优化。为了解决String
在运行时修改这些文字的相同问题,Java只是使String
该类不可变(即,不提供允许您更改String
内容的设置器)。String
如果不进行String
文字中间操作,则s不必是不变的。
String
和所代表StringBuffer
,但不幸的是,其他很少的类型遵循该模型。
我知道这是一个障碍,但是...它们真的是一成不变的吗?考虑以下。
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 );
}
}
安全不仅涉及“访问控制”,还涉及“安全”和“保证正确性”。如果不能轻易地编写和依赖一种方法来可靠地执行简单的计算或比较,则调用该方法并不安全,但是对编程语言本身提出疑问将是安全的。
unsafe
)或仅通过反射(您可以轻松获得基础字段)来改变。这使安全性问题变得毫无意义,因为任何有意更改字符串的人都可以很容易地做到这一点。但是,它为程序员提供了安全性:除非您做一些特殊的事情,否则字符串肯定是不可变的(但是它不是线程安全的!)。
不变性与安全性并没有那么紧密地联系在一起。为此,至少在.NET中,您获得了SecureString
该类。
以后的编辑:在Java中,您会发现GuardedString
,一个类似的实现。
在C ++中使字符串可变的决定会引起很多问题,请参阅Kelvin Henney撰写的有关Mad COW Disease的出色文章。
COW =写时复制。
这是一个权衡。String
进入String
池,当您创建多个相同String
的时,它们共享相同的内存。设计人员认为,这种内存节省技术在常见情况下会很好地起作用,因为程序往往会在相同的字符串上进行大量磨削。
缺点是串联会带来很多额外String
的,这些额外的只是过渡性的,只会变成垃圾,实际上会损害内存性能。在这些情况下,您必须使用StringBuffer
和StringBuilder
(在Java中,StringBuilder
在.NET中也)来保留内存。
String
Java中的s并不是真正不变的,您可以使用反射和/或类加载更改其值。您不应该依赖该属性来确保安全性。有关示例,请参见:Java中的魔术技巧
几乎每条规则都有一个例外:
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();
}
}
}
}