[重识 Java] Map集合-HashMap源码解析 我学废了~

62 篇文章 1 订阅
19 篇文章 1 订阅

背景

HashMap在平时工作中使用频率高的离谱。 要说不熟悉吧? 天天用哪里不熟悉了?

面试的时候,面试官:哦,那你说说底层怎么实现的吧?我:额,这个,我记得好像是通过hash计算存储到数组,还有链表和红黑树的转变过程、装载因子等,具体怎么实现的不是很清楚,平时有看过网上很多文章,不过不记得了。。

但是呢,要从本质上提升自己,还是要沉下心来去深入的学习,只有基础牢固了才能更好在平时工作的运用~

本文会从以下几个方面来进行学习:

  • 基本原理: 从整体的角度,简单介绍HashMap的原理和涉及到一些概念
  • 操作API: 主要是构造方法、插入、删除、遍历等具体细节
  • 与其他Hash表的对比: 与Map系列其他集合的对比,加深理解

一、基本原理

HashMap继承自AbstractMap类,实现了Map接口。

首先来自网上一张图,侵删:

image.png

说明:

  1. 当调用put(key,value)时,HashMap会对key进行hash操作,从而得到hash值,然后在根据hash值对数组的长度的进行取余操作(内部通过位运算进行了优化),得到数组的下标。
  2. 封装key、value、hash、nextnode节点,如果该数组下标不存在元素,则直接把Node节点存入数组下标中。
  3. 若有元素,判断是否是红黑树节点,是树节点则将新节点插入到红黑树中。
  4. 如果不是树节点,那么把新的节点插入到链表的尾部。此外,当链表的长度>8时,由于链表查询的时间复杂度是O(N),因此链表会树化为红黑树,时间复杂度变为O(logN),从而提高查询效率。
  5. remove移除某个红黑树的节点后,如果红黑树的节点数<6时,为了节省内存空间,红黑树又会退化为链表。

红黑树,是平衡二叉搜索树的一种。它是一种特殊的二叉查找树。特点是对任何一个节点来说,其左子树中的任意节点值都比自己小,右子树中的任意节点的值比自己大(左小右大)。而红黑树能够在面对频繁数据操作中,始终保持从整体来看左右子树处于对称的状态,因此插入、删除、查询都能够高效的完成,时间复杂度为O(logN) N是节点数,非常的稳定。

1.1 存储节点梳理

HashMap中的存储节点涉及到四个:Entry、Node、TreeNode、LinkedHashMapEntry。其中的依赖关系如下: image.png

了解节点的内容,可以帮助后续理解!

1.2 常量和成员变量初识

常量:

//数组的初始长度:16。 必须是2的n次方,1<n<=30
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//数组的最大长度: size=1 * 2^30
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,那么优先扩容而不是树化。
static final int MIN_TREEIFY_CAPACITY = 64;


成员变量:
// 所有键值对的个数
transient int size;
// 下一次扩容的阈值。计算公式:capacity * load factor
int threshold;
//加载因子
final float loadFactor; 

了解某些重要常量和成员变量的含义,方便后续理解!

二、操作API

2.1 构造方法

2.1.1 空参构造

public HashMap() {
    
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
} 

只初始化加载因子为 0.75。其他的值都为默认。

2.1.2 有参构造

 public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
        //数组的初始length不能超过限制                                       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的幂。
        //注意: 由于外部可以传initialCapacity值,因此,这个值可能是3,那么tableSizeFor方法的作用就是保证threshold值始终为2的幂。
        this.threshold = tableSizeFor(initialCapacity);
    } 

我们重点关注下tableSizeFor方法。

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;
} 

原理: 根据给定的cap数组容量,返回以2^n(2的n次幂)的最小值。n的值是多少?那要根据cap的值来决定。

