1. 程式人生 > >JDK原始碼分析(4)HashMap

JDK原始碼分析(4)HashMap

JDK版本

在這裡插入圖片描述

HashMap簡介

HashMap基於雜湊表的 Map 介面的實現。此實現提供所有可選的對映操作,並允許使用 null 值和 null 鍵。(除了不同步和允許使用 null 之外,HashMap 類與 Hashtable 大致相同。)此類不保證對映的順序,特別是它不保證該順序恆久不變。

值得注意的是HashMap不是執行緒安全的,如果想要執行緒安全的HashMap,可以通過Collections類的靜態方法synchronizedMap獲得執行緒安全的HashMap。

Map map = Collections.synchronizedMap(new HashMap());

HashMap的資料結構

HashMap的底層主要是基於陣列和連結串列來實現的,它之所以有相當快的查詢速度主要是因為它是通過計算雜湊碼來決定儲存的位置。HashMap中主要是通過key的hashCode來計算hash值的,只要hashCode相同,計算出來的hash值就一樣。如果儲存的物件對多了,就有可能不同的物件所算出來的hash值是相同的,這就出現了所謂的hash衝突。學過資料結構的同學都知道,解決hash衝突的方法有很多,HashMap底層是通過連結串列來解決hash衝突的。 下面一幅圖,形象的反映出HashMap的資料結構:陣列加連結串列實現

在這裡插入圖片描述

HashMap屬性

/**
     * 初始容量,必須是2的倍數,預設是16
     */
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 /** * 最大所能容納的key-value 個數 */ static final int MAXIMUM_CAPACITY = 1 << 30; /** * 預設的載入因子 */ static final float DEFAULT_LOAD_FACTOR = 0.75f; /** * 樹化連結串列節點的閾值,當某個連結串列的長度大於或者等於這個長度,則擴大陣列容量,或者數化連結串列 */
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;

HashMap構造方法

預設構造方法將使用預設的載入因子(0.75)初始化。

public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

HashMap(int initialCapacity, float loadFactor)

使用指定的初始容量和預設的載入因子初始化HashMap,這裡需要注意的是,並不是你指定的初始容量是多少那麼初始化之後的HashMap的容量就是多大,例如new HashMap(20,0.8); 那麼實際的初始化容量是32,因為tableSizeFor()方法會嚴格要求把初始化的容量是以2的次方數成長只能是16、32、64、128…

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);
    }
static final int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

HashMap(int initialCapacity)

其實這個方法也是呼叫HashMap(int initialCapacity, float loadFactor) 方法實現的,我們來看看原始碼實現:

public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

HashMap(Map<? extends K, ? extends V> m)

該方法是按照之前的hashMap的物件,重新深拷貝一份HashMap物件,使用的載入因子是預設的載入因子:0.75。

public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }

put

執行邏輯: 1)根據key計算當前Node的hash值,用於定位物件在HashMap陣列的哪個節點。 2)判斷table有沒有初始化,如果沒有初始化,則呼叫resize()方法為table初始化容量,以及threshold的值。 3)根據hash值定位該key 對應的陣列索引,如果對應的陣列索引位置無值,則呼叫newNode()方法,為該索引建立Node節點 4)如果根據hash值定位的陣列索引有Node,並且Node中的key和需要新增的key相等,則將對應的value值更新。 5)如果在已有的table中根據hash找到Node,其中Node中的hash值和新增的hash相等,但是key值不相等的,那麼建立新的Node,放到當前已存在的Node的連結串列尾部。 ​ 如果當前Node的長度大於8,則呼叫treeifyBin()方法擴大table陣列的容量,或者將當前索引的所有Node節點變成TreeNode節點,變成TreeNode節點的原因是由於TreeNode節點組成的連結串列索引元素會快很多。 6)將當前的key-value 數量標識size自增,然後和threshold對比,如果大於threshold的值,則呼叫resize()方法,擴大當前HashMap物件的儲存容量。 7)返回oldValue或者null。 put 方法比較經常使用的方法,主要功能是為HashMap物件新增一個Node 節點,如果Node存在則更新Node裡面的內容。

public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

put的主要的實現邏輯還是在putVal 實現的.下面我們來看看put主要實現邏輯:

/**
     * Implements Map.put and related methods
     *
     @param key的hash值
     * @param key值
     * @param value值
     * @param onlyIfAbsent如果是true,則不修改已存在的value值
     * @param evict if false, the table is in creation mode.
     * @return 返回被修改的value,或者返回null。
     */
    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;
    }

