Java集合框架分析(四)HashMap分析

本篇文章主要分析一下Java集合框架中的Map部分,HashMap,該源碼分析基於JDK1.8,分析工具,AndroidStudio,文章分析不足之處,還請見諒!

HashMap簡介

基於哈希表的一個 Map 接口實現,存儲的對象是一個鍵值對對象 (Entry<K,V>);值得注意的是 HashMap 不是線程安全的,如果想要線程安全的 HashMap,可以通過 Collections 類的靜態方法 synchronizedMap 獲得線程安全的 HashMap。

Map map = Collections.synchronizedMap(new HashMap());

數據結構

Java 最基本的數據結構有數組和鏈表。數組的特點是空間連續(大小固定)、尋址迅速,但是插入和刪除時需要移動元素,所以查詢快,增加刪除慢。鏈表恰好相反,可動態增加或減少空間以適應新增和刪除元素,但查找時只能順着一個個節點查找,所以增加刪除快,查找慢。有沒有一種結構綜合了數組和鏈表的優點呢?當然有,那就是哈希表(雖說是綜合優點,但實際上查找肯定沒有數組快,插入刪除沒有鏈表快,一種折中的方式吧),所有的數據結構都可以用這兩個基本結構來構造的,HashMap 也不例外。HashMap 實際上是一個“鏈表散列”的數據結構,即數組和鏈表的結合體。

HashMap 的底層主要是基於數組和鏈表來實現的,它之所以有相當快的查詢速度主要是因爲它是通過計算散列碼來決定存儲的位置。HashMap 中主要是通過 key 的 hashCode 來計算 hash 值的,只要 hashCode 相同,計算出來的 hash 值就一樣。如果存儲的對象對多了,就有可能不同的對象所算出來的 hash 值是相同的,這就出現了所謂的 hash 衝突。學過數據結構的同學都知道,解決hash衝突的方法有很多,HashMap 底層是通過鏈表來解決 hash 衝突的。圖中,左邊部分即代表哈希表,也稱爲哈希數組,數組的每個元素都是一個單鏈表的頭節點,鏈表是用來解決衝突的,如果不同的 key 映射到了數組的同一位置處,就將其放入單鏈表中。

內部接口

內部接口 EntryEntry 接口是 Map 定義的一個內部接口

 interface Entry<K, V> {
	K getKey();
	V getValue();
	V setValue(V value);
	boolean equals(Object o);
	int hashCode();
	public static <K extends Comparable<? super K>, V> Comparator<Map.Entry<K, V>> comparingByKey() {
		return (Comparator<Map.Entry<K, V>> & Serializable) (c1, c2) -> c1.getKey().compareTo(c2.getKey());
	}
	public static <K, V extends Comparable<? super V>> Comparator<Map.Entry<K, V>> comparingByValue() {
		return (Comparator<Map.Entry<K, V>> & Serializable) (c1, c2) -> c1.getValue().compareTo(c2.getValue());
	}
	public static <K, V> Comparator<Map.Entry<K, V>> comparingByKey(Comparator<? super K> cmp) {
		Objects.requireNonNull(cmp);
		return (Comparator<Map.Entry<K, V>> & Serializable) (c1, c2) -> cmp.compare(c1.getKey(), c2.getKey());
	}
	public static <K, V> Comparator<Map.Entry<K, V>> comparingByValue(Comparator<? super V> cmp) {
		Objects.requireNonNull(cmp);
		return (Comparator<Map.Entry<K, V>> & Serializable) (c1, c2) -> cmp.compare(c1.getValue(), c2.getValue());
	}
}

對於 HashMap,它的內部裏面實現一個靜態內部類,即爲 HashMapEntry<K,V> ,其重要的屬性有 key , value, next,從屬性 key,value 我們就能很明顯的看出來 HashMapEntry 就是 HashMap 鍵值對實現的一個基礎 bean,我們上面說到 HashMap 的基礎就是一個線性數組,這個數組就是 HashMapEntry[],Map 裏面的內容都保存在 HashMapEntry[] 裏面。

transient HashMapEntry<K,V>[] table = (HashMapEntry<K,V>[]) EMPTY_TABLE;

我們可以來簡單分析一下這個靜態內部類 HashMapEntry.java

