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

Why can't strings be mutable in Java and .NET?

提问人:chrissie1 提问时间:9/18/2008 最后编辑:deHaarchrissie1 更新时间:9/16/2019 访问量:45493

问:

为什么他们决定在 Java 和 .NET(以及其他一些语言)中实现不可变?他们为什么不让它变得可变?String

爪哇岛 。网 字符串 可变

评论

2赞 Alvin Wong 2/22/2013
请注意,在.NET中实际上是内部可变的。.NET 2.0 中的 StringBuilder 会更改字符串。我就把它留在这里。String
0赞 Bitterblue 7/8/2014
实际上,.NET 字符串可变的。这甚至不是一个黑客。

答:

-2赞 jsight 9/18/2008 #1

这主要是出于安全原因。如果你不能相信你的系统是防篡改的,那么保护系统就更难了。String

评论

2赞 Gergely Orosz 7/19/2011
您能举例说明您所说的“防篡改”是什么意思吗?这个答案感觉真的断章取义。
0赞 Sapphire_Brick 11/22/2020
按照你的逻辑,根本不应该有任何可变性,因为“如果你不能相信你的系统是防篡改的,那么保护系统就更难了”Object
104赞 Jorge Ferreira 9/18/2008 #2

至少有两个原因。

第一 - 安全 http://www.javafaq.nu/java-article1060.html

String 制作的主要原因 不可变的是安全性。看看这个 示例:我们有一个文件打开方法 通过登录检查。我们将一个字符串传递给 此方法处理身份验证 这是通话前必要的 将传递给操作系统。如果字符串是 可变的,有可能以某种方式 修改其内容后 操作系统获取之前的身份验证检查 从程序请求,然后是 可以请求任何文件。所以如果 您有权在以下位置打开文本文件 用户目录,但随后在飞行中 当您以某种方式设法更改 您可以请求打开的文件名 “passwd”文件或任何其他文件。然后一个 文件可以修改,它将 可以直接登录操作系统。

第二 - 内存效率 http://hikrish.blogspot.com/2006/07/why-string-class-is-immutable.html

JVM 在内部维护“字符串 游泳池“。为了实现记忆 efficiency,JVM 会引用 String 对象。它不会创建 新的 String 对象。所以,无论何时 创建一个新的字符串文本 JVM 将在池中检查它是否 是否已经存在。如果已经 存在于池中,只需给 引用同一对象或创建 池中的新对象。会有 许多参考资料都指向同一点 字符串对象,如果有人更改了 值,它会影响所有 引用。所以,Sun决定这样做 变。

评论

0赞 jsight 9/18/2008
这是关于重用的一个好点,如果你使用 String.intern(),尤其如此。在不使所有字符串都不可变的情况下,可以重用,但那时生活往往会变得复杂。
3赞 Brian Knoblauch 10/22/2008
在这个时代,这些似乎都不是非常合理的理由。
1赞 RobH 3/21/2009
我不太相信内存效率参数(即,当两个或多个 String 对象共享相同的数据时,其中一个被修改,然后两个对象都被修改)。MFC 中的 CString 对象通过使用引用计数来解决此问题。
10赞 snemarch 3/23/2009
对于不可变字符串来说,安全性并不是存在的理由 - 您的操作系统会将字符串复制到内核模式缓冲区并在那里进行访问检查,以避免定时攻击。这实际上都是关于螺纹安全和性能:)
1赞 wj32 3/24/2009
内存效率参数也不起作用。在像 C 这样的本地语言中,字符串常量只是指向初始化数据部分中数据的指针 - 无论如何它们都是只读/不可变的。“如果有人更改了值” - 同样,池中的字符串无论如何都是只读的。
32赞 Matt Howells 9/18/2008 #3

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

7赞 Evan DiBiase 9/18/2008 #4

One factor is that, if s were mutable, objects storing s would have to be careful to store copies, lest their internal data change without notice. Given that s are a fairly primitive type like numbers, it is nice when one can treat them as if they were passed by value, even if they are passed by reference (which also helps to save on memory).StringStringString

评论

0赞 Sapphire_Brick 11/22/2020
字符串并不像你想象的那样是“相当原始的类型”;原语可以廉价地复制、廉价地比较、廉价地存储、不能被 ,并且由编译器高度优化。这些都不是。nullString
2赞 aaronroyer 9/18/2008 #5

这是一个权衡。进入池中,当您创建多个相同的 s 时,它们共享相同的内存。设计者认为这种内存节省技术在常见情况下效果很好,因为程序往往会经常在相同的字符串上磨削。StringStringString

缺点是,串联会产生许多额外的 s,这些 s 只是过渡性的,只会变成垃圾,实际上会损害内存性能。在这些情况下,您有 and(在 Java 中,也在 .NET 中)用于保留内存。StringStringBufferStringBuilderStringBuilder

