LinkedHashMap笔记

107 阅读4分钟

LinkedHashMap

类头

This implementation differs from HashMap in that it maintains a doubly-linked list running through all of its entries.
  • 在hashMap的基础上,针对数组的bin,额外添加了双向链表存储结构。

类声明

public class LinkedHashMap<K,V>
    extends HashMap<K,V>
    implements Map<K,V>
  • 重写方法:

    newNode()
    replacementNode() //hashMap中,树退化时使用的生产方法,配合使用的是LHM中的transferLinks
    newTreeNode()
    replacementTreeNode()
    internalWriteEntries()//IO序列化相关
    get()//以及相关方法  
    reinitialize()//super()+首尾=null
    
  • 实现父类中留口的三个特殊方法:

    void afterNodeAccess(Node<K,V> p) { }
    void afterNodeInsertion(boolean evict) { }
    void afterNodeRemoval(Node<K,V> p) { }
    

hashMap中对这三个方法的注释是:

Callbacks to allow LinkedHashMap post-actions

维护变量

  • 数据结构相关
transient LinkedHashMap.Entry<K,V> head;

transient LinkedHashMap.Entry<K,V> tail;

声明了双向链表的关键托管结构:首尾节点,符合上面说的

it maintains a doubly-linked list running through all of its entries

  • 遍历相关
/**
 * The iteration ordering method for this linked hash map: <tt>true</tt>
 * for access-order, <tt>false</tt> for insertion-order.
 *
 * @serial
 */
final boolean accessOrder;

这个变量的含义是(from blog.csdn.net/weixin_3691…

含义
true会把访问过的元素放在链表后面,放置顺序是访问的顺序
false按插入顺序来遍历

主存储结构

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

类方法

结构特化方法
linkNodeLast(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;
    }
}
  • 本身没什么特殊的,就是把节点挂到尾节点的后面。

    • 调用该方法的方法:

      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;
      }
      
      TreeNode<K,V> newTreeNode(int hash, K key, V value, Node<K,V> next) {
          TreeNode<K,V> p = new TreeNode<K,V>(hash, key, value, next);
          linkNodeLast(p);
          return p;
      }	
      
    • 可以看到LinkedHashMap其实本质上,并不是用链表取代了HashMap中的数组,而是额外维护了一个双向链表来存储数据。

transferLinks
// apply src's links to dst
private void transferLinks(LinkedHashMap.Entry<K,V> src,
                           LinkedHashMap.Entry<K,V> dst) {
    LinkedHashMap.Entry<K,V> b = dst.before = src.before;
    LinkedHashMap.Entry<K,V> a = dst.after = src.after;
    if (b == null)
        head = dst;
    else
        b.after = dst;
    if (a == null)
        tail = dst;
    else
        a.before = dst;
}
  • 把src的节点,都赋给dst
    • 调用该方法的方法
Node<K,V> replacementNode(Node<K,V> p, Node<K,V> next) {
    LinkedHashMap.Entry<K,V> q = (LinkedHashMap.Entry<K,V>)p;
    LinkedHashMap.Entry<K,V> t =
        new LinkedHashMap.Entry<K,V>(q.hash, q.key, q.value, next);
    transferLinks(q, t);
    return t;
}
TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {
    LinkedHashMap.Entry<K,V> q = (LinkedHashMap.Entry<K,V>)p;
    TreeNode<K,V> t = new TreeNode<K,V>(q.hash, q.key, q.value, next);
    transferLinks(q, t);
    return t;
}
  • 一个是树化节点时调用的,一个是非树化节点调用的,只是前后指针的调换。
HashMap留口方法
void afterNodeAccess(Node<K,V> p) { }
void afterNodeInsertion(boolean evict) { }
void afterNodeRemoval(Node<K,V> p) { }
afterNodeAccess
  • 节点访问后的操作
    • 如果accessOrder为true,那么就把访问到的节点放到最后
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;
    }
}
afterNodeInsertion
  • 节点插入后的操作

    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);
        }
    }
    
        protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
            return false;
        }
    
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;
}
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;
 }

 /**
  * {@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的用处之一了:查询节点的控制。

  • 但是get方法并没有进行完整的重写,可能是hash值+节点数据结构的形式,查询起来直观上就能感觉到,比链表+节点数据结构要快的原因吧。
public boolean containsValue(Object value) {
    for (LinkedHashMap.Entry<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;
}
  • containsValue方法就进行了完全的重写。

还有子类的方法以及1.8中Map一些方法的重写,和HashMap中的实现大同小异,只有几个关键的点:

  • foreach方法会使用头节点来进行遍历。
  • 遍历器方法,也会使用头节点,进行next的查询遍历。

总结

其实可以看出来,LinkedHashMap并不是一个很完备的集合接口:

  • 维护了头尾节点但并不是主要的数据结构,主要的功能结构仍然是HashMap中的table数组。
    • 头尾节点的使用:取值和foreach方法
  • 有一个很有趣的问题:

linkedHashMap是否是有序的?

从某种程度上来说,确实是的。但是这个有序性有几个分歧的地方:

  • accessOrder是否为true?如果是的话,并不能保证整体有序性(因为查询会导致节点后置)
  • 是否重写了removeEldestEntry?如果该方法可能返回值,那么插入可能伴随着旧节点的删除。

LinkedHashMap中更多的是在其他中间件中继承并重写,利用维护的双向链表的性质,进行二次开发。

  • 例子:通过重写removeEldestEntry,可以很容易的构造一个LRUcache。