
什么是HashMap
它是將一個任意長度的二進制值通過一個映射關(guān)系轉(zhuǎn)換為一個固定長度的二進制值。
1、任意長度的二進制值
2、映射關(guān)系(哈希算法)
3、固定的二進制值(哈希值)
數(shù)組和鏈表組合成的鏈表散列結(jié)構(gòu),通過hash算法,盡量將數(shù)組中的數(shù)據(jù)分布均勻,如果hashcode相同再比較equals方法,如果equals方法返回false,那么就將數(shù)據(jù)以鏈表的形式存儲在數(shù)組的對應(yīng)位置,并將之前在該位置的數(shù)據(jù)往鏈表的后面移動,并記錄一個next屬性,來指示后移的那個數(shù)據(jù)。注意數(shù)組中保存的是entry,其中保存的是鍵值.
hash表
- 特點: 存儲效率高,取數(shù)據(jù)的時間復(fù)雜度是1 O(1)
- hash 通過一個key一個輸入,通過一個hash函數(shù),來找到數(shù)組中與這個key唯一映射的value
- table aaa = 【 】;
- int index = hash(key);
- int value = aaa[index];
- O(n);
hash函數(shù)
key,找下標,有哪些方法可以找打下標
-
1、取模法
- 定義了一個 aaa[]數(shù)組,長度是16.
- int index = key%m
- m的取值規(guī)則是: m要取比數(shù)組長度小的最大質(zhì)數(shù)
- 此時 m = 13
- int 1 = 1%13 ;key = 1, value = 23
- key = 17 ,value = 44
- int index = 17%15;
2、平方取中法
hash表處理沖突
- 1、線性探測法: 探測的步長是1;
- 2、鏈表形式 新的元素會在鏈表的頭部。next 指針指向老元素的頭部。
hashmap的put方法
- HashMap基于hashing原理,我們通過put()和get()方法儲存和獲取對象。當(dāng)我們將鍵值對傳遞給put()方法時,它調(diào)用鍵對象的hashCode()方法來計算hashcode,然后找到bucket位置來儲存值對象。當(dāng)獲取對象時,通過鍵對象的equals()方法找到正確的鍵值對,然后返回值對象。HashMap使用鏈表來解決碰撞問題,當(dāng)發(fā)生碰撞了,對象將會儲存在鏈表的下一個節(jié)點中。 HashMap在每個鏈表節(jié)點中儲存鍵值對對象。




hashmap的get方法

