Jus12
Jus12

Reputation: 18024

How to define custom equality in case classes

I have a case class Foo defined below. I want to override the behavior of == in that, so that the last element (optBar) is ignored in the comparison. Here is what I have tried and it seems to work.

case class Bar(i:Int)
case class Foo(i:Int, s:String, optBar:Option[Bar]) {
    override def equals(o:Any) = o match {
        case Foo(`i`, `s`, _) => true
        case _ => false
    }
    override def hashCode = i.hashCode*997  ^ s.hashCode * 991
}
val b = Bar(1)
val f1 = Foo(1, "hi", Some(b))
val f2 = Foo(1, "hi", None)
f1 == f2 // true

What I want to know is if the method of creating hashCode is correct. I got it from this link.

Upvotes: 10

Views: 5456

Answers (3)

Rüdiger Klaehn
Rüdiger Klaehn

Reputation: 12565

Your hashCode definition is correct as in that it complies with the equals/hashCode contract. But I think

override def hashCode = (i, s).##

is nicer to read.

To clarify what this does: ## is just a convenience method on scala.Any that calls hashCode, but properly deals with null and some corner cases related to primitives.

val x: String = null
x.## // works fine. returns 0
x.hashCode // throws NullPointerException

So (i, s).## creates a tuple of i and s (which has a well-defined hashCode method) and then returns its hash code. So you don't have to manually write a hash code method involving MurmurHash etc. By the way: this will also properly work if one of the elements of the tuple is null, whereas a hand-written hash method like the one in the question might throw a NPE.

However, in my experience if you want to modify any of the things that a case class provides for you, you don't really want a case class. Also, overriding equality to not take into account some of the data might seem a clever idea at some point, but it can lead to some very confusing behavior.

Upvotes: 13

mattinbits
mattinbits

Reputation: 10428

How about using a different operator for your own version of equality. I think this is nicer than overriding default behaviour of == e.g. ~= as "approximately equal"

case class Bar(i:Int)
case class Foo(i:Int, s:String, optBar:Option[Bar]) {

  def ~= (that:Foo): Boolean = (this.i, this.s) == (that.i, that.s)

}

val foo1 = Foo(1, "a", None)

val foo2 = Foo(1, "a", Some(Bar(4)))

foo1 == foo2 //false

foo1 ~= foo2 //true

Edit:

If you want to be able to use this as Map keys, then I'd try:

case class Bar(i: Int)

trait FooLike {
  def s: String
  def i: Int

  def ~=(that: FooLike) = (s, i) == (that.s, that.i) 
}

case class SubFoo(s: String, i: Int) extends FooLike

case class Foo(sub: SubFoo, barOpt: Option[Bar]) extends FooLike {
  def s = sub.s
  def i = sub.i
}

val map = scala.collection.mutable.Map.empty[SubFoo, String]

val sub = SubFoo("a", 1)

val foo = Foo(sub, None)

foo ~= sub //true

val foo2 = Foo(sub, Some(Bar(1)))

foo ~= foo2 ///true

map += sub -> "abc"

map.get(foo.sub) //Some("abc")

Upvotes: 1

Helder Pereira
Helder Pereira

Reputation: 5756

You can also remove the optBar from the case class definition and create a constructor with the three parameters. To avoid having to use the new keyword when you want to use that constructor you can create a companion object.

case class Bar(i:Int)
case class Foo(i:Int, s:String) {
  var optBar: Option[Bar] = None

  def this(i:Int, s:String, optBar:Option[Bar]) {
    this(i, s)
    this.optBar = optBar
  }
}
object Foo {
  def apply(i:Int, s:String, optBar:Option[Bar]) =
    new Foo(i, s, optBar)
}

val b = Bar(1)
val f1 = Foo(1, "hi", Some(b))
val f2 = Foo(1, "hi", None)
f1 == f2 // true

Upvotes: 1

Related Questions