Hash详解

本文详细解析HashMap的工作原理,包括Entry数组、哈希计算、节点结构、put和get方法,以及扩容策略、负载因子与多线程同步。重点讨论了为何扩容一倍及位与运算的应用,以及解决多线程扩容时的环形链表问题。
摘要由CSDN通过智能技术生成

HashMap

是多少在查询
首先HashMap中维护了一个Entry(安吹)数组,数组中存储的数据类型是Entry,就是键值对,当每一个Entry要被添加到hashmap时,首先要使用hash算法计算key的hashcode,然后将hashcode进行取模操作,然后根据取模后的值来判断添加到数组中的那个位置,

数组了存储的元素是node,node 实现了map.Entry接口,

Node

node有四个属性值,并且重写了hashcode和equals方法

final int hash;//哈希
final K key;//key
V value;//value
HashMap.Node<K, V> next;//指向node的下一个节点,因为他可以被扩展成一个链表

HashMap

属性:

transient HashMap.Node<K, V>[] table;  //node数组
transient Set<Entry<K, V>> entrySet;  //用于存放map中所有的Entyr
transient int size;						//元素数量
transient int modCount;					//计数器,记录hashmap结构发生变化的次数(put、扩容)
int threshold;							//size*负载因子
final float loadFactor;					//负载因子

对象继承序列化接口后,肯能被序列化,但transient修饰符修饰后,将不会被自动序列化,用于修饰一些敏感的字段

put方法

在这里插入图片描述
注:在JDK1.8之前hashMap的插入是头插法,之后变成了尾插法。

1.判断table是否初始化,没有初始化则调用resize()table初始化容量,以及threshold的值

2…根据**table数组长度和哈希地址做&运算(i = (n - 1) & hash)**计算出该key对应的table数组索引,

3.如果对应的数组索引位置没有值,则调用newNode(hash, key, value, null)方法,为该键值对创建节点。

4.如果哈希值相等,key也相等,则是覆盖value操作。

5.如果根据哈希地址计算出该key对应的table数组索引有节点,,但是节点的键key和传入的键key不相等,则遍历链表,如果遍历过程中找到节点的键key和传入的键key相等,则将对应的value值更新。否则调用newNode(hash, key, value, null)方法,为该键值对创建节点添加到链表尾部,如果追加节点后的链表长度 >= 8,则转为红黑树

7.最后所有元素处理完成后,判断是否超过阈值;threshold,超过则扩容。

get方法

1.table不为空,且table的长度大于0,且根据键keyhashCode()计算出哈希地址,再根据桶的数量-1和哈希地址做&运算计算出数组的下标,该下标下不为空(即存有链表头指针)则继续往下进行,否则返回null

2.如果和第一个节点的哈希地址、键key都相同,则返回第一个节点。

3.如果第一个节点的下个节点不为空,则继续,如果第一个节点为树的节点,则执行getTreeNode(hash, key),在树中寻找节点,并且返回。否则遍历链表,找到键key、哈希地址一样的则返回此节点

HashMap扩容

hashmap默认的初始长度是16,负载因子是0.75.当table中的元素被使用75%以上时,触发扩容操作,并且每次扩容一倍。

扩容时:将旧数组中的元素转换后,填充到新数组中。

因为我们使用的是2次幂的扩展(指长度扩为原来2倍),哈希值位与2^n-1就相当于取哈希值后n位,扩容时n+1了,所以就相当于取哈希值的n+1位,因此,我们在扩充HashMap的时候,不需要重新计算hash,。,只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap”,所以,元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置。
在这里插入图片描述

为什么要扩容一倍(为什么table的长度是2的次幂)?

当table的长度是2的次幂时,取模操作和位与运算结果相同

而取模操作比位与耗性能(因为位与运算直接对二级制进行操作,二取模要先将hash转化为10进制),所以可以用位与运算取代取模运算

为什么hash & (capacity - 1) == hash % capacity两者结果相同?

为什么X % 2^n = X & (2^n – 1)?

从2进制角度来看,X / 8相当于 X >> 3,即把X右移3位,此时得到了X / 8的商,而被移掉的部分(后三位),则是X % 8,也就是余数。而 X & (2^n – 1)这就是取X的后N位

为什么要右移16位再异或。

(h = key.hashCode()) ^ (h >>> 16)。把哈希值右移 16 位,也就正好是自己长度的一 半,之后与原哈希值做异或运算,这样就混合了原哈希值中的高位和低位,增大 了随机性。防止不同hashCode的高位不同但低位相同导致的hash冲突。简单点说,就是为了把高位的特征和低位的特征组合起来,降低哈希冲突的概率,也就是说,尽量做到任何一位的变化都能对最终得到的结果产生影响。

.为什么数组空间大于64之后再判断链表长度是否大于8再扩容,****小于64就只是做个扩容****

当负载因子为0.75时候,产生hash碰撞记录是最小的,此时Entry单链表的长度几乎不可能超过8。时间和空间的权衡

TreeNodes占用空间是普通Nodes的两倍,所以只有当bin包含足够多的节点时才会转成TreeNodes,而是否足够多就是由TREEIFY_THRESHOLD的值决定的。当bin中节点数变少时,又会转成普通的bin。并且我们查看源码的时候发现,链表长度达到8就转成红黑树,当长度降到6就转成普通bin。

多线程下扩容产生环形链表问题

待续~

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值