序言
本來是在講解List接口系列的集合,但是接下來我要講的是那個HashSet,要明白HashSet就必須先要明白HashMap,所以在此出穿插一篇hashMap的文章,為了更好的學習HashSet。個人感覺初次看HashMap源碼比較難,但是明白了,其實也不是很難,
--WZY
一、准備工作。
建議:先去看一下我的另一篇講解hashcode的文章,讓自己知道為什么使用hashcode值進行查詢會很快。如果你已經懂了hashcode的工作原理,那么就可以直接往下看了。http://www.cnblogs.com/whgk/p/6071617.html
1、鏈表散列
什么是鏈表散列呢?通過數組和鏈表結合在一起使用,就叫做鏈表散列。這其實就是hashmap存儲的原理圖。

2、hashMap的數據結構和存儲原理
HashMap的數據結構就是用的鏈表散列,大概是怎么存儲的呢?分兩步
1、HashMap內部有一個entry的內部類,其中有四個屬性,我們要存儲一個值,則需要一個key和一個value,存到map中就會先將key和value保存在這個Entry類創建的對象中。
//這里只看這一小部分,其他重點的在下面詳細解釋 static class Entry<K,V> implements Map.Entry<K,V> { final K key; //就是我們說的map的key V value; //value值,這兩個都不陌生 Entry<K,V> next;//指向下一個entry對象 int hash;//通過key算過來的你hashcode值。
物理模型就是這樣

2、構造好了entry對象,然后將該對象放入數組中,如何存放就是這hashMap的精華所在了。
大概的一個存放過程是:通過entry對象中的hash值來確定將該對象存放在數組中的哪個位置上,如果在這個位置上還有其他元素,則通過鏈表來存儲這個元素。

3、HashMap存放元素的大概過程?
通過key、value封裝成一個entry對象,然后通過key的值來計算該entry的hash值,通過entry的hash值和數組的長度length來計算出entry放在數組中的哪個位置上面,每次存放都是將entry放在第一個位置。在這個過程中,就是通過hash
3、loadFactor加載因子
loadFactor加載因子是控制數組存放數據的疏密程度,loadFactor越趨近於1,那么數組中存放的數據(entry)也就越多,也就越密,也就是會讓鏈表的長度增加,loadFactor越小,也就是趨近於0,那么數組中存放的數據也就越稀,也就是可能數組中每個位置上就放一個元素。那有人說,就把loadFactor變為1最好嗎,存的數據很多,但是這樣會有一個問題,就是我們在通過key拿到我們的value時,是先通過key的hashcode值,找到對應數組中的位置,如果該位置中有很多元素,則需要通過equals來依次比較鏈表中的元素,拿到我們的value值,這樣花費的性能就很高,如果能讓數組上的每個位置盡量只有一個元素最好,我們就能直接得到value值了,所以有人又會說,那把loadFactor變得很小不就好了,但是如果變得太小,在數組中的位置就會太稀,也就是分散的太開,浪費很多空間,這樣也不好,所以在hashMap中loadFactor的初始值就是0.75,一般情況下不需要更改它。
4、Size的意思?
size就是在該HashMap的實例中實際存儲的元素的個數
5、threshold的作用?
threshold = capacity * loadFactor,當Size>=threshold的時候,那么就要考慮對數組的擴增了,也就是說,這個的意思就是衡量數組是否需要擴增的一個標准。注意這里說的是考慮,因為實際上要擴增數組,除了這個size>=threshold條件外,還需要另外一個條件,這個就等在源碼中看吧。
6、什么是桶?
根據前面畫的HashMap存儲的數據結構圖,你這樣想,數組中每一個位置上都放有一個桶,每個桶里就是裝一個鏈表,鏈表中可以有很多個元素(entry),這就是桶的意思。也就相當於把元素都放在桶中。
7、capacity
這個就代表的數組的容量,也就是數組的長度,同時也是HashMap中桶的個數。默認值是16.
通過一張截圖和圖中的文字,來熟悉一下我們上面說的各種屬性,介紹這些屬性的博文:http://blog.csdn.net/fan2012huan/article/details/51087722

