Skip to content

Commit

Permalink
finish Article 9 for Effective Java
Browse files Browse the repository at this point in the history
  • Loading branch information
joyang1 committed Aug 19, 2019
1 parent 991da26 commit 069ad79
Showing 1 changed file with 51 additions and 1 deletion.
52 changes: 51 additions & 1 deletion docs/effective-java.md
Original file line number Diff line number Diff line change
Expand Up @@ -1001,4 +1001,54 @@ map.put(new PhoneNumber(21, 210, 20000), "tommy");
为 0,则整个散列值将不受这些初始域的影响,因为这些初始域会增加冲突的可能性。值 17 则是任选的。

步骤 2.b 中的乘法部分使得散列值依赖于域的顺序,如果一个类包含多个相似的域,这样的乘法运算就会产生一个更好的散列函数。例如,
如果 String 散列函数省略了这个乘法部分,那么只是字母顺序不同的所有字符串都会有相同的散列码。
如果 String 散列函数省略了这个乘法部分,那么只是字母顺序不同的所有字符串都会有相同的散列码。之所以选择 31,是因为它是一个
奇素数。如果乘数是偶数,并且乘法溢出的话,信息就会丢失。因为与 2 相乘等价于移位运算,使用素数的好处并不明显,但是习惯上都是
使用素数来计算散列结果。31 有个很好的特性,即用移位和减法来替代乘法,可以得到更好的性能:31 * i == (i << 5) - 1。现代
JVM 可以自动完成这种优化。

用上述方法,我们重写 hashCode 方法:

```java

@Override
public int hashCode() {
int res = 17;
res = 31 * res + areaCode;
res = 31 * res + prefix;
res = 31 * res + lineNumber;
return res;
}

```

实际上,对于 PhoneNumber 类的 hashCode 实现而言,上面这个方法是非常合理的,相当于 JDK 中的实现。它的做法非常简单,也相当
快捷,恰当地把不相等的电话号码分散到不同的散列桶中。

如果一个类是不可变的,并且计算散列码的开销的也比较大,就应该考虑把散列码缓存在对象内部,而不是每次请求的时候都重新计算散列码。
如果你觉得这种类型的大多数对象会被用做散列键(hash keys),就应该在创建实例的时候计算散列码。否则,可以选择 “延迟初始化(`lazily initialize`)” 散列码,一直到 hashCode 第一次被调用的时候才初始化(见 71 条)。如果是 PhoneNumber 会被
经常用来作为 hash key 的话,那么应该这样实现:

```java

// Lazily initialized, cached hashCode
private volatile int hashCode;

@Override
public int hashCode() {
int res = hashCode;
if (res == 0) {
int res = 17;
res = 31 * res + areaCode;
res = 31 * res + prefix;
res = 31 * res + lineNumber;
}

return res;
}

```

**不要试图从散列码计算中排除掉一个对象的关键部分来提高性能**。虽然这样得到的散列函数运行起来可能会更快,但是它的效果不见得会更好,可能会导致散列表慢到根本无法使用。

Java 平台类库中的许多类,比如 String、 Integer 和 Date,都可以把它们的 hashCode 方法返回的确切值规定为该实例值的一个函数。一般来说,这并不是一个好主意,因为这样做严格地限制了在将来的版本中改进散列函数的能力。如果没有规定散列函数的细节,那么当你
发现了它的内部缺陷时,就可以在后面的发行版本中修正它,确信没有任何客户端依赖于散列函数返回的确切值。

0 comments on commit 069ad79

Please sign in to comment.