// HashMapEntry.java靜態內部類,實現的HashMap線性數組   
static class HashMapEntry<K, V> implements Map.Entry<K, V> {
	// key,value值
	final K key;
	V value;
	// 每個數組裏麪包含的鏈表
	HashMapEntry<K, V> next;
	int hash;
	/** * 構造函數 * 輸入參數包括"哈希值(h)", "鍵(k)", "值(v)", "下一節點(n)" */
	HashMapEntry(int h, K k, V v, HashMapEntry<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;
	}
	public final int hashCode() {
		return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
	}
	public final String toString() {
		return getKey() + "=" + getValue();
	}
	/**
	 * * 當向HashMap中添加元素時,會調用recordAccess()
	 */
	void recordAccess(HashMap<K, V> m) {
	}
	/**
	 * * 當從HashMap中刪除元素時,會調用recordRemoval()。
	 */
	void recordRemoval(HashMap<K, V> m) {
	}
}

HashMap 其實就是一個 HashMapEntry 數組,HashMapEntry 對象中包含了鍵和值,其中 next 也是一個 HashMapEntry 對象,它就是用來處理 hash 衝突的,形成一個鏈表。基本結構我們已經分析了,接下來我們就開始正題,開始分析 HashMap 的源碼實現啦!

源碼分析

類申明

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

首先我們來看看 HashMap 內部申明的一些屬性,

//屬性申明	
//默認的初始容量,必須是2的冪次方。    
static final int DEFAULT_INITIAL_CAPACITY = 4;    
//最大容量,默認爲2的30次方,    
static final int MAXIMUM_CAPACITY = 1 << 30;    
//加載因子    
static final float DEFAULT_LOAD_FACTOR = 0.75f;    
//    
static final HashMapEntry<?,?>[] EMPTY_TABLE = {};    
//數組表,大小可以改變,且大小必須爲2的冪    
transient HashMapEntry<K,V>[] table = (HashMapEntry<K,V>[]) EMPTY_TABLE;    
//當前Map中key-value映射的個數    
transient int size;    
//當實際大小超過臨界值時,會進行擴容threshold = 加載因子*容量    
int threshold;    
//加載因子    
final float loadFactor = DEFAULT_LOAD_FACTOR;    
//Hash表結構性修改次數    
transient int modCount;

對於加載因子 loadFactor,我們可以稍作解釋:

loadFactor 加載因子是表示 Hsah 表中元素的填滿的程度.

若:加載因子越大,填滿的元素越多,好處是,空間利用率高了,但:衝突的機會加大了.鏈表長度會越來越長,查找效率降低。反之,加載因子越小,填滿的元素越少,好處是:衝突的機會減小了,但:空間浪費多了.表中的數據將過於稀疏(很多空間還沒用,就開始擴容了)衝突的機會越大,則查找的成本越高.因此,必須在“衝突的機會”與”空間利用率”之間尋找一種平衡與折衷. 這種平衡與折衷本質上是數據結構中有名的”時-空”矛盾的平衡與折衷.

如果機器內存足夠,並且想要提高查詢速度的話可以將加載因子設置小一點;相反如果機器內存緊張,並且對查詢速度沒有什麼要求的話可以將加載因子設置大一點。不過一般我們都不用去設置它,讓它取默認值 0.75 就好了。以上一些基本屬性是在 HashMap 中申明的,如果不明白什麼意思的話,可以等到後面再來看看。我們接着分析下面的代碼。

構造函數

接着我們來分析分析 HashMap 的構造函數

/**     
 * * 設置初始容量大小以及加載因子     
 * *     
 * * @param  initialCapacity the initial capacity     
 * * @param  loadFactor      the load factor     
 * * @throws IllegalArgumentException if the initial capacity is negative     
 * *         or the load factor is nonpositive     
 * */    
public HashMap(int initialCapacity, float loadFactor) {
	//初始容量大小在[DEFAULT_INITIAL_CAPACITY,MAXIMUM_CAPACITY]之間        
	if (initialCapacity < 0)           
		throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
	if (initialCapacity > MAXIMUM_CAPACITY) {
		initialCapacity = MAXIMUM_CAPACITY;       
		} 
	else if (initialCapacity < DEFAULT_INITIAL_CAPACITY) { 
		initialCapacity = DEFAULT_INITIAL_CAPACITY;       
		}       
	if (loadFactor <= 0 || Float.isNaN(loadFactor)) 
		throw new IllegalArgumentException("Illegal load factor: " +   
    loadFactor);               
	threshold = initialCapacity; 
	init();   
	}    
