麻省理工18年春軟件構造課程閱讀15「相等」

<font size="3">html

本文內容來自MIT_6.031_sp18: Software Construction課程的Readings部分,採用CC BY-SA 4.0協議。java

因爲咱們學校(哈工大)大二軟件構造課程的大部分素材取自此,也是推薦的閱讀材料之一,因而打算作一些翻譯工做,本身學習的同時也能幫到一些懶得看英文的朋友。另外,該課程的閱讀資料中有的練習題沒有標準答案,所給出的「正確答案」爲譯者所寫,有錯誤的地方還請指出。程序員

(更新:從第10章開始只翻譯正確答案)web

<br />編程


<br />api

譯者:李秋豪數組

審校:安全

V1.0 Thu Apr 12 21:02:06 CST 2018數據結構

<br />oracle

本次課程的目標

  • 理解分別經過抽象函數、等價關係以及觀察定義的「相等」。
  • 可以辨別索引相等和對象相等的不一樣。
  • 可以辨別可變類型中的觀察相等和行爲相等的不一樣。
  • 理解「對象契約」(Object contract)並可以正確地爲可變/不可變類型設計相等操做。

<br />

介紹

在以前的閱讀材料中,咱們已經描述了抽象數據類型(ADT)是由它對應的操做而非內部表示決定的。而ADT中的抽象函數解釋了該類型是如何將內部表示映射爲使用者理解的抽象數據的,咱們也看到了抽象函數決定了咱們應該如何實現ADT的各個操做。

在這篇閱讀中咱們會聚焦於如何定義ADT的相等:抽象函數會給咱們對相等操做一個清晰的定義。

在現實物理世界中,任何對象都是不相等的——在某些層次,即便是兩片雪花也是不一樣的,即便這種不一樣只是在空間中的位置(嚴格一點的話,在原子層次不能這麼說,不過對於現實生活中「大」的對象已經足夠正確了)。因此任何物理對象都不會真正相等,它們只會在某一些方面類似。

可是對於人類語言,或者對於數學世界,你能夠有不少徹底相同的東西。例若有兩個相等的表達式是很正常的,又例如√9 和 3表現了徹底相同的數值。

<br />

看待「相等」的三種方式

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

**抽象函數:**回憶一下抽象函數(AF: R → A ),它將具體的表示數據映射到了抽象的值。若是AF(a)=AF(b),咱們就說a和b相等。

**等價關係:**等價是指對於關係E ⊆ T x T ,它知足:

  • 自反性: E(t,t) ∀ t ∈ T
  • 對稱性: E(t,u) ⇒ E(u,t)
  • 傳遞性: E(t,u) ∧ E(u,v) ⇒ E(t,v)

咱們說a等於b當且僅當E(a,b)。

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

第三種斷定抽象值相等的方法是從使用者/外部的角度去觀察。

