成人国产在线小视频_日韩寡妇人妻调教在线播放_色成人www永久在线观看_2018国产精品久久_亚洲欧美高清在线30p_亚洲少妇综合一区_黄色在线播放国产_亚洲另类技巧小说校园_国产主播xx日韩_a级毛片在线免费

資訊專欄INFORMATION COLUMN

HashMap剖析之put()和get()方法

microcosm1994 / 3100人閱讀

摘要:判斷該首節(jié)點(diǎn)是否與插入的鍵值對(duì)的和一致,若一致則替換該節(jié)點(diǎn)的值為,否則進(jìn)入下一步判斷首節(jié)點(diǎn)是否為樹節(jié)點(diǎn),若是則調(diào)用樹節(jié)點(diǎn)的方法遍歷紅黑樹,否則遍歷鏈表。中的方法會(huì)在鏈表超過(guò)樹化閾值的時(shí)候,將鏈表轉(zhuǎn)化為紅黑樹。

前言

由于Java 1.7Java 1.8HashMapHashMap中的put()get()方法在實(shí)現(xiàn)上差異很大,所以本文將于分別分析這兩個(gè)版本的put()get()f方法

下面將會(huì)分析這部分的源碼,如果覺得源碼分析內(nèi)容太啰嗦,可以跳過(guò)源碼部分,直接看源碼下面的總結(jié)。

put()方法源碼分析

HashMapput()方法是我們最常用的方法,但是put()方法是怎么工作的呢?

