java之HashMap详解

匿名 (未验证) 提交于 2019-12-02 21:53:52

HashMap实现了Map接口,是一个存储键值对映射的集合,线程不安全,无序,键和值可以为null。

HashMap的底层维护着一个Entry数组,即所谓的hash表,表中元素是Entry链表的第一个节点,链表中存储的是hash冲突的元素。
Java8后Entry改名为Node,并且当链表的容量达到一定值的时候转换成红黑树

//hash表默认容量:16 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16  //最大容量  static final int MAXIMUM_CAPACITY = 1 << 30;  //装填因子,默认0.75 static final float DEFAULT_LOAD_FACTOR = 0.75f;  //将冲突链表转为红黑树的阈值 static final int TREEIFY_THRESHOLD = 8;  //由红黑树转链表时,数节点的阈值, 小于这个阈值才能转为链表 static final int UNTREEIFY_THRESHOLD = 6;  //能将链表转为红黑树的最小hash表容量。应该至少4倍于 TREEIFY_THRESHOLD //即只有当 冲突链表节点数大于等于8 且 hash表容量达到64时才将链表转为红黑树 static final int MIN_TREEIFY_CAPACITY = 64;
public V put(K key, V value) {         return putVal(hash(key), key, value, false, true);     }

这里直接调用了 hash() 方法计算 key 的hash值 并调用 putVal() 方法

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

key Ϊ null ,其hash值为0.

h = key.hashCode()计算出 key 的hash值,h >>> 16 是因为计算出的hash值是32位的,将h无符号右移16位,再与原值异或运算。得到的新hash值的高16位较h的原值不变,原值的低16位和高16位进行了异或运算。

这么做的主要目的在于,当hash数组较小时,hash值的所有bit位都参与了运算,防止hash冲突太大。

//hash: 当前key的hash值, onlyIfAbsent:是否覆盖重复值,hashMap 里面是false,evict:没用。。 final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {     //首先是创建变量,tab赋值为HashMap底层数组,p是当前数组元素,n 数组大小     Node<K,V>[] tab; Node<K,V> p; int n, i;     //初始时table肯定为null     if ((tab = table) == null || (n = tab.length) == 0)         //由于初始时table为null,所以先做一次扩容操作         n = (tab = resize()).length;     if ((p = tab[i = (n - 1) & hash]) == null)         //i = (n - 1) & hash 表示当前hash在数组中的下标,相当于取模运算         //如果当前位置为空,则新建一个Node节点放入数组         tab[i] = newNode(hash, key, value, null);     else {         //当前位置不为空,表示出现hash冲突         Node<K,V> e; K k;         if (p.hash == hash &&             ((k = p.key) == key || (key != null && key.equals(k))))             //hash值相同,key值相同表明要插入的值与当前值相同,直接将当前值赋给e             e = p;         else if (p instanceof TreeNode)             //p instanceof TreeNode 说明当前位置的hash冲突已经达到一定程度,链表转换成了红黑树             //将当前值作为红黑树节点插入             e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);         else {             //hash冲突较少,Node还是以链表形式存在             for (int binCount = 0; ; ++binCount) {                 if ((e = p.next) == null) {                     //这里就是把Node加到链表尾部                     p.next = newNode(hash, key, value, null);                     //注意这个binCount,当其值大到一定程度时,链表转为红黑树                     //默认TREEIFY_THRESHOLD为8                     if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st                         //链表转为红黑树                         treeifyBin(tab, hash);                     break;                 }                 //在遍历链表过程中发现相同的值,直接返回                 if (e.hash == hash &&                     ((k = e.key) == key || (key != null && key.equals(k))))                     break;                 p = e;             }         }         //e != null 说明存在相同的值,根据 onlyIfAbsent 选择是否覆盖         if (e != null) { // existing mapping for key             V oldValue = e.value;             if (!onlyIfAbsent || oldValue == null) //在hashMap 中新值会替换旧值                 e.value = value;             afterNodeAccess(e);             return oldValue;         }     }     //操作记录+1     ++modCount;     //判断数组大小是否达到阈值,进行扩容     if (++size > threshold)         resize();     afterNodeInsertion(evict);//不管。。。 LinkedHashMap 中有用到     return null; } 

