深度解析LinkedHashMap工作原理

一、引言

在 Java 集合框架中,LinkedHashMap 是一个特殊且实用的类。它继承自 HashMap,同时又维护了一个双向链表,这使得它不仅能像 HashMap 一样快速存储和查找键值对,还能按照插入顺序或者访问顺序来维护元素。本文将深入探讨 LinkedHashMap 的原理,包括其底层数据结构、核心属性、构造方法、常用操作的实现细节,并结合丰富的代码示例进行说明。

二、LinkedHashMap 概述

2.1 定义与用途

LinkedHashMapjava.util 包下的一个类,它实现了 Map 接口。与 HashMap 不同的是,LinkedHashMap 能够记住元素的插入顺序或者访问顺序。这一特性使得它在很多场景下非常有用,例如实现缓存(如 LRU 缓存)、记录操作历史等。

2.2 继承关系与实现接口

LinkedHashMap 的继承关系如下:

plaintext 复制代码
java.lang.Object
    └─ java.util.AbstractMap<K,V>
        └─ java.util.HashMap<K,V>
            └─ java.util.LinkedHashMap<K,V>

它实现了 Map<K,V>Cloneablejava.io.Serializable 接口,具备克隆和序列化的能力。

java 复制代码
import java.util.LinkedHashMap;
import java.util.Map;

public class LinkedHashMapOverview {
    public static void main(String[] args) {
        // 创建一个 LinkedHashMap 对象
        LinkedHashMap<String, Integer> linkedHashMap = new LinkedHashMap<>();
        // 可以将其赋值给 Map 接口类型的变量
        Map<String, Integer> map = linkedHashMap;
    }
}

三、底层数据结构:哈希表与双向链表

3.1 哈希表的作用

LinkedHashMap 基于 HashMap 实现,HashMap 的底层是哈希表(数组 + 链表 + 红黑树)。哈希表通过哈希函数将键映射到数组的某个位置,这个位置称为桶(Bucket)。当多个键映射到同一个桶时,会发生哈希冲突,HashMap 使用链地址法来解决冲突,即每个桶中存储一个链表或红黑树。

3.2 双向链表的作用

LinkedHashMap 额外维护了一个双向链表,链表中的节点不仅包含键值对,还包含指向前一个节点和后一个节点的引用。这个双向链表按照元素的插入顺序或者访问顺序将所有元素连接起来,从而保证了元素的有序性。

3.3 节点结构

LinkedHashMap 中的节点是 Entry 类型,它继承自 HashMap.Node,并额外添加了 beforeafter 两个引用,用于维护双向链表的顺序。

java 复制代码
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);
    }
}

四、核心属性

LinkedHashMap 有几个重要的核心属性,这些属性控制着 LinkedHashMap 的行为和状态:

java 复制代码
// 双向链表的头节点
transient LinkedHashMap.Entry<K,V> head;
// 双向链表的尾节点
transient LinkedHashMap.Entry<K,V> tail;
// 访问顺序标志,默认为 false,表示按照插入顺序
final boolean accessOrder;
  • head:指向双向链表的头节点,即最早插入的元素。
  • tail:指向双向链表的尾节点,即最晚插入的元素。
  • accessOrder:一个布尔值,用于控制双向链表的顺序。如果为 false,则按照元素的插入顺序;如果为 true,则按照元素的访问顺序。

五、构造方法

5.1 无参构造方法

java 复制代码
public LinkedHashMap() {
    super();
    accessOrder = false;
}

无参构造方法调用父类 HashMap 的构造方法,并将 accessOrder 设置为 false,表示按照插入顺序维护元素。

5.2 指定初始容量的构造方法

java 复制代码
public LinkedHashMap(int initialCapacity) {
    super(initialCapacity);
    accessOrder = false;
}

该构造方法允许指定 LinkedHashMap 的初始容量,accessOrder 仍然为 false

5.3 指定初始容量和负载因子的构造方法

java 复制代码
public LinkedHashMap(int initialCapacity, float loadFactor) {
    super(initialCapacity, loadFactor);
    accessOrder = false;
}

此构造方法允许同时指定初始容量和负载因子,accessOrderfalse

5.4 指定初始容量、负载因子和访问顺序的构造方法

java 复制代码
public LinkedHashMap(int initialCapacity,
                     float loadFactor,
                     boolean accessOrder) {
    super(initialCapacity, loadFactor);
    this.accessOrder = accessOrder;
}

该构造方法允许指定初始容量、负载因子和访问顺序,通过 accessOrder 参数可以控制元素是按照插入顺序还是访问顺序排列。

5.5 从其他 Map 创建 LinkedHashMap 的构造方法

