Java Collection系列之HashMap、ConcurrentHashMap、LinkedHashMap的使用及源碼分析

HashMap、ConcurrentHashMap、LinkedHashMap、TreeMap與Map的關係:

map.png

他們都實現了Map接口,都以key-value形式保存元素,同時各自又有不同的特點,下面會一一分析。

HashMap

HashMap在項目中使用很頻繁,底層實現是數組+鏈表(jdk1.8版本加入了紅黑樹)。我們知道數組的優點是查找快,增加或刪除元素慢;鏈表的優點是增加或刪除快,查找慢。HashMap結合了兩者的特點,增刪改查都有不錯的性能,下面就來了解一下HashMap的內部機制。

HashMap的存儲結構

hashmap.png

從圖中可以看出,HashMap是由數組+鏈表(jdk1.8在鏈表長度大於8時轉換成紅黑樹)組成。HashMap存儲的是鍵值對,Node<K,V>即是每個節點中存儲鍵值對的映射(Node<K,V>的內部結構後面分析)。

  • 當存儲元素時,Key值經過hash方法算出key對應table數組的索引位置,如果該位置沒有元素,則直接將該key、value生成Node並放入該索引位置;如果該位置有元素,利用頭結點方式放入該索引所在的鏈表中,當鏈表的長度超過8時,鏈表轉換成紅黑樹,這樣即使元素多也能保證高效性。
  • 當添加元素時發現HashMap的元素個數超過閾值後,會將數組大小翻倍並對所有元素重新進行hash計算並放入新的數組中。
  • 取元素時,依然是先通過key找到對應table數組中的索引,並最終找到該索引中對應的鏈表(或紅黑樹)中對應的值value。

以下源碼分析是基於jdk1.8版本的,跟1.7版本相比,1.8中有幾處優化,後面會列出。

初始化

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;//初始化大小,默認是16

static final float DEFAULT_LOAD_FACTOR = 0.75f;//負載因子默認值

static final int MAXIMUM_CAPACITY = 1 << 30;//最大容量

static final int TREEIFY_THRESHOLD = 8;//鏈表
static final int UNTREEIFY_THRESHOLD = 6;
static final int MIN_TREEIFY_CAPACITY = 64;

transient Node<K,V>[] table;//哈希桶數組
transient Set<Map.Entry<K,V>> entrySet;
transient int size;//key-value鍵值對的個數
transient int modCount;//HashMap內部結構發生變化次數
int threshold;//擴容閾值 threshold
final float loadFactor;//負載因子

//1
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; 
}

//2
public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

//3
public HashMap(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: "+loadFactor);
        
    this.loadFactor = loadFactor;
    //如果傳入的容量不是2的倍數 通過tableSizeFor方法返回一個最接近initialCapacity的值。
    this.threshold = tableSizeFor(initialCapacity);
}

//4
public HashMap(Map<? extends K, ? extends V> m) {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    putMapEntries(m, false);
}

static final int tableSizeFor(int cap) {
    int n = cap - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

Node是HashMap中的一個內部類,是一個保存key-value的映射。Node的內部結構:

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;
    }
}

Node中有4個成員變量:

  • hash:哈希值,用來確定Node在table數組中的索引位置
  • key: Key值
  • value: value值
  • next: HashMap使用哈希表(一維數組)存儲元素,當出現哈希衝突(即不同元素對應上table數組中的同一索引位置)時,HashMap採用鏈地址法解決衝突,可以簡單認爲table數組中的每個存儲都是一個鏈表結構,鏈表的節點是Node,Node.next指向的是鏈表中的下一個Node節點.

put & get

put元素

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);

