LinkedHashMap原理分析

LinkedHashMap原理分析

LinkedHashMap被称作“有序的Map”,作为对比HashMap被称作“无序的”。这里说的“有序”,“无序”到底什么意思?怎样实现的,来研究一下。

案例

在学习源码之前,先来看两个小测试案例。

@Test
public void testHashMap() {

    Map<String, String> map = new HashMap<>();
    map.put("caocao", "孟德");  
    map.put("liubei", "玄德");
    map.put("guanyu", "云长");
    map.put("zhaoyun", "子龙");
    map.put("zhouyu", "公瑾");
    map.put("guojia", "郭嘉"); 

    map.forEach((k, v) -> {
        System.out.println("key=" + k + ",value=" + v);
    });
}

运行结果:

key=guanyu,value=云长
key=guojia,value=郭嘉
key=liubei,value=玄德
key=caocao,value=孟德
key=zhaoyun,value=子龙
key=zhouyu,value=公瑾
@Test
public void testLinkedHashMap() {

    Map<String, String> map = new LinkedHashMap<>();
    map.put("caocao", "孟德");  
    map.put("liubei", "玄德");
    map.put("guanyu", "云长");
    map.put("zhaoyun", "子龙");
    map.put("zhouyu", "公瑾");
    map.put("guojia", "郭嘉"); 
    
    map.forEach((k, v) -> {
        System.out.println("key=" + k + ",value=" + v);
    });
}

运行结果:

key=caocao,value=孟德
key=liubei,value=玄德
key=guanyu,value=云长
key=zhaoyun,value=子龙
key=zhouyu,value=公瑾
key=guojia,value=郭嘉

先来解释一下有序,无序的意思,有序,无序通常指的是:遍历时能否按照插入的顺序遍历。对于HashMap来说,是无序的,插入的顺序与遍历的顺序不一致,上面的testHashMap()方法多次执行,结果都是一致的,所以无序并不是随机。LinkedHashMap插入顺序与遍历的顺序一致。

原理分析

我们先来分析HashMap无序的原理。HashMap的源码分析在这里HashMap源码分析

HashMap元素排列分析

画一图解释无序问题,下图为测试HashMap时插入元素的实际排布情况。

在这里插入图片描述
对照测试结果,可以看出实际的遍历顺序就是按照数组的索引顺序,以及链表的顺序遍历的。我们来看遍历的源码。

@Override
public void forEach(BiConsumer<? super K, ? super V> action) {
    Node<K,V>[] tab;
    if (action == null)
        throw new NullPointerException();
    if (size > 0 && (tab = table) != null) {
        int mc = modCount;
        for (int i = 0; i < tab.length; ++i) {  //遍历数组
            for (Node<K,V> e = tab[i]; e != null; e = e.next)  //如果存在链表结构,遍历链表
                action.accept(e.key, e.value);
        }
        if (modCount != mc)
            throw new ConcurrentModificationException();
    }
}

逻辑很清晰,与测试现象一致。

LinkedHashMap源码分析

再来看LinkedHashMap的实现源码。

LinkedHashMap继承了HashMap,为了实现有序,将每个节点保存了前一个,后一个节点的指针,组成双向链表结构。先来看几个关键字段。

static class Entry<K,V> extends HashMap.Node<K,V> {  //静态内部类,继承了HashMap.Node类,这时HashMap的节点类,增加了before, after两个指针
    Entry<K,V> before, after;
    Entry(int hash, K key, V value, Node<K,V> next) {
        super(hash, key, value, next);
    }
}

transient LinkedHashMap.Entry<K,V> head;  //链表头

transient LinkedHashMap.Entry<K,V> tail;  //链表尾

final boolean accessOrder;  //表示按照哪种顺序遍历,false:插入顺序   true:查询顺序,默认false,可以通过构造行数指定

LinkedHashMap并没有重写put方法,而是直接继承HashMap中的put方法,但是在HashMap方法中重写回调方法,实现类了元素的有序,我们来看HashMap的put源码,以及回调方法,研究具体的实现,put方法调用内部的putVal方法。

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;
    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))))
            e = p;
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    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)
                e.value = value;
            afterNodeAccess(e);  //按查询排列回调处理
            return oldValue;
        }
    }
    ++modCount;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);   //按插入顺序排列回调
    return null;
}

实例化新元素的方法,在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);  //创建新节点
    linkNodeLast(p);  //添加到链表尾部
    return p;
}

这里的关键逻辑是将新创建的元素添加到了链表的尾部,通过这里可以看出,LinkedHashMap是通过一个双向链表来保持插入元素的顺序的。这时按照上面的测试案例,LinkedHashMap内部会维护下图的链表结构。
在这里插入图片描述
下来看下插入元素后,回调的 afterNodeInsertion(evict); 方法,这个方法在HashMap为空实现,在LinkedHashMap中重写,我们直接看LinkedHashMap中的实现。

void afterNodeInsertion(boolean evict) { // possibly remove eldest
    LinkedHashMap.Entry<K,V> first;
    if (evict && (first = head) != null && removeEldestEntry(first)) {   //removeEldestEntry方法返回false,if永远不会成立
        K key = first.key;
        removeNode(hash(key), key, null, false, true);
    }
}

来看LinkedHashMap的遍历方法。

public void forEach(BiConsumer<? super K, ? super V> 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, e.value);
    if (modCount != mc)
        throw new ConcurrentModificationException();
}

看到这里应该明白了,LinkedHashMap的有序实现原理,但是还没结束,LinkedHashMap还有另外一种遍历顺序,当accessOrder为true时。我们改造下上面的测试案例代码。

@Test
public void testLinkedHashMap() {

    Map<String, String> map1 = new LinkedHashMap<>(16,0.75f,true);
    map.put("caocao", "孟德");  
    map.put("liubei", "玄德");
    map.put("guanyu", "云长");
    map.put("zhaoyun", "子龙");
    map.put("zhouyu", "公瑾");
    map.put("guojia", "郭嘉"); 
    
    map1.get("guanyu");
    
    map.forEach((k, v) -> {
        System.out.println("key=" + k + ",value=" + v);
    });
}

运行结果:

key=caocao,value=孟德
key=liubei,value=玄德
key=zhaoyun,value=子龙
key=zhouyu,value=公瑾
key=guojia,value=郭嘉
key=guanyu,value=云长

我们看到,顺序与插入顺序不一致了。这个测试方法,处理在构造器中将accessOrder设置为true,在第12行获取了一个元素。我们来看get方法的实现逻辑。

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

如果accessOrder为true会执行afterNodeAccess(e);

void afterNodeAccess(Node<K,V> e) { // move node to last
    LinkedHashMap.Entry<K,V> last;
    if (accessOrder && (last = tail) != e) {   //accessOrder为true才会执行下面的逻辑
        LinkedHashMap.Entry<K,V> p =
            (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
        p.after = null;
        if (b == null)
            head = a;
        else
            b.after = a;
        if (a != null)
            a.before = b;
        else
            last = b;
        if (last == null)
            head = p;
        else {
            p.before = last;
            last.after = p;
        }
        tail = p;
        ++modCount;
    }
}

这段代码实现的就是将查询到的元素移动到双向链表的尾部,这就是按照获取的获取的顺序遍历。在删除,修改中也存在类似的逻辑。

总结

LinkedHashMap是有序的,通过维护一个双向链表结构维护了元素的顺序,支持两种遍历顺序:按插入顺序遍历和按照查询顺序遍历。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值