看图学源码之— HashMap源码分析

简介:

  • 是基于 哈希表 实现的,存放 k-v 键值对,非同步的方式(未加 synchronized )非线程安全的,hashmap 无序的
  • 数据结构: 数组 + 链表 ====> 数组 +链表 + 红黑树「链表 和 链表 + 红黑树 都是为了解决 哈希冲突
  • 哈希冲突: 两个对象调用的hashCode方法计算的哈希码值一致导致计算的数组索引值相同 ,区别是: 链表 + 红黑树 在达到 : 阈值> 8 && 数组长度 > 64 才会进行转为红黑树———>原因: 虽然红黑树会让结构更加复杂,但是这时查询效率更高,要是数组长度比较少的话,应该尽量避免红黑树,不然的话 红黑树需要进行左旋,右旋,变色这些操作来保持平衡 并且 数组的长度 < 64 的时候 搜索的时间是相对来说比较快的」

数据结构:

存储说明:

  1. HashMap<String,Integer>map = new HashMap<>();创建HashMap对象后,

    • 在jdk8前 : 底层创建了长度是16的一维数组 Entry[]table

    • 在jdk8以后并没有 在创建集合对象时创建数组,而是在首次调用put()方法时,底层创建长度为16的Node[] table数组

  2. 假设向哈希表中存储数据 key1-value1,随后 根据 key1 调用String类中的hashCode()方法计算key1的哈希码值,此哈希码值经过某种算法计算以后,得到在Node 数组中的存放的位置,如果比位置上的数据为空,此时的 key1-value1添动加成功。例如计算的索引是2

    • 面试题:HashMap中hash函数是怎么实现的?还有哪些hash函数的实现方式?
      • 对key的hashCode做hash操作:结合数组的长度进行 无符号右移(>>> ) 、按位异或 (^)、按位与(& ) 运算。
      • 还有平方取中法,除留余数法,伪随机数法。其余三种方式效率都比较低。而无符号右移16位做异或运算效率是比较高的
  3. 假设向哈希表中存储 key2-value2,根据键的key2计算出的 哈希值,并结合数组长度计算出的索引;那么一旦此时的索引位置有数据,就会比较hash 值,要是hash 值不一样,那么就要在这个索引空间上新创建一个节点,然后 将key2-value2 存放到这个节点中。

  4. 假设向哈希表中存储 key1-value3,根据键的key1计算出的 哈希值(此时的索引位置和之前肯定是一样的);

    • 此时就会比较后添加的数据的key和已经存在的哈希值是否相同,

    • 如果相同,继续比较:调用后添加的key1的所属类的equals()比较内容是否相等。

      • 如果equals()返回false,此时继续添加。
      • 如果equals()返回true:则后添加的 value3 替换之前的 value1
    • 面试题:当两个对象的hashCode相等时会怎么样?

      • 会产生哈希碰撞
        • 进一步比较通过equals比较key 内容是否相同
          • 相同:则新的value覆盖之前的value
          • 不相同:否则遍历链接到链表后面,链表长度 > 8 且 数组长度 > 64 ,就转为红黑树存储

红黑树结构复杂,为什么使用红黑树?为什么阈值规定为 8?

  1. 哈希函数取得再好,也很难达到元素百分百均匀分布。当 HashMap 中有大量的元素都存放到同一个桶中时,这个桶下有一条长长的链表,这个时候 HashMap 就相当于一个单链表,假如单链表有 n 个元素,遍历的时间复杂度就是 O(n),完全失去了它的优势。针对这种情况,JDK 1.8 中引入了 红黑树(查找时间复杂度为 O(logn))来优化这个问题,当链表长度很小的时候,即使遍历,速度也非常快,但是当链表长度不断变长,肯定会对查询性能有一定的影响,所以才需要转成树。
  2. 见源码
  1. size表示 HashMap中K-V的实时数量 , 注意这个不等于数组的长度 。

  2. threshold( 临界值) =capacity(容量) * loadFactor( 加载因子 )。这个值是当前已占用数组长度的最大值。size超过这个临界值就重新resize(扩容),扩容后的 HashMap 容量是之前容量的两倍 。

源码分析

声明变量

// 默认初始容量 - 必须是 2 的幂
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 16   创建的时候无参默认16// 默认 集合的最大容量。必须是 2 的幂 <= 2 的 30 次方。
static final int MAXIMUM_CAPACITY = 1 << 30;// 默认 使用的负载因子。static final float DEFAULT_LOAD_FACTOR = 0.75f;// 树化的阈值
static final int TREEIFY_THRESHOLD = 8;// 调整大小扩容时 取消树化,转为链表 的阈值
static final int UNTREEIFY_THRESHOLD = 6;// 可以被 树化的最小 数组 的容量   为了避免进行扩容、树形化选择的冲突,这个值不能小于 4 * TREEIFY_THRESHOLD (8)
static final int MIN_TREEIFY_CAPACITY = 64;// 存储元素的数组   初始化的大小一般是 2的 n 次幂 
// jdk8之前数组类型是Entry<K,V>类型。从jdk1.8之后是Node<K,V>类型。只是换了个名字,都实现了一样的接口:Map.Entry<K,V>。负责存储键值对数据的。
transient Node<K,V>[] table;//存放具体的元素的集合 和 缓存
transient Set<Map.Entry<K,V>> entrySet;//table 数组的存储个数
transient int size;//记录 HashMap的修改次数,每次扩容和更改map结构的计数器
transient int modCount;  //临界值 当实际大小( 数组长度(容量) *  负载因子)超过临界值时,会进行扩容;可以 衡量数组是否需要扩增的一个标准。 扩容后的 HashMap 容量是之前容量的两倍.
int threshold;//加载因子 
// 计算HashMap的实时加载因子的方法为:size/capacity,而不是占用桶的数量去除以capacity。capacity 是桶的数量,也就是 table 的长度length。
final float loadFactor;

链表节点

