LinkedHashMap源码分析

LinkedHashMap源码分析

继承体系

在这里插入图片描述

实现的接口:

  • Map
  • Clonable
  • Serializable

继承的类:

  • 间接继承 AbstractMap(抽象类)
  • 直接继承 HashMap

LinkedHashMap直接继承了HashMap,间接继承了AbstractMap,拥有HashMap所拥有的所有特性,同时自己在结构上发生了变化(从名字可以看出,底层数据结构使用了链表),使其增加了可以按照一定顺序访问的特性。

存储结构

因为LinkedHashMap继承了HashMap,所以底层为数组 + 单链表 + 红黑树,同时在每个节点添加了一对指针,也就是双向链表,这使其增加了可以按照一定顺序访问的特性。

在这里插入图片描述

属性

  • head:双向链表的头节点,旧数据存在头节点。
  • tail:双向链表的尾节点,新数据存在尾节点。
  • accessOrder:是否需要按访问顺序排序,如果为false则按插入顺序存储元素,如果是true则按访问顺序存储元素。
/**
 * 双向链表的头节点
 */
transient LinkedHashMap.Entry<K,V> head;

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

/**
 *	此链接哈希映射的迭代排序方法:访问顺序为true ,插入顺序为false 。
 */
final boolean accessOrder;

内部类

实际上一个存储节点有三个指针,next继承子HashMap.Node,用于单链表存储于桶中,before和after用于双向链表存储所有元素。

/**
 * HashMap.Node的子类,在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);
    }
}

// 位于HashMap中的HashMap.Node
static class Node<K, V> implements Map.Entry<K, V> {
    final int hash;
    final K key;
    V value;
    Node<K, V> next;
}

构造方法

/*
    构造一个具有指定初始容量和负载因子的空插入排序LinkedHashMap实例。
    参数:
    initialCapacity - 初始容量 
    loadFactor – 负载因子
    抛出:
    IllegalArgumentException – 如果初始容量为负或负载因子为非正
 */
public LinkedHashMap(int initialCapacity, float loadFactor) {
    super(initialCapacity, loadFactor);
    accessOrder = false;
}

/*
    构造一个具有指定初始容量和默认加载因子 (0.75) 的空插入排序LinkedHashMap实例。
    参数:
    initialCapacity - 初始容量
    抛出:
    IllegalArgumentException – 如果初始容量为负
 */
public LinkedHashMap(int initialCapacity) {
    super(initialCapacity);
    accessOrder = false;
}

/*
	构造一个具有默认初始容量 (16) 和加载因子 (0.75) 的空插入排序LinkedHashMap实例。
 */
public LinkedHashMap() {
    super();
    accessOrder = false;
}

/*
    构造一个插入顺序的LinkedHashMap实例,其映射与指定的映射相同。 LinkedHashMap实例是使用默认加载因子 (0.75) 和足以在指定映射中保存映射的初始容量创建的。
    参数:
    m - 其映射将放置在此map中的map
    抛出:
    NullPointerException – 如果指定的地图为空
 */
public LinkedHashMap(Map<? extends K, ? extends V> m) {
    super();
    accessOrder = false;
    putMapEntries(m, false);
}

/*
    构造一个具有指定初始容量、加载因子和排序模式的空LinkedHashMap实例。
    参数:
    initialCapacity - 初始容量 
    loadFactor – 负载因子 
    accessOrder – 排序模式 - 访问顺序为true ,插入顺序为false
    抛出:
    IllegalArgumentException – 如果初始容量为负或负载因子为非正
 */
public LinkedHashMap(int initialCapacity,
                     float loadFactor,
                     boolean accessOrder) {
    super(initialCapacity, loadFactor);
    this.accessOrder = accessOrder;
}

注意观察:前4个构造方法都将 accessOrder(为true是按照访问顺序,为false是按照插入顺序)都等于false,说明双向链表是 按插入顺序存储元素

最后一个构造方法accessOrder从参数传递,意味着可以将其设置为true,也就是按照访问顺序存储元素,这是实现 LRU缓存策略 的关键。

成员方法

三个后置处理方法

  • afterNodeInsertion
  • afterNodeAccess
  • afterNodeRemoval

这些方法在HasMap的方法中调用过,但是在HashMap中没有实际实现,而在LinkedHashMap中有了实际实现,是LinkedHashMap中重要的回调方法。

HashMap中定义的这三个方法:

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

直接翻译方法名是在节点插入后做的事情,也就插入新节点的后置处理,这个方法在HashMap的putVal方法中被调用。

在这里插入图片描述

在LinkedHashMap中的实现:

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

// afterNodeInsertion调用的removeEldestEntry方法
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
    return false;
}

