Java集合框架之 Java HashMap 源碼解析
繼上一篇文章Java集合框架綜述后,今天正式開始分析具體集合類的代碼,首先以既熟悉又陌生的HashMap開始。
簽名(signature)
- public class HashMap<K,V>
- extends AbstractMap<K,V>
- implements Map<K,V>, Cloneable, Serializable
可以看到HashMap
繼承了
-
標(biāo)記接口Cloneable,用于表明
HashMap
對象會重寫java.lang.Object#clone()
方法,HashMap實(shí)現(xiàn)的是淺拷貝(shallow copy)。 -
標(biāo)記接口Serializable,用于表明
HashMap
對象可以被序列化
比較有意思的是,HashMap
同時繼承了抽象類AbstractMap
與接口Map
,因?yàn)槌橄箢?code>AbstractMap的簽名為
- public abstract class AbstractMap<K,V> implements Map<K,V>
Stack Overfloooow上解釋到:
在語法層面繼承接口
Map
是多余的,這么做僅僅是為了讓閱讀代碼的人明確知道HashMap
是屬于Map
體系的,起到了文檔的作用
AbstractMap
相當(dāng)于個輔助類,Map
的一些操作這里面已經(jīng)提供了默認(rèn)實(shí)現(xiàn),后面具體的子類如果沒有特殊行為,可直接使用AbstractMap
提供的實(shí)現(xiàn)。
- <code>It's evil, don't use it. </code>
Cloneable
這個接口設(shè)計(jì)的非常不好,最致命的一點(diǎn)是它里面竟然沒有clone
方法,也就是說我們自己寫的類完全可以實(shí)現(xiàn)這個接口的同時不重寫clone
方法。
關(guān)于Cloneable
的不足,大家可以去看看《Effective Java》一書的作者給出的理由,在所給鏈接的文章里,Josh Bloch也會講如何實(shí)現(xiàn)深拷貝比較好,我這里就不在贅述了。
Map接口
在Eclipse中的outline面板可以看到Map
接口里面包含以下成員方法與內(nèi)部類:

可以看到,這里的成員方法不外乎是“增刪改查”,這也反映了我們編寫程序時,一定是以“數(shù)據(jù)”為導(dǎo)向的。
在上篇文章講了Map
雖然并不是Collection
,但是它提供了三種“集合視角”(collection views),與下面三個方法一一對應(yīng):
-
Set<K> keySet()
,提供key的集合視角 -
Collection<V> values()
,提供value的集合視角 -
Set<Map.Entry<K, V>> entrySet()
,提供key-value序?qū)Φ募弦暯?,這里用內(nèi)部類Map.Entry
表示序?qū)?/p>
AbstractMap抽象類
AbstractMap
對Map
中的方法提供了一個基本實(shí)現(xiàn),減少了實(shí)現(xiàn)Map
接口的工作量。
舉例來說:
如果要實(shí)現(xiàn)個不可變(unmodifiable)的map,那么只需繼承
AbstractMap
,然后實(shí)現(xiàn)其entrySet
方法,這個方法返回的set不支持add與remove,同時這個set的迭代器(iterator)不支持remove操作即可。相反,如果要實(shí)現(xiàn)個可變(modifiable)的map,首先繼承
AbstractMap
,然后重寫(override)AbstractMap
的put方法,同時實(shí)現(xiàn)entrySet
所返回set的迭代器的remove方法即可。
設(shè)計(jì)理念(design concept)
哈希表(hash table)
HashMap
是一種基于哈希表(hash table)實(shí)現(xiàn)的map,哈希表(也叫關(guān)聯(lián)數(shù)組)一種通用的數(shù)據(jù)結(jié)構(gòu),大多數(shù)的現(xiàn)代語言都原生支持,其概念也比較簡單:key經(jīng)過hash函數(shù)作用后得到一個槽(buckets或slots)的索引(index),槽中保存著我們想要獲取的值
,如下圖所示