static final int hash(Object key) {
    int h;
    //hashCode的高16位也參與運算 增加Node元素在哈希數組分佈的均勻性
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    //如果table數組未初始化 調用resize去初始化數組
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
     //當前key的hashCode與數組大小取模取得在table數組中的索引位置,如果當前位置沒有元素,說明沒有Hash衝突,直接將當前key、value生成Node並放入當前位置中   
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        //如果該索引位置有元素(Hash衝突)且key、key的hashCode相等,直接用新值覆蓋舊值並返回舊值
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        //當前節點屬於紅黑樹的節點,則按紅黑樹規則添加   
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            //當前節點屬於鏈表,將Key、value封裝成Node放入鏈表的尾部(1.7是放入鏈表的頭部)
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    if (binCount >= TREEIFY_THRESHOLD - 1) 
                        //鏈表長度超過閾值,轉化成紅黑樹
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}
}

畫一個大概的流程圖:
put.png

get元素

public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

//獲取key經過hash算法之後的值
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    //table數組不爲空且key在table數組中索引位置處不能爲空
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        //如果table數組索引位置(可能是紅黑樹或鏈表結構)的key及hash值相等,直接返回索引位置對應的value
        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 {
                //按鏈表方式遍歷並返回查找值
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}

get(key)對應的流程圖:

get.png

擴容

擴容指的是當向HashMap中添加元素時,Node元素總數超過了設定的閾值,那麼HashMap就會進行擴容(擴大到2倍),那麼這個設定的閾值跟哪些變量相關呢?上面HashMap源碼中有這幾個變量:

  • loadFactor:負載因子(默認是0.75)。哈希數組大小固定的前提下,負載因子越大那麼HashMap容納的Node元素越多,反之容納的越少。但不是說loadFactor越大越好,loadFactor越大,意味着哈希衝突的可能性越大,查找需要的時間也就更多;反之loadFactor越小,哈希衝突可能性減小,但是同時哈希數組的空間利用率也就越小。默認值0.75是對空間和時間的一個平衡選擇,一般不需要改動。
  • Node<K,V>[] table:哈希數組,
  • threshold:擴容閾值(即HashMap能容納的最大數量的Node值),當HashMap中的元素Node個數超過此閾值時,需要進行擴容(resize)。threshold = table.length * loadFactor

遍歷Map

HashMap<String, String> hashMap = new HashMap<>();
hashMap.put("111", "222");
hashMap.put("333", "444");
hashMap.put("555", "666");

//1、通過entrySet遍歷map
for (Map.Entry<String, String> entry : hashMap.entrySet()) {
    System.out.println("entrySet方式,key:" + entry.getKey() + ",value:" + entry.getValue());
}
//2、通過keySet遍歷map
for (String key : hashMap.keySet()) {
    System.out.println("keySet 方 式,key:" + key + ",value:" + hashMap.get(key));
}

執行結果:

entrySet方式,key:111,value:222
entrySet方式,key:333,value:444
entrySet方式,key:555,value:666

keySet 方 式,key:111,value:222
keySet 方 式,key:333,value:444
keySet 方 式,key:555,value:666

jdk1.8中的優化

相比於jdk1.7的HashMap,jdk1.8中優化了下面幾個方面:

  • hash算法:hash算法的好壞決定了Node元素在哈希表中的分佈均勻情況,分佈的越均勻,那麼生成鏈表的可能性越小,查找的效率越高(數組查找效率>鏈表),在1.7中通過取模運算(hash = h & (table.length -1))來使Node元素均勻分佈;在1.8中優化了hash算法,hashCode是通過hashCode的高16位異或低16位實現的((h = k.hashCode()) ^ (h >>> 16)),保證了高低位都參與到hashCode的計算中,增加了離散性,使得哈希數組中的Node元素分佈的更均勻。

  • 紅黑樹的引入:極端情況下,所有的鍵值對應的哈希值都是一樣的,那麼經過鏈地址法後最後在哈希數組的某個位置上形成了一個鏈表,此時不管是空間性能還是時間性能都是糟糕的,HashMap已經退化成一個鏈表。基於此在1.8中對鏈表做了優化,當鏈表中的Node元素個數大於8時,將鏈表優化成紅黑樹,利用紅黑樹可以快速增刪改查的特點提高HashMap的性能。

ConcurrentHashMap

Hashmap中沒有任何同步操作,HashMap在單線程下使用沒有問題,但是當在多線程中使用時,可能會導致數據不一致問題,因此在多線程下改用JUC中的ConcurrentHashMap。

注:除了ConcurrentHashMap,還可以使用Hashtable或Collections.synchronizedMap(hashmap),但是他們都是直接在最上層加鎖,不管是put還是get操作都需要進行同步加鎖,效率並不高。

jdk1.7版本

ConcurrentHashMap在1.7版本中使用了分段鎖形式來存取元素,如下圖所示:

1.7.png

ConcurrentHashMap在1.7版本的大致流程:

  • 當put元素時,首先通過key的hashcode獲取所在的Segment, 其中Segment的父類是ReentrantLock,當對其中一個Segment進行put或get同步操作時,其他的Segment是不受影響的。在定位到的Segment中根據key的hashCode找到HashEntry,然後進行遍歷,如果key值相等,用新值覆蓋舊值;如果不相等新建一個HashEntry放入鏈表中。
  • get元素時,將key經過Hash後定位到對應的Segment,再經過遍歷後取得key對應的value。

ConcurrentHashMap相對於Hashtable或Collections.synchronizedMap(hashmap)來說是高效的,尤其是get元素時,不需要加鎖,但是如果鏈表中的元素比較多時,遍歷效率還是比較低(跟HashMap類似),接着看在1.8版本中做了哪些結構變化。

jdk1.8版本

put元素

public V put(K key, V value) {
    return putVal(key, value, false);
}

/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException();
    //根據key獲取hashCode
    int hash = spread(key.hashCode());
    int binCount = 0;
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        //table數組爲空,先初始化數組
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            //key對應的索引位置沒有Node元素,直接通過CAS方式嘗試寫入,失敗則進行自旋至執行成功
            if (casTabAt(tab, i, null,
                         new Node<K,V>(hash, key, value, null)))
                break;                   // no lock when adding to empty bin
        }
        else if ((fh = f.hash) == MOVED)
            //擴容
            tab = helpTransfer(tab, f);
        else {
            V oldVal = null;
            //加鎖 遍歷鏈表 有相應key的話value直接覆蓋舊value,沒有的話添加到鏈表的尾部 
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    if (fh >= 0) {
                        binCount = 1;
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;
                            if (e.hash == hash &&
                                ((ek = e.key) == key ||
                                 (ek != null && key.equals(ek)))) {
                                oldVal = e.val;
                                if (!onlyIfAbsent)
                                    e.val = value;
                                break;
                            }
                            Node<K,V> pred = e;
                            if ((e = e.next) == null) {
                                pred.next = new Node<K,V>(hash, key,
                                                          value, null);
                                break;
                            }
                        }
                    }
                    //如果是紅黑樹 按紅黑樹方式添加
                    else if (f instanceof TreeBin) {
                        Node<K,V> p;
                        binCount = 2;
                        if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                       value)) != null) {
                            oldVal = p.val;
                            if (!onlyIfAbsent)
                                p.val = value;
                        }
                    }
                }
            }
            if (binCount != 0) {
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    addCount(1L, binCount);
    return null;
}

static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
                                    Node<K,V> c, Node<K,V> v) {
    return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
}

static final int spread(int h) {
    return (h ^ (h >>> 16)) & HASH_BITS;
}

get元素

public V get(Object key) {
    Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
    //根據key計算出對應的hashCode
    int h = spread(key.hashCode());
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (e = tabAt(tab, (n - 1) & h)) != null) {
        if ((eh = e.hash) == h) {
            //如果key在table數組中,直接取得對應的value並返回
            if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                return e.val;
        }
        else if (eh < 0)
            return (p = e.find(h, key)) != null ? p.val : null;
        //遍歷鏈表 獲取key對應的value    
        while ((e = e.next) != null) {
            if (e.hash == h &&
                ((ek = e.key) == key || (ek != null && key.equals(ek))))
                return e.val;
        }
    }
    return null;
}

