1. LinkedHashMap
的概念
LinkedHashMap
是HashMap
的子类LinkedHashMap
可以保持元素按插入或访问有序,这与TreeMap
按键排序不同
2. LinkedHashMap
的数据结构图
3. LinkedHashMap
的基本用法
LinkedHashMap
是HashMap
的 子类LinkedHashMap
内部还有一个双向链表维护键值对的顺序,每个键值对既位于哈希表中,也位于这个双向链表中LinkedHashMap
支持两种顺序:一种是插入顺序,一种是访问顺序
4. 怎样理解插入顺序和访问顺序
插入顺序
- 先添加的在前面,后添加的在后面
- 修改操作(对已有的键进行
put
)不影响排序
访问顺序
- 所谓访问是指
get/put
读写操作 - 对一个键执行
get/put
操作后,其对应的键值对会移到链表末尾 - 所以,最末尾的键值对是最近访问的,最开始的键值对是最久没被访问的,这种顺序就是访问顺序
- 所谓访问是指
5. LinkedHashMap
基本用法之构造方法
- 源码基于 Java 7
LinkedHashMap
有 5 个构造方法,其中 4 个都是按插入排序,只有一个构造方法可以指定按访问顺序,即:public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder)
- 参数
accessOrder
就是用来指定是否按访问顺序,如果为true
,就是按访问顺序
6. 下面代码的输出结果
1 | Map<String, Integer> seqMap = new LinkedHashMap<> (); |
- 结果:
c 100 d 300 a 500
- 分析:
LinkedHashMap
的默认构造方法是按插入有序的,修改d
的值不会修改顺序
7. 什么时候希望保持插入顺序呢,或者说保持插入顺序的使用场景有哪些
场景一
Map
经常用来处理一些数据,其处理模式是:接收一些键值对作为输入,进行处理,然后输出,输出时希望保持原来的顺序- 比如,一个配置文件,其中有一些键值对形式的配置项,但其中有一些键是重复的,希望保留最后一个值,但还是按原来的键顺序输出,这种情况下,
LinkedHashMap
就是一个合适的数据结构
场景二
- 希望的数据模型可能就是一个
Map
,但希望保持添加时的顺序 - 比如,一个购物车,键为购买的项目,值为购买数量或者价格,按用户添加时的顺序保存
- 希望的数据模型可能就是一个
场景三
- 希望
Map
能够按键有序,但在添加到Map
前,键已经通过其他方式排好序了。这时,就没有必要使用TreeMap
了,毕竟TreeMap
的开销要大一些 - 比如,从数据库查询数据放到内存时,可以使用
SQL
的order by
语句让数据库对数据排序
- 希望
8. 下面代码的输出结果是
1 | Map<String, Integer> accessMap = new LinkedHashMap<> (16, 0.75f, true); |
- 结果:
a 500 c 100 d 300
- 分析:
accessOrder
参数为true
,即指定按访问顺序排序。所以,每次访问都会将被访问的键值对移到双向链表的末尾
9. 怎样理解缓存
缓存:计算机技术中一种非常有用的技术,是一个通用的提升数据访问性能的思路,一般用来保存常用的数据,容量较小,但访问速度更快
- 缓存是相对主存而言的,主存的容量更大,但访问更慢
- 缓存的基本假设是:数据会被多次访问,一般访问数据时都先从缓存中找,缓存中没有再从主存中找,找到后再放入缓存,这样下次如果再找相同数据访问就快了
缓存用于计算机技术的各个领域
- CPU 里有缓存,有一级缓存、二级缓存、三级缓存等。一级缓存非常小、非常贵、也非常快,三级缓存则大一些、便宜一些、也慢一些。CPU 缓存是相对于内存而言的,它们都比内存快
- 内存里也有缓存,内存的缓存一般是相对于硬盘数据而言的
- 硬盘也可能是缓存,缓存网络上其他机器的数据,比如浏览器访问网页时,会把一些网页缓存到本地硬盘
LinkedHashMap
可以用于缓存,比如缓存用户基本信息,键是用户 ID,值是用户信息。所有用户信息可能保存在数据库中,部分活跃用户的信息可能保存在缓存中
10. 怎样理解 LRU 缓存替换算法
- 替换算法:一般而言,缓存容量有限,不能无限存储所有数据。如果缓存满了,当准备存储新数据时,就需要一定的策略将一些老的数据清理出去,这个策略一般称为替换算法
- 概念:LRU 是一种流行的替换算法,它的全称是:Least Recently Used,即最近最少使用
- 思路:最近刚被使用的很快再次被用的可能性最高,而最久没被访问的很快再次被用的可能性最低,所以被优先清理
11. 什么时候希望按访问有序呢,或者说按访问有序的使用场景有哪些
一种典型的应用是 LRU 缓存。使用
LinkedHashMap
,可以非常容易地实现 LRU 缓存默认情况下,
LinkedHashMap
没有对容量做限制,但它可以容易地做到LinkedHashMap
有一个protected
方法,即:protected boolean removeEldestEntry(Map.Entry<K, V> eldest) { return false; }
- 在添加元素到
LinkedHashMap
后,LinkedHashMap
会调用这个方法,传递的参数是最久没被访问的键值对 - 如果这个方法返回
true
,则这个最久没被访问的键值对就会被删除
LinkedHashMap
对这个方法的实现总是返回false
,所以容量没有限制,但子类可以重写这个方法,在满足一定条件的情况下返回true
,实现替换清理策略
12. 请实现一个 LRU 缓存
Demo
1
2
3
4
5
6
7
8
9
10
11
12
13
14public class LRUCache<K, V> extends LinkedHashMap<K, V> {
private int maxEntries;
public LRUCache(int maxEntries) { //在构造方法中传递一个容量限制
super(16, 0.75f, true);
this.maxEntries = maxEntries;
}
@Override
protected boolean removeEldestEntry(Entry<K, V> eldest) {
return size() > maxEntries;
}
}这个缓存可以这么用
1
2
3
4
5
6
7LRUCache<String, Object> cache = new LRUCache<> (3);
cache.put("a", "abstract");
cache.put("b", "basic");
cache.put("c", "call");
cache.put("a");
cache.put("d", "call");
System.out.print(cache);因为限定缓存容量为 3,先后添加了 4 个键值对,最久没被访问(最近最少使用)的键是
b
,会被删除。所以输出为:{c=call, a=abstract, d=call}
13. LinkedHashMap
的内部组成
源码基于 Java 7
LinkedHashMap
是HashMap
的子类,内部增加了如下实例变量1
2private transient Entry<K, V> header;
private final boolean accessOrder;accessOrder
表示是按访问顺序还是插入顺序header
表示双向链表的头,它的类型Entry
是一个内部类,这个类是HashMap.Entry
的子类,增加了两个变量before
和after
,指向双向链表中的前驱和后继
LinkedHashMap
中的Entry
的代码1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34private static class Entry<K, V> extends HashMap.Entry<K, V> {
Entry<K, V> before, after;
Entry(int hash, K key, V value, HashMap.Entry<K, V> next) {
super(hash, key, value, next);
}
private void remove() {
before.after = after;
after.before = before;
}
private void addBefore(Entry<K, V> existingEntry) {
after = existingEntry;
before = existingEntry.before;
before.after = this;
after.before = this;
}
void recordAccess(HashMap<K, V> m) {
LinkedHashMap<K, V> lm = (LinkedHashMap<K, V> m);
if(lm.accessOrder) {
lm.modCount++;
remove();
addBefore(lm.header);
}
}
void recordRemoval(HashMap<K, V> m) {
remove();
}
}recordAccess()
和recordRemoval()
是HashMap.Entry
中定义的方法,在HashMap
中,这两个方法的实现为空,它们就是被设计用来被子类重写的。在put()
被调用且键存在时,HashMap
会调用Entry
的recordAccess()
方法;在键被删除时,HashMap
会调用Entry
的recordRemoval()
方法LinkedHashMap.Entry
重写了这两个方法。在recordAccess()
方法中,如果是按访问顺序的,则将该节点移到链表的末尾;在recordRemoval()
方法中,将该节点从链表中移除LinkedHashMap
及其节点类LinkedHashMap.Entry
重写了父类的init()
方法、addEntry()
方法、createEntry()
方法、get()
方法等相关方法以维护这种关系
14. LinkedHashSet
的概念
LinkedHashSet
是HashSet
的子类- 它内部的
Map
的实现类是LinkedHashMap
,所以它也可以保持插入顺序
15. 下面代码的输出结果是
1 | Set<String> set = new LinkedHashSet<> (); |
- 结果:
[b, c, a]
- 分析:默认是按插入排序,修改操作(对已有的键进行
put
)不影响排序