hashmap底层原理_Java集合框架(四)搞定HashMap底层原理

一、结构

58d45495d80a139e50ae69bb957d79c4.png

如图所示,HashMap 底层是基于数组和链表实现的。其中有两个重要的参数:

  • 容量
  • 负载因子

容量的默认大小是 16,负载因子是 0.75,当 HashMap 的 size > 16*0.75 时就会发生扩容(容量和负载因子都可以自由调整,无论是自动扩展还是手动初始化时,必须是2的幂)。

二、方法

2.1、put 方法

首先会将传入的 Key 做 hash 运算计算出 hashcode,然后根据数组长度取模计算出在数组中的 index 下标。

由于在计算中位运算比取模运算效率高的多且Hash算法均匀分布原则,所以 HashMap 规定数组的长度为 2^n 。这样用 2^n - 1 做位运算与取模效果一致,并且效率还要高出许多index = HashCode(Key) & (Length - 1)。

下面我们以值为“book”的Key来演示整个过程:

1.计算book的hashcode,结果为十进制的3029737,二进制的101110001110101110 1001。

2.假定HashMap长度是默认的16,计算Length-1的结果为十进制的15,二进制的1111。

3.把以上两个结果做与运算,101110001110101110 1001 & 1111 = 1001,十进制是9,所以 index=9。

可以说,Hash算法最终得到的index结果,完全取决于Key的Hashcode值的最后几位。

由于数组的长度有限,所以难免会出现不同的 Key 通过运算得到的 index 相同,这种情况可以利用链表来解决,HashMap 会在 table[index]处形成环形链表,采用头插法(不是插入链表的尾部,而是头部,因为:后插入的Entry被查找的可能性更大)将数据插入到链表中。

2.2、get 方法

get 和 put 类似,也是将传入的 Key 计算出 index ,如果该位置上是一个链表就需要遍历整个链表,通过 key.equals(k) 来找到对应的元素。

遍历方式

 Iterator> entryIterator = map.entrySet().iterator();  while (entryIterator.hasNext()) {   Map.Entry next = entryIterator.next();   System.out.println("key=" + next.getKey() + " value=" + next.getValue());}
Iterator iterator = map.keySet().iterator(); while (iterator.hasNext()){   String key = iterator.next();  System.out.println("key=" + key + " value=" + map.get(key)); }

强烈建议使用第一种 EntrySet 进行遍历。

第一种可以把 key value 同时取出,第二种还得需要通过 key 取一次 value,效率较低。

三、高并发下的HashMap

在并发环境下使用 HashMap 容易出现死循环。

HashMap的容量是有限的。当经过多次元素插入,使得HashMap达到一定饱和度时,Key映射位置发生冲突的几率会逐渐提高。这时候,HashMap需要扩展它的长度,也就是进行Resize。扩容的影响因子就是Capacity(容量)、LoadFactor(加载因子),当 HashMap 的 size > 16*0.75 时就会发生扩容。

3.1、扩容

3.1.1、创建一个新的Entry数组

长度时原来的两倍。

3.1.2、ReHash方法

/** * Transfers all entries from current table to newTable. */void transfer(Entry[] newTable, boolean rehash) {    int newCapacity = newTable.length;    for (Entry e : table) {        while(null != e) {            Entry next = e.next;            if (rehash) {                e.hash = null == e.key ? 0 : hash(e.key);            }            int i = indexFor(e.hash, newCapacity);            e.next = newTable[i];            newTable[i] = e;            e = next;        }    }}

并发场景发生扩容,调用 Resize() 方法里的 Rehash() 时,容易出现环形链表。这样当获取一个不存在的 key 时,计算出的 index 正好是环形链表的下标时就会出现死循环,避免这种情况,通常采用ConcurrentHashMap。

7247bfd1f40bccfed8f1fc2c26af92cc.png

所以 HashMap 只能在单线程中使用,并且尽量的预设容量,尽可能的减少扩容。

四、优化

在 JDK1.8 中对 HashMap 进行了优化:

当 hash 碰撞之后写入链表的长度超过了阈值(默认为8),链表将会转换为红黑树

假设 hash 冲突非常严重,一个数组后面接了很长的链表,此时时间复杂度就是 O(n) 。如果是红黑树,时间复杂度就是 O(logn) ,大大提高了查询效率。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值