skip to content

Pair and Triple are data classes — what does that give you for equality, hashing, and copying, and what are the implications for using them as map keys?

level: seniorimportance: should knowfreq 40%

answer

  1. data class → structural equals/hashCode/copy/componentN
  2. Valid HashMap/HashSet composite keys
  3. Contained values need stable hashCode
  4. copy() is shallow
  5. Prefer named data class for real keys

basics

~20 s

Because Pair and Triple are data classes, two pairs with equal contents are equal and hash the same, so they work well as map keys or set elements. You can also copy() one while changing a field.

solid answer

~30 s

Pair/Triple being data classes means the compiler generates structural `equals`/`hashCode` based on all components, `toString`, `copy`, and `componentN`. So `(1 to 2) == (1 to 2)` is true and they hash identically, making them valid `HashMap`/`HashSet` keys — composite-key lookups just work. The catch: the contained values must themselves have correct equals/hashCode (data classes or immutables). Mutable elements (e.g., an ArrayList inside a Pair) break the hashCode contract if mutated after insertion. `copy()` does a shallow copy — `pair.copy(second = 9)` reuses the same `first` reference. Prefer a named data class as a key for readability and to avoid positional mistakes.

code

kotlin · 5 lines
kotlin
val grid = hashMapOf((0 to 0) to "origin")
println(grid[0 to 0])          // origin — structural equality

val p = listOf(1) to "x"
val q = p.copy(second = "y")   // shallow: q.first === p.first

go deeper

for a junior

Knows two equal-content pairs are equal and can be used in a set/map.

for a middle

Explains structural equals/hashCode and copy come from being a data class.

for a senior

Articulates the stable-hashCode requirement for keys, the shallow nature of copy, and the mutable-element foot-gun.

for a principal

Recommends named data classes for composite keys and reasons about contract guarantees, immutability, and API readability trade-offs.

## What 'data class' generates For a `data class`, the Kotlin compiler synthesizes, based on the **primary-constructor properties**: - `equals(other)` — **structural** equality comparing every component with `==`. - `hashCode()` — combines each component's hashCode. - `toString()` — e.g. `(1, 2)` / `(1, 2, 3)`. - `copy(...)` — returns a new instance with optionally-overridden fields. - `componentN()` — for destructuring. For `Pair`/`Triple` the components are `first`/`second`(`/third`), so all of the above key off those values. ## Equality and hashing ```kotlin (1 to 2) == (1 to 2) // true (structural) (1 to 2).hashCode() == (1 to 2).hashCode() // true ``` This is exactly the **equals/hashCode contract** that `HashMap`/`HashSet` require, so a Pair/Triple is a valid **composite key**: ```kotlin val grid = hashMapOf((0 to 0) to "origin") grid[0 to 0] // "origin" — a freshly built equal Pair finds it ``` ## The mutability caveat Structural equality is only as reliable as the contained elements. The rule: anything used as a hash key must have a **stable** hashCode for as long as it's in the map. ```kotlin val key = mutableListOf(1) to "v" val m = hashMapOf(key to 1) key.first.add(2) // mutated after insertion m[key] // may be null — hashCode changed, bucket lost ``` So only put **immutable / value-like** elements (Int, String, other data classes) inside tuple keys. ## copy() is shallow ```kotlin val p = listOf(1) to "x" val q = p.copy(second = "y") // q.first IS p.first (same reference) ``` `copy` clones the top level only; nested references are shared. Fine for immutable contents, a foot-gun for mutable ones. ## Design guidance Named data classes (`data class Cell(val row: Int, val col: Int)`) are usually a better composite key than `Pair<Int, Int>`: they're self-documenting, prevent swapping the two ints, and give type-checking that distinguishes `Cell` from an unrelated `Pair<Int, Int>`.

  • Why might (mutableListOf(1) to "a") be a dangerous map key?
    Mutating the list after insertion changes the Pair's hashCode, violating the map contract and losing the entry.
  • Is copy() deep or shallow?
    Shallow — it copies the top-level fields but shares nested object references.
  • Why prefer a named data class over Pair<Int,Int> as a key?
    Self-documenting, prevents swapping the two ints, and the type distinguishes it from unrelated Pair<Int,Int> values.

A Pair key is like a lock keyed to the exact contents — change the contents after locking and your old key no longer fits.

saying these in an interview costs you the question

  • Thinking Pair uses reference equality so equal pairs wouldn't match as keys
  • Believing copy() is a deep copy
  • Using a Pair with mutable contents as a hash key and mutating it
  • Not knowing equals/hashCode are auto-generated from components

context