10

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.

Community
  • 1
  • 1
Jus12
  • 17,824
  • 28
  • 99
  • 157

3 Answers3

13

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.

Rüdiger Klaehn
  • 12,445
  • 3
  • 41
  • 57
  • I agree with the part of "leading to confusing behavior", so I am reconsidering if can really do without it. I can, but I need to add additional logic to take care of the last field in `Foo`. I need to weigh the options. – Jus12 Aug 10 '15 at 09:21
  • 4
    @Jus12 You can write `case class Foo(i:Int, s:String)(optBar:Option[Bar])`. The second param list is not considered to be part of the generated case class methods. – kiritsuku Aug 10 '15 at 09:41
  • Accepted as the correct answer because `override def hashCode = (i, s).##` is much nicer. Can you elaborate on what it does? :) – Jus12 Aug 11 '15 at 06:54
  • I put some explanation about ## into the answer – Rüdiger Klaehn Aug 11 '15 at 11:26
1

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")
mattinbits
  • 10,370
  • 1
  • 26
  • 35
  • I need to use this in collections. For example, `listOfFoos.contains(foo1)`. Is there any way to make this work for my use-case? – Jus12 Aug 10 '15 at 09:55
  • You could use `listOfFoos.exists(_ =~ foo1)`, but I realise `contains` is just an example and there may be other methods as well which rely on `==` under the hood. What about making `i` and `s` into another case class which is then a member of `Foo`, so you can do something like `listOfFoos.map(_.subFoo).contains(foo1.subFoo)`? – mattinbits Aug 10 '15 at 09:59
  • My mistake for the wrong use-case. As you correctly said, we *can* use this in `contains`, etc but can we use this in a map, as in `mapWithKeyFee.get(foo1)`? – Jus12 Aug 10 '15 at 10:35
  • I think in that case, it makes sense to go with two levels of case class, so you do `mapWithKeySubFoo.get(foo1.subFoo)`. I don't see the point in using Foo as a map key when one of its parameters is not consider as part of equality like that. – mattinbits Aug 10 '15 at 10:40
  • That is what I originally had, but it seemed error-prone if I forget to use `.subFoo`. – Jus12 Aug 10 '15 at 11:09
  • 1
    If your map is of type `Map[SubFoo, Whatever]` and you try to do `map.get(foo)` you should get a compile time error, so should be easy to catch such errors. – mattinbits Aug 10 '15 at 11:16
  • Good point. In my case, `subFoo` also is of type `Foo`. I will try to refactor and see if this is possible. – Jus12 Aug 10 '15 at 11:18
1

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
Helder Pereira
  • 5,522
  • 2
  • 35
  • 52