【Java集合】LinkedHashMap源码详解

2023/6/5 22:16:25

目录

一、介绍

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底层原理


http://www.jnnr.cn/a/273022.html

相关文章

2023-02-04 Elasticsearch 倒排索引的理解 Trie前缀树原理

1 搜索引擎 引出倒排表的原理 全文搜索引擎 自然语言处理(NLP) 、爬虫、网页处理、大数据处理 如谷歌、百度、搜狗、必应等等 垂直搜索引擎 有明确搜索目的的搜索行为 如各大电商网站、OA、站内搜索、视频网站等 要求&#xff1a; 查询快 &#xff08;高效的压缩算法 快速的编码…

图论(8)LCA

一、概念 给定一棵有根树&#xff0c;若节点z既是x的祖先&#xff0c;又是y的祖先&#xff0c;则称z是x和y的公共祖先。 在所有公共祖先中&#xff0c;深度最大的一个为最近公共祖先。 求最近公共祖先的方法有&#xff1a; &#xff08;1&#xff09;&#xff1a; 1.x向上走…

人工智能轨道交通行业周刊-第32期(2023.1.30-2.5)

本期关键词&#xff1a;智能装车系统、南昌地铁巡检机器人、中国铁道学会科学技术奖、AIGC报告、智慧城市 1 整理涉及公众号名单 1.1 行业类 RT轨道交通中关村轨道交通产业服务平台人民铁道世界轨道交通资讯网铁路信号技术交流北京铁路轨道交通网上榜铁路视点ITS World轨道交…

配置安全的linux-apache服务器(5)

实验简介 实验所属系列&#xff1a;Linux网络服务配置与安全 实验对象&#xff1a; 本科/专科信息安全专业、网络工程 相关课程及专业&#xff1a;系统安全配置、服务器配置、计算机网络 实验时数&#xff08;学分&#xff09;&#xff1a;2学时 实验类别&#xff1a;实践实验…

删除文件还有救吗?9个最好数据恢复软件你值得尝试

数据恢复软件是一种程序&#xff0c;可帮助从意外删除的存储设备中恢复数据。在这篇博文中&#xff0c;我们彻底研究并生成了适用于 Windows PC 的最佳数据恢复软件列表。我们推荐 奇客数据恢复 作为适用于 Windows 的最佳数据恢复软件。因为它带有不同的扫描模式和选项&#x…

axios中params和data的区别

在开发项目的过程中我们往往忽略了一点&#xff0c;请求接口的传参方式&#xff0c;习惯了post请求就用data,get请求就用params。 params是添加到url的请求字符串中的&#xff0c;用于get请求。服务器并不会读取http body里面的数据,这样我们传递的就是Params里的请求的参数了。…

面试官:Exception和Error有什么区别?

回答思路&#xff1a; 相同点和不同点 异常的分类 异常处理关键字 异常处理的原则 回答总结&#xff1a; 首先相同点是Exception和Error都继承了Throwable类&#xff0c;而不同的是Exception 和 Error 体现了不同异常情况的分类。可以说Error是天灾&#xff0c;出现了也恢复不…

C++基础(4) - 运算符

文章目录输入输出浅谈1、cout 进行 C 输出1.1 控制符 endl1.2 使用 cout 进行拼接2、cin 获取键盘输入常用运算符分类算术运算符1、除法&#xff08;/&#xff09;2、求模&#xff08;%&#xff09;3、自增和自减赋值运算符关系运算符逻辑运算符三元运算符位运算符1、原码、反码…

Java开发学习(四十二)----MyBatisPlus查询语句之条件查询

一、条件查询的类 MyBatisPlus将书写复杂的SQL查询条件进行了封装&#xff0c;使用编程的形式完成查询条件的组合。 这个我们在前面都有见过&#xff0c;比如查询所有和分页查询的时候&#xff0c;都有看到过一个Wrapper类&#xff0c;这个类就是用来构建查询条件的&#xff0…

springboot,vue教务管理系统

开发工具&#xff1a;IDEA服务器&#xff1a;Tomcat9.0&#xff0c; jdk1.8项目构建&#xff1a;maven数据库&#xff1a;mysql5.7前端技术&#xff1a;vue elementUI服务端技术&#xff1a;springbootmybatis本系统拥有三种角色&#xff1a;管理员、教师和学生&#xff0c;项目…

