💪
AndroidCollect
  • 写在前面
  • 计算机基础
    • 计算机组成原理
    • 算法
      • 查找
        • 二分查找
      • 排序
        • 简单排序
        • 高级排序
        • 特殊排序
      • 海量数据
      • 思想
        • 贪心
        • 分治
        • 动态规划
        • 回溯
      • 哈希算法
    • 数据结构
      • 队列
        • 知识点
        • 相关题目
          • 用两个栈实现队列
          • 实现循环队列
          • 用链表实现队列
          • 用数组实现队列
      • 栈
        • 相关算法题目
          • 用链表实现栈
          • 用数组实现栈
      • 链表
        • 知识点梳理
        • 相关算法题目
          • 删除倒数第n个结点
          • 合并两个有序链表
          • 检测单链表是否有环
          • 获取中间结点
          • 反转链表
      • 跳表
      • 哈希表
      • 树
        • 二叉树
        • 二叉查找树
        • AVL 树
        • Trie 树
        • 红黑树
      • 堆
        • 存储
        • 堆的应用
      • 图
    • 网络
      • 应用层协议
        • DNS
        • HTTP
        • HTTPS
      • 传输层协议
        • TCP
        • UDP
      • 输入网址后发生了什么
    • 操作系统
      • 内存
    • 数据库
  • 软件工程
    • 编程思想
    • 设计模式
      • 状态模式
      • 装饰器模式
      • 代理模式
      • 责任链模式
      • 建造者模式
      • 单例模式
      • 观察者模式
  • Java
    • 基础
    • 异常
    • 并发编程
      • ThreadLocal
      • 线程池
      • 理解 volatile
      • AbstractQueuedSynchronizer
    • 集合
      • LinkedHashMap 源码
      • HashMap 源码
    • 注解
    • 反射
      • JDK 动态代理
    • JVM
      • 自动内存管理机制
      • Class 文件格式
      • 类加载机制
      • Java 内存模型(JMM)
      • 字节码指令
      • HotSpot 虚拟机实现细节
    • 源码与原理
    • 各版本主要特性
  • Android
    • 基础组件
      • Context
      • Activity
        • 生命周期
        • 启动模式与任务栈
        • 启动流程
      • Service
      • ContentProvider
      • BroadcastReceiver
      • Fragment
      • View
        • 常用控件问题总结
          • RecyclerView
          • ViewPager2
        • CoordinatorLayout
        • SurfaceView
        • 事件分发
        • 绘制流程
        • 自定义 View
        • Window
    • 数据存储
      • 存储结构
      • Sqlite
      • 序列化
      • SharedPreferences
    • 资源
      • 图片加载
    • 动画
      • 属性动画
    • 线程和进程
      • Binder 机制
      • 跨进程通信
        • AIDL
    • 内部原理
      • 消息循环机制
      • Binder
      • Window
      • SparseArray
      • ArrayMap
      • RecyclerView
      • App 启动流程
    • 性能优化
      • 内存
        • 内存使用优化
        • 内存泄漏
      • 启动优化
      • 缩减包大小
      • 布局优化
      • ANR
    • 打包构建
      • dex 文件
      • APK 打包流程
      • APK 签名流程
    • 架构
      • 运行时
      • Android 系统架构
      • 应用项目架构
    • 开源框架源码或原理
      • RxJava
        • 使用笔记
        • 源码解析
      • Retrofit
      • ButterKnife
      • BlockCanary
      • LeakCanary
      • OkHttp
      • 图片加载
        • Glide
        • Picasso
    • 碎片化处理
      • 屏幕适配
    • 黑科技
      • 热修复
    • Jetpack
      • Lifecycle
      • Room
      • WorkManager
    • 新动态
      • AndroidX
      • 各系统版本特性
  • 开发工具
    • 正则表达式
    • ADB
    • Git
  • Kotlin
  • Flutter
  • 关于作者
  • 致谢
由 GitBook 提供支持
在本页
  • 一个参数
  • 节点类型
  • 节点创建
  • 更新迭代顺序的时机
  • 调整顺序的源码实现
  • linkNodeLast
  • afterNodeInsertion
  • afterNodeRemoval
  • afterNodeAccess
  • 其他优化
  • containsValue()
  • 总结

这有帮助吗?

  1. Java
  2. 集合

LinkedHashMap 源码

LinkedHashMap 是一个哈希表,拥有可预测的迭代顺序。与 HashMap 不同点在于使用了一个双向链表来维护所有的 Entry,双向链表定义了哈希表的迭代顺序。

一个参数

LinkedHashMap 的迭代顺序有两种:节点插入顺序和节点访问顺序。可通过在构造方法中传递 boolean accessOrder来指定——当 accessOrder 为 false 时,迭代顺序为节点插入顺序;当 accessOrder 为 true 时,迭代顺序为节点访问顺序。

节点类型

继承了 HashMap 的 Node 类,并增加了前后指针,以便构造双向链表。

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 中,创建节点是通过 newNode 方法:

Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
        return new Node<>(hash, key, value, next);
    }

