數組:採用一定的存儲單元存儲一羣數據,對於指定下標查找,時間複雜度爲O(1),但對定值查找,則需要遍歷整個數據,一個一個的比較,時間複雜度是O(n),如果說是有序數組,可以採用二分查找法,增大查找效率,不過對於新增刪除等涉及到數組元素的移動,時間複雜度都是O(n)
鏈表:對於鏈表的新增,刪除,只需要將對應的節點引用上去就ok,時間複雜度是O(1),而查找則需要遍歷鏈表,時間複雜度爲O(N)
二叉樹:對相對平衡的有序二叉樹,查找,刪除,插入等操作,複雜度爲0(logn)
哈希表:哈希表中進行添加,刪除,查找等操作,性能十分之高,不考慮哈希衝突的情況下,僅需一次定位即可完成,時間複雜度爲O(1)
哈希衝突:哈希表的主幹就是數組,插入一個新值的時候,會通過某個算法求出這個值的具體位置,而這個算法的優劣就直接決定哈希表的整個效率,而所謂的哈希衝突就是因爲算出來的位置可能是相等的,所以衝突,由此可見這個hash算法多麼的重要。哈希算法的設計要儘量的保證散列分佈均勻,雨漏均沾。
HashMap1.7實現原理
主幹是一個Entry的數組 ,Entry是HashMap中的靜態內部類
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
int hash;
/**
* Creates new entry.
*/
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
}
簡單來說,HashMap由數組+鏈表組成的,數組是HashMap的主體,鏈表則是主要爲了解決哈希衝突而存在的,如果定位到的數組位置不含鏈表(當前entry的next指向null),那麼對於查找,添加等操作很快,僅需一次尋址即可;如果定位到的數組包含鏈表,對於添加操作,其時間複雜度依然爲O(1),因爲最新的Entry會插入鏈表頭部,急需要簡單改變引用鏈即可,而對於查找操作來講,此時就需要遍歷鏈表,然後通過key對象的equals方法逐一比對查找。所以,性能考慮,HashMap中的鏈表出現越少,性能纔會越好。
基本屬性說明:
//默認初始化化容量,即16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;//最大容量,即2的30次方
static final int MAXIMUM_CAPACITY = 1 << 30;//默認裝載因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;//HashMap內部的存儲結構是一個數組,此處數組爲空,即沒有初始化之前的狀態
static final Entry<?,?>[] EMPTY_TABLE = {};//空的存儲實體
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;//實際存儲的key-value鍵值對的個數
transient int size;//閾值,當table == {}時,該值爲初始容量(初始容量默認爲16);當table被填充了,也就是爲table分配內存空間後,threshold一般爲 capacity*loadFactory。HashMap在進行擴容時需要參考threshold
int threshold;//負載因子,代表了table的填充度有多少,默認是0.75
final float loadFactor;//用於快速失敗,由於HashMap非線程安全,在對HashMap進行迭代時,如果期間其他線程的參與導致HashMap的結構發生變化了(比如put,remove等操作),需要拋出異常ConcurrentModificationException
transient int modCount;//默認的threshold值
static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;
構造函數
public HashMap(int initialCapacity, float loadFactor) { if (initialCapacity < 0) //判斷參數是否正確 throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); this.loadFactor = loadFactor; threshold = initialCapacity; init(); //在HashMap中沒有實現,子類LinkedHashMap實現了 }
從上面這段代碼我們可以看出,我們在構造HashMap的時候,並沒有馬上爲數組table分配內存空間(有一個入參爲指定Map的構造器例外),事實上是在執行第一次put操作的時候才真正構建table數組。
put操作流程:
public V put(K key, V value) { if (table == EMPTY_TABLE) { //table爲空,第一次put,創建數組 inflateTable(threshold); } if (key == null) return putForNullKey(value); //如果key爲null,存儲位置爲table[0]或table[0]的衝突鏈上 int hash = hash(key); //所謂的hash算法,儘可能保證均勻分佈 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; //如果value值相等,覆蓋 e.recordAccess(this); return oldValue; } } modCount++; addEntry(hash, key, value, i); //value不相等,新添加一個Entry return null; }void addEntry(int hash, K key, V value, int bucketIndex) { if ((size >= threshold) && (null != table[bucketIndex])) { //判斷是否需要擴容 resize(2 * table.length); hash = (null != key) ? hash(key) : 0; bucketIndex = indexFor(hash, table.length); } createEntry(hash, key, value, bucketIndex); }
void createEntry(int hash, K key, V value, int bucketIndex) { Entry<K,V> e = table[bucketIndex]; table[bucketIndex] = new Entry<>(hash, key, value, e); //JDK1.7版本put一個數據,是從頭部插入 size++; }
擴容操作:
//按新的容量擴容Hash表 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, initHashSeedAsNeeded(newCapacity));//將老的表中的數據拷貝到新的結構中 table = newTable;//修改HashMap的底層數組 threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);//修改閥值 }transfer(newTable, initHashSeedAsNeeded(newCapacity))這個方法將老數組中的數據逐個鏈表地遍歷,就不展開了,重新計算後放入新的擴容後的數組中,我們的數組索引位置的計算是通過 對key值的hashcode進行hash擾亂運算後,再通過和 length-1進行位運算得到最終數組索引位置。
get操作,其實就是遍歷數組+鏈表,這邊就不詳述啦
重寫equals方法需同時重寫hashCode方法
如果我們已經對HashMap的原理有了一定了解,這個結果就不難理解了。儘管我們在進行get和put操作的時候,使用的key從邏輯上講是等值的(通過equals比較是相等的),但由於沒有重寫hashCode方法,所以put操作時,key(hashcode1)-->hash-->indexFor-->最終索引位置 ,而通過key取出value的時候 key(hashcode2)-->hash-->indexFor-->最終索引位置,由於hashcode1不等於hashcode2,導致沒有定位到一個數組位置而返回邏輯上錯誤的值null(也有可能碰巧定位到一個數組位置,但是也會判斷其entry的hash值是否相等,上面get方法中有提到。)
所以,在重寫equals的方法的時候,必須注意重寫hashCode方法,同時還要保證通過equals判斷相等的兩個對象,調用hashCode方法要返回同樣的整數值。而如果equals判斷不相等的兩個對象,其hashCode可以相同(只不過會發生哈希衝突,應儘量避免)