JDK1.8源码逐字逐句带你理解LinkedHashMap底层

数据存储结构

我们已经知道HashMap是以散列表的形式存储数据的,LinkedHashMap继承了HashMap,所以LinkedHashMap其实也是散列表的结构,但是“linked”是它对HashMap功能的进一步增强,LinkedHashMap用双向链表的结构,把所有存储在HashMap中的数据连接起来。有人会说散列表不是已经有了链表的存储结构了嘛,为什么还要来个双向链表?桶(桶的概念就是数组的一个存储节点,比如说arr[0]是一个桶,arr[1]也是一个桶)中的链表和这个双向链表是两个概念,以下是我总结的区别:①桶中的链表是散列表结构的一部分;而双向链表是LinkedHashMap的额外引入;②桶中的链表只做数据存储,没有存储顺序的概念;双向链表的核心就是控制数据存储顺序(存储顺序是LinkedHashMap的核心);③桶中的链表产生是因为发生了hash碰撞,导致数据散落在一个桶中,用链表给予存储,所以这个链表控制了一个桶;双向链表是要串连所有的数据,也就是说有桶中的数据都是会被这个双向链表管理。

所以,我修改了HashMap的图片,大家参考下: 

所以,简单来说就是LinkedHashMap相比于HashMap来说就是多了这些红色的双向链表而已。

两种演示

LinkedHashMap的核心就是存在存储顺序和可以实现LRU算法,所以下面我会用两个demo先来证明这两种情况: 
①、放入到LinkedHashMap是有顺序的,会按照你放入的顺序存储:

package com.brickworkers;
import java.util.LinkedHashMap;

/**
 * @author Brickworker
 * Date:2017年4月12日下午12:46:25
 * 关于类LinkedHashMapTest.java的描述:jdk1.8逐字逐句带你理解linkedHashMap
 * Copyright (c) 2017, brcikworker All Rights Reserved.
 */
public class LinkedHashMapTest {

    public static void main(String[] args) {
        LinkedHashMap<Integer, Integer> map = new LinkedHashMap<Integer, Integer>();
        for (int i = 0; i < 10; i++) {//按顺序放入1~9
            map.put(i, i);
        }
        System.out.println("原数据:"+map.toString());
        map.get(3);
        System.out.println("查询存在的某一个:"+map.toString());
        map.put(4, 4);
        System.out.println("插入已存在的某一个:"+map.toString()); //直接调用已存在的toString方法,不然自己需要用迭代器实现
        map.put(10, 10);
        System.out.println("插入一个原本没存在的:"+map.toString());
    }

    //输出结果
//  原数据:{0=0, 1=1, 2=2, 3=3, 4=4, 5=5, 6=6, 7=7, 8=8, 9=9}
//  查询存在的某一个:{0=0, 1=1, 2=2, 3=3, 4=4, 5=5, 6=6, 7=7, 8=8, 9=9}
//  插入已存在的某一个:{0=0, 1=1, 2=2, 3=3, 4=4, 5=5, 6=6, 7=7, 8=8, 9=9}
//  插入一个原本没存在的:{0=0, 1=1, 2=2, 3=3, 4=4, 5=5, 6=6, 7=7, 8=8, 9=9, 10=10}

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

观察以上代码,其实它是符合先进先出的规则的,不管你怎么查询插入已存在的数据,不会对排序造成影响,如果有新插入的数据将会放在最尾部。

启用LRU规则的LinkedHashMap,启动这个规则需要在构造LinkedHashMap的时候,调用三个参数的构造器,这个构造器源码如下:

    /**
     * Constructs an empty <tt>LinkedHashMap</tt> instance with the
     * specified initial capacity, load factor and ordering mode.
     *
     * @param  initialCapacity the initial capacity
     * @param  loadFactor      the load factor
     * @param  accessOrder     the ordering mode - <tt>true</tt> for
     *         access-order, <tt>false</tt> for insertion-order
     * @throws IllegalArgumentException if the initial capacity is negative
     *         or the load factor is nonpositive
     */
    public LinkedHashMap(int initialCapacity,
                         float loadFactor,
                         boolean accessOrder) {
        super(initialCapacity, loadFactor);
        this.accessOrder = accessOrder;//是否开启LRU规则
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

第三个参数accessOrder就是用于控制LRU规则的。 
如下就是我写的demo:

package com.brickworkers;

import java.util.LinkedHashMap;

/**
 * @author Brickworker
 * Date:2017年4月12日下午12:46:25
 * 关于类LinkedHashMapTest.java的描述:jdk1.8逐字逐句带你理解linkedHashMap
 * Copyright (c) 2017, brcikworker All Rights Reserved.
 */
public class LinkedHashMapTest {

    public static void main(String[] args) {
        LinkedHashMap<Integer, Integer> map = new LinkedHashMap<Integer, Integer>(20, 0.75f, true);
        for (int i = 0; i < 10; i++) {//按顺序放入1~9
            map.put(i, i);
        }
        System.out.println("原数据:"+map.toString());
        map.get(3);
        System.out.println("查询存在的某一个:"+map.toString());
        map.put(4, 4);
        System.out.println("插入已存在的某一个:"+map.toString()); //直接调用已存在的toString方法,不然自己需要用迭代器实现
        map.put(10, 10);
        System.out.println("插入一个原本没存在的:"+map.toString());
    }

    //输出结果
//  原数据:{0=0, 1=1, 2=2, 3=3, 4=4, 5=5, 6=6, 7=7, 8=8, 9=9}
//  查询存在的某一个:{0=0, 1=1, 2=2, 4=4, 5=5, 6=6, 7=7, 8=8, 9=9, 3=3} //被访问(get)的3放到了最后面
//  插入已存在的某一个:{0=0, 1=1, 2=2, 5=5, 6=6, 7=7, 8=8, 9=9, 3=3, 4=4}//被访问(put)的4放到了最后面
//  插入一个原本没存在的:{0=0, 1=1, 2=2, 5=5, 6=6, 7=7, 8=8, 9=9, 3=3, 4=4, 10=10}//新增一个放到最后面

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34

从上面可以看出,每当我get或者put一个已存在的数据,就会把这个数据放到双向链表的尾部,put一个新的数据也会放到双向链表的尾部。

逐字逐句底层源码

接下来我们通过源码深入学习LinkedHash,同时解答上述出现的有趣的事情。

分析LinkedHashMap的类名和继承关系

public class LinkedHashMap<K,V>
    extends HashMap<K,V>
    implements Map<K,V>
{
  • 1
  • 2
  • 3
  • 4

从这里我们可以看出LinkedHashMap是继承了HashMap并实现了Map接口的,所以它和HashMap的关系肯定不一般。

分析LinkedHashMap的构造函数

//1
  public LinkedHashMap(int initialCapacity, float loadFactor) {
        super(initialCapacity, loadFactor);
        accessOrder = false;
    }
//2
  public LinkedHashMap(int initialCapacity) {
        super(initialCapacity);
        accessOrder = false;
    }
//3
   public LinkedHashMap() {
        super();
        accessOrder = false;
    }

//4
    public LinkedHashMap(Map<? extends K, ? extends V> m) {
        super();
        accessOrder = false;
        putMapEntries(m, false);
    }
//5
  public LinkedHashMap(int initialCapacity,
                         float loadFactor,
                         boolean accessOrder) {
        super(initialCapacity, loadFactor);
        this.accessOrder = accessOrder;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

它具有5个构造函数,可以设置容量和加载因子,且在默认情况下是不开启LRU规则的。同时它还以用具有继承K,V关系的map进行初始化。

分析双向链表

    /**
     * HashMap.Node subclass for normal LinkedHashMap entries.
     */
    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);
        }
    }

    /**
     * The head (eldest) of the doubly linked list.
     */
    transient LinkedHashMap.Entry<K,V> head;//双向链表头节点(最老)

    /**
     * The tail (youngest) of the doubly linked list.
     */
    transient LinkedHashMap.Entry<K,V> tail;//双向列表尾节点(最新)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

用一个静态内部类Entry表示双向链表,实现了HashMap中的Node内部类。before和after表示前后指针。我们在使用LinkedHashMap有序就是因此产生。

分析LRU规则实现,最近访问放在双向链表最后面

    void afterNodeAccess(Node<K,V> e) { // 把当前节点e放到双向链表尾部
        LinkedHashMap.Entry<K,V> last;
        //accessOrder就是我们前面说的LRU控制,当它为true,同时e对象不是尾节点(如果访问尾节点就不需要设置,该方法就是把节点放置到尾节点)
        if (accessOrder && (last = tail) != e) {
        //用a和b分别记录该节点前面和后面的节点
            LinkedHashMap.Entry<K,V> p =
                (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
             //释放当前节点与后节点的关系
            p.after = null;
            //如果当前节点的前节点是空,
            if (b == null)
            //那么头节点就设置为a
                head = a;
            else
            //如果b不为null,那么b的后节点指向a
                b.after = a;
            //如果a节点不为空
            if (a != null)
                //a的后节点指向b
                a.before = b;
            else
                //如果a为空,那么b就是尾节点
                last = b;
                //如果尾节点为空
            if (last == null)
            //那么p为头节点
                head = p;
            else {
            //否则就把p放到双向链表最尾处
                p.before = last;
                last.after = p;
            }
            //设置尾节点为P
            tail = p;
            //LinkedHashMap对象操作次数+1
            ++modCount;
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38

afterNodeAccess方法就是如何支持LRU规则的,如果在accessOrder为true的时候,节点调用这个函数,就会把该节点放置到最后面。put,get等都会调用这个函数来调整顺序,我手画了一张图来表示这个函数干了些什么: 

我们看看get方法中是否调用了此函数,以下是LinkedHashMap重写了HashMap的get方法:

    public V get(Object key) {
        Node<K,V> e;
        if ((e = getNode(hash(key), key)) == null)
            return null;
        if (accessOrder)//如果启用了LRU规则
            afterNodeAccess(e);//那么把该节点移到双向链表最后面
        return e.value;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

那么有些小伙伴就问了,那么put方法里面调用了嘛?肯定调用了,但是你不一定找得到,因为LinkedHashMap压根没有重写put方法,每次用LinkedHashMap的put方法的时候,其实你调用的都是HashMap的put方法。那为什么它也会执行afterNodeAccess()方法呢,因为这个方法HashMap就是存在的,但是没有实现,LinkedHashMap重写了afterNodeAccess()这个方法。下面给出HashMap的put局部方法:

 if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);//把当前节点移动到双向链表最尾处
                return oldValue;
            }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

其实这个方法在很多的调用都有,这里就不一一解释了。同时,LinkedHashMap对于红黑树的节点移动处理也有专门的方法,这里就不再深入讲解了,方式也是差不多。

分析一个LinkedHashMap自带的移除最头(最老)数据的方法

    protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
        return false;
    }
  • 1
  • 2
  • 3
  • 4

LinkedHashMap有一个自带的移除最老数据的方法,但是这个方法永远是返回false,但是如果我们要实现,可以在继承的时候重写这个方法,给定一个条件就可以控制存储在LinkedHashMap中的最老数据何时删除。具体的在我以前博文多种方式实现缓存机制中有写过。触发这个删除机制,一般是在PUT一个数据进入的时候,但是LinkedHashMap并没有重写Put方法如何实现呢?在LinekdHashMap中,这个方法被包含在afterNodeInsertion()方法之中,而这个方法是重写了HashMap的,但是HashMap中并没有去实现它,所以在put的时候就会触发删除这个机制。

尾记

技术是不断前进的,或许在JDK1.8的时候我写的这些是有用的,但是下一个版本出来就不一定了。比如说前面几个版本中,LinkedHashMap是一个循环的双向链表,而且需要用init()方法进行初始化,但是后来都被移除了,以下是部分原本的linkedHashMap源码:

 void init() {
        header = new Entry<K,V>(-1, null, null, null);
        header.before = header.after = header;  //循环的双向链表
    }  
  • 1
  • 2
  • 3
  • 4

所以,在日常的学习中,尤其是技术,要与时俱进,在查询某个技术点的时候,千万要注意版本号,不一样的版本之间可能是天差地别的。

原文地址:https://www.cnblogs.com/AndyAo/p/8119982.html

时间: 2024-11-10 00:25:10

JDK1.8源码逐字逐句带你理解LinkedHashMap底层的相关文章

Android AsyncTask完全解析,带你从源码的角度彻底理解

转载请注明出处:http://blog.csdn.net/guolin_blog/article/details/11711405 我们都知道,Android UI是线程不安全的,如果想要在子线程里进行UI操作,就需要借助Android的异步消息处理机制.之前我也写过了一篇文章从源码层面分析了Android的异步消息处理机制,感兴趣的朋友可以参考 Android Handler.Message完全解析,带你从源码的角度彻底理解 . 不过为了更加方便我们在子线程中更新UI元素,Android从1.

Android事件分发机制完全解析,带你从源码的角度彻底理解(上)

转载请注明出处:http://blog.csdn.net/guolin_blog/article/details/9097463 其实我一直准备写一篇关于Android事件分发机制的文章,从我的第一篇博客开始,就零零散散在好多地方使用到了Android事件分发的知识.也有好多朋友问过我各种问题,比如:onTouch和onTouchEvent有什么区别,又该如何使用?为什么给ListView引入了一个滑动菜单的功能,ListView就不能滚动了?为什么图片轮播器里的图片使用Button而不用Ima

[学习总结]7、Android AsyncTask完全解析,带你从源码的角度彻底理解

我们都知道,Android UI是线程不安全的,如果想要在子线程里进行UI操作,就需要借助Android的异步消息处理机制.之前我也写过了一篇文章从源码层面分析了Android的异步消息处理机制,感兴趣的朋友可以参考 Android Handler.Message完全解析,带你从源码的角度彻底理解 . 不过为了更加方便我们在子线程中更新UI元素,Android从1.5版本就引入了一个AsyncTask类,使用它就可以非常灵活方便地从子线程切换到UI线程,我们本篇文章的主角也就正是它了. Asyn

[学习总结]6、Android异步消息处理机制完全解析,带你从源码的角度彻底理解

开始进入正题,我们都知道,Android UI是线程不安全的,如果在子线程中尝试进行UI操作,程序就有可能会崩溃.相信大家在日常的工作当中都会经常遇到这个问题,解决的方案应该也是早已烂熟于心,即创建一个Message对象,然后借助Handler发送出去,之后在Handler的handleMessage()方法中获得刚才发送的Message对象,然后在这里进行UI操作就不会再出现崩溃了. 这种处理方式被称为异步消息处理线程,虽然我相信大家都会用,可是你知道它背后的原理是什么样的吗?今天我们就来一起

[转]Android事件分发机制完全解析,带你从源码的角度彻底理解(上)

Android事件分发机制 该篇文章出处:http://blog.csdn.net/guolin_blog/article/details/9097463 其实我一直准备写一篇关于Android事件分发机制的文章,从我的第一篇博客开始,就零零散散在好多地方使用到了Android事件分发的知识. 也有好多朋友问过我各种问题,比如:onTouch和onTouchEvent有什么区别,又该如何使用?为什么给ListView引入了一个滑动菜单的 功能,ListView就不能滚动了?为什么图片轮播器里的图

(转) Android事件分发机制完全解析,带你从源码的角度彻底理解(上)

转载请注明出处:http://blog.csdn.net/guolin_blog/article/details/9097463 其实我一直准备写一篇关于Android事件分发机制的文章,从我的第一篇博客开始,就零零散散在好多地方使用到了Android事件分发的知识.也有好多朋友问过我各种问题,比如:onTouch和onTouchEvent有什么区别,又该如何使用?为什么给ListView引入了一个滑动菜单的功能,ListView就不能滚动了?为什么图片轮播器里的图片使用Button而不用Ima

【转】Android事件分发机制完全解析,带你从源码的角度彻底理解(下)

转载请注明出处:http://blog.csdn.net/guolin_blog/article/details/9153761 记得在前面的文章中,我带大家一起从源码的角度分析了Android中View的事件分发机制,相信阅读过的朋友对View的事件分发已经有比较深刻的理解了. 还未阅读过的朋友,请先参考 Android事件分发机制完全解析,带你从源码的角度彻底理解(上) . 那么今天我们将继续上次未完成的话题,从源码的角度分析ViewGruop的事件分发. 首先我们来探讨一下,什么是View

AndroidEventBus源码解析带你深入理解EventBus(转载)

AndroidEventBus源码解析带你深入理解EventBus 转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/40920453,本文出自:[张鸿洋的博客] 上一篇带大家初步了解了EventBus的使用方式,详见:Android EventBus实战 没听过你就out了,本篇博客将解析EventBus的源码,相信能够让大家深入理解该框架的实现,也能解决很多在使用中的疑问:为什么可以这么做?为什么这么做不好呢? 1.概述 一般

Android 源码系列之&lt;十一&gt;从源码的角度深入理解AccessibilityService,打造自己的APP小外挂(下)

转载请注明出处:http://blog.csdn.net/llew2011/article/details/52843637 在上篇文章Android 源码系列之<十>从源码的角度深入理解AccessibilityService,打造自己的APP小外挂(上)中我们讲解了通过AccessibilityService实现自动安装APK小外挂的操作流程,如果你还没有看过上篇文章请点击这里.在这篇文章中我将带领小伙伴从源码的角度来深入学习一下AccessibilityServie的技术实现原理,希望这