十分详细的HashMap 源码

前置知识

1:HashMap在1.7 是 Entry数组+链表 1.8是 Node数组+链表+红黑树

重要变量的解释

首先是该方法的变量如下图
在这里插入图片描述

DEFAULT_INITIAL_CAPACITY

表示的意思就是 默认初始化的容量16
注意 容量必须是 2的幂次方倍数,至于源码如何实现的,是利用后面的位运算进行实现的,后面会讲。


    /**
     * The default initial capacity - MUST be a power of two.
     */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
MAXIMUM_CAPACITY

表示这个容器的长度最多不超过1,073,741,824


    /**
     * The maximum capacity, used if a higher value is implicitly specified
     * by either of the constructors with arguments.
     * MUST be a power of two <= 1<<30.
     */
    static final int MAXIMUM_CAPACITY = 1 << 30;
DEFAULT_LOAD_FACTOR

就是所谓的负载负载因子,当达到 这个要求:负载因子*当前容量<当前


    /**
     * The load factor used when none specified in constructor.
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
TREEIFY_THRESHOLD

则是 从链表变为树的一个阈值,当新插入的节点加起来>8 才有可能进行树化,其是从后面的源码可以知道 进行树化的具体条件:
该列表 已经有8个节点了 才能变成红黑树 (加上这个新节点9个)先插入 并且 所有元素的个数大于等于64个元素才能树化
如果 达到了 9个 但是没到 64个 就扩容 让链表变短
这个64则是 下面要介绍 MIN_TREEIFY_CAPACITY
源码:
在这里插入图片描述


    /**
     * The bin count threshold for using a tree rather than list for a
     * bin.  Bins are converted to trees when adding an element to a
     * bin with at least this many nodes. The value must be greater
     * than 2 and should be at least 8 to mesh with assumptions in
     * tree removal about conversion back to plain bins upon
     * shrinkage.
     */
    static final int TREEIFY_THRESHOLD = 8;
UNTREEIFY_THRESHOLD

这个变量表示的是 从树化变成链化的条件。当树化的元素小于6则进行 链化。


    /**
     * The bin count threshold for untreeifying a (split) bin during a
     * resize operation. Should be less than TREEIFY_THRESHOLD, and at
     * most 6 to mesh with shrinkage detection under removal.
     */
    static final int UNTREEIFY_THRESHOLD = 6;
MIN_TREEIFY_CAPACITY

就是进行树化的那个 表容量的判断 详细解释 TREEIFY_THRESHOLD
注意这个参数的值至少是 4*TREEIFY_THRESHOLD


    /**
     * The smallest table capacity for which bins may be treeified.
     * (Otherwise the table is resized if too many nodes in a bin.)
     * Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts
     * between resizing and treeification thresholds.
     */
    static final int MIN_TREEIFY_CAPACITY = 64;

方法开始执行:

看方法的启动:
在这里插入图片描述

无参构造创建:

很明显这次的初始化 我们只设置了负载因子,并没有进行数组的初始化
在这里插入图片描述

有参构建

直接看 两个参数的那个 有参构造 因为一个的也是调用的两个:
在这里插入图片描述
他只是 帮你 把负载因子给你用默认的传进去
有参构建

public HashMap(int initialCapacity, float loadFactor) {
        //判断 你初始化的大小 小于0 直接报错
        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的幂次方的关键
        this.threshold = tableSizeFor(initialCapacity);
    }

tableSizeFor方法:
在这里插入图片描述
进行测试的代码:
在这里插入图片描述
结果+1 则是他们最近的 2的次方数
注意 默认大小是16

put方法

在这里插入图片描述

putValue方法

