1. 程式人生 > >Java 集合學習--HashMap

Java 集合學習--HashMap

成對 刪除元素 對比 node節點 div zab 算法 instance 輸出

一、HashMap 定義

HashMap 是一個基於散列表(哈希表)實現的鍵值對集合,每個元素都是key-value對,jdk1.8後,底層數據結構涉及到了數組、鏈表以及紅黑樹。目的進一步的優化HashMap的性能和效率。允許key和value為NULL,同樣非線程安全。

技術分享圖片

①、繼承AbstractMap抽象類,AbstractMap實現Map接口,實現部分方法的。同樣在上面HashMap的結構中,HashMap同樣實現了Map接口,這樣做是否有什麽深層次的用意呢?網上查閱資料發現,這種寫法只是一種失誤,在java集合框架中發現很多這種寫法,不用過於在乎這種失誤。

②、實現Map接口,Map類的頂層接口,Map接口定義了一組鍵值對映射通用的操作。儲存一組成對的鍵-值對象,提供key(鍵)到value(值)的映射,Map中的key不要求有序,不允許重復。value同樣不要求有序,但可以重復。

技術分享圖片

③、實現 Cloneable 接口和Serializable接口,分別支持對象的克隆與對象的序列化

二、字段屬性

//1.序列化版本標記,序列化與反序列化時發揮作用
private static final long serialVersionUID = 362498820763181265L;
//2.HashMap集合默認初始化容量,1*2的4次方,即16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4
//3.集合最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
//4.默認的加載因子
static final float DEFAULT_LOAD_FACTOR = 0.75f; //5.當桶(bucket)上的結點數大於這個值時會轉成紅黑樹(JDK1.8新增) static final int TREEIFY_THRESHOLD = 8; //6.當bucket上的節點數少於這個值得時候,數轉化成鏈表 static final int UNTREEIFY_THRESHOLD = 6; //7.當集合的容量大於這個值的時候,HashMap中桶的節點進行樹形化 static final int MIN_TREEIFY_CAPACITY = 64; //8.Node數組,Node是HashMap中自定義hash節點類。
transient Node<K,V>[] table; //9.保存緩存的entrySet transient Set<Map.Entry<K,V>> entrySet; //10.集合元素個數,即HashMap中鍵值對的個數 transient int size; //11.集合修改次數 transient int modCount //12.下次擴容的臨界值,通常稱為閥值,等於capacity * load factor int threshold; //13.散列表的加載因子,可自己指定 final float loadFactor;

三、構造方法

①、默認無參構造器,設置加載因子為默認加載因子0.75

技術分享圖片

②、帶參構造器

1)指定初始化容量,使用默認加載因子

技術分享圖片

2)指定初始化容量,使用自定加載因子

技術分享圖片

四、常用方法

1.添加元素(鍵值對)

技術分享圖片
//添加鍵值對
public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
}
-------------------------------------------------------------------------
//具體的步驟
//1.通過key計算hash值
static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//2.如果table數組為null,初始化數組,通過hash值確定鍵值對在數組中的索引位置
//3.確定元素在數組中的位置後,判斷那個位置的元素的key是否與添加的key一樣,如果一致則直接覆蓋老的鍵值對,如果不一樣則判斷結點的類型,是否是樹,如果是樹則在樹中插入一個新構造的樹結點
//4.如果數組對應位置上的節點不是樹,則是鏈表,這個時候要麽在鏈表的最後插入新的Node節點,要麽是當發現鏈表的節點個數大於8,並且集合元素個數大於64的時候,則將鏈表轉換成紅黑樹,要麽則是進行擴容。
-------------------------------------------------------------------------
//具體細節如下:
/**
     * Implements Map.put and related methods
     *
     * @param hash hash for key    哈希值
     * @param key the key    鍵
     * @param value the value to pu    t值
     * @param onlyIfAbsent if true, don‘t change existing value
     * @param evict if false, the table is in creation mode. false表示table處於創建模式
     * @return previous value, or null if none
     */
    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;//首次進行數組的初始化,resize主要用來進行擴容的
        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;
}
View Code

