【Java集合】LinkedHashMap源码详解

目录

一、介绍

1.1 LinkedHashMap简介

1.2 继承关系

二、数据结构

2.1 成员属性

2.2 内部类

2.3 存储结构

三、源码分析

3.1 构造方法

3.2 LinkedHashMap的init()方法

3.3 维护链表的操作

3.3.1 afterNodeRemoval(Node e)方法,v>

3.3.2 afterNodeInsertion(boolean evict)方法

3.3.3 afterNodeAccess(Node e)方法,v>

3.4 get操作

3.4 put操作

3.5 remove操作

3.6 扩容操作

四、LinkedListMap与LRU(Least recently used,最近最少使用)算法

4.1 利用LinkedListMap实现LRU

五、总结


一、介绍

1.1 LinkedHashMap简介

LinkedHashMap 继承自 HashMap,在 HashMap 基础上,通过维护一条双向链表,解决了 HashMap 不能随时保持遍历顺序和插入顺序一致的问题。能保证元素按插入的顺序访问,也能以访问顺序访问,在一些场景下,该特性很有用,比如实现LRU缓存策略。

在实现上,LinkedHashMap 很多方法直接继承自 HashMap,仅为维护双向链表覆写了部分方法。LinkedHashMap可以看成是 LinkedList + HashMap。

当我们希望有顺序地去存储key-value时,就需要使用LinkedHashMap了。下文是基于JDK1.6的实现,实际上JDK1.8对其进行了改动,但实际上只是HashMap在jdk1.8版本有了较大变动,LinkedHashMap的实现原理还是没有太大变化的。

1.2 继承关系

方法原型:

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

LinkedHashMap继承自HashMap,它的多种操作都是建立在HashMap操作的基础上的。同HashMap不同的是,LinkedHashMap维护了一个Entry的双向链表,保证了插入的Entry中的顺序。这也是Linked的含义。继承关系图如下:

LinkedHashMap继承HashMap,拥有HashMap的所有特性,并且额外增加了按一定顺序访问的特性。

二、数据结构

2.1 成员属性

/**
* 双向链表头节点,旧数据存在头节点。
*/
transient LinkedHashMap.Entry<K,V> head;

/**
* 双向链表尾节点,新数据存在尾节点。 
*/
transient LinkedHashMap.Entry<K,V> tail;

/**
* 是否按访问顺序排序。默认为false按插入顺序存储元素,如果是true则按访问顺序存储元素(按照访问顺序迭代,支持实现LRU算法时)。
* LinkedHashMap可以维持插入的顺序,但是这个顺序并不是不变的,它可以被get操作打乱。
* 该变量控制了是否在get操作后将刚刚get到的节点重新移动到链表的尾部
*/
final boolean accessOrder;

其他的成员属性就是直接继承HashMap中的。

2.2 内部类

// 位于LinkedHashMap中的内部类。存储数据的节点,继承自HashMap的Node类,next用于单链表结构维护,是继承自HashMap.Node的,before和after用于双向链表存储所有元素,是LinkedHashMap新加入的。
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);
    }
}

我们来看一下HashMap中的Node内部类,LinkedHashMap中的内部类Entry就是继承自它。

// 位于HashMap中的Node类。HashMap中的Node类是继承的Map.Entry<K, V>
static class Node<K, V> implements Map.Entry<K, V> {
    final int hash;
    final K key;
    V value;
    Node<K, V> next;
}

在LinkedHashMap中,存储数据的节点就是Entry类(LinkedHashMap.Entry<K, V>),这个类继承了HashMap的Node类,在此基础上又加了before和after这两个指针用来维护双向链表结构。

  • before、after用于维护Entry插入的先后顺序
  • next用于维护HashMap各个桶中Entry的连接顺序

2.3 存储结构

我们知道HashMap使用(数组 + 单链表 + 红黑树)的存储结构,那LinkedHashMap是怎么存储的呢?

通过上面的继承体系,我们知道它继承了HashMap,所以它的内部也有这三种结构,但是它还额外添加了一种“双向链表”的结构存储所有元素的顺序。

添加删除元素的时候需要同时维护在HashMap中的存储,也要维护在LinkedList中的存储,所以性能上来说会比HashMap稍慢。

注意:JDK1.8中引入了红黑树,如下:

上图中,淡蓝色的箭头表示前驱引用,红色箭头表示后继引用。每当有新键值对节点插入,新节点最终会接在 tail 引用指向的节点后面。而 tail 引用则会移动到新的节点上,这样一个双向链表就建立起来了。

