JDK 12 LinkedHashMap 源码分析

LinkedHashMap 是什么,能做什么,这里就不再展开讲了。这篇博客 有相关介绍,并展示了 LinkedHashMap 的核心原理,但是我发现我的 jdk 里的源代码和博主提供的源代码示例不一致,我的是 "12.0.1" 2019-04-16,所以就写了这篇文章,看看新版本的有哪些调整,以及为什么有这些调整。

1. 类注释

在类注释中,总结一下大致有以下几个要点:

  • 与 HashMap 不同,LinkedHashMap 维护了一个双向链表来定义迭代顺序
  • re-insert 一个已有的元素不会改变迭代顺序
  • 迭代顺序默认按插入顺序,但是也可以初始化为按访问顺序,这样就很适合用来实现 LRU cache
  • 在 LinkedHashMap 上迭代的时间复杂度是 O(size),而在 HashMap 上迭代是 O(capacity). 其他操作基本上都是 O(1),但因为维护双向链表的原因,性能上稍微逊于 HashMap
  • LinkedHashMap 的性能受 initial capacityload factor 的影响,这两个参数是从 HashMap 继承下来的,所以 HashMap 也是;但是因为迭代策略的不同,initial capacity 的值很大,不会直接影响迭代性能
  • 线程不安全。因为继承自 HashMap,许多性质也一样
  • add/delete 会影响到迭代顺序;插入顺序下,改变 pair 的 value 不会影响;访问顺序下,get 操作也会影响到迭代顺序。这个根据定义很好理解
  • 迭代过程中如果被其他线程修改,会以 fail-fast 的策略尽快抛出 ConcurrentModificationException,需要注意的是,这个也只是 best-effort 的行为,并不能保证在冲突的第一时间就抛出异常,所以捕获异常后的 map 是不确定的
  • 应该是 JDK8 新增的并行部分,暂时没看 The spliterators returned by the spliterator method of the collections returned by all of this class's collection view methods are <a href="Spliterator.html#binding">late-binding</a></em>,<em>fail-fast</em>, and additionally report {@link Spliterator#ORDERED}.

2. 继承结构

LinkedHashMap 继承自 HashMap,重用了部分属性,重写了部分方法;自己额外定义的主要包括一些内部类、构造函数等。


LinkedHashMap 的结构

3. 从一个 demo 开始

public class Test {

    public static void main(String[] args) {
        LinkedHashMap<String, String> map = new LinkedHashMap<>();
        map.put("111", "111");
        map.put("333", "333");
        map.put("222", "222");
        System.out.println(map);

        HashMap<String, String> hashMap = new HashMap<>();
        hashMap.put("111", "111");
        hashMap.put("333", "333");
        hashMap.put("222", "222");
        System.out.println(hashMap);
    }
}

// output
{111=111, 333=333, 222=222}
{111=111, 222=222, 333=333}

println 方法对 object 类型的参数,会调用 object 的 toString() 方法;map 系列的这个方法是定义在 AbstractMap 里面的,拿到 entrySet 的 iterator,再通过 iterator 的 next 方法来迭代。

// AbstractMap.java
public String toString() {
        Iterator<Entry<K,V>> i = entrySet().iterator();
        //...
        for (;;) {
            Entry<K,V> e = i.next();
        //...
        }
}

// AbstractMap.java
public abstract Set<Entry<K,V>> entrySet();

而 LinkedHashMap 和 HashMap 的 entrySet() 方法也不相同,

// LinkedHashMap.java
public Set<Map.Entry<K,V>> entrySet() {
        Set<Map.Entry<K,V>> es;
        return (es = entrySet) == null ? (entrySet = new LinkedEntrySet()) : es;
}

// HashMap.java
public Set<Map.Entry<K,V>> entrySet() {
        Set<Map.Entry<K,V>> es;
        return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
}

所以主要是 LinkedEntrySet 和 EntrySet 导致的区别,下面列出了有区别的方法,可以发现实际上又是 Iterator 导致的区别

  final class EntrySet extends AbstractSet<Map.Entry<K,V>> {
        public final Iterator<Map.Entry<K,V>> iterator() {
            return new EntryIterator();
        }
        public final Spliterator<Map.Entry<K,V>> spliterator() {
            return new EntrySpliterator<>(HashMap.this, 0, -1, 0, 0);
        }
        public final void forEach(Consumer<? super Map.Entry<K,V>> action) {
            Node<K,V>[] tab;
            if (action == null)
                throw new NullPointerException();
            if (size > 0 && (tab = table) != null) {
                int mc = modCount;
                for (Node<K,V> e : tab) {
                    for (; e != null; e = e.next)
                        action.accept(e);
                }
                if (modCount != mc)
                    throw new ConcurrentModificationException();
            }
        }
  }

  final class LinkedEntrySet extends AbstractSet<Map.Entry<K,V>> {
        public final Iterator<Map.Entry<K,V>> iterator() {
            return new LinkedEntryIterator();
        }
        public final Spliterator<Map.Entry<K,V>> spliterator() {
            return Spliterators.spliterator(this, Spliterator.SIZED |
                                            Spliterator.ORDERED |
                                            Spliterator.DISTINCT);
        }
        public final void forEach(Consumer<? super Map.Entry<K,V>> action) {
            if (action == null)
                throw new NullPointerException();
            int mc = modCount;
            for (LinkedHashMap.Entry<K,V> e = head; e != null; e = e.after)
                action.accept(e);
            if (modCount != mc)
                throw new ConcurrentModificationException();
        }
    }

