【軟件構造】第三章第五節 ADT和OOP中的等價性

第三章第五節 ADT和OOP中的等價性

在不少場景下,須要斷定兩個對象是否 「相等」,例如:判斷某個Collection 中是否包含特定元素。 
==和equals()有和區別?如何爲自定義 ADT正確實現equals()?html

OutLine

  • 等價性equals() 和 ==
  • equals()的判斷方法
    • 自反、傳遞、對稱性
  • hashCode()
  • 不可變類型的等價性
  • 可變類型的等價性
    • 觀察等價性
    • 行爲等價性

Notes

##  等價性equals() 和 ==

  • 和不少其餘語言同樣,Java有兩種判斷相等的操做—— == 和 equals() 。
  • ==是引用等價性 ;而equals()是對象等價性。 
    • == 比較的是索引。更準確的說,它測試的是指向相等(referential equality)。若是兩個索引指向同一塊存儲區域,那它們就是==的。對於咱們以前提到過的快照圖來講,==就意味着它們的箭頭指向同一個對象。
    • equals()操做比較的是對象的內容,換句話說,它測試的是對象值相等(object equality)。e在每個ADT中,quals操做必須合理定義。

Java中的數據類型,可分爲兩類: java

  • 基本數據類型,也稱原始數據類型。byte,short,char,int,long,float,double,boolean 
    • 他們之間的比較,應用雙等號(==),比較的是他們的值。 
  • 複合數據類型(類) 
    • 當他們用(==)進行比較的時候,比較的是他們在內存中的存放地址,因此,除非是同一個new出來的對象,他們的比較後的結果爲true,不然比較後結果爲false。
    • JAVA當中全部的類都是繼承於Object這個基類的,在Object中的基類中定義了一個equals的方法,這個方法的初始行爲是比較對象的內存地址,但在一些類庫當中這個方法被覆蓋掉了,如String,Integer,Date在這些類當中equals有其自身的實現,而再也不是比較類在堆內存中的存放地址了。 
    • 對於複合數據類型之間進行equals比較,在沒有覆寫equals方法的狀況下,他們之間的比較仍是基於他們在內存中的存放位置的地址值的,由於Object的equals方法也是用雙等號(==)進行比較的,因此比較後的結果跟雙等號(==)的結果相同。

 關於equals()與== 歡迎閱讀 海子的博客程序員

## equals()的判斷方法

嚴格來講,咱們能夠從三個角度定義相等:ide

  • 抽象函數:回憶一下抽象函數(AF: R → A ),它將具體的表示數據映射到了抽象的值。若是AF(a)=AF(b),咱們就說a和b相等。
  • 等價關係:等價是指對於關係E ⊆ T x T ,它知足:
    • 自反性: x.equals(x)必須返回true
    • 對稱性: x.equals(y)與y.equals(x)的返回值必須相等。
    • 傳遞性: x.equals(y)爲true,y.equals(z)也爲true,那麼x.equals(z)必須爲true。

以上兩種角度/定義其實是同樣的,經過等價關係咱們能夠構建一個抽象函數(譯者注:就是一個封閉的二元關係運算);而抽象函數也能推出一個等價關係。函數

  • 從使用者/外部的角度去觀察:咱們說兩個對象相等,當且僅當使用者沒法觀察到它們之間有不一樣,即每個觀察總會都會獲得相同的結果。例如對於兩個集合對象 {1,2} 和 {2,1},咱們就沒法觀察到不一樣:
    • |{1,2}| = 2, |{2,1}| = 2
    • 1 ∈ {1,2} is true, 1 ∈ {2,1} is true
    • 2 ∈ {1,2} is true, 2 ∈ {2,1} is true
    • 3 ∈ {1,2} is false, 3 ∈ {2,1} is false

 

## hashCode()方法

  • 對於不可變類型:
    • equals() 應該比較抽象值是否相等。這和 equals() 比較行爲相等性是同樣的。
    • hashCode() 應該將抽象值映射爲整數。
    • 因此不可變類型應該同時覆蓋 equals() 和 hashCode().
  • 對於可變類型:
    • equals() 應該比較索引,就像 ==同樣。一樣的,這也是比較行爲相等性。
    • hashCode() 應該將索引映射爲整數。
    • 因此可變類型不該該將 equals() 和 hashCode() 覆蓋,而是直接繼承 Object中的方法。Java沒有爲大多數聚合類遵照這一規定,這也許會致使上面看到的隱祕bug。
  • equals與hashCode兩個方法均屬於Object對象,equals根據咱們的須要重寫, 用來判斷是不是同一個內容或同一個對象,具體是判斷什麼,怎麼判斷得看怎麼重寫,默認的equals是比較地址。
  • hashCode方法返回一個int的哈希碼, 一樣能夠重寫來自定義獲取哈希碼的方法。
  • equals斷定爲相同, hashCode必定相同。equals斷定爲不一樣,hashCode不必定不一樣。
  • hashCode必須爲兩個被該equals方法視爲相等的對象產生相同的結果。
  • 與equals()方法相似,hashCode()方法能夠被重寫。JDK中對hashCode()方法的做用,以及實現時的注意事項作了說明:
    • hashCode()在哈希表中起做用,如java.util.HashMap。
    • 若是對象在equals()中使用的信息都沒有改變,那麼hashCode()值始終不變。
    • 若是兩個對象使用equals()方法判斷爲相等,則hashCode()方法也應該相等。
    • 若是兩個對象使用equals()方法判斷爲不相等,則不要求hashCode()也必須不相等;可是開發人員應該認識到,不相等的對象產生不相同的hashCode能夠提升哈希表的性能。

 

