为什么在Java和。net中字符串是可变的?

为什么他们决定让String在Java和。net(以及其他一些语言)中是不可变的?为什么不让它变呢?

44908 次浏览

这主要是出于安全考虑。如果你不能相信你的__abc0是防篡改的,那么保护一个系统就更难了。

至少有两个原因。

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

String的主要原因 不可改变就是安全。看看这个 我们有一个文件打开方法 与登录检查。我们将一个String传递给 此方法用于处理身份验证 打电话前需要做什么 将被传递给操作系统。如果String是 可变是可能的 后修改其内容 在操作系统获取之前进行身份验证检查 从程序请求,然后是 可以请求任何文件。因此,如果 您有权打开文本文件 用户目录,然后运行 当你设法改变 可以请求打开的文件名 “passwd”文件或其他文件。然后一个 文件可以修改,它将

.

.

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

JVM内部维护“字符串 池”。实现记忆 效率,JVM将引用字符串 对象。它不会创建 新的String对象。所以,每当 创建一个新的字符串字面值JVM 会在泳池里检查一下吗 是否已经存在。如果已经 现在在泳池里,只是给 引用同一个对象或创建 池中的新对象。会有 许多参考文献都指向同一点 字符串对象,如果有人更改 价值,就会影响一切 参考文献所以,sun决定制造它 不可变的。< / p >

线程安全和性能。如果一个字符串不能被修改,那么在多个线程之间传递引用是安全且快速的。如果字符串是可变的,则总是必须将字符串的所有字节复制到新实例,或者提供同步。一个典型的应用程序在每次需要修改字符串时将读取字符串100次。参见wikipedia上不变性

一个因素是,如果Strings是可变的,那么存储Strings的对象必须小心地存储副本,以免它们的内部数据在没有通知的情况下发生变化。鉴于__abc0是一个相当基本的类型,就像数字一样,如果可以将它们当作是通过值传递的,即使它们是通过引用传递的(这也有助于节省内存),这是很好的。

这是一种权衡。String会进入String池,当你创建多个相同的String时,它们会共享相同的内存。设计人员认为这种节省内存的技术在一般情况下会很好地工作,因为程序倾向于反复处理相同的字符串。

缺点是串联会产生大量额外的__abc 0,这些__abc 0只是过渡性的,只是垃圾,实际上会损害内存性能。在这些情况下,可以使用StringBufferStringBuilder(在Java中,StringBuilder也在. net中)来保存内存。

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

这有很多很好的理由:不可变类更容易 设计、实现和使用可变类。他们更不容易

[…]

"不可变对象很简单。一个不可变对象可以在 恰好是一种状态,它被创建时的状态。如果你确定 所有构造函数都建立类不变量,那么它就是 保证这些不变量将始终为真,使用

.

.

.

[…]

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

[…]

同一章的其他要点:

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

[…]

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

[…]

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

不变性很好。参见有效的Java。如果每次传递String时都必须复制它,那么这将是大量容易出错的代码。您还会混淆哪些修改会影响哪些引用。同样地,Integer必须是不可变的才能像int一样,string必须是不可变的才能像原语一样。在c++中,按值传递字符串是这样做的,源代码中没有明确提到。

String不是一个基本类型,但你通常想用值语义使用它,即像一个值。

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

String作为一个Object具有天然的指针语义,为了获得值语义,它也需要是不可变的。

在c++中使用可变字符串会导致很多问题,请参阅Kelvin Henney关于疯牛病的优秀文章。

COW =写入时拷贝。

人们应该问,“为什么X应该是可变的?”最好默认为不可变,因为公主绒毛已经提到了这些好处。它应该是一个例外,某些东西是可变的。

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

__abc0在Java中不是真正不可变的,你可以使用反射和或类加载来改变它们的值。你不应该依赖这个属性来保证安全。 例如:爪哇的魔术

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

Thead安全:

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

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

性能:

虽然已经提到了字符串实习,但它只代表了Java程序内存效率的一小部分提高。只有字符串字面量被存储。这意味着只有在你的源代码中相同的字符串才会共享相同的字符串对象。如果你的程序动态地创建了相同的字符串,它们将在不同的对象中表示。

更重要的是,不可变字符串允许它们共享内部数据。对于许多字符串操作,这意味着底层字符数组不需要复制。例如,假设您想取String的前五个字符。在Java中,你会调用myString.substring(0,5)。在这种情况下,substring()方法所做的只是创建一个新的String对象,该对象共享myString的底层char[],但谁知道它从索引0开始,结束于索引5的char[]。以图形形式表示,你会得到以下结果:

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

这使得这种操作非常便宜,O(1),因为该操作既不依赖于原始字符串的长度,也不依赖于我们需要提取的子字符串的长度。这种行为也有一些内存好处,因为许多字符串可以共享它们的底层char[]。

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

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

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

在Java中,这被称为实习。这里的Java编译器只是遵循了编译器几十年来所做的标准内存优化。为了解决这些String文字在运行时被修改的相同问题,Java简单地使String类不可变(即,不给你允许你改变String内容的设置项)。如果没有发生String字面量的实习,String就不一定是不可变的。

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

因此,问为什么字符串的单个字符是不可变的,就像问为什么整数的单个比特是不可变的一样。

你应该知道原因。想想看。

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

我们对“字符串”执行计算和比较,类似于对数字的操作。如果字符串(或整数)是可变的,我们必须编写特殊的代码来将它们的值锁定为不可变的局部形式,以便可靠地执行任何类型的计算。因此,最好将字符串视为数字标识符,但它可能是数百位,而不是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类。

稍后编辑:在Java中,你会发现GuardedString是一个类似的实现。

几乎每条规则都有例外:

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

我知道这有点颠簸,但是… 它们真的是不可变的吗?

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