HashMap

学习链接:HashMap全B站最细致源码分析课程,看完月薪最少涨5k!_哔哩哔哩_bilibili

github源码链接:LiLi8080/map (github.com)

一、什么是Hash

  1.1 基本概念:

  Hash:        也称为散列、哈希,对应英文都是Hash。

  基本原理:  把任意长度的输入通过Hash算法变成固定长度的输出

  Hash算法:上述映射规则就是Hash算法

  Hash值:     原始数据映射后的二进制串就是Hash值

1.2 Hash特点

    1. 从hash值不可以反向推到出原始数据

    2. 输入数据的微小变化会得到完全不同的hash值,相同数据会得到相同的值

    3.哈希算法的执行效率要效,长的文本也能快速计算出hash值

    4. hash算法的冲突概率要小

   

二、HashMap原理

  

 

 

                 HashMap继承体系

 

 

 

             HashMap底层数据结构

  转换成红黑树的条件: 链表长度>8 即链表程度>=9并且Node数组中的总元素个数>=64时  该链表讲转换成红黑树

  2.1 put数据原理分析

      

 

 

       步骤详细分析:

      1.  map.put("暴躁”,“小刘")  // HashMap put数据
      2.    put方法执行流程
        1. 获取”暴躁“字符串的hash值
        2. 扰动函数扰动hash值,得到更均匀散列的hash值(详细方法后续讲解,如果不扰动,hash碰撞概率会增加)
        3. 构造Node对象 ( 用要插入的数据初始化Node对象)
          1.   Node对象
            static class Node<K,V> implements java.util.Map.Entry<K,V> {
            final int hash;
            final K key;
            V value;
            java.util.HashMap.Node<K,V> next;

            Node(int hash, K key, V value, java.util.HashMap.Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
            }
            ...
            }
        4. 路由算法,找到Node存放的数组位置
          1.   路由算法原理:  (table.length-1)&node.hash  = (16-1) & 1122 =0000 0000 1111 & 0100 0110 0010 = 2
          2.        table.length:  Node数组的长度(Node数组的长度一定为2的^x 因为2^x数据-1后二进制的最末尾的x个数一定为1 再与node.hash相与 范围一定在0-table.length-1之间,相当于对table.length取余)

  2.2  Hash碰撞

      多个Node经过hash映射函数映射到Node数组的同一个Node链表中

  2.3 什么是链化

       将那些发生Hash碰撞的Node元素链接到一个Node链表中

  2.4 JDK为什么引入红黑树

      因为Node链表元素过多 查询效率低下,引入红黑树后查询效率变为Logn  (虽然会增加插入时间 但查询效率大大提高)

  2.5 HashMap扩容原理  

      (59条消息) JDK 1.8 HashMap扩容原理_u013171997的专栏-CSDN博客      

三、源码

  3.1 核心属性分析

  // 缺省时table的大小 即缺省时Node数组的长度
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
    // table的最大长度  即hash表中Node数组的最大长度
    static final int MAXIMUM_CAPACITY = 1 << 30;
    // 缺省时负载因子的大小 即没有调用构造函数给负载因子赋值
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    // 树化域值 即当Node数组中某个链表中元素数量大于8时可能进行树化
    static final int TREEIFY_THRESHOLD = 8;
    // 树降级域值 即Node数组中某个链表中元素个数小于6时将对树进行链表化
    static final int UNTREEIFY_THRESHOLD = 6;
    // 树化的另一个域值 当hash表中所有元素个数大于64才可能对某个大于8个元素的链表树化
    static final int MIN_TREEIFY_CAPACITY = 64;


  /* Field */
    // hash表
    transient java.util.HashMap.Node<K,V>[] table;
    transient Set<Entry<K,V>> entrySet;
    // 当前hash表中元素个数
    transient int size;
    // 当前hash表结构修改次数  往hash表中增加/删除某个元素代表结构修改 但替换某个元素不算
    transient int modCount;
    // 扩容域指,当hash表中元素个数超过域值时触发扩容。
    int threshold;
    // 负载因子 threshold = loadFactor*capacity  capacity是hash表中Node数组长度
    final float loadFactor;

 

  3.2 构造方法分析

   

 /*  构造方法源码分析   */
    public HashMap(int initialCapacity, float loadFactor) {
        // 就是做了一些校验
        // capacity必须>0且<=MAXIMUM_CAPACITY 当输入capacity小于时抛出异常
        // 大于MAXIMUM_CAPACITY时直接初始化为MAXIMUM_CAPACITY
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                    initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        // loadFactor必须>0的数 否则抛异常
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                    loadFactor);

        this.loadFactor = loadFactor;
        // hash表中Node数组的长度必须是2^x,但传入的initialCapacity不一定是2^x 因此需要tableSizeFor去处理
        this.threshold = tableSizeFor(initialCapacity);
    }
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }
    public HashMap(java.util.Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }
    /*
     * 作用:返回一个>=当前cap的一个数字 ,并且这个数字一定是一个2的次方数
     *
     * 示例:
     *      cap = 10
     *      n = 10-1 = 9
     *      0b1001 | 0b0100 => 0b1101
     *      0b1101 | 0b0011 => 0b1111
     *      0b1111 | 0b0000 => 0b1111
     *      0b1111 | 0b0000 => 0b1111
     *      0b1111 | 0b0000 => 0b1111 = 15
     *
     *      return n+1 = 15+1 = 16 = 2^4
     *
     * 实际意义:
     *      将某个cap = 0001 1101 1100 ==> 0001 1111 1111 +1 = 0010 0000 0000
     *      return 0010 0000 0000  这个数一定是>= cap的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;
    }

  3.3 put源码分析

    /*  put源码分析 */
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
    /*
     * 作用: 让key的hash值的高16位也参与运算 即hash扰动函数
     *       当key为null时,hash(key) =0 ,路由算法查询在哈希表Node数组位置也为0,因此key=null的数据将插在hash表Node[0]的链表中
     * 如果不用扰乱函数 ,每次路由只有末尾n位参与有效运算
     */
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
    /*
     * onlyIfAbsent : =true时,如果散列表中有此key的数据,则不进行插入操作
     *                =false时,散列表中有此数据则进行替换
     */
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        // tab:引用当前hashMap的散列表
        // p:表示当前散列表的元素
        // n:表示散列表数组的长度
        // i:表示路由寻址结果
        java.util.HashMap.Node<K,V>[] tab;
        java.util.HashMap.Node<K,V> p;
        int n, i;

        // 延迟初始化,第一次putVal才会初始化hashMap对象中最耗费内存的散列表
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;

        // i = (n - 1) & hash  路由算法 i=路由找到的hash列表Node数组的下标
        // 即要将当前元素p插入到Node[i]中
        // 情况一:Node[i] == null 直接插入
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);

            // 情况二: Node[i]!=null
        else {
            // e: 不为null表示找到了一个与当前要插入的key-value一致的key的元素
            // k: 表示临时的一个key
            java.util.HashMap.Node<K,V> e; K k;

            // 表示桶位中的该元素与你当前插入的元素的key完全一致,表示后续需要进行替换操作
            if (p.hash == hash &&
                    ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;

                // 是红黑树时....
            else if (p instanceof java.util.HashMap.TreeNode)
                e = ((java.util.HashMap.TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);

                // 是链表时xxx
            else {
                // 链表的头元素与我们要插入的元素的key不一致
                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 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }

                    // 条件成立:说明找到与要插入元素相同的key的node,需要进行替换操作
                    if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k))))
                        break;

                    p = e;
                }
            }

            // e!=null说明找到了相同key的node 需要进行替换
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;  //  替换完后直接返回
            }
        }

        // 表示元素进行的是插入而不是替换操作  即替换操作modCount不+1
        ++modCount;
        // 插入新元素size++,如果自增后元素值大于threshold则进行扩容操作
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

 

  3.4 扩容分析

 

