文章目录
- HashMap的数据结构(图解+源码分析)
- 数组
- 单链表
- HashMap如何插入数据(图解+源码分析pos)
- 为什么初始化容量是2的倍数(源码分析)
- HashMap如何解决Key冲突(图解+源码分析)
- HashMap如何扩容(源码分析)
- HashMap查询与删除
- HashMap序列化(源码分析)
- 总结
HashMap的数据结构(图解+源码分析)
HashMap快速索引
数组
定义:连续的内存,具有共同特性的数据
现有一个8个元素的空数组,如下图
向数组尾部添加数据:
向数组中插入节点:
如果项目把475插入到下标为1的位置,就要把275的下标位置向后移一位
优点:连续内存,通过数组下标可以快速寻址
缺点:中间插入节点比较慢
单链表
定义:任意内存单元通过指针连接每个结点,存放线性表中的数据
现有如下三个结点的单链表:
向单链表尾部添加数据:把next指针指向下一个节点的Data域
向点链表中插入节点:
假设在“275”和“297”之间插入新的结点,那么只需把“297”的next指向新结点的data域,然后新结点的next指向“297”的data域。无需同数组那样调整后面结点的位置。
优点:插入和删除数据方便
缺点:单链表查询某个结点,需要从头结点开始遍历整个链表,所以单链表的查询效率比较低
HashMap如何插入数据(图解+源码分析pos)
建立插入值和数组所在下标的位置(pos = key % size key: 插入数据的内容;size:数组的大小)
比如要将100,201,303这三个数据插入到一个长度为10的以链表特性声明的数组中
pos1 = 100 % 10 = 0
pos2 = 201 % 10 = 1
pos3 = 303 % 10 = 3
HashMap的构造函数:
static final float DEFAULT_LOAD_FACTOR = 0.75f;
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
HashMap插入函数(put):
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;
}
其中table是 “transient Node<K,V>[]”单链表结构的数组
其中resize()
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
static final float DEFAULT_LOAD_FACTOR = 0.75f;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;//16
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//12
}
threshold = newThr;//12
为什么初始化容量是2的倍数(源码分析)
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
-
计算机申请内存是2的倍数,可以避免内存碎片
-
移位操作效率高
-
参与到hash运算中提高散列度
putVal()中:
(p = tab[i = (n - 1) & hash])
举例:
n = 16 (10000)
i=(n-1) : 0000 1111
hash1 : 0000 0001 -> 0000 0001
hash2 : 0001 0000 -> 0000 0000
若:n = 15 (01111)
i=(n-1) : 0000 1110
hash1 : 0000 0001 -> 0000 0000
hash2 : 0001 0000 -> 0000 0000
所以HashMap初始化数组使用2的倍数(因为2的倍数减1,后面的全1)可以提高散列度和降低冲突
HashMap如何解决Key冲突(图解+源码分析)
上一部分,咱们插入的数据是没有冲突的,如果我们插入200,那么pos取模后也是0,和100这个结点冲突了,HashMap针对这种冲突问题,通过单链表解决。
查询到0的时候,就可以定位到100和200了,
但是问题来了,如果以这种单链表的形式解决冲突问题,那么查询起来会很慢!
为了解决上面的这个问题JDK1.8中引入了“红黑树”(高度平衡的平衡二叉树)这一概念,因为二叉树的查询效率是树的深度,显然查询效率比单链表高。
HashMap如何扩容(源码分析)
HashMap查询与删除
HashMap序列化(源码分析)
总结
-
HashMap的数据结构包括数组、单链表、红黑树;
-
数组容量为2的倍数:
a.提高运算速度,
b.增加散列度,降低冲突,
c.减少内存碎片 。
-
通过hash函数和pos = key % size 确定数据插入位置,hashcode的高16位和低16位进行异或取模,增加散列度,降低冲突;
-
当两个及两个以上的key相同且data不同的时候(插入冲突),通过单链表解决冲突,如果链表的长度超过(TREEIFY_THRESHOLD = 8), 将单链表转化成红黑树以提高查询效率;
-
扩容条件:实际节点数大于等于容量的3/4;扩容后的数据排布:1.原下标的位置;2.原下标+原容量的位置
-
序列化:只存储了数组的容量、实际结点数量和各个结点的key,value值.
还有 HashMap如何扩容(源码分析) 、 HashMap查询与删除 、HashMap序列化(源码分析)这三部分在整理中~觉得看完有帮助的小伙伴,可以关注小名或者收藏此文等待后期更新哈