LinkedHashMap和LinkedHashSet源码解析

参考:
Java编程的逻辑
https://blog.csdn.net/blingfeng/article/details/79974169

LinkedHashMap(JDK1.8)

public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>

是HashMap的子类,但可以保持元素按插入或访问有序,这与TreeMap按键排序不同

内部多了一个双向链表维护键值对的顺序,每个键值对既位于哈希表中,也位于这个双向链表中。

1 应用场景

  • 保持插入顺序

比如一个配置文件,其中有一些键值对形式的配置项,但其中有一些键是重复的,希望保留最后一个值,但还是按原来的键顺序输出,LinkedHashMap就是一个合适的数据结构。

比如希望的数据模型可能就是一个Map,但希望保持添加的顺序;
比如一个购物车,键为购买项目,值为购买数量,按用户添加的顺序保存。

希望Map能够按键有序,但在添加到Map前,键已经通过其他方式排好序了,这时,就没有必要使用TreeMap了,毕竟TreeMap的开销要大一些。
比如,在从数据库查询数据放到内存时,可以使用SQL的order by语句让数据库对数据排序。

  • 保持访问有序

LRU缓存

2 实现原理

2.1 内部组成

transient LinkedHashMap.Entry<K,V> head;//双向链表的头
transient LinkedHashMap.Entry<K,V> tail;//双向链表的尾
final boolean accessOrder;//表示是按访问顺序还是插入顺序
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);
    }
}

2.2 构造方法

//accessOrder默认为false,即按照插入顺序来连接,true则为按照访问顺序来连接
public LinkedHashMap(int initialCapacity, float loadFactor) {
     
    super(initialCapacity, loadFactor);
    accessOrder = false;
}

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

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

public LinkedHashMap(Map<? extends K, ? extends V> m) {
     
    super();
    accessOrder = false;
    putMapEntries(m, false);
}

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

2.3 put方法

通过重写HashMap的put方法里的钩子函数来实现:当插入元素时节点会插入双向链表的链尾,如果key重复,则也会将节点移动至链尾

重写了newNode:体现了Entry要扩展Node的原因

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

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

重写了afterNodeAccess:

//已经存在此节点,则移动至链尾
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;
        //如果p前面没有元素,则p在修改位置之前为头结点,则直接让a成为头结点;
        //否则b的尾指针指向a
        if (b == null)
            head = a;
        else
            b.after = a;
         //如果a不为空,则a的头指针指向b;
        //否则,说明p在修改位置之前为尾指针,令b成为尾指针
        if (a != null)
            a.before = b;
        else
            last = b;
        //如果双向链表中只有p一个节点,则令p即为头结点,也为尾节点;否则,将p插入链尾
        if (last == null)
            head = p;
        else {
     
            p.before = last;
            last.after = p;
        }
        //设置p为链尾
        tail = p;
        ++modCount;
    }
}
/**
*      p将引用移除
*              b          |          p            |          a
*       -------------     |     -------------     |     -------------
*      |before| after| <==|==> |before| after| <==|==> |before| after|
*       -------------     |     -------------     |     -------------
*     
*     1.b为NULL时,则a变为头结点;否则b的after指向a
*                           head
*                             a                          p
*       (b)             -------------                ------------- 
*      NULL    <------ |before| after|  ......      |before| after| (p最后将插入链尾)
*                       -------------                -------------              
*     2.a为NULL时,则b变为链尾节点,after经过1会指向null;否则a的before指向b
*
*             tail
*              b                                           p
*       -------------             (a)                ------------- 
*      |before| after|  ------->  NULL   ......     |before| after| (p最后将插入链尾)
*       -------------                                -------------  
*     3.a,b都为NULL时,p即为头结点,又为尾节点
*     因为p前后都没有元素,则双向链表中只有p一个节点
*
*     4.最后,将p设置为tail
*                                (head)                   tail
*              b                                           p
*       -------------             (a)                ------------- 
*      |before| after|  -----> null或非null   .......|before| after| (p最后将插入链尾)
*       -------------                                -------------  
*       
*/

此外HashMap的putVal()方法,还调用了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);
    }
} 

2.4 get方法

通过重写HashMap的get方法来实现:当用get()方法获取value时会将节点移动至链尾

public V get(Object key) {
     
    Node<K,V> e;
    if ((e = getNode(hash(key), key)) == null)
        return null;
    // accessOder为true时,被访问的节点被置于双向链表尾部
    if (accessOrder)
        afterNodeAccess(e);
    return e.value;
}

2.5 remove方法

通过重写HashMap的HashMap方法里的钩子函数来实现

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

LinkedHashSet

之前介绍的Map接口的实现类都有一个对应的Set接口的实现类,比如HashMap有HashSet,TreeMap有TreeSet,LinkedHashMap也不例外,它也有一个对应的Set接口的实现类LinkedHashSet。

LinkedHashSet是HashSet的子类,但它内部的Map的实现类是LinkedHashMap,所以它也可以保持插入顺序

你可能感兴趣的:(Java基础,LinkedHashMap)