java之LinkedHashMap详解

~ 前言

如果之前没有了解过HashMap的源码,请先看 java之HashMap详解。如果已经了解过了,那么今天这篇文章非常简单。
后续讲解内容为源码实现,这里使用的是JDK8的版本。

请添加图片描述


LinkedHashMap

LinkedHashMap是HashMap的一个子类,它的用法与父类HashMap相同。不过LinkedHashMap比HashMap多了一个特点,那就是可以按照添加元素时的顺序来进行遍历。并且通过名字 Linked 我们可以猜到它是一个链表结构,但是又要与HashMap结合,这个就有点懵了,我们慢慢来探索吧。

首先认识一下很重要的几个成员与节点的定义。

	// 头节点,里面内容为空,访问顺序下是最年轻的
	transient LinkedHashMap.Entry<K,V> head;

	// 尾节点,里面内容为空,访问顺序下是最年轻的
    transient LinkedHashMap.Entry<K,V> tail;

	// 访问顺序
	// true可以按最近访问顺序遍历,最近访问的优先读
	final boolean accessOrder;

	// 添加LinkedHashMap节点独有的定义(双向指针: before,after)
	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的所有功能
  • cloneable,可以克隆
  • serializable,可以被序列化

初始化

简单分析一下:

	// 都是调用HashMap进行初始化
    public LinkedHashMap(int initialCapacity, float loadFactor) {
       super(initialCapacity, loadFactor);
       accessOrder = false;
    }

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

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

    public LinkedHashMap(Map<? extends K, ? extends V> m) {
        super();
        accessOrder = false;
        putMapEntries(m, false);
    }

	// 开启访问顺序参数 this.accessOrder = accessOrder;
    public LinkedHashMap(int initialCapacity,
                         float loadFactor,
                         boolean accessOrder) {
        super(initialCapacity, loadFactor);
        this.accessOrder = accessOrder;
    }

~ 基本方法解析

put

会直接会调用到HashMap的方法,所以效果与HashMap一样,但是到最后会有回调方法的调用,而回调方法实现在LinkedHashMap中。
那我们先来看看HashMap是在put方法什么情况下调用到LinkedHashMap的回调方法。

// 情况一
// 修改数据定位到的插槽已经存在一个或以上的节点
if (e != null) {
    V oldValue = e.value;
    if (!onlyIfAbsent || oldValue == null)
        e.value = value;
    // 回调方法
    afterNodeAccess(e);
    return oldValue;
}

// 情况二
// 插入新节点
++modCount;
if (++size > threshold)
    resize();
// 回调方法
afterNodeInsertion(evict);
return null;

那么我们根据以上HashMap的代码片段分别分析一下两种情况的回调方法作用。


