麻省理工18年春软件构造课程阅读15“相等”
本文內(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)實物理世界中,任何對象都是不相等的——在某些層次,即使是兩片雪花也是不同的,即使這種不同只是在空間中的位置(嚴格一點的話,在原子層次不能這么說,不過對于現(xiàn)實生活中“大”的對象已經(jīng)足夠正確了)。所以任何物理對象都不會真正相等,它們只會在某一些方面相似。
但是對于人類語言,或者對于數(shù)學(xué)世界,你可以有很多完全相同的東西。例如有兩個相等的表達式是很正常的,又例如√9 和 3表現(xiàn)了完全相同的數(shù)值。
看待“相等”的三種方式
嚴格來說,我們可以從三個角度定義相等:
抽象函數(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)該被認為是相等的呢?
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操作必須合理定義。
作為對比,這里列出來了幾個語言中的相等操作:
| Java | == | equals() |
| Objective C | == | isEqual: |
| C# | == | Equals() |
| Python | is | == |
| Javascript | == | n/a |
注意到==在Java和Python中的意義正好相反,別被這個弄混了。
作為程序員,我們不能改變測試指向相等操作的意義。在Java中,==總是判斷指向是否相等。但是當(dāng)我們定義了一個新的ADT,我們就需要判斷對于這個ADT來說對象值相等意味著什么,即如何判斷對象值相等/如何實現(xiàn)equals() 操作。
不可變類型的相等
equals() 是在 Object 中定義的,它的(默認)實現(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() 去判斷它們的值是否相等。表達式 (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) → trueinstanceof
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默認的 hashCode() 實現(xià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,因為我們還沒有覆蓋默認的 hashCode() ,實際上打破了對象契約:
Duration d1 = new Duration(1, 2); Duration d2 = new Duration(1, 2); d1.equals(d2) → true d1.hashCode() → 2392 d2.hashCode() → 4823d1 和 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的細節(jié),你可以參考Josh Bloch的書 Effective Java,他詳細介紹了hashcode應(yīng)該注意的問題和設(shè)計方法。另外StackOverflow上面也有關(guān)于這個的問答。在近些版本的Java中,你可以利用 Objects.hash() 方便的計算多個域的綜合hashcode。
要注意的是,只要你滿足了相等的對象產(chǎn)生相同的hashcode,不管你的hashcode是如何實現(xiàn)的,你的代碼都會是正確的。哈希碰撞僅僅只會性能,而一個錯誤哈希方法則會帶來錯誤!
最重要的是,如果你沒有覆蓋默認的hashCode,你就會繼承Object中根據(jù)存儲地址獲得的hashCode。如果你又覆蓋了equals,這就意味著你很大可能破壞了對象契約,所以一個通用準(zhǔn)則就是:
當(dāng)你覆蓋equals后,將hashCode也覆蓋
在很多年前,一個本課程的學(xué)生花了幾個小時找到了一個bug:他將 hashCode 拼成了 hashcode,也就是說他沒有將默認的 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)的是行為相等,以下哪一些表達式為真?
[ ] 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)的是觀察相等,以下哪一些表達式為真?
[ ] 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
在上面的代碼中:
表達式 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)系起來:
- 遠離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)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: EXCEL小技巧:如何统计非空单元格
- 下一篇: python语言key_Python语言