HashMap底层源码解析

 

目录

一、分析HashMap的数据结构

1.使用数组存储,加快访问速度

2.数组中的链表,解决hash冲突

3.使用红黑树优化链表,防止大量hash冲突

二、HashMap主要源码解读

三、总结


 

一、分析HashMap的数据结构

在看源码之前,了解一下它的数据结构和运行过程,才能更快更加有效率的读懂源码。

 

1.使用数组存储,加快访问速度

HashMap实际存储的是一个数组

transient Node[] table;

使用数组存储的好处有很多,最大的特点就是数组的访问速度相当快。

HashMap底层源码解析_第1张图片

每当添加/获取元素时,HashMap会根据key值的hashCode,计算出当前key对应的键值对对象(Node.class)存放在数组的哪个索引位置上。

 

2.数组中的链表,解决hash冲突

这个数组存储类型的是一个引用类型,在HashMap的内部类中,可以看一下它的源码(精简版)。

static class Node implements Map.Entry {
        //存储Key的hash码
        final int hash;
        //存储key对象
        final K key;
        //存储value对象
        V value;
        
        Node next;

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

        
    }

成员变量中除了保存着K,V,hash之外,还有一个Node变量,也就是他自己。这种组成方式,也是一种数据结构,叫做链。

链表

虽然数组的访问速度相当快,但是他的大小是固定不变的,而且每次插入元素和删除元素,其他的元素也要跟着一起发生变化,这对于经常插入或者删除元素的应用场景来说简直就是灾难。

因此也就有了另一种存储方式,叫做链,意思就是说,通过存储指针的方式,将下一个元素的地址引用,保存在上一个元素中。

根据其功能设计,又分为双向(需要保存上一个元素和下一个元素的引用地址)和单向链(仅需要保存下个元素的引用地址)。

HashMap底层源码解析_第2张图片

虽然链表的存储不连续,而且大小也不是一样的,但是只要根据引用一级一级向下查找,就能够找到你想要的任何元素。

关于链和数组的区别和优缺点在此就不多赘述了,大家在看ArrayList和LinkedList的区别的时候就基本上都看过了。

 

由上可知HashMap它的底层存储结构为:存储着链表的数组

HashMap底层源码解析_第3张图片

 

 

3.使用红黑树优化链表,防止大量hash冲突

通过数组和链表的双重结构,我们的hashMap已经可以说是很完善了,

但是如果发生大量的hash碰撞,就会导致一条甚至多条链过长的情况,这个时候由于链表的特性,会导致其获取元素的效率降低。

此时,使用树状存储结构将有效的解决该问题。

那么知道了HashMap的数据结构,可以进一步开始读取源码。

 

二、HashMap主要源码解读

ps:不想看源码就直接跳到文章末尾看总结吧。

HashMap代码的核心主要就是存储这一块了,在知道了HashMap的存储结构之后,我们大概就能猜到它的代码的流程了,这里说一下put()方法的流程(不包含HashMap的一些优化策略):

  1. 根据键值对(之后用K,V表示),计算出K的hash码(之后用hash表示)。
  2. 用hash和数组table的长度-1进行与运算,得到当前的键值对对应的数组索引位置。
  3. 如果数组的这个索引对应值为null,说明这里没有链,直接将用键值对构建一个Node对象,存储到数组该索引处。
  4. 如果不为空,说明此处已经有元素了,此时发生了hash碰撞,那么应该遍历这个链表,将该键值对添加到链表的末尾。

 

现在再来看源码吧,put()方法直接调用了putVal()方法

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

hash码的运算,进行了位运算,右移了16位(这里的意义是什么?求大佬指出)

问题已经解决,为什么要存在hash方法而不是直接使用hashCode()

    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

接着直接开始就是添加元素了,代码比较长,我们到后面一步一步拆解

1.计算得出该键值对应该存放的索引位置

