栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

HashMap源码分析

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

HashMap源码分析

  1、final属性

    
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

    
    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;

    
    static final int MIN_TREEIFY_CAPACITY = 64;

2、成员变量

    
    transient Node[] table;

    
    transient int size;


    
    int threshold;

    
    final float loadFactor;

3、构造函数

    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;
        //初始化的容量存在扩容阈值中
        //容量必须是2的正整数次方,tableSizeFor得到大于initialCapacity的最小的2的正整数次方
        this.threshold = tableSizeFor(initialCapacity);
    }
   //计算大于cap的最小的2的正整数次方
    static final int tableSizeFor(int cap) {
        //例如传入cap是13,对应的二进制为: 1101,则n为1100
        int n = cap - 1;
        //运行结果n为1100|0110=1110
        n |= n >>> 1;
        //运行结果n为1110|0011=1111
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        //假设原来cap的二进制数从高到低第一位为1所在的位为X
        //(1)cap就是二的正整数次方即二进制为  00...0100...00的形式,
        //   cap-1就为  00...0011...11的形式,经过上面左移或运算后任为00...0011...11
        //(2)cap不是二的正整数次方,假设二进制为00...0110...10的
        //   cap-1就为  00...0110...01,经过上面左移或运算后任为00...0011...11
        //将n+1得到结果  00...0100...00 即为cap对应的最小的2的正整数数次方
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

为什么HashMap的容量必须是2的整数次方?

这个设计是为了方便节点在数组中索引位置的计算,

通用情况下是取模运算:节点在数组中的位置=节点的hash值%数组长度,但是取模运算的效率比较低,为了提高运算效率将其修改为位运算,即

              节点在数组中的位置=节点的hash值&(数组长度-1)

这个公式等价于取模运算的前题是:数组长度是2的正整数次方。所以HashMap的容量必须是2的整数次方。

4、put方法源码解析

    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
    //被称为扰动函数,作用是减少hash冲突
    static final int hash(Object key) {
        int h;
        //h=key.hashcoede,通过Object的hashcode()得到的哈希码与key的内容和内存地址等相关
        //前面说过,计算索引的公式是 index=hashcode&(数组容量-1)
        //数组容量-1 的形式是00...0011...11即高位是M位的0低位是N位的1,M+N=32(即int为32位)
        //因此 计算索引的公式 的计算公式只与低N位有关
        //为了减少hash冲突 引进扰动函数  f(h)=h^(h >>> 16)
        //即的到的结果是: 高16位保持不变,低16位是高16位与原来低16位异或的结果
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node[] tab; Node p; int n, i;
        //1、hashmap是中的数组是懒加载的,只有当插入第一个元素时才进行初始化
        //数组为空通过扩容完成初始化
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        //2、数组不为空,但hashcode散列到的位置为空,直接创建节点放入到数组中的该位置
        //当n为2的正整数次方时,(n - 1) & hash=hash % n
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node e; K k;
            //3、发生了hash冲突,数组上的头节点的key与要插入的key相同,返回老的节点
            if (p.hash == hash &&
                    ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            //4、发生了hash冲突,数组上的头节点的key与要插入的key不同,头节点是树节点的实现
            //4.1、遍历红黑树,有相同的key,返回老的节点
            //4.2、无相同的value则创建新的节点插入树中,进行插入后的平衡调整,返回null
            else if (p instanceof TreeNode)
                e = ((TreeNode)p).putTreeval(this, tab, hash, key, value);
            else {
                //5、遍历链表
                for (int binCount = 0; ; ++binCount) {
                    //5.2、遍历完成,未发现key相同的节点,新建节点插入链表尾部,返回空
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        //在插入节点时,当前的节点个数为8(插入后为9),触发逻辑treeifyBin()
                        //触发逻辑treeifyBin(): 当前数组的长度小于64,会先进行扩容;当大于等于64,才会将链表转为红黑树
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    //5.1 发现有相同的key,返回老的节点
                    if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            //6、e为与key相同的老节点,
            // 如果key相同的老节点存在,表示本次为覆盖操作,将其value进行覆盖,并返回老的值
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        //7、本次的操作为插入,当本次插入数据完成后,键值对的总数量大于扩容时,进行扩容,返回null
        //注:size指的是总的键值对的数量,而容量指的是数组的长度,扩容阈值也是通过数组的长度*负载因子得到
        //当size==扩容阈值的时候,必定存在index数组上index位置上为空,且达到扩容阈值就表示,可能存在链表很长的情况
        //负载因子的意义是表示,数组上平均每个位置挂的节点数量为0.75
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

put的逻辑:(1)判断数组是否为空,空就通过扩容来初始化数组

                     (2)通过index=hashcode&(数组长度-1)计算出节点在数组上的索引位置

                     (3)做数据插入

                                (3.1)如果数组在index上为空,直接将节点插入到数组的index位置

                                (3.2)数组上不为空,即存在节点,比较原头节点的key是否与新插入的进行                                               比较,如果相同则将其覆盖

                                (3.3)如果不相同,通过节点的对象类型,判断此头节点下接的是链表还是

                                            红黑树。

                                (3.4)如果是红黑树,做遍历,找到key相同的节点就做value的覆盖,未找                                                到则创建新节点,将其插入红黑树,并对红黑树做平衡调整

                                (3.5)如果是链表,做遍历,找到key相同的节点就做value的覆盖,未找到

                                              则新创建节点,插入链表尾部,如果此时链表长度大于8,则运行

                                                treeifybin(数组长度小于64做扩容,否则将链表转为红黑树)

                        (4)判断当前的节点数量是否大于扩容阈值,大于就做扩容

                        (5)返回运行结果,是新插入返回空,是覆盖则返回原来节点的value.

5、resize扩容方法源码

final Node[] resize() {
        Node[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        //1、原来的数组长度不为0,即非第一次put数据
        // (插入数据后数据量大于阈值的情况,链表长度大于8要转化为红黑树但是数组长度小于64的情况)
        if (oldCap > 0) {
            //1.1、数组长度已经大于等于最大的数组长度,不再扩容,
            // 将阈值设为最大的int值,防止再触发扩容
            //返回原来的数组
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            //1.2、数组长度的长度扩大为原来的两倍
            //如果扩大的数组长度后小于最大的数组长度且大于默认的数组长度(16),将新的扩容阈值扩大为原来的2倍
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                    oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        //2、数组为空,即第一次put数据时触发
        //2.1、 有通过构造函数传入的初始大小,该初始大小存在threshold中,扩容阈值=构造参数传入值对应的最小的2的正整数次方*默认的扩容因子
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        //2.2、 如果没有传入初始的数组的长度,则使用默认的数组的长度16,扩容阈值=默认的数组长度*默认的扩容因子=16*0.75=12
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                    (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
        //3、创建新的数组长度等于上面步骤中得到的新的长度
        @SuppressWarnings({"rawtypes","unchecked"})
        Node[] newTab = (Node[])new Node[newCap];
        table = newTab;
        //4、是正真的扩容而不是第一次插入数据,遍历老的数组将所有的节点重新hash映射到新的数组上
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                Node e;
                //4.1、如果数组上的头节点不为空
                if ((e = oldTab[j]) != null) {
                    //4.1.1、 将数组上的引用置空,以便GC回收
                    oldTab[j] = null;
                    //4.1.1、 数组在该位置上的数据只有一个头节点,直接重新将该节点重新映射到新的而数组上
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    //4.1.2、 数组在该位置上头节点下接着一颗红黑树,调用split函数
                    //split函数逻辑:
                        //将所有的节点根据新的数组做hash映射,由于新数组的长度为原来的两倍且数组长度始终为2的正整数次,
                        // 运行  e.hash%newCap=e.hash&(newCap-1)=e.hash&(oldCap*2-1)
                        // 设oldCap中1所在的位置为X(也等于newCap-1中1所在的最高位)
                        //同一棵红黑树上的所有节点在老的数组中hash映射的结果值是相同的,
                        // 而在新的数组上映射只会有两个结果,结果取决于e.hash在X位置上的值是0还是1
                        //(1.1)因此可以将原来的一棵树转化为两个链表,loHead链表(X位置为0)和hiHead链表(X位置为0)
                        //(1.2)将loHead链表放在老的位置上,而hiHead链表放在新的位置(即oldCap+老的index上)
                        //(1.3)将长度大于8的链表转化为红黑树
                    else if (e instanceof TreeNode)
                        ((TreeNode)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                        //4.1.3 数组在该位置上头节点下接着链表,逻辑于split函数类似
                        //(1.1)因此可以将原来的一个链表转化为两个链表,loHead链表(X位置为0)和hiHead链表(X位置为0)
                        //(1.2)将loHead链表放在老的位置上,而hiHead链表放在新的位置(即oldCap+老的index上)
                        Node loHead = null, loTail = null;
                        Node hiHead = null, hiTail = null;
                        Node 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);
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

情况一(第一次调用put方法触发扩容)即原来的数组为空:

调用构造函数进行初始化时,将初始的容量通过计算对应最小的2的整数次方传入扩容阈值中

(1)获取扩容阈值,如果为0,表示构造函数没有设置数组的初始容量,则容量使用默认的16,

扩容阈值设置为12=默认容量*默认的阈值=16*0.75

(2)扩容阈值不为0,则将容量设置为扩容阈值,设置扩容阈值,新的扩容阈值=扩容阈值*负载因子

  (3) 创建对应容量的数组

情况二(正真的触发扩容)即原来的数组是存在:

(1)原来的容量已经大于等于允许的最大容量,直接将扩容阈值设置为int的最大值,然后直接返回

(2)将数组的长度(容量)和扩容阈值设置为原来的2倍

(3)遍历老的数组上的所有节点,重新放到新的数组上

        (3.1)老的数组在index上为空直接跳过

        (3.2)老的数组在index上只有一个节点,通过 hashcode&(新的长度-1) 定位到新的数组上

        (3.2)老的数组在index上接的是链表,通过 hashcode&老的长度,将原链表上的所有节点              分到两个新的链表上即loHead为首的链表(运行结果为0)和hiHead为首的链表链表(不为0)

           loHead放在原来的index下,而hiHead放在(index+老的数组长度)下。

        (3.3)老的数组在index上接的是红黑树,与链表逻辑相同,遍历后将节点,分到两个链表              上,然后分别凡在index和(index+老数组长度)下,最后如果新的链表长度大于8就将其转            为红黑树

          

6、触发扩容逻辑的情况

(1)第一次put

  (2) 节点的数量大于扩容阈值

(3) 要将链表转化为红黑树,但此时的容量小于64

7、HashMap中的位运算

(1)数组的长度必须时2的N次方

(2)将容量转化为对应的最小的2的N次方,通过 初始值-1的5个左移与运算

(3)hash()扰动函数,通过左移16位的异或运算保持高位不变,低位为高位和低位的异或的结   果,hashcode^(hashcode>>16)

(4)节点在数组中的定位运算,hashcode&(数组长度-1)

(5)扩容时将链表(红黑树)一分为2的运算,(hashcode&oldCap)

8、调试代码

package collections;

import java.util.HashMap;

public class HashMapDebug {
    public static void main(String[] args) {
        HashMap map=new HashMap<>();
        map.put(16,1);
        map.put(32,1);
        map.put(48,1);
        map.put(64,1);
        map.put(80,1);
        map.put(96,1);
        map.put(112,1);
        map.put(128,1);
        //在index=0位置上的链表插入这条数据后后长度大于8,但此时数组长度为16(小于64)
        //会触发扩容逻辑
        map.put(144,1);
        map.put(160,1);
        map.put(176,1);
        map.put(192,1);
    }
}
public class HashMapDebug2 {
    public static void main(String[] args) {
        HashMap map=new HashMap<>();
        //在index=0位置上的链表插入这条数据8条数据
        map.put(16,1);
        map.put(32,1);
        map.put(48,1);
        map.put(64,1);
        map.put(80,1);
        map.put(96,1);
        map.put(112,1);
        map.put(128,1);
        //在index=1插入5条数据
        map.put(17,1);
        map.put(33,1);
        map.put(49,1);
        map.put(65,1);
        //插入之后size大于扩容阈值触发扩容
        map.put(81,1);
    }
}

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/445172.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号