通过上面的图我们可以知道,LinkedHashMap首先内部数据都是完全按照HashMap的组织方式来存储的(数组 + 单链表 + 红黑树),这是因为LinkedHashMap继承了HashMap,然后LinkedHashMap在此基础上,将存储数据的每个节点又用一个双向链表串联了起来,这样就可以按照顺序来遍历每一个节点了。所以LinkedHashMap存储数据的节点中,既需要有维护挂在数组上的单链表结构的next指针,又需要有将所有节点按照一定顺序串连起来构成双链表的after、before两个指针。

LinkedHashMap和HashMap的Entry结构图:

三、源码分析

3.1 构造方法

相比于Hashmap,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;
}

// 传入的参数是一个Map的集合,调用父类的构造方法,构造一个与指定Map具有相同映射的 LinkedHashMap,取得键值对的顺序是插入顺序
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;
}

前四个构造方法accessOrder都等于false,说明LinkedHashMap默认是按插入顺序存储元素,构造函数如果不明确传入accessOrder的话,默认都是按插入序的。

最后一个构造方法accessOrder从构造方法参数传入,如果传入true,则就实现了按访问顺序存储元素,这也是实现LRU缓存策略的关键。

3.2 LinkedHashMapinit()方法

由 LinkedHashMap的五种构造方法可知:

  1. 无论采用何种方式创建LinkedHashMap,其都会调用HashMap相应的构造函数
  2. 不管调用HashMap的哪个构造函数,HashMap的构造函数都会在最后调用一个init()方法进行初始化
  3. init()方法在HashMap中是一个空实现,而在LinkedHashMap中重写了它,用于初始化它所维护的双向链表
// Hashmap
/**
 * Constructs an empty <tt>HashMap</tt> with the default initial capacity
 * (16) and the default load factor (0.75).
 */
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
    table = new Entry[DEFAULT_INITIAL_CAPACITY];
    init();
}
 
 
// LinkedHashmap
@Override
void init() {
    // header初始化
    // hash为-1,其他的参数均为null
    // 也就是说这个header不在数组中
    // 只是用来标志开始元素和标志结束元素的
    header = new Entry<>(-1, null, null, null);
    header.before = header.after = header;
}

3.3 维护链表的操作

维护链表主要使用三个方法。

afterNodeRemoval,afterNodeInsertion,afterNodeAccess。这三个方法的主要作用是,在删除,插入,获取节点之后,对链表进行维护。简单来说,在这三个方法中会执行双向链表的操作。

3.3.1 afterNodeRemoval(Node<K,V> e)方法

在节点被删除之后调用的方法。

//在节点删除后,维护链表,传入删除的节点
void afterNodeRemoval(Node<K,V> e) { // unlink
    // p指向待删除元素,b指向前驱,a指向后驱
    LinkedHashMap.Entry<K,V> p =(LinkedHashMap.Entry<K,V>)e,
        b = p.before, 
        a = p.after;
    // 这里执行双向链表删除p节点操作,很简单。
    p.before = p.after = null;
    if (b == null)
        head = a;
    else
        b.after = a;
    if (a == null)
        tail = b;
    else
        a.before = b;
}

本质就是一个把节点从双向链表中删除的方法。

3.3.2 afterNodeInsertion(boolean evict)方法

这是一个很奇葩的方法,虽然名字是在插入之后进行的维护链表的操作,但是默认实际上这个什么都没做。

该方法在HashMap中的putVal()方法中被调用,可以看到HashMap中这个方法的实现为空。

void afterNodeInsertion(boolean evict) { // evict,驱逐的意思。
    LinkedHashMap.Entry<K,V> first;
    // removeEldestEntry(first)默认返回false,所以afterNodeInsertion这个方法其实并不会执行
    if (evict && (first = head) != null && removeEldestEntry(first)) {
        K key = first.key;
        removeNode(hash(key), key, null, false, true);
    }
}

protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
    return false;
}

(1)如果evict为true,且头节点不为空,且确定移除最老的元素,那么就调用HashMap.removeNode()把头节点移除(这里的头节点是双向链表的头节点,而不是某个桶中的第一个元素);

(2)HashMap.removeNode()从HashMap中把这个节点移除之后,会调用afterNodeRemoval()方法;

(3)afterNodeRemoval()方法在LinkedHashMap中也有实现,用来在移除元素后修改双向链表;

(4)默认removeEldestEntry()方法返回false,也就是不删除元素。