首先来分析参数:boolean evict

  • evict 翻译过来是驱逐,赶出的意思。
  • 如果 evict 为 true ,且头节点不为空,且确定移除最老的元素,那么就调用HashMap.removeNode()把头节点移除(注意: 这里的头节点是双向链表的头节点,而不是某个桶中的第一个元素
  • afterNodeInsertion在LinkedHashMap中用来在移除元素后修改双向链表
  • if 判断中默认调用的removeEldestEntry方法默认返回false,也就是不移除最早插入元素。
afterNodeAccess(Node<K,V> e)方法

在节点访问之后被调用,主要在put()已经存在的元素或get()时被调用,如果accessOrder为true,调用这个方法把访问到的节点移动到双向链表的末尾。

void afterNodeAccess(Node<K,V> e) { // move node to last
    // 指针last,用于指向链表的尾部
    LinkedHashMap.Entry<K,V> last;
    // 如果accessOrder为true并且当前访问到的节点不位于链表尾部,就执行以下逻辑
    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;
        if (b == null)
            head = a;
        else
            b.after = a;
        if (a != null)
            a.before = b;
        else
            last = b;
        // 将p节点重新插入到链表的尾部
        if (last == null)
            head = p;
        else {
            p.before = last;
            last.after = p;
        }
        // 更新尾结点为p
        tail = p;
        ++modCount;
    }
}
afterNodeRemoval(Node<K,V> 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节点从链表中删除
    p.before = p.after = null;
    if (b == null)
        head = a;
    else
        b.after = a;
    if (a == null)
        tail = b;
    else
        a.before = b;
}

增添方法

LinkedHashMap并没有重写put方法,实际上调用的是父类的put方法,但重要的是重写了构建新节点的newNode()方法。newNode()方法就是创建一个新的节点并返回,但是因为LinkedHashMap的重写,返回的就不是HashMap中的内部类Node的对象,而是LinkedHashMap中内部类Entry的对象。

来看看具体实现:

重写后的newNode方法在返回节点之前会调用linkNodeLast方法。linkNodeLast方法会将新创建的节点插入到链表的尾部。

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

// newNode方法中调用的linkNodeLast方法。
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
    LinkedHashMap.Entry<K,V> last = tail;
    tail = p;
    // 如果链表中没有节点,就将head指针指向新创建的p节点
    if (last == null)
        head = p;
    else {
        // 将新创建的p节点插入到链表的尾部。
        p.before = last;
        last.after = p;
    }
}

移除方法

同样没有重写父类的remove()方法,所以逻辑与HashMap中发删除方法逻辑上没有什么变化。但是LinkedHashMap重写了afterNodeRemoval方法,该方法会在removeNode方法会涉及删除节点的操作中被回调,同时也是真正实现删除节点的方法。

查询方法

LinkedHashMap重写了get()getOrDefault()方法:

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

LinkedHashMap实现LRU缓存淘汰策略

LRU(Least Recently Used),是一种缓存淘汰策略,翻译过来是最近最少使用,也就是优先淘汰最近最少使用的元素。

accessOrder设置为true就可以实现LRU。

import java.util.LinkedHashMap;
import java.util.Map;

class LRU<K, V> extends LinkedHashMap<K, V> {
    // 保证缓存容量
    private int capacity;

    public LRU(int capacity, float loadFactor) {
        super(capacity, loadFactor, true);
        this.capacity = capacity;
    }

    /**
     * 重写LinkedHashMap的removeEldestEntry方法,当元素数量大于缓存(capacity)容量时删除旧元素
     *
     * @param eldest
     * @return
     */
    @Override
    protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
        return size() > this.capacity;
    }
}

测试:

/**
 * 测试实现LRU
 */
public class LRUTest {
    public static void main(String[] args) {
        // 创建一个只有5个元素的缓存
        LRU<Integer, Integer> lru = new LRU<>(5, 0.75f);
        lru.put(1, 1);
        lru.put(2, 2);
        lru.put(3, 3);
        lru.put(4, 4);
        lru.put(5, 5);
        lru.put(6, 6);
        lru.put(7, 7);

        System.out.println(lru.get(4));

        lru.put(6, 999);
        System.out.println(lru);
    }
}

控制台打印:

4
{3=3, 5=5, 7=7, 4=4, 6=999}

可以看到可以看到最旧的元素被删除了,同时最近访问过的元素被移动到了末尾位置。

总结

  • LinkedHashMap继承了HashMap,具有HashMap的所有特性
  • LinkedHashMap内部维护了一个双向链表存储所有的元素(在内部类Entry继承HashMap.Node的基础上加了一对指针)
  • 成员变量中新增了accessOrder属性:
    • 如果accessOrder为false,则可以按插入元素的顺序遍历元素
    • 如果accessOrder为true,则可以按访问元素的顺序遍历元素
  • LinkedHashMap很多方法都是在HashMap中留的钩子(Hook),直接实现这些Hook就可以实现对应的功能了,并不需要再重写put()等方法
  • 默认的LinkedHashMap并不会移除旧元素,如果需要移除旧元素,则需要重写removeEldestEntry()方法设定移除策略
  • LinkedHashMap可以用来实现LRU缓存淘汰策略
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

神烦狗闯入了你的博客

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值