初始化时,有以下几个核心点:
initialCapacity(初始容量) 默认值为 1<<4 (2^4 = 16) 后文会讲到为什么要是2的n次幂 最大值为1<<30loadFactor(负载因子) 默认值为 0.75 size(存放的k-v数量) 代表当前存放元素的数量,没存放一个元素进去size++threshold(阈值) threshold=initialCapacity*loadFactor 阈值决定扩容时机,当size>threshold就需要扩容了执行put操作底层先调用下面这个方法
// 当进行put操作后,底层先调用以下方法 public V put(K key, V value) { return putVal(hash(key), key, value, false, true); }这里可以看到,后续调用了putVal方法,这里注意其中的参数,hash(key):计算key的hash值,进入这个方法看看 为什么要与自身的高16位进行异或操作? 减少部分数据hash值高位变化低位不变,带来的hash冲突
//key如果不为空的话,将key的hash值与自身的hash值的高16位进行异或操作 static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }然后我们再看看真正put元素的代码:putVal() 桶:数组上的每一个位置叫做桶
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未初始化或者长度为0,进行扩容 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; // (n - 1) & hash 确定元素存放在哪个桶中,桶为空,新生成结点放入桶中(此时,这个结点是放在数组中) if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); // 桶中已经存在元素 else { Node<K,V> e; K k; // 比较桶中第一个元素(数组中的结点)的hash值相等,key相等 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) // 将第一个元素赋值给e,用e来记录 e = p; // hash值不相等,即key不相等; // 为红黑树结点; 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; } // 判断链表中结点的key值与插入的元素的key值是否相等 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) // 相等,跳出循环 break; // 用于遍历桶中的链表,与前面的e = p.next组合,可以遍历链表 p = e; } } // 表示在桶中找到key值、hash值与插入元素相等的结点 if (e != null) { // 记录e的value V oldValue = e.value; // onlyIfAbsent为false或者旧值为null if (!onlyIfAbsent || oldValue == null) //用新值替换旧值 e.value = value; // 访问后回调 afterNodeAccess(e); // 返回旧值 return oldValue; } } // 结构性修改 ++modCount; // 实际大小大于阈值则扩容 if (++size > threshold) resize(); // 插入后回调 afterNodeInsertion(evict); return null; }补充:桶内元素采用链表 尾插法(JDK1.8)进行追加。当链表长度增加到 binCount >= TREEIFY_THRESHOLD - 1 转为红黑树 TREEIFY_THRESHOLD 默认值为8
这里是如何找到存储在桶里的什么地方? (n - 1) & hash n代表桶的容量 hash代表需要存放的元素的key的hash值 以桶容量16,hash值为2154为例:
16-1=15 二进制表达:1111 【四位的二进制就可以表示出从0--15下标】2151&15 1111&1010 得到1010->9 即下标为9【由于&运算同1为1特殊性,前面部分都可以不用看,只看后四位就行了】后面扩容后桶的容量加倍从16变为32、64、128...减1之后的二进制表达都是1111 11... &运算的结果都是对应到每一个下标当++size> threshold (阈值) 后,会进行扩容操作 默认初始容量为2^4 之后扩容都是扩充为原本的2倍,这个地方设计得非常妙,因为扩容之后,其中的数据需要重新计算
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; } // 没超过最大值,就扩充为原来的2倍 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // double threshold } else if (oldThr > 0) // 初始容量设为阈值 newCap = oldThr; else { // 初始阈值为零表示使用默认值 newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } // 计算新的resize上限 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) { // 把每个bucket都移动到新的buckets中 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 { 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; } // 原索引+oldCap else { if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); // 原索引放到bucket里 if (loTail != null) { loTail.next = null; newTab[j] = loHead; } // 原索引+oldCap放到bucket里 if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } return newTab; }#.总结:就是达到扩容时机之后,新建一个容器,然后重新计算下标,将原本的元素移动过去
删除元素和增加元素有一部分是共通的,就是根据hash值计算下标。找到指定元素之后将其删除
根据key找到元素,然后修改value