HashMap的key能夠是可變的對象嗎???

HashMap的key能夠是可變的對象嗎???

  你們都知道,HashMap的是key-value(鍵值對)組成的,這個key既能夠是基本數據類型對象,如Integer,Float,同時也能夠是本身編寫的對象,那麼問題來了,這個做爲key的對象是否可以改變呢?或者說key可否是一個可變的對象?若是能夠該HashMap會怎麼樣?html

可變對象java

  可變對象是指建立後自身狀態能改變的對象。換句話說,可變對象是該對象在建立後它的哈希值(由類的hashCode()方法能夠得出哈希值)可能被改變面試

  爲了能直觀的看出哈希值的改變,下面編寫了一個類,同時重寫了該類的hashCode()方法和它的equals()方法【至於爲何要重寫equals方法能夠看博客:http://www.cnblogs.com/0201zcr/p/4769108.html】,在查找和添加(put方法)的時候都會用到equals方法。算法

  在下面的代碼中,對象MutableKey的鍵在建立時變量 i=10 j=20,哈希值是1291。shell

  而後咱們改變實例的變量值,該對象的鍵 i 和 j 從10和20分別改變成30和40。如今Key的哈希值已經變成1931。數組

  顯然,這個對象的鍵在建立後發生了改變。因此類MutableKey是可變的。安全

  讓咱們看看下面的示例代碼:數據結構

複製代碼
public class MutableKey {
    private int i;
    private int j;
 
    public MutableKey(int i, int j) {
        this.i = i;
        this.j = j;
    }
 
    public final int getI() {
        return i;
    }
 
    public final void setI(int i) {
        this.i = i;
    }
 
    public final int getJ() {
        return j;
    }
 
    public final void setJ(int j) {
        this.j = j;
    }
 
    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + i;
        result = prime * result + j;
        return result;
    }
 
    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        if (!(obj instanceof MutableKey)) {
            return false;
        }
        MutableKey other = (MutableKey) obj;
        if (i != other.i) {
            return false;
        }
        if (j != other.j) {
            return false;
        }
        return true;
    }
}
複製代碼

測試:多線程

複製代碼
public class MutableDemo {
 
    public static void main(String[] args) {
 
        // Object created
        MutableKey key = new MutableKey(10, 20);
        System.out.println("Hash code: " + key.hashCode());
 
        // Object State is changed after object creation.
        key.setI(30);
        key.setJ(40);
        System.out.println("Hash code: " + key.hashCode());
    }
}
複製代碼

結果:ide

Hash code: 1291
Hash code: 1931

   只要MutableKey 對象的成員變量i或者j改變了,那麼該對象的哈希值改變了,因此該對象是一個可變的對象。

HashMap如何存儲鍵值對

  HashMap底層是使用Entry對象數組存儲的,而Entry是一個單項的鏈表。當調用一個put()方法將一個鍵值對添加進來是,先使用hash()函數獲取該對象的hash值,而後調用indexFor方法查找到該對象在數組中應該存儲的下標,假如該位置爲空,就將value值插入,若是該下標出不爲空,則要遍歷該下標上面的對象,使用equals方法進行判斷,若是遇到equals()方法返回真的則進行替換,不然將其插入,源碼詳解可看:http://www.cnblogs.com/0201zcr/p/4769108.html

  查找時只須要查詢經過key值獲取獲取hash值,而後找到其下標,遍歷該下標下面的Entry對象便可查找到value。【具體看下面源碼及其解釋】

在HashMap中使用可變對象做爲Key帶來的問題

  若是HashMap Key的哈希值在存儲鍵值對後發生改變,Map可能再也查找不到這個Entry了

