为什么字符串在Java和.NET中不可变?


189

他们为什么决定使用StringJava和.NET(和其他一些语言)使其不可变?他们为什么不使它可变?


13
我确实有同样的想法,但是检查了原始海报的位置,发现它们来自比利时。鉴于这意味着他们不太可能会说英语。加上大多数当地人对该语言的掌握松散,我决定减少她的懈怠。
belugabob

8
谢谢你belugabob,但我不是她,我是他。显然,人们没有考虑文化差异。
chrissie1

7
我很抱歉-克里斯(在英国通常是一个女孩的名字)-使我成为另一种文化差异的受害者:-)
belugabob

请注意,.NET String实际上在内部是可变的。StringBuilder.NET 2.0中的变量会更改字符串。我就把它留在这里。
Alvin Wong

实际上.NET字符串可变的。而且这甚至不是骇客之举。
Bitterblue 2014年

Answers:


203

根据Effective Java第四章第73页第2版:

“这样做有很多充分的理由:不可变的类比可变的类更易于设计,实现和使用。它们不易出错并且更安全。

[...]

不可变对象很简单。不可变对象可以恰好处于一种状态,即创建时的状态。如果确保所有构造函数都建立类不变式,则可以确保这些不变式在所有时间都保持为真。您无需付出任何努力。

[...]

不可变的对象本质上是线程安全的。他们不需要同步。它们不能被同时访问它们的多个线程破坏。这无疑是实现线程安全的最简单方法。实际上,没有线程能够观察到另一个线程对不可变对象的任何影响。因此, 不可变的对象可以自由共享

[...]

同一章的其他要点:

您不仅可以共享不可变的对象,还可以共享它们的内部。

[...]

不可变的对象是可变或不可变的其他对象的重要构建块。

[...]

不变类的唯一真正的缺点是,对于每个不同的值,它们都需要一个单独的对象。


22
阅读我的回答的第二句话:不可变类比可变类更易于设计,实现和使用。它们不太容易出错,并且更安全。
公主绒毛2011年

5
@PRINCESSFLUFF我要补充一点,即使在单个线程上,共享可变字符串也是危险的。例如,复制报告:report2.Text = report1.Text;。然后,在其他地方修改文本:report2.Text.Replace(someWord, someOtherWord);。这将更改第一份报告以及第二份报告。
phoog 2012年

10
@Sam他没有问“为什么他们不可变”,他问“为什么他们决定不可变”,这很好地回答了。
詹姆斯

1
@PRINCESSFLUFF这个答案没有专门针对字符串。那是OP的问题。太令人沮丧了-这种情况一直在SO上以及String不变性问题中发生。这里的答案讨论了不变性的一般好处。那么,为什么所有类型都不都是不变的呢?您能回头再说一下字符串吗?
Howiecamp

@Howiecamp我认为答案可能是可变的,这暗示着字符串是隐含的(没有任何东西可以阻止假设的可变字符串类的存在)。他们只是为了简化而决定不这样做,因为它涵盖了99%的使用案例。他们仍然为其他1%的案例提供StringBuilder。
DanielGarcíaRubio

102

至少有两个原因。

第一-安全性 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决定使其保持不变。


这是一个关于重用的好地方,尤其是在使用String.intern()时。在不使所有字符串不变的情况下进行重用是可能的,但那时的生活往往会变得复杂。
jsight

3
在当今时代,这两个对我来说似乎都不是非常有效的理由。
Brian Knoblauch

1
我不太相信内存效率参数(即,当两个或多个String对象共享同一数据,并且其中一个被修改时,两个都被修改)。MFC中的CString对象通过使用引用计数来解决此问题。
罗布

7
对于字符串不变,安全性并不是真正的理由-您的操作系统会将字符串复制到内核模式缓冲区并在其中进行访问检查,以避免定时攻击。这实际上与线程安全性和性能有关:)
09年

1
内存效率参数也不起作用。在像C这样的本地语言中,字符串常量只是指向已初始化数据部分中的数据的指针-它们始终是只读/不可变的。“如果有人更改了值”-同样,池中的字符串无论如何都是只读的。
wj32 2009年

57

实际上,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 []。


6
将子字符串实现为共享基础的引用char[]是一个相当可疑的设计决策。如果您将整个文件读入单个字符串并维护对一个1个字符的子字符串的引用,则整个文件将必须保留在内存中。
加布

