我不明白原因。我总是像其他开发人员一样使用String类,但是当我修改它的值时,会创建String的新实例。

Java中String类不可变性的原因可能是什么?

我知道有一些替代方法,例如StringBuffer或StringBuilder。只是好奇而已。

评论

从技术上讲,这不是重复的,但是埃里克·利珀特(Eric Lippert)在这里对这个问题给出了很好的答案:

#1 楼

并发

Java是从一开始就考虑到并发定义的​​。正如经常提到的,共享的可变项是有问题的。一件事可以在另一个线程背后改变另一件事,而该线程却不知道。

由于共享字符串而出现了许多多线程C ++错误-一个模块认为这是错误的当代码中的另一个模块保存了指向它的指针并希望保持不变时,就可以安全地进行更改。传递给它。对于可变字符串,这是O(n)进行复制。对于不可变的字符串,因为它不是一个副本,所以复制为O(1),因为它是不变的对象。

在多线程环境中,始终可以安全地共享不可变的对象彼此之间。这样可以整体上减少内存使用量并改善内存缓存。

安全性

很多时候,字符串作为构造函数的参数传递-网络连接和协议是两个最容易想到的。能够在执行后的不确定时间更改此设置可能会导致安全问题(该功能以为它已连接到一台机器,但已转移到另一台机器上,但是对象中的所有内容看起来都像它已连接到第一台机器。它甚至是相同的字符串)。

Java允许使用反射-且此参数是字符串。将一个可以通过修改的字符串传递给另一种反映方法的危险。这非常糟糕。

哈希键

哈希表是最常用的数据结构之一。数据结构的键通常是字符串。具有不可变的字符串意味着(如上所述)哈希表不需要每次都复制哈希键。如果字符串是可变的,并且哈希表没有做到这一点,则有可能在远处更改哈希键。哈希键(通过hashCode()方法访问)。具有不可变的字符串意味着可以缓存hashCode。考虑到将字符串用作哈希键的频率,这可以显着提高性能(而不是每次都必须重新计算哈希码)。 String是不可变的,支持数据结构的基础字符数组也是不可变的。这样就可以对substring方法进行某些优化(不一定要做-还会引入一些内存泄漏的可能性)。

如果这样做:

String foo = "smiles";
String bar = foo.substring(1,5);


bar的值为'mile'。但是,foobar都可以由相同的字符数组支持,从而减少了更多字符数组的实例化或将其复制-只需在字符串中使用不同的起点和终点即可。


现在,这样做的缺点(内存泄漏)是,如果一个人的字符串长为1k,并采用了第一个和第二个字符的子字符串,那么它也将得到1k长的字符数组的支持。即使将具有整个字符数组值的原始字符串进行了垃圾回收,该数组也将保留在内存中。来源并用作示例)

foo |    | (0, 6)
    v    v
    smiles
     ^  ^
bar  |  |  (1, 5)


请注意,子字符串如何使用不涉及数组任何复制的包级String构造函数,并且将更快(以可能保留一些大数组为代价,尽管也不复制大数组)。

请注意,以上代码适用于Java 1.6。如Java 1.7.0_06
中对字符串内部表示的更改中所述,Java 1.7更改了子字符串构造函数的实现方式-我上面提到的导致内存泄漏的问题。 Java可能不被视为具有大量String操作的语言,因此提高子字符串的性能是一件好事。现在,由于将巨大的XML文档存储在从未收集过的字符串中,因此这成为一个问题……因此对String的更改没有使用带有子字符串的相同基础数组,因此可以更快地收集较大的字符数组。

不要滥用堆栈

可以传递字符串的值,而不是引用不可变的字符串,以避免发生可变性问题。但是,对于大字符串,将其传递到堆栈上将……对系统造成滥用(将整个xml文档作为字符串放在堆栈上,然后将其取走或继续传递它们……)。

重复数据删除的可能性

当然,这并不是为什么字符串不可变的最初动机,但是当人们研究为什么不可变字符串是一件好事的原因时,这当然是要考虑的事情。

任何与Strings一起工作的人都知道他们可以吸收内存。当您执行诸如从数据库中提取持续存在一段时间的数据之类的操作时,尤其如此。很多时候,这些st刺一次又一次地是相同的字符串(每行一次)。