## 不可變類型的等價性

首先來看Object中實現的缺省equals():性能

public class Object {
    ...
    public boolean equals(Object that) {
        return this == that;
    }
}

在Object中實現的缺省equals()是在判斷引用等價性。這一般不是程序員所指望的,所以須要重寫,下面是一個栗子:測試

public class Duration {
    ...   
    // Problematic definition of equals()
    public boolean equals(Duration that) {
        return this.getLength() == that.getLength();        
    }
}

嘗試以下客戶端代碼,可獲得this

Duration d1 = new Duration (1, 2);
Duration d2 = new Duration (1, 2);
Object o2 = d2;
d1.equals(d2) → true
d1.equals(o2) → false

基於以上結果進行如下解釋:spa

  • 即便d2o2最終參照相同的對象在內存中,對他們來講你仍然獲得不一樣的結果。 
  • 事實證實,該方法Duration已經超載equals(),由於方法簽名與Object’s 不相同。咱們實際上有兩種equals()方法:隱式equals(Object)繼承Object,和新的equals(Duration)
  • 若是咱們經過一個Object參考,那麼d1.equals(o2)咱們最終會調用equals(Object)實現。
  • 若是咱們經過Duration參考,如在d1.equals(d2),咱們最終調用equals(Duration)版本。
  • 即便發生這種狀況o2d2二者都會在運行時指向同一個對象!平等已經變得不一致。

 

咱們須要註釋 @Override ,重寫超類中的方法,所以,這裏實施正確的 equals() 方法:code

@Override
public boolean equals (Object thatObject) {
    if (!(thatObject instanceof Duration)) return false;
    Duration thatDuration = (Duration) thatObject;
    return this.getLength() == thatDuration.getLength();
}

再次執行客戶端代碼,可獲得:

Duration d1 = new Duration(1, 2);
Duration d2 = new Duration(1, 2);
Object o2 = d2;
d1.equals(d2) → true
d1.equals(o2) → true

 

## 可變類型的等價性

  回憶以前咱們對於相等的定義,即它們不能被使用者觀察出來不一樣。而對於可變對象來講,它們多了一種新的可能:經過在觀察前調用改造者,咱們能夠改變其內部的狀態,從而觀察出不一樣的結果。

  • 因此咱們從新定義兩種相等:
    • 觀察等價性:兩個索引在不改變各自對象狀態的前提下不能被區分。即經過只調用observer,producer和creator的方法,它測試的是這兩個索引在當前程序狀態下「看起來」相等。
    • 行爲等價性:兩個索引在任何代碼的狀況下都不能被區分,即便有一個對象調用了改造者。它測試的是兩個對象是否會在將來全部的狀態下「行爲」相等。
  • 對於不可變對象,觀察相等和行爲相等是徹底等價的,由於它們沒有改造者改變對象內部的狀態。
  • 對於可變對象,Java一般實現的是觀察相等。例如兩個不一樣的 List 對象包含相同的序列元素,那麼equals() 操做就會返回真。

在有些時候,觀察等價性可能致使bug,甚至可能破壞RI。

假設咱們作了一個List,而後把它放到Set

List<String> list = new ArrayList<>();
list.add("a");

Set<List<String>> set = new HashSet<List<String>>();
set.add(list);

咱們能夠檢查該集合是否包含咱們放入其中的列表,而且它會:

set.contains(list) → true

可是若是咱們修改這個存入的列表:

list.add("goodbye");

它彷佛就不在集合中了!

set.contains(list) → false!

事實上,更糟糕的是:當咱們(用迭代器)循環遍歷這個集合時,咱們依然會發現集合存在,可是contains() 仍是說它不存在!

for (List<String> l : set) { 
    set.contains(l) → false! 
}

  若是一個集合的迭代器和contains()都互相沖突的時候,顯然這個集合已經被破壞了。

  發生了什麼?咱們知道 List<String> 是一個可變對象,而在Java對可變對象的實現中,改造操做一般都會影響 equals() 和 hashCode()的結果。因此列表第一次放入 HashSet的時候,它是存儲在這時 hashCode() 對應的索引位置。可是後來列表發生了改變,計算 hashCode() 會獲得不同的結果,可是 HashSet 對此並不知道,因此咱們調用contains時候就會找不到列表。

  當 equals() 和 hashCode() 被改動影響的時候,咱們就破壞了哈希表利用對象做爲鍵的不變量。

下面是 java.util.Set規格說明中的一段話:

注意:當可變對象做爲集合的元素時要特別當心。若是對象內容改變後會影響相等比較並且對象是集合的元素,那麼集合的行爲是不肯定的。 

  咱們應該從這個例子中吸收教訓,對可變類型,實現行爲等價性便可,也就是說,只有指 向一樣內存空間的objects,纔是相等的。因此對可變類型來講,無需重寫這兩個函數,直接繼承 Object對象的兩個方法便可。 若是必定要判斷兩個可變對象看起來是否一致,最好定義一個新的方法。

相關文章
相關標籤/搜索