JDK源码解析---LinkedHashMap

1. 概述

LinkedHashMap 是 HashMap 的子类,增加了顺序访问的特性。
【默认】当 accessOrder = false 时,按照 key-value 的插入顺序进行访问。插入的节点连接在链表的末尾
当 accessOrder = true 时,按照 key-value 的读取顺序进行访问。调用get或者getOrDefault方法会将得到的节点 移动到链表的末尾。
LinkedHashMap 的顺序特性,通过内部的双向链表实现,所以我们把它看成是 LinkedList + LinkedHashMap 的组合。
LinkedHashMap 通过重写 HashMap 提供的回调方法,从而实现其对顺序的特性的处理。

LinkedHashMap 可以方便实现 LRU 算法的缓存。

2. 类图

在这里插入图片描述

  • 实现了Map借口、序列化接口、克隆接口
  • 继承了HashMap类,所以是对HashMap功能上的扩展。

3. 属性

//头结点,表示最先加入的结点
transient LinkedHashMap.Entry<K,V> head;
//尾结点,表示最后加入的结点
transient LinkedHashMap.Entry<K,V> tail;
//是否按照访问的顺序
//true:按照 key-value 的访问顺序进行访问。
//false:按照 key-value 的插入顺序进行访问。 插入顺序就是插入是什么顺序,读取就是按照什么样的顺序
final boolean accessOrder;

可以看到LinkedHashMap使用了新的结点定义Entry
Entry是继承了HashMap的Node。

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

由于每个结点都指向了它的前驱和后继,这就构成了双向链表。访问的顺序是从head到tail
根据accessOrder的值,可以确定访问的顺序。

  • 当为true时,Entry节点被访问的时候,则放置到链表的尾部,被tail确定。
  • 当为false时,Entry节点被添加的时候,则放置到链表的尾部,被tail确定。

4. 构造方法

  • LinkedHashMap()无参构造函数,调用父类的无参构造函数,额外在设置了访问标示位,默认为false。即使用插入顺序访问。
    public LinkedHashMap() {
        super();
        accessOrder = false;
    }
  • LinkedHashMap(int initialCapacity)初始化容量的构造函数
    public LinkedHashMap(int initialCapacity) {
        super(initialCapacity);
        accessOrder = false;
    }
  • LinkedHashMap(int initialCapacity, float loadFactor)初始化容量和装载因子的构造函数
    public LinkedHashMap(int initialCapacity, float loadFactor) {
        super(initialCapacity, loadFactor);
        accessOrder = false;
    }
  • LinkedHashMap(Map<? extends K, ? extends V> m)给定集合,将集合添加到初始化的LinkedHashMap中
    public LinkedHashMap(Map<? extends K, ? extends V> m) {
        super();
        accessOrder = false;
        putMapEntries(m, false);
    }

以上4种和父类HashMap基本一致,仅多了设置访问标示位,默认为false。

  • LinkedHashMap(int initialCapacity,float loadFactor,boolean accessOrder)该构造函数允许自定义accessOrder
    public LinkedHashMap(int initialCapacity,
                         float loadFactor,
                         boolean accessOrder) {
        super(initialCapacity, loadFactor);
        this.accessOrder = accessOrder;
    }

5. 创建结点

在插入 key-value 键值对时,例如说 #put(K key, V value) 方法,如果不存在对应的节点,则会调用 #newNode(int hash, K key, V value, Node<K,V> e) 方法,创建节点。

因为 LinkedHashMap 自定义了 Entry 节点,所以必然需要重写该方法。代码如下:

    Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {      
        // 创建Entry结点
        LinkedHashMap.Entry<K,V> p =
            new LinkedHashMap.Entry<K,V>(hash, key, value, e);
        // 添加到结尾
        linkNodeLast(p);
        return p;
    }

虽然方法参数重这里有Node<K,V> e指向该结点的下一个结点,但调用put方法的时候,传入的e等于null。
插入的时候连接到尾部。代码如下:

    private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
        // 获取原先尾部的结点
        LinkedHashMap.Entry<K,V> last = tail;
        // 更新尾部的结点为结点p
        tail = p;
        // 当原先尾部的结点为null,说明原先head也为空,则将head指向p
        if (last == null)
            head = p;
        else {
        // 原先不为空,则p的前驱指向原来的尾部的结点。
        // 原先尾部的结点指向p
            p.before = last;
            last.after = p;
        }
    }

所以新插入的结点都是连接到尾部

6. 节点操作回调

在 HashMap 的读取、添加、删除时,分别提供了 #afterNodeAccess(Node<K,V> e)、#afterNodeInsertion(boolean evict)、#afterNodeRemoval(Node<K,V> e) 回调方法。这样,LinkedHashMap 可以通过它们实现自定义拓展逻辑。

6.1 afterNodeAccess

当accessOrder为true的时候,每当某个entry被访问,则要将它放置到链表的尾部,并用tail指向它。代码如下:

    void afterNodeAccess(Node<K, V> e) { // move node to last
        LinkedHashMap.Entry<K, V> last;
        // 当accessOrder 为true 并且 e 不是尾部结点
        if (accessOrder && (last = tail) != e) {
            // p指向e,b指向e的前驱,a指向e的后继
            LinkedHashMap.Entry<K, V> p =
                    (LinkedHashMap.Entry<K, V>) e, b = p.before, a = p.after;
            p.after = null;// e的后继指针断开
            if (b == null)//如果e的前驱为null,则说明原来head指向e,此时e要挪到尾部,所以head要指向e的后继
                head = a;
            else//若e的前驱不为空,则e的前驱的后继指向e的后继
                b.after = a;
            if (a != null)//若e的后继不为空,则e的后继的前驱指向e的前驱
                a.before = b;
            else// TODO 此处有疑问。若e的后继为空,那么e不就是尾部结点了吗。
                last = b;
            if (last == null)//说明head也为空,则将head指向e
                head = p;
            else {// e的前驱指向尾部结点,尾部结点的后继指向e
                p.before = last;
                last.after = p;
            }
            tail = p;//更新 尾指针
            ++modCount;//修改次数+1
        }
    }

主要就两步 1.将e从链表中移除 2.e连接到链表尾部。
HashMap中提供了两个获取value的方法,LinkListHashMap对其进行了重写。
代码如下:

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

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

6.2 afterNodeInsertion

代码如下:

// evict 表示是否允许移除元素
void afterNodeInsertion(boolean evict) { // possibly remove eldest
    LinkedHashMap.Entry<K,V> first;
    // first = head 记录当前头节点。因为移除从头开始,最老
    if (evict && (first = head) != null && removeEldestEntry(first)) {
        // <2> 移除指定节点
        K key = first.key;
        removeNode(hash(key), key, null, false, true);
    }
}

protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
    return false;//默认是false
}

这里的方法是用于LRU 算法实现。LRUCache<K, V>继承了LinkedHashMap<K, V>
当map的size()大于缓存的大小,则移除最老的结点。重写了removeEldestEntry方法。

    protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
        // 当 map 中的数据量大于指定的缓存个数的时候,就自动删除最老的数据。
        return size() > CACHE_SIZE;
    }

此时在调用加入缓存中的方法,就会判断当前的大小是否大于缓存最大容量,然后删去最老的结点。

6.3 afterNodeRemoval

当节点删除后,要将节点从链表中删去。所以重写了afterNodeRemoval来实现。代码如下:

    void afterNodeRemoval(Node<K,V> e) { // unlink
    // p指向节点e b指向e的前驱 a指向e的后继
        LinkedHashMap.Entry<K, V> p =
                (LinkedHashMap.Entry<K, V>) e, b = p.before, a = p.after;
        // 将e的前驱和后继置为空
        p.before = p.after = null;
        // 若e的前驱为空,说明e是原先的头节点,此时将head指向e的后继
        if (b == null)
            head = a;
        else// e的前驱的后继指向e的后继
            b.after = a;
        if (a == null)// 若e的后继为空,则说明e是尾部节点,将尾指针指向e的前驱
            tail = b;
        else// e的后继的前驱指向e的前驱
            a.before = b;
}

7. 替换节点

用一个节点替换原来LinkListHashMap中的节点,代码如下:

    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);//调用transferLinks实现替换
        return t;
    }
    
    private void transferLinks(LinkedHashMap.Entry<K, V> src,
                               LinkedHashMap.Entry<K, V> dst) {
        LinkedHashMap.Entry<K, V> b = dst.before = src.before;//b指向src的前驱
        LinkedHashMap.Entry<K, V> a = dst.after = src.after;
        //a指向src的后继
        if (b == null)//若src的前驱为空,则说明src本来是链表的头结点。此时头指针替换为dst
            head = dst;
        else// src的前驱不为空,则将src的前驱的后继指向dst
            b.after = dst;
        if (a == null)// src的后继为空,则说明src本来是链表的尾结点,此时将尾指针指向dst
            tail = dst;
        else// src的后继不为空,则将src的后继的前驱指向dst
            a.before = dst;
    }

这就实现了将dst替换src

8. 清空

调用clear()代码如下:

    public void clear() {
        super.clear();
        head = tail = null;
    }

调用父类的clear。然后将头尾指针置空

9. 判断是否包含

代码如下:

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

这里可以看出 是从头结点开始遍历,遍历到尾结点。

猜你喜欢

转载自blog.csdn.net/gongsenlin341/article/details/105495871