hashMap 知识点
HashMap是什么?
HashMap是用来存储key-value键值对的集合类 它具有存储效率高,查询速度快的特点。
HashMap的底层实现原理是什么?
HashMap底层是基于(数组+链表+红黑树)结构来实现的,数组元素采用Node节点来保存key-value键值对的信息。
HashMap的默认初始容量为16 默认负载因子是0.75 最大容量是2^30
主要方法有put get
1.在put key-value时 如果哈希表没有初始化先进行初始化, 再根据key.HashCode计算出hash值 然后通过hash值和数组长度计算出相对应的数组下标
2.如果该下标对应位置的元素是null, 则直接将Node存入该位置,如果数组元素不为null 取得链表(红黑树)的头节点的key值 如果头节点的key等于put的key值 则直接更新该节点的value值
如果不等于 继续向后查找 如果链表中存在key值 则做覆盖 不存在则插入链表的尾部
3.当链表节点数量大于8时且数组长度大于等于64时 改用红黑树进行存储节 当节点数量小于6时改回链表存储。
4.插入完成后 如果当前节点数size>当前的阈值 则对数组进行扩容 扩容至当前容量的2倍 扩容后对数组里的元素进行重hash
5.实例化hashmap时 建议指定初始容量(kv键值对数量 / 0.75) 这样可以减少扩容重hash带来的性能消耗
hashmap 是非线程安全的 如需要线程安全可以使用 concurrentHashMap
为什么扩容是根据2的n次幂进行扩容
为了降低hash碰撞的几率
数组下标的计算方式 是根据Node的hash值 和 数组长度- 1做或运算 而2的n次幂-1 二级制就是全是1111 和hash值运算的结果 就是取hash值的后n位
只要hash值分布均匀 计算出来的下标也是均匀分布的
为什么默认负载因子是0.75?
负载因子过大 hash碰撞概率增大 导致链表长度增加 查询时间成本增加 从而导致性能降低
负载因子过小 会导致空间浪费
通过计算 负载因子为0.75 能够很好节省时间成本和空间成本 时间和空间的权衡
为什么线程不安全?
1.例如:线程A执行put数据操作,已找到需要存储的数组位置,且该位置元素为null, 此时线程A时间片用完,线程B开始执行put数据操作 且两个线程put的数据正好在数组的同一个位置,
B执行插入完成,A接着执行完成 结果是线程Aput的数据会把线程Bput的数据给覆盖掉 正确应该两个线程put的数据都存在哈希表中
2.头插法 resize时会出现死循环 线程A B 都对同一链表进行rehash时 线程A 取到头节点m next是节点n 此时线程时间片用完 线程B 整个rehahs操作执行完后的 链表是 头节点变成n next 是m
线程A 继续执行 m的next是n n的next又是m 这样就死循环了
为什么最大容量是为什么是2的30次幂? 1<<30
因为int类型的是4个字节 32位 而最高位用来区分正负数 如果把1左移31位的话 最高位就变成1 了 这个值就成了负数了 所以只能左移30位
jdk1.7和1.8有什么区别?
1.底层结构不一样 1.7使用的数组+链表 1.8使用的数组+链表+红黑树 (链表长度>8 时转红黑树存储)
2.hash冲突时,插入节点方式不同 1.7使用头插法 1.8使用尾插法 (头插入 多线程同时rehash时会出现死循环)
3.hash函数算法不一样 1.7节点的hash直接使用key的hashCoe 4次位移 5次异或 1.8 使用key的hashCode 和 hashCode右移16 异或的结果
使hashcode的高位也参与了运算 使元素更加均匀分布
4.扩容顺序不同 1.7是在插入数据前进行扩容 1.8是在插入数据后再进行扩容
5.扩容重哈希后链表节点顺序不一样 1.7会颠倒链表顺序 1.8 会保持原来的顺序
6.重哈希时 数组下标计算方式不一样 1.7 hash值(扩容后容量-1) 或 运算 1.8 直接hash&扩容前容量==0 当前下标 + 扩容前容量值
7.1.7 初始化table 调用inflateTable() 1.8 初始化table 放在扩容函数 resize 里
什么时候进行resize操作?
有两种情况会进行resize:1、初始化table;2、在size超过threshold之后进行扩容
节点在转移的过程中是一个个节点复制还是一串一串的转移?
遍历链表 把扩容后还出于同一下标的节点重新组合成一条链表,再把头链表放入新的数组下标中
从源码中我们可以看出,扩容时是先找到拆分后处于同一个桶的节点,将这些节点连接好,然后把头节点存入桶中即可
扩容算法(扩容至大于等于n的2的m次幂) 大于阈值(当前容量 * 负载因子) 时扩容
n -= 1
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
链表hash值的计算
key.hashCode() ^ key.hashCode() >>> 16
在计算数组下标时只用到了hash值的低位 为了使数组下标更加散列 所以把keyHashCode低位跟高位进行异或运算 一般来说数组长度小于2^16 所以位移16位
因为使用 & 结果偏向 0 使用 | 结果偏向 1 所以使用^ (异或)
数组下标的计算 (hash 就是 任意长度的数据映射到有限长度的域上)
hash & (length - 1) 即 hash % length 根据数组长度对node的hash值进行取余 使其散列 在数组内
红黑树 红黑树是一个平衡二叉树 但不是一个完美的平衡二叉树 在动态插入保持树的完美平衡代价太高了 我们希望查找时间复杂度为logN
1.节点非红即黑
2.根节点是黑色
3.每个叶子节点(nil)是黑色
4.红色节点的子节点必须是黑色(不能存在连续两个红节点)
5.任一节点到其叶子节点路径 包含相同的黑色节点数