举个例子:

  • cap=0,threshold=1。 2的0次幂
  • cap=1,threshold=1。 2的1次幂
  • cap=3,threshold=4。 2的2次幂,2的3次幂肯定不符合。
  • cap=4,threshold=4。因为4是2的幂,所以不变。
  • cap=5,threshold=8。2的3次幂

这样就清楚了。 这个函数的作用就是防止外部传入的cap不符合2^n,所以要调整。

有个疑问,成员变量定义的时候不是说threshold=capacity * load factor吗? 怎么现在又等于tableSizeFor的返回值了? 确实此时threshold有值,但是却没用,当扩容的时候才会用到。 看下如扩容代码:

final Node<K,V>[] resize() {
       // ...省略
       //1, threshold=4
       int oldThr = threshold;
       // ...省略
       else if (oldThr > 0) // initial capacity was placed in threshold
           //如果外部通过构造指定了初始化容量值,会走这里的初始化逻辑。new HashMap(3)
           //2, threshold=4,newCap=4
           newCap = oldThr;
       // ...省略
       if (newThr == 0) {
           //3, newCap=4, newThr=3
           float ft = (float)newCap * loadFactor;
           newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?(int)ft : Integer.MAX_VALUE);
       }
        //4,threshold=3 
        threshold = newThr;
        // ...省略 

先不要着急慌!扩容什么的先不管,我们看上面四步就可以了。

  1. 假设我们new HashMap(3)。那么threshold=4
  2. 经过四步,注释很清楚
  3. 最终,数组的容量就是4,而threshold就变成了3。所以按照公式来理解是没有问题的!

因此,了解清楚后其实就不用纠结这里了。

2.2 插入元素 put()方法

put方法会调用putVal方法,我们重点看putVal内部原理。

2.2.1 putVal 方法

 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        // 1,如果数组为null,就初始化。(第一次put,肯定是null)
        if ((tab = table) == null || (n = tab.length) == 0)
        // 2,初始化数组,初始化size、threshold等
            n = (tab = resize()).length;
        // 3,hash值对数组长度取余得到下标,如果下标i的位置为null,则表示没有元素,newNode节点 直接存进去
        if ((p = tab[i = (n - 1) & hash]) == null)
            //null:这是第一个节点,没有后继节点next
            tab[i] = newNode(hash, key, value, null); 
        else {
            // 4,下标i的位置有值了
            Node<K,V> e; K k;
            // 4.1,先比较头节点与要插入的hash和key值是否相等,相等肯定是同一个节点。
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
            // 4.2,头节点不等,如果是树节点,则插入到红黑树里面。
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
            // 4.3,既不是头节点,也不是二叉树节点。则去链表中查找
                for (int binCount = 0; ; ++binCount) {
                        //4.3.1 在整个链表都没有找到,这是新节点,插入到链表尾部
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        //4.3.2 检查是否触发树化逻辑
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    //4.4 如果链表中某个节点匹配,则break循环,要插入的节点已经存在!
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            //4,如果要插入的节点已经存在,那么就更新value
            if (e != null) { // existing mapping for key
                 // 更新
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                // 5.1 专门留给LinkedHashMap的后续操作
                afterNodeAccess(e);
                return oldValue; // 如果已经存在过,则返回该元素。
            }
        }
        //6, 只有插入新元素才会走这里逻辑,
        ++modCount; 
        // 更新size+1,判断插入的元素个数大于扩容阈值,则进行扩容
        if (++size > threshold)
            resize();
        //专门留给LinkedHashMap的后续操作
        afterNodeInsertion(evict);
        return null; //如果是新元素则返回null
    } 
  • 总结:

