日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

麻省理工18年春软件构造课程阅读15“相等”

發(fā)布時間:2023/12/10 编程问答 32 豆豆
生活随笔 收集整理的這篇文章主要介紹了 麻省理工18年春软件构造课程阅读15“相等” 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

本文內(nèi)容來自MIT_6.031_sp18: Software Construction課程的Readings部分,采用CC BY-SA 4.0協(xié)議。

由于我們學(xué)校(哈工大)大二軟件構(gòu)造課程的大部分素材取自此,也是推薦的閱讀材料之一,于是打算做一些翻譯工作,自己學(xué)習(xí)的同時也能幫到一些懶得看英文的朋友。另外,該課程的閱讀資料中有的練習(xí)題沒有標(biāo)準(zhǔn)答案,所給出的“正確答案”為譯者所寫,有錯誤的地方還請指出。

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




譯者:李秋豪

審校:

V1.0 Thu Apr 12 21:02:06 CST 2018


本次課程的目標(biāo)

  • 理解分別通過抽象函數(shù)、等價關(guān)系以及觀察定義的“相等”。
  • 能夠辨別索引相等和對象相等的不同。
  • 能夠辨別可變類型中的觀察相等和行為相等的不同。
  • 理解“對象契約”(Object contract)并能夠正確地為可變/不可變類型設(shè)計相等操作。


介紹

在之前的閱讀材料中,我們已經(jīng)描述了抽象數(shù)據(jù)類型(ADT)是由它對應(yīng)的操作而非內(nèi)部表示決定的。而ADT中的抽象函數(shù)解釋了該類型是如何將內(nèi)部表示映射為使用者理解的抽象數(shù)據(jù)的,我們也看到了抽象函數(shù)決定了我們應(yīng)該如何實現(xiàn)ADT的各個操作。

在這篇閱讀中我們會聚焦于如何定義ADT的相等:抽象函數(shù)會給我們對相等操作一個清晰的定義。

在現(xiàn)實物理世界中,任何對象都是不相等的——在某些層次,即使是兩片雪花也是不同的,即使這種不同只是在空間中的位置(嚴(yán)格一點的話,在原子層次不能這么說,不過對于現(xiàn)實生活中“大”的對象已經(jīng)足夠正確了)。所以任何物理對象都不會真正相等,它們只會在某一些方面相似。

但是對于人類語言,或者對于數(shù)學(xué)世界,你可以有很多完全相同的東西。例如有兩個相等的表達(dá)式是很正常的,又例如√9 和 3表現(xiàn)了完全相同的數(shù)值。


看待“相等”的三種方式

嚴(yán)格來說,我們可以從三個角度定義相等:

抽象函數(shù):回憶一下抽象函數(shù)(AF: R → A ),它將具體的表示數(shù)據(jù)映射到了抽象的值。如果AF(a)=AF(b),我們就說a和b相等。

等價關(guān)系:等價是指對于關(guān)系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當(dāng)且僅當(dāng)E(a,b)。

以上兩種角度/定義實際上是一樣的,通過等價關(guān)系我們可以構(gòu)建一個抽象函數(shù)(譯者注:就是一個封閉的二元關(guān)系運算);而抽象函數(shù)也能推出一個等價關(guān)系。

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

觀察:我們說兩個對象相等,當(dāng)且僅當(dāng)使用者無法觀察到它們之間有不同,即每一個觀察總會都會得到相同的結(jié)果。例如對于兩個集合對象 {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來說,“觀察”就意味著使用它的觀察者/操作。所以我們也可以說兩個對象相等當(dāng)且僅當(dāng)它們的所有觀察操作都返回相同的結(jié)果。

這里要注意一點,“觀察者/操作”都必須是ADT的規(guī)格說明中規(guī)定好的。Java允許使用者跨過抽象層次去觀察對象的不同之處。例如==就能夠判斷兩個變量是否是索引到同一個存儲地方的,而 System.identityHashCode() 則是根據(jù)存儲位置計算返回值的。但是這些操作都不是ADT規(guī)格說明中的操作,所以我們不能根據(jù)這些“觀察”去判斷兩個對象是否相等。

例子: 時間跨度

這里有一個不可變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;} }

那么下面哪一些變量/對象應(yīng)該被認(rèn)為是相等的呢?

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

試著分別從抽象函數(shù)、等價關(guān)系以及使用者觀察這三個角度分析。

閱讀小練習(xí)

Any second now

思考上面的 Duration 以及變量 d1, d2, d3, d4 ,從抽象函數(shù)或等價關(guān)系來看,哪一些選項和d1相等?

  • [x] d1

  • [ ] d2

  • [x] d3

  • [x] d4