第四个 参数 如果为 true 则表示 存放进行相同的key值 但是value值不会进行改变了 默认为flase
第五个 为false 表示 这个表正在创建中。

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; //对应创建的数组
        Node<K,V> p;//用来记录当前浏览的元素坐标
        int n, i;//n记录表的长度
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;//进来则表示 数组为空 需要创建
        if ((p = tab[i = (n - 1) & hash]) == null)//进行hahs计算 算坐标 这个位置 有没有 元素 没有直接放进去就行
            tab[i] = newNode(hash, key, value, null);
        else {//表示这个位置 有元素  则需要进行匹配
            Node<K,V> e;//临时元素 记录是否存在相同key的
            K k;//记录记录k的
            if (p.hash == hash &&
                    ((k = p.key) == key || (key != null && key.equals(k))))
                //表示 key的hash值相等 && key的内容也相等 则把p赋值给e
                //第一个元素就是 要替换的元素
                e = p;
            else if (p instanceof TreeNode)//这个节点是树 用红黑树的方法进行插入
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {//开始从这个列表找节点
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {//注意这一步就已经开始移动了  开始从第二个元素开始了  记住代表 0-》2
                        //表示到末尾了 直接把这个值插进去就行
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            //已存在8哥了 你是第9个  那 咱就开始树化吧
                            //为什么是8  不是》=8-1吗   注意前面说的 0代表第2个元素的坐标了  所以 满足条件的7 代表第9个元素
                            //里面还有一个逻辑判断  要64个元素才能开始 树化 否则就只能去扩容了
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k))))
                        //表示找到了 一样得 退出
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                //表示存在相同的key
                V oldValue = e.value;
                //查看是否要进行 值的替换
                if (!onlyIfAbsent || oldValue == null)
                    //替换
                    e.value = value;
                //空方法 留给LinkedHashMap操作的
                afterNodeAccess(e);
                return oldValue;
            }
        }
        //这个参数是记录修改次数的
        //快速失败 (fail-fast)有用
        ++modCount;
        //计算 目前的元素含量 是否大于了 threshold(负载因子*容量)
        if (++size > threshold)
            //大于开始扩容
            resize();
        //空方法 留给LinkedHashMap操作的
        afterNodeInsertion(evict);
        return null;
    }

resize 初始化或加倍表大小

在这里插入图片描述

具体的解释 已经写在代码中 自己写的 又不完善的地方 欢迎提出