当前,许多大型Java应用程序已成为内存瓶颈。测量表明,在这些类型的应用程序中,大约25%的Java堆活动数据集被String对象占用。此外,这些String对象中大约有一半是重复项,其中重复项意味着string1.equals(string2)为true。从本质上讲,在堆上具有重复的String对象只是浪费内存。 ...


随着Java 8 update 20的出现,正在实现JEP 192(上面引用的动机)来解决这个问题。在不深入研究字符串重复数据删除的工作原理的前提下,字符串本身不可变是至关重要的。您不能对StringBuilders进行重复数据删除,因为它们可以更改,并且您不希望有人在您下面更改某些内容。不可变的字符串(与该字符串池相关)意味着您可以遍历,如果找到两个相同的字符串,则可以将一个字符串引用指向另一个字符串,并让垃圾回收器使用新使用的未使用的字符串。

其他语言

目标C(早于Java)具有NSStringNSMutableString。 。

Lua字符串也是不可变的。

Python也是如此。一成不变。较现代的动态语言通常以某种方式使用字符串,这些字符串要求它们是不可变的(它可能不是String,但是它是不可变的)。

结论

这些设计注意事项一次又一次地用多种语言制作。普遍的共识是,不可变的字符串尽管具有笨拙性,但比替代字符串更好,并且可以带来更好的代码(更少的错误)和更快的可执行文件。

评论


Java提供了可变且不变的字符串。这个答案详细说明了在不可变字符串上可以实现的一些性能优势,以及一些可能选择不可变数据的原因。但没有讨论为什么不可变版本是默认版本。

–比利·奥尼尔(Billy ONeal)
13年4月17日在6:23

@BillyONeal:安全的默认设置和不安全的替代方法几乎总是比相反的方法导致更安全的系统。

–约阿希姆·绍尔(Joachim Sauer)
13年4月17日在14:31

@BillyONeal如果不可变不是默认值,那么并发性,安全性和哈希问题将变得更加普遍。语言设计人员选择(部分响应C)选择一种设置默认值的语言,以尝试防止一些常见的错误,从而提高程序员的效率(不必再担心这些错误)。与可变字符串相比,不可变字符串的错误(明显和隐藏)更少。

–user40980
2013年4月17日14:32在

@Joachim:我没有其他要求。

–比利·奥尼尔(Billy ONeal)
13年4月17日在17:23

从技术上讲,Common Lisp具有可变字符串,用于“类字符串”操作,而具有不可变名称的符号则用于不可变标识符。

–疫苗
15年7月22日在11:02

#2 楼

我记得的原因:根本不可能使字符串池不变而不使字符串不可变,因为在字符串池的情况下,一个字符串对象/字面量例如“ XYZ”将由许多引用变量引用,因此,如果其中任何一个更改,则其他值将自动受到影响。
字符串已广泛用作许多Java类的参数,例如用于打开网络连接,用于打开数据库连接,打开文件。如果String不可更改,则将导致严重的安全威胁。
不可变性允许String缓存其哈希码。
使其具有线程安全性。


#3 楼

