提示:下方有源代码地址,请自行拿取
你是不是还在面试时被面试官问懵HashMap?不会手写实现一个简单HashMap?看完这篇文章你再不会算我输!
提示:以下是本篇文章正文内容,案例仅供参考
基于哈希表的 Map 接口的实现。此实现提供所有可选的映射操作,并允许使用 null 值和 null 键。(除了非同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同。)此类不保证映射的顺序,特别是它不保证该顺序恒久不变。 此实现假定哈希函数将元素适当地分布在各桶之间,可为基本操作(get 和 put)提供稳定的性能。迭代 collection 视图所需的时间与 HashMap 实例的“容量”(桶的数量)及其大小(键-值映射关系数)成比例。所以,如果迭代性能很重要,则不要将初始容量设置得太高(或将加载因子设置得太低)。 --摘自百度百科
首先要明白在JDK1.7时HashMap的底层是由数组+链表实现的,到了JDK1.8后改成了数组+链表+红黑树实现,接下来我将对这几种数据结构详细讲解,并且
特点:
单向链表是链表的一种,它由节点组成,每个节点都包含下一个节点的指针。
特点
适用于节点的增加删除。
双向链表是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。一般我们都构造双向循环链表。
特点
红黑树是一种特定类型的二叉树,也是一种平衡二叉查找树的变体,它的左右子树高差有可能大于 1,所以红黑树不是严格意义上的平衡二叉树,由于每一棵红黑树都是一颗二叉排序树,因此,在对红黑树进行查找时,可以采用运用于普通二叉排序树上的查找算法。
特点
这些约束强制了红黑树的关键性质: 从根到叶子的最长的可能路径不多于最短的可能路径的两倍长。结果是这个树大致上是平衡的。因为操作比如插入、删除和查找某个值的最坏情况时间都要求与树的高度成比例,这个在高度上的理论上限允许红黑树在最坏情况下都是高效的,而不同于普通的二叉查找树
OK,了解了以上数据结构后咱们再来看看HashMap底层源码是如何实现的,
先看下HashMap的存储结构
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node[] tab; Node p; int n, i;
//1. 如果当前table为空,新建默认大小的table
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//2. 获取当前key对应的节点
if ((p = tab[i = (n - 1) & hash]) == null)
//3. 如果不存在,新建节点
tab[i] = newNode(hash, key, value, null);
else {
//4. 存在节点
Node e; K k;
//5. key的hash相同,key的引用相同或者key equals,则覆盖
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//6. 如果当前节点是一个红黑树树节点,则添加树节点
else if (p instanceof TreeNode)
e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
//7. 不是红黑树节点,也不是相同节点,则表示为链表结构
else {
for (int binCount = 0; ; ++binCount) {
//8. 找到最后那个节点
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//9. 如果链表长度超过8转成红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//10.如果链表中有相同的节点,则覆盖
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
//是否替换掉value值
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
//记录修改次数
++modCount;
//是否超过容量,超过需要扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
1.计算关于key的hashcode值
2.如果散列表为空时,调用resize()初始化散列表
3.如果没有发生碰撞,直接添加元素到散列表中去
4.如果发生了碰撞(hashCode值相同),进行三种判断
1:若key地址相同或者equals后内容相同,则替换旧值
2:如果是红黑树结构,就调用树的插入方法
3:链表结构,循环遍历直到链表中某个节点为空,尾插法进行插入,插入之后判断链表个数是否到达变成红黑树的阙值8;也可以遍历到有节点与插入元素的哈希值和内容相同,进行覆盖。
5.如果桶满了大于阀值,则resize进行扩容
final Node getNode(int hash, Object key) {
Node[] tab; Node first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
if (first instanceof TreeNode)
//若定位到的节点是 TreeNode 节点,则在树中进行查找
return ((TreeNode)first).getTreeNode(hash, key);
do {//否则在链表中进行查找
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
final TreeNode getTreeNode(int h, Object k) {
return ((parent != null) ? root() : this).find(h, k, null);
}
final TreeNode find(int h, Object k, Class> kc) {
TreeNode p = this;
do {
int ph, dir; K pk;
TreeNode pl = p.left, pr = p.right, q;
//首先进行hash 值的比较,若不同令当前节点变为它的左孩子或者右孩子
if ((ph = p.hash) > h)
p = pl;
else if (ph < h)
p = pr;
//hash 值相同,进行 key值的比较
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
else if (pl == null)
p = pr;
else if (pr == null)
p = pl;
/hash 值相同,key 值不同 ,若k 是可比较的并且k.compareTo(pk) 返回结果不 为0可进入下面else if
else if ((kc != null ||
(kc = comparableClassFor(k)) != null) &&
(dir = compareComparables(kc, k, pk)) != 0)
p = (dir < 0) ? pl : pr;
若 k 是不可比较的 或者 k.compareTo(pk) 返回结果为0则在整棵树中进行查找,先找右子树,右子树没有再找左子树
else if ((q = pr.find(h, k, kc)) != null)
return q;
else
p = pl;
} while (p != null);
return null;
}
扩容的时候,HashMap是把长度扩为原来2倍,所以,元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置。
final Node[] resize() {
Node[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
/*
1、resize()函数在size > threshold时被调用。
oldCap大于 0 代表原来的 table 表非空, oldCap 为原表的大小,
oldThr(threshold) 为 oldCap × load_factor
*/
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
/*
2、resize()函数在table为空被调用。
oldCap 小于等于 0 且 oldThr 大于0,代表用户创建了一个 HashMap,但是使用的构造函数为
HashMap(int initialCapacity, float loadFactor) 或 HashMap(int initialCapacity)
或 HashMap(Map extends K, ? extends V> m),导致 oldTab 为 null,oldCap 为0,
oldThr 为用户指定的 HashMap的初始容量。
*/
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
/*
3、resize()函数在table为空被调用。
oldCap 小于等于 0 且 oldThr 等于0,用户调用 HashMap()构造函数创建的 HashMap,所有值均采用默认值,
oldTab(Table)表为空,oldCap为0,oldThr等于0,
*/
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
Node[] newTab = (Node[])new Node[newCap];
table = newTab;
if (oldTab != null) {
//把 oldTab 中的节点 reHash 到 newTab 中去
for (int j = 0; j < oldCap; ++j) {
Node e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
//若节点是单个节点,直接在 newTab 中进行重定位
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
//若节点是 TreeNode 节点,要进行 红黑树的 rehash 操作
else if (e instanceof TreeNode)
((TreeNode)e).split(this, newTab, j, oldCap);
//若是链表,进行链表的 rehash 操作
else { // preserve order
Node loHead = null, loTail = null;
Node hiHead = null, hiTail = null;
Node next;
do {
next = e.next;
//根据算法 e.hash & oldCap 判断节点位置 rehash 后是否发生改变
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
// rehash 后节点新的位置一定为原来基础上加上 oldCap
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
//这个函数的功能是对红黑树进行 rehash 操作
final void split(HashMap map, Node[] tab, int index, int bit) {
TreeNode b = this;
// Relink into lo and hi lists, preserving order
TreeNode loHead = null, loTail = null;
TreeNode hiHead = null, hiTail = null;
int lc = 0, hc = 0;
//由于TreeNode 节点之间存在双端链表的关系,可以利用链表关系进行 rehash
for (TreeNode e = b, next; e != null; e = next) {
next = (TreeNode)e.next;
e.next = null;
if ((e.hash & bit) == 0) {
if ((e.prev = loTail) == null)
loHead = e;
else
loTail.next = e;
loTail = e;
++lc;
}
else {
if ((e.prev = hiTail) == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
++hc;
}
}
//rehash 操作之后注意对根据链表长度进行 untreeify 或 treeify 操作
if (loHead != null) {
if (lc <= UNTREEIFY_THRESHOLD)
tab[index] = loHead.untreeify(map);
else {
tab[index] = loHead;
if (hiHead != null) // (else is already treeified)
loHead.treeify(tab);
}
}
if (hiHead != null) {
if (hc <= UNTREEIFY_THRESHOLD)
tab[index + bit] = hiHead.untreeify(map);
else {
tab[index + bit] = hiHead;
if (loHead != null)
hiHead.treeify(tab);
}
}//end if
}//end split
HyhMap接口
package com.hyh.core.test.map;
/**
* 手写实现Map接口
*
* @Author heyuhua
* @create 2021/2/9 15:29
*/
public interface HyhMap {
/**
* PUT接口
*
* @param k
* @param v
*/
void put(K k, V v);
/**
* GET接口
*
* @param k
* @return
*/
V get(K k);
/**
* 获取map大小接口
*
* @return
*/
int size();
/**
* Entry 接口
*
* @param
* @param
*/
interface Entry {
/**
* 获取KEY值
*
* @return
*/
K getKey();
/**
* 获取Value值
*
* @return
*/
V getValue();
}
}
HyhHashMap实现类
package com.hyh.core.test.map;
import java.io.Serializable;
/**
* 手写实现HashMap
*
* @Author heyuhua
* @create 2021/2/9 15:31
*/
public class HyhHashMap implements HyhMap, Serializable {
/**
* 默认容量
*/
static final int DEFAULT_CAPACITY = 16;
int threshold;
/**
* 当前key索引位置
*/
int keyIndex;
/**
* 负载因子
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* 保存Node节点的数组
*/
Node[] table;
/**
* 存储当前Map容量的大小
*/
int size;
@Override
public void put(K key, V value) {
Node node;
if (table == null) {
table = resize();
//table里面为空的情况
node = new Node(hash(key), key, value, null);
table[keyIndex] = node;
size++;
} else {
table = resize();
//table不为空时
Node n;
//是否hash冲突
boolean hashConflict = false;
for (int i = 0; i < table.length; i++) {
n = table[i];
if (n != null) {
if (n.hash == hash(key)) {
hashConflict = true;
//hash相等时
while (n != null) {
if (n.key.equals(key)) {
//hash相等并且key也相等,直接替换原来的值就行了
n.value = value;
table[i] = n;
size++;
} else {
node = new Node(hash(key), key, value, null);
node.next = n;
table[i] = node;
size++;
}
n = n.next;
}
}
}
}
if (!hashConflict) {
//没有hash冲突,直接put
node = new Node(hash(key), key, value, null);
table[++keyIndex] = node;
size++;
}
}
}
@Override
public V get(K key) {
HyhHashMap.Node node;
return (node = getNode(key)) == null ? null : node.value;
}
/**
* 获取Node
*
* @param key
* @return
*/
final HyhHashMap.Node getNode(Object key) {
if (table != null) {
for (int i = 0; i < table.length; i++) {
Node node = table[i];
if (node != null) {
//hash相等
if (node.hash == hash(key)) {
while (node != null) {
if (node.key.equals(key)) {
//hash和key都相等时`
return node;
}
node = node.next;
}
}
}
}
}
return null;
}
/**
* 扩容
*
* @return
*/
final Node[] resize() {
Node[] newTable;
int newCapacity, oldCapacity;
if (table == null) {
keyIndex = 0;
threshold = (int) (DEFAULT_CAPACITY * DEFAULT_LOAD_FACTOR);
table = (HyhHashMap.Node[]) new HyhHashMap.Node[DEFAULT_CAPACITY];
newTable = table;
} else {
oldCapacity = table.length;
if (table.length > threshold) {
//扩容两倍
newCapacity = threshold *= 2;
newTable = (HyhHashMap.Node[]) new HyhHashMap.Node[newCapacity];
//把原来的table移动到newTable
int newIndex = 0;
for (int i = 0; i < oldCapacity; i++) {
Node node = table[i];
//咱们这只使用最简单的方式、不考虑其他情况、不涉及红黑树
if (node != null) {
if (node.next == null)
newTable[newIndex] = node;
else {
HyhHashMap.Node loHead = null, loTail = null, hiHead = null, hiTail = null, next;
do {
next = node.next;
if (node.hash == 0) {
if (loTail == null)
loHead = node;
else
loTail.next = node;
loTail = node;
} else {
if (hiTail == null)
hiHead = node;
else
hiTail.next = node;
hiTail = node;
}
} while ((node = next) != null);
if (loTail != null) {
loTail.next = null;
newTable[newIndex] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTable[newIndex + oldCapacity] = hiHead;
}
}
}
newIndex++;
}
} else {
newTable = table;
}
}
return newTable;
}
/**
* 计算Hash值
*
* @param key
* @return
*/
/**
* 计算Hash值
*
* @param key
* @return
*/
static final int hash(Object key) {
int h;
return (key == null) ? 0 : Math.abs((h = key.hashCode()) ^ (h >>> 16));
}
@Override
public int size() {
return size;
}
/**
* Node 实现HyhMap Entry接口
*
* @param
* @param
*/
static class Node implements HyhMap.Entry {
//hash值
final int hash;
// key
final K key;
// value
V value;
// next节点
HyhHashMap.Node next;
public Node(int hash, K key, V value, Node next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
public final K getKey() {
return key;
}
public final V getValue() {
return value;
}
}
}
OK,不废话,直接测试一下我花了半个小时写的HashMap,当然我这里并没有加入红黑树,性能和JDK的HashMap肯定没得比,所以兄弟们,别喷我哈!
package com.hyh.core.test.map;
import org.junit.Test;
/**
* HyhHasMap Test
*
* @Author heyuhua
* @create 2021/2/9 17:54
*/
public class HyhHashMapTest {
/**
* 普通测试
*/
@Test
public void test() {
HyhHashMap hyhHashMap = new HyhHashMap<>();
hyhHashMap.put("name", "heyuhua");
hyhHashMap.put("height", "180cm");
hyhHashMap.put("name", "hyh");
hyhHashMap.put("height", "180");
System.out.println("name:" + hyhHashMap.get("name") + ",height:" + hyhHashMap.get("height"));
}
/**
* Hash冲突测试
*/
@Test
public void testHashConfilct() {
HyhHashMap hyhHashMap = new HyhHashMap<>();
hyhHashMap.put("轷龚", "heyuhua1");
hyhHashMap.put("轸齻", "heyuhua2");
System.out.println("轷龚:" + hyhHashMap.get("轷龚") + ",轸齻:" + hyhHashMap.get("轸齻"));
}
}
看看Map里面的东西
当然了,这花半个小时写的肯定会有bug,代码仅供参考,旨在让大家能够更加深入的了解HashMap原理。
OK,大致的了解一下底层源码后,我们对HashMap有了一定深入的了解,接下来列举一些HashMap面试常问的问题
1. HashMap的特性?
2. HashMap底层原理?
3. HashMap put原理?
4. HashMap get原理?
5. HashMap扩容机制?
6. HashMap默认初始化长度为16,并且每次自动扩展或者是手动初始化容量时,为什么必须是2的次幂?
为了数据的均匀分布,减少哈希碰撞。因为确定数组位置是用的位运算,若数据不是2的次幂则会增加哈希碰撞的次数和浪费数组空间。
输入数据若不是2的幂,HashMap通过一通位移运算和或运算得到的肯定是2的幂次数,并且是离那个数最近的数字
7. HashMap大小超过了负载因子(load factor)定义的容量,怎么办?
路漫漫其修远兮,吾愿与君上下而求索,非常感谢各位帅哥、靓妹的点赞、收藏和评论,我们下期见。
源码地址:点此查看源码.