Java 1.7 put()方法
public V put(K key, V value) {
        if (key == null)// 處理key為null的情況
            return putForNullKey(value);
        // 計(jì)算key的hash值
        int hash = hash(key);
        // 計(jì)算命中table的索引
        int i = indexFor(hash, table.length);
        // 遍歷命中的鏈表
        for (Entry e = table[i]; e != null; e = e.next) {
            Object k;
            // 存在key和hash值相同則替換value
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        // 記錄結(jié)構(gòu)性變化
        modCount++;
        // 增加新鏈表
        addEntry(hash, key, value, i);
        // 上一次節(jié)點(diǎn)不存在,返回null
        return null;
    }

put()方法實(shí)際上是

keynull時(shí),直接調(diào)用putForNullKey()方法。否則進(jìn)入下一步

調(diào)用hash()方法獲取keyhash值,進(jìn)入下一步

調(diào)用indexFor()計(jì)算命中的散列表table的索引

遍歷鏈表,如果鏈表不存在或鏈表不存在keyhash值相同的節(jié)點(diǎn),則創(chuàng)建新的鏈表或尾部添加節(jié)點(diǎn),否則替換對(duì)應(yīng)節(jié)點(diǎn)的value

putForNullKey()
    private V putForNullKey(V value) {
        // 遍歷鏈表,但是命中的散列表的索引和key的hash值為0
        // 后續(xù)邏輯與`put()`類似
        for (Entry e = table[0]; e != null; e = e.next) {
            if (e.key == null) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        modCount++;
        addEntry(0, null, value, 0);
        return null;
    }

putForNullKey只是將命中散列表table的索引和keyhash值都設(shè)置為0,其他邏輯與put()方法后續(xù)的邏輯一致。

indexFor()方法
    /**
     * 計(jì)算命中散列表的索引
     */
    static int indexFor(int h, int length) {
        // 等價(jià)于length%h
        return h & (length-1);
    }
hash()方法
    /**
     * hash值計(jì)算方法
     */
    final int hash(Object k) {
        int h = 0;
        // 使用替代的hash方法
        if (useAltHashing) {
            if (k instanceof String) {
                // 為字符串則使用特定的hash方法
                return sun.misc.Hashing.stringHash32((String) k);
            }
            // 使用特定的hash種子計(jì)算hash值
            h = hashSeed;
        }
        
        h ^= k.hashCode();

        // 這部分代碼是為了減少哈希碰撞
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }
addEntry()方法
    void addEntry(int hash, K key, V value, int bucketIndex) {
        // 判斷散列表是否需要擴(kuò)容或者未初始化
        if ((size >= threshold) && (null != table[bucketIndex])) {
            // 散列表擴(kuò)容為原來(lái)的2倍
            resize(2 * table.length);
            // 計(jì)算key的hash值,key為null則返回0
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }
        // 創(chuàng)建新的鏈表
        // 如果鏈表已存在,則是將新節(jié)點(diǎn)插入頭部(頭插法)
        createEntry(hash, key, value, bucketIndex);
    }
createEntry()方法
    /**
     * 頭插法插入新的節(jié)點(diǎn)
     * 不需要判斷鏈表是否存在
     */
    void createEntry(int hash, K key, V value, int bucketIndex) {
        Entry e = table[bucketIndex];
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        size++;
    }
Java 1.8 put()方法
    /**
     * HashMap的put()方法支持key/value為null
     */
    public V put(K key, V value) {
        //實(shí)際上是先調(diào)用HashMap的hash()方法獲取到key的hash值
        //然后調(diào)用HashMap的putVal()方法
        return putVal(hash(key), key, value, false, true);
    }

put()方法實(shí)際上是

調(diào)用hash()方法獲取到keyhash

調(diào)用putVal()方法存儲(chǔ)key-value

核心方法是putVal()方法,下面我會(huì)先分析一下hash()方法,因?yàn)檫@個(gè)方法涉及到hash值這個(gè)關(guān)鍵屬性的計(jì)算。

hash()方法
    static final int hash(Object key) {
        int h;
        // key為null時(shí),hash值為0
        // key不為null時(shí),調(diào)用key對(duì)象的hashCode()方法并通過(guò)位運(yùn)算異或和無(wú)符號(hào)右移將高位分散到低位
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

hash()方法指定了nullhash值為0。這樣就可以支持keynull。

(h = key.hashCode()) ^ (h >>> 16)這段代碼通過(guò)位運(yùn)算異或和無(wú)符號(hào)右移將高位分散到低位,這樣做可以減少哈希碰撞的概率(這塊不是很清楚原理,是從方法注釋上了解到的)

putVal()方法
    /**
     * Map.put()方法的實(shí)際實(shí)現(xiàn)
     *
     * @param hash key的hash值
     * @param key 鍵值對(duì)中的key
     * @param value 鍵值對(duì)中的value
     * @param onlyIfAbsent 如果為true,則鍵值對(duì)中的值已經(jīng)存在則不修改這個(gè)值
     * @param evict 如果為false,則是處于創(chuàng)建模式
     * @return 上一次的value,如果上一次的value不存在,則為null
     */
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
        //tab用于暫存散列表table。p為散列表中對(duì)應(yīng)索引的鏈表的頭節(jié)點(diǎn)的指針。n存儲(chǔ)tab的長(zhǎng)度。i則為命中的散列表的索引
        Node[] tab; Node p; int n, i;
        //給tab和n賦值
        //當(dāng)tab為null或者tab的長(zhǎng)度n為0時(shí),觸發(fā)resize()來(lái)初始化tab
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        //使用(n - 1) & hash(等價(jià)于hash%n)計(jì)算命中的散列表索引,同時(shí)判斷散列表對(duì)應(yīng)索引的鏈表是否存在
        if ((p = tab[i = (n - 1) & hash]) == null)
            //散列表對(duì)應(yīng)索引的鏈表不存在則創(chuàng)建一個(gè)新的鏈表
            tab[i] = newNode(hash, key, value, null);
        else {//散列表對(duì)應(yīng)索引的鏈表已存在
            Node e; K k;
            // 判斷頭節(jié)點(diǎn)的hash值和key是否與入?yún)⒌膆ash值和key一致。需要注意,null的hash值為0
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                // 對(duì)應(yīng)的鍵值對(duì)已經(jīng)存在,記錄下來(lái)
                e = p;
            else if (p instanceof TreeNode)//判斷對(duì)應(yīng)的鏈表是否轉(zhuǎn)化為紅黑樹
                //若是,則直接調(diào)用紅黑樹的putTreeVal()方法
                e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
            else {//鏈表的頭節(jié)點(diǎn)與新的鍵值對(duì)不重復(fù),即沒(méi)有發(fā)生哈希碰撞
                for (int binCount = 0; ; ++binCount) {//遍歷鏈表
                    if ((e = p.next) == null) {//遍歷到尾節(jié)點(diǎn)
                        //尾插法添加一個(gè)新的節(jié)點(diǎn)
                        p.next = newNode(hash, key, value, null);
                        //鏈表長(zhǎng)度大于閾值
                        if (binCount >= TREEIFY_THRESHOLD - 1) // 從-1開始,所以為閾值-1 
                            // 將鏈表轉(zhuǎn)化為紅黑樹
                            treeifyBin(tab, hash);
                        // 中斷循環(huán)
                        break;
                    }
                    // 判斷當(dāng)前遍歷的節(jié)點(diǎn)的hash值和key是否與入?yún)⒌膆ash值和key一致,即key是否已經(jīng)存在
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        // key已經(jīng)存在,中斷循環(huán)
                        break;
                    // 記錄當(dāng)前遍歷的節(jié)點(diǎn)
                    p = e;
                }
            }
            if (e != null) { // Map中存在重復(fù)的key
                V oldValue = e.value;//記錄下舊值
                if (!onlyIfAbsent || oldValue == null)//判斷值存在是否可以進(jìn)行修改以及舊值是否為null
                    e.value = value;//修改該節(jié)點(diǎn)的值
                afterNodeAccess(e);// 鏈表節(jié)點(diǎn)的回調(diào)方法,此處為空方法
                return oldValue;//返回舊值
            }
        }
        // HashMap發(fā)生結(jié)構(gòu)變化,變化次數(shù)累加
        ++modCount;
        // 鍵值對(duì)個(gè)數(shù)自增,同時(shí)判斷是否達(dá)到擴(kuò)容的閾值
        if (++size > threshold)
            resize();
        // 鏈表節(jié)點(diǎn)的回調(diào)方法,此處為空方法
        afterNodeInsertion(evict);
        // 此處返回null是因?yàn)殒湵硇略隽斯?jié)點(diǎn),所以上一次的值必然為null
        return null;
    }

putVal()方法的關(guān)鍵點(diǎn):

table沒(méi)有初始化則調(diào)用reszie()方法初始化。

計(jì)算命中的散列表索引位置,公式為(n - 1) & hash(等價(jià)于hash%n)。其中n為散列表長(zhǎng)度,hash為插入的鍵值對(duì)的key的哈希值。

判斷散列表對(duì)應(yīng)索引中的首節(jié)點(diǎn)是否為null,若為null,則創(chuàng)建鏈表,否則進(jìn)入下一步。

判斷該首節(jié)點(diǎn)是否與插入的鍵值對(duì)的keyhash一致,若一致則替換該節(jié)點(diǎn)的值為value,否則進(jìn)入下一步

判斷首節(jié)點(diǎn)是否為樹節(jié)點(diǎn),若是則調(diào)用樹節(jié)點(diǎn)的putTreeVal()方法遍歷紅黑樹,否則遍歷鏈表。

遍歷紅黑樹時(shí),若存在keyhash相同的節(jié)點(diǎn)就替換對(duì)應(yīng)節(jié)點(diǎn)的值value,若不存在則插入新的樹節(jié)點(diǎn)。

遍歷鏈表時(shí),若存在keyhash相同的節(jié)點(diǎn)就替換對(duì)應(yīng)節(jié)點(diǎn)的值為value。若找不到keyhash相同的節(jié)點(diǎn),則鏈表尾部插入節(jié)點(diǎn),同時(shí)進(jìn)入下一步。

若當(dāng)前鏈表長(zhǎng)度大于或等于樹化閾值TREEIFY_THRESHOLD(8)時(shí),則將鏈表轉(zhuǎn)化為紅黑樹。

get()方法源碼分析

除了HashMapput()方法外,get()方法也是一個(gè)我們常用的方法,下面開始分析其關(guān)鍵的源碼。

Java 1.7 get()方法
    public V get(Object key) {
        if (key == null)// key為null時(shí)特殊處理
            return getForNullKey();
        // 關(guān)鍵獲取key對(duì)應(yīng)value的代碼
        Entry entry = getEntry(key);

        return null == entry ? null : entry.getValue();
    }

get()方法的關(guān)鍵點(diǎn)如下:

keynull,則調(diào)用getForNullKey()方法獲取value,否則進(jìn)入下一步

調(diào)用getEntry()方法獲取對(duì)應(yīng)的Entry對(duì)象

對(duì)應(yīng)的Entry對(duì)象為null時(shí)返回null,否則調(diào)用getValue()返回其value

getForNullKey()
    private V getForNullKey() {
        // 命中散列表索引為0,無(wú)需計(jì)算key的hash值
        // 遍歷命中的鏈表
        for (Entry e = table[0]; e != null; e = e.next) {
            if (e.key == null)
                return e.value;
        }
        return null;
    }
getEntry()
    final Entry getEntry(Object key) {
        // 計(jì)算key的hash值,key為null時(shí)返回0
        int hash = (key == null) ? 0 : hash(key);
        // 遍歷命中的鏈表
        for (Entry 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;
        }
        // 鏈表不存在或鏈表中不存在key和hash一致的節(jié)點(diǎn)
        return null;
    }
Java 1.8 get()方法
/**
 * 返回key對(duì)應(yīng)的value,如果不存在則返回null
 */
public V get(Object key) {
        Node e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }

get()方法實(shí)際上是

調(diào)用hash()方法獲取到keyhash

調(diào)用getNode()方法通過(guò)keyhash獲取對(duì)應(yīng)的value。不存在則返回null

核心方法是getNode()方法,下面我會(huì)先分析一下getNode()方法。

getNode()方法
    /**
     * Map.get()方法的實(shí)際實(shí)現(xiàn)
     * @param hash key的哈希值
     * @param key 查詢用的key
     * @return 節(jié)點(diǎn)或者是節(jié)點(diǎn)不存在是返回null
     */
    final Node getNode(int hash, Object key) {
        //tab用于暫存散列表table。first為散列表中對(duì)應(yīng)索引的鏈表的頭節(jié)點(diǎn)的指針。n存儲(chǔ)tab的長(zhǎng)度。i則為命中的散列表的索引
        Node[] tab; Node first, e; int n; K k;
        //初始化方法內(nèi)的變量,同時(shí)嘗試命中散列表
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            if (first.hash == hash && 
                ((k = first.key) == key || (key != null && key.equals(k))))// 總是先檢查鏈表的頭節(jié)點(diǎn)
                return first;//頭節(jié)點(diǎn)符合直接返回頭節(jié)點(diǎn)
            if ((e = first.next) != null) {//是否只有一個(gè)節(jié)點(diǎn)
                if (first instanceof TreeNode)//判斷頭節(jié)點(diǎn)是否為紅黑樹節(jié)點(diǎn)
                    return ((TreeNode)first).getTreeNode(hash, key);//改為遍歷紅黑樹
                do {//遍歷鏈表是否有符合的節(jié)點(diǎn)
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        //不存在對(duì)應(yīng)的key,返回null
        return null;
    }

getNode()方法的關(guān)鍵點(diǎn):

若散列表table不為null長(zhǎng)度大于0且其索引為(n - 1) & hash(等價(jià)于hash%n)的節(jié)點(diǎn)不為null。其中n為散列表長(zhǎng)度,hash為插入的鍵值對(duì)的key的哈希值。則進(jìn)入下一步,否則直接返回null

判斷首節(jié)點(diǎn)的keyhash是否與入?yún)⒁恢?,若相同則返回首節(jié)點(diǎn),否則進(jìn)入下一步。

判斷節(jié)點(diǎn)個(gè)數(shù)只有1個(gè),若是則返回null,否則進(jìn)入下一步

判斷首節(jié)點(diǎn)是否為樹節(jié)點(diǎn),若是則遍歷紅黑樹,否則為鏈表,進(jìn)入下一步

遍歷鏈表,檢索keyhash與入?yún)⑾嗤墓?jié)點(diǎn),若找到則返回該節(jié)點(diǎn),否則返回null

