第3章:抽象數據類型(ADT)和麪向對象編程(OOP) 3.5 ADT和OOP中的等價性

大綱

什麼是等價性?爲何要討論等價性?
三種等價性的方式
==與equals()
不可變類型的等價性
對象契約
可變類型的等價性
自動包裝和等價性java

什麼是等價性?爲何要討論等價性?

ADT上的相等操做程序員

ADT是經過建立以操做爲特徵的類型而不是其表示的數據抽象。
對於抽象數據類型,抽象函數(AF)解釋瞭如何將具體表示值解釋爲抽象類型的值,而且咱們看到了抽象函數的選擇如何決定如何編寫實現每一個ADT操做的代碼。
抽象函數(AF)提供了一種方法來清晰地定義ADT上的相等操做。編程

數據類型中值的相等性?數組

在物質世界中,每一個物體都是不一樣的 - 即便兩個雪花的區別僅僅是它們在太空中的位置,在某種程度上,即便是兩個雪花也是不一樣的。
因此兩個實體對象永遠不會真正「相等」。 他們只有類似的程度。
然而,在人類語言的世界中,在數學概念的世界中,對同一事物能夠有多個名稱。安全

  • 當兩個表達式表示相同的事物時,很天然地:1 + 2,√9和3是同一個理想數學值的替表明達式。

三種等價性的方式

使用AF或使用關係數據結構

使用抽象函數。 回想一下抽象函數f:R→A將數據類型的具體實例映射到它們相應的抽象值。 爲了使用f做爲等價性的定義,咱們說當且僅當f(a)= f(b)時等於b。
使用關係。 等價關係是E⊆T x T,即:ide

  • 自反:E(t,t)∀t∈T
  • 對稱:E(t,u)⇒E(u,t)
  • 傳遞:E(t,u)∧E(u,v)⇒E(t,v)
  • 用E做爲等價性的定義,當且僅當E(a,b)時,咱們會說a等於b。

等價關係:自反,對稱,傳遞
這兩個概念是等價的。函數

  • 等價關係致使抽象函數(關係分區T,所以f將每一個元素映射到其分區類)。
  • 抽象函數引起的關係是等價關係。

使用觀察性能

咱們能夠談論抽象價值之間的等價性的第三種方式就是外部人(客戶)能夠觀察他們的狀況
使用觀察。 咱們能夠說,當兩個對象沒法經過觀察進行區分時,這兩個對象是相同的 - 咱們能夠應用的每一個操做對兩個對象都產生相同的結果。站在外部觀察者角度測試

就ADT而言,「觀察」意味着調用對象的操做。 因此當且僅當經過調用抽象數據類型的任何操做不能區分它們時,兩個對象是相等的。

==與equals()

Java有兩種不一樣的操做,用於測試相等性,具備不一樣的語義。

  • ==運算符比較引用。

它測試引用等價性。 若是它們指向內存中的相同存儲,則兩個引用是==。 就快照圖而言,若是它們的箭頭指向相同的對象氣泡,則兩個引用是==。

  • equals()操做比較對象內容

換句話說,對象等價性。

必須爲每一個抽象數據類型適當地定義equals操做。在自定義ADT時,須要重寫對象的equals()方法

  • 當咱們定義一個新的數據類型時,咱們有責任決定數據類型值的對象相等是什麼意思,並適當地實現equals()操做。

==運算符與equals方法

對於基本數據類型,您必須使用==對基本數據類型,使用==斷定相等
對於對象引用類型對象類型,使用equals()

  • ==運算符提供身份語義若是用==,是在判斷兩個對象身份標識ID是否相等(指向內存裏的同一段空間)
  • 徹底由Object.equals實現
  • 即便Object.equals已被覆蓋,這不多是你想要的!
  • 你應該(幾乎)老是使用.equals

重寫方法的提示

若是你想覆蓋一個方法:

  • 確保簽名匹配
  • 使用@Override編譯器有你的背部
  • 複製粘貼聲明(或讓IDE爲你作)

