容器(十五):LinkedHashMap

容器(十五):LinkedHashMap

标签: Java编程思想


大多数情况下,只要不涉及线程安全问题,Map基本都可以使用HashMap,不过HashMap有一个问题,就是迭代HashMap的顺序并不是HashMap放置的顺序,也就是无序。HashMap的这一缺点往往会带来困扰,因为有些场景,我们期待一个有序的Map

这个时候,LinkedHashMap就闪亮登场了,它虽然增加了时间和空间上的开销,但是通过维护一个运行于所有条目的双向链表,LinkedHashMap保证了元素迭代的顺序。

在理解了HashMap源码的基础上,LinkedHashMap的源码理解起来也不难。

LinkedHashMap简介

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

LinkedHashMap是HashMap的子类。

与HashMap有着同样的存储结构,但它加入了一个双向链表的头结点,将所有put到LinkedHashmap的节点一一串成了一个双向循环链表,因此它保留了节点插入的顺序,可以使节点的输出顺序与输入顺序相同。

LinkedHashMap实现与HashMap的不同之处在于,一个是有序的,一个是无序的LinkedHashMap维护着一个运行于所有条目的双重链接列表。此链接列表定义了迭代顺序,该迭代顺序可以是插入顺序或者是访问顺序。

要特意说明的是什么是插入访问,什么是顺序访问?

    //access为true 表示按照访问顺序进行
    //access为false表示按照插入顺序进行
    final boolean accessOrder;
  • 按照访问的次序来排序的含义
    当调用LinkedHashMap的get(key)或者put(key,value)时,若此时key在map中被包含,那么LinkedHashMap会将key对象的entry放在线性结构的最后。(实现LRU Cache即最近最久未使用)
  • 按照插入顺序来排序的含义:
    调用get(key), 或者put(key, value)并不会对线性结构产生任何的影响。

注意,此实现不是同步的。如果多个线程同时访问链接的哈希映射,而其中至少一个线程从结构上修改了该映射,则它必须保持外部同步。

那么对LinkedHashMap进行总结:

  • LinkedHashMap是否允许键值对为空?
    答:Key和Value都允许空

  • LinkedHashMap是否允许重复数据?
    答:Key重复会覆盖、Value允许重复

  • LinkedHashMap是否有序?
    答:有序

  • LinkedHashMap是否线程安全?
    答:非线程安全

LinkedHashMap的数据结构

从继承关系上来看,LinkedHashMap是继承于HashMap的,因此也是散列表的结构。

从命名关系上来看,“Linked”是它对HashMap功能的进一步增强,LinkedHashMap用双向链表的结构,把所有存储在HashMap中的数据连接起来。

但是有人会说HashMap不是已经有了链表了么,为什么还要再加个双向链表?

这里要注意的是,HashMap的链表结构是存在于桶中的,即当一个桶中有不止一个Node时,就将其以链表的方式进行存储。数组-链表-红黑树构成了数据结构的整体。而双向链表,则是LinkedHashMap的额外引入,与HahMap的桶中的链表只存储而没有顺序不同:双向链表是在整体数据结构的基础上,将LinkedHashMap中所有的数据连接起来,使数据存储有序。

下面在HashMap的基础上进行添加,蓝色线头即为双向链表:

LinkedHashMap

说明:LinkedHashMap会将元素串起来,形成一个双链表结构。可以看到,其结构在HashMap结构上增加了链表结构。数据结构为(数组 + 单链表 + 红黑树 + 双链表),图中的标号是结点插入的顺序。

源码分析

静态内部类

源码的开头是一个静态内部类Entry<K,V>来表示双向链表中的节点,其实在JDK1.7之前,HashMap中的键值对使用Entry<K,V>存储的,在JDK1.8中改为了Node<K,V>,那么在LinkedHashMap中,Entry<K,V>继承实现了Node<K,V>。before和after表示前后指针。我们在使用LinkedHashMap有序就是因此产生。

    static class Entry<K,V> extends HashMap.Node<K,V> {
        //双向链表有前后指针
        Entry<K,V> before, after;
        //构造函数生成一个Node<K,V>
        Entry(int hash, K key, V value, Node<K,V> next) {
            super(hash, key, value, next);
        }
    }