两者的 iterator 的 next()方法都是调用了

public final Map.Entry<K,V> next() { return nextNode(); }

首先注意,nextNode() 返回的是初始化/上次计算 的 next 值,并计算出下一个值。nextNode 在 LinkedHashMap 和 HashMap 中有者不同的实现,

先来看看 HashIterator
next 由 HashIterator 构造函数初始化,并在 nextNode 方法中更新。table 是 hash bucket,一个数组。
首先是一个 fast-fail 地检测是否被并发 add/delete 了,(这个机制请参考其他博客,这里不再赘述),然后把指针在 table 上后移,如果 next 不为空则直接返回;如果为空,则要跳过一个槽看下一个,循环;

所以,HashMap 的迭代复杂度是 O(capacity),因为它需要检查 table 上的每一个元素

  abstract class HashIterator {
        Node<K,V> next;        // next entry to return
        Node<K,V> current;     // current entry
        int expectedModCount;  // for fast-fail
        int index;             // current slot
        
        HashIterator() {
            expectedModCount = modCount;
            Node<K,V>[] t = table;
            current = next = null;
            index = 0;
            if (t != null && size > 0) { // advance to first entry
                do {} while (index < t.length && (next = t[index++]) == null);
            }
        }

        final Node<K,V> nextNode() {
            Node<K,V>[] t;
            Node<K,V> e = next;
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            if (e == null)
                throw new NoSuchElementException();
            if ((next = (current = e).next) == null && (t = table) != null) {
                do {} while (index < t.length && (next = t[index++]) == null);
            }
            return e;
        }
    }

再来看看 LinkedHashIterator
一样的 fail-fast check,但是神奇的地方出现了,next = e.after,完事儿,完全不跟你多bb。可以肯定,这个 after 指向的肯定是按 insert order/access order 的下一个元素。那么这个 after 又是哪里冒出来的呢?

  abstract class LinkedHashIterator {
        final LinkedHashMap.Entry<K,V> nextNode() {
            LinkedHashMap.Entry<K,V> e = next;
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            if (e == null)
                throw new NoSuchElementException();
            current = e;
            next = e.after;
            return e;
        }
    }

所以,问题的核心还是回到了 LinkedHashMap.Entry 上。

在 Collections 框架里,Entry 应该是接口,用于定义键值对;实体类应该是 XXXNode 才对。对于这一点,源代码中的注释也给出了说明:HashMap now uses trees for some of its nodes, class LinkedHashMap.Entry is now treated as intermediary node class that can also be converted to tree form. The name of this class, LinkedHashMap.Entry, is confusing in several ways in its current context, but cannot be changed. but cannot be changed Otherwise, even though it is not exported outside this package, some existing source code is known to have relied on a symbol resolution corner case rule in calls to removeEldestEntry that suppressed compilation errors due to ambiguous usages. So, we keep the name to preserve unmodified compilability.
也就是说,一开始没有考虑到规范性的问题,而 HashMap 又用了 LinkedHashMap.Entry 来实现 TreeNode;即使这个静态内部类没有暴露出去,但是有的程序,是通过名字来解析这个类的,如果改了名字会导致编译都过不了,所以为了兼容就不改了。

HashMap 中定义了 Node,LinkedHashMap.Entry 继承自 Node。多了两个属性变量,before 和 after。根据名字我们可以猜到,这是一个双向链表的元素。源代码如下,但是初始化一个 Entry 的时候并没有设置 before 和 after 信息,那么双向链表的维护必定是在 Map 的操作过程中。

  /**
     * Basic hash bin node, used for most entries.  (See below for
     * TreeNode subclass, and in LinkedHashMap for its Entry subclass.)
     */
  static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;

        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }   
  }

  /**
     * HashMap.Node subclass for normal LinkedHashMap entries.
     */
  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 的大致结构和原理。下面我们来具体看看这个双向链表是怎么维护的。
回到我们一开始的程序

public class Test {

    public static void main(String[] args) {
        LinkedHashMap<String, String> map = new LinkedHashMap<>();
        map.put("111", "111");
        map.put("333", "333");
        map.put("222", "222");
        System.out.println(map);
    }
}

// output
{111=111, 333=333, 222=222}

检查 LinkedHashMap 的构造函数,accessOrder 被设置为 false.

public LinkedHashMap() {
        super();
        accessOrder = false;
}

public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

/**
 * 标识 LinkedHashMap 的迭代顺序: {@code true}
 * for access-order, {@code false} for insertion-order.
 */
