程式扎記: [Scala 小學堂] Scala Gossic : 了解更多 - 使用繼承 (重新定義 equals() 方法)

標籤

2016年7月2日 星期六

[Scala 小學堂] Scala Gossic : 了解更多 - 使用繼承 (重新定義 equals() 方法)

轉載自 這裡 
前言 : 
Scala 是一個可直譯、編譯、靜態、運行於 JVM 之上、可與 Java 互操作、融合物件導向編程特性與函式編程風格的程式語言. Scala 的繼承作了一些限制,這使你在使用繼承前必須多一份思考. 

重新定義 equals() 方法 : 
如果你要重新定義 equals(),必須注意幾個地方,例如,你可能如下定義了 equals() 方法 : 
- Point.scala 代碼 :
  1. class Point(val x: Int, val y: Int) {  
  2.     def equals(that: Point) = this.x == that.x && this.y == that.y  
  3. }  
  4.   
  5. val p1 = new Point(11)  
  6. val p2 = new Point(11)  
  7.   
  8. println(p1.equals(p2))     // 顯示 true  
  9. println(p1 == p2)          // 顯示 false  

不是說重新定義 equals() 實作物件相等性比較,再使用 == 就可以測試物件的實質相等性嗎?但上例中,equals() 的結果是 true,但 == 的結果是false?原因在於,你沒有重新定義繼承下來的 equals(),因為你另外定義了一個接受 Point 型態的 equals() 方法! 事實上,在 Scala 中,重新定義一定要加上 override 關鍵字,以確保你確實重新定義了父類別的某個方法,上例中沒有加上 override,而編譯器沒提出錯誤訊息時,你就要知道你並沒有重新定義父類別的 equals() 方法, 來看看真正有重新定義 equals() 的版本 : 
- Point2.scala 代碼 :
  1. import scala.collection.immutable._  
  2.   
  3. class Point(val x: Int, val y: Int) {  
  4.     override def equals(a: Any) = a match {  
  5.         case that: Point => this.x == that.x && this.y == that.y  
  6.         case _ => false  
  7.     }  
  8. }  
  9.   
  10. val p1 = new Point(11)  
  11. val p2 = new Point(11)  
  12.   
  13. println(p1.equals(p2))      // 顯示 true  
  14. println(p1 == p2)           // 顯示 true  
  15.   
  16. val pSet = HashSet(p1)  
  17. println(pSet.contains(p2))  // 可能顯示 false  

這邊用到了 模式比對Pattern match)中變數模式(Variable pattern)的語法,雖然還沒正式談到,不過並不難,match 語法中會嘗試看看傳入的 a 是否可以被Point 型態所宣告 that 變數參考,如果可以 讓 that 參考至 a 所參考的物件,然後執行 => 之後的程式碼.p1 與 p2 座標都是同一點,所以實際上指的相同的座標,使用 == 測試的結果也是true了,但是 HashSet 中放入的 p1 與要測試的 p2 明明是指同一點,為什麼 contains 測試會有可能顯示false? 因為你在重新定義 equals() 時,並沒有重新定義 hashCode,在許多場合,例如將物件加入群集 (Collection)時,會同時利用 equals() 與 hashCode() 來判斷是否加入的是(實質上)相同的物件. 來看看定義 hashCode 時必須遵守的約定(取自 java.lang.Object hashCode() 說明 ): 
* 在同一個應用程式執行期間,對同一物件呼叫 hashCode 方法,必須回傳相同的整數結果.
* 如果兩個物件使用equals(Object)測試結果為相等, 則這兩個物件呼叫 hashCode 時,必須獲得相同的整數結果.
* 如果兩個物件使用equals(Object)測試結果為不相等, 則這兩個物件呼叫 hashCode 時,可以獲得不同的整數結果.

以 HashSet 為例,會先使用 hashCode 得出該將物件放至哪個雜湊桶(hash buckets)中,如果雜湊桶有物件,再進一步使用 equals() 確定實質相等性,從而確定 Set 中不會有重複的物件。上例中說可能會顯示 false,是因為若湊巧物件 hashCode 算出在同一個雜湊桶,再進一步用 equals() 就有可能出現 true. 在重新定義 equals() 時,最好重新一併重新定義 hashCode. 例如 : 
- Point3.scala 代碼 :
  1. import scala.collection.immutable._  
  2.   
  3. class Point(val x: Int, val y: Int) {  
  4.     override def equals(a: Any) = a match {  
  5.         case that: Point => this.x == that.x && this.y == that.y  
  6.         case _ => false  
  7.     }  
  8.     override def hashCode = 41 * (41 + x) + y  
  9. }  
  10.   
  11. val p1 = new Point(11)  
  12. val p2 = new Point(11)  
  13. val pSet = HashSet(p1)  
  14. println(pSet.contains(p2))   // 顯示為 true  

