LinkedHashMap源码

前面分析了HashMap的实现,我们知道其底层数据存储是一个hash表(数组+单向链表)。接下来我们看一下另一个LinkedHashMap,它是HashMap的一个子类,他在HashMap的基础上维持了一个双向链表(hash表+双向链表),在遍历的时候可以使用插入顺序(先进先出,类似于FIFO),或者是最近最少使用(LRU)的顺序。

来具体看下LinkedHashMap的实现。

1.定义 


1

2

3

public class LinkedHashMap<K,V>

    extends HashMap<K,V>

    implements Map<K,V>

  从定义可以看到LinkedHashMap继承于HashMap,且实现了Map接口。这也就意味着HashMap的一些优秀因素可以被继承下来,比如hash寻址,使用链表解决hash冲突等实现的快速查找,对于HashMap中一些效率较低的内容,比如容器扩容过程,遍历方式,LinkedHashMap是否做了一些优化呢。继续看代码吧。

2.底层存储

 

开篇我们说了LinkedHashMap是基于HashMap,并在其基础上维持了一个双向链表,也就是说LinkedHashMap是一个hash表(数组+单向链表) +双向链表的实现,到底实现方式是怎么样的,来看一下:


1

2

3

4

5

6

7

8

9

10

11

12

/**

     * The head of the doubly linked list.

     */

    private transient Entry<K,V> header ;

    /**

     * The iteration ordering method for this linked hash map: <tt>true</tt>

     * for access -order, <tt> false</tt> for insertion -order.

     *

     * @serial

     */

    private final boolean accessOrder;

看到了一个无比熟悉的属性header,它在LinkedList中出现过,英文注释很明确,是双向链表的头结点对不对。再看accessOrder这个属性,true表示最近较少使用顺序,false表示插入顺序。当然你说怎么没看到数组呢,别忘了LinkedHashMap继承于HashMap,Entry[]这个东东就不用写了吧。。。

再来看下Entry这个节点类和HashMap中的有什么不同。


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

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

/**

     * LinkedHashMap entry.

     */

    private static class Entry<K,V> extends HashMap.Entry<K,V> {

        // These fields comprise the doubly linked list used for iteration.

        // 双向链表的上一个节点before和下一个节点after

        Entry<K,V> before, after ;

       // 构造方法直接调用父类HashMap的构造方法(super)

       Entry( int hash, K key, V value, HashMap.Entry<K,V> next) {

            super(hash, key, value, next);

        }

        /**

         * 从链表中删除当前节点的方法

         */

        private void remove() {

            // 改变当前节点前后两个节点的引用关系,当前节点没有被引用后,gc可以回收

            // 将上一个节点的after指向下一个节点

            before.after = after;

            // 将下一个节点的before指向前一个节点

            after.before = before;

        }

        /**

         * 在指定的节点前加入一个节点到链表中(也就是加入到链表尾部)

         */

        private void addBefore(Entry<K,V> existingEntry) {

            // 下面改变自己对前后的指向

            // 将当前节点的after指向给定的节点(加入到existingEntry前面嘛)

            after  = existingEntry;

            // 将当前节点的before指向给定节点的上一个节点

            before = existingEntry.before ;

            // 下面改变前后最自己的指向

            // 上一个节点的after指向自己

            before.after = this;

            // 下一个几点的before指向自己

            after.before = this;

        }

        // 当向Map中获取查询元素或修改元素(put相同key)的时候调用这个方法

        void recordAccess(HashMap<K,V> m) {

            LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;

            // 如果accessOrder为true,也就是使用最近较少使用顺序

            if (lm.accessOrder ) {

                lm. modCount++;

                // 先删除,再添加,也就相当于移动了

                // 删除当前元素

                remove();

                // 将当前元素加入到header前(也就是链表尾部)

                addBefore(lm. header);

            }

        }

        // 当从Map中删除元素的时候调动这个方法

        void recordRemoval(HashMap<K,V> m) {

            remove();

        }

}

