1. 程式人生 > >Java原始碼分析——java.util工具包解析(三)——HashMap、TreeMap、LinkedHashMap、Hashtable類解析

Java原始碼分析——java.util工具包解析(三)——HashMap、TreeMap、LinkedHashMap、Hashtable類解析

    Map,中文名字對映,它儲存了鍵-值對的一對一的關係形式,並用雜湊值來作為存貯的索引依據,在查詢、插入以及刪除時的時間複雜度都為O(1),是一種在程式中用的最多的幾種資料結構。Java在java.util工具包中實現了Map介面,來作為各大Map實現類的規範,其中主要的Map實現類有三個,分別是:HashMap、TreeMap以及LinkedHashMap類,三者的關係如圖所示:
在這裡插入圖片描述

    先從Map介面說起,討論其Java的Map規範以及實現的定義,在Map介面內,還定義了另外一個內部介面,該介面用來存貯鍵值對的,其原始碼如下:

interface Entry<K,V> {
        K getKey();
        V getValue();
        V setValue(V value);
        boolean equals(Object o);
        int hashCode();
        public static <K extends Comparable<? super K>, V> Comparator<Map.Entry<K,V>> comparingByKey() {
            return
(Comparator<Map.Entry<K, V>> & Serializable) (c1, c2) -> c1.getKey().compareTo(c2.getKey()); } public static <K, V extends Comparable<? super V>> Comparator<Map.Entry<K,V>> comparingByValue() { return (Comparator<
Map.Entry<K, V>> & Serializable) (c1, c2) -> c1.getValue().compareTo(c2.getValue()); } //...... }

    從該內部介面的實現來看,它在比較鍵或值時,需要的實現了Comparable與Comparator介面的類,以此來比較,也就是說,我們在用Map介面實現類存貯自定義的物件時,最好實現這兩個介面。通時,在定義增刪查改的基礎上,加了檢查的一項:

//檢查Map裡缺少值的鍵,並將新值賦給該鍵
default V computeIfAbsent(K key,
            Function<? super K, ? extends V> mappingFunction) 
//檢查Map裡包含值的鍵,並把新值賦給鍵
    default V computeIfPresent(K key,
            BiFunction<? super K, ? super V, ? extends V> remappingFunction) ;
 //這個方法是前兩者的綜合
    default V compute(K key,
            BiFunction<? super K, ? super V, ? extends V> remappingFunction) ;

HashMap

    HashMap繼承AbstractMap類,實現了Map、 Cloneable,、Serializable介面:

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

    HashMap提供了4個構造器用於初始化:

//提供容量與負載因子的構造器
public HashMap(int initialCapacity, float loadFactor) {
        //......
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
    }
	//提供容量的構造器
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
	//預設構造器
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; 
    }
	//提供自Map的gouzaoqi構造器
    public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }

    其中的loadFactor指的是負載因子,有的稱載入因子,是用來當雜湊桶容量不夠時,擴充套件threshold 大小使用的,雜湊桶是用來存放鍵值對的一個數組,其擴充套件的threshold 的大小是原來雜湊桶容量 的大小乘以負載因子的。threshold 就是個門檻,是來判定什麼時候需要擴充套件雜湊桶的大小的,當雜湊桶中存貯的元素多於threshold 值時,則需要進行桶容量的擴充套件,以此來優化hashMap的效率。其初始預設的桶容量、負載因子如下:

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
static final float DEFAULT_LOAD_FACTOR = 0.75f;

//開始擴充套件桶容量的判斷:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
                   //......
if (++size > threshold)
            resize();
            //......
}

    那麼,hashMap怎麼進行存貯鍵值對,而讓其查詢的時間複雜度為O(1)呢?是利用每個鍵的雜湊值,在進行存貯的時候,先計算鍵的雜湊值,通過雜湊值來確定索引值,這樣就可以完成時間複雜度為O(1)的操作:

static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

    然而這樣會引發一個危險的操作,因為雜湊值是不確定的,桶的容量是有限的,怎麼防止通過雜湊值得到的索引值不越界,是通過位運算子來操作的:

iedex = (length - 1) & hash;

    length代表著桶的容量,hash代表著鍵的雜湊值,從桶容量的擴充套件可以看出,桶的容量始終是2n,減去1用二進位制表示則是後n位全是1,前面都是0,這樣就可以成功的儲存後n位的雜湊值,從而讓它不產生越界的索引值。還有一個問題是,當鍵通過該方法產生的索引值一樣,怎麼解決這個鍵衝突,hashMap是採用經典的連結串列法來解決的,當產生的索引值上有資料時,便將該鍵鏈至當前索引值的鍵的next屬性。在hashMap類中的Node節點用來儲存鍵值對的資料:

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

    hashMap儲存資料時,首先會將鍵值對插入桶中,然後檢查容量是否到了threshold 門檻,到了則進行擴容,擴容的時候需要注意的是插入到新的桶中的元素處理,與剛開始插入舊桶裡一樣,計算鍵的雜湊值並插入桶中:

final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        //桶中有資料
        if (oldCap > 0) {
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            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;
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    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;
    }

    對於插入到桶中的元素,其操作策略是先判斷桶的容量夠不夠,再進行插入操作:

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 {
            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)
                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
                            treeifyBin(tab, hash);
                        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;
            }
        }
        ++modCount;
        //如果存貯的數量達到門檻
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

    其中注意的是:

static final int TREEIFY_THRESHOLD = 8;
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
     treeifyBin(tab, hash);

    這句話的意思是如果某個桶中的元素超過了8個,那麼就不再用連結串列法來解決鍵衝突,而是將整個連結串列轉化成一個平衡二叉樹來提升效率。樹的節點定義如下:

 static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
        TreeNode<K,V> parent;  // red-black tree links
        TreeNode<K,V> left;
        TreeNode<K,V> right;
        TreeNode<K,V> prev;    // needed to unlink next upon deletion
        boolean red;
}

    HashMap裡的平衡二叉樹是由紅黑樹來實現的,限於篇幅原因就不再講解樹的知識(就是懶,23333)。

TreeMap

    TreeMap類,如其名,是一個以樹結構來實現Map的一個類,其中節點的定義如下:

static final class Entry<K,V> implements Map.Entry<K,V> {
        K key;
        V value;
        Entry<K,V> left;
        Entry<K,V> right;
        Entry<K,V> parent;
        boolean color = BLACK;
}

    TreeMap是以紅黑樹來做存貯結構的,因為TreeMap是有序的,所以它存貯的物件都是必須實現Comparable或者Comparator介面的,以便於排序,它有4個構造方法:

	//預設的構造方法
	public TreeMap() {
        comparator = null;
    }
    //接受外部的比較器
    public TreeMap(Comparator<? super K> comparator) {
        this.comparator = comparator;
    }
	//接受一個Map實現類
    public TreeMap(Map<? extends K, ? extends V> m) {
        comparator = null;
        putAll(m);
    }
	//接受一個SortedMap實現類
    public TreeMap(SortedMap<K, ? extends V> m) {
        comparator = m.comparator();
        try {
            buildFromSorted(m.size(), m.entrySet().iterator(), null, null);
        } catch (java.io.IOException cannotHappen) {
        } catch (ClassNotFoundException cannotHappen) {
        }
    }

    預設是不用外部的比較器的,因為傳進來的的物件是實現了Comparable或者Comparator介面。它的插入方法:

public V put(K key, V value) 
{ 
    // 先以 t 儲存連結串列的 root 節點
    Entry<K,V> t = root; 
    // 如果 t==null,表明是一個空連結串列,即該 TreeMap 裡沒有任何 Entry 
    if (t == null) 
    { 
        // 將新的 key-value 建立一個 Entry,並將該 Entry 作為 root 
        root = new Entry<K,V>(key, value, null); 
        // 設定該 Map 集合的 size 為 1,代表包含一個 Entry 
        size = 1; 
        // 記錄修改次數為 1 
        modCount++; 
        return null; 
    } 
    int cmp; 
    Entry<K,V> parent; 
    Comparator<? super K> cpr = comparator; 
    // 如果比較器 cpr 不為 null,即表明採用定製排序
    if (cpr != null) 
    { 
        do { 
            // 使用 parent 上次迴圈後的 t 所引用的 Entry 
            parent = t; 
            // 拿新插入 key 和 t 的 key 進行比較
            cmp = cpr.compare(key, t.key); 
            // 如果新插入的 key 小於 t 的 key,t 等於 t 的左邊節點
            if (cmp < 0) 
                t = t.left; 
            // 如果新插入的 key 大於 t 的 key,t 等於 t 的右邊節點
            else if (cmp > 0) 
                t = t.right; 
            // 如果兩個 key 相等,新的 value 覆蓋原有的 value,
            // 並返回原有的 value 
            else 
                return t.setValue(value); 
        } while (t != null); 
    } 
    else 
    { 
        if (key == null) 
            throw new NullPointerException(); 
        Comparable<? 
            
           

相關推薦

Java原始碼分析——java.util工具解析——HashMapTreeMapLinkedHashMapHashtable解析

    Map,中文名字對映,它儲存了鍵-值對的一對一的關係形式,並用雜湊值來作為存貯的索引依據,在查詢、插入以及刪除時的時間複雜度都為O(1),是一種在程式中用的最多的幾種資料結構。Java在java.util工具包中實現了Map介面,來作為各大

spark mllib原始碼分析之隨機森林(Random Forest)

6. 隨機森林訓練 6.1. 資料結構 6.1.1. Node 樹中的每個節點是一個Node結構 class Node @Since("1.2.0") ( @Since("1.0.0") val id: Int, @S

Java併發程式設計——執行緒池的使用執行緒池執行任務取消任務

一、執行緒池執行Runnable任務 executor.execute(runnable) executor.execute(new Runnable() { @Override public void run(

Java原始碼分析——java.util工具解析——UUIDBase64內建觀察者模式Observer介面EventListenerRandomAccess

UUID     關於UUID,我們需要知道它最重要的一點,就是它會生成全地球唯一的一個id,它可以作為資料庫的主鍵存在,標識各個元組。 UUID保證對在同一時空中的所有機器都是唯一的,利用機器的當前日期和時間、時鐘序列、全域性唯一的IEEE機

Java原始碼分析——java.util工具解析——四大引用型別以及WeakHashMap解析

    WeakHashMap是Map的一種很獨特的實現,從它的名字可以看出,它是存貯弱引用的對映的,先來複習一下Java中的四大引用型別: 強引用:我們使用的大部分引用實際上都是強引用,這是使用最普遍的引用。強引用的物件垃圾回收器絕不

Java原始碼分析——java.util工具解析——HashSetTreeSetLinkedHashSet解析

    Set,即集合,與數學上的定義一樣,集合具有三個特點: 無序性:一個集合中,每個元素的地位都是相同的,元素之間是無序的。 互異性:一個集合中,任何兩個元素都認為是不相同的,即每個元素只能出現一次。 確定性:給定一個集

Java原始碼分析——java.util工具解析——ArrayListLinkedListVector解析

    Java中,List列表類與Set集合類的共同源頭是Collection介面,而Collection的父介面是Iterable介面,在Collection介面下又實現了三個常用的介面以及一個抽象方法,分別為Queue介面、List介面、Se

Java原始碼分析——java.lang.reflect反射解析() 動態代理ProxyWeakCache

    代理模式是一個經常被各種框架使用的模式,比如Spring AOP、Mybatis中就經常用到,當一個類訪問另外一個類困難時,可通過一個代理類來間接訪問,在Java中,為了保證程式的簡單性,代理類與目標類需要實現相同的介面。也就是說代理模式起

Java原始碼分析——java.lang.reflect反射解析(二) Array,陣列的建立

    在Java中,引用型別中有那麼幾個特殊的類,Object類是所有類的起源、Class類定義所有類的抽象與行為、ClassLoader類實現了類從.class檔案中載入進jvm,而Array陣列類,則實現了陣列手動的建立。  &

Java原始碼分析——java.lang.reflect反射解析(一) AccessibleObjectReflectionFactoryFiledMethodConstructor

    Java的反射機制一直是被人稱讚的,它的定義是:程式在執行中時,對於任意一個類,都能夠知道這個類的所有屬性和方法;對於任意一個物件,都能夠呼叫它的任意一個方法和屬性。簡單的來說就是可以通過Java的反射機制知道自己想知道的類的一切資訊。

Java原始碼分析--java.util.ArrayList

序列化問題 /** 使用transient關鍵字,即使繼承了Serializable,也不會序列化 * 一般情況下 elementData.capacity < element.size,我們並不希望將空的元素也序列化 * ps: 可以看

Java原始碼分析--java.util.Hashtable

都說Hashtable是執行緒安全的,我們看一看Hashtable與HashMap有那些不同。 定址方式 int hash = hash(key); int index = (hash & 0x7FFFFFFF) % tab.length; //

java集合原始碼解析--List

今天給大家帶來有序集合的介面List,我想也應該是大家在工作中用的比較多的 先來看看介面的定義: public interface List<E> extends Collection<E>可以看出介面List直接繼承於介面Collection,並且一樣使用了

java原始碼分析——java.lang.Object

所有的java類均繼承Object類, package java.lang; public class Object { public Object() { } private static native void registerNative

Java併發之ThreadPoolExecutor原始碼解析

Worker 先前,筆者講解到ThreadPoolExecutor.addWorker(Runnable firstTask, boolean core),在這個方法中工作執行緒可能建立成功,也可能建立失敗,具體視執行緒池的邊界條件,以及當前記憶體情況而定。 那麼,如果執行緒池當前的狀態,是允許建立Worke

死磕java concurrent系列基於ReentrantLock理解AQS的條件佇列

基於Codition分析AQS的條件佇列 前言 上一篇我們講了AQS中的同步佇列佇列,現在我們研究一下條件佇列。 在java中最常見的加鎖方式就是synchorinzed和Reentrantlock,我們都說Reentrantlock比synchorinzed更加靈活,其實就靈活在Reentrantlock中

Java原始碼】基於陣列實現的ArrayList

    眾所周知,Java中ArrayList是基於陣列實現的     咱們先看其基本屬性: private static final int DEFAULT_CAPACITY = 10; private static final Object[

Java單元測試工具:JUnit4——JUnit詳解之執行流程及常用註解

(三)執行流程及常用註解         這篇筆記記錄JUnit測試類執行時,類中方法的執行順序;以及JUnit中常用的註解。 1.JUnit的執行流程 1.1 新建測試類        

Java基礎HashMap原始碼剖析

關於HashMap,在網上看到了不少的好文章,萬花叢中過的過程中,我自己卻有了很大的感慨。 關於HashMap很多好的文章介紹,都是關注於HashMap的一個點,進行展開介紹。簡簡單單幾張圖,幾行文字

java執行緒深度解析——併發模型Future

Main:啟動系統,呼叫Client發出請求; Client:返回Data物件,理解返回FutureData,並開啟ClientThread執行緒裝配RealData; Data:返回資料的介面; FutureData:Future資料,構造很快,但是是一個虛擬的資料,需要裝配RealData; RealD