参考博文:https://fangjian0423.github.io/2016/03/29/jdk_hashmap/
参考文章: http://wiki.jikexueyuan.com/project/java-enhancement/java-twentythree.html
1. HashMap实现了 Map 接口,继承 AbstractMap。其中 Map 接口定义了键映射到值的规则,而 AbstractMap 类提供 Map 接口的骨干实现,以最大限度地减少实现此接口所需的工作,其实 AbstractMap 类已经实现了Map。
public class HashMap
extends AbstractMap
implements Map, Cloneable, Serializable
2.HashMap中几个变量
initialCapacity 和 loadFactor这两个变量决定了HashMap的性能,initialCapacity初始容量表示哈希表中桶的数量,loadFactor加载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度,它衡量的是一个散列表的空间的使用程度,负载因子越大表示散列表的装填程度越高,反之愈小。对于使用链表法的散列表来说,查找一个元素的平均时间是 O(1+a),因此如果负载因子越大,对空间的利用更充分,然而后果是查找效率的降低;如果负载因子太小,那么散列表的数据将过于稀疏,对空间造成严重浪费。系统默认负载因子为 0.75,一般情况下我们是无需修改的。
其他的一些重要属性:
transient Node[] table; // 哈希表数组
transient int size; // 键值对个数
/* 阀值。 值 = 容量 * 加载因子。
* 默认值为12(16(默认容量) * 0.75(默认加载因子))。
* 当哈希表中的键值对个数超过该值时,会进行扩容
*/
int threshold;
3.HashMap 的基本原理
(一),hash算法:把数据的 key 转化为 hash 值,放到某 n 长度的数组中改 hash 对应的 position,比如 hash 为 0,则放在数组的第1个位置,如果 hash 为111,则放在数组的第 112 个位置。这样每次根据 hash 值就能直接知道放在什么位置,或者反过来,根据 hash 值就能直接知道从哪个位置取值。
针对上述核心原理的几个常见疑问:
一,hash值很大时,需要的数组也要很大?
这个问题好解决,hash按数组长度取模,这样无论多大的 hash 总能在数组的范围之内,随便一提,取模操作为:(n - 1) & hash
二,取模后,总会有两个不一样的hash值取模得到相同的值,这个时候,该怎么办?
这种问题就叫做碰撞冲突,HashMap的源码中使用冲突解决方法是使用单独链表法,如下图:
(二),put操作:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
/**
* Implements Map.put and related methods
*
* @param hash hash for key
* @param key the key
* @param value the value to put
* @param onlyIfAbsent if true, don't change existing value
* @param evict if false, the table is in creation mode.
* @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; //hash表为空时,使用resize重新构建,进行扩容
// 注意 i = (n - 1) & hash 就是取模定位数组的索引
if ((p = tab[i = (n - 1) & hash]) == 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))))
// 已经存在一个一模一样的值
e = p;
else if (p instanceof TreeNode) //使用红黑树
e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
else {
// 碰撞冲突,顺着链表的next指针找到最后一个
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操作
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) {
// 如果哈希表容量为0或者关键字没有命中,直接返回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;
}
(四),hash过程和resize过程分析
hash函数如下:
static final int hash(Object key) {
int h;
// 使用hashCode的值和 hashCode的值无符号右移16位 做异或操作
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
resize扩容过程:
final Node[] resize() {
Node[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) { // 如果老容量大于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; // 阀值加倍
}
else if (oldThr > 0) // 根据thresold初始化数组
newCap = oldThr;
else { // 使用默认配置
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;
@SuppressWarnings({"rawtypes","unchecked"})
Node[] newTab = (Node[])new Node[newCap];
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) { // 扩容之后进行rehash操作
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);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
其他
1.巧妙的取模
加入数组长度是 n, 如果要对 hash 取模,大家可能想到的解法是: hash%n;而 HashMap 采用的方法是hash & (n - 1),这是为什么呢?
因为n 是 2 的次方,所以 n - 1 的而进制01111111111..,
hash “与” 01111111111实际上是取保留低位值,
结果在 n 的范围之内,类似于取模。
2.分析源码之后,我们很容易就发现了一个规律,那就是,HashMap的容量一直都是2的幂次方,但是,为什么要让HashMap的容量拥有这个特性呢?其实我们可以从性能角度去分析一下。
1. 取模快。
其实就是上面为什么快的原因:位与取模比 % 取模要快的多。
2. 分散平均,减少碰撞。
这个是主要原因。
如果二进制某位包含 0,则此位置上的数据不同对应的 hash 却是相同,碰撞发生,
而 (2^x - 1) 的二进制是 0111111…,分散非常平均,碰撞也是最少的。
HashMap底层是个哈希表,使用拉链法解决冲突
HashMap内部存储的数据是无序的,这是因为HashMap内部的数组的下表是根据hash值算出来的
HashMap允许key为null
HashMap不是一个线程安全的类