萬萬沒想到一個 hashcode() 方法,既然會引出一堆的知識盲區,簡直了。
老八股:為什麼重寫Equals方法要重寫HashCode方法。
大聲告訴我為什麼,閉著眼睛先把答案背出來,啥?這都忘了?沒事,也不是啥大事。這兩句話再Object類的 hashcode 中的註釋上就有,但是一般八股文不會告訴你是出自這裡的。凝聚成兩句話就是:
1 如果兩個物件通過equals()方法比較相等,那麼這兩個物件的hashCode一定相同。
2 如果兩個物件hashCode相同,不能證明兩個物件是同一個物件(不一定相等),只能證明兩個物件在雜湊結構中儲存在同一個地址(不同物件hashCode相同的情況稱為hash衝突)。
/**
* Returns a hash code value for the object. This method is
* supported for the benefit of hash tables such as those provided by
* {@link java.util.HashMap}.
* <p>
* The general contract of {@code hashCode} is:
* <ul>
* <li>Whenever it is invoked on the same object more than once during
* an execution of a Java application, the {@code hashCode} method
* must consistently return the same integer, provided no information
* used in {@code equals} comparisons on the object is modified.
* This integer need not remain consistent from one execution of an
* application to another execution of the same application.
* <li>If two objects are equal according to the {@code equals(Object)}
* method, then calling the {@code hashCode} method on each of
* the two objects must produce the same integer result.
* <li>It is <em>not</em> required that if two objects are unequal
* according to the {@link java.lang.Object#equals(java.lang.Object)}
* method, then calling the {@code hashCode} method on each of the
* two objects must produce distinct integer results. However, the
* programmer should be aware that producing distinct integer results
* for unequal objects may improve the performance of hash tables.
* </ul>
* <p>
* As much as is reasonably practical, the hashCode method defined by
* class {@code Object} does return distinct integers for distinct
* objects. (This is typically implemented by converting the internal
* address of the object into an integer, but this implementation
* technique is not required by the
* Java™ programming language.)
*
* @return a hash code value for this object.
* @see java.lang.Object#equals(java.lang.Object)
* @see java.lang.System#identityHashCode
*/
public native int hashCode();
Returns a hash code value for the object. This method is supported for the benefit of hash tables such as those provided by
返回 一個 hash code 值 為 這個 物件。這個 方法 是 支援 的 這個 好處 體現為 hash 表 比如 這些 提供的 {@link java.util.HashMap}.
什麼亂七八糟的,還是來看看比較權威的。
每個覆蓋了equals方法的類中,必須覆蓋hashCode。如果不這麼做,就違背了hashCode的通用約定,也就是上面註釋中所說的。進而導致該類無法結合所以與雜湊的集合一起正常運作,這裡指的是HashMap、HashSet、HashTable、ConcurrentHashMap。
提煉:如果重寫equals不重寫hashCode它與雜湊集合無法正常工作。
總結:恍然大悟,原來是這樣,那我這個物件,不存在這些雜湊集合中不就不用遵守這個規則了嘛。哈哈,大聰明,理論上確實是這樣。但是這篇文章我還想再寫長一些,多佔用一些面試時間,畢竟我還有主動權機會。
有人說,扯淡嘛,我看了Object類中的hashcode()方法,裡面什麼也沒有啊,只有方法,沒看到實現,哪裡來的返回值呢。
public native int hashCode();
首先看到看關鍵字 native,知識盲區來了,很多人不知道 這是啥東西,當然,說起這個,就要從盤古開天闢地開始講了,我們這一節也不著重講這個,篇幅有限,感興趣的去問問度娘吧。
native 用來修飾方法,用 native 宣告的方法表示告知 JVM 呼叫,該方法在外部定義,我們可以用任何語言去實現它。簡單地講,一個native Method就是一個 Java 呼叫非 Java 程式碼的介面。
原來是這樣,要是我寫的程式碼也能這樣就好了,我就寫個方法,用 native 修飾,然後作業系統就給我自動實現。
想的美了,其實操作給我們實現,也是其他程式設計師寫好的程式碼提前編譯好了,換句話來說就是:哪有歲月靜好,只是有猿替你負重前行罷了。
openjdk
├ corba:不流行的多語言、分散式通訊介面
├ hotspot:Java 虛擬機器器
├ jaxp:XML 處理
├ jaxws:一組 XML web services 的 Java API
├ jdk:java 開發工具包
├ langtools:Java 語言工具
└ nashorn:JVM 上的 JavaScript 執行時
hotspot
├─agent Serviceability Agent的使用者端實現
├─make 用來build出HotSpot的各種組態檔
├─src HotSpot VM的原始碼
│ ├─cpu CPU相關程式碼(組合器、模板直譯器、ad檔案、部分runtime函數在這裡實現)
│ ├─os 操作系相關程式碼
│ ├─os_cpu 作業系統+CPU的組合相關的程式碼
│ └─share 平臺無關的共通程式碼
│ ├─tools 工具
│ │ ├─hsdis 反組合外掛
│ │ ├─IdealGraphVisualizer 將server編譯器的中間程式碼視覺化的工具
│ │ ├─launcher 啟動程式「java」
│ │ ├─LogCompilation 將-XX:+LogCompilation輸出的紀錄檔(hotspot.log)整理成更容易閱讀的格式的工具
│ │ └─ProjectCreator 生成Visual Studio的project檔案的工具
│ └─vm HotSpot VM的核心程式碼
│ ├─adlc 平臺描述檔案(上面的cpu或os_cpu裡的*.ad檔案)的編譯器
│ ├─asm 組合器介面
│ ├─c1 client編譯器(又稱「C1」)
│ ├─ci 動態編譯器的公共服務/從動態編譯器到VM的介面
│ ├─classfile 類檔案的處理(包括類載入和系統符號表等)
│ ├─code 動態生成的程式碼的管理
│ ├─compiler 從VM呼叫動態編譯器的介面
│ ├─gc_implementation GC的實現
│ │ ├─concurrentMarkSweep Concurrent Mark Sweep GC的實現
│ │ ├─g1 Garbage-First GC的實現(不使用老的分代式GC框架)
│ │ ├─parallelScavenge ParallelScavenge GC的實現(server VM預設,不使用老的分代式GC框架)
│ │ ├─parNew ParNew GC的實現
│ │ └─shared GC的共通實現
│ ├─gc_interface GC的介面
│ ├─interpreter 直譯器,包括「模板直譯器」(官方版在用)和「C++直譯器」(官方版不在用)
│ ├─libadt 一些抽象資料結構
│ ├─memory 記憶體管理相關(老的分代式GC框架也在這裡)
│ ├─oops HotSpot VM的物件系統的實現
│ ├─opto server編譯器(又稱「C2」或「Opto」)
│ ├─prims HotSpot VM的對外介面,包括部分標準庫的native部分和JVMTI實現
│ ├─runtime 執行時支援庫(包括執行緒管理、編譯器排程、鎖、反射等)
│ ├─services 主要是用來支援JMX之類的管理功能的介面
│ ├─shark 基於LLVM的JIT編譯器(官方版裡沒有使用)
│ └─utilities 一些基本的工具類
└─test 單元測試
static inline intptr_t get_next_hash(Thread * Self, oop obj) {
intptr_t value = 0 ;
if (hashCode == 0) {
// 返回亂數
value = os::random() ;
} else
if (hashCode == 1) {
//用物件的記憶體地址根據某種演演算法進行計算
intptr_t addrBits = cast_from_oop<intptr_t>(obj) >> 3 ;
value = addrBits ^ (addrBits >> 5) ^ GVars.stwRandom ;
} else
if (hashCode == 2) {
// 始終返回1,用於測試
value = 1 ;
} else
if (hashCode == 3) {
//從0開始計算雜湊值
value = ++GVars.hcSequence ;
} else
if (hashCode == 4) {
//輸出物件的記憶體地址
value = cast_from_oop<intptr_t>(obj) ;
} else {
// 預設的hashCode生成演演算法,利用xor-shift演演算法產生偽亂數
unsigned t = Self->_hashStateX ;
t ^= (t << 11) ;
Self->_hashStateX = Self->_hashStateY ;
Self->_hashStateY = Self->_hashStateZ ;
Self->_hashStateZ = Self->_hashStateW ;
unsigned v = Self->_hashStateW ;
v = (v ^ (v >> 19)) ^ (t ^ (t >> 8)) ;
Self->_hashStateW = v ;
value = v ;
}
value &= markOopDesc::hash_mask;
if (value == 0) value = 0xBAD ;
assert (value != markOopDesc::no_hash, "invariant") ;
TEVENT (hashCode: GENERATE) ;
return value;
}
先上一首涼涼。不是看不懂啊,畢竟大學學過C,但是腦瓜子嗡嗡的,肯定不能專研這個了,先跳過,回到正題。
為什麼要重寫hashcode我們大概知道了,那如果我偏不重寫呢,先來個鐵頭娃demo看看效果。
public class Person {
//使用者Id,唯一確定使用者
private String id;
private String name;
public Person(String id, String name) {
this.id = id;
this.name = name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Person)) return false;
Person person = (Person) o;
return Objects.equals(id, person.id) && Objects.equals(name, person.name);
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
@Test
public void test() {
HashMap<Person, Integer> map = new HashMap<>();
//key:Person型別 value:Integer型別
Person personKey = new Person("1","張三");
Person getKey = new Person("1","張三");
map.put(personKey,100);
System.out.println("原物件map.get():"+map.get(personKey));
System.out.println("值相同的物件map.get():"+map.get(getKey));
}
輸出結果:
-------測試前準備-建立物件-------
原物件map.get():100
值相同的物件map.get():null
-------測試結束-------
這裡我們很明顯看到,我們要用之前的物件才能從HashMap中獲取到key。這不是我們想要的效果啊,再也不頭鐵了。
吶吶吶,問題又來了,我不用Map不就行了,可以。當然可以,但是現在的專案裡面,如果說整個專案沒有用到Map的,那這個專案估計也不叫專案了,寫java的人,誰能拒絕來個Map呢。
Person key = new Person("1","張三");
Person sameKey = new Person("1","張三");
Person unSameKey = new Person("2","張三");
其中 key 和 sameKey 的 hashcode()一定要相同,因為我們的業務員就是認為他們應該是相同的。但是同時 unSame 和 key 的 hashcode() 一定不能相同。
這個時候,使用Object的 hashcode() 很顯然就是不行了,應為它返回每個物件的 hashcode() 都是不相同的。
當然了,所有 hashcode() 都返回 0 ,這也不是不行,只是違背了這句話:如果兩個物件hashCode相同,不能證明兩個物件是同一個物件(不一定相等)。
來吧,開始發散思維。
@Override
public int hashCode() {
return id.hashCode() + name.hashCode();
}
但是我們還是要從各個角度出發去想一下的,比如判空,是否會值溢位之類的,這裡我們也參考String 的 hashcode() 套娃一個。優化後:
@Override
public int hashCode() {
//初始值,別問為什麼是 17, 因為我寫 18 你也會這樣問的
int result = 17;
// 31 作為基數,不知道啊,String 裡面 的 hashcode() 就是寫的 31
result = 31 * result + (id == null ? 0 : id.hashCode());
result = 31 * result + (name == null ? 0 : name.hashCode());
return result;
}
@Override
public int hashCode() {
return Objects.hash(id,name);
}
看了這兩個方法,沒錯,這都是站在巨人的肩膀上。其實要是自己動手實現,那難度就直線上升了,至少演演算法這一塊要啃一下。
寫hashcode()要遵循以下原則:
equals不相等時並不強制要求雜湊值相等,但是一個優秀的雜湊演演算法應儘可能讓元素均勻分佈,降低衝突發生的概率,即在equals不相等時儘量讓雜湊值也不相等,這樣&&或||短路操作一旦生效,會極大提高程式的效率。
重寫equals都是根據業務需求去進行重寫的,想想為什麼String是這麼判斷兩個字串物件相等的。如果拋開業務需求談這個,就是耍流氓。但是大多數情況下,都是不用重寫 equals() 和 hashcode() 的,就是因為有了特殊的業務邏輯,所以我們才需要重寫裡面的邏輯。
不想寫了, 今天就先結束吧,呼應一下開頭,看看都涉及到哪些知識點,下次再遇到這樣的八股文,一開口就能聊幾個小時。
1、hashcode() 方法是 native 關鍵字修飾的,你有了解過嘛、Java方法和本地方法有什麼不同、為什麼要註冊本地方法呢?
2、Object類中並沒有hashcode() 方法原始碼,真正的原始碼應該去哪裡看
3、jdk 原始碼目錄,你真正開啟過jdk原始碼嘛
4、 hashcode() 原始碼上有 @see java.lang.System#identityHashCode。那你知道 這兩個方法有什麼區別嘛
5、hashcode() 方法一定要重寫,強調的是和hash表相關,那你知道原始碼中都是這麼體現的嘛
6、hashcode() 手寫實現,需要有什麼功底呢?
7、呼叫了 String 的 hashcode() 去實現,那你知道 String#hashcode() 中為什麼要用常數 31 嘛
8、如果不從寫 hashcode() 會發生什麼呢
9、Objects#hash()實現和 String 的 hashcode() 有什麼異同呢
10、hashmap 中的 hash演演算法和 hashcode 有什麼關係呢,hahscode會不會影響到 hash演演算法的結果?
11、如果hashcode() 每次返回的數是一個亂數,會發生什麼。
12、為什麼先比較 hashcode() 在比較 equlas能提高效率呢?List 集合比較的弊端、Map key 實現的優勢