天呐,原来HashMap源码学起来这么容易!

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;

    /**
     *树化阈值
     */
    static final int TREEIFY_THRESHOLD = 8;

    /**
     * 解除树化阈值
     */
    static final int UNTREEIFY_THRESHOLD = 6;

    /**
     * 最小树化阈值
     */
    static final int MIN_TREEIFY_CAPACITY = 64;
		/**
     * Node数组桶
     */
    transient Node<K,V>[] table;
		/**
     * map大小
     */
    transient int size;

    /**
     * map结构修改次数
     */
    transient int modCount;

    /**
     * 扩容阈值
     *
     * @serial
     */
    // (The javadoc description is true upon serialization.
    // Additionally, if the table array has not been allocated, this
    // field holds the initial array capacity, or zero signifying
    // DEFAULT_INITIAL_CAPACITY.)
    int threshold;
		/**
     * 负载因子,没什么好说的
     *
     * @serial
     */
    final float loadFactor;

HashMap的几种构造方法

HashMap共有四种构造方法,主要讲一下第一种:指定初始化容量和负载因子,注意(真正的容量和阈值初始化放在了putVal()时进行)。

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;
    this.threshold = tableSizeFor(initialCapacity);
}

​ 使用该构造方法时,需要传递两个参数,第一个是指定的初始化值,第二个是默认的负载因子。

​ 接着看方法内部,首先对传递的初始化容量进行判断,如果初始化容量小于0则抛出异常。若初始化容量大于最大容量,就将初始化容量设置为最大容量,(不能再大了)。第三个判断,对负载因子进行判断处理,如果负载因子小于0或者传递的参数不是数字,抛出异常。

​ 判断进行完,就要对变量进行赋值了,一个是负载因子,一个是扩容阈值。

​ 在对扩容阈值进行赋值之前,要执行tableSizeFor(initialCapacity)的方法,这个方法的作用就是根据初始容量计算出扩容阈值,这个扩容阈值留到后面有用,小伙伴们可以注意一下,来看一下这个方法。

/**
 * Returns a power of two size for the given target capacity.
 *	返回一个大于等于cap的数,这个数一定是2的n次方
 */
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次方,返回结果还是它本身,如果传入的cap不是2的n次方,就会返回一个大于它并且是2的n次方的数。

​ 举个例子吧,比如传的cap为15

​ cap = 15

​ n = cap-1 = 15-1 => 14

​ n的二进制为 0b1110

​ n |= n >>> 1

0b1110 | 0b0111 = 0b1111

​ n |= n >>> 2

0b1111 | 0b0011 = 0b1111 => 15

到此为止就可以了,因为再往下都是15了。

也就是说下次扩容的阈值是n+1=16,那么为什么最开始要把cap-1呢,是因为如果不减1,那么最后返回的扩容阈值会是传入的cap参数的二倍。


put()和putVal()方法的解析

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

​ 先看一下方法内的hash(key)。

​ 想要将一个键值对插入到HashMap中,首先需要根据key来算出hashCode,再通过哈希扰动后,得到桶数组的下标,再将值插入到对应下标,也就是(table.length-1)&hash。

假设:h=hashCode():0b 1101 0100 1110 1010 0111 0100 0101 1011

​ h >>> 16 : 0b 0000 0000 0000 0000 1101 0100 1110 1010

​ 进行异或运算后,hash值为:

​ 1101 0100 1110 1010 1010 0000 1011 0001

​ 而桶数组长度只能取2的n次方,假设长度为16,根据算法,插入下标为

​ n = table.length-1 :0000 0000 0000 0000 0000 0000 0000 1111

​ hash = 1101 0100 1110 1010 1010 0000 1011 0001

​ 只算最后四位,0011 = 3,得到桶数组下标为3。

​ 那么为什么要对key的hashCode进行高16位运算呢,这是因为如果数组table的length比较小的时候,也能保证其参与到Hash的计算中。

