Java集合之HashMap源碼解析

原文地址

HashMap

HashMapMap 的一個實現類,它代表的是一種鍵值對的數據存儲形式。

大多數情況下可以直接定位到它的值,因而具有很快的訪問速度,但遍歷順序卻是不確定的。

HashMap最多只允許一條記錄的鍵為null,允許多條記錄的值為null。不保證有序(比如插入的順序)、也不保證序不隨時間變化。

jdk 8 之前,其內部是由數組+鏈表來實現的,而 jdk 8 對于鏈表長度超過 8 的鏈表將轉儲為紅黑樹。

HashMap非線程安全,即任一時刻可以有多個線程同時寫HashMap,可能會導致數據的不一致。如果需要滿足線程安全,可以用 CollectionssynchronizedMap方法使HashMap具有線程安全的能力,或者使用ConcurrentHashMap。

下面我們先來看一下HashMap內部所用到的存儲結構

HashMap數組+鏈表+紅黑樹(JDK1.8增加了紅黑樹部分)實現的

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

NodeHashMap的一個內部類,實現了Map.Entry接口,本質上就是一個映射(鍵值對)。

有時兩個key會定位到相同的位置,表示發生了Hash碰撞。當然Hash算法計算結果越分散均勻,Hash碰撞的概率就越小,map的存取效率就會越高。

HashMap類中有一個非常重要的字段,就是 Node[] table,即哈希桶數組。

如果哈希桶數組很大,即使較差的Hash算法也會比較分散,如果哈希桶數組數組很小,即使好的Hash算法也會出現較多碰撞。

所以就需要在空間成本和時間成本之間權衡,其實就是在根據實際情況確定哈希桶數組的大小,并在此基礎上設計好的hash算法減少Hash碰撞。那么通過什么方式來控制map使得Hash碰撞的概率又小,哈希桶數組(Node[] table)占用空間又少呢?答案就是好的Hash算法和擴容機制。

下面我們就來看一下hashmap中經過jdk1.8優化過的Hash算法和擴容機制。

不過在這之前我們先了解下hashmap中的變量

//初始化容量16 hashMap的容量必須是2的指數倍,Hashtable是11
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

//最大容量2的30次方
static final int MAXIMUM_CAPACITY = 1 << 30;

//默認加載因子默認的平衡因子為0.75,這是權衡了時間復雜度與空間復雜度之后的最好取值
static final float DEFAULT_LOAD_FACTOR = 0.75f;

// 如果鏈表的長度超過這個閾值就改用紅黑樹存儲
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;    //實際存儲的鍵值對個數

transient int modCount;

 //閾值,當table == {}時,該值為初始容量(初始容量默認為16);當table被填充了,也就是為table分配內存空間后,threshold一般為 capacity*loadFactory。
int threshold;  

final float loadFactor;    //負載因子,代表了table的填充度有多少,默認是0.75

在HashMap中有兩個很重要的參數,容量(Capacity)和負載因子(Load factor)

Capacity就是buckets的數目,Load factor就是buckets填滿程度的最大比例。如果對迭代性能要求很高的話不要把capacity設置過大,也不要把load factor設置過小。當bucket填充的數目(即hashmap中元素的個數)大于capacity*load factor時就需要調整buckets的數目為當前的2倍。

Hash算法