/** 
 *     
 *     * 設置初始容量,加載因子爲默認的0.75     
 *     * @param  initialCapacity the initial capacity.     
 *     * @throws IllegalArgumentException if the initial capacity is negative.     
 *     */    
public HashMap(int initialCapacity) {
	this(initialCapacity, DEFAULT_LOAD_FACTOR);    
	}   
/** 
 *     * 使用默認的容量大小和加載因子     
 *     */   
public HashMap() { 
	this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);    
	}    
/** 
 *     * 根據指定的map生成一個新的HashMap,負載因子使用默認值,
 *     初始容量大小爲Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,DEFAULT_INITIAL_CAPACITY)     
 *     *     
 *     * @param   m the map whose mappings are to be placed in this map     
 *     * @throws  NullPointerException if the specified map is null    
 *      */    
public HashMap(Map<? extends K, ? extends V> m) {  
	this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,  DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);		
	//初始化HashMapEntry數組        
	inflateTable(threshold);       
	putAllForCreate(m);    
	}
}

通過以上 HashMap 的四種構造函數我們可以發現,我們可以通過設置初始容量大小和加載因子或者直接通過一個 map 來構造一個 HashMap,其中初始容量大小我們設定爲 4,它必須是 2 的冪次方,同時它的範圍在 4 和 2 的 30 次方之間。構造函數比較簡單,沒什麼可以分析的,接下來我們着重分析一下 HashMap 的添加和獲取方法,也就是 put 和 get 方法。

存儲數據PUT

//向map存入一個鍵值對,如果key已存在,則覆蓋
public V put(K key, V value) {
	//如果HashMapEntry數組爲空,則重新初始化一下        
	if (table == EMPTY_TABLE) {
		inflateTable(threshold);        
		}		
	//對key爲null的鍵值對調用putForNullKey處理
	if (key == null)            
		return putForNullKey(value);
	//生成hash值        
	int hash = sun.misc.Hashing.singleWordWangJenkinsHash(key);
	//生成hash值索引        
	int i = indexFor(hash, table.length);
	//循環遍歷Entry數組,若“該key”對應的鍵值對已經存在,則用新的value取代舊的value。然後退出,同時返回舊的value!        
	for (HashMapEntry<K,V> e = table[i]; e != null; e = e.next) {
		Object k;			
		//如果找到了相同的key,則覆蓋舊的value
		if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { 
			V oldValue = e.value;                
			e.value = value;                
			//調用HashMapEntry增加這個Entry                
			e.recordAccess(this);                
			return oldValue;            
			}        
		}				
	//修改次數        
	modCount++;        
	//將key-value添加到table[i]處,也就是在鏈表的頭部插入一個數據<k,v>        
	addEntry(hash, key, value, i);        
	return null;    
	}
}

以上的大概內容就是,先獲取 key 的 hash 值,然後根據 hash 值來獲取在數組中的位置,判斷在 table[i] 中是否存在這個 key,也就是先循環遍歷鏈表,如果找到這個 key 的值,那麼就替換這個 key 值,然後將 value 修改進去,如果在鏈表中找不到這個 key 的話,那麼就在鏈表的頭部插入一個數據。在 put 方法中,我們來詳細分析其中的一些方法。開頭有一個判斷數組是否爲 null 的操作

//如果HashMapEntry數組爲空,則重新初始化一下        
if (table == EMPTY_TABLE) {
	inflateTable(threshold);
	}	
/**
 *      
 *      * 初始化這個數組     
 *      */    
private void inflateTable(int toSize) { 
	// 尋找大於toSize的2的冪次方的最小值        
	int capacity = roundUpToPowerOf2(toSize);
	float thresholdFloat = capacity * loadFactor;       
	if (thresholdFloat > MAXIMUM_CAPACITY + 1) { 
		thresholdFloat = MAXIMUM_CAPACITY + 1;        
		}				
	//初始化數組        
	threshold = (int) thresholdFloat;
	table = new HashMapEntry[capacity];    
	}
}

