Object Equality in Scala

Ruining (Ray) Li
3 min readApr 29, 2021

Three equality comparisons: ==, equals, eq

  • == is exactly the same as eq.
// Definition of == in class Any:
final def == (that: Any): Boolean =
if (null eq this) {null eq that} else {this equals that}
  • eq is for reference equality, equals is for reference equality by default, but can be customized to more natural notions of equality.

Contracts between equals and hashCode

  • If two objects are equal according to the equals method, then calling the hashCode method on each of the two objects must produce the same integer result.
  • equals should be an equivalence relation: reflexive, transitive, symmetric, consistent.

Common Pitfalls of writing equals method

class Point(val x: Int, val y: Int) { ... }

1. Defining equals with the wrong signature

def equals(other: Point): Boolean = 
this.x == other.x && this.y == other.y
val p1, p2 = new Point(1, 2)
val p2a: Any = p2
p1 equals p2 // true
p1 equals p2a // false

Reason:

  • equals here is actually overloading, not overriding — because in Any equals method takes a parameter of class Any.
  • p2a is of class Any, thus the generic equals in the Any class is executed, which by default is for reference equality.

Modification:

override def equals(other: Any): Boolean = other match {
case that: Point => this.x == that.x && this.y == that.y
case _ => false
}

2. Changing equals without also changing hashCode

val p1, p2 = new Point(2,3)
collection.mutable.hashSet(p1) contains p2 // false

Reason:

  • Without changing hashCode method (which by default is based on the reference), p1 and p2 go into different “hash buckets.”
  • Thus, nothing equals p2 in the bucket containing p2.

Modification:

class Point(val x: Int, val y: Int) {
override equals(other: Any): Boolean = ......
override hashCode: Int = (x, y).## // ## is synonym for hashCode
}

3. Defining equals in terms of mutable fields

class Point(var x: Int, var y: Int) { ...... }val p = new Point(1, 2)
val coll = collection.mutable.HashSet(p)
coll contains p // true
p.x += 1
coll contains p // false
coll.iterator contains p // true

It’s very confusing and misleading! ⬆️

Reason:

  • After changing p.x, p goes into a different “hash bucket” because its hashCode changes.
  • In that new “hash bucket,” no item equals p.

AVOID DEFINING EQUALS IN TERMS OF MUTABLE FIELDS!!!

4. Failing to define equals as an equivalence relation

class ColoredPoint(x:Int, y:Int, val color:Color.value) extends Point(x,y) {
override def equals(other: Any) = other match {
case that: ColoredPoint =>
this.color == that.color && super.equals(that)
case _ => false
}
}
val p = new Point(1,2)
val cp = new ColoredPoint(1, 2, Color.Red)
p equals cp // true
cp equals p // false

Not symmetric! ⬆️

Modification: Add a canEqual method!

// Inside Point classoverride def equals(other: Any) = other match {
case that: Point =>
(that canEqual this) && (this.x == that.x) && (this.y == that.y)
case _ => false
}
def canEqual(other: Any) = other.isInstanceOf[Point]// Inside ColoredPoint classoverride def hashCode = (super.hashCode, color).##override def equals(other: Any) = other match {
case that: ColoredPoint =>
(that canEqual this) && (super.equals(that)) && (this.color == that.color)
case _ => false
}
def canEqual(other: Any) = other.isInstanceOf[ColoredPoint]--------------------------------------------------------------------val p = new Point(1,2)
val cp = new Point(1,2,Color.Red)
val pAnon = new Point(1,1) { override val y = 2 }
p equals cp // false: cp canEqual p is false
cp equals p // false: p is not a ColoredPoint
p equals pAnon // true
  • Instances of different subclasses can be equal, as long as none of the classes redefines the equality method.

--

--

Ruining (Ray) Li

From Oxford. Studying Computer Science and on my way to a badass geek.