聊聊Java中的 " == "、equals以及hashCode

關于 “ == ”

“ == ”操作符主要比較的是操作符兩端對象的內存地址。如果兩個對象的內存地址是一致的,那么就返回true。反之,則返回false。

話不多說,先來看看下面這段代碼:

程序1-1

再來看看會輸出什么:

上面程序的輸出結果

怎么樣,這個結果你想到了嗎?

以下是關于上面結果的解釋:

其實在Java中對于字符串的創建,有兩種形式。

  • 一種是形如String a = "Hello"這樣的字面量形式;
  • 另一種是形如String d = new String("Hello")這樣的構造對象的方法。
字面量形式

在JVM中,為了減少對字符串變量的重復創建,其擁有一段特殊的內存空間,這段內存被稱為字符串常量池(constant pool)或者是“字符串字面量池”。

程序1-1中,我們首先使用 String a = "Hello" 創建了一個對象。此時默認字符串常量池中沒有內容為“Hello”的對象存在。當JVM在字符串常量池中沒有找到內容為"Hello"的對象時,就會創建一個內容為"Hello"的對象,并將它的引用返回給變量a。

那么當我們在后面使用 String b = "Hello" 的時候,會發生什么情況呢?
首先JVM會在字符串常量池中查詢是否有內容為“Hello”的對象。因為此時字符串常量池中已經有內容為“Hello”的對象了,故JVM不會創建新的地址空間,而是將原有的“Hello”對象的引用返回給b------所以此時變量a和變量b擁有的是同一個對象引用,即指向的是同一塊內存地址,因此使用 == 操作符對a和b進行比較,結果為true。

使用new來構造一個對象

然而當我們new一個字符串對象時,不管字符串常量池中是否有內容相同的對象,JVM都會創造一個新的對象,所以地址肯定不一樣,因此使用 == 操作符對a和d進行比較,結果為false。

關于equals

在Object類中,equals方法源碼如下:

Object類中的equals方法

我們可以看到,在Object類中的equals方法其實在內部也是使用了“ == ”操作符-----如果兩個對象地址一樣則返回true,反之則返回false。
既然equals方法里面也是使用“ == ”,那為什么還要設立一個equals方法,而不是直接用“ == ”操作符呢?

別急,這只是Object類里面的equals方法,其實絕大部分Object的子類都對equals方法進行了改寫:

String類中的equals方法
HashMap中的equals方法

除了會比較內存地址,以上兩個類中的equals方法還會比較對象所包含的內容。如果兩個對象所包含的內容相同,equals方法也是返回true。

官方文檔中對equals方法的描述:

equals方法在非空對象引用上實現相等關系:

  • 自反性:對于任何非空引用值x,x.equals(x)都應返回true。
  • 對稱性:對于任何非空引用值x和y,當且僅當y.equals(x)返回true時,x.equals(y)才應返回true。
  • 傳遞性:對于任何非空引用值x、y和z,如果x.equals(y)返回true,并且y.equals(z)返回true,那么x.equals(z)應返回true。
  • 一致性:對于任何非空引用值x和y,多次調用x.equals(y)始終返回true
    或始終返回false,前提是對象上equals比較中所用的信息沒有被修改。對于任何非空引用值x,x.equals(null)都應返回false。
    Object類的equals方法實現對象上差別可能性最大的相等關系;即,對于任何非空引用值x和y,當且僅當x和y引用同一個對象時,此方法才返回true(x == y具有值true)。
    注意:當此方法被重寫時,通常有必要重寫hashCode方法,以維護hashCode方法的常規協定,該協定聲明相等對象必須具有相等的哈希碼。

其實在很多時候,我們都需要改寫equals方法以適應我們的實際情況:

下面是一個Person類,包含name和age兩個屬性。

Person類

現在我們創建兩個Person對象,但是name和age分別都設置一樣的值,同時使用equals方法對這兩個對象進行比較:

創建兩個Person對象并用equals方法對其進行比較
比較之后的結果

雖然這從JVM的角度來看這個程序是對的,可是結果并不是我們想要的:對象one和對象two雖然是兩個不同的對象,但是它們包含的元素的值是相同的,也就是說one和two應該都表示的是同一個人-----name 為 EakonZhao,年齡為19??墒菫槭裁凑{用equals方法之后輸出的卻是false呢?

原因:由于我們沒有對equals方法進行改寫,所以當我們在調用equals方法的時候實際上調用的是Object類的equals方法。從前面我們可以得知,Object的equals方法在內部是直接使用“ == ”操作符對對象進行比較,這樣當然會返回false啦!

下面我將對equals方法進行改寫,以滿足我們的需求;

改寫之后的equals方法
改寫equals方法之后的輸出結果

對于改寫equals方法之后是否需要改寫hashCode方法以維護hashCode方法的常規協定,我在介紹完hashCode方法之后會繼續講

關于在使用"=="操作符和equals方法時發生的自動裝箱與自動拆箱現象

簡單介紹一下自動拆箱和自動裝箱

自動裝箱:將Java的基本類型轉換成包裝器類型;
自動拆箱:將Java的包裝器類型轉換成基本類型。

基本數據類型及其包裝器類型
自動裝箱與自動拆箱

如上圖所示,其實這是一個非常簡單的程序,但是實際上Java自動幫我們完成了拆裝箱的操作。

背景:
在JDK1.5之前,如果我們要生成一個數值為1的Integer對象,那么我們必須使用下面這行代碼:

創建一個數值為1的Integer對象

可是引入了自動裝箱功能之后,我們只需使用這樣一行代碼就能完成了:

自動裝箱

這是如何實現的呢?下面我們將上面那段代碼的class文件反編譯一下:

反編譯之后的代碼

我們可以看到,其實Java是使用了 Integer的valueOf(int)方法來完成自動裝箱的,而在拆箱過程當中調用的是Integer的intValue方法。對于其他的包裝器類型來說,其實這兩個過程也是類似的。

裝箱過程是通過調用包裝器類型的valueOf方法實現的,拆箱過程是通過調用包裝器類型的xxValue方法實現的(其中xx代表的是對應的基本類型)。

讓我們再來看看下面這段代碼:

Integer緩存示例

為什么上面兩條打印代碼會輸出不同的結果?
原因也很簡單:與字符串常量池類似,這其實也是JVM節省內存的一個方法-----對于Integer類型的對象來說,如果我們要創建的Integer對象的數值在 [-128,127]的區間之內,那么JVM就會在緩存中查找,看看有沒有已經存放在緩存中的數值一樣的Integer對象。如果有,就返回已經存在的對象的引用。

關于使用equals方法時發生的自動拆裝箱現象就不贅述了,其實很容易理解。

小結: “==”運算符其實比較的是地址相不相同,而equals方法比較的是值相不相同。

關于hashCode

官方文檔中隊hashCode方法的描述:

public int hashCode()
返回該對象的哈希碼值。支持此方法是為了提高哈希表(例如java.util.Hashtable
提供的哈希表)的性能。
hashCode的常規協定是:

  • 在 Java 應用程序執行期間,在對同一對象多次調用hashCode方法時,必須一致地返回相同的整數,前提是將對象進行equals比較時所用的信息沒有被修改。從某一應用程序的一次執行到同一應用程序的另一次執行,該整數無需保持一致。
  • 如果根據equals(Object)方法,兩個對象是相等的,那么對這兩個對象中的每個對象調用hashCode方法都必須生成相同的整數結果。
  • 如果根據equals(java.lang.Object)方法,兩個對象不相等,那么對這兩個對象中的任一對象上調用hashCode方法要求一定生成不同的整數結果。但是,程序員應該意識到,為不相等的對象生成不同整數結果可以提高哈希表的性能。

實際上,由Object類定義的 hashCode 方法確實會針對不同的對象返回不同的整數。(這一般是通過將該對象的內部地址轉換成一個整數來實現的,但是 JavaTM
編程語言不需要這種實現技巧。)

上面這段話,簡單的來說就是以下幾點:

  • hashCode存在的意義主要是提供查找的快捷性,比如說在Hashtable、HashMap中等。hashCode是用來在散列存儲結構中確定對象存儲的位置的;
  • 如果兩個對象相同,即調用equals方法返回的是true,那么它倆的hashCode值也要相同;
  • 如果equals方法被改寫了,那么hashCode方法也盡量要改寫,并且產生hashCode的對象也要和equals的對象保持一致;
  • 兩個對象的hashCode相同并不代表兩個對象就一定相同,也就是不一定適用于equals(java.lang.Object)方法,只能夠說明這兩個對象在散列存儲對象中,如Hashtable中,是存放在同一個籃子里的。(關于Hashtable的介紹我在后面會開博客探究其源碼)

下面這段話是從別人那里轉過來的,我覺得能幫助理解hashCode:

  • hashcode是用來查找的,如果你學過數據結構就應該知道,在查找和排序這一章有
    例如內存中有這樣的位置
    01234567
    而我有個類,這個類有個字段叫ID,我要把這個類存放在以上8個位置之一,如果不用hashcode而任意存放,那么當查找時就需要到這八個位置里挨個去找,或者用二分法一類的算法。
    但如果用hashcode那就會使效率提高很多。
    我們這個類中有個字段叫ID,那么我們就定義我們的hashcode為ID%8,然后把我們的類存放在取得得余數那個位置。比如我們的ID為9,9除8的余數為1,那么我們就把該類存在1這個位置,如果ID是13,求得的余數是5,那么我們就把該類放在5這個位置。這樣,以后在查找該類時就可以通過ID除8求余數直接找到存放的位置了。
  • 但是如果兩個類有相同的hashcode怎么辦呢?(我們假設上面的類的ID不是唯一的),例如9除以8和17除以8的余數都是1,那么這是不是合法的,回答是:可以這樣。那么如何判斷呢?在這個時候就需要定義equals了。
    也就是說,我們先通過hashcode來判斷兩個類是否存放某個桶里,但這個桶里可能有很多類,那么我們就需要再通過equals來在這個桶里找到我們要的類。
    那么。重寫了equals(),為什么還要重寫hashCode()呢?
    想想,你要在一個桶里找東西,你必須先要找到這個桶啊,你不通過重寫hashcode()來找到桶,光重寫equals()有什么用啊

下面是代碼示例:
我新建了一個HashExample類,里面定義了一個屬性為id,并改寫了hashCode方法:

HashExample類

現在我new兩個對象,這兩個對象的id我都賦予相同的值,并將它們兩個存入一個Set(Set中的元素是不重復的)當中,然后分別輸出它們兩個的hashCode以及使用equals方法比較的結果以及將這個Set也輸出:

改寫equals方法之前

以上這個示例,我們只是重寫了hashCode方法,從上面的結果可以看出,雖然兩個對象的hashCode相等,但是實際上兩個對象并不是相等;,我們沒有重寫equals方法,那么就會調用object默認的equals方法,是比較兩個對象的引用是不是相同,顯示這是兩個不同的對象,兩個對象的引用肯定是不定的。這里我們將生成的對象放到了hashSet中,而hashSet中只能夠存放唯一的對象,也就是相同的(適用于equals方法)的對象只會存放一個,但是這里實際上是兩個對象a,b都被放到了HashSet中,這樣hashSet就失去了他本身的意義了。

現在把equals也改寫一下:

改寫之后的equals方法
改寫equals方法之后的輸出結果

現在我們可以看到,這兩個對象已經完全相等了,并且hashSet中也只存放了一份對象。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容