static final int hash(Object key) {
    int h;
    // h = key.hashCode() 為第一步 取hashCode值
    // h ^ (h >>> 16)  為第二步 高位參與運算
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

static int indexFor(int h, int length) {  
     return h & (length-1);  //第三步 取模運算
}

indexFor是jdk1.7的源碼,jdk1.8沒有這個方法但是jdk1.8也是通過取模運算來計算的

這里的Hash算法本質上就是三步:取key的hashCode值高位運算、取模運算。

對于任意給定的對象,只要它的hashCode()返回值相同,那么程序調用方法一所計算得到的Hash碼值總是相同的。我們首先想到的就是把hash值對數組長度取模運算,這樣一來,元素的分布相對來說是比較均勻的。但是,模運算的消耗還是比較大的,這里我們用&位運算來優化效率。

這個方法非常巧妙,它通過h & (table.length -1)來得到該對象的保存位,而HashMap底層數組的長度總是2的n次方,這是HashMap在速度上的優化。當length總是2的n次方時,h& (length-1)運算等價于對length取模,也就是h%length,但是&%具有更高的效率。

JDK1.8的實現中,優化了高位運算的算法,通過hashCode()的高16位異或低16位實現的:(h = k.hashCode()) ^ (h >>> 16),主要是從速度、功效、質量來考慮的,這么做可以Node數組table的length比較小的時候,也能保證考慮到高低Bit都參與到Hash的計算中,同時不會有太大的開銷。

hash.png

擴容機制

擴容(resize)就是重新計算容量,向HashMap對象里不停的添加元素,而HashMap對象內部的數組無法裝載更多的元素時,對象就需要擴大數組的長度,以便能裝入更多的元素。

當然Java里的數組是無法自動擴容的,方法是使用一個新的數組代替已有的容量小的數組,就像我們用一個小桶裝水,如果想裝更多的水,就得換大水桶。

put時,如果發現目前的bucket占用程度已經超過了Load Factor所希望的比例,那么就會發生resize。在resize的過程,簡單的說就是把bucket擴充為2倍,之后重新計算index,把節點再放到新的bucket中。
因為我們使用的是2次冪的擴展(指長度擴為原來2倍),所以,元素的位置要么是在原位置,要么是在原位置再移動2次冪的位置。

例如我們從16擴展為32時,具體的變化如下所示:

1.png

因此元素在重新計算hash之后,因為n變為2倍,那么n-1的mask范圍在高位多1bit(紅色),因此新的index就會發生這樣的變化:

2.png

因此,我們在擴充HashMap的時候,不需要重新計算hash,只需要看看原來的hash值新增的那個bit是1還是0就好了,是0的話索引沒變,是1的話索引變成“原索引+oldCap”。

這個設計確實非常的巧妙,既省去了重新計算hash值的時間,而且同時,由于新增的1bit是0還是1可以認為是隨機的,因此resize的過程,均勻的把之前的沖突的節點分散到新的bucket了。

final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;    
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    if (oldCap > 0) {    //說明舊數組已經被初始化完成了,此處需要給舊數組擴容   
        if (oldCap >= MAXIMUM_CAPACITY) {    如果容量超過Hash Map限定的最大值,將不再擴容
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }    // 沒超過最大值,就擴充為原來的2倍
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1;   // 2倍
    }
    //數組未初始化,但閾值不為 0,為什么不為 0 ?
    //構造函數根據傳入的容量打造了一個合適的數組容量暫存在閾值中,這里直接使用
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    else {    //數組未初始化并且閾值也為0,說明一切都以默認值進行構造
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }    
    // newCap = oldThr 之后并沒有計算閾值,所以 newThr = 0
    // 重新計算下一次進行擴容的上限
    if (newThr == 0) {   
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];  //根據新的容量初始化一個數組
    table = newTab;
    if (oldTab != null) {
        for (int j = 0; j < oldCap; ++j) {     // 把每個bucket都移動到新的buckets中
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {  //獲取頭結點
                oldTab[j] = null;
                if (e.next == null)   //說明鏈表或者紅黑樹只有一個頭結點,轉移至新表
                    newTab[e.hash & (newCap - 1)] = e;
                else if (e instanceof TreeNode)  //如果 e 是紅黑樹結點,紅黑樹分裂,轉移至新表
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else {   //這部分是將鏈表中的各個節點原序地轉移至新表中
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        next = e.next;
                        if ((e.hash & oldCap) == 0) {    // 原索引
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        else {                          // 原索引+oldCap
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    if (loTail != null) {      // 原索引放到bucket里
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {     // 原索引+oldCap放到bucket里
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

下面我們再來看看hashmap中的其他方法

構造函數

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;
    this.threshold = tableSizeFor(initialCapacity);
}

這是一個最基本的構造函數,需要調用方傳入兩個參數,initialCapacityloadFactor

程序的大部分代碼在判斷傳入參數的合法性,initialCapacity 小于零將拋出異常,大于 MAXIMUM_CAPACITY 將被限定為 MAXIMUM_CAPACITY。loadFactor 如果小于等于零或者非數字類型也會拋出異常。

整個構造函數的核心在對 threshold 的初始化操作:

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

由以上代碼可以看出,當在實例化HashMap實例時,如果給定了initialCapacity,由于HashMapcapacity都是2的冪次方,因此這個方法用于找到大于等于initialCapacity的最小的2的冪(initialCapacity如果就是2的冪,則返回的還是這個數)。

下面分析這個算法:

首先,我們想一下為什么要對cap做減1操作?

int n = cap - 1 

這是為了防止,cap已經是2的冪。如果cap已經是2的冪,又沒有執行這個減1操作,則執行完后面的幾條無符號右移操作之后,返回的capacity將是這個cap的2倍。如果不懂,要看完后面的幾個無符號右移之后再回來看看。

下面看看這幾個無符號右移操作:

如果n這時為0了(經過了cap-1之后),則經過后面的幾次無符號右移依然是0,最后返回的capacity是1(最后有個n+1的操作)。

這里我們只討論n不等于0的情況。

n |= n >>> 1;

由于n不等于0,則n的二進制表示中總會有一bit為1,這時考慮最高位的1。通過無符號右移1位,則將最高位的1右移了1位,再做或操作,使得n的二進制表示中與最高位的1緊鄰的右邊一位也為1,如000011xxxxxx。

n |= n >>> 2;

注意,這個n已經進行過 n |= n >>> 1; 操作。假設此時n為000011xxxxxx ,則n無符號右移兩位,會將最高位兩個連續的1右移兩位,然后再與原來的n做或操作,這樣n的二進制表示的高位中會有4個連續的1。如00001111xxxxxx 。

n |= n >>> 4;

這次把已經有的高位中的連續的4個1,右移4位,再做或操作,這樣n的二進制表示的高位中會有8個連續的1。如00001111 1111xxxxxx 。

以此類推 。。。

注意,容量最大也就是32bit的正數,因此最后 n |= n >>> 16; 最多也就32個1,但是這時已經大于了MAXIMUM_CAPACITY ,所以取值到MAXIMUM_CAPACITY 。

下面我們通過一個圖片來看一下整個過程:

3.png

HashMap 中還有很多的重載構造函數,但幾乎都是基于上述的構造函數的。

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

public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

以上這些構造函數都沒有直接的創建一個切實存在的數組,他們都是在為創建數組需要的一些參數做初始化,
所以有些在構造函數中并沒有被初始化的屬性都會在實際初始化數組的時候用默認值替換。

實際對數組進行初始化是在添加元素的時候進行的(即put方法)

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

put方法

put 方法也是HashMap中比較重要的方法,因為通過該方法我們可以窺探到 HashMap 在內部是如何進行數據存儲的,所謂的數組+鏈表+紅黑樹的存儲結構是如何形成的,又是在何種情況下將鏈表轉換成紅黑樹來優化性能的。

put方法的大致實現過程如下:

  • 對key的hashCode()做hash,然后再計算index;
  • 如果沒碰撞直接放到bucket里;
  • 如果碰撞了,以鏈表的形式存在buckets后;
  • 如果碰撞導致鏈表過長(大于等于TREEIFY_THRESHOLD),就把鏈表轉換成紅黑樹;
  • 如果節點已經存在就替換old value(保證key的唯一性)
  • 如果bucket滿了(超過load factor*current capacity),就要resize。
public V put(K key, V value) {    // 對key的hashCode()做hash
    return putVal(hash(key), key, value, false, true);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)   // tab為空則創建(初次添加元素)
        n = (tab = resize()).length;
    if ((p = tab[i = (n - 1) & hash]) == null)   //根據鍵值key計算hash值得到插入的數組索引i,如果table[i]==null,直接新建節點添加 
        tab[i] = newNode(hash, key, value, null);
    else {   //如果對應的節點存在元素
        Node<K,V> e; K k;    
        if (p.hash == hash &&       //判斷table[i]的首個元素是否和key一樣,如果相同直接覆蓋value
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        else if (p instanceof TreeNode)   //判斷table[i] 是否為treeNode,即table[i] 是否是紅黑樹,如果是紅黑樹,則直接在樹中插入鍵值對
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
        // 遍歷table[i],判斷鏈表長度是否大于TREEIFY_THRESHOLD(默認值為8),大于8的話把鏈表轉換為紅黑樹,在紅黑樹中執行插入操作,否則進行鏈表的插入操作;
        // 遍歷過程中若發現key已經存在直接覆蓋value即可;
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }  //e 不是 null,說明當前的 put 操作是一次修改操作并且e指向的就是需要被修改的結點
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    // 插入成功后,判斷實際存在的鍵值對數量size是否超多了最大容量threshold,如果超過,進行擴容
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

get函數實現

在理解了put之后,get就很簡單了。大致思路如下:

bucket里的第一個節點,直接命中;
如果有沖突,則通過key.equals(k)去查找對應的entry
若為樹,則在樹中通過key.equals(k)查找,O(logn);
若為鏈表,則在鏈表中通過key.equals(k)查找,O(n)。

具體代碼的實現如下:

public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}
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) {
        // 直接命中
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        // 未命中
        if ((e = first.next) != null) {
            // 在樹中get
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            // 在鏈表中get
            do {
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}

remove方法

刪除操作就是一個查找+刪除的過程,相對于添加操作其實容易一些

public V remove(Object key) {
    Node<K,V> e;
    return (e = removeNode(hash(key), key, null, false, true)) == null ?
        null : e.value;
}

根據鍵值刪除指定節點,這是一個最常見的操作了。顯然,removeNode 方法是核心。

final Node<K,V> removeNode(int hash, Object key, Object value,boolean matchValue, boolean movable) {
    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;
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            node = p;
        else if ((e = p.next) != null) {
            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);
            }
        }
        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)
                tab[index] = node.next;
            else
                p.next = node.next;
            ++modCount;
            --size;
            afterNodeRemoval(node);
            return node;
        }
    }
    return null;
}

刪除操作需要保證在表不為空的情況下進行,并且 p 節點根據鍵的 hash 值對應到數組的索引,在該索引處必定有節點,如果為 null ,那么間接說明此鍵所對應的結點并不存在于整個 HashMap 中,這是不合法的,所以首先要在這兩個大前提下才能進行刪除結點的操作。

第一步

if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))
     node = p;

需要刪除的結點就是這個頭節點,讓 node 引用指向它。否則說明待刪除的結點在當前 p 所指向的頭節點的鏈表或紅黑樹中,于是需要我們遍歷查找。

第二步

else if ((e = p.next) != null) {
     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);
     }
}

