1. 程式人生 > >3.hashMap的資料結構,原始碼中的常用方法

3.hashMap的資料結構,原始碼中的常用方法

一:hashMap的資料結構

        HashMap儲存的是鍵值對,並允許使用null值和null鍵,不保證對映的順序。HashMap實際上是一個“連結串列雜湊”的資料結構,即陣列和連結串列和紅黑樹的結合體。

         陣列:儲存區間連續,佔用記憶體嚴重,定址容易,插入刪除困難;          連結串列:儲存區間離散,佔用記憶體比較寬鬆,定址困難,插入刪除容易;

        Hashmap綜合應用了這兩種資料結構,實現了定址容易,插入刪除也容易

      在jdk8中,HashMap處理“碰撞”增加了紅黑樹這種資料結構,當碰撞結點較少時,採用連結串列儲存,當較大時(>8個),採用紅黑樹(特點是查詢時間是O(logn))儲存(有一個閥值控制,大於閥值(8個),將連結串列儲存轉換成紅黑樹儲存)。

二:原始碼中的常用方法

(1):get()方法

  • bucket裡的第一個節點,直接命中
  • 如果有衝突,則通過key.equals(k)去查詢對應的entry
  • 若為樹,則在樹中通過key.equals(k)查詢,O(logn);
  • 若為連結串列,則在連結串列中通過key.equals(k)查詢,O(n)。

get值方法的過程是: 1、獲取key 2、通過hash函式得到hash值 int hash=key.hashCode();

3、得到桶號(一般都為hash值對桶數求模) int index =hash%Entry[].length;

4、比較桶的內部元素是否與key相等,若都不相等,則沒有找到。

5、取出相等的記錄的value。

(2):put()方法

put函式大致的思路為:

  • 對key的hashCode()做hash,然後再計算index
  • 如果沒碰撞直接放到bucket裡
  • 如果碰撞了,以連結串列的形式存在buckets後
  • 如果碰撞導致連結串列過長(大於等於TREEIFY_THRESHOLD)就把連結串列轉換成紅黑樹
  • 如果節點已經存在就替換old value(保證key的唯一性)
  • 如果bucket滿了(超過load factor*current capacity)就要resize

put鍵值對的方法的過程是: 1、獲取key ; 2、通過hash函式得到hash值; int hash=key.hashCode(); //獲取key的hashCode,這個值是一個固定的int值

3、得到桶號(一般都為hash值對桶數求模) ,也即陣列下標int index=hash%Entry[].length。//獲取陣列下標:key的hash值對Entry陣列長度進行取餘

4、 存放key和value在桶內。 table[index]=Entry物件;

(3):resize()方法

      當put時,如果發現目前的bucket佔用程度已經超過了Load Factor所希望的比例,那麼就會發生resize。在resize的過程,簡單的說就是把bucket擴充為2倍,之後重新計算index,把節點再放到新的bucket中。

(4):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。

這樣,相對複雜的 put 和 remove 方法的內部實現,我們已經完成解析了。下面看看其他常用的方法實現,它們或多或少都於這兩個方法有所關聯。

(5)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 中儲存的所有元素。

(6)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 中將會少一個節點。

(7)values

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

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

(8)entrySet

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

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