【博学谷学习记录】超强总结,用心分享|Java集合底层原理分析(2/2)(Map)
目录
一、Java 集合介绍
二、List
2.1 ArrayList
2.2 LinkedList
2.3 Vector
2.4 Stack
2.5 CopyOnWriteArrayList
2.6 CopyOnWriteArraySet
2.7 ArrayList 和 Vector 区别
2.8 ArrayList 与 LinkedList 的区别
三、Map
3.1 HashMap
3.2 HashTable
三、Map
3.1 HashMap
HashMap 是以key-value 键值对形式存储数据,允许 key 为 null(多个则覆盖),也允许 value 为 null。底层结构是数组 + 链表 + 红黑树。
主要属性:
initialCapacity:初始容量,默认 16,2 的 N 次方。
loadFactor:负载因子,默认 0.75,用于扩容。
threshold:阈值,等于 initialCapacity * loadFactor,比如:16 * 0.75 = 12。
size:存放元素的个数,非 Node 数组长度。
Node
//存储元素的数组 transient Node<K,V>[] table; //存放元素的个数,非Node数组长度 transient int size; //记录结构性修改次数,用于快速失败 transient int modCount; //阈值 int threshold; //负载因子,默认0.75,用于扩容 final float loadFactor; /\*\* \* 静态内部类,存储数据的节点 \*/ static class Node\<K,V\> implements Map.Entry\<K,V\> { //节点的hash值 final int hash; //节点的key值 final K key; //节点的value值 V value; //下一个节点的引用 Node<K,V> next; }
**数据结构:**数组 + 单链表,Node 结构:hash|key|value|next
**只允许一个 key 为 Null(多个则覆盖),但允许多个 value 为 Null **
查询、插入、删除效率都高(集成了数组和单链表的特性)
** * 默认的初始化大小为 16,之后每次扩充为原来的 2 倍
线程不安全
使用场景:
快速增删改查
随机存取
缓存
哈希冲突的解决方案:
开放定址法
再散列函数法
链地址法(拉链法,常用)
put() 存储的流程(Java 8):
(1)计算待新增数据 key 的 hash 值;
(2)判断 Node[] 数组是否为空或者数据长度为 0 的情况,则需要进行初始化;
(3)根据 hash 值通过位运算定计算出 Node 数组的下标,判断该数组第一个 Node 节点是否有数据,如果没有数据,则插入新值;
(4)如果有数据,则根据具体情况进行操作,如下:
1.如果该 Node 结点的 key(即链表头结点)与待新增的 key 相等(== 或者 equals),则直接覆盖值,最后返回旧值;
2.如果该结构是树形,则按照树的方式插入新值;
3.如果是链表结构,则判断链表长度是否大于阈值 8,如果 >=8 并且数组长度 >=64 才转为红黑树,如果 >=8 并且数组长度 < 64 则进行扩容;
4.如果不需要转为红黑树,则遍历链表,如果找到 key 和 hash 值同时相等,则进行覆盖返回旧值,如果没有找到,则将新值插入到链表的最后面(尾插法);
5.判断数组长度是否大于阈值,如果是则进入扩容阶段。
resize() 扩容的流程(Java 8):
扩容过程比较复杂, 迁移算法与 Java 7 不一样,Java 8 不需要每个元素都重新计算 hash,迁移过程中元素的位置要么是在原位置,要么是在原位置再移动 2 次幂的位置。
get() 查询的流程(Java 8):
根据 put() 方法的方式计算出数组的下标;
遍历数组下标对应的链表,如果找到 key 和 hash 值同时相等就返回对应的值,否则返回 null。
get() 注意事项:Java 8 没有把 key 为 null 放到数组 table[0] 中。
remove() 删除的流程(Java 8):
根据 get() 方法的方式计算出数组的下标,即定位到存储删除元素的 Node 结点;
如果待删结点是头节点,则用它的 next 结点顶替它作为头节点;
如果待删结点是红黑树结点,则直接调用红黑树的删除方法进行删除;
如果待删结点是链表中的一个节点,则用待删除结点的前一个节点的 next 属性指向它的 next 结点;
如果删除成功则返回被删结点的 value,否则返回 null。
remove() 注意事项:删除单个 key,注意返回是的键值对中的 value。
为什么使用位运算(&)来代替取模运算(%):
效率高,位运算直接对内存数据进行操作,不需转成十进制,因此处理速度非常快;
可以解决负数问题,比如:-17 % 10 = -7。
HashMap 在 Java 7 和 Java 8 中的区别:
(1)存放数据的结点名称不同,作用都一样,存的都是 hashcode、key、value、next 等数据:
Java 7:使用 Entry 存放数据
Java 8:改名为 Node
(2)定位数组下标位置方法不同:
Java 7:计算 key 的 hash,将 hash 值进行了四次扰动,再进行取模得出;
Java 8:计算 key 的 hash,将 hash 值进行高 16 位异或低 16 位,再进行与运算得出。
(3)扩容算法不同:
Java 7:扩容要重新计算 hash
Java 8:不用重新计算
(4)put 方法插入链表位置不同:
Java 7:头插法
Java 8:尾插法
(5)Java 8 引入了红黑树,当链表长度 >=8 时,并且同时数组的长度 >=64 时,链表就转换为红黑树,利用红黑树快速增删改查的特点提高 HashMap 的性能。
3.2 HashTable
和 HashMap 一样,Hashtable 也是一个哈希散列表,Hashtable 继承于 Dictionary,使用重入锁 Synchronized 实现线程安全,key 和 value 都不允许为 Null。HashTable 已被高性能的 ConcurrentHashMap 代替。
主要属性:
initialCapacity:初始容量,默认 11。
loadFactor:负载因子,默认 0.75。
threshold:阈值。
modCount:记录结构性修改次数,用于快速失败。
//真正存储数据的数组 private transient Entry<?,?>[] table; //存放元素的个数,非Entry数组长度 private transient int count; //阈值 private int threshold; //负载因子,默认0.75 private float loadFactor; //记录结构性修改次数,用于快速失败 private transient int modCount = 0; /\*\* \* 静态内部类,存储数据的节点 \*/ private static class Entry\<K,V\> implements Map.Entry\<K,V\> { //节点的hash值 final int hash; //节点的key值 final K key; //节点的value值 V value; //下一个节点的引用 Entry<K,V> next; }
快速失败原理是在并发场景下进行遍历操作时,如果有另外一个线程对它执行了写操作,此时迭代器可以发现并抛出 ConcurrentModificationException,而不需等到遍历完后才报异常。
**数据结构:**链表的数组,数组 + 链表,Entry 结构:hash|key|value|next
特征:
key 和 value 都不允许为 Null;
HashTable 默认的初始大小为 11,之后每次扩充为原来的 2 倍;
线程安全。
原理:
与 HashMap 不一样的流程是定位数组下标逻辑,HashTable 是在 key.hashcode() 后使用取模,HashMap 是位运算。HashTable 是 put() 之前进行判断是否扩容 resize(),而 HashMap 是 put() 之后扩容。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 使用C#创建一个MCP客户端
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 按钮权限的设计及实现