学习进展-HashMap源码

前置知识了解

位与 &
(1&1=1 ,1&0=0 ,0&0=0)
位或 |
(1|1=1 ,1|0=1, 0|0=0)
位非 ~
( ~1=0 ,~0=1)
位异或 ^
(1^1=0 ,1^0=1, 0^0=0)
有符号右移 >>
在执行右移操作时,若参与运算的数字为正数,则在高位补0;若为负数,则在高位补1。
无符号右移 >>>
无论参与运算的数字为正数或为负数,在执运算时,都会在高位补0。
左移
对于左移是没有正数跟负数这一说的,因为负数在CPU中是以补码的形式存储的,对于正数左移相当于乘以2的N次幂。

HashMap 在 JDK 中的定义

HashMap继承了AbstractMap 抽象类 ,实现了Map接口 ,其中Map接口定义了键映射到值的规则,而AbstractMap类提供 Map 接口的具体实现,以此来减少实现此接口所需的工作。

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {

HashMap源码中的几个重要属性。

// 默认容量 16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;  //16
// 最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
 // 默认的填充因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//当桶(bucket)上的链表长度大于阈值(或者红黑树的边界值,默认为 8)并且当前数组的长度大于64时,此时此索引位置上的所有数据改为使用红黑树存储。
static final int TREEIFY_THRESHOLD = 8;
// 当桶(bucket)上的结点数小于这个值时树转链表
static final int UNTREEIFY_THRESHOLD = 6;
//链表转变成树之前,还会有一次判断,只有数组长度大于 64 才会发生转换。这是为了避免在哈希表建立初期,多个键值对恰好被放入了同一个链表中而导致不必要的转化。
static final int MIN_TREEIFY_CAPACITY = 64;
// 存储元素的数组,总是2的幂次倍
transient Node<k,v>[] table; 
// 存放具体元素的集
transient Set<map.entry<k,v>> entrySet;
// 存放元素的个数,注意这个不等于数组的长度。
transient int size;
// 每次扩容和更改map结构的计数器
transient int modCount;   
// 临界值 当实际大小(容量*填充因子)超过临界值时,会进行扩容
int threshold;
// 填充因子
final float loadFactor;

HashMap源码中的构造函数

HashMap(int, float)型构造函数

//一个具有指定初始容量和负载因子的空HashMap 。
public HashMap(int initialCapacity, float loadFactor) {
  			// 初始容量不能小于0,否则抛出异常
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
  			// 初始容量如何大于最大容量,将初始容量的值替换成最大容量
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
  			// 填充因子不能小于或等于0,不能为非数字,否则抛出异常
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
  			// 初始化填充因子 
        this.loadFactor = loadFactor;
  			// 初始化threshold大小
        this.threshold = tableSizeFor(initialCapacity);
    }

  说明:tableSizeFor(initialCapacity)返回大于等于initialCapacity的最小的二次幂数值。

    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;
      //该算法让最高位的1后面的位全变为1。最后再让结果n+1,即得到了2的整数次幂的值了。
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

注意

  • |=(或等于):这个符号比较少见,但是“+=”应该都见过,看到这你应该明白了。例如:a |= b ,可以转成:a = a | b。
  • >>>(无符号右移):例如 a >>> b 指的是将 a 向右移动 b 指定的位数,右移后左边空出的位用零来填充,移出右边的位被丢弃。

HashMap(int)型构造函数。

构造一个具有指定的初始容量和默认负载因子(0.75) HashMap

 public HashMap(int initialCapacity) {
 			// 调用HashMap(int, float)型构造函数
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }	

HashMap()型构造函数。

构造一个空的 HashMap ,默认初始容量(16)和默认负载因子(0.75)

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

HashMap(Map<? extends K>)型构造函数。

    public HashMap(Map<? extends K, ? extends V> m) {
      	// 构造一个空的 HashMap ,默认初始容量(16)和默认负载因子(0.75)
        this.loadFactor = DEFAULT_LOAD_FACTOR;
      	// 将m中的所有元素添加至HashMap中
        putMapEntries(m, false);
    }

 说明:putMapEntries(Map<? extends K, ? extends V> m, boolean evict)函数将m的所有元素存入本HashMap实例中。

final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
  			//获取传入的map集合的大小
        int s = m.size();
        if (s > 0) {
          	//如果HashMap没有被初始化
            if (table == null) { // pre-size
              	// s/loadFactor 计算传进来的map的长度是否要达到阈值,因为会计算出小数因此+1.0F向上取整
                float ft = ((float)s / loadFactor) + 1.0F;
                //如果计算得到的最大负载容量大于最大值,则将t赋值为最大值
              	//当不大于最大值的时候使用ft的长度
                int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                         (int)ft : MAXIMUM_CAPACITY);
              	//如果大于当前数组下次要扩容的值
                if (t > threshold)
                  //重新计算下次扩容值的大小,在下面putVal方法当中进行map数组的初始化
                    threshold = tableSizeFor(t);
            }
          //如果table已经被初始化且传入map的大小大于当前的最大负载容量则开始调整HashMap的大小
            else if (s > threshold)
                resize();
          	//将map中的元素逐一添加到HashMap中
            for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
                K key = e.getKey();
                V value = e.getValue();
                putVal(hash(key), key, value, false, evict);
            }
        }
    }

