为什么在Java中String是不可变的?


78

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

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

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


20
从技术上讲,这不是重复的,但是埃里克·利珀特(Eric Lippert)在这里对这个问题给出了一个很好的答案:programmers.stackexchange.com/a/190913/33843
Heinzi 2013年

Answers:


105

并发

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

由于共享字符串而出现了许多多线程C ++错误-在一个模块中,当代码中的另一个模块保存了指向它的指针并希望它保持不变时,就可以安全地进行更改。

解决方案是,每个类都对传递给它的可变对象进行防御性复制。对于可变字符串,这是O(n)进行复制。对于不可变的字符串,因为不是副本,所以复制为O(1),它是不变的对象。

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

安全

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

Java让一个人使用反射-反射的参数是字符串。将一个可以通过修改的字符串传递给另一种反映方法的危险。这真是太糟了。

哈希键

哈希表是最常用的数据结构之一。数据结构的键通常是字符串。具有不变的字符串意味着(如上所述)哈希表不需要每次都复制哈希键。如果字符串是可变的,并且哈希表没有做到这一点,则有可能在远处更改哈希键。

Java中Object的工作方式是,所有内容都有一个哈希键(可通过hashCode()方法访问)。具有不可变的字符串意味着可以缓存hashCode。考虑到将字符串用作哈希键的频率,这可以显着提高性能(而不是每次都必须重新计算哈希码)。

子串

通过使String是不可变的,支持数据结构的基础字符数组也是不可变的。这允许对substring要完成的方法进行某些优化(不一定要完成-它还引入了一些内存泄漏的可能性)。

如果您这样做:

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

的值为bar“英里”。但是,这两个foobar都可以由相同的字符数组支持,从而减少了更多字符数组的实例化或将其复制-仅使用字符串中的不同起点和终点。

foo | | (0,6)
    vv
    微笑
     ^^
酒吧 | (1,5)

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

可以从JDK 6b14的String中看到这一点(以下代码来自GPL v2来源,并用作示例)

   public String(char value[], int offset, int count) {
       if (offset < 0) {
           throw new StringIndexOutOfBoundsException(offset);
       }
       if (count < 0) {
           throw new StringIndexOutOfBoundsException(count);
       }
       // Note: offset or count might be near -1>>>1.
       if (offset > value.length - count) {
           throw new StringIndexOutOfBoundsException(offset + count);
       }
       this.offset = 0;
       this.count = count;
       this.value = Arrays.copyOfRange(value, offset, offset+count);
   }

   // Package private constructor which shares value array for speed.
   String(int offset, int count, char value[]) {
       this.value = value;
       this.offset = offset;
       this.count = count;
   }

   public String substring(int beginIndex, int endIndex) {
       if (beginIndex < 0) {
           throw new StringIndexOutOfBoundsException(beginIndex);
       }
       if (endIndex > count) {
           throw new StringIndexOutOfBoundsException(endIndex);
       }
       if (beginIndex > endIndex) {
           throw new StringIndexOutOfBoundsException(endIndex - beginIndex);
       }
       return ((beginIndex == 0) && (endIndex == count)) ? this :
           new String(offset + beginIndex, endIndex - beginIndex, value);
   }

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

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

不要滥用堆栈

一个可以传递字符串的值,而不是周围的参考一成不变的字符串,以避免与可变性的问题。但是,对于大字符串,将其传递到堆栈上将对系统造成滥用(将整个xml文档作为字符串放在堆栈上,然后将其取下或继续传递...)。

重复数据删除的可能性

当然,这并不是为什么Strings不可变的最初动机,但是当人们探讨为什么不可变Strings是一件好事的理性时,这当然是要考虑的事情。

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

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

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

其他语言

目标C(早于Java)具有NSStringNSMutableString

C#和.NET对不可变的默认字符串进行了相同的设计选择。

Lua字符串也是不可变的。

Python也是如此。

从历史上看,Lisp,Scheme,Smalltalk都将字符串固定,因此使其不可变。更现代的动态语言通常以某种要求使用字符串的方式要求它们是不可变的(它可能不是String,但是它是不可变的)。

结论

