今天咱們來談談 Java 中的 hashCode()
方法——經過源碼的角度。衆所周知,Java 是一門面向對象的編程語言,全部的類都會默認繼承自 Object 類,而 Object 的中文意思就是「對象」。java
Object 類中就包含了 hashCode()
方法:git
@HotSpotIntrinsicCandidate
public native int hashCode();
複製代碼
意味着全部的類都會有一個 hashCode()
方法,該方法會返回一個 int 類型的值。因爲 hashCode()
方法是一個本地方法(native
關鍵字修飾的方法,用 C/C++
語言實現,由 Java 調用),意味着 Object 類中並無給出具體的實現。github
具體的實現能夠參考 jdk/src/hotspot/share/runtime/synchronizer.cpp
(源碼能夠到 GitHub 上 OpenJDK 的倉庫中下載)。get_next_hash()
方法會根據 hashCode 的取值來決定採用哪種哈希值的生成策略。面試
而且 hashCode()
方法被 @HotSpotIntrinsicCandidate
註解修飾,說明它在 HotSpot 虛擬機中有一套高效的實現,基於 CPU 指令。算法
我在 掘金 共輸出了 100 多篇 Java 方面的文章,總字數超過 30 萬字, 內容風趣幽默、通俗易懂,收穫了不少初學者的承認和支持,內容包括 Java 語法、Java 集合框架、Java 併發編程、Java 虛擬機等核心內容。 爲了幫助更多的 Java 初學者,我「一怒之下」就把這些文章從新整理並開源到了 GitHub,起名《教妹學 Java》,聽起來是否是就頗有趣?編程
GitHub 開源地址(歡迎 star):github.com/itwanger/jm…安全
你們有沒有想過這樣一個問題:爲何 Object 類須要一個 hashCode()
方法呢?markdown
在 Java 中,hashCode()
方法的主要做用就是爲了配合哈希表使用的。數據結構
哈希表(Hash Table),也叫散列表,是一種能夠經過關鍵碼值(key-value)直接訪問的數據結構,它最大的特色就是能夠快速實現查找、插入和刪除。其中用到的算法叫作哈希,就是把任意長度的輸入,變換成固定長度的輸出,該輸出就是哈希值。像 MD五、SHA1 都用的是哈希算法。多線程
像 Java 中的 HashSet、Hashtable(注意是小寫的 t)、HashMap 都是基於哈希表的具體實現。其中的 HashMap 就是最典型的表明,不只面試官常常問,工做中的使用頻率也很是的高。
你們想一下,若是沒有哈希表,但又須要這樣一個數據結構,它裏面存放的數據是不容許重複的,該怎麼辦呢?
要不使用 equals()
方法進行逐個比較?這種方案固然是可行的。但若是數據量特別特別大,採用 equals()
方法進行逐個對比的效率確定很低很低,最好的解決方案就是哈希表。
拿 HashMap 來講吧。當咱們要在它裏面添加對象時,先調用這個對象的 hashCode()
方法,獲得對應的哈希值,而後將哈希值和對象一塊兒放到 HashMap 中。當咱們要再添加一個新的對象時:
equals()
方法進行對象之間的比較,若是相等,不存了;就這麼一套下來,調用 equals()
方法的頻率就大大下降了。也就是說,只要哈希算法足夠的高效,把發生哈希衝突的頻率降到最低,哈希表的效率就特別的高。
來看一下 HashMap 的哈希算法:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
複製代碼
先調用對象的 hashCode()
方法,而後對該值進行右移運算,而後再進行異或運算。
一般來講,String 會用來做爲 HashMap 的鍵進行哈希運算,所以咱們再來看一下 String 的 hashCode()
方法:
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
hash = h = isLatin1() ? StringLatin1.hashCode(value)
: StringUTF16.hashCode(value);
}
return h;
}
public static int hashCode(byte[] value) {
int h = 0;
int length = value.length >> 1;
for (int i = 0; i < length; i++) {
h = 31 * h + getChar(value, i);
}
return h;
}
複製代碼
可想而知,通過這麼一系列複雜的運算,再加上 JDK 做者這種大師級別的設計,哈希衝突的機率我相信已經降到了最低。
固然了,從理論上來講,對於兩個不一樣對象,它們經過 hashCode()
方法計算後的值可能相同。所以,不能使用 hashCode()
方法來判斷兩個對象是否相等,必須得經過 equals()
方法。
也就是說:
equals()
方法獲得的結果爲 true,調用 hashCode()
方法獲得的結果一定相等;hashCode()
方法獲得的結果不相等,調用 equals()
方法獲得的結果一定爲 false;反之:
equals()
方法獲得的結果爲 false,調用 hashCode()
方法獲得的結果不必定不相等;hashCode()
方法獲得的結果相等,調用 equals()
方法獲得的結果不必定爲 true;來看下面這段代碼。
public class Test {
public static void main(String[] args) {
Student s1 = new Student(18, "張三");
Map<Student, Integer> scores = new HashMap<>();
scores.put(s1, 98);
System.out.println(scores.get(new Student(18, "張三")));
}
}
class Student {
private int age;
private String name;
public Student(int age, String name) {
this.age = age;
this.name = name;
}
@Override
public boolean equals(Object o) {
Student student = (Student) o;
return age == student.age &&
Objects.equals(name, student.name);
}
}
複製代碼
咱們重寫了 Student 類的 equals()
方法,若是兩個學生的年紀和姓名相同,咱們就認爲是同一個學生,雖然很離譜,但咱們就是這麼草率。
在 main()
方法中,18 歲的張三考試得了 98 分,很不錯的成績,咱們把張三和成績放到了 HashMap 中,而後準備輸出張三的成績:
null
複製代碼
很不巧,結果爲 null,而不是預期當中的 98。這是爲何呢?
緣由就在於重寫 equals()
方法的時候沒有重寫 hashCode()
方法。默認狀況下,hashCode()
方法是一個本地方法,會返回對象的存儲地址,顯然 put()
中的 s1 和 get()
中的 new Student(18, "張三")
是兩個對象,它們的存儲地址確定是不一樣的。
HashMap 的 get()
方法會調用 hash(key.hashCode())
計算對象的哈希值,雖然兩個不一樣的 hashCode()
結果通過 hash()
方法計算後有可能獲得相同的結果,但這種機率微乎其微,因此就致使 scores.get(new Student(18, "張三"))
沒法獲得預期的值 18。
怎麼解決這個問題呢?很簡單,重寫 hashCode()
方法。
@Override
public int hashCode() {
return Objects.hash(age, name);
}
複製代碼
Objects 類的 hash()
方法能夠針對不一樣數量的參數生成新的 hashCode()
值。
public static int hashCode(Object a[]) {
if (a == null)
return 0;
int result = 1;
for (Object element : a)
result = 31 * result + (element == null ? 0 : element.hashCode());
return result;
}
複製代碼
代碼彷佛很簡單,概括出的數學公式以下所示(n 爲字符串長度)。
注意:31 是個奇質數,不大不小,通常質數都很是適合哈希計算,偶數至關於移位運算,容易溢出,形成數據信息丟失。
這就意味着年紀和姓名相同的狀況下,會獲得相同的哈希值。scores.get(new Student(18, "張三"))
就會返回 98 的預期值了。
《Java 編程思想》這本聖經中有一段話,對 hashCode()
方法進行了一段描述。
設計
hashCode()
時最重要的因素就是:不管什麼時候,對同一個對象調用hashCode()
都應該生成一樣的值。若是在將一個對象用put()
方法添加進 HashMap 時產生一個hashCode()
值,而用get()
方法取出時卻產生了另一個hashCode()
值,那麼就沒法從新取得該對象了。因此,若是你的hashCode()
方法依賴於對象中易變的數據,用戶就要小心了,由於此數據發生變化時,hashCode()
就會生成一個不一樣的哈希值,至關於產生了一個不一樣的鍵。
也就是說,若是在重寫 hashCode()
和 equals()
方法時,對象中某個字段容易發生改變,那麼最好捨棄這些字段,以避免產生不可預期的結果。
好。有了上面這些內容做爲基礎後,咱們回頭再來看看本地方法 hashCode()
的 C++ 源碼。
static inline intptr_t get_next_hash(Thread* current, oop obj) {
intptr_t value = 0;
if (hashCode == 0) {
// This form uses global Park-Miller RNG.
// On MP system we'll have lots of RW access to a global, so the
// mechanism induces lots of coherency traffic.
value = os::random();
} else if (hashCode == 1) {
// This variation has the property of being stable (idempotent)
// between STW operations. This can be useful in some of the 1-0
// synchronization schemes.
intptr_t addr_bits = cast_from_oop<intptr_t>(obj) >> 3;
value = addr_bits ^ (addr_bits >> 5) ^ GVars.stw_random;
} else if (hashCode == 2) {
value = 1; // for sensitivity testing
} else if (hashCode == 3) {
value = ++GVars.hc_sequence;
} else if (hashCode == 4) {
value = cast_from_oop<intptr_t>(obj);
} else {
// Marsaglia's xor-shift scheme with thread-specific state
// This is probably the best overall implementation -- we'll
// likely make this the default in future releases.
unsigned t = current->_hashStateX;
t ^= (t << 11);
current->_hashStateX = current->_hashStateY;
current->_hashStateY = current->_hashStateZ;
current->_hashStateZ = current->_hashStateW;
unsigned v = current->_hashStateW;
v = (v ^ (v >> 19)) ^ (t ^ (t >> 8));
current->_hashStateW = v;
value = v;
}
value &= markWord::hash_mask;
if (value == 0) value = 0xBAD;
assert(value != markWord::no_hash, "invariant");
return value;
}
複製代碼
若是沒有 C++ 基礎的話,不用細緻去看每一行代碼,咱們只經過表面去了解一下 get_next_hash()
這個方法就行。其中的 hashCode
變量是 JVM 啓動時的一個全局參數,能夠經過它來切換哈希值的生成策略。
hashCode==0
,調用操做系統 OS 的 random()
方法返回隨機數。hashCode == 1
,在 STW(stop-the-world)操做中,這種策略一般用於同步方案中。利用對象地址進行計算,使用不常常更新的隨機數(GVars.stw_random
)參與其中。hashCode == 2
,使用返回 1,用於某些狀況下的測試。hashCode == 3
,從 0 開始計算哈希值,不是線程安全的,多個線程可能會獲得相同的哈希值。hashCode == 4
,與建立對象的內存位置有關,原樣輸出。hashCode == 5
,默認值,支持多線程,使用了 Marsaglia 的 xor-shift 算法產生僞隨機數。所謂的 xor-shift 算法,簡單來講,看起來就是一個移位寄存器,每次移入的位由寄存器中若干位取異或生成。所謂的僞隨機數,不是徹底隨機的,可是真隨機生成比較困難,因此只要能經過必定的隨機數統計檢測,就能夠看成真隨機數來使用。至於更深層次的挖掘,涉及到數學知識和物理知識,就不展開了。畢竟菜是原罪。
二哥在 CSDN 上寫了不少 Java 方面的系列文章,有 Java 核心語法、Java 集合框架、Java IO、Java 併發編程、Java 虛擬機等,也算是體系完整了。
爲了能幫助到更多的 Java 初學者,二哥把本身連載的《教妹學Java》開源到了 GitHub,儘管只整理了 50 篇,發現字數已經來到了 10 萬+,內容更是沒得說,通俗易懂、風趣幽默、圖文並茂。
GitHub 開源地址(歡迎 star):github.com/itwanger/jm…
若是有幫助的話,還請給二哥點個贊,這將是我繼續分享下去的最強動力!