Scala - Object Equality

Here we will discuss how to create and define some object equality design principle. 

java equality is 


  1. == : natual equality for value types, and object identity for references types. 
  2. equals: which is (user-defined) canonical equality for reference types. 
while the scala equality 


Scala Equality 

  1. eq: the object equality 
  2. ==: equality is reserved in Scala for the "natural" equality of each type. for value types, == is value comparison , just like Java, For reference types, the == is the same as equals in Scala. , redefinable.

and the global == method is as such  .


final def == (that : Any) : Boolean = 
  if (null eq this) { null eq that } else { this equals that }

Writing an equality method 


common pitfall that can cause inconsistent behavior when overriding equals. 

we will check the following comon pitfall. 


  1. Defining equals with wrong signature 
  2. Changing equals without also changing hashCode
  3. Defining equals in terms of mutalbe fields
  4. Failing to define equals as an equvilance relation 

Pitfall #1 Defining equals with wrong signature 


class Point(val x : Int, val y : Int)
{
	def equals(other : Point) : Boolean = 
	  this.x == other.x && this.y == other.y
}
well, seemly rigth?



// a seemly right choice ?
val p1, p2 = new Point(1, 2)

val q = new Point(2, 3)

p1 equals p2 // true 
p2 equals p1 // true
However, pitfall happens when you put element into collection. 



// however, pitfall, happens when you put element into collections
//
import scala.collection.mutable._

val coll = HashSet(p1)

coll contains p2 // return false .. what if the HashSet[Point]

val coll2 = HashSet[Point](p1)

coll contains p2 // return false .. what if the HashSet[Point]
reason shown in the code below. 



// reason: 
val p2a : Any = p2
p1 equals p2a
// reason is that equals defined in Any 
def equals(other : Any) : Boolean 
A better definitions, but stil not perfect. 



// a Better definitions, but still not perfect
class Point(val x : Int, val y : Int)
{
	def equals(other : Point) : Boolean = 
	  this.x == other.x && this.y == other.y
	
    override def equals(other : Any) = other match { 
	  case that : Point => this.x == that.x && this.y == that.y 
	  case _ => false 
	}
}
another pitfall is that you define == method, but with wrong signature. 



// another pitfall is that you define == method, but this time with wrong signature , that overload will not be loaded. 
class Point(val x : Int, val y : Int)
{
	def equals(other : Point) : Boolean = 
	  this.x == other.x && this.y == other.y
	
    override def equals(other : Any) = other match { 
	  case that : Point => this.x == that.x && this.y == that.y 
	  case _ => false 
	}
	
	def == (other : Point) : Boolean = equals(other)
}

Pitfall #2: Changing equals without also changing hashCode  



// If two object are equal according to the equals method , then calling the hashCode method on each of the two object must produce the same integer result. 
//
class Point(val x : Int, val y : Int)
{
	def equals(other : Point) : Boolean = 
	  this.x == other.x && this.y == other.y
	
    override def equals(other : Any) = other match { 
	  case that : Point => this.x == that.x && this.y == that.y 
	  case _ => false 
	}
	
	override def hashCode  = 41 * (41 + x) + y
}
and now you may find that HashSet now works with the Point object. 



val p1, p2 = new Point(1, 2)
HashSet(p1) contains p2 // run with overriden hashCode , the result is true 

Pitfall #3: Defining equals in terms of mutable fields   



class Point(var x : Int, var y : Int)
{
	def equals(other : Point) : Boolean = 
	  this.x == other.x && this.y == other.y
	
    override def equals(other : Any) = other match { 
	  case that : Point => this.x == that.x && this.y == that.y 
	  case _ => false 
	}
	
	override def hashCode  = 41 * (41 + x) + y
}
as you can see that x and y are now vars... 



val p = new Point(1, 2)
val coll = HashSet(p)
coll contains p  // return true

p.x += 1

coll contains p // we lose p 

coll.iterator contains p // yet, we find it, strange?
you see the problem, that calc hashCode on mutable field has dangerous on that the value may change and that can cause unexpected result on collection operation. 


a tip is to if you want to take into account those depends on state, define a method named equalsContents (rather than equals).

