自定义hashCode()
参考目录:
1. HashMap 散列初体验
2. 为什么HashMap 常用String 对象作key
3. HashMap 原理
4.自定义 hashCode()
5.HashMap 的性能因子
在我们了解如何散列之后,编写自己的hashCode() 就显得更有意义了。设计hashCode() 时最重要的因素就是:无论何时对同一个对象在调用时产生的hashCode() 都应该产生同样的值。如果将一个对象用put() 方法添加进HashMap 时产生一个hashCode() 值,而用get() 方法取出时却产生另一个hashCode() 值,那么就无法取出该对象了。
所以在覆盖hashCode() 时如果我们产生的散列码依赖于对象中异变的数据(这个异变的数据指的是在当前这个对象中),那我们就应该当心了,因为数据变化时,hashCode() 就会产生一个不同的散列码,相当于产生了一个不同的键。此外也不应该使hashCode() 依赖于具有唯一性的对象信息,尤其是使用this 的值,这样只会产生更糟糕的hashCode()。
要想使hashCode() 实用,它必须速度很快,并且必须有意义。也就是说它必须基于对象的内容生成散列码。散列码不一定必须是独一无二的,但是要能通过hashCode() 和equals() 确定对象的唯一身份。另一个重要的因素是:好的hashCode() 方法应该产生均匀的散列码。如果散列码都集中在某一块,那么HashMap 和 HashSet 在某些区域就会负载很严重,没有分布均匀的散列码块。
怎么写出一份像样的hashCode() 呢,我们可以参考下面的这些条件:
1. 给int 变量result 赋予某个非零值常量
2. 为对象内每个有意义的域f(即每个可以做equals() 操作的域)计算出一个int 散列码c
域类型 | 计算 |
---|---|
boolean | c = (f ? 0 : 1) |
byte char short 或int | c = (int)f |
long | c = (int)(f ^ (F >>> 32)) |
float | c = Float.floatToIntBits(f) |
double | long l = Double.doubleToLongBits(f) || c = (int)(l ^ (l >>> 32)) |
Object,其equals() 调用这个域的equals() | c = f.hashCode() |
数组 | 对每个元素进行上述操作 |
3.合并计算得到的散列码 :result = 37 * result + c;
4. 返回result
5. 检查hashCode() 最后生成的结果,确保相同的对象具有相同的散列码
下面就是一个遵循这些条件的一个例子:
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class CountedString {
private static List<String> created = new ArrayList<>();
private String s = "";
private int id = 0;
public CountedString(String str){
s = str;
created.add(s);
for(String string : created)
if(string.equals(s))
id++;
}
@Override
public int hashCode() {
int result = 17;
result = 37 * result + s.hashCode();
result = 37 * result + id;
return result;
}
@Override
public String toString() {
return "CountedString{" + "s='" + s + '\'' + ", id=" + id + ", hashCode=" + hashCode() + '}';
}
@Override
public boolean equals(Object obj) {
return obj instanceof CountedString &&
s.equals(((CountedString)obj).s) &&
id == ((CountedString)obj).id;
}
public static void main(String[] args) {
Map<CountedString,Integer> map = new HashMap<>();
CountedString[] cs = new CountedString[5];
for (int i = 0; i < cs.length; i++) {
cs[i] = new CountedString("Hi");
map.put(cs[i], i);
}
for(CountedString countedString : cs){
System.out.println("Looking up" + countedString);
System.out.println(map.get(countedString));
}
}
}
输出:
Looking upCountedString{s='Hi', id=1, hashCode=109743}
0
Looking upCountedString{s='Hi', id=2, hashCode=109744}
1
Looking upCountedString{s='Hi', id=3, hashCode=109745}
2
Looking upCountedString{s='Hi', id=4, hashCode=109746}
3
Looking upCountedString{s='Hi', id=5, hashCode=109747}
4
CountedString 由一个String 和一个 id 组成,此id 代表包含相同String 的 CountedString对象的编号。所有的String 都被保存在一个静态属性 ArrayList 中,在构造器中通过迭代遍历此ArrayList 完成对 id 的计算。
hashCode() 与 equals() 方法都基于CountedString 的这两个域来计算结果;如果它们只基于String 或者只基于id ,不同的对象就可能会产生相同的散列码。
在main() 方法中,我们使用相同的String 创建了多个CountedString 对象。这说明虽然String 相同,但是由于id 不同,所以它们产生的散列码也不会相同。
参考书籍:
《Java 编程思想》Bruce Eckel 著 陈昊鹏 译