java提供的map

hashMap

先說明hashmap是線程不安全的
參考此鏈接:http://www.importnew.com/20386.html
hashMap內部的實現比list複雜好多,內部是有數組加鏈表的形式存儲的,而put的鍵值的hashCode值的低位計算值爲其存儲在數組的下標,而數組裏指向的是一個鏈表,鏈表中存的是真正的數據,這裏的hashCode低位計算值可能會相等,相等時就會遍歷數組指向的這個鏈表的各個鍵值,判斷鍵值是不是一樣,一樣就覆蓋,不一樣就要添加到此鏈表中,每次擴爲原大小的2倍,這裏爲什麼兩倍這麼多呢,因爲在hashmap中使用了很多的位運算來提高效率,而二進制中升/進一位相當於十進制的乘2.

//put元素的具體實現
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)//判斷若數組爲空或長度爲0時會進行擴容,這裏的擴容是初始化長度,在初始化HashMap時不會初始化數組
            n = (tab = resize()).length;//保存擴容後的長度
        if ((p = tab[i = (n - 1) & hash]) == null)//如果插入數據的hashCode下標對應的數據爲空,會對其初始化新的Node,Node是鏈表中的節點
            tab[i] = newNode(hash, key, value, null);//在初始化Node時,將其要插入的值存入其中,然後數組對應位置執行這個鏈表節點
        else {//來到這裏的話就說明要插入的數組的對應下標上已經有節點/鏈表存在了
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))//判斷指向的節點的鍵值與要插入的鍵值是否一樣,一樣就覆蓋其值,這裏是先放到了e中
                e = p;
            else if (p instanceof TreeNode)//這是jdk8引入的紅黑樹,這裏的操作的目的是放入到紅黑樹中,而這裏是獲取樹中存在的鍵值對象,如果樹中沒有這個對象則會創建並返回null,有的話就返回這個對象用於後面覆蓋
                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);//創建一個節點,內容爲要插入的數據,並將最後一個節點的next指向新的節點
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);//這裏是jdk8的優化,在鏈表長度大於8是會將鏈表轉換成紅黑樹
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))//存在一樣的鍵值,退出遍歷,此時的e就是這個存在的鍵值的引用
                        break;
                    p = e;//用p繼續遍歷
                }
            }
            if (e != null) { // existing mapping for key//這意思是鏈表中有一樣的鍵值,需要進行覆蓋
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;//覆蓋原值
                afterNodeAccess(e);//hashMap中這個是空實現
                return oldValue;//返回被覆蓋的值,這裏因爲是覆蓋所以直接返回不用後面的長度判斷及增加
            }
        }
        ++modCount;
        if (++size > threshold)//長度+1,如果加了之後大於當前數組的長度,就進行擴容
            resize();//擴容
        afterNodeInsertion(evict);//hashMap中這個是空實現
        return null;
    }

計算存儲的下標即擴容

因爲默認的長度爲16,在2進制中爲4位數,所以在一開始會對鍵值的hashCode進行位運算獲取低4位作爲數組的下標,在數組爲空或者長度爲0(前兩個會進行初始化)或者在插入後的長度大於數組的長度會進行擴容,擴容會對hashCode的低4位擴成低5位,而在十進制中就是2倍的意思,在擴大後會將原數組添加到新數組中,在添加的過程中會更新下標,因爲原下標是4位計算的,要將其改爲5位的計算.
注:在jdk7即之前會重新計算hashCode,但在jdk8進行了優化,原來的元素不重新計算hashcode,而是判斷其高一位的二進制是1還是0,是1的話就加上該位的值來得到新的下標

