LinkedHashMap 源码解析

1、简述
  • LinkedHashMap 继承于 HashMap,在 HashMap 的基础上,通过维护一条双向链表,解决了 HashMap 不能随时保持遍历顺序和插入顺序一致性的问题。
2、归纳
  • 基于 HashMap 实现的,继承于 HashMap,有序(插序),不可重复,允许 null 键和 null 值存在。
  • 与 HashMap 的区别是,HashMap 的链表是单链表,而这里的链表是双向链表。
  • 比 HashMap 多一个 accessOrder 属性,默认 false,若为 false,遍历双向链表时,是按照插入顺序排序,若为 true,表示双向链表中的元素按照访问的先后顺序排列,最先遍历到(链表头)的是最近最少使用的元素。
  • LRU 算法底层就是基于 LinkedHashMap 实现的,且 accessOrder 为 true。它会将当前访问的 Entry(在这里指 put 进来的 Entry)移动到双向循环链表的尾部,从而实现双向链表中的元素按照访问顺序来排序(最近访问的 Entry 放到链表的最后,这样多次下来,前面就是最近没有被访问的元素,在实现 LRU 算法时,当双向链表中的节点数达到最大值时,将前面的元素删去即可,因为前面的元素是最近最少使用的),否则什么也不做。
  • 单线程安全,多线程不安全。
3、分析
3.1、接口
  • 在分析 LinkedHashMap 源码之前,我们先来看看集合的接口 Map。
public interface Map<K, V> {
    ...
    // 增
    V put(K key, V value);
    // 删
    V remove(Object key);
    // 查
    V get(Object key);
    ...
}
  • 在上述接口中,我只抽取了比较重要的几个方法,然后以此为后续重点分析目标,其 Map 接口对应的源码中远不止上述几个方法,有兴趣的同学可以自行翻阅。
3.2、静态内部类
public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>{
    
    ...
    // LinkedHashMap 的结构
    static class LinkedHashMapEntry<K,V> extends HashMap.Node<K,V> {
        LinkedHashMapEntry<K,V> before, after;
        LinkedHashMapEntry(int hash, K key, V value, Node<K,V> next) {
            super(hash, key, value, next);
        }
    }
    ...
    
}
  • 通过 HashMap 源码和上述可得知依赖关系链:
  • (1)HashMap.Node 实现于 Map.Entry;
  • (2)LinkedHashMapEntry 继承于 HashMap.Node;
  • (3)HashMap.TreeNode 继承于 LinkedHashMapEntry。
3.3、成员变量
public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>{
    
    // 序列化唯一表示 UID
    private static final long serialVersionUID = 3801124242820219131L;

    // 双向链表的头节点
    transient LinkedHashMapEntry<K,V> head;

    // 双向链表的尾节点
    transient LinkedHashMapEntry<K,V> tail;

    // 默认是 false,则迭代时输出的顺序是插入节点的顺序。
    // 若为 true,则输出的顺序是按照访问节点的顺序(最近最少使用原则)。
    // accessOrder 为 true 时,可以在这基础之上构建一个 LruCache。
    final boolean accessOrder;

    ...
    
}
3.4、构造函数
public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>{
    
    ...
    
    /**
     * 无参构造函数
     */
    public LinkedHashMap() {
        super();
        accessOrder = false;
    }

    /**
     * 有参构造函数
     *
     * @param initialCapacity 初始化时的容量
     */
    public LinkedHashMap(int initialCapacity) {
        super(initialCapacity);
        accessOrder = false;
    }

    /**
     * 有参构造函数
     *
     * @param initialCapacity 初始化时的容量
     * @param loadFactor      扩容的加载因子
     */
    public LinkedHashMap(int initialCapacity, float loadFactor) {
        super(initialCapacity, loadFactor);
        accessOrder = false;
    }

    /**
     * 有参构造函数
     *
     * @param initialCapacity 初始化时的容量
     * @param loadFactor      扩容的加载因子
     * @param accessOrder     迭代输出节点的顺序
     */
    public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) {
        super(initialCapacity, loadFactor);
        this.accessOrder = accessOrder;
    }

    /**
     * 有参构造函数
     *
     * @param m 利用另一个Map 来构建
     */
    public LinkedHashMap(Map<? extends K, ? extends V> m) {
        super();
        accessOrder = false;
        // 批量插入一个 map 中的所有数据到本集合中。
        putMapEntries(m, false);
    }

    ...
    
}
3.5、增操作
  • LinkedHashMap 并没有重写任何 put() 方法,但是其重写了构建新节点的 newNode() 方法。
  • newNode() 会在 HashMap 的 putVal() 方法里被调用,putVal() 方法会在批量插入数据 putMapEntries(Map<? extends K, ? extends V> m, boolean evict) 或者插入单个数据 public V put(K key, V value) 时被调用。
