
Any, which has a few methods with well-established contracts. These methods are:equalshashCodetoString
equals.-
Structural equality - checked with the
equalsmethod or the==operator (and its negated counterpart!=).a == btranslates toa.equals(b)whenais not nullable, otherwise it translates toa?.equals(b) ?: (b === null). -
Referential equality - checked with the
===operator (and its negated counterpart!==); returnstruewhen both sides point to the same object.
equals is implemented in Any, which is the superclass of every class, we can check the equality of any two objects. However, using operators to check equality is not allowed when objects are not of the same type:open class Animal class Book Animal() == Book() // Error: Operator == cannot be // applied to Animal and Book Animal() === Book() // Error: Operator === cannot be // applied to Animal and Book
class Cat : Animal() Animal() == Cat() // OK, because Cat is a subclass of // Animal Animal() === Cat() // OK, because Cat is a subclass of // Animal
equals checks if another object is exactly the same instance, just like the referential equality (===). It means that every object is unique by default:class Name(val name: String) val name1 = Name("Marcin") val name2 = Name("Marcin") val name1Ref = name1 name1 == name1 // true name1 == name2 // false name1 == name1Ref // true name1 === name1 // true name1 === name2 // false name1 === name1Ref // true
data class FullName(val name: String, val surname: String) val name1 = FullName("Marcin", "Moskała") val name2 = FullName("Marcin", "Moskała") val name3 = FullName("Maja", "Moskała") name1 == name1 // true name1 == name2 // true, because data are the same name1 == name3 // false name1 === name1 // true name1 === name2 // false name1 === name3 // false
asStringCache and changed, which should not be compared by equality checking:class DateTime( /** The millis from 1970-01-01T00:00:00Z */ private var millis: Long = 0L, private var timeZone: TimeZone? = null ) { private var asStringCache = "" private var changed = false override fun equals(other: Any?): Boolean = other is DateTime && other.millis == millis && other.timeZone == timeZone //... }
data class DateTime( private var millis: Long = 0L, private var timeZone: TimeZone? = null ) { private var asStringCache = "" private var changed = false //... }
copy in such a case will not copy properties that are not declared in the primary constructor. Such behavior is correct only when these additional properties are truly redundant (the object will behave correctly if they are lost).User class might have an assumption that two users are equal when their id is identical.class User( val id: Int, val name: String, val surname: String ) { override fun equals(other: Any?): Boolean = other is User && other.id == id override fun hashCode(): Int = id }
equals ourselves when:- We need its logic to differ from the default logic.
- We need to compare only a subset of properties.
- We do not want our object to be a data class, or the properties we need to compare are not in the primary constructor.
equals is described in its comments (Kotlin 1.9.0, formatted):requirements:
- Reflexive: for any non-null value
x,x.equals(x)should return true. - Symmetric: for any non-null values
xandy,x.equals(y)should return true if and only ify.equals(x)returns true. - Transitive: for any non-null values
x,y, andz, ifx.equals(y)returns true andy.equals(z)returns true, thenx.equals(z)should return true. - Consistent: for any non-null values
xandy, multiple invocations ofx.equals(y)consistently return true or consistently return false, provided no information used inequalscomparisons on the objects is modified. - Never equal to null: for any non-null value
x,x.equals(null)should return false.
equals, toString and hashCode to be fast. This is not a part of the official contract, but it would be highly unexpected to wait a few seconds to check if two elements are equal.x.equals(x) returns true. It sounds obvious, but this can be violated. For instance, someone might want to make a Time object that can compare milliseconds as well as represent the current time:// DO NOT DO THIS! class Time( val millisArg: Long = -1, val isNow: Boolean = false ) { val millis: Long get() = if (isNow) System.currentTimeMillis() else millisArg override fun equals(other: Any?): Boolean = other is Time && millis == other.millis } val now = Time(isNow = true) now == now // Sometimes true, sometimes false List(100000) { now }.all { it == now } // Most likely false
contains method. Such an object will not work correctly in most unit test assertions either.val now1 = Time(isNow = true) val now2 = Time(isNow = true) assertEquals(now1, now2) // Sometimes passes, sometimes not
Time class? A simple solution is checking separately if the object represents the current time; if it doesn’t, we should check if it has the same timestamp. Although this is a typical example of a tagged class, as described in Item 40: Prefer class hierarchies instead of tagged classes, it would be even better to use class hierarchy instead:sealed class Time data class TimePoint(val millis: Long) : Time() object Now : Time()
x == y and y == x should always be the same. This can easily be violated when we accept objects of a different type in our equality. For instance, let’s say that we implemented a class to represent complex numbers and made its equality accept Double:class Complex( val real: Double, val imaginary: Double ) { // DO NOT DO THIS, violates symmetry override fun equals(other: Any?): Boolean { if (other is Double) { return imaginary == 0.0 && real == other } return other is Complex && real == other.real && imaginary == other.imaginary } }
Double does not accept equality with Complex. Therefore, the result depends on the order of the elements:Complex(1.0, 0.0).equals(1.0) // true 1.0.equals(Complex(1.0, 0.0)) // false
contains collections or on unit tests’ assertions.val list = listOf<Any>(Complex(1.0, 0.0)) list.contains(1.0) // Currently on the JVM this is false, // but it depends on the collection’s implementation // and should not be trusted to stay the same
x to y or y to x. This fact is not documented, and it is not a part of the contract as object creators assume that both should work in the same way (they assume symmetry). Also, creators might do some refactorization at any time, thus accidentally changing the order of these values. If your object is not symmetric, it might lead to unexpected and really hard-to-debug errors in your implementation. This is why when we implement equals, we should always consider symmetry.== operator between two different types that do not have a common superclass other than Any:Complex(1.0, 0.0) == 1.0 // ERROR
x, y, and z, if x.equals(y) returns true and y.equals(z) returns true, then x.equals(z) should return true. The biggest problem with transitivity is when we implement different kinds of equality that check a different subtype of properties. For instance, let’s say that we have Date and DateTime defined this way:open class Date( val year: Int, val month: Int, val day: Int ) { // DO NOT DO THIS, symmetric but not transitive override fun equals(o: Any?): Boolean = when (o) { is DateTime -> this == o.date is Date -> o.day == day && o.month == month && o.year == year else -> false } // ... } class DateTime( val date: Date, val hour: Int, val minute: Int, val second: Int ) : Date(date.year, date.month, date.day) { // DO NOT DO THIS, symmetric but not transitive override fun equals(o: Any?): Boolean = when (o) { is DateTime -> o.date == date && o.hour == hour && o.minute == minute && o.second == second is Date -> date == o else -> false } // ... }
DateTime instances, we check more properties than when we compare DateTime and Date. Therefore, two DateTime instances with the same day but a different time will not be equal to each other, but they’ll both be equal to the same Date. As a result, their relation is not transitive:val o1 = DateTime(Date(1992, 10, 20), 12, 30, 0) val o2 = Date(1992, 10, 20) val o3 = DateTime(Date(1992, 10, 20), 14, 45, 30) o1 == o2 // true o2 == o3 // true o1 == o3 // false, so equality is not transitive setOf(o2, o1, o3).size // 1 or 2? // Depends on the collection’s implementation
data class Date( val year: Int, val month: Int, val day: Int ) data class DateTime( val date: Date, val hour: Int, val minute: Int, val second: Int ) val o1 = DateTime(Date(1992, 10, 20), 12, 30, 0) val o2 = Date(1992, 10, 20) val o3 = DateTime(Date(1992, 10, 20), 14, 45, 30) o1.equals(o2) // false o2.equals(o3) // false o1 == o3 // false o1.date.equals(o2) // true o2.equals(o3.date) // true o1.date == o3.date // true
equals to be a pure function (it should not modify the state of an object) whose result always depends only on the input and the state of its receiver. We’ve seen a Time class which violated this principle. This rule was also famously violated in java.net.URL.equals(), what will be explained soon.null should never be equal to null: for any non-null value x, x.equals(null) must return false. This is important because null should be unique, and no object should be equal to it.equals is the one from java.net.URL. The equality of two java.net.URL objects depends on a network operation as two hosts are considered equivalent if both hostnames can be resolved to the same IP addresses. Take a look at the following example:import java.net.URL fun main() { val enWiki = URL("https://en.wikipedia.org/") val wiki = URL("https://wikipedia.org/") println(enWiki == wiki) }
true, but the result is inconsistent. In normal conditions, it should print true because the IP address for both URLs is resolved as the same; however, if you have the internet disconnected, it will print false. You can check this yourself. This is a big mistake! Equality should not be network-dependent.- This behavior is inconsistent. For instance, two URLs could be equal when the internet connection is available but unequal when it is not. Also, IP addresses resolved by a URL can change over time, so the result might be inconsistent.
-
The network may be slow, and we expect
equalsandhashCodeto be fast. A typical problem is when we check if a URL is present in a list. Such an operation would require a network call for each element in the list. Also, on some platforms, like Android, network operations are prohibited on the main thread. As a result, even adding aURLto a set needs to be started on a separate thread. - The defined behavior is known to be inconsistent with virtual hosting in HTTP. Equal IP addresses do not imply equal content. Virtual hosting allows unrelated sites to share an IP address. This method could report two otherwise unrelated URLs to be equal because they're hosted on the same server.
java.net.URI instead of java.net.URL.equals yourself unless you have a good reason. Instead, use the default implementation or data class equality. If you do need custom equality, always consider whether your implementation is reflexive, symmetric, transitive, and consistent. The typical implementation of equals looks like this:override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is MyClass) return false return field1 == other.field1 && field2 == other.field2 && field3 == other.field3 } // or override fun equals(other: Any?) = this === other || (other is MyClass && field1 == other.field1 && field2 == other.field2 && field3 == other.field3)
equals final, or beware that subclasses should not change how equality behaves. It is hard to make custom equality while inheritance at the same time. Some even say it is impossible[^42_1]. This is one of the reasons why data classes are final.- The
==operator translates toequalsand checks structural equality. The===operator checks referential equality, i.e., if two values are exactly the same object. - Equality is reflexive, symmetric, transitive, and consistent. If you implement
equalsyourself, make sure it follows these rules. - To fulfill the contract of
equals, only classes of the same type should be considered equal.equalsshould be fast and should not require an internet connection. A famous example of a poor implementation from Java stdlib isjava.net.URLequality.