千鋒扣丁學(xué)堂Java培訓(xùn)之深入理解HashCode方法
2019-08-12 13:55:44
5770瀏覽
今天千鋒扣丁學(xué)堂Java培訓(xùn)老師給大家分享一篇關(guān)于Java中HashCode方法的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),下面我們一起來(lái)看一下吧。
1、0前言
在學(xué)習(xí)Go語(yǔ)言,Go語(yǔ)言中有指針對(duì)象,一個(gè)指針變量指向了一個(gè)值的內(nèi)存地址。學(xué)習(xí)過(guò)C語(yǔ)言的猿友應(yīng)該都知道指針的概念。Go語(yǔ)言語(yǔ)法與C相近,可以說(shuō)是類C的編程語(yǔ)言,所以Go語(yǔ)言中有指針也是很正常的。我們可以通過(guò)將取地址符&放在一個(gè)變量前使用就會(huì)得到相應(yīng)變量的內(nèi)存地址。
package main
import "fmt"
func main() {
var a int= 20 /* 聲明實(shí)際變量 */
var ip *int /* 聲明指針變量 */
ip = &a /* 指針變量的存儲(chǔ)地址 */
fmt.Printf("a 變量的地址是: %x\n", &a )
/* 指針變量的存儲(chǔ)地址 */
fmt.Printf("ip 變量?jī)?chǔ)存的指針地址: %x\n", ip )
/* 使用指針訪問(wèn)值 */
fmt.Printf("*ip 變量的值: %d\n", *ip )
}
因?yàn)楸救酥饕_(kāi)發(fā)語(yǔ)言是Java,所以我就聯(lián)想到Java中沒(méi)有指針,那么Java中如何獲取變量的內(nèi)存地址呢?
如果能獲取變量的內(nèi)存地址那么就可以清晰的知道兩個(gè)對(duì)象是否是同一個(gè)對(duì)象,如果兩個(gè)對(duì)象的內(nèi)存地址相等那么無(wú)疑是同一個(gè)對(duì)象反之則是不同的對(duì)象。
很多人說(shuō)對(duì)象的HashCode方法返回的就是對(duì)象的內(nèi)存地址,包括我在《Java核心編程·卷I》的第5章內(nèi)容中也發(fā)現(xiàn)說(shuō)是HashCode其值就是對(duì)象的內(nèi)存地址。
**但是HashCode方法真的是內(nèi)存地址嗎?**回答這個(gè)問(wèn)題前我們先回顧下一些基礎(chǔ)知識(shí)。
2|0==和equals
在Java中比較兩個(gè)對(duì)象是否相等主要是通過(guò)==號(hào),比較的是他們?cè)趦?nèi)存中的存放地址。Object類是Java中的超類,是所有類默認(rèn)繼承的,如果一個(gè)類沒(méi)有重寫(xiě)Object的equals方法,那么通過(guò)equals方法也可以判斷兩個(gè)對(duì)象是否相同,因?yàn)樗鼉?nèi)部就是通過(guò)==來(lái)實(shí)現(xiàn)的。
//Indicates whether some other object is "equal to" this one.
public boolean equals(Object obj) {
return (this == obj);
}
**Tips:**這里額外解釋個(gè)疑惑
我們學(xué)習(xí)Java的時(shí)候知道,Java的繼承是單繼承,如果所有的類都繼承了Object類,那么為何創(chuàng)建一個(gè)類的時(shí)候還可以extend其他的類?
這里涉及到直接繼承和間接繼承的問(wèn)題,當(dāng)創(chuàng)建的類沒(méi)有通過(guò)關(guān)鍵字extend顯示繼承指定的類時(shí),類默認(rèn)的直接繼承了Object,A-->Object。當(dāng)創(chuàng)建的類通過(guò)關(guān)鍵字extend顯示繼承指定的類時(shí),則它間接的繼承了Object類,A-->B-->Object。
這里的相同,是說(shuō)比較的兩個(gè)對(duì)象是否是同一個(gè)對(duì)象,即在內(nèi)存中的地址是否相等。而我們有時(shí)候需要比較兩個(gè)對(duì)象的內(nèi)容是否相同,即類具有自己特有的“邏輯相等”概念,而不是想了解它們是否指向同一個(gè)對(duì)象。
例如比較如下兩個(gè)字符串是否相同Stringa="Hello"和Stringb=newString("Hello"),這里的相同有兩種情形,是要比較a和b是否是同一個(gè)對(duì)象(內(nèi)存地址是否相同),還是比較它們的內(nèi)容是否相等?這個(gè)具體需要怎么區(qū)分呢?
如果使用==那么就是比較它們?cè)趦?nèi)存中是否是同一個(gè)對(duì)象,但是String對(duì)象的默認(rèn)父類也是Object,所以默認(rèn)的equals方法比較的也是內(nèi)存地址,所以我們要重寫(xiě)equals方法,正如String源碼中所寫(xiě)的那樣。
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;
}
這樣當(dāng)我們a==b時(shí)是判斷a和b是否是同一個(gè)對(duì)象,a.equals(b)則是比較a和b的內(nèi)容是否相同,這應(yīng)該很好理解。
JDK中不止String類重寫(xiě)了equals方法,還有數(shù)據(jù)類型Integer,Long,Double,F(xiàn)loat等基本也都重寫(xiě)了equals方法。所以我們?cè)诖a中用Long或者Integer做業(yè)務(wù)參數(shù)的時(shí)候,如果要比較它們是否相等,記得需要使用equals方法,而不要使用==。
因?yàn)槭褂?=號(hào)會(huì)有意想不到的坑出現(xiàn),像這種數(shù)據(jù)類型很多都會(huì)在內(nèi)部封裝一個(gè)常量池,例如IntegerCache,LongCache等等。當(dāng)數(shù)據(jù)值在某個(gè)范圍內(nèi)時(shí)會(huì)直接從常量池中獲取而不會(huì)去新建對(duì)象。
如果要使用==,可以將這些數(shù)據(jù)包裝類型轉(zhuǎn)換為基本類型之后,再通過(guò)==來(lái)比較,因?yàn)榛绢愋屯ㄟ^(guò)==比較的是數(shù)值,但是在轉(zhuǎn)換的過(guò)程中需要注意NPE(NullPointException)的發(fā)生。
3|0Object中的HashCode
equals方法能比較兩個(gè)對(duì)象的內(nèi)容是否相等,因此可以用來(lái)查找某個(gè)對(duì)象是否在集合容器中,通常大致就是逐一去取集合中的每個(gè)對(duì)象元素與需要查詢的對(duì)象進(jìn)行equals比較,當(dāng)發(fā)現(xiàn)某個(gè)元素與要查找的對(duì)象進(jìn)行equals方法比較的結(jié)果相等時(shí),則停止繼續(xù)查找并返回肯定的信息,否則,返回否定的信息。
但是通過(guò)這種比較的方式效率很低,時(shí)間復(fù)雜度比較高。那么我們是否可以通過(guò)某種編碼方式,將每一個(gè)對(duì)象都具有某個(gè)特定的碼值,根據(jù)碼值將對(duì)象分組然后劃分到不同的區(qū)域,這樣當(dāng)我們需要在集合中查詢某個(gè)對(duì)象時(shí),我們先根據(jù)該對(duì)象的碼值就能確定該對(duì)象存儲(chǔ)在哪一個(gè)區(qū)域,然后再到該區(qū)域中通過(guò)equals方式比較內(nèi)容是否相等,就能知道該對(duì)象是否存在集合中。
通過(guò)這種方式我們減少了查詢比較的次數(shù),優(yōu)化了查詢的效率同時(shí)也就減少了查詢的時(shí)間。
這種編碼方式在Java中就是hashCode方法,Object類中默認(rèn)定義了該方法,它是一個(gè)native修飾的本地方法,返回值是一個(gè)int類型。
/**
* 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方法返回該對(duì)象的哈希碼值。它可以為像HashMap這樣的哈希表有益。Object類中定義的hashCode方法為不同的對(duì)象返回不同的整形值。具有迷惑異議的地方就是Thisistypicallyimplementedbyconvertingtheinternaladdressoftheobjectintoaninteger這一句,意為通常情況下實(shí)現(xiàn)的方式是將對(duì)象的內(nèi)部地址轉(zhuǎn)換為整形值。
如果你不深究就會(huì)認(rèn)為它返回的就是對(duì)象的內(nèi)存地址,我們可以繼續(xù)看看它的實(shí)現(xiàn),但是因?yàn)檫@里是native方法所以我們沒(méi)辦法直接在這里看到內(nèi)部是如何實(shí)現(xiàn)的。native方法本身非java實(shí)現(xiàn),如果想要看源碼,只有下載完整的jdk源碼,Oracle的JDK是看不到的,OpenJDK或其他開(kāi)源JRE是可以找到對(duì)應(yīng)的C/C++代碼。我們?cè)贠penJDK中找到Object.c文件,可以看到hashCode方法指向JVM_IHashCode方法來(lái)處理。
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方法實(shí)現(xiàn)在jvm.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
這里是一個(gè)三目表達(dá)式,真正計(jì)算獲得hashCode值的是ObjectSynchronizer::FastHashCode,它具體的實(shí)現(xiàn)在synchronizer.cpp中,截取部分關(guān)鍵代碼片段。
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;
}
從以上代碼片段中可以發(fā)現(xiàn),實(shí)際計(jì)算hashCode的是get_next_hash,還在這份文件中我們搜索get_next_hash,得到他的關(guān)鍵代碼。
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<intptr_t>(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<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 = 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開(kāi)始算的話,這里提供了6種計(jì)算hash值的方案,有自增序列,隨機(jī)數(shù),關(guān)聯(lián)內(nèi)存地址等多種方式,其中官方默認(rèn)的是最后一種,即隨機(jī)數(shù)生成。可以看出hashCode也許和內(nèi)存地址有關(guān)系,但不是直接代表內(nèi)存地址的,具體需要看虛擬機(jī)版本和設(shè)置。
4|0equals和hashCode
equals和hashCode都是Object類擁有的方法,包括Object類中的toString方法打印的內(nèi)容也包含hashCode的無(wú)符號(hào)十六進(jìn)制值。
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
由于需要比較對(duì)象內(nèi)容,所以我們通常會(huì)重寫(xiě)equals方法,但是重寫(xiě)equals方法的同時(shí)也需要重寫(xiě)hashCode方法,有沒(méi)有想過(guò)為什么?
因?yàn)槿绻贿@樣做的話,就會(huì)違反hashCode的通用約定,從而導(dǎo)致該類無(wú)法結(jié)合所有基于散列的集合一起正常工作,這類集合包括HashMap和HashSet。
這里的通用約定,從Object類的hashCode方法的注釋可以了解,主要包括以下幾個(gè)方面,
在應(yīng)用程序的執(zhí)行期間,只要對(duì)象的equals方法的比較操作所用到的信息沒(méi)有被修改,那么對(duì)同一個(gè)對(duì)象的多次調(diào)用,hashCode方法都必須始終返回同一個(gè)值。
如果兩個(gè)對(duì)象根據(jù)equals方法比較是相等的,那么調(diào)用這兩個(gè)對(duì)象中的hashCode方法都必須產(chǎn)生同樣的整數(shù)結(jié)果。
如果兩個(gè)對(duì)象根據(jù)equals方法比較是不相等的,那么調(diào)用者兩個(gè)對(duì)象中的hashCode方法,則不一定要求hashCode方法必須產(chǎn)生不同的結(jié)果。但是給不相等的對(duì)象產(chǎn)生不同的整數(shù)散列值,是有可能提高散列表(hashtable)的性能。
從理論上來(lái)說(shuō)如果重寫(xiě)了equals方法而沒(méi)有重寫(xiě)hashCode方法則違背了上述約定的第二條,相等的對(duì)象必須擁有相等的散列值。
但是規(guī)則是大家默契的約定,如果我們就喜歡不走尋常路,在重寫(xiě)了equals方法后沒(méi)有覆蓋hashCode方法,會(huì)產(chǎn)生什么后果嗎?
我們自定義一個(gè)Student類,并且重寫(xiě)了equals方法,但是我們沒(méi)有重寫(xiě)hashCode方法,那么當(dāng)調(diào)用Student類的hashCode方法的時(shí)候,默認(rèn)就是調(diào)用超類Object的hashCode方法,根據(jù)隨機(jī)數(shù)返回的一個(gè)整型值。
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;
}
}
我們創(chuàng)建兩個(gè)對(duì)象并且設(shè)置屬性值一樣,測(cè)試下結(jié)果:
public static void main(String[] args) {
Student student1 = new Student("小明", "male");
Student student2 = new Student("小明", "male");
System.out.println("equals結(jié)果:" + student1.equals(student2));
System.out.println("對(duì)象1的散列值:" + student1.hashCode() + ",對(duì)象2的散列值:" + student2.hashCode());
}
得到的結(jié)果
equals結(jié)果:true
對(duì)象1的散列值:1058025095,對(duì)象2的散列值:665576141
我們重寫(xiě)了equals方法,根據(jù)姓名和性別的屬性來(lái)判斷對(duì)象的內(nèi)容是否相等,但是hashCode由于是調(diào)用Object類的hashCode方法,所以打印的是兩個(gè)不相等的整型值。
如果這個(gè)對(duì)象我們用HashMap存儲(chǔ),將對(duì)象作為key,熟知HashMap原理的同學(xué)應(yīng)該知道,HashMap是由數(shù)組+鏈表的結(jié)構(gòu)組成,這樣的結(jié)果就是因?yàn)樗鼈僪ashCode不相等,所以放在了數(shù)組的不同下標(biāo),當(dāng)我們根據(jù)Key去查詢的時(shí)候結(jié)果就為null。
public static void main(String[] args) {
Student student1 = new Student("小明", "male");
Student student2 = new Student("小明", "male");
HashMap<Student, String> hashMap = new HashMap<>();
hashMap.put(student1, "小明");
String value = hashMap.get(student2);
System.out.println(value);
}
輸出結(jié)果
null
得到的結(jié)果我們肯定不滿意,這里的student1和student2雖然內(nèi)存地址不同,但是它們的邏輯內(nèi)容相同,我們認(rèn)為它們應(yīng)該是相同的。
這里如果不好理解,猿友可以將Student類換成String類思考下,String類是我們常常作為HashMap的Key值使用的,試想如果String類只重寫(xiě)了equals方法而沒(méi)有重寫(xiě)HashCode方法,這里將某個(gè)字符串newString("s")作為Key然后put一個(gè)值,但是再根據(jù)newString("s")去Get的時(shí)候卻得到null的結(jié)果,這是難以讓人接受的。
所以無(wú)論是理論的約定上還是實(shí)際編程中,我們重寫(xiě)equals方法的同時(shí)總要重寫(xiě)hashCode方法,請(qǐng)記住這點(diǎn)。
雖然hashCode方法被重寫(xiě)了,但是如果我們想要獲取原始的Object類中的哈希碼,我們可以通過(guò)System.identityHashCode(Objecta)來(lái)獲取,該方法返回默認(rèn)的Object的hashCode方法值,即使對(duì)象的hashCode方法被重寫(xiě)了也不影響。
public static native int identityHashCode(Object x);
5|0總結(jié)
如果HashCode不是內(nèi)存地址,那么Java中怎么獲取內(nèi)存地址呢?找了一圈發(fā)現(xiàn)沒(méi)有直接可用的方法。
后來(lái)想想也許這是Java語(yǔ)言編寫(xiě)者認(rèn)為沒(méi)有直接獲取內(nèi)存地址的必要吧,因?yàn)镴ava是一門(mén)高級(jí)語(yǔ)言相對(duì)于機(jī)器語(yǔ)言的匯編或者C語(yǔ)言來(lái)說(shuō)更抽象并隱藏了復(fù)雜性,因?yàn)楫吘故窃贑和C++的基礎(chǔ)上進(jìn)一步封裝的。而且由于自動(dòng)垃圾回收機(jī)制和對(duì)象年齡代的問(wèn)題,Java中對(duì)象的地址是會(huì)變化的,因此獲取實(shí)際內(nèi)存地址的意義不大。
以上就是關(guān)于千鋒扣丁學(xué)堂Java培訓(xùn)之深入理解HashCode方法的全部?jī)?nèi)容了,
想要學(xué)好Java開(kāi)發(fā)小編給大家推薦口碑良好的扣丁學(xué)堂,扣丁學(xué)堂有專業(yè)老師制定的Java學(xué)習(xí)路線圖輔助學(xué)員學(xué)習(xí),此外還有與時(shí)俱進(jìn)的Java課程體系和Java視頻教程供大家學(xué)習(xí),想要學(xué)好Java開(kāi)發(fā)技術(shù)的小伙伴快快行動(dòng)吧。扣丁學(xué)堂Java技術(shù)交流群:850353792。
【關(guān)注微信公眾號(hào)獲取更多學(xué)習(xí)資料】 【掃碼進(jìn)入JavaEE/微服務(wù)VIP免費(fèi)公開(kāi)課】
查看更多關(guān)于“Java開(kāi)發(fā)資訊”的相關(guān)文章>>
標(biāo)簽:
Java培訓(xùn)
Java視頻教程
Java多線程
Java面試題
Java學(xué)習(xí)視頻
springBoot項(xiàng)目