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

最近事情有点多,今天抽出时间来看看LinkedHashMap的源码,其实一开始是想分析TreeMap来这,但是看了看源代码之后,决定还是等过几天再分析,原因是TreeMap涉及到了树的操作。。而之前没有接触过树的这种数据结构,只是在学校学一点皮毛而已。。所以我还是打算过几天先恶补一下相关的知识再来对TreeMap做分析。

言归正传,我们今天来看LinkedHashMap。从名字上我们可以看出来,这个对插入的值是保持顺序的,即我们插入的顺序就是我们输出的顺序,如果不相信,我们可以用HashMap和LinkedHashMap,按相同的顺序插入相同的值,最后看输出的结果,就可以知道他们的区别了。

我们首先来看LinkedHashMap的继承结构

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

我们可以看到,LinkedHashMap是直接继承了HashMap的,所以在一定程度上来说,他们两个是一样的。只不过LinkedHashMap重写了HashMap的一些方法。从而达到了输出有顺序的目的。

看我之前的一篇博文http://blog.csdn.net/zw0283/article/details/51177547 
大家应该对HashMap有一个大致的认识。而LinkedHashMap与HashMap在主要逻辑实现上并无差异,最大的不同,就是LinkedHashMap比HashMap多维护了一个链表,这个多出来的链表,就是存放我们插入顺序信息的。

这里我们在看一下LinkedHashMap的内部Entry实例的结构

有了上边的结构图,对下边源码的理解也更容易一些,好,我们开始分析LinkedHashMap的部分源码

构造方法分析

我们先来看构造方法

public LinkedHashMap(int initialCapacity, float loadFactor) {
        super(initialCapacity, loadFactor);
        accessOrder = false;
}

public LinkedHashMap(int initialCapacity) {
        super(initialCapacity);
        accessOrder = false;
}

public LinkedHashMap() {
        super();
        accessOrder = false;
}

public LinkedHashMap(Map<? extends K, ? extends V> m) {
        super(m);
        accessOrder = false;
}

public LinkedHashMap(int initialCapacity,
                         float loadFactor,
                         boolean accessOrder) {
        super(initialCapacity, loadFactor);
        this.accessOrder = accessOrder;
}

5个构造方法,也是够多的。。不过我们看到,大部分都是调用父类的构造方法,也就是HashMap的构造方法,这里我就不在赘述了,大家可以参考我上一篇博文。

我们还看到,在构造方法里多了一个boolean变量accessOrder,这是什么鬼?

看源码中的注释我们可以知道,这个变量是控制输出的顺序的,一共有两种顺序:

1、按插入顺序输出,类似于队列,先进去的先出来

2、按LRU顺序,何为LRU,就是最近最少使用,打个比方,我们插入值A、B、C、D,如果这样插入的话,那输出的时候就是A、B、C、D,看起来好像跟第一种没什么分别。那我们在测试,插入A、B、C、D、A,我们在输出的时候,发现输出变成了B、C、D、A。A为什么跑到后边去了?这是因为,A被插入了2次,而LRU是最近最少使用,所以A的使用频率要高于BCD,要将使用频率高的放到后边,使用频率小的放到前边。

还有一个不得不提的问题就是,在HashMap中,我们看到有一个空实现的init方法,这个方法在HashMap中没什么用,它的作用是留给子类覆盖的,也就是说,在LinkedhashMap构造方法中,调用super的构造方法后,还会调用自身的重写后的init方法,体现了Java的多态性。

我们来看看被重写后的init方法

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

大致就是构建了一个头结点,然后将改节点的前继和后继都指向自己,构成了一个单节点的双向链表。

我们再来看看Entry,即map的内部数据结构

private static class Entry<K,V> extends HashMap.Entry<K,V>{
    // 增加了前继和后继,构成双向链表,按插入顺序存储
    Entry<K,V> before, after;
}

根据上边的Entry结构图来看,应该更容易理解一些。

我们在前边已经知道了LinkedHashMap多了一个链表来保证输出顺序,我们就来看看LinkedHashMap是怎么实现的。

在看代码之前,我们先猜测一下这个LinkedHashMap的数据结构,有了数据结构图之后,相信理解代码也会十分容易。既然继承了HashMap,那整体结构肯定和HashMap一样了,只是细节上有变动,我们大胆猜测一下

上边的图是我参照网上的答案,并结合自己的想法画出来的,网上都是把上边这个图拆成了2个,画是好画了,但是很容易产生误导(反正对我来说有误导)我完全看不懂拆成2个之后的对应关系,索性我就直接自己用Visio画了一个,画的不好看,大家见谅。。

