你真的了解LinkedHashMap吗?进来看看

一、前言

LinkedHashMap 继承于 HashMap,因此,建议在学习本篇内容前,先学习 HashMap系列,这样使得更加容易理解。

二、LinkedHashMap使用

可能很多人会说,LinkedHashMap谁不会用?你真的确定你会用?

上例子之前,先写几个工具方法,以便后面理解方便:

public class Main {
    // 字符串左对齐(未考虑中英文长度,仅便于观看)
    private static String spaceFill(Object object) {
        return String.format("%-20s", object);
    }

    // 反射获取 HashMap 中的数组对象
    // 啥?你不知道数组对象名称?
    // 建议去看看源码,或是看看我写的 HashMap 系列
    private static Map.Entry<Integer, String>[] getArray(Object object, Field field) throws Exception {
        return (Map.Entry<Integer, String>[]) field.get(object);
    }

    // 反射获取 Map.Entry
    // 因为 HashMap.Node 是私有静态类
    private static Map.Entry<Integer, String> getObject(Object object, String name) throws Exception {
        Field field = getField(object.getClass(), name);
        return (Map.Entry<Integer, String>) field.get(object);
    }

    // 反射获取指定字段
    private static Field getField(Class<?> clazz, String name) {
        if (clazz == null) {
            return null;
        }

        Field field = null;
        try {
            field = clazz.getDeclaredField(name);
            field.setAccessible(true);

        } catch (NoSuchFieldException e) {
            field = getField(clazz.getSuperclass(), name);
        }
        return field;
    }
}

好了,上面的工具方法已经完成,后面的所有例子都会使用上面的方法。

我们来看两个例子:

2.1、例子一

public class Main {
    public static void main(String[] args) {
        // 一般大家都使用默认构造函数
        LinkedHashMap<Integer, String> map = new LinkedHashMap<>();
        map.put(1, "chris");
        map.put(2, "qingye");
        map.put(3, "24854015@qq.com");

        // 反射获取 table 数组对象
        Field field = getField(map.getClass(), "table");
        if (field != null && field.getType().isArray()) {
            try {
                // 类型强转
                Map.Entry<Integer, String>[] table = getArray(map, field);
                for (int i = 0; i < table.length; i ++) {
                    if (table[i] != null) {
                        // LinkedHashMap.Node 特有的 before & after 指针
                        Map.Entry<Integer, String> before = getObject(table[i], "before");
                        Map.Entry<Integer, String> after = getObject(table[i], "after");

                        if (before == null) {
                            System.out.print("This is Head ");
                        } else if (after == null) {
                            System.out.print("This is Tail ");
                        } else {
                            System.out.print("ttt ");
                        }
                        System.out.println("[" + i + "] => " + spaceFill(table[i]) + ", before => " + spaceFill(before) + ", after => " + spaceFill(after));
                    }
                }

            } catch (Exception e) {
            }
        }
    }
}

输出结果:

This is Head [1] => 1=chris             , before => null                , after => 2=qingye            
             [2] => 2=qingye            , before => 1=chris             , after => 3=24854015@qq.com   
This is Tail [3] => 3=24854015@qq.com   , before => 2=qingye            , after => null   

2.2、例子二

public class Main {

    public static void main(String[] args) {
        // 指定构造函数来初始化
        LinkedHashMap<Integer, String> map = new LinkedHashMap<>(16, 0.75f, true);
        map.put(1, "chris");
        map.put(2, "qingye");
        map.put(3, "24854015@qq.com");
        map.get(1); // 注意:这里仅仅 get 了一下

        Field field = getField(map.getClass(), "table");
        if (field != null && field.getType().isArray()) {
            try {
                Map.Entry<Integer, String>[] table = getArray(map, field);
                for (int i = 0; i < table.length; i ++) {
                    if (table[i] != null) {
                        Map.Entry<Integer, String> before = getObject(table[i], "before");
                        Map.Entry<Integer, String> after = getObject(table[i], "after");

                        if (before == null) {
                            System.out.print("This is Head ");
                        } else if (after == null) {
                            System.out.print("This is Tail ");
                        } else {
                            System.out.print("ttt ");
                        }
                        System.out.println("[" + i + "] => " + spaceFill(table[i]) + ", before => " + spaceFill(before) + ", after => " + spaceFill(after));
                    }
                }

            } catch (Exception e) {
            }
        }
    }
}

输出结果:

This is Tail [1] => 1=chris             , before => 3=24854015@qq.com   , after => null                
This is Head [2] => 2=qingye            , before => null                , after => 3=24854015@qq.com   
             [3] => 3=24854015@qq.com   , before => 2=qingye            , after => 1=chris   

WTF ?!发生了啥?怎么[1]元素从『Head』变成了『Tail』? 这就是 LinkedHashMap 的其妙之处!

三、深入解读 LinkedHashMap 的奥秘

3.1、LinkedHashMap 与 HashMap 的关系

本篇开头也说了,前者继承于后者,因此,LinkedHashMap 的 putXXX 和 getXXX 都直接继承于 HashMap,并没有重载,其 put & get 的逻辑也就和 HashMap 一样。HashMap 的 put & get 是啥逻辑?如果大家忘记了,建议去看看我写的 (四)HashMap系列:put元素(不看完将后悔一生!) 。既然是继承,同样的,它们的数组结构也就几乎一致(99%的一致):

数据结构:数组 + 链表 <---> 红黑树

那 1% 的不一致在哪?

3.2、Map.Entry