评论

1赞 jsight 9/18/2008
请记住,“字符串池”不会自动用于所有字符串,除非您显式使用 “inter()”'ed 字符串。
216赞 PRINCESS FLUFF 9/18/2008 #6

根据 Effective Java,第 4 章,第 73 页,第 2 版:

“这有很多很好的理由:不可变的类更容易 设计、实现和使用可变类。他们不太容易 出错,更安全。

[...]

"不可变对象很简单。不可变对象可以位于 正好是一个状态,即创建它的状态。如果您确定 所有构造函数都建立类不变量,那么它是 保证这些不变量将始终保持真,并且 你不费吹灰之力。

[...]

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

[...]

同一章的其他小要点:

您不仅可以共享不可变对象,还可以共享其内部结构。

[...]

不可变对象是其他对象(无论是可变对象还是不可变对象)的绝佳构建块。

[...]

不可变类的唯一真正缺点是它们需要为每个不同的值提供单独的对象。

评论

23赞 PRINCESS FLUFF 9/30/2011
阅读我回答的第二句话:不可变类比可变类更容易设计、实现和使用。它们不容易出错,而且更安全。
6赞 phoog 4/17/2012
@PRINCESSFLUFF 我要补充一点,即使在单个线程上共享可变字符串也是危险的。例如,复制报表:.然后,在其他地方修改文本:.这将改变第一份报告和第二份报告。report2.Text = report1.Text;report2.Text.Replace(someWord, someOtherWord);
12赞 James 5/2/2012
@Sam他没有问“为什么它们不能可变”,而是问“为什么他们决定使不可变”,这完美地回答了这个问题。
1赞 Howiecamp 7/30/2017
@PRINCESSFLUFF 此答案没有专门针对字符串。这就是OP的问题。这太令人沮丧了 - 这种情况在 SO 上一直发生,并且字符串不可变性问题也是如此。这里的答案谈到了不变性的一般好处。那么,为什么不是所有类型都是不可变的呢?你能回去解决字符串吗?
2赞 AlwaysLearning 1/16/2019
我仍然没有看到为什么出于同样的原因不使数组不可变的解释。
0赞 Tom Hawtin - tackline 9/18/2008 #7

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

8赞 David Pierre 9/18/2008 #8

String不是一个原始类型,但你通常希望它与值语义一起使用,即像一个值。

价值是您可以信任的东西,不会在背后改变。 如果你写:你不希望它改变,除非你对 .String str = someExpr();str

String由于 an 具有自然的指针语义,因此要获得值语义,它也需要是不可变的。Object

2赞 Motti 12/17/2008 #9

在 C++ 中让字符串可变的决定会导致很多问题,请参阅 Kelvin Henney 关于疯牛病的这篇优秀文章。

COW = 写入时复制。

11赞 Esko Luontola 2/6/2009 #10

人们真的应该问,“为什么X应该是可变的?最好默认为不变性,因为 Princess Fluff 已经提到了好处。某些东西是可变的应该是一个例外。

不幸的是,大多数当前的编程语言都默认为可变性,但希望将来默认的更多是不变性(参见下一个主流编程语言的愿望清单)。

2赞 user80494 3/20/2009 #11

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

评论

1赞 Antoine Aubry 3/20/2009
我相信,只有当您的代码在完全信任的情况下运行时,您才能执行此类技巧,因此不会造成安全损失。您也可以使用 JNI 直接在存储字符串的内存位置上写入。
0赞 Gqqnbig 1/29/2014
实际上,我相信你可以通过反射来改变任何不可变的对象。
59赞 LordOfThePigs 3/21/2009 #12

实际上,字符串在 java 中不可变的原因与安全性没有太大关系。主要有两个原因:

头部安全:

字符串是使用非常广泛的对象类型。因此,它或多或少可以保证在多线程环境中使用。字符串是不可变的,以确保在线程之间共享字符串是安全的。使用不可变字符串可确保在将字符串从线程 A 传递到另一个线程 B 时,线程 B 不会意外修改线程 A 的字符串。

Not only does this help simplify the already pretty complicated task of multi-threaded programming, but it also helps with performance of multi-threaded applications. Access to mutable objects must somehow be synchronized when they can be accessed from multiple threads, to make sure that one thread doesn't attempt to read the value of your object while it is being modified by another thread. Proper synchronization is both hard to do correctly for the programmer, and expensive at runtime. Immutable objects cannot be modified and therefore do not need synchronization.

Performance:

While String interning has been mentioned, it only represents a small gain in memory efficiency for Java programs. Only string literals are interned. This means that only the strings which are the same in your source code will share the same String Object. If your program dynamically creates string that are the same, they will be represented in different objects.

