Java容器深度总结:LinkedHashMap

旧游无处不堪寻。无寻处,惟有少年心。

1.LinkedHashMap概述

LinkedHashMap来自于JDK1.4,直接继承自 HashMap,并且在 HashMap 基础上,通过维护由所有Entry节点构成的双向链表,来保证元素有序。

LinkedHashMap 继承了 HashMap,因此具有和 HashMap一样的快速查找特性。LinkedHashMap 对 HashMap 高度复用,因此建议先学习HashMap的相关知识:Java容器深度总结:HashMap

2.LinkedHashMap的定义
public class LinkedHashMap<K,V>
    extends HashMap<K,V>
    implements Map<K,V>

LinkedHashMap类图:
在这里插入图片描述
LinkedHashMap数据结构:

在这里插入图片描述
由于JDK1.8开始,HashMap底层使用的数据结构是:数组+单链表+红黑树,因此LinkedHashMap的底层数据结构则是:HashMap的底层数据结构+双向链表。其中双向链表的性质由after和before指针维护。

3.主要类属性

LinkedHashMap在拥有HashMap的相关属性时,还具有以下自己特有的属性。

3.1 transient LinkedHashMap.Entry< K,V > head

用于保存双向链表首结点的引用,也就是头结点指针。

3.2 transient LinkedHashMap.Entry< K,V > tail

用于保存双向链表尾结点的引用,也就是尾指针。

3.3 final boolean accessOrder

LinkedHashMap排序方式标志。true:按访问顺序;false:按插入顺序。

4.Entry节点

LinkedHashMap的Entry继承了HashMap的Node类,并且每个Entry几点都包含前指针和后指针,用于双向链表的实现。

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

Entry的类图:
在这里插入图片描述

5.构造函数
5.1 LinkedHashMap()

构造一个具有默认初始容量 (16) 和 默认加载因子 (0.75) 的空LinkedHashMap 实例,并且双向链表维持的顺序默认为元素插入顺序(accessOrder = false)。

public LinkedHashMap() {
    //调用父类HashMap的无参构造器
    super();
    accessOrder = false;
}
5.2 LinkedHashMap(initialCapacity)

构造一个具有指定初始容量和默认加载因子 (0.75) 的LinkedHashMap 实例。并且双向链表维持的顺序默认为元素插入顺序。

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

需要注意的是:HashMap的容量必须是2的整数次幂,因此对于传入的初始容量initialCapacity,其哈希表的真实容量是大于等于initialCapacity的最小2的整数次幂的数,例如:initialCapacity = 10,哈希表实际的容量为16。

5.3 public LinkedHashMap(initialCapacity, loadFactor)

构造一个具有指定初始容量和加载因子的空 LinkedHashMap 实例。并且双向链表维持的顺序默认为元素插入顺序。

public LinkedHashMap(int initialCapacity, float loadFactor) {
    super(initialCapacity, loadFactor);
    accessOrder = false;
}
5.4 LinkedHashMap(initialCapacity,loadFactor,accessOrder)

构造一个具有指定初始容量、加载因子和排序方式的空 LinkedHashMap 实例。

public LinkedHashMap(int initialCapacity,float loadFactor, boolean  accessOrder) {
    super(initialCapacity, loadFactor);
    this.accessOrder = accessOrder;
}
5.5 LinkedHashMap(Map<? extends K,? extends V> m)

构造一个包含指定Map中的元素的 LinkedHashMap 实例。所创建的 LinkedHashMap 实例具有默认的加载因子 (0.75) 和 足以容纳指定映射中映射关系的初始容量,并且双向链表维持的顺序默认为元素插入顺序。

public LinkedHashMap(Map<? extends K, ? extends V> m) {
    super();
    accessOrder = false;
    //调用父类的方法
    putMapEntries(m, false);
}
6.双向链表的维护
6.1 插入节点后的维护(linkNodeLast)

LinkedHashMap并没有重写其父类HashMap的put方法,而是重写了linkNodeLast()方法将新节点连接到双向链表的尾部

HashMap在put元素时,会根据计算出的key所在的桶位的节点类型,再创建普通Node节点还是红黑树TreeNode节点,然后newNode()或newTreeNode()方法创建节点,LinkedHashMap重写了这两个方法,并且linkNodeLast()方法会在这两个方法中被调用,以维护双向链表的顺序。

	// Overrides method 'newNode' in java.util.HashMap
	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;
    }

	// Overrides method 'newTreeNode' in java.util.HashMap
    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;
    }

