1. 程式人生 > >java集合(4):HashMap原始碼分析(jdk1.8)

java集合(4):HashMap原始碼分析(jdk1.8)

前言

  • Map介面雖然也是集合體系中的重要一個分支,但是Map介面並不繼承自Collection,而是自成一派。
public interface Map<K,V>
  • Map集合儲存鍵對映到值的物件。一個集合中不能包含重複的鍵,每個鍵最多隻能對映到一個值。

  • Map 介面提供三種collection 檢視(意思是3種迭代器),允許以鍵集、值集或鍵-值對映關係集的形式來迭代某個Map的內容。對映順序 定義為迭代器在對映的 collection 檢視上返回其元素的順序。某些對映實現可明確保證其順序,如 TreeMap 類;另一些對映實現則不保證順序,如 HashMap 類。

  • Map介面最常用的兩個實現類就是HashMap和TreeMap。前者以雜湊表結構儲存元素,後者以二叉樹結構儲存元素,而且Set介面下的HashSet底層結構就是HashMap,所以講完HashMap再講HashSet就很簡單了。

正文

一,雜湊表結構

  • 雜湊表結構是資料結構中的重要概念之一,其原理不再贅述,本文重點介紹雜湊表結構的程式碼體現。需要說明的一點是:jdk1.8以前的HashMap只使用了陣列和連結串列的形式,這樣可能導致陣列某個位置上的連結串列過長,不利於查詢。jdk1.8以後,如果某位置的連結串列超過一定長度就會轉為紅黑樹,這樣就有利於查詢。

二,HashMap原始碼

1, 類名及常量

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

    private static final long serialVersionUID = 362498820763181265L;

    // Map集合初始容量
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

    // Map集合最大容量
static final int MAXIMUM_CAPACITY = 1 << 30; // 預設載入因子,用於動態擴充陣列容量 static final float DEFAULT_LOAD_FACTOR = 0.75f; // 當連結串列上的節點數大於等於8時,連結串列轉為樹結構 static final int TREEIFY_THRESHOLD = 8; // 當連結串列上的節點小於6時,樹結構轉連結串列 static final int UNTREEIFY_THRESHOLD = 6; // static final int MIN_TREEIFY_CAPACITY = 64; }

2,成員變數

    // 儲存元素的陣列,每個元素都是Node型的,Node是一個靜態內部類
    transient Node<K,V>[] table;

    // entrySet()方法返回的結果集
    transient Set<Map.Entry<K,V>> entrySet;

    // Map元素個數
    transient int size;

    // Map物件被修改的次數
    transient int modCount;

    // 增加陣列容量的閾值
    int threshold;

    // 載入因子
    final float loadFactor;

3,Node型別元素的內部類形式

static class Node<K,V> implements Map.Entry<K,V> { // 實現Map介面的內部介面Entry
        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;
        }
    }

4,HashMap的4個構造方法

    public HashMap() {  // 1,無參構造方法,成員變數使用預設值
        this.loadFactor = DEFAULT_LOAD_FACTOR; 
    }

    public HashMap(int initialCapacity) { // 2,初始容量的構造方法
        this(initialCapacity, DEFAULT_LOAD_FACTOR); // 去呼叫2個引數的構造方法
    }

    public HashMap(int initialCapacity, float loadFactor) { // 3,指定容量和載入因子
        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;  

        // 呼叫計算容量的方法,返回大於initialCapacity的最小2的冪。
        this.threshold = tableSizeFor(initialCapacity);  
    }

    public HashMap(Map<? extends K, ? extends V> m) { // 4,接收一個map物件
        this.loadFactor = DEFAULT_LOAD_FACTOR; // 預設載入因子
        putMapEntries(m, false); // 新增m到結構
    }

5,put()方法詳解,該方法也是最重要的方法,下面是方法的執行流程圖和原始碼分析

這裡寫圖片描述

執行過程:

①.判斷鍵值對陣列table[i]是否為空或為null,否則執行resize()進行擴容;

②.根據鍵值key計算hash值得到插入的陣列索引i,如果table[i]==null,直接新建節點新增,轉向⑥,如果table[i]不為空,轉向③;

③.判斷table[i]的首個元素是否和key一樣,如果相同直接覆蓋value,否則轉向④,這裡的相同指的是hashCode以及equals;

④.判斷table[i] 是否為treeNode,即table[i] 是否是紅黑樹,如果是紅黑樹,則直接在樹中插入鍵值對,否則轉向⑤;

⑤.遍歷table[i],判斷連結串列長度是否大於8,大於8的話把連結串列轉換為紅黑樹,在紅黑樹中執行插入操作,否則進行連結串列的插入操作;遍歷過程中若發現key已經存在直接覆蓋value即可;

⑥.插入成功後,判斷實際存在的鍵值對數量size是否超多了最大容量threshold,如果超過,進行擴容。