不可變類型的等價性

equals()方法由Object定義,其默認含義與引用相等相同。在對象中實現的缺省equals()方法是在判斷引用等價性
對於不可變的數據類型,這幾乎老是錯誤的。
咱們必須重寫equals()方法,將其替換爲咱們本身的實現。

重寫與重載

在方法簽名中犯一個錯誤很容易,而且當您打算覆蓋它時重載一個方法。
只要你的意圖是在你的超類中重寫一個方法,就應該使用Java的批註@Override。
經過這個註解,Java編譯器將檢查超類中是否存在具備相同簽名的方法,若是簽名中出現錯誤,則會給出編譯器錯誤。

instanceof

instanceof運算符測試對象是不是特定類型的實例。
使用instanceof是動態類型檢查,而不是靜態類型檢查。
通常來講,在面向對象編程中使用instanceof是一種陋習。 除了實施等價性以外,任何地方都應該禁止。
這種禁止還包括其餘檢查對象運行時類型的方法。

  • 例如,getClass()也是不容許的。

對象契約

對象中equals()的契約

您重寫equals()方法時,您必須遵照其整體契約:

  • 等於必須定義一個等價關係
  • 即一種等價關係:自反,傳遞,對稱
  • equals必須一致:對方法的重複調用必須產生相同的結果,前提是沒有在對象的等值比較中使用的信息被修改;除非對象被修改了,不然調用屢次等於應一樣的結果
  • 對於非空引用x,x.equals(null)應返回false;
  • hashCode()必須爲等於equals方法的兩個對象產生相同的結果。 「相等」的對象,其hashCode()的結果必須一致

Equals契約

equals方法實現等價關係:

  • 自反:對於任何非空參考值x,x.equals(x)必須返回true。
  • 對稱:對於任何非空引用值x和y,當且僅當y.equals(x)返回true時,x.equals(y)必須返回true。
  • 傳遞:對於任何非空引用值x,y,z,若是x.equals(y)返回true而且y.equals(z)返回true,則x.equals(z)mus返回true。
  • 一致性:對於任何非空引用值x和y,若是修改了在對象上的等值比較中沒有使用的信息,則x.equals(y)的多個調用始終返回true或始終返回false。
  • 對於任何非空引用值x,x.equals(null)必須返回false。

equals是全部對象的全局等價關係。

打破等價關係

咱們必須確保由equals()實現的等價性定義其實是一個前面定義的等價關係:自反,對稱和傳遞。

  • 若是不是,那麼依賴於等價性的操做(如集合,搜索)將表現出不規律和不可預測的行爲。
  • 你不想用一個數據類型進行編程,其中有時等於b,但b不等於a。
  • 會產生微妙而痛苦的錯誤。

打破哈希表

散列表是映射的表示:將鍵映射到值的抽象數據類型。

  • 哈希表提供了恆定的時間查找,因此它們每每比樹或列表執行得更好。 密鑰沒必要訂購,或具備任何特定的屬性,除了提供equals和hashCode。

哈希表如何工做:

  • 它包含一個數組,該數組的初始化大小與咱們但願插入的元素的數量相對應。
  • 當提供一個鍵和一個值用於插入時,咱們計算該鍵的哈希碼,並將其轉換爲數組範圍內的索引(例如,經過模分割)。 該值而後插入該索引。

哈希表的rep不變量包含密鑰在由其哈希碼肯定的時隙中的基本約束。

散列碼的設計使密鑰均勻分佈在索引上。
但偶爾會發生衝突,而且兩個鍵被放置在相同的索引處。
所以,不是在索引處保存單個值,而是使用哈希表實際上包含一個鍵/值對列表,一般稱爲哈希桶。
一個鍵/值對在Java中被簡單地實現爲具備兩個字段的對象。
插入時,您將一對添加到由散列碼肯定的陣列插槽中的列表中。
對於查找,您散列密鑰,找到正確的插槽,而後檢查每一個對,直到找到其中的密鑰等於查詢密鑰的對。