java 复制代码
public LinkedHashMap(Map<? extends K, ? extends V> m) {
    super();
    accessOrder = false;
    putMapEntries(m, false);
}

此构造方法接受一个 Map 对象作为参数,将该 Map 中的所有键值对添加到新创建的 LinkedHashMap 中,accessOrderfalse

java 复制代码
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

public class LinkedHashMapConstructors {
    public static void main(String[] args) {
        // 无参构造方法
        LinkedHashMap<String, Integer> linkedHashMap1 = new LinkedHashMap<>();

        // 指定初始容量的构造方法
        LinkedHashMap<String, Integer> linkedHashMap2 = new LinkedHashMap<>(20);

        // 指定初始容量和负载因子的构造方法
        LinkedHashMap<String, Integer> linkedHashMap3 = new LinkedHashMap<>(15, 0.8f);

        // 指定初始容量、负载因子和访问顺序的构造方法
        LinkedHashMap<String, Integer> linkedHashMap4 = new LinkedHashMap<>(10, 0.75f, true);

        // 从其他 Map 创建 LinkedHashMap 的构造方法
        Map<String, Integer> hashMap = new HashMap<>();
        hashMap.put("apple", 1);
        hashMap.put("banana", 2);
        LinkedHashMap<String, Integer> linkedHashMap5 = new LinkedHashMap<>(hashMap);

        System.out.println("LinkedHashMap1: " + linkedHashMap1);
        System.out.println("LinkedHashMap2: " + linkedHashMap2);
        System.out.println("LinkedHashMap3: " + linkedHashMap3);
        System.out.println("LinkedHashMap4: " + linkedHashMap4);
        System.out.println("LinkedHashMap5: " + linkedHashMap5);
    }
}

六、常用操作原理

6.1 插入元素(put 方法)

LinkedHashMapput 方法实际上是调用父类 HashMapput 方法,在插入元素时,会创建一个新的 Entry 节点,并将其添加到哈希表中,同时将该节点添加到双向链表的尾部。

java 复制代码
// HashMap 的 put 方法
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

// LinkedHashMap 重写的 newNode 方法
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;
}

// 将节点添加到双向链表的尾部
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;
    }
}

6.2 获取元素(get 方法)

java 复制代码
public V get(Object key) {
    Node<K,V> e;
    if ((e = getNode(hash(key), key)) == null)
        return null;
    if (accessOrder)
        afterNodeAccess(e);
    return e.value;
}

// 访问节点后调整双向链表的顺序
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 = b;
        if (last == null)
            head = p;
        else {
            p.before = last;
            last.after = p;
        }
        tail = p;
        ++modCount;
    }
}

accessOrdertrue 时,每次访问元素后,会将该元素移动到双向链表的尾部,从而保证链表按照访问顺序排列。

6.3 删除元素(remove 方法)

java 复制代码
public V remove(Object key) {
    Node<K,V> e;
    return (e = removeNode(hash(key), key, null, false, true)) == null ?
        null : e.value;
}

// LinkedHashMap 重写的 afterNodeRemoval 方法
void afterNodeRemoval(Node<K,V> e) { // unlink
    LinkedHashMap.Entry<K,V> p =
        (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
    p.before = p.after = null;
    if (b == null)
        head = a;
    else
        b.after = a;
    if (a == null)
        tail = b;
    else
        a.before = b;
}

删除元素时,会将该元素从哈希表中移除,同时从双向链表中移除。

6.4 遍历元素

LinkedHashMap 支持多种遍历方式,例如使用 entrySet()keySet()values() 方法获取相应的集合,然后使用迭代器或增强 for 循环进行遍历。由于 LinkedHashMap 维护了双向链表,遍历顺序会按照插入顺序或者访问顺序进行。

java 复制代码
import java.util.LinkedHashMap;
import java.util.Map;

public class LinkedHashMapTraversal {
    public static void main(String[] args) {
        LinkedHashMap<String, Integer> linkedHashMap = new LinkedHashMap<>();
        linkedHashMap.put("apple", 1);
        linkedHashMap.put("banana", 2);
        linkedHashMap.put("cherry", 3);

        // 遍历键值对
        for (Map.Entry<String, Integer> entry : linkedHashMap.entrySet()) {
            System.out.println(entry.getKey() + ": " + entry.getValue());
        }

        // 遍历键
        for (String key : linkedHashMap.keySet()) {
            System.out.println(key);
        }

        // 遍历值
        for (Integer value : linkedHashMap.values()) {
            System.out.println(value);
        }
    }
}

七、LRU 缓存实现示例

LinkedHashMap 可以很方便地实现 LRU(Least Recently Used,最近最少使用)缓存。通过设置 accessOrdertrue,并重写 removeEldestEntry 方法,可以在缓存达到最大容量时自动移除最旧的元素。

java 复制代码
import java.util.LinkedHashMap;
import java.util.Map;

public class LRUCache<K, V> extends LinkedHashMap<K, V> {
    private final int capacity;

    public LRUCache(int capacity) {
        // 初始容量为 capacity,负载因子为 0.75f,accessOrder 为 true
        super(capacity, 0.75f, true) {
            @Override
            protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
                // 当元素数量超过容量时,移除最旧的元素
                return size() > capacity;
            }
        };
        this.capacity = capacity;
    }

    public static void main(String[] args) {
        LRUCache<String, Integer> cache = new LRUCache<>(3);
        cache.put("apple", 1);
        cache.put("banana", 2);
        cache.put("cherry", 3);
        System.out.println(cache); // 输出: {apple=1, banana=2, cherry=3}

        cache.get("apple");
        System.out.println(cache); // 输出: {banana=2, cherry=3, apple=1}

        cache.put("date", 4);
        System.out.println(cache); // 输出: {cherry=3, apple=1, date=4}
    }
}