接下來,判斷了 key 是否爲 null,如果爲 null 的話,則進行 putForNullKey(V value) 操作

/**     
 * * 插入key爲null的值,在數組的第一個位置進行插入操作     
 * */    
private V putForNullKey(V value) {
	//循環遍歷數組第一個數組的鏈表,如果存在null的則進行覆蓋更新操作        
	for (HashMapEntry<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++;        
	//鏈表中不存在key爲null的值,則在鏈表的開頭插入這個key爲null的值        
	addEntry(0, null, value, 0);       
	return null;    
	}
}

如果 key 爲 null 的話,hash 值爲 0,對象存儲在數組中索引爲 0 的位置。即 table[0],接着我們分析一下 addEntry 方法,

//通過hash值來算出在table中哪個位置,然後進行插入操作
void addEntry(int hash, K key, V value, int bucketIndex) {
	//        
	if ((size >= threshold) && (null != table[bucketIndex])) {
		//數組擴容            
		resize(2 * table.length);            
		//如果key爲null的話,那麼hash值爲0,也就是在數組的第一個位置,即table[0]位置            
		hash = (null != key) ? sun.misc.Hashing.singleWordWangJenkinsHash(key) : 0;            
		//根據hash的值來算出在數組的中位置            
		bucketIndex = indexFor(hash, table.length);        
	}		
	//數據插入或者更新操作        
	createEntry(hash, key, value, bucketIndex);    
}

我們通過 hash 值計算得到了 table 下表中的值,接着繼續分析 createEntry 方法

//數據操作,在鏈表頭部插入一個數組,這就是在一個鏈表頭部插入一個節點的過程。獲取table[i]的對象e,將table[i]的對象修改爲新增對象,讓新增對象的next指向e。
void createEntry(int hash, K key, V value, int bucketIndex) {
	//保存table[i]的對象爲e        
	HashMapEntry<K,V> e = table[bucketIndex];        
	//然後將table[i]的值修改爲新增的對象,並將新增對象的next值設置爲原先table[i]的值e,這就相當於在鏈表頭部插入一個數據了。        
	table[bucketIndex] = new HashMapEntry<>(hash, key, value, e);        
	size++;   
	}
}

分析完了 key 爲 null 的情況,接着我們分析一下 key 不等於 null 的情況。接着,我們通過 key來計算出 hash 的,然後根據 hash 的值來計算在 table 數組中的位置 int i = indexFor(hash, table.length);

//  indexFor返回hash值和table數組長度減1的與運算結果。爲什麼使用的是length-1?應爲這樣可以保證結果的最大值是length-1,不會產生數組越界問題。
static int indexFor(int h, int length) {
	return h & (length-1);   
}

我們找到了在數組 table 中的位置後,我們就開始遍歷當前位置中的鏈表了,如果存在 key 則覆蓋操作,不存在的話則在鏈表頭部插入一個數據。上面便是一個完整的增加數據的操作,在這裏我們進行概括一下中心思想。首先,我們判斷我們需要增加數據的 key 是否爲 null,如果爲 null 的話,則在數組的第一個位置即 table[0] 處進行插入或者更新操作(如果在第一個位置處的鏈表中存在 key 未 null 的數據,那麼則進行覆蓋更新操作,如果不存在的話,則在鏈表頭插入一個數據,就是常見的鏈表插入操作),接下來就走 key 不等於 null 這一步了,我們需要根據 key 來計算出 hash 的值,其次,需要根據 hash 的值來計算在 table 中的位置,得到了位置之後,我們重複進行在鏈表中的操作(找到就覆蓋更新,沒找到就在鏈表頭部插入一個數據),這就是 HashMap 的 put 操作。