// 开启访问顺序前提下,将节点放到最后(尾巴表示最年轻)并维护指针
void afterNodeAccess(Node<K,V> e) {
        LinkedHashMap.Entry<K,V> last;
        
        // 是否开启访问顺序,尾节点不是当前修改的节点
        // 都符合,将新插入节点放到链表尾部
        if (accessOrder && (last = tail) != e) {

			// p = 刚修改的节点
			// b = before, a = after
            LinkedHashMap.Entry<K,V> p =
                (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;

			// 既然要移动到最后那么下一个节点就是空
            p.after = null;
            
            // 当前节点是头节点,就把头节点指向下一个节点
            //【head】->【this】-> 【after】 ===》 【head】->【after】
            if (b == null)
                head = a;
                
            // 不是头节点将上一个节点指向下一个节点
            //【head】->【before】->【this】-> 【after】 ===》【head】->【before】->【after】
            else
                b.after = a;

			// 下一个节点不是尾节点,将它上一个指针指向before
			//【before】<-【after】
            if (a != null)
                a.before = b;

			// 是尾节点了
			// 【before】<-【last】
            else
                last = b;

			// 到这里就已经找到尾节点了,走else逻辑
            if (last == null)
                head = p;

			// 不是,将修改节点放到最后
            else {
                p.before = last;
                last.after = p;
            }

			// 将尾节点指向修改节点
            tail = p;
            ++modCount;
        }
    }
// 删除没有访问过节点,最老的
void afterNodeInsertion(boolean evict) { 
    LinkedHashMap.Entry<K,V> first;

	// evict在调用put方法时为true
	// first = head 拿到最老的节点
	// removeEldestEntry(first) 默认返回false
    if (evict && (first = head) != null && removeEldestEntry(first)) {
        K key = first.key;
        // 条件符合就删除这个节点
        removeNode(hash(key), key, null, false, true);
    }
}

这里我们已经把两种回调都看完了,注释标注的十分明了。

  • 在第二个回调方法中我们看到有一段逻辑判断中调用了某个方法,并默认返回false,protected boolean removeEldestEntry(Map.Entry<K,V> eldest),那么默认都返回false了那么后面的逻辑就都不走了呀。那写这个方法有什么意义呢?

首先我们看到方法用了 “protected“ 这个关键字,说明它是可以被我们自定义实现继承的(源码中一般 protected 标注的方法我们都是可以继承实现,留给我们扩展),那么我们实现这个方法的话就可以让它返回true,就可以继续执行后面的逻辑了。
我们知道HashMap可以扩容的非常大,并且没有对数据的内容大小限制之类的,就说明了HashMap存储的数据在宏观上是无限的,这样就会有内存溢出的风险。LinkedHashMap还维护了一个链表的结构,这样看是不是就很像一个缓存呢?而缓存为了处理老化数据和内存溢出的风险都会采取一些措施,比如lru等算法。所以我们实现这个方法可以实现一些lru的数据淘汰策略。
请添加图片描述

ok,我们两个方法都分析完成了,也知道了LinkedHashMap在HashMap的数据结构上对节点之间维护了一个另外的链表。是不是非常简单呢。


remove

这里和put()方法一样会调用HashMap的实现,最后进行回调方法的调用,而回调方法实现在LinkedHashMap中。
按例先看一下HashMap是如何调用到回调方法的。

// 找了了要删除的节点,根据节点的结构进行移除操作
if (node != null && (!matchValue || (v = node.value) == value ||
	(value != null && value.equals(v)))) {
	
    if (node instanceof TreeNode)
        ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
    else if (node == p)
        tab[index] = node.next;
    else
        p.next = node.next;
    ++modCount;
    --size;
    // 删除完后开始执行回调方法
    afterNodeRemoval(node);
    return node;
}

接着就是回调方法

void afterNodeRemoval(Node<K,V> e) { 
    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;
}

上面的回调就是将需要移除的节点从链表移除。


get

public V get(Object key) {
    Node<K,V> e;
    
    // 调用HashMap拿节点
    if ((e = getNode(hash(key), key)) == null)
        return null;

	// 如果开启顺序访问就将访问到的值放到最后
	// 意味着是最年轻的
    if (accessOrder)
        afterNodeAccess(e);
    return e.value;
}

非常简单


keySet

public Set<K> keySet() {
    Set<K> ks = keySet;
    if (ks == null) {
    	// LinkedHashMap中
        ks = new LinkedKeySet();
        keySet = ks;
    }
    return ks;
}


final class LinkedKeySet extends AbstractSet<K> {
    public final int size()                 { return size; }
    public final void clear()               { LinkedHashMap.this.clear(); }
    
	// 这里是重点我们直接看这个方法
    public final Iterator<K> iterator() {
        return new LinkedKeyIterator();
    }
    public final boolean contains(Object o) { return containsKey(o); }
    public final boolean remove(Object key) {
        return removeNode(hash(key), key, null, false, true) != null;
    }
    public final Spliterator<K> spliterator()  {
        return Spliterators.spliterator(this, Spliterator.SIZED |
                                        Spliterator.ORDERED |
                                        Spliterator.DISTINCT);
    }
    public final void forEach(Consumer<? super K> action) {
        if (action == null)
            throw new NullPointerException();
        int mc = modCount;

		// 遍历
        for (LinkedHashMap.Entry<K,V> e = head; e != null; e = e.after)
            action.accept(e.key);

		// modcount 判断前后变化
        if (modCount != mc)
            throw new ConcurrentModificationException();
    }
}

这里的 iterator 是重点,我们直接看它的代码。

// 有继承我们直接先看继承的类
final class LinkedKeyIterator extends LinkedHashIterator
        implements Iterator<K> {
    public final K next() { return nextNode().getKey(); }
}


abstract class LinkedHashIterator {

	// 这三个变量的ArrayList里面的基本一样,可以理解为一样
    LinkedHashMap.Entry<K,V> next;
    LinkedHashMap.Entry<K,V> current;
    int expectedModCount;

    LinkedHashIterator() {

		// next指向头节点
        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;

		// 里面会回调afterNodeRemoval() 来维护链表
        removeNode(hash(key), key, null, false, false);
        expectedModCount = modCount;
    }
}

迭代器的初始化中,next = head;这个代码说明我们遍历的是LinkedHashMap维护的链表


最后

没错到这里就将一些基础的源码看完了,对于树节点我们都放在后面看。
如果有些同学很难理解其中的代码,就需要先看一下前面的文章(HashMap)。
最后补一张LinkedHashMap的简单示意图,紫色箭头线就是LinkedHashMap自己维护的特殊指针。
请添加图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值