static class Node<K,V> implements Map.Entry<K,V> {final int hash;final K key;V value;Node<K,V> next;Node(int hash, K key, V value, Node<K,V> next) {this.hash = hash;this.key = key;this.value = value;this.next = next;}public final K getKey()        { return key; }public final V getValue()      { return value; }public final String toString() { return key + "=" + value; }public final int hashCode() {return Objects.hashCode(key) ^ Objects.hashCode(value);}public final V setValue(V newValue) {V oldValue = value;value = newValue;return oldValue;}public final boolean equals(Object o) {if (o == this)return true;if (o instanceof Map.Entry) {Map.Entry<?,?> e = (Map.Entry<?,?>)o;if (Objects.equals(key, e.getKey()) &&Objects.equals(value, e.getValue()))return true;}return false;}
}

树节点

static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {TreeNode<K,V> parent;  // red-black tree linksTreeNode<K,V> left;TreeNode<K,V> right;TreeNode<K,V> prev;    // needed to unlink next upon deletionboolean red;TreeNode(int hash, K key, V val, Node<K,V> next) {super(hash, key, val, next);}/*** 返回包含此节点的树的根。*/final TreeNode<K,V> root() {for (TreeNode<K,V> r = this, p;;) {if ((p = r.parent) == null)return r;r = p;}}/*** 确保给定的根是其 bin 的第一个节点。*/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];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);}}/*** 使用给定的哈希值和密钥查找从根 p 开始的节点。 kc 参数在第一次使用比较键时缓存可比较的ClassFor(key)。*/final TreeNode<K,V> find(int h, Object k, Class<?> kc) {TreeNode<K,V> p = this;do {int ph, dir; K pk;TreeNode<K,V> pl = p.left, pr = p.right, q;if ((ph = p.hash) > h)p = pl;else if (ph < h)p = pr;else if ((pk = p.key) == k || (k != null && k.equals(pk)))return p;else if (pl == null)p = pr;else if (pr == null)p = pl;else if ((kc != null ||(kc = comparableClassFor(k)) != null) &&(dir = compareComparables(kc, k, pk)) != 0)p = (dir < 0) ? pl : pr;else if ((q = pr.find(h, k, kc)) != null)return q;elsep = pl;} while (p != null);return null;}/*** 调用 find 查找根节点。*/final TreeNode<K,V> getTreeNode(int h, Object k) {return ((parent != null) ? root() : this).find(h, k, null);}/*** 当 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;}/*** 形成从此节点链接的节点树。*/final void treeify(Node<K,V>[] tab) {TreeNode<K,V> root = null;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) {x.parent = null;x.red = false;root = x;}else {K k = x.key;int h = x.hash;Class<?> kc = null;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);TreeNode<K,V> xp = p;if ((p = (dir <= 0) ? p.left : p.right) == null) {x.parent = xp;if (dir <= 0)xp.left = x;elsexp.right = x;root = balanceInsertion(root, x);break;}}}}moveRootToFront(tab, root);}/*** 返回替换从此节点链接的非 TreeNode 的列表。*/final Node<K,V> untreeify(HashMap<K,V> map) {Node<K,V> hd = null, tl = null;for (Node<K,V> q = this; q != null; q = q.next) {Node<K,V> p = map.replacementNode(q, null);if (tl == null)hd = p;elsetl.next = p;tl = p;}return hd;}/***putVal 的树版本。*/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;;) {int dir, ph; K pk;if ((ph = p.hash) > h)dir = -1;else if (ph < h)dir = 1;else if ((pk = p.key) == k || (k != null && k.equals(pk)))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))return q;}dir = tieBreakOrder(k, pk);}TreeNode<K,V> xp = p;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)xp.left = x;elsexp.right = x;xp.next = x;x.parent = x.prev = xp;if (xpn != null)((TreeNode<K,V>)xpn).prev = x;moveRootToFront(tab, balanceInsertion(root, x));return null;}}}/**删除此调用之前必须存在的给定节点。这比典型的红黑删除代码更混乱,因为我们无法将内部节点的内容与叶子后继者交换,该叶子后继者由在遍历期间可独立访问的“下一个”指针固定。因此,我们交换树的链接。如果当前树的节点太少,则该 bin 会转换回普通 bin。 (测试会在 2 到 6 个节点之间触发,具体取决于树结构)。*/final void removeTreeNode(HashMap<K,V> map, Node<K,V>[] tab,boolean movable) {int n;if (tab == null || (n = tab.length) == 0)return;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;elsepred.next = succ;if (succ != null)succ.prev = pred;if (first == null)return;if (root.parent != null)root = root.root();if (root == null|| (movable&& (root.right == null|| (rl = root.left) == null|| rl.left == null))) {tab[index] = first.untreeify(map);  // too smallreturn;}TreeNode<K,V> p = this, pl = left, pr = right, replacement;if (pl != null && pr != null) {TreeNode<K,V> s = pr, sl;while ((sl = s.left) != null) // find successors = sl;boolean c = s.red; s.red = p.red; p.red = c; // swap colorsTreeNode<K,V> sr = s.right;TreeNode<K,V> pp = p.parent;if (s == pr) { // p was s's direct parentp.parent = s;s.right = p;}else {TreeNode<K,V> sp = s.parent;if ((p.parent = sp) != null) {if (s == sp.left)sp.left = p;elsesp.right = p;}if ((s.right = pr) != null)pr.parent = s;}p.left = null;if ((p.right = sr) != null)sr.parent = p;if ((s.left = pl) != null)pl.parent = s;if ((s.parent = pp) == null)root = s;else if (p == pp.left)pp.left = s;elsepp.right = s;if (sr != null)replacement = sr;elsereplacement = p;}else if (pl != null)replacement = pl;else if (pr != null)replacement = pr;elsereplacement = p;if (replacement != p) {TreeNode<K,V> pp = replacement.parent = p.parent;if (pp == null)root = replacement;else if (p == pp.left)pp.left = replacement;elsepp.right = replacement;p.left = p.right = p.parent = null;}TreeNode<K,V> r = p.red ? root : balanceDeletion(root, replacement);if (replacement == p) {  // detachTreeNode<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);}/*** 调整数组大小的时候调用 ,进行   取消树化 或者 增高 或者 减低 树的高度*/final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {TreeNode<K,V> b = this;// Relink into lo and hi lists, preserving orderTreeNode<K,V> loHead = null, loTail = null;TreeNode<K,V> hiHead = null, hiTail = null;int lc = 0, hc = 0;for (TreeNode<K,V> e = b, next; e != null; e = next) {next = (TreeNode<K,V>)e.next;e.next = null;if ((e.hash & bit) == 0) {if ((e.prev = loTail) == null)loHead = e;elseloTail.next = e;loTail = e;++lc;}else {if ((e.prev = hiTail) == null)hiHead = e;elsehiTail.next = e;hiTail = e;++hc;}}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);}}if (hiHead != null) {if (hc <= UNTREEIFY_THRESHOLD)tab[index + bit] = hiHead.untreeify(map);else {tab[index + bit] = hiHead;if (loHead != null)hiHead.treeify(tab);}}}/* ------------------------------------------------------------ */// 红黑树方法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;elsepp.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;elsepp.left = l;l.right = p;p.parent = l;}return 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) {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);}}}}}}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)return root;else if ((xp = x.parent) == null) {x.red = false;return x;}else if (x.red) {x.red = false;return root;}else if ((xpl = xp.left) == x) {if ((xpr = xp.right) != null && xpr.red) {xpr.red = false;xp.red = true;root = rotateLeft(root, xp);xpr = (xp = x.parent) == null ? null : xp.right;}if (xpr == null)x = xp;else {TreeNode<K,V> sl = xpr.left, sr = xpr.right;if ((sr == null || !sr.red) &&(sl == null || !sl.red)) {xpr.red = true;x = xp;}else {if (sr == null || !sr.red) {if (sl != null)sl.red = false;xpr.red = true;root = rotateRight(root, xpr);xpr = (xp = x.parent) == null ?null : xp.right;}if (xpr != null) {xpr.red = (xp == null) ? false : xp.red;if ((sr = xpr.right) != null)sr.red = false;}if (xp != null) {xp.red = false;root = rotateLeft(root, xp);}x = root;}}}else { // symmetricif (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;}}}}}/*** Recursive invariant check*/static <K,V> boolean checkInvariants(TreeNode<K,V> t) {TreeNode<K,V> tp = t.parent, tl = t.left, tr = t.right,tb = t.prev, tn = (TreeNode<K,V>)t.next;if (tb != null && tb.next != t)return false;if (tn != null && tn.prev != t)return false;if (tp != null && t != tp.left && t != tp.right)return false;if (tl != null && (tl.parent != t || tl.hash > t.hash))return false;if (tr != null && (tr.parent != t || tr.hash < t.hash))return false;if (t.red && tl != null && tl.red && tr != null && tr.red)return false;if (tl != null && !checkInvariants(tl))return false;if (tr != null && !checkInvariants(tr))return false;return true;}
}

