基於JDK1.8源碼分析HashMap容器

前言

基於JDK1.8源碼解析Map集合類下的HashMap,從源碼的角度出發,分析HashMap的運行原理,總結HashMap的知識點,以及HashMap與HashTable的對比。

本來想把TreeMap和LinkedHashMap放到一起寫的,但是考慮到篇幅原因,這裏先主要分析一下HashMap的源碼,再說說JDK1.7到JDK1.8這中間HashMap的改動以及HashMap和HashTable的區別;至於TreeMap和LinkedHashMap,放到其它地方在開一篇總結。

1、HashMap介紹

先從整體角度先分析一波HashMap,對HashMap有一個大概的把握。

1.1 HashMap頂部註解

在閱讀HashMap的源碼之前,先看HashMap的頂部註釋。

從上面那段註釋中,我們可以得到以下這些信息:

  • HashMap實現了Map接口,key和value都可以添加任意元素,包括null
  • HashMap與HashTable大致相等,除了HashMap是沒有加鎖,線程不安全的,HashTable的key不允許存儲null值;
  • HashMap不保證元素的有序性;
  • HashMap的默認裝載因子是0.75,當已經有數據的槽位與總的槽位的比例超過裝載因子時,會擴容並且再哈希
  • 迭代遍歷消耗的時間與HashMap實例的實際容量和鍵值對成正比,因此,在迭代器遍歷使用較多的情況下,不應該將初始容量設置的過大,或者負載因子設置的過小;
  • 迭代器採用快速失敗機制;
  • 在存儲大量數據時可以在初始化時指定好容量,減少擴容和在哈希帶來的時間消耗。

 1.2 HashMap的類結構

Hashmap的類結構相比於實現了Collection接口的List集合類的結構圖要簡單的多,繼承的是AbstractMap類。

2、HashMap源碼解讀

經過上面從整體分析過一波HashMap,已經對HashMap有了基本的認識。下面可以從源碼開始入手,深入去分析HashMap。

HashMap源碼的代碼量還是比較大的,全部閱讀完我覺得太耗時間了,但是隻要掌握好平時常用的幾個方法就可以適應日常的開發工作了。

2.1 HashMap的成員變量

/**
     * The default initial capacity - MUST be a power of two.
     */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

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

    /**
     * The load factor used when none specified in constructor.
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

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

結合註解,可以知道HashMap中幾個主要的成員變量的意義:

  • DEFAULT_INITIAL_CAPACITY:默認的初始容量是16,且是2的冪;
  • MAXIMUM_CAPACITY:HashMap的最大容量;
  • DELAULT_LOAD_FACTOR:如果初始化時使用了沒有指定裝載因子的構造器,裝載因子就使用這個變量。參數值爲0.75,即哈希表中3/4的位置已經被放置了元素時擴容;
  • TREEIFY_THRESHOLD:從鏈表轉換爲紅黑樹的閾值;
  • UNTREEIFY_THRESHOLD:從二叉樹轉換爲鏈表的閾值;
  • MIN_TREEIFY_CAPACITY:HashMap的最小樹形化閾值,當哈希表的容量大於這個值時才允許樹形化,低於這個閾值直接擴容;
    /**
     * The table, initialized on first use, and resized as
     * necessary. When allocated, length is always a power of two.
     * (We also tolerate length zero in some operations to allow
     * bootstrapping mechanics that are currently not needed.)
     */
    transient Node<K,V>[] table;

    /**
     * Holds cached entrySet(). Note that AbstractMap fields are used
     * for keySet() and values().
     */
    transient Set<Map.Entry<K,V>> entrySet;

    /**
     * The number of key-value mappings contained in this map.
     */
    transient int size;

    /**
     * The number of times this HashMap has been structurally modified
     * Structural modifications are those that change the number of mappings in
     * the HashMap or otherwise modify its internal structure (e.g.,
     * rehash).  This field is used to make iterators on Collection-views of
     * the HashMap fail-fast.  (See ConcurrentModificationException).
     */
    transient int modCount;

    /**
     * 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;
  • table:HashMap中用來存儲數據的鍵值對數組,不在初始化時分配內存而是在第一次put鍵值對時初始化到對應的容量,並且總是以兩倍擴容;
  • entrySet:保存緩存的entrySet();
  • size:HashMap中的槽位個數;
  • modCount:配合快速失敗機制的計數器,快速失敗機制:https://blog.csdn.net/weixin_39738307/article/details/106100118
  • threshold:決定是否擴容的閾值,也就是容量*裝載因子;
  • loadFactor:裝載因子。

2.2 hash方法

之所以單獨給hash開一個標題,是因爲hash方法是HashMap的核心方法之一,幾乎所有的常用方法,例如get,set等都是圍繞着hash方法展開的。

    /**
     * Computes key.hashCode() and spreads (XORs) higher bits of hash
     * to lower.  Because the table uses power-of-two masking, sets of
     * hashes that vary only in bits above the current mask will
     * always collide. (Among known examples are sets of Float keys
     * holding consecutive whole numbers in small tables.)  So we
     * apply a transform that spreads the impact of higher bits
     * downward. There is a tradeoff between speed, utility, and
     * quality of bit-spreading. Because many common sets of hashes
     * are already reasonably distributed (so don't benefit from
     * spreading), and because we use trees to handle large sets of
     * collisions in bins, we just XOR some shifted bits in the
     * cheapest possible way to reduce systematic lossage, as well as
     * to incorporate impact of the highest bits that would otherwise
     * never be used in index calculations because of table bounds.
     */
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