/**
*		扰动函数
*  作用:让key的hashCode参与高16位运算
*/
static final int hash(Object key) {
    int h;
   	// 返回hash值
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

回到put方法中,再来看一下putVal()方法,比较长,一步步分析。

/**
 * Implements Map.put and related methods.
 *
 * @param hash hash for key
 * @param key the key
 * @param value the value to put
 * @param onlyIfAbsent if true, don't change existing value
 * @param evict if false, the table is in creation mode.
 * @return previous value, or null if none
 */
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    // tab:引用当前hashmap的散列表
  	// p: 表示当前散列表的元素
  	// n: 表示散列表数组的长度
  	// i: 表示路由寻址结果
    Node<K,V>[] tab; Node<K,V> p; int n, i;
  	
  	// 延迟初始化,为了避免创建时占用内存空间而设计的。
  	// 第一步:使用table对tab进行赋值,并判断tab是否为空,长度是否为0
  	// 若为空,则对n进行赋值
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
  
  	// 插入判断
  	// 第一步:判断当前要插入的位置是否为空,如果为空,直接调用newNode生成一个新桶数组进行插入。若不为空,进行下一步判断。
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
      	// e: 不为null的话,找到了一个与当前插入的key-value一致的key元素
   			// k: 表示临时的一个key   
        Node<K,V> e; K k;
      	// 第二步插入判断:
      	// 如果当前被插入的数组位置上有元素,并且数组元素的hash值与待插入的hash值相等,而且key的值也相等,或者待插入的key不为空,并且key值等于数组中的key值
      	// 就将数组中的元素赋值给e
      	// 这一步要进行的就是同一个key并且hash值相同下的值覆盖操作
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
      	// 判断当前散列表是否为树形结构,这个以后再讲
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
          	// 根据HashMap的存储结构可以知道,这一步是对HashMap的桶数组中的链表结构进行判断,判断链表中是否有和待插入元素相匹配的。
          	// 定义for循环,循环链表
            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
                      // 树化操作  
                      treeifyBin(tab, hash);
                  	// 跳出此次循环
                    break;
                }
              	// 如果p的next元素e的哈希值等于待插入元素的哈希值,并且e的key等于待插入元素的key,或者key不为空并且待插入元素和链表的两个key相等
              	// 则进行值的覆盖操作
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
      	// 值覆盖方法
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    // 如果桶数组大小大于扩容阈值,就进行扩容。
    if (++size > threshold)
        resize();
  	// 尾插法
    afterNodeInsertion(evict);
    return null;
}

扩容方法resize()。

​ HashMap的扩容呢,其实就是创建一个新的桶数组,将旧数组的值移动到新数组中。

/**
 * Initializes or doubles table size.  If null, allocates in
 * accord with initial capacity target held in field threshold.
 * Otherwise, because we are using power-of-two expansion, the
 * elements from each bin must either stay at same index, or move
 * with a power of two offset in the new table.
 *
 * @return the table
 */