构造函数

/*** 构造一个具有指定初始容量和负载因子的空HashMap 。*/
public HashMap(int initialCapacity, float loadFactor) {if (initialCapacity < 0)throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);if (initialCapacity > MAXIMUM_CAPACITY)initialCapacity = MAXIMUM_CAPACITY;// loadFactor <= 0 || loadFactor 是否是一个非 Float型的数值if (loadFactor <= 0 || Float.isNaN(loadFactor))throw new IllegalArgumentException("Illegal load factor: " +loadFactor);this.loadFactor = loadFactor;this.threshold = tableSizeFor(initialCapacity);/**疑惑与解答: 疑惑:为什么不这么写:	this.threshold = tableSizeFor(initialCapacity) * this.loadFactor;解答:在jdk8以后的构造方法中,并没有对table这个成员变量进行初始化,table的初始化被推迟到了put方法中,在put方法中会对threshold重新计算,具体见put()*/
}
// 返回给定目标容量的两倍大小的幂。static final int tableSizeFor(int cap) {int n = cap - 1;n |= n >>> 1;n |= n >>> 2;n |= n >>> 4;n |= n >>> 8;n |= n >>> 16;return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;}/*** 构造一个具有指定初始容量和默认负载因子 (0.75) 的空HashMap */
public HashMap(int initialCapacity) {this(initialCapacity, DEFAULT_LOAD_FACTOR);
}/*** 使用默认初始容量 (16) 和默认负载因子 (0.75) 构造一个空HashMap 。*/
public HashMap() {this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}/*** 构造一个与指定Map具有相同映射的新HashMap 。 HashMap是使用默认负载因子 (0.75) 和足以容纳指定Map中的映射的初始容量创建的。*/
public HashMap(Map<? extends K, ? extends V> m) {this.loadFactor = DEFAULT_LOAD_FACTOR;putMapEntries(m, false);
}// 实现 Map.putAll 和 Map 构造函数。
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {int s = m.size();if (s > 0) {if (table == null) { // pre-size  数组值是null  初始化大小float ft = ((float)s / loadFactor) + 1.0F;    int t = ((ft < (float)MAXIMUM_CAPACITY) ? (int)ft : MAXIMUM_CAPACITY);if (t > threshold)threshold = tableSizeFor(t);//取出 >t 的最小 2的n次幂}else if (s > threshold)resize();  //扩容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); // 遍历调用  putVal 放值}}}
  1. tableSizeFor()方法:

    可以看到,当在实例化HashMap实例时,如果给定了initialCapacity(假设是10),由于HashMap的capacity必须都是2的幂,因此这个方法用于找到大于等于initialCapacity(假设是10)的最小的2的幂(initialCapacity如果就是2的幂,则返回的还是这个数)。
    下面分析这个算法:
    1) 、首先,为什么要对cap做减1操作。int n = cap - 1;
    这是为了防止,cap已经是2的幂。如果cap已经是2的幂, 又没有执行这个减1操作,则执行完后面的几条无符号右移操作之后,返回的capacity将是这个cap的2倍。

    2)、如果n这时为0了(经过了cap-1之后),则经过后面的几次无符号右移依然是0,最后返回的capacity是1(最后有个n+1的操作)。这里只讨论n不等于0的情况。

    3)、注意:|(按位或运算):运算规则:相同的二进制数位上,都是0的时候,结果为0,否则为1。