//擴容的具體實現
final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;//獲取原長度,若爲空則默認爲0
        int oldThr = threshold;//hashCode計算閾值,相當於計算的hashCode上限
        int newCap, newThr = 0;
        if (oldCap > 0) {//原數組長度大於0,也就是初始化過了
            if (oldCap >= MAXIMUM_CAPACITY) {//長度已經是最大值了,不進行擴容
                threshold = Integer.MAX_VALUE;//hashCode計算位數爲全部,對象的hashCode是通過地址算出來的,正常不會出現相等
                return oldTab;
            }
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)//新的長度小於最大值,原長度大於默認值
                newThr = oldThr << 1; // double threshold//提高hashCode計算位數
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;//數組長度小於0,計算閾值大於0的情況,將長度擴到計算閾值大小
        else {               // zero initial threshold signifies using defaults//初始化
            newCap = DEFAULT_INITIAL_CAPACITY;//默認大小16
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//默認計算閾值爲長度的0.75倍
        }
        if (newThr == 0) {//原數組長度大於0且計算閾值等於0或原數組長度等於0且閾值大於0的情況下
            float ft = (float)newCap * loadFactor;//計算新的閾值,loadFactor默認爲0.75,在初始化時可以設置
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);//新閾值和新數組長度都沒有超過上限就是用ft,否者就用int最大值
        }
        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)//這裏是這個下標上只有一個節點,就把這個節點直接放到新的下標上,就像原來低4位計算沒有重複的key那現在擴容到低5爲計算也不會有重複的key值
                        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;
//如上邊的注所說,jdk8在這裏進行了優化,因爲每次擴容2倍即2進制進一位,而鏈表中的鍵值的hashCode的這一位,如4位擴到5位,就是5位爲0的會在一個鏈表上,爲1的會在另一個鏈表上,這樣就生成了兩個鏈表,這兩個鏈表中的各節點的鍵值的hashcode低5爲時相等的,然後將這兩個鏈表放到新數組對應的地方,就完成的鏈表的新鍵值的計算與遷移
                        do {
                            next = e.next;
                            if ((e.hash & oldCap) == 0) {//擴容的那位數爲0
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {//擴容那位數不爲0
                                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;//返回數組
    }

獲取元素

hashmap在獲取元素的時候以獲取的鍵值的hashCode作爲數組下標(這裏也會做低位運算處理),找到對應的節點,再看看第一個節點鍵值是不是要找的,不是就開始遍歷這個鏈表,找不到返回null

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 &&
            (first = tab[(n - 1) & hash]) != null) {//存儲的數組不爲空,且長度大於0,且數組對應位置上的節點不爲空
            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;
    }

IdentityHashMap

與HashMap一樣,只是其在相等的判斷使用了==,即需要對象地址相等纔會相等

LinkedHashMap

參考鏈接:https://blog.csdn.net/justloveyou_/article/details/71713781
https://blog.csdn.net/ns_code/article/details/37867985
以HashMap爲基礎,將其中的所有節點用雙向鏈表的形式連在一起

ConcurrentHashMap

參考鏈接:https://www.cnblogs.com/leesf456/p/5453341.html
線程安全的HashMap,在線程安全的情況下保存的較好的性能.
先看其構造函數,

ConcurrentHashMap()
ConcurrentHashMap(int initialCapacity) 
ConcurrentHashMap(Map<? extends K, ? extends V> m) 
ConcurrentHashMap(int initialCapacity, float loadFactor)
 ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel)

構造函數可不傳參也可傳入參數"initialCapacity"來設置其初始大小,默認爲16的長度,傳入參數"loadFactor"設置其負載因數,此值爲定義map的滿的程度,默認爲0.75

其獲取元素的方法與hashMap類似,且沒有加鎖,那說好的線程安全在哪裏呢
再來看看插入元素
插入元素的具體實現,部分實現與Hashmap很像

final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException();
        int hash = spread(key.hashCode());
        int binCount = 0;
        for (Node<K,V>[] tab = table;;) {//死循環
            Node<K,V> f; int n, i, fh;
            if (tab == null || (n = tab.length) == 0)//數組爲空
                tab = initTable();//初始化數組
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {//獲取tab數組下標爲 (n - 1) & hash的節點,爲空時
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))//tab的i下標是否爲null,爲null就用new Node<K,V>(hash, key, value, null)替換
                    break;                   // no lock when adding to empty bin
            }
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            else {
                V oldVal = null;
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                        if (fh >= 0) {
                            binCount = 1;
                            for (Node<K,V> e = f;; ++binCount) {
                                K ek;
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
                                    oldVal = e.val;
                                    if (!onlyIfAbsent)
                                        e.val = value;
                                    break;
                                }
                                Node<K,V> pred = e;
                                if ((e = e.next) == null) {
                                    pred.next = new Node<K,V>(hash, key,
                                                              value, null);
                                    break;
                                }
                            }
                        }
                        else if (f instanceof TreeBin) {
                            Node<K,V> p;
                            binCount = 2;
                            if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                           value)) != null) {
                                oldVal = p.val;
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                    }
                }
                if (binCount != 0) {
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        addCount(1L, binCount);
        return null;
    }

