HashMap学习笔记
HashMap学习笔记
- 今天来学习面试以及平时工作学习中常用到的一个类HashMap。
概述:
- 什么是HashMap
- 常见操作(get和put)的工作原理
- Hash函数的实现
- Resize是什么
一、什么是HashMap
-
官方描述:
关键信息:基于Map、和HashTable类似但HashMap不能保证线程安全且允许键值对为null、不保证有序(个人觉得所谓的序是指“put”进来的顺序)、不保证顺序不随时间改变(个人认为是由于reSize的原因)
HashMap的结构:基于链表加数组
-
值得一提的参数:
Initial Capacity
即table的大小,如果初始值大于允许的最大值N/0.75
,那么reHash操作永远不会发生,默认值是16。Load Factor
:0.75,达到了良好的时间和空间之间的平衡开销的一个点。>0.75
将会降低空间开销但会增加查找开销(在get和put操作中得以体现。-
MIN_TREEIFY_CAPACITY
:64,只有当able的大小达到64之后,链表转成树才可能实现,否则,当某节点的链长度大于8时只会发生table的resize操作。 -
UNTREEIFY_THRESHOLD
:8,当某节点的链表长度大于该值时,发生链表到树的转换操作。 -
UNTREEIFY_THRESHOLD
:6,当某节点的链表长度减少到该值时,发生树到链表的转换操作。
-
值得一提的
fail-fast
:- 如果map在迭代器生成后被修改了(不包含迭代器自身的移除操作),迭代器将会抛出
ConcurrentModificationException
,当面对同步修改操作时,迭代器会快速失败。迭代器的快速失败原则应当尽量只在debug时被使用到。
- 如果map在迭代器生成后被修改了(不包含迭代器自身的移除操作),迭代器将会抛出
二、HashMap的常见操作
-
put操作
-
步骤
- 将key的hashCode()做hash运算,再计算index
- 碰撞了?链表长度小于等于8?链表形式放入buckets
- 碰撞了?链表长度大于8了?链表转成红黑树
- 没碰撞?直接放入buckets
- 节点存在了?替换!
- bucket大小大于阈值了?resize
源码:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
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;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
-
-
get操作
-
步骤:
- buckets中第一个节点的hash值是目标key的hash值:直接返回
- 有冲突时:是树吗?
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
- 不是树时:在链表中get
代码:
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> 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<K,V>)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;
}
-
三、Hash函数的实现
- 代码:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
} - 生动描述:
四、Resize()是什么
当进行put操作时,当前所占bucket的程度已达到了 LoadFactor*Capacity时,就会触发resize操作,也就是把bucket的容量扩大两倍,然后重新计算各个节点的index值重新分配到新的bucket中。注释如下:
Resize的实现
当hash1新增的比特位(右边开始第五个)为0的时候,则index不变,如果是1的话,则为原位置加oldCapacity。这样就不需要经过复杂的index计算,是不是很精妙呢?