(轉)java集合類深入分析之HashSet, HashMap篇
http://shmilyaw-hotmail-com.iteye.com/blog/1700600簡介
Map和Set是比較常用的兩種數據結構。我們在平常的編程中經常會用到他們。只是他們的內部實現機制到底是怎麼樣的呢?瞭解他們的具體實現對於我們如何有效的去使用他們也是很有幫助的。在這一篇文章裏,已經對HashMap, HashSet的實現做了一個詳細的討論。這裏主要是針對Map, Set這兩種類型的數據結構規約和典型的HashMap,HashSet實現做一個討論。
Map
Map是一種典型的名值對類型,它提供一種Key-Value對應保存的數據結構。我們通過Key值來訪問對應的Value。和Java集合類裏頭其他的類不太一樣,這個接口並沒有繼承Collection這接口。而其他的類或者接口不管是List, Set, Stack等都繼承了Collection。從這一點來說,它有點像一個異類。
從前面的這部分討論,我們可以簡單的歸類一下Map接口裏面定義的常用操作。最常見的兩種操作方法是get, put方法。get方法用於根據Key來取得所需要的Value值,而put方法用於根據特定的Key來放置對應的Value。除了這兩個方法以外還有判斷Key,Value是否存在的containsKey, containsValue方法。
Map類型的數據結構有一個比較好的地方就是在存取元素的時候都能夠有比較高的效率。 因爲每次存取元素的時候都是通過計算Key的hash值再通過一定的映射規則來實現,在理想的情況下可以達到一個常量值。
下面這部分是Map裏面主要方法的列表:
方法名 | 方法詳細定義 | 說明 |
containsKey | boolean containsKey(Object key); | 判斷名是否存在 |
containsValue | boolean containsValue(Object value); | 判斷值是否存在 |
get | V get(Object key); | 讀取元素 |
put | V put(K key, V value); | 設置元素 |
keySet | Set<K> keySet(); | 所有key值合集 |
values | Collection<V> values(); | 所有value的集合 |
entrySet | Set<Map.Entry<K, V>> entrySet(); | 鍵值對集合 |
掌握了以上這些主要的方法介紹,對於其他部分也就很好理解。
HashMap
我們從書本上看到的hash表根據不同的需要可以有不同的實現方式,比如有的直接用線性表,有的用鏈表數組。在hash值的映射規則上也各不相同。在jdk的實現裏,HashMap是採用鏈表數組形式的結構:
有了這部分的闡述,我們後面來理解它具體實現步驟就容易了很多。
內部結構
我們根據這種鏈表數組的類型,可以推斷它內部肯定是有一個鏈表的結構。在HashMap內部,有一個transient Entry[] table;這樣的結構數組,它保存所有Entry的一個列表。而Entry的定義是一個典型的鏈表結構,不過由於既要有Key也要有Value,所以包含了Key, Value兩個值。他們的定義如下:
- static class Entry<K,V> implements Map.Entry<K,V> {
- final K key;
- V value;
- Entry<K,V> next;
- final int hash;
- /**
- * Creates new entry.
- */
- Entry(int h, K k, V v, Entry<K,V> n) {
- value = v;
- next = n;
- key = k;
- hash = h;
- }
- //...
- }
這裏省略了其他部分,主要把他們這個鏈表結構部分突出來。這部分就相當於鏈表裏一個個的Node節點。ok,這樣我們至少已經清楚了它裏面是怎麼組成的了。
數組增長調整
現在再來看一個地方,我們實際中設計HashMap的時候,這裏面數組的長度該多少合適呢?是否需要進行動態調整呢?如果是固定死的話,如果我們需要放置的元素少了,豈不是浪費空間?如果我們要放的元素太多了,這樣也會導致更大程度的hash碰撞,會帶來性能方面的損失。在HashMap裏面保存元素的table是可以動態增長的,它有一個默認的長度16,
- static final int DEFAULT_INITIAL_CAPACITY = 16;
- static final int MAXIMUM_CAPACITY = 1 << 30;
在HashMap的構造函數中,可以指定初始數組的長度。通過這個初始長度值,構造一個長度爲2的若干次方的數組:
- // Find a power of 2 >= initialCapacity
- int capacity = 1;
- while (capacity < initialCapacity)
- capacity <<= 1;
在我們需要調整數組長度的時候,它的過程和前面討論過的List, Queue有些類似,但是又有不同的地方。相同的地方在於,它每次也是將原來的數組長度翻倍,同時將元素拷貝過去。但是由於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);
- }
- /**
- * Transfers all entries from current table to newTable.
- */
- void transfer(Entry[] newTable) {
- Entry[] src = table;
- int newCapacity = newTable.length;
- for (int j = 0; j < src.length; j++) { //遍歷原來的數組table
- Entry<K,V> e = src[j];
- if (e != null) {
- src[j] = null;
- do { //對該鏈表元素裏面所有鏈接的<key, value>對做重新的映射
- Entry<K,V> next = e.next;
- int i = indexFor(e.hash, newCapacity);
- e.next = newTable[i];
- newTable[i] = e;
- e = next;
- } while (e != null);
- }
- }
- }
前面這部分的代碼看起來比較長,實際上就是將舊的數組的元素挪到新的數組中來。因爲新數組的長度不一樣了,再映射的時候要對鏈表裏面所有的元素根據新的長度進行重新映射來對應到不同的位置。
那麼,我們可以看出來,元素存放的位置是和數組長度相關的。而這其中具體映射的過程和怎麼放置元素的呢?我們在這裏就可以找到一個入口點了。就是indexFor方法。
詳細映射過程
我們要把一個<K, V>Entry放到table中間的某個位置,首先是通過計算key的hashCode值,我們都知道。在java裏每個對象都有一個hashCode的方法,返回它對應的hash值。HashMap這邊通過這個hash值再進行一次hash()方法的計算,得到一個int的結果。再通過indexFor將它映射到數組的某個索引。
- static int indexFor(int h, int length) {
- return h & (length-1);
- }
- static int hash(int h) {
- // This function ensures that hashCodes that differ only by
- // constant multiples at each bit position have a bounded
- // number of collisions (approximately 8 at default load factor).
- h ^= (h >>> 20) ^ (h >>> 12);
- return h ^ (h >>> 7) ^ (h >>> 4);
- }
hash方法就是對傳進來的key的hashCode()值再進行一次運算。indexFor方法則是具體映射的方法。因爲最後得到的這個值將走爲存儲Entry的索引。這裏採用h & (length - 1)的手法比較有意思。因爲我們定義的數組長度爲2的若干次方,這意味着如果我們取長度減一的值時,它的二進制數字是最高位以下的所有位爲1.經過與運算之後它的結果肯定在0~2**x之間。就算前面hash方法計算出來的結果比數組長度大也沒關係,因爲這麼一與運算,前面長出來的部分都變成0了。它這一步運算的效果相當於h % length;
有了這部分對數組長度調整和映射關係的理解,我們再來看具體的get, put方法就很容易了。
get
get方法的定義如下:
- 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)];
- // table[indexFor(hash, table.length)] 就是將indexFor運算得到的值直接映射到數組的索引
- e != null;
- e = e.next) {
- Object k;
- if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
- //找到hash值相同的情況下可能出現hash碰撞,所以需要調用equals方法來比較是否相等
- return e.value;
- }
- return null;
- }
它這裏就是一個映射,查找的過程。找到映射的點之後再和鏈表裏的元素逐個比較,保證找到目標值。因爲是hash表,會存在多個值映射到同一個index裏面,所以這裏還要和鏈表裏的元素做對比。
put
put元素就是一個放置元素的過程,首先也是找到對應的索引,然後再把元素放到鏈表裏面去。如果鏈表裏有和元素相同的,則更新對應的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);
- 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;
- }
- }
- //在前面的循環裏面沒有找到,則新建一個Entry對象,加入到鏈表頭。
- modCount++;
- addEntry(hash, key, value, i);
- return null;
- }
addEntry方法會判斷表長度,如果達到一定的閥值則調整數組的長度,將其翻倍:
- void addEntry(int hash, K key, V value, int bucketIndex) {
- Entry<K,V> e = table[bucketIndex];
- table[bucketIndex] = new Entry<>(hash, key, value, e);
- if (size++ >= threshold)
- resize(2 * table.length);
- }
Set
Set接口裏面主要定義了常用的集合操作方法,包括添加元素,判斷元素是否在裏面和對元素過濾。常用的幾個方法如下:
方法名 | 方法詳細定義 | 說明 |
contains | boolean contains(Object o); | 判斷元素是否存在 |
add | boolean add(E e); | 添加元素 |
remove | boolean remove(Object o); | 刪除元素 |
retainAll | boolean retainAll(Collection<?> c); | 過濾元素 |
我們知道,集合裏面要求保存的元素是不能重複的,所以它裏面所有的元素都是唯一的。它的定義就有點不太一樣。
HashSet
HashSet是基於HashMap實現的,在它內部有如下的定義:
- private transient HashMap<E,Object> map;
- // Dummy value to associate with an Object in the backing Map
- private static final Object PRESENT = new Object();
在它裏面放置的元素都應到map裏面的key部分,而在map中與key對應的value用一個Object()對象保存。因爲內部是大量借用HashMap的實現,它本身不過是調用HashMap的一個代理,這些基本方法的實現就顯得很簡單:
- public boolean add(E e) {
- return map.put(e, PRESENT)==null;
- }
- public boolean remove(Object o) {
- return map.remove(o)==PRESENT;
- }
- public boolean contains(Object o) {
- return map.containsKey(o);
- }
總結
在前面的參考資料裏已經對HashMap做了一個很深入透徹的解析。這裏在前人的基礎上加入一點自己個人的理解體會。希望對以後使用類似的結構有一個更好的利用,也能夠充分利用裏面的設計思想。