SparseArray

此集合使用了兩個數組來存儲key和value,其中key限制爲int類型.初始默認長度10,每次擴容2倍,使用System.arraycopy()方法複製到新數組上
在插入元素的時候,會使用二分法在key數組中搜索key,若搜索到了會將其值覆蓋,若搜索不到會在二分法最後判斷的位置(其實也就是key數組進行排序後,新key的位置)插入key值,並在value數組對應的下標位置上插入新value值.
在取值時,會通過key值進行二分法搜索,如搜索成功會返回value數組中對應下標的value.
SparseArray有兩個優勢:(可能不止,大概說一下)
1.因爲使用數組存儲,佔用的空間會比較少;
2.因爲使用二分法存取值,所以存取值的效率會比較高;(這個是相對來說的,畢竟二分法也有最好最壞的情況)

ArrayMap

與SparseArray存儲的形式類似,都爲兩個數組,但其不限制key值的類型,一個數組用於存儲key的hashCode(hashcode數組),另一個數組用於存儲key和value值(元素數組),存儲的格式爲一個鍵一個值,初始默認長度10,每次擴容2倍,使用System.arraycopy()方法複製到新數組上.
在插入元素時會獲取鍵值的hashCode,並使用二分法查找hashcode數組,如查找到有對應的hashcode會去覆蓋元素數組的鍵和值,若找不到則在二分法最後搜索的位置進行插入操作,並在元素數組中插入對應的元素.
在取值的時候會獲取使用二分法在hashcode數組中查找對應的位置,有則返回元素數組對應的值.
ArrayMap的優勢與SparseArray類似,但其多了一個hashcode數組的開銷,所以內存理論上是SparseArray的1.5倍,但正常情況下會不止1.5倍

WeakHashMap

內部邏輯與HashMap基本一樣,相對於HashMap,其對鍵值的引用時弱引用,且在鍵值被GC時,將其加入ReferenceQueue(此隊列記錄了被GC的對象),在下次操作(size.get.put等操作)時會進行清除被回收對象,從源碼中看出,其操作是在返回值前進行清除的,且清除時進行了加鎖保護.
這裏需要注意的是,這裏鍵值被GC時不會主動刪除這個鍵值對,而是存在隊列中,等待下次操作才進行刪除,如果爲進行操作,這不會進行刪除,此時就像一個HashMap存在,而且在鍵值存在強引用時,此map就相當於一個普通的HashMap了.

//進行清除時,會調用此方法
private void expungeStaleEntries() {
//在回收的隊列中獲取被回收的鍵值
        for (Object x; (x = queue.poll()) != null; ) {
            synchronized (queue) {//加鎖
                @SuppressWarnings("unchecked")
                    Entry<K,V> e = (Entry<K,V>) x;
                int i = indexFor(e.hash, table.length);

                Entry<K,V> prev = table[i];
                Entry<K,V> p = prev;
                while (p != null) {
                    Entry<K,V> next = p.next;
                    if (p == e) {
                        if (prev == e)
                            table[i] = next;
                        else
                            prev.next = next;
                        // Must not null out e.next;
                        // stale entries may be in use by a HashIterator
                        e.value = null; // Help GC//置空
                        size--;
                        break;
                    }
                    prev = p;
                    p = next;
                }
            }
        }
    }

小結

java提供了很多的map,而且大部分操作都針對其痛點進行了優化,總的來說,看完這些map中的設計後,還是覺得自己還是太嫩了,雖然有些可能寫錯了,希望大家指出糾正

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章