重要方法分析

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;
          // 数组是否为空
        if ((tab = table) == null || (n = tab.length) == 0)
          //初始化数组
            n = (tab = resize()).length;
  			// 计算索引的位置,(n - 1) & hash 相当于对将hash % n,根据hash得到桶的索引
        if ((p = tab[i = (n - 1) & hash]) == null)
          //当桶内没有元素的时候,实例化一个元素作为桶的第一个元素
            tab[i] = newNode(hash, key, value, null);
        else {
          //当桶内有元素时候,就需要解决hash冲突问题了  
           // e-临时节点值,用于存放当前hash值对应桶位置的节点元素
            Node<K,V> e; K k;
          //桶内的第一个元素的key值与新加入key-value的key相同的时候,用e指向p(仅仅指向)
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
              //如果此时桶内已经树化,使用putTreeVal方法加入元素,若存在相同的key的元素,则将引用返回
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
              //如果桶内是链表,则插入链表,这里使用尾插法
                for (int binCount = 0; ; ++binCount) {
                  //当前节点的下一节点为空时,新增一个节点,此时要判断一下当前链表的长度,
                    //超过指定长度要将链表转换为红黑树
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                          	//bitCount大于树化的阈值,转化为红黑树
                            treeifyBin(tab, hash);
                        break;
                    }
                  //当前节点的下一节点是我要插入的节点
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                  	//桶内存在元素的key值与新加入key-value的key相同的时候,用e指向p(仅仅指向)
                    p = e;
                }
            }
           //通过上面的code,若map中已经存在相同的key,
            //我们则将Node<K,V> e指向该key-value,即Node(TreeNode是Node的子类)
            //若e == null,则说明已经插入成功
            //若e != null, 则将e.value作为旧值返回,将e.value设置为value
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
               //putVal中的参数。若onlyIfAbsent为null或者oldValue为空时才替换,
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
  			//插入后大于阈值,使用resize()进行调整
        if (++size > threshold)
            resize();
  			// 插入后回调
        afterNodeInsertion(evict);
        return null;
    }

注:HashMap并没有直接提供putVal接口给用户调用,而是提供的put函数,而put函数就是通过putVal来插入元素的。

getNode函数

