skip to content

How does `compareTo` back the comparison operators, and what conventions/contracts must operator functions like `compareTo`, `equals`, and `plusAssign` honor?

level: seniorimportance: should knowfreq 42%

answer

  1. `<`,`>`,`<=`,`>=` all use compareTo (Int)
  2. neg<0 / zero=eq / pos>0
  3. `==` is null-safe: `a?.equals(b) ?: (b===null)`
  4. `+=`: plusAssign (mutate) vs plus (reassign)
  5. compareTo consistent with equals/hashCode

basics

~20 s

Operators <, >, <=, >= all call one function: compareTo, which returns an Int. Negative means less, zero means equal, positive means greater. Operator functions must obey their expected contracts so comparisons and equality behave consistently.

solid answer

~50 s

The four relational operators all desugar through `compareTo`: `a < b` -> `a.compareTo(b) < 0`, `a > b` -> `a.compareTo(b) > 0`, `a <= b` -> `<= 0`, `a >= b` -> `>= 0`. `compareTo` must return `Int` and is provided by implementing `Comparable<T>` (whose method is already marked `operator`). Conventions matter: `compareTo` must be a consistent total order (anti-symmetric, transitive) and ideally consistent with `equals`; `equals` (backing `==`, with null handling) must be reflexive/symmetric/transitive and paired with `hashCode`. The `==` operator is special: `a == b` -> `a?.equals(b) ?: (b === null)`, so it's null-safe and not just `equals`. Augmented assignments (`+=`) prefer `plusAssign` (returns Unit, mutates) over `plus` when applicable; defining both for a mutable type causes ambiguity. Unary `++`/`--` use `inc`/`dec` which must return the type. Following these contracts keeps sorting, sets, and collections correct.

code

kotlin · 10 lines
kotlin
data class Temp(val c: Double) : Comparable<Temp> {
    override fun compareTo(other: Temp) = c.compareTo(other.c)
}
val a = Temp(10.0); val b = Temp(20.0)
println(a < b)   // a.compareTo(b) < 0 -> true
println(a >= b)  // a.compareTo(b) >= 0 -> false

// += ambiguity demo (DON'T do both on a mutable type):
val list = mutableListOf(1)
list += 2 // MutableList defines plusAssign -> in-place add

go deeper

for a junior

Knows </> compare and that compareTo returns negative/zero/positive.

for a middle

Implements Comparable.compareTo and knows all four operators route through it; uses compareValuesBy.

for a senior

Explains the null-safe == desugaring, plus vs plusAssign, and the equals/hashCode/compareTo consistency contracts.

for a principal

Owns the team's value-type contracts, catches contract violations that corrupt sorted/hashed collections, and reasons about immutable vs mutable operator design.

## Comparison operators -> `compareTo` All four relational operators route through a **single** function, `compareTo`, which returns an `Int`: - `a < b` -> `a.compareTo(b) < 0` - `a > b` -> `a.compareTo(b) > 0` - `a <= b` -> `a.compareTo(b) <= 0` - `a >= b` -> `a.compareTo(b) >= 0` The return convention: **negative** = `a` is less than `b`, **zero** = equal ordering, **positive** = greater. You normally get `compareTo` by implementing `Comparable<T>`; its `compareTo` is already declared `operator`, so you don't add the modifier yourself. ```kotlin class Version(val major: Int, val minor: Int) : Comparable<Version> { override fun compareTo(other: Version): Int = compareValuesBy(this, other, Version::major, Version::minor) } println(Version(1, 2) < Version(1, 5)) // compareTo(...) < 0 -> true ``` ## The `==` operator is NOT a plain `equals` call Structural equality is special-cased for null safety: - `a == b` -> `a?.equals(b) ?: (b === null)` - `a != b` -> `!(a == b)` So `==` never throws an NPE even if `a` is null. This is distinct from `===` (referential identity), which is **not** overloadable. ## Contracts you must honor Operator functions encode semantic promises the rest of the library relies on: - **`compareTo`**: defines a consistent **total order** — anti-symmetric (`a<b` implies `!(b<a)`), transitive, and should be **consistent with `equals`** (compareTo == 0 iff equals true) so `TreeSet`/`sortedSet` behave predictably. - **`equals`**: reflexive, symmetric, transitive, consistent; **must be paired with `hashCode`** (equal objects -> equal hash codes) or hash-based collections break. - **`hashCode`** isn't an operator but is part of the equality contract. ## Augmented assignment: `plus` vs `plusAssign` `a += b` has two possible desugarings: - If an applicable `operator fun plusAssign(b)` (returns `Unit`) exists -> `a.plusAssign(b)` (in-place mutation). - Otherwise -> `a = a.plus(b)` (reassign with a new value; requires `a` to be a `var`). For a **mutable** type, defining **both** `plus` and `plusAssign` makes `+=` **ambiguous** and is a compile error. Convention: immutable types use `plus`; mutable collections use `plusAssign`. ## Unary and increment operators - `+a` -> `a.unaryPlus()`, `-a` -> `a.unaryMinus()`, `!a` -> `a.not()`. - `a++`/`++a` -> `a.inc()`, `a--`/`--a` -> `a.dec()`; `inc`/`dec` must **return the receiver's type** and don't mutate — the compiler reassigns the variable (so the variable must be a `var`). ## Why conventions matter Operators are sugar, but `sorted()`, `max()`, `TreeMap`, `HashSet`, and `==` checks all assume the contracts hold. Breaking anti-symmetry or `equals`/`hashCode` consistency yields subtle, data-dependent bugs (lost set elements, wrong sort order). Implementing operators is therefore as much about honoring contracts as about syntax.

  • Is `a == b` exactly the same as `a.equals(b)`?
    No. `==` desugars to `a?.equals(b) ?: (b === null)`, making it null-safe. Calling `a.equals(b)` directly would NPE if `a` is null.
  • What happens if a mutable type defines both `plus` and `plusAssign`?
    `a += b` becomes ambiguous and fails to compile, because both desugarings are applicable. Pick one — `plusAssign` for in-place mutation, `plus` for immutable reassignment.
  • Can you overload `===`?
    No. Referential identity `===`/`!==` is not overloadable; only structural `==`/`!=` (via `equals`) is.

compareTo is one referee returning a signed score; all four comparison operators just read the sign of that single score.

saying these in an interview costs you the question

  • Implementing separate `<` and `>` functions instead of one `compareTo`
  • Saying `==` just calls `equals` (ignoring null-safe desugaring)
  • Overriding `equals` without `hashCode`
  • Defining both `plus` and `plusAssign` on a mutable type
  • Thinking `===` can be overloaded

context