1. 程式人生 > >深入理解HashMap

深入理解HashMap

什麼是HashMap

HashMap作為Java語言中一種重要的型別,其儲存資料通過鍵值對的形式儲存,即<key,value>的形式。HashMap繼承AbstractMap類,最終實現的是Map介面。HashMap中資料的Key值不允許重複,HashMap中儲存的資料可以為null,由於其key值不能重複,則也只有一個物件的key值可以為null。HashMap通過hashcode()來獲取雜湊值來決定在Map中的儲存位置,所以HashMap中資料的儲存順序與存入順序無關。
HashMap繼承關係圖

HashMap的基本用法

class StudentInfo
{
    private
int id; private String homeTown; private int score; private final static StudentInfo INSTANCE = new StudentInfo(); StudentInfo(){} StudentInfo(int id, String homeTown, int score) { this.id = id; this.homeTown = homeTown; this.score = score; } public
StudentInfo getInstance(){return INSTANCE;} public int getId() {return this.id;} public void setId(int id) {this.id = id;} public String getHomeTown() {return this.homeTown;} public void setHomeTown(String homeTown) {this.homeTown = homeTown;} public int getScore() {return this
.score;} public void setScore(int score) {this.score = score;} }

這裡首先定義一個學生資訊類,主要包含學生的id、家鄉和成績三條屬性以及set和get方法

   Map<String,StudentInfo> map = new HashMap<String, StudentInfo>();
   StudentInfo Tom = new StudentInfo(1,"New York",90);
   StudentInfo Abell = new StudentInfo(2,"Houston",95);

   map.put("Tom",Tom);
   map.put("Abell",Abell);

   StudentInfo aStudent = map.get("Abell");
   System.out.println("Id: " + aStudent.getId() + " Hometown: " + 
            aStudent.getHomeTown() + " Score: " + aStudent.getScore());

HashMap的類定義是一種範型的形式public class HashMap<K,V>所以HashMap的Key和Value值可以是各種型別。呼叫HashMap的put方法進行資料的儲存,呼叫get方法依靠key值來獲取對應的value值。
該程式的輸出為:

Id: 2 Hometown: Houston Score: 95

HashMap中put()和get()方法的實現

想要更好的理解HashMap,閱讀其原始碼是很有必要的,put()和get()方法又是HashMap資料操作中的最基本的兩個方法。put()和get()方法中又呼叫了我們常說的hashcode()以及equal()的方法,其中hashcode()用來獲取物件的雜湊值,equal()用來比較兩個物件是否相等;這兩個方法都是從Object類中繼承。這裡我們逐步給出分析。首先我們來看一下put()方法。

put()方法的實現

    /**
     * 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 {@code key}, or
     *         {@code null} if there was no mapping for {@code key}.
     *         (A {@code null} return can also indicate that the map
     *         previously associated {@code null} with {@code key}.)
     */
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

簡單來說,在執行put操作時,將根據插入的key值與之前的資料進行比較,且該方法以範型的形式進行定義,其返回值與value的型別相同。如果入參Key值相同(當然Key的hash值也需要相同)但value值不同的話,則方法返回老的value值,且將用新的value代替老的value值。如果新插入的資料與老資料沒有發生碰撞,即沒有因為key值相同重複,則put()方法返回null。

putVal()方法底層是通過陣列+連結串列+紅黑樹演算法實現的。

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict)

putVal()方法的入參如上,分別儲存為hash值、key和value;其中onlyIfAbsent,evict引數再HashMap格式中都沒有使用

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

在put()方法中先根據傳入的hash值進行判斷,通過(n-1) & hash來決定資料的儲存位置,其中n為當前儲存資料表的大小,若傳入的hash值與已存在的資料的hash值相同,則發生碰撞,將不走該if的邏輯。若tab[i] == null,則未發生碰撞直接將資料儲存。在此判斷中同時給p賦值,Node<K,V> p。可以理解p儲存了發生碰撞的資料的值,Node<K,V>為連結串列的格式,用來儲存單條資料。
如果沒有碰撞,將資料儲存之後,直接跳到以下程式碼進行處理

    ++modCount;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;

