HashMap作为最常用的容器类之一不一定每个人都对它非常了解。即使面试题也仅仅会比较几种容器的性能和线程安全问题。
HashMap如此常用确实有着它精巧的设计,即使我现在也无法真正理解其hash()策略是如何想到的。这里就把我一些理解的写出来,一方面是自己的笔记,另一方面是可以和广大程序员作一个交流,说不定能解开我一些无法解答的迷。
这里先提几个问题作为引子。
1. HashMap是以什么为基础结构来作到快速定位。
2. HashMap是如何解决Key冲突的。
3. 如何保证数组的容量在控制范围内(就是数组的下标是自己能掌控的)
4. HashMap的散列算法。
5. HashMap的一些弊端。
这几个问题如果扩展开来讲都代表着一些思想在里面。好,先从第一个开始。其实我们都知道Map有着Key-Value键值对,这样就可以保证快速定位,这些思想在硬件内存索引,搜索引擎,数据库中都有着类似的思想。这里就不扩展开来讲。下面就是要知道在java的基础类中有哪些可以满足这样的要求,可以直接根据key找到value。可能一时想不到数组,或许很多人没等想出答案已经在不同途径知道答案了。数组(PS:在Java中数组并不是一个常用的结构,一般都会用List来代替)。数组正有这样的特性。其下标正是我们想要的key。
这里又会出两个新问题:a)key是Integer类型;b)如何保证key的唯一性。这就是我上面所提出的第二个问题。为了使key成为Integer类型,可以用一定的算法让各种类型映射成Integer类型,其实这里难就难在如何有一个算法让不同类型的值能映射成Integer的时候唯一,至少保证大部分唯一。这里联想到hashCode可以满足这个要求,而且我们也可以实现自己的hashCode方法来达到这个目的。这样第二个问题就解决了。
这里就会到第三个问题,因为hashCode虽然可以实现唯一,但是下一个问题就出现了,那数组下标如何保证,我们不可能申请一个无限大的内存,而且hashCode是有负数的。这样就引出了HashMap第四个问题,它精巧的hash算法。关于HashMap中的hash()方法是根据位移来保证高位参与^的运算。这是因为当数组申请的容量很小的时候(HashMap默认是16)。很容易造成key冲突。
这样讲估计很难讲清楚,我先把代码贴出来:
第一个是它的hash算法,这里通过位移的方式让高位参与运算,很多人会问为什么要这样做,这里就是为了解决key冲突的算法。
static int hash(int h) {
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
第二个看一下它另外一个精巧的设计,就是如何保证数组key是保证在可控的范围内。这个方法在我第一次看到的时候真心感觉这个设计太精巧了。
/**
* Returns index for hash code h.
*/
static int indexFor(int h, int length) {
return h & (length-1);
}
这段代码是put()中的一段,就是用来判断是否有相同值,如果hashCode和key都相同,说明确实相同。极端情况下要把余下的数组扫描完。所以说一个好的哈希算法的重要性。
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
这两个方法其实要合并起来看,第一个方法是保证第二个方法不至于过多的key冲突。这里举个例子,如果HashMap的容量是16,那么(length-1)就是15,二进制是1111,这样它可以存储key从0~15的值,比如我要存储下标为1这个值,这样不通过hash算法将会出现大量的key冲突,比如说二进制0001,10001,110001这样只要低四位是0001的hashCode都可以满足这样要求。hash算法通过高位右移来参与低位的运算以求保证低位的不同。至于具体为什么可以达到这样的目标我也说不上来,看了半天也无法说出个理来。即使这样也不能保证百分之百的不冲突,只是概率会降低很多。
好,第三个问题就到此为止。
第四个问题。关于HashMap的一些弊端。第一个就是对于数组利用率,由于hashCode接近于随机性,导致对于数组的利用率并不能到达100%。大概只有70%左右。第二个就是当删除数据后数组无法弹性伸缩。
HashMap还有其他方法,有兴趣的可以慢慢看。