我們一般對哈希表的散列很自然地會想到用 hash 值對 length 取模(即除法散列法), Hashtable 中也是這樣實現的,這種方法基本能保證元素在哈希表中散列的比較均勻,但取模會用到除法運算,效率很低,HashMap 中則通過 h&(length-1) 的方法來代替取模,同樣實現了均勻的散列,但效率要高很多,這也是 HashMap 對 Hashtable 的一個改進。
接下來,我們分析下爲什麼哈希表的容量一定要是 2 的整數次冪。首先,length 爲 2 的整數次冪的話,h&(length-1) 就相當於對 length 取模,這樣便保證了散列的均勻,同時也提升了效率;其次,length 爲 2 的整數次冪的話,爲偶數,這樣 length-1 爲奇數,奇數的最後一位是 1,這樣便保證了 h&(length-1) 的最後一位可能爲 0,也可能爲 1(這取決於h的值),即與後的結果可能爲偶數,也可能爲奇數,這樣便可以保證散列的均勻性,而如果 length 爲奇數的話,很明顯 length-1 爲偶數,它的最後一位是 0,這樣 h&(length-1) 的最後一位肯定爲 0,即只能爲偶數,這樣任何 hash 值都只會被散列到數組的偶數下標位置上,這便浪費了近一半的空間,因此,length 取 2 的整數次冪,是爲了使不同 hash 值發生碰撞的概率較小,這樣就能使元素在哈希表中均勻地散列。分析完了 put 方法,接下來我們分析一下 putAll 方法,

/**     
 * * @param m mappings to be stored in this map     
 * * @throws NullPointerException if the specified map is null     
 * */    
public void putAll(Map<? extends K, ? extends V> m) { 
	int numKeysToBeAdded = m.size();        
	if (numKeysToBeAdded == 0)            
		return;        
	if (table == EMPTY_TABLE) {
		inflateTable((int) Math.max(numKeysToBeAdded * loadFactor, threshold));        
		}        /*         */        
	if (numKeysToBeAdded > threshold) {  
		int targetCapacity = (int)(numKeysToBeAdded / loadFactor + 1);          
		if (targetCapacity > MAXIMUM_CAPACITY)                
			targetCapacity = MAXIMUM_CAPACITY;            
		int newCapacity = table.length;           
		while (newCapacity < targetCapacity)               
			newCapacity <<= 1;           
		if (newCapacity > table.length)                
			resize(newCapacity);        
		}		
	//遍歷m中的內容,然後調用put方法將元素添加到table數組中        
	for (Map.Entry<? extends K, ? extends V> e : m.entrySet())            
		put(e.getKey(), e.getValue());   
	}
}

遍歷的時候涉及到了 entrySet 方法,這個方法定義在 Map 接口中,HashMap 中也有實現。我們在 HashMap 構造函數有個方法叫 putAllForCreate 方法,分析一下內容。

private void putAllForCreate(Map<? extends K, ? extends V> m) {        
    for (Map.Entry<? extends K, ? extends V> e : m.entrySet())                            
    	putForCreate(e.getKey(), e.getValue());    
}

看這個方法名就知道大概意思,在 HashMap 初始化的時候將一個 map 全部賦值給初始值。代碼也是,先遍歷一下 Map,然後依次添加進去,我們進入 putForCreate 方法中查看具體源碼

private void putForCreate(K key, V value) {
	//獲取key的hash值        
	int hash = null == key ? 0 : sun.misc.Hashing.singleWordWangJenkinsHash(key);        
	//根據hash值來計算在table中的位置        
	int i = indexFor(hash, table.length);        
	//循環遍歷數組下標爲i的鏈表,如果存在則覆蓋更新,也就是上面計算出來的table中的位置        
	for (HashMapEntry<K,V> e = table[i]; e != null; e = e.next) {            
		Object k;            
		if (e.hash == hash &&                
				((k = e.key) == key || (key != null && key.equals(k)))) { 
			e.value = value;                
			return;            
			}       
		}		
	//當前數組中的位置鏈表中不存在,則在鏈表頭部插入一條數據        
	createEntry(hash, key, value, i);    
	}
}

該方法先計算需要添加的元素的 hash 值和在 table 數組中的索引 i。接着遍歷 table[i] 的鏈表,若有元素的 key 值與傳入 key 值相等,則替換 value,結束方法。若不存在 key 值相同的元素,則調用 createEntry 創建並添加元素。繼續進入 createEntry 方法中查看源碼

void createEntry(int hash, K key, V value, int bucketIndex) {
	HashMapEntry<K,V> e = table[bucketIndex];        
	table[bucketIndex] = new HashMapEntry<>(hash, key, value, e);        
	size++;   
}

該方法比較簡單,就是在鏈表頭部插入一條數據,putAllForCreate 方法類似於 put 方法。至此所有 put 相關操作都解釋完畢了。put 之外,另一個常用的操作就是 get,下面就來看 get 方法。