final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;//这个参数属于是 负载因子*容量
        int newCap, newThr = 0;
        if (oldCap > 0) {//这个表示 数组创建了 是进来扩容的
            if (oldCap >= MAXIMUM_CAPACITY) {//如果容量已经达到最大值了
                //负载因子调到最大 以后别进来了 扩容不了了
                threshold = Integer.MAX_VALUE;
                return oldTab;//直接返回原来的不进行扩容的操作
            }//没有达到最大值 进行扩容的操作
            //这一步操作 则是 将老的扩大一倍 没有超过最大容量
            //并且老的容量比默认的16要大 则将threshold(负载因子*容量)扩大一倍
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                    oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }//这个表示 前面的没进去(那就是 不是来扩容的  是来初始化的) 且threshold有值
        //就是你在new的时候 传得有 参数就会进入这个  方法 并且oldThr的值 肯定是2的次方倍 为什么请看构造方法
        //则通过 threshold给你的这个容量赋值
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {              // zero initial threshold signifies using defaults
            //进入到这个表示 完全是 new的时候 什么参数也没传 那就全部都有源码的默认值
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        if (newThr == 0) {
            //这个主要是针对上面的else if (oldThr > 0) 因为走那个方法的话 newThr肯定为0
            //开始计算负载因子*容量
            float ft = (float)newCap * loadFactor;
            //给threshold赋值 如果容量超过了默认的最大容量 则给个最大值 没有则将刚刚计算的值赋予即可
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                    (int)ft : Integer.MAX_VALUE);
        }
        //完成threshold的赋值
        threshold = newThr;
        //下面这一步就是 初始化计算好的数组
        @SuppressWarnings({"rawtypes","unchecked"})
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        //将数组赋值回去
        table = newTab;
        //下面这一步则是进行 元素的转移
        //你扩容了 是不是 需要把数组里面的元素进行 hash计算然后赋值回去嘛
        if (oldTab != null) {
            //遍历一遍这个数组里面的所有元素
            for (int j = 0; j < oldCap; ++j) {
                //注意node 这个类 里面是 有next这个属性的
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    //把位置清空
                    oldTab[j] = null;
                    //判断该节点下面有没有元素
                    if (e.next == null)
                        //下一个元素没有值  算hash值把他放到行数组里面去
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)//表示 这个链表已经树化
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order表示 没有树化还是个 列表
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;//记录下一个元素的
                        do {
                            next = e.next;
                            //注意 这里是 oldcap  不是 oldcap-1
                            //下面这一些列的操作 起到的作用就是 将 该部分的链的元素 分成两个部分
                            //一部分叫 low  low这一部分 的坐标可以直接插进到雨原来的部位去 一部分叫 hei插入到新的计算后的坐标去
                            //例子: a:hash 14  b:hahs 30  在16的容量下 两个元素的位置都是在14的坐标下
                            //进行扩容后 a 应该还是 14  b 则应该时 30 
                            if ((e.hash & oldCap) == 0) {
                                //就是 将这些串起来
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {//不为0 那就用另一个 串起来
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        //经过这个循环  得到 两串链表 low(保持坐标不表) hig  (更新坐标)
                        //下面这一步  则是 将两串放到该放的位置
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

split (扩容时将树进行拆分)

注意 treenode 继承自node 它不仅时树 也是列表的结构
在这里插入图片描述

final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
            //tab是newtable  不是oldtable
            //index 时 咱要拆分的那个树所在 坐标
            //bit  是 oldcap容量
            TreeNode<K,V> b = this;
            // Relink into lo and hi lists, preserving order
            TreeNode<K,V> loHead = null, loTail = null;
            TreeNode<K,V> hiHead = null, hiTail = null;
            int lc = 0, hc = 0;  //这是用来记录两颗树的  长度的 要是 太小了 就要进行变换  将其变成链表
            for (TreeNode<K,V> e = b, next; e != null; e = next) {
                next = (TreeNode<K,V>)e.next;
                e.next = null;
                //这一步 也是和 前面的列表操作类似 也是 将 里面的树 进行区分位low 和hig
                if ((e.hash & bit) == 0) {
                    if ((e.prev = loTail) == null)
                        loHead = e;
                    else
                        loTail.next = e;
                    loTail = e;
                    ++lc;
                }
                else {
                    if ((e.prev = hiTail) == null)
                        hiHead = e;
                    else
                        hiTail.next = e;
                    hiTail = e;
                    ++hc;
                }
            }
            //执行完这操作 就分成了两颗树 说是树但是 low和hig 其实只是在用  pre 和nex 更像是 列表
            //然后 看看low这棵树 元素熟路是否小于等于UNTREEIFY_THRESHOLD(默认6)
            if (loHead != null) {
                if (lc <= UNTREEIFY_THRESHOLD)
                    //如果数量太少则进行链化
                    tab[index] = loHead.untreeify(map);
                else {
                    //没有则满足要求 进行位置的移动即可
                    tab[index] = loHead;
                    if (hiHead != null) // (else is already treeified)
                        //将这个low树进行重新树化   
                        // 为什么是树了还要树化?  上面说了 他们只用到了prev和next 完全不是树 所以需要进行树化 之所以叫他们说 是因为他们是treenode类
                        //至于为什么 需要判断hiHead != null 才能进行操作 是为了提高效率
                        //比如你想一下你搞半天  hig那个树 啥也没有 不就是证明 都在low上面 那还需要重新树化什么 直接搞过去就行
                        loHead.treeify(tab);
                }
            }
            //与上面的原理一致
            if (hiHead != null) {
                if (hc <= UNTREEIFY_THRESHOLD)
                    tab[index + bit] = hiHead.untreeify(map);
                else {
                    tab[index + bit] = hiHead;
                    if (loHead != null)
                        hiHead.treeify(tab);
                }
            }
        }

untreeify(将节点变成链表)

   final Node<K,V> untreeify(HashMap<K,V> map) {
            Node<K,V> hd = null, tl = null;
            //从当前传过来的节点开始遍历
            for (Node<K,V> q = this; q != null; q = q.next) {
                Node<K,V> p = map.replacementNode(q, null);
                if (tl == null)
                    hd = p;
                else
                    tl.next = p;
                tl = p;
            }
            return hd;
        }

treeify(将节点变成树)

 final void treeify(Node<K,V>[] tab) {
            TreeNode<K,V> root = null;
            //利用当前穿过来的这个 this 进行遍历  前面说了 low 和hig 只用到了 prev 和next 更像是列表
            //所以这个方法是让他们彻底变成真正得树
            for (TreeNode<K,V> x = this, next; x != null; x = next) {
                //转换成TreeNode对象
                next = (TreeNode<K,V>)x.next;
                x.left = x.right = null;
                //root为空,证明是第一个对象
                if (root == null) {
                    //设置根节点
                    x.parent = null;
                    x.red = false;
                    root = x;
                }
                //root不为空,根节点已经存在了,处理剩下对象
                else {
                    K k = x.key;
                    int h = x.hash;
                    Class<?> kc = null;
                    //对x之后的对象,进行划分左右
                    for (TreeNode<K,V> p = root;;) {
                        int dir, ph;
                        K pk = p.key;
                        //判断当前对象是应该划分到左边还是右边 -1放在左边  1放在右边
                        if ((ph = p.hash) > h)
                            dir = -1;
                        else if (ph < h)
                            dir = 1;
                        else if ((kc == null &&
                                (kc = comparableClassFor(k)) == null) ||
                                (dir = compareComparables(kc, k, pk)) == 0)
                            dir = tieBreakOrder(k, pk);

                        TreeNode<K,V> xp = p;
                        if ((p = (dir <= 0) ? p.left : p.right) == null) {
                            x.parent = xp;
                            //对象进行划分左右
                            if (dir <= 0)
                                xp.left = x;
                            else
                                xp.right = x;
                            //处理当前节点(判断是红色节点,还是黑色节点,是否需要右旋转,左旋转)
                            root = balanceInsertion(root, x);
                            break;
                        }
                    }
                }
            }
            //再次确定根节点
            moveRootToFront(tab, root);
        }

treeifyBin(列表进行树化)

final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        //判断 数组是空的 或者  数量小于64  那就去扩容吧 别来树化了
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();
            //以防万一 在确定一下 你传的这个位置上是不是空的  顺便给e赋值
        else if ((e = tab[index = (n - 1) & hash]) != null) {
            //
            TreeNode<K,V> hd = null, tl = null;
            do {
                //就是创建一个 treenode  你可以理解为 把e从node 变成了treenode
                TreeNode<K,V> p = replacementTreeNode(e, null);
                //下面这一步 就是熟悉的 将单链表 变成双向链表
                //然后交给treeify进行树化了
                if (tl == null)
                    hd = p;
                else {
                    p.prev = tl;
                    tl.next = p;
                }
                tl = p;
            } while ((e = e.next) != null);
            //执行完上面的循环  则这个列表是双向的列表
            //下面这个判断 在进行 判断 那个位置 是否为空
            if ((tab[index] = hd) != null)
                //进行树化
                hd.treeify(tab);
        }
    }

面试可能会问的问题:

扩容的条件

1.8 hashmap的扩容机制很简单: 就是当前的size(目前存放的元素)>threshold(负载因子(默认是0.75)*容量)
1.7 hahsmap的扩容机制 额外有个 条件:当前插入的格子不为空
下图是源码证明
在这里插入图片描述

扩容对元素坐标的处理

**1.8:**不论是树还是列表 都采用了low和 hig 进行存放
low表示 就算扩容了坐标的位置依旧不变,
hig表示 坐标的位置需要改变(+oldcap即可)
1.7老实巴交的计算hash的坐标然后放进去

key=null

key为null 是存放在0这个坐标的。

什么时候初始化

看了源码都知道 是在调用put的时候才会进行初始化

链表->红黑树(树化)的条件

结论: ** 你得是第9的元素 并且 所有元素的数量>=64 才能树化**
下图是部分解释 具体细节看putvalue的注释 很详细的
在这里插入图片描述
就是 你put 一个值叫key 叫a ,找出坐标 取出坐标对应的key值 假设为b
前面的判断了 然后到我图片这里 Bigcount=0 但是 你看下一步就是 直接指向next 这就是意味着 bigcount=0 对应的是 第2个元素!
而进入树化 需要 bigcount >=8-1 bigcount=7 对应的是第9个元素
而插入是在判断前操作的 所以 你得是第9的元素 才能树化

树化条件为什么是8

这个在源码的注释有写:就是 8的时候概率很低了
在这里插入图片描述

红黑树->链表

结论 红黑树的数量<=6 就开始链化
在这里插入图片描述

为什么不是 <=7 链化 而是6

因为 如果是7的话 频繁进行插入删除的话 会导致 容错空间就 8 超过8树化
小于8 链化 频繁切换 浪费效率。

坐标的计算为什么hash & (length- 1) 不是key & (length-1)

解释 多加了一层运算 是为了是存储的位置更加的散乱
加的这个hash算法 会使得 key的高位也进入到运算之中,如果只是key & (length-1) 如果length=16 那 这个运算只会和后四位有关15(1111)
到时候 数据的分布不会那么的散乱。

modCount

这个变量记录你增删的操作次数 用于快速失败fail-fast
这就是为什么 你用for each 进行遍历时候 如果进行 增加删除操作 会报错的原因,他会进行 迭代器里面记录的 和 hahsmap里面记录的 这个值比较 要是不一样 就会报错。
在这里插入图片描述

死循环问题

该问题是1.7的hashmap 进行扩容时,在多线程的情况下,可能产生。粗略可以这样回答:在1.7扩容过程中会新数组会和原来的数组有指针引用关系,所以将引起死循环问题。而1.8的扩容机制将原来的链表结构保存在节点e中,然后依次遍历e,根据hash&n是否等于0,分成两条支链,保存在新数组中。

有新的面试问题 会及时更新

  • 5
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值