HashMap源码分析
Hash算法
Hash也被称为散列、哈希。他们的基本原理都是**把任意长度的输入,通过Hash算法变成固定长度的输出。**这个映射的规则就是对应的Hash算法,而原始数据映射之后的二进制串就是哈希值。
HashMap
继承体系
-
HashMap 是一个散列表,它存储的内容是键值对(key-value)映射。 -
HashMap 实现了 Map 接口,根据键的 HashCode 值存储数据,具有很快的访问速度,最多允许一条记录的键为 null,不支持线程同步。 -
HashMap 是无序的,即不会记录插入的顺序。 -
HashMap 继承于AbstractMap,实现了 Map、Cloneable、java.io.Serializable 接口。 -
HashMap 的 key 与 value 类型可以相同也可以不同,可以是字符串(String)类型的 key 和 value,也可以是整型(Integer)的 key 和字符串(String)类型的 value。
存储结构
HashMap采用了数组 + 链表 + 红黑树的结构进行存储元素,并且数组的一个元素被称为桶。
在添加元素时,会先计算哈希值,然后根据哈希值算出元素要存放的位置。在存放元素前,会先判断要存放的位置上是否有元素,如果没有元素,就直接将元素放置到此处;如果该位置上已经有元素了,就该用链表的方式存储,将元素放入链表的尾部。
当链表达到一定长度时(即长度为8时),并且整个数组的达到达到一定长度时(即大小为64时),就会将链表改为红黑树的方式进行存储,从而提高效率。
扩容机制:
- HashMap桶(用于存放元素的数组)的初始大小默认为 16 (2的幂)
- 如果桶位数(数组)小于64,每次扩容都是原来的2倍(容量大小都是2的幂),这是扩容后会重新计算桶内元素的哈希值并重新计算存放位置,这样就可以使得使用链表存放的方式减少或使链表变短,提升桶空间利用率的同时提升效率。
- 当桶中某位的链表长度大于8并且桶位数大于64时,会对大于8的链表进行树化操作,即:将链表转换为红黑树
- 当红黑树的结点小于6时,红黑树会重新转换为链表
源码分析
HashMap的基本属性与常量
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
static final int MAXIMUM_CAPACITY = 1 << 30;
static final float DEFAULT_LOAD_FACTOR = 0.75f;
static final int TREEIFY_THRESHOLD = 8;
static final int UNTREEIFY_THRESHOLD = 6;
static final int MIN_TREEIFY_CAPACITY = 64;
transient Node<K,V>[] table;
transient Set<Map.Entry<K,V>> entrySet;
transient int size;
transient int modCount;
int threshold;
final float loadFactor;
总结:
(1)容量
容量为数组的长度,亦即桶的个数,默认为16,最大为2的30次方,当容量达到64时才可以树化。
(2)装载因子
装载因子用来计算容量达到多少时才进行扩容,默认装载因子为0.75。
(3)树化
树化,当容量达到64且链表的长度达到8时进行树化,当链表的长度小于6时反树化。
Node内部类
Node的实例表示一个单链表节点,用于存储key和value。
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;
}
}
HashMap()构造方法
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
}
HashMap(int initialCapacity)构造方法
调用HashMap(int initialCapacity, float loadFactor) 构造方法,传入默认装载因子。
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
HashMap(int initialCapacity, float loadFactor)构造方法
判断传入的初始化容量是否和法同时计算扩容的门槛,扩容门槛为传入的初始容量往上取最近的2的n次方。
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;
}
put(K key, V value)方法
向表中添加元素的方法
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)
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) {
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
总结:
- 实际实现功能的是
putVal 方法 - 计算插入元素的位置
- 计算出的位置上已经有元素了
- 比较key是否相同
- 判断要插入位置上的节点是否已经是一个红黑树
- 要插入元素的位置上是一个链表结构,需要依次比较链表中的元素
- 如果走到了链表的尽头,先创建新的节点准备插入
- 如果此时原来的链表长度已经到达8了,就将链表树化后,在插入新元素
- 如果链表长度没有大于8,就插入新元素
- 在链表中出现了相同的元素,就记录元素,后续进行替换操作
- 判断是否需要替换旧元素
- 如果是,就将旧的value值替换掉,并将旧的value值返回
- 对哈希表的修改次数(
modCount )加一,但是不包括key相同时值替换的操作 - 将哈希表元素数量(
size )加一,并判断是否超过threshold 阈值
- 如果没有出现过替换旧值得操作,返回null
resize()方法
HashMap的扩容方法。
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) {
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)
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<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;
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;
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;
}
总结:
- 一开始使用的是默认构造方法时,在插入第一个元素时进行库容操作,初始化为默认值,容量为16,扩容阈值为12
- 如果使用 的是非默认构造方法,则第一次插入元素时初始化容量等于扩容门槛,扩容门槛在构造方法里等于传入容量向上最近的2的n次方。
- 如果旧容量大于0,则新容量等于旧容量的2倍,但不超过最大容量2的30次方,新扩容门槛为旧扩容门槛的2倍。
- 扩容操作
- 计算出新的容量和新的阈值。
- 根据新容量创建出新的哈希表。
- 将旧哈希表中的元素搬迁到新的哈希表中(根据元素种类分类处理)。
- 单个元素:计算出新的位置
e.hash & (newCap - 1) ,存放到新的哈希表中。 - 红黑树类型:调用
split 方法将树分成两棵树存放到哈希表中。 - 链表类型:判断
e.hash & oldCap 的结果值将链表划分成高位链表和低位链表,低位链表还是放到原来的位置上,高位链表的存放位置变为原来的位置加上哈希表原长。 - 返回新的哈希表
get(Object key)方法
根据传入的key,获取value
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;
}
总结:
get 方法实际是调用了getNode 方法实现的功能- 计算key的哈希值
- 根据哈希值找到所在哈希表中的位置
- 如果对应位置上只有一个元素就判断这个元素是不是我们要找的值
- 如果不是判断是否为链表或者红黑树结构,如果是就在链表或者红黑树上查找元素
- 如果未找到,返回
null
remove(Object key)
根据指定的key删除元素。
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
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;
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;
else
p.next = node.next;
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
}
return null;
}
总结:
- 先计算hash值
- 根据hash值找到对应哈希表中的位置
- 判断指定位置上是否正好就是要删除的元素,是就后续进行删除操作,如果不是,就判断是不是链表或者红黑树,如果是就对其进行响应的删除操作
- 修改size值,modCount值以及后续处理
总结
- HashMap底层是哈希表,使用(数组 + 链表 + 红黑树)
- ashMap查找添加元素的时间复杂度都为
O
(
1
)
O(1)
O(1)
- 默认容量是16(1 << 4),如果一开始没有指定容量,就在第一次调用put方法时初始化容量
- 每次扩容为原来容量的二倍
- 存在一个扩容阈值,这个值是根据负载因子(0.75f)和容量大小动态计算的
- 当哈希表大小不超过64时不会进行树化操作
- 当一个位置上链表长度大于8并且哈希表大小大于64时会对链表进行树化操作
- 当哈希表中的某一位置上元素个数小于6时,进行反树化操作
- HashMap不是线程安全的,而HashTable是线程安全的
面试题: 为什么HashMap为什么每次扩容的倍数是2 ?
正常来讲取模使用 % ,但是Java对其进行了优化,就是使用 位运算(&)进行优化,而想要这样优化就需要把HashMap内部的数组长度固定为2的幂的长度,所以扩容都是原来的二倍。
HashMap中的tableSizeFor 方法根据传入的容量大小后经过位运算处理过的元素都是2的幂,这样处理好处是可以使添加的元素更均匀的分布在HashMap底层的数组中,减少hash碰撞,避免形成链表的结构,使得查询效率降低。
tableSizeFor方法的实现:
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;
}
参考文章
彤哥读源码: 死磕 java集合之HashMap源码分析
|