再谈数据结构,LinkedHashMap的小秘密都在这里

linkedHashMap.put(2, 2);

linkedHashMap.put(3, 3);

for (Map.Entry<Integer, Integer> a : linkedHashMap.entrySet()) {

Log.e(“TAG”, “key->” + a.getKey() + “”);

Log.e(“TAG”, “value->” + a.getValue() + “”);

}

}

}

2019-11-21 14:34:17.708 3421-3421/com.we.we E/TAG: key->2

2019-11-21 14:34:17.708 3421-3421/com.we.we E/TAG: value->2

2019-11-21 14:34:17.708 3421-3421/com.we.we E/TAG: key->3

2019-11-21 14:34:17.708 3421-3421/com.we.we E/TAG: value->3

和第一个例子相比,在LinkedHashMap实例化的过程中,重写了removeEldestEntry()方法,并根据当前linkedHashMap.size()和设置容量的判断结果返回数据,明明put了3条数据,打印却只打印了最后put的2条,这一结果证实了2点。

  • LinkedHashMap的容量是可控的。

  • LinkedHashMap是有插入顺序的。

那LinkedHashMap访问有序是怎么体现的呢?是直接调用get()方法就会自动排序么?我们写代码测试一下。例 3

public class LinkedHashMapActivity extends AppCompatActivity {

LinkedHashMap<Integer, Integer> linkedHashMap;

@Override

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_linked_hash_map);

linkedHashMap = new LinkedHashMap<Integer, Integer>(2) {

@Override

protected boolean removeEldestEntry(Entry eldest) {

return linkedHashMap.size() > 2;

}

};

linkedHashMap.put(1, 1);

linkedHashMap.put(2, 2);

//调用get进行排序

linkedHashMap.get(1);

for (Map.Entry<Integer, Integer> a : linkedHashMap.entrySet()) {

Log.e(“TAG”, “key->” + a.getKey() + “”);

Log.e(“TAG”, “value->” + a.getValue() + “”);

}

}

}

2019-11-21 14:55:08.481 3843-3843/com.we.we E/TAG: key->1

2019-11-21 14:55:08.481 3843-3843/com.we.we E/TAG: value->1

2019-11-21 14:55:08.481 3843-3843/com.we.we E/TAG: key->2

2019-11-21 14:55:08.481 3843-3843/com.we.we E/TAG: value->2

还是以上的例子,我们在put完以后调用了get(1)再打印日志发现并没有按我们预想的顺序将1放在最后,因为LinkedHashMap按访问顺序排序默认是关闭的,可通过构造函数实例化的时候打开。比如这样:例 4

public class LinkedHashMapActivity extends AppCompatActivity {

LinkedHashMap<Integer, Integer> linkedHashMap;

@Override

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_linked_hash_map);

//主要看最后这个参数,默认是false。

linkedHashMap = new LinkedHashMap<Integer, Integer>(2,0.75f,true) {

@Override

protected boolean removeEldestEntry(Entry eldest) {

return linkedHashMap.size() > 2;

}

};

linkedHashMap.put(1, 1);

linkedHashMap.put(2, 2);

//调用get进行排序

linkedHashMap.get(1);

for (Map.Entry<Integer, Integer> a : linkedHashMap.entrySet()) {

Log.e(“TAG”, “key->” + a.getKey() + “”);

Log.e(“TAG”, “value->” + a.getValue() + “”);

}

}

}

2019-11-21 15:07:46.672 4071-4071/com.we.we E/TAG: key->2

2019-11-21 15:07:46.672 4071-4071/com.we.we E/TAG: value->2

2019-11-21 15:07:46.672 4071-4071/com.we.we E/TAG: key->1

2019-11-21 15:07:46.672 4071-4071/com.we.we E/TAG: value->1

在初始化过程开启访问排序后,在通过日志发现,1已经排在的最后,达到了访问排序的效果。以上的几个例子是LinkedHashMap的基本使用,但是具体为什么能达到这种效果,接下来我们深入源码了解LinkedHashMap的原理。