**觀察:**咱們說兩個對象相等,當且僅當使用者沒法觀察到它們之間有不一樣,即每個觀察總會都會獲得相同的結果。例如對於兩個集合對象 {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

從ADT來講,「觀察」就意味着使用它的觀察者/操做。因此咱們也能夠說兩個對象相等當且僅當它們的全部觀察操做都返回相同的結果。

這裏要注意一點,**「觀察者/操做」都必須是ADT的規格說明中規定好的。**Java容許使用者跨過抽象層次去觀察對象的不一樣之處。例如==就可以判斷兩個變量是不是索引到同一個存儲地方的,而 System.identityHashCode() 則是根據存儲位置計算返回值的。可是這些操做都不是ADT規格說明中的操做,因此咱們不能根據這些「觀察」去判斷兩個對象是否相等。

例子: 時間跨度

這裏有一個不可變ADT的例子:

public class Duration {
    private final int mins;
    private final int secs;
    // Rep invariant:
    //    mins >= 0, secs >= 0
    // Abstraction function:
    //    AF(min, secs) = the span of time of mins minutes and secs seconds

    /** Make a duration lasting for m minutes and s seconds. */
    public Duration(int m, int s) {
        mins = m; secs = s;
    }
    /** @return length of this duration in seconds */
    public long getLength() {
        return mins*60 + secs;
    }
}

那麼下面哪一些變量/對象應該被認爲是相等的呢?

Duration d1 = new Duration (1, 2);
Duration d2 = new Duration (1, 3);
Duration d3 = new Duration (0, 62);
Duration d4 = new Duration (1, 2);

試着分別從抽象函數、等價關係以及使用者觀察這三個角度分析。

閱讀小練習

Any second now

思考上面的 Duration 以及變量 d1, d2, d3, d4 ,從抽象函數或等價關係來看,哪一些選項和d1相等?

  • [x] d1

  • [ ] d2

  • [x] d3

  • [x] d4

Eye on the clock

從使用者觀察的角度,哪一些選項和d1相等?

  • [x] d1

  • [ ] d2

  • [x] d3

  • [x] d4

<br />

== vs. equals()

和不少其餘語言同樣,Java有兩種判斷相等的操做—— ==equals()

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

做爲對比,這裏列出來了幾個語言中的相等操做:

referential equality object equality
Java == equals()
Objective C == isEqual:
C# == Equals()
Python is ==
Javascript == n/a

注意到==在Java和Python中的意義正好相反,別被這個弄混了。

做爲程序員,咱們不能改變測試指向相等操做的意義。在Java中,==老是判斷指向是否相等。可是當咱們定義了一個新的ADT,咱們就須要判斷對於這個ADT來講對象值相等意味着什麼,即如何判斷對象值相等/如何實現equals() 操做。

<br />

不可變類型的相等

equals() 是在 Object 中定義的,它的(默認)實現方式以下:

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

能夠看到, equals()Object中的實現方法就是測試指向/索引相等。對於不可變類型的對象來講,這幾乎老是錯的。因此你須要覆蓋(override) equals() 方法,將其替換爲你的實現。

咱們來看一個例子,Duration 的相等操做:

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

運行下面的測試代碼:

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

以下圖所示,能夠看到,雖然d2o2最終指向的是同一個對象/存儲區域,可是咱們的 equals()卻獲得的不一樣的結果。

這是怎麼回事呢?事實上, Duration 只是重載(overloaded)了 equals() 方法,由於它的方法標識和Object中的不同,也就是說,這是 Duration中有兩個 equals() 方法:一個是從 Object隱式繼承下來的equals(Object) ,還有一個就是咱們寫的 equals(Duration)

public class Duration extends Object {
    // explicit method that we declared:
    public boolean equals(Duration that) {
        return this.getLength() == that.getLength();
    }
    // implicit method inherited from Object:
    public boolean equals(Object that) {
        return this == that;
    }
}

咱們在以前的「靜態檢查」閱讀中已經說太重載了,回憶一下,編譯器會在重載操做之間根據參數類型作出選擇。例如,當你使用/操做符的時候,編譯器會根據參數是ints仍是floats選擇整數除法或浮點數觸發。同理,若是咱們對equals()傳入的是 Duration 索引,編譯器就會選擇equals(Duration) 這個操做。這樣,相等性就變得不肯定了。

這是一個很容易犯的錯誤,即由於方法標識的緣由重載而不是覆蓋了的方法。在Java中,你可使用 @Override來提示編譯器你是要後面的方法覆蓋父類中的方法,而編譯器會自動檢查這個方法是否和父類中的方法有着相同的標識(產生覆蓋),不然編譯器會報錯。

如今咱們更正 Duration equals()

@Override
public boolean equals(Object that) {
    return that instanceof Duration && this.sameValue((Duration)that);
}

// returns true iff this and that represent the same abstract value
private boolean sameValue(Duration that) {
    return this.getLength() == that.getLength();
}

它首先測試了傳入的thatDuration(譯者注:這裏that還能夠是 Duration的子類),而後調用sameValue() 去判斷它們的值是否相等。表達式 (Duration)that 是一個類型轉換操做,它告訴編譯器你確信 that指向的是一個 Duration對象。

咱們再次運行測試代碼,結果正確:

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

instanceof

instanceof 操做符 是用來測試一個實例是否屬於特定的類型。 instanceof 是動態檢查而非咱們更喜歡的靜態檢查。廣泛來講,在面向對象編程中使用 instanceof 是一個很差的選擇。在本門課程中——在不少Java編程中也是這樣——**除了實現相等操做,instanceof不能被使用。**這也包括其餘在運行時肯定對象類型的操做,例如 getClass

咱們會在之後學習如何使用更安全、可改動的代碼而不是 instanceof

譯者注:關於在equals()中使用 getClass 仍是 instanceof 操做符存在一些爭議,焦點集中於使用 instanceof 操做符可能會影響相等的對稱性(父子類)。《Java核心技術 卷一 第十版》的5.2.2節對此作了說明,讀者能夠參考一下。

<br />

對象契約

因爲Object的規格說明實在過重要了,咱們有時也稱它爲「對象契約」(the Object Contract)。你能夠在object類中找到這些規格說明。咱們在這裏主要研究equals的規格說明。當你在覆蓋equals時,要記得遵照這些規定:

  • equals 必須定義一個等價關係。即一個知足自反性、對稱性和傳遞性關係。
  • equals 必須是肯定的。即連續重複的進行相等操做,結果應該相同。
  • 對於不是null的索引xx.equals(null) 應該返回false。
  • 若是兩個對象使用 equals 操做後結果爲真,那麼它們各自的hashCode 操做的結果也應該相同。

破壞等價關係

正如前面所說,equals()操做必須構建出一個知足自反性、對稱性、傳遞性的等價關係。若是沒有知足,那麼與相等相關的操做(例如集合、搜索)將變得不可預測。例如你確定不但願a等於b可是後來發現b不等於a,這都是很是隱祕的bug。

這裏舉出了一個例子,它試圖將相等變得更復雜,結果致使了錯誤。假設咱們但願在判斷 Duration 相等的時候容許一些偏差,由於不一樣的電腦同步的時間可能會有一小點不一樣:

@Override
public boolean equals(Object that) {
    return that instanceof Duration && this.sameValue((Duration)that);
}

private static final int CLOCK_SKEW = 5; // seconds

// returns true iff this and that represent the same abstract value within a clock-skew tolerance
private boolean sameValue(Duration that) {
    return Math.abs(this.getLength() - that.getLength()) <= CLOCK_SKEW;
}

上面相等操做違背了等價關係裏面的什麼屬性?

閱讀小練習

Equals-ish

思考上面提到的 Duration

public class Duration {
    private final int mins;
    private final int secs;
    // Rep invariant:
    //    mins >= 0, secs >= 0
    // Abstraction function:
    //    AF(min, secs) = the span of time of mins minutes and secs seconds

    /** Make a duration lasting for m minutes and s seconds. */
    public Duration(int m, int s) {
        mins = m; secs = s;
    }
    /** @return length of this duration in seconds */
    public long getLength() {
        return mins*60 + secs;
    }

    @Override
    public boolean equals(Object that) {
        return that instanceof Duration && this.sameValue((Duration)that);
    }

    private static final int CLOCK_SKEW = 5; // seconds

    // returns true iff this and that represent the same abstract value within a clock-skew tolerance
    private boolean sameValue(Duration that) {
        return Math.abs(this.getLength() - that.getLength()) <= CLOCK_SKEW;
    }
}

假設下面這些 Duration 對象被建立:

Duration d_0_60 = new Duration(0, 60);
Duration d_1_00 = new Duration(1, 0);
Duration d_0_57 = new Duration(0, 57);
Duration d_1_03 = new Duration(1, 3);

如下哪一些選項會返回真?

  • [x] d_0_60.equals(d_1_00)

  • [x] d_1_00.equals(d_0_60)

  • [x] d_1_00.equals(d_1_00)

  • [x] d_0_57.equals(d_1_00)

  • [ ] d_0_57.equals(d_1_03)

  • [x] d_0_60.equals(d_1_03)

Skewed up

上面相等操做違背了等價關係裏面的什麼屬性?(忽略null索引)

  • [ ] recursivity

  • [ ] 自反性

  • [ ] sensitivity

  • [ ] 對稱性

  • [x] 傳遞性

Buggy equality

若是你想證實上面的equals違反了自反性,你須要建立幾個對象?

  • [ ] none

  • [x] 1 object

  • [ ] 2 objects

  • [ ] 3 objects

  • [ ] all the objects in the type

Null, null, null

和咱們以前說過的不一樣,equals操做容許參數爲null,這是由於Object的規格說明中提到了這種前置條件:

  • 對於非null的 x, x.equals(null) 應該返回false

若是 x.equals(null) 返回true,equals將會違背等價的什麼屬性?

  • [ ] recursivity

  • [ ] 自反性

  • [ ] sensitivity

  • [x] 對稱性

  • [ ] 傳遞性

哪一行代碼會讓 equals()that 是null時返回false?

1 @Override
2 public boolean equals(Object that) {
3     return that instanceof Duration 
4            && this.sameValue((Duration)that);
  }

  // returns true iff this and that represent the same abstract value
5 private boolean sameValue(Duration that) {
6     return this.getLength() == that.getLength();
  }

--> 3

破壞哈希表

爲了理解契約中有關hashCode的部分,你須要對哈希表的工做原理有必定的瞭解。兩個常見的聚合類型 HashSetHashMap 就用到了哈希表的數據結構,而且依賴hashCode保存集合中的對象以及產生合適的鍵(key)。

一個哈希表表示的是一種映射:從鍵值映射到值的抽象數據類型。哈希表提供了常數級別的查找,因此它一般比數或者列表的性能要好。鍵不必定是有序的,也不必定有什麼特別的屬性,除了類型必須提供 equalshashCode兩個方法。

哈希表是怎麼工做的呢?它包含了一個初始化的數組,其大小是咱們設計好的。當一個鍵值對準備插入時,咱們經過hashcode計算這個鍵,產生一個索引,它在咱們數組大小的範圍內(例如取模運算)。最後咱們將值插入到數組索引對應的位置。

哈希表的一個基本不變量就是鍵必須在hashcode規定的範圍內。

Hashcode最好被設計爲鍵計算後的索引應該平滑、均勻的分佈在全部範圍內。可是偶爾衝突也會發生,例如兩個鍵計算出了一樣的索引。所以哈希表一般存儲的是一個鍵值對的列表而非一個單個的值,這一般被稱爲哈希桶(hash bucket)。而在Java中,鍵值對就是一個有着兩個域的對象。當插入時,你只要像計算出的索引位置插入一個鍵值對。當查找時,你先根據鍵哈希出對應的索引,而後在索引對應的位置找到鍵值對列表,最後在這個列表中查找你的鍵。

如今你應該知道了爲何Object的規格說明要求相等的對象必須有一樣的hashcode。若是兩個相等的對象hashcode不一樣,那麼它們在聚合類存儲的時候位置也就不同——若是你存入了一個對象,而後查找一個相等的對象,就可能在錯誤的索引處進行查找,也就會獲得錯誤的結果。

Object默認的 hashCode() 實現和默認的 equals()保持一致:

public class Object {
  ...
  public boolean equals(Object that) { return this == that; }
  public int hashCode() { return /* the memory address of this */; }
}

對於索引ab,若是 a == b,那麼a和b的存儲地址也就相同,hashCode()的結果也就相同。因此Object的契約知足。

可是對於不可變對象來講,它們須要從新實現hashCode()。例如上面提到的 Duration,由於咱們尚未覆蓋默認的 hashCode() ,實際上打破了對象契約:

Duration d1 = new Duration(1, 2);
Duration d2 = new Duration(1, 2);
d1.equals(d2) → true
d1.hashCode() → 2392
d2.hashCode() → 4823

d1d2equals()爲真的,可是它們的hashcode不同,因此咱們須要修復它。

一個簡單粗暴的解決辦法就是讓hashCode老是返回相同的常量,這樣每個對象的hashcode就都同樣了。這樣確實知足了對象契約,可是會給性能帶來災難性的後果,由於咱們必須將每個鍵值對都保存到相同的位置,並且查找會是線性遍歷全部插入過的對象。

而一個廣泛(更合理)的方法就是計算對象每個內容的hashcode而後對它們進行一系列算術運算,最終返回一個綜合hashcode。對於 Duration而言就更簡單了,由於它只有一個整型內容:

@Override
public int hashCode() {
    return (int) getLength();
}

更多有關於hashcode的細節,你能夠參考Josh Bloch的書 Effective Java,他詳細介紹了hashcode應該注意的問題和設計方法。另外StackOverflow上面也有關於這個的問答。在近些版本的Java中,你能夠利用 Objects.hash() 方便的計算多個域的綜合hashcode。

要注意的是,只要你知足了相等的對象產生相同的hashcode,無論你的hashcode是如何實現的,你的代碼都會是正確的。哈希碰撞僅僅只會性能,而一個錯誤哈希方法則會帶來錯誤!

最重要的是,若是你沒有覆蓋默認的hashCode,你就會繼承Object中根據存儲地址得到的hashCode。若是你又覆蓋了equals,這就意味着你很大可能破壞了對象契約,因此一個通用準則就是:

當你覆蓋equals後,將hashCode也覆蓋

在不少年前,一個本課程的學生花了幾個小時找到了一個bug:他將 hashCode 拼成了 hashcode,也就是說他沒有將默認的 hashCode 覆蓋,最終奇怪的事情就發生了。因此記得使用 @Override

閱讀小練習

Give me the code

思考下面這個ADT:

class Person {
  private String firstName;
  private String lastName;
  ...

  @Override
  public boolean equals(Object that) {
      return that instanceof Person && this.sameValue(that);
  }

  // returns true iff this and that represent the same abstract value
  private boolean sameValue(Person that) {
      return this.lastName.toUpperCase().equals(that.lastName.toUpperCase());
  }

  public int hashCode() {
      // TODO
  }
}

TODO 的地方可使用如下哪些選項,讓 hashCode()equals()保持一致?

  • [x] return 42;
  • [ ] return firstName.toUpperCase();
  • [x] return lastName.toUpperCase().hashCode();
  • [ ] return firstName.hashCode() + lastName.hashCode();

<br />

可變類型的相等

以前咱們已經對不可變對象的相等性進行了討論,那麼可變類型對象會是怎樣呢?

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

因此讓咱們從新定義兩種相等:

  • **觀察相等:**兩個索引在不改變各自對象狀態的前提下不能被區分。例如,只調用觀察者、生產者、建立者。它測試的是這兩個索引在當前程序狀態下「看起來」相等。
  • **行爲相等:**兩個因此在任何代碼的狀況下都不能被區分,即便有一個對象調用了改造者。它測試的是兩個對象是否會在將來全部的狀態下「行爲」相等。

對於不可變對象,觀察相等和行爲相等是徹底等價的,由於它們沒有改造者改變對象內部的狀態。

**對於可變對象,Java一般實現的是觀察相等。**例如兩個不一樣的 List 對象包含相同的序列元素,那麼equals() 操做就會返回真。

可是使用觀察相等會帶來隱祕的bug,而且也會讓咱們很容易的破壞聚合類型的表示不變量。假設咱們如今有一個 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規格說明中的一段話:

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

不幸的是,Java庫堅持它對可變類型的 equals() 的實現,即聚合類使用觀察相等,不過也有一些可變類型(例如 StringBuilder)使用的是行爲相等。

咱們從上面的例子和分析能夠知道**可變類型的equals()應該實現爲行爲相等。**這一般都意味着兩個對象只有在是索引別名的時候equals()纔會返回真。索引可變類型的 equals()hashCode() 應該直接從 Object繼承。

對於須要觀察相等操做的可變類型(即當前狀態下是否「看起來」同樣),最好是設計一個新的操做,例如similar()sameValue(). 它們的實現或許和上文中的私有方法 sameValue() 類似(可是是公有的)。不幸的是Java沒有采起這種設計。

<br />

equals() 和 hashCode()的總結

對於不可變類型:

  • equals() 應該比較抽象值是否相等。這和 equals() 比較行爲相等性是同樣的。
  • hashCode() 應該將抽象值映射爲整數。

因此不可變類型應該同時覆蓋 equals()hashCode().

對於可變類型:

  • equals() 應該比較索引,就像 ==同樣。一樣的,這也是比較行爲相等性。
  • hashCode() 應該將索引映射爲整數。

因此可變類型不該該將 equals()hashCode() 覆蓋,而是直接繼承 Object中的方法。Java沒有爲大多數聚合類遵照這一規定,這也許會致使上面看到的隱祕bug。

閱讀小練習

Bag

假設 Bag<E> 是一個可變聚合類型,它表示的是一個multiset(元素能夠出現屢次並且無序)。它的操做以下:

/** make an empty bag */
public Bag<E>()

/** modify this bag by adding an occurrence of e, and return this bag */
public Bag<E> add(E e)

/** modify this bag by removing an occurrence of e (if any), and return this bag */
public Bag<E> remove(E e)

/** return number of times e occurs in this bag */
public int count(E e)

運行下面的代碼:

Bag<String> b1 = new Bag<>().add("a").add("b");
Bag<String> b2 = new Bag<>().add("a").add("b");
Bag<String> b3 = b1.remove("b");
Bag<String> b4 = new Bag<>().add("b").add("a"); // swap!

如下那些選項在運行事後爲真?

  • [x] b1.count("a") == 1

  • [ ] b1.count("b") == 1

  • [x] b2.count("a") == 1

  • [x] b2.count("b") == 1

  • [x] b3.count("a") == 1

  • [ ] b3.count("b") == 1

  • [x] b4.count("a") == 1

  • [x] b4.count("b") == 1

Bag behavior

若是 Bag 實現的是行爲相等,如下哪一些表達式爲真?

  • [ ] b1.equals(b2)

  • [x] b1.equals(b3)

  • [ ] b1.equals(b4)

  • [ ] b2.equals(b3)

  • [ ] b2.equals(b4)

  • [x] b3.equals(b1)

Bean bag

若是 Bag 是Java API的一部分,即它可能實現的是觀察相等,如下哪一些表達式爲真?

  • [ ] b1.equals(b2)

  • [x] b1.equals(b3)

  • [ ] b1.equals(b4)

  • [ ] b2.equals(b3)

  • [x] b2.equals(b4)

  • [x] b3.equals(b1)

自動裝箱(Autoboxing)與相等

咱們以前提到過原始/基本類型和它們的對應的包裝(對象)類型,例如intInteger。包裝類型的equals()比較的是兩個對象的值:

Integer x = new Integer(3);
Integer y = new Integer(3);
x.equals(y) → true

可是這裏有一個隱祕的問題: == 被重載了。對於 Integer這樣的類型, == 判斷的是索引相等:

x == y // returns false

可是對於基本類型 int, == 實現的是行爲相等:

(int)x == (int)y // returns true

因此你不能真正的將 Integerint互換。事實上Java會自動對 intInteger進行轉換(這被稱做自動裝箱和拆箱 autoboxing autounboxing),這也會致使bug,你應該意識到編譯期發生的類型轉換。思考下面的代碼:

Map<String, Integer> a = new HashMap(), b = new HashMap();
a.put("c", 130); // put ints into the map
b.put("c", 130);
a.get("c") == b.get("c") → ?? // what do we get out of the map?

閱讀小練習

Boxes

在上面的代碼中:

表達式 130在編譯期的類型是什麼?

--> int

a.put("c", 130)執行後,Map中表示130的值會是什麼類型?

--> Integer

a.get("c")在編譯期中的類型是什麼?

--> Integer

Circles

Map<String, Integer> a = new HashMap<>(), b = new HashMap<>();
a.put("c", 130); // put ints into the map
b.put("c", 130);

畫出上面代碼執行後的快照圖,在你的快照圖中有幾個 HashMap 對象?

--> 2

在你的快照圖中有幾個 Integer 對象?

--> 2

Equals

Map<String, Integer> a = new HashMap<>(), b = new HashMap<>();
a.put("c", 130); // put ints into the map
b.put("c", 130);

在上面代碼執行後, a.get("c").equals(b.get("c")) 會返回什麼?

--> true

a.get("c") == b.get("c") 會返回什麼?

--> false­

Unboxes

如今假設你將 get() 的結果存儲在int 變量中:

int i = a.get("c");
int j = b.get("c");
boolean isEqual = (i == j);

在上面代碼執行後, isEqual的返回值是什麼?

--> true

<br />

總結

  • 相等應該知足等價關係(自反、對稱、傳遞)。
  • 相等和哈希必須互相一致,以便讓使用哈希表的數據結構(例如 HashSetHashMap)正常工做。
  • 抽象函數是不可變類型相等的比較基礎。
  • 索引是可變類型相等的比較基礎。這也是確保相等一致性和保護哈希表不變量的惟一方法。

相等是實現抽象數據類型中的一部分。如今咱們將本文的知識點與咱們的三個目標聯繫起來:

  • 遠離bug. 正確的實現相等和哈希對於聚合類型的使用很重要(例如集合和映射),這也是寫測試時很須要的。由於每個對象都會繼承Object中的實現,實現不可變類型時必定要覆蓋它們。
  • 易於理解.使用者和其餘程序員在閱讀規格說明後會指望咱們的ADT實現合理的相等操做。
  • 可改動. 爲不可變類型正確實現的相等操做會把索引相等和抽象值相等分離,也對使用者隱藏對象是否進行了共享。爲可變類型選擇行爲相等而非觀察相等幫助咱們避開了隱祕的bug。

</font>

相關文章
相關標籤/搜索