HashMap是在面试中经常会问的一点,很多时候我们仅仅只是知道HashMap他是允许键值对都是Null,并且是非线程安全的,如果在多线程的环境下使用,是很容易出现问题的。 这是我们通常在面试中会说的,但是有时候问到底层的源码分析的时候,为什么允许为Null,为什么不安全,这些问题的时候,如果没有分析过源码的话,好像很难回答, 这样的话我们来研究一下这个源码。看看原因吧。
- 底层数据结构
第一,如图所示,HashMap有3个要素:hash函数+数组+单链表
第二,对于hash函数而言,需要考虑些什么?
要快,对于给定的Key,要能够快速计算出在数组中的index。那么什么运算够快呢?显然是位运算!
要均匀分布,要较少碰撞。说白了,我们希望通过hash函数,让数据均匀分布在数组中,不希望大量数据发生碰撞,导致链表过长。那么怎么办到呢?也是利用位运算,通过对数据的二进制的位进行移动,让hash函数得到的数据散列开来,从而减低了碰撞的概率。
如果发生了碰撞怎么办?上面的图其实已经说明了JDK的HashMap是如何处理hash冲突的,就是通过单链表解决的。那么除了这个方法,还有其他思路么?比如说,如果发生冲突,那么记下这个冲突的位置为index,然后在加上固定步长,即index+step,找到这个位置,看一下是否仍然冲突,如果继续冲突,那么按照这个思路,继续加上固定步长。其实这就是所谓的线性探测来解决Hash冲突的方法!
- 定义一个Map接口
定义一个接口,对外暴露快速存取的方法。
```java
public interface Map<K,V> {
//存值
public V put(K k, V v);
//取值
public V get(K k);
}
- 定义一个Entry链表
HashMap的要素之一,单链表的体现就在这里!
public class Entry<K, V> {
K k;
V v;
Entry<K, V> next;
public Entry() {
}
/**
* 在next指向下一个节点
*/
public Entry(K k, V v, Entry<K, V> next) {
this.k = k;
this.v = v;
this.next = next;
}
public K getKey() {
return k;
}
public V getValue() {
return v;
}
}
- 实现Map接口
- 重写有参/无参构造方法
仔细观察下,你会发现,其实这里使用到了“门面模式”。这里的2个构造方法其实指向的是同一个,但是对外却暴露了2个“门面”!
public class MyHashMap<K, V> implements Map<K, V> {
//数组默认长度
private final static int DEFAULT_INIT_CAPACITY = 1 << 4;
//默认负载因子
private final static float DEFAULT_LOAD_FACTOR = 0.75f;
//初始定义的长度
private int defaultInitCapacity;
//初始定义的负载因子
private float defaultLoadFactor;
//实际的数组长度
private int userSize;
//entry数组
Entry<K, V>[] tables;
//无参构造
public MyHashMap() {
this(DEFAULT_INIT_CAPACITY, DEFAULT_LOAD_FACTOR);
}
//有参构造
public MyHashMap(int defaultInitCapacity, Float defaultLoadFactor) {
if (defaultInitCapacity < 0) {
throw new IllegalArgumentException("初始长度不能是0" + defaultInitCapacity);
}
if (defaultLoadFactor <= 0 || Float.isNaN(defaultLoadFactor)) {
throw new IllegalArgumentException("负载因子错了!" + defaultLoadFactor);
}
this.defaultInitCapacity = defaultInitCapacity;
this.defaultLoadFactor = defaultLoadFactor;
this.tables = new Entry[this.defaultInitCapacity];
}
}
- 存值put
第一,要考虑是否扩容? HashMap中的Entry的数量(数组以及单链表中的所有Entry)是否达到阀值?
第二,如果扩容,意味着新生成一个Entry[],不仅如此还得重新散列。
第三,要根据Key计算出在Entry[]中的位置,定位后,如果Entry[]中的元素为null,那么可以放入其中,如果不为空,那么得遍历单链表,要么更新value,要么形成一个新的Entry“挤压”单链表!
//存值
@Override
public V put(K k, V v) {
//判断是否需要扩容
if (this.userSize > this.defaultInitCapacity * this.defaultLoadFactor) {
this.resize();
}
//计算出当前下标
int index = this.getIndex(k, this.defaultInitCapacity);
//获取下标上的entry
Entry<K, V> entry = this.tables[index];
//创建一个新的newEntry
Entry<K, V> newEntry = new Entry<K, V>(k, v, null);
//判断当前下标是否被使用,如果没有就将newEntry填入
if (entry == null) {
this.tables[index] = newEntry;
//使用后,使用长度+1
this.userSize++;
} else {
Entry<K, V> e = entry;
//判断当前key是否等于传入的k
if (e.getKey() == k) {
//如果相等,就将传入v赋值给e.v
e.v = v;
} else {
//如果不相等,就循环遍历entry的下一个entry.next
while (e.next != null) {
//判断如果下一个entry的key是否等于传入的k,如果相等就赋值v
if (e.next.getKey() == k || e.next.getKey().equals(k)) {
e.next.v = v;
} else {
e = e.next;
}
}
//判断上边循环后entry.next是否等于null,如果等于null
//将newEntry设置到entry.next的位置
if (e.next == null) {
e.next = newEntry;
}
}
}
// 返回newEntry.getValue()
return newEntry.getValue();
}
- 取值get
get很简单,只需要注意在遍历单链表的过程中使用== or equals来判断下即可。
//取值
@Override
public V get(K k) {
// 获取当前下标
int index = this.getIndex(k, this.tables.length);
// 得到下标上的entry
Entry<K, V> entry = this.tables[index];
// entry非空校验
if (entry == null) {
throw new NullPointerException("空空如也");
}
// 循环entry != null
while (entry != null) {
// key相等就返回
if (entry.getKey() == k || entry.getKey().equals(k)) {
return entry.getValue();
} else {
// 如果不相等,将next赋值给entry继续循环
entry = entry.next;
}
}
// 找不到就返回null
return null;
}
- 计算hashcode
我这里参考了JDK的HashMap的hash函数的实现,这里也再次说明了:要想散列均匀,就得进行二进制的位运算!
//计算hashcode
private int hash(int hashCode) {
hashCode = hashCode ^ ((hashCode >>> 20) ^ (hashCode >>> 12));
return hashCode ^ ((hashCode >>> 7) ^ (hashCode >>> 4));
}
- 扩容resize
这里可以看出,对于HashMap而言,如果频繁进行resize/rehash操作,是会影响性能的。
resize/rehash的过程,就是数组变大,原来数组中的entry元素一个个的put到新数组的过程,需要注意的是一些状态变量的改变。
//扩容
private void resize() {
// 创建一个新的entry数组 ,长度为 defualtLength*2
Entry<K, V>[] newEntryTable = new Entry[this.defaultInitCapacity * 2];
// 创建一个list 用来存放entry
List<Entry<K, V>> entryList = new ArrayList<Entry<K, V>>();
// 1.先将历史的数据保存
// 循环entry数组
for (int i = 0; i < tables.length; i++) {
Entry<K, V> entry = tables[i];
// 判断数组下标上的entry!=null
while (entry != null) {
entryList.add(entry);
// 将enrty存到list中,entry = entry.next;
entry = entry.next;
}
}
// 非空校验list
if (entryList != null && entryList.size() > 0) {
// 重新设置默认长度
this.defaultInitCapacity = this.defaultInitCapacity * 2;
// 使用长度重置为0
this.userSize = 0;
// 将newtable赋值给table
this.tables = newEntryTable;
// 2.将保存好的数据存到新的容器中
for (Entry<K, V> entry : entryList) {
// 循环list中的entry,并将其next置位null
if (entry.next != null) {
entry.next = null;
}
// 调用put方法,传入entry.k entry.v
put(entry.getKey(), entry.getValue());
}
}
}
- 测试代码
public class Test1 {
public static void main(String[] args) {
Map<String,Integer> map = new MyHashMap<>();
for(int i=0;i<10000;i++){
map.put(""+i, i);
}
for(int i=0;i<10000;i++){
System.out.println("key:"+i+"value:"+map.get(""+i));
}
}
}
- 测试结果

全部代码一览
package hashmap;
import java.util.ArrayList;
import java.util.List;
/**
* @Author: wangpeng
* @Date: 2020-8-10 23:01
*/
public class MyHashMap<K, V> implements Map<K, V> {
//数组默认长度
private final static int DEFAULT_INIT_CAPACITY = 1 << 4;
//默认负载因子
private final static float DEFAULT_LOAD_FACTOR = 0.75f;
//初始定义的长度
private int defaultInitCapacity;
//初始定义的负载因子
private float defaultLoadFactor;
//实际的数组长度
private int userSize;
//entry数组
Entry<K, V>[] tables;
//无参构造
public MyHashMap() {
this(DEFAULT_INIT_CAPACITY, DEFAULT_LOAD_FACTOR);
}
//有参构造
public MyHashMap(int defaultInitCapacity, Float defaultLoadFactor) {
if (defaultInitCapacity < 0) {
throw new IllegalArgumentException("初始长度不能是0" + defaultInitCapacity);
}
if (defaultLoadFactor <= 0 || Float.isNaN(defaultLoadFactor)) {
throw new IllegalArgumentException("负载因子错了!" + defaultLoadFactor);
}
this.defaultInitCapacity = defaultInitCapacity;
this.defaultLoadFactor = defaultLoadFactor;
this.tables = new Entry[this.defaultInitCapacity];
}
//计算hashcode
private int hash(int hashCode) {
hashCode = hashCode ^ ((hashCode >>> 20) ^ (hashCode >>> 12));
return hashCode ^ ((hashCode >>> 7) ^ (hashCode >>> 4));
}
//获取保存位置的数组下标
private int getIndex(K k, int length) {
//减1后,下标永远不会大于16
int m = length - 1;
int index = hash((k.hashCode())) & m;
return index > 0 ? index : -index;
}
//存值
@Override
public V put(K k, V v) {
//判断是否需要扩容
if (this.userSize > this.defaultInitCapacity * this.defaultLoadFactor) {
this.resize();
}
//计算出当前下标
int index = this.getIndex(k, this.defaultInitCapacity);
//获取下标上的entry
Entry<K, V> entry = this.tables[index];
//创建一个新的newEntry
Entry<K, V> newEntry = new Entry<K, V>(k, v, null);
//判断当前下标是否被使用,如果没有就将newEntry填入
if (entry == null) {
this.tables[index] = newEntry;
//使用后,使用长度+1
this.userSize++;
} else {
Entry<K, V> e = entry;
//判断当前key是否等于传入的k
if (e.getKey() == k) {
//如果相等,就将传入v赋值给e.v
e.v = v;
} else {
//如果不相等,就循环遍历entry的下一个entry.next
while (e.next != null) {
//判断如果下一个entry的key是否等于传入的k,如果相等就赋值v
if (e.next.getKey() == k || e.next.getKey().equals(k)) {
e.next.v = v;
} else {
e = e.next;
}
}
//判断上边循环后entry.next是否等于null,如果等于null
//将newEntry设置到entry.next的位置
if (e.next == null) {
e.next = newEntry;
}
}
}
// 返回newEntry.getValue()
return newEntry.getValue();
}
//取值
@Override
public V get(K k) {
// 获取当前下标
int index = this.getIndex(k, this.tables.length);
// 得到下标上的entry
Entry<K, V> entry = this.tables[index];
// entry非空校验
if (entry == null) {
throw new NullPointerException("空空如也");
}
// 循环entry != null
while (entry != null) {
// key相等就返回
if (entry.getKey() == k || entry.getKey().equals(k)) {
return entry.getValue();
} else {
// 如果不相等,将next赋值给entry继续循环
entry = entry.next;
}
}
// 找不到就返回null
return null;
}
//扩容
private void resize() {
// 创建一个新的entry数组 ,长度为 defualtLength*2
Entry<K, V>[] newEntryTable = new Entry[this.defaultInitCapacity * 2];
// 创建一个list 用来存放entry
List<Entry<K, V>> entryList = new ArrayList<Entry<K, V>>();
// 1.先将历史的数据保存
// 循环entry数组
for (int i = 0; i < tables.length; i++) {
Entry<K, V> entry = tables[i];
// 判断数组下标上的entry!=null
while (entry != null) {
entryList.add(entry);
// 将enrty存到list中,entry = entry.next;
entry = entry.next;
}
}
// 非空校验list
if (entryList != null && entryList.size() > 0) {
// 重新设置默认长度
this.defaultInitCapacity = this.defaultInitCapacity * 2;
// 使用长度重置为0
this.userSize = 0;
// 将newtable赋值给table
this.tables = newEntryTable;
// 2.将保存好的数据存到新的容器中
for (Entry<K, V> entry : entryList) {
// 循环list中的entry,并将其next置位null
if (entry.next != null) {
entry.next = null;
}
// 调用put方法,传入entry.k entry.v
put(entry.getKey(), entry.getValue());
}
}
}
}
本文详细解析了HashMap的底层数据结构,包括hash函数、数组和单链表的运用,探讨了hash函数的设计原则,如何处理hash冲突,以及HashMap的扩容机制。同时,提供了自定义HashMap的实现代码,帮助读者更深入地理解其工作原理。

3814

被折叠的 条评论
为什么被折叠?



