LinkedHashMap 在 Map 集合框架的位置
LinkedHashMap簡介
LinkedHashMap是Map 接口的 哈希表(hash table) 和 鏈表(linked list) 的實現,具有可預測的迭代順序。此實現與 HashMap 的不同之處在於,它維護一個雙鏈接列表,該列表貫穿其所有條目。這個鏈表定義了迭代的順序,通常是鍵值插入到 map 中的插入順序(insertion-order). 請注意,如果鍵重新插入到 map 中,則插入順序不受影響。
在遍歷LinkedHashMap時,不會像遍歷 HashMap 和 Hashtable 所得到的遍歷順序是不確定的。而且LinkedHashMap在遍歷有順序保證的同時,不會像TreeMap那樣招致額外的開銷。它可以用來拷貝已有的map, 不用考慮這個被拷貝的map對象原來的實現方式。拷貝出來的map對象和原始的map對象的順序是一致的。
void foo(Map m) {
Map copy = new LinkedHashMap(m);
...
}
寫一個測試方法跑一下看下結果:
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
public class LinkedHashMapTest {
public static void main(String args[]) {
Map<Integer, Integer> map = new HashMap();
for(int i=1; i<5; i++) {
map.put(i, i*i);
}
for(int i=1; i>-5; i--) {
map.put(i, i*i);
}
System.out.println(map);
System.out.println(map);
System.out.println(map);
System.out.println("-----------------copy from map -----------------------");
LinkedHashMap<Integer,Integer> copy = new LinkedHashMap<Integer, Integer>(map);
System.out.println(copy);
Map<Integer, Integer> linkedHashMap = new LinkedHashMap();
for(int i=1; i<5; i++) {
linkedHashMap.put(i, i*i);
}
for(int i=1; i>-5; i--) {
linkedHashMap.put(i, i*i);
}
System.out.println("-----------------------------------------");
System.out.println(linkedHashMap);
System.out.println("---------- put exist key again ---------------------");
for(int i=1; i<5; i++) {
linkedHashMap.put(i, i*i);
}
System.out.println(linkedHashMap);
System.out.println("--- remove exist key and put again, keep insert order ----");
linkedHashMap.remove(3);
linkedHashMap.put(3, 9);
System.out.println(linkedHashMap);
}
}
輸出內容:
{0=0, -1=1, 1=1, -2=4, 2=4, -3=9, 3=9, -4=16, 4=16}
{0=0, -1=1, 1=1, -2=4, 2=4, -3=9, 3=9, -4=16, 4=16}
{0=0, -1=1, 1=1, -2=4, 2=4, -3=9, 3=9, -4=16, 4=16}
-----------------copy from map -----------------------
{0=0, -1=1, 1=1, -2=4, 2=4, -3=9, 3=9, -4=16, 4=16}
-----------------------------------------
{1=1, 2=4, 3=9, 4=16, 0=0, -1=1, -2=4, -3=9, -4=16}
---------- put exist key again ---------------------
{1=1, 2=4, 3=9, 4=16, 0=0, -1=1, -2=4, -3=9, -4=16}
--- remove exist key and put again, keep insert order ----
{1=1, 2=4, 4=16, 0=0, -1=1, -2=4, -3=9, -4=16, 3=9}
如果模塊在輸入上獲取映射,將其複製並隨後返回結果(其順序由副本的順序確定),則此技術特別有用。(客戶通常喜歡按插入的順序返回。)
構造函數的 accessOrder 參數
LinkedHashMap 有一個特殊的構造函數:
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
這個構造函數的參數除了有初始化容量 initialCapacity 和加載因子 loadFactor 之外,還有一個布爾參數:accessOrder
accessOrder指定了維護順序的模式。
- accessOrder = false. 按照插入順序進行排序。
- accessOrder = true, 按照訪問順序排序。也就是根據上一次訪問截止時。按照最近最少到最近最多的訪問順序排列。
寫一個方法測試一下:
import java.util.LinkedHashMap;
import java.util.Map;
public class LinkedHashMapTest {
public static void main(String args[]) {
Map<Integer, Integer> map = new LinkedHashMap<Integer, Integer>(10, 0.75f, true);
for(int i=0; i<10; i++) {
map.put(i, i*i);
}
System.out.println("------------origin order---------");
System.out.println(map);
System.out.println(map.values());
for(int i=1; i<10; i++) {
map.get(3);
}
for(int i=1; i<30; i++) {
map.get(6);
}
for(int i=1; i<10; i++) {
map.get(4);
}
for(int i=1; i<9; i++) {
map.get(2);
}
System.out.println("------------ after access ---------");
System.out.println(map);
System.out.println(map.values());
}
}
輸出內容:
------------origin order---------
{0=0, 1=1, 2=4, 3=9, 4=16, 5=25, 6=36, 7=49, 8=64, 9=81}
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
------------ after access ---------
{0=0, 1=1, 5=25, 7=49, 8=64, 9=81, 3=9, 6=36, 4=16, 2=4}
[0, 1, 25, 49, 64, 81, 9, 36, 16, 4]
它的移動算法如下:
void afterNodeAccess(Node<K,V> e) { // move node to last
LinkedHashMap.Entry<K,V> last;
if (accessOrder && (last = tail) != e) {
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)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;
}
}
重寫 removeEldestEntry(Map.Entry<K,V> eldest) 方法
如果此 Map 應刪除其老的條目,則返回 true。 此方法在將新條目插入映射後,通過 put 和 putAll 調用。 它爲實現者提供了每次添加新條目時刪除最老條目的機會。 如果映射表示緩存,這非常有用:它允許Map通過刪除陳舊條目來減少內存消耗。
寫一個代碼測試一下:
import java.util.LinkedHashMap;
import java.util.Map;
public class CacheMap<K, V> extends LinkedHashMap<K, V> {
public static final int MAX_SIZE = 10;
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> entry) {
return size() > MAX_SIZE;
}
public CacheMap(int i, float v, boolean b) {
super(i, v, b);
}
public static void main(String[] args) {
CacheMap<Integer, Integer> cacheMap = new CacheMap<Integer, Integer>(40, 0.75f, true);
for (int i=0; i<40; i++) {
cacheMap.put(i, i*i);
}
for (int i=1; i<20; i++) {
cacheMap.get(3);
}
System.out.println(cacheMap);
System.out.println("cacheMap.size() = "+cacheMap.size());
cacheMap.put(20, 400);
System.out.println("-----------after insert new element---------------");
System.out.println(cacheMap);
System.out.println("cacheMap.size() = "+cacheMap.size());
}
}
輸出內容:
{30=900, 31=961, 32=1024, 33=1089, 34=1156, 35=1225, 36=1296, 37=1369, 38=1444, 39=1521}
cacheMap.size() = 10
-----------after insert new element---------------
{31=961, 32=1024, 33=1089, 34=1156, 35=1225, 36=1296, 37=1369, 38=1444, 39=1521, 20=400}
cacheMap.size() = 10
看到沒有,在CacheMap裏面,我設置最大容量是10.通過重寫removeEldestEntry方法,在裏面判斷size()和MAX_SIZE的大小決定要不要移除最老的元素。以達到插入新元素時移除老元素的目的。
此方法通常不會以任何方式修改map,而是允許map按照其返回值的指示進行自身修改。此方法允許直接修改map,但如果這樣做,則必須返回 false(指示map不應嘗試任何進一步修改)。未指定在此方法內修改映map返回 true 的效果。
在LinkedHashMap中,此實現僅返回 false(因此此映射的行爲類似於普通地圖 - 最長的元素永遠不會被刪除)。
LinkedHashMap 的數據結構
/**
* HashMap.Node subclass for normal LinkedHashMap entries.
*/
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.Node ----------------------------------------
//-------------------------------------------------------------------------------------------------
/**
* Basic hash bin node, used for most entries. (See below for
* TreeNode subclass, and in LinkedHashMap for its Entry subclass.)
*/
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;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
可以看到,Entry 和Map.Node相比,多了兩個屬性:before 和 after. 用於維護雙向隊列。
如何在隊尾插入元素
- 先把 tail 備份爲 last
- 把tail賦值爲新entry p
- 如果last爲null.表示空map.直接把新 entry p 賦值給 head
- 如果last不爲null,表示非空map. 把p的前驅節點賦值爲之前的last.把之前的last的後去賦值爲新 entry p.
// 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.before = last;
last.after = p;
}
```
### 如何替換Entry
```java
// apply src's links to dst
private void transferLinks(LinkedHashMap.Entry<K,V> src,
LinkedHashMap.Entry<K,V> dst) {
LinkedHashMap.Entry<K,V> b = dst.before = src.before;
LinkedHashMap.Entry<K,V> a = dst.after = src.after;
if (b == null)
head = dst;
else
b.after = dst;
if (a == null)
tail = dst;
else
a.before = dst;
}