日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

hashmap put过程_看完还不懂HashMap算我输(附互联网大厂面试常见问题)

發(fā)布時(shí)間:2025/3/15 编程问答 28 豆豆
生活随笔 收集整理的這篇文章主要介紹了 hashmap put过程_看完还不懂HashMap算我输(附互联网大厂面试常见问题) 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

HashMap的原理與實(shí)現(xiàn)

版本之更迭:

–》JDK 1.7 : Table數(shù)組+ Entry鏈表;

–》JDK1.8 : Table數(shù)組+ Entry鏈表/紅黑樹;(為什么要使用紅黑樹?)

一問HashMap的實(shí)現(xiàn)原理

  • 你看過HashMap源碼嗎,知道底層的原理嗎
  • 為什么使用數(shù)組+鏈表
  • 用LinkedList代替數(shù)組可以嗎
  • 既然是可以的,為什么不用反而用數(shù)組。

重要變量介紹:

ps:都是重要的變量記憶理解一下最好。

  • DEFAULT_INITIAL_CAPACITY Table數(shù)組的初始化長度: 1 << 4 2^4=16(為什么要是 2的n次方?)
  • MAXIMUM_CAPACITY Table數(shù)組的最大長度: 1<<30 2^30=1073741824
  • DEFAULT_LOAD_FACTOR 負(fù)載因子:默認(rèn)值為0.75。 當(dāng)元素的總個(gè)數(shù)>當(dāng)前數(shù)組的長度 * 負(fù)載因子。數(shù)組會(huì)進(jìn)行擴(kuò)容,擴(kuò)容為原來的兩倍(todo:為什么是兩倍?)
  • TREEIFY_THRESHOLD 鏈表樹化闕值: 默認(rèn)值為 8 。表示在一個(gè)node(Table)節(jié)點(diǎn)下的值的個(gè)數(shù)大于8時(shí)候,會(huì)將鏈表轉(zhuǎn)換成為紅黑樹。
  • UNTREEIFY_THRESHOLD 紅黑樹鏈化闕值: 默認(rèn)值為 6 。 表示在進(jìn)行擴(kuò)容期間,單個(gè)Node節(jié)點(diǎn)下的紅黑樹節(jié)點(diǎn)的個(gè)數(shù)小于6時(shí)候,會(huì)將紅黑樹轉(zhuǎn)化成為鏈表。
  • MIN_TREEIFY_CAPACITY = 64 最小樹化閾值,當(dāng)Table所有元素超過該值,才會(huì)進(jìn)行樹化(為了防止前期階段頻繁擴(kuò)容和樹化過程沖突)。

實(shí)現(xiàn)原理:

實(shí)現(xiàn)原理圖 我們都知道,在HashMap中,采用數(shù)組+鏈表的方式來實(shí)現(xiàn)對數(shù)據(jù)的儲(chǔ)存。

HashMap采?Entry數(shù)組來存儲(chǔ)key-value對,每?個(gè)鍵值對組成了?個(gè)Entry實(shí)體,Entry類實(shí)際上是?個(gè)單向的鏈表結(jié) 構(gòu),它具有Next指針,可以連接下?個(gè)Entry實(shí)體。 只是在JDK1.8中,鏈表?度?于8的時(shí)候,鏈表會(huì)轉(zhuǎn)成紅?樹!

第一問: 為什么使用鏈表+數(shù)組:要知道為什么使用鏈表首先需要知道Hash沖突是如何來的:

答: 由于我們的數(shù)組的值是限制死的,我們在對key值進(jìn)行散列取到下標(biāo)以后,放入到數(shù)組中時(shí),難免出現(xiàn)兩個(gè)key值不同,但是卻放入到下標(biāo)相同的格子中,此時(shí)我們就可以使用鏈表來對其進(jìn)行鏈?zhǔn)降拇娣拧?/p>

第二問 我?LinkedList代替數(shù)組結(jié)構(gòu)可以嗎?

對于題目的意思是說,在源碼中我們是這樣的

Entry[] table=new Entry[capacity]; // entry就是一個(gè)鏈表的節(jié)點(diǎn)

現(xiàn)在進(jìn)行替換,進(jìn)行如下的實(shí)現(xiàn)

List<Entry> table=new LinkedList<Entry>();

是否可以行得通? 答案當(dāng)然是肯定的。