get方法

相比較於 put 方法,get 方法還是比較簡單的,我們來具體看下實現。

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

首先判斷 key 是否等於 null,等於的話那就從 getForNullKey() 中獲取 value,如果不等於的話,通過 getEntry 獲取。我們首先看等於 null 的情況。

private V getForNullKey() {
	if (size == 0) {
		return null;        
	}        
	for (HashMapEntry<K,V> e = table[0]; e != null; e = e.next) {
		if (e.key == null)                
			return e.value;        
		}        
	return null;    
}

如果當前 table 的大小爲 0 的話,那麼將返回 null,如果不爲空的話,那麼則在數組 table[0] 中的鏈表進行循環匹配,尋找 key 的 null 的鏈表節點,如果找到的話,則直接返回 value,否則的話返回 null,很簡單。接着我們分析 key 不等於 null 的情況,即進入 getEntry 方法中分析。

final Entry<K,V> getEntry(Object key) {
	if (size == 0) {
		return null;        
	}        
	int hash = (key == null) ? 0 : sun.misc.Hashing.singleWordWangJenkinsHash(key);        
	for (HashMapEntry<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 方法,所以這裏面的代碼基本類似,很簡單了,首先先獲取 hash 值,然後根據 hash 值獲取在 table 中的索引 table[i],其次根據這個數組中的位置來循環遍歷 table[i] 中的鏈表,判斷是否存在這個 entry 如果存在的話則返回 value,不存在則返回 null。以上便是HashMap 中比較重要的兩個方法,一個 put,一個 get,我們已經全部分析完了,接着我們分析一下其餘剩下來的方法。

remove方法

分析完了 put、get 方法,接着分析一下 remove 方法。

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

在 remove 方法中,調用了一個 removeEntryForKey 方法,我們接着看看,

final Entry<K,V> removeEntryForKey(Object key) {
	//如果數組爲空,直接返回null        
	if (size == 0) { 
		return null;        
	}        
	//獲取需要移除的key的hash值        
	int hash = (key == null) ? 0 : sun.misc.Hashing.singleWordWangJenkinsHash(key);        
	//獲取在數組table[i]中的索引值i        
	int i = indexFor(hash, table.length);        
	//保存當前數組中的第一個鏈表節點        
	HashMapEntry<K,V> prev = table[i];        
	HashMapEntry<K,V> e = prev;        
	while (e != null) {            
		HashMapEntry<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;    
	}
}

上面的這個過程就是先找到 table 數組中對應的索引,接着就類似於一般的鏈表的刪除操作,而且是單向鏈表刪除節點,很簡單。在 C 語言中就是修改指針,這個例子中就是將要刪除節點的前一節點的 next 指向刪除被刪除節點的 next 即可。

其餘方法

以上分析了 HashMap 的大部分代碼,還有一些比較不重要的方法我們一次性給出解釋,就不一一做分析了。

清空map

/**     
 * * 清空map     
 * */    
public void clear() {
	modCount++;        
	Arrays.fill(table, null); 
	size = 0;   
	} 

**判斷是否存在指定的value **

/**     
 * * 判斷是否存在指定的value     
 * *     
 * * @param value value whose presence in this map is to be tested     
 * * @return <tt>true</tt> if this map maps one or more keys to the     
 * *         specified value     
 * */    
public boolean containsValue(Object value) {
	if (value == null)            
		return containsNullValue();        
	HashMapEntry[] tab = table;        
	for (int i = 0; i < tab.length ; i++)            
		for (HashMapEntry e = tab[i] ; e != null ; e = e.next)
			if (value.equals(e.value))                    
				return true;        
	return false;    
}  

循環遍歷數組,判斷數組table[i]下面的鏈表是否存在value等於null的節點

/**     
 * * 循環遍歷數組,判斷數組table[i]下面的鏈表是否存在value等於null的節點     
 * */    
private boolean containsNullValue() {
	HashMapEntry[] tab = table;        
	for (int i = 0; i < tab.length ; i++)           
		for (HashMapEntry e = tab[i] ; e != null ; e = e.next)
			if (e.value == null)                    
				return true;        
	return false;    
}    

**針對HashMap的淺複製 **

/**    
 *  * 針對HashMap的淺複製     
 *  *     
 *  * @return a shallow copy of this map     
 *  */    
public Object clone() {
	HashMap<K,V> result = null;        
	try {            
		result = (HashMap<K,V>)super.clone();        
		} 
	catch (CloneNotSupportedException e) { 
		// assert false;        
		}       
	if (result.table != EMPTY_TABLE) { 
		result.inflateTable(Math.min(  (int) Math.min( size * Math.min(1 / loadFactor, 4.0f), 
				// we have limits...                    
				HashMap.MAXIMUM_CAPACITY),              
				table.length));        
		}        result.entrySet = null;
		result.modCount = 0;        
		result.size = 0;        
		result.init();       
		result.putAllForCreate(this);
		return result;    
		}
	}
}

