前面分析了HashMap的实现,我们知道其底层数据存储是一个hash表(数组+单向链表)。接下来我们看一下另一个LinkedHashMap,它是HashMap的一个子类,他在HashMap的基础上维持了一个双向链表(hash表+双向链表),在遍历的时候可以使用插入顺序(先进先出,类似于FIFO),或者是最近最少使用(LRU)的顺序。
     来具体看下LinkedHashMap的实现。
     
1.定义 
 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表(数组+单向链表) +双向链表的实现,到底实现方式是怎么样的,来看一下:
     /**
* 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中的有什么不同。
     /**
* 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.构造方法
 /**
* 构造一个指定初始容量和加载因子的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的构造方法:
 /**
* 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()方法的实现吧。

     /**
* 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。
     /**
* 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方法吧。
 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.是否包含
 /**
* 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效率要好一些。
 
8.遍历
 
 
     LinkedHashMap分析的也就差不多了,要理解LinkedHashMap一定要先对HashMap的实现有所理解,因为它是HashMap的一个子类。我们明白了,LinkedHashMap的两个主要作用,一个是可以实现FIFO、LUR策略的Cache功能,另一个就是提高了HashMap遍历的效率(其他功能的效率有所降低的。。。),当然LinkedHashMap的遍历会在Set中统一讲解
 
     在最后,让我们简单基于LInkedHashMap实现一个Cache功能吧,go!
 import java.util.LinkedHashMap;
import java.util.Map; public class MyLocalCache extends LinkedHashMap<String, Object> { private static final long serialVersionUID = 7182816356402068265L; private static final int DEFAULT_MAX_CAPACITY = 1024; private static final float DEFAULT_LOAD_FACTOR = 0.75f; private int maxCapacity; public enum Policy {
FIFO, LRU
} public MyLocalCache(Policy policy) {
super(DEFAULT_MAX_CAPACITY, DEFAULT_LOAD_FACTOR, Policy.LRU .equals(policy));
this.maxCapacity = DEFAULT_MAX_CAPACITY;
} public MyLocalCache(int capacity, Policy policy) {
super(capacity, DEFAULT_LOAD_FACTOR, Policy. LRU.equals(policy));
this.maxCapacity = capacity;
} @Override
protected boolean removeEldestEntry(Map.Entry<String, Object> eldest) {
return this.size() > maxCapacity;
} public static void main(String[] args) {
MyLocalCache cache = new MyLocalCache(5, Policy.LRU);
cache.put( "k1", "v1" );
cache.put( "k2", "v2" );
cache.put( "k3", "v3" );
cache.put( "k4", "v4" );
cache.put( "k5", "v5" );
cache.put( "k6", "v6" ); System. out.println("size=" + cache.size()); System. out.println("----------------------" );
for (Map.Entry<String, Object> entry : cache.entrySet()) {
    System. out.println(entry.getValue());
    } System. out.println("----------------------" ); System. out.println("k3=" + cache.get("k3")); System. out.println("----------------------" );
for (Map.Entry<String, Object> entry : cache.entrySet()) {
    System. out.println(entry.getValue());
    }
} }

参见:

给jdk写注释系列之jdk1.6容器(4)-HashMap源码解析

给jdk写注释系列之jdk1.6容器(6)-HashSet源码解析&Map迭代器

给jdk写注释系列之jdk1.6容器(5)-LinkedHashMap源码解析的更多相关文章

  1. 给jdk写注释系列之jdk1.6容器(7)-TreeMap源码解析

    TreeMap是基于红黑树结构实现的一种Map,要分析TreeMap的实现首先就要对红黑树有所了解.      要了解什么是红黑树,就要了解它的存在主要是为了解决什么问题,对比其他数据结构比如数组,链 ...

  2. 给jdk写注释系列之jdk1.6容器(6)-HashSet源码解析&Map迭代器

    今天的主角是HashSet,Set是什么东东,当然也是一种java容器了.      现在再看到Hash心底里有没有会心一笑呢,这里不再赘述hash的概念原理等一大堆东西了(不懂得需要先回去看下Has ...

  3. 给jdk写注释系列之jdk1.6容器(4)-HashMap源码解析

    前面了解了jdk容器中的两种List,回忆一下怎么从list中取值(也就是做查询),是通过index索引位置对不对,由于存入list的元素时安装插入顺序存储的,所以index索引也就是插入的次序. M ...

  4. 给jdk写注释系列之jdk1.6容器(12)-PriorityQueue源码解析

    PriorityQueue是一种什么样的容器呢?看过前面的几个jdk容器分析的话,看到Queue这个单词你一定会,哦~这是一种队列.是的,PriorityQueue是一种队列,但是它又是一种什么样的队 ...

  5. 给jdk写注释系列之jdk1.6容器(2)-LinkedList源码解析

    LinkedList是基于链表结构的一种List,在分析LinkedList源码前有必要对链表结构进行说明.   1.链表的概念      链表是由一系列非连续的节点组成的存储结构,简单分下类的话,链 ...

  6. 给jdk写注释系列之jdk1.6容器(1)-ArrayList源码解析

    工作中经常听到别人讲“容器”,各种各样的容器,话说到底什么是容器,通俗的讲“容器就是用来装东西的器皿,比如:水桶就是用来盛水的,水桶就是一个容器.” ok,在我们写程序的时候常常要对大量的对象进行管理 ...

  7. 给jdk写注释系列之jdk1.6容器(13)-总结篇之Java集合与数据结构

         是的,这篇blogs是一个总结篇,最开始的时候我提到过,对于java容器或集合的学习也可以看做是对数据结构的学习与应用.在前面我们分析了很多的java容器,也接触了好多种常用的数据结构,今天 ...

  8. 给jdk写注释系列之jdk1.6容器(11)-Queue之ArrayDeque源码解析

    前面讲了Stack是一种先进后出的数据结构:栈,那么对应的Queue是一种先进先出(First In First Out)的数据结构:队列.      对比一下Stack,Queue是一种先进先出的容 ...

  9. 给jdk写注释系列之jdk1.6容器(10)-Stack&Vector源码解析

    前面我们已经接触过几种数据结构了,有数组.链表.Hash表.红黑树(二叉查询树),今天再来看另外一种数据结构:栈.      什么是栈呢,我就不找它具体的定义了,直接举个例子,栈就相当于一个很窄的木桶 ...

随机推荐

  1. HDU 5828 Rikka with Sequence (线段树)

    Rikka with Sequence 题目链接: http://acm.hdu.edu.cn/showproblem.php?pid=5828 Description As we know, Rik ...

  2. hdu 3038 How Many Answers Are Wrong

    http://acm.hdu.edu.cn/showproblem.php?pid=3038 How Many Answers Are Wrong Time Limit: 2000/1000 MS ( ...

  3. HDU1025:Constructing Roads In JGShining's Kingdom(LIS)

    Problem Description JGShining's kingdom consists of 2n(n is no more than 500,000) small cities which ...

  4. python登录csdn并自动评论下载资源脚本

    功能 1.自动登录csdn 2.查找未评论的资源并自动评论 用到的库 1.python自带的requests,获取以及发送网页数据 2.python自带的time,用作休眠,csdn资源一段时间内只允 ...

  5. opennebula虚拟机启动失败

    问题1 故障现象: Wed Jan :: [DiM][I]: New VM state is ACTIVE. Wed Jan :: [LCM][I]: New VM state is PROLOG. ...

  6. CSS关键字

    1.initial initial 关键字用于设置 CSS 属性为它的默认值. initial 关键字可用于任何 HTML 元素上的任何 CSS 属性. 版本: CSS3 JavaScript 语法: ...

  7. mockjs学习总结(方便前端模拟数据,加快开发效率)

      基本介绍: 在我们前端开发中经常遇到这样的事情,接口没有写好,只能写静态页面,如何才能用很简单的方法模拟后端数据呢?mockjs就干了这件事,而且干的还挺好. 下面是我作为初学者的一些总结经验,期 ...

  8. 【不积跬步,无以致千里】VIM查找替换归纳总结zz

    http://spaces.msn.com/dingy/blog/cns!2F24B9E66A542581!327.entry VIM中常用的替换模式总结. 1,简单替换表达式 替换命令可以在全文中用 ...

  9. SAP标准教材列表

    AC010 mySAP Financials Overview to Financial Accounting and ReportingAC020 mySAP Financials Investme ...

  10. c# windowsForm打印

    在windows应用程序中文档的打印是一项非常重要的功能,在以前一直是一个非常复杂的工作,Microsoft .net Framework的打 印功能都以组件的方式提供,为程序员提供了很大的方便,但是 ...