LinkedHashMap 源码分析

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


前言

HashMap 不是有序的当需要保持元素的顺序时就可以使用 LinkedHashMap 它支持维护两种顺序,插入顺序和访问顺序(可用于实现 Lru 算法),LinkedHashMap 是 HashMap 的子类所以原理相同只不过 LinkedHashMap 额外维护了一个双向链表来保持顺序,在对 LinkedHashMap 进行增删查改时同时操作这个双向链表


提示:本文基于 Android SDK 28 JDK 1.8

一、源码分析

1. accessOrder

    public LinkedHashMap(int initialCapacity,
                         float loadFactor,
                         boolean accessOrder) {
        super(initialCapacity, loadFactor);
        this.accessOrder = accessOrder;
    }

accessOrder 通过构造方法传入,为 true 表示元素顺序是访问顺序,为 false 表示元素顺序是插入顺序,默认是 false

2. put

LinkedHashMap 并没有重写 put 方法,HashMap 在 put 元素时会调用 newNode(数组和链表) 和 newTreeNode(红黑树) 创建节点对象,LinkedHashMap 重写了这两个方法

    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(p);
        return p;
    }

newNode 返回的是 LinkedHashMap.Entry 对象,newTreeNode 返回的是 TreeNode 它们的继承关系是 java.util.HashMap.TreeNode 继承自 java.util.LinkedHashMap.Entry 继承自java.util.HashMap.Node 实现了 java.util.Map.Entry 接口,LinkedHashMap.Entry 的定义如下

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

增加了了两个指针 before 和 after 用于实现双向链表,在上面的 newNode 和 newTreeNode 中都调用了 linkNodeLast 将节点插入到双向链表的表尾

    private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
        LinkedHashMap.Entry<K,V> last = tail;
        // 将双向链表的表尾指向 p
        tail = p;
        // 如果原来双向链表的表尾为 null 说明双向链表还没有元素把表头也指向 p
        if (last == null)
            head = p;
        else {
        	// 链接到链表尾部
            p.before = last;
            last.after = p;
        }
    }

3.afterNodeXxx

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

HashHap 会在插入删除访问元素时调用这几个方法让 LinkedHashMap 可以维护元素顺序

3.1 afterNodeAccess

LinkedHashMap 重写了 get 和 getOrDefault 来处理访问的情况

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

    /**
     * {@inheritDoc}
     */
    public V getOrDefault(Object key, V defaultValue) {
       Node<K,V> e;
       if ((e = getNode(hash(key), key)) == null)
           return defaultValue;
       if (accessOrder)
           afterNodeAccess(e);
       return e.value;
   }

如果 accessOrder 为 true 则调用 afterNodeAccess 更新访问顺序,把访问的元素移动到双向链表表尾

    void afterNodeAccess(Node<K,V> e) { // move node to last
        LinkedHashMap.Entry<K,V> last;
        // 如果是访问顺序并且表尾的元素不等于 e
        if (accessOrder && (last = tail) != e) {
        	// 记录前置节点和后置节点
            LinkedHashMap.Entry<K,V> p =
                (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
            // 因为要把 p 放到双向链表的尾部所以后置节点为 null    
            p.after = null;
            // 如果 p 的前置节点为 null 说明 p 之前是头节点,将 head 指向 p 的后置接点 a
            if (b == null) 
                head = a;
            else
            	// 将 b 的后置节点指向 p 的后置节点 a 移除 p 的引用
                b.after = a;
            // 如果 p 的后置节点 a 不为空将 a 的前置节点指向 b    
            if (a != null)
                a.before = b;
            else
            	// p 的后置节点 a 为 null 说明 p 之前是尾节点
            	// 将 last 指向 p 的前置节点 a
                last = b;
            // 原本的尾节点为 null 说明之前双向链表还没有元素直接把 head 指向 p
            if (last == null)
                head = p;
            else {
            	// 把 p 跟之前最后一个节点连接起来 p 变成最后一个节点
                p.before = last;
                last.after = p;
            }
            // 把尾节点指向 p
            tail = p;
            ++modCount;
        }
    }

3.2 afterNodeInsertion

在 put 时如果是插入的新元素会调用 afterNodeInsertion 方法,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);
        }
    }

在 put 方法中 evict 为 true,如果 removeEldestEntry 返回 true 就会移除最老的元素,LinkedHashMap 中默认返回 false,子类可以重写此方法实现自己的逻辑

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

移除元素的时候同样在双向链表中把元素移除,就是把引用断开 p 不再引用别的节点,别的节点也不引用它

4. 遍历

HashMap 有三个遍历相关的方法

  • keySet 返回 key 的 set 集合,对应的迭代器是 LinkedKeyIterator
  • values 返回 value 集合,对应的迭代器是 LinkedValueIterator
  • entrySet 返回 LinkedHashMapEntry 的 set 集合,对应的迭代器是 LinkedEntryIterator

