Search code examples
scalaequalscase-class

equals of case class with non-primitives is not working correctly for two logically equal instances


Equals is not working correctly for two logically equals instance of case class. Please find below simplified case classes :-

case class Item(name : String)
case class Basket(items : Array[Item], bills : Map[Int, Int])

def createBasketInstance() : Basket = {
  val maggi = Item("milk")
  val coffee = Item("coffee")

  val bills1 = Map(1 -> 20, 2 -> 75)
  Basket( Array(maggi, coffee), bills1 )
}

val basket1 = createBasketInstance()
val basket2 = createBasketInstance()

Expected Result for basket1.equals(basket2) is true, but it actually prints false.

scalafiddle link : https://scalafiddle.io/sf/iJRKnMk/0

Please find attached screenshot also to confirm equals comparison using scalatest.

enter image description here


Solution

  • This has nothing to do with the default equals of the case classes but that of the Arrays (this is one of the millions reasons you should use Lists over Arrays btw)

    In Scala Arrays are just plain Java arrays and arrays in Java are treated specially (because JVM)

    Even a simple

    Array(1, 2, 3) == Array(1, 2, 3)
    

    will return false.

    Why? Because Object.equals which the Arrays inherit the equals method from only compared the array references and NOT their content.

    I know this doesn't sound as bad as it is, but believe it gets worse.

    You might think simply assigning your array to a value would fix it right? Well it doesn't. The following will fail as well...

    def createBasketInstance() : Basket = {
      val maggi = Item("milk")
      val coffee = Item("coffee")
      val arr =  Array(maggi, coffee)
      val bills1 = Map(1 -> 20, 2 -> 75)
      Basket( arr, bills1 )
    }
    

    Because the call stacks of the different invocation of this function is not able to share references and thus each function call fill create a new reference even though the underlying content is the same.

    How do you solve this?

    1. There is a method in java.utils.Arrays called Arrays.equals that will do a deep comparison. You can override it and construct it such that the comparison uses Arrays.equals. You might also need to override hashCode

    2. Write an implicit conversion from Array to literally any other collection

    3. Don't use Arrays in the first place.

    I would say #3 is the way to go about this