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