HashMap 的节点实现了 Map.Entry:

public class HashMap<K,V> extends AbstractMap<K,V>implements Map<K,V>, Cloneable, Serializable {

    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;

        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
       }

}

那 LinkedHashMap的节点呢?

public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V> {

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

}

额,真是简单的令人发指啊! 仅仅多了两个索引(指针)节点:before & after !

3.3、HashMap.TreeNode

之前再讲 HashMap 时,并没有提到 TreeNode 这个类型,因为涉及到 LinkedHashMap,所以有所调整,这里进行补充:

public class HashMap<K,V> extends AbstractMap<K,V>implements Map<K,V>, Cloneable, Serializable {

    static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
        TreeNode<K,V> parent;  // red-black tree links
        TreeNode<K,V> left;
        TreeNode<K,V> right;
        TreeNode<K,V> prev;    // needed to unlink next upon deletion
        boolean red;
        TreeNode(int hash, K key, V val, Node<K,V> next) {
            super(hash, key, val, next);
       }

}

完美的继承:

你真的了解LinkedHashMap吗?进来看看

3.4、before & after

根据这两个字段的名称,我们就能很容易的猜测出,分别指向前一个节点和后一个节点(事实上,我们开头的两个例子,已经证实了这种关系),那么,具体是如何在数据结构或者说,节点对象上表现出关联关系的呢?

你真的了解LinkedHashMap吗?进来看看

画图很辛苦...... LinkedHashMap 之所以加了 before 与 after 节点索引,主要目的:

  • 双向链表(即可以双向访问),从头结点可以一直遍历到最后一个节点,而不需要中途定位到桶;
  • 可以按照插入顺序来访问(默认),【例子一】;

四、LinkedHashMap 特殊之处

/**
 * The iteration ordering method for this linked hash map: true for access-order, false for insertion-order.
 */
final boolean accessOrder;

该字段含义:

  • true时,遍历双向链表中元素是按照访问顺序来遍历(LRU);
  • false时,遍历双向链表中的元素是按照插入顺序来遍历(默认);

Android系统中的 LRU 的实现,其实就是基于 LinkedHashMap,并采用了【例子二】的方式来初始化对象实例。

五、LinkedHashMap索引的源码实现

我们同样从 put元素还是分析源码,还记得我们分析 HashMap 的 putVal 时,有两个空函数么?

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
        ......
        else {
            ......
            if (e != null) { // 元素已经存在
                ......
                afterNodeAccess(e); // 调整节点顺序
                return oldValue;
            }
        }
        ......
        afterNodeInsertion(evict); // 新增元素,调整节点顺序
        return null;
    }
}

5.1、LinkedHashMap.afterNodeAccess

该方法会被多个其它方法触发,例如:put一个已存在的节点对象,get对象,replace对象等等,该方法就是判断是否需要调整的遍历访问顺序。

public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V> {
    void afterNodeAccess(Node<K,V> e) { // move node to last
        LinkedHashMap.Entry<K,V> last;

        // accessOrder = true,表示 LRU
        // 将要移动的节点已经在最后一个,那么就不用调整了
        if (accessOrder && (last = tail) != e) {
            // p 指向 e本身,b 指向 e前一个,a 指向 e后一个
            LinkedHashMap.Entry<K,V> p = (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
            p.after = null;

            // 如果 e的前一个为null,表示 e 是头节点,那么将头节点指向 e的下一个元素
            if (b == null)
                head = a;
            else
                b.after = a; // 否则 e的上一个元素,其下一个指向从 e 改为 e的下一个

            // a 为null 则表示 e 是尾节点
            // 否则将 e的下一个元素,其上一个指向从 e 改为 e的上一个
            if (a != null)
                a.before = b;
            else
                last = b; // 否则,尾节点指向 e 的上一个元素

            // 总之,上面判断:b,a 不为null,则互指 

            // 如果 last 为null,则表示整个 LinkedHashMap 只有一个元素
            if (last == null)
                head = p;
            else {
                // 否则,将 p 移至最后一个(访问顺序上,即逻辑上最后一个,实际位置不会变)
                p.before = last;
                last.after = p;
            }
            tail = p; // 最后,尾指向最后一个元素
            ++modCount;
        }
    }
}

5.2、LinkedHashMap.afterNodeInsertion

public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V> {
    // 新增节点后,调用此方法判断是否需要移除最老的节点
    // 因为 accessOrder = true 时,访问次数多的节点一定是在链尾,而访问次数最少的则在链头
    void afterNodeInsertion(boolean evict) { // possibly remove eldest
        LinkedHashMap.Entry<K,V> first;
        if (evict && (first = head) != null && removeEldestEntry(first)) {
            K key = first.key;
             // 如果需要移除最老的,则先从链头开始移除
             // 移除节点,调用的是 HashMap.removeNode 方法
            // 同样是从数组开始,再红黑树,再链表查找,删除,调整
            removeNode(hash(key), key, null, false, true);
        }
    }
}

5.3、LinkedHashMap.removeEldestEntry

public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V> {
    // 该方法在 LinkedHashMap 中,默认返回 false,即不移除最老的元素
    protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
        return false;
    }
}

那为啥我还一直提到 LRU 呢? 因为,我们如果自己基于 LinkedHashMap 来实现 LRU,就可以重载此方法,返回 true ,这样就达到了 LRU 的效果。

好了,LinkedHashMap 就聊到这里!

来源:https://www.tuicool.com/articles/AzieeqN

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容