modCount為int型別,記錄HashMap結構變更的次數,在執行put()方法時,只有新增條目的時候才會發生結構的變更,同時該變數在remove()等操作中也將記錄次數的增加。size變數記錄HashMap中有多少個鍵值對,其大小限制為threshold,檢視resize()的原始碼,我們可以知道其初始值為16*0.75,當size大小超過threshold後再此呼叫resize()方法,將容量擴帶為當前的兩倍。
如果資料發生碰撞,則執行else的邏輯,這裡定義了Node<K,V> e; K k;,為儲存資料的臨時變數。afterNodeInsertion(evict)方法再HashMap中為空操作;執行return null結束put()方法的呼叫。

如果判斷hash值時,發生了資料的碰撞,將轉入下面的邏輯進行處理

    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;
        }
    }
if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;

我們分段來看一下這部分程式,如果入參與碰撞的資料hash值、並且key值相同,則直接用臨時變數來儲存發生碰撞的老資料,然後跳到後面對碰撞資料的處理

    if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }

在該部分中,首先用變數oldValue儲存老的value資料,然後通過將傳入的value賦值給e.value完成資料的覆蓋;隨後afterNodeAccess(e)在HashMap中操作為空(在LinkedHashMap中對該方法進行了重寫),return了發生碰撞老的value值。其餘分支的程式碼為儲存桶邏輯和紅黑樹的邏輯,從原始碼中我們可以看到如果桶的大小增長到8之後,將轉成紅黑樹進行處理。

get()方法的實現

看完put()方法之後,再來看一下get()方法的實現

/**
     * Returns the value to which the specified key is mapped,
     * or {@code null} if this map contains no mapping for the key.
     *
     * <p>More formally, if this map contains a mapping from a key
     * {@code k} to a value {@code v} such that {@code (key==null ? k==null :
     * key.equals(k))}, then this method returns {@code v}; otherwise
     * it returns {@code null}.  (There can be at most one such mapping.)
     *
     * <p>A return value of {@code null} does not <i>necessarily</i>
     * indicate that the map contains no mapping for the key; it's also
     * possible that the map explicitly maps the key to {@code null}.
     * The {@link #containsKey containsKey} operation may be used to
     * distinguish these two cases.
     *
     * @see #put(Object, Object)
     */
    public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }

從上面的原始碼可以看到,get()方法入參味key值,這裡沒用範型的形式,而是直接定義為Object型別引數,通過呼叫getNode()完成進一步的資料查詢操作。因為這部分原始碼也比較簡單,這裡直接給出分析

   /**
     * Implements Map.get and related methods.
     *
     * @param hash hash for key
     * @param key the key
     * @return the node, or null if none
     */
    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;
    }
 if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null)

首先也是進行臨時變數的定義,然後進行判空,如果資料表為空直接返回null,進一步在這個判斷中也通過hash值對資料的位置進行判斷(所以說如果要用自定義的類做key值的時候,重寫hashcode()是很必要的)。

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

這部分將以連結串列的形式對整個資料表進行遍歷,首先檢查連結串列中的第一個Node,之後通過連結串列的指標逐個指向後面的元素,進行資料的查詢,直到找到匹配的資料或者指標指到最後一個元素。

重寫hashcode()和equal()

當我們使用自定義的型別作為HashMap的key值的時候,我們需要重寫一下hashcode()equal()方法。在說原因之前,我們先看一個例子

class People
{
    private String name;

    People(){}
    People(String name){this.name = name;}

    public void setName(String name){this.name = name;}
    public String getName(){return this.name;}

首先定義一個簡單的People類,其中有對應的name屬性

        People jack = new People("Tony");
        int age = 10;

        Map<People,Integer> hasmap = new HashMap<People,Integer>();
        hasmap.put(jack,age);

        System.out.print("The Tony's age is : ");
        System.out.print(hasmap.get(new People("Tony")));

之後我們定義一個People的物件,名叫做Tony,然後將它放到hashmap中,然後用get方法來取出這個名叫Jack的People變數,看一下輸出結果

The Tony's age is : null

為什麼是null,而不是我們期望的10。這裡就可以想一下剛才看到的get()方法的原始碼。對,因為兩個People物件並不是同一個,所以其對應的HashCode也不相同,然而我們就像通過相同的名字來取資料怎麼辦。這裡就需要在People類中重寫hashcode()和euqal()方法了。

    @Override
    public int hashCode()
    {
        return name.hashCode();
    }

