Java集合類是非常重要的知識點(diǎn),其中HashMap、HashTable、ConcurrentHashMap最為重要。本文主要對HashMap進(jìn)行詳細(xì)的介紹以及總結(jié)一些面試中經(jīng)常問到的問題。
HashMap的內(nèi)部存儲(chǔ)結(jié)構(gòu)
Java中數(shù)據(jù)存儲(chǔ)方式最底層的兩種結(jié)構(gòu),一種是數(shù)組,另一種就是鏈表。
數(shù)組的特點(diǎn):連續(xù)空間,尋址迅速,但是在刪除或者添加元素的時(shí)候需要有較大幅度的移動(dòng),所以查詢速度快,增刪較慢。
鏈表的特點(diǎn):由于空間不連續(xù),尋址困難,增刪元素只需修改指針,所以查詢慢、增刪快。
有沒有一種數(shù)據(jù)結(jié)構(gòu)來綜合一下數(shù)組和鏈表,以便發(fā)揮他們各自的優(yōu)勢?答案是肯定的!就是:哈希表。哈希表具有較快(常量級)的查詢速度,及相對較快的增刪速度,所以很適合在海量數(shù)據(jù)的環(huán)境中使用。
HashMap的實(shí)現(xiàn)主要用到了哈希表的鏈地址法。即使用數(shù)組+鏈表的方式實(shí)現(xiàn)。
上圖是一個(gè)長度為16的數(shù)組中,每個(gè)元素存儲(chǔ)的是一個(gè)鏈表的頭結(jié)點(diǎn)。那么這些元素是按照什么樣的規(guī)則存儲(chǔ)到數(shù)組中呢。一般情況是通過hash(key)%len獲得,也就是元素的key的哈希值對數(shù)組長度取模得到。比如上述哈希表中,12%16=12 , 28%16=12, 108%16=12, 140%16=12。所以12、28、108以及140都存儲(chǔ)在數(shù)組下標(biāo)為12的位置。
它的內(nèi)部其實(shí)是用一個(gè)Entity數(shù)組來實(shí)現(xiàn)的,屬性有key、value、next。
HashMap重要方法詳細(xì)分析
-
初始化
一般使用new HashMap()方法初始化,我們先來看一下無參構(gòu)造方法的源代碼
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
table = new Entry[DEFAULT_INITIAL_CAPACITY];
init();
}
常量
static final int DEFAULT_INITIAL_CAPACITY = 16;
初始容量:16
static final int MAXIMUM_CAPACITY = 1 << 30;
最大容量:2的30次方 => 1073741824
static final float DEFAULT_LOAD_FACTOR = 0.75f;
負(fù)載因子:75%
上面還出現(xiàn)了一些變量,介紹一下這些重要變量
變量 | 術(shù)語 | 說明 |
---|---|---|
loadFactor | 負(fù)載因子 | HashMap大小負(fù)載因子,默認(rèn)為75% |
threshold | 臨界值 | HashMap大小達(dá)到臨界值,需要重新分配大小 |
Entry | 實(shí)體 | HashMap存儲(chǔ)對象的實(shí)際實(shí)體,由Key,value,hash,next組成 |
*modCount | 統(tǒng)一修改 | HashMap被修改或者刪除的次數(shù)總數(shù) |
HashMap中除了無參構(gòu)造方法,還有帶參數(shù)的構(gòu)造方法,我們也來看一下它的源代碼
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);
// Find a power of 2 >= initialCapacity
int capacity = 1;
while (capacity < initialCapacity)
capacity <<= 1;
this.loadFactor = loadFactor;
threshold = (int)(capacity * loadFactor);
table = new Entry[capacity];
init();
}
使用這個(gè)帶參數(shù)的構(gòu)造方法,我們就能指定初始時(shí)的table容量以及負(fù)載因子了。
-
put(Object key,Object value)方法
作用是存儲(chǔ)一個(gè)鍵-值對
/**
* Associates the specified value with the specified key in this map.
* If the map previously contained a mapping for the key, the old
* value is replaced.
*
* @param key key with which the specified value is to be associated
* @param value value to be associated with the specified key
* @return the previous value associated with <tt>key</tt>, or
* <tt>null</tt> if there was no mapping for <tt>key</tt>.
* (A <tt>null</tt> return can also indicate that the map
* previously associated <tt>null</tt> with <tt>key</tt>.)
*/
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;
}
處理步驟如下:
(1)判斷key是否為null,若為null,調(diào)用putForNullKey(value)處理。這個(gè)方法代碼如下:
/**
* Offloaded version of put for null keys
*/
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;
}
從代碼可以看出,如果key為null的值,默認(rèn)就存儲(chǔ)到table[0]開頭的鏈表了。然后遍歷table[0]的鏈表的每個(gè)節(jié)點(diǎn)Entry,如果發(fā)現(xiàn)其中存在節(jié)點(diǎn)Entry的key為null,就替換新的value,然后返回舊的value,如果沒發(fā)現(xiàn)key等于null的節(jié)點(diǎn)Entry,就增加新的節(jié)點(diǎn)。
(2)先計(jì)算key的hashcode,在使用計(jì)算的結(jié)果二次hash,使用indexFor(hash, table.length)方法找到Entry數(shù)組的索引i的位置。
(3)接著遍歷以table[i]為頭結(jié)點(diǎn)的鏈表,如果發(fā)現(xiàn)已經(jīng)存在節(jié)點(diǎn)的hash、key值與條件相同時(shí),將該節(jié)點(diǎn)的value值替換為新的value值,然后返回舊的value值。
(4)如果未找到hash、key值均相同的節(jié)點(diǎn),則調(diào)用addEntry方法增加新的節(jié)點(diǎn)(頭插法)。代碼如下:
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);
}
-
get(Object key)方法
作用是根據(jù)鍵來獲取值
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)))//-------------------1----------------
return e.value;
}
return null;
}
處理步驟如下:
(1)當(dāng)key為null時(shí),調(diào)用getForNullKey(),它的源碼如下:
private V getForNullKey() {
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null)
return e.value;
}
return null;
}
返回table[0]開頭的鏈表的鍵為null的節(jié)點(diǎn)的值
(2)當(dāng)鍵不為null時(shí),依然計(jì)算hash值,然后找到具體在哪個(gè)table[indexFor(hash, table.length)]節(jié)點(diǎn)開頭的鏈表中,遍歷此鏈表查找是否存在搜索條件中的key值,返回其value。若沒有符合條件的key值,返回null。
-
其他方法
HashMap其他方法,可以通過查看jdk源代碼中的java.util.HashMap.java了解,本文主要介紹最重要的幾個(gè)方法。
具體查看方法:
解壓jdk目錄下的src.zip文件
然后就可以查看大多數(shù)的源代碼了
HashMap常考問題總結(jié)
最后補(bǔ)充一些面試時(shí)候常問到的一些問題總結(jié)。
(1)HashMap和HashTable的區(qū)別?
- HashMap是非線程安全的,HashTable是線程安全的
- HashMap的鍵和值都允許有null值存在,而HashTable則不行
- 因?yàn)榫€程安全的問題,HashMap效率比HashTable的要高
- 哈希值的使用不同,HashMap要根據(jù)hashCode二次計(jì)算得到hash值,而HashTable直接使用對象的hashCode
- 繼承的父類不同,HashMap繼承自AbstractMap<K,V>,而HashTable繼承自Dictionary<K,V>
(2)HashMap中的鍵可以是任何對象或數(shù)據(jù)類型嗎?
- 可以為null,但是不能為可變對象。如果為可變對象的話,對象中的屬性改變則對象的hashCode也進(jìn)行了相應(yīng)的改變,導(dǎo)致下次無法查找到已存在Map中的數(shù)據(jù)。
- 如果可變對象在HashMap中被當(dāng)做鍵,那么就要小心在它的屬性改變時(shí),不要改變它的hashCode。只要保證成員變量的改變不會(huì)相應(yīng)改變其hashCode即可。
(3)HashTable如何實(shí)現(xiàn)線程安全?
實(shí)現(xiàn)原理是在對應(yīng)的方法上添加了synchronized關(guān)鍵字進(jìn)行修飾,由于在執(zhí)行此方法時(shí)需要獲得對象鎖,因此執(zhí)行起來比較慢。如果想實(shí)現(xiàn)線程安全的HashMap的話,推薦使用ConcurrentHashMap。
參考資料
[1] HashMap深度分析
http://www.lxweimin.com/p/8b372f3a195d
[2] [Java之美[從菜鳥到高手演變]之HashMap、HashTable] http://blog.csdn.net/zhangerqing/article/details/8193118