先上hashCode和equals源碼:
/** JNI,調用底層其它語言實現 */
public native int hashCode();
/** 默認同==,直接比較對象 */
public boolean equals(Object obj) {
return (this == obj);
}
equals方法:String類中重寫了equals方法,比較的是字符串值,看一下源碼實現:
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String) anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
// 逐個判斷字符是否相等
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
重寫equals要滿足幾個條件:
Object 類的 equals 方法實現對象上差別可能性最大的相等關係;即,對於任何非空引用值 x 和 y,當且僅當 x 和 y 引用同一個對象時,此方法才返回 true(x == y 具有值 true)。 當此方法被重寫時,通常有必要重寫 hashCode 方法,以維護 hashCode 方法的常規協定,該協定聲明相等對象必須具有相等的哈希碼。
下面來說說hashCode方法,這個方法我們平時通常是用不到的,它是爲哈希家族的集合類框架(HashMap、HashSet、HashTable)提供服務,hashCode 的常規協定是:
HashMap的類結構如下:
java.util
類 HashMap < K,V>
java.lang.Object
繼承者 java.util.AbstractMap< K,V>
繼承者 java.util.HashMap< K,V>
所有已實現的接口:
Serializable,Cloneable,Map< K,V>
直接已知子類:
LinkedHashMap,PrinterStateReasons
HashMap中我們最常用的就是put(K, V)和get(K)。我們都知道,HashMap的K值是唯一的,那如何保證唯一性呢?我們首先想到的是用equals比較,沒錯,這樣可以實現,但隨着內部元素的增多,put和get的效率將越來越低,這裏的時間複雜度是O(n),假如有1000個元素,put時需要比較1000次。實際上,HashMap很少會用到equals方法,因爲其內通過一個哈希表管理所有元素,哈希是通過hash單詞音譯過來的,也可以稱爲散列表,哈希算法可以快速的存取元素,當我們調用put存值時,HashMap首先會調用K的hashCode方法,獲取哈希碼,通過哈希碼快速找到某個存放位置,這個位置可以被稱之爲bucketIndex,通過上面所述hashCode的協定可以知道,如果hashCode不同,equals一定爲false,如果hashCode相同,equals不一定爲true。所以理論上,hashCode可能存在衝突的情況,有個專業名詞叫碰撞,當碰撞發生時,計算出的bucketIndex也是相同的,這時會取到bucketIndex位置已存儲的元素,最終通過equals來比較,equals方法就是哈希碼碰撞時纔會執行的方法,所以前面說HashMap很少會用到equals。HashMap通過hashCode和equals最終判斷出K是否已存在,如果已存在,則使用新V值替換舊V值,並返回舊V值,如果不存在 ,則存放新的鍵值對< K, V>到bucketIndex位置。
現在我們知道,執行put方法後,最終HashMap的存儲結構會有這三種情況,情形3是最少發生的,哈希碼發生碰撞屬於小概率事件。到目前爲止,我們瞭解了兩件事:
HashMap中put方法源碼:
public V put(K key, V value) {
// 處理key爲null,HashMap允許key和value爲null
if (key == null)
return putForNullKey(value);
// 得到key的哈希碼
int hash = hash(key);
// 通過哈希碼計算出bucketIndex
int i = indexFor(hash, table.length);
// 取出bucketIndex位置上的元素,並循環單鏈表,判斷key是否已存在
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;
}
}
// key不存在時,加入新元素
modCount++;
addEntry(hash, key, value, i);
return null;
}
Java Collections Framework中實際操作的都是數組或者鏈表,而我們通常不需要顯示的維護集合的大小,而是集合類框架中內部維護,方便的同時,也帶來了性能的問題。
HashMap有兩個參數影響其性能:初始容量和加載因子。默認初始容量是16,加載因子是0.75。容量是哈希表中桶(Entry數組)的數量,初始容量只是哈希表在創建時的容量。加載因子是哈希表在其容量自動增加之前可以達到多滿的一種尺度。當哈希表中的條目數超出了加載因子與當前容量的乘積時,通過調用 rehash 方法將容量翻倍。
HashMap中定義的成員變量如下:
/**
* The default initial capacity - MUST be a power of two.
*/
static final int DEFAULT_INITIAL_CAPACITY = 16;// 默認初始容量爲16,必須爲2的冪
/**
* The maximum capacity, used if a higher value is implicitly specified
* by either of the constructors with arguments.
* MUST be a power of two <= 1<<30.
*/
static final int MAXIMUM_CAPACITY = 1 << 30;// 最大容量爲2的30次方
/**
* The load factor used when none specified in constructor.
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;// 默認加載因子0.75
/**
* The table, resized as necessary. Length MUST Always be a power of two.
*/
transient Entry<K,V>[] table;// Entry數組,哈希表,長度必須爲2的冪
/**
* The number of key-value mappings contained in this map.
*/
transient int size;// 已存元素的個數
/**
* The next size value at which to resize (capacity * load factor).
* @serial
*/
int threshold;// 下次擴容的臨界值,size>=threshold就會擴容
/**
* The load factor for the hash table.
*
* @serial
*/
final float loadFactor;// 加載因子
我們看字段名稱大概就能知道其含義,看Doc描述就能知道其詳細要求,這也是我們日常編碼中特別需要注意的地方,不要寫讓別人看不懂的代碼,除非你寫的代碼是一次性的。需要注意的是,HashMap中的容量MUST be a power of two,翻譯過來就是必須爲2的冪,這裏的原因稍後再說。再來看一下HashMap初始化,HashMap一共重載了4個構造方法,分別爲:
構造方法摘要
HashMap()
構造一個具有默認初始容量 (16) 和默認加載因子 (0.75) 的空 HashMap。
HashMap(int initialCapacity)
構造一個帶指定初始容量和默認加載因子 (0.75) 的空 HashMap。
HashMap(int initialCapacity, float loadFactor)
構造一個帶指定初始容量和加載因子的空 HashMap。
HashMap(Map<? extendsK,? extendsV> m)
構造一個映射關係與指定 Map 相同的 HashMap。
看一下第三個構造方法源碼,其它構造方法最終調用的都是它。
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);
// Find a power of 2 >= initialCapacity
// 這裏需要注意一下
int capacity = 1;
while (capacity < initialCapacity)
capacity <<= 1;
// 設置加載因子
this.loadFactor = loadFactor;
// 設置下次擴容臨界值
threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
// 初始化哈希表
table = new Entry[capacity];
useAltHashing = sun.misc.VM.isBooted() &&
(capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
init();
}
我們在日常做底層開發時,必須要嚴格控制入參,可以參考一下Java源碼及各種開源項目源碼,如果參數不合法,適時的拋出一些運行時異常,最後到應用層捕獲。看第14-16行代碼,這裏做了一個移位運算,保證了初始容量一定爲2的冪,假如你傳的是5,那麼最終的初始容量爲8。源碼中的位運算隨處可見啊=。=!
到現在爲止,我們有一個很強烈的問題,爲什麼HashMap容量一定要爲2的冪呢?HashMap中的數據結構是數組+單鏈表的組合,我們希望的是元素存放的更均勻,最理想的效果是,Entry數組中每個位置都只有一個元素,這樣,查詢的時候效率最高,不需要遍歷單鏈表,也不需要通過equals去比較K,而且空間利用率最大。那如何計算纔會分佈最均勻呢?我們首先想到的就是%運算,哈希值%容量=bucketIndex,SUN的大師們是否也是如此做的呢?我們閱讀一下這段源碼:
/**
* Returns index for hash code h.
*/
static int indexFor(int h, int length) {
return h & (length-1);
}
又是位運算,高帥富啊!這裏h是通過K的hashCode最終計算出來的哈希值,並不是hashCode本身,而是在hashCode之上又經過一層運算的hash值,length是目前容量。這塊的處理很有玄機,與容量一定爲2的冪環環相扣,當容量一定是2^n時,h & (length - 1) == h % length,它倆是等價不等效的,位運算效率非常高,實際開發中,很多的數值運算以及邏輯判斷都可以轉換成位運算,但是位運算通常是難以理解的,因爲其本身就是給電腦運算的,運算的是二進制,而不是給人類運算的,人類運算的是十進制,這也是位運算在普遍的開發者中間不太流行的原因(門檻太高)。這個等式實際上可以推理出來,2^n轉換成二進制就是1+n個0,減1之後就是0+n個1,如16 -> 10000,15 -> 01111,那根據&位運算的規則,都爲1(真)時,才爲1,那0≤運算後的結果≤15,假設h <= 15,那麼運算後的結果就是h本身,h >15,運算後的結果就是最後三位二進制做&運算後的值,最終,就是%運算後的餘數,我想,這就是容量必須爲2的冪的原因。HashTable中的實現對容量的大小沒有規定,最終的bucketIndex是通過取餘來運算的。
通常,默認加載因子 (.75) 在時間和空間成本上尋求一種折衷。加載因子過高雖然減少了空間開銷,但同時也增加了查詢成本(在大多數 HashMap 類的操作中,包括 get 和 put 操作,都反映了這一點,可以想想爲什麼)。在設置初始容量時應該考慮到映射中所需的條目數及其加載因子,以便最大限度地降低 rehash 操作次數。如果初始容量大於最大條目數除以加載因子(實際上就是最大條目數小於初始容量*加載因子),則不會發生 rehash 操作。
如果很多映射關係要存儲在 HashMap 實例中,則相對於按需執行自動的 rehash 操作以增大表的容量來說,使用足夠大的初始容量創建它將使得映射關係能更有效地存儲。 當HashMap存放的元素越來越多,到達臨界值(閥值)threshold時,就要對Entry數組擴容,這是Java集合類框架最大的魅力,HashMap在擴容時,新數組的容量將是原來的2倍,由於容量發生變化,原有的每個元素需要重新計算bucketIndex,再存放到新數組中去,也就是所謂的rehash。HashMap默認初始容量16,加載因子0.75,也就是說最多能放16*0.75=12個元素,當put第13個時,HashMap將發生rehash,rehash的一系列處理比較影響性能,所以當我們需要向HashMap存放較多元素時,最好指定合適的初始容量和加載因子,否則HashMap默認只能存12個元素,將會發生多次rehash操作。
HashMap所有集合類視圖所返回迭代器都是快速失敗的(fail-fast),在迭代器創建之後,如果從結構上對映射進行修改,除非通過迭代器自身的 remove 或 add 方法,其他任何時間任何方式的修改,迭代器都將拋出 ConcurrentModificationException。因此,面對併發的修改,迭代器很快就會完全失敗。注意,迭代器的快速失敗行爲不能得到保證,一般來說,存在不同步的併發修改時,不可能作出任何堅決的保證。快速失敗迭代器盡最大努力拋出 ConcurrentModificationException。
HashMap是線程不安全的實現,而HashTable是線程安全的實現,所謂線程不安全,就是在多線程情況下直接使用HashMap會出現一些莫名其妙不可預知的問題,多線程和單線程的區別:單線程只有一條執行路徑,而多線程是併發執行(非並行),會有多條執行路徑。如果HashMap是隻讀的(加載一次,以後只有讀取,不會發生結構上的修改),那使用沒有問題。那如果HashMap是可寫的(會發生結構上的修改),則會引發諸多問題,如上面的fail-fast,也可以看下這裏,這裏就不去研究了。
那在多線程下使用HashMap我們需要怎麼做,幾種方案: