JDK1.8 HashMap解析

本篇不介绍红黑树,后续会单独分析。

  • JDK 1.8
    也没看过jdk1.7 所以不做对比分析了

需准备知识

  1. 异或运算
  2. 位移
  3. 知道HashMap的数据结构有哪些
  4. 知道单链表大致是怎么一回事

能学到什么

  1. HashMap如何计算扩容阙值
  2. 哈希表需要树化的标准条件
  3. 哈希表什么时候初始化
  4. 为什么需要扩容
  5. HashMap如何增加hash的散列性
  6. HashMap如何确定桶位
  7. 链表如何删除元素

这些问题都会在源码分析的注释中有体现出来。

可能有些没分析到,欢迎留言,后续继续补充。


文章目录

  1. HashMap核心属性
  2. HashMap构造方法
  3. 哈希表元素:Node
  4. 如何计算扩容阙值: tableSizeFor方法
  5. 如何加hash的散列性:扰动函数hash()
  6. 如何路由寻址(计算出桶位):`桶位 = (table.length-1)& hash1
  7. HashMap核心方法: putresizegetremovereplace
  8. 本篇不介绍树,会额外分析

HashMap 核心属性

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {
  /**
     * table默认大小的阙值
     */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

   /**
     * map容量的最大阙值
     */
    static final int MAXIMUM_CAPACITY = 1 << 30;

    /**
     * 默认的负载因子
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    /**
     * 树化阙值
     */
    static final int TREEIFY_THRESHOLD = 8;

    /**
     * 树降级为链表的阙值
     */
    static final int UNTREEIFY_THRESHOLD = 6;

    /**
     * 哈希表的所有元素个数超过64,才允许树化
     * 树化的标准条件:1. 链表长度>= 8 
     *              2. 哈希表元素个数超过64个才允许树化
     */
    static final int MIN_TREEIFY_CAPACITY = 64;


    /**
     * 哈希表
     * 问题:什么时候初始化?
     */
    transient Node<K,V>[] table;
    /**
     *  哈希表元素个数
     */
    transient int size;
    /**
     * 当前哈希表结构修改次数。(替换不会变)
     */
    transient int modCount;

     /**
     * 扩容阙值,哈希表中的元素超过阙值,触发扩容
     */
    int threshold;
    /**
     * 负载因子,可设置
     * threshold =capacity * loadFactor
     */
    final float loadFactor;

}

HashMap 构造方法

/**
* 第一个参数:initialCapacity --> 使用者设置的哈希表的大小
* 第二个参数:loadFactor --> 设置的加载因子
*/
 public HashMap(int initialCapacity, float loadFactor) {
      // 哈希表的长度不可设置为负数,否则抛出异常
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
     // 设置的大于定义的默认的最大值,就设置为最大值
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;
      // 通过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(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }

哈希表元素:Node

  static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;  // key经过扰动函数hash计算得出的hash值
        final K key;  // 要存储的key
        V value; // 要存储的value
        Node<K,V> next; 

        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }

如何计算扩容阙值: tableSizeFor方法

   /**
     * Returns a power of two size for the given target capacity.
     * 返回当前>=cap的一个数字,并且是2的次方数
     *
     * 假如输入cap的是27
     * n = cap - 1 =26
     * 26的二进制是 11010 => 即n的二进制是11010
     * 运算规则:
     * n |= n >>> 1; 的意思是:n 按位"或运算" | n向右位移1位的结果,得出的结果,并同时赋值给n
     * 11010 |= 01101 => 11111  ( 01101 是n向右位移1位的结果,然后与n进行"或运算" 得出11111,并赋值给n。以下相同规则)
     * 11111 |= 00111 => 11111
     * 11111 |= 00001 => 11111
     * 11111 |= 00000 => 11111
     * ..... 后面的位移都一样了
     *
     * 最后n的二进制111111
     * 二进制111111 = 31
     *
     * 返回结果:
     * return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
     * 解释--> n=31 执行 (n >= MAXIMUM_CAPACITY)
     *        (n=31)<MAXIMUM_CAPACITY  执行  n + 1
     *        最后返回结果:32
     */
    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;
    }


如何增加hash的散列性:扰动函数hash()

  /**
     * 作用:让key的hash值高16位,参与路由运算。减少hash值的碰撞
     *
     * 1. key=null 插入第一位
     * 2. (h = key的hashCode) ^ (h右移16位)
     * 异或算法:相同返回0 不同返回1
     * 例如:
     *         h = 1001 0011 1011 1111 0000 0110 1000 0011
     * h >>> 16  =>0000 0000 0000 0000 1001 0011 1011 1111
     *
     * 1001 0011 1011 1111 0000 0110 1000 0011
     * ^
     * 0000 0000 0000 0000 1001 0011 1011 1111
     * =
     * 1001 0011 1011 1111 1001 0101 0011 1100
     *
     */
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

如何路由寻址(计算出桶位)

桶位 = (table.length-1& hash

HashMap核心方法 putresizegetremovereplace

put -> putVal源码

先看一下put数据时的大致流程图:
image.png

源码注释比较多,后面对putVal源码的执行顺序做了文字总结流程图总结。结合起来看更容易理解:

先贴出源码流程图小结,方便对比源码查看流程

putVal源码流程图小结

对应源码中代码的执行顺序,可对比着源码查看:

image.png

putVal源码:


    /**
     * 问题: HashMap什么时候初始化哈希表的?
     *   答:第一次调用put时初始化。
     * 问题:为什么第一次put才去初始化哈希表?
     *   答:为什么第一次put才会初始化因为哈希表初始化会占用很大内存,用户可能只是new HashMap而没有使用
     * 
     * 路由寻址作用:通过路由寻址确定要插入的元素,在table中应该插入的桶位
     * 路由寻址公式 : index下标 = (table-1) & hash
     */
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K, V>[] tab; // 临时table
        Node<K, V> p; // 临时存储的元素对象(解释:通过路由寻址,找到的元素)
        int n, i; // n:是临时的table长度   i:通过路由寻址赋值的table的桶位(即:table表中的下标)
        // 分析1 --> table不存在初始化哈希表
        // 解释 --> table==null 或 table长度为0。哈希表不存在,触发扩容机制。 同时完成tab、n 的赋值
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        /**
         * put数据时的2种情况:
         *    路由寻址算法:桶位 = (table-1) & hash
         *    1.该桶位不存在数据  if
         *    2.该桶位已经存在数据  else
         * */
        // 分析2 --> 这个桶位不存在元素直接存入该桶位
        // 解释 --> 通过路由寻址 确定index, 检测index在table中元素是否存在。不存在直接存入该桶位。同时完成 p 的赋值
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            // 分析3 --> p 元素存在值,则属于 链表、红黑树 情况
            // 解释 --> p 在上面if判断中已经赋值。(即:当前该桶位已经存在的元素)
            Node<K, V> e; // 临时的元素对象
            K k; // 临时key
            // 分析3.1 -->  table 中已经存在元素和要存储的元素重复
            /**
             * p.hash:当前元素的key的hash值
             * 解释 --> 条件1: p.hash == hash : 当前table[index]元素对象的key的hash值和要存储元素的key的hash值相同
             * 解释 --> 条件2: ((k = p.key) == key || (key != null && key.equals(k)))): 当前元素的key和要存入元素的key相等 或 (要存入元素的key!=null  且 要存入元素的key和当前桶位的元素的key相同)
             * 满足2个条件,说明要存入的元素和当前桶位的元素相同
             * 对临时元素对象e赋值
             * 后面对e进行判断,e!=null 的情况,会进行覆盖
             * */
            if (p.hash == hash &&
                    ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
                //分析3.2 --> 检测p元素是否在树中
            else if (p instanceof TreeNode)
                e = ((TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value);
            else {
                // 分析 3.3 --> 链表。 说明链表头元素和和当前要存入的元素不同
                // 解释 --> 遍历链表
                for (int binCount = 0; ; ++binCount) {
                    // 分析 3.3.1 --> 遍历整个链表  p.next==null  没找到与当前要插入的的key的node
                    // 解释 --> 形成链表的关键,就是靠Node元素内的next来进行指向下一个元素的
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        //分析 3.3.2 --> 判断插入当前元素后,检查是否达到树化标准:1.链表>8  2.table 元素个数超过64
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);// 树化
                        break;
                    }
                    // 3.3.3 说明在链表中找到了,与当前要插入的key相同的node元素。break结束循环,后续要替换操作
                    if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            // 3.4  e != null 新值替换旧值 并返回老值
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        // 4. 记录对table操作的次数
        ++modCount;
        // 5. 再次检测是否需要扩容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }w
putVal源码流程文字小结
  1. map第一次put时,哈希表不存在元素,会触发扩容机制

  2. 通过路由寻址确定要插入元素的桶位,当前桶位无元素,则直接插入该桶位

  3. 前2个条件不符合,说明该桶位已经存在元素

    3.1 通过比对要插入的Node和当前桶位Node的key的hash值 && key是否相同,相同则对临时Node赋值,后续会进行新值替换

    3.2 已经树化

    3.3 遍历链表

    3.3.1 遍历查找与当前要插入的Node是否有相同,(e=p.next)=null 说明没有存在的,插入链表
    
    3.3.2 插入后,检测是否满足树化标准条件,满足的话进行树化。
    
    3.3.3 同3.1的比对逻辑一样。 是相同元素则break 结束循环
    

    3.4 e != null 新值替换旧值 并返回老值

  4. 记录对table操作的次数

  5. 再次检测是否需要扩容:符合条件进行扩容

resize扩容机制

扩容核心做了2件事情:
image.png

resize源码:
源码中注释已经写很详细。需要看官,脑海中有个结构。这段源码后面会放出美团技术团队博文中的一张图:

 /**
     * 问题:为什么需要扩容?
     *   答:为了解决hash冲突导致的链化影响查询效率的问题,扩容会缓解该问题
     */
 final Node<K, V>[] resize() {
        Node<K, V>[] oldTab = table; // 临时变量:扩容前table
        int oldCap = (oldTab == null) ? 0 : oldTab.length; // 临时 扩容前table长度
        int oldThr = threshold;  // 临时变量:触发扩容前阙值
        int newCap, newThr = 0; //newCap = 临时变量:扩容后table长度   newThr = 临时变量:扩容之后,下次触发扩容阙值  后面newThr会赋值给threshold
        /* ----------------- 计算 newCap newThr -----------------*/
        // 分析 1 --> 计算新的newCap值(临时新table长度) 、newThr值(临时新扩容阙值)
        // 分析 1.1 -->  哈希表已经初始化过
        if (oldCap > 0) {
            // 分析 1.1.1 --> 哈希表长度大于定义最大范围(这种情况一般不会发生)
            // 解释 --> 向左位移1位即:扩大一倍。所以 1 << 30是一个很大的数字
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            // 分析 1.1.2 --> 向左位移1位(扩大一倍)
            // 解释 --> 满足2个条件:1.当前哈希表的大小 < 定义的最大值  2. 当前哈希表的大小 > 哈希表的最小初始值
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                    oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold 位移后的扩容阙值,赋值给newThr
        }
        // 分析 1.2 --> 通过HashMap其他三个构造方法初始化.
        //解释 --> 只有new HashMap() 没有对threshold进行初始化
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {
            // 分析 1.3 -->  //  即:oldCap=0 , 当前table=null
            // 解释 --> 说明当前是通过new HashMap() 初始化的。
            // newCap : table
            // newThr 扩容阙值: 默认大小 * 负载因子
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int) (DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        // 分析 --> 1.4  对上述计算newThr进行检测。如果条件成立 需要重新计算出newThr
        if (newThr == 0) {
            float ft = (float) newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float) MAXIMUM_CAPACITY ?
                    (int) ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
        /*----------------- 扩容开始 -----------------*/
        //分析 2--> 真正开始扩容
        @SuppressWarnings({"rawtypes", "unchecked"})
        Node<K, V>[] newTab = (Node<K, V>[]) new Node[newCap];
        table = newTab;//扩容之后的哈希表 赋值给 table
        if (oldTab != null) {// 确定原来的哈希表不为null才进行扩容
            for (int j = 0; j < oldCap; ++j) {
                Node<K, V> e;
                if ((e = oldTab[j]) != null) { // 当前桶位存在元素
                    oldTab[j] = null; // 把当前桶位置为null,方便回收。上面if判断已经赋值给e
                    //2.1. e.next == null 说明当前桶位只有一个数据(不是链表、树)
                    // 把当前元素,通过路由寻址找到在新哈希表中的桶位,直接存入该桶位
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                        //2.2. 该节点,已经树化
                    else if (e instanceof TreeNode)
                        ((TreeNode<K, V>) e).split(this, newTab, j, oldCap);
                        //3.3. 链表
                    else { // preserve order
                        //底位链表:存放在扩容之后的数组的下标的位置,与当前数组下标的位置一致(例如:table 16扩容到32  ,假如15下标位置的数据,扩容后数据还是在15这个位置)
                        Node<K, V> loHead = null, loTail = null;
                        //高位链表:存放在扩容之后的数组的下标的位置 -->  当前数组下标位置+扩容之前数组的长度
                        Node<K, V> hiHead = null, hiTail = null;
                        Node<K, V> next;
                        do {
                            next = e.next;
                            //对链表进行拆分,分别存入低位链表、高位链表
                            /**
                             * 例:
                             * e.hash存在2种情况: 高位要么是1要么是0
                             *      1. 1 1111
                             *      2. 0 1111
                             * 假设oldCap=16 =>二进制: 1 0000
                             *
                             * &运算:
                             *   1 1111
                             * & 1 0000
                             * = 1 0000  =>16  说明高位是1  存入高链
                             *
                             *   0 1111
                             * & 1 0000
                             * = 0 0000  =>结果为0  说明高位是0  存入低链
                             * */
                            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; // 目的是把链断开 存入低链时,next还指向一个高位链的数据,所以需要断开
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;// 目的是把链断开
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

美团博文中的图:
image.png

get -> getNode
 final Node<K, V> getNode(int hash, Object key) {
        Node<K, V>[] tab; // 临时table
        Node<K, V> first, e; // first -> 头元素
        int n; // table长度
        K k;
        /**
         * 两个条件:
         * 1. 哈希表不能为null
         * 2. 路由寻址,桶位不能为null
         * */
        if ((tab = table) != null && (n = tab.length) > 0 &&
                (first = tab[(n - 1) & hash]) != null) {
            /**
             * 第一种情况:
             * 检查当前桶位元素
             * 1. first.hash == hash => 当前桶位的Node的key的hash值和要取的key的hash值相同
             * 2. 当前桶位的Node的key和要取的key相同
             * 满足2条件则返回当前桶位的Node
             * 进入此判断:
             *     说明当前桶位只有一个元素
             */
            if (first.hash == hash && // always check first node
                    ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            /**
             * 第二种情况:
             * 当前桶位的Node的next不为null,则当前桶位是 链表 或 树
             * */
            if ((e = first.next) != null) {
                // 树查找
                if (first instanceof TreeNode)
                    return ((TreeNode<K, V>) first).getTreeNode(hash, key);
                // 链表查找
                do {
                    /**
                     * 1. first.hash == hash => 当前桶位的Node的key的hash值和要取的key的hash值相同
                     * 2. 当前桶位的Node的key和要取的key相同
                     * */
                    if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }
remove -> removeNode
 final Node<K, V> removeNode(int hash, Object key, Object value,
                                boolean matchValue, boolean movable) {
        Node<K, V>[] tab;
        Node<K, V> p;
        int n, index;

        // p -> 根据寻址结果得到的桶为元素Note
        // n -> 哈希表长度
        // index -> 寻址结果
        /*---------分析 1 --> 查找要删除的Node ---------*/
        /**
         * 两个条件:
         * 1. 哈希表不能为null
         * 2. 路由寻址,桶位不能为null
         * */
        if ((tab = table) != null && (n = tab.length) > 0 &&
                (p = tab[index = (n - 1) & hash]) != null) {
            Node<K, V> node = null, e;
            //
            //e --> 当前桶位元素的下一个元素
            K k;
            V v;
            /**
             * 分析1.1 -->  同getNode的比对逻辑是一样的
             * 赋值给临时node
             * */
            if (p.hash == hash &&
                    ((k = p.key) == key || (key != null && key.equals(k))))
                node = p; //
            /**
             * 分析1.2 -->
             * 当前桶位的Node的next不为null,则当前桶位是 链表 或 树
             * */
            else if ((e = p.next) != null) {
                // 树
                if (p instanceof TreeNode)
                    node = ((TreeNode<K, V>) p).getTreeNode(hash, key);
                else {
                    // 链表查找
                    do {
                        if (e.hash == hash &&
                                ((k = e.key) == key ||
                                        (key != null && key.equals(k)))) {
                            // 链表中到元素 break结束循环
                            node = e;
                            break;
                        }
                        p = e;
                    } while ((e = e.next) != null);
                }
            }
            /*---------分析 2 --> 删除找到的Node ---------*/
            /**
             * Node不为null,说明找到了要删除的Node
             * */
            // Node不为null 比对要删除的value是否相等
            if (node != null && (!matchValue || (v = node.value) == value ||
                    (value != null && value.equals(v)))) {
                // 分析 2.1 --> 树删除
                if (node instanceof TreeNode)
                    ((TreeNode<K, V>) node).removeTreeNode(this, tab, movable);
                //分析 2.2 --> 把node的下一个元素,指向当前桶位
                // 解释 --> 只有一种情况,就是分析1.1 -->这种情况,当前的桶位元素即为要删除的情况
                else if (node == p) //
                    tab[index] = node.next;
                //分析 2.3 --> (链表中间元素删除)将当前桶位要删除元素的上一个元素next指向node的下一个元素
                        /**
                         * 解释-->:1,2,3三个元素
                         *    关系: 1.next指向2
                         *         2.next指向3
                         *         => 2是中间元素
                         *    删除:2是要删除的元素:
                         *        => 把1的next指向2的next(2的next是3) 即: 1.next = 3
                         *        这样就完成链表中间元素的删除
                        * 
                         * */
                else
                    p.next = node.next;

                // 同步哈希表的操作次数
                ++modCount;
                // 同步哈希表的长度
                --size;
                afterNodeRemoval(node);
                return node;
            }
        }
        return null;
    }

replace

getNode获取元素

 @Override
    public boolean replace(K key, V oldValue, V newValue) {
        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;
    }
  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值