HashMap笔记
-
哈希表/散列表 的底层是Node类型的数组。
Node节点存储hash、键、值与指针,方便形成链表/红黑树。transient Node<K,V>[] table; static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; Node<K,V> next; ... }
-
确定哈希桶数组索引的位置:
-
计算哈希值:
将key的hashCode的高16位保留,低16位用原高16为与原低16位进行二次哈希,防止hashCode不合理而导致分布不均匀。
例如:static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
-
计算索引位置
为了将hash值分配到哈希桶中,且保证不越界,我们应该对hash值取模,此处使用了位运算进行模运算(前提是哈希桶的数目,即table数组长度必须是 2n 。//其中(n - 1) & hash = n % hash final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { ... p = tab[i = (n - 1) & hash] ... }
-
-
哈希冲突的解决方法:拉链法
如果计算得到的hash索引相同,则出现哈希冲突,HashMap利用拉链法解决冲突,即将同一个哈希桶内的元素用链表连起来;当当前桶内的元素过多(链表长度大于8)时,会将链表转换为红黑树,从而将在同一桶内遍历查找元素的时间复杂度从O(n)降低为O(logn)。 -
扩容机制:
- 扩容条件:HashMap不会等到容量用完之后才会扩容,而会在达到阈值时就开始扩容。其中阈值取决于负载因子LoadFactor(默认0.75f)与容量Capacity的乘积。
- 扩容方法:创建一个容量为原容量oldCapacity 2倍的新数组,并将原数组中的元素重新Hash并放入新的桶中。重新Hash的过程即原来的通过位运算取模的过程,由于容量始终是2n, 所以在扩容(扩大两倍为2n+1)后,新计算的hash与旧值只可能在第n位有区别。如果第n位为0,则仍在原来位置的桶中,否则在原位置 + oldCap的桶中。
-
HashMap的容量必须是2的整数次幂
1.因为在计算哈希桶数组索引时,我们使用位运算来进行计算,只有 2n 才有 2n-1 的后 n 位全为1的特性,从而用位运算代替取模。
2.在扩容时,计算新的hash值时,仅需要查看原来的hash值新增的第n位bit是0还是1,省去了重新计算hash值的时间,而且同时,由于新增的1bit是0还是1可以认为是随机的,因此resize的过程,均匀的把之前的冲突的节点分散到新的bucket了。 -
HashMap增加新元素的步骤
1.首先根据key值,通过哈希算法得到value应该放在底层数组中的下标位置。
2.根据这个下标定位到底层数组中的元素,当然,这里可能是链表,也可能是树。
3.拿到当前位置上的key值,与要放入的key比较,是否 == 或者equals,如果成立的话就替换value值,并且需要返回原来的值。
4.如果是树的话就要便利中的节点,继续==和equals的判断,成立替换,否则添加到树里
5.链表的话需要遍历链表,同样的判断,成立。替换,否则就添加到链表的尾部
参考链接: