LinkedHashMap类似于HashMap,但是迭代遍历它时,取得“键值对”的顺序是插入次序,或者是最近最少使用(LRU)的次序。只比HashMap慢一点;而在迭代访问时反而更快,因为它使用链表维护内部次序(HashMap是基于散列表实现的,相关HashMap的内容可以看《Java集合类》《HashMap源码分析》)。

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

LinkedHashMap继承自HashMap并实现了Map接口。

LinkedHashMap只定义了两个属性:

 1 /**
2 * The head of the doubly linked list.
3 * 双向链表的头节点
4 */
5 private transient Entry<K,V> header;
6 /**
7 * The iteration ordering method for this linked hash map: true
8 * for access-order, false for insertion-order.
9 * true表示最近最少使用次序,false表示插入顺序
10 */
11 private final boolean accessOrder;

LinkedList一共提供了五个构造方法:

 1 // 构造方法1,构造一个指定初始容量和负载因子的、按照插入顺序的LinkedList
2 public LinkedHashMap(int initialCapacity, float loadFactor) {
3 super(initialCapacity, loadFactor);
4 accessOrder = false;
5 }
6 // 构造方法2,构造一个指定初始容量的LinkedHashMap,取得键值对的顺序是插入顺序
7 public LinkedHashMap(int initialCapacity) {
8 super(initialCapacity);
9 accessOrder = false;
10 }
11 // 构造方法3,用默认的初始化容量和负载因子创建一个LinkedHashMap,取得键值对的顺序是插入顺序
12 public LinkedHashMap() {
13 super();
14 accessOrder = false;
15 }
16 // 构造方法4,通过传入的map创建一个LinkedHashMap,容量为默认容量(16)和(map.zise()/DEFAULT_LOAD_FACTORY)+1的较大者,装载因子为默认值
17 public LinkedHashMap(Map<? extends K, ? extends V> m) {
18 super(m);
19 accessOrder = false;
20 }
21 // 构造方法5,根据指定容量、装载因子和键值对保持顺序创建一个LinkedHashMap
22 public LinkedHashMap(int initialCapacity,
23 float loadFactor,
24 boolean accessOrder) {
25 super(initialCapacity, loadFactor);
26 this.accessOrder = accessOrder;
27 }

从构造方法中可以看出,默认都采用插入顺序来维持取出键值对的次序。所有构造方法都是通过调用父类的构造方法来创建对象的。

LinkedHashMap是基于双向链表的,而且属性中定了一个header节点,为什么构造方法都没有对其进行初始化呢?

注意LinkedHashMap中有一个init()方法, HashMap的构造方法都调用了init()方法,这里LinkedHashMap的构造方法在调用父类构造方法后将从父类构造方法中调用init()方法(这也解释了为什么HashMap中会有一个没有内容的init()方法)。

1 void init() {
2 header = new Entry<K,V>(-1, null, null, null);
3 header.before = header.after = header;
4 }

看init()方法,的确是对header进行了初始化,并构造成一个双向循环链表(和LinkedList的存储结构是一样的)。

transfer(HashMap.Entry[] newTable)方法和init()方法一样也在HashTable中被调用。transfer(HashMap.Entry[] newTable)方法在HashMap调用resize(int newCapacity)方法的时候被调用。

1 void transfer(HashMap.Entry[] newTable) {
2 int newCapacity = newTable.length;
3 for (Entry<K,V> e = header.after; e != header; e = e.after) {
4 int index = indexFor(e.hash, newCapacity);
5 e.next = newTable[index];
6 newTable[index] = e;
7 }
8 }

根据链表节点e的哈希值计算e在新容量的table数组中的索引,并将e插入到计算出的索引所引用的链表中。

containsValue(Object value)

 1 public boolean containsValue(Object value) {
2 // Overridden to take advantage of faster iterator
3 if (value==null) {
4 for (Entry e = header.after; e != header; e = e.after)
5 if (e.value==null)
6 return true;
7 } else {
8 for (Entry e = header.after; e != header; e = e.after)
9 if (value.equals(e.value))
10 return true;
11 }
12 return false;
13 }

重写父类的containsValue(Object value)方法,直接通过header遍历链表判断是否有值和value相等,而不用查询table数组。

get(Object key)

1 public V get(Object key) {
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 }

get(Object key)方法通过HashMap的getEntry(Object key)方法获取节点,并返回该节点的value值,获取节点如果为null则返回null。recordAccess(HashMap<K,V> m)是LinkedHashMap的内部类Entry的一个方法,将在介绍Entry的时候进行详细的介绍。