可以看到Entry继承了HashMap中的Entry,但是LinkedHashMap中的Entry多了两个属性指向上一个节点的before和指向下一个节点的after,也正是这两个属性组成了一个双向链表。等等。。。Entry还有一个继承下来的next属性,这个next是单向链表中用来指向下一个节点的,怎么回事嘛,怎么又是单向链表又是双向链表呢,都要晕了对不对,其实想的没错,这里的节点即是Hash表中的单向链表中的一个节点,它又是LinkedHashMap维护的双向链表中的一个节点,是不是瞬间觉得高大上了。图解一下吧(不要告诉我图好乱,我看不懂。。。)

注:黑色箭头指向表示单向链表的next指向,红色箭头指向表示双向链表的before指向,蓝色箭头指向表示双向链表的after指向。另外LinkedHashMap种还有一个header节点是不保存数据的,这里没有画出来。

从上图可以看出LinkedHashMap仍然是一个Hash表,底层由一个数组组成,而数组的每一项都是个单向链表,由next指向下一个节点。但是LinkedHashMap所不同的是,在节点中多了两个属性before和after,由这两个属性组成了一个双向循环链表(你怎么知道是循环,下面在说喽),而由这个双向链表维持着Map容器中元素的顺序。看下Entry中的recordRemoval方法,该方法将在节点被删除时候调用,Hash表中链表节点被正常删除后,调用该方法修正由于节点被删除后双向链表的前后指向关系,从这一点来看,LinkedHashMap比HashMap的add、remove、set等操作要慢一些(因为要维护双向链表 )

明白了LinkedHashMap的底层存储结构后,我们来看一下它的构造方法以及怎么样对链表进行初始化的。

3.构造方法


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

39

40

41

/**

     * 构造一个指定初始容量和加载因子的LinkedHashMap,默认accessOrder为false

     */

    public LinkedHashMap( int initialCapacity, float loadFactor) {

        super(initialCapacity, loadFactor);

        accessOrder = false;

    }

    /**

     * 构造一个指定初始容量的LinkedHashMap,默认accessOrder为false

     */

    public LinkedHashMap( int initialCapacity) {

        super(initialCapacity);

        accessOrder = false;

    }

    /**

     * 构造一个使用默认初始容量(16)和默认加载因子(0.75)的LinkedHashMap,默认accessOrder为false

     */

    public LinkedHashMap() {

        super();

        accessOrder = false;

    }

    /**

     * 构造一个指定map的LinkedHashMap,所创建LinkedHashMap使用默认加载因子(0.75)和足以容纳指定map的初始容量,默认accessOrder为false 。

     */

    public LinkedHashMap(Map<? extends K, ? extends V> m) {

        super(m);

        accessOrder = false;

    }

    /**

     * 构造一个指定初始容量、加载因子和accessOrder的LinkedHashMap

     */

    public LinkedHashMap( int initialCapacity,

                      float loadFactor,

                         boolean accessOrder) {

        super(initialCapacity, loadFactor);

        this.accessOrder = accessOrder;

}

构造方法很简单基本都是调用父类HashMap的构造方法(super),只有一个区别就是对于accessOrder的设定,上面的构造参数中多数都是将accessOrder默认设置为false,只有一个构造方法留了一个出口可以设置accessOrder参数。看完了构造方法,发现一个问题,咦?头部节点header的初始化跑哪里去了?

回忆一下,看看HashMap的构造方法:


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