1.8中的變化

  • 1.7中同步策略採用的是Segment分段鎖(內部實現是ReentrantLock),1.8中採用的是CAS+synchronized,效率更高;
  • 1.8中引入了紅黑樹(跟HashMap一樣),當鏈表過長時直接將鏈表轉換成紅黑樹,查找效率從O(n)提升到O(logn)。

LinkedHashMap

LinkedHashMap繼承自HashMap,其內部結構是散列表+雙向鏈表,其中對散列表部分的put、get操作跟HashMap一樣,沒有變化。區別於HashMap的是,LinkedHashMap內部還維護了一個雙向鏈表(內部保存了所有元素),我們都知道HashMap基於Key-Value保存數據,但是HashMap不能按put順序去遍歷元素,如果想順序遍歷元素,可以使用HashMap的子類LinkedHashMap,LinkedHashMap維護了兩種迭代順序:

  • 按插入順序迭代: 默認設置,鏈表是按插入順序添加的,那麼遍歷時也是按插入順序訪問的。
  • 按訪問順序迭代: 調用get方法後,會將這次訪問的元素移動到鏈表的尾部,只有設置accessOrder=true時纔會生效。如果設置了最大容量(capacity)並重寫removeEldestEntry方法(返回true),當新添加元素時,會將鏈表中最老的元素移除。

內部結構

LinkedHashMap.png

  • 基於插入順序訪問:
Map<String, String> linkedHashMap = new LinkedHashMap<>();

linkedHashMap.put("A", "1");
linkedHashMap.put("B", "2");
linkedHashMap.put("C", "3");
linkedHashMap.put("D", "4");
linkedHashMap.put("E", "5");

for (Map.Entry<String, String> entry : linkedHashMap.entrySet()) {
    System.out.println("key: " + entry.getKey() + ",value: " + entry.getValue());
}

執行結果:

key: A,value: 1
key: B,value: 2
key: C,value: 3
key: D,value: 4
key: E,value: 5
  • 基於訪問順序遍歷:
int initialCapacity = 10;//初始化容量
float loadFactor = 0.75f;//負載因子,一般設置爲0.75
boolean accessOrder = true;//false 基於插入順序  true 基於訪問順序

Map<String, String> linkedHashMap = new LinkedHashMap<String, String>(initialCapacity, loadFactor, accessOrder);

linkedHashMap.put("A", "1");
linkedHashMap.put("B", "2");
linkedHashMap.put("C", "3");
linkedHashMap.put("D", "4");
linkedHashMap.put("E", "5");

for (Map.Entry<String, String> entry : linkedHashMap.entrySet()) {
    System.out.println("訪問前:key: " + entry.getKey() + ",value: " + entry.getValue());
}

linkedHashMap.get("C");//調用了get方法,執行完後此元素將被加入到鏈表尾部

System.out.println("-------------------");
for (Map.Entry<String, String> entry : linkedHashMap.entrySet()) {
    System.out.println("訪問後:key: " + entry.getKey() + ",value: " + entry.getValue());
}

linkedHashMap.put("F", "6");

System.out.println("-------------------");
for (Map.Entry<String, String> entry : linkedHashMap.entrySet()) {
    System.out.println("插入後:key: " + entry.getKey() + ",value: " + entry.getValue());
}

執行結果:

訪問前:key: A,value: 1
訪問前:key: B,value: 2
訪問前:key: C,value: 3
訪問前:key: D,value: 4
訪問前:key: E,value: 5
-------------------
訪問後:key: A,value: 1
訪問後:key: B,value: 2
訪問後:key: D,value: 4
訪問後:key: E,value: 5
訪問後:key: C,value: 3
-------------------
插入後:key: A,value: 1
插入後:key: B,value: 2
插入後:key: D,value: 4
插入後:key: E,value: 5
插入後:key: C,value: 3
插入後:key: F,value: 6