hashCode契約

只要在應用程序執行過程當中屢次調用同一對象時,只要修改了對象的等值比較中未使用的信息,hashCode方法就必須始終返回相同的整數。

  • 該整數不須要從應用程序的一次執行到同一應用程序的另外一次執行保持一致。

若是兩個對象根據equals(Object)方法相等,則對這兩個對象中的每一個對象調用hashCode方法必須產生相同的整數結果。等價的對象必須有相同的的hashCode

  • 根據equals(Object)方法,若是兩個對象不相等,則不要求對兩個對象中的每個調用hashCode方法都必須產生不一樣的整數結果。

可是,程序員應該意識到,爲不相等的對象生成不一樣的整數結果可能會提升散列表的性能。不相等的對象,也能夠映射爲一樣的的hashCode,但性能會變差

相等的對象必須具備相同的散列碼

  • 若是你重寫equals,你必須重寫hashCode

不相等的對象應該有不一樣的哈希碼

  • 構建時考慮全部的值域

除非對象發生變化,不然散列代碼不能更改

重寫hashCode()

確保合約知足的一個簡單而激烈的方法是讓hashCode始終返回一些常量值,所以每一個對象的散列碼都是相同的。

  • 這符合Object合同,可是它會帶來災難性的性能影響,由於每一個密鑰都將存儲在同一個槽中,而且每一個查找都會退化爲沿着長列表的線性搜索。

標準是計算用於肯定相等性的對象的每一個組件的哈希代碼(一般經過調用每一個組件的hashCode方法),而後組合這些哈希碼,引入幾個算術運算。

打破哈希表

爲何對象合同要求相同的對象具備相同的哈希碼?

  • 若是兩個相等的對象有不一樣的哈希碼,它們可能被放置在不一樣的槽中。
  • 所以,若是您嘗試使用與插入值相同的鍵來查找值,則查找可能會失敗。

Object的默認hashCode()實現與其默認的equals()一致:

重寫hashCode()

Java的最新版本如今有一個實用程序方法Objects.hash(),能夠更容易地實現涉及多個字段的哈希碼。
請注意,若是您根本不重寫hashCode(),您將從Object得到一個Object,該Object基於對象的地址。
若是你有等價性的權利,這將意味着你幾乎確定會違反合同

  • 兩個相同的對象,必定要有一樣的hashcode。

通常規則:
覆蓋equals()時老是覆蓋hashCode()。

可變類型的等價性

等價性:當兩個對象沒法經過觀察區分時,它們是等價的。

對於可變對象,有兩種解釋方法:

  • 當它們不能經過不改變對象狀態的觀察進行區分時,即只經過調用觀察者,生產者和建立者方法。這一般被嚴格地稱爲觀察等價性,由於它在當前的程序狀態下測試兩個對象是否「看起來」是相同的。

觀察等價性:在不改變狀態的狀況下,兩個可變對象是否看起來一致

  • 當他們沒法經過任何觀察來區分時,即便狀態發生變化。這個解釋容許調用兩個對象的任何方法,包括增變器。這被稱爲行爲等價性,由於它測試這兩個對象在這個和全部將來的狀態中是否會「表現」相同。

行爲等價性:調用對象的任何方法都展現出一致的結果

注意:對於不可變的對象,觀察和行爲的等價性是相同的,由於沒有任何變值器方法。

Java中的可變類型的等價性

對可變類型來講,每每傾向於實現嚴格的觀察等價性

  • Java對大多數可變數據類型(例如Collections)使用觀察等價性,但其餘可變類(如StringBuilder)使用行爲等價性。
  • 若是兩個不一樣的List對象包含相同的元素序列,則equals()報告它們相等。

可是使用觀察等價性致使微妙的錯誤,而且事實上容許咱們輕易地破壞其餘集合數據結構的表明不變量。但在有些時候,觀察等價性可能致使錯誤,甚至可能破壞RI

這是怎麼回事?

