HashMap居然可以和它直接合体???

LinkedHashMap集合继承于HashMap,学习LinkedHashMap重点对比 LinkedHashMapHashMap 的异同

特别强调两者的 Entry(节点)数据结构、数据结构的不同带来的特性差异、HashMap 的后置处理机制及最少访问删除策略。

LinkedHashMap = HashMap + LinkedList

就像这幅图一样?

image

1. Entry(节点)数据结构

1.1. HashMap.Node

/* by 01130.hk - online tools website : 01130.hk/zh/regexcode.html */
static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;
    Node<K,V> next;
    // … 构造、getKey/getValue/setValue、equals/hashCode 等 …
}

字段说明:hash:key 的哈希值,keyvalue:存储的键值对,next:链表或树化时的链表指针。

image

1.2. LinkedHashMap.Entry

/* by 01130.hk - online tools website : 01130.hk/zh/regexcode.html */
// 头节点
transient LinkedHashMap.Entry<K,V> head;
// 尾节点
transient LinkedHashMap.Entry<K,V> tail;
// 节点类
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);
    }
}

新增字段说明beforeafter:维护插入/访问顺序的双向链表;

链表头尾:在 LinkedHashMap 中,维护一个 headtail 指针,插入时追加到尾部。

image

LinkedHashMap 数据结构就像它的名称一样Linked + HashMap,它是在HashMap的基础上,维护了一个双向链表。这个双向链表就像LinkedList一样,可以维护节点插入的顺序。

LinkedHashMap 数据结构是两种形态共存的数据结构

  • 你可以忽略双向链表,把它看做普通的HashMap
  • 也可以忽略HashMap,把它看作是双向链表。

如果你不想使用LinkedHashMap,但又想要维护HashMap的插入顺序,那你可以在HashMap.put元素后,同时将该元素保存到LinkedList.add集合,但这样就需要你确保集合一致性,比如插入和删除。这么想想,还不如直接用LinkedHashMap集合较为稳妥。

其实LinkedHashMap 一部分源码为的就是维护HashMap与双向链表的一致性,及操作过程做的一些扩展。比如:节点创建时的双向链表尾部插入和HashMap的后置处理。

1.3. 两者对比

特性HashMapLinkedHashMap
底层数据结构数组 + 链表/红黑树数组 + 链表/红黑树 + 双向链表
迭代顺序不保证顺序按插入顺序(或访问顺序,可选)
内存开销较小较大(每个节点额外维护链表指针)
适用场景一般的键值存取需要按插入或访问顺序遍历 (如 LRU 缓存)

1.4. 详细的数据结构案例

通过下面的案例图,清楚地看见每个节点的指向。

假设插入顺序为:22、23、45、89、25、38、49、28

插入完成后的数据结构如图,

图中信息含义: 当前节点信息只显示key值,next为下一个映射冲突节点;before为双链结构的上一节点,after为双链结构的下一节点;绿色虚线是整个LinkedHashMap的双链结构的连接关系。

image

可以清楚地看到,相比HashMap每个节点都需要多维护beforeafter节点,LinkedHashMap也就需要更多的空间。

2. 节点创建和转化重写

2.1. 创建Entry节点

链表节点创建的同时,通过linkNodeLast(p) 方法,维护双链结构的尾部插入

Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
	LinkedHashMap.Entry<K,V> p =
		new LinkedHashMap.Entry<K,V>(hash, key, value, e);
	linkNodeLast(p);
	return p;
}

2.2. 树节点创建

红黑树节点创建的同时,通过linkNodeLast(p) 方法,维护双链结构的尾部插入

TreeNode<K,V> newTreeNode(int hash, K key, V value, Node<K,V> next) {
	TreeNode<K,V> p = new TreeNode<K,V>(hash, key, value, next);
	linkNodeLast(p);
	return p;
}

2.3. 节点转化

树节点转化为Entry节点Entry节点转化树节点都做了重写,通过transferLinks(q, t);方法完成节点转化