LinkedHashMap的原理是什么?

怎么实现的?

首先我们了解LinkedHashMap的继承关系图,实线代表继承关系,虚线代表实现接口。

以上的继承关系中可以发现LinkedHashMap继承于HashMap,因此LinkedHashMap底层也是基于链表,如果JDK1.8以上就是链表+红黑树。而LinkedHashMap的不同之处就在于它又多维护了一个双向链表。

LinkedHashMap的双向链表对象都包含什么属性?

LinkedHashMap的双向链表对象是通过LinkedHashMapEntry对象来维护的,具体我们看下源码。

//LinkedHashMap.LinkedHashMapEntry

//继承于 HashMap.Node,获得普通链表的能力,同时通过内部属性 before, after;维护双向链表。

static class LinkedHashMapEntry<K,V> extends HashMap.Node<K,V> {

//用来维护双向链表。

LinkedHashMapEntry<K,V> before, after;

LinkedHashMapEntry(int hash, K key, V value, Node<K,V> next) {

super(hash, key, value, next);

}

}

// HashMap.Node

// 链表元素对象

static class Node<K,V> implements Map.Entry<K,V> {

final int hash;

//元素的key

final K key;

//值value

V value;

//下一个元素的数据

Node<K,V> next;

…省略若干代码

}

// HashMap.TreeNode

//红黑树对象,主要看继承关系,如需了解内部逻辑可通过HashMap原理进行了解。

static final class TreeNode<K,V> extends LinkedHashMap.LinkedHashMapEntry<K,V> {

TreeNode<K,V> parent; // red-black tree links

TreeNode<K,V> left;

TreeNode<K,V> right;

TreeNode<K,V> prev; // needed to unlink next upon deletion

boolean red;

TreeNode(int hash, K key, V val, Node<K,V> next) {

super(hash, key, val, next);

}

}

以上源码中LinkedHashMapEntry就是LinkedHashMap中的每一个键值对对象,其中通过before, after两个属性维护了当前键值对的一前一后两个对象。以上源码的继承关系如下图所示:

补充一点小知识

HashMap.TreeNode继承LinkedHashMap.LinkedHashMapEntry,而LinkedHashMap.LinkedHashMapEntry又继承HashMap.Node,那为什么 HashMap.TreeNode不直接继承HashMap.Node?绕这一圈为的是什么?其实LinkedHashMapEntry有组成双向链表的能力,继承了它就获得了这个能力,但是TreeNode 就多了两个用不到的引用 (LinkedHashMapEntry<K,V> before, after;);同时内存占用也会比较大,这么做是为啥?官方解释是说TreeNode是Node内存占用的2倍左右,但是TreeNode是在HashMap的桶中的节点足够多的时候才会使用,(JDK1.8 当一个桶中的元素超过8才会转换为红黑树(TreeNode),<6又转回链表(Node)) 如果hash算法不是很差的话TreeNode使用的几率是比较低的,因此浪费那一点空间换取了组成双向链表的能力是值得的。如果TreeNode直接 extends Node 则需要Node extentds LinkedHashMapEntry,Node使用是很频繁的,这样空间浪费的就太多了。

LinkedHashMap对象是怎么维护每一个双向链表对象的?

既然LinkedHashMap内的每一个元素都包含其前后两个元素对象,那必然在put对象的过程中就进行了维护,因此我们先看下LinkedHashMap.put方法。因为LinkedHashMap继承于HashMap,并且LinkedHashMap没有重写put(),因此调用的就是父类HashMap的put,我们看下源码。

//HashMap的put()方法,同时也是LinkedHashMap的put方法

public V put(K key, V value) {

return putVal(hash(key), key, value, false, true);

}

