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

时间:2008-09-18 14:34:33

标签: java .net string mutable

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

17 个答案:

答案 0 :(得分:202)

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

  

“这有很多很好的理由:不可变的类更容易   设计,实现和使用比可变类。它们不太容易发生   错误,更安全。

     

[...]

     

不可变对象很简单。不可变对象可以在   恰好是一个州,即创建它的州。如果你确定   所有构造函数都建立了类不变量,然后它就是   保证这些不变量将始终保持为真   你没有努力。

     

[...]

     

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

     

[...]

同章的其他小点:

  

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

     

[...]

     

不可变对象为其他对象构建了很好的构建块,无论是可变的还是不可变的。

     

[...]

     

不可变类的唯一真正缺点是它们需要为每个不同的值分别使用一个对象。

答案 1 :(得分:102)

至少有两个原因。

首先 - 安全性 http://www.javafaq.nu/java-article1060.html

  

String的主要原因   不可变的是安全。看这个   示例:我们有一个文件打开方法   登录检查。我们将一个String传递给   这种处理身份验证的方法   在通话之前是必要的   将传递给操作系统。如果String是   可变的,有可能以某种方式   修改后的内容   OS获取之前的身份验证检查   来自程序的请求然后它   可以请求任何文件。因此,如果   你有权打开文本文件   用户目录,但随后即时   什么时候你设法改变了   您可以请求打开的文件名   “passwd”文件或任何其他文件。然后一个   文件可以修改,它将是   可以直接登录操作系统。

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

  

JVM在内部维护“String   游泳池“。要获得记忆   效率,JVM将引用String   池中的对象。它不会创造   新的String对象。所以,无论何时   你创建一个新的字符串文字,JVM   将在游泳池检查是否   已存在或不存在。如果已经   现在在游泳池,只是给   引用同一个对象或创建   池中的新对象。这里将   许多参考指向相同的   字符串对象,如果有人更改了   价值,它会影响所有   引用。所以,太阳决定成功   不可变的。

答案 2 :(得分:57)

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

Thead Safety:

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

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

性能:

虽然已经提到过String interning,但它只代表了Java程序内存效率的一小部分增益。仅限字符串文字。这意味着只有源代码中相同的字符串才会共享相同的String对象。如果您的程序动态创建相同的字符串,它们将在不同的对象中表示。

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

答案 3 :(得分:28)

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

答案 4 :(得分:11)

人们应该真的问,“为什么X应该是可变的?”由于Princess Fluff已经提到的好处,最好默认为不可变性。事情是可变的应该是一个例外。

不幸的是,大多数当前的编程语言都默认为可变性,但希望将来默认更多地依赖于immutablity(参见A Wish List for the Next Mainstream Programming Language)。

答案 5 :(得分:7)

一个因素是,如果字符串是可变的,那么存储字符串的对象必须小心存储副本,以免其内部数据发生变化而不另行通知。鉴于字符串是一个相当原始的类型,如数字,当人们可以将它们视为按值传递时,即使它们通过引用传递(这也有助于节省内存),这是很好的。

答案 6 :(得分:7)

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

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

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

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

答案 7 :(得分:7)

String不是基本类型,但您通常希望将它与值语义一起使用,即像值一样。

值得你信赖的东西不会在你背后改变。 如果你写:String str = someExpr(); 除非你用str。

做某事,否则你不希望它改变

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

答案 8 :(得分: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#中的字符串不完全不可变。

答案 9 :(得分:3)

对于大多数用途,“字符串”(使用/被视为/被认为/被认为是)有意义的原子单元, 就像数字

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

你应该知道为什么。试想一下。

我不想这么说,但不幸的是我们正在讨论这个问题,因为我们的语言很糟糕,我们正试图用一个单词 string 来描述一个复杂的,符合语境的概念或一类对象。

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

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

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

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

答案 10 :(得分:3)

不可变性与安全性没有密切关系。为此,至少在.NET中,您获得了SecureString类。

答案 11 :(得分:2)

在C ++中使用字符串变量的决定会导致很多问题,请参阅Kelvin Henney撰写的关于Mad COW Disease的优秀文章。

COW =写入时复制。

答案 12 :(得分:2)

Java中的字符串并非真正不可变,您可以使用反射和/或类加载来更改它们的值。出于安全考虑,您不应该依赖该属性。 有关示例,请参阅:Magic Trick In Java

答案 13 :(得分:2)

这是一种权衡。字符串进入字符串池,当您创建多个相同的字符串时,它们共享相同的内存。设计人员认为这种内存节省技术适用于常见情况,因为程序往往会磨损相同的字符串。

缺点是连接会产生许多额外的字符串,这些字符串只是过渡性的,只会变成垃圾,实际上会损害内存性能。你有StringBuffer和StringBuilder(在Java中,StringBuilder也在.NET中)用于在这些情况下保存内存。

答案 14 :(得分:0)

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

答案 15 :(得分: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();
            }
        }
    }
}

答案 16 :(得分:-1)

这主要是出于安全考虑。如果你不相信你的字符串是防篡改的,那么保护系统要困难得多。