public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>{
    
    ...
    
    /**
     * 在构建新节点时,构建的是 LinkedHashMap.Entry,不再是 HashMap.Node
     *
     * @param hash  hash值
     * @param key   键
     * @param value 元素值
     * @param e     节点
     * @return 返回新构建的节点
     */
    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;
    }

    /**
     * 将新增的节点,链接在链表的尾部
     *
     * @param 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;
        }
    }

    /**
     * 新节点插入之后的回调函数
     * 根据 evict 和判断是否需要删除最老插入的节点,如果实现 LruCache 会用到这个方法。
     *
     * @param evict
     */
    void afterNodeInsertion(boolean evict) {
        LinkedHashMap.Entry<K, V> first;
        // LinkedHashMap 默认返回 false 则不删除节点
        if (evict && (first = head) != null && removeEldestEntry(first)) {
            K key = first.key;
            removeNode(hash(key), key, null, false, true);
        }
    }

    /**
     * LinkedHashMap 默认返回 false 则不删除节点。 返回true 代表要删除最早的节点。
     * 通常构建一个 LruCache 会在达到 Cache 的上限是返回 true
     *
     * @param eldest
     * @return 是否需要删除最老的节点
     */
    protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
        return false;
    }

    ...
    
}
3.6、删操作
  • LinkedHashMap 并没有重写 remove() 方法,因为它的删除逻辑和 HashMap 并无区别。但它重写了 afterNodeRemoval() 这个回调方法。该方法会在 removeNode() 方法中回调。
public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>{
    
    ...
    
    /**
     * 节点移除后的回调
     *
     * @param e 待移除的节点
     */
    void afterNodeRemoval(Node<K, V> e) {
        LinkedHashMap.Entry<K, V> p =
                (LinkedHashMap.Entry<K, V>) e, b = p.before, a = p.after;
        // 待删除节点 p 的前置后置节点都置空
        p.before = p.after = null;
        // 如果前置节点是 null,则现在的头结点应该是后置节点 a
        if (b == null)
            head = a;
        else// 否则将前置节点 b 的后置节点指向 a
            b.after = a;
        // 同理如果后置节点时 null,则尾节点应是 b
        if (a == null)
            tail = b;
        else// 否则更新后置节点 a 的前置节点为 b
            a.before = b;
    }
    ...
    
}
3.7、查操作
public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>{
    
    ...
    
    /**
     * 根据 key 查操作
     *
     * @param key 键
     * @return 返回查找后的元素值,如果不存在则返回 null
     */
    public V get(Object key) {
        Node<K, V> e;
        if ((e = getNode(hash(key), key)) == null)
            return null;
        // 如果 accessOrder==true,则按照 LRU 算法排序
        if (accessOrder)
            afterNodeAccess(e);
        return e.value;
    }

    /**
     * 根据 key 查操作
     *
     * @param key          键
     * @param defaultValue 默认值
     * @return 返回查找后的元素值,如果不存在则返回默认值
     */
    public V getOrDefault(Object key, V defaultValue) {
        Node<K, V> e;
        if ((e = getNode(hash(key), key)) == null)
            return defaultValue;
        // 如果 accessOrder==true,则按照 LRU 算法排序
        if (accessOrder)
            afterNodeAccess(e);
        return e.value;
    }
    
    /**
     * 节点访问后的回调
     * 主要作用:将当前被访问到的节点e,移动至内部的双向链表的尾部
     *
     * @param e 待访问的节点
     */
    void afterNodeAccess(Node<K, V> e) {
        LinkedHashMap.Entry<K, V> last;// 原尾节点
        // 如果 accessOrder 是 true,且原尾节点不等于 e
        if (accessOrder && (last = tail) != e) {
            // 节点 e 强转成双向链表节点 p
            LinkedHashMap.Entry<K, V> p =
                    (LinkedHashMap.Entry<K, V>) e, b = p.before, a = p.after;
            // p 现在是尾节点, 后置节点一定是 null
            p.after = null;
            // 如果 p 的前置节点是 null,则 p 以前是头结点,所以更新现在的头结点是 p 的后置节点 a
            if (b == null)
                head = a;
            else// 否则更新 p 的前直接点 b 的后置节点为 a
                b.after = a;
            // 如果 p 的后置节点不是 null,则更新后置节点 a 的前置节点为 b
            if (a != null)
                a.before = b;
            else// 如果原本 p 的后置节点是 null,则 p 就是尾节点。此时更新 last 的引用为 p 的前置节点 b
                last = b;
            if (last == null)// 原本尾节点是 null 则,链表中就一个节点
                head = p;
            else {// 否则更新当前节点 p 的前置节点为原尾节点 last,last 的后置节点是 p
                p.before = last;
                last.after = p;
            }
            // 尾节点的引用赋值成 p
            tail = p;
            // 修改 modCount。
            ++modCount;
        }
    }
    ...
    
}
4、重写 containsValue() 方法
public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>{
    
    ...
    
    /**
     * 判断是否存在 value 元素值
     *
     * @param value 查找的元素值
     * @return 返回 true 代表存在,返回 false 代表不存在
     */
    public boolean containsValue(Object value) {
        // 遍历一遍链表,去比较有没有 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(两个for循环遍历)更加高效,之所以高效是因为新增的 after 链表可以将所有的元素串联起来。
5、为什么它不重写 containsKey() 方法,也去循环比对内部链表的key是否相等呢?
  • HashMap 最大的优点就是通过对 key 取 hash 来快速地位 key 在桶中的位置,这是结合了数组查找的优点,同时也是对链表节点查找的改进。所以如果 LinkedHashMap 覆写 containsKey() 方法,去循环比对内部链表的 key 是否相等,是低效的。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值