複製代碼
public V get(Object key)   
{   
 // 若是 key 是 null,調用 getForNullKey 取出對應的 value   
 if (key == null)   
     return getForNullKey();   
 // 根據該 key 的 hashCode 值計算它的 hash 碼  
 int hash = hash(key.hashCode());   
 // 直接取出 table 數組中指定索引處的值,  
 for (Entry<K,V> e = table[indexFor(hash, table.length)];   
     e != null;   
     // 搜索該 Entry 鏈的下一個 Entr   
     e = e.next)         // ①  
 {   
     Object k;   
     // 若是該 Entry 的 key 與被搜索 key 相同  
     if (e.hash == hash && ((k = e.key) == key   
         || key.equals(k)))   
         return e.value;   
 }   
 return null;   
}   
複製代碼

  上面是HashMap的get()方法源碼,經過上面咱們能夠知道,若是 HashMap 的每一個 bucket 裏只有一個 Entry 時,HashMap 能夠根據索引、快速地取出該 bucket 裏的 Entry;在發生「Hash 衝突」的狀況下,單個 bucket 裏存儲的不是一個 Entry,而是一個 Entry 鏈系統只能必須按順序遍歷每一個 Entry,直到找到想搜索的 Entry 爲止——若是剛好要搜索的 Entry 位於該 Entry 鏈的最末端(該 Entry 是最先放入該 bucket 中),那系統必須循環到最後才能找到該元素。 

  同時咱們也看到,判斷是否找到該對象,咱們還須要判斷他的哈希值是否相同,假如哈希值不相同,根本就找不到咱們要找的值。

  若是Key對象是可變的,那麼Key的哈希值就可能改變。在HashMap中可變對象做爲Key會形成數據丟失。

  下面的例子將會向你展現HashMap中有可變對象做爲Key帶來的問題。

複製代碼
import java.util.HashMap;
import java.util.Map;
 
public class MutableDemo1 {
 
    public static void main(String[] args) {
 
        // HashMap
        Map<MutableKey, String> map = new HashMap<>();
 
        // Object created
        MutableKey key = new MutableKey(10, 20);
 
        // Insert entry.
        map.put(key, "Robin");
 
        // This line will print 'Robin'
        System.out.println(map.get(key));
 
        // Object State is changed after object creation.
        // i.e. Object hash code will be changed.
        key.setI(30);
 
        // This line will print null as Map would be unable to retrieve the
        // entry.
        System.out.println(map.get(key));
    }
}
複製代碼

輸出:

Robin
null

 

如何解決

  在HashMap中使用不可變對象。在HashMap中,使用String、Integer等不可變類型用做Key是很是明智的。 

  咱們也能定義屬於本身的不可變類

  若是可變對象在HashMap中被用做鍵,那就要當心在改變對象狀態的時候,不要改變它的哈希值了。咱們只須要保證成員變量的改變能保證該對象的哈希值不變便可。

  在下面的Employee示例類中,哈希值是用實例變量id來計算的。一旦Employee的對象被建立,id的值就不能再改變。只有name能夠改變,但name不能用來計算哈希值。因此,一旦Employee對象被建立,它的哈希值不會改變。因此Employee在HashMap中用做Key是安全的。

複製代碼
import java.util.HashMap;
import java.util.Map;
 
public class MutableSafeKeyDemo {
 
    public static void main(String[] args) {
        Employee emp = new Employee(2);
        emp.setName("Robin");
 
        // Put object in HashMap.
        Map<Employee, String> map = new HashMap<>();
        map.put(emp, "Showbasky");
 
        System.out.println(map.get(emp));
 
        // Change Employee name. Change in 'name' has no effect
        // on hash code.
        emp.setName("Lily");
        System.out.println(map.get(emp));
    }
}
 
class Employee {
    // It is specified while object creation.
    // Cannot be changed once object is created. No setter for this field.
    private int id;
    private String name;
 
    public Employee(final int id) {
        this.id = id;
    }
 
    public final String getName() {
        return name;
    }
 
    public final void setName(final String name) {
        this.name = name;
    }
 
    public int getId() {
        return id;
    }
 
    // Hash code depends only on 'id' which cannot be
    // changed once object is created. So hash code will not change
    // on object's state change
    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + id;
        return result;
    }
 
    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        Employee other = (Employee) obj;
        if (id != other.id)
            return false;
        return true;
    }
}
複製代碼

輸出

Showbasky
Showbasky

   致謝:感謝您的耐心閱讀!

 

 

本文翻譯自 Coding Geek, 原文地址。英文水平有限,有些地方翻譯得不太精確

絕大多數Java開發者都在使用Map類,尤爲是HashMap。HashMap是一種簡單易用且強大的存取數據的方法。可是,有多少人知道HashMap內部是如何工做的?幾天前,爲了對這個基本的數據結構有深刻的瞭解,我閱讀大量的HashMap源碼(開始是Java7,而後是Java8)。在這篇文章裏,我會解釋HashMap的實現,介紹Java8的新實現,聊一聊性能,內存,還有使用HashMap時已知的一些問題。

內部存儲

