LinkedHashMap源码解析(JDK8)

零 Java教程评论61字数 14914阅读49分42秒阅读模式

LinkedHashMap源码解析(JDK8)

DEMO

LinkedHashMap是LinkedList和HashMap的结合体,它内部的存储结构可以简单表示为下面这样:

LinkedHashMap源码解析(JDK8)文章源自灵鲨社区-https://www.0s52.com/bcjc/javajc/16636.html

LinkedHashMap内部存储的Entry在HashMap的基础上增加了两个指针:before和after。这两个节点可以对插入的节点进行链接,以此来维护顺序性。同时,链表结构为了方便插入,也会持有“头尾节点”这两个指针。文章源自灵鲨社区-https://www.0s52.com/bcjc/javajc/16636.html

LinkedHashMap的优点。文章源自灵鲨社区-https://www.0s52.com/bcjc/javajc/16636.html

  • 一个是降低了遍历实现的复杂度。我们对比一下,HashMap的遍历是首先遍历哈希槽,然后遍历链表;但LinkedHashMap则可以基于头节点遍历,复杂度明显降低。
  • 引入链表的第二个优点则是提供了顺序性。

LinkedHashMap提供了两种顺序性机制:文章源自灵鲨社区-https://www.0s52.com/bcjc/javajc/16636.html

  • 按节点插入顺序,是LinkedHashMap的默认行为;
  • 按节点的访问性顺序,最新访问的节点将被放到链表的末尾。

arduino

复制代码
public class LinkedHashMapDemo {

    public static void main(String[] args) {

        //按插入顺序排序
        orderOfWrite();
        System.out.println("---------");
        //按访问顺序排序
        orderOfRreader();
    }
    
    public static void orderOfWrite() {
        LinkedHashMap<String, Integer> map = new LinkedHashMap<>();
        map.put("Apple", 1);
        map.put("Banana", 2);
        map.put("Cherry", 3);
        System.out.println("按插入顺序排序:");
        for (Map.Entry<String, Integer> entry : map.entrySet()) {
            System.out.println(entry.getKey() + ": " + entry.getValue());
        }
    }
    
    public static void orderOfRreader() {
        LinkedHashMap<String, Integer> map = new LinkedHashMap<>(16, 0.75f, true);
        map.put("Apple", 1);
        map.put("Banana", 2);
        map.put("Cherry", 3);
        System.out.println("按插入顺序排序:");
        // 访问"Banana"
        System.out.println(map.get("Banana"));
        for (Map.Entry<String, Integer> entry : map.entrySet()) {
            System.out.println(entry.getKey() + ": " + entry.getValue());
        }
    }

}

makefile

复制代码
按插入顺序排序:
Apple: 1
Banana: 2
Cherry: 3
---------
按插入顺序排序:
2
Apple: 1
Cherry: 3
Banana: 2

概述

LinkedHashMap源码解析(JDK8)文章源自灵鲨社区-https://www.0s52.com/bcjc/javajc/16636.html

事实上LinkedHashMapHashMap的直接子类,二者唯一的区别是 LinkedHashMap  HashMap 的基础上,采用双向链表(doubly-linked list)的形式将所有entry连接起来,这样是为保证元素的迭代顺序跟插入顺序相同。上图给出了LinkedHashMap的结构图,主体部分跟HashMap完全一样,多了header指向双向链表的头部(是一个哑元),该双向链表的迭代顺序就是entry的插入顺序文章源自灵鲨社区-https://www.0s52.com/bcjc/javajc/16636.html

除了可以保迭代历顺序,这种结构还有一个好处 : 迭代 LinkedHashMap 时不需要像 HashMap 那样遍历整个table,而只需要直接遍历header指向的双向链表即可,也就是说LinkedHashMap的迭代时间就只跟entry的个数相关,而跟table的大小无关。文章源自灵鲨社区-https://www.0s52.com/bcjc/javajc/16636.html

有两个参数可以影响LinkedHashMap的性能: 初始容量(inital capacity)和负载系数(load factor)。初始容量指定了初始table的大小,负载系数用来指定自动扩容的临界值。当entry的数量超过capacity*load_factor时,容器将自动扩容并重新哈希。对于插入元素较多的场景,将初始容量设大可以减少重新哈希的次数。文章源自灵鲨社区-https://www.0s52.com/bcjc/javajc/16636.html

将对象放入到LinkedHashMap,有两个方法需要特别关心: hashCode()和equals()。hashCode()方法决定了对象会被放到哪个bucket里,当多个对象的哈希值冲突时,equals()方法决定了这些对象是否是“同一个对象” 。所以,如果要将自定义的对象放入到LinkedHashMap或LinkedHashSet中,需要重写 hashCode()和equals()方法。文章源自灵鲨社区-https://www.0s52.com/bcjc/javajc/16636.html

出于性能原因,LinkedHashMap是非同步的(not synchronized),如果需要在多线程环境使用,需要程序员手动同步;或者通过如下方式将LinkedHashMap包装成(wrapped)同步的:文章源自灵鲨社区-https://www.0s52.com/bcjc/javajc/16636.html

ini

复制代码
Map m = Collections.synchronizedMap(new LinkedHashMap(...));

源码分析

LinkedHashMap 是一个关联数组、哈希表,它是线程不安全的,允许key为null,value为null。它继承自HashMap,实现了Map<K,V>接口。其内部还维护了一个双向链表,在每次插入数据,或者访问、修改数据时,会增加节点、或调整链表的节点顺序。以决定迭代时输出的顺序。

默认情况,遍历时的顺序是按照插入节点的顺序。这也是其与HashMap最大的区别。也可以在构造时传入accessOrder参数,使得其遍历顺序按照访问的顺序输出。

LinkedHashMap实现了Map接口,即允许放入key为null的元素,也允许插入value为null的元素。从名字上可以看出该容器是linked listHashMap的混合体,也就是说它同时满足HashMaplinked list的某些特性。可将 LinkedHashMap 看作采用 linked list 增强的 HashMap 

双向链表

scala

复制代码
public class LinkedHashMap<K,V>
    extends HashMap<K,V>
    implements Map<K,V>
{
    // 这段代码定义了一个静态内部类Entry,它继承了HashMap.Node类,
    // 用于表示哈希表中的键值对节点。Entry类添加了两个指向相邻节点的引用before和after,
    // 用于双向链表的维护。构造函数用于初始化节点的哈希值、键、值和下一个节点
    static class Entry<K,V> extends HashMap.Node<K,V> {
        // 新加成员变量before、after,用于双向链表的连接
        Entry<K,V> before, after;
        //构造方法
        Entry(int hash, K key, V value, Node<K,V> next) {
            //父类构造方法
            super(hash, key, value, next);
        }
    }    
}

同时类里有两个成员变量head tail,分别指向内部双向链表的表头、表尾。

ini

复制代码
  //双向链表的头结点
  transient LinkedHashMap.Entry<K,V> head;

  //双向链表的尾结点
  transient LinkedHashMap.Entry<K,V> tail;

  //排序方式,true-访问顺序 false-插入顺序
  //默认是插入顺序存储:accessOrder = false;
  //accessOrder = true时表示访问顺序存储,就是最新访问的数据会放到链表尾部!
  //(访问顺序的LinkedHashMap进行了get操作以后,重新排序,把get的Entry移动到双向链表的表尾。)
  final boolean accessOrder;

LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>,可发现LinkedHashMap相比较HashMap多维护了2个成员变量head、tail,这2个变量就是LinkedHashMap实现双向链表的关键变量。

LinkedHashMap 重写了父类新建节点的方法,在新建节点之后调用 linkNodeLast 方法将新添加的节点链接到双向链表的末尾:

ini

复制代码
    //HashMap.put()方法创建新Node会调用newNode()方法,LinkedHashMap重写了HashMap的newNode()
    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;
    }
 
    //将新节点加入双向链表尾部
    // 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;//旧尾节点作为新节点的before
            last.after = p;//新节点作为旧尾节点的after
        }
    }

HashMap.put()方法创建新Node会调用newNode()方法,LinkedHashMap重写了HashMap的newNode()。所以,往LinkedHashMap中put()数据时会调用linkNodeLast(),将新节点加入双向链表。

get()

默认情况下,LinkedHashMap 是按插入顺序维护链表。不过我们可以在初始化 LinkedHashMap,指定 accessOrder 参数为 true,即可让它按访问顺序维护链表。访问顺序的原理上并不复杂,当我们调用get/getOrDefault/replace等方法时,只需要将这些方法访问的节点移动到链表的尾部即可。相应的源码如下:

ini

复制代码
// LinkedHashMap 中覆写
public V get(Object key) {
    Node<K,V> e;
    if ((e = getNode(hash(key), key)) == null)
        return null;
    // 如果 accessOrder 为 true,则调用 afterNodeAccess 将被访问节点移动到链表最后
    if (accessOrder)
         //访问后调用回调方法调整双链表
        afterNodeAccess(e);
    return e.value;
}



/**
 * 根据给定的哈希值和键获取对应的节点。
 * 此方法用于在HashMap中查找特定键值对的节点,首先检查哈希表是否为空,然后按照哈希值定位到链表或红黑树的起始节点,
 * 并遍历链表或红黑树直到找到匹配的键值对节点或遍历结束。
 *
 * @param hash 节点的哈希值
 * @param key 要查找的键
 * @return 对应的节点,如果找不到则返回null
 */
final Node<K,V> getNode(int hash, Object key) {
    // 获取table数组以及长度,同时检查table是否已经被初始化
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    // 当table不为空且长度大于0时,进行进一步的查找
    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;
        // 如果第一个节点是一个TreeNode(表示当前段是一个红黑树),则使用特定的算法在树中查找
        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);
        }
    }
    // 如果没有找到匹配的节点,则返回null
    return null;
}




// LinkedHashMap 中覆写
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;
        // 如果 b 为 null,表明 p 为头节点
        if (b == null)
            head = a;
        else
            b.after = a;
            
        if (a != null)
            a.before = b;
        /*
         * 这里存疑,父条件分支已经确保节点 e 不会是尾节点,
         * 那么 e.after 必然不会为 null,不知道 else 分支有什么作用
         */
        else
            last = b;
    
        if (last == null)
            head = p;
        else {
            // 将 p 接在链表的最后
            p.before = last;
            last.after = p;
        }
        tail = p;
        ++modCount;
    }
}

下面举例演示一下,帮助大家理解。假设我们访问下图键值为3的节点,访问前结构为:

LinkedHashMap源码解析(JDK8)

访问后,键值为3的节点将会被移动到双向链表的最后位置,其前驱和后继也会跟着更新。访问后的结构如下:

LinkedHashMap源码解析(JDK8)

put()

插入有两重含义:

  1. 从table的角度看,新的entry需要插入到对应的bucket里,当有哈希冲突时,采用头插法将新的entry插入到冲突链表的头部。
  2. 从header的角度看,新的entry需要插入到双向链表的尾部。

LinkedHashMap源码解析(JDK8)

put(K key, V value)方法是将指定的key, value对添加到map里。该方法首先会对map做一次查找,看是否包含该元组,如果已经包含则直接返回,查找过程类似于get()方法;如果没有找到,则会通过addEntry(int hash, K key, V value, int bucketIndex)方法插入新的entry。

LinkedHashMap并没有重写任何put方法。但是其重写了构建新节点的newNode()方法,通过linkNodeLast(p);将新节点链接在内部双向链表的尾部。

newNode()会在HashMap的putVal()方法里被调用,putVal()方法会在批量插入数据putMapEntries(Map<? extends K, ? extends V> m, boolean evict)或者插入单个数据public V put(K key, V value)时被调用。

ini

复制代码
// HashMap 中实现
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

// HashMap 中实现
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) {...}
    // 通过节点 hash 定位节点所在的桶位置,并检测桶中是否包含节点引用
    if ((p = tab[i = (n - 1) & hash]) == 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) {...}
        else {
            // 遍历链表,并统计链表长度
            for (int binCount = 0; ; ++binCount) {
                // 未在单链表中找到要插入的节点,将新节点接在单链表的后面
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    if (binCount >= TREEIFY_THRESHOLD - 1) {...}
                    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) {...}
            afterNodeAccess(e);    // 回调方法
            return oldValue;
        }
    }
    ++modCount;
    if (++size > threshold) {...}
    afterNodeInsertion(evict);    // 回调方法
    return null;
}

// HashMap 中实现
Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
    return new Node<>(hash, key, value, next);
}

// LinkedHashMap 中覆写
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);
    // 将 Entry 接在双向链表的尾部
    linkNodeLast(p);
    return p;
}

// LinkedHashMap 中实现
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
    LinkedHashMap.Entry<K,V> last = tail;
    tail = p;
    // last 为 null,表明链表还未建立
    if (last == null)
        head = p;
    else {
        // 将新节点 p 接在链表尾部
        p.before = last;
        last.after = p;
    }
}

Map 类型的集合类是通过 put(K,V) 方法插入键值对,LinkedHashMap 本身并没有覆写父类的 put 方法,而是直接使用了父类的实现。但在 HashMap 中,put 方法插入的是 HashMap 内部类 Node 类型的节点,该类型的节点并不具备与 LinkedHashMap 内部类 Entry 及其子类型节点组成链表的能力。那么,LinkedHashMap 是怎样建立链表的呢?

LinkedHashMap重写了newNode()方法。 在这个方法中,LinkedHashMap 创建了 Entry,并通过 linkNodeLast 方法将 Entry 接在双向链表的尾部,实现了双向链表的建立。

remove()

删除有两重含义:

  1. 从table的角度看,需要将该entry从对应的bucket里删除,如果对应的冲突链表不空,需要修改冲突链表的相应引用。
  2. 从header的角度来看,需要将该entry从双向链表中删除,同时修改链表中前面以及后面元素的相应引用。

LinkedHashMap源码解析(JDK8)

LinkedHashMap 删除操作相关的代码也是直接用父类的实现。在删除节点时,父类的删除逻辑并不会修复 LinkedHashMap 所维护的双向链表,这不是它的职责。那么删除及节点后,被删除的节点该如何从双链表中移除呢?当然,办法还算是有的, HashMap 中三个回调方法运行 LinkedHashMap 对一些操作做出响应。所以,在删除及节点后,回调方法 afterNodeRemoval 会被调用。LinkedHashMap 覆写该方法,并在该方法中完成了移除被删除节点的操作。相关源码如下:

ini

复制代码
// HashMap 中实现
public V remove(Object key) {
    Node<K,V> e;
    return (e = removeNode(hash(key), key, null, false, true)) == null ?
        null : e.value;
}

// 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;
        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) {...}
            else {
                // 遍历单链表,寻找要删除的节点,并赋值给 node 变量
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key ||
                         (key != null && key.equals(k)))) {
                        node = e;
                        break;
                    }
                    p = e;
                } while ((e = e.next) != null);
            }
        }
        if (node != null && (!matchValue || (v = node.value) == value ||
                             (value != null && value.equals(v)))) {
            if (node instanceof TreeNode) {...}
            // 将要删除的节点从单链表中移除
            else if (node == p)
                tab[index] = node.next;
            else
                p.next = node.next;
            ++modCount;
            --size;
            afterNodeRemoval(node);    // 调用删除回调方法
            return node;
        }
    }
    return null;
}

// LinkedHashMap 中覆写
void afterNodeRemoval(Node<K,V> e) { // unlink
    LinkedHashMap.Entry<K,V> p =
        (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
    // 将 p 节点的前驱后后继引用置空
    p.before = p.after = null;
    // b 为 null,表明 p 是头节点
    if (b == null)
        head = a;
    else
        b.after = a;
    // a 为 null,表明 p 是尾节点
    if (a == null)
        tail = b;
    else
        a.before = b;
}

删除的过程并不复杂,上面这么多代码其实就做了三件事:

  1. 根据 hash 定位到桶位置
  2. 遍历链表或调用红黑树相关的删除方法
  3. 从 LinkedHashMap 维护的双链表中移除要删除的节点

举个例子说明一下,假如我们要删除下图键值为 3 的节点。

LinkedHashMap源码解析(JDK8)

根据 hash 定位到该节点属于3号桶,然后在对3号桶保存的单链表进行遍历。找到要删除的节点后,先从单链表中移除该节点。如下:

LinkedHashMap源码解析(JDK8)

然后再双向链表中移除该节点:

LinkedHashMap源码解析(JDK8)

删除及相关修复过程并不复杂,结合上面的图片,大家应该很容易就能理解,这里就不多说了。

如何维护插入序和访问序?

在 LinkedHashMap 中,所有的 Entry 都被串联在一个双向链表中。每次在新建一个节点时都会将新建的节点链接到双向链表的末尾。这样从双向链表的尾部向头部遍历就可以保证插入顺序了,头部节点是最早添加的节点,而尾部节点则是最近添加的节点。那么,访问顺序要怎么实现呢?

之前我们在分析 HashMap 的源码的时候,在添加及更新、查找、删除等操作中可以看到 afterNodeAccess、afterNodeInsertion、afterNodeRemoval 等几个方法的调用,不过在 HashMap 中这几个方法中没有任何操作。实际上,这几个方法就是供 LinkedHashMap 的重写的,我们不妨看一下在 HashMap 中这几个方法的声明:

typescript

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

在 LinkedHashMap 中对这几个方法进行了重写:

ini

复制代码
//移除节点的回调函数
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;
}

//插入节点的回调函数
//构造函数中调用,evict为false
void afterNodeInsertion(boolean evict) { // possibly remove eldest
    LinkedHashMap.Entry<K,V> first;
    //first是头元素,也是最老的元素
    //在插入序中,就是最先插入的元素
    //在访问序中,就是最远被访问的元素
    //这里removeEldestEntry(first)始终返回true,即不删除最老的元素
    //如果是一个容量固定的cache,可调整removeEldestEntry(first)的实现
    if (evict && (first = head) != null && removeEldestEntry(first)) {
        //不是构造方法中
        //头元素不为空
        //要删除最老的元素
        //在LinkedHashMap的实现中,不会进入这里
        K key = first.key;
        removeNode(hash(key), key, null, false, true);
    }
}

//访问节点的回调函数
void afterNodeAccess(Node<K,V> e) { // move node to last
    LinkedHashMap.Entry<K,V> last;
    //如果是访问序,且当前节点并不是尾节点
    //将该节点置为双向链表的尾部
    if (accessOrder && (last = tail) != e) {
        //p 当前节点, b 前驱结点, a 后继结点
        LinkedHashMap.Entry<K,V> p =
            (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
        p.after = null; //设为尾节点,则没有后继
        if (b == null)
            head = a; //p是头节点,调整后其后继结点变为头节点
        else
            b.after = a;//p不是头节点,前驱和后继结点相连
        if (a != null)
            a.before = b;
        else
            last = b;//应该不会出现这种情况,p是尾节点
        if (last == null)
            head = p;
        else {
            //将p置于尾节点之后
            p.before = last;
            last.after = p;
        }
        tail = p;//调整tail指向
        ++modCount;//结构性改变
    }
}

在插入节点、删除节点和访问节点后会调用相应的回调函数。可以看到,在 afterNodeAccess 方法中,如果该 LinkedHashMap 是访问序,且当前访问的节点不是尾部节点,则该节点会被置为双链表的尾节点。即,在访问序下,最近访问的节点会是尾节点,头节点则是最远访问的节点。

在 afterNodeInsertion 中,如果 removeEldestEntry(first) 节点返回 true,则会将头部节点删除。如果想要实现一个固定容量的 Map,可以在继承 LinkedHashMap 后重写 removeEldestEntry 方法。在 LinkedHashMap 中,该方法始终返回 false。

typescript

复制代码
//返回false
//是否移除最老的Entry
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
    return false;
}

在 HashMap 中,在 putVal 和 removeNode 中都调用了相应的回调函数,而 get 则没有,因而在 LinkedHahsMap 中进行了重写:

kotlin

复制代码
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;
}

遍历及迭代器

因为 LinkeHashMap 的所有的节点都在一个双向链表中,因而可以通过该双向链表来遍历所有的 Entry。而在 HashMap 中,要遍历所有的 Entry,则要依次遍历所有桶中的单链表。相比较而言,从时间复杂度的角度来看,LinkedHashMap 的复杂度为 O(size()),而 HashMap 则为 O(capacity + size())。

ini

复制代码
//因为所有的节点都被串联在双向链表中,迭代器在迭代时可以利用双向链表的链接关系进行
//双向链表的顺序是按照插入序或访问序排列的
//相比于HashMap中的迭代,LinkedHashMap更为高效,O(size())
//HashMapde 迭代,O(capacity + size())
abstract class LinkedHashIterator {
    LinkedHashMap.Entry<K,V> next;
    LinkedHashMap.Entry<K,V> current;
    int expectedModCount;

    LinkedHashIterator() {
        next = head;
        expectedModCount = modCount;
        current = null;
    }

    public final boolean hasNext() {
        return next != null;
    }

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

    public final void remove() {
        Node<K,V> p = current;
        if (p == null)
            throw new IllegalStateException();
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
        current = null;
        K key = p.key;
        removeNode(hash(key), key, null, false, false);
        expectedModCount = modCount;
    }
}

可以看到,在遍历所有节点时是通过节点的 after 引用进行的。这样,可以双链表的头部遍历到到双链表的尾部,就不用像 HahsMap 那样访问空槽了。

在 containsValue 和 internalWriteEntries 中也使用了双向链表进行遍历。

java

复制代码
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;
}

//覆盖父类方法
//序列化,Called only from writeObject, to ensure compatible ordering.
void internalWriteEntries(java.io.ObjectOutputStream s) throws IOException {
    //调整元素的遍历方式,使用双链表遍历
    for (LinkedHashMap.Entry<K,V> e = head; e != null; e = e.after) {
        s.writeObject(e.key);
        s.writeObject(e.value);
    }
}

LinkedHashMap经典用法

在LinkedHashMap中,如果顺行性机制选择“按访问顺序”,那么当元素被访问时,元素会默认被放到链表的尾部,并且在向LinkedHashMap添加元素时会调用afterNodeInsertion方法。这个方法的具体实现代码如下:

LinkedHashMap源码解析(JDK8)

从代码中可以看出,如果removeEldestEntry函数返回true,则会删除LinkedHashMap中的第一个元素,这样就淘汰了旧的数据,实现了LRU的效果。removeEledestEntry方法的代码如下:

LinkedHashMap源码解析(JDK8)

可以看到,默认返回的是false,表示LinkedHashMap并不会启用节点的淘汰机制。为了实现LRU算法,我们需要继承LinkedHashMap并重写该方法,具体实现代码如下:

scala

复制代码
public class LRUCache<K, V> extends LinkedHashMap<K, V> {

    private final int maxCapacity;

    public LRUCache(int maxCapacity) {

        super(maxCapacity, 0.75f, true);
        this.maxCapacity = maxCapacity;
    }

    @Override
    protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
        //如果超过了最大容量,则启动剔除机制
        return size() > maxCapacity;
    }
}

arduino

复制代码
public class LRUCacheTest {
    public static void main(String[] args) {

        LRUCache<String, String> cache = new LRUCache<>(5);

        for (int i = 0; i < 10; i++) {
            cache.put(String.valueOf(i), String.valueOf(i * i));
        }


        System.out.println("插入10个键值对后,缓存内容:");
        System.out.println(cache + "n");


        System.out.println("访问键值为7的节点后,缓存内容:");
        cache.get("7");
        System.out.println(cache + "n");


        System.out.println("插入键值为11的键值对后,缓存内容:");
        cache.put(String.valueOf(11), String.valueOf(11 * 11));
        System.out.println(cache);
    }
}

ini

复制代码
插入10个键值对后,缓存内容:
{5=25, 6=36, 7=49, 8=64, 9=81}

访问键值为7的节点后,缓存内容:
{5=25, 6=36, 8=64, 9=81, 7=49}

插入键值为11的键值对后,缓存内容:
{6=36, 8=64, 9=81, 7=49, 11=121}

零
  • 转载请务必保留本文链接:https://www.0s52.com/bcjc/javajc/16636.html
    本社区资源仅供用于学习和交流,请勿用于商业用途
    未经允许不得进行转载/复制/分享

发表评论