一文詳解java中HashMap集合

本文從HashMap的定義、成員變量、成員方法上詳解HashMap的底層實現。

1 HashMap的定義

1.1 首先來看HashMap的定義

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {
    ......
    }

可見HashMap是AbstractMap的子類,並實現了Map接口。其他兩個接口主要是複製和序列化的接口,暫時不管。

1.2 看看AbstractMap類和Map接口長什麼樣

public abstract class AbstractMap<K,V> implements Map<K,V> {}
public interface Map<K,V> {}

Map接口並未繼承其他接口,不同於List和Set接口,Map並未繼承Collection——>Map不能使用迭代器遍歷。


Map接口無非就是定義了一些Map數據結構常用的抽象方法,包括put/remove/get等方法,暫時不去看它。

1.3 粗略的剖析一下AbstractMap抽象類
先找到抽象類中成員變量如下:

transient volatile Set<K>        keySet;
transient volatile Collection<V> values;
public abstract Set<Entry<K,V>> entrySet();

上面三個成員變量是其中最重要的三個成員變量,根據變量名就能知道三個貨是幹什麼用的。分別用一個Set存儲所有的key,一個Collection存儲values,和一個Set<Entry>存儲所有的鍵值對。
  Entry大概瞭解一下:一個接口,實現類有SimpleEntry/SimpleImmutableEntry。就是一個key、一個value成員變量存儲鍵值對,還包括一些基本方法。

2 HashMap的成員變量

理解HashMap的底層實現,首先看它有哪些成員變量,它用什麼存儲鍵值對?用什麼存儲所有key值?…
  首先找到HashMap的所有成員變量(源碼387行開始),把每個成員變量上面的註釋用中文濃縮一下就是:

/* ---------------- Fields -------------- */

	//第一次使用時初始化,必要時擴容,每次擴容增大兩倍。(就是存放Node的數組)
    transient Node<K,V>[] table;

    //爲keySet()和values()使用。(存放所有Entry鍵值對)
    transient Set<Map.Entry<K,V>> entrySet;

    // 鍵值對個數
    transient int size;

    //the number of .......
    transient int modCount;

    //是否resize的閾值
    int threshold;

2.1 先談談entrySet的存在。
  可見有兩個成員變量存儲鍵值對,一個是table數組,一個是entrySet。那爲什麼有兩個呢?
  別忘了HashMap繼承了AbstractMap抽象類,而AbstractMap類中定義了EntrySet抽象方法,返回一個Set<Entry<K,V>>。既然是繼承,就必須實現抽象類中的方法。
  HashMap中主體實現並未使用entrySet這個成員變量,所以可以不去考慮它的存在。


2.2 貫穿整個HashMap的Node<K,V>[] table
Map集合的主要功能無非就是增刪改查,而這些操作其實就是在操作table這個成員變量。


2.3 詳解Node
  爲什麼不用Entry保存鍵值對,而用Node?
  因爲Node裏面多了一個成員變量:next!!
  因爲table是用來維持hash表,hash表可能會存在下標衝突,在hash表結構中一般有兩種方式處理下標衝突:1)將數據放在衝突下標的後一位    2)在下標的位置存放鏈表。
  既然有next存在,那就是鏈表咯。

static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;

2.4 其他成員變量
其他成員變量在講解成員方法時涉及到時解析。

3 HashMap的成員方法

爲了更好的理解HashMap的底層實現,光了解成員變量還是不行,下面看一下HashMap的put方法,詳解它如何存儲一個鍵值對。
3.1 put()方法

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

put方法傳入一個key和value,跟我們使用Map時一樣。主要看一下putVal這個方法的實現。

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)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {}
    }

傳入參數有5個,我們先只關注前面三個。hash:key的hash值,調用hash()方法計算得來;key、value沒什麼好說的。那先談談hash這個值的作用。
  通俗理解就是:hash這個值就是用來找到鍵值對應該存放在table數組的那個下標位置。我們知道Hash表結構的優點是查詢快,也就是我們能通過一個key值來快速得到value的值,而不是遍歷整個數組。當我們傳入一個key值,先計算key的hash值,通過hash值直接定位到數組下標,如此實現O(1)的時間複雜度查詢。
  第一個if語句判斷table是否爲空或長度是否爲0,若爲空或長度爲0則調用resize()方法初始化table數組,初始化數組長度爲16。
  第二個if語句中:首先計算 i = (n - 1) & hash;然後得到table[i]的值,然後判斷table[i]是否爲空。若爲空則將鍵值對存放在下標爲i處。i值的計算其實就是根據hash值計算下標,其中n爲數組長度,位與運算保證數組下標不越界。
  

  看完兩個if語句好像搞懂了一些了。那如果計算的下標已經存有其他Node了呢?
  代碼就不貼了。簡述一下:Node有一個成員變量爲next,那就在下標出存放一個鏈表,遍歷鏈表存將鍵值對放在鏈表尾部。那麼有個問題就是鏈表太長之後查詢效率不是大大降低麼?爲解決這個問題,源碼將鏈表長度大於8的鏈表轉換成一顆紅黑樹,增加查詢效率。前面說到table初始化長度爲16,當我們存儲大量數據時問題又來了,就算變成紅黑樹效率也低怎麼辦?別忘了threshold這個成員變量是用於擴容的,當size大於threshold時,自動將數組大小增加一倍。

3.2 get方法
通過key值獲取value

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 &&  //若table不爲空
            (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;  //若鏈表頭就是key值,則直接返回Node
            if ((e = first.next) != null) {  //若鏈表頭的key不能與傳入的key,則遍歷鏈表或樹
                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;  //未找到則返回null
    }
發佈了78 篇原創文章 · 獲贊 8 · 訪問量 8511
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章