Java集合之LinkedHashMap

273 阅读5分钟

一、概述

LinkedHashMap继承HashMap,实现Map接口

因而该类中很多方法都是继承HashMap中的方法

LinkedHashMap类的底层结构是基于HashMap + LinkedList,该类通过双向链表实现保存插入key-value的顺序。


该实现类的例子如下:

LinkedHashMap<String, Integer> lmap = new LinkedHashMap<String, Integer>();
lmap.put("语文", 1);
lmap.put("数学", 2);
lmap.put("英语", 3);
lmap.put("历史", 4);
lmap.put("政治", 5);
lmap.put("地理", 6);
lmap.put("生物", 7);
lmap.put("化学", 8);
for(Entry<String, Integer> entry : lmap.entrySet()) {
    System.out.println(entry.getKey() + ": " + entry.getValue());
}


其内部结构示意图如下:



二、在该实现类中重要的三个方法

在HashMap中提高了下面三种方法的定义:

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

而在LinkedHashMap中,重写了这三种方法,在这之前先看看LinkedHashMap中的get()方法

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

    final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            if ((e = first.next) != null) {
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }

从上述方法可以看出get()方法的时间复杂度O(n)(n为hash值相同的个数)

这三种方法的作用是:afterNodeAccess() 节点访问后、afterNodeInsertion() 节点插入后、afterNodeRemove() 节点移除后

  • afterNodeAccess()方法

    void afterNodeAccess(Node<K,V> e) { // move node to last
        LinkedHashMap.Entry<K,V> last;

        //若定义accessOrder,那么保证最近访问节点放在最后
        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;
        }
    }

也就是在进行put方法之后对节点的访问,此时更新链表,把最近访问的放在最后。

afterNodeAccess()方法的时间复杂度O(1),因为该方法对当前访问节点e的前驱节点和后继节点连接,而后将当前节点放在队尾tail即可

该方法可以用于实现LRU(最近最少使用)算法


  • afterNodeInsertion()方法

该算法的的对应的时间复杂度O(n)O(log(n))。根据该hash值下的链表或红黑树中元素的个数n。

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

    //该方法在HashMap类中
    final Node<K,V> removeNode(int hash, Object key, Object value,
                               boolean matchValue, boolean movable) {
        Node<K,V>[] tab; Node<K,V> p; int n, index;
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (p = tab[index = (n - 1) & hash]) != null) {
            Node<K,V> node = null, e; K k; V v;

            //移除链表中对应hash的元素,该hash位置还有该元素,无没有单链表或树
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                node = p;
            else if ((e = p.next) != null) {
                if (p instanceof TreeNode)
                    node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
                else {
                    //移除该hash值下的单链表中的对应的元素
                    do {
                        if (e.hash == hash &&
                            ((k = e.key) == key ||
                             (key != null && key.equals(k)))) {
                            node = e;
                            break;
                        }
                        p = e;
                    } while ((e = e.next) != null);
                }
            }

            //从该hash值下的红黑树中移除对应的元素。
            if (node != null && (!matchValue || (v = node.value) == value ||
                                 (value != null && value.equals(v)))) {
                if (node instanceof TreeNode)
                    ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
                else if (node == p)
                    tab[index] = node.next;
                else
                    p.next = node.next;
                ++modCount;
                --size;
                afterNodeRemoval(node);
                return node;
            }
        }
        return null;
    }

该方法中的removeEldestEntry()在源码中有有这样一段话:

Returns true
 if this map should remove its eldest entry. This method is invoked by put and putAll
 after inserting a new entry into the map.  It provides the implementor with the opportunity
 to remove the eldest entry each time a new one is added.  This is useful if the map represents
 a cache: it allows the map to reduce memory consumption by deleting stale entries. 

就是用于移除最早的插入的元素,当有新元素插入时


  • afterNodeRemoval()方法

    void afterNodeRemoval(Node<K,V> e) { // unlink
        //从链表中移除节点p,并将其前驱索引before和后继索引after置位null
        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;
    }

该方法是在移除节点后调用的,用于将节点从双向链表中移除。


上述三种方法,基本是为了保证双向链表中节点的次序和双向链表容量所做的一些额外的事情,目的就是保证双向链表中节点的顺序从eldest到yongest。


三、put()和get()方法

LinkedHashMap没有重写put()方法,只是实现了上述提到的afterNodeAccess()afterNdoeInsertion()两个回调方法。

get()方法则是重写,并加入了afterNodeAccess()来保证访问顺序,详细源码如下:

    /**
     * Returns the value to which the specified key is mapped,
     * or {@code null} if this map contains no mapping for the key.
     *
     * <p>More formally, if this map contains a mapping from a key
     * {@code k} to a value {@code v} such that {@code (key==null ? k==null :
     * key.equals(k))}, then this method returns {@code v}; otherwise
     * it returns {@code null}.  (There can be at most one such mapping.)
     *
     * <p>A return value of {@code null} does not <i>necessarily</i>
     * indicate that the map contains no mapping for the key; it's also
     * possible that the map explicitly maps the key to {@code null}.
     * The {@link #containsKey containsKey} operation may be used to
     * distinguish these two cases.
     */
    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;
    }

注意:在accessOrder模式下,只要执行get()或put()操作的时候,就会产生structural modificaiton。官方文档是这样描述的:

A structural modification is any operation that adds or deletes one or more mappings or, 
in the case of access-ordered linked hash maps, affects iteration order. In 
insertion-ordered linked hash maps, merely changing the value associated with a key that 
is already contained in the map is not a structural modification. 
In access-ordered linked hash maps, merely querying the map with get is a structural modification.


总之,记住LinkedHashMap是继承HashMap类,只是重写了部分方法,以便维护按插入顺序访问元素的双向链表的功能。

这是该实现类的重点。


补充:

在Set接口中,很多类,如HashSet、LinkedHashSet、TreeSet底层都是使用Map接口中的HashMap、LinkedHashMap、TreeMap来实现存储。


注:本文中的图片来源于github.com/LRH1993/and…

作者认为侵权,告知后立马删