根據註釋和代碼,可以知道用Object類自帶的hashCode方法得到哈希值,再對哈希值本身和哈希值邏輯右移16位後的數字做異或(Java中int變量是32位的,16剛好是32的一半),也就是說hashCode方法得到的非null的值的前16位與0做異或,後16位與前16位做異或組成一個新的hash值。目的是使哈希值的分散更加均勻(注意一下Java的移位>>和>>>還是有區別的,>>是算術右移,>>>是邏輯右移)。簡單來說是通過高位和低位的異或操作,讓高位的哈希值擴散影響到低位的哈希值,來讓後面插入的時候對數組大小做與運算之後的結果能分配的更均勻從而減少哈希衝突。具體爲什麼這麼做會讓哈希的分佈更加均勻,在下面的put方法分析中會提到。

HashMap允許key爲null,當key爲null的時候,hash值就爲0。

hashCode方法具體運行原理:https://blog.csdn.net/weixin_39738307/article/details/106079696

2.3 HashMap的構造方法

HashMap的構造方法有4個:

重點說說第一個,其實這幾個都差不多,第一個複雜點,第一個看弄明白了其他幾個構造器也就都迎刃而解了。

指定了初始容量和裝載因子的構造器:

    /**
     * Constructs an empty <tt>HashMap</tt> with the specified initial
     * capacity and load factor.
     *
     * @param  initialCapacity the initial capacity
     * @param  loadFactor      the load factor
     * @throws IllegalArgumentException if the initial capacity is negative
     *         or the load factor is nonpositive
     */
    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);
    }

這裏調用了tableSizeFor函數,看一下這個函數的實現:

    /**
     * Returns a power of two size for the given target capacity.
     */
    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;
    }

結合代碼和註解可以知道, 這個函數的功能是返回一個離輸入參數最近並且不小於輸入參數的2的整數次冪。這裏用到的是位運算的算法,關於移位算法,可以參考:https://www.cnblogs.com/wang-meng/p/9b6c35c4b2ef7e5b398db9211733292d.html

這裏是把一個離初始容量最近且不小於初始容量的2的整數次冪賦值給threshold變量,這裏可以拋出一個問題:爲什麼要選擇這樣一個數賦值爲threshold變量?