第三問 那既然可以使用進(jìn)行替換處理,為什么有偏偏使用到數(shù)組呢?

因?yàn)?數(shù)組效率最?! 在HashMap中,定位節(jié)點(diǎn)的位置是利?元素的key的哈希值對數(shù)組?度取模得到。此時(shí),我們已得到節(jié)點(diǎn)的位置。顯然數(shù)組的查 找效率?LinkedList?(底層是鏈表結(jié)構(gòu))。

那ArrayList,底層也是數(shù)組,查找也快啊,為啥不?ArrayList? 因?yàn)椴捎没緮?shù)組結(jié)構(gòu),擴(kuò)容機(jī)制可以??定義,HashMap中數(shù)組擴(kuò)容剛好是2的次冪,在做取模運(yùn)算的效率?。 ?ArrayList的擴(kuò)容機(jī)制是1.5倍擴(kuò)容(這一點(diǎn)我相信學(xué)習(xí)過的都應(yīng)該清楚),那ArrayList為什么是1.5倍擴(kuò)容這就不在本?說明了。

Hash沖突:得到下標(biāo)值:

我們都知道在HashMap中 使用數(shù)組加鏈表,這樣問題就來了,數(shù)組使用起來是有下標(biāo)的,但是我們平時(shí)使用HashMap都是這樣使用的:

HashMap<Integer,String> hashMap=new HashMap<>(); hashMap.put(2,"dd");

可以看到的是并沒有特地為我們存放進(jìn)來的值指定下標(biāo),那是因?yàn)槲覀兊膆ashMap對存放進(jìn)來的key值進(jìn)行了hashcode(),生成了一個(gè)值,但是這個(gè)值很大,我們不可以直接作為下標(biāo),此時(shí)我們想到了可以使用取余的方法,例如這樣:

key.hashcode()%Table.length;

即可以得到對于任意的一個(gè)key值,進(jìn)行這樣的操作以后,其值都落在0-Table.length-1 中,但是 HashMap的源碼卻不是這樣做?

它對其進(jìn)行了與操作,對Table的表長度減一再與生產(chǎn)的hash值進(jìn)行相與:

if ((p = tab[i = (n - 1) & hash]) == null)tab[i] = newNode(hash, key, value, null);

我們來畫張圖進(jìn)行進(jìn)一步的了解;

這里我們也就得知為什么Table數(shù)組的長度要一直都為2的n次方,只有這樣,減一進(jìn)行相與時(shí)候,才能夠達(dá)到最大的n-1值。

舉個(gè)栗子來反證一下:

我們現(xiàn)在 數(shù)組的長度為 15 減一為 14 ,二進(jìn)制表示 0000 1110 進(jìn)行相與時(shí)候,最后一位永遠(yuǎn)是0,這樣就可能導(dǎo)致,不能夠完完全全的進(jìn)行Table數(shù)組的使用。違背了我們最開始的想要對Table數(shù)組進(jìn)行最大限度的無序使用的原則,因?yàn)镠ashMap為了能夠存取高效,,要盡量較少碰撞,就是要盡量把數(shù)據(jù)分配均勻,每個(gè)鏈表?度?致相同。

此時(shí)還有一點(diǎn)需要注意的是: 我們對key值進(jìn)行hashcode以后,進(jìn)行相與時(shí)候都是只用到了后四位,前面的很多位都沒有能夠得到使用,這樣也可能會(huì)導(dǎo)致我們所生成的下標(biāo)值不能夠完全散列。

解決方案:將生成的hashcode值的高16位于低16位進(jìn)行異或運(yùn)算,這樣得到的值再進(jìn)行相與,一得到最散列的下標(biāo)值。

static final int hash(Object key) {int h;return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);}

二問講一講HashMap的get/put過程

  • 知道HashMap的put元素的過程是什么樣嗎?
  • 知道get過程是是什么樣嗎?
  • 你還知道哪些的hash算法?
  • 說一說String的hashcode的實(shí)現(xiàn)

Put方法

1.對key的hashCode()做hash運(yùn)算,計(jì)算index;

2.如果沒碰撞直接放到bucket?;

3.如果碰撞了,以鏈表的形式存在buckets后;

4.如果碰撞導(dǎo)致鏈表過?(?于等于TREEIFY_THRESHOLD),就把鏈表轉(zhuǎn)換成紅?樹(JDK1.8中的改動(dòng));

