1. 程式人生 > >Java 中的 ==, equals 與 hashCode 的區別與聯絡

Java 中的 ==, equals 與 hashCode 的區別與聯絡

一、概述

1、概念

== : 該操作符生成的是一個boolean結果,它計算的是運算元的值之間的關係 equals  : Object 的 例項方法,比較兩個物件的content是否相同 hashCode : Object 的 native方法 , 獲取物件的雜湊值,用於確定該物件在雜湊表中的索引位置,它實際上是一個int型整數

二、關係操作符 ==

1、運算元的值

基本資料型別變數

在Java中有八種基本資料型別:

  浮點型:float(4 byte), double(8 byte)

  整型:byte(1 byte), short(2 byte), int(4 byte) , long(8 byte)

  字元型: char(2 byte)

  布林型: boolean(JVM規範沒有明確規定其所佔的空間大小,僅規定其只能夠取字面值”true”和”false”)

  對於這八種基本資料型別的變數,變數直接儲存的是“值”。因此,在使用關係操作符 == 來進行比較時,比較的就是“值”本身。要注意的是,浮點型和整型都是有符號型別的(最高位僅用於表示正負,不參與計算【以 byte 為例,其範圍為 -2^7 ~ 2^7 - 1,-0即-128】),而char是無符號型別的(所有位均參與計算,所以char型別取值範圍為0~2^16-1)。  

引用型別變數  在Java中,引用型別的變數儲存的並不是“值”本身,而是與其關聯的物件在記憶體中的地址。比如下面這行程式碼,

    String str1;1

  這句話聲明瞭一個引用型別的變數,此時它並沒有和任何物件關聯。    而通過 new 來產生一個物件,並將這個物件和str1進行繫結:

str1= new String("hello");1

  那麼 str1 就指向了這個物件,此時引用變數str1中儲存的是它指向的物件在記憶體中的儲存地址,並不是“值”本身,也就是說並不是直接儲存的字串”hello”。這裡面的引用和 C/C++ 中的指標很類似。

2、小結

 因此,對於關係操作符 ==:

若運算元的型別是基本資料型別,則該關係操作符判斷的是左右兩邊運算元的值是否相等 若運算元的型別是引用資料型別,則該關係操作符判斷的是左右兩邊運算元的記憶體地址是否相同。也就是說,若此時返回true,則該操作符作用的一定是同一個物件。

三、equals方法

1、來源    equals方法是基類Object中的例項方法,因此對所有繼承於Object的類都會有該方法。        在 Object 中的宣告:

    public boolean equals(Object obj) {}1

2、equals方法的作用   初衷 : 判斷兩個物件的 content 是否相同

 為了更直觀地理解equals方法的作用,我們先看Object類中equals方法的實現。

  public boolean equals(Object obj) {     return (this == obj);   }123

  很顯然,在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));     } }12345678

原來是 String 類重寫了 equals 方法:

public boolean equals(Object anObject) {   // 方法簽名與 Object類 中的一致     if (this == anObject) {     // 先判斷引用是否相同(是否為同一物件),         return true;     }     if (anObject instanceof String) {   // 再判斷型別是否一致,         // 最後判斷內容是否一致.         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; }12345678910111213141516171819202122

即對於諸如“字串比較時用的什麼方法,內部實現如何?”之類問題的回答即為:

使用equals方法,內部實現分為三個步驟:

先 比較引用是否相同(是否為同一物件), 再 判斷型別是否一致(是否為同一型別), 最後 比較內容是否一致

Java 中所有內建的類的 equals 方法的實現步驟均是如此,特別是諸如 Integer,Double 等包裝器類。

3、equals 重寫原則

物件內容的比較才是設計equals()的真正目的,Java語言對equals()的要求如下,這些要求是重寫該方法時必須遵循的:

對稱性: 如果x.equals(y)返回是“true”,那麼y.equals(x)也應該返回是“true” ; 自反性: x.equals(x)必須返回是“true” ; 類推性: 如果x.equals(y)返回是“true”,而且y.equals(z)返回是“true”,那麼z.equals(x)也應該返回是“true” ; 一致性: 如果x.equals(y)返回是“true”,只要x和y內容一直不變,不管你重複x.equals(y)多少次,返回都是“true” ; 對稱性: 如果x.equals(y)返回是“true”,那麼y.equals(x)也應該返回是“true”。 任何情況下,x.equals(null)【應使用關係比較符 ==】,永遠返回是“false”;x.equals(和x不同型別的物件)永遠返回是“false”

4、小結   因此,對於 equals 方法:

其本意 是 比較兩個物件的 content 是否相同 必要的時候,我們需要重寫該方法,避免違背本意,且要遵循上述原則

四、hashCode 方法

1、hashCode 的來源    hashCode 方法是基類Object中的 例項native方法,因此對所有繼承於Object的類都會有該方法。        在 Object類 中的宣告(native方法暗示這些方法是有實現體的,但並不提供實現體,因為其實現體是由非java語言在外面實現的):

     public native int hashCode();1

2、雜湊相關概念   我們首先來了解一下雜湊表:

概念 : Hash 就是把任意長度的輸入(又叫做預對映, pre-image),通過雜湊演算法,變換成固定長度的輸出(int),該輸出就是雜湊值。這種轉換是一種  壓縮對映,也就是說,雜湊值的空間通常遠小於輸入的空間。不同的輸入可能會雜湊成相同的輸出,從而不可能從雜湊值來唯一的確定輸入值。簡單的說,就是一種將任意長度的訊息壓縮到某一固定長度的訊息摘要的函式。 應用–資料結構 : 陣列的特點是:定址容易,插入和刪除困難; 而連結串列的特點是:定址困難,插入和刪除容易。那麼我們能不能綜合兩者的特性,做出一種定址容易,插入和刪除也容易的資料結構?答案是肯定的,這就是我們要提起的雜湊表,雜湊表有多種不同的實現方法,我接下來解釋的是最常用的一種方法——拉鍊法,我們可以理解為 “連結串列的陣列”,如圖:

                                      圖1 雜湊表示例

 左邊很明顯是個陣列,陣列的每個成員是一個連結串列。該資料結構所容納的所有元素均包含一個指標,用於元素間的連結。我們根據元素的自身特徵把元素分配到不同的連結串列中去,也是根據這些特徵,找到正確的連結串列,再從連結串列中找出這個元素。其中,將根據元素特徵計算元素陣列下標的方法就是雜湊法。 拉鍊法的適用範圍 : 快速查詢,刪除的基本資料結構,通常需要總資料量可以放入記憶體。 要點 :   hash函式選擇,針對字串,整數,排列,具體相應的hash方法;  碰撞處理,一種是open hashing,也稱為拉鍊法,另一種就是closed hashing,也稱開地址法,opened addressing。

3、hashCode 簡述   在 Java 中,由 Object 類定義的 hashCode 方法會針對不同的物件返回不同的整數。(這是通過將該物件的內部地址轉換成一個整數來實現的,但是 JavaTM 程式語言不需要這種實現技巧)。

 hashCode 的常規協定是: 

在 Java 應用程式執行期間,在對同一物件多次呼叫 hashCode 方法時,必須一致地返回相同的整數,前提是將物件進行 equals 比較時所用的資訊沒有被修改。從某一應用程式的一次執行到同一應用程式的另一次執行,該整數無需保持一致。  如果根據 equals(Object) 方法,兩個物件是相等的,那麼對這兩個物件中的每個物件呼叫 hashCode 方法都必須生成相同的整數結果。  如果根據 equals(java.lang.Object) 方法,兩個物件不相等,那麼對這兩個物件中的任一物件上呼叫 hashCode 方法 不要求 一定生成不同的整數結果。但是,程式設計師應該意識到,為不相等的物件生成不同整數結果可以提高雜湊表的效能。        要想進一步瞭解 hashCode 的作用,我們必須先要了解Java中的容器,因為 HashCode 只是在需要用到雜湊演算法的資料結構中才有用,比如 HashSet, HashMap 和 Hashtable。

  Java中的集合(Collection)有三類,一類是List,一類是Queue,再有一類就是Set。 前兩個集合內的元素是有序的,元素可以重複;最後一個集合內的元素無序,但元素不可重複。

  那麼, 這裡就有一個比較嚴重的問題:要想保證元素不重複,可兩個元素是否重複應該依據什麼來判斷呢? 這就是 Object.equals  方法了。但是,如果每增加一個元素就檢查一次,那麼當元素很多時,後新增到集合中的元素比較的次數就非常多了。 也就是說,如果集合中現在已經有1000個元素,那麼第1001個元素加入集合時,它就要呼叫1000次equals方法。這顯然會大大降低效率。於是,Java採用了雜湊表的原理。 這樣,我們對每個要存入集合的元素使用雜湊演算法算出一個值,然後根據該值計算出元素應該在陣列的位置。所以,當集合要新增新的元素時,可分為兩個步驟:        先呼叫這個元素的 hashCode 方法,然後根據所得到的值計算出元素應該在陣列的位置。如果這個位置上沒有元素,那麼直接將它儲存在這個位置上; 如果這個位置上已經有元素了,那麼呼叫它的equals方法與新元素進行比較:相同的話就不存了,否則,將其存在這個位置對應的連結串列中(Java 中 HashSet, HashMap 和 Hashtable的實現總將元素放到連結串列的表頭)。

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 這個方法是給使用者呼叫的,而 hashcode 方法一般使用者不會去呼叫 ; 原則 5 : 當一個物件型別作為集合物件的元素時,那麼這個物件應該擁有自己的equals()和hashCode()設計,而且要遵守前面所說的幾個原則。

5、實現例證

 hashCode()在object類中定義如下:

public native int hashCode();1

 說明是一個本地方法,它的實現是根據本地機器相關的。

 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;     } }1234567891011121314151617181920212223242526272829

對程式的解釋:h =  s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1],由此可以看出,物件的hash地址不一定是實際的記憶體地址。

五、小結

hashcode是系統用來快速檢索物件而使用 equals方法本意是用來判斷引用的物件是否一致 重寫equals方法和hashcode方法時,equals方法中用到的成員變數也必定會在hashcode方法中用到,只不過前者作為比較項,後者作為生成摘要的資訊項,本質上所用到的資料是一樣的,從而保證二者的一致性