本文主要是介绍面试官!你又双叒叕问HashMap!,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
文章目录
- 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的构造函数:
/**
* The load factor used when none specified in constructor.
*/
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 1sttreeifyBin(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 keyV 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()
/**
* The default initial capacity - MUST be a power of two.
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
/**
* The load factor used when none specified in constructor.
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;else { // zero initial threshold signifies using defaultsnewCap = DEFAULT_INITIAL_CAPACITY;//16newThr = (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序列化(源码分析)这三部分在整理中~觉得看完有帮助的小伙伴,可以关注小名或者收藏此文等待后期更新哈😁
这篇关于面试官!你又双叒叕问HashMap!的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!