如果頭節點是紅黑樹結點,那么調用紅黑樹自己的遍歷方法去得到這個待刪結點。否則就是普通鏈表,我們使用 do while 循環去遍歷找到待刪結點。找到節點之后,接下來就是刪除操作了。

第三步

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)
            tab[index] = node.next;
       else
            p.next = node.next;
       ++modCount;
       --size;
       afterNodeRemoval(node);
       return node;
 }

刪除操作也很簡單,如果是紅黑樹結點的刪除,直接調用紅黑樹的刪除方法進行刪除即可,如果是待刪結點就是一個頭節點,那么用它的 next 結點頂替它作為頭節點存放在 table[index] 中,如果刪除的是普通鏈表中的一個節點,用該結點的前一個節點直接跳過該待刪結點指向它的 next 結點即可。

最后,如果 removeNode 方法刪除成功將返回被刪結點,否則返回 null。

其他常用方法

clear

public void clear() {
    Node<K,V>[] tab;
    modCount++;
    if ((tab = table) != null && size > 0) {
        size = 0;
        for (int i = 0; i < tab.length; ++i)
            tab[i] = null;
    }
}

該方法調用結束后將清除 HashMap 中存儲的所有元素。

keySet

//實例屬性 keySet
transient volatile Set<K>        keySet;

