JDK源码阅读—LinkedHashMap

简介

LinkedHashMap是一个散列表,储存的元素为键值对(key-value),允许空值和空键,非线程安全,实际上它在HashMap的基础上添加了一个双向链表用于保存遍历顺序,所以LinkedHashMap是一个有序集合。

LinkedHashMap的源码比较简单,其主要功能已由HashMap实现,下面暂时只分析链表部分,红黑树部分不做深入。

以下分析基于corretto-1.8.0_282版本。

继承关系

LinkedHashMap继承关系.png

  1. 继承自HashMap,包含HashMap的所有功能。

主要内部类

Entry<K,V>

/**
 * 元素节点,添加了两个指针用于组成双向链表,遍历时会沿着这个双向链表进行遍历
 */
static class Entry<K,V> extends 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

/**
 * 双向链表的头指针
 */
transient Entry<K,V> head;

tail

/**
 * 双向链表的尾指针
 */
transient Entry<K,V> tail;

accessOrder

/**
 * 遍历顺序,true代表使用访问顺序遍历,false代表使用插入顺序遍历
 * 插入顺序:指遍历时元素按插入的顺序输出
 * 访问顺序:指遍历时元素按访问的顺序输出,最近访问的元素会最后遍历
 * 最近访问的元素:指的是调用过get(key)、put(key, value)且key存在的对应的元素
 */
final boolean accessOrder;

构造方法

构造函数与HashMap基本类似,部分方法多了一个accessOrder参数用来设置LinkedHashMap的遍历顺序,accessOrder=true按元素访问顺序遍历,accessOrder=false按元素插入顺序遍历。

/**
 * 按给定的初始容量和加载因子实例化,遍历顺序设置为按插入顺序遍历
 */
public LinkedHashMap(int initialCapacity, float loadFactor) {
    super(initialCapacity, loadFactor);
    accessOrder = false;
}

/**
 * 按给定的初始容量实例化,加载因子为默认(0.75),遍历顺序设置为按插入顺序遍历
 */
public LinkedHashMap(int initialCapacity) {
    super(initialCapacity);
    accessOrder = false;
}

/**
 * 与HashMap类似,使用默认容量(16)和默认加载因子(0.75)进行
 * 实例化,并将遍历顺序设置为按插入顺序遍历
 */
public LinkedHashMap() {
    super();
    accessOrder = false;
}

/**
 * 与HashMap类似,根据给定Map实例化一个存放相同映射的HashMap,使用
 * 默认加载因子(0.75),初始容量根据传入的Map计算,遍历顺序设置为按插入顺序遍历
 */
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;
}

方法

LinkedHashMap大部分操作数据的方法都继承自其父类HashMap,在HashMap的对数据操作的方法中,有一些钩子函数,HashMap中没有做实现,是给LinkedHashMap预留的,例如插入元素的putVal方法,其中包含afterNodeAccess、afterNodeInsertion两个钩子函数,用来调整LinkedHashMap中元素在链表中的位置。

/**
 * 添加键值对
 * onlyIfAbsent参数决定若键已存在是否对其进行更新,true不进行更新,false进行更新
 * 通过源码可知,若此键对应旧值为null,则不管onlyIfAbsent参数,都进行更新
 * 若插入前键已存在,则返回其对应的值,不存在则返回null
 */
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)
        n = (tab = resize()).length;

    // 若hash对应的数组位置为空,则直接实例化一个链表节点插入此位置
    // 变量p为链表头或红黑树的根节点
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;

        if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
            // 如果链表头或红黑树的根节点的key与传入的key相同,则将其赋给变量e,不需要向下查找了
            e = p;
        else if (p instanceof TreeNode)
            // 若已经树化,则交由红黑树操作
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            // 遍历链表
            for (int binCount = 0; ; ++binCount) {
                // 若遍历到链表尾部还没有找到key相同的节点,则直接实例化一个新节点插入链表尾部
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);

                    // 若此时链表元素数量大于等于树化阈值,则进行树化操作
                    if (binCount >= TREEIFY_THRESHOLD - 1)
                        treeifyBin(tab, hash);
                    break;
                }

                // 若找到了key相同的节点,将其赋给变量e,终止遍历
                if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }

        // 若e不为空,说明已经存在key相同的节点,需要根据条件决定是否更新其value值
        if (e != null) {
            V oldValue = e.value;

            // 如果onlyIfAbsent为false或旧值为空,则对其进行更新
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;

            // LinkedHashMap使用
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;

    // 如果HashMap中的元素数量大于扩容阈值,则进行扩容操作
    if (++size > threshold)
        resize();

    // LinkedHashMap使用
    afterNodeInsertion(evict);
    return null;
}

对HashMap中存在的方法不再做说明,只介绍LinkedHashMap重写的方法。

newNode(int hash, K key, V value, Node<K,V> e)

/**
 * HashMap通过此方法创建新的链表节点,此处对其进行重写
 */
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
    Entry<K,V> p =
        new Entry<K,V>(hash, key, value, e);

    // 将新节点插入链表尾部,保存插入顺序
    linkNodeLast(p);
    return p;
}

/**
 * 节点链入链表尾部
 */
private void linkNodeLast(Entry<K,V> p) {

    // 保存尾部指针
    Entry<K,V> last = tail;

    // 将尾指针指向新节点
    tail = p;

    if (last == null)
        // 这是插入的第一个节点,将链表头指针也指向它
        head = p;
    else {
        // 将旧尾和新尾链接起来
        p.before = last;
        last.after = p;
    }
}