成员变量:

    // 版本序列号
    private static final long serialVersionUID = 3801124242820219131L;

    // Entry<K,V>类型的链表头结点
    transient LinkedHashMap.Entry<K,V> head;

    //  Entry<K,V>链表尾结点
    transient LinkedHashMap.Entry<K,V> tail;

    // 访问顺序
    //access为true表示按照访问顺序进行
    //access为false表示按照插入顺序进行
    final boolean accessOrder;

构造函数

  • LinkedHashMap(int, float)型构造函数:
    总是会在构造函数的第一行调用父类HashMap的构造函数,使用super关键字,accessOrder默认为false,即按照插入顺序进行。

什么是访问顺序,什么是插入顺序在后面说明

  public LinkedHashMap(int initialCapacity, float loadFactor) {
        super(initialCapacity, loadFactor);
        accessOrder = false;
    }
  • LinkedHashMap(int)型构造函数
  public LinkedHashMap(int initialCapacity) {
        super(initialCapacity);
        accessOrder = false;
    }
  • LinkedHashMap()型构造函数
   public LinkedHashMap() {
        super();
        accessOrder = false;
    }
  • LinkedHashMap(Map
    public LinkedHashMap(Map<? extends K, ? extends V> m) {
        super();
        accessOrder = false;
        putMapEntries(m, false);    //调用到父类HashMap的函数
    }
  • LinkedHashMap(int, float, boolean)型构造函数
    可以指定accessOrder的值,从而控制访问顺序,实现LRU。
  //当参数accessOrder为true时,即会按照访问顺序排序,最近访问的放在最前,最早访问的放在后面
  public LinkedHashMap(int initialCapacity,
                         float loadFactor,
                         boolean accessOrder) {
        super(initialCapacity, loadFactor);
        this.accessOrder = accessOrder;
    }

重要方法

将节点链接到尾部

首先确定last节点指向尾节点tail,将尾节点指向要添加的节点p,若last即原尾节点不为空,则将p的前指针指向last,last的后指针指向p

// link at the end of list
    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;
        }
    }
替换某个节点

transferLinks(),给出了两个节点
将链表中的src节点替换为dst节点。先将dst节点与src节点共用前后节点,然后再将前后节点的后前指针指向dst。

// apply src's links to dst
    private void transferLinks(LinkedHashMap.Entry<K,V> src,
                               LinkedHashMap.Entry<K,V> dst) {
        LinkedHashMap.Entry<K,V> b = dst.before = src.before;
        LinkedHashMap.Entry<K,V> a = dst.after = src.after;
        if (b == null)
            head = dst;
        else
            b.after = dst;
        if (a == null)
            tail = dst;
        else
            a.before = dst;
    }

replacementNode(Node<K,V> p, Node<K,V> next)给出了一个节点和它的后节点,调用transferLinks

Node<K,V> replacementNode(Node<K,V> p, Node<K,V> next) {
        LinkedHashMap.Entry<K,V> q = (LinkedHashMap.Entry<K,V>)p;
        LinkedHashMap.Entry<K,V> t =
            new LinkedHashMap.Entry<K,V>(q.hash, q.key, q.value, next);
        transferLinks(q, t);
        return t;
    }
新建一个节点

有两步:一是新建一个Entry节点,二是将其连接在表的末尾,这个操作维护了插入顺序。

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;
    }
新建TreeNode结点

当要需要红黑树的节点时,新建一个TreeNode结点,并接入双向链表尾部。

// 当桶中结点类型为HashMap.TreeNode时,调用此函数
TreeNode<K,V> newTreeNode(int hash, K key, V value, Node<K,V> next) {
    // 生成TreeNode结点
    TreeNode<K,V> p = new TreeNode<K,V>(hash, key, value, next);
    // 将该结点插入双链表末尾
    linkNodeLast(p);
    return p;
}
将节点添加到链表尾部(put方法)