一個重要的觀念是,定義 equals()  hashCode 時,最好別使用狀態會改變的資料成員. 你可能會想,以這個例子來說,點會移動,如果移動了就不是相同的點了,不是嗎? 假設 xy 是個允許會變動的成員,那麼就會發生這個情況 : 
- Point4.scala 代碼 :
  1. import scala.collection.immutable._  
  2.   
  3. class Point(var x: Int, var y: Int) {  
  4.     override def equals(a: Any) = a match {  
  5.         case that: Point => this.x == that.x && this.y == that.y  
  6.         case _ => false  
  7.     }  
  8.     override def hashCode = 41 * (41 + x) + y  
  9. }  
  10.   
  11. val p1 = new Point(11)  
  12. val pSet = HashSet(p1)  
  13.   
  14. println(pSet.contains(p1))    // 顯示 true  
  15. p1.x = 10  
  16. println(pSet.contains(p1))    // 顯示 false  

明 明是記憶體中同一個物件,但置入 HashSet 後,最後跟我說不包括 p1?這是因為,你改變了x,算出來的 hashCode 也就改變了,使用 contains() 嘗試比對時,會看看新算出來的雜湊桶中是不是有物件,而根本不是置入 p1 的雜湊桶中尋找,結果就是 false了. 再來看看在實作 equals() 時要遵守的約定(取自 java.lang.Object  equals() 說明 ) : 
* 反身性(Reflexive):x.equals(x) 的結果要是true。
* 對稱性(Symmetric): x.equals(y)與y.equals(x)的結果必須相同。
* 傳遞性(Transitive): x.equals(y)、y.equals(z) 的結果都是 true,則 x.equals(z) 的結果也必須是true。
* 一致性(Consistent): 同一個執行期間,對 x.equals(y) 的多次呼叫,結果必須相同。
* 對任何非 null 的x,x.equals(null) 必須傳回 false

目前定義的 Point,其 equals() 方法滿足以上幾個約定(你可以自行寫程式測試)。現在考慮繼承的情況,你要定義 3D 的點 : 
- Point3D.scala 代碼 :
  1. import scala.collection.immutable._  
  2.   
  3. class Point(varl x: Int, val y: Int) {  
  4.     override def equals(a: Any) = a match {  
  5.         case that: Point => this.x == that.x && this.y == that.y  
  6.         case _ => false  
  7.     }  
  8.     override def hashCode = 41 * (41 + x) + y  
  9. }  
  10.   
  11. class Point3D(x: Int, y: Int, val z: Int) extends Point(x, y) {  
  12.     override def equals(a: Any) = a match {  
  13.         case that: Point3D => super.equals(that) && this.z == that.z  
  14.         case _ => false  
  15.     }  
  16. }  
  17.   
  18. val p1 = new Point(11)  
  19. val p2 = new Point3D(111)  
  20. println(p1 == p2)            // 顯示 true  
  21. println(p2 == p1)            // 顯示 false  

結 果該是 true 或 false 需要討論一下。3D 的點與 2D 的點是否相等呢?假設你考慮的是點投射在 xy 平面上是否相等,那 p1 == p2 為 true就可以接受,在此假設之下,再來看 p2 == p1 為false,這違反 equals() 對稱性的對稱性合約。如果你要滿足對稱性,則要作個修改 : 
- Point3D_2.scala 代碼 :
  1. import scala.collection.immutable._  
  2.   
  3. class Point(val x: Int, val y: Int) {  
  4.     override def equals(a: Any) = a match {  
  5.         case that: Point => this.x == that.x && this.y == that.y  
  6.         case _ => false  
  7.     }  
  8.     override def hashCode = 41 * (41 + x) + y  
  9. }  
  10.   
  11. class Point3D(x: Int, y: Int, val z: Int) extends Point(x, y) {  
  12.     override def equals(a: Any) = a match {  
  13.         case that: Point3D => super.equals(that) && this.z == that.z  
  14.         case that: Point => that == this  
  15.         case _ => false  
  16.     }  
  17. }  
  18.   
  19. val p1 = new Point(11)  
  20. val p2 = new Point3D(111)  
  21. val p3 = new Point3D(112)  
  22. println(p1 == p2)              // 顯示為 true  
  23. println(p2 == p1)              // 顯示為 true  
  24. println(p1 == p3)              // 顯示為 true  
  25. println(p2 == p3)              // 顯示為 false  