final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
  			//1)数组不为空
        //2)数组长度>0
        //3)通过hash计算出该元素在数组中存放位置的索引,而且该索引处数据不为空null
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
          //判断该数组索引位置处第一个是否为我们要找的元素 判断条件需要满足hash 和 key 相同
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
              	//如果第一个就是我们要找的,直接返回即可
                return first;
          //如果第一个不是,我们需要循环遍历,然后找数据
            if ((e = first.next) != null) {
              //如果第1个的元素是红黑树类型的节点
                if (first instanceof TreeNode)
                  //那我们需要调用红黑树的方法查找节点
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
              //如果不是,则该为链表,需要遍历查找
                do {
                  //循环判断下一个节点的hash和key是否相同
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                  //更新e为下一个
                } while ((e = e.next) != null);
            }
        }
  		//没找到返回Null
        return null;
    }

 注:HashMap并没有直接提供getNode接口给用户调用,而是提供的get函数,而get函数就是通过getNode来取得元素的。

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;
  			//如果旧数组容量>0
        if (oldCap > 0) {
          	//如果旧数组容量大于等于最大容量
            if (oldCap >= MAXIMUM_CAPACITY) {
              	//则直接修改旧数组扩容阈值为最大值
                threshold = Integer.MAX_VALUE;
              	//并返回旧数组容量,不再做其他操作
                return oldTab;
            }
          	//若旧数组容量小于最大容量且新数组容量扩大至旧数组容量的2倍后依旧小于最大容量,并且//旧数组容量大于等于默认的初始化容量16
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
              //则将新数组扩容阈值扩大至旧数组扩容阈值的2倍
                newThr = oldThr << 1; // double threshold
        }
  			//若旧数组容量小于等于0,且旧数组扩容阈值大于0(当new HashMap(0)后再put时,会走到这里)
        else if (oldThr > 0) // initial capacity was placed in threshold
          //则将旧数组扩容阈值赋给新数组容量
            newCap = oldThr;
  				//若旧数组容量和旧数组扩容阈值均不大于0,说明数组需要初始化
        else {               // zero initial threshold signifies using defaults
          	//将新数组容量设为默认初始化容量16
            newCap = DEFAULT_INITIAL_CAPACITY;
          	//将新数组扩容阈值设为默认负载因子0.75*默认初始化容量16=12
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
  			//经上述逻辑后新数组扩容阈值仍为0,说明新数组扩容阈值尚未处理过,
  			//但走到这里之前新数组容量已经被处理完了,所以需按照新数组容量*负载因子的公式重新计算
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
  			//将新数组扩容阈值赋值给HashMap的扩容阈值字段
        threshold = newThr;
  			//按照新数组容量创建新数组
        @SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
  			//将创建的新数组赋值给HashMap的数组字段
        table = newTab;
  			//若旧数组不为null,则需要将旧数组中的数组迁移到新数组中,并将旧数组各位置置为null.
        if (oldTab != null) {
          	//根据旧数组长度,循环遍历各数组索引下标
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
              	//判断每个数组索引位置对应的链表的头节点是否为空,若为空则该索引位置无数据,
              	//就不需要接下来的操作,不为空才继续往下进行处理,将该链表的数据转移赋值给新数组
                if ((e = oldTab[j]) != null) {
                  //将旧数组该位置置为null,提醒gc回收
                    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;
                            if ((e.hash & oldCap) == 0) {
                              //低位链表的末尾为null, 对于每个链表来说,说明是第一次走到这里,
                              //而且此处也只会走进来一次,因为后续会将非null的e赋值给loTail了。
                                if (loTail == null)
                                  //说明e为低位链表头节点,并将其赋给代表低位链表头节点的loHead 
                                    loHead = e;
                              //说明低位链表末尾不为null,说明至少处理过一次loTail了,
                              //即头节点肯定已经处理过了,下面应该去处理低位链表头节点的后续节点了
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {
                              //高位链表的末尾为null,对于每个链表来说,说明是第一次走到这里,
                              //而且此处也只会走进来一次,因为后续会将非null的赋值给hiTail了。
                                if (hiTail == null)
                                  //说明e为高位链表头节点,并将其赋给代表高位链表头节点的hiTail 
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                          //链表后续还有节点时,才继续处理,否则跳出循环
                        } while ((e = next) != null);
                      //低位链表尾节点不为空,说明旧数组向低位链表的数据转移已处理完,可做进一步处理
                        if (loTail != null) {
                          //要保证低位链表尾节点的后续节点为null 
                            loTail.next = null;
                          //loHead代表了低位链表的头节点,
                          //也就代表了整条低位链表(其上已经将旧数组中j索引位置上的链表里的所有节点都转移到了该低位链表上),
                          //而从前面的处理逻辑可知,低位链表移动到新数组时的索引下标位置,与在旧数组上的索引位置相同
                          //故直接将低位链表头节点赋给新数组的j索引下标位置即完成转移。
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
  		//返回处理完的新数组
        return newTab;
    }

参考链接

1.JDK1.8HashMap源码分析系列文章

2.Java集合容器面试题(2020最新版)

3.面试阿里,HashMap 这一篇就够了

4.数组扩容源码

posted @ 2021-04-09 20:19  GMGood007  阅读(66)  评论(0)    收藏  举报