put方法主要做了如下几点:

  1. 如果数组没有初始化,则直接扩容初始化数组。
  2. 根据插入元素的key值计算hash值,hash值对数组大小取余得到下标
  3. 如果下标位置没有元素,则直接插入
  4. 如果下标位置已经有元素,那么分三种情况:
  • 4.1 该元素的key、hash跟要插入的元素一致(认为是同一个节点),直接更新节点的value

  • 4.2 不一致,若是红黑树节点, 则插入到红黑树。如果已经在红黑树中存在则返回该节点;如果不在则当做新的节点插入,且返回null;

  • 4.3 不一致且又不是红黑树节点,那么该元素只能是链表节点。 遍历链表,如果找到,则证明在链表中已经存在,更新value即可,同时局部变量引用该节点(也就是方法中的e变量);如果在链表中没有找到,则认为是新节点,直接加入到链表的尾部,此时 e=null,加入新节点后如果节点数>8则进行树化

  1. 插入了新元素,size+1。如果size>threshold,则进行扩容

流程图如下,图片来自美团技术:

image.png

不过图中有一些问题。

当插入的节点已经存在的时候,覆盖valueputVal方法是直接结束了的,不会触发size++后续的逻辑。只有新节点的插入才会触发后续的的逻辑。

2.2.2 扩容 resize()方法

 final Node<K,V>[] resize() {
        // 拿到旧的table数组 以及旧的size
        Node<K,V>[] oldTab = table;
        //oldCap一开始肯定是0
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold; // 旧的扩容阈值,
        int newCap, newThr = 0;
        // 1,oldCap>0 表示正常的扩容逻辑
        if (oldCap > 0) {
            //如果旧的size已经很大了,超过2^30,那么指定为Integer.MAX_VALUE,也就是2^32。
            if (oldCap >= MAXIMUM_CAPACITY) { 
                threshold = Integer.MAX_VALUE;
                // 直接返回旧的hash数组 ,无法扩容了!!扩容失败!
                return oldTab;
            }
            //新容量为旧的2倍。
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                     //如果新的数组长度是旧的2倍且小于2^30方 同时旧的数组长度> 16。
                     //那么新的扩容阈值是旧的2倍。本质上还是cap*load factor。没啥区别!
                newThr = oldThr << 1; // double threshold
        }
        //2,如果外部通过构造指定了初始化容量值,会走这里的初始化逻辑。new HashMap(3)
        else if (oldThr > 0) // initial capacity was placed in threshold
            
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            //3,走这里表示,我们是通过空参构造方法构造的如: new HashMap()
            newCap = DEFAULT_INITIAL_CAPACITY; //16
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); //16*0.75=12 
            // newThr=12,也就是说如果size >=12,那么就表示触发扩容阈值,需要扩容了。换句话说,达到原始容量的 0.75倍(加载因子倍),即表示快装不下了,再装效率就会下降,必须要扩容。
        }
        //如果外部是通过构造指定了初始化容量值,会走这里的初始化逻辑。手动计算新的threashHold
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        //4,至此,我们需要明确两个概念cap和thr:
        cap:数组容量。 thr:整个节点个数的扩容阈值。
        以上不管是初始化还是正常的扩容逻辑,都是在根据旧的容量和扩容阈值,来计算得到新的容量和扩容阈值。
        //更新扩容阈值,下次超过了它就需要再次扩容
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
        //5,直接根据newCap new出新数组newTab
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;// 赋值新数组
        //6,遍历旧的数组 ,准备拷贝到新数组里面
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                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)
                        //如果是二叉树类型,那么进行切割分组
                        //内部工作: 也是根据原来数组长度,进行切割分组:高、低。
                        //然后在重新存储对应新的下标位置。注意,这里有可能会因为二叉树的个数分组后会出现小于6的情况,也就是可能会触发二叉树退化为链表。
                        ((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;
                            //e.hash & oldCap 用来判断>=oldCap。
                            //==0表示 新插入的位置还是之前的位置。处于低位,也就是j。为什么叫低位?j<oldCap。
                            //else表示 新插入的位置是高位,也就是j+oldCap位置。
                            //本质是吧原来的链表进行分组,在插入到新的数组里。
                            if ((e.hash & oldCap) == 0) {//表示是低位组。
                                //原因:hash不一样,那么数组长度变化后的取余值肯定也不一样了。但是由于是2的n次幂,所以余数也是有规律的。      
                                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;
    } 
  • 总结

resize的扩容工作可以分为两个部分:

  1. 创建新数组 根据旧的数组长度和扩容阈值,计算得到新的数组长度和扩容阈值。
  2. 迁移旧数据 循环旧数据,进行切割分组,在存储新数组中

主要流程如下(详细的过程可以看注释):

创建新数组:

  • 数组=null时:如果调用的是空参构造则初始化数组长度为16,扩容阈值12;如果调用有参,则根据掺入的cap计算得到新的数组长度newCap,扩容阈值为newCap*0.75。
  • 数组不为null:如果超过最大容量2^30,则扩容失败,直接返回旧数组;反之,则直接以2倍大小扩容数组。同时按照之前的公式提高新的扩容阈值。
  • 根据得到的新数组容量newCap创建新数组newTable对象

迁移旧数据:

  1. 遍历旧数组,如果只有头结点,那么对头结点的hash值重新对新数组长度取余,得到新下标,存入新数组。
  2. 如果头结点还有后继节点,且头结点红黑树节点?该位置肯定是红黑树结构。那么就对红黑树进行切割分组,得到两个新的红黑树结构,成为高位树和低位树,在插入到新数组中。这里可能会触发反树化(节点数<6,红黑树转换为链表)
  3. 如果头结点还有后继节点,是链表节点? 也会进行分组,得到高位链表和低位链表,插入到新的数组中。

高位和低位怎么理解?其实本质就是要根据新的数组长度重新hash取余!

高低位切分过程如下,图片来自参考的文章,侵删:

image.png 如上,扩容前存储结构,数组长度为8。hash值为35、27、19、43都映射到了下标为3的位置。 当通过e.hash & oldCap操作后,我们可以看到蓝色部分分为了两组。

image.png

扩容后的存储结构,数组长度变为16。上面叫低位组,下面的叫高位组。

2.2.3 树化 treeifyBin()方法

树化的触发时机:当新元素插入到链表尾部后,链表长度>8

final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        //1,如果数组为空,或者数组的长度小于64. 没达到最小树化容量。那么只进行扩容。
            resize();
        else if ((e = tab[index = (n - 1) & hash]) != null) {
        // 获取新节点插入位置的链表头,开始遍历链表,转为树节点
            TreeNode<K,V> hd = null, tl = null;
            //hd 是什么? tl是什么? 
            do {
                TreeNode<K,V> p = replacementTreeNode(e, null);
                //p虽然是TreeNode,但是还是以链表结构相连。hd就是链表的头结点。
                //第一次tl肯定是null。所以hd、tl都指向第一个节点。
                if (tl == null)
                    hd = p;// 树头
                else {
                //
                    p.prev = tl;
                    tl.next = p;
                }
                //tl不断的指向最后一个节点
                tl = p;
            } while ((e = e.next) != null);// 遍历原链表节点。
            // 以上总结: 把以Node为节点的单向链表 转变为以TreeNode为节点的双向链表。双向链表用来干啥????
            //把双向链表存储到数组的位置
            if ((tab[index] = hd) != null)
                //从头结点开始真正构建红黑树
                hd.treeify(tab); 
        }
    } 
  • 总结

如果数组为null,则进入扩容方法。如果数组长度小于64,即使某个下标处的链表长度>8也只会扩容,而不是树化操作

树化的过程:

  1. 先得到节点插入位置对应的头结点,开始遍历链表。
  2. 每个节点(Node)都转换为树节点(TreeNode)
  3. 把以Node为节点的单向链表 转变为以TreeNode为节点的双向链表。存入头结点。至此,还没有树化
  4. 调用hd.treeify(tab)开始红黑树的转换,其实就是填充left、right、parent节点。
2.2.3.1 构造红黑树 TreeNode.treeify()
 final void treeify(Node<K,V>[] tab) {
            TreeNode<K,V> root = null;
            // 从头结点开始遍历
            for (TreeNode<K,V> x = this, next; x != null; x = next) {
                next = (TreeNode<K,V>)x.next;
                x.left = x.right = null;
                if (root == null) {
                //构造红黑树的根节点
                    x.parent = null;
                    x.red = false;
                    root = x;
                }
                else {
                // 根节点不为空,准备要插入的节点 x
                    K k = x.key;
                    int h = x.hash;
                    Class<?> kc = null;
                    //根节点不为空, 从根节点root开始遍历 ,
                    for (TreeNode<K,V> p = root;;) {
                        int dir, ph;
                        K pk = p.key;
                        // 如果节点p的hash大于要插入的节点x的 hash值,dir=-1.
                        if ((ph = p.hash) > h)
                            dir = -1;
                        else if (ph < h) //如果小,dir=1.
                            dir = 1;
                        else if ((kc == null &&
                                  (kc = comparableClassFor(k)) == null) ||
                                  //通过compareTo方法 比较一次,如果dir==0,那么后面在比较一次
                                 (dir = compareComparables(kc, k, pk)) == 0)
                                 //dir==0,第二次key比较!!
                            dir = tieBreakOrder(k, pk);

                        //走出if-else逻辑,得到dir的值了
                        TreeNode<K,V> xp = p;
                        //一直遍历:如果dir<=0,则往左子树继续遍历。如果> 0则往右子树继续遍历.
                        //直到找到最后一个节点。
                        if ((p = (dir <= 0) ? p.left : p.right) == null) {
                            //p就是最后一个节点了,要插入的节点x的父节点指向xp。
                            x.parent = xp;
                            //插到xp的左边节点
                            if (dir <= 0)
                                xp.left = x;
                            else
                                xp.right = x;//插到xp的右边节点
                            //保证红黑树平衡
                            root = balanceInsertion(root, x);
                            break;
                        }
                    }
                }
            }
            //确保root是树的根节点。
            moveRootToFront(tab, root);
        } 
  • 总结

理解的关键点在于两个循环:

  1. 链表维度的遍历 根据next后继节点遍历整个TreeNode链表(从next的角度来看,可以称为链表)。
  2. 红黑树维度的遍历 得到每个链表节点,根据红黑树的特点(左小右大)遍历,插入left或者right节点。

红黑树的特点就是,左小右大。因此,核心思想就是根据hash值的大小来决定是往左子树还是右子树插入。

根据hash值比较大小的过程,见注释。 至此,一颗红黑树已经构建完成

2.3 获取元素 get()方法

get方法会调用getNode方法, 直接看getNode方法:

 final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        // 数组不为null+数组长度>0+hash对数组长度取余得到下标对应的第一个节点不为null
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                //如果hash相同,且key也相同,那么就是第一个节点。
                return first;
            if ((e = first.next) != null) {
            //不是第一个节点。首先判断是否是红黑树节点,如果是那么接下来就去红黑树中查找
                if (first instanceof TreeNode)
                    //first就是红黑树的根节点。遍历二叉树。
                    // 内部遍历过程: 根据hash的大小,进行递归查找。
                    return ((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;
    } 
  • 逻辑比较简单:
  1. 根据key得到的hash值,取余得到数组下标。下标处如果不存在元素,那么返回null。
  2. 存在元素,是不是头结点??是返回。
  3. 不是头结点,是不是红黑树节点? 是就是去红黑树中查找(红黑树遍历~)。
  4. 不是头结点,不是红黑树节点,那么直接遍历链表,找到返回。

2.4 删除元素 remove() 方法

 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;
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (p = tab[index = (n - 1) & hash]) != null) {
            //得到hash取余对应数组下标的头结点
            Node<K,V> node = null, e; K k; V v;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                // 就是头结点
                node = p;
            else if ((e = p.next) != null) {
            //不是头结点,则判断next节点是不是红黑树节点
                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)))) {
                           //链表,找到节点
                            node = e;
                           
                            break;
                        }
                        p = e;
                    } while ((e = e.next) != null); //链表的遍历
                }
            }
            if (node != null && (!matchValue || (v = node.value) == value ||
                                 (value != null && value.equals(v)))) {
                                 //开始移除逻辑
                if (node instanceof TreeNode)
                // 走树节点的移除逻辑
                    ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
                else if (node == p)
                    tab[index] = node.next; // 如果是数组下标对应的头结点,那么直接删除头结点
                else
                    p.next = node.next; // 正常的节点node,指向node的next节点。删除node节点
                ++modCount;
                --size; //容量更新
                afterNodeRemoval(node);
                return node;
            }
        }
        return null;
    } 
  • 总结