八、性能分析

8.1 时间复杂度

  • 插入操作:插入操作的平均时间复杂度为 O(1),因为哈希表可以通过哈希函数快速定位到桶的位置,在没有哈希冲突的情况下,插入操作可以在常数时间内完成。同时,将节点添加到双向链表的尾部也只需要常数时间。
  • 查找操作 :查找操作的平均时间复杂度为 O(1),同样可以通过哈希函数快速定位到桶的位置,然后在桶中查找元素。如果 accessOrdertrue,还需要将访问的节点移动到双向链表的尾部,这也只需要常数时间。
  • 删除操作:删除操作的平均时间复杂度为 O(1),通过哈希函数定位到桶的位置,然后在桶中删除元素,并从双向链表中移除该节点。

8.2 空间复杂度

LinkedHashMap 的空间复杂度为 O(n),主要用于存储哈希表和双向链表中的节点。

九、注意事项

9.1 线程安全问题

LinkedHashMap 不是线程安全的。如果在多线程环境下需要使用线程安全的 Map,可以考虑使用 Collections.synchronizedMap() 方法将 LinkedHashMap 包装成线程安全的 Map

java 复制代码
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;

public class LinkedHashMapThreadSafety {
    public static void main(String[] args) {
        LinkedHashMap<String, Integer> linkedHashMap = new LinkedHashMap<>();
        Map<String, Integer> synchronizedMap = Collections.synchronizedMap(linkedHashMap);
    }
}

9.2 性能考虑

虽然 LinkedHashMap 的插入、查找和删除操作的平均时间复杂度与 HashMap 相同,但由于需要维护双向链表,会有一定的额外开销。因此,在不需要维护元素顺序的场景下,使用 HashMap 可能会有更好的性能。

十、总结

LinkedHashMap 是 Java 集合框架中一个非常实用的类,它结合了 HashMap 的快速查找和插入功能,以及双向链表的有序性。通过 accessOrder 参数,可以控制元素是按照插入顺序还是访问顺序排列。在实现 LRU 缓存等场景中,LinkedHashMap 表现出了强大的优势。但在使用时,需要注意线程安全问题和性能考虑。深入理解 LinkedHashMap 的原理和使用方法,有助于我们在实际开发中更好地利用它来解决各种问题。

相关推荐
会飞的皮卡丘EI3 分钟前
关于Blade框架对数字类型的null值转为-1问题
java·spring boot
雷渊6 分钟前
如何保证数据库和Es的数据一致性?
java·后端·面试
fjkxyl7 分钟前
Spring的启动流程
java·后端·spring
极客先躯9 分钟前
高级java每日一道面试题-2025年4月06日-微服务篇[Nacos篇]-如何诊断和解决Nacos中的常见问题?
java·开发语言·微服务
东锋1.316 分钟前
Spring AI 发布了它的 1.0.0 版本的第七个里程碑(M7)
java·人工智能·spring
KdanMin30 分钟前
Android系统通知机制深度解析:Framework至SystemUI全链路剖析
android
liwulin050635 分钟前
【JAVAFX】自定义FXML 文件存放的位置以及使用
java
2401_8906658643 分钟前
免费送源码:Java+ssm+MySQL 基于PHP在线考试系统的设计与实现 计算机毕业设计原创定制
java·hadoop·spring boot·python·mysql·spring cloud·php
逸风尊者1 小时前
开发可掌握的知识:基于事件驱动实现状态机
java
佩奇的技术笔记1 小时前
Java学习手册:Java线程安全与同步机制
java