最近在看HashMap的源码,了解了HashMap的原理和方法实现的过程,觉得如果自己仿写可以简单仿写一个HashMap可能就会更加清楚了,说干就干。
说明一下,我主要仿写了put、get和扩容resize方法,其他方法其实大致原理差不多。
源码已上传到GitHub上:仿写HashMap
准备工作
首先,HashMap的底层是一个数组,数组中的元素Node有四个属性,Key,Value,Key的Hash值和为了解决产生Hash冲突而设置的Next结点,如下:
class Node<K,V>{
K key;
V value;
int hash;
Node<K,V> next;
public Node(K key, V value, int hash) {
this.key = key;
this.value = value;
this.hash = hash;
}
}
在HashMap中,大体上有HashMap容量capacity,负载因子loadFactor,元素数量size(还没put元素,肯定是0)和Node泛型数组这四个属性。
public class MyHashMap<K,V> {
private int size = 0;
private int capacity;
private float loadFactor;
private Node<K,V>[] table;
}
对于构造函数,HashMap默认容量是16,默认负载因子是0.75,还要创建一个大小为16的Node泛型数组,这里我一开始是这样写的
public class MyHashMap<K,V> {
private int size = 0;
private int capacity;
private float loadFactor;
private Node<K,V>[] table;
public MyHashMap() {
this.capacity = 16;
this.loadFactor = 0.75f;
table = new Node<K,V>[capacity];
}
}
发现new Node<K,V>[capacity];
报错了,在网上搜索才知道Java是不支持实例化泛型数组的,因为Java认为数组是不可变的,而泛型数组中的元素类型是会变化的,后来查了一些资料,发现了一个解决方法,这样利用反射就可以实例化一个固定大小的泛型数组了,如下:
public class MyHashMap<K,V> {
private int size = 0;
private int capacity;
private float loadFactor;
private Node<K,V>[] table;
public MyHashMap() {
this.capacity = 16;
this.loadFactor = 0.75f;
table = (Node<K, V>[]) Array.newInstance(Node.class,capacity);
}
}
HashMap源码中还给出了一个可以自定义容量和负载因子的构造函数,我们也模仿着写一个,注意参数的合法性(其实HashMap源码中还限制了容量的最大值,为1<<30,也可以限制一下)
public class MyHashMap<K,V> {
private int size = 0;
private int capacity;
private float loadFactor;
private Node<K,V>[] table;
public MyHashMap() {
this.capacity = 16;
this.loadFactor = 0.75f;
table = (Node<K, V>[]) Array.newInstance(Node.class,capacity);
}
//可以自定义容量和负载因子的构造器
public MyHashMap(int initialCapacity,float loadFactor) {
//自定义容量不能小于0
if (initialCapacity < 0)
throw new IllegalArgumentException("不合法容量: " +
capacity);
//自定义负载因子不能小于等于0并且不能是NaN值
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("不合法负载因子: " +
loadFactor);
//自定义容量向上取2的n次方
this.capacity = tableSizeFor(initialCapacity);
this.loadFactor = loadFactor;
table = (Node<K, V>[]) Array.newInstance(Node.class,capacity);
}
}
在HashMap中,容量必须为2的n次方,这个和生成Hash值有关,后面我会讲一下,而如果自定义的容量不为2的n次方,就会向上取一个最小的2的n次方的数,比如自定义容量为9,HahsMap就会自动将容量变为16,所以我这里调用了一个tableSizeFor方法来实现,参考了HashMap源码中的tableSizeFor方法,如下:
//向上取2的n次方
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : n + 1;
}
在写put和get方法的之前,我们还要写一个Hash方法,目的是为了计算Key的Hash值以得到它在数组中的索引位置,后面会讲一下原理,如下:
//计算Hash值
int Hash(K key) {
return Objects.hashCode(key) & (capacity - 1);
}
这里和HashMap源码中的Hash方法一样,也用HashCode值与capacity-1,那么为什么要这样做呢,& capacity
不是也可以得到索引的位置吗?
这里就涉及到了一道面试题,就是为什么要&(capacity - 1),原因就在于:HashMap规定数组的容量必需是2的n次方,不是的话向上取最小的2的n次方,当容量为2的n次方的时候,length-1会把二进制中1以后的0全部变为1,当hashcode值和length-1做与时,生成的hash值都是按照hashcode值生成的,减少了hash冲突。
如果length不为2的n次方的话,length-1就会在某些位上出现0,做与后就是0,浪费了空间,生成的hash值很可能出现重复,发生hash冲突的几率提高
如果直接&capacity,由于capacity是2的n次方,后果可想而知
所以HashMap既限制了capacity是2的n次方,又使用&(capacity - 1),是减少Hash冲突最有效的方法。
至此,准备工作就做完了,可以开始写我们的put、get和resize方法了,先把之前写的整理一下:
public class MyHashMap<K,V> {
private int size = 0;
private int capacity;
private float loadFactor;
private Node<K,V>[] table;
public MyHashMap() {
this.capacity = 16;
this.loadFactor = 0.75f;
table = (Node<K, V>[]) Array.newInstance(Node.class,capacity);
}
public MyHashMap(int initialCapacity,float loadFactor) {
//自定义容量不能小于0
if (initialCapacity < 0)
throw new IllegalArgumentException("不合法容量: " +
capacity);
//自定义负载因子不能小于等于0并且不能是NaN值
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("不合法负载因子: " +
loadFactor);
//自定义容量向上取2的n次方
this.capacity = tableSizeFor(initialCapacity);
this.loadFactor = loadFactor;
table = (Node<K, V>[]) Array.newInstance(Node.class,capacity);
}
//计算Hash值
int Hash(K key) {
return Objects.hashCode(key) & (capacity - 1);
}
//向上取2的n次方
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : n + 1;
}
//put、get、resize
}
class Node<K,V>{
K key;
V value;
int hash;
Node<K,V> next;
public Node(K key, V value, int hash) {
this.key = key;
this.value = value;
this.hash = hash;
}
}
put方法
先写put方法吧,我这里就不考虑jdk1.8新加的红黑树了,使用的是数组加链表,思考一下HashMap的put方法的大致流程:
- 调用Hash方法计算得到key的Hash值,也就是索引位置
- 判断索引位置处是否有元素,如果没有元素,直接加进去,如果有元素,就要遍历链表了
- 遍历链表,判断每个结点的key是不是等于要加进去结点的key,如果有等于的,直接此结点覆盖value,如果没有等于的,利用头插法插到链表的第一个结点之前(这里也可以使用尾插法,注意要让最后一个结点的next指向新结点)
- 判断一下是否发生覆盖,如果没有发生覆盖,size++
- 判断size是否超过负载因子*容量,超过则扩容,调用resize
以上是大致思路,头插法和尾插法都有写,用一个就行,下面是具体代码实现:
void put(K key,V value){
int hash = Hash(key);
//判断是否发生覆盖的标识
boolean flag = true;
Node<K,V> node = new Node<>(key,value,hash);
//如果索引位置没有元素,直接加
if(this.table[hash] == null){
this.table[hash] = node;
}else{
Node<K,V> head = this.table[hash];
//发生hash冲突,搜索链表
while (head != null){
//如果Key相等,覆盖Value
if(key == head.key){
head.value = value;
flag = false;
break;
}
// //如果hash冲突没有发生覆盖,使用尾插法,让最后一个结点的next指向新结点
// if(head.next == null){
// head.next = node;
// break;
// }
head = head.next;
}
//如果hash冲突没有发生覆盖,使用头插法,插到第一个结点之前
if(flag){
head = this.table[hash];
this.table[hash] = node;
node.next = head;
}
}
//如果没有发生覆盖,元素数量size++
if(flag) this.size++;
//如果元素数量超过负载因子*容量,扩容
if(size > capacity * loadFactor) resize();
}
get方法
接下来写get方法,也是先思考一下大致流程:
- 调用Hash方法计算得到key的Hash值,也就是索引位置
- 如果索引位置没有元素,直接返回null
- 如果索引位置有元素,遍历索引位置处的所有结点,找到key一样的就取得value,找不到返回null
V get(K key){
V value = null;
int hash = Hash(key);
//如果索引位置没有元素
if(this.table[hash] == null){
return value;
}else{
Node<K,V> head = this.table[hash];
//遍历所有结点
while (head != null){
if(head.key == key){
value = head.value;
break;
}
head = head.next;
}
return value;
}
}
resize方法
最后是resize方法,这个方法是用来扩容的,HashMap扩容的时机为元素数量size大于负载因子loadFactor*容量capacity时,在jdk1.8中,如果链表元素超过8并且元素数量小于64时也会发生扩容,后者先不实现,先实现前者,也是思考一下大致流程:
- 拷贝一下老数组,实例化一个是原数组容量2倍的新数组作为HashMap的数组,并把capacity乘以2。
- 老数组中有所有元素,遍历老数组,如果某个索引处不为null,就遍历该索引位置的所有结点,每个结点都调用put方法加到新数组中,直到遍历完老数组为止。
void resize(){
//老的table
Node<K,V>[] oldTable = this.table;
//新的table,容量2倍
this.table = (Node<K, V>[]) Array.newInstance(Node.class,2 * capacity);
this.capacity *= 2;
//遍历老table,把键值对都put到新table中
for (int i = 0;i < oldTable.length; i++){
if(oldTable[i] != null){
Node<K,V> head = oldTable[i];
while (head != null) {
put(head.key,head.value);
head = head.next;
}
}
}
}
至此,我自己仿写的HashMap在基础功能上就做完了,完整代码如下:
public class MyHashMap<K,V> {
private int size = 0;
private int capacity;
private float loadFactor;
private Node<K,V>[] table;
public MyHashMap() {
this.capacity = 16;
this.loadFactor = 0.75f;
table = (Node<K, V>[]) Array.newInstance(Node.class,capacity);
}
public MyHashMap(int initialCapacity,float loadFactor) {
//自定义容量不能小于0
if (initialCapacity < 0)
throw new IllegalArgumentException("不合法容量: " +
capacity);
//自定义负载因子不能小于等于0并且不能是NaN值
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("不合法负载因子: " +
loadFactor);
//自定义容量向上取2的n次方
this.capacity = tableSizeFor(initialCapacity);
this.loadFactor = loadFactor;
table = (Node<K, V>[]) Array.newInstance(Node.class,capacity);
}
//计算Hash值
int Hash(K key) {
return Objects.hashCode(key) & (capacity - 1);
}
void put(K key,V value){
int hash = Hash(key);
//判断是否发生覆盖
boolean flag = true;
Node<K,V> node = new Node<>(key,value,hash);
if(this.table[hash] == null){
this.table[hash] = node;
}else{
Node<K,V> head = this.table[hash];
//发生hash冲突,搜索链表
while (head != null){
//如果Key相等,覆盖Value
if(key == head.key){
head.value = value;
flag = false;
break;
}
//如果hash冲突没有发生覆盖,使用尾插法,让最后一个结点的next指向新结点
if(head.next == null){
head.next = node;
break;
}
head = head.next;
}
// //如果hash冲突没有发生覆盖,使用头插法,插到第一个结点之前
// if(flag){
// head = this.table[hash];
// this.table[hash] = node;
// node.next = head;
// }
}
if(flag) this.size++;
//如果元素数量超过负载因子*容量,扩容
if(size > capacity * loadFactor) resize();
}
V get(K key){
V value = null;
int hash = Hash(key);
if(this.table[hash] == null){
return value;
}else{
Node<K,V> head = this.table[hash];
while (head != null){
if(head.key == key){
value = head.value;
break;
}
head = head.next;
}
return value;
}
}
void resize(){
//老的table
Node<K,V>[] oldTable = this.table;
//新的table,容量2倍
this.table = (Node<K, V>[]) Array.newInstance(Node.class,2 * capacity);
this.capacity *= 2;
//遍历老table,把键值对都put到新table中
for (int i = 0;i < oldTable.length; i++){
if(oldTable[i] != null){
Node<K,V> head = oldTable[i];
while (head != null) {
put(head.key,head.value);
head = head.next;
}
}
}
}
//向上取2的n次方
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : n + 1;
}
}
class Node<K,V>{
K key;
V value;
int hash;
Node<K,V> next;
public Node(K key, V value, int hash) {
this.key = key;
this.value = value;
this.hash = hash;
}
}
自己也只是按照流程仿写了一下,许多HashMap中的方法都没有写,目的只是为了加深自己对HashMap的认识,其中可能也有不足的地方需要指正