Sitemap
Tung Doan

Tung Doan

I enjoy sharing what I learn about Kotlin, Jetpack, and scalable app architecture with the developer community.

Equality in Kotlin — The Complete, Under-the-Hood Guide

4 min readOct 4, 2025

--

Press enter or click to view image in full size
Equality in Kotlin

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).

equals comes from Any. By default it’s referential; override it to get true content equality. Data classes and value classes override it for you. (You think equals() 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 != NaN
  • 0.0 == -0.0

Use these for numeric code and math APIs.

println(Double.NaN == Double.NaN) // false
println(0.0 == -0.0) // true

B) 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) → true
  • 0.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))) // true

For FP elements, contentEquals uses equals semantics (so NaN == NaN there)

4) Strings, data classes, value classes

  • Strings: == compares characters (content). === can be true due to interning, but don’t rely on it. (General rule; not a special Kotlin exception.)
  • Data classes: equals/hashCode are auto-generated from primary constructor properties; perfect for models and collection keys.
  • Value classes: equality is by the underlying value unless a final parent equals blocks 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

  • == → calls equals (or IEEE for FP) with built-in null safety.
  • === → identity (equivalent to Java’s == on references).
  • Comparison operators (<, >, …) → compareTo.

7) Best practices (the checklist)

  1. Use data/value classes for models → structural equality & stable hashes out of the box.
  2. If you override equals, also override hashCode (collections depend on it).
  3. Prefer == over === unless you really need identity (singletons, sentinels).
  4. Floating-point: understand the two modes (§2). Use compareTo/sorting for total order.
  5. Arrays: always use contentEquals / contentDeepEquals (+ matching hash/print helpers).
  6. 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 content

Sources (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 + hashCode contracts. (Kotlin)

Bottom line:

  • == means value, === means identity.
  • FP has two behaviors depending on type context.
  • Arrays need content helpers.
  • Always keep equals and hashCode in 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 🚀

--

--

Written by Tung Doan

I enjoy sharing what I learn about Kotlin, Jetpack, and scalable app architecture with the developer community.