二、初識HashMap
慣例:查看HashMapAPI,申明一下,如果還暫時看不懂說的是什么意思,很正常,有疑惑的地方不用一直抓着不放,先看下面的源碼分析,然后再回過頭來看這個api文檔講的東西。就會發現pai說的東西就是我們源碼中看到的那樣。
//1、哈希表基於map接口的實現,這個實現提供了map所有的操作,並且提供了key和value可以為null,(HashMap和HashTable大致上市一樣的除了hashmap是異步的和允許key和value為null),
這個類不確定map中元素的位置,特別要提的是,這個類也不確定元素的位置隨着時間會不會保持不變。 Hash table based implementation of the Map interface. This implementation provides all of the optional map operations, and permits null values and the null key.
(The HashMap class is roughly equivalent to Hashtable, except that it is unsynchronized and permits nulls.) This class makes no guarantees as to the order of the map;
in particular, it does not guarantee that the order will remain constant over time.
//假設哈希函數將元素合適的分到了每個桶(其實就是指的數組中位置上的鏈表)中,則這個實現為基本的操作(get、put)提供了穩定的性能,迭代這個集合視圖需要的時間跟hashMap實例(key-value映射的數量)的容量(在桶中)
成正比,因此,如果迭代的性能很重要的話,就不要將初始容量設置的太高或者loadfactor設置的太低,【這里的桶,相當於在數組中每個位置上放一個桶裝元素】
This implementation provides constant-time performance for the basic operations (get and put), assuming the hash function disperses the elements properly among the buckets.
Iteration over collection views requires time proportional to the "capacity" of the HashMap instance (the number of buckets) plus its size (the number of key-value mappings
). Thus, it's very important not to set the initial capacity too high (or the load factor too low) if iteration performance is important.
//HashMap的實例有兩個參數影響性能,初始化容量(initialCapacity)和loadFactor加載因子,在哈希表中這個容量是桶的數量【也就是數組的長度】,一個初始化容量僅僅是在哈希表被創建時容量,在
容量自動增長之前加載因子是衡量哈希表被允許達到的多少的。當entry的數量在哈希表中超過了加載因子乘以當前的容量,那么哈希表被修改(內部的數據結構會被重新建立)所以哈希表有大約兩倍的桶的數量
An instance of HashMap has two parameters that affect its performance: initial capacity and load factor. The capacity is the number of buckets in the hash table,
and the initial capacity is simply the capacity at the time the hash table is created. The load factor is a measure of how full the hash table is allowed to get before
its capacity is automatically increased. When the number of entries in the hash table exceeds the product of the load factor and the current capacity, the hash table
is rehashed (that is, internal data structures are rebuilt) so that the hash table has approximately twice the number of buckets.
//通常來講,默認的加載因子(0.75)能夠在時間和空間上提供一個好的平衡,更高的值會減少空間上的開支但是會增加查詢花費的時間(體現在HashMap類中get、put方法上),當設置初始化容量時,應該考慮到map中會存放
entry的數量和加載因子,以便最少次數的進行rehash操作,如果初始容量大於最大條目數除以加載因子,則不會發生 rehash 操作。
As a general rule, the default load factor (.75) offers a good tradeoff between time and space costs. Higher values decrease the space overhead but increase the lookup
cost (reflected in most of the operations of the HashMap class, including get and put). The expected number of entries in the map and its load factor should be taken
into account when setting its initial capacity, so as to minimize the number of rehash operations. If the initial capacity is greater than the maximum number of
entries divided by the load factor, no rehash operations will ever occur.
//如果很多映射關系要存儲在 HashMap 實例中,則相對於按需執行自動的 rehash 操作以增大表的容量來說,使用足夠大的初始容量創建它將使得映射關系能更有效地存儲。
If many mappings are to be stored in a HashMap instance, creating it with a sufficiently large capacity will allow the mappings to be stored more efficiently than letting
it perform automatic rehashing as needed to grow the table
三、HashMap的繼承結構和實現的接口
繼承結構很簡單:上面就繼承了一個abstractMap,也就是用來減輕實現Map接口的編寫負擔

