1. 程式人生 > >Java中的集合(Map)

Java中的集合(Map)

標準庫中包含了幾種Map的基本實現,包括:HashMap、TreeMap、LinkedHashMap、WeekHashMap、ConcurrentHashMap、IdentityHashMap。它們都有同樣的基本介面Map,但是行為特性各不相同,這表現在效率,鍵值對的儲存及呈現次序、物件的儲存週期、對映表如何在多執行緒程式中工作和判定“鍵”等價的策略等方面。

Map可以將鍵對映到值。一個對映不能包含重複的鍵;每個鍵最多隻能對映到一個值。Map 介面提供三種collection 檢視,允許以鍵集、值集或鍵-值對映關係集的形式檢視某個對映的內容。對映順序定義為迭代器在對映的Collection檢視上返回其元素的順序。某些對映實現可明確保證其順序,如TreeMap類;另一些對映實現則不保證順序,如HashMap類。

這幾種Map中HashMap是查詢效率最高的Map,LinkedHashMap只比HashMap慢一點兒,但是它可以更快的遍歷關鍵字,TreeMap中的關鍵字都是排序過的,所以可以按序輸出。


HashMap*

Map基於散列表的實現(它取代了Hashtable)。插入和查詢“鍵值對”的開銷是固定的。可以通過構造器設定容量和負載因子,以調整容器的效能。

LinkedHashMap

類似於HashMap,但是迭代遍歷它時,取得“鍵值對”的順序是其插入次序,或者是最近最少使用(LRU)的次序。只比HashMap慢一點;而在迭代訪問時反而更快,因為它使用連結串列維護內部次序。

TreeMap

基於紅黑樹的實現。檢視“鍵”或“鍵值對”時,它們會被排序(次序由Comparable或Comparator決定)。TreeMap的特點在於,所得到的結果是經過排序的,TreeMap是唯一帶有subMap方法的Map,它可以返回一個子樹。

WeekHashMap

弱鍵(week key)對映,允許釋放對映所指向的物件;這是為解決某些類特殊問題而設計的。如果對映之外沒有引用指向某個“鍵”,則此“鍵”可以被垃圾收集器回收。

ConcurrentHashMap

一種執行緒安全的Map,它不涉及同步加鎖。

IdentityHashMap

使用==代替equals對“鍵”進行比較的雜湊對映,專為解決特殊問題而設計。

Map介面中的(部分主要)方法

containsKey(Object key):如果此對映包含指定鍵的對映關係,則返回 true;
containsValue(Object value):如果此對映將一個或多個鍵對映到指定值,則返回 true;
entrySet():返回此對映中包含的對映關係的 Set 檢視;
get(Object key):返回指定鍵所對映的值;如果此對映不包含該鍵的對映關係,則返回 null;
keySet():返回此對映中包含的鍵的 Set 檢視;
put(K key, V value):將指定的值與此對映中的指定鍵關聯(可選操作)。

AbstractMap提供 Map 介面的骨幹實現,以最大限度地減少實現Map介面所需的工作。要實現不可修改的對映,程式設計人員只需擴充套件此類並提供 entrySet 方法的實現即可,該方法將返回對映的對映關係Set檢視。通常,返回的 set 將依次在AbstractSet上實現。此 set 不支援add或remove方法,其迭代器也不支援 remove 方法。要實現可修改的對映,程式設計人員必須另外重寫此類的 put 方法(否則將丟擲 UnsupportedOperationException),entrySet().iterator() 返回的迭代器也必須另外實現其remove方法。

HashMap及其實現方式

HashMap是基於雜湊表實現的,它實現了Map介面,同時允許使用null為key和null為value(除了非同步和允許使用 null 之外,HashMap 類與 Hashtable 大致相同)。HashMap不保證對映的順序,特別是它不保證該順序恆久不變。

假定雜湊函式將元素適當地分佈在各桶之間,可為基本操作(get 和 put)提供穩定的效能。迭代collection檢視所需的時間與HashMap例項的“容量”(桶的數量)及其大小(鍵-值對映關係數)成比例。所以,如果迭代效能很重要,則不要將初始容量設定得太高(或將載入因子設定得太低)。 

HashMap 的例項有兩個引數影響其效能:初始容量和載入因子。容量是雜湊表中桶的數量,初始容量只是雜湊表在建立時的容量。載入因子是雜湊表在其容量自動增加之前可以達到多滿的一種尺度。當雜湊表中的條目數超出了載入因子與當前容量的乘積時,則要對該雜湊表進行 rehash 操作(即重建內部資料結構),從而雜湊表將具有大約兩倍的桶數。在設定初始容量時應該考慮到對映中所需的條目數及其載入因子,以便最大限度地減少 rehash 操作次數。


HashMap的資料結構

在HashMap中比較重要的兩個引數時初始容量和載入因子:

public HashMap(int initialCapacity, float loadFactor);

在構造完成之後,loadFactor會被記錄下來,initialCapacity會變成2的最小次方數,並與loadFactor相乘得到threshold:

// Find a power of 2 >= initialCapacity
int capacity = 1;
while (capacity < initialCapacity)
    capacity <<= 1;
this.loadFactor = loadFactor;
threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
table = new Entry[capacity];

這樣HashMap的所有元素都被放進了Entry構成的陣列中,Entry相當於Hash表中的“桶”(這裡只稱HashMap內的Entry陣列為桶,而不包含連結串列中的Entry),它的內部包含著key,value以及指向下一個和前一個Entry的指標。

這個“桶”就是HashMap中的表現為table陣列:

transient Entry<K,V>[] table;

HashMap的get方法實際上就是為了找到某個Entry,這個Entry的key和給定物件相等:

public V get(Object key) {
        if (key == null)
            return getForNullKey();
        Entry<K,V> entry = getEntry(key);

        return null == entry ? null : entry.getValue();
}
final Entry<K,V> getEntry(Object key) {//根據key查詢Entry
        int hash = (key == null) ? 0 : hash(key);//找到hash值,確定桶的位置
        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;
}

Put方法就是要把資料放入特定的Entry中:

public V put(K key, V value) {
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key);
        int i = indexFor(hash, table.length);//計算位置
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {//判斷當前桶中是否已經包含該元素(判斷key是否已經存在)
            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;
}
void addEntry(int hash, K key, V value, int bucketIndex) {
        if ((size >= threshold) && (null != table[bucketIndex])) {//防止空間不足進行rehash
            resize(2 * table.length);
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }
        createEntry(hash, key, value, bucketIndex);//插入節點
}
void createEntry(int hash, K key, V value, int bucketIndex) {
        Entry<K,V> e = table[bucketIndex];//將當前bucketIndex元素存起來
        table[bucketIndex] = new Entry<>(hash, key, value, e);//設定當前bucketIndex元素,並把原來的元素e,設定為新元素的後繼(連結串列的頭插法,新元素插入頭部)
        size++;
}

put時在使用indexFor找到下標後,需要注意當前桶中是否已經存在給定key對應的元素,這時需要遍歷桶中的所有元素,然後在確定沒有給定key對應的元素時,就可以將當前給定的元素插入這個桶的第一個位置(其他元素後移)。

做put操作時,可能會出現桶的空間不足(也就是size比threshold要大了,此時衝突的可能性會很大),就需要rehash一次,將空間變為當前空間的兩倍(即,resize(2 * table.length)),然後將所有的桶移入新的桶中:

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];
        boolean oldAltHashing = useAltHashing;
        useAltHashing |= sun.misc.VM.isBooted() &&
                (newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
        boolean rehash = oldAltHashing ^ useAltHashing;
        transfer(newTable, rehash);//這裡進行了轉移操作
        table = newTable;
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        for (Entry<K,V> e : table) {//逐個移動桶中元素
            while(null != e) {
                Entry<K,V> next = e.next;
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];//依然採用頭插法,會導致桶中元素逆序
                newTable[i] = e;
                e = next;
            }
        }
}

刪除操作也是如此,找到對應的桶,然後遍歷桶中元素,並在找到元素後刪除它:

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

TreeMap及其實現方式(TreeMap的實現要看紅黑樹的實現方式)

TreeMap是基於紅黑樹(Red-Black tree,具體請參照部落格:Red Black Tree)的 NavigableMap 實現。該對映根據其鍵的自然順序進行排序,或者根據建立對映時提供的 Comparator 進行排序,具體取決於使用的構造方法。它能夠保證containsKey、get、put 和 remove 操作的時間開銷為 log(n)。這裡的紅黑樹演算法是依據演算法導論中的紅黑樹演算法實現的。

在TreeMap中儲存著紅黑樹的樹根:

private transient Entry<K,V> root = null;

當要使用put方法插入資料時,會依據紅黑樹的插入演算法,將資料插入特定的位置,由於紅黑樹本身是二叉排序樹,因此可以按照結點的大小找到目標位置,並插入當前位置,然後再維護紅黑樹的結構,使得它的結構符合紅黑樹的約束。

使用get方法獲取資料時,會按照二叉排序樹的規則比較從根到葉節點的元素,直到發現或找不到目標元素為止:

public V get(Object key) {
        Entry<K,V> p = getEntry(key);
        return (p==null ? null : p.value);
}
final Entry<K,V> getEntry(Object key) {
        // Offload comparator-based version for sake of performance
        if (comparator != null)
            return getEntryUsingComparator(key);
        if (key == null)
            throw new NullPointerException();
        Comparable<? super K> k = (Comparable<? super K>) key;
        Entry<K,V> p = root;
        while (p != null) {
            int cmp = k.compareTo(p.key);
            if (cmp < 0)
                p = p.left;
            else if (cmp > 0)
                p = p.right;
            else
                return p;
        }
        return null;
}

containsKey(Object)方法和get方法是一致的,也是使用getEntry來查詢目標元素。

remove(Object)方法是利用查詢到紅黑樹中的結點,然後刪除該結點,並維護紅黑樹的特徵來實現的。

對於TreeMap,重要的是遍歷的方法,其遍歷的方法是由EntryIterator來實現的,它繼承自PrivateEntryIterator。在PrivateEntryIterator中可以發現,它使用了紅黑樹中求當前結點的前驅(比當前元素小的最大元素)和後繼(比當前元素大且最小的元素)的演算法來進行遍歷。

LinkedHashMap及其實現方式

LinkedHashMap是基於雜湊表和連結串列對Map介面的實現,它可以儲存資料插入連結串列的順序(使用額外於HashMap的連結串列實現)。此實現與 HashMap 的不同之處在於,後者維護著一個運行於所有條目的雙重連結列表。此連結列表定義了迭代順序,該迭代順序通常就是將鍵插入到對映中的順序(插入順序)。注意,如果在對映中重新插入鍵,則插入順序不受影響。(如果在呼叫m.put(k, v)前m.containsKey(k)返回了true,則呼叫時會將鍵 k 重新插入到對映 m 中。)


注:這裡雖然把連結串列和桶的圖分開畫了,但是實際上它們中的結點(除了header)都是共用的

LinkedHashMap繼承自HashMap,所以它比HashMap的效能略差,但是可以維護元素間的插入順序(使用一個雙向連結串列來儲存順序):

private transient Entry<K,V> header;
private static class Entry<K,V> extends HashMap.Entry<K,V> {
        // These fields comprise the doubly linked list used for iteration.
        Entry<K,V> before, after;
		…….//省略
}

當要呼叫put方法插入元素時,會呼叫HashMap的put方法,這個方法會呼叫addEntry()方法,這個方法在LinkedHashMap中被重定義了:

//LinkedHashMap的addEntry方法
void addEntry(int hash, K key, V value, int bucketIndex) {
        super.addEntry(hash, key, value, bucketIndex);//呼叫HashMap中的addEntry方法,會建立結點,同時會維護新建立的結點到雙向連結串列中
        // Remove eldest entry if instructed
        Entry<K,V> eldest = header.after;
        if (removeEldestEntry(eldest)) {
            removeEntryForKey(eldest.key);
        }
}
//HashMap中的addEntry方法
void addEntry(int hash, K key, V value, int bucketIndex) {
        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }

        createEntry(hash, key, value, bucketIndex);
}
//LinkedHashMap中的createEntry,覆蓋HashMap中的createEntry
void createEntry(int hash, K key, V value, int bucketIndex) {
        HashMap.Entry<K,V> old = table[bucketIndex];
        Entry<K,V> e = new Entry<>(hash, key, value, old);
        table[bucketIndex] = e;
        e.addBefore(header);
        size++;
}

從以上程式碼中我們可以看到LinkedHashMap的put方法的過程,首先LinkedHashMap中沒有put方法,所以會呼叫HashMap中的put方法,這個put方法會檢查資料是否在Map中,如果不在就會呼叫addEntry方法,由於LinkedHashMap覆蓋了父類的addEntry方法,所以會直接呼叫LinkedHashMap的addEntry方法,這個方法中又呼叫了HashMap的addEntry方法,addEntry又呼叫了createEntry方法,這個方法也是LinkedHashMap覆蓋了HashMap的,它會建立結點到table中,同時會維護Entry(繼承自HashMap.Entry的LinkedHashMap.Entry)的前後元素。

//HashMap中的createEntry方法,對比以上LinkedHashMap中的createEntry方法發現,除了將Entry放入桶中之外,LinkedHashMap還維護了Entry指向之前元素和之後元素的指標
void createEntry(int hash, K key, V value, int bucketIndex) {
        Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        size++;
}

簡單來講,LinkedHashMap中的Entry是帶有指向在它自己插入Map之前和之後的元素引用的物件,在put元素時,首先檢查資料是否已經在Map中,如果不在就建立這個Entry,同時還要把這個Entry記錄插入到之前元素構成的連結串列中(並沒有真的簡單的建立了個連結串列結點,而是這個連結串列本身就是這些Entry元素構成的)。這些Entry本身不但是Map中table的元素,還是連結串列元素。

在進行遍歷時,它使用的是KeyIterator,而KeyIterator繼承自LinkedHashIterator,在LinkedHashIterator內部有連結串列的頭指標指向的下一個元素:

Entry<K,V> nextEntry = header.after;

由於這些Entry本身是連結串列元素,也是table中元素,故直接找到其後繼就可以得到所有元素。剩下的遍歷過程就是對一個連結串列的遍歷了,每遍歷到一個Entry就可以獲得它的key和value。

此外,LinkedHashMap還能維護一個最近最少訪問的序列,其本質還是維護Entry指標,每次使用get訪問元素時,都會將這個元素插入Map尾部,這樣連結串列頭部就是最近訪問次數最少的元素了,整個連結串列就是從近期訪問最少到近期訪問最多的順序。

其實現方式是,在get中找到要get的元素後呼叫元素的recordAccess方法,這個方法就把這個Entry的前後指標進行了調整。

//LinkedHashMap的get方法
public V get(Object key) {
        Entry<K,V> e = (Entry<K,V>)getEntry(key);
        if (e == null)
            return null;
        e.recordAccess(this);//調整指標
        return e.value;
}
//Entry的recordAccess方法,引數m就是一個LinkedHashMap
void recordAccess(HashMap<K,V> m) {
LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
        if (lm.accessOrder) {//是否按照最近最少訪問排列
            lm.modCount++;
            remove();//從當前鏈中刪除自己
            addBefore(lm.header);//加入到連結串列尾部
        }
}

總的來說,對於所有的集合類來說,對於List,如果隨機存取多於修改首尾元素的可能,則應該選擇ArrayList,如果要實現類似佇列或者棧的功能或者首尾新增的功能較多,則應該選擇LinkedList;對於Set,HashSet是常用的Set,畢竟通常對Set操作都是插入和查詢,但是如果希望產生帶有排序的Set則可以使用TreeSet,希望記錄插入順序則要使用LinkedHashSet;而Map和Set類似,如果需要快速的查詢和新增,則可以用HashMap,如果需要Map中的元素按照一定的規則排序,則可以用TreeMap,如果需要記錄資料加入Map的順序,或者需要使用最近最少使用的規則,則可以用LinkedHashMap。

相關推薦

java集合

一、Map介面 1.Map介面是儲存一組成對出現的鍵(key)---- 值(value)物件。 2.Map介面中的key集無序,唯一,可以為空null,也就是隻有一個鍵集為空,如果有重複的後面的會覆蓋前面的。value集無序,允許重複。 3.Map介面得到常用方法

Java集合Map

標準庫中包含了幾種Map的基本實現,包括:HashMap、TreeMap、LinkedHashMap、WeekHashMap、ConcurrentHashMap、IdentityHashMap。它們都有同樣的基本介面Map,但是行為特性各不相同,這表現在效率,鍵值對的儲存及

JavalongLong與intInteger之間的轉換

轉化 string long tar str 基礎數據類型 ava detail 參考 一、將long型轉化為int型,這裏的long型是基礎類型: long a = 10; int b = (int)a; 二、將Long型轉換為int型,這裏

java異常Exception的定義,意義和用法。舉例

use 詳情 put 視頻下載 ati itl url index ring 1.異常(Exception)的定義,意義和用法 (視頻下載) (全部書籍) 我們先給出一個例子,看看異常有什麽用? 例:1.1-本章源碼 public class Test { publi

java介面interface及使用方法和注意事項

1、介面:一種把類抽象的更徹底,接口裡只能包含抽象方法的“特殊類”。介面不關心類的內部狀態資料,定義的是一批類所遵守的規範。(它只規定這批類裡必須提供某些方法,提供這些方法就可以滿足實際要求)。 在JAVA程式語言中是一個抽象型別,是抽象方法的集合,介面通常以interface來宣告。一個類通過

JavaTimeZone時區類的簡單使用

package com.wk.time import java.util.TimeZone; public class LocaleTimeZone { public static void main(String[] args) { TimeZone zone =

javapackage的使用理解

java中package(包)的使用理解 2017年02月05日 02:30:08 FengGLA 閱讀數:17755 標籤: java 更多 個人分類: java學習筆記 版權宣告:本文為博主原創文章,未經博主允許不得轉載。

java實用類

一、String類 1.在java中String類比較特殊,它是一種引用資料型別,位於java.lang包中。 2.String類的常用方法 (1)length()方法,是求字串的長度 String str="abcdefg"; int s=str.length(); //注意,

java覆蓋重寫equals方法

package com.forming.sapinterface; import sun.java2d.pipe.SpanClipRenderer; import java.util.Objects; public class Sap { private Integer rsnu

javalongLong與intInteger之間的轉換

示例程式碼: public static void main(String[] args) { // 1、將long型轉化為int型,其中int、long是基礎型別 long a = 10; int b = (int) a; System.out.print

Java基礎——集合——集合體系、Collection集合

一、集合概述          Java是一種面嚮物件語言,如果我們要針對多個物件進行操作,就必須對多個物件進行儲存。而陣列長度固定,不能滿足變化的要求。所以,java提供了集合。          特點                 1.        長度可以發生改變

Java併發集合-ConcurrentSkipListMap分析和使用

一、ConcurrentSkipListMap介紹 ConcurrentSkipListMap是執行緒安全的有序的雜湊表,適用於高併發的場景。ConcurrentSkipListMap和TreeMap,它們雖然都是有序的雜湊表。但是,第一,它們的執行緒安全機制不同,TreeMap是非執行緒安全的,而Concu

Java併發集合-CopyOnWriteArrayList分析與使用

CopyOnWriteArrayList分析與使用  原文連結: 一、Copy-On-Write   Copy-On-Write簡稱COW,是一種用於程式設計中的優化策略。其基本思路是,從一開始大家都在共享同一個內容,當某個人想要修改這個內容的時候,才會真正把內容Copy出去形成一個新的內容然後再改,這是

Java併發集合-ConcurrentHashMap分析和使用

1 http://ifeve.com/hashmap-concurrenthashmap-%E7%9B%B8%E4%BF%A1%E7%9C%8B%E5%AE%8C%E8%BF%99%E7%AF%87%E6%B2%A1%E4%BA%BA%E8%83%BD%E9%9A%BE%E4%BD%8F%E4%BD%A0%E

javabyte負值作&運算時0xff的作用

1.問題由來 專案中遇到一個將byte位元組流轉換成有符號整數和無符號整數的,發現: byte aByte = ByteBuffer.get();----對應的二進位制各位 如果byte為正數:int

javaOverload過載和Override重寫、覆蓋

面試題:過載(Overload)和重寫(Override)的區別。過載的方法能否根據返回型別進行區分? 答:方法的過載和重寫都是實現多型的方式,區別在於前者實現的是編譯時的多型性,而後者實現的是執行時的多型性。過載發生在一個類中,同名的方法如果有不同的引數列

Github優秀java專案集合中文版

Java資源大全中文版 我想很多程式設計師應該記得 GitHub 上有一個 Awesome - XXX 系列的資源整理。awesome-java 就是 akullpp 發起維護的 Java 資源列表,內容包括:構建工具、資料庫、框架、模板、安全、程式碼分析、日誌、第三方庫、

Java集合List,Map,Set的使用

   結合框架體系應該最重要的是如何靈活利用三種介面,set,map,list,他們如何遍歷,各有什麼特徵,怎麼樣去處理,這是關鍵,在這個基礎上再去掌握在什麼場合用什麼型別的介面。比如說list和set,set是無序的一個空袋子,當我們只需要放入取出,這個介面當然是最實用的,但是如果我們需要按序取出,這個

Java-----HTML網頁的設計

網頁製造<介紹>: ☆靜態頁面和動態頁面     網站頁面分為靜態頁面和動態頁面兩種1, 靜態頁面:有一個html頁面檔案儲存在伺服器上,瀏覽器要這個頁面的時候伺服器就把這個頁面檔案發給

Java過載overload和重寫override的區別?

概念 方法的過載和重寫都是實現多型的方式,區別在於前者實現的是編譯時的多型性,而後者實現的是執行時的多型性。 過載發生在一個類中,同名的方法如果有不同的引數列表(引數型別不同、引數個數不同