很容易想到,一些不同的key經(jīng)過同一hash函數(shù)后可能產(chǎn)生相同的索引,也就是產(chǎn)生了沖突,這是在所難免的。
所以利用哈希表這種數(shù)據(jù)結(jié)構(gòu)實(shí)現(xiàn)具體類時,需要:
-
設(shè)計(jì)個好的hash函數(shù),使沖突盡可能的減少
-
其次是需要解決發(fā)生沖突后如何處理。
后面會重點(diǎn)介紹HashMap
是如何解決這兩個問題的。
HashMap的一些特點(diǎn)
-
線程非安全,并且允許key與value都為null值,
HashTable
與之相反,為線程安全,key與value都不允許null值。 -
不保證其內(nèi)部元素的順序,而且隨著時間的推移,同一元素的位置也可能改變(resize的情況)
-
put、get操作的時間復(fù)雜度為O(1)。
-
遍歷其集合視角的時間復(fù)雜度與其容量(capacity,槽的個數(shù))和現(xiàn)有元素的大小(entry的個數(shù))成正比,所以如果遍歷的性能要求很高, 不要把capactiy設(shè)置的過高或把平衡因子(load factor,當(dāng)entry數(shù)大于capacity*loadFactor時,會進(jìn)行resize,reside會導(dǎo)致key進(jìn)行rehash)設(shè)置的過 低。
-
由于HashMap是線程非安全的,這也就是意味著如果多個線程同時對一hashmap的集合試圖做迭代時有結(jié)構(gòu)的上改變(添加、刪除entry,只改變entry的value的值不算結(jié)構(gòu)改變),那么會報ConcurrentModificationException,專業(yè)術(shù)語叫
fail-fast
,盡早報錯對于多線程程序來說是很有必要的。 -
Map m = Collections.synchronizedMap(new HashMap(...));
通過這種方式可以得到一個線程安全的map。
源碼剖析
首先從構(gòu)造函數(shù)開始講,HashMap
遵循集合框架的約束,提供了一個參數(shù)為空的構(gòu)造函數(shù)與有一個參數(shù)且參數(shù)類型為Map的構(gòu)造函數(shù)。除此之外,還提供了兩個構(gòu)造函數(shù),用于設(shè)置HashMap
的容量(capacity)與平衡因子(loadFactor)。
- 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);
- this.loadFactor = loadFactor;
- threshold = initialCapacity;
- init();
- }
- public HashMap(int initialCapacity) {
- this(initialCapacity, DEFAULT_LOAD_FACTOR);
- }
- public HashMap() {
- this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
- }
- 從代碼上可以看到,容量與平衡因子都有個默認(rèn)值,并且容量有個***值
- /**
- * The default initial capacity - MUST be a power of two.
- */
- static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
- /**
- * The maximum capacity, used if a higher value is implicitly specified
- * by either of the constructors with arguments.
- * MUST be a power of two <= 1<<30.
- */
- static final int MAXIMUM_CAPACITY = 1 << 30;
- /**
- * The load factor used when none specified in constructor.
- */
- static final float DEFAULT_LOAD_FACTOR = 0.75f;
可以看到,默認(rèn)的平衡因子為0.75,這是權(quán)衡了時間復(fù)雜度與空間復(fù)雜度之后的***取值(JDK說是***的),過高的因子會降低存儲空間但是查找(lookup,包括HashMap中的put與get方法)的時間就會增加。
這里比較奇怪的是問題:容量必須為2的指數(shù)倍(默認(rèn)為16),這是為什么呢?解答這個問題,需要了解HashMap中哈希函數(shù)的設(shè)計(jì)原理。
哈希函數(shù)的設(shè)計(jì)原理
- /**
- * Retrieve object hash code and applies a supplemental hash function to the
- * result hash, which defends against poor quality hash functions. This is
- * critical because HashMap uses power-of-two length hash tables, that
- * otherwise encounter collisions for hashCodes that do not differ
- * in lower bits. Note: Null keys always map to hash 0, thus index 0.
- */
- final int hash(Object k) {
- int h = hashSeed;
- if (0 != h && k instanceof String) {
- return sun.misc.Hashing.stringHash32((String) k);
- }
- h ^= k.hashCode();
- // This function ensures that hashCodes that differ only by
- // constant multiples at each bit position have a bounded
- // number of collisions (approximately 8 at default load factor).
- h ^= (h >>> 20) ^ (h >>> 12);
- return h ^ (h >>> 7) ^ (h >>> 4);
- }
- /**
- * Returns index for hash code h.
- */
- 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);
- }
看到這么多位操作,是不是覺得暈頭轉(zhuǎn)向了呢,還是搞清楚原理就行了,畢竟位操作速度是很快的,不能因?yàn)椴缓美斫饩筒挥昧恕?/p>
網(wǎng)上說這個問題的也比較多,我這里根據(jù)自己的理解,盡量做到通俗易懂。
在哈希表容量(也就是buckets或slots大小)為length的情況下,為了使每個key都能在沖突最小的情況下映射到[0,length)
(注意是左閉右開區(qū)間)的索引(index)內(nèi),一般有兩種做法:
-
讓length為素?cái)?shù),然后用
hashCode(key) mod length
的方法得到索引 -
讓length為2的指數(shù)倍,然后用
hashCode(key) & (length-1)
的方法得到索引
HashTable用的是方法1,HashMap
用的是方法2。
因?yàn)楸酒黝}講的是HashMap,所以關(guān)于方法1為什么要用素?cái)?shù),我這里不想過多介紹,大家可以看這里。
重點(diǎn)說說方法2的情況,方法2其實(shí)也比較好理解:
因?yàn)閘ength為2的指數(shù)倍,所以
length-1
所對應(yīng)的二進(jìn)制位都為1,然后在與hashCode(key)
做與運(yùn)算,即可得到[0,length)
內(nèi)的索引
但是這里有個問題,如果hashCode(key)
的大于length
的值,而且hashCode(key)
的二進(jìn)制位的低位變化不大,那么沖突就會很多,舉個例子:
Java中對象的哈希值都32位整數(shù),而HashMap默認(rèn)大小為16,那么有兩個對象那么的哈希值分別為:
0xABAB0000
與0xBABA0000
,它們的后幾位都是一樣,那么與16異或后得到結(jié)果應(yīng)該也是一樣的,也就是產(chǎn)生了沖突。
造成沖突的原因關(guān)鍵在于16限制了只能用低位來計(jì)算,高位直接舍棄了,所以我們需要額外的哈希函數(shù)而不只是簡單的對象的hashCode
方法了。
具體來說,就是HashMap中hash
函數(shù)干的事了
首先有個隨機(jī)的hashSeed,來降低沖突發(fā)生的幾率
然后如果是字符串,用了
sun.misc.Hashing.stringHash32((String) k);
來獲取索引值***,通過一系列無符號右移操作,來把高位與低位進(jìn)行異或操作,來降低沖突發(fā)生的幾率
右移的偏移量20,12,7,4是怎么來的呢?因?yàn)镴ava中對象的哈希值都是32位的,所以這幾個數(shù)應(yīng)該就是把高位與低位做異或運(yùn)算,至于這幾個數(shù)是如何選取的,就不清楚了,網(wǎng)上搜了半天也沒統(tǒng)一且讓人信服的說法,大家可以參考下面幾個鏈接:
-
http://stackoverflow.com/questions/7922019/openjdks-rehashing-mechanism/7922219#7922219
-
http://stackoverflow.com/questions/9335169/understanding-strange-java-hash-function/9336103#9336103
HashMap.Entry
HashMap中存放的是HashMap.Entry對象,它繼承自Map.Entry,其比較重要的是構(gòu)造函數(shù)
- static class Entry<K,V> implements Map.Entry<K,V> {
- final K key;
- V value;
- Entry<K,V> next;
- int hash;
- Entry(int h, K k, V v, Entry<K,V> n) {
- value = v;
- next = n;
- key = k;
- hash = h;
- }
- // setter, getter, equals, toString 方法省略
- public final int hashCode() {
- //用key的hash值與上value的hash值作為Entry的hash值
- return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
- }
- /**
- * This method is invoked whenever the value in an entry is
- * overwritten by an invocation of put(k,v) for a key k that's already
- * in the HashMap.
- */
- void recordAccess(HashMap<K,V> m) {
- }
- /**
- * This method is invoked whenever the entry is
- * removed from the table.
- */
- void recordRemoval(HashMap<K,V> m) {
- }
- }
可以看到,Entry實(shí)現(xiàn)了單向鏈表的功能,用next
成員變量來級連起來。
介紹完Entry對象,下面要說一個比較重要的成員變量
/**
* The table, resized as necessary. Length MUST Always be a power of two.
*/
//HashMap內(nèi)部維護(hù)了一個為數(shù)組類型的Entry變量table,用來保存添加進(jìn)來的Entry對象
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
你也許會疑問,Entry不是單向鏈表嘛,怎么這里又需要個數(shù)組類型的table呢?
我翻了下之前的算法書,其實(shí)這是解決沖突的一個方式:鏈地址法(開散列法),效果如下:

就是相同索引值的Entry,會以單向鏈表的形式存在
鏈地址法的可視化
網(wǎng)上找到個很好的網(wǎng)站,用來可視化各種常見的算法,很棒。瞬間覺得國外大學(xué)比國內(nèi)的強(qiáng)不知多少倍。
下面的鏈接可以模仿哈希表采用鏈地址法解決沖突,大家可以自己去玩玩
get操作
get操作相比put操作簡單,所以先介紹get操作
- public V get(Object key) {
- //單獨(dú)處理key為null的情況
- 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;
- }
- //key為null的Entry用于放在table[0]中,但是在table[0]沖突鏈中的Entry的key不一定為null
- //所以需要遍歷沖突鏈,查找key是否存在
- for (Entry<K,V> e = table[0]; e != null; e = e.next) {
- if (e.key == null)
- return e.value;
- }
- return null;
- }
- final Entry<K,V> getEntry(Object key) {
- if (size == 0) {
- return null;
- }
- int hash = (key == null) ? 0 : hash(key);
- //首先定位到索引在table中的位置
- //然后遍歷沖突鏈,查找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;
- }
put操作(含update操作)
因?yàn)閜ut操作有可能需要對HashMap進(jìn)行resize,所以實(shí)現(xiàn)略復(fù)雜些
- private void inflateTable(int toSize) {
- //輔助函數(shù),用于填充HashMap到指定的capacity
- // Find a power of 2 >= toSize
- int capacity = roundUpToPowerOf2(toSize);
- //threshold為resize的閾值,超過后HashMap會進(jìn)行resize,內(nèi)容的entry會進(jìn)行rehash
- threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
- table = new Entry[capacity];
- initHashSeedAsNeeded(capacity);
- }
- /**
- * 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.
- */
- public V put(K key, V value) {
- if (table == EMPTY_TABLE) {
- inflateTable(threshold);
- }
- if (key == null)
- return putForNullKey(value);
- int hash = hash(key);
- int i = indexFor(hash, table.length);
- //這里的循環(huán)是關(guān)鍵
- //當(dāng)新增的key所對應(yīng)的索引i,對應(yīng)table[i]中已經(jīng)有值時,進(jìn)入循環(huán)體
- for (Entry<K,V> e = table[i]; e != null; e = e.next) {
- Object k;
- //判斷是否存在本次插入的key,如果存在用本次的value替換之前oldValue,相當(dāng)于update操作
- //并返回之前的oldValue
- if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
- V oldValue = e.value;
- e.value = value;
- e.recordAccess(this);
- return oldValue;
- }
- }
- //如果本次新增key之前不存在于HashMap中,modCount加1,說明結(jié)構(gòu)改變了
- modCount++;
- addEntry(hash, key, value, i);
- return null;
- }
- void addEntry(int hash, K key, V value, int bucketIndex) {
- //如果增加一個元素會后,HashMap的大小超過閾值,需要resize
- if ((size >= threshold) && (null != table[bucketIndex])) {
- //增加的幅度是之前的1倍
- 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) {
- //首先得到該索引處的沖突鏈Entries,有可能為null,不為null
- Entry<K,V> e = table[bucketIndex];
- //然后把新的Entry添加到?jīng)_突鏈的開頭,也就是說,后插入的反而在前面(***次還真沒看明白)
- //需要注意的是table[bucketIndex]本身并不存儲節(jié)點(diǎn)信息,
- //它就相當(dāng)于是單向鏈表的頭指針,數(shù)據(jù)都存放在沖突鏈中。
- table[bucketIndex] = new Entry<>(hash, key, value, e);
- size++;
- }
- //下面看看HashMap是如何進(jìn)行resize,廬山真面目就要揭曉了
- void resize(int newCapacity) {
- Entry[] oldTable = table;
- int oldCapacity = oldTable.length;
- //如果已經(jīng)達(dá)到***容量,那么就直接返回
- if (oldCapacity == MAXIMUM_CAPACITY) {
- threshold = Integer.MAX_VALUE;
- return;
- }
- Entry[] newTable = new Entry[newCapacity];
- //initHashSeedAsNeeded(newCapacity)的返回值決定了是否需要重新計(jì)算Entry的hash值
- transfer(newTable, initHashSeedAsNeeded(newCapacity));
- table = newTable;
- threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
- }
- /**
- * Transfers all entries from current table to newTable.
- */
- void transfer(Entry[] newTable, boolean rehash) {
- int newCapacity = newTable.length;
- //遍歷當(dāng)前的table,將里面的元素添加到新的newTable中
- 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];
- //***這兩句用了與put放過相同的技巧
- //將后插入的反而在前面
- newTable[i] = e;
- e = next;
- }
- }
- }
- /**
- * Initialize the hashing mask value. We defer initialization until we
- * really need it.
- */
- final boolean initHashSeedAsNeeded(int capacity) {
- boolean currentAltHashing = hashSeed != 0;
- boolean useAltHashing = sun.misc.VM.isBooted() &&
- (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
- //這里說明了,在hashSeed不為0或滿足useAltHash時,會重算Entry的hash值
- //至于useAltHashing的作用可以參考下面的鏈接
- // http://stackoverflow.com/questions/29918624/what-is-the-use-of-holder-class-in-hashmap
- boolean switching = currentAltHashing ^ useAltHashing;
- if (switching) {
- hashSeed = useAltHashing
- ? sun.misc.Hashing.randomHashSeed(this)
- : 0;
- }
- return switching;
- }
remove操作
- public V remove(Object key) {
- Entry<K,V> e = removeEntryForKey(key);
- //可以看到刪除的key如果存在,就返回其所對應(yīng)的value
- return (e == null ? null : e.value);
- }
- final Entry<K,V> removeEntryForKey(Object key) {
- if (size == 0) {
- return null;
- }
- int hash = (key == null) ? 0 : hash(key);
- int i = indexFor(hash, table.length);
- //這里用了兩個Entry對象,相當(dāng)于兩個指針,為的是防治沖突鏈發(fā)生斷裂的情況
- //這里的思路就是一般的單向鏈表的刪除思路
- Entry<K,V> prev = table[i];
- Entry<K,V> e = prev;
- //當(dāng)table[i]中存在沖突鏈時,開始遍歷里面的元素
- 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) //當(dāng)沖突鏈只有一個Entry時
- table[i] = next;
- else
- prev.next = next;
- e.recordRemoval(this);
- return e;
- }
- prev = e;
- e = next;
- }
- return e;
- }
到現(xiàn)在為止,HashMap的增刪改查都介紹完了。
一般而言,認(rèn)為HashMap的這四種操作時間復(fù)雜度為O(1),因?yàn)樗黨ash函數(shù)性質(zhì)較好,保證了沖突發(fā)生的幾率較小。
HashMap的序列化
介紹到這里,基本上算是把HashMap中一些核心的點(diǎn)講完了,但還有個比較嚴(yán)重的問題:保存Entry的table數(shù)組為transient的,也就是說在進(jìn)行序列化時,并不會包含該成員,這是為什么呢?
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
為了解答這個問題,我們需要明確下面事實(shí):
-
Object.hashCode方法對于一個類的兩個實(shí)例返回的是不同的哈希值
我們可以試想下面的場景:
我們在機(jī)器A上算出對象A的哈希值與索引,然后把它插入到HashMap中,然后把該HashMap序列化后,在機(jī)器B上重新算對象的哈希值與索引,這與機(jī)器A上算出的是不一樣的,所以我們在機(jī)器B上get對象A時,會得到錯誤的結(jié)果。
所以說,當(dāng)序列化一個HashMap對象時,保存Entry的table是不需要序列化進(jìn)來的,因?yàn)樗诹硪慌_機(jī)器上是錯誤的。
因?yàn)檫@個原因,HashMap重現(xiàn)了writeObject
與readObject
方法
- private void writeObject(java.io.ObjectOutputStream s)
- throws IOException
- {
- // Write out the threshold, loadfactor, and any hidden stuff
- s.defaultWriteObject();
- // Write out number of buckets
- if (table==EMPTY_TABLE) {
- s.writeInt(roundUpToPowerOf2(threshold));
- } else {
- s.writeInt(table.length);
- }
- // Write out size (number of Mappings)
- s.writeInt(size);
- // Write out keys and values (alternating)
- if (size > 0) {
- for(Map.Entry<K,V> e : entrySet0()) {
- s.writeObject(e.getKey());
- s.writeObject(e.getValue());
- }
- }
- }
- private static final long serialVersionUID = 362498820763181265L;
- private void readObject(java.io.ObjectInputStream s)
- throws IOException, ClassNotFoundException
- {
- // Read in the threshold (ignored), loadfactor, and any hidden stuff
- s.defaultReadObject();
- if (loadFactor <= 0 || Float.isNaN(loadFactor)) {
- throw new InvalidObjectException("Illegal load factor: " +
- loadFactor);
- }
- // set other fields that need values
- table = (Entry<K,V>[]) EMPTY_TABLE;
- // Read in number of buckets
- s.readInt(); // ignored.
- // Read number of mappings
- int mappings = s.readInt();
- if (mappings < 0)
- throw new InvalidObjectException("Illegal mappings count: " +
- mappings);
- // capacity chosen by number of mappings and desired load (if >= 0.25)
- int capacity = (int) Math.min(
- mappings * Math.min(1 / loadFactor, 4.0f),
- // we have limits...
- HashMap.MAXIMUM_CAPACITY);
- // allocate the bucket array;
- if (mappings > 0) {
- inflateTable(capacity);
- } else {
- threshold = capacity;
- }
- init(); // Give subclass a chance to do its thing.
- // Read the keys and values, and put the mappings in the HashMap
- for (int i = 0; i < mappings; i++) {
- K key = (K) s.readObject();
- V value = (V) s.readObject();
- putForCreate(key, value);
- }
- }
- private void putForCreate(K key, V value) {
- int hash = null == key ? 0 : hash(key);
- int i = indexFor(hash, table.length);
- /**
- * Look for preexisting entry for key. This will never happen for
- * clone or deserialize. It will only happen for construction if the
- * input Map is a sorted map whose ordering is inconsistent w/ equals.
- */
- for (Entry<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);
- }
簡單來說,在序列化時,針對Entry的key與value分別單獨(dú)序列化,當(dāng)反序列化時,再單獨(dú)處理即可。
總結(jié)
在總結(jié)完HashMap后,發(fā)現(xiàn)這里面一些核心的東西,像哈希表的沖突解決,都是算法課上學(xué)到,不過由于“年代久遠(yuǎn)”,已經(jīng)忘得差不多了,我覺得忘
-
一方面是由于時間久不用
-
另一方面是由于本身沒理解好
平時多去思考,這樣在遇到一些性能問題時也好排查。
還有一點(diǎn)就是我們在分析某些具體類或方法時,不要花太多時間一些細(xì)枝末節(jié)的邊界條件上,這樣很得不償失,倒不是說這么邊界條件不重要,程序的bug往往就是邊界條件沒考慮周全導(dǎo)致的。
只是說我們可以在理解了這個類或方法的總體思路后,再來分析這些邊界條件。
如果一開始就分析,那真是丈二和尚——摸不著頭腦了,隨著對它工作原理的加深,才有可能理解這些邊界條件的場景。