1)字符串池
Java设计师知道字符串将成为所有Java应用程序中最常用的数据类型,这就是为什么他们想要从头开始进行优化。在该方向上的关键步骤之一是将String文字存储在String池中的想法。目标是通过共享它们来减少临时String对象,并且为了共享,它们必须必须来自Immutable类。您不能与彼此未知的两个方共享可变对象。让我们举一个假设的例子,其中两个引用变量指向同一个String对象: ”,这甚至都不知道。通过使String不可变,可以实现String文字的这种共享。简而言之,如果不使String在Java中成为final或Immutable,就无法实现String pool的关键思想。在整个安全方面至关重要。字符串已被广泛用作许多Java类的参数,例如对于打开网络连接,您可以将主机和端口作为String传递;对于在Java中读取文件,可以将文件和目录的路径作为String传递;对于打开数据库连接,可以将数据库URL作为String传递。如果String并非一成不变,则用户可能已授权访问系统中的特定文件,但是在身份验证后,他可以将PATH更改为其他内容,这可能会导致严重的安全问题。同样,在连接数据库或网络中的任何其他计算机时,更改String值可能会带来安全威胁。由于参数是字符串,因此可变字符串也可能在反射中引起安全问题。
3)在类加载机制中使用字符串
使String最终或不可变的另一个原因是由于它在类加载机制中大量使用这一事实。由于String不是不可变的,因此攻击者可以利用这一事实以及请求来加载标准Java类,例如可以将java.io.Reader更改为恶意类com.unknown.DataStolenReader。通过保持String的最终值和不可变性,我们至少可以确保JVM加载了正确的类。
4)多线程的好处
由于并发和多线程是Java的关键产品,因此思考它很有意义关于String对象的线程安全性。由于人们期望String会被广泛使用,因此将其设置为Immutable意味着无需外部同步,这意味着涉及多个线程之间共享String的更简洁的代码。这个单一功能使已经复杂,混乱和易于出错的并发编码变得更加容易。因为String是不可变的,并且我们只是在线程之间共享它,所以它导致了更具可读性的代码。
5)优化和性能
现在,当您将一个类设为不可变时,您会事先知道,该类不是创建后将要更改。这保证了许多性能优化的开放路径,例如缓存。 String本身知道,我不会更改,因此String缓存其哈希码。它甚至懒惰地计算哈希码,一旦创建,就将其缓存。在简单的世界中,当您第一次调用任何String对象的hashCode()方法时,它会计算哈希码,并且随后对hashCode()的所有调用都会返回已计算的缓存值。给定String在基于散列的Maps中大量使用的情况下,这会导致良好的性能提升。 Hashtable和HashMap。如果不将哈希码设为不可变且最终的,则无法对其进行缓存,因为哈希码取决于String本身的内容。

#4 楼