为什么不执行也可以呢,这个要到put操作的时候就能看出来了。

那么removeEldestEntry这个方法有什么用呢,看名字可以知道是删除最久远的节点,也就是head节点,这个方法实际是给我们自己扩展的。默认是没有用的,接下来实现LRU的代码中将可以看到它的作用。

3.3.3 afterNodeAccess(Node<K,V> e)方法

在节点访问之后被调用,主要在put()已经存在的元素或get()时被调用,如果accessOrder为true,调用这个方法把访问到的节点移动到双向链表的末尾。

// 在节点被访问后根据accessOrder判断是否需要调整链表顺序。传入的是访问的节点
void afterNodeAccess(Node<K,V> e) { // move node to last
    LinkedHashMap.Entry<K,V> last;
    // 如果accessOrder为true,并且访问的节点不是尾节点
    // 如果accessOrder为false,什么都不做
    if (accessOrder && (last = tail) != e) {
        // p指向待移动元素(被访问的节点),b指向前驱,a指向后驱
        LinkedHashMap.Entry<K,V> p =
                (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
        // 把p节点从双向链表中移除
        p.after = null;
        if (b == null)
            head = a;
        else
            b.after = a;
        
        if (a != null)
            a.before = b;
        else
            last = b;
        
        // 把p节点放到双向链表的末尾
        if (last == null)
            head = p;
        else {
            p.before = last;
            last.after = p;
        }
        // 尾节点等于p
        tail = p;
        // 增加修改次数,保证并发读安全。
        ++modCount;
    }
}

(1)如果accessOrder为true,并且访问的节点不是尾节点;

(2)从双向链表中移除访问的节点;

(3)把访问的节点加到双向链表的末尾;(末尾为最新访问的元素)

3.4 get操作

LinkedHashMap重写了HashMap的get方法,如下:

/**
 * 调用hashmap的getNode方法获取到值之后,维护双向链表
 * @param key
 * @return
 */
public V get(Object key) {
    Node<K,V> e;
    // 使用HashMap的getNode方法来获取节点
    if ((e = getNode(hash(key), key)) == null)
        return null;
    // 访问完了数据之后,去根据accessOrder来决定是否调整双向链表
    if (accessOrder)
        afterNodeAccess(e);
    return e.value;
}

如果查找到了元素,且accessOrder为true,则调用afterNodeAccess()方法把访问的节点移到双向链表的末尾。

recordAccess方法:

  • 如果链表中元素的排序规则是按照插入的先后顺序排序的话(accessOrder=false),该方法什么也不做
  • 如果链表中元素的排序规则是按照访问的先后顺序排序的话(accessOrder=true),则将e移到链表的末尾处

调用LinkedHashMap的get(Object key)方法,返回值是 NULL,有如下两种可能:

  • 该 key 对应的值就是 null
  • HashMap 中不存在该 key

3.4 put操作

LinkedHashMap没有重写HashMap的put方法,所以执行put操作的时候,还是使用的是HashMap的put方法。那么这样如何保证链表的逻辑呢?

总体来看,LinkedHashMap中put的过程跟HashMap的put类似。当put元素时,不但要把它加入到HashMap中去,还要加入到双向链表中,所以可以看出LinkedHashMap就是HashMap+双向链表。

LinkedHashMap完全继承了HashMap的 put(Key,Value) 方法:

public V put(K key, V value) {
    if (table == EMPTY_TABLE) {      //数组为null时
        inflateTable(threshold);     //给数组根据阈值分配内容空间
    }
    if (key == null)      //key为null时
        return putForNullKey(value);
    int hash = hash(key);  //通过key计算hash
    int i = indexFor(hash, table.length);  //计算在数组中的索引位置
    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;
           //使用的是LinkedHashMap重写的方法
                     1 
            e.recordAccess(this);         
            return oldValue;
        }
    }
    modCount++;
   //addEntry调用的是LinkedHashMap重写了的方法
             2 
    addEntry(hash, key, value, i);     
    return null;
}
  • 只是对put(Key,Value)方法所调用的recordAccess方法和addEntry方法进行了重写
  • addEntry方法中还调用了removeEldestEntry方法,该方法是用来被重写的,一般如果用LinkedHashmap实现LRU算法,就要重写该方法
  • 比如可以将该方法覆写为如果设定的内存已满,则返回true,这样当再次向LinkedHashMap中putEntry时,在调用的addEntry方法中便会将近期最少使用的节点删除掉(header后的那个节点)