實現的接口:

Map<K,V>:在AbstractMap抽象類中已經實現過的接口,這里又實現,實際上是多余的。但每個集合都有這樣的錯誤,也沒過大影響
Cloneable:能夠使用Clone()方法
Serializable:能夠使之序列化
四、HashMap的構造方法
有四個構造方法,構造方法的作用就是記錄一下16這個數給threshold(這個數值最終會當作第一次數組的長度。),和初始化加載因子。注意,hashMap中table數組一開始就已經是個沒有長度的數組了。
static final Entry<?,?>[] EMPTY_TABLE = {}; /** * The table, resized as necessary. Length MUST Always be a power of two. */ transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;

HashMap()
/** * Constructs an empty <tt>HashMap</tt> with the default initial capacity * (16) and the default load factor (0.75). */ //看上面的注釋就已經知道,DEFAULT_INITIAL_CAPACITY=16,DEFAULT_LOAD_FACTOR=0.75 //初始化容量:也就是初始化數組的大小 //加載因子:數組上的存放數據疏密程度。 public HashMap() { this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR); }
HashMap(int)
/** * Constructs an empty <tt>HashMap</tt> with the specified initial * capacity and the default load factor (0.75). * * @param initialCapacity the initial capacity. * @throws IllegalArgumentException if the initial capacity is negative. */ public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); }
HashMap(int,float)
/** * Constructs an empty <tt>HashMap</tt> with the specified initial * capacity and load factor. * * @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) { if (initialCapacity < 0)//initialCapacity不能小於0 throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); if (initialCapacity > MAXIMUM_CAPACITY)//initialCapacity大於最大容量時 initialCapacity = MAXIMUM_CAPACITY; if (loadFactor <= 0 || Float.isNaN(loadFactor))//isNaN的作用是檢測loadFactor是否是一個浮點數 throw new IllegalArgumentException("Illegal load factor: " + loadFactor); this.loadFactor = loadFactor; //初始化loadFactor threshold = initialCapacity;//將threshold變為16,其實這里賦值為16沒什么用,只是單純的記錄一下這個16的值,好在后面講此值給數組的長度。后面再初始化數組長度的時候,就會把該值給重新賦值 init();//一個空的初始化方法 }
HashMap(Map<? extends K, ? extends V> m)
//將參數Map轉換為一個HashMap。
public HashMap(Map<? extends K, ? extends V> m) {
//根據m中的size來設置初始化容量為多少合適,如果(m.size()/DEFAULT_LOAD_FACTOR)+1>DEFAULT_INITLAL_CAPACITY那么久取(m.size()/DEFAULT_LOAD_FACTOR)+1為初始化容量,反之取默認的,而加載因子就一直是默認的0.75 this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1, DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
//增大table的容量的方法,table也就是鏈表散列的數組。threshold就是初始化的大小(m.size()/DEFAULT_LOAD_FACTOR)+1或者DEFAULT_INITLAL_CAPACITY16 inflateTable(threshold); putAllForCreate(m); }
inflateTable(threshold):這個方法只是在第一次對數組進行操作的時候,需要對數組進行增加來存儲元素,因為其中什么元素都沒有,就調用該方法。
/** * Inflates the table. */ //擴展table的功能 private void inflateTable(int toSize) { // Find a power of 2 >= toSize //返回一個大於等於 最接近toSize的2的冪數。 什么意思呢?2的3次方的冪數就是8.2的冪數就是每次都市2的幾次方,2的冪數有可能是1,2,4,8,16等。
比如toSize=16,則16的2的冪數就是2的4次方還是16,比如toSize=17,那么最接近他的2的冪數就是2的5次方,也就是32.
//這里不點進去看了,因為里面就是為了實現上面這個目的而寫的一些算法,有興趣的可以自己去讀一讀 int capacity = roundUpToPowerOf2(toSize); threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);//設置一個需要擴增數組的一個標准 上面也有說到這個變量,不清楚的回頭再看看
//將table數組大大小改變為capacity。 table = new Entry[capacity];
//初始化一個容量為capacity的哈希表,等用到的時候才真正初始化,返回值是boolean,先放一放。。。。。。。。。。 initHashSeedAsNeeded(capacity); }
五、常用的方法
介紹方法前,先看一下entry這個內部類,前面剛開始介紹了那么多,現在來看一下entry類中是如何構建一個什么樣的結構

