HashMap源码解析

548 阅读11分钟

前言

本文对于hashmap的解析是基于JDK1.8,JDK1.8的hashmap相较于JDK1.7差别还是蛮大的,最大的改变莫过于底层数据结构由数组+链表修改为数组+链表+红黑树,由于红黑树相关的操作需要一些前置知识,所以会写一篇文章详细讲解 image.png

HashMap源码解析

重要的成员变量和方法

    // 默认初始化长度
    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;
    // 链表转红黑树的阈值:8
    static final int TREEIFY_THRESHOLD = 8;
    // 红黑树转链表的阈值:6
    static final int UNTREEIFY_THRESHOLD = 6;
    // 链表转红黑树同时还需要满足一个条件,数组长度为64
    static final int MIN_TREEIFY_CAPACITY = 64;
    static final int hash(Object key) {
      int h;
      // table较小是,h >>> 16 可以充分利用hash的高低位进行运算,不仅仅取决与hash的后面几位
      return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
    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;
    }

这个函数是一个非常巧妙的设计,我们直接举例子,

// 假如一开始传参是0000 0110 0000 0000 0000 0000 0000 0000,经过多次运算之后会变成什么样呢?
0000 0110 0000 0000 0000 0000 0000 0000
0000 0011 0000 0000 0000 0000 0000 0000 右移1位 
0000 0111 0000 0000 0000 0000 0000 0000 |运算

-----------------------------------------------------
0000 0111 0000 0000 0000 0000 0000 0000
0000 0001 1100 0000 0000 0000 0000 0000 右移2位 
0000 0111 1100 0000 0000 0000 0000 0000 |运算

-----------------------------------------------------
0000 0111 1100 0000 0000 0000 0000 0000
0000 0000 0111 1100 0000 0000 0000 0000 右移4位 
0000 0111 1111 1100 0000 0000 0000 0000 |运算

-----------------------------------------------------
0000 0111 1111 1100 0000 0000 0000 0000
0000 0000 0000 0111 1111 1100 0000 0000 右移8位 
0000 0111 1111 1111 1111 1100 0000 0000 |运算

-----------------------------------------------------
0000 0111 1111 1111 1111 1100 0000 0000
0000 0000 0000 0000 0000 0111 1111 1111 右移16位 
0000 0111 1111 1111 1111 1111 1111 1111 |运算

// 可以看到经过多次右移动和|运算,能够得到一个低位全是1的数值,再+1就变成了2的幂次方的数,是一个非常巧妙的设计
// 一开始cap - 1 是为了应对传进来的值本身是2的幂次方数的情况
0000 0110 0000 0000 0000 0000 0000 0000 最开始
0000 0111 1111 1111 1111 1111 1111 1111 最终
0000 1000 0000 0000 0000 0000 0000 0000 返回值 n+1

put方法

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
  			// table还未初始化
        if ((tab = table) == null || (n = tab.length) == 0)
          	// 进行容器的初始化
            n = (tab = resize()).length;
  	// n-1:2的幂次方数高位为1,后续全部为0,减1之后,就会变成刚才为1的位置为0,后续所有值都为1,			
  	// 和任何数进行与运算,得到的结果,永远是2的幂次方减1,正好符合数组角标的范围
  	// 比如现在n为32,100000 -> 011111 与任何数进行&运算数值范围都在【0-31】
  	// 假如数组中该索引处没有元素,直接存入
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            // 代码走到这里,说明数组中该索引处已有元素
            // e 是为了暂存node节点
            Node<K,V> e; K k;
            // key重复
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
              	// 使用e暂存p节点
                e = p;
            // 如果p是红黑树节点,走添加红黑树节点的流程
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
              	// 走添加链表节点的流程
                for (int binCount = 0; ; ++binCount) {
                    // p的next节点为空,使用尾插法添加新节点
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                      	// 如果达到将链表转化为红黑树的标准,则树化,
                      	// 这里需要注意的是binCount是从0开始,TREEIFY_THRESHOLD-1为7
                      	// 链表长度(加上数组中的元素)要 >=9 才会进行树化
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    // 链表中的节点key重复
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            // 如果e不为空,说明找到了一个与插入元素key完全一致的数据,可以进行更新
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
              	// 判断是否可以进行更新
              	// onlyIfAbsent:传参,表示key重复是否允许更新
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
  	// 记录hashmap被修改的次数,key重复更新value的操作不计算在内
        ++modCount;
  	// hashmap元素+1后是否超过阈值,超过则进行扩容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

resize方法

了解resize()方法前我们首先需要带着两个问题

  1. 什么时候扩容?

    先添加元素,到达阈值则进行扩容

  2. 怎么扩容?

    下面代码会详细解释

final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
  	// oldCap:扩容前的数组容量
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
  	// oldThr:扩容前的阈值
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) {
            // 如果旧数组容量已经到达上限,则更新阈值为Integer类型的最大值,立即返回,不进行扩容
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            // oldCap << 1 左移一位,表示数组容量翻倍
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
              	// 阈值容量也翻倍
                newThr = oldThr << 1; // double threshold
        }
  	// oldCap为0,oldThr > 0是因为旧数组初始化的时候设置了阈值
  	// public HashMap() {this.loadFactor = DEFAULT_LOAD_FACTOR;} 例如这个空构造器
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
  	// oldCap为0,oldThr也为0
        else {               // zero initial threshold signifies using defaults
            // 新数组的容量设置为默认容量
            newCap = DEFAULT_INITIAL_CAPACITY;
            // 新数组的阈值设置为默认容量*0.75
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
     	    // 新的阈值为新的数组容量*负载因子(在新的阈值小于1 << 30的情况下)
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
  	// 设置新的阈值
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
  	// 创建新的数组
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        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)
                        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) {
                                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代码前需要了解一个前置知识点:数组容量是2的幂次方还有一个好处,扩容时,需要重新定位元素的索引位,扩容是把数组的容量扩大为原来的两倍,那么扩容后,数组容量还是2的幂次方,原数组中的元素在新数组中,要么在原索引位,要么在原索引位+扩容值的位置,避免了重新hash的效率问题

// 比如原数组的容量是8,索引值为2的位置处有2、6、10、14、22
0000 0000 0000 0000 0000 0000 0000 1000
// 扩容后变为16
0000 0000 0000 0000 0000 0000 0001 0000

扩容后对原数组中索引值为2处的数值重新索引
-----------------------------------------------------
0000 0000 0000 0000 0000 0000 0000 1000
0000 0000 0000 0000 0000 0000 0000 0010
0000 0000 0000 0000 0000 0000 0000 0000 与2&为0

-----------------------------------------------------
0000 0000 0000 0000 0000 0000 0000 1000
0000 0000 0000 0000 0000 0000 0000 0110
0000 0000 0000 0000 0000 0000 0000 0000 与6&为0

-----------------------------------------------------
0000 0000 0000 0000 0000 0000 0000 1000
0000 0000 0000 0000 0000 0000 0001 1110
0000 0000 0000 0000 0000 0000 0000 0000 与10&为1

-----------------------------------------------------
0000 0000 0000 0000 0000 0000 0000 1000
0000 0000 0000 0000 0000 0000 0000 1110
0000 0000 0000 0000 0000 0000 0000 0000 与14&为1

-----------------------------------------------------
0000 0000 0000 0000 0000 0000 0000 1000
0000 0000 0000 0000 0000 0000 0001 0110
0000 0000 0000 0000 0000 0000 0000 0000 与22&为0
// 原数组容量与2&为0
if ((e.hash & oldCap) == 0) {
  // loTail刚开始时为null
  if (loTail == null)
    // loHead指向e
    loHead = e;
  else
    loTail.next = e;
  // loTail也指向e
  loTail = e;
}

image.png

// 原数组容量与6&为0
if ((e.hash & oldCap) == 0) {
  // loTail已经不为null
  if (loTail == null)
    loHead = e;
  else
    // loTail指向6
    loTail.next = e;
  // loTail也指向6
  loTail = e;
}

image.png

// 原数组容量与10&不为0
else {
  // hiTail为null
  if (hiTail == null)
    // hiHead指向10
    hiHead = e;
  else
    hiTail.next = e;
  // hiTail指向10
  hiTail = e;
}

image.png

image.png

image.png

// loTail不为null的话
if (loTail != null) {
  // loTail = null,假如两条链表还相连的话,断开,将原数组中的链表一分为二
  loTail.next = null;
  // 放置在新数组中相同索引值处
  newTab[j] = loHead;
}
if (hiTail != null) {
  // 同理,断开相连的链表
  hiTail.next = null;
  // 放置在新数组中扩容后的索引值处
  newTab[j + oldCap] = hiHead;
}

image.png

get方法

   final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        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))))
                return first;
          	// 数组中该位置不止一位元素
            if ((e = first.next) != null) {
              	// 该位置已树化,在红黑树中寻找
                if (first instanceof TreeNode)
                    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;
    }

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) {
            // node用来暂存要删除的节点
            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) {
                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);
                }
            }
            // node不为空的话,说明找到了需要删除的数据	
            if (node != null && (!matchValue || (v = node.value) == value ||
                                 (value != null && value.equals(v)))) {
              	// node是红黑树节点,在红黑树中删除该节点
                if (node instanceof TreeNode)
                    ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
              	// 数组中该位置处的元素即为查找结果,将该元素的下一个元素放置到该位置处
                else if (node == p)
                    tab[index] = node.next;
                else
                // 查找结果处于链表中间,node节点位于p节点之后,p.next = node.next表示删除node节点
                // 将p指向node的下一个节点
                p.next = node.next;
                ++modCount;
              	// 维持hashmap中的节点个数
                --size;
                afterNodeRemoval(node);
                return node;
            }
        }
        return null;
    }

JDK1.7中的环形链表问题

由于jdk1.7中的环形链表是面试高频问题,所以这里顺带提一下

   // 关键代码
   void transfer(Entry[] newTable, boolean rehash) {  
        int newCapacity = newTable.length;
        for (Entry<K,V> e : table) {
            while(null != e) {  
                Entry<K,V> next = e.next;  
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);  
                }  
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }  
        }  
    }  

image.png