hashmap的擴容機制
- 當(dāng)HashMap中的元素個數(shù)超過數(shù)組大小(數(shù)組總大小length,不是數(shù)組中個數(shù)size)( defaultLoader = 0.75)時,就會進行數(shù)組擴容,defaultLoader的默認值為0.75,這是一個折中的取值。也就是說,默認情況下,數(shù)組大小為16,那么當(dāng)HashMap中元素個數(shù)超過160.75=12(這個值就是代碼中的threshold值,也叫做臨界值)的時候,就把數(shù)組的大小擴展為 2*16=32,即擴大一倍,然后重新計算每個元素在數(shù)組中的位置,而這是一個非常消耗性能的操作,所以如果我們已經(jīng)預(yù)知HashMap中元素的個數(shù),那么預(yù)設(shè)元素的個數(shù)能夠有效的提高HashMap的性能。
以下來自轉(zhuǎn)載:
HashMap的工作原理是近年來常見的Java面試題。幾乎每個Java程序員都知道HashMap,都知道哪里要用HashMap,知道Hashtable和HashMap之間的區(qū)別,那么為何這道面試題如此特殊呢?是因為這道題考察的深度很深。這題經(jīng)常出現(xiàn)在高級或中高級面試中。ConcurrentHashMap和其它同步集合的引入讓這道題變得更加復(fù)雜。讓我們開始探索的旅程吧!
“你用過HashMap嗎?” “什么是HashMap?你為什么用到它?”
幾乎每個人都會回答“是的”,然后回答HashMap的一些特性,譬如HashMap可以接受null鍵值和值,而Hashtable則不能;HashMap是非synchronized;HashMap很快;以及HashMap儲存的是鍵值對等等。這顯示出你已經(jīng)用過HashMap,而且對它相當(dāng)?shù)氖煜?。但是面試官來個急轉(zhuǎn)直下,從此刻開始問出一些刁鉆的問題,關(guān)于HashMap的更多基礎(chǔ)的細節(jié)。面試官可能會問出下面的問題:
“你知道HashMap的工作原理嗎?” “你知道HashMap的get()方法的工作原理嗎?”
你也許會回答“我沒有詳查標準的Java API,你可以看看Java源代碼或者Open JDK。”“我可以用Google找到答案?!?/p>
但一些面試者可能可以給出答案,“HashMap是基于hashing的原理,我們使用put(key, value)存儲對象到HashMap中,使用get(key)從HashMap中獲取對象。當(dāng)我們給put()方法傳遞鍵和值時,我們先對鍵調(diào)用hashCode()方法,返回的hashCode用于找到bucket位置來儲存Entry對象?!边@里關(guān)鍵點在于指出,HashMap是在bucket中儲存鍵對象和值對象,作為Map.Entry。這一點有助于理解獲取對象的邏輯。如果你沒有意識到這一點,或者錯誤的認為僅僅只在bucket中存儲值的話,你將不會回答如何從HashMap中獲取對象的邏輯。這個答案相當(dāng)?shù)恼_,也顯示出面試者確實知道hashing以及HashMap的工作原理。但是這僅僅是故事的開始,當(dāng)面試官加入一些Java程序員每天要碰到的實際場景的時候,錯誤的答案頻現(xiàn)。下個問題可能是關(guān)于HashMap中的碰撞探測(collision detection)以及碰撞的解決方法:
“當(dāng)兩個對象的hashcode相同會發(fā)生什么?”
從這里開始,真正的困惑開始了,一些面試者會回答因為hashcode相同,所以兩個對象是相等的,HashMap將會拋出異常,或者不會存儲它們。然后面試官可能會提醒他們有equals()和hashCode()兩個方法,并告訴他們兩個對象就算hashcode相同,但是它們可能并不相等。一些面試者可能就此放棄,而另外一些還能繼續(xù)挺進,他們回答“因為hashcode相同,所以它們的bucket位置相同,‘碰撞’會發(fā)生。因為HashMap使用鏈表存儲對象,這個Entry(包含有鍵值對的Map.Entry對象)會存儲在鏈表中?!边@個答案非常的合理,雖然有很多種處理碰撞的方法,這種方法是最簡單的,也正是HashMap的處理方法。但故事還沒有完結(jié),面試官會繼續(xù)問:
“如果兩個鍵的hashcode相同,你如何獲取值對象?”
面試者會回答:當(dāng)我們調(diào)用get()方法,HashMap會使用鍵對象的hashcode找到bucket位置,然后獲取值對象。面試官提醒他如果有兩個值對象儲存在同一個bucket,他給出答案:將會遍歷鏈表直到找到值對象。面試官會問因為你并沒有值對象去比較,你是如何確定確定找到值對象的?除非面試者直到HashMap在鏈表中存儲的是鍵值對,否則他們不可能回答出這一題。
其中一些記得這個重要知識點的面試者會說,找到bucket位置之后,會調(diào)用keys.equals()方法去找到鏈表中正確的節(jié)點,最終找到要找的值對象。完美的答案!
許多情況下,面試者會在這個環(huán)節(jié)中出錯,因為他們混淆了hashCode()和equals()方法。因為在此之前hashCode()屢屢出現(xiàn),而equals()方法僅僅在獲取值對象的時候才出現(xiàn)。一些優(yōu)秀的開發(fā)者會指出使用不可變的、聲明作final的對象,并且采用合適的equals()和hashCode()方法的話,將會減少碰撞的發(fā)生,提高效率。不可變性使得能夠緩存不同鍵的hashcode,這將提高整個獲取對象的速度,使用String,Interger這樣的wrapper類作為鍵是非常好的選擇。
如果你認為到這里已經(jīng)完結(jié)了,那么聽到下面這個問題的時候,你會大吃一驚。
“如果HashMap的大小超過了負載因子(load factor)定義的容量,怎么辦?”
除非你真正知道HashMap的工作原理,否則你將回答不出這道題。默認的負載因子大小為0.75,也就是說,當(dāng)一個map填滿了75%的bucket時候,和其它集合類(如ArrayList等)一樣,將會創(chuàng)建原來HashMap大小的兩倍的bucket數(shù)組,來重新調(diào)整map的大小,并將原來的對象放入新的bucket數(shù)組中。這個過程叫作rehashing,因為它調(diào)用hash方法找到新的bucket位置。
如果你能夠回答這道問題,下面的問題來了:
“你了解重新調(diào)整HashMap大小存在什么問題嗎?”
你可能回答不上來,這時面試官會提醒你當(dāng)多線程的情況下,可能產(chǎn)生條件競爭(race condition)。
當(dāng)重新調(diào)整HashMap大小的時候,確實存在條件競爭,因為如果兩個線程都發(fā)現(xiàn)HashMap需要重新調(diào)整大小了,它們會同時試著調(diào)整大小。在調(diào)整大小的過程中,存儲在鏈表中的元素的次序會反過來,因為移動到新的bucket位置的時候,HashMap并不會將元素放在鏈表的尾部,而是放在頭部,這是為了避免尾部遍歷(tail traversing)。如果條件競爭發(fā)生了,那么就死循環(huán)了。這個時候,你可以質(zhì)問面試官,為什么這么奇怪,要在多線程的環(huán)境下使用HashMap呢?:)
熱心的讀者貢獻了更多的關(guān)于HashMap的問題:
-
為什么String, Interger這樣的wrapper類適合作為鍵?
String, Interger這樣的wrapper類作為HashMap的鍵是再適合不過了,而且String最為常用。因為String是不可變的,也是final的,而且已經(jīng)重寫了equals()和hashCode()方法了。其他的wrapper類也有這個特點。不可變性是必要的,因為為了要計算hashCode(),就要防止鍵值改變,如果鍵值在放入時和獲取時返回不同的hashcode的話,那么就不能從HashMap中找到你想要的對象。不可變性還有其他的優(yōu)點如線程安全。如果你可以僅僅通過將某個field聲明成final就能保證hashCode是不變的,那么請這么做吧。因為獲取對象的時候要用到equals()和hashCode()方法,那么鍵對象正確的重寫這兩個方法是非常重要的。如果兩個不相等的對象返回不同的hashcode的話,那么碰撞的幾率就會小些,這樣就能提高HashMap的性能。
-
我們可以使用自定義的對象作為鍵嗎?
這是前一個問題的延伸。當(dāng)然你可能使用任何對象作為鍵,只要它遵守了equals()和hashCode()方法的定義規(guī)則,并且當(dāng)對象插入到Map中之后將不會再改變了。如果這個自定義對象時不可變的,那么它已經(jīng)滿足了作為鍵的條件,因為當(dāng)它創(chuàng)建之后就已經(jīng)不能改變了。
-
我們可以使用CocurrentHashMap來代替Hashtable嗎?
這是另外一個很熱門的面試題,因為ConcurrentHashMap越來越多人用了。我們知道Hashtable是synchronized的,但是ConcurrentHashMap同步性能更好,因為它僅僅根據(jù)同步級別對map的一部分進行上鎖。ConcurrentHashMap當(dāng)然可以代替HashTable,但是HashTable提供更強的線程安全性??纯?a target="_blank" rel="nofollow">這篇博客查看Hashtable和ConcurrentHashMap的區(qū)別。
我個人很喜歡這個問題,因為這個問題的深度和廣度,也不直接的涉及到不同的概念。讓我們再來看看這些問題設(shè)計哪些知識點:
- hashing的概念
- HashMap中解決碰撞的方法
- equals()和hashCode()的應(yīng)用,以及它們在HashMap中的重要性
- 不可變對象的好處
- HashMap多線程的條件競爭
- 重新調(diào)整HashMap的大小
HashMap的工作原理
HashMap基于hashing原理,我們通過put()和get()方法儲存和獲取對象。當(dāng)我們將鍵值對傳遞給put()方法時,它調(diào)用鍵對象的hashCode()方法來計算hashcode,讓后找到bucket位置來儲存值對象。當(dāng)獲取對象時,通過鍵對象的equals()方法找到正確的鍵值對,然后返回值對象。HashMap使用鏈表來解決碰撞問題,當(dāng)發(fā)生碰撞了,對象將會儲存在鏈表的下一個節(jié)點中。 HashMap在每個鏈表節(jié)點中儲存鍵值對對象。
當(dāng)兩個不同的鍵對象的hashcode相同時會發(fā)生什么? 它們會儲存在同一個bucket位置的鏈表中。鍵對象的equals()方法用來找到鍵值對。
所有代碼如下:interface DNMap
public interface DNMap <K,V> {
public V put(K k ,V v);
public V get(K k);
public int size();
public interface Entry<K ,V>{
public K getKey();
public V getValue();
}
}
/**
* Created by Kenvin on 2017/11/8.
*/
public class DNHashMap <K,V> implements DNMap<K,V>{
private static int defaultLength = 16;
private static double defaultLoader = 0.75;//警示標識
private Entry<K,V>[] table = null;
private int size = 0;
public DNHashMap(int length ,double loader) {
defaultLength = length;
defaultLoader = loader;
}
public DNHashMap(){
this(defaultLength,defaultLoader);
table = new Entry[defaultLength];
}
private int getIndex(K k){
int m = defaultLength;
int index = k.hashCode() %(m-1);
return index >= 0 ? index : -index;
}
@Override
public V put(K k, V v) {
//判斷size是否達到擴容的標準
if (size>=defaultLength*defaultLoader){
//當(dāng)哈希表的容量超過默認容量時,必須調(diào)整table的大小。
// 當(dāng)容量已經(jīng)達到最大可能值時,那么該方法就將容量調(diào)整到Integer.MAX_VALUE返回,
// 這時,需要創(chuàng)建一張新表,將原表的映射到新表中。
upDateSize();
}
int index = getIndex(k);
Entry<K,V> entry = table[index] ;
if (entry == null){
table[index] = newEntry(k,v,null);
size++;
}else {
//如何index 位置元素不為空,說明有數(shù)據(jù),指針指向老數(shù)據(jù)
table[index] = newEntry(k,v,entry);
}
return table[index].getValue();
}
private void upDateSize(){
Entry<K,V>[] newTable = new Entry[2*defaultLength];//擴容之后表的數(shù)據(jù)或者鏈表的數(shù)據(jù)需要重新放置
againHash(newTable);
}
private void againHash(Entry<K,V>[] newTable){
List<Entry<K,V>> list = new ArrayList<>();
//如何拿之前的老數(shù)據(jù)
for (int i = 0; i <table.length ; i++) {
if (table[i] == null){
continue;
}
findEntryByNext(table[i],list);
}
//不為空可能是一個鏈表。此時用一個集合來處理
if (list.size() > 0 ){
//進行新數(shù)組的再散列
size = 0;
defaultLength= 2*defaultLength;
table = newTable;
for (Entry<K,V> entry:list
) {
if (entry.next !=null){
entry.next =null;
}
put(entry.getKey(),entry.getValue());
}
}
}
// 遞歸操作
private void findEntryByNext(Entry<K,V> entry,List<Entry<K,V>> list){
if (entry!= null && entry.next!=null){
list.add(entry);
findEntryByNext(entry.next,list);
}else {
list.add(entry);
}
}
private Entry<K,V> newEntry(K k,V v,Entry<K,V> next){
return new Entry<>(k,v,next);
}
@Override
public V get(K k) {
int index = getIndex(k);
if (table[index] == null){
return null;
}
return findValueByEqualKey(k,table[index]);
}
public V findValueByEqualKey(K k,Entry<K,V> entry){
if (k == entry.getKey() || k.equals(entry.getKey())){
return entry.getValue();
}else {
if (entry.next != null){
return findValueByEqualKey(k,entry.next);
}
}
return entry.getValue();
}
@Override
public int size() {
return size;
}
class Entry<K,V> implements DNMap.Entry<K,V> {
K k;
V v;
Entry<K,V> next ;
public Entry(K k, V v, Entry<K, V> next) {
this.k = k;
this.v = v;
this.next = next;
}
@Override
public K getKey() {
return k;
}
@Override
public V getValue() {
return v;
}
}
}