Java集合---Map

Map接口

  1. 將鍵映射到值的對象,一個映射不能包含重複的鍵;每個鍵最多隻能映射到一個值。
  2. Map 接口提供三種 collection 視圖,允許以鍵集、值集或鍵-值映射關係集的形式查看某個映射的內容。
  3. 映射順序定義爲迭代器在映射的 collection 視圖上返回其元素的順序。某些 Map 實現可明確保證其順序,如 TreeMap 類;另一些Map實現則不保證順序,如 HashMap 類。
  4. 某些Map實現對可能包含的鍵和值有所限制。例如,某些實現禁止 null 鍵和值,另一些則對其鍵的類型有限制。

AbstractMap 抽象類

實現了Map中的絕大部分函數接口。它減少了“Map的實現類”的重複編碼。


具體實現類

HashMap

類聲明如下

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

HashMap 也是我們使用非常多的容器,它是基於哈希表的 Map 接口的實現,以key-value的形式存在。

在介紹構造函數之前,先介紹幾個簡單的概念

  • 散列(Hash)表:是根據關鍵字而直接進行訪問的數據結構,也就說,散列表建立了關鍵字和存儲地址之間的映射關係。從散列表的結構特性上可以看出,特別適合用來實現 Map 接口。
  • 散列函數 Hash(key) = Address , 即可以通過 key 來定位到元素的存儲位置。
  • 衝突,任何三散列函數都不可能絕對避免衝突,衝突指 Hash(key1) = Hash(key2)這種情況,即多個 key 通過散列函數被映射到同一個位置,key1 key2 稱作同義詞。
  • 拉鍊法,把所有的同義詞存儲在一個線性鏈表中,這個線性鏈表有散列地址(Hash(key) )唯一標示。HaspMap就是通過散列表這種數據結構進行實現,利用拉鍊法解決衝突的。
    這裏寫圖片描述
  • 負載因子,(負載因子=表中的記錄數/散列表的長度),負載因子越大,散列表越滿,發生衝突的機率越大。HashMap 默認的負載因子爲 0.75 ,HashMap 添加鍵值對時會不斷計算當前的負載因子,一旦負載因子大於 0.75 那麼必須對散列表進行擴容。重新 Hash 每一個元素,是一件非常費時的操作,所以在創建 HashMap 時指定合適的容量是一個不錯的選擇。

上面簡單介紹了 HashMap 所涉及的幾個概念,後面會詳細介紹 HashMap 是怎樣使用和實現這些概念的。

HashMap 的成員屬性

static final int DEFAULT_INITIAL_CAPACITY = 16;
static final int MAXIMUM_CAPACITY = 1 << 30;
static final float DEFAULT_LOAD_FACTOR = 0.75f;

transient Entry[] table;
transient int size;
int threshold;
final float loadFactor;
transient volatile int modCount;

DEFAULT_INITIAL_CAPACITY :默認 HashMap 容器的容量
MAXIMUM_CAPACITY : HashMap 容器最大容量
DEFAULT_LOAD_FACTOR : HashMap 默認的負載因子

size: 容器中有效元素的個數
threshold:容器中成員屬性 table 數組的長度與 loadFactor 的乘積,即容器需要擴容的界限。
loadFactor:負載因子

Entry[] table:table 是HashMap中比較關鍵的一個成員屬性,先看一下數組的類型 Entry

static class Entry<K, V> implements Map.Entry<K, V> {
    final K key;
    V value;
    Entry<K, V> next;
    final int hash;
    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;
    }
    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;
    }
    public final int hashCode() {
        return (key == null ? 0 : key.hashCode())
                ^ (value == null ? 0 : value.hashCode());
    }
    public final String toString() {
        return getKey() + "=" + getValue();
    }

    void recordAccess(HashMap<K, V> m) {
    }

    void recordRemoval(HashMap<K, V> m) {
    }
}

Entry 類保存鍵值對的 key 值,value 值,key 的 hashcode 值和下一個 Entry 的引用。這就是上面所說的拉鍊法就是通過 table 屬性和 Entry 類實現的。HashMap 在添加一個鍵值對 entry1 = < key1 , value1 >的時候,首先計算 key1 hash 值。假設 hash(key1)= 2,那麼設置 table[2] = value1 。接下來添加鍵值對 entry2 =< key2 , value2 >的時候,假設 hash(key1)= 2,那麼此時就產生衝突了,所以需要用拉鍊法解決,即 entry1.next = entry2 。


HashMap 的構造方法

Constructor 1
public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
        table = new Entry[DEFAULT_INITIAL_CAPACITY];
        init();
}

Constructor 1:
設定 hashMap 的容負載因子爲 0.75
設定 hashMap.threshold 當前允許添加元素的個數
初始化長度 16 的數組 table ,但是這個長度值跟容器 size 不是一回事,與 threshold 也不是一回事。

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

Constructor 2
直接調用 Constructor 3

Constructor 3
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);
        int capacity = 1;
        while (capacity < initialCapacity)
            capacity <<= 1;
        this.loadFactor = loadFactor;
        threshold = (int)(capacity * loadFactor);
        table = new Entry[capacity];
        init();
}

跟 Constructor 1 差不多,只有一個地方需要注意

while (capacity < initialCapacity)
            capacity <<= 1;

HashMap 默認的初始化長度爲 16 = 2^4 ,在指定長度的時候,HashMap 仍然會選擇一個 2 的整數倍的值。這樣做的目的是爲了後面進行 hashcode 計算。

Constructor 4
public HashMap(Map<? extends K, ? extends V> m) {
        this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
                      DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
        putAllForCreate(m);
}

調用 Constructor 3


接下來看一下 HashMap 類元素增加、刪除、訪問和迭代實現的具體細節
1 添加元素

public V put(K key, V value) {
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key.hashCode());
        int i = indexFor(hash, table.length);
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }

首先觀察到 HashMap 是允許 key=null 元素(此處所指的元素均代表 Entry 對象)加入的,通過 putForNullKey 方法添加。

 private V putForNullKey(V value) {
    for (Entry<K,V> e = table[0]; e != null; e = e.next) {
        if (e.key == null) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }
    modCount++;
    addEntry(0, null, value, 0);
    return null;
}

默認將 null 元素方法在 table[0] 處,或則 table[0] 連接的鏈表中。如果應經存在 key == null 的元素,那麼覆蓋原來的元素 value 值。否則新增一個元素。

void addEntry(int hash, K key, V value, int bucketIndex) {
    Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
        if (size++ >= threshold)
            resize(2 * table.length);
}

新建一個 Entry 對象,並複製給 table[0],然後判斷 HashMap 中有效元素數量是否已將達到上限,若達到,馬上進行擴展,擴展的過程會在後面講述。

接着往下將 put 方法中的其他內容

int hash = hash(key.hashCode());
static int hash(int h) {
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}

hash 方法可以進一步修正 key 的 hashcode 值,降低衝突的發生機率,但是函數背後的數學意義我不清楚,待以後慢慢修正,下一步需要根據修正後的 hash 去確定該元素的準確位置,即 Hash(key) == Address的過程,具體方法如下:

static int indexFor(int h, int length) {
   return h & (length-1);
}

HashMap 的底層數組長度總是 2 的 n 次方,在構造函數中存在:capacity <<= 1;這樣做總是能夠保證HashMap 的底層數組長度爲 2 的 n 次方。當 length 爲 2 的 n 次方時,h&(length – 1) 就相當於對 length取模,而且速度比直接取模快得多,這是 HashMap 在速度上的一個優化。至於爲什麼是2的n次方下面解釋。
h&(length – 1),這句話除了上面的取模運算外還有一個非常重要的責任:均勻分佈table數據和充分利用空間。這裏我們假設length爲 16(2^n) 和 15 ,h 爲 5、6、7。
這裏寫圖片描述
當n=15時,6和7的結果一樣,這樣表示他們在table存儲的位置是相同的,也就是產生了碰撞,6、7就會在一個位置形成鏈表,這樣就會導致查詢速度降低。誠然這裏只分析三個數字不是很多,那麼我們就看0-15。
這裏寫圖片描述
從上面的圖表中我們看到總共發生了8此碰撞,同時發現浪費的空間非常大,有1、3、5、7、9、11、13、15處沒有記錄,也就是沒有存放數據。這是因爲他們在與14進行&運算時,得到的結果最後一位永遠都是0,即0001、0011、0101、0111、1001、1011、1101、1111位置處是不可能存儲數據的,空間減少,進一步增加碰撞機率,這樣就會導致查詢速度慢。而當length = 16時,length – 1 = 15 即1111,那麼進行低位&運算時,值總是與原來hash值相同,而進行高位運算時,其值等於其低位值。所以說當length = 2^n時,不同的hash值發生碰撞的概率比較小,這樣就會使得數據在table數組中分佈較均勻,查詢速度也較快。

接下來就是加入元素的邏輯了

for (Entry<K,V> e = table[i]; e != null; e = e.next) {
    Object k;
    if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
        V oldValue = e.value;
        e.value = value;
        e.recordAccess(this);
        return oldValue;
    }
}

我們這裏假設 value = m,key = III,i = 2,目前 HashMap 容器的存儲情況如下:
這裏寫圖片描述
Jdk 對於一個對象的 equals 和 hashcode 方法有如下的規定:
如兩個對象 equals 方法返回 true,那麼這兩個對象的 hashcode 方法必須返回相同的值,若 equlas 方法返回 false,那麼這兩個對象的 hascode 方法可以返回相同值,也可以返回不同的值。也就說,若兩個對象的 hashcode 方法返回不同的值,那麼兩個對象肯定是不 equals的,如果兩個對象的 hashcode 方法返回的值相同,也不能確定這兩個對象是 equals的


從 table[2] 對象開始循環遍歷整個鏈表,若 hashcode 值一樣,那麼還不能確定兩個 key 是相等的,繼續 (k = e.key) == key || key.equals(k)) 判定。也許會有人有疑問,爲什麼這麼費事,直接比較 equals 方法不就好嗎?爲什麼還要先進行 hashcode 的比較呢?
有些時候某些類的 equals 方法比較複雜,需要耗費很長時間計算,而如果兩個對象的 hashcode 值不同,就可以直接判定兩個對象不 equals ,無需後續的 equals判定了。

按照上述的假設和規定,在 table[2] 處沒有找到相同的 key(若找到直接覆蓋原來的 vlaue),繼續執行以下代碼。

modCount++;
addEntry(hash, key, value, i);
return null;
void addEntry(int hash, K key, V value, int bucketIndex) {
    Entry<K,V> e = table[bucketIndex];
    table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
    if (size++ >= threshold)
        resize(2 * table.length);
}

加入新元素後, HashMap 當前的存儲情況如下:
這裏寫圖片描述
加入元素時,牽扯到一個擴容的問題,而且 HashMap 的擴容是一個非常費時的操作,因爲擴容的目標是增加容器數組的長度,table.length 變化時,需要重新計算每一個元素的 hascode,從新定位元素在容器中的位置,resize 方法定義如下

 void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }

        Entry[] newTable = new Entry[newCapacity];
        transfer(newTable);
        table = newTable;
        threshold = (int)(newCapacity * loadFactor);
    }

HashMap 允許加入 key = null 的鍵值對
HashMap 未對加入的元素進行排序
HashMap 未提供線程同步機制


2 刪除元素

public V remove(Object key) {
    Entry<K,V> e = removeEntryForKey(key);
    return (e == null ? null : e.value);
}

final Entry<K,V> removeEntryForKey(Object key) {
        int hash = (key == null) ? 0 : hash(key.hashCode());
        int i = indexFor(hash, table.length);
        Entry<K,V> prev = table[i];
        Entry<K,V> e = prev;

        while (e != null) {
            Entry<K,V> next = e.next;
            Object k;
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k)))) {
                modCount++;
                size--;
                if (prev == e)
                    table[i] = next;
                else
                    prev.next = next;
                e.recordRemoval(this);
                return e;
            }
            prev = e;
            e = next;
        }
        return e;
    }

刪除元素與加入元素的邏輯基本一致,計算 key 的 hash 值,從 table[hash]開始檢索,檢索到 kye,刪除 HashMap 中相應的元素。

3 訪問元素

 public V get(Object key) {
   if (key == null)
        return getForNullKey();
    int hash = hash(key.hashCode());
    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.equals(k)))
            return e.value;
    }
    return null;
}

get 訪問元素代碼基本上都在前面出現過,這裏就不在描述了。


4 迭代遍歷

HashMap 提拱了三種 Collection 視圖,允許以鍵集、值集或鍵-值集的形式查看 HashMap 的內容。分被對應以下方法和成員屬性

  1. 鍵-值集
private transient Set<Map.Entry<K,V>> entrySet = null;

public Set<Map.Entry<K,V>> entrySet() {
    return entrySet0();
}
  1. 值集
transient volatile Collection<V> values = null;

public Collection<V> values() {
   Collection<V> vs = values;
    return (vs != null ? vs : (values = new Values()));
}
  1. 鍵集
transient volatile Set<K> keySet = null;
public Set<K> keySet() {
 Set<K> ks = keySet;
    return (ks != null ? ks : (keySet = new KeySet()));
}

首先從鍵-值集開始介紹

public Set<Map.Entry<K,V>> entrySet() {
   return entrySet0();
}
private Set<Map.Entry<K,V>> entrySet0() {
   Set<Map.Entry<K,V>> es = entrySet;
   return es != null ? es : (entrySet = new 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 類型的 Set 容器,那麼我們看一下其中的 iterator 方法

public Iterator<Map.Entry<K,V>> iterator() {
    return newEntryIterator();
}
Iterator<Map.Entry<K,V>> newEntryIterator()   {
    return new EntryIterator();
}
private final class EntryIterator extends HashIterator<Map.Entry<K,V>> {
    public Map.Entry<K,V> next() {
        return nextEntry();
    }
}
private abstract class HashIterator<E> implements Iterator<E> {
   Entry<K,V> next; // next entry to return
    int expectedModCount;   // For fast-fail
    int index;      // current slot
    Entry<K,V> current; // current entry

    HashIterator() {
        expectedModCount = modCount;
        if (size > 0) { // advance to first entry
            Entry[] t = table;
            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();

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

}

HashIterator 類的構造方法中,遍歷 table 數組,尋找到第一個 table[index] != null 的位置。成員變量 next 指向此位置。 next()方法返回一個 Map.Entry類型的對象。下面用圖片的形式展現 鍵-值 集的迭代過程。
這裏寫圖片描述
測試用例代碼如下:

public static void main(String[] args) throws InterruptedException {
        HashMap<Integer,Character> hs = new HashMap<Integer,Character>();   
        hs.put(9, 'i');
        hs.put(8, 'h');
        hs.put(7, 'g');
        hs.put(6, 'f');
        hs.put(5, 'e');
        hs.put(4, 'd');
        hs.put(3, 'c');
        hs.put(2, 'b');
        hs.put(1, 'a');
        hs.put(null, '0');      
        Set<Entry<Integer, Character>> sets = hs.entrySet();
        for(Entry<Integer, Character> entry : hs.entrySet()){
            System.out.println(entry.getValue());
        }
    }

輸出結果爲
0
a
b
c
d
e
f
g
h
i
下面的截圖是斷點查看 table 數組中的 Entry
這裏寫圖片描述

關於其他兩種視圖,這裏就不在一一描述。


HashMap 總結

  1. HashMap 沒有提供同步機制
  2. HashMap 底層是通過散列表實現的,利用拉鍊法解決衝突
  3. HashMap 允許 key = null 的元素加入
  4. HashMap 允許 vlaue = null 的元素加入
  5. HashMap 加入元素時沒有進行排序
  6. HashMap 提供了三種 Collection 類型的視圖
  7. HashMap 繼承自 AbstractMap

Hashtable

Hashtable類聲明如下

public class Hashtable<K,V>
    extends Dictionary<K,V>
    implements Map<K,V>, Cloneable, java.io.Serializable 

可以看出 Hashtable 繼承 Dictionary 類,實現 Map 接口。其中 Dictionary 類是任何可將鍵映射到相應值的類的抽象父類。


Hashtable 構造函數

Constructor 1
public Hashtable() {
    this(11, 0.75f);
}
Constructor 2
public Hashtable(int initialCapacity) {
    this(initialCapacity, 0.75f);
}
Constructor 3
public Hashtable(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal Load: "+loadFactor);

        if (initialCapacity==0)
            initialCapacity = 1;
    this.loadFactor = loadFactor;
    table = new Entry[initialCapacity];
    threshold = (int)(initialCapacity * loadFactor);
}
Constructor 4
 public Hashtable(Map<? extends K, ? extends V> t) {
    this(Math.max(2*t.size(), 11), 0.75f);
    putAll(t);
}

四個構造方法與 HashMap 基本一致,不再贅述

1 Hashtable 底層也是散列表結構,利用拉鍊法解決衝突


下面看一下 Hashtable 的增加、刪除、訪問和迭代方法

1 增加元素

public synchronized V put(K key, V value) {
    // Make sure the value is not null
    if (value == null) {
        throw new NullPointerException();
    }

    // Makes sure the key is not already in the hashtable.
    Entry tab[] = table;
    int hash = key.hashCode();
    int index = (hash & 0x7FFFFFFF) % tab.length;
    for (Entry<K, V> e = tab[index]; e != null; e = e.next) {
        if ((e.hash == hash) && e.key.equals(key)) {
            V old = e.value;
            e.value = value;
            return old;
        }
    }

    modCount++;
    if (count >= threshold) {
        // Rehash the table if the threshold is exceeded
        rehash();

        tab = table;
        index = (hash & 0x7FFFFFFF) % tab.length;
    }

    // Creates the new entry.
    Entry<K, V> e = tab[index];
    tab[index] = new Entry<K, V>(hash, key, value, e);
    count++;
    return null;
}

put 方法與 HashMap 中的 put 方法邏輯基本一致,但是有以下個不同的地方:

if (value == null) {
    throw new NullPointerException();
}

Hashtable 不允許 value=null
Hashtavle 不允許 key = null ,調用 key.hashCode() 方法會出現空指針異常
Hashtavle 提供了同步機制


2 刪除元素

public synchronized V remove(Object key) {
    Entry tab[] = table;
    int hash = key.hashCode();
    int index = (hash & 0x7FFFFFFF) % tab.length;
    for (Entry<K, V> e = tab[index], prev = null; e != null; prev = e, e = e.next) {
        if ((e.hash == hash) && e.key.equals(key)) {
            modCount++;
            if (prev != null) {
                prev.next = e.next;
            } else {
                tab[index] = e.next;
            }
            count--;
            V oldValue = e.value;
            e.value = null;
            return oldValue;
        }
    }
    return null;
}

remove 操作沒有特別之處,不再贅述。

3 訪問元素

public synchronized V get(Object key) {
    Entry tab[] = table;
    int hash = key.hashCode();
    int index = (hash & 0x7FFFFFFF) % tab.length;
    for (Entry<K, V> e = tab[index]; e != null; e = e.next) {
        if ((e.hash == hash) && e.key.equals(key)) {
            return e.value;
        }
    }
    return null;
}

int hash = key.hashCode(),從這裏也可以看出,Hashtable 是不允許 key = null 的元素加入的。

4 迭代容器
Hashtable 同樣提供了三個 Collection 視圖

private transient volatile Set<K> keySet = null;
private transient volatile Set<Map.Entry<K,V>> entrySet = null;
private transient volatile Collection<V> values = null;

獲取視圖的方法

public Set<K> keySet() {
    if (keySet == null)
        keySet = Collections.synchronizedSet(new KeySet(), this);
    return keySet;
}

public Set<Map.Entry<K,V>> entrySet() {
    if (entrySet==null)
        entrySet = Collections.synchronizedSet(new EntrySet(), this);
    return entrySet;
}

 public Collection<V> values() {
    if (values==null)
        values = Collections.synchronizedCollection(new ValueCollection(),this);
        return values;
}

下面以 keySet 爲例進行介紹

 public Set<K> keySet() {
    if (keySet == null)
        keySet = Collections.synchronizedSet(new KeySet(), this);
    return keySet;
}

Collections.synchronizedSet 方法可以把 Set 包裝成一個線程安全的 Set ,接下來看一下 KeySet 類的定義

private class KeySet extends AbstractSet<K> {
   public Iterator<K> iterator() {
 return getIterator(KEYS);
    }
    public int size() {
        return count;
    }
    public boolean contains(Object o) {
        return containsKey(o);
    }
    public boolean remove(Object o) {
        return Hashtable.this.remove(o) != null;
    }
    public void clear() {
        Hashtable.this.clear();
    }
}

鍵集 返回一個 Hashtable.KeySet 類型的集合,接下來看一下該集合的 iterator 方法

public Iterator<K> iterator() {
    return getIterator(KEYS);
}

private <T> Iterator<T> getIterator(int type) {
    if (count == 0) {
        return (Iterator<T>) emptyIterator;
    } else {
        return new Enumerator<T>(type, true);
    }
}

如果 Hashtable 容器沒有有效元素則返回一個 EmptyEnumerator 類型的迭代器,如不爲空則返回一個 Enumerator 類型的迭代器,下面我們看一下 Enumerator 迭代器的類型聲明

 private class Enumerator<T> implements Enumeration<T>, Iterator<T>

實現了 Iterator 和 Enumeration 接口,那麼可以有兩種方式來迭代遍歷鍵集,如下所示

public static void main(String[] args) throws InterruptedException {
    Hashtable<Integer,Character> ht = new Hashtable<Integer,Character>();
    ht.put(1, 'a');     
    Collection<Integer> keys = ht.keySet();
    Iterator<Integer> iterator = keys.iterator();
    while(iterator.hasNext()){
        System.out.println(iterator.next());
    }

    Enumeration<Integer> enumerator = (Enumeration<Integer>) keys.iterator();
        while(enumerator.hasMoreElements()){
        System.out.println(enumerator.nextElement());
    }       
}

Hashtable有兩種迭代方式


Hashtable 總結

  1. Hashtable 提供同步機制
  2. Hashtable 不允許 key = null
  3. Hashtable 不允許 value = null
  4. Hashtable 底層由散列表數據結構實現,利用拉鍊法解決衝突問題
  5. Hashtable 元素插入的時候沒有進行排序
  6. Hashtable 提供三種 Colletion 類型的視圖

TreeMap

  • 底層基於紅黑樹算法實現的
  • TreeMap 可以根據元素鍵的自然順序進行排序,或者根據創建映射時提供的 Comparator 進行排序。
  • 實現 NavigableMap 接口,提供一系列的導航方法,具備針對給定搜索目標返回最接近匹配項的導航方法 。方法 lowerEntry、floorEntry、ceilingEntry 和 higherEntry 分別返回與小於、小於等於、大於等於、大於給定鍵的鍵關聯的 Map.Entry 對象,如果不存在這樣的鍵,則返回 null。

TreeMap 類聲明

public class TreeMap<K,V>
    extends AbstractMap<K,V>
    implements NavigableMap<K,V>, Cloneable, java.io.Serializable

TreeMap 構造函數

Constructor 1

public TreeMap() {
      comparator = null;
}

Constructor 2

public TreeMap(Comparator<? super K> comparator) {
     this.comparator = comparator;
}

Constructor 2

public TreeMap(Comparator<? super K> comparator) {
     this.comparator = comparator;
}

Constructor 3

public TreeMap(SortedMap<K, ? extends V> m) {
     comparator = m.comparator();
     try {
         buildFromSorted(m.size(), m.entrySet().iterator(), null, null);
     } catch (java.io.IOException cannotHappen) {
     } catch (ClassNotFoundException cannotHappen) {
}

Constructor 4

public TreeMap(Map<? extends K, ? extends V> m) {
    comparator = null;
    putAll(m);
}

TreeMap 成員屬性

private transient int size = 0;
private transient int modCount = 0;
private final Comparator<? super K> comparator;
private transient Entry<K,V> root = null;

size : 記錄 TreeMap 中的有效元素
modCount :記錄 TreeMap 被結構化修改的次數
Comparator : 記錄 TreeMap 比較器,通過 Comparator.compare(key1,key2) 對元素進行排序
Entry : TreeMap 底層紅黑樹節點的類型,即 TreeMap 元素的類型

static final class Entry<K,V> implements Map.Entry<K,V> {
      K key;
      V value;
      Entry<K,V> left = null;
      Entry<K,V> right = null;
      Entry<K,V> parent;
      boolean color = BLACK;
      Entry(K key, V value, Entry<K,V> parent) {
          this.key = key;
          this.value = value;
          this.parent = parent;
      }
}

接下來看一下 TreeMap 的 增加、刪除、訪問和迭代操作

1 增加元素

public V put(K key, V value) {
    Entry<K, V> t = root;
    if (t == null) {            
        root = new Entry<K, V>(key, value, null);
        size = 1;
        modCount++;
        return null;
    }
    int cmp;
    Entry<K, V> parent;
    // split comparator and comparable paths
    Comparator<? super K> cpr = comparator;
    if (cpr != null) {
        do {
            parent = t;
            cmp = cpr.compare(key, t.key);
            if (cmp < 0)
                t = t.left;
            else if (cmp > 0)
                t = t.right;
            else
                return t.setValue(value);
        } while (t != null);
    } else {
        if (key == null)
            throw new NullPointerException();
        Comparable<? super K> k = (Comparable<? super K>) key;
        do {
            parent = t;
            cmp = k.compareTo(t.key);
            if (cmp < 0)
                t = t.left;
            else if (cmp > 0)
                t = t.right;
            else
                return t.setValue(value);
        } while (t != null);
    }
    Entry<K, V> e = new Entry<K, V>(key, value, parent);
    if (cmp < 0)
        parent.left = e;
    else
        parent.right = e;
    fixAfterInsertion(e);
    size++;
    modCount++;
    return null;
}

在 TreeMap 的 put() 的實現方法中主要分爲兩個步驟
第一,對於排序二叉樹的創建,其添加節點的過程如下:
1. 以根節點爲初始節點進行檢索
2. 與當前節點進行比對,若新增節點值較大,則以當前節點的右子節點作爲新的當前節點。否則以當前節點的左子節點作爲新的當前節點
3. 循環遞歸2步驟知道檢索出合適的葉子節點爲止
4. 將新增節點與3步驟中找到的節點進行比對,如果新增節點較大,則添加爲右子節點;否則添加爲左子節點
5. 按照這個步驟我們就可以構建出一顆搜索二叉樹

第二,優化搜索二叉樹,構架一棵平衡二叉樹
fixAfterInsertion 方法對搜索二叉樹進行優化,優化的過程會涉及到紅黑樹的左旋、右旋、着色三個基本操作。(具體算法我不清楚,後續更正)


TreeMap 提供了兩種對元素 key 排序方法

  1. 通過在構造 TreeMap 時傳入指定的 Comparator 對象,調用Comparator.compare(key1,key2)方法進行比較。
  2. Key 必須繼承 Comparable 接口,實現 compareTo 方法,調用 key1.compareTo(key2)方法進行比較。

1 TreeMap 與 HashMap 、Hashtable 不同的是,TreeMap 使用 compareTo 或者 compare 方法完成 key 的唯一性檢驗,而後兩者通過 equals 和 hashcode 方法

下面的測試用例中,compareTo 方法與 hashcode 、equlas方法規則不同,導致 hm 容器中只有一個元素,輸出結果爲 1 2 3 4 5 5

public class NoName {
    public static void main(String[] args) throws InterruptedException {

        TreeMap<A,String> tm = new TreeMap<A,String>();
        tm.put(new A(), "1");
        tm.put(new A(), "2");
        tm.put(new A(), "3");
        tm.put(new A(), "4");
        tm.put(new A(), "5");

        Set<Map.Entry<A,String>>  entries = tm.entrySet();
        for(Map.Entry entry : entries){
            System.out.println(entry.getValue());
        }


        HashMap<A,String> hm = new HashMap<A,String>(tm);
        Set<Map.Entry<A,String>>  entries_1 = hm.entrySet();
        for(Map.Entry entry : entries_1){
            System.out.println(entry.getValue());
        }
    }


    private static final class A implements Comparable{
        public int compareTo(Object o) {
            return 1;
        }
        public int hashCode() {
            return 0;
        }
        public boolean equals(Object obj) {
            return true;
        }
    }
}

2 TreeMap 沒有提供同步機制
3 TreeMap 通過 Comparable 形式排序時,不允許 key = null ,因爲調用 compareTo 方法會拋出異常
*4 TreeMap 通過 Comparator 形式排序時,是否允許 key = null ,取決於你指定的 Comparator 對象的 compare 方法的具體實現。


2 刪除元素

 public V remove(Object key) {
    Entry<K,V> p = getEntry(key);
    if (p == null)
        return null;
    V oldValue = p.value;
    deleteEntry(p);
    return oldValue;
}

3 訪問元素

public V get(Object key) {
    Entry<K,V> p = getEntry(key);
    return (p==null ? null : p.value);
}

4 迭代元素

TreeMap 提供了三種 Collection 視圖,分別是鍵集、值集合鍵_值集,不同的是,這些集合都按照一定規則進行了排序


TreeMap 總結

  1. 底層由紅黑樹實現
  2. 未提供同步機制
  3. 加入元素時進行了排序,通過 Comparator 或者 Comparable
  4. 使用 Comparator 或者 Comparable 來確定 key 是否重複,不再使用 hashcode 和 equals 方法。
  5. 允許 value = null
  6. 使用 Comparable 比較方式時,不允許 key = null,但是使用 Comparator 方式時,是否允許 key = null,取決於 Comparator.compare()方法的實現方式

這裏寫圖片描述
這裏寫圖片描述

發佈了48 篇原創文章 · 獲贊 2 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章