HashMap 底层数据结构 JDK1.7 及之前:数组 + 链表 JDK1.8:数组 + 链表 + 红黑树 HashMap 添加元素分析 hash 算法:当添加元素时,会通过 哈希值
和 数组长度
计算下标来准确定位该元素应该 put
的位置,通常我们为了使元素时分布均匀会使用取模运算,用一个值去模上总长度,例如:index = hash % arr.length
(实际采用的与运算 hash & (length-1)
取代取模运算 hash % length
,因为这两种方式记算出来的结果是一致的,也就是hash & (length-1) = hash % length
),计算这个索引位置的算法就是 hash 算法
计算出 index 后,就会将该元素添加进去,理想状态下是将每个值都均匀的添加到数组中,但问题是不可能达到这样的理想状态,如果某个元素通过计算产生的索引位置已经有其他元素了,这时候就会产生哈希冲突,此时,就产生了第二种数据结构——链表,冲突的元素会在该索引处以链表的形式保存
但是当链表的长度过长时,其固有弊端就显现出来了,即查询效率较低,时间复杂度可能会达到 O(n)
级别,而数组的查询时间复杂度仅为 O(1)
此时,就引出了第三种数据结构——红黑树,红黑树是一棵接近于平衡的二叉树,其查询时间复杂度为 O(logn)
,远远比链表的查询效率高。但如果链表长度不到一定的阈值,直接使用红黑树代替链表也是不行的,因为红黑树的自身维护的代价也是比较高的,每插入一个元素都可能打破红黑树的平衡性,这就需要每时每刻对红黑树再平衡(左旋、右旋、重新着色)
jdk1.8 以前 HashMap 的实现是数组+链表,即使哈希函数取得再好,也很难达到元素百分百均匀分布。当 HashMap 中有大量的元素都存放到同一个桶中时,这个桶下有一条长长的链表,这个时候 HashMap 就相当于一个单链表,假如单链表有 n 个元素,遍历的时间复杂度就是 O(n)
,完全失去了它的优势。
针对这种情况,jdk1.8 中引入了红黑树(查找时间复杂度为 O(logn)
)来优化这个问题。当链表长度很小的时候,即使遍历,速度也非常快,但是当链表长度不断变长,肯定会对查询性能有一定的影响,所以才需要转成树。
HashMap 继承关系 1 2 public class HashMap <K ,V > extends AbstractMap <K ,V > implements Map <K ,V >, Cloneable , Serializable {
说明
Cloneable 空接口,表示可以克隆。创建并返回 HashMap 对象的一个副本
Serializable 序列化接口。属于标记性接口。HashMap 对象可以被序列化和反序列化
AbstractMap 父类提供了 Map 实现接口。以最大限度地减少实现此接口所需的工作
HashMap 的主要属性 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 private static final long serialVersionUID = 362498820763181265L ;static final int DEFAULT_INITIAL_CAPACITY = 1 << 4 ;static final int MAXIMUM_CAPACITY = 1 << 30 ;static final float DEFAULT_LOAD_FACTOR = 0.75f ;static final int TREEIFY_THRESHOLD = 8 ;static final int MIN_TREEIFY_CAPACITY = 64 ;static final int UNTREEIFY_THRESHOLD = 6 ;transient Node<K,V>[] table;transient Set<Map.Entry<K,V>> entrySet;transient int size;transient int modCount;int threshold;final float loadFactor;
构造方法 构造一个空的 HashMap,默认初始容量(16)和默认负载因子(0.75)
1 2 3 4 5 6 7 public HashMap () { this .loadFactor = DEFAULT_LOAD_FACTOR; }
构造一个具有指定的初始容量 和默认负载因子(0.75)HashMap 。
1 2 3 4 5 6 7 8 9 10 public HashMap (int initialCapacity) { this (initialCapacity, DEFAULT_LOAD_FACTOR); }
构造一个具有指定的初始容量和负载因子 的 HashMap。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 public HashMap (int initialCapacity, float loadFactor) { if (initialCapacity < 0 ) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); this .loadFactor = loadFactor; this .threshold = tableSizeFor(initialCapacity); } 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 ; }
调用 putMapEntries()
方法将目标 Map 拷贝到当前 Map
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 public HashMap (Map<? extends K, ? extends V> m) { this .loadFactor = DEFAULT_LOAD_FACTOR; putMapEntries(m, false ); } final void putMapEntries (Map<? extends K, ? extends V> m, boolean evict) { int s = m.size(); if (s > 0 ) { if (table == null ) { float ft = ((float )s / loadFactor) + 1.0F ; int t = ((ft < (float )MAXIMUM_CAPACITY) ? (int )ft : MAXIMUM_CAPACITY); if (t > threshold) threshold = tableSizeFor(t); } 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); } } }
关于 float ft = ((float)s / loadFactor) + 1.0F;
oldMap 使用的无参构造器(会使用到 HashMap 的默认值),因此容量是 16,装载因子是 0.75,阈值是 12。而 newMap 使用的拷贝构造器,而后会调用 putMapEntries
,因为传入 map 的 size 是 12,那么 12 / 0.75=16, 16 + 1 = 17, tableSizeFor(17) = 32,因此最终形成 newMap 的容量是 32,阈值是 24。 坏处:newMap 的 size 明明和 oldMap 的 size 同样,可是其容量和阈值都是 oldMap 的二倍了(变成了原有的二倍)。 可能你会想,这是为了保护另外一种特例,这种特例若是不作加 1 操做,就会致使分配的容量不够大,因此我这种特例就得牺牲一下了。但你就算把 s 的值(传入 map 的 size)从 6 试到 12,也会发现,就算不作加 1 操做,分配的容量也会够大。 其实咱们忽略了一点,就是 loadfactor 可能会被用户给一个奇怪的小数,由于在 HashMap 里,容量必须为 2 的 n 次幂,且默认的 loadfactor 又是 0.75,因此算出来的阈值确定是整数了。若是用户给一个小数,使得 capacity * loadfactor = 小数,那么这个阈值必须向下取整 (反过来想,若是阈值向上取整,那岂不是使得 size 可能会大于了真正的 threshold 而不用 resize,详见resize()
函数的newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE);
,这里的(int)ft
就是向下取整)。因此反过来想,用 size / loadfactor 算出的 capacity,确定也要向上取整了。 成员方法 增加方法 put() put 方法是比较复杂的,实现步骤大致如下:
先通过 hash 值计算出 key 映射到哪个桶;
如果桶上没有碰撞冲突,则直接插入;
如果出现碰撞冲突了,则需要处理冲突:
如果该桶使用红黑树处理冲突,则调用红黑树的方法插入数据; 否则采用传统的链式方法插入。如果链的长度达到临界值,则把链转变为红黑树; 如果桶中存在重复的键,则为该键替换新值 value;
如果 size 大于阈值 threshold,则进行扩容;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 public V put (K key, V value) { return putVal(hash(key), key, value, false , true ); } static final int hash (Object key) { int h; return (key == null ) ? 0 : (h = key.hashCode()) ^ (h >>> 16 ); } 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 ) treeifyBin(tab, hash); break ; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break ; p = e; } } if (e != null ) { V oldValue = e.value; if (!onlyIfAbsent || oldValue == null ) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict); return null ; }
扩容方法 resize() 下图为 16 扩充为 32 的 resize 示意图
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 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; } else if ((newCap = oldCap << 1 ) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1 ; } else if (oldThr > 0 ) newCap = oldThr; else { 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; if (oldTab != null ) { 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; } 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; }
删除方法 remove() 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 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; 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)))) node = p; else if ((e = p.next) != 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; } while ((e = e.next) != null ); } } 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; else p.next = node.next; ++modCount; --size; afterNodeRemoval(node); return node; } } return null ; }
查找方法 get() 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 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; if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1 ) & hash]) != null ) { if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k)))) return first; if ((e = first.next) != null ) { 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 ; }
总结 1、hash 函数 hash 函数,即散列函数,或叫哈希函数。它可以将不定长的输入,通过散列算法转换成一个定长的输出,这个输出就是散列值。 需要注意的是,不同的输入通过散列函数,也可能会得到同一个散列值。因此我们不能使用散列函数来获取唯一值。
2、HashMap 为什么要使用 hash 函数 Java 的 HashMap 中使用的是数组 + 链表的结构,但在保存时,一个 K - V 键值对如果按照存入顺序存放。那么在取值时需要遍历整个数组,然后一个个去比较它们的 key 是否相等,这对于性能的损耗无疑是很大的。所以通过 hash 函数来优化。
3、HashMap 中 hash 函数是怎么实现的?还有哪些 hash 函数的实现方式? 1 2 3 4 static final int hash (Object key) { int h; return (key == null ) ? 0 : (h = key.hashCode()) ^ (h >>> 16 ); }
对于 key 的 hashCode 做 hash 操作:无符号右移 16 位然后做异或运算。
无符号右移 16 位然后做异或运算的目的?
如果当 n 即数组长度很小,假设是 16 的话,那么 n - 1 即为 1111 ,这样的值和 hash 直接做按位与操作,实际上只使用了哈希值的后 4 位。如果当哈希值的高位变化很大,低位变化很小,这样就很容易造成哈希冲突了,所以这里把高低位都利用起来,从而解决了这个问题
除此以外还有平方取中法,伪随机数法和取余数法。这三种效率都比较低。而无符号右移 16 位 异或运算(^)效率是最高的
按位异或运算(^):相同的二进制数位上,都是 1 的时候,结果为 1,否则为 0
4、hash 算法【确定数组索引 index】 通过 hash() 方法得到 hash 函数 后,返回的是一个返回 int 类型的散列值,如果直接拿散列值作为下标访问 HashMap 主数组的话,考虑到 2 进制 32 位带符号的 int 表值范围从 -2147483648
到 2147483648
,前后加起来大概 42 亿的映射空间。只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。但是一个 40 亿长度的数组,内存是放不下的。所以这个散列值是不能直接拿来用的。用之前还要先做对数组的长度取模运算,得到的余数才能用来访问数组索引 index。
1 2 3 4 5 6 7 8 9 10 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 );
就是将 散列值 和 数组长度-1 进行与运算
按位与运算(&):相同的二进制数位上,都是 1 的时候,结果为 1,否则为 0
5、为什么要用 i = (n - 1) & hash
作为索引运算呢? 这其实是一种优化手段,由于数组的大小永远是一个 2 次幂,在扩容之后,一个元素的新索引要么是在原位置,要么就是在原位置加上扩容前的容量。这个方法的巧妙之处全在于&运算,&运算只会关注 n – 1(n =数组长度)的有效位
当扩容之后,n 的有效位相比之前会多增加一位(n 会变成之前的二倍,所以确保数组长度永远是 2 次幂很重要),然后只需要判断 hash 在新增的有效位的位置是 0 还是 1 就可以算出新的索引位置,如果是 0,那么索引没有发生变化,如果是 1,索引就为原索引加上扩容前的容量。
通过位运算,在每次扩容时都不用重新计算 hash,省去了不少时间,而且新增有效位是 0 还是 1 是带有随机性的,之前两个碰撞的 Entry 又有可能在扩容时再次均匀地散布开,达到较好的分布离散效果。
举个例子,数组长度为 16,经过扩容后变为 32
0001 1111 —>31
和上面图中的 hash 进行与运算
0001 0101
结果为 21,即索引为原索引 5 加上扩容前的容量 16=21
0001 0101
5、HashMap 为什么数组的大小是 2 的指数次幂,如果输入值不是 2 的幂比如 10 会怎么样? 为了提高效率,方便&运算
根据一开始提到的,hash 算法实际就是取模操作:hash % length
,因为计算机中直接求余效率不如位移运算
计算机的运行效率:加法(减法)>乘法>除法>取模,取模的效率是最低的。所以我们要在 HashMap 中避免频繁的取模运算,又因为在我们 HashMap 中他要通过取模去定位我们的索引,并且 HashMap 是在不停的扩容,数组一旦达到容量的阈值的时候就需要对数组进行扩容。那么扩容就意味着要进行数组的移动,数组一旦移动,每移动一次就要重回记算索引,这个过程中牵扯大量元素的迁移,就会大大影响效率。那么如果说我们直接使用与运算,这个效率是远远高于取模运算的
所以源码中做了优化,使用 hash & (length - 1)
而上面提到过 hash % length
等于 hash & ( length - 1)
,前提是 length 是 2 的 n 次幂
怎么确保数组大小一定是 2 的指数次幂
HashMap 构造方法可以指定集合的初始化容量大小(默认 16)
1 2 3 public HashMap (int initialCapacity) { this (initialCapacity, DEFAULT_LOAD_FACTOR); }
而在实例化 HashMap 实例时,如果给定了 initialCapacity
下面这个方法用于找到 大于等于 initialCapacity 的最小的 2 的幂
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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 ; }
_|=_: a|=b 就等于 a = a | b,按位或。两个二进制对应位都为 0 时,结果等于 0,否则结果等于 1
完整例子
比如输入的 cap 值为 10,最后得到的结果为 16
6、当两个对象的 hashCode 相等时会怎么样? 会产生哈希碰撞。若 key 值内容相同则替换旧的 value,不然连接到链表后面,链表长度超过阈值 8 就转换为红黑树存储。
7、什么是哈希碰撞,如何解决哈希碰撞? 只要两个元素的 key 计算的哈希码值相同就会发生哈希碰撞。jdk8 之前使用链表解决哈希碰撞。jdk8 之后使用链表 + 红黑树解决哈希碰撞。
8、如果两个键的 hashCode 相同,如何存储键值对? 通过 equals 比较内容是否相同。相同:则新的 value 覆盖之前的 value。不相同:则将新的键值对添加到哈希表中。
9、为什么选择红黑树而不用其他树,比如二叉查找树 二叉查找树在特殊情况下也会变成一条线性结构,和原先的链表存在一样的深度遍历问题,查找性能就会慢
使用红黑树主要是提升查找数据的速度,红黑树是平衡二叉树的一种,插入新数据后会通过左旋,右旋、变色等操作来保持平衡,解决单链表查询深度的问题
10、为什么不一直开始就用红黑树 数据量少的时候操作数据,遍历线性表比红黑树所消耗的资源少,且前期数据少。平衡二叉树保持平衡是需要消耗资源的,所以前期采用线性表,等到一定数之后变换到红黑树
这其实是基于空间和时间平衡的考虑 ,JDK 的源码里已经对这个问题做了解释:
单个 TreeNode 需要占用的空间大约是普通 Node 的两倍,所以只有当包含足够多的 Nodes 时才会转成 TreeNodes,这个足够多的标准就是由 TREEIFY_THRESHOLD 的值(默认值 8)决定的。而当桶中节点数由于移除或者 resize (扩容) 变少后,红黑树会转变为普通的链表,这个阈值是 UNTREEIFY_THRESHOLD(默认值 6)。
11、为什么树化标准是 8 个 在源码中,上面那段笔记后面还有一段较长的注释,我们可以从那一段注释中找到答案,原文是这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
大概意思就是:如果 hashCode 的分布离散良好的话,那么红黑树是很少会被用到的,因为各个值都均匀分布,很少出现链表很长的情况。在理想情况下,链表长度符合泊松分布,各个长度的命中概率依次递减,注释中给我们展示了 1-8 长度的具体命中概率,当长度为 8 的时候,概率概率仅为 0.00000006,这么小的概率,HashMap 的红黑树转换几乎不会发生,因为我们日常使用不会存储那么多的数据,当然,这是理想的算法,如果用户重写了hashCode ,并且算法实现比较差的话,就很可能会使 hashCode 分布离散很差,这个时候再转换为红黑树就是一种很好的退让策略。
总结
一方面遵循泊松分布,链表中元素个数为 8 时的概率已经非常小
另一方面红黑树查找时间复杂度是 log(n),长度为 8 的时候,平均查找长度大约为 2.83(取 3),如果继续使用链表(链表的平均查找长度为 n/2),平均查找长度为 8/2=4,这才有转换为树的必要 。链表长度如果是小于等于 6,6/2=3,,而 log(6) = 2.6,虽然速度也很快的,但是链表和红黑树之间的转换也很耗时
12、为什么退化为链表的阈值是 6 上面说到,当链表长度达到阈值 8 的时候会转为红黑树,但是红黑树退化为链表的阈值却是 6,为什么不是小于 8 就退化呢?比如说 7 的时候就退化,偏偏要小于或等于 6?
主要是一个过渡,避免链表和红黑树之间频繁的转换。如果阈值是 7 的话,删除一个元素红黑树就必须退化为链表,增加一个元素就必须树化,来回不断的转换结构无疑会降低性能,所以阈值才不设置的那么临界。
13、为什么 HashMap 的加载因子要设置为 0.75 加载因子如果定的太大,比如 1,这就意味着数组的每个空位都需要填满,如果一直等数组填满才扩容,虽然达到了最大的数组空间利用率,但会产生大量的哈希碰撞,同时产生更多的链表,显然不符合我们的需求。
但如果设置的过小,比如 0.5,这样一来保证了数组空间很充足,减少了哈希碰撞,这种情况下查询效率很高,但消耗了大量空间。
因此,我们就需要在时间和空间上做一个折中,选择最合适的负载因子以保证最优化,取到了 0.75
当 HashMap 里面容纳的元素已经达到 HashMap 数组长度的 75% 时,表示 HashMap 太挤了,需要扩容,而扩容这个过程涉及到 rehash、复制数据等操作,非常消耗性能。所以开发中尽量减少扩容的次数,可以通过创建 HashMap 集合对象时指定初始容量来减少扩容。