5
确实,我在创建一个网站爬网程序时遇到了这个难题,该爬网程序只需要从整个页面中提取几个单词即可。整个页面的HTML代码都在内存中,并且由于子字符串共享char [],即使我只需要几个字节,我仍保留了整个HTML代码。一种解决方法是使用新的String(original.substring(..,..)),String(String)构造函数复制基础数组的相关范围。
LordOfThePigs

1
涵盖后续更改的附录:自Jave 7起,String.substring()执行完整副本是为了防止上述注释中提及的问题。在Java 8中,删除了两个char[]共享区域,即countoffset,从而减少了String实例的内存占用。
Christian Semrau

我同意Thead安全性部分,但对子字符串的情况有所怀疑。
Gqqnbig 2014年

@LoveRight:然后检查java.lang.String(grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/…)的源代码,直到Java 6为止(在撰写此答案时是最新的)。我在Java 7中显然已经改变
LordOfThePigs

28

线程安全性和性能。如果不能修改字符串,则可以安全快捷地在多个线程之间传递引用。如果字符串是可变的,则始终必须将字符串的所有字节复制到新实例,或提供同步。典型的应用程序每次需要修改字符串时,都会读取该字符串100次。有关不变性,请参见维基百科。



7

哇!我不敢相信这里的错误信息。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不必是不变的。


3
我坚决反对不变性和线程注释,在我看来,您还不太了解这一点。如果Java实现者之一乔什·布洛赫(Josh Bloch)说那是设计问题之一,那怎么会是错误的信息呢?
javashlook

1
同步很昂贵。对可变对象的引用需要同步,而不是不变的。这就是使所有对象不可变的原因,除非它们必须是可变的。字符串可以是不可变的,因此可以使它们在多个线程中更有效。
David Thornley,2009年

5
@Jim:内存优化不是“ THE”原因,而是“ A”原因。线程安全也是'A'的原因,因为不可变对象本质上是线程安全的,并且不需要昂贵的同步,正如David提到的。线程安全实际上是对象不可变的副作用。您可以将同步视为使对象“暂时”不可变的一种方式(ReaderWriterLock将其设为只读,而常规锁将使其完全不可访问,这当然也使该对象不可变)。
Triynko,2009年

1
@DavidThornley:为可变的价值持有者创建多个独立的引用路径实际上将其变成了一个实体,并且除了线程问题之外,还很难进行推理。通常,在每个对象都只有一个引用路径的情况下,可变对象要比不可变对象有效,但是不可变对象允许通过共享引用来有效共享对象的内容。最好的模式由String和所代表StringBuffer,但不幸的是,其他很少的类型遵循该模型。
supercat 2014年

7

String 不是原始类型,但是您通常希望将其与值语义一起使用,即类似于值。

价值是您可以信赖的东西,不会在您的背后改变。如果您写:String str = someExpr(); 除非您使用进行某些操作,否则您不希望更改str

String作为Object自然具有指针语义的语言,要获取值语义也需要不可变的。


7

一个因素是,如果Strings是可变的,则存储Strings的对象将必须小心存储副本,以免其内部数据发生更改而不另行通知。鉴于Strings是一种非常原始的类型,例如数字,最好将它们视为按值传递,即使它们通过引用传递也是如此(这也有助于节省内存)。


6

我知道这是一个障碍,但是...它们真的是一成不变的吗?考虑以下。

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#中的字符串不完全不变。


1
+1。.NET字符串并不是真正不变的。实际上,出于性能方面的考虑,这始终在String和StringBuilder类中进行。
詹姆斯·柯

3

在大多数情况下,“字符串”是(像数字一样)被(认为/认为/认为/是)有意义的原子单元

因此,询问为什么字符串的各个字符不可变就像询问为什么整数的各个位不可变一样。

你应该知道为什么。考虑一下。

我不想这么说,但是不幸的是,我们正在辩论这个问题,因为我们的语言很烂,并且我们试图使用单个单词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 );
    }
}

安全不仅涉及“访问控制”,还涉及“安全”和“保证正确性”。如果不能轻易地编写和依赖一种方法来可靠地执行简单的计算或比较,则调用该方法并不安全,但是对编程语言本身提出疑问将是安全的。


