HashMap 基本介绍#
HashMap 是 Map 接口中最为常用的一个实现类,通过名字我们可以看出,它与 hash 算法密不可分。它的内部用来存放键值对。HashMap 的底层实现是数组和链表 / 红黑树。
简单解释一下它的原理来加深自己的印象:HashMap 主要维护了一个数组,数组中存放的是 Entry 对象,该对象包含了 key(键)、value(值)、hashcode(key.hashCode()
返回的值) 以及 next(下一个 Entry 对象),我们的键 / 值对就是存放在一个个的 Entry 对象中。但是如果仅仅是这样,就与普通数组没什么区别了,插入和删除的效率都很低。为了提高效率,HashMap 还引入了链表。下面直接上一张图吧,画功不好,简单嘲笑一下后就接着往下看吧🙃。
当我们把一个键值对存进 HashMap 时,它首先会对键的 hashCode()
进行重新计算,然后调用 indexFor()
方法计算出 table 数组的索引,最后将键值对存放在数组在该索引下的 Entry 对象中。但是,两个不同的键 A 和 B,它们计算出来的索引值有可能是相等的,这样一来就会产生“碰撞”,我们总不能在一个 Entry 对象中存储两个键值对。为了解决碰撞问题,链表的作用就显示出来了 —— 假设 A 和 B 经过计算后得到的索引都是 0,且 A 所在的 Entry 对象已经存在于数组的第一个位置,当 B 所在的 Entry 对象也准备来第一个位置的时候,它会自动链接到 A 所在 Entry 对象的 next
属性上,后续不管 C 还是 D,只要它们的 hashcode 相等,都依次往后链接,这就形成了一个链表。仔细一看,HashMap 维护的数组,里面的成员其实就是链表,链表上存放的才是包含键值对的 Entry 对象。
同理,当我们从 HashMap 中取数据时,会先对传进来的键进行计算得到数组的索引,然后对该索引下的链表进行遍历,如果链表中某个节点的键与传进来的键相等(调用 equals()
方法返回 true
),就返回该节点的值。
对了,在 Java 8 当中,链表还有可能是红黑树。事情是这样的,如果一个 HashMap 中的键值对特别多,那么链表中的节点数量就也变得很多,这样一来遍历链表的效率就会大大降低,从而查询效率就降低了,所以为了提高效率,Java 8 引入了红黑树。但并不是没有链表了,只有当一个链表的节点数量大于 8 的时候,才会将该链表转换为红黑树。另外, Java 7 当中的头插法,在 Java 8 中也被改成了尾插法。
HashMap 内部其实还很深,很多东西上面都没提到,或者没有细说。像初始容量(InitialCapacity)、加载因子(loadFactor),影响着 HashMap 的自动扩容等;indexFor()
具体是怎么计算键值对存放的数组索引、从而避免碰撞的;链表是如何转换为红黑树的……后续还需要加深阅读及学习。
HashMap 的实现#
今天简单实现了一下 HashMap,说简单,那是真的简单,感觉快与官方 HashMap 不搭边了😂。代码如下,仅仅是学习 HashMap 过程的笔记,不代表 HashMap 的真正原理……
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
| package com.realchoi.collection;
import java.util.LinkedList;
/**
* 自己简单实现 HashMap。
* Hashmap 底层实现:数组 + 链表
*/
public class MyHashMap {
// HashMap 中维护的链表数组(此处暂时将数组大小定为 999)
// 这里直接使用 Java 自带的 LinkedList
private LinkedList[] linkedList = new LinkedList[999];
// 键值对的个数
private int size;
/**
* 往 HashMap 中添加键值对
*
* @param key 键
* @param value 值
*/
public void put(Object key, Object value) {
// 初始化为一个键值对对象
MyEntry entry = new MyEntry(key, value);
// 先根据 key 的 hashcode 找到链表数组的索引,hashcode 可能为负数,转为正数
int index = key.hashCode() < 0 ? -key.hashCode() % linkedList.length : key.hashCode() % linkedList.length;
// 判断当前索引有没有数据,如果没有数据,则新建一个链表存放在该索引下,当前键值对是当前链表的第一个
if (linkedList[index] == null) {
LinkedList temp = new LinkedList();
temp.add(entry);
linkedList[index] = temp;
size++;
}
// 如果当前索引下已经有链表了,将新的键值对添加到该链表的尾部
else {
// 添加之前还需要遍历链表,判断当前 key 是否已经存在,如果存在,则将原 value 覆盖(因为 HashMap 不允许键重复)
for (int i = 0; i < linkedList[index].size(); i++) {
MyEntry e = (MyEntry) linkedList[index].get(i);
if (e.key.equals(key)) {
e.value = value;
return;
}
}
linkedList[index].add(entry);
size++;
}
}
/**
* 通过键获取值对象
*
* @param key 键
* @return 值对象
*/
public Object get(Object key) {
// 首先根据 key 的 hashcode 得到键值对所在链表的索引
int index = key.hashCode() < 0 ? -key.hashCode() % linkedList.length : key.hashCode() % linkedList.length;
// 然后根据索引找到所在的链表
LinkedList list = linkedList[index];
// 遍历链表,得到值对象
for (int i = 0; i < list.size(); i++) {
MyEntry e = (MyEntry) list.get(i);
if (e.key.equals(key)) {
return e.value;
}
}
return null;
}
/**
* HashMap 中键值对的个数
*
* @return
*/
public int size() {
return size;
}
}
/**
* 键值对
*/
class MyEntry {
// 键和值
public Object key;
public Object value;
// 构造方法
public MyEntry(Object key, Object value) {
this.key = key;
this.value = value;
}
}
|
参考资料#
1、Java 中常见数据结构 Map 之 HashMap
2、HashMap 之元素插入