final Node<K,V>[] resize() {
  	// oldTab代表旧数组。
    Node<K,V>[] oldTab = table;
  	// 旧数组的容量,若旧数组为空,则代表该hashmap还没有使用过,所以数组容量为0,否则为数组的长度。
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
  	// oldThr:旧数组的扩容阈值
    int oldThr = threshold;
  	// 扩容后table数组的容量和扩容阈值
    int newCap, newThr = 0;
  	// 下面这个长长的if判断呢,就是给newCap和newThr赋值的一个过程。
  	// 如果旧数组容量大于0,证明含有数据,是一次正常的扩容。
    if (oldCap > 0) {
      	// 判断旧数组容量是否大于最大值
        if (oldCap >= MAXIMUM_CAPACITY) {
          	// 如果成立,就将扩容阈值赋值为Integer的最大值,也就是2的31次方减一,非常大的一个数,不可能再次扩容了
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
      	// 如果旧数组容量没有大于最大值,就将旧数组的大小左移一位并赋值给新table数组的大小,新数组的大小为旧数组的2倍
      	// 并且旧数组的大小大于等于默认初始化大小16
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
          	// 新扩容阈值是旧的2倍
            newThr = oldThr << 1; // double threshold
    }
  	// 还记得我在上面说过HashMap的初始化被推迟了吗。
  	// new HashMap(initCap,loadFactor);
  	// new HashMap(initCap);
  	// new HashMap(map);并且map中有数据
    else if (oldThr > 0) // initial capacity was placed in threshold
      	// 将旧table数组的扩容阈值作为新数组的大小
        newCap = oldThr;
    else {               // zero initial threshold signifies using default
      	// 不指定初始化大小的,那么也不会有扩容阈值
      	// 就将默认初始化大小赋值给newCap
        newCap = DEFAULT_INITIAL_CAPACITY;
      	// 负载因子*大小=扩容阈值。
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
  	// oldCap==0,oldThr==0
  	// new HashMap();
    if (newThr == 0) {
      	// newCap=16
      	// ft = 16 * 0.75
        float ft = (float)newCap * loadFactor;
      	// 如果newCap小于最大容量,并且ft也小于最大容量
      	// 就将ft赋值给newThr,否则为Integer最大值。
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
  	// 
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
  	// 根据newCap,创建出一个更长,更大的数组
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
  	// 将table数组指向newTab
    table = newTab;
  	// 说明hashMap本次扩容前,oldTab不为null
    if (oldTab != null) {
      	// 需要循环来处理旧数组的数据
        for (int j = 0; j < oldCap; ++j) {
          	// 当前node节点
            Node<K,V> e;
          	// 说明旧数组中有数据,并赋值给node节点e
            if ((e = oldTab[j]) != null) {
              	// 将oldTab置空,方便GC回收
                oldTab[j] = null;
              	// 表明当前node节点不是链表
              	// 第一种情况: 当前桶位只有一个元素,从未发生过碰撞
                if (e.next == null)
                  	// e.hash & (newCap-1) 寻址公式
                  	// 将当前node节点赋值给新数组
                    newTab[e.hash & (newCap - 1)] = e;
              	// 如果当前node节点是树结构
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
              	// 如果为链表的话,执行到这步
                else { // preserve order
                  	// 低桶位链表
                    Node<K,V> loHead = null, loTail = null;
                  	// 高桶位链表
                  	// 这里就和hashMap的长度有关系了,当长度低时,部分key的hash值只有开始的四位能参与寻址运算。当长度达到一定程度时,第五位参与运算,那么部分第五位参与运算的数值的寻址结果就会发生变化。
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        next = e.next;
                      	// 如果e的hash值与oldCap运算为0,说明e节点在扩容后为低桶位链。
                        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;
                        }//这里可以参考一下美团的一篇文章,写的非常详细
                      // https://zhuanlan.zhihu.com/p/21673805
                    } 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;
}

剩下要讲的两个方法都非常简单,一看就懂

获取容器中的节点

/**
 * Implements Map.get and related methods.
 *
 * @param hash hash for key
 * @param key the key
 * @return the node, or null if none
 */
final Node<K,V> getNode(int hash, Object key) {
    // tab表示当前散列表
    // first:桶位中的头元素
    // e:临时元素
    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) {
        // 表明当前桶内有数据,并且要查找的数据不为null
        // 第一种情况,定位到的元素即为要get的元素
        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;
}
/**
 * Implements Map.remove and related methods.
 *
 * @param hash hash for key
 * @param key the key
 * @param value the value to match if matchValue, else ignored
 * @param matchValue if true only remove if value is equal
 * @param movable if false do not move other nodes while removing
 * @return the node, or null if none
 */
final Node<K,V> removeNode(int hash, Object key, Object value,
                           boolean matchValue, boolean movable) {
    // tab表示当前散列表
    // p表示路由寻址结果对应的当前node节点
    // n表示散列表长度
    // index:下标
    Node<K,V>[] tab; Node<K,V> p; int n, index;
    // 如果当前散列表不为空并且长度大于0并且p不为空,则进入内部判断
    // 说明桶内是有数据的,进行查找并删除
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (p = tab[index = (n - 1) & hash]) != null) {

        // node:查找到的结果
        // e为当前节点的下一个元素
        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不为空的话,说明按照key查找到需要删除的数据了
        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;
            //第三种情况:将当前元素p的下一个元素 设置成 要删除元素的 下一个元素。
            else
                p.next = node.next;
            ++modCount;
            --size;
            afterNodeRemoval(node);
            return node;
        }
    }
    return null;
}
  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值