Pitfall #4: Failing to define equals as an equivalent relation 

First let check on the equivalence relation on non-null objects. 


  1. it is reflexive: for any non-null value x, the expression x.equals(x) should return true 
  2. it is symmetric: for any non-null value x and y, x.equals(y) should return true if and only if y.equals(x) return true. 
  3. it is transitive: for any non-null value x, y , z, x.equals(y) returns true, and y.equals(z) return true, then x.equals(z) return true.   
  4. it is consistent: for any non-null value x, multiple invocations of x.equals(y) should consistently return true or consistently return false, provided no information used in equals comparisoin on the object is modified. 
  5. for any non-null value, x.equals(null) should return false
now, supposet that we have new Point extended class, which has a third member that is called Color. 



object Color extends Enumeration { 
   val Red, Orange, Yellow, Green, Blue, Indigo, Violet  = Value
}

// equals is stricter than hashCode 
class ColoredPoint(x : Int, y : Int, val color : Color.Value) extends Pint(x, y) { 
  // problem: Equals not symmetric 
    override def equals(other : Any) = other match { 
      case that : ColoredPoint => 
        this.color == that.color && super.equals(that)
      case _ => false
	}
}
 examples that symmetric is broke.



// examples shows that symmetric is broken 
val p = new Point(1, 2)
val cp = new ColoredPoint(1, 2, Color.Red)
p equals cp // return true 
cp equals p // return false  
HashSet[Point](p) contains cp // true 
HashSet[Point](cp) contains p // false
you can make the equals method stricter. 



// makes the equals method stricter
class ColoredPoint(x : Int, y : Int, val color : Color.Value) extends Point(x, y) { 
  // problem: transitivity is violated
    override def equals(other : Any) = other match { 
      case that : ColoredPoint => 
        this.color == that.color && super.equals(that)
      case that : Point => 
        that equals this 
      case _ => false
	}
}
with that transitivity is broken .



// check the transitivity is broken
//
val redp = new ColoredPoint(1, 2, Color.Red)
val bluep = new ColoredPoint(1, 2, Color.Blue)
redp == p // return true 
p == bluep // return true 
redp == bluep // return false 
make it even stricter. 



// A technically valid, but unsatisfying equals method 
class Point(val x : Int, val y : Int)
{
    override def equals(other : Any) = other match { 
	  case that : Point => this.x == that.x && this.y == that.y && this.getClass == that.getClass
	  case _ => false 
	}
	
	override def hashCode  = 41 * (41 + x) + y
}

class ColoredPoint(x : Int, y : Int, val color : Color.Value) extends Point(x, y) { 
  // problem: transitivity is violated
    override def equals(other : Any) = other match { 
      case that : ColoredPoint => 
        this.color == that.color && super.equals(that)
      case _ => false
	}
}


however, that strictness has some limitation 

// the following point, does not equal to Point(1, 2), but it should be ...
val pAnon = new Point(1, 1) { override val y = 2 }
solution is to define a canEqual method. 

// def canEqual(other : Any) : Boolean 
//
class Point(val x : Int, val y : Int)
{
    override def equals(other : Any) = other match { 
	  case that : Point => (that canEqual this) && this.x == that.x && this.y == that.y && this.getClass == that.getClass
	  case _ => false 
	}
	
	override def hashCode  = 41 * (41 + x) + y
	
	def canEqual(other: Any) = other.isInstanceOf[Point] // all instance of Point can equal to Point.
}

class ColoredPoint(x : Int, y : Int, val color : Color.Value) extends Point(x, y) { 
  // problem: transitivity is violated
    override def equals(other : Any) = other match { 
      case that : ColoredPoint => 
        (that canEqual this) && 
        this.color == that.color && super.equals(that)
      case _ => false
	}
    
    override def canEqual(other : Any) = other.isInstanceOf[Point] // all instance of Point can equal to ColoredPiont .
}
now, we can test it .