上面呼叫到了一個resize方法, 我們來看看這個方法裡面做了什麼,實現邏輯如下: 1)如果當前陣列為空,則初始化當前陣列

2)如果當前table陣列不為空,則將當前的table陣列擴大兩倍,同時將閾值(threshold)擴大兩倍陣列長度和閾值擴大成兩倍之後,將之前table陣列中的值全部放到新的table中去

/**
     * 初始化,或者是擴充套件table 的容量。
     * table的容量是按照2的指數增長的。
	 * 當擴大table 的容量的時候,元素的hash值以及位置可能發生變化
     *
     * @return the table
     */
    final Node<K,V>[] resize() {
        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;
            }
            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"})
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    if (e.next == null)
                        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);
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

下面我們來看看treeifyBin方法的具體實現

/**
     * 如果table長度太小,則擴大table 的陣列長度
     * 否則,將所有連結串列節點變成TreeNode,提高索引效率
     */
    final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();
        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);
        }
    }

get

根據key的hash值和key,可以唯一確定一個value,下面我們來看看get方法執行的邏輯

1)根據key計算hash值

2)根據hash值和key 確定所需要返回的結果,如果不存在,則返回空。

public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }

具體的實現在getNode方法實現

/**
     * @param key 的hash值
     * @param key的值
     * @return 返回由key和hash定位的Node,或者null
     */
    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;
    }

containKey

containsKey方法實際也是呼叫getNode方法實現的,如果key對應的value不存在則返回false

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

containsValue

containsValue方法的話需要遍歷物件所有的value,遇到value相等的,則返回true,具體實現如下

public boolean containsValue(Object value) {
        Node<K,V>[] tab; V v;
        if ((tab = table) != null && size > 0) {
            for (int i = 0; i < tab.length; ++i) {
                for (Node<K,V> e = tab[i]; e != null; e = e.next) {
                    if ((v = e.value) == value ||
                        (value != null && value.equals(v)))
                        return true;
                }
            }
        }
        return false;
    }

remove

執行邏輯:

1)根據key得到key的hash值

2)根據key 和hash值定位需要remove的Node

  1. 將Node從對應的連結串列移除,然後再將Node 前後的節點對接起來

4)返回被移除 的Node

5)key-value的數量減一,修改次數加一