注释已经很清楚啦~ ~,简单总结如下:

  1. 根据hash值对长度取余得到下标
  2. 如果是头结点,得到node变量
  3. 如果不是头结点,是不是红黑树节点? 是就去红黑树中查找,得到node
  4. 链表节点? 则直接遍历链表,得到node
  5. 找到node,进行删除操作。

2.5 遍历元素

2.5.1 三种方式遍历

 HashMap<String, String> map = new HashMap<>();
 
 map.entrySet().iterator() //1,从EntrySet维度
 map.keySet().iterator()// 2,从key的维度
 map.values().iterator()// 3,从values维度 

三个维度的遍历都通过iterator迭代器实现遍历。

提出一个问题:entrySet、keySet、values到底是啥

2.5.2 map.entrySet()

 //返回所有键值对数据。  EntrySet
  public Set<Map.Entry<K,V>> entrySet() {
        Set<Map.Entry<K,V>> es;
        return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
    } 

返回的是HashMap中的所有键值对集合(Set类型)抽象视图(理解为所有键值对即可)!EntrySet中没有重复数据,支持是否包含、删除、迭代等功能。

final class EntrySet extends AbstractSet<Map.Entry<K,V>> {
    public final int size()                 { return size; }
    public final void clear()               { HashMap.this.clear(); }
    public final Iterator<Map.Entry<K,V>> iterator() {
         //迭代器 
        return new EntryIterator();
    }
    public final boolean contains(Object o) {
        if (!(o instanceof Map.Entry))
            return false;
        Map.Entry<?,?> e = (Map.Entry<?,?>) o;
        Object key = e.getKey();
        Node<K,V> candidate = getNode(hash(key), key);
        return candidate != null && candidate.equals(e);
    }
    public final boolean remove(Object o) {
        if (o instanceof Map.Entry) {
            Map.Entry<?,?> e = (Map.Entry<?,?>) o;
            Object key = e.getKey();
            Object value = e.getValue();
            return removeNode(hash(key), key, value, true, true) != null;
        }
        return false;
    }
    public final Spliterator<Map.Entry<K,V>> spliterator() {
        return new EntrySpliterator<>(HashMap.this, 0, -1, 0, 0);
    }
    // ...省略
} 

