【JavaSE】Map集合,HashMap的常用方法put、get的原始碼解析

馮某r發表於2019-02-20

Map集合

Collection集合的特點是每次進行單個物件的儲存,如果現在要進行一對物件(偶物件)的儲存就只能使用Map集合來 完成,即Map集合中會一次性儲存兩個物件,且這兩個物件的關係:key=value結構。這種結構最大的特點是可以通 過key找到對應的value內容。
首先來觀察Map介面定義:

public interface Map<K,V>

在Map介面中有如下常用方法:

No 方法名稱 型別 描述
1. public V put(K key , V value); 普通 向Map中追加資料
2. public V get(Object key); 普通 根據Key取得對應的Value,如果沒有返回null
3. public Set KeySet(); 普通 取得所有key資訊,key不能重複
4. public Collection values(); 普通 取得所有value資訊,可以重複
5. public Set<Map.Entry<K,V>> entrySet(); 普通 將Map集合變為Set集合

Map本身是一個介面,要使用Map需要通過子類進行物件例項化。Map介面的常用子類有如下四個: HashMap、 Hashtable、TreeMap、ConcurrentHashMap。
這篇部落格重點講解一下HashMap。

HashMap子類

用法如下:

Map<K,V> map = new HashMap<>();

這樣就建立了一個Map集合,可以向裡面用put方法新增K,V鍵值對。
先了解一下HashMap是如何儲存鍵值對的,如圖:
在這裡插入圖片描述它結合了陣列查詢元素快,連結串列修改結構快的特點。先用雜湊表建立一個陣列,然後將產生雜湊衝突的元素按照鏈地址法的方式放在桶裡面的連結串列中。如果連結串列中的元素太多,連結串列還會樹化,下面再詳細說。

HashMap的構造方法

無參構造:

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

static final float DEFAULT_LOAD_FACTOR = 0.75f;

無參構造只是設定了一個雜湊表的負載因子,用的是預設值的0.75。
有參構造:

public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
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);
    }

可以發現所有的構造方法都沒有分配HashMap的容量,只是簡單的進行了負載因子和桶容量的賦值。其中tableSizeFor(initialCapacity)方法是為了將輸入的桶容量值,變為大於輸入值並且離輸入值最近的二次冪,原因下面解釋。

常用的方法put()

put方法是將鍵值對新增到HashMap的資料結構中:

public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

只是返回了putVal的方法,而hash函式是將key值通過Object的hashCode()計算出來的值再進行處理,因為利用Object的hashCode()返回的值達到231次方的數量,幾乎不會發生碰撞,需要的桶個數太多。

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        //如果初始的桶容量為空,或者桶個數為0,就呼叫resize()方法並將返回值賦給n。
        if ((tab = table) == null || (n = tab.length) == 0)
        	//resize()方法主要是進行桶的初始化
            n = (tab = resize()).length;
        //hash & n-1相當於hash%(n-1)這就是為什麼保證2次冪,使用位用算比取模速度快,然後判斷此下標的桶處是否為null
        if ((p = tab[i = (n - 1) & hash]) == null)
        	//如果為null就在這個桶的地方建立這個K,V節點作為頭節點
            tab[i] = newNode(hash, key, value, null);
        //雜湊表已經有初始化容量,並且此桶的頭節點不為null。
        else {
            Node<K,V> e; K k;
            //從上面可知p是首節點,所以首節點和當前節點如果K,V值相等,則當前節點替換首節點
            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 {
                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
                        	//當前連結串列個數已經達到樹化閾值8-1
                        	//嘗試呼叫樹化方法將連結串列樹化
                            treeifyBin(tab, hash);
                        break;
                    }
                    //判斷是否有節點和此節點一樣,如果一樣就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;
            }
        }
        //這個值和fail-fast有關
        ++modCount;
        // 新增節點後,整個HashMap的元素個數若要超過容量時,呼叫resize進行擴容操作
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

關於put方法的詳細解釋,在註釋中已經出現。下面做一個總結:

  1. put方法先判斷此雜湊表是否為null,如果為null就初始化雜湊表。(HashMap採用lazy-load策略(當第一次使用put時,才會將雜湊表初始化)
  2. 接下來通過HashMap自帶的hash函式計算此key值的下標,判斷此下標所在的桶是否為null,如果為null,就將此鍵值對作為頭節點。
  3. 如果上面兩步都否定,那麼判斷此桶是否已經樹化,如果樹化就用樹化的方式來新增新節點。
  4. 否則用連結串列的方式新增,在遍歷連結串列的過程中,如果發現和此節點一模一樣的key節點,就將舊value值替換為新的value值,如果新增完元素之後,桶中的節點數大於等於樹化的閾值8-1,就嘗試對此連結串列進行樹化(嘗試的意思是,呼叫樹化函式之後,還會判斷此雜湊表中的元素是否大於64,如果不大於64只是進行一次擴容,否則進行樹化
  5. 最後一步就是判斷新增完所有節點之後,判斷是否需要擴容。

常用的方法get()

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;
        //雜湊表已經初始化,並且當前key值的桶的頭節點不為null
        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) {
            	//若樹化,使用樹的方式查詢節點
                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;
    }

相關文章