• Java HashMap 源码研究


    Jdk 8 的hashmap ,内部使用Node 表示数组成员,Node 实现了Map.Entry接口。

    put() 过程:
     1 public class HashMap<K,V> extends AbstractMap<K,V>implements Map<K,V>, Cloneable, Serializable {
     2     // map 的元素就是保存在这个table数组变量之中,也就是“数组+链表” 之中的数组
     3     transient Node<K,V>[] table;
     4     
     5     public V put(K key, V value) {
     6         return putVal(hash(key), key, value, false, true); 
     7      }
     8     // hash 算法核心,减少碰撞
     9     static final int hash(Object key) {
    10         int h;
    11         /** 如果key是空,hash值返回0
    12             否则,用key的hashcode() h变量 和 h无符号右移16位【右移一半】后的值【h是int类型32位,右移使hash值的高位变成低             位,举例1010 右移两位变成0010】作异或运算。这么做的目的是为了是哈希值更加散列【因为求key的下标是tab[i = (n -             1) & hash],用数组长度减一的值和哈希值做与运算,大多数情况下,哈希值参与运算仅为低位部分值,
    13            举例:数组长度16,转换二进制:1111,哈希值为78897121,转换二进制:100101100111101111111100001与1111 运算如               下,结果是0000 0000 0000 0000 0000 0000 0000 0001,可见决定结果的是低4位,所以为了让高位也能参与到运算,减少            冲突。最后哈希值高位没变动,低位值被重新计算。】
    14         */
    15         return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    16     }
    17     // 散列码(哈希值)的获取,依赖本地方法,通常是将对象的内存地址转换为整数。
    18     public native int hashCode();
    19     // onlyIfAbsent:true 存在则不更新。evict:供LinkedHashMap 回调方法afterNodeInsertion()使用
    20     final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
    21                    boolean evict) {
    22         // tab 临时变量,p是桶位上的首个node,n添加元素后的数组长度,i是目标位置下标
    23         Node<K,V>[] tab; Node<K,V> p; int n, i;
    24         if ((tab = table) == null || (n = tab.length) == 0)
    25             n = (tab = resize()).length;
    26         // 如果之前桶位上没有值,那么构造一个node 放上去
    27         if ((p = tab[i = (n - 1) & hash]) == null)
    28             tab[i] = newNode(hash, key, value, null);
    29         else {
    30             // 到这里说明桶位上有值,要么添加自己到链表/红黑树末尾,要么找到和自己key一致的node,替换node的值
    31             // e 是存在的那个节点,k 是存在的节点的key
    32             Node<K,V> e; K k;
    33             // 如果p-原来的node的哈希值和新值的hash值一致,且内容也一样。则p就是新值要替代对象,赋给e
    34             if (p.hash == hash &&
    35                 ((k = p.key) == key || (key != null && key.equals(k))))
    36                 e = p;
    37             // 如果p已经不是node结构【链表结构】,而是红黑树
    38             else if (p instanceof TreeNode) 
    39                 // 调用红黑树的putTreeVal(),返回要替代的目标节点
    40                 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
    41             else {
    42                 // 走到这里,说明目前还是数组+链表结构,且桶位上p并不是我们要替代的目标,所以开始遍历链表
    43                 for (int binCount = 0; ; ++binCount) {
    44                     // 如果p的下一个结点不存在,说明遍历链表完毕,那好,就构造一个新节点插入到链尾
    45                     if ((e = p.next) == null) {
    46                         p.next = newNode(hash, key, value, null);
    47                         // 如果目前桶位上链表数超过 树化阈值,那么就转为红黑树
    48                         if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
    49                             treeifyBin(tab, hash);
    50                         break;
    51                     }
    52                     // 到这里说明e不为空,判断当前结点e和新值的哈希值和key内容是否相同
    53                     // 如果成立,好这就是要更新替代的目标节点,跳出循环,不再遍历后续链表
    54                     if (e.hash == hash &&
    55                         ((k = e.key) == key || (key != null && key.equals(k))))
    56                         break;
    57                     p = e;
    58                 }
    59             }
    60             // 如果e不为空,就返回老值,保存新值。
    61             // 如果e为空,说明之前已经构造新值并插入链表末尾
    62             if (e != null) { // existing mapping for key
    63                 V oldValue = e.value;
    64                 // onlyIfAbsent 方法形参,true:不能修改存在的值。控制是否可以修改值的一个开关
    65                 // 如果允许修改或者老值为空,则赋新值
    66                 if (!onlyIfAbsent || oldValue == null)
    67                     e.value = value;
    68                 afterNodeAccess(e);// 回调方法,空实现,模板模式
    69                 return oldValue;
    70             }
    71         }
    72         // 修改计数器加一
    73         ++modCount;
    74         // 走到这一步,说明新元素已经被构造成一个Node,添加到了链尾,最后返回值为空,因为新元素是新加入,没有替代谁【本方法说的替代准确说是替代老节点的value的值,而不是老节点】。
    75         // 如果hashmap添加新元素后,实际使用长度超过扩容阈值,完蛋,扩容吧。
    76         if (++size > threshold)
    77             resize();
    78         afterNodeInsertion(evict);// 回调方法,空实现,模板模式
    79         return null;
    80     }
    81     // Callbacks to allow LinkedHashMap post-actions
    82     void afterNodeAccess(Node<K,V> p) { }
    83     void afterNodeInsertion(boolean evict) { }
    84     
    85     // 树化阈值,如果链表结点数大于等于8,就会转为红黑树
    86     static final int TREEIFY_THRESHOLD = 8;
    87     // 链化阈值,如果红黑树结点数小于等于6,就会转为链表
    88     static final int UNTREEIFY_THRESHOLD = 6;
    89 
    90 
    91 }
    扩容过程 resize(),是Java 8 HashMap更改最大的部分,Jdk7 是使用头插法,多线程情况下会导致循环引用
      1 // 扩容方法,面试重点
      2 final Node<K,V>[] resize() {
      3     Node<K,V>[] oldTab = table;
      4     int oldCap = (oldTab == null) ? 0 : oldTab.length;
      5     int oldThr = threshold;
      6     int newCap, newThr = 0;
      7     if (oldCap > 0) {
      8         // 如果之前的容量大于等于1<<30 即1073741824;那么新扩容阈值变为2147483647,这个值要比容量大,
      9         if (oldCap >= MAXIMUM_CAPACITY) {
     10             threshold = Integer.MAX_VALUE;
     11             return oldTab;
     12         }
     13         // 如果之前容量扩大一倍后赋给新容量 小于最大容量,且老容量大于等于默认初始化容量16,新扩容阈值也变大
     14         else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
     15                  oldCap >= DEFAULT_INITIAL_CAPACITY)
     16             newThr = oldThr << 1; // double threshold
     17     }
     18     // 如果oldCap <= 0,且oldThr > 0,那么初始容量设置为阈值
     19     else if (oldThr > 0) // initial capacity was placed in threshold
     20         newCap = oldThr;
     21     else {               // zero initial threshold signifies using defaults
     22         newCap = DEFAULT_INITIAL_CAPACITY;
     23         newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
     24     }
     25     // 如果新的扩容阈值等于0,重新计算扩容阅知
     26     if (newThr == 0) {
     27         float ft = (float)newCap * loadFactor;
     28         newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
     29                   (int)ft : Integer.MAX_VALUE);
     30     }
     31     threshold = newThr;
     32     // 以新的容量构造一个数组
     33     @SuppressWarnings({"rawtypes","unchecked"})
     34     Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
     35     // map的数组引用指向新数组【扩容后数组】
     36     table = newTab;
     37     if (oldTab != null) {
     38         // 遍历原数组
     39         for (int j = 0; j < oldCap; ++j) {
     40             Node<K,V> e;
     41             // 如果原数组桶位上 存在节点node,不为空
     42             if ((e = oldTab[j]) != null) {
     43                 // 桶位上的node已经赋值到e,清空桶位
     44                 oldTab[j] = null;
     45                 /** 如果e的下一个节点不存在,也就是说链表只有一个节点,直接将e这个放到新数组的桶位上,
     46                 桶位的下标计算公式不变,唯一区别是使用新数组的容量-1来与运算。newCap是oldCap 的2倍,
     47                 比如 oldCap为16,oldCap-1→1111,newCap为32,newCap-1→11111。此时e.hash值的第三位决定了,新桶                        位的下标,如果e.hash的第三位是0,则计算出的下标和之前一样,可以说位置不变。如果是1,新下标就会变                            成11111,可分解为 原下标1111【15】+10000【16】,原下标15,新下标31。总结就是下标要么在原址,
     48                 要么跑到原址+oldCap【原来容量】的地址
     49                 */
     50                 if (e.next == null)
     51                     newTab[e.hash & (newCap - 1)] = e;
     52                 else if (e instanceof TreeNode)
     53                     // 如果e 是红黑树节点,调用split()方法来处理
     54                     ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
     55                 else { // preserve order
     56                     // 到这里说明,桶位上是链表,且链表结点不为1.
     57                     // loHead、loTail 是扩容后在落在原址的结点组成的链表的链表头尾,称之为低位链
     58                     // hiHead、hiTail 是扩容后到落在原址+oldCap的地址组成的链表的链表头尾,称之为高位链。
     59                     Node<K,V> loHead = null, loTail = null;
     60                     Node<K,V> hiHead = null, hiTail = null;
     61                     // next 临时变量,代表下一个被处理的节点
     62                     Node<K,V> next;
     63                     // 开始遍历待分配位置链表
     64                     do {
     65                         // 当前结点e的下一个结点地址赋给next
     66                         // ? next变量 能否省略。我的感觉是可以,退出条件改为while((e=e.next) !=null)
     67                         next = e.next;
     68                         /* 判断当前结点的哈希值和扩容前容量做与运算是否等于0,目的是为了将链表上的节点均匀的分配到低位链                                高位链。它是如何做到的呢?oldCap二进制一定是100~这种形式,做与运算只需判断e.hash二进制值和                               前者1所对应的位置上的数是0还是1,是0当前结点e加入低位链。是1当前节点e加入高位链。
     69                          注意:一个链表上的不同结点的散列码是不同的,来自不同的内存地址。
     70                         */
     71                         if ((e.hash & oldCap) == 0) {
     72                             // 如果是首次向低位链添加,就把e赋值给loHead链表头
     73                             if (loTail == null)
     74                                 loHead = e;
     75                             else
     76                                 // 不是第一次向低位链添加结点,那么就赋值给loTail.next,链接到链表尾部【尾插法】
     77                                 loTail.next = e;
     78                             // 更新低位链后,loTail重新指向链尾。【就像一个箭头下移了一位】
     79                             loTail = e;
     80                         }
     81                         // 执行else,说明结点e应该被添加到高位链上。
     82                         else {
     83                             // 如果是首次向高位链添加,就把e赋值给hiHead链表头
     84                             if (hiTail == null)
     85                                 hiHead = e;
     86                             else
     87                                 // 不是第一次向高位链添加结点,那么就赋值给hiTail.next,链接到链表尾部【尾插法】
     88                                 hiTail.next = e;
     89                             // 更新高位链后,hiTail重新指向链尾。【就像一个箭头下移了一位】
     90                             hiTail = e;
     91                         }
     92                     // 遍历老链表,当next 为null时,退出循环    
     93                     } while ((e = next) != null);
     94                     // 遍历完成后,待分配节点已经各自组成了高位链、低位链
     95                     if (loTail != null) {
     96                         // 低位链链尾指向null,把链表放置到新数组老位置
     97                         loTail.next = null;
     98                         newTab[j] = loHead;
     99                     }
    100                     if (hiTail != null) {
    101                         // 高位链链尾指向null,把链表放置到新数组新位置,相对老位置偏移oldCap位。
    102                         hiTail.next = null;
    103                         newTab[j + oldCap] = hiHead;
    104                     }
    105                 }
    106             }
    107             // 每次循环搞定一个桶位
    108         }
    109     }
    110     // 最终返回扩容后数组
    111     return newTab;
    112 }

    get()方法解析

     1     public V get(Object key) {
     2         Node<K,V> e;
     3         return (e = getNode(hash(key), key)) == null ? null : e.value;
     4     }
     5 
     6  
     7     final Node<K,V> getNode(int hash, Object key) {
     8         // tab 数组,first 存放key的桶位上的链表的首节点,n数组长度,k返回值
     9         Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    10         // 如果tab数组不为空且长度大于0且根据哈希值与n-1做与运算得到的下标位置上有节点对象
    11         if ((tab = table) != null && (n = tab.length) > 0 &&
    12             (first = tab[(n - 1) & hash]) != null) {
    13             // 如果first 首结点的哈希值和key和形参一致,那么这个结点就是我们要找到目标,返回他
    14             if (first.hash == hash && // always check first node
    15                 ((k = first.key) == key || (key != null && key.equals(k))))
    16                 return first;
    17             // 如果首结点的下一节点不为空的话,继续排查
    18             if ((e = first.next) != null) {
    19                 // 如果红黑树结构,调用此方法。
    20                 if (first instanceof TreeNode)
    21                     return ((TreeNode<K,V>)first).getTreeNode(hash, key);
    22                 // 不是红黑树,遍历这个链表,挨个对比哈希值和key,找到就返回结点。
    23                 do {
    24                     if (e.hash == hash &&
    25                         ((k = e.key) == key || (key != null && key.equals(k))))
    26                         return e;
    27                 } while ((e = e.next) != null);
    28             }
    29         }
    30         // 执行到这里说明不存在这个key,value 结点。
    31         return null;
    32     }
     
    
    
  • 相关阅读:
    220. 存在重复元素 III
    785. 判断二分图
    欢天喜地七仙女——Beta冲刺汇总
    欢天喜地七仙女——Alpha冲刺汇总
    欢天喜地七仙女——测试随笔
    欢天喜地七仙女——beta总结
    欢天喜地七仙女——Beta冲刺十
    欢天喜地七仙女——用户调查报告
    欢天喜地七仙女——Beta冲刺九
    欢天喜地七仙女——Beta冲刺八
  • 原文地址:https://www.cnblogs.com/houj/p/14275177.html
Copyright © 2020-2023  润新知