ref:http://www.javashuo.com/article/p-unpusnlq-dy.htmlhtml
http://www.cnblogs.com/xiaolovewei/p/7993440.htmljava
在JDK1.6,JDK1.7中,HashMap采用位桶+链表实现,即便用链表处理冲突,同一hash值的链表都存储在一个链表里。可是当位于一个桶中的元素较多,即hash值相等的元素较多时,经过node
key值依次查找的效率较低。而JDK1.8中,HashMap采用位桶+链表+红黑树实现,当链表长度超过阈值(8)时,将链表转换为红黑树,这样大大减小了查找时间。算法
简单说下HashMap的实现原理:数组
首先有一个每一个元素都是链表(可能表述不许确)的数组,当添加一个元素(key-value)时,就首先计算元素key的hash值,以此肯定插入数组中的位置,可是可能存在同一hash数据结构
值的元素已经被放在数组同一位置了,这时就添加到同一hash值的元素的后面,他们在数组的同一位置,可是造成了链表,同一各链表上的Hash值是相同的,因此说数组存放的是链app
表。而当链表长度太长时,链表就转换为红黑树,这样大大提升了查找的效率。函数
当链表数组的容量超过初始容量的0.75时,再散列将链表数组扩大2倍,把原链表数组的搬移到新的数组中。性能
即HashMap的原理图是:优化
/** * 哈希表的结构是桶(数组)+单向链表+红黑树。 * 因为不一样的key计算出的hash值可能相同,会形成hash冲突,引入单链表解决hash冲突。 * 因此,桶中的各个元素hash值虽然相同,可是key不相同,由于hashmap不容许相同的key。 * 当链表长度过大时,访问速度降低,引入红黑树解决这一问题 * 哈希表的容量(桶的个数)是2的倍数 * 该数组的初始化放在了put中,构造函数中并无对其进行初始化 */ transient Node<k,v>[] table;//存储(位桶)的数组</k,v>
加载因子(默认0.75):为何须要使用加载因子,为何须要扩容呢?由于若是填充比很大,说明利用的空间不少,若是一直不进行扩容的话,链表就会愈来愈长,这样查找的效率很低,
由于链表的长度很大(固然最新版本使用了红黑树后会改进不少),扩容以后,将原来链表数组的每个链表分红奇偶两个子链表分别挂在新链表数组的散列位置,这样就减小了每一个链表的长
度,增长查找效率。
HashMap原本是以空间换时间,因此填充比不必太大。可是填充比过小又会致使空间浪费。若是关注内存,填充比能够稍大,若是主要关注查找性能,填充比能够稍小。
先来看看hashmap中的几个参数:
这几个参数的含义上面都有标注。HashMap默认的容量(桶的个数)是16,。默认的加载因子是0.75,加载因子loadFactor是影响hashMap进行扩容的指标之一,还有一个是容量,也就是table
数组的大小(桶的个数)。threshold是进行扩容的门限值,为capacity*loadFactor 。
当一个桶中元素个数大于8时(添加元素时判断),会将链表转成红黑树;当树的节点个数小于6时(删除节点时判断),会转成链表。
MIN_TREEIFY_CAPACITY变量:最小树形化的值。意思是:当桶的个数没有达到这个值(64)时,即便桶中元素个数大于8时,也不会转成红黑树,而是直接扩容(resize(),该方法后面介
绍),扩大桶的个数。只有当桶的个数大于等于该值时,才会树形化。
HashMap的构造方法有4种,主要涉及到的参数有:指定初始容量、指定填充比和用来初始化的Map
从构造函数能够看出,构造函数并无对底层的table数组进行初始化,而是和ArrayList同样,将初始化数组的过程推迟到第一次添加元素时进行。第一个构造函数,将传入的容量赋值给了
threshold门限,后面会在resize方法中根据该门限进行初始化。
/** * 返回key对应的value * 1.计算key的hash值。 * 2.根据hash值找到对应桶的第一个节点。 * 3.判断第一个节点是否是(比较hash值和key)。 * 4.第一个节点不是就分成黑树和链表继续遍历 */ public V get(Object key) { Node<K,V> e; return (e = getNode(hash(key), key)) == null ? null : e.value; } /** * 根据hash值和key找到对应的节点 * 1.根据hash值找到对应的桶的第一个节点。若是第一个节点hash值以及key都对应的相等,则返回第一个。 * 2.日后遍历,看看是否是树,而后遍历。 * 这里找桶的算法是(n-1)&hash。n是桶的个数(2的幂次)。 * * 这里取模时,先后两个数位置无所谓,只要有一个是2的幂次(b),另一个是a, * 取模就等价于求余数(a%(b-1),(2的幂次-1)作分母)。 * 因此,这里是等价于hash%(n-1)。也就是找桶的位置,从0开始 */ final Node<K,V> getNode(int hash, Object key) { Node<K,V>[] tab; Node<K,V> first, e; int n; K k; if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k)))) return first; if ((e = first.next) != null) { if (first instanceof TreeNode) return ((TreeNode<K,V>)first).getTreeNode(hash, key); do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } return null; }
步骤简述:
1.计算key的hash值。
2.根据hash值找到对应桶的第一个节点,hash&(n-1)。
3.判断第一个节点是否是(比较hash值和key)。
4.第一个节点不是就分成黑树和链表继续遍历
/** * 存元素,见下个方法 */ public V put(K key, V value) { return putVal(hash(key), key, value, false, true); } /** * 存元素的步骤: * 1.根据key计算hash值; * 2.判断是不是第一次加入元素(table是否为空),若是是,则调用resize函数初始化: * 若是threshold=0,则初始化为16,;若是threshold不为0(构造函数中传入加载因子,会给threshold赋值,可是没有初始化table) * 3.根据hash值找到((n-1)&hash)对应桶的第一个元素;若是第一个元素为空,那么直接插入新节点。 * 4.若是第一个元素不为空,则判断结构是否是红黑树,若是是红黑树则调用红黑树插入的方法; * 5.若是不是红黑树,则依次遍历链表,若是链表有和传入的key相同的key,则用新的value替换原来的value,并返回旧value; * 6.若是没有相同的key,则插入到链表的最后。并判断新链表的大小是否超过门限,超过则转换成红黑树。 */ final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; /** * 构造函数中没有对table进行初始化,因此第一次put时,会进行判断table是否为空,为空则要进行初始化。 * 也就是table初始化为初始化容量16. * resize()函数就是扩容:table为空时,扩容(也就是初始化)为默认容量16;table不为空时,扩容两倍(知足容量是2的幂次) */ if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; //根据hash值,找到对应桶位置的第一个元素,若是该元素为空,则直接插入。 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { Node<K,V> e; K k; //若是第一个元素不为空,且该元素的key与传入的key同样,说明已经存在该key,记录下来 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { for (int binCount = 0; ; ++binCount) { //若是是最后一个了,且key都不相同,就将新节点插入到链表最后 if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); //若是新加入节点后,链表大小超过阈值8,就转成红黑树 if (binCount >= TREEIFY_THRESHOLD - 1) // 由于从-1开始的。也就是-1到7,也就是大于8个节点时 treeifyBin(tab, hash); break; }//若是有相同的key,跳出循环 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } /** * 若是有相同的key,将用新的value替换就的value,并返回原来的value */ if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value;//新换旧 afterNodeAccess(e); return oldValue; } } ++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }
*存元素的步骤:
1.根据key计算hash值;
2.判断是不是第一次加入元素(table是否为空),若是是,则调用resize函数初始化(扩容):(见下面resize)
若是threshold=0,则初始化为16,;若是threshold不为0,初始化为threshold(构造函数中传入加载因子,会给threshold赋值,可是没有初始化table)
3.根据hash值找到((n-1)&hash)对应桶的第一个元素;若是第一个元素为空,那么直接插入新节点。
4.若是第一个元素不为空,则判断结构是否是红黑树,若是是红黑树则调用红黑树插入的方法;
5.若是不是红黑树,则依次遍历链表,若是链表有和传入的key相同的key,则用新的value替换原来的value,并返回旧value;
6.若是没有相同的key,则插入到链表的最后。并判断新链表的大小是否超过门限,超过则转换成红黑树。
7.判断新size是否是大于threshold,是就扩容。
构造hash表时,若是不指明初始大小,默认大小为16(即Node数组大小16),若是Node[]数组中的元素达到(填充比*Node.length)从新调整HashMap大小 变为原来2倍大小,扩容很耗时。
/** * table为空时,扩容(也就是初始化)为默认容量16;table不为空时,扩容两倍(知足容量是2的幂次) * resize函数中新建一个散列表数组,容量为旧表的2倍,接着把旧表的键值对迁移到新表(从新计算hash值,存入新表), * 这里分三种状况: 1. 表项只有一个键值对时,针对新表计算新的桶位置并插入键值对 2. 表项节点是红黑树节点时(说明这个bin元素较多已经转成红黑树了),split这个bin。 3. 表项节点包含多个键值对组成的链表时(拉链法),把链表上的键值对按hash值分红两串,一串放到新表的原索引位置, 另一串放到新表的原索引位置+oldCap 处。 */ final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0; if (oldCap > 0) { //若是原来桶的个数已经最大了,就不扩容了,直接返回原来数组 if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } //若是原来的容量比默认容量大,且它的2倍也没有超过最大容量,那么新容量为两倍,新扩容门限也为原来两倍 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // double threshold }//若是原来门限大于0,则新容量为原来门限 else if (oldThr > 0) // initial capacity was placed in threshold newCap = oldThr; else { // zero initial threshold signifies using defaults newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } if (newThr == 0) { float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } threshold = newThr; @SuppressWarnings({"rawtypes","unchecked"}) Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab; if (oldTab != null) { for (int j = 0; j < oldCap; ++j) { Node<K,V> e; if ((e = oldTab[j]) != null) { oldTab[j] = null; if (e.next == null) newTab[e.hash & (newCap - 1)] = e; else if (e instanceof TreeNode) ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); else { // preserve order Node<K,V> loHead = null, loTail = null; Node<K,V> hiHead = null, hiTail = null; Node<K,V> next; do { next = e.next; if ((e.hash & oldCap) == 0) { if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } else { if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); if (loTail != null) { loTail.next = null; newTab[j] = loHead;//放到新表的原索引位置 } if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead;//另外一串放到新表的 原索引+原容量 位置。 } } } } } return newTab; }
步骤:
table不为空时(原来容量>0),扩容两倍(知足容量是2的幂次);
当table为空时:1,若是threshold>0,这就是构造函数中传进来的初始化容量,初始化为该容量threshold;2,threshold=0,没有传入初始化容量,初始化为默认容量16.
扩容两倍步骤:
* resize函数中新建一个散列表数组,容量为旧表的2倍,接着把旧表的键值对迁移到新表(从新计算hash值,存入新表),
* 这里分三种状况:遍历每一个桶 j,
1. 桶中只有一个键值对时,针对新表计算新的桶位置并插入键值对
2. 桶中节点是红黑树节点时(说明这个bin元素较多已经转成红黑树了),split这个bin。
3. 桶中节点包含多个键值对组成的链表时(拉链法),把链表上的键值对按hash值分红两串(根据(hash & oldCap) == 0),一串放到新表的原索引位置 j ,另一串放到新表的 原索引位置
j+原表容量oldCap 处。
在java jdk8中对HashMap的源码进行了优化,在jdk7中,HashMap处理“碰撞”的时候,都是采用链表来存储,当碰撞的结点不少时,查询时间是O(n)。
在jdk8中,HashMap处理“碰撞”增长了红黑树这种数据结构,当碰撞结点较少时,采用链表存储,当较大时(>8个),采用红黑树(特色是查询时间是O(logn))存储(有一个阀值控制,大
于阀值(8个),将链表存储转换成红黑树存储)。
问题分析:
你可能还知道哈希碰撞会对hashMap的性能带来灾难性的影响。若是多个hashCode()的值落到同一个桶内的时候,这些值是存储到一个链表中的。最坏的状况下,全部的key都映射到同一个
桶中,这样hashmap就退化成了一个链表——查找时间从O(1)到O(n)。
随着HashMap的大小的增加,get()方法的开销也愈来愈大。因为全部的记录都在同一个桶里的超长链表内,平均查询一条记录就须要遍历一半的列表。
JDK1.8HashMap的红黑树是这样解决的:
若是某个桶中的记录过大的话(当前是TREEIFY_THRESHOLD = 8),HashMap会动态的使用一个专门的treemap实现来替换掉它。这样作的结果会更好,是O(logn),而不是糟糕的O(n)。
它是如何工做的?前面产生冲突的那些KEY对应的记录只是简单的追加到一个链表后面,这些记录只能经过遍从来进行查找。可是超过这个阈值后HashMap开始将列表升级成一个二叉树,使
用哈希值做为树的分支变量,若是两个哈希值不等,但指向同一个桶的话,较大的那个会插入到右子树里。若是哈希值相等,HashMap但愿key值最好是实现了Comparable接口的,这样它能够按
照顺序来进行插入。这对HashMap的key来讲并非必须的,不过若是实现了固然最好。若是没有实现这个接口,在出现严重的哈希碰撞的时候,你就并别期望能得到性能提高了。