Три сравнения равенства: ==, 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
- Экземпляры разных подклассов могут быть равны, если ни один из классов не переопределяет метод равенства.