zoukankan      html  css  js  c++  java
  • HashMap源码分析——基于jdk1.8

    前言:上篇文章,笔者分析了jdk1.7中HashMap的源码,这里将对jdk1.8的HashMap的源码进行分析。

    注:本文jdk源码版本为jdk1.8.0_172


    1.再看put操作

    1  public V put(K key, V value) {
    2         return putVal(hash(key), key, value, false, true);
    3     }

    jdk1.8中的hash算法:

    1  static final int hash(Object key) {
    2         int h;
    3         // 这里的hash算法和jdk1.7中很不一样,直接高16位与低16位做异或,这样的话高低位都进行了运算,增加hash值的随机性
    4         return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    5     }

    再看put操作的核心函数:

     1 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
     2                    boolean evict) {
     3         // jdk1.8中HashMap底层数据结构使用的是Node
     4         Node<K,V>[] tab; Node<K,V> p; int n, i;
     5         // 如果table还未初始化,则初始化table,注意这里初始化使用的是resize函数[扩容函数]
     6         if ((tab = table) == null || (n = tab.length) == 0)
     7             n = (tab = resize()).length;
     8         /**
     9          * 这里表示如果tab[i]位置上为null,则直接插入数据
    10          * i=(n-1)&hash与jdk1.7中找出元素在tab上的index是一样的操作
    11          * 注意这里在多线程环境下会造成线程不安全问题
    12          */
    13         if ((p = tab[i = (n - 1) & hash]) == null)
    14             tab[i] = newNode(hash, key, value, null);
    15         else {// 如果i位置上有元素,则进行链式存储
    16             Node<K,V> e; K k;
    17             // 如果tab[i]上的元素与插入元素的key完全一样,则进行覆盖操作
    18             if (p.hash == hash &&
    19                     ((k = p.key) == key || (key != null && key.equals(k))))
    20                 e = p;
    21             // 判断当前元素是否是红黑树结构
    22             else if (p instanceof TreeNode)
    23                 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
    24             else {
    25                 for (int binCount = 0; ; ++binCount) {
    26                     // 如果p节点的next为空,则将待插入的元素,直接添加在链表尾
    27                     if ((e = p.next) == null) {
    28                         // 从这里可知道jdk1.8在,如果存在链表,插入数据是直接放在链表尾的
    29                         p.next = newNode(hash, key, value, null);
    30                         // 当同一节点链表中元素个数>=8时,底层数据结构转向红黑树,
    31                         if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
    32                             treeifyBin(tab, hash);  // 将底层数据结构转向红黑树
    33                         break;
    34                     }
    35                     // 判断next元素是否和插入元素相同,如果相同,则不做操作,跳出循环
    36                     if (e.hash == hash &&
    37                             ((k = e.key) == key || (key != null && key.equals(k))))
    38                         break;
    39                     p = e; // 将next赋值给p,继续循环
    40                 }
    41             }
    42             // 覆盖操作
    43             if (e != null) { // existing mapping for key
    44                 V oldValue = e.value;
    45                 // onlyIfAbsent表示是否要改变原来的值,true-不改变,false-改变
    46                 if (!onlyIfAbsent || oldValue == null)
    47                     e.value = value;
    48                 afterNodeAccess(e);
    49                 return oldValue;
    50             }
    51         }
    52         // 修改次数加1,fail-fast机制
    53         ++modCount;
    54         // 判断是否需要扩容
    55         if (++size > threshold)
    56             resize();
    57         afterNodeInsertion(evict);
    58         return null;
    59     }

    重点:

    jdk1.8中HashMap在进行put操作时:

    #1.如果同一hash值的节点数小于8时,底层数据结构仍然是链表;当节点数大于等于8时,会转向红黑树。

    #2.如果节点的数据结构是链表时,插入数据是直接放在链表尾的,从而避免插入元素时,形成环形链,造成死循环。

    put操作的核心代码中,还涉及一个比较重要的函数,这里进行详细分析。

    #resize()

      1 final Node<K,V>[] resize() {
      2         Node<K,V>[] oldTab = table;
      3         int oldCap = (oldTab == null) ? 0 : oldTab.length;
      4         int oldThr = threshold;
      5         int newCap, newThr = 0;
      6         // 如果table中有元素
      7         if (oldCap > 0) {
      8             // 容量是否已达限制
      9             if (oldCap >= MAXIMUM_CAPACITY) {
     10                 threshold = Integer.MAX_VALUE;
     11                 return oldTab;
     12             }
     13             // 扩容,并更新扩容阈值
     14             else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
     15                     oldCap >= DEFAULT_INITIAL_CAPACITY)
     16                 newThr = oldThr << 1; // double threshold
     17         }
     18         // 如果table中没有元素,但是已初始化扩容阈值,这里将table的新容量赋值为扩容阈值
     19         else if (oldThr > 0) // initial capacity was placed in threshold
     20             newCap = oldThr;
     21             // 如果以上条件都不满足,则利用默认值进行初始化
     22         else {               // zero initial threshold signifies using defaults
     23             newCap = DEFAULT_INITIAL_CAPACITY;
     24             newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
     25         }
     26         // 这里再次对扩容阈值进行判断,如果未初始化,则进行初始化
     27         if (newThr == 0) {
     28             float ft = (float)newCap * loadFactor;
     29             newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
     30                     (int)ft : Integer.MAX_VALUE);
     31         }
     32         threshold = newThr;
     33         @SuppressWarnings({"rawtypes","unchecked"})
     34         Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
     35         table = newTab;
     36         // 如果原来table有值,则循环将原值转移到newTab中
     37         if (oldTab != null) {
     38             for (int j = 0; j < oldCap; ++j) {
     39                 Node<K,V> e;
     40                 // 找到有值的节点
     41                 if ((e = oldTab[j]) != null) {
     42                     oldTab[j] = null; //将原来table中当前位置置null
     43                     if (e.next == null) // 如果当前节点next为null,将其放置在newTab中的新位置
     44                         newTab[e.hash & (newCap - 1)] = e;
     45                         // 如果是红黑树则进行红黑树操作,关于红黑树后面会进行分析
     46                     else if (e instanceof TreeNode)
     47                         ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
     48                     else { // preserve order  // 当走到这里,说明节点上为链表形式存储数据,需进行循环操作
     49                         // 存储位置在newTable和oldTable位置不变元素
     50                         Node<K,V> loHead = null, loTail = null;
     51                         // 存储oldTable中位置发生了变化的元素,当然这里是和oldTable相比较
     52                         // 参看下面的注释,应该可以很好理解
     53                         Node<K,V> hiHead = null, hiTail = null;
     54                         Node<K,V> next;
     55                         do {
     56                             // 由于是链表循环,因此需存储next节点的值,这种形式在jdk1.7中出现过多次
     57                             next = e.next;
     58                             /**
     59                              *这里需要注意一下,这里是用元素的hash值,与原来table长度做&操作
     60                              * 如果为0,则表示e.hash&(newCap-1)和e.hash&(oldCap-1)是一样的
     61                              * 也就说元素的位置在newTable中是不变的,因为newTable的大小为oldTable大小的2倍
     62                              * 相当于其二进制向左移动了1位,其newCap-1的二进制全都为1,且比原来oldCap-1的二进制多了一个1
     63                              * eg:oldCap=16,newCap=32,注意求key的位置是用e.hash&(table.length-1)
     64                              * e.hash&0x1111=原来key的位置
     65                              * e.hash&0x10000=0,表明e.hash在二进制的第5位上一定为0,所以:
     66                              * e.hash&0x11111=也一定是原来key的位置
     67                              * 如果:
     68                              * e.hash&0x10000=1,表明e.hash在二进制的第5位上一定为1,所以:
     69                              * e.hash&0x11111=原来key的位置加上oldCap的长度即可(0x10000)
     70                              * 这样根据一个二进制位就将原来的一条链表分成两条链表进行存储,这里非常的关键,不是很好理解
     71                              * 仔细理解上面的解释,相信你会发现这是非常神奇的一个技巧
     72                              */
     73                             // 有了上面的原理,再来看这就非常明确了
     74                             // 元素在newTable中位置不改变
     75                             if ((e.hash & oldCap) == 0) {
     76                                 // 初始时,将e放在loHead头上,然后尾又是e,后续循环的时候,只操作tail就行了,形成链表
     77                                 if (loTail == null)
     78                                     loHead = e;
     79                                 else
     80                                     loTail.next = e;
     81                                 // 尾部存储为e,形成链表,注意理解就好
     82                                 loTail = e;
     83                             }
     84                             // 元素在newTable中位置发生了变化[相对oldTable]
     85                             // 这里就相当于两条链表了,位置不变的一条,位置变了的又是一条
     86                             else {
     87                                 if (hiTail == null)
     88                                     hiHead = e;
     89                                 else
     90                                     hiTail.next = e;
     91                                 hiTail = e;
     92                             }
     93                         } while ((e = next) != null);
     94                         // 如果位置不变链表不为null
     95                         if (loTail != null) {
     96                             loTail.next = null;
     97                             // 从这里也可看出这里存储的是元素在newTable中位置不改变[相对oldTable]
     98                             // 只需要存储head值即可,因为已形成链表
     99                             newTab[j] = loHead;
    100                         }
    101                         if (hiTail != null) {
    102                             hiTail.next = null;
    103                             // 位置变化的元素,位置只需要加上oldCap的值就可以了,上面已进行分析
    104                             newTab[j + oldCap] = hiHead;
    105                         }
    106                     }
    107                 }
    108             }
    109         }
    110         return newTab;
    111     }

    重点:

    resize函数中在对节点元素存在链表时的处理有点小技巧,虽然是再次hash,但它是根据key在oldTable中位置与newTable的位置来进行区分的,变成两条链表存储,具体分析过程在函数中已注释写得非常详细。

    jdk1.8中HashMap主要在底层增加了红黑树的数据结构,关于红黑树这种数据结构笔者还是有点懵懂,后面会专门深入这方面的知识点。

    2.再看get操作

    1 public V get(Object key) {
    2         // 这里与jdk1.7的get方法有很大的不一样
    3         Node<K,V> e;
    4         // 通过getNode方法,如果e不为null,则返回e的value,否则返回null
    5         return (e = getNode(hash(key), key)) == null ? null : e.value;
    6     }

    get关键函数getNode:

     1 final Node<K,V> getNode(int hash, Object key) {
     2         Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
     3         // 如果HashMap中有值,其当前元素在table中有值,则进行寻找
     4         if ((tab = table) != null && (n = tab.length) > 0 &&
     5             (first = tab[(n - 1) & hash]) != null) {
     6             // 注释已经说的非常清楚了,检查第一个节点的值是否与要查找的相同,快速return
     7             if (first.hash == hash && // always check first node
     8                 ((k = first.key) == key || (key != null && key.equals(k))))
     9                 return first;
    10             // 如果当前节点下存在红黑树或链表结构,则进行循环操作    
    11             if ((e = first.next) != null) {
    12                 // 如果next节点为红黑树则在红黑树中查找元素
    13                 if (first instanceof TreeNode)
    14                     return ((TreeNode<K,V>)first).getTreeNode(hash, key);
    15                 // 走到该分支,则表明当前节点下为链表结构,下面的循环就比较简单了,通过循环不断的查找相同的元素,有则返回,无则返回null
    16                 do {
    17                     if (e.hash == hash &&
    18                         ((k = e.key) == key || (key != null && key.equals(k))))
    19                         return e;
    20                 } while ((e = e.next) != null);
    21             }
    22         }
    23         // 否则直接返回null
    24         return null;
    25     }

    分析:

    #1.get函数的核心代码逻辑还是非常简单的,注意:总是首先校验头结点,快速return

    #2.其次是红黑树中查找元素,由于红黑树的数据结构相对较复杂,后续在进行相应分析。

    3.其他重要函数

    #removeNode该函数为remove函数的核心函数:

     1 /**
     2      * HashMap删除元素
     3      * @param hash key的hash值
     4      * @param key  传入的删除的key值
     5      * @param value 值,传入为nul
     6      * @param matchValue if true only remove if value is equal 如果为true,则只移除value相等的元素,所以这里传入false
     7      * @param movable if false do not move other nodes while removing 该值主要用于红黑树移除节点后,再重塑红黑树,所以传入true
     8      * @return
     9      */
    10     final Node<K,V> removeNode(int hash, Object key, Object value,
    11                                boolean matchValue, boolean movable) {
    12         Node<K,V>[] tab; Node<K,V> p; int n, index;
    13         // 同样先判断是否存在该元素,并记录头结点元素p
    14         if ((tab = table) != null && (n = tab.length) > 0 &&
    15                 (p = tab[index = (n - 1) & hash]) != null) {
    16             Node<K,V> node = null, e; K k; V v;
    17             // 如果头结点直接命中,则用node存储下来
    18             if (p.hash == hash &&
    19                     ((k = p.key) == key || (key != null && key.equals(k))))
    20                 node = p;
    21             // 该分支表示头结点未命中,判断p结点的next是否不为null,因为要循环,所以需要将next元素记录下来,这种方式在HashMap的循环中很常用
    22             else if ((e = p.next) != null) {
    23                 // 如果p为红黑树结构,则走红黑树分支,找到要删除的结点
    24                 if (p instanceof TreeNode)
    25                     node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
    26                 // 否则进行循环查找
    27                 else {
    28                     do {
    29                         // 如果命中,则跳出循环
    30                         if (e.hash == hash &&
    31                                 ((k = e.key) == key ||
    32                                         (key != null && key.equals(k)))) {
    33                             node = e; // node结点保存命中的结点元素
    34                             break;
    35                         }
    36                         // 如果未命中,则用p记录next结点,为后续删除做准备,p表示要删除节点的前一个节点,因为这里有e=e.next操作,与jdk1.7相同。
    37                         p = e;
    38                     } while ((e = e.next) != null);
    39                 }
    40             }
    41             // 要删除结点不为null,&& 操作后:注意这里!matchValue,因为传入值为false,所以这里一直为true
    42             if (node != null && (!matchValue || (v = node.value) == value ||
    43                     (value != null && value.equals(v)))) {
    44                 // 如果要删除节点为红黑树,则走红黑树分支
    45                 if (node instanceof TreeNode)
    46                     ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
    47                 // 如果要删除节点与p相同,则说明是头结点,则直接将tab[index]位置指向node.next,这样就踢出了node元素,即删除
    48                 else if (node == p)
    49                     tab[index] = node.next;
    50                 else // 如果不相同,则直接将p.next指向node.next,同样跳过了node,也就删除了。
    51                     p.next = node.next;
    52                 ++modCount; //操作记录+1,fail-fast机制
    53                 --size;// 元素个数减1
    54                 afterNodeRemoval(node);
    55                 return node;
    56             }
    57         }
    58         // 上述都未找到,则直接返回null
    59         return null;
    60     }

    重点:

    删除函数与jdk1.7中原理一样,都是通过操作一个结点元素进行删除,只是如果结点为红黑树,需走红黑树分支。

    总结

    这里比较粗略的分析了jdk1.8中HashMap的源码,它与jdk1.7中源码的原理大致相同,这里总结其重点:

    #1.底层引入了红黑树数据结构,在添加元素时,如果table位置上的元素数量>=8时,则当前位置结点数据结构会转向红黑树;

    当table位置上元素数量<=6时,数据结构又会转换成链表(在resize中,红黑树分支)。

    #2.改变了hash算法,直接高16位与低16位做异或。

    #3.resize函数中,在做再次hash时,用两条链表分散存储节点,并且避免了jdk1.7中的死循环情况。

    #4.同样存在fail-fast机制。


    by Shawn Chen,2019.03.09,晚。

  • 相关阅读:
    Expression Web 3 安装问题
    按钮重置问题引发的思考 JQuery & Ajax
    将相同值的行内容进行合并操作Sql2005
    SmartDraw 2010 破解成功
    JQuery CheckBoxList
    JQuery this 和 $(this) 的区别
    Asp.net 随记 Part4 (7190 )
    项目发布错误的解决方法
    JSON In Code
    JQuery QA
  • 原文地址:https://www.cnblogs.com/developer_chan/p/10481558.html
Copyright © 2011-2022 走看看