在C#中,字符串可以通过其指针(使用unsafe)或仅通过反射(您可以轻松获得基础字段)来改变。这使安全性问题变得毫无意义,因为任何有意更改字符串的人都可以很容易地做到这一点。但是,它为程序员提供了安全性:除非您做一些特殊的事情,否则字符串肯定是不可变的(但是它不是线程安全的!)。
亚伯

是的,您可以通过指针更改任何数据对象的字节(字符串,整数等)。但是,我们正在谈论为什么字符串类是不可变的,因为它没有内置用于修改其字符的公共方法。我说的是,字符串与数字很像,因为处理单个字符比处理数字的单个位(将字符串作为整体标记(而不是字节数组)和数字作为一个数值(不是位字段),我们是在概念性对象级别,而不是在子对象级别
Triynko

2
需要澄清的是,面向对象代码中的指针本质上是不安全的,这完全是因为它们规避了为类定义的公共接口。我的意思是,如果一个字符串的公共接口允许其被其他线程修改,则该函数很容易被欺骗。当然,总是可以通过直接使用指针访问数据来欺骗它,而不是那么容易或无意中。
Triynko,2009年

1
除非您将其称为引用,否则“面向对象代码中的指针本质上是不安全的” 。Java中的引用与C ++中的指针没有区别(仅禁用了指针算术)。一个不同的概念是可以管理或手动管理的内存管理,但这是不同的事情。您可以在没有GC的情况下使用参考语义(没有算术的指针)(相反,在可到达性的语义更难理解但并非不可行的意义上,难度会更大)
-dribeas 2010年

另一件事是,如果字符串几乎是不可变的,但事实并非如此,(在这里我不了解足够的CLI),出于安全原因,这真的很糟糕。在某些较旧的Java实现中,您可以执行此操作,并且我发现了一段代码,用于片段字符串(尝试查找具有相同值的其他内部字符串,共享指针,删除旧的内存块)并使用后门重写字符串内容,以强制其他类中的错误行为。(考虑重写“SELECT *”到“DELETE”)
大卫·罗德里格斯- dribeas

3

不变性与安全性并没有那么紧密地联系在一起。为此,至少在.NET中,您获得了SecureString该类。

以后的编辑:在Java中,您会发现GuardedString,一个类似的实现。


2

在C ++中使字符串可变的决定会引起很多问题,请参阅Kelvin Henney撰写的有关Mad COW Disease的出色文章。

COW =写时复制。


2

这是一个权衡。String进入String池,当您创建多个相同String的时,它们共享相同的内存。设计人员认为,这种内存节省技术在常见情况下会很好地起作用,因为程序往往会在相同的字符串上进行大量磨削。

缺点是串联会带来很多额外String的,这些额外的只是过渡性的,只会变成垃圾,实际上会损害内存性能。在这些情况下,您必须使用StringBufferStringBuilder(在Java中,StringBuilder在.NET中也)来保留内存。


1
请记住,除非您显式使用“ inter()” ed字符串,否则不会自动将“字符串池”用于所有字符串。
jsight

2

StringJava中的s并不是真正不变的,您可以使用反射和/或类加载更改其值。您不应该依赖该属性来确保安全性。有关示例,请参见:Java中的魔术技巧


1
我相信,如果您的代码在完全信任的情况下运行,您将只能做这些技巧,因此不会造成安全损失。您也可以使用JNI直接在存储字符串的内存位置上写。
Antoine Aubry,2009年

实际上,我相信您可以通过反射更改任何不可变的对象。
Gqqnbig 2014年

0

不变性是好的。请参阅有效的Java。如果每次传递字符串时都必须复制一个String,那么这将是很多容易出错的代码。您还对哪些修改会影响哪些引用感到困惑。与Integer必须不可变才能像int一样,String也必须不可变要像原语一样。在C ++中,按值传递字符串会执行此操作,而无需在源代码中明确提及。


0

几乎每条规则都有一个例外:

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();
            }
        }
    }
}

-1

主要是出于安全原因。如果您不相信自己String的系统是防篡改的,那么保护系统的安全就困难得多。


1
您能否举一个“防篡改”的意思的例子。这个答案感觉确实与上下文无关。
Gergely Orosz
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.