简介:
- 是基于 哈希表 实现的,存放 k-v 键值对,非同步的方式(未加 synchronized )非线程安全的,hashmap 无序的
- 数据结构: 数组 + 链表 ====> 数组 +链表 + 红黑树「链表 和 链表 + 红黑树 都是为了解决 哈希冲突
- 哈希冲突: 两个对象调用的
hashCode
方法计算的哈希码值一致导致计算的数组索引值相同 ,区别是: 链表 + 红黑树 在达到 : 阈值> 8 && 数组长度 > 64 才会进行转为红黑树———>原因: 虽然红黑树会让结构更加复杂,但是这时查询效率更高
,要是数组长度比较少的话,应该尽量避免红黑树,不然的话红黑树需要进行左旋,右旋,变色这些操作来保持平衡
并且 数组的长度 < 64 的时候 搜索的时间是相对来说比较快的」
数据结构:
存储说明:
-
HashMap<String,Integer>map = new HashMap<>();
创建HashMap对象后,-
在jdk8前 : 底层创建了长度是16的一维数组
Entry[]table
。 -
在jdk8以后并没有 在创建集合对象时创建数组,而是在
首次调用put()
方法时,底层创建长度为16的Node[] table
数组
-
-
假设向哈希表中存储数据
key1-value1
,随后 根据 key1 调用String
类中的hashCode()
方法计算key1
的哈希码值,此哈希码值经过某种算法计算以后,得到在Node 数组中的存放的位置,如果比位置上的数据为空,此时的key1-value1
添动加成功。例如计算的索引是2- 面试题:HashMap中
hash函数
是怎么实现的?还有哪些hash函数的实现方式?- 对key的hashCode做hash操作:结合数组的长度进行 无符号右移(>>> ) 、按位异或 (^)、按位与(& ) 运算。
- 还有平方取中法,除留余数法,伪随机数法。其余三种方式效率都比较低。而无符号右移16位做异或运算效率是比较高的
- 面试题:HashMap中
-
假设向哈希表中存储
key2-value2
,根据键的key2
计算出的 哈希值,并结合数组长度计算出的索引;那么一旦此时的索引位置有数据,就会比较hash 值,要是hash 值不一样,那么就要在这个索引空间上新创建一个节点,然后 将key2-value2
存放到这个节点中。 -
假设向哈希表中存储
key1-value3
,根据键的key1
计算出的 哈希值(此时的索引位置和之前肯定是一样的);-
此时就会比较后添加的数据的
key
和已经存在的哈希值是否相同, -
如果相同,继续比较:调用后添加的
key1
的所属类的equals()
比较内容是否相等。- 如果
equals()
返回false,此时继续添加。 - 如果
equals()
返回true:则后添加的value3
替换之前的value1
- 如果
-
面试题:当两个对象的
hashCode
相等时会怎么样?- 会产生哈希碰撞
- 进一步比较通过equals比较
key 内容
是否相同- 相同:则新的value覆盖之前的value
- 不相同:否则遍历链接到链表后面,链表长度 > 8 且 数组长度 > 64 ,就转为红黑树存储
- 进一步比较通过equals比较
- 会产生哈希碰撞
-
红黑树结构复杂,为什么使用红黑树?为什么阈值规定为 8?
- 哈希函数取得再好,也很难达到元素百分百均匀分布。当 HashMap 中有大量的元素都存放到同一个桶中时,这个桶下有一条长长的链表,这个时候 HashMap 就相当于一个单链表,假如单链表有 n 个元素,遍历的时间复杂度就是 O(n),完全失去了它的优势。针对这种情况,JDK 1.8 中引入了 红黑树(查找时间复杂度为 O(logn))来优化这个问题,当链表长度很小的时候,即使遍历,速度也非常快,但是当链表长度不断变长,肯定会对查询性能有一定的影响,所以才需要转成树。
- 见源码
size表示 HashMap中K-V的实时数量 , 注意这个不等于数组的长度 。
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 放值}}}
-
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 //按位异或之后是13
第2次右移
n |= n >>> 2;//n通过第一次右移变为了:n=1300000000 00000000 00000000 00001101 // 13
|00000000 00000000 00000000 00000011 //13右移之后变为3
-------------------------------------------------00000000 00000000 00000000 00001111 //按位异或之后是15
第3次右移
n |= n >>> 4;//n通过第一、二次右移变为了:n=1500000000 00000000 00000000 00001111 // 15
|00000000 00000000 00000000 00000000 //15右移之后变为0
-------------------------------------------------00000000 00000000 00000000 00001111 //按位异或之后是15
第4、5次右移 还是 15 把已经有的高位中的连续的4个1,右移4位,再做或操作,这样n的二进制表示的高位中正常会有8个连续的1。如00001111 1111xxxxxx 。
以此类推
注意,容量最大也就是32 位的正数,因此最后n |= n >>> 16; ,最多也就32个1(但是这已经是负数了。在执行tableSizeFor之前,对initialCapacity做了判断,如果大于MAXIMUM_CAPACITY(2^30),则取MAXIMUM_CAPACITY。如果等于MAXIMUM_CAPACITY(2^30),会执行移位操作。所以这里面的移位操作之后,最大30个1,不会大于等于MAXIMUM_CAPACITY。30个1,加1之后得2^30) 。
-
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,则进行扩容;
// 将指定值与此映射中的指定键相关联。如果映射之前包含键的映射,则旧值将被替换。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、*HashMap中的元素个数 > 数组大小(数组长度)loadFactor(负载因子)
2、当HashMap中的其中一个链表的对象个数如果达到了8个,此时如果数组长度没有达到64,那么HashMap会先扩容解决,如果已经达到了64,那么这个链表会变成红黑树,节点类型由Node变成TreeNode类型。当然,如果映射关系被移除后,下次执行resize方法时判断树的节点个数低于6,也会再把树还原回链表。
-
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冲突,均匀的把之前的冲突的节点分散到新的桶中了。
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的时候要转链表。
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方法实现的步骤:
- 通过hash值获取该key映射到的桶
- 桶上的key就是要查找的key,则直接找到并返回
- 桶上的key不是要找的key,则查看后续的节点:
- 如果后续节点是红黑树节点,通过调用红黑树的方法根据key获取value
- 查找红黑树,由于之前添加时已经保证这个树是有序的了,因此查找时基本就是折半查找,效率更高。
- 这里和插入时一样,如果对比节点的哈希值和要查找的哈希值相等,就会判断key是否相等,相等就直接返回。不相等就从子树中递归查找。
- 如果后续节点是链表节点,则循环遍历链表根据key获取value
若为树,则在树中通过key.equals(k)查找,O(logn) ; 若为链表,则在链表中通过key.equals(k)查找,O(n)。
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
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;}
}
面试题:
-
为什么必须是2的n次幂?
-
当向HashMap中添加一个元素的时候,需要根据key的hash值,去确定其在数组中的具体位置。 HashMap为了存取高效,要尽量较少碰撞,就是要尽量把数据分配均匀,每个链表长度大致相同,这个实现就在把数据存到哪个链表中的算法。
-
这个算法实际就是取模,
hash%length
。所以源码中做了性能优化,使用hash&(length-1)
,而实际上hash%length等于hash&(length-1)
的前提
是length是2的n次幂。
-
为什么这样能均匀分布减少碰撞呢?
总结: 是 为了数据的均匀分布,减少hash冲突,因为 hash冲突越大,代表数组中一个链的长度越大,这样的话会降低 hashmap 的性能
举例: 说明: 2的n次方实际就是1后面n个0,2的n次方-1 实际就是n个1;1000 - 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的幂比如10会怎么样?
HashMap通过一通位移运算和或运算得到的肯定是2的幂次数,并且是离那个数最近的数字
-
为什么Map桶中节点个数超过8才转为红黑树?
总结:
选择8因为符合泊松分布,超过8的时候,概率已经非常小了
,所以我们选择8这个数字。- 红黑树的平均查找长度是log(n),如果长度为8,平均查找长度为log(8)=3,链表的平均查找长度为n/2,当长度为8时,平均查找长度为8/2=4,这才有转换成树的必要;链表长度如果是小于等于6,6/2=3,而log(6)=2.6,虽然速度也很快的,但是转化为树结构和生成树的时间并不会太短,而且树还有自旋、变色等操作
- 本质:
就是权衡,空间和时间的权衡
。
因为红黑树节点的大小 大约是链表节点的 两倍,所以我们只在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每一项改动和优化都是非常严谨和科学的。
-
为什么要选择 0.75
loadFactor 太大导致查找元素效率低(虽然size 达到了 loadfactor 的值,但是每个数组下挂载的链表个数有可能很多),太小导致数组的利用率低,存放的数据会很分散。loadFactor的默认值为0.75f是官方给出的一个比较好的临界值
loadFactor 越趋近于1,那么 数组中存放的数据(entry)也就越多,也就越密,也就是会让链表的长度增加,loadFactor越小,也就是趋近于0,数组中存放的数据(entry)也就越少,也就越稀疏。
注意,尽可能减少扩容次数,因为 扩容这个过程涉及到 rehash、复制数据等操作,非常消耗性能!可以通过创建HashMap集合对象时指定初始容量来尽量避免
-
HashMap中容量的初始化的值的建议是多少
initialCapacity=(需要存储的元素个数/负载因子)+1 【注意负载因子(即loaderfactor)默认为0.75】
建议可以把默认容量的数字设置成**
initialCapacity/ 0.75F + 1.0F
**
总结
-
1.8 之前直接创建 Entry 数组,1.8之后 调用 put() 的 时候才会创建 Node 数组
-
put 的 时候 经过 hash 运算得到数组的索引下标(此处是结合数组长度进行
>>> & ^
),要是对应的位置为空 直接插入。要是对应的位置有数据,但是hash
不一样,就创建一个链表节点,存储到链表节点上。hash
值一样【此时称为 hash冲突】,调用key的equals()
函数比较是不是一样的,不一样,继续添加(链表/ 树);一样,进行覆盖。 -
使用红黑树的原因: 提升查询效率, 链表中的时间复杂度(O(n)),但是红黑树的时间复杂度(O(log(n)))
-
为什么一定要是2 的幂次方:源码中 解决 哈希冲突的做法: 是将 计算的 hash 值与
数组的长度-1
进行与
运算,就是等价于 取模 数组长度的运算。但是进行与运算的效率更高,所以一定要是2 的幂次方 与其说是扩容的条件,不如说是 进行与运算的条件,就是为了让数据分布均匀,减少哈希冲突,提升效率。
-
数组在初始化的时候给定了指定的值,要是不是2的幂次方,那么会自动扩容为最接近给定值的2 的幂次方
-
阈值为什么是8? 1、源码上的解释是因为 符合 泊松分布,概率统计的结果决定的 2、站在 效率的角度看,log(n) 在n>8 的时候树的效率更高
不进行树化的时候,转为链表时的阈值为什么是6? 还是从效率考虑,虽然此时 链表和数的效率差不多,但是 树会多了 自旋、变色等步骤 是会降低效率的,而且一个
树的节点
所占用的空间是链表节点
的两倍。所以上面的两个阈值这么设置的原因就是: 效率。权衡时间和空间。
-
为什么加载因子是 0.75 ? 过大会导致效率降低,过小 会导致数组的利用率降低,频繁造成扩容、复制的操作
-
hashMap 的容量的初始值建议设置为:
initialCapacity=(需要存储的元素个数/负载因子)+1
此处的+ 1 的作用实际上是对计算出来的小数进行取整,扩大容量,防止处于临界状态的之后立刻扩容的现象发生。减少扩容的次数,提升性能。反例:要是不加1;此时数组的实际长度是 6 ,此时应该的初始值是8,但是很容易就会导致出现扩容的情况;所以此时就需要进行+1,之后的数组会扩容到16,就可以减少初始化之后立马又扩容的状况了。 -
Hashmap 的扩容是怎么进行优化的:因为扩容每次都是翻倍的,所有再重新hash 计算
(n-1)&hash
的时候就会比原来多一个bit位,所以节点要么就是原来的位置
,要么就是原来的容量 + 原来的位置
;所以 在扩充HashMap的时候,不需要rehash
,只需要看看原来的hash
值新增的那个bit是1还是0
就可以了,是0的话索引没变
,是1的话索引变成“原索引+oldCap(原位置+旧容量)”