5.如果節(jié)點(diǎn)已經(jīng)存在就替換old value(保證key的唯?性)

6.如果bucket滿了(超過load factor*current capacity),就要resize

在得到下標(biāo)值以后,可以開始put值進(jìn)入到數(shù)組+鏈表中,會(huì)有三種情況:

  • 數(shù)組的位置為空。
  • 數(shù)組的位置不為空,且面是鏈表的格式。
  • 數(shù)組的位置不為空,且下面是紅黑樹的格式。
  • 同時(shí) 對于Key 和Value 也要經(jīng)歷一下步驟

    • 通過 Key 散列獲取到對于的Table;’
    • 遍歷Table 下的Node節(jié)點(diǎn),做更新/添加操作;
    • 擴(kuò)容檢測;
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {Node<K,V>[] tab; Node<K,V> p; int n, i;if ((tab = table) == null || (n = tab.length) == 0) // HashMap的懶加載策略,當(dāng)執(zhí)行put操作時(shí)檢測Table數(shù)組初始化。n = (tab = resize()).length;if ((p = tab[i = (n - 1) & hash]) == null) //通過``Hash``函數(shù)獲取到對應(yīng)的Table,如果當(dāng)前Table為空,則直接初始化一個(gè)新的Node并放入該Table中。 tab[i] = newNode(hash, key, value, null);else {Node<K,V> e; K k;//進(jìn)行值的判斷: 判斷對于是不是對于相同的key值傳進(jìn)來不同的value,若是如此,將原來的value進(jìn)行返回if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))e = p;else if (p instanceof TreeNode)// 如果當(dāng)前Node類型為TreeNode,調(diào)用 PutTreeVal 方法。e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);else { //如果不是TreeNode,則就是鏈表,遍歷并與輸入key做命中碰撞。 for (int binCount = 0; ; ++binCount) {if ((e = p.next) == null) {//如果當(dāng)前Table中不存在當(dāng)前key,則添加。p.next = newNode(hash, key, value, null);if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st//超過了``TREEIFY_THRESHOLD``則轉(zhuǎn)化為紅黑樹。treeifyBin(tab, hash);break;}if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k)))) //做命中碰撞,使用hash、內(nèi)存和equals同時(shí)判斷(不同的元素hash可能會(huì)一致)。break;p = e;}}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)//擴(kuò)容檢測!resize();afterNodeInsertion(evict);return null;}

    以上就是HashMap的Put操作,若是對其中的紅黑樹的添加,以及Node鏈表和紅黑樹的轉(zhuǎn)換過程我們暫時(shí)不進(jìn)行深入的討論,這個(gè)流程大概還是可以進(jìn)行理解,下面來深入討論擴(kuò)容問題。

    resise方法

    HashMap 的擴(kuò)容實(shí)現(xiàn)機(jī)制是將老table數(shù)組中所有的Entry取出來,重新對其Hashcode做Hash散列到新的Table中,可以看到注解Initializes or doubles table size. resize表示的是對數(shù)組進(jìn)行初始化或

    進(jìn)行Double處理。現(xiàn)在我們來一步一步進(jìn)行分析。

    /*** Initializes or doubles table size. If null, allocates in* accord with initial capacity target held in field threshold.* Otherwise, because we are using power-of-two expansion, the* elements from each bin must either stay at same index, or move* with a power of two offset in the new table.** @return the table*/final Node<K,V>[] resize() {//先將老的Table取別名,這樣利于后面的操作。Node<K,V>[] oldTab = table;int oldCap = (oldTab == null) ? 0 : oldTab.length;int oldThr = threshold;int newCap, newThr = 0;//表示之前的數(shù)組容量不為空。if (oldCap > 0) {// 如果 此時(shí)的數(shù)組容量大于最大值if (oldCap >= MAXIMUM_CAPACITY) {// 擴(kuò)容 闕值為 Int類型的最大值,這種情況很少出現(xiàn)threshold = Integer.MAX_VALUE;return oldTab;}//表示 old數(shù)組的長度沒有那么大,進(jìn)行擴(kuò)容,兩倍(這里也是有講究的)對闕值也進(jìn)行擴(kuò)容else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&oldCap >= DEFAULT_INITIAL_CAPACITY)newThr = oldThr << 1; // double threshold}//表示之前的容量是0 但是之前的闕值卻大于零, 此時(shí)新的hash表長度等于此時(shí)的闕值else if (oldThr > 0) // initial capacity was placed in thresholdnewCap = oldThr;else { // zero initial threshold signifies using defaults//表示是初始化時(shí)候,采用默認(rèn)的 數(shù)組長度* 負(fù)載因子newCap = DEFAULT_INITIAL_CAPACITY;newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);}//此時(shí)表示若新的闕值為0 就得用 新容量* 加載因子重新進(jìn)行計(jì)算。if (newThr == 0) {float ft = (float)newCap * loadFactor;newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?(int)ft : Integer.MAX_VALUE);}// 開始對新的hash表進(jìn)行相對應(yīng)的操作。threshold = newThr;@SuppressWarnings({"rawtypes","unchecked"})Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];table = newTab;if (oldTab != null) {//遍歷舊的hash表,將之內(nèi)的元素移到新的hash表中。for (int j = 0; j < oldCap/***此時(shí)舊的hash表的闕值*/; ++j) {Node<K,V> e;if ((e = oldTab[j]) != null) {//表示這個(gè)格子不為空oldTab[j] = null;if (e.next == null)// 表示當(dāng)前只有一個(gè)元素,重新做hash散列并賦值計(jì)算。newTab[e.hash & (newCap - 1)] = e;else if (e instanceof TreeNode)// 如果在舊哈希表中,這個(gè)位置是樹形的結(jié)果,就要把新hash表中也變成樹形結(jié)構(gòu),((TreeNode<K,V>)e).split(this, newTab, j, oldCap);else { // preserve order//保留 舊hash表中是鏈表的順序Node<K,V> loHead = null, loTail = null;Node<K,V> hiHead = null, hiTail = null;Node<K,V> next;do {// 遍歷當(dāng)前Table內(nèi)的Node 賦值給新的Table。next = e.next;// 原索引if ((e.hash & oldCap) == 0) {if (loTail == null)loHead = e;elseloTail.next = e;loTail = e;}// 原索引+oldCapelse {if (hiTail == null)hiHead = e;elsehiTail.next = e;hiTail = e;}} while ((e = next) != null);// 原索引放到bucket里面if (loTail != null) {loTail.next = null;newTab[j] = loHead;}// 原索引+oldCap 放到bucket里面if (hiTail != null) {hiTail.next = null;newTab[j + oldCap] = hiHead;}}}}}return newTab;}

    get方法

    1.對key的hashCode()做hash運(yùn)算,計(jì)算index;

    2.如果在bucket?的第?個(gè)節(jié)點(diǎn)?直接命中,則直接返回;

    3.如果有沖突,則通過key.equals(k)去查找對應(yīng)的Entry;

    4. 若為樹,則在樹中通過key.equals(k)查找,O(logn);

    5. 若為鏈表,則在鏈表中通過key.equals(k)查找,O(n)。

    在進(jìn)行取值時(shí)候,因?yàn)閷τ谖覀儌鬟M(jìn)來的key值進(jìn)行了一系列的hash操作,首先,在傳進(jìn)來 key值時(shí)候,先進(jìn)性hash操作,

    final Node<K,V> getNode(int hash, Object key) {Node<K,V>[] tab; Node<K,V> first, e; int n; K k;// 判斷 表是否為空,表重讀是否大于零,并且根據(jù)此 key 對應(yīng)的表內(nèi)是否存在 Node節(jié)點(diǎn)。 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))))// 檢查第一個(gè)Node 節(jié)點(diǎn),若是命中則不需要進(jìn)行do... whirle 循環(huán)。return first;if ((e = first.next) != null) {if (first instanceof TreeNode)//樹形結(jié)構(gòu),采用 對應(yīng)的檢索方法,進(jìn)行檢索。return ((TreeNode<K,V>)first).getTreeNode(hash, key);do {//鏈表方法 做while循環(huán),直到命中結(jié)束或者遍歷結(jié)束。if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))return e;} while ((e = e.next) != null);}}return null;}

    containsKey 方法

    根據(jù)get方法的結(jié)果,判斷是否為空,判斷是否包含該key

    public boolean containsKey(Object key) {return getNode(hash(key), key) != null;}

    還知道哪些hash算法

    先說?下hash算法?嘛的,Hash函數(shù)是指把?個(gè)?范圍映射到?個(gè)?范圍。把?范圍映射到?個(gè)?范圍的?的往往是為了 節(jié)省空間,使得數(shù)據(jù)容易保存。

    ?較出名的有MurmurHash、MD4、MD5等等

    String中hashcode的實(shí)現(xiàn)

    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;}

    String類中的hashCode計(jì)算?法還是?較簡單的,就是以31為權(quán),每?位為字符的ASCII值進(jìn)?運(yùn)算,??然溢出來等效 取模。

    哈希計(jì)算公式可以計(jì)為ss[[00]]3311^^((nn–11)) ++ ss[[11]]3311^^((nn–22)) ++ …… ++ ss[[nn–11]]

    那為什么以31為質(zhì)數(shù)呢? 主要是因?yàn)?1是?個(gè)奇質(zhì)數(shù),所以31i=32i-i=(i<<5)-i,這種位移與減法結(jié)合的計(jì)算相??般的運(yùn)算快很多

    三問 為什么hashmap的在鏈表元素?cái)?shù)量超過8時(shí)候改為紅黑樹

    • 知道jdk1.8中hashmap改了什么嗎。
    • 說一下為什么會(huì)出現(xiàn)線程的不安全性
    • 為什么在解決hash沖突時(shí)候,不直接用紅黑樹,而是先用鏈表,再用紅黑樹
    • 當(dāng)鏈表轉(zhuǎn)為紅黑樹,什么時(shí)候退化為鏈表

    第一問改動(dòng)了什么

    1.由數(shù)組+鏈表的結(jié)構(gòu)改為數(shù)組+鏈表+紅?樹。

    2. 優(yōu)化了?位運(yùn)算的hash算法:h^(h>>>16)

    3. 擴(kuò)容后,元素要么是在原位置,要么是在原位置再移動(dòng)2次冪的位置,且鏈表順序不變。

    注意: 最后?條是重點(diǎn),因?yàn)樽詈?條的變動(dòng),hashmap在1.8中,不會(huì)在出現(xiàn)死循環(huán)問題。

    HashMap的線程不安全性

    HashMap 在jdk1.7中 使用 數(shù)組加鏈表的方式,并且在進(jìn)行鏈表插入時(shí)候使用的是頭結(jié)點(diǎn)插入的方法。

    注 :這里為什么使用 頭插法的原因是我們?nèi)羰窃谏⒘幸院?#xff0c;判斷得到值是一樣的,使用頭插法,不用每次進(jìn)行遍歷鏈表的長度。但是這樣會(huì)有一個(gè)缺點(diǎn),在進(jìn)行擴(kuò)容時(shí)候,會(huì)導(dǎo)致進(jìn)入新數(shù)組時(shí)候出現(xiàn)倒序的情況,也會(huì)在多線程時(shí)候出現(xiàn)線程的不安全性。

    但是對與 jdk1.8 而言,還是要進(jìn)行闕值的判斷,判斷在什么時(shí)候進(jìn)行紅黑樹和鏈表的轉(zhuǎn)換。所以無論什么時(shí)候都要進(jìn)行遍歷,于是插入到尾部,防止出現(xiàn)擴(kuò)容時(shí)候還會(huì)出現(xiàn)倒序情況。

    所以當(dāng)在多線程的使用場景中,盡量使用線程安全的ConcurrentHashMap。至于Hashtable而言,使用效率太低。

    線程安全

    // 擴(kuò)容 void transfer(Entry[] newTable, boolean rehash) {int newCapacity = newTable.length;for (Entry<K,V> e : table) { // Awhile(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];newTable[i] = e;e = next; }

    }

    }

    在jdk1.7若是產(chǎn)生了多線程,例如 thread1,和thread2,同時(shí)想要進(jìn)入到 transfer中,此時(shí)會(huì)出現(xiàn)如下圖所示的情況:

    此時(shí)對于我們的1會(huì)擁有兩個(gè)臨時(shí)變量,我們稱為e1與e2。這個(gè)時(shí)候,線程一會(huì)先執(zhí)行上述的函數(shù),進(jìn)行數(shù)組的翻倍,并且,會(huì)進(jìn)入逆序的狀態(tài), 此時(shí)的 臨時(shí)變量e1和next1都已經(jīng)消失,但是對于每個(gè)節(jié)點(diǎn)上面所擁有的連接不會(huì)更改,這個(gè)時(shí)候,1上還有一個(gè)e2臨時(shí)變量,2上有一個(gè)next2臨時(shí)變量。如下圖所示:

    完成了線程一的擴(kuò)容以后,線程二也會(huì)創(chuàng)建一個(gè)屬于自己的數(shù)組,長度也是6。這個(gè)時(shí)候開始又執(zhí)行一遍以上的程序。

    // 第一遍過來

    e.next = newTable[i]; newTable[i] = e; e = next;

    此時(shí)完成了第一次的循環(huán)以后,進(jìn)入到以上的情況,這個(gè)時(shí)候 執(zhí)行e.next = newTable[i]; 寓意為: 2所表示的下一個(gè)指向 newTable[i],此時(shí)我們就發(fā)現(xiàn)了問題的所在,在執(zhí)行完第一遍循環(huán)以后,2所表示的下一下就已經(jīng)指向了 newTable[i],就是我們的1 ,當(dāng)然這樣我們就不用動(dòng),那我們就不動(dòng)就好了,然后完成以后就如下圖所示。

    // 第二遍來 e.next = newTable[i]; newTable[i] = e; e = next;

    這個(gè)時(shí)候開始第三次的循環(huán),首先執(zhí)行 Entry<K,V> next = e.next; ,這個(gè)時(shí)候我們就發(fā)現(xiàn)了問題,e2和e2的next2都執(zhí)行了1,這個(gè)時(shí)候我們再度,執(zhí)行以上的語句就會(huì)指向一個(gè)空的節(jié)點(diǎn),當(dāng)然空就空了,暫時(shí)也還不會(huì)出現(xiàn)差錯(cuò),但是執(zhí)行到 e.next = newTable[i];時(shí)候,會(huì)發(fā)現(xiàn),執(zhí)行到如下圖所示的情況。這個(gè)時(shí)候出現(xiàn)了循環(huán)鏈表,若是不加以控制,就會(huì)耗盡我們的cpu。

    第三問為什么不一開始就使用紅黑樹,不是效率很高嗎?

    因?yàn)榧t?樹需要進(jìn)?左旋,右旋,變?這些操作來保持平衡,?單鏈表不需要。

    當(dāng)元素?于8個(gè)當(dāng)時(shí)候,此時(shí)做查詢操作,鏈表結(jié)構(gòu)已經(jīng)能保證查詢性能。

    當(dāng)元素?于8個(gè)的時(shí)候,此時(shí)需要紅?樹來加快查 詢速度,但是新增節(jié)點(diǎn)的效率變慢了。

    因此,如果?開始就?紅?樹結(jié)構(gòu),元素太少,新增效率??較慢,?疑這是浪費(fèi)性能的。

    第四問什么時(shí)候退化為鏈表

    為6的時(shí)候退轉(zhuǎn)為鏈表。中間有個(gè)差值7可以防?鏈表和樹之間頻繁的轉(zhuǎn)換。

    假設(shè)?下,如果設(shè)計(jì)成鏈表個(gè)數(shù)超過8則鏈表轉(zhuǎn) 換成樹結(jié)構(gòu),鏈表個(gè)數(shù)?于8則樹結(jié)構(gòu)轉(zhuǎn)換成鏈表,

    如果?個(gè)HashMap不停的插?、刪除元素,鏈表個(gè)數(shù)在8左右徘徊,就會(huì) 頻繁的發(fā)?樹轉(zhuǎn)鏈表、鏈表轉(zhuǎn)樹,效率會(huì)很低。

    四問HashMap的并發(fā)問題

    • HashMap在并發(fā)環(huán)境下會(huì)有什么問題
    • 一般是如何解決的

    問題的出現(xiàn)

    (1)多線程擴(kuò)容,引起的死循環(huán)問題

    (2)多線程put的時(shí)候可能導(dǎo)致元素丟失

    (3)put?null元素后get出來的卻是null

    不安全性的解決方案

    在之前使用hashtable。 在每一個(gè)函數(shù)前面都加上了synchronized 但是 效率太低 我們現(xiàn)在不常用了。

    使用 ConcurrentHashmap函數(shù),對于這個(gè)函數(shù)而言 我們可以每幾個(gè)元素共用一把鎖。用于提高效率。

    五問你一般用什么作為HashMap的key值

    • key可以是null嗎,value可以是null嗎
    • 一般用什么作為key值
    • 用可變類當(dāng)Hashmap1的Key會(huì)有什么問題
    • 讓你實(shí)現(xiàn)一個(gè)自定義的class作為HashMap的Key該如何實(shí)現(xiàn)

    key可以是null嗎,value可以是null嗎

    當(dāng)然都是可以的,但是對于 key來說只能運(yùn)行出現(xiàn)一個(gè)key值為null,但是可以出現(xiàn)多個(gè)value值為null

    一般用什么作為key值

    ?般?Integer、String這種不可變類當(dāng)HashMap當(dāng)key,?且String最為常?。

    (1)因?yàn)樽址遣豢勺兊?#xff0c;所以在它創(chuàng)建的時(shí)候hashcode就被緩存了,不需要重新計(jì)算。 這就使得字符串很適合作為Map中的鍵,字符串的處理速度要快過其它的鍵對象。 這就是HashMap中的鍵往往都使?字符串。

    (2)因?yàn)楂@取對象的時(shí)候要?到equals()和hashCode()?法,那么鍵對象正確的重寫這兩個(gè)?法是?常重要的,這些類已 經(jīng)很規(guī)范的覆寫了hashCode()以及equals()?法。

    用可變類當(dāng)Hashmap1的Key會(huì)有什么問題

    hashcode可能會(huì)發(fā)生變化,導(dǎo)致put進(jìn)行的值,無法get出來,如下代碼所示:

    HashMap<List<String>,Object> map=new HashMap<>();List<String> list=new ArrayList<>();list.add("hello");Object object=new Object();map.put(list,object);System.out.println(map.get(list));list.add("hello world");System.out.println(map.get(list));

    輸出值如下:

    java.lang.Object@1b6d3586 null

    實(shí)現(xiàn)一個(gè)自定義的class作為Hashmap的key該如何實(shí)現(xiàn)

    對于這個(gè)問題考查到了下面的兩個(gè)知識(shí)點(diǎn)

    • 重寫hashcode和equals方法需要注意什么?
    • 如何設(shè)計(jì)一個(gè)不變的類。

    針對問題?,記住下?四個(gè)原則即可

    (1)兩個(gè)對象相等,hashcode?定相等

    (2)兩個(gè)對象不等,hashcode不?定不等

    (3)hashcode相等,兩個(gè)對象不?定相等

    (4)hashcode不等,兩個(gè)對象?定不等

    針對問題?,記住如何寫?個(gè)不可變類

    (1)類添加final修飾符,保證類不被繼承。 如果類可以被繼承會(huì)破壞類的不可變性機(jī)制,只要繼承類覆蓋?類的?法并且繼承類可以改變成員變量值,那么?旦?類 以?類的形式出現(xiàn)時(shí),不能保證當(dāng)前類是否可變。

    (2)保證所有成員變量必須私有,并且加上final修飾 通過這種?式保證成員變量不可改變。但只做到這?步還不夠,因?yàn)槿绻菍ο蟪蓡T變量有可能再外部改變其值。所以第4 點(diǎn)彌補(bǔ)這個(gè)不?。

    (3)不提供改變成員變量的?法,包括setter 避免通過其他接?改變成員變量的值,破壞不可變特性。

    (4)通過構(gòu)造器初始化所有成員,進(jìn)?深拷?(deep copy)

    (5) 在getter?法中,不要直接返回對象本?,?是克隆對象,并返回對象的拷? 這種做法也是防?對象外泄,防?通過getter獲得內(nèi)部可變成員對象后對成員變量直接操作,導(dǎo)致成員變量發(fā)?改變

    后記

  • 對于HashMap而言,擴(kuò)容是一個(gè)特別消耗內(nèi)存的操作。所以當(dāng)程序員在使用HashMap的時(shí)候,估算map的大小,初始化的時(shí)候給一個(gè)大致的數(shù)值,避免map進(jìn)行頻繁的擴(kuò)容。
  • 負(fù)載因子是可以修改的,也可以大于1,但是建議不要輕易修改,除非情況非常特殊。
  • HashMap是線程不安全的,不要在并發(fā)的環(huán)境中同時(shí)操作HashMap,建議使用ConcurrentHashMap。
  • 原文鏈接:https://blog.csdn.net/weixin_44015043/java/article/details/105346187

    總結(jié)

    以上是生活随笔為你收集整理的hashmap put过程_看完还不懂HashMap算我输(附互联网大厂面试常见问题)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。