22

我正在阅读Joshua Bloch的Effective Java的第 15 条。在谈到“最小化可变性”的第 15 条中,他提到了使对象不可变的五个规则。其中之一是使所有字段最终。这是规则:

使所有字段最终确定:这以系统强制执行的方式清楚地表达了您的意图。此外,如果对新创建实例的引用在没有同步的情况下从一个线程传递到另一个线程,则必须确保行为正确,如内存模型 [JLS, 17.5; 格茨06 16]。

我知道 String 类是不可变类的一个例子。通过源代码,我发现它实际上有一个不是 final 的 hash 实例。

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

那么 String 是如何变得不可变的呢?

4

5 回答 5

27

这句话解释了为什么这不是最终的:

//缓存字符串的哈希码

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

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

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

于 2012-07-01T13:16:35.613 回答
9

String是不可变的,因为就其用户而言,它永远不能被修改,并且对于所有线程来说总是相同的。

hashCode()hashCode()使用活泼的单检查习语(EJ item 71)计算,它是安全的,因为如果意外计算不止一次,它不会伤害任何人。

使所有字段最终化是使类不可变的最简单和最简单的方法,但它不是严格要求的。只要所有方法都返回相同的东西,无论哪个线程何时调用它,这个类就是不可变的。

于 2012-07-01T13:15:59.823 回答
1

即使 String 是不可变的,它也可以通过反射来改变。如果您将哈希设为最终结果,那么如果发生这种情况,您可能会搞砸事情。哈希字段的不同之处在于它主要用作缓存,一种加快计算速度的方法,hashCode()并且应该真正将其视为计算字段,而不是常量。

于 2012-07-01T13:12:59.023 回答
1

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

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

  1. 如果一个对象从持有对某个特定不可变对象的引用更改为持有对具有相同语义内容的不同对象的引用,则这种更改不应影响持有该引用的对象的可观察状态,但如果结果是假定相同的对象并不是真正相同的,可能会发生坏事,特别是如果假定假定持有引用的对象可以替代其他语义相同的对象。
  2. 即使没有任何对象“相同”的错误,仍然可能存在看起来与进行替换的线程相同的对象可能与其他线程不同的危险。这种情况不太可能发生,但如果确实发生,影响可能会非常糟糕。

尽管如此,替换不可变对象还是有一些优势的。例如,如果一个程序将比较许多包含长字符串的对象,其中许多对象虽然是单独生成的,但彼此相同,则使用 aWeakDictionary构建一个不同的字符串实例池可能很有用,并替换任何发现与池中的字符串相同,并引用池副本。这样做会导致许多相同的字符串被映射到同一个字符串,从而极大地加速了它们之间可能进行的任何未来比较。当然,如前所述,对象在逻辑上是不可变的,正确地进行比较是非常重要的。这方面的任何问题都可能将本应进行的优化变成一团糟。

于 2012-07-02T00:54:26.347 回答
0

创建一个不可变的对象你需要使类最终和它的所有成员最终,这样一旦对象被装箱,就没有人可以修改它的状态。您可以通过将成员设置为非最终但私有并且除了在构造函数中之外不修改它们来实现相同的功能。

编辑:

注意:当对字符串进行哈希处理时,Java 也会在哈希属性中缓存哈希值,但 前提是结果不为零。

于 2012-07-01T13:11:40.840 回答