clear()

1 public void clear() {
2 super.clear();
3 header.before = header.after = header;
4 }

clear()方法先调用父类的方法clear()方法,之后将链表的header节点的before和after引用都指向header自身,即header节点就是一个双向循环链表。这样就无法访问到原链表中剩余的其他节点,他们都将被GC回收。

以上的内容多多少少都涉及到了LinkedHashMap的内部类Entry<K,V>,下面详细介绍这个内部类。

 1 // 这是一个私有的、静态的内部类,继承自HashMap的Entry。
2 private static class Entry<K,V> extends HashMap.Entry<K,V> {
3 // 对前后节点的引用
4 Entry<K,V> before, after;
5 // 构造方法直接调用父类的构造方法
6 Entry(int hash, K key, V value, HashMap.Entry<K,V> next) {
7 super(hash, key, value, next);
8 }
9
10 // 移除该节点,只需修改前一节点的after引用和后一节点的before引用
11 private void remove() {
12 // 修改后该节点服务再被访问,会被GC回收
13 before.after = after;
14 after.before = before;
15 }
16
17 // 在指定节点之前插入当前节点(双向链表插入节点的过程)
18 private void addBefore(Entry<K,V> existingEntry) {
19 // 将当前节点的after引用指向existingEntry
20 after = existingEntry;
21 // 将before的引用指向existingEntry节点的前一节点
22 before = existingEntry.before;
23 // 将原先existingEntry节点的前一节点的after引用指向当前节点
24 before.after = this;
25 // 将原先existingEntry节点的后一节点的before引用指向当前节点
26 after.before = this;
27 }
28
29 // 当调用此类的get方法或put方法(put方法将调用到父类HashMap.Entry的put
30 // 方法)都将调用到recordAccess(HashMap<K,V> m)方法
31 // 如果accessOrder为true,即使用的是最近最少使用的次序,则将当前被修改的
32 // 节点移动到header节点之前,即链表的尾部。
33 // 这也是为什么在HashMap.Entry中有一个空的
34 // recordAccess(HashMap<K,V> m)方法的原因
35 void recordAccess(HashMap<K,V> m) {
36 LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
37 if (lm.accessOrder) {
38 lm.modCount++;
39 remove();
40 addBefore(lm.header);
41 }
42 }
43 // 和recordAccess(HashMap<K.V> m)方法一样,在HashMap.Entry中同样有一个对应的空方法。当进行删除(remove)操作的时候会被调用
44 void recordRemoval(HashMap<K,V> m) {
45 remove();
46 }
47 }

介绍完了内部类Entry,下面是创建一个Entry节点和添加一个Entry的两个方法。

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

1 void createEntry(int hash, K key, V value, int bucketIndex) {
2 HashMap.Entry<K,V> old = table[bucketIndex];
3 Entry<K,V> e = new Entry<K,V>(hash, key, value, old);
4 table[bucketIndex] = e;
5 e.addBefore(header);
6 size++;
7 }

createEntry(int hash,K key,V value,int bucketIndex)方法覆盖了父类HashMap中的方法。这个方法不会拓展table数组的大小。该方法首先保留table中bucketIndex处的节点,然后调用Entry的构造方法(将调用到父类HashMap.Entry的构造方法)添加一个节点,即将当前节点的next引用指向table[bucketIndex] 的节点,之后调用的e.addBefore(header)是修改链表,将e节点添加到header节点之前。

该方法同时在table[bucketIndex]的链表中添加了节点,也在LinkedHashMap自身的链表中添加了节点。

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

 1 void addEntry(int hash, K key, V value, int bucketIndex) {
2 createEntry(hash, key, value, bucketIndex);
3 Entry<K,V> eldest = header.after;
4 if (removeEldestEntry(eldest)) {
5 removeEntryForKey(eldest.key);
6 } else {
7 if (size >= threshold)
8 resize(2 * table.length);
9 }
10 }

首先调用createEntry(int hash,K key,V value,int bucketIndex)方法,之后获取LinkedHashMap中“最老”(最近最少使用)的节点,接着涉及到了removeEldestEntry(Entry<K,V> eldest)方法,来看一下:

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

为什么这个方法始终返回false?

结合上面的addEntry(int hash,K key,V value,int bucketIndex)方法,这样设计可以使LinkedHashMap成为一个正常的Map,不会去移除“最老”的节点。

