java数据结构源码解读——LinkedHashMap

我们总说哈希表(HashMap)是以O(1)的时间插入,然后遍历时是无序的,也就是说,hash过后就不一定插在了数组什么位置了。这样导致我们无法保证获取到的键顺序。

但是LinkedHashMap就不一样,按照我们的理解,里面应该还有一个线性结构,来保存HashMap的插入顺序,以至于遍历时能够获得按插入顺序得到的顺序。

接下来我们就分析一下它的源码。

public class LinkedHashMap<K,V>
    extends HashMap<K,V>
    implements Map<K,V>
{
 //。。。
}

首先,我们可以看出实际上LinkedHashMap是HashMap的子类,那么按照继承的原则,只要看看哪些被修改了即可。

在我的.java文件(我的是jdk11)中,相比于HashMap的2400+行代码,LinkedHashMap的700+行代码显得更容易阅读。

下面是三个增加的字段:

transient LinkedHashMap.Entry<K,V> head;
transient LinkedHashMap.Entry<K,V> tail;
final boolean accessOrder;

因为大多数的数组之类的变量已经继承来了,所以本质上需要的head、tail不用说一定是链表的头、位,按照注释上的描述,这里面都是双端链表。

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();
        accessOrder = false;
        putMapEntries(m, false);
    }
    public LinkedHashMap(int initialCapacity,
                         float loadFactor,
                         boolean accessOrder) {
        super(initialCapacity, loadFactor);
        this.accessOrder = accessOrder;
    }

能够看出大多数都是把构造参数传递给父类的构造器而已。

那么,assessOrder是干什么用的呢?我们先不看源码,做个小实验:

    public static void main(String[] args){
        LinkedHashMap<String,Integer> map=new LinkedHashMap<>(16,0.75f,true);//默认assessOrder=false
        map.put("one",1);
        map.put("two",2);
        map.put("four",4);
        map.put("five",5);
        map.put("ten",10);
        System.out.println(map);
        map.get("five");
        System.out.println(map);
        map.get("ten");
        System.out.println(map);
        //{one=1, two=2, four=4, five=5, ten=10}
        //{one=1, two=2, four=4, ten=10, five=5}
        //{one=1, two=2, four=4, five=5, ten=10}

    }

然后等于false时:

    public static void main(String[] args){
        LinkedHashMap<String,Integer> map=new LinkedHashMap<>(16,0.75f,false);//默认assessOrder=false
        map.put("one",1);
        map.put("two",2);
        map.put("four",4);
        map.put("five",5);
        map.put("ten",10);
        System.out.println(map);
        map.get("five");
        System.out.println(map);
        map.get("ten");
        System.out.println(map);
        //{one=1, two=2, four=4, five=5, ten=10}
        //{one=1, two=2, four=4, five=5, ten=10}
        //{one=1, two=2, four=4, five=5, ten=10}


    }

哦天哪,好像与某个数据结构很像啊,似乎是。。。LRU?

对!assessOrder=true时,每次用get访问到的键都会被放到链表顺序的最末端,可以造成缓存的效果!

 

接下来看插入函数

 

 

。。。。。我们没有看到put函数!

说明LinkedHashMap的put完全与HashMap的一模一样,看来决定他的有序访问并不是插入时造成的,而是由get方法决定的。

 

get函数:

    public V get(Object key) {
        Node<K,V> e;
        if ((e = getNode(hash(key), key)) == null)//调用父类get
            return null;
        if (accessOrder)//开启LRU效果时候的动作——把访问到的结构放到末端
            afterNodeAccess(e);
        return e.value;
    }
    void afterNodeAccess(Node<K,V> e) { // move node to last
        LinkedHashMap.Entry<K,V> last;
        if (accessOrder && (last = tail) != e) {
            LinkedHashMap.Entry<K,V> p =
                (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
            p.after = null;//通过一系列指针操作把节点放到后面
            if (b == null)
                head = a;
            else
                b.after = a;
            if (a != null)
                a.before = b;
            else
                last = b;
            if (last == null)
                head = p;
            else {
                p.before = last;
                last.after = p;
            }
            tail = p;
            ++modCount;
        }
    }

 

那么假如说最常见的情况:无参数构造,然后多次插入/修改,是如何在遍历时得到一个有序的序列呢?

事实上遍历有序是由迭代器完成的,我们看一下里面的一个子类:

abstract class LinkedHashIterator {
        LinkedHashMap.Entry<K,V> next;//指向下一个节点
        LinkedHashMap.Entry<K,V> current;//指向当前节点
        int expectedModCount;

        LinkedHashIterator() {
            next = head;
            expectedModCount = modCount;//阻止并发修改的值
            current = null;
        }

        public final boolean hasNext() {
            return next != null;
        }

        final LinkedHashMap.Entry<K,V> nextNode() {
            LinkedHashMap.Entry<K,V> e = next;
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            if (e == null)
                throw new NoSuchElementException();
            current = e;
            next = e.after;
            return e;
        }

        public final void remove() {
            Node<K,V> p = current;
            if (p == null)
                throw new IllegalStateException();
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            current = null;
            removeNode(p.hash, p.key, null, false, false);
            expectedModCount = modCount;
        }
    }

那么我们不禁奇怪了,最开始head不是空的嘛,如果assessOrder=false那么似乎没有机会获得一个完整的已经连接好的链表

其实这里面连接的过程非常隐秘:

    private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
       //很显然,这是把节点接到末尾(保持有序)的方法
        LinkedHashMap.Entry<K,V> last = tail;
        tail = p;
        if (last == null)
            head = p;
        else {
            p.before = last;
            last.after = p;
        }
    }
    Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
        //覆盖了父类HashMap的newNode方法
        LinkedHashMap.Entry<K,V> p =
            new LinkedHashMap.Entry<>(hash, key, value, e);
        linkNodeLast(p);//在这个地方调用了上面的方法
        return p;
    }

所以这个过程是借用子类方法覆盖,创建新的节点时额外的利用指针把这些节点按顺序“串”起来。

所以:

没有用额外空间!

没有用额外空间!

没有用额外空间!

 

所以删除也不是咱们想象的,要搜索链表什么的,都是简单的指针操作。

迭代器也没有新建一个链表,而是利用

    static class Entry<K,V> extends HashMap.Node<K,V> {
        Entry<K,V> before, after;
        Entry(int hash, K key, V value, Node<K,V> next) {
            super(hash, key, value, next);
        }
    }

这个子类的before和after来迭代保持有序。

所以这个还真的是容易分析呢,也破除了原有的旧观点,这个就看出了继承的强大,使得那么复杂的LikedHashMap继承了HashMap的代码使得代码数量大大减少!

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值