JDK1.8源码逐字逐句带你理解LinkedHashMap底层
注意
我希望看这篇的文章的小伙伴如果没有了解过HashMap那么可以先看看我这篇文章:http://blog.csdn.net/u012403290/article/details/65442646,在这篇文章中我详细介绍了HashMap的底层实现和一些常见的成员变量。只有在对HashMap有一定的了解之后,才能很好的理解LinkedHashMap,因为它是继承HashMap实现的。所以对于加载因子,容量,桶的概念就不再赘述。
数据存储结构
我们已经知道HashMap是以散列表的形式存储数据的,LinkedHashMap继承了HashMap,所以LinkedHashMap其实也是散列表的结构,但是“linked”是它对HashMap功能的进一步增强,LinkedHashMap用双向链表的结构,把所有存储在HashMap中的数据连接起来。有人会说散列表不是已经有了链表的存储结构了嘛,为什么还要来个双向链表?桶(桶的概念就是数组的一个存储节点,比如说arr[0]是一个桶,arr[1]也是一个桶)中的链表和这个双向链表是两个概念,以下是我总结的区别:①桶中的链表是散列表结构的一部分;而双向链表是LinkedHashMap的额外引入;②桶中的链表只做数据存储,没有存储顺序的概念;双向链表的核心就是控制数据存储顺序(存储顺序是LinkedHashMap的核心);③桶中的链表产生是因为发生了hash碰撞,导致数据散落在一个桶中,用链表给予存储,所以这个链表控制了一个桶;双向链表是要串连所有的数据,也就是说有桶中的数据都是会被这个双向链表管理。
所以,我修改了HashMap的图片,大家参考下:
所以,简单来说就是LinkedHashMap相比于HashMap来说就是多了这些红色的双向链表而已。
两种演示
LinkedHashMap的核心就是存在存储顺序和可以实现LRU算法,所以下面我会用两个demo先来证明这两种情况:
①、放入到LinkedHashMap是有顺序的,会按照你放入的顺序存储:
package com.brickworkers;
import java.util.LinkedHashMap;
/**
* @author Brickworker
* Date:2017年4月12日下午12:46:25
* 关于类LinkedHashMapTest.java的描述:jdk1.8逐字逐句带你理解linkedHashMap
* Copyright (c) 2017, brcikworker All Rights Reserved.
*/
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());
map.get(3);
System.out.println("查询存在的某一个:"+map.toString());
map.put(4, 4);
System.out.println("插入已存在的某一个:"+map.toString()); //直接调用已存在的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}
// 查询存在的某一个:{0=0, 1=1, 2=2, 3=3, 4=4, 5=5, 6=6, 7=7, 8=8, 9=9}
// 插入已存在的某一个:{0=0, 1=1, 2=2, 3=3, 4=4, 5=5, 6=6, 7=7, 8=8, 9=9}
// 插入一个原本没存在的:{0=0, 1=1, 2=2, 3=3, 4=4, 5=5, 6=6, 7=7, 8=8, 9=9, 10=10}
}
观察以上代码,其实它是符合先进先出的规则的,不管你怎么查询插入已存在的数据,不会对排序造成影响,如果有新插入的数据将会放在最尾部。
②启用LRU规则的LinkedHashMap,启动这个规则需要在构造LinkedHashMap的时候,调用三个参数的构造器,这个构造器源码如下:
/**
* Constructs an empty <tt>LinkedHashMap</tt> instance with the
* specified initial capacity, load factor and ordering mode.
*
* @param initialCapacity the initial capacity
* @param loadFactor the load factor
* @param accessOrder the ordering mode - <tt>true</tt> for
* access-order, <tt>false</tt> for insertion-order
* @throws IllegalArgumentException if the initial capacity is negative
* or the load factor is nonpositive
*/
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;//是否开启LRU规则
}
第三个参数accessOrder就是用于控制LRU规则的。
如下就是我写的demo:
package com.brickworkers;
import java.util.LinkedHashMap;
/**
* @author Brickworker
* Date:2017年4月12日下午12:46:25
* 关于类LinkedHashMapTest.java的描述:jdk1.8逐字逐句带你理解linkedHashMap
* Copyright (c) 2017, brcikworker All Rights Reserved.
*/
public class LinkedHashMapTest {
public static void main(String[] args) {
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());
map.get(3);
System.out.println("查询存在的某一个:"+map.toString());
map.put(4, 4);
System.out.println("插入已存在的某一个:"+map.toString()); //直接调用已存在的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}
// 查询存在的某一个:{0=0, 1=1, 2=2, 4=4, 5=5, 6=6, 7=7, 8=8, 9=9, 3=3} //被访问(get)的3放到了最后面
// 插入已存在的某一个:{0=0, 1=1, 2=2, 5=5, 6=6, 7=7, 8=8, 9=9, 3=3, 4=4}//被访问(put)的4放到了最后面
// 插入一个原本没存在的:{0=0, 1=1, 2=2, 5=5, 6=6, 7=7, 8=8, 9=9, 3=3, 4=4, 10=10}//新增一个放到最后面
}
从上面可以看出,每当我get或者put一个已存在的数据,就会把这个数据放到双向链表的尾部,put一个新的数据也会放到双向链表的尾部。
逐字逐句底层源码
接下来我们通过源码深入学习LinkedHash,同时解答上述出现的有趣的事情。
①分析LinkedHashMap的类名和继承关系:
public class LinkedHashMap<K,V>
extends HashMap<K,V>
implements Map<K,V>
{
从这里我们可以看出LinkedHashMap是继承了HashMap并实现了Map接口的,所以它和HashMap的关系肯定不一般。
②分析LinkedHashMap的构造函数:
//1
public LinkedHashMap(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor);
accessOrder = false;
}
//2
public LinkedHashMap(int initialCapacity) {
super(initialCapacity);
accessOrder = false;
}
//3
public LinkedHashMap() {
super();
accessOrder = false;
}
//4
public LinkedHashMap(Map<? extends K, ? extends V> m) {
super();
accessOrder = false;
putMapEntries(m, false);
}
//5
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
它具有5个构造函数,可以设置容量和加载因子,且在默认情况下是不开启LRU规则的。同时它还以用具有继承K,V关系的map进行初始化。
③分析双向链表
/**
* HashMap.Node subclass for normal LinkedHashMap entries.
*/
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);
}
}
/**
* The head (eldest) of the doubly linked list.
*/
transient LinkedHashMap.Entry<K,V> head;//双向链表头节点(最老)
/**
* The tail (youngest) of the doubly linked list.
*/
transient LinkedHashMap.Entry<K,V> tail;//双向列表尾节点(最新)
用一个静态内部类Entry表示双向链表,实现了HashMap中的Node内部类。before和after表示前后指针。我们在使用LinkedHashMap有序就是因此产生。
④分析LRU规则实现,最近访问放在双向链表最后面
void afterNodeAccess(Node<K,V> e) { // 把当前节点e放到双向链表尾部
LinkedHashMap.Entry<K,V> 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;
//释放当前节点与后节点的关系
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;
}
}
afterNodeAccess方法就是如何支持LRU规则的,如果在accessOrder为true的时候,节点调用这个函数,就会把该节点放置到最后面。put,get等都会调用这个函数来调整顺序,我手画了一张图来表示这个函数干了些什么:
我们看看get方法中是否调用了此函数,以下是LinkedHashMap重写了HashMap的get方法:
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;
}
那么有些小伙伴就问了,那么put方法里面调用了嘛?肯定调用了,但是你不一定找得到,因为LinkedHashMap压根没有重写put方法,每次用LinkedHashMap的put方法的时候,其实你调用的都是HashMap的put方法。那为什么它也会执行afterNodeAccess()方法呢,因为这个方法HashMap就是存在的,但是没有实现,LinkedHashMap重写了afterNodeAccess()这个方法。下面给出HashMap的put局部方法:
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);//把当前节点移动到双向链表最尾处
return oldValue;
}
其实这个方法在很多的调用都有,这里就不一一解释了。同时,LinkedHashMap对于红黑树的节点移动处理也有专门的方法,这里就不再深入讲解了,方式也是差不多。
⑤分析一个LinkedHashMap自带的移除最头(最老)数据的方法
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return false;
}
LinkedHashMap有一个自带的移除最老数据的方法,但是这个方法永远是返回false,但是如果我们要实现,可以在继承的时候重写这个方法,给定一个条件就可以控制存储在LinkedHashMap中的最老数据何时删除。具体的在我以前博文多种方式实现缓存机制中有写过。触发这个删除机制,一般是在PUT一个数据进入的时候,但是LinkedHashMap并没有重写Put方法如何实现呢?在LinekdHashMap中,这个方法被包含在afterNodeInsertion()方法之中,而这个方法是重写了HashMap的,但是HashMap中并没有去实现它,所以在put的时候就会触发删除这个机制。
尾记
技术是不断前进的,或许在JDK1.8的时候我写的这些是有用的,但是下一个版本出来就不一定了。比如说前面几个版本中,LinkedHashMap是一个循环的双向链表,而且需要用init()方法进行初始化,但是后来都被移除了,以下是部分原本的linkedHashMap源码:
void init() {
header = new Entry<K,V>(-1, null, null, null);
header.before = header.after = header; //循环的双向链表
}
所以,在日常的学习中,尤其是技术,要与时俱进,在查询某个技术点的时候,千万要注意版本号,不一样的版本之间可能是天差地别的。
JDK1.8源码逐字逐句带你理解LinkedHashMap底层的更多相关文章
- JDK(六)JDK1.8源码分析【集合】LinkedHashMap
本文转载自joemsu,原文连接 [JDK1.8]JDK1.8集合源码阅读——LinkedHashMap LinkedHashMap的数据结构 可以从上图中看到,LinkedHashMap数据结构相比 ...
- Android AsyncTask完全解析,带你从源码的角度彻底理解
转载请注明出处:http://blog.csdn.net/guolin_blog/article/details/11711405 我们都知道,Android UI是线程不安全的,如果想要在子线程里进 ...
- [转]Android事件分发机制完全解析,带你从源码的角度彻底理解(上)
Android事件分发机制 该篇文章出处:http://blog.csdn.net/guolin_blog/article/details/9097463 其实我一直准备写一篇关于Android事件分 ...
- 【转】Android事件分发机制完全解析,带你从源码的角度彻底理解(下)
转载请注明出处:http://blog.csdn.net/guolin_blog/article/details/9153761 记得在前面的文章中,我带大家一起从源码的角度分析了Android中Vi ...
- [学习总结]7、Android AsyncTask完全解析,带你从源码的角度彻底理解
我们都知道,Android UI是线程不安全的,如果想要在子线程里进行UI操作,就需要借助Android的异步消息处理机制.之前我也写过了一篇文章从源码层面分析了Android的异步消息处理机制,感兴 ...
- JDK1.8源码阅读系列之三:Vector
本篇随笔主要描述的是我阅读 Vector 源码期间的对于 Vector 的一些实现上的个人理解,用于个人备忘,有不对的地方,请指出- 先来看一下 Vector 的继承图: 可以看出,Vector 的直 ...
- 微信小程序——智能小秘“遥知之”源码分享(语义理解基于olami)
微信小程序智能生活小秘书开发详解 >>>>>>>>>>>>>>>>>>>>> ...
- JDK1.8源码(三)——java.lang.String 类
String 类也是java.lang 包下的一个类,算是日常编码中最常用的一个类了,那么本篇博客就来详细的介绍 String 类. 1.String 类的定义 public final class ...
- 【集合框架】JDK1.8源码分析之HashMap(一) 转载
[集合框架]JDK1.8源码分析之HashMap(一) 一.前言 在分析jdk1.8后的HashMap源码时,发现网上好多分析都是基于之前的jdk,而Java8的HashMap对之前做了较大的优化 ...
随机推荐
- mysql指定编码集
DROP TABLE app_info CREATE TABLE `app_info` ( `app_id` ) NOT NULL COMMENT '应用ID', `) NOT NULL COMMEN ...
- InitializingBean和DisposableBean
InitializingBean 记住一点:InitializingBean接口为bean提供了初始化方法的方式,它只包括afterPropertiesSet方法,凡是继承该接口的子类,在初始化bea ...
- 雷林鹏分享:C# 基本语法
C# 基本语法 C# 是一种面向对象的编程语言.在面向对象的程序设计方法中,程序由各种相互交互的对象组成.相同种类的对象通常具有相同的类型,或者说,是在相同的 class 中. 例如,以 Rectan ...
- Shell函数和数组
函数的返回值用return,脚本的返回值用exit shell函数只允许返回数字,若不是则报line 6: return: num: numeric argument required:若是写了ret ...
- LeetCode--003--无重复字符的最长子串(java)
给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度. 示例 1: 输入: "abcabcbb" 输出: 3 解释: 因为无重复字符的最长子串是 "abc&qu ...
- linux文件管理之解压缩
文件的压缩与解压缩 Linux文件压缩工具有:gzip.bzip2.rar.7zip.lbzip2.xz.lrzip.PeaZip.arj等.============================= ...
- scrapy 爬虫框架之持久化存储
scrapy 持久化存储 一.主要过程: 以爬取校花网为例 : http://www.xiaohuar.com/hua/ 1. spider 回调函数 返回item 时 要用y ...
- Dropout的理解
https://zhuanlan.zhihu.com/p/23178423 这篇知乎文章讲的比较好,在神经网络权重取平均值和减少神经元之间复杂的共适应关系两个方面分析了为什么dropout可以解决过拟 ...
- 【洛谷p1932】A+B A-B A*B A/B A%B Problem
(emmmm) 这道题成功让我见识到了Dev撤回的高端大气上档(dàng)次. A+B A-B A*B A/B A%B Problem[传送门](真是个优秀的高精) 算法:::::::(模板题弄这么费 ...
- 5月21 练习AJAX的查看详细及批量删除
老师讲过之后的复习: 显示数据的代码部分: <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" &q ...