以上便是 HashMap 大概的源碼,其中還涉及到 entrySet 的概念,這是其他集合框架也涉及到的,所以打算一起說,本篇暫未說明。敬請期待後面的文章。如有錯誤,懇請指正,謝謝!最後我們來簡單寫個測試,來學習一下 HashMap 的常用法。

package MapDemo;
import java.util.HashMap;
import java.util.Map;
public class HashMapTest {
	public static void main(String[] args) {
		
		Map<String, String> mHashMap=new HashMap<String,String>();
		mHashMap.put("A", "AAAAAAAA...");
		mHashMap.put("B", "BBBBBBBB...");
		mHashMap.put("C", "CCCCCCCC...");
		mHashMap.put("D", "DDDDDDDD...");
		mHashMap.put("E", "EEEEEEEE...");
		mHashMap.put("F", "FFFFFFFF...");
		mHashMap.put("G", "GGGGGGGG...");
		mHashMap.put("H", "HHHHHHHH...");
		mHashMap.put("I", "IIIIIIII...");
		
		//打印HashMap
		System.out.println("mHashMap: "+mHashMap);
		
		//打印HashMap的size
		System.out.println("mHashMap size is: "+mHashMap.size());
		
		//判斷HashMap中是否存在A的key,結果爲TRUE
		System.out.println("mHashMap is containsKey of A:"+ mHashMap.containsKey("A"));
		
		//判斷HashMap中是否存在IIIIIIII的value,結果爲FALSE
		System.out.println("mHashMap is containsValue of IIIIIIII:"+ mHashMap.containsValue("IIIIIIII"));
		
		//打印HashMap中的key
		System.out.print("the key of mHashMap is :");
		for (String string : mHashMap.keySet()) {
			System.out.print("   "+string);
		}
		System.out.println();
		
		//打印HashMap中的value
		System.out.print("the value of mHashMap is :");
		for (String string : mHashMap.values()) {
			System.out.print("   "+string);
		}
		System.out.println();
		//打印key-value集合
		System.out.println("key-value集合:");
		for (Map.Entry<String, String> entry : mHashMap.entrySet()) {
			System.out.println("key: "+entry.getKey()+" value: "+entry.getValue());
		}
	}
}

輸出結果

mHashMap: {A=AAAAAAAA..., B=BBBBBBBB..., C=CCCCCCCC..., D=DDDDDDDD..., E=EEEEEEEE..., F=FFFFFFFF..., G=GGGGGGGG..., H=HHHHHHHH..., I=IIIIIIII...}
mHashMap size is: 9
mHashMap is containsKey of A:true
mHashMap is containsValue of IIIIIIII:false
the key of mHashMap is :   A   B   C   D   E   F   G   H   I
the value of mHashMap is :   AAAAAAAA...   BBBBBBBB...   CCCCCCCC...   DDDDDDDD...   EEEEEEEE...   FFFFFFFF...   GGGGGGGG...   HHHHHHHH...   IIIIIIII...
key-value集合:
key: A value: AAAAAAAA...
key: B value: BBBBBBBB...
key: C value: CCCCCCCC...
key: D value: DDDDDDDD...
key: E value: EEEEEEEE...
key: F value: FFFFFFFF...
key: G value: GGGGGGGG...
key: H value: HHHHHHHH...
key: I value: IIIIIIII...

關於作者

專注於 Android 開發多年,喜歡寫 blog 記錄總結學習經驗,blog 同步更新於本人的公衆號,歡迎大家關注,一起交流學習~

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章