规律: 扩容后的哈希表中相同桶中的元素一定也在原始未扩容的相同桶中

    但再原始未扩容的相同桶中的数据扩容后可能不在新table的相同桶中

 

 /* resize源码分析 */
    /*
     * 作用:扩容操作。为了解决h哈希冲突d导致的链化y影响查询效率的问题,扩容后会缓解该问题
     */
    final java.util.HashMap.Node<K,V>[] resize() {
        // 引用扩容前的hash表
        java.util.HashMap.Node<K,V>[] oldTab = table;
        // 表示扩容之前table数组的长度
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        // oldThr:表示扩容之前的扩容域指,触发本次扩容的域指
        int oldThr = threshold;
        // newCap: 扩容之后table数组的大小
        // newThr:扩容之后下次再次触发k扩容的条件
        int newCap, newThr = 0;

        // 计算newCap 和 newThr
        // 条件成立:说明hashMap中的散列表已经初始化过了,这是一次正常扩容
        if (oldCap > 0) {
            // 扩容之前的table数组大小已经达到了最大值,不扩容
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            // oldCap左移一位s实现数值翻倍,并赋值给newCap newCapx小于数组最大值限制q且扩容之前的域值>=16
            // 这种情况下 下次扩容的域指等于当前阈值翻倍
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                    oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }

        // oldCap == 0 说明hashmap的散列表是null
        // 1. new HahMap(initCap,loadFactor)
        // 2. new HashMap(initCap)
        // 3. new HashMap(map) 会更改oldThr的值
        // 4. new HashMap() 不会更改oldThr的
        // 第一次初始化 置newCap = oldThr 因为oldThr是通过tableSizeFor(cap)计算所得 一定为2的N次方
        else if (oldThr > 0)
            newCap = oldThr;

        // oldCap == 0 && oldThr == 0
        // new HashMap() 构造对象 只初始化loadFactor
        else {
            newCap = DEFAULT_INITIAL_CAPACITY;  // 16
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);  // 12
        }

        // newThr为零时,通过newCaph和loadFactor计算出一个newThr
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                    (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;



        // 根据newCap创建新的更大的数组
        @SuppressWarnings({"rawtypes","unchecked"})
        java.util.HashMap.Node<K,V>[] newTab = (java.util.HashMap.Node<K,V>[])new java.util.HashMap.Node[newCap];
        table = newTab;

        // 说明扩容前table中可能有数据
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                // 当前node节点
                java.util.HashMap.Node<K,V> e;
                // 说明当前桶中有数据,但数据具体是单个数据 / 链表/红黑树不确定
                if ((e = oldTab[j]) != null) {
                    // 方便JVM GC回收内存
                    oldTab[j] = null;

                    //  第一种情况:当前桶中只有一个元素,从未发生过碰撞,则直接计算出当前元素应该存放再新table中的位置
                    //  然后扔进去
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;

                    // 第二种情况 当前节点已经树化 ...
                    else if (e instanceof java.util.HashMap.TreeNode)
                        ((java.util.HashMap.TreeNode<K,V>)e).split(this, newTab, j, oldCap);

                    // 第三种情况 已经形成链表
                    else {
                        // 低位链表:存放扩容之后的数组的下标位置,与当前s数组的下标位置相同
                        java.util.HashMap.Node<K,V> loHead = null, loTail = null;
                        // 高位链表:存放扩容之后的数组的下标位置为当前数组的下标位置+扩容之前数组的长度
                        java.util.HashMap.Node<K,V> hiHead = null, hiTail = null;
                        java.util.HashMap.Node<K,V> next;
                        do {
                            next = e.next;
                            // oldIndex = e.hash & (oldCap-1) 当e.hash & oldCap == 0 证明e.hash在oldCap的最高位为0 转换成新数组的下标将无变化
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            // oldIndex = e.hash & (oldCap-1) 当e.hash & oldCap == 1 证明e.hash在oldCap的最高位为1 转换成新数组的下标将增加oldCap
                            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;
    }

  3.5 get源码分析

 /* get源码分析 */
    public V get(Object key) {
        java.util.HashMap.Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }
    final java.util.HashMap.Node<K,V> getNode(int hash, Object key) {
        // tab: 当前散列表
        // first:桶中首元素
        // e: 临时存放node 的元素
        // n : 当前散列表数组长度
        java.util.HashMap.Node<K,V>[] tab; java.util.HashMap.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 &&
                    ((k = first.key) == key || (key != null && key.equals(k))))
                return first;

            // 桶中元素不只一个
            if ((e = first.next) != null) {
                // 桶中元素已形成树结构 查找树
                if (first instanceof java.util.HashMap.TreeNode)
                    return ((java.util.HashMap.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;
    }

  3.6 remove源码分析

 /* remove 源码分析 */
    public V remove(Object key) {
        java.util.HashMap.Node<K,V> e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
                null : e.value;
    }
    @Override
    public boolean remove(Object key, Object value) {
        return removeNode(hash(key), key, value, true, true) != null;
    }
    final java.util.HashMap.Node<K,V> removeNode(int hash, Object key, Object value,
                                                 boolean matchValue, boolean movable) {
        // tab: 当前散列表
        // p: 当前node元素
        // n: 散列表数组长度
        // index:寻址结果 即寻址到的桶位
        java.util.HashMap.Node<K,V>[] tab; java.util.HashMap.Node<K,V> p; int n, index;

        // 条件为真: 哈希表不为空 且查找到的桶位有数据
        if ((tab = table) != null && (n = tab.length) > 0 &&
                (p = tab[index = (n - 1) & hash]) != null) {

            java.util.HashMap.Node<K,V> node = null, e; K k; V v;
            // 第一种情况: 当前桶的首元素即为要删除的元素 用node标记
            if (p.hash == hash &&
                    ((k = p.key) == key || (key != null && key.equals(k))))
                node = p;

            // 第二种情况 : 当前桶首元素不是要删除的元素,桶中不只一个元素
            else if ((e = p.next) != null) {
                // 当前桶为红黑树 利用红黑树方法获取删除node
                if (p instanceof java.util.HashMap.TreeNode)
                    node = ((java.util.HashMap.TreeNode<K,V>)p).getTreeNode(hash, key);

                // 档期桶为链表 遍历链表查找要删除的元素并用node标记
                else {
                    do {
                        if (e.hash == hash &&
                                ((k = e.key) == key ||
                                        (key != null && key.equals(k)))) {
                            node = e;
                            break;
                        }
                        p = e;
                    } while ((e = e.next) != null);
                }
            }

            // node标记不为空 即查找到了要删除的元素
            if (node != null && (!matchValue || (v = node.value) == value ||
                    (value != null && value.equals(v)))) {
                // 要删除的元素在红黑树中 利用红黑树方法删除
                if (node instanceof java.util.HashMap.TreeNode)
                    ((java.util.HashMap.TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
                // 要删除的元素是桶中链表的首元素
                else if (node == p)
                    tab[index] = node.next;
                // 要删除的元素是桶中链表的中间元素
                else
                    p.next = node.next;
                ++modCount;  //删除操作也要更新modCount
                --size;
                afterNodeRemoval(node);
                return node; // 返回删除的元素
            }
        }
        return null;
    }

3.7 replace源码分析

 /* replace源码分析 */
    @Override
    public boolean replace(K key, V oldValue, V newValue) {
        java.util.HashMap.Node<K,V> e; V v;
        if ((e = getNode(hash(key), key)) != null &&
                ((v = e.value) == oldValue || (v != null && v.equals(oldValue)))) {
            e.value = newValue;
            afterNodeAccess(e);
            return true;
        }
        return false;
    }
    @Override
    public V replace(K key, V value) {
        java.util.HashMap.Node<K,V> e;
        if ((e = getNode(hash(key), key)) != null) {
            V oldValue = e.value;
            e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
        return null;
    }

 

posted @ 2021-10-27 22:11  微~粒  阅读(143)  评论(0)    收藏  举报