要说明的是在LinkedHashMap中没有put方法,也就是说LinkedHashMap中使用put()是调用HashMap中的put方法,但是和HashMap中put方法不同的是,LinkedHashMap中实现了HashMap中没有实现的afterNodeAccess(Node<K,V> e)方法。

afterNodeAccess方法就是如何支持LRU规则的,如果在accessOrder为true的时候,节点调用这个函数,就会把这个节点从链表中取出,然后链接在链表的最后面。put,get等都会调用这个函数来调整顺序

void afterNodeAccess(Node<K,V> e) { // 把当前节点e放到双向链表尾部
        LinkedHashMap.Entry<K,V> last;  //节点last,用来指向尾节点

        //accessOrder就是我们前面说的LRU控制,当它为true,同时e对象不是尾节点(如果访问尾节点就不需要设置,该方法就是把节点放置到尾节点)
        if (accessOrder && (last = tail) != e) {
        //用a和b分别记录该节点前面和后面的节点
            LinkedHashMap.Entry<K,V> p =
                (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
             //将要添加的后节点设为null 
            p.after = null;
            //如果当前节点的前节点是空,
            if (b == null)
            //那么头节点就设置为a
                head = a;
            else
            //如果b不为null,那么b的后节点指向a
                b.after = a;
            //如果a节点不为空
            if (a != null)
                //a的后节点指向b
                a.before = b;
            else
                //如果a为空,那么b就是尾节点
                last = b;
                //如果尾节点为空
            if (last == null)
            //那么p为头节点
                head = p;
            else {
            //否则就把p放到双向链表最尾处
                p.before = last;
                last.after = p;
            }
            //设置尾节点为P
            tail = p;
            //LinkedHashMap对象操作次数+1
            ++modCount;
        }
    }
移除头结点

afterNodeInsertion(boolean evict) 这个方法用来移除头结点,是重写了HashMap中的,但是HashMap中并没有去实现它,所以在put的时候就会触发一个机制:给定一个条件evict,控制存储在LinkedHashMap中的最旧的数据何时删除。

必须要evict为true且头节点不为null且 removeEldestEntry(Map.Entry<K,V> eldest)的返回值必须为true,才能够获取头节点的key然后调用removeNode方法删除

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

removeEldestEntry()的作用是移除最老数据,但是这个方法永远是返回false:

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

因此,afterNodeInsertion()由于removeEldestEntry()所返回的false无执行意义。也就意味着如果想要让它有意义必须重写removeEldestEntry()。

使用LinkedHashMap实现一个简单的LRUCache。重写removeEldestEntry(),当超出缓存容器大小时移除最老的头节点(这里不考虑并发问题,如下):

@Override  
public boolean removeEldestEntry(Map.Entry<K, V> eldest){       
    return size() > capacity;          
}  
删除某一节点

afterNodeRemoval()方法相对简单,就是在删除后处理其对应链表前后关系(刨掉一截)。

void afterNodeRemoval(Node<K,V> e) { // unlink
        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;
    }
get方法

是LinkedHashMap重写了HashMap的get方法,get方法中是否调用了afterNodeAccess(e)函数:

public V get(Object key) {
        Node<K,V> e;
        if ((e = getNode(hash(key), key)) == null)
            return null;
        if (accessOrder)    //如果启用了LRU规则
            afterNodeAccess(e); //那么把该节点移到双向链表最后面
        return e.value;
    }

还有一个get方法:getOrDefault(Object key, V defaultValue)

如果之前的key对应的节点不存在,那么就返回默认的值defaultValue,如果按访问顺序则调用afterNodeAccess(e),返回其value

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

两种演示

LinkedHashMap的核心就是存在存储顺序和可以实现LRU算法,所以下面用两个demo先来证明这两种情况:

存储顺序

放入到LinkedHashMap是有顺序的,会按照你放入的顺序存储:

package char20;

import java.util.LinkedHashMap;

/**
 * Created by japson on 8/16/2017.
 */
public class LinkedHashMapTest {
    public static void main(String[] args) {
        LinkedHashMap<Integer, Integer> map = new LinkedHashMap<Integer, Integer>();
        for (int i = 0; i < 10; i++) {//按顺序放入1~9
            map.put(i, i);
        }
        System.out.println("原数据:"+map.toString());

        System.out.println("查询key为3的value:"+map.get(3));

        System.out.println("添加已经存在的key:<4,100>,并返回value:"+ map.get(map.put(4,100)));

        map.put(10, 10);
        System.out.println("插入一个原本不存在的<10,10>:"+map.toString());
    }
}

输出:

原数据:{0=0, 1=1, 2=2, 3=3, 4=4, 5=5, 6=6, 7=7, 8=8, 9=9}
查询key为3value3
添加已经存在的key:<4,100>,并返回value100
插入一个原本没存在的:{0=0, 1=1, 2=2, 3=3, 4=100, 5=5, 6=6, 7=7, 8=8, 9=9, 10=10}

其实它是符合先进先出的规则的,不管你怎么查询插入已存在的数据,不会对排序造成影响,如果有新插入的数据将会放在最尾部。

启动LRU

启用LRU规则的LinkedHashMap,启动这个规则需要在构造LinkedHashMap的时候,调用三个参数的构造器,第三个参数accessOrder就是用于控制LRU规则的。 演示如下:

package char20;

import java.util.LinkedHashMap;

/**
 * Created by japson on 8/16/2017.
 */
public class LinkedHashMapTest {
    public static void main(String[] args) {

        //调用构造器:public LinkedHashMap(int initialCapacity,float loadFactor,boolean accessOrder)
        LinkedHashMap<Integer, Integer> map = new LinkedHashMap<Integer, Integer>(20, 0.75f, true);
        for (int i = 0; i < 10; i++) {//按顺序放入1~9
            map.put(i, i);
        }
        System.out.println("原数据按顺序存储:"+map.toString());

        int get3 = map.get(3);
        System.out.println("查询key为3的键值对,返回其value:"+ get3);
        System.out.println("get操作后,根据LRU算法,map调整为:"+map.toString());

        int put4 = map.put(4, 100);
        System.out.println("添加已经存在的key:<4,100>,并返回value:"+ map.get(put4));
        System.out.println("put操作后,根据LRU算法,map调整为:"+ map.toString());

        map.put(10, 10);
        System.out.println("插入一个原本没存在的:"+map.toString());
    }
}

输出:

原数据按顺序存储:{0=0, 1=1, 2=2, 3=3, 4=4, 5=5, 6=6, 7=7, 8=8, 9=9}
查询key为3的键值对,返回其value:3
get操作后,根据LRU算法,map调整为:{0=0, 1=1, 2=2, 4=4, 5=5, 6=6, 7=7, 8=8, 9=9, 3=3}
添加已经存在的key:<4,100>,并返回value:100
put操作后,根据LRU算法,map调整为:{0=0, 1=1, 2=2, 5=5, 6=6, 7=7, 8=8, 9=9, 3=3, 4=100}
插入一个原本没存在的:{0=0, 1=1, 2=2, 5=5, 6=6, 7=7, 8=8, 9=9, 3=3, 4=100, 10=10}

从上面可以看出,每当我get或者put一个已存在的数据,就会把这个数据放到双向链表的尾部,put一个新的数据也会放到双向链表的尾部。

总结

相对于HashMap,LinkedHashMap的源码还是比较容易理解的,因为底层方式使用的是HashMap中的方法。值得注意的就是对于双向链表的维护,使其保存了插入时的顺序,并且通过accessOrder参数来控制是否实现LRU算法,LRU算法涉及到将最近使用(查询,修改,添加)过的数据取出并放在链表尾部,还涉及到当超出了容器大小时,要将最老的头节点删掉。

ps:用心学习,喜欢的话请点赞 (在左侧哦)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值