對Java中HashCode方法的深入思考
阿新 • • 發佈:2019-08-12
# 前言
最近在學習 Go 語言,Go 語言中有指標物件,一個指標變數指向了一個值的記憶體地址。學習過 C 語言的猿友應該都知道指標的概念。Go 語言語法與 C 相近,可以說是類 C 的程式語言,所以 Go 語言中有指標也是很正常的。我們可以通過將取地址符`&`放在一個變數前使用就會得到相應變數的記憶體地址。
```go
package main
import "fmt"
func main() {
var a int= 20 /* 宣告實際變數 */
var ip *int /* 宣告指標變數 */
ip = &a /* 指標變數的儲存地址 */
fmt.Printf("a 變數的地址是: %x\n", &a )
/* 指標變數的儲存地址 */
fmt.Printf("ip 變數儲存的指標地址: %x\n", ip )
/* 使用指標訪問值 */
fmt.Printf("*ip 變數的值: %d\n", *ip )
}
```
因為本人主要開發語言是 Java,所以我就聯想到 Java 中沒有指標,那麼 Java 中如何獲取變數的記憶體地址呢?
如果能獲取變數的記憶體地址那麼就可以清晰的知道兩個物件是否是同一個物件,如果兩個物件的記憶體地址相等那麼無疑是同一個物件反之則是不同的物件。
很多人說物件的 HashCode 方法返回的就是物件的記憶體地址,包括我在《Java核心程式設計·卷I》的第5章內容中也發現說是 HashCode 其值就是物件的記憶體地址。
![](https://img2018.cnblogs.com/blog/1162587/201908/1162587-20190805235645679-445761150.png)
**但是 HashCode 方法真的是記憶體地址嗎?**回答這個問題前我們先回顧下一些基礎知識。
# ==和equals
在 Java 中比較兩個物件是否相等主要是通過 `==`號,比較的是他們在記憶體中的存放地址。Object 類是 Java 中的超類,是所有類預設繼承的,如果一個類沒有重寫 Object 的 `equals`方法,那麼通過`equals`方法也可以判斷兩個物件是否相同,因為它內部就是通過`==`來實現的。
```java
//Indicates whether some other object is "equal to" this one.
public boolean equals(Object obj) {
return (this == obj);
}
```
> **Tips:**這裡額外解釋個疑惑
>
> **我們學習 Java 的時候知道,Java 的繼承是單繼承,如果所有的類都繼承了 Object 類,那麼為何建立一個類的時候還可以extend其他的類?**
>
> 這裡涉及到直接繼承和間接繼承的問題,當建立的類沒有通過關鍵字 `extend` 顯示繼承指定的類時,類預設的直接繼承了Object,A --> Object。當建立的類通過關鍵字 `extend` 顯示繼承指定的類時,則它間接的繼承了Object類,A --> B --> Object。
這裡的相同,是說比較的兩個物件是否是同一個物件,即在記憶體中的地址是否相等。而我們有時候需要比較兩個物件的內容是否相同,即類具有自己特有的“邏輯相等”概念,而不是想了解它們是否指向同一個物件。
例如比較如下兩個字串是否相同`String a = "Hello"` 和 `String b = new String("Hello")`,這裡的相同有兩種情形,是要比較 a 和 b 是否是同一個物件(記憶體地址是否相同),還是比較它們的內容是否相等?這個具體需要怎麼區分呢?
如果使用 `==` 那麼就是比較它們在記憶體中是否是同一個物件,但是 String 物件的預設父類也是 Object,所以預設的`equals`方法比較的也是記憶體地址,所以我們要重寫 `equals`方法,正如 String 原始碼中所寫的那樣。
```java
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;
}
```
這樣當我們 `a == b`時是判斷 a 和 b 是否是同一個物件,`a.equals(b)`則是比較 a 和 b 的內容是否相同,這應該很好理解。
JDK 中不止 String 類重寫了equals 方法,還有資料型別 Integer,Long,Double,Float等基本也都重寫了 `equals` 方法。所以我們在程式碼中用 Long 或者 Integer 做業務引數的時候,如果要比較它們是否相等,記得需要使用 `equals` 方法,而不要使用 `==`。
因為使用 `==`號會有意想不到的坑出現,像這種資料型別很多都會在內部封裝一個常量池,例如 IntegerCache,LongCache 等等。當資料值在某個範圍內時會直接從常量池中獲取而不會去新建物件。
如果要使用`==`,可以將這些資料包裝型別轉換為基本型別之後,再通過`==`來比較,因為基本型別通過`==`比較的是數值,但是在轉換的過程中需要注意 NPE(NullPointException)的發生。
# Object中的HashCode
equals 方法能比較兩個物件的內容是否相等,因此可以用來查詢某個物件是否在集合容器中,通常大致就是逐一去取集合中的每個物件元素與需要查詢的物件進行`equals`比較,當發現某個元素與要查詢的物件進行equals方法比較的結果相等時,則停止繼續查詢並返回肯定的資訊,否則,返回否定的資訊。
但是通過這種比較的方式效率很低,時間複雜度比較高。那麼我們是否可以通過某種編碼方式,將每一個物件都具有某個特定的碼值,根據碼值將物件分組然後劃分到不同的區域,這樣當我們需要在集合中查詢某個物件時,我們先根據該物件的碼值就能確定該物件儲存在哪一個區域,然後再到該區域中通過`equals`方式比較內容是否相等,就能知道該物件是否存在集合中。
通過這種方式我們減少了查詢比較的次數,優化了查詢的效率同時也就減少了查詢的時間。
這種編碼方式在 Java 中就是 hashCode 方法,Object 類中預設定義了該方法, 它是一個 native 修飾的本地方法,返回值是一個 int 型別。
```java
/**
* 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}.
* ...
* 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();
```
從註釋的描述可以知道,hashCode 方法返回該物件的雜湊碼值。它可以為像 HashMap 這樣的雜湊表有益。Object 類中定義的 hashCode 方法為不同的物件返回不同的整形值。具有迷惑異議的地方就是`This is typically implemented by converting the internal address of the object into an integer`這一句,意為通常情況下實現的方式是將物件的內部地址轉換為整形值。
如果你不深究就會認為它返回的就是物件的記憶體地址,我們可以繼續看看它的實現,但是因為這裡是 native 方法所以我們沒辦法直接在這裡看到內部是如何實現的。native 方法本身非 java 實現,如果想要看原始碼,只有下載完整的 jdk 原始碼,Oracle 的 JDK 是看不到的,OpenJDK 或其他開源 JRE 是可以找到對應的 C/C++ 程式碼。我們在 OpenJDK 中找到 [Object.c](http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/3462d04401ba/src/share/native/java/lang/Object.c) 檔案,可以看到hashCode 方法指向 `JVM_IHashCode` 方法來處理。
```cpp
static JNINativeMethod methods[] = {
{"hashCode", "()I", (void *)&JVM_IHashCode},
{"wait", "(J)V", (void *)&JVM_MonitorWait},
{"notify", "()V", (void *)&JVM_MonitorNotify},
{"notifyAll", "()V", (void *)&JVM_MonitorNotifyAll},
{"clone", "()Ljava/lang/Object;", (void *)&JVM_Clone},
};
```
而`JVM_IHashCode`方法實現在 [jvm.cpp](http://hg.openjdk.java.net/jdk8/jdk8/hotspot/file/87ee5ee27509/src/share/vm/prims/jvm.cpp)中的定義為:
```cpp
JVM_ENTRY(jint, JVM_IHashCode(JNIEnv* env, jobject handle))
JVMWrapper("JVM_IHashCode");
// as implemented in the classic virtual machine; return 0 if object is NULL
return handle == NULL ? 0 : ObjectSynchronizer::FastHashCode (THREAD, JNIHandles::resolve_non_null(handle)) ;
JVM_END
```
這裡是一個三目表示式,真正計算獲得 hashCode 值的是[ObjectSynchronizer::FastHashCode](http://hg.openjdk.java.net/jdk8/jdk8/hotspot/file/87ee5ee27509/src/share/vm/runtime/synchronizer.cpp),它具體的實現在[synchronizer.cpp](http://hg.openjdk.java.net/jdk8/jdk8/hotspot/file/87ee5ee27509/src/share/vm/runtime/synchronizer.cpp)中,擷取部分關鍵程式碼片段。
```cpp
intptr_t ObjectSynchronizer::FastHashCode (Thread * Self, oop obj) {
if (UseBiasedLocking) {
......
// Inflate the monitor to set hash code
monitor = ObjectSynchronizer::inflate(Self, obj);
// Load displaced header and check it has hash code
mark = monitor->header();
assert (mark->is_neutral(), "invariant") ;
hash = mark->hash();
if (hash == 0) {
hash = get_next_hash(Self, obj);
temp = mark->copy_set_hash(hash); // merge hash code into header
assert (temp->is_neutral(), "invariant") ;
test = (markOop) Atomic::cmpxchg_ptr(temp, monitor, mark);
if (test != mark) {
// The only update to the header in the monitor (outside GC)
// is install the hash code. If someone add new usage of
// displaced header, please update this code
hash = test->hash();
assert (test->is_neutral(), "invariant") ;
assert (hash != 0, "Trivial unexpected object/monitor header usage.");
}
}
// We finally get the hash
return hash;
}
```
從以上程式碼片段中可以發現,實際計算hashCode的是 `get_next_hash`,還在這份檔案中我們搜尋`get_next_hash`,得到他的關鍵程式碼。
```cpp
static inline intptr_t get_next_hash(Thread * Self, oop obj) {
intptr_t value = 0 ;
if (hashCode == 0) {
// This form uses an unguarded global Park-Miller RNG,
// so it's possible for two threads to race and generate the same 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 addrBits = cast_from_oop(obj) >> 3 ;
value = addrBits ^ (addrBits >> 5) ^ GVars.stwRandom ;
} else
if (hashCode == 2) {
value = 1 ; // for sensitivity testing
} else
if (hashCode == 3) {
value = ++GVars.hcSequence ;
} else
if (hashCode == 4) {
value = cast_from_oop(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 = 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;
}
```
從`get_next_hash`的方法中我們可以看到,如果從0開始算的話,這裡提供了6種計算 hash 值的方案,有自增序列,隨機數,關聯記憶體地址等多種方式,其中官方預設的是最後一種,即隨機數生成。可以看出 hashCode 也許和記憶體地址有關係,但不是直接代表記憶體地址的,具體需要看虛擬機器版本和設定。
# equals和hashCode
equals 和 hashCode 都是 Object 類擁有的方法,包括 Object 類中的 toString 方法列印的內容也包含 hashCode 的無符號十六進位制值。
```java
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
```
由於需要比較物件內容,所以我們通常會重寫 equals 方法,**但是重寫 equals 方法的同時也需要重寫 hashCode 方法,有沒有想過為什麼?**
因為如果不這樣做的話,就會違反 hashCode 的通用約定,從而導致該類無法結合所有基於雜湊的集合一起正常工作,這類集合包括 HashMap 和 HashSet。
這裡的**通用約定**,從 Object 類的 hashCode 方法的註釋可以瞭解,主要包括以下幾個方面,
- 在應用程式的執行期間,只要物件的 equals 方法的比較操作所用到的資訊沒有被修改,那麼對同一個物件的多次呼叫,hashCode 方法都必須始終返回同一個值。
- 如果兩個物件根據 equals 方法比較是相等的,那麼呼叫這兩個物件中的 hashCode 方法都必須產生同樣的整數結果。
- 如果兩個物件根據 equals 方法比較是不相等的,那麼呼叫者兩個物件中的 hashCode 方法,則不一定要求 hashCode 方法必須產生不同的結果。但是給不相等的物件產生不同的整數雜湊值,是有可能提高散列表(hash table)的效能。
從理論上來說如果重寫了 equals 方法而沒有重寫 hashCode 方法則違背了上述約定的第二條,**相等的物件必須擁有相等的雜湊值**。
但是規則是大家默契的約定,如果我們就喜歡不走尋常路,在重寫了 equals 方法後沒有覆蓋 hashCode 方法,會產生什麼後果嗎?
我們自定義一個 Student 類,並且重寫了 equals 方法,但是我們沒有重寫 hashCode 方法,那麼當呼叫 Student 類的 hashCode 方法的時候,預設就是呼叫超類 Object 的 hashCode 方法,根據隨機數返回的一個整型值。
```java
public class Student {
private String name;
private String gender;
public Student(String name, String gender) {
this.name = name;
this.gender = gender;
}
//省略 Setter,Gettter
@Override
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof Student) {
Student anotherStudent = (Student) anObject;
if (this.getName() == anotherStudent.getName()
|| this.getGender() == anotherStudent.getGender())
return true;
}
return false;
}
}
```
我們建立兩個物件並且設定屬性值一樣,測試下結果:
```java
public static void main(String[] args) {
Student student1 = new Student("小明", "male");
Student student2 = new Student("小明", "male");
System.out.println("equals結果:" + student1.equals(student2));
System.out.println("物件1的雜湊值:" + student1.hashCode() + ",物件2的雜湊值:" + student2.hashCode());
}
```
得到的結果
```java
equals結果:true
物件1的雜湊值:1058025095,物件2的雜湊值:665576141
```
我們重寫了 equals 方法,根據姓名和性別的屬性來判斷物件的內容是否相等,但是 hashCode 由於是呼叫 Object 類的 hashCode 方法,所以列印的是兩個不相等的整型值。
如果這個物件我們用 HashMap 儲存,將物件作為 key,熟知 HashMap 原理的同學應該知道,HashMap 是由陣列 + 連結串列的結構組成,這樣的結果就是因為它們 hashCode 不相等,所以放在了陣列的不同下標,當我們根據 Key 去查詢的時候結果就為 null。
```java
public static void main(String[] args) {
Student student1 = new Student("小明", "male");
Student student2 = new Student("小明", "male");
HashMap hashMap = new HashMap<>();
hashMap.put(student1, "小明");
String value = hashMap.get(student2);
System.out.println(value);
}
```
輸出結果
```java
null
```
得到的結果我們肯定不滿意,這裡的 student1 和 student2 雖然記憶體地址不同,但是它們的邏輯內容相同,我們認為它們應該是相同的。
這裡如果不好理解,猿友可以將 Student 類換成 String 類思考下,String 類是我們常常作為 HashMap 的 Key 值使用的,試想如果 String 類只重寫了 equals 方法而沒有重寫 HashCode 方法,這裡將某個字串 `new String("s")` 作為 Key 然後 put 一個值,但是再根據 `new String("s")` 去 Get 的時候卻得到 null 的結果,這是難以讓人接受的。
**所以無論是理論的約定上還是實際程式設計中,我們重寫 equals 方法的同時總要重寫 hashCode 方法,請記住這點**。
雖然 hashCode 方法被重寫了,但是如果我們想要獲取原始的 Object 類中的雜湊碼,我們可以通過 `System.identityHashCode(Object a)`來獲取,該方法返回預設的 Object 的 hashCode 方法值,即使物件的 hashCode 方法被重寫了也不影響。
```java
public static native int identityHashCode(Object x);
```
# 總結
如果 HashCode 不是記憶體地址,那麼 Java 中怎麼獲取記憶體地址呢?找了一圈發現沒有直接可用的方法。
後來想想也許這是 Java 語言編寫者認為沒有直接獲取記憶體地址的必要吧,因為 Java 是一門高階語言相對於機器語言的彙編或者 C 語言來說更抽象並隱藏了複雜性,因為畢竟是在 C 和 C++ 的基礎上進一步封裝的。而且由於自動垃圾回收機制和物件年齡代的問題,Java 中物件的地址是會變化的,因此獲取實際記憶體地址的意義不大。
當然以上是博主本人自己的觀點,如果猿友有其他不同的意見或見解也可以留言,大家一起共同