HashMap
前置基础:核心成员变量
JDK1.7
- 底层结构:数组 + 链表,数组为哈希桶,链表解决哈希冲突
- 核心节点:
Entry<K,V>链表节点,包含key、value、hash、next(链表指针)
深入解析 JDK 中核心哈希容器源码,涵盖 HashMap、LinkedHashMap、Hashtable、Properties、HashSet 及 LinkedHashSet。详细对比了 JDK 1.7 与 1.8 在底层结构、扩容机制、哈希计算等方面的差异,重点阐述了红黑树优化、尾插法改进及线程安全处理方案。同时介绍了 Properties 配置文件的读写技巧及中文乱码解决方案,并总结了各集合类的适用场景与面试高频考点,帮助开发者掌握 Java 集合框架的核心原理。
Entry<K,V> 链表节点,包含 key、value、hash、next(链表指针)// 默认初始容量 16(必须是 2 的幂,位运算优化) static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 最大容量 2^30 static final int MAXIMUM_CAPACITY = 1 << 30; // 默认负载因子,平衡内存与冲突 static final float DEFAULT_LOAD_FACTOR = 0.75f; // 底层哈希桶数组,存储 Entry 节点 transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE; // 空数组常量,懒加载使用 static final Entry<?,?>[] EMPTY_TABLE = {}; // 实际存储的键值对数量 transient int size; // 扩容阈值 = 容量 * 负载因子,超过则扩容 int threshold; // 负载因子,构造后不可变 final float loadFactor; // 修改次数,用于 fail-fast 机制 transient int modCount;
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; // 链表转红黑树阈值(链表长度≥8 触发) static final int TREEIFY_THRESHOLD = 8; // 红黑树转链表阈值(节点数≤6 触发) static final int UNTREEIFY_THRESHOLD = 6; // 树化最小容量(数组容量≥64 才树化,避免小容量频繁树化/退化) static final int MIN_TREEIFY_CAPACITY = 64; // 底层哈希桶数组,存储 Node/TreeNode transient Node<K,V>[] table; static final Node<?,?>[] EMPTY_TABLE = {}; transient int size; int threshold; final float loadFactor; transient int modCount;
public HashMap()创建默认配置(容量 16、负载因子 0.75)的 HashMap,懒加载延迟初始化数组
public HashMap() { this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR); } // 调用带参构造,初始化负载因子和阈值 public HashMap(int initialCapacity, float loadFactor) { this.loadFactor = loadFactor; threshold = initialCapacity; init(); // 空方法,供子类重写 }
threshold=16,但哈希桶 table 延迟至首次 put 时初始化public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; // 仅初始化负载因子,无其他计算 }
threshold 和 table 均延迟至首次 put 初始化,减少空实例内存占用JDK1.8 取消构造时的阈值计算,进一步优化懒加载逻辑
public HashMap(int initialCapacity)自定义初始容量,自动修正为最近的 2 的幂(保证哈希寻址效率)
public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); } // 核心:调用 tableSizeFor 修正容量 public HashMap(int initialCapacity, float loadFactor) { if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); this.loadFactor = loadFactor; // JDK1.7:直接赋值阈值,1.8:延迟计算,此处仅临时存储容量 this.threshold = tableSizeFor(initialCapacity); } // 容量修正核心方法:向上取整为最近的 2 的幂 static final int tableSizeFor(int cap) { int n = cap - 1; n |= n >>> 1; n |= n >>> 2; n |= n >>> 4; n |= n >>> 8; n |= n >>> 16; return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; }
tableSizeFor 通过位运算快速修正容量(如输入 10→16、输入 17→32),保证容量为 2 的幂hash & (容量 -1) 高效计算桶索引,等价于取模但性能更高public HashMap(int initialCapacity, float loadFactor)自定义容量和负载因子,适配不同性能/内存需求(如内存紧张时提高负载因子)
同上述 HashMap(int initialCapacity, float loadFactor),仅直接传入自定义 loadFactor
public HashMap(Map<? extends K, ? extends V> m)将现有 Map 转为 HashMap,复用原有键值对
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); } // 逐个调用 put 插入,无容量预计算 public void putAll(Map<? extends K, ? extends V> m) { int numKeysToAdd = m.size(); if (numKeysToAdd == 0) return; if (table == EMPTY_TABLE) { inflateTable(threshold); // 初始化数组 } for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) { put(e.getKey(), e.getValue()); } }
public HashMap(Map<? extends K, ? extends V> m) { this.loadFactor = DEFAULT_LOAD_FACTOR; putMapEntries(m, false); // 批量插入核心方法 } // 批量插入,预计算容量避免多次扩容 final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) { int s = m.size(); if (s > 0) { if (table == null) { // 数组未初始化 float ft = ((float)s / loadFactor) + 1.0F; int t = ((ft < (float)MAXIMUM_CAPACITY) ? (int)ft : MAXIMUM_CAPACITY); if (t > threshold) threshold = tableSizeFor(t); } else if (s > threshold) { // 数组已初始化,需扩容 resize(); } // 批量插入键值对 for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) { K key = e.getKey(); V value = e.getValue(); putVal(hash(key), key, value, false, evict); } } }
JDK1.8 新增 putMapEntries,先根据传入 Map 的大小预计算容量,减少扩容次数,比 1.7 逐个 put 更高效
public V put(K key, V value)添加键值对,若 key 已存在则覆盖旧值并返回旧值;不存在则新增,返回 null
public V put(K key, V value) { // 数组未初始化则先初始化 if (table == EMPTY_TABLE) { inflateTable(threshold); } // key 为 null 时,单独处理,哈希桶索引固定为 0 if (key == null) return putForNullKey(value); // 1. 计算 key 的哈希值(扰动处理,减少冲突) int hash = hash(key); // 2. 计算哈希桶索引 int i = indexFor(hash, table.length); // 3. 遍历链表,判断 key 是否存在 for (Entry<K,V> e = table[i]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; // 覆盖旧值 e.recordAccess(this); return oldValue; } } modCount++; // 4. key 不存在,头插法新增节点 addEntry(hash, key, value, i); return null; } // 哈希值扰动计算:4 次位运算 +1 次异或,让高位参与运算 final int hash(Object k) { int h = hashSeed; if (0 != h && k instanceof String) { return sun.misc.Hashing.stringHash32((String) k); } h ^= k.hashCode(); h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); } // 计算桶索引:利用 2 的幂特性,高效寻址 static int indexFor(int h, int length) { return h & (length - 1); } // 头插法新增节点,插入链表头部 void addEntry(int hash, K key, V value, int bucketIndex) { if ((size >= threshold) && (null != table[bucketIndex])) { resize(2 * table.length); // 扩容为原 2 倍 hash = (null != key) ? hash(key) : 0; bucketIndex = indexFor(hash, table.length); } createEntry(hash, key, value, bucketIndex); } void createEntry(int hash, K key, V value, int bucketIndex) { Entry<K,V> e = table[bucketIndex]; // 头插法:新节点 next 指向原头节点,桶索引指向新节点 table[bucketIndex] = new Entry<>(hash, key, value, e); size++; }
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); } // 哈希值扰动优化:1 次异或 +1 次无符号右移,简化但效果相当 static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } // 插入核心方法,整合扩容、树化、尾插法 final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; // 1. 数组未初始化则先扩容(初始化) if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; // 2. 桶为空,直接新增节点插入 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { Node<K,V> e; K k; // 3. 头节点 key 匹配,直接覆盖 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; // 4. 头节点是红黑树,树插入 else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); // 5. 遍历链表,尾插法新增 else { for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); // 链表长度≥8,触发树化判断 if (binCount >= TREEIFY_THRESHOLD - 1) treeifyBin(tab, hash); break; } // key 存在,跳出循环覆盖 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } // key 存在,覆盖旧值并返回 if (e != null) { V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } modCount++; // 6. 超过扩容阈值,触发扩容 if (++size > threshold) resize(); afterNodeInsertion(evict); return null; } // 树化方法:数组容量≥64 才树化,否则仅扩容 final void treeifyBin(Node<K,V>[] tab, int hash) { int n, index; Node<K,V> e; if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) resize(); else if ((e = tab[index = (n - 1) & hash]) != null) { TreeNode<K,V> hd = null, tl = null; do { TreeNode<K,V> p = replacementTreeNode(e, null); if (tl == null) hd = p; else { p.prev = tl; tl.next = p; } tl = p; } while ((e = e.next) != null); if ((tab[index] = hd) != null) hd.treeify(tab); } }
hash(key) 中,哈希值固定为 0,桶索引 0public void putAll(Map<? extends K, ? extends V> m)批量插入 Map 中的键值对,比循环 put 更高效
JDK1.7 直接遍历 put,JDK1.8 调用上述 putMapEntries(预计算容量 + 批量 putVal)
开发中批量插入优先使用 putAll,减少扩容次数和方法调用开销
final Node<K,V>[] resize()HashMap 动态扩容的核心,容量翻倍,重新哈希迁移所有节点,解决哈希冲突过多问题
void resize(int newCapacity) { Entry[] oldTable = table; int oldCapacity = oldTable.length; if (oldCapacity == MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return; } // 1. 新建 2 倍容量的新数组 Entry[] newTable = new Entry[newCapacity]; // 2. 头插法迁移节点,可能导致链表反转 transfer(newTable, initHashSeedAsNeeded(newCapacity)); table = newTable; // 3. 更新扩容阈值 threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1); } // 节点迁移核心:头插法 void transfer(Entry[] newTable, boolean rehash) { int newCapacity = newTable.length; for (Entry<K,V> e : table) { while(null != e) { Entry<K,V> next = e.next; if (rehash) { e.hash = null == e.key ? 0 : hash(e.key); } int i = indexFor(e.hash, newCapacity); // 头插法:新节点 next 指向新桶当前头节点 e.next = newTable[i]; newTable[i] = e; e = next; } } }
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. 计算新容量和新阈值 if (oldCap > 0) { // 原容量达最大值,不再扩容 if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } // 容量翻倍,阈值也翻倍 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; } // 初始化时的阈值(如无参构造首次扩容) else if (oldThr > 0) newCap = oldThr; // 无参构造首次扩容,默认容量 16,阈值 12 else { newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } // 计算新阈值 if (newThr == 0) { float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY) ? (int)ft : Integer.MAX_VALUE; } threshold = newThr; // 2. 新建 2 倍容量的新数组 @SuppressWarnings({"rawtypes","unchecked"}) Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab; // 3. 迁移原节点到新数组 if (oldTab != null) { for (int j = 0; j < oldCap; ++j) { Node<K,V> e; if ((e = oldTab[j]) != null) { oldTab[j] = null; // 桶内仅单个节点,直接计算新索引插入 if (e.next == null) newTab[e.hash & (newCap - 1)] = e; // 红黑树节点,拆分红黑树 else if (e instanceof TreeNode) ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); // 链表节点,尾插法迁移(无需重算哈希) else { Node<K,V> loHead = null, loTail = null; Node<K,V> hiHead = null, hiTail = null; Node<K,V> next; do { next = e.next; // 关键:通过 hash & oldCap 判断新索引,无需重算 hash if ((e.hash & oldCap) == 0) { if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } else { if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); // 低位节点:原索引插入 if (loTail != null) { loTail.next = null; newTab[j] = loHead; } // 高位节点:原索引+oldCap 插入 if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } return newTab; }
++size > threshold)hash & oldCap 直接判断新索引(0→原索引,1→原索引+oldCap),无需重算哈希,效率大幅提升resize(),逻辑更统一public V remove(Object key)根据 key 删除键值对,返回旧值;key 不存在则返回 null
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; // 遍历链表,找到待删除节点 while (e != null) { Entry<K,V> next = e.next; Object k; if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) { modCount++; size--; // 头节点删除:桶指向 next if (prev == e) table[i] = next; // 中间节点删除:prev.next 指向 next else prev.next = next; e.recordRemoval(this); return e; } prev = e; e = next; } return e; }
public V remove(Object key) { Node<K,V> e; return (e = removeNode(hash(key), key, null, false, true)) == null ? null : e.value; } // 删除核心方法,支持链表/红黑树删除 final Node<K,V> removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable) { Node<K,V>[] tab; Node<K,V> p; int n, index; if ((tab = table) != null && (n = tab.length) > 0 && (p = tab[index = (n - 1) & hash]) != null) { Node<K,V> node = null, e; K k; V v; // 头节点匹配 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) node = p; // 桶非空,遍历查找 else if ((e = p.next) != null) { // 红黑树节点,树查找 if (p instanceof TreeNode) node = ((TreeNode<K,V>)p).getTreeNode(hash, key); // 链表节点,遍历查找 else { do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) { node = e; break; } p = e; } while ((e = e.next) != null); } } // 找到节点,执行删除 if (node != null && (!matchValue || (v = node.value) == value || (value != null && value.equals(v)))) { // 红黑树删除,删除后调整结构,节点数≤6 则退化 if (node instanceof TreeNode) ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable); // 链表头节点删除 else if (node == p) tab[index] = node.next; // 链表中间节点删除 else p.next = node.next; modCount++; size--; afterNodeRemoval(node); return node; } } return null; }
removeNode,统一处理链表和红黑树删除,逻辑更简洁public void clear()public void clear() { Node<K,V>[] tab; modCount++; if ((tab = table) != null && size > 0) { size = 0; // 遍历数组,置空所有桶,让 GC 回收节点 for (int i = 0; i < tab.length; ++i) tab[i] = null; } }
resize() 指定小容量public V get(Object key)根据 key 获取 value,key 不存在则返回 null
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); // 哈希计算→桶定位→遍历链表匹配 for (Entry<K,V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } return null; } // key 为 null 单独处理,桶索引 0 private V getForNullKey() { if (size == 0) return null; for (Entry<K,V> e = table[0]; e != null; e = e.next) { if (e.key == null) return e.value; } return null; }
public V get(Object key) { Node<K,V> e; return (e = getNode(hash(key), key)) == null ? null : e.value; } // 查询核心方法,支持链表/红黑树查询 final Node<K,V> getNode(int hash, Object key) { Node<K,V>[] tab; Node<K,V> first, e; int n; K k; if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { // 头节点直接匹配,快速返回 if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k)))) return first; // 桶非空,继续查找 if ((e = first.next) != null) { // 红黑树查询,时间复杂度 O(logn) if (first instanceof TreeNode) return ((TreeNode<K,V>)first).getTreeNode(hash, key); // 链表查询,时间复杂度 O(n) do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } return null; }
getNode,统一处理链表/红黑树查询,头节点匹配直接返回,优化查询效率hash(key),无需单独处理,逻辑更统一public boolean containsKey(Object key)JDK1.7 调用 getEntry(key) != null,JDK1.8 调用 getNode(hash(key), key) != null
与 get 逻辑一致,仅返回布尔值,无额外性能开销
| 对比维度 | 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,统一处理链表/红黑树 |
java.util.LinkedHashMap 是HashMap 的子类,JDK1.4 引入,在 HashMap 的数组 + 链表/红黑树底层基础上,额外维护了一条双向链表,用于记录键值对的插入顺序/访问顺序,实现了有序的哈希表;非线程安全,继承 HashMap 的所有特性(支持 null 键值、哈希扰动、红黑树优化等),同时解决了 HashMap遍历无序的问题。
它是有序 Map 的首选实现(比 TreeMap 效率更高,无需排序),适用于需要保留插入顺序或实现LRU(最近最少使用)缓存的场景,平均增删查时间复杂度仍为 O(1),仅在有序维护上增加少量开销。
继承 HashMap:完全复用 HashMap 的底层结构(数组 + 链表/红黑树)、哈希计算、扩容机制、null 支持等,仅新增有序相关逻辑;
双向链表维护有序:通过**头节点(header)和尾节点(tail)**维护一条双向链表,所有键值对节点都加入该链表,记录顺序;
两种有序模式:
get/put/containsKey 等方法访问节点后,该节点会被移到双向链表尾部,遍历顺序为访问时间从早到晚(可实现 LRU 缓存);非线程安全:无同步锁,多线程并发修改会抛出 ConcurrentModificationException,高并发需手动加锁;
支持 null 键值:与 HashMap 一致,键允许 1 个 null,值允许多个 null;
迭代效率更高:遍历基于双向链表,无需遍历整个哈希表(跳过空桶),比 HashMap 的无序遍历效率更高;
快速失败机制:继承 HashMap 的 modCount,遍历过程中修改集合会触发快速失败;
扩容无额外成本:扩容时仅复用 HashMap 的扩容逻辑,双向链表的顺序在节点复制时自动保留,无需额外处理。
两者本质是子类与父类,LinkedHashMap 仅在 HashMap 基础上增加有序特性,核心区别是是否有序、遍历方式、底层节点:
| 特性 | LinkedHashMap | HashMap |
| 有序性 | 有序(插入/访问顺序) | 无序(由哈希值决定) |
| 底层结构 | 数组 + 链表/红黑树 + 双向链表 | 数组 + 链表/红黑树 |
| 节点类型 | Entry(继承 HashMap.Node,新增 before/after 指针) | Node/TreeNode |
| 遍历方式 | 基于双向链表(高效,无空桶) | 基于哈希表(需遍历空桶) |
| 重复 put 已存在键 | 不改变有序性 | 无有序性概念 |
| 访问节点后行为 | 访问顺序模式下移到尾部 | 无特殊行为 |
| 时间复杂度 | 平均 O(1)(略高于 HashMap,维护双向链表) | 平均 O(1) |
| 内存占用 | 稍高(节点多 2 个指针) | 稍低 |
| 适用场景 | 需保留顺序、LRU 缓存 | 纯键值对存储,追求极致性能 |
| 核心优势 | 有序且哈希表效率 | 极致的单线程性能 |
两者均为有序 Map,但有序实现方式、底层结构完全不同,LinkedHashMap 是绝大多数有序场景的首选:
| 特性 | LinkedHashMap | TreeMap |
| 有序实现 | 双向链表(插入/访问顺序,无需排序) | 红黑树(键的自然顺序/自定义 Comparator) |
| 时间复杂度 | 平均 O(1)(增删查) | O(logn)(增删查,红黑树操作) |
| 键要求 | 无要求(无需实现 Comparable) | 需实现 Comparable 或自定义 Comparator |
| null 键支持 | 允许 1 个 null 键 | 不支持 null 键(会抛 ClassCastException) |
| 底层结构 | 哈希表 + 双向链表 | 纯红黑树 |
| 遍历效率 | 高(双向链表直接遍历) | 中(红黑树中序遍历) |
| 适用场景 | 保留插入/访问顺序、LRU 缓存 | 需按键排序(升序/降序)的场景 |
| 性能开销 | 低(仅维护双向链表) | 高(红黑树旋转平衡) |
两者均支持 LRU 缓存,但核心区别是线程安全、是否 JDK 原生:
| 特性 | LinkedHashMap | ConcurrentLinkedHashMap |
| 线程安全 | 否(JDK 原生) | 是(第三方扩展,Guava 提供) |
| LRU 实现 | 重写removeEldestEntry即可 | 原生支持 LRU,无需自定义 |
| 底层结构 | 哈希表 + 双向链表 | 哈希表 + 双向链表 |
| null 支持 | 支持 | 不支持 |
| 适用场景 | 单线程 LRU 缓存 | 高并发 LRU 缓存 |
LinkedHashMap 的核心是继承 HashMap 并增强节点结构,通过在节点中增加 before/after 指针维护双向链表,既保留了 HashMap 的 O(1) 哈希表效率,又实现了有序遍历,是组合设计的经典案例。
public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V> { // 双向链表的头节点(最早插入/最早访问的节点) transient LinkedHashMap.Entry<K,V> head; // 双向链表的尾节点(最新插入/最新访问的节点) transient LinkedHashMap.Entry<K,V> tail; // 有序模式标识:true=访问顺序,false=插入顺序(默认) final boolean accessOrder; // 序列化版本号 private static final long serialVersionUID = 3801124242820219131L; }
核心常量:accessOrder 为 final,构造时指定且不可修改,决定 LinkedHashMap 的有序模式,默认 false(插入顺序)。
LinkedHashMap 的Entry 节点是 HashMap.Node 的子类,仅新增 before(前驱)和 after(后继)指针,用于构建双向链表,不改变 HashMap 的节点哈希特性:
static class Entry<K,V> extends HashMap.Node<K,V> { Entry<K,V> before, after; // 双向链表的前驱、后继指针 Entry(int hash, K key, V value, Node<K,V> next) { super(hash, key, value, next); // 调用 HashMap.Node 的构造方法 } }
节点双重身份:
next 指针用于解决哈希冲突,构成数组后的单向链表/红黑树;before/after 指针用于维护有序性,所有节点串联成双向链表。// 哈希表结构(数组 + 链表,与 HashMap 一致) 数组索引:0 1 2 ... 8 ... 15 ↓ ↓ ↓ ↓ ↓ 桶结构: - A - ... B→C ... - (B、C 哈希冲突,next 指针串联) // 双向链表结构(维护插入顺序:A→B→C,before/after 指针串联) head → A (before:null, after:B) → B (before:A, after:C) → C (before:B, after:null) → tail
get/put(已存在键)/containsKey 等方法访问节点后,该节点会被移除并重新插入到双向链表尾部(成为'最新访问节点');LinkedHashMap 提供4 个构造方法,均调用 HashMap 的构造方法完成哈希表初始化,再指定 accessOrder(默认 false),支持默认初始化、指定容量/加载因子、传入 Map、指定容量 + 加载因子 + 有序模式:
new LinkedHashMap<>()public LinkedHashMap() { // 调用 HashMap 空参构造(默认容量 16,加载因子 0.75) super(); // 默认插入顺序 accessOrder = false; }
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; }
new LinkedHashMap(Map<? extends K, ? extends V> m)public LinkedHashMap(Map<? extends K, ? extends V> m) { super(); accessOrder = false; // 调用 putAll 批量添加,双向链表按传入 Map 的迭代顺序构建 putAll(m); }
new LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder)public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) { super(initialCapacity, loadFactor); // 手动指定有序模式:true=访问顺序,false=插入顺序 this.accessOrder = accessOrder; }
new LinkedHashMap<>(16, 0.75f, true) 构建一个访问顺序的 LinkedHashMap。(预计元素数 / 0.75) + 1,减少扩容次数。LinkedHashMap 的有序性完全依赖双向链表的增删改,它通过重写 HashMap 的钩子方法实现双向链表的自动维护,无需修改 HashMap 的核心逻辑,这是其设计的精妙之处。
HashMap 为子类预留了3 个核心钩子方法,LinkedHashMap 通过重写这些方法,在哈希表节点增删时同步维护双向链表:
// HashMap 中的钩子方法,默认空实现,供子类重写 // 1. 新节点插入哈希表后调用 void afterNodeInsertion(boolean evict) {} // 2. 节点被访问后调用(get/put 已存在键) void afterNodeAccess(Node<K,V> e) {} // 3. 节点被删除后调用 void afterNodeRemoval(Node<K,V> e) {}
当调用 put 添加新节点(键不存在)时,HashMap 会先将节点插入哈希表(数组/链表/红黑树),然后调用 afterNodeInsertion,LinkedHashMap 重写该方法的核心逻辑是将新节点插入双向链表尾部:
// LinkedHashMap 重写:将节点链接到双向链表尾部 private void linkNodeLast(LinkedHashMap.Entry<K,V> p) { LinkedHashMap.Entry<K,V> last = tail; tail = p; if (last == null) // 双向链表为空,新节点作为头节点 head = p; else { // 双向链表非空,新节点接在尾部,更新前驱后继 p.before = last; last.after = p; } } // 重写 HashMap 的 newNode,创建 LinkedHashMap.Entry 节点并链接到尾部 Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) { LinkedHashMap.Entry<K,V> p = new LinkedHashMap.Entry<>(hash, key, value, e); linkNodeLast(p); return p; } // 红黑树节点的创建,同样链接到双向链表尾部 TreeNode<K,V> newTreeNode(int hash, K key, V value, Node<K,V> next) { TreeNode<K,V> p = new TreeNode<>(hash, key, value, next); linkNodeLast((LinkedHashMap.Entry<K,V>)p); return p; }
核心流程:创建 LinkedHashMap.Entry 节点 → 插入哈希表 → 调用 linkNodeLast 插入双向链表尾部,哈希表和双向链表同步更新。
当调用 get/put(已存在键) 访问节点时,HashMap 会找到该节点,然后调用 afterNodeAccess,LinkedHashMap 重写该方法,在访问顺序模式下将节点移到双向链表尾部:
// 重写 HashMap 的 afterNodeAccess:访问节点后处理 void afterNodeAccess(Node<K,V> e) { // move node to last LinkedHashMap.Entry<K,V> last; // 仅在访问顺序模式,且节点不是尾节点时执行 if (accessOrder && (last = tail) != e) { LinkedHashMap.Entry<K,V> p = (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after; p.after = null; // 断开当前节点的后继 if (b == null) // 当前节点是头节点,更新头节点为后继 head = a; else // 否则,更新前驱的后继为当前节点的后继 b.after = a; if (a != null) // 更新后继的前驱为当前节点的前驱 a.before = b; else // 若后继为空,更新 last 为前驱(防止空指针) last = b; if (last == null) // 双向链表为空,当前节点作为头节点 head = p; else { // 否则,将当前节点插入尾部 p.before = last; last.after = p; } tail = p; // 更新尾节点为当前节点 modCount++; // 修改次数 +1,触发快速失败 } }
核心效果:访问后的节点成为双向链表最新尾部,保证访问顺序模式下'最新访问的节点在尾部,最久未访问的在头部'。
当调用 remove 删除节点时,HashMap 会先从哈希表中删除节点,然后调用 afterNodeRemoval,LinkedHashMap 重写该方法,将节点从双向链表中同步移除,保证双向链表结构完整:
// 重写 HashMap 的 afterNodeRemoval:删除节点后处理 void afterNodeRemoval(Node<K,V> e) { // unlink LinkedHashMap.Entry<K,V> p = (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after; // 清空当前节点的前驱后继,便于 GC 回收 p.before = p.after = null; if (b == null) // 当前节点是头节点,更新头节点为后继 head = a; else // 否则,更新前驱的后继为后继 b.after = a; if (a == null) // 当前节点是尾节点,更新尾节点为前驱 tail = b; else // 否则,更新后继的前驱为前驱 a.before = b; }
核心保证:哈希表和双向链表的节点数量始终一致,删除操作不会破坏有序性。
LinkedHashMap 的扩容完全复用 HashMap 的扩容逻辑,无需额外处理双向链表:
before/after 指针保持不变;LinkedHashMap继承 HashMap 的所有核心方法(put/get/remove/size 等),仅重写了 get、containsValue 和遍历相关方法,新增了有序性的支持,核心方法的时间复杂度仍为 O(1)(访问顺序模式的 get 因移节点会稍慢,但仍为 O(1))。
get(Object key)(重写,支持访问顺序)public V get(Object key) { Node<K,V> e; // 调用 HashMap 的 getNode 方法查找节点(O(1)) if ((e = getNode(hash(key), key)) == null) return null; // 访问顺序模式,调用 afterNodeAccess 将节点移到尾部 if (accessOrder) afterNodeAccess(e); return e.value; }
核心区别:比 HashMap 的 get 多了访问顺序的处理,这是实现 LRU 的关键。
containsValue(Object value)(重写,效率更高)public boolean containsValue(Object value) { // 基于双向链表遍历,无需遍历哈希表的空桶,效率远高于 HashMap for (LinkedHashMap.Entry<K,V> e = head; e != null; e = e.after) { V v = e.value; if (v == value || (value != null && value.equals(v))) return true; } return false; }
效率提升:HashMap 的 containsValue 需要遍历整个哈希表(包括空桶),而 LinkedHashMap 基于双向链表遍历,直接跳过空桶,遍历节点数等于实际元素数,效率更高。
LinkedHashMap重写了 HashMap 的遍历相关方法,所有遍历(entrySet/keySet/values)均基于双向链表实现,遍历顺序与有序模式一致,且效率更高:
LinkedHashMap<String, String> lhm = new LinkedHashMap<>(); lhm.put("a", "1"); lhm.put("b", "2"); lhm.put("c", "3"); lhm.put("a", "4"); // 重复 put,顺序不变 // 遍历顺序:a→b→c(与首次插入顺序一致) for (Map.Entry<String, String> entry : lhm.entrySet()) { System.out.println(entry.getKey() + "=" + entry.getValue()); // a=4, b=2, c=3 }
// 构建访问顺序的 LinkedHashMap LinkedHashMap<String, String> lhm = new LinkedHashMap<>(16, 0.75f, true); lhm.put("a", "1"); lhm.put("b", "2"); lhm.put("c", "3"); // 双向链表:a→b→c(head→tail) lhm.get("a"); // 访问 a,a 移到尾部 → b→c→a lhm.put("b", "5"); // 访问 b,b 移到尾部 → c→a→b lhm.containsKey("c"); // 访问 c,c 移到尾部 → a→b→c // 遍历顺序:a(最久未访问)→b→c(最新访问) for (Map.Entry<String, String> entry : lhm.entrySet()) { System.out.println(entry.getKey()); // a, b, c }
removeEldestEntry(Map.Entry<K,V> eldest)LinkedHashMap 内置了LRU 缓存的核心能力,只需重写 removeEldestEntry 方法,指定缓存最大容量,当元素数超过容量时,会自动删除最久未访问的节点(双向链表头节点),实现 LRU(最近最少使用)缓存。
removeEldestEntry 是 LinkedHashMap 的保护方法,默认返回 false(不删除旧节点);put 添加新节点后,HashMap 会调用 afterNodeInsertion,该方法会判断 removeEldestEntry(eldest) 的返回值:
true,则自动删除双向链表的头节点(最久未访问节点);false,则不删除。// 自定义 LRU 缓存,继承 LinkedHashMap,重写 removeEldestEntry public class LruCache<K, V> extends LinkedHashMap<K, V> { private final int MAX_CAPACITY; // 缓存最大容量 // 构造方法:指定容量,访问顺序模式 public LruCache(int maxCapacity) { super(maxCapacity, 0.75f, true); this.MAX_CAPACITY = maxCapacity; } // 重写:当元素数超过最大容量时,删除最久未访问的节点 @Override protected boolean removeEldestEntry(Map.Entry<K, V> eldest) { // 关键:size() > 最大容量时返回 true,触发自动删除 return size() > MAX_CAPACITY; } // 测试 public static void main(String[] args) { LruCache<String, String> cache = new LruCache<>(3); // 最大容量 3 cache.put("a", "1"); cache.put("b", "2"); cache.put("c", "3"); // 容量 3,正常存储:a→b→c System.out.println(cache); // {a=1, b=2, c=3} cache.put("d", "4"); // 容量超 3,删除最久未访问的 a → b→c→d System.out.println(cache); // {b=2, c=3, d=4} cache.get("b"); // 访问 b,b 移到尾部 → c→d→b cache.put("e", "5"); // 容量超 3,删除最久未访问的 c → d→b→e System.out.println(cache); // {d=4, b=2, e=5} } }
核心效果:缓存容量固定,自动淘汰最久未使用的节点,完美实现 LRU 缓存,代码极简。
上述自定义 LruCache 是非线程安全的,若需高并发使用,有两种方案:
Collections.synchronizedMap 包装,低并发可用:Map<String, String> syncLruCache = Collections.synchronizedMap(new LruCache<>(3));
com.google.common.cache.CacheBuilder 构建 LRU 缓存(高并发、功能丰富):// Guava 的 LRU 缓存,线程安全,支持过期时间等 Cache<String, String> guavaCache = CacheBuilder.newBuilder() .maximumSize(3) // 最大容量 3 .build(); guavaCache.put("a", "1"); guavaCache.getIfPresent("a"); // 获取值
LinkedHashMap非线程安全,与 HashMap 一致,多线程并发修改(如同时 put/remove/get)会导致:
ConcurrentModificationException。// 包装为线程安全的 LinkedHashMap,所有方法加 synchronized 全局锁 Map<String, String> syncLhm = Collections.synchronizedMap(new LinkedHashMap<>()); // 遍历需手动加锁,避免快速失败 synchronized (syncLhm) { for (Map.Entry<String, String> entry : syncLhm.entrySet()) { System.out.println(entry); } }
LinkedHashMap<String, String> lhm = new LinkedHashMap<>(); ReentrantLock lock = new ReentrantLock(); // 加锁 put lock.lock(); try { lhm.put("a", "1"); } finally { lock.unlock(); } // 加锁 get lock.lock(); try { lhm.get("a"); } finally { lock.unlock(); }
removeEldestEntry(Map.Entry<K,V> eldest),默认返回 false,重写后实现缓存淘汰。LinkedHashMap 通过双向链表记录所有节点的顺序,哈希表负责高效增删查,双向链表负责有序遍历,两种结构独立维护且同步更新,因此实现了有序。
LinkedHashMap 的 Entry 节点继承自 HashMap.Node,仅新增了 before(前驱)和 after(后继)指针,使节点同时具备两种身份:
Collections.synchronizedMap 包装;2. 中等并发用 ReentrantLock 手动加锁;3. 高并发 LRU 缓存用 Guava ConcurrentLinkedHashMap/Caffeine。java.util.Hashtable 是 Java 集合框架中最早的哈希表实现(JDK1.0),实现 Map 接口,基于数组 + 链表的经典哈希表结构存储键值对,核心特性是键值均不允许为 null、天生线程安全,所有核心方法均加 synchronized 全局锁,适合低并发的线程安全键值对存储场景。
它是 JDK 早期的 Map 实现,后续被 JDK1.2 的 HashMap(非线程安全)和 JDK1.5 的 JUC 并发 Map(高并发)替代,目前仅用于旧代码兼容,新开发中完全不推荐使用。
hashCode() 计算哈希值,映射到数组索引,哈希冲突时用单向链表解决;synchronized 方法级锁,保证多线程操作的原子性;NullPointerException;hashCode(),通过 hash % table.length 映射数组索引(未做哈希扰动优化);modCount,遍历过程中集合被修改会抛出 ConcurrentModificationException;Map 接口,兼容旧的字典操作方法(如 elements()/keys());entrySet()/keySet()/values() 的迭代器,也保留旧的 Enumeration 遍历方式。两者均实现 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 首选) |
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 节点定义如下:
public class Hashtable<K,V> extends Dictionary<K,V> implements Map<K,V>, Cloneable, java.io.Serializable { // 存储键值对的 Entry 数组,哈希表的'桶' private transient Entry<?,?>[] table; // 哈希表中实际存储的键值对个数 private transient int count; // 扩容阈值 = 容量 × 加载因子,元素数≥阈值时触发扩容 private int threshold; // 加载因子,固定 0.75(不可自定义,JDK 硬编码) private float loadFactor; // 修改次数,用于快速失败机制 private transient int modCount = 0; // 序列化版本号 private static final long serialVersionUID = 1421746759512286392L; }
核心常量:加载因子 loadFactor 固定为 0.75f,是时间和空间的平衡值——减少哈希冲突的同时,避免数组空间浪费。
Entry 是 Hashtable 的内部静态类,代表哈希表中的一个键值对,构成单向链表解决哈希冲突:
private static class Entry<K,V> implements Map.Entry<K,V> { final int hash; // 键的哈希值(缓存,避免重复计算) final K key; // 键,final 修饰不可修改 V value; // 值,可修改 Entry<K,V> next;// 下一个 Entry 节点,单向链表指针 Entry(int hash, K key, V value, Entry<K,V> next) { this.hash = hash; this.key = key; this.value = value; this.next = next; } // 重写 equals、hashCode、setValue 等方法 }
单向链表特性:哈希冲突时,新节点插入到链表头部(头插法),遍历从头部开始到尾部结束。
数组索引:0 1 2 3 4 5 6 7 8 9 10 ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ 桶结构: - A - B→C - D - - - E -
Hashtable 提供4 个构造方法,支持默认初始化、指定初始容量、指定容量 + 加载因子、传入 Map 初始化,加载因子默认 0.75,且仅在构造时可指定(后续不可修改):
new Hashtable<>()public Hashtable() { // 调用指定容量 + 加载因子构造,初始容量 11,加载因子 0.75 this(11, 0.75f); }
table 数组在首次 put 时初始化(懒加载)。new Hashtable(int initialCapacity)public Hashtable(int initialCapacity) { // 调用指定容量 + 加载因子构造,加载因子固定 0.75 this(initialCapacity, 0.75f); }
(预计元素数 / 0.75) + 1,减少扩容次数。new Hashtable(int initialCapacity, float loadFactor)public Hashtable(int initialCapacity, float loadFactor) { // 校验参数:容量不能为负,加载因子必须>0 且≤1 if (initialCapacity < 0) throw new IllegalArgumentException("Illegal Capacity: " + initialCapacity); if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal Load: " + loadFactor); if (initialCapacity == 0) initialCapacity = 1; this.loadFactor = loadFactor; // 初始化 Entry 数组,容量为指定值(无需是 2 的幂) table = new Entry<?,?>[initialCapacity]; // 计算扩容阈值:容量×加载因子,向下取整 threshold = (int)Math.min(initialCapacity * loadFactor, Integer.MAX_VALUE + 1); }
new Hashtable(Map<? extends K, ? extends V> t)public Hashtable(Map<? extends K, ? extends V> t) { // 初始化容量:取 t.size()×2 和 11 的最大值,避免频繁扩容 this(Math.max(2*t.size(), 11), 0.75f); // 批量添加 Map 中的所有键值对(调用 putAll) putAll(t); }
NullPointerException;键值均不可为 null:所有 put 相关方法都会显式检查 key 和 value 是否为 null,只要有一个为 null 就抛 NPE:
Hashtable<String, String> ht = new Hashtable<>(); ht.put("name", null); // 抛 NullPointerException(value 为 null) ht.put(null, "Java"); // 抛 NullPointerException(key 为 null)
Hashtable 的哈希计算分为两步,无哈希扰动优化,直接使用键的 hashCode(),通过取模映射数组索引,哈希冲突概率较高:
直接调用键的 hashCode() 方法,若 key 为 null 直接抛 NPE:
int hash = key.hashCode();
通过取模运算将哈希值映射到数组的合法索引(0 ~ table.length-1):
int index = (hash & 0x7FFFFFFF) % table.length;
hash & 0x7FFFFFFF:将哈希值转为非负数(避免 hash 为负时,取模得到负索引);% table.length:取模运算,因 table.length 是奇数,能减少哈希冲突(比偶数更均匀)。哈希计算缺陷:未做哈希扰动(如 HashMap 的高 16 位^低 16 位),若键的 hashCode() 分布较差,极易导致哈希冲突,链表过长会降低查询效率。
当 Hashtable 的实际元素数 count ≥ 阈值 threshold时,添加元素会触发扩容,扩容规则固定为原容量×2 + 1,核心步骤为创建新数组→重新哈希所有元素→替换原数组。
所有 put 操作都会在添加元素后检查 count > threshold,满足则触发扩容:
// 扩容核心方法,加 synchronized 锁 protected void rehash() { int oldCapacity = table.length; // 保存原数组 Entry<?,?>[] oldMap = table; // 计算新容量:原容量×2 + 1(保证奇数) int newCapacity = (oldCapacity << 1) + 1; // 处理容量溢出,设置为 Integer.MAX_VALUE if (newCapacity - Integer.MAX_VALUE > 0) { if (oldCapacity == Integer.MAX_VALUE) return; // 容量已达最大值,不再扩容 newCapacity = Integer.MAX_VALUE; } // 创建新容量的 Entry 数组 Entry<?,?>[] newMap = new Entry<?,?>[newCapacity]; modCount++; // 修改次数 +1,触发快速失败 // 计算新阈值:新容量×加载因子,向下取整 threshold = (int)Math.min(newCapacity * loadFactor, Integer.MAX_VALUE + 1); table = newMap; // 核心:重新哈希所有元素,从原数组复制到新数组 for (int i = oldCapacity ; i-- > 0 ;) { for (Entry<K,V> old = (Entry<K,V>)oldMap[i] ; old != null ; ) { Entry<K,V> e = old; old = old.next; // 先保存下一个节点,避免链表断裂 // 重新计算新数组的索引 int index = (e.hash & 0x7FFFFFFF) % newCapacity; // 头插法:新节点插入到新数组索引的链表头部 e.next = (Entry<K,V>)newMap[index]; newMap[index] = e; } } }
Integer.MAX_VALUE;table 指向新数组,完成扩容。Hashtable 实现 Map 接口的所有核心方法,所有方法均加synchronized关键字,保证线程安全,平均时间复杂度 O(1)(哈希冲突少时),最坏 O(n)(链表过长时)。
put(K key, V value)(核心)核心作用:添加键值对,若键已存在则覆盖旧值并返回旧值;若键不存在则添加新节点,触发容量检查和扩容,key/value 均不能为 null。
public synchronized V put(K key, V value) { // 步骤 1:检查 value 是否为 null,抛 NPE if (value == null) { throw new NullPointerException(); } // 步骤 2:检查 table 数组是否为空,首次 put 则初始化 Entry<?,?> tab[] = table; // 计算 key 的哈希值和数组索引(key 为 null 时 hashCode() 抛 NPE) int hash = key.hashCode(); int index = (hash & 0x7FFFFFFF) % tab.length; // 步骤 3:遍历对应桶的链表,检查键是否已存在 @SuppressWarnings("unchecked") Entry<K,V> entry = (Entry<K,V>)tab[index]; for(; entry != null ; entry = entry.next) { // 哈希值相同且键 equals,覆盖旧值 if ((entry.hash == hash) && entry.key.equals(key)) { V old = entry.value; entry.value = value; return old; } } // 步骤 4:键不存在,添加新节点(头插法) addEntry(hash, key, value, index); return null; } // 添加新 Entry 节点的私有方法 private void addEntry(int hash, K key, V value, int index) { modCount++; // 修改次数 +1 Entry<?,?> tab[] = table; // 检查是否需要扩容:元素数≥阈值,且对应桶已有元素 if (count >= threshold) { rehash(); // 扩容 // 扩容后重新计算索引 tab = table; hash = key.hashCode(); index = (hash & 0x7FFFFFFF) % tab.length; } // 头插法:新节点插入到链表头部 @SuppressWarnings("unchecked") Entry<K,V> e = (Entry<K,V>)tab[index]; tab[index] = new Entry<>(hash, key, value, e); count++; // 元素数 +1 }
核心特性:
synchronized,多线程同时 put 不会出现元素覆盖;get(Object key)(核心)核心作用:根据键查询对应的值,若键不存在返回 null;若键为 null 抛 NPE,时间复杂度平均 O(1)。
public synchronized V get(Object key) { Entry<?,?> tab[] = table; // 计算哈希值和索引(key 为 null 抛 NPE) int hash = key.hashCode(); int index = (hash & 0x7FFFFFFF) % tab.length; // 遍历链表,匹配哈希值和 equals for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) { if ((e.hash == hash) && e.key.equals(key)) { return (V)e.value; } } return null; }
核心优化:缓存了键的哈希值(Entry.hash),无需重复计算,提升遍历效率。
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; // 遍历链表,找到目标节点 while (e != null) { if ((e.hash == hash) && e.key.equals(key)) { modCount++; // 修改次数 +1 // 调整链表指针:删除当前节点 if (prev != null) { prev.next = e.next; } else { tab[index] = e.next; // 目标节点是头节点,直接替换桶的头 } count--; // 元素数 -1 V oldValue = e.value; e.value = null; // 清空引用,便于 GC 回收 return oldValue; } prev = e; e = e.next; } return null; }
核心步骤:找到目标节点→调整前驱/后继指针→清空引用→更新计数,保证链表结构完整。
isEmpty()/size()public synchronized boolean isEmpty() { return count == 0; } public synchronized int size() { return count; }
synchronized,保证多线程下计数的准确性。containsKey(Object key)/containsValue(Object value)public synchronized boolean containsKey(Object key) { // 逻辑与 get() 类似,仅返回是否存在 } public synchronized boolean containsValue(Object value) { // 遍历整个哈希表(所有桶 + 所有链表),时间复杂度 O(n) if (value == null) { throw new NullPointerException(); } Entry<?,?>[] tab = table; for (int i = tab.length ; i-- > 0 ;) { for (Entry<?,?> e = tab[i] ; e != null ; e = e.next) { if (e.value.equals(value)) { return true; } } } return false; }
containsValue 效率极低,需全表遍历,尽量避免使用。clear()public synchronized void clear() { Entry<?,?>[] tab = table; modCount++; // 遍历所有桶,清空 Entry 节点的引用,便于 GC 回收 for (int i = tab.length ; i-- > 0 ;) tab[i] = null; count = 0; // 元素数置 0,数组容量不变 }
Hashtable 支持现代迭代器和旧版 Enumeration两种遍历方式,均为 O(n) 时间复杂度,迭代器支持快速失败,Enumeration 不支持,新代码推荐使用迭代器,旧代码兼容用 Enumeration。
Hashtable 提供 entrySet()/keySet()/values() 三个视图的迭代器,支持边遍历边删除(迭代器的 remove() 方法),是新代码的首选:
Hashtable<String, String> ht = new Hashtable<>(); ht.put("name", "Java"); ht.put("age", "25"); ht.put("gender", "male"); // 遍历所有键值对(推荐) for (Map.Entry<String, String> entry : ht.entrySet()) { String key = entry.getKey(); String value = entry.getValue(); System.out.println(key + "=" + value); } // 迭代器方式,支持边遍历边删除 Iterator<Map.Entry<String, String>> it = ht.entrySet().iterator(); while (it.hasNext()) { Map.Entry<String, String> entry = it.next(); if (entry.getKey().equals("age")) { it.remove(); // 安全删除,无异常 } }
// 遍历所有键 for (String key : ht.keySet()) { System.out.println(key); } // 遍历所有值 for (String value : ht.values()) { System.out.println(value); }
Enumeration 是 JDK1.0 的旧迭代器,仅支持遍历(读),不支持删除,方法名为 hasMoreElements()/nextElement(),通过 keys()/elements() 获取:
// 遍历所有键 Enumeration<String> keyEnum = ht.keys(); while (keyEnum.hasMoreElements()) { String key = keyEnum.nextElement(); System.out.println(key); } // 遍历所有值 Enumeration<String> valueEnum = ht.elements(); while (valueEnum.hasMoreElements()) { String value = valueEnum.nextElement(); System.out.println(value); }
put/remove 修改集合,会抛出 ConcurrentModificationException,仅能通过迭代器的 remove() 修改;Hashtable 因设计早、优化少,存在多个致命缺陷,也是 Java 官方弃用、被 HashMap/ConcurrentHashMap 替代的核心原因,这是面试的高频考点:
Hashtable 的线程安全是通过给所有核心方法加 synchronized 全局锁实现的,相当于给整个 Hashtable 对象加锁:
if (!ht.containsKey(key)) { ht.put(key, value); })仍需手动加锁,否则会出现线程安全问题。hashCode(),未做任何哈希扰动处理,若键的 hashCode() 分布较差(如连续整数),极易导致哈希冲突;hash & (length-1));与 HashMap 相比,Hashtable 禁止 null 键和 null 值,添加 null 会直接抛 NPE,导致:
原容量×2+1,无法自定义,若初始容量设置不合理,会导致频繁扩容;put/putEntry、get/getEntry);ConcurrentModificationException;Hashtable 在新代码开发中完全禁止使用,根据是否需要线程安全,有明确的替代方案,优先级从高到低:
示例:
Map<String, String> map = new HashMap<>(); map.put("name", "Java"); map.put(null, "nullKey"); // 支持 null 键 map.put("age", null); // 支持 null 值
示例:
Map<String, String> concurrentMap = new ConcurrentHashMap<>(); concurrentMap.put("name", "Java"); concurrentMap.put("age", "25"); // 高并发下安全操作,无需手动加锁
synchronized 全局锁,实现简单;示例:
// 包装 HashMap 为线程安全 Map Map<String, String> syncMap = Collections.synchronizedMap(new HashMap<>()); syncMap.put("name", "Java"); // 遍历需手动加锁 synchronized (syncMap) { for (Map.Entry<String, String> entry : syncMap.entrySet()) { System.out.println(entry); } }
官方设计的刻意选择,核心原因:
get(key) 返回 null 时,无法区分'键不存在'和'值为 null',Hashtable 通过抛 NPE 彻底避免这种歧义;put/get/contains 等方法中单独处理 null 的情况,简化代码逻辑。最核心的 5 个区别:
核心是并发效率极低,加上哈希计算无优化、不支持 null、扩容成本高等缺陷,无法满足现代开发的性能和灵活性需求。
hashCode() 获取哈希值(key 为 null 抛 NPE);hash & 0x7FFFFFFF 将哈希值转为非负数;hash % table.length 取模,映射到数组索引。hash & (length-1));remove()方法实现;remove() 方法,会修改 modCount,触发迭代器的快速失败机制,抛出 ConcurrentModificationException;java.util.Properties 是Hashtable 的子类,专门用于处理配置文件的键值对集合,核心特性是键和值均为 String 类型(底层自动做类型转换),并提供了从文件/流加载配置、将配置写入文件/流的便捷方法,是 Java 中处理'纯文本配置文件(如.properties/.ini)\u201d的原生标准工具。
它继承了 Hashtable 的线程安全、哈希表结构特性,同时针对配置场景做了轻量化封装,适用于加载项目的基础配置(如数据库连接信息、系统参数、业务配置等),是 Java 后端开发中最基础、最常用的配置处理类。
synchronized 锁),区别于 HashMap/HashSet 的非线程安全;load()(从文件/流加载配置)、store()(将配置写入文件/流)、loadFromXML()/storeToXML()(XML 格式配置)等核心方法,适配配置文件的读写;getProperty(String key, String defaultValue) 方法,键不存在时返回默认值,避免空指针,适配配置缺失场景;getProperty(String key) 方法,若自身配置中无该键,会自动从'系统属性(System.getProperties())\u201d中查找,实现配置兜底;load(Reader)/store(Writer) 支持自定义编码(如 UTF-8),解决早期默认 ISO-8859-1 的中文乱码问题。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)读写 | 高并发通用键值存储 | 有序通用键值存储 | 非并发通用键值存储 |
Properties 的底层设计非常简洁,继承 Hashtable<Object, Object>,并通过封装专属的 String 类型方法屏蔽底层的 Object 类型,同时新增配置文件读写、默认值获取等特性,核心逻辑可总结为: Hashtable(哈希表 + 线程安全) + String 键值强制转换 + 配置文件 IO 方法 + 系统属性关联 = Properties
Properties 无新增底层哈希表相关属性,完全继承 Hashtable 的所有属性(如数组、容量、加载因子等),仅新增 1 个用于父配置兜底的属性,核心类定义如下:
public class Properties extends Hashtable<Object, Object> { // 父配置集合:若当前 Properties 无指定键,会从父配置中查找(多层配置兜底) protected Properties defaults; // 序列化版本号 private static final long serialVersionUID = 4112578634029874840L; // 构造方法 public Properties() { this(null); } public Properties(Properties defaults) { this.defaults = defaults; } }
核心设计巧思:
defaults 父配置:实现多层配置兜底(如全局配置→模块配置→业务配置),键不存在时逐级查找;原容量*2+1),无需重复开发。Properties 对外暴露的'所有核心方法(如 getProperty/setProperty)\u201d均强制使用 String 类型,底层通过 Object.toString() 和强制类型转换实现 Object↔String 的转换,若传入非 String 类型,会自动调用 toString():
// 设置键值对,强制 String 类型 public Object setProperty(String key, String value) { return put(key, value); // 调用 Hashtable 的 put(Object, Object) } // 获取键值对,返回 String,键不存在返回 null public String getProperty(String key) { Object oval = super.get(key); // 调用 Hashtable 的 get(Object) String sval = (oval instanceof String) ? (String)oval : null; // 自身无值 → 查父配置 defaults → 查系统属性 System.getProperties() return (sval == null && defaults != null) ? defaults.getProperty(key) : sval; }
关键限制:Properties不支持 null 键和 null 值(继承 Hashtable 的特性),若传入 null,会抛出 NullPointerException。
Properties 的所有核心方法(setProperty/getProperty/load/store/clear 等),要么直接继承 Hashtable 的加 synchronized 锁的方法,要么自身加锁,保证所有操作的原子性:
put/get/remove/containsKey 等方法均被 synchronized 修饰;load/store 等 IO 方法,自身也添加了 synchronized 锁,避免多线程同时读写配置文件。JDK1.6 之前,Properties 的底层是纯 Hashtable,遍历无序;JDK1.6 及以后,Java 官方将 Properties 的底层实现改为LinkedHashtable(Hashtable 的子类,类似 LinkedHashMap),在哈希表基础上维护了双向链表,因此保留配置项的加载顺序/插入顺序,遍历顺序与配置文件中的顺序完全一致。
getProperty(String key) 的核心查找逻辑为三级兜底,是 Properties 的重要特性:
defaults 父配置,则从父配置中递归查找;System.getProperty(key) 从JVM 系统属性中查找(如 java.version、user.dir 等);null。Properties 的底层结构 = Hashtable 的数组 + 链表哈希表 + 双向链表(JDK1.6+) + 三级兜底引用,结合配置文件的读写能力:
// 哈希表结构(继承 Hashtable,数组 + 链表,线程安全) 数组索引:0 1 2 ... 5 ... 10 ↓ ↓ ↓ ↓ ↓ 桶结构: k1=v1 - k2=v2 k3=v3→k4=v4 ... - (所有 k/v 均为 String,底层存储为 Object,获取时隐式转换) // 双向链表(JDK1.6+,维护加载/插入顺序) head → k1=v1 → k2=v2 → k3=v3 → k4=v4 → tail // 三级兜底引用 当前 Properties → defaults(父配置)→ System.getProperties()(系统属性)
Properties 仅提供2 个构造方法,均为无参/单参,核心作用是初始化实例并指定父配置兜底集合,无容量/加载因子指定方法(复用 Hashtable 的默认值:初始容量 11,加载因子 0.75):
new Properties()public Properties() { this(null); // 父配置 defaults 为 null,关闭多层兜底 }
getProperty 仅会查找当前实例和系统属性。new Properties(Properties defaults)public Properties(Properties defaults) { this.defaults = defaults; // 指定父配置,开启多层兜底 }
globalProps,再加载模块配置 moduleProps = new Properties(globalProps),模块配置中未定义的键会从全局配置中查找。defaults 仅保存父配置的引用,若父配置后续修改,当前 Properties 的 getProperty 会获取到最新值;Properties 的方法分为三大核心类别,均为配置场景量身设计,摒弃了 Hashtable 的通用 Object 方法,对外仅暴露 String 类型的便捷操作,核心方法覆盖配置的增删查、文件读写、兜底获取全流程。
这类方法是 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(); // 1. 添加/修改配置项 props.setProperty("db.url", "jdbc:mysql://localhost:3306/test"); props.setProperty("db.username", "root"); props.setProperty("db.password", "123456"); // 2. 获取配置项(带默认值,推荐) String url = props.getProperty("db.url", "jdbc:mysql://localhost:3306/default"); String port = props.getProperty("db.port", "3306"); // 键不存在,返回默认值 3306 // 3. 获取配置项(无默认值) String username = props.getProperty("db.username"); // root // 4. 判断是否包含键 boolean hasPwd = props.containsKey("db.password"); // true // 5. 删除配置项 props.remove("db.password");
这是 Properties 的核心价值,专门用于从文件/字节流/字符流加载配置,或将配置写入文件/字节流/字符流,支持纯文本.properties和XML两种格式,解决了手动读取配置文件的繁琐。
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):从行号字符流加载,适用于需要定位配置行号的场景。.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
加载示例(UTF-8 编码,推荐):
Properties props = new Properties(); // 从文件加载,使用 FileReader 指定 UTF-8 编码,解决中文乱码 try (Reader reader = new FileReader("src/main/resources/config.properties", StandardCharsets.UTF_8)) { props.load(reader); // 加载配置,保留文件中的顺序 } catch (IOException e) { e.printStackTrace(); }
store() 系列(写入.properties 纯文本)用于将 Properties 中的配置项写入文件/字节流/字符流,生成标准的.properties 文件,同样推荐使用字符流(Writer) 支持自定义编码。
void store(OutputStream out, String comments):写入字节流,默认 ISO-8859-1,中文乱码;void store(Writer writer, String comments):写入字符流,支持自定义编码,推荐;写入示例(UTF-8 编码,推荐):
Properties props = new Properties(); props.setProperty("name", "张三"); props.setProperty("age", "20"); props.setProperty("gender", "男"); // 写入文件,UTF-8 编码,注释为"用户配置" try (Writer writer = new FileWriter("user.properties", StandardCharsets.UTF_8)) { props.store(writer, "User Config"); } catch (IOException e) { e.printStackTrace(); }
生成的 user.properties 文件:
#User Config #Mon Jan 29 10:00:00 CST 2026 name=张三 age=20 gender=男
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 配置文件示例(config.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://localhost:3306/test</entry> <entry key="db.username">root</entry> </properties>
XML 读写示例:
Properties props = new Properties(); // 加载 XML 配置 try (InputStream in = new FileInputStream("config.xml")) { props.loadFromXML(in); } catch (IOException e) { e.printStackTrace(); } // 写入 XML 配置(UTF-8,注释为"数据库配置") try (OutputStream out = new FileOutputStream("db.xml")) { props.storeToXML(out, "数据库配置", StandardCharsets.UTF_8.name()); } catch (IOException e) { e.printStackTrace(); }
用于将 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,线程安全。Properties props = new Properties(); props.setProperty("a", "1"); props.setProperty("b", "2"); props.setProperty("c", "3"); // 推荐:获取 String 类型键集,增强 for 遍历 Set<String> keys = props.stringPropertyNames(); for (String key : keys) { String value = props.getProperty(key); System.out.println(key + "=" + value); // 顺序与插入/加载顺序一致 } // 调试:打印所有配置项到控制台 props.list(System.out);
Properties 处理中文配置时极易出现乱码,核心原因是早期的字节流方法(load(InputStream)/store(OutputStream))默认使用 ISO-8859-1 编码,该编码不支持中文,导致中文被错误解析。
原因:使用 load(InputStream) 字节流加载,默认 ISO-8859-1 编码,无法解析中文。
解决方案:使用load(Reader)字符流,通过 FileReader/InputStreamReader 指定UTF-8 编码(推荐),示例:
// 正确:InputStreamReader 指定 UTF-8 编码(兼容所有流场景) try (InputStream in = new FileInputStream("config.properties"); Reader reader = new InputStreamReader(in, StandardCharsets.UTF_8)) { props.load(reader); } // 简化:FileReader 直接指定 UTF-8(JDK11+ 支持,推荐) try (Reader reader = new FileReader("config.properties", StandardCharsets.UTF_8)) { props.load(reader); }
原因:使用 store(OutputStream) 字节流写入,默认 ISO-8859-1 编码,中文被转码为乱码。
解决方案:使用store(Writer)字符流,通过 FileWriter/OutputStreamWriter 指定UTF-8 编码,示例:
// 正确:OutputStreamWriter 指定 UTF-8 编码 try (OutputStream out = new FileOutputStream("config.properties"); Writer writer = new OutputStreamWriter(out, StandardCharsets.UTF_8)) { props.store(writer, "中文配置"); } // 简化:FileWriter 直接指定 UTF-8(JDK11+ 支持) try (Writer writer = new FileWriter("config.properties", StandardCharsets.UTF_8)) { props.store(writer, "中文配置"); }
JDK1.8 及以后,始终使用字符流(Reader/Writer)的 load/store 方法,指定 UTF-8 编码,彻底避免中文乱码问题,摒弃字节流(InputStream/OutputStream)的相关方法。
Properties 是 Java 原生的配置处理工具,适用于轻量级、纯文本、无复杂结构的配置场景,是项目启动时加载基础配置的首选,以下是最常用的实战场景:
加载项目 resources 目录下的配置文件(如数据库配置、系统参数),结合类加载器获取资源流,适配项目打包后的 jar 包场景:
// 加载 resources/config/db.properties 配置文件(UTF-8) Properties dbProps = new Properties(); // 类加载器获取资源流(推荐,适配 jar 包) try (InputStream in = PropertiesDemo.class.getClassLoader().getResourceAsStream("config/db.properties"); Reader reader = new InputStreamReader(in, StandardCharsets.UTF_8)) { dbProps.load(reader); // 获取数据库配置 String url = dbProps.getProperty("db.url"); String username = dbProps.getProperty("db.username"); String password = dbProps.getProperty("db.password"); } catch (IOException e) { throw new RuntimeException("加载数据库配置失败", e); }
通过 Properties(Properties defaults) 实现全局公共配置和模块专属配置的兜底,模块配置覆盖全局配置,未定义的键从全局配置中查找:
// 1. 加载全局公共配置 Properties globalProps = new Properties(); try (Reader reader = new FileReader("global.properties", StandardCharsets.UTF_8)) { globalProps.load(reader); // 含:app.name=JavaDemo, app.version=1.0 } // 2. 加载模块配置,指定全局配置为父配置 Properties userProps = new Properties(globalProps); try (Reader reader = new FileReader("user-module.properties", StandardCharsets.UTF_8)) { userProps.load(reader); // 含:user.maxAge=20,无 app.name/app.version } // 3. 获取配置:模块有则取模块,无则取全局 String userMaxAge = userProps.getProperty("user.maxAge"); // 20(模块配置) String appName = userProps.getProperty("app.name"); // JavaDemo(全局配置兜底) String appVersion = userProps.getProperty("app.version"); // 1.0(全局配置)
动态生成/修改配置文件(如用户个性化配置、运行时参数保存),将内存中的 Properties 写入本地文件:
// 动态创建配置项 Properties userConfig = new Properties(); userConfig.setProperty("user.id", "1001"); userConfig.setProperty("user.name", "张三"); userConfig.setProperty("user.theme", "dark"); userConfig.setProperty("user.language", "zh-CN"); // 写入用户目录下的配置文件 String userHome = System.getProperty("user.home"); // 获取用户主目录 File configFile = new File(userHome, ".javademo/user.config"); // 确保父目录存在 if (!configFile.getParentFile().exists()) { configFile.getParentFile().mkdirs(); } // 写入文件(UTF-8) try (Writer writer = new FileWriter(configFile, StandardCharsets.UTF_8)) { userConfig.store(writer, "User Personal Config"); } catch (IOException e) { e.printStackTrace(); }
通过 Properties 的 getProperty 或直接使用 System.getProperties() 读取 JVM 的系统属性(如 Java 版本、用户目录、项目路径):
Properties sysProps = System.getProperties(); // 读取系统属性 String javaVersion = sysProps.getProperty("java.version"); // Java 版本 String userDir = sysProps.getProperty("user.dir"); // 项目运行目录 String osName = sysProps.getProperty("os.name"); // 操作系统名称 // 直接通过 System.getProperty 获取(更简洁) String javaHome = System.getProperty("java.home"); // JDK 安装目录
load(InputStream) 和 store(OutputStream) 方法默认使用 ISO-8859-1 编码,该编码不支持中文,导致中文被错误解析/转码;getProperty 方法的查找逻辑是什么?getProperty(String key) 采用三级兜底查找,顺序为:
defaults 父配置,则从父配置中递归查找;System.getProperty(key) 从JVM 系统属性中查找;null。setProperty(null, "value") 或 setProperty("key", null) 会直接抛出 NullPointerException;getResourceAsStream(String path)\u201d方法获取资源流,该方法会自动查找 resources 目录下的文件,适配项目打包后的 jar 包场景(jar 包中的资源无法通过 File 访问);PropertiesDemo.class.getClassLoader().getResourceAsStream("config/db.properties");/,直接写相对路径,且使用字符流加载并指定 UTF-8 编码。Properties 是纯文本键值对配置,yml/yaml 是层级化结构化配置,二者互补,适用场景不同:
| 特性 | Properties | Yml/Yaml |
| 结构 | 扁平键值对(key=value) | 层级化(缩进表示层级) |
| 可读性 | 简单配置友好,复杂配置繁琐 | 复杂层级配置友好,可读性高 |
| 数据类型 | 仅支持 String,需手动转换 | 原生支持 String/int/bool/list/map |
| 解析工具 | JDK 原生支持(Properties 类) | 需第三方依赖(如 Spring Boot 的 snakeyaml) |
| 适用场景 | 轻量级、简单、无层级的基础配置 | 复杂、层级化、多数据类型的业务配置 |
setProperty(增改)、getProperty(key, defaultValue)(获取,推荐带默认值)、load(Reader)(加载)、store(Writer)(写入)、stringPropertyNames()(遍历);getResourceAsStream\u201d加载项目 resources 目录下的配置文件,适配 jar 包场景;java.util.HashSet 是 Java 集合框架中基于 HashMap 实现的无序、不可重复的 Set 集合,JDK1.2 引入,实现 Set 接口,本质是HashMap 的'键集'包装器——将 Set 的元素作为 HashMap 的键,值固定为一个静态常量 PRESENT,通过 HashMap 的键唯一性实现 Set 的不可重复性,通过 HashMap 的哈希表结构实现 O(1) 平均时间复杂度的增删查。
它是最常用的 Set 实现,非线程安全,支持 null 元素(仅一个),无序且遍历顺序不固定(由元素哈希值决定),适用于无需有序、仅需去重的绝大多数场景,是 Set 接口的工业标准实现。
false,保证元素唯一;hashCode() 和 HashMap 的哈希表结构决定,且扩容后遍历顺序可能改变;ConcurrentModificationException,高并发需手动加锁;modCount,遍历过程中修改集合会触发快速失败;LinkedHashSet 是 HashSet 的子类,基于 LinkedHashMap 实现,核心区别是是否有序,其余特性(不可重复、null 支持、O(1) 效率)一致:
| 特性 | HashSet | LinkedHashSet |
| 底层实现 | 基于 HashMap | 基于 LinkedHashMap |
| 有序性 | 无序(哈希表顺序) | 有序(插入顺序/访问顺序) |
| 底层结构 | 数组 + 链表/红黑树 | 数组 + 链表/红黑树 + 双向链表 |
| 遍历效率 | 中(需遍历哈希表空桶) | 高(基于双向链表,无空桶) |
| 内存占用 | 低 | 稍高(节点多 2 个指针) |
| 插入性能 | 极致 O(1)(无额外维护) | 略低(维护双向链表) |
| 适用场景 | 纯去重,无需有序 | 去重 + 保留插入顺序 |
| 重复元素处理 | 覆盖键,返回 false | 覆盖键,返回 false |
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) | 无扩容,动态平衡红黑树 |
ArraySet 是 Android 系统提供的 Set 实现,底层是数组,适用于小数据量场景,JDK 中无此实现:
| 特性 | HashSet | ArraySet(Android) |
| 底层实现 | 哈希表 | 有序数组 |
| 时间复杂度 | 平均 O(1) | O(logn)(二分查找) |
| 内存占用 | 高(哈希表冗余) | 低(无冗余) |
| 数据量适配 | 大数据量 | 小数据量(<1000) |
| 插入性能 | 大数据量更优 | 小数据量更优 |
| 有序性 | 无序 | 有序(自然顺序) |
HashSet 的核心设计是'复用 HashMap 实现 Set\u201d,无自己的底层结构,仅通过一个 HashMap 实例存储元素,将 Set 的**元素作为 HashMap 的键',值固定为一个私有的静态常量 PRESENT(Object 类型),通过 HashMap 的键特性实现 Set 的所有核心功能。
这种设计属于组合模式(通过组合 HashMap 实现功能,而非继承),避免了继承 HashMap 带来的方法冗余,同时严格遵循 Set 接口的规范。
HashSet 的源码极简,仅 3 个核心属性,全部围绕 HashMap 展开:
public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, java.io.Serializable { // 核心:存储 HashSet 元素的 HashMap 实例,元素作为键,值为 PRESENT private transient HashMap<E,Object> map; // 固定值:所有 HashMap 键对应的默认值,节省内存(静态常量,唯一实例) private static final Object PRESENT = new Object(); // 序列化版本号 private static final long serialVersionUID = -5024744406713321676L; }
核心设计巧思:
PRESENT 是静态常量,所有 HashSet 实例共享同一个对象,无需为每个键创建新值,大幅节省内存;HashSet 的底层结构就是 HashMap 的哈希表结构,元素仅作为 HashMap 的键存在,值固定为 PRESENT:
// HashSet 的元素:A、B、C、null(对应 HashMap 的键) HashMap 键:null A - B→C - ... (数组 + 链表结构,与 HashMap 一致) HashMap 值:PRESENT PRESENT PRESENT ... (所有值均为同一个 PRESENT)
PRESENT,无实际业务意义。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 的接口,无额外逻辑:
new HashSet<>()public HashSet() { // 创建默认的 HashMap 实例(容量 16,加载因子 0.75) map = new HashMap<>(); }
new HashSet(int initialCapacity)/new HashSet(int initialCapacity, float loadFactor)public HashSet(int initialCapacity) { // 创建指定容量的 HashMap(加载因子 0.75) map = new HashMap<>(initialCapacity); } public HashSet(int initialCapacity, float loadFactor) { // 创建指定容量 + 加载因子的 HashMap,参数直接透传 map = new HashMap<>(initialCapacity, loadFactor); }
(预计元素数 / 0.75) + 1,与 HashMap 的容量计算规则一致,减少扩容次数。new HashSet(Collection<? extends E> c)public HashSet(Collection<? extends E> c) { // 创建 HashMap,容量取 c.size()/0.75 和 16 的最大值,避免扩容 map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16)); // 批量添加集合元素(调用 addAll,底层是 HashMap 的 put) addAll(c); }
false,仅保留一个 null;hashCode()和equals()方法,否则会导致重复元素(HashMap 无法判断键是否相同)。HashSet 实现 Set 接口的所有核心方法,无任何自己的实现逻辑,全部委托给底层的 HashMap,方法的入参/返回值适配 Set 接口的规范(如将 HashMap 的键操作转为 Set 的元素操作),平均时间复杂度均为 O(1)。
add(E e)(核心,实现去重)核心作用:添加元素到 Set,若元素已存在则返回 false,若不存在则添加并返回 true,实现不可重复性。
public boolean add(E e) { // 委托给 HashMap 的 put 方法:将元素 e 作为键,PRESENT 作为值 // HashMap.put 返回 null:键不存在(添加成功),返回旧值:键已存在(添加失败) return map.put(e, PRESENT) == null; }
核心逻辑解析:
null,因此 map.put(...) == null 为 true,HashSet.add 返回 true(添加成功);map.put(...) == null 为 false,HashSet.add 返回 false(添加失败,保证唯一)。关键要求:存入的元素必须重写 hashCode() 和 equals(),否则 HashMap 无法判断键是否重复,会导致 HashSet 存入重复元素。
remove(Object o)核心作用:从 Set 中删除指定元素,若元素存在则删除并返回 true,不存在则返回 false。
public boolean remove(Object o) { // 委托给 HashMap 的 remove 方法:删除键为 o 的键值对,返回对应的值 // 若返回 PRESENT:元素存在(删除成功),返回 null:元素不存在(删除失败) return map.remove(o) == PRESENT; }
逻辑适配:将 HashMap 的'删除键并返回值'转换为 Set 的'删除元素并返回是否存在'。
contains(Object o)核心作用:判断 Set 中是否包含指定元素,包含返回 true,否则返回 false,平均时间复杂度 O(1)。
public boolean contains(Object o) { // 直接委托给 HashMap 的 containsKey 方法(判断键是否存在) return map.containsKey(o); }
效率核心:依托 HashMap 的哈希表查找,无需遍历所有元素,平均 O(1) 效率。
isEmpty()/size()public boolean isEmpty() { // 委托给 HashMap 的 isEmpty(判断键值对数量是否为 0) return map.isEmpty(); } public int size() { // 委托给 HashMap 的 size(返回键值对数量,即 Set 的元素个数) return map.size(); }
clear()public void clear() { // 委托给 HashMap 的 clear(清空所有键值对,哈希表容量不变) map.clear(); }
toArray()public Object[] toArray() { // 委托给 HashMap 的 keySet().toArray(将 HashMap 的键集转为数组) return map.keySet().toArray(); } public <T> T[] toArray(T[] a) { // 泛型版数组转换,同样委托给 HashMap 的键集 return map.keySet().toArray(a); }
HashSet无独立的遍历实现,所有遍历方式均委托给底层 HashMap 的键集(keySet),遍历顺序与 HashMap 的键集遍历顺序一致(无序,由哈希值决定),支持'增强 for 循环、迭代器、forEach(JDK8+)\u201d三种方式,均为 O(n) 时间复杂度,无普通 for 循环(无索引,不支持)。
HashSet<String> set = new HashSet<>(); set.add("Java"); set.add("Python"); set.add("C++"); set.add(null); // 支持一个 null // 增强 for 循环:底层基于 HashMap 键集的迭代器,无序遍历 for (String s : set) { System.out.println(s); // 输出顺序不固定,可能为:null, Java, C++, Python }
注意:遍历顺序与插入顺序无关,且扩容后遍历顺序可能改变。
HashSet 的迭代器是HashMap 键集的迭代器,支持快速失败机制,且仅能通过迭代器的 remove() 方法边遍历边删除(唯一安全方式):
Iterator<String> it = set.iterator(); while (it.hasNext()) { String s = it.next(); if (s == null || s.equals("Python")) { it.remove(); // 迭代器删除,无 ConcurrentModificationException } }
核心注意:
set.remove(),会修改 modCount,触发快速失败,抛出 ConcurrentModificationException;add() 方法(Set 的迭代器均为只读,除了 remove)。// forEach:底层基于迭代器,支持 Lambda 表达式,语法简洁 set.forEach(s -> System.out.println(s));
hashCode() 和 HashMap 的哈希表结构决定,与插入顺序无关;ConcurrentModificationException;HashSet 的不可重复性完全依赖 HashMap 的键唯一性,而 HashMap 判断键是否相同的规则是:
两个对象的hashCode() 返回值相等且equals(Object o)返回 true,则认为是同一个键。
因此,存入 HashSet 的元素必须正确重写hashCode()和equals()方法,否则会导致重复元素存入,违背 Set 的设计初衷。
若元素类未重写 hashCode() 和 equals(),则使用Object 类的默认实现:
hashCode():返回对象的内存地址哈希值,每个新对象的 hashCode() 都不同;equals():判断两个对象的内存地址是否相同(即 this == o)。此时,即使两个对象的属性完全相同,HashMap 也会认为是不同的键,HashSet 会存入重复元素:
// 自定义类:未重写 hashCode() 和 equals() class Student { private String name; private int age; public Student(String name, int age) { this.name = name; this.age = age; } } // 测试:属性相同的两个对象被认为是不同元素 public static void main(String[] args) { HashSet<Student> set = new HashSet<>(); set.add(new Student("张三", 20)); set.add(new Student("张三", 20)); // 未重写,会被存入,Set 大小为 2 System.out.println(set.size()); // 输出:2(错误,预期为 1) }
重写 hashCode() 和 equals() 需遵循三大原则,保证 HashMap/HashSet 的正确工作:
若两个对象通过 equals() 判断为相等,则它们的 hashCode() 必须返回相同的值(否则 HashMap 会将其放入不同的桶,无法判断重复)。
hashCode 相等仅表示两个对象的哈希值相同,可能发生哈希冲突,需通过 equals() 进一步判断是否为同一个对象。
单独重写其中一个会导致原则 1 被破坏,引发重复元素问题。
基于对象的业务主键(如 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; } // 重写 equals:基于 name 和 age 判断相等 @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Student student = (Student) o; return age == student.age && Objects.equals(name, student.name); } // 重写 hashCode:基于 name 和 age 计算哈希值 @Override public int hashCode() { return Objects.hash(name, age); // JDK7+ 的 Objects 工具类,简洁高效 } }
// 测试:重复元素被过滤 public static void main(String[] args) { HashSet<Student> set = new HashSet<>(); set.add(new Student("张三", 20)); set.add(new Student("张三", 20)); // 重写后,添加失败,Set 大小为 1 System.out.println(set.size()); // 输出:1(正确) }
推荐工具:IDEA/Eclipse 可自动生成 hashCode() 和 equals() 方法,遵循通用规范,无需手动编写。
HashSet 允许一个 null 元素,因为 HashMap 对 null 键有特殊处理:
HashSet非线程安全,与 HashMap 一致,多线程并发操作(如同时 add/remove/遍历)会导致以下问题:
ConcurrentModificationException;通过集合工具类 Collections.synchronizedSet 将 HashSet 包装为线程安全的 Set,底层为 SynchronizedSet,所有方法均加 synchronized 全局锁:
// 包装为线程安全的 HashSet Set<String> syncSet = Collections.synchronizedSet(new HashSet<>()); // 并发操作:add/remove/get 均为线程安全 syncSet.add("Java"); syncSet.remove("Python"); syncSet.contains("C++"); // 遍历需手动加锁(迭代器非线程安全,避免快速失败) synchronized (syncSet) { for (String s : syncSet) { System.out.println(s); } }
特点:
JDK1.6+ 提供 Collections.newSetFromMap 方法,可通过ConcurrentHashMap构建线程安全的 Set(ConcurrentHashMap 是高并发 HashMap,节点级锁,并发效率极高):
// 基于 ConcurrentHashMap 构建线程安全的 Set(推荐,高并发首选) Set<String> concurrentSet = Collections.newSetFromMap(new ConcurrentHashMap<>()); // 高并发操作:无需手动加锁,遍历也无需加锁(弱一致性,不抛异常) concurrentSet.add("Java"); concurrentSet.remove("Python"); for (String s : concurrentSet) { System.out.println(s); }
核心优势:
ConcurrentModificationException,无需手动加锁;这是 JDK 原生高并发 Set 的首选实现。
CopyOnWriteArraySet 是 JUC 提供的线程安全 Set,底层基于 CopyOnWriteArrayList,采用写时复制策略:
// 写时复制的线程安全 Set Set<String> cowSet = new CopyOnWriteArraySet<>(); cowSet.add("Java"); cowSet.forEach(s -> System.out.println(s));
特点:
PRESENT,通过 HashMap 的'键唯一性(hashCode+equals)\u201d实现 Set 的不可重复性,通过 HashMap 的哈希表结构实现 O(1) 的平均时间复杂度。Object 类的默认 hashCode() 返回对象内存地址的哈希值,equals() 判断内存地址是否相同,若不重写,即使两个对象属性完全相同,HashMap 也会认为是不同的键,导致 HashSet 存入重复元素。
结论:必须同时重写 hashCode() 和 equals(),且遵循'equals 相等则 hashCode 必相等'的原则。
hashCode() 和哈希表的桶结构决定,与元素的插入顺序、自然顺序均无关,且扩容后遍历顺序可能改变。两种情况会导致 add 返回 false,元素添加失败:
| 维度 | 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 |
Collections.synchronizedSet(new HashSet<>())(实现简单);Collections.newSetFromMap(new ConcurrentHashMap<>())(推荐,效率极高);CopyOnWriteArraySet(写时复制,读无锁)。(预计元素数 / 0.75) + 1,减少扩容次数。java.util.LinkedHashSet 是HashSet 的子类,JDK1.4 引入,底层基于LinkedHashMap实现,是有序、不可重复的 Set 集合。它完全复用 HashSet 的包装器设计,将 Set 元素作为 LinkedHashMap 的键,值固定为常量 PRESENT,通过 LinkedHashMap 的哈希表 + 双向链表结构,既保留了 HashSet 的O(1) 平均时间复杂度、去重特性、null 支持,又实现了插入顺序的有序遍历,解决了 HashSet 遍历无序的问题。
它是去重 + 有序场景的首选 Set 实现,非线程安全,遍历效率高于 HashSet,仅在维护双向链表上增加少量性能开销,适用于需要保留元素插入顺序且去重的场景(如日志去重、配置项有序存储、按添加顺序遍历去重数据等)。
false;before/after 双向链表指针,内存占用略高于 HashSet,但开销极小。这是 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 的幂) | 无扩容,动态平衡红黑树 |
| 适用场景 | 纯去重,无需有序 | 去重 + 保留插入顺序 | 去重 + 按键排序/范围查询 |
LinkedHashSet 的设计是双重复用的经典案例,无任何自己的核心实现,核心逻辑可总结为: 继承 HashSet,将父类中的 HashMap 替换为 LinkedHashMap,通过 LinkedHashMap 的插入顺序特性实现有序,复用 HashSet 的 Set 接口包装逻辑实现去重。
简单来说,LinkedHashSet = HashSet 的 Set 接口封装 + LinkedHashMap 的插入有序特性。
LinkedHashSet无自己的成员属性,完全继承 HashSet 的所有属性(map、PRESENT),仅通过构造方法将 map 的实际类型从 HashMap 改为 LinkedHashMap:
// LinkedHashSet 无新增属性,所有属性均来自 HashSet public class LinkedHashSet<E> extends HashSet<E> implements Set<E>, Cloneable, java.io.Serializable { private static final long serialVersionUID = -2851667679971038690L; } // 父类 HashSet 的核心属性 public class HashSet<E> { private transient HashMap<E,Object> map; // 实际指向 LinkedHashMap 实例 private static final Object PRESENT = new Object(); }
底层就是 LinkedHashMap 的结构,元素作为LinkedHashMap 的键,值为 PRESENT,哈希表负责 O(1) 增删查,双向链表负责维护插入顺序:
// 哈希表结构(数组 + 链表,与 HashMap/LinkedHashMap 一致) 数组索引:0 1 2 ... 5 ... 15 ↓ ↓ ↓ ↓ ↓ 桶结构:null A - ... B→C ... - (键:Set 元素,值:PRESENT) // 双向链表结构(维护插入顺序:null→A→B→C,LinkedHashMap 的核心) head → null (before:null, after:A) → A (before:null, after:B) → B (before:A, after:C) → C (before:B, after:null) → tail
LinkedHashSet 的所有特性均由HashSet和LinkedHashMap共同提供,无额外实现:
| LinkedHashSet 特性 | 底层实现依据 |
| 不可重复 | HashSet 的包装逻辑+LinkedHashMap 的键唯一性(hashCode+equals) |
| 插入有序 | LinkedHashMap 的双向链表维护插入顺序 |
| 支持一个 null 元素 | LinkedHashMap 允许一个 null 键 |
| 平均 O(1) 时间复杂度 | LinkedHashMap 的哈希表结构 |
| 遍历效率高 | LinkedHashMap 的双向链表遍历(无空桶) |
| 快速失败机制 | 继承 LinkedHashMap 的 modCount |
| 扩容不破坏有序 | LinkedHashMap 扩容时保留双向链表顺序 |
LinkedHashSet仅重写了 HashSet 的所有 4 个构造方法,核心逻辑是将父类 HashSet 中的map属性初始化为 LinkedHashMap,所有构造参数均透传给 LinkedHashMap,无任何自己的逻辑,代码极简。
注:HashSet 的构造方法原本是初始化 HashMap,LinkedHashSet 通过重写构造方法,替换为 LinkedHashMap,这是其实现有序的唯一关键操作。
new LinkedHashSet<>()public LinkedHashSet() { // 调用父类 HashSet 的构造方法,初始化 LinkedHashMap(容量 16,加载因子 0.75) super(16, 0.75f, true); }
HashSet(int initialCapacity, float loadFactor, boolean dummy))是专门为 LinkedHashSet 设计的,dummy 为占位符,仅用于区分其他构造方法,作用是初始化 LinkedHashMap 而非 HashMap。new LinkedHashSet(int initialCapacity)/new LinkedHashSet(int initialCapacity, float loadFactor)public LinkedHashSet(int initialCapacity) { // 初始化 LinkedHashMap(指定容量,加载因子 0.75) super(initialCapacity, 0.75f, true); } public LinkedHashSet(int initialCapacity, float loadFactor) { // 初始化 LinkedHashMap(指定容量 + 加载因子,透传参数) super(initialCapacity, loadFactor, true); }
(预计元素数 / 0.75) + 1,减少扩容次数;new LinkedHashSet(Collection<? extends E> c)public LinkedHashSet(Collection<? extends E> c) { // 初始化 LinkedHashMap,容量取 c.size()/0.75 和 16 的最大值,避免扩容 super(Math.max((int) (c.size()/.75f) + 1, 16), 0.75f, true); // 复用 HashSet 的 addAll 方法,批量添加并去重,双向链表按传入集合的迭代顺序构建 addAll(c); }
accessOrder=false),不支持访问顺序,因此无法像 LinkedHashMap 那样实现 LRU 缓存;accessOrder 的构造方法,这是与 LinkedHashMap 的核心区别,也是 Set 接口的设计初衷(Set 无需访问顺序);add/remove/contains/size 等任何核心方法,所有操作均通过父类 HashSet 委托给底层的 LinkedHashMap。LinkedHashSet没有重写 HashSet 的任何核心方法,add/remove/contains/clear/size 等所有 Set 接口方法,均直接继承自 HashSet,而 HashSet 又将这些方法委托给底层的 LinkedHashMap。
所有方法的逻辑、时间复杂度、返回值规则与 HashSet完全一致,唯一的区别是底层执行的是 LinkedHashMap 的方法,在增删元素时会同步维护双向链表的插入顺序。
add(E e)(去重 + 保留插入顺序)// 直接继承 HashSet 的 add 方法,无任何修改 public boolean add(E e) { return map.put(e, PRESENT) == null; }
核心逻辑:
null,add 返回 true;PRESENT,add 返回 false,且不会改变该元素在双向链表中的位置(保留首次插入顺序);false。remove(Object o)(同步维护双向链表)// 直接继承 HashSet 的 remove 方法 public boolean remove(Object o) { return map.remove(o) == PRESENT; }
核心逻辑:
PRESENT,remove 返回 true;null,remove 返回 false。contains(Object o)(O(1) 效率)// 直接继承 HashSet 的 contains 方法 public boolean contains(Object o) { return map.containsKey(o); }
| 方法名 | 功能 | 时间复杂度 |
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 的遍历效率提升明显。
LinkedHashSet<String> lhs = new LinkedHashSet<>(); lhs.add("Java"); lhs.add("Python"); lhs.add("C++"); lhs.add(null); lhs.add("Python"); // 重复添加,顺序不变 // 遍历顺序:Java → Python → C++ → null(与首次插入顺序完全一致) for (String s : lhs) { System.out.println(s); }
LinkedHashSet 的迭代器是LinkedHashMap 键集的迭代器,基于双向链表实现,支持快速失败机制,仅能通过迭代器的 remove() 方法边遍历边删除(唯一安全方式):
Iterator<String> it = lhs.iterator(); while (it.hasNext()) { String s = it.next(); if (s == null || s.equals("C++")) { it.remove(); // 安全删除,不破坏双向链表结构 } } // 删除后遍历:Java → Python(仍保持剩余元素的插入顺序)
注意:禁止直接调用 lhs.remove(),会修改 modCount,触发 ConcurrentModificationException。
// 底层基于迭代器,按插入顺序遍历,语法简洁 lhs.forEach(s -> System.out.println(s));
LinkedHashSet 的不可重复性与 HashSet、LinkedHashMap 完全一致,依托键的 hashCode() 和 equals() 方法判断元素是否相同,因此存入 LinkedHashSet 的元素必须同时重写这两个方法,否则会导致重复元素存入。
必须遵循三大原则:
equals() 相等的两个对象,hashCode() 必须返回相同值;hashCode() 相等的两个对象,equals() 不一定相等(哈希冲突);equals() 时,必须同时重写 hashCode()。若元素类未重写这两个方法,会使用 Object 类的默认实现:
hashCode():返回对象的内存地址哈希值,每个新对象的哈希值都不同;equals():判断两个对象的内存地址是否相同(this == o)。此时,即使两个对象的属性完全相同,LinkedHashMap 也会认为是不同的键,LinkedHashSet 会存入重复元素,违背 Set 的设计初衷。
基于对象的业务主键重写,保证属性相同的对象满足 hashCode() 相等且 equals() 为 true:
class User { private String id; private String name; public User(String id, String name) { this.id = id; this.name = name; } // 基于业务主键 id 重写 equals @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; User user = (User) o; return Objects.equals(id, user.id); } // 基于 id 重写 hashCode @Override public int hashCode() { return Objects.hash(id); } }
// 测试:属性 id 相同的 User 会被去重,保留首次插入顺序 public static void main(String[] args) { LinkedHashSet<User> lhs = new LinkedHashSet<>(); lhs.add(new User("1", "张三")); lhs.add(new User("1", "张三")); // 重复,添加失败 lhs.add(new User("2", "李四")); System.out.println(lhs.size()); // 输出:2(正确去重) }
LinkedHashSet 允许一个 null 元素,因为 LinkedHashMap 对 null 键有特殊处理:
false,仅保留一个 null,且 null 在双向链表中的位置为首次插入的位置。LinkedHashSet非线程安全,与 HashSet、LinkedHashMap 一致,多线程并发操作(同时 add/remove/遍历)会导致以下问题:
ConcurrentModificationException;其线程安全处理方案与 HashSet完全一致,但因 LinkedHashSet 是有序 Set,需注意有序性的线程安全(即多线程操作后,遍历顺序仍符合预期),推荐方案分低并发和高并发,优先级明确。
通过 Collections.synchronizedSet 将 LinkedHashSet 包装为线程安全的 Set,底层为 SynchronizedSet,所有方法加 synchronized 全局锁,保证单操作原子性:
// 包装为线程安全的 LinkedHashSet,保留插入顺序 Set<String> syncLhs = Collections.synchronizedSet(new LinkedHashSet<>()); // 并发操作:add/remove/contains 均为线程安全 syncLhs.add("Java"); syncLhs.remove("Python"); // 遍历必须手动加锁(迭代器非线程安全,避免快速失败) synchronized (syncLhs) { for (String s : syncLhs) { System.out.println(s); } }
特点:
JDK 原生无高并发的 LinkedHashMap,但可通过Guava 的 ConcurrentLinkedHashMap(线程安全的 LinkedHashMap)结合 Collections.newSetFromMap,构建高并发、有序、线程安全的 Set,保留插入顺序和 O(1) 效率:
// 引入 Guava 依赖,创建 ConcurrentLinkedHashMap 实例 Map<String, Object> concurrentLinkedMap = new ConcurrentLinkedHashMap.Builder<String, Object>() .initialCapacity(16) .build(); // 基于高并发有序 Map 构建线程安全的 LinkedHashSet Set<String> concurrentLhs = Collections.newSetFromMap(concurrentLinkedMap); // 高并发操作:无需手动加锁,保留插入顺序,遍历无快速失败 concurrentLhs.add("Java"); concurrentLhs.add("Python"); for (String s : concurrentLhs) { System.out.println(s); // 仍为插入顺序 }
特点:
CopyOnWriteArraySet 是 JUC 提供的线程安全 Set,底层基于 CopyOnWriteArrayList,采用写时复制策略,遍历顺序与插入顺序一致,适合遍历操作远多于修改操作的高并发场景:
// 写时复制的线程安全有序 Set Set<String> cowSet = new CopyOnWriteArraySet<>(); cowSet.add("Java"); cowSet.add("Python"); cowSet.add("C++"); // 高并发遍历:无锁,效率极高,顺序为插入顺序 cowSet.forEach(s -> System.out.println(s)); // 写操作:复制整个数组,成本较高,适合修改少的场景 cowSet.remove("Python");
特点:
LinkedHashSet 的底层 LinkedHashMap 维护了一条双向链表,所有元素节点都加入该链表,新元素始终插入到链表尾部,重复添加不改变节点位置,删除元素同步移除链表节点,遍历基于该双向链表实现,因此保证了插入顺序。
hashCode() 相等且equals() 返回 true。若不重写,会使用 Object 类的默认实现(基于内存地址),导致属性相同的对象被认为是不同元素,存入重复数据。Collections.synchronizedSet(new LinkedHashSet<>())(实现简单,全局锁);ConcurrentLinkedHashMap + Collections.newSetFromMap(高效,保留插入顺序);CopyOnWriteArraySet(写时复制,读无锁,天然插入有序)。Collections.synchronizedSet 包装,高并发用 Guava 的 ConcurrentLinkedHashMap 构建,遍历多修改少用 CopyOnWriteArraySet;
微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online