为什么不在代码中直接去除这部分逻辑而是设计成这样呢?

这为开发者提供了方便,若希望将Map当做Cache来使用,并且限制大小,只需继承LinkedHashMap并重写removeEldestEntry(Entry<K,V> eldest)方法,像这样:

1 private static final int MAX_ENTRIES = 100;
2 protected boolean removeEldestEntry(Map.Entry eldest) {
3 return size() > MAX_ENTRIES;
4 }

LinkedHashMap除了以上内容外还有和迭代相关的三个方法及三个内部类以及一个抽象内部类,分别是:newKeyIterator()、newValueIterator()、newEntryIterator()和KeyIterator类、ValueIterator类、EntryIterator类以及LinkedHashIterator类。

三个new方法分别返回对应的三个类的实例。而三个类都继承自抽象类LinkedHashIterator。下面看迭代相关的三个类。

1 private class KeyIterator extends LinkedHashIterator<K> {
2 public K next() { return nextEntry().getKey(); }
3 }
4 private class ValueIterator extends LinkedHashIterator<V> {
5 public V next() { return nextEntry().value; }
6 }
7 private class EntryIterator extends LinkedHashIterator<Map.Entry<K,V>> {
8 public Map.Entry<K,V> next() { return nextEntry(); }
9 }

从上面可以看出这三个类都很简单,只有一个next()方法,next()方法也只是去调用LinkedHashIterator类中相应的方法。 和KeyIterator类、ValueIterator类、EntryIterator类以及LinkedHashIterator类。

下面是LinkedHashIterator类的内容。

 1 private abstract class LinkedHashIterator<T> implements Iterator<T> {
2 Entry<K,V> nextEntry = header.after;
3 Entry<K,V> lastReturned = null;
4 // 和LinkedList中ListItr类定义了expectedModCount用途一致
5 int expectedModCount = modCount;
6 // 下一个节点如果是header节点说明当前节点是链表的最后一个节点,即已经遍历完链表了,没有下一个节点了
7 public boolean hasNext() {
8 return nextEntry != header;
9 }
10 //移除上一次被返回的节点lastReturned
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 // 获取并记录返回的节点
27 Entry<K,V> e = lastReturned = nextEntry;
28 // 保存对下一个节点的引用
29 nextEntry = e.after;
30 return e;
31 }
32 }

LinkedHashMap本应和HashMap及LinkedList一起分析,比较他们的异同。为了弥补,这里简单的总结一些他们之间的异同:

HashMap使用哈希表来存储数据,并用拉链法来处理冲突。LinkedHashMap继承自HashMap,同时自身有一个链表,使用链表存储数据,不存在冲突。LinkedList和LinkedHashMap一样使用一个双向循环链表,但存储的是简单的数据,并不是“键值对”。所以HashMap和LinkedHashMap是Map,而LinkedList是一个List,这是他们本质的区别。LinkedList和LinkedHashMap都可以维护内容的顺序,但HashMap不维护顺序。

附上HashMap和LinkedList的源码分析,便于对比:

HashMap:http://www.cnblogs.com/hzmark/archive/2012/12/24/HashMap.html

LinkedList:http://www.cnblogs.com/hzmark/archive/2012/12/25/LinkedList.html

