思路
本质其实就是数组+链表,我们的数组主要查询速度很快,我们每次放入元素,会验证它的位置是否冲突->如果该位置没人,我们就会进入addEntry方法,创建一个当前空节点(体现出了头插法)(比如当前i位置),是一个空节点,然后我们再将赋值的新节点平替当前i位置节点,然后next为空节点;
插入:
如果当前位置有节点,也就是不为空,就会得到它的next,比如上面的i位置,它现在是有值的,但是它的next就是之前的空节点所以当前位置节点下一位置为null,然后我们验证hash值比较,发现key、hash值都不等(毕竟是空节点)->然后我们就会再次进入addEntry方法,一样的思路,空节点会后置,此时插入成功
那么,哈希冲突是怎么解决的呢?
这里设置的很巧妙,其实我上述已经说了,当两个相同key插入时,后插入的发现已经有值了,就会对它的next进行操作,对比它的hash以及key,next为空所以插入,从而有效达到哈希冲突的解决;
如果key不相同,说明它的位置肯定就不一样,位置的判断——>用key的hash值然后位运算与数组长度与运算获得
后续操作与上述一样
public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
int hash = myHash(key);
int i = indexForTable(hash, CAPACITY);
for (MyEntry<K, V> e = table[i]; e != null; e = e.next) {
if (e.hash == hash && (e.key == key || e.key.equals(key))) {
V old = e.value;
e.value = value;
return old;
}
}
addEntry(hash, key, value, i);
return null;
}
得到位置:
根据key得到hash值,然后进行高位运算
为什么要进行高位运算?
这里体现出了HashMap的容量为什么是2的n次幂
因为hash值时整数->高位位运算得到一个很大的值,这样就保证了我们后面hash&数组-1的低位是有效的,保证了位置都在数组范围内;
模运算不是也可以吗?
如果用(hash % capicity)当然也能得到相同的结果,但是因为位运算的效率更高,所以当然要选择与运算,当然这是不一定的
(30条消息) HashMap中的hash()函数以及如何通过哈希值确定下标_KKKLxxx的博客-CSDN博客_hashmap如何计算下标
(30条消息) Java 位运算和普通运算,效率比较_Magicflowersbloom的博客-CSDN博客_位运算为什么效率高
/**
* 6.计算机key的hash值
*/
private int MyHash(K key) {
if (key == null) {
return 0;
}
int h = key.hashCode();
h = h ^ (h >>> 16);
return h;
}
为什么要h>>>16并且异或
/**
* 2.1插入key!=null的value到哈希表中
* 计算插入位置:hash&
*/
private int indexForTable(int hash, int CAPACITY) {
//hash值与数组长度进行位运算得到下标
return hash & (CAPACITY - 1);
}
扩容机制:
我们来说说源码的扩容,当hashmap中的元素个数超过数组大小 * loadFactor(负载因子)时,就会进行数组扩容,loadFactor的默认值(DEFAULT_LOAD_FACTOR)是0.75这是一个折中的取值,也就是说,默认情况下数组大小为16,那么当hashmap中的元素个数超过16*0.75 = 12 (阈值或者边界值的时候)就把数组的大小扩展为2 * 16 = 32,然后重新计算出每个元素在数组中的位置,而这是一个非常耗性能的操作,所以我们最好能够提前预知并设置元素的个数。
流程:
hashMap首先会创建一个两倍大的新数组,然后我们会将原数组中的值放在我们的新数组中——>这时候需要重新hash——>(因为我们的位置是:hash&(数组.length-1)),数组长度发生变化所以,肯定是要重新哈希的;——>这里一个很重要的点:
节点迁移1.7使用的头插法,不过多线程下会发送并发死链的现象,1.8采用的尾插法
以下代码的do-while很细节:
先得到原哈希表(数组)里的每个元素e,然后得到它的下一个元素next,不管怎么样是e,e.next还是怎么着,他们的hashcode肯定是一样的,那么重新hash肯定也是一样的
那么我们对e重新hash确定位置,位置为i,此时新哈希表newtable当前位置肯定是null,我们将这个值赋予e.next,e.next=null,这样的目的和插入addEntry方法一样,就是为了让e的下一个节点为null,然后新哈希表当前位置i为e(newtable[i]=e),然后将Entry链上下一个值给到e(e=next)——>此时e就是第二个节点了,一直循环至e!=null
private void resize() {
CAPACITY= CAPACITY* 2;
MyEntry[] newtable = new MyEntry[CAPACITY];
for (Entry<K, V> entry : table) {
MyEntry<K, V> e = entry; // 取得旧Entry数组的每个元素
if (e != null) {
entry = null;// 释放旧Entry数组的对象引用(循环后,旧的Entry数组不再引用任何对象)
do {
MyEntry<K, V> next = e.next;
int i = indexForTable(e.hash, capacity); // 重新计算每个元素在数组中的位置
e.next = newtable[i];
newtable[i] = e; // 将元素放在数组上
e = next; // 访问下一个Entry链上的元素
} while (e != null);
}
}
table = newtable;
}
代码
package com.wyh;
import java.util.Map;
/**
* @author diao 2022/6/29
*/
public class MyHashMap<K, V> {
private int CAPACITY = 16;
private int size = 0;
private MyEntry[] table;
/**
* 1.构造HashMap
* 传阈值CAPACITY情况
* 初始化哈希表,设置初始一个阈值
*
* @param CAPACITY
*/
public MyHashMap(int CAPACITY) {
this.CAPACITY = CAPACITY;
this.table = new MyEntry[CAPACITY];
}
/**
* 默认情况
*/
public MyHashMap() {
this.table = new MyEntry[CAPACITY];
}
/**
* 2.传入值
* 传入值
*/
public V put(K key, V value) {
if (key == null) {
return putForNullKey(value);
}
//1.当key不为null,计算hash值->得到下标
int hash = MyHash(key);
int index = indexForTable(hash, CAPACITY);
//2.当表当前位置不为空说明有人了,进行插入替换该值(这里没有解决hash冲突)
for (MyEntry<K, V> e = table[index]; e != null; e = e.next) {
//3.如果哈希表上该位置hash与你key的hahs相等就赋值返回
if (e.hash == hash && (e.key == key || e.key.equals(key))) {
e.value = value;
V old = value;
return old;
}
}
//4.当前表index位置没有元素进行插入
addEntry(hash, key, value, index);
return null;
}
/**
* 2.1插入key!=null的value到哈希表中
* 计算插入位置:hash&
*/
private int indexForTable(int hash, int CAPACITY) {
//hash值与数组长度进行位运算得到下标
return hash & (CAPACITY - 1);
}
/**
* 3.插入key=null的value
* key为null传入值情况
*/
private V putForNullKey(V value) {
//从哈希表的一个元素开始遍历->直至找到为null的key
for (MyEntry<K, V> e = table[0]; e != null; e = e.next) {
//给当前key为空对应的value赋值
if (e.key == null) {
e.value = value;
V old = value;
return old;
}
}
//添加新的节点
addEntry(0, null, value, 0);
return null;
}
/**
* 4.给数组添加新节点
* 因为遍历HashEntry可能key都不为空,我们可以拓展一个HashEntry
* 比如之前传空key而哈希表又满了,hash传入0,key=null,value,i=0
*/
private void addEntry(int hash, K key, V value, int i) {
//1、这里插入的策略是直接前面第一个插入一个,并且赋值
MyEntry<K, V> e = table[i];
table[i] = new MyEntry<K, V>(hash, key, value, e);
//2、插入后,判断HashEntry是否达到阈值
if (size == CAPACITY) {
//达到阈值扩容
resize();
}
size++;
}
/**
* 5.扩容机制
* 重置HashEntry数组,当size达到阈值时
*/
private void resize() {
CAPACITY=CAPACITY*2;
//1.重新创建扩容后的一个Hash表
MyEntry[] newtable = new MyEntry[CAPACITY];
//2.然后将原来的hash表赋值上去
for (MyEntry<K,V> entry : table) {
MyEntry<K, V> e = entry;
if (e != null) {
entry = null;//释放旧的哈希表的元素
do {
MyEntry<K, V> next = e.next;
int i = indexForTable(e.hash, CAPACITY); // 重新计算每个元素在数组中的位置
e.next = newtable[i];
newtable[i] = e; // 将元素放在数组上
e = next; // 访问下一个Entry链上的元素
} while (e != null);
}
}
}
/**
* 6.计算机key的hash值
*/
private int MyHash(K key) {
if (key == null) {
return 0;
}
int h = key.hashCode();
h = h ^ (h >>> 16);
return h;
}
/**
* 7.取值
*/
public V get(K key){
//1.当键为空值
if(key==null){
return getForNullKey();
}
//2.当不为空key
int hash = MyHash(key);
int index = indexForTable(hash, CAPACITY);
//2.2得到表当前位置,然后遍历,get值
for (MyEntry<K,V>e=table[index];e!=null;e=e.next){
if(e.hash==hash&&(e.key==key||e.key.equals(key))){
return e.value;
}
}
return null;
}
/**
* 8.当key为null值取值
* key为null,从表中的头元素寻找,直至空key为止
*/
private V getForNullKey(){
for(MyEntry<K,V>e=table[0];e!=null;e=e.next){
if(e.key==null) {
return e.value;
}
}
return null;
}
}
/**
* hash表:本质是一个数组
*
* @param <K>:键
* @param <V>:值
*/
class MyEntry<K, V> {
public int hash;
public K key;
public V value;
//下一个节点
public MyEntry<K, V> next;
public MyEntry(int hash, K key, V value, MyEntry<K, V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
}