LinkedHashMap概述

  JDK对LinkedHashMap的介绍:

Hash table and linked list implementation of the Map interface, with predictable iteration order. This implementation differs from HashMap in that it maintains a doubly-linked list running through all of its entries. This linked list defines the iteration ordering, which is normally the order in which keys were inserted into the map (insertion-order). Note that insertion order is not affected if a key is re-inserted into the map. (A key k is reinserted into a map m if m.put(k, v) is invoked when m.containsKey(k) would return true immediately prior to the invocation.)

大意是:LinkedHashMap是通过哈希表和链表来实现Map接口,它通过维护一个链表来保证对哈希表迭代时的有序性,而这个有序是指键值对插入的顺序。另外,当向哈希表中重复插入某个键的时候,不会影响到原来的有序性。

继承结构

  可以看到LinkedHashMap直接继承了HashMap,复用了HashMap的很多方法,比如put、resize等方法,LinkedHashMap是在HashMap的基础上实现了自己的功能:有序插入。

数据结构

  可以看到,LinkedHashMap数据结构相比较于HashMap来说,添加了双向指针,其中before指向节点的前继节点,after指向节点的后继节点,从而将所有的节点串联在一起形成一个双向链表。

内部字段及构造方法

内部字段

	//双链表头节点
transient Entry<K,V> head;
//双链表尾节点
transient Entry<K,V> tail;
//accessOrder为true则表示按照基于访问的顺序来排列,意思就是最近使用的entry,
//放在链表的最末尾,为false表示按照基于插入的顺序来排列,后插入的放在链表末尾,不指定默认为false
final boolean accessOrder; 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);
}
}

构造方法

  可以看到,LinkedHashMap调用了父类的构造方法,而且默认的accessOrder是false,至于是怎么通过accessOrder控制元素顺序,我们将在方法讲到。

	//指定accessOrder的值
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
//按照默认值初始化
public LinkedHashMap() {
super();
accessOrder = false;
}
//指定初始化时的容量
public LinkedHashMap(int initialCapacity) {
super(initialCapacity);
accessOrder = false;
}
//指定初始化时的容量,和扩容的加载因子
public LinkedHashMap(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor);
accessOrder = false;
}

存储元素

  LinkedHashMap并没有重写父类的put方法,所以增加元素调用的是父类方法,具体来说是putVal方法,下面是HashMap的putVal方法:

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
...
tab[i] = newNode(hash, key, value, null);
...
afterNodeAccess(e);
...
afterNodeInsertion(evict);
...

LinkedHashMap重写了newNode和回调方法afterNodeAccess、afterNodeInsertion:

    //在构建新节点时,构建的是LinkedHashMap.Entry 不再是Node.
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
Entry<K,V> p =
new Entry<K,V>(hash, key, value, e);
linkNodeLast(p);
return p;
} //将新增的节点,连接在链表的尾部
private void linkNodeLast(Entry<K,V> p) {
Entry<K,V> last = tail;
tail = p;
//若集合是空的
if (last == null)
head = p;
//新节点插到链表顶部
else {
p.before = last;
last.after = p;
}
} //仅仅在accessOrder为true时进行,把当前访问的元素移动到链表尾部
void afterNodeAccess(Node<K,V> e) { // move node to last
Entry<K,V> last;
//当accessOrder的值为true,且e不是尾节点
if (accessOrder && (last = tail) != e) {
//将e赋值临时节点p, b是e的前一个节点, a是e的后一个节点
Entry<K,V> p =
(Entry<K,V>)e, b = p.before, a = p.after;
//设置p的后一个节点为null,因为执行后p在链表末尾,after肯定为null
p.after = null;
//p的前一个节点不存在,p就是头节点,那么把p放到最后,a就是头节点
if (b == null)
head = a;
//p的前一个节点存在,p放到最后,b的后一个节点指向a
else
b.after = a;
//p的后一个节点存在,p放到最后,a的前一个节点指向a
if (a != null)
a.before = b;
//p的后一个节点不存在
else
last = b;
//只有一个p节点
if (last == null)
head = p;
//last不为空,把p放到last节点后面
else {
p.before = last;
last.after = p;
}
//p为尾节点
tail = p;
++modCount;
}
} //回调函数,新节点插入之后回调 , 根据evict和accessOrder判断是否需要删除最老/早插入的节点。
//如果实现LruCache会用到这个方法。
//removeEldestEntry制定删除规则,JDK8中默认返回false
void afterNodeInsertion(boolean evict) { // possibly remove eldest
Entry<K,V> first;
if (evict && (first = head) != null && removeEldestEntry(first)) {
K key = first.key;
removeNode(hash(key), key, null, false, true);
}
}

