skip to content

`Any` declares `equals` and `hashCode`. What is the contract between them, and how does a Kotlin `data class` satisfy it?

level: middleimportance: must knowfreq 75%

answer

  1. equal ⇒ same hashCode (one-way)
  2. Override one ⇒ override both
  3. data class uses constructor props only
  4. == is equals; === is identity
  5. Body properties excluded from equals

basics

~10 s

If two objects are equal by equals, they must return the same hashCode. data class auto-generates both from the constructor properties, so equal data objects always share a hash code.

solid answer

~40 s

`Any.equals(other: Any?)` and `Any.hashCode()` form a contract: (1) `equals` must be reflexive, symmetric, transitive, consistent, and `x.equals(null)` is false; (2) **if `a == b` then `a.hashCode() == b.hashCode()`** (the reverse need not hold — hash collisions are allowed). Default `equals` is referential identity and default `hashCode` is identity-based, so distinct instances differ. A Kotlin `data class` overrides both, deriving them from the **primary-constructor properties only** (properties declared in the class body are excluded). This keeps the contract intact automatically, which is why data classes work correctly as `HashMap`/`HashSet` keys. In Kotlin, `==` calls `equals` (null-safe), while `===` is referential identity. If you override `equals` by hand, you must override `hashCode` too or you break hash-based collections.

code

kotlin · 3 lines
kotlin
data class Money(val cents: Long, val currency: String)
val set = hashSetOf(Money(100, "USD"))
println(set.contains(Money(100, "USD"))) // true: equals+hashCode generated

go deeper

for a junior

States the basic rule: equal objects must have equal hash codes; data class generates both.

for a middle

Lists the full equals contract, the one-way hashCode implication, and the constructor-only data-class rule.

for a senior

Explains failure modes in hash collections, == vs ===, and writes a correct manual override with smart-cast.

for a principal

Discusses immutability for hash keys, collision/distribution quality, and contract stability across evolving schemas/serialization.

## The two methods on `Any` - `equals(other: Any?): Boolean` — logical equality. In Kotlin the `==` operator compiles to a null-safe `equals` call: `a == b` ≈ `a?.equals(b) ?: (b === null)`. - `hashCode(): Int` — an integer bucket used by hash-based collections (`HashMap`, `HashSet`, `LinkedHashMap`). `===` (and `!==`) is **referential** identity and never calls `equals`. ## The contract `equals` must be: - **Reflexive:** `x == x`. - **Symmetric:** `x == y` ⇔ `y == x`. - **Transitive:** `x == y` and `y == z` ⇒ `x == z`. - **Consistent:** repeated calls give the same result if nothing changes. - `x == null` is `false` for non-null `x`. `hashCode` must be: - **Consistent** with itself across calls. - **Agreeing with equals:** `a == b` ⇒ `a.hashCode() == b.hashCode()`. - Unequal objects *may* share a hash code (collisions are legal but should be rare for performance). **The critical rule:** override one, override both. If you override `equals` but not `hashCode`, two equal objects can fall in different buckets and a `HashSet` will store both. ## How `data class` helps ```kotlin data class Point(val x: Int, val y: Int) { var label: String = "" // NOT in equals/hashCode (body property) } val a = Point(1, 2).apply { label = "A" } val b = Point(1, 2).apply { label = "B" } println(a == b) // true — only x,y count println(a.hashCode() == b.hashCode()) // true ``` The compiler generates `equals`, `hashCode`, `toString`, `copy`, and `componentN` from the **primary-constructor `val`/`var` parameters only**. Body-declared properties (`label`) are excluded — a common gotcha. ## Manual override ```kotlin class User(val id: Long) { override fun equals(other: Any?): Boolean = this === other || (other is User && other.id == id) override fun hashCode(): Int = id.hashCode() } ``` Use `is` for the type check (it smart-casts `other` to `User`), and combine fields with `Objects.hash(...)` or `31 * result + field.hashCode()`. ## Why it matters Breaking the contract silently corrupts `HashMap`/`HashSet` behavior: lost lookups, duplicate entries, leaks. This is one of the most common real-world Kotlin/Java bugs.

  • Does `a.hashCode() == b.hashCode()` imply `a == b`?
    No. The implication only runs one way. Equal objects must share a hash code, but two unequal objects may collide on the same hash code.
  • If a `data class` has a property in the body (not the constructor), is it used in `equals`?
    No. Only primary-constructor properties participate in the generated `equals`/`hashCode`/`toString`/`copy`. Body properties are ignored.

Equal items must land in the same hash bucket, like identical letters going to the same mail slot — different letters may share a slot, but identical ones must never split.

saying these in an interview costs you the question

  • Overriding `equals` without `hashCode`
  • Claiming equal hash codes imply equality
  • Thinking `==` is referential identity in Kotlin
  • Believing data-class body properties count in equality
  • Using mutable fields in `hashCode` for hash-map keys

context