很容易看出来,细的蓝色箭头,是原来HashMap里本身就有的,而粗的箭头,则是LinkedHashMap新增了,每个Entry实例旁边的数字代表的是插入的顺序。我们可以想象一下,如果我们用左手捏住head节点,右手捏住任意节点,用力拽开,会发现这些Entry实例会变成一个环状结构(注意:其实在第6个Entry实例和Head节点处还有一个双向箭头,这里为了不引起混淆就没有画,但实际上是有的)就像这样。(大家注意,我下边这个其实是双向箭头,为了方便我就弄成了单向的。。)

配合上边几个图,相信大家应该对LinkedHashMap的结构有一个了解了,那我们现在注意来分析一下被LinkedHashMap重写的几个方法。

recordAccess方法分析

我们通过源码可以知道,LinkedHashMap并没有直接重写put方法,而是重写了put方法里调用的一些方法,笨一点的方法就是,在HashMap里put调用的每一个方法,都去LinkedhashMap里看一看有没有重写。。。(话说我就是这么找的。。)

// LinkedHashMap重写的方法,HashMap里为空实现
        void recordAccess(HashMap<K,V> m) {
            LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
            // 判断一下要用哪种顺序,LRU还是正常的顺序,LRU要做特别操作,而正常顺序留不需要操作了
            if (lm.accessOrder) {
                lm.modCount++;
                remove();
                addBefore(lm.header);
            }
}

我们跟进去看看remove方法

private void remove() {
       before.after = after;
       after.before = before;
}

这个不难理解,假设现在有ABC,B为当前对象,则第一句为将A的后继指向为B的后继,即C。第二句将B的后继C的前继设为B的前继,即A。所以最后变成了AC,当前对象B就被删除掉了。

为什么要删除呢?我们在来看看addBefore方法

private void addBefore(Entry<K,V> existingEntry) {
            after  = existingEntry;
            before = existingEntry.before;
            before.after = this;
            after.before = this;
        }

结合上边的图我们在来看这个代码,recordAccess调用addBefore传入的参数是当前Map的head节点,所以从这个方法名我们可以看出它要做的是将当前节点(this)加入到head之前,但是,别忘了,LinkedHashMap内部维护的是循环双向链表,所以加入到head之前,意思就是加到链表的结尾。(不知道有没有表述明白,反正我看的时候在这绕了好半天。。)

我用一张图为大家说明一下,在展示图之前,各位要先明白一些问题。就是这个recordAccess是在什么地方调用的?我们看HashMap的put方法可知,是在有重复key的时候才调用的。

/****************HashMap**************/
// 遍历该位置的链表,如果有重复的key,则将value覆盖
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

所以,现在很清楚了,当有重复的key时,相当于“使用频率”增加了,若使用普通的顺序,则不需要做什么,若使用LRU算法的话,就需要把使用频率高的放到后边,自然,使用频率小的就到前边了。所以才会有上边的recordAccess方法。

我们来看看图就明白了

transfer方法分析

transfer方法是当Entry数组需要扩容时调用的。我们来看源码中transfer方法的注释:

 /**
     * Transfers all entries to new table array.  This method is called
     * by superclass resize.  It is overridden for performance, as it is
     * faster to iterate using our linked list.
     */
    @Override
    void transfer(HashMap.Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        for (Entry<K,V> e = header.after; e != header; e = e.after) {
            if (rehash)
                e.hash = (e.key == null) ? 0 : hash(e.key);
            int index = indexFor(e.hash, newCapacity);
            e.next = newTable[index];
            newTable[index] = e;
        }
    }

重写这个方法的原因主要是为了优化,因为LinkedHashMap内部有一个链表,做查询的时候,相对于HashMap的遍历方式,重写后的遍历链表在效率上要高于原来的处理。不过做的事情都是一样的。将原来的数据转存到一个新的数组里。只不过遍历的方式不一样而已。

get方法分析

get方法相对来说就简单了许多,这里把源码列出,不在过多赘述,注意的是,取出操作也会触发链表位置的调整。

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;
    }
时间: 2024-10-29 19:09:49

Java进阶之----LinkedHashMap源码分析的相关文章

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

今天我们接着来看HashMap的源码,对几个常用的方法进行分析.在分析之前,我们还是要先对HashMap的结构有一个了解.看过之前我分析的ArrayList和LinkedList源码的朋友应该清楚,ArrayList内部是以数组实现的,LinkedList内部是以链表实现的.而HashMap则是对数组和链表的结合,虽然看上去复杂了一些,不过仔细分析一下,还是很好理解的.我们来看一张图片,是我根据我的理解画的. 我们在来看看Entry的内部结构是什么: 以上两个图,相信大家对HashMap的结构有

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

