0

I wonder why the equal method (==) does not work as expected.

Is there any way to fix the commented section in the code below?

As you can see p1 and p2 are not equals, neither reference nor value. So why p1 == p2 is true?!

object Main {
    @JvmStatic
    fun main(args: Array<String>) {

        val f1 = Foo(1)
        Thread.sleep(3) // to set a different value in parent of f2
        val f2 = Foo(1)

        val p1 = (f1 as Parent)
        val p2 = (f2 as Parent)

        println(p1 == p2) // true
        println(p1.b == p2.b) // false
    }
}

data class Foo(val a: Int) : Parent("$a-${System.currentTimeMillis()}")

sealed class Parent(val b: String) {

    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (javaClass != other?.javaClass) return false

        other as Parent

        if (b != other.b) return false

        return true
    }
    override fun hashCode(): Int {
        return b.hashCode()
    }
}

2 Answers2

5

The reason this doesn't behave as you expect is because a data class automatically provides you with an implementation of equals(), one that checks (only) the values of the properties specified in its constructor.

So your data class Foo(val a: Int) gets an equals() that checks only the value of a, overriding that in the Parent class.

In general, data classes are simple value holders, whose value is characterised entirely by those properties, and so the automatically-generated equals() (and hashCode() and toString() and copy() and componentX() methods) make good sense. If that doesn't apply, a data class might not be a good fit for your case.

gidds
  • 16,558
  • 2
  • 19
  • 26
  • The only restriction that the Kotlin documentation mention about data-classes is: Data classes cannot be abstract, open, sealed, or inner. So, it should work fine, but as you can see, it is not. Imagine that I wasn't testing it and using it in production, then the project will behave differently because of the wrong generated code. – Mohammad Reza Khahani Dec 23 '21 at 23:59
  • I think this sentence should be added to Kotlin documentation: don't use inheritance with data classes. as @Andrey Breslav mentioned here: https://stackoverflow.com/a/26467380/6751083 – Mohammad Reza Khahani Dec 24 '21 at 00:03
  • That advice is very old. There are valid reasons for data classes to have parent classes. It’s very common for sealed classes to have data class children. (Maybe that will become less common now that there are sealed interfaces.) You just need to be aware that the equals/hashcode implementation exclusively incorporates properties of the data class’s primary constructor. – Tenfour04 Dec 24 '21 at 01:04
  • I even checked that with the sealed class, which did not work correctly. I understand that there are many ways to use data classes correctly. Still, I can't accept that Kotlin let it be used with half working functionality or without a clear warning in the documentation. – Mohammad Reza Khahani Dec 24 '21 at 01:16
  • 3
    It works according to the definition of what a data class does, so it works correctly by design, whether or not that’s an optimal design. The documentation clearly says the generated functions only look at properties in the primary constructor. – Tenfour04 Dec 24 '21 at 02:20
1

There is one solution, and you have to override the equals and hashCode method explicitly in the Foo class. Then it will work fine.

object Main {
    @JvmStatic
    fun main(args: Array<String>) {

        val f1 = Foo(1)
        Thread.sleep(3)
        val f2 = Foo(1)

        val p1 = (f1 as Parent)
        val p2 = (f2 as Parent)

        println(p1 == p2) // false
        println(p1.b == p2.b) // false
    }
}

data class Foo(val a: Int) : Parent("$a-${System.currentTimeMillis()}") {
    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (javaClass != other?.javaClass) return false
        if (!super.equals(other)) return false

        other as Foo

        if (a != other.a) return false

        return true
    }

    override fun hashCode(): Int {
        var result = super.hashCode()
        result = 31 * result + a
        return result
    }
}

sealed class Parent(val b: String) {

    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (javaClass != other?.javaClass) return false

        other as Parent

        if (b != other.b) return false

        return true
    }
    override fun hashCode(): Int {
        return b.hashCode()
    }
}