HashMap作为java中经常使用到的一个数据结构,同时也是面试中常考的数据结构,掌握其结构与实现原理是必须的。
一、什么是HashMap
HashMap 是一个散列表,它存储的内容是键值对(key-value)映射。
HashMap 实现了 Map 接口,根据键的 HashCode 值存储数据,具有很快的访问速度,最多允许一条记录的键为 null,不支持线程同步。
HashMap 是无序的,即不会记录插入的顺序。
二、简单实现
首先要建立一个HNode类
class HNode<K, V> {
K key;
V value;
HNode<K, V> next;
public HNode(K key, V value) {
this.key = key;
this.value = value;
}
public int hashCode() {
return key.hashCode();
}
}
K用于存储键值,V用于存储数据
接下来是简单的实现
public class MyHashMap<K, V> {
// 存储节点的数组
HNode<K, V>[] table;
// - 已存储节点的个数
int elmSize;
// - 数组的长度
int length;
// - 数组中被占用的下标个数
int usedArrSize;
// - 扩容的阈值 0.75
double loadFactor = 0.75;
// 扩容倍数
int factor = 2;
// 数组的初始长度
static final int initLength = 16;
public MyHashMap() {
table = new HNode[initLength];
length = initLength;
}
public void put(K key, V value) {
HNode<K, V> node = new HNode<>(key, value);
// 根据key的hash值 计算一个需要存储的位置下标
int hash = node.hashCode();
int index = hash & (length - 1);//
// 0000 1000 1001 1101 & 0000 0000 0000 1111 = 1101
// 0000 1010 1100 1101 & 0000 0000 0000 1111 = 1101
// 不同的hash值运算下标时也会出现下标冲突: 解决方式 拉链
// & : 按位与运算符,两边的数进行按位与运算,相同保留源码,不同为0 (都为1才为1)
HNode<K, V> first = table[index];
if (first == null) {
table[index] = node;
elmSize++;
usedArrSize++;
} else {
// 验证first的key是否与node的key是否相同
// 两个对象之间使用==判断 判断的是地址
// hash值不一样的情况 就肯定不一样了
// hash值一样,会存在两个不同的key算出来相同的hash值
// 接着判断是不是同一个对象,从地址 和 值比较
if (first.hashCode() == hash && first.key == key || first.key.equals(key)) {
// 相等就要替换
first.value = value;
} else {
HNode<K, V> kvNode = null;
HNode<K, V> temp = first;
while (temp.next != null) {
temp = temp.next;
if (temp.hashCode() == hash && temp.key == key ||
temp.key.equals(key)) {
kvNode = temp;
break;
}
}
if (kvNode == null) {
temp.next = node;
elmSize++;
} else {
kvNode.value = value;
}
}
}
// 计算是否需要扩容
if (length * loadFactor <= usedArrSize) {
resize();
}
}
private void resize() {
length = length * factor;
HNode<K, V>[] newTable = new HNode[length];
usedArrSize = 0;
for (HNode<K, V> node : table) {
while (node != null) {
HNode<K, V> next = node.next;
int index = node.hashCode() & (length - 1);
node.next = newTable[index];
newTable[index] = node;
node = next;
usedArrSize++;
}
}
table = newTable;
}
public V get(K key) {
int hash = key.hashCode();
int index = hash & (length - 1);
HNode<K, V> first = table[index];
return null;
}
public static void main(String[] args) {
MyHashMap<String, Integer> map = new MyHashMap<>();
for (int i = 0; i < 100; i++) {
map.put("hello" + i, 1);
}
}
}
这段代码是一个简化版的哈希表 (MyHashMap
) 实现,哈希表是一种用于存储键值对的数据结构。以下是对每个函数的作用以及变量的用途的解释:
变量解释
-
HNode<K, V>[] table
: 存储哈希表节点 (HNode
) 的数组,其中每个元素都是一个链表的头节点。每个链表节点存储一个键值对。 -
int elmSize
: 哈希表中已存储的节点总数。 -
int length
: 哈希表数组的当前长度,即table
的大小。 -
int usedArrSize
: 哈希表数组中已被占用的下标数量,即有多少个数组位置存储了链表的头节点。 -
double loadFactor = 0.75
: 负载因子,表示当哈希表的填充程度达到length * loadFactor
时,需要进行扩容。 -
int factor = 2
: 扩容倍数,每次扩容时,数组的大小会乘以这个倍数。 -
static final int initLength = 16
: 数组的初始长度,默认为16。
构造函数
public MyHashMap() {
table = new HNode[initLength];
length = initLength;
}
这个构造函数初始化了哈希表的数组 table
,并将数组的初始长度设置为16。
put
方法
public void put(K key, V value) {
// ...代码略
}
put
方法用于在哈希表中插入键值对,如果键已经存在则更新对应的值。
-
计算哈希值: 首先通过
key.hashCode()
计算键的哈希值。 -
计算数组下标: 使用哈希值与数组长度减1进行按位与运算 (
hash & (length - 1)
),得到在table
中的存储位置。 -
链表冲突处理: 如果该下标对应的链表为空,则直接插入节点。如果不为空,判断该位置的链表中是否已经存在相同的键,如果存在则更新其值,否则将新节点添加到链表的末尾。
-
扩容检查: 在每次插入后,检查当前填充的数量是否超过了扩容阈值 (
length * loadFactor
),如果超过则调用resize()
方法进行扩容。
resize
方法
private void resize() {
// ...代码略
}
resize
方法用于哈希表的扩容。
-
扩容: 将哈希表的数组长度乘以扩容倍数 (
factor
)。 -
重新分布: 扩容后,重新计算所有已有键值对在新数组中的位置,并将它们放入新数组中。
get
方法
public V get(K key) {
int hash = key.hashCode();
int index = hash & (length - 1);
HNode<K, V> first = table[index];
return null;
}
get
方法用于根据键来获取对应的值。
-
计算哈希值和下标: 同
put
方法,先计算哈希值并确定数组中的下标。 -
遍历链表: 在计算出的下标位置查找链表中的节点,如果找到匹配的键则返回对应的值,否则返回
null
。