總結(jié)

put()get()方法是HashMap的常用方法,通過(guò)學(xué)習(xí)其源碼了解到HashMap是如何使用拉鏈法解決哈希沖突。而下面將會(huì)通過(guò)兩幅圖展示put()get()的執(zhí)行過(guò)程:

Java 1.7

put()方法圖解

get()方法圖解

Java 1.8

put()方法圖解

get()方法圖解

比較

既然分析了Java 1.7Java 1.8HashMapput()get()
方法,當(dāng)然少不了對(duì)二者的比較:

Java 1.7HashMap中存在很多重復(fù)的代碼。例如putForNullKey()put()方法中重復(fù)的鏈表遍歷,大量重復(fù)的hash值計(jì)算邏輯等等。而在Java 1.8中則對(duì)這部分的代碼進(jìn)行了重構(gòu)。例如將putForNullKey()put()方法重復(fù)的代碼整合成putVal()方法,hash()方法處理keynull時(shí)的情況。

Java 1.8中的put()方法會(huì)在鏈表超過(guò)樹化閾值的時(shí)候,將鏈表轉(zhuǎn)化為紅黑樹。而Java 1.7中則只有鏈表

Java 1.7的鏈表節(jié)點(diǎn)插入為頭插法(不需要判斷鏈表是否存在),而Java 1.8的鏈表節(jié)點(diǎn)插入則為尾插法。

