HashMap底层原理和源码解析(1分钟全面掌握)

2023-01-31 0 1,032

HashMap下层基本上原理是在复试中问的最多的,假如只是为了复试,第三拍子就满足用户了。上面是写手重新整理的HashMap从下层基本上原理、源代码导出等数个层次来全面性掌控并介绍HashMap,为复试添砖加瓦。

产品目录如是说

HashMap基本上叙述HashMap源代码常见特性put 源代码形式如是说HashMap提速形式源代码二叉树转瑙脂树JDK1.7 多处理器提速马蹄形二叉树

一、HashMap基本上叙述

HashMap是如前所述基元表的MapUSB的非并行同时实现。此同时实现提供所有较旧的态射操作形式,并容许使用null值和null键。因此不确保态射的次序。

1.1HashMap的计算机程序?

基元表内部结构(二叉树杂凑:字符串+二叉树)同时实现,紧密结合字符串和二叉树的缺点。当二叉树长度超过8时,二叉树切换为瑙脂树

1.2HashMap的工作基本上原理?

HashMap(下层选用字符串+二叉树),选用Entry字符串来存储key-value对,每两个数组对共同组成了两个Entry虚拟,Entry类事实上是两个双向的二叉树内部结构,它具有Next操作形式符,可以相连下两个Entry虚拟,依序来化解Hash武装冲突的问题,因为HashMap是依照Key的hash位来排序Entry在HashMap中存储的边线的,假如hash值完全相同,而key文本不成正比,那么就用二叉树来化解这种hash武装冲突。

1.3HashMap的put同时实现操作形式过程?

排序得字符串负号,用作找出bucket边线,来存储Entry第三类。

假如hash值在HashMap中不存有,则继续执行填入,若存有,则发生对撞,则填入二叉树的前部(尾插法)或是瑙脂树中(树的加进形式)。

假如hash值在HashMap中存有,且它二者equals回到true,则预览数组对。

假如HashMap子集中的数组对小于12,初始化resize形式进行字符串提速。

1.4HashMap的get同时实现操作形式过程?

从HashMap中get原素时,具体来说排序key的hashCode,找出字符串中对应边线的某一原素,然后通过key的equals形式在对应边线的二叉树中找出需要的原素。

下层的计算机程序:HashMap的下层主要是基于字符串和二叉树来同时实现的,它之所以有相当快的查询速度主要是因为它是通过排序杂凑码来决定存储的边线。

1.5 JDK8中什么时候会转为瑙脂树?

假如二叉树的宽度超过了8,那么二叉树将切换为瑙脂树。(桶的数量必须小于64,小于64的时候只会提速)。

1.6 什么是Hash武装冲突(对撞)?

Hash 简介

一般翻译做杂凑、杂凑,或音译为基元,是把任意宽度的输入(又叫做预态射pre-image)通过杂凑算法变换成固定宽度的输出,该输出就是杂凑值(hash)。

什么是Hash武装冲突?

答:两个不同的数据经过hash得到完全相同的hash值,就称为hash武装冲突(对撞)。

为什么会产生Hash武装冲突(对撞)?

答:是把任意宽度的输入通过杂凑算法变换成固定宽度的输出。(抽屉基本上原理)。

抽屉基本上原理:桌上有十个苹果,要把这十个苹果放到九个抽屉里,无论怎样放,我们会发现至少会有两个抽屉里面放不少于两个苹果。这一现象就是我们所说的“抽屉基本上原理”。

当读到这里时,上面的知识点是完全可以帮助完成复试。假如有精力,上面我们从源代码来看一下

二、HashMap源代码常见特性

//基元表字符串的默认宽度 16 static final intDEFAULT_INITIAL_CAPACITY =1 << 4; // aka 16 //基元表字符串的最大宽度,2的30次方的原因是,int 最大值是 2的31次方减 1,所以只能是 30 次方 static final intMAXIMUM_CAPACITY =1 << 30; //默认的加载因子,加载因子指的是 hashmap 中数据个数超过字符串宽度*当前加载因子的时候会触发提速 static final floatDEFAULT_LOAD_FACTOR =0.75f; //当单个边线的数据二叉树宽度超过 8 的时候会将该二叉树切换为瑙脂树 static final int TREEIFY_THRESHOLD = 8; //当当前边线的瑙脂树文本宽度小于 6 的时候会重新变回二叉树 static final int UNTREEIFY_THRESHOLD = 6; //当某个边线的二叉树在切换为瑙脂树的时候,假如此时字符串宽度小于 64 会先进行提速 static final intMIN_TREEIFY_CAPACITY =64;