//直接看关键部分的代码,看我注释的部分

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {

Node<K,V>[] tab; Node<K,V> p; int n, i;

if ((tab = table) == null || (n = tab.length) == 0)

n = (tab = resize()).length;

if ((p = tab[i = (n - 1) & hash]) == null)

//调用了newNode()方法,这里用到了一个面向对象里多态的特性,而LinkedHashMap重写了newNode()方法

//如果是LindedHashMap对象调用,触发的是LinkedHashMap重写的newNode()

tab[i] = newNode(hash, key, value, null);

else {

Node<K,V> e; K k;

if (p.hash == hash &&

((k = p.key) == key || (key != null && key.equals(k))))

e = p;

else if (p instanceof TreeNode)

e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);

else {

for (int binCount = 0; ; ++binCount) {

if ((e = p.next) == null) {

p.next = newNode(hash, key, value, null);

if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st

treeifyBin(tab, hash);

break;

}

if (e.hash == hash &&

((k = e.key) == key || (key != null && key.equals(k))))

break;

p = e;

}

}

//有重复的key,则用待插入值进行覆盖,返回旧值。

if (e != null) { // existing mapping for key

V oldValue = e.value;

if (!onlyIfAbsent || oldValue == null)

e.value = value;

//HashMap中该方法没做任何操作,LinkedHashMap进行了重写实现,并调用。

//该方法主要将当前节点移动到双向链表尾部,后续进行详细分析

afterNodeAccess(e);

return oldValue;

}

}

++modCount;

if (++size > threshold)

resize();

//每次put新元素都会调用(如果是更新已有元素则不调用,因为没有新增元素,不会导致size+1)

//但是HashMap中该方法没做任何操作, LinkedHashMap进行了重写实现.

//主要用于移除最早的元素,后续详细解析

afterNodeInsertion(evict);

return null;

}

//HashMap内的newNode

Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {

return new Node<>(hash, key, value, next);

}

//LinkedHashMap重写的newNode

Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {

//构造带双向链表属性的对象。

LinkedHashMapEntry<K,V> p =

new LinkedHashMapEntry<K,V>(hash, key, value, e);

//双向链表维护

linkNodeLast§;

return p;

}

private void linkNodeLast(LinkedHashMapEntry<K,V> p) {

//首先获取当前链表的最后一个元素

LinkedHashMapEntry<K,V> last = tail;

//当前插入的元素定义为最后一个元素

tail = p;

if (last == null)

//如果之前的最后元素是null,说明之前的链表就是空的,所以当前的元素是一个元素。

head = p;

else {

//如果之前的链表不是null

//put前的最后一个元素设置为当前put元素的前一个。

p.before = last;

//当前put元素设置为put前最后一个元素的下一个

last.after = p;

}

}

源码有点长,可以直接看我注释部分,注释部分完全可以描述新增键值对时双向链表的维护细节了。其实以上的大段逻辑做的事很简单。

  1. 先创建新的元素,通过hash计算位置并存放。

  2. 每次newNode创建元素时都连带调用linkNodeLast()将新插入元素的双向链表关系维护起来(新插入的放末尾)。

  3. 每次put新元素均通过LinkedHashMap中重写的afterNodeInsertion()判断是否删除头节点元素。

//这个方法是在HashMap的代码里调用的,在put方法中调用的时候参数evict传的是true

void afterNodeInsertion(boolean evict) { // possibly remove eldest

LinkedHashMapEntry<K,V> first;

//evict是true,(first = head)=true,而removeEldestEntry()方法默认返回的是false,因此if内的逻辑默认是不执行的。

if (evict && (first = head) != null && removeEldestEntry(first)) {

K key = first.key;

//移除链表头部的元素

removeNode(hash(key), key, null, false, true);

}

}

//HashMap中的removeNode方法,主要用于删除某一个元素

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;

…省略若干代码

}

通过以上代码发现,其实每次调用afterNodeInsertion()方法,只是内部的if判断中removeEldestEntry()默认返回false ,因此移除链表头部元素的逻辑没能执行,但是可通过重写removeEldestEntry()方法手动返回true,这样可以做到每添加一个元素都移除头部的元素。如果put的key已存在则用新值覆盖并调用LinkedHashMap重写的afterNodeAccess()将新值移动到链表尾部。

