LinkedHashMap源码分析

释放双眼,带上耳机,听听看~!

先来说说它的特点,然后在一一通过分析源码来验证其实现原理

1、能够保证插入元素的顺序。深入一点讲,有两种迭代元素的方式,一种是按照插入元素时的顺序迭代,比如,插入A,B,C,那么迭代也是A,B,C,另一种是按照访问顺序,比如,在迭代前,访问了B,那么迭代的顺序就是A,C,B,比如在迭代前,访问了B,接着又访问了A,那么迭代顺序为C,B,A,比如,在迭代前访问了B,接着又访问了B,然后在访问了A,迭代顺序还是C,B,A。要说明的意思就是不是近期访问的次数最多,就放最后面迭代,而是看迭代前被访问的时间长短决定。

2、内部存储的元素的模型。entry是下面这样的,相比HashMap,多了两个属性,一个before,一个after。next和after有时候会指向同一个entry,有时候next指向null,而after指向entry。这个具体后面分析,
同时,这里提醒一点,在这里的header一般指的是虚拟节点(不通过用户加入的节点,只是一种虚拟表示)。

LinkedHashMap源码分析

3、linkedHashMap和HashMap在存储操作上是一样的,但是LinkedHashMap多的东西是会记住在此之前插入的元素,这些元素不一定是在一个桶中,画个图。

LinkedHashMap源码分析

       也就是说,对于linkedHashMap的基本操作还是和HashMap一样,在其上面加了两个属性,也就是为了记录前一个插入的元素和记录后一个插入的元素。也就是只要和hashmap一样进行操作之后把这两个属性的值设置好,就OK了。注意一点,会有一个header的实体,目的是为了记录第一个插入的元素是谁,在遍历的时候能够找到第一个元素。
实际上存储的样子就像上面这个图一样,这里要分清楚哦。实际上的存储方式是和hashMap一样,但是同时增加了一个新的东西就是 双向循环链表。就是因为有了这个双向循环链表,LinkedHashMap才和HashMap不一样。

4、其他一些比如如何实现的循环双向链表,插入顺序和访问顺序如何实现的就看下面的详细讲解了。


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
1/**
2     * LinkedHashMap entry.
3     */
4    private static class Entry<K,V> extends HashMap.Entry<K,V> {
5        // These fields comprise the doubly linked list used for iteration.
6        //通过上面这句源码的解释,我们可以知道这两个字段,是用来给迭代时使用的,相当于一个双向链表,实际上用的时候,操作LinkedHashMap的entry和操作HashMap的Entry是一样的,只操作相同的四个属性,这两个字段是由linkedHashMap中一些方法所操作。所以LinkedHashMap的很多方法度是直接继承自HashMap。
7        //before:指向前一个entry元素。after:指向后一个entry元素
8        Entry<K,V> before, after;
9        //使用的是HashMap的Entry构造,调用父类构造器
10        Entry(int hash, K key, V value, HashMap.Entry<K,V> next) {
11            super(hash, key, value, next);
12        }
13        //删除当前的entry
14        private void remove() {
15            before.after = after;
16            after.before = before;
17        }
18        //一般传进来的都是header,把当前的entry(也就是this)插入到链表的末尾,同时更新一下四个引用关系
19        private void addBefore(Entry<K,V> existingEntry) {
20            after  = existingEntry;
21            before = existingEntry.before;
22            before.after = this;
23            after.before = this;
24        }
25        //在LRU算法里,一般很少访问的元素都会排在双向循环链表的靠前部分,而经常访问的元素一般都是在双向链表的靠后部分,而这些功能就是取决于该方法的。当前的entry被访问到时,我们调用此方法,先删除掉当前链表里的 this(就是entry),然后再通过addBefore方法把this重新添加到链表的尾部,并且更新一下四个引用(当然这都是addBefore里的过程了),这样,访问到的元素就转移到了末尾,达到了活跃更新的状态
26        void recordAccess(HashMap<K,V> m) {
27            LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
28            if (lm.accessOrder) {
29                lm.modCount++;
30                remove();
31                addBefore(lm.header);
32            }
33        }
34
35        void recordRemoval(HashMap<K,V> m) {
36            remove();
37        }
38    }
39

接下来就是常用方法操作了,例如put方法(该方法是继承于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
1public V put(K key, V value) {
2        if (table == EMPTY_TABLE) {
3            inflateTable(threshold);
4        }
5        if (key == null)
6            return putForNullKey(value);
7        int hash = hash(key);
8        int i = indexFor(hash, table.length);
9        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
10            Object k;
11            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
12                V oldValue = e.value;
13                e.value = value;
14                //如果该entry存在,直接改值,然后将该entry移到末尾
15                e.recordAccess(this);
16                return oldValue;
17            }
18        }
19
20        modCount++;
21        //注意,这里的addEntry方法是被LinkedHashMap覆盖了,所以调用的其实是LinkedHashMap的addEntry
22        addEntry(hash, key, value, i);
23        return null;
24    }
25

 