注:HashMap定义了三个回调方法,用于LinkedHashMap维持有序:

    // Callbacks to allow LinkedHashMap post-actions
void afterNodeAccess(Node<K,V> p) { }
void afterNodeInsertion(boolean evict) { }
void afterNodeRemoval(Node<K,V> p) { }

取出元素

  LinkedHashMap的get方法,调用HashMap的getNode方法后,对accessOrder的值进行了判断,我们之前提到:accessOrder为true时进行,把当前访问的元素移动到链表尾部,调用重写的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;
}

删除元素

  LinkedHashMap删除元素调用的是弗雷德remove方法,在removeNode设置了回调方法afterNodeRemoval

    final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
...
afterNodeRemoval(node);
...

LinkedHashMap回调方法的实现:

    //在删除节点e时,同步将e从双向链表上删除
void afterNodeRemoval(Node<K,V> e) { // unlink
Entry<K,V> p =
(Entry<K,V>)e, b = p.before, a = p.after;
//待删除节点 p 的前置后置节点都置空,相当于从双链表上取下来
p.before = p.after = null;
//p是尾节点,它的前一个节点为空,那么它的后一个节点做头节点
if (b == null)
head = a;
//p的前一个节点不为空,把它的后一个节点指向a
else
b.after = a;
//同理,a为空,b就是尾节点
if (a == null)
tail = b;
//a不为空,
else
a.before = b;
}

迭代器

    abstract class LinkedHashIterator {
//记录下一个迭代的节点
Entry<K,V> next;
//当前迭代的节点
Entry<K,V> current;
//用于fail-fast机制
int expectedModCount; //迭代器初始化next指向head
LinkedHashIterator() {
next = head;
expectedModCount = modCount;
current = null;
} public final boolean hasNext() {
return next != null;
} //链表方式迭代
final Entry<K,V> nextNode() {
Entry<K,V> e = next;
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
if (e == null)
throw new NoSuchElementException();
current = e;
//双链表的后继节点节点指向next
next = e.after;
return e;
} public final void remove() {
Node<K,V> p = current;
if (p == null)
throw new IllegalStateException();
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
current = null;
K key = p.key;
removeNode(hash(key), key, null, false, false);
expectedModCount = modCount;
}
}

利用LinkedHashMap简单实现LRU算法

  在查阅相关资料时,都提到利用LinkedHashMap实现LRU算法,首先介绍一下LRU(Least Recently Used)算法:

LRU(Least recently used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。也就是说,当有限的空间已存满数据时,应当把最久没有被访问到的数据淘汰。

根据上面的源码,我们知道当把accessOrder设为true时,LinkedHashMap就会根据访问顺序排序,把最近访问过的元素放在链表的尾部,而没有访问的元素就放在链表头部,只要重写removeEldestEntry设置丢弃头部节点条件就行了。这样就可以简单实现LRU算法了

public class LRUCache<K, V> {
private final int CACHE_SIZE;
private final float DEFAULT_LOAD_FACTORY = 0.75f;
LinkedHashMap<K, V> map;
public LRUCache(int cacheSize) {
CACHE_SIZE = cacheSize;
int capacity = (int)Math.ceil(CACHE_SIZE / DEFAULT_LOAD_FACTORY) + 1;
map = new LinkedHashMap<K,V>(capacity, DEFAULT_LOAD_FACTORY, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return map.size() > cacheSize;
}
};
} public void put(K key, V value) {
map.put(key, value);
} public V get(K key) {
return map.get(key);
} public void remove(K key) {
map.remove(key);
} @Override
public String toString() {
StringBuilder stringBuilder = new StringBuilder();
for (Map.Entry<K, V> entry : map.entrySet()) {
stringBuilder.append(String.format("%s: %s ", entry.getKey(), entry.getValue()));
}
return stringBuilder.toString();
} public static void main(String[] args) {
LRUCache<Integer, Integer> cache = new LRUCache<>(5);
cache.put(1,1);
cache.put(2,2);
cache.put(3,3);
System.out.println(cache);
cache.get(1);
cache.put(4,4);
cache.put(5,5);
cache.put(6,6);
System.out.println(cache);
}
}

结果:

超出范围后,1:1键值对被访问过,最早没有被访问过的2:2键值对就被丢弃了。

总结

  LinkedHashMap基于HashMap,所谓大树底下好乘凉,重写了部分代码就能够实现有序插入。LinkedHashMap总体上较为简单,在理解了HashMap的基础上就能很快理解LinkedHashMap的实现。

