欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

HashMap原理及put方法與get方法的調(diào)用過程

 更新時間:2021年09月13日 11:53:13   作者:hansmall  
這篇文章主要介紹了HashMap原理及put方法與get方法的調(diào)用過程,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教

HashMap的原理

HashMap的數(shù)據(jù)結(jié)構(gòu)為數(shù)組+鏈表,以key,value的形式存值,通過調(diào)用put與get方法來存值與取值。

它內(nèi)部維護了一個Entry數(shù)組,得到key的hashCode值將其移位按位與運算,然后再通過跟數(shù)組的長度-1作邏輯與運算得到一個index值來確定數(shù)據(jù)存儲在Entry數(shù)組當(dāng)中的位置,通過鏈表來解決hash沖突問題。

當(dāng)發(fā)生碰撞了,對象將會儲存在鏈表的下一個節(jié)點中。

這里寫圖片描述

HashMap底層原理(當(dāng)你put,get時內(nèi)部會發(fā)生什么呢?)

接觸過HashMap的小伙伴都會經(jīng)常使用put和get這些方法,那接下來就對HashMap的內(nèi)部存儲進行詳解.(以初學(xué)者的角度進行分析)-(小白篇)

當(dāng)程序試圖將多個 key-value 放入 HashMap 中時,以如下代 碼片段為例:

上面代碼,創(chuàng)建了一個HashMap對象,并且指定了容量(capacity)和負載因子(loadFactor),然后put,以鍵值對的方式儲存值. 容量咱們很容易理解(默認16容量),也就是給它一個初始化的長度,那么負載因子又是個啥?

負載因子 : 表示HashMap滿的程度,默認值為0.75f,也就是說默認情況下,當(dāng)HashMap中元素個數(shù)達到了容量的3/4的時候就會進行自動擴容.(這里我把負載因子設(shè)置到0.9f,這么做的原因是想讓"效果"更明顯,啥效果,后面講解.) 具體擴容多少,源碼有這樣一段代碼如下:

在這里插入圖片描述

我們從這里可以知道閾(yu)值的計算公式:

閾值(threshold) = 負載因子(loadFactor) * 容量(capacity)

來,上源碼 如下:

在這里插入圖片描述

這是源碼的構(gòu)造函數(shù),來看看最后一行代碼用 tableSizeFor(initialCapacity) 方法來計算出閾值,

查看此方法源碼 如下:

在這里插入圖片描述cap

參數(shù)也就是給的初始容量,這段算法會給出一個 距離參數(shù)cap 最近的并且沒有變小的 2 的冪次方數(shù),比如傳入10 返回 16,就是這么神奇!

以上我們了解了HashMap的擴容機制,也知道了創(chuàng)建一個HashMap對象的內(nèi)部活動. 下面我們對put添加一個鍵值對的方法進行解析.

我們知道HashMap是以key-value的形式保存的,取用get()方法查找key來獲取相對應(yīng)的value. 我們可以調(diào)試put值時看出HashMap底層是用數(shù)組構(gòu)成的,并且存放的位置是散列無序的,這點不像數(shù)組按存放的先后順序來排列.如下圖:

在這里插入圖片描述

當(dāng)put完第4個值時發(fā)現(xiàn)只顯示了3個元素,之后一個個點開元素后發(fā)現(xiàn),第4個元素出現(xiàn)在next這個屬性中. 如下圖:

在這里插入圖片描述

然后繼續(xù)put完全部值,在看,一共存放了12個值,但是table中只有9個元素,還發(fā)現(xiàn)閾(yu)[ threshold ]值從最初的7增加到了15,容量(capacity)也從原來給的8變成了16,說明觸發(fā)了擴容機制(從源代碼可看到容量擴充至原來的二倍),在一個我們剛剛發(fā)現(xiàn)了有些值跑到了另一些值的next屬性里去了.我們點開元素的next屬性看看,是不是跑到這里頭了.如下圖:

在這里插入圖片描述

果然,這三倆跑人家的底盤來了.在下標(biāo) 7,13,14中的Next的屬性中找到了"遺失"的三個元素.

在看如下圖:

在這里插入圖片描述

仔細瞅瞅,發(fā)現(xiàn)每個元素都有這么一個next的屬性,有些為空,有些不為空,不為空的則是元素存放在此next中,有沒有感覺元素被next屬性組成了一條鏈子.來上圖(形象又生動):

