skip to content

Explain Kotlin's comparator DSL: compareBy, thenBy, thenByDescending. How do you sort by multiple keys, and how do ascending/descending mix?

level: middleimportance: must knowfreq 70%

answer

  1. compareBy = primary ascending key
  2. thenBy = tie-breaker, only when earlier keys equal
  3. thenByDescending flips ONE level
  4. Apply with sortedWith / sortWith
  5. nullsFirst/nullsLast for nullable keys

basics

~10 s

compareBy builds a comparator from a property. thenBy adds a tie-breaker used only when the previous keys are equal. Use thenByDescending to reverse one of those keys. Pass the result to sortedWith.

solid answer

~40 s

compareBy { selector } creates a Comparator<T> ordering ascending by the extracted key. Chaining .thenBy { selector } adds secondary keys: each is consulted only when all earlier keys compared equal, giving multi-level sorting. .thenByDescending { } flips one level to descending while leaving others ascending — this per-key direction is the key advantage over reversing the whole comparator. compareByDescending starts descending. You apply the comparator with list.sortedWith(comparator) (or MutableList.sortWith for in-place). compareBy also has a vararg form: compareBy({ it.last }, { it.first }) for several ascending keys at once. For nullable keys, nullsFirst()/nullsLast() wrap a comparator to define where nulls land. The comparators are stable and composable, so you can store and reuse them.

code

kotlin · 11 lines
kotlin
data class Emp(val dept: String, val name: String, val salary: Int)

val cmp = compareBy<Emp> { it.dept }
    .thenByDescending { it.salary }
    .thenBy { it.name }

val staff = listOf(
    Emp("Eng", "Zed", 100), Emp("Eng", "Amy", 100), Emp("Eng", "Bo", 120)
)
println(staff.sortedWith(cmp))
// Eng/120 Bo, then Eng/100 Amy, then Eng/100 Zed (name breaks the salary tie)

go deeper

for a junior

Can use compareBy and sortedWith for a single key sort.

for a middle

Builds multi-key comparators with thenBy/thenByDescending and knows each level has its own direction.

for a senior

Handles null keys with nullsFirst/nullsLast, distinguishes reversed() from per-key descending, reuses comparators as values.

for a principal

Designs stable, composable comparator APIs, reasons about correctness of comparator contracts (transitivity/consistency) and the cost of selector re-evaluation across levels.

## What a Comparator is A `Comparator<T>` is an object with `compare(a, b)` returning a negative number, zero, or positive number when `a` is less than, equal to, or greater than `b`. Kotlin's stdlib gives a **DSL** to build comparators declaratively instead of writing `compare` by hand. ## compareBy — the entry point `compareBy { it.age }` returns a `Comparator<T>` that orders **ascending** by the selector's `Comparable` result. A vararg overload takes several selectors evaluated in order: ```kotlin val byLastThenFirst = compareBy<Person>({ it.last }, { it.first }) ``` `compareByDescending { it.age }` starts in descending order. ## thenBy / thenByDescending — tie-breakers Chaining adds **secondary keys** consulted only when every earlier key compares equal: ```kotlin val cmp = compareBy<Person> { it.last } .thenBy { it.first } .thenByDescending { it.age } ``` This sorts by last name ascending; for equal last names, by first name ascending; and for equal last+first, by age **descending**. The crucial point: **each level has its own direction** — you cannot get that by reversing the whole comparator. ## thenComparator and comparator argument `thenBy` / `thenByDescending` also accept an explicit `Comparator` for the key (e.g. case-insensitive `String.CASE_INSENSITIVE_ORDER`), and there is `then(otherComparator)` / `thenComparator { a, b -> ... }` for fully custom tie-breaks. ## Applying the comparator ```kotlin val sorted = people.sortedWith(cmp) // new list (read-only safe) val mutable = people.toMutableList() mutable.sortWith(cmp) // in-place, returns Unit ``` ## Nulls If a selector can return null, wrap with `nullsFirst()` / `nullsLast()`: ```kotlin val cmp = compareBy<Person>(nullsLast()) { it.nickname } ``` Without this, a null key would throw an NPE during comparison. ## Reversing `comparator.reversed()` flips the **entire** ordering, including all tie-break levels — useful when you genuinely want the full inverse, distinct from per-key `thenByDescending`.

  • Difference between thenByDescending and calling reversed() on the whole comparator?
    thenByDescending flips only that one key's direction; reversed() inverts the entire ordering including every tie-break level.
  • How do you avoid an NPE when a sort key can be null?
    Wrap with nullsFirst()/nullsLast(), e.g. compareBy(nullsLast()) { it.nickname }, which defines where nulls are placed.
  • Can compareBy take more than one selector directly?
    Yes — compareBy(sel1, sel2, ...) is a vararg overload that builds an ascending multi-key comparator equivalent to compareBy(sel1).thenBy(sel2).

saying these in an interview costs you the question

  • Thinking thenBy applies even when the primary key differs
  • Believing reversed() and thenByDescending are interchangeable
  • Forgetting nullsFirst/nullsLast and getting NPEs on null keys
  • Trying to chain compareBy onto sortedBy instead of sortedWith

context