HashMap的原理就是用一个数组存储一系列 k-v 结构的 Node 实体,存储的位置通过对 key 的 hashCode 进行计算得到,Node 对象因为有 next 指针,所以可以作为单链表的一个节点。如果发生 hash 冲突,就将新的 Node 插入链表里。链表过长超过阈值的话为了提高查询效率会转为红黑树(JDK 1.8以后)。
成员变量
1 | //默认初始化容量 |
hashMap默认初始化长度是多少?为什么是这么多?
默认初始化长度是 1<<< 4,之所以不直接写16,是因为位运算比算术运算效率高,之所以选择16,是为了服务与 index 的计算公式 index = hash & (length - 1)(这里采用与运算和取模的效果一样,但性能更好),初始化长度减1后为15,二进制位1111,和 hash 值运算后的结果等同于 hash 后几位的值,只要 hash 值本身分布均匀,那 hash 算法的结果就是均匀的,从而实现均匀分布。
为什么树化标准是8个?
在源码注释中有提到:如果 hashCode 分布良好的话,很少出现链表比较长的情况。理想情况下,链表的长度符合泊松分布。各长度的概率如下
1 | /* 0: 0.60653066 |
当链表长度为8时,概率为0.00000006,概率非常小,红黑树的转换基本不会发生。当然也会有用到:用户使用自己的 hash 算法,导致 hashCode 分布离散很差,链表很长的情况。
为什么树退化为链表的阈值是6个?
增加一个过渡,防止在临界值时增加/删除一个元素时,在树和链表之间频繁的转换,降低性能。
初始化方法
1 | // 最常用的一个,负载因子0.75 |
hash方法
hash() 方法是 HashMap 的核心方法,算出 key 的 hashCode 后,将算出的结果右移16位,与原 hashCode 按位异或得到 hash 后的值,
1 | static final int hash(Object key) { |
为什么要右移16位?
让 hashCode 的高16位也参与运算,增加扰动,减少碰撞冲突,因为大部分元素的 hashCode 在低位是相同的。
相关问题:String的hashCode的实现?
1 | public int hashCode() { |
简单来说就是给字符串的每一位,按位乘31的n次幂,再相加,用自然溢出来等效取模。选择31因为这个质数不大不小,且可以被虚拟机优化,运算效率更高。i*31 = i*(32-1) = i<<5-1
put方法
1 | public V put(K key, V value) { |
在 putVal 方法中为什么要用 i = (n-1) & hash
作为索引运算呢?
这其实是种优化手段,由于数组的大小永远是一个2次幂,在扩容后,一个元素的新索引要么在原位置,要么在原位置+扩容前的容量。这个方法的巧妙处全在于&运算,&运算只会关注 n-1(n=数组长度)的有效位,当扩容后,n的有效位相比之前会多增加一位(n会变成之前的两倍,所以确保数组长度永远是2次幂很重要),然后只需要判断 hash 在新增的有效位的位置是 0 还是 1 就可以算出新的索引位置,如果是 0,如果是 2,索引就是原索引+扩容前的容量。
resize方法
resize()方法主要是用来扩容,具体操作是新建一个hash表,然后将旧表中的内容重新散列复制到新表中。源码主要分为2部分:
- 第一部分主要用于判断当前操作的类型(初始化or扩容)并且计算出新生成的表的容量和阈值。
- 第二部分只用于扩容操作时,将旧表中的元素重新散列放入新表。
1 | //扩容方法 |
get方法
get 方法逻辑比较简单,直接看注释
1 | public V get(Object key) { |
remove方法
1 | public V remove(Object key) { |