linkNodeLast()方法:

/**
 * 新节点链接到双向链表尾部
 *
 * @param p 新节点
 */
private void linkNodeLast(LinkedHashMap.Entry<K, V> p) {

    LinkedHashMap.Entry<K, V> last = tail;
    //如果tail和head都为null,那么新添加第一个节点时,tail和head都指向该节点
    tail = p;
    if (last == null)
        head = p;
        /*否则,将新节点链接到双向链表末尾,新节点成为新的tail节点*/
    else {
        p.before = last;
        last.after = p;
    }
}
6.2 删除元素后的维护(afterNodeRemoval)

HashMap调用removeNode()方法实现对HashMap中元素的删除,removeNode()方法调用了afterNodeRemoval()方法,该方法在HashMap中为空实现。LinkedHashMap通过重写HashMap中的afterNodeRemoval()方法,实现双向链表中该元素结点的删除。

// HashMap中afterNodeRemoval的实现
// HashMap移除节点后调用,为空实现
void afterNodeRemoval(Node<K,V> p) { }

//Overrides method 'afterNodeRemoval' in java.util.HashMap
// 参数 e 为被删除的节点
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;
}
6.3 访问元素后双向链表的顺序维护(afterNodeAccess)

LinkedHashMap中的双向链表默认按元素插入的顺序排列,也可以在构造LinkedHashMap是指定为元素的访问顺序(accessOrder = true)。

afterNodeAccess()方法会在一个元素节点被访问到时被调用,同样该方法在HashMap中为空实现,LinkedHashMap 中覆写了afterNodeAccess()方法。

也就是说,当一个节点被访问时,如果 accessOrder 为 true,则会将该节点移到链表尾部。保证链表尾部是最近访问的节点,那么链表首部就是最近未使用的节点

//在元素被访问时,会调用afterNodeAccess方法,HashMap中的方法为空实现
void afterNodeAccess(Node<K, V> p) {
}


/**
 * LinkedHashMap 中重写的afterNodeAccess方法,用于将被访问到的节点移动到双向链表末尾
 *
 * @param e 被访问的节点
 */
void afterNodeAccess(Node<K, V> e) { // move node to last
    LinkedHashMap.Entry<K, V> last;
    /*如果e不是尾节点,那么尝试移动e到尾部*/
    if (accessOrder && (last = tail) != e) {
        //p记录e,b保存p在大链表中的前驱,a保存p在大链表中的后继
        LinkedHashMap.Entry<K, V> p =
                (LinkedHashMap.Entry<K, V>) e, b = p.before, a = p.after;
        //p的后继置空
        p.after = null;
        //如果b为null,表明p为头节点
        if (b == null)
            //头节点设置为p的后继a
            head = a;
        else
            //否则b的后继设置为a
            b.after = a;
        /*如果a不为null,a的前驱设置为b*/
        if (a != null) {
            a.before = b;
        }
        /*否则,尾节点设置为b*/
        else {
            last = b;
        }
        //如果,last为null
        if (last == null)
            //那么头节点指向p
            head = p;
        else {
            /*否则,将p链接在链表的最后*/
            p.before = last;
            last.after = p;
        }
        //尾节点指向p
        tail = p;
        ++modCount;
    }
}

两种排序方式:

public class LinkedHashMapTest {

    public static void main(String[] args) throws Exception {
        
        LinkedHashMap map = new LinkedHashMap(16, 0.75f, false);
        map.put(1, 1);
        map.put(2, 2);
        map.put(3, 3);
        map.put(4, 4);
        map.put(5, 5);
        System.out.println("按元素插入顺序:"+map.keySet());

        LinkedHashMap map2 = new LinkedHashMap(16, 0.75f, true);
        map2.put(1, 1);
        map2.put(2, 2);
        map2.put(3, 3);
        map2.put(4, 4);
        map2.put(5, 5);
        map2.get(1);
        System.out.println("按元素访问顺序:"+map2.keySet());
    }
}
输出:
按元素插入顺序:[1, 2, 3, 4, 5]
按元素访问顺序:[2, 3, 4, 5, 1]
7.LRU缓存

