集合操做-HashMap源碼分析

HashMap有4個構造函數java

public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
    }

   
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

    
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

   
    public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }

在HashMap中有兩個很重要的參數,容量(Capacity)和負載因子(Load factor).node

    Capacity就是bucket的大小,Load factor就是bucket填滿程度的最大比例。若是對迭代性能要求很高的話不要把Capacity設置過大,也不要把Load factor設置太小。當bucket中的entries的數目大於Capacity*Load factor時就須要調整bucket的大小爲當前的2倍。數組

static final float DEFAULT_LOAD_FACTOR = 0.75f;

先看下put方法:

public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

   
    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;
    }

put函數大體的思路爲:app

  1. 對key的hashCode()作hash,而後再計算index;
  2. 若是沒碰撞直接放到bucket裏;
  3. 若是碰撞了,以鏈表的形式存在buckets後;
  4. 若是碰撞致使鏈表過長(大於等於TREEIFY_THRESHOLD),就把鏈表轉換成紅黑樹;
  5. 若是節點已經存在就替換old value(保證key的惟一性)
  6. 若是bucket滿了(超過load factor*current capacity),就要resize。

get實現:

public V get(Object key) {
        Node<K,V> e;
        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) {
            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;
    }

get大體思路以下:函數

  1. bucket裏的第一個節點,直接命中;
  2. 若是有衝突,則經過key.equals(k)去查找對應的entry
    若爲樹,則在樹中經過key.equals(k)查找,O(logn);
    若爲鏈表,則在鏈表中經過key.equals(k)查找,O(n)。

resize實現:

    當put時,若是發現目前的bucket佔用程度已經超過了Load Factor所但願的比例,那麼就會發生resize。在resize的過程,簡單的說就是把bucket擴充爲2倍,以後從新計算index,把節點再放到新的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;
        }
        // 沒超過最大值,就擴充爲原來的2倍
        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);
    }
    // 計算新的resize上限
    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) {
        // 把每一個bucket都移動到新的buckets中
        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;
                        }
                        // 原索引+oldCap
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    // 原索引放到bucket裏
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    // 原索引+oldCap放到bucket裏
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

總結:

HashMap基於Map接口實現、容許null鍵/值、非同步、不保證有序(好比插入的順序)、也不保證序不隨時間變化this

能夠總結幾個常見問題:spa

HashMap的工做原理是什麼?code

    經過hash的方法,經過put和get存儲和獲取對象。存儲對象時,咱們將K/V傳給put方法時,它調用hashCode計算hash從而獲得bucket位置,進一步存儲,HashMap會根據當前bucket的佔用狀況自動調整容量(超過Load Facotr則resize爲原來的2倍)。獲取對象時,咱們將K傳給get,它調用hashCode計算hash從而獲得bucket位置,並進一步調用equals()方法肯定鍵值對。若是發生碰撞的時候,Hashmap經過鏈表將產生碰撞衝突的元素組織起來,在Java 8中,若是一個bucket中碰撞衝突的元素超過某個限制(默認是8),則使用紅黑樹來替換鏈表,從而提升速度。對象

咱們可使用自定義的對象做爲鍵嗎?

    固然你可能使用任何對象做爲鍵,只要它遵照了equals()和hashCode()方法的定義規則,而且當對象插入到Map中以後將不會再改變了。若是這個自定義對象時不可變的,那麼它已經知足了做爲鍵的條件,由於當它建立以後就已經不能改變了。

equals()和hashCode()的都有什麼做用?

    經過對key的hashCode()進行hashing,並計算下標( n-1 & hash),從而得到buckets的位置。若是產生碰撞,則利用key.equals()方法去鏈表或樹中去查找對應的節點

 若是HashMap的大小超過了負載因子(load factor)定義的容量,怎麼辦?

若是超過了負載因子(默認0.75),則會從新resize一個原來長度兩倍的HashMap,而且從新調用hash方法。

爲何桶的大小爲2的冪次?

    以Entry[]數組實現的哈希桶數組,用Key的哈希值取模桶數組的大小可獲得數組下標。

    插入元素時,若是兩條Key落在同一個桶(好比哈希值1和17取模16後都屬於第一個哈希桶),Entry用一個next屬性實現多個Entry以單向鏈表存放,後入桶的Entry將next指向桶當前的Entry。

    查找哈希值爲17的key時,先定位到第一個哈希桶,而後以鏈表遍歷桶裏全部元素,逐個比較其key值。

當Entry數量達到桶數量的75%時(不少文章說使用的桶數量達到了75%,但看代碼不是),會成倍擴容桶數組,並從新分配全部原來的Entry,因此這裏也最好有個預估值。

    取模用位運算(hash & (arrayLength-1))會比較快,因此數組的大小永遠是2的N次方, 你隨便給一個初始值好比17會轉爲32。默認第一次放入元素時的初始值是16。

hashMap遍歷時爲何是亂序?

    iterator()時順着哈希桶數組來遍歷,看起來是個亂序。

HashSet 

    咱們順便看一下HashSet的實現,它是由HashMap實現的,沒有重複元素的集合。不保證元素的順序,並且HashSet容許使用 null 元素。

package java.util;

public class HashSet<E>
    extends AbstractSet<E>
    implements Set<E>, Cloneable, java.io.Serializable
{
    static final long serialVersionUID = -5024744406713321676L;

    // HashSet是經過map(HashMap對象)保存內容的
    private transient HashMap<E,Object> map;

    // PRESENT是向map中插入key-value對應的value
    // 由於HashSet中只須要用到key,而HashMap是key-value鍵值對;
    // 因此,向map中添加鍵值對時,鍵值對的值固定是PRESENT
    private static final Object PRESENT = new Object();

    // 默認構造函數
    public HashSet() {
        // 調用HashMap的默認構造函數,建立map
        map = new HashMap<E,Object>();
    }

    // 帶集合的構造函數
    public HashSet(Collection<? extends E> c) {
        // 建立map。
        // 爲何要調用Math.max((int) (c.size()/.75f) + 1, 16),從 (c.size()/.75f) + 1 和 16 中選擇一個比較大的樹呢?        
        // 首先,說明(c.size()/.75f) + 1
        //   由於從HashMap的效率(時間成本和空間成本)考慮,HashMap的加載因子是0.75。
        //   當HashMap的「閾值」(閾值=HashMap總的大小*加載因子) < 「HashMap實際大小」時,
        //   就須要將HashMap的容量翻倍。
        //   因此,(c.size()/.75f) + 1 計算出來的正好是總的空間大小。
        // 接下來,說明爲何是 16 。
        //   HashMap的總的大小,必須是2的指數倍。若建立HashMap時,指定的大小不是2的指數倍;
        //   HashMap的構造函數中也會從新計算,找出比「指定大小」大的最小的2的指數倍的數。
        //   因此,這裏指定爲16是從性能考慮。避免重複計算。
        map = new HashMap<E,Object>(Math.max((int) (c.size()/.75f) + 1, 16));
        // 將集合(c)中的所有元素添加到HashSet中
        addAll(c);
    }

    // 指定HashSet初始容量和加載因子的構造函數
    public HashSet(int initialCapacity, float loadFactor) {
        map = new HashMap<E,Object>(initialCapacity, loadFactor);
    }

    // 指定HashSet初始容量的構造函數
    public HashSet(int initialCapacity) {
        map = new HashMap<E,Object>(initialCapacity);
    }

    HashSet(int initialCapacity, float loadFactor, boolean dummy) {
        map = new LinkedHashMap<E,Object>(initialCapacity, loadFactor);
    }

    // 返回HashSet的迭代器
    public Iterator<E> iterator() {
        // 實際上返回的是HashMap的「key集合的迭代器」
        return map.keySet().iterator();
    }

    public int size() {
        return map.size();
    }

    public boolean isEmpty() {
        return map.isEmpty();
    }

    public boolean contains(Object o) {
        return map.containsKey(o);
    }

    // 將元素(e)添加到HashSet中
    public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }

    // 刪除HashSet中的元素(o)
    public boolean remove(Object o) {
        return map.remove(o)==PRESENT;
    }

    public void clear() {
        map.clear();
    }

    // 克隆一個HashSet,並返回Object對象
    public Object clone() {
        try {
            HashSet<E> newSet = (HashSet<E>) super.clone();
            newSet.map = (HashMap<E, Object>) map.clone();
            return newSet;
        } catch (CloneNotSupportedException e) {
            throw new InternalError();
        }
    }

    // java.io.Serializable的寫入函數
    // 將HashSet的「總的容量,加載因子,實際容量,全部的元素」都寫入到輸出流中
    private void writeObject(java.io.ObjectOutputStream s)
        throws java.io.IOException {
        // Write out any hidden serialization magic
        s.defaultWriteObject();

        // Write out HashMap capacity and load factor
        s.writeInt(map.capacity());
        s.writeFloat(map.loadFactor());

        // Write out size
        s.writeInt(map.size());

        // Write out all elements in the proper order.
        for (Iterator i=map.keySet().iterator(); i.hasNext(); )
            s.writeObject(i.next());
    }


    // java.io.Serializable的讀取函數
    // 將HashSet的「總的容量,加載因子,實際容量,全部的元素」依次讀出
    private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
        // Read in any hidden serialization magic
        s.defaultReadObject();

        // Read in HashMap capacity and load factor and create backing HashMap
        int capacity = s.readInt();
        float loadFactor = s.readFloat();
        map = (((HashSet)this) instanceof LinkedHashSet ?
               new LinkedHashMap<E,Object>(capacity, loadFactor) :
               new HashMap<E,Object>(capacity, loadFactor));

        // Read in size
        int size = s.readInt();

        // Read in all elements in the proper order.
        for (int i=0; i<size; i++) {
            E e = (E) s.readObject();
            map.put(e, PRESENT);
        }
    }
}

HashSet的實現依賴於HashMap,若是理解了HashMap的實現,HashSet仍是比較容易理解的。

相關文章
相關標籤/搜索