JDK 7 / 8版本下,Map的學習

HashMap(JDK 8):

爲什麼用HashMap:

  • HashMap 是一個散列桶(數組和鏈表/紅黑樹),它存儲的內容是鍵值對 key-value 映射
  • 當hash衝突越少時(鏈表長度短,紅黑樹深度小),查找的時間複雜度越接近O(1);添加,刪除,修改的時間複雜度都爲O(1)
  • HashMap 是非 synchronized,所以對 HashMap的操作很快
  • HashMap、LinkedHashMap 可以接受 null 鍵和值,TreeMap可以接收值爲null,其餘Map的K 、V 不能接受null

HashMap的工作原理:

  • HashMap是基於hashing原理的,使用 put(key, value) 存儲對象到 HashMap 中,使用 get(key) 從 HashMap 中獲取對象。
  • 當使用 put() 方法傳遞鍵和值時,先對鍵調用 hashCode() 方法,然後找到bucket中的位置來存儲Node對象。當對鍵計算得到的hash值出現衝突時,採用鏈表尾插法鏈接多個Entry,當鏈表元素超過8轉紅黑樹,當紅黑樹節點少於6轉爲鏈表。
  • 當使用get()方法時,使用鍵對象的hashcode 找到 bucket 位置。如果該 bucket 位置,存在多個Entry,開始從頭遍歷調用 keys.equals() 方法去找到鏈表中正確的節點,最終找到要找的值對象。

put與get實現原理參考

爲什麼紅黑樹與鏈表的轉換會使用8 / 6:

  1. 爲避免紅黑樹、鏈表的重複轉換,使用當樹中節點少於6時,鏈表節點大於8時,才發生轉換。當元素少於8時,使用Node佔用空間少,操作速度快。

  2. 紅黑樹的插入、刪除和遍歷的最壞時間複雜度都是log(n),TreeNode佔用更多的空間,只有當擁有足夠的元素,纔有使用紅黑樹的必要。桶中的某一位置出現hash衝突是一個隨機事件,發生概率遵循泊松分佈,桶中某一位置Entry對長度超過8的概率非常小,在源碼中有舉例說明,故選擇8。

更多關於8的解釋

HashMap擴容機制:

使用空參構造方法:

Map容量爲16,當Map中元素個數大於12(= DEFAULT_INITIAL_CAPACITY(16) * DEFAULT_LOAD_FACTOR(0.75))後,擴容爲大於16的第一個2的冪(32)

使用指定初始容量的構造方法:

Map容量爲大於等於傳入參數的第一個2的冪(如:傳入7,則Map容量爲:8;傳入8,則Map容量爲:8),當Map中元素個數大於capacity (= 初始化的容量 * DEFAULT_LOAD_FACTOR(0.75))後,擴容爲大於capacity的第一個2的冪


《阿里Java開發手冊》建議設置initialCapacity=(需要存儲的元素個數 / 負載因子(默認:0.75))+1,來減少重建hash表對性能的消耗。

更多擴容分析

爲什麼負載因子選擇0.75:

  如果使用 0.5,那麼 Map 中未利用的空間將會隨着容量的增長而增長,如果使用1,那麼在使用put方法時,將會延長等待時間。在源碼中也只是說了“默認負載因子(0.75)在時間和空間成本上提供了很好的折衷”,負載因子的選擇可以按實際項目情況來。

擴容時,需要重新計算hash嗎?

  在 JDK 8 之前需要重新計算 hash 值,JDK 8 開始,hash 值不重新計算,只是根據不同 hash 值和之前的 bucket 容量進行與運算( (e.hash & oldCap) == 0 ),如果結果爲0,元素在 bucket 中的索引值不變;如果不爲0,元素的索引值變爲 j(原索引)+OldCap(原bucket大小) 。參考下列源碼:

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;
-start-------------------------
                            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;
                        }
                    }
                }
-end-------------------------
            }
        }
        return newTab;
    }