final boolean accessOrder;

再看 put 方法,直接就进入了 HashMap 里面。奇怪了对吧?没有重写 put 方法。那是在哪里设置的 before/after 呢?
PS:putVal 方法比较复杂,是个核心算法,可以研究下。

  public V put(K key, V value) {
      return putVal(hash(key), key, value, false, true);
  }

    /**
     * Implements Map.put and related methods.
     *
     * @param hash hash for key
     * @param key the key
     * @param value the value to put
     * @param onlyIfAbsent if true, don't change existing value
     * @param evict if false, the table is in creation mode.
     * @return previous value, or null if none
     */
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

HashMap 的 putVal 方法调用了 newNode(),afterNodeAccess() 等。在 HashMap 的源代码中可以看见如下注释:

    /* ------------------------------------------------------------ */
    // LinkedHashMap support


    /*
     * The following package-protected methods are designed to be
     * overridden by LinkedHashMap, but not by any other subclass.
     * Nearly all other internal methods are also package-protected
     * but are declared final, so can be used by LinkedHashMap, view
     * classes, and HashSet.
     */

    // Create a regular (non-tree) node
    Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
        return new Node<>(hash, key, value, next);
    }
    ...

而在 LinkedHashMap 中,这些方法都被重写了


LinkedHashMap 重写父类中的方法

以 newNode 为例,初始化一个 entry 后,调用 linkNodeLast 来维护 before/after 指针。到这里,我们终于知道为什么 LinkedHashMap 有顺序了。LinkedHashMap 也需要在其他方法里补上对 before/after 的操作,这里不再逐一分析。

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

// internal utilities

// link at the end of list
  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;
        }
  }

还有一件事,那就是如何通过 accessOrder 来区分 insert order/access order的。默认是 insert order,不需要做额外的操作;而 access order,则需要在每次访问 entry 后,调整 entry 的位置。HashMap 的设计者暴露出了一个 afterNodeAccess 回调,以 LinkedHashMap#get(K) 方法为例,如果是 access order,会执行 afterNodeAccess(e)

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

在 LinkedHashMap.afterNodeAccess 中,会判断是否是 accessOrder,是的话把这个 entry 放到双向链表的最后。至于为什么是最后,正常人应该是如 LRU cache 一样放到最前,别问,问就是最后。

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

所以,LinkedHashMap 还提供了一个 removeEldestEntry 回调,默认为 false,子类可以重写来实现当是否删除最 eldest 的 entry。这个回调会在 put/putAll 方法时触发,何为 eldest 呢?对于 insert order,eldest 就是 put/putAll 加入的(最后一个)元素;对于 access order,eldest 就是 head 指针指向的元素(对应前面的移到最后)。

  /**
     * @param    eldest The least recently inserted entry in the map, or if
     *           this is an access-ordered map, the least recently accessed
     *           entry.  This is the entry that will be removed it this
     *           method returns {@code true}.  If the map was empty prior
     *           to the {@code put} or {@code putAll} invocation resulting
     *           in this invocation, this will be the entry that was just
     *           inserted; in other words, if the map contains a single
     *           entry, the eldest entry is also the newest.
     * @return   {@code true} if the eldest entry should be removed
     *           from the map; {@code false} if it should be retained.
     */
    protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
        return false;
    }

这种回调设计还有好几个,下面是一些定义在 HashMap 中的回调

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

下面再看一下 access order 的验证 demo,因为 "333" 被 "最近访问" 了,所以他被移到了链表的最后。

    public static void main(String[] args) {
        LinkedHashMap<String, String> map = new LinkedHashMap<>(16, 0.75f, true);
        map.put("111", "111");
        map.put("333", "333");
        map.put("222", "222");
        System.out.println(map);

        map.get("333");
        System.out.println(map);
    }

// output
{111=111, 333=333, 222=222}
{111=111, 222=222, 333=333}

4. 如何实现 LRU cache

这是 leetcode 上的一个实现,思路很明显了

class LRUCache extends LinkedHashMap<Integer, Integer> {
    private int capacity;
    public LRUCache(int capacity) {
        super(capacity, 0.8F, true);
        this.capacity = capacity;
    }
    
    public int get(int key) {
        return super.getOrDefault(key, -1);
    }
    
    public void put(int key, int value) {
        super.put(key, value);
    }
    
    @Override
    public boolean removeEldestEntry(Map.Entry<Integer, Integer> entry) {
        return size() > capacity;
    }
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 148,827评论 1 317
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 63,511评论 1 266
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 99,318评论 0 218
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,108评论 0 189
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,112评论 1 266
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,387评论 1 185
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 30,905评论 2 283
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,657评论 0 177
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,144评论 0 223
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,744评论 2 225
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,100评论 1 236
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,565评论 2 222
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,041评论 3 216
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,769评论 0 9
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,301评论 0 178
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,173评论 2 239
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,313评论 2 242

推荐阅读更多精彩内容