Java面试之Java集合5——HashMap的底层实现

JDK1.8之前(下面以1.7为例)

源码分析

添加元素到HashMap集合中的put()方法源码如下:

    public V put(K key, V value) {
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key);
        int i = indexFor(hash, table.length);
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }

注释如下:

public class HashMap7<K,V> {
    public V put(K key, V value) {
        // 判断非空,如果是空则初始化
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
        // 判断key是否为null,如果是null则单独处理
        if (key == null)
            return putForNullKey(value);
        // 重头戏来了
        // hash(key)方法通过扰动算法计算键(key)的哈希值
        // hash(key)更详细了解参考:https://blog.csdn.net/cnds123321/article/details/113745628
        int hash = hash(key);
        // indexFor()计算该对象在table数组中的位置,即数组下标
        // indexFor()更详细了解参考:https://blog.csdn.net/cnds123321/article/details/113742176
        int i = indexFor(hash, table.length);
        // 从得出的数组下标的位置开始遍历,看看key值是否存在,存在就覆盖值,并返回旧的值
        for (HashMap.Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            // 比较table[i]元素的hash值是否等于输入的参数key的hash值,并且同时判断table[i]元素的key值是否等于输入的参数key值
            // 即判断是否是重复元素
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {// 如果存在重复元素
                // 临时变量,保存以前的重复key的旧value值
                V oldValue = e.value;
                // 然后覆盖为新输入的value值
                e.value = value;
                // 该方法是留给LinkedHashMap实现的,其功能是设置按访问顺序排序时,每次访问entry就将其移至链表头部。是给LinkedHashMap使用的,暂不关注
                e.recordAccess(this);
                // 然后将旧值返回
                return oldValue;
            }
        }

        // 循环结束后没有执行return,表示HashMap中还没有这对键值对,即不存在重复元素,那么就要添加这对键值对到HashMap中
        // 记录修改次数
        modCount++;
        // 调用addEntry()方法添加该键值对到HashMap中
        addEntry(hash, key, value, i);
        return null;
    }
}

该方法中最值得注意的就是hash()方法和indexFor()方法,这里不做说明,更多详细请参考博客:

说了hash()方法和indexFor()方法后,把最后的addEntry()方法说下:

    /**
     * 向table链表数组中添加新的键值对(未与已有的键值对产生重复)
     *
     * @param hash        要被添加元素的hashCode值
     * @param key         键值对中的键(key)
     * @param value       键值对中的值(value)
     * @param bucketIndex 该元素应该放在table链表数组中的哪个位置(即数组的下标)
     */
    void addEntry(int hash, K key, V value, int bucketIndex) {
        // 如果当前 HashMap 大小已经达到了阈值,并且新值要插入的数组位置已经有元素了,那么要扩容
        if ((size >= threshold) && (null != table[bucketIndex])) {
            // 扩容,HashMap容量变为原来的2倍
            // 注意:HashMap的初始容量为16,而length必须是2^n
            resize(2 * table.length);
            // 扩容以后,重新计算 hash 值
            hash = (null != key) ? hash(key) : 0;
            // 扩容以后,也需要重新计算在数组中的存放位置(数组下标)
            bucketIndex = indexFor(hash, table.length);
        }
        // 然后才将元素插入到table链表数组中
        createEntry(hash, key, value, bucketIndex);
    }

    /**
     * 创建Entry结点,然后添加到数组对应下标位置
     *
     * @param hash        hashCode值
     * @param key         键值对中的键(key)
     * @param value       键值对中的值(value)
     * @param bucketIndex 该元素应该放在table链表数组中的哪个位置(即数组的下标)
     */
    void createEntry(int hash, K key, V value, int bucketIndex) {
        // 原链头
        HashMap.Entry<K, V> e = table[bucketIndex];
        // 使用new HashMap.Entry<>()新建一个元素,然后将新元素插入指定数组下标的链头
        table[bucketIndex] = new HashMap.Entry<>(hash, key, value, e);
        // 然后HashMap中元素个数加1
        size++;
    }

执行流程

HashMap的put()方法执行流程如下图:

JDK1.8之后(下面以1.8为例)

源码分析