HashMap 類實現了Map<k,v>接口,這個接口的基本主要方法有:

  • V put(K key, V value)
  • V get(Object key)
  • V remove(Object key)
  • Boolean containsKey(Object key)

HashMap使用了內部類Entry<k,v>來存儲數據,這個類是一個帶有兩個額外數據的簡單 鍵-值對 結構:

  • 一個是另外一個Entry<k,v>的引用,這樣HashMap能夠像單獨的鏈表同樣存儲數據
  • 一個hash值,表明了key的哈希值,避免了HashMap每次須要的時候再來計算

下面是Java7裏Entry的部分實現:

static class Entry<K,V> implements Map.Entry<K,V> { final K key; V value; Entry<K,V> next; int hash; … } 

HashMap存儲數據到多個單獨的entry鏈表裏,全部的鏈表都登記到一個Entry數組裏(Entry<K,V>[] array),而且這個內部數組默認容量是16。

下面的圖片展現了一個HashMap實例的內部存儲,一個可爲null的Entry數組,每個Entry均可以連接到另外一個Entry來造成一個鏈表:

HashMap數據存儲示意圖

全部具備相同哈希值的key都會放到同一個鏈表裏,具備不一樣哈希值的key最終也有可能在同一個鏈表裏。

當調用 put(K key, V value)或者get(Object key)這些方法時,會先計算這個Entry應該存放的鏈表在內部數組中的索引(index),而後方法會迭代整個鏈表來尋找具備相同key的Entry(使用key的 equals()方法)

get()方法,會返回這個Entry關聯的value值(若是Entry存在)
put(K key, V value)方法,若是Entry存在則重置value值,若是不存在,則以key,value參數構造一個Entry並插入到鏈表的頭部。

獲取鏈表在數組內的索引經過三個步驟肯定:

  • 首先獲取Key的哈希值
  • 對哈希值再次進行哈希運算,避免出現一個不好的哈希算法,把全部的數據放到內部數組的同一個鏈表裏
  • 對再次哈希的哈希值進行數組長度(最小爲1)的位掩碼運算,這個運算保證生成的索引不會比數組的長度大,你能夠把它當成一個優化過的取模運算

下面是Java7 和 Java8處理索引的源代碼:

// the "rehash" function in JAVA 7 that takes the hashcode of the key static int hash(int h) { h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); } // the "rehash" function in JAVA 8 that directly takes the key static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } // the function that returns the index from the rehashed hash static int indexFor(int h, int length) { return h & (length-1); } 

爲了更高效的運做,內部數組的大小必須是2的指數大小,讓咱們來看看這是爲何。

想象一下數組大小是17,掩碼值就是16(size-1),16的二進制表示是 0…010000,那麼對於任何哈希值H經過位運算H AND 16獲得的索引就只會是16或者0,這意味着17大小的鏈表數組只會使用到兩個:索引爲0的和索引爲16的,很是浪費。

可是,若是你取2的指數大小例如16,位運算是 H AND 15,15的二進制表示是 0…001111, 那麼取索引的運算就會輸出0~15之間的值,大小16的數據就能徹底使用到。舉例:

  • 若是H = 952 二進制表示爲 0..0111011 1000, 相關的索引就是 0…01000 = 8
  • 若是H = 1576 二進制表示爲 0..01100010 1000, 相關的索引就是 0…01000 = 8
  • 若是H = 12356146 二進制表示爲 010111100100010100011 0010, 相關的索引就是 0…00010 = 2
  • 若是H = 59843 二進制表示爲 0111010011100 0011, 相關的索引就是 0…00011 = 3

這就是爲何數組的大小必須是2的指數大小,這個機制對開發人員是透明的,若是選擇了一個37大小的HashMap,那麼Map會自動選擇37以後的一個2的指數大小(64)來作爲內部數組的容量。

自動調整大小

咱們獲取到索引以後,函數(put,get或者remove) 訪問/迭代 關聯的鏈表,檢查是否有指定key對應的Entry。 不作改動的話,這個機制會帶來性能問題,由於這個函數會遍歷整個鏈表來檢查Entry是否存在。

想象一下若是內部數組大小是初始值16,咱們有兩百萬條數據須要存儲,最好的狀況下, 每一個鏈表裏平均有 125 000個數據(2000000/16).所以,每一個get(),remove(),put()會致使125 000個迭代或者操做。爲了不出現這種狀況,HashMap會自動調整它的內部數組大小來保持每一個鏈表儘量的短。