LinkedHashMap源码分析(基于JDK1.6)的更多相关文章

  1. HashMap 源码分析 基于jdk1.8分析

    HashMap 源码分析  基于jdk1.8分析 1:数据结构: transient Node<K,V>[] table;  //这里维护了一个 Node的数组结构: 下面看看Node的数 ...

  2. CopyOnWriteArrayList 源码分析 基于jdk1.8

    CopyOnWriteArrayList  源码分析: 1:成员属性: final transient ReentrantLock lock = new ReentrantLock();  //内部是 ...

  3. HashMap源码分析-基于JDK1.8

    hashMap数据结构 类注释 HashMap的几个重要的字段 hash和tableSizeFor方法 HashMap的数据结构 由上图可知,HashMap的基本数据结构是数组和单向链表或红黑树. 以 ...

  4. ArrayList 源码分析 基于jdk1.8:

    1:数据结构: transient Object[] elementData;  //说明内部维护的数据结构是一个Object[] 数组 成员属性: private static final int ...

  5. LinkedList的源码分析(基于jdk1.8)

    1.初始化 public LinkedList() { } 并未开辟任何类似于数组一样的存储空间,那么链表是如何存储元素的呢? 2.Node类型 存储到链表中的元素会被封装为一个Node类型的结点.并 ...

  6. ArrayList的源码分析(基于jdk1.8)

    1.初始化 transient Object[] elementData; //实际存储元素的数组 private static final Object[] DEFAULTCAPACITY_EMPT ...

  7. AtomicInteger源码分析——基于CAS的乐观锁实现

    AtomicInteger源码分析——基于CAS的乐观锁实现 1. 悲观锁与乐观锁 我们都知道,cpu是时分复用的,也就是把cpu的时间片,分配给不同的thread/process轮流执行,时间片与时 ...

  8. 并发-AtomicInteger源码分析—基于CAS的乐观锁实现

    AtomicInteger源码分析—基于CAS的乐观锁实现 参考: http://www.importnew.com/22078.html https://www.cnblogs.com/mantu/ ...

  9. Spring IoC 源码分析 (基于注解) 之 包扫描

    在上篇文章Spring IoC 源码分析 (基于注解) 一我们分析到,我们通过AnnotationConfigApplicationContext类传入一个包路径启动Spring之后,会首先初始化包扫 ...

  10. 断点调试/认证/权限/频率-源码分析/基于APIView编写分页/异常处理

    内容概要 断点调试 认证/权限/频率-源码分析 基于APIView编写分页 异常处理 断点调试 # 程序以 debug模式运行,可以在任意位置停下,查看当前情况下变量数据的变化情况 # pycharm ...

随机推荐

  1. SpringBoot定时任务实现数据同步

    业务的需求是,通过中台调用api接口获得,设备数据,要求现实设备数据的同步. 方案一:通过轮询接口的方式执行 pullData() 方法实现数据同步 该方式的原理是先清空之前的所有数据,然后重新插入通 ...

  2. containerd 拉取k8s.gcr.io/pause镜像i/o timeout

    由于k8s.gcr.io 需要连外网才可以拉取到,导致 k8s 的基础容器 pause 经常无法获取.k8s docker 可使用代理服拉取,再利用 docker tag 解决问题 docker pu ...

  3. 想好新年去哪了吗?合合信息扫描全能王用AI“留住”年味

    还有不到十天,除夕就要到了.近几年春节假期中,有人第一次带着孩子直击海面冰风,坐船回老家:也有人选择"漫游"国内外,在旅行中迎接新春的朝气.合合信息旗下扫描全能王APP通过AI扫描 ...

  4. 扫描全能王启动鸿蒙原生应用开发,系HarmonyOS NEXT智能扫描领域首批

    近期,"鸿蒙合作签约暨扫描全能王鸿蒙原生应用开发启动仪式"(简称"签约仪式")正式举行.合合信息与华为达成鸿蒙合作,旗下扫描全能王将基于HarmonyOS NE ...

  5. ASP.NET Core – DateTime, DateTimeOffset, DateOnly, TimeOnly, TimeSpan, TimeZone, NodaTime 使用基础

    前言 心血来潮,这篇讲点基础的东西. 对日期和时区 timezone 不熟悉的读者,请先看这篇 Time Zone, Leap Year, Date Format, Epoch Time 时区, 闰年 ...

  6. MySQL存储引擎:InnoDB与MyISAM

    InnoDB和MyISAM是MySQL数据库中两种常用的存储引擎,它们在数据存储结构.事务支持.锁的支持.外键支持.性能等方面存在显著的差异.下面将详细介绍这两种存储引擎的特点和优势. 什么是存储引擎 ...

  7. hive操作 -- zeppelin安装及配置

    当时写hive用的是zeppelin,这个工具可以直接在页面上写sql语句,操作服务器上的hive库,还挺方便的 通过zeppelin实现hive的查询结果的可视化 启动过程中会报错:User: to ...

  8. el-table 宽度自适应bug

    和 flex 一起使用的时候会有这个问题.只能自动变宽不能自动变窄. 在  flex-grow:1;  的那一层设置  overflow: auto;

  9. 墨天轮国产数据库沙龙 | 张晓庆:GoldenDB分布式数据库的自动安装与备份恢复

    在共同推进国产化生态发展的进程下,墨天轮正式推出"墨天轮国产数据库沙龙"系列直播活动,将定期邀请各国产数据库产品专家.掌门人,共同探讨如何达成技术"自主可控"的 ...

  10. kotlin类和对象—>接口

    1.接口定义,使用关键字interface 来定义接口 interface MyInterface { fun bar() fun foo() { // 可选的方法体 } } 2.实现接口,一个类和对 ...