在這里插入圖片描述

此圖模擬了內(nèi)部的結(jié)構(gòu)方式,在同一下標(biāo)中同時存在多個元素,產(chǎn)生了鏈表結(jié)構(gòu)圖中的箭頭也就表示著每個元素中的next屬性,看到這會發(fā)現(xiàn)許多詭異所思的問題, 為啥它存儲是無序的呢? , 為啥存著存著都跑到一塊去了,成了鏈表結(jié)構(gòu)呢?,等一些問題.咱們下面通過源碼來看看.(源碼如下):

/**
     * 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);
    }

既然叫HashMap,當(dāng)然得和Hash扯點關(guān)系啦.

HashMap 采用一種所謂的“Hash 算法”來決定每個元素的存儲位置。當(dāng)程序執(zhí)行 map.put(String,Obect)方法 時,系統(tǒng)將調(diào)用String的 hashCode() 方法得到其 hashCode 值——每個 Java 對象都有 hashCode() 方法,都可通過該方法獲得它的 hashCode 值。得到這個對象的 hashCode 值之后,系統(tǒng)會根據(jù)該 hashCode 值來決定該元素的存儲位置。小伙伴可以試試調(diào)用hashCode()方法看看經(jīng)過此算法會得出怎樣的結(jié)果.

咱們現(xiàn)在知道為啥是無序存放的了,key通過哈希算法的值來決定它存儲的位置,那出現(xiàn)的重疊現(xiàn)象表明,不同的key經(jīng)過哈希算法得出的值會出現(xiàn)相等的可能(這樣的現(xiàn)狀稱為碰撞/沖突),所以一個下標(biāo)會出現(xiàn)多個元素,形成鏈表結(jié)構(gòu).至于為什么采用鏈表,是為了節(jié)省空間,鏈表在內(nèi)存中并不是連續(xù)存儲,所以我們可以更充分地使用內(nèi)存。

(下面我們將每個下標(biāo)統(tǒng)稱為Entry(桶),也就是一個 key-value 對)

有沒有覺得這樣會降低查詢的效率(鏈表),進行查詢時,先查找到Entry,在通過鏈的遍歷.想著都覺得麻煩,雖然這樣解決了碰撞這樣的沖突,但是引來了一個大毛病(查找效率降低),這得不行啊,人家HashMap同志就是以快出名啊,所以在jdk8中進行了優(yōu)化, 引入了樹結(jié)構(gòu),在鏈表長度大于8的時候,將后面的數(shù)據(jù)存在紅黑樹中,以加快檢索速度,,來優(yōu)化 鏈 過長所帶來的性能低化的問題.

來上碼,繼續(xù)查看putVal(hash(key), key, value, false, true); 的源碼:

/**
     * 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;
        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;
            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 {
                for (int binCount = 0; ; ++binCount) {
                    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;
                }
            }
            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;
    }

咱們看完注釋應(yīng)該了解它的大概了,繼續(xù)查看treeifyBin()將鏈表改為紅黑樹 (jdk8新特性)方法碼:

/**
     * Replaces all linked nodes in bin at index for given hash unless
     * table is too small, in which case resizes instead.
     */
    final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        // 如果數(shù)組等于null 或 數(shù)組長度小于 64
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        // 重新散列,使得鏈表變短
            resize();
        // 如果hash沖突,且數(shù)組長度大于 64,則只能使用紅黑樹結(jié)構(gòu)
        else if ((e = tab[index = (n - 1) & hash]) != null) {
            TreeNode<K,V> hd = null, tl = null;
             // 返回新的紅黑樹
            do {
                TreeNode<K,V> p = replacementTreeNode(e, null);
                if (tl == null)
                    hd = p;
                else {
                    p.prev = tl;
                    tl.next = p;
                }
                tl = p;
            } while ((e = e.next) != null);
            if ((tab[index] = hd) != null)
                hd.treeify(tab);
        }
    }

以上介紹了MashMap對存儲數(shù)據(jù)的機制進行簡短的介紹.我們已經(jīng)知道產(chǎn)生碰撞會導(dǎo)致查詢效率打折扣,那么如何能有效的避免哈希碰撞呢?

咱們先反向思維一下,你認為什么情況會導(dǎo)致HashMap的哈希碰撞比較多?

無外乎兩種情況:

