Object Equality in Scala
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.yval 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.