计算用例:int n = cap - 1;//cap=10  n=9
n |= n >>> 1;1次右移00000000 00000000 00000000 00001001 //9
|	00000000 00000000 00000000 00000100 //9右移之后变为4
-------------------------------------------------00000000 00000000 00000000 00001101 //按位异或之后是132次右移
n |= n >>> 2;//n通过第一次右移变为了:n=1300000000 00000000 00000000 00001101  // 13
|00000000 00000000 00000000 00000011  //13右移之后变为3
-------------------------------------------------00000000 00000000 00000000 00001111 //按位异或之后是153次右移
n |= n >>> 4;//n通过第一、二次右移变为了:n=1500000000 00000000 00000000 00001111  // 15
|00000000 00000000 00000000 00000000  //15右移之后变为0
-------------------------------------------------00000000 00000000 00000000 00001111 //按位异或之后是1545次右移  还是 15 把已经有的高位中的连续的41,右移4位,再做或操作,这样n的二进制表示的高位中正常会有8个连续的1。如00001111 1111xxxxxx 。
以此类推
注意,容量最大也就是32 位的正数,因此最后n |= n >>> 16; ,最多也就321(但是这已经是负数了。在执行tableSizeFor之前,对initialCapacity做了判断,如果大于MAXIMUM_CAPACITY(2^30),则取MAXIMUM_CAPACITY。如果等于MAXIMUM_CAPACITY(2^30),会执行移位操作。所以这里面的移位操作之后,最大301,不会大于等于MAXIMUM_CAPACITY301,加1之后得2^30) 。
  1. float ft = ((float)s / loadFactor) + 1.0F;此处 为什么 要加1.0F

    s/loadFactor的结果是小数,加1.0F与(int)ft相当于是对小数做一个向上取整以尽可能的保证更大容量,更大的容量能够减少resize的调用次数。所以 + 1.0F是为了获取更大的容量,减少扩容次数,提升性能!

    例如:原来集合的元素个数是6个,那么6/0.75是8,是2的n次幂,那么新的数组大小就是8了。然后原来数组的数据就会存储到长度是8的新的数组中了,这样会导致在存储元素的时候,容量不够,还得继续扩容,那么性能降低了,而如果+1呢,数组长度直接变为16了,这样可以减少数组的扩容。

put

1)先通过hash值计算出key映射到哪个桶;

2)如果桶上没有碰撞冲突,则直接插入;

3)如果出现碰撞冲突了,则需要处理冲突:

​ a. 如果该桶使用红黑树处理冲突,则调用红黑树的方法插入数据;

​ b.否则采用传统的链式方法插入。如果链的长度达到临界值,则把链转变为红黑树;

4)如果桶中存在重复的键,则为该键替换新值value;

5)如果size大于阈值threshold,则进行扩容;

image-20231206231940505