Eye on the clock

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

  • [x] d1

  • [ ] d2

  • [x] d3

  • [x] d4


== vs. equals()

和很多其他語言一樣,Java有兩種判斷相等的操作—— == 和 equals() 。

  • ==比較的是索引。更準(zhǔn)確的說,它測試的是指向相等(referential equality)。如果兩個索引指向同一塊存儲區(qū)域,那它們就是==的。對于我們之前提到過的快照圖來說,==就意味著它們的箭頭指向同一個對象。
  • equals()操作比較的是對象的內(nèi)容,換句話說,它測試的是對象值相等(object equality)。e在每一個ADT中,quals操作必須合理定義。

作為對比,這里列出來了幾個語言中的相等操作:

referential equalityobject equality
Java==equals()
Objective C==isEqual:
C#==Equals()
Pythonis==
Javascript==n/a

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

作為程序員,我們不能改變測試指向相等操作的意義。在Java中,==總是判斷指向是否相等。但是當(dāng)我們定義了一個新的ADT,我們就需要判斷對于這個ADT來說對象值相等意味著什么,即如何判斷對象值相等/如何實現(xiàn)equals() 操作。


不可變類型的相等

equals() 是在 Object 中定義的,它的(默認(rèn))實現(xiàn)方式如下:

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

可以看到, equals() 在Object中的實現(xiàn)方法就是測試指向/索引相等。對于不可變類型的對象來說,這幾乎總是錯的。所以你需要覆蓋(override) equals() 方法,將其替換為你的實現(xiàn)。

我們來看一個例子,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

如下圖所示,可以看到,雖然d2和o2最終指向的是同一個對象/存儲區(qū)域,但是我們的 equals()卻得到的不同的結(jié)果。

這是怎么回事呢?事實上, Duration 只是重載(overloaded)了 equals() 方法,因為它的方法標(biāo)識和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;} }

我們在之前的“靜態(tài)檢查”閱讀中已經(jīng)說過重載了,回憶一下,編譯器會在重載操作之間根據(jù)參數(shù)類型做出選擇。例如,當(dāng)你使用/操作符的時候,編譯器會根據(jù)參數(shù)是ints還是floats選擇整數(shù)除法或浮點數(shù)觸發(fā)。同理,如果我們對equals()傳入的是 Duration 索引,編譯器就會選擇equals(Duration) 這個操作。這樣,相等性就變得不確定了。

這是一個很容易犯的錯誤,即因為方法標(biāo)識的原因重載而不是覆蓋了的方法。在Java中,你可以使用 @Override來提示編譯器你是要后面的方法覆蓋父類中的方法,而編譯器會自動檢查這個方法是否和父類中的方法有著相同的標(biāo)識(產(chǎn)生覆蓋),否則編譯器會報錯。

現(xiàn)在我們更正 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(); }

它首先測試了傳入的that是 Duration(譯者注:這里that還可以是 Duration的子類),然后調(diào)用sameValue() 去判斷它們的值是否相等。表達(dá)式 (Duration)that 是一個類型轉(zhuǎn)換操作,它告訴編譯器你確信 that指向的是一個 Duration對象。

我們再次運行測試代碼,結(jié)果正確:

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 是動態(tài)檢查而非我們更喜歡的靜態(tài)檢查。普遍來說,在面向?qū)ο缶幊讨惺褂?instanceof 是一個不好的選擇。在本門課程中——在很多Java編程中也是這樣——除了實現(xiàn)相等操作,instanceof不能被使用。這也包括其他在運行時確定對象類型的操作,例如 getClass 。

我們會在以后學(xué)習(xí)如何使用更安全、可改動的代碼而不是 instanceof。

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


對象契約

由于Object的規(guī)格說明實在太重要了,我們有時也稱它為“對象契約”(the Object Contract)。你可以在object類中找到這些規(guī)格說明。我們在這里主要研究equals的規(guī)格說明。當(dāng)你在覆蓋equals時,要記得遵守這些規(guī)定:

  • equals 必須定義一個等價關(guān)系。即一個滿足自反性、對稱性和傳遞性關(guān)系。
  • equals 必須是確定的。即連續(xù)重復(fù)的進行相等操作,結(jié)果應(yīng)該相同。
  • 對于不是null的索引x, x.equals(null) 應(yīng)該返回false。
  • 如果兩個對象使用 equals 操作后結(jié)果為真,那么它們各自的hashCode 操作的結(jié)果也應(yīng)該相同。

破壞等價關(guān)系

正如前面所說,equals()操作必須構(gòu)建出一個滿足自反性、對稱性、傳遞性的等價關(guān)系。如果沒有滿足,那么與相等相關(guān)的操作(例如集合、搜索)將變得不可預(yù)測。例如你肯定不希望a等于b但是后來發(fā)現(xiàn)b不等于a,這都是非常隱秘的bug。

這里舉出了一個例子,它試圖將相等變得更復(fù)雜,結(jié)果導(dǎo)致了錯誤。假設(shè)我們希望在判斷 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; }

上面相等操作違背了等價關(guān)系里面的什么屬性?

閱讀小練習(xí)

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;}@Overridepublic 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 toleranceprivate boolean sameValue(Duration that) {return Math.abs(this.getLength() - that.getLength()) <= CLOCK_SKEW;} }

假設(shè)下面這些 Duration 對象被創(chuàng)建:

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

上面相等操作違背了等價關(guān)系里面的什么屬性?(忽略null索引)

  • [ ] recursivity

  • [ ] 自反性

  • [ ] sensitivity

  • [ ] 對稱性

  • [x] 傳遞性

Buggy equality

如果你想證明上面的equals違反了自反性,你需要創(chuàng)建幾個對象?

  • [ ] none

  • [x] 1 object

  • [ ] 2 objects

  • [ ] 3 objects

  • [ ] all the objects in the type

Null, null, null

和我們之前說過的不同,equals操作允許參數(shù)為null,這是因為Object的規(guī)格說明中提到了這種前置條件:

  • 對于非null的 x, x.equals(null) 應(yīng)該返回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

破壞哈希表

為了理解契約中有關(guān)hashCode的部分,你需要對哈希表的工作原理有一定的了解。兩個常見的聚合類型 HashSet 和 HashMap 就用到了哈希表的數(shù)據(jù)結(jié)構(gòu),并且依賴hashCode保存集合中的對象以及產(chǎn)生合適的鍵(key)。

一個哈希表表示的是一種映射:從鍵值映射到值的抽象數(shù)據(jù)類型。哈希表提供了常數(shù)級別的查找,所以它通常比數(shù)或者列表的性能要好。鍵不一定是有序的,也不一定有什么特別的屬性,除了類型必須提供 equals 和 hashCode兩個方法。

哈希表是怎么工作的呢?它包含了一個初始化的數(shù)組,其大小是我們設(shè)計好的。當(dāng)一個鍵值對準(zhǔn)備插入時,我們通過hashcode計算這個鍵,產(chǎn)生一個索引,它在我們數(shù)組大小的范圍內(nèi)(例如取模運算)。最后我們將值插入到數(shù)組索引對應(yīng)的位置。

哈希表的一個基本不變量就是鍵必須在hashcode規(guī)定的范圍內(nèi)。

Hashcode最好被設(shè)計為鍵計算后的索引應(yīng)該平滑、均勻的分布在所有范圍內(nèi)。但是偶爾沖突也會發(fā)生,例如兩個鍵計算出了同樣的索引。因此哈希表通常存儲的是一個鍵值對的列表而非一個單個的值,這通常被稱為哈希桶(hash bucket)。而在Java中,鍵值對就是一個有著兩個域的對象。當(dāng)插入時,你只要像計算出的索引位置插入一個鍵值對。當(dāng)查找時,你先根據(jù)鍵哈希出對應(yīng)的索引,然后在索引對應(yīng)的位置找到鍵值對列表,最后在這個列表中查找你的鍵。

現(xiàn)在你應(yīng)該知道了為什么Object的規(guī)格說明要求相等的對象必須有同樣的hashcode。如果兩個相等的對象hashcode不同,那么它們在聚合類存儲的時候位置也就不一樣——如果你存入了一個對象,然后查找一個相等的對象,就可能在錯誤的索引處進行查找,也就會得到錯誤的結(jié)果。

Object默認(rèn)的 hashCode() 實現(xiàn)和默認(rèn)的 equals()保持一致:

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

對于索引a和b,如果 a == b,那么a和b的存儲地址也就相同,hashCode()的結(jié)果也就相同。所以O(shè)bject的契約滿足。

但是對于不可變對象來說,它們需要重新實現(xiàn)hashCode()。例如上面提到的 Duration,因為我們還沒有覆蓋默認(rèn)的 hashCode() ,實際上打破了對象契約:

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

d1 和 d2 是 equals()為真的,但是它們的hashcode不一樣,所以我們需要修復(fù)它。

一個簡單粗暴的解決辦法就是讓hashCode總是返回相同的常量,這樣每一個對象的hashcode就都一樣了。這樣確實滿足了對象契約,但是會給性能帶來災(zāi)難性的后果,因為我們必須將每一個鍵值對都保存到相同的位置,而且查找會是線性遍歷所有插入過的對象。

而一個普遍(更合理)的方法就是計算對象每一個內(nèi)容的hashcode然后對它們進行一系列算術(shù)運算,最終返回一個綜合hashcode。對于 Duration而言就更簡單了,因為它只有一個整型內(nèi)容:

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

更多有關(guān)于hashcode的細(xì)節(jié),你可以參考Josh Bloch的書 Effective Java,他詳細(xì)介紹了hashcode應(yīng)該注意的問題和設(shè)計方法。另外StackOverflow上面也有關(guān)于這個的問答。在近些版本的Java中,你可以利用 Objects.hash() 方便的計算多個域的綜合hashcode。

要注意的是,只要你滿足了相等的對象產(chǎn)生相同的hashcode,不管你的hashcode是如何實現(xiàn)的,你的代碼都會是正確的。哈希碰撞僅僅只會性能,而一個錯誤哈希方法則會帶來錯誤!

最重要的是,如果你沒有覆蓋默認(rèn)的hashCode,你就會繼承Object中根據(jù)存儲地址獲得的hashCode。如果你又覆蓋了equals,這就意味著你很大可能破壞了對象契約,所以一個通用準(zhǔn)則就是:

當(dāng)你覆蓋equals后,將hashCode也覆蓋

在很多年前,一個本課程的學(xué)生花了幾個小時找到了一個bug:他將 hashCode 拼成了 hashcode,也就是說他沒有將默認(rèn)的 hashCode 覆蓋,最終奇怪的事情就發(fā)生了。所以記得使用 @Override!

閱讀小練習(xí)

Give me the code

思考下面這個ADT:

class Person {private String firstName;private String lastName;...@Overridepublic boolean equals(Object that) {return that instanceof Person && this.sameValue(that);}// returns true iff this and that represent the same abstract valueprivate 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();


可變類型的相等

之前我們已經(jīng)對不可變對象的相等性進行了討論,那么可變類型對象會是怎樣呢?

回憶之前我們對于相等的定義,即它們不能被使用者觀察出來不同。而對于可變對象來說,它們多了一種新的可能:通過在觀察前調(diào)用改造者,我們可以改變其內(nèi)部的狀態(tài),從而觀察出不同的結(jié)果。

所以讓我們重新定義兩種相等:

  • 觀察相等:兩個索引在不改變各自對象狀態(tài)的前提下不能被區(qū)分。例如,只調(diào)用觀察者、生產(chǎn)者、創(chuàng)建者。它測試的是這兩個索引在當(dāng)前程序狀態(tài)下“看起來”相等。
  • 行為相等:兩個所以在任何代碼的情況下都不能被區(qū)分,即使有一個對象調(diào)用了改造者。它測試的是兩個對象是否會在未來所有的狀態(tài)下“行為”相等。

對于不可變對象,觀察相等和行為相等是完全等價的,因為它們沒有改造者改變對象內(nèi)部的狀態(tài)。

對于可變對象,Java通常實現(xiàn)的是觀察相等。例如兩個不同的 List 對象包含相同的序列元素,那么equals() 操作就會返回真。

但是使用觀察相等會帶來隱秘的bug,并且也會讓我們很容易的破壞聚合類型的表示不變量。假設(shè)我們現(xiàn)在有一個 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!

事實上,更糟糕的是:當(dāng)我們(用迭代器)循環(huán)遍歷這個集合時,我們依然會發(fā)現(xiàn)集合存在,但是contains() 還是說它不存在!

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

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

發(fā)生了什么?我們知道 List<String> 是一個可變對象,而在Java對可變對象的實現(xiàn)中,改造操作通常都會影響 equals() 和 hashCode()的結(jié)果。所以列表第一次放入 HashSet的時候,它是存儲在這時 hashCode() 對應(yīng)的索引位置。但是后來列表發(fā)生了改變,計算 hashCode() 會得到不一樣的結(jié)果,但是 HashSet 對此并不知道,所以我們調(diào)用contains時候就會找不到列表。

當(dāng) equals() 和 hashCode() 被改動影響的時候,我們就破壞了哈希表利用對象作為鍵的不變量。

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

注意:當(dāng)可變對象作為集合的元素時要特別小心。如果對象內(nèi)容改變后會影響相等比較而且對象是集合的元素,那么集合的行為是不確定的。

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

我們從上面的例子和分析可以知道可變類型的equals()應(yīng)該實現(xiàn)為行為相等。這通常都意味著兩個對象只有在是索引別名的時候equals()才會返回真。索引可變類型的 equals() 和 hashCode() 應(yīng)該直接從 Object繼承。

對于需要觀察相等操作的可變類型(即當(dāng)前狀態(tài)下是否“看起來”一樣),最好是設(shè)計一個新的操作,例如similar() 或 sameValue(). 它們的實現(xiàn)或許和上文中的私有方法 sameValue() 相似(但是是公有的)。不幸的是Java沒有采取這種設(shè)計。


equals() 和 hashCode()的總結(jié)

對于不可變類型:

  • equals() 應(yīng)該比較抽象值是否相等。這和 equals() 比較行為相等性是一樣的。
  • hashCode() 應(yīng)該將抽象值映射為整數(shù)。

所以不可變類型應(yīng)該同時覆蓋 equals() 和 hashCode().

對于可變類型:

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

所以可變類型不應(yīng)該將 equals() 和 hashCode() 覆蓋,而是直接繼承 Object中的方法。Java沒有為大多數(shù)聚合類遵守這一規(guī)定,這也許會導(dǎo)致上面看到的隱秘bug。

閱讀小練習(xí)

Bag

假設(shè) Bag<E> 是一個可變聚合類型,它表示的是一個multiset(元素可以出現(xiàn)多次而且無序)。它的操作如下:

/** 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 實現(xiàn)的是行為相等,以下哪一些表達(dá)式為真?

  • [ ] 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的一部分,即它可能實現(xiàn)的是觀察相等,以下哪一些表達(dá)式為真?

  • [ ] b1.equals(b2)

  • [x] b1.equals(b3)

  • [ ] b1.equals(b4)

  • [ ] b2.equals(b3)

  • [x] b2.equals(b4)

  • [x] b3.equals(b1)

自動裝箱(Autoboxing)與相等

我們之前提到過原始/基本類型和它們的對應(yīng)的包裝(對象)類型,例如int和Integer。包裝類型的equals()比較的是兩個對象的值:

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

但是這里有一個隱秘的問題: == 被重載了。對于 Integer這樣的類型, == 判斷的是索引相等:

x == y // returns false

但是對于基本類型 int, == 實現(xiàn)的是行為相等:

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

所以你不能真正的將 Integer 和int互換。事實上Java會自動對 int 和Integer進行轉(zhuǎn)換(這被稱作自動裝箱和拆箱 autoboxing autounboxing),這也會導(dǎo)致bug,你應(yīng)該意識到編譯期發(fā)生的類型轉(zhuǎn)換。思考下面的代碼:

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?

閱讀小練習(xí)

Boxes

在上面的代碼中:

表達(dá)式 130在編譯期的類型是什么?

--> int

在 a.put("c", 130)執(zhí)行后,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);

畫出上面代碼執(zhí)行后的快照圖,在你的快照圖中有幾個 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);

在上面代碼執(zhí)行后, a.get("c").equals(b.get("c")) 會返回什么?

--> true

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

--> false-

Unboxes

現(xiàn)在假設(shè)你將 get() 的結(jié)果存儲在int 變量中:

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

在上面代碼執(zhí)行后, isEqual的返回值是什么?

--> true


總結(jié)

  • 相等應(yīng)該滿足等價關(guān)系(自反、對稱、傳遞)。
  • 相等和哈希必須互相一致,以便讓使用哈希表的數(shù)據(jù)結(jié)構(gòu)(例如 HashSet 和 HashMap)正常工作。
  • 抽象函數(shù)是不可變類型相等的比較基礎(chǔ)。
  • 索引是可變類型相等的比較基礎(chǔ)。這也是確保相等一致性和保護哈希表不變量的唯一方法。

相等是實現(xiàn)抽象數(shù)據(jù)類型中的一部分。現(xiàn)在我們將本文的知識點與我們的三個目標(biāo)聯(lián)系起來:

  • 遠(yuǎn)離bug. 正確的實現(xiàn)相等和哈希對于聚合類型的使用很重要(例如集合和映射),這也是寫測試時很需要的。因為每一個對象都會繼承Object中的實現(xiàn),實現(xiàn)不可變類型時一定要覆蓋它們。
  • 易于理解.使用者和其他程序員在閱讀規(guī)格說明后會期望我們的ADT實現(xiàn)合理的相等操作。
  • 可改動. 為不可變類型正確實現(xiàn)的相等操作會把索引相等和抽象值相等分離,也對使用者隱藏對象是否進行了共享。為可變類型選擇行為相等而非觀察相等幫助我們避開了隱秘的bug。

轉(zhuǎn)載于:https://www.cnblogs.com/liqiuhao/p/8810465.html

總結(jié)

以上是生活随笔為你收集整理的麻省理工18年春软件构造课程阅读15“相等”的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。