當你建立一個HashMap時,你能夠指定一個初始化大小和一個載入因數:

public HashMap(int initialCapacity, float loadFactor) 

若是不指定參數,缺省的initialCapacity是16,loadFactor是0.75,initialCapacity即表明了Map內部數組的大小。

每次當你調用put()方法加入一個新的Entry時,這個方法會檢測是否須要增長內部數組大小,所以map存儲了兩個數據:

  • map的大小,表明了HashMap裏 Entry的數量,每次新增或者移除Entry時都會更新這個值
  • 一個閾值: 內部數組大小 * 載入因數 ,每次自動調整大小後都會刷新

添加一個新Entry時,put函數會檢查 map的大小 是否大於閾值 ,若是大於,則會建立一個雙倍大小的數組,當新數組的大小改變,索引計算函數(返回 哈希值 & (數組大小-1) 的位運算)也會跟着改變。所以,數組的從新調整新建了兩倍數量的鏈表,而且 從新分發現有的Entry到這些數組內(注:原文括號有下面一句補充,暫時不明白是什麼意思。看HashMap的源代碼,是全部的數據分發到新的數組內,舊的直接棄用)

(the old ones and the newly created).

自動調整的目的是減小鏈表的長度從而減少 put(),remove(),get()等函數的時間開銷,全部具備相同哈希值的Entry在從新調整大小後還會在同一個鏈表內,原來在同一個鏈表內具備不一樣哈希值的Entry則有可能不在同一個鏈表內了。

上面這個圖展現了一個HashMap自動調整先後的狀況,在調整前,爲了拿到Entry E,必需要迭代5次,調整後,只須要兩次。速度快了兩倍!

注意:HashMap只會增長內部數組的大小,沒有提供方法變小。

線程安全

若是你已經瞭解過HashMap,你知道它不是線程安全的,可是有沒有想過爲何?

想象一下這種場景:你有一個寫線程只往Map裏寫新數據,還有一個讀線程只往裏讀數據,爲何不能很好的運做?

由於在從新調整內部數組大小的時候,若是線程正在寫或者取對象,Map可能會使用調整前的索引,這樣就找不到調整後的Entry所在的位置了。

最壞的狀況是:兩個線程同時往裏面放數據,同時調用了調整內部數組大小的方法。當兩個線程都在修改鏈表時,Map其中的某個鏈表可能會陷入一個內部循環,若是你試圖在這個鏈表裏取數據時,可能會永遠取不到值。

HashTable 爲了不這種狀況,作了線程安全的實現。可是,全部的CRUD方法都是 同步阻塞的,因此會很慢。例如,線程1調用get(key1),線程2調用get(key2),線程3調用get(key3),同一時間只會有一個線程能拿到值,即便他們原本能夠同時獲取這三個值。

其實從Java5開始就有一個更高效的線程安全的HashMap的實現了:ConcurrentHashMap。只有鏈表是同步阻塞的,所以多線程能夠同時get,put,或者remove數據,只要沒有訪問同一個鏈表或者從新調整內部數組大小就行。在多線程應用裏,使用這種實現顯然會更好。

key的不變性

爲何字符串和整數是HashMap的Key的一種很好的實現呢? 大可能是由於他們的不變性。若是你選擇本身新建一個Key類而且不保證它的不變性的話,在HashMap裏面可能就會丟失數據,讓咱們來看下面一種使用狀況:

  • 你有一個key,內部值是1
  • 你用這個key往HashMap裏存了一個數據
  • HashMap從這個key的哈希碼裏生成了一個哈希值(就是從1的哈希碼獲取)
  • Map在最近建立的Entry裏存儲了這個哈希值
  • 你把key的內部值改爲2
  • key的哈希碼改變了可是HashMap不知道(由於已經存了舊的哈希值)
  • 你想要用改變後的key獲取數據
  • Map會計算你的key的新哈希碼,來定位到數據位於哪一個鏈表:
    • 狀況1:你已經改了你的key,map試圖從錯誤的鏈表裏尋找數據,固然找不到
    • 狀況2:你很幸運!改變後的key生成的索引和改變前同樣,map遍歷整個鏈表尋找具備相同key的Entry。可是爲了匹配key,map先會匹配key的哈希值而後調用equals()方法來對照。由於你改變後的key哈希值也已經變了,map最終也找不到相應的Entry (注:應該也有可能找到錯誤的數據出來)

