HashMap详解
【基本介绍】
链接相关知识:Map, List, Set基本介绍。
HashMap是一个散装桶(数组或链表),它存储的内容是键值对(key-value)映射。
HashMap采用了数组和链表的数据结构,继承了数组的线性查找和链表的寻址修改。
HashMap是非synchronized的,所以很快。
HashMap可以接受null键和值,而HashTable不能。
ConcurrentHashMap与HashTable因为在多线程并发的情况下,put操作时无法分辨key是没找到为null,还是key值对应的value为null,所以基于这类情况不允许键值为null。
HashMap是线程不安全的,也就无所谓了。
【工作原理】
HashMap是基于hashing的原理,咱们使用put(key,value)把对象存储到HashMap中,使用get(key)从HashMap中获取对象。
当我们使用put方法传递键值对的时候,先对键调用hashCode方法,计算并返回的hashCode……
HashMap在bucket中存储键对象和值对象,作为Map.node。
以下是HashMap的初始化(简单模拟):
Node table = new Node[16] //散列桶初始化,table hash; //hash值 key; //键 value; //值 Node next; //用于指向链表的下一层(产生冲突,用拉链法) } |
以下是具体的put过程(jdk1.8)
1. 对Key求Hash值,然后再计算下标
2. 如果没有碰撞,直接放入桶中(碰撞的意思是计算得到的Hash值相同,需要放到同一个bucket中)
3. 如果碰撞了,则调用equals() 比较value,相同则替换旧值,不同则以链表的方式链接到后面
4. 如果链表长度超过阀值( TREEIFY THRESHOLD==8),就把链表转成红黑树,链表长度低于6,就把红黑树转回链表
5. 如果桶满了(容量16*加载因子0.75),就需要 resize(扩容2倍后重排)
当我们调用get()方法,HashMap会使用键对象的hashcode找到bucket位置,找到bucket位置之后,会调用keys.equals()方法去找到链表中正确的节点,最终找到要找的值对象。
【如何减少碰撞】
扰动函数可以减少碰撞,原理是如果两个不相等的对象返回不同的hashcode的话,那么碰撞的几率就会小些,这就意味着存链表结构减小,这样取值的话就不会频繁调用equal方法,这样就能提高HashMap的性能。(扰动即Hash方法内部的算法实现,目的是让不同对象返回不同hashcode。
使用不可变的、声明作final的对象,并且采用合适的equals()和hashCode()方法的话,将会减少碰撞的发生。
为什么String, Interger这样的wrapper类适合作为键?因为String是final的,而且已经重写了equals()和hashCode()方法了。
不可变性是必须的,因为为了要计算hashCode(),就要防止键值改变,如果键值在放入时和获取时返回不同的hashcode的话,那么就不能从HashMap中找到你想要的对象。
【hash函数的实现】
在hashmap中要找到某个元素,需要根据key的hash值来求得对应数组中的位置。如何计算这个位置就是hash算法。
hashmap的数据结构是数组和链表的结合,所以我们希望这个hashmap里面的元素位置分布均匀些,尽量使得每个位置上的元素数量只有一个,那么当我们用hash算法求得这个位置的时候,马上就可以知道对应位置的元素就是我们要的,而不用再去遍历链表。
所以我们首先想到的就是把hashcode对数组长度取模运算,这样一来,元素的分布相对来说是比较均匀的。
但是,"模"运算的消耗还是比较大的,咱们可以看一下jdk1.8怎么做的:
static final int hash(Object key) { if (key == null){ return 0; } int h; h=key.hashCode(); //返回散列值也就是hashcode // ^ :按位异或 // >>>:无符号右移,忽略符号位,空位都以0补齐 //其中n是数组的长度,即Map的数组部分初始化长度 return (n-1)&(h ^ (h >>> 16)); } |
简单来说
1. 高16bt不变,低16bit和高16bit做了一个异或(得到的hashcode转化为32位的二进制,前16位高16bit和后16位低16bit做了一个异或)
2. (n-1)&hash=->得到下标
【链表过深的问题讨论】
拉链法会导致链表过深,所以选择红黑树作为替代。
没有选择二叉查找树的原因是因为在部分情况下,二叉查找树会变成一条线性结构,不能从根本解决问题。
红黑树属于平衡二叉树,但是为了保持平衡需要付出一定的代价,红黑树在插入新数据后,可能需要左旋,右旋,变色等操作保持平衡,但是总体来讲该代价较遍历线性数据要小。
所以当长度超过8的时候,引入红黑树。
【关于红黑树】
·每个节点非红即黑
·根节点总是黑色
·如果节点为红色,则其子节点是黑色
·每个叶子节点都是黑色(NIL节点)
·黑色高度一致(从根节点开始的每条路径包含的黑色节点相同)
【解决Hash碰撞的其他方法】
开放地址法
当冲突发生时,使用某种探查方法在散列表中形成一个探查序列。沿此序列一直向下查找,直到查到有效的地址。
【超过负载因子】
默认的负载因子是0.75.
也就是说,当一个map填满了75% 的bucket的时候,和别的集合类(如ArrayList等)一样,将会创建原来HashMap大小的两倍的bucket数组,用以重新调整map的大小,并将原来的对象放入新的bucket数组中。
这个过程叫做rehashing,最后的值只会有两种可能,原先的位置或者原先的位置+原size。
【rehashing】
重新调整HashMap大小的时候,有可能存在条件竞争,因为如果两个线程同时同时发现HashMap需要rehashing的时候,会同时尝试resize。
如果条件竞争发生了,那么就死循环了。
在rehashing的过程中,存储在链表中的元素的次序会反过来,这是由于为了避免尾部遍历,HashMap不会将元素每次都放在尾部,而是放在头部。
【HashTable】
数组 + 链表方式存储
默认容量:11(一般为质数)
put:
索引计算 : (key.hashCode() & 0x7FFFFFFF)% table.length
如果在链表中找到了,则替换旧值,未找到则继续
当总元素个数超过容量*加载因子的时候,扩容为原来的2倍,重新散列
新元素插入到链表头部
修改HashTable内部共享数据的方法添加了 synchronized,保证了线程安全。
【区别】
默认容量不同。
线程安全性,HashTable是线程安全的。
HashTable比较慢。
【CocurrentHashMap】
ConcurrentHashMap同步性能比HashTable更好,它仅仅根据同步级别,对map的一部分进行上锁。
ConcurrentHashMap可以用来替代HashTable,但是后者具有更好的线程安全性。
当HashTable的大小增加到一定程度的时候,性能会急剧下降,因为迭代时会锁定很长时间。
但是ConcurrentHashMap引入了分割(segmentation),无论它变得多大,都只需要锁定map的其中一部分,这时候其他线程可以访问其未锁定部分。