跳到主要内容
JDK 核心哈希容器源码深度解析:HashMap、LinkedHashMap 等 | 极客日志
Java java 算法
JDK 核心哈希容器源码深度解析:HashMap、LinkedHashMap 等 综述由AI生成 深入解析 JDK 中核心哈希容器源码,涵盖 HashMap、LinkedHashMap、Hashtable、Properties、HashSet 及 LinkedHashSet。详细对比了 JDK 1.7 与 1.8 在底层结构、扩容机制、哈希计算等方面的差异,重点阐述了红黑树优化、尾插法改进及线程安全处理方案。同时介绍了 Properties 配置文件的读写技巧及中文乱码解决方案,并总结了各集合类的适用场景与面试高频考点,帮助开发者掌握 Java 集合框架的核心原理。
竹影清风 发布于 2026/3/25 更新于 2026/5/3 17 浏览HashMap
前置基础:核心成员变量
JDK1.7
底层结构:数组 + 链表 ,数组为哈希桶,链表解决哈希冲突
核心节点:Entry<K,V> 链表节点,包含 key、value、hash、next(链表指针)
核心变量 + 源码:
JDK1.8
底层结构:数组 + 链表 + 红黑树 ,解决长链表查询性能问题
核心节点:Node<K,V>(基础链表节点,替代 1.7 Entry)、TreeNode<K,V>(红黑树节点,继承 Node)
核心变量 + 源码(新增树化相关,优化懒加载):
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4 ; static final int MAXIMUM_CAPACITY = 1 << 30 ; static final float DEFAULT_LOAD_FACTOR = 0.75f ;
一、初始化:4 个构造方法
1. 无参构造:public HashMap()
核心作用 创建默认配置(容量 16、负载因子 0.75)的 HashMap,懒加载延迟初始化数组
JDK1.7 源码 + 解析 public HashMap () { this (DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR); }
直接计算阈值 threshold=16,但哈希桶 table 延迟至首次 put 时初始化
JDK1.8 源码 + 解析 public HashMap () { this .loadFactor = DEFAULT_LOAD_FACTOR;
极致懒加载:仅赋值负载因子,threshold 和 table 均延迟至首次 put 初始化,减少空实例内存占用
阅读重点 JDK1.8 取消构造时的阈值计算,进一步优化懒加载逻辑
2. 指定初始容量:public HashMap(int initialCapacity)
核心作用 自定义初始容量,自动修正为最近的 2 的幂 (保证哈希寻址效率)
JDK1.7/1.8 通用源码 + 解析 public HashMap (int initialCapacity) { this (initialCapacity, DEFAULT_LOAD_FACTOR); }
阅读重点
tableSizeFor 通过位运算快速修正容量(如输入 10→16、输入 17→32),保证容量为 2 的幂
2 的幂容量是为了后续通过 hash & (容量 -1) 高效计算桶索引,等价于取模但性能更高
3. 指定初始容量 + 负载因子:public HashMap(int initialCapacity, float loadFactor)
核心作用 自定义容量和负载因子,适配不同性能/内存需求(如内存紧张时提高负载因子)
核心源码 同上述 HashMap(int initialCapacity, float loadFactor),仅直接传入自定义 loadFactor
阅读重点
负载因子越大:内存利用率越高,哈希冲突概率越高;反之则冲突少、内存占用高
构造后负载因子不可变,需提前确定合适值
4. 传入 Map 构造:public HashMap(Map<? extends K, ? extends V> m)
核心作用 将现有 Map 转为 HashMap,复用原有键值对
JDK1.7 源码 + 解析 public HashMap (Map<? extends K, ? extends V> m) { this (Math.max((int ) (m.size() / DEFAULT_LOAD_FACTOR) + 1 , DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR); putAll(m); }
JDK1.8 源码 + 解析 public HashMap (Map<? extends K, ? extends V> m) { this .loadFactor = DEFAULT_LOAD_FACTOR; putMapEntries(m, false );
阅读重点 JDK1.8 新增 putMapEntries,先根据传入 Map 的大小预计算容量,减少扩容次数,比 1.7 逐个 put 更高效
二、添加/修改:核心方法
1. 核心插入:public V put(K key, V value)
核心作用 添加键值对,若 key 已存在则覆盖旧值并返回旧值;不存在则新增,返回 null
JDK1.7 源码 + 解析 public V put (K key, V value) {
JDK1.8 源码 + 解析(核心简化,新增树化) public V put (K key, V value) { return putVal(hash(key), key, value, false , true ); }
核心差异&阅读重点
JDK1.8 用尾插法 替代 1.7 头插法:避免多线程下链表成环问题,且保持链表插入顺序
哈希计算简化:1.8 仅 1 次异或 + 右移,减少计算开销,同时让高位哈希参与运算
树化逻辑:链表长度≥8且 数组容量≥64 才树化,避免小容量时频繁树化/退化
key 为 null:1.7 单独处理,1.8 整合到 hash(key) 中,哈希值固定为 0,桶索引 0
2. 批量添加:public void putAll(Map<? extends K, ? extends V> m)
核心作用 批量插入 Map 中的键值对,比循环 put 更高效
核心源码 JDK1.7 直接遍历 put,JDK1.8 调用上述 putMapEntries(预计算容量 + 批量 putVal)
阅读重点 开发中批量插入优先使用 putAll,减少扩容次数和方法调用开销
三、容量管理:扩容核心方法 final Node<K,V>[] resize()
核心作用 HashMap 动态扩容的核心,容量翻倍 ,重新哈希迁移所有节点,解决哈希冲突过多问题
JDK1.7 源码 + 解析 void resize (int newCapacity) { Entry[] oldTable = table; int oldCapacity = oldTable.length; if (oldCapacity == MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return ; }
JDK1.8 源码 + 解析(优化迁移,新增红黑树拆分) final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; int oldCap = (oldTab == null ) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0 ;
核心差异&阅读重点
扩容触发:1.7 插入前判断,1.8 插入后判断(++size > threshold)
节点迁移:
1.7 头插法:导致链表反转,多线程下易成环,且需重新计算哈希
1.8 尾插法:保持链表顺序,通过 hash & oldCap 直接判断新索引(0→原索引,1→原索引+oldCap),无需重算哈希,效率大幅提升
红黑树处理:1.8 扩容时拆分红黑树为高低位子树,若子树节点数≤6 则退化为链表
初始化整合:1.8 将数组初始化和扩容整合到 resize(),逻辑更统一
四、删除元素:核心方法
1. 核心删除:public V remove(Object key)
核心作用 根据 key 删除键值对,返回旧值;key 不存在则返回 null
JDK1.7 源码 + 解析 public V remove (Object key) { Entry<K,V> e = removeEntryForKey(key); return (e == null ? null : e.value); } final Entry<K,V> removeEntryForKey (Object key) { if (size == 0 ) return null ; int hash = (key == null ) ? 0 : hash(key); int i = indexFor(hash, table.length); Entry<K,V> prev = table[i]; Entry<K,V> e = prev;
JDK1.8 源码 + 解析 public V remove (Object key) { Node<K,V> e; return (e = removeNode(hash(key), key, null , false , true )) == null ? null : e.value; }
阅读重点
JDK1.8 新增 removeNode,统一处理链表和红黑树删除,逻辑更简洁
红黑树删除后会自动调整树结构,若节点数≤6 则退化为链表,保证性能
删除后仅调整节点指针,不缩容数组,哈希桶容量保持不变
2. 清空集合:public void clear()
核心源码 + 解析(JDK1.7/1.8 逻辑一致) public void clear () { Node<K,V>[] tab; modCount++; if ((tab = table) != null && size > 0 ) { size = 0 ;
阅读重点
仅置空哈希桶,不修改数组容量,实现数组复用
若需缩容,需手动调用 resize() 指定小容量
五、查询元素:核心方法
1. 核心查询:public V get(Object key)
核心作用 根据 key 获取 value,key 不存在则返回 null
JDK1.7 源码 + 解析 public V get (Object key) { if (key == null ) return getForNullKey(); Entry<K,V> entry = getEntry(key); return null == entry ? null : entry.getValue(); } final Entry<K,V> getEntry (Object key) { int hash = (key == null ) ? 0 : hash(key);
JDK1.8 源码 + 解析 public V get (Object key) { Node<K,V> e; return (e = getNode(hash(key), key)) == null ? null : e.value; }
核心差异&阅读重点
JDK1.8 新增 getNode,统一处理链表/红黑树查询,头节点匹配直接返回,优化查询效率
红黑树查询:将长链表的 O(n) 查询复杂度降为 O(logn),解决 1.7 长链表查询慢的问题
key 为 null:1.8 整合到 hash(key),无需单独处理,逻辑更统一
2. 包含判断:public boolean containsKey(Object key)
核心源码 JDK1.7 调用 getEntry(key) != null,JDK1.8 调用 getNode(hash(key), key) != null
阅读重点 与 get 逻辑一致,仅返回布尔值,无额外性能开销
六、JDK1.7/1.8 核心差异总表 对比维度 JDK1.7 JDK1.8 底层结构 数组 + 链表 数组 + 链表 + 红黑树 链表插入方式 头插法(多线程成环风险) 尾插法(避免成环,保持顺序) 扩容节点迁移 头插法(链表反转,重算哈希) 尾插法(保持顺序,无需重算哈希) 哈希计算 4 次扰动 +1 次异或(复杂) 1 次异或 +1 次无符号右移(简化) 桶索引计算 封装indexFor方法 内联(n-1) & hash,取消方法 树化机制 无 链表≥8 且容量≥64 时树化 退化机制 无 红黑树≤6 时退化为链表 初始化逻辑 构造时计算阈值,数组懒加载 极致懒加载,首次 put 才初始化数组 + 阈值 批量插入 遍历逐个 put,无容量预计算 putMapEntries预计算容量,减少扩容查询效率 链表 O(n),长链表性能差 红黑树 O(logn),长链表性能大幅提升 核心统一方法 增删查各有独立逻辑 新增putVal/removeNode/getNode,统一处理链表/红黑树
LinkedHashMap
前置基础:核心特性&设计定位
核心定位 java.util.LinkedHashMap 是HashMap 的子类 ,JDK1.4 引入,在 HashMap 的数组 + 链表/红黑树 底层基础上,额外维护了一条双向链表 ,用于记录键值对的插入顺序/访问顺序 ,实现了有序的哈希表 ;非线程安全,继承 HashMap 的所有特性(支持 null 键值、哈希扰动、红黑树优化等),同时解决了 HashMap遍历无序 的问题。
它是有序 Map 的首选实现 (比 TreeMap 效率更高,无需排序),适用于需要保留插入顺序 或实现LRU(最近最少使用)缓存 的场景,平均增删查时间复杂度仍为 O(1),仅在有序维护上增加少量开销。
核心特性
继承 HashMap :完全复用 HashMap 的底层结构(数组 + 链表/红黑树)、哈希计算、扩容机制、null 支持等,仅新增有序相关逻辑;
双向链表维护有序 :通过**头节点(header)和 尾节点(tail)**维护一条双向链表,所有键值对节点都加入该链表,记录顺序;
两种有序模式 :
插入顺序(默认) :遍历顺序与键值对的插入顺序 一致,重复 put 已存在的键,顺序不改变 ;
访问顺序 :调用 get/put/containsKey 等方法访问节点后,该节点会被移到双向链表尾部 ,遍历顺序为访问时间从早到晚 (可实现 LRU 缓存);
非线程安全 :无同步锁,多线程并发修改会抛出 ConcurrentModificationException,高并发需手动加锁;
支持 null 键值 :与 HashMap 一致,键允许 1 个 null,值允许多个 null;
迭代效率更高 :遍历基于双向链表,无需遍历整个哈希表(跳过空桶),比 HashMap 的无序遍历效率更高;
快速失败机制 :继承 HashMap 的 modCount,遍历过程中修改集合会触发快速失败;
扩容无额外成本 :扩容时仅复用 HashMap 的扩容逻辑,双向链表的顺序在节点复制时自动保留,无需额外处理。
与核心 Map 的对比(开发/面试必问)
1. LinkedHashMap vs HashMap(核心继承关系,有序 vs 无序) 两者本质是子类与父类 ,LinkedHashMap 仅在 HashMap 基础上增加有序特性,核心区别是是否有序、遍历方式、底层节点 :
特性 LinkedHashMap HashMap 有序性 有序(插入/访问顺序) 无序(由哈希值决定) 底层结构 数组 + 链表/红黑树 + 双向链表 数组 + 链表/红黑树 节点类型 Entry(继承 HashMap.Node,新增 before/after 指针) Node/TreeNode 遍历方式 基于双向链表(高效,无空桶) 基于哈希表(需遍历空桶) 重复 put 已存在键 不改变有序性 无有序性概念 访问节点后行为 访问顺序模式下移到尾部 无特殊行为 时间复杂度 平均 O(1)(略高于 HashMap,维护双向链表) 平均 O(1) 内存占用 稍高(节点多 2 个指针) 稍低 适用场景 需保留顺序、LRU 缓存 纯键值对存储,追求极致性能 核心优势 有序且哈希表效率 极致的单线程性能
2. LinkedHashMap vs TreeMap(有序 Map 的两大实现) 两者均为有序 Map,但有序实现方式、底层结构完全不同,LinkedHashMap 是绝大多数有序场景的首选 :
特性 LinkedHashMap TreeMap 有序实现 双向链表(插入/访问顺序,无需排序) 红黑树(键的自然顺序/自定义 Comparator) 时间复杂度 平均 O(1)(增删查) O(logn)(增删查,红黑树操作) 键要求 无要求(无需实现 Comparable) 需实现 Comparable 或自定义 Comparator null 键支持 允许 1 个 null 键 不支持 null 键(会抛 ClassCastException) 底层结构 哈希表 + 双向链表 纯红黑树 遍历效率 高(双向链表直接遍历) 中(红黑树中序遍历) 适用场景 保留插入/访问顺序、LRU 缓存 需按键排序(升序/降序)的场景 性能开销 低(仅维护双向链表) 高(红黑树旋转平衡)
3. LinkedHashMap vs ConcurrentLinkedHashMap 两者均支持 LRU 缓存,但核心区别是线程安全、是否 JDK 原生 :
特性 LinkedHashMap ConcurrentLinkedHashMap 线程安全 否(JDK 原生) 是(第三方扩展,Guava 提供) LRU 实现 重写removeEldestEntry即可 原生支持 LRU,无需自定义 底层结构 哈希表 + 双向链表 哈希表 + 双向链表 null 支持 支持 不支持 适用场景 单线程 LRU 缓存 高并发 LRU 缓存
一、底层核心原理:HashMap+ 双向链表 LinkedHashMap 的核心是继承 HashMap 并增强节点结构 ,通过在节点中增加 before/after 指针维护双向链表,既保留了 HashMap 的 O(1) 哈希表效率,又实现了有序遍历,是组合设计的经典案例 。
1. 核心底层属性 public class LinkedHashMap <K,V> extends HashMap <K,V> implements Map <K,V> {
核心常量 :accessOrder 为 final,构造时指定且不可修改 ,决定 LinkedHashMap 的有序模式,默认 false(插入顺序)。
2. 节点定义:继承 HashMap.Node 并新增双向链表指针 LinkedHashMap 的Entry 节点 是 HashMap.Node 的子类,仅新增 before(前驱)和 after(后继)指针,用于构建双向链表,不改变 HashMap 的节点哈希特性 :
static class Entry <K,V> extends HashMap .Node<K,V> { Entry<K,V> before, after;
作为哈希表节点 :next 指针用于解决哈希冲突,构成数组后的单向链表/红黑树;
作为双向链表节点 :before/after 指针用于维护有序性,所有节点串联成双向链表。
3. 底层结构示意图(插入顺序,容量 16)
哈希表负责O(1) 增删查 ,双向链表负责有序遍历 ;
所有节点同时属于哈希表和双向链表,两种结构互不干扰,仅在节点增删时同步维护。
4. 有序模式核心逻辑
(1)插入顺序(默认,accessOrder=false)
新节点始终插入到双向链表的尾部 ;
重复 put 已存在的键(覆盖值),节点在双向链表中的位置不变 ;
遍历顺序与首次插入顺序 完全一致,符合'先进先出'。
(2)访问顺序(accessOrder=true)
新节点仍插入到双向链表尾部;
调用 get/put(已存在键)/containsKey 等方法访问节点 后,该节点会被移除并重新插入到双向链表尾部 (成为'最新访问节点');
遍历顺序为访问时间从早到晚 ,最尾部是最新访问的节点,最头部是最久未访问的节点(LRU 缓存的核心原理 )。
二、核心构造方法 LinkedHashMap 提供4 个构造方法 ,均调用 HashMap 的构造方法 完成哈希表初始化,再指定 accessOrder(默认 false),支持默认初始化、指定容量/加载因子、传入 Map、指定容量 + 加载因子 + 有序模式:
1. 空参构造:new LinkedHashMap<>()
最常用构造方法,默认插入顺序,满足绝大多数有序存储场景;
哈希表懒加载,首次 put 时初始化数组。
2. 指定初始容量/加载因子:new LinkedHashMap(int initialCapacity)/new LinkedHashMap(int initialCapacity, float loadFactor) public LinkedHashMap (int initialCapacity) { super (initialCapacity); accessOrder = false ; } public LinkedHashMap (int initialCapacity, float loadFactor) { super (initialCapacity, loadFactor); accessOrder = false ; }
适合已知键值对大致数量的场景,避免频繁扩容;
加载因子建议使用默认 0.75(时间/空间平衡值)。
3. 传入 Map 初始化:new LinkedHashMap(Map<? extends K, ? extends V> m) public LinkedHashMap (Map<? extends K, ? extends V> m) { super (); accessOrder = false ;
将传入的 Map(如 HashMap)键值对批量复制,双向链表按传入 Map 的迭代顺序排列 ;
若传入的 Map 是 LinkedHashMap,会保留其原有顺序;若为 HashMap,顺序为其无序的哈希遍历顺序。
4. 核心构造:new LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) public LinkedHashMap (int initialCapacity, float loadFactor, boolean accessOrder) { super (initialCapacity, loadFactor);
唯一能指定访问顺序的构造方法 ,是实现 LRU 缓存的关键;
示例:new LinkedHashMap<>(16, 0.75f, true) 构建一个访问顺序的 LinkedHashMap。
关键注意点
继承 HashMap 的所有特性 :支持 null 键(1 个)、null 值(多个),扩容机制与 HashMap 一致(原容量×2,2 的幂),红黑树优化(链表≥8 转红黑树);
accessOrder 不可修改 :一旦构造时指定,运行时无法改变有序模式;
初始容量计算 :与 HashMap 一致,建议设置为 (预计元素数 / 0.75) + 1,减少扩容次数。
三、核心底层逻辑:双向链表的维护(新增/访问/删除) LinkedHashMap 的有序性完全依赖双向链表的增删改 ,它通过重写 HashMap 的钩子方法 实现双向链表的自动维护,无需修改 HashMap 的核心逻辑,这是其设计的精妙之处。
HashMap 为子类预留了3 个核心钩子方法 ,LinkedHashMap 通过重写这些方法,在哈希表节点增删时同步维护双向链表:
1. 新增节点:维护双向链表(插入尾部) 当调用 put 添加新节点(键不存在)时,HashMap 会先将节点插入哈希表(数组/链表/红黑树),然后调用 afterNodeInsertion,LinkedHashMap 重写该方法的核心逻辑是将新节点插入双向链表尾部 :
核心流程 :创建 LinkedHashMap.Entry 节点 → 插入哈希表 → 调用 linkNodeLast 插入双向链表尾部,哈希表和双向链表同步更新 。
2. 访问节点:访问顺序模式下移到尾部(LRU 核心) 当调用 get/put(已存在键) 访问节点时,HashMap 会找到该节点,然后调用 afterNodeAccess,LinkedHashMap 重写该方法,在访问顺序模式下将节点移到双向链表尾部 :
核心效果 :访问后的节点成为双向链表最新尾部 ,保证访问顺序模式下'最新访问的节点在尾部,最久未访问的在头部'。
3. 删除节点:同步移除双向链表中的节点 当调用 remove 删除节点时,HashMap 会先从哈希表中删除节点,然后调用 afterNodeRemoval,LinkedHashMap 重写该方法,将节点从双向链表中同步移除 ,保证双向链表结构完整:
核心保证 :哈希表和双向链表的节点数量始终一致,删除操作不会破坏有序性。
4. 扩容时的有序性保留 LinkedHashMap 的扩容完全复用 HashMap 的扩容逻辑,无需额外处理双向链表 :
扩容时,HashMap 会将原哈希表的节点复制到新数组,LinkedHashMap 的节点在复制时,before/after 指针保持不变;
新哈希表的双向链表与原链表的顺序完全一致 ,有序性不会因扩容丢失。
四、核心方法:继承 + 增强(有序性相关) LinkedHashMap继承 HashMap 的所有核心方法 (put/get/remove/size 等),仅重写了 get、containsValue 和遍历相关方法,新增了有序性的支持,核心方法的时间复杂度仍为 O(1)(访问顺序模式的 get 因移节点会稍慢,但仍为 O(1))。
1. 查询方法:get(Object key)(重写,支持访问顺序) public V get (Object key) { Node<K,V> e;
核心区别 :比 HashMap 的 get 多了访问顺序的处理 ,这是实现 LRU 的关键。
2. 包含值判断:containsValue(Object value)(重写,效率更高) public boolean containsValue (Object value) {
效率提升 :HashMap 的 containsValue 需要遍历整个哈希表(包括空桶),而 LinkedHashMap 基于双向链表遍历,直接跳过空桶,遍历节点数等于实际元素数,效率更高。
3. 有序遍历(核心优势,基于双向链表) LinkedHashMap重写了 HashMap 的遍历相关方法 ,所有遍历(entrySet/keySet/values)均基于双向链表 实现,遍历顺序与有序模式一致 ,且效率更高:
(1)插入顺序模式(默认):遍历与插入顺序一致 LinkedHashMap<String, String> lhm = new LinkedHashMap <>(); lhm.put("a" , "1" ); lhm.put("b" , "2" ); lhm.put("c" , "3" ); lhm.put("a" , "4" );
(2)访问顺序模式:遍历与访问时间一致(LRU)
4. LRU 缓存实现:重写 removeEldestEntry(Map.Entry<K,V> eldest) LinkedHashMap 内置了LRU 缓存的核心能力 ,只需重写 removeEldestEntry 方法,指定缓存最大容量 ,当元素数超过容量时,会自动删除最久未访问的节点(双向链表头节点) ,实现 LRU(最近最少使用)缓存。
(1)核心原理
removeEldestEntry 是 LinkedHashMap 的保护方法 ,默认返回 false(不删除旧节点);
每次 put 添加新节点后,HashMap 会调用 afterNodeInsertion,该方法会判断 removeEldestEntry(eldest) 的返回值:
若返回 true,则自动删除双向链表的头节点(最久未访问节点) ;
若返回 false,则不删除。
(2)自定义 LRU 缓存(固定容量,线程不安全) 核心效果 :缓存容量固定,自动淘汰最久未使用的节点,完美实现 LRU 缓存,代码极简。
(3)线程安全的 LRU 缓存 上述自定义 LruCache 是非线程安全的,若需高并发使用,有两种方案:
简单方案 :通过 Collections.synchronizedMap 包装,低并发可用:
Map<String, String> syncLruCache = Collections.synchronizedMap(new LruCache <>(3 ));
推荐方案 :使用 Guava 的 com.google.common.cache.CacheBuilder 构建 LRU 缓存(高并发、功能丰富):
五、线程安全处理 LinkedHashMap非线程安全 ,与 HashMap 一致,多线程并发修改(如同时 put/remove/get)会导致:
双向链表结构错乱,遍历得到错误顺序;
哈希表节点覆盖,数据丢失;
触发快速失败,抛出 ConcurrentModificationException。
线程安全处理方案(3 种,优先级从高到低)
1. 低并发场景:Collections.synchronizedMap 包装
实现简单,适合低并发;
并发效率低,全局锁导致所有操作串行。
2. 高并发场景:手动加锁(ReentrantLock) LinkedHashMap<String, String> lhm = new LinkedHashMap <>(); ReentrantLock lock = new ReentrantLock ();
比 synchronized 更灵活(支持公平锁、可中断锁);
适合需要自定义锁策略的场景。
3. 高并发 LRU 缓存:Guava ConcurrentLinkedHashMap/ Caffeine
JDK 原生无线程安全的 LinkedHashMap,高并发 LRU 缓存推荐使用Guava 的 ConcurrentLinkedHashMap 或Caffeine (性能更高);
原生支持 LRU、线程安全、高并发,无需自定义继承。
六、开发/面试高频考点
1. LinkedHashMap 的底层实现是什么?核心原理是什么?(核心考点)
底层是HashMap 的子类 ,复用 HashMap 的数组 + 链表/红黑树 哈希表结构,额外维护一条双向链表 ;
核心原理:通过继承 HashMap 并重写其钩子方法 (afterNodeInsertion/afterNodeAccess/afterNodeRemoval),在哈希表节点增删查时,同步维护双向链表的有序性,既保留 HashMap 的 O(1) 效率,又实现有序遍历。
2. LinkedHashMap 有哪两种有序模式?分别适用于什么场景?
插入顺序(默认,accessOrder=false) :遍历与首次插入顺序一致,重复 put 不改变顺序,适用于需要保留插入顺序 的场景(如配置解析、日志记录);
访问顺序(accessOrder=true) :访问节点后节点移到双向链表尾部,遍历顺序为访问时间从早到晚,适用于实现 LRU(最近最少使用)缓存 的场景。
3. LinkedHashMap 如何实现 LRU 缓存?核心方法是什么?
核心依托访问顺序模式(accessOrder=true)和重写 removeEldestEntry 方法 ;
步骤:1. 继承 LinkedHashMap,构造时指定 accessOrder=true;2. 重写 removeEldestEntry,当 size() 超过缓存最大容量时返回 true;3. 每次添加新节点后,LinkedHashMap 会自动删除双向链表头节点(最久未访问节点);
核心方法:removeEldestEntry(Map.Entry<K,V> eldest),默认返回 false,重写后实现缓存淘汰。
4. LinkedHashMap 和 HashMap 的核心区别?为什么 LinkedHashMap 是有序的?
核心区别
有序性 :LinkedHashMap 有序(插入/访问),HashMap 无序;
底层结构 :LinkedHashMap 多了双向链表,节点多 before/after 指针;
遍历方式 :LinkedHashMap 基于双向链表(高效,无空桶),HashMap 基于哈希表(需遍历空桶);
内存占用 :LinkedHashMap 稍高(节点多 2 个指针);
方法重写 :LinkedHashMap 重写了 get/containsValue/遍历方法,HashMap 无。
有序的原因 LinkedHashMap 通过双向链表记录所有节点的顺序 ,哈希表负责高效增删查,双向链表负责有序遍历,两种结构独立维护且同步更新,因此实现了有序。
5. LinkedHashMap 和 TreeMap 的区别?如何选择有序 Map?
核心区别
有序实现 :LinkedHashMap 是双向链表的插入/访问顺序 (无需排序),TreeMap 是红黑树的键排序 (自然顺序/自定义 Comparator);
时间复杂度 :LinkedHashMap 增删查平均 O(1),TreeMap O(logn);
键要求 :LinkedHashMap 无要求,TreeMap 的键需实现 Comparable 或自定义 Comparator;
null 键 :LinkedHashMap 允许 1 个 null 键,TreeMap 不支持;
性能开销 :LinkedHashMap 仅维护双向链表,开销低;TreeMap 需红黑树旋转平衡,开销高。
选择原则
选 LinkedHashMap :需要保留插入/访问顺序 ,无需按键排序,追求哈希表的 O(1) 效率(绝大多数有序场景);
选 TreeMap :需要按键的大小排序 (升序/降序),如排行榜、数据统计等场景。
6. LinkedHashMap 的节点有什么特殊之处? LinkedHashMap 的 Entry 节点继承自 HashMap.Node ,仅新增了 before(前驱)和 after(后继)指针 ,使节点同时具备两种身份:
作为哈希表节点:通过 next 指针解决哈希冲突,构成链表/红黑树;
作为双向链表节点:通过 before/after 指针维护有序性,所有节点串联成双向链表。
7. LinkedHashMap 的扩容会破坏有序性吗?为什么?
不会 ;
原因:LinkedHashMap 的扩容完全复用 HashMap 的逻辑,扩容时仅将原哈希表的节点复制到新数组,节点的 before/after 指针在复制过程中保持不变 ,因此双向链表的顺序与原链表完全一致,有序性不会丢失。
8. LinkedHashMap 的 containsValue 方法为什么比 HashMap 高效?
HashMap 的 containsValue 需要遍历整个哈希表 (包括所有桶,即使桶为空),遍历的节点数可能远大于实际元素数;
LinkedHashMap 的 containsValue基于双向链表遍历 ,直接跳过哈希表的空桶,遍历的节点数等于实际元素数 ,因此效率更高。
9. LinkedHashMap 是线程安全的吗?如何实现线程安全?
非线程安全 ,与 HashMap 一致,无同步锁;
线程安全实现方案:1. 低并发用 Collections.synchronizedMap 包装;2. 中等并发用 ReentrantLock 手动加锁;3. 高并发 LRU 缓存用 Guava ConcurrentLinkedHashMap/Caffeine。
10. LinkedHashMap 中,重复 put 已存在的键,会改变其在双向链表中的位置吗?
插入顺序模式(默认) :不会改变,仅覆盖值,双向链表位置保持首次插入的顺序;
访问顺序模式 :会改变,重复 put 属于'访问节点',节点会被移到双向链表尾部,成为最新访问节点。
七、核心总结
LinkedHashMap 是HashMap 的有序子类 ,底层为数组 + 链表/红黑树 + 双向链表 ,继承 HashMap 的所有特性(支持 null、O(1) 效率、红黑树优化),额外实现有序性;
核心通过重写 HashMap 的钩子方法 维护双向链表,支持插入顺序(默认)和访问顺序 两种模式,插入顺序保留插入先后,访问顺序是 LRU 缓存的核心;
有序遍历基于双向链表实现,效率高于 HashMap,containsValue 方法因跳过空桶也更高效;
仅需重写 removeEldestEntry 方法 即可快速实现 LRU 缓存,是 JDK 原生最简洁的 LRU 实现方式;
非线程安全,低并发用 Collections.synchronizedMap 包装,高并发需手动加锁或使用第三方库(Guava/Caffeine);
是有序 Map 的首选实现 ,比 TreeMap 效率更高,适用于保留插入/访问顺序、LRU 缓存等场景,无需按键排序的有序场景均优先选择;
面试核心考点:底层实现、两种有序模式、LRU 缓存实现、与 HashMap/TreeMap 的区别,是 Java 集合框架中子类增强父类的经典设计案例 。
Hashtable
前置基础:核心特性&设计定位
核心定位 java.util.Hashtable 是 Java 集合框架中最早的哈希表实现 (JDK1.0),实现 Map 接口,基于数组 + 链表 的经典哈希表结构存储键值对,核心特性是键值均不允许为 null、天生线程安全 ,所有核心方法均加 synchronized 全局锁,适合低并发的线程安全键值对存储场景 。
它是 JDK 早期的 Map 实现,后续被 JDK1.2 的 HashMap(非线程安全)和 JDK1.5 的 JUC 并发 Map(高并发)替代,目前仅用于旧代码兼容 ,新开发中完全不推荐使用。
核心特性
经典哈希表结构 :底层是Entry 数组 + 单向链表 ,通过键的 hashCode() 计算哈希值,映射到数组索引,哈希冲突时用单向链表解决;
天生线程安全 :所有核心方法(put/get/remove/contains)均加 synchronized 方法级锁,保证多线程操作的原子性;
键值均不可为 null :键(key)和值(value)都不能是 null,添加 null 会直接抛出 NullPointerException;
固定扩容机制 :默认初始容量 11,加载因子 0.75 ,扩容为原容量×2 + 1 (保证奇数,减少哈希冲突);
哈希计算简单 :直接使用键的 hashCode(),通过 hash % table.length 映射数组索引(未做哈希扰动优化);
快速失败机制 :维护 modCount,遍历过程中集合被修改会抛出 ConcurrentModificationException;
继承 Dictionary 类 :JDK1.0 的旧集合类,同时实现 Map 接口,兼容旧的字典操作方法(如 elements()/keys());
迭代器支持 :提供 entrySet()/keySet()/values() 的迭代器,也保留旧的 Enumeration 遍历方式。
与核心 Map 的对比(开发/面试必问)
1. Hashtable vs HashMap(最核心对比,同源不同命) 两者均实现 Map 接口,底层都是数组 + 链表,核心区别是线程安全、null 支持、扩容机制、哈希计算 ,HashMap 是 Hashtable 的非线程安全优化版 ,也是新开发的首选:
特性 Hashtable HashMap 线程安全 是(synchronized 全局锁) 否(单线程高效) null 支持 键/值均不支持(抛 NPE) 键允许 1 个 null、值允许多个 null 底层结构 数组 + 单向链表(JDK1.0 至今) 数组 + 链表 + 红黑树(JDK1.8+,链表≥8 转红黑树) 默认初始容量 11 16(2 的幂) 扩容机制 原容量×2+1(保证奇数) 原容量×2(保证 2 的幂,位运算优化索引) 哈希计算 直接用 hashCode(),hash%length 哈希扰动(hashCode() 高 16 位^低 16 位),hash&(length-1) 加载因子 固定 0.75 默认 0.75,支持自定义 继承/实现 继承 Dictionary+ 实现 Map 实现 Map,继承 AbstractMap 遍历方式 迭代器/Enumeration 迭代器/forEach(JDK8+) 并发效率 极低(全局锁串行执行) 无锁,单线程效率极高 扩容触发 元素数 ≥ 容量×加载因子 元素数 ≥ 容量×加载因子 官方态度 弃用(仅兼容旧代码) 推荐(单线程 Map 首选)
2. Hashtable vs ConcurrentHashMap(线程安全 Map 的新旧对比) ConcurrentHashMap 是 JDK1.5 为解决 Hashtable并发效率低 而设计的高并发 Map,是线程安全 Map 的首选 ,完全替代 Hashtable:
特性 Hashtable ConcurrentHashMap 线程安全实现 synchronized 全局锁(方法级) JDK1.7:分段锁(Segment);JDK1.8+:CAS+synchronized(节点级) 并发效率 极低(所有操作串行) 极高(多线程并行操作,无全局锁) null 支持 键/值均不支持 键/值均不支持 底层结构 数组 + 单向链表 JDK1.8+:数组 + 链表 + 红黑树 扩容机制 原容量×2+1 原容量×2(2 的幂) 快速失败 支持(遍历抛异常) 弱一致性(遍历不抛异常,允许脏读) 适用场景 低并发兼容旧代码 高/低并发线程安全场景
一、底层核心结构 Hashtable 的底层是经典的哈希表实现 ,由Entry 数组 和单向链表 组成,无红黑树优化(JDK 所有版本均如此),核心属性和 Entry 节点定义如下:
1. 核心底层属性 public class Hashtable <K,V> extends Dictionary <K,V> implements Map <K,V>, Cloneable, java.io.Serializable {
核心常量 :加载因子 loadFactor 固定为 0.75f,是时间和空间的平衡值——减少哈希冲突的同时,避免数组空间浪费。
2. Entry 节点定义(单向链表) Entry 是 Hashtable 的内部静态类 ,代表哈希表中的一个键值对,构成单向链表解决哈希冲突:
private static class Entry <K,V> implements Map .Entry<K,V> { final int hash;
单向链表特性 :哈希冲突时,新节点插入到链表头部 (头插法),遍历从头部开始到尾部结束。
3. 哈希表结构示意图(容量 11,加载因子 0.75,阈值 8) 数组索引:0 1 2 3 4 5 6 7 8 9 10 ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ 桶结构: - A - B→C - D - - - E -
键 A 的哈希值映射到索引 1,无冲突,直接存为数组元素;
键 B、C 哈希冲突,映射到索引 3,形成单向链表(B 是头节点,C 是后继);
当元素数达到 8(11×0.75),触发扩容,新容量=11×2+1=23。
二、核心构造方法 Hashtable 提供4 个构造方法 ,支持默认初始化、指定初始容量、指定容量 + 加载因子、传入 Map 初始化,加载因子默认 0.75,且仅在构造时可指定(后续不可修改) :
1. 空参构造:new Hashtable<>()
最常用的构造方法,默认容量 11、阈值 8(11×0.75),满足绝大多数低并发简单场景;
无初始元素,table 数组在首次 put 时初始化 (懒加载)。
2. 指定初始容量:new Hashtable(int initialCapacity) public Hashtable (int initialCapacity) {
适合已知键值对大致数量 的场景,避免频繁扩容;
初始容量建议设置为 (预计元素数 / 0.75) + 1,减少扩容次数。
3. 指定初始容量 + 加载因子:new Hashtable(int initialCapacity, float loadFactor) public Hashtable (int initialCapacity, float loadFactor) {
唯一可自定义加载因子的构造方法,不建议修改 0.75 (官方优化的平衡值);
初始容量支持任意正整数(无需是 2 的幂),Hashtable 会直接使用。
4. 传入 Map 初始化:new Hashtable(Map<? extends K, ? extends V> t) public Hashtable (Map<? extends K, ? extends V> t) {
将传入的 Map(如 HashMap)键值对批量复制 到 Hashtable 中;
若传入的 Map 包含 null 键/值,会直接抛出 NullPointerException;
时间复杂度 O(n),n 为传入 Map 的元素个数。
关键注意点 键值均不可为 null :所有 put 相关方法都会显式检查 key 和 value 是否为 null,只要有一个为 null 就抛 NPE:
Hashtable<String, String> ht = new Hashtable <>(); ht.put("name" , null );
三、核心底层逻辑:哈希计算与扩容机制
1. 哈希计算(简单无优化,易冲突) Hashtable 的哈希计算分为两步 ,无哈希扰动优化,直接使用键的 hashCode(),通过取模映射数组索引,哈希冲突概率较高 :
步骤 1:计算键的哈希值 直接调用键的 hashCode() 方法,若 key 为 null 直接抛 NPE:
int hash = key.hashCode();
步骤 2:映射到数组索引 通过取模运算 将哈希值映射到数组的合法索引(0 ~ table.length-1):
int index = (hash & 0x7FFFFFFF ) % table.length;
hash & 0x7FFFFFFF:将哈希值转为非负数 (避免 hash 为负时,取模得到负索引);
% table.length:取模运算,因 table.length 是奇数,能减少哈希冲突(比偶数更均匀)。
哈希计算缺陷 :未做哈希扰动(如 HashMap 的高 16 位^低 16 位),若键的 hashCode() 分布较差,极易导致哈希冲突,链表过长会降低查询效率。
2. 扩容机制(固定规则,无优化) 当 Hashtable 的实际元素数 count ≥ 阈值 threshold 时,添加元素会触发扩容,扩容规则固定为原容量×2 + 1 ,核心步骤为创建新数组→重新哈希所有元素→替换原数组 。
(1)扩容触发时机 所有 put 操作都会在添加元素后检查 count > threshold,满足则触发扩容:
(2)扩容核心步骤
计算新容量 :原容量×2+1,保证奇数,若溢出则设为 Integer.MAX_VALUE;
创建新数组 :容量为新容量的 Entry 数组;
更新阈值 :新容量×加载因子,向下取整;
重新哈希 :遍历原数组的所有桶,对每个链表的所有元素重新计算索引 ,通过头插法 插入到新数组的对应桶中;
替换原数组 :将 table 指向新数组,完成扩容。
(3)扩容特性
扩容成本高 :需要对所有元素重新哈希 + 复制 ,时间复杂度 O(n),n 为元素个数,频繁扩容会严重影响性能;
头插法 :新节点插入链表头部,原链表的顺序会被反转(无实际影响,仅遍历顺序变化);
无红黑树优化 :即使链表过长,也不会转为红黑树,查询效率会线性下降(O(n))。
四、核心 Map 接口方法(线程安全版,O(1) 平均复杂度) Hashtable 实现 Map 接口的所有核心方法,所有方法均加 synchronized关键字 ,保证线程安全,平均时间复杂度 O(1)(哈希冲突少时),最坏 O(n)(链表过长时)。
1. 新增/修改键值对:put(K key, V value)(核心) 核心作用 :添加键值对,若键已存在则覆盖旧值 并返回旧值;若键不存在则添加新节点,触发容量检查和扩容,key/value 均不能为 null 。
public synchronized V put (K key, V value) {
方法加 synchronized,多线程同时 put 不会出现元素覆盖;
自动处理键重复的情况,覆盖旧值并返回,符合 Map 规范;
首次 put 时初始化数组,懒加载优化。
2. 查询键值对:get(Object key)(核心) 核心作用 :根据键查询对应的值,若键不存在返回 null;若键为 null 抛 NPE,时间复杂度平均 O(1) 。
public synchronized V get (Object key) { Entry<?,?> tab[] = table;
核心优化 :缓存了键的哈希值(Entry.hash),无需重复计算,提升遍历效率。
3. 删除键值对:remove(Object key)(核心) 核心作用 :根据键删除对应的键值对,返回被删除的值;若键不存在返回 null,需要调整链表指针 。
public synchronized V remove (Object key) { Entry<?,?> tab[] = table; int hash = key.hashCode(); int index = (hash & 0x7FFFFFFF ) % tab.length; @SuppressWarnings("unchecked") Entry<K,V> e = (Entry<K,V>)tab[index]; Entry<K,V> prev = null ;
核心步骤 :找到目标节点→调整前驱/后继指针→清空引用→更新计数,保证链表结构完整。
4. 其他核心方法
(1)判空/获取大小:isEmpty()/size() public synchronized boolean isEmpty () { return count == 0 ; } public synchronized int size () { return count; }
均加 synchronized,保证多线程下计数的准确性。
(2)包含判断:containsKey(Object key)/containsValue(Object value) public synchronized boolean containsKey (Object key) {
containsValue 效率极低,需全表遍历,尽量避免使用。
(3)清空哈希表:clear() public synchronized void clear () { Entry<?,?>[] tab = table; modCount++;
五、遍历方式(两种:迭代器/Enumeration) Hashtable 支持现代迭代器 和旧版 Enumeration 两种遍历方式,均为 O(n) 时间复杂度,迭代器支持快速失败,Enumeration 不支持 ,新代码推荐使用迭代器,旧代码兼容用 Enumeration。
1. 迭代器遍历(推荐,支持 Map 的标准遍历) Hashtable 提供 entrySet()/keySet()/values() 三个视图的迭代器,支持边遍历边删除(迭代器的 remove() 方法) ,是新代码的首选:
(1)遍历键值对(entrySet,最常用) Hashtable<String, String> ht = new Hashtable <>(); ht.put("name" , "Java" ); ht.put("age" , "25" ); ht.put("gender" , "male" );
(2)遍历键/值
2. Enumeration 遍历(旧版,仅兼容) Enumeration 是 JDK1.0 的旧迭代器,仅支持遍历(读),不支持删除 ,方法名为 hasMoreElements()/nextElement(),通过 keys()/elements() 获取:
遍历注意点
快速失败 :迭代器遍历过程中,若通过 Hashtable 的 put/remove 修改集合,会抛出 ConcurrentModificationException,仅能通过迭代器的 remove() 修改;
Enumeration 无快速失败 :遍历过程中集合被修改,不会抛异常,但可能得到脏数据;
遍历顺序无序 :哈希表的遍历顺序与键的插入顺序无关,由哈希值决定。
六、Hashtable 的核心缺陷(面试高频,官方弃用原因) Hashtable 因设计早、优化少 ,存在多个致命缺陷,也是 Java 官方弃用、被 HashMap/ConcurrentHashMap 替代的核心原因,这是面试的高频考点:
缺陷 1:线程安全的实现方式导致并发效率极低 Hashtable 的线程安全是通过给所有核心方法加 synchronized 全局锁 实现的,相当于给整个 Hashtable 对象加锁:
同一时间只有一个线程能执行任意核心方法 (put/get/remove/contains),所有操作串行执行;
即使是多个线程的读操作(get) ,也会互相阻塞,高并发读场景性能极差;
组合操作(如 if (!ht.containsKey(key)) { ht.put(key, value); })仍需手动加锁,否则会出现线程安全问题。
缺陷 2:哈希计算无优化,哈希冲突概率高
直接使用键的 hashCode(),未做任何哈希扰动处理,若键的 hashCode() 分布较差(如连续整数),极易导致哈希冲突;
仅通过取模运算 映射索引,效率低于 HashMap 的位运算 (hash & (length-1));
无红黑树优化,链表过长时查询效率从 O(1) 退化到 O(n)。
缺陷 3:键值均不支持 null,使用灵活性低 与 HashMap 相比,Hashtable 禁止 null 键和 null 值,添加 null 会直接抛 NPE,导致:
无法表示'键不存在'和'值为 null'的场景,使用场景受限;
与其他 Map 实现(如 HashMap)的兼容性差,无法直接转换。
缺陷 4:扩容机制固定,扩容成本高且不灵活
扩容规则固定为 原容量×2+1,无法自定义,若初始容量设置不合理,会导致频繁扩容;
扩容时需要重新哈希所有元素 ,并通过头插法复制到新数组,时间复杂度 O(n),成本极高;
加载因子固定为 0.75,不可在运行时修改,无法根据实际场景调整时间/空间平衡。
缺陷 5:继承 Dictionary 类,设计冗余且不规范
Dictionary 是 JDK1.0 的旧集合类,Hashtable 同时继承 Dictionary 和实现 Map 接口,导致方法冗余(如 put/putEntry、get/getEntry);
不符合 Java 集合框架的设计规范(如 HashMap 继承 AbstractMap,仅实现 Map 接口),代码冗余且维护成本高。
缺陷 6:快速失败机制,遍历灵活性低
迭代器遍历过程中,任何非迭代器的修改都会触发快速失败,抛 ConcurrentModificationException;
无法像 ConcurrentHashMap 那样实现弱一致性遍历 (允许脏读,但不抛异常),遍历灵活性低。
七、替代方案(开发首选,完全替代 Hashtable) Hashtable 在新代码开发中完全禁止使用 ,根据是否需要线程安全 ,有明确的替代方案,优先级从高到低:
1. 无需线程安全(单线程/多线程无并发修改):HashMap(首选)
HashMap 是 Hashtable 的非线程安全优化版 ,修复了 Hashtable 的大部分缺陷(支持 null、哈希扰动、位运算索引、红黑树优化);
单线程下性能远高于 Hashtable,是 Java 中最常用的 Map 实现 ;
支持 JDK8+ 的 Lambda 表达式(forEach),遍历方式更丰富,兼容性更好。
Map<String, String> map = new HashMap <>(); map.put("name" , "Java" ); map.put(null , "nullKey" );
2. 需要线程安全(低/高并发):ConcurrentHashMap(首选)
ConcurrentHashMap 是线程安全 Map 的工业标准 ,JDK1.8+ 采用 CAS+ 节点级 synchronized 实现,并发效率远超 Hashtable;
无全局锁,多线程可并行操作不同的桶,读操作无锁,高并发下性能极佳;
支持弱一致性遍历,遍历过程中修改集合不会抛异常,适合高并发场景;
与 Hashtable 一致,键值均不支持 null ,兼容性好。
Map<String, String> concurrentMap = new ConcurrentHashMap <>(); concurrentMap.put("name" , "Java" ); concurrentMap.put("age" , "25" );
3. 低并发线程安全(兼容旧代码):Collections.synchronizedMap
通过集合工具类将 HashMap 包装为线程安全的 Map,底层基于 synchronized 全局锁,实现简单;
并发效率与 Hashtable 相当,但底层是 HashMap,支持 null 键/值,灵活性更高;
仅适合低并发 场景,高并发仍推荐 ConcurrentHashMap。
八、开发/面试高频考点
1. Hashtable 的底层实现是什么?核心结构是什么?
底层是经典的哈希表 ,核心结构为Entry 数组 + 单向链表 (无红黑树优化);
通过 Entry 数组存储'桶',哈希冲突时用单向链表解决,新节点采用头插法 插入链表头部。
2. Hashtable 为什么键值都不能为 null?
避免空指针歧义 :get(key) 返回 null 时,无法区分'键不存在'和'值为 null',Hashtable 通过抛 NPE 彻底避免这种歧义;
早期设计理念 :JDK1.0 的集合类普遍不支持 null,强调数据的非空性,减少空指针异常;
方法实现简化 :无需在 put/get/contains 等方法中单独处理 null 的情况,简化代码逻辑。
3. Hashtable 和 HashMap 的核心区别有哪些?(核心考点)
线程安全 :Hashtable 是(synchronized 全局锁),HashMap 否;
null 支持 :Hashtable 键/值均不支持,HashMap 键允许 1 个 null、值允许多个 null;
哈希计算 :Hashtable 直接用 hashCode()+ 取模,HashMap 做哈希扰动 + 位运算;
扩容机制 :Hashtable 扩容为×2+1(奇数),HashMap 扩容为×2(2 的幂);
底层结构 :Hashtable 始终是数组 + 链表,HashMapJDK1.8+ 是数组 + 链表 + 红黑树。
4. Hashtable 的线程安全是如何实现的?有什么缺陷?
实现方式:所有核心方法加 synchronized 方法级锁 ,等价于给 Hashtable 对象加全局锁,保证单个方法的原子性;
核心缺陷:并发效率极低 ——全局锁导致所有操作串行执行,读操作互相阻塞,高并发下性能极差;且组合操作仍需手动加锁。
5. Hashtable 的扩容机制是什么?为什么扩容为原容量×2+1?
扩容触发:元素数≥容量×加载因子(0.75)时触发;
扩容规则:原容量×2+1,保证新容量为奇数 ;
原因:奇数的容量能让哈希值的取模结果更均匀,减少哈希冲突 (偶数容量会导致偶数哈希值集中在偶数索引,奇数哈希值集中在奇数索引)。
6. Hashtable 为什么被官方弃用?替代方案是什么?
弃用原因 核心是并发效率极低 ,加上哈希计算无优化、不支持 null、扩容成本高等缺陷,无法满足现代开发的性能和灵活性需求。
替代方案
无需线程安全:HashMap (首选);
需要线程安全:ConcurrentHashMap (首选,高低并发均适用);
低并发兼容:Collections.synchronizedMap(new HashMap<>()) 。
7. Hashtable 的哈希计算过程是怎样的?为什么不如 HashMap 高效?
计算过程
调用 key 的 hashCode() 获取哈希值(key 为 null 抛 NPE);
通过 hash & 0x7FFFFFFF 将哈希值转为非负数;
通过 hash % table.length 取模,映射到数组索引。
效率低的原因
无哈希扰动 处理,若 key 的 hashCode() 分布差,极易哈希冲突;
取模运算 的效率远低于 HashMap 的位运算 (hash & (length-1));
容量为奇数,无法利用位运算优化,只能用取模。
8. Hashtable 支持边遍历边删除吗?如何实现?
支持,但仅能通过迭代器的 remove()方法 实现;
若直接调用 Hashtable 的 remove() 方法,会修改 modCount,触发迭代器的快速失败机制,抛出 ConcurrentModificationException;
旧版的 Enumeration 遍历不支持删除,仅能读。
9. Hashtable 和 ConcurrentHashMap 的线程安全实现有什么区别?
Hashtable:synchronized 全局锁 ,方法级加锁,所有操作串行执行,并发效率极低;
ConcurrentHashMap(JDK1.8+):CAS+ 节点级 synchronized 锁 ,仅对哈希冲突的链表/红黑树节点加锁,多线程可并行操作不同桶,并发效率极高;
此外,ConcurrentHashMap 是弱一致性 ,遍历不抛异常;Hashtable 是快速失败 ,遍历抛异常。
九、核心总结
Hashtable 是 JDK1.0 的经典哈希表实现 ,底层为 Entry 数组 + 单向链表,天生线程安全(synchronized 全局锁),键值均不支持 null;
核心缺陷:并发效率极低 、哈希计算无优化、扩容成本高、使用灵活性低,Java 官方明确弃用 ,新开发中完全禁止使用;
扩容机制固定:默认容量 11、加载因子 0.75,扩容为原容量×2+1(保证奇数),扩容时需重新哈希所有元素,成本高;
哈希计算简单:直接用 hashCode()+ 取模,无扰动优化,哈希冲突概率较高,且无红黑树优化,链表过长会退化查询效率;
遍历方式:支持迭代器 (快速失败,推荐)和Enumeration (旧版,仅兼容),迭代器支持边遍历边删除;
替代方案:无需线程安全用HashMap ,需要线程安全用ConcurrentHashMap (高低并发均适用),低并发兼容用Collections.synchronizedMap ;
面试核心考点:与 HashMap/ConcurrentHashMap 的区别、线程安全实现缺陷、扩容机制、null 不支持的原因,是 Java 集合框架中旧版设计的典型代表 。
Properties
前置基础:核心特性&设计定位
核心定位 java.util.Properties 是Hashtable 的子类 ,专门用于处理配置文件 的键值对集合,核心特性是键和值均为 String 类型 (底层自动做类型转换),并提供了从文件/流加载配置 、将配置写入文件/流 的便捷方法,是 Java 中处理'纯文本配置文件(如.properties/.ini)\u201d的原生标准工具。
它继承了 Hashtable 的线程安全 、哈希表结构 特性,同时针对配置场景做了轻量化封装,适用于加载项目的基础配置(如数据库连接信息、系统参数、业务配置等),是 Java 后端开发中最基础、最常用的配置处理类。
核心特性
键值均为 String :底层虽继承 Hashtable(K/V 为 Object),但对外暴露的核心方法仅支持 String 类型,自动做 Object↔String 转换,避免类型转换异常;
继承 Hashtable :复用哈希表结构,增删查平均 O(1) 时间复杂度,天生线程安全 (所有方法加 synchronized 锁),区别于 HashMap/HashSet 的非线程安全;
配置文件专属方法 :提供 load()(从文件/流加载配置)、store()(将配置写入文件/流)、loadFromXML()/storeToXML()(XML 格式配置)等核心方法,适配配置文件的读写;
支持默认值获取 :提供 getProperty(String key, String defaultValue) 方法,键不存在时返回默认值,避免空指针,适配配置缺失场景;
配置项有序性 :JDK1.6+ 后,Properties 底层实际使用LinkedHashtable 实现,保留配置项的加载/插入顺序 ,遍历顺序与配置文件中的顺序一致;
支持系统属性关联 :提供 getProperty(String key) 方法,若自身配置中无该键,会自动从'系统属性(System.getProperties())\u201d中查找,实现配置兜底;
轻量无依赖 :作为 JDK 核心类(java.util 包),无需引入第三方依赖,是处理纯文本配置的原生方案;
编码兼容 :JDK1.8+ 的 load(Reader)/store(Writer) 支持自定义编码(如 UTF-8),解决早期默认 ISO-8859-1 的中文乱码问题。
与核心 Map 的对比(开发/面试必问) Properties 是专用型 Map ,与通用 Map(HashMap/Hashtable/LinkedHashMap)的核心区别是定位不同 ,前者专为配置文件设计,后者为通用键值对存储:
特性 Properties Hashtable LinkedHashMap HashMap 键值类型 强制 String(底层隐式转换) Object/Object Object/Object Object/Object 核心定位 配置文件读写 通用线程安全 Map 通用有序 Map 通用高性能 Map 线程安全 是(继承 Hashtable) 是(方法加 synchronized) 否 否 有序性 是(JDK1.6+,插入/加载顺序) 否(JDK1.6 前) 是(插入/访问顺序) 否 配置专属方法 有(load/store/XML) 无 无 无 默认值获取 支持(getProperty) 无 无 无 系统属性关联 支持 无 无 无 平均时间复杂度 O(1)(继承哈希表) O(1) O(1) O(1) 空键/空值支持 不支持 null 键/值 不支持 null 键/值 支持 1 个 null 键/多 null 值 支持 1 个 null 键/多 null 值 适用场景 配置文件(.properties)读写 高并发通用键值存储 有序通用键值存储 非并发通用键值存储
一、底层核心原理:Hashtable 的子类+String 键值封装 Properties 的底层设计非常简洁,继承 Hashtable<Object, Object> ,并通过封装专属的 String 类型方法 屏蔽底层的 Object 类型,同时新增配置文件读写、默认值获取等特性,核心逻辑可总结为:
Hashtable(哈希表 + 线程安全) + String 键值强制转换 + 配置文件 IO 方法 + 系统属性关联 = Properties
1. 核心类结构与属性 Properties 无新增底层哈希表相关属性,完全继承 Hashtable 的所有属性(如数组、容量、加载因子等),仅新增 1 个用于父配置兜底 的属性,核心类定义如下:
public class Properties extends Hashtable <Object, Object> {
继承 Hashtable 而非 HashMap:为了天生的线程安全 ,适配配置文件在多线程环境下的加载/读取(如项目启动时的全局配置);
保留 defaults 父配置:实现多层配置兜底 (如全局配置→模块配置→业务配置),键不存在时逐级查找;
无独立哈希表实现:完全复用 Hashtable 的哈希表结构、扩容机制(默认容量 11,加载因子 0.75,扩容为 原容量*2+1),无需重复开发。
2. 核心特性的底层实现
(1)键值均为 String:底层隐式转换 Properties 对外暴露的'所有核心方法(如 getProperty/setProperty)\u201d均强制使用 String 类型,底层通过 Object.toString() 和强制类型转换实现 Object↔String 的转换,若传入非 String 类型,会自动调用 toString():
关键限制 :Properties不支持 null 键和 null 值 (继承 Hashtable 的特性),若传入 null,会抛出 NullPointerException。
(2)线程安全:继承 Hashtable 的 synchronized 锁 Properties 的所有核心方法(setProperty/getProperty/load/store/clear 等),要么直接继承 Hashtable 的加 synchronized 锁 的方法,要么自身加锁,保证所有操作的原子性:
Hashtable 的 put/get/remove/containsKey 等方法均被 synchronized 修饰;
Properties 的 load/store 等 IO 方法,自身也添加了 synchronized 锁,避免多线程同时读写配置文件。
(3)有序性:JDK1.6+ 底层为 LinkedHashtable JDK1.6 之前,Properties 的底层是纯 Hashtable,遍历无序;JDK1.6 及以后 ,Java 官方将 Properties 的底层实现改为LinkedHashtable (Hashtable 的子类,类似 LinkedHashMap),在哈希表基础上维护了双向链表 ,因此保留配置项的加载顺序/插入顺序 ,遍历顺序与配置文件中的顺序完全一致。
(4)系统属性关联:getProperty 的兜底逻辑 getProperty(String key) 的核心查找逻辑为三级兜底 ,是 Properties 的重要特性:
先从当前 Properties 实例 中查找键,存在则返回对应 String 值;
若不存在,且存在 defaults 父配置,则从父配置 中递归查找;
若仍不存在,调用 System.getProperty(key) 从JVM 系统属性 中查找(如 java.version、user.dir 等);
所有层级均无则返回 null。
3. 底层结构示意图 Properties 的底层结构 = Hashtable 的数组 + 链表 哈希表 + 双向链表(JDK1.6+) + 三级兜底引用,结合配置文件的读写能力:
二、核心构造方法:极简,支持父配置兜底 Properties 仅提供2 个构造方法 ,均为无参/单参,核心作用是初始化实例并指定父配置兜底集合 ,无容量/加载因子指定方法(复用 Hashtable 的默认值:初始容量 11,加载因子 0.75):
1. 无参构造:new Properties() public Properties () { this (null );
最常用构造方法,适用于单一层级的配置 (如单独加载一个.properties 文件);
无父配置兜底,getProperty 仅会查找当前实例和系统属性。
2. 带父配置的构造:new Properties(Properties defaults) public Properties (Properties defaults) { this .defaults = defaults;
适用于多层配置兜底场景 (如全局公共配置 + 模块专属配置);
示例:先加载全局配置 globalProps,再加载模块配置 moduleProps = new Properties(globalProps),模块配置中未定义的键会从全局配置中查找。
关键注意点
无自定义容量/加载因子 :Properties 不提供指定初始容量、加载因子的构造方法,固定使用 Hashtable 的默认值(容量 11,加载因子 0.75),因配置文件的键值对数量通常较少,无需自定义;
父配置为浅引用 :defaults 仅保存父配置的引用,若父配置后续修改,当前 Properties 的 getProperty 会获取到最新值;
线程安全的初始化 :构造方法无锁,但后续的所有操作方法均为线程安全,可在多线程中安全使用。
三、核心方法:三大类(String 键值操作/配置 IO/辅助方法) Properties 的方法分为三大核心类别 ,均为配置场景量身设计,摒弃了 Hashtable 的通用 Object 方法,对外仅暴露 String 类型的便捷操作,核心方法覆盖配置的增删查、文件读写、兜底获取 全流程。
第一类:String 键值核心操作(替代 Hashtable 的 Object 方法) 这类方法是 Properties 的基础,强制键值为 String 类型,替代了 Hashtable 的 put/get/remove 等 Object 方法,推荐优先使用,避免类型转换问题。
方法名 功能 核心特点 setProperty(String key, String value)添加/修改配置项 键存在则覆盖值,返回旧值;键不存在则添加,返回 null getProperty(String key)获取配置项 三级兜底(当前→父配置→系统属性),无则返回 null getProperty(String key, String defaultValue)获取配置项(带默认值) 最常用!键不存在时直接返回默认值,避免空指针 remove(Object key)删除配置项 继承 Hashtable,键为 Object,实际建议传 String containsKey(Object key)判断是否包含键 继承 Hashtable,线程安全
常用示例 Properties props = new Properties ();
第二类:配置文件 IO 操作(核心,.properties/XML 格式) 这是 Properties 的核心价值 ,专门用于从文件/字节流/字符流 加载配置,或将配置写入文件/字节流/字符流 ,支持纯文本.properties 和XML 两种格式,解决了手动读取配置文件的繁琐。
1. 加载配置:load() 系列(加载.properties 纯文本) 用于从字节流/字符流 加载.properties 格式的配置文件,JDK1.8+ 推荐使用字符流(Reader) ,支持自定义编码(如 UTF-8),解决中文乱码问题。
void load(InputStream inStream):从字节流加载,默认编码 ISO-8859-1 ,中文会乱码,不推荐;
void load(Reader reader):从字符流加载,支持自定义编码 (如 FileReader 指定 UTF-8),推荐使用;
void load(LineNumberReader lnr):从行号字符流加载,适用于需要定位配置行号的场景。
# 这是注释(#/!开头为注释) db.url=jdbc:mysql:
Properties props = new Properties ();
2. 写入配置:store() 系列(写入.properties 纯文本) 用于将 Properties 中的配置项写入文件/字节流/字符流 ,生成标准的.properties 文件,同样推荐使用字符流(Writer) 支持自定义编码。
void store(OutputStream out, String comments):写入字节流,默认 ISO-8859-1,中文乱码;
void store(Writer writer, String comments):写入字符流,支持自定义编码,推荐;
comments :配置文件的注释,会写入文件头部,传 null 则无注释。
Properties props = new Properties (); props.setProperty("name" , "张三" ); props.setProperty("age" , "20" ); props.setProperty("gender" , "男" );
#User Config #Mon Jan 29 10 :00 :00 CST 2026 name=张三 age=20 gender=男
3. XML 格式配置:loadFromXML()/storeToXML() 支持 XML 格式的配置文件读写,适用于需要跨语言/跨系统的配置场景,底层自动处理 XML 的解析和生成,无需手动操作 DOM/SAX。
void loadFromXML(InputStream in):从字节流加载 XML 配置;
void storeToXML(OutputStream out, String comments):写入 XML 配置,默认 UTF-8 编码;
void storeToXML(OutputStream out, String comments, String encoding):自定义编码写入 XML。
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd" > <properties> <comment>数据库配置</comment> <entry key="db.url" >jdbc:mysql:
Properties props = new Properties ();
第三类:辅助方法(遍历/转换/系统属性) 用于将 Properties 转换为其他集合、遍历配置项、操作系统属性等,适配配置的后续处理。
Set<String> stringPropertyNames():获取所有配置项的键集(String 类型),推荐遍历使用 ;
Enumeration<?> propertyNames():获取所有键的枚举(底层 Hashtable 方法),兼容旧代码,不推荐;
void list(PrintStream out)/ void list(PrintWriter out):将配置项打印到控制台/流,适用于调试;
static Properties getProperties():获取 JVM 系统属性(System.getProperties() 的别名);
void clear():清空所有配置项,继承 Hashtable,线程安全。
遍历配置项示例(推荐 stringPropertyNames()) Properties props = new Properties (); props.setProperty("a" , "1" ); props.setProperty("b" , "2" ); props.setProperty("c" , "3" );
四、核心问题:中文乱码解决方案(必看) Properties 处理中文配置时极易出现乱码,核心原因是早期的字节流方法(load(InputStream)/store(OutputStream))默认使用 ISO-8859-1 编码 ,该编码不支持中文,导致中文被错误解析。
乱码的两种场景及解决方案
场景 1:加载.properties 文件时中文乱码 原因 :使用 load(InputStream) 字节流加载,默认 ISO-8859-1 编码,无法解析中文。
解决方案 :使用 load(Reader)字符流 ,通过 FileReader/InputStreamReader 指定UTF-8 编码 (推荐),示例:
场景 2:写入.properties 文件时中文乱码 原因 :使用 store(OutputStream) 字节流写入,默认 ISO-8859-1 编码,中文被转码为乱码。
解决方案 :使用 store(Writer)字符流 ,通过 FileWriter/OutputStreamWriter 指定UTF-8 编码 ,示例:
终极避坑原则 JDK1.8 及以后,始终使用字符流(Reader/Writer)的 load/store 方法,指定 UTF-8 编码 ,彻底避免中文乱码问题,摒弃字节流(InputStream/OutputStream)的相关方法。
五、典型使用场景(开发实战) Properties 是 Java 原生的配置处理工具,适用于轻量级、纯文本、无复杂结构 的配置场景,是项目启动时加载基础配置的首选,以下是最常用的实战场景:
场景 1:加载项目本地.properties 配置文件(最常用) 加载项目 resources 目录下的配置文件(如数据库配置、系统参数),结合类加载器 获取资源流,适配项目打包后的 jar 包场景:
场景 2:多层配置兜底(全局 + 模块) 通过 Properties(Properties defaults) 实现全局公共配置 和模块专属配置 的兜底,模块配置覆盖全局配置,未定义的键从全局配置中查找:
场景 3:写入自定义配置文件 动态生成/修改配置文件(如用户个性化配置、运行时参数保存),将内存中的 Properties 写入本地文件:
场景 4:读取 JVM 系统属性 通过 Properties 的 getProperty 或直接使用 System.getProperties() 读取 JVM 的系统属性(如 Java 版本、用户目录、项目路径):
Properties sysProps = System.getProperties();
六、开发/面试高频考点
1. Properties 的底层实现是什么?核心特性是什么?(核心考点)
底层是java.util.Hashtable 的子类 ,JDK1.6+ 后底层为 LinkedHashtable,继承了 Hashtable 的哈希表结构、线程安全、不支持 null 键值 特性;
核心特性:键值均为 String 类型 、提供配置文件 IO 专属方法(load/store) 、支持默认值获取 、三级兜底查找(当前→父配置→系统属性) 、JDK1.6+ 保留插入/加载顺序 、天生线程安全 。
2. Properties 和 HashMap/Hashtable 的核心区别是什么?
定位不同 :Properties 是配置文件专用 Map ,HashMap/Hashtable 是通用键值对 Map ;
键值类型 :Properties 强制 String 类型,HashMap/Hashtable 支持任意 Object 类型;
配置方法 :Properties 提供 load/store/XML 等配置 IO 方法,HashMap/Hashtable 无;
兜底逻辑 :Properties 支持父配置和系统属性兜底,HashMap/Hashtable 无;
有序性 :JDK1.6+ Properties 有序,Hashtable 无序,LinkedHashMap 有序;
null 支持 :Properties/Hashtable 不支持 null 键值,HashMap 支持 1 个 null 键/多 null 值;
线程安全 :Properties/Hashtable 天生线程安全,HashMap/LinkedHashMap 非线程安全。
3. Properties 处理中文时为什么会乱码?如何解决?(高频)
乱码原因 :早期的 load(InputStream) 和 store(OutputStream) 方法默认使用 ISO-8859-1 编码 ,该编码不支持中文,导致中文被错误解析/转码;
解决方案 :JDK1.8 及以后,始终使用字符流(Reader/Writer)的 load/store 方法 ,并明确指定UTF-8 编码 ,摒弃字节流的相关方法。
4. Properties 的 getProperty 方法的查找逻辑是什么? getProperty(String key) 采用三级兜底查找 ,顺序为:
先从当前 Properties 实例 中查找,存在则返回 String 值;
若不存在且存在 defaults 父配置,则从父配置 中递归查找;
若仍不存在,调用 System.getProperty(key) 从JVM 系统属性 中查找;
所有层级均无则返回 null。
5. Properties 是线程安全的吗?为什么?
是线程安全的 ;
原因:Properties 继承自 Hashtable,Hashtable 的所有核心方法(put/get/remove/containsKey)均被synchronized 修饰,Properties 的专属方法(load/store/getProperty)也自身添加了 synchronized 锁,保证所有操作的原子性。
6. JDK1.6 前后,Properties 的有序性有什么变化?
JDK1.6 之前 :Properties 的底层是纯 Hashtable,哈希表结构决定了遍历无序 ,与插入/加载顺序无关;
JDK1.6 及以后 :Java 官方将 Properties 的底层实现改为LinkedHashtable (Hashtable 的子类),在哈希表基础上维护了双向链表 ,因此保留配置项的插入/加载顺序 ,遍历顺序与配置文件中的顺序完全一致。
7. Properties 支持 null 键和 null 值吗?为什么?
不支持 ,调用 setProperty(null, "value") 或 setProperty("key", null) 会直接抛出 NullPointerException;
原因:Properties 继承自 Hashtable,Hashtable 的设计原则是拒绝 null 键和 null 值 ,底层会在 put/get 时做 null 校验,避免空指针异常。
8. 如何加载项目 resources 目录下的.properties 文件?适配 jar 包场景。
推荐使用'类加载器的 getResourceAsStream(String path)\u201d方法获取资源流,该方法会自动查找 resources 目录下的文件,适配项目打包后的 jar 包场景 (jar 包中的资源无法通过 File 访问);
示例:PropertiesDemo.class.getClassLoader().getResourceAsStream("config/db.properties");
注意:资源路径无需加 /,直接写相对路径,且使用字符流加载并指定 UTF-8 编码。
9. Properties 和 yml/yaml 配置文件的区别?适用场景是什么? Properties 是纯文本键值对配置 ,yml/yaml 是层级化结构化配置 ,二者互补,适用场景不同:
特性 Properties Yml/Yaml 结构 扁平键值对(key=value) 层级化(缩进表示层级) 可读性 简单配置友好,复杂配置繁琐 复杂层级配置友好,可读性高 数据类型 仅支持 String,需手动转换 原生支持 String/int/bool/list/map 解析工具 JDK 原生支持(Properties 类) 需第三方依赖(如 Spring Boot 的 snakeyaml) 适用场景 轻量级、简单、无层级的基础配置 复杂、层级化、多数据类型的业务配置
Properties :Java 原生无依赖,适合项目启动的基础配置 (如数据库、端口、系统参数);
yml/yaml :适合Spring Boot/Spring Cloud 等框架的业务配置 ,支持复杂的层级和数据类型。
七、核心总结
Properties 是Hashtable 的子类 ,Java 原生的配置文件专用 Map ,核心设计为哈希表 + String 键值封装 + 配置 IO 方法 + 线程安全 ,是处理.properties 纯文本配置的标准工具;
核心特性:键值均为 String 、天生线程安全 、JDK1.6+ 有序(加载/插入顺序) 、三级兜底查找 、配置文件读写专属方法 、不支持 null 键值 ;
核心避坑:始终使用字符流(Reader/Writer)的 load/store 方法并指定 UTF-8 编码 ,彻底解决中文乱码问题,摒弃字节流方法;
核心方法:setProperty(增改)、getProperty(key, defaultValue)(获取,推荐带默认值)、load(Reader)(加载)、store(Writer)(写入)、stringPropertyNames()(遍历);
加载资源:推荐使用'类加载器的 getResourceAsStream\u201d加载项目 resources 目录下的配置文件,适配 jar 包场景;
适用场景:轻量级、无复杂结构、纯文本 的配置(如数据库、端口、系统参数),JDK 原生无依赖,是项目基础配置的首选;
与 yml/yaml 的区别:Properties 是扁平键值对,JDK 原生支持;yml/yaml 是层级化结构,需第三方依赖,二者互补,分别适用于简单和复杂配置场景。
HashSet
前置基础:核心特性&设计定位
核心定位 java.util.HashSet 是 Java 集合框架中基于 HashMap 实现的无序、不可重复的 Set 集合 ,JDK1.2 引入,实现 Set 接口,本质是HashMap 的'键集'包装器 ——将 Set 的元素作为 HashMap 的键 ,值固定为一个静态常量 PRESENT,通过 HashMap 的键唯一性实现 Set 的不可重复性,通过 HashMap 的哈希表结构实现 O(1) 平均时间复杂度的增删查。
它是最常用的 Set 实现 ,非线程安全,支持 null 元素(仅一个),无序且遍历顺序不固定(由元素哈希值决定),适用于无需有序、仅需去重 的绝大多数场景,是 Set 接口的工业标准实现。
核心特性
基于 HashMap 实现 :无独立底层结构,直接复用 HashMap 的数组 + 链表/红黑树 哈希表、哈希计算、扩容机制、null 支持等,仅封装了 Set 的接口方法;
不可重复 :依托 HashMap 的键唯一性 实现——添加重复元素时,HashMap 的 put 方法会覆盖旧键,HashSet 则返回 false,保证元素唯一;
无序 :遍历顺序与元素插入顺序、自然顺序均无关,由元素的 hashCode() 和 HashMap 的哈希表结构决定,且扩容后遍历顺序可能改变;
支持 null 元素 :与 HashMap 一致,仅允许一个 null 元素 (作为 HashMap 的键);
非线程安全 :无同步锁,多线程并发修改会抛出 ConcurrentModificationException,高并发需手动加锁;
平均 O(1) 时间复杂度 :增(add)、删(remove)、查(contains)的平均时间复杂度均为 O(1),最坏 O(n)(哈希冲突严重,链表过长);
无索引 :实现 Set 接口,无 List 的索引操作(如 get(int index)),无法通过索引访问元素;
快速失败机制 :继承 HashMap 的 modCount,遍历过程中修改集合会触发快速失败;
轻量封装 :仅维护一个 HashMap 实例和一个静态常量值,代码极简,无额外性能开销。
与核心 Set 的对比(开发/面试必问)
1. HashSet vs LinkedHashSet(无序 vs 有序 Set) LinkedHashSet 是 HashSet 的子类,基于 LinkedHashMap 实现,核心区别是是否有序 ,其余特性(不可重复、null 支持、O(1) 效率)一致:
特性 HashSet LinkedHashSet 底层实现 基于 HashMap 基于 LinkedHashMap 有序性 无序(哈希表顺序) 有序(插入顺序/访问顺序) 底层结构 数组 + 链表/红黑树 数组 + 链表/红黑树 + 双向链表 遍历效率 中(需遍历哈希表空桶) 高(基于双向链表,无空桶) 内存占用 低 稍高(节点多 2 个指针) 插入性能 极致 O(1)(无额外维护) 略低(维护双向链表) 适用场景 纯去重,无需有序 去重 + 保留插入顺序 重复元素处理 覆盖键,返回 false 覆盖键,返回 false
2. HashSet vs TreeSet(哈希表 vs 红黑树 Set) TreeSet 基于 TreeMap 实现,底层是红黑树,核心区别是有序性、实现原理、时间复杂度 ,是有序 Set 的两大实现:
特性 HashSet TreeSet 底层实现 基于 HashMap(哈希表) 基于 TreeMap(红黑树) 有序性 无序 有序(自然顺序/自定义 Comparator) 不可重复实现 哈希值 +equals 自然比较/Comparator null 元素支持 允许 1 个 null 不支持 null(抛 ClassCastException) 时间复杂度 平均 O(1),最坏 O(n) O(logn)(红黑树操作) 元素要求 重写 hashCode+equals 实现 Comparable/自定义 Comparator 遍历方式 哈希表遍历 红黑树中序遍历 适用场景 纯去重,追求极致性能 去重 + 按键排序 扩容机制 哈希表扩容(×2) 无扩容,动态平衡红黑树
3. HashSet vs ArraySet(JDK vs Android) ArraySet 是 Android 系统提供的 Set 实现,底层是数组,适用于小数据量 场景,JDK 中无此实现:
特性 HashSet ArraySet(Android) 底层实现 哈希表 有序数组 时间复杂度 平均 O(1) O(logn)(二分查找) 内存占用 高(哈希表冗余) 低(无冗余) 数据量适配 大数据量 小数据量(<1000) 插入性能 大数据量更优 小数据量更优 有序性 无序 有序(自然顺序)
一、底层核心原理:HashMap 的包装器 HashSet 的核心设计是'复用 HashMap 实现 Set\u201d,无自己的底层结构,仅通过一个 HashMap 实例存储元素,将 Set 的**元素作为 HashMap 的键',值固定为一个私有的静态常量 PRESENT(Object 类型),通过 HashMap 的键特性实现 Set 的所有核心功能。
这种设计属于组合模式 (通过组合 HashMap 实现功能,而非继承),避免了继承 HashMap 带来的方法冗余,同时严格遵循 Set 接口的规范。
1. 核心底层属性 HashSet 的源码极简,仅 3 个核心属性,全部围绕 HashMap 展开:
public class HashSet <E> extends AbstractSet <E> implements Set <E>, Cloneable, java.io.Serializable {
PRESENT 是静态常量 ,所有 HashSet 实例共享同一个对象,无需为每个键创建新值,大幅节省内存;
仅维护 HashMap 的引用,无额外数据结构,实现了 Set 接口的'最小封装'。
2. 底层结构示意图 HashSet 的底层结构就是 HashMap 的哈希表结构,元素仅作为 HashMap 的键存在,值固定为 PRESENT:
元素 null 作为 HashMap 的键,存储在哈希表的索引 0 位置;
元素 B、C 哈希冲突,形成链表,作为 HashMap 的键串联;
所有键对应的值都是同一个 PRESENT,无实际业务意义。
3. 核心特性的底层映射 HashSet 的所有特性均由 HashMap 的特性直接映射而来,无需额外实现:
HashSet 特性 底层 HashMap 的实现依据 不可重复 HashMap 的键唯一性(hashCode+equals) 无序 HashMap 的哈希表遍历顺序(由 hashCode 决定) 支持一个 null 元素 HashMap 允许一个 null 键 平均 O(1) 时间复杂度 HashMap 的哈希表增删查效率 快速失败机制 继承 HashMap 的 modCount 扩容机制(默认 16,0.75) 复用 HashMap 的扩容规则
二、核心构造方法 HashSet 提供4 个构造方法 ,全部通过创建对应的 HashMap 实例实现 ,构造参数与 HashMap 完全一致,仅封装了 Set 的接口,无额外逻辑:
1. 空参构造:new HashSet<>()
最常用构造方法,默认容量 16、加载因子 0.75,满足绝大多数纯去重场景;
HashMap 采用懒加载 ,首次 add 元素时才初始化底层数组。
2. 指定初始容量/加载因子:new HashSet(int initialCapacity)/new HashSet(int initialCapacity, float loadFactor) public HashSet (int initialCapacity) {
适合已知元素大致数量 的场景,避免频繁扩容,提升性能;
初始容量建议设置为 (预计元素数 / 0.75) + 1,与 HashMap 的容量计算规则一致,减少扩容次数。
3. 传入 Collection 初始化:new HashSet(Collection<? extends E> c) public HashSet (Collection<? extends E> c) {
将传入的 Collection(如 List、ArrayList)元素批量去重 后存入 HashSet;
若传入的 Collection 包含重复元素,仅保留一个(依托 HashMap 的键唯一性);
时间复杂度 O(n),n 为传入 Collection 的元素个数。
关键注意点
与 HashMap 参数完全一致 :HashSet 的构造参数本质是 HashMap 的构造参数,无自己的参数规则;
仅支持一个 null 元素 :添加多个 null 元素时,后续的 add 操作会返回 false,仅保留一个 null;
元素的唯一性要求 :存入 HashSet 的元素必须重写 hashCode()和 equals()方法 ,否则会导致重复元素(HashMap 无法判断键是否相同)。
三、核心 Set 接口方法:全部委托给 HashMap HashSet 实现 Set 接口的所有核心方法,无任何自己的实现逻辑,全部委托给底层的 HashMap ,方法的入参/返回值适配 Set 接口的规范(如将 HashMap 的键操作转为 Set 的元素操作),平均时间复杂度均为 O(1)。
1. 添加元素:add(E e)(核心,实现去重) 核心作用 :添加元素到 Set,若元素已存在则返回 false ,若不存在则添加并返回 true,实现不可重复性。
public boolean add (E e) {
当元素 e不存在 时,HashMap.put 返回 null,因此 map.put(...) == null 为 true,HashSet.add 返回 true(添加成功);
当元素 e已存在 时,HashMap.put 会覆盖旧值并返回旧值 PRESENT ,因此 map.put(...) == null 为 false,HashSet.add 返回 false(添加失败,保证唯一)。
关键要求 :存入的元素必须重写 hashCode() 和 equals(),否则 HashMap 无法判断键是否重复,会导致 HashSet 存入重复元素。
2. 删除元素:remove(Object o) 核心作用 :从 Set 中删除指定元素,若元素存在则删除并返回 true,不存在则返回 false。
public boolean remove (Object o) {
逻辑适配 :将 HashMap 的'删除键并返回值'转换为 Set 的'删除元素并返回是否存在'。
3. 包含判断:contains(Object o) 核心作用 :判断 Set 中是否包含指定元素,包含返回 true,否则返回 false,平均时间复杂度 O(1)。
public boolean contains (Object o) {
效率核心 :依托 HashMap 的哈希表查找,无需遍历所有元素,平均 O(1) 效率。
4. 空判断/获取大小:isEmpty()/size() public boolean isEmpty () {
无额外计算,直接返回 HashMap 的状态,时间复杂度 O(1)。
5. 清空集合:clear()
仅清空哈希表的节点引用,不缩容 ,底层数组的容量仍为当前值。
6. 转换为数组:toArray() public Object[] toArray() {
转换后的数组是HashMap 的键集数组 ,顺序与 HashSet 的遍历顺序一致(无序)。
四、遍历方式:与 HashMap 键集遍历一致 HashSet无独立的遍历实现 ,所有遍历方式均委托给底层 HashMap 的键集(keySet) ,遍历顺序与 HashMap 的键集遍历顺序一致(无序,由哈希值决定),支持'增强 for 循环、迭代器、forEach(JDK8+)\u201d三种方式,均为 O(n) 时间复杂度,无普通 for 循环(无索引,不支持) 。
1. 增强 for 循环(推荐,语法简洁) HashSet<String> set = new HashSet <>(); set.add("Java" ); set.add("Python" ); set.add("C++" ); set.add(null );
注意 :遍历顺序与插入顺序无关,且扩容后遍历顺序可能改变。
2. 迭代器遍历(推荐,支持边遍历边删除) HashSet 的迭代器是HashMap 键集的迭代器 ,支持快速失败机制 ,且仅能通过迭代器的 remove() 方法边遍历边删除(唯一安全方式):
Iterator<String> it = set.iterator(); while (it.hasNext()) { String s = it.next(); if (s == null || s.equals("Python" )) { it.remove();
禁止直接调用 set.remove(),会修改 modCount,触发快速失败,抛出 ConcurrentModificationException;
迭代器不支持 add() 方法(Set 的迭代器均为只读,除了 remove)。
3. forEach 方法(JDK8+,推荐,Lambda 风格)
与增强 for 循环一致,无序遍历,不支持边遍历边删除。
遍历注意点
无序性 :所有遍历方式的顺序均由元素的 hashCode() 和 HashMap 的哈希表结构决定,与插入顺序无关;
快速失败 :遍历过程中,若通过非迭代器方式(如 set.add/remove)修改集合,会立即抛出 ConcurrentModificationException;
null 元素处理 :遍历中需注意 null 元素,避免空指针异常(如调用 s.equals() 前需判断 s != null);
无索引遍历 :Set 接口无索引,无法通过普通 for 循环(i++)遍历,这是 Set 与 List 的核心区别。
五、元素的唯一性:hashCode() + equals() 重写要求 HashSet 的不可重复性 完全依赖 HashMap 的键唯一性 ,而 HashMap 判断键是否相同的规则是:
两个对象的hashCode() 返回值相等且 equals(Object o)返回 true,则认为是同一个键。
因此,存入 HashSet 的元素必须正确重写 hashCode()和 equals()方法 ,否则会导致重复元素存入 ,违背 Set 的设计初衷。
1. 未重写的问题:重复元素存入 若元素类未重写 hashCode() 和 equals(),则使用Object 类的默认实现 :
hashCode():返回对象的内存地址哈希值 ,每个新对象的 hashCode() 都不同;
equals():判断两个对象的内存地址是否相同 (即 this == o)。
此时,即使两个对象的属性完全相同,HashMap 也会认为是不同的键,HashSet 会存入重复元素:
2. 正确重写的原则(通用规范) 重写 hashCode() 和 equals() 需遵循三大原则 ,保证 HashMap/HashSet 的正确工作:
(1)equals 相等,hashCode 必须相等 若两个对象通过 equals() 判断为相等,则它们的 hashCode() 必须返回相同的值(否则 HashMap 会将其放入不同的桶,无法判断重复)。
(2)hashCode 相等,equals 不一定相等 hashCode 相等仅表示两个对象的哈希值相同,可能发生哈希冲突,需通过 equals() 进一步判断是否为同一个对象。
(3)重写 equals 时,必须同时重写 hashCode 单独重写其中一个会导致原则 1 被破坏,引发重复元素问题。
3. 正确重写示例(Student 类) 基于对象的业务主键 (如 name+age)重写,保证属性相同的对象 hashCode 相等且 equals 为 true:
class Student { private String name; private int age; public Student (String name, int age) { this .name = name; this .age = age; }
推荐工具 :IDEA/Eclipse 可自动生成 hashCode() 和 equals() 方法,遵循通用规范,无需手动编写。
4. 特殊情况:null 元素的唯一性 HashSet 允许一个 null 元素,因为 HashMap 对 null 键有特殊处理:
null 键的 hashCode() 被固定为 0,映射到哈希表的索引 0 位置;
多次添加 null 时,HashMap 会判断键已存在,HashSet.add 返回 false,仅保留一个 null。
六、线程安全处理 HashSet非线程安全 ,与 HashMap 一致,多线程并发操作(如同时 add/remove/遍历)会导致以下问题:
数据不一致 :多个线程同时 add,可能导致重复元素存入(HashMap 的 put 非原子操作);
结构错乱 :哈希表的链表/红黑树结构被破坏,遍历得到错误结果;
快速失败 :遍历过程中其他线程修改集合,抛出 ConcurrentModificationException;
数据丢失 :多线程同时 remove,可能导致元素被重复删除。
线程安全处理方案(3 种,优先级从高到低)
1. 低并发场景:Collections.synchronizedSet 包装(推荐,实现简单) 通过集合工具类 Collections.synchronizedSet 将 HashSet 包装为线程安全的 Set ,底层为 SynchronizedSet,所有方法均加 synchronized 全局锁:
实现简单,一行代码即可完成包装;
并发效率低 (全局锁,所有操作串行执行),仅适合低并发 场景(如少量线程的简单操作);
遍历必须手动加锁,否则仍可能触发快速失败。
2. 高并发场景:ConcurrentHashMap+Collections.newSetFromMap(推荐,高效) JDK1.6+ 提供 Collections.newSetFromMap 方法,可通过ConcurrentHashMap 构建线程安全的 Set (ConcurrentHashMap 是高并发 HashMap,节点级锁,并发效率极高):
依托 ConcurrentHashMap 的高并发特性 (CAS+ 节点级 synchronized),多线程可并行操作,并发效率远高于 synchronizedSet;
遍历采用弱一致性 ,不会抛出 ConcurrentModificationException,无需手动加锁;
与 HashSet 特性一致:支持一个 null 元素、O(1) 平均时间复杂度、无序。
3. 高并发有序场景:CopyOnWriteArraySet(适合遍历多、修改少) CopyOnWriteArraySet 是 JUC 提供的线程安全 Set,底层基于 CopyOnWriteArrayList,采用写时复制 策略:
写操作(add/remove)时复制整个数组,读/遍历操作无锁 ,效率极高;
适合遍历操作远多于修改操作 的高并发场景(如日志收集、配置查询);
缺点:写操作成本高(复制数组),数据存在弱一致性 (遍历的是旧数组);
有序:遍历顺序与插入顺序一致,无哈希表的无序性。
七、开发/面试高频考点
1. HashSet 的底层实现是什么?核心原理是什么?(核心考点)
底层基于 HashMap 实现 ,无独立的底层结构,是 HashMap 的'键集包装器';
核心原理:将 Set 的元素作为 HashMap 的键 ,值固定为一个静态常量 PRESENT,通过 HashMap 的'键唯一性(hashCode+equals)\u201d实现 Set 的不可重复性,通过 HashMap 的哈希表结构实现 O(1) 的平均时间复杂度。
2. HashSet 如何保证元素的唯一性?
依托HashMap 的键唯一性 实现,而 HashMap 判断键是否相同的规则是:两个对象的 hashCode() 相等 且 equals(Object o) 返回 true ;
当添加元素时,HashSet.add 会委托给 HashMap.put,若键已存在(hashCode+equals 相同),put 返回旧值 PRESENT,add 返回 false,元素添加失败,保证唯一性。
3. 存入 HashSet 的元素为什么要重写 hashCode() 和 equals()?如果只重写其中一个会怎样?
重写的原因 Object 类的默认 hashCode() 返回对象内存地址的哈希值,equals() 判断内存地址是否相同,若不重写,即使两个对象属性完全相同,HashMap 也会认为是不同的键,导致 HashSet 存入重复元素。
只重写一个的问题
仅重写 equals,未重写 hashCode :两个 equals 相等的对象,hashCode 可能不同,HashMap 会将其放入不同的桶,无法判断为同一个键,导致重复元素;
仅重写 hashCode,未重写 equals :两个 hashCode 相等的对象,equals 可能返回 false,HashMap 会认为是不同的键(哈希冲突),导致重复元素。
结论 :必须同时重写 hashCode() 和 equals(),且遵循'equals 相等则 hashCode 必相等'的原则。
4. HashSet 支持 null 元素吗?支持几个?
支持,仅允许一个 null 元素 ;
原因:HashMap 允许一个 null 键,HashSet 将元素作为 HashMap 的键,多次添加 null 时,HashMap 会判断键已存在,HashSet.add 返回 false,仅保留一个 null。
5. HashSet 是有序的吗?为什么?
无序 ;
原因:HashSet 的遍历顺序由底层 HashMap 的键集遍历顺序决定,而 HashMap 的键集顺序由元素的 hashCode() 和哈希表的桶结构决定,与元素的插入顺序、自然顺序均无关 ,且扩容后遍历顺序可能改变。
6. HashSet 和 HashMap 的关系是什么?
组合关系 :HashSet 通过组合 HashMap 实现所有功能,而非继承,属于设计模式中的组合模式 ;
包装器 :HashSet 是 HashMap 的'键集包装器',仅暴露 Set 接口的方法,隐藏 HashMap 的值操作;
特性完全映射 :HashSet 的所有特性(不可重复、null 支持、O(1) 效率、扩容机制)均由 HashMap 的特性决定。
7. HashSet 的 add 方法返回 false 的情况有哪些? 两种情况会导致 add 返回 false,元素添加失败:
元素已存在 :该元素的 hashCode() 和 equals() 与 Set 中已有元素相同,HashMap.put 返回旧值 PRESENT;
添加重复的 null :Set 中已存在 null 元素,再次添加 null 时,HashMap 判断键已存在。
8. HashSet 与 LinkedHashSet、TreeSet 的核心区别?如何选择?(核心考点)
核心区别 维度 HashSet LinkedHashSet TreeSet 底层实现 HashMap(哈希表) LinkedHashMap(哈希表 + 双向链表) TreeMap(红黑树) 有序性 无序 有序(插入顺序) 有序(自然/自定义顺序) 唯一性实现 hashCode+equals hashCode+equals Comparable/Comparator null 支持 允许 1 个 null 允许 1 个 null 不支持 null 时间复杂度 平均 O(1) 平均 O(1) O(logn) 内存占用 低 稍高 中 元素要求 重写 hashCode+equals 重写 hashCode+equals 实现 Comparable/自定义 Comparator
选择原则
选 HashSet :绝大多数场景的首选 ,纯去重、无需有序、追求极致性能;
选 LinkedHashSet :需要去重 + 保留插入顺序 的场景(如日志去重、按插入顺序遍历);
选 TreeSet :需要去重 + 按键排序 的场景(如数据排序、排行榜、范围查询)。
9. HashSet 是线程安全的吗?如何实现线程安全?(高频)
低并发:Collections.synchronizedSet(new HashSet<>())(实现简单);
高并发:Collections.newSetFromMap(new ConcurrentHashMap<>())(推荐,效率极高);
遍历多修改少:CopyOnWriteArraySet(写时复制,读无锁)。
10. HashSet 的扩容机制是什么?
HashSet 无独立的扩容机制,完全复用 HashMap 的扩容规则 ;
默认初始容量 16,加载因子 0.75,扩容阈值=容量×0.75;
当元素个数≥扩容阈值时,扩容为原容量×2 (保证 2 的幂),重新哈希所有元素并复制到新数组;
初始容量建议设置为 (预计元素数 / 0.75) + 1,减少扩容次数。
八、核心总结
HashSet 是基于 HashMap 实现的无序、不可重复 Set ,本质是 HashMap 的键集包装器,无独立底层结构,所有功能均委托给 HashMap;
核心特性:不可重复(hashCode+equals)、无序、支持一个 null、平均 O(1) 时间复杂度、非线程安全 ,是 Set 接口的最常用实现;
底层巧思:将元素作为 HashMap 的键,值固定为静态常量 PRESENT,节省内存,实现最小封装;
关键要求:存入的元素必须同时重写 hashCode() 和 equals() ,否则会导致重复元素,违背 Set 的设计初衷;
遍历方式:支持增强 for、迭代器、forEach,均为无序遍历,迭代器支持边遍历边删除(仅能通过 it.remove());
线程安全:低并发用 Collections.synchronizedSet 包装,高并发首选基于 ConcurrentHashMap 构建的 Set,遍历多修改少用 CopyOnWriteArraySet;
选型原则:纯去重选 HashSet,去重 + 插入顺序选 LinkedHashSet,去重 + 排序选 TreeSet ,HashSet 是绝大多数场景的首选;
面试核心考点:底层实现、元素唯一性的保证、hashCode+equals 的重写要求、与 HashMap/LinkedHashSet/TreeSet 的区别、线程安全处理,是 Java 集合框架中组合模式的经典案例 。
LinkedHashSet
前置基础:核心特性&设计定位
核心定位 java.util.LinkedHashSet 是HashSet 的子类 ,JDK1.4 引入,底层基于LinkedHashMap 实现,是有序、不可重复的 Set 集合 。它完全复用 HashSet 的包装器设计,将 Set 元素作为 LinkedHashMap 的键,值固定为常量 PRESENT,通过 LinkedHashMap 的哈希表 + 双向链表 结构,既保留了 HashSet 的O(1) 平均时间复杂度、去重特性、null 支持 ,又实现了插入顺序的有序遍历 ,解决了 HashSet 遍历无序的问题。
它是去重 + 有序场景的首选 Set 实现 ,非线程安全,遍历效率高于 HashSet,仅在维护双向链表上增加少量性能开销,适用于需要保留元素插入顺序且去重 的场景(如日志去重、配置项有序存储、按添加顺序遍历去重数据等)。
核心特性
基于 LinkedHashMap 实现 :继承 HashSet,将底层 HashMap 替换为 LinkedHashMap,复用其数组 + 链表/红黑树 + 双向链表 结构,无独立底层逻辑;
有序性 :默认维护插入顺序 (唯一有序模式),遍历顺序与元素首次插入顺序完全一致 ,重复添加已存在元素不会改变其在有序链表中的位置;
不可重复 :与 HashSet 一致,依托 LinkedHashMap 的'键唯一性(hashCode+equals)\u201d实现,添加重复元素返回 false;
继承 HashSet 所有特性 :支持一个 null 元素 、非线程安全、平均 O(1) 增删查时间复杂度、快速失败机制、无索引访问;
遍历效率更高 :基于 LinkedHashMap 的双向链表遍历 ,跳过哈希表的空桶,遍历节点数等于实际元素数,效率远高于 HashSet 的哈希表遍历;
轻量继承 :仅重写了 HashSet 的所有构造方法,将底层 Map 初始化为 LinkedHashMap,无额外代码,完全复用父类的 add/remove/contains 等方法;
扩容不破坏有序 :扩容时复用 LinkedHashMap 的逻辑,双向链表的节点顺序在复制时保持不变,有序性不会因扩容丢失;
内存占用稍高 :因 LinkedHashMap 的节点多 before/after 双向链表指针,内存占用略高于 HashSet,但开销极小。
与核心 Set 的对比(开发/面试必问) 这是 Set 接口最核心的三个实现类对比,覆盖了无序、插入有序、排序有序 所有场景,是选型的关键依据:
特性 HashSet LinkedHashSet TreeSet 底层实现 HashMap(哈希表) LinkedHashMap(哈希表 + 双向链表) TreeMap(红黑树) 有序性 无序(哈希表顺序) 有序(插入顺序,唯一) 有序(自然/自定义比较顺序) 唯一性实现 键的 hashCode+equals 键的 hashCode+equals 元素的 Comparable/Comparator null 元素支持 允许 1 个 null 允许 1 个 null 不支持 null(抛异常) 平均时间复杂度 增删查 O(1) 增删查 O(1)(稍高,维护双向链表) 增删查 O(logn)(红黑树操作) 遍历效率 中(遍历哈希表,含空桶) 高(遍历双向链表,无空桶) 中(红黑树中序遍历) 内存占用 最低 稍高(节点多 2 个指针) 中等 元素要求 重写 hashCode+equals 重写 hashCode+equals 实现 Comparable/自定义 Comparator 扩容机制 哈希表×2(2 的幂) 哈希表×2(2 的幂) 无扩容,动态平衡红黑树 适用场景 纯去重,无需有序 去重 + 保留插入顺序 去重 + 按键排序/范围查询
一、底层核心原理:HashSet+LinkedHashMap LinkedHashSet 的设计是双重复用 的经典案例,无任何自己的核心实现,核心逻辑可总结为:
继承 HashSet,将父类中的 HashMap 替换为 LinkedHashMap,通过 LinkedHashMap 的插入顺序特性实现有序,复用 HashSet 的 Set 接口包装逻辑实现去重 。
简单来说,LinkedHashSet = HashSet 的 Set 接口封装 + LinkedHashMap 的插入有序特性 。
1. 核心底层属性 LinkedHashSet无自己的成员属性 ,完全继承 HashSet 的所有属性(map、PRESENT),仅通过构造方法将 map 的实际类型从 HashMap 改为 LinkedHashMap:
2. 底层结构示意图 底层就是 LinkedHashMap 的结构,元素作为LinkedHashMap 的键 ,值为 PRESENT,哈希表负责 O(1) 增删查,双向链表负责维护插入顺序:
哈希表:解决元素的高效增删查,处理哈希冲突;
双向链表:仅维护元素的首次插入顺序 ,与哈希表结构互不干扰,增删元素时同步更新。
3. 核心特性的底层映射 LinkedHashSet 的所有特性均由HashSet 和LinkedHashMap 共同提供,无额外实现:
LinkedHashSet 特性 底层实现依据 不可重复 HashSet 的包装逻辑+LinkedHashMap 的键唯一性(hashCode+equals) 插入有序 LinkedHashMap 的双向链表维护插入顺序 支持一个 null 元素 LinkedHashMap 允许一个 null 键 平均 O(1) 时间复杂度 LinkedHashMap 的哈希表结构 遍历效率高 LinkedHashMap 的双向链表遍历(无空桶) 快速失败机制 继承 LinkedHashMap 的 modCount 扩容不破坏有序 LinkedHashMap 扩容时保留双向链表顺序
二、核心构造方法:重写并初始化 LinkedHashMap LinkedHashSet仅重写了 HashSet 的所有 4 个构造方法 ,核心逻辑是将父类 HashSet 中的 map属性初始化为 LinkedHashMap ,所有构造参数均透传给 LinkedHashMap,无任何自己的逻辑,代码极简。
注:HashSet 的构造方法原本是初始化 HashMap,LinkedHashSet 通过重写构造方法,替换为 LinkedHashMap,这是其实现有序的唯一关键操作 。
1. 空参构造:new LinkedHashSet<>()
父类 HashSet 的包私有构造方法 (HashSet(int initialCapacity, float loadFactor, boolean dummy))是专门为 LinkedHashSet 设计的,dummy 为占位符,仅用于区分其他构造方法,作用是初始化 LinkedHashMap 而非 HashMap。
2. 指定初始容量/加载因子:new LinkedHashSet(int initialCapacity)/new LinkedHashSet(int initialCapacity, float loadFactor) public LinkedHashSet (int initialCapacity) {
与 HashSet 的容量规则一致,初始容量建议设置为 (预计元素数 / 0.75) + 1,减少扩容次数;
加载因子默认 0.75(时间/空间的平衡值),非特殊场景无需修改。
3. 传入 Collection 初始化:new LinkedHashSet(Collection<? extends E> c) public LinkedHashSet (Collection<? extends E> c) {
将传入的 Collection(如 List、HashSet)元素批量去重 后存入,双向链表按传入集合的迭代顺序 构建;
若传入的是有序集合(如 ArrayList),则保留其顺序;若为无序集合(如 HashSet),则保留其哈希遍历顺序。
关键注意点
仅支持插入顺序 :LinkedHashSet 的底层是 LinkedHashMap,但仅使用其插入顺序模式 (accessOrder=false),不支持访问顺序,因此无法像 LinkedHashMap 那样实现 LRU 缓存;
无访问顺序模式 :LinkedHashSet 没有提供指定 accessOrder 的构造方法,这是与 LinkedHashMap 的核心区别,也是 Set 接口的设计初衷(Set 无需访问顺序);
继承的方法完全复用 :LinkedHashSet 没有重写 HashSet 的 add/remove/contains/size 等任何核心方法,所有操作均通过父类 HashSet 委托给底层的 LinkedHashMap。
三、核心方法:完全复用 HashSet,无新增/重写 LinkedHashSet没有重写 HashSet 的任何核心方法 ,add/remove/contains/clear/size 等所有 Set 接口方法,均直接继承自 HashSet,而 HashSet 又将这些方法委托给底层的 LinkedHashMap。
所有方法的逻辑、时间复杂度、返回值规则与 HashSet完全一致 ,唯一的区别是底层执行的是 LinkedHashMap 的方法 ,在增删元素时会同步维护双向链表的插入顺序。
1. 添加元素:add(E e)(去重 + 保留插入顺序)
元素不存在:LinkedHashMap.put 将元素作为键插入,同时将节点添加到双向链表尾部 ,返回 null,add 返回 true;
元素已存在:LinkedHashMap.put 覆盖旧值(仍为 PRESENT),返回旧值 PRESENT,add 返回 false,且不会改变该元素在双向链表中的位置 (保留首次插入顺序);
重复添加 null:与 HashSet 一致,仅保留一个 null,add 返回 false。
2. 删除元素:remove(Object o)(同步维护双向链表)
元素存在:LinkedHashMap.remove 从哈希表中删除节点,同时将该节点从双向链表中移除 ,保证双向链表结构完整,返回 PRESENT,remove 返回 true;
元素不存在:返回 null,remove 返回 false。
3. 包含判断:contains(Object o)(O(1) 效率)
依托 LinkedHashMap 的哈希表查找,平均时间复杂度 O(1),与 HashSet 效率一致;
比 TreeSet 的 O(logn) 效率高,是有序 Set 中查询效率最高的实现。
4. 其他核心方法(均继承自 HashSet) 方法名 功能 时间复杂度 size()获取元素个数 O(1) isEmpty()判断是否为空 O(1) clear()清空所有元素 O(n) toArray()转换为数组(插入顺序) O(n) iterator()获取迭代器(插入顺序) O(1)
关键特性 :toArray() 返回的数组顺序、迭代器的遍历顺序,均与元素首次插入顺序完全一致 。
四、遍历方式:有序遍历(插入顺序),效率更高 LinkedHashSet 的遍历方式与 HashSet语法完全一致 (增强 for、迭代器、forEach),但遍历顺序是固定的插入顺序 ,且遍历效率远高于 HashSet ,原因是其遍历基于 LinkedHashMap 的双向链表 ,无需遍历哈希表的空桶,直接遍历所有实际元素。
所有遍历方式的时间复杂度均为 O(n) (n 为实际元素数),而 HashSet 的遍历时间复杂度是 O(m)(m 为哈希表容量,m≥n),当哈希表稀疏时,LinkedHashSet 的遍历效率提升明显。
1. 增强 for 循环(推荐,语法简洁,插入顺序) LinkedHashSet<String> lhs = new LinkedHashSet <>(); lhs.add("Java" ); lhs.add("Python" ); lhs.add("C++" ); lhs.add(null ); lhs.add("Python" );
2. 迭代器遍历(推荐,支持边遍历边删除) LinkedHashSet 的迭代器是LinkedHashMap 键集的迭代器 ,基于双向链表实现,支持快速失败机制 ,仅能通过迭代器的 remove() 方法边遍历边删除(唯一安全方式):
Iterator<String> it = lhs.iterator(); while (it.hasNext()) { String s = it.next(); if (s == null || s.equals("C++" )) { it.remove();
注意 :禁止直接调用 lhs.remove(),会修改 modCount,触发 ConcurrentModificationException。
3. forEach 方法(JDK8+,Lambda 风格,插入顺序)
遍历核心优势
顺序固定 :无论哈希表如何扩容、哈希冲突如何处理,遍历顺序始终与首次插入顺序 一致;
效率更高 :跳过哈希表的空桶,直接遍历实际元素,哈希表越稀疏,效率优势越明显;
无额外开销 :双向链表的遍历是线性的,无需任何哈希计算,遍历过程简单高效。
五、元素的唯一性:与 HashSet 完全一致(hashCode+equals) LinkedHashSet 的不可重复性 与 HashSet、LinkedHashMap 完全一致,依托键的 hashCode() 和 equals() 方法 判断元素是否相同,因此存入 LinkedHashSet 的元素必须同时重写这两个方法 ,否则会导致重复元素存入。
1. 重写要求(与 HashSet 一致)
equals() 相等的两个对象,hashCode() 必须返回相同值;
hashCode() 相等的两个对象,equals() 不一定相等(哈希冲突);
重写 equals() 时,必须同时重写 hashCode()。
2. 未重写的问题(与 HashSet 一致) 若元素类未重写这两个方法,会使用 Object 类的默认实现:
hashCode():返回对象的内存地址哈希值,每个新对象的哈希值都不同;
equals():判断两个对象的内存地址是否相同(this == o)。
此时,即使两个对象的属性完全相同,LinkedHashMap 也会认为是不同的键,LinkedHashSet 会存入重复元素,违背 Set 的设计初衷。
3. 正确重写示例(与 HashSet 通用) 基于对象的业务主键 重写,保证属性相同的对象满足 hashCode() 相等且 equals() 为 true:
class User { private String id; private String name; public User (String id, String name) { this .id = id; this .name = name; }
4. null 元素的处理(与 HashSet 一致) LinkedHashSet 允许一个 null 元素 ,因为 LinkedHashMap 对 null 键有特殊处理:
null 键的哈希值固定为 0,映射到哈希表索引 0 位置;
多次添加 null 时,LinkedHashMap 判断键已存在,LinkedHashSet.add 返回 false,仅保留一个 null,且 null 在双向链表中的位置为首次插入的位置 。
六、线程安全处理 LinkedHashSet非线程安全 ,与 HashSet、LinkedHashMap 一致,多线程并发操作(同时 add/remove/遍历)会导致以下问题:
数据不一致 :多线程同时 add,可能导致重复元素存入(LinkedHashMap 的 put 非原子操作);
双向链表结构错乱 :增删元素时未同步,导致有序链表断裂,遍历得到错误顺序;
快速失败 :遍历过程中其他线程修改集合,抛出 ConcurrentModificationException;
数据丢失 :多线程同时 remove,可能导致元素被重复删除,或链表指针异常。
其线程安全处理方案与 HashSet完全一致 ,但因 LinkedHashSet 是有序 Set,需注意有序性的线程安全 (即多线程操作后,遍历顺序仍符合预期),推荐方案分低并发 和高并发 ,优先级明确。
线程安全处理方案(3 种,按优先级排序)
1. 低并发场景:Collections.synchronizedSet 包装(实现简单,首选) 通过 Collections.synchronizedSet 将 LinkedHashSet 包装为线程安全的 Set,底层为 SynchronizedSet,所有方法加 synchronized 全局锁,保证单操作原子性:
一行代码实现,无需额外开发;
并发效率低 (全局锁,所有操作串行),仅适合低并发 场景(如少量线程的简单增删查);
能保证有序性的线程安全 ,操作后遍历顺序仍为插入顺序。
2. 高并发场景:ConcurrentLinkedHashMap+Collections.newSetFromMap(推荐,高效) JDK 原生无高并发的 LinkedHashMap,但可通过Guava 的 ConcurrentLinkedHashMap (线程安全的 LinkedHashMap)结合 Collections.newSetFromMap,构建高并发、有序、线程安全的 Set ,保留插入顺序和 O(1) 效率:
依托 ConcurrentLinkedHashMap 的节点级锁 ,多线程可并行操作,并发效率极高;
保留插入顺序 ,是高并发有序 Set 的最佳实现 ;
需引入 Guava 依赖(Maven/Gradle),JDK 原生不支持。
3. 遍历多修改少场景:CopyOnWriteArraySet(有序,写时复制) CopyOnWriteArraySet 是 JUC 提供的线程安全 Set,底层基于 CopyOnWriteArrayList,采用写时复制 策略,遍历顺序与插入顺序一致 ,适合遍历操作远多于修改操作 的高并发场景:
读/遍历无锁 ,效率远超其他方案,无快速失败机制;
写操作(add/remove)复制整个数组,成本高 ,仅适合修改频率极低的场景;
天然有序(插入顺序),无需依赖 LinkedHashSet,是 JDK 原生的高并发有序 Set 方案。
七、开发/面试高频考点
1. LinkedHashSet 的底层实现是什么?核心原理是什么?(核心考点)
底层是HashSet 的子类,基于 LinkedHashMap 实现 ,无独立底层结构;
核心原理:继承 HashSet 的包装器设计(元素作为 Map 的键,值为 PRESENT),将父类中的 HashMap 替换为 LinkedHashMap,通过 LinkedHashMap 的哈希表 + 双向链表 结构,既依托哈希表实现 O(1) 增删查和去重(hashCode+equals),又依托双向链表实现插入顺序的有序遍历 。
2. LinkedHashSet 和 HashSet 的核心区别是什么?为什么 LinkedHashSet 是有序的?
核心区别
有序性 :LinkedHashSet 是插入有序 ,遍历顺序与首次插入顺序一致;HashSet 是无序,遍历顺序由哈希值决定;
底层实现 :LinkedHashSet 基于 LinkedHashMap(哈希表 + 双向链表);HashSet 基于 HashMap(仅哈希表);
遍历效率 :LinkedHashSet 遍历效率更高(双向链表,无空桶);HashSet 遍历效率较低(哈希表,含空桶);
内存占用 :LinkedHashSet 稍高(节点多 before/after 指针);HashSet 内存占用更低。
有序的原因 LinkedHashSet 的底层 LinkedHashMap 维护了一条双向链表 ,所有元素节点都加入该链表,新元素始终插入到链表尾部,重复添加不改变节点位置,删除元素同步移除链表节点 ,遍历基于该双向链表实现,因此保证了插入顺序。
3. LinkedHashSet 支持访问顺序吗?为什么?
不支持 ;
原因:1. LinkedHashSet 的设计目标是保留插入顺序 ,Set 接口的使用场景无需访问顺序;2. LinkedHashSet 仅重写了构造方法,将 LinkedHashMap 初始化为插入顺序模式(accessOrder=false) ,未提供任何设置访问顺序的入口;3. 若需要访问顺序的 Set,需自定义实现(无原生支持)。
4. LinkedHashSet 和 TreeSet 的核心区别?如何选择有序 Set?(高频)
核心区别
有序实现 :LinkedHashSet 是插入顺序 (双向链表,无需排序,天然有序);TreeSet 是比较顺序 (红黑树,基于 Comparable/Comparator 排序);
时间复杂度 :LinkedHashSet 增删查平均 O(1) ;TreeSet 增删查O(logn) ;
元素要求 :LinkedHashSet 要求重写hashCode+equals ;TreeSet 要求元素实现Comparable 或自定义 Comparator;
null 支持 :LinkedHashSet 允许1 个 null ;TreeSet 不支持 null(抛 ClassCastException);
适用场景 :LinkedHashSet 用于去重 + 保留插入顺序 ;TreeSet 用于去重 + 按键排序/范围查询 。
选择原则
选 LinkedHashSet :需要去重且保留元素的插入顺序 ,无需排序,追求 O(1) 效率(绝大多数有序 Set 场景);
选 TreeSet :需要去重且按元素的自然顺序/自定义顺序排序 ,或需要范围查询(如 ceiling/floor/higher)。
5. LinkedHashSet 重复添加已存在的元素,会改变其在有序链表中的位置吗?
不会 ;
原因:重复添加已存在的元素时,LinkedHashMap 的 put 方法仅会覆盖值(仍为 PRESENT),不会执行任何双向链表的节点移动操作,元素在双向链表中的位置始终为首次插入的位置 ,保证了插入顺序的唯一性。
6. LinkedHashSet 的扩容会破坏有序性吗?为什么?
不会 ;
原因:LinkedHashSet 的扩容复用 LinkedHashMap 的扩容逻辑,扩容时仅将原哈希表的节点复制到新数组,节点的before/after 双向链表指针保持不变 ,双向链表的整体顺序与原顺序完全一致,因此有序性不会因扩容丢失。
7. LinkedHashSet 需要重写 hashCode 和 equals 吗?为什么?
需要,且必须同时重写 ;
原因:LinkedHashSet 的不可重复性依托 LinkedHashMap 的键唯一性 实现,而 LinkedHashMap 判断键是否相同的规则是:hashCode() 相等且 equals() 返回 true。若不重写,会使用 Object 类的默认实现(基于内存地址),导致属性相同的对象被认为是不同元素,存入重复数据。
8. LinkedHashSet 是线程安全的吗?如何实现线程安全的有序 Set?
非线程安全 ,无任何同步锁;
线程安全有序 Set 的实现方案:
低并发:Collections.synchronizedSet(new LinkedHashSet<>())(实现简单,全局锁);
高并发:Guava 的 ConcurrentLinkedHashMap + Collections.newSetFromMap(高效,保留插入顺序);
遍历多修改少:JUC 的 CopyOnWriteArraySet(写时复制,读无锁,天然插入有序)。
9. LinkedHashSet 的构造方法有什么特点?为什么要重写所有构造方法?
特点:LinkedHashSet仅重写了构造方法 ,无任何其他重写/新增方法,所有构造方法均调用父类 HashSet 的包私有构造方法,将底层 Map 初始化为 LinkedHashMap;
原因:HashSet 的默认构造方法是初始化 HashMap,LinkedHashSet 需要通过重写构造方法,替换底层 Map 的实现为 LinkedHashMap ,这是其实现插入有序的唯一关键操作 ,因此必须重写所有构造方法,保证无论通过哪种方式创建 LinkedHashSet,底层都是 LinkedHashMap。
10. LinkedHashSet 和 LinkedHashMap 的关系是什么?
底层依赖关系 :LinkedHashSet 的底层是 LinkedHashMap,所有操作均委托给 LinkedHashMap;
包装与被包装关系 :LinkedHashSet 是 LinkedHashMap 的Set 接口包装器 ,将 LinkedHashMap 的键集暴露为 Set,隐藏值操作(值固定为 PRESENT);
特性继承关系 :LinkedHashSet 的所有特性(有序、去重、null 支持、O(1) 效率)均由 LinkedHashMap 提供,HashSet 仅做了接口封装。
八、核心总结
LinkedHashSet 是HashSet 的子类,基于 LinkedHashMap 实现的插入有序、不可重复 Set ,无独立底层结构,是 Set 接口中去重 + 有序场景的首选实现 ;
核心设计:双重复用 ,复用 HashSet 的 Set 包装器逻辑(元素为键,值为 PRESENT),复用 LinkedHashMap 的哈希表 + 双向链表结构(O(1) 效率 + 插入有序);
核心特性:插入有序(唯一有序模式)、不可重复(hashCode+equals)、支持一个 null、平均 O(1) 增删查、遍历效率高、非线程安全 ,重复添加元素不改变其有序位置,扩容不破坏有序性;
关键实现:仅重写了 HashSet 的所有构造方法,将底层 HashMap 替换为 LinkedHashMap,无任何其他重写/新增方法,所有核心操作均继承自 HashSet 并委托给 LinkedHashMap;
元素要求:存入的元素必须同时重写 hashCode() 和 equals() ,否则会导致重复元素,违背 Set 的设计初衷;
遍历方式:支持增强 for、迭代器、forEach,均为插入顺序的有序遍历 ,基于双向链表实现,效率远高于 HashSet;
线程安全:低并发用 Collections.synchronizedSet 包装,高并发用 Guava 的 ConcurrentLinkedHashMap 构建,遍历多修改少用 CopyOnWriteArraySet;
选型原则:纯去重选 HashSet,去重 + 插入顺序选 LinkedHashSet,去重 + 排序选 TreeSet ,三者覆盖了 Set 的所有主流使用场景;
面试核心:底层实现、与 HashSet/TreeSet 的区别、有序性的实现原理、hashCode+equals 的重写要求、线程安全方案,是 Java 集合框架中复用设计的经典案例 。
目录
HashMap 前置基础:核心成员变量 JDK1.7 JDK1.8 一、初始化:4 个构造方法 1\. 无参构造:public HashMap() 核心作用 JDK1.7 源码 + 解析 JDK1.8 源码 + 解析 阅读重点 2\. 指定初始容量:public HashMap(int initialCapacity) 核心作用 JDK1.7/1.8 通用源码 + 解析 阅读重点 3\. 指定初始容量 + 负载因子:public HashMap(int initialCapacity, float loadFactor) 核心作用 核心源码 阅读重点 4\. 传入 Map 构造:public HashMap(Map<? extends K, ? extends V> m) 核心作用 JDK1.7 源码 + 解析 JDK1.8 源码 + 解析 阅读重点 二、添加/修改:核心方法 1\. 核心插入:public V put(K key, V value) 核心作用 JDK1.7 源码 + 解析 JDK1.8 源码 + 解析(核心简化,新增树化) 核心差异&阅读重点 2\. 批量添加:public void putAll(Map<? extends K, ? extends V> m) 核心作用 核心源码 阅读重点 三、容量管理:扩容核心方法 final Node<K,V>[] resize() 核心作用 JDK1.7 源码 + 解析 JDK1.8 源码 + 解析(优化迁移,新增红黑树拆分) 核心差异&阅读重点 四、删除元素:核心方法 1\. 核心删除:public V remove(Object key) 核心作用 JDK1.7 源码 + 解析 JDK1.8 源码 + 解析 阅读重点 2\. 清空集合:public void clear() 核心源码 + 解析(JDK1.7/1.8 逻辑一致) 阅读重点 五、查询元素:核心方法 1\. 核心查询:public V get(Object key) 核心作用 JDK1.7 源码 + 解析 JDK1.8 源码 + 解析 核心差异&阅读重点 2\. 包含判断:public boolean containsKey(Object key) 核心源码 阅读重点 六、JDK1.7/1.8 核心差异总表 LinkedHashMap 前置基础:核心特性&设计定位 核心定位 核心特性 与核心 Map 的对比(开发/面试必问) 1\. LinkedHashMap vs HashMap(核心继承关系,有序 vs 无序) 2\. LinkedHashMap vs TreeMap(有序 Map 的两大实现) 3\. LinkedHashMap vs ConcurrentLinkedHashMap 一、底层核心原理:HashMap+ 双向链表 1\. 核心底层属性 2\. 节点定义:继承 HashMap.Node 并新增双向链表指针 3\. 底层结构示意图(插入顺序,容量 16) 4\. 有序模式核心逻辑 (1)插入顺序(默认,accessOrder=false) (2)访问顺序(accessOrder=true) 二、核心构造方法 1\. 空参构造:new LinkedHashMap<>() 2\. 指定初始容量/加载因子:new LinkedHashMap(int initialCapacity)/new LinkedHashMap(int initialCapacity, float loadFactor) 3\. 传入 Map 初始化:new LinkedHashMap(Map<? extends K, ? extends V> m) 4\. 核心构造:new LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) 关键注意点 三、核心底层逻辑:双向链表的维护(新增/访问/删除) 1\. 新增节点:维护双向链表(插入尾部) 2\. 访问节点:访问顺序模式下移到尾部(LRU 核心) 3\. 删除节点:同步移除双向链表中的节点 4\. 扩容时的有序性保留 四、核心方法:继承 + 增强(有序性相关) 1\. 查询方法:get(Object key)(重写,支持访问顺序) 2\. 包含值判断:containsValue(Object value)(重写,效率更高) 3\. 有序遍历(核心优势,基于双向链表) (1)插入顺序模式(默认):遍历与插入顺序一致 (2)访问顺序模式:遍历与访问时间一致(LRU) 4\. LRU 缓存实现:重写 removeEldestEntry(Map.Entry<K,V> eldest) (1)核心原理 (2)自定义 LRU 缓存(固定容量,线程不安全) (3)线程安全的 LRU 缓存 五、线程安全处理 线程安全处理方案(3 种,优先级从高到低) 1\. 低并发场景:Collections.synchronizedMap 包装 2\. 高并发场景:手动加锁(ReentrantLock) 3\. 高并发 LRU 缓存:Guava ConcurrentLinkedHashMap/ Caffeine 六、开发/面试高频考点 1\. LinkedHashMap 的底层实现是什么?核心原理是什么?(核心考点) 2\. LinkedHashMap 有哪两种有序模式?分别适用于什么场景? 3\. LinkedHashMap 如何实现 LRU 缓存?核心方法是什么? 4\. LinkedHashMap 和 HashMap 的核心区别?为什么 LinkedHashMap 是有序的? 核心区别 有序的原因 5\. LinkedHashMap 和 TreeMap 的区别?如何选择有序 Map? 核心区别 选择原则 6\. LinkedHashMap 的节点有什么特殊之处? 7\. LinkedHashMap 的扩容会破坏有序性吗?为什么? 8\. LinkedHashMap 的 containsValue 方法为什么比 HashMap 高效? 9\. LinkedHashMap 是线程安全的吗?如何实现线程安全? 10\. LinkedHashMap 中,重复 put 已存在的键,会改变其在双向链表中的位置吗? 七、核心总结 Hashtable 前置基础:核心特性&设计定位 核心定位 核心特性 与核心 Map 的对比(开发/面试必问) 1\. Hashtable vs HashMap(最核心对比,同源不同命) 2\. Hashtable vs ConcurrentHashMap(线程安全 Map 的新旧对比) 一、底层核心结构 1\. 核心底层属性 2\. Entry 节点定义(单向链表) 3\. 哈希表结构示意图(容量 11,加载因子 0.75,阈值 8) 二、核心构造方法 1\. 空参构造:new Hashtable<>() 2\. 指定初始容量:new Hashtable(int initialCapacity) 3\. 指定初始容量 + 加载因子:new Hashtable(int initialCapacity, float loadFactor) 4\. 传入 Map 初始化:new Hashtable(Map<? extends K, ? extends V> t) 关键注意点 三、核心底层逻辑:哈希计算与扩容机制 1\. 哈希计算(简单无优化,易冲突) 步骤 1:计算键的哈希值 步骤 2:映射到数组索引 2\. 扩容机制(固定规则,无优化) (1)扩容触发时机 (2)扩容核心步骤 (3)扩容特性 四、核心 Map 接口方法(线程安全版,O(1) 平均复杂度) 1\. 新增/修改键值对:put(K key, V value)(核心) 2\. 查询键值对:get(Object key)(核心) 3\. 删除键值对:remove(Object key)(核心) 4\. 其他核心方法 (1)判空/获取大小:isEmpty()/size() (2)包含判断:containsKey(Object key)/containsValue(Object value) (3)清空哈希表:clear() 五、遍历方式(两种:迭代器/Enumeration) 1\. 迭代器遍历(推荐,支持 Map 的标准遍历) (1)遍历键值对(entrySet,最常用) (2)遍历键/值 2\. Enumeration 遍历(旧版,仅兼容) 遍历注意点 六、Hashtable 的核心缺陷(面试高频,官方弃用原因) 缺陷 1:线程安全的实现方式导致并发效率极低 缺陷 2:哈希计算无优化,哈希冲突概率高 缺陷 3:键值均不支持 null,使用灵活性低 缺陷 4:扩容机制固定,扩容成本高且不灵活 缺陷 5:继承 Dictionary 类,设计冗余且不规范 缺陷 6:快速失败机制,遍历灵活性低 七、替代方案(开发首选,完全替代 Hashtable) 1\. 无需线程安全(单线程/多线程无并发修改):HashMap(首选) 2\. 需要线程安全(低/高并发):ConcurrentHashMap(首选) 3\. 低并发线程安全(兼容旧代码):Collections.synchronizedMap 八、开发/面试高频考点 1\. Hashtable 的底层实现是什么?核心结构是什么? 2\. Hashtable 为什么键值都不能为 null? 3\. Hashtable 和 HashMap 的核心区别有哪些?(核心考点) 4\. Hashtable 的线程安全是如何实现的?有什么缺陷? 5\. Hashtable 的扩容机制是什么?为什么扩容为原容量×2+1? 6\. Hashtable 为什么被官方弃用?替代方案是什么? 弃用原因 替代方案 7\. Hashtable 的哈希计算过程是怎样的?为什么不如 HashMap 高效? 计算过程 效率低的原因 8\. Hashtable 支持边遍历边删除吗?如何实现? 9\. Hashtable 和 ConcurrentHashMap 的线程安全实现有什么区别? 九、核心总结 Properties 前置基础:核心特性&设计定位 核心定位 核心特性 与核心 Map 的对比(开发/面试必问) 一、底层核心原理:Hashtable 的子类+String 键值封装 1\. 核心类结构与属性 2\. 核心特性的底层实现 (1)键值均为 String:底层隐式转换 (2)线程安全:继承 Hashtable 的 synchronized 锁 (3)有序性:JDK1.6+ 底层为 LinkedHashtable (4)系统属性关联:getProperty 的兜底逻辑 3\. 底层结构示意图 二、核心构造方法:极简,支持父配置兜底 1\. 无参构造:new Properties() 2\. 带父配置的构造:new Properties(Properties defaults) 关键注意点 三、核心方法:三大类(String 键值操作/配置 IO/辅助方法) 第一类:String 键值核心操作(替代 Hashtable 的 Object 方法) 常用示例 第二类:配置文件 IO 操作(核心,.properties/XML 格式) 1\. 加载配置:load() 系列(加载.properties 纯文本) 这是注释(#/!开头为注释) db.url=jdbc:mysql://localhost:3306/test db.username=root db.password=123456 # 多行值:末尾加\,下一行继续 db.params=useUnicode=true&characterEncoding=utf8\ &autoReconnect=true # 键值分隔符:= / : / 空格(推荐用=) db.driverClass com.mysql.cj.jdbc.Driver 2\. 写入配置:store() 系列(写入.properties 纯文本) 3\. XML 格式配置:loadFromXML()/storeToXML() 第三类:辅助方法(遍历/转换/系统属性) 遍历配置项示例(推荐 stringPropertyNames()) 四、核心问题:中文乱码解决方案(必看) 乱码的两种场景及解决方案 场景 1:加载.properties 文件时中文乱码 场景 2:写入.properties 文件时中文乱码 终极避坑原则 五、典型使用场景(开发实战) 场景 1:加载项目本地.properties 配置文件(最常用) 场景 2:多层配置兜底(全局 + 模块) 场景 3:写入自定义配置文件 场景 4:读取 JVM 系统属性 六、开发/面试高频考点 1\. Properties 的底层实现是什么?核心特性是什么?(核心考点) 2\. Properties 和 HashMap/Hashtable 的核心区别是什么? 3\. Properties 处理中文时为什么会乱码?如何解决?(高频) 4\. Properties 的 getProperty 方法的查找逻辑是什么? 5\. Properties 是线程安全的吗?为什么? 6\. JDK1.6 前后,Properties 的有序性有什么变化? 7\. Properties 支持 null 键和 null 值吗?为什么? 8\. 如何加载项目 resources 目录下的.properties 文件?适配 jar 包场景。 9\. Properties 和 yml/yaml 配置文件的区别?适用场景是什么? 七、核心总结 HashSet 前置基础:核心特性&设计定位 核心定位 核心特性 与核心 Set 的对比(开发/面试必问) 1\. HashSet vs LinkedHashSet(无序 vs 有序 Set) 2\. HashSet vs TreeSet(哈希表 vs 红黑树 Set) 3\. HashSet vs ArraySet(JDK vs Android) 一、底层核心原理:HashMap 的包装器 1\. 核心底层属性 2\. 底层结构示意图 3\. 核心特性的底层映射 二、核心构造方法 1\. 空参构造:new HashSet<>() 2\. 指定初始容量/加载因子:new HashSet(int initialCapacity)/new HashSet(int initialCapacity, float loadFactor) 3\. 传入 Collection 初始化:new HashSet(Collection<? extends E> c) 关键注意点 三、核心 Set 接口方法:全部委托给 HashMap 1\. 添加元素:add(E e)(核心,实现去重) 2\. 删除元素:remove(Object o) 3\. 包含判断:contains(Object o) 4\. 空判断/获取大小:isEmpty()/size() 5\. 清空集合:clear() 6\. 转换为数组:toArray() 四、遍历方式:与 HashMap 键集遍历一致 1\. 增强 for 循环(推荐,语法简洁) 2\. 迭代器遍历(推荐,支持边遍历边删除) 3\. forEach 方法(JDK8+,推荐,Lambda 风格) 遍历注意点 五、元素的唯一性:hashCode() + equals() 重写要求 1\. 未重写的问题:重复元素存入 2\. 正确重写的原则(通用规范) (1)equals 相等,hashCode 必须相等 (2)hashCode 相等,equals 不一定相等 (3)重写 equals 时,必须同时重写 hashCode 3\. 正确重写示例(Student 类) 4\. 特殊情况:null 元素的唯一性 六、线程安全处理 线程安全处理方案(3 种,优先级从高到低) 1\. 低并发场景:Collections.synchronizedSet 包装(推荐,实现简单) 2\. 高并发场景:ConcurrentHashMap+Collections.newSetFromMap(推荐,高效) 3\. 高并发有序场景:CopyOnWriteArraySet(适合遍历多、修改少) 七、开发/面试高频考点 1\. HashSet 的底层实现是什么?核心原理是什么?(核心考点) 2\. HashSet 如何保证元素的唯一性? 3\. 存入 HashSet 的元素为什么要重写 hashCode() 和 equals()?如果只重写其中一个会怎样? 重写的原因 只重写一个的问题 4\. HashSet 支持 null 元素吗?支持几个? 5\. HashSet 是有序的吗?为什么? 6\. HashSet 和 HashMap 的关系是什么? 7\. HashSet 的 add 方法返回 false 的情况有哪些? 8\. HashSet 与 LinkedHashSet、TreeSet 的核心区别?如何选择?(核心考点) 核心区别 选择原则 9\. HashSet 是线程安全的吗?如何实现线程安全?(高频) 10\. HashSet 的扩容机制是什么? 八、核心总结 LinkedHashSet 前置基础:核心特性&设计定位 核心定位 核心特性 与核心 Set 的对比(开发/面试必问) 一、底层核心原理:HashSet+LinkedHashMap 1\. 核心底层属性 2\. 底层结构示意图 3\. 核心特性的底层映射 二、核心构造方法:重写并初始化 LinkedHashMap 1\. 空参构造:new LinkedHashSet<>() 2\. 指定初始容量/加载因子:new LinkedHashSet(int initialCapacity)/new LinkedHashSet(int initialCapacity, float loadFactor) 3\. 传入 Collection 初始化:new LinkedHashSet(Collection<? extends E> c) 关键注意点 三、核心方法:完全复用 HashSet,无新增/重写 1\. 添加元素:add(E e)(去重 + 保留插入顺序) 2\. 删除元素:remove(Object o)(同步维护双向链表) 3\. 包含判断:contains(Object o)(O(1) 效率) 4\. 其他核心方法(均继承自 HashSet) 四、遍历方式:有序遍历(插入顺序),效率更高 1\. 增强 for 循环(推荐,语法简洁,插入顺序) 2\. 迭代器遍历(推荐,支持边遍历边删除) 3\. forEach 方法(JDK8+,Lambda 风格,插入顺序) 遍历核心优势 五、元素的唯一性:与 HashSet 完全一致(hashCode+equals) 1\. 重写要求(与 HashSet 一致) 2\. 未重写的问题(与 HashSet 一致) 3\. 正确重写示例(与 HashSet 通用) 4\. null 元素的处理(与 HashSet 一致) 六、线程安全处理 线程安全处理方案(3 种,按优先级排序) 1\. 低并发场景:Collections.synchronizedSet 包装(实现简单,首选) 2\. 高并发场景:ConcurrentLinkedHashMap+Collections.newSetFromMap(推荐,高效) 3\. 遍历多修改少场景:CopyOnWriteArraySet(有序,写时复制) 七、开发/面试高频考点 1\. LinkedHashSet 的底层实现是什么?核心原理是什么?(核心考点) 2\. LinkedHashSet 和 HashSet 的核心区别是什么?为什么 LinkedHashSet 是有序的? 核心区别 有序的原因 3\. LinkedHashSet 支持访问顺序吗?为什么? 4\. LinkedHashSet 和 TreeSet 的核心区别?如何选择有序 Set?(高频) 核心区别 选择原则 5\. LinkedHashSet 重复添加已存在的元素,会改变其在有序链表中的位置吗? 6\. LinkedHashSet 的扩容会破坏有序性吗?为什么? 7\. LinkedHashSet 需要重写 hashCode 和 equals 吗?为什么? 8\. LinkedHashSet 是线程安全的吗?如何实现线程安全的有序 Set? 9\. LinkedHashSet 的构造方法有什么特点?为什么要重写所有构造方法? 10\. LinkedHashSet 和 LinkedHashMap 的关系是什么? 八、核心总结 相关免费在线工具 Keycode 信息 查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
Escape 与 Native 编解码 JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
JavaScript / HTML 格式化 使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
JavaScript 压缩与混淆 Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
加密/解密文本 使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
Gemini 图片去水印 基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online