2020年12月7日
1.总的来说,今天还行,除了早上多睡了六七分钟,没能8点准时起,三四节课看源码非常困,下午喝过咖啡就没那么困了,昨天回去洗漱完11.40太困了没背单词
2.今天多少做了点事,也坚持没玩手机,有空就投一下简历,上课时背了单词,背完单词后去查了下,发现要看源码分析的文章最好还是去github,牛客的太多了,也没个按热度分类什么的,大部分时间都在看HashMap的源码,78节要上课就看了计算机网络,学了TCP和UDP特点概述/区别、TCP首部、三次握手、晚上做了安硕的笔试
3.趁着还记得些东西,我把我学到的HashMap知识点记录下来:
基本内容
- HashMap在元素均匀分布的情况下,可以实现O(1)的存取
- 扩容会影响性能,所以最好预设合适的容量
考虑到以前基本什么都不知道就不再乱写了
今天学到的内容:
- 初始化HashMap时可以预设容量也可以不预设
- 如果预设容量,最终会把容量设为大于等于你给定的容量的2的n次方
- 如果不预设或者预设不合法,会给你一个默认初始容量16
- 源码中有这些常量:默认初始容量16、最大容量2^30、扩容因子0.75、链表树化阈值8、树链表化阈值6、树化的最小容量64。这些值,还有之前限定的容量只能为2的n次方都是为了优化性能。具体细节我也迷糊,写不写看情况
- map是数组+链表/红黑树的结构,其中数组是Node<K,V>[ ] table的形式,这种使用方式很齐发我,比如我们可以这样:
-
import java.util.*; public class Test { public static void main(String[] args) { LinkedList[] lists; Deque<String>[] queues; queues=new LinkedList[5]; queues[0]=new LinkedList<String>(); queues[0].add("abc"); lists=new LinkedList[5]; } }
-
size(当前容量)>table(数组)的大小*扩容因子=扩容阈值。并不是满了才扩容
-
到了最大容量2^30后就没办法扩容了
-
hash=高16位异或低16位,具体算法:
-
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
-
通过(n - 1) & hash算出键对应的索引,样例如下:
-
if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null);
-
从这里开始讲put操作
-
将数组长度保存在n中,如果数组没有初始化就resize()
-
如果算出来的桶(通过前面说到的算法算出的索引称为桶,源码中使用bucket命名,我们以后都说桶)为空,就把键值对转成结点添加到这里
-
否则如果不为空,第一个元素的key与我们要存的key相同,就覆盖。这里同时对比了hash和key是因为hash相同key不一定相同。但我不知道为什么不直接对比key,又为什么还要用key.equals(要存的key),代码如下
-
// 两个节点相等,则覆盖 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p;
-
如果也不相同,判断这个位置是不是一个红黑树,如果是就用插入红黑树的方法插入键值对
-
如果不是就遍历链表,如果遇到相同的元素就结束遍历,否则遍历到链表尾将键值对添加到链表尾
-
如果提前退出,说明在链表中找到了key相同的结点,用键值对生成新节点并替换该结点,并返回旧结点的值
-
modCount++,我猜测modCount表示数组中有效桶的个数,因为不涉及到增加新桶的操作比如把元素插入链表、插入红黑树都直接return了,执行不到该参数自加,其他的操作则会执行到这一步
-
当modCount超过扩容阈值就扩容,别人写的带注释的源码如下:
-
/** * Implements Map.put and related methods * * @param hash : key 的 hash 值 * @param key : key * @param value :value * @param onlyIfAbsent:为true 时表示,当节点存在时不覆盖 * @param evict : false 表示有 构造函数调用的方法 * @return value : 返回之前的值,空时返回 null。 */ final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; // 当 table 为空时惊醒扩容,n表示容量 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; // 定位 table数组中节点的位置,后面分析。 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; // 如果是树节点,此时链表长度大于8, 转为红黑树 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; }
-
下面将HashMap的扩容
-
通过对容量的把控:改变给定的容量、提供默认初始容量、扩容机制把容量限制在2的n次方
-
因为索引的计算依赖容量,扩容后容量变了索引自然也要变
-
扩容时,对于只有一个元素的旧桶重新计算索引
-
对于红黑树,这里只写了树分裂,大概机制会更复杂,但原理应该与对链表的处理相似,我不懂红黑树,也不觉得现在需要研究这么深,时间紧迫就不学了
-
因为前面的处理,主要是对容量的控制,原来的hash和新hash有一定规律:
-
之前hash的结果相同,现在不同,只有两种可能。一种是该key的新hash等于oldCap,我们把这种结果存到一个链表,因为低位都是零我们也称这种链表为低位链表,由于我们只需要操作首位结点,这里也只设置了低位头尾结点
-
另一种可能是新hash与老hash相同,因为高位为零(不为零的位,说明不够高...它就是这么起的名我也不懂,怎么好理解怎么来),我们称他的结点为高位头尾结点
-
通过将老hash&oldCap可以区分这两种结点,遍历链表把一个链表转成两个。头节点不懂,有新节点就插尾。最后把头节点存在新索引(老位置或者老位置+oldCap)。虽然我感觉这么操作复杂度还是O(n),但多少还是比一个一个hash算快一点把。。。但应该没啥质变,最多是少遍历次链表,可是链表的长度不会超过8。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. * 初始化或者扩容为 2 倍大小。由于其始终为2 的 n 次方,所以计算的下标或者相等, 或者偏移 2的 n 次方。 * @return the table */ final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; // 旧数组 int oldCap = (oldTab == null) ? 0 : oldTab.length; //旧 table 的容量 int oldThr = threshold; //旧 table 的阈值 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 } // 还未初始化,为0 else if (oldThr > 0) // initial capacity was placed in threshold newCap = oldThr; // 这里解释了构造函数为什么将 tablesizefor 赋值 threshold。 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; // 非初始化,旧 table 有数据 if (oldTab != null) { // 移动到新 table 里面 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, 确定新 table 中下标的位置 // 后续介绍 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; }
-
get操作基本就是算索引,查链表或者查树
23.28了,我该洗洗睡了