1. 程式人生 > >面試題--HashMap詳解

面試題--HashMap詳解

先上hashCode和equals原始碼:

/** JNI,呼叫底層其它語言實現 */  
public native int hashCode();  

/** 默認同==,直接比較物件 */  
public boolean equals(Object obj) {  
    return (this == obj);  
}  

equals方法:String類中重寫了equals方法,比較的是字串值,看一下原始碼實現:

public boolean equals(Object anObject) {  
    if (this == anObject) {  
        return
true; } if (anObject instanceof String) { String anotherString = (String) anObject; int n = value.length; if (n == anotherString.value.length) { char v1[] = value; char v2[] = anotherString.value; int i = 0; // 逐個判斷字元是否相等
while (n-- != 0) { if (v1[i] != v2[i]) return false; i++; } return true; } } return false; }

重寫equals要滿足幾個條件:

  • 自反性:對於任何非空引用值 x,x.equals(x) 都應返回 true。
  • 對稱性:對於任何非空引用值 x 和 y,當且僅當 y.equals(x) 返回 true 時,x.equals(y) 才應返回 true。
  • 傳遞性:對於任何非空引用值 x、y 和 z,如果 x.equals(y) 返回 true,並且 y.equals(z) 返回 true,那麼 x.equals(z) 應返回 true。
  • 一致性:對於任何非空引用值 x 和 y,多次呼叫 x.equals(y) 始終返回 true 或始終返回 false,前提是物件上 equals 比較中所用的資訊沒有被修改。
  • 對於任何非空引用值 x,x.equals(null) 都應返回 false
  • Object 類的 equals 方法實現物件上差別可能性最大的相等關係;即,對於任何非空引用值 x 和 y,當且僅當 x 和 y 引用同一個物件時,此方法才返回 true(x == y 具有值 true)。 當此方法被重寫時,通常有必要重寫 hashCode 方法,以維護 hashCode 方法的常規協定,該協定宣告相等物件必須具有相等的雜湊碼。

    下面來說說hashCode方法,這個方法我們平時通常是用不到的,它是為雜湊家族的集合類框架(HashMap、HashSet、HashTable)提供服務,hashCode 的常規協定是:

  • 在 Java 應用程式執行期間,在同一物件上多次呼叫 hashCode 方法時,必須一致地返回相同的整數,前提是物件上 equals 比較中所用的資訊沒有被修改。從某一應用程式的一次執行到同一應用程式的另一次執行,該整數無需保持一致。
  • 如果根據 equals(Object) 方法,兩個物件是相等的,那麼在兩個物件中的每個物件上呼叫 hashCode 方法都必須生成相同的整數結果。
  • 以下情況不 是必需的:如果根據 equals(java.lang.Object) 方法,兩個物件不相等,那麼在兩個物件中的任一物件上呼叫 hashCode 方法必定會生成不同的整數結果。但是,程式設計師應該知道,為不相等的物件生成不同整數結果可以提高雜湊表的效能。
  • HashMap的類結構如下:
    java.util 
    類 HashMap < K,V>
    
    java.lang.Object
      繼承者 java.util.AbstractMap< K,V>
          繼承者 java.util.HashMap< K,V>
    
    所有已實現的介面:
    Serializable,Cloneable,Map< K,V>
    直接已知子類:
    LinkedHashMap,PrinterStateReasons

    HashMap中我們最常用的就是put(K, V)和get(K)。我們都知道,HashMap的K值是唯一的,那如何保證唯一性呢?我們首先想到的是用equals比較,沒錯,這樣可以實現,但隨著內部元素的增多,put和get的效率將越來越低,這裡的時間複雜度是O(n),假如有1000個元素,put時需要比較1000次。實際上,HashMap很少會用到equals方法,因為其內通過一個雜湊表管理所有元素,雜湊是通過hash單詞音譯過來的,也可以稱為散列表,雜湊演算法可以快速的存取元素,當我們呼叫put存值時,HashMap首先會呼叫K的hashCode方法,獲取雜湊碼,通過雜湊碼快速找到某個存放位置,這個位置可以被稱之為bucketIndex,通過上面所述hashCode的協定可以知道,如果hashCode不同,equals一定為false,如果hashCode相同,equals不一定為true。所以理論上,hashCode可能存在衝突的情況,有個專業名詞叫碰撞,當碰撞發生時,計算出的bucketIndex也是相同的,這時會取到bucketIndex位置已儲存的元素,最終通過equals來比較,equals方法就是雜湊碼碰撞時才會執行的方法,所以前面說HashMap很少會用到equals。HashMap通過hashCode和equals最終判斷出K是否已存在,如果已存在,則使用新V值替換舊V值,並返回舊V值,如果不存在 ,則存放新的鍵值對< K, V>到bucketIndex位置。

    這裡寫圖片描述

    現在我們知道,執行put方法後,最終HashMap的儲存結構會有這三種情況,情形3是最少發生的,雜湊碼發生碰撞屬於小概率事件。到目前為止,我們瞭解了兩件事:

  • HashMap通過鍵的hashCode來快速的存取元素。
  • 當不同的物件hashCode發生碰撞時,HashMap通過單鏈表來解決,將新元素加入連結串列表頭,通過next指向原有的元素。單鏈表在Java中的實現就是物件的引用(複合)。
  • HashMap中put方法原始碼:

    public V put(K key, V value) {  
        // 處理key為null,HashMap允許key和value為null  
        if (key == null)  
            return putForNullKey(value);  
        // 得到key的雜湊碼  
        int hash = hash(key);  
        // 通過雜湊碼計算出bucketIndex  
        int i = indexFor(hash, table.length);  
        // 取出bucketIndex位置上的元素,並迴圈單鏈表,判斷key是否已存在  
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {  
            Object k;  
            // 雜湊碼相同並且物件相同時  
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {  
                // 新值替換舊值,並返回舊值  
                V oldValue = e.value;  
                e.value = value;  
                e.recordAccess(this);  
                return oldValue;  
            }  
        }  
    
        // key不存在時,加入新元素  
        modCount++;  
        addEntry(hash, key, value, i);  
        return null;  
    }  

    Java Collections Framework中實際操作的都是陣列或者連結串列,而我們通常不需要顯示的維護集合的大小,而是集合類框架中內部維護,方便的同時,也帶來了效能的問題。

    HashMap有兩個引數影響其效能:初始容量和載入因子。預設初始容量是16,載入因子是0.75。容量是雜湊表中桶(Entry陣列)的數量,初始容量只是雜湊表在建立時的容量。載入因子是雜湊表在其容量自動增加之前可以達到多滿的一種尺度。當雜湊表中的條目數超出了載入因子與當前容量的乘積時,通過呼叫 rehash 方法將容量翻倍。

    HashMap中定義的成員變數如下:

    /** 
     * The default initial capacity - MUST be a power of two. 
     */  
    static final int DEFAULT_INITIAL_CAPACITY = 16;// 預設初始容量為16,必須為2的冪  
    
    /** 
     * 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;// 最大容量為2的30次方  
    
    /** 
     * The load factor used when none specified in constructor. 
     */  
    static final float DEFAULT_LOAD_FACTOR = 0.75f;// 預設載入因子0.75  
    
    /** 
     * The table, resized as necessary. Length MUST Always be a power of two. 
     */  
    transient Entry<K,V>[] table;// Entry陣列,雜湊表,長度必須為2的冪  
    
    /** 
     * The number of key-value mappings contained in this map. 
     */  
    transient int size;// 已存元素的個數  
    
    /** 
     * The next size value at which to resize (capacity * load factor). 
     * @serial 
     */  
    int threshold;// 下次擴容的臨界值,size>=threshold就會擴容  
    
    
    /** 
     * The load factor for the hash table. 
     * 
     * @serial 
     */  
    final float loadFactor;// 載入因子  

    我們看欄位名稱大概就能知道其含義,看Doc描述就能知道其詳細要求,這也是我們日常編碼中特別需要注意的地方,不要寫讓別人看不懂的程式碼,除非你寫的程式碼是一次性的。需要注意的是,HashMap中的容量MUST be a power of two,翻譯過來就是必須為2的冪,這裡的原因稍後再說。再來看一下HashMap初始化,HashMap一共過載了4個構造方法,分別為:

    構造方法摘要
    HashMap()
              構造一個具有預設初始容量 (16) 和預設載入因子 (0.75) 的空 HashMap。
    HashMap(int initialCapacity)
              構造一個帶指定初始容量和預設載入因子 (0.75) 的空 HashMap。
    HashMap(int initialCapacity, float loadFactor)
              構造一個帶指定初始容量和載入因子的空 HashMap。
    HashMap(Map<? extendsK,? extendsV> m)
              構造一個對映關係與指定 Map 相同的 HashMap。

    看一下第三個構造方法原始碼,其它構造方法最終呼叫的都是它。

    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);  
    
        // Find a power of 2 >= initialCapacity  
        // 這裡需要注意一下  
        int capacity = 1;  
        while (capacity < initialCapacity)  
            capacity <<= 1;  
    
        // 設定載入因子  
        this.loadFactor = loadFactor;  
        // 設定下次擴容臨界值  
        threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);  
        // 初始化雜湊表  
        table = new Entry[capacity];  
        useAltHashing = sun.misc.VM.isBooted() &&  
                (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);  
        init();  
    }  

    我們在日常做底層開發時,必須要嚴格控制入參,可以參考一下Java原始碼及各種開源專案原始碼,如果引數不合法,適時的丟擲一些執行時異常,最後到應用層捕獲。看第14-16行程式碼,這裡做了一個移位運算,保證了初始容量一定為2的冪,假如你傳的是5,那麼最終的初始容量為8。原始碼中的位運算隨處可見啊=。=!

    到現在為止,我們有一個很強烈的問題,為什麼HashMap容量一定要為2的冪呢?HashMap中的資料結構是陣列+單鏈表的組合,我們希望的是元素存放的更均勻,最理想的效果是,Entry陣列中每個位置都只有一個元素,這樣,查詢的時候效率最高,不需要遍歷單鏈表,也不需要通過equals去比較K,而且空間利用率最大。那如何計算才會分佈最均勻呢?我們首先想到的就是%運算,雜湊值%容量=bucketIndex,SUN的大師們是否也是如此做的呢?我們閱讀一下這段原始碼:

    /** 
     * Returns index for hash code h. 
     */  
    static int indexFor(int h, int length) {  
        return h & (length-1);  
    }  

    又是位運算,高帥富啊!這裡h是通過K的hashCode最終計算出來的雜湊值,並不是hashCode本身,而是在hashCode之上又經過一層運算的hash值,length是目前容量。這塊的處理很有玄機,與容量一定為2的冪環環相扣,當容量一定是2^n時,h & (length - 1) == h % length,它倆是等價不等效的,位運算效率非常高,實際開發中,很多的數值運算以及邏輯判斷都可以轉換成位運算,但是位運算通常是難以理解的,因為其本身就是給電腦運算的,運算的是二進位制,而不是給人類運算的,人類運算的是十進位制,這也是位運算在普遍的開發者中間不太流行的原因(門檻太高)。這個等式實際上可以推理出來,2^n轉換成二進位制就是1+n個0,減1之後就是0+n個1,如16 -> 10000,15 -> 01111,那根據&位運算的規則,都為1(真)時,才為1,那0≤運算後的結果≤15,假設h <= 15,那麼運算後的結果就是h本身,h >15,運算後的結果就是最後三位二進位制做&運算後的值,最終,就是%運算後的餘數,我想,這就是容量必須為2的冪的原因。HashTable中的實現對容量的大小沒有規定,最終的bucketIndex是通過取餘來運算的。

    通常,預設載入因子 (.75) 在時間和空間成本上尋求一種折衷。載入因子過高雖然減少了空間開銷,但同時也增加了查詢成本(在大多數 HashMap 類的操作中,包括 get 和 put 操作,都反映了這一點,可以想想為什麼)。在設定初始容量時應該考慮到對映中所需的條目數及其載入因子,以便最大限度地降低 rehash 操作次數。如果初始容量大於最大條目數除以載入因子(實際上就是最大條目數小於初始容量*載入因子),則不會發生 rehash 操作。

    如果很多對映關係要儲存在 HashMap 例項中,則相對於按需執行自動的 rehash 操作以增大表的容量來說,使用足夠大的初始容量建立它將使得對映關係能更有效地儲存。 當HashMap存放的元素越來越多,到達臨界值(閥值)threshold時,就要對Entry陣列擴容,這是Java集合類框架最大的魅力,HashMap在擴容時,新陣列的容量將是原來的2倍,由於容量發生變化,原有的每個元素需要重新計算bucketIndex,再存放到新陣列中去,也就是所謂的rehash。HashMap預設初始容量16,載入因子0.75,也就是說最多能放16*0.75=12個元素,當put第13個時,HashMap將發生rehash,rehash的一系列處理比較影響效能,所以當我們需要向HashMap存放較多元素時,最好指定合適的初始容量和載入因子,否則HashMap預設只能存12個元素,將會發生多次rehash操作。

    HashMap所有集合類檢視所返回迭代器都是快速失敗的(fail-fast),在迭代器建立之後,如果從結構上對對映進行修改,除非通過迭代器自身的 remove 或 add 方法,其他任何時間任何方式的修改,迭代器都將丟擲 ConcurrentModificationException。因此,面對併發的修改,迭代器很快就會完全失敗。注意,迭代器的快速失敗行為不能得到保證,一般來說,存在不同步的併發修改時,不可能作出任何堅決的保證。快速失敗迭代器盡最大努力丟擲 ConcurrentModificationException。

    HashMap是執行緒不安全的實現,而HashTable是執行緒安全的實現,所謂執行緒不安全,就是在多執行緒情況下直接使用HashMap會出現一些莫名其妙不可預知的問題,多執行緒和單執行緒的區別:單執行緒只有一條執行路徑,而多執行緒是併發執行(非並行),會有多條執行路徑。如果HashMap是隻讀的(載入一次,以後只有讀取,不會發生結構上的修改),那使用沒有問題。那如果HashMap是可寫的(會發生結構上的修改),則會引發諸多問題,如上面的fail-fast,也可以看下這裡,這裡就不去研究了。

    那在多執行緒下使用HashMap我們需要怎麼做,幾種方案:

  • 在外部包裝HashMap,實現同步機制
  • 使用Map m = Collections.synchronizedMap(new HashMap(…));,這裡就是對HashMap做了一次包裝
  • 使用java.util.HashTable,效率最低
  • 使用java.util.concurrent.ConcurrentHashMap,相對安全,效率較高
  • 相關推薦

    試題--HashMap

    先上hashCode和equals原始碼: /** JNI,呼叫底層其它語言實現 */ public native int hashCode(); /** 默認同==,直接比較物件 */ public boolean equals(Object

    百度、騰訊、阿里、谷歌 試題視訊合集

    我特意整理了一下百度、騰訊、阿里、谷歌 面試題,有很多問題不是靠幾句話能講清楚,所以乾脆找一些一線公司大佬錄得的視訊,用來回來這些面試題。很多問題其實答案很簡單,但是背後的思考和邏輯不簡單,要做到知其然還要知其所以然。 以下目錄全為視訊講解,沒有任何套路,就是免

    堆疊相關試題

    1. 實現一個棧,要求實現Push(出棧)、Pop(入棧)、Min(返回最小值的操作)時間複雜度為O(1)。 分析:設計兩個棧,一個棧用來push 、pop操作,另一個棧用來儲存當前最小值Min。當push元素小於等於Min棧頂元素時,將其壓入Min棧頂,當pop元素等於M

    js試題知識點全(一作用域和閉包)

    foo true 方式 聲明 提升 function 這樣的 all 變量提升 問題: 1.說一下對變量提升的理解 2.說明this幾種不同的使用場景 3.如何理解作用域 4.實際開發中閉包的應用 知識點: js沒有塊級作用域只有函數和全局作用域,如下代碼: if(tru

    HashMap

    == nan lba illegal 需要 我們 bar toolbar 它的 HashMap也是我們使用非常多的Collection,它是基於哈希表的 Map 接口的實現,以key-value的形式存在。在HashMap中,key-value總是會當做一個整體來處理,

    JAVA 圖形界開發基礎

    .so 積木 並且 init 中間 ram stat 字符 tle /*文章中用到的代碼只是一部分,需要源碼的可通過郵箱聯系我 [email protected]*/ 與C的win32一樣,JAVA也有自己的圖形界面開發,將在此篇博客中對基礎部分進行講解。 1.Java提供

    BBS項目 登錄界代碼

    raw 數字0 efault import fun pri panel 行修改 use forms from django import forms# 創建form類class LoginInfo(forms.Form): # 創建賬號字段 username =

    淺談java試題hashmap,hashtable,treemap,linkedhashmap的區別

    淺談java面試題hashmap,hashtable,treemap,linkedhashmap的區別 hashmap hashtable TreeMap LinkedHashMap #先談談hashmap的實

    java程式設計思想讀書筆記三(HashMap

    Map Map介面規定了一系列的操作,作為一個總規範它所定義的方法也是最基礎,最通用的。 AbstractMap AbstractMap是HashMap、TreeMap,、ConcurrentHashMap 等類的父類。當我們巨集觀去理解Map時會發現,其實Map就是一

    HashMap

    紅黑樹性質 紅黑樹是平衡二叉樹的一種, 但是它的平衡因子是可以大於 1 紅黑樹的節點要麼是紅色, 要麼是黑色, 這裡的紅黑色只是用來區分的一種方式, 為了定義規則 根節點一定是黑色 葉子節點也是黑色, 實際上葉子節點都是由 NULL 組成 紅色節點的子節點是黑色 根節點到葉

    HashMap

    連結串列轉樹結構 根據詳解四, 當連結串列長度大於 8 時, 為了更高效的查詢, 需要轉成紅黑樹結構, 使用的方法是 treeifyBin. 過程是先把連結串列結構調整為雙向連結串列結構, 再把雙向連結串列結構調整為紅黑樹結構. /** * tab: 陣列 * hash: 新節點 key 的雜

    Java:hashMap

    原 Java集合:HashMap詳解(JDK 1.8) 置頂 2018年01月07日 18:00:41 JoonWhee

    經------JVM

    1、記憶體模型 堆:Java虛擬機器管理記憶體中最大的一塊,執行緒共享區域。所有物件例項和陣列都在堆上分配記憶體空間。 棧:在Hotspot中虛擬機器棧和本地方法棧是在一起的。它是執行緒私有,每個執行緒都會建立一個虛擬機器棧,生命週期與執行緒相同。每個方法被執

    HashMap( JDK8 之前與之後對比)

    HashMap簡介 HashMap 是一個散列表,它儲存的內容是鍵值對(key-value)對映。 HashMap 繼承於AbstractMap,實現了Map、Cloneable、java.io.Serializable介面。 HashMap 的實現不是同步的,這意味著它不是執行緒安全的。它的k

    hashMap與例項

    在Java集合類中最常用的除了ArrayList外,就是HashMap了。本文儘自己所能,儘量詳細的解釋HashMap的原始碼。一山還有一山高,有不足之處請之處,定感謝指定並及時修正。     在看HashMap原始碼之前先複習一下資料結構。     Ja

    2017年華為Fx計劃線上機試題

    1,題目描述 小k是x區域的銷售經理,他平時常駐“5”城市,並且經常要到“1”,“2”,“3”,“4”,“6”城市出差。當機場出現大霧的情況下,會導致對應城市的所有航班的起飛及降落均停止(即不能從該城市出發,其他城市也不能到達該城市)。小k希望知道如果他需要到

    java開發常被問到的試題-HashMap的底層原理

    java開發人員面試的時候會經常被問到HashMap的底層是怎麼實現的,以下做簡要分析: HashMap是基於雜湊表的Map介面的非同步實現, HashMap實際上是一個“連結串列雜湊”的資料結構,即陣列和連結串列的結合體。 首先來了解一下資料結構中陣

    java中HashMap

    上面方法的程式碼很簡單,但其中包含了一個非常優雅的設計:系統總是將新新增的 Entry 物件放入 table 陣列的 bucketIndex 索引處——如果 bucketIndex 索引處已經有了一個 Entry 物件,那新新增的 Entry 物件指向原有的 Entry 物件(產生一個 Entry 鏈),如果

    Java集合(四)HashMap

    HashMap簡介 java.lang.Object ↳ java.util.AbstractMap<K, V> ↳ java.util.HashMap<K, V> public class HashMap<K,V> ex

    Java8 HashMap

    Java8 HashMap Java8 對 HashMap 進行了一些修改,最大的不同就是利用了紅黑樹,所以其由 陣列+連結串列+紅黑樹 組成。 根據 Java7 HashMap 的介紹,我們知道,查詢的時候,根據 hash 值我們能夠快速定位到陣列的具