public Set<K> keySet() {
    Set<K> ks;
    return (ks = keySet) == null ? (keySet = new KeySet()) : ks;
}
final class KeySet extends AbstractSet<K> {
    public final int size()                 { return size; }
    public final void clear()               { HashMap.this.clear(); }
    public final Iterator<K> iterator()     { return new KeyIterator(); }
    public final boolean contains(Object o) { return containsKey(o); }
    public final boolean remove(Object key) {
        return removeNode(hash(key), key, null, false, true) != null;
    }
    public final Spliterator<K> spliterator() {
        return new KeySpliterator<>(HashMap.this, 0, -1, 0, 0);
    }
}

HashMap 中定義了一個 keySet 的實例屬性,它保存的是整個 HashMap 中所有鍵的集合。上述所列出的 KeySet 類是 Set 的一個實現類,它負責為我們提供有關 HashMap 中所有對鍵的操作。

可以看到,KeySet 中的所有的實例方法都依賴當前的 HashMap 實例,也就是說,我們對返回的 keySet 集中的任意一個操作都會直接映射到當前 HashMap 實例中,例如你執行刪除一個鍵的操作,那么 HashMap 中將會少一個節點。

values

public Collection<V> values() {
    Collection<V> vs;
    return (vs = values) == null ? (values = new Values()) : vs;
}

values 方法其實和 keySet 方法類似,它返回了所有節點的 value 屬性所構成的 Collection 集合,此處不再贅述。

entrySet

public Set<Map.Entry<K,V>> entrySet() {
    Set<Map.Entry<K,V>> es;
    return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
}

它返回的是所有節點的集合,或者說是所有的鍵值對集合。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,606評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,582評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,540評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,028評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,801評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,223評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,294評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,442評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,976評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,800評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,996評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,543評論 5 360
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,233評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,926評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,702評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,991評論 2 374

推薦閱讀更多精彩內容

  • HashMap 是 Java 面試必考的知識點,面試官從這個小知識點就可以了解我們對 Java 基礎的掌握程度。網...
    野狗子嗷嗷嗷閱讀 6,681評論 9 107
  • 一、基本數據類型 注釋 單行注釋:// 區域注釋:/* */ 文檔注釋:/** */ 數值 對于byte類型而言...
    龍貓小爺閱讀 4,283評論 0 16
  • 解釋型語言,由Python解釋器把源文件編譯為pyc文件(字節碼文件), 然后讓Python虛擬機去運行字節碼 鏈...
    yangqi916閱讀 1,398評論 0 0
  • 這幾天由于事情的耽誤所以書也沒有看多少,今天又重新拾起這本書《全球通史》。 在提到這本書為何而寫,對于未來有何妙用...
    褲襪電源甜閱讀 239評論 0 0
  • 今天去見了兩位朋友。 中午見的那位,聊得很開心。一起聊心態,聊近況,聊之后的計劃與想法。還吃了好吃的家鄉菜,滿足。...
    養成系菜菜在成長閱讀 176評論 0 1