Authors Top

If you have a few years of experience with the Kotlin language and server-side development, and you’re interested in sharing that experience with the community, have a look at our Contribution Guidelines.

1. Overview

As we know, Kotlin has data classes for holding data.

In this tutorial, we’ll discuss data class’s equals() method through a couple of common pitfalls.

2. Kotlin’s data class in a Nutshell

Kotlin’s data class is designed for holding data, for example:

data class Person(val firstname: String, val lastname: String)

A data class must have a non-empty primary constructor. Further, data classes cannot be inherited.

Kotlin’s data class pre-implements a set of commonly used methods, such as getters/setters, copy(), toString(), hashcode() and equals(). These methods allow us to manipulate data pretty easily.

However, if we don’t know the data class’s equals() method well, we may encounter problems.

Next, let’s better understand the equals() method through two common pitfalls.

For simplicity, we’ll use unit test assertions to verify the results in this tutorial.

3. Data Class Equality Pitfall #1

First, let’s extend our Person class a little bit. Let’s say we want to introduce a new dateOfBirth property and make the Person class into the PersonV1 class:

data class PersonV1(val firstname: String, val lastname: String) {
    lateinit var dateOfBirth: LocalDate
}

Next, let’s introduce the first pitfall through an example.

3.1. Introduction to the Pitfall

An example can show the problem quickly. Let’s say we create two PersonV1‘s instances:

val p1 = PersonV1("Amanda", "Smith").also { it.dateOfBirth = LocalDate.of(1992, 8, 8) }
val p2 = PersonV1("Amanda", "Smith").also { it.dateOfBirth = LocalDate.of(1976, 11, 18) }

As we’ve learned, that data class has already provided hashcode() and equals() methods. If we compare p1 and p2  using the equals() method, although they have the same first and last names, we’d think they’re obviously not equal since their dateOfBirth values are different,.

However, if we run the test below, it passes:

assertTrue { p1 == p2 }

So, this result tells us that p1 and p2 are actually equal. We know that, unlike Java, Kotlin’s ==‘ operator checks structural equality. In this case, it calls the data class’s equals() method and performs the value comparison.

If we thought p1 and p2 weren’t equal, it could lead to some problems. For example, we may use them as keys to store some data in a hashmap and expect there would be two entries. However, the latter entry will overwrite the existing one due to the fact of p1 == p2.

Next, let’s understand why Kotlin tells us p1 == p2, although their dateOfBirth properties have different values.

3.2. Understanding Data Class’s equals() Method and Solving the Problem

Here’s the reason why p1 == p2: data class automatically generates those convenient methods, such as equals(), hashcode(), toString() and copy(), only on properties declared in the primary constructor. 

In other words, when we check p1 == p2, only properties in the primary constructor get compared. The dateOfBirth property isn’t in the primary constructor. Therefore, it’s not compared at all.

Now that we understand how data classes’ default equals() work, solving the problem won’t be a challenging job for us.

So, there are two ways to go. The first option, which is also the recommended approach, is to refactor related code and move the dateOfBirth property to the primary constructor. In this way, all three properties will participate in the equals() check.

Alternatively, we can override the hashcode() and equals() methods so that when we call the equals() method on this data class, our own implemented equals() method will be invoked.

So next, let’s create a new class PersonV2 to override hashcode() and equals():

data class PersonV2(val firstname: String, val lastname: String) {
    lateinit var dateOfBirth: LocalDate

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

        if (firstname != other.firstname) return false
        if (lastname != other.lastname) return false
        if (dateOfBirth != other.dateOfBirth) return false

        return true
    }

    override fun hashCode(): Int {
        var result = firstname.hashCode()
        result = 31 * result + lastname.hashCode()
        result = 31 * result + dateOfBirth.hashCode()
        return result
    }

}

Next, let’s test if the equals() method works as expected:

val p1 = PersonV2("Amanda", "Smith").also { it.dateOfBirth = LocalDate.of(1992, 8, 8) }
val p2 = PersonV2("Amanda", "Smith").also { it.dateOfBirth = LocalDate.of(1976, 11, 18) }
assertFalse { p1 == p2 }

