HashMap源码解析

想要了解HashMap集合,先来了解其中的哈希值计算原理

为何哈希?

Hash也称散列、哈希,其中基本原理就是把任意长度的输入,通过Hash算法变成固定长度的输出。这个映射的规则就是对应的Hash算法,而这个输入出后的固定长度的结果(二进制串)就是哈希值。但是由于通过Hash算法产生的哈希值是有限的,而存储的数据可以说是无限的。这样就可能会导致存储的数据不同,而两者产生的哈希值却相同,此情况称为哈希冲突

HashMap的如何解决哈希冲突?

为了更好的解决这个问题,我们先来了解HashMap类中的基本信息属性和数据结构。

// 哈希表,数组结构
transient Node<K,V>[] table;
// 默认的哈希表数组容量大小
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 16
// 哈希表数组最大容量
static final int MAXIMUM_CAPACITY = 1 << 30; // 1073741824

// 默认负载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 通过map的构造方法手动指定的负载因子
final float loadFactor;

// 哈希表扩容临界值,它的值 = 哈希表当前容量 * 负载因子。当map中的存放的键值对大于threshold时,则进行数组扩容
int threshold;

// 针对于链表来说,树化(即链表转换成红黑树)的临界值。当链表中的结点数量大于等于8时,满足树化的条件之一
static final int TREEIFY_THRESHOLD = 8;
// 解除树化的临界值,即红黑树转换回链表
static final int UNTREEIFY_THRESHOLD = 6;
// 针对于哈希表来说,树化(即链表转换成红黑树)的临界值。当哈希表中的容量大于等于64时,满足树化的条件之一
static final int MIN_TREEIFY_CAPACITY = 64;

// map中的key集合
transient Set<Map.Entry<K,V>> entrySet;

// 记录map中元素数量
transient int size;

// 记录map中存储结构改变次数
transient int modCount;

以上属性便是HashMap中的基本信息,但只是单单查看这几个属性是不足以了解HashMap的内部结构的。为此,请看下图:
在这里插入图片描述
通过结合属性讲解和观察上图,相信大家都对HashMap的内部存储结构有了初步的了解。为了更加深入HashMap,我们通过下图再来看下它是如何决定数据保存的位置的:
在这里插入图片描述

到此,可以得知HashMap中就是通过链表或者红黑树的方式解决哈希冲突问题。

哈希表table为何要扩容?

从以上内容可以了解到,哈希冲突的问题可以通过转换成链表或者红黑树结构就可以解决。换个角度想,只有哈希表table是数组,有长度的限制,但是链表和红黑树的话,其中的结点只通过前后指针的指向来寻找前任或后继节点,不存在长度限制问题。
也就是说,即使发生了很多哈希冲突,只要遵循HashMap的存储数据规则,在硬件条件允许的情况下,就不存在数据溢出的问题。那么为什么还要对哈希表进行扩容处理呢?
这就要说到,数组和链表的特性了。

  • 数组:查询快、增删慢
  • 链表:查询慢、增删快

要知道,之所以采用数组+链表+红黑树这样的实现结构,就是为了提高键值对数据存取的效率(链表树化也是同样的道理)。
如果哈希表不扩容的话,默认的容量只有16,当数据量一大,这区区的16就变得微不足道了。而此时,整体的结构除了那只拥有16个长度的哈希表之外,就只有链表了,那么又回到了链表的查询效率过慢的问题。
因此,为了避免这种情况,哈希表扩容志在必行。

HashMap中的链表内部类Node

在了解底层方法的源码之前,先来了解下其中的数据的封装形式——链表结点。
(当然还有红黑树结点,但是要了解红黑树的话,就要从红黑树的各种特性开始讲起,这样又是另一篇博文了。因此在此只了解链表的具体实现,对于红黑树的实现,在此文中不做详细的说明)

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash; // 该结点中key所生成的hash值
    final K key; // key值
    V value; // value值
    Node<K,V> next; // 指向下一个拥有同样hash值的结点

    Node(int hash, K key, V value, Node<K,V> next) {
        this.hash = hash;
        this.key = key;
        this.value = value;
        this.next = next;
    }

    public final K getKey()        { return key; }
    public final V getValue()      { return value; }
    public final String toString() { return key + "=" + value; }

	/**
	* 获取该结点的hash码
	*/
    public final int hashCode() {
        return Objects.hashCode(key) ^ Objects.hashCode(value);
    }

	/**
	* 设置新的value值,并返回旧的value值
	*/
    public final V setValue(V newValue) {
        V oldValue = value;
        value = newValue;
        return oldValue;
    }

	/**
	* 比较指定结点的key、value是否相等
	*/
    public final boolean equals(Object o) {
        if (o == this)
            return true;
        if (o instanceof Map.Entry) {
            Map.Entry<?,?> e = (Map.Entry<?,?>)o;
            if (Objects.equals(key, e.getKey()) &&
                Objects.equals(value, e.getValue()))
                return true;
        }
        return false;
    }
}

HashMap常用重点方法源码解析:

put——添加键值对

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true); // 添加键值对
}

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); // 返回由key所生成的hash值
}

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) // 哈希table为空,或者长度为0,说明未初始化
        n = (tab = resize()).length; // 初始化哈希table,并返回容量长度
    if ((p = tab[i = (n - 1) & hash]) == null) // 根据路由算法计算数据存储索引位置,且索引位置尚无数据
        tab[i] = newNode(hash, key, value, null); // 将该数据添加到索引位置,添加数据成功,结束
    else { // 索引位置已经有数据了,说明出现哈希冲突
        Node<K,V> e; K k;
        if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) // 与已存在节点p做比较,hash值、key值相等(之后会进行value值替换)
            e = p; // 临时结点e指向已存在的结点p
        else if (p instanceof TreeNode) // 已存在的结点p的类型为红黑树结点类型,说明该链表已树化
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); // 调用红黑树添加结点的方法
        else { // 否则,需要将数据节点添加到链表末尾
            for (int binCount = 0; ; ++binCount) { 
                if ((e = p.next) == null) { // 结点p的后继结点为空,说明p为末尾结点
                    p.next = newNode(hash, key, value, null); // 添加新节点作为p的后继结点
                    if (binCount >= TREEIFY_THRESHOLD - 1) // 此时判断,链表的长度大于等树化临界值
                        treeifyBin(tab, hash); // 链表树化
                    break;
                }
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k)))) // 在链表中找到与hash值、key值相等的节点,跳出循环(之后会进行value值替换)
                    break;
                p = e;
            }
        }
        if (e != null) { // 临时节点e不为空,说明存在hash值、key值相等的节点,则替换value值
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    if (++size > threshold) // map中存放的键值对数量大于扩容临界值
        resize(); // 哈希表扩容
    afterNodeInsertion(evict);
    return null;
}

以上put方法添加数据的源码,搭配下图理解效果更佳:
在这里插入图片描述

treeifyBin——树化链表

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) // 此处判断是重点。当哈希表的容量小于树化临界值,说明无须树化(因此,树化的条件有两个:链表长度超8;哈希表容量超64)
        resize(); // 扩容即可
    else if ((e = tab[index = (n - 1) & hash]) != null) { // 否则,说明哈希表的容量已超过树化临界值,进行树化
        TreeNode<K,V> hd = null, tl = null;
        do {
            TreeNode<K,V> p = replacementTreeNode(e, null);
            if (tl == null)
                hd = p;
            else {
                p.prev = tl;
                tl.next = p;
            }
            tl = p;
        } while ((e = e.next) != null);
        if ((tab[index] = hd) != null)
            hd.treeify(tab);
    }
}

resize——调整哈希表table容量

final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    if (oldCap > 0) { // 原哈希表table容量oldCap大于0,说明哈希表已初始化,进行哈希表扩容
        if (oldCap >= MAXIMUM_CAPACITY) { // oldCap大于指定的最大容量
            threshold = Integer.MAX_VALUE; // 扩容临界值只能给整型的最大值了
            return oldTab; 
        }
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY) // oldCap大于等于默认容量,而且将其增加2倍后仍小于指定最大容量
            newThr = oldThr << 1; // 则将扩容临界值增加2倍
    }
    else if (oldThr > 0) // oldCap小于等于0,说明哈希表未初始化
        newCap = oldThr; // 将新容量设置为原扩容临界值
    else { // 否则,则使用默认的容量和扩容临界值
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    if (newThr == 0) { // 新扩容临界值为0,则使用 新容量 * 负载因子 作为新扩容临界值
        float ft = (float)newCap * loadFactor;
        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) { // oldTab不为空,说明原哈希表已初始化过,则将原哈希表中的数据全部移动到新哈希表中
        for (int j = 0; j < oldCap; ++j) { // 循环原哈希表中的结点
            Node<K,V> e;
            if ((e = oldTab[j]) != null) { // 原哈希表中的元素不为空,则将元素作为临时节点e保留下来
                oldTab[j] = null; // 清空原哈希表
                if (e.next == null) // 结点e的后继结点为空,说明该位置没有出现过哈希冲突
                    newTab[e.hash & (newCap - 1)] = e; // 则直接将该结点添加到新哈希表即可
                else if (e instanceof TreeNode) // 结点e类型为红黑树结点,说明该链表已经树化
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); // 则调用红黑树迁移结点方法
                else { // 否则,说明需要迁移整条链表的结点
 					
 					/**
 					* 此处需要额外说明:因为迁移整条链表的话,并不是我们想象中的那样,整条链表原封不动的迁移
 					* 而实际上,在链表迁移的过程中,它已经被拆分成两条新的链表进行添加
 					* 在这里,我们称这两条链表为lo链表、hi链表
 					* 想知道具体原因的朋友可以访问如下链接:
 					* https://blog.youkuaiyun.com/weixin_41565013/article/details/93190786
 					*/
                    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) { // 此处判断等于0,说明节点e属于lo链表,使用尾插法将其插入lo链表中
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        else { // 否则,属于hi链表,使用尾插法将其插入hi链表中
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    // 最后将lo链表、hi链表添加到新哈希表中
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

get——根据key值获取value值

public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

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) { // 通过路由算法找到对应的结点first
        if (first.hash == hash &&
            ((k = first.key) == key || (key != null && key.equals(k)))) // 先查看firs结点是否是要找的目标结点
            return first; // 找到直接返回
        if ((e = first.next) != null) { // first不是要找的目标结点,则继续往后继结点寻找,找到后返回
            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——根据key值删除元素

public V remove(Object key) {
    Node<K,V> e;
    return (e = removeNode(hash(key), key, null, false, true)) == null ? null : e.value;
}

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) { // 通过路由算法找到对应的结点p
        Node<K,V> node = null, e; K k; V v;
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k)))) // 首先查看结点p是否是要删除的目标结点
            node = p; // 找到后,则用临时结点node指向链表头结点
        else if ((e = p.next) != null) { // 结点p不是目标结点,则继续往后继结点寻找,找到后用临时结点node指向目标结点
            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;
            ++modCount;
            --size;
            afterNodeRemoval(node);
            return node;
        }
    }
    return null; // 找不到目标结点,删除失败,返回空
}

到此,HashMap的源码解析就结束了。本文针对了HashMap的原理,以及增、删、查方法进行源码的解析,其他剩余的方法就不一一列举讲解了。如果能够看懂这篇博文,那么就可以举一反三,一通百通了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值