在明白了HashMap 具有哪些功能之后,学习如何写一个 hashCode()会更有意义。
首先,你无法控制bucket 数组的索引值的产生。这个值依赖于具体的 HashMap 对象的
容量,而容量的改变与负载因子和容器有多满有关。hashCode()生成的结果,经过处理后
成为“桶”的索引(在 SimpleHashMap 中,只是对其取模,模数为 bucket 数组的大小)。
设计 hashCode()时最重要的因素就是:无论何时,对同一个对象调用 hashCode()都应
该生成同样的值。如果在将一个对象用 put()添加进 HashMap 时,产生一个 hashCode()
值,而用get()取出时,却产生了另一个 hashCode()值,那么你就无法重新取得该对象了。
所以,如果你的hashCode()依赖于对象中易变的数据,用户就必须当心了,因为此数据
发生变化时,hashCode()就会生成一个不同的散列码,相当于产生了一个不同的“键”。
此外,你也不应该使hashCode()依赖于具有唯一性的对象信息。尤其是使用 this 的值,
这只能作出很糟糕的hashCode()。因为你无法生成一个新的“键”,使之与 put()中原始
的“键值对”中的“键”相同。这正是 SpringDetector.java 的问题所在,因为它默认的
hashCode()使用的是对象的地址。所以,你应该使用对象内有意义的识别信息。
下面以 String 类为例。String 有个特点:如果程序中有多个 String 对象,都包含相同的
字符串序列,那么这些 String 对象都映射到同一块内存区域(附录 A 详细描述此机制)。
所以 newString(“hello”)生成的两个实例,虽然是相互独立的,但是对它们使用
hashCode()应该生成同样的结果。通过下面的程序可以看到这种情况:
//:c11:StringHashCode.java
import com.bruceeckel.simpletest.*;
public class StringHashCode {
private static Test monitor = new Test();
public static void main(String[] args) {
System.out.println("Hello".hashCode());
System.out.println("Hello".hashCode());
monitor.expect(new String[] {
"69609650",
"69609650"
});
}
} ///:~
对于 String 而言,hashCode()明显是基于 String 的内容的。
因此,要想使hashCode()实用,它必须速度快,并且必须有意义。也就是说,它必须基
于对象的内容生成散列码。记得吗,散列码不必是独一无二的(应该更关注生成速度,而
不是唯一性),但是通过 hashCode()和 equals(),必须能够完全确定对象的身份。
因为在生成bucket 的索引前,hashCode()还需要进一步的处理,所以散列码的生成范围
并不重要,只要是int 即可。
还有另一个影响因素:好的 hashCode()应该产生分布均匀的散列码。如果散列码都集中
在一块,那么HashMap 或者 HashSet 在某些区域的负载会很重,就不会有分布均匀的散
列函数那么快。
在 EffectiveJava (Addison-Wesley2001) 这本书中,Joshua Bloch 为怎样写出一份
像样的 hashCode(),给出了基本的指导:
1. 给 int 变量 result 赋予某个非零值常量,例如 17。
2.为对象内每个有意义的属性 f(即每个可以作 equals()操作的属性)计算出一个
int 散列码 c:
域类型
boolean
c = (f ? 0 : 1)
计算
byte,char, short, or c = (int)f
int
long
float
double
c = (int)(f ^ (f >>>32))
c = Float.floatToIntBits(f);
long l = Double.doubleToLongBits(f);
c = (int)(l ^ (l >>> 32))
Object, 其 equals() c = f.hashCode( )
调用这个域的
equals( )
数组
对每个元素应用上述规则
1. 合并计算得到的散列码:
result = 37 *result + c;
2.返回 result
3.检查 hashCode()最后生成的结果,确保相同的对象有相同的散列码。
下面便是遵循这些指导的一个例子:
//:c11:CountedString.java
// Creating agood hashCode().
import com.bruceeckel.simpletest.*;
import java.util.*;
public class CountedString {
private static Test monitor = new Test();
private static List created = new ArrayList();
private String s;
private int id = 0;
public CountedString(String str) {
s = str;
created.add(s);
Iterator it = created.iterator();
// Id is thetotal number of instances
// of thisstring in use by CountedString:
while(it.hasNext())
if(it.next().equals(s))
id++;
}
public String toString() {
return "String: " + s + " id: " + id +
"hashCode(): "+ hashCode();
}
public int hashCode() {
// Very simpleapproach:
// returns.hashCode() * id;
// Using JoshuaBloch's recipe:
int result = 17;
result = 37*result + s.hashCode();
result = 37*result + id;
return result;
}
public boolean equals(Object o) {
return (o instanceof CountedString)
&& s.equals(((CountedString)o).s)
&& id == ((CountedString)o).id;
}
public static void main(String[] args) {
Map map = new HashMap();
CountedString[] cs = new CountedString[10];
for(int i = 0; i < cs.length; i++) {
cs[i] = new CountedString("hi");
map.put(cs[i], new Integer(i));
}
System.out.println(map);
for(int i = 0; i < cs.length; i++) {
System.out.println("Looking up " + cs[i]);
System.out.println(map.get(cs[i]));
}
monitor.expect(new String[] {
"{String:hi id: 4 hashCode(): 146450=3," +
" String:hi id: 10 hashCode(): 146456=9," +
" String:hi id: 6 hashCode(): 146452=5," +
" String:hi id: 1 hashCode(): 146447=0," +
" String:hi id: 9 hashCode(): 146455=8," +
" String:hi id: 8 hashCode(): 146454=7," +
" String:hi id: 3 hashCode(): 146449=2," +
" String:hi id: 5 hashCode(): 146451=4," +
" String:hi id: 7 hashCode(): 146453=6," +
" String:hi id: 2 hashCode(): 146448=1}",
"Lookingup String: hi id: 1 hashCode(): 146447",
"0",
"Lookingup String: hi id: 2 hashCode(): 146448",
"1",
"Lookingup String: hi id: 3 hashCode(): 146449",
"2",
"Lookingup String: hi id: 4 hashCode(): 146450",
"3",
"Lookingup String: hi id: 5 hashCode(): 146451",
"4",
"Lookingup String: hi id: 6 hashCode(): 146452",
"5",
"Lookingup String: hi id: 7 hashCode(): 146453",
"6",
"Lookingup String: hi id: 8 hashCode(): 146454",
"7",
"Lookingup String: hi id: 9 hashCode(): 146455",
"8",
"Lookingup String: hi id: 10 hashCode(): 146456",
"9"
});
}
} ///:~
CountedString 由一个 String 和一个 id 组成,此 id 代表包含相同 String 的
CountedString 对象的编号。所有的 String 都被存储在 static ArrayList 中,构造器中
使用迭代器遍历此ArrayList,此时完成对 id 的计算。
hashCode()和 equals()都基于 CountedString 的这两个属性来生成结果;如果它们只
基于 String 或者只基于 id,不同的对象就可能产生相同的值。
在 main()中,使用相同的 String 创建了一串 CountedString 对象。以此说明,虽然 String
相同,但是由于id 不同,所以使得它们的散列码并不相同。在程序中,HashMap 被打印
了出来,因此你可以看到它内部是如何存储元素的(以无法辨别的次序),然后程序单独
查询每一个“键”,以此证明查询机制工作正常。
为你的类编写正确的hashCode()和equals()是很需要技巧的。Apache的“Jakarta
Commons”项目中有许多工具可以帮助你,可在jakarta.apache.org/commons的
“lang”下面找到。