HashMap原理是JAVA和Android面试中常常会遇到的问题,这篇文章将经过HashMap在JDK1.7和1.8 中的源码来解析HashMap的原理。html
采用一段连续的存储单元来存储数据。对于指定下标的查找,时间复杂度为O(1);经过给定值进行查找,须要遍历数组,逐一比对给定关键字和数组元素,时间复杂度为O(n),固然,对于有序数组,则可采用二分查找,插值查找,斐波那契查找等方式,可将查找复杂度提升为O(logn);对于通常的插入删除操做,涉及到数组元素的移动,其平均复杂度也为O(n)面试
对于链表的新增,删除等操做(在找到指定操做位置后),仅需处理结点间的引用便可,时间复杂度为O(1),而查找操做须要遍历链表逐一进行比对,复杂度为O(n)数组
红黑树(Red Black Tree) 是一种自平衡二叉查找树,是在计算机科学中用到的一种数据结构,典型的用途是实现关联数组。
红黑树是每一个节点都带有颜色属性的二叉查找树,颜色或红色或黑色。在二叉查找树强制通常要求之外,对于任何有效的红黑树咱们增长了以下的额外要求:
性质1. 节点是红色或黑色。
性质2. 根节点是黑色。
性质3. 每一个叶节点(NIL节点,空节点)是黑色的。
性质4. 每一个红色节点的两个子节点都是黑色。(从每一个叶子到根的全部路径上不能有两个连续的红色节点)
性质5. 从任一节点到其每一个叶子的全部路径都包含相同数目的黑色节点。数据结构
散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它经过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫作散列函数,存放记录的数组叫作散列表。
给定表M,存在函数f(key),对任意给定的关键字值key,代入函数后若能获得包含该关键字的记录在表中的地址,则称表M为哈希(Hash)表,函数f(key)为哈希(Hash) 函数。app
若是两个不一样的元素,经过哈希函数得出的实际存储地址相同怎么办?也就是说,当咱们对某个元素进行哈希运算,获得一个存储地址,而后要进行插入的时候,发现已经被其余元素占用了,其实这就是所谓的哈希冲突,也叫哈希碰撞。前面咱们提到过,哈希函数的设计相当重要,好的哈希函数会尽量地保证 计算简单和散列地址分布均匀,可是,咱们须要清楚的是,数组是一块连续的固定长度的内存空间,再好的哈希函数也不能保证获得的存储地址绝对不发生冲突。那么哈希冲突如何解决呢?哈希冲突的解决方案有多种:开放定址法(发生冲突,继续寻找下一块未被占用的存储地址),再散列函数法,链地址法,而HashMap便是采用了链地址法,也就是数组+链表的方式。异步
首先咱们看下源码中的注释:ide
//一、哈希表基于map接口的实现,这个实现提供了map全部的操做,而且提供了key和value能够为null,(HashMap和HashTable大体上市同样的除了hashmap是异步的和容许key和value为null), 这个类不肯定map中元素的位置,特别要提的是,这个类也不肯定元素的位置随着时间会不会保持不变。 Hash table based implementation of the Map interface. This implementation provides all of the optional map operations, and permits null values and the null key. (The HashMap class is roughly equivalent to Hashtable, except that it is unsynchronized and permits nulls.) This class makes no guarantees as to the order of the map; in particular, it does not guarantee that the order will remain constant over time. //假设哈希函数将元素合适的分到了每一个桶(其实就是指的数组中位置上的链表)中,则这个实现为基本的操做(get、put)提供了稳定的性能,迭代这个集合视图须要的时间跟hashMap实例(key-value映射的数量)的容量(在桶中) 成正比,所以,若是迭代的性能很重要的话,就不要将初始容量设置的过高或者loadfactor设置的过低,【这里的桶,至关于在数组中每一个位置上放一个桶装元素】 This implementation provides constant-time performance for the basic operations (get and put), assuming the hash function disperses the elements properly among the buckets. Iteration over collection views requires time proportional to the "capacity" of the HashMap instance (the number of buckets) plus its size (the number of key-value mappings ). Thus, it's very important not to set the initial capacity too high (or the load factor too low) if iteration performance is important. //HashMap的实例有两个参数影响性能,初始化容量(initialCapacity)和loadFactor加载因子,在哈希表中这个容量是桶的数量【也就是数组的长度】,一个初始化容量仅仅是在哈希表被建立时容量,在 容量自动增加以前加载因子是衡量哈希表被容许达到的多少的。当entry的数量在哈希表中超过了加载因子乘以当前的容量,那么哈希表被修改(内部的数据结构会被从新创建)因此哈希表有大约两倍的桶的数量 An instance of HashMap has two parameters that affect its performance: initial capacity and load factor. The capacity is the number of buckets in the hash table, and the initial capacity is simply the capacity at the time the hash table is created. The load factor is a measure of how full the hash table is allowed to get before its capacity is automatically increased. When the number of entries in the hash table exceeds the product of the load factor and the current capacity, the hash table is rehashed (that is, internal data structures are rebuilt) so that the hash table has approximately twice the number of buckets. //一般来说,默认的加载因子(0.75)可以在时间和空间上提供一个好的平衡,更高的值会减小空间上的开支可是会增长查询花费的时间(体如今HashMap类中get、put方法上),当设置初始化容量时,应该考虑到map中会存放 entry的数量和加载因子,以便最少次数的进行rehash操做,若是初始容量大于最大条目数除以加载因子,则不会发生 rehash 操做。 As a general rule, the default load factor (.75) offers a good tradeoff between time and space costs. Higher values decrease the space overhead but increase the lookup cost (reflected in most of the operations of the HashMap class, including get and put). The expected number of entries in the map and its load factor should be taken into account when setting its initial capacity, so as to minimize the number of rehash operations. If the initial capacity is greater than the maximum number of entries divided by the load factor, no rehash operations will ever occur. //若是不少映射关系要存储在 HashMap 实例中,则相对于按需执行自动的 rehash 操做以增大表的容量来讲,使用足够大的初始容量建立它将使得映射关系能更有效地存储。 If many mappings are to be stored in a HashMap instance, creating it with a sufficiently large capacity will allow the mappings to be stored more efficiently than letting it perform automatic rehashing as needed to grow the table
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable { /****省略代码****/ }
咱们看到HashMap继承自AbstractMap
实现了Map
,Cloneable
,Serializable
接口。函数
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable { //序列号,序列化的时候使用。 private static final long serialVersionUID = 362498820763181265L; /**默认容量,1向左移位4个,00000001变成00010000,也就是2的4次方为16,使用移位是由于移位是计算机基础运算,效率比加减乘除快。**/ static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //最大容量,2的30次方。 static final int MAXIMUM_CAPACITY = 1 << 30; //加载因子,用于扩容使用。 static final float DEFAULT_LOAD_FACTOR = 0.75f; //当某个桶节点数量大于8时,会转换为红黑树。 static final int TREEIFY_THRESHOLD = 8; //当某个桶节点数量小于6时,会转换为链表,前提是它当前是红黑树结构。 static final int UNTREEIFY_THRESHOLD = 6; //当整个hashMap中元素数量大于64时,也会进行转为红黑树结构。 static final int MIN_TREEIFY_CAPACITY = 64; //存储元素的数组,transient关键字表示该属性不能被序列化 transient Node<K,V>[] table; //将数据转换成set的另外一种存储形式,这个变量主要用于迭代功能。 transient Set<Map.Entry<K,V>> entrySet; //元素数量 transient int size; //统计该map修改的次数 transient int modCount; //临界值,也就是元素数量达到临界值时,会进行扩容。 int threshold; //也是加载因子,只不过这个是变量。 final float loadFactor; /****省略代码****/ }
这里有一点就是默认为何容量大小为16,加载因子为0.75.咱们经过注释来看:
`
As a general rule, the default load factor (.75) offers a good tradeoff between time and space costs. Higher values decrease the space overhead but increase the lookup
cost (reflected in most of the operations of the HashMap class, including get and put). The expected number of entries in the map and its load factor should be taken
into account when setting its initial capacity, so as to minimize the number of rehash operations. If the initial capacity is greater than the maximum number of
entries divided by the load factor, no rehash operations will ever occur.
`
大体意思就是 16和0.75是通过大量计算得出的最优解,当设置默认的大小和加载因子时,进行的rehhash此书后最少,性能上最优。性能
咱们看到HashMap的构造方法有四个,
第一个:空参构造方法,使用默认的负载因子为0.75;
第二个:设置初始容量并使用默认加载因子;
第三个:设置容量和加载因子,第二个构造方法最终仍是调用了第三个构造方法;
第四个:将一个Map转换为HashMap。
下面咱们看下第四个构造方法的源码:学习
public HashMap(Map<? extends K, ? extends V> m) { this.loadFactor = DEFAULT_LOAD_FACTOR; putMapEntries(m, false); } final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) { //获取该map的实际长度 int s = m.size(); if (s > 0) { //判断table是否初始化,若是没有初始化 if (table == null) { // pre-size /**求出须要的容量,由于实际使用的长度=容量*0.75得来的,+1是由于小数相除,基本都不会是整数,容量大小不能为小数的,后面转换为int,多余的小数就要被丢掉,因此+1,例如,map实际长度22,22/0.75=29.3,所须要的容量确定为30,有人会问若是刚恰好除得整数呢,除得整数的话,容量大小多1也没什么影响**/ float ft = ((float)s / loadFactor) + 1.0F; //判断该容量大小是否超出上限。 int t = ((ft < (float)MAXIMUM_CAPACITY) ? (int)ft : MAXIMUM_CAPACITY); /**对临界值进行初始化,tableSizeFor(t)这个方法会返回大于t值的,且离其最近的2次幂,例如t为29,则返回的值是32**/ if (t > threshold) threshold = tableSizeFor(t); } //若是table已经初始化,则进行扩容操做,resize()就是扩容。 else if (s > threshold) resize(); //遍历,把map中的数据转到hashMap中。 for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) { K key = e.getKey(); V value = e.getValue(); putVal(hash(key), key, value, false, evict); } } }
这里咱们看到构造函数中传入了一个Map,而后把该Map转换为hashMap,这里面还调用了resize()
进行扩容,下面咱们会详细介绍。在上面的entrySet方法会返回一个Set<Map.Entry<K,V>>,泛型为Map的内部类Entry,它是一个存放key-value的实例,为何要用这种结构就是上面咱们说的hash表的遍历,插入效率高。构造函数基本已经讲完了,下面咱们重点看下HashMap
是如何将key和value存储的。下面咱们看HashMap的put(K key,V value)
方法.
public V put(K key, V value) { /**四个参数,第一个hash值,第四个参数表示若是该key存在值,若是为null的话,则插入新的value,最后一个参数,在hashMap中没有用,能够不用管,使用默认的便可**/ return putVal(hash(key), key, value, false, true); }
咱们看到这里调用了putVal以前调用了hash方法;
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
咱们看到这里是将键值的hashCode作了异或运算,至于为何这么复杂,目的大体就是为了减小哈希冲突。
下面咱们看看putVal
方法的源码:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { //tab 哈希数组,p 该哈希桶的首节点,n hashMap的长度,i 计算出的数组下标 Node<K,V>[] tab; Node<K,V> p; int n, i; //获取长度并进行扩容,使用的是懒加载,table一开始是没有加载的,等put后才开始加载 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; /**若是计算出的该哈希桶的位置没有值,则把新插入的key-value放到此处,此处就算没有插入成功,也就是发生哈希冲突时也会把哈希桶的首节点赋予p**/ if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); //发生哈希冲突的几种状况 else { // e 临时节点的做用, k 存放该当前节点的key Node<K,V> e; K k; //第一种,插入的key-value的hash值,key都与当前节点的相等,e = p,则表示为首节点 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; //第二种,hash值不等于首节点,判断该p是否属于红黑树的节点 else if (p instanceof TreeNode) /**为红黑树的节点,则在红黑树中进行添加,若是该节点已经存在,则返回该节点(不为null),该值很重要,用来判断put操做是否成功,若是添加成功返回null**/ e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); //第三种,hash值不等于首节点,不为红黑树的节点,则为链表的节点 else { //遍历该链表 for (int binCount = 0; ; ++binCount) { //若是找到尾部,则代表添加的key-value没有重复,在尾部进行添加 if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); //判断是否要转换为红黑树结构 if (binCount >= TREEIFY_THRESHOLD - 1) treeifyBin(tab, hash); break; } //若是链表中有重复的key,e则为当前重复的节点,结束循环 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } //有重复的key,则用待插入值进行覆盖,返回旧值。 if (e != null) { V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } //到了此步骤,则代表待插入的key-value是没有key的重复,由于插入成功e节点的值为null //修改次数+1 ++modCount; //实际长度+1,判断是否大于临界值,大于则扩容 if (++size > threshold) resize(); afterNodeInsertion(evict); //添加成功 return null; }
能够看到这里主要有如下几步:
一、根据key计算出在数组中存储的下标
二、根据使用的大小,判断是否须要扩容。
三、根据数组下标判断是否当前下标已存储数据,若是没有则直接插入。
四、若是存储了则存在哈希冲突,判断当前entry的key是否相等,若是相等则替换,不然判断下一个节点是否为空,为空则直接插入,不然取下一节点重复上述步骤。
五、判断链表长度是否大于8当达到8时转换为红黑树。
下面咱们看下HashMap的扩容函数resize()
resize()
final Node<K,V>[] resize() { //把没插入以前的哈希数组作我诶oldTal Node<K,V>[] oldTab = table; //old的长度 int oldCap = (oldTab == null) ? 0 : oldTab.length; //old的临界值 int oldThr = threshold; //初始化new的长度和临界值 int newCap, newThr = 0; //oldCap > 0也就是说不是首次初始化,由于hashMap用的是懒加载 if (oldCap > 0) { //大于最大值 if (oldCap >= MAXIMUM_CAPACITY) { //临界值为整数的最大值 threshold = Integer.MAX_VALUE; return oldTab; } //标记##,其它状况,扩容两倍,而且扩容后的长度要小于最大值,old长度也要大于16 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) //临界值也扩容为old的临界值2倍 newThr = oldThr << 1; } /**若是oldCap<0,可是已经初始化了,像把元素删除完以后的状况,那么它的临界值确定还存在, 若是是首次初始化,它的临界值则为0 **/ else if (oldThr > 0) newCap = oldThr; //首次初始化,给与默认的值 else { newCap = DEFAULT_INITIAL_CAPACITY; //临界值等于容量*加载因子 newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } //此处的if为上面标记##的补充,也就是初始化时容量小于默认值16的,此时newThr没有赋值 if (newThr == 0) { //new的临界值 float ft = (float)newCap * loadFactor; //判断是否new容量是否大于最大值,临界值是否大于最大值 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 table = newTab; //此处天然是把old中的元素,遍历到new中 if (oldTab != null) { for (int j = 0; j < oldCap; ++j) { //临时变量 Node<K,V> e; //当前哈希桶的位置值不为null,也就是数组下标处有值,由于有值表示可能会发生冲突 if ((e = oldTab[j]) != null) { //把已经赋值以后的变量置位null,固然是为了好回收,释放内存 oldTab[j] = null; //若是下标处的节点没有下一个元素 if (e.next == null) //把该变量的值存入newCap中,e.hash & (newCap - 1)并不等于j newTab[e.hash & (newCap - 1)] = e; //该节点为红黑树结构,也就是存在哈希冲突,该哈希桶中有多个元素 else if (e instanceof TreeNode) //✨✨✨把此树进行转移到newCap中✨✨✨ ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); else { /**此处表示为链表结构,一样把链表转移到newCap中,就是把链表遍历后,把值转过去,在置位null**/ 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; } } } } } //返回扩容后的hashMap return newTab; }
前面主要介绍了, HashMap
的结构为数组+ 链表(红黑树)。
总结一下上面的逻辑就是:
一、对数组进行扩容,
二、扩容后从新计算hashCode也就是key的下标,将原数据塞到新扩容后的数据结构中。
三、当存在hash冲突时,在数组后面以链表的形式追加到后面,当链表长度达到8时,就会将链表转换为红黑树。
那么对于红黑树新增一个节点 ,咱们考虑到前面所说的红黑树的性质。就须要对红黑树作调整,是红黑树达到平衡。这种平衡就是红黑树的旋转。下面咱们看看红黑树的旋转:
红黑树的旋转分为左旋和右旋,以某个节点为圆心向左或向右旋转,具体咱们经过下面的图来看下[https://www.cnblogs.com/Carpe...]。
static <K,V> TreeNode<K,V> rotateLeft(TreeNode<K,V> root, TreeNode<K,V> p) { TreeNode<K,V> r, pp, rl; if (p != null && (r = p.right) != null) { if ((rl = p.right = r.left) != null) rl.parent = p; if ((pp = r.parent = p.parent) == null) (root = r).red = false; else if (pp.left == p) pp.left = r; else pp.right = r; r.left = p; p.parent = r; } return root; }
static <K,V> TreeNode<K,V> rotateRight(TreeNode<K,V> root, TreeNode<K,V> p) { TreeNode<K,V> l, pp, lr; if (p != null && (l = p.left) != null) { if ((lr = p.left = l.right) != null) lr.parent = p; if ((pp = l.parent = p.parent) == null) (root = l).red = false; else if (pp.right == p) pp.right = l; else pp.left = l; l.right = p; p.parent = l; } return root; }
TreeMap的结构也是红黑树,它新增节点的过程以下:这里跟HashMap的红黑树的新增原理同样
咱们经过这个例子有差很少已经了解了红黑树的原理。咱们回到 resize()
方法,里面咱们看
//✨✨✨把此树进行转移到newCap中✨✨✨
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
TreeNode.split
final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) { TreeNode<K,V> b = this; // 拿到调用此方法的节点 TreeNode<K,V> loHead = null, loTail = null; // 存储跟原索引位置相同的节点 TreeNode<K,V> hiHead = null, hiTail = null; // 存储索引位置为:原索引+oldCap的节点 int lc = 0, hc = 0; for (TreeNode<K,V> e = b, next; e != null; e = next) { // 从b节点开始遍历 next = (TreeNode<K,V>)e.next; // next赋值为e的下个节点 e.next = null; // 同时将老表的节点设置为空,以便垃圾收集器回收 //若是e的hash值与老表的容量进行与运算为0,则扩容后的索引位置跟老表的索引位置同样 if ((e.hash & bit) == 0) { if ((e.prev = loTail) == null) // 若是loTail为空, 表明该节点为第一个节点 loHead = e; // 则将loHead赋值为第一个节点 else loTail.next = e; // 不然将节点添加在loTail后面 loTail = e; // 并将loTail赋值为新增的节点 ++lc; // 统计原索引位置的节点个数 } //若是e的hash值与老表的容量进行与运算为1,则扩容后的索引位置为:老表的索引位置+oldCap else { if ((e.prev = hiTail) == null) // 若是hiHead为空, 表明该节点为第一个节点 hiHead = e; // 则将hiHead赋值为第一个节点 else hiTail.next = e; // 不然将节点添加在hiTail后面 hiTail = e; // 并将hiTail赋值为新增的节点 ++hc; // 统计索引位置为原索引+oldCap的节点个数 } } if (loHead != null) { // 原索引位置的节点不为空 if (lc <= UNTREEIFY_THRESHOLD) // 节点个数少于6个则将红黑树转为链表结构 tab[index] = loHead.untreeify(map); else { tab[index] = loHead; // 将原索引位置的节点设置为对应的头结点 // hiHead不为空则表明原来的红黑树(老表的红黑树因为节点被分到两个位置) // 已经被改变, 须要从新构建新的红黑树 if (hiHead != null) loHead.treeify(tab); // 以loHead为根结点, 构建新的红黑树 } } if (hiHead != null) { // 索引位置为原索引+oldCap的节点不为空 if (hc <= UNTREEIFY_THRESHOLD) // 节点个数少于6个则将红黑树转为链表结构 tab[index + bit] = hiHead.untreeify(map); else { tab[index + bit] = hiHead; // 将索引位置为原索引+oldCap的节点设置为对应的头结点 // loHead不为空则表明原来的红黑树(老表的红黑树因为节点被分到两个位置) // 已经被改变, 须要从新构建新的红黑树 if (loHead != null) hiHead.treeify(tab); // 以hiHead为根结点, 构建新的红黑树 } } }
这个方法中咱们重点看treeify
treeify
final void treeify(Node<K,V>[] tab) { // 构建红黑树 TreeNode<K,V> root = null; for (TreeNode<K,V> x = this, next; x != null; x = next) {// this即为调用此方法的TreeNode next = (TreeNode<K,V>)x.next; // next赋值为x的下个节点 x.left = x.right = null; // 将x的左右节点设置为空 if (root == null) { // 若是尚未根结点, 则将x设置为根结点 x.parent = null; // 根结点没有父节点 x.red = false; // 根结点必须为黑色 root = x; // 将x设置为根结点 } else { K k = x.key; // k赋值为x的key int h = x.hash; // h赋值为x的hash值 Class<?> kc = null; // 若是当前节点x不是根结点, 则从根节点开始查找属于该节点的位置 for (TreeNode<K,V> p = root;;) { int dir, ph; K pk = p.key; if ((ph = p.hash) > h) // 若是x节点的hash值小于p节点的hash值 dir = -1; // 则将dir赋值为-1, 表明向p的左边查找 else if (ph < h) // 与上面相反, 若是x节点的hash值大于p节点的hash值 dir = 1; // 则将dir赋值为1, 表明向p的右边查找 // 走到这表明x的hash值和p的hash值相等,则比较key值 else if ((kc == null && // 若是k没有实现Comparable接口 或者 x节点的key和p节点的key相等 (kc = comparableClassFor(k)) == null) || (dir = compareComparables(kc, k, pk)) == 0) // 使用定义的一套规则来比较x节点和p节点的大小,用来决定向左仍是向右查找 dir = tieBreakOrder(k, pk); TreeNode<K,V> xp = p; // xp赋值为x的父节点,中间变量用于下面给x的父节点赋值 // dir<=0则向p左边查找,不然向p右边查找,若是为null,则表明该位置即为x的目标位置 if ((p = (dir <= 0) ? p.left : p.right) == null) { x.parent = xp; // x的父节点即为最后一次遍历的p节点 if (dir <= 0) // 若是时dir <= 0, 则表明x节点为父节点的左节点 xp.left = x; else // 若是时dir > 0, 则表明x节点为父节点的右节点 xp.right = x; // 进行红黑树的插入平衡(经过左旋、右旋和改变节点颜色来保证当前树符合红黑树的要求) root = balanceInsertion(root, x); break; } } } } moveRootToFront(tab, root); // 若是root节点不在table索引位置的头结点, 则将其调整为头结点 }
咱们重点看这个方法balanceInsertion(root, x)
这个方法就是使红黑树达到平衡。咱们接着继续看,要平衡红黑树就得左右旋转。
balanceInsertion
static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root, TreeNode<K,V> x) { x.red = true; for (TreeNode<K,V> xp, xpp, xppl, xppr;;) { if ((xp = x.parent) == null) { x.red = false; return x; } else if (!xp.red || (xpp = xp.parent) == null) return root; if (xp == (xppl = xpp.left)) { if ((xppr = xpp.right) != null && xppr.red) { xppr.red = false; xp.red = false; xpp.red = true; x = xpp; } else { if (x == xp.right) { root = rotateLeft(root, x = xp);//对红黑树进行左旋 xpp = (xp = x.parent) == null ? null : xp.parent; } if (xp != null) { xp.red = false; if (xpp != null) { xpp.red = true; root = rotateRight(root, xpp);//对红黑树进行右旋 } } } } else { if (xppl != null && xppl.red) { xppl.red = false; xp.red = false; xpp.red = true; x = xpp; } else { if (x == xp.left) { root = rotateRight(root, x = xp);//对红黑树进行右旋 xpp = (xp = x.parent) == null ? null : xp.parent; } if (xp != null) { xp.red = false; if (xpp != null) { xpp.red = true; root = rotateLeft(root, xpp);//对红黑树进行左旋 } } } } } }
看到这里基本思想已经明白了,咱们下面总结一下:
咱们经过下面一副图来看,数组+链表+红黑树
咱们经过下面的图来看看HashMap的扩容过程
以上就是本文主要讲解的HashMap 的核心思想,若有不对请指证。下一篇文章将带你们手撸HashMap。[深刻理解HashMap原理(二)——手写HashMap
](https://blog.csdn.net/u013132...
深刻理解HashMap原理(一)——HashMap源码解析(JDK 1.8)
Android热修复——深刻剖析AndFix热修复及本身动手实现
手撸一款Android屏幕适配SDK
Android自定义无压缩加载超清大图