本文所有源码来自 JDK 1.8.0_181
HashMap简介
Map是Key-Value对映射的抽象接口,Map用于保存具有映射关系的数据。Map集合里有两组值,一组值用于保存Map里的key,另外一组值用于保存Map里的value,key和value都可以是任何引用类型的数据。key不允许重复,key和value之间存在单向一对一关系,通过key能找到相应的value。
HashMap是基于哈希表的Map接口的实现,以Key-Value的形式存在,即存储的对象是Entry(同时包含了Key和Value)。在HashMap中,根据hash算法来计算key-value的存储位置并进行快速存取。最多只允许一条Entry的键为Null,但允许多条Entry的值为Null。此外,HashMap是线程不安全的。
结构示意
当链表达到一定长度时会进行树化。
理想状态下哈希表的每个箱子中,元素的数量遵守泊松分布:
当负载因子为 0.75 时,上述公式中 λ 约等于 0.5,因此箱子中元素个数和概率的关系如下:
数量 | 概率 |
---|---|
0 | 0.60653066 |
1 | 0.30326533 |
2 | 0.07581633 |
3 | 0.01263606 |
4 | 0.00157952 |
5 | 0.00015795 |
6 | 0.00001316 |
7 | 0.00000094 |
8 | 0.00000006 |
这就是为什么箱子中链表长度超过8以后要变成红黑树,因为在正常情况下出现这种现象的几率小到忽略不计。一旦出现,几乎可以认为是哈希函数设计有问题导致的。Java对哈希表的设计一定程度上避免了不恰当的哈希函数导致的性能问题,每一个箱子中的链表可以与红黑树切换。
最基本的存储单位
一个结点也就是一个键值对
static class Node<K,V> implements Map.Entry<K,V> {
final int hash; // 散列值
final K key; // 键
V value; // 值
Node<K,V> next; // 下一个结点
Node(int hash, K key, V value, Node<K,V> 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; }
public final String toString() { return key + "=" + value; }
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
一些关键的属性
/* 默认初始化容量为16 */
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
/* 最大容量为2^30 */
static final int MAXIMUM_CAPACITY = 1 << 30;
/* 默认负载因子为0.75 */
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/* 一个桶中结点的存储方式由链表转换成树的阈值。即当桶中结点的数量超过
* TREEIFY_THRESHOLD时使用树来代替链表,默认值是8。 */
static final int TREEIFY_THRESHOLD = 8;
/* 当执行resize操作时,当桶中结点的数量少于UNTREEIFY_THRESHOLD时
* 使用链表来代替树,默认值是6。 */
static final int UNTREEIFY_THRESHOLD = 6;
/* 当桶中的结点被树化时最小的hash表容量(如果hash表容量小于
* MIN_TREEIFY_CAPACITY,当桶中bin的数量太多时会执行resize扩容操作),
* 这个MIN_TREEIFY_CAPACITY的值至少是TREEIFY_THRESHOLD的4倍。 */
static final int MIN_TREEIFY_CAPACITY = 64;
/*哈希表,在第一次使用被初始化,必要时扩容,长度总为2的整数幂*/
transient Node<K,V>[] table;
/*键值对的集合*/
transient Set<Map.Entry<K,V>> entrySet;
/*这个map中的键值对的数量*/
transient int size;
/*HashMap修改的次数*/
transient int modCount;
/*当HashMap的size大于threshold时会执行resize操作。*/
int threshold;
/*负载因子*/
final float loadFactor;
modCount的作用:HashMap是线程不安全的,在迭代时会将modCount赋值到迭代器的expectedModCount属性中,然后进行迭代,如果在迭代的过程中HashMap被其他线程修改了,modCount的数值就会发生变化,这个时候expectedModCount和ModCount不相等,会抛出ConcurrentModificationException()异常。
创建HashMap的方法
// 指定初始容量和负载因子
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);
}
// 指定初始容量
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
// 无参创建
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
}
// 创建HashMap的同时将指定的Map添加进去
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
tableSizeFor
/* 用于找到大于等于initialCapacity最小的2的整数幂 */
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;
}
putMapEntries
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);
}
}
}
扩容
// 初始化兼扩容
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
// 已经初始化过
if (oldCap > 0) {
// 扩容前的数组大小如果已经达到最大(2^30)了
// 修改阈值为int的最大值(2^31-1),这样以后就不会扩容了
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
//新数组长度变为原来的2倍,临界值也扩大为原来2倍
newThr = oldThr << 1;
}
// 带参数初始化
else if (oldThr > 0)
newCap = oldThr;
// 在默认无参数初始化
else {
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 如果新容量为0(初始化或扩容时容量已到最大的情况)
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
// 将临界值设置为新临界值
threshold = newThr;
// 扩容,将哈希表设为新表
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
// 若旧表中有数据,则需将数据复制到新表中
if (oldTab != null) {
// 遍历整个数组,将非空元素进行复制
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
// 获取数组的第j个元素
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
// 如果链表只有一个,则进行直接赋值
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
// 树型存储
// 关于红黑树的操作
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else {
// 链式存储
// 进行链表复制,并没有重新计算元素在数组中的位置
// 而是采用了原始位置加原数组长度的方法计算得到位置
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
// 处在一个桶中的hash值可能不一样
// 单链表
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;
}
扩容后有些结点的下标可能会改变,如图:
根据key获取相应结点
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 &&
((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);
}
key.hashCode()函数调用的是key自带的哈希函数,返回int型散列值。
如果直接拿散列值作为下标访问HashMap主数组的话,考虑到2进制32位带符号的int表值范围从 -2^32 到 2^32 - 1。总共要大概40亿的映射空间,但内存放不下一个长度为40亿的数组。HashMap的数组默认初始大小为16,所以这个散列值是不能直接拿来用,用之前还要先做对数组的长度取模运算,得到的余数才能用来访问数组下标。
当我们从HashMap取出一个结点时:
first = tab[(n - 1) & hash]
当数组长度n为2的整数幂,(n-1)与散列值进行操作的结果就是,散列值只保留低位值(高位全部归零)用来做数组下标访问。但是这样出现碰撞的几率会大大的增加,如果散列本身做得不好,使最后几个低位规律性重复,那么HashMap会非常糟糕。
所以要对散列值进行一些操作,扰动函数的价值就体现出来了。将高半区和低半区做异或,可以加大低位的随机性,而且低位掺杂了高位的部分特征,高位的信息也被变相保留下来。但是在一些非常极端情况下,hash 值还是会重复。
为了性能,仅仅异或一次就可以啦,当发生较大碰撞时用树形存储降低了冲突。
新增或更新结点
// 新增或更新一个键值对
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
// 将另一个map中所有键值对都添加进来
public void putAll(Map<? extends K, ? extends V> m) {
putMapEntries(m, true);
}
/*如果onlyIfAbsent为true,不改变已存在的值;
*如果evict为false,这个表在创建模式。*/
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);
// 否则判断key是否存在
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) {
// key原本不存在,建立新结点
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// 判断是否要转化为树型结构
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// key原本存在,更新value并返回旧值
if (e != null) {
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
// 修改次数+1,并判断是否需要扩容
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
根据key删除指定的结点
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
/*如果matchValue为true,删除value相等的结点即可;
*如果movable为false则在删除过程中不移动其他结点。*/
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
Node<K,V>[] tab; Node<K,V> p; int n, index;
// 首先根据hash值取出首结点
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
Node<K,V> node = null, e; K k; V v;
// 判断首节点
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
// 继续寻找目标结点
else if ((e = p.next) != null) {
// 树型结构
if (p instanceof TreeNode)
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
// 链式结构
else {
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
// 删除目标结点
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
// 在红黑树删除目标结点
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
// 链式结构
// 首节点为目标结点
else if (node == p)
tab[index] = node.next;
// 非首节点,p为目标结点的上一个结点
else
p.next = node.next;
// 操作数加1,结点总数-1
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
}
return null;
}
注意:下面的方法在上面的一些操作中是无作用的,是为LinkedHashMap预留的,LinkedHashMap会重写这些方法 。
void afterNodeAccess(Node<K,V> p) { }
void afterNodeInsertion(boolean evict) { }
void afterNodeRemoval(Node<K,V> p) { }
一些简单的方法
/*获取键值对的数量*/
public int size() {
return size;
}
/*是否为空*/
public boolean isEmpty() {
return size == 0;
}
/*清空所有的键值对*/
public void clear() {
Node<K,V>[] tab;
modCount++;
if ((tab = table) != null && size > 0) {
size = 0;
for (int i = 0; i < tab.length; ++i)
tab[i] = null;
}
}
/* 是否包含某个键 */
public boolean containsKey(Object key) {
return getNode(hash(key), key) != null;
}
/* 是否包含某个值 */
public boolean containsValue(Object value) {
Node<K,V>[] tab; V v;
if ((tab = table) != null && size > 0) {
// 依次遍历每个桶中的所有的结点
for (int i = 0; i < tab.length; ++i) {
for (Node<K,V> e = tab[i]; e != null; e = e.next) {
if ((v = e.value) == value ||
(value != null && value.equals(v)))
return true;
}
}
}
return false;
}
写在最后
花了两天看了一些关于HashMap的源码,也算是重新认识了HashMap,其实阅读源码真的是很有用的。看到了自己与写类库那些大神们的差距,不过也学到了一些很棒的东西,比如Hash表如何更好的处理碰撞等。
源码不止这一点点,还有很多没学习的,后面的还有很多东西,比如键值对、键、值各自的Set集合,迭代器,红黑树相关的操作等。打算下面先把数据结构树方面的知识好好补一下,再去学习后面的源码。
参考
JDK 1.8.0_181 HashMap.java
HashMap源码注解 之 常量定义(一)
HashMap的hash()
HashMap的modCount