第一次實(shí)習(xí)面試的時(shí)候,小房間里面試官拿著一本本子,上來就告訴我自己是做Java的,然后問了我很多Java基礎(chǔ),當(dāng)從HashMap如何使用到怎么實(shí)現(xiàn)的時(shí)候,我就兩眼懵逼了,當(dāng)問到安卓的Context是干什么的時(shí)候,我的內(nèi)心直接被擊中了。雖然寫了好幾個(gè)月的程序,卻只停留在會用的基礎(chǔ)上,而沒去知其所以然,太臉紅了。這次面試打開了我對程序語言執(zhí)著研究的大門,還是菜鳥的我感覺到能問出這種問題,還如此謙虛的人真是厲害,懷抱著大神帶我飛的心情,幾乎熱淚盈眶的我欣然帶著這份offer,準(zhǔn)備跟著大神大干一場。結(jié)果當(dāng)我入職的事后才知道,大神在我來的前幾天就去支付寶了...言歸正傳,我就整理八經(jīng)研究一下這個(gè)HashMap。
HashMap
HashMap是程序編寫過程中,再平常不過的一個(gè)集合類,通常通過他的put(key,value)
和get(key)
方法來存取數(shù)據(jù),數(shù)據(jù)存取的類型非常多樣,而且允許key
和value
為null
。直接上源碼:
HashMap
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>,
Cloneable, Serializable { ... }
AbstractMap
public abstract class AbstractMap<K,V> implements Map<K,V> { ... }
HashMap繼承于AbstractMap,Map定義了鍵值對的映射規(guī)則。AbstractMap內(nèi)部也也實(shí)現(xiàn)了Map接口,
這是為什么嘞?難道是為了強(qiáng)調(diào)嗎,或者是反射getInterfaces的時(shí)候能夠直接獲取到Map,這里不是和明白,后續(xù)再補(bǔ)吧。
構(gòu)造方法
HashMap的構(gòu)造方法四個(gè)
Ps: 全局變量中已經(jīng)指定默認(rèn)的加載因子0.75,初始容量16
public HashMap(int initialCapacity, float loadFactor)
<!--構(gòu)造一個(gè)帶指定初始容量和默認(rèn)加載因子(0.75)的空HashMap-->
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
<!--構(gòu)造一個(gè)具有默認(rèn)初始容量(16)和默認(rèn)加載因子(0.75)的空HashMap-->
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
public HashMap(Map<? extends K, ? extends V> m){
this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
}
殊途同歸,最終都指向了第一個(gè)構(gòu)造方法。嗯,我們來看一下。
public HashMap(int initialCapacity, float loadFactor)
這里有初始容量,加載因子兩個(gè)參數(shù),影響HashMap性能。
初始容量 是哈希表在創(chuàng)建時(shí)的容量。
加載因子 是哈希表在其容量自動增加之前可以達(dá)到多滿的一種尺度。當(dāng)哈希表中的條目數(shù)超出了加載因子與當(dāng)前容量的乘積時(shí),通過調(diào)用rehash 方法將容量翻倍。
以下內(nèi)容來自百度對于他們的理解:
比如說向水桶中裝水,此時(shí)HashMap就是一個(gè)桶, 這個(gè)桶的容量就是加載容量,而加載因子就是你要控制向這個(gè)桶中倒的水不超過水桶容量的比例,比如加載因子是0.75 ,那么在裝水的時(shí)候這個(gè)桶最多能裝到3/4 處,超過這個(gè)比例時(shí),桶會自動擴(kuò)容。因此,這個(gè)桶
最多能裝水 = 桶的容量 x 加載因子。
如果桶的容量是40,加載因子是0.75
那么你的桶最多能裝40 x 0.75 = 30的水,如果你裝了30的水還想繼續(xù)裝水,那么就該用大一點(diǎn)的桶,調(diào)用rehash就是負(fù)責(zé)增加桶的容量的方法。
數(shù)據(jù)結(jié)構(gòu)
Java中最常用的兩種結(jié)構(gòu)是數(shù)組和模擬指針(引用),幾乎所有的數(shù)據(jù)結(jié)構(gòu)都可以利用這兩種來組合實(shí)現(xiàn),HashMap也是如此。實(shí)際上HashMap是一個(gè)“鏈表散列”,網(wǎng)上扒取了一張結(jié)構(gòu)圖:
HashMap底層還是數(shù)組,只是數(shù)組的每一項(xiàng)都是一條鏈,initialCapacity 代表數(shù)組長度。
來吧,看看構(gòu)造函數(shù)的內(nèi)容:
public HashMap(int initialCapacity, float loadFactor) {
<!-- 初始容量必須大于或者等于0 -->
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
<!-- 初始容量不能 > 最大容量值,HashMap的最大容量值為2^30 -->
if (initialCapacity > MAXIMUM_CAPACITY) {
initialCapacity = MAXIMUM_CAPACITY;
} else if (initialCapacity < DEFAULT_INITIAL_CAPACITY) {
initialCapacity = DEFAULT_INITIAL_CAPACITY;
}
<!-- 加載因子不能小于0 -->
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
threshold = initialCapacity;
<!-- 這個(gè)方法留給子類重寫,里面什么都沒有 -->
init();
}
存儲數(shù)據(jù)
數(shù)據(jù)存儲用的是put(k,v)
的方式
public V put(K key, V value) {
// 如果表是空的 就新建一個(gè)HashTable
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
// key可以是null,調(diào)用putForNullKey方法,保存null與table第一個(gè)位置中
if (key == null)
return putForNullKey(value);
// 獲取key的哈希
int hash = sun.misc.Hashing.singleWordWangJenkinsHash(key);
// 計(jì)算key hash 值在 table 數(shù)組中的位置
int i = indexFor(hash, table.length);
// 迭代找出key的位置,為了得到 value
for (HashMapEntry<K, V> e = table[i]; e != null; e = e.next) {
Object k;
// 如果key重復(fù)了 返回老的value (所以put的結(jié)果是個(gè)object)
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
// 添加一次修改
modCount++;
// 將key、value添加至i位置處 替換
addEntry(hash, key, value, i);
return null;
}
計(jì)算哈希值的方法
static int hash(int h) {
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
我們知道對于HashMap的table而言,數(shù)據(jù)分布需要均勻(最好每項(xiàng)都只有一個(gè)元素,這樣就可以直接找到),不能太緊也不能太松,太緊會導(dǎo)致查詢速度慢,太松則浪費(fèi)空間。計(jì)算hash值后,怎么才能保證table元素分布均與呢?我們會想到取模,但是由于取模的消耗較大,HashMap是這樣處理的:調(diào)用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);
}
HashMap的底層數(shù)組長度總是2的n次方,在構(gòu)造函數(shù)中存在:capacity <<= 1;這樣做總是能夠保證HashMap的底層數(shù)組長度為2的n次方。當(dāng)length為2的n次方時(shí),h&(length - 1)就相當(dāng)于對length取模,而且速度比直接取模快得多,這是HashMap在速度上的一個(gè)優(yōu)化。
取數(shù)據(jù)
數(shù)據(jù)獲取用的是get(key)
的形式。
public V get(Object key) {
<!--key為空的情況下,內(nèi)容的獲取-->
if (key == null)
return getForNullKey();
Entry<K, V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
<!--獲取方法-->
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;
}
還有個(gè)比較重要的方法
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;
}
Entry是Map接口內(nèi)的一個(gè)接口,他的作用就是包裝一個(gè)map的節(jié)點(diǎn),這個(gè)節(jié)封裝了key,value,以及別的值(比如hashmap中的哈希碼和next指針),方便對Map的操作。
總結(jié)
由此看出,數(shù)據(jù)結(jié)構(gòu)基礎(chǔ)不好,看這個(gè)真費(fèi)勁。明白是明白,但是真叫你一點(diǎn)點(diǎn)寫出來,是想撲街的。
HashMap基于hashing原理,我們通過put()和get()方法儲存和獲取對象。當(dāng)我們將鍵值對傳遞給put()方法時(shí),它調(diào)用鍵對象的hashCode()方法來計(jì)算hashcode,然后找到bucket位置來儲存值對象。當(dāng)獲取對象時(shí),通過鍵對象的equals()方法找到正確的鍵值對,然后返回值對象。HashMap使用鏈表來解決碰撞問題,當(dāng)發(fā)生碰撞了,對象將會儲存在鏈表的下一個(gè)節(jié)點(diǎn)中。 HashMap在每個(gè)鏈表節(jié)點(diǎn)中儲存鍵值對對象。
當(dāng)兩個(gè)不同的鍵對象的hashcode相同時(shí)會發(fā)生什么? 它們會儲存在同一個(gè)bucket位置的鏈表中。鍵對象的equals()方法用來找到鍵值對。
因?yàn)镠ashMap的好處非常多,我曾經(jīng)在電子商務(wù)的應(yīng)用中使用HashMap作為緩存。因?yàn)榻鹑陬I(lǐng)域非常多的運(yùn)用Java,也出于性能的考慮,我們會經(jīng)常用到HashMap和ConcurrentHashMap。
尊重原創(chuàng),借鑒:http://www.cnblogs.com/chenssy/p/3521565.html