在上面的分析中,threshold是決定是否擴容並且再散列的閾值,也就是容量*裝載因子;那麼這麼一來,就和上面的threshold變量的賦值操作矛盾了。其實這麼做問題不大,因爲這時候只是在初始化的時候賦值,還沒有真正的分配內存給HashMap,並且在真正分配了內存之後會再一次賦新的值給這個變量。等到第一次put數據的時候,會觸發擴容機制,這個時候不僅容量會發送變化,threshold變量也會被重新賦值,這時候賦的新值就是裝載因子*容量。具體的過程可以參考下面resize()方法的解析。

綜上,可以知道這個構造函數主要做了這麼幾件事情:

  • 判斷傳入的容量和裝載因子是否合法;
  • 如果傳入的容量超過了限定的最大值,就用最大值代替;
  • 初始化裝載因子,將裝載因子成員變量設置爲傳入的裝載因子;
  • 將閾值設置爲一個不小於初始容量且離初始容量最近的2的冪次方;

順便說一下爲什麼HashMap的容量要設置成2的冪,這個在面試中也常常會問到,可以參考https://www.jianshu.com/p/7d59251d28f3

根據hashCode把要存儲的鍵值對映射到哈希表上的一個位置,可能第一反應想到的是取餘。但是HashMap的作者沒有那麼做,而是用了位運算。位運算的速度要比取餘來得快,可能這也是使用位運算而捨棄取餘的一個原因。但是爲什麼必須是把容量設置成2的冪次呢?

2的冪次寫成二進制數的形式是100..(n個0),所以2的冪次-1就是011..(n個1)。因爲是key的哈希值對散列表的長度做與運算,所以最後得到的數值一定是在0到哈希表的長度之間,與取餘操作的效果相同,但是效率上有很大提升。

另外的,HashMap的哈希表的長度取二的冪次的另外一個重要原因,是保證哈希表長度-1的二進制數的數值最後一位一定要爲1!如果爲哈希表長度-1的二進制數的數值的最後一位爲0的話,0與0或者1做與運算最後的結果都是0,那麼key的哈希碼&哈希表長度-1的結果的二進制數值的最後一位一定是0。那也就是說下標的二進制值的最後一位爲1的位置將永遠沒有辦法被映射到,也就是說這些位置是存不了值的。所以,如果最後得到的結果二進制數值的最後一位一定爲0的話,那麼,那將會造成空間浪費,並且增加哈希碰撞,拖慢查詢效率,最嚴重的的話會導致無法擴容(因爲那些無法存放鍵值對的位置的存在,將永遠達不到擴容閾值)!!!

再看看剩下的幾個構造函數。

指定了初始容量的構造器,裝載因子爲默認的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);
    }

HashMap的無參構造器,也是日常開發中最常用的一種構造器,初始容量默認爲16,裝載因子默認爲0.75:

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

創建指定map的構造器:

    /**
     * Constructs a new <tt>HashMap</tt> with the same mappings as the
     * specified <tt>Map</tt>.  The <tt>HashMap</tt> is created with
     * default load factor (0.75) and an initial capacity sufficient to
     * hold the mappings in the specified <tt>Map</tt>.
     *
     * @param   m the map whose mappings are to be placed in this map
     * @throws  NullPointerException if the specified map is null
     */
    public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }

2.4 put(K, V)方法

添加方法和查看方法是HashMap中最常用的兩個方法,先來看一下HashMap是怎麼添加一個元素的。

先看看put方法:

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

hash方法剛剛已經分析過了,現在再去看看putVal方法:

經過總結提煉可以得到以下的流程圖,

  • HashMap不是在初始化時分配好內存,而是在第一次插入鍵值對時分配初始容量大小的內存!!!
  • 通過哈希算法映射到hash表中的一個位置,如果這個位置沒有放過value的話,那就直接放進去了,但是如果放過value的話,也就是說發生哈希衝突了(按照數據結構課本上面的概念,這個時候一般可以採用兩種方法:開放地址法和拉鍊法,這裏是在桶的下面接上一個紅黑樹/鏈表,屬於拉鍊法),先看看要存放的value的key以及key的hash值是否相等,如果相等說明要插入的和原來就有的是同一個映射,就直接把值給替換了。但是如果不是相等的,就要根據那個位置下面接的是鏈表/紅黑樹來做相應的處理了;
  • 鏈表:遍歷鏈表找一找是不是有相同的映射,如果找到的話就把值給替換掉,沒有的話就在尾部加個新的結點存放value,也就是尾插法(在JDK1.8是尾插法,在JDK1.7以及更早的版本是頭插法)。如果插入了結點,在插入之後在檢查一下桶下面接的結點數量是否超過樹形化閾值了,也就是TREEIFY_THRESHOLD,如果超過了的話在執行樹形化操作;
  • 紅黑樹:按照紅黑樹的方法插入新的結點;
  • 當然了,所有put操作最後都要檢查一下是否達到了擴容閾值,達到了就擴容並且再哈希。

這裏還要注意一個點,

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

 決定鍵值對存放的桶的位置,是對哈希表的長度進行與運算之後的值,也就是p = tab[i = (n - 1) & hash]。之前提到過的哈希操作會通過異或運算把高位的哈希值擴散影響到低位的哈希值,如果實例化對象時沒有指定初始化容量的話,那麼第一次插入之後容量就默認爲16,由於是對哈希表的長度做與運算,以16爲例子,這個時候做與運算僅僅是hash值的後4位有效。也就是說,當哈希表的容量不夠大時,這個時候hash值的高位對於插入操作來說失去作用,如果碰到高位變化特別大而低位的變化特別小時,這個時候很容易發生哈希碰撞在位置上變得集中的情況。那麼,JDK1.8中對哈希值在做一步處理,通過位運算把hash值的高位和低位都再次打亂,增加了隨機性,就可以用較小的代價解決可能發生的哈希衝突集中出現的情況。

2.5 get(Object key)方法

看完了HashMap添加一個元素的方法,再來看一下HashMap是如何通過key來獲取一個value的。

結合註解,可以知道通過調用HashMap的hash方法計算key的hash值,調用getNode方法獲取對應的value。

再來看看getNode方法的實現,

可以簡單的概括,查找的時候,如果哈希映射到的桶的位置上沒有元素 ,就返回null;如果有存放元素並且桶上的第一個元素就符合,那麼直接返回,否則,就按照鏈表/紅黑樹的方法查找下去~

2.6 remove方法

HashMap中的remove方法有兩個,分別是remove(Object key)和remove(Object key, Object value)。先看一下最常用的remove(Object key)方法的實現:

結合註解可以知道remove方法會刪除指定key的鍵值對,並且返回刪除的value。remove方法的內容只有一行調用removeNode函數的代碼,看一下removeNode函數的實現:

再來看一下另外一個remove(Object key, Object value)方法,在平時的開發中,用到的次數比較少。先看一下是怎麼實現的:

同樣是沒有返回刪除的value,用一個boolean類型的返回值代表刪除成功或者失敗。雖然沒有註釋,但是根據上面一個remove的分析可以看出,這裏的remove方法是刪除key與value都匹配的鍵值對。

2.7 resize方法

之前在put方法中有提到過擴容方法,這裏做一個補充。先看註解給出的信息:

可以知道,擴容之後的容量爲原來容量的2倍,以及擴容會導致再哈希。

再來看一下源碼是如何實現的:

總結一下擴容的步驟:

3、HashMap和HashTable的區別

因爲HashTable在現在的開發中基本不怎麼使用,並且有更好的替代方案,所以就不再去分析HashTable的源碼了。這裏主要再講一下HashMap和HashTable的區別。關於HashTable的源碼分析可以參考:https://blog.csdn.net/panweiwei1994/article/details/77427010