三、put 源代码形式如是说

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { //声明了两个局部变量 tab,局部变量 Node 类型的数据 p,int 类型 n,i Node<K,V>[] tab; Node<K,V> p; intn, i;//具体来说将当前 hashmap 中的 table(基元表)赋值给当前的局部变量 tab,然后判断tab 是不是空或是宽度是不是 0,事实上就是判断当前 hashmap 中的基元表是不是空或是宽度等于 0 if ((tab = table) == null || (n = tab.length) == 0) //假如是空的或是宽度等于0,代表现在还没基元表,所以需要创建新的基元表,默认就是创建了两个宽度为 16 的基元表 n = (tab = resize()).length; //将当前基元表中与要填入的数据边线对应的数据取出来,(n – 1) & hash])就是找当前要填入的数据应该在基元表中的边线,假如没找出,代表基元表中当前的边线是空的,否则就代表找出数据了, 并赋值给变量 p if ((p = tab[i = (n – 1) & hash]) == null) tab[i] = newNode(hash, key,value, null);//创建两个新的数据,这个数据没有下一条,并将数据放到当前这个边线 else {//代表要填入的数据所在的边线是有文本的 //声明了两个节点 e, 两个 key k Node<K,V> e; K k; if (p.hash == hash && //假如当前边线上的那个数据的 hash 和我们要填入的 hash 是一样,代表没有放错边线 //假如当前这个数据的 key 和我们要放的 key 是一样的,实际操作形式应该是就替换值 ((k = p.key) == key || (key != null && key.equals(k)))) //将当前的节点赋值给局部变量 e e = p; else if (p instanceof TreeNode)//假如当前节点的 key 和要填入的 key 不一样,然后要判断当前节点是不是两个瑙脂色类型的节点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);//超出了之后就将当前二叉树切换为树,注意切换树的时候,假如当前字符串的宽度小于MIN_TREEIFY_CAPACITY(默认 64),会触发提速,我个人感觉可能是因为觉得两个节点上面的数据都超过8 了,说明 hash寻址重复的厉害(比如字符串宽度为 16 ,hash 值刚好是 0或是 16 的倍数,导致都去同两个边线),需要重新提速重新 hash break; } //假如当前遍历到的数据和要填入的数据的 key 是一样,和上面之前的一样,赋值给变量 e,上面替换文本 if(e.hash == hash && ((k = e.key) == key || (key !=null && key.equals(k)))) break; p = e; } } if (e != null) { //假如当前的节点不等于空, V oldValue = e.value;//将当前节点的值赋值给 oldvalue if (!onlyIfAbsent || oldValue == null) e.value = value; //将当前要填入的 value 替换当前的节点里面值 afterNodeAccess(e); return oldValue; } } ++modCount;//增加宽度 if(++size > threshold) resize();//假如当前的 hash表的宽度已经超过了当前 hash 需要提速的宽度, 重新提速,条件是 haspmap 中存放的数据超过了临界值(经过测试),而不是字符串中被使用的负号 afterNodeInsertion(evict); return null; }

四、HashMap提速形式源代码

//haspmap 触发提速的条件有两个,两个是当存放的数据超过临界值的时候会触发提速,另外两个是当需要转成瑙脂树的时候,假如当前字符串的宽度小于 64,会触发提速 final Node<K,V>[] resize() { //声明了两个 oldtab ,并且把当前(提速前) hashmap里面的基元表赋值过来,假如是第三次放数据,此时这两个其实都是空 Node<K,V>[] oldTab = table; 话,就是 0,否则就是提速之前的基元表的宽度 int oldCap = (oldTab == null) ? 0 : oldTab.length; //当前(提速前)基元表需要提速时候的宽度,其实这值就是基元表的宽度*加载因子的宽度,假如是第三次放数据,就是 0 int oldThr = threshold; //新的宽度和新的提速宽度 int newCap, newThr = 0; if (oldCap > 0) { //假如是第三次的时候,这个宽度是 0,所以不符合当前判断,假如小于 0 代表是原先的老基元表宽度已经超出限制了 if (oldCap >= MAXIMUM_CAPACITY) { //看看最新的宽度是不是小于等于hashmap 对字符串宽度的最大限制 threshold = Integer.MAX_VALUE;//设置为默认的最大宽度 return oldTab; } else if((newCap = oldCap <<1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; //假如没有超出宽度限制,新的字符串宽度等于老的字符串宽度*2(向左移动 1 位) } else if (oldThr > 0) //假如当前的提速宽度小于 0,代表已经有基元表 newCap = oldThr; else { //代表还没有基元表,事实上就是第三次向 map 中放数据 newCap = DEFAULT_INITIAL_CAPACITY;//新的基元表宽度为当前map 的默认值 newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//新的提速宽度为默认宽度*默认的加载因子,这里算它的原因是为了不在后面放数据的时候每次都重新排序,因为每次都要算是不是应该提速,假如不找变量接收,每次都要做数学运算 } if (newThr == 0) {//假如新的宽度还是 0,则继续排序 float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } threshold = newThr;//当前 hashma的提速宽度等于最新排序出来的提速宽度 @SuppressWarnings({“rawtypes”,“unchecked”}) Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];//根据最新的宽度创建对应宽度的基元表,假如是首次创建,默认就是 16table = newTab;//将当前 hashmap 中的基元表赋值为最新刚刚创建的基元表 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 { // 创建两个二叉树,主要是因为基本上上提速的时候,部分数据会在原始边线,另外一部分数据会去向后便宜老字符串的宽度,比如原先是字符串宽度是 16,原先在 1 边线上面的数据,提速到 32 后要么就还在 1,要么就应该去17,也就是向后移动原始宽度(或是是提速增加的宽度) Node<K,V> loHead = null, loTail = null; Node<K,V> hiHead =null, hiTail = null; Node<K,V> next; do { next = e.next; //具体来说将当前的下两个数据赋值给 e if((e.hash & oldCap) ==0) {//符合应该在原始边线条件的创建一条二叉树 if (loTail == null)//假如没有数据 loHead = e;//当前节点就是两个头 elseloTail.next = e;//否则当前的尾节点下一条数据就是 e loTail = e;//e 就成为了尾结点 } else {//代表不符合原始边线的条件,就创建另外两个二叉树,来存放另外一部分数据 if (hiTail == null)//假如没有数据 hiHead = e;//当前节点就是两个头 else hiTail.next = e;//否则当前的尾节点下一条数据就是 e hiTail = e;//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; //回到最新创建的那个基元表 }

五、二叉树转瑙脂树

/** * Replaces all linked nodes in bin at index for given hash unless * table is too small, in which case resizes instead. */ final void treeifyBin(Node<K,V>[] tab, int hash) { int n, index; Node<K,V> e; //假如当前基元表是空的或是是基元表的字符串宽度小于 64,则触发提速,这也是 hashmap 提速的第二个条件和形式,这里的目的是先通过提速来重新分配数据,让数据均匀一些 if (tab == null|| (n = tab.length) < MIN_TREEIFY_CAPACITY) resize();else if ((e = tab[index = (n – 1) & hash]) != null) {//不能提速的情况下才会进行转树操作形式TreeNode<K,V> hd =null, tl = null; //转成树节点 do { TreeNode<K,V> p = replacementTreeNode(e, null); if (tl == null) hd = p; else { p.prev = tl; tl.next = p; } tl = p; } while ((e = e.next) != null); if ((tab[index] = hd) != null) hd.treeify(tab); } }

六、JDK1.7 多处理器提速马蹄形二叉树

resize 提速形式

void resize(int newCapacity) { Entry[] oldTable = table; int oldCapacity = oldTable.length; …… //创建两个新的Hash Table Entry[] newTable = new Entry[newCapacity]; //将Old Hash Table上的数据迁移到New Hash Table上transfer(newTable); table = newTable; threshold = (int)(newCapacity * loadFactor); }

好,重点在这里面的transfer()!

void transfer(Entry[] newTable) { Entry[] src = table; int newCapacity = newTable.length; //上面这段代码的意思是: // 从OldTable里摘两个原素出来,然后放到NewTable中 for (int j = 0; j < src.length; j++) { Entry<K,V> e = src[j];if (e != null) { src[j] = null; do { Entry<K,V> next = e.next; //假如线程1在这里卡住 inti = indexFor(e.hash, newCapacity); e.next = newTable[i]; newTable[i] = e; e = next; }while (e != null); } } }

假设我们称当前要 重新放的数据为 a, 当前a 的下两个为 b, 目标边线上面的数据为 c(c 可能会null,但是无所谓)

线程 1

Entry<K,V> next = e.next; //假如线程1在这里卡住

这个时候线程 1 中的 next 是 b, e就是 a 第三类

线程 2

Entry<K,V> next = e.next; //这里的 next 也是 b, e 就是 a 第三类

int i = indexFor(e.hash, newCapacity);//排序a 应该出现的边线

e.next = newTable[i];//将a 第三类的 next 特性指定为它应该存放的新边线的表头第三类 c

newTable[i] = e; //将当前表头第三类设置为 a

当上述代码继续执行完成后现在字符串的最新边线的数据为 a—>c 这样的次序

线程 1

此时切换会线程 1

Entry<K,V> next = e.next; //因为此时线程 1 中的这个局部变量 e 仍旧指向的是第三类 a, 所以此时的 next 就是 c(但是无所谓)

int i = indexFor(e.hash, newCapacity);//排序a 应该出现的边线,和线程 2 排序的边线是一样的

e.next = newTable[i];//将a 第三类的 next 特性指定为它应该存放的新边线的表头第三类 ,但是因为在线程2 中表头第三类已经被替换为a 了,所以此时事实上 a.next 第三类仍旧是a 自己,因为事实上指向的是同两个第三类,所以线程 2 中给 a 设置的 next 为 c 就会被覆盖为 a

newTable[i] = e; //将当前表头第三类设置为 a,这个 a 事实上和之前线程 2 的 a 是同两个第三类

当上述代码继续执行完成后,我们发现当前这个边线的表头数据是 a ,然后 a.next 还是自己 a,然后接着它的 next 还是自己 a,最终陷入无限循环的二叉树中

相关文章

发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务