Три сравнения равенства: ==, equals, eq

  • == точно так же, как уравнение.
// Definition of == in class Any:
final def == (that: Any): Boolean = 
  if (null eq this) {null eq that} else {this equals that}
  • eq — для равенства ссылок, equals — для равенства ссылок по умолчанию, но его можно настроить для более естественных представлений о равенстве.

Контракты между equals и hashCode

  • Если два объекта равны согласно методу equals, то вызов метода hashCode для каждого из двух объектов должен привести к одному и тому же целочисленному результату.
  • equals должно быть отношением эквивалентности: рефлексивным, транзитивным, симметричным, непротиворечивым.

Распространенные ошибки при написании метода equals

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

1. Определение equals с неправильной подписью

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

Причина:

  • equals здесь на самом деле перегружает, а не переопределяет, потому что в Any метод equals принимает параметр класса Any.
  • p2a относится к классу Any, поэтому выполняется универсальное equals в классе Any, которое по умолчанию используется для равенства ссылок.

Модификация:

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

2. Изменение равно без изменения хэш-кода.

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

Причина:

  • Без изменения метода hashCode (который по умолчанию основан на ссылке), p1 и p2 переходят в разные «хеш-сегменты».
  • Таким образом, в ведре, содержащем p2, нет ничего равного p2.

Модификация:

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

3. Определение equals с точки зрения изменяемых полей

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

Это очень запутанно и вводит в заблуждение! ⬆️

Причина:

  • После изменения p.x p переходит в другое «хеш-сегмент», потому что его hashCode меняется.
  • В этом новом «хэш-корзине» нет элементов, равных p.

ИЗБЕГАЙТЕ ОПРЕДЕЛЕНИЯ РАВНЫХ В ТЕРМИНАХ ИЗМЕНЧИВЫХ ПОЛЕЙ!!!

4. Отсутствие определения equals как отношения эквивалентности

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

Не симметрично! ⬆️

Модификация: добавьте метод canEqual!

// Inside Point class
override 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 class
override 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
  • Экземпляры разных подклассов могут быть равны, если ни один из классов не переопределяет метод равенства.