HashMap底层的数据结构是什么?
数组+链表 转红黑树
HashMap是如何进行扩容的?
HashMap的默认负载因子:
/**
* The load factor used when none specified in constructor.
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
*默认的负载因子是0.75f,也就是75% 负载因子的作用就是计算扩容阈值用,比如说使用
*无参构造方法创建的HashMap 对象,他初始长度默认是16 阈值 = 当前长度 * 0.75 就
*能算出阈值,当当前长度大于等于阈值的时候HashMap就会进行自动扩容
*/
HashMap的扩容机制:
阈值(threshold) = 负载因子(loadFactor) x 容量(capacity)
当HashMap中table数组(也称为桶)长度 >= 阈值(threshold) 就会自动进行扩容。
因为table数组长度必须是2的次方数,扩容其实每次都是按照上一次tableSize位运算得到的就是做一次左移1位运算,
假设当前tableSize是16的话 16转为二进制再向左移一位就得到了32 即 16 << 1 == 32 即扩容后的容量,也就是说扩容后的容量是当前
容量的两倍,但记住HashMap的扩容是采用当前容量向左位移一位(newtableSize = tableSize << 1)详细查看 resize(),得到的扩容后容量,而不是当前容量x2
为什么HashMap的默认负载因子是0.75,而不是0.5或者是整数1呢?
阈值(threshold) = 负载因子(loadFactor) x 容量(capacity) 根据HashMap的扩容机制,他会保证容量(capacity)的值永远都是2的幂 为了保证负载因子x容量的结果是一个整数,这个值是0.75(4/3)比较合理,因为这个数和任何2的次幂乘积结果都是整数。
为什么计算扩容后容量要采用位移运算呢,怎么不直接乘以2呢?
位移运算在特定操作下是最快的运算方法,因为cpu毕竟它不支持乘法运算,所有的乘法运算它最终都是再指令层面转化为了加法实现的,这样效率很低,如果用位运算的话对cpu来说就非常的简洁高效。
为什么要转红黑树?
当链表长度超过阈值(8)时,将链表转换为红黑树,
链表查找元素的时间复杂度为 O(n),远远大于红黑树的 O(logn),尤其是在节点越来越多的情况下,O(logn) 体现出的优势会更加明显;简而言之就是为了提升查询的效率。
为什么不一开始就用红黑树?
JDK 的源码注释中已经对这个问题作了解释:
单个 TreeNode 需要占用的空间大约是普通 Node 的两倍,所以只有当包含足够多的 Nodes 时才会转成 TreeNodes,而是否足够多就是由 TREEIFY_THRESHOLD 的值(默认值8)决定的。
而当桶中节点数由于移除或者 resize 变少后,又会变回普通的链表的形式,以便节省空间,这个阈值是 UNTREEIFY_THRESHOLD(默认值6)。
转换阈值 8 是怎么来的?
在源码中也对选择 8 这个数字做了说明:
链表长度超过 8 就转为红黑树的设计,更多的是为了防止用户自己实现了不好的哈希算法时导致链表过长,从而导致查询效率低,而此时转为红黑树更多的是一种保底策略,用来保证极端情况下查询的效率。
如果 hashCode 分布良好,也就是 hash 计算的结果离散好的话,那么红黑树这种形式是很少会被用到的,因为各个值都均匀分布,很少出现链表很长的情况。在理想情况下,链表长度符合泊松分布,
各个长度的命中概率依次递减,当长度为 8 的时候,概率仅为 0.00000006。这是一个小于千万分之一的概率,通常我们的 Map 里面是不会存储这么多的数据的,
所以通常情况下,并不会发生从链表向红黑树的转换。