In Kotlin, == Is the One You Actually Want
Every Java developer has been bitten by this: two strings that are obviously equal, compared with ==, returning false. The reason is that Java’s == compares references, not contents, and you were supposed to type .equals(). Kotlin swaps the defaults. == does what you meant all along, and there’s a separate operator for the rare time you wanted reference identity.
This post follows extension functions. Equality is a small topic with a sharp Java-shaped edge, so it’s worth its own stop.
== is structural, === is referential
In Kotlin, == means “are these equal in value?” — it calls .equals() under the hood. The triple === means “are these the same object?”:
val a = "hello"
val b = StringBuilder("hel").append("lo").toString()
a == b // true — same contents
a === b // false — different objects
So the Java string bug simply can’t happen. == on strings compares characters, because == is the .equals() call. When you genuinely care about object identity — usually only when checking against a singleton or for caching — you reach for ===.
There’s a null-safety bonus, too: == handles null on both sides without throwing, so a == b is safe even when either is null. No more Objects.equals(a, b) wrapper.
Defining equality for your own types
== is only as good as the equals it calls, and for your own classes there’s a second obligation that trips people: if you override equals, you must override hashCode to match. Hash-based collections find the right bucket by hash first and only then check equality, so break the pairing and equal objects vanish:
class Bad(val id: Int) {
override fun equals(other: Any?) = other is Bad && other.id == id
// no hashCode!
}
Bad(1) in hashSetOf(Bad(1)) // false — equal objects, different default hashes
The contract is simple — equal objects must have equal hash codes — and getting it right by hand is the boilerplate data class exists to kill. It generates both, in sync, from the primary-constructor properties:
data class Point(val x: Int, val y: Int)
Point(1, 2) == Point(1, 2) // true — generated equals compares fields
Ordering: Comparable and the comparison operators
Equality answers “are these the same?” Ordering answers “which comes first?” Kotlin wires the <, >, <=, and >= operators directly to the Comparable interface: implement compareTo, and the operators work on your type:
class Version(val major: Int, val minor: Int) : Comparable<Version> {
override fun compareTo(other: Version) =
compareValuesBy(this, other, { it.major }, { it.minor })
}
Version(2, 1) > Version(2, 0) // true
compareValuesBy is a standard-library helper that compares by each selector in turn — major first, and minor only as a tiebreaker — so you don’t hand-roll the cascade.
For sorting collections you usually don’t need Comparable at all; sortedBy and compareBy take the selector inline:
people.sortedBy { it.age }
people.sortedWith(compareBy({ it.lastName }, { it.firstName }))
When you actually want ===
The rare time === earns its keep is confirming that a “copy” really did allocate something new:
val a = listOf(1, 2, 3)
val b = a.toList()
a == b // true — same contents
a === b // false — toList() made a fresh list
A worked example
A small value type that’s both comparable and value-equal — the common shape for things like versions or money:
data class SemVer(val major: Int, val minor: Int, val patch: Int) : Comparable<SemVer> {
override fun compareTo(other: SemVer) =
compareValuesBy(this, other, { it.major }, { it.minor }, { it.patch })
}
SemVer(1, 2, 0) == SemVer(1, 2, 0) // true — generated equals
SemVer(1, 3, 0) > SemVer(1, 2, 9) // true — compareTo wired to >
listOf(SemVer(1, 2, 0), SemVer(1, 0, 5)).sorted() // ordering for free
data class gives value equality; Comparable gives ordering; together they make the type behave like one of the built-ins.
Final thoughts
The whole topic comes down to one swapped default: == means value, === means identity, and the Java footgun is gone. Pair that with data class generating a correct equals/hashCode for free, and the comparison operators delegating to Comparable, and equality stops being a thing you get subtly wrong — it’s just an operator that means what it says.
Next, a small syntax feature that leans on the same data class machinery: destructuring, pulling several values out of an object in one line.
Practice: reinforce this with the companion workbook — short, click-to-reveal problems.