hashMap底层实现

HashMap数据结构

hashMap的概念

HashMap 是一个利用散列表(哈希表)原理来存储元素的集合,是根据Key value而直接进行访问的数
据结构

在 JDK1.7 中,HashMap 是由 数组+链表构成的。
在 JDK1.8 中,HashMap 是由 数组+链表+红黑树构成

hashMap变化历程

jdk1.7数据结构:基于链表+数组,总所周知链表时间复杂度为O(n),当链表过长每个节点进行equals判断很消耗性能
在这里插入图片描述

jdk1.8数据结构:基于链表+红黑树+数组,基于红黑树的添加查询性能(大的放右边,小的放左边,相等的不存)一定程度上形成了有序,进行了查询业务赋能大大优化
在这里插入图片描述
链表和红黑树相互转化的临界点条件:链表长度大于8(红黑树结点<6)&&数组长度大于64 两者缺一不可,才会发生树化反应,否则只会扩容操作

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)
            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);
        }
    }
HashMap深度源码探究

目标:
通过阅读HashMap源码,我们可以知道以下几个问题在源码是如何解决的?
(1)HashMap的底层数据结构是什么?
(2)HashMap中put操作的底部实现原理是什么?
(3)HashMap是如何解决hash冲突的?(寻址计算+hash运算流程)

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //16,默认容量:左位移4位,即
//2的4次幂
static final int MAXIMUM_CAPACITY = 1 << 30;//最大容量:即2的30次幂
static final float DEFAULT_LOAD_FACTOR = 0.75f;//负载因子:扩容使用,统计学计算出
的最合理的
static final int TREEIFY_THRESHOLD = 8;//链表转红黑树阈值
static final int UNTREEIFY_THRESHOLD = 6;//当链表的值小<6, 红黑树转链表
..transient Node<K,V>[] table;//HashMap中的数组,中间状态数据;不参与序列化(重点)
transient Set<Map.Entry<K,V>> entrySet;//用来存放缓存,中间状态数据;
transient int size;//size为HashMap中K-V的实时数量(重点),中间状态数据;
transient int modCount;//用来记录HashMap的修改次数
int threshold;//扩容临界点;(capacity * loadFactor)(重点)
final float loadFactor;//负载因子 DEFAULT_LOAD_FACTOR = 0.75f赋值

这里TREEIFY_THRESHOLD = 8和UNTREEIFY_THRESHOLD = 6和DEFAULT_LOAD_FACTOR = 0.75f都是基于泊松分布实现的。

泊松分布

泊松分布(法语:loi de Poisson,英语:Poisson distribution)又称Poisson分布帕松分布布瓦松分布布阿松分布普阿松分布波以松分布卜氏分布帕松小数法则(Poisson law of small numbers),是一种统计概率学里常见到的离散概率分布,由法国数学家西莫恩·德尼·泊松在1838年时发表。

泊松分布适合于描述单位时间内随机事件发生的次数的概率分布。如某一服务设施在一定时间内受到的服务请求的次数,电话交换机接到呼叫的次数、汽车站台的候客人数、机器出现的故障数、自然灾害发生的次数、DNA序列的变异数、放射性原子核的衰变数、激光的光子数分布等等。

泊松分布的概率质量函数为:

在这里插入图片描述
泊松分布的参数λ是单位时间(或单位面积)内随机事件的平均发生率。

HashMap的put方法(jdk1.8)
在这里插入图片描述
工作流程:

当我们进行put的操作的时候,先将你传入过来的key进行hash,(该过程可以分为3个步骤,下文会讲),得到一个索引值index,之后取得索引值会得到table[]数组的索引位置。(这里可以分为4种情况,下文会讲),根据相应的情况进行不同的put操作

hash工作流程

  1. 先根据object的hashCode的方法得到一个int类型的hash值,此时hash值有可能为负数,但总所周知,索引值肯定不能为负数,否则会出现数组越界异常

  2. 将得到的hashCode的值进行右移16位,并与其进行异或运算,主要是将高位也参与进运算,降低hash冲突

  3. 再将得到的hash值与(table.length-1) &运算,得到数组下标索引.

源码

static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
    ............
     if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null); 

从这也可以得出hashMap是可以key为null的集合

put方法可分为4种情况:

A.未产生hash碰撞

  1. 如果当前桶位没有元素,则没什么好说的,直接putValue即可

B.产生hash碰撞

  1. 如果当前桶位有元素,但没产生链化(当前桶位只有一个元素),进行equlas判断,为true则直接覆盖oldValue,这也是hashMap不能有重复的元素,如果为false,则将oldValue挂在newValue下方,形成链表

  2. 如果产生已经链化反应,跟1步骤差不多,不过多了判断是否会产生树化反应

  3. 如果当前p instanceof TreeNode,则进行红黑树的添加操作

源码实例:

 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
                   //onlyIfAbsent:true不更改现有值;evict:false表
//示table为创建状态
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
        初始化数组(or扩容)
            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;
            //如果与第一个元素(数组中的结点)的hash值相等,key相等,直接覆盖
            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) {
                //遍历到链表最后 1.生成链表 2.已经是链表()
                    if ((e = p.next) == null) {
                    //生成链表
                        p.next = newNode(hash, key, value, null);
                        //判断是否满足树化条件
                        if (binCount >= TREEIFY_THRESHOLD - 1) // 从0开始
                        //树化操作,点进这方法还需要判断数组容量是否大于最大树化容量64,否则扩容
                            treeifyBin(tab, hash);
                        break;
                    }
                    //
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // // key已经存在
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value; //用新的value值去覆盖老的value值
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount; //用来记录HashMap的修改次数
        if (++size > threshold) //map数量是否大于容量
            resize();//如果size大于threshold,就需要进行扩容
        afterNodeInsertion(evict); 
        return null;
    }

为什么hashMap要进行2幂次方运算(寻址计算)

主要是为了控制数组不发生越界异常,比如数组默认长度为16(二进制=0001 0000),table.length-1为15(0000 1111),key为0101 0101,进行&运算之后

高位跟经过减一的高位保持一样,低位最多为1111 //0000 1111(高位 低位) 0-15的数组范围,所以这样的特殊数字只能是2的幂次方

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值