// 将指定值与此映射中的指定键相关联。如果映射之前包含键的映射,则旧值将被替换。public V put(K key, V value) {return putVal(hash(key), key, value, false, true);}//计算 key.hashCode() 并将散列的高位扩展到低位(异或)。static final int hash(Object key) {int h;/*1)如果key等于null:     可以看到当key等于null的时候也是有哈希值的,返回的是0.2)如果key不等于null:  首先计算出key的hashCode赋值给h,然后与h无符号右移16位后的二进制进行按位异或得到最后的hash值*/return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);  //所以可以存 null/*(h = key.hashCode()) ^ (h >>> 16);计算规则就是:1.key.hashCode();返回散列值也就是hashcode。假设随便生成的一个值。2.n表示数组初始化的长度是163.&(按位与运算):运算规则:相同的二进制数位上,都是1的时候,结果为1,否则为零。4.^(按位异或运算):运算规则:相同的二进制数位上,数字相同,结果为0,不同为1。*/}
//实现Map.put和相关方法。
/*
主要参数:- hash key的hash值- key 原始Key- value 要存放的值- onlyIfAbsent 如果true代表不更改现有的值- evict 如果为false表示table为创建状态
*/final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {Node<K,V>[] tab; Node<K,V> p; int n, i; //map 数组 无值 扩容if ((tab = table) == null || (n = tab.length) == 0)n = (tab = resize()).length;//i = (n - 1) & hash  规范化 数组长度为 2的n次幂if ((p = tab[i = (n - 1) & hash]) == null)/*   i = (n - 1) & hash   <=>   (n-1) & (h = key.hashCode()) ^ (h >>> 16)  计算出索引总结:简单来说就是:高16 bit 不变,低16 bit 和高16 bit 做了一个异或(得到的 hashcode 转化为32位二进制,前16位和后16位  低16bit和高16bit做了一个异或)问题:为什么要这样操作呢???????  充分利用高位和低位,而不是只利用低位,可以减少hash冲突即:如果当n即数组长度很小,假设是16的话,那么n-1即为  15 --->1111 ,这样的值和hashCode()直接做按位与操作,实际上只使用了哈希值的后4位。如果当哈希值的高位变化很大,低位变化很小,这样就很容易造成哈希冲突了,所以这里把高低位都利用起来,从而解决了这个问题。其实就是将hashCode值作为数组索引,那么如果下个高位hashCode不一致,低位一致的话,就会造成计算的索引还是10,从而造成了哈希冲突了。降低性能*/// 第一次存储为null ,创建新的节点,并把下一个 节点 置为nulltab[i] = newNode(hash, key, value, null);else {//走这里表示 该位置出现了 hash 冲突Node<K,V> e; K k; // p 表示出现冲突的地方 已有的对象 和现在要 put 的对象的 hash 是不是相等 &&( key 是不是一个key    ||  key != null && key 的内容一样不一样)if (p.hash == hash                                   && ((k = p.key) == key ||  (key != null && key.equals(k))))e = p;   // e = 原来的值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) {//下一个节点是null  // e = p.next p.next = newNode(hash, key, value, null);  //尾插法,在链表后面加上这个节点, if (binCount >= TREEIFY_THRESHOLD - 1) // 要是 > 8 (bitcount 0表示第一个节点,7表示第八个节点,加上数组中的的一个元素,所以总共的元素个数是9)treeifyBin(tab, hash);//树化 break; // 停止 for 循环}下一个节点不是 是null
// p 表示出现冲突的地方 已有的对象 和现在要 put 的对象的 hash 是不是相等 &&( key 是不是一个key    ||  key != null && key 的内容一样不一样)if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))break;  // 停止 for 循环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;}// 树化
// 替换 bin 中给定哈希索引处的所有链接节点,除非表太小,在这种情况下会调整大小。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)   // 即使 链表 长度 > 8  但是 数组长度  < 64 仍然进行扩容resize();else if ((e = tab[index = (n - 1) & hash]) != null) {  //数组长度   >= 64 TreeNode<K,V> hd = null, tl = null; // hd  头结点 , tl =  尾节点//循环的操作就是 将 链表 变为一个 树  do {TreeNode<K,V> p = replacementTreeNode(e, null);/*TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {return new TreeNode<>(p.hash, p.key, p.value, next);}TreeNode(int hash, K key, V val, Node<K,V> next) {super(hash, key, val, next);}*/if (tl == null)hd = p; // 将新创键的p节点赋值给红黑树的头结点else {/*p.prev = tl; 将上一个节点 p 赋值给现在的 p 的前一个节点tl.next = p; 将现在节点 p 作为树的尾结点的下一个节点*/p.prev = tl;tl.next = p;}tl = p;/*e = e.next 将当前节点的下一个节点赋值给e,如果下一个节点不等于null则回到上面继续取出链表中节点转换为红黑树*/} while ((e = e.next) != null);if ((tab[index] = hd) != null)hd.treeify(tab);   // 进行(左旋 右旋 )旋转、平衡等操作转为链表 }}小结:
1.根据哈希表中元素个数确定是扩容还是树形化
2.如果  是树形化就遍历桶中的元素,创建相同个数的树形节点,复制内容,建立起联系
3.然后让桶中的第一个元素 指向 新创建的树根节点,替换桶的链表内容为树化之后的内容

resize

扩容机制
  1. 扩容时机:

    1、*HashMap中的元素个数 > 数组大小(数组长度)loadFactor(负载因子)

    2、当HashMap中的其中一个链表的对象个数如果达到了8个,此时如果数组长度没有达到64,那么HashMap会先扩容解决,如果已经达到了64,那么这个链表会变成红黑树,节点类型由Node变成TreeNode类型。当然,如果映射关系被移除后,下次执行resize方法时判断树的节点个数低于6,也会再把树还原回链表。

  2. HashMap的扩容是什么:

    原来的时候每次进行扩容都会伴随着一次 rehash,效率较低,现在的话,rehash 做了优化;

    如何优化:

    因为每次扩容都是翻倍,与原来计算的 (n-1)&hash的结果相比,只是多了一个bit位,所以节点要么就在原来的位置,要么就被分配到"原位置+旧容量"这个位置,所以我们在扩充HashMap的时候,不需要 rehash,只需要看看原来的 hash 值新增的那个bit是1还是0就可以了,是0的话索引没变,是1的话索引变成“原索引+oldCap(原位置+旧容量)”。

    好处:

    正是因为这样巧妙的rehash方式,既省去了重新计算hash值的时间,而且同时,由于新增的1bit是0还是1可以认为是随机的,在resize的过程中保证了rehash之后每个桶上的节点数一定小于等于原来桶上的节点数,保证了rehash之后不会出现更严重的hash冲突,均匀的把之前的冲突的节点分散到新的桶中了。

image-20231111225828871

image-20231206232102247

final Node<K,V>[] resize() {Node<K,V>[] oldTab = table; // 第一次的时候  旧的数组 = null  非第一次,oldTab 不是空的int oldCap = (oldTab == null) ? 0 : oldTab.length;int oldThr = threshold; // 第一次的时候 threshold = 0       非第一次, 根据之前的计算int newCap, newThr = 0; //新的数组容量,新的边界值// 确定 newCap, newThrif (oldCap > 0) {if (oldCap >= MAXIMUM_CAPACITY) {  //2的30次方threshold = Integer.MAX_VALUE;return oldTab;}//  新的容量  2 * oldCap  <   2的30次方      &&     olacap  >= 16else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&  oldCap >= DEFAULT_INITIAL_CAPACITY)// 新的边界值  =  oldThr * 2newThr = oldThr << 1; // double threshold}else if (oldThr > 0) // initial capacity was placed in thresholdnewCap = oldThr;else {               // zero initial threshold signifies using defaultsnewCap = DEFAULT_INITIAL_CAPACITY; // 第一次的时候  新的数组的容量默认值 16 newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); //  第一次的时候  新的边界值 12}if (newThr == 0) {float ft = (float)newCap * loadFactor;newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE);}threshold = newThr;  // threshold 边界值 12@SuppressWarnings({"rawtypes","unchecked"})Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];  //第一次的时候创建数组Node[16]     非第一次, 根据之前的计算Node[newCap]table = newTab;  // 第一次的时候  table 原来 null ;  现在 newTab       非第一次, table 为 旧的数组if (oldTab != null) {for (int j = 0; j < oldCap; ++j) { // 遍历旧的数组Node<K,V> e;if ((e = oldTab[j]) != null) {  //旧数据  给 e oldTab[j] = null; // 旧数组的位置 置为 null if (e.next == null)// 只有数组有元素,链表没有元素newTab[e.hash & (newCap - 1)] = e;  //重新计算 hash:  e.hash & (newCap - 1)  要么是原位置,要么是原位置+旧容量 ;将旧数据给新数组的对应位置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;  // 指针下移//e.hash & oldCap 计算高位是 1 还是 0 ,if ((e.hash & oldCap) == 0) { //进行移动操作if (loTail == null)loHead = e;elseloTail.next = e;loTail = e;}//进行移动操作else {if (hiTail == null)hiHead = e;elsehiTail.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;  //第一次的时候 默认16
}

remove

首先先找到元素的位置,如果是链表就遍历链表找到元素之后删除。如果是用红黑树就遍历树然后找到之后做删除,树小于6的时候要转链表。

image-20231206232030261

public V remove(Object key) {Node<K,V> e;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;// 数组存在           数组长度 > 0               指定的位置有元素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)))) // 找到对应 key 的元素node = p;   // 找到的元素 赋值给 node // else if ((e = p.next) != null) { // e = p.next    e != 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; // p 设置为 e } while ((e = e.next) != null);}} // node != null    &&  ( true (matchValue指定为 false)  || node 的值是 传入的值 || (值是相等的)   )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;elsep.next = node.next;++modCount; //修改次数 ++--size; // 长度 --afterNodeRemoval(node);  // 回调钩子函数return node;}}return null;}

get

get方法实现的步骤:

  1. 通过hash值获取该key映射到的桶
  2. 桶上的key就是要查找的key,则直接找到并返回
  3. 桶上的key不是要找的key,则查看后续的节点:
    1. 如果后续节点是红黑树节点,通过调用红黑树的方法根据key获取value
      1. 查找红黑树,由于之前添加时已经保证这个树是有序的了,因此查找时基本就是折半查找,效率更高。
      2. 这里和插入时一样,如果对比节点的哈希值和要查找的哈希值相等,就会判断key是否相等,相等就直接返回。不相等就从子树中递归查找。
    2. 如果后续节点是链表节点,则循环遍历链表根据key获取value

若为树,则在树中通过key.equals(k)查找,O(logn) ; 若为链表,则在链表中通过key.equals(k)查找,O(n)。

image-20231206232212615

public V get(Object key) {Node<K,V> e;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;// tab 不是 null   &&  长度 > 0     &&    指定的位置有元素  if ((tab = table) != null && (n = tab.length) > 0 &&(first = tab[(n - 1) & hash]) != null) {// 找到对应的hash if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k))))//返回  指定位置的元素  return first;if ((e = first.next) != null) {  //  e = first.next  链表下移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;}final TreeNode<K,V> getTreeNode(int h, Object k) {return ((parent != null) ? root() : this).find(h, k, null);}final TreeNode<K,V> find(int h, Object k, Class<?> kc) {TreeNode<K,V> p = this;// 遍历 树节点,折半查找do {int ph, dir; K pk;TreeNode<K,V> pl = p.left, pr = p.right, q;if ((ph = p.hash) > h)p = pl;else if (ph < h)p = pr;else if ((pk = p.key) == k || (k != null && k.equals(pk)))return p;else if (pl == null)p = pr;else if (pr == null)p = pl;else if ((kc != null ||(kc = comparableClassFor(k)) != null) &&(dir = compareComparables(kc, k, pk)) != 0)p = (dir < 0) ? pl : pr;else if ((q = pr.find(h, k, kc)) != null)  // 递归调用左右子树return q;elsep = pl;} while (p != null);return null;}

clear

image-20231206232158324

public void clear() {Node<K,V>[] tab;//修改次数++modCount++;// 数组存在且有元素if ((tab = table) != null && size > 0) {//将数组 容量置为 0size = 0;// 遍历所有元素,全部设置为nullfor (int i = 0; i < tab.length; ++i)tab[i] = null;}
}

面试题:

  1. 为什么必须是2的n次幂?

    1. 当向HashMap中添加一个元素的时候,需要根据key的hash值,去确定其在数组中的具体位置。 HashMap为了存取高效,要尽量较少碰撞,就是要尽量把数据分配均匀,每个链表长度大致相同,这个实现就在把数据存到哪个链表中的算法。

    2. 这个算法实际就是取模,hash%length。所以源码中做了性能优化,使用 hash&(length-1),而实际上hash%length等于hash&(length-1)前提length是2的n次幂。

    3. 为什么这样能均匀分布减少碰撞呢?

      总结: 是 为了数据的均匀分布,减少hash冲突,因为 hash冲突越大,代表数组中一个链的长度越大,这样的话会降低 hashmap 的性能

    举例:
    说明: 
    2的n次方实际就是1后面n个02的n次方-1  实际就是n个11000
    -	   1
    ---------------------111按位与运算:相同的二进制数位上,都是1的时候,结果为1,否则为零。hash & (length-1)
    3   & (8    - 1) = 3  00000011  3  hash
    & 00000111  7  length-1
    ---------------------00000011  3 数组下标
    同理   2   & (8    - 1) = 2  
    从而减少碰撞
    
  2. 如果输入值不是2的幂比如10会怎么样?

    HashMap通过一通位移运算和或运算得到的肯定是2的幂次数,并且是离那个数最近的数字

  3. 为什么Map桶中节点个数超过8才转为红黑树?

    总结:

    1. 选择8因为符合泊松分布,超过8的时候,概率已经非常小了,所以我们选择8这个数字。
    2. 红黑树的平均查找长度是log(n),如果长度为8,平均查找长度为log(8)=3,链表的平均查找长度为n/2,当长度为8时,平均查找长度为8/2=4,这才有转换成树的必要;链表长度如果是小于等于6,6/2=3,而log(6)=2.6,虽然速度也很快的,但是转化为树结构和生成树的时间并不会太短,而且树还有自旋、变色等操作
    3. 本质: 就是权衡,空间和时间的权衡

    因为红黑树节点的大小 大约是链表节点的 两倍,所以我们只在bin (bin就是bucket(桶))包含足够的节点时才使用树节点。

    当它们变得太小(由于删除或调整大小)时,就会被转换回普通的桶。在使用分布良好的用户hashcode时,很少使用树箱。理想情况下,在随机哈希码下,箱子中节点的频率服从泊松分布: 默认调整阈值为0.75,平均参数约为0.5,尽管由于调整粒度的差异很大。忽略方差,列表大小k的预期出现次数是(exp(-0.5)*pow(0.5, k)/factorial(k))

    第一个值是:0:    0.60653066
    1:    0.30326533
    2:    0.07581633
    3:    0.01263606
    4:    0.00157952
    5:    0.00015795
    6:    0.00001316
    7:    0.00000094
    8:    0.00000006
    more: less than 1 in ten million
    

TreeNodes占用空间是普通Nodes的两倍,所以只有当bin包含足够多的节点时才会转成TreeNodes,而是否足够多就是由TREEIFY_THRESHOLD = 8的值决定的。当bin中节点数变少时,又会转成普通的bin。并且我们查看源码的时候发现,链表长度达到8就转成红黑树,当长度降到6就转成普通bin

这样就解释了为什么不是一开始就将其转换为TreeNodes,而是需要一定节点数才转为TreeNodes,说白了就是权衡,空间和时间的权衡

这段内容还说到:当hashCode离散性很好的时候,树型bin用到的概率非常小,因为数据均匀分布在每个bin中,几乎不会有bin中链表长度会达到阈值。但是在随机hashCode下,离散性可能会变差,然而JDK又不能阻止用户实现这种不好的hash算法,因此就可能导致不均匀的数据分布。不过理想情况下随机hashCode算法下所有bin中节点的分布频率会遵循泊松分布,我们可以看到,一个bin中链表长度达到8个元素的概率为0.00000006,几乎是不可能事件。所以,之所以选择8,不是随便决定的,而是根据概率统计决定的。由此可见,发展将近30年的Java每一项改动和优化都是非常严谨和科学的。

  1. 为什么要选择 0.75

    loadFactor 太大导致查找元素效率低(虽然size 达到了 loadfactor 的值,但是每个数组下挂载的链表个数有可能很多),太小导致数组的利用率低,存放的数据会很分散。loadFactor的默认值为0.75f是官方给出的一个比较好的临界值

    loadFactor 越趋近于1,那么 数组中存放的数据(entry)也就越多,也就越密,也就是会让链表的长度增加,loadFactor越小,也就是趋近于0,数组中存放的数据(entry)也就越少,也就越稀疏。

    注意,尽可能减少扩容次数,因为 扩容这个过程涉及到 rehash、复制数据等操作,非常消耗性能!可以通过创建HashMap集合对象时指定初始容量来尽量避免

  2. HashMap中容量的初始化的值的建议是多少

    initialCapacity=(需要存储的元素个数/负载因子)+1 【注意负载因子(即loaderfactor)默认为0.75】

    建议可以把默认容量的数字设置成**initialCapacity/ 0.75F + 1.0F**

总结

  1. 1.8 之前直接创建 Entry 数组,1.8之后 调用 put() 的 时候才会创建 Node 数组

  2. put 的 时候 经过 hash 运算得到数组的索引下标(此处是结合数组长度进行 >>> & ^),要是对应的位置为空 直接插入。要是对应的位置有数据,但是 hash不一样,就创建一个链表节点,存储到链表节点上。 hash 值一样【此时称为 hash冲突】,调用key的 equals() 函数比较是不是一样的,不一样,继续添加(链表/ 树);一样,进行覆盖。

  3. 使用红黑树的原因: 提升查询效率, 链表中的时间复杂度(O(n)),但是红黑树的时间复杂度(O(log(n)))

  4. 为什么一定要是2 的幂次方:源码中 解决 哈希冲突的做法: 是将 计算的 hash 值与数组的长度-1进行运算,就是等价于 取模 数组长度的运算。但是进行与运算的效率更高,所以一定要是2 的幂次方 与其说是扩容的条件,不如说是 进行与运算的条件,就是为了让数据分布均匀,减少哈希冲突,提升效率。

  5. 数组在初始化的时候给定了指定的值,要是不是2的幂次方,那么会自动扩容为最接近给定值的2 的幂次方

  6. 阈值为什么是8? 1、源码上的解释是因为 符合 泊松分布,概率统计的结果决定的 2、站在 效率的角度看,log(n) 在n>8 的时候树的效率更高

    不进行树化的时候,转为链表时的阈值为什么是6? 还是从效率考虑,虽然此时 链表和数的效率差不多,但是 树会多了 自旋、变色等步骤 是会降低效率的,而且一个树的节点 所占用的空间是链表节点的两倍。

    所以上面的两个阈值这么设置的原因就是: 效率。权衡时间和空间。

  7. 为什么加载因子是 0.75 ? 过大会导致效率降低,过小 会导致数组的利用率降低,频繁造成扩容、复制的操作

  8. hashMap 的容量的初始值建议设置为:initialCapacity=(需要存储的元素个数/负载因子)+1 此处的+ 1 的作用实际上是对计算出来的小数进行取整,扩大容量,防止处于临界状态的之后立刻扩容的现象发生。减少扩容的次数,提升性能。反例:要是不加1;此时数组的实际长度是 6 ,此时应该的初始值是8,但是很容易就会导致出现扩容的情况;所以此时就需要进行+1,之后的数组会扩容到16,就可以减少初始化之后立马又扩容的状况了。

  9. Hashmap 的扩容是怎么进行优化的:因为扩容每次都是翻倍的,所有再重新hash 计算 (n-1)&hash的时候就会比原来多一个bit位,所以节点要么就是原来的位置,要么就是 原来的容量 + 原来的位置;所以 在扩充HashMap的时候,不需要 rehash,只需要看看原来的 hash 值新增的那个bit是1还是0就可以了,是0的话索引没变,是1的话索引变成“原索引+oldCap(原位置+旧容量)”

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/203516.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

QT+Unity3D 超详细(将unity3D与QT进行连接,并实现信息传递)

QTUnity3D连接 在QT中连接unity3D&#xff0c;首先要有一个unity.exe执行文件。在这里不提供unity执行文件的编写&#xff0c;只提供QT这边与unity3D连接和信息传递。 创建项目 创建一个新的项目&#xff0c;我创建的项目名称如下。 下图是我建立新项目的文件。APP文件就是…

<Linux>(极简关键、省时省力)《Linux操作系统原理分析之Linux文件管理(1)》(25)

《Linux操作系统原理分析之Linux文件管理&#xff08;1&#xff09;》&#xff08;25&#xff09; 8 Linux文件管理8.1 Linux 文件系统概述8.2 EXT2 文件系统8.2.1 EXT2 文件系统的构造8.2.2 EXT2 超级块&#xff08;super block&#xff09;8.2.3 组描述符8.2.4 块位图 8.3 EX…

智能优化算法应用:基于社交网络算法无线传感器网络(WSN)覆盖优化 - 附代码

智能优化算法应用&#xff1a;基于社交网络算法无线传感器网络(WSN)覆盖优化 - 附代码 文章目录 智能优化算法应用&#xff1a;基于社交网络算法无线传感器网络(WSN)覆盖优化 - 附代码1.无线传感网络节点模型2.覆盖数学模型及分析3.社交网络算法4.实验参数设定5.算法结果6.参考…

用23种设计模式打造一个cocos creator的游戏框架----(七)代理模式

1、模式标准 模式名称&#xff1a;代理模式 模式分类&#xff1a;结构型 模式意图&#xff1a;为其他对象提供一种代理以控制对这个对象的访问。 结构图&#xff1a; ​ 适用于&#xff1a; 远程代理&#xff1a;也称为大使&#xff0c;这是最常见的类型&#xff0c;在分…

2022年第十一届数学建模国际赛小美赛C题人类活动分类解题全过程文档及程序

2022年第十一届数学建模国际赛小美赛 C题 人类活动分类 原题再现&#xff1a; 人类行为理解的一个重要方面是对日常活动的识别和监控。可穿戴式活动识别系统可以改善许多关键领域的生活质量&#xff0c;如动态监测、家庭康复和跌倒检测。基于惯性传感器的活动识别系统用于通过…

Kubernetes入门笔记——(2)k8s设计文档

​k8s最初源自谷歌的Brog项目&#xff0c;架构与其类似&#xff0c;主要包括etcd、api server、controller manager、scheduler、kubelet和kube-proxy等组件 etcd&#xff1a;分布式存储&#xff0c;保存k8s集群的状态 api server&#xff1a;资源操作的唯一入口&#xff0c;…

Kafka 的消息格式:了解消息结构与序列化

Kafka 作为一款高性能的消息中间件系统&#xff0c;其消息格式对于消息的生产、传输和消费起着至关重要的作用。本篇博客将深入讨论 Kafka 的消息格式&#xff0c;包括消息的结构、序列化与反序列化&#xff0c;以及一些常用的消息格式选项。通过更丰富的示例代码和深入的解析&…

2023年山东省职业院校技能大赛信息安全管理与评估二三阶段样题

2023年山东省职业院校技能大赛信息安全管理与评估二三阶段 样题 第二阶段 模块二 网络安全事件响应、数字取证调查、应用程序安全 一、竞赛内容 Geek极安云科专注技能竞赛技术提升&#xff0c;基于各大赛项提供全面的系统性培训&#xff0c;拥有完整的培训体系。团队拥有曾…

docker部署elasticsearch8.x

docker部署elasticsearch8.x 提示1 注意版本差别1.1 docker修改配置1.1.2 docker使用vim报命令不存在的解决办法1.1.3 docker 容器内报错 E: List directory /var/lib/apt/lists/partial is missing. - Acquire ( : No such file or directory) 或者其他权限 PermissionError: …

【Delphi】一个函数实现ios,android震动功能 Vibrate(包括3D Touch 中 Peek 震动等)

一、前言 我们在开发移动端APP的时候&#xff0c;有时可能需要APP能够提供震动功能&#xff0c;以便提醒操作者&#xff0c;特别是ios提供的3D Touch触感功能&#xff0c;操作者操作时会有触感震动&#xff0c;给操作者的感觉很友好。那么&#xff0c;在Delphi的移动端FMX开发中…

团建策划信息展示服务预约小程序效果如何

团建是中大型企业商家每年举办的员工活动&#xff0c;其形式多样化、具备全部参与的娱乐性。但在实际策划流程及内容时&#xff0c;部分公司便会难以入手&#xff0c;术业有专攻&#xff0c;这个时候团建策划公司便会发挥效果。 如拓展训练、露营、运动会、体育竞技等往往更具…

【算法】算法题-20231207

这里写目录标题 一、共同路径二、数字列表排序三、给定两个整数 n 和 k&#xff0c;返回 1 … n 中所有可能的 k 个数的组合。 一、共同路径 给你一个完整文件名组成的列表&#xff0c;请编写一个函数&#xff0c;返回他们的共同目录路径。 # nums[/hogwarts/assets/style.cs…

算法通关村第十七关-黄金挑战跳跃问题

大家好我是苏麟 , 今天说说跳跃问题 . 跳跃游戏 描述 : 给你一个非负整数数组 nums &#xff0c;你最初位于数组的 第一个下标 。数组中的每个元素代表你在该位置可以跳跃的最大长度。 判断你是否能够到达最后一个下标&#xff0c;如果可以&#xff0c;返回 true &#xff…

HBase-架构与设计

HBase架构与设计 一、背景二、HBase概述1.设计特点2.适用场景2.1 海量数据2.2 稀疏数据2.3 多版本数据2.4 半结构或者非结构化数据 三、数据模型1.RowKey2.Column Family3.TimeStamp 四、HBase架构图1.Client2.Zookeeper3.HMaster4.HRegionServer5.HRegion6.Store7.StoreFile8.…

Elasticsearch:什么是机器学习?

机器学习定义 机器学习 (ML) 是人工智能 (AI) 的一个分支&#xff0c;专注于使用数据和算法来模仿人类的学习方式&#xff0c;并随着时间的推移逐渐提高准确性。 计算机科学家和人工智能创新者 Arthur Samuel 在 20 世纪 50 年代首次将其定义为 “赋予计算机无需明确编程即可学…

【基于openGauss5.0.0简单使用DBMind】

基于openGauss5.0.0简单使用DBMind 一、环境说明二、初始化tpch测试数据三、使用DBMind索引推荐功能四、使用DBMind实现SQL优化功能 一、环境说明 虚拟机&#xff1a;virtualbox操作系统&#xff1a;openEuler 20.03 TLS数据库&#xff1a;openGauss-5.0.0DBMind&#xff1a;d…

2022年第十一届数学建模国际赛小美赛A题翼龙如何飞行解题全过程文档及程序

2022年第十一届数学建模国际赛小美赛 A题 翼龙如何飞行 原题再现&#xff1a; 翼龙是翼龙目中一个已灭绝的飞行爬行动物分支。它们存在于中生代的大部分时期&#xff1a;从三叠纪晚期到白垩纪末期。翼龙是已知最早进化出动力飞行的脊椎动物。它们的翅膀是由皮肤、肌肉和其他组…

云服务器与nas实现在冷热资源访问,nginx代理

在实际项目中&#xff0c;我们的文件存储是一个必不可少的环节&#xff0c;本博主了解到现在的存储方案有 购买纯系统的云服务器&#xff0c;自己安装个mino,再使用nginx代理给web使用购买OSS服务&#xff0c;现在有云厂商都有提供&#xff0c;储存价格也挺便宜的&#xff0c;…

13款趣味性不错(炫酷)的前端动画特效及源码(预览获取)分享(附源码)

文字激光打印特效 基于canvas实现的动画特效&#xff0c;你既可以设置初始的打印文字也可以在下方输入文字可实现激光字体打印&#xff0c;精简易用。 预览获取 核心代码 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8&q…

生物动力葡萄酒的快速指南

虽然我们大多数人都熟悉有机酿酒和农业&#xff0c;但围绕生物动力学仍有许多困惑和神秘。无论你是否完全陌生&#xff0c;或者你已经听到一些小道消息&#xff0c;我们在这里揭开这种独特的葡萄酒生产方法的神秘面纱。 生物动力葡萄酒就是一个更全面的有机酿酒过程&#xff0c…