LinkedHashMap底层实现原理与使用详解

LinkedHashMap底层实现原理与使用详解-mikechen

LinkedHashMap就是HashMap和双向链表合二为一的结果,本篇将详解LinkedHashMap的底层实现与使用详解@mikechen

LinkedHashMap概述

LinkedHashMap继承了HashMap类,它的多种操作都是建立在HashMap操作的基础上的,只是在原来的单链表的基础上改成了双向链表。

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

这样做的目的是为了让它能够实现插入数据的排序,也就是说如果遍历整个LinkedHashMap时,是会按照插入数据的顺序来遍历数据的。

 

LinkedHashMap的特点

  • 每个节点间由一个before引用 和 after 引用串联起来成为一个双向链表;
  • 链表节点按照访问时间进行排序,最近访问过的链表放在链表尾;
  • key和value都允许为空;
  • key重复会覆盖,value可以重复;
  • 有序的;
  • LinkedHashMap是非线程安全的。

 

LinkedHashMap数据结构

HashMap和双向链表的密切配合和分工合作造就了LinkedHashMap,如下图所示:

LinkedHashMap底层实现原理与使用详解-mikechen

LinkedHashMap是通过双向链表和散列表这两种数据结构组合实现的,这样在查询的时候就可以通过散列表来操作,遍历的时候通过双向链表来操作。

 

LinkedHashMap使用

LinkedHashMap<String,String> hm=new LinkedHashMap<String,String>();

//创建并添加元素
hm.put("1", "mike");
hm.put("2", "chen");
hm.put("3", "hello");
hm.put("4", "world");
hm.put("5", "java");
hm.put("6", "javaee");

//遍历
Set<String> set=hm.keySet();
for(String x:set){
    String value=hm.get(x);
    System.out.println(x+"---"+value);
}

输出结果为:

1---mike
2---chen
3---hello
4---world
5---java
6---javaee

 

LinkedHashMap底层原理

1.继承结构继承结构

LinkedHashMap底层实现原理与使用详解-mikechen

LinkedHashMap也就是继承HashMap,所以HashMap大都方法,它是可以直接使用的。

 

2.LinkedHashMap主要属性

//双向链表的头节点
  transient LinkedHashMap.Entry<K,V> head;
  
   //双向链表的尾节点
  transient LinkedHashMap.Entry<K,V> tail;

 //排序的规则,false按插入顺序排序,true访问顺序排序 
  final boolean accessOrder;
  
  //所以说accessOrder的作用就是控制访问顺序,设置为true后每次访问一个元素,就将该元素所在的Node变成最后一个节点,
  改变该元素在LinkedHashMap中的存储顺序。

 

3.LinkedHashMap构造函数

LinkedHashMap有五个构造器:

//用默认的初始容量和负载因子构建一个LinkedHashMap,取出键值对的方式是插入顺序
    public LinkedHashMap() {
        super();
        accessOrder = false;
    }

    //构造一个指定初始容量的LinkedHashMap,取得键值对的顺序
    public LinkedHashMap(int initialCapacity) {
        super(initialCapacity);
        accessOrder = false;
    }

    //构造一个指定初始容量和负载因子,按照插入顺序的LinkedHashMap
    public LinkedHashMap(int initialCapacity, float loadFactor) {
        super(initialCapacity, loadFactor);
        accessOrder = false;
    }

    //根据给定的初始容量,负载因子和键值对迭代顺序构建一个LinkedHashMap
    public LinkedHashMap(int initialCapacity,
                         float loadFactor,
                         boolean accessOrder) {
        super(initialCapacity, loadFactor);
        this.accessOrder = accessOrder;
    }

    //通过给定的map创建一个LinkedHashMap,负载因子是默认值,迭代方式是插入顺序.
    public LinkedHashMap(Map<? extends K, ? extends V> m) {
        super(m);
        accessOrder = false;
    }

从构造方法可以看出,默认都是采用插入顺序来维持取出键值对的次序.所有的构造方法都是通过父类的构造方法来建造对象的。

 

4.LinkedHashMap Entry

LinkedHashMap和HashMap的区别在于他们的基本数据机构上,我们来看一下LinkedHashMap的基本数据结构Entry,这个比较重要:

private static class Entry<K,V> extends HashMap.Entry<K,V> {
    //定义Entry类型的两个变量,或者称之为前后的两个指针    
    Entry<K,V> before, after;
    //构造方法与HashMap的没有区别,也是调用父类的Entry构造方法
    Entry(int hash, K key, V value, HashMap.Entry<K,V> next) {
            super(hash, key, value, next);
    }

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

    //插入节点到指定的节点之前
    private void addBefore(Entry<K,V> existingEntry) {
        after  = existingEntry;
        before = existingEntry.before;
        before.after = this;
        after.before = this;
    }

    //方法重写,HashMap中为空
    void recordAccess(HashMap<K,V> m) {
        LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
        if (lm.accessOrder) {
            lm.modCount++;
            remove();
            addBefore(lm.header);
        }
    }
    //方法重写 ,HashMap中方法为空
    void recordRemoval(HashMap<K,V> m) {
        remove();
    }
}

到这里已经可以知道,相对比HashMap,LinkedHashMap内部不光是使用HashMap中的哈希表来存储Entry对象。

另外维护了一个LinkedHashMapEntry,这些LinkedHashMapEntry内部又保存了前驱跟后继的引用,可以确定这是个双向链表。

而这个LinkedHashMapEntry提供了对象的增加删除方法都是去更改节点的前驱后继指向。

 

5.put()/get()方法

在LinkedHashMap类使用的仍然是父类HashMap的put方法,所以插入节点对象的流程基本一致。

不同的是,LinkedHashMap重写了afterNodeInsertion和afterNodeAccess方法。

public V get(Object key) {
    Node<K,V> e;
    if ((e = getNode(hash(key), key)) == null)
        return null;
    if (accessOrder)
        afterNodeAccess(e);
    return e.value;
}

afterNodeInsertion方法用于移除链表中的最旧的节点对象,也就是链表头部的对象。

void afterNodeInsertion(boolean evict) { // possibly remove eldest
    LinkedHashMap.Entry<K,V> first;
    if (evict && (first = head) != null && removeEldestEntry(first)) {
        K key = first.key;
        removeNode(hash(key), key, null, false, true);
    }
}

afterNodeAccess方法实现的逻辑,是把入参的节点放置在链表的尾部。

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

 

LinkedHashMap总结

LinkedHashMap 内部包含一个双向链表维护顺序,支持两种顺序——添加顺序,访问顺序。

默认就是按照添加顺序来的,如果要改成访问顺序的话,构造方法中的 accessOrder 需要设置成 true。这样,每次调用 get 方法,就会将刚刚访问的元素更新到链表尾部。

陈睿mikechen

10年+大厂架构经验,资深技术专家,就职于阿里巴巴、淘宝、百度等一线互联网大厂。

关注「mikechen」公众号,获取更多技术干货!

后台回复面试即可获取《史上最全阿里Java面试题总结》,后台回复架构,即可获取《阿里架构师进阶专题全部合集

评论交流
    说说你的看法