Java虚拟机对字符串操作执行了一些优化,否则将无法执行。例如,如果您有一个值为“ Mississippi”的字符串,并且已将“ Mississippi” .substring(0,4)分配给另一个字符串,就您所知,会复制前四个字符来制作“ Miss” 。您不知道的是,它们都共享相同的原始字符串“ Mississippi”,其中一个是所有者,另一个则是从位置0到4对该字符串的引用。(对所有者的引用会阻止所有者被收集

对于像“ Mississippi”这样小的字符串来说,这是微不足道的,但是对于较大的字符串和多个操作,不必复制字符串是很重要的节省!如果字符串是可变的,那么您将无法执行此操作,因为修改原始字符串也会影响子字符串“副本”。

另外,正如多纳尔(Donal)所提到的那样,其劣势将大大削弱其优势。假设您编写了一个依赖于库的程序,并且使用了一个返回字符串的函数。您如何确定该值将保持不变?为确保不会发生此类情况,您始终必须提供一份副本。

如果有两个共享同一字符串的线程怎么办?您不想读取当前正在被另一个线程重写的字符串,对吗?因此,字符串必须是线程安全的,这是它的通用类,实际上会使每个Java程序变慢得多。否则,您必须为每个需要该字符串的线程制作一个副本,或者必须将使用该字符串的代码放入同步块中,这只会减慢程序的速度。出于所有这些原因,这是Java做出的早期决定之一,以使其与C ++相区别。

评论


从理论上讲,您可以进行多层缓冲区管理,以便在共享时进行突变复制,但是很难在多线程环境中高效地进行工作。

–研究员
13年4月16日在13:29

@DonalFellows我只是假设,因为Java虚拟机不是用Java编写的(显然),所以它是使用共享指针或类似的东西在内部进行管理的。

–尼尔
13年4月16日在13:32

#5 楼

字符串不可改变的原因来自与语言中其他原始类型的一致性。如果您有一个包含值42的int,并向其添加值1,则无需更改42。您将获得一个新值43,该值与起始值完全无关。除了字符串之外,对基本体进行突变没有任何概念上的意义;而且,将字符串视为不可变的此类程序通常更易于推理和理解。实际上,只有默认值是不可变的字符串。如果您想在任何地方传递对StringBuilder的引用,我们非常欢迎这样做。 Java为这些概念使用了单独的类型(StringBuilderString),因为Java在其类型系统中不支持表达可变性或缺少可变性。在支持类型系统中不可变性的语言(例如C ++的StringBuilder)中,通常只有一个字符串类型可以同时满足这两个目的。到不可变的字符串(例如Interning),并允许传递字符串引用,而无需跨线程同步。但是,这会使该机制与具有简单且一致的类型系统的语言的预期目标相混淆。我把这比作每个人对垃圾回收的错误看法。垃圾回收不是“回收未使用的内存”;它是“模拟具有无限内存的计算机”。讨论的性能优化是为了使不可变字符串的目标在实际计算机上表现良好而进行的工作;并不是这些字符串一开始就不可改变的原因。

评论


@ Billy-Oneal ..关于“如果您有一个包含值42的int,并且向其中添加了值1,则无需更改42。您将获得一个新的值43,这与开始完全无关价值观。”您确定吗?

– Shamit Verma
2014年4月28日在11:11



@Shamit:是的,我确定。将1加到42将得到43。它不会使数字42与数字43具有相同的含义。

–比利·奥尼尔(Billy ONeal)
2014年4月28日在17:14

@Shamit:类似地,您不能执行类似43 = 6的操作,并且期望数字43与数字6具有相同的含义。

–比利·奥尼尔(Billy ONeal)
2014年4月28日在17:15

我= 42; i = i + 1;此代码将在内存中存储42,然后将同一位置的值更改为43。因此,实际上,变量“ i”将获得新值43。

– Shamit Verma
2014年4月29日20:00



@Shamit:在这种情况下,您突变了i,而不是42。考虑字符串s =“ Hello”; s + =“世界”;。您对变量s的值进行了突变。但是字符串“ Hello”,“ World”和“ Hello World”是不可变的。

–比利·奥尼尔(Billy ONeal)
2014年4月29日在23:16

#6 楼

不变性意味着您不拥有的类持有的常量不能被修改。您不拥有的类包括那些位于Java实现核心中的类,并且不应修改的字符串包括诸如安全性令牌,服务地址等之类的东西。您实际上不应该能够修改这些类型事情(在沙盒模式下运行时,这种情况会倍加适用)。

如果String不是不可变的,那么每次您从某个不希望字符串内容在脚下改变的上下文中检索字符串时,您必须“以防万一”复制一份。那变得非常昂贵。

评论


这个完全相同的参数适用于任何类型,而不仅适用于String。但是,例如,数组仍然是可变的。因此,为什么字符串是不可变的而数组不是。如果不可变性是如此重要,那么为什么Java很难创建和使用不可变对象呢?

–Jörg W Mittag
13年4月16日在12:45

@JörgWMittag:我认为这基本上是他们想要成为一个多么激进的问题。在Java 1.0时代,拥有不可变的String是非常激进的。拥有(主要或什至唯一)不可变的收集框架,对于广泛使用该语言来说可能过于激进。

–约阿希姆·绍尔(Joachim Sauer)
13年4月16日在13:04

做一个有效的,不可变的集合框架对于提高性能是非常棘手的,就像写过这种东西的人说话一样(但不是用Java)。我也完全希望我拥有一成不变的数组。那会节省我很多工作。

–研究员
13年4月16日在13:21



@DonalFellows:pcollections旨在做到这一点(但是,我自己从未使用过)。

–约阿希姆·绍尔(Joachim Sauer)
13年4月16日在13:23

@JörgWMittag:有些人(通常从纯粹的功能角度来看)会认为所有类型都应该是不变的。同样,我认为,如果将一个在并行和并发软件中使用可变状态处理的所有问题加起来,您可能会同意使用不可变对象通常比可变对象容易得多。

–史蒂文·埃弗斯(Steven Evers)
13年4月16日在14:33

#7 楼

想象一个系统,您在其中接受一些数据,验证其正确性然后将其传递(例如,存储在数据库中)。

假设数据是String,并且至少必须是5个字符长。您的方法如下所示:

public void handle(String input) {
  if (input.length() < 5) {
    throw new IllegalArgumentException();
  }
  storeInDatabase(input);
}


现在我们可以同意,当在此处调用storeInDatabase时,input将符合要求。但是,如果String是可变的,则调用者可以在验证input对象并将其存储在数据库中之前立即更改它(来自另一个线程)。这将需要适当的时机,并且可能每次都不会很好,但是偶尔,他将使您能够将无效值存储在数据库中。

不可变的数据类型是一种非常简单的解决方案这个(以及很多相关的)问题:每当您检查某个值时,您都可以依赖于以后检查的条件仍然为真的事实。

评论


感谢您的解释。如果我这样调用handle方法,该怎么办?句柄(新字符串(输入+“ naberlan”))。我想我可以像这样在db中存储无效值。

– yfklon
13年4月16日在13:18



@blank:好吧,由于handle方法的输入已经太长了(无论原始输入是什么),它只会抛出一个异常。您将在调用该方法之前创建一个新的输入。那不是问题。

–约阿希姆·绍尔(Joachim Sauer)
13年4月16日在13:21

#8 楼

通常,您会遇到值类型和引用类型。对于值类型,您无需关心表示它的对象,而可以关心值。如果我给您一个价值,您希望该价值保持不变。您不希望它突然改变。数字5是一个值。您不希望它突然变为6。字符串“ Hello”是一个值。您不希望它突然变成“ P *** off”。

使用引用类型,您会关心对象,并且希望它会发生变化。例如,您通常会期望数组发生变化。如果我给您一个数组,并且您希望保持其原样,则必须让我相信不要更改它,或者您要复制它。

使用Java字符串类,设计者必须做出决定:如果字符串表现为值类型,还是应该表现为引用类型,会更好吗?对于Java字符串,决定将它们设为值类型,这意味着由于它们是对象,因此它们必须是不可变的对象。

可以做出相反的决定,但我认为这会引起很多头痛。正如其他地方所说,许多语言做出相同的决定并得出相同的结论。 C ++是一个例外,它具有一个字符串类,并且字符串可以是常量或非常量,但是在C ++中,与Java不同,对象参数可以作为值而不是引用进行传递。

#9 楼

我真的很惊讶,没有人指出这一点。它不会给您带来太大的好处,因为那会带来更多的麻烦。让我们研究一下两种最常见的突变情况:

更改字符串的一个字符

由于Java字符串中的每个字符需要2或4个字节,请问自己,如果可以更改现有副本,可以获得任何好处吗?

在您将2字节字符替换为4字节字符的情况下(反之亦然),您必须将字符串的其余部分移位左边或右边2个字节。从计算的角度来看,这与完全复制整个字符串没什么不同。想象有人用英语测试一个应用程序,当该应用程序被外国(例如中国)采用时,整个过程开始表现很奇怪。 br />
如果您有两个任意字符串,则它们将位于两个不同的内存位置。如果要通过附加第二个来更改第一个,则不能只在第一个字符串的末尾请求额外的内存,因为它可能已被占用。

您必须将连接的字符串复制到全新的位置,就像两个字符串都是不可变的一样。字符串的结尾,仅出于此目的,将来可能会追加。

#10 楼


它们很昂贵,并且保持它们不变是可能的,例如子字符串共享主字符串的字节数组。 (也可以提高速度,因为不需要创建新的字节数组并进行复制)

安全性-不想将您的包或类代码重命名为

[已删除老3看过StringBuilder src-它不与字符串共享内存(直到修改),我认为是在1.3或1.4中。根据需要)


评论


1.当然,如果发生这种情况,将无法销毁琴弦的较大部分。实习不是免费的;尽管它确实可以提高许多实际程序的性能。 2.很容易就有可以满足该要求的“字符串”和“ ImmutableString”。 3.我不确定我是否理解...

–比利·奥尼尔(Billy ONeal)
13年4月17日在6:20

.3。应该已经缓存了哈希码。这也可以通过可变字符串来完成。 @ billy-oneal

– tgkprog
2013年12月3日在16:04



#11 楼

字符串在Java中应该是原始数据类型。如果已经存在,则字符串将默认为可变的,而final关键字将生成不可变的字符串。可变字符串很有用,因此在stringbuffer,stringbuilder和charsequence类中有很多针对可变字符串的技巧。

评论


这并不能回答问题确实提出的“为什么”方面。另外,java final不能那样工作。可变字符串不是黑客,而是基于最常见的字符串用法和可以改进jvm的优化的实际设计注意事项。

–user40980
13年4月16日在17:56

“为什么”的答案是糟糕的语言设计决策。支持可变字符串的三种略有不同的方法是编译器/ JVM应该处理的黑客。

– CWallach
13年4月16日在18:06



String和StringBuffer是原始的。后来添加了StringBuilder,以认识到StringBuffer的设计困难。由于反复考虑了设计考虑,并决定每次都是不同的对象,因此在许多语言中都可以找到作为不同对象的可变和不变字符串。 C#“字符串是不可变的”,为什么.NET字符串是不可变的?,目标C NSString是不可变的,而NSMutableString是可变的。 stackoverflow.com/questions/9544182

–user40980
13年4月16日在18:12