Java数据结构和算法:HashMap的实现原理
1. HashMap概述
HashMap是基于哈希表的Map接口的非同步實現。此實現提供所有可選的映射操作,并允許使用null值和null鍵。此類不保證映射的順序,特別是它不保證該順序恒久不變。
2. HashMap的數據結構
在java編程語言中,最基本的結構就是兩種,一個是數組,另外一個是模擬指針(引用),所有的數據結構都可以用這兩個基本結構來構造的,HashMap也不例外。HashMap實際上是一個“鏈表散列”的數據結構,即數組和鏈表的結合體。
從上圖中可以看出,HashMap底層就是一個數組結構,數組中的每一項又是一個鏈表。當新建一個HashMap的時候,就會初始化一個數組
3. ArrayMap對比HashMap
在Java里面用Collection里面的HashMap作為容器我們使用的頻率很高,而ArrayMap是Android api提供的一種用來提升特定場和內存使用率的特殊數據結構。今天我就寫一篇博客記錄一下
4. HashMap
Java庫里的HashMap其實是一個連續的鏈表數組,通過讓key計算hash值后插入對應的index里。當hash值發生碰撞時,可以采用線性探測,二次hash,或者后面直接變成鏈表的結構來避免碰撞。因為hash的值不是連續的,所以hashmap實際需要占用的大小會比它實際能裝的item的容量要大。我們可以看一下HashMap的源碼:
public HashMap(int initialCapacity, float loadFactor) { // 初始容量不能為負數 if (initialCapacity < 0) throw new IllegalArgumentException( "Illegal initial capacity: " + initialCapacity); // 如果初始容量大于最大容量,讓出示容量 if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; // 負載因子必須大于 0 的數值 if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException( loadFactor); //....// 設置容量極限等于容量 * 負載因子 threshold = (int)(capacity * loadFactor); // 初始化 HashMap用于存儲的數組 table = new Entry[capacity]; // ① init(); }你會發現它又一個變量叫loadfactor,還有threshold。threshold就是臨界值的意思,代表當前HashMap的儲存機構能容納的最大容量,它等于loadfactor * 容量。當HashMap記錄存入的item size大于threshold后,HashMap就會進行擴容(resize)。當我們第一次新建一個HashMap對象的時候,默認的容量是16,若你只打算在HashMap里放入3個元素那將浪費至少13個空間。
6. ArrayMap
ArrayMap是怎么實現節省內存的呢?先放數據結構圖:
他用兩個數組來模擬Map,第一個數組存放存放item的hash值,第二數組是把key,value連續的存放在數組里,通過先算hash在第一個數組里找到它的hash index,根據這個index在去第二個數組里找到這個key-value。
在這里,在第一個數組里查找hash index的方法當然是用二分查找啦(binary search)。
這個數據結構的設計就做到了,有多個item我就分配多少內存,做到了memory的節約。并且因為數據結構是通過數組組織的,所以遍歷的時候可以用index直接遍歷也是很方便的有沒有!但是缺點也很明顯,查找達不到HashMap O(1)的查找時間。
當要存儲的對象較少的時候(1000以下的時候)可以考慮用ArrayMap來減少內存的占用。
7. hashmap和hashtable的區別
http://www.233.com/ncre2/JAVA/jichu/20100717/084230917.html
繼承和實現區別
Hashtable是基于陳舊的Dictionary類,完成了Map接口;HashMap是Java 1.2引進的Map接口的一個實現(HashMap繼承于AbstractMap,AbstractMap完成了Map接口)。
線程安全不同
HashTable的方法是同步的,HashMap是未同步,所以在多線程場合要手動同步HashMap。
對null的處理不同
HashTable不允許null值(key和value都不可以),HashMap允許null值(key和value都可以)。即 HashTable不允許null值其實在編譯期不會有任何的不一樣,會照樣執行,只是在運行期的時候Hashtable中設置的話回出現空指針異常。 HashMap允許null值是指可以有一個或多個鍵所對應的值為null。當get()方法返回null值時,即可以表示 HashMap中沒有該鍵,也可以表示該鍵所對應的值為null。因此,在HashMap中不能由get()方法來判斷HashMap中是否存在某個鍵,而應該用containsKey()方法來判斷。
方法不同
HashTable有一個contains(Object value),功能和containsValue(Object value)功能一樣。
5、HashTable使用Enumeration,HashMap使用Iterator。
6、HashTable中hash數組默認大小是11,增加的方式是 old*2+1。HashMap中hash數組的默認大小是16,而且一定是2的指數。
7、哈希值的使用不同,HashTable直接使用對象的hashCode,代碼是這樣的:
int hash = key.hashCode();int index = (hash & 0x7FFFFFFF) % tab.length;而HashMap重新計算hash值,而且用與代替求模:
int hash = hash(k);int i = indexFor(hash, table.length);static int hash(Object x) {int h = x.hashCode();h += ~(h << 9);h ^= (h >>> 14);h += (h << 4);h ^= (h >>> 10);return h;}static int indexFor(int h, int length) {return h & (length-1);}Hashtable的實現原理
Hashtable類似HashMap,使用hash表來存儲鍵值對。hash表定義:根據設定的hash函數和處理沖突的方式(開放定址、公共溢出區、鏈地址、重哈?!?#xff09;將一組關鍵字映射到一個有限的連續的地址集上(即bucket數組或桶數組),并以關鍵字在地址集中的“像”作為記錄在表中的存儲位置,這種表稱為hash表。
hash沖突發生時,通過“鏈表法”或叫”拉鏈法”來處理沖突,即通過一個鏈表存儲鍵值對(Map.Entry)。每個Entry對象都有next指針用于指向下一個具有相同hashcode值的Entry。
HashMap的實現原理
public class HashMap<K, V> extends AbstractMap<K, V> implements Cloneable, Serializable {private static final int MINIMUM_CAPACITY = 4;//最小容量private static final int MAXIMUM_CAPACITY = 1 << 30;//最大容量static final float DEFAULT_LOAD_FACTOR = .75F;//裝載因子transient int size;private static final Entry[] EMPTY_TABLE = new HashMapEntry[MINIMUM_CAPACITY >>> 1];transient HashMapEntry<K, V>[] table;static class HashMapEntry<K, V> implements Entry<K, V> {final K key;V value;final int hash;HashMapEntry<K, V> next;}} class HashMapEntry{K key;V value;int hash;HashMapEntry<K, V> next; }二次哈希
@Override public V put(K key, V value) {...int hash = Collections.secondaryHash(key);HashMapEntry<K, V>[] tab = table;int index = hash & (tab.length - 1);... }//Collections.secondaryHashpublic static int secondaryHash(Object key) {return secondaryHash(key.hashCode()); }private static int secondaryHash(int h) {// Spread bits to regularize both segment and index locations,// using variant of single-word Wang/Jenkins hash.h += (h << 15) ^ 0xffffcd7d;h ^= (h >>> 10);h += (h << 3);h ^= (h >>> 6);h += (h << 2) + (h << 14);return h ^ (h >>> 16);}table 是一個大小為 2 n 的一維數組,其中存放的是一個個的 HashMapEntry,而 HashMapEntry 是包含了 hash、key 與 value 值及一個指向 HashMapEntry 的 next 指針
key.hashCode()
哈希碼就是將對象的信息經過一些轉變形成一個獨一無二的int值,這個值存儲在一個array中
int hash = secondaryHash(key.hashCode()) 二次 hash,減少碰撞
求出key的hash值,根據hash值得出在table中的索引,而后遍歷對應的單鏈表,碰撞
int index = hash & (tab.length - 1)根據哈希值計算出對應的key在哈希數組中的索引,若在存放的過程中,index 值相同,則會鏈接當前 entry 的 next 指針上。
如果兩個鍵的hashcode相同,你如何獲取值對象?
當我們調用get()方法,HashMap會使用鍵對象的hashcode找到bucket位置,找到bucket位置之后,會調用keys.equals()方法去找到鏈表中正確的節點。
負載因子(百分比):HashMap的大小 = 初始容量*負載因子,擴容集合,負載因子和初始容量會影響HashMap的性能,初始容量默認是16,負載因子默認是0.75
先通過key.hashCode()計算出key的哈希值,如果哈希值相等,則通過equals()方法比較內容是否相同
JDK8:位桶+鏈表/紅黑樹
concurrentHashMap:線程安全,分段鎖
Collections.synchronizedMap()
底層數據結構使用的是哈希表(哈希數組),數組的每個元素都是一個單鏈表的頭節點,鏈表是用來解決沖突的,如果不同的key映射到了數組的同一位置處,就將其放入單鏈表中
LinkedHashMap:雙向鏈表,LruCache底層使用
LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder)
參數1:初始容量,參數2:負載因子,參數3:是否開啟按訪問順序排序
LinkedHashMap
雙向循環鏈表,LinkedHashMap可以用來實現LRU算法,accessOrder為true,表示按訪問順序排序
當accessOrder為true時,才會開啟按訪問順序排序的模式,才能用來實現LRU算法。我們可以看到,無論是put方法還是get方法,都會導致目標Entry成為最近訪問的Entry,因此便把該Entry加入到了雙向鏈表的末尾(get方法通過調用recordAccess方法來實現,put方法在覆蓋已有key的情況下,也是通過調用recordAccess方法來實現,在插入新的Entry時,則是通過createEntry中的addBefore方法來實現),這樣便把最近使用了的Entry放入到了雙向鏈表的后面,多次操作后,雙向鏈表前面的Entry便是最近沒有使用的,這樣當節點個數滿的時候,刪除的最前面的Entry(head后面的那個Entry)便是最近最少使用的Entry。
HashSet的實現原理
HashSet是通過HashMap實現的,只是使用了HashMap的鍵,沒有使用HashMap的值
hashCode(),哈希值,HashSet的元素會根據哈希值存儲,哈希值一樣的元素會存儲在同一個區域,也叫桶原理(bucket),這也查找起來效率會高很多
但是在元素被添加進HashSet集合后,修改元素中參與計算哈希值的屬性,再調用remove()方法時不起作用,會導致內存泄露
HashMap與HashTable的主要區別
- HashTable線程更加安全,代價就是因為它粗暴的添加了同步鎖,所以會有性能損失。其實有更好的concurrentHashMap可以替代HashTable
- HashTable:hash值對length取模,HashMap中則通過h&(length-1)的方法來代替取模
- Hashtable不允許key或者value使用null值,而HashMap可以。
- Hashtable擴容時,將容量變為原來的2倍加1,而HashMap擴容時,將容量變為原來的2倍。
- Hashtable計算hash值,直接用key的hashCode(),而HashMap重新計算了key的hash值,Hashtable在求hash值對應的位置索引時,用取模運算,而HashMap在求位置索引時,則用與運算,且這里一般先用hash&0x7FFFFFFF后,再對length取模,&0x7FFFFFFF的目的是為了將負的hash值轉化為正值,因為hash值有可能為負數,而&0x7FFFFFFF后,只有符號外改變,而后面的位都不變。
ArrayList與HashSet的區別
Android 5.0之后對HashMap的修改
原文鏈接:http://blog.csdn.net/l2show/article/details/46970507
之前發現在Android 5.0的機子上放在HashMap里面的數據取出后跟Android 5.0之下的機子不一樣,導致項目里面一個接口出了問題(接口做了緩存,request參數順序變化的話就會導致一些數據拿不到),然后去查看了一下Android 5.0和Android 4.4 關于HashMap的源碼,使用meld查看差異能夠看到果然google對HashMap的實現做了修改.
下圖左邊為Android 5.0的源碼,右邊為Android 4.4的源碼
從源碼中可以看到,Android 5.0 在計算key的HashCode使用的是下面的算法.
private static int secondaryHash(int h) { // Spread bits to regularize both segment and index locations, // using variant of single-word Wang/Jenkins hash. h += (h << 15) ^ 0xffffcd7d; h ^= (h >>> 10); h += (h << 3); h ^= (h >>> 6); h += (h << 2) + (h << 14); return h ^ (h >>> 16); }而Android 4.4中計算Key的HashCode的算法明顯跟Android 5.0中不同,所以這也導致了在get之后,在兩個系統上同樣的數據不同的順序。如果對存儲的數據有順序需求的話改為使用紅黑樹構建的TreeMap就OK了.
static int secondaryHash(Object key) { int hash = key.hashCode(); hash ^= (hash >>> 20) ^ (hash >>> 12); hash ^= (hash >>> 7) ^ (hash >>> 4); return hash; }總結
以上是生活随笔為你收集整理的Java数据结构和算法:HashMap的实现原理的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 国内一线互联网公司内部面试题库
- 下一篇: Java数据结构和算法:HashMap,