HashMap的底层实现·

Jdk1.8之前:

jdk1.8之前HashMap底层是数组跟链表,结合以前就是链表散列HashMap通过key的hashCode经过扰动函数处理的到hash值,然后通过(数组长度-1)&hash值判断当前元素存放的位置,如果当前元素存在的话,就判断该元素与要存入的元素的hash值以及key是否相同,如果相同直接覆盖,如果不相同就通过拉链法解决冲突。

扰动函数是指的是HashMap的hash方法。使用hash方法也就是扰动函数是为了防止一些实现比较差的hashCode()方法换句话说使用扰动函数是可以减少碰撞。

Jdk1.8的hash方法相比较于jdk1.7的hash方法更加简化,但是原理不变。

 static final int hash(Object key) {
 int h;
 // key.hashCode():返回散列值也就是hashcode
 // ^ :按位异或
 // >>>:⽆符号右移,忽略符号位,空位都以0补⻬
 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
 }

对比一下Jdk1.7的HashMap的hash方法源码

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);
}

相比于jdk1.8的hash方法,jdk1.7的hash方法的性能会比较差一点毕竟扰动了4次。

所谓“拉链法”就是:将数组和链表相结合。就是创建一个链表数组,数组中每一各就是一个链表。若遇到哈希冲突,则将冲突的值加入到链表就行。

jdk1.8之后

相比较于之前的版本,jdk1.8之后在解决哈希冲突有了比较大的变化,当链表长度大于阈值(默认8)时候

 将链表转化为红黑树,减少搜索时间。

 TreeMap,TreeSet以及jdk之后的HashMap底层都用到了红黑树,红黑树是为了解决二叉树的缺陷,因为二叉树在某种情况会退化成一个线性结构。

 HashMap的长度为什么是2的幂次方

为了能让HashMap存储高效,尽量减少碰撞,也就是把数据分配均匀,Hash值得范围值-2147483648到2147483647,前后加起来大概40亿的映射空间,只要哈希函数映射的比较松散均匀,一般很难出现碰撞,但是问题是40亿的数组 内存是放不下的所以这个散列值是不能直接拿来用的。用之前还要先做对数组长度的取模运算,得到的余数才能用来存放的位置也就是对应数组的下标。这个数组的计算方法"(n-1)&hash"。(n代表数组的长度)。这也就是解释了Hash的长度为什么是2的幂次方。

这个算法如何设计的呢?

我们首先可能会想到%取余的操作来实现。但是重点:“取余(%)会导致如果除数2的幂次方等价于与除数建议的与(&)操作(也就是说hash%length==hash&(length-1)的前提是length是2的n次方;”并且采用二进制位操作&相对于%能够提高运算效率,这就是解释了HashMap的长度为什么2的幂次方了

hashMap多线程操作导致死循环的问题:

主要原因在于并发的情况下的Rehash会造成元素之间造成循环链表,不过jdk1.8解决了这个问题,但是建议多线程的情况下使用HashMap,因为多线程下使用Hash还会造成数据丢失的情况。并发的情况下推荐使用ConcurrentHashMap。

猜你喜欢

转载自www.cnblogs.com/jinronga/p/12466601.html