Node<K,V> replacementNode(Node<K,V> p, Node<K,V> next) {
	LinkedHashMap.Entry<K,V> q = (LinkedHashMap.Entry<K,V>)p;
	LinkedHashMap.Entry<K,V> t =
		new LinkedHashMap.Entry<K,V>(q.hash, q.key, q.value, next);
	transferLinks(q, t);
	return t;
}

TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {
	LinkedHashMap.Entry<K,V> q = (LinkedHashMap.Entry<K,V>)p;
	TreeNode<K,V> t = new TreeNode<K,V>(q.hash, q.key, q.value, next);
	transferLinks(q, t);
	return t;
}

这些都是多态的简单应用,HashMap 引用指向不同的实例化子类,实现不同的功能。

3. HashMap 的后置处理(post-processing)

HashMap 本身在节点插入、访问、删除后并不做额外操作。LinkedHashMap 则通过重写以下钩子方法,在插入、访问或删除时维护自己的双链表结构。

3.1. 插入后

仅在evict=true 并且removeEldestEntry(first)==true时,插入后才需要移除头部节点

void afterNodeInsertion(boolean evict) { // possibly remove eldest
	LinkedHashMap.Entry<K,V> first;
	if (evict && (first = head) != null && removeEldestEntry(first)) {
		K key = first.key;
		removeNode(hash(key), key, null, false, true);
	}
}

3.2. 访问后

仅在 accessOrder = true 时,访问后需调整顺序;需要在LinkedHashMap 的构造方法中设定accessOrder的值。

void afterNodeAccess(Node<K,V> e) {
    LinkedHashMap.Entry<K,V> p = (LinkedHashMap.Entry<K,V>)e;
    // 将 p 移到双向链表尾部
    moveNodeToLast(p);
}

3.3. 删除后

只要节点作删除,LinkedHashMap集合就必须删除双链表上的Entry 节点

void afterNodeRemoval(Node<K,V> e) {
    // 将 e 从双向链表中摘除
    unlinkNode((LinkedHashMap.Entry<K,V>)e);
}

这三步合称为 后置处理,保证在 putgetremove 等操作时,链表结构的正确维护。

但是、

为什么LinkedHashMap集合在插入完成后,需要多做一步删除头节点的操作呢?

为什么访问完成后需要将访问节点移动到双链表的尾部呢?

4. 最少访问删除策略

👉 为什么是 LinkedHashMap 而不是 HashMap

LinkedHashMapHashMap 基础上,增加了 双向链表,用来记录:插入顺序(默认)或访问顺序(accessOrder = true 时)

这就可以实现:记录最近访问的节点(最近访问的放到链表尾部);删除最久未访问的节点(链表头)

4.1. 核心方法

protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
    return false; // 默认不删除
}

这是一个 钩子,这个钩子removeEldestEntry(first) 在插入后缀处理中调用,LinkedHashMap 的源码如下:

void afterNodeInsertion(boolean evict) { // possibly remove eldest
	LinkedHashMap.Entry<K,V> first;
	if (evict && (first = head) != null && removeEldestEntry(first)) {
		K key = first.key;
		removeNode(hash(key), key, null, false, true);
	}
}

你可以自己重写,制定属于自己的处理策略

LinkedHashMap<K,V> lru = new LinkedHashMap<K,V>(16, 0.75f, true) {
    protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
        return size() > 100; // 超过 100 个就删除最老的
    }
};

当每次 put 后:afterNodeInsertion 调用 removeEldestEntry,如果返回 true,就从链表头删除最老节点

总结:

LinkedHashMapremoveEldestEntry + accessOrder=true 可实现简单 LRU 缓存。

HashMap 不自带任何访问追踪或自动删除机制,必须由使用者自己实现。

4.2. LRU缓存例子

LRU 缓存(Least Recently Used Cache,最近最少使用缓存)是一种常用的 缓存淘汰策略,它的核心思想是:

如果数据最近被访问过,那么将来被访问的可能性也更高;反之则淘汰。

规定固定大小为4的缓存容器,源码如下