/**
     * Implements Map.remove and related methods
     *
     * @param key的hash值
     * @param key值
     * @param 需要remove 的value,
     * @param 為true時候,當value相等的時候才remove
     * @param 如果為false 的時候,不會移動其他節點。
     * @return 返回被移除的Node,或者返回null
     */
    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;
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (p = tab[index = (n - 1) & hash]
            
           

相關推薦

JDK原始碼分析4HashMap

JDK版本 HashMap簡介 HashMap基於雜湊表的 Map 介面的實現。此實現提供所有可選的對映操作,並允許使用 null 值和 null 鍵。(除了不同步和允許使用 null 之外,HashMap 類與 Hashtable 大致相同。)此類不保證對映

JDK原始碼分析4HashSet

JDK版本 HashSet簡介 HashSet特點 非執行緒安全 允許null值 新增值得時候會先獲取物件的hashCode方法,如果hashCode 方法返回的值一致,則再呼叫equals方法判斷是否一致,如果不一致才add元素。 注意: 對於HashSet中儲存的物件,請注意正確重

JDK原始碼分析3HashMap

JDK版本 HashMap簡介 HashMap基於雜湊表的 Map 介面的實現。此實現提供所有可選的對映操作,並允許使用 null 值和 null 鍵。(除了不同步和允許使用 null 之外,HashMap 類與 Hashtable 大致相同。)此類不保證對映的順序,特別是它不保證該順序恆久不變。

Mybatis原始碼分析4—— Mapper的建立和獲取

Mybatis我們一般都是和Spring一起使用的,它們是怎麼融合到一起的,又各自發揮了什麼作用? 就拿這個Mapper來說,我們定義了一個介面,聲明瞭一個方法,然後對應的xml寫了這個sql語句, 它怎麼就執行成功了?這傢伙是怎麼實現的,帶著這個好奇心,我一步步跟蹤,慢慢揭開了它的

jdk原始碼分析1java.lang.Object

java.lang.Object原始碼分析 public final native Class<?> getClass() public native int hashCode(); public boolean e

集合原始碼分析HashMap集合

1、HashMap概述: 底層是雜湊演算法,針對鍵。HashMap允許null鍵和null值,執行緒不安全,效率高。鍵不可以重複儲存,值可以。 雜湊結構:不能保證資料的迭代順序,也不能保證順序的恆久不變。 Map集合(無序、無索引、不可以重複)是雙列集合,一個鍵對應一個

JDK原始碼分析3HashSet

HashSet HashSet簡介 HashSet特點 非執行緒安全 允許null值 新增值得時候會先獲取物件的hashCode方法,如果hashCode 方法返回的值一致,則再呼叫equals方法判斷是否一致,如果不一致才add元素。 注意: 對於HashS

jdk原始碼分析——垃圾收集器與記憶體分配策略

本章介紹的垃圾收集器與記憶體分配策略主要就三點。 第一點:垃圾收集(垃圾回收)。問題:哪些記憶體需要回收?什麼時候回收?如何回收? 第二點:介紹垃圾收集器。問題:有幾種型別是垃圾收集器?根據第一點的介紹,屬於那種型別的? 第三點:記憶體分配。問題:怎麼分配的? 一、垃

springcloud feign原始碼分析4——來看看將@FeignClient介面構造為bean的過程以及是如何註冊到容器裡的

接著上一篇,繼續來看 registerFeignClient() 方法 這邊一看就是在構造構造一個BeanDefiniction的東西,這個東西的話,構造的過程,其實就是用了構造器模式,這個構造器模式呢,就會將@FeignClient註解的屬性以及ServiceAClien

JDK原始碼分析6ConcurrentHashMap

JDK版本 ConcurrentHashMap原始碼分析 table:預設為null,初始化發生在第一次插入操作,預設大小為16的陣列,用來儲存Node節點資料,擴容時大小總是2的冪次方。 nextTable:預設為null,擴容時新生成的陣列,其大小為原陣列的兩倍。 sizeC

JDK原始碼分析5Vector

JDK版本 Vector簡介 /** * The {@code Vector} class implements a growable array of * objects. Like an array, it contains components that can be * accessed

jdk 原始碼分析20java NIO包簡單分析

BIO 是一種阻塞的io,主要體現在: 1)accept 時候或者客戶端嘗試連線時是阻塞的, 2)資料讀寫是阻塞的,即使是沒有讀到資料,而且每次都是讀寫一個位元組。 對於服務端一般系統中常用的方式是沒接收一個請求new 一個thread,然後由這個handler去

Django rest framework原始碼分析4----版本

目錄 版本  新建一個工程Myproject和一個app名為api (1)api/models.py from django.db import models class UserInfo(models.Model): USER_TYPE = ( (1,'普通

jdk 原始碼分析8java synchronized和鎖lock對比

因為synchronized 是關鍵字,無法看到原始碼,所以只能做一個簡單的分析對比了, synchronized 能鎖方法,也能鎖程式碼塊,其實也是一種重入鎖(也就是自己的鎖,自己可以進去),程式碼塊或方法離開,自動釋放鎖。 lock:lock能做synchro

jdk 原始碼分析10java unsafe 分析

jdk裡面原子操作unsafe都是native方法,看不到原始碼,所以特意下載openjdk 9 的版本。 1)獲取unsafe 物件,這個是openjdk裡的方法。通過反射獲得。 static {Reflection.registerMethodsToFi

【libevent】原始碼分析4--與event相關的一些函式和操作

        Libevent提供了一些與event相關的操作函式和操作。本文就重點講一下這方面的原始碼。         在Libevent中,無論是event還是event_base,都是使用指標而不會使用變數。實際上,如果檢視Libeve

Spring原始碼分析4---BeanFactoryPostProcessor看見的不一定是真的

在第二編對BeanFactory的分析中,我們老能看見BeanFactoyPostProcessor的身影,那麼在這一節中,我們來詳細的討論一下BeanFactoryPostProcessor的程式碼結構,從中學習他的優秀之處;BeanFactoryPostProcessor

Kafka原始碼分析4

四、Replication Subsystem 1、Replica Replica是kafka分發資料的最小單元,主要程式碼如下: class Replica(val brokerId: Int, val partition: Partiti

HLS學習HLSDownloader原始碼分析4解析Master PlayList

解析Master PlayList     PlayList就是m3u8檔案或者索引檔案,Master PlayList也叫一級索引檔案。 解析Master PlayList的過程如下: 1、

Java 集合原始碼分析HashMap

目錄 Java 集合原始碼分析(一)HashMap 1. 概要 2. JDK 7 的 HashMap 3. JDK 1.8 的 HashMap 4. Hashtable 5.