putValue() 小结: 当向 HashMap 中添加新的值时:
1. 首先计算 key 的 hashCode
2. 如果数组为空,则先扩容
3. 检查 key 是否已存在于数组中,如果不存在直接添加节点,转到 6
4. 如果已经存在key,则在冲突链表或者红黑树中寻找是否存在相同的 Value,如果不存在则将此结点加到链表末尾或者RB树中,转到6
5. 如果Value也已经存在,则覆盖原值
6. 检查添加节点后是否需要扩容
7. 结束

final Node<K,V>[] resize() {     //记录旧的数组     Node<K,V>[] oldTab = table;//1     int oldCap = (oldTab == null) ? 0 : oldTab.length;//2     int oldThr = threshold;//3     int newCap, newThr = 0;//4     if (oldCap > 0) {// 5         if (oldCap >= MAXIMUM_CAPACITY) {             /*             如果数组旧的容量已经达到最大容量,将阈值也设为最大值             此时不在进行数组2倍扩容,直接无限增加结点数量             */             threshold = Integer.MAX_VALUE;             return oldTab;         }         /*         如果数组还没有达到最大容量,则进行2倍扩容,扩容时还要判断是否达到最大容量         如果达到最大容量则转到第8步         oldCap >= DEFAULT_INITIAL_CAPACITY 说明已经进行过扩容,不再是初始容量         */         else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&                  oldCap >= DEFAULT_INITIAL_CAPACITY)             //扩容同时增大新的阈值             newThr = oldThr << 1; // double threshold     }     else if (oldThr > 0) // 6 initial capacity was placed in threshold         newCap = oldThr;     else {               //7 第一次resize时运行         newCap = DEFAULT_INITIAL_CAPACITY; //初始默认容量16         //初始默认装填因子0.75,因此默认阈值为 16*0.75 = 12         newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);     }     if (newThr == 0) { // 8         //如果运行该if, 表示扩容2倍到达上限或原数组大小小于16         float ft = (float)newCap * loadFactor;         newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?                   (int)ft : Integer.MAX_VALUE);     }     threshold = newThr;     @SuppressWarnings({"rawtypes","unchecked"})         //重新申请大小为newCap的数组         Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];     table = newTab;    //下面是resize() 后迁移节点的代码     if (oldTab != null) {         for (int j = 0; j < oldCap; ++j) {             Node<K,V> e;             if ((e = oldTab[j]) != null) {                 oldTab[j] = null;                 //如果当前位置只有一个节点,则直接迁到新的数组,然后重新计算index                 if (e.next == null)                     newTab[e.hash & (newCap - 1)] = e;                 //如果节点是红黑树,则转到split方法,split方法对于节点新数组中的位置计算与下面一样                 else if (e instanceof TreeNode)                     ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);                 else { // 重点部分, 看如何计算节点在新的数组中的下标                     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);                      // e.hash 相较于 oldCap 高位为0,则沿用旧的下标值                     if (loTail != null) {                         loTail.next = null;                         newTab[j] = loHead;                     }                     // 高位为 1, 则新的下标值等于重新计算后加上旧的容量                     if (hiTail != null) {                         hiTail.next = null;                         newTab[j + oldCap] = hiHead;                     }                 }             }         }     }     return newTab; } 

如何计算节点在新的hash表中的位置:

e.hash & oldCap 判断用来计算hash值的高位是否为1。 这个1决定着节点在新的数组中的位置,这是因为数组扩容即 oldCap << 1 右移了一位 举个栗子,假设原数组容量为 4 (只要是2的幂就行),即: 100 hash值为 5,(101) 的节点在原数组的下标为:  5 & (4-1)  =   101   & 011  =   001; 数组扩容后,容量为8 (1000), 节点在新数组下标为 :  5&(8-1)  =  101  & 111 =  101; 新的下标正好等于 旧的下标 001 加上 旧容量 4(100) ,即 101.  上面是hash & oldCapacity = 1 的情况 当 hash & oldCapacity = 0 时自然容易算出新的下标和旧的下标相等。

Resize() 小结
HashMap 在扩容时:
1. 检查数组容量是否已经达到最大允许容量值, 如果达到了就把阈值也调整为最大int值,不再进行扩容。
2. 如果没有达到最大容量值,则进行两倍扩容,同时更新阈值。
3. 如果扩容之后达到的最大容量值,则把数组长度和阈值设置为最大值。
4. 数组的最大容量为 1 << 30.

上面的扩容后计算新的下标值设计确实非常的巧妙,既省去了重新计算hash值的时间,而且同时,由于新增的1bit是0还是1可以认为是随机的,因此resize的过程,均匀的把之前的冲突的节点分散到新的bucket了。这一块就是JDK1.8新增的优化点。有一点注意区别,JDK1.7中rehash的时候,旧链表迁移新链表的时候,如果在新表的数组索引位置相同,则链表元素会倒置,但是从上面可以看出,JDK1.8不会倒置.
http://www.importnew.com/20386.html

//get操作 public V get(Object key) {     Node<K,V> e;     //先计算key的hashCode, 然后调用 getNode()方法     return (e = getNode(hash(key), key)) == null ? null : e.value; }  final Node<K,V> getNode(int hash, Object key) {     Node<K,V>[] tab; Node<K,V> first, e; int n; K k;      /*      * 这里先进行了一系列判断      * 1. 判断hash表是否为null 或 空       * 2. 判断 key 有没有在hash表中      */     if ((tab = table) != null && (n = tab.length) > 0 &&         (first = tab[(n - 1) & hash]) != null) {          //如果查找到的第一个节点就等于key 则直接返回         if (first.hash == hash && // always check first node             ((k = first.key) == key || (key != null && key.equals(k))))             return first;         //如果第一个节点不等于 key 则表明出现hash冲突         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; } 

通过上面的分析我们知道HashMap的基本数据结构是数组、链表和红黑树,初始时处理hash冲突使用链表,当链表规模达到8时,转化成红黑树。同时每当数组元素达到阈值会进行扩容,扩容后的数组是原来的两倍。因此在插入请求较多时,最好指定初始容量以获得更好地性能。

这里的内容来源于另一个博客,地址忘了。。。

HashMap在使用指定容量初始化时使用了一个很精妙的算法来保证初始化后的数组容量大于等于指定容量的2的幂中的最小数。
举个栗子:假如传给HashMap初始化的容量cap = 13, 那么HashMap实际的初始容量为
16 = 2^4 > 13
这样既能保证容量够用,又方便以后计算元素位置 (length - 1) & hash, 相当于对2的幂取余操作。
方法如下:

static final int tableSizeFor(int cap) {     int n = cap - 1;     n |= n >>> 1;     n |= n >>> 2;     n |= n >>> 4;     n |= n >>> 8;     n |= n >>> 16;     return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; }

举例:

cap = 9  16 cap - 1  第一步结果8 00000000000000000000000000001000    8 00000000000000000000000000000100移1 00000000000000000000000000001100    或运算 结果 00000000000000000000000000000011移2 00000000000000000000000000001111    或运算 结果  00000000000000000000000000001111    右移 4 8 16没用全是0结果还是这个15 最终 +1   16  分析下大点的数 12345678 00000000101111000110000101001110  12345678 00000000101111000110000101001101  -1结果   12345677 00000000010111100011000010100110移1 00000000111111100111000111101111  或运算 结果 00000000001111111001110001111011移2 00000000111111111111110111111111  差不多了在移0就没了都是1,+1不是肯定是2的倍数了

至于为什么最终只右移到16位,我猜想是因为最大的容量 MAXIMUM_CAPACITY = 1 <<< 30
16+8+4+2+1 = 31 > 30 ,所以右移31位已经可以保证移位后的n的低位全是1,高位全是0了。

最后这里有篇文章介绍 HashMap 在多线程环境下的不安全情况:
http://www.importnew.com/22011.html

参考:
http://www.importnew.com/20386.html
http://www.importnew.com/22011.html
http://www.cnblogs.com/skywang12345/p/3310835.html

文章来源: java之HashMap详解
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!