简介:
基于哈希表的 Map 接口的实现。此实现提供所有可选的映射操作,并允许使用 null 值和 null 键。(除了非同步和允许使用 null 之外,HashMap 类与 HashTable 大致相同。)此类不保证映射的顺序,特别是它不保证该顺序恒久不变。 此实现假定哈希函数将元素适当地分布在各桶之间,可为基本操作(get 和 put)提供稳定的性能。迭代 collection 视图所需的时间与 HashMap 实例的“容量”(桶的数量)及其大小(键-值映射关系数)成比例。所以,如果迭代性能很重要,则不要将初始容量设置得太高(或将加载因子设置得太低)
类图结构:
特性:
1 HashMap 允许key,value都为空。
2 HashMap 在操作键值对时不是线程安全的。
3 HashMap 底层以数组+单链表(节点数大于8则,单链表转红黑树)存储键值对。
4 HashMap实际就是 Node<K,V>[] table 数组,Node<K,V>节点实现了Map.Entry<K,V>。
主要方法:
1 put 方法详解:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
// 根据key计算hash值
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
// 根据hash值,将键值对存放到HashMap中
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 判断Node数组是否为空,空的就需要对数组初始化(resize 中会判断是否进行初始化)。
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// (n - 1) & hash]计算数组下标,如果数组下标所在位置的节点为空(为空表明没有 Hash 冲突)就直接在当前位置创建一个新Node节点即可。
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
// 如果当前桶(数组下标)有值( Hash 冲突),那么就要比较当前桶中的 key、key 的 hashcode 与写入的 key 是否相等,相等就赋值给 e
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);
// 如果是个链表,就需要将当前的 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;
}
// 如果在遍历过程中找到 key 相同时直接退出遍历。
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 如果存在相同的 key,那就需要将值覆盖。
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;
}
根据key计算数组下标的步骤如下:
(1)h = key.hashCode():根据key值计算hashCode。这里的 hashCode() 是一个 native 方法,根据一定的规则将与对象相关的信息(比如对象的存储地址,对象的字段等)映射成一个数值,这个数值称作为散列值。
(2)h >>> 16:将得到的hashCode无符号右移16位,低位移出(舍弃),高位的空位补符号位,即正数补0,负数补1。
(3 )(h = key.hashCode()) ^ (h >>> 16):h与h右移16位进行异或运算(两者相等为0,不等为1)得到hash值。
(4)计算(n - 1) & hash 的值即为数组下标。n代表数组大小,初始化时默认是16。
2 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;
// 根据key和hash定位到数组下标
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// 否则判断桶的第一个位置(有可能是链表、红黑树)的 key 是否为查询的 key,是就直接返回 。
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;
}
3 resize 方法详解:
// 初始化或加倍数组大小 : 如果为null,则分配初始容量。否则,进行2次幂扩展。
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
// 旧的数组容量
int oldCap = (oldTab == null) ? 0 : oldTab.length;
// 旧的扩容阈值
int oldThr = threshold;
// 声明新的数组容量和扩容阈值
int newCap, newThr = 0;
// 旧的数组容量大于0说明本次是扩容操作
if (oldCap > 0) {
// 扩容前的数组大小如果已经达到最大(2^30)了
if (oldCap >= MAXIMUM_CAPACITY) {
// 修改阈值为int的最大值(2^31-1),这样以后就不会扩容了
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 扩容:oldCap*2,即把新的数组容量 "newCap" 扩大2倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
// 新的扩容阈值同样扩大2倍
newThr = oldThr << 1;
}
// 当 oldThr > 0 就说明用户调用了有参构造方法(指定了初始容量,并被构造方法 "缓存" 到了threshold中了)
else if (oldThr > 0)
newCap = oldThr;
// 初始化的数组容量为缺省的 16,初始化的扩容阈值为缺省的 16 * 0.75
else {
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 第一次resize()时,计算扩容阈值
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<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
//不是第一次reizie()时
if (oldTab != null) {
// 遍历之前的table,重新hash排序
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
// 删除了旧节点的引用
oldTab[j] = null;
// 当桶里只有一个节点而没有链表,重新计算索引位置落桶
if (e.next == null)
// 注意:扩容前的元素地址为 (oldCap - 1) & e.hash ,所以这里的新的地址只有两种可能,一是地址不变,二是变为 老位置+oldCap
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
// 创建两条链表, loHead是用来保存新链表上的头元素的,loTail是用来保存尾元素的,直到遍历完链表。
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
// 这里如果判断成立,那么该元素的地址在新的数组中就不会改变。因为oldCap的最高位的1,在e.hash对应的位上为0,所以扩容后得到的地址是一样的,位置不会改变 ,在后面的代码的执行中会放到loHead中去,最后赋值给newTab[j];
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
// 如果判断不成立,那么该元素的地址变为 原下标位置+oldCap,也就是lodCap最高位的1,在e.hash对应的位置上也为1,所以扩容后的地址改变了,在后面的代码中会放到hiHead中,最后赋值给newTab[j + oldCap]
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;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
上述 (e.hash & oldCap) == 0 即可将原桶中的数据分成2类:元素的位置要么是在原位置,要么是“j + oldCap”(当前位置索引+原容量的值)。这也是resize方法扩容后为什么是原来的2倍的原因。
JDK1.7中,resize时,index取得时,全部采用重新hash的方式进行了。JDK1.8对这个进行了改善:
以前要确定index的时候用的是(e.hash & oldCap-1),是取模取余,而这里用到的是(e.hash & oldCap),它有两种结果,一个是0,一个是oldCap,比如oldCap=8,hash是3,11,19,27时,(e.hash & oldCap)的结果是0,8,0,8,这样3,19组成新的链表,index为3;而11,27组成新的链表,新分配的index为3+8;
JDK1.7中重写hash是(e.hash & newCap-1),也就是3,11,19,27对16取余,也是3,11,3,11,和上面的结果一样,但是index为3的链表是19,3,index为3+8的链表是27,11,也就是说1.7中经过resize后数据的顺序变成了倒叙,而JDK1.8没有改变顺序。