在上面標記的一段中,(e.hash & oldCap) == 0 是爲了求得當前 hash 值與 oldCap(=2^n)二進制的最高位1,是否相同,同則結果爲1,不同結果爲0。例如:16 & 32 = 0,hash值爲16的元素在擴容後索引值不變; 17 & 16 !=0,hash值爲17的元素在擴容後索引值變爲 j +16

HashMap 如何求得 key 的 hash 值?爲什麼容量使用的是2的n次方?HashMap 是如何通過 hash 值定位某一個元素的?

JDK 8 HashMap 中的源碼:

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

//獲取某一元素在桶中的位置
int index=(n - 1) & hash;//n = tab.length ,tab爲散列桶的數組

在這裏插入圖片描述
hash 值是 32bit 的 int 類型,不能使用 hash 值的範圍(40億個位置)爲散列桶數組的容量,故需要將 hash 值縮小到適合的大小。

通過右移(>>>)和異或(^)運算,將 hash 值的高16位與低16位進行異或運算,這樣做是爲了讓高低位的信息混合,再把得到的 hash 值和散列桶容量 - 1 進行與(&)運算,得到 hash 值的最後幾位,這便是散列桶數組中的索引。

在進行 與運算 時,爲了得到在數組範圍內的索引,就必須要和 2^n(散列桶容量) - 1 (該式保證低位全爲1)做運算。


LinkedHashMap(按鍵值對添加順序排序):

public class LinkedHashMap<K,V>    extends HashMap<K,V>    implements Map<K,V>{... ...}

EnumMap:

特點:

  1. 用Enum類型作爲key的HashMap

  2. 可以根據Enum類型的key,快速定位,而不需要對key求hashcode(),故效率高

public class EnumMapTest {
    @Test
    public void enumMapTest() {
        System.out.println(Color.GREEN.ordinal() + " : " + Color.GREEN.name());
        EnumMap<Color, String> map = new EnumMap<>(Color.class);
        map.put(Color.RED, "red");
        map.put(Color.BLACK, "black");

        map.forEach((k, v) -> {
            System.out.println(k + " : " + v);
        });
        //RED : red
        //BLACK : black
    }
}

enum Color {
    RED, GREEN, YELLOW, BLACK
}

WeakHashMap:

定義:

其key是弱引用類型,在GC執行垃圾回收時,會移除弱引用對象(當將鍵值對中的key設置爲null時,其指向的對象便是弱引用對象),所以,WeakHashMap的size()方法返回值會隨着程序的運行變小,isEmpty()方法的返回值會從false變成true等。

WeakHashMap可作爲緩存使用,在Tomcat中有具體的使用。

WeakHashMap的更多介紹


TreeMap(按key排序):

TreeMap的底層是通過紅黑樹來實現的,其查找、修改、增加的時間複雜度都爲O( log n )。

TreeMap的排序是因爲實現了NavigableMap接口,而它又繼承了SortedMap接口。

定義TreeMap的key排序規則,默認是對key排升序:

/**
 * @Author Snail
 * @Describe 自定義treemap的升序與降序
 * @CreateTime 2019/8/16
 */
public class TestTreeMap {
    @Test
    public void treeMapOrder(){
        TreeMap<Integer,String> treeMap=new TreeMap(new Comparator() {
            @Override
            public int compare(Object o1, Object o2) {
                //o1-o2:表示用升序,o2-o1:爲降序
                return (Integer) o2-(Integer) o1;
            }
        });
        treeMap.put(1,"aaa");
        treeMap.put(3,"bbb");
        treeMap.put(2,"bbb");
        treeMap.put(9,"bbb");
        treeMap.put(5,"bbb");

        for(Map.Entry<Integer,String> entry:treeMap.entrySet()){
            System.out.println(entry.getKey() + "::::::::::::::::" + entry.getValue());
        }
    }
    @Test
    public void treeMapOrderBean(){
        TreeMap<Bean,String> treeMap=new TreeMap(new Comparator<Bean>() {
            @Override
            public int compare(Bean o1, Bean o2) {
                //o1-o2:表示用升序,o2-o1:爲降序

                return o2.getOrder()-o1.getOrder();
            }
        });
        treeMap.put(new Bean(2),"aaa");
        treeMap.put(new Bean(5),"aaa");
        treeMap.put(new Bean(3),"aaa");
        treeMap.put(new Bean(6),"aaa");
        treeMap.put(new Bean(10),"aaa");
        treeMap.put(new Bean(1),"aaa");

        for(Map.Entry<Bean,String> entry:treeMap.entrySet()){
            System.out.println(entry.getKey().getOrder() + "::::::::::::::::" + entry.getValue());
        }
    }
}