/**

     * Constructs an empty <tt>HashMap</tt> with the specified initial

     * capacity and load factor.

     *

     * @param  initialCapacity the initial capacity

     * @param  loadFactor      the load factor

     * @throws IllegalArgumentException if the initial capacity is negative

     *         or the load factor is nonpositive

     */

    public HashMap( int initialCapacity, float loadFactor) {

        if (initialCapacity < 0)

            throw new IllegalArgumentException( "Illegal initial capacity: " +

                                               initialCapacity);

        if (initialCapacity > MAXIMUM_CAPACITY)

            initialCapacity = MAXIMUM_CAPACITY;

        if (loadFactor <= 0 || Float.isNaN(loadFactor))

            throw new IllegalArgumentException( "Illegal load factor: " +

                                               loadFactor);

        // Find a power of 2 >= initialCapacity

        int capacity = 1;

        while (capacity < initialCapacity)

            capacity <<= 1;

        this.loadFactor = loadFactor;

        threshold = (int)(capacity * loadFactor);

        table = new Entry[capacity];

        init();

    }

    /**

     * Initialization hook for subclasses. This method is called

     * in all constructors and pseudo -constructors (clone, readObject)

     * after HashMap has been initialized but before any entries have

     * been inserted.  (In the absence of this method, readObject would

     * require explicit knowledge of subclasses.)

     */

    void init() {

哦,明白了,init()在HashMap中是一个空方法,也就是给子类留的一个回调函数,ok,我们来看下LinkedHashMap对init()方法的实现吧。


1

2

3

4

5

6

7

8

9

10

/**

     * Called by superclass constructors and pseudoconstructors (clone,

     * readObject) before any entries are inserted into the map.  Initializes

     * the chain.

     */

    void init() {

        // 初始化话header,将hash设置为-1,key、value、next设置为null

        header = new Entry<K,V>(-1, null, null, null);

        // header的before和after都指向header自身

        header.before = header. after = header ;

init()方法看完了,看出点什么嘛?LinkedHashMap中维护的是个双向循环链表对不对?(什么?还不明白,去好好看看给jdk写注释系列之jdk1.6容器(2)-LinkedList源码解析

4.增加

 

LinkedHashMap没有重写put方法,只是重写了HashMap中被put方法调用的addEntry。


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

39

40

41

42

43

44

45

46

47

48

/**

     * This override alters behavior of superclass put method. It causes newly

     * allocated entry to get inserted at the end of the linked list and

     * removes the eldest entry if appropriate.

     */

    void addEntry( int hash, K key, V value, int bucketIndex) {

        // 调用createEntry方法创建一个新的节点

        createEntry(hash, key, value, bucketIndex);

        // Remove eldest entry if instructed, else grow capacity if appropriate

        // 取出header后的第一个节点(因为header不保存数据,所以取header后的第一个节点)

        Entry<K,V> eldest = header.after ;

        // 判断是容量不够了是要删除第一个节点还是需要扩容

        if (removeEldestEntry(eldest)) {

            // 删除第一个节点(可实现FIFO、LRU策略的Cache)

            removeEntryForKey(eldest. key);

        } else {

            // 和HashMap一样进行扩容

            if (size >= threshold)

                resize(2 * table.length );

        }

    }

    /**

     * This override differs from addEntry in that it doesn‘t resize the

     * table or remove the eldest entry.

     */

    void createEntry( int hash, K key, V value, int bucketIndex) {

        // 下面三行代码的逻辑是,创建一个新节点放到单向链表的头部

        // 取出数组bucketIndex位置的旧节点

        HashMap.Entry<K,V> old = table[bucketIndex];

        // 创建一个新的节点,并将next指向旧节点

       Entry<K,V> e = new Entry<K,V>(hash, key, value, old);

        // 将新创建的节点放到数组的bucketIndex位置

        table[bucketIndex] = e;

        // 维护双向链表,将新节点添加在双向链表header前面(链表尾部)

        e.addBefore( header);

        // 计数器size加1

        size++;

    }

    /**

     * 默认返回false,也就是不会进行元素删除了。如果想实现cache功能,只需重写该方法

     */

    protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {

        return false;

}

可以看到,在添加方法上,比HashMap中多了两个逻辑,一个是当Map容量不足后判断是删除第一个元素,还是进行扩容,另一个是维护双向链表。而在判断是否删除元素的时候,我们发现removeEldestEntry这个方法竟然是永远返回false,这什么鬼。。。哦,想了下,原来想要实现Cache功能,需要自己继承LinkedHashMap然后重写removeEldestEntry方法,这里默认提供的是容器的功能。

5.删除

LinkedHashMap没有重写remove方法,只是在实现了Entry类的recordRemoval方法,该方法是HashMap的提供的一个回调方法,在HashMap的remove方法进行回调,而LinkedHashMap中recordRemoval的主要当然是要维护双向链表了,返回上面去看下Entry类的recordRemoval方法吧。

6.查找

     LinkedHashMap重写了get方法,但是确复用了HashMap中的getEntry方法,LinkedHashMap是在get方法中指加入了调用recoreAccess方法的逻辑,recoreAccess方法的目的当然也是维护双向链表了,具体逻辑返回上面去看下Entry类的recoreAccess方法吧。


1

2

3

4

5

6

7

public V get(Object key) {

        Entry<K,V> e = (Entry<K,V>)getEntry(key);

        if (e == null)

            return null;

        e.recordAccess( this);

        return e.value ;

}

7.是否包含


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

/**

     * Returns <tt>true</tt> if this map maps one or more keys to the

     * specified value.

     *

     * @param value value whose presence in this map is to be tested

     * @return <tt> true</tt> if this map maps one or more keys to the

     *         specified value

     */

    public boolean containsValue(Object value) {

        // Overridden to take advantage of faster iterator

        // 遍历双向链表,查找指定的value

        if (value==null) {

            for (Entry e = header .after; e != header; e = e.after )

                if (e.value ==null)

                    return true;

        } else {

            for (Entry e = header .after; e != header; e = e.after )

                if (value.equals(e.value ))

                    return true;

        }

        return false;

 }

LinkedHashMap对containsValue进行了重写,我们在HashMap中说过,HashMap的containsValue需要遍历整个hash表,这样是十分低效的。而LinkedHashMap中重写后,不再遍历hash表,而是遍历其维护的双向链表,这样在效率上难道就有所改善吗?我们分析下:hash表是由数组+单向链表组成,而由于使用hash算法,可能会导致散列不均匀,甚至数组的有些项是没有元素的(没有hash出对应的散列值),而LinkedHashMap的双向链表呢,是不存在空项的,所以LinkedHashMap的containsValue比HashMap的containsValue效率要好一些。

时间: 2024-11-05 14:49:48

LinkedHashMap源码的相关文章

【Java集合源码剖析】LinkedHashmap源码剖析

LinkedHashMap简介 LinkedHashMap是HashMap的子类,与HashMap有着同样的存储结构,但它加入了一个双向链表的头结点,将所有put到LinkedHashmap的节点一一串成了一个双向循环链表,因此它保留了节点插入的顺序,可以使节点的输出顺序与输入顺序相同. LinkedHashMap可以用来实现LRU算法(这会在下面的源码中进行分析). LinkedHashMap同样是非线程安全的,只在单线程环境下使用. LinkedHashMap源码剖析 LinkedHashM

转:【Java集合源码剖析】LinkedHashmap源码剖析

转载请注明出处:http://blog.csdn.net/ns_code/article/details/37867985   前言:有网友建议分析下LinkedHashMap的源码,于是花了一晚上时间研究了下,分享出此文(这个系列的最后一篇博文了),希望大家相互学习.LinkedHashMap的源码理解起来也不难(当然,要建立在对HashMap源码有较好理解的基础上). LinkedHashMap简介 LinkedHashMap是HashMap的子类,与HashMap有着同样的存储结构,但它加

【源码】LinkedHashMap源码剖析

注:以下源码基于jdk1.7.0_11 之前的两篇文章通过源码分析了两种常见的Map集合,HashMap和Hashtable.本文将继续介绍另一种Map集合--LinkedHashMap. 顾名思义,LinkedHashMap除了是一个HashMap之外,还带有LinkedList的特点,也就是说能够保持遍历的顺序和插入的顺序一致,那么它是怎么做到的呢?下面我们开始分析. 首先看构造器. public class LinkedHashMap<K,V> extends HashMap<K,

LinkedHashMap 源码分析

LinkedHashMap 源码分析 1. 基本结构 1. 实现 实现的接口是 Map 2. 继承 ?? 继承的是 HashMap 这个就比较熟悉了,事实上我们会看到 LinkedHashMap 代码量非常的少,主要就是因为他继承的 HashMap ,继承了大多数的操作. 仔细一点的都会发现 HashMap 里面有非常多的空白方法,这些方法其实是模板方法,为了让继承 HashMap 的类重写一些自己的特性.而不破坏代码结构. 3. 数据域 1. 基本字段 ?? 在 HashMap 的基础上他添加

LRU、FIFO缓存实现以及LinkedHashMap源码

本篇将描述如何使用LinkedHashMap实现LRU以及FIFO缓存,并将从LinkedHashMap源码层面描述是如何实现这两种缓存的. 1.缓存描述 首先介绍一下FIFO.LRU两种缓存: FIFO(First In First out):先见先出,淘汰最先近来的页面,新进来的页面最迟被淘汰,完全符合队列. LRU(Least recently used):最近最少使用,淘汰最近不使用的页面. 2.代码实现 以下是通过LinkedHashMap实现两种缓存. public class Ca

LRU算法实现,HashMap与LinkedHashMap源码的部分总结

关于HashMap与LinkedHashMap源码的一些总结 JDK1.8之后的HashMap底层结构中,在数组(Node<K,V> table)长度大于64的时候且链表(依然是Node)长度大于8的时候,链表在转换为红黑树时,链表长度小于等于6时将不会进行转化为红黑树.目的是为了保证效率.其中链表的结点只有next,LinkedHashMap是在Entry<K,V>中添加before, after(双向链表的定义),保证可迭代,遍历时为存入顺序. 下面是LinkedHashMap

分析LinkedHashMap源码的LRU实现

一.前言 前段时间研究了memcached,而且操作系统的课程也刚刚完成,在两个里面多次出现LRU(last recently used最近最少使用)算法,虽然思想很简单.但是还是值得我们研究,无意间在看LinkedHashMap的源码的时候看见貌似这个类里面有默认的LRU实现.我们现在就来分析一下他的源代码 /** * Returns <tt>true</tt> if this map should remove its eldest entry. * This method i

Java进阶之----LinkedHashMap源码分析

最近事情有点多,今天抽出时间来看看LinkedHashMap的源码,其实一开始是想分析TreeMap来这,但是看了看源代码之后,决定还是等过几天再分析,原因是TreeMap涉及到了树的操作..而之前没有接触过树的这种数据结构,只是在学校学一点皮毛而已..所以我还是打算过几天先恶补一下相关的知识再来对TreeMap做分析. 言归正传,我们今天来看LinkedHashMap.从名字上我们可以看出来,这个对插入的值是保持顺序的,即我们插入的顺序就是我们输出的顺序,如果不相信,我们可以用HashMap和

LinkedHashMap源码及LRU算法应用

先来说说它的特点,然后在一一通过分析源码来验证其实现原理 1.能够保证插入元素的顺序.深入一点讲,有两种迭代元素的方式,一种是按照插入元素时的顺序迭代,比如,插入A,B,C,那么迭代也是A,B,C,另一种是按照访问顺序,比如,在迭代前,访问了B,那么迭代的顺序就是A,C,B,比如在迭代前,访问了B,接着又访问了A,那么迭代顺序为C,B,A,比如,在迭代前访问了B,接着又访问了B,然后在访问了A,迭代顺序还是C,B,A.要说明的意思就是不是近期访问的次数最多,就放最后面迭代,而是看迭代前被访问的时