Проведя немало исследований, я не смог найти ответ с необходимой и правильной реализацией шаблона equals
и hashCode
для класса Scala (не для класса case, поскольку он генерируется автоматически компилятором ине должно быть переопределено).Я действительно нашел макрос 2.10 (устаревший) , который доблестно пытался решить эту проблему.
В итоге я соединил шаблоны, рекомендованные в «Эффективная Java, 2-е издание» (ДжошуаBloch) и в этой статье "Как написать метод равенства в Java" (автор Martin Martin Odersky, Lex Spoon и Bill Venners) и создали стандартный шаблон по умолчанию, который я сейчас использую для реализацииequals
и hashCode
для моих классов Scala.
Основная цель шаблона equals
состоит в минимизации количества фактических сравнений, необходимых для выполнения, чтобы получить действительные и окончательные значения true
или false
.
Кроме того, метод hashCode
ВСЕГДА должен быть переопределен и повторно реализован при переопределении метода equals
(снова см. «Эффективная Java, 2-е издание» (автор Джошуа Блох)) ).Следовательно, мое включение «шаблона» метода hashCode
в приведенный ниже код, который также включает критический совет о с использованием ##
вместо hashCode
в реальной реализации.
Стоит отметить, что каждый из super.equals
и super.hashCode
должен вызываться только в том случае, если предок уже переопределил его.Если нет, то крайне важно НЕ вызывать super.*
, поскольку реализация по умолчанию в java.lang.Object
(equals
сравнивает для того же экземпляра класса , а hashCode
, скорее всего, преобразует адрес памятиобъекта в целое число) , оба из которых нарушат указанный equals
и hashCode
контракт для переопределенных методов.
class Person(val name: String, val age: Int) extends Equals {
override def canEqual(that: Any): Boolean =
that.isInstanceOf[Person]
//Intentionally avoiding the call to super.equals because no ancestor has overridden equals (see note 7 below)
override def equals(that: Any): Boolean =
that match {
case person: Person =>
( (this eq person) //optional, but highly recommended sans very specific knowledge about this exact class implementation
|| ( person.canEqual(this) //optional only if this class is marked final
&& (hashCode == person.hashCode) //optional, exceptionally execution efficient if hashCode is cached, at an obvious space inefficiency tradeoff
&& ( (name == person.name)
&& (age == person.age)
)
)
)
case _ =>
false
}
//Intentionally avoiding the call to super.hashCode because no ancestor has overridden hashCode (see note 7 below)
override def hashCode(): Int =
31 * (
name.##
) + age.##
}
Код имеет ряд нюансов, которыекритически важно:
- Расширение
scala.Equals
- Гарантирует, что идиоматический шаблон equals
, который включает в себя формализацию метода canEqual
, полностью реализуется.Несмотря на то, что расширение является технически необязательным, оно настоятельно рекомендуется. - То же самое короткое замыкание экземпляра - тестирование
(this eq person)
для true
не гарантирует дальнейших (дорогих) сравнений, поскольку это буквально тот же экземпляр.Этот тест должен быть внутри сопоставления с шаблоном, так как метод eq
доступен для AnyRef
, а не для Any
(тип that
).И поскольку AnyRef
является предком Person
, этот метод выполняет две одновременные проверки типов посредством проверки типа потомка Person
, что подразумевает автоматическую проверку типа всех его предков, включая AnyRef
, что требуетсядля проверки eq
.Хотя этот тест технически необязателен, он по-прежнему настоятельно рекомендуется. - Проверьте
that
s canEqual
- Это очень легко получить назад, что НЕПРАВИЛЬНО.Крайне важно, чтобы проверка canEqual
была выполнена на экземпляре that
с параметром this
.И хотя это может показаться избыточным для сопоставления с шаблоном (учитывая, что мы получаем эту строку кода, that
должен быть Person
экземпляром), мы все равно должны сделать вызов метода, так как не можем предположить, что that
равносовместимый потомок Person
(все потомки Person
будут успешно соответствовать шаблону как Person
).Если класс помечен final
, этот тест необязателен и может быть безопасно удален.В противном случае это требуется. - Проверка
hashCode
короткого замыкания - Хотя этого теста hashCode
недостаточно или не требуется, если это false
, это устраняет необходимость выполнения всех проверок уровня значения (элемент 5).Если этот тест равен true
, то проверка по полю фактически требуется.Этот тест является необязательным и может быть исключен, если значение hashCode не кэшируется и , а общая стоимость проверок на равенство полей достаточно мала. - Проверка на равенство полей - даже если тест
hashCode
предоставлен и успешно выполнен, все значения уровня поля все равно должны быть проверены. Это связано с тем, что, хотя это крайне маловероятно, для остается возможность для двух разных экземпляров сгенерировать одно и то же значение hashCode
, и все же оно не может быть фактически эквивалентным на уровне поля . Родительский equals
также должен быть вызван, чтобы гарантировать, что любые дополнительные поля, определенные в предках, также проверены.
- Pattern match
case _ =>
- На самом деле достигается два разных эффекта. Во-первых, сопоставление с шаблоном Scala гарантирует правильную маршрутизацию null
, поэтому null
не должно появляться нигде в нашем чистом коде Scala. Во-вторых, сопоставление с шаблоном гарантирует, что бы that
ни было, это не экземпляр Person
или один из его потомков.
- Когда вызывать каждый из
super.equals
и super.hashCode
немного сложно - если предок уже переопределил оба (никогда не должно быть), обязательно включите super.*
в свои собственные переопределенные реализации. И если предок не переопределил оба, тогда ваши переопределенные реализации должны избегать вызова super.*
. В приведенном выше примере кода Person
показан случай, когда нет предка , который переопределил оба. Таким образом, вызов каждого вызова метода super.*
будет неправильно соответствовать реализации по умолчанию java.lang.Object.*
, которая сделает недействительным предполагаемый комбинированный контракт для equals
и hashCode
.
Это основанный на super.equals
код, который нужно использовать ТОЛЬКО ЕСЛИ хотя бы один предок уже явно переопределил equals
.
override def equals(that: Any): Boolean =
...
case person: Person =>
( ...
//WARNING: including the next line ASSUMES at least one ancestor has already overridden equals; i.e. that this does not end up invoking java.lang.Object.equals
&& ( super.equals(person) //incorporate checking ancestor(s)' fields
&& (name == person.name)
&& (age == person.age)
)
...
)
...
Это основанный на super.hashCode
код, который нужно использовать ТОЛЬКО ЕСЛИ есть хотя бы один предок, который уже явно переопределил hashCode
.
override def hashCode(): Int =
31 * (
31 * (
//WARNING: including the next line ASSUMES at least one ancestor has already overridden hashCode; i.e. that this does not end up invoking java.lang.Object.hashCode
super.hashCode //incorporate adding ancestor(s)' hashCode (and thereby, their fields)
) + name.##
) + age.##
И последнее замечание: в моих исследованиях я не мог поверить, сколько существует ошибочных реализаций этого шаблона. Это, безусловно, все еще область, в которой трудно правильно понять детали:
- Программирование в Scala, первое издание - пропущено 1, 2 и 4 выше.
- Кулинарная книга Алвина Александра Скала - Пропущено 1, 2 и 4.
- Примеры кода для программирования в Scala - Неправильно используется
.hashCode
вместо .##
в полях классов при генерации переопределения и реализации класса hashCode
. Смотрите Tree3.scala