LinkedHashMap特点

LinkedHashMap 是一个键有序的 HashMap,可以将 LinkedHashMap 理解为 LinkList + HashMap。
LinkedHashSet继承自HashSet,源码更少、更简单,唯一的区别是LinkedHashSet内部使用的是LinkHashMap。这样做的好处就是LinkedHashSet中的元素顺序是可以保证的,也就是说遍历序和插入序是一致的。

LinkedHashMap 链表长度小于8时数据结构图为:

java LinkedHashMap 根据下标获取值_构造方法

LinkedHashMap 链表长度大于等于8时转为红黑树:

java LinkedHashMap 根据下标获取值_构造方法_02

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

LinkedHashMap是HashMap的子类,自然LinkedHashMap也就继承了HashMap中所有非private的方法。

LinkedHashMap属性
// 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);
  }
}
// 链表头节点
transient LinkedHashMap.Entry<K,V> head;
// 链表尾节点
transient LinkedHashMap.Entry<K,V> tail;
// 通过iterator访问时的顺序,true表示按照访问顺序,false表示按照插入顺序。
final boolean accessOrder;

成员变量中增加了 head 和 tail 两个变量,可以实现对插入元素的链表维护。
accessOrder 表示遍历 LinkedHashMap 时将按照什么顺序输出。

LinkedHashMap和HashMap的区别在于它们的基本数据结构上,LinkedHashMap的基本数据结构,也就是Entry如图:

java LinkedHashMap 根据下标获取值_构造方法_03

LinkedHashMap与HashMap的Entry区别:

java LinkedHashMap 根据下标获取值_链表_04

LinkedHashMap构造方法
// 构造方法1,构造一个指定初始容量和负载因子的、按照插入顺序的LinkedList
public LinkedHashMap(int initialCapacity, float loadFactor) {
    super(initialCapacity, loadFactor);
    accessOrder = false;
}
// 构造方法2,构造一个指定初始容量的LinkedHashMap,取得键值对的顺序是插入顺序
public LinkedHashMap(int initialCapacity) {
    super(initialCapacity);
    accessOrder = false;
}
// 构造方法3,用默认的初始化容量和负载因子创建一个LinkedHashMap,取得键值对的顺序是插入顺序
public LinkedHashMap() {
    super();
    accessOrder = false;
}
// 构造方法4,通过传入的map创建一个LinkedHashMap,容量为默认容量(16)和(map.zise()/DEFAULT_LOAD_FACTORY)+1的较大者,装载因子为默认值
public LinkedHashMap(Map<? extends K, ? extends V> m) {
    super(m);
    accessOrder = false;
}
// 构造方法5,根据指定容量、装载因子和键值对保持顺序创建一个LinkedHashMap
public LinkedHashMap(int initialCapacity,
             float loadFactor,
                         boolean accessOrder) {
    super(initialCapacity, loadFactor);
    this.accessOrder = accessOrder;
}
LinkedList方法分析

类源码方法层面的分析最好的方法是使用Debug一步步走一遍该方法。

get(Object key)方法
public V get(Object key) {
    Node<K,V> e;
    if ((e = getNode(hash(key), key)) == null)//直接调用了 HashMap 的 getNode 方法获取到对应的节点
        return null;
    if (accessOrder)
        afterNodeAccess(e);
    return e.value;
}
------------------------------
// 将节点挪到链表尾部
void afterNodeAccess(Node<K,V> e) { 
    LinkedHashMap.Entry<K,V> last;
    // 如果 accessOrder 为true,且尾部节点不是 e 节点,那么将其挪到尾部
    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;
    }
}

如果 accessOrder 为 true,那么表示访问时就要按照访问顺序去访问。而在 get 方法中,我们每访问一个节点,我们就会将该节点放入链表尾部,所以我们通过 iterator 访问链表时就是按照访问顺序得到的遍历(越早访问的越在后面)。这就是Java在LinkedHashMap中实现LRU的一个实例。使用LinkedHashMap实现一个LRU缓存也是LinkedHashMap的常见应用。

LRU 是Least Recently Used的缩写,即最近最少使用,常用于页面置换算法。

LRU 缓存淘汰过程:

java LinkedHashMap 根据下标获取值_ci_05

基于LinkedHashMap实现一个基于LRU算法的缓存设计示例:

package com.hpmp.utils;

import java.util.LinkedHashMap;

/**
 * 基于LinkedHashMap实现一个基于LRU算法的缓存设计
 * 自定义LruCache实现思路
 * 1)有容量限制
 * 2)容器满了要基于LRU算法移出元素
 */
class LruCache extends LinkedHashMap<String, Object> {
    private static final long serialVersionUID = 1L;
    /**
     * 设计缓存的最大容量
     */
    private int maxCapacity;

    public LruCache(int maxCapacity) {
        super(maxCapacity, 0.75f, true);
        this.maxCapacity = maxCapacity;
    }