val p = new Point(1, 2)
val cp = new ColoredPoint(1, 2, Color.Indigo)
val pAnon = new Point(1, 1) { override val y = 2}
val coll = List(p)
coll contains p     // true 
coll contains cp    // false
coll contains pAnon // true 
 One potential criticism of canEqual approach is that it violate the Liskov Substitution Principle (LSP)..  while LSP states you should be able to use (Substitute) a subclass instance where a superclass instance is required. 

 Scala has different interpretation of Liskov rule that a subclass behaves identical to super class, but state that it behaves in a way that fullfil the contract of its superclass. 


Defininig Equality for parameterized types 

things become a bit complicated and need to be adapted a little bit. 

trait Tree[+T] {
  def elem : T
  def left : Tree[T]
  def right : Tree[T]
}

object EmptyTree extends Tree[Nothing] { 
  def elem = throw new NoSuchElementException("EmptyTree.elem")
  def left = throw new NoSuchElementException("EmptyTree.left")
  def right = throw new NoSuchElementException("EmptyTree.right")
}

class Branch[+T] (
    val elem : T,
    val left : Tree[T],
    val right : Tree[T]
) extends Tree[T]
now, we add equals method and hashCode method. 

// add the equals method and hasCode method 
class Branch[T](
  val elem : T, 
  val left : Tree[T],
  val right : Tree[T]
) extends Tree[T] {
  override def equals (other : Any) = other match { 
    case that : Branch[T] => this.elem == that.elem &&
                             this.left == that.left && 
                             this.right == that.right 
    case _ => false 
  }
}
however, when you compile it , you get an error. 

 argument T in type pattern Branch[T] is unchecked since it is eliminated by erasure
    case that : Branch[T] => this.elem == that.elem &&
                ^
 one warning found
to fix it, a tiny change is 

// because T is unchecked , and a tiny change to do (remember that we used to say lower-cased variable is for variable-binding (nit-picking, it is an type variable binding) , as opposed to the Constant binding on upper-cased)//
class Branch[T](
  val elem : T, 
  val left : Tree[T],
  val right : Tree[T]
) extends Tree[T] {
  override def equals (other : Any) = other match {
    // lower case t in place of upper case T
    //
    case that : Branch[t] => this.elem == that.elem &&
                             this.left == that.left && 
                             this.right == that.right 
    case _ => false 
  }
}
since t is a variable type binding. we can as well just use a wild-card (_)...

// since that that lower cased t binds to any types, that has the same implication as binding to nothing. 
//  t => _
class Branch[T](
  val elem : T, 
  val left : Tree[T],
  val right : Tree[T]
) extends Tree[T] {
  override def equals (other : Any) = other match {
    // lower case t in place of upper case T
    // Branch[_] is a shorthand for a so-called existential type, which is roughly speaking a type with some unknown parts in it.
    case that : Branch[_] => this.elem == that.elem &&
                             this.left == that.left && 
                             this.right == that.right 
    case _ => false 
  }
  
  override def hashCode : Int =
    	41 * (
          41 * (
            41 + elem.hashCode    
          ) + left.hashCode
    	) + right.hashCode
    	
  def canEqual(other : Any) = other match {
    case that : Branch[_] => true
    case _ => false 
  }
  
  // an alternative definition of canEqual can be written as such 
//    def canEqual(other : Any) =  other.isInstanceOf[Branch[_]]
}

Recipe for Equals and Hashcode

the following examples shows you an recipe that to define equals and hashCode method.  

class Rational(n : Int, d : Int) {
  require (d != 0)

  private val g = gcd (n.abs, d.abs)
  val number = (if (d < 0) -n else n) / g
  val denom = d.abs / g
  
  private def gcd(a : Int, b : Int) : Int = if (b == 0) a else gcd(b, a % b)
  
  override def equals(other : Any) : Boolean  = {
    other match { 
      case that : Rational => 
        (that canEqual this) && 
        number == that.number && 
        denom == that.denom 
      case _ => false 
    }
  }
  
  def canEqual(other : Any) : Boolean = other.isInstanceOf[Rational]
  
  override def hashCode : Int = 
    41 * (
      41 + number    
    ) + denom
    
  override def toString = 
    if (denom == 1) number.toString else number + "/" + denom
}


你可能感兴趣的:(scala)