p1 等於 p2,p2 等於 p1,這符合對稱性合約了。但 p2 等於 p1,p1 等於 p3,但 p2 不等於 p3,這違反傳遞性合約。問題點在於,2D 的點並沒有 z 軸資訊,無論如何也沒辦法滿足傳遞性了. 一般來說,對於不同的類別實例,會將之視為不同,基本上你可以這麼設計 : 
- Point3D_3.scala 代碼 :
  1. class Point(val x: Int, val y: Int) {  
  2.     override def equals(a: Any) = a match {  
  3.         case that: Point => this.getClass == that.getClass &&   
  4.                             this.x == that.x && this.y == that.y  
  5.         case _ => false  
  6.     }  
  7.     override def hashCode = 41 * (41 + x) + y  
  8. }  
  9.   
  10. class Point3D(x: Int, y: Int, val z: Int) extends Point(x, y) {  
  11.     override def equals(a: Any) = a match {  
  12.         case that: Point3D => super.equals(that) && this.z == that.z  
  13.         case _ => false  
  14.     }  
  15. }  

直接判斷類別,讓不同類別的實例視為不相等,就這個例子而言,使得 Point 只能與 Point 比,Point3D 只能與 Point3D 比,直接解決了不同繼承階層下 equals() 的合約問題. 不過在以下這種需求時,這樣的定義也許不符合你的需求 : 
val p1 = new Point(1, 1)
val p2 = new Point(1, 1) { override def toString = "(" + x + ", " + y + ")" }
println(p1 == p2)
 // 顯示 false

你也許是在某處建立了個匿名類別物件,然後在程式中某處又打算測試看看與 p1 是否相等,但結果並不是顯示 true,這是因為你嚴格地在 equals() 中檢查了實例的類別名稱. 你可以將定義改為以下 : 
- Point3D_4.scala 代碼 :
  1. import scala.collection.immutable._  
  2.   
  3. class Point(val x: Int, val y: Int) {  
  4.     override def equals(a: Any) = a match {  
  5.         case that: Point => that.canEquals(this) &&  
  6.                             this.x == that.x && this.y == that.y  
  7.         case _ => false  
  8.     }  
  9.     override def hashCode = 41 * (41 + x) + y  
  10.   
  11.     def canEquals(that: Any) = that.isInstanceOf[Point]  
  12. }  
  13.   
  14. class Point3D(x: Int, y: Int, val z: Int) extends Point(x, y) {  
  15.     override def equals(a: Any) = a match {  
  16.         case that: Point3D => that.canEquals(this) &&  
  17.                               super.equals(that) && this.z == that.z  
  18.         case _ => false  
  19.     }  
  20.     override def hashCode = 41 * super.hashCode + z  
  21.     override def canEquals(that: Any) = that.isInstanceOf[Point3D]  
  22. }  
  23.   
  24. val p1 = new Point(11)  
  25. val p2 = new Point(11) { override def toString = "(" + x + ", " + y + ")" }  
  26. println(p1 == p2)           // 顯示 true  
  27. val pSet = HashSet(p1)  
  28. println(pSet.contains(p2))  // 顯示 true  

在 equals() 中,你不僅檢查傳入的實例是否為 Point,也反過來讓傳入的實例取得 this 的型態進行測試(這是 Visitor 模式 的實現)。如果 p1 是 Point 物件,而 p2 是 Point3D 物件,p1.equals(p2) 時,由於傳入的實例可以取得 this 的型態進行測試,p2 反過來測試 p1 是不是Point3D,結果不是,所以 equals() 傳回false,利用這個方式,讓有具體名稱的子類別實例,不會與父類別實例有相等成立的可能性。如果是直接繼承 Point 類別的匿名類別物件,則直接繼承 canEquals() 方法,由於匿名類別物件還是一種 Point 實例,因此equals() 的結果會是 true.

沒有留言:

張貼留言

網誌存檔

關於我自己

我的相片
Where there is a will, there is a way!