1. 程式人生 > >分散式Java應用之集合框架篇(下)

分散式Java應用之集合框架篇(下)

前言:分散式Java應用之集合框架篇(上)一文中,從整體上對Java分散式應用中的集合框架進行了介紹,以及對於其中的List家族給出了原始碼分析;本文將繼續介紹集合框架中的Set家族和Map家族,其實Set家族和Map家族之間是有著很深的淵源,在本文的後續內容中,將從兩大家族的成員的關鍵實現進行原始碼層面的分析!

首先,還是給出集合框架的整體類圖關係,通過類圖展開下面的介紹; 集合框架類圖

對於Collection介面的子介面Set來說,介面的實現類同樣是存放一系列有序的元素,且這些元素均為一個個單體物件,相比於List家族中的實現類而言,最大的區別在於List介面的實現類可以存放重複的元素(即元素之間通過==或equals判斷為true),而Set介面的實現類不可以存放重複的元素;

我們先看一下,Set家族中常用的實現類HashSet,下面是HashSet類的底層實現原始碼:

 	//底層元素的儲存結構
    private transient HashMap<E,Object> map;
    /**
     * 構造一個新的空集合,實際上就是構造了一個初始容量為16,負載因子為0.75的HashMap物件作為底層儲存
     */
    public HashSet() {
        map = new HashMap<>();
    }
    /**
     * 構造一個新的空集合,實際上就是構造了一個初始容量為指定容量,負載因子為指定負載因子的HashMap物件作為底層儲存
     * @param      initialCapacity   初始化容量
     * @param      loadFactor       負載因子
     * @throws     IllegalArgumentException 如果初始化容量引數為負數,或負載因子不為正數,那麼將會丟擲異常
     */
public HashSet(int initialCapacity, float loadFactor) { map = new HashMap<>(initialCapacity, loadFactor); } /** * 構造一個新的空集合,實際上就是構造了一個初始容量為指定容量,負載因子為預設負載因子0.75的HashMap物件作為底層儲存 * @param initialCapacity 初始化容量 * @throws IllegalArgumentException 如果初始化容量為負數,則丟擲異常 */
public HashSet(int initialCapacity) { map = new HashMap<>(initialCapacity); }

從上述原始碼可以看出,HashSet底層採用HashMap實現,因此想要理解HashSet的具體實現原理,首先需要搞清楚HashMap的底層實現原理,而HashMap又是Map家族的成員,這就正好印證了本文開頭處給出了言論:Set家族與Map家族是有很深的淵源的,底層都是相似的,淵源當然不會小!

那麼,我們就將Set家族放下,先看一看Map家族的核心成員及其實現原理;通過集合框架圖可以看出,其實研究Map也就是研究HashMap,下面對HashMap類的底層結構以及常用操作的實現進行原始碼分析;

HashMap底層儲存結構:

	/**
     * 採用陣列實現的底層儲存結構,陣列元素型別是內部類Node,陣列在有必要的時候,
     * 會進行擴容;當為陣列分配記憶體時,陣列的大小始終保持為2的整數次冪(JDK1.8中的HashMap
     * 相關優化的基礎)
     */
    transient Node<K,V>[] table;

從這裡可以看出,HashMap底層的基本資料結構是陣列,而陣列的元素型別是Node<K,V>,下面是Node<K,V>的原始碼:

    /**
     * 基本的hash節點類,HashMap大多數元素都是以這種結構進行包裝存放的
     */
    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; }
        //final修飾的方法,不可以被重寫,保證hashcode求法的一致性
        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;
        }
    }

從原始碼可以看出Node<K,V>實現了Map介面中的子介面Entry<K,V>,關鍵看一下Node<K,V>的欄位,可以看到,每一個Node<K,V>物件都有四個欄位,分別是hash、key、value以及next;hash、key、value不用說,直接看next,next屬性是Node<K,V>型別的,這與連結串列是相同的,那麼可以斷定Node<K,V>是一個連結串列節點類;

小結: HashMap底層採用的是陣列+連結串列實現的儲存結構,每一個數組元素都可以看做是一個連結串列;

下面看一下HashMap物件的常用操作:

新增鍵值對:put(K key, V value)

    /**
     * 計算鍵的雜湊值,然後將雜湊值的高16位與低16位按位與,得到的結果作為最後參與雜湊桶定位的雜湊值
     * 這樣可以保證當陣列長度較小時,高位能夠參與雜湊桶的定位操作,這是在速度、效率以及質量方面對定位
     * 操作的一種折中處理
     */
    static final int hash(Object key) {
        int h;
        //如果key為null,則預設在陣列的第一個桶中進行插入
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
    /**
     * 初始化陣列或者擴容2倍,如果為null,則按照threshold的值分配陣列;
     * 否則,擴容兩倍,優化向新陣列中移動元素時的操作
     * @return 新的陣列
     */
    final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        //判斷當前陣列是否為null,如果為null,說明還沒有初始化陣列
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        //如果當前陣列的容量大於0,說明需要進行擴容
        if (oldCap > 0) {
            //如果當前陣列的容量已經超過MAXIMUM_CAPACITY,那麼不可以進一步擴容,將threshold賦值為最大上限值
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            //如果當前陣列的容量沒有超過MAXIMUM_CAPACITY,那麼就擴大兩倍,同時將新陣列對應的threshold也擴大兩倍
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                    oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        //如果當前陣列的容量不大於0,說明需要初始化,如果當前陣列的threshold大於0,那麼threshold的值就是新的陣列容量
        else if (oldThr > 0) 
            // initial capacity was placed in threshold
            newCap = oldThr;
        else {              
            // 否則表示使用預設的初始化容量作為陣列的新容量
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        //如果當前threshold為0,那麼就會計算新的threshold
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                    (int)ft : Integer.MAX_VALUE);
        }
        //更新threshold
        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;
                    else if (e instanceof TreeNode)
                        ((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;
                            }
                            //移動oldCap個位置
                            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;
    }
    // 構造一個常規節點(非樹節點)
    Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
        return new Node<>(hash, key, value, next);
    }
    /**
     * 將以key和value對映的鍵值對插入HashMap中,如果HashMap中已經存在以key為鍵的鍵值對
     * 那麼就以value替換已有鍵值對的值
     * @return 如果已存在以key為鍵的鍵值對,那麼就返回舊值;否則,返回null
     */
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
    /**
     * 實現Map介面中的put方法
     * @param hash 鍵的雜湊值
     * @param key 鍵
     * @param value 值
     * @param onlyIfAbsent 如果為true,那麼不會修改已存在的鍵值對
     * @param evict 如果為false,那麼陣列處於建立模式
     * @return 返回舊值或null
     */
    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屬性是否為null或者table屬性的長度是否為0;如果是則呼叫resize()對table進行初始化
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        //利用陣列長度減一的值與hash進行按位與,來定位鍵值對應該插入的桶位置
        //判斷該位置處的陣列元素是否為null;如果為null,則構造一個新的Node物件放到陣列的該位置上
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            //如果不為null,發生雜湊衝突
            Node<K,V> e; K k;
            //如果陣列元素的雜湊值與待插入鍵值對的雜湊值相等且鍵相等,則找到衝突元素,更新該鍵值對的值
            if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            //如果陣列元素與待插入鍵值對沒有發生衝突,則判斷陣列元素的型別
            else if (p instanceof TreeNode)
                //陣列元素為TreeNode型別,呼叫TreeNode的putTreeVal方法將鍵值對進行插入處理
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                //陣列元素為Node型別,則按照連結串列結構進行查詢插入
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        //如果到了連結串列尾部都沒有找到相等的鍵值對,就將鍵值對插入連結串列尾部
                        p.next = newNode(hash, key, value, null);
                        //如果當前位置的桶中的節點數超過TREEIFY_THRESHOLD,就將連結串列結構轉換為紅黑樹結構,結束查詢
                        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,說明查詢到相等的鍵值對,那麼替換已有的鍵值對中的值
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        //結構調整次數加一
        ++modCount;
        //如果插入後的鍵值對數量超過threshold,則進行擴容操作
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

小結: 對於鍵值對的put操作,需要注意的地方有:雜湊值的求取、雜湊桶定位、連結串列與紅黑樹轉換、擴容以及擴容之後鍵值對的移動(保持原有連結串列的順序),key為null元素的插入位置、鍵值對在連結串列尾部插入等;

put操作的流程梳理: 首先,判斷當前陣列是否還沒有初始化,沒有初始化先初始化,如果初始化過了,那麼進行雜湊值的求取以及雜湊桶的定位;定位之後,根據陣列元素是否為null,進行分情況處理;如果元素為null,直接插入鍵值對,如果不為null,則判斷是否鍵相等,相等替換舊值,如果不相等,進入下一步;判斷陣列元素的型別,如果是紅黑樹,則按照紅黑樹的方法進行插入,如果是連結串列則按照連結串列的方式進行插入,插入的過程中需要判斷是否需要進行連結串列與紅黑樹之間的轉換;插入之後,判斷是否需要進行擴容,如果需要擴容,則進行擴容;否則,方法結束

類似的常用操作還有刪除鍵值對:remove(Object key),獲取鍵值對:get(Object key)等等,但是核心的東西在上面的方法中已經給出,不同的方法關鍵的東西差不多,所以這裡不再詳述;

通過分析我們對HashMap的關鍵實現已經有了一個大概的理解,下面回過頭來看一下文章開頭提到的HashSet,現在看HashSet就可以知道,底層採用的是HashMap來實現的,放入HashSet中的元素都是作為鍵值對的key放入底層的HashMap例項中的,而鍵值對中的value對於HashSet來說是無關緊要的,所以每一個鍵值對都會共用一個Object物件作為value;

既然是基於HashMap實現HashSet的,那麼HashSet中的常用操作也就利用HashMap中對應的方法來實現,因此這裡就不給出原始碼分析,相信理解了HashMap的操作就可以類推出HashSet的操作是如何實現的!

以上就是Set家族和Map家族中的最常用的實現類成員,通過上述的分析,對於這兩個家族有了比較深的認識和理解,其實對於HashMap其實還有一個比較關鍵的東西尚未提及,比如JDK1.8之前,多執行緒併發操作HashMap例項可能會出現死迴圈,導致CPU佔用率一致飆高等問題,但是由於本文只是對常用實現類的常用操作的實現進行分析,故此處不再贅述其他內容;

總結: 通過本文以及分散式Java應用之集合框架篇(上)這兩篇文章,我們對於集合框架中的常用實現類有了一定的認識,但是細細觀察可以發現,其實這些常用實現類中大都不是執行緒安全的,在多執行緒、高併發大行其道的今天,對於執行緒安全的問題十分關注,那麼,我們就必須掌握如何在併發的場景下使用這些集合類,後面將會對JDK中併發包JUC中的常用類進行分析,由於個人的理解能力有限,因此有不對的地方還希望讀者可以指出,不勝感激!