2.5.3 map.keySet()

public Set<K> keySet() {
    Set<K> ks = keySet;
    if (ks == null) {
        ks = new KeySet();
        keySet = ks;
    }
    return ks;
} 

返回的是HashMap中的所有键集合(Set类型)的抽象视图!KeySet中没有重复数据,支持是否包含、删除、迭代等功能。

final class KeySet extends AbstractSet<K> {
    public final int size()                 { return size; }
    public final void clear()               { HashMap.this.clear(); }
    //迭代器
    public final Iterator<K> iterator()     { return new KeyIterator(); }
    public final boolean contains(Object o) { return containsKey(o); }
    public final boolean remove(Object key) {
    //移除也是通过HashMap的函数
        return removeNode(hash(key), key, null, false, true) != null;
    }
    public final Spliterator<K> spliterator() {
        return new KeySpliterator<>(HashMap.this, 0, -1, 0, 0);
    }
    // 省略...
} 

2.5.4 map.values()

public Collection<V> values() {
    Collection<V> vs = values;
    if (vs == null) {
        vs = new Values();
        values = vs;
    }
    return vs;
} 

返回的是HashMap中的所有值的集合(Collection类型)的抽象视图!Values中可以有重复数据,支持是否包含、迭代等功能。

final class Values extends AbstractCollection<V> {
    public final int size()                 { return size; }
    public final void clear()               { HashMap.this.clear(); }
    public final Iterator<V> iterator()     { return new ValueIterator(); }
    public final boolean contains(Object o) { return containsValue(o); }
    public final Spliterator<V> spliterator() {
        return new ValueSpliterator<>(HashMap.this, 0, -1, 0, 0);
    }
    public final void forEach(Consumer<? super V> action) {
        Node<K,V>[] tab;
        if (action == null)
            throw new NullPointerException();
        if (size > 0 && (tab = table) != null) {
            int mc = modCount;
            // Android-changed: Detect changes to modCount early.
            for (int i = 0; (i < tab.length && modCount == mc); ++i) {
                for (Node<K,V> e = tab[i]; e != null; e = e.next)
                    action.accept(e.value);
            }
            if (modCount != mc)
                throw new ConcurrentModificationException();
        }
    }
} 

