Skip to content

Commit 991da26

Browse files
committed
update Article 9 for Effective Java
1 parent f7d5900 commit 991da26

File tree

1 file changed

+64
-1
lines changed

1 file changed

+64
-1
lines changed

docs/effective-java.md

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -938,4 +938,67 @@ public final class PhoneNumber {
938938
... // Remainder omitted
939939
}
940940

941-
```
941+
```
942+
943+
假设你企图将这个类与 HashMap 一起使用:
944+
945+
```java
946+
947+
Map<PhoneNumber, String> map = new HashMap<PhoneNumber, String>();
948+
map.put(new PhoneNumber(21, 210, 20000), "tommy");
949+
950+
```
951+
952+
这时候,你期望的是 map.get(new PhoneNumber(21, 210, 20000)) 会返回 "tommy",但它实际上返回的是 null。注意,
953+
这里涉及两个实例:第一个被用于插入到 HashMap 中,第二个实例和第一个相等,被作为用于获取数据的 key。由于 PhoneNumber
954+
没有覆盖 hashCode 方法,从而导致两个相等的实例具有不相等的散列码,违反了 hashCode 的约定。因此,put 方法把 PhoneNumber
955+
对象存放到一个散列桶(hash bucket)中,get 方法却在另一个散列桶中查找这个 PhoneNumber 对象。即使这两个实例正好被放到
956+
同一个散列桶里,get 方法也必定会返回 null,因为 HashMap 有一项优化,可以将与每个项相关联的散列码缓存起来,如果散列码不
957+
匹配,也不必检验对象的等同性。
958+
959+
下面说说如何设计一个好的散列函数,好的散列函数通常倾向于“为不相等的对象产生不同的散列码”。这正是 hashCode 约定中第三条
960+
的含义。理想情况下,散列函数应该把集合中不相等的实例均匀的分布到所有可能的散列值上。要想完全达到这种理想的情形是非常困难的。
961+
幸运的是,相对接近这种理想情形并不太困难。下面给出一种简单的解决办法:
962+
963+
1. 把某个非零的常数值,比如说 17,保存在一个名为 res 的 int 类型变量中。
964+
965+
2. 对于对象中每个关键域 f (指 equals 方法中涉及的每个域),完成以下步骤:
966+
967+
a. 为该域计算 int 类型的散列码 c:
968+
969+
i. 如果该域是 boolean 类型,则计算(f ? 1 : 0)。
970+
971+
ii. 如果该域是 byte、char、short 或者 int 类型,则计算 (int)f。
972+
973+
iii. 如果该域是 long 类型,则计算 (int)(f ^ (f >>> 32))。
974+
975+
iv. 如果该域是 float 类型,则计算 Float.floatToIntBits(f)。
976+
977+
v. 如果该域是 double 类型,则计算 Double.floatToLongBits(f),然后按步骤 2.a.iii,
978+
为得到的 long 类型值计算散列值。
979+
980+
vi. 如果该域是一个对象引用,如果该类的 equals 方法通过递归地调用 equals 的方式来比较这个域,则同样为这个
981+
域递归地调用 hashCode。如果需要更复杂的比较,则为这个域计算一个“范式(canonical representation)”,
982+
然后针对这个范式调用 hashCode。如果这个域的值为 null,则返回 0(或者其它某个常数,但通常是0)。
983+
984+
vii. 如果该域是一个数组,则要把每个元素当作单独的域来处理。也就是说,递归地应用上述规则,对每个重要的元素计算
985+
一个散列码,然后根据步骤 2.b 中做法把这些散列值组合起来。如果数组域中的每个元素都很重要,可以利用发行版
986+
本 1.5 中增加的其中一个 Arrays.hashCode 方法。
987+
988+
b. 按照以下公式,把步骤 2.a 中计算得到的散列码 c 合并到 res 中:
989+
990+
res = res * 31 + c;
991+
992+
3. 返回 res。
993+
994+
4. 写完 hashCode 方法之后,问问自己“相等的实例是否都具有相同的散列码”。要编写单元测试来验证你的推断。如果相等的实例有着
995+
不相同的散列码,则要找出原因,并修正错误。
996+
997+
在散列码的计算过程中,可以把冗余域(`redundant field`)排除在外。换句话说,如果一个域的值可以根据参与计算的其它域值计算
998+
出来,则可以把这样的域排除在外。必须排除 equals 比较计算中没有用到的任何域,否则很有可能违反 hashCode 约定的第二条。
999+
1000+
上述步骤 1 中用到了一个非零的初始值,因此步骤 2.a 中计算的散列值为 0 的那些初始域,会影响到散列值。如果步骤 1 中的初始值
1001+
为 0,则整个散列值将不受这些初始域的影响,因为这些初始域会增加冲突的可能性。值 17 则是任选的。
1002+
1003+
步骤 2.b 中的乘法部分使得散列值依赖于域的顺序,如果一个类包含多个相似的域,这样的乘法运算就会产生一个更好的散列函数。例如,
1004+
如果 String 散列函数省略了这个乘法部分,那么只是字母顺序不同的所有字符串都会有相同的散列码。

0 commit comments

Comments
 (0)