为什么他们决定在 Java 和 .NET(以及其他一些语言)中使字符串不可变?他们为什么不让它可变呢?

有帮助吗?

解决方案

根据 有效的Java, ,第 4 章,第 73 页,第二版:

“这有很多充分的理由:与可变的类别相比,不变的类更容易设计,实现和使用。它们不太容易出错,更安全。

[...]

"不可变对象很简单。 一个不变的对象可以正好处于一个状态,即其创建的状态。如果您确保所有构造函数都建立班级不变性,那么可以保证这些不变性将在您一直保持不变,而不会付出任何努力。

[...]

不可变对象本质上是线程安全的;它们不需要同步。 它们不能被多个线程并同时访问它们而损坏。这是实现线程安全的最简单方法。实际上,任何线程都无法观察到另一个线程对不变对象的任何效果。所以, 不可变对象可以自由共享

[...]

同一章的其他小点:

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

[...]

不可变对象可以为其他对象(无论是可变的还是不可变的)提供很好的构建块。

[...]

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

其他提示

至少有两个原因。

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

绳子变得不变的主要原因是安全性。看看这个示例:我们有一个带有登录检查的文件打开方法。我们将字符串传递给此方法以处理身份验证,这是在调用将传递给OS之前所必需的。如果字符串是可变的,则可以在身份验证检查后从程序获取请求之前以某种方式修改其内容,则可以请求任何文件。因此,如果您有权在用户目录中打开文本文件,但是当您设法更改文件名时,您可以请求打开“ passwd”文件或任何其他文件。然后可以修改文件,并且可以直接登录到OS。

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

JVM内部维护“弦池”。为了实现内存效率,JVM将从池中引用字符串对象。它不会创建新的字符串对象。因此,每当您创建一个新的字符串文字时,JVM都会在池中检查是否存在。如果已经存在于池中,则只需给出对同一对象的引用或在池中创建新对象即可。会有许多引用指向相同的字符串对象,如果某人更改值,它将影响所有引用。因此,太阳决定使其不变。

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

头部安全:

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

这不仅有助于简化已经相当复杂的多线程编程任务,而且还有助于提高多线程应用程序的性能。当可以从多个线程访问可变对象时,对可变对象的访问必须以某种方式同步,以确保一个线程在另一线程修改对象时不会尝试读取对象的值。对于程序员来说,正确的同步很难正确完成,而且运行时的成本也很高。不可变对象无法修改,因此不需要同步。

表现:

虽然已经提到了字符串驻留,但它只代表了 Java 程序内存效率的微小提升。仅字符串文字被保留。这意味着只有与您的字符串中相同的字符串 源代码 将共享相同的字符串对象。如果您的程序动态创建相同的字符串,它们将在不同的对象中表示。

更重要的是,不可变字符串允许它们共享内部数据。对于许多字符串操作,这意味着不需要复制底层字符数组。例如,假设您想获取 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[]。

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

一个人应该真正问:“ X为什么要变形?”最好默认为不变性,因为 绒毛公主. 。某些东西是可变的,这应该是一个例外。

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

一个因素是,如果字符串是可变的,则存储字符串的对象必须小心存储副本,以免其内部数据在没有通知的情况下发生更改。鉴于字符串是一种像数字一样相当原始的类型,如果可以将它们视为按值传递,即使它们是按引用传递(这也有助于节省内存),那就太好了。

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

String 的不变性正在解决线程问题,这是一个相当新颖的想法。嗯...我有一个对象正在被两个不同的线程更改。我该如何解决这个问题?同步对象的访问?呐呐……我们根本不让任何人更改对象——这将解决我们所有混乱的并发问题!事实上,让所有对象都是不可变的,然后我们就可以从 Java 语言中删除同步结构。

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

在 Java 中,这称为实习。这里的 Java 编译器只是遵循编译器几十年来完成的标准内存优化。为了解决这些字符串文字在运行时被修改的相同问题,Java 简单地使 String 类不可变(即。e,没有给您提供任何允许您更改字符串内容的设置器)。如果不发生字符串文字的驻留,则字符串不必是不可变的。

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

价值观是你可以相信的东西,不会在你背后改变。如果你写: String str = someExpr();你不希望它改变,除非你对 str 做了一些事情。

字符串作为对象自然具有指针语义,为了获取值语义,它也需要是不可变的。

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

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

对于大多数目的,“字符串”是(使用/视为/认为/假定为)有意义的 原子单位, 就像一个数字一样.

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

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

我不想这么说,但不幸的是我们正在争论这个,因为我们的语言很糟糕,而且我们试图使用一个词, 细绳, ,描述一个复杂的、上下文相关的概念或对象类。

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

当有人说“字符串”时,我们都会想到不同的东西。那些仅仅将其视为一组字符、没有任何特定目的的人当然会感到震惊,因为有人 刚刚决定 他们不应该能够操纵这些角色。但“string”类不仅仅是字符数组。它是 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 类。

这是一个权衡。字符串进入字符串池,当您创建多个相同的字符串时,它们共享相同的内存。设计者认为这种内存节省技术对于常见情况很有效,因为程序往往会大量处理相同的字符串。

缺点是连接会产生大量额外的字符串,这些字符串只是过渡性的,只会变成垃圾,实际上会损害内存性能。在这些情况下,您可以使用 StringBuffer 和 StringBuilder(在 Java 中,StringBuilder 也在 .NET 中)来保留内存。

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

COW = 写入时复制。

Java 中的字符串并不是真正不可变的,您可以使用反射和/或类加载来更改它们的值。您不应该依赖该财产来获得安全。示例请参见: Java 中的魔术

不变性很好。请参阅有效的 Java。如果每次传递字符串时都必须复制它,那么这将是很多容易出错的代码。您还不清楚哪些修改会影响哪些引用。就像整数必须是不可变的才能像 int 一样,字符串也必须是不可变的才能像基元一样。在 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();
            }
        }
    }
}

这主要是出于安全原因。如果您不能相信您的字符串是防篡改的,那么保护系统的安全就会困难得多。

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top