總結一下HashMap和HashTable的區別(HashMap在JDK1.7和1.8的實現差別還是挺大的,這裏的HashMap以1.8的爲例):

比較點 HashMap HashTable
底層數據結構 數組+鏈表/紅黑樹 數組+鏈表
線程安全性 線程不安全 線程安全
擴容方式 擴容後爲當前容量的2倍 擴容後爲當前容量的2倍+1
性能
默認的初始容量 16

11

容量要求 必須是2的整數次冪 沒有
繼承的類 AbstractMap Dictionary
遍歷方式 Iterator(迭代器) Iterator(迭代器)和Enumeration(枚舉器)
Iterator索引遍歷數組的順序 索引從小到大 索引從大到小
根據key的hashCode確定到數組中的位置 位運算 位運算+取餘

4、JDK1.7和JDK1.8中HashMap的不同

從JDK1.8對JDK1.7的HashMap做了改進,其中最重要的就是HashMap的底層結構由數組+鏈表->數組+鏈表/紅黑樹(前面分析過,當一個桶中的結點達到8個時,把鏈表轉換爲紅黑樹;如果結點減少到6個,紅黑樹轉換爲鏈表);

改動可以總結出以下這張表:

比較點 JDK1.8 JDK1.7
底層數據結構 數組+鏈表/紅黑樹 數組+鏈表
鏈表的插入方式 頭插法 尾插法
擴容流程    
擴容後數據存儲位置的計算方式    

補充1:爲什麼在JDK1.7的時候,是先擴容後面再插入,而到了JDK1.8的時候是先插入再擴容呢?

補充2:爲什麼JDK1.7的時候是用頭插法,而到了JDK1.8就變成了尾插法

補充3:爲什麼在JDK1.8中要引入紅黑樹機制?

主要是考慮到同一個位置衝突太多,查找效率的問題,當鏈表過長時轉換成紅黑樹可以把時間複雜度從O(n)降低到O(logn)。

總結

說了那麼多,其實HashMap的重要知識點可以做出以下的要點總結

  • HashMap底層是數組+鏈表/紅黑樹(在JDK1.7及之前是數組+鏈表);
  • HashMap的元素排列是無序的(散列算法);
  • HashMap根據哈希算法,計算key的HashCode(具體可參考Object類的HashCode()方法),爲了減少衝突概率,在與數組長度做與運算之前,將高16位與低16位做異或運算,高位與低位結合,增加隨機性,減少碰撞的可能;
  • 當數組中同一個位置的衝突數量>=8,且散列表容量>=64時,鏈表轉換爲紅黑樹;當衝突數量從>=8減少至6以下時,紅黑樹轉換爲鏈表;
  • HashMap的默認裝載因子是0.75,即當散列表中3/4的位置有元素時,HashMap會進行擴容;
  • HashMap當插入第一個元素時執行初始化操作;
  • HashMap默認初始容量爲16(可以通過構造器自定義初始容量,但是一定是2的冪),而且每次擴容容量擴大兩倍;
  • HashMap線程不安全,HashTable線程安全;現在HashTable因爲效率等因素已經不常用,一般在併發環境下用JUC包下線程安全的容器ConcurrentHashMap來代替HashTable,或者加鎖,也可以把HashMap包裝成一個線程安全的容器;
  • HashMap初始容量的設置不能過大或者過小。過大會造成空間浪費,過小會導致頻繁的擴容增加時間和空間的開銷。

總的來說,HashMap還是比List集合類要難多了,也難怪面試官都喜歡在面試的時候把HashMap當做重要知識點來提問。

補充1:最後的最後需要注意的一點是,如果面試中HashMap回答的不錯,面試官有可能會發出靈魂拷問:“爲什麼hashMap在衝突個數爲8的時候鏈表轉換爲紅黑樹,而在衝突個數爲6的時候在紅黑樹轉換回鏈表?