这些设计考虑已经用多种语言反复提出。普遍的共识是,不可变的字符串(尽管它们很笨拙)比替代的字符串更好,并导致更好的代码(更少的错误)和更快的可执行文件。


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

3
@BillyONeal:安全的默认设置和不安全的替代方法通常会比相反的方法导致更安全的系统。
Joachim Sauer

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

@Joachim:我没有其他要求。
Billy ONeal

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

21

我记得的原因:

  1. 根本无法实现不使字符串不可变的字符串池功能,因为在字符串池的情况下,一个字符串对象/文字(例如“ XYZ”)将被许多引用变量引用,因此,如果其中任何一个更改了值,其他变量将自动受到影响。 。

  2. 字符串已被广泛用作许多Java类的参数,例如用于打开网络连接,用于打开数据库连接,打开文件。如果String不可变,则将导致严重的安全威胁。

  3. 不变性允许String缓存其哈希码。

  4. 使它成为线程安全的。


7

1)字符串池

Java设计人员知道String将在所有Java应用程序中成为最常用的数据类型,这就是为什么他们希望从头开始进行优化。在该方向上的关键步骤之一是将String文字存储在String池中的想法。目标是通过共享它们来减少临时String对象,并且为了共享,它们必须必须来自Immutable类。您不能与彼此未知的两个方共享可变对象。让我们来看一个假设的示例,其中两个引用变量指向同一个String对象:

String s1 = "Java";
String s2 = "Java";

现在,如果s1将对象从“ Java”更改为“ C ++”,则引用变量也将获得值s2 =“ C ++”,它甚至都不知道。通过使String不可变,可以实现String文字的这种共享。简而言之,如果不使用Java将String设为final或Immutable,则无法实现String pool的关键思想。

2)安全性

Java在提供每个服务级别的安全环境方面有明确的目标,而String在所有安全方面都是至关重要的。String已被广泛用作许多Java类的参数,例如,用于打开网络连接,您可以以String的形式传递主机和端口,以Java读取文件,可以以String的形式传递文件和目录的路径,以及用于打开数据库连接,将数据库URL作为字符串传递。如果String并非一成不变,则用户可能已经授权访问系统中的特定文件,但是在身份验证之后,他可以将PATH更改为其他内容,这可能会导致严重的安全问题。同样,在连接到数据库或网络中的任何其他计算机时,更改String值可能会带来安全威胁。可变字符串也可能会在反射中引起安全问题,

3)在类加载机制中使用字符串

将String最终更改为Immutable或Immutable的另一个原因是,它在类加载机制中大量使用。由于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本身的内容。


5

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

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

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

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

由于所有这些原因,这是Java做出的早期决定之一,以使其与C ++相区别。


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

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

5

字符串不可改变的原因是与语言中其他原始类型的一致性。如果您有一个int包含值42的值,并且向其中添加了值1,则无需更改42。您将获得一个新值43,该值与起始值完全无关。除了字符串之外,对基本体进行突变没有任何概念上的意义;因此,将字符串视为不变的程序通常更容易推理和理解。

而且,Java确实提供了可变的和不可变的字符串,如您所见StringBuilder;实际上,只有默认值是不可变的字符串。如果您想将引用传递给StringBuilder周围的任何地方,则非常欢迎这样做。Java 为这些概念使用单独的类型(StringStringBuilder),因为它不支持在类型系统中表达可变性或缺少可变性。在支持其类型系统中不变性的语言(例如C ++ const)中,通常只有一个字符串类型可同时满足这两种目的。

是的,使字符串为不可变的允许人们实施一些特定于不可变的字符串的优化,例如interning,并允许传递字符串引用而无需跨线程同步。但是,这使该机制与具有简单且一致的类型系统的语言的预期目标相混淆。我把这比作每个人对垃圾回收的错误看法。垃圾回收不是“回收未使用的内存”;它是“模拟具有无限内存的计算机”。讨论的性能优化是为了使不可变字符串的目标在实际计算机上表现良好而进行的工作;并不是这些字符串一开始就不可变的原因。


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

@Shamit:是的,我确定。在43加1至42的结果它没有数字42的意思是一样的东西数量43
比利·奥尼尔