put方法原始碼:

    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true); // 呼叫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;

        // 如果table未初始化或長度為0,則進行初始化
        if ((tab = table) == null || (n = tab.length) == 0) 
            n = (tab = resize()).length;

        // 如果節點hash值對應的陣列位置為空,直接賦值
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);


        else {
            Node<K,V> e; K k;

            // 如果節點hash值相同且key相同,則直接覆蓋
            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);

                        // 如果連結串列長度大於8,則將連結串列轉為紅黑樹
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }

                    // 如果key存在,直接覆蓋
                    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;  // map結構被修改次數自增
        if (++size > threshold) // 新增元素後,如果陣列長度超過閾值,則擴容。
            resize();
        afterNodeInsertion(evict);
        return null;
    }

5.1,擴容機制

  • 擴容(resize)就是重新計算容量,向HashMap物件裡不停的新增元素,而HashMap物件內部的陣列無法裝載更多的元素時,物件就需要擴大陣列的長度,以便能裝入更多的元素。當然Java裡的陣列是無法自動擴容的,方法是使用一個新的陣列代替已有的容量小的陣列,就像我們用一個小桶裝水,如果想裝更多的水,就得換大水桶。

  • 陣列的容量是2的次冪,也就是說每次擴充套件,長度擴充套件為原來的2倍。接著元素也要變換位置,那麼,元素的位置要麼是在原位置,要麼是在原位置加上2的次冪的位置。所以,我們在擴充的時候,不需要再重新計算hash值,而是看原先的hash值新增的1bit是1還是0。如果是0,索引不變,元素待在原位;如果是1,新索引為“原索引+擴充套件長度”。這個設計確實非常的巧妙,既省去了重新計算hash值的時間,而且同時,由於新增的1bit是0還是1可以認為是隨機的,因此resize的過程,均勻的把之前的衝突的節點分散到新的bucket了。這一塊就是JDK1.8新增的優化點。

  • 擴容方法resize()
final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;   // 當前陣列賦值給oldTab
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;  // 當前閾值賦值給oldThr 
        int newCap, newThr = 0;  // 新陣列容量,新閾值
        if (oldCap > 0) {

            // 如果超過最大容量,就不再擴容
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }

            // 如果老容量擴大2倍仍不超過最大值,則新容量為原來的2倍
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }

        // 計算新的閾值
        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) {
                Node<K,V> e;  // 迴圈取出老陣列上的每一個元素
                if ((e = oldTab[j]) != null) { 
                    oldTab[j] = null; // 將老陣列元素置空,讓垃圾回收器回收

                    // 如果陣列元素沒有連結串列,直接新增到新陣列
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e; 

                    // 如果e是樹節點,則按照樹結構處理該分支
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);

                    // 如果e是連結串列節點,則按照連結串列結構處理該分支
                    else { // preserve order
                        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 {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);

                        // 原索引放在老位置上
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }

                        // 新索引放在新位置上
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

6,get()方法詳解,get的過程就是遍歷的過程。

    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;  // tab是table的副本
        Node<K,V> first, e;  // first代表陣列上鍊表的第一個元素
        int n; 
        K k; // k是first元素的key屬性副本

        // 如果陣列不為空,陣列長度大於0,陣列上鍊表第一個元素不為空,則進行遍歷
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {

            // 如果引數的hash值和key與first的hash值和key相同,則返回first
            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;
    }

7,keySet()和entrySet()方法,

  • Map本身沒有迭代器,若要迭代,需要將Map轉化為Set集合,使用Set集合的迭代器進行迭代。如下圖,這兩個方法均返回一個Set集合物件,分別將key和(key,value)作為整體構成一個Set。

這裡寫圖片描述

方法原始碼:

    public Set<K> keySet() {
        Set<K> ks = keySet;
        if (ks == null) {
            ks = new KeySet(); // KetSet是個內部類,繼承自Set體系,所以有迭代器方法
            keySet = ks;
        }
        return ks;
    }

    public Set<Map.Entry<K,V>> entrySet() {
        Set<Map.Entry<K,V>> es; 

        // EntrySet也是內部類,同樣繼承自Set體系,有增強的迭代器方法
        return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
    }

這兩個方法常見用法:

package com.jimmy.map;

import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

public class MapDemo1 {
    public static void main(String[] args) {

        Map<String, String> map = new HashMap<>();
        System.out.println(map.put("name1", "jimmy")); 
        System.out.println(map.put("name2", "jimmy")); 
        System.out.println(map.put("name3", "jimmy")); 

        Set<String> keySet = map.keySet();  // keySet()方法返回map集合中所有key的set集合
        for (String eachKey : keySet) {
            System.out.println(eachKey+"::"+map.get(eachKey));
        }

        System.out.println("------------------");
        // entrySet()方法返回map集合中(k,v)對的set集合
        Set<Entry<String, String>> entrySet = map.entrySet();
        for (Entry<String, String> eachMap : entrySet) {
            System.out.println(eachMap.getKey()+"--"+eachMap.getValue());
        }
    }
}

總結

map結構相對複雜,其實現類按照不同的資料結構(雜湊表結構或者樹結構)來儲存元素,應該在學習了相應資料結構的基礎上再學習相應的集合實現。