三个迭代器都继承自 LinkedHashIterator

    abstract class LinkedHashIterator {
        LinkedHashMapEntry<K,V> next;
        LinkedHashMapEntry<K,V> current;
        int expectedModCount;

        LinkedHashIterator() {
        	// 初始化时 next 指向双向链表的表头
            next = head;
            // 修改次数,遍历过程中有修改抛出异常
            expectedModCount = modCount;
            current = null;
        }

        public final boolean hasNext() {
        	// 下一个节点不为空
            return next != null;
        }

        final LinkedHashMapEntry<K,V> nextNode() {
            LinkedHashMapEntry<K,V> e = next;
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            if (e == null)
                throw new NoSuchElementException();
            // 记录当前节点用于删除    
            current = e;
			// 指向后置节点
            next = e.after;
            return e;
        }

        public final void remove() {
            Node<K,V> p = current;
            if (p == null)
                throw new IllegalStateException();
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            current = null;
            K key = p.key;
            // 调用 HashMap 的方法删除节点
            removeNode(hash(key), key, null, false, false);
            // 删除节点后会对 modCount 做加 1 对 expectedModCount 重新赋值
            expectedModCount = modCount;
        }
    }

二、LruCache

LRU 即 Least Recently Used 最近最少使用算法,命中之后移动到表尾超出最大容量后移除表头,LruCache 是 Android 中 LRU 算法实现的数据结构,使用 LinkedHashMap 实现,创建
LinkedHashMap 时 accessOrder 传入 true

1. put

    public final V put(@NonNull K key, @NonNull V value) {
        if (key == null || value == null) {
            throw new NullPointerException("key == null || value == null");
        }

        V previous;
        synchronized (this) {
            putCount++;
            // 记录新元素的大小
            size += safeSizeOf(key, value);
            previous = map.put(key, value);
            if (previous != null) {
            	// 如果存在老的元素减去老的元素的大小
                size -= safeSizeOf(key, previous);
            }
        }

        if (previous != null) {
            // 删除元素时的回调方法,这里是空实现
            entryRemoved(false, key, previous, value);
        }
		
		// 如果超出容量则删除最老的元素
        trimToSize(maxSize);
        return previous;
    }

    public void trimToSize(int maxSize) {
    	// 循环遍历元素
        while (true) {
            K key;
            V value;
            synchronized (this) {
                if (size < 0 || (map.isEmpty() && size != 0)) {
                    throw new IllegalStateException(getClass().getName()
                            + ".sizeOf() is reporting inconsistent results!");
                }
				
				// 如果当前元素的大小小于允许大小或者 map 为空跳出循环
                if (size <= maxSize || map.isEmpty()) {
                    break;
                }
				
                Map.Entry<K, V> toEvict = map.entrySet().iterator().next();
                key = toEvict.getKey();
                value = toEvict.getValue();
                // 移除表头元素
                map.remove(key);
                // 减去元素大小(可以是元素个数也可以是其他比如元素占用内存)
                size -= safeSizeOf(key, value);
                evictionCount++;
            }
			// 删除元素回调(空实现,子类可重写实现自己的逻辑)
            entryRemoved(true, key, value, null);
        }
    }

在 put 元素后会判断如果超出了最大允许的元素个数(或者是其他子类实现的逻辑,比如图片缓存时的内存占用)则移除最早的元素,这里没有通过重写 removeEldestEntry 实现我理解是因为重写 removeEldestEntry 只能满足元素个数的场景,比如最多允许 20 个元素满了移除最早的元素,如果是内存占用可能新加入的元素比较大需要移除多个元素 removeEldestEntry 就满足不了了。另外 Android SDK 28 中的 android.util.LruCache 实现是错的,它移除了表尾的元素,在 Android SDK 30 已经修复了,androidx.collection.LruCache 的实现是对的

2. get

    public final V get(@NonNull K key) {
        if (key == null) {
            throw new NullPointerException("key == null");
        }

        V mapValue;
        synchronized (this) {
       		// get 时使用 LinkedHashMap 维护顺序,之后会调用 afterNodeAccess 
            mapValue = map.get(key);
            if (mapValue != null) {
                hitCount++;
                return mapValue;
            }
            missCount++;
        }
    }

get 比较简单直接调用了 LinkedHashMap 的 get 方法让 LinkedHashMap 来维护顺序

3. remove

    public final V remove(@NonNull K key) {
        if (key == null) {
            throw new NullPointerException("key == null");
        }

        V previous;
        synchronized (this) {
            // 调用 LinkedHashMap 的 remove 方法
            previous = map.remove(key);
            if (previous != null) {
                size -= safeSizeOf(key, previous);
            }
        }

        if (previous != null) {
            entryRemoved(false, key, previous, null);
        }

        return previous;
    }

remove 与 get 一样都是直接调用了 LinkedHashMap 的方法

总结

在已经分析过 HashMap 的前提下 LinkedHashMap 总体来说比较简单,只是额外维护了一个双向链表来保证顺序,根据构造方法中传入的 accessOrder 的值确定是插入顺序还是访问顺序,在删除插入访问时在 HashMap 原有逻辑的基础上回调几个方法操作双向链表,把删除的元素的引用断开,把插入和访问(accessOrder 为 true 时)的元素移动到链表尾。LinkedHashMap 可以用来实现 LRU 算法的数据结构,Android 中的 LruCache 就使用它来实现

参考

面试必备:LinkedHashMap源码解析(JDK8)
图解LinkedHashMap原理
深度分析:那些阿里,腾讯面试官都喜欢问的LinkedHashMap源码
LinkedHashMap为何有序?详解源码及底层原理

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值