如果希望某个类作为HashMap的键,则此类必须实现自己的hashCode和equals方法。
例如有一个Groundhog类
public class Groundhog{
protected int number;
public Groundhog(int n){number = n;}
public String toString(){
return "Groundhog #" + number;
}
}
下面使用Groundhog作为Key,value为一个字符串
Map map = new HashMap();
map.put(new Groundhog(3), "Ground3");
下面查询此map:
Groundhog gh = new Groundhog(3);
if(map.containsKey(gh))
{
System.out.println((String)map.get(gh));
}
else
{
System.out.println("key not found:" + gh);
}
这段程序代码看起来很简单,但是它无效。因为Groundhog继承基类Object,所以
这里使用Object的hashCode方法生成散列码,而它默认是使用对象的地址计算散列码。
由于两个Groundhog(3)生成的散列码不一样,所以在Map里面就无法查找。
所以需要实现hashcode方法,使得相同的对象具有同样的hashcode,这里所说的相同的对象
是从业务上来说的,比如业务上认为number相同,则Groundhog的对象则相同。
仅仅实现合适的hashcode方法还是不够的,hashcode只用于实现查找hash地址。
还是实现equals方法,equals方法严格判断两个对象是否相等。
下面的类Groundhog2就能够作为Map的key。
public class Groundhog2 extends Groundhog{
protected int number;
public Groundhog2(int n){super(n);}
public int hashCode(){return number;}
public boolean equals(Object o){
return (o instanceof Groundhog2)
&& (number == ((Groundhog2)o).number);
}
}
不仅是HashMap,所有使用散列的数据结构(HashSet,HashMap,LinkedHashSet,and
LinkedHashMap)都无法正确的处理你的键。当然要很好的理解这个问题,必须了解这些数据结构的内部构造。
首先散列的目的是要使用一个对象来查找另一个对象。散列就类似于
家里挂的珠帘,一个个的珠子构成一串,每一串并行而挂。如果把
每一个珠子看成是Key-value对,串看成是bucket,每一串悬挂的地方定义hash地址
那么这就是一个HashMap了,只不过珠帘每一串的珠子个数是一样多的,而HashMap通常很难做到
每一个bucket里面的key-value的个数一样多。HashMap就是根据key的hashcode计算其hash code
,hashcode值一样的key-value对放在同一个bucket里面,同一个bucket里面也就可能好多个key-value值。
当进行查找的时候,HashMap先计算这个key的hashcode,通过hashcode马上定位到这个key-value在哪个bucket
里面,这样查询的速度就非常快,然后在bucket里面通过equals方法在一个个查找,如果有一个key-value满足,则就找到了key,value就可以取到了,否则就没有相应的key了。
从上面的对Hash原理的剖析可以知道,对于这样的key必须实现其hashCode和equals方法,缺一不可。
这里顺便提一下HashMap的性能因子,要理解这个问题必须解释一些术语。
容量(Capacity):散列表中bucket的数量,俗称桶的数量
初始化容量(initial capacity):创建散列表时桶的数量。HashMap和HashSet都允许在构造函数中指定初始化容量
尺寸(Size):当前散列表中记录的数量
负载因子(load factor):等于”尺寸/容量“。负载因子为0,表示空的散列表,0.5表示半满的散列表,以此类推。
轻负载的散列表具有冲突少,适宜插入与查询的特点。较高的负载因子虽然会降低空间的需求,但会提高查询的时间开销。
如果知道HashMap中会有很多记录,在创建时就使用较大的初始化容量,这样可以避免自动重散列的开销。