hashmap

来源:互联网 发布:淘宝会查物流重量吗 编辑:程序博客网 时间:2024/06/09 21:28

1. Object对象的hashcode()

       对象的hashcode值是如何得来的,这个问题想必会困扰很多人。这里贴出一篇文章:http://blog.csdn.net/bluetjs/article/details/52610414,http://blog.csdn.net/cor_twi/article/details/46554641

 

2. JDK 源码中 HashMap 的 hash 方法原理是什么?

JDK HashMap中使用了一个 hash方法来做 bit shifting,在注释中说明是为了防止一些实现比较差的hashCode()方法,那原理是什么呢?jdk1.8之前的实现:

/**

 * Applies asupplemental hash function to a given hashCode, which

 * defends againstpoor quality hash functions.  This iscritical

 * because HashMapuses power-of-two length hash tables, that

 * otherwiseencounter collisions for hashCodes that do not differ

 * in lower bits.Note: Null keys always map to hash 0, thus index 0.

 */

static int hash(int h) {

   // This functionensures that hashCodes that differ only by

   // constantmultiples at each bit position have a bounded

   // number ofcollisions (approximately 8 at default load factor).

    //多次右移并且做异或运算,目的是使对象的hashcode各个二进制位充分混淆,增加低位的随机性,使高二进制位影响hash结果,减少碰撞

    h^= (h >>>20)^ (h >>> 12);

   return h ^ (h>>> 7)^ (h>>> 4);

}

在jdk8中这个方法已经做了优化只做一次16位右位移异或混合,而不是四次,但原理是不变的。源代码:

 static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^(h >>> 16);
    }

 大家都知道上面代码里的key.hashCode()函数调用的是key键值类型自带的哈希函数,返回int型散列值。

理论上散列值是一个int型,如果直接拿散列值作为下标访问HashMap主数组(即存放链表头节点的数组)的话,考虑到2进制32位带符号的int表值范围从­-2147483648到2147483648。前后加起来大概40亿的映射空间。只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。但问题是一个40亿长度的数组,内存是放不下的。你想,HashMap扩容之前的数组初始大小才16。所以这个散列值是不能直接拿来用的。用之前还要先做对数组的长度取模运算,得到的余数才能用来访问数组下标。源码中模运算是在这个indexFor( )函数里完成的。
bucketIndex = indexFor(hash, table.length);
indexFor的代码也很简单,就是把散列值和数组长度做一个"与"操作,
static int indexFor(int h, int length) {

         //HashMap的容量是 size = 2^n,这个方便在取模计算桶的时候可以直接用与运算,length-1刚好使低位为1。h%length的结果与 h & (length‐1)是一样的
      return h & (length‐1);
}
顺便说一下,这也正好解释了为什么HashMap的数组长度要取2的整次幂。因为这样(数组长度­1)正好相当于一个“低位掩码”。“与”操作的结果就是散列值的高位全部归零,只保留低位值,用来做数组下标访问。以初始长度16为例,16­-1=15。2进制表示是00000000 00000000 00001111。和某散列值做“与”操作如下,结果就是截取了最低的四位值。
    10100101 11000100 00100101
& 00000000 00000000 00001111
‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐
    00000000 00000000 00000101 //高位全部归零,只保留末四位
但这时候问题就来了,这样就算我的散列值分布再松散,要是只取最后几位的话,碰撞也会很严重(因为散列值的高位做与运算后,全部为0)。更要命的是如果散列本身做得不好,分布上成等差数列的漏洞,恰好使最后几个低位呈现规律性重复,就无比蛋疼。这样做hash后,会导致大量的重复。

这时候“扰动函数”的价值就体现出来了,说到这里大家应该猜出来了。看下面这个图:


右位移16位,正好是32bit的一半,自己的高半区和低半区做异或,就是为了混合原始哈希码的高位和低位,以此来加大低位的随机性。而且混合后的低位掺杂了高位的部分特征,这样高位的信息也被变相保留下来。
最后我们来看一下Peter Lawley的一篇专栏文章《An introduction to optimising a hashingstrategy》里的的一个实验:他随机选取了352个字符串,在他们散列值完全没有冲突的前提下,对它们做低位掩码,取数组下标。


结果显示,当HashMap数组长度为512的时候,也就是用掩码取低9位的时候,在没有扰动函数的情况下,发生了103次碰撞,接近30%。而在使用了扰动函数之后只有92次碰撞。碰撞减少了将近10%。看来扰动函数确实还是有功效的。但明显Java 8觉得扰动做一次就够了,做4次的话,多了可能边际效用也不大,所谓为了效率考虑就改成一次了。

 

3. hashmap的实现原理

(1) hashmap解决的问题

数组:数组存储区间是连续的,占用内存大,空间复杂度大。但数组的二分查找时间复杂度小,为O(1);数组的特点是:寻址容易,插入和删除困难;

链表:链表存储区间离散,占用内存比较宽松,故空间复杂度很小,但时间复杂度很大,达O(N)。链表的特点是:寻址困难,插入和删除容易。

哈希表:哈希表((Hash table)综合了两者的优点,同时避免了两者的缺点。既满足了数据的查找方便,同时不占用太多的内容空间,使用也十分方便。

(2) 数据结构

hashmap存储数据主要是通过数组+链表实现的,通过将key的hashcode映射到数组的不同元素(桶,hash中的叫法),然后冲突的元素放入链表中。如图:


链接中Entry的结构

static classNode<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;

         //省略部分代码

}


HashMap中存储的是类型为Node<K,V>的数组。HashMap中的hash运算的目的就是使元素均匀的分布到该数组链表中。

这里有对HashMap非常详细的解释:http://blog.csdn.net/yissan/article/details/50888070

0 0
原创粉丝点击