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.