首页 > 编程知识 正文

HashMap的底层实现原理,hashmap存储原理

时间:2023-05-03 14:38:37 阅读:29854 作者:1022

ngdej表ngdej表是数组的扩展,利用了数组支持下标直接检索的特性。 没有数组就没有ngdej表。

数组是一系列连续的内存空间。

使用数组时,可以按下标直接得到存储器地址的值,从而直接得到想要的数据,所以数组在使用下标访问时算法的复杂度位o(1)。

基于这种高效的搜索方法,ngdej表应运而生:

在ngdej表中存储数据时,用ngdej函数将key值映射为未数组下标,在对应下标的数组空间中存储value。 这样访问后,hash(key可以取得对应的下标,并从对应的排列下标的位置取得数据。 实现o(1)的算法复杂性。

ngdej表的弊端:

ngdej表中有重要的算法。 是ngdej函数。 ngdej函数最重要的功能是1 .通过相同的key得到的ngdej值相同。 ngdej值因key而异。

但是,现有算法存在ngdej的冲突,不同的key也可能得到相同的ngdej值。

业界有名的MD5、SHA、CRC等ngdej算法也无法完全避免此散列冲突。 另外,由于数组的存储区域有限,散列冲突的概率也变高。 因此,很少能找到没有完美冲突的散列函数,即使能找到,时间成本、计算成本也很大,所以对于散列冲突问题需要用其他的方法来解决。 由于ngdej表与HashMap存在冲突,因此为了解决冲突,出现了新的数据结构、数组链表

每个数组的下标对应于一个时段,冲突的密钥落入同一时段。 每个bucket的数据使用链表存储,链表节点存储条目。 每个条目都包含一对键值(key,value )和下一个节点的地址(next )。 对于查询,计算与key对应的数组下标以找到对应的时段,遍历与时段对应的链表,比较key值,最后检索数据。

以上分析了HashMap的主要结构模型。 现在,结合源代码,我们来看看HashMap是如何初始化、扩展以及put和get的。

源代码分析在进行源代码分析之前,我们先看看HashMap中的一些重要变量。

size :表示存储在hashmap中的密钥-值对(KV )的数量。 capacity :表示hashmap中的桶数,即数组的长度。 默认值为16,随着数据量的增加而扩展。 加载因子:负载因子。 加载因子用于测量HashMap的充满度,默认值为0.75f。 计算方法是将size除以capacity。 阈值:阈值。 如果HashMap的size大于threshold,则执行“扩展”操作。 看看容量*加载因子初始化和扩展put的源代码。

finalvputval(inthash,K key,V value,boolean onlyIfAbsent,boolean evict ) { NodeK,V[] tab; NodeK,V p; int n,I; if () tab=table )==null|| ) n=tab.Length )==0) /初始化映射容量n=(tab=resize () ).length; 继续查看resize ()方法:

final NodeK,V[] resize (),V[] oldTab=table; intoldcap=(oldtab==null )? 0 : oldTab.length; int oldThr=threshold; int newCap,newThr=0; 判断if(oldcap0)//容量是否在最大容量(1 30 )//以上时无法扩展,将旧的table if (old cap=maximum _ capacity )//阈值设为最大int值threshold=iid () /可扩展,) oldCap 1)即乘以2,elseif ) ) newcap=oldcap1) maximum _ capacityoldcap=default _ initial _ capacity

newCap = oldThr; else { // zero initial threshold signifies using defaults newCap = DEFAULT_INITIAL_CAPACITY; //默认负载因子*默认容量 newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } if (newThr == 0) {//新的capacity*loadFactor 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 { // preserve order //使用两个链表,loHead和loTail链表放入下标和旧数组相同的下标桶中 //loHead和loTail链表放入下标为 旧数组所在下标+旧容量 的下标桶中 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; }

看完resize()方法后我们再结合下图就能很顺畅的了解HashMap的扩容机制了:

在JDK1.8中,当链表长度大于8的时候,链表会转化为红黑树以提升查询效率。

put方法

我们来看下源码:

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; //判断桶中是否有数据,没有则直接创建一个node放入桶中 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); //桶中有数据,则追加链表或者红黑树 else { //e:已经存在的 Node<K,V> e; K k; //判断当前node是否匹配传入的key,匹配则替换。不是则往下查询 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) { //将e指向下一个节点,下个节点为null,追加链表 if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); //判断链表长度是否达到8,达到则转换为红黑树存储 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } //判断当前节点是否为put的节点,是则结束遍历。 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; //判断size是否大于阈值,大于触发扩容操作 if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }

看完源码,我们再来总结下流程:

总结

HashMap主要的数据结构、初始化和扩容及put流程都分析完了,其中关于红黑树的部分没有做详细分析,因为红黑树本身旧比较复杂,后面会将红黑树单独来一波分析。

版权声明:该文观点仅代表作者本人。处理文章:请发送邮件至 三1五14八八95#扣扣.com 举报,一经查实,本站将立刻删除。