1、容量太小。容量小,碰撞的概率就高了。狼多肉少,就會發(fā)生爭強。

2、hash算法不夠好。算法不合理,就可能都分到同一個或幾個桶中。分配不均,也會發(fā)生爭搶。

所以,解決HashMap中的哈希碰撞也是從這兩方面入手。

這兩點在HashMap中都有很好的提現(xiàn)。兩種方法相結(jié)合,在合適的時候擴大數(shù)組容量,再通過一個合適的hash算法計算元素分配到哪個數(shù)組中,就可以大大的減少沖突的概率。但數(shù)據(jù)量大時,碰撞也會成正比的增長,所以引入紅黑樹的結(jié)構(gòu),就能避免查詢效率低下的問題。

咱們再來看看負載因子這個影響性能的平衡點有啥規(guī)律.上文已經(jīng)對啥是負載因子進行了解釋.

它Hsah表中元素的填滿的程度.

若:加載因子越大,填滿的元素越多,好處是,空間利用率高了,但:沖突的機會加大了.鏈表長度會越來越長,查找效率降低。

反之,加載因子越小,填滿的元素越少,好處是:沖突的機會減小了,但:空間浪費多了.表中的數(shù)據(jù)將過于稀疏(很多空間還沒用,就開始擴容了)

沖突的機會越大,則查找的成本越高.

因此,必須在 "沖突的機會"與"空間利用率"之間尋找一種平衡與折衷. 這種平衡與折衷本質(zhì)上是數(shù)據(jù)結(jié)構(gòu)中有名的"時-空"矛盾的平衡與折衷.

這里寫了段測試代碼 如下:

public class HashTest {
 public static void main(String[] args) {
  // 對"負載因子的大小對程序的影響規(guī)律"進行測試
  // threshold=capacity * loadFactor ---- 閾值 = 容量 x 負載因子
  // 源代碼擴容后容量是擴容前的二倍
  int n1 = 10;   // 對照組
  int n2 = 1000000; // put/get多少組
  long t0 = 0;      //總耗時
  float lf = 0.9f;  //負載因子
  int capacity = 100; //初始容量
  HashMap map = null;
  
  //對照組循環(huán)
  for (int j = 1; j <= n1; j++) {
   map = new HashMap(capacity, lf);
   List<String> list = new ArrayList<String>();
   // 利用循環(huán)進行put
   for (int i = 0; i < n2; i++) {
    String temp = HashTest.randomString();
    map.put(temp, i);
    list.add(temp);
   }
   long time = 0; // 總耗費時間
   // 利用循環(huán)get
   for (int i = 0; i < n2; i++) {
    String temp = list.get(i);
    long t1 = System.currentTimeMillis();
    map.get(temp);
    long t2 = System.currentTimeMillis();
    long t3 = t2 - t1;// 花費時間
    time += t3;
   }
   System.out.println("組"+j+"花費時間(ms)=" + time);
   t0 += time;
   map = null;
  }
  System.out.println("get出 "+n2+" 對鍵值對中,"+n1+"組數(shù)據(jù)得出:");
  System.out.println("---------------------------------");
  System.out.println("每get"+n2+"對鍵值對 平均花費時間(毫秒):"+(t0/n1));
 }
 /**
  * 產(chǎn)生隨機字符串方法 
  * @return
  */
 public static String randomString() {
  // 最終產(chǎn)生的字符串
  StringBuffer sb = new StringBuffer();
  // 字符串樣本
  String str = "回到家卡薩恒大帝景阿薩德節(jié)快樂就看見了困窘企業(yè)無辜的鄙視你別這么想按一個預(yù)告的哈上東國際按時大大伽伽匯頂科技啊啥看的撒打算大的歐亞報出去qwertyuiopasdfghjklzxvcbnm,.;p[']/\1234567890zxcvbnmaksjhfgdlpoiuytrewq阿斯加德克拉斯近段時間的書上方法更符合輔導(dǎo)費的冠福股份極樂空間流口水";
  // System.out.println("樣本字符串長度:"+str.length());
  // 產(chǎn)生一個1到30的數(shù)字
  int num = (int) (Math.random() * 30 + 1);
  // System.out.println("num="+num);
  // 用for循環(huán)從樣本字符串中提取出字符進行組合
  for (int i = 0; i < num; i++) {
   int num1 = (int) (Math.random() * str.length()); // 產(chǎn)生一個0到字符串樣本的數(shù)字
   // 根據(jù)索引值獲取對應(yīng)的字符
   char charAt = str.charAt(num1);
   sb.append(charAt);
  }
  // System.out.println("產(chǎn)生一個長度為"+num+"的字符串");
  return sb.toString();
 }

小伙伴可以調(diào)節(jié)負載因子的大小來測試,時間上的差異.

我們可以發(fā)現(xiàn),為了保證哈希的結(jié)果可以分散、為了提高哈希的效率,JDK在一個小小的hash方法上就有很多考慮,做了很多事情。當(dāng)然,我希望我們不僅可以深入了解背后的原理,還要學(xué)會這種對代碼精益求精的態(tài)度。

Jdk的源代碼,每一行都很有意思,都值得花時間去鉆研、推敲。

以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。

相關(guān)文章

