面试结束复盘查漏补缺
每次面试都是检验自己知识与技术实力的一次机会,面试结束后建议大家及时总结复盘,查漏补缺,然后有针对性地进行学习,既能提高下一场面试的成功概率,还能增加自己的技术知识栈储备,可谓是一举两得。
以下最新总结的阿里P6资深Java必考题范围和答案,包含最全MySQL、Redis、Java并发编程等等面试题和答案,用于参考~
重要的事说三遍,关注+关注+关注!
更多笔记分享
- 继承了HashMap。其实LinkedHashMap可以看成是 LinkedList + HashMap。可以看考 【JDK源码】HashMap(一)
继承体系
- LinkedHashMap继承HashMap,拥有
HashMap的所有特性
,并且额外增加了按一定顺序访问的特性。
LinkedHashMap是怎样保证顺序的呢?
- 通过上面的继承体系,我们知道它继承了HashMap,所以它的内部也有(数组+链表+红黑树)这三种结构,但是它还额外添加了一种“双向链表”的结构存储所有元素的顺序。
添加删除元素的时候需要同时维护在HashMap中的存储,也要维护在LinkedList中的存储,所以性能上
来说会比HashMap稍慢。
基本属性
/**
- 双向链表头节点
*/
transient LinkedHashMap.Entry<K,V> head;
/**
- 双向链表尾节点
*/
transient LinkedHashMap.Entry<K,V> tail;
/**
-
是否按访问顺序排序
-
true : 按照访问顺序排序
-
false: 按照插入顺序排序
*/
final boolean accessOrder;
(1)head
双向链表的头节点,旧数据存在头节点。
(2)tail
双向链表的尾节点,新数据存在尾节点。
(3)accessOrder
是否需要按访问顺序排序,通过注释发现该变量为true时access-order,即按访问顺序遍历,此时你任何一次的操作,包括put、get操作,都会改变map中已有的存储顺序,如果为false,则表示按插入顺序遍历。默认为false 也就是按照插入顺序
构造方法
/**
- 构造方法1,构造一个指定初始容量和负载因子的、按照插入顺序的LinkedList
*/
public LinkedHashMap(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor);
accessOrder = false;
}
/**
- 构造方法2,构造一个指定初始容量的LinkedHashMap,取得键值对的顺序是插入顺序
*/
public LinkedHashMap(int initialCapacity) {
super(initialCapacity);
accessOrder = false;
}
/**
- 构造方法3,用默认的初始化容量和负载因子创建一个LinkedHashMap,取得键值对的顺序是插入顺序
*/
public LinkedHashMap() {
super(); //其实就是调用的hashmap的默认构造方法,默认加载因子0.75
accessOrder = false;
}
/**
- 构造方法4,通过传入的map创建一个LinkedHashMap,容量为默认容量(16)和(map.zise()/DEFAULT_LOAD_FACTORY)+1的较大者,加
*载因子为默认值
*/
public LinkedHashMap(Map<? extends K, ? extends V> m) {
super(m);
accessOrder = false;
}
/**
- 构造方法5,根据指定容量、加载因子和键值对保持顺序创建一个LinkedHashMap
*/
public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
-
前四个构造方法accessOrder都等于false,说明双向链表是按插入顺序存储元素。
-
最后一个构造方法accessOrder从构造方法参数传入,如果传入true,则就实现了
按访问顺序存储元素
,这也是实现LRU缓存策略的关键。
内部类
// 位于LinkedHashMap中
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);
}
}
// 位于HashMap中
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
…
}
存储节点,继承自HashMap的Node类,next用于单链表存储于桶中,before和after用于双向链表存储所有元素。
newNode()
LinkedHashMap重写了newNode()方法,通过此方法保证了插入的顺序性,在此之前我们先看一下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) {
LinkedHashMap.Entry<K,V> p =
new LinkedHashMap.Entry<K,V>(hash, key, value, e);//其实还是用的hashmap中的方法
linkNodeLast§;
return p;
}
这里调用了一个方法 linkNodeLast()
,我们看一下这个方法,但是这和方法不止完成了串联后置,也完成了串联前置,所以插入的顺序性
是通过这个方法保证的。
// link at the end of list 链接当前结点和尾结点
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
//保存尾结点引用
LinkedHashMap.Entry<K,V> last = tail;
tail = p;
//如果这条链表为空。则直接设置为头结点
if (last == null)
head = p;
else {
//将p结点与尾结点相连通
p.before = last;
last.after = p;
}
}
-
插入的顺序的保证是通过newNode()方法再调用linkNodeLast()
-
将新插入的结点连接到尾结点
afterNodeAccess()
//也就是把p结点从中间拿出来放到尾部
void afterNodeAccess(Node<K,V> e) { // move node to last 将节点移动到最后一个
LinkedHashMap.Entry<K,V> last;
// accessOrder 确定是按照访问顺序的,如果当前节点不是最后节点,因为是的话就不用移了
if (accessOrder && (last = tail) != e) {
//强转一下e给到p。然后把e的前后结点都取出来
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
//因为要插到最后所以先给null
p.after = null;
//如果p结点前结点为空,将p的后结点变头结点
if (b == null)
head = a;
else
//将p的前结点指向p的后结点 b>p>a b>a
b.after = a;
//将a也指向b a<>b
if (a != null)
a.before = b;
else
//a等于null,就将b作为尾结点
last = b;
if (last == null)
head = p;
else {
// last<>p
p.before = last;
last.after = p;
}
//尾结点p
tail = p;
++modCount;
}
}
-
在HashMap中没给具体实现,而在LinkedHashMap重写了,目的是保证操作过的Node节点永远在最后,从而保证读取的顺序性,在调用put方法和get方法时都会用到
-
从双向链表中移除访问的节点,把访问的节点加到双向链表的末尾;(末尾为最新访问的元素)
newNode()方法中调用的 linkNodeLast(Entry e)方法和现在的afterNodeAccess(Node e)都是将
传入的Node节点放到最后
,那么它们的使用场景如何呢?
HashMap的put流程中,如果在对应的hash位置上还没有元素,那么直接new Node()放到数组table中,这个时候对应到LinkedHashMap
中,调用了newNode()方法,就会用到linkNodeLast(),将新node放到最后,
如果对应的hash位置上有元素,进行元素值的覆盖时,就会调用afterNodeAccess()
,将原本可能不是最后的node节点移动到了最后删减之后的put代码逻辑
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
if ((p = tab[i = (n - 1) & hash]) == null)
// 调用linkNodeLast方法将新元素放到最后
tab[i] = newNode(hash, key, value, null);
else {
if (e != null) {
// 如果key已经存在
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
// 将存在的节点放到最后(因为存在所以是Access,也就是访问存在的元素)
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
get代码逻辑
public V get(Object key) {
Node<K,V> e;
if ((e = getNode(hash(key), key)) == null)
return null;
if (accessOrder)
afterNodeAccess(e); // 如果是采用访问顺序遍历就会将get的key放到尾部
return e.value;
}
下面给出一个例子
//这里是按照访问顺序排序的
final HashMap<Integer, Integer> m = new LinkedHashMap<Integer, Integer>(16, 0.75f,true);
m.put(3,1);
m.put(4,1);
m.put(1,1);
m.put(2,1);
m.put(8,1);
m.get(3);//也会调用到afterNodeAccess
m.put(2,1); //重新put就会调用afterNodeAccess()将key=2的结点放到尾部
for (Map.Entry<Integer, Integer> key : m.entrySet()) {
System.out.print(key.getKey()+“\t”);
}
//4 1 8 3 2
afterNodeInsertion()
最后
面试前一定少不了刷题,为了方便大家复习,我分享一波个人整理的面试大全宝典
- Java核心知识整理
Java核心知识
- Spring全家桶(实战系列)
- 其他电子书资料
Step3:刷题
既然是要面试,那么就少不了刷题,实际上春节回家后,哪儿也去不了,我自己是刷了不少面试题的,所以在面试过程中才能够做到心中有数,基本上会清楚面试过程中会问到哪些知识点,高频题又有哪些,所以刷题是面试前期准备过程中非常重要的一点。
以下是我私藏的面试题库:
全宝典**
- Java核心知识整理
[外链图片转存中…(img-O4XOoVz9-1715654823219)]
Java核心知识
- Spring全家桶(实战系列)
[外链图片转存中…(img-1IV2CpOF-1715654823220)]
- 其他电子书资料
[外链图片转存中…(img-Pqkx8o6M-1715654823220)]
Step3:刷题
既然是要面试,那么就少不了刷题,实际上春节回家后,哪儿也去不了,我自己是刷了不少面试题的,所以在面试过程中才能够做到心中有数,基本上会清楚面试过程中会问到哪些知识点,高频题又有哪些,所以刷题是面试前期准备过程中非常重要的一点。
以下是我私藏的面试题库:
[外链图片转存中…(img-FmGcWYmd-1715654823220)]