本文主要針對如下三個問題進行解釋:
- 默認情況下hashCode相同是不是意味著equals方法相等肆汹?
- 默認情況下equals方法相等是不是意味著hashCode相同硬萍?
- 重寫equals方法是不是需要重寫hashCode方法匹中?為什么继谚?
默認情況下hashCode相同是不是意味著equals方法相等和問題?equals方法相等是不是意味著hashCode相同?
之所以將這兩個問題放在一起篓吁,是因為兩個問題可以聯(lián)系在一起回答隙袁,在Object類中的hashCode和equals方法中已經(jīng)有了該問題的答案
圖中紅色區(qū)域的意思為:如果兩個對象根據(jù)equals方法判定相等滑臊,那么這兩個對象的hashCode方法必定是相同的integer的整形值口芍。其中暗含了兩層意思:
- equals相等的兩個對象,其hashCode必定相等
- 通過equals判定前雇卷,必定有hashCode值比較判斷的步驟
看到這里我們自然疑惑hashCode方法是如何得到某個對象的hash值的鬓椭,我們再看如下這句話
其中說到hashCode方法一種典型的實現(xiàn)是將對象在堆內(nèi)的地址通過某種手段轉成一個integer整形值小染,但是該方法是native修飾的,需要通過查閱openjdk的源碼得到贮折,查閱相關資料得到真正對應的hashCode生成方法如下
intptr_t ObjectSynchronizer::FastHashCode (Thread * Self, oop obj) {
if (UseBiasedLocking) {
// NOTE: many places throughout the JVM do not expect a safepoint
// to be taken here, in particular most operations on perm gen
// objects. However, we only ever bias Java instances and all of
// the call sites of identity_hash that might revoke biases have
// been checked to make sure they can handle a safepoint. The
// added check of the bias pattern is to avoid useless calls to
// thread-local storage.
if (obj->mark()->has_bias_pattern()) {
// Box and unbox the raw reference just in case we cause a STW safepoint.
Handle hobj (Self, obj) ;
// Relaxing assertion for bug 6320749.
assert (Universe::verify_in_progress() ||
!SafepointSynchronize::is_at_safepoint(),
biases should not be seen by VM thread here);
BiasedLocking::revoke_and_rebias(hobj, false, JavaThread::current());
obj = hobj() ;
assert(!obj->mark()->has_bias_pattern(), biases should be revoked by now);
}
}
// hashCode() is a heap mutator ...
// Relaxing assertion for bug 6320749.
assert (Universe::verify_in_progress() ||
!SafepointSynchronize::is_at_safepoint(), invariant) ;
assert (Universe::verify_in_progress() ||
Self->is_Java_thread() , invariant) ;
assert (Universe::verify_in_progress() ||
((JavaThread *)Self)->thread_state() != _thread_blocked, invariant) ;
ObjectMonitor* monitor = NULL;
markOop temp, test;
intptr_t hash;
markOop mark = ReadStableMark (obj);
// object should remain ineligible for biased locking
assert (!mark->has_bias_pattern(), invariant) ;
if (mark->is_neutral()) {
hash = mark->hash(); // this is a normal header
if (hash) { // if it has hash, just return it
return hash;
}
hash = get_next_hash(Self, obj); // allocate a new hash code
temp = mark->copy_set_hash(hash); // merge the hash code into header
// use (machine word version) atomic operation to install the hash
test = (markOop) Atomic::cmpxchg_ptr(temp, obj->mark_addr(), mark);
if (test == mark) {
return hash;
}
// If atomic operation failed, we must inflate the header
// into heavy weight monitor. We could add more code here
// for fast path, but it does not worth the complexity.
} else if (mark->has_monitor()) {
monitor = mark->monitor();
temp = monitor->header();
assert (temp->is_neutral(), invariant) ;
hash = temp->hash();
if (hash) {
return hash;
}
// Skip to the following code to reduce code size
} else if (Self->is_lock_owned((address)mark->locker())) {
temp = mark->displaced_mark_helper(); // this is a lightweight monitor owned
assert (temp->is_neutral(), invariant) ;
hash = temp->hash(); // by current thread, check if the displaced
if (hash) { // header contains hash code
return hash;
}
// WARNING:
// The displaced header is strictly immutable.
// It can NOT be changed in ANY cases. So we have
// to inflate the header into heavyweight monitor
// even the current thread owns the lock. The reason
// is the BasicLock (stack slot) will be asynchronously
// read by other threads during the inflate() function.
// Any change to stack may not propagate to other threads
// correctly.
}
// 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;
重寫equals方法是不是需要重寫hashCode方法氧映?為什么?
首先該問題的答案仍然在Object中的equals方法注釋中寫的很清楚脱货,如下圖所示
按紅框處的官方解釋來說岛都,只要equals方法被重寫了就必須重寫hashCode方法,此處也解釋了“必須”的原因振峻,要維持hashCode方法的contract約定臼疫,hashCode方法中申明了相同的對象必須有相同的hash code。
為了進一步加深對該“必須”的理解扣孟,這里又從兩個方面舉例說明:
<li> 自己創(chuàng)建一個自定義對象烫堤,只重寫equals方法而不重寫hashCode方法,看看有什么問題
<li> 從HashMap源碼的角度分析一下凤价,如果只重寫equals而不重寫hashCode有什么問題
<p> 自己創(chuàng)建一個Person對象鸽斟,有pname和age字段,如下所示
public class Person implements Serializable {
private static final long serialVersionUID = 7592930394427200495L;
private String pname;
private int age;
public Person() {
}
public Person(String pname, int age) {
this.pname = pname;
this.age = age;
}
public String getPname() {
return pname;
}
public void setPname(String pname) {
this.pname = pname;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Person)) return false;
Person person = (Person) o;
if (age != person.age) return false;
return !(pname != null ? !pname.equals(person.pname) : person.pname != null);
}
// @Override
// public int hashCode() {
// int result = pname != null ? pname.hashCode() : 0;
// result = 31 * result + age;
// return result;
// }
}
<p> 進行測試
@Test
public void fun() {
Person p1 = new Person("lisi", 15);
Person p2 = new Person("lisi", 15);
Assert.assertEquals(false, p1.equals(p2));
}
<p> 結果如下
p1和p2在java堆中肯定分屬不同的Person實例對象利诺,其地址必定不相同富蓄,但因為我們僅僅重寫了equals方法,只對pname和age的值進行了比對從而導致了結果的錯誤慢逾,如果重寫了hashCode方法立倍,根據(jù)兩個實例地址的相關算法進行判斷就會避免這個問題
同樣的我們再來分析HashMap中的一段源碼再次說明hashCode和equals方法同時重寫的重要性灭红,其中的關鍵點在于put操作時的邏輯
當新元素放入HashMap時,會首先計算出該元素對應放在哪一個Entry鏈表上(HashMap原理不了解的請查閱相關文檔)口注,然后通過和鏈表上的每一個元素比較变擒,來判斷新加入元素是否是重復元素,而判斷重復元素的思路就體現(xiàn)了兩個方法協(xié)同的重要性寝志,首先會判斷兩個元素的hash值是否相等娇斑,再判斷兩個元素equals是否相等,設想一下材部,如果沒有重寫元素的hashCode方法毫缆,那么就有可能存在這種可能,兩個元素不等败富,但hash code相等,重寫的equals也相等(如Person例中)摩窃,從而導致錯誤的覆蓋