get(Object key)

/**
 * 重写了HashMap的对应方法,在原有逻辑上添加了调整遍历顺序的功能
 */
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;
}

getOrDefault(Object key, V defaultValue)

 /**
  * 若key不存在,则返回defaultValue的值
  */
 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;
}

forEach(BiConsumer<? super K, ? super V> action)

/**
 * 遍历所有元素
 */
public void forEach(BiConsumer<? super K, ? super V> action) {
    if (action == null)
        throw new NullPointerException();
    int mc = modCount;

    // 重写遍历方法,改为对双向链表进行遍历
    for (Entry<K,V> e = head; e != null; e = e.after)
        action.accept(e.key, e.value);
    if (modCount != mc)
        throw new ConcurrentModificationException();
}

replaceAll(BiFunction<? super K, ? super V, ? extends V> function)

/**
 * 遍历所有元素,根据传入的方法对值进行替换
 * @param function
 */
public void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) {
    if (function == null)
        throw new NullPointerException();
    int mc = modCount;

    // 重写遍历方法,改为对双向链表进行遍历
    for (Entry<K,V> e = head; e != null; e = e.after)
        e.value = function.apply(e.key, e.value);
    if (modCount != mc)
        throw new ConcurrentModificationException();
}

afterNodeRemoval(Node<K,V> e)

/**
 * 当删除元素时,会调用此方法,从双向链表中删除对应节点
 */
void afterNodeRemoval(Node<K,V> e) {

    // 保存节点e的前驱、后继节点
    Entry<K,V> p =
        (Entry<K,V>)e, b = p.before, a = p.after;
    p.before = p.after = null;

    if (b == null)
        // 若前驱b节点为null,说明e是链表头,则将链表头指针指向e的后继节点
        head = a;
    else
        // 不是链表头,需要修改前驱节点b的后继指针
        b.after = a;

    if (a == null)
        // 若后继a节点为null,说明e是链表尾,则将链表尾指针指向e的前驱节点
        tail = b;
    else
        // 不是链表尾,需要修改后继节点a的前驱指针
        a.before = b;
}

afterNodeAccess(Node<K,V> e)

/**
 * 当访问元素时,会调用此方法,遍历除外
 */
void afterNodeAccess(Node<K,V> e) {
    Entry<K,V> last;

    // 若遍历顺序为访问顺序且给定节点不是链表尾节点
    // 则将此节点移动到链表尾部
    if (accessOrder && (last = tail) != e) {

        // 保存给定节点e的前驱节点b、后继节点a
        Entry<K,V> p =
            (Entry<K,V>)e, b = p.before, a = p.after;
        p.after = null;

        if (b == null)
            // 如果前驱节点b为null,说明e是链表头,需要将链表头指针指向后继节点a
            head = a;
        else
            // e不是链表头,则需要将其前驱节点b的后继指针链接到后继节点a上
            b.after = a;

        if (a != null)
            // e不是链表尾,需要将其后继节点a的前驱指针链接到前驱节点b上
            a.before = b;
        else
            // e是链表尾,将last指向e的前驱节点b
            last = b;

        if (last == null)
            // 此时链表中只有e一个元素,将链表头指向e
            head = p;
        else {
            // 将e链接到链表尾部
            p.before = last;
            last.after = p;
        }

        // 链表尾指针指向传入节点e
        tail = p;

        // 修改次数加一
        // 当对LinkedHashMap迭代时,若调用了get(key)等访问方法,会抛出ConcurrentModificationException异常
        // 所以当accessOrder为true时,不能使用类似下方的代码来对LinkedHashMap进行遍历
        // for (Key key : map.keySet()) {
        //     map.get(key); // throw ConcurrentModificationException
        // }
        ++modCount;
    }
}

afterNodeInsertion(boolean evict)

/**
 * 当插入元素后,会调用此方法
 */
void afterNodeInsertion(boolean evict) { // possibly remove eldest
    Entry<K,V> first;

    // 若传入参数为true且Map不为空,则根据removeEldestEntry(first)方法的返回值决定是否移除链表的第一个元素
    // 当遍历顺序为插入顺序时,链表头节点为最先插入的元素
    // 当遍历顺序为访问顺序时,链表头节点为最远访问的元素(最近访问的元素会调整到链表尾部)
    // 在这里removeEldestEntry始终返回false,即永远不会删除节点
    // 可以通过重写此方法使其变得有意义,例如使用LinkedHashMap实现LRU缓存时,需要重写此方法,当元素数量大于缓存最大容量时,删除最近最少使用的节点
    if (evict && (first = head) != null && removeEldestEntry(first)) {
        K key = first.key;
        removeNode(hash(key), key, null, false, true);
    }
}

总结

  1. LinkedHashMap继承自HashMap,在支持HashMap的所有功能的基础上,添加了按顺序遍历元素的功能。
  2. LinkedHashMap中元素的遍历顺序时通过一个双向链表来保存的,当对HashMap中的元素进行增删改查操作时,会对这个链表中元素的顺序进行相应的调整。
  3. LinkedHashMap重写了HashMap中的迭代器,使其从原来的遍历数组->链表这种不确定顺序的方式,变成遍历一个有序的链表。保证LinkedHashMap的遍历顺序。
  4. 通过accessOrder参数可以设置LinkedHashMap的遍历顺序是按插入顺序还是按访问顺序。
  5. 可以将accessOrder设置为true和重写removeEldestEntry方法来快速实现一个LRU缓存。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值