LinkedHashMap初始化時傳入了loadFactor並且設置爲true,當添加完元素後,通過linkedHashMap.get("C")訪問了其中的某個元素,之後再遍歷LinkedHashMap,發現被訪問的元素已經被放到鏈表的尾部了。

  • 移除最近最少使用的元素
int initialCapacity = 10;//初始化容量
float loadFactor = 0.75f;//負載因子,一般設置爲0.75
boolean accessOrder = true;//false 基於插入順序  true 基於訪問順序

Map<String, String> linkedHashMap = new LinkedHashMap<String, String>(initialCapacity, loadFactor, accessOrder){
    @Override
    protected boolean removeEldestEntry(Map.Entry<String, String> eldest) {
        return size() > 3;
    }
};

linkedHashMap.put("A", "1");
linkedHashMap.put("B", "2");
linkedHashMap.put("C", "3");
linkedHashMap.put("D", "4");
linkedHashMap.put("E", "5");

for (Map.Entry<String, String> entry : linkedHashMap.entrySet()) {
    System.out.println("key: " + entry.getKey() + ",value: " + entry.getValue());
}

執行結果:

key: C,value: 3
key: D,value: 4
key: E,value: 5

我們添加了5個元素,但是最後遍歷時只顯示了3個元素,這是因爲重寫了removeEldestEntry方法,當元素的size>3時,此方法會返回true, 當每次添加元素時,會判斷removeEldestEntry方法返回值是否爲true, 是true的話會去除鏈表中最近最少使用的元素。

put元素

LinkedHashMap中沒有重寫put方法,直接使用父類HashMap#put方法,具體實現可以參看上面HashMap的介紹,LinkedHashMap#put絕大部分跟HashMap#put實現一致,在HashMap#put方法中,調用了newNode方法,目的是生成一個新節點Node,而在LinkedhashMap中重寫了這個方法:

//當散列表中沒有key對應的Node節點時,新生成一個key對應的Node節點,並向鏈表的尾部添加元素
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);    
    linkNodeLast(p);
    return p;
}

//向鏈表的尾部添加元素
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
    //定義了head tail雙向鏈表
    LinkedHashMap.Entry<K,V> last = tail;
    tail = p;
    if (last == null)
        head = p;
    else {
        p.before = last;
        last.after = p;
    }
}

//如果key已經在散列表中,散列表中的舊值會被覆蓋,同時鏈表中對應的節點也會移動到鏈表的尾部
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;
    }
}

可見在LinkedhashMap#put方法中調用newNode()時,除了新生成一個節點之外,還將此節點元素加入到一個雙向鏈表中,從而可以按一定順序去遍歷元素。

get元素

public V get(Object key) {
    Node<K,V> e;
    //調用父類HashMap中的getNode獲取key對應的value元素
    if ((e = getNode(hash(key), key)) == null)
        return null;
    if (accessOrder)
        afterNodeAccess(e);
    return e.value;
}

get方法調用父類HashMap中的getNode獲取key對應的value元素,同時如果accessOrde爲true,同樣會調afterNodeAccess方法將元素添加到鏈表的尾部,實現按訪問順序排序。

移除最近最少使用元素

在HashMap#put中,調用了afterNodeInsertion()方法,意思是在插入新元素後做一些處理,但是在HashMap中該方法只是一個空實現,該方法在LinkedHashMaP中被重寫:

void afterNodeInsertion(boolean evict) { // possibly remove eldest
    LinkedHashMap.Entry<K,V> first;
    if (evict && (first = head) != null && removeEldestEntry(first)) {
        K key = first.key;
        //移除元素
        removeNode(hash(key), key, null, false, true);
    }
}

protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
    return false;
}

LinkedHashMap#removeEldestEntry默認返回了false,所以默認往LinkedHashMap中put元素時,並不會移除鏈表中頭部的元素(最近最少使用),當我們重寫removeEldestEntry方法並符合條件時返回true,那麼便會觸發移除操作,移除掉最近最少使用的元素,這也是LRUCache的原理。

TreeMap

  • TreeMap實現了Map接口,可以存儲有序key-value集合,內部結構是紅黑樹
  • TreeMap能比較元素大小,對傳入的k也進行大小排序,可以使用自然順序,也可以自定義比較器(實現Comparable接口並重寫compareTo方法)進行排序

使用元素自然排序

  • key是String類型:
public static void main(String[] args) {

TreeMap<String, Integer> treeMap = new TreeMap<>();
treeMap.put("D", 4);
treeMap.put("A", 1);
treeMap.put("C", 3);
treeMap.put("B", 2);

//遍歷TreeMap
for (Map.Entry<String, Integer> entry : treeMap.entrySet()) {
    System.out.println("key: " + entry.getKey() + ",value: " + entry.getValue());
 }
}

執行結果:

key: A,value: 1
key: B,value: 2
key: C,value: 3
key: D,value: 4
  • key是Integer類型
public static void main(String[] args) {
  
  TreeMap<Integer, Integer> treeMap = new TreeMap<>();
  treeMap.put(4, 4);
  treeMap.put(1, 1);
  treeMap.put(3, 3);
  treeMap.put(2, 2);

  for (Map.Entry<Integer, Integer> entry : treeMap.entrySet()) {
      System.out.println("key: " + entry.getKey() + ",value: " + entry.getValue());
  }
}

執行結果:

key: 1,value: 1
key: 2,value: 2
key: 3,value: 3
key: 4,value: 4

String、Integer類內部都實現了Comparable接口並在重寫的compareTo方法中比較大小。

使用自定義排序

public static void main(String[] args) {

  Person person1 = new Person("張三", 20);
  Person person2 = new Person("李四", 10);
  Person person3 = new Person("王五", 30);
  Person person4 = new Person("趙六", 40);

  TreeMap<Person, Integer> treeMap = new TreeMap<>();
    treeMap.put(person1, person1.getAge());
    treeMap.put(person2, person2.getAge());
    treeMap.put(person3, person3.getAge());
    treeMap.put(person4, person4.getAge());
    
    //遍歷treeMap,其中key是自定義Person類型,Person內部是按age從小到大排序
    for (Map.Entry<Person, Integer> entry : treeMap.entrySet()) {
        System.out.println("key: " + entry.getKey() + ",value: " + entry.getValue());
    }
}

static class Person implements Comparable<Person> {

    private String Name;
    private int age;

    Person(String name, int age) {
        this.Name = name;
        this.age = age;
    }

    public String getName() {
        return Name;
    }

    public void setName(String name) {
        Name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public int compareTo(Person o) {
        if (age >= o.getAge()) {
            //年齡大的元素放在後面
            return 1;
        }
        return -1;
    }

    @Override
    public String toString() {
        return "[person(name:" + getName() + ",age:" + getAge() + ")]";
    }
}

執行結果:

key: [person(name:李四,age:10)],value: 10
key: [person(name:張三,age:20)],value: 20
key: [person(name:王五,age:30)],value: 30
key: [person(name:趙六,age:40)],value: 40

最終遍歷結果是按Person#age從小到大排序的,這裏需要注意一點,放入TreeMap中的Key對象必須實現了Comparable接口,否則會拋出java.lang.ClassCastException: Collection._Map$xxx cannot be cast to java.lang.Comparable異常信息。

關於TreeMaP的源碼,可以參考下面兩篇文章:

  • https://www.jianshu.com/p/2dcff3634326
  • https://www.cnblogs.com/skywang12345/p/3310928.html

參考

【1】Java8系列之重新認識HashMap:https://mp.weixin.qq.com/s/oIE4Nnqs5_lOE1D-r9xXWg
【2】https://crossoverjie.top/2018/07/23/java-senior/ConcurrentHashMap/
【3】LinkedHashMap: https://www.cnblogs.com/yulinfeng/p/8590010.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章