skip to content

Show how property references combine with collection operators (sortedBy, groupBy, associateBy, maxByOrNull) and explain what happens at the type level.

level: middleimportance: should knowfreq 38%

answer

  1. Selectors are (T) -> K; Person::age fits
  2. groupBy -> Map<K, List<T>>, associateBy -> Map<K, T>
  3. Key type inferred from the property's return type
  4. compareBy(Person::city, Person::age) for multi-key sort
  5. sortedBy needs Comparable; else sortedWith

basics

~10 s

Many collection functions take a 'selector' function that returns a value per item. A property reference like Person::age fits perfectly: people.sortedBy(Person::age) sorts by age, people.groupBy(Person::city) groups by city.

solid answer

~30 s

Collection operators such as `sortedBy`, `maxByOrNull`, `minByOrNull`, `groupBy`, `associateBy`, `distinctBy`, and `sumOf` take a **selector** `(T) -> K`. Because `Person::age` is a `KProperty1<Person, Int>` that behaves as `(Person) -> Int`, it drops straight in: `people.sortedBy(Person::age)`. `groupBy(Person::city)` yields `Map<String, List<Person>>`; `associateBy(Person::id)` yields `Map<Id, Person>`. The compiler infers the key type from the property's return type, keeping everything type-safe with no lambda boilerplate. For `Comparable` keys, `sortedBy`/`maxByOrNull` work directly; otherwise use `sortedWith(compareBy(Person::age))` to combine multiple selectors.

code

kotlin · 4 lines
kotlin
data class P(val city: String, val age: Int)
val ps = listOf(P("NY", 30), P("LA", 20))
val ordered = ps.sortedWith(compareBy(P::city, P::age))
val byCity = ps.groupBy(P::city) // Map<String, List<P>>

go deeper

for a junior

Can use people.sortedBy(Person::age) and people.map(Person::name).

for a middle

Knows return types of groupBy/associateBy and the inferred key type.

for a senior

Combines references with compareBy for multi-key sorts and handles Comparable/nullable rules.

for a principal

Designs key-extraction APIs around references for type-safe, refactor-proof pipelines.

## Selectors and references Most Kotlin **collection** transforms accept a **selector**: a function `(T) -> K` that extracts a key or value from each element. A property reference is exactly such a function, so it slots in with no lambda. ```kotlin data class Person(val id: Int, val name: String, val age: Int, val city: String) val people: List<Person> = load() val byAge = people.sortedBy(Person::age) // List<Person>, ascending age val oldest = people.maxByOrNull(Person::age) // Person? val byCity = people.groupBy(Person::city) // Map<String, List<Person>> val byId = people.associateBy(Person::id) // Map<Int, Person> val names = people.map(Person::name) // List<String> val ages = people.sumOf(Person::age) // Int val cities = people.distinctBy(Person::city) // unique-by-city ``` ## What happens at the type level - `Person::age` has type `KProperty1<Person, Int>`, which is assignable to `(Person) -> Int`. - `groupBy(keySelector: (T) -> K)` infers `K = String` from `Person::city`, producing `Map<String, List<Person>>`. - `associateBy(keySelector: (T) -> K)` infers `K = Int`, producing `Map<Int, Person>` (later duplicates overwrite earlier ones). - `sortedBy`/`maxByOrNull` require the selector's result to be `Comparable`; `Int`, `String`, etc. satisfy this. ## Multiple keys A single property reference is one key. For composite ordering use **`compareBy`** with several references: ```kotlin val ordered = people.sortedWith(compareBy(Person::city, Person::age)) ``` `compareBy(vararg selectors)` accepts references directly and chains them. ## Why this is idiomatic - **Type-safe**: the key type flows from the property's declared type; rename the property and the compiler flags every use. - **Concise**: `Person::age` reads as a field name, not as `{ it.age }`. - **Composable**: references combine with `compareBy`, `thenBy`, and standard operators. ## Gotcha `sortedBy` needs a `Comparable` selector result. If you select a non-comparable type, use `sortedWith` and supply a `Comparator`. For nullable selector results, the standard ordering puts `null` first (`nullsFirst`) unless you specify otherwise.

  • What type does people.associateBy(Person::id) return, and what about duplicate ids?
    Map<Int, Person>. With duplicate keys, the last element with that id wins, overwriting earlier ones.
  • How do you sort by two properties using references?
    Use sortedWith(compareBy(Person::city, Person::age)); compareBy chains the selectors left to right.

saying these in an interview costs you the question

  • Thinking associateBy keeps all duplicates (it overwrites)
  • Believing sortedBy works on non-Comparable selector results
  • Saying you must write { it.age } because references don't fit selectors
  • Not knowing groupBy returns Map<K, List<T>> vs associateBy Map<K, T>

context