  • Java軟件設(shè)計模式之橋接模式詳解

    Java軟件設(shè)計模式之橋接模式詳解

    這篇文章主要介紹了Java軟件設(shè)計模式之橋接模式詳解,橋接模式也叫做橋梁模式,結(jié)構(gòu)型設(shè)計模式的一種,顧名思義,就是用來連接兩個部分,為被分離了的抽象部分和實現(xiàn)部分搭橋,需要的朋友可以參考下
    2023-07-07
  • nacos客戶端一致性hash負載需求實現(xiàn)

    nacos客戶端一致性hash負載需求實現(xiàn)

    這篇文章主要介紹了nacos客戶端一致性hash負載的需求實現(xiàn)過程及步驟詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步
    2022-02-02
  • java多線程下載文件原理解析

    java多線程下載文件原理解析

    這篇文章主要為大家詳細介紹了java多線程下載文件原理,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2019-04-04
  • SpringBoot整合MyBatis超詳細教程

    SpringBoot整合MyBatis超詳細教程

    這篇文章主要介紹了SpringBoot整合MyBatis超詳細教程,下面從配置模式、注解模式、混合模式三個方面進行說明MyBatis與SpringBoot的整合,需要的朋友可以參考下
    2021-05-05
  • 詳解Java中的八種單例創(chuàng)建方式

    詳解Java中的八種單例創(chuàng)建方式

    單例設(shè)計模式,就是采取一定的方法保證在整個的軟件系統(tǒng)中,對某個類只能存在一個對象實例,并且該類只提供一個取得其對象實例的方法。本文將詳細介紹Java中單例的八種創(chuàng)建方式,需要的可以參考一下
    2022-02-02
  • Java模擬UDP通信示例代碼

    Java模擬UDP通信示例代碼

    這篇文章主要介紹了Java模擬UDP通信,文中示例代碼非常詳細,供大家參考和學(xué)習(xí),感興趣的朋友可以了解下
    2020-06-06
  • Java NIO Path接口和Files類配合操作文件的實例

    Java NIO Path接口和Files類配合操作文件的實例

    下面小編就為大家分享一篇Java NIO Path接口和Files類配合操作文件的實例,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2017-11-11
  • java反射方式創(chuàng)建代碼詳解

    java反射方式創(chuàng)建代碼詳解

    在本篇文章里小編給大家整理的是一篇關(guān)于java反射方式創(chuàng)建代碼詳解內(nèi)容,對此有興趣的朋友們可以學(xué)習(xí)下。
    2021-01-01
  • 深入淺析Spring 的aop實現(xiàn)原理

    深入淺析Spring 的aop實現(xiàn)原理

    AOP(Aspect-OrientedProgramming,面向方面編程),可以說是OOP(Object-Oriented Programing,面向?qū)ο缶幊蹋┑难a充和完善。本文給大家介紹Spring 的aop實現(xiàn)原理,感興趣的朋友一起學(xué)習(xí)吧
    2016-03-03
  • SpringBoot自定義全局異常處理器的問題總結(jié)

    SpringBoot自定義全局異常處理器的問題總結(jié)

    Springboot框架提供兩個注解幫助我們十分方便實現(xiàn)全局異常處理器以及自定義異常,處理器會優(yōu)先處理更具體的異常類型,如果沒有找到匹配的處理器,那么它會尋找處理更一般異常類型的處理器,本文介紹SpringBoot自定義全局異常處理器的問題,一起看看吧
    2024-01-01

最新評論