HashMap 分析

HashMap 在 JDK1.8 之前使用了 “数据 + 链表” 的结构,数组是 HashMap 的主体,链表主要是解决 哈希冲突而处在的。在 JDK 1.8 以后采用了 “数组+链表+红黑树” 的结构,当链表长度大于阈值,就可以将链表转化成红黑树。

如果所示这是 JDK1.7 基于 “数据+链表” 的 HashMap 结构。
在这里插入图片描述

在 JDK 1.8 中,当链表长度大于阈值时,就会将 “链表” 转化成 “红黑树”。
在这里插入图片描述

本文仅基于 JDK1.8 的源码进行分析。

首先看看 HashMap 的结构,如源码:

tatic class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;
}

HashMap 的节点由四个元素组成,分别是 hash,key,value,以及指针(用于指向下一个节点)。

HashMap 在 put 元素时,

  1. 首先需要判断数组 table 的长度,如果首次判断为空,需要对数组长度进行初始化操作;
  2. 如果当前 node 不存在,将需要新建一个 node 节点来保存元素;
  3. 如果 node 存在,那么需要判断该节点元素是需要进行 “替换”,“红黑树插入”,还是 “链表插入”。
    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;
         // put 元素对于 HashMap 而言是新增加一个 Node 节点。
         // (n - 1) & hash 来计算得到数组下标
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            // 如果 key 和 hash 是一模一样的,表明插入的节点已经存在,就可以将 oldNode 进行替换 
            if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
              // 如果是红黑树的结构,就可以对应的方式进行 Node 的插入。
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
              // 如果是链表结构,就以链表的方式进行 Node 的插入。
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
						// 如果链表的长度大于 TREEIFY_THRESHOLD, 就需要将链表转化成红黑树
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        	// 树化操作
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
			//如果插入的节点已经存在,就可以直接将节点的 value 进行替换操作。同时可以返回 oldValue 的值。
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        
        ++modCount;
        // 当数组的长度超过阈值,那么需要重新分配数组的大小。threshold = 容量 * 负载因子
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

在阅读源码之前还需要了解

// HashMap 默认数组容量
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

// HashMap 默认负载因子,则首次扩容在当 threshold > 12 时,即进行数组的扩容
static final float DEFAULT_LOAD_FACTOR = 0.75f;

// HashMap 数组容量的最大值
static final int MAXIMUM_CAPACITY = 1 << 30;
  • 为什么每次容量扩容后得大小是 2 的幂次方

为了保证数据元素在存取时,尽量减少碰撞,保证数据元素尽可能的分配均匀。由于 HashMap 的元素的存储是根据 (n - 1) & hash 来计算下标。也就是先对数组长度取模运算,得到的余数是要存放元素位置的数组下标。“取余(%)操作中如果除数是2的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length == hash&(length-1) 的前提是 length 是2的 n 次方”。

所以,如果链表长度小于 TREEIFY_THRESHOLD,那么存储方式类似于这样:

  1. 先判断 hash,如果不存在任何节点,则直接存放在数组上;
  2. 如果 hash 存在相等的情况,再判断 key 值是否存在;
  3. 如果 key 不存在,则生成一个新 Node,在前一个链表 Node 尾部添加;
  4. 如果 key 存在,则仅需对 Node 的值进行替换。
    在这里插入图片描述
  5. 当链表长度大于默认值 8,那么链表将转化成红黑树,红黑树以实现 O(log n)时间复杂度内查找元素;
    在这里插入图片描述
  • HashMap 的 hash 实现

已经知道 hash 是进行取余来得到数组的下标位置的。由于容量采用的是 2 的幂次方,因此 hash集合可能会发生冲突。所以 hashCode 采用了 高 16 位 与 低 16 位进行异或计算,得到 hash。然后再对 HashCode 进行取余计算 下标 = (n-1)&hash。

    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
  • 扩容过程
    当数组的大小超过阈值(3/4 * capacity)时,此时 HashMap 的数组就需要扩容了。同时需要对所有节点重新计算哈希值。 此时新位置可能是原下标的位置,或者是(原下标 + 原容量)的位置。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值