1. Overview
In this tutorial, we’re going to explore different ways of object comparison in Kotlin.
=== and its counterpart !== are binary operators used for referential identity. It means when two references point to the same object, the result will be true. However, most of the time, we need to check if two objects have equivalent values.
So, let’s dive in to investigate different ways of structural equality in Kotlin.
2. Equality Using the == Operator
== and its opposite != are used to compare two objects. If we compare two strings with ==, we achieve the desired result:
val a = "Baeldung"
val b = "Baeldung"
assertTrue(a == b)
However, that may not always be what we want. Suppose we have a class like the following one:
class Storage(private val name: String, private val capacity: Int)
Now let’s compare two instances of this class:
val storage1 = Storage("980Pro M.2 NVMe", 1024)
val storage2 = Storage("980Pro M.2 NVMe", 1024)
assertFalse(storage1 == storage2)
The result is false! But how is this possible? The reason is String Pool. When we declare a string object, the runtime checks for the identical value in the string pool. If it finds a match, it replaces the string literal with the reference of the string object found in the pool. That is why “Baeldung” is equal to “Baeldung”. The idea of a constant pool is applicable for numbers, chars and booleans too.
However, in the Storage class, we must explicitly specify whether storage1 and storage2 are equal. Otherwise, the identity check will take place. In other words, it checks for equality by matching references. That being the case storage1 == storage2 returns false.
3. Overriding equals() and hashCode()
Each time we use the == operator, it calls the equals() function under the hood. And as said earlier, the identity check occurs. We can override equals() to provide custom equality check implementation:
class StorageOverriddenEquals(val name: String, val capacity: Int) {
override fun equals(other: Any?): Boolean {
if (other == null) return false
if (this === other) return true
if (other !is StorageOverriddenEquals) return false
if (name != other.name || capacity != other.capacity) return false
return true
}
}
Now the answer of equals() is true and meaningful for us:
val storage1 = StorageOverriddenEquals("980Pro M.2 NVMe", 1024)
val storage2 = StorageOverriddenEquals("980Pro M.2 NVMe", 1024)
assertTrue(storage1 == storage2)
An important point is that we must always override both equals() and hashCode() methods together. The reason is the hash code contract.
If we don’t respect the rule, hash-based collections like Sets won’t work correctly. To fix that, we can override the hashCode() function:
class StorageOverriddenEqualsAndHashCode(private val name: String, private val capacity: Int) {
override fun equals(other: Any?): Boolean {
if (other == null) return false
if (this === other) return true
if (other !is StorageOverriddenEqualsAndHashCode) return false
other as StorageOverriddenEqualsAndHashCode
if (name != other.name || capacity != other.capacity) return false
return true
}
override fun hashCode(): Int = name.hashCode() * 31 + capacity
}
4. Using Data Classes
Alternatively, Data classes can be used to avoid the boilerplate we need to compare objects:
data class StorageDataClass(private val name: String, private val capacity: Int)
val storage1 = StorageDataClass("980Pro M.2 NVMe", 1024)
val storage2 = StorageDataClass("980Pro M.2 NVMe", 1024)
assertTrue(storage1 == storage2)
The result of this comparison is true.
5. Conclusion
In this article, we saw how to override the default behavior of the == operator.
All of the examples are available over on GitHub.