HashMap源码轻松学会:原理剖析、初始化与2次幂原因

一、HashMap初始化原理

常规的初始化HashMap就是直接new,那么new之后到底底层做了哪些工作呢,接下来将详细展开讲解。

  • 首先new HashMap(),然后给一个默认的负载因子,就结了。
public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }
  • 然后进行put值,这时候就要思考了,put值的时候,数据是怎么存放的,我们知道它是键值对。
    那使用什么样的数据结构存放的键值对呢?

1. Node的介绍

  • Node来存储键值对,那么Node的结构一定包含key、value了。
  • 既然是HashMap,那一定也要有hash值,所以Node里面还需要存放hash值。
  • 我们知道HashMap的底层结构是数组+链表+红黑树,那么当hash冲突的时候,就需要链化或者树化,就需要记录下一个节点的位置,所以Node中会有next。

所以HashMap是用Node存储的键值对。

2. put初始值

上面我们知道了Node来存储键值对,HashMap的结构是数组+链表+红黑树,那么就要Node[]来存储各个Node。put的话,肯定是要知道put到了数组中的哪个位置的。

现在我们初始化值,先思考new HashMap,然后put一个值的话,什么逻辑?
答:既然之前是new的空map,那么就需要先给map分配空间,然后计算put值的索引位置,放到对应位置里面就行了。
在这里插入图片描述

那要是put多个值呢?
答:同样分配空间,计算put值的索引位置,判断是否位置冲突,如果冲突了那么应该如何解决?

下面就详细介绍下put一个值的过程。
按照常规开发逻辑,我们一定是要先判断HashMap是否为空的,这里用Node[]来存储各个Node值。

  • 判断Node[]是否为空,长度是否为0(我们初始化的空map,所以满足条件)
  • 确定是空map,分配空间resize()
  • 设置Node[]长度是初始值,默认长度1<<4,即16
  • 设置初始阈值,用来判断是否扩容的,负载因子初始长度,即0.7516
  • 空间分配好了,new Node[16],长度16
  • 计算put值的索引位置,计算方式是(n-1)&hash
  • 比较位置是否已经存在值,我们的情况不存在,那么就可以给node赋值了
  • node[i] = newNode(hash,key,value,null)

以上就是new一个空map之后,插入一个值的底层方法。是不是也不复杂~~~~
下面详细介绍一下计算索引位置的方法。

3. 索引位置的计算原理

代码中使用的计算公式是index=(n-1)&hash,n是数组的长度,hash是插入值的hash值
是位运算,我们能想到的第一个好处就是快,二进制效率肯定杠杠滴!!
那为什么这样算呢?
首先数组的长度都是2的次幂,那以初始化的长度举例计算一下,初始化长度是16

16:0001 0000
15:0000 1111(n-1=16-1=15)

那分别&hash值,&算法是全为1值为1,其他都是0

接下来分别例举
16&hash

16    :0001 0000            
hash  :0000 1010 
&     :0000 0000
16    :0001 0000            
hash  :0001 1010 
&     :0001 0000

16&hash值,可以看出,只能出现两种情况,一个是16,一个是0,很明显,这样值分布的是非常不均匀的,会出现大量哈希冲突问题。

15&hash

15    :0000 1111            
hash  :0000 1010 
&     :0000 1010

15&hash,可以看出,低四位都是1,那么&hash的结果,就是hash的低四位结果。那么值的范围就是0~15,相比之下,15&hash的计算结果一定是分布的更均匀的。

这里另提一点(n-1)&hash=hash%n,但是为什么不用取模呢,我想大概是因为效率原因吧。

二、手写HashMap初始化和简单赋值

  • 初始化代码
public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }
  • 赋值
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;
        // 判断数组是否为空,如果是空的,就进行扩容,给n赋值数组长度
        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);
        // 记录元素增减次数
        ++modCount;
        return null;
    }    
  • 扩容
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) {
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
        else {               // zero initial threshold signifies using defaults
            // 上面情况不存在,目前就是初始化数组容量和阈值
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        threshold = newThr;
        // 初始化数组空间
        @SuppressWarnings({"rawtypes","unchecked"})
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        return newTab;
    }

三、为什么HashMap数组长度一定是2的次幂

n=17,16&hash

16    :0001 0000            
hash  :0000 1010 
&     :0000 0000
16    :0001 0000            
hash  :0001 1010 
&     :0001 0000

n=16,15&hash

15    :0000 1111            
hash  :0000 1010 
&     :0000 1010

结论上面说过,就不复述了。

总之,HashMap 的数组长度是2的次幂,是为了提高性能、简化操作、减少哈希冲突,并在扩容时保持高效的索引计算。

  • 29
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
HashMapJava中常用的数据结构之一,它基于哈希表实现,可以快速地存取和检索数据。下面是HashMap原理初始化和扩容的详细解释: 1. 原理 HashMap是通过将key通过hash函数转换成一个索引,然后将value存储在对应的索引位置上,来实现数据的存储和检索的。当我们需要从HashMap中获取某个key对应的value时,只需要通过对key再进行hash计算,找到对应的索引位置,然后直接返回该位置上存储的value即可。 2. 初始化 HashMap初始化是通过构造函数来完成的,其中最常用的构造函数是无参构造函数和带初始容量和负载因子的构造函数。无参构造函数会使用默认的容量(16)和负载因子(0.75)来创建一个空的HashMap。带初始容量和负载因子的构造函数可以指定HashMap的初始容量和负载因子,如下所示: ``` HashMap<String, String> map = new HashMap<>(16, 0.75f); ``` 3. 扩容 当HashMap中的元素数量超过容量与负载因子的乘积时,就会触发扩容操作。扩容的过程会重新计算每个元素的索引位置,并将它们移动到新的位置上。具体的扩容过程如下: - 创建一个新的数组,容量是原数组的两倍(或者是指定的新容量)。 - 遍历原数组中的每个元素,重新计算其索引位置,并将其移动到新数组中对应的位置上。 - 释放原数组的内存空间,将新数组的引用赋值给HashMap对象。 如果HashMap中的元素数量非常大,扩容的过程可能会比较耗时。因此,在初始化HashMap时,我们应该尽量减少扩容的数,可以通过适当调整初始容量和负载因子来实现。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

陈年小趴菜

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值