Here we will discuss how to create and define some object equality design principle.
java equality is
Scala Equality
and the global == method is as such .
final def == (that : Any) : Boolean = if (null eq this) { null eq that } else { this equals that }
common pitfall that can cause inconsistent behavior when overriding equals.
we will check the following comon pitfall.
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 // trueHowever, 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) : BooleanA 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) }
// 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
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).
First let check on the equivalence relation on non-null objects.
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 // falseyou 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 falsemake 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 // trueOne 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.
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 foundto 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[_]] }
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 }