在經常使用的數據結構中,有鏈表,數組,哈希表,樹等幾種數據結構,因爲數組和鏈表是兩種極端(當數據量較大,且插入的位置靠前),數組插入的時候,須要進行arraycopy,所以空間上也是一種浪費,效率也會下降,而鏈表,若是去查找某個數據的時候,又須要從頭至尾的遍歷,所以,這兩種數據結構是存在一些缺陷的,可是不能否認是很是好用的兩種結構。java
因而爲了平衡,進行了一個折中的處理,採用 數組+鏈表 的結構進行存儲。node
爲何叫作哈希表呢,由於這裏面關鍵的存儲方法中使用了 hash算法。面試
以前想着細緻,不過本身讀了一遍,發現那算是囉嗦了,所以之後寫出比較關鍵的地方。算法
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
從代碼中能夠看出來實現的是map接口,而不是 ArrayList和linkedList 中的 List接口。所以咱們經常會問的一個題目就是,map 是否實現了Collection接口(Collection接口是List的父接口)?json
答案是沒有的。數組
實現了 Collection 接口的類有: ArrayList、LinkedList、HashSet、TreeSet 這幾個最多見的。固然還有其餘的,等寫徹底部的時候,我去偷一個族譜傳上來吧。緩存
/** * Holds cached entrySet(). Note that AbstractMap fields are used * for keySet() and values(). */ transient Set<Map.Entry<K,V>> entrySet;
以前只知道HashSet 用的 HashMap的數據結構,今天看到HashMap中又使用到了Set的接口。。。數據結構
有點迷。多線程
爲了方便理解,貼上英語吧,cached 應該是緩存的意思。。app
擁有緩存的entryset()
其實這個HashMap 中這個緩存我第一次看到,也不知道用法,之前歷來沒有注意過啊~~
transient int size;
表示大小。
/** * The next size value at which to resize (capacity * load factor). * * @serial */ // (The javadoc description is true upon serialization. // Additionally, if the table array has not been allocated, this // field holds the initial array capacity, or zero signifying // DEFAULT_INITIAL_CAPACITY.) int threshold;
這個應該是 下次擴容是的 大小吧。
一直覺得它擴容就直接擴了,原來還有記錄下一次的。。。。
/** * The load factor for the hash table. * * @serial */ final float loadFactor;
人稱 負載因子 。。。聽着很厲害的樣子,其實就是記錄,何時擴容,別名是一個閾值(yuzhi),之前居然一直覺得是fazhi。。。。就是當 size/table.length 若是大於或者等於 了 loadFactor ,就表明着達到了這個map的閾值,須要進行resize擴容了。(其實等因而否擴容,我不知道,瞎說的哈哈。)
/** * The default initial capacity - MUST be a power of two. */ static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
設定初始化長度的,若是咱們只new 一個HashMap,沒有指定長度,就會生成一個數組長度爲 16 的。
爲何不直接寫個16 。而非寫個 1<<4 (二進制 :000001 右移操做====二進制: 0010000 ===16)?
英語:默認的初始化長度, 必須是2的整數 冪 呢?
問題:爲何非得是2的 整數 冪 呢????這個跟 hash算法,跟put,get等方法都有關係,把答案寫到最後的總結吧。
/** * 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;
最大的容量,。
英語第三行: 必須是2的整數 冪,且 小於等於 1 而後 右移30位。
這個就是限制一下 HashMap底層數組的最大length 。
/** * The load factor used when none specified in constructor. */ static final float DEFAULT_LOAD_FACTOR = 0.75f;
默認的 負載因子 是 0.75 ,也就是說,當size 存到 四分之三的時候就進行擴容。
/** * The bin count threshold for using a tree rather than list for a * bin. Bins are converted to trees when adding an element to a * bin with at least this many nodes. The value must be greater * than 2 and should be at least 8 to mesh with assumptions in * tree removal about conversion back to plain bins upon * shrinkage. */ static final int TREEIFY_THRESHOLD = 8; /** * The bin count threshold for untreeifying a (split) bin during a * resize operation. Should be less than TREEIFY_THRESHOLD, and at * most 6 to mesh with shrinkage detection under removal. */ static final int UNTREEIFY_THRESHOLD = 6; /** * The smallest table capacity for which bins may be treeified. * (Otherwise the table is resized if too many nodes in a bin.) * Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts * between resizing and treeification thresholds. */ static final int MIN_TREEIFY_CAPACITY = 64;
這三個放一塊,其實我也不知道他們的做用是啥。。。。。先放着吧,不過 看 第二行
the bin。。 說一個 哈希表中的一個 經常使用術語: bucket 桶。
這個我也是在後面學到 多線程,currentHashMap中才看到的一個概念,爲了未來,提早說吧:
什麼是bucket?
bucket的英文解釋:
Hash table lookup operations are often O(n/m) (where n is the number of objects in the table and m is the number of buckets), which is close to O(1), especially when the hash function has spread the hashed objects evenly through the hash table, and there are more hash buckets than objects to be stored.
能夠這樣理解:
一個HASH的結果所對應的地址可存放兩個BUCKET。可解決HASH衝突。
更直觀的體現:
每個紅圈都是一個桶。
ps;是否 達到 根據 size 去衡量的,而不是 非空桶去 衡量的,沒有數據的是空桶好比 4 ,5,6,7。
transient Node<K,V>[] table; /** * Holds cached entrySet(). Note that AbstractMap fields are used * for keySet() and values(). */
底層的數據結構是一個Node數組,關於Node類:
** * Basic hash bin node, used for most entries. (See below for * TreeNode subclass, and in LinkedHashMap for its Entry subclass.) */ static class Node<K,V> implements Map.Entry<K,V> { //哈希值,具體的這幾個使用到哈希算法的集合中,哈希算法都重寫了,有區別的 final int hash; //key值,也就是你存儲對象的別名吧。暫時這麼理解,這個屬於惟一不可重複的。 final K key; //真正存你的數據的地方。 V value; //下一個節點的引用,由於哈希表是一個數組+鏈表的組合。 Node<K,V> next; // 構造方法。 Node(int hash, K key, V value, Node<K,V> next) { this.hash = hash; this.key = key; this.value = value; this.next = next; }
其實這個Node是一個 靜態內部類,是存在於HashMap中的,圖來自百度,圖侵刪。
(之前沒注意,此次看了看類,發現裏面的內部類好多啊!!!!!就寫個我知道的吧,其餘的真是用過可是沒去仔細看過,等看過再添加一下)
圖:
其實我感受這個圖還不是很準確吧,
感受0 -15 也應該存的有節點的。這個圖僅供 參考理解吧。
/** * Constructs an empty <tt>HashMap</tt> with the default initial capacity * (16) and the default load factor (0.75). */ public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted }
默認 初始化 第一次長度 是16,負載因子是 0.75,不過我也沒見他底層數組進行初始化啊~~
/** * 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); }
這個是能夠指定長度的構造方法,不過建議初始化 的長度 是2 的整數倍 。 負載因子是默認的 0.75。
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; this.threshold = tableSizeFor(initialCapacity); }
上個方法 調用的是這個方法。
這個方法 支持自定義長度 跟自定義 負載因子。
不過 仍是建議 長度 是2 的整數倍, 負載因子真的不建議去改,除非是大牛,本身去切身體驗過設麼時候resize最好。
public HashMap(Map<? extends K, ? extends V> m) { this.loadFactor = DEFAULT_LOAD_FACTOR; putMapEntries(m, false); }
這個是傳入一個 map 的子類,而後轉換成一個 HashMap, 我是沒怎麼用過。。
因爲 HashMap是以 Key----Value , 這種一對 一對出現的,因此put方法就必須傳入兩個參數。
謹記Key 是不容許重複的,Value能夠。
如下是 put方法的實現:
/** * 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) { return putVal(hash(key), key, value, false, true); }
在這個裏面,傳入兩個參數, 並且內部條用了 hash方法,跟 putVal方法。
首先介紹hash方法,比較核心的。
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
這個方法寫的很是簡潔,三目表達式。
首先判斷key 是否是 null, 若是是直接返回 0
若是不是 ,就進行運算。
咱們都知道,每一個類都是繼承自Object 方法,由於Object類中 寫了hashCode 方法。
代碼貼上:
public native int hashCode();
有的類重寫了此方法,好比String類中的
public int hashCode() { int h = hash; if (h == 0 && value.length > 0) { char val[] = value; for (int i = 0; i < value.length; i++) { h = 31 * h + val[i]; } hash = h; } return h; }
好比Integer 類中的 hashCode方法:
public int hashCode() { return Integer.hashCode(value); } public static int hashCode(int value) { return value; }
這都重寫了方法。
不過通常都是String 作Key 的吧。。
其餘的類就不列舉了,有興趣就本身瞅瞅,應該都有本身的寫法。
若是沒有衝寫,這個類 就會調用父類的 hashCode 方法, 最不濟就是Obejct 調用本地的 HashCode方法。
迴歸正題:
拿 String爲Key 來說解吧:
/** *將上面的代碼進行分步,更加便於理解 * */ int h=key.hashCode(); //這個用人話就是 h和h/2的16次方的數 進行 與運算 int hash= h^h>>>16; return hash;
爲何這麼算呢?
首先, 咱們知道 HashMap 是無序的,一樣是效率很是高的, 這是爲何呢,就是由於他實現它的存放的時候,不是像ArrayList 或 LinkedList 那樣子 obejct[size++] 或者 往表尾直接添加一個 節點。
他存放的位置 直接就是 找一個 數 , 做爲他的下標 存到底層的數組中,若是取的時候,直接再經過這個下標 進行取就好了。
那麼怎麼去 找這麼一個數字呢 ,那就是hash算法的事情了。
而後講解 hash算法爲何這麼寫:
咱們知道,底層是數組,要找一個 下標 存這個節點,固然那須要 這個 數字 還不能 大於 數組的長度,
用 hash%table.length 是最好的,獲得的數字確定不會大於 數組長。
首先我先打印幾個:
就打印2個吧,發現了吧,這個數字挺大的。。
若是你讓他去 %table.length , 估計 計算機 也是挺難受的。 這是 我大學 學哈希表的時候 求 index 的方法。)
個人方法求一個 index和用jdk的方法求一個index方法進行比較:
很厲害,很神奇,咱們都知道 計算機中的 位運算 是效率比較高的運算。
這樣子,很神奇 效果 一毛同樣。。
細心的同窗會發現, 這兩個圖的 11 行 hashCode&15
爲何是 15 呢? 第10行 %16 ?
這都是爲何呢?
這就不的不提 上面的 一直強調的 爲何 底層數組的長度是2的整數 冪
如今稍微總結和解釋:
知乎專欄: https://zhuanlan.zhihu.com/p/21673805
方法一: static final int hash(Object key) { //jdk1.8 & jdk1.7 int h; // h = key.hashCode() 爲第一步 取hashCode值 // h ^ (h >>> 16) 爲第二步 高位參與運算 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } 方法二:JDK1.8沒有這個方法 切記,切記,以前面試問我,我還回答有。。都記混了。。 static int indexFor(int h, int length) { //jdk1.7的源碼,jdk1.8沒有這個方法,可是實現原理同樣的 // jdk1.8中是在put的時候,直接使用的 h&(length-1)的,而不是寫了一個方法 return h & (length-1); //第三步 取模運算 }
這裏的Hash算法本質上就是三步:取key的hashCode值、高位運算、取模運算。
對於任意給定的對象,只要它的hashCode()返回值相同,那麼程序調用方法一所計算獲得的Hash碼值老是相同的。咱們首先想到的就是把hash值對數組長度取模運算,這樣一來,元素的分佈相對來講是比較均勻的。可是,模運算的消耗仍是比較大的,在HashMap中是這樣作的:調用方法二來計算該對象應該保存在table數組的哪一個索引處。
這個方法很是巧妙,它經過h & (table.length -1)來獲得該對象的保存位,而HashMap底層數組的長度老是2的n次方,這是HashMap在速度上的優化。當length老是2的n次方時,h& (length-1)運算等價於對length取模,也就是h%length,可是&比%具備更高的效率。
在JDK1.8的實現中,優化了高位運算的算法,經過hashCode()的高16位異或低16位實現的:(h = k.hashCode()) ^ (h >>> 16),主要是從速度、功效、質量來考慮的,這麼作能夠在數組table的length比較小的時候,也能保證考慮到高低Bit都參與到Hash的計算中,同時不會有太大的開銷。
求餘數的過程圖解:
咱們都知道 2的整數 冪 確定是 10 ,100,1000,10000 只有一個1 ,
對 上圖稍微改進一下:
當 n爲 2的整數冪的時候,
全部高位(就是在 n 這個 惟一 的 1 前面數確定能 整除 ):
而 1 的右邊的 四位 確定是除不盡的。
這樣,因此 1 位置 的 右邊 後四位,2進制 換成 10進制,就完美的是 餘數。
所以, 將 table.length -1 後 與 hash 進行 & 運算:
很快就 計算出來餘數。
跟取餘 的效果 一毛同樣,並且 速度快。
延伸: 當除數是2的 整數冪的時候, 求商,能夠直接使用 >> 右移計算,這也是比普通除法快的。
好比: 12231/8 == 12231>>3 8 是 2 的3次方, 所以右移3個,幾回方 就是左移幾回。
接下來是put 的 方法代碼:
/** * Implements Map.put and related methods * * @param hash hash for key * @param key the key * @param value the value to put * @param onlyIfAbsent if true, don't change existing value * @param evict if false, the table is in creation mode. * @return previous value, or null if none */ final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; //首先判斷table 是否是空,或者是否是爲null if ((tab = table) == null || (n = tab.length) == 0) //擴容,其實不明白爲何不初始化直接初始化好,而是用的時候才擴容。。。 n = (tab = resize()).length; // 判斷 獲得的 下表位置是否存在數據 if ((p = tab[i = (n - 1) & hash]) == null) //不存在就直接放入就行 tab[i] = newNode(hash, key, value, null); else { Node<K,V> e; K k; // 若是已經存在數據了,就去遍歷這個桶的 鏈表進行比較。 // 先判斷 這個點的 hash 是否同樣,若是同樣比較key 的地址,最後使用equals方法比較 //注意幾點:hash同樣,key不必定同樣(概率特小) // key同樣,hash確定同樣(計算方法同樣,確定同樣) // == 同樣,表示是同一對象,確定equals 同樣。 // 這裏面 有 短路 現象, 自查~ if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) //若是符合條件,就獲得這個 節點 的引用 e = p; else if (p instanceof TreeNode) //說實在,這個不是太明白爲何 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { //接下來是遍歷鏈表查找,表中的 key是否已經存在, for (int binCount = 0; ; ++binCount) { //先指向下一個節點,若是是null,就將 node 放入到這裏, //第一個節點已經判斷過了,這種方式相似do{}while()吧。 if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } // 判斷條件同上 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } //若是 e 是存在的,也就是說,key值已經 存過了 // 這就是 爲何 HashMap爲何不能存重複key 的緣由了! if (e != null) { // existing mapping for key //獲得老的值 V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) //新值直接覆蓋 e.value = value; //這個函數不知道。。暫時無論他吧 afterNodeAccess(e); return oldValue; } } ++modCount; if (++size > threshold) resize();//擴容方法 afterNodeInsertion(evict); return null; }
這個方法寫的賊多。。。。
接下來是擴容方法:
final Node<K,V>[] resize() { //記錄擴容前的Hash表 Node<K,V>[] oldTab = table; //記錄擴容前數組的長度 int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0; //這下面是判斷 擴容前的長度是不是一個 異常的長度 if (oldCap > 0) { //若是擴容前的長度超過了 規定的最大的長度 if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } //擴容前的長度 *2 是否 超過規定的最大值(這擴容也是每次都是 2被,初始化 是 16,每次都是2倍, // 擴容後仍是2的 整數冪 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // double threshold } // 這一堆 判斷 就不看了。。 else if (oldThr > 0) // initial capacity was placed in threshold newCap = oldThr; else { // zero initial threshold signifies using defaults newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } if (newThr == 0) { float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } threshold = newThr; @SuppressWarnings({"rawtypes","unchecked"}) //初始化一個table Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab; //判斷 擴容前的表是否是一個null 的,若是是null,就不用數據複製了。 if (oldTab != null) { for (int j = 0; j < oldCap; ++j) { Node<K,V> e; //若是 當前桶 不爲空 if ((e = oldTab[j]) != null) { //將桶制空,當全部桶都空,方便gc回收 oldTab[j] = null; if (e.next == null) //再次用hash值進行求一個 index newTab[e.hash & (newCap - 1)] = e; else if (e instanceof TreeNode) ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); else { // preserve order //若是當前桶 有一個鏈表 Node<K,V> loHead = null, loTail = null; Node<K,V> hiHead = null, hiTail = null; Node<K,V> next; do { next = e.next; //這裏爲何這麼處理??? if ((e.hash & oldCap) == 0) { //這個判斷若是 是否是 空,不是空,就接着組成一個鏈表 if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } else { if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); //把分好類的 放入到 新的table 數組中 //咱們發現 若是以前的 hash&length ==0 就放入newtable[j] if (loTail != null) { loTail.next = null; newTab[j] = loHead; } //咱們發現 若是以前的 hash&length !=0 就放入newtable[j+oldCap] if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } return newTab; }
resize 也是一個大工程
其實主要的沒什麼看的,就是 新new 一個 table[] 而後 進行數據轉移就好 。
擴容是 resize 前的2倍,這樣就能 是2的 整數冪 進行擴容了。
若是仔細看的 同窗, 會 有一個 地方 就是
我在裏面註釋的 這裏爲何處理??? 這應該就是數據轉移的
很神奇 啊。 稍微 研究了一下:
首先 上一個 比較神奇的圖:
再來一個:
你們發現沒:
就是 hashCode&16+hashCode&15(也就是代碼中的 j ,由於這個j就是表明 桶的 index)
就==== hashCode&31 這很強大啊 ,
接下來數學角度分析一波、
仍是以前的圖:
前 2 的 結果相加 就是 第三個。。。
難道 &運算還符合 結合律嗎???
有上面的圖 很清晰的看出來, 其實 (2n-1)&hash 就是 等於 是 (n&hash)+((n-1)&hash)
固然 n 確定是 2 的整數冪、、
不得不說, 這東西,很是厲害。
put方法就是核心,講解完了以後,後面的應該都不是問題:
根據key 進行 remove
public V remove(Object key) { Node<K,V> e; return (e = removeNode(hash(key), key, null, false, true)) == null ? null : e.value; }
remove 內層調用方法
final Node<K,V> removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable) { Node<K,V>[] tab; Node<K,V> p; int n, index; //判斷標是否是空, 同時判斷 表長是否是 大於 0, 還有 直接使用 hash算法求得桶的第一個是否是null if ((tab = table) != null && (n = tab.length) > 0 && (p = tab[index = (n - 1) & hash]) != null) { Node<K,V> node = null, e; K k; V v; //先判斷第一個點,判斷方法相同 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) node = p; else if ((e = p.next) != null) { //不知道什麼意思 if (p instanceof TreeNode) node = ((TreeNode<K,V>)p).getTreeNode(hash, key); else { //從第二個點開始 遍歷 判斷是否存在 相同key do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) { node = e; break; } p = e; } while ((e = e.next) != null); } } //若是找到了 這個節點(不是null),就進行移除。 if (node != null && (!matchValue || (v = node.value) == value || (value != null && value.equals(v)))) { if (node instanceof TreeNode) ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable); else if (node == p) tab[index] = node.next; else p.next = node.next; ++modCount; --size; afterNodeRemoval(node); return node; } } return null; }
public V get(Object key) { Node<K,V> e; return (e = getNode(hash(key), key)) == null ? null : e.value; }
內層調用的方法:
final Node<K,V> getNode(int hash, Object key) { Node<K,V>[] tab; Node<K,V> first, e; int n; K k; if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k)))) return first; if ((e = first.next) != null) { if (first instanceof TreeNode) return ((TreeNode<K,V>)first).getTreeNode(hash, key); do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } return null; }
get 方法是一樣的套路,都是 遍歷查找,用的判斷條件也都同樣,hash,下表的求法都大同小異,偷個懶,這些都不用去解釋了吧。
是否包含對應的key | public boolean containsKey(Object key) |
是否包含對應的value | public boolean containsValue(Object value) |
放入一個map的子類 | public void putAll(Map<? extends K, ? extends V> m) |
返回size | public int size() |
返回是否爲空 | public boolean isEmpty() |
其實 HashMap 沒有什麼神祕的。
底層就是 數組+鏈表
就是放入的時候 看個人解釋,put方法的流程,何時使用equals, 何時使用 hashCode,爲何擴容是2的冪?
到後來 看了 HashSet 的源碼後,還要知道,hash計算方法有什麼不一樣,爲何要重寫hash方法?
HashMap 爲何不能重複? (put 方法第一行就處理了,是null得 hash值是 0 )
大致的 知識點應該就是這些吧。若是有補充就告訴我~~歡迎~
-----------------------------------------------------------------------------
不保證代碼徹底正確,也不保證代碼是最優。
僅僅是根據本身的理解,寫出來直觀的代碼,方便理解。
錯誤請指出,感激涕零!