1. 程式人生 > >thinking in java (十五) ----- 集合之HahsMap

thinking in java (十五) ----- 集合之HahsMap

HashMap介紹

  • HashMap簡介

HashMap是一個散列表,儲存的內容是鍵值對對映(key-value)k-v

HashMap繼承於AbstractMap,實現了Map,Cloneable,Serializable介面

HashMap的實現是不同步的(執行緒不安全)

HashMap的例項有倆引數影響效能:“初始容量”和“載入因子”,容量是雜湊表中通的數量,初始容量只是雜湊表在建立時的容量,載入因子是雜湊表在其容量自動增加之前可以達到多滿的尺度。當雜湊表中的條目超出了載入因子與當前容量的乘積時,則要對該雜湊表進行rehash操作(內部重建內部資料結構)從而雜湊表擁有兩倍的桶數,通常預設的載入因子是0.75,這是在時間和空間成本上的一種折中。

  • 建構函式

HashMap有四個建構函式

// 預設建構函式。
HashMap()

// 指定“容量大小”的建構函式
HashMap(int capacity)

// 指定“容量大小”和“載入因子”的建構函式
HashMap(int capacity, float loadFactor)

// 包含“子Map”的建構函式
HashMap(Map<? extends K, ? extends V> map)

capacity容量,loaFactor載入因子,預設0.75.

HashMap資料結構

HashMap的繼承關係

java.lang.Object
   ↳     java.util.AbstractMap<K, V>
         ↳     java.util.HashMap<K, V>

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

HashMap與Map的關係

從圖中可以看出:

1,HashMap繼承與AbstractMap類,實現了Map介面,Map是鍵值對介面,AbstractMap實現了鍵值對的通用函式介面

2,HashMap是通過“拉鍊法”實現的雜湊表,包括幾個重要成員,table,size,threshold,loadFactor,modCount

table是一個Entry[] 陣列型別,而Entry實際上是一個單向連結串列,雜湊表的鍵值對都是儲存在Entry陣列中的

size是HashMap的帶下,它是HashMap實際儲存鍵值對的數量

threshold是HashMap的閾值,用於判斷是否需要調整HashMap的容量,threshold的值=容量 乘以 載入因子。當HashMap中的儲存數量達到了threshold,就需要將HashMap的容量加

loadFactor  載入因子

modCount 用來實現fail-fast機制的??、、?

原始碼解析

說明:我們首先需要了解,HashMap就是一個散列表,他是通過拉鍊法來解決雜湊衝突的,拉鍊法就是把具有相同雜湊地址的關鍵字(同義詞)值放在同一個單鏈表中(自行百度)

HashMap的拉鍊法相關內容

  • HashMap資料儲存陣列
transient Entry[] table;
  • 資料節點Entry的結構資料
  • static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        // 指向下一個節點
        Entry<K,V> next;
        final int hash;
    
        // 建構函式。
        // 輸入引數包括"雜湊值(h)", "鍵(k)", "值(v)", "下一節點(n)"
        Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        }
    
        public final K getKey() {
            return key;
        }
    
        public final V getValue() {
            return value;
        }
    
        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }
    
        // 判斷兩個Entry是否相等
        // 若兩個Entry的“key”和“value”都相等,則返回true。
        // 否則,返回false
        public final boolean equals(Object o) {
            if (!(o instanceof Map.Entry))
                return false;
            Map.Entry e = (Map.Entry)o;
            Object k1 = getKey();
            Object k2 = e.getKey();
            if (k1 == k2 || (k1 != null && k1.equals(k2))) {
                Object v1 = getValue();
                Object v2 = e.getValue();
                if (v1 == v2 || (v1 != null && v1.equals(v2)))
                    return true;
            }
            return false;
        }
    
        // 實現hashCode()
        public final int hashCode() {
            return (key==null   ? 0 : key.hashCode()) ^
                   (value==null ? 0 : value.hashCode());
        }
    
        public final String toString() {
            return getKey() + "=" + getValue();
        }
    
        // 當向HashMap中新增元素時,繪呼叫recordAccess()。
        // 這裡不做任何處理
        void recordAccess(HashMap<K,V> m) {
        }
    
        // 當從HashMap中刪除元素時,繪呼叫recordRemoval()。
        // 這裡不做任何處理
        void recordRemoval(HashMap<K,V> m) {
        }
    }

    從中我們可以看出,Entry實際上是一個單向連結串列,這也是為什麼我們說HashMap是通過拉鍊法解決雜湊衝突的,Entry實現了Map.Entry 介面,就是實現了getKey,getValue,setValue,equals,hashcode等方法,這些都是基本的KV操作方法

  • HashMap的建構函式