HashMap第一次进行初始化是在第一次添加元素时,默认的数组长度时1<<4=16,默认的负载因子是0.75,也就是说,存放的元素超过数组的0.75就会进行重新扩容2倍(也就是new一个新的数组,长度=oldLength*2);

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        //局部变量tab = 成员变量table
        Node[] tab; 
        //局部变量p存储table[i]   i是该键值对对应的索引
        Node p; 
        //局部变量n存储数组table的length
        int n, i;

        //如果数组table=null,或者table.length=0,那么调用resize()方法,重新创建数组table并重置数组大小
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;

        //i = (n - 1) & hash:计算出该键值对对应的数组索引
        //如果(p = tab[i]) == null ,说明没有存储节点,直接new一个Node存进去
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else{
             //省略若干行代码
        }
        //省略若干行代码
        
    }

2.当发生hash碰撞时,如何处理

如果发生hash碰撞,首先判断key值是否相等,如果相等则覆盖旧值,如果不等则将该键值对添加到链表末尾。

在添加元素之后,需要判断链表的长度(1.8版本之后),如果hashMap的长度大于等于8,则会将链表转换成红黑树存储(树状存储结构在查找和删除上花费的时间更少)。

//省略上面的代码
{
            //e存储的是最后一次从链表中获取的元素索引
            Node e; K k;
            //当碰撞的k的hash码以及equals方法相同时,或者两个k是同一个对象时
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                //因为k相同,所以最后一次获取的链就是p了,直接赋值给e
                e = p;
            
            //此处涉及到了HashMap的优化(当链表达到一定长度时,改用红黑树存储),暂时跳过
            else if (p instanceof TreeNode)
                e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
            
            //这里就是哈希碰撞的车祸现场了
            else {
                //循环遍历链表,binCount保存的是链表的长度
                for (int binCount = 0; ; ++binCount) {
                    //当该链的下一链为null时
                    if ((e = p.next) == null) {
                        //构造该键值对的node,并且将引用地址保存在链表的末尾
                        p.next = newNode(hash, key, value, null);
                        
                        //此处是链表的优化,暂时跳过
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);

                        //注意,当成功添加该键值对到链表末尾之后,此时跳出循环,e=null
                        break;
                    }

                    //同上,这里也是判断K值重复操作
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            
            //有上面的代码可以知道,当成功添加之后,e=null值,
            //如果k值相同,e对应的就是那个k相同的node索引
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                //因为onlyIfAbsent是写死的false,所以此处一定会将旧的value值替换成新键值对的value值
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;

                //默认配置是啥也没干,该方法是为了HashMap的子类实现的
                afterNodeAccess(e);
                //返回旧的value值
                return oldValue;
            }
        }
        
        //省略代码

3.插入成功后,判断是否需要扩容

        //下面三个都是成员变量
        ++modCount;
        //当Map的大小足够大时,此时需要扩容
        if (++size > threshold)
            resize();
        //默认啥也没干,该方法是为了HashMap的子类实现的
        afterNodeInsertion(evict);
        
        return null;

 

 

三、总结

看到这里呢,整个HashMap的核心基本上也就看完了。

总结一下就是:

  • HashMap的底层是数组加链表。
  • HashMap会在第一次添加元素时,初始化数组核心。
  • 当发生hash碰撞时,hashMap会判断Key是否相同(引用比较||(hash&equals))三重判断,如果一样,就用Newvalue替换Oldvalue,并且返回Oldvalue
  • 如果key不同的话,hashMap会在将该键值对添加到链表的末尾。
  • 在添加完毕之后,hashMap需要先判断链表的长度是否足够长,如果超过8,会进行结构优化,使用TreeNode(红黑树)代替Node
  • 最后,HashMap会判断此时Node数量有多少个了,如果足够大(默认负载因子0.75*当前数组长度),会重新扩容为两倍(默认初始长度时1<<4,也就是16)。

你可能感兴趣的:(集合,Java)