什麼是等價性?爲何要討論等價性?
三種等價性的方式
==與equals()
不可變類型的等價性
對象契約
可變類型的等價性
自動包裝和等價性java
ADT上的相等操做程序員
ADT是經過建立以操做爲特徵的類型而不是其表示的數據抽象。
對於抽象數據類型,抽象函數(AF)解釋瞭如何將具體表示值解釋爲抽象類型的值,而且咱們看到了抽象函數的選擇如何決定如何編寫實現每一個ADT操做的代碼。
抽象函數(AF)提供了一種方法來清晰地定義ADT上的相等操做。編程
數據類型中值的相等性?數組
在物質世界中,每一個物體都是不一樣的 - 即便兩個雪花的區別僅僅是它們在太空中的位置,在某種程度上,即便是兩個雪花也是不一樣的。
因此兩個實體對象永遠不會真正「相等」。 他們只有類似的程度。
然而,在人類語言的世界中,在數學概念的世界中,對同一事物能夠有多個名稱。安全
使用AF或使用關係數據結構
使用抽象函數。 回想一下抽象函數f:R→A將數據類型的具體實例映射到它們相應的抽象值。 爲了使用f做爲等價性的定義,咱們說當且僅當f(a)= f(b)時等於b。
使用關係。 等價關係是E⊆T x T,即:ide
等價關係:自反,對稱,傳遞
這兩個概念是等價的。函數
使用觀察性能
咱們能夠談論抽象價值之間的等價性的第三種方式就是外部人(客戶)能夠觀察他們的狀況
使用觀察。 咱們能夠說,當兩個對象沒法經過觀察進行區分時,這兩個對象是相同的 - 咱們能夠應用的每一個操做對兩個對象都產生相同的結果。站在外部觀察者角度測試
就ADT而言,「觀察」意味着調用對象的操做。 因此當且僅當經過調用抽象數據類型的任何操做不能區分它們時,兩個對象是相等的。
Java有兩種不一樣的操做,用於測試相等性,具備不一樣的語義。
它測試引用等價性。 若是它們指向內存中的相同存儲,則兩個引用是==。 就快照圖而言,若是它們的箭頭指向相同的對象氣泡,則兩個引用是==。
換句話說,對象等價性。
必須爲每一個抽象數據類型適當地定義equals操做。在自定義ADT時,須要重寫對象的equals()方法
==運算符與equals方法
對於基本數據類型,您必須使用==對基本數據類型,使用==斷定相等
對於對象引用類型對象類型,使用equals()
重寫方法的提示
若是你想覆蓋一個方法:
equals()方法由Object定義,其默認含義與引用相等相同。在對象中實現的缺省equals()方法是在判斷引用等價性
對於不可變的數據類型,這幾乎老是錯誤的。
咱們必須重寫equals()方法,將其替換爲咱們本身的實現。
重寫與重載
在方法簽名中犯一個錯誤很容易,而且當您打算覆蓋它時重載一個方法。
只要你的意圖是在你的超類中重寫一個方法,就應該使用Java的批註@Override。
經過這個註解,Java編譯器將檢查超類中是否存在具備相同簽名的方法,若是簽名中出現錯誤,則會給出編譯器錯誤。
instanceof
instanceof運算符測試對象是不是特定類型的實例。
使用instanceof是動態類型檢查,而不是靜態類型檢查。
通常來講,在面向對象編程中使用instanceof是一種陋習。 除了實施等價性以外,任何地方都應該禁止。
這種禁止還包括其餘檢查對象運行時類型的方法。
對象中equals()的契約
您重寫equals()方法時,您必須遵照其整體契約:
Equals契約
equals方法實現等價關係:
equals是全部對象的全局等價關係。
打破等價關係
咱們必須確保由equals()實現的等價性定義其實是一個前面定義的等價關係:自反,對稱和傳遞。
打破哈希表
散列表是映射的表示:將鍵映射到值的抽象數據類型。
哈希表如何工做:
哈希表的rep不變量包含密鑰在由其哈希碼肯定的時隙中的基本約束。
散列碼的設計使密鑰均勻分佈在索引上。
但偶爾會發生衝突,而且兩個鍵被放置在相同的索引處。
所以,不是在索引處保存單個值,而是使用哈希表實際上包含一個鍵/值對列表,一般稱爲哈希桶。
一個鍵/值對在Java中被簡單地實現爲具備兩個字段的對象。
插入時,您將一對添加到由散列碼肯定的陣列插槽中的列表中。
對於查找,您散列密鑰,找到正確的插槽,而後檢查每一個對,直到找到其中的密鑰等於查詢密鑰的對。
hashCode契約
只要在應用程序執行過程當中屢次調用同一對象時,只要修改了對象的等值比較中未使用的信息,hashCode方法就必須始終返回相同的整數。
若是兩個對象根據equals(Object)方法相等,則對這兩個對象中的每一個對象調用hashCode方法必須產生相同的整數結果。等價的對象必須有相同的的hashCode
可是,程序員應該意識到,爲不相等的對象生成不一樣的整數結果可能會提升散列表的性能。不相等的對象,也能夠映射爲一樣的的hashCode,但性能會變差
相等的對象必須具備相同的散列碼
不相等的對象應該有不一樣的哈希碼
除非對象發生變化,不然散列代碼不能更改
重寫hashCode()
確保合約知足的一個簡單而激烈的方法是讓hashCode始終返回一些常量值,所以每一個對象的散列碼都是相同的。
標準是計算用於肯定相等性的對象的每一個組件的哈希代碼(一般經過調用每一個組件的hashCode方法),而後組合這些哈希碼,引入幾個算術運算。
打破哈希表
爲何對象合同要求相同的對象具備相同的哈希碼?
Object的默認hashCode()實現與其默認的equals()一致:
重寫hashCode()
Java的最新版本如今有一個實用程序方法Objects.hash(),能夠更容易地實現涉及多個字段的哈希碼。
請注意,若是您根本不重寫hashCode(),您將從Object得到一個Object,該Object基於對象的地址。
若是你有等價性的權利,這將意味着你幾乎確定會違反合同
通常規則:
覆蓋equals()時老是覆蓋hashCode()。
等價性:當兩個對象沒法經過觀察區分時,它們是等價的。
對於可變對象,有兩種解釋方法:
觀察等價性:在不改變狀態的狀況下,兩個可變對象是否看起來一致
行爲等價性:調用對象的任何方法都展現出一致的結果
注意:對於不可變的對象,觀察和行爲的等價性是相同的,由於沒有任何變值器方法。
Java中的可變類型的等價性
對可變類型來講,每每傾向於實現嚴格的觀察等價性
可是使用觀察等價性致使微妙的錯誤,而且事實上容許咱們輕易地破壞其餘集合數據結構的表明不變量。但在有些時候,觀察等價性可能致使錯誤,甚至可能破壞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()的最終規則
對於不可變類型:
對於可變類型:
對象中的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)的一部分。
減小錯誤保證安全
容易明白
準備好改變