In the test, we’ve created two PersonV2 instances with identical first and last names, but their dateOfBirth values are different. So this time, we expect they aren’t equal.

4. Data Class Equality Pitfall #2

We’ve learned that Kotlin’s data class only examines the properties declared in its primary constructor when it performs the equals() check. So let’s see another data class example:

data class BaeldungString(
    val value: String,
    val chars: CharArray,
)

As the code above shows, all properties are declared in BaeldungString‘s primary constructor this time. So, next, let’s create some instances and see whether the equals() check gives us the expected result.

4.1. Introduction to the Pitfall

Again, let’s create two BaeldungString instances:

val s1 = BaeldungString("Amanda", charArrayOf('A', 'm', 'a', 'n', 'd', 'a'))
val s2 = BaeldungString("Amanda", charArrayOf('A', 'm', 'a', 'n', 'd', 'a'))

As we have seen, even though s1 and s2 are different objects, they have the same values in the String value and the CharArray chars properties. Thus, we may believe s1 == s2 should return true.

Surprisingly, if we execute the test below, it passes. That is to say, s1 is not equal to s2.

​assertFalse { s1 == s2 }

Next, let’s understand why we get this result and fix the problem.

4.2. Understanding Array.equals() in Kotlin and Solving the Problem

When we call the data class’s equal() method, Kotlin compares properties declared in the primary constructor. Further, when Kotlin compares properties, it invokes the equals() method of property type.

For instance, for s1 == s2, Kotlin checks s1.value == s2.value and s1.chars == s2.chars. As we’ve mentioned, the == operator checks for structural equality.

But why does s1 == s2 return false? This is because, unlike List, array1 == array2 compares their references in Kotlin. 

Let’s create a test to see the difference:

val list1 = listOf("one", "two", "three", "four")
val list2 = listOf("one", "two", "three", "four")

val array1 = arrayOf("one", "two", "three", "four")
val array2 = arrayOf("one", "two", "three", "four")

assertTrue { list1 == list2 }
assertFalse { array1 == array2 }

The test above passes if we give it a run.

Therefore, == isn’t the right way to compare two arrays’ values. If we want to check for array structural equality, we should use the array’s contentEquals() method:

assertTrue { array1 contentEquals array2 }

The above assertion passes.

Now, let’s fix the problem.

As List‘s ‘==‘ operator checks values, the most straightforward solution is to replace the array in the data class with a list. Further, generally speaking, we should prefer Lists over arrays.

If for some reason, we must use the array type in the data class, we can solve the problem by overriding the equals() and the hashcode() methods. But we should note that in the equals() method, we should use the contentEquals() method instead of ‘==‘ to compare arrays’ values.

Let’s create BaeldungStringV2 and override these two methods:

data class BaeldungStringV2(val value: String, val chars: CharArray) {
    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (other !is BaeldungStringV2) return false

        if (value != other.value) return false
        if (!chars.contentEquals(other.chars)) return false

        return true
    }

    override fun hashCode(): Int {
        var result = value.hashCode()
        result = 31 * result + chars.contentHashCode()
        return result
    }
}

Finally, let’s create two BaeldungStringV2 objects with identical values and test if we get the expected result:

val s1 = BaeldungStringV2("Amanda", charArrayOf('A', 'm', 'a', 'n', 'd', 'a'))
val s2 = BaeldungStringV2("Amanda", charArrayOf('A', 'm', 'a', 'n', 'd', 'a'))

assertTrue { s1 == s2 }

The test passes when we execute it. So, the problem is solved.

5. Conclusion

In this article, we’ve discussed Kotlin data class’s equals() method through a couple of common pitfalls.

As always, the full source code used in the article can be found over on GitHub.

Authors Bottom

If you have a few years of experience with the Kotlin language and server-side development, and you’re interested in sharing that experience with the community, have a look at our Contribution Guidelines.

Comments are closed on this article!