一、概述
1、概念
- == : 該操作符生成的是一個boolean結(jié)果,它計算的是操作數(shù)的值之間的關(guān)系
- equals : Object 的 實例方法,比較兩個對象的content是否相同
- hashCode : Object 的 native方法 , 獲取對象的哈希值,用于確定該對象在哈希表中的索引位置,它實際上是一個int型整數(shù)
二、關(guān)系操作符 ==
1、操作數(shù)的值
基本數(shù)據(jù)類型變量
在Java中有八種基本數(shù)據(jù)類型:
浮點型:float(4 byte), double(8 byte)
整型:byte(1 byte), short(2 byte), int(4 byte) , long(8 byte)
字符型: char(2 byte)
布爾型: boolean(JVM規(guī)范沒有明確規(guī)定其所占的空間大小,僅規(guī)定其只能夠取字面值”true”和”false”)
對于這八種基本數(shù)據(jù)類型的變量,變量直接存儲的是“值”。因此,在使用關(guān)系操作符 == 來進行比較時,比較的就是“值”本身。要注意的是,浮點型和整型都是有符號類型的(最高位僅用于表示正負,不參與計算【以 byte 為例,其范圍為 -2^7 ~ 2^7 - 1,-0即-128】),而char是無符號類型的(所有位均參與計算,所以char類型取值范圍為0~2^16-1)。
- 引用類型變量
在Java中,引用類型的變量存儲的并不是“值”本身,而是與其關(guān)聯(lián)的對象在內(nèi)存中的地址。比如下面這行代碼,
String str1;
這句話聲明了一個引用類型的變量,此時它并沒有和任何對象關(guān)聯(lián)。
而通過 new 來產(chǎn)生一個對象,并將這個對象和str1進行綁定:
str1= new String("hello");
那么 str1 就指向了這個對象,此時引用變量str1中存儲的是它指向的對象在內(nèi)存中的存儲地址,并不是“值”本身,也就是說并不是直接存儲的字符串”hello”。這里面的引用和 C/C++ 中的指針很類似。
2、小結(jié)
因此,對于關(guān)系操作符 ==:
- 若操作數(shù)的類型是基本數(shù)據(jù)類型,則該關(guān)系操作符判斷的是左右兩邊操作數(shù)的值是否相等
- 若操作數(shù)的類型是引用數(shù)據(jù)類型,則該關(guān)系操作符判斷的是左右兩邊操作數(shù)的內(nèi)存地址是否相同。也就是說,若此時返回true,則該操作符作用的一定是同一個對象。
三、equals方法
1、來源
equals方法是基類Object中的實例方法,因此對所有繼承于Object的類都會有該方法。
在 Object 中的聲明:
public boolean equals(Object obj) {}
2、equals方法的作用
初衷 : 判斷兩個對象的 content 是否相同
為了更直觀地理解equals方法的作用,我們先看Object類中equals方法的實現(xiàn)。
public boolean equals(Object obj) {
return (this == obj);
}
很顯然,在Object類中,equals方法是用來比較兩個對象的引用是否相等,即是否指向同一個對象。
但我們都知道,下面代碼輸出為 true:
public class Main {
public static void main(String[] args) {
String str1 = new String("hello");
String str2 = new String("hello");
System.out.println(str1.equals(str2));
}
}
原來是 String 類重寫了 equals 方法:
public boolean equals(Object anObject) { // 方法簽名與 Object類 中的一致
if (this == anObject) { // 先判斷引用是否相同(是否為同一對象),
return true;
}
if (anObject instanceof String) { // 再判斷類型是否一致,
// 最后判斷內(nèi)容是否一致.
String anotherString = (String)anObject;
int n = count;
if (n == anotherString.count) {
char v1[] = value;
char v2[] = anotherString.value;
int i = offset;
int j = anotherString.offset;
while (n-- != 0) {
if (v1[i++] != v2[j++])
return false;
}
return true;
}
}
return false;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
即對于諸如“字符串比較時用的什么方法,內(nèi)部實現(xiàn)如何?”之類問題的回答即為:
使用equals方法,內(nèi)部實現(xiàn)分為三個步驟:
- 先 比較引用是否相同(是否為同一對象),
- 再 判斷類型是否一致(是否為同一類型),
- 最后 比較內(nèi)容是否一致
Java 中所有內(nèi)置的類的 equals 方法的實現(xiàn)步驟均是如此,特別是諸如 Integer,Double 等包裝器類。
3、equals 重寫原則
對象內(nèi)容的比較才是設(shè)計equals()的真正目的,Java語言對equals()的要求如下,這些要求是重寫該方法時必須遵循的:
對稱性: 如果x.equals(y)返回是“true”,那么y.equals(x)也應(yīng)該返回是“true” ;
自反性: x.equals(x)必須返回是“true” ;
類推性: 如果x.equals(y)返回是“true”,而且y.equals(z)返回是“true”,那么z.equals(x)也應(yīng)該返回是“true” ;
一致性: 如果x.equals(y)返回是“true”,只要x和y內(nèi)容一直不變,不管你重復(fù)x.equals(y)多少次,返回都是“true” ;
對稱性: 如果x.equals(y)返回是“true”,那么y.equals(x)也應(yīng)該返回是“true”。
任何情況下,x.equals(null)【應(yīng)使用關(guān)系比較符 ==】,永遠返回是“false”;x.equals(和x不同類型的對象)永遠返回是“false”
4、小結(jié)
因此,對于 equals 方法:
- 其本意 是 比較兩個對象的 content 是否相同
- 必要的時候,我們需要重寫該方法,避免違背本意,且要遵循上述原則
四、hashCode 方法
1、hashCode 的來源
hashCode 方法是基類Object中的 實例native方法,因此對所有繼承于Object的類都會有該方法。
在 Object類 中的聲明(native方法暗示這些方法是有實現(xiàn)體的,但并不提供實現(xiàn)體,因為其實現(xiàn)體是由非java語言在外面實現(xiàn)的):
public native int hashCode();
2、哈希相關(guān)概念
我們首先來了解一下哈希表:
概念 : Hash 就是把任意長度的輸入(又叫做預(yù)映射, pre-image),通過散列算法,變換成固定長度的輸出(int),該輸出就是散列值。這種轉(zhuǎn)換是一種 壓縮映射,也就是說,散列值的空間通常遠小于輸入的空間。不同的輸入可能會散列成相同的輸出,從而不可能從散列值來唯一的確定輸入值。簡單的說,就是一種將任意長度的消息壓縮到某一固定長度的消息摘要的函數(shù)。
應(yīng)用–數(shù)據(jù)結(jié)構(gòu) : 數(shù)組的特點是:尋址容易,插入和刪除困難; 而鏈表的特點是:尋址困難,插入和刪除容易。那么我們能不能綜合兩者的特性,做出一種尋址容易,插入和刪除也容易的數(shù)據(jù)結(jié)構(gòu)?答案是肯定的,這就是我們要提起的哈希表,哈希表有多種不同的實現(xiàn)方法,我接下來解釋的是最常用的一種方法——拉鏈法,我們可以理解為 “鏈表的數(shù)組”,如圖:
圖1 哈希表示例
左邊很明顯是個數(shù)組,數(shù)組的每個成員是一個鏈表。該數(shù)據(jù)結(jié)構(gòu)所容納的所有元素均包含一個指針,用于元素間的鏈接。我們根據(jù)元素的自身特征把元素分配到不同的鏈表中去,也是根據(jù)這些特征,找到正確的鏈表,再從鏈表中找出這個元素。其中,將根據(jù)元素特征計算元素數(shù)組下標(biāo)的方法就是散列法。
拉鏈法的適用范圍 : 快速查找,刪除的基本數(shù)據(jù)結(jié)構(gòu),通常需要總數(shù)據(jù)量可以放入內(nèi)存。
- 要點 :
hash函數(shù)選擇,針對字符串,整數(shù),排列,具體相應(yīng)的hash方法;
碰撞處理,一種是open hashing,也稱為拉鏈法,另一種就是closed hashing,也稱開地址法,opened addressing。
3、hashCode 簡述
在 Java 中,由 Object 類定義的 hashCode 方法會針對不同的對象返回不同的整數(shù)。(這是通過將該對象的內(nèi)部地址轉(zhuǎn)換成一個整數(shù)來實現(xiàn)的,但是 JavaTM 編程語言不需要這種實現(xiàn)技巧)。
hashCode 的常規(guī)協(xié)定是:
在 Java 應(yīng)用程序執(zhí)行期間,在對同一對象多次調(diào)用 hashCode 方法時,必須一致地返回相同的整數(shù),前提是將對象進行 equals 比較時所用的信息沒有被修改。從某一應(yīng)用程序的一次執(zhí)行到同一應(yīng)用程序的另一次執(zhí)行,該整數(shù)無需保持一致。
如果根據(jù) equals(Object) 方法,兩個對象是相等的,那么對這兩個對象中的每個對象調(diào)用 hashCode 方法都必須生成相同的整數(shù)結(jié)果。
如果根據(jù) equals(java.lang.Object) 方法,兩個對象不相等,那么對這兩個對象中的任一對象上調(diào)用 hashCode 方法 不要求 一定生成不同的整數(shù)結(jié)果。但是,程序員應(yīng)該意識到,為不相等的對象生成不同整數(shù)結(jié)果可以提高哈希表的性能。
要想進一步了解 hashCode 的作用,我們必須先要了解Java中的容器,因為 HashCode 只是在需要用到哈希算法的數(shù)據(jù)結(jié)構(gòu)中才有用,比如 HashSet, HashMap 和 Hashtable。
Java中的集合(Collection)有三類,一類是List,一類是Queue,再有一類就是Set。 前兩個集合內(nèi)的元素是有序的,元素可以重復(fù);最后一個集合內(nèi)的元素?zé)o序,但元素不可重復(fù)。
那么, 這里就有一個比較嚴重的問題:要想保證元素不重復(fù),可兩個元素是否重復(fù)應(yīng)該依據(jù)什么來判斷呢? 這就是 Object.equals 方法了。但是,如果每增加一個元素就檢查一次,那么當(dāng)元素很多時,后添加到集合中的元素比較的次數(shù)就非常多了。 也就是說,如果集合中現(xiàn)在已經(jīng)有1000個元素,那么第1001個元素加入集合時,它就要調(diào)用1000次equals方法。這顯然會大大降低效率。于是,Java采用了哈希表的原理。 這樣,我們對每個要存入集合的元素使用哈希算法算出一個值,然后根據(jù)該值計算出元素應(yīng)該在數(shù)組的位置。所以,當(dāng)集合要添加新的元素時,可分為兩個步驟:
先調(diào)用這個元素的 hashCode 方法,然后根據(jù)所得到的值計算出元素應(yīng)該在數(shù)組的位置。如果這個位置上沒有元素,那么直接將它存儲在這個位置上;
如果這個位置上已經(jīng)有元素了,那么調(diào)用它的equals方法與新元素進行比較:相同的話就不存了,否則,將其存在這個位置對應(yīng)的鏈表中(Java 中 HashSet, HashMap 和 Hashtable的實現(xiàn)總將元素放到鏈表的表頭)。
4、equals 與 hashCode
前提: 談到hashCode就不得不說equals方法,二者均是Object類里的方法。由于Object類是所有類的基類,所以一切類里都可以重寫這兩個方法。
- 原則 1 : 如果 x.equals(y) 返回 “true”,那么 x 和 y 的 hashCode() 必須相等 ;
- 原則 2 : 如果 x.equals(y) 返回 “false”,那么 x 和 y 的 hashCode() 有可能相等,也有可能不等 ;
- 原則 3 : 如果 x 和 y 的 hashCode() 不相等,那么 x.equals(y) 一定返回 “false” ;
- 原則 4 : 一般來講,equals 這個方法是給用戶調(diào)用的,而 hashcode 方法一般用戶不會去調(diào)用 ;
- 原則 5 : 當(dāng)一個對象類型作為集合對象的元素時,那么這個對象應(yīng)該擁有自己的equals()和hashCode()設(shè)計,而且要遵守前面所說的幾個原則。
5、實現(xiàn)例證
hashCode()在object類中定義如下:
public native int hashCode();
說明是一個本地方法,它的實現(xiàn)是根據(jù)本地機器相關(guān)的。
String 類是這樣重寫它的:
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence
{
/** The value is used for character storage. */
private final char value[]; //成員變量1
/** The offset is the first index of the storage that is used. */
private final int offset; //成員變量2
/** The count is the number of characters in the String. */
private final int count; //成員變量3
/** Cache the hash code for the string */
private int hash; // Default to 0 //非成員變量
public int hashCode() {
int h = hash;
int len = count; //用到成員變量3
if (h == 0 && len > 0) {
int off = offset; //用到成員變量2
char val[] = value; //用到成員變量1
for (int i = 0; i < len; i++) {
h = 31*h + val[off++]; //遞推公式
}
hash = h;
}
return h;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
對程序的解釋:h = s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1] ,由此可以看出,對象的hash地址不一定是實際的內(nèi)存地址。
五、小結(jié)
- hashcode是系統(tǒng)用來快速檢索對象而使用
- equals方法本意是用來判斷引用的對象是否一致
- 重寫equals方法和hashcode方法時,equals方法中用到的成員變量也必定會在hashcode方法中用到,只不過前者作為比較項,后者作為生成摘要的信息項,本質(zhì)上所用到的數(shù)據(jù)是一樣的,從而保證二者的一致性
|