一共有四個建構函式

// 預設建構函式。
public HashMap() {
    // 設定“載入因子”
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    // 設定“HashMap閾值”,當HashMap中儲存資料的數量達到threshold時,就需要將HashMap的容量加倍。
    threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
    // 建立Entry陣列,用來儲存資料
    table = new Entry[DEFAULT_INITIAL_CAPACITY];
    init();
}

// 指定“容量大小”和“載入因子”的建構函式
public HashMap(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    // HashMap的最大容量只能是MAXIMUM_CAPACITY
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);

    // Find a power of 2 >= initialCapacity
    int capacity = 1;
    while (capacity < initialCapacity)
        capacity <<= 1;

    // 設定“載入因子”
    this.loadFactor = loadFactor;
    // 設定“HashMap閾值”,當HashMap中儲存資料的數量達到threshold時,就需要將HashMap的容量加倍。
    threshold = (int)(capacity * loadFactor);
    // 建立Entry陣列,用來儲存資料
    table = new Entry[capacity];
    init();
}

// 指定“容量大小”的建構函式
public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

// 包含“子Map”的建構函式
public HashMap(Map<? extends K, ? extends V> m) {
    this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
                  DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
    // 將m中的全部元素逐個新增到HashMap中
    putAllForCreate(m);
}

HashMap主要對外介面

  • clear()方法

clear方法的作用是清除HashMap,他是將所有的元素設定為null,

public void clear() {
    modCount++;
    Entry[] tab = table;
    for (int i = 0; i < tab.length; i++)
        tab[i] = null;
    size = 0;
}
  • containKey()方法

判斷HashMap中,是否包含key.

public boolean containsKey(Object key) {
    return getEntry(key) != null;
}

containKey()方法先通過getEntry(try)獲取Entry陣列,然後判斷是否為空

  • getEntry()方法 
final Entry<K,V> getEntry(Object key) {
    // 獲取雜湊值
    // HashMap將“key為null”的元素儲存在table[0]位置,“key不為null”的則呼叫hash()計算雜湊值
    int hash = (key == null) ? 0 : hash(key.hashCode());
    // 在“該hash值對應的連結串列”上查詢“鍵值等於key”的元素
    for (Entry<K,V> e = table[indexFor(hash, table.length)];
         e != null;
         e = e.next) {
        Object k;
        if (e.hash == hash &&
            ((k = e.key) == key || (key != null && key.equals(k))))
            return e;
    }
    return null;
}

getEntry的作用就是返回鍵位key的例項,HashMap將鍵為null的值放在table[0]上,key不為null的放在table的其他位置。

  • containsValue()方法

判斷HashMap裡面是否含有值為Value的鍵值對

public boolean containsValue(Object value) {
    // 若“value為null”,則呼叫containsNullValue()查詢
    if (value == null)
        return containsNullValue();

    // 若“value不為null”,則查詢HashMap中是否有值為value的節點。
    Entry[] tab = table;
    for (int i = 0; i < tab.length ; i++)
        for (Entry e = tab[i] ; e != null ; e = e.next)
            if (value.equals(e.value))
                return true;
    return false;
}

從中,我們可以看出containsNullValue()分為兩步進行處理:第一,若“value為null”,則呼叫containsNullValue()。第二,若“value不為null”,則查詢HashMap中是否有值為value的節點。

  • entrySet()、values()、keySet()遍歷

這三個的原始碼大致相似。以entrySet()為例。

// 返回“HashMap的Entry集合”
public Set<Map.Entry<K,V>> entrySet() {
    return entrySet0();
}

// 返回“HashMap的Entry集合”,它實際是返回一個EntrySet物件
private Set<Map.Entry<K,V>> entrySet0() {
    Set<Map.Entry<K,V>> es = entrySet;
    return es != null ? es : (entrySet = new EntrySet());
}

// EntrySet對應的集合
// EntrySet繼承於AbstractSet,說明該集合中沒有重複的EntrySet。
private final class EntrySet extends AbstractSet<Map.Entry<K,V>> {
    public Iterator<Map.Entry<K,V>> iterator() {
        return newEntryIterator();
    }
    public boolean contains(Object o) {
        if (!(o instanceof Map.Entry))
            return false;
        Map.Entry<K,V> e = (Map.Entry<K,V>) o;
        Entry<K,V> candidate = getEntry(e.getKey());
        return candidate != null && candidate.equals(e);
    }
    public boolean remove(Object o) {
        return removeMapping(o) != null;
    }
    public int size() {
        return size;
    }
    public void clear() {
        HashMap.this.clear();
    }
}

可以看出entrySet中最主要的方法是 EntryIterator()方法,

// 返回一個“entry迭代器”
Iterator<Map.Entry<K,V>> newEntryIterator()   {
    return new EntryIterator();
}

// Entry的迭代器
private final class EntryIterator extends HashIterator<Map.Entry<K,V>> {
    public Map.Entry<K,V> next() {
        return nextEntry();
    }
}

// HashIterator是HashMap迭代器的抽象出來的父類,實現了公共了函式。
// 它包含“key迭代器(KeyIterator)”、“Value迭代器(ValueIterator)”和“Entry迭代器(EntryIterator)”3個子類。
private abstract class HashIterator<E> implements Iterator<E> {
    // 下一個元素
    Entry<K,V> next;
    // expectedModCount用於實現fast-fail機制。
    int expectedModCount;
    // 當前索引
    int index;
    // 當前元素
    Entry<K,V> current;

    HashIterator() {
        expectedModCount = modCount;
        if (size > 0) { // advance to first entry
            Entry[] t = table;
            // 將next指向table中第一個不為null的元素。
            // 這裡利用了index的初始值為0,從0開始依次向後遍歷,直到找到不為null的元素就退出迴圈。
            while (index < t.length && (next = t[index++]) == null)
                ;
        }
    }

    public final boolean hasNext() {
        return next != null;
    }

    // 獲取下一個元素
    final Entry<K,V> nextEntry() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
        Entry<K,V> e = next;
        if (e == null)
            throw new NoSuchElementException();

        // 注意!!!
        // 一個Entry就是一個單向連結串列
        // 若該Entry的下一個節點不為空,就將next指向下一個節點;
        // 否則,將next指向下一個連結串列(也是下一個Entry)的不為null的節點。
        if ((next = e.next) == null) {
            Entry[] t = table;
            while (index < t.length && (next = t[index++]) == null)
                ;
        }
        current = e;
        return e;
    }

    // 刪除當前元素
    public void remove() {
        if (current == null)
            throw new IllegalStateException();
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
        Object k = current.key;
        current = null;
        HashMap.this.removeEntryForKey(k);
        expectedModCount = modCount;
    }

}

我們通過entrySet中的next方法去遍歷HashMap時,實際上呼叫的是nextEntry,而nexrEntry的實現方式,先遍歷Entry(根據Entry在table在table中的序號,從小到大遍歷)

 

,HashMap由陣列+連結串列組成的,陣列是HashMap的主體,連結串列則是主要為了解決雜湊衝突而存在的,如果定位到的陣列位置不含連結串列(當前entry的next指向null),那麼對於查詢,新增等操作很快,僅需一次定址即可;如果定位到的陣列包含連結串列,對於新增操作,其時間複雜度為O(n),首先遍歷連結串列,存在即覆蓋,否則新增;對於查詢操作來講,仍需遍歷連結串列,然後通過key物件的equals方法逐一比對查詢。所以,效能考慮,HashMap中的連結串列出現越少,效能才會越好。