第一个构造器有两个参数,一个是初始容量,一个是负载因子 第二个构造器只有一个初始容量的参数 第三个不需要参数,全部使用默认的
看完构造器再来看一下常用的添加,查询,删除操作
第一步调用putVal(),里面hash(key)比较重要
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }这里主要用了一个异或运算,将key的hashCode值的高16位和低16位进行异或,目的就是为了解决Hash冲突,因为数组的默认容量为16,转换为二进制是10000,完整的hash是32位,这里只有五位,使用高16位和低16位异或一下就能充分把32位利用起来了,而且这个异或运算在效率上比起原来的取模运算效率更快
/** * Implements Map.put and related methods. * * @param hash hash for key * @param key the key * @param value the value to put * @param onlyIfAbsent if true, don't change existing value * @param evict if false, the table is in creation mode. * @return previous value, or null if none */ 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 1st treeifyBin(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 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; }初看一下这代码还挺吓人的,不过仔细一看,不就是一堆if,else吗,容我慢慢分析
1:五个变量
int hash //哈希值,用于计算下标 K key //键值 V value //需要存储的Value值 boolean onlyIfAbsent//是否替换原来的值,false为替换,true为不替换 boolean evict //是否为创建模式2:初始化桶
Node<K,V>[] tab; Node<K,V> p; int n, i; if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length;这里通过判断tab来决定是否进行初始化,而tab又是table赋值而来,通过前面的构造器我们并没有看见有对table初始化,所以这里会进入resize()方法,下面看一下他是怎么初始化的,在初始化的时候又做了些什么事情?
/** * Initializes or doubles table size. If null, allocates in * accord with initial capacity target held in field threshold. * Otherwise, because we are using power-of-two expansion, the * elements from each bin must either stay at same index, or move * with a power of two offset in the new table. * * @return the table */ 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) 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; 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; 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; }本以为只是一个初始化,可看这代码不像啊,这也太复杂了吧,看来不仅仅是初始化这么简单啊,是的,其实这个方法的作用是扩容,初始化也当作扩容的一部分,别看他这么复杂,其实我们这次进来执行的代码就几句
newCap = DEFAULT_INITIAL_CAPACITY;//桶的初始容量 newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//扩容阀值,达到这个值就需要扩大桶的容量了,默认为0.75 * 16 = 12下面才正式开始添加的步骤,之前都是准备 通过上面的数据结构我们可以看出有三种结构,分别是数组,链表,红黑树,在添加时我们把这三种结构分为了三种情况 第一种:数组
if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null);第一种最简单,通过hash值计算出下标,然后判断桶中对应下标是否有值,当为null时则添加进去 第二种,红黑树
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);首先用桶下标对应值的key和现在需要添加的新key用equals进行比较,如果相同,则覆盖之前值, 判断桶下标存的值是否为红黑树,如果是则进行红黑树的节点添加 第三种:链表
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)))) break; p = e; } }循环链表的元素,通过next取得下一个元素,如果下一个元素为null,则把当前要添加的元素添加进去,添加过后判断链表元素是否达到8个,如果达到则把链表转换为红黑树,这里还有一个判断是,如果链表里面的key和新添加的key相同则直接退出,这里也是用equals做比较
最后一步,采用尾插法插入节点保存数据,并返回老数据,最后判断添加过后的容量大小是否达到扩容的阀值,如果达到则进行扩容
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);再来看一下扩容操作
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; } //如果扩容后小于最大值,并且大于默认值16 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // 扩容为原来的两倍 } 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"}) //创建一个新hash桶,将原来的hash桶传进去 Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab; if (oldTab != null) { //用循环把原来hash桶的数据,插入到新hash桶里面去 for (int j = 0; j < oldCap; ++j) { Node<K,V> e; if ((e = oldTab[j]) != null) { oldTab[j] = null; //原来hash桶数据只有一个元素,不是链表时 if (e.next == null) newTab[e.hash & (newCap - 1)] = e; //原来hash桶数据是一个红黑树 else if (e instanceof TreeNode) ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); //原来hash桶数据一个链表 else { // preserve order //把链表的值重新hash分布一遍 Node<K,V> loHead = null, loTail = null;//重新分布的索引为原索引 Node<K,V> hiHead = null, hiTail = null;//重新分布的索引为原索引+原hash桶容量大小值 Node<K,V> next; //这里又使用一个循环来 do { next = e.next; //如果节点的hash于原来的桶容量大小等于0 if ((e.hash & oldCap) == 0) { if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } //如果e的hash值与老表的容量进行与运算为1, //则扩容后的索引位置为:老表的索引位置+oldCap else { if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); //如果loTail不为空(说明老表的数据有分布到新表上“原索引位置”的节点), //则将最后一个节点的next设为空,并将新表上索引位置为“原索引位置”的节点设置为对应的头节点 if (loTail != null) { loTail.next = null; newTab[j] = loHead; } //如果hiTail不为空(说明老表的数据有分布到新表上“原索引+oldCap位置”的节点), //则将最后 一个节点的next设为空,并将新表上索引位置为“原索引+oldCap”的节点设置为对应的头节点 if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } return newTab; }特别需要注意,在扩容的时候又一个判断来确定扩容以后元素的存放位置 if ((e.hash & oldCap) == 0) . 为什么要与原来的数组长度来做与运算呢 ?这其实也是为了让元素更加均匀的分布,就是用元素的 hash 值 与老的数组长度的与运算,当不等于0的时候就进行将元素迁移到 +oldCap 的位置上。这个设计确实非常的巧妙,既省去了重新计算hash值的时间,而且同时,由于新增的1bit是0还是1可以认为是随机的,因此resize的过程,均匀的把之前的冲突的节点分散到新的bucket了。这一块就是JDK1.8新增的优化点。
在看完添加过程之后再来看获取,这时就很舒服了,上代码
public V get(Object key) { Node<K,V> e; return (e = getNode(hash(key), key)) == null ? null : e.value; }和添加时一样,先把key使用hash算法hash一下,存的时候都是根据这个值存的,取的时候那肯定也需要这个值 下面看一下获取的主要过程
final Node<K,V> getNode(int hash, Object key) { Node<K,V>[] tab; Node<K,V> first, e; int n; K k; //table不为null并且已经初始化了 if ((tab = table) != null && (n = tab.length) > 0 && //根据hash算出下标,并且桶下标所对应的值不为null (first = tab[(n - 1) & hash]) != null) { //如果第一个节点的hash和要取出的hash值相同,则返回这个值 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); //不为红黑树时,就为链表,使用循环判断链表元素的key和要取出的key是否相同,使用equals来进行比较 do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } return null; }判断表或key是否是null,如果是直接返回null 判断索引处第一个key与传入key是否相等,如果相等直接返回 如果不相等,判断链表是否是红黑二叉树,如果是,直接从树中取值 如果不是树,就遍历链表查找
老规矩,还是先计算一下hash值,然后交给另外一个方法
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 && //再看一下桶里面有木有这个经过hash算出来的下标所对应的值 (p = tab[index = (n - 1) & hash]) != null) { Node<K,V> node = null, e; K k; V v; //如果第一个节点的hash和要取出的hash值相同 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 { do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) { node = e; break; } p = e; } while ((e = e.next) != null); } } //节点不为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; }删除操作,先计算指定key的hash值,然后计算出table中的存储位置,判断当前位置是否Entry实体存在,如果没有直接返回,若当前位置有Entry实体存在,则判断节点的状态,如果为红黑树,则使用红黑树的方法进行删除,如果为普通元素则把下一个元素赋值给当前元素,如果为链表则把他的赋值给下一个,使要删除的这个元素失去引用
参考文章:
https://www.cnblogs.com/xiaoxi/p/7233201.htmlhttps://www.cnblogs.com/wuzhenzhao/p/13199350.htmlhttps://blog.csdn.net/qq_41345773/article/details/92066554