这部分的面试题并不多,在问到HashMap的时候,可以做个加分项吧!
自己给自己提问题吧~
---------------------------------------------------------- 美丽的分割线 ----------------------------------------------------------
浏览一遍 LinkedHashMap 注释 和 源码 …
LinkedHashMap 继承于HashMap(站在巨人的肩膀上),自然拥有了HashMap的所有特性。包括 数组 + 链表/红黑树 的数据结构外,还有 一个独有的特性(敲黑板) ---- 按顺序迭代的特性。可以设置为 按插入的顺序 或者 访问的顺序(LRU)。
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.
蹩脚翻译一下:
具有 可预测的迭代顺序Map 接口实现,采用 哈希表 和 链表 实现。此实现与 HashMap 的不同之处在于 它维护了一个贯穿其所有条目的双向链表 。此链接列表定义迭代排序,默认(通常)是键插入映射的顺序(插入顺序)。 注意,如果将键重新插入到Map中,则插入顺序不会受到影响。(如果在m.containsKey时调用 m.put(k,v),则将键k 重新插入到map m 中,会在调用之前立即返回true 。)
来看一下 LinkedHashMap 的真面目,上代码:
继承关系:
public class LinkedHashMap extends HashMap implements Map
为什么继承了 HashMap 还要实现 Map 接口:这里再次实现Map接口,可能只是强调,LinkedHashMap是Map的一员,如果不实现也没问题。(传送门:https://stackoverflow.com/questions/3854748/why-do-many-collection-classes-in-java-extend-the-abstract-class-and-implement-t)
成员变量:
/**
* 双向链表的头结点(最老).
*/
transient LinkedHashMap.Entry head;
/**
* 双向链表的尾结点(最年轻).
*/
transient LinkedHashMap.Entry tail;
/**
* Map 的迭代顺序:
* true for access-order, false for insertion-order.
*/
final boolean accessOrder;
讲道理,LiekedHashMap 需要维护双向链表,那么就需要重写 HahsMap 中的 crud 的方法,如 put(K k,V v),get(K k) 等等。但是,但是,但是,并没有!!!看一下 LinkedHashMap 的所有方法:
LiekedHashMap 只是重写 以上这些 HashMap 的部分方法,那么 LiekedHashMap 是怎么维护这个双向聊表呢?
(1)双向链表的 创建 和 添加:
还是从 HashMap 的 put 方法说起,HashMap的put 方法中调用的方法里,newNode 、afterNodeAccess、afterNodeInsertion方法被重写了。撸一下代码:
Node newNode(int hash, K key, V value, Node e) {
// 新建一个 Entry 节点
LinkedHashMap.Entry p =
new LinkedHashMap.Entry(hash, key, value, e);
// 添加到 双向链表 的尾部
linkNodeLast(p);
return p;
}
private void linkNodeLast(LinkedHashMap.Entry p) {
// 拿到 尾节点
LinkedHashMap.Entry last = tail;
// 新添加的节点 作为尾节点(并未添加带链表中)
tail = p;
// 双向链表为空 -- 当前节点 作为 头结点
// 双向链表不为空 -- 尾节点 添加到 链表中
if (last == null)
head = p;
else {
p.before = last;
last.after = p;
}
}
双向链表的添加,删除等操作 移步其他博客学习吧~(就是懒的画图哈哈哈 ~~~ )。
到这就大概知道了 LinkedHashMap 的双向链表插入过程:在 put 时通过重写的newNode方法新建节点到链表中 。
再来看看 afterNodeAccess、afterNodeInsertion 方法:
/**
* HashMap 插入函数 的 回调
* @param evict 是否 删除 最老的元素
*/
void afterNodeInsertion(boolean evict) { // possibly remove eldest
LinkedHashMap.Entry first;
// removeEldestEntry(first) 判断 是否满足删除条件,默认 false,留给子类重写
if (evict && (first = head) != null && removeEldestEntry(first)) {
K key = first.key;
// 移除头节点
removeNode(hash(key), key, null, false, true);
}
}
protected boolean removeEldestEntry(Map.Entry eldest) {
return false;
}
void afterNodeAccess(HashMap.Node e) { // move node to last
LinkedHashMap.Entry last;
// 判断 是否是 根据 访问 顺序迭代
if (accessOrder && (last = tail) != e) {
LinkedHashMap.Entry p =
(LinkedHashMap.Entry)e, b = p.before, a = p.after;
// 把 当前节点 移动到链表尾部 -- 成为最新的节点
p.after = null;
if (b == null)
head = a;
else
b.after = a;
if (a != null)
a.before = b;
else
last = b;
if (last == null)
head = p;
else {
p.before = last;
last.after = p;
}
tail = p;
++modCount;
}
}
总结一下 LinkedHashMap 的 put 过程中 如何维护双向链表:
a. 如果插入节点 K 不存在,需要新建为新节点,那么调用newNode方法,在新建节点的同时,添加本节点到链表的尾部,成为最新的节点 ;
b. 如果插入节点的 K 已存在,需要覆盖旧值时, 如果 accessOrder = true(也就按照访问顺序遍历),那么 会将当前节点移动到链表的尾部,成为最新的节点 。
(2)get 方法,被 LinkedHashMap 重写,没什么特别的,只是在查找到 元素时,如果按访问顺序遍历,就调整一下当前节点 到 链表尾部
public V get(Object key) {
Node e;
if ((e = getNode(hash(key), key)) == null)
return null;
// 如果是 按访问顺序,调整当前节点的位置
if (accessOrder)
afterNodeAccess(e);
return e.value;
}
(3)remove 方法:同 put 方法一样,只是重写了 removeNode 方法中的回调 afterNodeRemoval 方法:
void afterNodeRemoval(Node e) { // unlink
LinkedHashMap.Entry p =
(LinkedHashMap.Entry)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;
}
LRU(Least recently used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。
还记得 LinkedHashMap 中,accessOrder 的注释:
/**
* Map 的迭代顺序:
* true for access-order, false for insertion-order.
*/
final boolean accessOrder;
当accessOrder = true 的时候,LinkedHashMap 将会按照访问顺序排列,在看 afterNodeInsertion 中的这段代码
// 满足三个条件: 1允许删除 2.链表不为空 3.removeEldestEntry(first)返回 true
// 才会删除 链表头部的元素,也就是 删除最老的元素
if (evict && (first = head) != null && removeEldestEntry(first)) {
K key = first.key;
removeNode(hash(key), key, null, false, true);
}
所以,可以通过 设置 accessOrder = true 和 重写 removeEldestEntry 方法 实现LRU 算法。
package Linked;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
/**
* @author: Black
* @description: LruCache TODO
* @date: 2019/2/27 19:53
* @version: v 1.0.0
*/
public class LruCache extends LinkedHashMap {
private static final float DEFAULT_LOAD_FACTOR = 0.75f;
public static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
private int size;
public LruCache(int size) {
// 设置 accessOrder = true
super(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR, true);
this.size = size;
}
@Override
protected boolean removeEldestEntry(Map.Entry eldest) {
// 超过最大缓存空间 就 删除链表头部
return size() > size;
}
@Override
public String toString() {
StringBuilder str = new StringBuilder();
Set> entries = this.entrySet();
for (Map.Entry entry : entries) {
str.append(entry.getKey()).append(" ").append(entry.getValue()).append("||");
}
return str.toString();
}
public static void main(String[] args) {
LruCache lruCache = new LruCache<>(4);
lruCache.put(1,"1");
lruCache.put(2,"2");
lruCache.put(3,"3");
lruCache.get(1);
lruCache.put(4,"4");
lruCache.put(5,"5");
lruCache.put(6,"6");
System.out.println(lruCache);
}
}
输出:
1 1||4 4||5 5||6 6||
万一 让你来一个手写 LRU …那就 来吧~
这部分就是模仿 LinkedHashMap 的双向链表来实现的。
直接上代码,看注释(懒…)
public class MyLruCache {
/**
* 头结点 和 尾节点
*/
private Node head;
private Node tail;
/**
* 缓存 容量
*/
private int capacity;
/**
* 当前缓存大小
*/
private int size;
public MyLruCache(int capacity) {
this.capacity = capacity;
}
public T add(T entity) {
Node newNode = new Node(entity, null, null);
linkedLast(newNode);
T old = null;
if (size > capacity) {
old = head.data;
unlink(head);
}
return old;
}
public T get(T entity) {
Node node = getNode(entity);
if (node != null) {
move2Last(node);
}
return node == null ? null : node.data;
}
private Node getNode(T entity) {
T data;
for (Node node = head; node != null; node = node.nxt) {
if (entity == null) {
if (node.data == null) {
return node;
}
} else {
if (entity == (data = node.data) || entity.equals(data)) {
return node;
}
}
}
return null;
}
private void move2Last(Node node) {
assert node != null;
Node pre = node.pre, nxt = node.nxt;
node.nxt = node.pre = null;
if (pre == null) {
head = nxt;
} else {
pre.nxt = nxt;
}
if (nxt == null) {
return;
} else {
nxt.pre = pre;
}
linkedLast(node);
}
private void linkedLast(Node node) {
assert node != null;
Node last = tail;
tail = node;
if (last == null) {
head = node;
} else {
last.nxt = node;
node.pre = last;
}
size++;
}
private void unlink(Node node) {
assert node != null;
Node b = node.pre, a = node.nxt;
node.pre = node.nxt = null;
if (b == null) {
head = a;
} else {
b.nxt = a;
}
if (a == null) {
tail = b;
} else {
a.pre = b;
}
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
for (Node node = head; node != null; node = node.nxt) {
sb.append("<<>> ").append(node.data);
}
return sb.toString();
}
class Node {
T data;
Node pre;
Node nxt;
public Node(T data, Node pre, Node nxt) {
this.data = data;
this.pre = pre;
this.nxt = nxt;
}
}
public static void main(String[] args) {
MyLruCache mLru = new MyLruCache<>(7);
mLru.add(1);
mLru.add(2);
mLru.add(3);
mLru.add(4);
mLru.add(5);
mLru.add(6);
mLru.add(7);
mLru.add(8);
mLru.get(3);
mLru.get(2);
mLru.get(6);
System.out.println(mLru);
}
}
测试结果:
<<>> 4<<>> 5<<>> 7<<>> 8<<>> 3<<>> 2<<>> 6
没毛病…
如何回答:你知道 LinkedHashMap 吗
// TODO 巴啦巴拉~~~ 明天继续写
5.