-
前言
前文中已经解析了LinkedHashMap的插入操作,LinkedHashMap源码解析一,接下来我们接着看它剩余的操作。
-
取数据操作
首先我们看linkedHashMap的get的2个方法
public V get(Object key) {
Node<K,V> e;
//第一步是直接使用Hashmap中的函数getNode方法,获取value,如果为null,返回null
if ((e = getNode(hash(key), key)) == null)
return null;
//如果构造函数accessOrder为true,那么就把方法的这个函数放置到双向链表的末尾。
//这就是最新使用的元素。
if (accessOrder)
afterNodeAccess(e);
return e.value;
}
//与get相比多了一个返回默认值的功能
public V getOrDefault(Object key, V defaultValue) {
Node<K,V> e;
if ((e = getNode(hash(key), key)) == null)
return defaultValue;
if (accessOrder)
afterNodeAccess(e);
return e.value;
}
接下来看HashMap的getNode的方法。
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
//如果此key对应的hash桶不为空执行以下
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
//如果需要查询的是此哈希桶的第一个元素,直接返回
return first;
if ((e = first.next) != null) {
//如果是红黑树的结果,就需要按照红黑树的方式查找
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
//遍历整个链表,找到key对应的value并且返回
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
接下来我们看看afterNodeAccess这个函数,来了解linkedHashMap的操作方式
//首先要明确这个函数的目的是将访问的这个元素移动到双向链表的尾端,并且要将
//他的后面的元素向前移动
void afterNodeAccess(Node<K,V> e) {
//last为原链表的尾节点
LinkedHashMapEntry<K,V> last;
//如果accessOrder为true,并且尾端元素不是需要访问的元素
if (accessOrder && (last = tail) != e) {
//将节点e强制转换成linkedHashMapEntry,b为这个节点的前一个节点,a为它的后一个节点
LinkedHashMapEntry<K,V> p =
(LinkedHashMapEntry<K,V>)e, b = p.before, a = p.after;
//p为最后一个元素,那么他的后置节点必定是空
p.after = null;
//b为e的前置元素,如果b为空,说明此元素必定是链表的第一个元素,更新之后
//链表的头结点已经变成尾节点,那么原链表的第二个节点就要变为头结点
if (b == null)
head = a;
else
//如果b不是空,那么b的后置节点就由p变为p的后置节点
b.after = a;
//如果p的后置节点不为空,那么更新后置节点a的前置节点为b
if (a != null)
a.before = b;
else
//如果p的后置节点为空,那么p就是尾节点,那么更新last的节点为p的前置节点
last = b;
//如果原来的尾节点为空,那么原链表就只有一个元素
if (last == null)
head = p;
else {
//更新当前节点p的前置节点为 原尾节点last, last的后置节点是p
p.before = last;
last.after = p;
}
//p为最新的尾节点
tail = p;
//修改modCount
++modCount;
}
}
在这里需要注意两点,一是调用次函数之后,访问的这个元素会移动到双向链表的尾端,二是在accessOrder=true
的模式下,迭代LinkedHashMap
时,如果同时查询访问数据,也会导致fail-fast
,因为迭代的顺序已经改变。
-
删除操作
//如果key对应的value存在,则删除这个键值对。 并返回value。如果不存在 返回null。
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
之后是removeNode函数,所有的删除操作最后都需要执行到此函数
//删除指定元素,value是指需要删除的key对应的value,matchValue为true表示必须
//key,value均相等才可以删除,如果movable为false则删除之后不移动其他节点
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
//tab指向hashmap的数组,p为待删元素所在哈希桶的首个元素
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;
//如果第一个节点就是需要删除的节点。则将首节点赋值给node
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
else if ((e = p.next) != null) {
//遍历整个链表找到需要删除的节点。赋值给node
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);
}
}
//如果找到了对应的node且不为空,match为false或者value也一致则进行删除
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)
//如果node == p,说明是链表头是待删除节点
tab[index] = node.next;
else
//如果是中间节点,进行删除并修改前后顺序
p.next = node.next;
//修改modCount
++modCount;
//size减一
--size;
//在hashmap中次函数为空,LinkedHashMap重写此方法
afterNodeRemoval(node);
return node;
}
}
return null;
}
以上就是hashmap的删除操作,在linkedHashMap中还有afterNodeRemoval函数来删除双向链表中指定的元素。
void afterNodeRemoval(Node<K,V> e) { // unlink
LinkedHashMapEntry<K,V> p =
(LinkedHashMapEntry<K,V>)e, b = p.before, a = p.after;
//待删除节点 p 的前置后置节点都置空
p.before = p.after = null;
//如果b为空,p就是头节点,删除之后a就是头节点,否则将前置节点b的后置节点指向a
if (b == null)
head = a;
else
b.after = a;
//如果a为空,那么p就是尾节点,删除之后,b就是新的尾节点,否则a的前置节点就改为b
if (a == null)
tail = b;
else
a.before = b;
}
-
遍历操作
一般通过keyset获取key的set集合。
public Set<K> keySet() {
Set<K> ks = keySet;
if (ks == null) {
//在这里linkedHashMap重写了此函数。
ks = new LinkedKeySet();
keySet = ks;
}
return ks;
}
final class LinkedKeySet extends AbstractSet<K> {
//链表元素个数
public final int size() { return size; }
public final void clear() { LinkedHashMap.this.clear(); }
//遍历的目的是获取Iterator
public final Iterator<K> iterator() {
return new LinkedKeyIterator();
}
//这个直接使用的hashmap的containsKey函数,最终使用的是getNode,前文已经讲过就不在强调
public final boolean contains(Object o) { return containsKey(o); }
//remove最后也是调用了removeNode,前文已经讲过。就不在强调
public final boolean remove(Object key) {
return removeNode(hash(key), key, null, false, true) != null;
}
}
我们再看看map中value的集合
//此函数目的是获取value的集合
public Collection<V> values() {
Collection<V> vs = values;
if (vs == null) {
vs = new LinkedValues();
values = vs;
}
return vs;
}
final class LinkedValues extends AbstractCollection<V> {
public final int size() { return size; }
public final void clear() { LinkedHashMap.this.clear(); }
//最重要的一点是获取迭代器
public final Iterator<V> iterator() {
return new LinkedValueIterator();
}
//linkedHashMap重写了contains函数,
public final boolean contains(Object o) { return containsValue(o); }
public final Spliterator<V> spliterator() {
return Spliterators.spliterator(this, Spliterator.SIZED |
Spliterator.ORDERED);
}
}
//判断是否含有此函数就是遍历一下双向链表
public boolean containsValue(Object value) {
for (LinkedHashMapEntry<K,V> e = head; e != null; e = e.after) {
V v = e.value;
if (v == value || (value != null && value.equals(v)))
return true;
}
return false;
}
看完这个之后,我们再看看entry的遍历
public Set<Map.Entry<K,V>> entrySet() {
Set<Map.Entry<K,V>> es;
return (es = entrySet) == null ? (entrySet = new LinkedEntrySet()) : es;
}
final class LinkedEntrySet extends AbstractSet<Map.Entry<K,V>> {
public final int size() { return size; }
public final void clear() { LinkedHashMap.this.clear(); }
//之后的遍历都是通过Iterator来进行
public final Iterator<Map.Entry<K,V>> iterator() {
return new LinkedEntryIterator();
}
public final boolean contains(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry<?,?> e = (Map.Entry<?,?>) o;
Object key = e.getKey();
//通过getnode方法来获取节点
Node<K,V> candidate = getNode(hash(key), key);
return candidate != null && candidate.equals(e);
}
}
通过以上的内容我们可以明白。无论是通过哪种方式遍历,核心都在迭代器。无论哪个迭代器,最终都是调用到了LinkedEntryIterator
abstract class LinkedHashIterator {
//下一个节点
LinkedHashMapEntry<K,V> next;
//当前节点
LinkedHashMapEntry<K,V> current;
int expectedModCount;
LinkedHashIterator() {
//初始化的时候将next指向双向链表的头结点
next = head;
//记录当前的modCount,与fail-fast有关
expectedModCount = modCount;
//current为空
current = null;
}
public final boolean hasNext() {
return next != null;
}
//nextNode方法就是我们用到的next方法,
//迭代LinkedHashMap,就是从内部维护的双链表的表头开始循环输出
final LinkedHashMapEntry<K,V> nextNode() {
LinkedHashMapEntry<K,V> e = next;
//如果查询的时候,有其他对这个map的操作,导致改变了modCount,就造成fail-fast
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
//如果返回的是空,则抛出异常
if (e == null)
throw new NoSuchElementException();
current = e;
next = e.after;
return e;
}
//删除方法,就直接使用了hashmap的remove,前文已经介绍过。
public final void remove() {
Node<K,V> p = current;
if (p == null)
throw new IllegalStateException();
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
current = null;
K key = p.key;
removeNode(hash(key), key, null, false, false);
expectedModCount = modCount;
}
}
-
总结
1 linkedHashMap与hashmap的结构是一样的,都是数组+链表+红黑树,他们的扩容机制也是一样的。
2 linkedhashmap是通过双向链表来维护数据的。与hashmap的拉链式存储不一样。
3 linkedhashmap存储顺序与添加顺序是一样得。同事可以根据accessOrder的值来决定是否移动元素达到lru的目的。
最后针对一些网上的面试题做一些回答: