最近和同事談到equals和==的差別。這其實是個非常老套簡單的問題,但當你要親自覆蓋equals方法時,才發現,有一些你不知道卻又不得不知道的事。覆蓋equals,講究很多。儘管Object是一個很具體的類,但他的主要作用還是為了擴展。他的所有非final方法都有明確的通用約定。因為他們被設計成要被覆蓋的方法。任何一個類,在覆寫equals、hashCode、toString、clone、finalize時,都有責任遵守這些方法的通用約定。如果不能做到這一點,那麼當多個類別組合時將難以發揮我們期望的效果。
覆寫equals方法看起來簡單,但是有許多覆寫方法會導致錯誤。最容易避免這種錯誤的方法就是不覆寫equals,這種情況下每個類別的實例都只與自身相等。那麼在什麼情況下我們可以選擇不覆寫equals方法呢?
類別的每個實例本質上是唯一的
對於代表活動實體而不是值的類別確實如此,例如每個執行緒實例。我們用equals方法比較他是毫無意義的,因為每個線程都是唯一的。在這種情況下,我們不用覆寫equals方法,因為Object類別中的equals已經完全夠用了。
Object類別中equals方法實作:
public boolean equals(Object obj) { return (this == obj); }登入後複製登入後複製
#不關心類別是否需要邏輯相等的判斷
有些類別是一些“數值類別”,比較大小,數學運算是這些類別的本職工作。在這種情況下,需要我們將類別中存放的數值進行比較,需要進行邏輯相等的判斷。除此之外的類,很大部分類別沒有「一個是否等於另外一個」的概念。這種不關心邏輯相等的類別不需要覆寫equals方法。
超類別實作的equals對子類別適用
舉個例子,繼承了AbstractSet類別的HashSet類別在equals方法上並沒有任何區別,那麼HashSet直接使用AbstractSet的equals方法即可。
與上面的內容相反的,我們需要覆寫equals方法的情況就是:如果類別具有自己的邏輯相等的概念,並且父類沒有進行可用的equals方法覆蓋。這時需要我們親自進行覆蓋。
equals方法實現了等價關係。離散數學中學過等價關係的概念,對於一個R上的二元關係,如果它滿足自反對稱和傳遞,那麼他就是等價的。讓我們來具體分析一下equals和這三種性質的關係。
自反性 : 對於任何非null的參考值x,x.equals(x)必須傳回true
對稱性 : 對於任何非null的參考值x和y,當且僅當y.equals(x)回傳true時,x.equals(y)必定傳回true
傳遞性 : 對於任何非null的參考值x、y和z,如果x.equals(y)回傳true、y.equals(z)也回傳true,那麼x.equals(z)必定回傳true。
自反:∀ a ∈A, => (a, a) ∈ R
對稱性:(a, b) ∈R∧ a ≠ b => (b, a) ∈R
傳遞性:(a, b)∈R,(b, c)∈R =>(a, c)∈R
對比這三個性質,沒有任何問題。由此可得,equals方法實現了等價關係。
Object的equals方法只是簡單的看看位址,這顯然不可能滿足我們的要求。那麼在自己編寫equals方法進行覆蓋時如何保證編寫出高品質的,邏輯比較的方法呢? equals的編寫可以概括為下面四個步驟:
1.使用==運算子檢查參數是否只是物件的參考
如果結果相等則傳回true,說明x與y是一個物件的不同引用,不需要再進行判斷了。
2.使用instanceof運算子檢查參數是否類型正確
如果結果不是正確的類型則傳回false,因為我們的equals方法是繼承自Object類別的,所以參數的型別無法避免的是Object,我們先使用instanceof對參數做型別判斷,如果型別都不正確,就不用進行下一步判斷了。
3.把參數轉換成正確的型別
因為之前做了偵測,所以這一步的型別轉換沒有問題。
4.對每個類別中需要邏輯比較的域值進行判斷
已經確保x和y是相同類型的不同實例,將需要判斷的邏輯比較的域值取出進行比較判斷即可。如果全部正確則回傳true,否則傳回false。
當寫完equals方法後,一定要反覆的判斷是否符合自反性,對稱性和傳遞性。不僅僅如此,在確保等價的情況下,編寫equals方法時還有一些值得注意的事情,我們需要以此改進。
覆寫equals方法時總是要覆寫hashCode方法
如果我們在寫有關雜湊的類別時,必須在覆寫equals方法時覆寫hashCode方法。因為在散列表中,邏輯相同的物件應該具有相同的雜湊碼。舉個比較簡單的例子:將String存入HashSet,有可能兩個內容相同的String字串用==判斷為false,但是他們在HashSet中只存在了一份。這就是因為邏輯相同的String擁有這相同的hashCode。
普遍性的,如果你為你的類別覆寫了equals方法,那麼證明在某種情況下會有兩個不同物件是邏輯相等的。此時如果與雜湊相關,那麼這兩個物件需要相同的hashCode。所以覆寫equals方法時總是要覆寫hashCode方法。
不要讓equals方法過於智慧
如果我們只是簡單的按照上面的實作流程來寫equals的方法,既符合規定也不會導致奇怪的錯誤。但如果我們非要去追求各種、花俏的等價關係,而把程式碼搞得臃腫不堪,那麼既違反了高內聚的初衷,也會讓程式碼出一些莫名其妙的錯誤。
不要將equals方法的參數類型弄錯
說出來可能感覺好笑,但這確實是會發生的情況。修改了參數類型之後的equals方法已完全於Object類別沒有了關係,編譯器不會報錯,留下的只是給程式設計師無盡的頭痛。如果無法意識到參數類型是Object,很有可能花幾個小時也搞不清楚程式為什麼不能正常運作。
最近和同事談到equals和==的差別。這其實是個非常老套簡單的問題,但當你要親自覆蓋equals方法時,才發現,有一些你不知道卻又不得不知道的事。覆蓋equals,講究很多。儘管Object是一個很具體的類,但他的主要作用還是為了擴展。他的所有非final方法都有明確的通用約定。因為他們被設計成要被覆蓋的方法。任何一個類,在覆寫equals、hashCode、toString、clone、finalize時,都有責任遵守這些方法的通用約定。如果不能做到這一點,那麼當多個類別組合時將難以發揮我們期望的效果。
覆寫equals方法看起來簡單,但是有許多覆寫方法會導致錯誤。最容易避免這種錯誤的方法就是不覆寫equals,這種情況下每個類別的實例都只與自身相等。那麼在什麼情況下我們可以選擇不覆寫equals方法呢?
類別的每個實例本質上是唯一的
對於代表活動實體而不是值的類別確實如此,例如每個執行緒實例。我們用equals方法比較他是毫無意義的,因為每個線程都是唯一的。在這種情況下,我們不用覆寫equals方法,因為Object類別中的equals已經完全夠用了。
Object類別中equals方法實作:
public boolean equals(Object obj) { return (this == obj); }登入後複製登入後複製
#不關心類別是否需要邏輯相等的判斷
有些類別是一些“數值類別”,比較大小,數學運算是這些類別的本職工作。在這種情況下,需要我們將類別中存放的數值進行比較,需要進行邏輯相等的判斷。除此之外的類,很大部分類別沒有「一個是否等於另外一個」的概念。這種不關心邏輯相等的類別不需要覆寫equals方法。
超類別實作的equals對子類別適用
舉個例子,繼承了AbstractSet類別的HashSet類別在equals方法上並沒有任何區別,那麼HashSet直接使用AbstractSet的equals方法即可。
與上面的內容相反的,我們需要覆寫equals方法的情況就是:如果類別具有自己的邏輯相等的概念,並且父類沒有進行可用的equals方法覆蓋。這時需要我們親自進行覆蓋。
equals方法實現了等價關係。離散數學中學過等價關係的概念,對於一個R上的二元關係,如果它滿足自反對稱和傳遞,那麼他就是等價的。讓我們來具體分析一下equals和這三種性質的關係。
自反性 : 對於任何非null的參考值x,x.equals(x)必須傳回true
對稱性 : 對於任何非null的參考值x和y,當且僅當y.equals(x)回傳true時,x.equals(y)必定傳回true
傳遞性 : 對於任何非null的參考值x、y和z,如果x.equals(y)回傳true、y.equals(z)也回傳true,那麼x.equals(z)必定回傳true。
自反:∀ a ∈A, => (a, a) ∈ R
對稱性:(a, b) ∈R∧ a ≠ b => (b, a) ∈R
傳遞性:(a, b)∈R,(b, c)∈R =>(a, c)∈R
對比這三個性質,沒有任何問題。由此可得,equals方法實現了等價關係。
Object的equals方法只是簡單的看看位址,這顯然不可能滿足我們的要求。那麼在自己編寫equals方法進行覆蓋時如何保證編寫出高品質的,邏輯比較的方法呢? equals的編寫可以概括為下面四個步驟:
1.使用==運算子檢查參數是否只是物件的參考
如果結果相等則傳回true,說明x與y是一個物件的不同引用,不需要再進行判斷了。
2.使用instanceof運算子檢查參數是否類型正確
如果結果不是正確的類型則傳回false,因為我們的equals方法是繼承自Object類別的,所以參數的型別無法避免的是Object,我們先使用instanceof對參數做型別判斷,如果型別都不正確,就不用進行下一步判斷了。
3.把參數轉換成正確的型別
因為之前做了偵測,所以這一步的型別轉換沒有問題。
4.對每個類別中需要邏輯比較的域值進行判斷
已經確保x和y是相同類型的不同實例,將需要判斷的邏輯比較的域值取出進行比較判斷即可。如果全部正確則回傳true,否則傳回false。
當寫完equals方法後,一定要反覆的判斷是否符合自反性,對稱性和傳遞性。不僅僅如此,在確保等價的情況下,編寫equals方法時還有一些值得注意的事情,我們需要以此改進。
覆寫equals方法時總是要覆寫hashCode方法
如果我們在寫有關雜湊的類別時,必須在覆寫equals方法時覆寫hashCode方法。因為在散列表中,邏輯相同的物件應該具有相同的雜湊碼。舉個比較簡單的例子:將String存入HashSet,有可能兩個內容相同的String字串用==判斷為false,但是他們在HashSet中只存在了一份。這就是因為邏輯相同的String擁有這相同的hashCode。
普遍性的,如果你為你的類別覆寫了equals方法,那麼證明在某種情況下會有兩個不同物件是邏輯相等的。此時如果與雜湊相關,那麼這兩個物件需要相同的hashCode。所以覆寫equals方法時總是要覆寫hashCode方法。
不要讓equals方法過於智慧
如果我們只是簡單的按照上面的實作流程來寫equals的方法,既符合規定也不會導致奇怪的錯誤。但如果我們非要去追求各種、花俏的等價關係,而把程式碼搞得臃腫不堪,那麼既違反了高內聚的初衷,也會讓程式碼出一些莫名其妙的錯誤。
不要將equals方法的參數類型弄錯
說出來可能感覺好笑,但這確實是會發生的情況。修改了參數類型之後的equals方法已完全於Object類別沒有了關係,編譯器不會報錯,留下的只是給程式設計師無盡的頭痛。如果無法意識到參數類型是Object,很有可能花幾個小時也搞不清楚程式為什麼不能正常運作。
以上是關於java覆蓋equals更深層的方法概述的詳細內容。更多資訊請關注PHP中文網其他相關文章!