關於這個,主要還是通過概率統計的方法的計算得出,涉及到數學問題,感興趣的可以自行研究。

總的來說,根據概率統計中的泊松分佈,得到6和8是最適合的兩個點,同一個位置衝突數量大於8和remove操作之後衝突數量從大於8減少到小於6的情況都很少發生,是空間成本和時間成本的折中策略。至於爲什麼沒用到7,是因爲如果兩個閾值只相差1,那麼就會帶來頻繁的樹形化和退樹形化操作,反而會增加額外的時間成本。中間有個差值,可以防止頻繁轉換。

也可能會問到爲什麼裝載因子默認的是0.75。其實道理是一樣的,總結的來說,是減少空間成本(包括擴容帶來的時間和空間的消耗)和提高查詢效率的折中,主要是通過泊松分佈計算出來的一個數值。

感興趣的可以參考源碼註釋:

     * Because TreeNodes are about twice the size of regular nodes, we
     * use them only when bins contain enough nodes to warrant use
     * (see TREEIFY_THRESHOLD). And when they become too small (due to
     * removal or resizing) they are converted back to plain bins.  In
     * usages with well-distributed user hashCodes, tree bins are
     * rarely used.  Ideally, under random hashCodes, the frequency of
     * nodes in bins follows a Poisson distribution
     * (http://en.wikipedia.org/wiki/Poisson_distribution) with a
     * parameter of about 0.5 on average for the default resizing
     * threshold of 0.75, although with a large variance because of
     * resizing granularity. Ignoring variance, the expected
     * occurrences of list size k are (exp(-0.5) * pow(0.5, k) /
     * factorial(k)). The first values are:
     *
     * 0:    0.60653066
     * 1:    0.30326533
     * 2:    0.07581633
     * 3:    0.01263606
     * 4:    0.00157952
     * 5:    0.00015795
     * 6:    0.00001316
     * 7:    0.00000094
     * 8:    0.00000006
     * more: less than 1 in ten million
     *

補充2:HashMap中的key若爲Object類型,需要實現哪些方法?

結論:如果HashMap中put的key爲自定義的類,那麼就必須要重寫equals()方法和hashCode()方法。

  • equals():所有類都隱式地繼承Object類,在Object類中equals()方法是用"=="來做判斷,也就是比較兩個對象引用指向的是不是同一塊內存地址。如果沒有重寫過equals()方法,那麼比較的是對象在內存中的地址,如果put了兩個完全相同的獨立的對象,按照正常的理解,兩個對象內容完全相等就可以認定爲兩個對象相等,那麼就會導致會導致HashMap中存在兩個相同的key!
  • hashCode():道理同上,Object類的hashCode()方法的返回值是由對象的地址轉換來的。那麼會出現這麼一種情況,對於兩個內容完全相同的對象,由於是存放在兩塊不同的內存地址上,調用hashCode()方法返回了兩個不同的哈希碼。把這兩個對象put到HashMap上,正常理解key相同,應該是映射到同一個位置上,但是由於hashCode不同,會導致HashMap中存在兩個相同的key!其次,HashMap在put的時候比較key,會先判斷哈希碼是否相同之後再判斷key是否相同,重寫hashCode()方法也有助於提高效率。另外,恰當的實現hashCode()也能減少哈希碰撞~

補充3:爲什麼String,Integer這樣的包裝類適合做HashMap中的key?

  • 這些類被final修飾,保證了key的不可更改性,所以不會存在存取時哈希碼不一致的情況;
  • 類的內部已經重寫過equals()、hashCode()方法,道理同補充2。(更新:String內部會維護一個哈希碼緩存,避免重複計算,Integer的hashCode()返回的是自身的int值,重複概率高,一般不推薦使用,一般HashMap的key推薦使用String)

參考資料

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章