Equality in Kotlin — The Complete, Under-the-Hood Guide
Kotlin splits equality into value and identity, and adds special rules for floating-point numbers and arrays. Here’s the deep dive you can actually rely on.
1) Two kinds of equality
Structural equality (== / !=)
Desugars to: a?.equals(b) ?: (b === null)
- (null-safe call to
equals, or a null identity check).
equalscomes fromAny. By default it’s referential; override it to get true content equality. Data classes and value classes override it for you. (You thinkequals()only compares values, right? 😂)
// Default == calls equals(); write equals/hashCode for custom types
class Point(val x: Int, val y: Int) {
override fun equals(other: Any?): Boolean =
other is Point && x == other.x && y == other.y
override fun hashCode(): Int = 31 * x + y
}Contract: equals must be reflexive, symmetric, transitive, consistent, and never equal to null. If you override it, also override hashCode.
Referential equality — === / !==
True if two references point to the same instance.
Get Tung Doan’s stories in your inbox
Join Medium for free to get updates from this writer.
On the JVM, for values represented as primitives at runtime (e.g., Int), === behaves like == (but don’t use identity on boxable numbers).
2) Floating-point rules (the gotchas)
Kotlin deliberately supports two semantics:
A) When operands are statically Float/Double (or nullable variants)
Follows IEEE-754:
NaN != NaN0.0 == -0.0
Use these for numeric code and math APIs.
println(Double.NaN == Double.NaN) // false
println(0.0 == -0.0) // trueB) When operands are not statically FP (e.g., inside Any, generics, collections)
Uses Double.equals/Float.equals & compareTo to get total ordering:
NaN.equals(NaN)→ true0.0.equals(0.0)→ false
Ordering treats 0.0 < 0.0, and NaN is greater than everything.
println(listOf(Double.NaN) == listOf(Double.NaN)) // true
println(listOf(0.0) == listOf(-0.0)) // false
println(listOf(Double.NaN, Double.POSITIVE_INFINITY, 0.0, -0.0).sorted())
// [-0.0, 0.0, Infinity, NaN]Practical tips
- Prefer
compareTo/sorting semantics when you need consistent ordering (maps/sets). - For numeric algorithms, avoid exact equality; compare with an epsilon unless you specifically want IEEE behavior.
3) Arrays never use == for content
Array and primitive arrays (IntArray, …) don’t override equals; == is reference comparison. Use the content helpers:
- Shallow content:
contentEquals - Deep content (nested arrays):
contentDeepEquals - Matching hash/toString:
contentHashCode,contentDeepHashCode,contentToString,contentDeepToString
val a1 = arrayOf(1, 2, 3)
val a2 = arrayOf(1, 2, 3)
println(a1 == a2) // false (refs)
println(a1.contentEquals(a2)) // true
println(arrayOf(a1).contentDeepEquals(arrayOf(a2))) // trueFor FP elements, contentEquals uses equals semantics (so NaN == NaN there)
4) Strings, data classes, value classes
- Strings:
==compares characters (content).===can betruedue to interning, but don’t rely on it. (General rule; not a special Kotlin exception.) - Data classes:
equals/hashCodeare auto-generated from primary constructor properties; perfect for models and collection keys. - Value classes: equality is by the underlying value unless a
finalparentequalsblocks override.
5) Numbers on the JVM: boxing & identity traps
On the JVM:
- Non-nullable numeric types compile to primitives; nullable/generic positions box them.
- Small boxed integers (−128..127) can be cached, so identity checks may appear “random”:
val a: Int? = 100; val b: Int? = 100; a === b // true (cache)val x: Int? = 10000; val y: Int? = 10000; x === y // false- Kotlin warns against identity checks on these types — use
==instead.
6) Interop & operator desugaring
==→ callsequals(or IEEE for FP) with built-in null safety.===→ identity (equivalent to Java’s==on references).- Comparison operators (
<,>, …) →compareTo.
7) Best practices (the checklist)
- Use data/value classes for models → structural equality & stable hashes out of the box.
- If you override
equals, also overridehashCode(collections depend on it). - Prefer
==over===unless you really need identity (singletons, sentinels). - Floating-point: understand the two modes (§2). Use
compareTo/sorting for total order. - Arrays: always use
contentEquals/contentDeepEquals(+ matching hash/print helpers). - Collections already do content equality; don’t convert lists to arrays just to compare.
8) Pocket reference (copy/paste)
// Null-safe structural equality
a == b // a?.equals(b) ?: (b === null)
// Identity
a === b // same instance
// Floats & Doubles
Double.NaN == Double.NaN // false (IEEE mode)
listOf(Double.NaN) == listOf(Double.NaN) // true (generic mode)
0.0 == -0.0 // true (IEEE)
listOf(0.0) == listOf(-0.0) // false (generic mode)
// Arrays
arr1 == arr2 // reference compare
arr1.contentEquals(arr2) // shallow content
arr1.contentDeepEquals(arr2) // deep content
arr.contentHashCode() // hash by content
arr.contentDeepHashCode() // hash by deep contentSources (official)
- Kotlin docs — Equality (structural vs referential; FP & array sections). (Kotlin)
- Kotlin docs — Numbers → Floating-point numbers comparison (IEEE vs generic mode, examples). (Kotlin)
- Kotlin docs — Arrays (use
contentEquals/contentDeepEquals). (Kotlin) - Kotlin stdlib refs —
contentEquals,contentDeepEquals. (Kotlin) - Kotlin stdlib —
Any.equals+hashCodecontracts. (Kotlin)
Bottom line:
==means value,===means identity.- FP has two behaviors depending on type context.
- Arrays need content helpers.
- Always keep
equalsandhashCodein sync.
👉 If this guide clarified Kotlin’s equality for you, drop a CLAP 👏 (or a few 👏) so more developers can find it.
Follow me on Medium for more deep dives into Kotlin, Android, and modern software design.
Let’s learn and build better code together 🚀










