Explain Kotlin's comparator DSL: compareBy, thenBy, thenByDescending. How do you sort by multiple keys, and how do ascending/descending mix?
answer
- compareBy = primary ascending key
- thenBy = tie-breaker, only when earlier keys equal
- thenByDescending flips ONE level
- Apply with sortedWith / sortWith
- nullsFirst/nullsLast for nullable keys
basics
~10 scompareBy 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 scompareBy { 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 linesdata 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
Can use compareBy and sortedWith for a single key sort.
Builds multi-key comparators with thenBy/thenByDescending and knows each level has its own direction.
Handles null keys with nullsFirst/nullsLast, distinguishes reversed() from per-key descending, reuses comparators as values.
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