还是分析put()方法,该方法的源码如下:

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

    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;
        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))))
                e = p;
            else if (p instanceof TreeNode)
                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
                            treeifyBin(tab, hash);
                        break;
                    }
                    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;
    }

注释如下:

    /**
     * 添加键值对到HashMap中
     *
     * @param hash         key所对应的哈希值
     * @param key          键值对中的键(key)
     * @param value        键值对中的值(value)
     * @param onlyIfAbsent 如果存在相同的值,是否替换已有的值,true表示替换,false表示不替换
     * @param evict        表是否在创建模式,如果为false,则表是在创建模式
     * @return 返回旧值或者null
     */
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
        HashMap.Node<K, V>[] tab;// 临时变量,用来临时存放链表数组
        HashMap.Node<K, V> p;
        int n, i;
        // 检查链表数组table是否为空,table为null或者table数组的长度为0都表示为空
        if ((tab = table) == null || (n = tab.length) == 0)
            // 如果为空则初始化,并扩容,然后返回新链表数组的长度,将长度赋值给变量n
            // resize()方法就是初始化并扩容,该方法具体请参考:
            n = (tab = resize()).length;
        // (n-1)&hash这条语句就是JDK1.7中HashMap源码中的indexFor()方法的功能,即得到该对象存放在数组中的具体位置(下标)
        // 判断该位置的元素是否为null,即是否存在元素,如果存在则表示已经发生哈希冲突,如果不存在,则添加元素结点
        if ((p = tab[i = (n - 1) & hash]) == null)
            // 表示不存在元素的情况
            // 则新添加一个元素到链表数组的对应下标位置,该结点也是链表的链头
            tab[i] = newNode(hash, key, value, null);
        else {
            // 表示存在元素的情况
            // 则发生了哈希冲突,下面的代码则是尝试解决冲突问题
            HashMap.Node<K, V> e;
            K k;
            // 判断待添加元素的hash值和key值是否同已经存在(冲突)的元素的hash值和key值同时相等
            if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
                // 如果相等,则表示两个元素相互重复了,那么使用变量e来临时存储这个重复元素
                e = p;
                // 如果不相等,表示没有重复,并且判断结点类型是否是红黑树类型
            else if (p instanceof HashMap.TreeNode)
                // 那么就将该键值对存储到红黑树中
                e = ((HashMap.TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value);
                // 如果不相等,且结点类型不是红黑树类型,那么就是链表,即采用拉链法解决冲突
            else {
                // 遍历链表中所有结点,这是一个死循环,需要通过break跳出循环
                for (int binCount = 0; ; ++binCount) {
                    // 如果p的下一个结点为null,则p是链表中的最后一个结点
                    if ((e = p.next) == null) {
                        // 则将键值对添加到最后一个结点的后面
                        p.next = newNode(hash, key, value, null);
                        // 同时binCount也是一个计数器,统计该链表已经有几个元素了
                        // TREEIFY_THRESHOLD是常量,表示阈值,默认值为8
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            // 但链表中元素个数超过了阈值,则将链表转换成红黑树
                            treeifyBin(tab, hash);
                        // 跳出循环
                        break;
                    }
                    // 判断待添加元素的hash值和key值是否同链表中已有元素的hash值和key值同时相等
                    if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
                        // 如果相等,则表示已经存在相同的键,跳出循环
                        break;
                    // 将下一个节点赋值给当前节点,继续往下遍历链表
                    p = e;
                }
            }
            // 如果e不为空,则表示已经存在重复的值,即存在hash值和key值同时相等的元素
            if (e != null) {
                // 保存旧值
                V oldValue = e.value;
                // 然后替换为新值
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                // 此函数会将链表中最近使用的Node节点放到链表末端,因为未使用的节点下次使用的概率较低
                afterNodeAccess(e);
                // 返回旧值
                return oldValue;
            }
        }
        // 记录修改次数
        ++modCount;
        // 如果添加元素后,超过阈值
        if (++size > threshold)
            // 则对HashMap进行扩容
            resize();
        // 给LinkedHashMap使用
        afterNodeInsertion(evict);
        return null;
    }

putVal()方法更详细地说明参考:

执行流程

HashMap的put()方法执行流程如下图:

 

 

 

 

 

 

 

 

 

 

 

 

参考博客:

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值