LinkedHashMap重写了void addEntry(int hash, K key, V value, int bucketIndex) 和void createEntry(int hash, K key, V value, int bucketIndex)


1
2
3
4
5
6
7
8
9
1void addEntry(int hash, K key, V value, int bucketIndex) {         //先调用父类的addEntry,把元素添加进哈希链表中
2        super.addEntry(hash, key, value, bucketIndex);
3        // Remove eldest entry if instructed         //这里的header是虚拟节点,他的after是真正的最老的那个节点
4        Entry<K,V> eldest = header.after;
5        if (removeEldestEntry(eldest)) {
6            removeEntryForKey(eldest.key);
7        }
8    }
9

我们创建一个entry,因为是新创建的,我们先计算出该entry在数组中的位置,然后把它添加到链表的第一个节点,这时候在双向链表中,新建的entry应该添加到双向链表的末尾,因为新创建是最新鲜的,所以我们应该移到末尾,如上图操作。


1
2
3
4
5
6
7
8
1void createEntry(int hash, K key, V value, int bucketIndex) {
2        HashMap.Entry<K,V> old = table[bucketIndex];
3        Entry<K,V> e = new Entry<>(hash, key, value, old);
4        table[bucketIndex] = e;         //把最新鲜的元素e转到双向链表的尾部
5        e.addBefore(header);
6        size++;
7    }
8

其实到这里,LinkedHashMap的常用操作流程已经差不多了

5、来看看迭代器的使用。对双向循环链表的遍历操作。但是这个迭代器是abstract的,不能直接被对象所用,但是能够间接使用,就是通过keySet().interator(),就是使用的这个迭代器


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
1//主要是针对双向循环链表的遍历操作 private abstract class LinkedHashIterator<T> implements Iterator<T> {         // 要返回的值
2        Entry<K,V> nextEntry = header.after;
3        Entry<K,V> lastReturned = null;
4
5        int expectedModCount = modCount;
6        // 只要nextEntry不是指向头节点,代表有元素
7        public boolean hasNext() {
8            return nextEntry != header;
9        }
10
11        public void remove() {
12            if (lastReturned == null)
13                throw new IllegalStateException();
14            if (modCount != expectedModCount)
15                throw new ConcurrentModificationException();
16            LinkedHashMap.this.remove(lastReturned.key);
17            lastReturned = null;
18            expectedModCount = modCount;
19        }
20
21        Entry<K,V> nextEntry() {
22            if (modCount != expectedModCount)
23                throw new ConcurrentModificationException();
24            if (nextEntry == header)
25                throw new NoSuchElementException();
26            // 记录lastReturned,更新nextEntry,返回上一个nextEntry
27            Entry<K,V> e = lastReturned = nextEntry;
28            nextEntry = e.after;
29            return e;
30        }
31    }
32

6、我们再来看看LinkedHashMap是如何实现访问顺序和插入顺序一致的,其实就是通过双向链表,我们找到LinkedHashMap的get方法,发现竟然是属于自己的


1
2
3
4
5
6
7
8
1public V get(Object key) {         // getEntry方法是调用的父类的
2        Entry<K,V> e = (Entry<K,V>)getEntry(key);
3        if (e == null)
4            return null;
5        e.recordAccess(this);
6        return e.value;
7    }
8

       之所以该Map可以达到访问顺序和插入顺序一致,双向链表功不可没,但是如果lm.accessOrder为true的话,这时候顺序就是一个全新的顺序,因为访问的顺序是不怎么访问的元素靠前,而经常访问的元素靠末尾。


1
2
3
4
5
6
7
8
9
1void recordAccess(HashMap<K,V> m) {
2            LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
3            if (lm.accessOrder) {
4                lm.modCount++;
5                remove();
6                addBefore(lm.header);
7            }
8        }
9

总结:

(1)LinkedHashMap和HashMap在数组链表存储时都是一样的

(2)LinkedHashMap的老元素靠前,新元素靠后,取决于accessOrder属性和recordAccess方法

(3)
LinkedHashMap是HashMap的子类,实现的原理跟HashMap差不多,唯一的区别就是LinkedHashMap多了一个双向循环链表。
因为有双向循环列表,所以LinkedHashMap能够记录插入元素的顺序,而HashMap不能

 

给TA打赏
共{{data.count}}人
人已打赏
安全经验

Google Adsense的技巧、诀窍和秘密

2021-10-11 16:36:11

安全经验

安全咨询服务

2022-1-12 14:11:49

个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索