RL笔记:动态规划(1): 策略估计和策略提升

目录 0. 前言 (4.1)策略估计&#xff0c;Policy Evaluation(Prediction) Example 4.1 (python代码) Exercise 4.1 Exercise 4.2 Exercise 4.3 (4.2)Policy Improvement 0. 前言 Sutton-book第4章&#xff08;动态规划&#xff09;学习笔记。本文是关于其中4.1节&#xf…

fpga图像处理(sobel算子)

【声明:版权所有,欢迎转载,请勿用于商业用途。 联系信箱:feixiaoxing @163.com】 关于sobel算子,前面已经讲过计算方法了。一种是上下的sobel算子,一种是左右的sobel算子,两者都相当于prewitt算子的进一步拓展。当然,之前的实现方法都是基于python和opencv实现…

Cadence PCB仿真 使用 Allegro PCB SI 为电源网络分配电压并选择仿真的电源网络的方法图文教程

🏡《总目录》   🏡《分目录》 目录 1,概述2,分配电压3,选择仿真网络4,总结1,概述 进行电源分配网络PDN的仿真前,需要进行一些准备工作。首先需要为电源网络分配适合的电压,并选择需要进行PDN分析的网络。本文介绍其具体方法。 2,分配电压 第1步:执行Analyze→P…

TensorFlow Serving模型部署

7.7 TensorFlow Serving模型部署 学习目标 目标 无应用 应用TensorFlow Serving完成模型服务运行 7.7.1 TensorFlow Serving TensorFlow Serving是一种灵活的高性能服务系统&#xff0c;适用于机器学习模型&#xff0c;专为生产环境而设计。TensorFlow Serving可以轻松部署新…

OpenGL | OpenGL 绘制其他图形

一、绘制点GL_POINTSOpenGL默认绘制的点大小是1px&#xff0c;可以使用glPointSize()来改变点的大小&#xff0c;但是要注意&#xff1a;glPointSize()不能放在glBegin()和glEnd()之间&#xff0c;要放在glBegin()之前。1.代码void Draw() {glClearColor(1, 1, 1, 1.0f); //白…

大数据之HBase基础

文章目录前言一、HBase基础简介&#xff08;一&#xff09;基础介绍&#xff08;二&#xff09;应用场景&#xff08;三&#xff09;特点二、数据模型&#xff08;一&#xff09;行键&#xff08;row key&#xff09;&#xff08;二&#xff09;列&#xff08;三&#xff09;列…

(深度学习快速入门)第四章第三节:卷积层详解1

文章目录一&#xff1a;什么是卷积运算&#xff08;了解&#xff09;二&#xff1a;从全连接层到卷积层&#xff08;1&#xff09;解决空间不变性&#xff08;2&#xff09;解决参数爆炸-稀疏连接和权值共享三&#xff1a;CNN中的图像卷积卷积层&#xff1a;卷积层是CNN中的核心…

道路病害识别监测系统 CNN网络

道路病害识别监测系统通过CNN网络深度学习算法&#xff0c;道路病害识别监测对巡检车上实时监控道路影像数据进行分析&#xff0c;输出道路病害裂缝巡检报告并落图展示。在CNN出现之前&#xff0c;对于图像的处理一直都是一个很大的问题&#xff0c;一方面因为图像处理的数据量…

mybatis plus 更新时间 创建时间自动填充失效的情况和解决方案

问题描述&#xff1a; 调用mybatisplus的IService接口中的update(Wrapper updateWrapper)&#xff0c;update_time没有自动填充。 spuService.update(updateWrapper); 解决方案&#xff1a; 不使用update(wrapper xxx)方法&#xff0c;使用以下方法替代&#xff1a; 1.boole…

Traffic Signs Recognition with 95% Accuracy using CNNKeras

Traffic Signs Recognition 导读 本文采用CNN模型和Keras库 使用GTSRB数据集 构建模型可以分为四部分&#xff1a;1、先探索数据集 2、构建CNN模型 3、训练和验证模型 4、使用测试数据测试模型 保存模型并使用Python自带包tkinter实现GUI 一、数据集 下载完数据后&#xff0c;可…