为什么String类是不可变的,即使它有一个名为“hash”的非最终字段

时间:2012-07-01 13:08:09

标签: java string immutability

我正在阅读Joshua Bloch撰写的Effective Java第15项。在第15项中谈到“最小化可变性”时,他提到了使对象不可变的五条规则。其中之一就是让所有领域都是最终的。这是规则:

  

将所有字段设为最终:这会以强制执行的方式明确表达您的意图   由系统。此外,如果引用,有必要确保正确的行为   新创建的实例从一个线程传递到另一个线程而没有   同步,如内存模型中所述[JLS,17.5; Goetz06 16]。

我知道String类是一个不可变类的例子。通过source code我发现它实际上有一个非最终的哈希实例。

//Cache the hash code for the string
private int hash; // Default to 0

String如何变为不可变?

5 个答案:

答案 0 :(得分:26)

该评论解释了为什么这不是最终的:

  

//缓存字符串

的哈希码

这是一个缓存。如果您不致电hashCode,则不会设置其值。它可以在创建字符串期间设置,但这意味着更长的创建时间,对于您可能不需要的功能(哈希代码)。另一方面,每次询问时计算哈希值都是浪费,给字符串 immutable ,哈希码永远不会改变。

有一个非final字段的事实确实与你引用的定义有些矛盾,但这里它不是对象界面的一部分。它只是一个内部实现细节,它对字符串的可变性没有影响(作为字符容器)。

编辑 - 由于受欢迎的需求,完成了我的回答:虽然hash不是公共接口的直接部分,但它可能会影响该接口的行为,因为hashCode返回其值。现在,由于hashCode未同步,如果多个线程同时使用该方法,则可能会多次设置hash。但是,设置为hash的值始终是稳定计算的结果,该计算仅依赖于最终字段(valueoffsetcount)。因此,散列的每次计算都会产生完全相同的结果。对于外部用户来说,就好像hash计算了一次 - 就好像每次都计算它一样,因为hashCode的合同要求它始终为a返回相同的结果给定的价值。最重要的是,即使hash不是最终的,它的可变性对于外部查看器来说永远不可见,因此该类可以被认为是不可变的。

答案 1 :(得分:8)

String是不可变的,因为就其用户而言,它永远不会被修改,并且对所有线程看起来总是一样。

hashCode()是使用活泼的单一检查惯用法(EJ第71项)计算的,并且它是安全的,因为如果hashCode()被意外地计算多次,它不会伤害任何人。

使所有字段成为最终是使类不可变的最简单和最简单的方法,但并不是严格要求的。

,只要所有方法返回相同的东西,无论哪个线程调用它,该类都是不可变的。

答案 2 :(得分:1)

即使String是不可变的,也可以通过反射进行更改。如果你做哈希最终,如果发生这种情况,你可以把事情弄得一团糟。哈希字段也是不同的,因为它主要是作为缓存,一种加速hashCode()计算的方法,应该被认为是一个计算字段,而不是常量。

答案 3 :(得分:1)

在许多情况下,对于逻辑上不可变的类,对于相同的可观察状态具有多个不同的表示,并且类的实例能够在它们之间切换,这可能是有帮助的。 。将从散列字段为零的字符串返回的散列码值将与散列字段保存早期散列码调用的结果时返回的值相同。因此,将哈希值从前者更改为后者将不会更改对象的可观察状态,但会使将来的操作运行得更快。

以这些方式编码事物的最大困难是

  1. 如果将对象从对某个特定不可变对象的引用更改为持有对具有相同语义内容的其他对象的引用,则此类更改不应影响持有该引用的对象的可观察状态,但如果事实证明所谓的相同对象并不完全相同,那么可能会发生不好的事情,特别是如果假定持有引用的对象被认为可替代其他语义相同的对象。
  2. 即使没有任何对象“相同”的错误,仍然存在这样的危险,即与替换的线程看起来相同的对象可能看起来与其他线程不同。这种情况不太可能发生,但如果确实发生,效果可能非常糟糕。

但是,对不可变对象进行替换可能会有一些优势。例如,如果程序将比较许多持有长字符串的对象,并且其中许多对象虽然是单独生成的,但它们彼此相同,使用WeakDictionary构建不同字符串池可能很有用。实例,并使用对池副本的引用替换任何与池中的字符串相同的字符串。这样做会导致许多相同的字符串被映射到相同的字符串,从而极大地促进了它们之间可能进行的任何未来比较。当然,正如所指出的那样,对象在逻辑上是不可变的非常重要,比较是正确完成的。在这方面的任何问题都可以将优化变成混乱。

答案 4 :(得分:0)

创建一个不可变的对象你需要使类最终及其所有成员都是final,这样一旦对象被装箱,没有人可以修改它的状态。 您可以实现相同的功能,将成员设为非最终但私有,除了构造函数外不修改它们。

修改

通知:  对散列字符串进行散列时,Java还会在散列属性中缓存散列值,但仅当结果与零不同时才