2.5.5 迭代逻辑 HashIterator

abstract class HashIterator {
       Node<K,V> next;        // next entry to return
       Node<K,V> current;     // current entry
       int expectedModCount;  // for fast-fail
       int index;             // current slot

       HashIterator() {
           expectedModCount = modCount;
           Node<K,V>[] t = table;
           current = next = null;
           index = 0;
           
           if (t != null && size > 0) { // advance to first entry
               do {} while (index < t.length && (next = t[index++]) == null);
           }
           // 1,next是table数组中第一个有值的Node节点。
       }

       public final boolean hasNext() {
           return next != null;
       }
       // 迭代的核心方法
       final Node<K,V> nextNode() {
           Node<K,V>[] t;
           Node<K,V> e = next;
           if (modCount != expectedModCount)
               throw new ConcurrentModificationException();
           if (e == null)
               throw new NoSuchElementException();
               //如果下一个不为null则返回,直到链表或红黑树的末尾
           if ((next = (current = e).next) == null && (t = table) != null) {
               //已经遍历完上一个节点,接着往数组下一个位置遍历。知道数组最后一个位置
               do {} while (index < t.length && (next = t[index++]) == null);
           }
           //下一个不为null则返回.
           return e;
       }

       public final void remove() {
           Node<K,V> p = current;
           if (p == null)
               throw new IllegalStateException();
           if (modCount != expectedModCount)
               throw new ConcurrentModificationException();
           current = null;
           K key = p.key;
           removeNode(hash(key), key, null, false, false);
           expectedModCount = modCount;
       }
   }

   final class KeyIterator extends HashIterator
       implements Iterator<K> {
       public final K next() { return nextNode().key; }
   }

   final class ValueIterator extends HashIterator
       implements Iterator<V> {
       public final V next() { return nextNode().value; }
   }

   final class EntryIterator extends HashIterator
       implements Iterator<Map.Entry<K,V>> {
       public final Map.Entry<K,V> next() { return nextNode(); }
   } 
  • 总结

