基本概念
HashMap又叫哈希表、散列表,是一种以key/value方式存储数据的数据结构,它利用不重复、无序的键实现了快速查找。每个key对应唯一的value,查询和修改的效率都很快,能达到O(1)的平均时间复杂度。它是非线程安全的,且不保证元素存储的顺序。
继承关系
public class HashMap extends AbstractMap
implements Map, Cloneable, Serializable {
- 继承自
AbstractMap
,并实现Map
,具备Map
的所有功能 - 实现了
Cloneable
,可以被复制,原型设计模式 - 实现了
Serializable
,可以被序列化
储存结构
- 蓝色部分是数组,数组的每一个元素也叫做桶,但插入元素和已有元素发生Hash碰撞时,这两个Hash碰撞的元素将会以单链表方式存放在同一个桶里。
- 在
JDK1.8
版本之前,采用数组+链表的方式,当发生Hash碰撞的次数足够多时,桶内的单链表将会变的非常长,查找元素的时间复杂度将会退化成O(n)。为了解决这个问题,在JDK1.8
之后,引入了红黑树,在桶里单链表长度>=8
时,单链表将会转换成红黑树,查找时间复杂度从O(n)降低到O(log n),极大的提升了查找效率。在桶内单链表长度<=6
,红黑树转换回单链表的方式存储。
构造函数
- 无参构造
static final float DEFAULT_LOAD_FACTOR = 0.75f;
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
}
无参构造扩容因子默认值0.75f
- int参数构造
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
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 >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
- 传入
initialCapacity
初始容量,将initialCapacity
和默认扩容因子传入调用自身HashMap(int initialCapacity, float loadFactor)
构造方法 -
HashMap(int initialCapacity, float loadFactor)
判断传入的初始容量initialCapacity
是否合法,必须大于0。 - tableSizeFor(initialCapacity)方法计算是否需要扩容的阀值,并赋值给
threshold
,
tableSizeFor(int cap)
保证扩容阀值为2的幂次,接下来重点看下这个方法是怎么保证为2的幂次的。
举个例子
假如tableSizeFor(int cap)方法传入的参数是10,
int n = cap - 1;
//n=9 转二进制 1001 补足8位= 0000 1001
n |= n >>> 1;
// 9|9无符号右移1位 = 0000 1001|0000 0100 = 0000 1101
n |= n >>> 2;
//0000 1101|0000 0011 = 0000 1111 //后面的值不再有变化,都是一样
n |= n >>> 4;
// 0000 1111|0000 0000 = 0000 1111
n |= n >>> 8;
//0000 1111
n |= n >>> 16;
//0000 1111
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
//后面return的值等于 0000 1111 + 1 = 0001 0000 再转10进制 = 2的4次方 =16
再解释一下为什么需要int n = cap - 1
,为了避免参数cap本来就是2的幂次方,经过后续的未操作的,cap将会变成2 * cap,不符合预期。
为什么扩容阀值threshold要保证为2的幂次?
threshold要保证为2的幂次方主要原因是HashMap中数据存储有关,HashMap中key的Hash值由hash(Object key)方法计算得来。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
- HashMap中存储数据table的index是由key的Hash值决定的。
- 在HashMap存储数据的时候,我们期望数据能够均匀分布,以避免哈希冲突。我们首先可能会想到采用%取余的操作来实现。
- 取余(%)操作中如果除数是2的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是2的幂次方。
- 采用二进制位操作 &,相对于%能够提高运算效率,这就是HashMap的长度为什么是2的幂次方的原因。
- Map参数构造
public HashMap(Map extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
final void putMapEntries(Map extends K, ? extends V> m, boolean evict) {
int s = m.size();
if (s > 0) {
if (table == null) {
float ft = ((float)s / loadFactor) + 1.0F;
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
if (t > threshold)
threshold = tableSizeFor(t);
}
else if (s > threshold)
resize();
for (Map.Entry extends K, ? extends V> e : m.entrySet()) {
K key = e.getKey();
V value = e.getValue();
putVal(hash(key), key, value, false, evict);
}
}
}
- 扩容因子默认值
0.75f
-
putMapEntries
方法将传入的Map或Map子类传入放入自身进行存储
put(K key, V value)方法
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
put调用了内部的putVal(hash(key), key, value, false, true)
@param hash -> key.hashCode())^ (h >>> 16) 这里可以认为是key的hash值
@param key
@param value
@param onlyIfAbsent 是否不改变现有的值
@param evict 如果为false,则表处于创建模式。
@return previous value, or null if none
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node[] tab; Node p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;//如果是第一次put,初始化tab
if ((p = tab[i = (n - 1) & hash]) == null)
// 通过hash计算当前的下标索引对应的value为null,创建新的链表节点赋值
tab[i] = newNode(hash, key, value, null);
else {
Node e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
// 如果key相同,p赋值给e
e = p;
else if (p instanceof TreeNode)
// 如果p是红黑树类型,调用putTreeVal方式赋值
e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
else {
//单链表
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
// 如果p的next为空,将新的value值添加至链表后面
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // 循环初始值0 ,所以-1
//static final int TREEIFY_THRESHOLD = 8;
// 如果链表长度大于等于8,链表转化为红黑树,执行插入
treeifyBin(tab, hash);
break;
}
// key相同则跳出循环
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;
//已存在相同的key,覆盖并返回旧的value
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);//给子类的回调方法
return oldValue;
}
}
++modCount;
if (++size > threshold)
// size大于加载因子,扩容
resize();
afterNodeInsertion(evict);//给子类的回调方法
return null;
}
扩容resize()方法
final Node[] resize() {
Node[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {//旧表为空,不是第一次put
// 超过容量最大值就不再扩容了,就只好返回旧表 基本不可能发生
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 没超过最大值,扩容oldThr << 1,即原来的2倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1;
}
else if (oldThr > 0)
newCap = oldThr;// 新的容量大小 = 旧扩容阀值
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 计算新的扩容阀值 容量*扩容因子 默认扩容因子0.75
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node[] newTab = (Node[])new Node[newCap];
table = newTab;
if (oldTab != null) {
// 把每个数组元素(桶)都移动到新的桶中
for (int j = 0; j < oldCap; ++j) {
Node e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode)e).split(this, newTab, j, oldCap);
else { // 保证顺序
Node loHead = null, loTail = null;
Node hiHead = null, hiTail = null;
Node next;
do {
next = e.next;
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);
// 原索引放到bucket里
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
// 原索引+oldCap放到bucket里
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
get(Object key)方法
public V get(Object key) {
Node e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
- 调用
tab[(n - 1) & hash]
方法获取到key的hash对应的数组元素(桶) - 调用
getNode()
方法通过key和hash获取对应的value。不存在则返回null
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)
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;
}
- 1.若数组table不为null且长度大于0且其索引为(n - 1) & hash(等价于hash%n)的节点不为null。其中n为数组长度,hash为插入的键值对的key的哈希值。则进入下一步,否则直接返回null
- 2.判断首节点的key和hash是否与入参一致,若相同则返回首节点,否则进入下一步。
- 3.判断节点个数只有1个,若是则返回null,否则进入下一步
- 4.判断首节点是否为树节点,若是则遍历红黑树,否则为链表,进入下一步
- 5.遍历链表,检索key和hash与入参相同的节点,若找到则返回该节点,否则返回null
remove(Object key)/remove(Object key, Object value)方法
public V remove(Object key) {
Node e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
public boolean remove(Object key, Object value) {
return removeNode(hash(key), key, value, true, true) != null;
}
remove调用removeNode方法
@param hash -> key.hashCode())^ (h >>> 16) 这里可以认为是key的hash值
@param key
@param value 要删除的键值对的value,该值是否作为删除的条件取决于matchValue是否为true
@param matchValue 如果为true,则当key对应的键值对的值equals(value)为true时才删除;否则不关心value的值
@param movable 删除后是否移动节点,如果为false,则不移动
@return 返回节点,如果没有,则返回null
final Node removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
Node[] tab; Node p; int n, index; // 声明节点数组、当前节点、数组长度、索引值
/*
* 如果 节点数组tab不为空、数组长度n大于0、根据hash定位到的节点对象p(该节点为 树的根节点 或 链表的首节点)不为空
* 需要从该节点p向下遍历,找到那个和key匹配的节点对象
*/
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
Node node = null, e; K k; V v; // 定义要返回的节点对象,声明一个临时节点变量、键变量、值变量
// 如果当前节点的键和key相等,那么当前节点就是要删除的节点,赋值给node
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
/*
* 到这一步说明首节点没有匹配上,那么检查下是否有next节点
* 如果没有next节点,就说明该节点所在位置上没有发生hash碰撞, 就一个节点并且还没匹配上,也就没得删了,最终也就返回null了
* 如果存在next节点,就说明该数组位置上发生了hash碰撞,此时可能存在一个链表,也可能是一颗红黑树
*/
else if ((e = p.next) != null) {
// 如果当前节点是TreeNode类型,说明已经是一个红黑树,那么调用getTreeNode方法从树结构中查找满足条件的节点
if (p instanceof TreeNode)
node = ((TreeNode)p).getTreeNode(hash, key);
// 如果不是树节点,那么就是一个链表,只需要从头到尾逐个节点比对即可
else {
do {
// 如果e节点的键是否和key相等,e节点就是要删除的节点,赋值给node变量,调出循环
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
// 走到这里,说明e也没有匹配上
p = e; // 把当前节点p指向e,这一步是让p存储的永远下一次循环里e的父节点,如果下一次e匹配上了,那么p就是node的父节点
} while ((e = e.next) != null); // 如果e存在下一个节点,那么继续去匹配下一个节点。直到匹配到某个节点跳出 或者 遍历完链表所有节点
}
}
/*
* 如果node不为空,说明根据key匹配到了要删除的节点
* 如果不需要对比value值 或者 需要对比value值但是value值也相等
* 那么就可以删除该node节点了
*/
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
if (node instanceof TreeNode) // 如果该节点是个TreeNode对象,说明此节点存在于红黑树结构中,调用removeTreeNode方法(该方法单独解析)移除该节点
((TreeNode)node).removeTreeNode(this, tab, movable);
else if (node == p) // 如果该节点不是TreeNode对象,node == p 的意思是该node节点就是首节点
tab[index] = node.next; // 由于删除的是首节点,那么直接将节点数组对应位置指向到第二个节点即可
else // 如果node节点不是首节点,此时p是node的父节点,由于要删除node,所有只需要把p的下一个节点指向到node的下一个节点即可把node从链表中删除了
p.next = node.next;
++modCount; // HashMap的修改次数递增
--size; // HashMap的元素个数递减
afterNodeRemoval(node); // 调用afterNodeRemoval方法,该方法HashMap没有任何实现逻辑,目的是为了让子类根据需要自行覆写
return node;
}
}
return null;
}