class Bean{
    private Integer order;

    public Bean(int i) {
        order=i;
    }

    public Integer getOrder() {
        return order;
    }

    public void setOrder(Integer order) {
        this.order = order;
    }
}

HashSet(基於HashMap實現的HashSet):

HashSet中,所有鍵值對存儲的值都是PRESENT(源碼:private static final Object PRESENT = new Object();),HashSet的add方法將值存入了key(key的存儲,滿足HashMap的put方法),這也就滿足了 Set 中元素不重複的特性。


ConcurrentHashMap(高效率的線程安全的Map容器)

JDK 1.7 下的分段式鎖

  將 HashMap 中的散列桶數組分成多段 Segment 存儲,加鎖時使用 Segment 對象,當一個線程佔用鎖操作其中一段數據的時候,其他段的數據也能被其他線程操作;並配合 CAS 完成對數據的添加。

//Segment.put(...) 方法中嘗試獲取鎖的代碼
HashEntry<K,V> node = tryLock() ? null : scanAndLockForPut(key, hash, value);

JDK 1.8 在添加元素時,對數組頭節點加鎖

  添加元素源碼的大致流程: 在擴容時,如果當前數組位置爲空,則直接添加;如果當前數組位置的節點 在擴容 ( (fh = f.hash) == MOVED),則當前線程加入擴容操作;如果當前數組位置不爲空,則使用 synchronized 加鎖數組桶中的頭節點,完成加入元素操作。所以,ConcurrentHashMap支持多線程擴容。


Map的遍歷建議:

  1. JDK 8 後建議使用 map.forEach()

map.forEach((k, v) -> {
System.out.println(k + " " + v);
});
2. 使用entrySet來遍歷map集合

for (Map.Entry entry:map.entrySet()){
System.out.println(entry.getKey()+" : "+entry.getValue());
}


Map間的比較:

  1. HashMap是無序的,有序的是TreeMap(可按key排升降序)和LinkedHashMap(按插入順序排序)。

  2. HashMap是線程不安全的。在JDK 8下,多線程的put操作會出現數據覆蓋的情況,鏈表的元素插入使用尾插法;在JDK 7下,由於在擴容期間,擴容操作會對新鏈表使用到頭插法,多個線程操作時就可能出現循環鏈表,在獲取該位置的元素時,就會導致應用卡死。但如果設置了初始容量後不會出現擴容操作,那麼就不會發生該情況。

  3. HashMap 與 Hashtable 之間的區別有那些?

    • HashMap 是線程不安全的,適合在單線程的環境下使用;Hashtable 是線程安全的,可以(但不推薦)在多線程環境下使用

    • HashMap 允許 key-value 爲 Null ,Hashtable 不允許 key-value 爲 Null

    • HashMap 的 initialCapacity=16 ,擴容規則是大於當前容量的第一個2的n次方;Hash table的 initialCapacity=11,擴容規則爲 oldCapacity * 2 + 1

    • 兩者對 key 求 hash 時運算規則不同,HashMap 的運算規則更加的高效與隨機。

線程安全的幾種Map:

  Hashtable<>() 與 Coolections.synchronizedMap(new HashMap<>()) 都是線程安全的,對每個方法添加synchronized修飾,同一時刻,僅允許一個線程對容器進行操作,導致對容器的訪問變成串行化的了,故效率低下。

  ConcurrentHashMap<>():JDK1.8使用 Synchronized + CAS 保證線程安全;CAS是一個樂觀鎖,通過判斷標記決定這個線程能否操作。效率高,推薦使用。

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