java資料結構-HashMap
阿新 • • 發佈:2018-12-25
一直以來似乎都有一個錯覺,認為map跟其他的集合類一樣繼承自Collection,其實不然,Map和Collection在結構層次上是沒有任何關係的,通過檢視原始碼可以發現map所有操作都是基於key-value對,而不是單獨的元素。
下面以HashMap為例子,深入對Map的實現機制進行了解,在這個過程中,請開啟jdk原始碼。
Hash演算法
HashMap使用Hash演算法,所以在解剖HashMap之間,需要先簡單的瞭解Hash演算法,Hash演算法一般也成為雜湊演算法,通過雜湊演算法將任意的值轉化成固定的長度輸出,該輸出就是雜湊值,這是一種壓縮對映,也就是,雜湊值的空間遠遠小於輸入的值空間。
簡單的說,hash演算法的意義在於提供了一種快速存取資料的方法,它用一種演算法建立鍵值與真實值之間的對應關係,(每一個真實值只能有一個鍵值,但是一個鍵值可以對應多個真實值),這樣可以快速在陣列等裡面存取資料。
下面我們建立一個HashMap,然後往裡面放入12對key-value,這個HashMap的預設陣列長度為16,我們的key分別存放在該陣列的格子中,每個格子下面存放的元素又是以連結串列的方式存放元素。
publicstaticvoid main(String[] args) {
Map map =new HashMap();
map.put("What", "chenyz");
map.put("You", "chenyz");
map.put("Don't", "chenyz");
map.put("Know" , "chenyz");
map.put("About", "chenyz");
map.put("Geo", "chenyz");
map.put("APIs", "chenyz");
map.put("Can't", "chenyz");
map.put("Hurt", "chenyz");
map.put("you", "chenyz");
map.put("google", "chenyz");
map.put("map", "chenyz");
map.put( "hello", "chenyz");
}
當我們新新增一個元素時,首先我們通過Hash演算法計算出這個元素的Hash值的hashcode,通過這個hashcode的值,我們就可以計算出這個新元素應該存放在這個hash表的哪個格子裡面,如果這個格子中已經存在元素,那麼就把新的元素加入到已經存在格子元素的連結串列中。
執行上面的程式,我們對HashMap原始碼進行一點修改,打印出每個key物件的hash值
What-->hash值:8
You-->hash值:3
Don't-->hash值:7
Know-->hash值:13
About-->hash值:11
Geo-->hash值:12
APIs-->hash值:1
Can't-->hash值:7
Hurt-->hash值:1
you-->hash值:10
google-->hash值:3
map-->hash值:8
hello-->hash值:0
計算出來的Hash值分別代表該key應該存放在Hash表中對應數字的格子中,如果該格子已經有元素存在,那麼該key就以連結串列的方式依次放入格子中
從上表可以看出,Hash表是線性表和連結串列的綜合所得,根據資料結構的定義,可以得出粗劣的結論,Hash演算法的存取速度要比陣列差一些,但是比起單純的連結串列,在查詢和存取方面卻要好多。
如果要查詢一個元素時,同樣的方式,通過Hash函式計算出這個元素的Hash值hashcode,然後通過這個hashcode值,直接找到跟這個hash值相對應的線性格子,進如該格子後,對這個格子存放的連結串列元素逐個進行比較,直到找到對應的hash值。
在簡單瞭解完Hash演算法後,我們開啟HashMap原始碼
初始化HashMap
下面我們看看Map map = new HashMap();這段程式碼究竟做了什麼,發生了什麼資料結構的變化。
HashMap中幾個重要的屬性
transient Entry[] table;
用來儲存key-value的物件Entry陣列,也就是Hash表
transient int size;
返回HashMap的鍵值對個數
final float loadFactor;
負載因子,用來決定Entry陣列是否擴容的因子,HashMap預設是0.75f
int threshold;
重構因子,(capacity * load factor)負載因子與Entry[]陣列容積的乘值
publicclass HashMap<K,V>extends AbstractMap<K,V>implements Map<K,V>, Cloneable, Serializable
{
int threshold;
finalfloat loadFactor;
transient Entry[] table;
staticfinalfloat DEFAULT_LOAD_FACTOR =0.75f;
staticfinalint DEFAULT_INITIAL_CAPACITY =16;
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity <0)
thrownew IllegalArgumentException("Illegal initial capacity: "+
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <=0|| Float.isNaN(loadFactor))
thrownew IllegalArgumentException("Illegal load factor: "+
loadFactor);
// Find a power of 2 >= initialCapacityint capacity =1;
while (capacity < initialCapacity)
capacity <<=1;
this.loadFactor = loadFactor;
threshold = (int)(capacity * loadFactor);
table =new Entry[capacity];
init();
} 以public HashMap(int initialCapacity, float loadFactor)建構函式為例,另外兩個建構函式實際上也是以同種方式來構建HashMap.
首先是要確定hashMap的初始化的長度,這裡使用的策略是迴圈查出一個大於initialCapacity的2的次方的數,例如initialCapacity的值是10,那麼大於10的數是2的4次方,也就是16,capacity的值被賦予了16,那麼實際上table陣列的長度是16,之所以採用這樣的策略來構建Hash表的長度,是因為2的次方運算對於計算機來說是有相當的效率。
loadFactor,被稱為負載因子,HashMap的預設負載因子是0.75f
threshold,接下來是重構因子,由負載因子和容量的乘機組成,它表示當HashMap元素被存放了多少個之後,需要對HashMap進行重構。
通過這一系列的計算和定義後,初始化Entry[] table;
put(key,value)
接下來看一對key-value是如何被存放到HashMap中:put(key,value)
public V put(K key, V value) {
if (key ==null)
return putForNullKey(value);
int hash = hash(key.hashCode());
int i = indexFor(hash, table.length);
System.out.println(key+"-->hash值:"+i);//這就是剛才程式打印出來的key對應hash值for (Entry<K,V> e = table[i]; e !=null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);
returnnull;
}
staticint hash(int h) {
h ^= (h >>>20) ^ (h >>>12);
return h ^ (h >>>7) ^ (h >>>4);
}
staticint indexFor(int h, int length) {
return h & (length-1);
}
這裡是整個hash的關鍵,請開啟原始碼檢視一步一步檢視。
hash(key.hashCode()) 計算出key的hash碼 //對於hash()的演算法,這裡有一篇分析很透徹的文章<HashMap hash方法分析>
indexFor(hash, table.length) 通過一個與演算法計算出來,該key應在存放在Hash表的哪個格子中。
for (Entry<K,V> e = table[i]; e != null; e = e.next) 然後再遍歷table[i]格中的連結串列,判斷是否已經存在一樣的key,如果存在一樣的key值,那麼就用新的value覆蓋舊的value,並把舊的value值返回。
addEntry(hash, key, value, i) 如果經過遍歷連結串列沒有發現同樣的key,那麼進行addEntry函式的操作,增加當前key到hash表中的第i個格子中的連結串列中
void addEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] =new Entry<K,V>(hash, key, value, e);
if (size++>= threshold)
resize(2* table.length);
}
Entry<K,V> e = table[bucketIndex]; 建立一個Entry物件來存放鍵值(ps:Entry物件是一個連結串列物件)
table[bucketIndex] = new Entry<K,V>(hash, key, value, e); 將Entry物件新增到連結串列中
if (size++ >= threshold) resize(2 * table.length); 最後將size進行自增,判斷size值是否大於重構因子,如果大於那麼就是用resize進行擴容重構。
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
Entry[] newTable =new Entry[newCapacity];
transfer(newTable);
table = newTable;
threshold = (int)(newCapacity * loadFactor);
}
這裡為什麼是否需要擴容重構,其實是涉及到負載因子的效能問題
loadFactor負載因子
上面說過loadFactor是一個hashMap的決定性屬性,HashSet和HashMap的預設負載因子都是0.75,它表示,如果雜湊表的容量超過3/4時,將自動成倍的增加雜湊表的容量,這個值是權衡了時間和空間的成本,如果負載因子較高,雖然會減少對記憶體空間的需求,但也會增加查詢資料的時間開銷,無論是put()和get()都涉及到對資料進行查詢的動作,所以負載因子是不適宜設定過高
get(key)
接下來看看get(key)做了什麼
public V get(Object key) {
if (key ==null)
return getForNullKey();
int hash = hash(key.hashCode());
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e !=null;
e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
return e.value;
}
returnnull;
}
這些動作似乎是跟put(key,value)相識,通過hash演算法獲取key的hash碼,再通過indexFor定位出該key存在於table的哪一個下表,獲取該下標然後對下標中的連結串列進行遍歷比對,如果有符合就直接返回該key的value值。
keySet()
這裡還涉及另一個問題,上面說了HashMap是跟set沒有任何親屬關係,但map也一樣實現了keySet介面,下面譜析一下keySet在hashMap中是如何實現的,這裡給出部分程式碼,請結合原始碼檢視
public K next() {
return nextEntry().getKey();
}
final Entry<K,V> nextEntry() {
if (modCount != expectedModCount)
thrownew ConcurrentModificationException();
Entry<K,V> e = next;
if (e ==null)
thrownew NoSuchElementException();
if ((next = e.next) ==null) {
Entry[] t = table;
while (index < t.length && (next = t[index++]) ==null)
;
}
current = e;
return e;
}
程式碼很簡單,就是對每個格子裡面的連結串列進行遍歷,也正是這個原因,當我們依次將key值put進hashMap中,但在使用map.entrySet().iterator()進行遍歷時候卻不是put時候的順序。
擴容
在前面說到put函式的時候,已經提過了擴容的問題
if (size++>= threshold)
resize(2* table.length);
這裡一個是否擴容的判斷,當資料達到了threshold所謂的重構因子,而不是HashMap的最大容量,就進行擴容。
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
Entry[] newTable =new Entry[newCapacity];
transfer(newTable);
table = newTable;
threshold = (int)(newCapacity * loadFactor);
}
void transfer(Entry[] newTable) {
Entry[] src = table;
int newCapacity = newTable.length;
for (int j =0; j < src.length; j++) {
Entry<K,V> e = src[j];
if (e !=null) {
src[j] =null;
do {
Entry<K,V> next = e.next;
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
} while (e !=null);
}
}
}
transfer方法實際上是將所有的元素重新進行一些hash,這是因為容量變化了,每個元素相對應的hash值也會不一樣。
使用HashMap
1.不要再高併發中使用HashMap,HashMap是執行緒不安全,如果被多個執行緒共享之後,將可能發生不可預知的問題。
2.如果資料大小事固定的,最好在初始化的時候就給HashMap一個合理的容量值,如果使用new HashMap()預設建構函式,重構因子的值是16*0.75=12,當HashMap的容量超過了12後,就會進行一系列的擴容運算,重建一個原來成倍的陣列,並且對原來存在的元素進行重新的hash運算,如果你的資料是有成千上萬的,那麼你的成千上萬的資料也要跟這你的擴容不斷的hash,這將產生高額的記憶體和cpu的大量開銷。
當然啦,HashMap的函式還有很多,不過都是基於table的連結串列進行操作,當然也就是hash演算法,Map & hashMap在平時我們的應用非常多,最重要的是我們要對每句程式碼中每塊資料結構變化心中有數。
上面主要是參考了jdk原始碼,資料結構和一些相關資料本著好記性不如爛部落格的精神記錄下來,希望朋友們如果發覺哪裡不對請指出來,虛心請教
下面以HashMap為例子,深入對Map的實現機制進行了解,在這個過程中,請開啟jdk原始碼。
Hash演算法
HashMap使用Hash演算法,所以在解剖HashMap之間,需要先簡單的瞭解Hash演算法,Hash演算法一般也成為雜湊演算法,通過雜湊演算法將任意的值轉化成固定的長度輸出,該輸出就是雜湊值,這是一種壓縮對映,也就是,雜湊值的空間遠遠小於輸入的值空間。
簡單的說,hash演算法的意義在於提供了一種快速存取資料的方法,它用一種演算法建立鍵值與真實值之間的對應關係,(每一個真實值只能有一個鍵值,但是一個鍵值可以對應多個真實值),這樣可以快速在陣列等裡面存取資料。
下面我們建立一個HashMap,然後往裡面放入12對key-value,這個HashMap的預設陣列長度為16,我們的key分別存放在該陣列的格子中,每個格子下面存放的元素又是以連結串列的方式存放元素。
publicstaticvoid main(String[] args) {
Map map =new HashMap();
map.put("What", "chenyz");
map.put("You", "chenyz");
map.put("Don't", "chenyz");
map.put("Know"
map.put("About", "chenyz");
map.put("Geo", "chenyz");
map.put("APIs", "chenyz");
map.put("Can't", "chenyz");
map.put("Hurt", "chenyz");
map.put("you", "chenyz");
map.put("google", "chenyz");
map.put("map", "chenyz");
map.put(
}
當我們新新增一個元素時,首先我們通過Hash演算法計算出這個元素的Hash值的hashcode,通過這個hashcode的值,我們就可以計算出這個新元素應該存放在這個hash表的哪個格子裡面,如果這個格子中已經存在元素,那麼就把新的元素加入到已經存在格子元素的連結串列中。
執行上面的程式,我們對HashMap原始碼進行一點修改,打印出每個key物件的hash值
What-->hash值:8
You-->hash值:3
Don't-->hash值:7
Know-->hash值:13
About-->hash值:11
Geo-->hash值:12
APIs-->hash值:1
Can't-->hash值:7
Hurt-->hash值:1
you-->hash值:10
google-->hash值:3
map-->hash值:8
hello-->hash值:0
計算出來的Hash值分別代表該key應該存放在Hash表中對應數字的格子中,如果該格子已經有元素存在,那麼該key就以連結串列的方式依次放入格子中
從上表可以看出,Hash表是線性表和連結串列的綜合所得,根據資料結構的定義,可以得出粗劣的結論,Hash演算法的存取速度要比陣列差一些,但是比起單純的連結串列,在查詢和存取方面卻要好多。
如果要查詢一個元素時,同樣的方式,通過Hash函式計算出這個元素的Hash值hashcode,然後通過這個hashcode值,直接找到跟這個hash值相對應的線性格子,進如該格子後,對這個格子存放的連結串列元素逐個進行比較,直到找到對應的hash值。
在簡單瞭解完Hash演算法後,我們開啟HashMap原始碼
初始化HashMap
下面我們看看Map map = new HashMap();這段程式碼究竟做了什麼,發生了什麼資料結構的變化。
HashMap中幾個重要的屬性
transient Entry[] table;
用來儲存key-value的物件Entry陣列,也就是Hash表
transient int size;
返回HashMap的鍵值對個數
final float loadFactor;
負載因子,用來決定Entry陣列是否擴容的因子,HashMap預設是0.75f
int threshold;
重構因子,(capacity * load factor)負載因子與Entry[]陣列容積的乘值
publicclass HashMap<K,V>extends AbstractMap<K,V>implements Map<K,V>, Cloneable, Serializable
{
int threshold;
finalfloat loadFactor;
transient Entry[] table;
staticfinalfloat DEFAULT_LOAD_FACTOR =0.75f;
staticfinalint DEFAULT_INITIAL_CAPACITY =16;
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity <0)
thrownew IllegalArgumentException("Illegal initial capacity: "+
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <=0|| Float.isNaN(loadFactor))
thrownew IllegalArgumentException("Illegal load factor: "+
loadFactor);
// Find a power of 2 >= initialCapacityint capacity =1;
while (capacity < initialCapacity)
capacity <<=1;
this.loadFactor = loadFactor;
threshold = (int)(capacity * loadFactor);
table =new Entry[capacity];
init();
} 以public HashMap(int initialCapacity, float loadFactor)建構函式為例,另外兩個建構函式實際上也是以同種方式來構建HashMap.
首先是要確定hashMap的初始化的長度,這裡使用的策略是迴圈查出一個大於initialCapacity的2的次方的數,例如initialCapacity的值是10,那麼大於10的數是2的4次方,也就是16,capacity的值被賦予了16,那麼實際上table陣列的長度是16,之所以採用這樣的策略來構建Hash表的長度,是因為2的次方運算對於計算機來說是有相當的效率。
loadFactor,被稱為負載因子,HashMap的預設負載因子是0.75f
threshold,接下來是重構因子,由負載因子和容量的乘機組成,它表示當HashMap元素被存放了多少個之後,需要對HashMap進行重構。
通過這一系列的計算和定義後,初始化Entry[] table;
put(key,value)
接下來看一對key-value是如何被存放到HashMap中:put(key,value)
public V put(K key, V value) {
if (key ==null)
return putForNullKey(value);
int hash = hash(key.hashCode());
int i = indexFor(hash, table.length);
System.out.println(key+"-->hash值:"+i);//這就是剛才程式打印出來的key對應hash值for (Entry<K,V> e = table[i]; e !=null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);
returnnull;
}
staticint hash(int h) {
h ^= (h >>>20) ^ (h >>>12);
return h ^ (h >>>7) ^ (h >>>4);
}
staticint indexFor(int h, int length) {
return h & (length-1);
}
這裡是整個hash的關鍵,請開啟原始碼檢視一步一步檢視。
hash(key.hashCode()) 計算出key的hash碼 //對於hash()的演算法,這裡有一篇分析很透徹的文章<HashMap hash方法分析>
indexFor(hash, table.length) 通過一個與演算法計算出來,該key應在存放在Hash表的哪個格子中。
for (Entry<K,V> e = table[i]; e != null; e = e.next) 然後再遍歷table[i]格中的連結串列,判斷是否已經存在一樣的key,如果存在一樣的key值,那麼就用新的value覆蓋舊的value,並把舊的value值返回。
addEntry(hash, key, value, i) 如果經過遍歷連結串列沒有發現同樣的key,那麼進行addEntry函式的操作,增加當前key到hash表中的第i個格子中的連結串列中
void addEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] =new Entry<K,V>(hash, key, value, e);
if (size++>= threshold)
resize(2* table.length);
}
Entry<K,V> e = table[bucketIndex]; 建立一個Entry物件來存放鍵值(ps:Entry物件是一個連結串列物件)
table[bucketIndex] = new Entry<K,V>(hash, key, value, e); 將Entry物件新增到連結串列中
if (size++ >= threshold) resize(2 * table.length); 最後將size進行自增,判斷size值是否大於重構因子,如果大於那麼就是用resize進行擴容重構。
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
Entry[] newTable =new Entry[newCapacity];
transfer(newTable);
table = newTable;
threshold = (int)(newCapacity * loadFactor);
}
這裡為什麼是否需要擴容重構,其實是涉及到負載因子的效能問題
loadFactor負載因子
上面說過loadFactor是一個hashMap的決定性屬性,HashSet和HashMap的預設負載因子都是0.75,它表示,如果雜湊表的容量超過3/4時,將自動成倍的增加雜湊表的容量,這個值是權衡了時間和空間的成本,如果負載因子較高,雖然會減少對記憶體空間的需求,但也會增加查詢資料的時間開銷,無論是put()和get()都涉及到對資料進行查詢的動作,所以負載因子是不適宜設定過高
get(key)
接下來看看get(key)做了什麼
public V get(Object key) {
if (key ==null)
return getForNullKey();
int hash = hash(key.hashCode());
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e !=null;
e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
return e.value;
}
returnnull;
}
這些動作似乎是跟put(key,value)相識,通過hash演算法獲取key的hash碼,再通過indexFor定位出該key存在於table的哪一個下表,獲取該下標然後對下標中的連結串列進行遍歷比對,如果有符合就直接返回該key的value值。
keySet()
這裡還涉及另一個問題,上面說了HashMap是跟set沒有任何親屬關係,但map也一樣實現了keySet介面,下面譜析一下keySet在hashMap中是如何實現的,這裡給出部分程式碼,請結合原始碼檢視
public K next() {
return nextEntry().getKey();
}
final Entry<K,V> nextEntry() {
if (modCount != expectedModCount)
thrownew ConcurrentModificationException();
Entry<K,V> e = next;
if (e ==null)
thrownew NoSuchElementException();
if ((next = e.next) ==null) {
Entry[] t = table;
while (index < t.length && (next = t[index++]) ==null)
;
}
current = e;
return e;
}
程式碼很簡單,就是對每個格子裡面的連結串列進行遍歷,也正是這個原因,當我們依次將key值put進hashMap中,但在使用map.entrySet().iterator()進行遍歷時候卻不是put時候的順序。
擴容
在前面說到put函式的時候,已經提過了擴容的問題
if (size++>= threshold)
resize(2* table.length);
這裡一個是否擴容的判斷,當資料達到了threshold所謂的重構因子,而不是HashMap的最大容量,就進行擴容。
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
Entry[] newTable =new Entry[newCapacity];
transfer(newTable);
table = newTable;
threshold = (int)(newCapacity * loadFactor);
}
void transfer(Entry[] newTable) {
Entry[] src = table;
int newCapacity = newTable.length;
for (int j =0; j < src.length; j++) {
Entry<K,V> e = src[j];
if (e !=null) {
src[j] =null;
do {
Entry<K,V> next = e.next;
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
} while (e !=null);
}
}
}
transfer方法實際上是將所有的元素重新進行一些hash,這是因為容量變化了,每個元素相對應的hash值也會不一樣。
使用HashMap
1.不要再高併發中使用HashMap,HashMap是執行緒不安全,如果被多個執行緒共享之後,將可能發生不可預知的問題。
2.如果資料大小事固定的,最好在初始化的時候就給HashMap一個合理的容量值,如果使用new HashMap()預設建構函式,重構因子的值是16*0.75=12,當HashMap的容量超過了12後,就會進行一系列的擴容運算,重建一個原來成倍的陣列,並且對原來存在的元素進行重新的hash運算,如果你的資料是有成千上萬的,那麼你的成千上萬的資料也要跟這你的擴容不斷的hash,這將產生高額的記憶體和cpu的大量開銷。
當然啦,HashMap的函式還有很多,不過都是基於table的連結串列進行操作,當然也就是hash演算法,Map & hashMap在平時我們的應用非常多,最重要的是我們要對每句程式碼中每塊資料結構變化心中有數。
上面主要是參考了jdk原始碼,資料結構和一些相關資料本著好記性不如爛部落格的精神記錄下來,希望朋友們如果發覺哪裡不對請指出來,虛心請教