Java 1.8增加了對(duì)putIfAbsent()方法(存在才進(jìn)行更新)的支持,詳情可以看putVal()中關(guān)于onlyIfAbsent參數(shù)的處理邏輯。

文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/73262.html

相關(guān)文章

  • LinkedHashMap就這么簡(jiǎn)單【源碼剖析

    摘要:習(xí)慣在微信看技術(shù)文章,想要獲取更多的資源的同學(xué),可以關(guān)注微信公眾號(hào)。為了大家方便,剛新建了一下群,大家也可以去交流交流。謝謝支持了希望能多介紹給其他有需要的朋友 前言 聲明,本文用得是jdk1.8 前面已經(jīng)講了Collection的總覽和剖析List集合以及散列表、Map集合、紅黑樹還有HashMap基礎(chǔ)了: Collection總覽 List集合就這么簡(jiǎn)單【源碼剖析】 Map集合、...

    avwu 評(píng)論0 收藏0
  • HashMap就是這么簡(jiǎn)單【源碼剖析

    前言 聲明,本文用得是jdk1.8 前面已經(jīng)講了Collection的總覽和剖析List集合以及散列表、Map集合、紅黑樹的基礎(chǔ)了: Collection總覽 List集合就這么簡(jiǎn)單【源碼剖析】 Map集合、散列表、紅黑樹介紹 本篇主要講解HashMap,以及涉及到一些與hashtable的比較~ 看這篇文章之前最好是有點(diǎn)數(shù)據(jù)結(jié)構(gòu)的基礎(chǔ): Java實(shí)現(xiàn)單向鏈表 棧和隊(duì)列就是這么簡(jiǎn)單 二叉樹就...

    entner 評(píng)論0 收藏0
  • ZStack源碼剖析核心庫(kù)鑒賞——ThreadFacade

    摘要:每個(gè)消息都會(huì)被一個(gè)線程消費(fèi),同時(shí)最大并發(fā)量為。然后提交一個(gè)任務(wù)到線程池中,這個(gè)任務(wù)的內(nèi)容是從等待隊(duì)列中取出一個(gè),如果等待隊(duì)列為空,則刪除這個(gè)等待隊(duì)列的。小結(jié)本文分析了的久經(jīng)生產(chǎn)考驗(yàn)的核心組件線程池。 本文首發(fā)于泊浮目的專欄:https://segmentfault.com/blog... 前言 在ZStack中,最基本的執(zhí)行單位不僅僅是一個(gè)函數(shù),也可以是一個(gè)任務(wù)(Task。其本質(zhì)實(shí)現(xiàn)...

    enali 評(píng)論0 收藏0
  • ConcurrentHashMap基于JDK1.8源碼剖析

    摘要:下面我來(lái)簡(jiǎn)單總結(jié)一下的核心要點(diǎn)底層結(jié)構(gòu)是散列表數(shù)組鏈表紅黑樹,這一點(diǎn)和是一樣的。是將所有的方法進(jìn)行同步,效率低下。而作為一個(gè)高并發(fā)的容器,它是通過(guò)部分鎖定算法來(lái)進(jìn)行實(shí)現(xiàn)線程安全的。 前言 聲明,本文用的是jdk1.8 前面章節(jié)回顧: Collection總覽 List集合就這么簡(jiǎn)單【源碼剖析】 Map集合、散列表、紅黑樹介紹 HashMap就是這么簡(jiǎn)單【源碼剖析】 LinkedHas...

    sanyang 評(píng)論0 收藏0
  • 集合源碼學(xué)習(xí)路---hashMap(jdk1.8)

    摘要:值得位數(shù)有的次方,如果直接拿散列值作為下標(biāo)訪問(wèn)主數(shù)組的話,只要算法比較均勻,一般是很難出現(xiàn)碰撞的。但是內(nèi)存裝不下這么大的數(shù)組,所以計(jì)算數(shù)組下標(biāo)就采取了一種折中的辦法,就是將得到的散列值與數(shù)組長(zhǎng)度做一個(gè)與操作。 hashMap簡(jiǎn)單介紹 hashMap是面試中的高頻考點(diǎn),或許日常工作中我們只需把hashMap給new出來(lái),調(diào)用put和get方法就完了。但是hashMap給我們提供了一個(gè)絕佳...

    kamushin233 評(píng)論0 收藏0

發(fā)表評(píng)論

0條評(píng)論

microcosm1994

|高級(jí)講師

TA的文章

閱讀更多
最新活動(dòng)
閱讀需要支付1元查看
<