More importantly, immutable strings allow them to share their internal data. For many string operations, this means that the underlying array of characters does not need to be copied. For example, say you want to take the five first characters of String. In Java, you would calls myString.substring(0,5). In this case, what the substring() method does is simply to create a new String object that shares myString's underlying char[] but who knows that it starts at index 0 and ends at index 5 of that char[]. To put this in graphical form, you would end up with the following:

 |               myString                  |
 v                                         v
"The quick brown fox jumps over the lazy dog"   <-- shared char[]
 ^   ^
 |   |  myString.substring(0,5)

This makes this kind of operations extremely cheap, and O(1) since the operation neither depends on the length of the original string, nor on the length of the substring we need to extract. This behavior also has some memory benefits, since many strings can share their underlying char[].

评论

6赞 Gabe 12/8/2010
将子字符串实现为共享基础的引用是一个相当值得怀疑的设计决策。如果将整个文件读入单个字符串,并且仅保留对 1 个字符子字符串的引用,则必须将整个文件保留在内存中。char[]
5赞 LordOfThePigs 12/22/2010
确切地说,我在创建一个只需要从整个页面中提取几个单词的网站爬虫时遇到了那个特定的陷阱。整个页面的 HTML 代码都在内存中,由于子字符串共享 char[],即使我只需要几个字节,我也保留了整个 HTML 代码。解决方法是使用 new String(original.substring(..,..)),String(String) 构造函数复制基础数组的相关范围。
1赞 Christian Semrau 3/24/2013
涵盖后续更改的附录:从 Jave 7 开始,执行完整副本,以防止上述评论中提到的问题。在 Java 8 中,删除了两个启用共享的字段,即 和 ,从而减少了 String 实例的内存占用。String.substring()char[]countoffset
0赞 Gqqnbig 1/29/2014
我同意 Thead 安全部分,但怀疑子字符串情况。
0赞 LordOfThePigs 1/29/2014
@LoveRight:然后检查java.lang.String(grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/...)的源代码,一直到Java 6(编写此答案时是最新的)都是这样。我显然在 Java 7 中发生了变化。
8赞 Jim Barton 3/28/2009 #13

哇!我简直不敢相信这里的错误信息。是不可变的,与安全性无关。如果有人已经可以访问正在运行的应用程序中的对象(如果您试图防止有人在您的应用程序中“入侵”,则必须假设这一点),那么它们肯定会有很多其他可供黑客攻击的机会。StringString

这是一个非常新颖的想法,即不变性正在解决线程问题。嗯......我有一个对象正在被两个不同的线程更改。如何解决此问题?同步对对象的访问?呜......我们不要让任何人更改对象 - 这将解决我们所有混乱的并发问题!事实上,让我们使所有对象都不可变,然后我们可以从 Java 语言中删除同步结构。String

真正的原因(上面其他人指出的)是内存优化。在任何应用程序中,重复使用相同的字符串文本是很常见的。事实上,它是如此普遍,以至于几十年前,许多编译器都进行了优化,只存储一个文本的单个实例。此优化的缺点是,修改文本的运行时代码会引入问题,因为它正在修改共享该文本的所有其他代码的实例。例如,应用程序中某处的函数将文本更改为 是不利的。A 将导致被写入 stdout。出于这个原因,需要有一种方法来防止试图更改文字的代码(即使它们不可变)。一些编译器(在操作系统的支持下)会通过将 literal 放入特殊的只读内存段来实现这一点,如果进行写入尝试,将导致内存故障。StringStringString"dog""cat"printf("dog")"cat"StringString

在 Java 中,这称为实习。这里的 Java 编译器只是遵循编译器几十年来所做的标准内存优化。为了解决这些文字在运行时被修改的相同问题,Java 只是使类不可变(即,没有给你任何允许你更改内容的 setter)。如果不发生文字实习,则 s 不必是不可变的。StringStringStringStringString

评论

3赞 javashlook 3/28/2009
我强烈不同意不变性和线程注释,在我看来,你还没有完全明白这一点。如果 Java 实现者之一 Josh Bloch 说这是设计问题之一,那怎么会是错误信息呢?
1赞 David Thornley 3/28/2009
同步成本高昂。对可变对象的引用需要同步,而不可变对象则不然。这就是使所有对象不可变的原因,除非它们必须是可变的。字符串可以是不可变的,因此这样做可以使它们在多个线程中更有效率。
5赞 Triynko 11/4/2009
@Jim:内存优化不是“THE”原因,而是“A”原因。线程安全也是“A”的原因,因为不可变对象本质上是线程安全的,不需要昂贵的同步,正如 David 所提到的。线程安全实际上是对象不可变的副作用。您可以将同步视为使对象“暂时”不可变的一种方式(ReaderWriterLock 将使其为只读,而常规锁将使其完全不可访问,这当然也使其不可变)。
1赞 supercat 2/9/2014
@DavidThornley:创建多个指向可变值持有者的独立引用路径有效地将其转换为一个实体,并且即使除了线程问题之外,也很难进行推理。通常,在每个对象只存在一个引用路径的情况下,可变对象比不可变对象更有效,但不可变对象允许通过共享引用来有效地共享对象的内容。最好的模式是 和 ,但不幸的是,很少有其他类型遵循该模型。StringStringBuffer
3赞 Triynko 5/8/2009 #14