LRU(Least recently used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是:如果数据最近被访问过,那么将来被访问的几率也更高。

7.1 LRU基于链表的实现算法

以链表尾部保存最近访问数据为例 :

  • 新数据插入到链表尾部;
  • 每当缓存命中(即缓存数据被访问),则将数据移到链表尾部;
  • 指定LRU缓存的容量,当链表长度大于容量时,将链表头部的数据丢弃。
7.2 LinkedHashMap与LRU缓存

LinkedHashMap已经提供了基于访问顺序的迭代机制,最近被访问的节点在尾部,最远被访问的节点在头部,并且提供了链表头部的数据丢弃的实现。

7.2.1 afterNodeInsertion()方法

由于LRU缓存必然要有容量限制,因此在每次添加完元素之后需要判断,当前元素个数是否大于缓存容量:

若元素个数 > 缓存容量 ,则删除链表头部最近未使用的节点。

因此,LinkedHashMap在成功插入元素操作之后,不光会调用linkNodeLast方法,将新元素插入链表尾部,最后还会调用afterNodeInsertion()方法确保元素个数不会超过缓存容量。

//HashMap提供的空实现
void afterNodeInsertion(boolean evict) {
}

/**
 * LinkedHashMap重写的实现
 *
 * @param evict 构造器中传递false,单独调用方法传递true
 */
void afterNodeInsertion(boolean evict) {

    LinkedHashMap.Entry<K, V> first;
    //如果evict为true,并且大链表头节点不为null,并且removeEldestEntry(first)方法返回true
    if (evict && (first = head) != null && removeEldestEntry(first)) {
        K key = first.key;
        //那么调用removeNode移除头节点,这一移除方法中具有afterNodeRemoval方法
        removeNode(hash(key), key, null, false, true);
    }
}

Tip:

  • afterNodeInsertion()方法在HashMap中提供空实现。
  • evict 参数只有在调用包含指定Map中的元素的构造方法中被指定为false。
7.2.2 removeEldestEntry()方法

afterNodeInsertion()方法内部调用了removeEldestEntry()方法并以返回值作为是否需要移除头节点的判断条件之一。在LinkedHashMap中,该方法始终返回false。

// 始终返回 fasle
// 重写该方法 实现LRU缓存
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
    return false;
}

也就是说,LinkedHashMap默认是不会删除链表首部最近未使用的节点。

removeEldestEntry()方法在实现LRU缓存的过程中,起着不可或缺的作用:在该方法中比较节点总数和缓存容量的大小,当元素个数 > 缓存容量时返回true,这样afterNodeInsertion()方法就能去删除最近未使用的节点,从而保证缓存空间足够。

7.3 LinkedHashMap实现LRU缓存

基于上面的分析,我们可以了解到:LinkedHashMap实现LRU缓存的关键在于重写removeEldestEntry()方法,因此LinkedHashMap实现LRU缓存的步骤为:

  • 设定最大缓存空间 maxEntries;
  • 使用 LinkedHashMap 的构造函数将 accessOrder 设置为 true,开启 LRU 顺序;
  • 覆盖 removeEldestEntry() 方法实现,在节点多于 maxEntries 就会将最近未使用的数据移除。

示例代码:

public class LRUCache<K, V> extends LinkedHashMap<K, V> {

    private int maxEntries;//cache 容量

    public LRUCache(int maxEntries) {
        super(16, 0.75f, true);
        this.maxEntries = maxEntries;
    }

    @Override
    protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
        return super.size() > maxEntries;
    }

    public static void main(String[] args) {
        LRUCache cache = new LRUCache(3);
        cache.put(1, 1);
        cache.put(2, 2);
        cache.put(3, 3);
        cache.get(1); // cache命中
        cache.put(4, 4);
        System.out.println(cache.keySet());
    }
}
输出:
[3, 1, 4]
8.迭代器

LinkedHashMap的迭代器在设计上与HashMap有异曲同工之处。
在这里插入图片描述
在这里插入图片描述

LinkedHashMap的迭代器有三种:

  • LinkedKeyIterator
  • LinkedValueIterator
  • LinkedEntryIterator

他们都继承自LinkedHashIterator抽象类,并且实现了Iterator接口。其中LinkedHashIterator为这些迭代器提供了对Iterator接口方法的基本实现,他们惟一的区别就是next()方法的实现不同。

注意:LinkedHashMap的三种迭代器都是快速失败(fail-fast)的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值