继承关系很清晰。KeyIterator、ValueIterator、EntryIterator都继承了HashIterator,实现了next方法。本质都是在调用nextNode方法,返回Node、key、或者value不同而已。

迭代具体逻辑:

  1. 在构造方法中,会遍历数组,找到数组中第一个有元素的下标位置,拿到头结点。
  2. nextNode方法被调用,如果map为空,则会抛出异常。不为空,则根据next节点获取下一个节点(不管是链表还是红黑树都是通过next指针来获取)。
  3. 随着nextNode方法不断调用,一直遍历到拉链的末尾节点为null,表示当前下标位置的元素已经遍历完。
  4. 接着下标加1, 往数组下一个位置获取头结点,如果不为空则按照第3步的逻辑再获取next节点。

三、与其他hash表的区别

后面再补充~

至此,HashMap的源码基本学习完毕,期间收获还是蛮多的。如果其中有什么错误之处,还请指正,共同进步~

参考:

搞懂 Java HashMap 源码

HashMap 源码详细分析(JDK1.8)

面试必备:HashMap源码解析(JDK8)

树都是通过next指针来获取)。
3. 随着nextNode方法不断调用,一直遍历到拉链的末尾节点为null,表示当前下标位置的元素已经遍历完。
4. 接着下标加1, 往数组下一个位置获取头结点,如果不为空则按照第3步的逻辑再获取next节点。

