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. 学习笔记:robots.txt文件

    1.1 介绍 robots.txt文件是一种用于指导搜索引擎爬虫在网站上哪些页面可以被抓取,哪些页面不应该被抓取的文本文件.这个文件通常放置在网站的根目录下. 1.2 由来 robots.txt标准最 ...

  2. JS 希尔排序完全理解

    希尔排序的思想直白点来说就是间隔对比,比如说 我有一个数组,长度为9,则第一次分割间隔为长度的1/3 + 1,则第一次对比就是1 比 4,2 比 5, 3 比 6,4 比 7,5 比 8 , 6 比 ...

  3. JavaScript – Async Iterator & Generator

    前言 要看懂这篇请先看下面几篇 JavaScript – Iterator JavaScript – Generator Function JavaScript – Promise JavaScrip ...

  4. Yarn 3.0 Plug'n'Play (PnP) 安装和迁移

    前言 以前用 npm, 后来 yarn 火了就用 yarn. 后来 yarn 2.0 大改版, Angular 不支持就一直没用. 一直到去年的 Angular 13 才开始支持. 最近又开始写 An ...

  5. JavaScript – ES Module

    前言 关于 JavaScript Modular 的多种版本和历史看这篇. 参考: 阮一峰 – Module 的语法 阮一峰 – Module 的加载实现 Export 语法 逐个 export 在想 ...

  6. 选择:Qt Creator、Qt Designer、Qt Design Studio,三种开发工具,您选择哪一种?

    前言 本文介绍了三种QT开发工具的特点,以及各自的优点和缺点. QT三种工具对比 QT提供的工具 Qt Creator: 扮演的是QT Widgets和QT QML的IDE的角色. 优点: 可以编写C ...

  7. /proc/zoneinfo

    root@pita2_mr813_tina35:/# cat /proc/zoneinfo Node 0, zone DMA per-node stats nr_inactive_anon 4749 ...

  8. AMBA总线协议(一)——一文看懂APB总线协议

    0.AMBA总线概括 AMBA(Advanced Microcontroller Bus Architecture) 总线是由ARM公司提出的一种开放性的片上总线标准,它独立于处理器和工艺技术,具有高 ...

  9. CDQ&整体二分-三维偏序(陌上花开)

    题面 本文讲cdq,整体二分的思路与做法.=分治VS数据结构 其实维度这一方面,空间几何可以是维度,像时间这样有规定顺序的词语也可能是维度. cdq 三维偏序,一般可以用一维一维的消.可以用cdq嵌套 ...

  10. GDOI绝望记——人生第一次省选普及

    时光匆匆,如白驹过隙. 转眼之间,我一在OI之路上走了2年半了.. 岁月不饶人,我却在不经意间饶了岁月. 自己到底是不是不如别人,这,是取决于自己的心态吧 Preface 人生中第一次去深圳(应该是吧 ...