、put(K,V) 這一個put方法,真是有一大堆,大家應該慢慢消化完。一步一步,不懂得就看我寫的注釋,看看到底做了些什么。
//添加元素
public V put(K key, V value) { if (table == EMPTY_TABLE) {//判斷是不是一個空的數組,也就是數組沒有長度,通過構造方法創建,還沒開始用,所以長度為0,這里就開始對數組進行增長。 inflateTable(threshold);//這個方法在第四個構造方法中介紹過,就是用來將數組變為大於等於threshold的2次冪。一開始threshold為16,那么根據算法,數組的開始長度也就是為16. } if (key == null)//這里可以看到,HashMap為什么可以使用null值了。 return putForNullKey(value);//將key為null的值存放到table中去。具體看下面putForNullKey的分析。 int hash = hash(key);//通過hash函數來將key轉換為一個hash值 int i = indexFor(hash, table.length);//通過這個方法,將key轉換來的hash值和數組的長度進行操作,得到在數組中的位置。 for (Entry<K,V> e = table[i]; e != null; e = e.next) {//在對應位置上加入新元素 Object k;
//遍歷這個桶中的元素,看有沒有相同的key值。 if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {//只有相同的hash值並且 (可能key相同但是Key的hashcode不同也算key一樣或者用equals比較得到相同)這說明里面有相同的key值。 V oldValue = e.value;//將老value用新value來替代。 e.value = value; e.recordAccess(this); return oldValue; } } modCount++; addEntry(hash, key, value, i);//增加元素,方法內部實現很簡單,就是將新增加的元素放第一位。而不是往后追加。 return null; }
putForNullKey
//key為null的元素,默認放在table數組中的第一個位置上。並且可以知道,如果第一個位置上有元素,則將原來的值覆蓋掉,如果第一個位置上沒有entry。那么就將自己放在第一個位置。 private V putForNullKey(V value) { for (Entry<K,V> e = table[0]; e != null; e = e.next) {//遍歷在數組第一個位置上的鏈表的每個元素(entry),其實就一個,因為null就一個。 if (e.key == null) {//如果發現有key為null的值,將現在的值賦值給原來key為null的value。 V oldValue = e.value; e.value = value; e.recordAccess(this);//一個空的方法。 return oldValue; } } modCount++; addEntry(0, null, value, 0);//上面的情況是有key為null的元素。現在這里是沒有key為null的元素,則要在第一個位置上放上自己。請看下面對這個方法的解析。 return null; }
addEntry
void addEntry(int hash, K key, V value, int bucketIndex) {
//增加元素前,看一下元素的個數是否大於等於了我們規定存放在數組中的個數(threshold=數組容量*加載因子,只是一個存放判定數組是否需要擴增標准的變量),並且在table這個指定位置上有元素,則對數組進行擴展
//前面一個條件成立,擴展數組,可以理解,為什么還要加上后面一個條件呢?原因是:我們希望盡量在每個數組的每個位置上只有一個元素是最好的,數組的容量是大於threshold的,也就是說size雖然到了要擴增的那個標准,
但是在數組中可能還是有很多位置上沒有放元素,所以在這些位置上增加元素是合理的,不需要擴增。只有等到在size達到了擴增的標准並且添加元素的位置上有別的元素的情況下,才進行闊增。 if ((size >= threshold) && (null != table[bucketIndex])) { resize(2 * table.length);//擴增數組,看下面的分析。 hash = (null != key) ? hash(key) : 0;//擴增完數組后,原來的那些參數就沒用了。需要重新計算,計算添加元素的hash值 bucketIndex = indexFor(hash, table.length);//通過hash值和數組的長度來計算出在數組中哪個位置 } createEntry(hash, key, value, bucketIndex);//如果沒有擴增,則直接用傳過來的參數進行創建entry,很簡單,將添加進入的元素放桶中的第一個元素,也就是數組對應位置就是該元素,然后把之后的元素給現在元素的next,具體可以看這個方法的源碼,很簡單 }
resize()
void resize(int newCapacity) { Entry[] oldTable = table;//將老的數組存放在oldTable中 int oldCapacity = oldTable.length;//老的數組容量 if (oldCapacity == MAXIMUM_CAPACITY) {//判斷老的容量 threshold = Integer.MAX_VALUE;//數組已經擴增到最大值了,所以將判定的標准增加到最大。 return; } Entry[] newTable = new Entry[newCapacity];//創建一個是原先兩倍大小的數組。 transfer(newTable, initHashSeedAsNeeded(newCapacity));//因為新數組的長度改變了,所以每個元素在新數組中的位置也會改變,所以需要將每個元素的key得到的hashcode值重新算一遍,放入新數組中 table = newTable; threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);//這里就可以知道threshold的真正作用了,就是上面說的,作為判定是否需要擴增數組的一個標准。 }
transfer()
void transfer(Entry[] newTable, boolean rehash) { int newCapacity = newTable.length; for (Entry<K,V> e : table) {//遍歷老數組中的每一個桶,其實就是遍歷數組的每一個位置。 while(null != e) {//遍歷桶中的元素。e==null的情況是在一個桶中的最后一個元素的next為指向null,或者一開始這個桶就是空的。則需要遍歷下一個桶。 Entry<K,V> next = e.next;//將e元素的下一個元素保存到next中。 if (rehash) {// e.hash = null == e.key ? 0 : hash(e.key);//將每個元素的hash值算出來,通過的是每個元素的key,這個算法感興趣的就點進去看。key和value為null的hash就為0,所以都在數組的第一個位置。 } int i = indexFor(e.hash, newCapacity);//通過每個元素的hash值和所在數組的長度,計算出放在數組中哪個位置,這里就揭示了一開始我們的疑惑,不知道通過hash值怎么得到對應數組中的位置。 e.next = newTable[i];//每次在桶中添加新的數據,都是把新數據放在開頭,舊數據放后面,這個桶就相當於是一個棧,先進去的就在最底層。 newTable[i] = e;//將自己放入數組中的對應位置 e = next;//桶中下一個元素。 } } }
indexFor()
static int indexFor(int h, int length) { // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2"; return h & (length-1);//通過與運算,將h的二進制,和length-1的二進制進行與運算得出的結果就是數組中的位置。 }
經過這個方法,我們可以知道以下幾點
1、構造方法中,並沒有初始化數組的大小,數組在一開始就已經被創建了,構造方法只做兩件事情,一個是初始化加載因子,另一個是用threshold記錄下數組初始化的大小。注意是記錄。
2、什么時候會擴增數組的大小?在put一個元素時先size>=threshold並且還要在對應數組位置上有元素,這才能擴增數組
3、對幾個重要的方法的實現了解其作用,
putForNullKey:在put時,先判斷可以是不是null值,是null值則用該方法進行處理
addEntry:增加元素的方法,其中會先判斷是不是需要擴增數組,
不需要則調用createEntry():將以擁有的四個屬性創建entry,並且做添加元素的邏輯代碼,在第一位添加,而不是在末尾追加
需要擴增調用resize():這里面就是擴增的操作,將數組擴增為原來的兩倍。擴增后,就需要使用transfer方法進行一些元素的移動,因為數組長度變化了,原來的元素就不會呆在原來的地方不動。
indexFor:算出元素在數組中的位置索引。
Remove
//通過key刪除entry並返回其value值, public V remove(Object key) { //通過removeEntryForKey來完成刪除功能 Entry<K,V> e = removeEntryForKey(key); //返回值。 return (e == null ? null : e.value); }
removeEntryForKey:里面代碼很簡單,就是找到key,然后將單鏈表的一些指向改一下。
final Entry<K,V> removeEntryForKey(Object key) { if (size == 0) {//看hashMap中有沒有值 return null; } int hash = (key == null) ? 0 : hash(key);//看key是不是為null,如果為null,就直接返回0,否則通過hash函數計算出hash值 int i = indexFor(hash, table.length);//得到在數組中的位置。 Entry<K,V> prev = table[i]; Entry<K,V> e = prev; while (e != null) {//開始遍歷桶中所有的元素,看有沒有該key值,這個下面,prev代表前一個元素、e代表當前要檢測的元素,next代表e的后一個元素,除了第一次prev=e,其他時候都市像前面這樣。 Entry<K,V> next = e.next;//next記下一個元素 Object k; if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) {//判斷是該key值, modCount++; size--;//要刪除元素了,size自減 if (prev == e)//只有在剛開始,自己處於第一個元素的時候,這個才會等於,其他情況prev代表的是刪除元素的前一個元素, table[i] = next;//如果是第一個元素,直接把桶中第一個元素指向next,在next中保存着原先的第二個元素,現在變為第一個元素了 else //刪除的就不是第一個元素,而是之后的,由於是單鏈表,就只需要改變一個指向引用,就是在要刪除的元素之前的元素的next指向要刪除的元素的next。 prev.next = next;//next:刪除元素之后的一個元素,prev:刪除元素的前一個元素,所以就有了這句話 e.recordRemoval(this);//一個空方法 return e; } prev = e;//記錄要刪除元素的前一個元素 e = next;//這個就是可能要刪除的元素。 } return e; }
get(key):通過key來找到對應的value值
//通過key獲得value,知道了hashMap的原理后,其實這些都市大同小異。 public V get(Object key) { if (key == null)//判斷是否為null return getForNullKey();//這個方法里太簡單了,做兩件事情,第一,如果size=0,返回null,反之到數組的第一個位置獲取null對應的value值,前提是有,沒有也返回null。 Entry<K,V> entry = getEntry(key);//通過key獲得entry對象,看一下里面是如何獲得的,我猜跟那個通過key刪除元素差不多。也還是先找到對應位置,然后遍歷鏈表。 return null == entry ? null : entry.getValue();//返回 }
getEntry
//和remove(key)這個方法的邏輯一樣,但是簡單得多,因為不用刪除,只需要找到然后返回entry對象即可 final Entry<K,V> getEntry(Object key) { if (size == 0) { return null; } int hash = (key == null) ? 0 : 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; }
個人感覺其他方法都是大同小異,沒有什么特別需要講解的了,知道了上面的原理,基本上已經沒有什么難度。
接下來看一下hashMap的迭代器有哪些特別的沒有?

發現四個迭代器內部類都市私有的,並沒有什么特別,HashMap對象不支持直接調用迭代器,但是可以獲得對象中所有的key集合(keySet)或者entrySet等,然后通過這些來調用迭代器獲得自己所有的key或者entry對象或者value值。
六、總結
我感覺這個hashMap花了我快一天的時間了,還是基礎太差,不懂得都要去翻閱資料。通過閱讀了HashMap源碼,看一下我們學到了什么東西。
1、對於有些人可能還有一個疑問,就是為什么在使用inflateTable的時候需要數組的長度大於等於 最接近指定大小的2的冪呢?
這個問題,是關於hash算法的問題了,這里推薦一篇博文,就可以幫助你理解好這個,http://blog.csdn.net/oqqYeYi/article/details/39831029
2、通過源碼的學習,hashMap是一個能快速通過key獲取到value值得一個集合,原因是內部使用的是hash查找值得方法
3、要知道hashMap是一個鏈表散列這樣一個數據結構
4、hashMap中的幾個變量要知道什么意思,比如加載因子等知道這些,才能看得懂源碼。
