前言
LinkedHashMap看名字就知道是链表结构, LinkedHashMap继承了HashMap, 上篇文章已经了解HashMap的数据结构是数组+单链表, 那么LinkedHashMap的数据结构也是如此吗, 让我们通过源码揭开它的面纱.
源码分析
首先看到有两个属性:
/**
* The head of the doubly linked list.
*/
//双向链表的头部
private transient LinkedHashMapEntry header;
/**
* The iteration ordering method for this linked hash map: true
* for access-order, false for insertion-order.
*/
//数据元素的排序方法, true:按照访问顺序排序, false:按照插入顺序排序
private final boolean accessOrder;
- LinkedHashMapEntry
private static class LinkedHashMapEntry extends HashMapEntry {
// These fields comprise the doubly linked list used for iteration.
LinkedHashMapEntry before, after;
LinkedHashMapEntry(int hash, K key, V value, HashMapEntry next) {
super(hash, key, value, next);
}
}
上篇文章分析过HashMapEntry是一个单链表, LinkedHashMapEntry继承了HashmapEntry, 内部又维护了一个before和after, 从而形成了一个双链表.
- accessOrder
决定了数据元素的排序方法, 具体实现方法, 后面会介绍到.
构造方法
public LinkedHashMap(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor);
accessOrder = false;
}
public LinkedHashMap(int initialCapacity) {
super(initialCapacity);
accessOrder = false;
}
public LinkedHashMap() {
super();
accessOrder = false;
}
public LinkedHashMap(Map extends K, ? extends V> m) {
super(m);
accessOrder = false;
}
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
都是继承了父类HashMap的构造方法, 并初始化了accessOrder.
@Override
void init() {
header = new LinkedHashMapEntry<>(-1, null, null, null);
header.before = header.after = header;
}
LinkedHashMap重写了父类HashMap构造方法中的init方法, 初始化header.
header是一个key和value都为空, 只保留前后引用的特殊节点.
header.before = header.after = header初始化了一个空的环形链表.
来了解下LinkedHashMapEntry的结构:
private static class LinkedHashMapEntry extends HashMapEntry {
LinkedHashMapEntry before, after;//前后元素
LinkedHashMapEntry(int hash, K key, V value, HashMapEntry next) {
super(hash, key, value, next);
}
//修改前后元素的指向, 把该元素从双链表中删除
private void remove() {
before.after = after;
after.before = before;
}
//修改该元素及existingEntry的前后指向, 在existingEntry元素之前插入该元素
private void addBefore(LinkedHashMapEntry existingEntry) {
after = existingEntry;
before = existingEntry.before;
before.after = this;
after.before = this;
}
//记录操作
void recordAccess(HashMap m) {
LinkedHashMap lm = (LinkedHashMap)m;
if (lm.accessOrder) {
lm.modCount++;
remove();
addBefore(lm.header);
}
}
void recordRemoval(HashMap m) {
remove();
}
}
这里accessOrder就起作用了, 如果为true(按访问顺序排列), 就把当前位置的元素删除, 加到链表头部前面(实际位置不变, 只是修改双链表中前后元素指向)
重写父类的方法
后面的一些方法都是重写了父类HashMap中的方法, 让我们看下它都重写了哪些方法.
- get
public V get(Object key) {
LinkedHashMapEntry e = (LinkedHashMapEntry)getEntry(key);
if (e == null)
return null;
e.recordAccess(this);
return e.value;
}
很简单, 就是把父类HashMap中getEntry(key)返回的Entry强转成LinkedHashMapEntry, 最后返回entry的value.
注意, 这里每次获取key对应的值, 都有访问记录e.recordAccess(this), 这里会判断如果accessOrder为true, 就把该元素放到链表头部前面.
- createEntry
LinkedHashMap没有重写父类的put方法, 但是重写了父类中的addEntry和createEntry两个方法来.
void createEntry(int hash, K key, V value, int bucketIndex) {
HashMapEntry old = table[bucketIndex];
LinkedHashMapEntry e = new LinkedHashMapEntry<>(hash, key, value, old);
table[bucketIndex] = e;
e.addBefore(header);
size++;
}
父类HashMap中的createEntry
void createEntry(int hash, K key, V value, int bucketIndex) {
HashMapEntry e = table[bucketIndex];
table[bucketIndex] = new HashMapEntry<>(hash, key, value, e);
size++;
}
经过对比可以发现, LinkedHashMap与HashMap一样, 在数组对应位置上新建了一个Entry, next指向该位置原先的元素, 不同的是LinkedHashMap又把该新建的Entry放到了环形链表头部之前, 因此每次创建的新元素都是放到头部前面, 那头部后面的元素就成了最旧的元素.
网上发现一个很生动形象的图来表达LinkHashMap的数据结构及插入元素的顺序, 这里盗用一下:
- addEntry
void addEntry(int hash, K key, V value, int bucketIndex) {
//获取链表头部后面的元素, 及最旧的元素
LinkedHashMapEntry eldest = header.after;
if (eldest != header) {
boolean removeEldest;
size++;
try {
//判断是否移除最旧的元素
removeEldest = removeEldestEntry(eldest);
} finally {
size--;
}
//为true, 则调用父类HashMap中的removeEntryForKey移除该key对应的元素
if (removeEldest) {
removeEntryForKey(eldest.key);
}
}
//然后调用父类的addEntry方法增加元素
super.addEntry(hash, key, value, bucketIndex);
}
addEntry方法中LinkedHashMap在HashMap增加元素之前多加了一步是否删除最旧元素的判断.
看过hashMap的源码, 我们知道addEntry方法先判断是否需要扩容然后createEntry, createEntry中LinkedHashMap的改变已经看过, 下面看下扩容时LinkedHashMap做的改变.
- transfer
transfer是数组扩充到原来两倍时, 需要把老数组中所有元素移到新数组的操作.
@Override
void transfer(HashMapEntry[] newTable) {
int newCapacity = newTable.length;
for (LinkedHashMapEntry e = header.after; e != header; e = e.after) {
int index = indexFor(e.hash, newCapacity);
e.next = newTable[index];
newTable[index] = e;
}
}
对比HashMap的transfer方法.
void transfer(HashMapEntry[] newTable) {
int newCapacity = newTable.length;
for (HashMapEntry e : table) {
while(null != e) {
HashMapEntry next = e.next;
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
经对比不难发现, HashMap是对老数组中每一个位置的单链表上每一个元素重新计算新坐标,重新放位置, 而LinkedHashMap是对环形链表中的每一个元素重新计算新坐标, 重新放位置, 这样就避免了对每一个元素进行判断, 加快了数组扩容的速率.
- containsValue
public boolean containsValue(Object value) {
// Overridden to take advantage of faster iterator
if (value==null) {
for (LinkedHashMapEntry e = header.after; e != header; e = e.after)
if (e.value==null)
return true;
} else {
for (LinkedHashMapEntry e = header.after; e != header; e = e.after)
if (value.equals(e.value))
return true;
}
return false;
}
对比HashMap的containsValue:
public boolean containsValue(Object value) {
if (value == null)
return containsNullValue();
HashMapEntry[] tab = table;
for (int i = 0; i < tab.length ; i++)
for (HashMapEntry e = tab[i] ; e != null ; e = e.next)
if (value.equals(e.value))
return true;
return false;
}
private boolean containsNullValue() {
HashMapEntry[] tab = table;
for (int i = 0; i < tab.length ; i++)
for (HashMapEntry e = tab[i] ; e != null ; e = e.next)
if (e.value == null)
return true;
return false;
}
同样, LinkedHashMap是对遍历环形链表的元素, 判断是否包含该value的元素, 而HashMap是遍历数组中每个位置单链表上的每一个元素, 判断是否包含该value的元素.
- clear
public void clear() {
super.clear();
header.before = header.after = header;
}
很简单, 就是修改环形链表头部的前后指向.
- eldest
public Map.Entry eldest() {
Entry eldest = header.after;
return eldest != header ? eldest : null;
}
获取最旧的元素, 也就是环形链表头部后面的元素. 因为每次创建的新元素都是放到头部之前. 如果accessOrder为true, 查看和修改元素时, 也是把元素重新放到头部之前.
简单总结一下, LinkedHashMap就是在不改变HashMap数据存储结构的基础上又单独维护了一个环形链表. 每次创建元素的时候放到环形链表头部之前, 如果按照访问顺序排列的话, 每次查看或者修改元素也都重新放到头部之前, 这样很容易就能获取到最老的元素.