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:
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陣列:
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本身是連結串列元素,也是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,但是行為特性各不相同,這表現在效率,鍵值對的儲存及
Java中long(Long)與int(Integer)之間的轉換(轉)
轉化 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來宣告。一個類通過
Java中TimeZone(時區)類的簡單使用
package com.wk.time import java.util.TimeZone; public class LocaleTimeZone { public static void main(String[] args) { TimeZone zone =
java中package(包)的使用理解
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
java中long(Long)與int(Integer)之間的轉換
示例程式碼: 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
java中byte(負值)作&運算時0xff的作用
1.問題由來 專案中遇到一個將byte位元組流轉換成有符號整數和無符號整數的,發現: byte aByte = ByteBuffer.get();----對應的二進位制各位 如果byte為正數:int
java中Overload(過載)和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)的區別?
概念 方法的過載和重寫都是實現多型的方式,區別在於前者實現的是編譯時的多型性,而後者實現的是執行時的多型性。 過載發生在一個類中,同名的方法如果有不同的引數列表(引數型別不同、引數個數不同