@Shamit:同样,你不能这样做43 = 6,预计数43指的是同样的事情6号
比利·奥尼尔

我= 42; i = i + 1;这个代码将存储在存储器42中,然后在相同的位置改变值至43。所以,事实上,变量“i”获取的43的新值
Shamit Verma的

@Shamit:在这种情况下,您突变了i,而不是42。考虑一下string s = "Hello "; s += "World";。您对variable的值进行了变异s。但是字符串"Hello ""World""Hello World"是不可变的。
Billy ONeal 2014年

4

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

如果String并不是一成不变的,那么每次您从某个不希望其内容在其脚下改变的上下文中检索它时,都必须“以防万一”进行复制。那变得非常昂贵。


4
此参数完全相同,适用于任何类型,而不仅适用于String。但是,例如,Arrays仍然是可变的。因此,为什么Strings是不可变的,而Array不是不可变的。如果不可变性是如此重要,那么为什么Java很难创建和使用不可变对象呢?
约尔格W¯¯米塔格

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

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

@DonalFellows:pcollections就是这样做的(但是我自己从未使用过)。
Joachim Sauer

3
@JörgWMittag:有些人(通常从纯粹的功能角度来看)会认为所有类型都应该是不变的。同样,我认为,如果你把所有的问题,随着并行,并发软件可变状态工作的一个交易,你可能会同意,与不可变对象的工作往往是很多比那些可变容易。
史蒂文·埃弗斯

2

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

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

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

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

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


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

1
@blank:很好,因为input该的handle方法已经是太长(不管是什么原来 input是),它只会引发异常。您将 调用该方法之前创建一个新输入。那不是问题。
Joachim Sauer

0

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

使用引用类型,您会关心对象,并且期望它会发生变化。例如,您经常会期望数组发生变化。如果我给您一个数组,并且您希望保持其原样,那么您要么必须信任我不要对其进行更改,要么就可以对其进行复制。

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

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


0

我真的很惊讶没有人指出这一点。

答:即使它是可变的,也不会显着地使您受益。它不会给您带来太大的好处,因为这会带来其他麻烦。让我们研究两种最常见的突变情况:

更改字符串的一个字符

由于Java字符串中的每个字符占用2或4个字节,问问自己,如果您可以对现有副本进行突变,您会得到什么收益吗?

在用2个字节的字符替换4个字节的字符的情况下(反之亦然),您必须将字符串的其余部分向左或向右移动2个字节。从计算的角度来看,这与完全复制整个字符串没有什么不同。

这也是一种非常不正常的行为,通常是不希望的。想象有人用英文文本测试一个应用程序,当该应用程序被国外(例如中国)采用时,整个过程开始变得奇怪。

将另一个字符串(或字符)追加到现有的字符串(或字符)上

如果您有两个任意字符串,则它们将位于两个不同的内存位置。如果要通过附加第二个来更改第一个,则不能只在第一个字符串的末尾请求额外的内存,因为它可能已经被占用了。

您必须将串联的字符串复制到一个全新的位置,这与两个字符串都是不可变的完全相同。

如果您想高效地执行附加操作,则可以使用StringBuilder,它在字符串的末尾保留了相当大的空间,仅用于将来可能的附加操作。


-2
  1. 它们很昂贵,并且保持它们不变,这允许子字符串共享主字符串的字节数组之类的事情。(也可以提高速度,因为不需要创建新的字节数组并复制过来)

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

    [删除旧的3看过StringBuilder src-它不与字符串共享内存(直到修改),我认为那是在1.3或1.4中的]

  3. 缓存哈希码

  4. 对于易变的字符串,请使用SB(根据需要使用生成器或缓冲区)


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

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

-4

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


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

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

3
String和StringBuffer是原始的。后来添加了StringBuilder,以认识到StringBuffer的设计困难。由于反复考虑了设计考虑,并决定每次都是不同的对象,因此在许多语言中都发现了可变对象和不可变字符串是不同的对象。C#“字符串是不可变的”为什么.NET字符串是不可变的?,目标C NSString是不可变的,而NSMutableString是可变的。stackoverflow.com/questions/9544182
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.