void afterNodeAccess(Node<K,V> e) { // move node to last

LinkedHashMapEntry<K,V> last;

//accessOrder 是LinkedHashMap实例化的时候的入参,需手动传true该方法的逻辑才会启用。

if (accessOrder && (last = tail) != e) {

LinkedHashMapEntry<K,V> p = (LinkedHashMapEntry<K,V>)e, b = p.before, a = p.after;

//p为当前尾节点,因此p.after = null;

p.after = null;

//p.before==null,说明p以前是头节点,但是现在需要把p放在尾节点,则以前p.after就变成了头节点。

if (b == null)

head = a;

else

//原来的顺序是b <-> p <-> a…现在p需要移动到尾部,则就变成了b <-> a… <->p

b.after = a;

if (a != null)

//原来a.before是p,现在p移动走了,那p.before就变成了a.before

a.before = b;

else

//如果之前p就是尾节点,则将last引用p.before

last = b;

if (last == null)

//如果原来尾节点是空,则说明当前链表只有一个节点,因此head引用p

head = p;

else {

//之前尾节点不为空,因为p移动到了最后,因此p.before引用原尾节点,原尾节点.after引用p

p.before = last;

last.after = p;

}

tail = p;

//计数器自增1

++modCount;

}

}

以上复杂的操作均是处理链表移动的各种异常情况,最终目的是将put的元素移动到链表末端。注意:在afterNodeAccess()函数中,会修改modCount变量,迭代LinkedHashMap时,如果同时查询访问数据,也会导致fail-fast,因为迭代的顺序已经改变。**LinkedHashMap为什么调用get()就会触发排序?**以上例 4已经展示了调用get()排序的场景,如不记得例4的逻辑,可上翻查看。老规矩吧,点看源码一顿瞅…

//LinkedHashMap重写的get方法

public V get(Object key) {

Node<K,V> e;

//getNode()调用是HashMap.getNode()

if ((e = getNode(hash(key), key)) == null)

return null;

if (accessOrder)

//哈哈哈,看到这我一下子就明白他怎么做的排序了。

afterNodeAccess(e);

return e.value;

}

LinkedHashMap每次调用get方法,在结果返回前就触发了afterNodeAccess(),而afterNodeAccess()作用就是将当前元素移动到尾节点。**LinkedHashMap调用remove()后链表怎么维护?**首先我们猜下,如果是我们自己实现这个逻辑需要怎么操作?比如现在调用remove(),删了了链表中间的一个元素,应该怎么维护?

  1. 首先我们通过当前元素前后的元素获取前后节点,然后让前后节点做关联,但是需要注意首位节点。

  2. 如果LinkedHashMap重写了remove()则在重写的方法里实现逻辑,如果调用的是HashMap.remove()则需要有和afterNodeAccess()类似的after…()方法进行重写实现,且after…()方法需在remove()方法中调用。

我们验证下猜想是否正确。

//HashMap.remove()

public V remove(Object key) {

Node<K,V> e;

return (e = removeNode(hash(key), key, null, false, true)) == null ?

null : e.value;

}

//HashMap.removeNode(),核心的删除方法

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;

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

}

}

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;

//维护双向链表的after…()方法,在HashMap中该方法是空方法。

afterNodeRemoval(node);

return node;

最后

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助

因此我收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
e;

//维护双向链表的after…()方法,在HashMap中该方法是空方法。

afterNodeRemoval(node);

return node;

最后

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助

因此我收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

[外链图片转存中…(img-tD4EtlPe-1715884459126)]

[外链图片转存中…(img-5efiavwI-1715884459127)]

[外链图片转存中…(img-xxU3G1lH-1715884459128)]

[外链图片转存中…(img-cMMxooav-1715884459130)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值