JDK源码分析(四)——LinkedHashMap的更多相关文章

  1. JDK 源码分析(4)—— HashMap/LinkedHashMap/Hashtable

    JDK 源码分析(4)-- HashMap/LinkedHashMap/Hashtable HashMap HashMap采用的是哈希算法+链表冲突解决,table的大小永远为2次幂,因为在初始化的时 ...

  2. 【JDK】JDK源码分析-LinkedHashMap

    概述 前文「JDK源码分析-HashMap(1)」分析了 HashMap 主要方法的实现原理(其他问题以后分析),本文分析下 LinkedHashMap. 先看一下 LinkedHashMap 的类继 ...

  3. JDK源码分析—— ArrayBlockingQueue 和 LinkedBlockingQueue

    JDK源码分析—— ArrayBlockingQueue 和 LinkedBlockingQueue 目的:本文通过分析JDK源码来对比ArrayBlockingQueue 和LinkedBlocki ...

  4. 【JDK】JDK源码分析-HashMap(1)

    概述 HashMap 是 Java 开发中最常用的容器类之一,也是面试的常客.它其实就是前文「数据结构与算法笔记(二)」中「散列表」的实现,处理散列冲突用的是“链表法”,并且在 JDK 1.8 做了优 ...

  5. 【JDK】JDK源码分析-TreeMap(2)

    前文「JDK源码分析-TreeMap(1)」分析了 TreeMap 的一些方法,本文分析其中的增删方法.这也是红黑树插入和删除节点的操作,由于相对复杂,因此单独进行分析. 插入操作 该操作其实就是红黑 ...

  6. 【JDK】JDK源码分析-Vector

    概述 上文「JDK源码分析-ArrayList」主要分析了 ArrayList 的实现原理.本文分析 List 接口的另一个实现类:Vector. Vector 的内部实现与 ArrayList 类似 ...

  7. 【JDK】JDK源码分析-AbstractQueuedSynchronizer(2)

    概述 前文「JDK源码分析-AbstractQueuedSynchronizer(1)」初步分析了 AQS,其中提到了 Node 节点的「独占模式」和「共享模式」,其实 AQS 也主要是围绕对这两种模 ...

  8. JDK源码分析(三)—— LinkedList

    参考文档 JDK源码分析(4)之 LinkedList 相关

  9. JDK源码分析(一)—— String

    dir 参考文档 JDK源码分析(1)之 String 相关

随机推荐

  1. Javascript Ajax异步读取RSS文档

    RSS 是一种基于 XML的文件标准,通过符合 RSS 规范的 XML文件可以简单实现网站之间的内容共享.Ajax 是Asynchronous JavaScript and XML的缩写.通过 Aja ...

  2. ETL testing

    https://www.tutorialspoint.com/etl_testing/index.htm querysurge-installer-6.0.5-linux-x64  测试ETL的工具.

  3. 20155306 2016-2017-2 《Java程序设计》第七周学习总结

    20155306 2016-2017-2 <Java程序设计>第七周学习总结 教材学习内容总结 第十三章 时间与日期 三种时间: 格林威治标准时间(GMT)的正午是太阳抵达天空最高点之时, ...

  4. DockerFile指令集

     FROM            语法:FROM <image>[:<tag>]         解释:设置要制作的镜像基于哪个镜像,FROM指令必须是整个Dockerfile ...

  5. 多进程+协程 处理IO问题

    from multiprocessing import Pool import gevent,os import time def recursion(n): if n == 1 or n ==2: ...

  6. 《区块链100问》第85集:资产代币化之对标美元USDT

    USDT是Tether公司推出的对标美元(USD)的代币Tether USD.1USDT=1美元,用户可以随时使用USDT与USD进行1:1兑换.Tether公司执行1:1准备金保证制度,即每个USD ...

  7. sparse coding

    Deep Learning(深度学习)学习笔记整理系列 zouxy09@qq.com http://blog.csdn.net/zouxy09 作者:Zouxy version 1.0 2013-04 ...

  8. opencv 摄像头

    VideoCapture cap(); if(!cap.isOpened()) ; Mat frame, edges; namedWindow(); for(;;) { cap >> fr ...

  9. 解决多个python的兼容问题

    方法1:将(安装路径和scripts)路径添加到系统环境变量,谁的顺序在前面谁就是默认的 方法2:修改python的名字,然后再终端输入比如python2或者python3

  10. 解决 Electron 包下载太慢问题

    项目下新建 .npmrc 文件,加入如下配置: electron_mirror=https://npm.taobao.org/mirrors/electron/ 即使用淘宝的源,重新 npm inst ...