    /**
     * 方法的返回值决定了容器在满的时是否移除元素.
     * 当前集合元素数量大于设置的最大容量时:表示要移除
     * 当前集合元素数量小于或等于设置的最大容量时:表示不移除
     */
    @Override
    protected boolean removeEldestEntry(java.util.Map.Entry<String, Object> eldest) {
        return size() > maxCapacity;
    }
}

public class TestLruCache {
    public static void main(String[] args) {
        LruCache cache = new LruCache(3);
        cache.put("A", 100);
        cache.put("B", 200);
        cache.put("C", 300);
        System.out.println(cache);//{A=100, B=200, C=300}
        cache.get("A");
        System.out.println(cache);//{B=200, C=300, A=100}
        cache.put("D", 400);
        System.out.println(cache);//{C=300, A=100, D=400}
    }
}
put(K key, V value)方法

LinkedHashMap 中并没有 put 方法的实现,直接使用HashMap的put方法实现。不过重写了put方法内部的newNode方法,afterNodeAccess方法,afterNodeInsertion方法。

/**
 * 创建一个节点
 * @param hash  hash值
 * @param key   键
 * @param value 值
 * @param e     下一个节点,这个是HashMap节点的属性
 * @return
 */
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
    //调用构造方法
    LinkedHashMap.Entry<K,V> p =
            new LinkedHashMap.Entry<K,V>(hash, key, value, e);
    //维护链表
    linkNodeLast(p);
    return p;
}

/**
 * 添加一个节点到末尾
 * @param p 节点
 */
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;
    }
}

-----------------------------------
/**
 * accessOrder为true时,将操作的节点移到链表尾部
 * @param e 节点
 */
void afterNodeAccess(Node<K,V> e) {
    LinkedHashMap.Entry<K,V> last;
    //accessOrder 这个参数是指在进行操作的时候,是否将操作的节点移动到链表的最后,默认false
    //也就是说accessOrder为false的时候链表就是按照插入顺序维护的
    //true的时候,会将最近使用的节点移动到链表最后
    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
            //说明该节点就是尾部节点,设置前置节点为后节点
            //a == null 说明p就是尾部节点? 有点不清楚
            last = b;
        //统一更新尾部节点
        if (last == null)
            //说明只有这么一个节点
            head = p;
        else {
            //将当前节点挂到链表末尾
            p.before = last;
            last.after = p;
        }
        //设置尾部节点
        tail = p;
        ++modCount;
    }
}

--------------------------
void afterNodeInsertion(boolean evict) {
    LinkedHashMap.Entry<K,V> first;
    if (evict && (first = head) != null && removeEldestEntry(first)) {
        K key = first.key;
        removeNode(hash(key), key, null, false, true);
    }
}
//LinkedHashMap这个方法不生效
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
    return false;
}
remove(Object key)方法

LinkedHashMap 中并没有 remove 方法的实现,直接使用HashMap的remove方法实现。不过重写其中的afterNodeRemoval方法。

/**
 * 删除链表中的节点
 */
void afterNodeRemoval(Node<K,V> e) {
    //获取当前节点的前置后置节点
    LinkedHashMap.Entry<K,V> p =
            (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
    //清空前置后置节点
    p.before = p.after = null;

    if (b == null)
        //前置节点为空,说明为头节点,更新头节点为后置节点
        head = a;
    else
        //前置节点不为空,设置前置节点的后置节点为删除节点的后置节点
        b.after = a;
    if (a == null)
        //后置节点为空,说明为尾部节点,更新尾部节点为其前置节点
        tail = b;
    else
        //后置节点不为空,更新后置节点的前置节点
        a.before = b;
}
LinkedHashMap的使用
public static void main(String[] args)
{
    LinkedHashMap<String, String> linkedHashMap =
            new LinkedHashMap<String, String>(16, 0.75f, true);//初始化
    linkedHashMap.put("111", "111");//添加
    linkedHashMap.put("222", "222");
    linkedHashMap.put("333", "333");
    linkedHashMap.put("444", "444");
    loopLinkedHashMap(linkedHashMap);//遍历
    linkedHashMap.get("111");//查询
    loopLinkedHashMap(linkedHashMap);//遍历
    linkedHashMap.put("222", "2222");
    loopLinkedHashMap(linkedHashMap);//遍历
}
//遍历
public static void loopLinkedHashMap(LinkedHashMap<String, String> linkedHashMap)
{
    Set<Map.Entry<String, String>> set = linkedHashMap.entrySet();
    Iterator<Map.Entry<String, String>> iterator = set.iterator();
    while (iterator.hasNext())
    {
        System.out.print(iterator.next() + "\t");
    }
    System.out.println();
}

注意这里的构造方法要用三个参数那个且最后的要传入true,这样才表示按照访问顺序排序。看一下代码运行结果:

111=111    222=222    333=333    444=444    
222=222    333=333    444=444    111=111    
333=333    444=444    111=111    222=2222

代码运行结果证明了两点:

  1. LinkedList是有序的
  2. 每次访问一个元素(get或put),被访问的元素都被提到最后面去了