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;
    }


























评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值