而 LinkedHashMap 通过重写 HashMap 的 newNode 方法,创建Entry实例:

@Override
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;
}

更新迭代顺序的时机

在 HashMap 中,定义了如下三个方法:

 // Callbacks to allow LinkedHashMap post-actions
    void afterNodeAccess(Node<K,V> p) { }
    void afterNodeInsertion(boolean evict) { }
    void afterNodeRemoval(Node<K,V> p) { }

通过注释可以看出是专门为 LinkedHashMap 进行后续行为而定义的。

afterNodeAccess 是节点被访问后调用,在 HashMap 中,这个方法只在更新旧节点的值时被调用了,在get方法内并没有被调用,因此 LinkedHashMap 重写了 get方法,当需要根据访问顺序迭代时,调用afterNodeAccess 调整:

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;
}

afterNodeInsertion 处理新增节点后行为,在 HashMap 中的 putVal方法中最后就调用了这个方法,但是 afterNodeInsertion 只处理了新节点添加之后的顺序更新,比如如果需要删除头节点,则在此时进行删除调整;而对于新创建的节点,更新迭代顺序是在linkNodeLast方法中,如上面代码所示,这个方法是在newNode方法中被调用的。

afterNodeRemoval 在 HashMap 中的removeNode方法中调用。

综上,LinkedHashMap 更新迭代顺序链表的时机为:

时机

调用方法

插入新节点时

linkNodeLast

插入新节点后(处理删除头节点)

afterNodeInsertion

更新旧节点

afterNodeAccess

删除节点

afterNodeRemoval

访问节点

afterNodeAccess

调整顺序的源码实现

linkNodeLast

linkNodeLast 的作用就是将新创建的节点放置于链表尾部,其源码如下:

private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
    //tail 是指向链表尾结点的引用
    LinkedHashMap.Entry<K,V> last = tail;
    tail = p;
    if (last == null){
        //链表为空时,初始化头结点
        head = p;
    }else {
        //新尾节点before指向原尾结点
        p.before = last;
        //原尾节点的after 指向的新尾节点
        last.after = p;
    }
}

afterNodeInsertion

void afterNodeInsertion(boolean evict) { // possibly remove eldest
        LinkedHashMap.Entry<K,V> first;
        // evict=true & 链表不为空 & removeEldestEntry返回 true
        if (evict && (first = head) != null && removeEldestEntry(first)) {
            K key = first.key;
            //删除头结点
            removeNode(hash(key), key, null, false, true);
        }
    }

evict 标识 HashMap 是否处于创建中,evict 为 false 的情况主要包括在构造方法中、clone、readFromObject等方法中调用 put 相关操作时,其他情况一律为 true。

可以看出 afterNodeInsertion 主要是根据需要(removeEldestEntry 方法返回值)移除头结点,然后在移除后会有调整顺序的操作,不过已经是在afterNodeRemoval方法中了。 (removeNode 会调用 afterNodeRemoval)

afterNodeRemoval

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;
    }
}

afterNodeAccess

afterNodeAccess 方法在 accessOrder为true时,将被访问的节点移到链表尾部。

void afterNodeAccess(Node<K,V> e) { // move node to last
    LinkedHashMap.Entry<K,V> last;
    //accessOrder=true && 访问的不是尾节点
    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{
            //疑问:如果后继节点为null,则当前节点是尾结点(tail有可能为null?),而方法开始已经加了非尾节点的判断,为什么这里又判断一次呢?
            //last 指向前驱节点
            last = b;
        }
        if (last == null){
            head = p;
        }else {
            //移到链表尾部
            p.before = last;
            last.after = p;
        }
        //更新尾节点指针
        tail = p;
        ++modCount;
    }
}

其他优化

containsValue()

LinkedHashMap 直接从头遍历双向链表查找, HashMap 遍历数组同时还要遍历每个桶内的链表,效率比LinkedHashMap 低。

//LinkedHashMap 
public boolean containsValue(Object value) {
    for (LinkedHashMapEntry<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
public boolean containsValue(Object value) {
    Node<K,V>[] tab; V v;
    if ((tab = table) != null && size > 0) {
        for (int i = 0; i < tab.length; ++i) {
            for (Node<K,V> e = tab[i]; e != null; e = e.next) {
                if ((v = e.value) == value ||
                        (value != null && value.equals(v)))
                    return true;
            }
        }
    }
    return false;
}

总结

LinkedHashMap 是一个拥有可预测迭代顺序的哈希表,它继承自 HashMap,并用一个双向链表来维护所有 entry 的顺序,默认迭代顺序是按节点插入顺序,可以通过将accessOrder设为true将迭代顺序指定为节点访问顺序。当节点被添加、修改、访问和删除时,通过调用对应的方法来调整节点在双向链表中的顺序,从而达到维护访问顺序的目的。

上一页集合下一页HashMap 源码

最后更新于5年前

这有帮助吗?