    @Override
    public boolean equals(Object obj)
    {
        if(obj instanceof People)
        {
            People inPeople = (People)obj;
            return this.name.equals(inPeople.getName());
        }
        return false;
    }

重寫之後,將不在呼叫Object類中的方法,再次執行,結果符合預期

The Tony's age is : 10

總結

我們都知道如果用陣列的形式儲存資料,執行插入或者刪除的操作的時候,陣列中所有元素都要移動,該操作對於頻繁的插入、刪除操作將十分耗記憶體;而使用連結串列的形式,雖不需要去移動資料,但執行查詢遍歷整個連結串列又是十分耗時的。

HashMap通過hash值的方式很好的解決了連結串列查詢耗時的缺點,同時通過動態的擴容以及靈活的指標操作,解決了記憶體損耗的問題。可以說是兩種基本儲存結構的結合體,也是在Java中頻繁使用的一種資料儲存格式。

通過此文,小火箭希望大家對HashMap的實現有更好的理解,避免程式設計中的問題。更多文章可以持續關注小火箭的微訊號

寫程式的小火箭

相關推薦

深入理解HashMap(及hash函數的真正巧妙之處)

ssa 什麽 關聯 表示 廣泛 要求 傳遞 所有 總結 原文地址:http://www.iteye.com/topic/539465 Hashmap是一種非常常用的、應用廣泛的數據類型,最近研究到相關的內容,就正好復習一下。網上關於hashmap的文章很多,但到底是自己

深入理解 HashMap

包含 刪除 不同 鍵值 code 1.8 信息 索引 導致 1. 簡介 HashMap 是Java開發中使用頻率最高的鍵值對數據類型容器。它根據鍵的哈希值(hashCode)來存儲數據,訪問速度高,但無法按照順序遍歷。HashMap 允許鍵值為空和記錄為空,非線程安全。 另

深入理解HashMap及面試相關問答

前言 HashMap是面試必備的一個知識點,無論你是初級中級還是高階,基本上逃不過這個問題,下面的內容很簡單,只要你理解了其中的含義,這對你使用hashmap和麵試都是很有幫助的。 正文 首先開啟HashMap,看看中都定義了哪些成員變數。 解釋幾個重點的變數 transi

深入理解hashmap理論篇

之前有過一篇介紹java中hashmap使用的,深入理解hashmap,比較側重於 程式碼分析,沒有從理論上分析hashmap,今天把hashmap的理論部分補充一下(之後應該還有兩篇補充 一篇講紅黑樹一篇講多執行緒)。 雜湊(雜湊)函式到底是幹嘛的?和雜湊表是啥關係?其主要作用和應用場景到底在哪裡? 簡

深入理解hashmap(三)雜湊表和二叉搜尋樹的恩怨情仇

前面兩篇文章介紹了hashmap的原始碼和理論,今天把剩餘的部分紅黑樹講一下。理解好紅黑樹,對我們後續對hashmap或者其他資料結構的理解都是很有好處的。比方說為什麼後面jdk要把hashmap中的單鏈表更新成紅黑樹? 要理解紅黑樹首先要弄清楚普通二叉樹的一些基本概念 父節點和子節點,這個我就不多說了。

深入理解HashMap(原理,查詢,擴容)

面試的時候聞到了Hashmap的擴容機制,之前只看到了Hasmap的實現機制,補一下基礎知識,講的非常好 原文連結: Hashmap是一種非常常用的、應用廣泛的資料型別,最近研究到相關的內容,就正好複習一下。網上關於hashmap的文章很多,但到底是自己學習的總結,就

深入理解HashMap(一次性徹底掌握)

Hashmap是一種非常常用的、應用廣泛的資料型別,最近研究到相關的內容,就正好複習一下。網上關於hashmap的文章很多,但到底是自己學習的總結,就發出來跟大家一起分享,一起討論。  1、hashmap的資料結構  要知道hashmap是什麼,首先要搞清楚它的資料結構,在j

深入理解HashMap(及hash函式的真正巧妙之處)

Hashmap是一種非常常用的、應用廣泛的資料型別,最近研究到相關的內容,就正好複習一下。網上關於hashmap的文章很多,但到底是自己學習的總結,就發出來跟大家一起分享,一起討論。  1、hashmap的資料結構  要知道hashmap是什麼,首先要搞清楚它的資料結

從原始碼深入理解HashMap(附加HashMap面試題)

HashMap向來是面試中的熱點話題,深入理解了HashMap的底層實現後,才能更好的掌握它,也有利於在專案中更加靈活的使用。 本文基於JDK8進行解析 一、HashMap解析 1. 結構 HashMap結構由陣列加**連結串列(或紅黑樹)**構成。主幹是E

深入理解HashMap(精華必看)

3、hashmap的resize         當hashmap中的元素越來越多的時候,碰撞的機率也就越來越高(因為陣列的長度是固定的),所以為了提高查詢的效率,就要對hashmap的陣列進行擴容,陣列擴容這個操作也會出現在ArrayList中,所以這是一個通用的操作,很多人對它的效能表示過懷疑,不過想想我

深入理解HashMap及底層實現

概述:HashMap是我們常用的一種集合類,資料以鍵值對的形式儲存。我們可以在HashMap中儲存指定的key,value鍵值對;也可以根據key值從HashMap中取出相應的value值;也可以通過keySet方法返回key檢視進行迭代。以上是基於HashMap的常見應用,但是光會使用是遠

深入理解HashMap

什麼是HashMap HashMap作為Java語言中一種重要的型別,其儲存資料通過鍵值對的形式儲存,即<key,value>的形式。HashMap繼承AbstractMap類,最終實現的是Map介面。HashMap中資料的Key值不允許重複,Ha

深入理解HashMap原理

3、hashmap的resize        當hashmap中的元素越來越多的時候,碰撞的機率也就越來越高(因為陣列的長度是固定的),所以為了提高查詢的效率,就要對hashmap的陣列進行擴容,陣列擴容這個操作也會出現在ArrayList中,所以這是一個通用的操作,很多人對它的效能表示過懷疑,不過想想我們

深入理解HashMap原始碼

1.HashMap資料結構 首先先看下HashMap的資料結構圖 我們都知道陣列的儲存方式在是連續的,查詢速度比較快,但插入和刪除資料比較慢 而連結串列的儲存方式是非連續的,所以插入和刪除速度較快,但查詢速度就比較慢,HashMap在資料結構上兩種都採用了。

HashMap為什麼這麼快? ---深入理解HashMap的雜湊機制

在通過上篇部落格瞭解了 Map 的維持內部的 鍵-值 對的基本方式之後,----不瞭解的看這篇(Java集合的基本概括), 我們便會思考, 在 HashMap 內部是如何組織和排列這些封裝了 鍵-值對的 Map.Entry 實體呢? 如何組織才能達到更高的效率呢? 在瞭解

深入理解JAVA集合系列三:HashMap的死循環解讀

現在 最新 star and 場景 所有 image cap 時也 由於在公司項目中偶爾會遇到HashMap死循環造成CPU100%,重啟後問題消失,隔一段時間又會反復出現。今天在這裏來仔細剖析下多線程情況下HashMap所帶來的問題: 1、多線程put操作後,get操作導

hash函式 hashMap深入理解,jdk8 hashMap加入紅黑樹演算法

一 hash表的介紹 非hash表的特點:關鍵字在表中的位置和它之間不存在一個確定的關係,查詢的過程為給定值一次和各個關鍵進行比較,查詢的效率取決於和給定值進行比較的次數。 雜湊表的特點:關鍵字在表中位置和它之間存在一種確定的關係 雜湊函式:翻譯為雜湊,就是把任意長度的輸入,通過雜湊

深入理解 hashcode() 和 HashMap 中的hash 演算法

前言 Java中的HashMap非常常用也非常重要, 提到HashMap是離不開hashcode()方法的, 整天嘴邊掛著HashMap、Hashtable、TreeMap、LinkedHashMap、IdentityHashMap、ConcurrentHashMap和WeakHashMap等詞

HashMap深入理解

        容量(capacity):雜湊表中容器的數量,初始容量只是雜湊表在建立時的容量。         負載因子(load factor):雜湊表在其容量自動增加之前可以達到多滿的一種尺度。當雜湊表中的條目數超出了負載因子與當前容量的乘積時,則要對該雜湊表進行 rehash 操作(即重建內部資

Java 集合深入理解(17):HashMap 在 JDK 1.8 後新增的紅黑樹結構

上篇文章我們介紹了 HashMap 的主要特點和關鍵方法原始碼解讀,這篇文章我們介紹 HashMap 在 JDK1.8 新增樹形化相關的內容。 讀完本文你將瞭解到: 傳統 HashMap 的缺點 JDK 1.8 以前 HashMap 的實