先看源码:
解释一下位移运算:
1<<4 是位移运算的表示,为十进制16
1的二进制表示:1
左移4位之后的二进制表示为B(10000) = D(16)
更简单的计算方法就是 1<< n 等效于 1 乘以 2的 n 次方
进入正题
HashMap底层数据结构是数组+链表,JDK1.8中还引入了红黑树,当链表长度超过8个时,会将链表转成红黑树,以提升其查找性能。
HashMap有两个参数影响其性能:初始容量和加载因子。
1、HashMap的初始容量
容量是哈希表中桶的数量,初始容量只是哈希表在创建时的容量。
2、HashMap的加载因子
加载因子是哈希表在其容量自动扩容之前可以达到多满的一种度量。
3、作用
当哈希表中的条目数超出了加载因子与当前容量的乘积时,则要对该哈希表进行扩容、rehash操作(即重建内部数据结构),扩容后的哈希表容量为原来的两倍。
为了减少冲突的概率,当HashMap的数组长度到了一个临界值就会触发扩容,把所有元素rehash再放到扩容后的容器中,所以说rehash是一个非常耗时的操作。
而这个临界值是由加载因子和当前容器的容量大小来确定:
DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR
即默认情况下是 16x0.75 =12 时,就会触发扩容操作。
/*** 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 1sttreeifyBin(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 keyV oldValue = e.value;if (!onlyIfAbsent || oldValue == null)e.value = value;afterNodeAccess(e);return oldValue;}}++modCount;if (++size > threshold)resize();afterNodeInsertion(evict);return null;}
4、面试高频:为什么加载因子初始化是0.75呢?
- 也是一个综合考虑,如果设置过小如0.5,HashMap 每 put 少量的数据,都要进行一次扩容,而扩容操作会消耗大量的性能。使得空间利用率很低,同时提高了rehash(重建内部数据结构)操作的次数。
- 如果设置过大的话,比如设成1,容量还是16,假设现在数组上已经占用了15个,再要put数据进来,计算数组 index 时,发生 hash碰撞 的概率将达到15/16,这违背了 HashMap 减少 hash碰撞 的原则。同时,这样会减少空间开销,提高空间利用率,但同时会增加查询时间的成本。
- 因此,选择0.75作为默认的加载因子,完全是时间和空间成本上寻求折中的选择。
- 在设置初始容量时应该考虑到映射中所需的条目数及其加载因子,以便最大限度地减少rehash操作次数,所以,一般在使用HashMap时建议根据预估值设置初始容量,减少扩容操作。
4、面试高频:为什么初始容量是16
当容量为2的幂次方时,源码中 n -1 对应的二进制数全为1,这样才能保证它和 key 的 hashcode 做&运算后,能够均匀分布,这样才能减少hash碰撞的次数。至于默认值为什么是16,而不是2 、4、8,或者32、64、1024等,应该就是个折中处理,过小会导致放不下几个元素,就要进行扩容了,而扩容是一个很消耗性能的操作。取值过大的话,无疑会浪费更多的内存空间。因此在日常开发中,如果可以预估HashMap会存入节点的数量,则应该在初始化时,指定其容量。
参考原文:
HashMap容量和负载因子:https://blog.csdn.net/ye17186/article/details/88876417
HashMap中的初始容量和加载因子:https://blog.csdn.net/weixin_44723496/article/details/112387738