這裏有一個具體的例子,我存了兩個鍵值對到Map裏,我修改了第一個key而且試圖拿出這兩個值,只有第二個值有返回,第一個值已經丟失在Map裏:

public class MutableKeyTest { public static void main(String[] args) { class MyKey { Integer i; public void setI(Integer i) { this.i = i; } public MyKey(Integer i) { this.i = i; } @Override public int hashCode() { return i; } @Override public boolean equals(Object obj) { if (obj instanceof MyKey) { return i.equals(((MyKey) obj).i); } else return false; } } Map<MyKey, String> myMap = new HashMap<>(); MyKey key1 = new MyKey(1); MyKey key2 = new MyKey(2); myMap.put(key1, "test " + 1); myMap.put(key2, "test " + 2); // modifying key1 key1.setI(3); String test1 = myMap.get(key1); String test2 = myMap.get(key2); System.out.println("test1= " + test1 + " test2=" + test2); } } 

輸出結果是test1= null test2=test 2,和預期的同樣,Map用改變後的key1找不回第一個字符串。

JAVA8的改進

Java8裏,HashMap的內部表示已經改變了不少了。的確,Java7裏HashMap的實現有1K行代碼,而Java8裏有2K。我前面所說的大部分都是真的,除了Entry鏈表。在Java8裏,仍然存在一個內部數組不過裏面存儲的都是節點(Node),可是節點包含的信息和Entry徹底同樣,由於也能夠看作鏈表,下面是Java8裏節點實現的部分代碼:

static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; Node<K,V> next; 

那麼對比Java7最大的變化是什麼呢?節點(Nodes)能夠被樹節點(TreeNodes)繼承。樹節點是一種紅黑樹的數據結構,存儲了更多信息,可讓你以O(log(n))的算法複雜度新增,刪除或者是獲取一個元素。

下面是一個樹節點內存儲的數據的詳細列表供參考:

static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> { final int hash; // inherited from Node<K,V> final K key; // inherited from Node<K,V> V value; // inherited from Node<K,V> Node<K,V> next; // inherited from Node<K,V> Entry<K,V> before, after;// inherited from LinkedHashMap.Entry<K,V> TreeNode<K,V> parent; TreeNode<K,V> left; TreeNode<K,V> right; TreeNode<K,V> prev; boolean red; 

紅黑樹是一種自平衡的二分搜索樹。它的內部機制肯定了無論是新增仍是移除節點,長度永遠在log(n)內。使用這種樹的一個主要優勢是,當一個內部表有許多相同的數據在同一個容器內時,在樹中搜索會花費O(log(n))的時間複雜度,而鏈表會花費log(n)

如你所見,樹比鏈表佔用了更多的空間(咱們稍後會談到這個)。

經過繼承,內部表能夠包含 節點(鏈表) 和 樹節點(紅黑樹)兩種節點。Oracle經過下面的規則,決定同時使用這兩種數據結構:

  • 若是一個內部表的索引超過8個節點,鏈表會轉化爲紅黑樹
  • 若是內部表的索引少於6個節點,樹會變回鏈表

上圖展現了一個Java8 HashMap的內部數組的結構,具備樹(桶0),和鏈表(桶1,2,3) ,桶0由於有超過8個節點因此結構是樹。

內存開銷

JAVA7

使用HashMap會帶來必定的內存開銷,在Java7裏,一個HashMap用Entry包含了 許多鍵值對,一個Entry裏會有:

  • 下一個entry的引用
  • 一個預計算好的哈希值(整型)
  • 一個key的引用
  • 一個value的引用

此外,Java7裏 HashMap使用一個 Entry的內部數組。假設 一個HashMap包含了N個元素,內部數組容量是 C, 額外內存開銷約爲:
sizeOf(integer) * N + sizeOf(reference) * (3 * N +C)

  • 一個整數是 4 字節
  • 一個引用的大小取決於 JVM/OS/Precessor 不過一般也是4字節

小貼士:從JAVA7起,HashMap類初始化的方法是懶惰的,這意味着即便你分配了一個HashMap,內部Entry數組在內存裏也不會分配到空間( 4 * 數組大小 個字節),直到你調用第一個put()方法

JAVA8

java8的實現裏,獲取內存用量變得稍微複雜了一點。由於 Entry 和 樹節點包含的數據是同樣的,可是樹節點會多6個引用和1個布爾值。

若是所有都是 普通鏈表節點,那麼內存用量和java7同樣。
若是所有都是 樹節點,內存用量變成:
N * sizeOf(integer) + N * sizeOf(boolean) + sizeOf(reference)* (9*N+CAPACITY )
在大多數標準的 JVM裏,這個式子等於 44 * N + 4 * CAPACITY字節

性能問題

傾斜HashMap和平衡HashMap

最好的狀況下,get/put方法只有 O(1)的時間複雜度。可是,若是你不關心key的哈希函數,調用put/get/方法可能會很是慢。

put/get的良好性能取決於如何分配數據到內部數組不一樣的索引。若是key的哈希函數設計不良,你會獲得一個傾斜的HashMap(和內部數組大小無關)。全部在最長鏈表上的put/get會很是慢,由於會遍歷整個鏈表。最壞的狀況下(全部數據都在同一個索引下), 時間複雜度是O(n).

下面是一個例子,第一個圖片展現了一個傾斜HashMap,第二個圖則是一個平衡的HashMap:

這個傾斜HashMap在索引0上的get/put很是耗時,獲取Entry K會進行6次迭代

在這個平衡HashMap內,獲取Entry K只要進行3次迭代。這兩個HashMap存儲的數據量相同,內部數組大小也同樣。惟一的區別,就是分發數據的key的哈希函數。

下面是一個極端的例子,我建立了一個哈希函數,把兩百萬的數據都放到同一個數組索引下:

public class Test { public static void main(String[] args) { class MyKey { Integer i; public MyKey(Integer i){ this.i =i; } @Override public int hashCode() { return 1; } @Override public boolean equals(Object obj) { … } } Date begin = new Date(); Map <MyKey,String> myMap= new HashMap<>(2_500_000,1); for (int i=0;i<2_000_000;i++){ myMap.put( new MyKey(i), "test "+i); } Date end = new Date(); System.out.println("Duration (ms) "+ (end.getTime()-begin.getTime())); } } 

在個人機器上(core i5-2500k @ 3.6Ghz),這個程序跑了超過45分鐘(java 8u40),45分鐘後我中斷了這個程序。

如今,我運行相同的代碼,只是使用下面的哈希函數:

@Override public int hashCode() { int key = 2097152-1; return key+2097152*i; } 

結果只花了 46秒 !! 這個哈希函數比先前那一個有一個更好的數據分發因此put函數運行快得多。

若是我仍是運行這段代碼,可是換成下面這個更好的哈希函數:

@Override public int hashCode() { return i; } 

如今,程序只須要2秒。

我但願你意識到哈希函數有多麼重要。若是上面的測試在java7上運行,第一個和第二個測試的性能甚至還會更差(java7的複雜度是 O(n),java8是 O(log(n)))

當你使用HashMap時,你須要找到一個哈希函數,能夠 把key分發到儘可能多的索引上,爲了作到這一點,你須要避免哈希碰撞。字符串是不錯的一種key,由於它有 很不錯的哈希函數。整數作key也不錯,由於它的哈希函數就是自己的值。

重設大小的開銷

若是你須要存儲大量數據,你應該在建立HashMap時設置一個接近你預期值的初始化大小。若是你不這麼作,map會用默認的 16數組大小和0.75的 載入因數。 前面11個put會很快可是第12個(16*0.75)會建立一個容量爲32的新數組,第13~23個put也會很快可是第24個會再次建立一個雙倍大小的數組。這個內部重設大小的操做會出如今第48次,96次,192次……。在數據量較小時,這個操做很快,可是當數據量增大時,這個操做會費時數秒到數分鐘不等。經過指定預期初始化大小,你能夠避免這些操做開銷。

可是這也有一個弊端,若是你設置了一個很大的數組大小像 2^28而你只用了2^26,你會浪費掉大量的內存(這個例子裏大約是 2^30 字節)

總結

對於簡單的使用,你不須要知道HashMap是如何工做的,由於你感受不出 O(1)、O(n)、O(log(n))的區別。可是瞭解這種最經常使用的數據結果的底層機制老是有好處的,況且,對於java開發者來講,這是一個很典型的面試問題。在大數據量時,知道它是若是工做的,知道哈希函數的重要性 就變得很是重要了。

但願這篇文章能幫助你加深對HashMap實現細節的瞭解。

相關文章
相關標籤/搜索