LinkedHashMap是HashMap的子类,在拥有HashMap功能之外可以保存元素插入顺序,使得元素遍历顺序与元素插入顺序相同。同时LinkedHashMap还可以根据元素访问的时间先后顺序来遍历元素,Lru(Least recently used)算法中就是应用了LinkdHashMap这一性质。那么这些功能具体是如何实现的,只能通过解析源码来揭秘了。
LinkedHashMap构造函数
构造函数除了正常调用父类HashMap的构造函数之外,还初始化了accessOrder为false.accessOrder为false时,表明遍历元素是按照插入顺序来访问,即遍历输出顺序和当初元素插入顺序相同;accessOrder为true时,表明遍历元素顺序是根据访问元素时间先后顺序来遍历元素,即最近访问的元素最后输出。具体会在下文解释。
public LinkedHashMap(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor);
accessOrder = false;
}
在HashMap构造函数有个init函数没有实现,在LinkedHashMap中实现了。可以看出这个函数new了一个LinkedHashMapEntry类型的header节点,LinkedHashMapEntry继承了HashMapEntry,增加了before和after指针。这个before和after和next有什么区别?从第二行代码只能看出是建立了一个环形双向链表。要想知道有什么区别,需要继续挖掘。
@Override
void init() {
header = new LinkedHashMapEntry<>(-1, null, null, null);
header.before = header.after = header;
}
LinkedHashMap的put和扩容
LinkedHashMap没有实现put函数,还是调用HashMap的put函数。
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);
int hash = sun.misc.Hashing.singleWordWangJenkinsHash(key);
int i = indexFor(hash, table.length);
for (HashMapEntry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
其中recordAccess是HashMapEntry的成员函数,在HashMap中是个空函数,具体实现在LInkedHashMap中。可以猜想这个函数与LInkedHashMap的顺序存储有关。
void recordAccess(HashMap<K,V> m) {
LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
//首先判断是否为当前是否为访问顺序模式。
if (lm.accessOrder) {
lm.modCount++;
remove();
addBefore(lm.header);
}
}
// 这里其实是删除双链表单节点的操作,
private void remove() {
//当前节点的前一个节点的after指针指向当前节点的后一个节点。
before.after = after;
// before指针指向当前节点的前一个节点。
after.before = before;
}
// 将当前节点插入到环形链表的头节点的前一个节点,其实也就是双链表的尾部
private void addBefore(LinkedHashMapEntry<K,V> existingEntry) {
after = existingEntry;
before = existingEntry.before;
before.after = this;
after.before = this;
}
因此在put操作时,如果该key的元素存在,recordAccess会通过删除节点并将该节点重新插入到双链表的尾部。这样就可以解释第一节中的疑问,在遍历时最近访问的元素会最后输出,就是因为每次put操作会把节点重新放置到双链表尾部。
LinkedHashMap重写了addEntry,但是removeEldestEntry默认返回false。其实和HashMap的addEntry一样。同时重写了createEntry().相比于HashMap,不同在于多调用了addBefore函数。在上面已经分析了,是将该节点移动到双链表的尾部。也就是LinkedHashMap除了要维护一个数组加链表,还要维护一个双链表。
void addEntry(int hash, K key, V value, int bucketIndex) {
LinkedHashMapEntry<K,V> eldest = header.after;
if (eldest != header) {
boolean removeEldest;
size++;
try {
removeEldest = removeEldestEntry(eldest);
} finally {
size--;
}
if (removeEldest) {
removeEntryForKey(eldest.key);
}
}
super.addEntry(hash, key, value, bucketIndex);
}
void createEntry(int hash, K key, V value, int bucketIndex) {
HashMapEntry<K,V> old = table[bucketIndex];
LinkedHashMapEntry<K,V> e = new LinkedHashMapEntry<>(hash, key, value, old);
table[bucketIndex] = e;
e.addBefore(header);
size++;
}
LinkedHashMap的扩容也是不同的,重写了resize中的transfer函数。HashMap中是通过遍历每个链表来将旧元素拷贝到新的数组中,而LinkedHashMap由于有双向链表来维护所有元素,因此直接遍历双向链表来拷贝旧元素。从遍历的效率来说,遍历双向链表的效率要高于遍历table,因为遍历双向链表是N次(N为元素个数);而遍历table是N+table的空余个数(N为元素个数)。
void transfer(HashMapEntry[] newTable) {
int newCapacity = newTable.length;
for (LinkedHashMapEntry<K,V> e = header.after; e != header; e = e.after) {
int index = indexFor(e.hash, newCapacity);
e.next = newTable[index];
newTable[index] = e;
}
}