注意:下面的这几个覆写的方法在jdk1.8及以后就没有了。

LinkedHashMap覆写了HashMap中的recordAccess和addEntry方法:

void recordAccess(HashMap<K,V> m) {      // 执行顺序1
    // 将传入的HashMap类型的m强制转换成LinkedHashMap类型的    
    LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;  
    // accessOrder默认的是false,当accessOrder为true时进入
    if (lm.accessOrder) { 
        lm.modCount++;
        //移除当前节点 
        remove(); 
        // 执行顺序3      
        addBefore(lm.header);           
    }
}  

void addEntry(int hash, K key, V value, int bucketIndex) {           // 执行顺序2
    // 重写了HashMap中的createEntry方法
    createEntry(hash, key, value, bucketIndex);  
    // Remove eldest entry if instructed
    Entry<K,V> eldest = header.after;   //还是header自身
    if (removeEldestEntry(eldest)) {            // 执行顺序4
        removeEntryForKey(eldest.key);
    } else {  
    //扩容到原来的2倍  
    if (size >= threshold)             // 执行顺序5
        resize(2 * table.length);  
    }  
}

protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {   // 执行顺序4
    return false;
}

在LinkedHashMap的addEntry方法中,它重写了HashMap中的createEntry方法:

void createEntry(int hash, K key, V value, int bucketIndex) { 
    // 向哈希表中插入Entry,这点与HashMap中相同 
    // 创建新的Entry并将其链入到数组对应桶的链表的头结点处, 
    HashMap.Entry<K,V> old = table[bucketIndex];  
    Entry<K,V> e = new Entry<K,V>(hash, key, value, old);  
    table[bucketIndex] = e;     
 
    // 在每次向哈希表插入Entry的同时,都会将其插入到双向链表的尾部,  
    // 这样就按照Entry插入LinkedHashMap的先后顺序来迭代元素
    // (LinkedHashMap根据双向链表重写了迭代器)
    // 同时,新put进来的Entry是最近访问的Entry,把其放在链表末尾 ,也符合LRU算法的实现  
    e.addBefore(header);  
    size++;  
}  

在LinkedHashMap中向哈希表中插入新Entry的同时,还会通过Entry的addBefore方法将其链入到双向链表中。addBefore方法本质上是一个双向链表的插入操作:

//插入有序不做处理,在访问有序做相应处理:addBefore(将当前节点插到header的前面)
private void addBefore(Entry<K,V> existingEntry) {             3
    after  = existingEntry;     //existingEntry即为header
    before = existingEntry.before;
    before.after = this;     //this即为要插入的节点
    after.before = this;
}

3.5 remove操作

和put操作一样,也是直接使用HashMap的方法来完成的:

final Node<K, V> removeNode(int hash, Object key, Object value,
        boolean matchValue, boolean movable) {
    Node<K, V>[] tab;
    Node<K, V> p;
    int n, index;
    //判断table是否为空,该key对应的桶是否为空
    if ((tab = table) != null && (n = tab.length) > 0 &&
            (p = tab[index = (n - 1) & hash]) != null) {
        Node<K, V> node = null, e;
        K k;
        V v;
        if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
            node = p;
        else if ((e = p.next) != null) {
            if (p instanceof TreeNode)
                node = ((TreeNode<K, V>) p).getTreeNode(hash, key);
            else {
                do {
                    if (e.hash == hash &&
                            ((k = e.key) == key ||
                                    (key != null && key.equals(k)))) {
                        node = e;
                        break;
                    }
                    p = e;
                } while ((e = e.next) != null);
            }
        }
        //到这里了其实node就已经指向了要删除的节点了
        //matchValue的作用是指现在是否需要值匹配。因为可能没有传入value,所以判断一下
        if (node != null && (!matchValue || (v = node.value) == value ||
                (value != null && value.equals(v)))) {
            if (node instanceof TreeNode)
                ((TreeNode<K, V>) node).removeTreeNode(this, tab, movable);
            else if (node == p)
                tab[index] = node.next;
            else
                p.next = node.next;
            ++modCount;
            --size;
            //调用维护双向链表的操作
            afterNodeRemoval(node);
            return node;
        }
    }
    return null;
}

3.6 扩容操作

在HashMap的put方法中,如果发现前元素个数超过了扩容阀值时,会调用resize方法。

LinkedHashMap完全继承了HashMap的resize()方法,只是对它所调用的transfer方法进行了重写。

Map扩容操作的核心在于重哈希。重哈希是指重新计算原HashMap中的元素在新table数组中的位置并进行复制处理的过程,鉴于性能和LinkedHashMap自身特点的考量,LinkedHashMap对重哈希过程(transfer方法)进行了重写。

void resize(int newCapacity) {       // 执行步骤5
    Entry[] oldTable = table;
    int oldCapacity = oldTable.length;
 
    // 若 oldCapacity 已达到最大值,直接将 threshold 设为 Integer.MAX_VALUE
    if (oldCapacity == MAXIMUM_CAPACITY) {  
        threshold = Integer.MAX_VALUE;
        return;             // 直接返回
    }
 
    // 否则,创建一个更大的数组
    Entry[] newTable = new Entry[newCapacity];
 
    //将每条Entry重新哈希到新的数组中
    // 执行步骤6
    transfer(newTable);  //LinkedHashMap对它所调用的transfer方法进行了重写
 
    table = newTable;
    threshold = (int)(newCapacity * loadFactor);  // 重新设定 threshold
}
 

LinkedHashMap重写了transfer方法,数据的迁移,它的实现如下:

void transfer(HashMap.Entry[] newTable) {     // 执行步骤6
    int newCapacity = newTable.length;
    // 与HashMap相比,借助于双向链表的特点进行重哈希使得代码更加简洁
    for (Entry<K,V> e = header.after; e != header; e = e.after) {
        int index = indexFor(e.hash, newCapacity);   // 计算每个Entry所在的桶
        // 将其链入桶中的链表
        e.next = newTable[index];
        newTable[index] = e;   
    }
}

可以看出,LinkedHashMap扩容时,数据的再散列和HashMap是不一样的。

HashMap是先遍历旧table,再遍历旧table中每个元素的单向链表,取得Entry以后,重新计算hash值,然后存放到新table的对应位置。

LinkedHashMap是遍历的双向链表,取得每一个Entry,然后重新计算hash值,然后存放到新table的对应位置。

从遍历的效率来说,遍历双向链表的效率要高于遍历table,因为遍历双向链表是N次(N为元素个数);而遍历table是N+table的空余个数(N为元素个数)。

四、LinkedListMapLRU(Least recently used,最近最少使用)算法

当accessOrder标志位为true时,表示双向链表中的元素按照访问的先后顺序排列,可以看到,虽然Entry插入链表的顺序依然是按照其put到LinkedHashMap中的顺序,但put和get方法均有调用recordAccess方法(put方法在key相同时会调用)

recordAccess方法判断accessOrder是否为true,如果是,则将当前访问的Entry(put进来的Entry或get出来的Entry)移到双向链表的尾部(key不相同时,put新Entry时,会调用addEntry,它会调用createEntry,该方法同样将新插入的元素放入到双向链表的尾部,既符合插入的先后顺序,又符合访问的先后顺序,因为这时该Entry也被访问了)

当标志位accessOrder的值为false时,表示双向链表中的元素按照Entry插入LinkedHashMap到中的先后顺序排序,即每次put到LinkedHashMap中的Entry都放在双向链表的尾部,这样遍历双向链表时,Entry的输出顺序便和插入的顺序一致,这也是默认的双向链表的存储顺序。当标志位accessOrder的值为false时,虽然也会调用recordAccess方法,但不做任何操作

4.1 利用LinkedListMap实现LRU

LRU,即最近最少使用,优先淘汰最近最少使用的元素。LRU中保存的数据如果满了,那么就会将最近最少使用的数据删除。

LinkedHashMap通过使accessOrder为true,可以满足这种特性。所以可以使用LinkedHashMap实现LRU算法。

测试链接:146. LRU 缓存 - 力扣(LeetCode)

实现代码:

class LRUCache extends LinkedHashMap {
    // 设置容器总容量,即规定容器中有多少个元素是算是满了
    private int capacity;

    public LRUCache(int capacity) {
        // accessOrder为true
        super(capacity, 0.75F, true);
        this.capacity = capacity;
    }

    public int get(int key) {
        return (int)super.getOrDefault(key, -1);
    }

    public void put(int key, int value) {
        super.put(key, value);
    }

    // 重写removeEldestEntry()方法设置何时移除旧元素
    protected boolean removeEldestEntry(Map.Entry eldest) {
        // 当元素个数大于了缓存的容量, 就移除元素
        return size() > capacity;
    }
}

这里重写了removeEldestEntry方法,然后removeEldestEntry方法在afterNodeInsertion中被调用,如果这个方法返回真,那么就会删除head指向的节点。根据每次get的节点都会放到尾部的特性,所以head指向的节点就是最久没有使用到的节点,所以可以删除。由于我们每次put完(HashMap#putVal())都会调用这个afterNodeInsertion方法,所以可以上面的设计可以使put过后如果size超了,将删除最久没有使用的一个节点,从而腾出空间给新的节点。

五、总结

  1. LinkedHashMap和HashMap是Java Collection Framework 的重要成员,也是Map家族成员;
  2. LinkedHashMap继承自HashMap,具有HashMap的所有特性;
  3. LinkedHashMap是HashMap和双向链表的合二为一,即一个将所有Entry节点链入一个双向链表的HashMap(LinkedHashMap = HashMap + 双向链表),将所有节点连成了一个双向链表;
  4. LinkedHashMap和HashMap最多只允许一条Entry的键为Null(多条会覆盖),但允许多条Entry的值为Null;
  5. LinkedHashMap 也是 Map 的一个非同步的实现;
  6. LinkedHashMap的实现非常精妙,很多方法都是在HashMap中留的钩子(Hook),直接实现这些Hook就可以实现对应的功能了,并不需要再重写put()等方法;
    • 在put操作上,虽然LinkedHashMap完全继承了HashMap的put操作,但是在细节上还是做了一定的调整,比如,在LinkedHashMap中向哈希表中插入新Entry的同时,还会通过Entry的addBefore方法将其链入到双向链表中
  7. LinkedHashMap的扩容比HashMap来的方便,因为HashMap需要将原来的每个链表的元素分别在新数组进行反向插入链化,而LinkedHashMap的元素都连在一个链表上,可以直接迭代然后插入;
    • 在扩容操作上,虽然LinkedHashMap完全继承了HashMap的resize操作,但是鉴于性能和LinkedHashMap自身特点的考量,LinkedHashMap对其中的重哈希过程(transfer方法)进行了重写
  8. 在读取操作上,LinkedHashMap中重写了HashMap中的get方法(加入recordAccess方法,重写transfer方法),通过HashMap中的getEntry方法获取Entry对象;
  9. HashMap是无序的,LinkedHashMap通过维护一个额外的双向链表保证了迭代顺序;
  10. 迭代顺序可以是插入顺序,也可以是访问顺序(即根据链表中元素的顺序可以将LinkedHashMap分为:保持插入顺序的LinkedHashMap和保持访问顺序的LinkedHashMap,其中LinkedHashMap的默认实现是按插入顺序排序的)
    • 如果accessOrder为false,则可以按插入元素的顺序遍历元素;当主动传入的accessOrder参数为false时, 使用put方法时,新加入元素不仅加入哈希桶中,还被加入双向链表末尾,get方法使用时不会把元素放到双向链表尾部
    • 如果accessOrder为true,则可以按访问元素的顺序遍历元素;当主动传入的accessOrder参数为true时,使用put方法新加入的元素,如果遇到了哈希冲突,并且对key值相同的元素进行了替换,就会被放在双向链表的尾部,当元素超过上限且removeEldestEntry方法返回true时,直接删除最早元素以便新元素插入。如果没有冲突直接放入,同样加入到链表尾部。使用get方法时会把get到的元素放入双向链表尾部
  11. 默认的LinkedHashMap并不会移除旧元素(removeEldestEntry方法默认返回false),如果需要移除旧元素,则需要重写removeEldestEntry()方法设定移除策略;
  12. LinkedHashMap很好的支持LRU算法。LinkedHashMap的removeEldestEntry方法默认返回false,要实现LRU很重要的一点就是集合满时要将最久未访问的元素删除,在LinkedHashMap中这个元素就是头指针指向的元素。实现LRU可以直接实现继承LinkedHashMap并重写removeEldestEntry方法来设置缓存大小。jdk中实现了LRUCache也可以直接使用

一句话总结,LinkedHashMap就是HashMap中将其node维护成了一个双向链表。只要搞懂了HashMap就可以很容易搞懂LinkedHashMap。


  相关文章:【Java集合】HashMap系列(一)——底层数据结构分析
                  【Java集合】HashMap系列(二)——底层源码分析
                  【Java集合】HashMap系列(三)——TreeNode内部类源码分析
                  【Java集合】HashMap系列(四)——JDK1.7和JDK1.8中的并发问题的分析
                  【Java集合】一文快速了解HashMap底层原理

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值