In Kotlin, there are two operators for comparison:
== — structural comparison (equivalent to .equals()). It checks the contents: a == b calls a?.equals(b) ?: (b == null).=== — referential comparison. It checks whether both variables point to the same object: a === b is equivalent to Java a == b for objects.For primitive types (e.g., Int): == and === may behave the same due to auto-boxing, but in general, == should be used.
val a = "Kotlin" val b = "Kotlin" val c = a println(a == b) // true (content comparison) println(a === b) // false (different references — string interning depends on the compiler) println(a === c) // true (the same object)
What is the difference between a.equals(b) and a == b in Kotlin?
Some believe there is no difference, however, if a is nullable, calling a.equals(b) will throw a NullPointerException, while a == b is safe and will return true if both are null or false if only one of them is null.
Example:
val a: String? = null val b: String? = null println(a == b) // true println(a?.equals(b)) // null (not true, but not a crash) println(a.equals(b)) // NullPointerException
Story
In a large project, nullable strings were actively compared using a.equals(b), unaware that if a == null, the application crashes. The bug occurred quite rarely but led to fatal crashes in production — it was fixed by replacing it with a == b.
Story
When comparing objects using ===, the expectation was for content comparison, but === checks object identity — it turned out that two different strings with the same content are not equal via ===, which broke the logic of data caching.
Story
When working with boxed Int values (e.g., from collections), developers compared them using === and received unexpected results because object instances of different numbers are not necessarily interned like primitives. This led to incorrect behavior when working with object collections.