三、与其他hash表的区别

后面再补充~

至此,HashMap的源码基本学习完毕,期间收获还是蛮多的。如果其中有什么错误之处,还请指正,共同进步~

参考:

搞懂 Java HashMap 源码

HashMap 源码详细分析(JDK1.8)

面试必备:HashMap源码解析(JDK8)

最后

按照国际惯例,给大家分享一套十分好用的Android进阶资料:《全网最全Android开发笔记》。

整个笔记一共8大模块、729个知识点,3382页,66万字,可以说覆盖了当下Android开发最前沿的技术点,和阿里、腾讯、字节等等大厂面试看重的技术。

图片

图片

因为所包含的内容足够多,所以,这份笔记不仅仅可以用来当学习资料,还可以当工具书用。

如果你需要了解某个知识点,不管是Shift+F 搜索,还是按目录进行检索,都能用最快的速度找到你要的内容。

相对于我们平时看的碎片化内容,这份笔记的知识点更系统化,更容易理解和记忆,是严格按照整个知识体系编排的。

(一)架构师必备Java基础

1、深入理解Java泛型

2、注解深入浅出

3、并发编程

4、数据传输与序列化

5、Java虚拟机原理

6、高效IO

……

图片

(二)设计思想解读开源框架

1、热修复设计

2、插件化框架设计

3、组件化框架设计

4、图片加载框架

5、网络访问框架设计

6、RXJava响应式编程框架设计

……

图片

(三)360°全方位性能优化

1、设计思想与代码质量优化

2、程序性能优化

  • 启动速度与执行效率优化
  • 布局检测与优化
  • 内存优化
  • 耗电优化
  • 网络传输与数据储存优化
  • APK大小优化

3、开发效率优化

  • 分布式版本控制系统Git
  • 自动化构建系统Gradle

……

图片

(四)Android框架体系架构

1、高级UI晋升

2、Android内核组件

3、大型项目必备IPC

4、数据持久与序列化

5、Framework内核解析

……

图片

(五)NDK模块开发

1、NDK开发之C/C++入门

2、JNI模块开发

3、Linux编程

4、底层图片处理

5、音视频开发

6、机器学习

……

图片

(六)Flutter学习进阶

1、Flutter跨平台开发概述

2、Windows中Flutter开发环境搭建

3、编写你的第一个Flutter APP

4、Flutter Dart语言系统入门

……

图片

(七)微信小程序开发

1、小程序概述及入门

2、小程序UI开发

3、API操作

4、购物商场项目实战

……

图片

(八)kotlin从入门到精通

1、准备开始

2、基础

3、类和对象

4、函数和lambda表达式

5、其他

……

图片

好啦,这份资料就给大家介绍到这了,有需要详细文档的小伙伴,可以微信扫下方二维码免费领取哈~

图片

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值