今天在看LinkedList的源代码的时候,遇到了一个坑.我研究源码时,发现LinkedList是一个直线型的链表结构,但是我在baidu搜索资料的时候,关于这部分的源码解析,全部都说LinkedList是一个环形链表结构..我纠结了好长时间,还以为我理解错了,最后还是在Google搜到了结果:因为我看的源码是1.7的而baidu出来的几乎全部都是1.6的.而且也没有对应的说明.在1.7之后,oracle将LinkedList做了一些优化,将1.6中的环形结构优化为了直线型了链表结构.这里要提示

Java集合之LinkedHashMap源码分析

简介 LinkedHashMap内部维护了一个双向链表,能保证元素按插入的顺序访问,也能以访问顺序访问,可以用来实现LRU缓存策略. LinkedHashMap可以看成是 LinkedList + HashMap. 继承体系 LinkedHashMap继承HashMap,拥有HashMap的所有特性,并且额外增加了按一定顺序访问的特性. 存储结构 我们知道HashMap使用(数组 + 单链表 + 红黑树)的存储结构,那LinkedHashMap是怎么存储的呢? 通过上面的继承体系,我们知道它继承

Java集合(13)--LinkedHashMap源码分析

HashMap使用哈希表来存储数据,并用拉链法来处理冲突.LinkedHashMap继承自HashMap,同时自身有一个链表,使用链表存储数据,不存在冲突. LinkedList和LinkedHashMap一样使用一个双向循环链表,但LinkedList存储的是简单的数据,并不是“键值对”. LinkedList和LinkedHashMap都可以维护内容的顺序,但HashMap不维护顺序. public class LinkedHashMap<K,V> extends HashMap<K

死磕 java集合之LinkedHashSet源码分析

问题 (1)LinkedHashSet的底层使用什么存储元素? (2)LinkedHashSet与HashSet有什么不同? (3)LinkedHashSet是有序的吗? (4)LinkedHashSet支持按元素访问顺序排序吗? 简介 上一节我们说HashSet中的元素是无序的,那么有没有什么办法保证Set中的元素是有序的呢? 答案是当然可以. 我们今天的主角LinkedHashSet就有这个功能,它是怎么实现有序的呢?让我们来一起学习吧. 源码分析 LinkedHashSet继承自HashS

java线程池ThreadPoolExector源码分析

java线程池ThreadPoolExector源码分析 今天研究了下ThreadPoolExector源码,大致上总结了以下几点跟大家分享下: 一.ThreadPoolExector几个主要变量 先了解下ThreadPoolExector中比较重要的几个变量.  corePoolSize:核心线程数量     maximumPoolSize:最大线程数量 allowCoreThreadTimeOut:是否允许线程超时(设置为true时与keepAliveTime,TimeUnit一起起作用)

There is no getter for property named &#39;*&#39; in &#39;class java.lang.String&#39;之源码分析

There is no getter for property named '*' in 'class java.lang.String',此错误之所以出现,是因为mybatis在对parameterType="String"的sql语句做了限制,假如你使用<when test="username != null">这样的条件判断时,就会出现该错误,不过今天我们来刨根问底一下. 一.错误再现 想要追本溯源,就需要错误再现,那么假设我们有这样一个sql查询

Java NIO——Selector机制源码分析---转

一直不明白pipe是如何唤醒selector的,所以又去看了jdk的源码(openjdk下载),整理了如下: 以Java nio自带demo : OperationServer.java   OperationClient.java(见附件) 其中server端的核心代码: public void initSelector() { try { selector = SelectorProvider.provider().openSelector(); this.serverChannel1 =

Java并发编程 ReentrantLock 源码分析

ReentrantLock 一个可重入的互斥锁 Lock,它具有与使用 synchronized 方法和语句所访问的隐式监视器锁相同的一些基本行为和语义,但功能更强大. 这个类主要基于AQS(AbstractOwnableSynchronizer)封装的 公平与非公平锁. 所谓公平锁就是指 在多个线程的争用下,这些锁倾向于将访问权授予等待时间最长的线程,换句话说也就是先被锁定的线程首先获得锁. 非公平锁正好相反,解锁时没有固定顺序. 让我们边分析源代码边学习如何使用该类 先来看一下构造参数,默认