HashMap
- HashMap
- 使用分析
- 特点
- 键值存储
- 无序存储
- 动态扩容
- 允许空键和空值
- 快速查找和插入
- 不是线程安全的
- 使用 2 的幂作为哈希表的大小
- 使用场景
- 单线程下的无序的键值存储集合,如果要有序可以用TreeMap,如果要多线程可以用ConcurrentHashMap
- 特点
- 设计分析
- 设计目标
- 单线程下的键值存储集合
- 设计思路
- 基于数据拉链和红黑树的结构实现
- 哈希算法的应用确定key再数组中的下标
- 动态扩容机制:
- 单线程场景
- 存储内容
- Key可以为null, 但 null 作为键只能有一个
- value可以为null,但null 作为值可以有多个
- 快速失败机制fail- fast
- 在增加删除操作时modcount增加
- 在获取迭代器遍历时,迭代器保存了ArrayList当前modcoun值,在迭代器每次执行next时先比较保存的modcoun和ArrayList当前的modcoun不同时,抛出异常ConcurrentModificationException
- https://www.cnblogs.com/ldq2016/p/18751225
- 设计模式
- 工厂方法模式
- 在 HashMap 中的体现:HashMap 中的 newNode 方法可看作是工厂方法。当需要创建新的节点(Node)时,并没有直接使用 new 关键字创建,而是通过调用 newNode 方法。这为后续扩展节点类型提供了便利,例如在 LinkedHashMap 中重写了 newNode 方法,创建了 LinkedHashMap.Entry 类型的节点,实现了不同的功能(如维护插入顺序)。
- 迭代器模式
- 在 HashMap 中的体现:HashMap 实现了 Iterable 接口,提供了 iterator() 方法来获取迭代器。通过迭代器,用户可以方便地遍历 HashMap 中的元素,而无需了解其内部存储结构(如数组、链表、红黑树)。例如 KeyIterator、ValueIterator 和 EntryIterator 分别用于遍历键、值和键值对。
- 策略模式
- 在 HashMap 中的体现:HashMap 中的哈希函数可以看作是一种策略。不同的键类型可能需要不同的哈希计算方式,但 HashMap 通过 hash() 方法将这种计算策略封装起来,用户可以使用默认的哈希计算方式,也可以通过自定义键的 hashCode() 方法来改变哈希计算策略。
- 模板方法模式
- 在 HashMap 中的体现:HashMap 中的 putVal 方法定义了插入元素的基本流程,包括计算哈希值、定位桶位置、处理哈希冲突等步骤,但其中一些步骤(如 afterNodeAccess、afterNodeInsertion)在 HashMap 中只是空实现,在 LinkedHashMap 等子类中进行了具体实现,用于实现额外的功能(如维护访问顺序)。
- 组合模式
- 概念:将对象组合成树形结构以表示 “部分 - 整体” 的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性。 在 HashMap 中的体现:HashMap 内部使用数组存储节点,当发生哈希冲突时,会以链表或红黑树的形式存储多个节点。从整体上看,HashMap 是一个容器,其中的数组、链表和红黑树可以看作是不同层次的组合结构,用户可以将它们统一视为存储元素的整体来进行操作。
- 工厂方法模式
- 设计目标
- 源码分析 jdk1.8
- 继承
- public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
- public class HashMap<K,V> extends AbstractMap<K,V>
- 数据结构
- jdk1.8 数组+链表+红黑树
- 数组:数组里存储的是链表节点的头对象Node
- 链表:当有多个键值数据要存储到一个下标时,将新的数据使用尾插入,插入链表
- 红黑树:
- 当数组长度大于或等于64,并且某个下标里的链表长度大于等于8个时,将链表结构改为红黑树结构,提高查找效率
- 当红黑树节点小于或等于6个时,将红黑树转化为链表
- jdk1.8 数组+链表+红黑树
- 确定键值在数组中的下标
- key获取hashcode
- key如果是null 返回为0
- key如果不是null 返回key.hashCode()
- hashcode进行扰动处理:
- 扰动的目的是为了减少哈希碰撞
- 将hashcode和hashcode的低16位进行异或处理
- 让和数组容量与hashCode进行取模运算时,只用到了hashcode的低16位,所以要通过hash扰动处理让低16位同时具有原本hashCode低16位和高16位的特点,来减少哈希碰撞
- 将扰动后的hashcode和数组最大下标进行取模运算得到要插入的下标
- 计算方式:(tab.length - 1)& hash
- 代码
- key获取hashcode
- 容量
- initialCapacity 初始容量(默认16)
- 构建时没有指定容量Size ,使用16长度
- 构建时指定容量Size小于16 ,使用16
- 构建时指定容量Size大于16 ,使用大于等于size的最小2次幂
- 构建时指定容量Size大于MAXIMUM_CAPACITY( 2 的 30 次方) ,使用MAXIMUM_CAPACITY( 2 的 30 次方)
- hashmap的最大容量为MAXIMUM_CAPACITY( 2 的 30 次方)
- 如果旧容量已经达到 HashMap 支持的最大容量 MAXIMUM_CAPACITY( 2 的 30 次方),不再扩容,只将阈值 threshold调整为 Integer.MAX_VALUE(2 的 31 次方 - 1)
- threshold 阀值:
- map保存的键值个数大于阀值时开始扩容
- 计算方式:数组长度*加载因子 threshold=initialCapacity*loadFactor
- threshold 阈值:hashMap所能容纳的最大键值对数量,如果超过则需要扩容,计算方式:threshold=initialCapacity*loadFactor(构造方法中直接通过tableSizeFor(initialCapacity)方法进行了赋值,主要原因是在构造方法中,数组table并没有初始化,put方法中进行初始化,同时put方法中也会对threshold进行重新赋值,这个会在后面的源码中进行分析)
- loadFactor 加载因子(默认0.75)
- 加载因子越大,空间利用率越高,但是查找效率越低,因为hash碰撞概率就越高,查找时遇到的链表越多越长,导致查找效率降低
- 加载因子越小,hash碰撞概率就越低,查找时遇到的链表越少越短,查找效率越高,但是会导致空间利用率降低
- 考虑空间利用率和查找效率的平衡,结合大量数据实践和分析,选出0.75作为一个平衡点
- 扩容倍数为2倍
- HashMap 通常使用 2 的幂作为数组的长度
- 扩容
- 创建新数组
- 原长度为0时,新长度为16,阀值为16*0.75
- 原长度为MAXIMUM_CAPACITY( 2 的 30 次方)时,新长度为MAXIMUM_CAPACITY( 2 的 30 次方),阀值为Integer.MAX_VALUE(2 的 31 次方 - 1)
- 其他情况,长度为2倍原长度,阀值为新长度*0.75
- 将老数据移动到新数据,链表使用头插法
- 移动数据到新数组
- 在 JDK 8 的数组长度为2的幂次和hash 扰动算法下,数组扩容2倍后的索引位置,要么就是原来的索引位置,要么就是“原索引+原来的容量”,遵循一定的规律。
- jdk1.8链表在插入新数据时用的尾部插法,jdk1.7用的是头插法
- 头插法会导致,在扩容后,链表里的数据顺序颠倒
- 如果节点Node的next == null,直接放在新数组中
- 如果节点Node是树节点,将树分裂成两个树,如果分裂后的树节点小于等于6个则将树转换为链表结构
- 其他情况,将原列表分列成两个链表
- 低位链表在新数组的下标为:原来的索引位置
- 高位链表在新数组的下标为:原索引+原来的容量
- 创建新数组
- initialCapacity 初始容量(默认16)
- 快速失败机制fail- fast
- 在增加删除操作时modcount增加
- 在获取迭代器遍历时,迭代器保存了ArrayList当前modcoun值,在迭代器每次执行next时先比较保存的modcoun和ArrayList当前的modcoun不同时,抛出异常ConcurrentModificationException
- https://www.cnblogs.com/ldq2016/p/18751225
- 源码文章
- https://javabetter.cn/collection/hashmap.htm
- https://javaguide.cn/java/collection/hashmap-source-code.html
- 继承
- 面试问题
- jdk1.7 在多线程下扩容因为使用头插法可能出现死循环,jdk1.8修复,改为尾插法
- 多线程下 put 会导致元素丢失
- put 和 get 并发时会导致 get 到 null
- 线程 1 执行 put 时,因为元素个数超出阈值而导致出现扩容,线程 2 此时执行 get,就有可能出现这个问题。
- 使用分析
作者: 一点点征服
出处:http://www.cnblogs.com/ldq2016/
本文版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接,否则保留追究法律责任的权利