在大多数情况下,“字符串”是一个有意义的原子单位,就像一个数字一样

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

你应该知道为什么。想想看。

我不想这么说,但不幸的是,我们正在争论这个问题,因为我们的语言很糟糕,我们试图用一个词,字符串来描述一个复杂的、上下文中的概念或对象类。

我们用“字符串”进行计算和比较,类似于我们对数字的处理方式。如果字符串(或整数)是可变的,我们必须编写特殊的代码来将它们的值锁定为不可变的局部形式,以便可靠地执行任何类型的计算。因此,最好将字符串视为数字标识符,但它的长度不是 16、32 或 64 位,而是数百位。

当有人说“字符串”时,我们都会想到不同的事情。那些简单地将其视为一组角色,没有特定目的的人,当然会感到震惊,因为有人刚刚决定他们不应该操纵这些角色。但是“string”类不仅仅是一个字符数组。这是一个,而不是一个。关于我们称之为“字符串”的概念有一些基本假设,它通常可以被描述为编码数据的有意义的原子单位,如数字。当人们谈论“操纵字符串”时,也许他们实际上是在谈论操纵字符来构建字符串,而 StringBuilder 非常适合这一点。想一想“字符串”这个词的真正含义。STRINGchar[]

想一想,如果字符串是可变的,会是什么样子。如果可用户名字符串在使用此函数时被另一个线程有意或无意地修改,则以下 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 );
    }
}

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

评论

0赞 Abel 11/2/2009
在 C# 中,字符串可以通过其指针 (use ) 或仅通过反射(您可以轻松获取基础字段)进行可变。这使得安全性的观点无效,因为任何有意想要更改字符串的人都可以很容易地做到这一点。然而,它为程序员提供了安全性:除非你做一些特别的事情,否则字符串是不可变的(但它不是线程安全的!unsafe
0赞 Triynko 11/3/2009
是的,您可以通过指针更改任何数据对象(字符串、int 等)的字节。但是,我们谈论的是为什么字符串类是不可变的,因为它没有内置用于修改其字符的公共方法。我是说字符串很像一个数字,因为操作单个字符并不比操作数字的单个位更有意义(当您将字符串视为整个标记(而不是字节数组)时,将数字视为数值(而不是位字段)。我们谈论的是概念对象级别,而不是子对象级别。
2赞 Triynko 11/3/2009
澄清一下,面向对象代码中的指针本质上是不安全的,正是因为它们绕过了为类定义的公共接口。我说的是,如果字符串的公共接口允许其他线程修改函数,则很容易欺骗函数。当然,它总是可以通过直接使用指针访问数据来欺骗,但不会那么容易或无意。
1赞 David Rodríguez - dribeas 5/27/2010
“面向对象代码中的指针本质上是不安全的”,除非你称它们为引用。Java 中的引用与 C++ 中的指针没有什么不同(仅禁用指针算术)。一个不同的概念是可以管理或手动的内存管理,但这是另一回事。你可以有引用语义(没有算术的指针),而没有GC(相反会更难,因为可访问性的语义更难做到干净,但并非不可行)
0赞 David Rodríguez - dribeas 5/27/2010
另一件事是,如果字符串几乎是不可变的,但又不完全是不可变的(我在这里对 CLI 的了解不够),出于安全原因,这可能非常糟糕。在一些较旧的 Java 实现中,您可以这样做,我找到了一段使用它来内部化字符串的代码片段(尝试找到具有相同值的其他内部字符串,共享指针,删除旧的内存块),并使用后门重写字符串内容,从而在不同的类中强制出现不正确的行为。 (考虑将“SELECT *”重写为“DELETE”)
3赞 Andrei Rînea 10/23/2010 #15

不可变性与安全性的联系并不紧密。为此,至少在 .NET 中,您可以获得该类。SecureString

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

0赞 Lu4 11/2/2013 #16

几乎每条规则都有例外:

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();
            }
        }
    }
}
6赞 Bauss 3/7/2015 #17

我知道这是一个颠簸,但是...... 它们真的是不可变的吗? 请考虑以下几点。

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赞 James Ko 8/9/2015
+1..NET 字符串并不是真正不可变的;事实上,由于性能原因,这在 String 和 StringBuilder 类中一直都在执行。