原创做品,能够转载,可是请标注出处地址:http://www.javashuo.com/article/p-eygkcuto-a.htmlhtml
HashMap是基于哈希表实现的映射集合。 HashMap能够拥有null键和null值,可是null键只能有一个,null值不作限制。HashTable是不容许null键和值的。 HashMap是非线程安全的集合,HashTable是添加了同步功能的HashMap,是线程安全的。 HashMap是无序的,并不能保证其内部键值对的顺序。 HashMap提供了常量级复杂度的元素获取和添加操做(固然是在hash分散均匀的状况下)。 HashMap有两个影响功能的因素:初始容量与负载因子,当集合中的元素数量超过了初始容量和负载因子的乘积值时,会触发resize扩容 HashMap默认的初始容量是16,负载因子是0.75 HashMap在链表添加元素是采用尾插法,以前的版本采用头插法,由于会致使循环链表的问题,改为了尾插法,并添加了红黑树来优化链表过长的状况下查询慢的问题 HashMap底层结构为数组+链表/红黑树 HashMap底层链表的元素达到8个的状况下,若是HasnMap内部桶个数(即桶容量)达到64个则进行树形化,不然进行resize扩容
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable { //... // 默认的初始容量,值为16,若是自定义也必须为2的次幂 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 最大容量值 static final int MAXIMUM_CAPACITY = 1 << 30; // 默认的负载因子,值为0.75,可自定义,必须小于1 static final float DEFAULT_LOAD_FACTOR = 0.75f; // 链表转树结构的元素数量界限值,当某个hash值下的链表元素个数达到8个, // 则将其改成树结构 static final int TREEIFY_THRESHOLD = 8; // 红黑树反转链表的元素数量界限值,当数量小于6的时候才会反转 static final int UNTREEIFY_THRESHOLD = 6; // 树形阈值,这个阈值针对的是整个Map中桶的数量,表示只有在所拥有的桶数量 // 达到64时才能执行树形化,不然先去扩容去吧,可见在桶数小于64时,优先执行扩容 static final int MIN_TREEIFY_CAPACITY = 64; // 表示桶数组 transient Node<K,V>[] table; // 缓存 transient Set<Map.Entry<K,V>> entrySet; // 集合中元素的数量, transient int size; // 集合结构的修改次数,包括集合元素的增删,和集合结构的变化,仅仅更改已有 // 元素的值并不会增长该值,主要用于Iterator的快速失败 transient int modCount; // 集合的扩容阈值 int threshold; // 集合的负载因子,默认0.75,时间与空间的折中,增长负载因子, // 能增长元素容纳量,减少空间消耗,却增长的查询的时间消耗, // 减少负载因子,能减小元素容纳量,减小查询时间消耗,但却要及早的去扩容, // 增长了空间消耗 final float loadFactor; // ... }
添加新的映射元素(newKey,newValue),首先经过特定的hash算法计算newKey的hash值(newHash)。java
Hash算法:获取newKey的hashCode值,而后进行高低位相异或。
hashCode值的获取方法在Object类中已有定义,固然也有某些类进行了重写,总的来讲有如下几种:node
- String类型的hashCode:自定义算法较复杂
- 包装类型的hashCode:当前值
- 其余类型的hashCode:类名+@+内存位置的16进制表示
若是是首次添加元素,那么就意味着桶还没有初始化,因此这里会先执行初始化操做(resize),
若是初始化成功或者非首次添加元素,那么开始定位元素的桶位算法
桶定位算法:用以前hash算法的结果newHash与桶的个数-1进行与操做
该算法的本意是保留newHash值的后几进制位来肯定桶位,如何保留后几位呢?咱们知道二进制算法中1的与操做具备保留原值的效果
这里正是使用这种原理来实现,假设桶位数为16位,16的二进制位10000,16-1=15,15的二进制位就是1111,末四位全是1,经过1的
保留原值的做用,当那它与newHash值的二进制值进行与操做后,结果就是newHash保留后4位的结果。而4位正好在桶位0-15以内。
而这也就是桶位数必须是2的次幂的缘由,由于2的次幂的数字的二进制值所有是首位为1,其后全是0的值,当其-1以后就会变成首位
变0,其后全是1的值,而桶的下标是从0开始,最高位正好是-1以后的值。数组
查看确认桶位是否已有元素,若是没有,直接存放新元素到该桶位,
若是桶位已有元素存在,那么就是出现碰撞,这时的解决办法就是使用链表或者红黑树来存储,
若是该桶位存储的数据结构已是红黑树,那么执行红黑树添加元素操做,
不然执行链表的后插法,将新元素插入到链表的末尾。缓存
后插法:1.7以前的版本全是前插法,将新元素做为表头元素,1.8以后改为后插法,将新元素做为表尾元素
在执行后插法的时候须要遍历链表,查找是否存在相同key的元素,若存在则直接用newValue替换旧值,再也不执行插入操做。安全
新元素插入完成以后,校验Map中总元素个数是否达到了阈值(这个阈值是桶容量和负载因子的乘积),若是超过阈值则进行扩容。数据结构
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable { //... public V put(K key, V value) { // 首先经过hash方法计算hash值,而后执行存值操做 return putVal(hash(key), key, value, false, true); } // hash算法:首先获取key的hashCode,而后将其高低16位相异或,全员参与(hashcode值的全部二进制位都能参与hash运算) static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } 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; // 定位桶下标,n的值为2的次幂,同时也是桶的数量,hash是以前经过hash算法得出的结果,n-1以后末几位所有是1, // 再和hash与运算,等于保留hash的后几位做为结果。好比:(1111)&(01010101)的结果为0101,保留了后四位 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)))) // 存在相同key的状况(桶位置) 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)))) // 存在相同key的状况(链表元素位置) break; p = e; } } // 针对存在相同key的状况进行统一处理:替换value 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; } //... }
注意:桶容量为2的次幂的缘由正是由于便于元素经过位运算实现定位。多线程
执行扩容方法的缘由主要是集合中元素数量达到阈值或者是集合桶数组某个桶位置的元素数量达到8个,但集合桶容量未超过64的状况下,
特殊的状况是首次添加元素时的初始化操做也走这个方法。并发
针对初始化操做:
只会计算初始化容量和初始化阈值而后建立一个初始桶数组并返回结果。
对于使用了带参构造器的状况,会定制初始容量和负载因子,若是只定制了初始容量则使用默认负载因子,
构造器会经过一个进制运算根据自定义的容量算出一个大于等于自定义容量值的最小的2的次幂值做为真正的容量
好比:自定义容量为10,则计算容量值为16,而后再根据这个容量计算阈值为12。
而针对扩容操做:
首先校验旧容量是否已经达到或者大于容量最大值MAXIMUM_CAPACITY,若是是则再也不进行扩容操做,还在原桶数组中保存元素,
并将阈值设置为Integer的最大值,设置为最大值以后就不会再触发扩容操做(由于Map中元素的总个数最大也就是Integer的最大值了,不可能比之更大),
而后校验容量加倍后的新容量是否超过容量最大值MAXIMUM_CAPACITY,若是没有的话则将阈值加倍。
新容量和新阈值都有了,而后建立新的桶数组,在以后就是元素迁移了。
元素迁移:
遍历旧桶数组,校验每一个桶位的元素结构,
若是只有一个元素,直接在新桶数组进行重定位,定位方式不变,
若是是红黑树,走树结构迁移逻辑,
不然就是链表,进行链表迁移,链表迁移进行了平衡优化,因为新桶数组和旧数组的两倍容量,
咱们简单的将新容量分红相等的两半,称之为低位区与高位区,低位区下标与旧数组相同,
高位区下标为旧数组下标+旧数组容量。
链表迁移时,会根据该链表中元素的键的hash值与旧容量进行与运算,这就会有两个结果,为0或者不为0。
旧容量也是2的次幂,高位为1,其后全是0,好比10000(表示容量为16),将其和hash结果相与,
只会保留旧容量二进制为1的那一位对应的hash值的那一位,其他位全变成0,若是hash值的那一
位为0结果就是0,hash值那一位为1结果就是10000。
根据相与的结果来进行链表分拆,将结果为0的链表元素还定位到相同的桶下标位,即新桶数组的低位区,将结果为1的链表元素定位到原下标+旧桶容量的位置,即高位区。
这两个链表会先组装链表结构,而后将链表表头元素定位到低位区或高位区。
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable { //... final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; int oldCap = (oldTab == null) ? 0 : oldTab.length;// 旧容量 int oldThr = threshold;// 旧阈值 int newCap, newThr = 0;// 新容量、新阈值 if (oldCap > 0) { if (oldCap >= MAXIMUM_CAPACITY) { // 若是旧容量已经达到最大容量值,将阈值设置为最大值,返回旧桶数组 threshold = Integer.MAX_VALUE; return oldTab; } else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) // 若是加倍后的新容量没有超过最大容量,且旧容量大于等于16,则新阈值加倍 newThr = oldThr << 1; // double threshold } else if (oldThr > 0) // initial capacity was placed in threshold // 只在经过带参数的构造器(初始容量和负载因子) // 建立的容器首次添加元素进行桶数组初始化时会走这里 newCap = oldThr; else { // zero initial threshold signifies using defaults // 初始化容量和阈值,这就是首次添加元素时执行的初始化逻辑 newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } if (newThr == 0) {// 计算新阈值 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; // 若是是初始化操做,此处oldTab为null,会直接返回新建桶数组,不然执行元素迁移 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 针对链表的状况 Node<K,V> loHead = null, loTail = null; Node<K,V> hiHead = null, hiTail = null; Node<K,V> next; do {// 遍历旧桶位的旧链表 next = e.next; // 这个判断的结果取决于在于hash值在于oldCap的1所在进制位对应的进制位是1仍是0, // 因为oldCap只有这一位为1,那么hash的该位将保留原值,其他位所有得0,增长这么 // 一个貌似随机的判断,用于进一步分散元素到不一样的桶。 // 其实就是将旧桶第i位桶的链表元素分散到新桶的第i和第i+oldCap桶位上,为0仍是为1随机 // 在循环中造成两个小链表,而后将首个元素赋值给新桶的对应桶位便可。 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; } //... }
简单看代码
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable { //... public V get(Object key) { Node<K,V> e; // 计算key的hash值用做桶定位的基础 return (e = getNode(hash(key), key)) == null ? null : e.value; } final Node<K,V> getNode(int hash, Object key) { Node<K,V>[] tab; Node<K,V> first, e; int n; K k; if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { // 只有一个元素、链表头元素、树根元素要单独校验 if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k)))) return first; // 存在链表或者树结构的状况 if ((e = first.next) != null) { if (first instanceof TreeNode) // 执行树结构获取元素逻辑 return ((TreeNode<K,V>)first).getTreeNode(hash, key); // 不然就是链表结构,遍历链表寻找匹配的元素 do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } return null; } //... }
简单看源码
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable { //... public V remove(Object key) { Node<K,V> e; // 计算key的hash值,用做后面定位元素桶位的基础 return (e = removeNode(hash(key), key, null, false, true)) == null ? null : e.value; } final Node<K,V> removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable) { Node<K,V>[] tab; Node<K,V> p; int n, index; // 先进行桶定位,定位方式不变 if ((tab = table) != null && (n = tab.length) > 0 && (p = tab[index = (n - 1) & hash]) != null) { Node<K,V> node = null, e; K k; V v; // 针对只有一个元素、链表头元素、树根元素进行处理 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) node = p; // 若是第一个元素不是,针对链表和树结构进行后续元素处理 else if ((e = p.next) != null) { if (p instanceof TreeNode) // 执行树结构获取元素逻辑 node = ((TreeNode<K,V>)p).getTreeNode(hash, key); else { // 链表循环获取等key的元素 do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) { node = e; break; } p = e; } while ((e = e.next) != null); } } if (node != null && (!matchValue || (v = node.value) == value || (value != null && value.equals(v)))) { if (node instanceof TreeNode) // 执行树结构删除元素操做 ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable); else if (node == p) // 针对链表头和树根元素的状况 tab[index] = node.next; else // 针对链表内元素进行删除 p.next = node.next; ++modCount; --size; afterNodeRemoval(node); return node; } } return null; } //... }
参照源码
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable { //... // 树形化准备 final void treeifyBin(Node<K,V>[] tab, int hash) { int n, index; Node<K,V> e; if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) // 对于触发了树形化操做,可是桶容量还没达到64的状况下优先去作扩容处理,扩容也会分拆链表 resize(); // 定位要作树形下的桶位置,获取桶位元素e else if ((e = tab[index = (n - 1) & hash]) != null) { TreeNode<K,V> hd = null, tl = null; // 循环遍历链表中的元素,将其改形成为双向链表结构,表头元素为hd do { // 将e元素封装成为树节点TreeNode 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); } } // 将Node节点封装成树节点 TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) { return new TreeNode<>(p.hash, p.key, p.value, next); } static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> { //... // 树形化操做 final void treeify(Node<K,V>[] tab) { TreeNode<K,V> root = null;//表明根节点 // 此处循环将this赋值给x,this表明的是当前树节点,这个类是HashMap的内部类用于标识树节点, // this就是当前类的实例,也就是一个树节点,可是是哪一个树节点,就要依靠之间的代码上下文来判 // 断了,看看调用该方法的地方有这样的代码:hd.treeify(tab);这就表示当前节点就是那额hd节 // 点,而这个hd节点就是以前改造好的双向链表的表头结点 // 这里循环的是双向链表中的元素 for (TreeNode<K,V> x = this, next; x != null; x = next) { next = (TreeNode<K,V>)x.next; x.left = x.right = null; if (root == null) { // root == null的状况是链表头结点的时候才会出现,这时候将这个头结点做为树根节点 x.parent = null;//根节点无父节点 x.red = false;//黑色 root = x;//赋值 } else { // 这里只有非链表头节点才能进来 K k = x.key; int h = x.hash; Class<?> kc = null; // 此处循环的是已构建的红黑树的节点,从根节点开始,遍历比较当前链表节点与当前红黑树节点的 // hash值,dir用于保存比较结果,若是当前链表节点小,则dir为-1,不然为1,实际状况倒是,能 // 拨到同一个桶位的全部元素的hash值那是同样的呀,因此dir的值是没法依靠hash值比较得出结果 // 的,那么重点就靠最后一个条件判断来得出结果了, for (TreeNode<K,V> p = root;;) { int dir, ph; K pk = p.key; if ((ph = p.hash) > h) dir = -1; else if (ph < h) dir = 1; else if ((kc == null && (kc = comparableClassFor(k)) == null) || (dir = compareComparables(kc, k, pk)) == 0) dir = tieBreakOrder(k, pk);// 最后须要依靠这个方法来决定dir的值 TreeNode<K,V> xp = p; // 根据dir的值来决定将当前链表节点保存到当前树节点的左边仍是右边, // 或者当前链表节点须要与当前树节点的左节点仍是右节点接着比较 // 主要寻找子节点为null的状况,将节点保存到null位置 if ((p = (dir <= 0) ? p.left : p.right) == null) { x.parent = xp; if (dir <= 0) // dir<=0,将链表节点保存到当前树节点的左边子节点位置 xp.left = x; else // dir<=0,将链表节点保存到当前树节点的右边子节点位置 xp.right = x; // 一旦添加的一个新节点,就要进行树平衡操做,以此保证红黑树结构 // 树的平衡操做依靠的就是其左右旋转操做 root = balanceInsertion(root, x); break; } } } } // 最后将组装好的树的根节点保存到桶下标位 moveRootToFront(tab, root); } static <K,V> void moveRootToFront(Node<K,V>[] tab, TreeNode<K,V> root) { int n; if (root != null && tab != null && (n = tab.length) > 0) { // 首先定位桶下标位 int index = (n - 1) & root.hash; TreeNode<K,V> first = (TreeNode<K,V>)tab[index]; // 校验当前桶下标位的值是否为根节点的值,可能会存在不一样的缘由是树的平衡操做将本来的根节点挪移了 // 若是相同,那么不做任何处理,若是不一样,就须要替换桶位元素为树根节点元素,而后改变双向链表结构 // 将root根节点做为双向链表表头元素,为什么要替换呢,由于在判断桶位元素类型时会对链表进行遍历,如 // 果桶位置放的不是链表头或者尾元素,遍历将变得很是麻烦 if (root != first) { Node<K,V> rn; tab[index] = root; TreeNode<K,V> rp = root.prev; if ((rn = root.next) != null) ((TreeNode<K,V>)rn).prev = rp; if (rp != null) rp.next = rn; if (first != null) first.prev = root; root.next = first; root.prev = null; } // 校验链表和树的结构 assert checkInvariants(root); } } //... } //... }
很简单,看源码
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable { //... static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> { //... // 将一颗大树分拆为两颗小树,若是树过小,退化为单向链表 final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) { // this表明当前节点,也就是树的根节点,桶位节点 // map表明当前集合 // tab表明新桶数组 // index表明当前节点的桶位下标 // bit为旧桶容量 TreeNode<K,V> b = this; // Relink into lo and hi lists, preserving order TreeNode<K,V> loHead = null, loTail = null; TreeNode<K,V> hiHead = null, hiTail = null; int lc = 0, hc = 0;// lc表示低位树容量,hc表示高位树容量 for (TreeNode<K,V> e = b, next; e != null; e = next) { next = (TreeNode<K,V>)e.next; e.next = null; // 分拆树节点的依据,结果为0的一组(低位组),结果不为0的一组(高位组) if ((e.hash & bit) == 0) { // 组装低位组双向链表 if ((e.prev = loTail) == null) loHead = e; else loTail.next = e; loTail = e; ++lc; } else { // 组装高位组双向链表 if ((e.prev = hiTail) == null) hiHead = e; else hiTail.next = e; hiTail = e; ++hc; } } // 针对低位组进行树形化处理,若是该组元素数量少于6个则退化为单向链表 if (loHead != null) { if (lc <= UNTREEIFY_THRESHOLD) tab[index] = loHead.untreeify(map); else { tab[index] = loHead; if (hiHead != null) // (else is already treeified) loHead.treeify(tab); } } // 针对高位组进行树形化处理,若是该组元素少于6个则退化为单向链表 if (hiHead != null) { if (hc <= UNTREEIFY_THRESHOLD) tab[index + bit] = hiHead.untreeify(map); else { tab[index + bit] = hiHead; if (loHead != null) hiHead.treeify(tab); } } } //... } //... }
参照源码
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable { //... static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> { //... // 红黑树的添加元素,map为当前HashMap,tab为当前桶数组,h为新增元素的key的hash值,k为新增元素的key,v为新增元素的value final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab, int h, K k, V v) { Class<?> kc = null; boolean searched = false; // 当前节点是已定位的桶位元素,其实就是树结构的根节点元素 TreeNode<K,V> root = (parent != null) ? root() : this; for (TreeNode<K,V> p = root;;) { // dir表明当前树节点与待添加节点的hash比较结果 // ph表明当前树节点的hash值 // pk表明当前树节点的key // 因为一个桶位的全部元素hash值相等,因此最后得出结果须要依靠 int dir, ph; K pk; if ((ph = p.hash) > h) // 若是当前节点的hash值大,dir为-1 dir = -1; else if (ph < h) // 若是当前节点的hash值小,dir为1 dir = 1; else if ((pk = p.key) == k || (k != null && k.equals(pk))) // hash值相等的状况下,若是key也同样直接返回当前节点,返回去以后会执行value的替换操做 return p; else if ((kc == null && (kc = comparableClassFor(k)) == null) || (dir = compareComparables(kc, k, pk)) == 0) { if (!searched) { TreeNode<K,V> q, ch; searched = true; if (((ch = p.left) != null && (q = ch.find(h, k, kc)) != null) || ((ch = p.right) != null && (q = ch.find(h, k, kc)) != null)) // 这个找到的q也是与待添加元素key相同的元素,执行替换 return q; } // 最终须要依靠这个方法来得出dir值的结果 dir = tieBreakOrder(k, pk); } TreeNode<K,V> xp = p; // 根据dir的值来决定是当前节点的左侧仍是右侧,若是该侧右子节点则继续循环寻找位置,不然直接将新元素添加到该侧子节点位置 if ((p = (dir <= 0) ? p.left : p.right) == null) { Node<K,V> xpn = xp.next; TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);//封装树节点 if (dir <= 0) // dir<=0,将新节点添加到当前节点左侧 xp.left = x; else // 不然将新节点添加到当前节点右侧 xp.right = x; // 设置新节点的链表位置,将其做为xp的下级节点 xp.next = x; x.parent = x.prev = xp; if (xpn != null) // 若是xp节点本来有下级节点xpn,则要将新节点插入到xp和xpn之间(指双向链表中) ((TreeNode<K,V>)xpn).prev = x; // 插入了新节点以后,要进行树平衡操做,平衡操做完成,将根节点设置为桶位节点 moveRootToFront(tab, balanceInsertion(root, x)); return null; } } } final TreeNode<K,V> root() { for (TreeNode<K,V> r = this, p;;) { if ((p = r.parent) == null) return r; r = p; } } // 通常咱们在HashMap中保存的键值对的类型都是不变的,这通常用泛型控制, // 那么就意味着,两个元素的key的类型时同样的,因此才须要靠其hashCode来决定大小 // System.identityHashCode(parameter)是本地方法,用于获取和hashCode同样的结果, // 这里的hashCode指的是默认的hashCode方法,与某些类重写的无关 static int tieBreakOrder(Object a, Object b) { int d; if (a == null || b == null || (d = a.getClass().getName(). compareTo(b.getClass().getName())) == 0) d = (System.identityHashCode(a) <= System.identityHashCode(b) ? -1 : 1); return d; } //... } //... }
绕A节点左旋,等于将A的右子节点B甩上来替换本身的位置,而本身顺势下沉成为其左子节点,这时你会发现,B有三个子节点,明显结构不对,将B的原来的左子节点C转移到下沉的A上,成为其右子节点,旋转结束
其实,要保证左子树节点值小于其根节点,右子树节点值大于其根节点,那么在替换AB节点以后,C节点的值就出现了问题,只有将其挪到A节点右边才能继续保证上面的结构。
首先咱们知道B节点为A的右节点,那么B>A,而C为B的左节点,则C<B,而C又位于A的右子树,则C>A,所以:A<C<B。要保证这个式子永远成立,就必须依靠挪移节点来完成。
如今B为最顶部节点且为最大值,那么A和C必须位于其左子树,而C>A则,C必须位于A的右子树,再看看以前的状况,若是A为顶点节点,那么BC均应位于其右子树,而B>C,那么要么B为C的右节点,要么C为B的左节点
绕A几点右旋,等于将A的左子节点B甩上来替换本身的位置,而本身顺势下沉成为其右子节点,这是你会发现,B有三个子节点,明显结构不对,将B的原来的右子节点C转移到下沉的A上,成为其左子节点,旋转结束
首先咱们知道B为A的左子节点,因此B<A,再者C为B的右子节点,那么C>B,而C又位于A的左子树,则C<A,最后:A>C>B。要保证这个结果成立,那么再B替换A的位置以后,A下沉为B的右子节点,由于A>B,因此往右走,
这时C和A均位于B的右侧,比较两者发现C<A,那么将C放到A的左侧成为其左子节点
新增节点所有初始化为红色节点,而后分如下几种状况:
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable { //... static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> { //... // 左旋操做,其中root为根节点,p为当前节点,r为p的右节点,rl为r的左节点,pp为p的父节点 // 左旋以后,r替换p的位置,rl挪到p的右节点 // 节点位置变换以后,既要改变其父节点的left/right值,也要改变当前节点中parent的值, // 改变是双向的,父子均有指向,改变以后均要修改 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) { // 首先将r节点的左子节点(rl)送给p当其右子节点 if ((rl = p.right = r.left) != null) rl.parent = p;//变换rl的父节点为p,原来为r if ((pp = r.parent = p.parent) == null) // 原p节点为根节点的状况,r替换以后,须要从新着色为黑色,保证根节点为黑色 (root = r).red = false; else if (pp.left == p) // 原p节点为其父节点pp的左子节点的状况,r替换后,须要修改pp节点的left指向r节点 pp.left = r; else // 原p节点为其父节点pp的右子节点的状况,r替换后,须要修改pp节点的right指向r节点 pp.right = r; //而后将p节点做为r节点的左子节点,即为p节点顺势下沉为r的左子节点 r.left = p; p.parent = r;//变换p的父节点为r } return root; } // 右旋操做,嘿,那就是左旋的反向操做罢了 // root为根节点,p为当前节点,l为其左子节点,lr为l的右子节点,pp为p的父节点 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) { // 首先将l的右子节点lr挪给p if ((lr = p.left = l.right) != null) lr.parent = p;//变换lr的父节点为p,原来为l if ((pp = l.parent = p.parent) == null) // 若是p节点是根节点,替换为l以后,l便成为新的根节点,须要从新着色为黑色,保证红黑树结构 (root = l).red = false; else if (pp.right == p) // 若是原p节点是其父节点pp的右子节点,那么须要将其右子节点改为l pp.right = l; else // 若是原p节点是其父节点pp的左子节点,那么须要将其左子节点改为l pp.left = l; // 最后将原p节点置为l节点的右子节点,并修改p的父节点为l l.right = p; p.parent = l; } return root; } // 平衡操做,x为新增节点,root为根节点 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) { // 1 x为根节点的状况,将其从新着色为黑色 x.red = false; return x; } else if (!xp.red || (xpp = xp.parent) == null) // 2 若是x节点的父节点为黑色,又或者x的父节点是根节点,没有影响,不操做 return root; if (xp == (xppl = xpp.left)) { // 3 若是x节点的父节点是其父节点(x的祖父节点)的左子节点 if ((xppr = xpp.right) != null && xppr.red) { // 3-1 再若是x的祖父节点的右子节点存在且为红色,则将这个节点和x的父节点通通改为黑色, // 再把x的祖父节点改为红色,将x祖父节点做为新的x节点执行循环 xppr.red = false; xp.red = false; xpp.red = true; x = xpp; } else { // 3-2 不然的状况 if (x == xp.right) { // 3-2-1 若是x节点是其父节点的右子节点,则执行以x父节点为基准的左旋操做, // 左旋以后新增节点x替了其原父节点xp,将原xp节点当作如今的x节点,原来的x // 节点是如今x节点的父节点xp,原来的x节点的祖父节仍是如今x的祖父节点 root = rotateLeft(root, x = xp); xpp = (xp = x.parent) == null ? null : xp.parent; } if (xp != null) {// xp为原来的x节点 // 将xp节点置为黑色 xp.red = false; if (xpp != null) {// xpp仍是以前的xpp // 将xpp节点置为红色,而后执行右旋,右旋能够将xpp节点用xp节点替换,红黑交换 xpp.red = true; root = rotateRight(root, xpp); } } } } else { // 4 若是x节点的父节点是其父节点(x的祖父节点)的右子节点 if (xppl != null && xppl.red) { // 4-1 再若是x的祖父节点的左子节点存在而且为红色,则将该节点置为黑色, // 将x的父节点置为黑色,祖父节点置为红色,而后把xpp祖父节点做为新的x节点 xppl.red = false; xp.red = false; xpp.red = true; x = xpp; } else { // 4-2 不然的状况 if (x == xp.left) { // 4-2-1 若是x节点是其父节点的左子节点的状况,先以x父节点进行右旋, // 右旋以后原来的xp节点被新的x节点替换,原来的xp节点做为新xp节点的右子节点, // 如今看做为x,而后从新定义xpp,其实xpp位置不变 root = rotateRight(root, x = xp); xpp = (xp = x.parent) == null ? null : xp.parent; } if (xp != null) { // 将如今的xp节点置为黑色 xp.red = false; if (xpp != null) { // 将祖父节点置为红色。而后执行左旋,左旋以后,原来的xp节点接替了xpp节点的位置,xpp变成原来xp的左子节点 xpp.red = true; root = rotateLeft(root, xpp); } } } } } } //... } //... }
红黑树的节点删除操做主要分为这么三种:
针对第一种状况,真的好简单,待删节点即为叶子节点,直接删除便可;
针对第二种状况,也不难,将那个子节点替换待删节点便可;
至于第三种状况,那就麻烦了,但经过变换,能够将其转化为第一种或者第二种状况:处理方式是,找到待删节点的右子树中的最左节点(或者左子树中的最右节点),将其与待删节点互换位置,而后就将状况转换为第一种或者第二种了。
针对第三种状况转换方法的解析:为何要找到待删节点的右子树最左节点呢,由于红黑树是二叉搜索树,这个二叉搜索树中知足"左子节点<其父节点<其父节点的右子节点"的规则,那么找到的右子树的最左节点,就是整颗树中大于待删节点值的最小值节点了,为了保证二叉搜索树的搜索结构(也就是刚刚那个公式),咱们只能找最接近待删节点值的节点值来接替它的位置,如此能保证二叉搜索的结构,可是可能会破坏红黑树的结构,由于若是待删节点为红色,而替换节点为黑色的话,那岂不是在待删节点分支多加了一个黑色节点嘛,还有其余各类状况,种种,须要进行删除节点后的树平衡操做来保证红黑树的结构完整。
下面重点说说删除后的平衡问题:
其实只要待删节点是黑色节点,一旦删除必然会致使分支中黑色节点缺一(红黑树再也不平衡),具体状况又分为如下几种:(基础条件:待删节点p为黑色,其只有一个子节点x,操做在待删节点被删除以后,子节点替换其位置以后)
貌似有点难...你们要看进去思考才能理解,光看没用!
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable { //... static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> { //... // 当前节点即为要删除的节点,map为当前集合,tab为当前桶数组 final void removeTreeNode(HashMap<K,V> map, Node<K,V>[] tab, boolean movable) { int n; if (tab == null || (n = tab.length) == 0) return; // 定位待删节点的桶位下标index int index = (n - 1) & hash; TreeNode<K,V> first = (TreeNode<K,V>)tab[index], root = first, rl; TreeNode<K,V> succ = (TreeNode<K,V>)next, pred = prev; if (pred == null) // 若是当前节点是双向链表头节点/树根节点,则将链表第二元素置为桶位元素,即删除该节点 tab[index] = first = succ; else // 若是当前节点不是双向链表头节点,则将其后置节点赋给其前置节点做为后置节点,即删除该节点 pred.next = succ; if (succ != null) // 修改后置节点中prev指向新的前置元素节点 succ.prev = pred; if (first == null) return; if (root.parent != null) root = root.root(); if (root == null || root.right == null || (rl = root.left) == null || rl.left == null) { // 退化为链表机构 tab[index] = first.untreeify(map); // too small return; } //// 以前的操做是在双向链表中删除当前节点的痕迹,下面是在树结构中删除的操做 // p为待删节点(即当前节点),pl为p的左子节点,pr为p的右子节点, TreeNode<K,V> p = this, pl = left, pr = right, replacement; if (pl != null && pr != null) { // 当前节点同时拥有左右子节点的状况,sl表示当前节点的右子树的最左节点 // 要删除当前节点,须要找到与当前节点值最靠近的左右两侧的节点之一,这 // 里找的是右侧的,即找的是整个树中大于当前节点值的最小值节点,将找到 // 的节点与待删节点互换,互换以后再删除节点,若是原来的那个最左节点还 // 有右子节点,则将该右子节点替换其父节点(待删节点) TreeNode<K,V> s = pr, sl; while ((sl = s.left) != null) // find successor s = sl;// 找到右子树的最左节点 boolean c = s.red; s.red = p.red; p.red = c; // swap colors 首先互换颜色 TreeNode<K,V> sr = s.right;// s为最左节点,那么它不可能有左子节点,最多有右子节点 TreeNode<K,V> pp = p.parent; if (s == pr) { // p was s's direct parent // 若是找到的s即为待删节点的直接右子节点(说明s无左子节点),那么直接替换这两个节点 p.parent = s; s.right = p; } else { // 不然的状况,先找到s的父节点sp,将其设置为p的父节点, TreeNode<K,V> sp = s.parent; if ((p.parent = sp) != null) { if (s == sp.left) // 将p做为原来s的父节点的左子节点(即替换p和s的位置) sp.left = p; else // TODO 这里是什么意思呢?找的就是sp的最左节点,这里怎么跑到右节点上去了呢,虽然p是要删除的节点 sp.right = p; } // 把p的右子节点pr置为s的右子节点 if ((s.right = pr) != null) // 把s置为pr的父节点 pr.parent = s; } // 替换以后p是无左子节点的,(即原来的s是最左节点,无左子节点) p.left = null; // 把s的右子节点sr置为p的右子节点 if ((p.right = sr) != null) // 把sr的父节点设置为p sr.parent = p; if ((s.left = pl) != null) // 将p的左子节点置为s的左子节点 pl.parent = s; // 把p的父节点设置为s的父节点 if ((s.parent = pp) == null) // 若是p没有父节点,将s节点设置为根节点 root = s; // 不然若是p是其父节点pp的左子节点 else if (p == pp.left) // 如今将s设置为pp的左子节点 pp.left = s; else // 不然若是p是其父节点的右子节点,则将s设置为pp的右子节点 pp.right = s; if (sr != null) // 若是s存在右子节点,则将其置为replacement,如今和待删节点只有右子节点的状况同样 replacement = sr; else // 不然将p置为replacement,至此第一种状况替换结束,如今和待删节点没子节点的状况同样 replacement = p; } else if (pl != null) // 待删节点只有左子节点的状况,将其左子节点置为replacement replacement = pl; else if (pr != null) // 当前节点只有右子节点的状况,将其右子节点置为replacement replacement = pr; else // 待删节点没有子节点的状况,直接将其设置为replacement replacement = p; if (replacement != p) {// 若是待删节点有子节点replacement的状况 // 准备替换replacement节点和p节点 TreeNode<K,V> pp = replacement.parent = p.parent; if (pp == null) // 待删节点p为根节点的状况,将replacement设置为根节点便可 root = replacement; else if (p == pp.left) // p是做为其父节点pp的左子节点,则将replacement设置为pp的左子节点 pp.left = replacement; else // 不然p是做为其父节点pp的右子节点,则将replacement设置为pp的右子节点 pp.right = replacement; // 最后将p节点的全部关系置空 p.left = p.right = p.parent = null; } // 若是待删节点是红色节点则不影响平衡,无需执行树平衡操做,不然须要进行树平衡操做 TreeNode<K,V> r = p.red ? root : balanceDeletion(root, replacement); // 若是p节点没有任何子节点的状况 if (replacement == p) { // detach // 根据实际状况置空p节点的各属性 TreeNode<K,V> pp = p.parent; p.parent = null; if (pp != null) { if (p == pp.left) pp.left = null; else if (p == pp.right) pp.right = null; } } if (movable) // 若是可移动,那么将根节点设置为桶位节点 moveRootToFront(tab, r); } // 删除节点后的平衡操做,root为根节点,x为上面提到的replacement节点,该节点其实为替换p节点,为其子节点 static <K,V> TreeNode<K,V> balanceDeletion(TreeNode<K,V> root, TreeNode<K,V> x) { for (TreeNode<K,V> xp, xpl, xpr;;) { if (x == null || x == root) // 若是x不存在或者其原本就是根节点,字节返回根节点 return root; else if ((xp = x.parent) == null) { // 若是x节点不存在父节点,那么则其为根节点,可是还未设置为root节点,那么直接置黑色,并将其设置为root节点 x.red = false; return x; } else if (x.red) { // 若是x是红色节点,则将其置黑,由于若是x为红色,那么其父节点p必然为黑色, // 删掉以后,会致使黑色节点减小,正好x为红色拿来补充黑色节点,使黑色节点数不变, // 若是x是黑色,那么就会致使当前分支黑色节点减小,须要使用其余方法进行平衡 x.red = false; return root; } // 若是x为其父节点左子节点(删除后的结果) else if ((xpl = xp.left) == x) { // 若是x存在兄弟节点(其父节点的右子节点),且为红色,那么xp一定为黑色 // 那么就代表x节点分支的黑色节点少了一个,也就是其兄弟节点多一个(其余全部分支都多一个), if ((xpr = xp.right) != null && xpr.red) { // 那么将兄弟节点置黑,父节点置红,这是x分支仍是少一个黑节点,兄弟分支黑节点不变 xpr.red = false; xp.red = true; // 再而后执行xp节点的左旋,将其右子节点(即x的兄弟节点)甩上去, // xp做为其左子节点,如此一来将兄弟节点这一黑色几点变成两份之共享, // 无形之中使得x分支黑色节点加1,从而达到平衡 root = rotateLeft(root, xp); xpr = (xp = x.parent) == null ? null : xp.right; } if (xpr == null) x = xp;// 若是x没有兄弟节点,那么循环以x的父节点为基准再次进行平衡 else { // 不然,那就是x存在兄弟节点,且为黑色的状况,则其父节点颜色不明,x颜色不明 // sl为兄弟节点的左子节点,sr为兄弟节点右子节点 TreeNode<K,V> sl = xpr.left, sr = xpr.right; // 若是sr和sl中,所有为null,或者所有为黑色节点,或者有一个为黑色节点,另外一个是null if ((sr == null || !sr.red) && (sl == null || !sl.red)) { // 将兄弟节点置为红色 xpr.red = true; x = xp;// 为了循环 } // 不然,就是sl和sr全为红色或者一红一null,或者一红一黑 else { // 若是sr为null(则sl必为红色),或者sr为黑色(则sl必为红色) if (sr == null || !sr.red) { if (sl != null) // 将sl置为黑色 sl.red = false; // 兄弟节点置红 xpr.red = true; // 而后右旋兄弟节点,将其左子节点甩上去作本身的父节点,这时兄弟分支黑节点数量不变 root = rotateRight(root, xpr); xpr = (xp = x.parent) == null ? null : xp.right;// 更新xpr的指向,将其指向旋转以后新的兄弟节点,即原来的sl(黑色) } if (xpr != null) { // 将xp和xpr颜色弄一致,由于若是xp是黑色,左旋以后兄弟分支会少一个黑节点, // 这样xpr就会补充这个黑色,若是xp是红色,那么xpr也是红色,左旋以后xpr落座 // xp的位置,仍是原来的颜色,而左侧确多出了xp这个黑色节点。 xpr.red = (xp == null) ? false : xp.red; if ((sr = xpr.right) != null) // 将sr置黑即把原来的xpr置黑 sr.red = false; } if (xp != null) { // 将xp置黑 xp.red = false; // 而后在执行xp左旋,等于将sl甩到了xp的位置, // 并且这个sl必然为黑色,是为了补充x分支缺乏的那一个黑节点 root = rotateLeft(root, xp); } x = root; } } } // 不然若是x是其父节点的右子节点的话 else { // symmetric (对称的) if (xpl != null && xpl.red) { xpl.red = false; xp.red = true; root = rotateRight(root, xp); xpl = (xp = x.parent) == null ? null : xp.left; } if (xpl == null) x = xp; else { TreeNode<K,V> sl = xpl.left, sr = xpl.right; if ((sl == null || !sl.red) && (sr == null || !sr.red)) { xpl.red = true; x = xp; } else { if (sl == null || !sl.red) { if (sr != null) sr.red = false; xpl.red = true; root = rotateLeft(root, xpl); xpl = (xp = x.parent) == null ? null : xp.left; } if (xpl != null) { xpl.red = (xp == null) ? false : xp.red; if ((sl = xpl.left) != null) sl.red = false; } if (xp != null) { xp.red = false; root = rotateRight(root, xp); } x = root; } } } } } //... } //... }
HashMap中涉及到了数组操做,单向链表操做,双向链表操做,红黑树操做:
数组操做:
- HashMap中的数组指的就是桶数组,用于承载链表(的头结点)和红黑树(根节点)
- 数组的建立在第一次往集合中添加元素的时候进行,默认的数组长度为16,也能够手动指定,系统会自动计算一个大于等于给定容量的最小的2的次幂的容量值做为数组初始容量,数组的最大容量为不超过两倍的Integer的最大限值。数组还有一个负载因子,默认为0.75,这个值可算是一个时间空间的折中了,通常不会手动修改,但也能手动指定,若是设置过大,查询消耗增长,若是设置太小,空间消耗增长。
- 当向集合中添加一个新元素的时候,经过元素的key的hash算法结果来定位其在数组中的位置,这个hash算法要选择的足够好来使得元素可以尽可能平均的散布在数组的各个位置,而不是堆积在几处。
- 数组的容量是不可变的,因此一旦原数组容量受限,通常是建立新的数组来替代,这叫数组的扩容,扩容须要知足必定的条件,HashMap中有一个阈值用于做为扩容的条件,这个值是当前容量和负载因子的乘积,只要当前的集合的元素数量达到了阈值,就要执行扩容操做,固然还有一种状况也要扩容,那就是在单个数组位上的元素数量达到8个时,但数组容量未达到64个时,优先执行数组扩容。数组扩容后新数组为原数组的2倍容量。
单向链表操做:
- hashMap是依靠hash来存储元素的,hash存储老是难以免碰撞的出现,HashMap使用单向链表来保存发生碰撞的元素。新元素会保存到链表的尾部,若是新元素的key已经存在,那么将会是一个更新操做,不会有新元素增长。
- 数组扩容的时候须要进行元素的迁移,这里就是链表的迁移,链表迁移的时候会触发链表分拆,将一个完整链表分拆成为两个链表,咱们成为低位链表和高位链表,低位链表的数组位置同旧数组,而高位链表的数组位置为低位链表数组位+旧数组容量。可见经过链表分拆也是能够下降链表中元素数量的。
- 在Jdk1.8之前的版本中在高并发的状况下,HashMap数组扩容的时候可能会出现死循环,这是由于两个线程同时进行扩容和元素迁移致使出现了循环链表,Jdk1.8中已经修复这一Bug,采用的是将头插法改成尾插法,可是HashMap仍然是线程不安全的集合,多线程环境中最好使用ConcurrentHashMap。
双向链表操做:
- hashMap中存在双向链表,他是在单向链表元素达到8个,且数组容量达到64位以后执行树形化时转换的,也就是说,HashMap中的红黑树同时也是一个双向链表。这个双向链表的做用是在某些特殊状况下(树过小的时候),在将红黑树退化为单向链表结构时使用的。正由于如此,在红黑树的增删节点、平衡节点的时候还须要保证双向链表的结构。
红黑树操做:
- 这一部分能够算是hashMap中最最复杂难懂的东西了
- 红黑树的树形化操做
- 红黑树的增长元素操做
- 红黑树的增长元素平衡操做
- 红黑树的树分拆操做
- 红黑树的删除元素操做
- 红黑树的删除元素平衡操做
- 红黑树的退化单项链表操做,有两处退化,一处是在扩容时,树分拆以后,子树内元素容量少于6个时,执行退化操做,还有就是在移除树中元素以后,若是树结构知足某些条件则执行退化操做
- 其实平衡的目的就是为了恢复被添加和移除元素操做破坏的红黑树结构罢了,使用的手段无非变色和旋转。