2.查找元素(鍵值對)

①、通過 key 查找 value,首先通過key計算hash值,再根絕hash值確定key在所在數組的索引位置後,最後遍歷其後面的鏈表或者紅黑樹找到對應的value.

public V get(Object key) {
        Node<K,V> e;
        //確定key的hash值,計算方法與插入的時候一致
        return (e = getNode(hash(key), key)) == null ? null : e.value;
}

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) {//數組不為空,並且在相關的位置存在Node結點。
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))//如果在第一個位置存在則直接返回Nnode結點
                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;
    }

②、判斷是否存在給定的 key或者 value,

//判斷集合中是否存在某個鍵,原理與查找鍵值類似
public boolean containsKey(Object key) {
        return getNode(hash(key), key) != null;
}
//遍歷整個數組,明顯效率很低
public boolean containsValue(Object value) {
        Node<K,V>[] tab; V v;
        if ((tab = table) != null && size > 0) {
            //遍歷桶
            for (int i = 0; i < tab.length; ++i) {
                //遍歷桶中的每個節點元素
                for (Node<K,V> e = tab[i]; e != null; e = e.next) {
                    if ((v = e.value) == value ||
                        (value != null && value.equals(v)))
                        return true;
                }
            }
        }
        return false;
    }

3.刪除元素(鍵值對),刪除的原理本質上是類似通過key查找value,首先通過key計算hash值,通過hash值確定key在散列表中的位置,由於key不重復,所以確定位置後,要麽是遍歷鏈表,要麽遍歷紅黑樹找到對應key所在的節點,找到node節點後刪除。

技術分享圖片
public V remove(Object key) {
        Node<K,V> e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
            null : e.value;
    }
    
    final Node<K,V> removeNode(int hash, Object key, Object value,
            boolean matchValue, boolean movable) {
        Node<K,V>[] tab; Node<K,V> p; int n, index;
        //(n - 1) & hash找到桶的位置
        if ((tab = table) != null && (n = tab.length) > 0 &&
        (p = tab[index = (n - 1) & hash]) != null) {
        Node<K,V> node = null, e; K k; V v;
        //如果鍵的值與鏈表第一個節點相等,則將 node 指向該節點
        if (p.hash == hash &&
        ((k = p.key) == key || (key != null && key.equals(k))))
        node = p;
        //如果桶節點存在下一個節點
        else if ((e = p.next) != null) {
            //節點為紅黑樹
        if (p instanceof TreeNode)
         node = ((TreeNode<K,V>)p).getTreeNode(hash, key);//找到需要刪除的紅黑樹節點
        else {
         do {//遍歷鏈表,找到待刪除的節點
             if (e.hash == hash &&
                 ((k = e.key) == key ||
                  (key != null && key.equals(k)))) {
                 node = e;
                 break;
             }
             p = e;
         } while ((e = e.next) != null);
        }
        }
        //刪除節點,並進行調節紅黑樹平衡
        if (node != null && (!matchValue || (v = node.value) == value ||
                      (value != null && value.equals(v)))) {
        if (node instanceof TreeNode)
         ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
        else if (node == p)
         tab[index] = node.next;
        else
         p.next = node.next;
        ++modCount;
        --size;
        afterNodeRemoval(node);
        return node;
        }
        }
        return null;
    }
View Code

4.遍歷集合,map的遍歷方法有多種,通常推薦的叠代器遍歷,不過遍歷的本質都是循環整個table數組,對數組的每個桶進行遍歷。

 1 public class HashMapDemo {
 2     
 3     public static void main(String[] args) {
 4         
 5         HashMap<String, String> map = new HashMap<>();
 6         map.put("1", "a");
 7         map.put("2", "b");
 8         map.put("3", "c");
 9         //通過獲取所有的key後,再通過key獲取value的方式遍歷
10         for(String str : map.keySet()){
11             System.out.print(map.get(str)+" ");
12         }
13         System.out.println("\n----------------");
14         for(Entry entry : map.entrySet()){
15             System.out.println(entry.getKey()+" "+entry.getValue());
16         }
17     }
18 
19 }

輸出結果:

a b c 
----------------
1 a
2 b
3 c

四、HashMap的hash值得秘密

1.HashMap 是數組+鏈表+紅黑樹的組合,hash值主要牽扯到它在hash表,也叫散列表中的位置。Hash表是一種根據關鍵字值(key - value)而直接進行訪問的數據結構。也就是說它通過把關鍵碼值映射到表中的一個位置來訪問記錄,它的查找速度非常快,查找一個元素可用通過key的hash值直接確定所在的位置。HashMap中確定key的值有其特定的算法,並且最終確定在散列表中的位置還需要經過&運算。具體如下:

static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
    
    i = (table.length - 1) & hash;//這一步是在後面添加元素putVal()方法中進行位置的確定

主要分為三步:

  ①、取 hashCode 值: key.hashCode(),註意hashcode()的值是key對象在堆中的地址經過特殊的hahs算法映射後的一個int值

  ②、高位參與運算:h>>>16

  ③、取模運算:(n-1) & hash,註意:在n是偶數的時候,(n-1)&hash==hash%n;這也是為什麽HashMap的容量為2的n次方的原因,便於使用按位與運算代替取模,因為位運算的速度遠快於取模。這是一種優化。

2.為什麽要進行(h = k.hashCode()) ^ (h >>> 16)?

通過hashCode()的高16位 異或 低16位實現的:(h = k.hashCode()) ^ (h >>> 16),主要是從速度、功效、質量來考慮的,這麽做可以在數組table的length比較小的時候,也能保證考慮到高低Bit都參與到Hash的計算中,同時不會有太大的開銷。具體如下:

技術分享圖片

五、HashMap的擴容機制

當HashMap 集合的元素已經大於了最大承載容量threshold(capacity * loadFactor)的時候,會進行擴容,即擴大數組的長度。通常不指定集合容量的時候,初始容量為16.閥值為12.當集合的元素個數大於12的時候,集合進行擴容,擴大打原來集合的2倍大小,如下初始集合的首次擴容為32,依次是64,128。對應的閥值進行變化。依次為24,48,96。在擴容的同時,需要重新進行鍵值對的hash映射。在jdk1.8之前,擴容首先是創建一個新的大容量數組,然後依次重新計算原集合所有元素的索引,然後重新賦值。如果數組某個位置發生了hash沖突,使用的是單鏈表的頭插入方法,同一位置的新元素總是放在鏈表的頭部,這樣與原集合鏈表對比,擴容之後的可能就是倒序的鏈表了。在jdk1.8後,進行進一步的優化,使擴容的時候具體細節如下:

技術分享圖片
  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;
    }
View Code

從上面的源碼可以看出,容量的增長是成倍增長,容量的大小為2的n次冪,那麽擴容的時候,元素的key的hash值要麽在原來位置不變,要麽在原來的位置再移動2次冪的位置。不需要再次進行按位與運算,進一步優化擴容的機制。僅僅需要看看原來的hash值新增的那個bit是1還是0就好了,是0的話索引沒變,是1的話索引變成“原索引+oldCap”。

六、總結

  • HashMap中兩個重要的屬性,初始容量和加載因子,這兩個屬性影響HashMap的性能,它的設值需要非常謹慎。初始容量是創建哈希表時的容量,加載因子是哈希表在其容量自動增加之前可以達到多滿的一種尺度,兩者決定何時進行擴容。
  • HashMap中key和value都允許為null。
  • 哈希表的容量一定要是2的整數次冪,length為2的整數次冪的話,h&(length-1)就相當於對h%length,這樣便保證了散列的均勻,使不同hash值發生碰撞的概率,同時也提升了效率。

Java 集合學習--HashMap