public class LRUDemo {
    public static void main(String[] args) {
        // 指定只能缓存四个数据
        LinkedHashMap<String, String> linkedHashMap = new LinkedHashMap<>(16, 0.75f, true) {
            protected boolean removeEldestEntry(Map.Entry<String, String> eldest) {
                return size() > 4;
            }
        };

        linkedHashMap.put("A","1");
        linkedHashMap.put("B","2");
        linkedHashMap.put("C","3");
        linkedHashMap.put("D","4");
        printOrder(linkedHashMap);
        linkedHashMap.get("B"); // B 被访问,移到末尾
        printOrder(linkedHashMap);
        linkedHashMap.put("E","5");   // 淘汰最老的 A
        printOrder(linkedHashMap);
    }
    public static void printOrder(LinkedHashMap<String, String> linkedHashMap ) {
        System.out.print("数据结构:" + "\n[head]");
        for (Map.Entry<String, String> entry : linkedHashMap.entrySet()) {
            System.out.print(" ⇄ " + entry.getKey());
        }
        System.out.println(" ⇄ [tail]\n");
    }
}

执行输出结果如下:

数据结构:

[head] ⇄ A ⇄ B ⇄ C ⇄ D ⇄ [tail]

数据结构:

[head] ⇄ A ⇄ C ⇄ D ⇄ B ⇄ [tail]

数据结构:

[head] ⇄ C ⇄ D ⇄ B ⇄ E ⇄ [tail]

5. HashMap与LinkedHashMap区别汇总

