skip to content

How do you build a multi-key Comparator in Kotlin using compareBy, thenBy, and thenByDescending? Show ascending-then-descending ordering.

level: middleimportance: must knowfreq 75%

answer

  1. compareBy = primary key; thenBy / thenByDescending = tiebreakers
  2. then* only runs when earlier keys tie
  3. Apply with sortedWith (copy) or sortWith (in place)
  4. compareByDescending starts descending
  5. reversed() flips all keys; thenByDescending flips one

basics

~10 s

Use compareBy to pick the first sort key, then chain thenBy for the next key and thenByDescending to flip a key's direction. Pass the result to sortedWith to sort by several fields in order.

solid answer

~40 s

Kotlin's stdlib gives factory functions that build a Comparator from selector lambdas. compareBy { it.a } creates a comparator ordering by a ascending. You chain refinements with thenBy { it.b } (next tiebreaker, ascending) and thenByDescending { it.c } (descending tiebreaker). Each link only matters when the previous keys tie. You apply it with list.sortedWith(comparator) or sortWith for in-place. compareByDescending starts descending. For custom element comparison you can pass a comparator to a selector, e.g. compareBy(String.CASE_INSENSITIVE_ORDER) { it.name }. Under the hood these use compareValuesBy, which is null-safe and overflow-safe. This composition is preferred over a hand-written compareTo with branchy if-logic because it is declarative, readable, and stable.

code

kotlin · 8 lines
kotlin
data class Task(val priority: Int, val name: String)

val order = compareByDescending<Task> { it.priority }   // high priority first
    .thenBy { it.name }                                   // then name A->Z

val tasks = listOf(Task(1, "b"), Task(2, "a"), Task(1, "a"))
println(tasks.sortedWith(order))
// [Task(2,a), Task(1,a), Task(1,b)]

go deeper

for a junior

Can use compareBy to sort by a single field and knows sortedWith applies a comparator.

for a middle

Chains thenBy/thenByDescending correctly for multi-key order and explains then* only breaks ties.

for a senior

Distinguishes reversed() vs thenByDescending, leverages stable sort, and passes custom key comparators like CASE_INSENSITIVE_ORDER.

for a principal

Reasons about comparator composition as reusable, testable ordering policy and its performance/stability guarantees across List and Sequence pipelines.

## The builder functions Kotlin's standard library provides composable `Comparator` factories so you rarely hand-write comparison logic: - `compareBy { selector }` — first/primary key, **ascending** - `compareByDescending { selector }` — first key, **descending** - `comparator.thenBy { selector }` — tiebreaker, ascending - `comparator.thenByDescending { selector }` — tiebreaker, descending - `comparator.thenComparator(other)` — chain an arbitrary Comparator Each `then*` link is consulted **only when all earlier keys compare equal**. This produces a lexicographic, multi-key ordering. ## Ascending-then-descending example Sort people by `lastName` ascending, then by `age` descending (oldest first within a name): ```kotlin data class Person(val lastName: String, val age: Int) val byNameThenAgeDesc = compareBy<Person> { it.lastName } .thenByDescending { it.age } val sorted = people.sortedWith(byNameThenAgeDesc) ``` `compareBy<Person>` needs the explicit type argument when the receiver list type cannot be inferred at that point. ## Applying a comparator - `list.sortedWith(comparator)` -> new sorted `List` (non-mutating) - `mutableList.sortWith(comparator)` -> sorts in place - `sequence.sortedWith(comparator)` -> works on sequences too - `maxWith` / `minWith` -> pick extremes by a Comparator ## Custom element comparison in a key You can give a selector its own Comparator: ```kotlin val ci = compareBy<Person>(String.CASE_INSENSITIVE_ORDER) { it.lastName } ``` Here `String.CASE_INSENSITIVE_ORDER` is a stdlib `Comparator<String>` used to compare the selected key. ## Why prefer this over manual compareTo - **Declarative**: reads like the spec ("by name, then by age desc"). - **Safe**: built on `compareValuesBy`, avoiding overflow from `a - b`. - **Stable**: Kotlin/JVM sorts are stable, so equal-key elements keep input order — important when you only sort by some keys. ## Reversing a whole comparator `comparator.reversed()` flips the entire ordering (every key), distinct from `thenByDescending`, which flips only one key.

  • What's the difference between comparator.reversed() and thenByDescending?
    reversed() inverts the entire ordering (all keys). thenByDescending only makes that one tiebreaker key descending while leaving earlier keys as-is.
  • Why does sortedWith keep equal elements in input order?
    Kotlin/JVM uses a stable sort, so elements that compare equal under the comparator retain their relative original order.

saying these in an interview costs you the question

  • Thinking thenBy re-sorts everything instead of acting only on ties
  • Using reversed() when they meant to flip just one key
  • Calling sort() expecting a return value (it mutates, returns Unit)
  • Hand-writing nested if/else compareTo instead of composing builders
  • Forgetting the explicit type param compareBy<Person> { ... } when inference fails

context