List <String>是一個可變對象。 在像List這樣的集合類的標準Java實現中,突變會影響equals()和hashCode()的結果。
當列表第一次放入HashSet時,它將存儲在當時與其hashCode()結果相對應的哈希桶/散列桶中。
當列表隨後發生變化時,其hashCode()會發生變化,但HashSet沒有意識到它應該移動到不一樣的存儲桶中。 因此它再也找不到了。
當equals()和hashCode()可能受突變影響時,咱們能夠打破使用該對象做爲關鍵字的哈希表的不變性。

若是可變對象用做集合元素,必須很是當心。

若是對象的值以影響等於比較的方式更改,而對象是集合中的元素,則不會指定集合的行爲。 若是某個可變的對象包含在集合類中,當其發生改變後,集合類的行爲不肯定,務必當心
不幸的是,Java庫對於可變類的equals()的解釋並不一致。 集合使用觀察等價性,但其餘可變類(如StringBuilder)使用行爲等價性。 在JDK中,不一樣的mutable類使用不一樣的等價性標準...

從這個例子中學到的經驗教訓

equals()對可變類型,實現行爲等價性便可

一般,這意味着兩個引用應該是equals()當且僅當它們是同一個對象的別名。也就是說,只有指向一樣內存空間的對象,纔是相等的。
因此可變對象應該繼承Object的equals()和hashCode()。 對可變類型來講,無需重寫這兩個函數,直接繼承Object對象的兩個方法便可。
對於須要觀察等價性概念的客戶(兩個可變對象在當前狀態下「看起來」是否相同),最好定義一個新方法,例如similar()。

equals()和hashCode()的最終規則

對於不可變類型:

  • equals()應該比較抽象值。 這與equals()應該提供行爲等價性相同。
  • hashCode()應該將抽象值映射到一個整數。
  • 因此不可變類型必須覆蓋equals()和hashCode()。

對於可變類型:

  • equals()應該比較引用,就像==同樣。 一樣,這與等價性()應該提供行爲等價性同樣。
  • hashCode()應該將引用映射爲一個整數。
  • 因此可變類型不該該重寫equals()和hashCode(),而應該簡單地使用Object提供的默認實現。 不幸的是,Java不遵循這個規則,致使咱們上面看到的陷阱。

對象中的clone()

clone()建立並返回此對象的副本。
「拷貝複製」的確切含義可能取決於對象的類別。
通常意圖是,對於任何對象x:

x.clone() != x
x.clone().getClass() == x.getClass() 
x.clone().equals(x)

自動打包和等價性

基本類型及其對象類型等價性,例如int和Integer。
若是您建立兩個具備相同值的Integer對象,則它們將相互爲equals()。
可是若是x == y呢?
-----錯誤(由於引用等價性)
可是若是(int)x ==(int)y呢?
-----正確

總結

等價性是實現抽象數據類型(ADT)的一部分。

  • 等價性應該是一種等價關係(反身,對稱,傳遞)。
  • 相等和散列碼必須相互一致,以便使用散列表(如HashSet和HashMap)的數據結構可以正常工做。
  • 抽象函數是不變數據類型中等式的基礎。
  • 引用等價性是可變數據類型中等價性的基礎; 這是確保隨時間的一致性並避免破壞散列表的不變式的惟一方法。

減小錯誤保證安全

  • 使用集合數據類型(如集合和地圖)須要正確實現相等和散列碼。 編寫測試也是很是理想的。 因爲Java中的每一個對象都繼承了Object實現,因此不可變類型必須重寫它們。

容易明白

  • 讀過咱們規範的客戶和其餘程序員會但願咱們的類型實現適當的等價性操做,若是咱們不這樣作,會感到驚訝和困惑。

準備好改變

  • 爲不可變類型正確實施的等價性將參考等價性與抽象價值的等價性分開,從客戶身上隱藏咱們是否共享價值的決定。 選擇可變類型的行爲而不是觀察等價性有助於避免意外的別名錯誤。
相關文章
相關標籤/搜索