对比维度HashMapLinkedHashMap
节点类型HashMap.NodeLinkedHashMap.Entry(继承自 HashMap.Node
顺序保证按插入顺序或访问顺序
额外字段nextnext + before + after
内存消耗较低较高(每个节点多两个引用)
put 后置处理afterNodeInsertion(链表尾部插入 + 可选淘汰)
get 后置处理afterNodeAccess(访问时链表重排序,仅 accessOrder = true)
remove 后置处理afterNodeRemoval(从链表中摘除)
用途高效快速随机存取需要遍历顺序、实现 LRU 缓存、保持可预测迭代顺序

通过上述对比,可以看出 LinkedHashMap 只是对 HashMap 的轻量增强:

  1. 核心额外逻辑:在每次增删改查操作后,钩入双向链表维护;
  2. 额外空间开销:每个节点多俩指针;
  3. 功能收益:可提供插入顺序或访问顺序的迭代、可实现基于访问顺序的缓存淘汰(如 LRU)。

6. 总结

LinkedHashMap集合继承于HashMap,重点对比 LinkedHashMapHashMap 不同的数据结构的带来的特性差异;为什么需要LinkedHashMap这种两种形态共存的数据结构;以及通过HashMap 的后置处理机制轻松实现数据结构的功能扩展;并且对LinkedHashMap最少访问删除策略LRU做了简单案例演示。

往期推荐

分类往期文章
Java集合原理HashMap集合--基本操作流程的源码可视化
Java集合--HashMap底层原理可视化,秒懂扩容、链化、树化
Java集合--从本质出发理解HashMap
Java集合--LinkedList源码可视化
Java集合源码--ArrayList的可视化操作过程
设计模式秘籍
(已全部开源)
掌握设计模式的两个秘籍
往期设计模式文章的:设计模式
软件设计师软考中级--软件设计师毫无保留的备考分享通过软考后却领取不到实体证书?
2023年下半年软考考试重磅消息
Java学习路线
和相应资源
Java全栈学习路线、学习资源和面试题一条龙

原创不易,觉得还不错的,三连支持:点赞、分享、推荐↓

<think>我们正在讨论HashMap、HashtableConcurrentHashMap之间的区别。这些类都是Java集合框架中用于存储键值对的类,但它们在实现、线程安全性其他特性上有显著不同。以下将详细分析它们之间的区别。 ### 一、线程安全性 - **HashMap**:非线程安全,多线程环境下使用可能导致数据不一致。例如,多个线程同时扩容可能导致链表成环[^1][^2]。 - **Hashtable**:线程安全,通过在所有方法上添加`synchronized`关键字实现,但性能较差(锁整个实例)[^2][^4]。 - **ConcurrentHashMap**:线程安全,采用分段锁(JDK 1.7)或CAS + synchronized(JDK 1.8)实现,性能更高[^3][^4]。 ### 二、null值处理 - **HashMap**:允许键值均为`null`(一个`null`键,多个`null`值)[^2][^5]。 - **Hashtable**:键或值均不允许为`null`,否则抛出`NullPointerException`[^4][^5]。 - **ConcurrentHashMap**:键值均不允许为`null`(避免并发场景下的歧义)[^3][^5]。 ### 三、继承结构与接口 - **HashMap**:继承`AbstractMap`,实现`Map`接口[^1][^2]。 - **Hashtable**:继承`Dictionary`(已过时),实现`Map`接口[^2][^4]。 - **ConcurrentHashMap**:继承`AbstractMap`,实现`ConcurrentMap`接口[^3][^4]。 ### 四、哈希算法与冲突解决 - **HashMap**: - 哈希算法:`(key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16)` - 冲突解决:链表→红黑树(阈值=8,退化阈值=6)[^1][^2]。 - **Hashtable**: - 哈希算法:直接使用`key.hashCode()`(易产生碰撞)[^2][^4]。 - **ConcurrentHashMap**: - 与HashMap类似,但操作需保证线程安全(如CAS计算size)[^3][^4]。 ### 五、扩容机制 | 类 | 初始容量 | 扩容规则 | |-------------------|----------|------------------------------| | HashMap | 16 | 新容量 = 旧容量 × 2 | | Hashtable | 11 | 新容量 = 旧容量 × 2 + 1 | | ConcurrentHashMap | 16 | 分段扩容(桶级别CAS操作) | ### 六、性能对比(多线程场景) | 操作 | HashMap | Hashtable | ConcurrentHashMap | |------------|--------------|--------------|-------------------| | 读(10线程)| 可能数据错乱 | ≈100ms | ≈20ms | | 写(10线程)| 可能死循环 | ≈150ms | ≈40ms | > **关键结论**: > - 单线程用`HashMap`,性能最优[^1]; > - 遗留系统用`Hashtable`(不推荐新项目)[^2][^4]; > - 高并发用`ConcurrentHashMap`[^3][^4]。 ### 七、方法差异 - **contains方法**: - `Hashtable`保留`contains()`方法(等同于`containsValue()`)[^4]; - `HashMap``ConcurrentHashMap`只有`containsKey()`/`containsValue()`[^2][^4]。 - **迭代器**: - `HashMap`迭代器是`fail-fast`的; - `ConcurrentHashMap`迭代器是弱一致的(不抛`ConcurrentModificationException`)[^3][^4]。 --- ### 相关问题 1. **为什么ConcurrentHashMap在JDK 1.8中放弃分段锁?** (分析CAS + synchronized相比分段锁的性能优势) 2. **HashMap在多线程下如何导致死循环?** (图解扩容时链表成环的过程) 3. **ConcurrentHashMap的size()方法如何实现无锁计数?** (解析JDK 1.8的CounterCell机制) 4. **Hashtable与Collections.synchronizedMap的区别是什么?** (对比两种线程安全实现方式的优劣) [^1]: HashMap、HashTable、ConcurrentHashMap 之间的区别 [^2]: HashMapHashtable以及ConcurrentHashMap的区别 [^3]: ConcurrentHashMap剖析 [^4]: HashMapConcurrentHashMapHashtable的区别 [^5]: HashMap HashTable ConcurrentHashMap keyvalue是否可以null的问题 源码分析</think>### HashMap、Hashtable、ConcurrentHashMap 核心区别对比 #### 一、线程安全性(核心差异) | **类** | **线程安全** | **实现机制** | |---------------------|--------------|------------------------------------------------------------------------------| | **HashMap** | ❌ 不安全 | 无同步机制 | | **Hashtable** | ✅ 安全 | 全表锁(所有方法用 `synchronized` 修饰) | | **ConcurrentHashMap** | ✅ 安全 | 分段锁(JDK 1.7)或 CAS + `synchronized` 桶锁(JDK 1.8)[^1][^3] | > **性能对比**: > 多线程写操作时: > $$ T_{\text{ConcurrentHashMap}} \ll T_{\text{Hashtable}} \quad (\text{吞吐量差 10 倍以上}) $$ #### 二、Null 值处理 | **类** | **Key 为 null** | **Value 为 null** | **原因** | |---------------------|-----------------|-------------------|--------------------------------------------------------------------------| | **HashMap** | ✅ 允许 | ✅ 允许 | 单独处理 `null` 键(存放在索引 0 的桶)[^2][^5] | | **Hashtable** | ❌ 禁止 | ❌ 禁止 | 调用 `key.hashCode()` 会抛 `NullPointerException`[^4][^5] | | **ConcurrentHashMap** | ❌ 禁止 | ❌ 禁止 | 避免并发场景下的歧义(如 `map.containsKey(key)` `map.get(key)` 不一致)[^3] | #### 三、继承结构与接口设计 ```mermaid classDiagram Map <|-- HashMap Map <|-- Hashtable Map <|-- ConcurrentMap ConcurrentMap <|-- ConcurrentHashMap class HashMap { +继承 AbstractMap +迭代器 fail-fast } class Hashtable { +继承 Dictionary(已过时) +elements() 方法(枚举) } class ConcurrentHashMap { +实现 ConcurrentMap 接口 +弱一致迭代器 } ``` - **过时设计**:`Hashtable` 的 `elements()` 方法返回枚举(`Enumeration`),而 `HashMap`/`ConcurrentHashMap` 使用迭代器(`Iterator`)[^2][^4] - **特殊方法**:`Hashtable` 保留 `contains()` 方法(等价于 `containsValue()`),其他两者只有 `containsKey()`/`containsValue()`[^4] #### 四、哈希冲突与扩容 | **特性** | **HashMap** | **Hashtable** | **ConcurrentHashMap** | |--------------------|--------------------|--------------------|---------------------------| | **初始容量** | 16 | 11 | 16 | | **扩容规则** | 2 的幂次(2×旧容量)| 2×旧容量+1 | 分段扩容(桶粒度 CAS) | | **冲突解决** | 链表→红黑树(JDK8)| 纯链表 | 同 HashMap(线程安全实现)| | **哈希计算** | `(h = key.hashCode()) ^ (h >>> 16)` | 直接使用 `key.hashCode()` | 类似 HashMap[^1][^2] | > **工程优化**: > HashMap 的 $2^n$ 容量设计使得桶索引计算简化为: > $$ \text{index} = \text{hash} \& (\text{capacity} - 1) $$ > 避免昂贵的取模运算[^1] #### 五、适用场景总结 | **场景** | **推荐选择** | **原因** | |------------------------------|----------------------|------------------------------------| | 单线程环境 | HashMap | 性能最优,API 丰富 | | 低并发读写 | Collections.synchronizedMap | 轻量级封装 | | 高并发读写 | ConcurrentHashMap | 分段锁/CAS 保证高性能 | | 遗留系统兼容 | Hashtable | 不推荐新项目使用(全表锁性能差)[^4] | --- ### 相关问题 1. **为什么 ConcurrentHashMap 在 JDK 1.8 中放弃分段锁?** (分析 CAS + `synchronized` 如何提升并发粒度) 2. **HashMap 多线程扩容为什么会造成死循环?** (图解链表成环的过程与条件) 3. **ConcurrentHashMap 的 size() 方法如何实现无锁计数?** (解析 JDK 1.8 的 CounterCell 机制) 4. **为什么 Hashtable 初始容量是 11 而 HashMap 是 16?** (对比质数取模与位运算的哈希分布差异) [^1]: HashMap、HashTable、ConcurrentHashMap 之间的区别 [^2]: HashMapHashtable以及ConcurrentHashMap的区别 [^3]: ConcurrentHashMap剖析 [^4]: HashMapConcurrentHashMapHashtable的区别 [^5]: HashMap HashTable ConcurrentHashMap keyvalue是否可以null的问题 源码分析
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值